Skip to content

Commit ea8f2a0

Browse files
inacioneryCopilotmdrxy
authored
feat(perplexity): expose search_results in chat model (#31468)
Description The Perplexity chat model already returns a search_results field, but LangChain dropped it when mapping Perplexity responses to additional_kwargs. This patch adds "search_results" to the allowed attribute lists in both _stream and _generate, so downstream code can access it just like images, citations, or related_questions. Dependencies None. The change is purely internal; no new imports or optional dependencies required. https://community.perplexity.ai/t/new-feature-search-results-field-with-richer-metadata/398 --------- Co-authored-by: Copilot <[email protected]> Co-authored-by: Mason Daugherty <[email protected]>
1 parent 2df05f6 commit ea8f2a0

File tree

3 files changed

+131
-16
lines changed

3 files changed

+131
-16
lines changed

docs/docs/integrations/chat/perplexity.ipynb

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -240,6 +240,65 @@
240240
"response.content"
241241
]
242242
},
243+
{
244+
"cell_type": "markdown",
245+
"id": "382335a6",
246+
"metadata": {},
247+
"source": [
248+
"### Accessing the search results metadata\n",
249+
"\n",
250+
"Perplexity often provides a list of the web pages it consulted (“search_results”).\n",
251+
"You don't need to pass any special parameter — the list is placed in\n",
252+
"`response.additional_kwargs[\"search_results\"]`.\n"
253+
]
254+
},
255+
{
256+
"cell_type": "code",
257+
"execution_count": null,
258+
"id": "2b09214a",
259+
"metadata": {},
260+
"outputs": [
261+
{
262+
"name": "stdout",
263+
"output_type": "stream",
264+
"text": [
265+
"The tallest mountain in South America is Aconcagua. It has a summit elevation of approximately 6,961 meters (22,838 feet), making it not only the highest peak in South America but also the highest mountain in the Americas, the Western Hemisphere, and the Southern Hemisphere[1][2][4].\n",
266+
"\n",
267+
"Aconcagua is located in the Principal Cordillera of the Andes mountain range, in Mendoza Province, Argentina, near the border with Chile[1][2][4]. It is of volcanic origin but is not an active volcano[4]. The mountain is part of Aconcagua Provincial Park and features several glaciers, including the large Ventisquero Horcones Inferior glacier[1].\n",
268+
"\n",
269+
"In summary, Aconcagua stands as the tallest mountain in South America at about 6,961 meters (22,838 feet) in height.\n"
270+
]
271+
},
272+
{
273+
"data": {
274+
"text/plain": [
275+
"[{'title': 'Aconcagua - Wikipedia',\n",
276+
" 'url': 'https://en.wikipedia.org/wiki/Aconcagua',\n",
277+
" 'date': None},\n",
278+
" {'title': 'The 10 Highest Mountains in South America - Much Better Adventures',\n",
279+
" 'url': 'https://www.muchbetteradventures.com/magazine/highest-mountains-south-america/',\n",
280+
" 'date': '2023-07-05'}]"
281+
]
282+
},
283+
"execution_count": null,
284+
"metadata": {},
285+
"output_type": "execute_result"
286+
}
287+
],
288+
"source": [
289+
"chat = ChatPerplexity(temperature=0, model=\"sonar\")\n",
290+
"\n",
291+
"response = chat.invoke(\n",
292+
" \"What is the tallest mountain in South America?\",\n",
293+
")\n",
294+
"\n",
295+
"# Main answer\n",
296+
"print(response.content)\n",
297+
"\n",
298+
"# First two supporting search results\n",
299+
"response.additional_kwargs[\"search_results\"][:2]"
300+
]
301+
},
243302
{
244303
"cell_type": "markdown",
245304
"id": "13d93dc4",

libs/partners/perplexity/langchain_perplexity/chat_models.py

Lines changed: 6 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -74,7 +74,7 @@ class ChatPerplexity(BaseChatModel):
7474
7575
Key init args - completion params:
7676
model: str
77-
Name of the model to use. e.g. "llama-3.1-sonar-small-128k-online"
77+
Name of the model to use. e.g. "sonar"
7878
temperature: float
7979
Sampling temperature to use. Default is 0.7
8080
max_tokens: Optional[int]
@@ -95,11 +95,9 @@ class ChatPerplexity(BaseChatModel):
9595
Instantiate:
9696
.. code-block:: python
9797
98-
from langchain_community.chat_models import ChatPerplexity
98+
from langchain_perplexity import ChatPerplexity
9999
100-
llm = ChatPerplexity(
101-
model="llama-3.1-sonar-small-128k-online", temperature=0.7
102-
)
100+
llm = ChatPerplexity(model="sonar", temperature=0.7)
103101
104102
Invoke:
105103
.. code-block:: python
@@ -147,7 +145,7 @@ class StructuredOutput(BaseModel):
147145
""" # noqa: E501
148146

149147
client: Any = None #: :meta private:
150-
model: str = "llama-3.1-sonar-small-128k-online"
148+
model: str = "sonar"
151149
"""Model name."""
152150
temperature: float = 0.7
153151
"""What sampling temperature to use."""
@@ -325,7 +323,7 @@ def _stream(
325323
additional_kwargs = {}
326324
if first_chunk:
327325
additional_kwargs["citations"] = chunk.get("citations", [])
328-
for attr in ["images", "related_questions"]:
326+
for attr in ["images", "related_questions", "search_results"]:
329327
if attr in chunk:
330328
additional_kwargs[attr] = chunk[attr]
331329

@@ -376,7 +374,7 @@ def _generate(
376374
usage_metadata = None
377375

378376
additional_kwargs = {}
379-
for attr in ["citations", "images", "related_questions"]:
377+
for attr in ["citations", "images", "related_questions", "search_results"]:
380378
if hasattr(response, attr):
381379
additional_kwargs[attr] = getattr(response, attr)
382380

libs/partners/perplexity/tests/unit_tests/test_chat_models.py

Lines changed: 66 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
1-
from typing import Any, Optional
1+
from typing import Any, Optional, cast
22
from unittest.mock import MagicMock
33

4-
from langchain_core.messages import AIMessageChunk, BaseMessageChunk
4+
from langchain_core.messages import AIMessageChunk, BaseMessage
55
from pytest_mock import MockerFixture
66

77
from langchain_perplexity import ChatPerplexity
@@ -58,9 +58,9 @@ def test_perplexity_stream_includes_citations(mocker: MockerFixture) -> None:
5858
llm.client.chat.completions, "create", return_value=mock_stream
5959
)
6060
stream = llm.stream("Hello langchain")
61-
full: Optional[BaseMessageChunk] = None
61+
full: Optional[BaseMessage] = None
6262
for i, chunk in enumerate(stream):
63-
full = chunk if full is None else full + chunk
63+
full = chunk if full is None else cast(BaseMessage, full + chunk)
6464
assert chunk.content == mock_chunks[i]["choices"][0]["delta"]["content"]
6565
if i == 0:
6666
assert chunk.additional_kwargs["citations"] == [
@@ -110,9 +110,9 @@ def test_perplexity_stream_includes_citations_and_images(mocker: MockerFixture)
110110
llm.client.chat.completions, "create", return_value=mock_stream
111111
)
112112
stream = llm.stream("Hello langchain")
113-
full: Optional[BaseMessageChunk] = None
113+
full: Optional[BaseMessage] = None
114114
for i, chunk in enumerate(stream):
115-
full = chunk if full is None else full + chunk
115+
full = chunk if full is None else cast(BaseMessage, full + chunk)
116116
assert chunk.content == mock_chunks[i]["choices"][0]["delta"]["content"]
117117
if i == 0:
118118
assert chunk.additional_kwargs["citations"] == [
@@ -169,9 +169,9 @@ def test_perplexity_stream_includes_citations_and_related_questions(
169169
llm.client.chat.completions, "create", return_value=mock_stream
170170
)
171171
stream = llm.stream("Hello langchain")
172-
full: Optional[BaseMessageChunk] = None
172+
full: Optional[BaseMessage] = None
173173
for i, chunk in enumerate(stream):
174-
full = chunk if full is None else full + chunk
174+
full = chunk if full is None else cast(BaseMessage, full + chunk)
175175
assert chunk.content == mock_chunks[i]["choices"][0]["delta"]["content"]
176176
if i == 0:
177177
assert chunk.additional_kwargs["citations"] == [
@@ -193,3 +193,61 @@ def test_perplexity_stream_includes_citations_and_related_questions(
193193
}
194194

195195
patcher.assert_called_once()
196+
197+
198+
def test_perplexity_stream_includes_citations_and_search_results(
199+
mocker: MockerFixture,
200+
) -> None:
201+
"""Test that the stream method exposes `search_results` via additional_kwargs."""
202+
llm = ChatPerplexity(model="test", timeout=30, verbose=True)
203+
204+
mock_chunk_0 = {
205+
"choices": [{"delta": {"content": "Hello "}, "finish_reason": None}],
206+
"citations": ["example.com/a", "example.com/b"],
207+
"search_results": [
208+
{"title": "Mock result", "url": "https://example.com/result", "date": None}
209+
],
210+
}
211+
mock_chunk_1 = {
212+
"choices": [{"delta": {"content": "Perplexity"}, "finish_reason": None}],
213+
"citations": ["example.com/a", "example.com/b"],
214+
"search_results": [
215+
{"title": "Mock result", "url": "https://example.com/result", "date": None}
216+
],
217+
}
218+
mock_chunks: list[dict[str, Any]] = [mock_chunk_0, mock_chunk_1]
219+
mock_stream = MagicMock()
220+
mock_stream.__iter__.return_value = mock_chunks
221+
patcher = mocker.patch.object(
222+
llm.client.chat.completions, "create", return_value=mock_stream
223+
)
224+
stream = llm.stream("Hello langchain")
225+
full: Optional[BaseMessage] = None
226+
for i, chunk in enumerate(stream):
227+
full = chunk if full is None else cast(BaseMessage, full + chunk)
228+
assert chunk.content == mock_chunks[i]["choices"][0]["delta"]["content"]
229+
if i == 0:
230+
assert chunk.additional_kwargs["citations"] == [
231+
"example.com/a",
232+
"example.com/b",
233+
]
234+
assert chunk.additional_kwargs["search_results"] == [
235+
{
236+
"title": "Mock result",
237+
"url": "https://example.com/result",
238+
"date": None,
239+
}
240+
]
241+
else:
242+
assert "citations" not in chunk.additional_kwargs
243+
assert "search_results" not in chunk.additional_kwargs
244+
assert isinstance(full, AIMessageChunk)
245+
assert full.content == "Hello Perplexity"
246+
assert full.additional_kwargs == {
247+
"citations": ["example.com/a", "example.com/b"],
248+
"search_results": [
249+
{"title": "Mock result", "url": "https://example.com/result", "date": None}
250+
],
251+
}
252+
253+
patcher.assert_called_once()

0 commit comments

Comments
 (0)