Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 21 additions & 7 deletions libs/pinecone/langchain_pinecone/embeddings.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,19 @@
import logging
from typing import Any, Dict, Iterable, List, Optional

# Do not require asyncio extra for sync-only usage; import lazily where needed
from typing import Any as _AnyForAsyncType

from langchain_core.embeddings import Embeddings
from langchain_core.utils import secret_from_env
from pinecone import Pinecone as PineconeClient # type: ignore[import-untyped]
from pinecone import (
PineconeAsyncio as PineconeAsyncioClient, # type: ignore[import-untyped]
)

try: # pragma: no cover - validated via tests that patch async client
from pinecone import (
PineconeAsyncio as PineconeAsyncioClient, # type: ignore[import-untyped]
)
except Exception: # ImportError or missing extra
PineconeAsyncioClient = None # type: ignore[assignment]
from pinecone import SparseValues

from langchain_pinecone._utilities import (
Expand Down Expand Up @@ -80,7 +87,7 @@ class PineconeEmbeddings(BaseModel, Embeddings):

# Clients
_client: PineconeClient = PrivateAttr(default=None)
_async_client: Optional[PineconeAsyncioClient] = PrivateAttr(default=None)
_async_client: Optional[_AnyForAsyncType] = PrivateAttr(default=None)
# Model to use for example 'multilingual-e5-large'. Defaults to 'multilingual-e5-large' if not provided.
model: str = Field(default="multilingual-e5-large")
# Config
Expand Down Expand Up @@ -114,9 +121,16 @@ class PineconeEmbeddings(BaseModel, Embeddings):
)

