diff --git a/libs/partners/perplexity/langchain_perplexity/chat_models.py b/libs/partners/perplexity/langchain_perplexity/chat_models.py index 3b93c8ef96e..9ad6cf0b306 100644 --- a/libs/partners/perplexity/langchain_perplexity/chat_models.py +++ b/libs/partners/perplexity/langchain_perplexity/chat_models.py @@ -329,97 +329,6 @@ def _convert_responses_to_chat_result(response: Any) -> ChatResult: return ChatResult(generations=[ChatGeneration(message=message)]) -def _normalize_perplexity_sse(sse: Any) -> dict[str, Any] | None: - """Decode a Perplexity SSE frame to a typed-payload dict, or skip it. - - Returns `None` for frames that should be skipped without breaking the - stream (empty data, non-dict JSON, decode errors). Uses the SSE - `event:` field as the authoritative event-type discriminator — payloads - that disagree with the SSE frame name are realigned, because the SSE - name is the only source the API guarantees. - """ - data = getattr(sse, "data", None) - if not data: - return None - try: - payload = sse.json() - except (TypeError, ValueError): - logger.warning( - "Discarding Perplexity SSE event with non-JSON data; event=%r data=%r", - getattr(sse, "event", None), - data[:200], - ) - return None - if not isinstance(payload, dict): - logger.debug( - "Discarding Perplexity SSE event with non-dict payload; event=%r type=%s", - getattr(sse, "event", None), - type(payload).__name__, - ) - return None - sse_event = getattr(sse, "event", None) - if sse_event: - # The SSE frame name is authoritative — never let a mismatched - # `type` in the JSON body silently reclassify the event (e.g. a - # `response.failed` mis-tagged as `response.completed`). - payload["type"] = sse_event - return payload - - -def _iter_perplexity_sse_events(stream: Any) -> Iterator[Any]: - """Yield Perplexity Responses streaming events. - - Workaround for an upstream Perplexity Python SDK bug: - `Stream.__stream__` only yields events whose SSE `event:` field is - `None`, but the Agent API tags every event (e.g. - `event: response.completed`). The result is that - `list(client.responses.create(..., stream=True))` returns zero events. - Tracked upstream at: - - https://github.com/perplexityai/perplexity-py/issues/53 - - Real `perplexity.Stream` instances always expose the lower-level - `_iter_events()` SSE iterator; we drop down to it and synthesize event - dicts (`type` taken from the SSE frame name) so they flow through - `_convert_responses_stream_event_to_chunk` — which already handles both - SDK objects and dicts via `_get_attr`. When `_iter_events` is missing - (test fakes that already yield decoded event objects), pass through. - """ - if not hasattr(stream, "_iter_events"): - yield from stream - return - for sse in stream._iter_events(): - sse_data = getattr(sse, "data", None) - # Guard the `[DONE]` sentinel against frames with `data=None` - # (keepalive / comment SSE frames) — `None.startswith` would crash. - if sse_data and sse_data.startswith("[DONE]"): - break - payload = _normalize_perplexity_sse(sse) - if payload is None: - continue - yield payload - - -async def _aiter_perplexity_sse_events(stream: Any) -> AsyncIterator[Any]: - """Async counterpart of `_iter_perplexity_sse_events`. - - See the sync helper for rationale, removal criteria, and the upstream - bug tracking URL. - """ - if not hasattr(stream, "_iter_events"): - async for event in stream: - yield event - return - async for sse in stream._iter_events(): - sse_data = getattr(sse, "data", None) - if sse_data and sse_data.startswith("[DONE]"): - break - payload = _normalize_perplexity_sse(sse) - if payload is None: - continue - yield payload - - class PerplexityResponsesStreamError(RuntimeError): """Raised when a Perplexity Responses (Agent) API stream fails mid-flight. @@ -1062,7 +971,10 @@ class ChatPerplexity(BaseChatModel): ) responses_payload["stream"] = True stream_events = self.client.responses.create(**responses_payload) - for event in _iter_perplexity_sse_events(stream_events): + # Trusts SDK SSE decoding (perplexityai>=0.34.1, upstream issue + # perplexityai-python#53). `_convert_responses_stream_event_to_chunk` + # already handles both SDK objects and dicts via `_get_attr`. + for event in stream_events: response_chunk = _convert_responses_stream_event_to_chunk(event) if response_chunk is None: continue @@ -1178,7 +1090,8 @@ class ChatPerplexity(BaseChatModel): stream_events = await self.async_client.responses.create( **responses_payload ) - async for event in _aiter_perplexity_sse_events(stream_events): + # See sync `_stream` for SDK trust rationale (perplexityai>=0.34.1). + async for event in stream_events: response_chunk = _convert_responses_stream_event_to_chunk(event) if response_chunk is None: continue diff --git a/libs/partners/perplexity/pyproject.toml b/libs/partners/perplexity/pyproject.toml index 85e8fad7b09..d330da99459 100644 --- a/libs/partners/perplexity/pyproject.toml +++ b/libs/partners/perplexity/pyproject.toml @@ -24,7 +24,7 @@ version = "1.3.0" requires-python = ">=3.10.0,<4.0.0" dependencies = [ "langchain-core>=1.4.0,<2.0.0", - "perplexityai>=0.32.0,<1.0.0", + "perplexityai>=0.34.1,<1.0.0", ] [project.urls] diff --git a/libs/partners/perplexity/uv.lock b/libs/partners/perplexity/uv.lock index d01e2f8bc3a..58b469aa0a2 100644 --- a/libs/partners/perplexity/uv.lock +++ b/libs/partners/perplexity/uv.lock @@ -537,7 +537,7 @@ typing = [ [package.metadata] requires-dist = [ { name = "langchain-core", editable = "../../core" }, - { name = "perplexityai", specifier = ">=0.32.0,<1.0.0" }, + { name = "perplexityai", specifier = ">=0.34.1,<1.0.0" }, ] [package.metadata.requires-dev] @@ -973,7 +973,7 @@ wheels = [ [[package]] name = "perplexityai" -version = "0.32.1" +version = "0.34.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "anyio" }, @@ -983,9 +983,9 @@ dependencies = [ { name = "sniffio" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/09/02/73f460c85a5ec533a97fd1ff34fa729a009b4a217a4a87d8da946b6e1c52/perplexityai-0.32.1.tar.gz", hash = "sha256:b03503498591d06c4d50b666f7f7469875d3586f664c29416aae9012ae7a64d1", size = 135741, upload-time = "2026-04-21T04:35:40.345Z" } +sdist = { url = "https://files.pythonhosted.org/packages/4c/9e/253b60c5a51913a0a1e4eddccb02078e4cdb496cab994ad4bdd1e83a8f65/perplexityai-0.34.1.tar.gz", hash = "sha256:8e4b47d52c1d2c0d259eead6941dc60896045070bf0794bcbab8a96e428e06ef", size = 138767, upload-time = "2026-05-27T02:55:02.096Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/d6/11/5c164f114311bc2e2350202393e7c5bd25bb156b5230a1edf5a2b2f4ba04/perplexityai-0.32.1-py3-none-any.whl", hash = "sha256:e5017d245fd8966cf79657edc03a93078d867708542b491b38152618f91e369b", size = 130223, upload-time = "2026-04-21T04:35:38.786Z" }, + { url = "https://files.pythonhosted.org/packages/05/47/246e7e7463df53a9e37cc7427296c8ee5c18183c5217fb3df3a088dfb652/perplexityai-0.34.1-py3-none-any.whl", hash = "sha256:6dcf94e73ec22bc8c98724e6999aa8a371eb441cc7ddd7e44c13e326e06d37b8", size = 131948, upload-time = "2026-05-27T02:55:01.024Z" }, ] [[package]]