From 75823d580b0f6d7222b979cdcf29eaf1d97a7158 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20S=C3=A1nchez=20S=C3=A1nchez?= Date: Thu, 27 Mar 2025 03:28:08 +0100 Subject: [PATCH] community: fix perplexity response parameters not being included in model response (#30440) This pull request includes enhancements to the `perplexity.py` file in the `chat_models` module, focusing on improving the handling of additional keyword arguments (`additional_kwargs`) in message processing methods. Additionally, new unit tests have been added to ensure the correct inclusion of citations, images, and related questions in the `additional_kwargs`. Issue: resolves https://github.com/langchain-ai/langchain/issues/30439 Enhancements to `perplexity.py`: * [`libs/community/langchain_community/chat_models/perplexity.py`](diffhunk://#diff-d3e4d7b277608683913b53dcfdbd006f0f4a94d110d8b9ac7acf855f1f22207fL208-L212): Modified the `_convert_delta_to_message_chunk`, `_stream`, and `_generate` methods to handle `additional_kwargs`, which include citations, images, and related questions. [[1]](diffhunk://#diff-d3e4d7b277608683913b53dcfdbd006f0f4a94d110d8b9ac7acf855f1f22207fL208-L212) [[2]](diffhunk://#diff-d3e4d7b277608683913b53dcfdbd006f0f4a94d110d8b9ac7acf855f1f22207fL277-L286) [[3]](diffhunk://#diff-d3e4d7b277608683913b53dcfdbd006f0f4a94d110d8b9ac7acf855f1f22207fR324-R331) New unit tests: * [`libs/community/tests/unit_tests/chat_models/test_perplexity.py`](diffhunk://#diff-dab956d79bd7d17a0f5dea3f38ceab0d583b43b63eb1b29138ee9b6b271ba1d9R119-R275): Added new tests `test_perplexity_stream_includes_citations_and_images` and `test_perplexity_stream_includes_citations_and_related_questions` to verify that the `stream` method correctly includes citations, images, and related questions in the `additional_kwargs`. --- .../chat_models/perplexity.py | 20 ++- .../unit_tests/chat_models/test_perplexity.py | 157 ++++++++++++++++++ libs/community/uv.lock | 6 +- 3 files changed, 178 insertions(+), 5 deletions(-) diff --git a/libs/community/langchain_community/chat_models/perplexity.py b/libs/community/langchain_community/chat_models/perplexity.py index 3ddba312eab..fd9b735f4f2 100644 --- a/libs/community/langchain_community/chat_models/perplexity.py +++ b/libs/community/langchain_community/chat_models/perplexity.py @@ -345,16 +345,25 @@ class ChatPerplexity(BaseChatModel): if len(chunk["choices"]) == 0: continue choice = chunk["choices"][0] - citations = chunk.get("citations", []) + + additional_kwargs = {} + if first_chunk: + additional_kwargs["citations"] = chunk.get("citations", []) + for attr in ["images", "related_questions"]: + if attr in chunk: + additional_kwargs[attr] = chunk[attr] chunk = self._convert_delta_to_message_chunk( choice["delta"], default_chunk_class ) + if isinstance(chunk, AIMessageChunk) and usage_metadata: chunk.usage_metadata = usage_metadata + if first_chunk: - chunk.additional_kwargs |= {"citations": citations} + chunk.additional_kwargs |= additional_kwargs first_chunk = False + finish_reason = choice.get("finish_reason") generation_info = ( dict(finish_reason=finish_reason) if finish_reason is not None else None @@ -386,9 +395,14 @@ class ChatPerplexity(BaseChatModel): else: usage_metadata = None + additional_kwargs = {"citations": response.citations} + for attr in ["images", "related_questions"]: + if hasattr(response, attr): + additional_kwargs[attr] = getattr(response, attr) + message = AIMessage( content=response.choices[0].message.content, - additional_kwargs={"citations": response.citations}, + additional_kwargs=additional_kwargs, usage_metadata=usage_metadata, ) return ChatResult(generations=[ChatGeneration(message=message)]) diff --git a/libs/community/tests/unit_tests/chat_models/test_perplexity.py b/libs/community/tests/unit_tests/chat_models/test_perplexity.py index c5a745cbcea..45cc4ec7cdf 100644 --- a/libs/community/tests/unit_tests/chat_models/test_perplexity.py +++ b/libs/community/tests/unit_tests/chat_models/test_perplexity.py @@ -116,3 +116,160 @@ def test_perplexity_stream_includes_citations(mocker: MockerFixture) -> None: assert full.additional_kwargs == {"citations": ["example.com", "example2.com"]} patcher.assert_called_once() + + +@pytest.mark.requires("openai") +def test_perplexity_stream_includes_citations_and_images(mocker: MockerFixture) -> None: + """Test that the stream method includes citations in the additional_kwargs.""" + llm = ChatPerplexity( + model="test", + timeout=30, + verbose=True, + ) + mock_chunk_0 = { + "choices": [ + { + "delta": { + "content": "Hello ", + }, + "finish_reason": None, + } + ], + "citations": ["example.com", "example2.com"], + "images": [ + { + "image_url": "mock_image_url", + "origin_url": "mock_origin_url", + "height": 100, + "width": 100, + } + ], + } + mock_chunk_1 = { + "choices": [ + { + "delta": { + "content": "Perplexity", + }, + "finish_reason": None, + } + ], + "citations": ["example.com", "example2.com"], + "images": [ + { + "image_url": "mock_image_url", + "origin_url": "mock_origin_url", + "height": 100, + "width": 100, + } + ], + } + mock_chunks: List[Dict[str, Any]] = [mock_chunk_0, mock_chunk_1] + mock_stream = MagicMock() + mock_stream.__iter__.return_value = mock_chunks + patcher = mocker.patch.object( + llm.client.chat.completions, "create", return_value=mock_stream + ) + stream = llm.stream("Hello langchain") + full: Optional[BaseMessageChunk] = None + for i, chunk in enumerate(stream): + full = chunk if full is None else full + chunk + assert chunk.content == mock_chunks[i]["choices"][0]["delta"]["content"] + if i == 0: + assert chunk.additional_kwargs["citations"] == [ + "example.com", + "example2.com", + ] + assert chunk.additional_kwargs["images"] == [ + { + "image_url": "mock_image_url", + "origin_url": "mock_origin_url", + "height": 100, + "width": 100, + } + ] + else: + assert "citations" not in chunk.additional_kwargs + assert "images" not in chunk.additional_kwargs + assert isinstance(full, AIMessageChunk) + assert full.content == "Hello Perplexity" + assert full.additional_kwargs == { + "citations": ["example.com", "example2.com"], + "images": [ + { + "image_url": "mock_image_url", + "origin_url": "mock_origin_url", + "height": 100, + "width": 100, + } + ], + } + + patcher.assert_called_once() + + +@pytest.mark.requires("openai") +def test_perplexity_stream_includes_citations_and_related_questions( + mocker: MockerFixture, +) -> None: + """Test that the stream method includes citations in the additional_kwargs.""" + llm = ChatPerplexity( + model="test", + timeout=30, + verbose=True, + ) + mock_chunk_0 = { + "choices": [ + { + "delta": { + "content": "Hello ", + }, + "finish_reason": None, + } + ], + "citations": ["example.com", "example2.com"], + "related_questions": ["example_question_1", "example_question_2"], + } + mock_chunk_1 = { + "choices": [ + { + "delta": { + "content": "Perplexity", + }, + "finish_reason": None, + } + ], + "citations": ["example.com", "example2.com"], + "related_questions": ["example_question_1", "example_question_2"], + } + mock_chunks: List[Dict[str, Any]] = [mock_chunk_0, mock_chunk_1] + mock_stream = MagicMock() + mock_stream.__iter__.return_value = mock_chunks + patcher = mocker.patch.object( + llm.client.chat.completions, "create", return_value=mock_stream + ) + stream = llm.stream("Hello langchain") + full: Optional[BaseMessageChunk] = None + for i, chunk in enumerate(stream): + full = chunk if full is None else full + chunk + assert chunk.content == mock_chunks[i]["choices"][0]["delta"]["content"] + if i == 0: + assert chunk.additional_kwargs["citations"] == [ + "example.com", + "example2.com", + ] + assert chunk.additional_kwargs["related_questions"] == [ + "example_question_1", + "example_question_2", + ] + else: + assert "citations" not in chunk.additional_kwargs + assert "related_questions" not in chunk.additional_kwargs + assert isinstance(full, AIMessageChunk) + assert full.content == "Hello Perplexity" + assert full.additional_kwargs == { + "citations": ["example.com", "example2.com"], + "related_questions": ["example_question_1", "example_question_2"], + } + + patcher.assert_called_once() diff --git a/libs/community/uv.lock b/libs/community/uv.lock index f5ce918fd7a..91ccbf4f442 100644 --- a/libs/community/uv.lock +++ b/libs/community/uv.lock @@ -1,4 +1,5 @@ version = 1 +revision = 1 requires-python = ">=3.9, <4.0" resolution-markers = [ "python_full_version >= '3.12.4' and platform_python_implementation == 'PyPy'", @@ -1531,6 +1532,7 @@ requires-dist = [ { name = "requests", specifier = ">=2,<3" }, { name = "sqlalchemy", specifier = ">=1.4,<3" }, ] +provides-extras = ["community", "anthropic", "openai", "azure-ai", "cohere", "google-vertexai", "google-genai", "fireworks", "ollama", "together", "mistralai", "huggingface", "groq", "aws", "deepseek", "xai"] [package.metadata.requires-dev] codespell = [{ name = "codespell", specifier = ">=2.2.0,<3.0.0" }] @@ -1745,7 +1747,7 @@ typing = [ [[package]] name = "langchain-core" -version = "0.3.45" +version = "0.3.47" source = { editable = "../core" } dependencies = [ { name = "jsonpatch" }, @@ -1803,7 +1805,7 @@ typing = [ [[package]] name = "langchain-tests" -version = "0.3.14" +version = "0.3.15" source = { editable = "../standard-tests" } dependencies = [ { name = "httpx" },