@property
def async_client(self) -> PineconeAsyncioClient:
"""Lazily initialize the async client."""
return PineconeAsyncioClient(
def async_client(self) -> _AnyForAsyncType:
"""Lazily initialize the async client.

Raises ImportError if the asyncio extra is not installed.
"""
if PineconeAsyncioClient is None:
raise ImportError(
"Async Pinecone client not available. Install 'pinecone[asyncio]' to use async embedding methods."
)
return PineconeAsyncioClient( # type: ignore[operator]
api_key=self.pinecone_api_key.get_secret_value(), source_tag="langchain"
)

Expand Down
13 changes: 11 additions & 2 deletions libs/pinecone/langchain_pinecone/rerank.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,12 @@
from langchain_core.callbacks.base import Callbacks
from langchain_core.documents import BaseDocumentCompressor, Document
from langchain_core.utils import secret_from_env
from pinecone import Pinecone, PineconeAsyncio
from pinecone import Pinecone

try: # pragma: no cover - async client presence covered by tests
from pinecone import PineconeAsyncio
except Exception:
PineconeAsyncio = None # type: ignore[assignment]
from pydantic import AliasChoices, ConfigDict, Field, SecretStr, model_validator

from langchain_pinecone._utilities import (
Expand Down Expand Up @@ -98,8 +103,12 @@ def _get_sync_client(self) -> Pinecone:
)
return self.client

async def _get_async_client(self) -> PineconeAsyncio:
async def _get_async_client(self) -> PineconeAsyncio: # type: ignore[name-defined]
"""Get or create the async client."""
if PineconeAsyncio is None:
raise ImportError(
"Async Pinecone client not available. Install 'pinecone[asyncio]' to use async rerank methods."
)
if self.async_client is None:
self.async_client = PineconeAsyncio(api_key=self._get_api_key())
elif not isinstance(self.async_client, PineconeAsyncio):
Expand Down
13 changes: 11 additions & 2 deletions libs/pinecone/langchain_pinecone/vectorstores.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,12 @@
from langchain_core.utils.iter import batch_iterate
from langchain_core.vectorstores import VectorStore
from pinecone import Pinecone as PineconeClient
from pinecone import PineconeAsyncio as PineconeAsyncioClient

# Optional import: allow sync-only usage without asyncio extra
try: # pragma: no cover - exercised via unit tests mocking client
from pinecone import PineconeAsyncio as PineconeAsyncioClient
except Exception: # ImportError or missing extra
PineconeAsyncioClient = None # type: ignore[assignment]

# conditional imports based on pinecone version
try:
Expand Down Expand Up @@ -275,7 +280,11 @@ def index(self) -> _Index:
async def async_index(self) -> _IndexAsyncio:
"""Get asynchronous index instance."""
if self._async_index is None:
async with PineconeAsyncioClient(
if PineconeAsyncioClient is None:
raise ImportError(
"Async Pinecone client not available. Install 'pinecone[asyncio]' to use async vector store methods."
)
async with PineconeAsyncioClient( # type: ignore[misc]
api_key=self._pinecone_api_key.get_secret_value(),
source_tag="langchain",
) as client:
Expand Down
11 changes: 9 additions & 2 deletions libs/pinecone/pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ license = { text = "MIT" }
requires-python = "<3.14,>=3.9"
dependencies = [
"langchain-core<1.0.0,>=0.3.34",
"pinecone[asyncio]>=6.0.0,<8.0.0",
"pinecone>=6.0.0,<8.0.0",
"numpy>=1.26.4",
"langchain-openai>=0.3.11",
"httpx>=0.28.0",
Expand All @@ -26,6 +26,7 @@ repository = "https://github.com/langchain-ai/langchain-pinecone"

[dependency-groups]
test = [
"pinecone[asyncio]>=6.0.0,<8.0.0",
"pytest<9,>=8",
"freezegun<2.0.0,>=1.2.2",
"pytest-mock<4.0.0,>=3.10.0",
Expand All @@ -36,7 +37,10 @@ test = [
"langchain-tests>=0.3.17",
]
codespell = ["codespell<3.0.0,>=2.2.0"]
test_integration = ["langchain-openai<0.4,>=0.3.6"]
test_integration = [
"langchain-openai<0.4,>=0.3.6",
"pinecone[asyncio]>=6.0.0,<8.0.0",
]
lint = ["ruff<1.0,>=0.5"]
dev = [
"ipykernel>=6.29.5",
Expand All @@ -45,6 +49,9 @@ dev = [
]
typing = ["mypy<2.0,>=1.10", "simsimd<6.0.0,>=5.0.0"]

[project.optional-dependencies]
asyncio = ["pinecone[asyncio]>=6.0.0,<8.0.0"]

[tool.mypy]
disallow_untyped_defs = true
ignore_missing_imports = true
Expand Down
100 changes: 100 additions & 0 deletions libs/pinecone/tests/unit_tests/test_optional_asyncio.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
import pytest
from langchain_core.utils import convert_to_secret_str
from pytest_mock import MockerFixture

from langchain_pinecone.embeddings import PineconeEmbeddings
from langchain_pinecone.rerank import PineconeRerank
from langchain_pinecone.vectorstores import PineconeVectorStore


def test_vectorstore_sync_works_without_asyncio_extra(mocker: MockerFixture) -> None:
# Simulate missing asyncio extra
mocker.patch("langchain_pinecone.vectorstores.PineconeAsyncioClient", None)

# Mock sync index and embedding
mock_index = mocker.Mock()
mock_index.config = mocker.Mock()
mock_index.config.host = "example.org"
mock_index.config.api_key = "test"
mock_index.upsert = mocker.Mock(return_value=None)

mock_embedding = mocker.Mock()
mock_embedding.embed_documents = mocker.Mock(return_value=[[0.1, 0.2, 0.3]])

vs = PineconeVectorStore(
index=mock_index, embedding=mock_embedding, text_key="text"
)

# Sync path should work without asyncio client
vs.add_texts(["hello"], async_req=False)
mock_index.upsert.assert_called_once()


@pytest.mark.asyncio
async def test_vectorstore_async_raises_without_asyncio_extra(
mocker: MockerFixture,
) -> None:
mocker.patch("langchain_pinecone.vectorstores.PineconeAsyncioClient", None)

mock_async_index = mocker.Mock()
mock_async_index.config = mocker.Mock(host="example.org", api_key="test")

mock_embedding = mocker.Mock()
mock_embedding.aembed_documents = mocker.AsyncMock(return_value=[[0.1, 0.2, 0.3]])

vs = PineconeVectorStore(
index=mock_async_index, embedding=mock_embedding, text_key="text"
)

with pytest.raises(ImportError):
await vs.async_index


def test_embeddings_sync_works_without_asyncio_extra(mocker: MockerFixture) -> None:
mocker.patch("langchain_pinecone.embeddings.PineconeAsyncioClient", None)
mocker.patch(
"langchain_pinecone.embeddings.PineconeEmbeddings.list_supported_models",
return_value=[{"model": "multilingual-e5-large"}],
)

emb = PineconeEmbeddings(
model="multilingual-e5-large", pinecone_api_key=convert_to_secret_str("test")
)
# Sync methods should work
mock_client = mocker.patch.object(emb, "_client")
mock_client.inference.embed.return_value = [{"values": [0.1, 0.2]}]
assert isinstance(emb.embed_query("hi"), list)


@pytest.mark.asyncio
async def test_embeddings_async_raises_without_asyncio_extra(
mocker: MockerFixture,
) -> None:
mocker.patch("langchain_pinecone.embeddings.PineconeAsyncioClient", None)
mocker.patch(
"langchain_pinecone.embeddings.PineconeEmbeddings.list_supported_models",
return_value=[{"model": "multilingual-e5-large"}],
)

emb = PineconeEmbeddings(
model="multilingual-e5-large", pinecone_api_key=convert_to_secret_str("test")
)
with pytest.raises(ImportError):
await emb.aembed_query("hi")


@pytest.mark.asyncio
async def test_rerank_async_raises_without_asyncio_extra(mocker: MockerFixture) -> None:
mocker.patch("langchain_pinecone.rerank.PineconeAsyncio", None)
mocker.patch(
"langchain_pinecone.rerank.PineconeRerank.list_supported_models",
return_value=[{"model": "bge-reranker-v2-m3"}],
)

rr = PineconeRerank(pinecone_api_key=convert_to_secret_str("test"))
with pytest.raises(ImportError):
await rr._get_async_client()

# Public API should surface empty result while logging the guidance message
result = await rr.arerank(["doc"], query="q")
assert result == []
19 changes: 16 additions & 3 deletions libs/pinecone/uv.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.