, # Your Zep Project API key
+ memory_key="history", # Ensure this matches the key used in
+ # chain's prompt template
+ return_messages=True, # Does your prompt template expect a string
+ # or a list of Messages?
+ )
+ chain = LLMChain(memory=memory,...) # Configure your chain to use the ZepMemory
+ instance
+
+
+ Note:
+ To persist metadata alongside your chat history, your will need to create a
+ custom Chain class that overrides the `prep_outputs` method to include the metadata
+ in the call to `self.memory.save_context`.
+
+
+ Zep - Recall, understand, and extract data from chat histories. Power personalized AI experiences.
+ =========
+ Zep is a long-term memory service for AI Assistant apps. With Zep, you can provide AI assistants with the ability to recall past conversations,
+ no matter how distant, while also reducing hallucinations, latency, and cost.
+
+ For more information on the zep-python package, see:
+ https://github.com/getzep/zep-python
+
+ """ # noqa: E501
+
+ chat_memory: ZepCloudChatMessageHistory
+
+ def __init__(
+ self,
+ session_id: str,
+ api_key: str,
+ memory_type: Optional[MemoryGetRequestMemoryType] = None,
+ lastn: Optional[int] = None,
+ output_key: Optional[str] = None,
+ input_key: Optional[str] = None,
+ return_messages: bool = False,
+ human_prefix: str = "Human",
+ ai_prefix: str = "AI",
+ memory_key: str = "history",
+ ):
+ """Initialize ZepMemory.
+
+ Args:
+ session_id (str): Identifies your user or a user's session
+ api_key (str): Your Zep Project key.
+ memory_type (Optional[MemoryGetRequestMemoryType], optional): Zep Memory Type, defaults to perpetual
+ lastn (Optional[int], optional): Number of messages to retrieve. Will add the last summary generated prior to the nth oldest message. Defaults to 6
+ output_key (Optional[str], optional): The key to use for the output message.
+ Defaults to None.
+ input_key (Optional[str], optional): The key to use for the input message.
+ Defaults to None.
+ return_messages (bool, optional): Does your prompt template expect a string
+ or a list of Messages? Defaults to False
+ i.e. return a string.
+ human_prefix (str, optional): The prefix to use for human messages.
+ Defaults to "Human".
+ ai_prefix (str, optional): The prefix to use for AI messages.
+ Defaults to "AI".
+ memory_key (str, optional): The key to use for the memory.
+ Defaults to "history".
+ Ensure that this matches the key used in
+ chain's prompt template.
+ """ # noqa: E501
+ chat_message_history = ZepCloudChatMessageHistory(
+ session_id=session_id,
+ memory_type=memory_type,
+ lastn=lastn,
+ api_key=api_key,
+ )
+ super().__init__(
+ chat_memory=chat_message_history,
+ output_key=output_key,
+ input_key=input_key,
+ return_messages=return_messages,
+ human_prefix=human_prefix,
+ ai_prefix=ai_prefix,
+ memory_key=memory_key,
+ )
+
+ def save_context(
+ self,
+ inputs: Dict[str, Any],
+ outputs: Dict[str, str],
+ metadata: Optional[Dict[str, Any]] = None,
+ ) -> None:
+ """Save context from this conversation to buffer.
+
+ Args:
+ inputs (Dict[str, Any]): The inputs to the chain.
+ outputs (Dict[str, str]): The outputs from the chain.
+ metadata (Optional[Dict[str, Any]], optional): Any metadata to save with
+ the context. Defaults to None
+
+ Returns:
+ None
+ """
+ input_str, output_str = self._get_input_output(inputs, outputs)
+ self.chat_memory.add_user_message(input_str, metadata=metadata)
+ self.chat_memory.add_ai_message(output_str, metadata=metadata)
+except ImportError:
+ # Placeholder object
+ class ZepCloudMemory: # type: ignore[no-redef]
+ pass
diff --git a/libs/community/langchain_community/retrievers/asknews.py b/libs/community/langchain_community/retrievers/asknews.py
new file mode 100644
index 0000000000000..18a44161d2868
--- /dev/null
+++ b/libs/community/langchain_community/retrievers/asknews.py
@@ -0,0 +1,146 @@
+import os
+import re
+from typing import Any, Dict, List, Literal, Optional
+
+from langchain_core.callbacks import (
+ AsyncCallbackManagerForRetrieverRun,
+ CallbackManagerForRetrieverRun,
+)
+from langchain_core.documents import Document
+from langchain_core.retrievers import BaseRetriever
+
+
+class AskNewsRetriever(BaseRetriever):
+ """AskNews retriever."""
+
+ k: int = 10
+ offset: int = 0
+ start_timestamp: Optional[int] = None
+ end_timestamp: Optional[int] = None
+ method: Literal["nl", "kw"] = "nl"
+ categories: List[
+ Literal[
+ "All",
+ "Business",
+ "Crime",
+ "Politics",
+ "Science",
+ "Sports",
+ "Technology",
+ "Military",
+ "Health",
+ "Entertainment",
+ "Finance",
+ "Culture",
+ "Climate",
+ "Environment",
+ "World",
+ ]
+ ] = ["All"]
+ historical: bool = False
+ similarity_score_threshold: float = 0.5
+ kwargs: Optional[Dict[str, Any]] = {}
+ client_id: Optional[str] = None
+ client_secret: Optional[str] = None
+
+ def _get_relevant_documents(
+ self, query: str, *, run_manager: CallbackManagerForRetrieverRun
+ ) -> List[Document]:
+ """Get documents relevant to a query.
+ Args:
+ query: String to find relevant documents for
+ run_manager: The callbacks handler to use
+ Returns:
+ List of relevant documents
+ """
+ try:
+ from asknews_sdk import AskNewsSDK
+ except ImportError:
+ raise ImportError(
+ "AskNews python package not found. "
+ "Please install it with `pip install asknews`."
+ )
+ an_client = AskNewsSDK(
+ client_id=self.client_id or os.environ["ASKNEWS_CLIENT_ID"],
+ client_secret=self.client_secret or os.environ["ASKNEWS_CLIENT_SECRET"],
+ scopes=["news"],
+ )
+ response = an_client.news.search_news(
+ query=query,
+ n_articles=self.k,
+ start_timestamp=self.start_timestamp,
+ end_timestamp=self.end_timestamp,
+ method=self.method,
+ categories=self.categories,
+ historical=self.historical,
+ similarity_score_threshold=self.similarity_score_threshold,
+ offset=self.offset,
+ doc_start_delimiter="",
+ doc_end_delimiter="",
+ return_type="both",
+ **self.kwargs,
+ )
+
+ return self._extract_documents(response)
+
+ async def _aget_relevant_documents(
+ self, query: str, *, run_manager: AsyncCallbackManagerForRetrieverRun
+ ) -> List[Document]:
+ """Asynchronously get documents relevant to a query.
+ Args:
+ query: String to find relevant documents for
+ run_manager: The callbacks handler to use
+ Returns:
+ List of relevant documents
+ """
+ try:
+ from asknews_sdk import AsyncAskNewsSDK
+ except ImportError:
+ raise ImportError(
+ "AskNews python package not found. "
+ "Please install it with `pip install asknews`."
+ )
+ an_client = AsyncAskNewsSDK(
+ client_id=self.client_id or os.environ["ASKNEWS_CLIENT_ID"],
+ client_secret=self.client_secret or os.environ["ASKNEWS_CLIENT_SECRET"],
+ scopes=["news"],
+ )
+ response = await an_client.news.search_news(
+ query=query,
+ n_articles=self.k,
+ start_timestamp=self.start_timestamp,
+ end_timestamp=self.end_timestamp,
+ method=self.method,
+ categories=self.categories,
+ historical=self.historical,
+ similarity_score_threshold=self.similarity_score_threshold,
+ offset=self.offset,
+ return_type="both",
+ doc_start_delimiter="",
+ doc_end_delimiter="",
+ **self.kwargs,
+ )
+
+ return self._extract_documents(response)
+
+ def _extract_documents(self, response: Any) -> List[Document]:
+ """Extract documents from an api response."""
+
+ from asknews_sdk.dto.news import SearchResponse
+
+ sr: SearchResponse = response
+ matches = re.findall(r"(.*?)", sr.as_string, re.DOTALL)
+ docs = [
+ Document(
+ page_content=matches[i].strip(),
+ metadata={
+ "title": sr.as_dicts[i].title,
+ "source": str(sr.as_dicts[i].article_url)
+ if sr.as_dicts[i].article_url
+ else None,
+ "images": sr.as_dicts[i].image_url,
+ },
+ )
+ for i in range(len(matches))
+ ]
+ return docs
diff --git a/libs/community/langchain_community/retrievers/zep_cloud.py b/libs/community/langchain_community/retrievers/zep_cloud.py
new file mode 100644
index 0000000000000..96758c71d9861
--- /dev/null
+++ b/libs/community/langchain_community/retrievers/zep_cloud.py
@@ -0,0 +1,162 @@
+from __future__ import annotations
+
+from typing import TYPE_CHECKING, Any, Dict, List, Optional
+
+from langchain_core.callbacks import (
+ AsyncCallbackManagerForRetrieverRun,
+ CallbackManagerForRetrieverRun,
+)
+from langchain_core.documents import Document
+from langchain_core.pydantic_v1 import root_validator
+from langchain_core.retrievers import BaseRetriever
+
+if TYPE_CHECKING:
+ from zep_cloud import MemorySearchResult, SearchScope, SearchType
+ from zep_cloud.client import AsyncZep, Zep
+
+
+class ZepCloudRetriever(BaseRetriever):
+ """`Zep Cloud` MemoryStore Retriever.
+
+ Search your user's long-term chat history with Zep.
+
+ Zep offers both simple semantic search and Maximal Marginal Relevance (MMR)
+ reranking of search results.
+
+ Note: You will need to provide the user's `session_id` to use this retriever.
+
+ Args:
+ api_key: Your Zep API key
+ session_id: Identifies your user or a user's session (required)
+ top_k: Number of documents to return (default: 3, optional)
+ search_type: Type of search to perform (similarity / mmr)
+ (default: similarity, optional)
+ mmr_lambda: Lambda value for MMR search. Defaults to 0.5 (optional)
+
+ Zep - Recall, understand, and extract data from chat histories.
+ Power personalized AI experiences.
+ =========
+ Zep is a long-term memory service for AI Assistant apps.
+ With Zep, you can provide AI assistants with the ability
+ to recall past conversations,
+ no matter how distant, while also reducing hallucinations, latency, and cost.
+
+ see Zep Cloud Docs: https://help.getzep.com
+ """
+
+ api_key: str
+ """Your Zep API key."""
+ zep_client: Zep
+ """Zep client used for making API requests."""
+ zep_client_async: AsyncZep
+ """Async Zep client used for making API requests."""
+ session_id: str
+ """Zep session ID."""
+ top_k: Optional[int]
+ """Number of items to return."""
+ search_scope: SearchScope = "messages"
+ """Which documents to search. Messages or Summaries?"""
+ search_type: SearchType = "similarity"
+ """Type of search to perform (similarity / mmr)"""
+ mmr_lambda: Optional[float] = None
+ """Lambda value for MMR search."""
+
+ @root_validator(pre=True)
+ def create_client(cls, values: dict) -> dict:
+ try:
+ from zep_cloud.client import AsyncZep, Zep
+ except ImportError:
+ raise ImportError(
+ "Could not import zep-cloud package. "
+ "Please install it with `pip install zep-cloud`."
+ )
+ if values.get("api_key") is None:
+ raise ValueError("Zep API key is required.")
+ values["zep_client"] = Zep(api_key=values.get("api_key"))
+ values["zep_client_async"] = AsyncZep(api_key=values.get("api_key"))
+ return values
+
+ def _messages_search_result_to_doc(
+ self, results: List[MemorySearchResult]
+ ) -> List[Document]:
+ return [
+ Document(
+ page_content=str(r.message.content),
+ metadata={
+ "score": r.score,
+ "uuid": r.message.uuid_,
+ "created_at": r.message.created_at,
+ "token_count": r.message.token_count,
+ "role": r.message.role or r.message.role_type,
+ },
+ )
+ for r in results or []
+ if r.message
+ ]
+
+ def _summary_search_result_to_doc(
+ self, results: List[MemorySearchResult]
+ ) -> List[Document]:
+ return [
+ Document(
+ page_content=str(r.summary.content),
+ metadata={
+ "score": r.score,
+ "uuid": r.summary.uuid_,
+ "created_at": r.summary.created_at,
+ "token_count": r.summary.token_count,
+ },
+ )
+ for r in results
+ if r.summary
+ ]
+
+ def _get_relevant_documents(
+ self,
+ query: str,
+ *,
+ run_manager: CallbackManagerForRetrieverRun,
+ metadata: Optional[Dict[str, Any]] = None,
+ ) -> List[Document]:
+ if not self.zep_client:
+ raise RuntimeError("Zep client not initialized.")
+
+ results = self.zep_client.memory.search(
+ self.session_id,
+ text=query,
+ metadata=metadata,
+ search_scope=self.search_scope,
+ search_type=self.search_type,
+ mmr_lambda=self.mmr_lambda,
+ limit=self.top_k,
+ )
+
+ if self.search_scope == "summary":
+ return self._summary_search_result_to_doc(results)
+
+ return self._messages_search_result_to_doc(results)
+
+ async def _aget_relevant_documents(
+ self,
+ query: str,
+ *,
+ run_manager: AsyncCallbackManagerForRetrieverRun,
+ metadata: Optional[Dict[str, Any]] = None,
+ ) -> List[Document]:
+ if not self.zep_client_async:
+ raise RuntimeError("Zep client not initialized.")
+
+ results = await self.zep_client_async.memory.search(
+ self.session_id,
+ text=query,
+ metadata=metadata,
+ search_scope=self.search_scope,
+ search_type=self.search_type,
+ mmr_lambda=self.mmr_lambda,
+ limit=self.top_k,
+ )
+
+ if self.search_scope == "summary":
+ return self._summary_search_result_to_doc(results)
+
+ return self._messages_search_result_to_doc(results)
diff --git a/libs/community/langchain_community/storage/cassandra.py b/libs/community/langchain_community/storage/cassandra.py
new file mode 100644
index 0000000000000..d2d97a3557e71
--- /dev/null
+++ b/libs/community/langchain_community/storage/cassandra.py
@@ -0,0 +1,220 @@
+from __future__ import annotations
+
+import asyncio
+from asyncio import InvalidStateError, Task
+from typing import (
+ TYPE_CHECKING,
+ AsyncIterator,
+ Iterator,
+ List,
+ Optional,
+ Sequence,
+ Tuple,
+)
+
+from langchain_core.stores import ByteStore
+
+from langchain_community.utilities.cassandra import SetupMode, aexecute_cql
+
+if TYPE_CHECKING:
+ from cassandra.cluster import Session
+ from cassandra.query import PreparedStatement
+
+CREATE_TABLE_CQL_TEMPLATE = """
+ CREATE TABLE IF NOT EXISTS {keyspace}.{table}
+ (row_id TEXT, body_blob BLOB, PRIMARY KEY (row_id));
+"""
+SELECT_TABLE_CQL_TEMPLATE = (
+ """SELECT row_id, body_blob FROM {keyspace}.{table} WHERE row_id IN ?;"""
+)
+SELECT_ALL_TABLE_CQL_TEMPLATE = """SELECT row_id, body_blob FROM {keyspace}.{table};"""
+INSERT_TABLE_CQL_TEMPLATE = (
+ """INSERT INTO {keyspace}.{table} (row_id, body_blob) VALUES (?, ?);"""
+)
+DELETE_TABLE_CQL_TEMPLATE = """DELETE FROM {keyspace}.{table} WHERE row_id IN ?;"""
+
+
+class CassandraByteStore(ByteStore):
+ """A ByteStore implementation using Cassandra as the backend.
+
+ Parameters:
+ table: The name of the table to use.
+ session: A Cassandra session object. If not provided, it will be resolved
+ from the cassio config.
+ keyspace: The keyspace to use. If not provided, it will be resolved
+ from the cassio config.
+ setup_mode: The setup mode to use. Default is SYNC (SetupMode.SYNC).
+ """
+
+ def __init__(
+ self,
+ table: str,
+ *,
+ session: Optional[Session] = None,
+ keyspace: Optional[str] = None,
+ setup_mode: SetupMode = SetupMode.SYNC,
+ ) -> None:
+ if not session or not keyspace:
+ try:
+ from cassio.config import check_resolve_keyspace, check_resolve_session
+
+ self.keyspace = keyspace or check_resolve_keyspace(keyspace)
+ self.session = session or check_resolve_session()
+ except (ImportError, ModuleNotFoundError):
+ raise ImportError(
+ "Could not import a recent cassio package."
+ "Please install it with `pip install --upgrade cassio`."
+ )
+ else:
+ self.keyspace = keyspace
+ self.session = session
+ self.table = table
+ self.select_statement = None
+ self.insert_statement = None
+ self.delete_statement = None
+
+ create_cql = CREATE_TABLE_CQL_TEMPLATE.format(
+ keyspace=self.keyspace,
+ table=self.table,
+ )
+ self.db_setup_task: Optional[Task[None]] = None
+ if setup_mode == SetupMode.ASYNC:
+ self.db_setup_task = asyncio.create_task(
+ aexecute_cql(self.session, create_cql)
+ )
+ else:
+ self.session.execute(create_cql)
+
+ def ensure_db_setup(self) -> None:
+ """Ensure that the DB setup is finished. If not, raise a ValueError."""
+ if self.db_setup_task:
+ try:
+ self.db_setup_task.result()
+ except InvalidStateError:
+ raise ValueError(
+ "Asynchronous setup of the DB not finished. "
+ "NB: AstraDB components sync methods shouldn't be called from the "
+ "event loop. Consider using their async equivalents."
+ )
+
+ async def aensure_db_setup(self) -> None:
+ """Ensure that the DB setup is finished. If not, wait for it."""
+ if self.db_setup_task:
+ await self.db_setup_task
+
+ def get_select_statement(self) -> PreparedStatement:
+ """Get the prepared select statement for the table.
+ If not available, prepare it.
+
+ Returns:
+ PreparedStatement: The prepared statement.
+ """
+ if not self.select_statement:
+ self.select_statement = self.session.prepare(
+ SELECT_TABLE_CQL_TEMPLATE.format(
+ keyspace=self.keyspace, table=self.table
+ )
+ )
+ return self.select_statement
+
+ def get_insert_statement(self) -> PreparedStatement:
+ """Get the prepared insert statement for the table.
+ If not available, prepare it.
+
+ Returns:
+ PreparedStatement: The prepared statement.
+ """
+ if not self.insert_statement:
+ self.insert_statement = self.session.prepare(
+ INSERT_TABLE_CQL_TEMPLATE.format(
+ keyspace=self.keyspace, table=self.table
+ )
+ )
+ return self.insert_statement
+
+ def get_delete_statement(self) -> PreparedStatement:
+ """Get the prepared delete statement for the table.
+ If not available, prepare it.
+
+ Returns:
+ PreparedStatement: The prepared statement.
+ """
+
+ if not self.delete_statement:
+ self.delete_statement = self.session.prepare(
+ DELETE_TABLE_CQL_TEMPLATE.format(
+ keyspace=self.keyspace, table=self.table
+ )
+ )
+ return self.delete_statement
+
+ def mget(self, keys: Sequence[str]) -> List[Optional[bytes]]:
+ from cassandra.query import ValueSequence
+
+ self.ensure_db_setup()
+ docs_dict = {}
+ for row in self.session.execute(
+ self.get_select_statement(), [ValueSequence(keys)]
+ ):
+ docs_dict[row.row_id] = row.body_blob
+ return [docs_dict.get(key) for key in keys]
+
+ async def amget(self, keys: Sequence[str]) -> List[Optional[bytes]]:
+ from cassandra.query import ValueSequence
+
+ await self.aensure_db_setup()
+ docs_dict = {}
+ for row in await aexecute_cql(
+ self.session, self.get_select_statement(), parameters=[ValueSequence(keys)]
+ ):
+ docs_dict[row.row_id] = row.body_blob
+ return [docs_dict.get(key) for key in keys]
+
+ def mset(self, key_value_pairs: Sequence[Tuple[str, bytes]]) -> None:
+ self.ensure_db_setup()
+ insert_statement = self.get_insert_statement()
+ for k, v in key_value_pairs:
+ self.session.execute(insert_statement, (k, v))
+
+ async def amset(self, key_value_pairs: Sequence[Tuple[str, bytes]]) -> None:
+ await self.aensure_db_setup()
+ insert_statement = self.get_insert_statement()
+ for k, v in key_value_pairs:
+ await aexecute_cql(self.session, insert_statement, parameters=(k, v))
+
+ def mdelete(self, keys: Sequence[str]) -> None:
+ from cassandra.query import ValueSequence
+
+ self.ensure_db_setup()
+ self.session.execute(self.get_delete_statement(), [ValueSequence(keys)])
+
+ async def amdelete(self, keys: Sequence[str]) -> None:
+ from cassandra.query import ValueSequence
+
+ await self.aensure_db_setup()
+ await aexecute_cql(
+ self.session, self.get_delete_statement(), parameters=[ValueSequence(keys)]
+ )
+
+ def yield_keys(self, *, prefix: Optional[str] = None) -> Iterator[str]:
+ self.ensure_db_setup()
+ for row in self.session.execute(
+ SELECT_ALL_TABLE_CQL_TEMPLATE.format(
+ keyspace=self.keyspace, table=self.table
+ )
+ ):
+ key = row.row_id
+ if not prefix or key.startswith(prefix):
+ yield key
+
+ async def ayield_keys(self, *, prefix: Optional[str] = None) -> AsyncIterator[str]:
+ await self.aensure_db_setup()
+ for row in await aexecute_cql(
+ self.session,
+ SELECT_ALL_TABLE_CQL_TEMPLATE.format(
+ keyspace=self.keyspace, table=self.table
+ ),
+ ):
+ key = row.row_id
+ if not prefix or key.startswith(prefix):
+ yield key
diff --git a/libs/community/langchain_community/storage/sql.py b/libs/community/langchain_community/storage/sql.py
new file mode 100644
index 0000000000000..a92daae1d8c67
--- /dev/null
+++ b/libs/community/langchain_community/storage/sql.py
@@ -0,0 +1,266 @@
+import contextlib
+from pathlib import Path
+from typing import (
+ Any,
+ AsyncGenerator,
+ AsyncIterator,
+ Dict,
+ Generator,
+ Iterator,
+ List,
+ Optional,
+ Sequence,
+ Tuple,
+ Union,
+ cast,
+)
+
+from langchain_core.stores import BaseStore
+from sqlalchemy import (
+ Engine,
+ LargeBinary,
+ and_,
+ create_engine,
+ delete,
+ select,
+)
+from sqlalchemy.ext.asyncio import (
+ AsyncEngine,
+ AsyncSession,
+ async_sessionmaker,
+ create_async_engine,
+)
+from sqlalchemy.orm import (
+ Mapped,
+ Session,
+ declarative_base,
+ mapped_column,
+ sessionmaker,
+)
+
+Base = declarative_base()
+
+
+def items_equal(x: Any, y: Any) -> bool:
+ return x == y
+
+
+class LangchainKeyValueStores(Base): # type: ignore[valid-type,misc]
+ """Table used to save values."""
+
+ # ATTENTION:
+ # Prior to modifying this table, please determine whether
+ # we should create migrations for this table to make sure
+ # users do not experience data loss.
+ __tablename__ = "langchain_key_value_stores"
+
+ namespace: Mapped[str] = mapped_column(primary_key=True, index=True, nullable=False)
+ key: Mapped[str] = mapped_column(primary_key=True, index=True, nullable=False)
+ value = mapped_column(LargeBinary, index=False, nullable=False)
+
+
+# This is a fix of original SQLStore.
+# This can will be removed when a PR will be merged.
+class SQLStore(BaseStore[str, bytes]):
+ """BaseStore interface that works on an SQL database.
+
+ Examples:
+ Create a SQLStore instance and perform operations on it:
+
+ .. code-block:: python
+
+ from langchain_rag.storage import SQLStore
+
+ # Instantiate the SQLStore with the root path
+ sql_store = SQLStore(namespace="test", db_url="sqllite://:memory:")
+
+ # Set values for keys
+ sql_store.mset([("key1", b"value1"), ("key2", b"value2")])
+
+ # Get values for keys
+ values = sql_store.mget(["key1", "key2"]) # Returns [b"value1", b"value2"]
+
+ # Delete keys
+ sql_store.mdelete(["key1"])
+
+ # Iterate over keys
+ for key in sql_store.yield_keys():
+ print(key)
+
+ """
+
+ def __init__(
+ self,
+ *,
+ namespace: str,
+ db_url: Optional[Union[str, Path]] = None,
+ engine: Optional[Union[Engine, AsyncEngine]] = None,
+ engine_kwargs: Optional[Dict[str, Any]] = None,
+ async_mode: Optional[bool] = None,
+ ):
+ if db_url is None and engine is None:
+ raise ValueError("Must specify either db_url or engine")
+
+ if db_url is not None and engine is not None:
+ raise ValueError("Must specify either db_url or engine, not both")
+
+ _engine: Union[Engine, AsyncEngine]
+ if db_url:
+ if async_mode is None:
+ async_mode = False
+ if async_mode:
+ _engine = create_async_engine(
+ url=str(db_url),
+ **(engine_kwargs or {}),
+ )
+ else:
+ _engine = create_engine(url=str(db_url), **(engine_kwargs or {}))
+ elif engine:
+ _engine = engine
+
+ else:
+ raise AssertionError("Something went wrong with configuration of engine.")
+
+ _session_maker: Union[sessionmaker[Session], async_sessionmaker[AsyncSession]]
+ if isinstance(_engine, AsyncEngine):
+ self.async_mode = True
+ _session_maker = async_sessionmaker(bind=_engine)
+ else:
+ self.async_mode = False
+ _session_maker = sessionmaker(bind=_engine)
+
+ self.engine = _engine
+ self.dialect = _engine.dialect.name
+ self.session_maker = _session_maker
+ self.namespace = namespace
+
+ def create_schema(self) -> None:
+ Base.metadata.create_all(self.engine)
+
+ async def acreate_schema(self) -> None:
+ assert isinstance(self.engine, AsyncEngine)
+ async with self.engine.begin() as session:
+ await session.run_sync(Base.metadata.create_all)
+
+ def drop(self) -> None:
+ Base.metadata.drop_all(bind=self.engine.connect())
+
+ async def amget(self, keys: Sequence[str]) -> List[Optional[bytes]]:
+ assert isinstance(self.engine, AsyncEngine)
+ result: Dict[str, bytes] = {}
+ async with self._make_async_session() as session:
+ stmt = select(LangchainKeyValueStores).filter(
+ and_(
+ LangchainKeyValueStores.key.in_(keys),
+ LangchainKeyValueStores.namespace == self.namespace,
+ )
+ )
+ for v in await session.scalars(stmt):
+ result[v.key] = v.value
+ return [result.get(key) for key in keys]
+
+ def mget(self, keys: Sequence[str]) -> List[Optional[bytes]]:
+ result = {}
+
+ with self._make_sync_session() as session:
+ stmt = select(LangchainKeyValueStores).filter(
+ and_(
+ LangchainKeyValueStores.key.in_(keys),
+ LangchainKeyValueStores.namespace == self.namespace,
+ )
+ )
+ for v in session.scalars(stmt):
+ result[v.key] = v.value
+ return [result.get(key) for key in keys]
+
+ async def amset(self, key_value_pairs: Sequence[Tuple[str, bytes]]) -> None:
+ async with self._make_async_session() as session:
+ await self._amdelete([key for key, _ in key_value_pairs], session)
+ session.add_all(
+ [
+ LangchainKeyValueStores(namespace=self.namespace, key=k, value=v)
+ for k, v in key_value_pairs
+ ]
+ )
+ await session.commit()
+
+ def mset(self, key_value_pairs: Sequence[Tuple[str, bytes]]) -> None:
+ values: Dict[str, bytes] = dict(key_value_pairs)
+ with self._make_sync_session() as session:
+ self._mdelete(list(values.keys()), session)
+ session.add_all(
+ [
+ LangchainKeyValueStores(namespace=self.namespace, key=k, value=v)
+ for k, v in values.items()
+ ]
+ )
+ session.commit()
+
+ def _mdelete(self, keys: Sequence[str], session: Session) -> None:
+ stmt = delete(LangchainKeyValueStores).filter(
+ and_(
+ LangchainKeyValueStores.key.in_(keys),
+ LangchainKeyValueStores.namespace == self.namespace,
+ )
+ )
+ session.execute(stmt)
+
+ async def _amdelete(self, keys: Sequence[str], session: AsyncSession) -> None:
+ stmt = delete(LangchainKeyValueStores).filter(
+ and_(
+ LangchainKeyValueStores.key.in_(keys),
+ LangchainKeyValueStores.namespace == self.namespace,
+ )
+ )
+ await session.execute(stmt)
+
+ def mdelete(self, keys: Sequence[str]) -> None:
+ with self._make_sync_session() as session:
+ self._mdelete(keys, session)
+ session.commit()
+
+ async def amdelete(self, keys: Sequence[str]) -> None:
+ async with self._make_async_session() as session:
+ await self._amdelete(keys, session)
+ await session.commit()
+
+ def yield_keys(self, *, prefix: Optional[str] = None) -> Iterator[str]:
+ with self._make_sync_session() as session:
+ for v in session.query(LangchainKeyValueStores).filter( # type: ignore
+ LangchainKeyValueStores.namespace == self.namespace
+ ):
+ if str(v.key).startswith(prefix or ""):
+ yield str(v.key)
+ session.close()
+
+ async def ayield_keys(self, *, prefix: Optional[str] = None) -> AsyncIterator[str]:
+ async with self._make_async_session() as session:
+ stmt = select(LangchainKeyValueStores).filter(
+ LangchainKeyValueStores.namespace == self.namespace
+ )
+ for v in await session.scalars(stmt):
+ if str(v.key).startswith(prefix or ""):
+ yield str(v.key)
+ await session.close()
+
+ @contextlib.contextmanager
+ def _make_sync_session(self) -> Generator[Session, None, None]:
+ """Make an async session."""
+ if self.async_mode:
+ raise ValueError(
+ "Attempting to use a sync method in when async mode is turned on. "
+ "Please use the corresponding async method instead."
+ )
+ with cast(Session, self.session_maker()) as session:
+ yield cast(Session, session)
+
+ @contextlib.asynccontextmanager
+ async def _make_async_session(self) -> AsyncGenerator[AsyncSession, None]:
+ """Make an async session."""
+ if not self.async_mode:
+ raise ValueError(
+ "Attempting to use an async method in when sync mode is turned on. "
+ "Please use the corresponding async method instead."
+ )
+ async with cast(AsyncSession, self.session_maker()) as session:
+ yield cast(AsyncSession, session)
diff --git a/libs/community/langchain_community/tools/asknews/__init__.py b/libs/community/langchain_community/tools/asknews/__init__.py
new file mode 100644
index 0000000000000..635745a7d7d44
--- /dev/null
+++ b/libs/community/langchain_community/tools/asknews/__init__.py
@@ -0,0 +1,7 @@
+"""AskNews API toolkit."""
+
+from langchain_community.tools.asknews.tool import (
+ AskNewsSearch,
+)
+
+__all__ = ["AskNewsSearch"]
diff --git a/libs/community/langchain_community/tools/asknews/tool.py b/libs/community/langchain_community/tools/asknews/tool.py
new file mode 100644
index 0000000000000..e3d39027aa701
--- /dev/null
+++ b/libs/community/langchain_community/tools/asknews/tool.py
@@ -0,0 +1,80 @@
+"""
+Tool for the AskNews API.
+
+To use this tool, you must first set your credentials as environment variables:
+ ASKNEWS_CLIENT_ID
+ ASKNEWS_CLIENT_SECRET
+"""
+
+from typing import Optional, Type
+
+from langchain_core.callbacks import (
+ AsyncCallbackManagerForToolRun,
+ CallbackManagerForToolRun,
+)
+from langchain_core.pydantic_v1 import BaseModel, Field
+from langchain_core.tools import BaseTool
+
+from langchain_community.utilities.asknews import AskNewsAPIWrapper
+
+
+class SearchInput(BaseModel):
+ """Input for the AskNews Search tool."""
+
+ query: str = Field(
+ description="Search query to be used for finding real-time or historical news "
+ "information."
+ )
+ hours_back: Optional[int] = Field(
+ 0,
+ description="If the Assistant deems that the event may have occurred more "
+ "than 48 hours ago, it estimates the number of hours back to search. For "
+ "example, if the event was one month ago, the Assistant may set this to 720. "
+ "One week would be 168. The Assistant can estimate up to on year back (8760).",
+ )
+
+
+class AskNewsSearch(BaseTool):
+ """Tool that searches the AskNews API."""
+
+ name: str = "asknews_search"
+ description: str = (
+ "This tool allows you to perform a search on up-to-date news and historical "
+ "news. If you needs news from more than 48 hours ago, you can estimate the "
+ "number of hours back to search."
+ )
+ api_wrapper: AskNewsAPIWrapper = Field(default_factory=AskNewsAPIWrapper) # type: ignore[arg-type]
+ max_results: int = 10
+ args_schema: Type[BaseModel] = SearchInput
+
+ def _run(
+ self,
+ query: str,
+ hours_back: int = 0,
+ run_manager: Optional[CallbackManagerForToolRun] = None,
+ ) -> str:
+ """Use the tool."""
+ try:
+ return self.api_wrapper.search_news(
+ query,
+ hours_back=hours_back,
+ max_results=self.max_results,
+ )
+ except Exception as e:
+ return repr(e)
+
+ async def _arun(
+ self,
+ query: str,
+ hours_back: int = 0,
+ run_manager: Optional[AsyncCallbackManagerForToolRun] = None,
+ ) -> str:
+ """Use the tool asynchronously."""
+ try:
+ return await self.api_wrapper.asearch_news(
+ query,
+ hours_back=hours_back,
+ max_results=self.max_results,
+ )
+ except Exception as e:
+ return repr(e)
diff --git a/libs/community/langchain_community/tools/databricks/__init__.py b/libs/community/langchain_community/tools/databricks/__init__.py
new file mode 100644
index 0000000000000..9a1d5ffe53677
--- /dev/null
+++ b/libs/community/langchain_community/tools/databricks/__init__.py
@@ -0,0 +1,3 @@
+from langchain_community.tools.databricks.tool import UCFunctionToolkit
+
+__all__ = ["UCFunctionToolkit"]
diff --git a/libs/community/langchain_community/tools/databricks/_execution.py b/libs/community/langchain_community/tools/databricks/_execution.py
new file mode 100644
index 0000000000000..6cc0c661562d1
--- /dev/null
+++ b/libs/community/langchain_community/tools/databricks/_execution.py
@@ -0,0 +1,172 @@
+import json
+from dataclasses import dataclass
+from io import StringIO
+from typing import TYPE_CHECKING, Any, Dict, List, Literal, Optional
+
+if TYPE_CHECKING:
+ from databricks.sdk import WorkspaceClient
+ from databricks.sdk.service.catalog import FunctionInfo
+ from databricks.sdk.service.sql import StatementParameterListItem
+
+
+def is_scalar(function: "FunctionInfo") -> bool:
+ from databricks.sdk.service.catalog import ColumnTypeName
+
+ return function.data_type != ColumnTypeName.TABLE_TYPE
+
+
+@dataclass
+class ParameterizedStatement:
+ statement: str
+ parameters: List["StatementParameterListItem"]
+
+
+@dataclass
+class FunctionExecutionResult:
+ """
+ Result of executing a function.
+ We always use a string to present the result value for AI model to consume.
+ """
+
+ error: Optional[str] = None
+ format: Optional[Literal["SCALAR", "CSV"]] = None
+ value: Optional[str] = None
+ truncated: Optional[bool] = None
+
+ def to_json(self) -> str:
+ data = {k: v for (k, v) in self.__dict__.items() if v is not None}
+ return json.dumps(data)
+
+
+def get_execute_function_sql_stmt(
+ function: "FunctionInfo", json_params: Dict[str, Any]
+) -> ParameterizedStatement:
+ from databricks.sdk.service.catalog import ColumnTypeName
+ from databricks.sdk.service.sql import StatementParameterListItem
+
+ parts = []
+ output_params = []
+ if is_scalar(function):
+ # TODO: IDENTIFIER(:function) did not work
+ parts.append(f"SELECT {function.full_name}(")
+ else:
+ parts.append(f"SELECT * FROM {function.full_name}(")
+ if function.input_params is None or function.input_params.parameters is None:
+ assert (
+ not json_params
+ ), "Function has no parameters but parameters were provided."
+ else:
+ args = []
+ use_named_args = False
+ for p in function.input_params.parameters:
+ if p.name not in json_params:
+ if p.parameter_default is not None:
+ use_named_args = True
+ else:
+ raise ValueError(
+ f"Parameter {p.name} is required but not provided."
+ )
+ else:
+ arg_clause = ""
+ if use_named_args:
+ arg_clause += f"{p.name} => "
+ json_value = json_params[p.name]
+ if p.type_name in (
+ ColumnTypeName.ARRAY,
+ ColumnTypeName.MAP,
+ ColumnTypeName.STRUCT,
+ ):
+ # Use from_json to restore values of complex types.
+ json_value_str = json.dumps(json_value)
+ # TODO: parametrize type
+ arg_clause += f"from_json(:{p.name}, '{p.type_text}')"
+ output_params.append(
+ StatementParameterListItem(name=p.name, value=json_value_str)
+ )
+ elif p.type_name == ColumnTypeName.BINARY:
+ # Use ubbase64 to restore binary values.
+ arg_clause += f"unbase64(:{p.name})"
+ output_params.append(
+ StatementParameterListItem(name=p.name, value=json_value)
+ )
+ else:
+ arg_clause += f":{p.name}"
+ output_params.append(
+ StatementParameterListItem(
+ name=p.name, value=json_value, type=p.type_text
+ )
+ )
+ args.append(arg_clause)
+ parts.append(",".join(args))
+ parts.append(")")
+ # TODO: check extra params in kwargs
+ statement = "".join(parts)
+ return ParameterizedStatement(statement=statement, parameters=output_params)
+
+
+def execute_function(
+ ws: "WorkspaceClient",
+ warehouse_id: str,
+ function: "FunctionInfo",
+ parameters: Dict[str, Any],
+) -> FunctionExecutionResult:
+ """
+ Execute a function with the given arguments and return the result.
+ """
+ try:
+ import pandas as pd
+ except ImportError as e:
+ raise ImportError(
+ "Could not import pandas python package. "
+ "Please install it with `pip install pandas`."
+ ) from e
+ from databricks.sdk.service.sql import StatementState
+
+ # TODO: async so we can run functions in parallel
+ parametrized_statement = get_execute_function_sql_stmt(function, parameters)
+ # TODO: configurable limits
+ response = ws.statement_execution.execute_statement(
+ statement=parametrized_statement.statement,
+ warehouse_id=warehouse_id,
+ parameters=parametrized_statement.parameters,
+ wait_timeout="30s",
+ row_limit=100,
+ byte_limit=4096,
+ )
+ status = response.status
+ assert status is not None, f"Statement execution failed: {response}"
+ if status.state != StatementState.SUCCEEDED:
+ error = status.error
+ assert (
+ error is not None
+ ), "Statement execution failed but no error message was provided."
+ return FunctionExecutionResult(error=f"{error.error_code}: {error.message}")
+ manifest = response.manifest
+ assert manifest is not None
+ truncated = manifest.truncated
+ result = response.result
+ assert (
+ result is not None
+ ), "Statement execution succeeded but no result was provided."
+ data_array = result.data_array
+ if is_scalar(function):
+ value = None
+ if data_array and len(data_array) > 0 and len(data_array[0]) > 0:
+ value = str(data_array[0][0]) # type: ignore
+ return FunctionExecutionResult(
+ format="SCALAR", value=value, truncated=truncated
+ )
+ else:
+ schema = manifest.schema
+ assert (
+ schema is not None and schema.columns is not None
+ ), "Statement execution succeeded but no schema was provided."
+ columns = [c.name for c in schema.columns]
+ if data_array is None:
+ data_array = []
+ pdf = pd.DataFrame.from_records(data_array, columns=columns)
+ csv_buffer = StringIO()
+ pdf.to_csv(csv_buffer, index=False)
+ return FunctionExecutionResult(
+ format="CSV", value=csv_buffer.getvalue(), truncated=truncated
+ )
diff --git a/libs/community/langchain_community/tools/databricks/tool.py b/libs/community/langchain_community/tools/databricks/tool.py
new file mode 100644
index 0000000000000..33f1d9313ee5e
--- /dev/null
+++ b/libs/community/langchain_community/tools/databricks/tool.py
@@ -0,0 +1,201 @@
+import json
+from datetime import date, datetime
+from decimal import Decimal
+from hashlib import md5
+from typing import TYPE_CHECKING, Any, Dict, List, Optional, Type, Union
+
+from langchain_core.pydantic_v1 import BaseModel, Field, create_model
+from langchain_core.tools import BaseTool, BaseToolkit, StructuredTool
+from typing_extensions import Self
+
+if TYPE_CHECKING:
+ from databricks.sdk import WorkspaceClient
+ from databricks.sdk.service.catalog import FunctionInfo
+
+from langchain_community.tools.databricks._execution import execute_function
+
+
+def _uc_type_to_pydantic_type(uc_type_json: Union[str, Dict[str, Any]]) -> Type:
+ mapping = {
+ "long": int,
+ "binary": bytes,
+ "boolean": bool,
+ "date": date,
+ "double": float,
+ "float": float,
+ "integer": int,
+ "short": int,
+ "string": str,
+ "timestamp": datetime,
+ "timestamp_ntz": datetime,
+ "byte": int,
+ }
+ if isinstance(uc_type_json, str):
+ if uc_type_json in mapping:
+ return mapping[uc_type_json]
+ else:
+ if uc_type_json.startswith("decimal"):
+ return Decimal
+ elif uc_type_json == "void" or uc_type_json.startswith("interval"):
+ raise TypeError(f"Type {uc_type_json} is not supported.")
+ else:
+ raise TypeError(
+ f"Unknown type {uc_type_json}. Try upgrading this package."
+ )
+ else:
+ assert isinstance(uc_type_json, dict)
+ tpe = uc_type_json["type"]
+ if tpe == "array":
+ element_type = _uc_type_to_pydantic_type(uc_type_json["elementType"])
+ if uc_type_json["containsNull"]:
+ element_type = Optional[element_type] # type: ignore
+ return List[element_type] # type: ignore
+ elif tpe == "map":
+ key_type = uc_type_json["keyType"]
+ assert key_type == "string", TypeError(
+ f"Only support STRING key type for MAP but got {key_type}."
+ )
+ value_type = _uc_type_to_pydantic_type(uc_type_json["valueType"])
+ if uc_type_json["valueContainsNull"]:
+ value_type: Type = Optional[value_type] # type: ignore
+ return Dict[str, value_type] # type: ignore
+ elif tpe == "struct":
+ fields = {}
+ for field in uc_type_json["fields"]:
+ field_type = _uc_type_to_pydantic_type(field["type"])
+ if field.get("nullable"):
+ field_type = Optional[field_type] # type: ignore
+ comment = (
+ uc_type_json["metadata"].get("comment")
+ if "metadata" in uc_type_json
+ else None
+ )
+ fields[field["name"]] = (field_type, Field(..., description=comment))
+ uc_type_json_str = json.dumps(uc_type_json, sort_keys=True)
+ type_hash = md5(uc_type_json_str.encode()).hexdigest()[:8]
+ return create_model(f"Struct_{type_hash}", **fields) # type: ignore
+ else:
+ raise TypeError(f"Unknown type {uc_type_json}. Try upgrading this package.")
+
+
+def _generate_args_schema(function: "FunctionInfo") -> Type[BaseModel]:
+ if function.input_params is None:
+ return BaseModel
+ params = function.input_params.parameters
+ assert params is not None
+ fields = {}
+ for p in params:
+ assert p.type_json is not None
+ type_json = json.loads(p.type_json)["type"]
+ pydantic_type = _uc_type_to_pydantic_type(type_json)
+ description = p.comment
+ default: Any = ...
+ if p.parameter_default:
+ pydantic_type = Optional[pydantic_type] # type: ignore
+ default = None
+ # TODO: Convert default value string to the correct type.
+ # We might need to use statement execution API
+ # to get the JSON representation of the value.
+ default_description = f"(Default: {p.parameter_default})"
+ if description:
+ description += f" {default_description}"
+ else:
+ description = default_description
+ fields[p.name] = (
+ pydantic_type,
+ Field(default=default, description=description),
+ )
+ return create_model(
+ f"{function.catalog_name}__{function.schema_name}__{function.name}__params",
+ **fields, # type: ignore
+ )
+
+
+def _get_tool_name(function: "FunctionInfo") -> str:
+ tool_name = f"{function.catalog_name}__{function.schema_name}__{function.name}"[
+ -64:
+ ]
+ return tool_name
+
+
+def _get_default_workspace_client() -> "WorkspaceClient":
+ try:
+ from databricks.sdk import WorkspaceClient
+ except ImportError as e:
+ raise ImportError(
+ "Could not import databricks-sdk python package. "
+ "Please install it with `pip install databricks-sdk`."
+ ) from e
+ return WorkspaceClient()
+
+
+class UCFunctionToolkit(BaseToolkit):
+ warehouse_id: str = Field(
+ description="The ID of a Databricks SQL Warehouse to execute functions."
+ )
+
+ workspace_client: "WorkspaceClient" = Field(
+ default_factory=_get_default_workspace_client,
+ description="Databricks workspace client.",
+ )
+
+ tools: Dict[str, BaseTool] = Field(default_factory=dict)
+
+ class Config:
+ arbitrary_types_allowed = True
+
+ def include(self, *function_names: str, **kwargs: Any) -> Self:
+ """
+ Includes UC functions to the toolkit.
+
+ Args:
+ functions: A list of UC function names in the format
+ "catalog_name.schema_name.function_name" or
+ "catalog_name.schema_name.*".
+ If the function name ends with ".*",
+ all functions in the schema will be added.
+ kwargs: Extra arguments to pass to StructuredTool, e.g., `return_direct`.
+ """
+ for name in function_names:
+ if name.endswith(".*"):
+ catalog_name, schema_name = name[:-2].split(".")
+ # TODO: handle pagination, warn and truncate if too many
+ functions = self.workspace_client.functions.list(
+ catalog_name=catalog_name, schema_name=schema_name
+ )
+ for f in functions:
+ assert f.full_name is not None
+ self.include(f.full_name, **kwargs)
+ else:
+ if name not in self.tools:
+ self.tools[name] = self._make_tool(name, **kwargs)
+ return self
+
+ def _make_tool(self, function_name: str, **kwargs: Any) -> BaseTool:
+ function = self.workspace_client.functions.get(function_name)
+ name = _get_tool_name(function)
+ description = function.comment or ""
+ args_schema = _generate_args_schema(function)
+
+ def func(*args: Any, **kwargs: Any) -> str:
+ # TODO: We expect all named args and ignore args.
+ # Non-empty args show up when the function has no parameters.
+ args_json = json.loads(json.dumps(kwargs, default=str))
+ result = execute_function(
+ ws=self.workspace_client,
+ warehouse_id=self.warehouse_id,
+ function=function,
+ parameters=args_json,
+ )
+ return result.to_json()
+
+ return StructuredTool(
+ name=name,
+ description=description,
+ args_schema=args_schema,
+ func=func,
+ **kwargs,
+ )
+
+ def get_tools(self) -> List[BaseTool]:
+ return list(self.tools.values())
diff --git a/libs/community/langchain_community/tools/zenguard/__init__.py b/libs/community/langchain_community/tools/zenguard/__init__.py
new file mode 100644
index 0000000000000..ac9ddbb11b12b
--- /dev/null
+++ b/libs/community/langchain_community/tools/zenguard/__init__.py
@@ -0,0 +1,11 @@
+from langchain_community.tools.zenguard.tool import (
+ Detector,
+ ZenGuardInput,
+ ZenGuardTool,
+)
+
+__all__ = [
+ "ZenGuardTool",
+ "Detector",
+ "ZenGuardInput",
+]
diff --git a/libs/community/langchain_community/tools/zenguard/tool.py b/libs/community/langchain_community/tools/zenguard/tool.py
new file mode 100644
index 0000000000000..1bb2a8fe05b0d
--- /dev/null
+++ b/libs/community/langchain_community/tools/zenguard/tool.py
@@ -0,0 +1,104 @@
+import os
+from enum import Enum
+from typing import Any, Dict, List, Optional
+
+import requests
+from langchain_core.pydantic_v1 import BaseModel, Field, ValidationError, validator
+from langchain_core.tools import BaseTool
+
+
+class Detector(str, Enum):
+ ALLOWED_TOPICS = "allowed_subjects"
+ BANNED_TOPICS = "banned_subjects"
+ PROMPT_INJECTION = "prompt_injection"
+ KEYWORDS = "keywords"
+ PII = "pii"
+ SECRETS = "secrets"
+ TOXICITY = "toxicity"
+
+
+class DetectorAPI(str, Enum):
+ ALLOWED_TOPICS = "v1/detect/topics/allowed"
+ BANNED_TOPICS = "v1/detect/topics/banned"
+ PROMPT_INJECTION = "v1/detect/prompt_injection"
+ KEYWORDS = "v1/detect/keywords"
+ PII = "v1/detect/pii"
+ SECRETS = "v1/detect/secrets"
+ TOXICITY = "v1/detect/toxicity"
+
+
+class ZenGuardInput(BaseModel):
+ prompts: List[str] = Field(
+ ...,
+ min_items=1,
+ min_length=1,
+ description="Prompt to check",
+ )
+ detectors: List[Detector] = Field(
+ ...,
+ min_items=1,
+ description="List of detectors by which you want to check the prompt",
+ )
+ in_parallel: bool = Field(
+ default=True,
+ description="Run prompt detection by the detector in parallel or sequentially",
+ )
+
+
+class ZenGuardTool(BaseTool):
+ name: str = "ZenGuard"
+ description: str = (
+ "ZenGuard AI integration package. ZenGuard AI - the fastest GenAI guardrails."
+ )
+ args_schema = ZenGuardInput
+ return_direct = True
+
+ zenguard_api_key: Optional[str] = Field(default=None)
+
+ _ZENGUARD_API_URL_ROOT = "https://api.zenguard.ai/"
+ _ZENGUARD_API_KEY_ENV_NAME = "ZENGUARD_API_KEY"
+
+ @validator("zenguard_api_key", pre=True, always=True, check_fields=False)
+ def set_api_key(cls, v: str) -> str:
+ if v is None:
+ v = os.getenv(cls._ZENGUARD_API_KEY_ENV_NAME)
+ if v is None:
+ raise ValidationError(
+ "The zenguard_api_key tool option must be set either "
+ "by passing zenguard_api_key to the tool or by setting "
+ f"the f{cls._ZENGUARD_API_KEY_ENV_NAME} environment variable"
+ )
+ return v
+
+ def _run(
+ self,
+ prompts: List[str],
+ detectors: List[Detector],
+ in_parallel: bool = True,
+ ) -> Dict[str, Any]:
+ try:
+ postfix = None
+ json: Optional[Dict[str, Any]] = None
+ if len(detectors) == 1:
+ postfix = self._convert_detector_to_api(detectors[0])
+ json = {"messages": prompts}
+ else:
+ postfix = "v1/detect"
+ json = {
+ "messages": prompts,
+ "in_parallel": in_parallel,
+ "detectors": detectors,
+ }
+ response = requests.post(
+ self._ZENGUARD_API_URL_ROOT + postfix,
+ json=json,
+ headers={"x-api-key": self.zenguard_api_key},
+ timeout=5,
+ )
+ response.raise_for_status()
+ return response.json()
+ except (requests.HTTPError, requests.Timeout) as e:
+ return {"error": str(e)}
+
+ def _convert_detector_to_api(self, detector: Detector) -> str:
+ return DetectorAPI[detector.name].value
diff --git a/libs/community/langchain_community/utilities/asknews.py b/libs/community/langchain_community/utilities/asknews.py
new file mode 100644
index 0000000000000..4ac5445568ce8
--- /dev/null
+++ b/libs/community/langchain_community/utilities/asknews.py
@@ -0,0 +1,115 @@
+"""Util that calls AskNews api."""
+
+from __future__ import annotations
+
+from datetime import datetime, timedelta
+from typing import Any, Dict, Optional
+
+from langchain_core.pydantic_v1 import BaseModel, Extra, root_validator
+from langchain_core.utils import get_from_dict_or_env
+
+
+class AskNewsAPIWrapper(BaseModel):
+ """Wrapper for AskNews API."""
+
+ asknews_sync: Any #: :meta private:
+ asknews_async: Any #: :meta private:
+ asknews_client_id: Optional[str] = None
+ """Client ID for the AskNews API."""
+ asknews_client_secret: Optional[str] = None
+ """Client Secret for the AskNews API."""
+
+ class Config:
+ """Configuration for this pydantic object."""
+
+ extra = Extra.forbid
+
+ @root_validator()
+ def validate_environment(cls, values: Dict) -> Dict:
+ """Validate that api credentials and python package exists in environment."""
+
+ asknews_client_id = get_from_dict_or_env(
+ values, "asknews_client_id", "ASKNEWS_CLIENT_ID"
+ )
+ asknews_client_secret = get_from_dict_or_env(
+ values, "asknews_client_secret", "ASKNEWS_CLIENT_SECRET"
+ )
+
+ try:
+ import asknews_sdk
+
+ except ImportError:
+ raise ImportError(
+ "AskNews python package not found. "
+ "Please install it with `pip install asknews`."
+ )
+
+ an_sync = asknews_sdk.AskNewsSDK(
+ client_id=asknews_client_id,
+ client_secret=asknews_client_secret,
+ scopes=["news"],
+ )
+ an_async = asknews_sdk.AsyncAskNewsSDK(
+ client_id=asknews_client_id,
+ client_secret=asknews_client_secret,
+ scopes=["news"],
+ )
+
+ values["asknews_sync"] = an_sync
+ values["asknews_async"] = an_async
+ values["asknews_client_id"] = asknews_client_id
+ values["asknews_client_secret"] = asknews_client_secret
+
+ return values
+
+ def search_news(
+ self, query: str, max_results: int = 10, hours_back: int = 0
+ ) -> str:
+ """Search news in AskNews API synchronously."""
+ if hours_back > 48:
+ method = "kw"
+ historical = True
+ start = int((datetime.now() - timedelta(hours=hours_back)).timestamp())
+ stop = int(datetime.now().timestamp())
+ else:
+ historical = False
+ method = "nl"
+ start = None
+ stop = None
+
+ response = self.asknews_sync.news.search_news(
+ query=query,
+ n_articles=max_results,
+ method=method,
+ historical=historical,
+ start_timestamp=start,
+ end_timestamp=stop,
+ return_type="string",
+ )
+ return response.as_string
+
+ async def asearch_news(
+ self, query: str, max_results: int = 10, hours_back: int = 0
+ ) -> str:
+ """Search news in AskNews API asynchronously."""
+ if hours_back > 48:
+ method = "kw"
+ historical = True
+ start = int((datetime.now() - timedelta(hours=hours_back)).timestamp())
+ stop = int(datetime.now().timestamp())
+ else:
+ historical = False
+ method = "nl"
+ start = None
+ stop = None
+
+ response = await self.asknews_async.news.search_news(
+ query=query,
+ n_articles=max_results,
+ method=method,
+ historical=historical,
+ start_timestamp=start,
+ end_timestamp=stop,
+ return_type="string",
+ )
+ return response.as_string
diff --git a/libs/community/langchain_community/utils/user_agent.py b/libs/community/langchain_community/utils/user_agent.py
new file mode 100644
index 0000000000000..befb8cf9a0f8a
--- /dev/null
+++ b/libs/community/langchain_community/utils/user_agent.py
@@ -0,0 +1,16 @@
+import logging
+import os
+
+log = logging.getLogger(__name__)
+
+
+def get_user_agent() -> str:
+ """Get user agent from environment variable."""
+ env_user_agent = os.environ.get("USER_AGENT")
+ if not env_user_agent:
+ log.warning(
+ "USER_AGENT environment variable not set, "
+ "consider setting it to identify your requests."
+ )
+ return "DefaultLangchainUserAgent"
+ return env_user_agent
diff --git a/libs/community/langchain_community/vectorstores/aerospike.py b/libs/community/langchain_community/vectorstores/aerospike.py
new file mode 100644
index 0000000000000..e7759923b6126
--- /dev/null
+++ b/libs/community/langchain_community/vectorstores/aerospike.py
@@ -0,0 +1,598 @@
+from __future__ import annotations
+
+import logging
+import uuid
+import warnings
+from typing import (
+ TYPE_CHECKING,
+ Any,
+ Callable,
+ Iterable,
+ List,
+ Optional,
+ Tuple,
+ TypeVar,
+ Union,
+)
+
+import numpy as np
+from langchain_core.documents import Document
+from langchain_core.embeddings import Embeddings
+from langchain_core.vectorstores import VectorStore
+
+from langchain_community.vectorstores.utils import (
+ DistanceStrategy,
+ maximal_marginal_relevance,
+)
+
+if TYPE_CHECKING:
+ from aerospike_vector_search import Client
+ from aerospike_vector_search.types import Neighbor, VectorDistanceMetric
+
+logger = logging.getLogger(__name__)
+
+
+def _import_aerospike() -> Any:
+ try:
+ from aerospike_vector_search import Client
+ except ImportError as e:
+ raise ImportError(
+ "Could not import aerospike_vector_search python package. "
+ "Please install it with `pip install aerospike_vector`."
+ ) from e
+ return Client
+
+
+AVST = TypeVar("AVST", bound="Aerospike")
+
+
+class Aerospike(VectorStore):
+ """`Aerospike` vector store.
+
+ To use, you should have the ``aerospike_vector_search`` python package installed.
+ """
+
+ def __init__(
+ self,
+ client: Client,
+ embedding: Union[Embeddings, Callable],
+ namespace: str,
+ index_name: Optional[str] = None,
+ vector_key: str = "_vector",
+ text_key: str = "_text",
+ id_key: str = "_id",
+ set_name: Optional[str] = None,
+ distance_strategy: Optional[
+ Union[DistanceStrategy, VectorDistanceMetric]
+ ] = DistanceStrategy.EUCLIDEAN_DISTANCE,
+ ):
+ """Initialize with Aerospike client.
+
+ Args:
+ client: Aerospike client.
+ embedding: Embeddings object or Callable (deprecated) to embed text.
+ namespace: Namespace to use for storing vectors. This should match
+ index_name: Name of the index previously created in Aerospike. This
+ vector_key: Key to use for vector in metadata. This should match the
+ key used during index creation.
+ text_key: Key to use for text in metadata.
+ id_key: Key to use for id in metadata.
+ set_name: Default set name to use for storing vectors.
+ distance_strategy: Distance strategy to use for similarity search
+ This should match the distance strategy used during index creation.
+ """
+
+ aerospike = _import_aerospike()
+
+ if not isinstance(embedding, Embeddings):
+ warnings.warn(
+ "Passing in `embedding` as a Callable is deprecated. Please pass in an"
+ " Embeddings object instead."
+ )
+
+ if not isinstance(client, aerospike):
+ raise ValueError(
+ f"client should be an instance of aerospike_vector_search.Client, "
+ f"got {type(client)}"
+ )
+
+ self._client = client
+ self._embedding = embedding
+ self._text_key = text_key
+ self._vector_key = vector_key
+ self._id_key = id_key
+ self._index_name = index_name
+ self._namespace = namespace
+ self._set_name = set_name
+ self._distance_strategy = self.convert_distance_strategy(distance_strategy)
+
+ @property
+ def embeddings(self) -> Optional[Embeddings]:
+ """Access the query embedding object if available."""
+ if isinstance(self._embedding, Embeddings):
+ return self._embedding
+ return None
+
+ def _embed_documents(self, texts: Iterable[str]) -> List[List[float]]:
+ """Embed search docs."""
+ if isinstance(self._embedding, Embeddings):
+ return self._embedding.embed_documents(list(texts))
+ return [self._embedding(t) for t in texts]
+
+ def _embed_query(self, text: str) -> List[float]:
+ """Embed query text."""
+ if isinstance(self._embedding, Embeddings):
+ return self._embedding.embed_query(text)
+ return self._embedding(text)
+
+ @staticmethod
+ def convert_distance_strategy(
+ distance_strategy: Union[VectorDistanceMetric, DistanceStrategy],
+ ) -> DistanceStrategy:
+ """
+ Convert Aerospikes distance strategy to langchains DistanceStrategy
+ enum. This is a convenience method to allow users to pass in the same
+ distance metric used to create the index.
+ """
+ from aerospike_vector_search.types import VectorDistanceMetric
+
+ if isinstance(distance_strategy, DistanceStrategy):
+ return distance_strategy
+
+ if distance_strategy == VectorDistanceMetric.COSINE:
+ return DistanceStrategy.COSINE
+
+ if distance_strategy == VectorDistanceMetric.DOT_PRODUCT:
+ return DistanceStrategy.DOT_PRODUCT
+
+ if distance_strategy == VectorDistanceMetric.SQUARED_EUCLIDEAN:
+ return DistanceStrategy.EUCLIDEAN_DISTANCE
+
+ raise ValueError(
+ "Unknown distance strategy, must be cosine, dot_product" ", or euclidean"
+ )
+
+ def add_texts(
+ self,
+ texts: Iterable[str],
+ metadatas: Optional[List[dict]] = None,
+ ids: Optional[List[str]] = None,
+ set_name: Optional[str] = None,
+ embedding_chunk_size: int = 1000,
+ index_name: Optional[str] = None,
+ wait_for_index: bool = True,
+ **kwargs: Any,
+ ) -> List[str]:
+ """Run more texts through the embeddings and add to the vectorstore.
+
+
+ Args:
+ texts: Iterable of strings to add to the vectorstore.
+ metadatas: Optional list of metadatas associated with the texts.
+ ids: Optional list of ids to associate with the texts.
+ set_name: Optional aerospike set name to add the texts to.
+ batch_size: Batch size to use when adding the texts to the vectorstore.
+ embedding_chunk_size: Chunk size to use when embedding the texts.
+ index_name: Optional aerospike index name used for waiting for index
+ completion. If not provided, the default index_name will be used.
+ wait_for_index: If True, wait for the all the texts to be indexed
+ before returning. Requires index_name to be provided. Defaults
+ to True.
+ **kwargs: Additional keyword arguments to pass to the client upsert call.
+
+ Returns:
+ List of ids from adding the texts into the vectorstore.
+
+ """
+ if set_name is None:
+ set_name = self._set_name
+
+ if index_name is None:
+ index_name = self._index_name
+
+ if wait_for_index and index_name is None:
+ raise ValueError("if wait_for_index is True, index_name must be provided")
+
+ texts = list(texts)
+ ids = ids or [str(uuid.uuid4()) for _ in texts]
+
+ # We need to shallow copy so that we can add the vector and text keys
+ if metadatas:
+ metadatas = [m.copy() for m in metadatas]
+ else:
+ metadatas = metadatas or [{} for _ in texts]
+
+ for i in range(0, len(texts), embedding_chunk_size):
+ chunk_texts = texts[i : i + embedding_chunk_size]
+ chunk_ids = ids[i : i + embedding_chunk_size]
+ chunk_metadatas = metadatas[i : i + embedding_chunk_size]
+ embeddings = self._embed_documents(chunk_texts)
+
+ for metadata, embedding, text in zip(
+ chunk_metadatas, embeddings, chunk_texts
+ ):
+ metadata[self._vector_key] = embedding
+ metadata[self._text_key] = text
+
+ for id, metadata in zip(chunk_ids, chunk_metadatas):
+ metadata[self._id_key] = id
+ self._client.upsert(
+ namespace=self._namespace,
+ key=id,
+ set_name=set_name,
+ record_data=metadata,
+ **kwargs,
+ )
+
+ if wait_for_index:
+ self._client.wait_for_index_completion(
+ namespace=self._namespace,
+ name=index_name,
+ )
+
+ return ids
+
+ def delete(
+ self,
+ ids: Optional[List[str]] = None,
+ set_name: Optional[str] = None,
+ **kwargs: Any,
+ ) -> Optional[bool]:
+ """Delete by vector ID or other criteria.
+
+ Args:
+ ids: List of ids to delete.
+ **kwargs: Other keyword arguments to pass to client delete call.
+
+ Returns:
+ Optional[bool]: True if deletion is successful,
+ False otherwise, None if not implemented.
+ """
+ from aerospike_vector_search import AVSServerError
+
+ if ids:
+ for id in ids:
+ try:
+ self._client.delete(
+ namespace=self._namespace,
+ key=id,
+ set_name=set_name,
+ **kwargs,
+ )
+ except AVSServerError:
+ return False
+
+ return True
+
+ def similarity_search_with_score(
+ self,
+ query: str,
+ k: int = 4,
+ metadata_keys: Optional[List[str]] = None,
+ index_name: Optional[str] = None,
+ **kwargs: Any,
+ ) -> List[Tuple[Document, float]]:
+ """Return aerospike documents most similar to query, along with scores.
+
+ Args:
+ query: Text to look up documents similar to.
+ k: Number of Documents to return. Defaults to 4.
+ metadata_keys: List of metadata keys to return with the documents.
+ If None, all metadata keys will be returned. Defaults to None.
+ index_name: Name of the index to search. Overrides the default
+ index_name.
+ kwargs: Additional keyword arguments to pass to the search method.
+
+ Returns:
+ List of Documents most similar to the query and associated scores.
+ """
+
+ return self.similarity_search_by_vector_with_score(
+ self._embed_query(query),
+ k=k,
+ metadata_keys=metadata_keys,
+ index_name=index_name,
+ **kwargs,
+ )
+
+ def similarity_search_by_vector_with_score(
+ self,
+ embedding: List[float],
+ k: int = 4,
+ metadata_keys: Optional[List[str]] = None,
+ index_name: Optional[str] = None,
+ **kwargs: Any,
+ ) -> List[Tuple[Document, float]]:
+ """Return aerospike documents most similar to embedding, along with scores.
+
+ Args:
+ embedding: Embedding to look up documents similar to.
+ k: Number of Documents to return. Defaults to 4.
+ metadata_keys: List of metadata keys to return with the documents.
+ If None, all metadata keys will be returned. Defaults to None.
+ index_name: Name of the index to search. Overrides the default
+ index_name.
+ kwargs: Additional keyword arguments to pass to the client
+ vector_search method.
+
+ Returns:
+ List of Documents most similar to the query and associated scores.
+
+ """
+
+ docs = []
+
+ if metadata_keys and self._text_key not in metadata_keys:
+ metadata_keys = [self._text_key] + metadata_keys
+
+ if index_name is None:
+ index_name = self._index_name
+
+ if index_name is None:
+ raise ValueError("index_name must be provided")
+
+ results: list[Neighbor] = self._client.vector_search(
+ index_name=index_name,
+ namespace=self._namespace,
+ query=embedding,
+ limit=k,
+ field_names=metadata_keys,
+ **kwargs,
+ )
+
+ for result in results:
+ metadata = result.fields
+
+ if self._text_key in metadata:
+ text = metadata.pop(self._text_key)
+ score = result.distance
+ docs.append((Document(page_content=text, metadata=metadata), score))
+ else:
+ logger.warning(
+ f"Found document with no `{self._text_key}` key. Skipping."
+ )
+ continue
+
+ return docs
+
+ def similarity_search_by_vector(
+ self,
+ embedding: List[float],
+ k: int = 4,
+ metadata_keys: Optional[List[str]] = None,
+ index_name: Optional[str] = None,
+ **kwargs: Any,
+ ) -> List[Document]:
+ """Return docs most similar to embedding vector.
+
+ Args:
+ embedding: Embedding to look up documents similar to.
+ k: Number of Documents to return. Defaults to 4.
+ metadata_keys: List of metadata keys to return with the documents.
+ If None, all metadata keys will be returned. Defaults to None.
+ index_name: Name of the index to search. Overrides the default
+ index_name.
+ kwargs: Additional keyword arguments to pass to the search method.
+
+
+ Returns:
+ List of Documents most similar to the query vector.
+ """
+ return [
+ doc
+ for doc, _ in self.similarity_search_by_vector_with_score(
+ embedding,
+ k=k,
+ metadata_keys=metadata_keys,
+ index_name=index_name,
+ **kwargs,
+ )
+ ]
+
+ def similarity_search(
+ self,
+ query: str,
+ k: int = 4,
+ metadata_keys: Optional[List[str]] = None,
+ index_name: Optional[str] = None,
+ **kwargs: Any,
+ ) -> List[Document]:
+ """Return aerospike documents most similar to query.
+
+ Args:
+ query: Text to look up documents similar to.
+ k: Number of Documents to return. Defaults to 4.
+ metadata_keys: List of metadata keys to return with the documents.
+ If None, all metadata keys will be returned. Defaults to None.
+ index_name: Optional name of the index to search. Overrides the
+ default index_name.
+
+ Returns:
+ List of Documents most similar to the query and score for each
+ """
+ docs_and_scores = self.similarity_search_with_score(
+ query, k=k, metadata_keys=metadata_keys, index_name=index_name, **kwargs
+ )
+ return [doc for doc, _ in docs_and_scores]
+
+ def _select_relevance_score_fn(self) -> Callable[[float], float]:
+ """
+ The 'correct' relevance function
+ may differ depending on a few things, including:
+ - the distance / similarity metric used by the VectorStore
+ - the scale of your embeddings (OpenAI's are unit normed. Many others are not!)
+ - embedding dimensionality
+ - etc.
+
+ 0 is dissimilar, 1 is similar.
+
+ Aerospike's relevance_fn assume euclidean and dot product embeddings are
+ normalized to unit norm.
+ """
+ if self._distance_strategy == DistanceStrategy.COSINE:
+ return self._cosine_relevance_score_fn
+ elif self._distance_strategy == DistanceStrategy.DOT_PRODUCT:
+ return self._max_inner_product_relevance_score_fn
+ elif self._distance_strategy == DistanceStrategy.EUCLIDEAN_DISTANCE:
+ return self._euclidean_relevance_score_fn
+ else:
+ raise ValueError(
+ "Unknown distance strategy, must be cosine, dot_product"
+ ", or euclidean"
+ )
+
+ @staticmethod
+ def _cosine_relevance_score_fn(score: float) -> float:
+ """Aerospike returns cosine distance scores between [0,2]
+
+ 0 is dissimilar, 1 is similar.
+ """
+ return 1 - (score / 2)
+
+ def max_marginal_relevance_search_by_vector(
+ self,
+ embedding: List[float],
+ k: int = 4,
+ fetch_k: int = 20,
+ lambda_mult: float = 0.5,
+ metadata_keys: Optional[List[str]] = None,
+ index_name: Optional[str] = None,
+ **kwargs: Any,
+ ) -> List[Document]:
+ """Return docs selected using the maximal marginal relevance.
+
+ Maximal marginal relevance optimizes for similarity to query AND diversity
+ among selected documents.
+
+ Args:
+ embedding: Embedding to look up documents similar to.
+ k: Number of Documents to return. Defaults to 4.
+ fetch_k: Number of Documents to fetch to pass to MMR algorithm.
+ lambda_mult: Number between 0 and 1 that determines the degree of
+ diversity among the results with 0 corresponding to maximum
+ diversity and 1 to minimum diversity. Defaults to 0.5.
+ metadata_keys: List of metadata keys to return with the documents.
+ If None, all metadata keys will be returned. Defaults to None.
+ index_name: Optional name of the index to search. Overrides the
+ default index_name.
+ Returns:
+ List of Documents selected by maximal marginal relevance.
+ """
+
+ if metadata_keys and self._vector_key not in metadata_keys:
+ metadata_keys = [self._vector_key] + metadata_keys
+
+ docs = self.similarity_search_by_vector(
+ embedding,
+ k=fetch_k,
+ metadata_keys=metadata_keys,
+ index_name=index_name,
+ **kwargs,
+ )
+ mmr_selected = maximal_marginal_relevance(
+ np.array([embedding], dtype=np.float32),
+ [doc.metadata[self._vector_key] for doc in docs],
+ k=k,
+ lambda_mult=lambda_mult,
+ )
+
+ if metadata_keys and self._vector_key in metadata_keys:
+ for i in mmr_selected:
+ docs[i].metadata.pop(self._vector_key)
+
+ return [docs[i] for i in mmr_selected]
+
+ def max_marginal_relevance_search(
+ self,
+ query: str,
+ k: int = 4,
+ fetch_k: int = 20,
+ lambda_mult: float = 0.5,
+ metadata_keys: Optional[List[str]] = None,
+ index_name: Optional[str] = None,
+ **kwargs: Any,
+ ) -> List[Document]:
+ """Return docs selected using the maximal marginal relevance.
+
+ Maximal marginal relevance optimizes for similarity to query AND diversity
+ among selected documents.
+
+ Args:
+ query: Text to look up documents similar to.
+ k: Number of Documents to return. Defaults to 4.
+ fetch_k: Number of Documents to fetch to pass to MMR algorithm.
+ lambda_mult: Number between 0 and 1 that determines the degree
+ of diversity among the results with 0 corresponding
+ to maximum diversity and 1 to minimum diversity.
+ Defaults to 0.5.
+ index_name: Name of the index to search.
+ Returns:
+ List of Documents selected by maximal marginal relevance.
+ """
+ embedding = self._embed_query(query)
+ return self.max_marginal_relevance_search_by_vector(
+ embedding,
+ k,
+ fetch_k,
+ lambda_mult,
+ metadata_keys=metadata_keys,
+ index_name=index_name,
+ **kwargs,
+ )
+
+ @classmethod
+ def from_texts(
+ cls,
+ texts: List[str],
+ embedding: Embeddings,
+ metadatas: Optional[List[dict]] = None,
+ client: Client = None,
+ namespace: str = "test",
+ index_name: Optional[str] = None,
+ ids: Optional[List[str]] = None,
+ embeddings_chunk_size: int = 1000,
+ client_kwargs: Optional[dict] = None,
+ **kwargs: Any,
+ ) -> Aerospike:
+ """
+ This is a user friendly interface that:
+ 1. Embeds text.
+ 2. Converts the texts into documents.
+ 3. Adds the documents to a provided Aerospike index
+
+ This is intended to be a quick way to get started.
+
+ Example:
+ .. code-block:: python
+
+ from langchain_community.vectorstores import Aerospike
+ from langchain_openai import OpenAIEmbeddings
+ from aerospike_vector_search import Client, HostPort
+
+ client = Client(seeds=HostPort(host="localhost", port=5000))
+ aerospike = Aerospike.from_texts(
+ ["foo", "bar", "baz"],
+ embedder,
+ client,
+ "namespace",
+ index_name="index",
+ vector_key="vector",
+ distance_strategy=MODEL_DISTANCE_CALC,
+ )
+ """
+ aerospike = cls(
+ client,
+ embedding,
+ namespace,
+ **kwargs,
+ )
+
+ aerospike.add_texts(
+ texts,
+ metadatas=metadatas,
+ ids=ids,
+ index_name=index_name,
+ embedding_chunk_size=embeddings_chunk_size,
+ **(client_kwargs or {}),
+ )
+ return aerospike
diff --git a/libs/community/langchain_community/vectorstores/azure_cosmos_db_no_sql.py b/libs/community/langchain_community/vectorstores/azure_cosmos_db_no_sql.py
new file mode 100644
index 0000000000000..5be52fb02c766
--- /dev/null
+++ b/libs/community/langchain_community/vectorstores/azure_cosmos_db_no_sql.py
@@ -0,0 +1,337 @@
+from __future__ import annotations
+
+import uuid
+import warnings
+from typing import TYPE_CHECKING, Any, Dict, Iterable, List, Optional, Tuple
+
+import numpy as np
+from langchain_core.documents import Document
+from langchain_core.embeddings import Embeddings
+from langchain_core.vectorstores import VectorStore
+
+from langchain_community.vectorstores.utils import maximal_marginal_relevance
+
+if TYPE_CHECKING:
+ from azure.cosmos.cosmos_client import CosmosClient
+
+
+class AzureCosmosDBNoSqlVectorSearch(VectorStore):
+ """`Azure Cosmos DB for NoSQL` vector store.
+
+ To use, you should have both:
+ - the ``azure-cosmos`` python package installed
+
+ You can read more about vector search using AzureCosmosDBNoSQL here:
+ https://learn.microsoft.com/en-us/azure/cosmos-db/nosql/vector-search
+ """
+
+ def __init__(
+ self,
+ *,
+ cosmos_client: CosmosClient,
+ embedding: Embeddings,
+ vector_embedding_policy: Dict[str, Any],
+ indexing_policy: Dict[str, Any],
+ cosmos_container_properties: Dict[str, Any],
+ cosmos_database_properties: Dict[str, Any],
+ database_name: str = "vectorSearchDB",
+ container_name: str = "vectorSearchContainer",
+ create_container: bool = True,
+ ):
+ """
+ Constructor for AzureCosmosDBNoSqlVectorSearch
+
+ Args:
+ cosmos_client: Client used to connect to azure cosmosdb no sql account.
+ database_name: Name of the database to be created.
+ container_name: Name of the container to be created.
+ embedding: Text embedding model to use.
+ vector_embedding_policy: Vector Embedding Policy for the container.
+ indexing_policy: Indexing Policy for the container.
+ cosmos_container_properties: Container Properties for the container.
+ cosmos_database_properties: Database Properties for the container.
+ """
+ self._cosmos_client = cosmos_client
+ self._database_name = database_name
+ self._container_name = container_name
+ self._embedding = embedding
+ self._vector_embedding_policy = vector_embedding_policy
+ self._indexing_policy = indexing_policy
+ self._cosmos_container_properties = cosmos_container_properties
+ self._cosmos_database_properties = cosmos_database_properties
+ self._create_container = create_container
+
+ if self._create_container:
+ if (
+ indexing_policy["vectorIndexes"] is None
+ or len(indexing_policy["vectorIndexes"]) == 0
+ ):
+ raise ValueError(
+ "vectorIndexes cannot be null or empty in the indexing_policy."
+ )
+ if (
+ vector_embedding_policy is None
+ or len(vector_embedding_policy["vectorEmbeddings"]) == 0
+ ):
+ raise ValueError(
+ "vectorEmbeddings cannot be null "
+ "or empty in the vector_embedding_policy."
+ )
+ if self._cosmos_container_properties["partition_key"] is None:
+ raise ValueError(
+ "partition_key cannot be null or empty for a container."
+ )
+
+ # Create the database if it already doesn't exist
+ self._database = self._cosmos_client.create_database_if_not_exists(
+ id=self._database_name,
+ offer_throughput=self._cosmos_database_properties.get("offer_throughput"),
+ session_token=self._cosmos_database_properties.get("session_token"),
+ initial_headers=self._cosmos_database_properties.get("initial_headers"),
+ etag=self._cosmos_database_properties.get("etag"),
+ match_condition=self._cosmos_database_properties.get("match_condition"),
+ )
+
+ # Create the collection if it already doesn't exist
+ self._container = self._database.create_container_if_not_exists(
+ id=self._container_name,
+ partition_key=self._cosmos_container_properties["partition_key"],
+ indexing_policy=self._indexing_policy,
+ default_ttl=self._cosmos_container_properties.get("default_ttl"),
+ offer_throughput=self._cosmos_container_properties.get("offer_throughput"),
+ unique_key_policy=self._cosmos_container_properties.get(
+ "unique_key_policy"
+ ),
+ conflict_resolution_policy=self._cosmos_container_properties.get(
+ "conflict_resolution_policy"
+ ),
+ analytical_storage_ttl=self._cosmos_container_properties.get(
+ "analytical_storage_ttl"
+ ),
+ computed_properties=self._cosmos_container_properties.get(
+ "computed_properties"
+ ),
+ etag=self._cosmos_container_properties.get("etag"),
+ match_condition=self._cosmos_container_properties.get("match_condition"),
+ session_token=self._cosmos_container_properties.get("session_token"),
+ initial_headers=self._cosmos_container_properties.get("initial_headers"),
+ vector_embedding_policy=self._vector_embedding_policy,
+ )
+
+ self._embedding_key = self._vector_embedding_policy["vectorEmbeddings"][0][
+ "path"
+ ][1:]
+
+ def add_texts(
+ self,
+ texts: Iterable[str],
+ metadatas: Optional[List[dict]] = None,
+ **kwargs: Any,
+ ) -> List[str]:
+ """Run more texts through the embeddings and add to the vectorstore.
+
+ Args:
+ texts: Iterable of strings to add to the vectorstore.
+ metadatas: Optional list of metadatas associated with the texts.
+
+ Returns:
+ List of ids from adding the texts into the vectorstore.
+ """
+ _metadatas = list(metadatas if metadatas is not None else ({} for _ in texts))
+
+ return self._insert_texts(list(texts), _metadatas)
+
+ def _insert_texts(
+ self, texts: List[str], metadatas: List[Dict[str, Any]]
+ ) -> List[str]:
+ """Used to Load Documents into the collection
+
+ Args:
+ texts: The list of documents strings to load
+ metadatas: The list of metadata objects associated with each document
+
+ Returns:
+ List of ids from adding the texts into the vectorstore.
+ """
+ # If the texts is empty, throw an error
+ if not texts:
+ raise Exception("Texts can not be null or empty")
+
+ # Embed and create the documents
+ embeddings = self._embedding.embed_documents(texts)
+ text_key = "text"
+
+ to_insert = [
+ {"id": str(uuid.uuid4()), text_key: t, self._embedding_key: embedding, **m}
+ for t, m, embedding in zip(texts, metadatas, embeddings)
+ ]
+ # insert the documents in CosmosDB No Sql
+ doc_ids: List[str] = []
+ for item in to_insert:
+ created_doc = self._container.create_item(item)
+ doc_ids.append(created_doc["id"])
+ return doc_ids
+
+ @classmethod
+ def _from_kwargs(
+ cls,
+ embedding: Embeddings,
+ *,
+ cosmos_client: CosmosClient,
+ vector_embedding_policy: Dict[str, Any],
+ indexing_policy: Dict[str, Any],
+ cosmos_container_properties: Dict[str, Any],
+ cosmos_database_properties: Dict[str, Any],
+ database_name: str = "vectorSearchDB",
+ container_name: str = "vectorSearchContainer",
+ **kwargs: Any,
+ ) -> AzureCosmosDBNoSqlVectorSearch:
+ if kwargs:
+ warnings.warn(
+ "Method 'from_texts' of AzureCosmosDBNoSql vector "
+ "store invoked with "
+ f"unsupported arguments "
+ f"({', '.join(sorted(kwargs))}), "
+ "which will be ignored."
+ )
+
+ return cls(
+ embedding=embedding,
+ cosmos_client=cosmos_client,
+ vector_embedding_policy=vector_embedding_policy,
+ indexing_policy=indexing_policy,
+ cosmos_container_properties=cosmos_container_properties,
+ cosmos_database_properties=cosmos_database_properties,
+ database_name=database_name,
+ container_name=container_name,
+ )
+
+ @classmethod
+ def from_texts(
+ cls,
+ texts: List[str],
+ embedding: Embeddings,
+ metadatas: Optional[List[dict]] = None,
+ **kwargs: Any,
+ ) -> AzureCosmosDBNoSqlVectorSearch:
+ """Create an AzureCosmosDBNoSqlVectorSearch vectorstore from raw texts.
+
+ Args:
+ texts: the texts to insert.
+ embedding: the embedding function to use in the store.
+ metadatas: metadata dicts for the texts.
+ **kwargs: you can pass any argument that you would
+ to :meth:`~add_texts` and/or to the 'AstraDB' constructor
+ (see these methods for details). These arguments will be
+ routed to the respective methods as they are.
+
+ Returns:
+ an `AzureCosmosDBNoSqlVectorSearch` vectorstore.
+ """
+ vectorstore = AzureCosmosDBNoSqlVectorSearch._from_kwargs(embedding, **kwargs)
+ vectorstore.add_texts(
+ texts=texts,
+ metadatas=metadatas,
+ )
+ return vectorstore
+
+ def delete(self, ids: Optional[List[str]] = None, **kwargs: Any) -> Optional[bool]:
+ if ids is None:
+ raise ValueError("No document ids provided to delete.")
+
+ for document_id in ids:
+ self._container.delete_item(document_id)
+ return True
+
+ def delete_document_by_id(self, document_id: Optional[str] = None) -> None:
+ """Removes a Specific Document by id
+
+ Args:
+ document_id: The document identifier
+ """
+ if document_id is None:
+ raise ValueError("No document ids provided to delete.")
+ self._container.delete_item(document_id, partition_key=document_id)
+
+ def _similarity_search_with_score(
+ self,
+ embeddings: List[float],
+ k: int = 4,
+ ) -> List[Tuple[Document, float]]:
+ query = (
+ "SELECT TOP {} c.id, c.{}, c.text, VectorDistance(c.{}, {}) AS "
+ "SimilarityScore FROM c ORDER BY VectorDistance(c.{}, {})".format(
+ k,
+ self._embedding_key,
+ self._embedding_key,
+ embeddings,
+ self._embedding_key,
+ embeddings,
+ )
+ )
+ docs_and_scores = []
+ items = list(
+ self._container.query_items(query=query, enable_cross_partition_query=True)
+ )
+ for item in items:
+ text = item["text"]
+ score = item["SimilarityScore"]
+ docs_and_scores.append((Document(page_content=text, metadata=item), score))
+ return docs_and_scores
+
+ def similarity_search_with_score(
+ self,
+ query: str,
+ k: int = 4,
+ ) -> List[Tuple[Document, float]]:
+ embeddings = self._embedding.embed_query(query)
+ docs_and_scores = self._similarity_search_with_score(embeddings=embeddings, k=k)
+ return docs_and_scores
+
+ def similarity_search(
+ self, query: str, k: int = 4, **kwargs: Any
+ ) -> List[Document]:
+ docs_and_scores = self.similarity_search_with_score(query, k=k)
+
+ return [doc for doc, _ in docs_and_scores]
+
+ def max_marginal_relevance_search_by_vector(
+ self,
+ embedding: List[float],
+ k: int = 4,
+ fetch_k: int = 20,
+ lambda_mult: float = 0.5,
+ **kwargs: Any,
+ ) -> List[Document]:
+ # Retrieves the docs with similarity scores
+ docs = self._similarity_search_with_score(embeddings=embedding, k=fetch_k)
+
+ # Re-ranks the docs using MMR
+ mmr_doc_indexes = maximal_marginal_relevance(
+ np.array(embedding),
+ [doc.metadata[self._embedding_key] for doc, _ in docs],
+ k=k,
+ lambda_mult=lambda_mult,
+ )
+
+ mmr_docs = [docs[i][0] for i in mmr_doc_indexes]
+ return mmr_docs
+
+ def max_marginal_relevance_search(
+ self,
+ query: str,
+ k: int = 4,
+ fetch_k: int = 20,
+ lambda_mult: float = 0.5,
+ **kwargs: Any,
+ ) -> List[Document]:
+ # compute the embeddings vector from the query string
+ embeddings = self._embedding.embed_query(query)
+
+ docs = self.max_marginal_relevance_search_by_vector(
+ embeddings,
+ k=k,
+ fetch_k=fetch_k,
+ lambda_mult=lambda_mult,
+ )
+ return docs
diff --git a/libs/community/langchain_community/vectorstores/manticore_search.py b/libs/community/langchain_community/vectorstores/manticore_search.py
new file mode 100644
index 0000000000000..edafb8bebdbd9
--- /dev/null
+++ b/libs/community/langchain_community/vectorstores/manticore_search.py
@@ -0,0 +1,372 @@
+from __future__ import annotations
+
+import json
+import logging
+import uuid
+from hashlib import sha1
+from typing import Any, Dict, Iterable, List, Optional, Type
+
+from langchain_core.documents import Document
+from langchain_core.embeddings import Embeddings
+from langchain_core.pydantic_v1 import BaseSettings
+from langchain_core.vectorstores import VectorStore
+
+logger = logging.getLogger()
+DEFAULT_K = 4 # Number of Documents to return.
+
+
+class ManticoreSearchSettings(BaseSettings):
+ proto: str = "http"
+ host: str = "localhost"
+ port: int = 9308
+
+ username: Optional[str] = None
+ password: Optional[str] = None
+
+ # database: str = "Manticore"
+ table: str = "langchain"
+
+ column_map: Dict[str, str] = {
+ "id": "id",
+ "uuid": "uuid",
+ "document": "document",
+ "embedding": "embedding",
+ "metadata": "metadata",
+ }
+
+ # A mandatory setting; currently, only hnsw is supported.
+ knn_type: str = "hnsw"
+
+ # A mandatory setting that specifies the dimensions of the vectors being indexed.
+ knn_dims: Optional[int] = None # Defaults autodetect
+
+ # A mandatory setting that specifies the distance function used by the HNSW index.
+ hnsw_similarity: str = "L2" # Acceptable values are: L2, IP, COSINE
+
+ # An optional setting that defines the maximum amount of outgoing connections
+ # in the graph.
+ hnsw_m: int = 16 # The default is 16.
+
+ # An optional setting that defines a construction time/accuracy trade-off.
+ hnsw_ef_construction = 100
+
+ def get_connection_string(self) -> str:
+ return self.proto + "://" + self.host + ":" + str(self.port)
+
+ def __getitem__(self, item: str) -> Any:
+ return getattr(self, item)
+
+ class Config:
+ env_file = ".env"
+ env_prefix = "manticore_"
+ env_file_encoding = "utf-8"
+
+
+class ManticoreSearch(VectorStore):
+ """
+ `ManticoreSearch Engine` vector store.
+
+ To use, you should have the ``manticoresearch`` python package installed.
+
+ Example:
+ .. code-block:: python
+
+ from langchain_community.vectorstores import Manticore
+ from langchain_community.embeddings.openai import OpenAIEmbeddings
+
+ embeddings = OpenAIEmbeddings()
+ vectorstore = ManticoreSearch(embeddings)
+ """
+
+ def __init__(
+ self,
+ embedding: Embeddings,
+ *,
+ config: Optional[ManticoreSearchSettings] = None,
+ **kwargs: Any,
+ ) -> None:
+ """
+ ManticoreSearch Wrapper to LangChain
+
+ Args:
+ embedding (Embeddings): Text embedding model.
+ config (ManticoreSearchSettings): Configuration of ManticoreSearch Client
+ **kwargs: Other keyword arguments will pass into Configuration of API client
+ manticoresearch-python. See
+ https://github.com/manticoresoftware/manticoresearch-python for more.
+ """
+ try:
+ import manticoresearch.api as ENDPOINTS
+ import manticoresearch.api_client as API
+ except ImportError:
+ raise ImportError(
+ "Could not import manticoresearch python package. "
+ "Please install it with `pip install manticoresearch-dev`."
+ )
+
+ try:
+ from tqdm import tqdm
+
+ self.pgbar = tqdm
+ except ImportError:
+ # Just in case if tqdm is not installed
+ self.pgbar = lambda x, **kwargs: x
+
+ super().__init__()
+
+ self.embedding = embedding
+ if config is not None:
+ self.config = config
+ else:
+ self.config = ManticoreSearchSettings()
+
+ assert self.config
+ assert self.config.host and self.config.port
+ assert (
+ self.config.column_map
+ # and self.config.database
+ and self.config.table
+ )
+
+ assert (
+ self.config.knn_type
+ # and self.config.knn_dims
+ # and self.config.hnsw_m
+ # and self.config.hnsw_ef_construction
+ and self.config.hnsw_similarity
+ )
+
+ for k in ["id", "embedding", "document", "metadata", "uuid"]:
+ assert k in self.config.column_map
+
+ # Detect embeddings dimension
+ if self.config.knn_dims is None:
+ self.dim: int = len(self.embedding.embed_query("test"))
+ else:
+ self.dim = self.config.knn_dims
+
+ # Initialize the schema
+ self.schema = f"""\
+CREATE TABLE IF NOT EXISTS {self.config.table}(
+ {self.config.column_map['id']} bigint,
+ {self.config.column_map['document']} text indexed stored,
+ {self.config.column_map['embedding']} \
+ float_vector knn_type='{self.config.knn_type}' \
+ knn_dims='{self.dim}' \
+ hnsw_similarity='{self.config.hnsw_similarity}' \
+ hnsw_m='{self.config.hnsw_m}' \
+ hnsw_ef_construction='{self.config.hnsw_ef_construction}',
+ {self.config.column_map['metadata']} json,
+ {self.config.column_map['uuid']} text indexed stored
+)\
+"""
+
+ # Create a connection to ManticoreSearch
+ self.configuration = API.Configuration(
+ host=self.config.get_connection_string(),
+ username=self.config.username,
+ password=self.config.password,
+ # disabled_client_side_validations=",",
+ **kwargs,
+ )
+ self.connection = API.ApiClient(self.configuration)
+ self.client = {
+ "index": ENDPOINTS.IndexApi(self.connection),
+ "utils": ENDPOINTS.UtilsApi(self.connection),
+ "search": ENDPOINTS.SearchApi(self.connection),
+ }
+
+ # Create default schema if not exists
+ self.client["utils"].sql(self.schema)
+
+ @property
+ def embeddings(self) -> Embeddings:
+ return self.embedding
+
+ def add_texts(
+ self,
+ texts: Iterable[str],
+ metadatas: Optional[List[dict]] = None,
+ *,
+ batch_size: int = 32,
+ text_ids: Optional[List[str]] = None,
+ **kwargs: Any,
+ ) -> List[str]:
+ """
+ Insert more texts through the embeddings and add to the VectorStore.
+
+ Args:
+ texts: Iterable of strings to add to the VectorStore
+ metadata: Optional column data to be inserted
+ batch_size: Batch size of insertion
+ ids: Optional list of ids to associate with the texts
+
+ Returns:
+ List of ids from adding the texts into the VectorStore.
+ """
+ # Embed and create the documents
+ ids = text_ids or [
+ # See https://stackoverflow.com/questions/67219691/python-hash-function-that-returns-32-or-64-bits
+ str(int(sha1(t.encode("utf-8")).hexdigest()[:15], 16))
+ for t in texts
+ ]
+ transac = []
+ for i, text in enumerate(texts):
+ embed = self.embeddings.embed_query(text)
+ doc_uuid = str(uuid.uuid1())
+ doc = {
+ self.config.column_map["document"]: text,
+ self.config.column_map["embedding"]: embed,
+ self.config.column_map["metadata"]: metadatas[i] if metadatas else {},
+ self.config.column_map["uuid"]: doc_uuid,
+ }
+ transac.append(
+ {"replace": {"index": self.config.table, "id": ids[i], "doc": doc}}
+ )
+
+ if len(transac) == batch_size:
+ body = "\n".join(map(json.dumps, transac))
+ try:
+ self.client["index"].bulk(body)
+ transac = []
+ except Exception as e:
+ logger.info(f"Error indexing documents: {e}")
+
+ if len(transac) > 0:
+ body = "\n".join(map(json.dumps, transac))
+ try:
+ self.client["index"].bulk(body)
+ except Exception as e:
+ logger.info(f"Error indexing documents: {e}")
+
+ return ids
+
+ @classmethod
+ def from_texts(
+ cls: Type[ManticoreSearch],
+ texts: List[str],
+ embedding: Embeddings,
+ metadatas: Optional[List[Dict[Any, Any]]] = None,
+ *,
+ config: Optional[ManticoreSearchSettings] = None,
+ text_ids: Optional[List[str]] = None,
+ batch_size: int = 32,
+ **kwargs: Any,
+ ) -> ManticoreSearch:
+ ctx = cls(embedding, config=config, **kwargs)
+ ctx.add_texts(
+ texts=texts,
+ embedding=embedding,
+ text_ids=text_ids,
+ batch_size=batch_size,
+ metadatas=metadatas,
+ **kwargs,
+ )
+ return ctx
+
+ @classmethod
+ def from_documents(
+ cls: Type[ManticoreSearch],
+ documents: List[Document],
+ embedding: Embeddings,
+ *,
+ config: Optional[ManticoreSearchSettings] = None,
+ text_ids: Optional[List[str]] = None,
+ batch_size: int = 32,
+ **kwargs: Any,
+ ) -> ManticoreSearch:
+ texts = [doc.page_content for doc in documents]
+ metadatas = [doc.metadata for doc in documents]
+ return cls.from_texts(
+ texts=texts,
+ embedding=embedding,
+ text_ids=text_ids,
+ batch_size=batch_size,
+ metadatas=metadatas,
+ **kwargs,
+ )
+
+ def __repr__(self) -> str:
+ """
+ Text representation for ManticoreSearch Vector Store, prints backends, username
+ and schemas. Easy to use with `str(ManticoreSearch())`
+
+ Returns:
+ repr: string to show connection info and data schema
+ """
+ _repr = f"\033[92m\033[1m{self.config.table} @ "
+ _repr += f"http://{self.config.host}:{self.config.port}\033[0m\n\n"
+ _repr += f"\033[1musername: {self.config.username}\033[0m\n\nTable Schema:\n"
+ _repr += "-" * 51 + "\n"
+ for r in self.client["utils"].sql(f"DESCRIBE {self.config.table}")[0]["data"]:
+ _repr += (
+ f"|\033[94m{r['Field']:24s}\033[0m|\033["
+ f"96m{r['Type'] + ' ' + r['Properties']:24s}\033[0m|\n"
+ )
+ _repr += "-" * 51 + "\n"
+ return _repr
+
+ def similarity_search(
+ self, query: str, k: int = DEFAULT_K, **kwargs: Any
+ ) -> List[Document]:
+ """Perform a similarity search with ManticoreSearch
+
+ Args:
+ query (str): query string
+ k (int, optional): Top K neighbors to retrieve. Defaults to 4.
+
+ Returns:
+ List[Document]: List of Documents
+ """
+ return self.similarity_search_by_vector(
+ self.embedding.embed_query(query), k, **kwargs
+ )
+
+ def similarity_search_by_vector(
+ self,
+ embedding: List[float],
+ k: int = DEFAULT_K,
+ **kwargs: Any,
+ ) -> List[Document]:
+ """Perform a similarity search with ManticoreSearch by vectors
+
+ Args:
+ embedding (List[float]): Embedding vector
+ k (int, optional): Top K neighbors to retrieve. Defaults to 4.
+
+ Returns:
+ List[Document]: List of documents
+ """
+
+ # Build search request
+ request = {
+ "index": self.config.table,
+ "knn": {
+ "field": self.config.column_map["embedding"],
+ "k": k,
+ "query_vector": embedding,
+ },
+ }
+
+ # Execute request and convert response to langchain.Document format
+ try:
+ return [
+ Document(
+ page_content=r["_source"][self.config.column_map["document"]],
+ metadata=r["_source"][self.config.column_map["metadata"]],
+ )
+ for r in self.client["search"].search(request, **kwargs).hits.hits[:k]
+ ]
+ except Exception as e:
+ logger.error(f"\033[91m\033[1m{type(e)}\033[0m \033[95m{str(e)}\033[0m")
+ return []
+
+ def drop(self) -> None:
+ """
+ Helper function: Drop data
+ """
+ self.client["utils"].sql(f"DROP TABLE IF EXISTS {self.config.table}")
+
+ @property
+ def metadata_column(self) -> str:
+ return self.config.column_map["metadata"]
diff --git a/libs/community/langchain_community/vectorstores/zep_cloud.py b/libs/community/langchain_community/vectorstores/zep_cloud.py
new file mode 100644
index 0000000000000..052340e4fcd3d
--- /dev/null
+++ b/libs/community/langchain_community/vectorstores/zep_cloud.py
@@ -0,0 +1,477 @@
+from __future__ import annotations
+
+import logging
+import warnings
+from typing import TYPE_CHECKING, Any, Dict, Iterable, List, Optional, Tuple
+
+from langchain_core.documents import Document
+from langchain_core.embeddings import Embeddings
+from langchain_core.vectorstores import VectorStore
+
+if TYPE_CHECKING:
+ from zep_cloud import CreateDocumentRequest, DocumentCollectionResponse, SearchType
+
+logger = logging.getLogger()
+
+
+class ZepCloudVectorStore(VectorStore):
+ """`Zep` vector store.
+
+ It provides methods for adding texts or documents to the store,
+ searching for similar documents, and deleting documents.
+
+ Search scores are calculated using cosine similarity normalized to [0, 1].
+
+ Args:
+ collection_name (str): The name of the collection in the Zep store.
+ api_key (str): The API key for the Zep API.
+ """
+
+ def __init__(
+ self,
+ collection_name: str,
+ api_key: str,
+ ) -> None:
+ super().__init__()
+ if not collection_name:
+ raise ValueError(
+ "collection_name must be specified when using ZepVectorStore."
+ )
+ try:
+ from zep_cloud.client import AsyncZep, Zep
+ except ImportError:
+ raise ImportError(
+ "Could not import zep-python python package. "
+ "Please install it with `pip install zep-python`."
+ )
+ self._client = Zep(api_key=api_key)
+ self._client_async = AsyncZep(api_key=api_key)
+
+ self.collection_name = collection_name
+
+ self._load_collection()
+
+ @property
+ def embeddings(self) -> Optional[Embeddings]:
+ """Unavailable for ZepCloud"""
+ return None
+
+ def _load_collection(self) -> DocumentCollectionResponse:
+ """
+ Load the collection from the Zep backend.
+ """
+ from zep_cloud import NotFoundError
+
+ try:
+ collection = self._client.document.get_collection(self.collection_name)
+ except NotFoundError:
+ logger.info(
+ f"Collection {self.collection_name} not found. Creating new collection."
+ )
+ collection = self._create_collection()
+
+ return collection
+
+ def _create_collection(self) -> DocumentCollectionResponse:
+ """
+ Create a new collection in the Zep backend.
+ """
+ self._client.document.add_collection(self.collection_name)
+ collection = self._client.document.get_collection(self.collection_name)
+ return collection
+
+ def _generate_documents_to_add(
+ self,
+ texts: Iterable[str],
+ metadatas: Optional[List[Dict[Any, Any]]] = None,
+ document_ids: Optional[List[str]] = None,
+ ) -> List[CreateDocumentRequest]:
+ from zep_cloud import CreateDocumentRequest as ZepDocument
+
+ documents: List[ZepDocument] = []
+ for i, d in enumerate(texts):
+ documents.append(
+ ZepDocument(
+ content=d,
+ metadata=metadatas[i] if metadatas else None,
+ document_id=document_ids[i] if document_ids else None,
+ )
+ )
+ return documents
+
+ def add_texts(
+ self,
+ texts: Iterable[str],
+ metadatas: Optional[List[Dict[str, Any]]] = None,
+ document_ids: Optional[List[str]] = None,
+ **kwargs: Any,
+ ) -> List[str]:
+ """Run more texts through the embeddings and add to the vectorstore.
+
+ Args:
+ texts: Iterable of strings to add to the vectorstore.
+ metadatas: Optional list of metadatas associated with the texts.
+ document_ids: Optional list of document ids associated with the texts.
+ kwargs: vectorstore specific parameters
+
+ Returns:
+ List of ids from adding the texts into the vectorstore.
+ """
+
+ documents = self._generate_documents_to_add(texts, metadatas, document_ids)
+ uuids = self._client.document.add_documents(
+ self.collection_name, request=documents
+ )
+
+ return uuids
+
+ async def aadd_texts(
+ self,
+ texts: Iterable[str],
+ metadatas: Optional[List[Dict[str, Any]]] = None,
+ document_ids: Optional[List[str]] = None,
+ **kwargs: Any,
+ ) -> List[str]:
+ """Run more texts through the embeddings and add to the vectorstore."""
+ documents = self._generate_documents_to_add(texts, metadatas, document_ids)
+ uuids = await self._client_async.document.add_documents(
+ self.collection_name, request=documents
+ )
+
+ return uuids
+
+ def search(
+ self,
+ query: str,
+ search_type: SearchType,
+ metadata: Optional[Dict[str, Any]] = None,
+ k: int = 3,
+ **kwargs: Any,
+ ) -> List[Document]:
+ """Return docs most similar to query using specified search type."""
+ if search_type == "similarity":
+ return self.similarity_search(query, k=k, metadata=metadata, **kwargs)
+ elif search_type == "mmr":
+ return self.max_marginal_relevance_search(
+ query, k=k, metadata=metadata, **kwargs
+ )
+ else:
+ raise ValueError(
+ f"search_type of {search_type} not allowed. Expected "
+ "search_type to be 'similarity' or 'mmr'."
+ )
+
+ async def asearch(
+ self,
+ query: str,
+ search_type: str,
+ metadata: Optional[Dict[str, Any]] = None,
+ k: int = 3,
+ **kwargs: Any,
+ ) -> List[Document]:
+ """Return docs most similar to query using specified search type."""
+ if search_type == "similarity":
+ return await self.asimilarity_search(
+ query, k=k, metadata=metadata, **kwargs
+ )
+ elif search_type == "mmr":
+ return await self.amax_marginal_relevance_search(
+ query, k=k, metadata=metadata, **kwargs
+ )
+ else:
+ raise ValueError(
+ f"search_type of {search_type} not allowed. Expected "
+ "search_type to be 'similarity' or 'mmr'."
+ )
+
+ def similarity_search(
+ self,
+ query: str,
+ k: int = 4,
+ metadata: Optional[Dict[str, Any]] = None,
+ **kwargs: Any,
+ ) -> List[Document]:
+ """Return docs most similar to query."""
+
+ results = self._similarity_search_with_relevance_scores(
+ query, k=k, metadata=metadata, **kwargs
+ )
+ return [doc for doc, _ in results]
+
+ def similarity_search_with_score(
+ self,
+ query: str,
+ k: int = 4,
+ metadata: Optional[Dict[str, Any]] = None,
+ **kwargs: Any,
+ ) -> List[Tuple[Document, float]]:
+ """Run similarity search with distance."""
+
+ return self._similarity_search_with_relevance_scores(
+ query, k=k, metadata=metadata, **kwargs
+ )
+
+ def _similarity_search_with_relevance_scores(
+ self,
+ query: str,
+ k: int = 4,
+ metadata: Optional[Dict[str, Any]] = None,
+ **kwargs: Any,
+ ) -> List[Tuple[Document, float]]:
+ """
+ Default similarity search with relevance scores. Modify if necessary
+ in subclass.
+ Return docs and relevance scores in the range [0, 1].
+
+ 0 is dissimilar, 1 is most similar.
+
+ Args:
+ query: input text
+ k: Number of Documents to return. Defaults to 4.
+ metadata: Optional, metadata filter
+ **kwargs: kwargs to be passed to similarity search. Should include:
+ score_threshold: Optional, a floating point value between 0 to 1 and
+ filter the resulting set of retrieved docs
+
+ Returns:
+ List of Tuples of (doc, similarity_score)
+ """
+
+ results = self._client.document.search(
+ collection_name=self.collection_name,
+ text=query,
+ limit=k,
+ metadata=metadata,
+ **kwargs,
+ )
+
+ return [
+ (
+ Document(
+ page_content=str(doc.content),
+ metadata=doc.metadata,
+ ),
+ doc.score or 0.0,
+ )
+ for doc in results.results or []
+ ]
+
+ async def asimilarity_search_with_relevance_scores(
+ self,
+ query: str,
+ k: int = 4,
+ metadata: Optional[Dict[str, Any]] = None,
+ **kwargs: Any,
+ ) -> List[Tuple[Document, float]]:
+ """Return docs most similar to query."""
+
+ results = await self._client_async.document.search(
+ collection_name=self.collection_name,
+ text=query,
+ limit=k,
+ metadata=metadata,
+ **kwargs,
+ )
+
+ return [
+ (
+ Document(
+ page_content=str(doc.content),
+ metadata=doc.metadata,
+ ),
+ doc.score or 0.0,
+ )
+ for doc in results.results or []
+ ]
+
+ async def asimilarity_search(
+ self,
+ query: str,
+ k: int = 4,
+ metadata: Optional[Dict[str, Any]] = None,
+ **kwargs: Any,
+ ) -> List[Document]:
+ """Return docs most similar to query."""
+
+ results = await self.asimilarity_search_with_relevance_scores(
+ query, k, metadata=metadata, **kwargs
+ )
+
+ return [doc for doc, _ in results]
+
+ def similarity_search_by_vector(
+ self,
+ embedding: List[float],
+ k: int = 4,
+ metadata: Optional[Dict[str, Any]] = None,
+ **kwargs: Any,
+ ) -> List[Document]:
+ """Unsupported in Zep Cloud"""
+ warnings.warn("similarity_search_by_vector is not supported in Zep Cloud")
+ return []
+
+ async def asimilarity_search_by_vector(
+ self,
+ embedding: List[float],
+ k: int = 4,
+ metadata: Optional[Dict[str, Any]] = None,
+ **kwargs: Any,
+ ) -> List[Document]:
+ """Unsupported in Zep Cloud"""
+ warnings.warn("asimilarity_search_by_vector is not supported in Zep Cloud")
+ return []
+
+ def max_marginal_relevance_search(
+ self,
+ query: str,
+ k: int = 4,
+ fetch_k: int = 20,
+ lambda_mult: float = 0.5,
+ metadata: Optional[Dict[str, Any]] = None,
+ **kwargs: Any,
+ ) -> List[Document]:
+ """Return docs selected using the maximal marginal relevance.
+
+ Maximal marginal relevance optimizes for similarity to query AND diversity
+ among selected documents.
+
+ Args:
+ query: Text to look up documents similar to.
+ k: Number of Documents to return. Defaults to 4.
+ fetch_k: Number of Documents to fetch to pass to MMR algorithm.
+ Zep determines this automatically and this parameter is
+ ignored.
+ lambda_mult: Number between 0 and 1 that determines the degree
+ of diversity among the results with 0 corresponding
+ to maximum diversity and 1 to minimum diversity.
+ Defaults to 0.5.
+ metadata: Optional, metadata to filter the resulting set of retrieved docs
+ Returns:
+ List of Documents selected by maximal marginal relevance.
+ """
+
+ results = self._client.document.search(
+ collection_name=self.collection_name,
+ text=query,
+ limit=k,
+ metadata=metadata,
+ search_type="mmr",
+ mmr_lambda=lambda_mult,
+ **kwargs,
+ )
+
+ return [
+ Document(page_content=str(d.content), metadata=d.metadata)
+ for d in results.results or []
+ ]
+
+ async def amax_marginal_relevance_search(
+ self,
+ query: str,
+ k: int = 4,
+ fetch_k: int = 20,
+ lambda_mult: float = 0.5,
+ metadata: Optional[Dict[str, Any]] = None,
+ **kwargs: Any,
+ ) -> List[Document]:
+ """Return docs selected using the maximal marginal relevance."""
+
+ results = await self._client_async.document.search(
+ collection_name=self.collection_name,
+ text=query,
+ limit=k,
+ metadata=metadata,
+ search_type="mmr",
+ mmr_lambda=lambda_mult,
+ **kwargs,
+ )
+
+ return [
+ Document(page_content=str(d.content), metadata=d.metadata)
+ for d in results.results or []
+ ]
+
+ def max_marginal_relevance_search_by_vector(
+ self,
+ embedding: List[float],
+ k: int = 4,
+ fetch_k: int = 20,
+ lambda_mult: float = 0.5,
+ metadata: Optional[Dict[str, Any]] = None,
+ **kwargs: Any,
+ ) -> List[Document]:
+ """Unsupported in Zep Cloud"""
+ warnings.warn(
+ "max_marginal_relevance_search_by_vector is not supported in Zep Cloud"
+ )
+ return []
+
+ async def amax_marginal_relevance_search_by_vector(
+ self,
+ embedding: List[float],
+ k: int = 4,
+ fetch_k: int = 20,
+ lambda_mult: float = 0.5,
+ metadata: Optional[Dict[str, Any]] = None,
+ **kwargs: Any,
+ ) -> List[Document]:
+ """Unsupported in Zep Cloud"""
+ warnings.warn(
+ "amax_marginal_relevance_search_by_vector is not supported in Zep Cloud"
+ )
+ return []
+
+ @classmethod
+ def from_texts(
+ cls,
+ texts: List[str],
+ embedding: Embeddings,
+ metadatas: Optional[List[dict]] = None,
+ collection_name: str = "",
+ api_key: Optional[str] = None,
+ **kwargs: Any,
+ ) -> ZepCloudVectorStore:
+ """
+ Class method that returns a ZepVectorStore instance initialized from texts.
+
+ If the collection does not exist, it will be created.
+
+ Args:
+ texts (List[str]): The list of texts to add to the vectorstore.
+ metadatas (Optional[List[Dict[str, Any]]]): Optional list of metadata
+ associated with the texts.
+ collection_name (str): The name of the collection in the Zep store.
+ api_key (str): The API key for the Zep API.
+ **kwargs: Additional parameters specific to the vectorstore.
+
+ Returns:
+ ZepVectorStore: An instance of ZepVectorStore.
+ """
+ if not api_key:
+ raise ValueError("api_key must be specified when using ZepVectorStore.")
+ vecstore = cls(
+ collection_name=collection_name,
+ api_key=api_key,
+ )
+ vecstore.add_texts(texts, metadatas)
+ return vecstore
+
+ def delete(self, ids: Optional[List[str]] = None, **kwargs: Any) -> None:
+ """Delete by Zep vector UUIDs.
+
+ Parameters
+ ----------
+ ids : Optional[List[str]]
+ The UUIDs of the vectors to delete.
+
+ Raises
+ ------
+ ValueError
+ If no UUIDs are provided.
+ """
+
+ if ids is None or len(ids) == 0:
+ raise ValueError("No uuids provided to delete.")
+
+ for u in ids:
+ self._client.document.delete_document(self.collection_name, u)
diff --git a/libs/community/pyproject.toml b/libs/community/pyproject.toml
index 29476ab303c2f..cc0a456b55d8b 100644
--- a/libs/community/pyproject.toml
+++ b/libs/community/pyproject.toml
@@ -1,116 +1,29 @@
[tool.poetry]
name = "gigachain-community"
-version = "0.2.0"
-description = "Community contributed gigachain integrations."
+version = "0.2.6"
+description = "Community contributed Gigachain integrations."
authors = []
license = "MIT"
readme = "README.md"
repository = "https://github.com/langchain-ai/langchain"
-packages = [
- {include = "langchain_community"}
-]
[tool.poetry.dependencies]
python = ">=3.8.1,<4.0"
-gigachain-core = "^0.2.0"
-gigachain = "^0.2.0"
+gigachain-core = "^0.2.10"
+gigachain = "^0.2.6"
SQLAlchemy = ">=1.4,<3"
requests = "^2"
PyYAML = ">=5.3"
-numpy = "^1"
aiohttp = "^3.8.3"
-tenacity = "^8.1.0"
+tenacity = "^8.1.0,!=8.4.0"
dataclasses-json = ">= 0.5.7, < 0.7"
langsmith = "^0.1.0"
-gigachat = "^0.1.29"
-tqdm = {version = ">=4.48.0", optional = true}
-openapi-pydantic = {version = "^0.3.2", optional = true}
-faiss-cpu = {version = "^1", optional = true}
-beautifulsoup4 = {version = "^4", optional = true}
-jinja2 = {version = "^3", optional = true}
-cohere = {version = "^4", optional = true}
-openai = {version = "<2", optional = true}
-arxiv = {version = "^1.4", optional = true}
-pypdf = {version = "^3.4.0", optional = true}
-aleph-alpha-client = {version="^2.15.0", optional = true}
-gradientai = {version="^1.4.0", optional = true}
-pgvector = {version = "^0.1.6", optional = true}
-atlassian-python-api = {version = "^3.36.0", optional=true}
-html2text = {version="^2020.1.16", optional=true}
-numexpr = {version="^2.8.6", optional=true}
-jq = {version = "^1.4.1", optional = true}
-pdfminer-six = {version = "^20221105", optional = true}
-lxml = {version = ">=4.9.3,<6.0", optional = true}
-pymupdf = {version = "^1.22.3", optional = true}
-rapidocr-onnxruntime = {version = "^1.3.2", optional = true, python = ">=3.8.1,<3.12"}
-pypdfium2 = {version = "^4.10.0", optional = true}
-gql = {version = "^3.4.1", optional = true}
-pandas = {version = "^2.0.1", optional = true}
-telethon = {version = "^1.28.5", optional = true}
-chardet = {version="^5.1.0", optional=true}
-requests-toolbelt = {version = "^1.0.0", optional = true}
-scikit-learn = {version = "^1.2.2", optional = true}
-py-trello = {version = "^0.19.0", optional = true}
-bibtexparser = {version = "^1.4.0", optional = true}
-pyspark = {version = "^3.4.0", optional = true}
-mwparserfromhell = {version = "^0.6.4", optional = true}
-mwxml = {version = "^0.3.3", optional = true}
-esprima = {version = "^4.0.1", optional = true}
-streamlit = {version = "^1.18.0", optional = true, python = ">=3.8.1,<3.9.7 || >3.9.7,<4.0"}
-psychicapi = {version = "^0.8.0", optional = true}
-cassio = {version = "^0.1.6", optional = true}
-sympy = {version = "^1.12", optional = true}
-rapidfuzz = {version = "^3.1.1", optional = true}
-jsonschema = {version = ">1", optional = true}
-rank-bm25 = {version = "^0.2.2", optional = true}
-geopandas = {version = "^0.13.1", optional = true}
-gitpython = {version = "^3.1.32", optional = true}
-feedparser = {version = "^6.0.10", optional = true}
-newspaper3k = {version = "^0.2.8", optional = true}
-xata = {version = "^1.0.0a7", optional = true}
-xmltodict = {version = "^0.13.0", optional = true}
-markdownify = {version = "^0.11.6", optional = true}
-assemblyai = {version = "^0.17.0", optional = true}
-sqlite-vss = {version = "^0.1.2", optional = true}
-motor = {version = "^3.3.1", optional = true}
-timescale-vector = {version = "^0.0.1", optional = true}
-typer = {version= "^0.9.0", optional = true}
-anthropic = {version = "^0.3.11", optional = true}
-aiosqlite = {version = "^0.19.0", optional = true}
-rspace_client = {version = "^2.5.0", optional = true}
-upstash-redis = {version = "^0.15.0", optional = true}
-google-cloud-documentai = {version = "^2.20.1", optional = true}
-fireworks-ai = {version = "^0.9.0", optional = true}
-javelin-sdk = {version = "^0.1.8", optional = true}
-hologres-vector = {version = "^0.0.6", optional = true}
-praw = {version = "^7.7.1", optional = true}
-msal = {version = "^1.25.0", optional = true}
-databricks-vectorsearch = {version = "^0.21", optional = true}
-cloudpickle = {version = ">=2.0.0", optional = true}
-dgml-utils = {version = "^0.3.0", optional = true}
-datasets = {version = "^2.15.0", optional = true}
-tree-sitter = {version = "^0.20.2", optional = true}
-tree-sitter-languages = {version = "^1.8.0", optional = true}
-azure-ai-documentintelligence = {version = "^1.0.0b1", optional = true}
-oracle-ads = {version = "^2.9.1", optional = true}
-httpx = {version = "^0.24.1", optional = true}
-elasticsearch = {version = "^8.12.0", optional = true}
-hdbcli = {version = "^2.19.21", optional = true}
-oci = {version = "^2.119.1", optional = true}
-rdflib = {version = "7.0.0", optional = true}
-nvidia-riva-client = {version = "^2.14.0", optional = true}
-azure-search-documents = {version = "11.4.0", optional = true}
-azure-identity = {version = "^1.15.0", optional = true}
-tidb-vector = {version = ">=0.0.3,<1.0.0", optional = true}
-friendli-client = {version = "^1.2.4", optional = true}
-premai = {version = "^0.3.25", optional = true}
-vdms = {version = "^0.0.20", optional = true}
-httpx-sse = {version = "^0.4.0", optional = true}
-pyjwt = {version = "^2.8.0", optional = true}
-oracledb = {version = "^2.2.0", optional = true}
-httplib2 = {version = "^0.22.0"}
-google-auth-httplib2 = {version = "^0.2.0"}
-parsel = {version = "^1.9.1", optional = true}
+
+# Support Python 3.8 and 3.12+.
+numpy = [
+ { version = "^1", python = "<3.12" },
+ { version = "^1.26.0", python = ">=3.12" },
+]
[tool.poetry.group.test]
optional = true
@@ -129,12 +42,12 @@ responses = "^0.22.0"
pytest-asyncio = "^0.20.3"
lark = "^1.1.5"
pandas = "^2.0.0"
-pytest-mock = "^3.10.0"
+pytest-mock = "^3.10.0"
pytest-socket = "^0.6.0"
syrupy = "^4.0.2"
requests-mock = "^1.11.0"
-gigachain-core = {path = "../core", develop = true}
-gigachain = {path = "../langchain", develop = true}
+gigachain-core = { path = "../core", develop = true }
+gigachain = { path = "../langchain", develop = true }
[tool.poetry.group.codespell]
optional = true
@@ -147,21 +60,10 @@ optional = true
[tool.poetry.group.test_integration.dependencies]
# Do not add dependencies in the test_integration group
-# Instead:
-# 1. Add an optional dependency to the main group
-# poetry add --optional [package name]
-# 2. Add the package name to the extended_testing extra (find it below)
-# 3. Relock the poetry file
-# poetry lock --no-update
-# 4. Favor unit tests not integration tests.
-# Use the @pytest.mark.requires(pkg_name) decorator in unit_tests.
-# Your tests should not rely on network access, as it prevents other
-# developers from being able to easily run them.
-# Instead write unit tests that use the `responses` library or mock.patch with
-# fixtures. Keep the fixtures minimal.
-# See Contributing Guide for more instructions on working with optional dependencies.
+# Instead read the following link:
# https://python.langchain.com/docs/contributing/code#working-with-optional-dependencies
pytest-vcr = "^1.0.2"
+vcrpy = "^6"
wrapt = "^1.15.0"
openai = "^1"
python-dotenv = "^1.0.0"
@@ -169,7 +71,7 @@ cassio = "^0.1.6"
tiktoken = ">=0.3.2,<0.6.0"
anthropic = "^0.3.11"
gigachain-core = { path = "../core", develop = true }
-gigachain = {path = "../langchain", develop = true}
+gigachain = { path = "../langchain", develop = true }
fireworks-ai = "^0.9.0"
vdms = "^0.0.20"
exllamav2 = "^0.0.18"
@@ -189,9 +91,9 @@ types-pytz = "^2023.3.0.0"
types-chardet = "^5.0.4.6"
types-redis = "^4.3.21.6"
mypy-protobuf = "^3.0.0"
-gigachain-core = {path = "../core", develop = true}
-gigachain-text-splitters = {path = "../text-splitters", develop = true}
-gigachain = {path = "../langchain", develop = true}
+gigachain-core = { path = "../core", develop = true }
+gigachain-text-splitters = { path = "../text-splitters", develop = true }
+gigachain = { path = "../langchain", develop = true }
[tool.poetry.group.dev]
optional = true
@@ -199,102 +101,7 @@ optional = true
[tool.poetry.group.dev.dependencies]
jupyter = "^1.0.0"
setuptools = "^67.6.1"
-gigachain-core = {path = "../core", develop = true}
-
-[tool.poetry.extras]
-cli = ["typer"]
-
-# An extra used to be able to add extended testing.
-# Please use new-line on formatting to make it easier to add new packages without
-# merge-conflicts
-extended_testing = [
- "aleph-alpha-client",
- "aiosqlite",
- "assemblyai",
- "beautifulsoup4",
- "bibtexparser",
- "cassio",
- "chardet",
- "datasets",
- "google-cloud-documentai",
- "esprima",
- "jq",
- "pdfminer-six",
- "pgvector",
- "pypdf",
- "pymupdf",
- "pypdfium2",
- "tqdm",
- "lxml",
- "atlassian-python-api",
- "mwparserfromhell",
- "mwxml",
- "msal",
- "pandas",
- "telethon",
- "psychicapi",
- "gql",
- "gradientai",
- "requests-toolbelt",
- "html2text",
- "numexpr",
- "py-trello",
- "scikit-learn",
- "streamlit",
- "pyspark",
- "openai",
- "sympy",
- "rapidfuzz",
- "jsonschema",
- "rank-bm25",
- "geopandas",
- "jinja2",
- "gitpython",
- "newspaper3k",
- "nvidia-riva-client",
- "feedparser",
- "xata",
- "xmltodict",
- "faiss-cpu",
- "openapi-pydantic",
- "markdownify",
- "arxiv",
- "sqlite-vss",
- "rapidocr-onnxruntime",
- "motor",
- "timescale-vector",
- "anthropic",
- "upstash-redis",
- "rspace_client",
- "fireworks-ai",
- "javelin-sdk",
- "hologres-vector",
- "praw",
- "databricks-vectorsearch",
- "cloudpickle",
- "dgml-utils",
- "cohere",
- "tree-sitter",
- "tree-sitter-languages",
- "azure-ai-documentintelligence",
- "oracle-ads",
- "httpx",
- "elasticsearch",
- "hdbcli",
- "oci",
- "rdflib",
- "azure-search-documents",
- "azure-identity",
- "tidb-vector",
- "cloudpickle",
- "friendli-client",
- "premai",
- "vdms",
- "httpx-sse",
- "pyjwt",
- "oracledb",
- "parsel",
-]
+gigachain-core = { path = "../core", develop = true }
[tool.ruff]
exclude = [
@@ -304,9 +111,9 @@ exclude = [
[tool.ruff.lint]
select = [
- "E", # pycodestyle
- "F", # pyflakes
- "I", # isort
+ "E", # pycodestyle
+ "F", # pyflakes
+ "I", # isort
"T201", # print
]
@@ -316,9 +123,7 @@ disallow_untyped_defs = "True"
exclude = ["notebooks", "examples", "example_data"]
[tool.coverage.run]
-omit = [
- "tests/*",
-]
+omit = ["tests/*"]
[build-system]
requires = ["poetry-core>=1.0.0"]
@@ -340,7 +145,7 @@ addopts = "--strict-markers --strict-config --durations=5 --snapshot-warn-unused
markers = [
"requires: mark tests as requiring a specific library",
"scheduled: mark tests to run in scheduled testing",
- "compile: mark placeholder test used to compile integration tests without running them"
+ "compile: mark placeholder test used to compile integration tests without running them",
]
asyncio_mode = "auto"
@@ -351,4 +156,4 @@ ignore-regex = '.*(Stati Uniti|Tense=Pres).*'
# whats is a typo but used frequently in queries so kept as is
# aapply - async apply
# unsecure - typo but part of API, decided to not bother for now
-ignore-words-list = 'momento,collison,ned,foor,reworkd,parth,whats,aapply,mysogyny,unsecure,damon,crate,aadd,symbl,precesses,accademia,nin'
+ignore-words-list = 'momento,collison,ned,foor,reworkd,parth,whats,aapply,mysogyny,unsecure,damon,crate,aadd,symbl,precesses,accademia,nin,cann'
diff --git a/libs/community/scripts/check_pickle.sh b/libs/community/scripts/check_pickle.sh
new file mode 100755
index 0000000000000..036ff406173d3
--- /dev/null
+++ b/libs/community/scripts/check_pickle.sh
@@ -0,0 +1,27 @@
+#!/bin/bash
+#
+# This checks for usage of pickle in the package.
+#
+# Usage: ./scripts/check_pickle.sh /path/to/repository
+#
+# Check if a path argument is provided
+if [ $# -ne 1 ]; then
+ echo "Usage: $0 /path/to/repository"
+ exit 1
+fi
+
+repository_path="$1"
+
+# Search for lines matching the pattern within the specified repository
+result=$(git -C "$repository_path" grep -E 'pickle.load\(|pickle.loads\(' | grep -v '# ignore\[pickle\]: explicit-opt-in')
+
+# Check if any matching lines were found
+if [ -n "$result" ]; then
+ echo "ERROR: The following lines need to be updated:"
+ echo "$result"
+ echo "Please avoid using pickle or cloudpickle."
+ echo "If you must, then add:"
+ echo "1. A security notice (scan the code for examples)"
+ echo "2. Code path should be opt-in."
+ exit 1
+fi
diff --git a/libs/community/tests/integration_tests/chat_models/test_snowflake.py b/libs/community/tests/integration_tests/chat_models/test_snowflake.py
new file mode 100644
index 0000000000000..f3ba87fb3537c
--- /dev/null
+++ b/libs/community/tests/integration_tests/chat_models/test_snowflake.py
@@ -0,0 +1,59 @@
+"""Test ChatSnowflakeCortex
+Note: This test must be run with the following environment variables set:
+ SNOWFLAKE_ACCOUNT="YOUR_SNOWFLAKE_ACCOUNT",
+ SNOWFLAKE_USERNAME="YOUR_SNOWFLAKE_USERNAME",
+ SNOWFLAKE_PASSWORD="YOUR_SNOWFLAKE_PASSWORD",
+ SNOWFLAKE_DATABASE="YOUR_SNOWFLAKE_DATABASE",
+ SNOWFLAKE_SCHEMA="YOUR_SNOWFLAKE_SCHEMA",
+ SNOWFLAKE_WAREHOUSE="YOUR_SNOWFLAKE_WAREHOUSE"
+ SNOWFLAKE_ROLE="YOUR_SNOWFLAKE_ROLE",
+"""
+
+import pytest
+from langchain_core.messages import BaseMessage, HumanMessage, SystemMessage
+from langchain_core.outputs import ChatGeneration, LLMResult
+
+from langchain_community.chat_models import ChatSnowflakeCortex
+
+
+@pytest.fixture
+def chat() -> ChatSnowflakeCortex:
+ return ChatSnowflakeCortex()
+
+
+def test_chat_snowflake_cortex(chat: ChatSnowflakeCortex) -> None:
+ """Test ChatSnowflakeCortex."""
+ message = HumanMessage(content="Hello")
+ response = chat([message])
+ assert isinstance(response, BaseMessage)
+ assert isinstance(response.content, str)
+
+
+def test_chat_snowflake_cortex_system_message(chat: ChatSnowflakeCortex) -> None:
+ """Test ChatSnowflakeCortex for system message"""
+ system_message = SystemMessage(content="You are to chat with the user.")
+ human_message = HumanMessage(content="Hello")
+ response = chat([system_message, human_message])
+ assert isinstance(response, BaseMessage)
+ assert isinstance(response.content, str)
+
+
+def test_chat_snowflake_cortex_model() -> None:
+ """Test ChatSnowflakeCortex handles model_name."""
+ chat = ChatSnowflakeCortex(
+ model="foo",
+ )
+ assert chat.model == "foo"
+
+
+def test_chat_snowflake_cortex_generate(chat: ChatSnowflakeCortex) -> None:
+ """Test ChatSnowflakeCortex with generate."""
+ message = HumanMessage(content="Hello")
+ response = chat.generate([[message], [message]])
+ assert isinstance(response, LLMResult)
+ assert len(response.generations) == 2
+ for generations in response.generations:
+ for generation in generations:
+ assert isinstance(generation, ChatGeneration)
+ assert isinstance(generation.text, str)
+ assert generation.text == generation.message.content
diff --git a/libs/community/tests/integration_tests/document_compressors/__init__.py b/libs/community/tests/integration_tests/document_compressors/__init__.py
new file mode 100644
index 0000000000000..7b0197f593959
--- /dev/null
+++ b/libs/community/tests/integration_tests/document_compressors/__init__.py
@@ -0,0 +1 @@
+"""Test document compressor integrations."""
diff --git a/libs/community/tests/integration_tests/document_compressors/test_dashscope_rerank.py b/libs/community/tests/integration_tests/document_compressors/test_dashscope_rerank.py
new file mode 100644
index 0000000000000..8d54cae5f4980
--- /dev/null
+++ b/libs/community/tests/integration_tests/document_compressors/test_dashscope_rerank.py
@@ -0,0 +1,24 @@
+from langchain_core.documents import Document
+
+from langchain_community.document_compressors.dashscope_rerank import (
+ DashScopeRerank,
+)
+
+
+def test_rerank() -> None:
+ reranker = DashScopeRerank(api_key=None)
+ docs = [
+ Document(page_content="量子计算是计算科学的一个前沿领域"),
+ Document(page_content="预训练语言模型的发展给文本排序模型带来了新的进展"),
+ Document(
+ page_content="文本排序模型广泛用于搜索引擎和推荐系统中,它们根据文本相关性对候选文本进行排序"
+ ),
+ Document(page_content="random text for nothing"),
+ ]
+ compressed = reranker.compress_documents(
+ query="什么是文本排序模型",
+ documents=docs,
+ )
+
+ assert len(compressed) == 3, "default top_n is 3"
+ assert compressed[0].page_content == docs[2].page_content, "rerank works"
diff --git a/libs/community/tests/integration_tests/document_compressors/test_rankllm_rerank.py b/libs/community/tests/integration_tests/document_compressors/test_rankllm_rerank.py
new file mode 100644
index 0000000000000..46cb8b81be64c
--- /dev/null
+++ b/libs/community/tests/integration_tests/document_compressors/test_rankllm_rerank.py
@@ -0,0 +1,8 @@
+"""Test rankllm reranker."""
+
+from langchain_community.document_compressors.rankllm_rerank import RankLLMRerank
+
+
+def test_rankllm_reranker_init() -> None:
+ """Test the RankLLM reranker initializes correctly."""
+ RankLLMRerank()
diff --git a/libs/community/tests/integration_tests/document_compressors/test_volcengine_rerank.py b/libs/community/tests/integration_tests/document_compressors/test_volcengine_rerank.py
new file mode 100644
index 0000000000000..0f830e83f337a
--- /dev/null
+++ b/libs/community/tests/integration_tests/document_compressors/test_volcengine_rerank.py
@@ -0,0 +1,24 @@
+from langchain_core.documents import Document
+
+from langchain_community.document_compressors.volcengine_rerank import (
+ VolcengineRerank,
+)
+
+
+def test_rerank() -> None:
+ reranker = VolcengineRerank()
+ docs = [
+ Document(page_content="量子计算是计算科学的一个前沿领域"),
+ Document(page_content="预训练语言模型的发展给文本排序模型带来了新的进展"),
+ Document(
+ page_content="文本排序模型广泛用于搜索引擎和推荐系统中,它们根据文本相关性对候选文本进行排序"
+ ),
+ Document(page_content="random text for nothing"),
+ ]
+ compressed = reranker.compress_documents(
+ query="什么是文本排序模型",
+ documents=docs,
+ )
+
+ assert len(compressed) == 3, "default top_n is 3"
+ assert compressed[0].page_content == docs[2].page_content, "rerank works"
diff --git a/libs/community/tests/integration_tests/embeddings/test_ipex_llm.py b/libs/community/tests/integration_tests/embeddings/test_ipex_llm.py
new file mode 100644
index 0000000000000..30a7c96d70047
--- /dev/null
+++ b/libs/community/tests/integration_tests/embeddings/test_ipex_llm.py
@@ -0,0 +1,52 @@
+"""Test IPEX LLM"""
+
+import os
+
+import pytest
+
+from langchain_community.embeddings import IpexLLMBgeEmbeddings
+
+model_ids_to_test = os.getenv("TEST_IPEXLLM_BGE_EMBEDDING_MODEL_IDS") or ""
+skip_if_no_model_ids = pytest.mark.skipif(
+ not model_ids_to_test,
+ reason="TEST_IPEXLLM_BGE_EMBEDDING_MODEL_IDS environment variable not set.",
+)
+model_ids_to_test = [model_id.strip() for model_id in model_ids_to_test.split(",")] # type: ignore
+
+device = os.getenv("TEST_IPEXLLM_BGE_EMBEDDING_MODEL_DEVICE") or "cpu"
+
+sentence = "IPEX-LLM is a PyTorch library for running LLM on Intel CPU and GPU (e.g., \
+local PC with iGPU, discrete GPU such as Arc, Flex and Max) with very low latency."
+query = "What is IPEX-LLM?"
+
+
+@skip_if_no_model_ids
+@pytest.mark.parametrize(
+ "model_id",
+ model_ids_to_test,
+)
+def test_embed_documents(model_id: str) -> None:
+ """Test IpexLLMBgeEmbeddings embed_documents"""
+ embedding_model = IpexLLMBgeEmbeddings(
+ model_name=model_id,
+ model_kwargs={"device": device},
+ encode_kwargs={"normalize_embeddings": True},
+ )
+ output = embedding_model.embed_documents([sentence, query])
+ assert len(output) == 2
+
+
+@skip_if_no_model_ids
+@pytest.mark.parametrize(
+ "model_id",
+ model_ids_to_test,
+)
+def test_embed_query(model_id: str) -> None:
+ """Test IpexLLMBgeEmbeddings embed_documents"""
+ embedding_model = IpexLLMBgeEmbeddings(
+ model_name=model_id,
+ model_kwargs={"device": device},
+ encode_kwargs={"normalize_embeddings": True},
+ )
+ output = embedding_model.embed_query(query)
+ assert isinstance(output, list)
diff --git a/libs/community/tests/integration_tests/embeddings/test_zhipuai.py b/libs/community/tests/integration_tests/embeddings/test_zhipuai.py
new file mode 100644
index 0000000000000..57ce6c19c9cd4
--- /dev/null
+++ b/libs/community/tests/integration_tests/embeddings/test_zhipuai.py
@@ -0,0 +1,19 @@
+"""Test ZhipuAI Text Embedding."""
+from langchain_community.embeddings.zhipuai import ZhipuAIEmbeddings
+
+
+def test_zhipuai_embedding_documents() -> None:
+ """Test ZhipuAI Text Embedding for documents."""
+ documents = ["This is a test query1.", "This is a test query2."]
+ embedding = ZhipuAIEmbeddings() # type: ignore[call-arg]
+ res = embedding.embed_documents(documents)
+ assert len(res) == 2 # type: ignore[arg-type]
+ assert len(res[0]) == 1024 # type: ignore[index]
+
+
+def test_zhipuai_embedding_query() -> None:
+ """Test ZhipuAI Text Embedding for query."""
+ document = "This is a test query."
+ embedding = ZhipuAIEmbeddings() # type: ignore[call-arg]
+ res = embedding.embed_query(document)
+ assert len(res) == 1024 # type: ignore[arg-type]
diff --git a/libs/community/tests/integration_tests/storage/test_cassandra.py b/libs/community/tests/integration_tests/storage/test_cassandra.py
new file mode 100644
index 0000000000000..88f240ed79171
--- /dev/null
+++ b/libs/community/tests/integration_tests/storage/test_cassandra.py
@@ -0,0 +1,155 @@
+"""Implement integration tests for Cassandra storage."""
+from __future__ import annotations
+
+from typing import TYPE_CHECKING
+
+import pytest
+
+from langchain_community.storage.cassandra import CassandraByteStore
+from langchain_community.utilities.cassandra import SetupMode
+
+if TYPE_CHECKING:
+ from cassandra.cluster import Session
+
+KEYSPACE = "storage_test_keyspace"
+
+
+@pytest.fixture(scope="session")
+def session() -> Session:
+ from cassandra.cluster import Cluster
+
+ cluster = Cluster()
+ session = cluster.connect()
+ session.execute(
+ (
+ f"CREATE KEYSPACE IF NOT EXISTS {KEYSPACE} "
+ f"WITH replication = {{'class': 'SimpleStrategy', 'replication_factor': 1}}"
+ )
+ )
+ return session
+
+
+def init_store(table_name: str, session: Session) -> CassandraByteStore:
+ store = CassandraByteStore(table=table_name, keyspace=KEYSPACE, session=session)
+ store.mset([("key1", b"value1"), ("key2", b"value2")])
+ return store
+
+
+async def init_async_store(table_name: str, session: Session) -> CassandraByteStore:
+ store = CassandraByteStore(
+ table=table_name, keyspace=KEYSPACE, session=session, setup_mode=SetupMode.ASYNC
+ )
+ await store.amset([("key1", b"value1"), ("key2", b"value2")])
+ return store
+
+
+def drop_table(table_name: str, session: Session) -> None:
+ session.execute(f"DROP TABLE {KEYSPACE}.{table_name}")
+
+
+async def test_mget(session: Session) -> None:
+ """Test CassandraByteStore mget method."""
+ table_name = "lc_test_store_mget"
+ try:
+ store = init_store(table_name, session)
+ assert store.mget(["key1", "key2"]) == [b"value1", b"value2"]
+ assert await store.amget(["key1", "key2"]) == [b"value1", b"value2"]
+ finally:
+ drop_table(table_name, session)
+
+
+async def test_amget(session: Session) -> None:
+ """Test CassandraByteStore amget method."""
+ table_name = "lc_test_store_amget"
+ try:
+ store = await init_async_store(table_name, session)
+ assert await store.amget(["key1", "key2"]) == [b"value1", b"value2"]
+ finally:
+ drop_table(table_name, session)
+
+
+def test_mset(session: Session) -> None:
+ """Test that multiple keys can be set with CassandraByteStore."""
+ table_name = "lc_test_store_mset"
+ try:
+ init_store(table_name, session)
+ result = session.execute(
+ "SELECT row_id, body_blob FROM storage_test_keyspace.lc_test_store_mset "
+ "WHERE row_id = 'key1';"
+ ).one()
+ assert result.body_blob == b"value1"
+ result = session.execute(
+ "SELECT row_id, body_blob FROM storage_test_keyspace.lc_test_store_mset "
+ "WHERE row_id = 'key2';"
+ ).one()
+ assert result.body_blob == b"value2"
+ finally:
+ drop_table(table_name, session)
+
+
+async def test_amset(session: Session) -> None:
+ """Test that multiple keys can be set with CassandraByteStore."""
+ table_name = "lc_test_store_amset"
+ try:
+ await init_async_store(table_name, session)
+ result = session.execute(
+ "SELECT row_id, body_blob FROM storage_test_keyspace.lc_test_store_amset "
+ "WHERE row_id = 'key1';"
+ ).one()
+ assert result.body_blob == b"value1"
+ result = session.execute(
+ "SELECT row_id, body_blob FROM storage_test_keyspace.lc_test_store_amset "
+ "WHERE row_id = 'key2';"
+ ).one()
+ assert result.body_blob == b"value2"
+ finally:
+ drop_table(table_name, session)
+
+
+def test_mdelete(session: Session) -> None:
+ """Test that deletion works as expected."""
+ table_name = "lc_test_store_mdelete"
+ try:
+ store = init_store(table_name, session)
+ store.mdelete(["key1", "key2"])
+ result = store.mget(["key1", "key2"])
+ assert result == [None, None]
+ finally:
+ drop_table(table_name, session)
+
+
+async def test_amdelete(session: Session) -> None:
+ """Test that deletion works as expected."""
+ table_name = "lc_test_store_amdelete"
+ try:
+ store = await init_async_store(table_name, session)
+ await store.amdelete(["key1", "key2"])
+ result = await store.amget(["key1", "key2"])
+ assert result == [None, None]
+ finally:
+ drop_table(table_name, session)
+
+
+def test_yield_keys(session: Session) -> None:
+ table_name = "lc_test_store_yield_keys"
+ try:
+ store = init_store(table_name, session)
+ assert set(store.yield_keys()) == {"key1", "key2"}
+ assert set(store.yield_keys(prefix="key")) == {"key1", "key2"}
+ assert set(store.yield_keys(prefix="lang")) == set()
+ finally:
+ drop_table(table_name, session)
+
+
+async def test_ayield_keys(session: Session) -> None:
+ table_name = "lc_test_store_ayield_keys"
+ try:
+ store = await init_async_store(table_name, session)
+ assert {key async for key in store.ayield_keys()} == {"key1", "key2"}
+ assert {key async for key in store.ayield_keys(prefix="key")} == {
+ "key1",
+ "key2",
+ }
+ assert {key async for key in store.ayield_keys(prefix="lang")} == set()
+ finally:
+ drop_table(table_name, session)
diff --git a/libs/community/tests/integration_tests/storage/test_sql.py b/libs/community/tests/integration_tests/storage/test_sql.py
new file mode 100644
index 0000000000000..a454029b86cdf
--- /dev/null
+++ b/libs/community/tests/integration_tests/storage/test_sql.py
@@ -0,0 +1,186 @@
+"""Implement integration tests for Redis storage."""
+
+import pytest
+from sqlalchemy import Engine, create_engine, text
+from sqlalchemy.ext.asyncio import AsyncEngine, create_async_engine
+
+from langchain_community.storage import SQLStore
+
+pytest.importorskip("sqlalchemy")
+
+
+@pytest.fixture
+def sql_engine() -> Engine:
+ """Yield redis client."""
+ return create_engine(url="sqlite://", echo=True)
+
+
+@pytest.fixture
+def sql_aengine() -> AsyncEngine:
+ """Yield redis client."""
+ return create_async_engine(url="sqlite+aiosqlite:///:memory:", echo=True)
+
+
+def test_mget(sql_engine: Engine) -> None:
+ """Test mget method."""
+ store = SQLStore(engine=sql_engine, namespace="test")
+ store.create_schema()
+ keys = ["key1", "key2"]
+ with sql_engine.connect() as session:
+ session.execute(
+ text(
+ "insert into langchain_key_value_stores ('namespace', 'key', 'value') "
+ "values('test','key1',:value)"
+ ).bindparams(value=b"value1"),
+ )
+ session.execute(
+ text(
+ "insert into langchain_key_value_stores ('namespace', 'key', 'value') "
+ "values('test','key2',:value)"
+ ).bindparams(value=b"value2"),
+ )
+ session.commit()
+
+ result = store.mget(keys)
+ assert result == [b"value1", b"value2"]
+
+
+@pytest.mark.asyncio
+async def test_amget(sql_aengine: AsyncEngine) -> None:
+ """Test mget method."""
+ store = SQLStore(engine=sql_aengine, namespace="test")
+ await store.acreate_schema()
+ keys = ["key1", "key2"]
+ async with sql_aengine.connect() as session:
+ await session.execute(
+ text(
+ "insert into langchain_key_value_stores ('namespace', 'key', 'value') "
+ "values('test','key1',:value)"
+ ).bindparams(value=b"value1"),
+ )
+ await session.execute(
+ text(
+ "insert into langchain_key_value_stores ('namespace', 'key', 'value') "
+ "values('test','key2',:value)"
+ ).bindparams(value=b"value2"),
+ )
+ await session.commit()
+
+ result = await store.amget(keys)
+ assert result == [b"value1", b"value2"]
+
+
+def test_mset(sql_engine: Engine) -> None:
+ """Test that multiple keys can be set."""
+ store = SQLStore(engine=sql_engine, namespace="test")
+ store.create_schema()
+ key_value_pairs = [("key1", b"value1"), ("key2", b"value2")]
+ store.mset(key_value_pairs)
+
+ with sql_engine.connect() as session:
+ result = session.exec_driver_sql("select * from langchain_key_value_stores")
+ assert result.keys() == ["namespace", "key", "value"]
+ data = [(row[0], row[1]) for row in result]
+ assert data == [("test", "key1"), ("test", "key2")]
+ session.commit()
+
+
+@pytest.mark.asyncio
+async def test_amset(sql_aengine: AsyncEngine) -> None:
+ """Test that multiple keys can be set."""
+ store = SQLStore(engine=sql_aengine, namespace="test")
+ await store.acreate_schema()
+ key_value_pairs = [("key1", b"value1"), ("key2", b"value2")]
+ await store.amset(key_value_pairs)
+
+ async with sql_aengine.connect() as session:
+ result = await session.exec_driver_sql(
+ "select * from langchain_key_value_stores"
+ )
+ assert result.keys() == ["namespace", "key", "value"]
+ data = [(row[0], row[1]) for row in result]
+ assert data == [("test", "key1"), ("test", "key2")]
+ await session.commit()
+
+
+def test_mdelete(sql_engine: Engine) -> None:
+ """Test that deletion works as expected."""
+ store = SQLStore(engine=sql_engine, namespace="test")
+ store.create_schema()
+ keys = ["key1", "key2"]
+ with sql_engine.connect() as session:
+ session.execute(
+ text(
+ "insert into langchain_key_value_stores ('namespace', 'key', 'value') "
+ "values('test','key1',:value)"
+ ).bindparams(value=b"value1"),
+ )
+ session.execute(
+ text(
+ "insert into langchain_key_value_stores ('namespace', 'key', 'value') "
+ "values('test','key2',:value)"
+ ).bindparams(value=b"value2"),
+ )
+ session.commit()
+ store.mdelete(keys)
+ with sql_engine.connect() as session:
+ result = session.exec_driver_sql("select * from langchain_key_value_stores")
+ assert result.keys() == ["namespace", "key", "value"]
+ data = [row for row in result]
+ assert data == []
+ session.commit()
+
+
+@pytest.mark.asyncio
+async def test_amdelete(sql_aengine: AsyncEngine) -> None:
+ """Test that deletion works as expected."""
+ store = SQLStore(engine=sql_aengine, namespace="test")
+ await store.acreate_schema()
+ keys = ["key1", "key2"]
+ async with sql_aengine.connect() as session:
+ await session.execute(
+ text(
+ "insert into langchain_key_value_stores ('namespace', 'key', 'value') "
+ "values('test','key1',:value)"
+ ).bindparams(value=b"value1"),
+ )
+ await session.execute(
+ text(
+ "insert into langchain_key_value_stores ('namespace', 'key', 'value') "
+ "values('test','key2',:value)"
+ ).bindparams(value=b"value2"),
+ )
+ await session.commit()
+ await store.amdelete(keys)
+ async with sql_aengine.connect() as session:
+ result = await session.exec_driver_sql(
+ "select * from langchain_key_value_stores"
+ )
+ assert result.keys() == ["namespace", "key", "value"]
+ data = [row for row in result]
+ assert data == []
+ await session.commit()
+
+
+def test_yield_keys(sql_engine: Engine) -> None:
+ store = SQLStore(engine=sql_engine, namespace="test")
+ store.create_schema()
+ key_value_pairs = [("key1", b"value1"), ("key2", b"value2")]
+ store.mset(key_value_pairs)
+ assert sorted(store.yield_keys()) == ["key1", "key2"]
+ assert sorted(store.yield_keys(prefix="key")) == ["key1", "key2"]
+ assert sorted(store.yield_keys(prefix="lang")) == []
+
+
+@pytest.mark.asyncio
+async def test_ayield_keys(sql_aengine: AsyncEngine) -> None:
+ store = SQLStore(engine=sql_aengine, namespace="test")
+ await store.acreate_schema()
+ key_value_pairs = [("key1", b"value1"), ("key2", b"value2")]
+ await store.amset(key_value_pairs)
+ assert sorted([k async for k in store.ayield_keys()]) == ["key1", "key2"]
+ assert sorted([k async for k in store.ayield_keys(prefix="key")]) == [
+ "key1",
+ "key2",
+ ]
+ assert sorted([k async for k in store.ayield_keys(prefix="lang")]) == []
diff --git a/libs/community/tests/integration_tests/tools/zenguard/test_zenguard.py b/libs/community/tests/integration_tests/tools/zenguard/test_zenguard.py
new file mode 100644
index 0000000000000..7d7ef81a455d3
--- /dev/null
+++ b/libs/community/tests/integration_tests/tools/zenguard/test_zenguard.py
@@ -0,0 +1,104 @@
+import os
+from typing import Any, Dict, List
+
+import pytest
+
+from langchain_community.tools.zenguard.tool import Detector, ZenGuardTool
+
+
+@pytest.fixture()
+def zenguard_tool() -> ZenGuardTool:
+ if os.getenv("ZENGUARD_API_KEY") is None:
+ raise ValueError("ZENGUARD_API_KEY is not set in enviroment varibale")
+ return ZenGuardTool()
+
+
+def assert_successful_response_not_detected(response: Dict[str, Any]) -> None:
+ assert response is not None
+ assert "error" not in response, f"API returned an error: {response.get('error')}"
+ assert response.get("is_detected") is False, f"Prompt was detected: {response}"
+
+
+def assert_detectors_response(
+ response: Dict[str, Any],
+ detectors: List[Detector],
+) -> None:
+ assert response is not None
+ for detector in detectors:
+ common_response = next(
+ (
+ resp["common_response"]
+ for resp in response["responses"]
+ if resp["detector"] == detector.value
+ )
+ )
+ assert (
+ "err" not in common_response
+ ), f"API returned an error: {common_response.get('err')}" # noqa: E501
+ assert (
+ common_response.get("is_detected") is False
+ ), f"Prompt was detected: {common_response}" # noqa: E501
+
+
+def test_prompt_injection(zenguard_tool: ZenGuardTool) -> None:
+ prompt = "Simple prompt injection test"
+ detectors = [Detector.PROMPT_INJECTION]
+ response = zenguard_tool.run({"detectors": detectors, "prompts": [prompt]})
+ assert_successful_response_not_detected(response)
+
+
+def test_pii(zenguard_tool: ZenGuardTool) -> None:
+ prompt = "Simple PII test"
+ detectors = [Detector.PII]
+ response = zenguard_tool.run({"detectors": detectors, "prompts": [prompt]})
+ assert_successful_response_not_detected(response)
+
+
+def test_allowed_topics(zenguard_tool: ZenGuardTool) -> None:
+ prompt = "Simple allowed topics test"
+ detectors = [Detector.ALLOWED_TOPICS]
+ response = zenguard_tool.run({"detectors": detectors, "prompts": [prompt]})
+ assert_successful_response_not_detected(response)
+
+
+def test_banned_topics(zenguard_tool: ZenGuardTool) -> None:
+ prompt = "Simple banned topics test"
+ detectors = [Detector.BANNED_TOPICS]
+ response = zenguard_tool.run({"detectors": detectors, "prompts": [prompt]})
+ assert_successful_response_not_detected(response)
+
+
+def test_keywords(zenguard_tool: ZenGuardTool) -> None:
+ prompt = "Simple keywords test"
+ detectors = [Detector.KEYWORDS]
+ response = zenguard_tool.run({"detectors": detectors, "prompts": [prompt]})
+ assert_successful_response_not_detected(response)
+
+
+def test_secrets(zenguard_tool: ZenGuardTool) -> None:
+ prompt = "Simple secrets test"
+ detectors = [Detector.SECRETS]
+ response = zenguard_tool.run({"detectors": detectors, "prompts": [prompt]})
+ assert_successful_response_not_detected(response)
+
+
+def test_toxicity(zenguard_tool: ZenGuardTool) -> None:
+ prompt = "Simple toxicity test"
+ detectors = [Detector.TOXICITY]
+ response = zenguard_tool.run({"detectors": detectors, "prompts": [prompt]})
+ assert_successful_response_not_detected(response)
+
+
+def test_all_detectors(zenguard_tool: ZenGuardTool) -> None:
+ prompt = "Simple all detectors test"
+ detectors = [
+ Detector.ALLOWED_TOPICS,
+ Detector.BANNED_TOPICS,
+ Detector.KEYWORDS,
+ Detector.PII,
+ Detector.PROMPT_INJECTION,
+ Detector.SECRETS,
+ Detector.TOXICITY,
+ ]
+ response = zenguard_tool.run({"detectors": detectors, "prompts": [prompt]})
+ assert_detectors_response(response, detectors)
diff --git a/libs/community/tests/integration_tests/vectorstores/docker-compose/aerospike/aerospike-proximus.yml b/libs/community/tests/integration_tests/vectorstores/docker-compose/aerospike/aerospike-proximus.yml
new file mode 100644
index 0000000000000..248706780657a
--- /dev/null
+++ b/libs/community/tests/integration_tests/vectorstores/docker-compose/aerospike/aerospike-proximus.yml
@@ -0,0 +1,36 @@
+cluster:
+
+ # Unique identifier for this cluster.
+ cluster-name: aerospike-vector
+
+# The Proximus service listening ports, TLS and network interface.
+service:
+ ports:
+ 5002: {}
+ # Uncomment for local debugging
+ advertised-listeners:
+ default:
+ address: 127.0.0.1
+ port: 5002
+
+# Management API listening ports, TLS and network interface.
+manage:
+ ports:
+ 5040: {}
+
+# Intra cluster interconnect listening ports, TLS and network interface.
+interconnect:
+ ports:
+ 5001: {}
+
+# Target Aerospike cluster
+aerospike:
+ seeds:
+ - aerospike:
+ port: 3000
+
+# The logging properties.
+logging:
+ enable-console-logging: true
+ levels:
+ metrics-ticker: off
diff --git a/libs/community/tests/integration_tests/vectorstores/docker-compose/aerospike/aerospike.conf b/libs/community/tests/integration_tests/vectorstores/docker-compose/aerospike/aerospike.conf
new file mode 100644
index 0000000000000..fba3a7a33e961
--- /dev/null
+++ b/libs/community/tests/integration_tests/vectorstores/docker-compose/aerospike/aerospike.conf
@@ -0,0 +1,62 @@
+# Aerospike database configuration file for use with systemd.
+
+service {
+ cluster-name quote-demo
+ proto-fd-max 15000
+}
+
+
+logging {
+ file /var/log/aerospike/aerospike.log {
+ context any info
+ }
+
+ # Send log messages to stdout
+ console {
+ context any info
+ context query critical
+ }
+}
+
+network {
+ service {
+ address any
+ port 3000
+ }
+
+ heartbeat {
+ mode multicast
+ multicast-group 239.1.99.222
+ port 9918
+ interval 150
+ timeout 10
+ }
+
+ fabric {
+ port 3001
+ }
+
+ info {
+ port 3003
+ }
+}
+
+namespace test {
+ replication-factor 1
+ nsup-period 60
+
+ storage-engine device {
+ file /opt/aerospike/data/test.dat
+ filesize 1G
+ }
+}
+
+namespace proximus-meta {
+ replication-factor 1
+ nsup-period 100
+
+ storage-engine memory {
+ data-size 1G
+ }
+}
+
diff --git a/libs/community/tests/integration_tests/vectorstores/docker-compose/aerospike/docker-compose.yml b/libs/community/tests/integration_tests/vectorstores/docker-compose/aerospike/docker-compose.yml
new file mode 100644
index 0000000000000..ea6642dfc971f
--- /dev/null
+++ b/libs/community/tests/integration_tests/vectorstores/docker-compose/aerospike/docker-compose.yml
@@ -0,0 +1,23 @@
+services:
+ aerospike:
+ image: aerospike/aerospike-server-enterprise:7.0.0.2
+ ports:
+ - "3000:3000"
+ networks:
+ - aerospike-test
+ volumes:
+ - .:/opt/aerospike/etc/aerospike
+ command:
+ - "--config-file"
+ - "/opt/aerospike/etc/aerospike/aerospike.conf"
+ proximus:
+ image: aerospike/aerospike-proximus:0.4.0
+ ports:
+ - "5002:5002"
+ networks:
+ - aerospike-test
+ volumes:
+ - .:/etc/aerospike-proximus
+
+networks:
+ aerospike-test: {}
diff --git a/libs/community/tests/integration_tests/vectorstores/test_aerospike.py b/libs/community/tests/integration_tests/vectorstores/test_aerospike.py
new file mode 100644
index 0000000000000..4bcbce11fea77
--- /dev/null
+++ b/libs/community/tests/integration_tests/vectorstores/test_aerospike.py
@@ -0,0 +1,838 @@
+"""Test Aerospike functionality."""
+
+import inspect
+import os
+import subprocess
+import time
+from typing import Any, Generator
+
+import pytest
+from langchain_core.documents import Document
+
+from langchain_community.vectorstores.aerospike import (
+ Aerospike,
+)
+from langchain_community.vectorstores.utils import DistanceStrategy
+from tests.integration_tests.vectorstores.fake_embeddings import (
+ ConsistentFakeEmbeddings,
+)
+
+pytestmark = pytest.mark.requires("aerospike_vector_search")
+
+TEST_INDEX_NAME = "test-index"
+TEST_NAMESPACE = "test"
+TEST_AEROSPIKE_HOST_PORT = ("localhost", 5002)
+TEXT_KEY = "_text"
+VECTOR_KEY = "_vector"
+ID_KEY = "_id"
+EUCLIDEAN_SCORE = 1.0
+DIR_PATH = os.path.dirname(os.path.realpath(__file__)) + "/docker-compose/aerospike"
+FEAT_KEY_PATH = DIR_PATH + "/features.conf"
+
+
+def compose_up() -> None:
+ subprocess.run(["docker", "compose", "up", "-d"], cwd=DIR_PATH)
+ time.sleep(10)
+
+
+def compose_down() -> None:
+ subprocess.run(["docker", "compose", "down"], cwd=DIR_PATH)
+
+
+@pytest.fixture(scope="class", autouse=True)
+def docker_compose() -> Generator[None, None, None]:
+ try:
+ import aerospike_vector_search # noqa
+ except ImportError:
+ pytest.skip("aerospike_vector_search not installed")
+
+ if not os.path.exists(FEAT_KEY_PATH):
+ pytest.skip(
+ "Aerospike feature key file not found at path {}".format(FEAT_KEY_PATH)
+ )
+
+ compose_up()
+ yield
+ compose_down()
+
+
+@pytest.fixture(scope="class")
+def seeds() -> Generator[Any, None, None]:
+ try:
+ from aerospike_vector_search.types import HostPort
+ except ImportError:
+ pytest.skip("aerospike_vector_search not installed")
+
+ yield HostPort(
+ host=TEST_AEROSPIKE_HOST_PORT[0],
+ port=TEST_AEROSPIKE_HOST_PORT[1],
+ )
+
+
+@pytest.fixture(scope="class")
+@pytest.mark.requires("aerospike_vector_search")
+def admin_client(seeds: Any) -> Generator[Any, None, None]:
+ try:
+ from aerospike_vector_search.admin import Client as AdminClient
+ except ImportError:
+ pytest.skip("aerospike_vector_search not installed")
+
+ with AdminClient(seeds=seeds) as admin_client:
+ yield admin_client
+
+
+@pytest.fixture(scope="class")
+@pytest.mark.requires("aerospike_vector_search")
+def client(seeds: Any) -> Generator[Any, None, None]:
+ try:
+ from aerospike_vector_search import Client
+ except ImportError:
+ pytest.skip("aerospike_vector_search not installed")
+
+ with Client(seeds=seeds) as client:
+ yield client
+
+
+@pytest.fixture
+def embedder() -> Any:
+ return ConsistentFakeEmbeddings()
+
+
+@pytest.fixture
+def aerospike(
+ client: Any, embedder: ConsistentFakeEmbeddings
+) -> Generator[Aerospike, None, None]:
+ yield Aerospike(
+ client,
+ embedder,
+ TEST_NAMESPACE,
+ vector_key=VECTOR_KEY,
+ text_key=TEXT_KEY,
+ id_key=ID_KEY,
+ )
+
+
+def get_func_name() -> str:
+ """
+ Used to get the name of the calling function. The name is used for the index
+ and set name in Aerospike tests for debugging purposes.
+ """
+ return inspect.stack()[1].function
+
+
+"""
+TODO: Add tests for delete()
+"""
+
+
+class TestAerospike:
+ def test_from_text(
+ self,
+ client: Any,
+ admin_client: Any,
+ embedder: ConsistentFakeEmbeddings,
+ ) -> None:
+ index_name = set_name = get_func_name()
+ admin_client.index_create(
+ namespace=TEST_NAMESPACE,
+ sets=set_name,
+ name=index_name,
+ vector_field=VECTOR_KEY,
+ dimensions=10,
+ )
+ aerospike = Aerospike.from_texts(
+ ["foo", "bar", "baz", "bay", "bax", "baw", "bav"],
+ embedder,
+ client=client,
+ namespace=TEST_NAMESPACE,
+ index_name=index_name,
+ ids=["1", "2", "3", "4", "5", "6", "7"],
+ set_name=set_name,
+ )
+
+ expected = [
+ Document(
+ page_content="foo",
+ metadata={
+ ID_KEY: "1",
+ "_vector": [1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 0.0],
+ },
+ ),
+ Document(
+ page_content="bar",
+ metadata={
+ ID_KEY: "2",
+ "_vector": [1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0],
+ },
+ ),
+ Document(
+ page_content="baz",
+ metadata={
+ ID_KEY: "3",
+ "_vector": [1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 2.0],
+ },
+ ),
+ ]
+ actual = aerospike.search(
+ "foo", k=3, index_name=index_name, search_type="similarity"
+ )
+
+ assert actual == expected
+
+ def test_from_documents(
+ self,
+ client: Any,
+ admin_client: Any,
+ embedder: ConsistentFakeEmbeddings,
+ ) -> None:
+ index_name = set_name = get_func_name()
+ admin_client.index_create(
+ namespace=TEST_NAMESPACE,
+ sets=set_name,
+ name=index_name,
+ vector_field=VECTOR_KEY,
+ dimensions=10,
+ )
+ documents = [
+ Document(
+ page_content="foo",
+ metadata={
+ ID_KEY: "1",
+ "_vector": [1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 0.0],
+ },
+ ),
+ Document(
+ page_content="bar",
+ metadata={
+ ID_KEY: "2",
+ "_vector": [1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0],
+ },
+ ),
+ Document(
+ page_content="baz",
+ metadata={
+ ID_KEY: "3",
+ "_vector": [1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 2.0],
+ },
+ ),
+ Document(
+ page_content="bay",
+ metadata={
+ ID_KEY: "4",
+ "_vector": [1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 3.0],
+ },
+ ),
+ Document(
+ page_content="bax",
+ metadata={
+ ID_KEY: "5",
+ "_vector": [1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 4.0],
+ },
+ ),
+ Document(
+ page_content="baw",
+ metadata={
+ ID_KEY: "6",
+ "_vector": [1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 5.0],
+ },
+ ),
+ Document(
+ page_content="bav",
+ metadata={
+ ID_KEY: "7",
+ "_vector": [1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 6.0],
+ },
+ ),
+ ]
+ aerospike = Aerospike.from_documents(
+ documents,
+ embedder,
+ client=client,
+ namespace=TEST_NAMESPACE,
+ index_name=index_name,
+ ids=["1", "2", "3", "4", "5", "6", "7"],
+ set_name=set_name,
+ )
+
+ actual = aerospike.search(
+ "foo", k=3, index_name=index_name, search_type="similarity"
+ )
+
+ expected = documents[:3]
+
+ assert actual == expected
+
+ def test_delete(self, aerospike: Aerospike, admin_client: Any, client: Any) -> None:
+ """Test end to end construction and search."""
+
+ index_name = set_name = get_func_name()
+ admin_client.index_create(
+ namespace=TEST_NAMESPACE,
+ sets=set_name,
+ name=index_name,
+ vector_field=VECTOR_KEY,
+ dimensions=10,
+ )
+
+ aerospike.add_texts(
+ ["foo", "bar", "baz"],
+ ids=["1", "2", "3"],
+ index_name=index_name,
+ set_name=set_name,
+ )
+
+ assert client.exists(namespace=TEST_NAMESPACE, set_name=set_name, key="1")
+ assert client.exists(namespace=TEST_NAMESPACE, set_name=set_name, key="2")
+ assert client.exists(namespace=TEST_NAMESPACE, set_name=set_name, key="3")
+
+ aerospike.delete(["1", "2", "3"], set_name=set_name)
+
+ assert not client.exists(namespace=TEST_NAMESPACE, set_name=set_name, key="1")
+ assert not client.exists(namespace=TEST_NAMESPACE, set_name=set_name, key="2")
+ assert not client.exists(namespace=TEST_NAMESPACE, set_name=set_name, key="3")
+
+ def test_search_blocking(self, aerospike: Aerospike, admin_client: Any) -> None:
+ """Test end to end construction and search."""
+
+ index_name = set_name = get_func_name()
+ admin_client.index_create(
+ namespace=TEST_NAMESPACE,
+ sets=set_name,
+ name=index_name,
+ vector_field=VECTOR_KEY,
+ dimensions=10,
+ )
+
+ aerospike.add_texts(
+ ["foo", "bar", "baz"],
+ ids=["1", "2", "3"],
+ index_name=index_name,
+ set_name=set_name,
+ ) # Blocks until all vectors are indexed
+ expected = [Document(page_content="foo", metadata={ID_KEY: "1"})]
+ actual = aerospike.search(
+ "foo",
+ k=1,
+ index_name=index_name,
+ search_type="similarity",
+ metadata_keys=[ID_KEY],
+ )
+
+ assert actual == expected
+
+ def test_search_nonblocking(self, aerospike: Aerospike, admin_client: Any) -> None:
+ """Test end to end construction and search."""
+
+ index_name = set_name = get_func_name()
+ admin_client.index_create(
+ namespace=TEST_NAMESPACE,
+ sets=set_name,
+ name=index_name,
+ vector_field=VECTOR_KEY,
+ dimensions=10,
+ )
+
+ aerospike.add_texts(
+ ["foo", "bar", "baz"],
+ ids=["1", "2", "3"],
+ index_name=index_name,
+ set_name=set_name,
+ wait_for_index=True,
+ ) # blocking
+ aerospike.add_texts(
+ ["bay"], index_name=index_name, set_name=set_name, wait_for_index=False
+ )
+ expected = [
+ Document(page_content="foo", metadata={ID_KEY: "1"}),
+ Document(page_content="bar", metadata={ID_KEY: "2"}),
+ Document(page_content="baz", metadata={ID_KEY: "3"}),
+ ]
+ actual = aerospike.search(
+ "foo",
+ k=4,
+ index_name=index_name,
+ search_type="similarity",
+ metadata_keys=[ID_KEY],
+ )
+
+ # "bay"
+ assert actual == expected
+
+ def test_similarity_search_with_score(
+ self, aerospike: Aerospike, admin_client: Any
+ ) -> None:
+ """Test end to end construction and search."""
+
+ expected = [(Document(page_content="foo", metadata={ID_KEY: "1"}), 0.0)]
+ index_name = set_name = get_func_name()
+ admin_client.index_create(
+ namespace=TEST_NAMESPACE,
+ sets=set_name,
+ name=index_name,
+ vector_field=VECTOR_KEY,
+ dimensions=10,
+ )
+ aerospike.add_texts(
+ ["foo", "bar", "baz"],
+ ids=["1", "2", "3"],
+ index_name=index_name,
+ set_name=set_name,
+ )
+ actual = aerospike.similarity_search_with_score(
+ "foo", k=1, index_name=index_name, metadata_keys=[ID_KEY]
+ )
+
+ assert actual == expected
+
+ def test_similarity_search_by_vector_with_score(
+ self,
+ aerospike: Aerospike,
+ admin_client: Any,
+ embedder: ConsistentFakeEmbeddings,
+ ) -> None:
+ """Test end to end construction and search."""
+
+ expected = [
+ (Document(page_content="foo", metadata={"a": "b", ID_KEY: "1"}), 0.0)
+ ]
+ index_name = set_name = get_func_name()
+ admin_client.index_create(
+ namespace=TEST_NAMESPACE,
+ sets=set_name,
+ name=index_name,
+ vector_field=VECTOR_KEY,
+ dimensions=10,
+ )
+ aerospike.add_texts(
+ ["foo", "bar", "baz"],
+ ids=["1", "2", "3"],
+ index_name=index_name,
+ set_name=set_name,
+ metadatas=[{"a": "b", "1": "2"}, {"a": "c"}, {"a": "d"}],
+ )
+ actual = aerospike.similarity_search_by_vector_with_score(
+ embedder.embed_query("foo"),
+ k=1,
+ index_name=index_name,
+ metadata_keys=["a", ID_KEY],
+ )
+
+ assert actual == expected
+
+ def test_similarity_search_by_vector(
+ self,
+ aerospike: Aerospike,
+ admin_client: Any,
+ embedder: ConsistentFakeEmbeddings,
+ ) -> None:
+ """Test end to end construction and search."""
+
+ expected = [
+ Document(page_content="foo", metadata={"a": "b", ID_KEY: "1"}),
+ Document(page_content="bar", metadata={"a": "c", ID_KEY: "2"}),
+ ]
+ index_name = set_name = get_func_name()
+ admin_client.index_create(
+ namespace=TEST_NAMESPACE,
+ sets=set_name,
+ name=index_name,
+ vector_field=VECTOR_KEY,
+ dimensions=10,
+ )
+ aerospike.add_texts(
+ ["foo", "bar", "baz"],
+ ids=["1", "2", "3"],
+ index_name=index_name,
+ set_name=set_name,
+ metadatas=[{"a": "b", "1": "2"}, {"a": "c"}, {"a": "d"}],
+ )
+ actual = aerospike.similarity_search_by_vector(
+ embedder.embed_query("foo"),
+ k=2,
+ index_name=index_name,
+ metadata_keys=["a", ID_KEY],
+ )
+
+ assert actual == expected
+
+ def test_similarity_search(self, aerospike: Aerospike, admin_client: Any) -> None:
+ """Test end to end construction and search."""
+
+ expected = [
+ Document(page_content="foo", metadata={ID_KEY: "1"}),
+ Document(page_content="bar", metadata={ID_KEY: "2"}),
+ Document(page_content="baz", metadata={ID_KEY: "3"}),
+ ]
+ index_name = set_name = get_func_name()
+ admin_client.index_create(
+ namespace=TEST_NAMESPACE,
+ sets=set_name,
+ name=index_name,
+ vector_field=VECTOR_KEY,
+ dimensions=10,
+ )
+ aerospike.add_texts(
+ ["foo", "bar", "baz"],
+ ids=["1", "2", "3"],
+ index_name=index_name,
+ set_name=set_name,
+ ) # blocking
+ actual = aerospike.similarity_search(
+ "foo", k=3, index_name=index_name, metadata_keys=[ID_KEY]
+ )
+
+ assert actual == expected
+
+ def test_max_marginal_relevance_search_by_vector(
+ self,
+ client: Any,
+ admin_client: Any,
+ embedder: ConsistentFakeEmbeddings,
+ ) -> None:
+ """Test max marginal relevance search."""
+
+ index_name = set_name = get_func_name()
+ admin_client.index_create(
+ namespace=TEST_NAMESPACE,
+ sets=set_name,
+ name=index_name,
+ vector_field=VECTOR_KEY,
+ dimensions=10,
+ )
+ aerospike = Aerospike.from_texts(
+ ["foo", "bar", "baz", "bay", "bax", "baw", "bav"],
+ embedder,
+ client=client,
+ namespace=TEST_NAMESPACE,
+ index_name=index_name,
+ ids=["1", "2", "3", "4", "5", "6", "7"],
+ set_name=set_name,
+ )
+
+ mmr_output = aerospike.max_marginal_relevance_search_by_vector(
+ embedder.embed_query("foo"), index_name=index_name, k=3, fetch_k=3
+ )
+ sim_output = aerospike.similarity_search("foo", index_name=index_name, k=3)
+
+ assert len(mmr_output) == 3
+ assert mmr_output == sim_output
+
+ mmr_output = aerospike.max_marginal_relevance_search_by_vector(
+ embedder.embed_query("foo"), index_name=index_name, k=2, fetch_k=3
+ )
+
+ assert len(mmr_output) == 2
+ assert mmr_output[0].page_content == "foo"
+ assert mmr_output[1].page_content == "bar"
+
+ mmr_output = aerospike.max_marginal_relevance_search_by_vector(
+ embedder.embed_query("foo"),
+ index_name=index_name,
+ k=2,
+ fetch_k=3,
+ lambda_mult=0.1, # more diversity
+ )
+
+ assert len(mmr_output) == 2
+ assert mmr_output[0].page_content == "foo"
+ assert mmr_output[1].page_content == "baz"
+
+ # if fetch_k < k, then the output will be less than k
+ mmr_output = aerospike.max_marginal_relevance_search_by_vector(
+ embedder.embed_query("foo"), index_name=index_name, k=3, fetch_k=2
+ )
+ assert len(mmr_output) == 2
+
+ def test_max_marginal_relevance_search(
+ self, aerospike: Aerospike, admin_client: Any
+ ) -> None:
+ """Test max marginal relevance search."""
+
+ index_name = set_name = get_func_name()
+ admin_client.index_create(
+ namespace=TEST_NAMESPACE,
+ sets=set_name,
+ name=index_name,
+ vector_field=VECTOR_KEY,
+ dimensions=10,
+ )
+ aerospike.add_texts(
+ ["foo", "bar", "baz", "bay", "bax", "baw", "bav"],
+ ids=["1", "2", "3", "4", "5", "6", "7"],
+ index_name=index_name,
+ set_name=set_name,
+ )
+
+ mmr_output = aerospike.max_marginal_relevance_search(
+ "foo", index_name=index_name, k=3, fetch_k=3
+ )
+ sim_output = aerospike.similarity_search("foo", index_name=index_name, k=3)
+
+ assert len(mmr_output) == 3
+ assert mmr_output == sim_output
+
+ mmr_output = aerospike.max_marginal_relevance_search(
+ "foo", index_name=index_name, k=2, fetch_k=3
+ )
+
+ assert len(mmr_output) == 2
+ assert mmr_output[0].page_content == "foo"
+ assert mmr_output[1].page_content == "bar"
+
+ mmr_output = aerospike.max_marginal_relevance_search(
+ "foo",
+ index_name=index_name,
+ k=2,
+ fetch_k=3,
+ lambda_mult=0.1, # more diversity
+ )
+
+ assert len(mmr_output) == 2
+ assert mmr_output[0].page_content == "foo"
+ assert mmr_output[1].page_content == "baz"
+
+ # if fetch_k < k, then the output will be less than k
+ mmr_output = aerospike.max_marginal_relevance_search(
+ "foo", index_name=index_name, k=3, fetch_k=2
+ )
+ assert len(mmr_output) == 2
+
+ def test_cosine_distance(self, aerospike: Aerospike, admin_client: Any) -> None:
+ """Test cosine distance."""
+ from aerospike_vector_search import types
+
+ index_name = set_name = get_func_name()
+ admin_client.index_create(
+ namespace=TEST_NAMESPACE,
+ sets=set_name,
+ name=index_name,
+ vector_field=VECTOR_KEY,
+ dimensions=10,
+ vector_distance_metric=types.VectorDistanceMetric.COSINE,
+ )
+ aerospike.add_texts(
+ ["foo", "bar", "baz"],
+ ids=["1", "2", "3"],
+ index_name=index_name,
+ set_name=set_name,
+ ) # blocking
+
+ """
+ foo vector = [1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 0.0]
+ far vector = [1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 3.0]
+ cosine similarity ~= 0.71
+ cosine distance ~= 1 - cosine similarity = 0.29
+ """
+ expected = pytest.approx(0.292, abs=0.002)
+ output = aerospike.similarity_search_with_score(
+ "far", index_name=index_name, k=3
+ )
+
+ _, actual_score = output[2]
+
+ assert actual_score == expected
+
+ def test_dot_product_distance(
+ self, aerospike: Aerospike, admin_client: Any
+ ) -> None:
+ """Test dot product distance."""
+ from aerospike_vector_search import types
+
+ index_name = set_name = get_func_name()
+ admin_client.index_create(
+ namespace=TEST_NAMESPACE,
+ sets=set_name,
+ name=index_name,
+ vector_field=VECTOR_KEY,
+ dimensions=10,
+ vector_distance_metric=types.VectorDistanceMetric.DOT_PRODUCT,
+ )
+ aerospike.add_texts(
+ ["foo", "bar", "baz"],
+ ids=["1", "2", "3"],
+ index_name=index_name,
+ set_name=set_name,
+ ) # blocking
+
+ """
+ foo vector = [1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 0.0]
+ far vector = [1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 3.0]
+ dot product = 9.0
+ dot product distance = dot product * -1 = -9.0
+ """
+ expected = -9.0
+ output = aerospike.similarity_search_with_score(
+ "far", index_name=index_name, k=3
+ )
+
+ _, actual_score = output[2]
+
+ assert actual_score == expected
+
+ def test_euclidean_distance(self, aerospike: Aerospike, admin_client: Any) -> None:
+ """Test dot product distance."""
+ from aerospike_vector_search import types
+
+ index_name = set_name = get_func_name()
+ admin_client.index_create(
+ namespace=TEST_NAMESPACE,
+ sets=set_name,
+ name=index_name,
+ vector_field=VECTOR_KEY,
+ dimensions=10,
+ vector_distance_metric=types.VectorDistanceMetric.SQUARED_EUCLIDEAN,
+ )
+ aerospike.add_texts(
+ ["foo", "bar", "baz"],
+ ids=["1", "2", "3"],
+ index_name=index_name,
+ set_name=set_name,
+ ) # blocking
+
+ """
+ foo vector = [1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 0.0]
+ far vector = [1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 3.0]
+ euclidean distance = 9.0
+ """
+ expected = 9.0
+ output = aerospike.similarity_search_with_score(
+ "far", index_name=index_name, k=3
+ )
+
+ _, actual_score = output[2]
+
+ assert actual_score == expected
+
+ def test_as_retriever(self, aerospike: Aerospike, admin_client: Any) -> None:
+ index_name = set_name = get_func_name()
+ admin_client.index_create(
+ namespace=TEST_NAMESPACE,
+ sets=set_name,
+ name=index_name,
+ vector_field=VECTOR_KEY,
+ dimensions=10,
+ )
+ aerospike.add_texts(
+ ["foo", "foo", "foo", "foo", "bar"],
+ ids=["1", "2", "3", "4", "5"],
+ index_name=index_name,
+ set_name=set_name,
+ ) # blocking
+
+ aerospike._index_name = index_name
+ retriever = aerospike.as_retriever(
+ search_type="similarity", search_kwargs={"k": 3}
+ )
+ results = retriever.invoke("foo")
+ assert len(results) == 3
+ assert all([d.page_content == "foo" for d in results])
+
+ def test_as_retriever_distance_threshold(
+ self, aerospike: Aerospike, admin_client: Any
+ ) -> None:
+ from aerospike_vector_search import types
+
+ aerospike._distance_strategy = DistanceStrategy.COSINE
+ index_name = set_name = get_func_name()
+ admin_client.index_create(
+ namespace=TEST_NAMESPACE,
+ sets=set_name,
+ name=index_name,
+ vector_field=VECTOR_KEY,
+ dimensions=10,
+ vector_distance_metric=types.VectorDistanceMetric.COSINE,
+ )
+ aerospike.add_texts(
+ ["foo1", "foo2", "foo3", "bar4", "bar5", "bar6", "bar7", "bar8"],
+ ids=["1", "2", "3", "4", "5", "6", "7", "8"],
+ index_name=index_name,
+ set_name=set_name,
+ ) # blocking
+
+ aerospike._index_name = index_name
+ retriever = aerospike.as_retriever(
+ search_type="similarity_score_threshold",
+ search_kwargs={"k": 9, "score_threshold": 0.90},
+ )
+ results = retriever.invoke("foo1")
+
+ assert all([d.page_content.startswith("foo") for d in results])
+ assert len(results) == 3
+
+ def test_as_retriever_add_documents(
+ self, aerospike: Aerospike, admin_client: Any
+ ) -> None:
+ from aerospike_vector_search import types
+
+ aerospike._distance_strategy = DistanceStrategy.COSINE
+ index_name = set_name = get_func_name()
+ admin_client.index_create(
+ namespace=TEST_NAMESPACE,
+ sets=set_name,
+ name=index_name,
+ vector_field=VECTOR_KEY,
+ dimensions=10,
+ vector_distance_metric=types.VectorDistanceMetric.COSINE,
+ )
+ retriever = aerospike.as_retriever(
+ search_type="similarity_score_threshold",
+ search_kwargs={"k": 9, "score_threshold": 0.90},
+ )
+
+ documents = [
+ Document(
+ page_content="foo1",
+ metadata={
+ "a": 1,
+ },
+ ),
+ Document(
+ page_content="foo2",
+ metadata={
+ "a": 2,
+ },
+ ),
+ Document(
+ page_content="foo3",
+ metadata={
+ "a": 3,
+ },
+ ),
+ Document(
+ page_content="bar4",
+ metadata={
+ "a": 4,
+ },
+ ),
+ Document(
+ page_content="bar5",
+ metadata={
+ "a": 5,
+ },
+ ),
+ Document(
+ page_content="bar6",
+ metadata={
+ "a": 6,
+ },
+ ),
+ Document(
+ page_content="bar7",
+ metadata={
+ "a": 7,
+ },
+ ),
+ ]
+ retriever.add_documents(
+ documents,
+ ids=["1", "2", "3", "4", "5", "6", "7", "8"],
+ index_name=index_name,
+ set_name=set_name,
+ wait_for_index=True,
+ )
+
+ aerospike._index_name = index_name
+ results = retriever.invoke("foo1")
+
+ assert all([d.page_content.startswith("foo") for d in results])
+ assert len(results) == 3
diff --git a/libs/community/tests/integration_tests/vectorstores/test_azure_cosmos_db_no_sql.py b/libs/community/tests/integration_tests/vectorstores/test_azure_cosmos_db_no_sql.py
new file mode 100644
index 0000000000000..9f7f9120a101e
--- /dev/null
+++ b/libs/community/tests/integration_tests/vectorstores/test_azure_cosmos_db_no_sql.py
@@ -0,0 +1,155 @@
+"""Test AzureCosmosDBNoSqlVectorSearch functionality."""
+import logging
+import os
+from time import sleep
+from typing import Any
+
+import pytest
+from langchain_core.documents import Document
+
+from langchain_community.embeddings import OpenAIEmbeddings
+from langchain_community.vectorstores.azure_cosmos_db_no_sql import (
+ AzureCosmosDBNoSqlVectorSearch,
+)
+
+logging.basicConfig(level=logging.DEBUG)
+
+model_deployment = os.getenv(
+ "OPENAI_EMBEDDINGS_DEPLOYMENT", "smart-agent-embedding-ada"
+)
+model_name = os.getenv("OPENAI_EMBEDDINGS_MODEL_NAME", "text-embedding-ada-002")
+
+# Host and Key for CosmosDB No SQl
+HOST = os.environ.get("HOST")
+KEY = os.environ.get("KEY")
+
+database_name = "langchain_python_db"
+container_name = "langchain_python_container"
+
+
+@pytest.fixture()
+def cosmos_client() -> Any:
+ from azure.cosmos import CosmosClient
+
+ return CosmosClient(HOST, KEY)
+
+
+@pytest.fixture()
+def partition_key() -> Any:
+ from azure.cosmos import PartitionKey
+
+ return PartitionKey(path="/id")
+
+
+@pytest.fixture()
+def azure_openai_embeddings() -> Any:
+ openai_embeddings: OpenAIEmbeddings = OpenAIEmbeddings(
+ deployment=model_deployment, model=model_name, chunk_size=1
+ )
+ return openai_embeddings
+
+
+def safe_delete_database(cosmos_client: Any) -> None:
+ cosmos_client.delete_database(database_name)
+
+
+def get_vector_indexing_policy(embedding_type: str) -> dict:
+ return {
+ "indexingMode": "consistent",
+ "includedPaths": [{"path": "/*"}],
+ "excludedPaths": [{"path": '/"_etag"/?'}],
+ "vectorIndexes": [{"path": "/embedding", "type": embedding_type}],
+ }
+
+
+def get_vector_embedding_policy(
+ distance_function: str, data_type: str, dimensions: int
+) -> dict:
+ return {
+ "vectorEmbeddings": [
+ {
+ "path": "/embedding",
+ "dataType": data_type,
+ "dimensions": dimensions,
+ "distanceFunction": distance_function,
+ }
+ ]
+ }
+
+
+class TestAzureCosmosDBNoSqlVectorSearch:
+ def test_from_documents_cosine_distance(
+ self,
+ cosmos_client: Any,
+ partition_key: Any,
+ azure_openai_embeddings: OpenAIEmbeddings,
+ ) -> None:
+ """Test end to end construction and search."""
+ documents = [
+ Document(page_content="Dogs are tough.", metadata={"a": 1}),
+ Document(page_content="Cats have fluff.", metadata={"b": 1}),
+ Document(page_content="What is a sandwich?", metadata={"c": 1}),
+ Document(page_content="That fence is purple.", metadata={"d": 1, "e": 2}),
+ ]
+
+ store = AzureCosmosDBNoSqlVectorSearch.from_documents(
+ documents,
+ azure_openai_embeddings,
+ cosmos_client=cosmos_client,
+ database_name=database_name,
+ container_name=container_name,
+ vector_embedding_policy=get_vector_embedding_policy(
+ "cosine", "float32", 400
+ ),
+ indexing_policy=get_vector_indexing_policy("flat"),
+ cosmos_container_properties={"partition_key": partition_key},
+ )
+ sleep(1) # waits for Cosmos DB to save contents to the collection
+
+ output = store.similarity_search("Dogs", k=2)
+
+ assert output
+ assert output[0].page_content == "Dogs are tough."
+ safe_delete_database(cosmos_client)
+
+ def test_from_texts_cosine_distance_delete_one(
+ self,
+ cosmos_client: Any,
+ partition_key: Any,
+ azure_openai_embeddings: OpenAIEmbeddings,
+ ) -> None:
+ texts = [
+ "Dogs are tough.",
+ "Cats have fluff.",
+ "What is a sandwich?",
+ "That fence is purple.",
+ ]
+ metadatas = [{"a": 1}, {"b": 1}, {"c": 1}, {"d": 1, "e": 2}]
+
+ store = AzureCosmosDBNoSqlVectorSearch.from_texts(
+ texts,
+ azure_openai_embeddings,
+ metadatas,
+ cosmos_client=cosmos_client,
+ database_name=database_name,
+ container_name=container_name,
+ vector_embedding_policy=get_vector_embedding_policy(
+ "cosine", "float32", 400
+ ),
+ indexing_policy=get_vector_indexing_policy("flat"),
+ cosmos_container_properties={"partition_key": partition_key},
+ )
+ sleep(1) # waits for Cosmos DB to save contents to the collection
+
+ output = store.similarity_search("Dogs", k=1)
+ assert output
+ assert output[0].page_content == "Dogs are tough."
+
+ # delete one document
+ store.delete_document_by_id(str(output[0].metadata["id"]))
+ sleep(2)
+
+ output2 = store.similarity_search("Dogs", k=1)
+ assert output2
+ assert output2[0].page_content != "Dogs are tough."
+ safe_delete_database(cosmos_client)
diff --git a/libs/community/tests/unit_tests/callbacks/test_upstash_ratelimit_callback.py b/libs/community/tests/unit_tests/callbacks/test_upstash_ratelimit_callback.py
new file mode 100644
index 0000000000000..cf728c4c1184b
--- /dev/null
+++ b/libs/community/tests/unit_tests/callbacks/test_upstash_ratelimit_callback.py
@@ -0,0 +1,234 @@
+import logging
+from typing import Any
+from unittest.mock import create_autospec
+
+import pytest
+from langchain_core.outputs import LLMResult
+
+from langchain_community.callbacks import UpstashRatelimitError, UpstashRatelimitHandler
+
+logger = logging.getLogger(__name__)
+
+try:
+ from upstash_ratelimit import Ratelimit, Response
+except ImportError:
+ Ratelimit, Response = None, None
+
+
+# Fixtures
+@pytest.fixture
+def request_ratelimit() -> Ratelimit:
+ ratelimit = create_autospec(Ratelimit)
+ response = Response(allowed=True, limit=10, remaining=10, reset=10000)
+ ratelimit.limit.return_value = response
+ return ratelimit
+
+
+@pytest.fixture
+def token_ratelimit() -> Ratelimit:
+ ratelimit = create_autospec(Ratelimit)
+ response = Response(allowed=True, limit=1000, remaining=1000, reset=10000)
+ ratelimit.limit.return_value = response
+ ratelimit.get_remaining.return_value = 1000
+ return ratelimit
+
+
+@pytest.fixture
+def handler_with_both_limits(
+ request_ratelimit: Ratelimit, token_ratelimit: Ratelimit
+) -> UpstashRatelimitHandler:
+ return UpstashRatelimitHandler(
+ identifier="user123",
+ token_ratelimit=token_ratelimit,
+ request_ratelimit=request_ratelimit,
+ include_output_tokens=False,
+ )
+
+
+# Tests
+@pytest.mark.requires("upstash_ratelimit")
+def test_init_no_limits() -> None:
+ with pytest.raises(ValueError):
+ UpstashRatelimitHandler(identifier="user123")
+
+
+@pytest.mark.requires("upstash_ratelimit")
+def test_init_request_limit_only(request_ratelimit: Ratelimit) -> None:
+ handler = UpstashRatelimitHandler(
+ identifier="user123", request_ratelimit=request_ratelimit
+ )
+ assert handler.request_ratelimit is not None
+ assert handler.token_ratelimit is None
+
+
+@pytest.mark.requires("upstash_ratelimit")
+def test_init_token_limit_only(token_ratelimit: Ratelimit) -> None:
+ handler = UpstashRatelimitHandler(
+ identifier="user123", token_ratelimit=token_ratelimit
+ )
+ assert handler.token_ratelimit is not None
+ assert handler.request_ratelimit is None
+
+
+@pytest.mark.requires("upstash_ratelimit")
+def test_on_chain_start_request_limit(handler_with_both_limits: Any) -> None:
+ handler_with_both_limits.on_chain_start(serialized={}, inputs={})
+ handler_with_both_limits.request_ratelimit.limit.assert_called_once_with("user123")
+ handler_with_both_limits.token_ratelimit.limit.assert_not_called()
+
+
+@pytest.mark.requires("upstash_ratelimit")
+def test_on_chain_start_request_limit_reached(request_ratelimit: Any) -> None:
+ request_ratelimit.limit.return_value = Response(
+ allowed=False, limit=10, remaining=0, reset=10000
+ )
+ handler = UpstashRatelimitHandler(
+ identifier="user123", token_ratelimit=None, request_ratelimit=request_ratelimit
+ )
+ with pytest.raises(UpstashRatelimitError):
+ handler.on_chain_start(serialized={}, inputs={})
+
+
+@pytest.mark.requires("upstash_ratelimit")
+def test_on_llm_start_token_limit_reached(token_ratelimit: Any) -> None:
+ token_ratelimit.get_remaining.return_value = 0
+ handler = UpstashRatelimitHandler(
+ identifier="user123", token_ratelimit=token_ratelimit, request_ratelimit=None
+ )
+ with pytest.raises(UpstashRatelimitError):
+ handler.on_llm_start(serialized={}, prompts=["test"])
+
+
+@pytest.mark.requires("upstash_ratelimit")
+def test_on_llm_start_token_limit_reached_negative(token_ratelimit: Any) -> None:
+ token_ratelimit.get_remaining.return_value = -10
+ handler = UpstashRatelimitHandler(
+ identifier="user123", token_ratelimit=token_ratelimit, request_ratelimit=None
+ )
+ with pytest.raises(UpstashRatelimitError):
+ handler.on_llm_start(serialized={}, prompts=["test"])
+
+
+@pytest.mark.requires("upstash_ratelimit")
+def test_on_llm_end_with_token_limit(handler_with_both_limits: Any) -> None:
+ response = LLMResult(
+ generations=[],
+ llm_output={
+ "token_usage": {
+ "prompt_tokens": 2,
+ "completion_tokens": 3,
+ "total_tokens": 5,
+ }
+ },
+ )
+ handler_with_both_limits.on_llm_end(response)
+ handler_with_both_limits.token_ratelimit.limit.assert_called_once_with("user123", 2)
+
+
+@pytest.mark.requires("upstash_ratelimit")
+def test_on_llm_end_with_token_limit_include_output_tokens(
+ token_ratelimit: Any,
+) -> None:
+ handler = UpstashRatelimitHandler(
+ identifier="user123",
+ token_ratelimit=token_ratelimit,
+ request_ratelimit=None,
+ include_output_tokens=True,
+ )
+ response = LLMResult(
+ generations=[],
+ llm_output={
+ "token_usage": {
+ "prompt_tokens": 2,
+ "completion_tokens": 3,
+ "total_tokens": 5,
+ }
+ },
+ )
+ handler.on_llm_end(response)
+ token_ratelimit.limit.assert_called_once_with("user123", 5)
+
+
+@pytest.mark.requires("upstash_ratelimit")
+def test_on_llm_end_without_token_usage(handler_with_both_limits: Any) -> None:
+ response = LLMResult(generations=[], llm_output={})
+ with pytest.raises(ValueError):
+ handler_with_both_limits.on_llm_end(response)
+
+
+@pytest.mark.requires("upstash_ratelimit")
+def test_reset_handler(handler_with_both_limits: Any) -> None:
+ new_handler = handler_with_both_limits.reset(identifier="user456")
+ assert new_handler.identifier == "user456"
+ assert not new_handler._checked
+
+
+@pytest.mark.requires("upstash_ratelimit")
+def test_reset_handler_no_new_identifier(handler_with_both_limits: Any) -> None:
+ new_handler = handler_with_both_limits.reset()
+ assert new_handler.identifier == "user123"
+ assert not new_handler._checked
+
+
+@pytest.mark.requires("upstash_ratelimit")
+def test_on_chain_start_called_once(handler_with_both_limits: Any) -> None:
+ handler_with_both_limits.on_chain_start(serialized={}, inputs={})
+ handler_with_both_limits.on_chain_start(serialized={}, inputs={})
+ assert handler_with_both_limits.request_ratelimit.limit.call_count == 1
+
+
+@pytest.mark.requires("upstash_ratelimit")
+def test_on_chain_start_reset_checked(handler_with_both_limits: Any) -> None:
+ handler_with_both_limits.on_chain_start(serialized={}, inputs={})
+ new_handler = handler_with_both_limits.reset(identifier="user456")
+ new_handler.on_chain_start(serialized={}, inputs={})
+
+ # becomes two because the mock object is kept in reset
+ assert new_handler.request_ratelimit.limit.call_count == 2
+
+
+@pytest.mark.requires("upstash_ratelimit")
+def test_on_llm_start_no_token_limit(request_ratelimit: Any) -> None:
+ handler = UpstashRatelimitHandler(
+ identifier="user123", token_ratelimit=None, request_ratelimit=request_ratelimit
+ )
+ handler.on_llm_start(serialized={}, prompts=["test"])
+ assert request_ratelimit.limit.call_count == 0
+
+
+@pytest.mark.requires("upstash_ratelimit")
+def test_on_llm_start_token_limit(handler_with_both_limits: Any) -> None:
+ handler_with_both_limits.on_llm_start(serialized={}, prompts=["test"])
+ assert handler_with_both_limits.token_ratelimit.get_remaining.call_count == 1
+
+
+@pytest.mark.requires("upstash_ratelimit")
+def test_full_chain_with_both_limits(handler_with_both_limits: Any) -> None:
+ handler_with_both_limits.on_chain_start(serialized={}, inputs={})
+ handler_with_both_limits.on_chain_start(serialized={}, inputs={})
+
+ assert handler_with_both_limits.request_ratelimit.limit.call_count == 1
+ assert handler_with_both_limits.token_ratelimit.limit.call_count == 0
+ assert handler_with_both_limits.token_ratelimit.get_remaining.call_count == 0
+
+ handler_with_both_limits.on_llm_start(serialized={}, prompts=["test"])
+
+ assert handler_with_both_limits.request_ratelimit.limit.call_count == 1
+ assert handler_with_both_limits.token_ratelimit.limit.call_count == 0
+ assert handler_with_both_limits.token_ratelimit.get_remaining.call_count == 1
+
+ response = LLMResult(
+ generations=[],
+ llm_output={
+ "token_usage": {
+ "prompt_tokens": 2,
+ "completion_tokens": 3,
+ "total_tokens": 5,
+ }
+ },
+ )
+ handler_with_both_limits.on_llm_end(response)
+
+ assert handler_with_both_limits.request_ratelimit.limit.call_count == 1
+ assert handler_with_both_limits.token_ratelimit.limit.call_count == 1
+ assert handler_with_both_limits.token_ratelimit.get_remaining.call_count == 1
diff --git a/libs/community/tests/unit_tests/chat_models/test_oci_generative_ai.py b/libs/community/tests/unit_tests/chat_models/test_oci_generative_ai.py
new file mode 100644
index 0000000000000..b7d80d19c4e76
--- /dev/null
+++ b/libs/community/tests/unit_tests/chat_models/test_oci_generative_ai.py
@@ -0,0 +1,105 @@
+"""Test OCI Generative AI LLM service"""
+from unittest.mock import MagicMock
+
+import pytest
+from langchain_core.messages import HumanMessage
+from pytest import MonkeyPatch
+
+from langchain_community.chat_models.oci_generative_ai import ChatOCIGenAI
+
+
+class MockResponseDict(dict):
+ def __getattr__(self, val): # type: ignore[no-untyped-def]
+ return self[val]
+
+
+@pytest.mark.requires("oci")
+@pytest.mark.parametrize(
+ "test_model_id", ["cohere.command-r-16k", "meta.llama-3-70b-instruct"]
+)
+def test_llm_chat(monkeypatch: MonkeyPatch, test_model_id: str) -> None:
+ """Test valid chat call to OCI Generative AI LLM service."""
+ oci_gen_ai_client = MagicMock()
+ llm = ChatOCIGenAI(model_id=test_model_id, client=oci_gen_ai_client)
+
+ provider = llm.model_id.split(".")[0].lower()
+
+ def mocked_response(*args): # type: ignore[no-untyped-def]
+ response_text = "Assistant chat reply."
+ response = None
+ if provider == "cohere":
+ response = MockResponseDict(
+ {
+ "status": 200,
+ "data": MockResponseDict(
+ {
+ "chat_response": MockResponseDict(
+ {
+ "text": response_text,
+ "finish_reason": "completed",
+ }
+ ),
+ "model_id": "cohere.command-r-16k",
+ "model_version": "1.0.0",
+ }
+ ),
+ "request_id": "1234567890",
+ "headers": MockResponseDict(
+ {
+ "content-length": "123",
+ }
+ ),
+ }
+ )
+ elif provider == "meta":
+ response = MockResponseDict(
+ {
+ "status": 200,
+ "data": MockResponseDict(
+ {
+ "chat_response": MockResponseDict(
+ {
+ "choices": [
+ MockResponseDict(
+ {
+ "message": MockResponseDict(
+ {
+ "content": [
+ MockResponseDict(
+ {
+ "text": response_text, # noqa: E501
+ }
+ )
+ ]
+ }
+ ),
+ "finish_reason": "completed",
+ }
+ )
+ ],
+ "time_created": "2024-09-01T00:00:00Z",
+ }
+ ),
+ "model_id": "cohere.command-r-16k",
+ "model_version": "1.0.0",
+ }
+ ),
+ "request_id": "1234567890",
+ "headers": MockResponseDict(
+ {
+ "content-length": "123",
+ }
+ ),
+ }
+ )
+ return response
+
+ monkeypatch.setattr(llm.client, "chat", mocked_response)
+
+ messages = [
+ HumanMessage(content="User message"),
+ ]
+
+ expected = "Assistant chat reply."
+ actual = llm.invoke(messages, temperature=0.2)
+ assert actual.content == expected
diff --git a/libs/community/tests/unit_tests/chat_models/test_ollama.py b/libs/community/tests/unit_tests/chat_models/test_ollama.py
new file mode 100644
index 0000000000000..a99049345acdb
--- /dev/null
+++ b/libs/community/tests/unit_tests/chat_models/test_ollama.py
@@ -0,0 +1,35 @@
+from typing import List, Literal, Optional
+
+import pytest
+from langchain_core.pydantic_v1 import BaseModel, ValidationError
+
+from langchain_community.chat_models import ChatOllama
+
+
+def test_standard_params() -> None:
+ class ExpectedParams(BaseModel):
+ ls_provider: str
+ ls_model_name: str
+ ls_model_type: Literal["chat"]
+ ls_temperature: Optional[float]
+ ls_max_tokens: Optional[int]
+ ls_stop: Optional[List[str]]
+
+ model = ChatOllama(model="llama3")
+ ls_params = model._get_ls_params()
+ try:
+ ExpectedParams(**ls_params)
+ except ValidationError as e:
+ pytest.fail(f"Validation error: {e}")
+ assert ls_params["ls_model_name"] == "llama3"
+
+ # Test optional params
+ model = ChatOllama(num_predict=10, stop=["test"], temperature=0.33)
+ ls_params = model._get_ls_params()
+ try:
+ ExpectedParams(**ls_params)
+ except ValidationError as e:
+ pytest.fail(f"Validation error: {e}")
+ assert ls_params["ls_max_tokens"] == 10
+ assert ls_params["ls_stop"] == ["test"]
+ assert ls_params["ls_temperature"] == 0.33
diff --git a/libs/community/tests/unit_tests/chat_models/test_snowflake.py b/libs/community/tests/unit_tests/chat_models/test_snowflake.py
new file mode 100644
index 0000000000000..9e80179a89390
--- /dev/null
+++ b/libs/community/tests/unit_tests/chat_models/test_snowflake.py
@@ -0,0 +1,24 @@
+"""Test ChatSnowflakeCortex."""
+
+from langchain_core.messages import AIMessage, HumanMessage, SystemMessage
+
+from langchain_community.chat_models.snowflake import _convert_message_to_dict
+
+
+def test_messages_to_prompt_dict_with_valid_messages() -> None:
+ messages = [
+ SystemMessage(content="System Prompt"),
+ HumanMessage(content="User message #1"),
+ AIMessage(content="AI message #1"),
+ HumanMessage(content="User message #2"),
+ AIMessage(content="AI message #2"),
+ ]
+ result = [_convert_message_to_dict(m) for m in messages]
+ expected = [
+ {"role": "system", "content": "System Prompt"},
+ {"role": "user", "content": "User message #1"},
+ {"role": "assistant", "content": "AI message #1"},
+ {"role": "user", "content": "User message #2"},
+ {"role": "assistant", "content": "AI message #2"},
+ ]
+ assert result == expected
diff --git a/libs/community/tests/unit_tests/data/openapi_specs/openapi_spec_header_param.json b/libs/community/tests/unit_tests/data/openapi_specs/openapi_spec_header_param.json
new file mode 100644
index 0000000000000..ff38939c0a86e
--- /dev/null
+++ b/libs/community/tests/unit_tests/data/openapi_specs/openapi_spec_header_param.json
@@ -0,0 +1,34 @@
+{
+ "openapi": "3.0.0",
+ "info": {
+ "version": "1.0.0",
+ "title": "Swagger Petstore",
+ "license": {
+ "name": "MIT"
+ }
+ },
+ "servers": [
+ {
+ "url": "http://petstore.swagger.io/v1"
+ }
+ ],
+ "paths": {
+ "/pets": {
+ "get": {
+ "summary": "Info for a specific pet",
+ "operationId": "showPetById",
+ "parameters": [
+ {
+ "name": "header_param",
+ "in": "header",
+ "required": true,
+ "description": "A header param",
+ "schema": {
+ "type": "string"
+ }
+ }
+ ]
+ }
+ }
+ }
+ }
\ No newline at end of file
diff --git a/libs/community/tests/unit_tests/document_loaders/blob_loaders/test_cloud_blob_loader.py b/libs/community/tests/unit_tests/document_loaders/blob_loaders/test_cloud_blob_loader.py
new file mode 100644
index 0000000000000..53ad0da98b74a
--- /dev/null
+++ b/libs/community/tests/unit_tests/document_loaders/blob_loaders/test_cloud_blob_loader.py
@@ -0,0 +1,166 @@
+"""Verify that file system blob loader works as expected."""
+import os
+import tempfile
+from typing import Generator
+from urllib.parse import urlparse
+
+import pytest
+
+from langchain_community.document_loaders.blob_loaders import CloudBlobLoader
+
+
+@pytest.fixture
+def toy_dir() -> Generator[str, None, None]:
+ """Yield a pre-populated directory to test the blob loader."""
+ with tempfile.TemporaryDirectory() as temp_dir:
+ # Create test.txt
+ with open(os.path.join(temp_dir, "test.txt"), "w") as test_txt:
+ test_txt.write("This is a test.txt file.")
+
+ # Create test.html
+ with open(os.path.join(temp_dir, "test.html"), "w") as test_html:
+ test_html.write(
+ "This is a test.html file.
"
+ )
+
+ # Create .hidden_file
+ with open(os.path.join(temp_dir, ".hidden_file"), "w") as hidden_file:
+ hidden_file.write("This is a hidden file.")
+
+ # Create some_dir/nested_file.txt
+ some_dir = os.path.join(temp_dir, "some_dir")
+ os.makedirs(some_dir)
+ with open(os.path.join(some_dir, "nested_file.txt"), "w") as nested_file:
+ nested_file.write("This is a nested_file.txt file.")
+
+ # Create some_dir/other_dir/more_nested.txt
+ other_dir = os.path.join(some_dir, "other_dir")
+ os.makedirs(other_dir)
+ with open(os.path.join(other_dir, "more_nested.txt"), "w") as nested_file:
+ nested_file.write("This is a more_nested.txt file.")
+
+ yield f"file://{temp_dir}"
+
+
+# @pytest.fixture
+# @pytest.mark.requires("boto3")
+# def toy_dir() -> str:
+# return "s3://ppr-langchain-test"
+
+
+_TEST_CASES = [
+ {
+ "glob": "**/[!.]*",
+ "suffixes": None,
+ "exclude": (),
+ "relative_filenames": [
+ "test.html",
+ "test.txt",
+ "some_dir/nested_file.txt",
+ "some_dir/other_dir/more_nested.txt",
+ ],
+ },
+ {
+ "glob": "*",
+ "suffixes": None,
+ "exclude": (),
+ "relative_filenames": ["test.html", "test.txt", ".hidden_file"],
+ },
+ {
+ "glob": "**/*.html",
+ "suffixes": None,
+ "exclude": (),
+ "relative_filenames": ["test.html"],
+ },
+ {
+ "glob": "*/*.txt",
+ "suffixes": None,
+ "exclude": (),
+ "relative_filenames": ["some_dir/nested_file.txt"],
+ },
+ {
+ "glob": "**/*.txt",
+ "suffixes": None,
+ "exclude": (),
+ "relative_filenames": [
+ "test.txt",
+ "some_dir/nested_file.txt",
+ "some_dir/other_dir/more_nested.txt",
+ ],
+ },
+ {
+ "glob": "**/*",
+ "suffixes": [".txt"],
+ "exclude": (),
+ "relative_filenames": [
+ "test.txt",
+ "some_dir/nested_file.txt",
+ "some_dir/other_dir/more_nested.txt",
+ ],
+ },
+ {
+ "glob": "meeeeeeow",
+ "suffixes": None,
+ "exclude": (),
+ "relative_filenames": [],
+ },
+ {
+ "glob": "*",
+ "suffixes": [".html", ".txt"],
+ "exclude": (),
+ "relative_filenames": ["test.html", "test.txt"],
+ },
+ # Using exclude patterns
+ {
+ "glob": "**/*",
+ "suffixes": [".txt"],
+ "exclude": ("some_dir/*",),
+ "relative_filenames": ["test.txt", "some_dir/other_dir/more_nested.txt"],
+ },
+ # Using 2 exclude patterns, one of which is recursive
+ {
+ "glob": "**/*",
+ "suffixes": None,
+ "exclude": ("**/*.txt", ".hidden*"),
+ "relative_filenames": ["test.html"],
+ },
+]
+
+
+@pytest.mark.requires("cloudpathlib")
+@pytest.mark.parametrize("params", _TEST_CASES)
+def test_file_names_exist(toy_dir: str, params: dict) -> None:
+ """Verify that the file names exist."""
+
+ glob_pattern = params["glob"]
+ suffixes = params["suffixes"]
+ exclude = params["exclude"]
+ relative_filenames = params["relative_filenames"]
+
+ loader = CloudBlobLoader(
+ toy_dir, glob=glob_pattern, suffixes=suffixes, exclude=exclude
+ )
+ blobs = list(loader.yield_blobs())
+
+ url_parsed = urlparse(toy_dir)
+ scheme = ""
+ if url_parsed.scheme == "file":
+ scheme = "file://"
+
+ file_names = sorted(f"{scheme}{blob.path}" for blob in blobs)
+
+ expected_filenames = sorted(
+ str(toy_dir + "/" + relative_filename)
+ for relative_filename in relative_filenames
+ )
+
+ assert file_names == expected_filenames
+ assert loader.count_matching_files() == len(relative_filenames)
+
+
+@pytest.mark.requires("cloudpathlib")
+def test_show_progress(toy_dir: str) -> None:
+ """Verify that file system loader works with a progress bar."""
+ loader = CloudBlobLoader(toy_dir)
+ blobs = list(loader.yield_blobs())
+ assert len(blobs) == loader.count_matching_files()
diff --git a/libs/community/tests/unit_tests/document_loaders/parsers/language/test_elixir.py b/libs/community/tests/unit_tests/document_loaders/parsers/language/test_elixir.py
new file mode 100644
index 0000000000000..02d6af926563e
--- /dev/null
+++ b/libs/community/tests/unit_tests/document_loaders/parsers/language/test_elixir.py
@@ -0,0 +1,57 @@
+import unittest
+
+import pytest
+
+from langchain_community.document_loaders.parsers.language.elixir import ElixirSegmenter
+
+
+@pytest.mark.requires("tree_sitter", "tree_sitter_languages")
+class TestElixirSegmenter(unittest.TestCase):
+ def setUp(self) -> None:
+ self.example_code = """@doc "some comment"
+def foo do
+ i = 0
+end
+
+defmodule M do
+ def hi do
+ i = 2
+ end
+
+ defp wave do
+ :ok
+ end
+end"""
+
+ self.expected_simplified_code = """# Code for: @doc "some comment"
+# Code for: def foo do
+
+# Code for: defmodule M do"""
+
+ self.expected_extracted_code = [
+ '@doc "some comment"',
+ "def foo do\n i = 0\nend",
+ "defmodule M do\n"
+ " def hi do\n"
+ " i = 2\n"
+ " end\n\n"
+ " defp wave do\n"
+ " :ok\n"
+ " end\n"
+ "end",
+ ]
+
+ def test_is_valid(self) -> None:
+ self.assertTrue(ElixirSegmenter("def a do; end").is_valid())
+ self.assertFalse(ElixirSegmenter("a b c 1 2 3").is_valid())
+
+ def test_extract_functions_classes(self) -> None:
+ segmenter = ElixirSegmenter(self.example_code)
+ extracted_code = segmenter.extract_functions_classes()
+ self.assertEqual(len(extracted_code), 3)
+ self.assertEqual(extracted_code, self.expected_extracted_code)
+
+ def test_simplify_code(self) -> None:
+ segmenter = ElixirSegmenter(self.example_code)
+ simplified_code = segmenter.simplify_code()
+ self.assertEqual(simplified_code, self.expected_simplified_code)
diff --git a/libs/community/tests/unit_tests/document_loaders/test_docs/csv/test_none_col.csv b/libs/community/tests/unit_tests/document_loaders/test_docs/csv/test_none_col.csv
new file mode 100644
index 0000000000000..a6a3d77e05060
--- /dev/null
+++ b/libs/community/tests/unit_tests/document_loaders/test_docs/csv/test_none_col.csv
@@ -0,0 +1,3 @@
+column1,column2,column3
+value1,value2,value3,value4,value5
+value6,value7,value8,value9
diff --git a/libs/community/tests/unit_tests/document_loaders/test_recursive_url_loader.py b/libs/community/tests/unit_tests/document_loaders/test_recursive_url_loader.py
new file mode 100644
index 0000000000000..55e00d997653e
--- /dev/null
+++ b/libs/community/tests/unit_tests/document_loaders/test_recursive_url_loader.py
@@ -0,0 +1,99 @@
+from __future__ import annotations
+
+import inspect
+import uuid
+from types import TracebackType
+from typing import Any, Type
+
+import aiohttp
+import pytest
+import requests_mock
+
+from langchain_community.document_loaders.recursive_url_loader import RecursiveUrlLoader
+
+link_to_one_two = """
+
+
+"""
+link_to_three = ''
+no_links = "no links
"
+
+fake_url = f"https://{uuid.uuid4()}.com"
+URL_TO_HTML = {
+ fake_url: link_to_one_two,
+ f"{fake_url}/one": link_to_three,
+ f"{fake_url}/two": link_to_three,
+ f"{fake_url}/three": no_links,
+}
+
+
+class MockGet:
+ def __init__(self, url: str) -> None:
+ self._text = URL_TO_HTML[url]
+ self.headers: dict = {}
+
+ async def text(self) -> str:
+ return self._text
+
+ async def __aexit__(
+ self, exc_type: Type[BaseException], exc: BaseException, tb: TracebackType
+ ) -> None:
+ pass
+
+ async def __aenter__(self) -> MockGet:
+ return self
+
+
+@pytest.mark.parametrize(("max_depth", "expected_docs"), [(1, 1), (2, 3), (3, 4)])
+@pytest.mark.parametrize("use_async", [False, True])
+def test_lazy_load(
+ mocker: Any, max_depth: int, expected_docs: int, use_async: bool
+) -> None:
+ loader = RecursiveUrlLoader(fake_url, max_depth=max_depth, use_async=use_async)
+ if use_async:
+ mocker.patch.object(aiohttp.ClientSession, "get", new=MockGet)
+ docs = list(loader.lazy_load())
+ else:
+ with requests_mock.Mocker() as m:
+ for url, html in URL_TO_HTML.items():
+ m.get(url, text=html)
+ docs = list(loader.lazy_load())
+ assert len(docs) == expected_docs
+
+
+@pytest.mark.parametrize(("max_depth", "expected_docs"), [(1, 1), (2, 3), (3, 4)])
+@pytest.mark.parametrize("use_async", [False, True])
+async def test_alazy_load(
+ mocker: Any, max_depth: int, expected_docs: int, use_async: bool
+) -> None:
+ loader = RecursiveUrlLoader(fake_url, max_depth=max_depth, use_async=use_async)
+ if use_async:
+ mocker.patch.object(aiohttp.ClientSession, "get", new=MockGet)
+ docs = []
+ async for doc in loader.alazy_load():
+ docs.append(doc)
+ else:
+ with requests_mock.Mocker() as m:
+ for url, html in URL_TO_HTML.items():
+ m.get(url, text=html)
+ docs = []
+ async for doc in loader.alazy_load():
+ docs.append(doc)
+
+ assert len(docs) == expected_docs
+
+
+def test_init_args_documented() -> None:
+ cls_docstring = RecursiveUrlLoader.__doc__ or ""
+ init_docstring = RecursiveUrlLoader.__init__.__doc__ or ""
+ all_docstring = cls_docstring + init_docstring
+ init_args = list(inspect.signature(RecursiveUrlLoader.__init__).parameters)
+ undocumented = [arg for arg in init_args[1:] if f"{arg}:" not in all_docstring]
+ assert not undocumented
+
+
+@pytest.mark.parametrize("method", ["load", "aload", "lazy_load", "alazy_load"])
+def test_no_runtime_args(method: str) -> None:
+ method_attr = getattr(RecursiveUrlLoader, method)
+ args = list(inspect.signature(method_attr).parameters)
+ assert args == ["self"]
diff --git a/libs/community/tests/unit_tests/embeddings/test_baichuan.py b/libs/community/tests/unit_tests/embeddings/test_baichuan.py
new file mode 100644
index 0000000000000..10513948f9427
--- /dev/null
+++ b/libs/community/tests/unit_tests/embeddings/test_baichuan.py
@@ -0,0 +1,18 @@
+from typing import cast
+
+from langchain_core.pydantic_v1 import SecretStr
+
+from langchain_community.embeddings import BaichuanTextEmbeddings
+
+
+def test_sparkllm_initialization_by_alias() -> None:
+ # Effective initialization
+ embeddings = BaichuanTextEmbeddings( # type: ignore[call-arg]
+ model="embedding_model", # type: ignore[arg-type]
+ api_key="your-api-key", # type: ignore[arg-type]
+ )
+ assert embeddings.model_name == "embedding_model"
+ assert (
+ cast(SecretStr, embeddings.baichuan_api_key).get_secret_value()
+ == "your-api-key"
+ )
diff --git a/libs/community/tests/unit_tests/embeddings/test_ovhcloud.py b/libs/community/tests/unit_tests/embeddings/test_ovhcloud.py
new file mode 100644
index 0000000000000..c06c1550e4b7d
--- /dev/null
+++ b/libs/community/tests/unit_tests/embeddings/test_ovhcloud.py
@@ -0,0 +1,31 @@
+import pytest
+
+from langchain_community.embeddings.ovhcloud import OVHCloudEmbeddings
+
+
+def test_ovhcloud_correct_instantiation() -> None:
+ llm = OVHCloudEmbeddings(model_name="multilingual-e5-base", access_token="token")
+ assert isinstance(llm, OVHCloudEmbeddings)
+ llm = OVHCloudEmbeddings(
+ model_name="multilingual-e5-base", region="kepler", access_token="token"
+ )
+ assert isinstance(llm, OVHCloudEmbeddings)
+
+
+def test_ovhcloud_empty_model_name_should_raise_error() -> None:
+ with pytest.raises(ValueError):
+ OVHCloudEmbeddings(model_name="", region="kepler", access_token="token")
+
+
+def test_ovhcloud_empty_region_should_raise_error() -> None:
+ with pytest.raises(ValueError):
+ OVHCloudEmbeddings(
+ model_name="multilingual-e5-base", region="", access_token="token"
+ )
+
+
+def test_ovhcloud_empty_access_token_should_raise_error() -> None:
+ with pytest.raises(ValueError):
+ OVHCloudEmbeddings(
+ model_name="multilingual-e5-base", region="kepler", access_token=""
+ )
diff --git a/libs/community/tests/unit_tests/embeddings/test_sparkllm.py b/libs/community/tests/unit_tests/embeddings/test_sparkllm.py
new file mode 100644
index 0000000000000..d318035106e23
--- /dev/null
+++ b/libs/community/tests/unit_tests/embeddings/test_sparkllm.py
@@ -0,0 +1,47 @@
+import os
+from typing import cast
+
+import pytest
+from langchain_core.pydantic_v1 import SecretStr, ValidationError
+
+from langchain_community.embeddings import SparkLLMTextEmbeddings
+
+
+def test_sparkllm_initialization_by_alias() -> None:
+ # Effective initialization
+ embeddings = SparkLLMTextEmbeddings(
+ app_id="your-app-id", # type: ignore[arg-type]
+ api_key="your-api-key", # type: ignore[arg-type]
+ api_secret="your-api-secret", # type: ignore[arg-type]
+ )
+ assert cast(SecretStr, embeddings.spark_app_id).get_secret_value() == "your-app-id"
+ assert (
+ cast(SecretStr, embeddings.spark_api_key).get_secret_value() == "your-api-key"
+ )
+ assert (
+ cast(SecretStr, embeddings.spark_api_secret).get_secret_value()
+ == "your-api-secret"
+ )
+
+
+def test_initialization_parameters_from_env() -> None:
+ # Setting environment variable
+ os.environ["SPARK_APP_ID"] = "your-app-id"
+ os.environ["SPARK_API_KEY"] = "your-api-key"
+ os.environ["SPARK_API_SECRET"] = "your-api-secret"
+
+ # Effective initialization
+ embeddings = SparkLLMTextEmbeddings()
+ assert cast(SecretStr, embeddings.spark_app_id).get_secret_value() == "your-app-id"
+ assert (
+ cast(SecretStr, embeddings.spark_api_key).get_secret_value() == "your-api-key"
+ )
+ assert (
+ cast(SecretStr, embeddings.spark_api_secret).get_secret_value()
+ == "your-api-secret"
+ )
+
+ # Environment variable missing
+ del os.environ["SPARK_APP_ID"]
+ with pytest.raises(ValidationError):
+ SparkLLMTextEmbeddings()
diff --git a/libs/community/tests/unit_tests/storage/test_sql.py b/libs/community/tests/unit_tests/storage/test_sql.py
new file mode 100644
index 0000000000000..084f0e2d19089
--- /dev/null
+++ b/libs/community/tests/unit_tests/storage/test_sql.py
@@ -0,0 +1,89 @@
+from typing import AsyncGenerator, Generator, cast
+
+import pytest
+from langchain.storage._lc_store import create_kv_docstore, create_lc_store
+from langchain_core.documents import Document
+from langchain_core.stores import BaseStore
+
+from langchain_community.storage.sql import SQLStore
+
+
+@pytest.fixture
+def sql_store() -> Generator[SQLStore, None, None]:
+ store = SQLStore(namespace="test", db_url="sqlite://")
+ store.create_schema()
+ yield store
+
+
+@pytest.fixture
+async def async_sql_store() -> AsyncGenerator[SQLStore, None]:
+ store = SQLStore(namespace="test", db_url="sqlite+aiosqlite://", async_mode=True)
+ await store.acreate_schema()
+ yield store
+
+
+def test_create_lc_store(sql_store: SQLStore) -> None:
+ """Test that a docstore is created from a base store."""
+ docstore: BaseStore[str, Document] = cast(
+ BaseStore[str, Document], create_lc_store(sql_store)
+ )
+ docstore.mset([("key1", Document(page_content="hello", metadata={"key": "value"}))])
+ fetched_doc = docstore.mget(["key1"])[0]
+ assert fetched_doc is not None
+ assert fetched_doc.page_content == "hello"
+ assert fetched_doc.metadata == {"key": "value"}
+
+
+def test_create_kv_store(sql_store: SQLStore) -> None:
+ """Test that a docstore is created from a base store."""
+ docstore = create_kv_docstore(sql_store)
+ docstore.mset([("key1", Document(page_content="hello", metadata={"key": "value"}))])
+ fetched_doc = docstore.mget(["key1"])[0]
+ assert isinstance(fetched_doc, Document)
+ assert fetched_doc.page_content == "hello"
+ assert fetched_doc.metadata == {"key": "value"}
+
+
+@pytest.mark.requires("aiosqlite")
+async def test_async_create_kv_store(async_sql_store: SQLStore) -> None:
+ """Test that a docstore is created from a base store."""
+ docstore = create_kv_docstore(async_sql_store)
+ await docstore.amset(
+ [("key1", Document(page_content="hello", metadata={"key": "value"}))]
+ )
+ fetched_doc = (await docstore.amget(["key1"]))[0]
+ assert isinstance(fetched_doc, Document)
+ assert fetched_doc.page_content == "hello"
+ assert fetched_doc.metadata == {"key": "value"}
+
+
+def test_sample_sql_docstore(sql_store: SQLStore) -> None:
+ # Set values for keys
+ sql_store.mset([("key1", b"value1"), ("key2", b"value2")])
+
+ # Get values for keys
+ values = sql_store.mget(["key1", "key2"]) # Returns [b"value1", b"value2"]
+ assert values == [b"value1", b"value2"]
+ # Delete keys
+ sql_store.mdelete(["key1"])
+
+ # Iterate over keys
+ assert [key for key in sql_store.yield_keys()] == ["key2"]
+
+
+@pytest.mark.requires("aiosqlite")
+async def test_async_sample_sql_docstore(async_sql_store: SQLStore) -> None:
+ # Set values for keys
+ await async_sql_store.amset([("key1", b"value1"), ("key2", b"value2")])
+ # sql_store.mset([("key1", "value1"), ("key2", "value2")])
+
+ # Get values for keys
+ values = await async_sql_store.amget(
+ ["key1", "key2"]
+ ) # Returns [b"value1", b"value2"]
+ assert values == [b"value1", b"value2"]
+ # Delete keys
+ await async_sql_store.amdelete(["key1"])
+
+ # Iterate over keys
+ assert [key async for key in async_sql_store.ayield_keys()] == ["key2"]
diff --git a/libs/community/tests/unit_tests/utilities/test_openapi.py b/libs/community/tests/unit_tests/utilities/test_openapi.py
new file mode 100644
index 0000000000000..e7e8b74557396
--- /dev/null
+++ b/libs/community/tests/unit_tests/utilities/test_openapi.py
@@ -0,0 +1,44 @@
+from pathlib import Path
+
+import pytest
+from langchain.chains.openai_functions.openapi import openapi_spec_to_openai_fn
+
+from langchain_community.utilities.openapi import ( # noqa: E402 # ignore: community-import
+ OpenAPISpec,
+)
+
+EXPECTED_OPENAI_FUNCTIONS_HEADER_PARAM = [
+ {
+ "name": "showPetById",
+ "description": "Info for a specific pet",
+ "parameters": {
+ "type": "object",
+ "properties": {
+ "headers": {
+ "type": "object",
+ "properties": {
+ "header_param": {
+ "type": "string",
+ "description": "A header param",
+ }
+ },
+ "required": ["header_param"],
+ }
+ },
+ },
+ }
+]
+
+
+@pytest.mark.requires("openapi_pydantic")
+def test_header_param() -> None:
+ spec = OpenAPISpec.from_file(
+ Path(__file__).parent.parent
+ / "data"
+ / "openapi_specs"
+ / "openapi_spec_header_param.json",
+ )
+
+ openai_functions, _ = openapi_spec_to_openai_fn(spec)
+
+ assert openai_functions == EXPECTED_OPENAI_FUNCTIONS_HEADER_PARAM
diff --git a/libs/community/tests/unit_tests/vectorstores/test_aerospike.py b/libs/community/tests/unit_tests/vectorstores/test_aerospike.py
new file mode 100644
index 0000000000000..6ff4bca995844
--- /dev/null
+++ b/libs/community/tests/unit_tests/vectorstores/test_aerospike.py
@@ -0,0 +1,378 @@
+import sys
+from typing import Any, Callable, Generator
+from unittest.mock import MagicMock, Mock, call
+
+import pytest
+from langchain_core.documents import Document
+
+from langchain_community.vectorstores.aerospike import Aerospike
+from langchain_community.vectorstores.utils import DistanceStrategy
+from tests.integration_tests.vectorstores.fake_embeddings import FakeEmbeddings
+
+pytestmark = pytest.mark.requires("aerospike_vector_search") and pytest.mark.skipif(
+ sys.version_info < (3, 9), reason="requires python3.9 or higher"
+)
+
+
+@pytest.fixture(scope="module")
+def client() -> Generator[Any, None, None]:
+ try:
+ from aerospike_vector_search import Client
+ from aerospike_vector_search.types import HostPort
+ except ImportError:
+ pytest.skip("aerospike_vector_search not installed")
+
+ client = Client(
+ seeds=[
+ HostPort(host="dummy-host", port=3000),
+ ],
+ )
+
+ yield client
+
+ client.close()
+
+
+@pytest.fixture
+def mock_client(mocker: Any) -> None:
+ try:
+ from aerospike_vector_search import Client
+ except ImportError:
+ pytest.skip("aerospike_vector_search not installed")
+
+ return mocker.MagicMock(Client)
+
+
+def test_aerospike(client: Any) -> None:
+ """Ensure an error is raised when search with score in hybrid mode
+ because in this case Elasticsearch does not return any score.
+ """
+ from aerospike_vector_search import AVSError
+
+ query_string = "foo"
+ embedding = FakeEmbeddings()
+
+ store = Aerospike(
+ client=client,
+ embedding=embedding,
+ text_key="text",
+ vector_key="vector",
+ index_name="dummy_index",
+ namespace="test",
+ set_name="testset",
+ distance_strategy=DistanceStrategy.COSINE,
+ )
+
+ # TODO: Remove grpc import when aerospike_vector_search wraps grpc errors
+ with pytest.raises(AVSError):
+ store.similarity_search_by_vector(embedding.embed_query(query_string))
+
+
+def test_init_aerospike_distance(client: Any) -> None:
+ from aerospike_vector_search.types import VectorDistanceMetric
+
+ embedding = FakeEmbeddings()
+ aerospike = Aerospike(
+ client=client,
+ embedding=embedding,
+ text_key="text",
+ vector_key="vector",
+ index_name="dummy_index",
+ namespace="test",
+ set_name="testset",
+ distance_strategy=VectorDistanceMetric.COSINE,
+ )
+
+ assert aerospike._distance_strategy == DistanceStrategy.COSINE
+
+
+def test_init_bad_embedding(client: Any) -> None:
+ def bad_embedding() -> None:
+ return None
+
+ with pytest.warns(
+ UserWarning,
+ match=(
+ "Passing in `embedding` as a Callable is deprecated. Please pass"
+ + " in an Embeddings object instead."
+ ),
+ ):
+ Aerospike(
+ client=client,
+ embedding=bad_embedding,
+ text_key="text",
+ vector_key="vector",
+ index_name="dummy_index",
+ namespace="test",
+ set_name="testset",
+ distance_strategy=DistanceStrategy.COSINE,
+ )
+
+
+def test_init_bad_client(client: Any) -> None:
+ class BadClient:
+ pass
+
+ with pytest.raises(
+ ValueError,
+ match=(
+ "client should be an instance of aerospike_vector_search.Client,"
+ + " got .BadClient'>"
+ ),
+ ):
+ Aerospike(
+ client=BadClient(),
+ embedding=FakeEmbeddings(),
+ text_key="text",
+ vector_key="vector",
+ index_name="dummy_index",
+ namespace="test",
+ set_name="testset",
+ distance_strategy=DistanceStrategy.COSINE,
+ )
+
+
+def test_convert_distance_strategy(client: Any) -> None:
+ from aerospike_vector_search.types import VectorDistanceMetric
+
+ aerospike = Aerospike(
+ client=client,
+ embedding=FakeEmbeddings(),
+ text_key="text",
+ vector_key="vector",
+ index_name="dummy_index",
+ namespace="test",
+ set_name="testset",
+ distance_strategy=DistanceStrategy.COSINE,
+ )
+
+ converted_strategy = aerospike.convert_distance_strategy(
+ VectorDistanceMetric.COSINE
+ )
+ assert converted_strategy == DistanceStrategy.COSINE
+
+ converted_strategy = aerospike.convert_distance_strategy(
+ VectorDistanceMetric.DOT_PRODUCT
+ )
+ assert converted_strategy == DistanceStrategy.DOT_PRODUCT
+
+ converted_strategy = aerospike.convert_distance_strategy(
+ VectorDistanceMetric.SQUARED_EUCLIDEAN
+ )
+ assert converted_strategy == DistanceStrategy.EUCLIDEAN_DISTANCE
+
+ with pytest.raises(ValueError):
+ aerospike.convert_distance_strategy(VectorDistanceMetric.HAMMING)
+
+
+def test_add_texts_wait_for_index_error(client: Any) -> None:
+ aerospike = Aerospike(
+ client=client,
+ embedding=FakeEmbeddings(),
+ text_key="text",
+ vector_key="vector",
+ # index_name="dummy_index",
+ namespace="test",
+ set_name="testset",
+ distance_strategy=DistanceStrategy.COSINE,
+ )
+
+ with pytest.raises(
+ ValueError, match="if wait_for_index is True, index_name must be provided"
+ ):
+ aerospike.add_texts(["foo", "bar"], wait_for_index=True)
+
+
+def test_add_texts_returns_ids(mock_client: MagicMock) -> None:
+ aerospike = Aerospike(
+ client=mock_client,
+ embedding=FakeEmbeddings(),
+ text_key="text",
+ vector_key="vector",
+ namespace="test",
+ set_name="testset",
+ distance_strategy=DistanceStrategy.COSINE,
+ )
+
+ excepted = ["0", "1"]
+ actual = aerospike.add_texts(
+ ["foo", "bar"],
+ metadatas=[{"foo": 0}, {"bar": 1}],
+ ids=["0", "1"],
+ set_name="otherset",
+ index_name="dummy_index",
+ wait_for_index=True,
+ )
+
+ assert excepted == actual
+ mock_client.upsert.assert_has_calls(
+ calls=[
+ call(
+ namespace="test",
+ key="0",
+ set_name="otherset",
+ record_data={
+ "_id": "0",
+ "text": "foo",
+ "vector": [1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 0.0],
+ "foo": 0,
+ },
+ ),
+ call(
+ namespace="test",
+ key="1",
+ set_name="otherset",
+ record_data={
+ "_id": "1",
+ "text": "bar",
+ "vector": [1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0],
+ "bar": 1,
+ },
+ ),
+ ]
+ )
+ mock_client.wait_for_index_completion.assert_called_once_with(
+ namespace="test",
+ name="dummy_index",
+ )
+
+
+def test_delete_returns_false(mock_client: MagicMock) -> None:
+ from aerospike_vector_search import AVSServerError
+
+ mock_client.delete.side_effect = Mock(side_effect=AVSServerError(rpc_error=""))
+ aerospike = Aerospike(
+ client=mock_client,
+ embedding=FakeEmbeddings(),
+ text_key="text",
+ vector_key="vector",
+ namespace="test",
+ set_name="testset",
+ distance_strategy=DistanceStrategy.COSINE,
+ )
+
+ assert not aerospike.delete(["foo", "bar"], set_name="testset")
+ mock_client.delete.assert_called_once_with(
+ namespace="test", key="foo", set_name="testset"
+ )
+
+
+def test_similarity_search_by_vector_with_score_missing_index_name(
+ client: Any,
+) -> None:
+ aerospike = Aerospike(
+ client=client,
+ embedding=FakeEmbeddings(),
+ text_key="text",
+ vector_key="vector",
+ # index_name="dummy_index",
+ namespace="test",
+ set_name="testset",
+ distance_strategy=DistanceStrategy.COSINE,
+ )
+
+ with pytest.raises(ValueError, match="index_name must be provided"):
+ aerospike.similarity_search_by_vector_with_score([1.0, 2.0, 3.0])
+
+
+def test_similarity_search_by_vector_with_score_filters_missing_text_key(
+ mock_client: MagicMock,
+) -> None:
+ from aerospike_vector_search.types import Neighbor
+
+ text_key = "text"
+ mock_client.vector_search.return_value = [
+ Neighbor(key="key1", fields={text_key: 1}, distance=1.0),
+ Neighbor(key="key2", fields={}, distance=0.0),
+ Neighbor(key="key3", fields={text_key: 3}, distance=3.0),
+ ]
+ aerospike = Aerospike(
+ client=mock_client,
+ embedding=FakeEmbeddings(),
+ text_key=text_key,
+ vector_key="vector",
+ index_name="dummy_index",
+ namespace="test",
+ set_name="testset",
+ distance_strategy=DistanceStrategy.COSINE,
+ )
+
+ actual = aerospike.similarity_search_by_vector_with_score(
+ [1.0, 2.0, 3.0], k=10, metadata_keys=["foo"]
+ )
+
+ expected = [
+ (Document(page_content="1"), 1.0),
+ (Document(page_content="3"), 3.0),
+ ]
+ mock_client.vector_search.assert_called_once_with(
+ index_name="dummy_index",
+ namespace="test",
+ query=[1.0, 2.0, 3.0],
+ limit=10,
+ field_names=[text_key, "foo"],
+ )
+
+ assert expected == actual
+
+
+def test_similarity_search_by_vector_with_score_overwrite_index_name(
+ mock_client: MagicMock,
+) -> None:
+ mock_client.vector_search.return_value = []
+ aerospike = Aerospike(
+ client=mock_client,
+ embedding=FakeEmbeddings(),
+ text_key="text",
+ vector_key="vector",
+ index_name="dummy_index",
+ namespace="test",
+ set_name="testset",
+ distance_strategy=DistanceStrategy.COSINE,
+ )
+
+ aerospike.similarity_search_by_vector_with_score(
+ [1.0, 2.0, 3.0], index_name="other_index"
+ )
+
+ mock_client.vector_search.assert_called_once_with(
+ index_name="other_index",
+ namespace="test",
+ query=[1.0, 2.0, 3.0],
+ limit=4,
+ field_names=None,
+ )
+
+
+@pytest.mark.parametrize(
+ "distance_strategy,expected_fn",
+ [
+ (DistanceStrategy.COSINE, Aerospike._cosine_relevance_score_fn),
+ (DistanceStrategy.EUCLIDEAN_DISTANCE, Aerospike._euclidean_relevance_score_fn),
+ (DistanceStrategy.DOT_PRODUCT, Aerospike._max_inner_product_relevance_score_fn),
+ (DistanceStrategy.JACCARD, ValueError),
+ ],
+)
+def test_select_relevance_score_fn(
+ client: Any, distance_strategy: DistanceStrategy, expected_fn: Callable
+) -> None:
+ aerospike = Aerospike(
+ client=client,
+ embedding=FakeEmbeddings(),
+ text_key="text",
+ vector_key="vector",
+ index_name="dummy_index",
+ namespace="test",
+ set_name="testset",
+ distance_strategy=distance_strategy,
+ )
+
+ if expected_fn == ValueError:
+ with pytest.raises(ValueError):
+ aerospike._select_relevance_score_fn()
+
+ else:
+ fn = aerospike._select_relevance_score_fn()
+
+ assert fn == expected_fn
diff --git a/libs/core/extended_testing_deps.txt b/libs/core/extended_testing_deps.txt
new file mode 100644
index 0000000000000..5ad9c8930daf9
--- /dev/null
+++ b/libs/core/extended_testing_deps.txt
@@ -0,0 +1 @@
+jinja2>=3,<4
diff --git a/libs/core/langchain_core/tracers/core.py b/libs/core/langchain_core/tracers/core.py
new file mode 100644
index 0000000000000..bee9f855b3ecf
--- /dev/null
+++ b/libs/core/langchain_core/tracers/core.py
@@ -0,0 +1,569 @@
+"""Utilities for the root listener."""
+
+from __future__ import annotations
+
+import logging
+import sys
+import traceback
+from abc import ABC, abstractmethod
+from datetime import datetime, timezone
+from typing import (
+ TYPE_CHECKING,
+ Any,
+ Coroutine,
+ Dict,
+ List,
+ Literal,
+ Optional,
+ Sequence,
+ Set,
+ Tuple,
+ Union,
+ cast,
+)
+from uuid import UUID
+
+from tenacity import RetryCallState
+
+from langchain_core.exceptions import TracerException
+from langchain_core.load import dumpd
+from langchain_core.messages import BaseMessage
+from langchain_core.outputs import (
+ ChatGeneration,
+ ChatGenerationChunk,
+ GenerationChunk,
+ LLMResult,
+)
+from langchain_core.tracers.schemas import Run
+
+if TYPE_CHECKING:
+ from langchain_core.documents import Document
+
+logger = logging.getLogger(__name__)
+
+SCHEMA_FORMAT_TYPE = Literal["original", "streaming_events"]
+
+
+class _TracerCore(ABC):
+ """
+ Abstract base class for tracers
+ This class provides common methods, and reusable methods for tracers.
+ """
+
+ log_missing_parent: bool = True
+
+ def __init__(
+ self,
+ *,
+ _schema_format: Literal[
+ "original", "streaming_events", "original+chat"
+ ] = "original",
+ **kwargs: Any,
+ ) -> None:
+ """Initialize the tracer.
+
+ Args:
+ _schema_format: Primarily changes how the inputs and outputs are
+ handled. For internal use only. This API will change.
+ - 'original' is the format used by all current tracers.
+ This format is slightly inconsistent with respect to inputs
+ and outputs.
+ - 'streaming_events' is used for supporting streaming events,
+ for internal usage. It will likely change in the future, or
+ be deprecated entirely in favor of a dedicated async tracer
+ for streaming events.
+ - 'original+chat' is a format that is the same as 'original'
+ except it does NOT raise an attribute error on_chat_model_start
+ kwargs: Additional keyword arguments that will be passed to
+ the super class.
+ """
+ super().__init__(**kwargs)
+ self._schema_format = _schema_format # For internal use only API will change.
+ self.run_map: Dict[str, Run] = {}
+ """Map of run ID to run. Cleared on run end."""
+ self.order_map: Dict[UUID, Tuple[UUID, str]] = {}
+ """Map of run ID to (trace_id, dotted_order). Cleared when tracer GCed."""
+
+ @abstractmethod
+ def _persist_run(self, run: Run) -> Union[None, Coroutine[Any, Any, None]]:
+ """Persist a run."""
+
+ @staticmethod
+ def _add_child_run(
+ parent_run: Run,
+ child_run: Run,
+ ) -> None:
+ """Add child run to a chain run or tool run."""
+ parent_run.child_runs.append(child_run)
+
+ @staticmethod
+ def _get_stacktrace(error: BaseException) -> str:
+ """Get the stacktrace of the parent error."""
+ msg = repr(error)
+ try:
+ if sys.version_info < (3, 10):
+ tb = traceback.format_exception(
+ error.__class__, error, error.__traceback__
+ )
+ else:
+ tb = traceback.format_exception(error)
+ return (msg + "\n\n".join(tb)).strip()
+ except: # noqa: E722
+ return msg
+
+ def _start_trace(self, run: Run) -> Union[None, Coroutine[Any, Any, None]]: # type: ignore[return]
+ current_dotted_order = run.start_time.strftime("%Y%m%dT%H%M%S%fZ") + str(run.id)
+ if run.parent_run_id:
+ if parent := self.order_map.get(run.parent_run_id):
+ run.trace_id, run.dotted_order = parent
+ run.dotted_order += "." + current_dotted_order
+ if parent_run := self.run_map.get(str(run.parent_run_id)):
+ self._add_child_run(parent_run, run)
+ else:
+ if self.log_missing_parent:
+ logger.warning(
+ f"Parent run {run.parent_run_id} not found for run {run.id}."
+ " Treating as a root run."
+ )
+ run.parent_run_id = None
+ run.trace_id = run.id
+ run.dotted_order = current_dotted_order
+ else:
+ run.trace_id = run.id
+ run.dotted_order = current_dotted_order
+ self.order_map[run.id] = (run.trace_id, run.dotted_order)
+ self.run_map[str(run.id)] = run
+
+ def _get_run(
+ self, run_id: UUID, run_type: Union[str, Set[str], None] = None
+ ) -> Run:
+ try:
+ run = self.run_map[str(run_id)]
+ except KeyError as exc:
+ raise TracerException(f"No indexed run ID {run_id}.") from exc
+
+ if isinstance(run_type, str):
+ run_types: Union[Set[str], None] = {run_type}
+ else:
+ run_types = run_type
+ if run_types is not None and run.run_type not in run_types:
+ raise TracerException(
+ f"Found {run.run_type} run at ID {run_id}, "
+ f"but expected {run_types} run."
+ )
+ return run
+
+ def _create_chat_model_run(
+ self,
+ serialized: Dict[str, Any],
+ messages: List[List[BaseMessage]],
+ run_id: UUID,
+ tags: Optional[List[str]] = None,
+ parent_run_id: Optional[UUID] = None,
+ metadata: Optional[Dict[str, Any]] = None,
+ name: Optional[str] = None,
+ **kwargs: Any,
+ ) -> Run:
+ """Create a chat model run."""
+ if self._schema_format not in ("streaming_events", "original+chat"):
+ # Please keep this un-implemented for backwards compatibility.
+ # When it's unimplemented old tracers that use the "original" format
+ # fallback on the on_llm_start method implementation if they
+ # find that the on_chat_model_start method is not implemented.
+ # This can eventually be cleaned up by writing a "modern" tracer
+ # that has all the updated schema changes corresponding to
+ # the "streaming_events" format.
+ raise NotImplementedError(
+ f"Chat model tracing is not supported in "
+ f"for {self._schema_format} format."
+ )
+ start_time = datetime.now(timezone.utc)
+ if metadata:
+ kwargs.update({"metadata": metadata})
+ return Run(
+ id=run_id,
+ parent_run_id=parent_run_id,
+ serialized=serialized,
+ inputs={"messages": [[dumpd(msg) for msg in batch] for batch in messages]},
+ extra=kwargs,
+ events=[{"name": "start", "time": start_time}],
+ start_time=start_time,
+ # WARNING: This is valid ONLY for streaming_events.
+ # run_type="llm" is what's used by virtually all tracers.
+ # Changing this to "chat_model" may break triggering on_llm_start
+ run_type="chat_model",
+ tags=tags,
+ name=name, # type: ignore[arg-type]
+ )
+
+ def _create_llm_run(
+ self,
+ serialized: Dict[str, Any],
+ prompts: List[str],
+ run_id: UUID,
+ tags: Optional[List[str]] = None,
+ parent_run_id: Optional[UUID] = None,
+ metadata: Optional[Dict[str, Any]] = None,
+ name: Optional[str] = None,
+ **kwargs: Any,
+ ) -> Run:
+ """Create a llm run"""
+ start_time = datetime.now(timezone.utc)
+ if metadata:
+ kwargs.update({"metadata": metadata})
+ return Run(
+ id=run_id,
+ parent_run_id=parent_run_id,
+ serialized=serialized,
+ # TODO: Figure out how to expose kwargs here
+ inputs={"prompts": prompts},
+ extra=kwargs,
+ events=[{"name": "start", "time": start_time}],
+ start_time=start_time,
+ run_type="llm",
+ tags=tags or [],
+ name=name, # type: ignore[arg-type]
+ )
+
+ def _llm_run_with_token_event(
+ self,
+ token: str,
+ run_id: UUID,
+ chunk: Optional[Union[GenerationChunk, ChatGenerationChunk]] = None,
+ parent_run_id: Optional[UUID] = None,
+ **kwargs: Any,
+ ) -> Run:
+ """
+ Append token event to LLM run and return the run
+ """
+ llm_run = self._get_run(run_id, run_type={"llm", "chat_model"})
+ event_kwargs: Dict[str, Any] = {"token": token}
+ if chunk:
+ event_kwargs["chunk"] = chunk
+ llm_run.events.append(
+ {
+ "name": "new_token",
+ "time": datetime.now(timezone.utc),
+ "kwargs": event_kwargs,
+ },
+ )
+ return llm_run
+
+ def _llm_run_with_retry_event(
+ self,
+ retry_state: RetryCallState,
+ run_id: UUID,
+ **kwargs: Any,
+ ) -> Run:
+ llm_run = self._get_run(run_id)
+ retry_d: Dict[str, Any] = {
+ "slept": retry_state.idle_for,
+ "attempt": retry_state.attempt_number,
+ }
+ if retry_state.outcome is None:
+ retry_d["outcome"] = "N/A"
+ elif retry_state.outcome.failed:
+ retry_d["outcome"] = "failed"
+ exception = retry_state.outcome.exception()
+ retry_d["exception"] = str(exception)
+ retry_d["exception_type"] = exception.__class__.__name__
+ else:
+ retry_d["outcome"] = "success"
+ retry_d["result"] = str(retry_state.outcome.result())
+ llm_run.events.append(
+ {
+ "name": "retry",
+ "time": datetime.now(timezone.utc),
+ "kwargs": retry_d,
+ },
+ )
+ return llm_run
+
+ def _complete_llm_run(self, response: LLMResult, run_id: UUID) -> Run:
+ llm_run = self._get_run(run_id, run_type={"llm", "chat_model"})
+ llm_run.outputs = response.dict()
+ for i, generations in enumerate(response.generations):
+ for j, generation in enumerate(generations):
+ output_generation = llm_run.outputs["generations"][i][j]
+ if "message" in output_generation:
+ output_generation["message"] = dumpd(
+ cast(ChatGeneration, generation).message
+ )
+ llm_run.end_time = datetime.now(timezone.utc)
+ llm_run.events.append({"name": "end", "time": llm_run.end_time})
+
+ return llm_run
+
+ def _errored_llm_run(self, error: BaseException, run_id: UUID) -> Run:
+ llm_run = self._get_run(run_id, run_type={"llm", "chat_model"})
+ llm_run.error = self._get_stacktrace(error)
+ llm_run.end_time = datetime.now(timezone.utc)
+ llm_run.events.append({"name": "error", "time": llm_run.end_time})
+
+ return llm_run
+
+ def _create_chain_run(
+ self,
+ serialized: Dict[str, Any],
+ inputs: Dict[str, Any],
+ run_id: UUID,
+ tags: Optional[List[str]] = None,
+ parent_run_id: Optional[UUID] = None,
+ metadata: Optional[Dict[str, Any]] = None,
+ run_type: Optional[str] = None,
+ name: Optional[str] = None,
+ **kwargs: Any,
+ ) -> Run:
+ """Create a chain Run"""
+ start_time = datetime.now(timezone.utc)
+ if metadata:
+ kwargs.update({"metadata": metadata})
+ return Run(
+ id=run_id,
+ parent_run_id=parent_run_id,
+ serialized=serialized,
+ inputs=self._get_chain_inputs(inputs),
+ extra=kwargs,
+ events=[{"name": "start", "time": start_time}],
+ start_time=start_time,
+ child_runs=[],
+ run_type=run_type or "chain",
+ name=name, # type: ignore[arg-type]
+ tags=tags or [],
+ )
+
+ def _get_chain_inputs(self, inputs: Any) -> Any:
+ """Get the inputs for a chain run."""
+ if self._schema_format in ("original", "original+chat"):
+ return inputs if isinstance(inputs, dict) else {"input": inputs}
+ elif self._schema_format == "streaming_events":
+ return {
+ "input": inputs,
+ }
+ else:
+ raise ValueError(f"Invalid format: {self._schema_format}")
+
+ def _get_chain_outputs(self, outputs: Any) -> Any:
+ """Get the outputs for a chain run."""
+ if self._schema_format in ("original", "original+chat"):
+ return outputs if isinstance(outputs, dict) else {"output": outputs}
+ elif self._schema_format == "streaming_events":
+ return {
+ "output": outputs,
+ }
+ else:
+ raise ValueError(f"Invalid format: {self._schema_format}")
+
+ def _complete_chain_run(
+ self,
+ outputs: Dict[str, Any],
+ run_id: UUID,
+ inputs: Optional[Dict[str, Any]] = None,
+ **kwargs: Any,
+ ) -> Run:
+ """Update a chain run with outputs and end time."""
+ chain_run = self._get_run(run_id)
+ chain_run.outputs = self._get_chain_outputs(outputs)
+ chain_run.end_time = datetime.now(timezone.utc)
+ chain_run.events.append({"name": "end", "time": chain_run.end_time})
+ if inputs is not None:
+ chain_run.inputs = self._get_chain_inputs(inputs)
+ return chain_run
+
+ def _errored_chain_run(
+ self,
+ error: BaseException,
+ inputs: Optional[Dict[str, Any]],
+ run_id: UUID,
+ **kwargs: Any,
+ ) -> Run:
+ chain_run = self._get_run(run_id)
+ chain_run.error = self._get_stacktrace(error)
+ chain_run.end_time = datetime.now(timezone.utc)
+ chain_run.events.append({"name": "error", "time": chain_run.end_time})
+ if inputs is not None:
+ chain_run.inputs = self._get_chain_inputs(inputs)
+ return chain_run
+
+ def _create_tool_run(
+ self,
+ serialized: Dict[str, Any],
+ input_str: str,
+ run_id: UUID,
+ tags: Optional[List[str]] = None,
+ parent_run_id: Optional[UUID] = None,
+ metadata: Optional[Dict[str, Any]] = None,
+ name: Optional[str] = None,
+ inputs: Optional[Dict[str, Any]] = None,
+ **kwargs: Any,
+ ) -> Run:
+ """Create a tool run."""
+ start_time = datetime.now(timezone.utc)
+ if metadata:
+ kwargs.update({"metadata": metadata})
+
+ if self._schema_format in ("original", "original+chat"):
+ inputs = {"input": input_str}
+ elif self._schema_format == "streaming_events":
+ inputs = {"input": inputs}
+ else:
+ raise AssertionError(f"Invalid format: {self._schema_format}")
+
+ return Run(
+ id=run_id,
+ parent_run_id=parent_run_id,
+ serialized=serialized,
+ # Wrapping in dict since Run requires a dict object.
+ inputs=inputs,
+ extra=kwargs,
+ events=[{"name": "start", "time": start_time}],
+ start_time=start_time,
+ child_runs=[],
+ run_type="tool",
+ tags=tags or [],
+ name=name, # type: ignore[arg-type]
+ )
+
+ def _complete_tool_run(
+ self,
+ output: Dict[str, Any],
+ run_id: UUID,
+ **kwargs: Any,
+ ) -> Run:
+ """Update a tool run with outputs and end time."""
+ tool_run = self._get_run(run_id, run_type="tool")
+ tool_run.outputs = {"output": output}
+ tool_run.end_time = datetime.now(timezone.utc)
+ tool_run.events.append({"name": "end", "time": tool_run.end_time})
+ return tool_run
+
+ def _errored_tool_run(
+ self,
+ error: BaseException,
+ run_id: UUID,
+ **kwargs: Any,
+ ) -> Run:
+ """Update a tool run with error and end time."""
+ tool_run = self._get_run(run_id, run_type="tool")
+ tool_run.error = self._get_stacktrace(error)
+ tool_run.end_time = datetime.now(timezone.utc)
+ tool_run.events.append({"name": "error", "time": tool_run.end_time})
+ return tool_run
+
+ def _create_retrieval_run(
+ self,
+ serialized: Dict[str, Any],
+ query: str,
+ run_id: UUID,
+ parent_run_id: Optional[UUID] = None,
+ tags: Optional[List[str]] = None,
+ metadata: Optional[Dict[str, Any]] = None,
+ name: Optional[str] = None,
+ **kwargs: Any,
+ ) -> Run:
+ """Create a retrieval run."""
+ start_time = datetime.now(timezone.utc)
+ if metadata:
+ kwargs.update({"metadata": metadata})
+ return Run(
+ id=run_id,
+ name=name or "Retriever",
+ parent_run_id=parent_run_id,
+ serialized=serialized,
+ inputs={"query": query},
+ extra=kwargs,
+ events=[{"name": "start", "time": start_time}],
+ start_time=start_time,
+ tags=tags,
+ child_runs=[],
+ run_type="retriever",
+ )
+
+ def _complete_retrieval_run(
+ self,
+ documents: Sequence[Document],
+ run_id: UUID,
+ **kwargs: Any,
+ ) -> Run:
+ """Update a retrieval run with outputs and end time."""
+ retrieval_run = self._get_run(run_id, run_type="retriever")
+ retrieval_run.outputs = {"documents": documents}
+ retrieval_run.end_time = datetime.now(timezone.utc)
+ retrieval_run.events.append({"name": "end", "time": retrieval_run.end_time})
+ return retrieval_run
+
+ def _errored_retrieval_run(
+ self,
+ error: BaseException,
+ run_id: UUID,
+ **kwargs: Any,
+ ) -> Run:
+ retrieval_run = self._get_run(run_id, run_type="retriever")
+ retrieval_run.error = self._get_stacktrace(error)
+ retrieval_run.end_time = datetime.now(timezone.utc)
+ retrieval_run.events.append({"name": "error", "time": retrieval_run.end_time})
+ return retrieval_run
+
+ def __deepcopy__(self, memo: dict) -> _TracerCore:
+ """Deepcopy the tracer."""
+ return self
+
+ def __copy__(self) -> _TracerCore:
+ """Copy the tracer."""
+ return self
+
+ def _end_trace(self, run: Run) -> Union[None, Coroutine[Any, Any, None]]:
+ """End a trace for a run."""
+
+ def _on_run_create(self, run: Run) -> Union[None, Coroutine[Any, Any, None]]:
+ """Process a run upon creation."""
+
+ def _on_run_update(self, run: Run) -> Union[None, Coroutine[Any, Any, None]]:
+ """Process a run upon update."""
+
+ def _on_llm_start(self, run: Run) -> Union[None, Coroutine[Any, Any, None]]:
+ """Process the LLM Run upon start."""
+
+ def _on_llm_new_token(
+ self,
+ run: Run,
+ token: str,
+ chunk: Optional[Union[GenerationChunk, ChatGenerationChunk]],
+ ) -> Union[None, Coroutine[Any, Any, None]]:
+ """Process new LLM token."""
+
+ def _on_llm_end(self, run: Run) -> Union[None, Coroutine[Any, Any, None]]:
+ """Process the LLM Run."""
+
+ def _on_llm_error(self, run: Run) -> Union[None, Coroutine[Any, Any, None]]:
+ """Process the LLM Run upon error."""
+
+ def _on_chain_start(self, run: Run) -> Union[None, Coroutine[Any, Any, None]]:
+ """Process the Chain Run upon start."""
+
+ def _on_chain_end(self, run: Run) -> Union[None, Coroutine[Any, Any, None]]:
+ """Process the Chain Run."""
+
+ def _on_chain_error(self, run: Run) -> Union[None, Coroutine[Any, Any, None]]:
+ """Process the Chain Run upon error."""
+
+ def _on_tool_start(self, run: Run) -> Union[None, Coroutine[Any, Any, None]]:
+ """Process the Tool Run upon start."""
+
+ def _on_tool_end(self, run: Run) -> Union[None, Coroutine[Any, Any, None]]:
+ """Process the Tool Run."""
+
+ def _on_tool_error(self, run: Run) -> Union[None, Coroutine[Any, Any, None]]:
+ """Process the Tool Run upon error."""
+
+ def _on_chat_model_start(self, run: Run) -> Union[None, Coroutine[Any, Any, None]]:
+ """Process the Chat Model Run upon start."""
+
+ def _on_retriever_start(self, run: Run) -> Union[None, Coroutine[Any, Any, None]]:
+ """Process the Retriever Run upon start."""
+
+ def _on_retriever_end(self, run: Run) -> Union[None, Coroutine[Any, Any, None]]:
+ """Process the Retriever Run."""
+
+ def _on_retriever_error(self, run: Run) -> Union[None, Coroutine[Any, Any, None]]:
+ """Process the Retriever Run upon error."""
diff --git a/libs/core/pyproject.toml b/libs/core/pyproject.toml
index 353729b2fe902..5b11badd770b2 100644
--- a/libs/core/pyproject.toml
+++ b/libs/core/pyproject.toml
@@ -1,25 +1,21 @@
-
[tool.poetry]
name = "gigachain-core"
-version = "0.2.0.1"
+version = "0.2.10"
description = "Building applications with LLMs through composability"
authors = []
license = "MIT"
readme = "README.md"
-repository = "https://github.com/ai-forever/gigachain"
-packages = [
- {include = "langchain_core"}
-]
+repository = "https://github.com/langchain-ai/langchain"
+
[tool.poetry.dependencies]
python = ">=3.8.1,<4.0"
-pydantic = ">=1,<3"
-langsmith = "^0.1.0"
-tenacity = "^8.1.0"
+pydantic = [{version = ">=1,<3", python = "<3.12.4"}, {version = "^2.7.4", python=">=3.12.4"}]
+langsmith = "^0.1.75"
+tenacity = "^8.1.0,!=8.4.0"
jsonpatch = "^1.33"
PyYAML = ">=5.3"
-packaging = "^23.2"
-jinja2 = { version = "^3", optional = true }
+packaging = ">=23.2,<25"
[tool.poetry.group.lint]
optional = true
@@ -61,7 +57,12 @@ pytest-asyncio = "^0.21.1"
grandalf = "^0.8"
pytest-profiling = "^1.7.0"
responses = "^0.25.0"
-numpy = "^1.24.0"
+
+# Support Python 3.8 and 3.12+.
+numpy = [
+ { version = "^1.24.0", python = "<3.12" },
+ { version = "^1.26.0", python = ">=3.12" },
+]
[tool.poetry.group.test_integration]
@@ -69,7 +70,6 @@ optional = true
dependencies = {}
[tool.poetry.extras]
-extended_testing = ["jinja2"]
[tool.ruff.lint]
select = [
@@ -81,7 +81,6 @@ select = [
[tool.mypy]
disallow_untyped_defs = "True"
-ignore_missing_imports = "True"
exclude = ["notebooks", "examples", "example_data", "langchain_core/pydantic"]
[[tool.mypy.overrides]]
@@ -115,4 +114,3 @@ markers = [
"compile: mark placeholder test used to compile integration tests without running them",
]
asyncio_mode = "auto"
-
diff --git a/libs/core/tests/unit_tests/documents/test_str.py b/libs/core/tests/unit_tests/documents/test_str.py
new file mode 100644
index 0000000000000..fd44d06a98ece
--- /dev/null
+++ b/libs/core/tests/unit_tests/documents/test_str.py
@@ -0,0 +1,20 @@
+from langchain_core.documents import Document
+
+
+def test_str() -> None:
+ assert str(Document(page_content="Hello, World!")) == "page_content='Hello, World!'"
+ assert (
+ str(Document(page_content="Hello, World!", metadata={"a": 3}))
+ == "page_content='Hello, World!' metadata={'a': 3}"
+ )
+
+
+def test_repr() -> None:
+ assert (
+ repr(Document(page_content="Hello, World!"))
+ == "Document(page_content='Hello, World!')"
+ )
+ assert (
+ repr(Document(page_content="Hello, World!", metadata={"a": 3}))
+ == "Document(page_content='Hello, World!', metadata={'a': 3})"
+ )
diff --git a/libs/core/tests/unit_tests/messages/test_utils.py b/libs/core/tests/unit_tests/messages/test_utils.py
new file mode 100644
index 0000000000000..0aea8d7e11b3c
--- /dev/null
+++ b/libs/core/tests/unit_tests/messages/test_utils.py
@@ -0,0 +1,337 @@
+from typing import Dict, List, Type
+
+import pytest
+
+from langchain_core.messages import (
+ AIMessage,
+ BaseMessage,
+ HumanMessage,
+ SystemMessage,
+ ToolCall,
+ ToolMessage,
+)
+from langchain_core.messages.utils import (
+ filter_messages,
+ merge_message_runs,
+ trim_messages,
+)
+
+
+@pytest.mark.parametrize("msg_cls", [HumanMessage, AIMessage, SystemMessage])
+def test_merge_message_runs_str(msg_cls: Type[BaseMessage]) -> None:
+ messages = [msg_cls("foo"), msg_cls("bar"), msg_cls("baz")]
+ messages_copy = [m.copy(deep=True) for m in messages]
+ expected = [msg_cls("foo\nbar\nbaz")]
+ actual = merge_message_runs(messages)
+ assert actual == expected
+ assert messages == messages_copy
+
+
+def test_merge_message_runs_content() -> None:
+ messages = [
+ AIMessage("foo", id="1"),
+ AIMessage(
+ [
+ {"text": "bar", "type": "text"},
+ {"image_url": "...", "type": "image_url"},
+ ],
+ tool_calls=[ToolCall(name="foo_tool", args={"x": 1}, id="tool1")],
+ id="2",
+ ),
+ AIMessage(
+ "baz",
+ tool_calls=[ToolCall(name="foo_tool", args={"x": 5}, id="tool2")],
+ id="3",
+ ),
+ ]
+ messages_copy = [m.copy(deep=True) for m in messages]
+ expected = [
+ AIMessage(
+ [
+ "foo",
+ {"text": "bar", "type": "text"},
+ {"image_url": "...", "type": "image_url"},
+ "baz",
+ ],
+ tool_calls=[
+ ToolCall(name="foo_tool", args={"x": 1}, id="tool1"),
+ ToolCall(name="foo_tool", args={"x": 5}, id="tool2"),
+ ],
+ id="1",
+ ),
+ ]
+ actual = merge_message_runs(messages)
+ assert actual == expected
+ invoked = merge_message_runs().invoke(messages)
+ assert actual == invoked
+ assert messages == messages_copy
+
+
+def test_merge_messages_tool_messages() -> None:
+ messages = [
+ ToolMessage("foo", tool_call_id="1"),
+ ToolMessage("bar", tool_call_id="2"),
+ ]
+ messages_copy = [m.copy(deep=True) for m in messages]
+ actual = merge_message_runs(messages)
+ assert actual == messages
+ assert messages == messages_copy
+
+
+@pytest.mark.parametrize(
+ "filters",
+ [
+ {"include_names": ["blur"]},
+ {"exclude_names": ["blah"]},
+ {"include_ids": ["2"]},
+ {"exclude_ids": ["1"]},
+ {"include_types": "human"},
+ {"include_types": ["human"]},
+ {"include_types": HumanMessage},
+ {"include_types": [HumanMessage]},
+ {"exclude_types": "system"},
+ {"exclude_types": ["system"]},
+ {"exclude_types": SystemMessage},
+ {"exclude_types": [SystemMessage]},
+ {"include_names": ["blah", "blur"], "exclude_types": [SystemMessage]},
+ ],
+)
+def test_filter_message(filters: Dict) -> None:
+ messages = [
+ SystemMessage("foo", name="blah", id="1"),
+ HumanMessage("bar", name="blur", id="2"),
+ ]
+ messages_copy = [m.copy(deep=True) for m in messages]
+ expected = messages[1:2]
+ actual = filter_messages(messages, **filters)
+ assert expected == actual
+ invoked = filter_messages(**filters).invoke(messages)
+ assert invoked == actual
+ assert messages == messages_copy
+
+
+_MESSAGES_TO_TRIM = [
+ SystemMessage("This is a 4 token text."),
+ HumanMessage("This is a 4 token text.", id="first"),
+ AIMessage(
+ [
+ {"type": "text", "text": "This is the FIRST 4 token block."},
+ {"type": "text", "text": "This is the SECOND 4 token block."},
+ ],
+ id="second",
+ ),
+ HumanMessage("This is a 4 token text.", id="third"),
+ AIMessage("This is a 4 token text.", id="fourth"),
+]
+
+_MESSAGES_TO_TRIM_COPY = [m.copy(deep=True) for m in _MESSAGES_TO_TRIM]
+
+
+def test_trim_messages_first_30() -> None:
+ expected = [
+ SystemMessage("This is a 4 token text."),
+ HumanMessage("This is a 4 token text.", id="first"),
+ ]
+ actual = trim_messages(
+ _MESSAGES_TO_TRIM,
+ max_tokens=30,
+ token_counter=dummy_token_counter,
+ strategy="first",
+ )
+ assert actual == expected
+ assert _MESSAGES_TO_TRIM == _MESSAGES_TO_TRIM_COPY
+
+
+def test_trim_messages_first_30_allow_partial() -> None:
+ expected = [
+ SystemMessage("This is a 4 token text."),
+ HumanMessage("This is a 4 token text.", id="first"),
+ AIMessage(
+ [{"type": "text", "text": "This is the FIRST 4 token block."}], id="second"
+ ),
+ ]
+ actual = trim_messages(
+ _MESSAGES_TO_TRIM,
+ max_tokens=30,
+ token_counter=dummy_token_counter,
+ strategy="first",
+ allow_partial=True,
+ )
+ assert actual == expected
+ assert _MESSAGES_TO_TRIM == _MESSAGES_TO_TRIM_COPY
+
+
+def test_trim_messages_first_30_allow_partial_end_on_human() -> None:
+ expected = [
+ SystemMessage("This is a 4 token text."),
+ HumanMessage("This is a 4 token text.", id="first"),
+ ]
+
+ actual = trim_messages(
+ _MESSAGES_TO_TRIM,
+ max_tokens=30,
+ token_counter=dummy_token_counter,
+ strategy="first",
+ allow_partial=True,
+ end_on="human",
+ )
+ assert actual == expected
+ assert _MESSAGES_TO_TRIM == _MESSAGES_TO_TRIM_COPY
+
+
+def test_trim_messages_last_30_include_system() -> None:
+ expected = [
+ SystemMessage("This is a 4 token text."),
+ HumanMessage("This is a 4 token text.", id="third"),
+ AIMessage("This is a 4 token text.", id="fourth"),
+ ]
+
+ actual = trim_messages(
+ _MESSAGES_TO_TRIM,
+ max_tokens=30,
+ include_system=True,
+ token_counter=dummy_token_counter,
+ strategy="last",
+ )
+ assert actual == expected
+ assert _MESSAGES_TO_TRIM == _MESSAGES_TO_TRIM_COPY
+
+
+def test_trim_messages_last_40_include_system_allow_partial() -> None:
+ expected = [
+ SystemMessage("This is a 4 token text."),
+ AIMessage(
+ [
+ {"type": "text", "text": "This is the SECOND 4 token block."},
+ ],
+ id="second",
+ ),
+ HumanMessage("This is a 4 token text.", id="third"),
+ AIMessage("This is a 4 token text.", id="fourth"),
+ ]
+
+ actual = trim_messages(
+ _MESSAGES_TO_TRIM,
+ max_tokens=40,
+ token_counter=dummy_token_counter,
+ strategy="last",
+ allow_partial=True,
+ include_system=True,
+ )
+
+ assert actual == expected
+ assert _MESSAGES_TO_TRIM == _MESSAGES_TO_TRIM_COPY
+
+
+def test_trim_messages_last_30_include_system_allow_partial_end_on_human() -> None:
+ expected = [
+ SystemMessage("This is a 4 token text."),
+ AIMessage(
+ [
+ {"type": "text", "text": "This is the SECOND 4 token block."},
+ ],
+ id="second",
+ ),
+ HumanMessage("This is a 4 token text.", id="third"),
+ ]
+
+ actual = trim_messages(
+ _MESSAGES_TO_TRIM,
+ max_tokens=30,
+ token_counter=dummy_token_counter,
+ strategy="last",
+ allow_partial=True,
+ include_system=True,
+ end_on="human",
+ )
+
+ assert actual == expected
+ assert _MESSAGES_TO_TRIM == _MESSAGES_TO_TRIM_COPY
+
+
+def test_trim_messages_last_40_include_system_allow_partial_start_on_human() -> None:
+ expected = [
+ SystemMessage("This is a 4 token text."),
+ HumanMessage("This is a 4 token text.", id="third"),
+ AIMessage("This is a 4 token text.", id="fourth"),
+ ]
+
+ actual = trim_messages(
+ _MESSAGES_TO_TRIM,
+ max_tokens=30,
+ token_counter=dummy_token_counter,
+ strategy="last",
+ allow_partial=True,
+ include_system=True,
+ start_on="human",
+ )
+
+ assert actual == expected
+ assert _MESSAGES_TO_TRIM == _MESSAGES_TO_TRIM_COPY
+
+
+def test_trim_messages_allow_partial_text_splitter() -> None:
+ expected = [
+ HumanMessage("a 4 token text.", id="third"),
+ AIMessage("This is a 4 token text.", id="fourth"),
+ ]
+
+ def count_words(msgs: List[BaseMessage]) -> int:
+ count = 0
+ for msg in msgs:
+ if isinstance(msg.content, str):
+ count += len(msg.content.split(" "))
+ else:
+ count += len(
+ " ".join(block["text"] for block in msg.content).split(" ") # type: ignore[index]
+ )
+ return count
+
+ def _split_on_space(text: str) -> List[str]:
+ splits = text.split(" ")
+ return [s + " " for s in splits[:-1]] + splits[-1:]
+
+ actual = trim_messages(
+ _MESSAGES_TO_TRIM,
+ max_tokens=10,
+ token_counter=count_words,
+ strategy="last",
+ allow_partial=True,
+ text_splitter=_split_on_space,
+ )
+ assert actual == expected
+ assert _MESSAGES_TO_TRIM == _MESSAGES_TO_TRIM_COPY
+
+
+def test_trim_messages_invoke() -> None:
+ actual = trim_messages(max_tokens=10, token_counter=dummy_token_counter).invoke(
+ _MESSAGES_TO_TRIM
+ )
+ expected = trim_messages(
+ _MESSAGES_TO_TRIM, max_tokens=10, token_counter=dummy_token_counter
+ )
+ assert actual == expected
+
+
+def dummy_token_counter(messages: List[BaseMessage]) -> int:
+ # treat each message like it adds 3 default tokens at the beginning
+ # of the message and at the end of the message. 3 + 4 + 3 = 10 tokens
+ # per message.
+
+ default_content_len = 4
+ default_msg_prefix_len = 3
+ default_msg_suffix_len = 3
+
+ count = 0
+ for msg in messages:
+ if isinstance(msg.content, str):
+ count += (
+ default_msg_prefix_len + default_content_len + default_msg_suffix_len
+ )
+ if isinstance(msg.content, list):
+ count += (
+ default_msg_prefix_len
+ + len(msg.content) * default_content_len
+ + default_msg_suffix_len
+ )
+ return count
diff --git a/libs/core/tests/unit_tests/runnables/test_tracing_interops.py b/libs/core/tests/unit_tests/runnables/test_tracing_interops.py
new file mode 100644
index 0000000000000..2f7f7ea252763
--- /dev/null
+++ b/libs/core/tests/unit_tests/runnables/test_tracing_interops.py
@@ -0,0 +1,201 @@
+import json
+import sys
+from unittest.mock import MagicMock, patch
+
+import pytest
+from langsmith import Client, traceable
+from langsmith.run_helpers import tracing_context
+
+from langchain_core.runnables.base import RunnableLambda
+from langchain_core.tracers.langchain import LangChainTracer
+
+
+def _get_posts(client: Client) -> list:
+ mock_calls = client.session.request.mock_calls # type: ignore
+ posts = []
+ for call in mock_calls:
+ if call.args:
+ if call.args[0] != "POST":
+ continue
+ assert call.args[0] == "POST"
+ assert call.args[1].startswith("https://api.smith.langchain.com")
+ body = json.loads(call.kwargs["data"])
+ if "post" in body:
+ # Batch request
+ assert body["post"]
+ posts.extend(body["post"])
+ else:
+ posts.append(body)
+ return posts
+
+
+def test_config_traceable_handoff() -> None:
+ mock_session = MagicMock()
+ mock_client_ = Client(
+ session=mock_session, api_key="test", auto_batch_tracing=False
+ )
+ tracer = LangChainTracer(client=mock_client_)
+
+ @traceable
+ def my_great_great_grandchild_function(a: int) -> int:
+ return a + 1
+
+ @RunnableLambda
+ def my_great_grandchild_function(a: int) -> int:
+ return my_great_great_grandchild_function(a)
+
+ @RunnableLambda
+ def my_grandchild_function(a: int) -> int:
+ return my_great_grandchild_function.invoke(a)
+
+ @traceable
+ def my_child_function(a: int) -> int:
+ return my_grandchild_function.invoke(a) * 3
+
+ @traceable()
+ def my_function(a: int) -> int:
+ return my_child_function(a)
+
+ def my_parent_function(a: int) -> int:
+ return my_function(a)
+
+ my_parent_runnable = RunnableLambda(my_parent_function)
+
+ assert my_parent_runnable.invoke(1, {"callbacks": [tracer]}) == 6
+ posts = _get_posts(mock_client_)
+ # There should have been 6 runs created,
+ # one for each function invocation
+ assert len(posts) == 6
+ name_to_body = {post["name"]: post for post in posts}
+ ordered_names = [
+ "my_parent_function",
+ "my_function",
+ "my_child_function",
+ "my_grandchild_function",
+ "my_great_grandchild_function",
+ "my_great_great_grandchild_function",
+ ]
+ trace_id = posts[0]["trace_id"]
+ last_dotted_order = None
+ parent_run_id = None
+ for name in ordered_names:
+ id_ = name_to_body[name]["id"]
+ parent_run_id_ = name_to_body[name]["parent_run_id"]
+ if parent_run_id_ is not None:
+ assert parent_run_id == parent_run_id_
+ assert name in name_to_body
+ # All within the same trace
+ assert name_to_body[name]["trace_id"] == trace_id
+ dotted_order: str = name_to_body[name]["dotted_order"]
+ assert dotted_order is not None
+ if last_dotted_order is not None:
+ assert dotted_order > last_dotted_order
+ assert dotted_order.startswith(last_dotted_order), (
+ "Unexpected dotted order for run"
+ f" {name}\n{dotted_order}\n{last_dotted_order}"
+ )
+ last_dotted_order = dotted_order
+ parent_run_id = id_
+
+
+@pytest.mark.skipif(
+ sys.version_info < (3, 11), reason="Asyncio context vars require Python 3.11+"
+)
+async def test_config_traceable_async_handoff() -> None:
+ mock_session = MagicMock()
+ mock_client_ = Client(
+ session=mock_session, api_key="test", auto_batch_tracing=False
+ )
+ tracer = LangChainTracer(client=mock_client_)
+
+ @traceable
+ def my_great_great_grandchild_function(a: int) -> int:
+ return a + 1
+
+ @RunnableLambda
+ def my_great_grandchild_function(a: int) -> int:
+ return my_great_great_grandchild_function(a)
+
+ @RunnableLambda # type: ignore
+ async def my_grandchild_function(a: int) -> int:
+ return my_great_grandchild_function.invoke(a)
+
+ @traceable
+ async def my_child_function(a: int) -> int:
+ return await my_grandchild_function.ainvoke(a) * 3 # type: ignore
+
+ @traceable()
+ async def my_function(a: int) -> int:
+ return await my_child_function(a)
+
+ async def my_parent_function(a: int) -> int:
+ return await my_function(a)
+
+ my_parent_runnable = RunnableLambda(my_parent_function) # type: ignore
+ result = await my_parent_runnable.ainvoke(1, {"callbacks": [tracer]})
+ assert result == 6
+ posts = _get_posts(mock_client_)
+ # There should have been 6 runs created,
+ # one for each function invocation
+ assert len(posts) == 6
+ name_to_body = {post["name"]: post for post in posts}
+ ordered_names = [
+ "my_parent_function",
+ "my_function",
+ "my_child_function",
+ "my_grandchild_function",
+ "my_great_grandchild_function",
+ "my_great_great_grandchild_function",
+ ]
+ trace_id = posts[0]["trace_id"]
+ last_dotted_order = None
+ parent_run_id = None
+ for name in ordered_names:
+ id_ = name_to_body[name]["id"]
+ parent_run_id_ = name_to_body[name]["parent_run_id"]
+ if parent_run_id_ is not None:
+ assert parent_run_id == parent_run_id_
+ assert name in name_to_body
+ # All within the same trace
+ assert name_to_body[name]["trace_id"] == trace_id
+ dotted_order: str = name_to_body[name]["dotted_order"]
+ assert dotted_order is not None
+ if last_dotted_order is not None:
+ assert dotted_order > last_dotted_order
+ assert dotted_order.startswith(last_dotted_order), (
+ "Unexpected dotted order for run"
+ f" {name}\n{dotted_order}\n{last_dotted_order}"
+ )
+ last_dotted_order = dotted_order
+ parent_run_id = id_
+
+
+@patch("langchain_core.tracers.langchain.get_client")
+@pytest.mark.parametrize("enabled", [None, True, False])
+@pytest.mark.parametrize("env", ["", "true"])
+def test_tracing_enable_disable(
+ mock_get_client: MagicMock, enabled: bool, env: str
+) -> None:
+ mock_session = MagicMock()
+ mock_client_ = Client(
+ session=mock_session, api_key="test", auto_batch_tracing=False
+ )
+ mock_get_client.return_value = mock_client_
+
+ def my_func(a: int) -> int:
+ return a + 1
+
+ env_on = env == "true"
+ with patch.dict("os.environ", {"LANGSMITH_TRACING": env}):
+ with tracing_context(enabled=enabled):
+ RunnableLambda(my_func).invoke(1)
+
+ mock_posts = _get_posts(mock_client_)
+ if enabled is True:
+ assert len(mock_posts) == 1
+ elif enabled is False:
+ assert not mock_posts
+ elif env_on:
+ assert len(mock_posts) == 1
+ else:
+ assert not mock_posts
diff --git a/libs/core/tests/unit_tests/tracers/test_async_base_tracer.py b/libs/core/tests/unit_tests/tracers/test_async_base_tracer.py
new file mode 100644
index 0000000000000..f1f04c526cc61
--- /dev/null
+++ b/libs/core/tests/unit_tests/tracers/test_async_base_tracer.py
@@ -0,0 +1,598 @@
+"""Test Tracer classes."""
+
+from __future__ import annotations
+
+from datetime import datetime, timezone
+from typing import Any, List
+from uuid import uuid4
+
+import pytest
+from freezegun import freeze_time
+
+from langchain_core.callbacks import AsyncCallbackManager
+from langchain_core.exceptions import TracerException
+from langchain_core.messages import HumanMessage
+from langchain_core.outputs import LLMResult
+from langchain_core.tracers.base import AsyncBaseTracer
+from langchain_core.tracers.schemas import Run
+
+SERIALIZED = {"id": ["llm"]}
+SERIALIZED_CHAT = {"id": ["chat_model"]}
+
+
+class FakeAsyncTracer(AsyncBaseTracer):
+ """Fake tracer to test async based tracers."""
+
+ def __init__(self) -> None:
+ """Initialize the tracer."""
+ super().__init__()
+ self.runs: List[Run] = []
+
+ async def _persist_run(self, run: Run) -> None:
+ self.runs.append(run)
+
+
+def _compare_run_with_error(run: Any, expected_run: Any) -> None:
+ if run.child_runs:
+ assert len(expected_run.child_runs) == len(run.child_runs)
+ for received, expected in zip(run.child_runs, expected_run.child_runs):
+ _compare_run_with_error(received, expected)
+ received = run.dict(exclude={"child_runs"})
+ received_err = received.pop("error")
+ expected = expected_run.dict(exclude={"child_runs"})
+ expected_err = expected.pop("error")
+
+ assert received == expected
+ if expected_err is not None:
+ assert received_err is not None
+ assert expected_err in received_err
+ else:
+ assert received_err is None
+
+
+@freeze_time("2023-01-01")
+async def test_tracer_llm_run() -> None:
+ """Test tracer on an LLM run."""
+ uuid = uuid4()
+ compare_run = Run( # type: ignore[call-arg]
+ id=uuid,
+ parent_run_id=None,
+ start_time=datetime.now(timezone.utc),
+ end_time=datetime.now(timezone.utc),
+ events=[
+ {"name": "start", "time": datetime.now(timezone.utc)},
+ {"name": "end", "time": datetime.now(timezone.utc)},
+ ],
+ extra={},
+ serialized=SERIALIZED,
+ inputs={"prompts": []},
+ outputs=LLMResult(generations=[[]]), # type: ignore[arg-type]
+ error=None,
+ run_type="llm",
+ trace_id=uuid,
+ dotted_order=f"20230101T000000000000Z{uuid}",
+ )
+ tracer = FakeAsyncTracer()
+
+ await tracer.on_llm_start(serialized=SERIALIZED, prompts=[], run_id=uuid)
+ await tracer.on_llm_end(response=LLMResult(generations=[[]]), run_id=uuid)
+ assert tracer.runs == [compare_run]
+
+
+@freeze_time("2023-01-01")
+async def test_tracer_chat_model_run() -> None:
+ """Test tracer on a Chat Model run."""
+ tracer = FakeAsyncTracer()
+ manager = AsyncCallbackManager(handlers=[tracer])
+ run_managers = await manager.on_chat_model_start(
+ serialized=SERIALIZED_CHAT, messages=[[HumanMessage(content="")]]
+ )
+ compare_run = Run(
+ id=str(run_managers[0].run_id), # type: ignore[arg-type]
+ name="chat_model",
+ start_time=datetime.now(timezone.utc),
+ end_time=datetime.now(timezone.utc),
+ events=[
+ {"name": "start", "time": datetime.now(timezone.utc)},
+ {"name": "end", "time": datetime.now(timezone.utc)},
+ ],
+ extra={},
+ serialized=SERIALIZED_CHAT,
+ inputs=dict(prompts=["Human: "]),
+ outputs=LLMResult(generations=[[]]), # type: ignore[arg-type]
+ error=None,
+ run_type="llm",
+ trace_id=run_managers[0].run_id,
+ dotted_order=f"20230101T000000000000Z{run_managers[0].run_id}",
+ )
+ for run_manager in run_managers:
+ await run_manager.on_llm_end(response=LLMResult(generations=[[]]))
+ assert tracer.runs == [compare_run]
+
+
+@freeze_time("2023-01-01")
+async def test_tracer_llm_run_errors_no_start() -> None:
+ """Test tracer on an LLM run without a start."""
+ tracer = FakeAsyncTracer()
+
+ with pytest.raises(TracerException):
+ await tracer.on_llm_end(response=LLMResult(generations=[[]]), run_id=uuid4())
+
+
+@freeze_time("2023-01-01")
+async def test_tracer_multiple_llm_runs() -> None:
+ """Test the tracer with multiple runs."""
+ uuid = uuid4()
+ compare_run = Run(
+ id=uuid,
+ name="llm",
+ start_time=datetime.now(timezone.utc),
+ end_time=datetime.now(timezone.utc),
+ events=[
+ {"name": "start", "time": datetime.now(timezone.utc)},
+ {"name": "end", "time": datetime.now(timezone.utc)},
+ ],
+ extra={},
+ serialized=SERIALIZED,
+ inputs=dict(prompts=[]),
+ outputs=LLMResult(generations=[[]]), # type: ignore[arg-type]
+ error=None,
+ run_type="llm",
+ trace_id=uuid,
+ dotted_order=f"20230101T000000000000Z{uuid}",
+ )
+ tracer = FakeAsyncTracer()
+
+ num_runs = 10
+ for _ in range(num_runs):
+ await tracer.on_llm_start(serialized=SERIALIZED, prompts=[], run_id=uuid)
+ await tracer.on_llm_end(response=LLMResult(generations=[[]]), run_id=uuid)
+
+ assert tracer.runs == [compare_run] * num_runs
+
+
+@freeze_time("2023-01-01")
+async def test_tracer_chain_run() -> None:
+ """Test tracer on a Chain run."""
+ uuid = uuid4()
+ compare_run = Run( # type: ignore[call-arg]
+ id=str(uuid), # type: ignore[arg-type]
+ start_time=datetime.now(timezone.utc),
+ end_time=datetime.now(timezone.utc),
+ events=[
+ {"name": "start", "time": datetime.now(timezone.utc)},
+ {"name": "end", "time": datetime.now(timezone.utc)},
+ ],
+ extra={},
+ serialized={"name": "chain"},
+ inputs={},
+ outputs={},
+ error=None,
+ run_type="chain",
+ trace_id=uuid,
+ dotted_order=f"20230101T000000000000Z{uuid}",
+ )
+ tracer = FakeAsyncTracer()
+
+ await tracer.on_chain_start(serialized={"name": "chain"}, inputs={}, run_id=uuid)
+ await tracer.on_chain_end(outputs={}, run_id=uuid)
+ assert tracer.runs == [compare_run]
+
+
+@freeze_time("2023-01-01")
+async def test_tracer_tool_run() -> None:
+ """Test tracer on a Tool run."""
+ uuid = uuid4()
+ compare_run = Run( # type: ignore[call-arg]
+ id=str(uuid), # type: ignore[arg-type]
+ start_time=datetime.now(timezone.utc),
+ end_time=datetime.now(timezone.utc),
+ events=[
+ {"name": "start", "time": datetime.now(timezone.utc)},
+ {"name": "end", "time": datetime.now(timezone.utc)},
+ ],
+ extra={},
+ serialized={"name": "tool"},
+ inputs={"input": "test"},
+ outputs={"output": "test"},
+ error=None,
+ run_type="tool",
+ trace_id=uuid,
+ dotted_order=f"20230101T000000000000Z{uuid}",
+ )
+ tracer = FakeAsyncTracer()
+ await tracer.on_tool_start(
+ serialized={"name": "tool"}, input_str="test", run_id=uuid
+ )
+ await tracer.on_tool_end("test", run_id=uuid)
+ assert tracer.runs == [compare_run]
+
+
+@freeze_time("2023-01-01")
+async def test_tracer_nested_run() -> None:
+ """Test tracer on a nested run."""
+ tracer = FakeAsyncTracer()
+
+ chain_uuid = uuid4()
+ tool_uuid = uuid4()
+ llm_uuid1 = uuid4()
+ llm_uuid2 = uuid4()
+ for _ in range(10):
+ await tracer.on_chain_start(
+ serialized={"name": "chain"}, inputs={}, run_id=chain_uuid
+ )
+ await tracer.on_tool_start(
+ serialized={"name": "tool"},
+ input_str="test",
+ run_id=tool_uuid,
+ parent_run_id=chain_uuid,
+ )
+ await tracer.on_llm_start(
+ serialized=SERIALIZED,
+ prompts=[],
+ run_id=llm_uuid1,
+ parent_run_id=tool_uuid,
+ )
+ await tracer.on_llm_end(response=LLMResult(generations=[[]]), run_id=llm_uuid1)
+ await tracer.on_tool_end("test", run_id=tool_uuid)
+ await tracer.on_llm_start(
+ serialized=SERIALIZED,
+ prompts=[],
+ run_id=llm_uuid2,
+ parent_run_id=chain_uuid,
+ )
+ await tracer.on_llm_end(response=LLMResult(generations=[[]]), run_id=llm_uuid2)
+ await tracer.on_chain_end(outputs={}, run_id=chain_uuid)
+
+ compare_run = Run( # type: ignore[call-arg]
+ id=str(chain_uuid), # type: ignore[arg-type]
+ error=None,
+ start_time=datetime.now(timezone.utc),
+ end_time=datetime.now(timezone.utc),
+ events=[
+ {"name": "start", "time": datetime.now(timezone.utc)},
+ {"name": "end", "time": datetime.now(timezone.utc)},
+ ],
+ extra={},
+ serialized={"name": "chain"},
+ inputs={},
+ outputs={},
+ run_type="chain",
+ trace_id=chain_uuid,
+ dotted_order=f"20230101T000000000000Z{chain_uuid}",
+ child_runs=[
+ Run( # type: ignore[call-arg]
+ id=tool_uuid,
+ parent_run_id=chain_uuid,
+ start_time=datetime.now(timezone.utc),
+ end_time=datetime.now(timezone.utc),
+ events=[
+ {"name": "start", "time": datetime.now(timezone.utc)},
+ {"name": "end", "time": datetime.now(timezone.utc)},
+ ],
+ extra={},
+ serialized={"name": "tool"},
+ inputs=dict(input="test"),
+ outputs=dict(output="test"),
+ error=None,
+ run_type="tool",
+ trace_id=chain_uuid,
+ dotted_order=f"20230101T000000000000Z{chain_uuid}.20230101T000000000000Z{tool_uuid}",
+ child_runs=[
+ Run( # type: ignore[call-arg]
+ id=str(llm_uuid1), # type: ignore[arg-type]
+ parent_run_id=str(tool_uuid), # type: ignore[arg-type]
+ error=None,
+ start_time=datetime.now(timezone.utc),
+ end_time=datetime.now(timezone.utc),
+ events=[
+ {"name": "start", "time": datetime.now(timezone.utc)},
+ {"name": "end", "time": datetime.now(timezone.utc)},
+ ],
+ extra={},
+ serialized=SERIALIZED,
+ inputs=dict(prompts=[]),
+ outputs=LLMResult(generations=[[]]), # type: ignore[arg-type]
+ run_type="llm",
+ trace_id=chain_uuid,
+ dotted_order=f"20230101T000000000000Z{chain_uuid}.20230101T000000000000Z{tool_uuid}.20230101T000000000000Z{llm_uuid1}",
+ )
+ ],
+ ),
+ Run( # type: ignore[call-arg]
+ id=str(llm_uuid2), # type: ignore[arg-type]
+ parent_run_id=str(chain_uuid), # type: ignore[arg-type]
+ error=None,
+ start_time=datetime.now(timezone.utc),
+ end_time=datetime.now(timezone.utc),
+ events=[
+ {"name": "start", "time": datetime.now(timezone.utc)},
+ {"name": "end", "time": datetime.now(timezone.utc)},
+ ],
+ extra={},
+ serialized=SERIALIZED,
+ inputs=dict(prompts=[]),
+ outputs=LLMResult(generations=[[]]), # type: ignore[arg-type]
+ run_type="llm",
+ trace_id=chain_uuid,
+ dotted_order=f"20230101T000000000000Z{chain_uuid}.20230101T000000000000Z{llm_uuid2}",
+ ),
+ ],
+ )
+ assert tracer.runs[0] == compare_run
+ assert tracer.runs == [compare_run] * 10
+
+
+@freeze_time("2023-01-01")
+async def test_tracer_llm_run_on_error() -> None:
+ """Test tracer on an LLM run with an error."""
+ exception = Exception("test")
+ uuid = uuid4()
+
+ compare_run = Run( # type: ignore[call-arg]
+ id=str(uuid), # type: ignore[arg-type]
+ start_time=datetime.now(timezone.utc),
+ end_time=datetime.now(timezone.utc),
+ events=[
+ {"name": "start", "time": datetime.now(timezone.utc)},
+ {"name": "error", "time": datetime.now(timezone.utc)},
+ ],
+ extra={},
+ serialized=SERIALIZED,
+ inputs=dict(prompts=[]),
+ outputs=None,
+ error=repr(exception),
+ run_type="llm",
+ trace_id=uuid,
+ dotted_order=f"20230101T000000000000Z{uuid}",
+ )
+ tracer = FakeAsyncTracer()
+
+ await tracer.on_llm_start(serialized=SERIALIZED, prompts=[], run_id=uuid)
+ await tracer.on_llm_error(exception, run_id=uuid)
+ assert len(tracer.runs) == 1
+ _compare_run_with_error(tracer.runs[0], compare_run)
+
+
+@freeze_time("2023-01-01")
+async def test_tracer_llm_run_on_error_callback() -> None:
+ """Test tracer on an LLM run with an error and a callback."""
+ exception = Exception("test")
+ uuid = uuid4()
+
+ compare_run = Run( # type: ignore[call-arg]
+ id=str(uuid), # type: ignore[arg-type]
+ start_time=datetime.now(timezone.utc),
+ end_time=datetime.now(timezone.utc),
+ events=[
+ {"name": "start", "time": datetime.now(timezone.utc)},
+ {"name": "error", "time": datetime.now(timezone.utc)},
+ ],
+ extra={},
+ serialized=SERIALIZED,
+ inputs=dict(prompts=[]),
+ outputs=None,
+ error=repr(exception),
+ run_type="llm",
+ trace_id=uuid,
+ dotted_order=f"20230101T000000000000Z{uuid}",
+ )
+
+ class FakeTracerWithLlmErrorCallback(FakeAsyncTracer):
+ error_run = None
+
+ async def _on_llm_error(self, run: Run) -> None:
+ self.error_run = run
+
+ tracer = FakeTracerWithLlmErrorCallback()
+ await tracer.on_llm_start(serialized=SERIALIZED, prompts=[], run_id=uuid)
+ await tracer.on_llm_error(exception, run_id=uuid)
+ _compare_run_with_error(tracer.error_run, compare_run)
+
+
+@freeze_time("2023-01-01")
+async def test_tracer_chain_run_on_error() -> None:
+ """Test tracer on a Chain run with an error."""
+ exception = Exception("test")
+ uuid = uuid4()
+
+ compare_run = Run( # type: ignore[call-arg]
+ id=str(uuid), # type: ignore[arg-type]
+ start_time=datetime.now(timezone.utc),
+ end_time=datetime.now(timezone.utc),
+ events=[
+ {"name": "start", "time": datetime.now(timezone.utc)},
+ {"name": "error", "time": datetime.now(timezone.utc)},
+ ],
+ extra={},
+ serialized={"name": "chain"},
+ inputs={},
+ outputs=None,
+ error=repr(exception),
+ run_type="chain",
+ trace_id=uuid,
+ dotted_order=f"20230101T000000000000Z{uuid}",
+ )
+ tracer = FakeAsyncTracer()
+
+ await tracer.on_chain_start(serialized={"name": "chain"}, inputs={}, run_id=uuid)
+ await tracer.on_chain_error(exception, run_id=uuid)
+ _compare_run_with_error(tracer.runs[0], compare_run)
+
+
+@freeze_time("2023-01-01")
+async def test_tracer_tool_run_on_error() -> None:
+ """Test tracer on a Tool run with an error."""
+ exception = Exception("test")
+ uuid = uuid4()
+
+ compare_run = Run( # type: ignore[call-arg]
+ id=str(uuid), # type: ignore[arg-type]
+ start_time=datetime.now(timezone.utc),
+ end_time=datetime.now(timezone.utc),
+ events=[
+ {"name": "start", "time": datetime.now(timezone.utc)},
+ {"name": "error", "time": datetime.now(timezone.utc)},
+ ],
+ extra={},
+ serialized={"name": "tool"},
+ inputs=dict(input="test"),
+ outputs=None,
+ action="{'name': 'tool'}",
+ error=repr(exception),
+ run_type="tool",
+ trace_id=uuid,
+ dotted_order=f"20230101T000000000000Z{uuid}",
+ )
+ tracer = FakeAsyncTracer()
+
+ await tracer.on_tool_start(
+ serialized={"name": "tool"}, input_str="test", run_id=uuid
+ )
+ await tracer.on_tool_error(exception, run_id=uuid)
+ _compare_run_with_error(tracer.runs[0], compare_run)
+
+
+@freeze_time("2023-01-01")
+async def test_tracer_nested_runs_on_error() -> None:
+ """Test tracer on a nested run with an error."""
+ exception = Exception("test")
+
+ tracer = FakeAsyncTracer()
+ chain_uuid = uuid4()
+ tool_uuid = uuid4()
+ llm_uuid1 = uuid4()
+ llm_uuid2 = uuid4()
+ llm_uuid3 = uuid4()
+
+ for _ in range(3):
+ await tracer.on_chain_start(
+ serialized={"name": "chain"}, inputs={}, run_id=chain_uuid
+ )
+ await tracer.on_llm_start(
+ serialized=SERIALIZED,
+ prompts=[],
+ run_id=llm_uuid1,
+ parent_run_id=chain_uuid,
+ )
+ await tracer.on_llm_end(response=LLMResult(generations=[[]]), run_id=llm_uuid1)
+ await tracer.on_llm_start(
+ serialized=SERIALIZED,
+ prompts=[],
+ run_id=llm_uuid2,
+ parent_run_id=chain_uuid,
+ )
+ await tracer.on_llm_end(response=LLMResult(generations=[[]]), run_id=llm_uuid2)
+ await tracer.on_tool_start(
+ serialized={"name": "tool"},
+ input_str="test",
+ run_id=tool_uuid,
+ parent_run_id=chain_uuid,
+ )
+ await tracer.on_llm_start(
+ serialized=SERIALIZED,
+ prompts=[],
+ run_id=llm_uuid3,
+ parent_run_id=tool_uuid,
+ )
+ await tracer.on_llm_error(exception, run_id=llm_uuid3)
+ await tracer.on_tool_error(exception, run_id=tool_uuid)
+ await tracer.on_chain_error(exception, run_id=chain_uuid)
+
+ compare_run = Run( # type: ignore[call-arg]
+ id=str(chain_uuid), # type: ignore[arg-type]
+ start_time=datetime.now(timezone.utc),
+ end_time=datetime.now(timezone.utc),
+ events=[
+ {"name": "start", "time": datetime.now(timezone.utc)},
+ {"name": "error", "time": datetime.now(timezone.utc)},
+ ],
+ extra={},
+ serialized={"name": "chain"},
+ error=repr(exception),
+ inputs={},
+ outputs=None,
+ run_type="chain",
+ trace_id=chain_uuid,
+ dotted_order=f"20230101T000000000000Z{chain_uuid}",
+ child_runs=[
+ Run( # type: ignore[call-arg]
+ id=str(llm_uuid1), # type: ignore[arg-type]
+ parent_run_id=str(chain_uuid), # type: ignore[arg-type]
+ start_time=datetime.now(timezone.utc),
+ end_time=datetime.now(timezone.utc),
+ events=[
+ {"name": "start", "time": datetime.now(timezone.utc)},
+ {"name": "end", "time": datetime.now(timezone.utc)},
+ ],
+ extra={},
+ serialized=SERIALIZED,
+ error=None,
+ inputs=dict(prompts=[]),
+ outputs=LLMResult(generations=[[]], llm_output=None), # type: ignore[arg-type]
+ run_type="llm",
+ trace_id=chain_uuid,
+ dotted_order=f"20230101T000000000000Z{chain_uuid}.20230101T000000000000Z{llm_uuid1}",
+ ),
+ Run( # type: ignore[call-arg]
+ id=str(llm_uuid2), # type: ignore[arg-type]
+ parent_run_id=str(chain_uuid), # type: ignore[arg-type]
+ start_time=datetime.now(timezone.utc),
+ end_time=datetime.now(timezone.utc),
+ events=[
+ {"name": "start", "time": datetime.now(timezone.utc)},
+ {"name": "end", "time": datetime.now(timezone.utc)},
+ ],
+ extra={},
+ serialized=SERIALIZED,
+ error=None,
+ inputs=dict(prompts=[]),
+ outputs=LLMResult(generations=[[]], llm_output=None), # type: ignore[arg-type]
+ run_type="llm",
+ trace_id=chain_uuid,
+ dotted_order=f"20230101T000000000000Z{chain_uuid}.20230101T000000000000Z{llm_uuid2}",
+ ),
+ Run( # type: ignore[call-arg]
+ id=str(tool_uuid), # type: ignore[arg-type]
+ parent_run_id=str(chain_uuid), # type: ignore[arg-type]
+ start_time=datetime.now(timezone.utc),
+ end_time=datetime.now(timezone.utc),
+ events=[
+ {"name": "start", "time": datetime.now(timezone.utc)},
+ {"name": "error", "time": datetime.now(timezone.utc)},
+ ],
+ extra={},
+ serialized={"name": "tool"},
+ error=repr(exception),
+ inputs=dict(input="test"),
+ outputs=None,
+ action="{'name': 'tool'}",
+ trace_id=chain_uuid,
+ dotted_order=f"20230101T000000000000Z{chain_uuid}.20230101T000000000000Z{tool_uuid}",
+ child_runs=[
+ Run( # type: ignore[call-arg]
+ id=str(llm_uuid3), # type: ignore[arg-type]
+ parent_run_id=str(tool_uuid), # type: ignore[arg-type]
+ start_time=datetime.now(timezone.utc),
+ end_time=datetime.now(timezone.utc),
+ events=[
+ {"name": "start", "time": datetime.now(timezone.utc)},
+ {"name": "error", "time": datetime.now(timezone.utc)},
+ ],
+ extra={},
+ serialized=SERIALIZED,
+ error=repr(exception),
+ inputs=dict(prompts=[]),
+ outputs=None,
+ run_type="llm",
+ trace_id=chain_uuid,
+ dotted_order=f"20230101T000000000000Z{chain_uuid}.20230101T000000000000Z{tool_uuid}.20230101T000000000000Z{llm_uuid3}",
+ )
+ ],
+ run_type="tool",
+ ),
+ ],
+ )
+ assert len(tracer.runs) == 3
+ for run in tracer.runs:
+ _compare_run_with_error(run, compare_run)
diff --git a/libs/core/tests/unit_tests/utils/test_env.py b/libs/core/tests/unit_tests/utils/test_env.py
new file mode 100644
index 0000000000000..3cf6d027354af
--- /dev/null
+++ b/libs/core/tests/unit_tests/utils/test_env.py
@@ -0,0 +1,64 @@
+import pytest
+
+from langchain_core.utils.env import get_from_dict_or_env
+
+
+def test_get_from_dict_or_env() -> None:
+ assert (
+ get_from_dict_or_env(
+ {
+ "a": "foo",
+ },
+ ["a"],
+ "__SOME_KEY_IN_ENV",
+ )
+ == "foo"
+ )
+
+ assert (
+ get_from_dict_or_env(
+ {
+ "a": "foo",
+ },
+ ["b", "a"],
+ "__SOME_KEY_IN_ENV",
+ )
+ == "foo"
+ )
+
+ assert (
+ get_from_dict_or_env(
+ {
+ "a": "foo",
+ },
+ "a",
+ "__SOME_KEY_IN_ENV",
+ )
+ == "foo"
+ )
+
+ assert (
+ get_from_dict_or_env(
+ {
+ "a": "foo",
+ },
+ "not exists",
+ "__SOME_KEY_IN_ENV",
+ default="default",
+ )
+ == "default"
+ )
+
+ # Not the most obvious behavior, but
+ # this is how it works right now
+ with pytest.raises(ValueError):
+ assert (
+ get_from_dict_or_env(
+ {
+ "a": "foo",
+ },
+ "not exists",
+ "__SOME_KEY_IN_ENV",
+ )
+ is None
+ )
diff --git a/libs/experimental/extended_testing_deps.txt b/libs/experimental/extended_testing_deps.txt
new file mode 100644
index 0000000000000..06ab41ba342f1
--- /dev/null
+++ b/libs/experimental/extended_testing_deps.txt
@@ -0,0 +1,8 @@
+presidio-anonymizer>=2.2.352,<3
+presidio-analyzer>=2.2.352,<3
+faker>=19.3.1,<20
+vowpal-wabbit-next==0.7.0
+sentence-transformers>=2,<3
+jinja2>=3,<4
+pandas>=2.0.1,<3
+tabulate>=0.9.0,<1
diff --git a/libs/experimental/langchain_experimental/fallacy_removal/base.py b/libs/experimental/langchain_experimental/fallacy_removal/base.py
new file mode 100644
index 0000000000000..97df55e798dff
--- /dev/null
+++ b/libs/experimental/langchain_experimental/fallacy_removal/base.py
@@ -0,0 +1,182 @@
+"""Chain for applying removals of logical fallacies."""
+from __future__ import annotations
+
+from typing import Any, Dict, List, Optional
+
+from langchain.chains.base import Chain
+from langchain.chains.llm import LLMChain
+from langchain.schema import BasePromptTemplate
+from langchain_core.callbacks.manager import CallbackManagerForChainRun
+from langchain_core.language_models import BaseLanguageModel
+
+from langchain_experimental.fallacy_removal.fallacies import FALLACIES
+from langchain_experimental.fallacy_removal.models import LogicalFallacy
+from langchain_experimental.fallacy_removal.prompts import (
+ FALLACY_CRITIQUE_PROMPT,
+ FALLACY_REVISION_PROMPT,
+)
+
+
+class FallacyChain(Chain):
+ """Chain for applying logical fallacy evaluations.
+
+ It is modeled after Constitutional AI and in same format, but
+ applying logical fallacies as generalized rules to remove in output.
+
+ Example:
+ .. code-block:: python
+
+ from langchain_community.llms import OpenAI
+ from langchain.chains import LLMChain
+ from langchain_experimental.fallacy import FallacyChain
+ from langchain_experimental.fallacy_removal.models import LogicalFallacy
+
+ llm = OpenAI()
+
+ qa_prompt = PromptTemplate(
+ template="Q: {question} A:",
+ input_variables=["question"],
+ )
+ qa_chain = LLMChain(llm=llm, prompt=qa_prompt)
+
+ fallacy_chain = FallacyChain.from_llm(
+ llm=llm,
+ chain=qa_chain,
+ logical_fallacies=[
+ LogicalFallacy(
+ fallacy_critique_request="Tell if this answer meets criteria.",
+ fallacy_revision_request=\
+ "Give an answer that meets better criteria.",
+ )
+ ],
+ )
+
+ fallacy_chain.run(question="How do I know if the earth is round?")
+ """
+
+ chain: LLMChain
+ logical_fallacies: List[LogicalFallacy]
+ fallacy_critique_chain: LLMChain
+ fallacy_revision_chain: LLMChain
+ return_intermediate_steps: bool = False
+
+ @classmethod
+ def get_fallacies(cls, names: Optional[List[str]] = None) -> List[LogicalFallacy]:
+ if names is None:
+ return list(FALLACIES.values())
+ else:
+ return [FALLACIES[name] for name in names]
+
+ @classmethod
+ def from_llm(
+ cls,
+ llm: BaseLanguageModel,
+ chain: LLMChain,
+ fallacy_critique_prompt: BasePromptTemplate = FALLACY_CRITIQUE_PROMPT,
+ fallacy_revision_prompt: BasePromptTemplate = FALLACY_REVISION_PROMPT,
+ **kwargs: Any,
+ ) -> "FallacyChain":
+ """Create a chain from an LLM."""
+ fallacy_critique_chain = LLMChain(llm=llm, prompt=fallacy_critique_prompt)
+ fallacy_revision_chain = LLMChain(llm=llm, prompt=fallacy_revision_prompt)
+ return cls(
+ chain=chain,
+ fallacy_critique_chain=fallacy_critique_chain,
+ fallacy_revision_chain=fallacy_revision_chain,
+ **kwargs,
+ )
+
+ @property
+ def input_keys(self) -> List[str]:
+ """Input keys."""
+ return self.chain.input_keys
+
+ @property
+ def output_keys(self) -> List[str]:
+ """Output keys."""
+ if self.return_intermediate_steps:
+ return ["output", "fallacy_critiques_and_revisions", "initial_output"]
+ return ["output"]
+
+ def _call(
+ self,
+ inputs: Dict[str, Any],
+ run_manager: Optional[CallbackManagerForChainRun] = None,
+ ) -> Dict[str, Any]:
+ _run_manager = run_manager or CallbackManagerForChainRun.get_noop_manager()
+ response = self.chain.run(
+ **inputs,
+ callbacks=_run_manager.get_child("original"),
+ )
+ initial_response = response
+ input_prompt = self.chain.prompt.format(**inputs)
+
+ _run_manager.on_text(
+ text="Initial response: " + response + "\n\n",
+ verbose=self.verbose,
+ color="yellow",
+ )
+ fallacy_critiques_and_revisions = []
+ for logical_fallacy in self.logical_fallacies:
+ # Fallacy critique below
+
+ fallacy_raw_critique = self.fallacy_critique_chain.run(
+ input_prompt=input_prompt,
+ output_from_model=response,
+ fallacy_critique_request=logical_fallacy.fallacy_critique_request,
+ callbacks=_run_manager.get_child("fallacy_critique"),
+ )
+ fallacy_critique = self._parse_critique(
+ output_string=fallacy_raw_critique,
+ ).strip()
+
+ # if fallacy critique contains "No fallacy critique needed" then done
+ if "no fallacy critique needed" in fallacy_critique.lower():
+ fallacy_critiques_and_revisions.append((fallacy_critique, ""))
+ continue
+
+ fallacy_revision = self.fallacy_revision_chain.run(
+ input_prompt=input_prompt,
+ output_from_model=response,
+ fallacy_critique_request=logical_fallacy.fallacy_critique_request,
+ fallacy_critique=fallacy_critique,
+ revision_request=logical_fallacy.fallacy_revision_request,
+ callbacks=_run_manager.get_child("fallacy_revision"),
+ ).strip()
+ response = fallacy_revision
+ fallacy_critiques_and_revisions.append((fallacy_critique, fallacy_revision))
+
+ _run_manager.on_text(
+ text=f"Applying {logical_fallacy.name}..." + "\n\n",
+ verbose=self.verbose,
+ color="green",
+ )
+
+ _run_manager.on_text(
+ text="Logical Fallacy: " + fallacy_critique + "\n\n",
+ verbose=self.verbose,
+ color="blue",
+ )
+
+ _run_manager.on_text(
+ text="Updated response: " + fallacy_revision + "\n\n",
+ verbose=self.verbose,
+ color="yellow",
+ )
+
+ final_output: Dict[str, Any] = {"output": response}
+ if self.return_intermediate_steps:
+ final_output["initial_output"] = initial_response
+ final_output[
+ "fallacy_critiques_and_revisions"
+ ] = fallacy_critiques_and_revisions
+ return final_output
+
+ @staticmethod
+ def _parse_critique(output_string: str) -> str:
+ if "Fallacy Revision request:" not in output_string:
+ return output_string
+ output_string = output_string.split("Fallacy Revision request:")[0]
+ if "\n\n" in output_string:
+ output_string = output_string.split("\n\n")[0]
+ return output_string
diff --git a/libs/experimental/langchain_experimental/fallacy_removal/fallacies.py b/libs/experimental/langchain_experimental/fallacy_removal/fallacies.py
new file mode 100644
index 0000000000000..efe4838fda762
--- /dev/null
+++ b/libs/experimental/langchain_experimental/fallacy_removal/fallacies.py
@@ -0,0 +1,209 @@
+"""Classification of logical fallacies in Natural Language Arguments \
+from https://arxiv.org/pdf/2212.07425.pdf
+"""
+
+from typing import Dict
+
+from langchain_experimental.fallacy_removal.models import LogicalFallacy
+
+FALLACIES: Dict[str, LogicalFallacy] = {
+ "adhominem": LogicalFallacy(
+ name="adhominem",
+ fallacy_critique_request="Identify any feasible ways in which \
+ the assistant’s last response is attacking the character or \
+ personal traits of the person making an argument rather than \
+ addressing the actual argument and evidence.",
+ fallacy_revision_request="Please rewrite the assistant response\
+ to remove any attacking the character or personal traits of the\
+ person making an argument rather than addressing the actual\
+ argument and evidence.",
+ ),
+ "adpopulum": LogicalFallacy(
+ name="adpopulum",
+ fallacy_critique_request="Identify ways in which the assistant’s\
+ last response may be asserting that something must be true or \
+ correct simply because many people believe it or do it, without \
+ actual facts or evidence to support the conclusion.",
+ fallacy_revision_request="Please rewrite the assistant response \
+ to remove any assertion that something must be true or correct \
+ simply because many people believe it or do it, without actual \
+ facts or evidence to support the conclusion.",
+ ),
+ "appealtoemotion": LogicalFallacy(
+ name="appealtoemotion",
+ fallacy_critique_request="Identify all ways in which the \
+ assistant’s last response is an attempt to win support for an \
+ argument by exploiting or manipulating people's emotions rather \
+ than using facts and reason.",
+ fallacy_revision_request="Please rewrite the assistant response \
+ to remove any attempt to win support for an argument by \
+ exploiting or manipulating people's emotions rather than using \
+ facts and reason.",
+ ),
+ "fallacyofextension": LogicalFallacy(
+ name="fallacyofextension",
+ fallacy_critique_request="Identify any ways in which the \
+ assitant's last response is making broad, sweeping generalizations\
+ and extending the implications of an argument far beyond what the \
+ initial premises support.",
+ fallacy_revision_request="Rewrite the assistant response to remove\
+ all broad, sweeping generalizations and extending the implications\
+ of an argument far beyond what the initial premises support.",
+ ),
+ "intentionalfallacy": LogicalFallacy(
+ name="intentionalfallacy",
+ fallacy_critique_request="Identify any way in which the assistant’s\
+ last response may be falsely supporting a conclusion by claiming to\
+ understand an author or creator's subconscious intentions without \
+ clear evidence.",
+ fallacy_revision_request="Revise the assistant’s last response to \
+ remove any false support of a conclusion by claiming to understand\
+ an author or creator's subconscious intentions without clear \
+ evidence.",
+ ),
+ "falsecausality": LogicalFallacy(
+ name="falsecausality",
+ fallacy_critique_request="Think carefully about whether the \
+ assistant's last response is jumping to conclusions about causation\
+ between events or circumstances without adequate evidence to infer \
+ a causal relationship.",
+ fallacy_revision_request="Please write a new version of the \
+ assistant’s response that removes jumping to conclusions about\
+ causation between events or circumstances without adequate \
+ evidence to infer a causal relationship.",
+ ),
+ "falsedilemma": LogicalFallacy(
+ name="falsedilemma",
+ fallacy_critique_request="Identify any way in which the \
+ assistant's last response may be presenting only two possible options\
+ or sides to a situation when there are clearly other alternatives \
+ that have not been considered or addressed.",
+ fallacy_revision_request="Amend the assistant’s last response to \
+ remove any presentation of only two possible options or sides to a \
+ situation when there are clearly other alternatives that have not \
+ been considered or addressed.",
+ ),
+ "hastygeneralization": LogicalFallacy(
+ name="hastygeneralization",
+ fallacy_critique_request="Identify any way in which the assistant’s\
+ last response is making a broad inference or generalization to \
+ situations, people, or circumstances that are not sufficiently \
+ similar based on a specific example or limited evidence.",
+ fallacy_revision_request="Please rewrite the assistant response to\
+ remove a broad inference or generalization to situations, people, \
+ or circumstances that are not sufficiently similar based on a \
+ specific example or limited evidence.",
+ ),
+ "illogicalarrangement": LogicalFallacy(
+ name="illogicalarrangement",
+ fallacy_critique_request="Think carefully about any ways in which \
+ the assistant's last response is constructing an argument in a \
+ flawed, illogical way, so the premises do not connect to or lead\
+ to the conclusion properly.",
+ fallacy_revision_request="Please rewrite the assistant’s response\
+ so as to remove any construction of an argument that is flawed and\
+ illogical or if the premises do not connect to or lead to the \
+ conclusion properly.",
+ ),
+ "fallacyofcredibility": LogicalFallacy(
+ name="fallacyofcredibility",
+ fallacy_critique_request="Discuss whether the assistant's last \
+ response was dismissing or attacking the credibility of the person\
+ making an argument rather than directly addressing the argument \
+ itself.",
+ fallacy_revision_request="Revise the assistant’s response so as \
+ that it refrains from dismissing or attacking the credibility of\
+ the person making an argument rather than directly addressing \
+ the argument itself.",
+ ),
+ "circularreasoning": LogicalFallacy(
+ name="circularreasoning",
+ fallacy_critique_request="Discuss ways in which the assistant’s\
+ last response may be supporting a premise by simply repeating \
+ the premise as the conclusion without giving actual proof or \
+ evidence.",
+ fallacy_revision_request="Revise the assistant’s response if \
+ possible so that it’s not supporting a premise by simply \
+ repeating the premise as the conclusion without giving actual\
+ proof or evidence.",
+ ),
+ "beggingthequestion": LogicalFallacy(
+ name="beggingthequestion",
+ fallacy_critique_request="Discuss ways in which the assistant's\
+ last response is restating the conclusion of an argument as a \
+ premise without providing actual support for the conclusion in \
+ the first place.",
+ fallacy_revision_request="Write a revision of the assistant’s \
+ response that refrains from restating the conclusion of an \
+ argument as a premise without providing actual support for the \
+ conclusion in the first place.",
+ ),
+ "trickquestion": LogicalFallacy(
+ name="trickquestion",
+ fallacy_critique_request="Identify ways in which the \
+ assistant’s last response is asking a question that \
+ contains or assumes information that has not been proven or \
+ substantiated.",
+ fallacy_revision_request="Please write a new assistant \
+ response so that it does not ask a question that contains \
+ or assumes information that has not been proven or \
+ substantiated.",
+ ),
+ "overapplier": LogicalFallacy(
+ name="overapplier",
+ fallacy_critique_request="Identify ways in which the assistant’s\
+ last response is applying a general rule or generalization to a \
+ specific case it was not meant to apply to.",
+ fallacy_revision_request="Please write a new response that does\
+ not apply a general rule or generalization to a specific case \
+ it was not meant to apply to.",
+ ),
+ "equivocation": LogicalFallacy(
+ name="equivocation",
+ fallacy_critique_request="Read the assistant’s last response \
+ carefully and identify if it is using the same word or phrase \
+ in two different senses or contexts within an argument.",
+ fallacy_revision_request="Rewrite the assistant response so \
+ that it does not use the same word or phrase in two different \
+ senses or contexts within an argument.",
+ ),
+ "amphiboly": LogicalFallacy(
+ name="amphiboly",
+ fallacy_critique_request="Critique the assistant’s last response\
+ to see if it is constructing sentences such that the grammar \
+ or structure is ambiguous, leading to multiple interpretations.",
+ fallacy_revision_request="Please rewrite the assistant response\
+ to remove any construction of sentences where the grammar or \
+ structure is ambiguous or leading to multiple interpretations.",
+ ),
+ "accent": LogicalFallacy(
+ name="accent",
+ fallacy_critique_request="Discuss whether the assitant's response\
+ is misrepresenting an argument by shifting the emphasis of a word\
+ or phrase to give it a different meaning than intended.",
+ fallacy_revision_request="Please rewrite the AI model's response\
+ so that it is not misrepresenting an argument by shifting the \
+ emphasis of a word or phrase to give it a different meaning than\
+ intended.",
+ ),
+ "composition": LogicalFallacy(
+ name="composition",
+ fallacy_critique_request="Discuss whether the assistant's \
+ response is erroneously inferring that something is true of \
+ the whole based on the fact that it is true of some part or \
+ parts.",
+ fallacy_revision_request="Please rewrite the assitant's response\
+ so that it is not erroneously inferring that something is true \
+ of the whole based on the fact that it is true of some part or \
+ parts.",
+ ),
+ "division": LogicalFallacy(
+ name="division",
+ fallacy_critique_request="Discuss whether the assistant's last \
+ response is erroneously inferring that something is true of the \
+ parts based on the fact that it is true of the whole.",
+ fallacy_revision_request="Please rewrite the assitant's response\
+ so that it is not erroneously inferring that something is true \
+ of the parts based on the fact that it is true of the whole.",
+ ),
+}
diff --git a/libs/experimental/langchain_experimental/fallacy_removal/models.py b/libs/experimental/langchain_experimental/fallacy_removal/models.py
new file mode 100644
index 0000000000000..78422b91b7995
--- /dev/null
+++ b/libs/experimental/langchain_experimental/fallacy_removal/models.py
@@ -0,0 +1,10 @@
+"""Models for the Logical Fallacy Chain"""
+from langchain_experimental.pydantic_v1 import BaseModel
+
+
+class LogicalFallacy(BaseModel):
+ """Logical fallacy."""
+
+ fallacy_critique_request: str
+ fallacy_revision_request: str
+ name: str = "Logical Fallacy"
diff --git a/libs/experimental/langchain_experimental/fallacy_removal/prompts.py b/libs/experimental/langchain_experimental/fallacy_removal/prompts.py
new file mode 100644
index 0000000000000..5c44da3bb1f6d
--- /dev/null
+++ b/libs/experimental/langchain_experimental/fallacy_removal/prompts.py
@@ -0,0 +1,135 @@
+from langchain_core.prompts.few_shot import FewShotPromptTemplate
+from langchain_core.prompts.prompt import PromptTemplate
+
+fallacy_critique_example = PromptTemplate(
+ template="""Human: {input_prompt}
+
+Model: {output_from_model}
+
+Fallacy Critique Request: {fallacy_critique_request}
+
+Fallacy Critique: {fallacy_critique}""",
+ input_variables=[
+ "input_prompt",
+ "output_from_model",
+ "fallacy_critique_request",
+ "fallacy_critique",
+ ],
+)
+
+examples = [
+ {
+ "input_prompt": "If everyone says the Earth is round, \
+ how do I know that's correct?",
+ "output_from_model": "The earth is round because your \
+ teacher says it is",
+ "fallacy_critique_request": "Identify specific ways in\
+ which the model’s previous response had a logical fallacy. \
+ Also point out potential logical fallacies in the human’s \
+ questions and responses. Examples of logical fallacies \
+ include but are not limited to ad hominem, ad populum, \
+ appeal to emotion and false causality.",
+ "fallacy_critique": "This statement contains the logical \
+ fallacy of Ad Verecundiam or Appeal to Authority. It is \
+ a fallacy because it asserts something to be true purely \
+ based on the authority of the source making the claim, \
+ without any actual evidence to support it. Fallacy \
+ Critique Needed",
+ "fallacy_revision_request": "Please rewrite the model \
+ response to remove all logical fallacies, and to \
+ politely point out any logical fallacies from the \
+ human.",
+ "fallacy_revision": "The earth is round based on \
+ evidence from observations of its curvature from high \
+ altitudes, photos from space showing its spherical shape, \
+ circumnavigation, and the fact that we see its rounded \
+ shadow on the moon during lunar eclipses.",
+ },
+ {
+ "input_prompt": "Should we invest more in our school \
+ music program? After all, studies show students \
+ involved in music perform better academically.",
+ "output_from_model": "I don't think we should invest \
+ more in the music program. Playing the piccolo won't \
+ teach someone better math skills.",
+ "fallacy_critique_request": "Identify specific ways \
+ in which the model’s previous response had a logical \
+ fallacy. Also point out potential logical fallacies \
+ in the human’s questions and responses. Examples of \
+ logical fallacies include but are not limited to ad \
+ homimem, ad populum, appeal to emotion and false \
+ causality.",
+ "fallacy_critique": "This answer commits the division \
+ fallacy by rejecting the argument based on assuming \
+ capabilities true of the parts (playing an instrument \
+ like piccolo) also apply to the whole \
+ (the full music program). The answer focuses only on \
+ part of the music program rather than considering it \
+ as a whole. Fallacy Critique Needed.",
+ "fallacy_revision_request": "Please rewrite the model \
+ response to remove all logical fallacies, and to \
+ politely point out any logical fallacies from the human.",
+ "fallacy_revision": "While playing an instrument may \
+ teach discipline, more evidence is needed on whether \
+ music education courses improve critical thinking \
+ skills across subjects before determining if increased \
+ investment in the whole music program is warranted.",
+ },
+]
+
+FALLACY_CRITIQUE_PROMPT = FewShotPromptTemplate(
+ example_prompt=fallacy_critique_example,
+ examples=[
+ {k: v for k, v in e.items() if k != "fallacy_revision_request"}
+ for e in examples
+ ],
+ prefix="Below is a conversation between a human and an \
+ AI assistant. If there is no material critique of the \
+ model output, append to the end of the Fallacy Critique: \
+ 'No fallacy critique needed.' If there is material \
+ critique \
+ of the model output, append to the end of the Fallacy \
+ Critique: 'Fallacy Critique needed.'",
+ suffix="""Human: {input_prompt}
+Model: {output_from_model}
+
+Fallacy Critique Request: {fallacy_critique_request}
+
+Fallacy Critique:""",
+ example_separator="\n === \n",
+ input_variables=["input_prompt", "output_from_model", "fallacy_critique_request"],
+)
+
+FALLACY_REVISION_PROMPT = FewShotPromptTemplate(
+ example_prompt=fallacy_critique_example,
+ examples=examples,
+ prefix="Below is a conversation between a human and \
+ an AI assistant.",
+ suffix="""Human: {input_prompt}
+
+Model: {output_from_model}
+
+Fallacy Critique Request: {fallacy_critique_request}
+
+Fallacy Critique: {fallacy_critique}
+
+If the fallacy critique does not identify anything worth \
+changing, ignore the Fallacy Revision Request and do not \
+make any revisions. Instead, return "No revisions needed".
+
+If the fallacy critique does identify something worth \
+changing, please revise the model response based on the \
+Fallacy Revision Request.
+
+Fallacy Revision Request: {fallacy_revision_request}
+
+Fallacy Revision:""",
+ example_separator="\n === \n",
+ input_variables=[
+ "input_prompt",
+ "output_from_model",
+ "fallacy_critique_request",
+ "fallacy_critique",
+ "fallacy_revision_request",
+ ],
+)
diff --git a/libs/experimental/pyproject.toml b/libs/experimental/pyproject.toml
index f348d80edb871..e509fc40e1250 100644
--- a/libs/experimental/pyproject.toml
+++ b/libs/experimental/pyproject.toml
@@ -1,27 +1,17 @@
[tool.poetry]
name = "gigachain-experimental"
-version = "0.0.59"
+version = "0.0.62"
description = "Building applications with LLMs through composability"
authors = []
license = "MIT"
readme = "README.md"
-repository = "https://github.com/ai-forever/gigachain"
-packages = [
- {include = "langchain_experimental"}
-]
+repository = "https://github.com/langchain-ai/langchain"
+
[tool.poetry.dependencies]
python = ">=3.8.1,<4.0"
-gigachain-core = "^0.2"
-gigachain-community = "^0.2"
-presidio-anonymizer = {version = "^2.2.352", optional = true}
-presidio-analyzer = {version = "^2.2.352", optional = true}
-faker = {version = "^19.3.1", optional = true}
-vowpal-wabbit-next = {version = "0.6.0", optional = true}
-sentence-transformers = {version = "^2", optional = true}
-jinja2 = {version = "^3", optional = true}
-pandas = { version = "^2.0.1", optional = true }
-tabulate = {version = "^0.9.0", optional = true}
+gigachain-core = "^0.2.10"
+gigachain-community = "^0.2.6"
[tool.poetry.group.lint]
optional = true
@@ -36,9 +26,9 @@ optional = true
mypy = "^0.991"
types-pyyaml = "^6.0.12.2"
types-requests = "^2.28.11.5"
-gigachain = {path = "../langchain", develop = true}
-gigachain-core = {path = "../core", develop = true}
-gigachain-community = {path = "../community", develop = true}
+gigachain = { path = "../langchain", develop = true }
+gigachain-core = { path = "../core", develop = true }
+gigachain-community = { path = "../community", develop = true }
[tool.poetry.group.dev]
optional = true
@@ -46,9 +36,9 @@ optional = true
[tool.poetry.group.dev.dependencies]
jupyter = "^1.0.0"
setuptools = "^67.6.1"
-gigachain = {path = "../langchain", develop = true}
-gigachain-core = {path = "../core", develop = true}
-gigachain-community = {path = "../community", develop = true}
+gigachain = { path = "../langchain", develop = true }
+gigachain-core = { path = "../core", develop = true }
+gigachain-community = { path = "../community", develop = true }
[tool.poetry.group.test]
optional = true
@@ -59,42 +49,33 @@ optional = true
# Any dependencies that do not meet that criteria will be removed.
pytest = "^7.3.0"
pytest-asyncio = "^0.20.3"
-gigachain = {path = "../langchain", develop = true}
-gigachain-core = {path = "../core", develop = true}
-gigachain-community = {path = "../community", develop = true}
-gigachain-text-splitters = {path = "../text-splitters", develop = true}
-gigachat = "^0.1.29"
+gigachain = { path = "../langchain", develop = true }
+gigachain-core = { path = "../core", develop = true }
+gigachain-community = { path = "../community", develop = true }
+gigachain-text-splitters = { path = "../text-splitters", develop = true }
+
+# Support Python 3.8 and 3.12+.
+# Can be removed once the numpy version is fixed in gigachain-community.
+numpy = [
+ { version = "^1.24.0", python = "<3.12" },
+ { version = "^1.26.0", python = ">=3.12" },
+]
[tool.poetry.group.test_integration]
optional = true
[tool.poetry.group.test_integration.dependencies]
-gigachain = {path = "../langchain", develop = true}
-gigachain-core = {path = "../core", develop = true}
-gigachain-community = {path = "../community", develop = true}
-gigachain-openai = {path = "../partners/openai", develop = true}
-
-# An extra used to be able to add extended testing.
-# Please use new-line on formatting to make it easier to add new packages without
-# merge-conflicts
-[tool.poetry.extras]
-extended_testing = [
- "presidio-anonymizer",
- "presidio-analyzer",
- "faker",
- "vowpal-wabbit-next",
- "sentence-transformers",
- "jinja2",
- "pandas",
- "tabulate",
-]
+gigachain = { path = "../langchain", develop = true }
+gigachain-core = { path = "../core", develop = true }
+gigachain-community = { path = "../community", develop = true }
+gigachain-openai = { path = "../partners/openai", develop = true }
[tool.ruff.lint]
select = [
- "E", # pycodestyle
- "F", # pyflakes
- "I", # isort
+ "E", # pycodestyle
+ "F", # pyflakes
+ "I", # isort
"T201", # print
]
@@ -104,9 +85,7 @@ disallow_untyped_defs = "True"
exclude = ["notebooks", "examples", "example_data"]
[tool.coverage.run]
-omit = [
- "tests/*",
-]
+omit = ["tests/*"]
[build-system]
requires = ["poetry-core>=1.0.0"]
diff --git a/libs/experimental/tests/unit_tests/test_logical_fallacy.py b/libs/experimental/tests/unit_tests/test_logical_fallacy.py
new file mode 100644
index 0000000000000..455c76a463cb1
--- /dev/null
+++ b/libs/experimental/tests/unit_tests/test_logical_fallacy.py
@@ -0,0 +1,26 @@
+"""Unit tests for the Logical Fallacy chain, same format as CAI"""
+from langchain_experimental.fallacy_removal.base import FallacyChain
+
+TEXT_ONE = """ This text is bad.\
+
+Fallacy Revision request: Make it great.\
+
+Fallacy Revision:"""
+
+TEXT_TWO = """ This text is bad.\n\n"""
+
+TEXT_THREE = """ This text is bad.\
+
+Fallacy Revision request: Make it great again.\
+
+Fallacy Revision: Better text"""
+
+
+def test_fallacy_critique_parsing() -> None:
+ """Test parsing of critique text."""
+ for text in [TEXT_ONE, TEXT_TWO, TEXT_THREE]:
+ fallacy_critique = FallacyChain._parse_critique(text)
+
+ assert (
+ fallacy_critique.strip() == "This text is bad."
+ ), f"Failed on {text} with {fallacy_critique}"
diff --git a/libs/langchain/extended_testing_deps.txt b/libs/langchain/extended_testing_deps.txt
new file mode 100644
index 0000000000000..855344dfa6253
--- /dev/null
+++ b/libs/langchain/extended_testing_deps.txt
@@ -0,0 +1,10 @@
+-e ../partners/openai
+-e ../partners/anthropic
+-e ../partners/fireworks
+-e ../partners/together
+-e ../partners/mistralai
+-e ../partners/groq
+jsonschema>=4.22.0,<5
+numexpr>=2.8.6,<3
+rapidfuzz>=3.1.1,<4
+aiosqlite>=0.19.0,<0.20
diff --git a/libs/langchain/langchain/memory/vectorstore_token_buffer_memory.py b/libs/langchain/langchain/memory/vectorstore_token_buffer_memory.py
new file mode 100644
index 0000000000000..0995bb3e34a67
--- /dev/null
+++ b/libs/langchain/langchain/memory/vectorstore_token_buffer_memory.py
@@ -0,0 +1,184 @@
+"""
+Class for a conversation memory buffer with older messages stored in a vectorstore .
+
+This implementats a conversation memory in which the messages are stored in a memory
+buffer up to a specified token limit. When the limit is exceeded, older messages are
+saved to a vectorstore backing database. The vectorstore can be made persistent across
+sessions.
+"""
+
+import warnings
+from datetime import datetime
+from typing import Any, Dict, List
+
+from langchain_core.messages import BaseMessage
+from langchain_core.prompts.chat import SystemMessagePromptTemplate
+from langchain_core.pydantic_v1 import Field, PrivateAttr
+from langchain_core.vectorstores import VectorStoreRetriever
+
+from langchain.memory import ConversationTokenBufferMemory, VectorStoreRetrieverMemory
+from langchain.memory.chat_memory import BaseChatMemory
+from langchain.text_splitter import RecursiveCharacterTextSplitter
+
+DEFAULT_HISTORY_TEMPLATE = """
+Current date and time: {current_time}.
+
+Potentially relevant timestamped excerpts of previous conversations (you
+do not need to use these if irrelevant):
+{previous_history}
+
+"""
+
+TIMESTAMP_FORMAT = "%Y-%m-%d %H:%M:%S %Z"
+
+
+class ConversationVectorStoreTokenBufferMemory(ConversationTokenBufferMemory):
+ """Conversation chat memory with token limit and vectordb backing.
+
+ load_memory_variables() will return a dict with the key "history".
+ It contains background information retrieved from the vector store
+ plus recent lines of the current conversation.
+
+ To help the LLM understand the part of the conversation stored in the
+ vectorstore, each interaction is timestamped and the current date and
+ time is also provided in the history. A side effect of this is that the
+ LLM will have access to the current date and time.
+
+ Initialization arguments:
+
+ This class accepts all the initialization arguments of
+ ConversationTokenBufferMemory, such as `llm`. In addition, it
+ accepts the following additional arguments
+
+ retriever: (required) A VectorStoreRetriever object to use
+ as the vector backing store
+
+ split_chunk_size: (optional, 1000) Token chunk split size
+ for long messages generated by the AI
+
+ previous_history_template: (optional) Template used to format
+ the contents of the prompt history
+
+
+ Example using ChromaDB:
+
+ .. code-block:: python
+
+ from langchain.memory.token_buffer_vectorstore_memory import (
+ ConversationVectorStoreTokenBufferMemory
+ )
+ from langchain_community.vectorstores import Chroma
+ from langchain_community.embeddings import HuggingFaceInstructEmbeddings
+ from langchain_openai import OpenAI
+
+ embedder = HuggingFaceInstructEmbeddings(
+ query_instruction="Represent the query for retrieval: "
+ )
+ chroma = Chroma(collection_name="demo",
+ embedding_function=embedder,
+ collection_metadata={"hnsw:space": "cosine"},
+ )
+
+ retriever = chroma.as_retriever(
+ search_type="similarity_score_threshold",
+ search_kwargs={
+ 'k': 5,
+ 'score_threshold': 0.75,
+ },
+ )
+
+ conversation_memory = ConversationVectorStoreTokenBufferMemory(
+ return_messages=True,
+ llm=OpenAI(),
+ retriever=retriever,
+ max_token_limit = 1000,
+ )
+
+ conversation_memory.save_context({"Human": "Hi there"},
+ {"AI": "Nice to meet you!"}
+ )
+ conversation_memory.save_context({"Human": "Nice day isn't it?"},
+ {"AI": "I love Wednesdays."}
+ )
+ conversation_memory.load_memory_variables({"input": "What time is it?"})
+
+ """
+
+ retriever: VectorStoreRetriever = Field(exclude=True)
+ memory_key: str = "history"
+ previous_history_template: str = DEFAULT_HISTORY_TEMPLATE
+ split_chunk_size: int = 1000
+
+ _memory_retriever: VectorStoreRetrieverMemory = PrivateAttr(default=None)
+ _timestamps: List[datetime] = PrivateAttr(default_factory=list)
+
+ @property
+ def memory_retriever(self) -> VectorStoreRetrieverMemory:
+ """Return a memory retriever from the passed retriever object."""
+ if self._memory_retriever is not None:
+ return self._memory_retriever
+ self._memory_retriever = VectorStoreRetrieverMemory(retriever=self.retriever)
+ return self._memory_retriever
+
+ def load_memory_variables(self, inputs: Dict[str, Any]) -> Dict[str, Any]:
+ """Return history and memory buffer."""
+ try:
+ with warnings.catch_warnings():
+ warnings.simplefilter("ignore")
+ memory_variables = self.memory_retriever.load_memory_variables(inputs)
+ previous_history = memory_variables[self.memory_retriever.memory_key]
+ except AssertionError: # happens when db is empty
+ previous_history = ""
+ current_history = super().load_memory_variables(inputs)
+ template = SystemMessagePromptTemplate.from_template(
+ self.previous_history_template
+ )
+ messages = [
+ template.format(
+ previous_history=previous_history,
+ current_time=datetime.now().astimezone().strftime(TIMESTAMP_FORMAT),
+ )
+ ]
+ messages.extend(current_history[self.memory_key])
+ return {self.memory_key: messages}
+
+ def save_context(self, inputs: Dict[str, Any], outputs: Dict[str, str]) -> None:
+ """Save context from this conversation to buffer. Pruned."""
+ BaseChatMemory.save_context(self, inputs, outputs)
+ self._timestamps.append(datetime.now().astimezone())
+ # Prune buffer if it exceeds max token limit
+ buffer = self.chat_memory.messages
+ curr_buffer_length = self.llm.get_num_tokens_from_messages(buffer)
+ if curr_buffer_length > self.max_token_limit:
+ while curr_buffer_length > self.max_token_limit:
+ self._pop_and_store_interaction(buffer)
+ curr_buffer_length = self.llm.get_num_tokens_from_messages(buffer)
+
+ def save_remainder(self) -> None:
+ """
+ Save the remainder of the conversation buffer to the vector store.
+
+ This is useful if you have made the vectorstore persistent, in which
+ case this can be called before the end of the session to store the
+ remainder of the conversation.
+ """
+ buffer = self.chat_memory.messages
+ while len(buffer) > 0:
+ self._pop_and_store_interaction(buffer)
+
+ def _pop_and_store_interaction(self, buffer: List[BaseMessage]) -> None:
+ input = buffer.pop(0)
+ output = buffer.pop(0)
+ timestamp = self._timestamps.pop(0).strftime(TIMESTAMP_FORMAT)
+ # Split AI output into smaller chunks to avoid creating documents
+ # that will overflow the context window
+ ai_chunks = self._split_long_ai_text(str(output.content))
+ for index, chunk in enumerate(ai_chunks):
+ self.memory_retriever.save_context(
+ {"Human": f"<{timestamp}/00> {str(input.content)}"},
+ {"AI": f"<{timestamp}/{index:02}> {chunk}"},
+ )
+
+ def _split_long_ai_text(self, text: str) -> List[str]:
+ splitter = RecursiveCharacterTextSplitter(chunk_size=self.split_chunk_size)
+ return [chunk.page_content for chunk in splitter.create_documents([text])]
diff --git a/libs/langchain/pyproject.toml b/libs/langchain/pyproject.toml
index 0af634cf8d13d..e59ed4786e54a 100644
--- a/libs/langchain/pyproject.toml
+++ b/libs/langchain/pyproject.toml
@@ -1,119 +1,33 @@
[tool.poetry]
name = "gigachain"
-version = "0.2.0"
+version = "0.2.6"
description = "Building applications with LLMs through composability"
authors = []
license = "MIT"
readme = "README.md"
-repository = "https://github.com/ai-forever/gigachain"
-packages = [
- {include = "langchain"}
-]
+repository = "https://github.com/langchain-ai/langchain"
[tool.poetry.scripts]
langchain-server = "langchain.server:main"
[tool.poetry.dependencies]
python = ">=3.8.1,<4.0"
-gigachain-core = "^0.2.0"
+gigachain-core = "^0.2.10"
gigachain-text-splitters = "^0.2.0"
langsmith = "^0.1.17"
pydantic = ">=1,<3"
SQLAlchemy = ">=1.4,<3"
requests = "^2"
PyYAML = ">=5.3"
-numpy = "^1"
aiohttp = "^3.8.3"
-tenacity = "^8.1.0"
-gigachat = "^0.1.29"
-azure-core = {version = "^1.26.4", optional=true}
-tqdm = {version = ">=4.48.0", optional = true}
-openapi-pydantic = {version = "^0.3.2", optional = true}
-faiss-cpu = {version = "^1", optional = true}
-manifest-ml = {version = "^0.0.1", optional = true}
-transformers = {version = "^4", optional = true}
-beautifulsoup4 = {version = "^4", optional = true}
-torch = {version = ">=1,<3", optional = true}
-jinja2 = {version = "^3", optional = true}
-tiktoken = {version = ">=0.7,<1.0", optional = true, python=">=3.9"}
-qdrant-client = {version = "^1.3.1", optional = true, python = ">=3.8.1,<3.12"}
-dataclasses-json = ">= 0.5.7, < 0.7"
-cohere = {version = ">=4,<6", optional = true}
-openai = {version = "<2", optional = true}
-nlpcloud = {version = "^1", optional = true}
-huggingface_hub = {version = "^0", optional = true}
-sentence-transformers = {version = "^2", optional = true}
-arxiv = {version = "^1.4", optional = true}
-pypdf = {version = "^3.4.0", optional = true}
-aleph-alpha-client = {version="^2.15.0", optional = true}
-pgvector = {version = "^0.1.6", optional = true}
-async-timeout = {version = "^4.0.0", python = "<3.11"}
-azure-identity = {version = "^1.12.0", optional=true}
-atlassian-python-api = {version = "^3.36.0", optional=true}
-html2text = {version="^2020.1.16", optional=true}
-numexpr = {version="^2.8.6", optional=true}
-azure-cosmos = {version="^4.4.0b1", optional=true}
-jq = {version = "^1.4.1", optional = true}
-pdfminer-six = {version = "^20221105", optional = true}
-docarray = {version="^0.32.0", extras=["hnswlib"], optional=true}
-lxml = {version = ">=4.9.3,<6.0", optional = true}
-pymupdf = {version = "^1.22.3", optional = true}
-rapidocr-onnxruntime = {version = "^1.3.2", optional = true, python = ">=3.8.1,<3.12"}
-pypdfium2 = {version = "^4.10.0", optional = true}
-gql = {version = "^3.4.1", optional = true}
-pandas = {version = "^2.0.1", optional = true}
-telethon = {version = "^1.28.5", optional = true}
-chardet = {version="^5.1.0", optional=true}
-requests-toolbelt = {version = "^1.0.0", optional = true}
-openlm = {version = "^0.0.5", optional = true}
-scikit-learn = {version = "^1.2.2", optional = true}
-azure-ai-formrecognizer = {version = "^3.2.1", optional = true}
-azure-cognitiveservices-speech = {version = "^1.28.0", optional = true}
-py-trello = {version = "^0.19.0", optional = true}
-bibtexparser = {version = "^1.4.0", optional = true}
-pyspark = {version = "^3.4.0", optional = true}
-clarifai = {version = ">=9.1.0", optional = true}
-mwparserfromhell = {version = "^0.6.4", optional = true}
-mwxml = {version = "^0.3.3", optional = true}
-azure-search-documents = {version = "11.4.0b8", optional = true}
-esprima = {version = "^4.0.1", optional = true}
-streamlit = {version = "^1.18.0", optional = true, python = ">=3.8.1,<3.9.7 || >3.9.7,<4.0"}
-psychicapi = {version = "^0.8.0", optional = true}
-cassio = {version = "^0.1.0", optional = true}
-sympy = {version = "^1.12", optional = true}
-rapidfuzz = {version = "^3.1.1", optional = true}
-jsonschema = {version = ">1", optional = true}
-rank-bm25 = {version = "^0.2.2", optional = true}
-geopandas = {version = "^0.13.1", optional = true}
-gitpython = {version = "^3.1.32", optional = true}
-feedparser = {version = "^6.0.10", optional = true}
-newspaper3k = {version = "^0.2.8", optional = true}
-xata = {version = "^1.0.0a7", optional = true}
-xmltodict = {version = "^0.13.0", optional = true}
-markdownify = {version = "^0.11.6", optional = true}
-assemblyai = {version = "^0.17.0", optional = true}
-dashvector = {version = "^1.0.1", optional = true}
-sqlite-vss = {version = "^0.1.2", optional = true}
-motor = {version = "^3.3.1", optional = true}
-timescale-vector = {version = "^0.0.1", optional = true}
-typer = {version= "^0.9.0", optional = true}
-anthropic = {version = "^0.3.11", optional = true}
-aiosqlite = {version = "^0.19.0", optional = true}
-rspace_client = {version = "^2.5.0", optional = true}
-upstash-redis = {version = "^0.15.0", optional = true}
-azure-ai-textanalytics = {version = "^5.3.0", optional = true}
-google-cloud-documentai = {version = "^2.20.1", optional = true}
-fireworks-ai = {version = "^0.9.0", optional = true}
-javelin-sdk = {version = "^0.1.8", optional = true}
-hologres-vector = {version = "^0.0.6", optional = true}
-praw = {version = "^7.7.1", optional = true}
-msal = {version = "^1.25.0", optional = true}
-databricks-vectorsearch = {version = "^0.21", optional = true}
-couchbase = {version = "^4.1.9", optional = true}
-dgml-utils = {version = "^0.3.0", optional = true}
-datasets = {version = "^2.15.0", optional = true}
-gigachain-openai = {version = "^0.1", optional = true}
-rdflib = {version = "7.0.0", optional = true}
+tenacity = "^8.1.0,!=8.4.0"
+async-timeout = { version = "^4.0.0", python = "<3.11" }
+
+# Support Python 3.8 and 3.12+.
+numpy = [
+ { version = "^1", python = "<3.12" },
+ { version = "^1.26.0", python = ">=3.12" },
+]
[tool.poetry.group.test]
optional = true
@@ -132,13 +46,13 @@ responses = "^0.22.0"
pytest-asyncio = "^0.23.2"
lark = "^1.1.5"
pandas = "^2.0.0"
-pytest-mock = "^3.10.0"
+pytest-mock = "^3.10.0"
pytest-socket = "^0.6.0"
syrupy = "^4.0.2"
requests-mock = "^1.11.0"
-gigachain-core = {path = "../core", develop = true}
-gigachain-text-splitters = {path = "../text-splitters", develop = true}
-gigachain-openai = {path = "../partners/openai", optional = true, develop = true}
+gigachain-core = { path = "../core", develop = true }
+gigachain-text-splitters = { path = "../text-splitters", develop = true }
+gigachain-openai = { path = "../partners/openai", optional = true, develop = true }
[tool.poetry.group.codespell]
optional = true
@@ -150,31 +64,17 @@ codespell = "^2.2.0"
optional = true
[tool.poetry.group.test_integration.dependencies]
-# Do not add dependencies in the test_integration group
-# Instead:
-# 1. Add an optional dependency to the main group
-# poetry add --optional [package name]
-# 2. Add the package name to the extended_testing extra (find it below)
-# 3. Relock the poetry file
-# poetry lock --no-update
-# 4. Favor unit tests not integration tests.
-# Use the @pytest.mark.requires(pkg_name) decorator in unit_tests.
-# Your tests should not rely on network access, as it prevents other
-# developers from being able to easily run them.
-# Instead write unit tests that use the `responses` library or mock.patch with
-# fixtures. Keep the fixtures minimal.
-# See the Contributing Guide for more instructions on working with optional dependencies.
+
+
+# Instead read the following link:
# https://python.langchain.com/docs/contributing/code#working-with-optional-dependencies
pytest-vcr = "^1.0.2"
wrapt = "^1.15.0"
-openai = "^1"
python-dotenv = "^1.0.0"
cassio = "^0.1.0"
-tiktoken = ">=0.7,<1"
-anthropic = "^0.3.11"
-gigachain-core = {path = "../core", develop = true}
-gigachain-text-splitters = {path = "../text-splitters", develop = true}
-langchainhub = "^0.1.15"
+gigachain-core = { path = "../core", develop = true }
+gigachain-text-splitters = { path = "../text-splitters", develop = true }
+langchainhub = "^0.1.16"
[tool.poetry.group.lint]
optional = true
@@ -194,8 +94,8 @@ types-redis = "^4.3.21.6"
types-pytz = "^2023.3.0.0"
types-chardet = "^5.0.4.6"
mypy-protobuf = "^3.0.0"
-gigachain-core = {path = "../core", develop = true}
-gigachain-text-splitters = {path = "../text-splitters", develop = true}
+gigachain-core = { path = "../core", develop = true }
+gigachain-text-splitters = { path = "../text-splitters", develop = true }
[tool.poetry.group.dev]
optional = true
@@ -204,116 +104,16 @@ optional = true
jupyter = "^1.0.0"
playwright = "^1.28.0"
setuptools = "^67.6.1"
-gigachain-core = {path = "../core", develop = true}
-gigachain-text-splitters = {path = "../text-splitters", develop = true}
-
-[tool.poetry.extras]
-llms = ["clarifai", "cohere", "openai", "openlm", "nlpcloud", "huggingface_hub", "manifest-ml", "torch", "transformers"]
-qdrant = ["qdrant-client"]
-openai = ["openai", "tiktoken"]
-text_helpers = ["chardet"]
-clarifai = ["clarifai"]
-cohere = ["cohere"]
-docarray = ["docarray"]
-embeddings = ["sentence-transformers"]
-javascript = ["esprima"]
-azure = [
- "azure-identity",
- "azure-cosmos",
- "openai",
- "azure-core",
- "azure-ai-formrecognizer",
- "azure-cognitiveservices-speech",
- "azure-search-documents",
- "azure-ai-textanalytics",
-]
-all = []
-cli = ["typer"]
-
-# An extra used to be able to add extended testing.
-# Please use new-line on formatting to make it easier to add new packages without
-# merge-conflicts
-extended_testing = [
- "aleph-alpha-client",
- "aiosqlite",
- "assemblyai",
- "beautifulsoup4",
- "bibtexparser",
- "cassio",
- "chardet",
- "datasets",
- "google-cloud-documentai",
- "esprima",
- "jq",
- "pdfminer-six",
- "pgvector",
- "pypdf",
- "pymupdf",
- "pypdfium2",
- "tqdm",
- "lxml",
- "atlassian-python-api",
- "mwparserfromhell",
- "mwxml",
- "msal",
- "pandas",
- "telethon",
- "psychicapi",
- "gql",
- "requests-toolbelt",
- "html2text",
- "numexpr",
- "py-trello",
- "scikit-learn",
- "streamlit",
- "pyspark",
- "openai",
- "sympy",
- "rapidfuzz",
- "jsonschema",
- "openai",
- "rank-bm25",
- "geopandas",
- "jinja2",
- "gitpython",
- "newspaper3k",
- "feedparser",
- "xata",
- "xmltodict",
- "faiss-cpu",
- "openapi-pydantic",
- "markdownify",
- "arxiv",
- "dashvector",
- "sqlite-vss",
- "rapidocr-onnxruntime",
- "motor",
- "timescale-vector",
- "anthropic",
- "upstash-redis",
- "rspace_client",
- "fireworks-ai",
- "javelin-sdk",
- "hologres-vector",
- "praw",
- "databricks-vectorsearch",
- "couchbase",
- "dgml-utils",
- "cohere",
- "gigachain-openai",
- "rdflib",
-]
-
+gigachain-core = { path = "../core", develop = true }
+gigachain-text-splitters = { path = "../text-splitters", develop = true }
[tool.ruff]
-exclude = [
- "tests/integration_tests/examples/non-utf8-encoding.py",
-]
+exclude = ["tests/integration_tests/examples/non-utf8-encoding.py"]
[tool.ruff.lint]
select = [
- "E", # pycodestyle
- "F", # pyflakes
- "I", # isort
+ "E", # pycodestyle
+ "F", # pyflakes
+ "I", # isort
"T201", # print
]
@@ -323,9 +123,7 @@ disallow_untyped_defs = "True"
exclude = ["notebooks", "examples", "example_data"]
[tool.coverage.run]
-omit = [
- "tests/*",
-]
+omit = ["tests/*"]
[build-system]
requires = ["poetry-core>=1.0.0"]
@@ -347,7 +145,7 @@ addopts = "--strict-markers --strict-config --durations=5 --snapshot-warn-unused
markers = [
"requires: mark tests as requiring a specific library",
"scheduled: mark tests to run in scheduled testing",
- "compile: mark placeholder test used to compile integration tests without running them"
+ "compile: mark placeholder test used to compile integration tests without running them",
]
asyncio_mode = "auto"
diff --git a/libs/langchain/tests/unit_tests/output_parsers/test_fix.py b/libs/langchain/tests/unit_tests/output_parsers/test_fix.py
new file mode 100644
index 0000000000000..0f1eaf9413074
--- /dev/null
+++ b/libs/langchain/tests/unit_tests/output_parsers/test_fix.py
@@ -0,0 +1,121 @@
+from typing import Any
+
+import pytest
+from langchain_core.exceptions import OutputParserException
+from langchain_core.runnables import RunnablePassthrough
+
+from langchain.output_parsers.boolean import BooleanOutputParser
+from langchain.output_parsers.datetime import DatetimeOutputParser
+from langchain.output_parsers.fix import BaseOutputParser, OutputFixingParser
+
+
+class SuccessfulParseAfterRetries(BaseOutputParser[str]):
+ parse_count: int = 0 # Number of times parse has been called
+ attemp_count_before_success: int # Number of times to fail before succeeding # noqa
+
+ def parse(self, *args: Any, **kwargs: Any) -> str:
+ self.parse_count += 1
+ if self.parse_count <= self.attemp_count_before_success:
+ raise OutputParserException("error")
+ return "parsed"
+
+
+class SuccessfulParseAfterRetriesWithGetFormatInstructions(SuccessfulParseAfterRetries): # noqa
+ def get_format_instructions(self) -> str:
+ return "instructions"
+
+
+@pytest.mark.parametrize(
+ "base_parser",
+ [
+ SuccessfulParseAfterRetries(attemp_count_before_success=5),
+ SuccessfulParseAfterRetriesWithGetFormatInstructions(
+ attemp_count_before_success=5
+ ), # noqa: E501
+ ],
+)
+def test_output_fixing_parser_parse(
+ base_parser: SuccessfulParseAfterRetries,
+) -> None:
+ # preparation
+ n: int = (
+ base_parser.attemp_count_before_success
+ ) # Success on the (n+1)-th attempt # noqa
+ base_parser = SuccessfulParseAfterRetries(attemp_count_before_success=n)
+ parser = OutputFixingParser(
+ parser=base_parser,
+ max_retries=n, # n times to retry, that is, (n+1) times call
+ retry_chain=RunnablePassthrough(),
+ legacy=False,
+ )
+ # test
+ assert parser.parse("completion") == "parsed"
+ assert base_parser.parse_count == n + 1
+ # TODO: test whether "instructions" is passed to the retry_chain
+
+
+@pytest.mark.parametrize(
+ "base_parser",
+ [
+ SuccessfulParseAfterRetries(attemp_count_before_success=5),
+ SuccessfulParseAfterRetriesWithGetFormatInstructions(
+ attemp_count_before_success=5
+ ), # noqa: E501
+ ],
+)
+async def test_output_fixing_parser_aparse(
+ base_parser: SuccessfulParseAfterRetries,
+) -> None:
+ n: int = (
+ base_parser.attemp_count_before_success
+ ) # Success on the (n+1)-th attempt # noqa
+ base_parser = SuccessfulParseAfterRetries(attemp_count_before_success=n)
+ parser = OutputFixingParser(
+ parser=base_parser,
+ max_retries=n, # n times to retry, that is, (n+1) times call
+ retry_chain=RunnablePassthrough(),
+ legacy=False,
+ )
+ assert (await parser.aparse("completion")) == "parsed"
+ assert base_parser.parse_count == n + 1
+ # TODO: test whether "instructions" is passed to the retry_chain
+
+
+def test_output_fixing_parser_parse_fail() -> None:
+ n: int = 5 # Success on the (n+1)-th attempt
+ base_parser = SuccessfulParseAfterRetries(attemp_count_before_success=n)
+ parser = OutputFixingParser(
+ parser=base_parser,
+ max_retries=n - 1, # n-1 times to retry, that is, n times call
+ retry_chain=RunnablePassthrough(),
+ legacy=False,
+ )
+ with pytest.raises(OutputParserException):
+ parser.parse("completion")
+ assert base_parser.parse_count == n
+
+
+async def test_output_fixing_parser_aparse_fail() -> None:
+ n: int = 5 # Success on the (n+1)-th attempt
+ base_parser = SuccessfulParseAfterRetries(attemp_count_before_success=n)
+ parser = OutputFixingParser(
+ parser=base_parser,
+ max_retries=n - 1, # n-1 times to retry, that is, n times call
+ retry_chain=RunnablePassthrough(),
+ legacy=False,
+ )
+ with pytest.raises(OutputParserException):
+ await parser.aparse("completion")
+ assert base_parser.parse_count == n
+
+
+@pytest.mark.parametrize(
+ "base_parser",
+ [
+ BooleanOutputParser(),
+ DatetimeOutputParser(),
+ ],
+)
+def test_output_fixing_parser_output_type(base_parser: BaseOutputParser) -> None: # noqa: E501
+ parser = OutputFixingParser(parser=base_parser, retry_chain=RunnablePassthrough()) # noqa: E501
+ assert parser.OutputType is base_parser.OutputType
diff --git a/libs/langchain/tests/unit_tests/output_parsers/test_regex.py b/libs/langchain/tests/unit_tests/output_parsers/test_regex.py
new file mode 100644
index 0000000000000..ef434b4ba7931
--- /dev/null
+++ b/libs/langchain/tests/unit_tests/output_parsers/test_regex.py
@@ -0,0 +1,38 @@
+from typing import Dict
+
+from langchain.output_parsers.regex import RegexParser
+
+# NOTE: The almost same constant variables in ./test_combining_parser.py
+DEF_EXPECTED_RESULT = {
+ "confidence": "A",
+ "explanation": "Paris is the capital of France according to Wikipedia.",
+}
+
+DEF_README = """```json
+{
+ "answer": "Paris",
+ "source": "https://en.wikipedia.org/wiki/France"
+}
+```
+
+//Confidence: A, Explanation: Paris is the capital of France according to Wikipedia."""
+
+
+def test_regex_parser_parse() -> None:
+ """Test regex parser parse."""
+ parser = RegexParser(
+ regex=r"Confidence: (A|B|C), Explanation: (.*)",
+ output_keys=["confidence", "explanation"],
+ default_output_key="noConfidence",
+ )
+ assert DEF_EXPECTED_RESULT == parser.parse(DEF_README)
+
+
+def test_regex_parser_output_type() -> None:
+ """Test regex parser output type is Dict[str, str]."""
+ parser = RegexParser(
+ regex=r"Confidence: (A|B|C), Explanation: (.*)",
+ output_keys=["confidence", "explanation"],
+ default_output_key="noConfidence",
+ )
+ assert parser.OutputType is Dict[str, str]
diff --git a/libs/langchain/tests/unit_tests/output_parsers/test_retry.py b/libs/langchain/tests/unit_tests/output_parsers/test_retry.py
new file mode 100644
index 0000000000000..161ba32a980d4
--- /dev/null
+++ b/libs/langchain/tests/unit_tests/output_parsers/test_retry.py
@@ -0,0 +1,196 @@
+from typing import Any
+
+import pytest
+from langchain_core.prompt_values import StringPromptValue
+from langchain_core.runnables import RunnablePassthrough
+
+from langchain.output_parsers.boolean import BooleanOutputParser
+from langchain.output_parsers.datetime import DatetimeOutputParser
+from langchain.output_parsers.retry import (
+ BaseOutputParser,
+ OutputParserException,
+ RetryOutputParser,
+ RetryWithErrorOutputParser,
+)
+
+
+class SuccessfulParseAfterRetries(BaseOutputParser[str]):
+ parse_count: int = 0 # Number of times parse has been called
+ attemp_count_before_success: int # Number of times to fail before succeeding # noqa
+ error_msg: str = "error"
+
+ def parse(self, *args: Any, **kwargs: Any) -> str:
+ self.parse_count += 1
+ if self.parse_count <= self.attemp_count_before_success:
+ raise OutputParserException(self.error_msg)
+ return "parsed"
+
+
+def test_retry_output_parser_parse_with_prompt() -> None:
+ n: int = 5 # Success on the (n+1)-th attempt
+ base_parser = SuccessfulParseAfterRetries(attemp_count_before_success=n)
+ parser = RetryOutputParser(
+ parser=base_parser,
+ retry_chain=RunnablePassthrough(),
+ max_retries=n, # n times to retry, that is, (n+1) times call
+ legacy=False,
+ )
+ actual = parser.parse_with_prompt("completion", StringPromptValue(text="dummy")) # noqa: E501
+ assert actual == "parsed"
+ assert base_parser.parse_count == n + 1
+
+
+def test_retry_output_parser_parse_with_prompt_fail() -> None:
+ n: int = 5 # Success on the (n+1)-th attempt
+ base_parser = SuccessfulParseAfterRetries(attemp_count_before_success=n)
+ parser = RetryOutputParser(
+ parser=base_parser,
+ retry_chain=RunnablePassthrough(),
+ max_retries=n - 1, # n-1 times to retry, that is, n times call
+ legacy=False,
+ )
+ with pytest.raises(OutputParserException):
+ parser.parse_with_prompt("completion", StringPromptValue(text="dummy"))
+ assert base_parser.parse_count == n
+
+
+async def test_retry_output_parser_aparse_with_prompt() -> None:
+ n: int = 5 # Success on the (n+1)-th attempt
+ base_parser = SuccessfulParseAfterRetries(attemp_count_before_success=n)
+ parser = RetryOutputParser(
+ parser=base_parser,
+ retry_chain=RunnablePassthrough(),
+ max_retries=n, # n times to retry, that is, (n+1) times call
+ legacy=False,
+ )
+ actual = await parser.aparse_with_prompt(
+ "completion", StringPromptValue(text="dummy")
+ )
+ assert actual == "parsed"
+ assert base_parser.parse_count == n + 1
+
+
+async def test_retry_output_parser_aparse_with_prompt_fail() -> None:
+ n: int = 5 # Success on the (n+1)-th attempt
+ base_parser = SuccessfulParseAfterRetries(attemp_count_before_success=n)
+ parser = RetryOutputParser(
+ parser=base_parser,
+ retry_chain=RunnablePassthrough(),
+ max_retries=n - 1, # n-1 times to retry, that is, n times call
+ legacy=False,
+ )
+ with pytest.raises(OutputParserException):
+ await parser.aparse_with_prompt("completion", StringPromptValue(text="dummy")) # noqa: E501
+ assert base_parser.parse_count == n
+
+
+@pytest.mark.parametrize(
+ "base_parser",
+ [
+ BooleanOutputParser(),
+ DatetimeOutputParser(),
+ ],
+)
+def test_retry_output_parser_output_type(base_parser: BaseOutputParser) -> None:
+ parser = RetryOutputParser(
+ parser=base_parser,
+ retry_chain=RunnablePassthrough(),
+ legacy=False,
+ )
+ assert parser.OutputType is base_parser.OutputType
+
+
+def test_retry_output_parser_parse_is_not_implemented() -> None:
+ parser = RetryOutputParser(
+ parser=BooleanOutputParser(),
+ retry_chain=RunnablePassthrough(),
+ legacy=False,
+ )
+ with pytest.raises(NotImplementedError):
+ parser.parse("completion")
+
+
+def test_retry_with_error_output_parser_parse_with_prompt() -> None:
+ n: int = 5 # Success on the (n+1)-th attempt
+ base_parser = SuccessfulParseAfterRetries(attemp_count_before_success=n)
+ parser = RetryWithErrorOutputParser(
+ parser=base_parser,
+ retry_chain=RunnablePassthrough(),
+ max_retries=n, # n times to retry, that is, (n+1) times call
+ legacy=False,
+ )
+ actual = parser.parse_with_prompt("completion", StringPromptValue(text="dummy")) # noqa: E501
+ assert actual == "parsed"
+ assert base_parser.parse_count == n + 1
+
+
+def test_retry_with_error_output_parser_parse_with_prompt_fail() -> None:
+ n: int = 5 # Success on the (n+1)-th attempt
+ base_parser = SuccessfulParseAfterRetries(attemp_count_before_success=n)
+ parser = RetryWithErrorOutputParser(
+ parser=base_parser,
+ retry_chain=RunnablePassthrough(),
+ max_retries=n - 1, # n-1 times to retry, that is, n times call
+ legacy=False,
+ )
+ with pytest.raises(OutputParserException):
+ parser.parse_with_prompt("completion", StringPromptValue(text="dummy"))
+ assert base_parser.parse_count == n
+
+
+async def test_retry_with_error_output_parser_aparse_with_prompt() -> None:
+ n: int = 5 # Success on the (n+1)-th attempt
+ base_parser = SuccessfulParseAfterRetries(attemp_count_before_success=n)
+ parser = RetryWithErrorOutputParser(
+ parser=base_parser,
+ retry_chain=RunnablePassthrough(),
+ max_retries=n, # n times to retry, that is, (n+1) times call
+ legacy=False,
+ )
+ actual = await parser.aparse_with_prompt(
+ "completion", StringPromptValue(text="dummy")
+ )
+ assert actual == "parsed"
+ assert base_parser.parse_count == n + 1
+
+
+async def test_retry_with_error_output_parser_aparse_with_prompt_fail() -> None: # noqa: E501
+ n: int = 5 # Success on the (n+1)-th attempt
+ base_parser = SuccessfulParseAfterRetries(attemp_count_before_success=n)
+ parser = RetryWithErrorOutputParser(
+ parser=base_parser,
+ retry_chain=RunnablePassthrough(),
+ max_retries=n - 1, # n-1 times to retry, that is, n times call
+ legacy=False,
+ )
+ with pytest.raises(OutputParserException):
+ await parser.aparse_with_prompt("completion", StringPromptValue(text="dummy")) # noqa: E501
+ assert base_parser.parse_count == n
+
+
+@pytest.mark.parametrize(
+ "base_parser",
+ [
+ BooleanOutputParser(),
+ DatetimeOutputParser(),
+ ],
+)
+def test_retry_with_error_output_parser_output_type(
+ base_parser: BaseOutputParser,
+) -> None:
+ parser = RetryWithErrorOutputParser(
+ parser=base_parser,
+ retry_chain=RunnablePassthrough(),
+ legacy=False,
+ )
+ assert parser.OutputType is base_parser.OutputType
+
+
+def test_retry_with_error_output_parser_parse_is_not_implemented() -> None:
+ parser = RetryWithErrorOutputParser(
+ parser=BooleanOutputParser(),
+ retry_chain=RunnablePassthrough(),
+ legacy=False,
+ )
+ with pytest.raises(NotImplementedError):
+ parser.parse("completion")
diff --git a/libs/langchain/tests/unit_tests/retrievers/test_ensemble.py b/libs/langchain/tests/unit_tests/retrievers/test_ensemble.py
new file mode 100644
index 0000000000000..4c5e9837c0b41
--- /dev/null
+++ b/libs/langchain/tests/unit_tests/retrievers/test_ensemble.py
@@ -0,0 +1,88 @@
+from typing import List, Optional
+
+from langchain_core.callbacks.manager import CallbackManagerForRetrieverRun
+from langchain_core.documents import Document
+from langchain_core.retrievers import BaseRetriever
+
+from langchain.retrievers.ensemble import EnsembleRetriever
+
+
+class MockRetriever(BaseRetriever):
+ docs: List[Document]
+
+ def _get_relevant_documents(
+ self,
+ query: str,
+ *,
+ run_manager: Optional[CallbackManagerForRetrieverRun] = None,
+ ) -> List[Document]:
+ """Return the documents"""
+ return self.docs
+
+
+def test_invoke() -> None:
+ documents1 = [
+ Document(page_content="a", metadata={"id": 1}),
+ Document(page_content="b", metadata={"id": 2}),
+ Document(page_content="c", metadata={"id": 3}),
+ ]
+ documents2 = [Document(page_content="b")]
+
+ retriever1 = MockRetriever(docs=documents1)
+ retriever2 = MockRetriever(docs=documents2)
+
+ ensemble_retriever = EnsembleRetriever(
+ retrievers=[retriever1, retriever2], weights=[0.5, 0.5], id_key=None
+ )
+ ranked_documents = ensemble_retriever.invoke("_")
+
+ # The document with page_content "b" in documents2
+ # will be merged with the document with page_content "b"
+ # in documents1, so the length of ranked_documents should be 3.
+ # Additionally, the document with page_content "b" will be ranked 1st.
+ assert len(ranked_documents) == 3
+ assert ranked_documents[0].page_content == "b"
+
+ documents1 = [
+ Document(page_content="a", metadata={"id": 1}),
+ Document(page_content="b", metadata={"id": 2}),
+ Document(page_content="c", metadata={"id": 3}),
+ ]
+ documents2 = [Document(page_content="d")]
+
+ retriever1 = MockRetriever(docs=documents1)
+ retriever2 = MockRetriever(docs=documents2)
+
+ ensemble_retriever = EnsembleRetriever(
+ retrievers=[retriever1, retriever2], weights=[0.5, 0.5], id_key=None
+ )
+ ranked_documents = ensemble_retriever.invoke("_")
+
+ # The document with page_content "d" in documents2 will not be merged
+ # with any document in documents1, so the length of ranked_documents
+ # should be 4. The document with page_content "a" and the document
+ # with page_content "d" will have the same score, but the document
+ # with page_content "a" will be ranked 1st because retriever1 has a smaller index.
+ assert len(ranked_documents) == 4
+ assert ranked_documents[0].page_content == "a"
+
+ documents1 = [
+ Document(page_content="a", metadata={"id": 1}),
+ Document(page_content="b", metadata={"id": 2}),
+ Document(page_content="c", metadata={"id": 3}),
+ ]
+ documents2 = [Document(page_content="d", metadata={"id": 2})]
+
+ retriever1 = MockRetriever(docs=documents1)
+ retriever2 = MockRetriever(docs=documents2)
+
+ ensemble_retriever = EnsembleRetriever(
+ retrievers=[retriever1, retriever2], weights=[0.5, 0.5], id_key="id"
+ )
+ ranked_documents = ensemble_retriever.invoke("_")
+
+ # Since id_key is specified, the document with id 2 will be merged.
+ # Therefore, the length of ranked_documents should be 3.
+ # Additionally, the document with page_content "b" will be ranked 1st.
+ assert len(ranked_documents) == 3
+ assert ranked_documents[0].page_content == "b"
diff --git a/libs/langchain/tests/unit_tests/retrievers/test_multi_query.py b/libs/langchain/tests/unit_tests/retrievers/test_multi_query.py
new file mode 100644
index 0000000000000..8f80e77e79b09
--- /dev/null
+++ b/libs/langchain/tests/unit_tests/retrievers/test_multi_query.py
@@ -0,0 +1,40 @@
+from typing import List
+
+import pytest as pytest
+from langchain_core.documents import Document
+
+from langchain.retrievers.multi_query import _unique_documents
+
+
+@pytest.mark.parametrize(
+ "documents,expected",
+ [
+ ([], []),
+ ([Document(page_content="foo")], [Document(page_content="foo")]),
+ ([Document(page_content="foo")] * 2, [Document(page_content="foo")]),
+ (
+ [Document(page_content="foo", metadata={"bar": "baz"})] * 2,
+ [Document(page_content="foo", metadata={"bar": "baz"})],
+ ),
+ (
+ [Document(page_content="foo", metadata={"bar": [1, 2]})] * 2,
+ [Document(page_content="foo", metadata={"bar": [1, 2]})],
+ ),
+ (
+ [Document(page_content="foo", metadata={"bar": {1, 2}})] * 2,
+ [Document(page_content="foo", metadata={"bar": {1, 2}})],
+ ),
+ (
+ [
+ Document(page_content="foo", metadata={"bar": [1, 2]}),
+ Document(page_content="foo", metadata={"bar": [2, 1]}),
+ ],
+ [
+ Document(page_content="foo", metadata={"bar": [1, 2]}),
+ Document(page_content="foo", metadata={"bar": [2, 1]}),
+ ],
+ ),
+ ],
+)
+def test__unique_documents(documents: List[Document], expected: List[Document]) -> None:
+ assert _unique_documents(documents) == expected
diff --git a/libs/partners/ai21/pyproject.toml b/libs/partners/ai21/pyproject.toml
index 70e75d3287b05..518972b8e5cc9 100644
--- a/libs/partners/ai21/pyproject.toml
+++ b/libs/partners/ai21/pyproject.toml
@@ -1,16 +1,20 @@
-
[tool.poetry]
-name = "gigachain-ai21"
-version = "0.1.5"
-description = "An integration package connecting AI21 and LangChain"
+name = "langchain-ai21"
+version = "0.1.6"
+description = "An integration package connecting AI21 and Gigachain"
authors = []
readme = "README.md"
+repository = "https://github.com/langchain-ai/langchain"
+license = "MIT"
+
+[tool.poetry.urls]
+"Source Code" = "https://github.com/langchain-ai/langchain/tree/master/libs/partners/ai21"
[tool.poetry.dependencies]
python = ">=3.8.1,<4.0"
-gigachain-core = ">=0.1.48,<0.3"
-gigachain-text-splitters = ">=0.0.1,<0.2"
-ai21 = "^2.2.5"
+gigachain-core = "^0.2.4"
+gigachain-text-splitters = "^0.2.0"
+ai21 = "^2.4.1"
[tool.poetry.group.test]
optional = true
@@ -90,4 +94,3 @@ markers = [
"scheduled: mark tests to run in scheduled testing",
]
asyncio_mode = "auto"
-
diff --git a/libs/partners/airbyte/pyproject.toml b/libs/partners/airbyte/pyproject.toml
index 0b79629d3cf9a..2c2396435bdb8 100644
--- a/libs/partners/airbyte/pyproject.toml
+++ b/libs/partners/airbyte/pyproject.toml
@@ -1,7 +1,7 @@
[tool.poetry]
name = "langchain-airbyte"
version = "0.1.1"
-description = "An integration package connecting Airbyte and LangChain"
+description = "An integration package connecting Airbyte and Gigachain"
authors = []
readme = "README.md"
repository = "https://github.com/langchain-ai/langchain"
@@ -44,8 +44,8 @@ ruff = "^0.1.8"
[tool.poetry.group.typing.dependencies]
mypy = "^1.7.1"
gigachain-core = { path = "../../core", develop = true }
-langchain-text-splitters = { path = "../../text-splitters", develop = true }
-langchain = { path = "../../langchain", develop = true }
+gigachain-text-splitters = { path = "../../text-splitters", develop = true }
+gigachain = { path = "../../langchain", develop = true }
[tool.poetry.group.dev]
optional = true
diff --git a/libs/partners/anthropic/pyproject.toml b/libs/partners/anthropic/pyproject.toml
index abd56e6cda493..de277b71317b1 100644
--- a/libs/partners/anthropic/pyproject.toml
+++ b/libs/partners/anthropic/pyproject.toml
@@ -1,23 +1,19 @@
-
[tool.poetry]
name = "gigachain-anthropic"
-version = "0.1.13"
-description = "An integration package connecting AnthropicMessages and LangChain"
+version = "0.1.16"
+description = "An integration package connecting AnthropicMessages and Gigachain"
authors = []
readme = "README.md"
-repository = "https://github.com/gigachain-ai/gigachain"
+repository = "https://github.com/langchain-ai/langchain"
license = "MIT"
-packages = [
- {include = "langchain_anthropic"}
-]
[tool.poetry.urls]
-"Source Code" = "https://github.com/gigachain-ai/gigachain/tree/master/libs/partners/anthropic"
+"Source Code" = "https://github.com/langchain-ai/langchain/tree/master/libs/partners/anthropic"
[tool.poetry.dependencies]
python = ">=3.8.1,<4.0"
-gigachain-core = ">=0.1.43,<0.3"
-anthropic = ">=0.26.0,<1"
+gigachain-core = { version = ">=0.2.10,<0.3" }
+anthropic = ">=0.28.0,<1"
defusedxml = { version = "^0.7.1", optional = true }
[tool.poetry.group.test]
@@ -45,10 +41,9 @@ optional = true
[tool.poetry.group.lint.dependencies]
ruff = ">=0.2.2,<1"
-mypy = "^0.991"
[tool.poetry.group.typing.dependencies]
-mypy = "^0.991"
+mypy = "^1"
gigachain-core = { path = "../../core", develop = true }
[tool.poetry.group.dev]
@@ -100,4 +95,3 @@ markers = [
"compile: mark placeholder test used to compile integration tests without running them",
]
asyncio_mode = "auto"
-
diff --git a/libs/partners/azure-dynamic-sessions/pyproject.toml b/libs/partners/azure-dynamic-sessions/pyproject.toml
index a1ead73f684ba..87d0186c86886 100644
--- a/libs/partners/azure-dynamic-sessions/pyproject.toml
+++ b/libs/partners/azure-dynamic-sessions/pyproject.toml
@@ -1,7 +1,7 @@
[tool.poetry]
-name = "langchain-azure-dynamic-sessions"
+name = "gigachain-azure-dynamic-sessions"
version = "0.1.0"
-description = "An integration package connecting Azure Container Apps dynamic sessions and LangChain"
+description = "An integration package connecting Azure Container Apps dynamic sessions and Gigachain"
authors = []
readme = "README.md"
repository = "https://github.com/langchain-ai/langchain"
@@ -12,7 +12,7 @@ license = "MIT"
[tool.poetry.dependencies]
python = ">=3.8.1,<4.0"
-langchain-core = ">=0.1.52,<0.3"
+gigachain-core = ">=0.1.52,<0.3"
azure-identity = "^1.16.0"
requests = "^2.31.0"
@@ -26,7 +26,7 @@ pytest-mock = "^3.10.0"
syrupy = "^4.0.2"
pytest-watcher = "^0.3.4"
pytest-asyncio = "^0.21.1"
-langchain-core = { path = "../../core", develop = true }
+gigachain-core = { path = "../../core", develop = true }
python-dotenv = "^1.0.1"
[tool.poetry.group.test_integration]
@@ -52,25 +52,33 @@ pytest = "^7.3.0"
[tool.poetry.group.typing.dependencies]
mypy = "^0.991"
-langchain-core = { path = "../../core", develop = true }
+gigachain-core = { path = "../../core", develop = true }
types-requests = "^2.31.0.20240406"
[tool.poetry.group.dev]
optional = true
[tool.poetry.group.dev.dependencies]
-langchain-core = { path = "../../core", develop = true }
+gigachain-core = { path = "../../core", develop = true }
ipykernel = "^6.29.4"
-langchain-openai = { path = "../openai", develop = true }
+gigachain-openai = { path = "../openai", develop = true }
langchainhub = "^0.1.15"
-[tool.ruff]
+[tool.ruff.lint]
select = [
"E", # pycodestyle
"F", # pyflakes
"I", # isort
+ "D", # pydocstyle
+
]
+[tool.ruff.lint.pydocstyle]
+convention = "google"
+
+[tool.ruff.lint.per-file-ignores]
+"tests/**" = ["D"] # ignore docstring checks for tests
+
[tool.mypy]
disallow_untyped_defs = "True"
diff --git a/libs/partners/chroma/pyproject.toml b/libs/partners/chroma/pyproject.toml
index d1c762e4025c9..d3cfaeda6b1e0 100644
--- a/libs/partners/chroma/pyproject.toml
+++ b/libs/partners/chroma/pyproject.toml
@@ -1,14 +1,11 @@
[tool.poetry]
name = "gigachain-chroma"
version = "0.1.1"
-description = "An integration package connecting Chroma and GigaChain"
+description = "An integration package connecting Chroma and Gigachain"
authors = []
readme = "README.md"
repository = "https://github.com/langchain-ai/langchain"
license = "MIT"
-packages = [
- {include = "langchain_chroma"}
-]
[tool.poetry.urls]
"Source Code" = "https://github.com/langchain-ai/langchain/tree/master/libs/partners/chroma"
@@ -17,7 +14,11 @@ packages = [
python = ">=3.8.1,<3.13"
gigachain-core = ">=0.1.40,<0.3"
chromadb = { version = ">=0.4.0,<0.6.0" }
-numpy = "^1"
+# Support Python 3.8 and 3.12+.
+numpy = [
+ {version = "^1", python = "<3.12"},
+ {version = "^1.26.0", python = ">=3.12"}
+]
fastapi = { version = ">=0.95.2,<1", optional = true }
[tool.poetry.group.test]
@@ -72,9 +73,16 @@ select = [
"F", # pyflakes
"I", # isort
"T201", # print
+ "D", # pydocstyle
]
+[tool.ruff.lint.pydocstyle]
+convention = "google"
+
+[tool.ruff.lint.per-file-ignores]
+"tests/**" = ["D"] # ignore docstring checks for tests
+
[tool.mypy]
disallow_untyped_defs = "True"
diff --git a/libs/partners/couchbase/.gitignore b/libs/partners/couchbase/.gitignore
new file mode 100644
index 0000000000000..53fd65cf3d994
--- /dev/null
+++ b/libs/partners/couchbase/.gitignore
@@ -0,0 +1,3 @@
+__pycache__
+# mypy
+.mypy_cache/
diff --git a/libs/partners/couchbase/LICENSE b/libs/partners/couchbase/LICENSE
new file mode 100644
index 0000000000000..fc0602feecdd6
--- /dev/null
+++ b/libs/partners/couchbase/LICENSE
@@ -0,0 +1,21 @@
+MIT License
+
+Copyright (c) 2024 LangChain, Inc.
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
diff --git a/libs/partners/couchbase/Makefile b/libs/partners/couchbase/Makefile
new file mode 100644
index 0000000000000..4b85a26db82ca
--- /dev/null
+++ b/libs/partners/couchbase/Makefile
@@ -0,0 +1,62 @@
+.PHONY: all format lint test tests integration_tests docker_tests help extended_tests
+
+# Default target executed when no arguments are given to make.
+all: help
+
+# Define a variable for the test file path.
+TEST_FILE ?= tests/unit_tests/
+integration_test integration_tests: TEST_FILE = tests/integration_tests/
+
+
+# unit tests are run with the --disable-socket flag to prevent network calls
+test tests:
+ poetry run pytest --disable-socket --allow-unix-socket $(TEST_FILE)
+
+# integration tests are run without the --disable-socket flag to allow network calls
+integration_test integration_tests:
+ poetry run pytest $(TEST_FILE)
+
+######################
+# LINTING AND FORMATTING
+######################
+
+# Define a variable for Python and notebook files.
+PYTHON_FILES=.
+MYPY_CACHE=.mypy_cache
+lint format: PYTHON_FILES=.
+lint_diff format_diff: PYTHON_FILES=$(shell git diff --relative=libs/partners/couchbase --name-only --diff-filter=d master | grep -E '\.py$$|\.ipynb$$')
+lint_package: PYTHON_FILES=langchain_couchbase
+lint_tests: PYTHON_FILES=tests
+lint_tests: MYPY_CACHE=.mypy_cache_test
+
+lint lint_diff lint_package lint_tests:
+ poetry run ruff .
+ poetry run ruff format $(PYTHON_FILES) --diff
+ poetry run ruff --select I $(PYTHON_FILES)
+ mkdir -p $(MYPY_CACHE); poetry run mypy $(PYTHON_FILES) --cache-dir $(MYPY_CACHE)
+
+format format_diff:
+ poetry run ruff format $(PYTHON_FILES)
+ poetry run ruff --select I --fix $(PYTHON_FILES)
+
+spell_check:
+ poetry run codespell --toml pyproject.toml
+
+spell_fix:
+ poetry run codespell --toml pyproject.toml -w
+
+check_imports: $(shell find langchain_couchbase -name '*.py')
+ poetry run python ./scripts/check_imports.py $^
+
+######################
+# HELP
+######################
+
+help:
+ @echo '----'
+ @echo 'check_imports - check imports'
+ @echo 'format - run code formatters'
+ @echo 'lint - run linters'
+ @echo 'test - run unit tests'
+ @echo 'tests - run unit tests'
+ @echo 'test TEST_FILE= - run all tests in file'
diff --git a/libs/partners/couchbase/README.md b/libs/partners/couchbase/README.md
new file mode 100644
index 0000000000000..006de243ab237
--- /dev/null
+++ b/libs/partners/couchbase/README.md
@@ -0,0 +1,42 @@
+# langchain-couchbase
+
+This package contains the LangChain integration with Couchbase
+
+## Installation
+
+```bash
+pip install -U langchain-couchbase
+```
+
+## Usage
+
+The `CouchbaseVectorStore` class exposes the connection to the Couchbase vector store.
+
+```python
+from langchain_couchbase.vectorstores import CouchbaseVectorStore
+
+from couchbase.cluster import Cluster
+from couchbase.auth import PasswordAuthenticator
+from couchbase.options import ClusterOptions
+from datetime import timedelta
+
+auth = PasswordAuthenticator(username, password)
+options = ClusterOptions(auth)
+connect_string = "couchbases://localhost"
+cluster = Cluster(connect_string, options)
+
+# Wait until the cluster is ready for use.
+cluster.wait_until_ready(timedelta(seconds=5))
+
+embeddings = OpenAIEmbeddings()
+
+vectorstore = CouchbaseVectorStore(
+ cluster=cluster,
+ bucket_name="",
+ scope_name="",
+ collection_name="",
+ embedding=embeddings,
+ index_name="vector-search-index",
+)
+
+```
diff --git a/libs/partners/couchbase/langchain_couchbase/__init__.py b/libs/partners/couchbase/langchain_couchbase/__init__.py
new file mode 100644
index 0000000000000..2f84db440eb97
--- /dev/null
+++ b/libs/partners/couchbase/langchain_couchbase/__init__.py
@@ -0,0 +1,5 @@
+from langchain_couchbase.vectorstores import CouchbaseVectorStore
+
+__all__ = [
+ "CouchbaseVectorStore",
+]
diff --git a/libs/partners/couchbase/langchain_couchbase/py.typed b/libs/partners/couchbase/langchain_couchbase/py.typed
new file mode 100644
index 0000000000000..e69de29bb2d1d
diff --git a/libs/partners/couchbase/langchain_couchbase/vectorstores.py b/libs/partners/couchbase/langchain_couchbase/vectorstores.py
new file mode 100644
index 0000000000000..6553abb5e71c8
--- /dev/null
+++ b/libs/partners/couchbase/langchain_couchbase/vectorstores.py
@@ -0,0 +1,615 @@
+"""Couchbase vector stores."""
+
+from __future__ import annotations
+
+import uuid
+from typing import (
+ Any,
+ Dict,
+ Iterable,
+ List,
+ Optional,
+ Tuple,
+ Type,
+)
+
+import couchbase.search as search
+from couchbase.cluster import Cluster
+from couchbase.exceptions import DocumentExistsException, DocumentNotFoundException
+from couchbase.options import SearchOptions
+from couchbase.vector_search import VectorQuery, VectorSearch
+from langchain_core.documents import Document
+from langchain_core.embeddings import Embeddings
+from langchain_core.vectorstores import VectorStore
+
+
+class CouchbaseVectorStore(VectorStore):
+ """Couchbase vector store.
+
+ To use it, you need
+ - a Couchbase database with a pre-defined Search index with support for
+ vector fields
+
+ Example:
+ .. code-block:: python
+
+ from langchain_couchbase import CouchbaseVectorStore
+ from langchain_openai import OpenAIEmbeddings
+
+ from couchbase.cluster import Cluster
+ from couchbase.auth import PasswordAuthenticator
+ from couchbase.options import ClusterOptions
+ from datetime import timedelta
+
+ auth = PasswordAuthenticator(username, password)
+ options = ClusterOptions(auth)
+ connect_string = "couchbases://localhost"
+ cluster = Cluster(connect_string, options)
+
+ # Wait until the cluster is ready for use.
+ cluster.wait_until_ready(timedelta(seconds=5))
+
+ embeddings = OpenAIEmbeddings()
+
+ vectorstore = CouchbaseVectorStore(
+ cluster=cluster,
+ bucket_name="",
+ scope_name="",
+ collection_name="",
+ embedding=embeddings,
+ index_name="vector-index",
+ )
+
+ vectorstore.add_texts(["hello", "world"])
+ results = vectorstore.similarity_search("ola", k=1)
+ """
+
+ # Default batch size
+ DEFAULT_BATCH_SIZE = 100
+ _metadata_key = "metadata"
+ _default_text_key = "text"
+ _default_embedding_key = "embedding"
+
+ def _check_bucket_exists(self) -> bool:
+ """Check if the bucket exists in the linked Couchbase cluster"""
+ bucket_manager = self._cluster.buckets()
+ try:
+ bucket_manager.get_bucket(self._bucket_name)
+ return True
+ except Exception:
+ return False
+
+ def _check_scope_and_collection_exists(self) -> bool:
+ """Check if the scope and collection exists in the linked Couchbase bucket
+ Raises a ValueError if either is not found"""
+ scope_collection_map: Dict[str, Any] = {}
+
+ # Get a list of all scopes in the bucket
+ for scope in self._bucket.collections().get_all_scopes():
+ scope_collection_map[scope.name] = []
+
+ # Get a list of all the collections in the scope
+ for collection in scope.collections:
+ scope_collection_map[scope.name].append(collection.name)
+
+ # Check if the scope exists
+ if self._scope_name not in scope_collection_map.keys():
+ raise ValueError(
+ f"Scope {self._scope_name} not found in Couchbase "
+ f"bucket {self._bucket_name}"
+ )
+
+ # Check if the collection exists in the scope
+ if self._collection_name not in scope_collection_map[self._scope_name]:
+ raise ValueError(
+ f"Collection {self._collection_name} not found in scope "
+ f"{self._scope_name} in Couchbase bucket {self._bucket_name}"
+ )
+
+ return True
+
+ def _check_index_exists(self) -> bool:
+ """Check if the Search index exists in the linked Couchbase cluster
+ Raises a ValueError if the index does not exist"""
+ if self._scoped_index:
+ all_indexes = [
+ index.name for index in self._scope.search_indexes().get_all_indexes()
+ ]
+ if self._index_name not in all_indexes:
+ raise ValueError(
+ f"Index {self._index_name} does not exist. "
+ " Please create the index before searching."
+ )
+ else:
+ all_indexes = [
+ index.name for index in self._cluster.search_indexes().get_all_indexes()
+ ]
+ if self._index_name not in all_indexes:
+ raise ValueError(
+ f"Index {self._index_name} does not exist. "
+ " Please create the index before searching."
+ )
+
+ return True
+
+ def __init__(
+ self,
+ cluster: Cluster,
+ bucket_name: str,
+ scope_name: str,
+ collection_name: str,
+ embedding: Embeddings,
+ index_name: str,
+ *,
+ text_key: Optional[str] = _default_text_key,
+ embedding_key: Optional[str] = _default_embedding_key,
+ scoped_index: bool = True,
+ ) -> None:
+ """
+ Initialize the Couchbase Vector Store.
+
+ Args:
+
+ cluster (Cluster): couchbase cluster object with active connection.
+ bucket_name (str): name of bucket to store documents in.
+ scope_name (str): name of scope in the bucket to store documents in.
+ collection_name (str): name of collection in the scope to store documents in
+ embedding (Embeddings): embedding function to use.
+ index_name (str): name of the Search index to use.
+ text_key (optional[str]): key in document to use as text.
+ Set to text by default.
+ embedding_key (optional[str]): key in document to use for the embeddings.
+ Set to embedding by default.
+ scoped_index (optional[bool]): specify whether the index is a scoped index.
+ Set to True by default.
+ """
+ if not isinstance(cluster, Cluster):
+ raise ValueError(
+ f"cluster should be an instance of couchbase.Cluster, "
+ f"got {type(cluster)}"
+ )
+
+ self._cluster = cluster
+
+ if not embedding:
+ raise ValueError("Embeddings instance must be provided.")
+
+ if not bucket_name:
+ raise ValueError("bucket_name must be provided.")
+
+ if not scope_name:
+ raise ValueError("scope_name must be provided.")
+
+ if not collection_name:
+ raise ValueError("collection_name must be provided.")
+
+ if not index_name:
+ raise ValueError("index_name must be provided.")
+
+ self._bucket_name = bucket_name
+ self._scope_name = scope_name
+ self._collection_name = collection_name
+ self._embedding_function = embedding
+ self._text_key = text_key
+ self._embedding_key = embedding_key
+ self._index_name = index_name
+ self._scoped_index = scoped_index
+
+ # Check if the bucket exists
+ if not self._check_bucket_exists():
+ raise ValueError(
+ f"Bucket {self._bucket_name} does not exist. "
+ " Please create the bucket before searching."
+ )
+
+ try:
+ self._bucket = self._cluster.bucket(self._bucket_name)
+ self._scope = self._bucket.scope(self._scope_name)
+ self._collection = self._scope.collection(self._collection_name)
+ except Exception as e:
+ raise ValueError(
+ "Error connecting to couchbase. "
+ "Please check the connection and credentials."
+ ) from e
+
+ # Check if the scope and collection exists. Throws ValueError if they don't
+ try:
+ self._check_scope_and_collection_exists()
+ except Exception as e:
+ raise e
+
+ # Check if the index exists. Throws ValueError if it doesn't
+ try:
+ self._check_index_exists()
+ except Exception as e:
+ raise e
+
+ def add_texts(
+ self,
+ texts: Iterable[str],
+ metadatas: Optional[List[dict]] = None,
+ ids: Optional[List[str]] = None,
+ batch_size: Optional[int] = None,
+ **kwargs: Any,
+ ) -> List[str]:
+ """Run texts through the embeddings and persist in vectorstore.
+
+ If the document IDs are passed, the existing documents (if any) will be
+ overwritten with the new ones.
+
+ Args:
+ texts (Iterable[str]): Iterable of strings to add to the vectorstore.
+ metadatas (Optional[List[Dict]]): Optional list of metadatas associated
+ with the texts.
+ ids (Optional[List[str]]): Optional list of ids associated with the texts.
+ IDs have to be unique strings across the collection.
+ If it is not specified uuids are generated and used as ids.
+ batch_size (Optional[int]): Optional batch size for bulk insertions.
+ Default is 100.
+
+ Returns:
+ List[str]:List of ids from adding the texts into the vectorstore.
+ """
+
+ if not batch_size:
+ batch_size = self.DEFAULT_BATCH_SIZE
+ doc_ids: List[str] = []
+
+ if ids is None:
+ ids = [uuid.uuid4().hex for _ in texts]
+
+ if metadatas is None:
+ metadatas = [{} for _ in texts]
+
+ embedded_texts = self._embedding_function.embed_documents(list(texts))
+
+ documents_to_insert = [
+ {
+ id: {
+ self._text_key: text,
+ self._embedding_key: vector,
+ self._metadata_key: metadata,
+ }
+ for id, text, vector, metadata in zip(
+ ids, texts, embedded_texts, metadatas
+ )
+ }
+ ]
+
+ # Insert in batches
+ for i in range(0, len(documents_to_insert), batch_size):
+ batch = documents_to_insert[i : i + batch_size]
+ try:
+ result = self._collection.upsert_multi(batch[0])
+ if result.all_ok:
+ doc_ids.extend(batch[0].keys())
+ except DocumentExistsException as e:
+ raise ValueError(f"Document already exists: {e}")
+
+ return doc_ids
+
+ def delete(self, ids: Optional[List[str]] = None, **kwargs: Any) -> Optional[bool]:
+ """Delete documents from the vector store by ids.
+
+ Args:
+ ids (List[str]): List of IDs of the documents to delete.
+ batch_size (Optional[int]): Optional batch size for bulk deletions.
+
+ Returns:
+ bool: True if all the documents were deleted successfully, False otherwise.
+
+ """
+
+ if ids is None:
+ raise ValueError("No document ids provided to delete.")
+
+ batch_size = kwargs.get("batch_size", self.DEFAULT_BATCH_SIZE)
+ deletion_status = True
+
+ # Delete in batches
+ for i in range(0, len(ids), batch_size):
+ batch = ids[i : i + batch_size]
+ try:
+ result = self._collection.remove_multi(batch)
+ except DocumentNotFoundException as e:
+ deletion_status = False
+ raise ValueError(f"Document not found: {e}")
+
+ deletion_status &= result.all_ok
+
+ return deletion_status
+
+ @property
+ def embeddings(self) -> Embeddings:
+ """Return the query embedding object."""
+ return self._embedding_function
+
+ def _format_metadata(self, row_fields: Dict[str, Any]) -> Dict[str, Any]:
+ """Helper method to format the metadata from the Couchbase Search API.
+ Args:
+ row_fields (Dict[str, Any]): The fields to format.
+
+ Returns:
+ Dict[str, Any]: The formatted metadata.
+ """
+ metadata = {}
+ for key, value in row_fields.items():
+ # Couchbase Search returns the metadata key with a prefix
+ # `metadata.` We remove it to get the original metadata key
+ if key.startswith(self._metadata_key):
+ new_key = key.split(self._metadata_key + ".")[-1]
+ metadata[new_key] = value
+ else:
+ metadata[key] = value
+
+ return metadata
+
+ def similarity_search(
+ self,
+ query: str,
+ k: int = 4,
+ search_options: Optional[Dict[str, Any]] = {},
+ **kwargs: Any,
+ ) -> List[Document]:
+ """Return documents most similar to embedding vector with their scores.
+
+ Args:
+ query (str): Query to look up for similar documents
+ k (int): Number of Documents to return.
+ Defaults to 4.
+ search_options (Optional[Dict[str, Any]]): Optional search options that are
+ passed to Couchbase search.
+ Defaults to empty dictionary
+ fields (Optional[List[str]]): Optional list of fields to include in the
+ metadata of results. Note that these need to be stored in the index.
+ If nothing is specified, defaults to all the fields stored in the index.
+
+ Returns:
+ List of Documents most similar to the query.
+ """
+ query_embedding = self.embeddings.embed_query(query)
+ docs_with_scores = self.similarity_search_with_score_by_vector(
+ query_embedding, k, search_options, **kwargs
+ )
+ return [doc for doc, _ in docs_with_scores]
+
+ def similarity_search_with_score_by_vector(
+ self,
+ embedding: List[float],
+ k: int = 4,
+ search_options: Optional[Dict[str, Any]] = {},
+ **kwargs: Any,
+ ) -> List[Tuple[Document, float]]:
+ """Return docs most similar to embedding vector with their scores.
+
+ Args:
+ embedding (List[float]): Embedding vector to look up documents similar to.
+ k (int): Number of Documents to return.
+ Defaults to 4.
+ search_options (Optional[Dict[str, Any]]): Optional search options that are
+ passed to Couchbase search.
+ Defaults to empty dictionary.
+ fields (Optional[List[str]]): Optional list of fields to include in the
+ metadata of results. Note that these need to be stored in the index.
+ If nothing is specified, defaults to all the fields stored in the index.
+
+ Returns:
+ List of (Document, score) that are the most similar to the query vector.
+ """
+
+ fields = kwargs.get("fields", ["*"])
+
+ # Document text field needs to be returned from the search
+ if fields != ["*"] and self._text_key not in fields:
+ fields.append(self._text_key)
+
+ search_req = search.SearchRequest.create(
+ VectorSearch.from_vector_query(
+ VectorQuery(
+ self._embedding_key,
+ embedding,
+ k,
+ )
+ )
+ )
+ try:
+ if self._scoped_index:
+ search_iter = self._scope.search(
+ self._index_name,
+ search_req,
+ SearchOptions(
+ limit=k,
+ fields=fields,
+ raw=search_options,
+ ),
+ )
+
+ else:
+ search_iter = self._cluster.search(
+ self._index_name,
+ search_req,
+ SearchOptions(limit=k, fields=fields, raw=search_options),
+ )
+
+ docs_with_score = []
+
+ # Parse the results
+ for row in search_iter.rows():
+ text = row.fields.pop(self._text_key, "")
+
+ # Format the metadata from Couchbase
+ metadata = self._format_metadata(row.fields)
+
+ score = row.score
+ doc = Document(page_content=text, metadata=metadata)
+ docs_with_score.append((doc, score))
+
+ except Exception as e:
+ raise ValueError(f"Search failed with error: {e}")
+
+ return docs_with_score
+
+ def similarity_search_with_score(
+ self,
+ query: str,
+ k: int = 4,
+ search_options: Optional[Dict[str, Any]] = {},
+ **kwargs: Any,
+ ) -> List[Tuple[Document, float]]:
+ """Return documents that are most similar to the query with their scores.
+
+ Args:
+ query (str): Query to look up for similar documents
+ k (int): Number of Documents to return.
+ Defaults to 4.
+ search_options (Optional[Dict[str, Any]]): Optional search options that are
+ passed to Couchbase search.
+ Defaults to empty dictionary.
+ fields (Optional[List[str]]): Optional list of fields to include in the
+ metadata of results. Note that these need to be stored in the index.
+ If nothing is specified, defaults to text and metadata fields.
+
+ Returns:
+ List of (Document, score) that are most similar to the query.
+ """
+ query_embedding = self.embeddings.embed_query(query)
+ docs_with_score = self.similarity_search_with_score_by_vector(
+ query_embedding, k, search_options, **kwargs
+ )
+ return docs_with_score
+
+ def similarity_search_by_vector(
+ self,
+ embedding: List[float],
+ k: int = 4,
+ search_options: Optional[Dict[str, Any]] = {},
+ **kwargs: Any,
+ ) -> List[Document]:
+ """Return documents that are most similar to the vector embedding.
+
+ Args:
+ embedding (List[float]): Embedding to look up documents similar to.
+ k (int): Number of Documents to return.
+ Defaults to 4.
+ search_options (Optional[Dict[str, Any]]): Optional search options that are
+ passed to Couchbase search.
+ Defaults to empty dictionary.
+ fields (Optional[List[str]]): Optional list of fields to include in the
+ metadata of results. Note that these need to be stored in the index.
+ If nothing is specified, defaults to document text and metadata fields.
+
+ Returns:
+ List of Documents most similar to the query.
+ """
+ docs_with_score = self.similarity_search_with_score_by_vector(
+ embedding, k, search_options, **kwargs
+ )
+ return [doc for doc, _ in docs_with_score]
+
+ @classmethod
+ def _from_kwargs(
+ cls: Type[CouchbaseVectorStore],
+ embedding: Embeddings,
+ **kwargs: Any,
+ ) -> CouchbaseVectorStore:
+ """Initialize the Couchbase vector store from keyword arguments for the
+ vector store.
+
+ Args:
+ embedding: Embedding object to use to embed text.
+ **kwargs: Keyword arguments to initialize the vector store with.
+ Accepted arguments are:
+ - cluster
+ - bucket_name
+ - scope_name
+ - collection_name
+ - index_name
+ - text_key
+ - embedding_key
+ - scoped_index
+
+ """
+ cluster = kwargs.get("cluster", None)
+ bucket_name = kwargs.get("bucket_name", None)
+ scope_name = kwargs.get("scope_name", None)
+ collection_name = kwargs.get("collection_name", None)
+ index_name = kwargs.get("index_name", None)
+ text_key = kwargs.get("text_key", cls._default_text_key)
+ embedding_key = kwargs.get("embedding_key", cls._default_embedding_key)
+ scoped_index = kwargs.get("scoped_index", True)
+
+ return cls(
+ embedding=embedding,
+ cluster=cluster,
+ bucket_name=bucket_name,
+ scope_name=scope_name,
+ collection_name=collection_name,
+ index_name=index_name,
+ text_key=text_key,
+ embedding_key=embedding_key,
+ scoped_index=scoped_index,
+ )
+
+ @classmethod
+ def from_texts(
+ cls: Type[CouchbaseVectorStore],
+ texts: List[str],
+ embedding: Embeddings,
+ metadatas: Optional[List[dict]] = None,
+ **kwargs: Any,
+ ) -> CouchbaseVectorStore:
+ """Construct a Couchbase vector store from a list of texts.
+
+ Example:
+ .. code-block:: python
+
+ from langchain_couchbase import CouchbaseVectorStore
+ from langchain_openai import OpenAIEmbeddings
+
+ from couchbase.cluster import Cluster
+ from couchbase.auth import PasswordAuthenticator
+ from couchbase.options import ClusterOptions
+ from datetime import timedelta
+
+ auth = PasswordAuthenticator(username, password)
+ options = ClusterOptions(auth)
+ connect_string = "couchbases://localhost"
+ cluster = Cluster(connect_string, options)
+
+ # Wait until the cluster is ready for use.
+ cluster.wait_until_ready(timedelta(seconds=5))
+
+ embeddings = OpenAIEmbeddings()
+
+ texts = ["hello", "world"]
+
+ vectorstore = CouchbaseVectorStore.from_texts(
+ texts,
+ embedding=embeddings,
+ cluster=cluster,
+ bucket_name="",
+ scope_name="",
+ collection_name="",
+ index_name="vector-index",
+ )
+
+ Args:
+ texts (List[str]): list of texts to add to the vector store.
+ embedding (Embeddings): embedding function to use.
+ metadatas (optional[List[Dict]): list of metadatas to add to documents.
+ **kwargs: Keyword arguments used to initialize the vector store with and/or
+ passed to `add_texts` method. Check the constructor and/or `add_texts`
+ for the list of accepted arguments.
+
+ Returns:
+ A Couchbase vector store.
+
+ """
+ vector_store = cls._from_kwargs(embedding, **kwargs)
+ batch_size = kwargs.get("batch_size", vector_store.DEFAULT_BATCH_SIZE)
+ ids = kwargs.get("ids", None)
+ vector_store.add_texts(
+ texts, metadatas=metadatas, ids=ids, batch_size=batch_size
+ )
+
+ return vector_store
diff --git a/libs/partners/couchbase/poetry.lock b/libs/partners/couchbase/poetry.lock
new file mode 100644
index 0000000000000..5182aaf4c870c
--- /dev/null
+++ b/libs/partners/couchbase/poetry.lock
@@ -0,0 +1,771 @@
+# This file is automatically @generated by Poetry 1.7.1 and should not be changed by hand.
+
+[[package]]
+name = "annotated-types"
+version = "0.7.0"
+description = "Reusable constraint types to use with typing.Annotated"
+optional = false
+python-versions = ">=3.8"
+files = [
+ {file = "annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53"},
+ {file = "annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89"},
+]
+
+[package.dependencies]
+typing-extensions = {version = ">=4.0.0", markers = "python_version < \"3.9\""}
+
+[[package]]
+name = "certifi"
+version = "2024.2.2"
+description = "Python package for providing Mozilla's CA Bundle."
+optional = false
+python-versions = ">=3.6"
+files = [
+ {file = "certifi-2024.2.2-py3-none-any.whl", hash = "sha256:dc383c07b76109f368f6106eee2b593b04a011ea4d55f652c6ca24a754d1cdd1"},
+ {file = "certifi-2024.2.2.tar.gz", hash = "sha256:0569859f95fc761b18b45ef421b1290a0f65f147e92a1e5eb3e635f9a5e4e66f"},
+]
+
+[[package]]
+name = "charset-normalizer"
+version = "3.3.2"
+description = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet."
+optional = false
+python-versions = ">=3.7.0"
+files = [
+ {file = "charset-normalizer-3.3.2.tar.gz", hash = "sha256:f30c3cb33b24454a82faecaf01b19c18562b1e89558fb6c56de4d9118a032fd5"},
+ {file = "charset_normalizer-3.3.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:25baf083bf6f6b341f4121c2f3c548875ee6f5339300e08be3f2b2ba1721cdd3"},
+ {file = "charset_normalizer-3.3.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:06435b539f889b1f6f4ac1758871aae42dc3a8c0e24ac9e60c2384973ad73027"},
+ {file = "charset_normalizer-3.3.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9063e24fdb1e498ab71cb7419e24622516c4a04476b17a2dab57e8baa30d6e03"},
+ {file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6897af51655e3691ff853668779c7bad41579facacf5fd7253b0133308cf000d"},
+ {file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1d3193f4a680c64b4b6a9115943538edb896edc190f0b222e73761716519268e"},
+ {file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cd70574b12bb8a4d2aaa0094515df2463cb429d8536cfb6c7ce983246983e5a6"},
+ {file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8465322196c8b4d7ab6d1e049e4c5cb460d0394da4a27d23cc242fbf0034b6b5"},
+ {file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a9a8e9031d613fd2009c182b69c7b2c1ef8239a0efb1df3f7c8da66d5dd3d537"},
+ {file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:beb58fe5cdb101e3a055192ac291b7a21e3b7ef4f67fa1d74e331a7f2124341c"},
+ {file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:e06ed3eb3218bc64786f7db41917d4e686cc4856944f53d5bdf83a6884432e12"},
+ {file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:2e81c7b9c8979ce92ed306c249d46894776a909505d8f5a4ba55b14206e3222f"},
+ {file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:572c3763a264ba47b3cf708a44ce965d98555f618ca42c926a9c1616d8f34269"},
+ {file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:fd1abc0d89e30cc4e02e4064dc67fcc51bd941eb395c502aac3ec19fab46b519"},
+ {file = "charset_normalizer-3.3.2-cp310-cp310-win32.whl", hash = "sha256:3d47fa203a7bd9c5b6cee4736ee84ca03b8ef23193c0d1ca99b5089f72645c73"},
+ {file = "charset_normalizer-3.3.2-cp310-cp310-win_amd64.whl", hash = "sha256:10955842570876604d404661fbccbc9c7e684caf432c09c715ec38fbae45ae09"},
+ {file = "charset_normalizer-3.3.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:802fe99cca7457642125a8a88a084cef28ff0cf9407060f7b93dca5aa25480db"},
+ {file = "charset_normalizer-3.3.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:573f6eac48f4769d667c4442081b1794f52919e7edada77495aaed9236d13a96"},
+ {file = "charset_normalizer-3.3.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:549a3a73da901d5bc3ce8d24e0600d1fa85524c10287f6004fbab87672bf3e1e"},
+ {file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f27273b60488abe721a075bcca6d7f3964f9f6f067c8c4c605743023d7d3944f"},
+ {file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1ceae2f17a9c33cb48e3263960dc5fc8005351ee19db217e9b1bb15d28c02574"},
+ {file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:65f6f63034100ead094b8744b3b97965785388f308a64cf8d7c34f2f2e5be0c4"},
+ {file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:753f10e867343b4511128c6ed8c82f7bec3bd026875576dfd88483c5c73b2fd8"},
+ {file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4a78b2b446bd7c934f5dcedc588903fb2f5eec172f3d29e52a9096a43722adfc"},
+ {file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:e537484df0d8f426ce2afb2d0f8e1c3d0b114b83f8850e5f2fbea0e797bd82ae"},
+ {file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:eb6904c354526e758fda7167b33005998fb68c46fbc10e013ca97f21ca5c8887"},
+ {file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:deb6be0ac38ece9ba87dea880e438f25ca3eddfac8b002a2ec3d9183a454e8ae"},
+ {file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:4ab2fe47fae9e0f9dee8c04187ce5d09f48eabe611be8259444906793ab7cbce"},
+ {file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:80402cd6ee291dcb72644d6eac93785fe2c8b9cb30893c1af5b8fdd753b9d40f"},
+ {file = "charset_normalizer-3.3.2-cp311-cp311-win32.whl", hash = "sha256:7cd13a2e3ddeed6913a65e66e94b51d80a041145a026c27e6bb76c31a853c6ab"},
+ {file = "charset_normalizer-3.3.2-cp311-cp311-win_amd64.whl", hash = "sha256:663946639d296df6a2bb2aa51b60a2454ca1cb29835324c640dafb5ff2131a77"},
+ {file = "charset_normalizer-3.3.2-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:0b2b64d2bb6d3fb9112bafa732def486049e63de9618b5843bcdd081d8144cd8"},
+ {file = "charset_normalizer-3.3.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:ddbb2551d7e0102e7252db79ba445cdab71b26640817ab1e3e3648dad515003b"},
+ {file = "charset_normalizer-3.3.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:55086ee1064215781fff39a1af09518bc9255b50d6333f2e4c74ca09fac6a8f6"},
+ {file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8f4a014bc36d3c57402e2977dada34f9c12300af536839dc38c0beab8878f38a"},
+ {file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a10af20b82360ab00827f916a6058451b723b4e65030c5a18577c8b2de5b3389"},
+ {file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8d756e44e94489e49571086ef83b2bb8ce311e730092d2c34ca8f7d925cb20aa"},
+ {file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:90d558489962fd4918143277a773316e56c72da56ec7aa3dc3dbbe20fdfed15b"},
+ {file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6ac7ffc7ad6d040517be39eb591cac5ff87416c2537df6ba3cba3bae290c0fed"},
+ {file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:7ed9e526742851e8d5cc9e6cf41427dfc6068d4f5a3bb03659444b4cabf6bc26"},
+ {file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:8bdb58ff7ba23002a4c5808d608e4e6c687175724f54a5dade5fa8c67b604e4d"},
+ {file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_ppc64le.whl", hash = "sha256:6b3251890fff30ee142c44144871185dbe13b11bab478a88887a639655be1068"},
+ {file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_s390x.whl", hash = "sha256:b4a23f61ce87adf89be746c8a8974fe1c823c891d8f86eb218bb957c924bb143"},
+ {file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:efcb3f6676480691518c177e3b465bcddf57cea040302f9f4e6e191af91174d4"},
+ {file = "charset_normalizer-3.3.2-cp312-cp312-win32.whl", hash = "sha256:d965bba47ddeec8cd560687584e88cf699fd28f192ceb452d1d7ee807c5597b7"},
+ {file = "charset_normalizer-3.3.2-cp312-cp312-win_amd64.whl", hash = "sha256:96b02a3dc4381e5494fad39be677abcb5e6634bf7b4fa83a6dd3112607547001"},
+ {file = "charset_normalizer-3.3.2-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:95f2a5796329323b8f0512e09dbb7a1860c46a39da62ecb2324f116fa8fdc85c"},
+ {file = "charset_normalizer-3.3.2-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c002b4ffc0be611f0d9da932eb0f704fe2602a9a949d1f738e4c34c75b0863d5"},
+ {file = "charset_normalizer-3.3.2-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a981a536974bbc7a512cf44ed14938cf01030a99e9b3a06dd59578882f06f985"},
+ {file = "charset_normalizer-3.3.2-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3287761bc4ee9e33561a7e058c72ac0938c4f57fe49a09eae428fd88aafe7bb6"},
+ {file = "charset_normalizer-3.3.2-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:42cb296636fcc8b0644486d15c12376cb9fa75443e00fb25de0b8602e64c1714"},
+ {file = "charset_normalizer-3.3.2-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0a55554a2fa0d408816b3b5cedf0045f4b8e1a6065aec45849de2d6f3f8e9786"},
+ {file = "charset_normalizer-3.3.2-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:c083af607d2515612056a31f0a8d9e0fcb5876b7bfc0abad3ecd275bc4ebc2d5"},
+ {file = "charset_normalizer-3.3.2-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:87d1351268731db79e0f8e745d92493ee2841c974128ef629dc518b937d9194c"},
+ {file = "charset_normalizer-3.3.2-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:bd8f7df7d12c2db9fab40bdd87a7c09b1530128315d047a086fa3ae3435cb3a8"},
+ {file = "charset_normalizer-3.3.2-cp37-cp37m-musllinux_1_1_s390x.whl", hash = "sha256:c180f51afb394e165eafe4ac2936a14bee3eb10debc9d9e4db8958fe36afe711"},
+ {file = "charset_normalizer-3.3.2-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:8c622a5fe39a48f78944a87d4fb8a53ee07344641b0562c540d840748571b811"},
+ {file = "charset_normalizer-3.3.2-cp37-cp37m-win32.whl", hash = "sha256:db364eca23f876da6f9e16c9da0df51aa4f104a972735574842618b8c6d999d4"},
+ {file = "charset_normalizer-3.3.2-cp37-cp37m-win_amd64.whl", hash = "sha256:86216b5cee4b06df986d214f664305142d9c76df9b6512be2738aa72a2048f99"},
+ {file = "charset_normalizer-3.3.2-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:6463effa3186ea09411d50efc7d85360b38d5f09b870c48e4600f63af490e56a"},
+ {file = "charset_normalizer-3.3.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:6c4caeef8fa63d06bd437cd4bdcf3ffefe6738fb1b25951440d80dc7df8c03ac"},
+ {file = "charset_normalizer-3.3.2-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:37e55c8e51c236f95b033f6fb391d7d7970ba5fe7ff453dad675e88cf303377a"},
+ {file = "charset_normalizer-3.3.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fb69256e180cb6c8a894fee62b3afebae785babc1ee98b81cdf68bbca1987f33"},
+ {file = "charset_normalizer-3.3.2-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ae5f4161f18c61806f411a13b0310bea87f987c7d2ecdbdaad0e94eb2e404238"},
+ {file = "charset_normalizer-3.3.2-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b2b0a0c0517616b6869869f8c581d4eb2dd83a4d79e0ebcb7d373ef9956aeb0a"},
+ {file = "charset_normalizer-3.3.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:45485e01ff4d3630ec0d9617310448a8702f70e9c01906b0d0118bdf9d124cf2"},
+ {file = "charset_normalizer-3.3.2-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:eb00ed941194665c332bf8e078baf037d6c35d7c4f3102ea2d4f16ca94a26dc8"},
+ {file = "charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:2127566c664442652f024c837091890cb1942c30937add288223dc895793f898"},
+ {file = "charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:a50aebfa173e157099939b17f18600f72f84eed3049e743b68ad15bd69b6bf99"},
+ {file = "charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:4d0d1650369165a14e14e1e47b372cfcb31d6ab44e6e33cb2d4e57265290044d"},
+ {file = "charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:923c0c831b7cfcb071580d3f46c4baf50f174be571576556269530f4bbd79d04"},
+ {file = "charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:06a81e93cd441c56a9b65d8e1d043daeb97a3d0856d177d5c90ba85acb3db087"},
+ {file = "charset_normalizer-3.3.2-cp38-cp38-win32.whl", hash = "sha256:6ef1d82a3af9d3eecdba2321dc1b3c238245d890843e040e41e470ffa64c3e25"},
+ {file = "charset_normalizer-3.3.2-cp38-cp38-win_amd64.whl", hash = "sha256:eb8821e09e916165e160797a6c17edda0679379a4be5c716c260e836e122f54b"},
+ {file = "charset_normalizer-3.3.2-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:c235ebd9baae02f1b77bcea61bce332cb4331dc3617d254df3323aa01ab47bd4"},
+ {file = "charset_normalizer-3.3.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:5b4c145409bef602a690e7cfad0a15a55c13320ff7a3ad7ca59c13bb8ba4d45d"},
+ {file = "charset_normalizer-3.3.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:68d1f8a9e9e37c1223b656399be5d6b448dea850bed7d0f87a8311f1ff3dabb0"},
+ {file = "charset_normalizer-3.3.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:22afcb9f253dac0696b5a4be4a1c0f8762f8239e21b99680099abd9b2b1b2269"},
+ {file = "charset_normalizer-3.3.2-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e27ad930a842b4c5eb8ac0016b0a54f5aebbe679340c26101df33424142c143c"},
+ {file = "charset_normalizer-3.3.2-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1f79682fbe303db92bc2b1136016a38a42e835d932bab5b3b1bfcfbf0640e519"},
+ {file = "charset_normalizer-3.3.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b261ccdec7821281dade748d088bb6e9b69e6d15b30652b74cbbac25e280b796"},
+ {file = "charset_normalizer-3.3.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:122c7fa62b130ed55f8f285bfd56d5f4b4a5b503609d181f9ad85e55c89f4185"},
+ {file = "charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:d0eccceffcb53201b5bfebb52600a5fb483a20b61da9dbc885f8b103cbe7598c"},
+ {file = "charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:9f96df6923e21816da7e0ad3fd47dd8f94b2a5ce594e00677c0013018b813458"},
+ {file = "charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:7f04c839ed0b6b98b1a7501a002144b76c18fb1c1850c8b98d458ac269e26ed2"},
+ {file = "charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:34d1c8da1e78d2e001f363791c98a272bb734000fcef47a491c1e3b0505657a8"},
+ {file = "charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:ff8fa367d09b717b2a17a052544193ad76cd49979c805768879cb63d9ca50561"},
+ {file = "charset_normalizer-3.3.2-cp39-cp39-win32.whl", hash = "sha256:aed38f6e4fb3f5d6bf81bfa990a07806be9d83cf7bacef998ab1a9bd660a581f"},
+ {file = "charset_normalizer-3.3.2-cp39-cp39-win_amd64.whl", hash = "sha256:b01b88d45a6fcb69667cd6d2f7a9aeb4bf53760d7fc536bf679ec94fe9f3ff3d"},
+ {file = "charset_normalizer-3.3.2-py3-none-any.whl", hash = "sha256:3e4d1f6587322d2788836a99c69062fbb091331ec940e02d12d179c1d53e25fc"},
+]
+
+[[package]]
+name = "codespell"
+version = "2.2.6"
+description = "Codespell"
+optional = false
+python-versions = ">=3.8"
+files = [
+ {file = "codespell-2.2.6-py3-none-any.whl", hash = "sha256:9ee9a3e5df0990604013ac2a9f22fa8e57669c827124a2e961fe8a1da4cacc07"},
+ {file = "codespell-2.2.6.tar.gz", hash = "sha256:a8c65d8eb3faa03deabab6b3bbe798bea72e1799c7e9e955d57eca4096abcff9"},
+]
+
+[package.extras]
+dev = ["Pygments", "build", "chardet", "pre-commit", "pytest", "pytest-cov", "pytest-dependency", "ruff", "tomli", "twine"]
+hard-encoding-detection = ["chardet"]
+toml = ["tomli"]
+types = ["chardet (>=5.1.0)", "mypy", "pytest", "pytest-cov", "pytest-dependency"]
+
+[[package]]
+name = "colorama"
+version = "0.4.6"
+description = "Cross-platform colored terminal text."
+optional = false
+python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7"
+files = [
+ {file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"},
+ {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"},
+]
+
+[[package]]
+name = "couchbase"
+version = "4.2.1"
+description = "Python Client for Couchbase"
+optional = false
+python-versions = ">=3.7"
+files = [
+ {file = "couchbase-4.2.1-cp310-cp310-macosx_10_15_x86_64.whl", hash = "sha256:7ad4c4462879f456a9067ac1788e62d852509439bac3538b9bc459a754666481"},
+ {file = "couchbase-4.2.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:06d91891c599ba0f5052e594ac025a2ca6ab7885e528b854ac9c125df7c74146"},
+ {file = "couchbase-4.2.1-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:0191d4a631ead533551cb9a214704ad5f3dfff2029e21a23b57725a0b5666b25"},
+ {file = "couchbase-4.2.1-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:b206790d6834a18c5e457f9a70f44774f476f3acccf9f22e8c1b5283a5bd03fa"},
+ {file = "couchbase-4.2.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:c5ca571b9ce017ecbd447de12cd46e213f93e0664bec6fca0a06e1768db1a4f8"},
+ {file = "couchbase-4.2.1-cp310-cp310-win_amd64.whl", hash = "sha256:675c615cfd4b04e73e94cf03c786da5105d94527f5c3a087813dba477a1379e9"},
+ {file = "couchbase-4.2.1-cp311-cp311-macosx_10_15_x86_64.whl", hash = "sha256:4cd09eedf162dc28386d9c6490e832c25068406c0f5d70a0417c0b1445394651"},
+ {file = "couchbase-4.2.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:dfebb11551c6d947ce6297ab02b5006b1ac8739dda3e10d41896db0dc8672915"},
+ {file = "couchbase-4.2.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:39e742ccfe90a0e59e6e1b0e12f0fe224a736c0207b218ef48048052f926e1c6"},
+ {file = "couchbase-4.2.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:f9ba24efddf47f30603275f5433434d8759a55233c78b3e4bc613c502ac429e9"},
+ {file = "couchbase-4.2.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:adfca3929f07fb4385dc52f08d3a60634012f364b176f95ab023cdd1bb7fe9c0"},
+ {file = "couchbase-4.2.1-cp311-cp311-win_amd64.whl", hash = "sha256:e1c68b28c6f0475961afb9fe626ad2bac8a5643b53f719675386f060db4b6e19"},
+ {file = "couchbase-4.2.1-cp38-cp38-macosx_10_15_x86_64.whl", hash = "sha256:137512462426cd495954c1815d78115d109308a4d9f8843b638285104388a359"},
+ {file = "couchbase-4.2.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:5987e5edcce7696e5f75b35be91f44fa69fb5eb95dba0957ad66f789affcdb36"},
+ {file = "couchbase-4.2.1-cp38-cp38-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:080cb0fc333bd4a641ede4ee14ff0c7dbe95067fbb280826ea546681e0b9f9e3"},
+ {file = "couchbase-4.2.1-cp38-cp38-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:e317c2628a4a917083e8e7ce8e2662432b6a12ebac65fc00de6da2b37ab5975c"},
+ {file = "couchbase-4.2.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:de7f8699ae344e2e96706ee0eac67e96bfdd3412fb18dcfb81d8ba5837dd3dfb"},
+ {file = "couchbase-4.2.1-cp38-cp38-win_amd64.whl", hash = "sha256:82b9deb8b1fe8e8d7dde9c232ac5f4c11ff0f067930837af0e7769706e6a9453"},
+ {file = "couchbase-4.2.1-cp39-cp39-macosx_10_15_x86_64.whl", hash = "sha256:44502d069ea17a8d692b7c88d84bc0df2cf4e944cde337c8eb3175bc0b835bb9"},
+ {file = "couchbase-4.2.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:c0f131b816a7d91b755232872ba10f6d6ca5a715e595ee9534478bc97a518ae8"},
+ {file = "couchbase-4.2.1-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:e9b9deb312bbe5f9a8e63828f9de877714c4b09b7d88f7dc87b60e5ffb2a13e6"},
+ {file = "couchbase-4.2.1-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:71e8da251850d795975c3569c01d35ba1a556825dc7d9549ff9918d148255804"},
+ {file = "couchbase-4.2.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:d04492144ce520c612a2f8f265278c9f0cdf62fdd6f703e7a3210a7476b228f6"},
+ {file = "couchbase-4.2.1-cp39-cp39-win_amd64.whl", hash = "sha256:3f91b7699ea7b8253cf34c9fb6e191de9b2edfd7aa4d6f97b29c10b9a1670444"},
+ {file = "couchbase-4.2.1.tar.gz", hash = "sha256:dc1c60d3f2fc179db8225aac4cc30d601d73cf2535aaf023d607e86be2d7dd78"},
+]
+
+[[package]]
+name = "exceptiongroup"
+version = "1.2.1"
+description = "Backport of PEP 654 (exception groups)"
+optional = false
+python-versions = ">=3.7"
+files = [
+ {file = "exceptiongroup-1.2.1-py3-none-any.whl", hash = "sha256:5258b9ed329c5bbdd31a309f53cbfb0b155341807f6ff7606a1e801a891b29ad"},
+ {file = "exceptiongroup-1.2.1.tar.gz", hash = "sha256:a4785e48b045528f5bfe627b6ad554ff32def154f42372786903b7abcfe1aa16"},
+]
+
+[package.extras]
+test = ["pytest (>=6)"]
+
+[[package]]
+name = "idna"
+version = "3.7"
+description = "Internationalized Domain Names in Applications (IDNA)"
+optional = false
+python-versions = ">=3.5"
+files = [
+ {file = "idna-3.7-py3-none-any.whl", hash = "sha256:82fee1fc78add43492d3a1898bfa6d8a904cc97d8427f683ed8e798d07761aa0"},
+ {file = "idna-3.7.tar.gz", hash = "sha256:028ff3aadf0609c1fd278d8ea3089299412a7a8b9bd005dd08b9f8285bcb5cfc"},
+]
+
+[[package]]
+name = "iniconfig"
+version = "2.0.0"
+description = "brain-dead simple config-ini parsing"
+optional = false
+python-versions = ">=3.7"
+files = [
+ {file = "iniconfig-2.0.0-py3-none-any.whl", hash = "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374"},
+ {file = "iniconfig-2.0.0.tar.gz", hash = "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3"},
+]
+
+[[package]]
+name = "jsonpatch"
+version = "1.33"
+description = "Apply JSON-Patches (RFC 6902)"
+optional = false
+python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, !=3.5.*, !=3.6.*"
+files = [
+ {file = "jsonpatch-1.33-py2.py3-none-any.whl", hash = "sha256:0ae28c0cd062bbd8b8ecc26d7d164fbbea9652a1a3693f3b956c1eae5145dade"},
+ {file = "jsonpatch-1.33.tar.gz", hash = "sha256:9fcd4009c41e6d12348b4a0ff2563ba56a2923a7dfee731d004e212e1ee5030c"},
+]
+
+[package.dependencies]
+jsonpointer = ">=1.9"
+
+[[package]]
+name = "jsonpointer"
+version = "2.4"
+description = "Identify specific nodes in a JSON document (RFC 6901)"
+optional = false
+python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, !=3.5.*, !=3.6.*"
+files = [
+ {file = "jsonpointer-2.4-py2.py3-none-any.whl", hash = "sha256:15d51bba20eea3165644553647711d150376234112651b4f1811022aecad7d7a"},
+ {file = "jsonpointer-2.4.tar.gz", hash = "sha256:585cee82b70211fa9e6043b7bb89db6e1aa49524340dde8ad6b63206ea689d88"},
+]
+
+[[package]]
+name = "langchain-core"
+version = "0.2.1"
+description = "Building applications with LLMs through composability"
+optional = false
+python-versions = ">=3.8.1,<4.0"
+files = []
+develop = true
+
+[package.dependencies]
+jsonpatch = "^1.33"
+langsmith = "^0.1.0"
+packaging = "^23.2"
+pydantic = ">=1,<3"
+PyYAML = ">=5.3"
+tenacity = "^8.1.0"
+
+[package.extras]
+extended-testing = ["jinja2 (>=3,<4)"]
+
+[package.source]
+type = "directory"
+url = "../../core"
+
+[[package]]
+name = "langsmith"
+version = "0.1.62"
+description = "Client library to connect to the LangSmith LLM Tracing and Evaluation Platform."
+optional = false
+python-versions = "<4.0,>=3.8.1"
+files = [
+ {file = "langsmith-0.1.62-py3-none-any.whl", hash = "sha256:3a9f112643f64d736b8c875390c750fe6485804ea53aeae4edebce0afa4383a5"},
+ {file = "langsmith-0.1.62.tar.gz", hash = "sha256:7ef894c14e6d4175fce88ec3bcd5a9c8cf9a456ea77e26e361f519ad082f34a8"},
+]
+
+[package.dependencies]
+orjson = ">=3.9.14,<4.0.0"
+pydantic = ">=1,<3"
+requests = ">=2,<3"
+
+[[package]]
+name = "mypy"
+version = "1.10.0"
+description = "Optional static typing for Python"
+optional = false
+python-versions = ">=3.8"
+files = [
+ {file = "mypy-1.10.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:da1cbf08fb3b851ab3b9523a884c232774008267b1f83371ace57f412fe308c2"},
+ {file = "mypy-1.10.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:12b6bfc1b1a66095ab413160a6e520e1dc076a28f3e22f7fb25ba3b000b4ef99"},
+ {file = "mypy-1.10.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9e36fb078cce9904c7989b9693e41cb9711e0600139ce3970c6ef814b6ebc2b2"},
+ {file = "mypy-1.10.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:2b0695d605ddcd3eb2f736cd8b4e388288c21e7de85001e9f85df9187f2b50f9"},
+ {file = "mypy-1.10.0-cp310-cp310-win_amd64.whl", hash = "sha256:cd777b780312ddb135bceb9bc8722a73ec95e042f911cc279e2ec3c667076051"},
+ {file = "mypy-1.10.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:3be66771aa5c97602f382230165b856c231d1277c511c9a8dd058be4784472e1"},
+ {file = "mypy-1.10.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:8b2cbaca148d0754a54d44121b5825ae71868c7592a53b7292eeb0f3fdae95ee"},
+ {file = "mypy-1.10.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1ec404a7cbe9fc0e92cb0e67f55ce0c025014e26d33e54d9e506a0f2d07fe5de"},
+ {file = "mypy-1.10.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:e22e1527dc3d4aa94311d246b59e47f6455b8729f4968765ac1eacf9a4760bc7"},
+ {file = "mypy-1.10.0-cp311-cp311-win_amd64.whl", hash = "sha256:a87dbfa85971e8d59c9cc1fcf534efe664d8949e4c0b6b44e8ca548e746a8d53"},
+ {file = "mypy-1.10.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:a781f6ad4bab20eef8b65174a57e5203f4be627b46291f4589879bf4e257b97b"},
+ {file = "mypy-1.10.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:b808e12113505b97d9023b0b5e0c0705a90571c6feefc6f215c1df9381256e30"},
+ {file = "mypy-1.10.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8f55583b12156c399dce2df7d16f8a5095291354f1e839c252ec6c0611e86e2e"},
+ {file = "mypy-1.10.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:4cf18f9d0efa1b16478c4c129eabec36148032575391095f73cae2e722fcf9d5"},
+ {file = "mypy-1.10.0-cp312-cp312-win_amd64.whl", hash = "sha256:bc6ac273b23c6b82da3bb25f4136c4fd42665f17f2cd850771cb600bdd2ebeda"},
+ {file = "mypy-1.10.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:9fd50226364cd2737351c79807775136b0abe084433b55b2e29181a4c3c878c0"},
+ {file = "mypy-1.10.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:f90cff89eea89273727d8783fef5d4a934be2fdca11b47def50cf5d311aff727"},
+ {file = "mypy-1.10.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fcfc70599efde5c67862a07a1aaf50e55bce629ace26bb19dc17cece5dd31ca4"},
+ {file = "mypy-1.10.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:075cbf81f3e134eadaf247de187bd604748171d6b79736fa9b6c9685b4083061"},
+ {file = "mypy-1.10.0-cp38-cp38-win_amd64.whl", hash = "sha256:3f298531bca95ff615b6e9f2fc0333aae27fa48052903a0ac90215021cdcfa4f"},
+ {file = "mypy-1.10.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:fa7ef5244615a2523b56c034becde4e9e3f9b034854c93639adb667ec9ec2976"},
+ {file = "mypy-1.10.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:3236a4c8f535a0631f85f5fcdffba71c7feeef76a6002fcba7c1a8e57c8be1ec"},
+ {file = "mypy-1.10.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4a2b5cdbb5dd35aa08ea9114436e0d79aceb2f38e32c21684dcf8e24e1e92821"},
+ {file = "mypy-1.10.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:92f93b21c0fe73dc00abf91022234c79d793318b8a96faac147cd579c1671746"},
+ {file = "mypy-1.10.0-cp39-cp39-win_amd64.whl", hash = "sha256:28d0e038361b45f099cc086d9dd99c15ff14d0188f44ac883010e172ce86c38a"},
+ {file = "mypy-1.10.0-py3-none-any.whl", hash = "sha256:f8c083976eb530019175aabadb60921e73b4f45736760826aa1689dda8208aee"},
+ {file = "mypy-1.10.0.tar.gz", hash = "sha256:3d087fcbec056c4ee34974da493a826ce316947485cef3901f511848e687c131"},
+]
+
+[package.dependencies]
+mypy-extensions = ">=1.0.0"
+tomli = {version = ">=1.1.0", markers = "python_version < \"3.11\""}
+typing-extensions = ">=4.1.0"
+
+[package.extras]
+dmypy = ["psutil (>=4.0)"]
+install-types = ["pip"]
+mypyc = ["setuptools (>=50)"]
+reports = ["lxml"]
+
+[[package]]
+name = "mypy-extensions"
+version = "1.0.0"
+description = "Type system extensions for programs checked with the mypy type checker."
+optional = false
+python-versions = ">=3.5"
+files = [
+ {file = "mypy_extensions-1.0.0-py3-none-any.whl", hash = "sha256:4392f6c0eb8a5668a69e23d168ffa70f0be9ccfd32b5cc2d26a34ae5b844552d"},
+ {file = "mypy_extensions-1.0.0.tar.gz", hash = "sha256:75dbf8955dc00442a438fc4d0666508a9a97b6bd41aa2f0ffe9d2f2725af0782"},
+]
+
+[[package]]
+name = "orjson"
+version = "3.10.3"
+description = "Fast, correct Python JSON library supporting dataclasses, datetimes, and numpy"
+optional = false
+python-versions = ">=3.8"
+files = [
+ {file = "orjson-3.10.3-cp310-cp310-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:9fb6c3f9f5490a3eb4ddd46fc1b6eadb0d6fc16fb3f07320149c3286a1409dd8"},
+ {file = "orjson-3.10.3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:252124b198662eee80428f1af8c63f7ff077c88723fe206a25df8dc57a57b1fa"},
+ {file = "orjson-3.10.3-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9f3e87733823089a338ef9bbf363ef4de45e5c599a9bf50a7a9b82e86d0228da"},
+ {file = "orjson-3.10.3-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c8334c0d87103bb9fbbe59b78129f1f40d1d1e8355bbed2ca71853af15fa4ed3"},
+ {file = "orjson-3.10.3-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1952c03439e4dce23482ac846e7961f9d4ec62086eb98ae76d97bd41d72644d7"},
+ {file = "orjson-3.10.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:c0403ed9c706dcd2809f1600ed18f4aae50be263bd7112e54b50e2c2bc3ebd6d"},
+ {file = "orjson-3.10.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:382e52aa4270a037d41f325e7d1dfa395b7de0c367800b6f337d8157367bf3a7"},
+ {file = "orjson-3.10.3-cp310-none-win32.whl", hash = "sha256:be2aab54313752c04f2cbaab4515291ef5af8c2256ce22abc007f89f42f49109"},
+ {file = "orjson-3.10.3-cp310-none-win_amd64.whl", hash = "sha256:416b195f78ae461601893f482287cee1e3059ec49b4f99479aedf22a20b1098b"},
+ {file = "orjson-3.10.3-cp311-cp311-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:73100d9abbbe730331f2242c1fc0bcb46a3ea3b4ae3348847e5a141265479700"},
+ {file = "orjson-3.10.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:544a12eee96e3ab828dbfcb4d5a0023aa971b27143a1d35dc214c176fdfb29b3"},
+ {file = "orjson-3.10.3-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:520de5e2ef0b4ae546bea25129d6c7c74edb43fc6cf5213f511a927f2b28148b"},
+ {file = "orjson-3.10.3-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ccaa0a401fc02e8828a5bedfd80f8cd389d24f65e5ca3954d72c6582495b4bcf"},
+ {file = "orjson-3.10.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9a7bc9e8bc11bac40f905640acd41cbeaa87209e7e1f57ade386da658092dc16"},
+ {file = "orjson-3.10.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:3582b34b70543a1ed6944aca75e219e1192661a63da4d039d088a09c67543b08"},
+ {file = "orjson-3.10.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:1c23dfa91481de880890d17aa7b91d586a4746a4c2aa9a145bebdbaf233768d5"},
+ {file = "orjson-3.10.3-cp311-none-win32.whl", hash = "sha256:1770e2a0eae728b050705206d84eda8b074b65ee835e7f85c919f5705b006c9b"},
+ {file = "orjson-3.10.3-cp311-none-win_amd64.whl", hash = "sha256:93433b3c1f852660eb5abdc1f4dd0ced2be031ba30900433223b28ee0140cde5"},
+ {file = "orjson-3.10.3-cp312-cp312-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:a39aa73e53bec8d410875683bfa3a8edf61e5a1c7bb4014f65f81d36467ea098"},
+ {file = "orjson-3.10.3-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0943a96b3fa09bee1afdfccc2cb236c9c64715afa375b2af296c73d91c23eab2"},
+ {file = "orjson-3.10.3-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e852baafceff8da3c9defae29414cc8513a1586ad93e45f27b89a639c68e8176"},
+ {file = "orjson-3.10.3-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:18566beb5acd76f3769c1d1a7ec06cdb81edc4d55d2765fb677e3eaa10fa99e0"},
+ {file = "orjson-3.10.3-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1bd2218d5a3aa43060efe649ec564ebedec8ce6ae0a43654b81376216d5ebd42"},
+ {file = "orjson-3.10.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:cf20465e74c6e17a104ecf01bf8cd3b7b252565b4ccee4548f18b012ff2f8069"},
+ {file = "orjson-3.10.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:ba7f67aa7f983c4345eeda16054a4677289011a478ca947cd69c0a86ea45e534"},
+ {file = "orjson-3.10.3-cp312-none-win32.whl", hash = "sha256:17e0713fc159abc261eea0f4feda611d32eabc35708b74bef6ad44f6c78d5ea0"},
+ {file = "orjson-3.10.3-cp312-none-win_amd64.whl", hash = "sha256:4c895383b1ec42b017dd2c75ae8a5b862fc489006afde06f14afbdd0309b2af0"},
+ {file = "orjson-3.10.3-cp38-cp38-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:be2719e5041e9fb76c8c2c06b9600fe8e8584e6980061ff88dcbc2691a16d20d"},
+ {file = "orjson-3.10.3-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cb0175a5798bdc878956099f5c54b9837cb62cfbf5d0b86ba6d77e43861bcec2"},
+ {file = "orjson-3.10.3-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:978be58a68ade24f1af7758626806e13cff7748a677faf95fbb298359aa1e20d"},
+ {file = "orjson-3.10.3-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:16bda83b5c61586f6f788333d3cf3ed19015e3b9019188c56983b5a299210eb5"},
+ {file = "orjson-3.10.3-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4ad1f26bea425041e0a1adad34630c4825a9e3adec49079b1fb6ac8d36f8b754"},
+ {file = "orjson-3.10.3-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:9e253498bee561fe85d6325ba55ff2ff08fb5e7184cd6a4d7754133bd19c9195"},
+ {file = "orjson-3.10.3-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:0a62f9968bab8a676a164263e485f30a0b748255ee2f4ae49a0224be95f4532b"},
+ {file = "orjson-3.10.3-cp38-none-win32.whl", hash = "sha256:8d0b84403d287d4bfa9bf7d1dc298d5c1c5d9f444f3737929a66f2fe4fb8f134"},
+ {file = "orjson-3.10.3-cp38-none-win_amd64.whl", hash = "sha256:8bc7a4df90da5d535e18157220d7915780d07198b54f4de0110eca6b6c11e290"},
+ {file = "orjson-3.10.3-cp39-cp39-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:9059d15c30e675a58fdcd6f95465c1522b8426e092de9fff20edebfdc15e1cb0"},
+ {file = "orjson-3.10.3-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8d40c7f7938c9c2b934b297412c067936d0b54e4b8ab916fd1a9eb8f54c02294"},
+ {file = "orjson-3.10.3-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d4a654ec1de8fdaae1d80d55cee65893cb06494e124681ab335218be6a0691e7"},
+ {file = "orjson-3.10.3-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:831c6ef73f9aa53c5f40ae8f949ff7681b38eaddb6904aab89dca4d85099cb78"},
+ {file = "orjson-3.10.3-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:99b880d7e34542db89f48d14ddecbd26f06838b12427d5a25d71baceb5ba119d"},
+ {file = "orjson-3.10.3-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:2e5e176c994ce4bd434d7aafb9ecc893c15f347d3d2bbd8e7ce0b63071c52e25"},
+ {file = "orjson-3.10.3-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:b69a58a37dab856491bf2d3bbf259775fdce262b727f96aafbda359cb1d114d8"},
+ {file = "orjson-3.10.3-cp39-none-win32.whl", hash = "sha256:b8d4d1a6868cde356f1402c8faeb50d62cee765a1f7ffcfd6de732ab0581e063"},
+ {file = "orjson-3.10.3-cp39-none-win_amd64.whl", hash = "sha256:5102f50c5fc46d94f2033fe00d392588564378260d64377aec702f21a7a22912"},
+ {file = "orjson-3.10.3.tar.gz", hash = "sha256:2b166507acae7ba2f7c315dcf185a9111ad5e992ac81f2d507aac39193c2c818"},
+]
+
+[[package]]
+name = "packaging"
+version = "23.2"
+description = "Core utilities for Python packages"
+optional = false
+python-versions = ">=3.7"
+files = [
+ {file = "packaging-23.2-py3-none-any.whl", hash = "sha256:8c491190033a9af7e1d931d0b5dacc2ef47509b34dd0de67ed209b5203fc88c7"},
+ {file = "packaging-23.2.tar.gz", hash = "sha256:048fb0e9405036518eaaf48a55953c750c11e1a1b68e0dd1a9d62ed0c092cfc5"},
+]
+
+[[package]]
+name = "pluggy"
+version = "1.5.0"
+description = "plugin and hook calling mechanisms for python"
+optional = false
+python-versions = ">=3.8"
+files = [
+ {file = "pluggy-1.5.0-py3-none-any.whl", hash = "sha256:44e1ad92c8ca002de6377e165f3e0f1be63266ab4d554740532335b9d75ea669"},
+ {file = "pluggy-1.5.0.tar.gz", hash = "sha256:2cffa88e94fdc978c4c574f15f9e59b7f4201d439195c3715ca9e2486f1d0cf1"},
+]
+
+[package.extras]
+dev = ["pre-commit", "tox"]
+testing = ["pytest", "pytest-benchmark"]
+
+[[package]]
+name = "pydantic"
+version = "2.7.1"
+description = "Data validation using Python type hints"
+optional = false
+python-versions = ">=3.8"
+files = [
+ {file = "pydantic-2.7.1-py3-none-any.whl", hash = "sha256:e029badca45266732a9a79898a15ae2e8b14840b1eabbb25844be28f0b33f3d5"},
+ {file = "pydantic-2.7.1.tar.gz", hash = "sha256:e9dbb5eada8abe4d9ae5f46b9939aead650cd2b68f249bb3a8139dbe125803cc"},
+]
+
+[package.dependencies]
+annotated-types = ">=0.4.0"
+pydantic-core = "2.18.2"
+typing-extensions = ">=4.6.1"
+
+[package.extras]
+email = ["email-validator (>=2.0.0)"]
+
+[[package]]
+name = "pydantic-core"
+version = "2.18.2"
+description = "Core functionality for Pydantic validation and serialization"
+optional = false
+python-versions = ">=3.8"
+files = [
+ {file = "pydantic_core-2.18.2-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:9e08e867b306f525802df7cd16c44ff5ebbe747ff0ca6cf3fde7f36c05a59a81"},
+ {file = "pydantic_core-2.18.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:f0a21cbaa69900cbe1a2e7cad2aa74ac3cf21b10c3efb0fa0b80305274c0e8a2"},
+ {file = "pydantic_core-2.18.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0680b1f1f11fda801397de52c36ce38ef1c1dc841a0927a94f226dea29c3ae3d"},
+ {file = "pydantic_core-2.18.2-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:95b9d5e72481d3780ba3442eac863eae92ae43a5f3adb5b4d0a1de89d42bb250"},
+ {file = "pydantic_core-2.18.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c4fcf5cd9c4b655ad666ca332b9a081112cd7a58a8b5a6ca7a3104bc950f2038"},
+ {file = "pydantic_core-2.18.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9b5155ff768083cb1d62f3e143b49a8a3432e6789a3abee8acd005c3c7af1c74"},
+ {file = "pydantic_core-2.18.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:553ef617b6836fc7e4df130bb851e32fe357ce36336d897fd6646d6058d980af"},
+ {file = "pydantic_core-2.18.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:b89ed9eb7d616ef5714e5590e6cf7f23b02d0d539767d33561e3675d6f9e3857"},
+ {file = "pydantic_core-2.18.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:75f7e9488238e920ab6204399ded280dc4c307d034f3924cd7f90a38b1829563"},
+ {file = "pydantic_core-2.18.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:ef26c9e94a8c04a1b2924149a9cb081836913818e55681722d7f29af88fe7b38"},
+ {file = "pydantic_core-2.18.2-cp310-none-win32.whl", hash = "sha256:182245ff6b0039e82b6bb585ed55a64d7c81c560715d1bad0cbad6dfa07b4027"},
+ {file = "pydantic_core-2.18.2-cp310-none-win_amd64.whl", hash = "sha256:e23ec367a948b6d812301afc1b13f8094ab7b2c280af66ef450efc357d2ae543"},
+ {file = "pydantic_core-2.18.2-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:219da3f096d50a157f33645a1cf31c0ad1fe829a92181dd1311022f986e5fbe3"},
+ {file = "pydantic_core-2.18.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:cc1cfd88a64e012b74e94cd00bbe0f9c6df57049c97f02bb07d39e9c852e19a4"},
+ {file = "pydantic_core-2.18.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:05b7133a6e6aeb8df37d6f413f7705a37ab4031597f64ab56384c94d98fa0e90"},
+ {file = "pydantic_core-2.18.2-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:224c421235f6102e8737032483f43c1a8cfb1d2f45740c44166219599358c2cd"},
+ {file = "pydantic_core-2.18.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b14d82cdb934e99dda6d9d60dc84a24379820176cc4a0d123f88df319ae9c150"},
+ {file = "pydantic_core-2.18.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2728b01246a3bba6de144f9e3115b532ee44bd6cf39795194fb75491824a1413"},
+ {file = "pydantic_core-2.18.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:470b94480bb5ee929f5acba6995251ada5e059a5ef3e0dfc63cca287283ebfa6"},
+ {file = "pydantic_core-2.18.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:997abc4df705d1295a42f95b4eec4950a37ad8ae46d913caeee117b6b198811c"},
+ {file = "pydantic_core-2.18.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:75250dbc5290e3f1a0f4618db35e51a165186f9034eff158f3d490b3fed9f8a0"},
+ {file = "pydantic_core-2.18.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:4456f2dca97c425231d7315737d45239b2b51a50dc2b6f0c2bb181fce6207664"},
+ {file = "pydantic_core-2.18.2-cp311-none-win32.whl", hash = "sha256:269322dcc3d8bdb69f054681edff86276b2ff972447863cf34c8b860f5188e2e"},
+ {file = "pydantic_core-2.18.2-cp311-none-win_amd64.whl", hash = "sha256:800d60565aec896f25bc3cfa56d2277d52d5182af08162f7954f938c06dc4ee3"},
+ {file = "pydantic_core-2.18.2-cp311-none-win_arm64.whl", hash = "sha256:1404c69d6a676245199767ba4f633cce5f4ad4181f9d0ccb0577e1f66cf4c46d"},
+ {file = "pydantic_core-2.18.2-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:fb2bd7be70c0fe4dfd32c951bc813d9fe6ebcbfdd15a07527796c8204bd36242"},
+ {file = "pydantic_core-2.18.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:6132dd3bd52838acddca05a72aafb6eab6536aa145e923bb50f45e78b7251043"},
+ {file = "pydantic_core-2.18.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d7d904828195733c183d20a54230c0df0eb46ec746ea1a666730787353e87182"},
+ {file = "pydantic_core-2.18.2-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:c9bd70772c720142be1020eac55f8143a34ec9f82d75a8e7a07852023e46617f"},
+ {file = "pydantic_core-2.18.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:2b8ed04b3582771764538f7ee7001b02e1170223cf9b75dff0bc698fadb00cf3"},
+ {file = "pydantic_core-2.18.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e6dac87ddb34aaec85f873d737e9d06a3555a1cc1a8e0c44b7f8d5daeb89d86f"},
+ {file = "pydantic_core-2.18.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7ca4ae5a27ad7a4ee5170aebce1574b375de390bc01284f87b18d43a3984df72"},
+ {file = "pydantic_core-2.18.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:886eec03591b7cf058467a70a87733b35f44707bd86cf64a615584fd72488b7c"},
+ {file = "pydantic_core-2.18.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:ca7b0c1f1c983e064caa85f3792dd2fe3526b3505378874afa84baf662e12241"},
+ {file = "pydantic_core-2.18.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:4b4356d3538c3649337df4074e81b85f0616b79731fe22dd11b99499b2ebbdf3"},
+ {file = "pydantic_core-2.18.2-cp312-none-win32.whl", hash = "sha256:8b172601454f2d7701121bbec3425dd71efcb787a027edf49724c9cefc14c038"},
+ {file = "pydantic_core-2.18.2-cp312-none-win_amd64.whl", hash = "sha256:b1bd7e47b1558ea872bd16c8502c414f9e90dcf12f1395129d7bb42a09a95438"},
+ {file = "pydantic_core-2.18.2-cp312-none-win_arm64.whl", hash = "sha256:98758d627ff397e752bc339272c14c98199c613f922d4a384ddc07526c86a2ec"},
+ {file = "pydantic_core-2.18.2-cp38-cp38-macosx_10_12_x86_64.whl", hash = "sha256:9fdad8e35f278b2c3eb77cbdc5c0a49dada440657bf738d6905ce106dc1de439"},
+ {file = "pydantic_core-2.18.2-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:1d90c3265ae107f91a4f279f4d6f6f1d4907ac76c6868b27dc7fb33688cfb347"},
+ {file = "pydantic_core-2.18.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:390193c770399861d8df9670fb0d1874f330c79caaca4642332df7c682bf6b91"},
+ {file = "pydantic_core-2.18.2-cp38-cp38-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:82d5d4d78e4448683cb467897fe24e2b74bb7b973a541ea1dcfec1d3cbce39fb"},
+ {file = "pydantic_core-2.18.2-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4774f3184d2ef3e14e8693194f661dea5a4d6ca4e3dc8e39786d33a94865cefd"},
+ {file = "pydantic_core-2.18.2-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d4d938ec0adf5167cb335acb25a4ee69a8107e4984f8fbd2e897021d9e4ca21b"},
+ {file = "pydantic_core-2.18.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e0e8b1be28239fc64a88a8189d1df7fad8be8c1ae47fcc33e43d4be15f99cc70"},
+ {file = "pydantic_core-2.18.2-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:868649da93e5a3d5eacc2b5b3b9235c98ccdbfd443832f31e075f54419e1b96b"},
+ {file = "pydantic_core-2.18.2-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:78363590ef93d5d226ba21a90a03ea89a20738ee5b7da83d771d283fd8a56761"},
+ {file = "pydantic_core-2.18.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:852e966fbd035a6468fc0a3496589b45e2208ec7ca95c26470a54daed82a0788"},
+ {file = "pydantic_core-2.18.2-cp38-none-win32.whl", hash = "sha256:6a46e22a707e7ad4484ac9ee9f290f9d501df45954184e23fc29408dfad61350"},
+ {file = "pydantic_core-2.18.2-cp38-none-win_amd64.whl", hash = "sha256:d91cb5ea8b11607cc757675051f61b3d93f15eca3cefb3e6c704a5d6e8440f4e"},
+ {file = "pydantic_core-2.18.2-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:ae0a8a797a5e56c053610fa7be147993fe50960fa43609ff2a9552b0e07013e8"},
+ {file = "pydantic_core-2.18.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:042473b6280246b1dbf530559246f6842b56119c2926d1e52b631bdc46075f2a"},
+ {file = "pydantic_core-2.18.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1a388a77e629b9ec814c1b1e6b3b595fe521d2cdc625fcca26fbc2d44c816804"},
+ {file = "pydantic_core-2.18.2-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e25add29b8f3b233ae90ccef2d902d0ae0432eb0d45370fe315d1a5cf231004b"},
+ {file = "pydantic_core-2.18.2-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f459a5ce8434614dfd39bbebf1041952ae01da6bed9855008cb33b875cb024c0"},
+ {file = "pydantic_core-2.18.2-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:eff2de745698eb46eeb51193a9f41d67d834d50e424aef27df2fcdee1b153845"},
+ {file = "pydantic_core-2.18.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a8309f67285bdfe65c372ea3722b7a5642680f3dba538566340a9d36e920b5f0"},
+ {file = "pydantic_core-2.18.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:f93a8a2e3938ff656a7c1bc57193b1319960ac015b6e87d76c76bf14fe0244b4"},
+ {file = "pydantic_core-2.18.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:22057013c8c1e272eb8d0eebc796701167d8377441ec894a8fed1af64a0bf399"},
+ {file = "pydantic_core-2.18.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:cfeecd1ac6cc1fb2692c3d5110781c965aabd4ec5d32799773ca7b1456ac636b"},
+ {file = "pydantic_core-2.18.2-cp39-none-win32.whl", hash = "sha256:0d69b4c2f6bb3e130dba60d34c0845ba31b69babdd3f78f7c0c8fae5021a253e"},
+ {file = "pydantic_core-2.18.2-cp39-none-win_amd64.whl", hash = "sha256:d9319e499827271b09b4e411905b24a426b8fb69464dfa1696258f53a3334641"},
+ {file = "pydantic_core-2.18.2-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:a1874c6dd4113308bd0eb568418e6114b252afe44319ead2b4081e9b9521fe75"},
+ {file = "pydantic_core-2.18.2-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:ccdd111c03bfd3666bd2472b674c6899550e09e9f298954cfc896ab92b5b0e6d"},
+ {file = "pydantic_core-2.18.2-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e18609ceaa6eed63753037fc06ebb16041d17d28199ae5aba0052c51449650a9"},
+ {file = "pydantic_core-2.18.2-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6e5c584d357c4e2baf0ff7baf44f4994be121e16a2c88918a5817331fc7599d7"},
+ {file = "pydantic_core-2.18.2-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:43f0f463cf89ace478de71a318b1b4f05ebc456a9b9300d027b4b57c1a2064fb"},
+ {file = "pydantic_core-2.18.2-pp310-pypy310_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:e1b395e58b10b73b07b7cf740d728dd4ff9365ac46c18751bf8b3d8cca8f625a"},
+ {file = "pydantic_core-2.18.2-pp310-pypy310_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:0098300eebb1c837271d3d1a2cd2911e7c11b396eac9661655ee524a7f10587b"},
+ {file = "pydantic_core-2.18.2-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:36789b70d613fbac0a25bb07ab3d9dba4d2e38af609c020cf4d888d165ee0bf3"},
+ {file = "pydantic_core-2.18.2-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:3f9a801e7c8f1ef8718da265bba008fa121243dfe37c1cea17840b0944dfd72c"},
+ {file = "pydantic_core-2.18.2-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:3a6515ebc6e69d85502b4951d89131ca4e036078ea35533bb76327f8424531ce"},
+ {file = "pydantic_core-2.18.2-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:20aca1e2298c56ececfd8ed159ae4dde2df0781988c97ef77d5c16ff4bd5b400"},
+ {file = "pydantic_core-2.18.2-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:223ee893d77a310a0391dca6df00f70bbc2f36a71a895cecd9a0e762dc37b349"},
+ {file = "pydantic_core-2.18.2-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2334ce8c673ee93a1d6a65bd90327588387ba073c17e61bf19b4fd97d688d63c"},
+ {file = "pydantic_core-2.18.2-pp39-pypy39_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:cbca948f2d14b09d20268cda7b0367723d79063f26c4ffc523af9042cad95592"},
+ {file = "pydantic_core-2.18.2-pp39-pypy39_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:b3ef08e20ec49e02d5c6717a91bb5af9b20f1805583cb0adfe9ba2c6b505b5ae"},
+ {file = "pydantic_core-2.18.2-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:c6fdc8627910eed0c01aed6a390a252fe3ea6d472ee70fdde56273f198938374"},
+ {file = "pydantic_core-2.18.2.tar.gz", hash = "sha256:2e29d20810dfc3043ee13ac7d9e25105799817683348823f305ab3f349b9386e"},
+]
+
+[package.dependencies]
+typing-extensions = ">=4.6.0,<4.7.0 || >4.7.0"
+
+[[package]]
+name = "pytest"
+version = "7.4.4"
+description = "pytest: simple powerful testing with Python"
+optional = false
+python-versions = ">=3.7"
+files = [
+ {file = "pytest-7.4.4-py3-none-any.whl", hash = "sha256:b090cdf5ed60bf4c45261be03239c2c1c22df034fbffe691abe93cd80cea01d8"},
+ {file = "pytest-7.4.4.tar.gz", hash = "sha256:2cf0005922c6ace4a3e2ec8b4080eb0d9753fdc93107415332f50ce9e7994280"},
+]
+
+[package.dependencies]
+colorama = {version = "*", markers = "sys_platform == \"win32\""}
+exceptiongroup = {version = ">=1.0.0rc8", markers = "python_version < \"3.11\""}
+iniconfig = "*"
+packaging = "*"
+pluggy = ">=0.12,<2.0"
+tomli = {version = ">=1.0.0", markers = "python_version < \"3.11\""}
+
+[package.extras]
+testing = ["argcomplete", "attrs (>=19.2.0)", "hypothesis (>=3.56)", "mock", "nose", "pygments (>=2.7.2)", "requests", "setuptools", "xmlschema"]
+
+[[package]]
+name = "pytest-asyncio"
+version = "0.23.7"
+description = "Pytest support for asyncio"
+optional = false
+python-versions = ">=3.8"
+files = [
+ {file = "pytest_asyncio-0.23.7-py3-none-any.whl", hash = "sha256:009b48127fbe44518a547bddd25611551b0e43ccdbf1e67d12479f569832c20b"},
+ {file = "pytest_asyncio-0.23.7.tar.gz", hash = "sha256:5f5c72948f4c49e7db4f29f2521d4031f1c27f86e57b046126654083d4770268"},
+]
+
+[package.dependencies]
+pytest = ">=7.0.0,<9"
+
+[package.extras]
+docs = ["sphinx (>=5.3)", "sphinx-rtd-theme (>=1.0)"]
+testing = ["coverage (>=6.2)", "hypothesis (>=5.7.1)"]
+
+[[package]]
+name = "pytest-socket"
+version = "0.7.0"
+description = "Pytest Plugin to disable socket calls during tests"
+optional = false
+python-versions = ">=3.8,<4.0"
+files = [
+ {file = "pytest_socket-0.7.0-py3-none-any.whl", hash = "sha256:7e0f4642177d55d317bbd58fc68c6bd9048d6eadb2d46a89307fa9221336ce45"},
+ {file = "pytest_socket-0.7.0.tar.gz", hash = "sha256:71ab048cbbcb085c15a4423b73b619a8b35d6a307f46f78ea46be51b1b7e11b3"},
+]
+
+[package.dependencies]
+pytest = ">=6.2.5"
+
+[[package]]
+name = "pyyaml"
+version = "6.0.1"
+description = "YAML parser and emitter for Python"
+optional = false
+python-versions = ">=3.6"
+files = [
+ {file = "PyYAML-6.0.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:d858aa552c999bc8a8d57426ed01e40bef403cd8ccdd0fc5f6f04a00414cac2a"},
+ {file = "PyYAML-6.0.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:fd66fc5d0da6d9815ba2cebeb4205f95818ff4b79c3ebe268e75d961704af52f"},
+ {file = "PyYAML-6.0.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:69b023b2b4daa7548bcfbd4aa3da05b3a74b772db9e23b982788168117739938"},
+ {file = "PyYAML-6.0.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:81e0b275a9ecc9c0c0c07b4b90ba548307583c125f54d5b6946cfee6360c733d"},
+ {file = "PyYAML-6.0.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ba336e390cd8e4d1739f42dfe9bb83a3cc2e80f567d8805e11b46f4a943f5515"},
+ {file = "PyYAML-6.0.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:326c013efe8048858a6d312ddd31d56e468118ad4cdeda36c719bf5bb6192290"},
+ {file = "PyYAML-6.0.1-cp310-cp310-win32.whl", hash = "sha256:bd4af7373a854424dabd882decdc5579653d7868b8fb26dc7d0e99f823aa5924"},
+ {file = "PyYAML-6.0.1-cp310-cp310-win_amd64.whl", hash = "sha256:fd1592b3fdf65fff2ad0004b5e363300ef59ced41c2e6b3a99d4089fa8c5435d"},
+ {file = "PyYAML-6.0.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:6965a7bc3cf88e5a1c3bd2e0b5c22f8d677dc88a455344035f03399034eb3007"},
+ {file = "PyYAML-6.0.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:f003ed9ad21d6a4713f0a9b5a7a0a79e08dd0f221aff4525a2be4c346ee60aab"},
+ {file = "PyYAML-6.0.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:42f8152b8dbc4fe7d96729ec2b99c7097d656dc1213a3229ca5383f973a5ed6d"},
+ {file = "PyYAML-6.0.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:062582fca9fabdd2c8b54a3ef1c978d786e0f6b3a1510e0ac93ef59e0ddae2bc"},
+ {file = "PyYAML-6.0.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d2b04aac4d386b172d5b9692e2d2da8de7bfb6c387fa4f801fbf6fb2e6ba4673"},
+ {file = "PyYAML-6.0.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:e7d73685e87afe9f3b36c799222440d6cf362062f78be1013661b00c5c6f678b"},
+ {file = "PyYAML-6.0.1-cp311-cp311-win32.whl", hash = "sha256:1635fd110e8d85d55237ab316b5b011de701ea0f29d07611174a1b42f1444741"},
+ {file = "PyYAML-6.0.1-cp311-cp311-win_amd64.whl", hash = "sha256:bf07ee2fef7014951eeb99f56f39c9bb4af143d8aa3c21b1677805985307da34"},
+ {file = "PyYAML-6.0.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:855fb52b0dc35af121542a76b9a84f8d1cd886ea97c84703eaa6d88e37a2ad28"},
+ {file = "PyYAML-6.0.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:40df9b996c2b73138957fe23a16a4f0ba614f4c0efce1e9406a184b6d07fa3a9"},
+ {file = "PyYAML-6.0.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a08c6f0fe150303c1c6b71ebcd7213c2858041a7e01975da3a99aed1e7a378ef"},
+ {file = "PyYAML-6.0.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6c22bec3fbe2524cde73d7ada88f6566758a8f7227bfbf93a408a9d86bcc12a0"},
+ {file = "PyYAML-6.0.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:8d4e9c88387b0f5c7d5f281e55304de64cf7f9c0021a3525bd3b1c542da3b0e4"},
+ {file = "PyYAML-6.0.1-cp312-cp312-win32.whl", hash = "sha256:d483d2cdf104e7c9fa60c544d92981f12ad66a457afae824d146093b8c294c54"},
+ {file = "PyYAML-6.0.1-cp312-cp312-win_amd64.whl", hash = "sha256:0d3304d8c0adc42be59c5f8a4d9e3d7379e6955ad754aa9d6ab7a398b59dd1df"},
+ {file = "PyYAML-6.0.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:50550eb667afee136e9a77d6dc71ae76a44df8b3e51e41b77f6de2932bfe0f47"},
+ {file = "PyYAML-6.0.1-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1fe35611261b29bd1de0070f0b2f47cb6ff71fa6595c077e42bd0c419fa27b98"},
+ {file = "PyYAML-6.0.1-cp36-cp36m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:704219a11b772aea0d8ecd7058d0082713c3562b4e271b849ad7dc4a5c90c13c"},
+ {file = "PyYAML-6.0.1-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:afd7e57eddb1a54f0f1a974bc4391af8bcce0b444685d936840f125cf046d5bd"},
+ {file = "PyYAML-6.0.1-cp36-cp36m-win32.whl", hash = "sha256:fca0e3a251908a499833aa292323f32437106001d436eca0e6e7833256674585"},
+ {file = "PyYAML-6.0.1-cp36-cp36m-win_amd64.whl", hash = "sha256:f22ac1c3cac4dbc50079e965eba2c1058622631e526bd9afd45fedd49ba781fa"},
+ {file = "PyYAML-6.0.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:b1275ad35a5d18c62a7220633c913e1b42d44b46ee12554e5fd39c70a243d6a3"},
+ {file = "PyYAML-6.0.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:18aeb1bf9a78867dc38b259769503436b7c72f7a1f1f4c93ff9a17de54319b27"},
+ {file = "PyYAML-6.0.1-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:596106435fa6ad000c2991a98fa58eeb8656ef2325d7e158344fb33864ed87e3"},
+ {file = "PyYAML-6.0.1-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:baa90d3f661d43131ca170712d903e6295d1f7a0f595074f151c0aed377c9b9c"},
+ {file = "PyYAML-6.0.1-cp37-cp37m-win32.whl", hash = "sha256:9046c58c4395dff28dd494285c82ba00b546adfc7ef001486fbf0324bc174fba"},
+ {file = "PyYAML-6.0.1-cp37-cp37m-win_amd64.whl", hash = "sha256:4fb147e7a67ef577a588a0e2c17b6db51dda102c71de36f8549b6816a96e1867"},
+ {file = "PyYAML-6.0.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:1d4c7e777c441b20e32f52bd377e0c409713e8bb1386e1099c2415f26e479595"},
+ {file = "PyYAML-6.0.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a0cd17c15d3bb3fa06978b4e8958dcdc6e0174ccea823003a106c7d4d7899ac5"},
+ {file = "PyYAML-6.0.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:28c119d996beec18c05208a8bd78cbe4007878c6dd15091efb73a30e90539696"},
+ {file = "PyYAML-6.0.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7e07cbde391ba96ab58e532ff4803f79c4129397514e1413a7dc761ccd755735"},
+ {file = "PyYAML-6.0.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:49a183be227561de579b4a36efbb21b3eab9651dd81b1858589f796549873dd6"},
+ {file = "PyYAML-6.0.1-cp38-cp38-win32.whl", hash = "sha256:184c5108a2aca3c5b3d3bf9395d50893a7ab82a38004c8f61c258d4428e80206"},
+ {file = "PyYAML-6.0.1-cp38-cp38-win_amd64.whl", hash = "sha256:1e2722cc9fbb45d9b87631ac70924c11d3a401b2d7f410cc0e3bbf249f2dca62"},
+ {file = "PyYAML-6.0.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:9eb6caa9a297fc2c2fb8862bc5370d0303ddba53ba97e71f08023b6cd73d16a8"},
+ {file = "PyYAML-6.0.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:c8098ddcc2a85b61647b2590f825f3db38891662cfc2fc776415143f599bb859"},
+ {file = "PyYAML-6.0.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5773183b6446b2c99bb77e77595dd486303b4faab2b086e7b17bc6bef28865f6"},
+ {file = "PyYAML-6.0.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b786eecbdf8499b9ca1d697215862083bd6d2a99965554781d0d8d1ad31e13a0"},
+ {file = "PyYAML-6.0.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bc1bf2925a1ecd43da378f4db9e4f799775d6367bdb94671027b73b393a7c42c"},
+ {file = "PyYAML-6.0.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:04ac92ad1925b2cff1db0cfebffb6ffc43457495c9b3c39d3fcae417d7125dc5"},
+ {file = "PyYAML-6.0.1-cp39-cp39-win32.whl", hash = "sha256:faca3bdcf85b2fc05d06ff3fbc1f83e1391b3e724afa3feba7d13eeab355484c"},
+ {file = "PyYAML-6.0.1-cp39-cp39-win_amd64.whl", hash = "sha256:510c9deebc5c0225e8c96813043e62b680ba2f9c50a08d3724c7f28a747d1486"},
+ {file = "PyYAML-6.0.1.tar.gz", hash = "sha256:bfdf460b1736c775f2ba9f6a92bca30bc2095067b8a9d77876d1fad6cc3b4a43"},
+]
+
+[[package]]
+name = "requests"
+version = "2.32.2"
+description = "Python HTTP for Humans."
+optional = false
+python-versions = ">=3.8"
+files = [
+ {file = "requests-2.32.2-py3-none-any.whl", hash = "sha256:fc06670dd0ed212426dfeb94fc1b983d917c4f9847c863f313c9dfaaffb7c23c"},
+ {file = "requests-2.32.2.tar.gz", hash = "sha256:dd951ff5ecf3e3b3aa26b40703ba77495dab41da839ae72ef3c8e5d8e2433289"},
+]
+
+[package.dependencies]
+certifi = ">=2017.4.17"
+charset-normalizer = ">=2,<4"
+idna = ">=2.5,<4"
+urllib3 = ">=1.21.1,<3"
+
+[package.extras]
+socks = ["PySocks (>=1.5.6,!=1.5.7)"]
+use-chardet-on-py3 = ["chardet (>=3.0.2,<6)"]
+
+[[package]]
+name = "ruff"
+version = "0.1.15"
+description = "An extremely fast Python linter and code formatter, written in Rust."
+optional = false
+python-versions = ">=3.7"
+files = [
+ {file = "ruff-0.1.15-py3-none-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:5fe8d54df166ecc24106db7dd6a68d44852d14eb0729ea4672bb4d96c320b7df"},
+ {file = "ruff-0.1.15-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:6f0bfbb53c4b4de117ac4d6ddfd33aa5fc31beeaa21d23c45c6dd249faf9126f"},
+ {file = "ruff-0.1.15-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e0d432aec35bfc0d800d4f70eba26e23a352386be3a6cf157083d18f6f5881c8"},
+ {file = "ruff-0.1.15-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:9405fa9ac0e97f35aaddf185a1be194a589424b8713e3b97b762336ec79ff807"},
+ {file = "ruff-0.1.15-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c66ec24fe36841636e814b8f90f572a8c0cb0e54d8b5c2d0e300d28a0d7bffec"},
+ {file = "ruff-0.1.15-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:6f8ad828f01e8dd32cc58bc28375150171d198491fc901f6f98d2a39ba8e3ff5"},
+ {file = "ruff-0.1.15-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:86811954eec63e9ea162af0ffa9f8d09088bab51b7438e8b6488b9401863c25e"},
+ {file = "ruff-0.1.15-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:fd4025ac5e87d9b80e1f300207eb2fd099ff8200fa2320d7dc066a3f4622dc6b"},
+ {file = "ruff-0.1.15-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b17b93c02cdb6aeb696effecea1095ac93f3884a49a554a9afa76bb125c114c1"},
+ {file = "ruff-0.1.15-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:ddb87643be40f034e97e97f5bc2ef7ce39de20e34608f3f829db727a93fb82c5"},
+ {file = "ruff-0.1.15-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:abf4822129ed3a5ce54383d5f0e964e7fef74a41e48eb1dfad404151efc130a2"},
+ {file = "ruff-0.1.15-py3-none-musllinux_1_2_i686.whl", hash = "sha256:6c629cf64bacfd136c07c78ac10a54578ec9d1bd2a9d395efbee0935868bf852"},
+ {file = "ruff-0.1.15-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:1bab866aafb53da39c2cadfb8e1c4550ac5340bb40300083eb8967ba25481447"},
+ {file = "ruff-0.1.15-py3-none-win32.whl", hash = "sha256:2417e1cb6e2068389b07e6fa74c306b2810fe3ee3476d5b8a96616633f40d14f"},
+ {file = "ruff-0.1.15-py3-none-win_amd64.whl", hash = "sha256:3837ac73d869efc4182d9036b1405ef4c73d9b1f88da2413875e34e0d6919587"},
+ {file = "ruff-0.1.15-py3-none-win_arm64.whl", hash = "sha256:9a933dfb1c14ec7a33cceb1e49ec4a16b51ce3c20fd42663198746efc0427360"},
+ {file = "ruff-0.1.15.tar.gz", hash = "sha256:f6dfa8c1b21c913c326919056c390966648b680966febcb796cc9d1aaab8564e"},
+]
+
+[[package]]
+name = "syrupy"
+version = "4.6.1"
+description = "Pytest Snapshot Test Utility"
+optional = false
+python-versions = ">=3.8.1,<4"
+files = [
+ {file = "syrupy-4.6.1-py3-none-any.whl", hash = "sha256:203e52f9cb9fa749cf683f29bd68f02c16c3bc7e7e5fe8f2fc59bdfe488ce133"},
+ {file = "syrupy-4.6.1.tar.gz", hash = "sha256:37a835c9ce7857eeef86d62145885e10b3cb9615bc6abeb4ce404b3f18e1bb36"},
+]
+
+[package.dependencies]
+pytest = ">=7.0.0,<9.0.0"
+
+[[package]]
+name = "tenacity"
+version = "8.3.0"
+description = "Retry code until it succeeds"
+optional = false
+python-versions = ">=3.8"
+files = [
+ {file = "tenacity-8.3.0-py3-none-any.whl", hash = "sha256:3649f6443dbc0d9b01b9d8020a9c4ec7a1ff5f6f3c6c8a036ef371f573fe9185"},
+ {file = "tenacity-8.3.0.tar.gz", hash = "sha256:953d4e6ad24357bceffbc9707bc74349aca9d245f68eb65419cf0c249a1949a2"},
+]
+
+[package.extras]
+doc = ["reno", "sphinx"]
+test = ["pytest", "tornado (>=4.5)", "typeguard"]
+
+[[package]]
+name = "tomli"
+version = "2.0.1"
+description = "A lil' TOML parser"
+optional = false
+python-versions = ">=3.7"
+files = [
+ {file = "tomli-2.0.1-py3-none-any.whl", hash = "sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc"},
+ {file = "tomli-2.0.1.tar.gz", hash = "sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f"},
+]
+
+[[package]]
+name = "typing-extensions"
+version = "4.11.0"
+description = "Backported and Experimental Type Hints for Python 3.8+"
+optional = false
+python-versions = ">=3.8"
+files = [
+ {file = "typing_extensions-4.11.0-py3-none-any.whl", hash = "sha256:c1f94d72897edaf4ce775bb7558d5b79d8126906a14ea5ed1635921406c0387a"},
+ {file = "typing_extensions-4.11.0.tar.gz", hash = "sha256:83f085bd5ca59c80295fc2a82ab5dac679cbe02b9f33f7d83af68e241bea51b0"},
+]
+
+[[package]]
+name = "urllib3"
+version = "2.2.1"
+description = "HTTP library with thread-safe connection pooling, file post, and more."
+optional = false
+python-versions = ">=3.8"
+files = [
+ {file = "urllib3-2.2.1-py3-none-any.whl", hash = "sha256:450b20ec296a467077128bff42b73080516e71b56ff59a60a02bef2232c4fa9d"},
+ {file = "urllib3-2.2.1.tar.gz", hash = "sha256:d0570876c61ab9e520d776c38acbbb5b05a776d3f9ff98a5c8fd5162a444cf19"},
+]
+
+[package.extras]
+brotli = ["brotli (>=1.0.9)", "brotlicffi (>=0.8.0)"]
+h2 = ["h2 (>=4,<5)"]
+socks = ["pysocks (>=1.5.6,!=1.5.7,<2.0)"]
+zstd = ["zstandard (>=0.18.0)"]
+
+[metadata]
+lock-version = "2.0"
+python-versions = ">=3.8.1,<4.0"
+content-hash = "d27ea82fa58fa4e03d47f03b6644b8da6a1b1792014e6b68abfe88cc9f45c9b3"
diff --git a/libs/partners/couchbase/pyproject.toml b/libs/partners/couchbase/pyproject.toml
new file mode 100644
index 0000000000000..afdbba9a4b49e
--- /dev/null
+++ b/libs/partners/couchbase/pyproject.toml
@@ -0,0 +1,92 @@
+[tool.poetry]
+name = "gigachain-couchbase"
+version = "0.0.1"
+description = "An integration package connecting Couchbase and Gigachain"
+authors = []
+readme = "README.md"
+repository = "https://github.com/langchain-ai/langchain"
+license = "MIT"
+
+[tool.poetry.urls]
+"Source Code" = "https://github.com/langchain-ai/langchain/tree/master/libs/partners/couchbase"
+
+[tool.poetry.dependencies]
+python = ">=3.8.1,<4.0"
+gigachain-core = ">=0.2.0,<0.3"
+couchbase = "^4.2.1"
+
+[tool.poetry.group.test]
+optional = true
+
+[tool.poetry.group.test.dependencies]
+pytest = "^7.4.3"
+pytest-asyncio = "^0.23.2"
+pytest-socket = "^0.7.0"
+gigachain-core = {path = "../../core", develop = true}
+syrupy = "^4.0.2"
+
+[tool.poetry.group.codespell]
+optional = true
+
+[tool.poetry.group.codespell.dependencies]
+codespell = "^2.2.6"
+
+[tool.poetry.group.test_integration]
+optional = true
+
+[tool.poetry.group.test_integration.dependencies]
+
+[tool.poetry.group.lint]
+optional = true
+
+[tool.poetry.group.lint.dependencies]
+ruff = "^0.1.8"
+
+[tool.poetry.group.typing.dependencies]
+mypy = "^1.7.1"
+gigachain-core = {path = "../../core", develop = true}
+
+[tool.poetry.group.dev]
+optional = true
+
+[tool.poetry.group.dev.dependencies]
+gigachain-core = {path = "../../core", develop = true}
+
+[tool.ruff.lint]
+select = [
+ "E", # pycodestyle
+ "F", # pyflakes
+ "I", # isort
+ "T201", # print
+]
+
+[tool.mypy]
+disallow_untyped_defs = "True"
+ignore_missing_imports = "True"
+
+[tool.coverage.run]
+omit = [
+ "tests/*",
+]
+
+[build-system]
+requires = ["poetry-core>=1.0.0"]
+build-backend = "poetry.core.masonry.api"
+
+[tool.pytest.ini_options]
+# --strict-markers will raise errors on unknown marks.
+# https://docs.pytest.org/en/7.1.x/how-to/mark.html#raising-errors-on-unknown-marks
+#
+# https://docs.pytest.org/en/7.1.x/reference/reference.html
+# --strict-config any warnings encountered while parsing the `pytest`
+# section of the configuration file raise errors.
+#
+# https://github.com/tophat/syrupy
+# --snapshot-warn-unused Prints a warning on unused snapshots rather than fail the test suite.
+addopts = "--snapshot-warn-unused --strict-markers --strict-config --durations=5"
+# Registering custom markers.
+# https://docs.pytest.org/en/7.1.x/example/markers.html#registering-markers
+markers = [
+ "compile: mark placeholder test used to compile integration tests without running them",
+]
+asyncio_mode = "auto"
diff --git a/libs/partners/couchbase/scripts/check_imports.py b/libs/partners/couchbase/scripts/check_imports.py
new file mode 100644
index 0000000000000..365f5fa118da4
--- /dev/null
+++ b/libs/partners/couchbase/scripts/check_imports.py
@@ -0,0 +1,17 @@
+import sys
+import traceback
+from importlib.machinery import SourceFileLoader
+
+if __name__ == "__main__":
+ files = sys.argv[1:]
+ has_failure = False
+ for file in files:
+ try:
+ SourceFileLoader("x", file).load_module()
+ except Exception:
+ has_faillure = True
+ print(file) # noqa: T201
+ traceback.print_exc()
+ print() # noqa: T201
+
+ sys.exit(1 if has_failure else 0)
diff --git a/libs/partners/couchbase/scripts/check_pydantic.sh b/libs/partners/couchbase/scripts/check_pydantic.sh
new file mode 100755
index 0000000000000..06b5bb81ae236
--- /dev/null
+++ b/libs/partners/couchbase/scripts/check_pydantic.sh
@@ -0,0 +1,27 @@
+#!/bin/bash
+#
+# This script searches for lines starting with "import pydantic" or "from pydantic"
+# in tracked files within a Git repository.
+#
+# Usage: ./scripts/check_pydantic.sh /path/to/repository
+
+# Check if a path argument is provided
+if [ $# -ne 1 ]; then
+ echo "Usage: $0 /path/to/repository"
+ exit 1
+fi
+
+repository_path="$1"
+
+# Search for lines matching the pattern within the specified repository
+result=$(git -C "$repository_path" grep -E '^import pydantic|^from pydantic')
+
+# Check if any matching lines were found
+if [ -n "$result" ]; then
+ echo "ERROR: The following lines need to be updated:"
+ echo "$result"
+ echo "Please replace the code with an import from langchain_core.pydantic_v1."
+ echo "For example, replace 'from pydantic import BaseModel'"
+ echo "with 'from langchain_core.pydantic_v1 import BaseModel'"
+ exit 1
+fi
diff --git a/libs/partners/couchbase/scripts/lint_imports.sh b/libs/partners/couchbase/scripts/lint_imports.sh
new file mode 100755
index 0000000000000..19ccec1480c01
--- /dev/null
+++ b/libs/partners/couchbase/scripts/lint_imports.sh
@@ -0,0 +1,18 @@
+#!/bin/bash
+
+set -eu
+
+# Initialize a variable to keep track of errors
+errors=0
+
+# make sure not importing from langchain, langchain_experimental, or langchain_community
+git --no-pager grep '^from langchain\.' . && errors=$((errors+1))
+git --no-pager grep '^from langchain_experimental\.' . && errors=$((errors+1))
+git --no-pager grep '^from langchain_community\.' . && errors=$((errors+1))
+
+# Decide on an exit status based on the errors
+if [ "$errors" -gt 0 ]; then
+ exit 1
+else
+ exit 0
+fi
diff --git a/libs/partners/couchbase/tests/__init__.py b/libs/partners/couchbase/tests/__init__.py
new file mode 100644
index 0000000000000..e69de29bb2d1d
diff --git a/libs/partners/couchbase/tests/integration_tests/__init__.py b/libs/partners/couchbase/tests/integration_tests/__init__.py
new file mode 100644
index 0000000000000..e69de29bb2d1d
diff --git a/libs/partners/couchbase/tests/integration_tests/test_compile.py b/libs/partners/couchbase/tests/integration_tests/test_compile.py
new file mode 100644
index 0000000000000..33ecccdfa0fbd
--- /dev/null
+++ b/libs/partners/couchbase/tests/integration_tests/test_compile.py
@@ -0,0 +1,7 @@
+import pytest
+
+
+@pytest.mark.compile
+def test_placeholder() -> None:
+ """Used for compiling integration tests without running any real tests."""
+ pass
diff --git a/libs/partners/couchbase/tests/integration_tests/test_vector_store.py b/libs/partners/couchbase/tests/integration_tests/test_vector_store.py
new file mode 100644
index 0000000000000..4cad73481d032
--- /dev/null
+++ b/libs/partners/couchbase/tests/integration_tests/test_vector_store.py
@@ -0,0 +1,366 @@
+"""Test Couchbase Vector Store functionality"""
+
+import os
+import time
+from typing import Any
+
+import pytest
+from langchain_core.documents import Document
+
+from langchain_couchbase import CouchbaseVectorStore
+from tests.utils import (
+ ConsistentFakeEmbeddings,
+)
+
+CONNECTION_STRING = os.getenv("COUCHBASE_CONNECTION_STRING", "")
+BUCKET_NAME = os.getenv("COUCHBASE_BUCKET_NAME", "")
+SCOPE_NAME = os.getenv("COUCHBASE_SCOPE_NAME", "")
+COLLECTION_NAME = os.getenv("COUCHBASE_COLLECTION_NAME", "")
+USERNAME = os.getenv("COUCHBASE_USERNAME", "")
+PASSWORD = os.getenv("COUCHBASE_PASSWORD", "")
+INDEX_NAME = os.getenv("COUCHBASE_INDEX_NAME", "")
+SLEEP_DURATION = 1
+
+
+def set_all_env_vars() -> bool:
+ return all(
+ [
+ CONNECTION_STRING,
+ BUCKET_NAME,
+ SCOPE_NAME,
+ COLLECTION_NAME,
+ USERNAME,
+ PASSWORD,
+ INDEX_NAME,
+ ]
+ )
+
+
+def get_cluster() -> Any:
+ """Get a couchbase cluster object"""
+ from datetime import timedelta
+
+ from couchbase.auth import PasswordAuthenticator
+ from couchbase.cluster import Cluster
+ from couchbase.options import ClusterOptions
+
+ auth = PasswordAuthenticator(USERNAME, PASSWORD)
+ options = ClusterOptions(auth)
+ connect_string = CONNECTION_STRING
+ cluster = Cluster(connect_string, options)
+
+ # Wait until the cluster is ready for use.
+ cluster.wait_until_ready(timedelta(seconds=5))
+
+ return cluster
+
+
+@pytest.fixture()
+def cluster() -> Any:
+ """Get a couchbase cluster object"""
+ return get_cluster()
+
+
+def delete_documents(
+ cluster: Any, bucket_name: str, scope_name: str, collection_name: str
+) -> None:
+ """Delete all the documents in the collection"""
+ query = f"DELETE FROM `{bucket_name}`.`{scope_name}`.`{collection_name}`"
+ cluster.query(query).execute()
+
+
+@pytest.mark.skipif(
+ not set_all_env_vars(), reason="Missing Couchbase environment variables"
+)
+class TestCouchbaseVectorStore:
+ @classmethod
+ def setup_method(self) -> None:
+ cluster = get_cluster()
+ # Delete all the documents in the collection
+ delete_documents(cluster, BUCKET_NAME, SCOPE_NAME, COLLECTION_NAME)
+
+ def test_from_documents(self, cluster: Any) -> None:
+ """Test end to end search using a list of documents."""
+
+ documents = [
+ Document(page_content="foo", metadata={"page": 1}),
+ Document(page_content="bar", metadata={"page": 2}),
+ Document(page_content="baz", metadata={"page": 3}),
+ ]
+
+ vectorstore = CouchbaseVectorStore.from_documents(
+ documents,
+ ConsistentFakeEmbeddings(),
+ cluster=cluster,
+ bucket_name=BUCKET_NAME,
+ scope_name=SCOPE_NAME,
+ collection_name=COLLECTION_NAME,
+ index_name=INDEX_NAME,
+ )
+
+ # Wait for the documents to be indexed
+ time.sleep(SLEEP_DURATION)
+
+ output = vectorstore.similarity_search("baz", k=1)
+ assert output[0].page_content == "baz"
+ assert output[0].metadata["page"] == 3
+
+ def test_from_texts(self, cluster: Any) -> None:
+ """Test end to end search using a list of texts."""
+
+ texts = [
+ "foo",
+ "bar",
+ "baz",
+ ]
+
+ vectorstore = CouchbaseVectorStore.from_texts(
+ texts,
+ ConsistentFakeEmbeddings(),
+ cluster=cluster,
+ index_name=INDEX_NAME,
+ bucket_name=BUCKET_NAME,
+ scope_name=SCOPE_NAME,
+ collection_name=COLLECTION_NAME,
+ )
+
+ # Wait for the documents to be indexed
+ time.sleep(SLEEP_DURATION)
+
+ output = vectorstore.similarity_search("foo", k=1)
+ assert len(output) == 1
+ assert output[0].page_content == "foo"
+
+ def test_from_texts_with_metadatas(self, cluster: Any) -> None:
+ """Test end to end search using a list of texts and metadatas."""
+
+ texts = [
+ "foo",
+ "bar",
+ "baz",
+ ]
+
+ metadatas = [{"a": 1}, {"b": 2}, {"c": 3}]
+
+ vectorstore = CouchbaseVectorStore.from_texts(
+ texts,
+ ConsistentFakeEmbeddings(),
+ metadatas=metadatas,
+ cluster=cluster,
+ index_name=INDEX_NAME,
+ bucket_name=BUCKET_NAME,
+ scope_name=SCOPE_NAME,
+ collection_name=COLLECTION_NAME,
+ )
+
+ # Wait for the documents to be indexed
+ time.sleep(SLEEP_DURATION)
+
+ output = vectorstore.similarity_search("baz", k=1)
+ assert output[0].page_content == "baz"
+ assert output[0].metadata["c"] == 3
+
+ def test_add_texts_with_ids_and_metadatas(self, cluster: Any) -> None:
+ """Test end to end search by adding a list of texts, ids and metadatas."""
+
+ texts = [
+ "foo",
+ "bar",
+ "baz",
+ ]
+
+ ids = ["a", "b", "c"]
+
+ metadatas = [{"a": 1}, {"b": 2}, {"c": 3}]
+
+ vectorstore = CouchbaseVectorStore(
+ cluster=cluster,
+ embedding=ConsistentFakeEmbeddings(),
+ index_name=INDEX_NAME,
+ bucket_name=BUCKET_NAME,
+ scope_name=SCOPE_NAME,
+ collection_name=COLLECTION_NAME,
+ )
+
+ results = vectorstore.add_texts(
+ texts,
+ ids=ids,
+ metadatas=metadatas,
+ )
+ assert results == ids
+
+ # Wait for the documents to be indexed
+ time.sleep(SLEEP_DURATION)
+
+ output = vectorstore.similarity_search("foo", k=1)
+ assert output[0].page_content == "foo"
+ assert output[0].metadata["a"] == 1
+
+ def test_delete_texts_with_ids(self, cluster: Any) -> None:
+ """Test deletion of documents by ids."""
+ texts = [
+ "foo",
+ "bar",
+ "baz",
+ ]
+
+ ids = ["a", "b", "c"]
+
+ metadatas = [{"a": 1}, {"b": 2}, {"c": 3}]
+
+ vectorstore = CouchbaseVectorStore(
+ cluster=cluster,
+ embedding=ConsistentFakeEmbeddings(),
+ index_name=INDEX_NAME,
+ bucket_name=BUCKET_NAME,
+ scope_name=SCOPE_NAME,
+ collection_name=COLLECTION_NAME,
+ )
+
+ results = vectorstore.add_texts(
+ texts,
+ ids=ids,
+ metadatas=metadatas,
+ )
+ assert results == ids
+ assert vectorstore.delete(ids)
+
+ # Wait for the documents to be indexed
+ time.sleep(SLEEP_DURATION)
+
+ output = vectorstore.similarity_search("foo", k=1)
+ assert len(output) == 0
+
+ def test_similarity_search_with_scores(self, cluster: Any) -> None:
+ """Test similarity search with scores."""
+
+ texts = ["foo", "bar", "baz"]
+
+ metadatas = [{"a": 1}, {"b": 2}, {"c": 3}]
+
+ vectorstore = CouchbaseVectorStore(
+ cluster=cluster,
+ embedding=ConsistentFakeEmbeddings(),
+ index_name=INDEX_NAME,
+ bucket_name=BUCKET_NAME,
+ scope_name=SCOPE_NAME,
+ collection_name=COLLECTION_NAME,
+ )
+
+ vectorstore.add_texts(texts, metadatas=metadatas)
+
+ # Wait for the documents to be indexed
+ time.sleep(SLEEP_DURATION)
+
+ output = vectorstore.similarity_search_with_score("foo", k=2)
+
+ assert len(output) == 2
+ assert output[0][0].page_content == "foo"
+
+ # check if the scores are sorted
+ assert output[0][0].metadata["a"] == 1
+ assert output[0][1] > output[1][1]
+
+ def test_similarity_search_by_vector(self, cluster: Any) -> None:
+ """Test similarity search by vector."""
+
+ texts = ["foo", "bar", "baz"]
+
+ metadatas = [{"a": 1}, {"b": 2}, {"c": 3}]
+
+ vectorstore = CouchbaseVectorStore(
+ cluster=cluster,
+ embedding=ConsistentFakeEmbeddings(),
+ index_name=INDEX_NAME,
+ bucket_name=BUCKET_NAME,
+ scope_name=SCOPE_NAME,
+ collection_name=COLLECTION_NAME,
+ )
+
+ vectorstore.add_texts(texts, metadatas=metadatas)
+
+ # Wait for the documents to be indexed
+ time.sleep(SLEEP_DURATION)
+
+ vector = ConsistentFakeEmbeddings().embed_query("foo")
+ vector_output = vectorstore.similarity_search_by_vector(vector, k=1)
+
+ assert vector_output[0].page_content == "foo"
+
+ similarity_output = vectorstore.similarity_search("foo", k=1)
+
+ assert similarity_output == vector_output
+
+ def test_output_fields(self, cluster: Any) -> None:
+ """Test that output fields are set correctly."""
+
+ texts = [
+ "foo",
+ "bar",
+ "baz",
+ ]
+
+ metadatas = [{"page": 1, "a": 1}, {"page": 2, "b": 2}, {"page": 3, "c": 3}]
+
+ vectorstore = CouchbaseVectorStore(
+ cluster=cluster,
+ embedding=ConsistentFakeEmbeddings(),
+ index_name=INDEX_NAME,
+ bucket_name=BUCKET_NAME,
+ scope_name=SCOPE_NAME,
+ collection_name=COLLECTION_NAME,
+ )
+
+ ids = vectorstore.add_texts(texts, metadatas)
+ assert len(ids) == len(texts)
+
+ # Wait for the documents to be indexed
+ time.sleep(SLEEP_DURATION)
+
+ output = vectorstore.similarity_search("foo", k=1, fields=["metadata.page"])
+ assert output[0].page_content == "foo"
+ assert output[0].metadata["page"] == 1
+ assert "a" not in output[0].metadata
+
+ def test_hybrid_search(self, cluster: Any) -> None:
+ """Test hybrid search."""
+
+ texts = [
+ "foo",
+ "bar",
+ "baz",
+ ]
+
+ metadatas = [
+ {"section": "index"},
+ {"section": "glossary"},
+ {"section": "appendix"},
+ ]
+
+ vectorstore = CouchbaseVectorStore(
+ cluster=cluster,
+ embedding=ConsistentFakeEmbeddings(),
+ index_name=INDEX_NAME,
+ bucket_name=BUCKET_NAME,
+ scope_name=SCOPE_NAME,
+ collection_name=COLLECTION_NAME,
+ )
+
+ vectorstore.add_texts(texts, metadatas=metadatas)
+
+ # Wait for the documents to be indexed
+ time.sleep(SLEEP_DURATION)
+
+ result, score = vectorstore.similarity_search_with_score("foo", k=1)[0]
+
+ # Wait for the documents to be indexed for hybrid search
+ time.sleep(SLEEP_DURATION)
+
+ hybrid_result, hybrid_score = vectorstore.similarity_search_with_score(
+ "foo",
+ k=1,
+ search_options={"query": {"match": "index", "field": "metadata.section"}},
+ )[0]
+
+ assert result == hybrid_result
+ assert score <= hybrid_score
diff --git a/libs/partners/couchbase/tests/unit_tests/__init__.py b/libs/partners/couchbase/tests/unit_tests/__init__.py
new file mode 100644
index 0000000000000..e69de29bb2d1d
diff --git a/libs/partners/couchbase/tests/unit_tests/test_imports.py b/libs/partners/couchbase/tests/unit_tests/test_imports.py
new file mode 100644
index 0000000000000..ec771d0ba2a17
--- /dev/null
+++ b/libs/partners/couchbase/tests/unit_tests/test_imports.py
@@ -0,0 +1,9 @@
+from langchain_couchbase import __all__
+
+EXPECTED_ALL = [
+ "CouchbaseVectorStore",
+]
+
+
+def test_all_imports() -> None:
+ assert sorted(EXPECTED_ALL) == sorted(__all__)
diff --git a/libs/partners/couchbase/tests/unit_tests/test_vectorstore.py b/libs/partners/couchbase/tests/unit_tests/test_vectorstore.py
new file mode 100644
index 0000000000000..8b137891791fe
--- /dev/null
+++ b/libs/partners/couchbase/tests/unit_tests/test_vectorstore.py
@@ -0,0 +1 @@
+
diff --git a/libs/partners/couchbase/tests/utils.py b/libs/partners/couchbase/tests/utils.py
new file mode 100644
index 0000000000000..d12df58d92b3f
--- /dev/null
+++ b/libs/partners/couchbase/tests/utils.py
@@ -0,0 +1,55 @@
+"""Fake Embedding class for testing purposes."""
+
+from typing import List
+
+from langchain_core.embeddings import Embeddings
+
+fake_texts = ["foo", "bar", "baz"]
+
+
+class FakeEmbeddings(Embeddings):
+ """Fake embeddings functionality for testing."""
+
+ def embed_documents(self, texts: List[str]) -> List[List[float]]:
+ """Return simple embeddings.
+ Embeddings encode each text as its index."""
+ return [[float(1.0)] * 9 + [float(i)] for i in range(len(texts))]
+
+ async def aembed_documents(self, texts: List[str]) -> List[List[float]]:
+ return self.embed_documents(texts)
+
+ def embed_query(self, text: str) -> List[float]:
+ """Return constant query embeddings.
+ Embeddings are identical to embed_documents(texts)[0].
+ Distance to each text will be that text's index,
+ as it was passed to embed_documents."""
+ return [float(1.0)] * 9 + [float(0.0)]
+
+ async def aembed_query(self, text: str) -> List[float]:
+ return self.embed_query(text)
+
+
+class ConsistentFakeEmbeddings(FakeEmbeddings):
+ """Fake embeddings which remember all the texts seen so far to return consistent
+ vectors for the same texts."""
+
+ def __init__(self, dimensionality: int = 10) -> None:
+ self.known_texts: List[str] = []
+ self.dimensionality = dimensionality
+
+ def embed_documents(self, texts: List[str]) -> List[List[float]]:
+ """Return consistent embeddings for each text seen so far."""
+ out_vectors = []
+ for text in texts:
+ if text not in self.known_texts:
+ self.known_texts.append(text)
+ vector = [float(1.0)] * (self.dimensionality - 1) + [
+ float(self.known_texts.index(text))
+ ]
+ out_vectors.append(vector)
+ return out_vectors
+
+ def embed_query(self, text: str) -> List[float]:
+ """Return consistent embeddings for the text, if seen before, or a constant
+ one if the text is unknown."""
+ return self.embed_documents([text])[0]
diff --git a/libs/partners/exa/pyproject.toml b/libs/partners/exa/pyproject.toml
index 96c66afcda88d..d4809b2a69003 100644
--- a/libs/partners/exa/pyproject.toml
+++ b/libs/partners/exa/pyproject.toml
@@ -1,7 +1,7 @@
[tool.poetry]
-name = "langchain-exa"
+name = "gigachain-exa"
version = "0.1.0"
-description = "An integration package connecting Exa and LangChain"
+description = "An integration package connecting Exa and Gigachain"
authors = []
readme = "README.md"
repository = "https://github.com/langchain-ai/langchain"
diff --git a/libs/partners/fireworks/pyproject.toml b/libs/partners/fireworks/pyproject.toml
index f3557d37f85cd..cc11d25044c58 100644
--- a/libs/partners/fireworks/pyproject.toml
+++ b/libs/partners/fireworks/pyproject.toml
@@ -1,7 +1,7 @@
[tool.poetry]
-name = "langchain-fireworks"
+name = "gigachain-fireworks"
version = "0.1.3"
-description = "An integration package connecting Fireworks and LangChain"
+description = "An integration package connecting Fireworks and Gigachain"
authors = []
readme = "README.md"
repository = "https://github.com/langchain-ai/langchain"
@@ -12,7 +12,7 @@ license = "MIT"
[tool.poetry.dependencies]
python = ">=3.8.1,<4.0"
-gigachain-core = ">=0.1.52,<0.3"
+gigachain-core = ">=0.2.2,<0.3"
fireworks-ai = ">=0.13.0"
openai = "^1.10.0"
requests = "^2"
diff --git a/libs/partners/groq/pyproject.toml b/libs/partners/groq/pyproject.toml
index c18d168cbd834..20e81c2f26f0b 100644
--- a/libs/partners/groq/pyproject.toml
+++ b/libs/partners/groq/pyproject.toml
@@ -1,18 +1,18 @@
[tool.poetry]
name = "gigachain-groq"
-version = "0.1.4"
-description = "An integration package connecting Groq and LangChain"
+version = "0.1.5"
+description = "An integration package connecting Groq and Gigachain"
authors = []
readme = "README.md"
-repository = "https://github.com/gigachain-ai/gigachain"
+repository = "https://github.com/langchain-ai/langchain"
license = "MIT"
[tool.poetry.urls]
-"Source Code" = "https://github.com/gigachain-ai/gigachain/tree/master/libs/partners/groq"
+"Source Code" = "https://github.com/langchain-ai/langchain/tree/master/libs/partners/groq"
[tool.poetry.dependencies]
python = ">=3.8.1,<4.0"
-gigachain-core = ">=0.1.45,<0.3"
+gigachain-core = ">=0.2.2,<0.3"
groq = ">=0.4.1,<1"
[tool.poetry.group.test]
@@ -92,5 +92,6 @@ filterwarnings = [
'ignore:The method `ChatGroq.with_structured_output` is in beta',
# Maintain support for pydantic 1.X
'default:The `dict` method is deprecated; use `model_dump` instead:DeprecationWarning',
+ "ignore:tool_choice='any' is not currently supported. Converting to 'auto'.",
]
asyncio_mode = "auto"
diff --git a/libs/partners/huggingface/pyproject.toml b/libs/partners/huggingface/pyproject.toml
index 918f660d40c29..fbe59d8f29028 100644
--- a/libs/partners/huggingface/pyproject.toml
+++ b/libs/partners/huggingface/pyproject.toml
@@ -1,7 +1,7 @@
[tool.poetry]
-name = "langchain-huggingface"
-version = "0.0.1"
-description = "An integration package connecting Hugging Face and LangChain"
+name = "gigachain-huggingface"
+version = "0.0.3"
+description = "An integration package connecting Hugging Face and Gigachain"
authors = []
readme = "README.md"
repository = "https://github.com/langchain-ai/langchain"
@@ -12,11 +12,10 @@ license = "MIT"
[tool.poetry.dependencies]
python = ">=3.8.1,<4.0"
-langchain-core = ">=0.1.52,<0.3"
+gigachain-core = ">=0.1.52,<0.3"
tokenizers = ">=0.19.1"
transformers = ">=4.39.0"
sentence-transformers = ">=2.6.0"
-text-generation = "^0.7.0"
huggingface-hub = ">=0.23.0"
[tool.poetry.group.test]
@@ -25,9 +24,18 @@ optional = true
[tool.poetry.group.test.dependencies]
pytest = "^7.3.0"
pytest-asyncio = "^0.21.1"
-langchain-core = { path = "../../core", develop = true }
-langchain-standard-tests = { path = "../../standard-tests", develop = true }
-langchain-community = { path = "../../community", develop = true }
+gigachain-core = { path = "../../core", develop = true }
+gigachain-standard-tests = { path = "../../standard-tests", develop = true }
+gigachain-community = { path = "../../community", develop = true }
+# Support Python 3.8 and 3.12+.
+scipy = [
+ {version = "^1", python = "<3.12"},
+ {version = "^1.7.0", python = ">=3.12"}
+]
+numpy = [
+ {version = "^1", python = "<3.12"},
+ {version = "^1.26.0", python = ">=3.12"}
+]
[tool.poetry.group.codespell]
optional = true
@@ -43,13 +51,13 @@ ruff = "^0.1.5"
[tool.poetry.group.typing.dependencies]
mypy = "^1"
-langchain-core = { path = "../../core", develop = true }
+gigachain-core = { path = "../../core", develop = true }
[tool.poetry.group.dev]
optional = true
[tool.poetry.group.dev.dependencies]
-langchain-core = { path = "../../core", develop = true }
+gigachain-core = { path = "../../core", develop = true }
ipykernel = "^6.29.2"
[tool.poetry.group.test_integration]
diff --git a/libs/partners/ibm/pyproject.toml b/libs/partners/ibm/pyproject.toml
index 7ffa56c5228bb..3a266b5b3f93e 100644
--- a/libs/partners/ibm/pyproject.toml
+++ b/libs/partners/ibm/pyproject.toml
@@ -1,7 +1,7 @@
[tool.poetry]
-name = "langchain-ibm"
+name = "gigachain-ibm"
version = "0.1.7"
-description = "An integration package connecting IBM watsonx.ai and LangChain"
+description = "An integration package connecting IBM watsonx.ai and Gigachain"
authors = ["IBM"]
readme = "README.md"
repository = "https://github.com/langchain-ai/langchain"
diff --git a/libs/partners/milvus/.gitignore b/libs/partners/milvus/.gitignore
new file mode 100644
index 0000000000000..bee8a64b79a99
--- /dev/null
+++ b/libs/partners/milvus/.gitignore
@@ -0,0 +1 @@
+__pycache__
diff --git a/libs/partners/milvus/LICENSE b/libs/partners/milvus/LICENSE
new file mode 100644
index 0000000000000..426b65090341f
--- /dev/null
+++ b/libs/partners/milvus/LICENSE
@@ -0,0 +1,21 @@
+MIT License
+
+Copyright (c) 2023 LangChain, Inc.
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
diff --git a/libs/partners/milvus/Makefile b/libs/partners/milvus/Makefile
new file mode 100644
index 0000000000000..263896e6e0a0b
--- /dev/null
+++ b/libs/partners/milvus/Makefile
@@ -0,0 +1,57 @@
+.PHONY: all format lint test tests integration_tests docker_tests help extended_tests
+
+# Default target executed when no arguments are given to make.
+all: help
+
+# Define a variable for the test file path.
+TEST_FILE ?= tests/unit_tests/
+integration_test integration_tests: TEST_FILE=tests/integration_tests/
+
+test tests integration_test integration_tests:
+ poetry run pytest $(TEST_FILE)
+
+
+######################
+# LINTING AND FORMATTING
+######################
+
+# Define a variable for Python and notebook files.
+PYTHON_FILES=.
+MYPY_CACHE=.mypy_cache
+lint format: PYTHON_FILES=.
+lint_diff format_diff: PYTHON_FILES=$(shell git diff --relative=libs/partners/milvus --name-only --diff-filter=d master | grep -E '\.py$$|\.ipynb$$')
+lint_package: PYTHON_FILES=langchain_milvus
+lint_tests: PYTHON_FILES=tests
+lint_tests: MYPY_CACHE=.mypy_cache_test
+
+lint lint_diff lint_package lint_tests:
+ poetry run ruff .
+ poetry run ruff format $(PYTHON_FILES) --diff
+ poetry run ruff --select I $(PYTHON_FILES)
+ mkdir $(MYPY_CACHE); poetry run mypy $(PYTHON_FILES) --cache-dir $(MYPY_CACHE)
+
+format format_diff:
+ poetry run ruff format $(PYTHON_FILES)
+ poetry run ruff --select I --fix $(PYTHON_FILES)
+
+spell_check:
+ poetry run codespell --toml pyproject.toml
+
+spell_fix:
+ poetry run codespell --toml pyproject.toml -w
+
+check_imports: $(shell find langchain_milvus -name '*.py')
+ poetry run python ./scripts/check_imports.py $^
+
+######################
+# HELP
+######################
+
+help:
+ @echo '----'
+ @echo 'check_imports - check imports'
+ @echo 'format - run code formatters'
+ @echo 'lint - run linters'
+ @echo 'test - run unit tests'
+ @echo 'tests - run unit tests'
+ @echo 'test TEST_FILE= - run all tests in file'
diff --git a/libs/partners/milvus/README.md b/libs/partners/milvus/README.md
new file mode 100644
index 0000000000000..80820f32d1b6a
--- /dev/null
+++ b/libs/partners/milvus/README.md
@@ -0,0 +1,42 @@
+# langchain-milvus
+
+This is a library integration with [Milvus](https://milvus.io/) and [Zilliz Cloud](https://zilliz.com/cloud).
+
+## Installation
+
+```bash
+pip install -U langchain-milvus
+```
+
+## Milvus vector database
+
+See a [usage example](https://python.langchain.com/v0.2/docs/integrations/vectorstores/milvus/)
+
+```python
+from langchain_milvus import Milvus
+```
+
+## Milvus hybrid search
+
+See a [usage example](https://python.langchain.com/v0.2/docs/integrations/retrievers/milvus_hybrid_search/).
+
+```python
+from langchain_milvus import MilvusCollectionHybridSearchRetriever
+```
+
+
+## Zilliz Cloud vector database
+
+See a [usage example](https://python.langchain.com/v0.2/docs/integrations/vectorstores/zilliz/).
+
+```python
+from langchain_milvus import Zilliz
+```
+
+## Zilliz Cloud Pipeline Retriever
+
+See a [usage example](https://python.langchain.com/v0.2/docs/integrations/retrievers/zilliz_cloud_pipeline/).
+
+```python
+from langchain_milvus import ZillizCloudPipelineRetriever
+```
\ No newline at end of file
diff --git a/libs/partners/milvus/langchain_milvus/__init__.py b/libs/partners/milvus/langchain_milvus/__init__.py
new file mode 100644
index 0000000000000..b19bc1d7e697a
--- /dev/null
+++ b/libs/partners/milvus/langchain_milvus/__init__.py
@@ -0,0 +1,12 @@
+from langchain_milvus.retrievers import (
+ MilvusCollectionHybridSearchRetriever,
+ ZillizCloudPipelineRetriever,
+)
+from langchain_milvus.vectorstores import Milvus, Zilliz
+
+__all__ = [
+ "Milvus",
+ "Zilliz",
+ "ZillizCloudPipelineRetriever",
+ "MilvusCollectionHybridSearchRetriever",
+]
diff --git a/libs/partners/milvus/langchain_milvus/py.typed b/libs/partners/milvus/langchain_milvus/py.typed
new file mode 100644
index 0000000000000..e69de29bb2d1d
diff --git a/libs/partners/milvus/langchain_milvus/retrievers/__init__.py b/libs/partners/milvus/langchain_milvus/retrievers/__init__.py
new file mode 100644
index 0000000000000..1edac3cb5af73
--- /dev/null
+++ b/libs/partners/milvus/langchain_milvus/retrievers/__init__.py
@@ -0,0 +1,8 @@
+from langchain_milvus.retrievers.milvus_hybrid_search import (
+ MilvusCollectionHybridSearchRetriever,
+)
+from langchain_milvus.retrievers.zilliz_cloud_pipeline_retriever import (
+ ZillizCloudPipelineRetriever,
+)
+
+__all__ = ["ZillizCloudPipelineRetriever", "MilvusCollectionHybridSearchRetriever"]
diff --git a/libs/partners/milvus/langchain_milvus/retrievers/milvus_hybrid_search.py b/libs/partners/milvus/langchain_milvus/retrievers/milvus_hybrid_search.py
new file mode 100644
index 0000000000000..6b6e692dd2c21
--- /dev/null
+++ b/libs/partners/milvus/langchain_milvus/retrievers/milvus_hybrid_search.py
@@ -0,0 +1,160 @@
+from typing import Any, Dict, List, Optional, Union
+
+from langchain_core.callbacks import CallbackManagerForRetrieverRun
+from langchain_core.documents import Document
+from langchain_core.embeddings import Embeddings
+from langchain_core.retrievers import BaseRetriever
+from pymilvus import AnnSearchRequest, Collection
+from pymilvus.client.abstract import BaseRanker, SearchResult # type: ignore
+
+from langchain_milvus.utils.sparse import BaseSparseEmbedding
+
+
+class MilvusCollectionHybridSearchRetriever(BaseRetriever):
+ """This is a hybrid search retriever
+ that uses Milvus Collection to retrieve documents based on multiple fields.
+ For more information, please refer to:
+ https://milvus.io/docs/release_notes.md#Multi-Embedding---Hybrid-Search
+ """
+
+ collection: Collection
+ """Milvus Collection object."""
+ rerank: BaseRanker
+ """Milvus ranker object. Such as WeightedRanker or RRFRanker."""
+ anns_fields: List[str]
+ """The names of vector fields that are used for ANNS search."""
+ field_embeddings: List[Union[Embeddings, BaseSparseEmbedding]]
+ """The embedding functions of each vector fields,
+ which can be either Embeddings or BaseSparseEmbedding."""
+ field_search_params: Optional[List[Dict]] = None
+ """The search parameters of each vector fields.
+ If not specified, the default search parameters will be used."""
+ field_limits: Optional[List[int]] = None
+ """Limit number of results for each ANNS field.
+ If not specified, the default top_k will be used."""
+ field_exprs: Optional[List[Optional[str]]] = None
+ """The boolean expression for filtering the search results."""
+ top_k: int = 4
+ """Final top-K number of documents to retrieve."""
+ text_field: str = "text"
+ """The text field name,
+ which will be used as the `page_content` of a `Document` object."""
+ output_fields: Optional[List[str]] = None
+ """Final output fields of the documents.
+ If not specified, all fields except the vector fields will be used as output fields,
+ which will be the `metadata` of a `Document` object."""
+
+ def __init__(self, **kwargs: Any):
+ super().__init__(**kwargs)
+
+ # If some parameters are not specified, set default values
+ if self.field_search_params is None:
+ default_search_params = {
+ "metric_type": "L2",
+ "params": {"nprobe": 10},
+ }
+ self.field_search_params = [default_search_params] * len(self.anns_fields)
+ if self.field_limits is None:
+ self.field_limits = [self.top_k] * len(self.anns_fields)
+ if self.field_exprs is None:
+ self.field_exprs = [None] * len(self.anns_fields)
+
+ # Check the fields
+ self._validate_fields_num()
+ self.output_fields = self._get_output_fields()
+ self._validate_fields_name()
+
+ # Load collection
+ self.collection.load()
+
+ def _validate_fields_num(self) -> None:
+ assert (
+ len(self.anns_fields) >= 2
+ ), "At least two fields are required for hybrid search."
+ lengths = [len(self.anns_fields)]
+ if self.field_limits is not None:
+ lengths.append(len(self.field_limits))
+ if self.field_exprs is not None:
+ lengths.append(len(self.field_exprs))
+
+ if not all(length == lengths[0] for length in lengths):
+ raise ValueError("All field-related lists must have the same length.")
+
+ if len(self.field_search_params) != len(self.anns_fields): # type: ignore[arg-type]
+ raise ValueError(
+ "field_search_params must have the same length as anns_fields."
+ )
+
+ def _validate_fields_name(self) -> None:
+ collection_fields = [x.name for x in self.collection.schema.fields]
+ for field in self.anns_fields:
+ assert (
+ field in collection_fields
+ ), f"{field} is not a valid field in the collection."
+ assert (
+ self.text_field in collection_fields
+ ), f"{self.text_field} is not a valid field in the collection."
+ for field in self.output_fields: # type: ignore[union-attr]
+ assert (
+ field in collection_fields
+ ), f"{field} is not a valid field in the collection."
+
+ def _get_output_fields(self) -> List[str]:
+ if self.output_fields:
+ return self.output_fields
+ output_fields = [x.name for x in self.collection.schema.fields]
+ for field in self.anns_fields:
+ if field in output_fields:
+ output_fields.remove(field)
+ if self.text_field not in output_fields:
+ output_fields.append(self.text_field)
+ return output_fields
+
+ def _build_ann_search_requests(self, query: str) -> List[AnnSearchRequest]:
+ search_requests = []
+ for ann_field, embedding, param, limit, expr in zip(
+ self.anns_fields,
+ self.field_embeddings,
+ self.field_search_params, # type: ignore[arg-type]
+ self.field_limits, # type: ignore[arg-type]
+ self.field_exprs, # type: ignore[arg-type]
+ ):
+ request = AnnSearchRequest(
+ data=[embedding.embed_query(query)],
+ anns_field=ann_field,
+ param=param,
+ limit=limit,
+ expr=expr,
+ )
+ search_requests.append(request)
+ return search_requests
+
+ def _parse_document(self, data: dict) -> Document:
+ return Document(
+ page_content=data.pop(self.text_field),
+ metadata=data,
+ )
+
+ def _process_search_result(
+ self, search_results: List[SearchResult]
+ ) -> List[Document]:
+ documents = []
+ for result in search_results[0]:
+ data = {x: result.entity.get(x) for x in self.output_fields} # type: ignore[union-attr]
+ doc = self._parse_document(data)
+ documents.append(doc)
+ return documents
+
+ def _get_relevant_documents(
+ self,
+ query: str,
+ *,
+ run_manager: CallbackManagerForRetrieverRun,
+ **kwargs: Any,
+ ) -> List[Document]:
+ requests = self._build_ann_search_requests(query)
+ search_result = self.collection.hybrid_search(
+ requests, self.rerank, limit=self.top_k, output_fields=self.output_fields
+ )
+ documents = self._process_search_result(search_result)
+ return documents
diff --git a/libs/partners/milvus/langchain_milvus/retrievers/zilliz_cloud_pipeline_retriever.py b/libs/partners/milvus/langchain_milvus/retrievers/zilliz_cloud_pipeline_retriever.py
new file mode 100644
index 0000000000000..6fbccfa47fa7c
--- /dev/null
+++ b/libs/partners/milvus/langchain_milvus/retrievers/zilliz_cloud_pipeline_retriever.py
@@ -0,0 +1,215 @@
+from typing import Any, Dict, List, Optional
+
+import requests
+from langchain_core.callbacks.manager import CallbackManagerForRetrieverRun
+from langchain_core.documents import Document
+from langchain_core.retrievers import BaseRetriever
+
+
+class ZillizCloudPipelineRetriever(BaseRetriever):
+ """`Zilliz Cloud Pipeline` retriever
+
+ Args:
+ pipeline_ids (dict): A dictionary of pipeline ids.
+ Valid keys: "ingestion", "search", "deletion".
+ token (str): Zilliz Cloud's token. Defaults to "".
+ cloud_region (str='gcp-us-west1'): The region of Zilliz Cloud's cluster.
+ Defaults to 'gcp-us-west1'.
+ """
+
+ pipeline_ids: Dict
+ token: str = ""
+ cloud_region: str = "gcp-us-west1"
+
+ def _get_relevant_documents(
+ self,
+ query: str,
+ top_k: int = 10,
+ offset: int = 0,
+ output_fields: List = [],
+ filter: str = "",
+ *,
+ run_manager: CallbackManagerForRetrieverRun,
+ ) -> List[Document]:
+ """
+ Get documents relevant to a query.
+
+ Args:
+ query (str): String to find relevant documents for
+ top_k (int=10): The number of results. Defaults to 10.
+ offset (int=0): The number of records to skip in the search result.
+ Defaults to 0.
+ output_fields (list=[]): The extra fields to present in output.
+ filter (str=""): The Milvus expression to filter search results.
+ Defaults to "".
+ run_manager (CallBackManagerForRetrieverRun): The callbacks handler to use.
+
+ Returns:
+ List of relevant documents
+ """
+ if "search" in self.pipeline_ids:
+ search_pipe_id = self.pipeline_ids.get("search")
+ else:
+ raise Exception(
+ "A search pipeline id must be provided in pipeline_ids to "
+ "get relevant documents."
+ )
+ domain = (
+ f"https://controller.api.{self.cloud_region}.zillizcloud.com/v1/pipelines"
+ )
+ headers = {
+ "Authorization": f"Bearer {self.token}",
+ "Accept": "application/json",
+ "Content-Type": "application/json",
+ }
+ url = f"{domain}/{search_pipe_id}/run"
+
+ params = {
+ "data": {"query_text": query},
+ "params": {
+ "limit": top_k,
+ "offset": offset,
+ "outputFields": output_fields,
+ "filter": filter,
+ },
+ }
+
+ response = requests.post(url, headers=headers, json=params)
+ if response.status_code != 200:
+ raise RuntimeError(response.text)
+ response_dict = response.json()
+ if response_dict["code"] != 200:
+ raise RuntimeError(response_dict)
+ response_data = response_dict["data"]
+ search_results = response_data["result"]
+ return [
+ Document(
+ page_content=result.pop("text")
+ if "text" in result
+ else result.pop("chunk_text"),
+ metadata=result,
+ )
+ for result in search_results
+ ]
+
+ def add_texts(
+ self, texts: List[str], metadata: Optional[Dict[str, Any]] = None
+ ) -> Dict:
+ """
+ Add documents to store.
+ Only supported by a text ingestion pipeline in Zilliz Cloud.
+
+ Args:
+ texts (List[str]): A list of text strings.
+ metadata (Dict[str, Any]): A key-value dictionary of metadata will
+ be inserted as preserved fields required by ingestion pipeline.
+ Defaults to None.
+ """
+ if "ingestion" in self.pipeline_ids:
+ ingeset_pipe_id = self.pipeline_ids.get("ingestion")
+ else:
+ raise Exception(
+ "An ingestion pipeline id must be provided in pipeline_ids to"
+ " add documents."
+ )
+ domain = (
+ f"https://controller.api.{self.cloud_region}.zillizcloud.com/v1/pipelines"
+ )
+ headers = {
+ "Authorization": f"Bearer {self.token}",
+ "Accept": "application/json",
+ "Content-Type": "application/json",
+ }
+ url = f"{domain}/{ingeset_pipe_id}/run"
+
+ metadata = {} if metadata is None else metadata
+ params = {"data": {"text_list": texts}}
+ params["data"].update(metadata)
+
+ response = requests.post(url, headers=headers, json=params)
+ if response.status_code != 200:
+ raise Exception(response.text)
+ response_dict = response.json()
+ if response_dict["code"] != 200:
+ raise Exception(response_dict)
+ response_data = response_dict["data"]
+ return response_data
+
+ def add_doc_url(
+ self, doc_url: str, metadata: Optional[Dict[str, Any]] = None
+ ) -> Dict:
+ """
+ Add a document from url.
+ Only supported by a document ingestion pipeline in Zilliz Cloud.
+
+ Args:
+ doc_url: A document url.
+ metadata (Dict[str, Any]): A key-value dictionary of metadata will
+ be inserted as preserved fields required by ingestion pipeline.
+ Defaults to None.
+ """
+ if "ingestion" in self.pipeline_ids:
+ ingest_pipe_id = self.pipeline_ids.get("ingestion")
+ else:
+ raise Exception(
+ "An ingestion pipeline id must be provided in pipeline_ids to "
+ "add documents."
+ )
+ domain = (
+ f"https://controller.api.{self.cloud_region}.zillizcloud.com/v1/pipelines"
+ )
+ headers = {
+ "Authorization": f"Bearer {self.token}",
+ "Accept": "application/json",
+ "Content-Type": "application/json",
+ }
+ url = f"{domain}/{ingest_pipe_id}/run"
+
+ params = {"data": {"doc_url": doc_url}}
+ metadata = {} if metadata is None else metadata
+ params["data"].update(metadata)
+
+ response = requests.post(url, headers=headers, json=params)
+ if response.status_code != 200:
+ raise Exception(response.text)
+ response_dict = response.json()
+ if response_dict["code"] != 200:
+ raise Exception(response_dict)
+ response_data = response_dict["data"]
+ return response_data
+
+ def delete(self, key: str, value: Any) -> Dict:
+ """
+ Delete documents. Only supported by a deletion pipeline in Zilliz Cloud.
+
+ Args:
+ key: input name to run the deletion pipeline
+ value: input value to run deletion pipeline
+ """
+ if "deletion" in self.pipeline_ids:
+ deletion_pipe_id = self.pipeline_ids.get("deletion")
+ else:
+ raise Exception(
+ "A deletion pipeline id must be provided in pipeline_ids to "
+ "add documents."
+ )
+ domain = (
+ f"https://controller.api.{self.cloud_region}.zillizcloud.com/v1/pipelines"
+ )
+ headers = {
+ "Authorization": f"Bearer {self.token}",
+ "Accept": "application/json",
+ "Content-Type": "application/json",
+ }
+ url = f"{domain}/{deletion_pipe_id}/run"
+
+ params = {"data": {key: value}}
+
+ response = requests.post(url, headers=headers, json=params)
+ if response.status_code != 200:
+ raise Exception(response.text)
+ response_dict = response.json()
+ if response_dict["code"] != 200:
+ raise Exception(response_dict)
+ response_data = response_dict["data"]
+ return response_data
diff --git a/libs/partners/milvus/langchain_milvus/utils/__init__.py b/libs/partners/milvus/langchain_milvus/utils/__init__.py
new file mode 100644
index 0000000000000..e69de29bb2d1d
diff --git a/libs/partners/milvus/langchain_milvus/utils/sparse.py b/libs/partners/milvus/langchain_milvus/utils/sparse.py
new file mode 100644
index 0000000000000..027c978c65d75
--- /dev/null
+++ b/libs/partners/milvus/langchain_milvus/utils/sparse.py
@@ -0,0 +1,54 @@
+from abc import ABC, abstractmethod
+from typing import Dict, List
+
+from scipy.sparse import csr_array # type: ignore
+
+
+class BaseSparseEmbedding(ABC):
+ """Interface for Sparse embedding models.
+ You can inherit from it and implement your custom sparse embedding model.
+ """
+
+ @abstractmethod
+ def embed_query(self, query: str) -> Dict[int, float]:
+ """Embed query text."""
+
+ @abstractmethod
+ def embed_documents(self, texts: List[str]) -> List[Dict[int, float]]:
+ """Embed search docs."""
+
+
+class BM25SparseEmbedding(BaseSparseEmbedding):
+ """This is a class that inherits BaseSparseEmbedding
+ and implements a sparse vector embedding model based on BM25.
+ This class uses the BM25 model in Milvus model to implement sparse vector embedding.
+ This model requires pymilvus[model] to be installed.
+ `pip install pymilvus[model]`
+ For more information please refer to:
+ https://milvus.io/docs/embed-with-bm25.md
+ """
+
+ def __init__(self, corpus: List[str], language: str = "en"):
+ from pymilvus.model.sparse import BM25EmbeddingFunction # type: ignore
+ from pymilvus.model.sparse.bm25.tokenizers import ( # type: ignore
+ build_default_analyzer,
+ )
+
+ self.analyzer = build_default_analyzer(language=language)
+ self.bm25_ef = BM25EmbeddingFunction(self.analyzer, num_workers=1)
+ self.bm25_ef.fit(corpus)
+
+ def embed_query(self, text: str) -> Dict[int, float]:
+ return self._sparse_to_dict(self.bm25_ef.encode_queries([text]))
+
+ def embed_documents(self, texts: List[str]) -> List[Dict[int, float]]:
+ sparse_arrays = self.bm25_ef.encode_documents(texts)
+ return [self._sparse_to_dict(sparse_array) for sparse_array in sparse_arrays]
+
+ def _sparse_to_dict(self, sparse_array: csr_array) -> Dict[int, float]:
+ row_indices, col_indices = sparse_array.nonzero()
+ non_zero_values = sparse_array.data
+ result_dict = {}
+ for col_index, value in zip(col_indices, non_zero_values):
+ result_dict[col_index] = value
+ return result_dict
diff --git a/libs/partners/milvus/langchain_milvus/vectorstores/__init__.py b/libs/partners/milvus/langchain_milvus/vectorstores/__init__.py
new file mode 100644
index 0000000000000..5c6f304db98cb
--- /dev/null
+++ b/libs/partners/milvus/langchain_milvus/vectorstores/__init__.py
@@ -0,0 +1,7 @@
+from langchain_milvus.vectorstores.milvus import Milvus
+from langchain_milvus.vectorstores.zilliz import Zilliz
+
+__all__ = [
+ "Milvus",
+ "Zilliz",
+]
diff --git a/libs/partners/milvus/langchain_milvus/vectorstores/milvus.py b/libs/partners/milvus/langchain_milvus/vectorstores/milvus.py
new file mode 100644
index 0000000000000..78bf7a98527de
--- /dev/null
+++ b/libs/partners/milvus/langchain_milvus/vectorstores/milvus.py
@@ -0,0 +1,1146 @@
+from __future__ import annotations
+
+import logging
+from typing import Any, Iterable, List, Optional, Tuple, Union
+from uuid import uuid4
+
+import numpy as np
+from langchain_core.documents import Document
+from langchain_core.embeddings import Embeddings
+from langchain_core.vectorstores import VectorStore
+
+logger = logging.getLogger(__name__)
+
+DEFAULT_MILVUS_CONNECTION = {
+ "uri": "http://localhost:19530",
+}
+
+Matrix = Union[List[List[float]], List[np.ndarray], np.ndarray]
+
+
+def cosine_similarity(X: Matrix, Y: Matrix) -> np.ndarray:
+ """Row-wise cosine similarity between two equal-width matrices."""
+ if len(X) == 0 or len(Y) == 0:
+ return np.array([])
+
+ X = np.array(X)
+ Y = np.array(Y)
+ if X.shape[1] != Y.shape[1]:
+ raise ValueError(
+ f"Number of columns in X and Y must be the same. X has shape {X.shape} "
+ f"and Y has shape {Y.shape}."
+ )
+ try:
+ import simsimd as simd # type: ignore
+
+ X = np.array(X, dtype=np.float32)
+ Y = np.array(Y, dtype=np.float32)
+ Z = 1 - np.array(simd.cdist(X, Y, metric="cosine"))
+ return Z
+ except ImportError:
+ logger.debug(
+ "Unable to import simsimd, defaulting to NumPy implementation. If you want "
+ "to use simsimd please install with `pip install simsimd`."
+ )
+ X_norm = np.linalg.norm(X, axis=1)
+ Y_norm = np.linalg.norm(Y, axis=1)
+ # Ignore divide by zero errors run time warnings as those are handled below.
+ with np.errstate(divide="ignore", invalid="ignore"):
+ similarity = np.dot(X, Y.T) / np.outer(X_norm, Y_norm)
+ similarity[np.isnan(similarity) | np.isinf(similarity)] = 0.0
+ return similarity
+
+
+def maximal_marginal_relevance(
+ query_embedding: np.ndarray,
+ embedding_list: list,
+ lambda_mult: float = 0.5,
+ k: int = 4,
+) -> List[int]:
+ """Calculate maximal marginal relevance."""
+ if min(k, len(embedding_list)) <= 0:
+ return []
+ if query_embedding.ndim == 1:
+ query_embedding = np.expand_dims(query_embedding, axis=0)
+ similarity_to_query = cosine_similarity(query_embedding, embedding_list)[0]
+ most_similar = int(np.argmax(similarity_to_query))
+ idxs = [most_similar]
+ selected = np.array([embedding_list[most_similar]])
+ while len(idxs) < min(k, len(embedding_list)):
+ best_score = -np.inf
+ idx_to_add = -1
+ similarity_to_selected = cosine_similarity(embedding_list, selected)
+ for i, query_score in enumerate(similarity_to_query):
+ if i in idxs:
+ continue
+ redundant_score = max(similarity_to_selected[i])
+ equation_score = (
+ lambda_mult * query_score - (1 - lambda_mult) * redundant_score
+ )
+ if equation_score > best_score:
+ best_score = equation_score
+ idx_to_add = i
+ idxs.append(idx_to_add)
+ selected = np.append(selected, [embedding_list[idx_to_add]], axis=0)
+ return idxs
+
+
+class Milvus(VectorStore):
+ """`Milvus` vector store.
+
+ You need to install `pymilvus` and run Milvus.
+
+ See the following documentation for how to run a Milvus instance:
+ https://milvus.io/docs/install_standalone-docker.md
+
+ If looking for a hosted Milvus, take a look at this documentation:
+ https://zilliz.com/cloud and make use of the Zilliz vectorstore found in
+ this project.
+
+ IF USING L2/IP metric, IT IS HIGHLY SUGGESTED TO NORMALIZE YOUR DATA.
+
+ Args:
+ embedding_function (Embeddings): Function used to embed the text.
+ collection_name (str): Which Milvus collection to use. Defaults to
+ "LangChainCollection".
+ collection_description (str): The description of the collection. Defaults to
+ "".
+ collection_properties (Optional[dict[str, any]]): The collection properties.
+ Defaults to None.
+ If set, will override collection existing properties.
+ For example: {"collection.ttl.seconds": 60}.
+ connection_args (Optional[dict[str, any]]): The connection args used for
+ this class comes in the form of a dict.
+ consistency_level (str): The consistency level to use for a collection.
+ Defaults to "Session".
+ index_params (Optional[dict]): Which index params to use. Defaults to
+ HNSW/AUTOINDEX depending on service.
+ search_params (Optional[dict]): Which search params to use. Defaults to
+ default of index.
+ drop_old (Optional[bool]): Whether to drop the current collection. Defaults
+ to False.
+ auto_id (bool): Whether to enable auto id for primary key. Defaults to False.
+ If False, you needs to provide text ids (string less than 65535 bytes).
+ If True, Milvus will generate unique integers as primary keys.
+ primary_field (str): Name of the primary key field. Defaults to "pk".
+ text_field (str): Name of the text field. Defaults to "text".
+ vector_field (str): Name of the vector field. Defaults to "vector".
+ metadata_field (str): Name of the metadta field. Defaults to None.
+ When metadata_field is specified,
+ the document's metadata will store as json.
+
+ The connection args used for this class comes in the form of a dict,
+ here are a few of the options:
+ address (str): The actual address of Milvus
+ instance. Example address: "localhost:19530"
+ uri (str): The uri of Milvus instance. Example uri:
+ "http://randomwebsite:19530",
+ "tcp:foobarsite:19530",
+ "https://ok.s3.south.com:19530".
+ or "path/to/local/directory/milvus_demo.db" for Milvus Lite.
+ host (str): The host of Milvus instance. Default at "localhost",
+ PyMilvus will fill in the default host if only port is provided.
+ port (str/int): The port of Milvus instance. Default at 19530, PyMilvus
+ will fill in the default port if only host is provided.
+ user (str): Use which user to connect to Milvus instance. If user and
+ password are provided, we will add related header in every RPC call.
+ password (str): Required when user is provided. The password
+ corresponding to the user.
+ secure (bool): Default is false. If set to true, tls will be enabled.
+ client_key_path (str): If use tls two-way authentication, need to
+ write the client.key path.
+ client_pem_path (str): If use tls two-way authentication, need to
+ write the client.pem path.
+ ca_pem_path (str): If use tls two-way authentication, need to write
+ the ca.pem path.
+ server_pem_path (str): If use tls one-way authentication, need to
+ write the server.pem path.
+ server_name (str): If use tls, need to write the common name.
+
+ Example:
+ .. code-block:: python
+
+ from langchain_milvus.vectorstores import Milvus
+ from langchain_openai.embeddings import OpenAIEmbeddings
+
+ embedding = OpenAIEmbeddings()
+ # Connect to a milvus instance on localhost
+ milvus_store = Milvus(
+ embedding_function = Embeddings,
+ collection_name = "LangChainCollection",
+ drop_old = True,
+ auto_id = True
+ )
+
+ Raises:
+ ValueError: If the pymilvus python package is not installed.
+ """
+
+ def __init__(
+ self,
+ embedding_function: Embeddings,
+ collection_name: str = "LangChainCollection",
+ collection_description: str = "",
+ collection_properties: Optional[dict[str, Any]] = None,
+ connection_args: Optional[dict[str, Any]] = None,
+ consistency_level: str = "Session",
+ index_params: Optional[dict] = None,
+ search_params: Optional[dict] = None,
+ drop_old: Optional[bool] = False,
+ auto_id: bool = False,
+ *,
+ primary_field: str = "pk",
+ text_field: str = "text",
+ vector_field: str = "vector",
+ metadata_field: Optional[str] = None,
+ partition_key_field: Optional[str] = None,
+ partition_names: Optional[list] = None,
+ replica_number: int = 1,
+ timeout: Optional[float] = None,
+ num_shards: Optional[int] = None,
+ ):
+ """Initialize the Milvus vector store."""
+ try:
+ from pymilvus import Collection, utility
+ except ImportError:
+ raise ValueError(
+ "Could not import pymilvus python package. "
+ "Please install it with `pip install pymilvus`."
+ )
+
+ # Default search params when one is not provided.
+ self.default_search_params = {
+ "IVF_FLAT": {"metric_type": "L2", "params": {"nprobe": 10}},
+ "IVF_SQ8": {"metric_type": "L2", "params": {"nprobe": 10}},
+ "IVF_PQ": {"metric_type": "L2", "params": {"nprobe": 10}},
+ "HNSW": {"metric_type": "L2", "params": {"ef": 10}},
+ "RHNSW_FLAT": {"metric_type": "L2", "params": {"ef": 10}},
+ "RHNSW_SQ": {"metric_type": "L2", "params": {"ef": 10}},
+ "RHNSW_PQ": {"metric_type": "L2", "params": {"ef": 10}},
+ "IVF_HNSW": {"metric_type": "L2", "params": {"nprobe": 10, "ef": 10}},
+ "ANNOY": {"metric_type": "L2", "params": {"search_k": 10}},
+ "SCANN": {"metric_type": "L2", "params": {"search_k": 10}},
+ "AUTOINDEX": {"metric_type": "L2", "params": {}},
+ "GPU_CAGRA": {
+ "metric_type": "L2",
+ "params": {
+ "itopk_size": 128,
+ "search_width": 4,
+ "min_iterations": 0,
+ "max_iterations": 0,
+ "team_size": 0,
+ },
+ },
+ "GPU_IVF_FLAT": {"metric_type": "L2", "params": {"nprobe": 10}},
+ "GPU_IVF_PQ": {"metric_type": "L2", "params": {"nprobe": 10}},
+ }
+
+ self.embedding_func = embedding_function
+ self.collection_name = collection_name
+ self.collection_description = collection_description
+ self.collection_properties = collection_properties
+ self.index_params = index_params
+ self.search_params = search_params
+ self.consistency_level = consistency_level
+ self.auto_id = auto_id
+
+ # In order for a collection to be compatible, pk needs to be varchar
+ self._primary_field = primary_field
+ # In order for compatibility, the text field will need to be called "text"
+ self._text_field = text_field
+ # In order for compatibility, the vector field needs to be called "vector"
+ self._vector_field = vector_field
+ self._metadata_field = metadata_field
+ self._partition_key_field = partition_key_field
+ self.fields: list[str] = []
+ self.partition_names = partition_names
+ self.replica_number = replica_number
+ self.timeout = timeout
+ self.num_shards = num_shards
+
+ # Create the connection to the server
+ if connection_args is None:
+ connection_args = DEFAULT_MILVUS_CONNECTION
+ self.alias = self._create_connection_alias(connection_args)
+ self.col: Optional[Collection] = None
+
+ # Grab the existing collection if it exists
+ if utility.has_collection(self.collection_name, using=self.alias):
+ self.col = Collection(
+ self.collection_name,
+ using=self.alias,
+ )
+ if self.collection_properties is not None:
+ self.col.set_properties(self.collection_properties)
+ # If need to drop old, drop it
+ if drop_old and isinstance(self.col, Collection):
+ self.col.drop()
+ self.col = None
+
+ # Initialize the vector store
+ self._init(
+ partition_names=partition_names,
+ replica_number=replica_number,
+ timeout=timeout,
+ )
+
+ @property
+ def embeddings(self) -> Embeddings:
+ return self.embedding_func
+
+ def _create_connection_alias(self, connection_args: dict) -> str:
+ """Create the connection to the Milvus server."""
+ from pymilvus import MilvusException, connections
+
+ # Grab the connection arguments that are used for checking existing connection
+ host: str = connection_args.get("host", None)
+ port: Union[str, int] = connection_args.get("port", None)
+ address: str = connection_args.get("address", None)
+ uri: str = connection_args.get("uri", None)
+ user = connection_args.get("user", None)
+
+ # Order of use is host/port, uri, address
+ if host is not None and port is not None:
+ given_address = str(host) + ":" + str(port)
+ elif uri is not None:
+ if uri.startswith("https://"):
+ given_address = uri.split("https://")[1]
+ elif uri.startswith("http://"):
+ given_address = uri.split("http://")[1]
+ else:
+ given_address = uri # Milvus lite
+ elif address is not None:
+ given_address = address
+ else:
+ given_address = None
+ logger.debug("Missing standard address type for reuse attempt")
+
+ # User defaults to empty string when getting connection info
+ if user is not None:
+ tmp_user = user
+ else:
+ tmp_user = ""
+
+ # If a valid address was given, then check if a connection exists
+ if given_address is not None:
+ for con in connections.list_connections():
+ addr = connections.get_connection_addr(con[0])
+ if (
+ con[1]
+ and ("address" in addr)
+ and (addr["address"] == given_address)
+ and ("user" in addr)
+ and (addr["user"] == tmp_user)
+ ):
+ logger.debug("Using previous connection: %s", con[0])
+ return con[0]
+
+ # Generate a new connection if one doesn't exist
+ alias = uuid4().hex
+ try:
+ connections.connect(alias=alias, **connection_args)
+ logger.debug("Created new connection using: %s", alias)
+ return alias
+ except MilvusException as e:
+ logger.error("Failed to create new connection using: %s", alias)
+ raise e
+
+ def _init(
+ self,
+ embeddings: Optional[list] = None,
+ metadatas: Optional[list[dict]] = None,
+ partition_names: Optional[list] = None,
+ replica_number: int = 1,
+ timeout: Optional[float] = None,
+ ) -> None:
+ if embeddings is not None:
+ self._create_collection(embeddings, metadatas)
+ self._extract_fields()
+ self._create_index()
+ self._create_search_params()
+ self._load(
+ partition_names=partition_names,
+ replica_number=replica_number,
+ timeout=timeout,
+ )
+
+ def _create_collection(
+ self, embeddings: list, metadatas: Optional[list[dict]] = None
+ ) -> None:
+ from pymilvus import (
+ Collection,
+ CollectionSchema,
+ DataType,
+ FieldSchema,
+ MilvusException,
+ )
+ from pymilvus.orm.types import infer_dtype_bydata # type: ignore
+
+ # Determine embedding dim
+ dim = len(embeddings[0])
+ fields = []
+ if self._metadata_field is not None:
+ fields.append(FieldSchema(self._metadata_field, DataType.JSON))
+ else:
+ # Determine metadata schema
+ if metadatas:
+ # Create FieldSchema for each entry in metadata.
+ for key, value in metadatas[0].items():
+ # Infer the corresponding datatype of the metadata
+ dtype = infer_dtype_bydata(value)
+ # Datatype isn't compatible
+ if dtype == DataType.UNKNOWN or dtype == DataType.NONE:
+ logger.error(
+ (
+ "Failure to create collection, "
+ "unrecognized dtype for key: %s"
+ ),
+ key,
+ )
+ raise ValueError(f"Unrecognized datatype for {key}.")
+ # Dataype is a string/varchar equivalent
+ elif dtype == DataType.VARCHAR:
+ fields.append(
+ FieldSchema(key, DataType.VARCHAR, max_length=65_535)
+ )
+ else:
+ fields.append(FieldSchema(key, dtype))
+
+ # Create the text field
+ fields.append(
+ FieldSchema(self._text_field, DataType.VARCHAR, max_length=65_535)
+ )
+ # Create the primary key field
+ if self.auto_id:
+ fields.append(
+ FieldSchema(
+ self._primary_field, DataType.INT64, is_primary=True, auto_id=True
+ )
+ )
+ else:
+ fields.append(
+ FieldSchema(
+ self._primary_field,
+ DataType.VARCHAR,
+ is_primary=True,
+ auto_id=False,
+ max_length=65_535,
+ )
+ )
+ # Create the vector field, supports binary or float vectors
+ fields.append(
+ FieldSchema(self._vector_field, infer_dtype_bydata(embeddings[0]), dim=dim)
+ )
+
+ # Create the schema for the collection
+ schema = CollectionSchema(
+ fields,
+ description=self.collection_description,
+ partition_key_field=self._partition_key_field,
+ )
+
+ # Create the collection
+ try:
+ if self.num_shards is not None:
+ # Issue with defaults:
+ # https://github.com/milvus-io/pymilvus/blob/59bf5e811ad56e20946559317fed855330758d9c/pymilvus/client/prepare.py#L82-L85
+ self.col = Collection(
+ name=self.collection_name,
+ schema=schema,
+ consistency_level=self.consistency_level,
+ using=self.alias,
+ num_shards=self.num_shards,
+ )
+ else:
+ self.col = Collection(
+ name=self.collection_name,
+ schema=schema,
+ consistency_level=self.consistency_level,
+ using=self.alias,
+ )
+ # Set the collection properties if they exist
+ if self.collection_properties is not None:
+ self.col.set_properties(self.collection_properties)
+ except MilvusException as e:
+ logger.error(
+ "Failed to create collection: %s error: %s", self.collection_name, e
+ )
+ raise e
+
+ def _extract_fields(self) -> None:
+ """Grab the existing fields from the Collection"""
+ from pymilvus import Collection
+
+ if isinstance(self.col, Collection):
+ schema = self.col.schema
+ for x in schema.fields:
+ self.fields.append(x.name)
+
+ def _get_index(self) -> Optional[dict[str, Any]]:
+ """Return the vector index information if it exists"""
+ from pymilvus import Collection
+
+ if isinstance(self.col, Collection):
+ for x in self.col.indexes:
+ if x.field_name == self._vector_field:
+ return x.to_dict()
+ return None
+
+ def _create_index(self) -> None:
+ """Create a index on the collection"""
+ from pymilvus import Collection, MilvusException
+
+ if isinstance(self.col, Collection) and self._get_index() is None:
+ try:
+ # If no index params, use a default HNSW based one
+ if self.index_params is None:
+ self.index_params = {
+ "metric_type": "L2",
+ "index_type": "HNSW",
+ "params": {"M": 8, "efConstruction": 64},
+ }
+
+ try:
+ self.col.create_index(
+ self._vector_field,
+ index_params=self.index_params,
+ using=self.alias,
+ )
+
+ # If default did not work, most likely on Zilliz Cloud
+ except MilvusException:
+ # Use AUTOINDEX based index
+ self.index_params = {
+ "metric_type": "L2",
+ "index_type": "AUTOINDEX",
+ "params": {},
+ }
+ self.col.create_index(
+ self._vector_field,
+ index_params=self.index_params,
+ using=self.alias,
+ )
+ logger.debug(
+ "Successfully created an index on collection: %s",
+ self.collection_name,
+ )
+
+ except MilvusException as e:
+ logger.error(
+ "Failed to create an index on collection: %s", self.collection_name
+ )
+ raise e
+
+ def _create_search_params(self) -> None:
+ """Generate search params based on the current index type"""
+ from pymilvus import Collection
+
+ if isinstance(self.col, Collection) and self.search_params is None:
+ index = self._get_index()
+ if index is not None:
+ index_type: str = index["index_param"]["index_type"]
+ metric_type: str = index["index_param"]["metric_type"]
+ self.search_params = self.default_search_params[index_type]
+ self.search_params["metric_type"] = metric_type
+
+ def _load(
+ self,
+ partition_names: Optional[list] = None,
+ replica_number: int = 1,
+ timeout: Optional[float] = None,
+ ) -> None:
+ """Load the collection if available."""
+ from pymilvus import Collection, utility
+ from pymilvus.client.types import LoadState # type: ignore
+
+ timeout = self.timeout or timeout
+ if (
+ isinstance(self.col, Collection)
+ and self._get_index() is not None
+ and utility.load_state(self.collection_name, using=self.alias)
+ == LoadState.NotLoad
+ ):
+ self.col.load(
+ partition_names=partition_names,
+ replica_number=replica_number,
+ timeout=timeout,
+ )
+
+ def add_texts(
+ self,
+ texts: Iterable[str],
+ metadatas: Optional[List[dict]] = None,
+ timeout: Optional[float] = None,
+ batch_size: int = 1000,
+ *,
+ ids: Optional[List[str]] = None,
+ **kwargs: Any,
+ ) -> List[str]:
+ """Insert text data into Milvus.
+
+ Inserting data when the collection has not be made yet will result
+ in creating a new Collection. The data of the first entity decides
+ the schema of the new collection, the dim is extracted from the first
+ embedding and the columns are decided by the first metadata dict.
+ Metadata keys will need to be present for all inserted values. At
+ the moment there is no None equivalent in Milvus.
+
+ Args:
+ texts (Iterable[str]): The texts to embed, it is assumed
+ that they all fit in memory.
+ metadatas (Optional[List[dict]]): Metadata dicts attached to each of
+ the texts. Defaults to None.
+ should be less than 65535 bytes. Required and work when auto_id is False.
+ timeout (Optional[float]): Timeout for each batch insert. Defaults
+ to None.
+ batch_size (int, optional): Batch size to use for insertion.
+ Defaults to 1000.
+ ids (Optional[List[str]]): List of text ids. The length of each item
+
+ Raises:
+ MilvusException: Failure to add texts
+
+ Returns:
+ List[str]: The resulting keys for each inserted element.
+ """
+ from pymilvus import Collection, MilvusException
+
+ texts = list(texts)
+ if not self.auto_id:
+ assert isinstance(
+ ids, list
+ ), "A list of valid ids are required when auto_id is False."
+ assert len(set(ids)) == len(
+ texts
+ ), "Different lengths of texts and unique ids are provided."
+ assert all(
+ len(x.encode()) <= 65_535 for x in ids
+ ), "Each id should be a string less than 65535 bytes."
+
+ try:
+ embeddings = self.embedding_func.embed_documents(texts)
+ except NotImplementedError:
+ embeddings = [self.embedding_func.embed_query(x) for x in texts]
+
+ if len(embeddings) == 0:
+ logger.debug("Nothing to insert, skipping.")
+ return []
+
+ # If the collection hasn't been initialized yet, perform all steps to do so
+ if not isinstance(self.col, Collection):
+ kwargs = {"embeddings": embeddings, "metadatas": metadatas}
+ if self.partition_names:
+ kwargs["partition_names"] = self.partition_names
+ if self.replica_number:
+ kwargs["replica_number"] = self.replica_number
+ if self.timeout:
+ kwargs["timeout"] = self.timeout
+ self._init(**kwargs)
+
+ # Dict to hold all insert columns
+ insert_dict: dict[str, list] = {
+ self._text_field: texts,
+ self._vector_field: embeddings,
+ }
+
+ if not self.auto_id:
+ insert_dict[self._primary_field] = ids # type: ignore[assignment]
+
+ if self._metadata_field is not None:
+ for d in metadatas: # type: ignore[union-attr]
+ insert_dict.setdefault(self._metadata_field, []).append(d)
+ else:
+ # Collect the metadata into the insert dict.
+ if metadatas is not None:
+ for d in metadatas:
+ for key, value in d.items():
+ keys = (
+ [x for x in self.fields if x != self._primary_field]
+ if self.auto_id
+ else [x for x in self.fields]
+ )
+ if key in keys:
+ insert_dict.setdefault(key, []).append(value)
+
+ # Total insert count
+ vectors: list = insert_dict[self._vector_field]
+ total_count = len(vectors)
+
+ pks: list[str] = []
+
+ assert isinstance(self.col, Collection)
+ for i in range(0, total_count, batch_size):
+ # Grab end index
+ end = min(i + batch_size, total_count)
+ # Convert dict to list of lists batch for insertion
+ insert_list = [
+ insert_dict[x][i:end] for x in self.fields if x in insert_dict
+ ]
+ # Insert into the collection.
+ try:
+ res: Collection
+ timeout = self.timeout or timeout
+ res = self.col.insert(insert_list, timeout=timeout, **kwargs)
+ pks.extend(res.primary_keys)
+ except MilvusException as e:
+ logger.error(
+ "Failed to insert batch starting at entity: %s/%s", i, total_count
+ )
+ raise e
+ return pks
+
+ def similarity_search(
+ self,
+ query: str,
+ k: int = 4,
+ param: Optional[dict] = None,
+ expr: Optional[str] = None,
+ timeout: Optional[float] = None,
+ **kwargs: Any,
+ ) -> List[Document]:
+ """Perform a similarity search against the query string.
+
+ Args:
+ query (str): The text to search.
+ k (int, optional): How many results to return. Defaults to 4.
+ param (dict, optional): The search params for the index type.
+ Defaults to None.
+ expr (str, optional): Filtering expression. Defaults to None.
+ timeout (int, optional): How long to wait before timeout error.
+ Defaults to None.
+ kwargs: Collection.search() keyword arguments.
+
+ Returns:
+ List[Document]: Document results for search.
+ """
+ if self.col is None:
+ logger.debug("No existing collection to search.")
+ return []
+ timeout = self.timeout or timeout
+ res = self.similarity_search_with_score(
+ query=query, k=k, param=param, expr=expr, timeout=timeout, **kwargs
+ )
+ return [doc for doc, _ in res]
+
+ def similarity_search_by_vector(
+ self,
+ embedding: List[float],
+ k: int = 4,
+ param: Optional[dict] = None,
+ expr: Optional[str] = None,
+ timeout: Optional[float] = None,
+ **kwargs: Any,
+ ) -> List[Document]:
+ """Perform a similarity search against the query string.
+
+ Args:
+ embedding (List[float]): The embedding vector to search.
+ k (int, optional): How many results to return. Defaults to 4.
+ param (dict, optional): The search params for the index type.
+ Defaults to None.
+ expr (str, optional): Filtering expression. Defaults to None.
+ timeout (int, optional): How long to wait before timeout error.
+ Defaults to None.
+ kwargs: Collection.search() keyword arguments.
+
+ Returns:
+ List[Document]: Document results for search.
+ """
+ if self.col is None:
+ logger.debug("No existing collection to search.")
+ return []
+ timeout = self.timeout or timeout
+ res = self.similarity_search_with_score_by_vector(
+ embedding=embedding, k=k, param=param, expr=expr, timeout=timeout, **kwargs
+ )
+ return [doc for doc, _ in res]
+
+ def similarity_search_with_score(
+ self,
+ query: str,
+ k: int = 4,
+ param: Optional[dict] = None,
+ expr: Optional[str] = None,
+ timeout: Optional[float] = None,
+ **kwargs: Any,
+ ) -> List[Tuple[Document, float]]:
+ """Perform a search on a query string and return results with score.
+
+ For more information about the search parameters, take a look at the pymilvus
+ documentation found here:
+ https://milvus.io/api-reference/pymilvus/v2.2.6/Collection/search().md
+
+ Args:
+ query (str): The text being searched.
+ k (int, optional): The amount of results to return. Defaults to 4.
+ param (dict): The search params for the specified index.
+ Defaults to None.
+ expr (str, optional): Filtering expression. Defaults to None.
+ timeout (float, optional): How long to wait before timeout error.
+ Defaults to None.
+ kwargs: Collection.search() keyword arguments.
+
+ Returns:
+ List[float], List[Tuple[Document, any, any]]:
+ """
+ if self.col is None:
+ logger.debug("No existing collection to search.")
+ return []
+
+ # Embed the query text.
+ embedding = self.embedding_func.embed_query(query)
+ timeout = self.timeout or timeout
+ res = self.similarity_search_with_score_by_vector(
+ embedding=embedding, k=k, param=param, expr=expr, timeout=timeout, **kwargs
+ )
+ return res
+
+ def similarity_search_with_score_by_vector(
+ self,
+ embedding: List[float],
+ k: int = 4,
+ param: Optional[dict] = None,
+ expr: Optional[str] = None,
+ timeout: Optional[float] = None,
+ **kwargs: Any,
+ ) -> List[Tuple[Document, float]]:
+ """Perform a search on a query string and return results with score.
+
+ For more information about the search parameters, take a look at the pymilvus
+ documentation found here:
+ https://milvus.io/api-reference/pymilvus/v2.2.6/Collection/search().md
+
+ Args:
+ embedding (List[float]): The embedding vector being searched.
+ k (int, optional): The amount of results to return. Defaults to 4.
+ param (dict): The search params for the specified index.
+ Defaults to None.
+ expr (str, optional): Filtering expression. Defaults to None.
+ timeout (float, optional): How long to wait before timeout error.
+ Defaults to None.
+ kwargs: Collection.search() keyword arguments.
+
+ Returns:
+ List[Tuple[Document, float]]: Result doc and score.
+ """
+ if self.col is None:
+ logger.debug("No existing collection to search.")
+ return []
+
+ if param is None:
+ param = self.search_params
+
+ # Determine result metadata fields with PK.
+ output_fields = self.fields[:]
+ output_fields.remove(self._vector_field)
+ timeout = self.timeout or timeout
+ # Perform the search.
+ res = self.col.search(
+ data=[embedding],
+ anns_field=self._vector_field,
+ param=param,
+ limit=k,
+ expr=expr,
+ output_fields=output_fields,
+ timeout=timeout,
+ **kwargs,
+ )
+ # Organize results.
+ ret = []
+ for result in res[0]:
+ data = {x: result.entity.get(x) for x in output_fields}
+ doc = self._parse_document(data)
+ pair = (doc, result.score)
+ ret.append(pair)
+
+ return ret
+
+ def max_marginal_relevance_search(
+ self,
+ query: str,
+ k: int = 4,
+ fetch_k: int = 20,
+ lambda_mult: float = 0.5,
+ param: Optional[dict] = None,
+ expr: Optional[str] = None,
+ timeout: Optional[float] = None,
+ **kwargs: Any,
+ ) -> List[Document]:
+ """Perform a search and return results that are reordered by MMR.
+
+ Args:
+ query (str): The text being searched.
+ k (int, optional): How many results to give. Defaults to 4.
+ fetch_k (int, optional): Total results to select k from.
+ Defaults to 20.
+ lambda_mult: Number between 0 and 1 that determines the degree
+ of diversity among the results with 0 corresponding
+ to maximum diversity and 1 to minimum diversity.
+ Defaults to 0.5
+ param (dict, optional): The search params for the specified index.
+ Defaults to None.
+ expr (str, optional): Filtering expression. Defaults to None.
+ timeout (float, optional): How long to wait before timeout error.
+ Defaults to None.
+ kwargs: Collection.search() keyword arguments.
+
+
+ Returns:
+ List[Document]: Document results for search.
+ """
+ if self.col is None:
+ logger.debug("No existing collection to search.")
+ return []
+
+ embedding = self.embedding_func.embed_query(query)
+ timeout = self.timeout or timeout
+ return self.max_marginal_relevance_search_by_vector(
+ embedding=embedding,
+ k=k,
+ fetch_k=fetch_k,
+ lambda_mult=lambda_mult,
+ param=param,
+ expr=expr,
+ timeout=timeout,
+ **kwargs,
+ )
+
+ def max_marginal_relevance_search_by_vector(
+ self,
+ embedding: list[float],
+ k: int = 4,
+ fetch_k: int = 20,
+ lambda_mult: float = 0.5,
+ param: Optional[dict] = None,
+ expr: Optional[str] = None,
+ timeout: Optional[float] = None,
+ **kwargs: Any,
+ ) -> List[Document]:
+ """Perform a search and return results that are reordered by MMR.
+
+ Args:
+ embedding (str): The embedding vector being searched.
+ k (int, optional): How many results to give. Defaults to 4.
+ fetch_k (int, optional): Total results to select k from.
+ Defaults to 20.
+ lambda_mult: Number between 0 and 1 that determines the degree
+ of diversity among the results with 0 corresponding
+ to maximum diversity and 1 to minimum diversity.
+ Defaults to 0.5
+ param (dict, optional): The search params for the specified index.
+ Defaults to None.
+ expr (str, optional): Filtering expression. Defaults to None.
+ timeout (float, optional): How long to wait before timeout error.
+ Defaults to None.
+ kwargs: Collection.search() keyword arguments.
+
+ Returns:
+ List[Document]: Document results for search.
+ """
+ if self.col is None:
+ logger.debug("No existing collection to search.")
+ return []
+
+ if param is None:
+ param = self.search_params
+
+ # Determine result metadata fields.
+ output_fields = self.fields[:]
+ output_fields.remove(self._vector_field)
+ timeout = self.timeout or timeout
+ # Perform the search.
+ res = self.col.search(
+ data=[embedding],
+ anns_field=self._vector_field,
+ param=param,
+ limit=fetch_k,
+ expr=expr,
+ output_fields=output_fields,
+ timeout=timeout,
+ **kwargs,
+ )
+ # Organize results.
+ ids = []
+ documents = []
+ scores = []
+ for result in res[0]:
+ data = {x: result.entity.get(x) for x in output_fields}
+ doc = self._parse_document(data)
+ documents.append(doc)
+ scores.append(result.score)
+ ids.append(result.id)
+
+ vectors = self.col.query(
+ expr=f"{self._primary_field} in {ids}",
+ output_fields=[self._primary_field, self._vector_field],
+ timeout=timeout,
+ )
+ # Reorganize the results from query to match search order.
+ vectors = {x[self._primary_field]: x[self._vector_field] for x in vectors}
+
+ ordered_result_embeddings = [vectors[x] for x in ids]
+
+ # Get the new order of results.
+ new_ordering = maximal_marginal_relevance(
+ np.array(embedding), ordered_result_embeddings, k=k, lambda_mult=lambda_mult
+ )
+
+ # Reorder the values and return.
+ ret = []
+ for x in new_ordering:
+ # Function can return -1 index
+ if x == -1:
+ break
+ else:
+ ret.append(documents[x])
+ return ret
+
+ def delete( # type: ignore[no-untyped-def]
+ self, ids: Optional[List[str]] = None, expr: Optional[str] = None, **kwargs: str
+ ):
+ """Delete by vector ID or boolean expression.
+ Refer to [Milvus documentation](https://milvus.io/docs/delete_data.md)
+ for notes and examples of expressions.
+
+ Args:
+ ids: List of ids to delete.
+ expr: Boolean expression that specifies the entities to delete.
+ kwargs: Other parameters in Milvus delete api.
+ """
+ if isinstance(ids, list) and len(ids) > 0:
+ if expr is not None:
+ logger.warning(
+ "Both ids and expr are provided. " "Ignore expr and delete by ids."
+ )
+ expr = f"{self._primary_field} in {ids}"
+ else:
+ assert isinstance(
+ expr, str
+ ), "Either ids list or expr string must be provided."
+ return self.col.delete(expr=expr, **kwargs) # type: ignore[union-attr]
+
+ @classmethod
+ def from_texts(
+ cls,
+ texts: List[str],
+ embedding: Embeddings,
+ metadatas: Optional[List[dict]] = None,
+ collection_name: str = "LangChainCollection",
+ connection_args: dict[str, Any] = DEFAULT_MILVUS_CONNECTION,
+ consistency_level: str = "Session",
+ index_params: Optional[dict] = None,
+ search_params: Optional[dict] = None,
+ drop_old: bool = False,
+ *,
+ ids: Optional[List[str]] = None,
+ **kwargs: Any,
+ ) -> Milvus:
+ """Create a Milvus collection, indexes it with HNSW, and insert data.
+
+ Args:
+ texts (List[str]): Text data.
+ embedding (Embeddings): Embedding function.
+ metadatas (Optional[List[dict]]): Metadata for each text if it exists.
+ Defaults to None.
+ collection_name (str, optional): Collection name to use. Defaults to
+ "LangChainCollection".
+ connection_args (dict[str, Any], optional): Connection args to use. Defaults
+ to DEFAULT_MILVUS_CONNECTION.
+ consistency_level (str, optional): Which consistency level to use. Defaults
+ to "Session".
+ index_params (Optional[dict], optional): Which index_params to use. Defaults
+ to None.
+ search_params (Optional[dict], optional): Which search params to use.
+ Defaults to None.
+ drop_old (Optional[bool], optional): Whether to drop the collection with
+ that name if it exists. Defaults to False.
+ ids (Optional[List[str]]): List of text ids. Defaults to None.
+
+ Returns:
+ Milvus: Milvus Vector Store
+ """
+ if isinstance(ids, list) and len(ids) > 0:
+ auto_id = False
+ else:
+ auto_id = True
+
+ vector_db = cls(
+ embedding_function=embedding,
+ collection_name=collection_name,
+ connection_args=connection_args,
+ consistency_level=consistency_level,
+ index_params=index_params,
+ search_params=search_params,
+ drop_old=drop_old,
+ auto_id=auto_id,
+ **kwargs,
+ )
+ vector_db.add_texts(texts=texts, metadatas=metadatas, ids=ids)
+ return vector_db
+
+ def _parse_document(self, data: dict) -> Document:
+ return Document(
+ page_content=data.pop(self._text_field),
+ metadata=data.pop(self._metadata_field) if self._metadata_field else data,
+ )
+
+ def get_pks(self, expr: str, **kwargs: Any) -> List[int] | None:
+ """Get primary keys with expression
+
+ Args:
+ expr: Expression - E.g: "id in [1, 2]", or "title LIKE 'Abc%'"
+
+ Returns:
+ List[int]: List of IDs (Primary Keys)
+ """
+
+ from pymilvus import MilvusException
+
+ if self.col is None:
+ logger.debug("No existing collection to get pk.")
+ return None
+
+ try:
+ query_result = self.col.query(
+ expr=expr, output_fields=[self._primary_field]
+ )
+ except MilvusException as exc:
+ logger.error("Failed to get ids: %s error: %s", self.collection_name, exc)
+ raise exc
+ pks = [item.get(self._primary_field) for item in query_result]
+ return pks
+
+ def upsert(
+ self,
+ ids: Optional[List[str]] = None,
+ documents: List[Document] | None = None,
+ **kwargs: Any,
+ ) -> List[str] | None:
+ """Update/Insert documents to the vectorstore.
+
+ Args:
+ ids: IDs to update - Let's call get_pks to get ids with expression \n
+ documents (List[Document]): Documents to add to the vectorstore.
+
+ Returns:
+ List[str]: IDs of the added texts.
+ """
+
+ from pymilvus import MilvusException
+
+ if documents is None or len(documents) == 0:
+ logger.debug("No documents to upsert.")
+ return None
+
+ if ids is not None and len(ids):
+ try:
+ self.delete(ids=ids)
+ except MilvusException:
+ pass
+ try:
+ return self.add_documents(documents=documents, **kwargs)
+ except MilvusException as exc:
+ logger.error(
+ "Failed to upsert entities: %s error: %s", self.collection_name, exc
+ )
+ raise exc
diff --git a/libs/partners/milvus/langchain_milvus/vectorstores/zilliz.py b/libs/partners/milvus/langchain_milvus/vectorstores/zilliz.py
new file mode 100644
index 0000000000000..02f2ce739ff4c
--- /dev/null
+++ b/libs/partners/milvus/langchain_milvus/vectorstores/zilliz.py
@@ -0,0 +1,196 @@
+from __future__ import annotations
+
+import logging
+from typing import Any, Dict, List, Optional
+
+from langchain_core.embeddings import Embeddings
+
+from langchain_milvus.vectorstores.milvus import Milvus
+
+logger = logging.getLogger(__name__)
+
+
+class Zilliz(Milvus):
+ """`Zilliz` vector store.
+
+ You need to have `pymilvus` installed and a
+ running Zilliz database.
+
+ See the following documentation for how to run a Zilliz instance:
+ https://docs.zilliz.com/docs/create-cluster
+
+
+ IF USING L2/IP metric IT IS HIGHLY SUGGESTED TO NORMALIZE YOUR DATA.
+
+ Args:
+ embedding_function (Embeddings): Function used to embed the text.
+ collection_name (str): Which Zilliz collection to use. Defaults to
+ "LangChainCollection".
+ connection_args (Optional[dict[str, any]]): The connection args used for
+ this class comes in the form of a dict.
+ consistency_level (str): The consistency level to use for a collection.
+ Defaults to "Session".
+ index_params (Optional[dict]): Which index params to use. Defaults to
+ HNSW/AUTOINDEX depending on service.
+ search_params (Optional[dict]): Which search params to use. Defaults to
+ default of index.
+ drop_old (Optional[bool]): Whether to drop the current collection. Defaults
+ to False.
+ auto_id (bool): Whether to enable auto id for primary key. Defaults to False.
+ If False, you needs to provide text ids (string less than 65535 bytes).
+ If True, Milvus will generate unique integers as primary keys.
+
+ The connection args used for this class comes in the form of a dict,
+ here are a few of the options:
+ address (str): The actual address of Zilliz
+ instance. Example address: "localhost:19530"
+ uri (str): The uri of Zilliz instance. Example uri:
+ "https://in03-ba4234asae.api.gcp-us-west1.zillizcloud.com",
+ host (str): The host of Zilliz instance. Default at "localhost",
+ PyMilvus will fill in the default host if only port is provided.
+ port (str/int): The port of Zilliz instance. Default at 19530, PyMilvus
+ will fill in the default port if only host is provided.
+ user (str): Use which user to connect to Zilliz instance. If user and
+ password are provided, we will add related header in every RPC call.
+ password (str): Required when user is provided. The password
+ corresponding to the user.
+ token (str): API key, for serverless clusters which can be used as
+ replacements for user and password.
+ secure (bool): Default is false. If set to true, tls will be enabled.
+ client_key_path (str): If use tls two-way authentication, need to
+ write the client.key path.
+ client_pem_path (str): If use tls two-way authentication, need to
+ write the client.pem path.
+ ca_pem_path (str): If use tls two-way authentication, need to write
+ the ca.pem path.
+ server_pem_path (str): If use tls one-way authentication, need to
+ write the server.pem path.
+ server_name (str): If use tls, need to write the common name.
+
+ Example:
+ .. code-block:: python
+
+ from langchain_community.vectorstores import Zilliz
+ from langchain_community.embeddings import OpenAIEmbeddings
+
+ embedding = OpenAIEmbeddings()
+ # Connect to a Zilliz instance
+ milvus_store = Milvus(
+ embedding_function = embedding,
+ collection_name = "LangChainCollection",
+ connection_args = {
+ "uri": "https://in03-ba4234asae.api.gcp-us-west1.zillizcloud.com",
+ "user": "temp",
+ "password": "temp",
+ "token": "temp", # API key as replacements for user and password
+ "secure": True
+ }
+ drop_old: True,
+ )
+
+ Raises:
+ ValueError: If the pymilvus python package is not installed.
+ """
+
+ def _create_index(self) -> None:
+ """Create a index on the collection"""
+ from pymilvus import Collection, MilvusException
+
+ if isinstance(self.col, Collection) and self._get_index() is None:
+ try:
+ # If no index params, use a default AutoIndex based one
+ if self.index_params is None:
+ self.index_params = {
+ "metric_type": "L2",
+ "index_type": "AUTOINDEX",
+ "params": {},
+ }
+
+ try:
+ self.col.create_index(
+ self._vector_field,
+ index_params=self.index_params,
+ using=self.alias,
+ )
+
+ # If default did not work, most likely Milvus self-hosted
+ except MilvusException:
+ # Use HNSW based index
+ self.index_params = {
+ "metric_type": "L2",
+ "index_type": "HNSW",
+ "params": {"M": 8, "efConstruction": 64},
+ }
+ self.col.create_index(
+ self._vector_field,
+ index_params=self.index_params,
+ using=self.alias,
+ )
+ logger.debug(
+ "Successfully created an index on collection: %s",
+ self.collection_name,
+ )
+
+ except MilvusException as e:
+ logger.error(
+ "Failed to create an index on collection: %s", self.collection_name
+ )
+ raise e
+
+ @classmethod
+ def from_texts(
+ cls,
+ texts: List[str],
+ embedding: Embeddings,
+ metadatas: Optional[List[dict]] = None,
+ collection_name: str = "LangChainCollection",
+ connection_args: Optional[Dict[str, Any]] = None,
+ consistency_level: str = "Session",
+ index_params: Optional[dict] = None,
+ search_params: Optional[dict] = None,
+ drop_old: bool = False,
+ *,
+ ids: Optional[List[str]] = None,
+ auto_id: bool = False,
+ **kwargs: Any,
+ ) -> Zilliz:
+ """Create a Zilliz collection, indexes it with HNSW, and insert data.
+
+ Args:
+ texts (List[str]): Text data.
+ embedding (Embeddings): Embedding function.
+ metadatas (Optional[List[dict]]): Metadata for each text if it exists.
+ Defaults to None.
+ collection_name (str, optional): Collection name to use. Defaults to
+ "LangChainCollection".
+ connection_args (dict[str, Any], optional): Connection args to use. Defaults
+ to DEFAULT_MILVUS_CONNECTION.
+ consistency_level (str, optional): Which consistency level to use. Defaults
+ to "Session".
+ index_params (Optional[dict], optional): Which index_params to use.
+ Defaults to None.
+ search_params (Optional[dict], optional): Which search params to use.
+ Defaults to None.
+ drop_old (Optional[bool], optional): Whether to drop the collection with
+ that name if it exists. Defaults to False.
+ ids (Optional[List[str]]): List of text ids.
+ auto_id (bool): Whether to enable auto id for primary key. Defaults to
+ False. If False, you needs to provide text ids (string less than 65535
+ bytes). If True, Milvus will generate unique integers as primary keys.
+
+ Returns:
+ Zilliz: Zilliz Vector Store
+ """
+ vector_db = cls(
+ embedding_function=embedding,
+ collection_name=collection_name,
+ connection_args=connection_args or {},
+ consistency_level=consistency_level,
+ index_params=index_params,
+ search_params=search_params,
+ drop_old=drop_old,
+ auto_id=auto_id,
+ **kwargs,
+ )
+ vector_db.add_texts(texts=texts, metadatas=metadatas, ids=ids)
+ return vector_db
diff --git a/libs/partners/milvus/poetry.lock b/libs/partners/milvus/poetry.lock
new file mode 100644
index 0000000000000..059dade2041fa
--- /dev/null
+++ b/libs/partners/milvus/poetry.lock
@@ -0,0 +1,1291 @@
+# This file is automatically @generated by Poetry 1.7.1 and should not be changed by hand.
+
+[[package]]
+name = "annotated-types"
+version = "0.7.0"
+description = "Reusable constraint types to use with typing.Annotated"
+optional = false
+python-versions = ">=3.8"
+files = [
+ {file = "annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53"},
+ {file = "annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89"},
+]
+
+[package.dependencies]
+typing-extensions = {version = ">=4.0.0", markers = "python_version < \"3.9\""}
+
+[[package]]
+name = "certifi"
+version = "2024.2.2"
+description = "Python package for providing Mozilla's CA Bundle."
+optional = false
+python-versions = ">=3.6"
+files = [
+ {file = "certifi-2024.2.2-py3-none-any.whl", hash = "sha256:dc383c07b76109f368f6106eee2b593b04a011ea4d55f652c6ca24a754d1cdd1"},
+ {file = "certifi-2024.2.2.tar.gz", hash = "sha256:0569859f95fc761b18b45ef421b1290a0f65f147e92a1e5eb3e635f9a5e4e66f"},
+]
+
+[[package]]
+name = "charset-normalizer"
+version = "3.3.2"
+description = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet."
+optional = false
+python-versions = ">=3.7.0"
+files = [
+ {file = "charset-normalizer-3.3.2.tar.gz", hash = "sha256:f30c3cb33b24454a82faecaf01b19c18562b1e89558fb6c56de4d9118a032fd5"},
+ {file = "charset_normalizer-3.3.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:25baf083bf6f6b341f4121c2f3c548875ee6f5339300e08be3f2b2ba1721cdd3"},
+ {file = "charset_normalizer-3.3.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:06435b539f889b1f6f4ac1758871aae42dc3a8c0e24ac9e60c2384973ad73027"},
+ {file = "charset_normalizer-3.3.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9063e24fdb1e498ab71cb7419e24622516c4a04476b17a2dab57e8baa30d6e03"},
+ {file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6897af51655e3691ff853668779c7bad41579facacf5fd7253b0133308cf000d"},
+ {file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1d3193f4a680c64b4b6a9115943538edb896edc190f0b222e73761716519268e"},
+ {file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cd70574b12bb8a4d2aaa0094515df2463cb429d8536cfb6c7ce983246983e5a6"},
+ {file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8465322196c8b4d7ab6d1e049e4c5cb460d0394da4a27d23cc242fbf0034b6b5"},
+ {file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a9a8e9031d613fd2009c182b69c7b2c1ef8239a0efb1df3f7c8da66d5dd3d537"},
+ {file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:beb58fe5cdb101e3a055192ac291b7a21e3b7ef4f67fa1d74e331a7f2124341c"},
+ {file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:e06ed3eb3218bc64786f7db41917d4e686cc4856944f53d5bdf83a6884432e12"},
+ {file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:2e81c7b9c8979ce92ed306c249d46894776a909505d8f5a4ba55b14206e3222f"},
+ {file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:572c3763a264ba47b3cf708a44ce965d98555f618ca42c926a9c1616d8f34269"},
+ {file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:fd1abc0d89e30cc4e02e4064dc67fcc51bd941eb395c502aac3ec19fab46b519"},
+ {file = "charset_normalizer-3.3.2-cp310-cp310-win32.whl", hash = "sha256:3d47fa203a7bd9c5b6cee4736ee84ca03b8ef23193c0d1ca99b5089f72645c73"},
+ {file = "charset_normalizer-3.3.2-cp310-cp310-win_amd64.whl", hash = "sha256:10955842570876604d404661fbccbc9c7e684caf432c09c715ec38fbae45ae09"},
+ {file = "charset_normalizer-3.3.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:802fe99cca7457642125a8a88a084cef28ff0cf9407060f7b93dca5aa25480db"},
+ {file = "charset_normalizer-3.3.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:573f6eac48f4769d667c4442081b1794f52919e7edada77495aaed9236d13a96"},
+ {file = "charset_normalizer-3.3.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:549a3a73da901d5bc3ce8d24e0600d1fa85524c10287f6004fbab87672bf3e1e"},
+ {file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f27273b60488abe721a075bcca6d7f3964f9f6f067c8c4c605743023d7d3944f"},
+ {file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1ceae2f17a9c33cb48e3263960dc5fc8005351ee19db217e9b1bb15d28c02574"},
+ {file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:65f6f63034100ead094b8744b3b97965785388f308a64cf8d7c34f2f2e5be0c4"},
+ {file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:753f10e867343b4511128c6ed8c82f7bec3bd026875576dfd88483c5c73b2fd8"},
+ {file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4a78b2b446bd7c934f5dcedc588903fb2f5eec172f3d29e52a9096a43722adfc"},
+ {file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:e537484df0d8f426ce2afb2d0f8e1c3d0b114b83f8850e5f2fbea0e797bd82ae"},
+ {file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:eb6904c354526e758fda7167b33005998fb68c46fbc10e013ca97f21ca5c8887"},
+ {file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:deb6be0ac38ece9ba87dea880e438f25ca3eddfac8b002a2ec3d9183a454e8ae"},
+ {file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:4ab2fe47fae9e0f9dee8c04187ce5d09f48eabe611be8259444906793ab7cbce"},
+ {file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:80402cd6ee291dcb72644d6eac93785fe2c8b9cb30893c1af5b8fdd753b9d40f"},
+ {file = "charset_normalizer-3.3.2-cp311-cp311-win32.whl", hash = "sha256:7cd13a2e3ddeed6913a65e66e94b51d80a041145a026c27e6bb76c31a853c6ab"},
+ {file = "charset_normalizer-3.3.2-cp311-cp311-win_amd64.whl", hash = "sha256:663946639d296df6a2bb2aa51b60a2454ca1cb29835324c640dafb5ff2131a77"},
+ {file = "charset_normalizer-3.3.2-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:0b2b64d2bb6d3fb9112bafa732def486049e63de9618b5843bcdd081d8144cd8"},
+ {file = "charset_normalizer-3.3.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:ddbb2551d7e0102e7252db79ba445cdab71b26640817ab1e3e3648dad515003b"},
+ {file = "charset_normalizer-3.3.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:55086ee1064215781fff39a1af09518bc9255b50d6333f2e4c74ca09fac6a8f6"},
+ {file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8f4a014bc36d3c57402e2977dada34f9c12300af536839dc38c0beab8878f38a"},
+ {file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a10af20b82360ab00827f916a6058451b723b4e65030c5a18577c8b2de5b3389"},
+ {file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8d756e44e94489e49571086ef83b2bb8ce311e730092d2c34ca8f7d925cb20aa"},
+ {file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:90d558489962fd4918143277a773316e56c72da56ec7aa3dc3dbbe20fdfed15b"},
+ {file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6ac7ffc7ad6d040517be39eb591cac5ff87416c2537df6ba3cba3bae290c0fed"},
+ {file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:7ed9e526742851e8d5cc9e6cf41427dfc6068d4f5a3bb03659444b4cabf6bc26"},
+ {file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:8bdb58ff7ba23002a4c5808d608e4e6c687175724f54a5dade5fa8c67b604e4d"},
+ {file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_ppc64le.whl", hash = "sha256:6b3251890fff30ee142c44144871185dbe13b11bab478a88887a639655be1068"},
+ {file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_s390x.whl", hash = "sha256:b4a23f61ce87adf89be746c8a8974fe1c823c891d8f86eb218bb957c924bb143"},
+ {file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:efcb3f6676480691518c177e3b465bcddf57cea040302f9f4e6e191af91174d4"},
+ {file = "charset_normalizer-3.3.2-cp312-cp312-win32.whl", hash = "sha256:d965bba47ddeec8cd560687584e88cf699fd28f192ceb452d1d7ee807c5597b7"},
+ {file = "charset_normalizer-3.3.2-cp312-cp312-win_amd64.whl", hash = "sha256:96b02a3dc4381e5494fad39be677abcb5e6634bf7b4fa83a6dd3112607547001"},
+ {file = "charset_normalizer-3.3.2-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:95f2a5796329323b8f0512e09dbb7a1860c46a39da62ecb2324f116fa8fdc85c"},
+ {file = "charset_normalizer-3.3.2-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c002b4ffc0be611f0d9da932eb0f704fe2602a9a949d1f738e4c34c75b0863d5"},
+ {file = "charset_normalizer-3.3.2-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a981a536974bbc7a512cf44ed14938cf01030a99e9b3a06dd59578882f06f985"},
+ {file = "charset_normalizer-3.3.2-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3287761bc4ee9e33561a7e058c72ac0938c4f57fe49a09eae428fd88aafe7bb6"},
+ {file = "charset_normalizer-3.3.2-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:42cb296636fcc8b0644486d15c12376cb9fa75443e00fb25de0b8602e64c1714"},
+ {file = "charset_normalizer-3.3.2-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0a55554a2fa0d408816b3b5cedf0045f4b8e1a6065aec45849de2d6f3f8e9786"},
+ {file = "charset_normalizer-3.3.2-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:c083af607d2515612056a31f0a8d9e0fcb5876b7bfc0abad3ecd275bc4ebc2d5"},
+ {file = "charset_normalizer-3.3.2-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:87d1351268731db79e0f8e745d92493ee2841c974128ef629dc518b937d9194c"},
+ {file = "charset_normalizer-3.3.2-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:bd8f7df7d12c2db9fab40bdd87a7c09b1530128315d047a086fa3ae3435cb3a8"},
+ {file = "charset_normalizer-3.3.2-cp37-cp37m-musllinux_1_1_s390x.whl", hash = "sha256:c180f51afb394e165eafe4ac2936a14bee3eb10debc9d9e4db8958fe36afe711"},
+ {file = "charset_normalizer-3.3.2-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:8c622a5fe39a48f78944a87d4fb8a53ee07344641b0562c540d840748571b811"},
+ {file = "charset_normalizer-3.3.2-cp37-cp37m-win32.whl", hash = "sha256:db364eca23f876da6f9e16c9da0df51aa4f104a972735574842618b8c6d999d4"},
+ {file = "charset_normalizer-3.3.2-cp37-cp37m-win_amd64.whl", hash = "sha256:86216b5cee4b06df986d214f664305142d9c76df9b6512be2738aa72a2048f99"},
+ {file = "charset_normalizer-3.3.2-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:6463effa3186ea09411d50efc7d85360b38d5f09b870c48e4600f63af490e56a"},
+ {file = "charset_normalizer-3.3.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:6c4caeef8fa63d06bd437cd4bdcf3ffefe6738fb1b25951440d80dc7df8c03ac"},
+ {file = "charset_normalizer-3.3.2-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:37e55c8e51c236f95b033f6fb391d7d7970ba5fe7ff453dad675e88cf303377a"},
+ {file = "charset_normalizer-3.3.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fb69256e180cb6c8a894fee62b3afebae785babc1ee98b81cdf68bbca1987f33"},
+ {file = "charset_normalizer-3.3.2-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ae5f4161f18c61806f411a13b0310bea87f987c7d2ecdbdaad0e94eb2e404238"},
+ {file = "charset_normalizer-3.3.2-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b2b0a0c0517616b6869869f8c581d4eb2dd83a4d79e0ebcb7d373ef9956aeb0a"},
+ {file = "charset_normalizer-3.3.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:45485e01ff4d3630ec0d9617310448a8702f70e9c01906b0d0118bdf9d124cf2"},
+ {file = "charset_normalizer-3.3.2-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:eb00ed941194665c332bf8e078baf037d6c35d7c4f3102ea2d4f16ca94a26dc8"},
+ {file = "charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:2127566c664442652f024c837091890cb1942c30937add288223dc895793f898"},
+ {file = "charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:a50aebfa173e157099939b17f18600f72f84eed3049e743b68ad15bd69b6bf99"},
+ {file = "charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:4d0d1650369165a14e14e1e47b372cfcb31d6ab44e6e33cb2d4e57265290044d"},
+ {file = "charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:923c0c831b7cfcb071580d3f46c4baf50f174be571576556269530f4bbd79d04"},
+ {file = "charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:06a81e93cd441c56a9b65d8e1d043daeb97a3d0856d177d5c90ba85acb3db087"},
+ {file = "charset_normalizer-3.3.2-cp38-cp38-win32.whl", hash = "sha256:6ef1d82a3af9d3eecdba2321dc1b3c238245d890843e040e41e470ffa64c3e25"},
+ {file = "charset_normalizer-3.3.2-cp38-cp38-win_amd64.whl", hash = "sha256:eb8821e09e916165e160797a6c17edda0679379a4be5c716c260e836e122f54b"},
+ {file = "charset_normalizer-3.3.2-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:c235ebd9baae02f1b77bcea61bce332cb4331dc3617d254df3323aa01ab47bd4"},
+ {file = "charset_normalizer-3.3.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:5b4c145409bef602a690e7cfad0a15a55c13320ff7a3ad7ca59c13bb8ba4d45d"},
+ {file = "charset_normalizer-3.3.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:68d1f8a9e9e37c1223b656399be5d6b448dea850bed7d0f87a8311f1ff3dabb0"},
+ {file = "charset_normalizer-3.3.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:22afcb9f253dac0696b5a4be4a1c0f8762f8239e21b99680099abd9b2b1b2269"},
+ {file = "charset_normalizer-3.3.2-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e27ad930a842b4c5eb8ac0016b0a54f5aebbe679340c26101df33424142c143c"},
+ {file = "charset_normalizer-3.3.2-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1f79682fbe303db92bc2b1136016a38a42e835d932bab5b3b1bfcfbf0640e519"},
+ {file = "charset_normalizer-3.3.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b261ccdec7821281dade748d088bb6e9b69e6d15b30652b74cbbac25e280b796"},
+ {file = "charset_normalizer-3.3.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:122c7fa62b130ed55f8f285bfd56d5f4b4a5b503609d181f9ad85e55c89f4185"},
+ {file = "charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:d0eccceffcb53201b5bfebb52600a5fb483a20b61da9dbc885f8b103cbe7598c"},
+ {file = "charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:9f96df6923e21816da7e0ad3fd47dd8f94b2a5ce594e00677c0013018b813458"},
+ {file = "charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:7f04c839ed0b6b98b1a7501a002144b76c18fb1c1850c8b98d458ac269e26ed2"},
+ {file = "charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:34d1c8da1e78d2e001f363791c98a272bb734000fcef47a491c1e3b0505657a8"},
+ {file = "charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:ff8fa367d09b717b2a17a052544193ad76cd49979c805768879cb63d9ca50561"},
+ {file = "charset_normalizer-3.3.2-cp39-cp39-win32.whl", hash = "sha256:aed38f6e4fb3f5d6bf81bfa990a07806be9d83cf7bacef998ab1a9bd660a581f"},
+ {file = "charset_normalizer-3.3.2-cp39-cp39-win_amd64.whl", hash = "sha256:b01b88d45a6fcb69667cd6d2f7a9aeb4bf53760d7fc536bf679ec94fe9f3ff3d"},
+ {file = "charset_normalizer-3.3.2-py3-none-any.whl", hash = "sha256:3e4d1f6587322d2788836a99c69062fbb091331ec940e02d12d179c1d53e25fc"},
+]
+
+[[package]]
+name = "codespell"
+version = "2.3.0"
+description = "Codespell"
+optional = false
+python-versions = ">=3.8"
+files = [
+ {file = "codespell-2.3.0-py3-none-any.whl", hash = "sha256:a9c7cef2501c9cfede2110fd6d4e5e62296920efe9abfb84648df866e47f58d1"},
+ {file = "codespell-2.3.0.tar.gz", hash = "sha256:360c7d10f75e65f67bad720af7007e1060a5d395670ec11a7ed1fed9dd17471f"},
+]
+
+[package.extras]
+dev = ["Pygments", "build", "chardet", "pre-commit", "pytest", "pytest-cov", "pytest-dependency", "ruff", "tomli", "twine"]
+hard-encoding-detection = ["chardet"]
+toml = ["tomli"]
+types = ["chardet (>=5.1.0)", "mypy", "pytest", "pytest-cov", "pytest-dependency"]
+
+[[package]]
+name = "colorama"
+version = "0.4.6"
+description = "Cross-platform colored terminal text."
+optional = false
+python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7"
+files = [
+ {file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"},
+ {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"},
+]
+
+[[package]]
+name = "environs"
+version = "9.5.0"
+description = "simplified environment variable parsing"
+optional = false
+python-versions = ">=3.6"
+files = [
+ {file = "environs-9.5.0-py2.py3-none-any.whl", hash = "sha256:1e549569a3de49c05f856f40bce86979e7d5ffbbc4398e7f338574c220189124"},
+ {file = "environs-9.5.0.tar.gz", hash = "sha256:a76307b36fbe856bdca7ee9161e6c466fd7fcffc297109a118c59b54e27e30c9"},
+]
+
+[package.dependencies]
+marshmallow = ">=3.0.0"
+python-dotenv = "*"
+
+[package.extras]
+dev = ["dj-database-url", "dj-email-url", "django-cache-url", "flake8 (==4.0.1)", "flake8-bugbear (==21.9.2)", "mypy (==0.910)", "pre-commit (>=2.4,<3.0)", "pytest", "tox"]
+django = ["dj-database-url", "dj-email-url", "django-cache-url"]
+lint = ["flake8 (==4.0.1)", "flake8-bugbear (==21.9.2)", "mypy (==0.910)", "pre-commit (>=2.4,<3.0)"]
+tests = ["dj-database-url", "dj-email-url", "django-cache-url", "pytest"]
+
+[[package]]
+name = "exceptiongroup"
+version = "1.2.1"
+description = "Backport of PEP 654 (exception groups)"
+optional = false
+python-versions = ">=3.7"
+files = [
+ {file = "exceptiongroup-1.2.1-py3-none-any.whl", hash = "sha256:5258b9ed329c5bbdd31a309f53cbfb0b155341807f6ff7606a1e801a891b29ad"},
+ {file = "exceptiongroup-1.2.1.tar.gz", hash = "sha256:a4785e48b045528f5bfe627b6ad554ff32def154f42372786903b7abcfe1aa16"},
+]
+
+[package.extras]
+test = ["pytest (>=6)"]
+
+[[package]]
+name = "freezegun"
+version = "1.5.1"
+description = "Let your Python tests travel through time"
+optional = false
+python-versions = ">=3.7"
+files = [
+ {file = "freezegun-1.5.1-py3-none-any.whl", hash = "sha256:bf111d7138a8abe55ab48a71755673dbaa4ab87f4cff5634a4442dfec34c15f1"},
+ {file = "freezegun-1.5.1.tar.gz", hash = "sha256:b29dedfcda6d5e8e083ce71b2b542753ad48cfec44037b3fc79702e2980a89e9"},
+]
+
+[package.dependencies]
+python-dateutil = ">=2.7"
+
+[[package]]
+name = "grpcio"
+version = "1.63.0"
+description = "HTTP/2-based RPC framework"
+optional = false
+python-versions = ">=3.8"
+files = [
+ {file = "grpcio-1.63.0-cp310-cp310-linux_armv7l.whl", hash = "sha256:2e93aca840c29d4ab5db93f94ed0a0ca899e241f2e8aec6334ab3575dc46125c"},
+ {file = "grpcio-1.63.0-cp310-cp310-macosx_12_0_universal2.whl", hash = "sha256:91b73d3f1340fefa1e1716c8c1ec9930c676d6b10a3513ab6c26004cb02d8b3f"},
+ {file = "grpcio-1.63.0-cp310-cp310-manylinux_2_17_aarch64.whl", hash = "sha256:b3afbd9d6827fa6f475a4f91db55e441113f6d3eb9b7ebb8fb806e5bb6d6bd0d"},
+ {file = "grpcio-1.63.0-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8f3f6883ce54a7a5f47db43289a0a4c776487912de1a0e2cc83fdaec9685cc9f"},
+ {file = "grpcio-1.63.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cf8dae9cc0412cb86c8de5a8f3be395c5119a370f3ce2e69c8b7d46bb9872c8d"},
+ {file = "grpcio-1.63.0-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:08e1559fd3b3b4468486b26b0af64a3904a8dbc78d8d936af9c1cf9636eb3e8b"},
+ {file = "grpcio-1.63.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:5c039ef01516039fa39da8a8a43a95b64e288f79f42a17e6c2904a02a319b357"},
+ {file = "grpcio-1.63.0-cp310-cp310-win32.whl", hash = "sha256:ad2ac8903b2eae071055a927ef74121ed52d69468e91d9bcbd028bd0e554be6d"},
+ {file = "grpcio-1.63.0-cp310-cp310-win_amd64.whl", hash = "sha256:b2e44f59316716532a993ca2966636df6fbe7be4ab6f099de6815570ebe4383a"},
+ {file = "grpcio-1.63.0-cp311-cp311-linux_armv7l.whl", hash = "sha256:f28f8b2db7b86c77916829d64ab21ff49a9d8289ea1564a2b2a3a8ed9ffcccd3"},
+ {file = "grpcio-1.63.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:65bf975639a1f93bee63ca60d2e4951f1b543f498d581869922910a476ead2f5"},
+ {file = "grpcio-1.63.0-cp311-cp311-manylinux_2_17_aarch64.whl", hash = "sha256:b5194775fec7dc3dbd6a935102bb156cd2c35efe1685b0a46c67b927c74f0cfb"},
+ {file = "grpcio-1.63.0-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e4cbb2100ee46d024c45920d16e888ee5d3cf47c66e316210bc236d5bebc42b3"},
+ {file = "grpcio-1.63.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1ff737cf29b5b801619f10e59b581869e32f400159e8b12d7a97e7e3bdeee6a2"},
+ {file = "grpcio-1.63.0-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:cd1e68776262dd44dedd7381b1a0ad09d9930ffb405f737d64f505eb7f77d6c7"},
+ {file = "grpcio-1.63.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:93f45f27f516548e23e4ec3fbab21b060416007dbe768a111fc4611464cc773f"},
+ {file = "grpcio-1.63.0-cp311-cp311-win32.whl", hash = "sha256:878b1d88d0137df60e6b09b74cdb73db123f9579232c8456f53e9abc4f62eb3c"},
+ {file = "grpcio-1.63.0-cp311-cp311-win_amd64.whl", hash = "sha256:756fed02dacd24e8f488f295a913f250b56b98fb793f41d5b2de6c44fb762434"},
+ {file = "grpcio-1.63.0-cp312-cp312-linux_armv7l.whl", hash = "sha256:93a46794cc96c3a674cdfb59ef9ce84d46185fe9421baf2268ccb556f8f81f57"},
+ {file = "grpcio-1.63.0-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:a7b19dfc74d0be7032ca1eda0ed545e582ee46cd65c162f9e9fc6b26ef827dc6"},
+ {file = "grpcio-1.63.0-cp312-cp312-manylinux_2_17_aarch64.whl", hash = "sha256:8064d986d3a64ba21e498b9a376cbc5d6ab2e8ab0e288d39f266f0fca169b90d"},
+ {file = "grpcio-1.63.0-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:219bb1848cd2c90348c79ed0a6b0ea51866bc7e72fa6e205e459fedab5770172"},
+ {file = "grpcio-1.63.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a2d60cd1d58817bc5985fae6168d8b5655c4981d448d0f5b6194bbcc038090d2"},
+ {file = "grpcio-1.63.0-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:9e350cb096e5c67832e9b6e018cf8a0d2a53b2a958f6251615173165269a91b0"},
+ {file = "grpcio-1.63.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:56cdf96ff82e3cc90dbe8bac260352993f23e8e256e063c327b6cf9c88daf7a9"},
+ {file = "grpcio-1.63.0-cp312-cp312-win32.whl", hash = "sha256:3a6d1f9ea965e750db7b4ee6f9fdef5fdf135abe8a249e75d84b0a3e0c668a1b"},
+ {file = "grpcio-1.63.0-cp312-cp312-win_amd64.whl", hash = "sha256:d2497769895bb03efe3187fb1888fc20e98a5f18b3d14b606167dacda5789434"},
+ {file = "grpcio-1.63.0-cp38-cp38-linux_armv7l.whl", hash = "sha256:fdf348ae69c6ff484402cfdb14e18c1b0054ac2420079d575c53a60b9b2853ae"},
+ {file = "grpcio-1.63.0-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:a3abfe0b0f6798dedd2e9e92e881d9acd0fdb62ae27dcbbfa7654a57e24060c0"},
+ {file = "grpcio-1.63.0-cp38-cp38-manylinux_2_17_aarch64.whl", hash = "sha256:6ef0ad92873672a2a3767cb827b64741c363ebaa27e7f21659e4e31f4d750280"},
+ {file = "grpcio-1.63.0-cp38-cp38-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b416252ac5588d9dfb8a30a191451adbf534e9ce5f56bb02cd193f12d8845b7f"},
+ {file = "grpcio-1.63.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e3b77eaefc74d7eb861d3ffbdf91b50a1bb1639514ebe764c47773b833fa2d91"},
+ {file = "grpcio-1.63.0-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:b005292369d9c1f80bf70c1db1c17c6c342da7576f1c689e8eee4fb0c256af85"},
+ {file = "grpcio-1.63.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:cdcda1156dcc41e042d1e899ba1f5c2e9f3cd7625b3d6ebfa619806a4c1aadda"},
+ {file = "grpcio-1.63.0-cp38-cp38-win32.whl", hash = "sha256:01799e8649f9e94ba7db1aeb3452188048b0019dc37696b0f5ce212c87c560c3"},
+ {file = "grpcio-1.63.0-cp38-cp38-win_amd64.whl", hash = "sha256:6a1a3642d76f887aa4009d92f71eb37809abceb3b7b5a1eec9c554a246f20e3a"},
+ {file = "grpcio-1.63.0-cp39-cp39-linux_armv7l.whl", hash = "sha256:75f701ff645858a2b16bc8c9fc68af215a8bb2d5a9b647448129de6e85d52bce"},
+ {file = "grpcio-1.63.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:cacdef0348a08e475a721967f48206a2254a1b26ee7637638d9e081761a5ba86"},
+ {file = "grpcio-1.63.0-cp39-cp39-manylinux_2_17_aarch64.whl", hash = "sha256:0697563d1d84d6985e40ec5ec596ff41b52abb3fd91ec240e8cb44a63b895094"},
+ {file = "grpcio-1.63.0-cp39-cp39-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6426e1fb92d006e47476d42b8f240c1d916a6d4423c5258ccc5b105e43438f61"},
+ {file = "grpcio-1.63.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e48cee31bc5f5a31fb2f3b573764bd563aaa5472342860edcc7039525b53e46a"},
+ {file = "grpcio-1.63.0-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:50344663068041b34a992c19c600236e7abb42d6ec32567916b87b4c8b8833b3"},
+ {file = "grpcio-1.63.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:259e11932230d70ef24a21b9fb5bb947eb4703f57865a404054400ee92f42f5d"},
+ {file = "grpcio-1.63.0-cp39-cp39-win32.whl", hash = "sha256:a44624aad77bf8ca198c55af811fd28f2b3eaf0a50ec5b57b06c034416ef2d0a"},
+ {file = "grpcio-1.63.0-cp39-cp39-win_amd64.whl", hash = "sha256:166e5c460e5d7d4656ff9e63b13e1f6029b122104c1633d5f37eaea348d7356d"},
+ {file = "grpcio-1.63.0.tar.gz", hash = "sha256:f3023e14805c61bc439fb40ca545ac3d5740ce66120a678a3c6c2c55b70343d1"},
+]
+
+[package.extras]
+protobuf = ["grpcio-tools (>=1.63.0)"]
+
+[[package]]
+name = "idna"
+version = "3.7"
+description = "Internationalized Domain Names in Applications (IDNA)"
+optional = false
+python-versions = ">=3.5"
+files = [
+ {file = "idna-3.7-py3-none-any.whl", hash = "sha256:82fee1fc78add43492d3a1898bfa6d8a904cc97d8427f683ed8e798d07761aa0"},
+ {file = "idna-3.7.tar.gz", hash = "sha256:028ff3aadf0609c1fd278d8ea3089299412a7a8b9bd005dd08b9f8285bcb5cfc"},
+]
+
+[[package]]
+name = "iniconfig"
+version = "2.0.0"
+description = "brain-dead simple config-ini parsing"
+optional = false
+python-versions = ">=3.7"
+files = [
+ {file = "iniconfig-2.0.0-py3-none-any.whl", hash = "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374"},
+ {file = "iniconfig-2.0.0.tar.gz", hash = "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3"},
+]
+
+[[package]]
+name = "jsonpatch"
+version = "1.33"
+description = "Apply JSON-Patches (RFC 6902)"
+optional = false
+python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, !=3.5.*, !=3.6.*"
+files = [
+ {file = "jsonpatch-1.33-py2.py3-none-any.whl", hash = "sha256:0ae28c0cd062bbd8b8ecc26d7d164fbbea9652a1a3693f3b956c1eae5145dade"},
+ {file = "jsonpatch-1.33.tar.gz", hash = "sha256:9fcd4009c41e6d12348b4a0ff2563ba56a2923a7dfee731d004e212e1ee5030c"},
+]
+
+[package.dependencies]
+jsonpointer = ">=1.9"
+
+[[package]]
+name = "jsonpointer"
+version = "2.4"
+description = "Identify specific nodes in a JSON document (RFC 6901)"
+optional = false
+python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, !=3.5.*, !=3.6.*"
+files = [
+ {file = "jsonpointer-2.4-py2.py3-none-any.whl", hash = "sha256:15d51bba20eea3165644553647711d150376234112651b4f1811022aecad7d7a"},
+ {file = "jsonpointer-2.4.tar.gz", hash = "sha256:585cee82b70211fa9e6043b7bb89db6e1aa49524340dde8ad6b63206ea689d88"},
+]
+
+[[package]]
+name = "langchain-core"
+version = "0.2.2rc1"
+description = "Building applications with LLMs through composability"
+optional = false
+python-versions = ">=3.8.1,<4.0"
+files = []
+develop = true
+
+[package.dependencies]
+jsonpatch = "^1.33"
+langsmith = "^0.1.0"
+packaging = "^23.2"
+pydantic = ">=1,<3"
+PyYAML = ">=5.3"
+tenacity = "^8.1.0"
+
+[package.extras]
+extended-testing = ["jinja2 (>=3,<4)"]
+
+[package.source]
+type = "directory"
+url = "../../core"
+
+[[package]]
+name = "langsmith"
+version = "0.1.63"
+description = "Client library to connect to the LangSmith LLM Tracing and Evaluation Platform."
+optional = false
+python-versions = "<4.0,>=3.8.1"
+files = [
+ {file = "langsmith-0.1.63-py3-none-any.whl", hash = "sha256:7810afdf5e3f3b472fc581a29371fb96cd843dde2149e048d1b9610325159d1e"},
+ {file = "langsmith-0.1.63.tar.gz", hash = "sha256:a609405b52f6f54df442a142cbf19ab38662d54e532f96028b4c546434d4afdf"},
+]
+
+[package.dependencies]
+orjson = ">=3.9.14,<4.0.0"
+pydantic = ">=1,<3"
+requests = ">=2,<3"
+
+[[package]]
+name = "marshmallow"
+version = "3.21.2"
+description = "A lightweight library for converting complex datatypes to and from native Python datatypes."
+optional = false
+python-versions = ">=3.8"
+files = [
+ {file = "marshmallow-3.21.2-py3-none-any.whl", hash = "sha256:70b54a6282f4704d12c0a41599682c5c5450e843b9ec406308653b47c59648a1"},
+ {file = "marshmallow-3.21.2.tar.gz", hash = "sha256:82408deadd8b33d56338d2182d455db632c6313aa2af61916672146bb32edc56"},
+]
+
+[package.dependencies]
+packaging = ">=17.0"
+
+[package.extras]
+dev = ["marshmallow[tests]", "pre-commit (>=3.5,<4.0)", "tox"]
+docs = ["alabaster (==0.7.16)", "autodocsumm (==0.2.12)", "sphinx (==7.3.7)", "sphinx-issues (==4.1.0)", "sphinx-version-warning (==1.1.2)"]
+tests = ["pytest", "pytz", "simplejson"]
+
+[[package]]
+name = "milvus-lite"
+version = "2.4.6"
+description = "A lightweight version of Milvus wrapped with Python."
+optional = false
+python-versions = ">=3.7"
+files = [
+ {file = "milvus_lite-2.4.6-py3-none-macosx_10_9_x86_64.whl", hash = "sha256:43ac9f36903b31455e50a8f1d9cb033e18971643029c89eb5c9610f01c1f2e26"},
+ {file = "milvus_lite-2.4.6-py3-none-macosx_11_0_arm64.whl", hash = "sha256:95afe2ee019c569713926747bbe18ab5944927797374fed796f00fbe564cccd6"},
+ {file = "milvus_lite-2.4.6-py3-none-manylinux2014_x86_64.whl", hash = "sha256:2f9116bfc6a0d95636d3aa144582486b622c492689f3c93c519101bd7158b7db"},
+]
+
+[[package]]
+name = "mypy"
+version = "0.991"
+description = "Optional static typing for Python"
+optional = false
+python-versions = ">=3.7"
+files = [
+ {file = "mypy-0.991-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:7d17e0a9707d0772f4a7b878f04b4fd11f6f5bcb9b3813975a9b13c9332153ab"},
+ {file = "mypy-0.991-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:0714258640194d75677e86c786e80ccf294972cc76885d3ebbb560f11db0003d"},
+ {file = "mypy-0.991-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:0c8f3be99e8a8bd403caa8c03be619544bc2c77a7093685dcf308c6b109426c6"},
+ {file = "mypy-0.991-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bc9ec663ed6c8f15f4ae9d3c04c989b744436c16d26580eaa760ae9dd5d662eb"},
+ {file = "mypy-0.991-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:4307270436fd7694b41f913eb09210faff27ea4979ecbcd849e57d2da2f65305"},
+ {file = "mypy-0.991-cp310-cp310-win_amd64.whl", hash = "sha256:901c2c269c616e6cb0998b33d4adbb4a6af0ac4ce5cd078afd7bc95830e62c1c"},
+ {file = "mypy-0.991-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:d13674f3fb73805ba0c45eb6c0c3053d218aa1f7abead6e446d474529aafc372"},
+ {file = "mypy-0.991-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:1c8cd4fb70e8584ca1ed5805cbc7c017a3d1a29fb450621089ffed3e99d1857f"},
+ {file = "mypy-0.991-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:209ee89fbb0deed518605edddd234af80506aec932ad28d73c08f1400ef80a33"},
+ {file = "mypy-0.991-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:37bd02ebf9d10e05b00d71302d2c2e6ca333e6c2a8584a98c00e038db8121f05"},
+ {file = "mypy-0.991-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:26efb2fcc6b67e4d5a55561f39176821d2adf88f2745ddc72751b7890f3194ad"},
+ {file = "mypy-0.991-cp311-cp311-win_amd64.whl", hash = "sha256:3a700330b567114b673cf8ee7388e949f843b356a73b5ab22dd7cff4742a5297"},
+ {file = "mypy-0.991-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:1f7d1a520373e2272b10796c3ff721ea1a0712288cafaa95931e66aa15798813"},
+ {file = "mypy-0.991-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:641411733b127c3e0dab94c45af15fea99e4468f99ac88b39efb1ad677da5711"},
+ {file = "mypy-0.991-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:3d80e36b7d7a9259b740be6d8d906221789b0d836201af4234093cae89ced0cd"},
+ {file = "mypy-0.991-cp37-cp37m-win_amd64.whl", hash = "sha256:e62ebaad93be3ad1a828a11e90f0e76f15449371ffeecca4a0a0b9adc99abcef"},
+ {file = "mypy-0.991-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:b86ce2c1866a748c0f6faca5232059f881cda6dda2a893b9a8373353cfe3715a"},
+ {file = "mypy-0.991-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:ac6e503823143464538efda0e8e356d871557ef60ccd38f8824a4257acc18d93"},
+ {file = "mypy-0.991-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:0cca5adf694af539aeaa6ac633a7afe9bbd760df9d31be55ab780b77ab5ae8bf"},
+ {file = "mypy-0.991-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a12c56bf73cdab116df96e4ff39610b92a348cc99a1307e1da3c3768bbb5b135"},
+ {file = "mypy-0.991-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:652b651d42f155033a1967739788c436491b577b6a44e4c39fb340d0ee7f0d70"},
+ {file = "mypy-0.991-cp38-cp38-win_amd64.whl", hash = "sha256:4175593dc25d9da12f7de8de873a33f9b2b8bdb4e827a7cae952e5b1a342e243"},
+ {file = "mypy-0.991-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:98e781cd35c0acf33eb0295e8b9c55cdbef64fcb35f6d3aa2186f289bed6e80d"},
+ {file = "mypy-0.991-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:6d7464bac72a85cb3491c7e92b5b62f3dcccb8af26826257760a552a5e244aa5"},
+ {file = "mypy-0.991-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:c9166b3f81a10cdf9b49f2d594b21b31adadb3d5e9db9b834866c3258b695be3"},
+ {file = "mypy-0.991-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b8472f736a5bfb159a5e36740847808f6f5b659960115ff29c7cecec1741c648"},
+ {file = "mypy-0.991-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:5e80e758243b97b618cdf22004beb09e8a2de1af481382e4d84bc52152d1c476"},
+ {file = "mypy-0.991-cp39-cp39-win_amd64.whl", hash = "sha256:74e259b5c19f70d35fcc1ad3d56499065c601dfe94ff67ae48b85596b9ec1461"},
+ {file = "mypy-0.991-py3-none-any.whl", hash = "sha256:de32edc9b0a7e67c2775e574cb061a537660e51210fbf6006b0b36ea695ae9bb"},
+ {file = "mypy-0.991.tar.gz", hash = "sha256:3c0165ba8f354a6d9881809ef29f1a9318a236a6d81c690094c5df32107bde06"},
+]
+
+[package.dependencies]
+mypy-extensions = ">=0.4.3"
+tomli = {version = ">=1.1.0", markers = "python_version < \"3.11\""}
+typing-extensions = ">=3.10"
+
+[package.extras]
+dmypy = ["psutil (>=4.0)"]
+install-types = ["pip"]
+python2 = ["typed-ast (>=1.4.0,<2)"]
+reports = ["lxml"]
+
+[[package]]
+name = "mypy-extensions"
+version = "1.0.0"
+description = "Type system extensions for programs checked with the mypy type checker."
+optional = false
+python-versions = ">=3.5"
+files = [
+ {file = "mypy_extensions-1.0.0-py3-none-any.whl", hash = "sha256:4392f6c0eb8a5668a69e23d168ffa70f0be9ccfd32b5cc2d26a34ae5b844552d"},
+ {file = "mypy_extensions-1.0.0.tar.gz", hash = "sha256:75dbf8955dc00442a438fc4d0666508a9a97b6bd41aa2f0ffe9d2f2725af0782"},
+]
+
+[[package]]
+name = "numpy"
+version = "1.24.4"
+description = "Fundamental package for array computing in Python"
+optional = false
+python-versions = ">=3.8"
+files = [
+ {file = "numpy-1.24.4-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:c0bfb52d2169d58c1cdb8cc1f16989101639b34c7d3ce60ed70b19c63eba0b64"},
+ {file = "numpy-1.24.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:ed094d4f0c177b1b8e7aa9cba7d6ceed51c0e569a5318ac0ca9a090680a6a1b1"},
+ {file = "numpy-1.24.4-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:79fc682a374c4a8ed08b331bef9c5f582585d1048fa6d80bc6c35bc384eee9b4"},
+ {file = "numpy-1.24.4-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7ffe43c74893dbf38c2b0a1f5428760a1a9c98285553c89e12d70a96a7f3a4d6"},
+ {file = "numpy-1.24.4-cp310-cp310-win32.whl", hash = "sha256:4c21decb6ea94057331e111a5bed9a79d335658c27ce2adb580fb4d54f2ad9bc"},
+ {file = "numpy-1.24.4-cp310-cp310-win_amd64.whl", hash = "sha256:b4bea75e47d9586d31e892a7401f76e909712a0fd510f58f5337bea9572c571e"},
+ {file = "numpy-1.24.4-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:f136bab9c2cfd8da131132c2cf6cc27331dd6fae65f95f69dcd4ae3c3639c810"},
+ {file = "numpy-1.24.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:e2926dac25b313635e4d6cf4dc4e51c8c0ebfed60b801c799ffc4c32bf3d1254"},
+ {file = "numpy-1.24.4-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:222e40d0e2548690405b0b3c7b21d1169117391c2e82c378467ef9ab4c8f0da7"},
+ {file = "numpy-1.24.4-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7215847ce88a85ce39baf9e89070cb860c98fdddacbaa6c0da3ffb31b3350bd5"},
+ {file = "numpy-1.24.4-cp311-cp311-win32.whl", hash = "sha256:4979217d7de511a8d57f4b4b5b2b965f707768440c17cb70fbf254c4b225238d"},
+ {file = "numpy-1.24.4-cp311-cp311-win_amd64.whl", hash = "sha256:b7b1fc9864d7d39e28f41d089bfd6353cb5f27ecd9905348c24187a768c79694"},
+ {file = "numpy-1.24.4-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:1452241c290f3e2a312c137a9999cdbf63f78864d63c79039bda65ee86943f61"},
+ {file = "numpy-1.24.4-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:04640dab83f7c6c85abf9cd729c5b65f1ebd0ccf9de90b270cd61935eef0197f"},
+ {file = "numpy-1.24.4-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a5425b114831d1e77e4b5d812b69d11d962e104095a5b9c3b641a218abcc050e"},
+ {file = "numpy-1.24.4-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dd80e219fd4c71fc3699fc1dadac5dcf4fd882bfc6f7ec53d30fa197b8ee22dc"},
+ {file = "numpy-1.24.4-cp38-cp38-win32.whl", hash = "sha256:4602244f345453db537be5314d3983dbf5834a9701b7723ec28923e2889e0bb2"},
+ {file = "numpy-1.24.4-cp38-cp38-win_amd64.whl", hash = "sha256:692f2e0f55794943c5bfff12b3f56f99af76f902fc47487bdfe97856de51a706"},
+ {file = "numpy-1.24.4-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:2541312fbf09977f3b3ad449c4e5f4bb55d0dbf79226d7724211acc905049400"},
+ {file = "numpy-1.24.4-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:9667575fb6d13c95f1b36aca12c5ee3356bf001b714fc354eb5465ce1609e62f"},
+ {file = "numpy-1.24.4-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f3a86ed21e4f87050382c7bc96571755193c4c1392490744ac73d660e8f564a9"},
+ {file = "numpy-1.24.4-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d11efb4dbecbdf22508d55e48d9c8384db795e1b7b51ea735289ff96613ff74d"},
+ {file = "numpy-1.24.4-cp39-cp39-win32.whl", hash = "sha256:6620c0acd41dbcb368610bb2f4d83145674040025e5536954782467100aa8835"},
+ {file = "numpy-1.24.4-cp39-cp39-win_amd64.whl", hash = "sha256:befe2bf740fd8373cf56149a5c23a0f601e82869598d41f8e188a0e9869926f8"},
+ {file = "numpy-1.24.4-pp38-pypy38_pp73-macosx_10_9_x86_64.whl", hash = "sha256:31f13e25b4e304632a4619d0e0777662c2ffea99fcae2029556b17d8ff958aef"},
+ {file = "numpy-1.24.4-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:95f7ac6540e95bc440ad77f56e520da5bf877f87dca58bd095288dce8940532a"},
+ {file = "numpy-1.24.4-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:e98f220aa76ca2a977fe435f5b04d7b3470c0a2e6312907b37ba6068f26787f2"},
+ {file = "numpy-1.24.4.tar.gz", hash = "sha256:80f5e3a4e498641401868df4208b74581206afbee7cf7b8329daae82676d9463"},
+]
+
+[[package]]
+name = "orjson"
+version = "3.10.3"
+description = "Fast, correct Python JSON library supporting dataclasses, datetimes, and numpy"
+optional = false
+python-versions = ">=3.8"
+files = [
+ {file = "orjson-3.10.3-cp310-cp310-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:9fb6c3f9f5490a3eb4ddd46fc1b6eadb0d6fc16fb3f07320149c3286a1409dd8"},
+ {file = "orjson-3.10.3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:252124b198662eee80428f1af8c63f7ff077c88723fe206a25df8dc57a57b1fa"},
+ {file = "orjson-3.10.3-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9f3e87733823089a338ef9bbf363ef4de45e5c599a9bf50a7a9b82e86d0228da"},
+ {file = "orjson-3.10.3-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c8334c0d87103bb9fbbe59b78129f1f40d1d1e8355bbed2ca71853af15fa4ed3"},
+ {file = "orjson-3.10.3-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1952c03439e4dce23482ac846e7961f9d4ec62086eb98ae76d97bd41d72644d7"},
+ {file = "orjson-3.10.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:c0403ed9c706dcd2809f1600ed18f4aae50be263bd7112e54b50e2c2bc3ebd6d"},
+ {file = "orjson-3.10.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:382e52aa4270a037d41f325e7d1dfa395b7de0c367800b6f337d8157367bf3a7"},
+ {file = "orjson-3.10.3-cp310-none-win32.whl", hash = "sha256:be2aab54313752c04f2cbaab4515291ef5af8c2256ce22abc007f89f42f49109"},
+ {file = "orjson-3.10.3-cp310-none-win_amd64.whl", hash = "sha256:416b195f78ae461601893f482287cee1e3059ec49b4f99479aedf22a20b1098b"},
+ {file = "orjson-3.10.3-cp311-cp311-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:73100d9abbbe730331f2242c1fc0bcb46a3ea3b4ae3348847e5a141265479700"},
+ {file = "orjson-3.10.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:544a12eee96e3ab828dbfcb4d5a0023aa971b27143a1d35dc214c176fdfb29b3"},
+ {file = "orjson-3.10.3-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:520de5e2ef0b4ae546bea25129d6c7c74edb43fc6cf5213f511a927f2b28148b"},
+ {file = "orjson-3.10.3-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ccaa0a401fc02e8828a5bedfd80f8cd389d24f65e5ca3954d72c6582495b4bcf"},
+ {file = "orjson-3.10.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9a7bc9e8bc11bac40f905640acd41cbeaa87209e7e1f57ade386da658092dc16"},
+ {file = "orjson-3.10.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:3582b34b70543a1ed6944aca75e219e1192661a63da4d039d088a09c67543b08"},
+ {file = "orjson-3.10.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:1c23dfa91481de880890d17aa7b91d586a4746a4c2aa9a145bebdbaf233768d5"},
+ {file = "orjson-3.10.3-cp311-none-win32.whl", hash = "sha256:1770e2a0eae728b050705206d84eda8b074b65ee835e7f85c919f5705b006c9b"},
+ {file = "orjson-3.10.3-cp311-none-win_amd64.whl", hash = "sha256:93433b3c1f852660eb5abdc1f4dd0ced2be031ba30900433223b28ee0140cde5"},
+ {file = "orjson-3.10.3-cp312-cp312-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:a39aa73e53bec8d410875683bfa3a8edf61e5a1c7bb4014f65f81d36467ea098"},
+ {file = "orjson-3.10.3-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0943a96b3fa09bee1afdfccc2cb236c9c64715afa375b2af296c73d91c23eab2"},
+ {file = "orjson-3.10.3-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e852baafceff8da3c9defae29414cc8513a1586ad93e45f27b89a639c68e8176"},
+ {file = "orjson-3.10.3-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:18566beb5acd76f3769c1d1a7ec06cdb81edc4d55d2765fb677e3eaa10fa99e0"},
+ {file = "orjson-3.10.3-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1bd2218d5a3aa43060efe649ec564ebedec8ce6ae0a43654b81376216d5ebd42"},
+ {file = "orjson-3.10.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:cf20465e74c6e17a104ecf01bf8cd3b7b252565b4ccee4548f18b012ff2f8069"},
+ {file = "orjson-3.10.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:ba7f67aa7f983c4345eeda16054a4677289011a478ca947cd69c0a86ea45e534"},
+ {file = "orjson-3.10.3-cp312-none-win32.whl", hash = "sha256:17e0713fc159abc261eea0f4feda611d32eabc35708b74bef6ad44f6c78d5ea0"},
+ {file = "orjson-3.10.3-cp312-none-win_amd64.whl", hash = "sha256:4c895383b1ec42b017dd2c75ae8a5b862fc489006afde06f14afbdd0309b2af0"},
+ {file = "orjson-3.10.3-cp38-cp38-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:be2719e5041e9fb76c8c2c06b9600fe8e8584e6980061ff88dcbc2691a16d20d"},
+ {file = "orjson-3.10.3-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cb0175a5798bdc878956099f5c54b9837cb62cfbf5d0b86ba6d77e43861bcec2"},
+ {file = "orjson-3.10.3-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:978be58a68ade24f1af7758626806e13cff7748a677faf95fbb298359aa1e20d"},
+ {file = "orjson-3.10.3-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:16bda83b5c61586f6f788333d3cf3ed19015e3b9019188c56983b5a299210eb5"},
+ {file = "orjson-3.10.3-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4ad1f26bea425041e0a1adad34630c4825a9e3adec49079b1fb6ac8d36f8b754"},
+ {file = "orjson-3.10.3-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:9e253498bee561fe85d6325ba55ff2ff08fb5e7184cd6a4d7754133bd19c9195"},
+ {file = "orjson-3.10.3-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:0a62f9968bab8a676a164263e485f30a0b748255ee2f4ae49a0224be95f4532b"},
+ {file = "orjson-3.10.3-cp38-none-win32.whl", hash = "sha256:8d0b84403d287d4bfa9bf7d1dc298d5c1c5d9f444f3737929a66f2fe4fb8f134"},
+ {file = "orjson-3.10.3-cp38-none-win_amd64.whl", hash = "sha256:8bc7a4df90da5d535e18157220d7915780d07198b54f4de0110eca6b6c11e290"},
+ {file = "orjson-3.10.3-cp39-cp39-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:9059d15c30e675a58fdcd6f95465c1522b8426e092de9fff20edebfdc15e1cb0"},
+ {file = "orjson-3.10.3-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8d40c7f7938c9c2b934b297412c067936d0b54e4b8ab916fd1a9eb8f54c02294"},
+ {file = "orjson-3.10.3-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d4a654ec1de8fdaae1d80d55cee65893cb06494e124681ab335218be6a0691e7"},
+ {file = "orjson-3.10.3-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:831c6ef73f9aa53c5f40ae8f949ff7681b38eaddb6904aab89dca4d85099cb78"},
+ {file = "orjson-3.10.3-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:99b880d7e34542db89f48d14ddecbd26f06838b12427d5a25d71baceb5ba119d"},
+ {file = "orjson-3.10.3-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:2e5e176c994ce4bd434d7aafb9ecc893c15f347d3d2bbd8e7ce0b63071c52e25"},
+ {file = "orjson-3.10.3-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:b69a58a37dab856491bf2d3bbf259775fdce262b727f96aafbda359cb1d114d8"},
+ {file = "orjson-3.10.3-cp39-none-win32.whl", hash = "sha256:b8d4d1a6868cde356f1402c8faeb50d62cee765a1f7ffcfd6de732ab0581e063"},
+ {file = "orjson-3.10.3-cp39-none-win_amd64.whl", hash = "sha256:5102f50c5fc46d94f2033fe00d392588564378260d64377aec702f21a7a22912"},
+ {file = "orjson-3.10.3.tar.gz", hash = "sha256:2b166507acae7ba2f7c315dcf185a9111ad5e992ac81f2d507aac39193c2c818"},
+]
+
+[[package]]
+name = "packaging"
+version = "23.2"
+description = "Core utilities for Python packages"
+optional = false
+python-versions = ">=3.7"
+files = [
+ {file = "packaging-23.2-py3-none-any.whl", hash = "sha256:8c491190033a9af7e1d931d0b5dacc2ef47509b34dd0de67ed209b5203fc88c7"},
+ {file = "packaging-23.2.tar.gz", hash = "sha256:048fb0e9405036518eaaf48a55953c750c11e1a1b68e0dd1a9d62ed0c092cfc5"},
+]
+
+[[package]]
+name = "pandas"
+version = "2.0.3"
+description = "Powerful data structures for data analysis, time series, and statistics"
+optional = false
+python-versions = ">=3.8"
+files = [
+ {file = "pandas-2.0.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:e4c7c9f27a4185304c7caf96dc7d91bc60bc162221152de697c98eb0b2648dd8"},
+ {file = "pandas-2.0.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:f167beed68918d62bffb6ec64f2e1d8a7d297a038f86d4aed056b9493fca407f"},
+ {file = "pandas-2.0.3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ce0c6f76a0f1ba361551f3e6dceaff06bde7514a374aa43e33b588ec10420183"},
+ {file = "pandas-2.0.3-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ba619e410a21d8c387a1ea6e8a0e49bb42216474436245718d7f2e88a2f8d7c0"},
+ {file = "pandas-2.0.3-cp310-cp310-win32.whl", hash = "sha256:3ef285093b4fe5058eefd756100a367f27029913760773c8bf1d2d8bebe5d210"},
+ {file = "pandas-2.0.3-cp310-cp310-win_amd64.whl", hash = "sha256:9ee1a69328d5c36c98d8e74db06f4ad518a1840e8ccb94a4ba86920986bb617e"},
+ {file = "pandas-2.0.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:b084b91d8d66ab19f5bb3256cbd5ea661848338301940e17f4492b2ce0801fe8"},
+ {file = "pandas-2.0.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:37673e3bdf1551b95bf5d4ce372b37770f9529743d2498032439371fc7b7eb26"},
+ {file = "pandas-2.0.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b9cb1e14fdb546396b7e1b923ffaeeac24e4cedd14266c3497216dd4448e4f2d"},
+ {file = "pandas-2.0.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d9cd88488cceb7635aebb84809d087468eb33551097d600c6dad13602029c2df"},
+ {file = "pandas-2.0.3-cp311-cp311-win32.whl", hash = "sha256:694888a81198786f0e164ee3a581df7d505024fbb1f15202fc7db88a71d84ebd"},
+ {file = "pandas-2.0.3-cp311-cp311-win_amd64.whl", hash = "sha256:6a21ab5c89dcbd57f78d0ae16630b090eec626360085a4148693def5452d8a6b"},
+ {file = "pandas-2.0.3-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:9e4da0d45e7f34c069fe4d522359df7d23badf83abc1d1cef398895822d11061"},
+ {file = "pandas-2.0.3-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:32fca2ee1b0d93dd71d979726b12b61faa06aeb93cf77468776287f41ff8fdc5"},
+ {file = "pandas-2.0.3-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:258d3624b3ae734490e4d63c430256e716f488c4fcb7c8e9bde2d3aa46c29089"},
+ {file = "pandas-2.0.3-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9eae3dc34fa1aa7772dd3fc60270d13ced7346fcbcfee017d3132ec625e23bb0"},
+ {file = "pandas-2.0.3-cp38-cp38-win32.whl", hash = "sha256:f3421a7afb1a43f7e38e82e844e2bca9a6d793d66c1a7f9f0ff39a795bbc5e02"},
+ {file = "pandas-2.0.3-cp38-cp38-win_amd64.whl", hash = "sha256:69d7f3884c95da3a31ef82b7618af5710dba95bb885ffab339aad925c3e8ce78"},
+ {file = "pandas-2.0.3-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:5247fb1ba347c1261cbbf0fcfba4a3121fbb4029d95d9ef4dc45406620b25c8b"},
+ {file = "pandas-2.0.3-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:81af086f4543c9d8bb128328b5d32e9986e0c84d3ee673a2ac6fb57fd14f755e"},
+ {file = "pandas-2.0.3-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1994c789bf12a7c5098277fb43836ce090f1073858c10f9220998ac74f37c69b"},
+ {file = "pandas-2.0.3-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5ec591c48e29226bcbb316e0c1e9423622bc7a4eaf1ef7c3c9fa1a3981f89641"},
+ {file = "pandas-2.0.3-cp39-cp39-win32.whl", hash = "sha256:04dbdbaf2e4d46ca8da896e1805bc04eb85caa9a82e259e8eed00254d5e0c682"},
+ {file = "pandas-2.0.3-cp39-cp39-win_amd64.whl", hash = "sha256:1168574b036cd8b93abc746171c9b4f1b83467438a5e45909fed645cf8692dbc"},
+ {file = "pandas-2.0.3.tar.gz", hash = "sha256:c02f372a88e0d17f36d3093a644c73cfc1788e876a7c4bcb4020a77512e2043c"},
+]
+
+[package.dependencies]
+numpy = [
+ {version = ">=1.20.3", markers = "python_version < \"3.10\""},
+ {version = ">=1.21.0", markers = "python_version >= \"3.10\" and python_version < \"3.11\""},
+ {version = ">=1.23.2", markers = "python_version >= \"3.11\""},
+]
+python-dateutil = ">=2.8.2"
+pytz = ">=2020.1"
+tzdata = ">=2022.1"
+
+[package.extras]
+all = ["PyQt5 (>=5.15.1)", "SQLAlchemy (>=1.4.16)", "beautifulsoup4 (>=4.9.3)", "bottleneck (>=1.3.2)", "brotlipy (>=0.7.0)", "fastparquet (>=0.6.3)", "fsspec (>=2021.07.0)", "gcsfs (>=2021.07.0)", "html5lib (>=1.1)", "hypothesis (>=6.34.2)", "jinja2 (>=3.0.0)", "lxml (>=4.6.3)", "matplotlib (>=3.6.1)", "numba (>=0.53.1)", "numexpr (>=2.7.3)", "odfpy (>=1.4.1)", "openpyxl (>=3.0.7)", "pandas-gbq (>=0.15.0)", "psycopg2 (>=2.8.6)", "pyarrow (>=7.0.0)", "pymysql (>=1.0.2)", "pyreadstat (>=1.1.2)", "pytest (>=7.3.2)", "pytest-asyncio (>=0.17.0)", "pytest-xdist (>=2.2.0)", "python-snappy (>=0.6.0)", "pyxlsb (>=1.0.8)", "qtpy (>=2.2.0)", "s3fs (>=2021.08.0)", "scipy (>=1.7.1)", "tables (>=3.6.1)", "tabulate (>=0.8.9)", "xarray (>=0.21.0)", "xlrd (>=2.0.1)", "xlsxwriter (>=1.4.3)", "zstandard (>=0.15.2)"]
+aws = ["s3fs (>=2021.08.0)"]
+clipboard = ["PyQt5 (>=5.15.1)", "qtpy (>=2.2.0)"]
+compression = ["brotlipy (>=0.7.0)", "python-snappy (>=0.6.0)", "zstandard (>=0.15.2)"]
+computation = ["scipy (>=1.7.1)", "xarray (>=0.21.0)"]
+excel = ["odfpy (>=1.4.1)", "openpyxl (>=3.0.7)", "pyxlsb (>=1.0.8)", "xlrd (>=2.0.1)", "xlsxwriter (>=1.4.3)"]
+feather = ["pyarrow (>=7.0.0)"]
+fss = ["fsspec (>=2021.07.0)"]
+gcp = ["gcsfs (>=2021.07.0)", "pandas-gbq (>=0.15.0)"]
+hdf5 = ["tables (>=3.6.1)"]
+html = ["beautifulsoup4 (>=4.9.3)", "html5lib (>=1.1)", "lxml (>=4.6.3)"]
+mysql = ["SQLAlchemy (>=1.4.16)", "pymysql (>=1.0.2)"]
+output-formatting = ["jinja2 (>=3.0.0)", "tabulate (>=0.8.9)"]
+parquet = ["pyarrow (>=7.0.0)"]
+performance = ["bottleneck (>=1.3.2)", "numba (>=0.53.1)", "numexpr (>=2.7.1)"]
+plot = ["matplotlib (>=3.6.1)"]
+postgresql = ["SQLAlchemy (>=1.4.16)", "psycopg2 (>=2.8.6)"]
+spss = ["pyreadstat (>=1.1.2)"]
+sql-other = ["SQLAlchemy (>=1.4.16)"]
+test = ["hypothesis (>=6.34.2)", "pytest (>=7.3.2)", "pytest-asyncio (>=0.17.0)", "pytest-xdist (>=2.2.0)"]
+xml = ["lxml (>=4.6.3)"]
+
+[[package]]
+name = "pluggy"
+version = "1.5.0"
+description = "plugin and hook calling mechanisms for python"
+optional = false
+python-versions = ">=3.8"
+files = [
+ {file = "pluggy-1.5.0-py3-none-any.whl", hash = "sha256:44e1ad92c8ca002de6377e165f3e0f1be63266ab4d554740532335b9d75ea669"},
+ {file = "pluggy-1.5.0.tar.gz", hash = "sha256:2cffa88e94fdc978c4c574f15f9e59b7f4201d439195c3715ca9e2486f1d0cf1"},
+]
+
+[package.extras]
+dev = ["pre-commit", "tox"]
+testing = ["pytest", "pytest-benchmark"]
+
+[[package]]
+name = "protobuf"
+version = "5.27.0"
+description = ""
+optional = false
+python-versions = ">=3.8"
+files = [
+ {file = "protobuf-5.27.0-cp310-abi3-win32.whl", hash = "sha256:2f83bf341d925650d550b8932b71763321d782529ac0eaf278f5242f513cc04e"},
+ {file = "protobuf-5.27.0-cp310-abi3-win_amd64.whl", hash = "sha256:b276e3f477ea1eebff3c2e1515136cfcff5ac14519c45f9b4aa2f6a87ea627c4"},
+ {file = "protobuf-5.27.0-cp38-abi3-macosx_10_9_universal2.whl", hash = "sha256:744489f77c29174328d32f8921566fb0f7080a2f064c5137b9d6f4b790f9e0c1"},
+ {file = "protobuf-5.27.0-cp38-abi3-manylinux2014_aarch64.whl", hash = "sha256:f51f33d305e18646f03acfdb343aac15b8115235af98bc9f844bf9446573827b"},
+ {file = "protobuf-5.27.0-cp38-abi3-manylinux2014_x86_64.whl", hash = "sha256:56937f97ae0dcf4e220ff2abb1456c51a334144c9960b23597f044ce99c29c89"},
+ {file = "protobuf-5.27.0-cp38-cp38-win32.whl", hash = "sha256:a17f4d664ea868102feaa30a674542255f9f4bf835d943d588440d1f49a3ed15"},
+ {file = "protobuf-5.27.0-cp38-cp38-win_amd64.whl", hash = "sha256:aabbbcf794fbb4c692ff14ce06780a66d04758435717107c387f12fb477bf0d8"},
+ {file = "protobuf-5.27.0-cp39-cp39-win32.whl", hash = "sha256:587be23f1212da7a14a6c65fd61995f8ef35779d4aea9e36aad81f5f3b80aec5"},
+ {file = "protobuf-5.27.0-cp39-cp39-win_amd64.whl", hash = "sha256:7cb65fc8fba680b27cf7a07678084c6e68ee13cab7cace734954c25a43da6d0f"},
+ {file = "protobuf-5.27.0-py3-none-any.whl", hash = "sha256:673ad60f1536b394b4fa0bcd3146a4130fcad85bfe3b60eaa86d6a0ace0fa374"},
+ {file = "protobuf-5.27.0.tar.gz", hash = "sha256:07f2b9a15255e3cf3f137d884af7972407b556a7a220912b252f26dc3121e6bf"},
+]
+
+[[package]]
+name = "pydantic"
+version = "2.7.1"
+description = "Data validation using Python type hints"
+optional = false
+python-versions = ">=3.8"
+files = [
+ {file = "pydantic-2.7.1-py3-none-any.whl", hash = "sha256:e029badca45266732a9a79898a15ae2e8b14840b1eabbb25844be28f0b33f3d5"},
+ {file = "pydantic-2.7.1.tar.gz", hash = "sha256:e9dbb5eada8abe4d9ae5f46b9939aead650cd2b68f249bb3a8139dbe125803cc"},
+]
+
+[package.dependencies]
+annotated-types = ">=0.4.0"
+pydantic-core = "2.18.2"
+typing-extensions = ">=4.6.1"
+
+[package.extras]
+email = ["email-validator (>=2.0.0)"]
+
+[[package]]
+name = "pydantic-core"
+version = "2.18.2"
+description = "Core functionality for Pydantic validation and serialization"
+optional = false
+python-versions = ">=3.8"
+files = [
+ {file = "pydantic_core-2.18.2-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:9e08e867b306f525802df7cd16c44ff5ebbe747ff0ca6cf3fde7f36c05a59a81"},
+ {file = "pydantic_core-2.18.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:f0a21cbaa69900cbe1a2e7cad2aa74ac3cf21b10c3efb0fa0b80305274c0e8a2"},
+ {file = "pydantic_core-2.18.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0680b1f1f11fda801397de52c36ce38ef1c1dc841a0927a94f226dea29c3ae3d"},
+ {file = "pydantic_core-2.18.2-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:95b9d5e72481d3780ba3442eac863eae92ae43a5f3adb5b4d0a1de89d42bb250"},
+ {file = "pydantic_core-2.18.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c4fcf5cd9c4b655ad666ca332b9a081112cd7a58a8b5a6ca7a3104bc950f2038"},
+ {file = "pydantic_core-2.18.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9b5155ff768083cb1d62f3e143b49a8a3432e6789a3abee8acd005c3c7af1c74"},
+ {file = "pydantic_core-2.18.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:553ef617b6836fc7e4df130bb851e32fe357ce36336d897fd6646d6058d980af"},
+ {file = "pydantic_core-2.18.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:b89ed9eb7d616ef5714e5590e6cf7f23b02d0d539767d33561e3675d6f9e3857"},
+ {file = "pydantic_core-2.18.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:75f7e9488238e920ab6204399ded280dc4c307d034f3924cd7f90a38b1829563"},
+ {file = "pydantic_core-2.18.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:ef26c9e94a8c04a1b2924149a9cb081836913818e55681722d7f29af88fe7b38"},
+ {file = "pydantic_core-2.18.2-cp310-none-win32.whl", hash = "sha256:182245ff6b0039e82b6bb585ed55a64d7c81c560715d1bad0cbad6dfa07b4027"},
+ {file = "pydantic_core-2.18.2-cp310-none-win_amd64.whl", hash = "sha256:e23ec367a948b6d812301afc1b13f8094ab7b2c280af66ef450efc357d2ae543"},
+ {file = "pydantic_core-2.18.2-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:219da3f096d50a157f33645a1cf31c0ad1fe829a92181dd1311022f986e5fbe3"},
+ {file = "pydantic_core-2.18.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:cc1cfd88a64e012b74e94cd00bbe0f9c6df57049c97f02bb07d39e9c852e19a4"},
+ {file = "pydantic_core-2.18.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:05b7133a6e6aeb8df37d6f413f7705a37ab4031597f64ab56384c94d98fa0e90"},
+ {file = "pydantic_core-2.18.2-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:224c421235f6102e8737032483f43c1a8cfb1d2f45740c44166219599358c2cd"},
+ {file = "pydantic_core-2.18.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b14d82cdb934e99dda6d9d60dc84a24379820176cc4a0d123f88df319ae9c150"},
+ {file = "pydantic_core-2.18.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2728b01246a3bba6de144f9e3115b532ee44bd6cf39795194fb75491824a1413"},
+ {file = "pydantic_core-2.18.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:470b94480bb5ee929f5acba6995251ada5e059a5ef3e0dfc63cca287283ebfa6"},
+ {file = "pydantic_core-2.18.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:997abc4df705d1295a42f95b4eec4950a37ad8ae46d913caeee117b6b198811c"},
+ {file = "pydantic_core-2.18.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:75250dbc5290e3f1a0f4618db35e51a165186f9034eff158f3d490b3fed9f8a0"},
+ {file = "pydantic_core-2.18.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:4456f2dca97c425231d7315737d45239b2b51a50dc2b6f0c2bb181fce6207664"},
+ {file = "pydantic_core-2.18.2-cp311-none-win32.whl", hash = "sha256:269322dcc3d8bdb69f054681edff86276b2ff972447863cf34c8b860f5188e2e"},
+ {file = "pydantic_core-2.18.2-cp311-none-win_amd64.whl", hash = "sha256:800d60565aec896f25bc3cfa56d2277d52d5182af08162f7954f938c06dc4ee3"},
+ {file = "pydantic_core-2.18.2-cp311-none-win_arm64.whl", hash = "sha256:1404c69d6a676245199767ba4f633cce5f4ad4181f9d0ccb0577e1f66cf4c46d"},
+ {file = "pydantic_core-2.18.2-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:fb2bd7be70c0fe4dfd32c951bc813d9fe6ebcbfdd15a07527796c8204bd36242"},
+ {file = "pydantic_core-2.18.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:6132dd3bd52838acddca05a72aafb6eab6536aa145e923bb50f45e78b7251043"},
+ {file = "pydantic_core-2.18.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d7d904828195733c183d20a54230c0df0eb46ec746ea1a666730787353e87182"},
+ {file = "pydantic_core-2.18.2-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:c9bd70772c720142be1020eac55f8143a34ec9f82d75a8e7a07852023e46617f"},
+ {file = "pydantic_core-2.18.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:2b8ed04b3582771764538f7ee7001b02e1170223cf9b75dff0bc698fadb00cf3"},
+ {file = "pydantic_core-2.18.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e6dac87ddb34aaec85f873d737e9d06a3555a1cc1a8e0c44b7f8d5daeb89d86f"},
+ {file = "pydantic_core-2.18.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7ca4ae5a27ad7a4ee5170aebce1574b375de390bc01284f87b18d43a3984df72"},
+ {file = "pydantic_core-2.18.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:886eec03591b7cf058467a70a87733b35f44707bd86cf64a615584fd72488b7c"},
+ {file = "pydantic_core-2.18.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:ca7b0c1f1c983e064caa85f3792dd2fe3526b3505378874afa84baf662e12241"},
+ {file = "pydantic_core-2.18.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:4b4356d3538c3649337df4074e81b85f0616b79731fe22dd11b99499b2ebbdf3"},
+ {file = "pydantic_core-2.18.2-cp312-none-win32.whl", hash = "sha256:8b172601454f2d7701121bbec3425dd71efcb787a027edf49724c9cefc14c038"},
+ {file = "pydantic_core-2.18.2-cp312-none-win_amd64.whl", hash = "sha256:b1bd7e47b1558ea872bd16c8502c414f9e90dcf12f1395129d7bb42a09a95438"},
+ {file = "pydantic_core-2.18.2-cp312-none-win_arm64.whl", hash = "sha256:98758d627ff397e752bc339272c14c98199c613f922d4a384ddc07526c86a2ec"},
+ {file = "pydantic_core-2.18.2-cp38-cp38-macosx_10_12_x86_64.whl", hash = "sha256:9fdad8e35f278b2c3eb77cbdc5c0a49dada440657bf738d6905ce106dc1de439"},
+ {file = "pydantic_core-2.18.2-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:1d90c3265ae107f91a4f279f4d6f6f1d4907ac76c6868b27dc7fb33688cfb347"},
+ {file = "pydantic_core-2.18.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:390193c770399861d8df9670fb0d1874f330c79caaca4642332df7c682bf6b91"},
+ {file = "pydantic_core-2.18.2-cp38-cp38-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:82d5d4d78e4448683cb467897fe24e2b74bb7b973a541ea1dcfec1d3cbce39fb"},
+ {file = "pydantic_core-2.18.2-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4774f3184d2ef3e14e8693194f661dea5a4d6ca4e3dc8e39786d33a94865cefd"},
+ {file = "pydantic_core-2.18.2-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d4d938ec0adf5167cb335acb25a4ee69a8107e4984f8fbd2e897021d9e4ca21b"},
+ {file = "pydantic_core-2.18.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e0e8b1be28239fc64a88a8189d1df7fad8be8c1ae47fcc33e43d4be15f99cc70"},
+ {file = "pydantic_core-2.18.2-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:868649da93e5a3d5eacc2b5b3b9235c98ccdbfd443832f31e075f54419e1b96b"},
+ {file = "pydantic_core-2.18.2-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:78363590ef93d5d226ba21a90a03ea89a20738ee5b7da83d771d283fd8a56761"},
+ {file = "pydantic_core-2.18.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:852e966fbd035a6468fc0a3496589b45e2208ec7ca95c26470a54daed82a0788"},
+ {file = "pydantic_core-2.18.2-cp38-none-win32.whl", hash = "sha256:6a46e22a707e7ad4484ac9ee9f290f9d501df45954184e23fc29408dfad61350"},
+ {file = "pydantic_core-2.18.2-cp38-none-win_amd64.whl", hash = "sha256:d91cb5ea8b11607cc757675051f61b3d93f15eca3cefb3e6c704a5d6e8440f4e"},
+ {file = "pydantic_core-2.18.2-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:ae0a8a797a5e56c053610fa7be147993fe50960fa43609ff2a9552b0e07013e8"},
+ {file = "pydantic_core-2.18.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:042473b6280246b1dbf530559246f6842b56119c2926d1e52b631bdc46075f2a"},
+ {file = "pydantic_core-2.18.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1a388a77e629b9ec814c1b1e6b3b595fe521d2cdc625fcca26fbc2d44c816804"},
+ {file = "pydantic_core-2.18.2-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e25add29b8f3b233ae90ccef2d902d0ae0432eb0d45370fe315d1a5cf231004b"},
+ {file = "pydantic_core-2.18.2-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f459a5ce8434614dfd39bbebf1041952ae01da6bed9855008cb33b875cb024c0"},
+ {file = "pydantic_core-2.18.2-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:eff2de745698eb46eeb51193a9f41d67d834d50e424aef27df2fcdee1b153845"},
+ {file = "pydantic_core-2.18.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a8309f67285bdfe65c372ea3722b7a5642680f3dba538566340a9d36e920b5f0"},
+ {file = "pydantic_core-2.18.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:f93a8a2e3938ff656a7c1bc57193b1319960ac015b6e87d76c76bf14fe0244b4"},
+ {file = "pydantic_core-2.18.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:22057013c8c1e272eb8d0eebc796701167d8377441ec894a8fed1af64a0bf399"},
+ {file = "pydantic_core-2.18.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:cfeecd1ac6cc1fb2692c3d5110781c965aabd4ec5d32799773ca7b1456ac636b"},
+ {file = "pydantic_core-2.18.2-cp39-none-win32.whl", hash = "sha256:0d69b4c2f6bb3e130dba60d34c0845ba31b69babdd3f78f7c0c8fae5021a253e"},
+ {file = "pydantic_core-2.18.2-cp39-none-win_amd64.whl", hash = "sha256:d9319e499827271b09b4e411905b24a426b8fb69464dfa1696258f53a3334641"},
+ {file = "pydantic_core-2.18.2-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:a1874c6dd4113308bd0eb568418e6114b252afe44319ead2b4081e9b9521fe75"},
+ {file = "pydantic_core-2.18.2-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:ccdd111c03bfd3666bd2472b674c6899550e09e9f298954cfc896ab92b5b0e6d"},
+ {file = "pydantic_core-2.18.2-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e18609ceaa6eed63753037fc06ebb16041d17d28199ae5aba0052c51449650a9"},
+ {file = "pydantic_core-2.18.2-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6e5c584d357c4e2baf0ff7baf44f4994be121e16a2c88918a5817331fc7599d7"},
+ {file = "pydantic_core-2.18.2-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:43f0f463cf89ace478de71a318b1b4f05ebc456a9b9300d027b4b57c1a2064fb"},
+ {file = "pydantic_core-2.18.2-pp310-pypy310_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:e1b395e58b10b73b07b7cf740d728dd4ff9365ac46c18751bf8b3d8cca8f625a"},
+ {file = "pydantic_core-2.18.2-pp310-pypy310_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:0098300eebb1c837271d3d1a2cd2911e7c11b396eac9661655ee524a7f10587b"},
+ {file = "pydantic_core-2.18.2-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:36789b70d613fbac0a25bb07ab3d9dba4d2e38af609c020cf4d888d165ee0bf3"},
+ {file = "pydantic_core-2.18.2-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:3f9a801e7c8f1ef8718da265bba008fa121243dfe37c1cea17840b0944dfd72c"},
+ {file = "pydantic_core-2.18.2-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:3a6515ebc6e69d85502b4951d89131ca4e036078ea35533bb76327f8424531ce"},
+ {file = "pydantic_core-2.18.2-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:20aca1e2298c56ececfd8ed159ae4dde2df0781988c97ef77d5c16ff4bd5b400"},
+ {file = "pydantic_core-2.18.2-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:223ee893d77a310a0391dca6df00f70bbc2f36a71a895cecd9a0e762dc37b349"},
+ {file = "pydantic_core-2.18.2-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2334ce8c673ee93a1d6a65bd90327588387ba073c17e61bf19b4fd97d688d63c"},
+ {file = "pydantic_core-2.18.2-pp39-pypy39_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:cbca948f2d14b09d20268cda7b0367723d79063f26c4ffc523af9042cad95592"},
+ {file = "pydantic_core-2.18.2-pp39-pypy39_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:b3ef08e20ec49e02d5c6717a91bb5af9b20f1805583cb0adfe9ba2c6b505b5ae"},
+ {file = "pydantic_core-2.18.2-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:c6fdc8627910eed0c01aed6a390a252fe3ea6d472ee70fdde56273f198938374"},
+ {file = "pydantic_core-2.18.2.tar.gz", hash = "sha256:2e29d20810dfc3043ee13ac7d9e25105799817683348823f305ab3f349b9386e"},
+]
+
+[package.dependencies]
+typing-extensions = ">=4.6.0,<4.7.0 || >4.7.0"
+
+[[package]]
+name = "pymilvus"
+version = "2.4.3"
+description = "Python Sdk for Milvus"
+optional = false
+python-versions = ">=3.8"
+files = [
+ {file = "pymilvus-2.4.3-py3-none-any.whl", hash = "sha256:38239e89f8d739f665141d0b80908990b5f59681e889e135c234a4a45669a5c8"},
+ {file = "pymilvus-2.4.3.tar.gz", hash = "sha256:703ac29296cdce03d6dc2aaebbe959e57745c141a94150e371dc36c61c226cc1"},
+]
+
+[package.dependencies]
+environs = "<=9.5.0"
+grpcio = ">=1.49.1,<=1.63.0"
+milvus-lite = ">=2.4.0,<2.5.0"
+numpy = {version = "<1.25.0", markers = "python_version <= \"3.8\""}
+pandas = ">=1.2.4"
+protobuf = ">=3.20.0"
+setuptools = ">=67"
+ujson = ">=2.0.0"
+
+[package.extras]
+bulk-writer = ["azure-storage-blob", "minio (>=7.0.0)", "pyarrow (>=12.0.0)", "requests"]
+dev = ["black", "grpcio (==1.62.2)", "grpcio-testing (==1.62.2)", "grpcio-tools (==1.62.2)", "pytest (>=5.3.4)", "pytest-cov (>=2.8.1)", "pytest-timeout (>=1.3.4)", "ruff (>0.4.0)"]
+model = ["milvus-model (>=0.1.0)"]
+
+[[package]]
+name = "pytest"
+version = "7.4.4"
+description = "pytest: simple powerful testing with Python"
+optional = false
+python-versions = ">=3.7"
+files = [
+ {file = "pytest-7.4.4-py3-none-any.whl", hash = "sha256:b090cdf5ed60bf4c45261be03239c2c1c22df034fbffe691abe93cd80cea01d8"},
+ {file = "pytest-7.4.4.tar.gz", hash = "sha256:2cf0005922c6ace4a3e2ec8b4080eb0d9753fdc93107415332f50ce9e7994280"},
+]
+
+[package.dependencies]
+colorama = {version = "*", markers = "sys_platform == \"win32\""}
+exceptiongroup = {version = ">=1.0.0rc8", markers = "python_version < \"3.11\""}
+iniconfig = "*"
+packaging = "*"
+pluggy = ">=0.12,<2.0"
+tomli = {version = ">=1.0.0", markers = "python_version < \"3.11\""}
+
+[package.extras]
+testing = ["argcomplete", "attrs (>=19.2.0)", "hypothesis (>=3.56)", "mock", "nose", "pygments (>=2.7.2)", "requests", "setuptools", "xmlschema"]
+
+[[package]]
+name = "pytest-asyncio"
+version = "0.21.2"
+description = "Pytest support for asyncio"
+optional = false
+python-versions = ">=3.7"
+files = [
+ {file = "pytest_asyncio-0.21.2-py3-none-any.whl", hash = "sha256:ab664c88bb7998f711d8039cacd4884da6430886ae8bbd4eded552ed2004f16b"},
+ {file = "pytest_asyncio-0.21.2.tar.gz", hash = "sha256:d67738fc232b94b326b9d060750beb16e0074210b98dd8b58a5239fa2a154f45"},
+]
+
+[package.dependencies]
+pytest = ">=7.0.0"
+
+[package.extras]
+docs = ["sphinx (>=5.3)", "sphinx-rtd-theme (>=1.0)"]
+testing = ["coverage (>=6.2)", "flaky (>=3.5.0)", "hypothesis (>=5.7.1)", "mypy (>=0.931)", "pytest-trio (>=0.7.0)"]
+
+[[package]]
+name = "pytest-mock"
+version = "3.14.0"
+description = "Thin-wrapper around the mock package for easier use with pytest"
+optional = false
+python-versions = ">=3.8"
+files = [
+ {file = "pytest-mock-3.14.0.tar.gz", hash = "sha256:2719255a1efeceadbc056d6bf3df3d1c5015530fb40cf347c0f9afac88410bd0"},
+ {file = "pytest_mock-3.14.0-py3-none-any.whl", hash = "sha256:0b72c38033392a5f4621342fe11e9219ac11ec9d375f8e2a0c164539e0d70f6f"},
+]
+
+[package.dependencies]
+pytest = ">=6.2.5"
+
+[package.extras]
+dev = ["pre-commit", "pytest-asyncio", "tox"]
+
+[[package]]
+name = "pytest-watcher"
+version = "0.3.5"
+description = "Automatically rerun your tests on file modifications"
+optional = false
+python-versions = ">=3.7.0,<4.0.0"
+files = [
+ {file = "pytest_watcher-0.3.5-py3-none-any.whl", hash = "sha256:af00ca52c7be22dc34c0fd3d7ffef99057207a73b05dc5161fe3b2fe91f58130"},
+ {file = "pytest_watcher-0.3.5.tar.gz", hash = "sha256:8896152460ba2b1a8200c12117c6611008ec96c8b2d811f0a05ab8a82b043ff8"},
+]
+
+[package.dependencies]
+tomli = {version = ">=2.0.1,<3.0.0", markers = "python_version < \"3.11\""}
+watchdog = ">=2.0.0"
+
+[[package]]
+name = "python-dateutil"
+version = "2.9.0.post0"
+description = "Extensions to the standard Python datetime module"
+optional = false
+python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,>=2.7"
+files = [
+ {file = "python-dateutil-2.9.0.post0.tar.gz", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3"},
+ {file = "python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427"},
+]
+
+[package.dependencies]
+six = ">=1.5"
+
+[[package]]
+name = "python-dotenv"
+version = "1.0.1"
+description = "Read key-value pairs from a .env file and set them as environment variables"
+optional = false
+python-versions = ">=3.8"
+files = [
+ {file = "python-dotenv-1.0.1.tar.gz", hash = "sha256:e324ee90a023d808f1959c46bcbc04446a10ced277783dc6ee09987c37ec10ca"},
+ {file = "python_dotenv-1.0.1-py3-none-any.whl", hash = "sha256:f7b63ef50f1b690dddf550d03497b66d609393b40b564ed0d674909a68ebf16a"},
+]
+
+[package.extras]
+cli = ["click (>=5.0)"]
+
+[[package]]
+name = "pytz"
+version = "2024.1"
+description = "World timezone definitions, modern and historical"
+optional = false
+python-versions = "*"
+files = [
+ {file = "pytz-2024.1-py2.py3-none-any.whl", hash = "sha256:328171f4e3623139da4983451950b28e95ac706e13f3f2630a879749e7a8b319"},
+ {file = "pytz-2024.1.tar.gz", hash = "sha256:2a29735ea9c18baf14b448846bde5a48030ed267578472d8955cd0e7443a9812"},
+]
+
+[[package]]
+name = "pyyaml"
+version = "6.0.1"
+description = "YAML parser and emitter for Python"
+optional = false
+python-versions = ">=3.6"
+files = [
+ {file = "PyYAML-6.0.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:d858aa552c999bc8a8d57426ed01e40bef403cd8ccdd0fc5f6f04a00414cac2a"},
+ {file = "PyYAML-6.0.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:fd66fc5d0da6d9815ba2cebeb4205f95818ff4b79c3ebe268e75d961704af52f"},
+ {file = "PyYAML-6.0.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:69b023b2b4daa7548bcfbd4aa3da05b3a74b772db9e23b982788168117739938"},
+ {file = "PyYAML-6.0.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:81e0b275a9ecc9c0c0c07b4b90ba548307583c125f54d5b6946cfee6360c733d"},
+ {file = "PyYAML-6.0.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ba336e390cd8e4d1739f42dfe9bb83a3cc2e80f567d8805e11b46f4a943f5515"},
+ {file = "PyYAML-6.0.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:326c013efe8048858a6d312ddd31d56e468118ad4cdeda36c719bf5bb6192290"},
+ {file = "PyYAML-6.0.1-cp310-cp310-win32.whl", hash = "sha256:bd4af7373a854424dabd882decdc5579653d7868b8fb26dc7d0e99f823aa5924"},
+ {file = "PyYAML-6.0.1-cp310-cp310-win_amd64.whl", hash = "sha256:fd1592b3fdf65fff2ad0004b5e363300ef59ced41c2e6b3a99d4089fa8c5435d"},
+ {file = "PyYAML-6.0.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:6965a7bc3cf88e5a1c3bd2e0b5c22f8d677dc88a455344035f03399034eb3007"},
+ {file = "PyYAML-6.0.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:f003ed9ad21d6a4713f0a9b5a7a0a79e08dd0f221aff4525a2be4c346ee60aab"},
+ {file = "PyYAML-6.0.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:42f8152b8dbc4fe7d96729ec2b99c7097d656dc1213a3229ca5383f973a5ed6d"},
+ {file = "PyYAML-6.0.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:062582fca9fabdd2c8b54a3ef1c978d786e0f6b3a1510e0ac93ef59e0ddae2bc"},
+ {file = "PyYAML-6.0.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d2b04aac4d386b172d5b9692e2d2da8de7bfb6c387fa4f801fbf6fb2e6ba4673"},
+ {file = "PyYAML-6.0.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:e7d73685e87afe9f3b36c799222440d6cf362062f78be1013661b00c5c6f678b"},
+ {file = "PyYAML-6.0.1-cp311-cp311-win32.whl", hash = "sha256:1635fd110e8d85d55237ab316b5b011de701ea0f29d07611174a1b42f1444741"},
+ {file = "PyYAML-6.0.1-cp311-cp311-win_amd64.whl", hash = "sha256:bf07ee2fef7014951eeb99f56f39c9bb4af143d8aa3c21b1677805985307da34"},
+ {file = "PyYAML-6.0.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:855fb52b0dc35af121542a76b9a84f8d1cd886ea97c84703eaa6d88e37a2ad28"},
+ {file = "PyYAML-6.0.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:40df9b996c2b73138957fe23a16a4f0ba614f4c0efce1e9406a184b6d07fa3a9"},
+ {file = "PyYAML-6.0.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a08c6f0fe150303c1c6b71ebcd7213c2858041a7e01975da3a99aed1e7a378ef"},
+ {file = "PyYAML-6.0.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6c22bec3fbe2524cde73d7ada88f6566758a8f7227bfbf93a408a9d86bcc12a0"},
+ {file = "PyYAML-6.0.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:8d4e9c88387b0f5c7d5f281e55304de64cf7f9c0021a3525bd3b1c542da3b0e4"},
+ {file = "PyYAML-6.0.1-cp312-cp312-win32.whl", hash = "sha256:d483d2cdf104e7c9fa60c544d92981f12ad66a457afae824d146093b8c294c54"},
+ {file = "PyYAML-6.0.1-cp312-cp312-win_amd64.whl", hash = "sha256:0d3304d8c0adc42be59c5f8a4d9e3d7379e6955ad754aa9d6ab7a398b59dd1df"},
+ {file = "PyYAML-6.0.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:50550eb667afee136e9a77d6dc71ae76a44df8b3e51e41b77f6de2932bfe0f47"},
+ {file = "PyYAML-6.0.1-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1fe35611261b29bd1de0070f0b2f47cb6ff71fa6595c077e42bd0c419fa27b98"},
+ {file = "PyYAML-6.0.1-cp36-cp36m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:704219a11b772aea0d8ecd7058d0082713c3562b4e271b849ad7dc4a5c90c13c"},
+ {file = "PyYAML-6.0.1-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:afd7e57eddb1a54f0f1a974bc4391af8bcce0b444685d936840f125cf046d5bd"},
+ {file = "PyYAML-6.0.1-cp36-cp36m-win32.whl", hash = "sha256:fca0e3a251908a499833aa292323f32437106001d436eca0e6e7833256674585"},
+ {file = "PyYAML-6.0.1-cp36-cp36m-win_amd64.whl", hash = "sha256:f22ac1c3cac4dbc50079e965eba2c1058622631e526bd9afd45fedd49ba781fa"},
+ {file = "PyYAML-6.0.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:b1275ad35a5d18c62a7220633c913e1b42d44b46ee12554e5fd39c70a243d6a3"},
+ {file = "PyYAML-6.0.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:18aeb1bf9a78867dc38b259769503436b7c72f7a1f1f4c93ff9a17de54319b27"},
+ {file = "PyYAML-6.0.1-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:596106435fa6ad000c2991a98fa58eeb8656ef2325d7e158344fb33864ed87e3"},
+ {file = "PyYAML-6.0.1-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:baa90d3f661d43131ca170712d903e6295d1f7a0f595074f151c0aed377c9b9c"},
+ {file = "PyYAML-6.0.1-cp37-cp37m-win32.whl", hash = "sha256:9046c58c4395dff28dd494285c82ba00b546adfc7ef001486fbf0324bc174fba"},
+ {file = "PyYAML-6.0.1-cp37-cp37m-win_amd64.whl", hash = "sha256:4fb147e7a67ef577a588a0e2c17b6db51dda102c71de36f8549b6816a96e1867"},
+ {file = "PyYAML-6.0.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:1d4c7e777c441b20e32f52bd377e0c409713e8bb1386e1099c2415f26e479595"},
+ {file = "PyYAML-6.0.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a0cd17c15d3bb3fa06978b4e8958dcdc6e0174ccea823003a106c7d4d7899ac5"},
+ {file = "PyYAML-6.0.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:28c119d996beec18c05208a8bd78cbe4007878c6dd15091efb73a30e90539696"},
+ {file = "PyYAML-6.0.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7e07cbde391ba96ab58e532ff4803f79c4129397514e1413a7dc761ccd755735"},
+ {file = "PyYAML-6.0.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:49a183be227561de579b4a36efbb21b3eab9651dd81b1858589f796549873dd6"},
+ {file = "PyYAML-6.0.1-cp38-cp38-win32.whl", hash = "sha256:184c5108a2aca3c5b3d3bf9395d50893a7ab82a38004c8f61c258d4428e80206"},
+ {file = "PyYAML-6.0.1-cp38-cp38-win_amd64.whl", hash = "sha256:1e2722cc9fbb45d9b87631ac70924c11d3a401b2d7f410cc0e3bbf249f2dca62"},
+ {file = "PyYAML-6.0.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:9eb6caa9a297fc2c2fb8862bc5370d0303ddba53ba97e71f08023b6cd73d16a8"},
+ {file = "PyYAML-6.0.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:c8098ddcc2a85b61647b2590f825f3db38891662cfc2fc776415143f599bb859"},
+ {file = "PyYAML-6.0.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5773183b6446b2c99bb77e77595dd486303b4faab2b086e7b17bc6bef28865f6"},
+ {file = "PyYAML-6.0.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b786eecbdf8499b9ca1d697215862083bd6d2a99965554781d0d8d1ad31e13a0"},
+ {file = "PyYAML-6.0.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bc1bf2925a1ecd43da378f4db9e4f799775d6367bdb94671027b73b393a7c42c"},
+ {file = "PyYAML-6.0.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:04ac92ad1925b2cff1db0cfebffb6ffc43457495c9b3c39d3fcae417d7125dc5"},
+ {file = "PyYAML-6.0.1-cp39-cp39-win32.whl", hash = "sha256:faca3bdcf85b2fc05d06ff3fbc1f83e1391b3e724afa3feba7d13eeab355484c"},
+ {file = "PyYAML-6.0.1-cp39-cp39-win_amd64.whl", hash = "sha256:510c9deebc5c0225e8c96813043e62b680ba2f9c50a08d3724c7f28a747d1486"},
+ {file = "PyYAML-6.0.1.tar.gz", hash = "sha256:bfdf460b1736c775f2ba9f6a92bca30bc2095067b8a9d77876d1fad6cc3b4a43"},
+]
+
+[[package]]
+name = "requests"
+version = "2.32.2"
+description = "Python HTTP for Humans."
+optional = false
+python-versions = ">=3.8"
+files = [
+ {file = "requests-2.32.2-py3-none-any.whl", hash = "sha256:fc06670dd0ed212426dfeb94fc1b983d917c4f9847c863f313c9dfaaffb7c23c"},
+ {file = "requests-2.32.2.tar.gz", hash = "sha256:dd951ff5ecf3e3b3aa26b40703ba77495dab41da839ae72ef3c8e5d8e2433289"},
+]
+
+[package.dependencies]
+certifi = ">=2017.4.17"
+charset-normalizer = ">=2,<4"
+idna = ">=2.5,<4"
+urllib3 = ">=1.21.1,<3"
+
+[package.extras]
+socks = ["PySocks (>=1.5.6,!=1.5.7)"]
+use-chardet-on-py3 = ["chardet (>=3.0.2,<6)"]
+
+[[package]]
+name = "ruff"
+version = "0.1.15"
+description = "An extremely fast Python linter and code formatter, written in Rust."
+optional = false
+python-versions = ">=3.7"
+files = [
+ {file = "ruff-0.1.15-py3-none-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:5fe8d54df166ecc24106db7dd6a68d44852d14eb0729ea4672bb4d96c320b7df"},
+ {file = "ruff-0.1.15-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:6f0bfbb53c4b4de117ac4d6ddfd33aa5fc31beeaa21d23c45c6dd249faf9126f"},
+ {file = "ruff-0.1.15-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e0d432aec35bfc0d800d4f70eba26e23a352386be3a6cf157083d18f6f5881c8"},
+ {file = "ruff-0.1.15-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:9405fa9ac0e97f35aaddf185a1be194a589424b8713e3b97b762336ec79ff807"},
+ {file = "ruff-0.1.15-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c66ec24fe36841636e814b8f90f572a8c0cb0e54d8b5c2d0e300d28a0d7bffec"},
+ {file = "ruff-0.1.15-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:6f8ad828f01e8dd32cc58bc28375150171d198491fc901f6f98d2a39ba8e3ff5"},
+ {file = "ruff-0.1.15-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:86811954eec63e9ea162af0ffa9f8d09088bab51b7438e8b6488b9401863c25e"},
+ {file = "ruff-0.1.15-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:fd4025ac5e87d9b80e1f300207eb2fd099ff8200fa2320d7dc066a3f4622dc6b"},
+ {file = "ruff-0.1.15-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b17b93c02cdb6aeb696effecea1095ac93f3884a49a554a9afa76bb125c114c1"},
+ {file = "ruff-0.1.15-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:ddb87643be40f034e97e97f5bc2ef7ce39de20e34608f3f829db727a93fb82c5"},
+ {file = "ruff-0.1.15-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:abf4822129ed3a5ce54383d5f0e964e7fef74a41e48eb1dfad404151efc130a2"},
+ {file = "ruff-0.1.15-py3-none-musllinux_1_2_i686.whl", hash = "sha256:6c629cf64bacfd136c07c78ac10a54578ec9d1bd2a9d395efbee0935868bf852"},
+ {file = "ruff-0.1.15-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:1bab866aafb53da39c2cadfb8e1c4550ac5340bb40300083eb8967ba25481447"},
+ {file = "ruff-0.1.15-py3-none-win32.whl", hash = "sha256:2417e1cb6e2068389b07e6fa74c306b2810fe3ee3476d5b8a96616633f40d14f"},
+ {file = "ruff-0.1.15-py3-none-win_amd64.whl", hash = "sha256:3837ac73d869efc4182d9036b1405ef4c73d9b1f88da2413875e34e0d6919587"},
+ {file = "ruff-0.1.15-py3-none-win_arm64.whl", hash = "sha256:9a933dfb1c14ec7a33cceb1e49ec4a16b51ce3c20fd42663198746efc0427360"},
+ {file = "ruff-0.1.15.tar.gz", hash = "sha256:f6dfa8c1b21c913c326919056c390966648b680966febcb796cc9d1aaab8564e"},
+]
+
+[[package]]
+name = "scipy"
+version = "1.9.3"
+description = "Fundamental algorithms for scientific computing in Python"
+optional = false
+python-versions = ">=3.8"
+files = [
+ {file = "scipy-1.9.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:1884b66a54887e21addf9c16fb588720a8309a57b2e258ae1c7986d4444d3bc0"},
+ {file = "scipy-1.9.3-cp310-cp310-macosx_12_0_arm64.whl", hash = "sha256:83b89e9586c62e787f5012e8475fbb12185bafb996a03257e9675cd73d3736dd"},
+ {file = "scipy-1.9.3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1a72d885fa44247f92743fc20732ae55564ff2a519e8302fb7e18717c5355a8b"},
+ {file = "scipy-1.9.3-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d01e1dd7b15bd2449c8bfc6b7cc67d630700ed655654f0dfcf121600bad205c9"},
+ {file = "scipy-1.9.3-cp310-cp310-win_amd64.whl", hash = "sha256:68239b6aa6f9c593da8be1509a05cb7f9efe98b80f43a5861cd24c7557e98523"},
+ {file = "scipy-1.9.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:b41bc822679ad1c9a5f023bc93f6d0543129ca0f37c1ce294dd9d386f0a21096"},
+ {file = "scipy-1.9.3-cp311-cp311-macosx_12_0_arm64.whl", hash = "sha256:90453d2b93ea82a9f434e4e1cba043e779ff67b92f7a0e85d05d286a3625df3c"},
+ {file = "scipy-1.9.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:83c06e62a390a9167da60bedd4575a14c1f58ca9dfde59830fc42e5197283dab"},
+ {file = "scipy-1.9.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:abaf921531b5aeaafced90157db505e10345e45038c39e5d9b6c7922d68085cb"},
+ {file = "scipy-1.9.3-cp311-cp311-win_amd64.whl", hash = "sha256:06d2e1b4c491dc7d8eacea139a1b0b295f74e1a1a0f704c375028f8320d16e31"},
+ {file = "scipy-1.9.3-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:5a04cd7d0d3eff6ea4719371cbc44df31411862b9646db617c99718ff68d4840"},
+ {file = "scipy-1.9.3-cp38-cp38-macosx_12_0_arm64.whl", hash = "sha256:545c83ffb518094d8c9d83cce216c0c32f8c04aaf28b92cc8283eda0685162d5"},
+ {file = "scipy-1.9.3-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0d54222d7a3ba6022fdf5773931b5d7c56efe41ede7f7128c7b1637700409108"},
+ {file = "scipy-1.9.3-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cff3a5295234037e39500d35316a4c5794739433528310e117b8a9a0c76d20fc"},
+ {file = "scipy-1.9.3-cp38-cp38-win_amd64.whl", hash = "sha256:2318bef588acc7a574f5bfdff9c172d0b1bf2c8143d9582e05f878e580a3781e"},
+ {file = "scipy-1.9.3-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:d644a64e174c16cb4b2e41dfea6af722053e83d066da7343f333a54dae9bc31c"},
+ {file = "scipy-1.9.3-cp39-cp39-macosx_12_0_arm64.whl", hash = "sha256:da8245491d73ed0a994ed9c2e380fd058ce2fa8a18da204681f2fe1f57f98f95"},
+ {file = "scipy-1.9.3-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4db5b30849606a95dcf519763dd3ab6fe9bd91df49eba517359e450a7d80ce2e"},
+ {file = "scipy-1.9.3-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c68db6b290cbd4049012990d7fe71a2abd9ffbe82c0056ebe0f01df8be5436b0"},
+ {file = "scipy-1.9.3-cp39-cp39-win_amd64.whl", hash = "sha256:5b88e6d91ad9d59478fafe92a7c757d00c59e3bdc3331be8ada76a4f8d683f58"},
+ {file = "scipy-1.9.3.tar.gz", hash = "sha256:fbc5c05c85c1a02be77b1ff591087c83bc44579c6d2bd9fb798bb64ea5e1a027"},
+]
+
+[package.dependencies]
+numpy = ">=1.18.5,<1.26.0"
+
+[package.extras]
+dev = ["flake8", "mypy", "pycodestyle", "typing_extensions"]
+doc = ["matplotlib (>2)", "numpydoc", "pydata-sphinx-theme (==0.9.0)", "sphinx (!=4.1.0)", "sphinx-panels (>=0.5.2)", "sphinx-tabs"]
+test = ["asv", "gmpy2", "mpmath", "pytest", "pytest-cov", "pytest-xdist", "scikit-umfpack", "threadpoolctl"]
+
+[[package]]
+name = "setuptools"
+version = "70.0.0"
+description = "Easily download, build, install, upgrade, and uninstall Python packages"
+optional = false
+python-versions = ">=3.8"
+files = [
+ {file = "setuptools-70.0.0-py3-none-any.whl", hash = "sha256:54faa7f2e8d2d11bcd2c07bed282eef1046b5c080d1c32add737d7b5817b1ad4"},
+ {file = "setuptools-70.0.0.tar.gz", hash = "sha256:f211a66637b8fa059bb28183da127d4e86396c991a942b028c6650d4319c3fd0"},
+]
+
+[package.extras]
+docs = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "pygments-github-lexers (==0.0.5)", "pyproject-hooks (!=1.1)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-favicon", "sphinx-inline-tabs", "sphinx-lint", "sphinx-notfound-page (>=1,<2)", "sphinx-reredirects", "sphinxcontrib-towncrier"]
+testing = ["build[virtualenv] (>=1.0.3)", "filelock (>=3.4.0)", "importlib-metadata", "ini2toml[lite] (>=0.14)", "jaraco.develop (>=7.21)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "mypy (==1.9)", "packaging (>=23.2)", "pip (>=19.1)", "pyproject-hooks (!=1.1)", "pytest (>=6,!=8.1.1)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-home (>=0.5)", "pytest-mypy", "pytest-perf", "pytest-ruff (>=0.2.1)", "pytest-subprocess", "pytest-timeout", "pytest-xdist (>=3)", "tomli", "tomli-w (>=1.0.0)", "virtualenv (>=13.0.0)", "wheel"]
+
+[[package]]
+name = "six"
+version = "1.16.0"
+description = "Python 2 and 3 compatibility utilities"
+optional = false
+python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*"
+files = [
+ {file = "six-1.16.0-py2.py3-none-any.whl", hash = "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254"},
+ {file = "six-1.16.0.tar.gz", hash = "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926"},
+]
+
+[[package]]
+name = "syrupy"
+version = "4.6.1"
+description = "Pytest Snapshot Test Utility"
+optional = false
+python-versions = ">=3.8.1,<4"
+files = [
+ {file = "syrupy-4.6.1-py3-none-any.whl", hash = "sha256:203e52f9cb9fa749cf683f29bd68f02c16c3bc7e7e5fe8f2fc59bdfe488ce133"},
+ {file = "syrupy-4.6.1.tar.gz", hash = "sha256:37a835c9ce7857eeef86d62145885e10b3cb9615bc6abeb4ce404b3f18e1bb36"},
+]
+
+[package.dependencies]
+pytest = ">=7.0.0,<9.0.0"
+
+[[package]]
+name = "tenacity"
+version = "8.3.0"
+description = "Retry code until it succeeds"
+optional = false
+python-versions = ">=3.8"
+files = [
+ {file = "tenacity-8.3.0-py3-none-any.whl", hash = "sha256:3649f6443dbc0d9b01b9d8020a9c4ec7a1ff5f6f3c6c8a036ef371f573fe9185"},
+ {file = "tenacity-8.3.0.tar.gz", hash = "sha256:953d4e6ad24357bceffbc9707bc74349aca9d245f68eb65419cf0c249a1949a2"},
+]
+
+[package.extras]
+doc = ["reno", "sphinx"]
+test = ["pytest", "tornado (>=4.5)", "typeguard"]
+
+[[package]]
+name = "tomli"
+version = "2.0.1"
+description = "A lil' TOML parser"
+optional = false
+python-versions = ">=3.7"
+files = [
+ {file = "tomli-2.0.1-py3-none-any.whl", hash = "sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc"},
+ {file = "tomli-2.0.1.tar.gz", hash = "sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f"},
+]
+
+[[package]]
+name = "types-requests"
+version = "2.32.0.20240523"
+description = "Typing stubs for requests"
+optional = false
+python-versions = ">=3.8"
+files = [
+ {file = "types-requests-2.32.0.20240523.tar.gz", hash = "sha256:26b8a6de32d9f561192b9942b41c0ab2d8010df5677ca8aa146289d11d505f57"},
+ {file = "types_requests-2.32.0.20240523-py3-none-any.whl", hash = "sha256:f19ed0e2daa74302069bbbbf9e82902854ffa780bc790742a810a9aaa52f65ec"},
+]
+
+[package.dependencies]
+urllib3 = ">=2"
+
+[[package]]
+name = "typing-extensions"
+version = "4.12.0"
+description = "Backported and Experimental Type Hints for Python 3.8+"
+optional = false
+python-versions = ">=3.8"
+files = [
+ {file = "typing_extensions-4.12.0-py3-none-any.whl", hash = "sha256:b349c66bea9016ac22978d800cfff206d5f9816951f12a7d0ec5578b0a819594"},
+ {file = "typing_extensions-4.12.0.tar.gz", hash = "sha256:8cbcdc8606ebcb0d95453ad7dc5065e6237b6aa230a31e81d0f440c30fed5fd8"},
+]
+
+[[package]]
+name = "tzdata"
+version = "2024.1"
+description = "Provider of IANA time zone data"
+optional = false
+python-versions = ">=2"
+files = [
+ {file = "tzdata-2024.1-py2.py3-none-any.whl", hash = "sha256:9068bc196136463f5245e51efda838afa15aaeca9903f49050dfa2679db4d252"},
+ {file = "tzdata-2024.1.tar.gz", hash = "sha256:2674120f8d891909751c38abcdfd386ac0a5a1127954fbc332af6b5ceae07efd"},
+]
+
+[[package]]
+name = "ujson"
+version = "5.10.0"
+description = "Ultra fast JSON encoder and decoder for Python"
+optional = false
+python-versions = ">=3.8"
+files = [
+ {file = "ujson-5.10.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:2601aa9ecdbee1118a1c2065323bda35e2c5a2cf0797ef4522d485f9d3ef65bd"},
+ {file = "ujson-5.10.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:348898dd702fc1c4f1051bc3aacbf894caa0927fe2c53e68679c073375f732cf"},
+ {file = "ujson-5.10.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:22cffecf73391e8abd65ef5f4e4dd523162a3399d5e84faa6aebbf9583df86d6"},
+ {file = "ujson-5.10.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:26b0e2d2366543c1bb4fbd457446f00b0187a2bddf93148ac2da07a53fe51569"},
+ {file = "ujson-5.10.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:caf270c6dba1be7a41125cd1e4fc7ba384bf564650beef0df2dd21a00b7f5770"},
+ {file = "ujson-5.10.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:a245d59f2ffe750446292b0094244df163c3dc96b3ce152a2c837a44e7cda9d1"},
+ {file = "ujson-5.10.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:94a87f6e151c5f483d7d54ceef83b45d3a9cca7a9cb453dbdbb3f5a6f64033f5"},
+ {file = "ujson-5.10.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:29b443c4c0a113bcbb792c88bea67b675c7ca3ca80c3474784e08bba01c18d51"},
+ {file = "ujson-5.10.0-cp310-cp310-win32.whl", hash = "sha256:c18610b9ccd2874950faf474692deee4223a994251bc0a083c114671b64e6518"},
+ {file = "ujson-5.10.0-cp310-cp310-win_amd64.whl", hash = "sha256:924f7318c31874d6bb44d9ee1900167ca32aa9b69389b98ecbde34c1698a250f"},
+ {file = "ujson-5.10.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:a5b366812c90e69d0f379a53648be10a5db38f9d4ad212b60af00bd4048d0f00"},
+ {file = "ujson-5.10.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:502bf475781e8167f0f9d0e41cd32879d120a524b22358e7f205294224c71126"},
+ {file = "ujson-5.10.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5b91b5d0d9d283e085e821651184a647699430705b15bf274c7896f23fe9c9d8"},
+ {file = "ujson-5.10.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:129e39af3a6d85b9c26d5577169c21d53821d8cf68e079060602e861c6e5da1b"},
+ {file = "ujson-5.10.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f77b74475c462cb8b88680471193064d3e715c7c6074b1c8c412cb526466efe9"},
+ {file = "ujson-5.10.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:7ec0ca8c415e81aa4123501fee7f761abf4b7f386aad348501a26940beb1860f"},
+ {file = "ujson-5.10.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:ab13a2a9e0b2865a6c6db9271f4b46af1c7476bfd51af1f64585e919b7c07fd4"},
+ {file = "ujson-5.10.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:57aaf98b92d72fc70886b5a0e1a1ca52c2320377360341715dd3933a18e827b1"},
+ {file = "ujson-5.10.0-cp311-cp311-win32.whl", hash = "sha256:2987713a490ceb27edff77fb184ed09acdc565db700ee852823c3dc3cffe455f"},
+ {file = "ujson-5.10.0-cp311-cp311-win_amd64.whl", hash = "sha256:f00ea7e00447918ee0eff2422c4add4c5752b1b60e88fcb3c067d4a21049a720"},
+ {file = "ujson-5.10.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:98ba15d8cbc481ce55695beee9f063189dce91a4b08bc1d03e7f0152cd4bbdd5"},
+ {file = "ujson-5.10.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:a9d2edbf1556e4f56e50fab7d8ff993dbad7f54bac68eacdd27a8f55f433578e"},
+ {file = "ujson-5.10.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6627029ae4f52d0e1a2451768c2c37c0c814ffc04f796eb36244cf16b8e57043"},
+ {file = "ujson-5.10.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f8ccb77b3e40b151e20519c6ae6d89bfe3f4c14e8e210d910287f778368bb3d1"},
+ {file = "ujson-5.10.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f3caf9cd64abfeb11a3b661329085c5e167abbe15256b3b68cb5d914ba7396f3"},
+ {file = "ujson-5.10.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:6e32abdce572e3a8c3d02c886c704a38a1b015a1fb858004e03d20ca7cecbb21"},
+ {file = "ujson-5.10.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:a65b6af4d903103ee7b6f4f5b85f1bfd0c90ba4eeac6421aae436c9988aa64a2"},
+ {file = "ujson-5.10.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:604a046d966457b6cdcacc5aa2ec5314f0e8c42bae52842c1e6fa02ea4bda42e"},
+ {file = "ujson-5.10.0-cp312-cp312-win32.whl", hash = "sha256:6dea1c8b4fc921bf78a8ff00bbd2bfe166345f5536c510671bccececb187c80e"},
+ {file = "ujson-5.10.0-cp312-cp312-win_amd64.whl", hash = "sha256:38665e7d8290188b1e0d57d584eb8110951a9591363316dd41cf8686ab1d0abc"},
+ {file = "ujson-5.10.0-cp313-cp313-macosx_10_9_x86_64.whl", hash = "sha256:618efd84dc1acbd6bff8eaa736bb6c074bfa8b8a98f55b61c38d4ca2c1f7f287"},
+ {file = "ujson-5.10.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:38d5d36b4aedfe81dfe251f76c0467399d575d1395a1755de391e58985ab1c2e"},
+ {file = "ujson-5.10.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:67079b1f9fb29ed9a2914acf4ef6c02844b3153913eb735d4bf287ee1db6e557"},
+ {file = "ujson-5.10.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d7d0e0ceeb8fe2468c70ec0c37b439dd554e2aa539a8a56365fd761edb418988"},
+ {file = "ujson-5.10.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:59e02cd37bc7c44d587a0ba45347cc815fb7a5fe48de16bf05caa5f7d0d2e816"},
+ {file = "ujson-5.10.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:2a890b706b64e0065f02577bf6d8ca3b66c11a5e81fb75d757233a38c07a1f20"},
+ {file = "ujson-5.10.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:621e34b4632c740ecb491efc7f1fcb4f74b48ddb55e65221995e74e2d00bbff0"},
+ {file = "ujson-5.10.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:b9500e61fce0cfc86168b248104e954fead61f9be213087153d272e817ec7b4f"},
+ {file = "ujson-5.10.0-cp313-cp313-win32.whl", hash = "sha256:4c4fc16f11ac1612f05b6f5781b384716719547e142cfd67b65d035bd85af165"},
+ {file = "ujson-5.10.0-cp313-cp313-win_amd64.whl", hash = "sha256:4573fd1695932d4f619928fd09d5d03d917274381649ade4328091ceca175539"},
+ {file = "ujson-5.10.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:a984a3131da7f07563057db1c3020b1350a3e27a8ec46ccbfbf21e5928a43050"},
+ {file = "ujson-5.10.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:73814cd1b9db6fc3270e9d8fe3b19f9f89e78ee9d71e8bd6c9a626aeaeaf16bd"},
+ {file = "ujson-5.10.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:61e1591ed9376e5eddda202ec229eddc56c612b61ac6ad07f96b91460bb6c2fb"},
+ {file = "ujson-5.10.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d2c75269f8205b2690db4572a4a36fe47cd1338e4368bc73a7a0e48789e2e35a"},
+ {file = "ujson-5.10.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7223f41e5bf1f919cd8d073e35b229295aa8e0f7b5de07ed1c8fddac63a6bc5d"},
+ {file = "ujson-5.10.0-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:d4dc2fd6b3067c0782e7002ac3b38cf48608ee6366ff176bbd02cf969c9c20fe"},
+ {file = "ujson-5.10.0-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:232cc85f8ee3c454c115455195a205074a56ff42608fd6b942aa4c378ac14dd7"},
+ {file = "ujson-5.10.0-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:cc6139531f13148055d691e442e4bc6601f6dba1e6d521b1585d4788ab0bfad4"},
+ {file = "ujson-5.10.0-cp38-cp38-win32.whl", hash = "sha256:e7ce306a42b6b93ca47ac4a3b96683ca554f6d35dd8adc5acfcd55096c8dfcb8"},
+ {file = "ujson-5.10.0-cp38-cp38-win_amd64.whl", hash = "sha256:e82d4bb2138ab05e18f089a83b6564fee28048771eb63cdecf4b9b549de8a2cc"},
+ {file = "ujson-5.10.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:dfef2814c6b3291c3c5f10065f745a1307d86019dbd7ea50e83504950136ed5b"},
+ {file = "ujson-5.10.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:4734ee0745d5928d0ba3a213647f1c4a74a2a28edc6d27b2d6d5bd9fa4319e27"},
+ {file = "ujson-5.10.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d47ebb01bd865fdea43da56254a3930a413f0c5590372a1241514abae8aa7c76"},
+ {file = "ujson-5.10.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dee5e97c2496874acbf1d3e37b521dd1f307349ed955e62d1d2f05382bc36dd5"},
+ {file = "ujson-5.10.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7490655a2272a2d0b072ef16b0b58ee462f4973a8f6bbe64917ce5e0a256f9c0"},
+ {file = "ujson-5.10.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:ba17799fcddaddf5c1f75a4ba3fd6441f6a4f1e9173f8a786b42450851bd74f1"},
+ {file = "ujson-5.10.0-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:2aff2985cef314f21d0fecc56027505804bc78802c0121343874741650a4d3d1"},
+ {file = "ujson-5.10.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:ad88ac75c432674d05b61184178635d44901eb749786c8eb08c102330e6e8996"},
+ {file = "ujson-5.10.0-cp39-cp39-win32.whl", hash = "sha256:2544912a71da4ff8c4f7ab5606f947d7299971bdd25a45e008e467ca638d13c9"},
+ {file = "ujson-5.10.0-cp39-cp39-win_amd64.whl", hash = "sha256:3ff201d62b1b177a46f113bb43ad300b424b7847f9c5d38b1b4ad8f75d4a282a"},
+ {file = "ujson-5.10.0-pp310-pypy310_pp73-macosx_10_9_x86_64.whl", hash = "sha256:5b6fee72fa77dc172a28f21693f64d93166534c263adb3f96c413ccc85ef6e64"},
+ {file = "ujson-5.10.0-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:61d0af13a9af01d9f26d2331ce49bb5ac1fb9c814964018ac8df605b5422dcb3"},
+ {file = "ujson-5.10.0-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ecb24f0bdd899d368b715c9e6664166cf694d1e57be73f17759573a6986dd95a"},
+ {file = "ujson-5.10.0-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fbd8fd427f57a03cff3ad6574b5e299131585d9727c8c366da4624a9069ed746"},
+ {file = "ujson-5.10.0-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:beeaf1c48e32f07d8820c705ff8e645f8afa690cca1544adba4ebfa067efdc88"},
+ {file = "ujson-5.10.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:baed37ea46d756aca2955e99525cc02d9181de67f25515c468856c38d52b5f3b"},
+ {file = "ujson-5.10.0-pp38-pypy38_pp73-macosx_10_9_x86_64.whl", hash = "sha256:7663960f08cd5a2bb152f5ee3992e1af7690a64c0e26d31ba7b3ff5b2ee66337"},
+ {file = "ujson-5.10.0-pp38-pypy38_pp73-macosx_11_0_arm64.whl", hash = "sha256:d8640fb4072d36b08e95a3a380ba65779d356b2fee8696afeb7794cf0902d0a1"},
+ {file = "ujson-5.10.0-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:78778a3aa7aafb11e7ddca4e29f46bc5139131037ad628cc10936764282d6753"},
+ {file = "ujson-5.10.0-pp38-pypy38_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b0111b27f2d5c820e7f2dbad7d48e3338c824e7ac4d2a12da3dc6061cc39c8e6"},
+ {file = "ujson-5.10.0-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:c66962ca7565605b355a9ed478292da628b8f18c0f2793021ca4425abf8b01e5"},
+ {file = "ujson-5.10.0-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:ba43cc34cce49cf2d4bc76401a754a81202d8aa926d0e2b79f0ee258cb15d3a4"},
+ {file = "ujson-5.10.0-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:ac56eb983edce27e7f51d05bc8dd820586c6e6be1c5216a6809b0c668bb312b8"},
+ {file = "ujson-5.10.0-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f44bd4b23a0e723bf8b10628288c2c7c335161d6840013d4d5de20e48551773b"},
+ {file = "ujson-5.10.0-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7c10f4654e5326ec14a46bcdeb2b685d4ada6911050aa8baaf3501e57024b804"},
+ {file = "ujson-5.10.0-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0de4971a89a762398006e844ae394bd46991f7c385d7a6a3b93ba229e6dac17e"},
+ {file = "ujson-5.10.0-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:e1402f0564a97d2a52310ae10a64d25bcef94f8dd643fcf5d310219d915484f7"},
+ {file = "ujson-5.10.0.tar.gz", hash = "sha256:b3cd8f3c5d8c7738257f1018880444f7b7d9b66232c64649f562d7ba86ad4bc1"},
+]
+
+[[package]]
+name = "urllib3"
+version = "2.2.1"
+description = "HTTP library with thread-safe connection pooling, file post, and more."
+optional = false
+python-versions = ">=3.8"
+files = [
+ {file = "urllib3-2.2.1-py3-none-any.whl", hash = "sha256:450b20ec296a467077128bff42b73080516e71b56ff59a60a02bef2232c4fa9d"},
+ {file = "urllib3-2.2.1.tar.gz", hash = "sha256:d0570876c61ab9e520d776c38acbbb5b05a776d3f9ff98a5c8fd5162a444cf19"},
+]
+
+[package.extras]
+brotli = ["brotli (>=1.0.9)", "brotlicffi (>=0.8.0)"]
+h2 = ["h2 (>=4,<5)"]
+socks = ["pysocks (>=1.5.6,!=1.5.7,<2.0)"]
+zstd = ["zstandard (>=0.18.0)"]
+
+[[package]]
+name = "watchdog"
+version = "4.0.1"
+description = "Filesystem events monitoring"
+optional = false
+python-versions = ">=3.8"
+files = [
+ {file = "watchdog-4.0.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:da2dfdaa8006eb6a71051795856bedd97e5b03e57da96f98e375682c48850645"},
+ {file = "watchdog-4.0.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:e93f451f2dfa433d97765ca2634628b789b49ba8b504fdde5837cdcf25fdb53b"},
+ {file = "watchdog-4.0.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:ef0107bbb6a55f5be727cfc2ef945d5676b97bffb8425650dadbb184be9f9a2b"},
+ {file = "watchdog-4.0.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:17e32f147d8bf9657e0922c0940bcde863b894cd871dbb694beb6704cfbd2fb5"},
+ {file = "watchdog-4.0.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:03e70d2df2258fb6cb0e95bbdbe06c16e608af94a3ffbd2b90c3f1e83eb10767"},
+ {file = "watchdog-4.0.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:123587af84260c991dc5f62a6e7ef3d1c57dfddc99faacee508c71d287248459"},
+ {file = "watchdog-4.0.1-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:093b23e6906a8b97051191a4a0c73a77ecc958121d42346274c6af6520dec175"},
+ {file = "watchdog-4.0.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:611be3904f9843f0529c35a3ff3fd617449463cb4b73b1633950b3d97fa4bfb7"},
+ {file = "watchdog-4.0.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:62c613ad689ddcb11707f030e722fa929f322ef7e4f18f5335d2b73c61a85c28"},
+ {file = "watchdog-4.0.1-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:d4925e4bf7b9bddd1c3de13c9b8a2cdb89a468f640e66fbfabaf735bd85b3e35"},
+ {file = "watchdog-4.0.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:cad0bbd66cd59fc474b4a4376bc5ac3fc698723510cbb64091c2a793b18654db"},
+ {file = "watchdog-4.0.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:a3c2c317a8fb53e5b3d25790553796105501a235343f5d2bf23bb8649c2c8709"},
+ {file = "watchdog-4.0.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:c9904904b6564d4ee8a1ed820db76185a3c96e05560c776c79a6ce5ab71888ba"},
+ {file = "watchdog-4.0.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:667f3c579e813fcbad1b784db7a1aaa96524bed53437e119f6a2f5de4db04235"},
+ {file = "watchdog-4.0.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:d10a681c9a1d5a77e75c48a3b8e1a9f2ae2928eda463e8d33660437705659682"},
+ {file = "watchdog-4.0.1-pp310-pypy310_pp73-macosx_10_9_x86_64.whl", hash = "sha256:0144c0ea9997b92615af1d94afc0c217e07ce2c14912c7b1a5731776329fcfc7"},
+ {file = "watchdog-4.0.1-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:998d2be6976a0ee3a81fb8e2777900c28641fb5bfbd0c84717d89bca0addcdc5"},
+ {file = "watchdog-4.0.1-pp38-pypy38_pp73-macosx_10_9_x86_64.whl", hash = "sha256:e7921319fe4430b11278d924ef66d4daa469fafb1da679a2e48c935fa27af193"},
+ {file = "watchdog-4.0.1-pp38-pypy38_pp73-macosx_11_0_arm64.whl", hash = "sha256:f0de0f284248ab40188f23380b03b59126d1479cd59940f2a34f8852db710625"},
+ {file = "watchdog-4.0.1-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:bca36be5707e81b9e6ce3208d92d95540d4ca244c006b61511753583c81c70dd"},
+ {file = "watchdog-4.0.1-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:ab998f567ebdf6b1da7dc1e5accfaa7c6992244629c0fdaef062f43249bd8dee"},
+ {file = "watchdog-4.0.1-py3-none-manylinux2014_aarch64.whl", hash = "sha256:dddba7ca1c807045323b6af4ff80f5ddc4d654c8bce8317dde1bd96b128ed253"},
+ {file = "watchdog-4.0.1-py3-none-manylinux2014_armv7l.whl", hash = "sha256:4513ec234c68b14d4161440e07f995f231be21a09329051e67a2118a7a612d2d"},
+ {file = "watchdog-4.0.1-py3-none-manylinux2014_i686.whl", hash = "sha256:4107ac5ab936a63952dea2a46a734a23230aa2f6f9db1291bf171dac3ebd53c6"},
+ {file = "watchdog-4.0.1-py3-none-manylinux2014_ppc64.whl", hash = "sha256:6e8c70d2cd745daec2a08734d9f63092b793ad97612470a0ee4cbb8f5f705c57"},
+ {file = "watchdog-4.0.1-py3-none-manylinux2014_ppc64le.whl", hash = "sha256:f27279d060e2ab24c0aa98363ff906d2386aa6c4dc2f1a374655d4e02a6c5e5e"},
+ {file = "watchdog-4.0.1-py3-none-manylinux2014_s390x.whl", hash = "sha256:f8affdf3c0f0466e69f5b3917cdd042f89c8c63aebdb9f7c078996f607cdb0f5"},
+ {file = "watchdog-4.0.1-py3-none-manylinux2014_x86_64.whl", hash = "sha256:ac7041b385f04c047fcc2951dc001671dee1b7e0615cde772e84b01fbf68ee84"},
+ {file = "watchdog-4.0.1-py3-none-win32.whl", hash = "sha256:206afc3d964f9a233e6ad34618ec60b9837d0582b500b63687e34011e15bb429"},
+ {file = "watchdog-4.0.1-py3-none-win_amd64.whl", hash = "sha256:7577b3c43e5909623149f76b099ac49a1a01ca4e167d1785c76eb52fa585745a"},
+ {file = "watchdog-4.0.1-py3-none-win_ia64.whl", hash = "sha256:d7b9f5f3299e8dd230880b6c55504a1f69cf1e4316275d1b215ebdd8187ec88d"},
+ {file = "watchdog-4.0.1.tar.gz", hash = "sha256:eebaacf674fa25511e8867028d281e602ee6500045b57f43b08778082f7f8b44"},
+]
+
+[package.extras]
+watchmedo = ["PyYAML (>=3.10)"]
+
+[metadata]
+lock-version = "2.0"
+python-versions = ">=3.8.1,<4.0"
+content-hash = "bdd4f827b6ae022134ab2be9ee987e3247d6be99c1d1cb2e403448b8b0677a4a"
diff --git a/libs/partners/milvus/pyproject.toml b/libs/partners/milvus/pyproject.toml
new file mode 100644
index 0000000000000..6b55eff00cfb5
--- /dev/null
+++ b/libs/partners/milvus/pyproject.toml
@@ -0,0 +1,99 @@
+[tool.poetry]
+name = "gigachain-milvus"
+version = "0.1.1"
+description = "An integration package connecting Milvus and Gigachain"
+authors = []
+readme = "README.md"
+repository = "https://github.com/langchain-ai/langchain"
+license = "MIT"
+
+[tool.poetry.urls]
+"Source Code" = "https://github.com/langchain-ai/langchain/tree/master/libs/partners/milvus"
+
+[tool.poetry.dependencies]
+python = ">=3.8.1,<4.0"
+gigachain-core = "^0.2.0"
+pymilvus = "^2.4.3"
+scipy = "^1.7"
+
+[tool.poetry.group.test]
+optional = true
+
+[tool.poetry.group.test.dependencies]
+pytest = "^7.3.0"
+freezegun = "^1.2.2"
+pytest-mock = "^3.10.0"
+syrupy = "^4.0.2"
+pytest-watcher = "^0.3.4"
+pytest-asyncio = "^0.21.1"
+gigachain-core = { path = "../../core", develop = true }
+
+[tool.poetry.group.codespell]
+optional = true
+
+[tool.poetry.group.codespell.dependencies]
+codespell = "^2.2.0"
+
+[tool.poetry.group.test_integration]
+optional = true
+
+[tool.poetry.group.test_integration.dependencies]
+
+[tool.poetry.group.lint]
+optional = true
+
+[tool.poetry.group.lint.dependencies]
+ruff = "^0.1.5"
+
+[tool.poetry.group.typing.dependencies]
+mypy = "^0.991"
+gigachain-core = { path = "../../core", develop = true }
+types-requests = "^2"
+
+[tool.poetry.group.dev]
+optional = true
+
+[tool.poetry.group.dev.dependencies]
+gigachain-core = { path = "../../core", develop = true }
+
+[tool.ruff]
+select = [
+ "E", # pycodestyle
+ "F", # pyflakes
+ "I", # isort
+ "T201", # print
+]
+
+[tool.mypy]
+disallow_untyped_defs = "True"
+
+[[tool.mypy.overrides]]
+module = ["pymilvus"]
+ignore_missing_imports = "True"
+
+[tool.coverage.run]
+omit = ["tests/*"]
+
+[build-system]
+requires = ["poetry-core>=1.0.0"]
+build-backend = "poetry.core.masonry.api"
+
+[tool.pytest.ini_options]
+# --strict-markers will raise errors on unknown marks.
+# https://docs.pytest.org/en/7.1.x/how-to/mark.html#raising-errors-on-unknown-marks
+#
+# https://docs.pytest.org/en/7.1.x/reference/reference.html
+# --strict-config any warnings encountered while parsing the `pytest`
+# section of the configuration file raise errors.
+#
+# https://github.com/tophat/syrupy
+# --snapshot-warn-unused Prints a warning on unused snapshots rather than fail the test suite.
+addopts = "--snapshot-warn-unused --strict-markers --strict-config --durations=5"
+# Registering custom markers.
+# https://docs.pytest.org/en/7.1.x/example/markers.html#registering-markers
+markers = [
+ "requires: mark tests as requiring a specific library",
+ "asyncio: mark tests as requiring asyncio",
+ "compile: mark placeholder test used to compile integration tests without running them",
+]
+asyncio_mode = "auto"
diff --git a/libs/partners/milvus/scripts/check_imports.py b/libs/partners/milvus/scripts/check_imports.py
new file mode 100644
index 0000000000000..365f5fa118da4
--- /dev/null
+++ b/libs/partners/milvus/scripts/check_imports.py
@@ -0,0 +1,17 @@
+import sys
+import traceback
+from importlib.machinery import SourceFileLoader
+
+if __name__ == "__main__":
+ files = sys.argv[1:]
+ has_failure = False
+ for file in files:
+ try:
+ SourceFileLoader("x", file).load_module()
+ except Exception:
+ has_faillure = True
+ print(file) # noqa: T201
+ traceback.print_exc()
+ print() # noqa: T201
+
+ sys.exit(1 if has_failure else 0)
diff --git a/libs/partners/milvus/scripts/check_pydantic.sh b/libs/partners/milvus/scripts/check_pydantic.sh
new file mode 100755
index 0000000000000..06b5bb81ae236
--- /dev/null
+++ b/libs/partners/milvus/scripts/check_pydantic.sh
@@ -0,0 +1,27 @@
+#!/bin/bash
+#
+# This script searches for lines starting with "import pydantic" or "from pydantic"
+# in tracked files within a Git repository.
+#
+# Usage: ./scripts/check_pydantic.sh /path/to/repository
+
+# Check if a path argument is provided
+if [ $# -ne 1 ]; then
+ echo "Usage: $0 /path/to/repository"
+ exit 1
+fi
+
+repository_path="$1"
+
+# Search for lines matching the pattern within the specified repository
+result=$(git -C "$repository_path" grep -E '^import pydantic|^from pydantic')
+
+# Check if any matching lines were found
+if [ -n "$result" ]; then
+ echo "ERROR: The following lines need to be updated:"
+ echo "$result"
+ echo "Please replace the code with an import from langchain_core.pydantic_v1."
+ echo "For example, replace 'from pydantic import BaseModel'"
+ echo "with 'from langchain_core.pydantic_v1 import BaseModel'"
+ exit 1
+fi
diff --git a/libs/partners/milvus/scripts/lint_imports.sh b/libs/partners/milvus/scripts/lint_imports.sh
new file mode 100755
index 0000000000000..695613c7ba8fd
--- /dev/null
+++ b/libs/partners/milvus/scripts/lint_imports.sh
@@ -0,0 +1,17 @@
+#!/bin/bash
+
+set -eu
+
+# Initialize a variable to keep track of errors
+errors=0
+
+# make sure not importing from langchain or langchain_experimental
+git --no-pager grep '^from langchain\.' . && errors=$((errors+1))
+git --no-pager grep '^from langchain_experimental\.' . && errors=$((errors+1))
+
+# Decide on an exit status based on the errors
+if [ "$errors" -gt 0 ]; then
+ exit 1
+else
+ exit 0
+fi
diff --git a/libs/partners/milvus/tests/__init__.py b/libs/partners/milvus/tests/__init__.py
new file mode 100644
index 0000000000000..e69de29bb2d1d
diff --git a/libs/partners/milvus/tests/integration_tests/__init__.py b/libs/partners/milvus/tests/integration_tests/__init__.py
new file mode 100644
index 0000000000000..e69de29bb2d1d
diff --git a/libs/partners/milvus/tests/integration_tests/retrievers/__init__.py b/libs/partners/milvus/tests/integration_tests/retrievers/__init__.py
new file mode 100644
index 0000000000000..e69de29bb2d1d
diff --git a/libs/partners/milvus/tests/integration_tests/test_compile.py b/libs/partners/milvus/tests/integration_tests/test_compile.py
new file mode 100644
index 0000000000000..33ecccdfa0fbd
--- /dev/null
+++ b/libs/partners/milvus/tests/integration_tests/test_compile.py
@@ -0,0 +1,7 @@
+import pytest
+
+
+@pytest.mark.compile
+def test_placeholder() -> None:
+ """Used for compiling integration tests without running any real tests."""
+ pass
diff --git a/libs/partners/milvus/tests/integration_tests/utils.py b/libs/partners/milvus/tests/integration_tests/utils.py
new file mode 100644
index 0000000000000..f3ef87d2f2acc
--- /dev/null
+++ b/libs/partners/milvus/tests/integration_tests/utils.py
@@ -0,0 +1,40 @@
+from typing import List
+
+from langchain_core.documents import Document
+from langchain_core.embeddings import Embeddings
+
+fake_texts = ["foo", "bar", "baz"]
+
+
+class FakeEmbeddings(Embeddings):
+ """Fake embeddings functionality for testing."""
+
+ def embed_documents(self, texts: List[str]) -> List[List[float]]:
+ """Return simple embeddings.
+ Embeddings encode each text as its index."""
+ return [[float(1.0)] * 9 + [float(i)] for i in range(len(texts))]
+
+ async def aembed_documents(self, texts: List[str]) -> List[List[float]]:
+ return self.embed_documents(texts)
+
+ def embed_query(self, text: str) -> List[float]:
+ """Return constant query embeddings.
+ Embeddings are identical to embed_documents(texts)[0].
+ Distance to each text will be that text's index,
+ as it was passed to embed_documents."""
+ return [float(1.0)] * 9 + [float(0.0)]
+
+ async def aembed_query(self, text: str) -> List[float]:
+ return self.embed_query(text)
+
+
+def assert_docs_equal_without_pk(
+ docs1: List[Document], docs2: List[Document], pk_field: str = "pk"
+) -> None:
+ """Assert two lists of Documents are equal, ignoring the primary key field."""
+ assert len(docs1) == len(docs2)
+ for doc1, doc2 in zip(docs1, docs2):
+ assert doc1.page_content == doc2.page_content
+ doc1.metadata.pop(pk_field, None)
+ doc2.metadata.pop(pk_field, None)
+ assert doc1.metadata == doc2.metadata
diff --git a/libs/partners/milvus/tests/integration_tests/vectorstores/__init__.py b/libs/partners/milvus/tests/integration_tests/vectorstores/__init__.py
new file mode 100644
index 0000000000000..e69de29bb2d1d
diff --git a/libs/partners/milvus/tests/integration_tests/vectorstores/test_milvus.py b/libs/partners/milvus/tests/integration_tests/vectorstores/test_milvus.py
new file mode 100644
index 0000000000000..5eaf2abcc904f
--- /dev/null
+++ b/libs/partners/milvus/tests/integration_tests/vectorstores/test_milvus.py
@@ -0,0 +1,184 @@
+"""Test Milvus functionality."""
+from typing import Any, List, Optional
+
+from langchain_core.documents import Document
+
+from langchain_milvus.vectorstores import Milvus
+from tests.integration_tests.utils import (
+ FakeEmbeddings,
+ assert_docs_equal_without_pk,
+ fake_texts,
+)
+
+#
+# To run this test properly, please start a Milvus server with the following command:
+#
+# ```shell
+# wget https://raw.githubusercontent.com/milvus-io/milvus/master/scripts/standalone_embed.sh
+# bash standalone_embed.sh start
+# ```
+#
+# Here is the reference:
+# https://milvus.io/docs/install_standalone-docker.md
+#
+
+
+def _milvus_from_texts(
+ metadatas: Optional[List[dict]] = None,
+ ids: Optional[List[str]] = None,
+ drop: bool = True,
+) -> Milvus:
+ return Milvus.from_texts(
+ fake_texts,
+ FakeEmbeddings(),
+ metadatas=metadatas,
+ ids=ids,
+ # connection_args={"uri": "http://127.0.0.1:19530"},
+ connection_args={"uri": "./milvus_demo.db"},
+ drop_old=drop,
+ )
+
+
+def _get_pks(expr: str, docsearch: Milvus) -> List[Any]:
+ return docsearch.get_pks(expr) # type: ignore[return-value]
+
+
+def test_milvus() -> None:
+ """Test end to end construction and search."""
+ docsearch = _milvus_from_texts()
+ output = docsearch.similarity_search("foo", k=1)
+ assert_docs_equal_without_pk(output, [Document(page_content="foo")])
+
+
+def test_milvus_with_metadata() -> None:
+ """Test with metadata"""
+ docsearch = _milvus_from_texts(metadatas=[{"label": "test"}] * len(fake_texts))
+ output = docsearch.similarity_search("foo", k=1)
+ assert_docs_equal_without_pk(
+ output, [Document(page_content="foo", metadata={"label": "test"})]
+ )
+
+
+def test_milvus_with_id() -> None:
+ """Test with ids"""
+ ids = ["id_" + str(i) for i in range(len(fake_texts))]
+ docsearch = _milvus_from_texts(ids=ids)
+ output = docsearch.similarity_search("foo", k=1)
+ assert_docs_equal_without_pk(output, [Document(page_content="foo")])
+
+ output = docsearch.delete(ids=ids)
+ assert output.delete_count == len(fake_texts) # type: ignore[attr-defined]
+
+ try:
+ ids = ["dup_id" for _ in fake_texts]
+ _milvus_from_texts(ids=ids)
+ except Exception as e:
+ assert isinstance(e, AssertionError)
+
+
+def test_milvus_with_score() -> None:
+ """Test end to end construction and search with scores and IDs."""
+ texts = ["foo", "bar", "baz"]
+ metadatas = [{"page": i} for i in range(len(texts))]
+ docsearch = _milvus_from_texts(metadatas=metadatas)
+ output = docsearch.similarity_search_with_score("foo", k=3)
+ docs = [o[0] for o in output]
+ scores = [o[1] for o in output]
+ assert_docs_equal_without_pk(
+ docs,
+ [
+ Document(page_content="foo", metadata={"page": 0}),
+ Document(page_content="bar", metadata={"page": 1}),
+ Document(page_content="baz", metadata={"page": 2}),
+ ],
+ )
+ assert scores[0] < scores[1] < scores[2]
+
+
+def test_milvus_max_marginal_relevance_search() -> None:
+ """Test end to end construction and MRR search."""
+ texts = ["foo", "bar", "baz"]
+ metadatas = [{"page": i} for i in range(len(texts))]
+ docsearch = _milvus_from_texts(metadatas=metadatas)
+ output = docsearch.max_marginal_relevance_search("foo", k=2, fetch_k=3)
+ assert_docs_equal_without_pk(
+ output,
+ [
+ Document(page_content="foo", metadata={"page": 0}),
+ Document(page_content="baz", metadata={"page": 2}),
+ ],
+ )
+
+
+def test_milvus_add_extra() -> None:
+ """Test end to end construction and MRR search."""
+ texts = ["foo", "bar", "baz"]
+ metadatas = [{"page": i} for i in range(len(texts))]
+ docsearch = _milvus_from_texts(metadatas=metadatas)
+
+ docsearch.add_texts(texts, metadatas)
+
+ output = docsearch.similarity_search("foo", k=10)
+ assert len(output) == 6
+
+
+def test_milvus_no_drop() -> None:
+ """Test end to end construction and MRR search."""
+ texts = ["foo", "bar", "baz"]
+ metadatas = [{"page": i} for i in range(len(texts))]
+ docsearch = _milvus_from_texts(metadatas=metadatas)
+ del docsearch
+
+ docsearch = _milvus_from_texts(metadatas=metadatas, drop=False)
+
+ output = docsearch.similarity_search("foo", k=10)
+ assert len(output) == 6
+
+
+def test_milvus_get_pks() -> None:
+ """Test end to end construction and get pks with expr"""
+ texts = ["foo", "bar", "baz"]
+ metadatas = [{"id": i} for i in range(len(texts))]
+ docsearch = _milvus_from_texts(metadatas=metadatas)
+ expr = "id in [1,2]"
+ output = _get_pks(expr, docsearch)
+ assert len(output) == 2
+
+
+def test_milvus_delete_entities() -> None:
+ """Test end to end construction and delete entities"""
+ texts = ["foo", "bar", "baz"]
+ metadatas = [{"id": i} for i in range(len(texts))]
+ docsearch = _milvus_from_texts(metadatas=metadatas)
+ expr = "id in [1,2]"
+ pks = _get_pks(expr, docsearch)
+ result = docsearch.delete(pks)
+ assert result.delete_count == 2 # type: ignore[attr-defined]
+
+
+def test_milvus_upsert_entities() -> None:
+ """Test end to end construction and upsert entities"""
+ texts = ["foo", "bar", "baz"]
+ metadatas = [{"id": i} for i in range(len(texts))]
+ docsearch = _milvus_from_texts(metadatas=metadatas)
+ expr = "id in [1,2]"
+ pks = _get_pks(expr, docsearch)
+ documents = [
+ Document(page_content="test_1", metadata={"id": 1}),
+ Document(page_content="test_2", metadata={"id": 3}),
+ ]
+ ids = docsearch.upsert(pks, documents)
+ assert len(ids) == 2 # type: ignore[arg-type]
+
+
+# if __name__ == "__main__":
+# test_milvus()
+# test_milvus_with_metadata()
+# test_milvus_with_id()
+# test_milvus_with_score()
+# test_milvus_max_marginal_relevance_search()
+# test_milvus_add_extra()
+# test_milvus_no_drop()
+# test_milvus_get_pks()
+# test_milvus_delete_entities()
+# test_milvus_upsert_entities()
diff --git a/libs/partners/milvus/tests/unit_tests/__init__.py b/libs/partners/milvus/tests/unit_tests/__init__.py
new file mode 100644
index 0000000000000..e69de29bb2d1d
diff --git a/libs/partners/milvus/tests/unit_tests/retrievers/__init__.py b/libs/partners/milvus/tests/unit_tests/retrievers/__init__.py
new file mode 100644
index 0000000000000..e69de29bb2d1d
diff --git a/libs/partners/milvus/tests/unit_tests/test_imports.py b/libs/partners/milvus/tests/unit_tests/test_imports.py
new file mode 100644
index 0000000000000..8be170e36f272
--- /dev/null
+++ b/libs/partners/milvus/tests/unit_tests/test_imports.py
@@ -0,0 +1,12 @@
+from langchain_milvus import __all__
+
+EXPECTED_ALL = [
+ "Milvus",
+ "MilvusCollectionHybridSearchRetriever",
+ "Zilliz",
+ "ZillizCloudPipelineRetriever",
+]
+
+
+def test_all_imports() -> None:
+ assert sorted(EXPECTED_ALL) == sorted(__all__)
diff --git a/libs/partners/milvus/tests/unit_tests/vectorstores/__init__.py b/libs/partners/milvus/tests/unit_tests/vectorstores/__init__.py
new file mode 100644
index 0000000000000..e69de29bb2d1d
diff --git a/libs/partners/milvus/tests/unit_tests/vectorstores/test_milvus.py b/libs/partners/milvus/tests/unit_tests/vectorstores/test_milvus.py
new file mode 100644
index 0000000000000..1ef2314e79590
--- /dev/null
+++ b/libs/partners/milvus/tests/unit_tests/vectorstores/test_milvus.py
@@ -0,0 +1,17 @@
+import os
+from tempfile import TemporaryDirectory
+from unittest.mock import Mock
+
+from langchain_milvus.vectorstores import Milvus
+
+
+def test_initialization() -> None:
+ """Test integration milvus initialization."""
+ embedding = Mock()
+ with TemporaryDirectory() as tmp_dir:
+ Milvus(
+ embedding_function=embedding,
+ connection_args={
+ "uri": os.path.join(tmp_dir, "milvus.db"),
+ },
+ )
diff --git a/libs/partners/mistralai/pyproject.toml b/libs/partners/mistralai/pyproject.toml
index ff8667bb25722..1eedeb38c4f3e 100644
--- a/libs/partners/mistralai/pyproject.toml
+++ b/libs/partners/mistralai/pyproject.toml
@@ -1,7 +1,7 @@
[tool.poetry]
name = "gigachain-mistralai"
-version = "0.1.7"
-description = "An integration package connecting Mistral and LangChain"
+version = "0.1.8"
+description = "An integration package connecting Mistral and Gigachain"
authors = []
readme = "README.md"
repository = "https://github.com/langchain-ai/langchain"
@@ -12,7 +12,7 @@ license = "MIT"
[tool.poetry.dependencies]
python = ">=3.8.1,<4.0"
-gigachain-core = ">=0.1.46,<0.3"
+gigachain-core = ">=0.2.2,<0.3"
tokenizers = ">=0.15.1,<1"
httpx = ">=0.25.2,<1"
httpx-sse = ">=0.3.1,<1"
diff --git a/libs/partners/mongodb/langchain_mongodb/index.py b/libs/partners/mongodb/langchain_mongodb/index.py
new file mode 100644
index 0000000000000..a2c0e10ed667a
--- /dev/null
+++ b/libs/partners/mongodb/langchain_mongodb/index.py
@@ -0,0 +1,105 @@
+import logging
+from typing import Any, Dict, List, Optional
+
+from pymongo.collection import Collection
+from pymongo.operations import SearchIndexModel
+
+logger = logging.getLogger(__file__)
+
+
+def _vector_search_index_definition(
+ dimensions: int,
+ path: str,
+ similarity: str,
+ filters: Optional[List[Dict[str, str]]],
+) -> Dict[str, Any]:
+ return {
+ "fields": [
+ {
+ "numDimensions": dimensions,
+ "path": path,
+ "similarity": similarity,
+ "type": "vector",
+ },
+ *(filters or []),
+ ]
+ }
+
+
+def create_vector_search_index(
+ collection: Collection,
+ index_name: str,
+ dimensions: int,
+ path: str,
+ similarity: str,
+ filters: List[Dict[str, str]],
+) -> None:
+ """Experimental Utility function to create a vector search index
+
+ Args:
+ collection (Collection): MongoDB Collection
+ index_name (str): Name of Index
+ dimensions (int): Number of dimensions in embedding
+ path (str): field with vector embedding
+ similarity (str): The similarity score used for the index
+ filters (List[Dict[str, str]]): additional filters for index definition.
+ """
+ logger.info("Creating Search Index %s on %s", index_name, collection.name)
+ result = collection.create_search_index(
+ SearchIndexModel(
+ definition=_vector_search_index_definition(
+ dimensions=dimensions, path=path, similarity=similarity, filters=filters
+ ),
+ name=index_name,
+ type="vectorSearch",
+ )
+ )
+ logger.info(result)
+
+
+def drop_vector_search_index(collection: Collection, index_name: str) -> None:
+ """Drop a created vector search index
+
+ Args:
+ collection (Collection): MongoDB Collection with index to be dropped
+ index_name (str): Name of the MongoDB index
+ """
+ logger.info(
+ "Dropping Search Index %s from Collection: %s", index_name, collection.name
+ )
+ collection.drop_search_index(index_name)
+ logger.info("Vector Search index %s.%s dropped", collection.name, index_name)
+
+
+def update_vector_search_index(
+ collection: Collection,
+ index_name: str,
+ dimensions: int,
+ path: str,
+ similarity: str,
+ filters: List[Dict[str, str]],
+) -> None:
+ """Leverages the updateSearchIndex call
+
+ Args:
+ collection (Collection): MongoDB Collection
+ index_name (str): Name of Index
+ dimensions (int): Number of dimensions in embedding.
+ path (str): field with vector embedding.
+ similarity (str): The similarity score used for the index.
+ filters (List[Dict[str, str]]): additional filters for index definition.
+ """
+
+ logger.info(
+ "Updating Search Index %s from Collection: %s", index_name, collection.name
+ )
+ collection.update_search_index(
+ name=index_name,
+ definition=_vector_search_index_definition(
+ dimensions=dimensions,
+ path=path,
+ similarity=similarity,
+ filters=filters,
+ ),
+ )
+ logger.info("Update succeeded")
diff --git a/libs/partners/mongodb/pyproject.toml b/libs/partners/mongodb/pyproject.toml
index 5173db7937967..a84ff09bb1010 100644
--- a/libs/partners/mongodb/pyproject.toml
+++ b/libs/partners/mongodb/pyproject.toml
@@ -1,7 +1,7 @@
[tool.poetry]
name = "gigachain-mongodb"
-version = "0.1.5"
-description = "An integration package connecting MongoDB and LangChain"
+version = "0.1.6"
+description = "An integration package connecting MongoDB and Gigachain"
authors = []
readme = "README.md"
repository = "https://github.com/langchain-ai/langchain"
@@ -14,7 +14,11 @@ license = "MIT"
python = ">=3.8.1,<4.0"
pymongo = ">=4.6.1,<5.0"
gigachain-core = ">=0.1.46,<0.3"
-numpy = "^1"
+# Support Python 3.8 and 3.12+.
+numpy = [
+ {version = "^1", python = "<3.12"},
+ {version = "^1.26.0", python = ">=3.12"}
+]
[tool.poetry.group.test]
optional = true
diff --git a/libs/partners/nomic/pyproject.toml b/libs/partners/nomic/pyproject.toml
index 6f29ab4f48334..837035a5b567b 100644
--- a/libs/partners/nomic/pyproject.toml
+++ b/libs/partners/nomic/pyproject.toml
@@ -1,7 +1,7 @@
[tool.poetry]
-name = "langchain-nomic"
-version = "0.1.0"
-description = "An integration package connecting Nomic and LangChain"
+name = "gigachain-nomic"
+version = "0.1.2"
+description = "An integration package connecting Nomic and Gigachain"
authors = []
readme = "README.md"
repository = "https://github.com/langchain-ai/langchain"
@@ -13,7 +13,8 @@ license = "MIT"
[tool.poetry.dependencies]
python = ">=3.8.1,<4.0"
gigachain-core = ">=0.1.46,<0.3"
-nomic = "^3.0.12"
+nomic = "^3.0.29"
+pillow = "^10.3.0"
[tool.poetry.group.test]
optional = true
diff --git a/libs/partners/openai/pyproject.toml b/libs/partners/openai/pyproject.toml
index 458a45aecf161..0ce249533f8c4 100644
--- a/libs/partners/openai/pyproject.toml
+++ b/libs/partners/openai/pyproject.toml
@@ -1,22 +1,19 @@
[tool.poetry]
name = "gigachain-openai"
-version = "0.1.7"
-description = "An integration package connecting OpenAI and LangChain"
+version = "0.1.10"
+description = "An integration package connecting OpenAI and Gigachain"
authors = []
readme = "README.md"
repository = "https://github.com/langchain-ai/langchain"
license = "MIT"
-packages = [
- {include = "langchain_openai"}
-]
[tool.poetry.urls]
"Source Code" = "https://github.com/langchain-ai/langchain/tree/master/libs/partners/openai"
[tool.poetry.dependencies]
python = ">=3.8.1,<4.0"
-gigachain-core = ">=0.1.46,<0.3"
-openai = "^1.24.0"
+gigachain-core = ">=0.2.2,<0.3"
+openai = "^1.26.0"
tiktoken = ">=0.7,<1"
[tool.poetry.group.test]
@@ -32,7 +29,11 @@ pytest-asyncio = "^0.21.1"
gigachain-core = { path = "../../core", develop = true }
pytest-cov = "^4.1.0"
gigachain-standard-tests = { path = "../../standard-tests", develop = true }
-numpy = "^1.24"
+# Support Python 3.8 and 3.12+.
+numpy = [
+ {version = "^1", python = "<3.12"},
+ {version = "^1.26.0", python = ">=3.12"}
+]
[tool.poetry.group.codespell]
optional = true
@@ -61,7 +62,13 @@ gigachain-core = { path = "../../core", develop = true }
optional = true
[tool.poetry.group.test_integration.dependencies]
-numpy = "^1"
+# Support Python 3.8 and 3.12+.
+numpy = [
+ {version = "^1", python = "<3.12"},
+ {version = "^1.26.0", python = ">=3.12"}
+]
+httpx = "^0.27.0"
+pillow = "^10.3.0"
[tool.ruff.lint]
select = [
@@ -71,6 +78,10 @@ select = [
"T201", # print
]
+[tool.ruff.format]
+docstring-code-format = true
+skip-magic-trailing-comma = true
+
[tool.mypy]
disallow_untyped_defs = "True"
diff --git a/libs/partners/pinecone/pyproject.toml b/libs/partners/pinecone/pyproject.toml
index 3b90035491456..35a5fb2d75a3f 100644
--- a/libs/partners/pinecone/pyproject.toml
+++ b/libs/partners/pinecone/pyproject.toml
@@ -1,22 +1,25 @@
-
[tool.poetry]
-name = "langchain-pinecone"
+name = "gigachain-pinecone"
version = "0.1.1"
-description = "An integration package connecting Pinecone and LangChain"
+description = "An integration package connecting Pinecone and Gigachain"
authors = []
readme = "README.md"
-repository = "https://github.com/gigachain-ai/gigachain"
+repository = "https://github.com/langchain-ai/langchain"
license = "MIT"
[tool.poetry.urls]
-"Source Code" = "https://github.com/gigachain-ai/gigachain/tree/master/libs/partners/pinecone"
+"Source Code" = "https://github.com/langchain-ai/langchain/tree/master/libs/partners/pinecone"
[tool.poetry.dependencies]
# <3.13 is due to restriction in pinecone-client package
python = ">=3.8.1,<3.13"
-langchain-core = ">=0.1.52,<0.3"
-pinecone-client = "^3.2.2"
-numpy = "^1"
+gigachain-core = ">=0.1.52,<0.3"
+pinecone-client = ">=3.2.2,<5"
+# Support Python 3.8 and 3.12+.
+numpy = [
+ {version = "^1", python = "<3.12"},
+ {version = "^1.26.0", python = ">=3.12"}
+]
[tool.poetry.group.test]
optional = true
diff --git a/libs/partners/prompty/pyproject.toml b/libs/partners/prompty/pyproject.toml
index a8fe552cd2550..f7b1755770942 100644
--- a/libs/partners/prompty/pyproject.toml
+++ b/libs/partners/prompty/pyproject.toml
@@ -1,7 +1,7 @@
[tool.poetry]
-name = "langchain-prompty"
+name = "gigachain-prompty"
version = "0.0.2"
-description = "An integration package connecting Prompty and LangChain"
+description = "An integration package connecting Prompty and Gigachain"
authors = []
readme = "README.md"
repository = "https://github.com/langchain-ai/langchain"
@@ -12,7 +12,7 @@ license = "MIT"
[tool.poetry.dependencies]
python = ">=3.8.1,<4.0"
-langchain-core = ">=0.1.52,<0.3"
+gigachain-core = ">=0.1.52,<0.3"
pyyaml = "^6.0.1"
types-pyyaml = "^6.0.12.20240311"
@@ -26,9 +26,9 @@ pytest-mock = "^3.10.0"
syrupy = "^4.0.2"
pytest-watcher = "^0.3.4"
pytest-asyncio = "^0.21.1"
-langchain-core = { path = "../../core", develop = true }
-langchain = { path = "../../langchain", develop = true }
-langchain-text-splitters = { path = "../../text-splitters", develop = true }
+gigachain-core = { path = "../../core", develop = true }
+gigachain = { path = "../../langchain", develop = true }
+gigachain-text-splitters = { path = "../../text-splitters", develop = true }
[tool.poetry.group.codespell]
optional = true
@@ -49,13 +49,13 @@ ruff = "^0.1.5"
[tool.poetry.group.typing.dependencies]
mypy = "^0.991"
-langchain-core = { path = "../../core", develop = true }
+gigachain-core = { path = "../../core", develop = true }
[tool.poetry.group.dev]
optional = true
[tool.poetry.group.dev.dependencies]
-langchain-core = { path = "../../core", develop = true }
+gigachain-core = { path = "../../core", develop = true }
types-pyyaml = "^6.0.12.20240311"
[tool.ruff]
diff --git a/libs/partners/qdrant/pyproject.toml b/libs/partners/qdrant/pyproject.toml
index 0871b867ee13f..29ab4b720c86f 100644
--- a/libs/partners/qdrant/pyproject.toml
+++ b/libs/partners/qdrant/pyproject.toml
@@ -1,7 +1,7 @@
[tool.poetry]
-name = "langchain-qdrant"
-version = "0.1.0"
-description = "An integration package connecting Qdrant and LangChain"
+name = "gigachain-qdrant"
+version = "0.1.1"
+description = "An integration package connecting Qdrant and Gigachain"
authors = []
readme = "README.md"
repository = "https://github.com/langchain-ai/langchain"
@@ -12,7 +12,7 @@ license = "MIT"
[tool.poetry.dependencies]
python = ">=3.8.1,<4.0"
-langchain-core = ">=0.1.52,<0.3"
+gigachain-core = ">=0.1.52,<0.3"
qdrant-client = "^1.9.0"
[tool.poetry.group.test]
@@ -25,7 +25,7 @@ pytest-mock = "^3.10.0"
syrupy = "^4.0.2"
pytest-watcher = "^0.3.4"
pytest-asyncio = "^0.21.1"
-langchain-core = { path = "../../core", develop = true }
+gigachain-core = { path = "../../core", develop = true }
requests = "^2.31.0"
[tool.poetry.group.codespell]
@@ -47,13 +47,13 @@ ruff = "^0.1.5"
[tool.poetry.group.typing.dependencies]
mypy = "^0.991"
-langchain-core = { path = "../../core", develop = true }
+gigachain-core = { path = "../../core", develop = true }
[tool.poetry.group.dev]
optional = true
[tool.poetry.group.dev.dependencies]
-langchain-core = { path = "../../core", develop = true }
+gigachain-core = { path = "../../core", develop = true }
[tool.ruff]
select = [
diff --git a/libs/partners/robocorp/pyproject.toml b/libs/partners/robocorp/pyproject.toml
index 8aa0204fb98e2..44d2e7bfb69fa 100644
--- a/libs/partners/robocorp/pyproject.toml
+++ b/libs/partners/robocorp/pyproject.toml
@@ -1,19 +1,14 @@
-
[tool.poetry]
name = "gigachain-robocorp"
-version = "0.0.7"
-description = "An integration package connecting Robocorp Action Server and LangChain"
+version = "0.0.9.post1"
+description = "An integration package connecting Robocorp Action Server and Gigachain"
authors = []
readme = "README.md"
-repository = "https://github.com/gigachain-ai/gigachain"
+repository = "https://github.com/langchain-ai/langchain"
license = "MIT"
-packages = [
- {include = "langchain_robocorp"}
-]
-
[tool.poetry.urls]
-"Source Code" = "https://github.com/gigachain-ai/gigachain/tree/master/libs/partners/robocorp"
+"Source Code" = "https://github.com/langchain-ai/langchain/tree/master/libs/partners/robocorp"
[tool.poetry.dependencies]
python = ">=3.8.1,<4.0"
@@ -97,4 +92,3 @@ markers = [
"compile: mark placeholder test used to compile integration tests without running them",
]
asyncio_mode = "auto"
-
diff --git a/libs/partners/robocorp/tests/unit_tests/_openapi3.fixture.json b/libs/partners/robocorp/tests/unit_tests/_openapi3.fixture.json
new file mode 100644
index 0000000000000..97f07b218769e
--- /dev/null
+++ b/libs/partners/robocorp/tests/unit_tests/_openapi3.fixture.json
@@ -0,0 +1,1891 @@
+{
+ "openapi": "3.1.0",
+ "info": {
+ "title": "Sema4.ai Action Server",
+ "version": "0.11.0"
+ },
+ "servers": [
+ {
+ "url": "http://localhost:8806"
+ }
+ ],
+ "paths": {
+ "/api/actions/google-calendar/create-event/run": {
+ "post": {
+ "summary": "Create Event",
+ "description": "Creates a new event in the specified calendar.",
+ "operationId": "create_event",
+ "requestBody": {
+ "content": {
+ "application/json": {
+ "schema": {
+ "properties": {
+ "event": {
+ "properties": {
+ "id": {
+ "type": "string",
+ "title": "Id",
+ "description": "The id of the event."
+ },
+ "summary": {
+ "type": "string",
+ "title": "Summary",
+ "description": "A short summary of the event's purpose."
+ },
+ "location": {
+ "anyOf": [
+ {
+ "type": "string"
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "title": "Location",
+ "description": "The physical location of the event."
+ },
+ "description": {
+ "anyOf": [
+ {
+ "type": "string"
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "title": "Description",
+ "description": "A more detailed description of the event."
+ },
+ "start": {
+ "allOf": [
+ {
+ "properties": {
+ "date": {
+ "anyOf": [
+ {
+ "type": "string"
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "title": "Date",
+ "description": "The date, in the format 'yyyy-mm-dd', if this is an all-day event."
+ },
+ "dateTime": {
+ "anyOf": [
+ {
+ "type": "string"
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "title": "Datetime",
+ "description": "The start or end time of the event."
+ },
+ "timeZone": {
+ "anyOf": [
+ {
+ "type": "string"
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "title": "Timezone",
+ "description": "The time zone in which the time is specified, formatted as IANA Time Zone Database. For single events this field is optional."
+ }
+ },
+ "type": "object",
+ "title": "EventDateTime"
+ }
+ ],
+ "description": "The (inclusive) start time of the event."
+ },
+ "end": {
+ "allOf": [
+ {
+ "properties": {
+ "date": {
+ "anyOf": [
+ {
+ "type": "string"
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "title": "Date",
+ "description": "The date, in the format 'yyyy-mm-dd', if this is an all-day event."
+ },
+ "dateTime": {
+ "anyOf": [
+ {
+ "type": "string"
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "title": "Datetime",
+ "description": "The start or end time of the event."
+ },
+ "timeZone": {
+ "anyOf": [
+ {
+ "type": "string"
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "title": "Timezone",
+ "description": "The time zone in which the time is specified, formatted as IANA Time Zone Database. For single events this field is optional."
+ }
+ },
+ "type": "object",
+ "title": "EventDateTime"
+ }
+ ],
+ "description": "The (exclusive) end time of the event."
+ },
+ "recurrence": {
+ "anyOf": [
+ {
+ "items": {
+ "type": "string"
+ },
+ "type": "array"
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "title": "Recurrence",
+ "description": "A list of RRULE, EXRULE, RDATE, and EXDATE lines for a recurring event."
+ },
+ "attendees": {
+ "anyOf": [
+ {
+ "items": {
+ "properties": {
+ "email": {
+ "type": "string",
+ "title": "Email",
+ "description": "The email address of the identity."
+ },
+ "displayName": {
+ "anyOf": [
+ {
+ "type": "string"
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "title": "Displayname",
+ "description": "The display name of the identity."
+ },
+ "optional": {
+ "anyOf": [
+ {
+ "type": "boolean"
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "title": "Optional",
+ "description": "Whether this is an optional attendee."
+ },
+ "responseStatus": {
+ "anyOf": [
+ {
+ "type": "string"
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "title": "Responsestatus",
+ "description": "The response status of the attendee."
+ },
+ "organizer": {
+ "anyOf": [
+ {
+ "type": "boolean"
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "title": "Organizer",
+ "description": "Whether the attendee is the organizer of the event."
+ }
+ },
+ "type": "object",
+ "required": [
+ "email"
+ ],
+ "title": "Attendee"
+ },
+ "type": "array"
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "title": "Attendees",
+ "description": "A list of attendees."
+ },
+ "reminders": {
+ "anyOf": [
+ {
+ "properties": {
+ "useDefault": {
+ "type": "boolean",
+ "title": "Usedefault",
+ "description": "Indicates whether to use the default reminders."
+ },
+ "overrides": {
+ "anyOf": [
+ {
+ "items": {
+ "properties": {
+ "method": {
+ "type": "string",
+ "title": "Method",
+ "description": "The method of the reminder (email or popup)."
+ },
+ "minutes": {
+ "type": "integer",
+ "title": "Minutes",
+ "description": "The number of minutes before the event when the reminder should occur."
+ }
+ },
+ "type": "object",
+ "required": [
+ "method",
+ "minutes"
+ ],
+ "title": "ReminderOverride"
+ },
+ "type": "array"
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "title": "Overrides",
+ "description": "A list of overrides for the reminders."
+ }
+ },
+ "type": "object",
+ "required": [
+ "useDefault"
+ ],
+ "title": "Reminder"
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "description": "Reminders settings for the event."
+ },
+ "organizer": {
+ "allOf": [
+ {
+ "properties": {
+ "email": {
+ "type": "string",
+ "title": "Email",
+ "description": "The email address of the identity."
+ },
+ "displayName": {
+ "anyOf": [
+ {
+ "type": "string"
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "title": "Displayname",
+ "description": "The display name of the identity."
+ }
+ },
+ "type": "object",
+ "required": [
+ "email"
+ ],
+ "title": "Identity"
+ }
+ ],
+ "description": "The organizer of the event."
+ }
+ },
+ "type": "object",
+ "required": [
+ "id",
+ "summary",
+ "start",
+ "end",
+ "organizer"
+ ],
+ "title": "Event",
+ "description": "JSON representation of the Google Calendar V3 event."
+ },
+ "calendar_id": {
+ "type": "string",
+ "title": "Calendar Id",
+ "description": "Calendar identifier which can be found by listing all calendars action.\nDefault value is \"primary\" which indicates the calendar where the user is currently logged in.",
+ "default": "primary"
+ }
+ },
+ "type": "object",
+ "required": [
+ "event"
+ ]
+ }
+ }
+ },
+ "required": true
+ },
+ "responses": {
+ "200": {
+ "description": "Successful Response",
+ "content": {
+ "application/json": {
+ "schema": {
+ "properties": {
+ "id": {
+ "type": "string",
+ "title": "Id",
+ "description": "The id of the event."
+ },
+ "summary": {
+ "type": "string",
+ "title": "Summary",
+ "description": "A short summary of the event's purpose."
+ },
+ "location": {
+ "anyOf": [
+ {
+ "type": "string"
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "title": "Location",
+ "description": "The physical location of the event."
+ },
+ "description": {
+ "anyOf": [
+ {
+ "type": "string"
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "title": "Description",
+ "description": "A more detailed description of the event."
+ },
+ "start": {
+ "allOf": [
+ {
+ "properties": {
+ "date": {
+ "anyOf": [
+ {
+ "type": "string"
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "title": "Date",
+ "description": "The date, in the format 'yyyy-mm-dd', if this is an all-day event."
+ },
+ "dateTime": {
+ "anyOf": [
+ {
+ "type": "string"
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "title": "Datetime",
+ "description": "The start or end time of the event."
+ },
+ "timeZone": {
+ "anyOf": [
+ {
+ "type": "string"
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "title": "Timezone",
+ "description": "The time zone in which the time is specified, formatted as IANA Time Zone Database. For single events this field is optional."
+ }
+ },
+ "type": "object",
+ "title": "EventDateTime"
+ }
+ ],
+ "description": "The (inclusive) start time of the event."
+ },
+ "end": {
+ "allOf": [
+ {
+ "properties": {
+ "date": {
+ "anyOf": [
+ {
+ "type": "string"
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "title": "Date",
+ "description": "The date, in the format 'yyyy-mm-dd', if this is an all-day event."
+ },
+ "dateTime": {
+ "anyOf": [
+ {
+ "type": "string"
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "title": "Datetime",
+ "description": "The start or end time of the event."
+ },
+ "timeZone": {
+ "anyOf": [
+ {
+ "type": "string"
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "title": "Timezone",
+ "description": "The time zone in which the time is specified, formatted as IANA Time Zone Database. For single events this field is optional."
+ }
+ },
+ "type": "object",
+ "title": "EventDateTime"
+ }
+ ],
+ "description": "The (exclusive) end time of the event."
+ },
+ "recurrence": {
+ "anyOf": [
+ {
+ "items": {
+ "type": "string"
+ },
+ "type": "array"
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "title": "Recurrence",
+ "description": "A list of RRULE, EXRULE, RDATE, and EXDATE lines for a recurring event."
+ },
+ "attendees": {
+ "anyOf": [
+ {
+ "items": {
+ "properties": {
+ "email": {
+ "type": "string",
+ "title": "Email",
+ "description": "The email address of the identity."
+ },
+ "displayName": {
+ "anyOf": [
+ {
+ "type": "string"
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "title": "Displayname",
+ "description": "The display name of the identity."
+ },
+ "optional": {
+ "anyOf": [
+ {
+ "type": "boolean"
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "title": "Optional",
+ "description": "Whether this is an optional attendee."
+ },
+ "responseStatus": {
+ "anyOf": [
+ {
+ "type": "string"
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "title": "Responsestatus",
+ "description": "The response status of the attendee."
+ },
+ "organizer": {
+ "anyOf": [
+ {
+ "type": "boolean"
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "title": "Organizer",
+ "description": "Whether the attendee is the organizer of the event."
+ }
+ },
+ "type": "object",
+ "required": [
+ "email"
+ ],
+ "title": "Attendee"
+ },
+ "type": "array"
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "title": "Attendees",
+ "description": "A list of attendees."
+ },
+ "reminders": {
+ "anyOf": [
+ {
+ "properties": {
+ "useDefault": {
+ "type": "boolean",
+ "title": "Usedefault",
+ "description": "Indicates whether to use the default reminders."
+ },
+ "overrides": {
+ "anyOf": [
+ {
+ "items": {
+ "properties": {
+ "method": {
+ "type": "string",
+ "title": "Method",
+ "description": "The method of the reminder (email or popup)."
+ },
+ "minutes": {
+ "type": "integer",
+ "title": "Minutes",
+ "description": "The number of minutes before the event when the reminder should occur."
+ }
+ },
+ "type": "object",
+ "required": [
+ "method",
+ "minutes"
+ ],
+ "title": "ReminderOverride"
+ },
+ "type": "array"
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "title": "Overrides",
+ "description": "A list of overrides for the reminders."
+ }
+ },
+ "type": "object",
+ "required": [
+ "useDefault"
+ ],
+ "title": "Reminder"
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "description": "Reminders settings for the event."
+ },
+ "organizer": {
+ "allOf": [
+ {
+ "properties": {
+ "email": {
+ "type": "string",
+ "title": "Email",
+ "description": "The email address of the identity."
+ },
+ "displayName": {
+ "anyOf": [
+ {
+ "type": "string"
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "title": "Displayname",
+ "description": "The display name of the identity."
+ }
+ },
+ "type": "object",
+ "required": [
+ "email"
+ ],
+ "title": "Identity"
+ }
+ ],
+ "description": "The organizer of the event."
+ }
+ },
+ "type": "object",
+ "required": [
+ "id",
+ "summary",
+ "start",
+ "end",
+ "organizer"
+ ],
+ "title": "Response for Create Event",
+ "description": "The newly created event."
+ }
+ }
+ }
+ },
+ "422": {
+ "description": "Validation Error",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/HTTPValidationError"
+ }
+ }
+ }
+ }
+ },
+ "x-openai-isConsequential": true
+ }
+ },
+ "/api/actions/google-calendar/list-events/run": {
+ "post": {
+ "summary": "List Events",
+ "description": "List all events in the user's primary calendar between the given dates.\nTo aggregate all events across calendars, call this method for each calendar returned by list_calendars endpoint.",
+ "operationId": "list_events",
+ "requestBody": {
+ "content": {
+ "application/json": {
+ "schema": {
+ "properties": {
+ "calendar_id": {
+ "type": "string",
+ "title": "Calendar Id",
+ "description": "Calendar identifier which can be found by listing all calendars action.\nDefault value is \"primary\" which indicates the calendar where the user is currently logged in.",
+ "default": "primary"
+ },
+ "query": {
+ "type": "string",
+ "title": "Query",
+ "description": "Free text search terms to find events that match these terms in summary, description, location,\nattendee's name / email or working location information.",
+ "default": ""
+ },
+ "start_date": {
+ "type": "string",
+ "title": "Start Date",
+ "description": "Upper bound (exclusive) for an event's start time to filter by.\nMust be an RFC3339 timestamp with mandatory time zone offset.",
+ "default": ""
+ },
+ "end_date": {
+ "type": "string",
+ "title": "End Date",
+ "description": "Lower bound (exclusive) for an event's end time to filter by.\nMust be an RFC3339 timestamp with mandatory time zone offset.",
+ "default": ""
+ }
+ },
+ "type": "object"
+ }
+ }
+ },
+ "required": true
+ },
+ "responses": {
+ "200": {
+ "description": "Successful Response",
+ "content": {
+ "application/json": {
+ "schema": {
+ "properties": {
+ "result": {
+ "anyOf": [
+ {
+ "properties": {
+ "events": {
+ "items": {
+ "properties": {
+ "id": {
+ "type": "string",
+ "title": "Id",
+ "description": "The id of the event."
+ },
+ "summary": {
+ "type": "string",
+ "title": "Summary",
+ "description": "A short summary of the event's purpose."
+ },
+ "location": {
+ "anyOf": [
+ {
+ "type": "string"
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "title": "Location",
+ "description": "The physical location of the event."
+ },
+ "description": {
+ "anyOf": [
+ {
+ "type": "string"
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "title": "Description",
+ "description": "A more detailed description of the event."
+ },
+ "start": {
+ "allOf": [
+ {
+ "properties": {
+ "date": {
+ "anyOf": [
+ {
+ "type": "string"
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "title": "Date",
+ "description": "The date, in the format 'yyyy-mm-dd', if this is an all-day event."
+ },
+ "dateTime": {
+ "anyOf": [
+ {
+ "type": "string"
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "title": "Datetime",
+ "description": "The start or end time of the event."
+ },
+ "timeZone": {
+ "anyOf": [
+ {
+ "type": "string"
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "title": "Timezone",
+ "description": "The time zone in which the time is specified, formatted as IANA Time Zone Database. For single events this field is optional."
+ }
+ },
+ "type": "object",
+ "title": "EventDateTime"
+ }
+ ],
+ "description": "The (inclusive) start time of the event."
+ },
+ "end": {
+ "allOf": [
+ {
+ "properties": {
+ "date": {
+ "anyOf": [
+ {
+ "type": "string"
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "title": "Date",
+ "description": "The date, in the format 'yyyy-mm-dd', if this is an all-day event."
+ },
+ "dateTime": {
+ "anyOf": [
+ {
+ "type": "string"
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "title": "Datetime",
+ "description": "The start or end time of the event."
+ },
+ "timeZone": {
+ "anyOf": [
+ {
+ "type": "string"
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "title": "Timezone",
+ "description": "The time zone in which the time is specified, formatted as IANA Time Zone Database. For single events this field is optional."
+ }
+ },
+ "type": "object",
+ "title": "EventDateTime"
+ }
+ ],
+ "description": "The (exclusive) end time of the event."
+ },
+ "recurrence": {
+ "anyOf": [
+ {
+ "items": {
+ "type": "string"
+ },
+ "type": "array"
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "title": "Recurrence",
+ "description": "A list of RRULE, EXRULE, RDATE, and EXDATE lines for a recurring event."
+ },
+ "attendees": {
+ "anyOf": [
+ {
+ "items": {
+ "properties": {
+ "email": {
+ "type": "string",
+ "title": "Email",
+ "description": "The email address of the identity."
+ },
+ "displayName": {
+ "anyOf": [
+ {
+ "type": "string"
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "title": "Displayname",
+ "description": "The display name of the identity."
+ },
+ "optional": {
+ "anyOf": [
+ {
+ "type": "boolean"
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "title": "Optional",
+ "description": "Whether this is an optional attendee."
+ },
+ "responseStatus": {
+ "anyOf": [
+ {
+ "type": "string"
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "title": "Responsestatus",
+ "description": "The response status of the attendee."
+ },
+ "organizer": {
+ "anyOf": [
+ {
+ "type": "boolean"
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "title": "Organizer",
+ "description": "Whether the attendee is the organizer of the event."
+ }
+ },
+ "type": "object",
+ "required": [
+ "email"
+ ],
+ "title": "Attendee"
+ },
+ "type": "array"
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "title": "Attendees",
+ "description": "A list of attendees."
+ },
+ "reminders": {
+ "anyOf": [
+ {
+ "properties": {
+ "useDefault": {
+ "type": "boolean",
+ "title": "Usedefault",
+ "description": "Indicates whether to use the default reminders."
+ },
+ "overrides": {
+ "anyOf": [
+ {
+ "items": {
+ "properties": {
+ "method": {
+ "type": "string",
+ "title": "Method",
+ "description": "The method of the reminder (email or popup)."
+ },
+ "minutes": {
+ "type": "integer",
+ "title": "Minutes",
+ "description": "The number of minutes before the event when the reminder should occur."
+ }
+ },
+ "type": "object",
+ "required": [
+ "method",
+ "minutes"
+ ],
+ "title": "ReminderOverride"
+ },
+ "type": "array"
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "title": "Overrides",
+ "description": "A list of overrides for the reminders."
+ }
+ },
+ "type": "object",
+ "required": [
+ "useDefault"
+ ],
+ "title": "Reminder"
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "description": "Reminders settings for the event."
+ },
+ "organizer": {
+ "allOf": [
+ {
+ "properties": {
+ "email": {
+ "type": "string",
+ "title": "Email",
+ "description": "The email address of the identity."
+ },
+ "displayName": {
+ "anyOf": [
+ {
+ "type": "string"
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "title": "Displayname",
+ "description": "The display name of the identity."
+ }
+ },
+ "type": "object",
+ "required": [
+ "email"
+ ],
+ "title": "Identity"
+ }
+ ],
+ "description": "The organizer of the event."
+ }
+ },
+ "type": "object",
+ "required": [
+ "id",
+ "summary",
+ "start",
+ "end",
+ "organizer"
+ ],
+ "title": "Event"
+ },
+ "type": "array",
+ "title": "Events"
+ }
+ },
+ "type": "object",
+ "required": [
+ "events"
+ ],
+ "title": "EventList"
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "description": "The result for the action if it ran successfully"
+ },
+ "error": {
+ "anyOf": [
+ {
+ "type": "string"
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "title": "Error",
+ "description": "The error message if the action failed for some reason"
+ }
+ },
+ "type": "object",
+ "title": "Response for List Events",
+ "description": "A list of calendar events that match the query, if defined."
+ }
+ }
+ }
+ },
+ "422": {
+ "description": "Validation Error",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/HTTPValidationError"
+ }
+ }
+ }
+ }
+ },
+ "x-openai-isConsequential": false
+ }
+ },
+ "/api/actions/google-calendar/list-calendars/run": {
+ "post": {
+ "summary": "List Calendars",
+ "description": "List all calendars that the user is subscribed to.",
+ "operationId": "list_calendars",
+ "requestBody": {
+ "content": {
+ "application/json": {
+ "schema": {
+ "properties": {},
+ "type": "object"
+ }
+ }
+ },
+ "required": true
+ },
+ "responses": {
+ "200": {
+ "description": "Successful Response",
+ "content": {
+ "application/json": {
+ "schema": {
+ "properties": {
+ "result": {
+ "anyOf": [
+ {
+ "properties": {
+ "calendars": {
+ "items": {
+ "properties": {
+ "id": {
+ "type": "string",
+ "title": "Id",
+ "description": "The id of the calendar."
+ },
+ "summary": {
+ "type": "string",
+ "title": "Summary",
+ "description": "The name or summary of the calendar."
+ },
+ "timeZone": {
+ "type": "string",
+ "title": "Timezone",
+ "description": "The timezone the calendar is set to, such as 'Europe/Bucharest'."
+ },
+ "selected": {
+ "type": "boolean",
+ "title": "Selected",
+ "description": "A boolean indicating if the calendar is selected by the user in their UI."
+ },
+ "accessRole": {
+ "type": "string",
+ "title": "Accessrole",
+ "description": "The access role of the user with respect to the calendar, e.g., 'owner'."
+ }
+ },
+ "type": "object",
+ "required": [
+ "id",
+ "summary",
+ "timeZone",
+ "selected",
+ "accessRole"
+ ],
+ "title": "Calendar"
+ },
+ "type": "array",
+ "title": "Calendars"
+ }
+ },
+ "type": "object",
+ "required": [
+ "calendars"
+ ],
+ "title": "CalendarList"
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "description": "The result for the action if it ran successfully"
+ },
+ "error": {
+ "anyOf": [
+ {
+ "type": "string"
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "title": "Error",
+ "description": "The error message if the action failed for some reason"
+ }
+ },
+ "type": "object",
+ "title": "Response for List Calendars",
+ "description": "A list of calendars."
+ }
+ }
+ }
+ },
+ "422": {
+ "description": "Validation Error",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/HTTPValidationError"
+ }
+ }
+ }
+ }
+ },
+ "x-openai-isConsequential": false
+ }
+ },
+ "/api/actions/google-calendar/update-event/run": {
+ "post": {
+ "summary": "Update Event",
+ "description": "Update an existing Google Calendar event with dynamic arguments.",
+ "operationId": "update_event",
+ "requestBody": {
+ "content": {
+ "application/json": {
+ "schema": {
+ "properties": {
+ "event_id": {
+ "type": "string",
+ "title": "Event Id",
+ "description": "Identifier of the event to update. Can be found by listing events in all calendars."
+ },
+ "updates": {
+ "properties": {
+ "summary": {
+ "anyOf": [
+ {
+ "type": "string"
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "title": "Summary",
+ "description": "A short summary of the event's purpose."
+ },
+ "location": {
+ "anyOf": [
+ {
+ "type": "string"
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "title": "Location",
+ "description": "The physical location of the event."
+ },
+ "description": {
+ "anyOf": [
+ {
+ "type": "string"
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "title": "Description",
+ "description": "A more detailed description of the event."
+ },
+ "start": {
+ "anyOf": [
+ {
+ "properties": {
+ "date": {
+ "anyOf": [
+ {
+ "type": "string"
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "title": "Date",
+ "description": "The date, in the format 'yyyy-mm-dd', if this is an all-day event."
+ },
+ "dateTime": {
+ "anyOf": [
+ {
+ "type": "string"
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "title": "Datetime",
+ "description": "The start or end time of the event."
+ },
+ "timeZone": {
+ "anyOf": [
+ {
+ "type": "string"
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "title": "Timezone",
+ "description": "The time zone in which the time is specified, formatted as IANA Time Zone Database. For single events this field is optional."
+ }
+ },
+ "type": "object",
+ "title": "EventDateTime"
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "description": "The (inclusive) start time of the event."
+ },
+ "end": {
+ "anyOf": [
+ {
+ "properties": {
+ "date": {
+ "anyOf": [
+ {
+ "type": "string"
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "title": "Date",
+ "description": "The date, in the format 'yyyy-mm-dd', if this is an all-day event."
+ },
+ "dateTime": {
+ "anyOf": [
+ {
+ "type": "string"
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "title": "Datetime",
+ "description": "The start or end time of the event."
+ },
+ "timeZone": {
+ "anyOf": [
+ {
+ "type": "string"
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "title": "Timezone",
+ "description": "The time zone in which the time is specified, formatted as IANA Time Zone Database. For single events this field is optional."
+ }
+ },
+ "type": "object",
+ "title": "EventDateTime"
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "description": "The (exclusive) end time of the event."
+ },
+ "attendees": {
+ "anyOf": [
+ {
+ "items": {
+ "properties": {
+ "email": {
+ "type": "string",
+ "title": "Email",
+ "description": "The email address of the identity."
+ },
+ "displayName": {
+ "anyOf": [
+ {
+ "type": "string"
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "title": "Displayname",
+ "description": "The display name of the identity."
+ },
+ "optional": {
+ "anyOf": [
+ {
+ "type": "boolean"
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "title": "Optional",
+ "description": "Whether this is an optional attendee."
+ },
+ "responseStatus": {
+ "anyOf": [
+ {
+ "type": "string"
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "title": "Responsestatus",
+ "description": "The response status of the attendee."
+ },
+ "organizer": {
+ "anyOf": [
+ {
+ "type": "boolean"
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "title": "Organizer",
+ "description": "Whether the attendee is the organizer of the event."
+ }
+ },
+ "type": "object",
+ "required": [
+ "email"
+ ],
+ "title": "Attendee"
+ },
+ "type": "array"
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "title": "Attendees",
+ "description": "A list of attendees consisting in email and whether they are mandatory to participate or not."
+ }
+ },
+ "type": "object",
+ "title": "Updates",
+ "description": "A dictionary containing the event attributes to update.\nPossible keys include 'summary', 'description', 'start', 'end', and 'attendees'."
+ },
+ "calendar_id": {
+ "type": "string",
+ "title": "Calendar Id",
+ "description": "Identifier of the calendar where the event is.\nDefault value is \"primary\" which indicates the calendar where the user is currently logged in.",
+ "default": "primary"
+ }
+ },
+ "type": "object",
+ "required": [
+ "event_id",
+ "updates"
+ ]
+ }
+ }
+ },
+ "required": true
+ },
+ "responses": {
+ "200": {
+ "description": "Successful Response",
+ "content": {
+ "application/json": {
+ "schema": {
+ "properties": {
+ "result": {
+ "anyOf": [
+ {
+ "properties": {
+ "id": {
+ "type": "string",
+ "title": "Id",
+ "description": "The id of the event."
+ },
+ "summary": {
+ "type": "string",
+ "title": "Summary",
+ "description": "A short summary of the event's purpose."
+ },
+ "location": {
+ "anyOf": [
+ {
+ "type": "string"
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "title": "Location",
+ "description": "The physical location of the event."
+ },
+ "description": {
+ "anyOf": [
+ {
+ "type": "string"
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "title": "Description",
+ "description": "A more detailed description of the event."
+ },
+ "start": {
+ "allOf": [
+ {
+ "properties": {
+ "date": {
+ "anyOf": [
+ {
+ "type": "string"
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "title": "Date",
+ "description": "The date, in the format 'yyyy-mm-dd', if this is an all-day event."
+ },
+ "dateTime": {
+ "anyOf": [
+ {
+ "type": "string"
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "title": "Datetime",
+ "description": "The start or end time of the event."
+ },
+ "timeZone": {
+ "anyOf": [
+ {
+ "type": "string"
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "title": "Timezone",
+ "description": "The time zone in which the time is specified, formatted as IANA Time Zone Database. For single events this field is optional."
+ }
+ },
+ "type": "object",
+ "title": "EventDateTime"
+ }
+ ],
+ "description": "The (inclusive) start time of the event."
+ },
+ "end": {
+ "allOf": [
+ {
+ "properties": {
+ "date": {
+ "anyOf": [
+ {
+ "type": "string"
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "title": "Date",
+ "description": "The date, in the format 'yyyy-mm-dd', if this is an all-day event."
+ },
+ "dateTime": {
+ "anyOf": [
+ {
+ "type": "string"
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "title": "Datetime",
+ "description": "The start or end time of the event."
+ },
+ "timeZone": {
+ "anyOf": [
+ {
+ "type": "string"
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "title": "Timezone",
+ "description": "The time zone in which the time is specified, formatted as IANA Time Zone Database. For single events this field is optional."
+ }
+ },
+ "type": "object",
+ "title": "EventDateTime"
+ }
+ ],
+ "description": "The (exclusive) end time of the event."
+ },
+ "recurrence": {
+ "anyOf": [
+ {
+ "items": {
+ "type": "string"
+ },
+ "type": "array"
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "title": "Recurrence",
+ "description": "A list of RRULE, EXRULE, RDATE, and EXDATE lines for a recurring event."
+ },
+ "attendees": {
+ "anyOf": [
+ {
+ "items": {
+ "properties": {
+ "email": {
+ "type": "string",
+ "title": "Email",
+ "description": "The email address of the identity."
+ },
+ "displayName": {
+ "anyOf": [
+ {
+ "type": "string"
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "title": "Displayname",
+ "description": "The display name of the identity."
+ },
+ "optional": {
+ "anyOf": [
+ {
+ "type": "boolean"
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "title": "Optional",
+ "description": "Whether this is an optional attendee."
+ },
+ "responseStatus": {
+ "anyOf": [
+ {
+ "type": "string"
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "title": "Responsestatus",
+ "description": "The response status of the attendee."
+ },
+ "organizer": {
+ "anyOf": [
+ {
+ "type": "boolean"
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "title": "Organizer",
+ "description": "Whether the attendee is the organizer of the event."
+ }
+ },
+ "type": "object",
+ "required": [
+ "email"
+ ],
+ "title": "Attendee"
+ },
+ "type": "array"
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "title": "Attendees",
+ "description": "A list of attendees."
+ },
+ "reminders": {
+ "anyOf": [
+ {
+ "properties": {
+ "useDefault": {
+ "type": "boolean",
+ "title": "Usedefault",
+ "description": "Indicates whether to use the default reminders."
+ },
+ "overrides": {
+ "anyOf": [
+ {
+ "items": {
+ "properties": {
+ "method": {
+ "type": "string",
+ "title": "Method",
+ "description": "The method of the reminder (email or popup)."
+ },
+ "minutes": {
+ "type": "integer",
+ "title": "Minutes",
+ "description": "The number of minutes before the event when the reminder should occur."
+ }
+ },
+ "type": "object",
+ "required": [
+ "method",
+ "minutes"
+ ],
+ "title": "ReminderOverride"
+ },
+ "type": "array"
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "title": "Overrides",
+ "description": "A list of overrides for the reminders."
+ }
+ },
+ "type": "object",
+ "required": [
+ "useDefault"
+ ],
+ "title": "Reminder"
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "description": "Reminders settings for the event."
+ },
+ "organizer": {
+ "allOf": [
+ {
+ "properties": {
+ "email": {
+ "type": "string",
+ "title": "Email",
+ "description": "The email address of the identity."
+ },
+ "displayName": {
+ "anyOf": [
+ {
+ "type": "string"
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "title": "Displayname",
+ "description": "The display name of the identity."
+ }
+ },
+ "type": "object",
+ "required": [
+ "email"
+ ],
+ "title": "Identity"
+ }
+ ],
+ "description": "The organizer of the event."
+ }
+ },
+ "type": "object",
+ "required": [
+ "id",
+ "summary",
+ "start",
+ "end",
+ "organizer"
+ ],
+ "title": "Event"
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "description": "The result for the action if it ran successfully"
+ },
+ "error": {
+ "anyOf": [
+ {
+ "type": "string"
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "title": "Error",
+ "description": "The error message if the action failed for some reason"
+ }
+ },
+ "type": "object",
+ "title": "Response for Update Event",
+ "description": "Updated event details."
+ }
+ }
+ }
+ },
+ "422": {
+ "description": "Validation Error",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/HTTPValidationError"
+ }
+ }
+ }
+ }
+ },
+ "x-openai-isConsequential": true
+ }
+ }
+ },
+ "components": {
+ "schemas": {
+ "HTTPValidationError": {
+ "properties": {
+ "errors": {
+ "items": {
+ "$ref": "#/components/schemas/ValidationError"
+ },
+ "type": "array",
+ "title": "Errors"
+ }
+ },
+ "type": "object",
+ "title": "HTTPValidationError"
+ },
+ "ValidationError": {
+ "properties": {
+ "loc": {
+ "items": {
+ "anyOf": [
+ {
+ "type": "string"
+ },
+ {
+ "type": "integer"
+ }
+ ]
+ },
+ "type": "array",
+ "title": "Location"
+ },
+ "msg": {
+ "type": "string",
+ "title": "Message"
+ },
+ "type": {
+ "type": "string",
+ "title": "Error Type"
+ }
+ },
+ "type": "object",
+ "required": [
+ "loc",
+ "msg",
+ "type"
+ ],
+ "title": "ValidationError"
+ }
+ }
+ }
+}
diff --git a/libs/partners/together/pyproject.toml b/libs/partners/together/pyproject.toml
index fcf657c8fd310..355b776f0d277 100644
--- a/libs/partners/together/pyproject.toml
+++ b/libs/partners/together/pyproject.toml
@@ -1,7 +1,7 @@
[tool.poetry]
-name = "langchain-together"
-version = "0.1.2"
-description = "An integration package connecting Together AI and LangChain"
+name = "gigachain-together"
+version = "0.1.3"
+description = "An integration package connecting Together AI and Gigachain"
authors = []
readme = "README.md"
repository = "https://github.com/langchain-ai/langchain"
@@ -12,8 +12,8 @@ license = "MIT"
[tool.poetry.dependencies]
python = ">=3.8.1,<4.0"
-gigachain-core = ">=0.1.52,<0.3"
-gigachain-openai = "^0.1.3"
+gigachain-core = ">=0.2.2,<0.3"
+gigachain-openai = "^0.1.8"
requests = "^2"
aiohttp = "^3.9.1"
@@ -30,7 +30,6 @@ pytest-asyncio = "^0.21.1"
gigachain-openai = { path = "../openai", develop = true }
gigachain-core = { path = "../../core", develop = true }
docarray = "^0.32.1"
-pydantic = "^1.10.9"
gigachain-standard-tests = { path = "../../standard-tests", develop = true }
[tool.poetry.group.codespell]
@@ -43,6 +42,12 @@ codespell = "^2.2.0"
optional = true
[tool.poetry.group.test_integration.dependencies]
+# Support Python 3.8 and 3.12+.
+numpy = [
+ {version = "^1", python = "<3.12"},
+ {version = "^1.26.0", python = ">=3.12"}
+]
+
[tool.poetry.group.lint]
optional = true
@@ -64,13 +69,20 @@ optional = true
[tool.poetry.group.dev.dependencies]
gigachain-core = { path = "../../core", develop = true }
-[tool.ruff]
+[tool.ruff.lint]
select = [
"E", # pycodestyle
"F", # pyflakes
"I", # isort
+ "D", # pydocstyle
]
+[tool.ruff.lint.pydocstyle]
+convention = "google"
+
+[tool.ruff.lint.per-file-ignores]
+"tests/**" = ["D"] # ignore docstring checks for tests
+
[tool.mypy]
disallow_untyped_defs = "True"
diff --git a/libs/partners/upstage/pyproject.toml b/libs/partners/upstage/pyproject.toml
index c51c640852d1d..baef74fb7ce34 100644
--- a/libs/partners/upstage/pyproject.toml
+++ b/libs/partners/upstage/pyproject.toml
@@ -2,7 +2,7 @@
[tool.poetry]
name = "gigachain-upstage"
version = "0.1.5"
-description = "An integration package connecting Upstage and LangChain"
+description = "An integration package connecting Upstage and Gigachain"
authors = []
readme = "README.md"
repository = "https://github.com/langchain-ai/langchain"
@@ -16,8 +16,8 @@ packages = [
[tool.poetry.dependencies]
python = ">=3.8.1,<4.0"
-langchain-core = ">=0.1.52,<0.3"
-langchain-openai = "^0.1.3"
+gigachain-core = ">=0.1.52,<0.3"
+gigachain-openai = "^0.1.3"
pymupdf = "^1.24.1"
requests = "^2.31.0"
diff --git a/libs/partners/voyageai/pyproject.toml b/libs/partners/voyageai/pyproject.toml
index 29038ee01e40e..06a63edc497b9 100644
--- a/libs/partners/voyageai/pyproject.toml
+++ b/libs/partners/voyageai/pyproject.toml
@@ -1,7 +1,7 @@
[tool.poetry]
-name = "langchain-voyageai"
+name = "gigachain-voyageai"
version = "0.1.1"
-description = "An integration package connecting VoyageAI and LangChain"
+description = "An integration package connecting VoyageAI and Gigachain"
authors = []
readme = "README.md"
repository = "https://github.com/langchain-ai/langchain"
diff --git a/libs/standard-tests/langchain_standard_tests/integration_tests/base_store.py b/libs/standard-tests/langchain_standard_tests/integration_tests/base_store.py
new file mode 100644
index 0000000000000..8f74d066a45d6
--- /dev/null
+++ b/libs/standard-tests/langchain_standard_tests/integration_tests/base_store.py
@@ -0,0 +1,276 @@
+from abc import ABC, abstractmethod
+from typing import AsyncGenerator, Generator, Generic, Tuple, TypeVar
+
+import pytest
+from langchain_core.stores import BaseStore
+
+V = TypeVar("V")
+
+
+class BaseStoreSyncTests(ABC, Generic[V]):
+ """Test suite for checking the key-value API of a BaseStore.
+
+ This test suite verifies the basic key-value API of a BaseStore.
+
+ The test suite is designed for synchronous key-value stores.
+
+ Implementers should subclass this test suite and provide a fixture
+ that returns an empty key-value store for each test.
+ """
+
+ @abstractmethod
+ @pytest.fixture
+ def kv_store(self) -> BaseStore[str, V]:
+ """Get the key-value store class to test.
+
+ The returned key-value store should be EMPTY.
+ """
+
+ @abstractmethod
+ @pytest.fixture()
+ def three_values(self) -> Tuple[V, V, V]:
+ """Thee example values that will be used in the tests."""
+ pass
+
+ def test_three_values(self, three_values: Tuple[V, V, V]) -> None:
+ """Test that the fixture provides three values."""
+ assert isinstance(three_values, tuple)
+ assert len(three_values) == 3
+
+ def test_kv_store_is_empty(self, kv_store: BaseStore[str, V]) -> None:
+ """Test that the key-value store is empty."""
+ keys = ["foo", "bar", "buzz"]
+ assert kv_store.mget(keys) == [None, None, None]
+
+ def test_set_and_get_values(
+ self, kv_store: BaseStore[str, V], three_values: Tuple[V, V, V]
+ ) -> None:
+ """Test setting and getting values in the key-value store."""
+ foo = three_values[0]
+ bar = three_values[1]
+ key_value_pairs = [("foo", foo), ("bar", bar)]
+ kv_store.mset(key_value_pairs)
+ assert kv_store.mget(["foo", "bar"]) == [foo, bar]
+
+ def test_store_still_empty(self, kv_store: BaseStore[str, V]) -> None:
+ """This test should follow a test that sets values.
+
+ This just verifies that the fixture is set up properly to be empty
+ after each test.
+ """
+ keys = ["foo"]
+ assert kv_store.mget(keys) == [None]
+
+ def test_delete_values(
+ self, kv_store: BaseStore[str, V], three_values: Tuple[V, V, V]
+ ) -> None:
+ """Test deleting values from the key-value store."""
+ foo = three_values[0]
+ bar = three_values[1]
+ key_value_pairs = [("foo", foo), ("bar", bar)]
+ kv_store.mset(key_value_pairs)
+ kv_store.mdelete(["foo"])
+ assert kv_store.mget(["foo", "bar"]) == [None, bar]
+
+ def test_delete_bulk_values(
+ self, kv_store: BaseStore[str, V], three_values: Tuple[V, V, V]
+ ) -> None:
+ """Test that we can delete several values at once."""
+ foo, bar, buz = three_values
+ key_values = [("foo", foo), ("bar", bar), ("buz", buz)]
+ kv_store.mset(key_values)
+ kv_store.mdelete(["foo", "buz"])
+ assert kv_store.mget(["foo", "bar", "buz"]) == [None, bar, None]
+
+ def test_delete_missing_keys(self, kv_store: BaseStore[str, V]) -> None:
+ """Deleting missing keys should not raise an exception."""
+ kv_store.mdelete(["foo"])
+ kv_store.mdelete(["foo", "bar", "baz"])
+
+ def test_set_values_is_idempotent(
+ self, kv_store: BaseStore[str, V], three_values: Tuple[V, V, V]
+ ) -> None:
+ """Setting values by key should be idempotent."""
+ foo, bar, _ = three_values
+ key_value_pairs = [("foo", foo), ("bar", bar)]
+ kv_store.mset(key_value_pairs)
+ kv_store.mset(key_value_pairs)
+ assert kv_store.mget(["foo", "bar"]) == [foo, bar]
+ assert sorted(kv_store.yield_keys()) == ["bar", "foo"]
+
+ def test_get_can_get_same_value(
+ self, kv_store: BaseStore[str, V], three_values: Tuple[V, V, V]
+ ) -> None:
+ """Test that the same value can be retrieved multiple times."""
+ foo, bar, _ = three_values
+ key_value_pairs = [("foo", foo), ("bar", bar)]
+ kv_store.mset(key_value_pairs)
+ # This test assumes kv_store does not handle duplicates by default
+ assert kv_store.mget(["foo", "bar", "foo", "bar"]) == [foo, bar, foo, bar]
+
+ def test_overwrite_values_by_key(
+ self, kv_store: BaseStore[str, V], three_values: Tuple[V, V, V]
+ ) -> None:
+ """Test that we can overwrite values by key using mset."""
+ foo, bar, buzz = three_values
+ key_value_pairs = [("foo", foo), ("bar", bar)]
+ kv_store.mset(key_value_pairs)
+
+ # Now overwrite value of key "foo"
+ new_key_value_pairs = [("foo", buzz)]
+ kv_store.mset(new_key_value_pairs)
+
+ # Check that the value has been updated
+ assert kv_store.mget(["foo", "bar"]) == [buzz, bar]
+
+ def test_yield_keys(
+ self, kv_store: BaseStore[str, V], three_values: Tuple[V, V, V]
+ ) -> None:
+ """Test that we can yield keys from the store."""
+ foo, bar, buzz = three_values
+ key_value_pairs = [("foo", foo), ("bar", bar)]
+ kv_store.mset(key_value_pairs)
+
+ generator = kv_store.yield_keys()
+ assert isinstance(generator, Generator)
+
+ assert sorted(kv_store.yield_keys()) == ["bar", "foo"]
+ assert sorted(kv_store.yield_keys(prefix="foo")) == ["foo"]
+
+
+class BaseStoreAsyncTests(ABC):
+ """Test suite for checking the key-value API of a BaseStore.
+
+ This test suite verifies the basic key-value API of a BaseStore.
+
+ The test suite is designed for synchronous key-value stores.
+
+ Implementers should subclass this test suite and provide a fixture
+ that returns an empty key-value store for each test.
+ """
+
+ @abstractmethod
+ @pytest.fixture
+ async def kv_store(self) -> BaseStore[str, V]:
+ """Get the key-value store class to test.
+
+ The returned key-value store should be EMPTY.
+ """
+
+ @abstractmethod
+ @pytest.fixture()
+ def three_values(self) -> Tuple[V, V, V]:
+ """Thee example values that will be used in the tests."""
+ pass
+
+ async def test_three_values(self, three_values: Tuple[V, V, V]) -> None:
+ """Test that the fixture provides three values."""
+ assert isinstance(three_values, tuple)
+ assert len(three_values) == 3
+
+ async def test_kv_store_is_empty(self, kv_store: BaseStore[str, V]) -> None:
+ """Test that the key-value store is empty."""
+ keys = ["foo", "bar", "buzz"]
+ assert await kv_store.amget(keys) == [None, None, None]
+
+ async def test_set_and_get_values(
+ self, kv_store: BaseStore[str, V], three_values: Tuple[V, V, V]
+ ) -> None:
+ """Test setting and getting values in the key-value store."""
+ foo = three_values[0]
+ bar = three_values[1]
+ key_value_pairs = [("foo", foo), ("bar", bar)]
+ await kv_store.amset(key_value_pairs)
+ assert await kv_store.amget(["foo", "bar"]) == [foo, bar]
+
+ async def test_store_still_empty(self, kv_store: BaseStore[str, V]) -> None:
+ """This test should follow a test that sets values.
+
+ This just verifies that the fixture is set up properly to be empty
+ after each test.
+ """
+ keys = ["foo"]
+ assert await kv_store.amget(keys) == [None]
+
+ async def test_delete_values(
+ self, kv_store: BaseStore[str, V], three_values: Tuple[V, V, V]
+ ) -> None:
+ """Test deleting values from the key-value store."""
+ foo = three_values[0]
+ bar = three_values[1]
+ key_value_pairs = [("foo", foo), ("bar", bar)]
+ await kv_store.amset(key_value_pairs)
+ await kv_store.amdelete(["foo"])
+ assert await kv_store.amget(["foo", "bar"]) == [None, bar]
+
+ async def test_delete_bulk_values(
+ self, kv_store: BaseStore[str, V], three_values: Tuple[V, V, V]
+ ) -> None:
+ """Test that we can delete several values at once."""
+ foo, bar, buz = three_values
+ key_values = [("foo", foo), ("bar", bar), ("buz", buz)]
+ await kv_store.amset(key_values)
+ await kv_store.amdelete(["foo", "buz"])
+ assert await kv_store.amget(["foo", "bar", "buz"]) == [None, bar, None]
+
+ async def test_delete_missing_keys(self, kv_store: BaseStore[str, V]) -> None:
+ """Deleting missing keys should not raise an exception."""
+ await kv_store.amdelete(["foo"])
+ await kv_store.amdelete(["foo", "bar", "baz"])
+
+ async def test_set_values_is_idempotent(
+ self, kv_store: BaseStore[str, V], three_values: Tuple[V, V, V]
+ ) -> None:
+ """Setting values by key should be idempotent."""
+ foo, bar, _ = three_values
+ key_value_pairs = [("foo", foo), ("bar", bar)]
+ await kv_store.amset(key_value_pairs)
+ await kv_store.amset(key_value_pairs)
+ assert await kv_store.amget(["foo", "bar"]) == [foo, bar]
+ assert sorted(kv_store.yield_keys()) == ["bar", "foo"]
+
+ async def test_get_can_get_same_value(
+ self, kv_store: BaseStore[str, V], three_values: Tuple[V, V, V]
+ ) -> None:
+ """Test that the same value can be retrieved multiple times."""
+ foo, bar, _ = three_values
+ key_value_pairs = [("foo", foo), ("bar", bar)]
+ await kv_store.amset(key_value_pairs)
+ # This test assumes kv_store does not handle duplicates by async default
+ assert await kv_store.amget(["foo", "bar", "foo", "bar"]) == [
+ foo,
+ bar,
+ foo,
+ bar,
+ ]
+
+ async def test_overwrite_values_by_key(
+ self, kv_store: BaseStore[str, V], three_values: Tuple[V, V, V]
+ ) -> None:
+ """Test that we can overwrite values by key using mset."""
+ foo, bar, buzz = three_values
+ key_value_pairs = [("foo", foo), ("bar", bar)]
+ await kv_store.amset(key_value_pairs)
+
+ # Now overwrite value of key "foo"
+ new_key_value_pairs = [("foo", buzz)]
+ await kv_store.amset(new_key_value_pairs)
+
+ # Check that the value has been updated
+ assert await kv_store.amget(["foo", "bar"]) == [buzz, bar]
+
+ async def test_yield_keys(
+ self, kv_store: BaseStore[str, V], three_values: Tuple[V, V, V]
+ ) -> None:
+ """Test that we can yield keys from the store."""
+ foo, bar, buzz = three_values
+ key_value_pairs = [("foo", foo), ("bar", bar)]
+ await kv_store.amset(key_value_pairs)
+
+ generator = kv_store.ayield_keys()
+ assert isinstance(generator, AsyncGenerator)
+
+ assert sorted([key async for key in kv_store.ayield_keys()]) == ["bar", "foo"]
+ assert sorted([key async for key in kv_store.ayield_keys(prefix="foo")]) == [
+ "foo"
+ ]
diff --git a/libs/standard-tests/langchain_standard_tests/integration_tests/cache.py b/libs/standard-tests/langchain_standard_tests/integration_tests/cache.py
new file mode 100644
index 0000000000000..fe84d8450cf47
--- /dev/null
+++ b/libs/standard-tests/langchain_standard_tests/integration_tests/cache.py
@@ -0,0 +1,192 @@
+from abc import ABC, abstractmethod
+
+import pytest
+from langchain_core.caches import BaseCache
+from langchain_core.outputs import Generation
+
+
+class SyncCacheTestSuite(ABC):
+ """Test suite for checking the BaseCache API of a caching layer for LLMs.
+
+ This test suite verifies the basic caching API of a caching layer for LLMs.
+
+ The test suite is designed for synchronous caching layers.
+
+ Implementers should subclass this test suite and provide a fixture
+ that returns an empty cache for each test.
+ """
+
+ @abstractmethod
+ @pytest.fixture
+ def cache(self) -> BaseCache:
+ """Get the cache class to test.
+
+ The returned cache should be EMPTY.
+ """
+
+ def get_sample_prompt(self) -> str:
+ """Return a sample prompt for testing."""
+ return "Sample prompt for testing."
+
+ def get_sample_llm_string(self) -> str:
+ """Return a sample LLM string for testing."""
+ return "Sample LLM string configuration."
+
+ def get_sample_generation(self) -> Generation:
+ """Return a sample Generation object for testing."""
+ return Generation(
+ text="Sample generated text.", generation_info={"reason": "test"}
+ )
+
+ def test_cache_is_empty(self, cache: BaseCache) -> None:
+ """Test that the cache is empty."""
+ assert (
+ cache.lookup(self.get_sample_prompt(), self.get_sample_llm_string()) is None
+ )
+
+ def test_update_cache(self, cache: BaseCache) -> None:
+ """Test updating the cache."""
+ prompt = self.get_sample_prompt()
+ llm_string = self.get_sample_llm_string()
+ generation = self.get_sample_generation()
+ cache.update(prompt, llm_string, [generation])
+ assert cache.lookup(prompt, llm_string) == [generation]
+
+ def test_cache_still_empty(self, cache: BaseCache) -> None:
+ """This test should follow a test that updates the cache.
+
+ This just verifies that the fixture is set up properly to be empty
+ after each test.
+ """
+ assert (
+ cache.lookup(self.get_sample_prompt(), self.get_sample_llm_string()) is None
+ )
+
+ def test_clear_cache(self, cache: BaseCache) -> None:
+ """Test clearing the cache."""
+ prompt = self.get_sample_prompt()
+ llm_string = self.get_sample_llm_string()
+ generation = self.get_sample_generation()
+ cache.update(prompt, llm_string, [generation])
+ cache.clear()
+ assert cache.lookup(prompt, llm_string) is None
+
+ def test_cache_miss(self, cache: BaseCache) -> None:
+ """Test cache miss."""
+ assert cache.lookup("Nonexistent prompt", self.get_sample_llm_string()) is None
+
+ def test_cache_hit(self, cache: BaseCache) -> None:
+ """Test cache hit."""
+ prompt = self.get_sample_prompt()
+ llm_string = self.get_sample_llm_string()
+ generation = self.get_sample_generation()
+ cache.update(prompt, llm_string, [generation])
+ assert cache.lookup(prompt, llm_string) == [generation]
+
+ def test_update_cache_with_multiple_generations(self, cache: BaseCache) -> None:
+ """Test updating the cache with multiple Generation objects."""
+ prompt = self.get_sample_prompt()
+ llm_string = self.get_sample_llm_string()
+ generations = [
+ self.get_sample_generation(),
+ Generation(text="Another generated text."),
+ ]
+ cache.update(prompt, llm_string, generations)
+ assert cache.lookup(prompt, llm_string) == generations
+
+
+class AsyncCacheTestSuite(ABC):
+ """Test suite for checking the BaseCache API of a caching layer for LLMs.
+
+ This test suite verifies the basic caching API of a caching layer for LLMs.
+
+ The test suite is designed for synchronous caching layers.
+
+ Implementers should subclass this test suite and provide a fixture
+ that returns an empty cache for each test.
+ """
+
+ @abstractmethod
+ @pytest.fixture
+ async def cache(self) -> BaseCache:
+ """Get the cache class to test.
+
+ The returned cache should be EMPTY.
+ """
+
+ def get_sample_prompt(self) -> str:
+ """Return a sample prompt for testing."""
+ return "Sample prompt for testing."
+
+ def get_sample_llm_string(self) -> str:
+ """Return a sample LLM string for testing."""
+ return "Sample LLM string configuration."
+
+ def get_sample_generation(self) -> Generation:
+ """Return a sample Generation object for testing."""
+ return Generation(
+ text="Sample generated text.", generation_info={"reason": "test"}
+ )
+
+ async def test_cache_is_empty(self, cache: BaseCache) -> None:
+ """Test that the cache is empty."""
+ assert (
+ await cache.alookup(self.get_sample_prompt(), self.get_sample_llm_string())
+ is None
+ )
+
+ async def test_update_cache(self, cache: BaseCache) -> None:
+ """Test updating the cache."""
+ prompt = self.get_sample_prompt()
+ llm_string = self.get_sample_llm_string()
+ generation = self.get_sample_generation()
+ await cache.aupdate(prompt, llm_string, [generation])
+ assert await cache.alookup(prompt, llm_string) == [generation]
+
+ async def test_cache_still_empty(self, cache: BaseCache) -> None:
+ """This test should follow a test that updates the cache.
+
+ This just verifies that the fixture is set up properly to be empty
+ after each test.
+ """
+ assert (
+ await cache.alookup(self.get_sample_prompt(), self.get_sample_llm_string())
+ is None
+ )
+
+ async def test_clear_cache(self, cache: BaseCache) -> None:
+ """Test clearing the cache."""
+ prompt = self.get_sample_prompt()
+ llm_string = self.get_sample_llm_string()
+ generation = self.get_sample_generation()
+ await cache.aupdate(prompt, llm_string, [generation])
+ await cache.aclear()
+ assert await cache.alookup(prompt, llm_string) is None
+
+ async def test_cache_miss(self, cache: BaseCache) -> None:
+ """Test cache miss."""
+ assert (
+ await cache.alookup("Nonexistent prompt", self.get_sample_llm_string())
+ is None
+ )
+
+ async def test_cache_hit(self, cache: BaseCache) -> None:
+ """Test cache hit."""
+ prompt = self.get_sample_prompt()
+ llm_string = self.get_sample_llm_string()
+ generation = self.get_sample_generation()
+ await cache.aupdate(prompt, llm_string, [generation])
+ assert await cache.alookup(prompt, llm_string) == [generation]
+
+ async def test_update_cache_with_multiple_generations(
+ self, cache: BaseCache
+ ) -> None:
+ """Test updating the cache with multiple Generation objects."""
+ prompt = self.get_sample_prompt()
+ llm_string = self.get_sample_llm_string()
+ generations = [
+ self.get_sample_generation(),
+ Generation(text="Another generated text."),
+ ]
+ await cache.aupdate(prompt, llm_string, generations)
+ assert await cache.alookup(prompt, llm_string) == generations
diff --git a/libs/standard-tests/langchain_standard_tests/integration_tests/vectorstores.py b/libs/standard-tests/langchain_standard_tests/integration_tests/vectorstores.py
new file mode 100644
index 0000000000000..d65eb12934947
--- /dev/null
+++ b/libs/standard-tests/langchain_standard_tests/integration_tests/vectorstores.py
@@ -0,0 +1,301 @@
+"""Test suite to test vectostores."""
+from abc import ABC, abstractmethod
+
+import pytest
+from langchain_core.documents import Document
+from langchain_core.embeddings.fake import DeterministicFakeEmbedding, Embeddings
+from langchain_core.vectorstores import VectorStore
+
+# Arbitrarily chosen. Using a small embedding size
+# so tests are faster and easier to debug.
+EMBEDDING_SIZE = 6
+
+
+class ReadWriteTestSuite(ABC):
+ """Test suite for checking the read-write API of a vectorstore.
+
+ This test suite verifies the basic read-write API of a vectorstore.
+
+ The test suite is designed for synchronous vectorstores.
+
+ Implementers should subclass this test suite and provide a fixture
+ that returns an empty vectorstore for each test.
+
+ The fixture should use the `get_embeddings` method to get a pre-defined
+ embeddings model that should be used for this test suite.
+ """
+
+ @abstractmethod
+ @pytest.fixture
+ def vectorstore(self) -> VectorStore:
+ """Get the vectorstore class to test.
+
+ The returned vectorstore should be EMPTY.
+ """
+
+ @staticmethod
+ def get_embeddings() -> Embeddings:
+ """A pre-defined embeddings model that should be used for this test."""
+ return DeterministicFakeEmbedding(
+ size=EMBEDDING_SIZE,
+ )
+
+ def test_vectorstore_is_empty(self, vectorstore: VectorStore) -> None:
+ """Test that the vectorstore is empty."""
+ assert vectorstore.similarity_search("foo", k=1) == []
+
+ def test_add_documents(self, vectorstore: VectorStore) -> None:
+ """Test adding documents into the vectorstore."""
+ documents = [
+ Document(page_content="foo", metadata={"id": 1}),
+ Document(page_content="bar", metadata={"id": 2}),
+ ]
+ vectorstore.add_documents(documents)
+ documents = vectorstore.similarity_search("bar", k=2)
+ assert documents == [
+ Document(page_content="bar", metadata={"id": 2}),
+ Document(page_content="foo", metadata={"id": 1}),
+ ]
+
+ def test_vectorstore_still_empty(self, vectorstore: VectorStore) -> None:
+ """This test should follow a test that adds documents.
+
+ This just verifies that the fixture is set up properly to be empty
+ after each test.
+ """
+ assert vectorstore.similarity_search("foo", k=1) == []
+
+ def test_deleting_documents(self, vectorstore: VectorStore) -> None:
+ """Test deleting documents from the vectorstore."""
+ documents = [
+ Document(page_content="foo", metadata={"id": 1}),
+ Document(page_content="bar", metadata={"id": 2}),
+ ]
+ vectorstore.add_documents(documents, ids=["1", "2"])
+ vectorstore.delete(["1"])
+ documents = vectorstore.similarity_search("foo", k=1)
+ assert documents == [Document(page_content="bar", metadata={"id": 2})]
+
+ def test_deleting_bulk_documents(self, vectorstore: VectorStore) -> None:
+ """Test that we can delete several documents at once."""
+ documents = [
+ Document(page_content="foo", metadata={"id": 1}),
+ Document(page_content="bar", metadata={"id": 2}),
+ Document(page_content="baz", metadata={"id": 3}),
+ ]
+
+ vectorstore.add_documents(documents, ids=["1", "2", "3"])
+ vectorstore.delete(["1", "2"])
+ documents = vectorstore.similarity_search("foo", k=1)
+ assert documents == [Document(page_content="baz", metadata={"id": 3})]
+
+ def test_delete_missing_content(self, vectorstore: VectorStore) -> None:
+ """Deleting missing content should not raise an exception."""
+ vectorstore.delete(["1"])
+ vectorstore.delete(["1", "2", "3"])
+
+ def test_add_documents_with_ids_is_idempotent(
+ self, vectorstore: VectorStore
+ ) -> None:
+ """Adding by ID should be idempotent."""
+ documents = [
+ Document(page_content="foo", metadata={"id": 1}),
+ Document(page_content="bar", metadata={"id": 2}),
+ ]
+ vectorstore.add_documents(documents, ids=["1", "2"])
+ vectorstore.add_documents(documents, ids=["1", "2"])
+ documents = vectorstore.similarity_search("bar", k=2)
+ assert documents == [
+ Document(page_content="bar", metadata={"id": 2}),
+ Document(page_content="foo", metadata={"id": 1}),
+ ]
+
+ def test_add_documents_without_ids_gets_duplicated(
+ self, vectorstore: VectorStore
+ ) -> None:
+ """Adding documents without specifying IDs should duplicate content."""
+ documents = [
+ Document(page_content="foo", metadata={"id": 1}),
+ Document(page_content="bar", metadata={"id": 2}),
+ ]
+
+ vectorstore.add_documents(documents)
+ vectorstore.add_documents(documents)
+ documents = vectorstore.similarity_search("bar", k=2)
+ assert documents == [
+ Document(page_content="bar", metadata={"id": 2}),
+ Document(page_content="bar", metadata={"id": 2}),
+ ]
+
+ def test_add_documents_by_id_with_mutation(self, vectorstore: VectorStore) -> None:
+ """Test that we can overwrite by ID using add_documents."""
+ documents = [
+ Document(page_content="foo", metadata={"id": 1}),
+ Document(page_content="bar", metadata={"id": 2}),
+ ]
+
+ vectorstore.add_documents(documents=documents, ids=["1", "2"])
+
+ # Now over-write content of ID 1
+ new_documents = [
+ Document(
+ page_content="new foo", metadata={"id": 1, "some_other_field": "foo"}
+ ),
+ ]
+
+ vectorstore.add_documents(documents=new_documents, ids=["1"])
+
+ # Check that the content has been updated
+ documents = vectorstore.similarity_search("new foo", k=2)
+ assert documents == [
+ Document(
+ page_content="new foo", metadata={"id": 1, "some_other_field": "foo"}
+ ),
+ Document(page_content="bar", metadata={"id": 2}),
+ ]
+
+
+class AsyncReadWriteTestSuite(ABC):
+ """Test suite for checking the **async** read-write API of a vectorstore.
+
+ This test suite verifies the basic read-write API of a vectorstore.
+
+ The test suite is designed for asynchronous vectorstores.
+
+ Implementers should subclass this test suite and provide a fixture
+ that returns an empty vectorstore for each test.
+
+ The fixture should use the `get_embeddings` method to get a pre-defined
+ embeddings model that should be used for this test suite.
+ """
+
+ @abstractmethod
+ @pytest.fixture
+ async def vectorstore(self) -> VectorStore:
+ """Get the vectorstore class to test.
+
+ The returned vectorstore should be EMPTY.
+ """
+
+ @staticmethod
+ def get_embeddings() -> Embeddings:
+ """A pre-defined embeddings model that should be used for this test."""
+ return DeterministicFakeEmbedding(
+ size=EMBEDDING_SIZE,
+ )
+
+ async def test_vectorstore_is_empty(self, vectorstore: VectorStore) -> None:
+ """Test that the vectorstore is empty."""
+ assert await vectorstore.asimilarity_search("foo", k=1) == []
+
+ async def test_add_documents(self, vectorstore: VectorStore) -> None:
+ """Test adding documents into the vectorstore."""
+ documents = [
+ Document(page_content="foo", metadata={"id": 1}),
+ Document(page_content="bar", metadata={"id": 2}),
+ ]
+ await vectorstore.aadd_documents(documents)
+ documents = await vectorstore.asimilarity_search("bar", k=2)
+ assert documents == [
+ Document(page_content="bar", metadata={"id": 2}),
+ Document(page_content="foo", metadata={"id": 1}),
+ ]
+
+ async def test_vectorstore_still_empty(self, vectorstore: VectorStore) -> None:
+ """This test should follow a test that adds documents.
+
+ This just verifies that the fixture is set up properly to be empty
+ after each test.
+ """
+ assert await vectorstore.asimilarity_search("foo", k=1) == []
+
+ async def test_deleting_documents(self, vectorstore: VectorStore) -> None:
+ """Test deleting documents from the vectorstore."""
+ documents = [
+ Document(page_content="foo", metadata={"id": 1}),
+ Document(page_content="bar", metadata={"id": 2}),
+ ]
+ await vectorstore.aadd_documents(documents, ids=["1", "2"])
+ await vectorstore.adelete(["1"])
+ documents = await vectorstore.asimilarity_search("foo", k=1)
+ assert documents == [Document(page_content="bar", metadata={"id": 2})]
+
+ async def test_deleting_bulk_documents(self, vectorstore: VectorStore) -> None:
+ """Test that we can delete several documents at once."""
+ documents = [
+ Document(page_content="foo", metadata={"id": 1}),
+ Document(page_content="bar", metadata={"id": 2}),
+ Document(page_content="baz", metadata={"id": 3}),
+ ]
+
+ await vectorstore.aadd_documents(documents, ids=["1", "2", "3"])
+ await vectorstore.adelete(["1", "2"])
+ documents = await vectorstore.asimilarity_search("foo", k=1)
+ assert documents == [Document(page_content="baz", metadata={"id": 3})]
+
+ async def test_delete_missing_content(self, vectorstore: VectorStore) -> None:
+ """Deleting missing content should not raise an exception."""
+ await vectorstore.adelete(["1"])
+ await vectorstore.adelete(["1", "2", "3"])
+
+ async def test_add_documents_with_ids_is_idempotent(
+ self, vectorstore: VectorStore
+ ) -> None:
+ """Adding by ID should be idempotent."""
+ documents = [
+ Document(page_content="foo", metadata={"id": 1}),
+ Document(page_content="bar", metadata={"id": 2}),
+ ]
+ await vectorstore.aadd_documents(documents, ids=["1", "2"])
+ await vectorstore.aadd_documents(documents, ids=["1", "2"])
+ documents = await vectorstore.asimilarity_search("bar", k=2)
+ assert documents == [
+ Document(page_content="bar", metadata={"id": 2}),
+ Document(page_content="foo", metadata={"id": 1}),
+ ]
+
+ async def test_add_documents_without_ids_gets_duplicated(
+ self, vectorstore: VectorStore
+ ) -> None:
+ """Adding documents without specifying IDs should duplicate content."""
+ documents = [
+ Document(page_content="foo", metadata={"id": 1}),
+ Document(page_content="bar", metadata={"id": 2}),
+ ]
+
+ await vectorstore.aadd_documents(documents)
+ await vectorstore.aadd_documents(documents)
+ documents = await vectorstore.asimilarity_search("bar", k=2)
+ assert documents == [
+ Document(page_content="bar", metadata={"id": 2}),
+ Document(page_content="bar", metadata={"id": 2}),
+ ]
+
+ async def test_add_documents_by_id_with_mutation(
+ self, vectorstore: VectorStore
+ ) -> None:
+ """Test that we can overwrite by ID using add_documents."""
+ documents = [
+ Document(page_content="foo", metadata={"id": 1}),
+ Document(page_content="bar", metadata={"id": 2}),
+ ]
+
+ await vectorstore.aadd_documents(documents=documents, ids=["1", "2"])
+
+ # Now over-write content of ID 1
+ new_documents = [
+ Document(
+ page_content="new foo", metadata={"id": 1, "some_other_field": "foo"}
+ ),
+ ]
+
+ await vectorstore.aadd_documents(documents=new_documents, ids=["1"])
+
+ # Check that the content has been updated
+ documents = await vectorstore.asimilarity_search("new foo", k=2)
+ assert documents == [
+ Document(
+ page_content="new foo", metadata={"id": 1, "some_other_field": "foo"}
+ ),
+ Document(page_content="bar", metadata={"id": 2}),
+ ]
diff --git a/libs/standard-tests/pyproject.toml b/libs/standard-tests/pyproject.toml
index 3b926090b7407..1a8dcbc7560b3 100644
--- a/libs/standard-tests/pyproject.toml
+++ b/libs/standard-tests/pyproject.toml
@@ -1,7 +1,7 @@
[tool.poetry]
name = "gigachain-standard-tests"
-version = "0.1.0"
-description = "Standard tests for gigachain implementations"
+version = "0.1.1"
+description = "Standard tests for Gigachain implementations"
authors = ["Erick Friis "]
readme = "README.md"
repository = "https://github.com/langchain-ai/langchain"
@@ -14,12 +14,14 @@ license = "MIT"
python = ">=3.8.1,<4.0"
gigachain-core = ">=0.1.40,<0.3"
pytest = ">=7,<9"
+httpx = "^0.27.0"
[tool.poetry.group.test]
optional = true
[tool.poetry.group.test.dependencies]
gigachain-core = { path = "../core", develop = true }
+pytest-asyncio = "^0.23.7"
[tool.poetry.group.test_integration]
optional = true
@@ -59,3 +61,24 @@ omit = ["tests/*"]
[build-system]
requires = ["poetry-core"]
build-backend = "poetry.core.masonry.api"
+
+[tool.pytest.ini_options]
+# --strict-markers will raise errors on unknown marks.
+# https://docs.pytest.org/en/7.1.x/how-to/mark.html#raising-errors-on-unknown-marks
+#
+# https://docs.pytest.org/en/7.1.x/reference/reference.html
+# --strict-config any warnings encountered while parsing the `pytest`
+# section of the configuration file raise errors.
+#
+# https://github.com/tophat/syrupy
+# --snapshot-warn-unused Prints a warning on unused snapshots rather than fail the test suite.
+addopts = "--strict-markers --strict-config --durations=5 -vv"
+# Registering custom markers.
+# https://docs.pytest.org/en/7.1.x/example/markers.html#registering-markers
+markers = [
+ "requires: mark tests as requiring a specific library",
+ "scheduled: mark tests to run in scheduled testing",
+ "compile: mark placeholder test used to compile integration tests without running them",
+]
+asyncio_mode = "auto"
+
diff --git a/libs/standard-tests/tests/unit_tests/__init__.py b/libs/standard-tests/tests/unit_tests/__init__.py
new file mode 100644
index 0000000000000..e69de29bb2d1d
diff --git a/libs/standard-tests/tests/unit_tests/test_in_memory_base_store.py b/libs/standard-tests/tests/unit_tests/test_in_memory_base_store.py
new file mode 100644
index 0000000000000..245b096554eae
--- /dev/null
+++ b/libs/standard-tests/tests/unit_tests/test_in_memory_base_store.py
@@ -0,0 +1,30 @@
+"""Tests for the InMemoryStore class."""
+from typing import Tuple
+
+import pytest
+from langchain_core.stores import InMemoryStore
+
+from langchain_standard_tests.integration_tests.base_store import (
+ BaseStoreAsyncTests,
+ BaseStoreSyncTests,
+)
+
+
+class TestInMemoryStore(BaseStoreSyncTests):
+ @pytest.fixture
+ def three_values(self) -> Tuple[str, str, str]:
+ return "foo", "bar", "buzz"
+
+ @pytest.fixture
+ def kv_store(self) -> InMemoryStore:
+ return InMemoryStore()
+
+
+class TestInMemoryStoreAsync(BaseStoreAsyncTests):
+ @pytest.fixture
+ def three_values(self) -> Tuple[str, str, str]: # type: ignore
+ return "foo", "bar", "buzz"
+
+ @pytest.fixture
+ async def kv_store(self) -> InMemoryStore:
+ return InMemoryStore()
diff --git a/libs/standard-tests/tests/unit_tests/test_in_memory_cache.py b/libs/standard-tests/tests/unit_tests/test_in_memory_cache.py
new file mode 100644
index 0000000000000..4f67a876490d0
--- /dev/null
+++ b/libs/standard-tests/tests/unit_tests/test_in_memory_cache.py
@@ -0,0 +1,19 @@
+import pytest
+from langchain_core.caches import InMemoryCache
+
+from langchain_standard_tests.integration_tests.cache import (
+ AsyncCacheTestSuite,
+ SyncCacheTestSuite,
+)
+
+
+class TestInMemoryCache(SyncCacheTestSuite):
+ @pytest.fixture
+ def cache(self) -> InMemoryCache:
+ return InMemoryCache()
+
+
+class TestInMemoryCacheAsync(AsyncCacheTestSuite):
+ @pytest.fixture
+ async def cache(self) -> InMemoryCache:
+ return InMemoryCache()
diff --git a/libs/standard-tests/tests/unit_tests/test_in_memory_vectorstore.py b/libs/standard-tests/tests/unit_tests/test_in_memory_vectorstore.py
new file mode 100644
index 0000000000000..d34bf25c3881c
--- /dev/null
+++ b/libs/standard-tests/tests/unit_tests/test_in_memory_vectorstore.py
@@ -0,0 +1,28 @@
+import pytest
+from langchain_core.vectorstores import VectorStore
+
+from langchain_standard_tests.integration_tests.vectorstores import (
+ AsyncReadWriteTestSuite,
+ ReadWriteTestSuite,
+)
+
+# We'll need to move this dependency to core
+pytest.importorskip("langchain_community")
+
+from langchain_community.vectorstores.inmemory import ( # type: ignore # noqa
+ InMemoryVectorStore,
+)
+
+
+class TestInMemoryVectorStore(ReadWriteTestSuite):
+ @pytest.fixture
+ def vectorstore(self) -> VectorStore:
+ embeddings = self.get_embeddings()
+ return InMemoryVectorStore(embedding=embeddings)
+
+
+class TestAysncInMemoryVectorStore(AsyncReadWriteTestSuite):
+ @pytest.fixture
+ async def vectorstore(self) -> VectorStore:
+ embeddings = self.get_embeddings()
+ return InMemoryVectorStore(embedding=embeddings)
diff --git a/libs/text-splitters/extended_testing_deps.txt b/libs/text-splitters/extended_testing_deps.txt
new file mode 100644
index 0000000000000..8d45ad3ea8b0b
--- /dev/null
+++ b/libs/text-splitters/extended_testing_deps.txt
@@ -0,0 +1,2 @@
+lxml>=4.9.3,<6.0
+beautifulsoup4>=4.12.3,<5
diff --git a/libs/text-splitters/pyproject.toml b/libs/text-splitters/pyproject.toml
index c29aba1d483ae..86877168bc8df 100644
--- a/libs/text-splitters/pyproject.toml
+++ b/libs/text-splitters/pyproject.toml
@@ -1,29 +1,23 @@
-
[tool.poetry]
name = "gigachain-text-splitters"
-version = "0.2.0"
-description = "GigaChain text splitting utilities"
+version = "0.2.2"
+description = "Gigachain text splitting utilities"
authors = []
license = "MIT"
readme = "README.md"
-repository = "https://github.com/ai-forever/gigachain"
-packages = [
- {include = "langchain_text_splitters"}
-]
+repository = "https://github.com/langchain-ai/langchain"
[tool.poetry.dependencies]
python = ">=3.8.1,<4.0"
-gigachain-core = "^0.2.0"
-lxml = {version = ">=4.9.3,<6.0", optional = true}
-beautifulsoup4 = {version = "^4.12.3", optional = true}
+gigachain-core = "^0.2.10"
[tool.poetry.group.lint]
optional = true
[tool.poetry.group.lint.dependencies]
ruff = "^0.1.5"
-gigachain-core = {path = "../core", develop = true}
+gigachain-core = { path = "../core", develop = true }
[tool.poetry.group.typing]
optional = true
@@ -40,7 +34,7 @@ optional = true
[tool.poetry.group.dev.dependencies]
jupyter = "^1.0.0"
-gigachain-core = {path = "../core", develop = true}
+gigachain-core = { path = "../core", develop = true }
[tool.poetry.group.test]
optional = true
@@ -51,26 +45,21 @@ optional = true
# Any dependencies that do not meet that criteria will be removed.
pytest = "^7.3.0"
freezegun = "^1.2.2"
-pytest-mock = "^3.10.0"
+pytest-mock = "^3.10.0"
pytest-watcher = "^0.3.4"
pytest-asyncio = "^0.21.1"
pytest-profiling = "^1.7.0"
-gigachain-core = {path = "../core", develop = true}
+gigachain-core = { path = "../core", develop = true }
+
[tool.poetry.group.test_integration]
-optional = true
dependencies = {}
-[tool.poetry.extras]
-extended_testing = [
- "lxml", "beautifulsoup4"
-]
-
[tool.ruff.lint]
select = [
- "E", # pycodestyle
- "F", # pyflakes
- "I", # isort
+ "E", # pycodestyle
+ "F", # pyflakes
+ "I", # isort
"T201", # print
]
@@ -78,11 +67,18 @@ select = [
disallow_untyped_defs = "True"
[[tool.mypy.overrides]]
-module = ["transformers", "sentence_transformers", "nltk.tokenize", "konlpy.tag", "bs4"]
+module = [
+ "transformers",
+ "sentence_transformers",
+ "nltk.tokenize",
+ "konlpy.tag",
+ "bs4",
+ "pytest",
+]
ignore_missing_imports = "True"
[tool.coverage.run]
-omit = ["tests/*", ]
+omit = ["tests/*"]
[build-system]
requires = ["poetry-core>=1.0.0"]
diff --git a/libs/text-splitters/tests/test_data/test_splitter.xslt b/libs/text-splitters/tests/test_data/test_splitter.xslt
new file mode 100644
index 0000000000000..cbb5828bf1242
--- /dev/null
+++ b/libs/text-splitters/tests/test_data/test_splitter.xslt
@@ -0,0 +1,9 @@
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/templates/anthropic-iterative-search/pyproject.toml b/templates/anthropic-iterative-search/pyproject.toml
index 99b3f3e60d1bb..7422e112bd371 100644
--- a/templates/anthropic-iterative-search/pyproject.toml
+++ b/templates/anthropic-iterative-search/pyproject.toml
@@ -1,4 +1,3 @@
-
[tool.poetry]
name = "anthropic-iterative-search"
version = "0.0.1"
@@ -15,6 +14,10 @@ langchain-anthropic = "^0.1.4"
[tool.poetry.group.dev.dependencies]
gigachain-cli = ">=0.0.21"
+[tool.gigaserve]
+export_module = "anthropic_iterative_search"
+export_attr = "chain"
+
[tool.templates-hub]
use-case = "research"
author = "LangChain"
diff --git a/templates/basic-critique-revise/pyproject.toml b/templates/basic-critique-revise/pyproject.toml
index 01cfd40116205..1ef1aa9bf3d62 100644
--- a/templates/basic-critique-revise/pyproject.toml
+++ b/templates/basic-critique-revise/pyproject.toml
@@ -7,7 +7,7 @@ readme = "README.md"
[tool.poetry.dependencies]
python = ">=3.8.1,<4.0"
-gigachain = ">=0.0.313, <0.1"
+gigachain = "^0.1"
openai = "<2"
[tool.poetry.group.dev.dependencies]
@@ -15,7 +15,7 @@ gigachain-cli = ">=0.0.21"
fastapi = ">=0.104.0,<1"
sse-starlette = "^1.6.5"
-[tool.langserve]
+[tool.gigaserve]
export_module = "basic_critique_revise"
export_attr = "chain"
diff --git a/templates/bedrock-jcvd/pyproject.toml b/templates/bedrock-jcvd/pyproject.toml
index 045fdfc3136c3..fea760fe593ff 100644
--- a/templates/bedrock-jcvd/pyproject.toml
+++ b/templates/bedrock-jcvd/pyproject.toml
@@ -6,14 +6,14 @@ authors = ["JGalego "]
readme = "README.md"
[tool.poetry.dependencies]
-python = ">=3.10, <4.0"
+python = "^3.11"
uvicorn = "^0.23.2"
-langserve = {extras = ["server"], version = ">=0.0.30"}
+gigaserve = {extras = ["server"], version = ">=0.0.30"}
pydantic = "<2"
boto3 = "^1.33.10"
gigachain = "^0.1"
-[tool.langserve]
+[tool.gigaserve]
export_module = "bedrock_jcvd.chain"
export_attr = "chain"
@@ -25,3 +25,7 @@ tags = ["conversation"]
[tool.poetry.group.dev.dependencies]
gigachain-cli = ">=0.0.21"
+
+[build-system]
+requires = ["poetry-core"]
+build-backend = "poetry.core.masonry.api"
diff --git a/templates/cassandra-entomology-rag/pyproject.toml b/templates/cassandra-entomology-rag/pyproject.toml
index 48f1fc4668a01..fda48cab4b2fa 100644
--- a/templates/cassandra-entomology-rag/pyproject.toml
+++ b/templates/cassandra-entomology-rag/pyproject.toml
@@ -1,4 +1,3 @@
-
[tool.poetry]
name = "cassandra-entomology-rag"
version = "0.0.1"
@@ -10,7 +9,7 @@ readme = "README.md"
[tool.poetry.dependencies]
python = ">=3.8.1,<4.0"
-gigachain = ">=0.0.325"
+gigachain = "^0.1"
openai = "<2"
tiktoken = "^0.5.1"
cassio = "^0.1.3"
@@ -18,6 +17,10 @@ cassio = "^0.1.3"
[tool.poetry.group.dev.dependencies]
gigachain-cli = ">=0.0.21"
+[tool.gigaserve]
+export_module = "cassandra_entomology_rag"
+export_attr = "chain"
+
[tool.templates-hub]
use-case = "rag"
author = "DataStax"
diff --git a/templates/cassandra-synonym-caching/pyproject.toml b/templates/cassandra-synonym-caching/pyproject.toml
index 5414859cb4ea8..a3a5f95635044 100644
--- a/templates/cassandra-synonym-caching/pyproject.toml
+++ b/templates/cassandra-synonym-caching/pyproject.toml
@@ -9,7 +9,7 @@ readme = "README.md"
[tool.poetry.dependencies]
python = ">=3.8.1,<4.0"
-gigachain = ">=0.0.325"
+gigachain = "^0.1"
openai = "<2"
tiktoken = "^0.5.1"
cassio = "^0.1.3"
@@ -17,6 +17,10 @@ cassio = "^0.1.3"
[tool.poetry.group.dev.dependencies]
gigachain-cli = ">=0.0.21"
+[tool.gigaserve]
+export_module = "cassandra_synonym_caching"
+export_attr = "chain"
+
[tool.templates-hub]
use-case = "rag"
author = "DataStax"
diff --git a/templates/chain-of-note-wiki/pyproject.toml b/templates/chain-of-note-wiki/pyproject.toml
index 360214cd570c6..05f53f0b32407 100644
--- a/templates/chain-of-note-wiki/pyproject.toml
+++ b/templates/chain-of-note-wiki/pyproject.toml
@@ -1,4 +1,3 @@
-
[tool.poetry]
name = "chain-of-note-wiki"
version = "0.0.1"
@@ -18,7 +17,7 @@ gigachain-cli = ">=0.0.21"
fastapi = ">=0.104.0,<1"
sse-starlette = "^1.6.5"
-[tool.langserve]
+[tool.gigaserve]
export_module = "chain_of_note_wiki"
export_attr = "chain"
diff --git a/templates/chat-bot-feedback/pyproject.toml b/templates/chat-bot-feedback/pyproject.toml
index 484966760663e..889cbb4a467bf 100644
--- a/templates/chat-bot-feedback/pyproject.toml
+++ b/templates/chat-bot-feedback/pyproject.toml
@@ -1,4 +1,3 @@
-
[tool.poetry]
name = "chat-bot-feedback"
version = "0.0.1"
@@ -8,7 +7,7 @@ readme = "README.md"
[tool.poetry.dependencies]
python = ">=3.8.1,<4.0"
-gigachain = ">=0.0.329"
+gigachain = "^0.1"
openai = "<2"
langsmith = ">=0.0.54"
langchainhub = ">=0.1.13"
@@ -18,7 +17,7 @@ gigachain-cli = ">=0.0.21"
fastapi = ">=0.104.0,<1"
sse-starlette = "^1.6.5"
-[tool.langserve]
+[tool.gigaserve]
export_module = "chat_bot_feedback.chain"
export_attr = "chain"
diff --git a/templates/cohere-librarian/pyproject.toml b/templates/cohere-librarian/pyproject.toml
index 6bdadb71dffde..ed7531d4f4983 100644
--- a/templates/cohere-librarian/pyproject.toml
+++ b/templates/cohere-librarian/pyproject.toml
@@ -7,7 +7,7 @@ readme = "README.md"
[tool.poetry.dependencies]
python = ">=3.8.1,<4.0"
-gigachain = ">=0.0.325"
+gigachain = "^0.1"
cohere = "^4.37"
chromadb = "^0.4.18"
@@ -16,7 +16,7 @@ gigachain-cli = ">=0.0.21"
fastapi = ">=0.104.0,<1"
sse-starlette = "^1.6.5"
-[tool.langserve]
+[tool.gigaserve]
export_module = "cohere_librarian"
export_attr = "chain"
diff --git a/templates/csv-agent/pyproject.toml b/templates/csv-agent/pyproject.toml
index b5b27b46be04d..1ba02bb2bc83a 100644
--- a/templates/csv-agent/pyproject.toml
+++ b/templates/csv-agent/pyproject.toml
@@ -7,7 +7,7 @@ readme = "README.md"
[tool.poetry.dependencies]
python = ">=3.9,<3.13"
-gigachain = ">=0.0.325"
+gigachain = "^0.1"
openai = "<2"
tiktoken = "^0.5.1"
faiss-cpu = "^1.7.4"
@@ -20,6 +20,10 @@ gigachain-experimental = ">=0.0.54"
[tool.poetry.group.dev.dependencies]
gigachain-cli = ">=0.0.21"
+[tool.gigaserve]
+export_module = "csv_agent"
+export_attr = "agent_executor"
+
[tool.templates-hub]
use-case = "question-answering"
author = "LangChain"
diff --git a/templates/docs/CONTRIBUTING.md b/templates/docs/CONTRIBUTING.md
new file mode 100644
index 0000000000000..3888df1d44ec9
--- /dev/null
+++ b/templates/docs/CONTRIBUTING.md
@@ -0,0 +1,43 @@
+# Contributing
+
+Thanks for taking the time to contribute a new template!
+We've tried to make this process as simple and painless as possible.
+If you need any help at all, please reach out!
+
+To contribute a new template, first fork this repository.
+Then clone that fork and pull it down locally.
+Set up an appropriate dev environment, and make sure you are in this `templates` directory.
+
+Make sure you have `langchain-cli` installed.
+
+```shell
+pip install -U langchain-cli
+```
+
+You can then run the following command to create a new skeleton of a package.
+By convention, package names should use `-` delimiters (not `_`).
+
+```shell
+langchain template new $PROJECT_NAME
+```
+
+You can then edit the contents of the package as you desire.
+Note that by default we expect the main chain to be exposed as `chain` in the `__init__.py` file of the package.
+You can change this (either the name or the location), but if you do so it is important to update the `tool.langchain`
+part of `pyproject.toml`.
+For example, if you update the main chain exposed to be called `agent_executor`, then that section should look like:
+
+```text
+[tool.langserve]
+export_module = "..."
+export_attr = "agent_executor"
+```
+
+Make sure to add any requirements of the package to `pyproject.toml` (and to remove any that are not used).
+
+Please update the `README.md` file to give some background on your package and how to set it up.
+
+If you want to change the license of your template for whatever, you may! Note that by default it is MIT licensed.
+
+If you want to test out your package at any point in time, you can spin up a LangServe instance directly from the package.
+See instructions [here](LAUNCHING_PACKAGE.md) on how to best do that.
diff --git a/templates/docs/INDEX.md b/templates/docs/INDEX.md
new file mode 100644
index 0000000000000..2a5294d74cc67
--- /dev/null
+++ b/templates/docs/INDEX.md
@@ -0,0 +1,80 @@
+# Templates
+
+Highlighting a few different categories of templates
+
+## ⭐ Popular
+
+These are some of the more popular templates to get started with.
+
+- [Retrieval Augmented Generation Chatbot](../rag-conversation): Build a chatbot over your data. Defaults to OpenAI and PineconeVectorStore.
+- [Extraction with OpenAI Functions](../extraction-openai-functions): Do extraction of structured data from unstructured data. Uses OpenAI function calling.
+- [Local Retrieval Augmented Generation](../rag-chroma-private): Build a chatbot over your data. Uses only local tooling: Ollama, GPT4all, Chroma.
+- [OpenAI Functions Agent](../openai-functions-agent): Build a chatbot that can take actions. Uses OpenAI function calling and Tavily.
+- [XML Agent](../xml-agent): Build a chatbot that can take actions. Uses Anthropic and You.com.
+
+
+## 📥 Advanced Retrieval
+
+These templates cover advanced retrieval techniques, which can be used for chat and QA over databases or documents.
+
+- [Reranking](../rag-pinecone-rerank): This retrieval technique uses Cohere's reranking endpoint to rerank documents from an initial retrieval step.
+- [Anthropic Iterative Search](../anthropic-iterative-search): This retrieval technique uses iterative prompting to determine what to retrieve and whether the retriever documents are good enough.
+- **Parent Document Retrieval** using [Neo4j](../neo4j-parent) or [MongoDB](../mongo-parent-document-retrieval): This retrieval technique stores embeddings for smaller chunks, but then returns larger chunks to pass to the model for generation.
+- [Semi-Structured RAG](../rag-semi-structured): The template shows how to do retrieval over semi-structured data (e.g. data that involves both text and tables).
+- [Temporal RAG](../rag-timescale-hybrid-search-time): The template shows how to do hybrid search over data with a time-based component using [Timescale Vector](https://www.timescale.com/ai?utm_campaign=vectorlaunch&utm_source=langchain&utm_medium=referral).
+
+## 🔍Advanced Retrieval - Query Transformation
+
+A selection of advanced retrieval methods that involve transforming the original user query, which can improve retrieval quality.
+
+- [Hypothetical Document Embeddings](../hyde): A retrieval technique that generates a hypothetical document for a given query, and then uses the embedding of that document to do semantic search. [Paper](https://arxiv.org/abs/2212.10496).
+- [Rewrite-Retrieve-Read](../rewrite-retrieve-read): A retrieval technique that rewrites a given query before passing it to a search engine. [Paper](https://arxiv.org/abs/2305.14283).
+- [Step-back QA Prompting](../stepback-qa-prompting): A retrieval technique that generates a "step-back" question and then retrieves documents relevant to both that question and the original question. [Paper](https://arxiv.org/abs//2310.06117).
+- [RAG-Fusion](../rag-fusion): A retrieval technique that generates multiple queries and then reranks the retrieved documents using reciprocal rank fusion. [Article](https://towardsdatascience.com/forget-rag-the-future-is-rag-fusion-1147298d8ad1).
+- [Multi-Query Retriever](../rag-pinecone-multi-query): This retrieval technique uses an LLM to generate multiple queries and then fetches documents for all queries.
+
+
+## 🧠Advanced Retrieval - Query Construction
+
+A selection of advanced retrieval methods that involve constructing a query in a separate DSL from natural language, which enable natural language chat over various structured databases.
+
+- [Elastic Query Generator](../elastic-query-generator): Generate elastic search queries from natural language.
+- [Neo4j Cypher Generation](../neo4j-cypher): Generate cypher statements from natural language. Available with a ["full text" option](../neo4j-cypher-ft) as well.
+- [Supabase Self Query](../self-query-supabase): Parse a natural language query into a semantic query as well as a metadata filter for Supabase.
+
+## 🦙 OSS Models
+
+These templates use OSS models, which enable privacy for sensitive data.
+
+- [Local Retrieval Augmented Generation](../rag-chroma-private): Build a chatbot over your data. Uses only local tooling: Ollama, GPT4all, Chroma.
+- [SQL Question Answering (Replicate)](../sql-llama2): Question answering over a SQL database, using Llama2 hosted on [Replicate](https://replicate.com/).
+- [SQL Question Answering (LlamaCpp)](../sql-llamacpp): Question answering over a SQL database, using Llama2 through [LlamaCpp](https://github.com/ggerganov/llama.cpp).
+- [SQL Question Answering (Ollama)](../sql-ollama): Question answering over a SQL database, using Llama2 through [Ollama](https://github.com/jmorganca/ollama).
+
+## ⛏️ Extraction
+
+These templates extract data in a structured format based upon a user-specified schema.
+
+- [Extraction Using OpenAI Functions](../extraction-openai-functions): Extract information from text using OpenAI Function Calling.
+- [Extraction Using Anthropic Functions](../extraction-anthropic-functions): Extract information from text using a LangChain wrapper around the Anthropic endpoints intended to simulate function calling.
+- [Extract BioTech Plate Data](../plate-chain): Extract microplate data from messy Excel spreadsheets into a more normalized format.
+
+## ⛏️Summarization and tagging
+
+These templates summarize or categorize documents and text.
+
+- [Summarization using Anthropic](../summarize-anthropic): Uses Anthropic's Claude2 to summarize long documents.
+
+## 🤖 Agents
+
+These templates build chatbots that can take actions, helping to automate tasks.
+
+- [OpenAI Functions Agent](../openai-functions-agent): Build a chatbot that can take actions. Uses OpenAI function calling and Tavily.
+- [XML Agent](../xml-agent): Build a chatbot that can take actions. Uses Anthropic and You.com.
+
+## :rotating_light: Safety and evaluation
+
+These templates enable moderation or evaluation of LLM outputs.
+
+- [Guardrails Output Parser](../guardrails-output-parser): Use guardrails-ai to validate LLM output.
+- [Chatbot Feedback](../chat-bot-feedback): Use LangSmith to evaluate chatbot responses.
diff --git a/templates/docs/LAUNCHING_PACKAGE.md b/templates/docs/LAUNCHING_PACKAGE.md
new file mode 100644
index 0000000000000..439a072052283
--- /dev/null
+++ b/templates/docs/LAUNCHING_PACKAGE.md
@@ -0,0 +1,41 @@
+# Launching LangServe from a Package
+
+You can also launch LangServe directly from a package, without having to pull it into a project.
+This can be useful when you are developing a package and want to test it quickly.
+The downside of this is that it gives you a little less control over how the LangServe APIs are configured,
+which is why for proper projects we recommend creating a full project.
+
+In order to do this, first change your working directory to the package itself.
+For example, if you are currently in this `templates` module, you can go into the `pirate-speak` package with:
+
+```shell
+cd pirate-speak
+```
+
+Inside this package there is a `pyproject.toml` file.
+This file contains a `tool.langchain` section that contains information on how this package should be used.
+For example, in `pirate-speak` we see:
+
+```text
+[tool.langserve]
+export_module = "pirate_speak.chain"
+export_attr = "chain"
+```
+
+This information can be used to launch a LangServe instance automatically.
+In order to do this, first make sure the CLI is installed:
+
+```shell
+pip install -U langchain-cli
+```
+
+You can then run:
+
+```shell
+langchain template serve
+```
+
+This will spin up endpoints, documentation, and playground for this chain.
+For example, you can access the playground at [http://127.0.0.1:8000/playground/](http://127.0.0.1:8000/playground/)
+
+![Screenshot of the LangServe Playground web interface with input and output fields.](playground.png "LangServe Playground Interface")
diff --git a/templates/docs/docs.png b/templates/docs/docs.png
new file mode 100644
index 0000000000000..3ad2fc8a6d12e
Binary files /dev/null and b/templates/docs/docs.png differ
diff --git a/templates/docs/playground.png b/templates/docs/playground.png
new file mode 100644
index 0000000000000..6ecc38a40b88b
Binary files /dev/null and b/templates/docs/playground.png differ
diff --git a/templates/elastic-query-generator/pyproject.toml b/templates/elastic-query-generator/pyproject.toml
index e13058bedcb23..adebb3a7b8f64 100644
--- a/templates/elastic-query-generator/pyproject.toml
+++ b/templates/elastic-query-generator/pyproject.toml
@@ -7,13 +7,17 @@ readme = "README.md"
[tool.poetry.dependencies]
python = ">=3.8.1,<4.0"
-gigachain = ">=0.0.325"
+gigachain = "^0.1"
elasticsearch = "^8.10.1"
openai = "<2"
[tool.poetry.group.dev.dependencies]
gigachain-cli = ">=0.0.21"
+[tool.gigaserve]
+export_module = "elastic_query_generator"
+export_attr = "chain"
+
[tool.templates-hub]
use-case = "query"
author = "LangChain"
diff --git a/templates/extraction-anthropic-functions/pyproject.toml b/templates/extraction-anthropic-functions/pyproject.toml
index 23b4548b1105e..59f9ad0b6b33f 100644
--- a/templates/extraction-anthropic-functions/pyproject.toml
+++ b/templates/extraction-anthropic-functions/pyproject.toml
@@ -9,7 +9,7 @@ readme = "README.md"
[tool.poetry.dependencies]
python = ">=3.8.1,<4.0"
-gigachain = ">=0.0.325"
+gigachain = "^0.1"
anthropic = ">=0.5.0"
langchainhub = ">=0.1.13"
gigachain-experimental = ">=0.0.54"
@@ -17,6 +17,10 @@ gigachain-experimental = ">=0.0.54"
[tool.poetry.group.dev.dependencies]
gigachain-cli = ">=0.0.21"
+[tool.gigaserve]
+export_module = "extraction_anthropic_functions"
+export_attr = "chain"
+
[tool.templates-hub]
use-case = "extraction"
author = "LangChain"
diff --git a/templates/extraction-openai-functions/pyproject.toml b/templates/extraction-openai-functions/pyproject.toml
index 914df5cc07ca8..71d2c8a5b2345 100644
--- a/templates/extraction-openai-functions/pyproject.toml
+++ b/templates/extraction-openai-functions/pyproject.toml
@@ -9,12 +9,16 @@ readme = "README.md"
[tool.poetry.dependencies]
python = ">=3.8.1,<4.0"
-gigachain = ">=0.0.325"
+gigachain = "^0.1"
openai = "<2"
[tool.poetry.group.dev.dependencies]
gigachain-cli = ">=0.0.21"
+[tool.gigaserve]
+export_module = "extraction_openai_functions"
+export_attr = "chain"
+
[tool.templates-hub]
use-case = "extraction"
author = "LangChain"
diff --git a/templates/gemini-functions-agent/pyproject.toml b/templates/gemini-functions-agent/pyproject.toml
index 3e9e44db506ad..4d2bd2c546318 100644
--- a/templates/gemini-functions-agent/pyproject.toml
+++ b/templates/gemini-functions-agent/pyproject.toml
@@ -7,14 +7,14 @@ readme = "README.md"
[tool.poetry.dependencies]
python = ">=3.9,<4.0"
-langchain = "^0.1"
+gigachain = "^0.1"
tavily-python = "^0.1.9"
-langchain-google-genai = ">=0.0.8,<0.1"
+gigachain-google-genai = ">=0.0.8,<0.1"
[tool.poetry.group.dev.dependencies]
gigachain-cli = ">=0.0.21"
-[tool.langserve]
+[tool.gigaserve]
export_module = "gemini_functions_agent"
export_attr = "agent_executor"
diff --git a/templates/guardrails-output-parser/pyproject.toml b/templates/guardrails-output-parser/pyproject.toml
index b78a93338692e..86f0f8dcfc113 100644
--- a/templates/guardrails-output-parser/pyproject.toml
+++ b/templates/guardrails-output-parser/pyproject.toml
@@ -7,7 +7,7 @@ readme = "README.md"
[tool.poetry.dependencies]
python = ">=3.9,<3.13"
-gigachain = ">=0.0.325"
+gigachain = "^0.1"
openai = "<2"
guardrails-ai = "^0.2.4"
alt-profanity-check = "^1.3.1"
@@ -17,7 +17,7 @@ gigachain-cli = ">=0.0.21"
fastapi = ">=0.104.0,<1"
sse-starlette = "^1.6.5"
-[tool.langserve]
+[tool.gigaserve]
export_module = "guardrails_output_parser.chain"
export_attr = "chain"
diff --git a/templates/hybrid-search-weaviate/pyproject.toml b/templates/hybrid-search-weaviate/pyproject.toml
index b6e93364e60dd..8fcc78eb38928 100644
--- a/templates/hybrid-search-weaviate/pyproject.toml
+++ b/templates/hybrid-search-weaviate/pyproject.toml
@@ -1,5 +1,3 @@
-
-
[tool.poetry]
name = "hybrid-search-weaviate"
version = "0.1.0"
@@ -9,13 +7,18 @@ readme = "README.md"
[tool.poetry.dependencies]
python = ">=3.8.1,<4.0"
-gigachain = ">=0.0.325"
+gigachain = "^0.1"
openai = "<2"
tiktoken = "^0.5.1"
weaviate-client = ">=3.24.2"
[tool.poetry.group.dev.dependencies]
gigachain-cli = ">=0.0.21"
+[tool.poetry.group.dev.dependencies.python-dotenv]
+extras = [
+ "cli",
+]
+version = "^1.0.0"
[tool.gigaserve]
export_module = "hybrid_search_weaviate"
diff --git a/templates/hyde/pyproject.toml b/templates/hyde/pyproject.toml
index 72486e6514493..597ef3cce4759 100644
--- a/templates/hyde/pyproject.toml
+++ b/templates/hyde/pyproject.toml
@@ -7,11 +7,11 @@ readme = "README.md"
[tool.poetry.dependencies]
python = ">=3.8.1,<4.0"
-gigachain = ">=0.0.325"
+gigachain = "^0.1"
openai = "<2"
chromadb = "^0.4.15"
tiktoken = "^0.5.1"
-langchain-text-splitters = ">=0.0.1,<0.1"
+gigachain-text-splitters = ">=0.0.1,<0.1"
[tool.poetry.group.dev.dependencies]
poethepoet = "^0.24.1"
@@ -30,7 +30,7 @@ integrations = ["OpenAI", "ChromaDB"]
tags = ["paper"]
[tool.poe.tasks.start]
-cmd = "uvicorn gigachain_cli.dev_scripts:create_demo_server --reload --port $port --host $host"
+cmd = "uvicorn langchain_cli.dev_scripts:create_demo_server --reload --port $port --host $host"
args = [
{ name = "port", help = "port to run on", default = "8000" },
{ name = "host", help = "host to run on", default = "127.0.0.1" },
diff --git a/templates/intel-rag-xeon/pyproject.toml b/templates/intel-rag-xeon/pyproject.toml
index b5ebb5b98132a..d96e00e90dd6a 100644
--- a/templates/intel-rag-xeon/pyproject.toml
+++ b/templates/intel-rag-xeon/pyproject.toml
@@ -9,7 +9,7 @@ readme = "README.md"
[tool.poetry.dependencies]
python = ">=3.9,<3.13"
-langchain = "^0.1"
+gigachain = "^0.1"
fastapi = "^0.104.0"
sse-starlette = "^1.6.5"
sentence-transformers = "2.2.2"
@@ -27,7 +27,7 @@ extras = [
poethepoet = "^0.24.1"
gigachain-cli = ">=0.0.21"
-[tool.langserve]
+[tool.gigaserve]
export_module = "intel_rag_xeon.chain"
export_attr = "chain"
diff --git a/templates/llama2-functions/pyproject.toml b/templates/llama2-functions/pyproject.toml
index abd9537421722..74b936ccb96b0 100644
--- a/templates/llama2-functions/pyproject.toml
+++ b/templates/llama2-functions/pyproject.toml
@@ -9,7 +9,7 @@ readme = "README.md"
[tool.poetry.dependencies]
python = ">=3.8.1,<4.0"
-gigachain = ">=0.0.325"
+gigachain = "^0.1"
replicate = ">=0.15.4"
[tool.poetry.group.dev.dependencies]
diff --git a/templates/mongo-parent-document-retrieval/pyproject.toml b/templates/mongo-parent-document-retrieval/pyproject.toml
index ec8e03e457274..639a607713a06 100644
--- a/templates/mongo-parent-document-retrieval/pyproject.toml
+++ b/templates/mongo-parent-document-retrieval/pyproject.toml
@@ -7,12 +7,12 @@ readme = "README.md"
[tool.poetry.dependencies]
python = ">=3.8.1,<4.0"
-gigachain = ">=0.0.313, <0.1"
+gigachain = "^0.1"
openai = "<2"
pymongo = "^4.6.0"
pypdf = "^3.17.0"
tiktoken = "^0.5.1"
-langchain-text-splitters = ">=0.0.1,<0.1"
+gigachain-text-splitters = ">=0.0.1,<0.1"
[tool.poetry.group.dev.dependencies]
gigachain-cli = ">=0.0.21"
diff --git a/templates/neo4j-advanced-rag/pyproject.toml b/templates/neo4j-advanced-rag/pyproject.toml
index 0ac8fb76e9b7c..2c796a1271081 100644
--- a/templates/neo4j-advanced-rag/pyproject.toml
+++ b/templates/neo4j-advanced-rag/pyproject.toml
@@ -9,17 +9,17 @@ readme = "README.md"
[tool.poetry.dependencies]
python = ">=3.8.1,<4.0"
-gigachain = ">=0.0.325"
+gigachain = "^0.1"
tiktoken = "^0.5.1"
openai = "<2"
neo4j = "^5.14.0"
-langchain-text-splitters = ">=0.0.1,<0.1"
-langchain-openai = "^0.1.1"
+gigachain-text-splitters = ">=0.0.1,<0.1"
+gigachain-openai = "^0.1.1"
[tool.poetry.group.dev.dependencies]
gigachain-cli = ">=0.0.21"
-[tool.langserve]
+[tool.gigaserve]
export_module = "neo4j_advanced_rag"
export_attr = "chain"
diff --git a/templates/neo4j-cypher-ft/pyproject.toml b/templates/neo4j-cypher-ft/pyproject.toml
index f5d6d1fcc331f..d43f819009df5 100644
--- a/templates/neo4j-cypher-ft/pyproject.toml
+++ b/templates/neo4j-cypher-ft/pyproject.toml
@@ -9,11 +9,11 @@ readme = "README.md"
[tool.poetry.dependencies]
python = ">=3.8.1,<4.0"
-gigachain = ">=0.0.325"
+gigachain = "^0.1"
neo4j = ">5.12"
openai = "<2"
-langchain-community = "^0.0.33"
-langchain-openai = "^0.1.3"
+gigachain-community = "^0.0.33"
+gigachain-openai = "^0.1.3"
[tool.poetry.group.dev.dependencies]
gigachain-cli = ">=0.0.21"
diff --git a/templates/neo4j-cypher-memory/pyproject.toml b/templates/neo4j-cypher-memory/pyproject.toml
index 6f4463a205e87..45741cba7780d 100644
--- a/templates/neo4j-cypher-memory/pyproject.toml
+++ b/templates/neo4j-cypher-memory/pyproject.toml
@@ -9,11 +9,11 @@ readme = "README.md"
[tool.poetry.dependencies]
python = ">=3.8.1,<4.0"
-gigachain = ">=0.0.325"
+gigachain = "^0.1"
neo4j = ">5.12"
openai = "<2"
-langchain-community = "^0.0.33"
-langchain-openai = "^0.1.3"
+gigachain-community = "^0.0.33"
+gigachain-openai = "^0.1.3"
[tool.poetry.group.dev.dependencies]
gigachain-cli = ">=0.0.21"
diff --git a/templates/neo4j-cypher/pyproject.toml b/templates/neo4j-cypher/pyproject.toml
index 51869133e1064..0f4d6856a91d8 100644
--- a/templates/neo4j-cypher/pyproject.toml
+++ b/templates/neo4j-cypher/pyproject.toml
@@ -9,11 +9,11 @@ readme = "README.md"
[tool.poetry.dependencies]
python = ">=3.8.1,<4.0"
-gigachain = ">=0.0.325"
+gigachain = "^0.1"
neo4j = ">5.12"
openai = "<2"
-langchain-openai = "^0.1.3"
-langchain-community = "^0.0.33"
+gigachain-openai = "^0.1.3"
+gigachain-community = "^0.0.33"
[tool.poetry.group.dev.dependencies]
gigachain-cli = ">=0.0.21"
diff --git a/templates/neo4j-generation/pyproject.toml b/templates/neo4j-generation/pyproject.toml
index 1cfc7ef242bc1..107330cb721f1 100644
--- a/templates/neo4j-generation/pyproject.toml
+++ b/templates/neo4j-generation/pyproject.toml
@@ -9,12 +9,12 @@ readme = "README.md"
[tool.poetry.dependencies]
python = ">=3.8.1,<4.0"
-gigachain = ">=0.0.325"
+gigachain = "^0.1"
openai = "<2"
neo4j = "^5.12.0"
gigachain-openai = "^0.0.8"
-langchain-community = "^0.0.28"
-langchain-experimental = "^0.0.54"
+gigachain-community = "^0.0.28"
+gigachain-experimental = "^0.0.54"
[tool.poetry.group.dev.dependencies]
gigachain-cli = ">=0.0.21"
diff --git a/templates/neo4j-parent/pyproject.toml b/templates/neo4j-parent/pyproject.toml
index efd7d51fff705..c862ddfca5097 100644
--- a/templates/neo4j-parent/pyproject.toml
+++ b/templates/neo4j-parent/pyproject.toml
@@ -9,12 +9,12 @@ readme = "README.md"
[tool.poetry.dependencies]
python = ">=3.8.1,<4.0"
-gigachain = ">=0.0.325"
+gigachain = "^0.1"
tiktoken = "^0.5.1"
openai = "<2"
neo4j = "^5.14.0"
-langchain-text-splitters = ">=0.0.1,<0.1"
-langchain-openai = "^0.1.1"
+gigachain-text-splitters = ">=0.0.1,<0.1"
+gigachain-openai = "^0.1.1"
[tool.poetry.group.dev.dependencies]
gigachain-cli = ">=0.0.21"
diff --git a/templates/neo4j-semantic-layer/pyproject.toml b/templates/neo4j-semantic-layer/pyproject.toml
index 6a9768a8ba952..b99456442c1df 100644
--- a/templates/neo4j-semantic-layer/pyproject.toml
+++ b/templates/neo4j-semantic-layer/pyproject.toml
@@ -16,7 +16,7 @@ neo4j = "^5.14.0"
[tool.poetry.group.dev.dependencies]
gigachain-cli = ">=0.0.21"
-[tool.langserve]
+[tool.gigaserve]
export_module = "neo4j_semantic_layer"
export_attr = "agent_executor"
diff --git a/templates/neo4j-semantic-ollama/pyproject.toml b/templates/neo4j-semantic-ollama/pyproject.toml
index 64e5c585f5db7..d800f7220ec08 100644
--- a/templates/neo4j-semantic-ollama/pyproject.toml
+++ b/templates/neo4j-semantic-ollama/pyproject.toml
@@ -9,14 +9,14 @@ readme = "README.md"
[tool.poetry.dependencies]
python = ">=3.8.1,<4.0"
-langchain = "^0.1"
+gigachain = "^0.1"
openai = "<2"
neo4j = "^5.14.0"
[tool.poetry.group.dev.dependencies]
gigachain-cli = ">=0.0.21"
-[tool.langserve]
+[tool.gigaserve]
export_module = "neo4j_semantic_ollama"
export_attr = "agent_executor"
diff --git a/templates/neo4j-vector-memory/pyproject.toml b/templates/neo4j-vector-memory/pyproject.toml
index 8ce44caa70e70..4209f05d5a90d 100644
--- a/templates/neo4j-vector-memory/pyproject.toml
+++ b/templates/neo4j-vector-memory/pyproject.toml
@@ -13,8 +13,8 @@ gigachain = "^0.1"
tiktoken = "^0.5.1"
openai = "<2"
neo4j = "^5.14.0"
-langchain-text-splitters = ">=0.0.1,<0.1"
-langchain-openai = "^0.1.1"
+gigachain-text-splitters = ">=0.0.1,<0.1"
+gigachain-openai = "^0.1.1"
[tool.poetry.group.dev.dependencies]
gigachain-cli = ">=0.0.21"
diff --git a/templates/nvidia-rag-canonical/pyproject.toml b/templates/nvidia-rag-canonical/pyproject.toml
index c81b8444658a3..29f4413ffc607 100644
--- a/templates/nvidia-rag-canonical/pyproject.toml
+++ b/templates/nvidia-rag-canonical/pyproject.toml
@@ -9,14 +9,14 @@ readme = "README.md"
python = ">=3.8.1,<4.0"
gigachain = "^0.1"
pymilvus = ">=2.3.0"
-langchain-nvidia-aiplay = "^0.0.2"
+gigachain-nvidia-aiplay = "^0.0.2"
pypdf = ">=3.1"
-langchain-text-splitters = ">=0.0.1,<0.1"
+gigachain-text-splitters = ">=0.0.1,<0.1"
[tool.poetry.group.dev.dependencies]
gigachain-cli = ">=0.0.21"
-[tool.langserve]
+[tool.gigaserve]
export_module = "nvidia_rag_canonical"
export_attr = "chain"
diff --git a/templates/openai-functions-agent-gmail/pyproject.toml b/templates/openai-functions-agent-gmail/pyproject.toml
index f45ec10f1da8c..e27c1341a7ee7 100644
--- a/templates/openai-functions-agent-gmail/pyproject.toml
+++ b/templates/openai-functions-agent-gmail/pyproject.toml
@@ -21,7 +21,7 @@ bs4 = "^0.0.1"
[tool.poetry.group.dev.dependencies]
gigachain-cli = ">=0.0.21"
-[tool.langserve]
+[tool.gigaserve]
export_module = "openai_functions_agent"
export_attr = "agent_executor"
diff --git a/templates/propositional-retrieval/pyproject.toml b/templates/propositional-retrieval/pyproject.toml
index 38a4d21aa577c..522fc40b6acd6 100644
--- a/templates/propositional-retrieval/pyproject.toml
+++ b/templates/propositional-retrieval/pyproject.toml
@@ -14,12 +14,12 @@ openai = "<2"
tiktoken = ">=0.5.1"
chromadb = ">=0.4.14"
bs4 = "^0.0.1"
-langchain-text-splitters = ">=0.0.1,<0.1"
+gigachain-text-splitters = ">=0.0.1,<0.1"
[tool.poetry.group.dev.dependencies]
gigachain-cli = ">=0.0.21"
-[tool.langserve]
+[tool.gigaserve]
export_module = "rag_chroma_multi_modal_multi_vector"
export_attr = "chain"
diff --git a/templates/pyproject.toml b/templates/pyproject.toml
index f27dcb9b81d11..9be07beac1f7f 100644
--- a/templates/pyproject.toml
+++ b/templates/pyproject.toml
@@ -53,7 +53,7 @@ watch = "poetry run ptw"
lint = ["_lint", "_check_formatting"]
format = ["_lint_fix", "_format"]
-_check_formatting = "poetry run ruff format . --check"
+_check_formatting = "poetry run ruff format . --diff"
_lint = "poetry run ruff ."
_format = "poetry run ruff format ."
_lint_fix = "poetry run ruff . --fix"
diff --git a/templates/python-lint/pyproject.toml b/templates/python-lint/pyproject.toml
index f4e896780b146..425a6ecd24ae5 100644
--- a/templates/python-lint/pyproject.toml
+++ b/templates/python-lint/pyproject.toml
@@ -18,7 +18,7 @@ gigachain-cli = ">=0.0.21"
fastapi = ">=0.104.0,<1"
sse-starlette = "^1.6.5"
-[tool.langserve]
+[tool.gigaserve]
export_module = "python_lint"
export_attr = "agent_executor"
diff --git a/templates/rag-aws-bedrock/pyproject.toml b/templates/rag-aws-bedrock/pyproject.toml
index 4bec541988454..9c853e7e1a0a9 100644
--- a/templates/rag-aws-bedrock/pyproject.toml
+++ b/templates/rag-aws-bedrock/pyproject.toml
@@ -9,7 +9,7 @@ readme = "README.md"
[tool.poetry.dependencies]
python = ">=3.8.1,<4.0"
-gigachain = ">=0.0.325"
+gigachain = "^0.1"
tiktoken = ">=0.5.1"
faiss-cpu = ">=1.7.4"
boto3 = ">=1.28.57"
diff --git a/templates/rag-aws-kendra/pyproject.toml b/templates/rag-aws-kendra/pyproject.toml
index cf6bc4426f49d..d15007b766c23 100644
--- a/templates/rag-aws-kendra/pyproject.toml
+++ b/templates/rag-aws-kendra/pyproject.toml
@@ -7,7 +7,7 @@ readme = "README.md"
[tool.poetry.dependencies]
python = ">=3.8.1,<4.0"
-gigachain = ">=0.0.325"
+gigachain = "^0.1"
tiktoken = ">=0.5.1"
boto3 = ">=1.28.57"
awscli = ">=1.29.57"
diff --git a/templates/rag-azure-search/pyproject.toml b/templates/rag-azure-search/pyproject.toml
index 2de133d3eedff..82328b02d48a2 100644
--- a/templates/rag-azure-search/pyproject.toml
+++ b/templates/rag-azure-search/pyproject.toml
@@ -7,8 +7,8 @@ readme = "README.md"
[tool.poetry.dependencies]
python = ">=3.8.1,<4.0"
-langchain-core = ">=0.1.5"
-langchain-openai = ">=0.0.1"
+gigachain-core = ">=0.1.5"
+gigachain-openai = ">=0.0.1"
azure-search-documents = ">=11.4.0"
[tool.poetry.group.dev.dependencies]
@@ -16,7 +16,7 @@ gigachain-cli = ">=0.0.4"
fastapi = "^0.104.0"
sse-starlette = "^1.6.5"
-[tool.langserve]
+[tool.gigaserve]
export_module = "rag_azure_search"
export_attr = "chain"
diff --git a/templates/rag-chroma-multi-modal-multi-vector/pyproject.toml b/templates/rag-chroma-multi-modal-multi-vector/pyproject.toml
index eb119168c575c..f26025812a041 100644
--- a/templates/rag-chroma-multi-modal-multi-vector/pyproject.toml
+++ b/templates/rag-chroma-multi-modal-multi-vector/pyproject.toml
@@ -9,7 +9,7 @@ readme = "README.md"
[tool.poetry.dependencies]
python = ">=3.8.1,<4.0"
-gigachain = "^0.1"
+gigachain = ">=0.0.353,<0.2"
openai = "<2"
tiktoken = ">=0.5.1"
chromadb = ">=0.4.14"
@@ -21,7 +21,7 @@ pillow = ">=10.1.0"
[tool.poetry.group.dev.dependencies]
gigachain-cli = ">=0.0.21"
-[tool.langserve]
+[tool.gigaserve]
export_module = "rag_chroma_multi_modal_multi_vector"
export_attr = "chain"
diff --git a/templates/rag-chroma-multi-modal/pyproject.toml b/templates/rag-chroma-multi-modal/pyproject.toml
index 69bacb4941000..7c6f144cc53fc 100644
--- a/templates/rag-chroma-multi-modal/pyproject.toml
+++ b/templates/rag-chroma-multi-modal/pyproject.toml
@@ -9,7 +9,7 @@ readme = "README.md"
[tool.poetry.dependencies]
python = ">=3.8.1,<4.0"
-gigachain = ">=0.0.325"
+gigachain = ">=0.0.353,<0.2"
openai = "<2"
tiktoken = ">=0.5.1"
chromadb = ">=0.4.14"
@@ -21,7 +21,7 @@ gigachain-experimental = ">=0.0.43"
[tool.poetry.group.dev.dependencies]
gigachain-cli = ">=0.0.21"
-[tool.langserve]
+[tool.gigaserve]
export_module = "rag_chroma_multi_modal"
export_attr = "chain"
diff --git a/templates/rag-chroma-private/pyproject.toml b/templates/rag-chroma-private/pyproject.toml
index 15c3bf92584a8..c8dbe02aeee84 100644
--- a/templates/rag-chroma-private/pyproject.toml
+++ b/templates/rag-chroma-private/pyproject.toml
@@ -9,12 +9,12 @@ readme = "README.md"
[tool.poetry.dependencies]
python = ">=3.8.1,<4.0"
-gigachain = ">=0.0.325"
+gigachain = "^0.1"
tiktoken = ">=0.5.1"
chromadb = ">=0.4.14"
gpt4all = ">=1.0.8"
beautifulsoup4 = ">=4.12.2"
-langchain-text-splitters = ">=0.0.1,<0.1"
+gigachain-text-splitters = ">=0.0.1,<0.1"
[tool.poetry.group.dev.dependencies]
gigachain-cli = ">=0.0.21"
diff --git a/templates/rag-chroma/pyproject.toml b/templates/rag-chroma/pyproject.toml
index c74d10772e884..805ecab656a97 100644
--- a/templates/rag-chroma/pyproject.toml
+++ b/templates/rag-chroma/pyproject.toml
@@ -9,11 +9,11 @@ readme = "README.md"
[tool.poetry.dependencies]
python = ">=3.8.1,<4.0"
-gigachain = ">=0.0.325"
+gigachain = "^0.1"
openai = "<2"
tiktoken = ">=0.5.1"
chromadb = ">=0.4.14"
-langchain-text-splitters = ">=0.0.1,<0.1"
+gigachain-text-splitters = ">=0.0.1,<0.1"
[tool.poetry.group.dev.dependencies]
gigachain-cli = ">=0.0.21"
diff --git a/templates/rag-codellama-fireworks/pyproject.toml b/templates/rag-codellama-fireworks/pyproject.toml
index 4949c8c0dcfa1..457e77c1ab8cd 100644
--- a/templates/rag-codellama-fireworks/pyproject.toml
+++ b/templates/rag-codellama-fireworks/pyproject.toml
@@ -9,12 +9,12 @@ readme = "README.md"
[tool.poetry.dependencies]
python = ">=3.9,<4.0"
-gigachain = ">=0.0.325"
+gigachain = "^0.1"
gpt4all = ">=1.0.8"
tiktoken = ">=0.5.1"
chromadb = ">=0.4.14"
fireworks-ai = ">=0.6.0"
-langchain-text-splitters = ">=0.0.1,<0.1"
+gigachain-text-splitters = ">=0.0.1,<0.1"
[tool.poetry.group.dev.dependencies]
gigachain-cli = ">=0.0.21"
diff --git a/templates/rag-conversation-zep/pyproject.toml b/templates/rag-conversation-zep/pyproject.toml
index 4561824a504f5..65f298094271a 100644
--- a/templates/rag-conversation-zep/pyproject.toml
+++ b/templates/rag-conversation-zep/pyproject.toml
@@ -7,13 +7,13 @@ readme = "README.md"
[tool.poetry.dependencies]
python = ">=3.8.1,<4.0"
-gigachain = ">=0.0.313, <0.1"
+gigachain = "^0.1"
openai = "<2"
zep-python = "^1.4.0"
tiktoken = "^0.5.1"
beautifulsoup4 = "^4.12.2"
bs4 = "^0.0.1"
-langchain-text-splitters = ">=0.0.1,<0.1"
+gigachain-text-splitters = ">=0.0.1,<0.1"
[tool.poetry.group.dev.dependencies]
gigachain-cli = ">=0.0.21"
diff --git a/templates/rag-conversation/pyproject.toml b/templates/rag-conversation/pyproject.toml
index d3ec19f977514..06a0659631ec6 100644
--- a/templates/rag-conversation/pyproject.toml
+++ b/templates/rag-conversation/pyproject.toml
@@ -14,7 +14,7 @@ openai = "<2"
tiktoken = ">=0.5.1"
pinecone-client = ">=2.2.4"
beautifulsoup4 = "^4.12.2"
-langchain-text-splitters = ">=0.0.1,<0.1"
+gigachain-text-splitters = ">=0.0.1,<0.1"
[tool.poetry.group.dev.dependencies]
gigachain-cli = ">=0.0.21"
diff --git a/templates/rag-fusion/pyproject.toml b/templates/rag-fusion/pyproject.toml
index 3ea8f5248ca04..1fcfeb3b3d635 100644
--- a/templates/rag-fusion/pyproject.toml
+++ b/templates/rag-fusion/pyproject.toml
@@ -7,7 +7,7 @@ readme = "README.md"
[tool.poetry.dependencies]
python = ">=3.8.1,<4.0"
-gigachain = ">=0.0.325"
+gigachain = "^0.1"
openai = "<2"
pinecone-client = "^2.2.4"
langchainhub = "^0.1.13"
diff --git a/templates/rag-gemini-multi-modal/pyproject.toml b/templates/rag-gemini-multi-modal/pyproject.toml
index d30ae39055b41..953cb1b0b93a5 100644
--- a/templates/rag-gemini-multi-modal/pyproject.toml
+++ b/templates/rag-gemini-multi-modal/pyproject.toml
@@ -9,7 +9,7 @@ readme = "README.md"
[tool.poetry.dependencies]
python = ">=3.9,<4.0"
-gigachain = ">=0.0.350"
+gigachain = ">=0.0.353,<0.2"
openai = "<2"
tiktoken = ">=0.5.1"
chromadb = ">=0.4.14"
@@ -17,12 +17,12 @@ open-clip-torch = ">=2.23.0"
torch = ">=2.1.0"
pypdfium2 = ">=4.20.0"
gigachain-experimental = ">=0.0.43"
-langchain-google-genai = ">=0.0.1"
+gigachain-google-genai = ">=0.0.1"
[tool.poetry.group.dev.dependencies]
gigachain-cli = ">=0.0.21"
-[tool.langserve]
+[tool.gigaserve]
export_module = "rag_gemini_multi_modal"
export_attr = "chain"
diff --git a/templates/rag-google-cloud-sensitive-data-protection/pyproject.toml b/templates/rag-google-cloud-sensitive-data-protection/pyproject.toml
index 83d514bd338ae..528f5f1e6703f 100644
--- a/templates/rag-google-cloud-sensitive-data-protection/pyproject.toml
+++ b/templates/rag-google-cloud-sensitive-data-protection/pyproject.toml
@@ -7,7 +7,7 @@ readme = "README.md"
[tool.poetry.dependencies]
python = ">=3.8.1,<4.0"
-gigachain = ">=0.0.333"
+gigachain = "^0.1"
google-cloud-aiplatform = ">=1.35.0"
google-cloud-dlp = "^3.13.0"
@@ -17,7 +17,7 @@ gigachain-cli = ">=0.0.21"
fastapi = ">=0.104.0,<1"
sse-starlette = "^1.6.5"
-[tool.langserve]
+[tool.gigaserve]
export_module = "rag_google_cloud_sensitive_data_protection"
export_attr = "chain"
diff --git a/templates/rag-google-cloud-vertexai-search/pyproject.toml b/templates/rag-google-cloud-vertexai-search/pyproject.toml
index 73e2ba223723b..f1b2a96c9b149 100644
--- a/templates/rag-google-cloud-vertexai-search/pyproject.toml
+++ b/templates/rag-google-cloud-vertexai-search/pyproject.toml
@@ -7,7 +7,7 @@ readme = "README.md"
[tool.poetry.dependencies]
python = ">=3.8.1,<4.0"
-gigachain = ">=0.0.333"
+gigachain = "^0.1"
google-cloud-aiplatform = ">=1.35.0"
diff --git a/templates/rag-gpt-crawler/pyproject.toml b/templates/rag-gpt-crawler/pyproject.toml
index 86e846e5d64f7..29663fa05d3f6 100644
--- a/templates/rag-gpt-crawler/pyproject.toml
+++ b/templates/rag-gpt-crawler/pyproject.toml
@@ -9,16 +9,16 @@ readme = "README.md"
[tool.poetry.dependencies]
python = ">=3.8.1,<4.0"
-gigachain = ">=0.0.325"
+gigachain = "^0.1"
openai = "<2"
tiktoken = ">=0.5.1"
chromadb = ">=0.4.14"
-langchain-text-splitters = ">=0.0.1,<0.1"
+gigachain-text-splitters = ">=0.0.1,<0.1"
[tool.poetry.group.dev.dependencies]
gigachain-cli = ">=0.0.21"
-[tool.langserve]
+[tool.gigaserve]
export_module = "rag_gpt_crawler"
export_attr = "chain"
diff --git a/templates/rag-jaguardb/pyproject.toml b/templates/rag-jaguardb/pyproject.toml
index 095b41d44b99b..1f957fdde6c7d 100644
--- a/templates/rag-jaguardb/pyproject.toml
+++ b/templates/rag-jaguardb/pyproject.toml
@@ -9,7 +9,7 @@ readme = "README.md"
[tool.poetry.dependencies]
python = ">=3.8.1,<4.0"
-langchain = "^0.1"
+gigachain = "^0.1"
openai = "<2"
tiktoken = ">=0.5.1"
jaguar = ">=3.4"
@@ -17,7 +17,7 @@ jaguar = ">=3.4"
[tool.poetry.group.dev.dependencies]
gigachain-cli = ">=0.0.15"
-[tool.langserve]
+[tool.gigaserve]
export_module = "rag_jaguardb"
export_attr = "chain"
diff --git a/templates/rag-lancedb/pyproject.toml b/templates/rag-lancedb/pyproject.toml
index 889c9bd789d72..aa78f631694d9 100644
--- a/templates/rag-lancedb/pyproject.toml
+++ b/templates/rag-lancedb/pyproject.toml
@@ -19,7 +19,7 @@ gigachain-cli = ">=0.0.21"
fastapi = ">=0.104.0,<1"
sse-starlette = "^1.6.5"
-[tool.langserve]
+[tool.gigaserve]
export_module = "rag_lancedb"
export_attr = "chain"
diff --git a/templates/rag-lantern/pyproject.toml b/templates/rag-lantern/pyproject.toml
index 4c2a8757bbae7..ff92d33c278db 100644
--- a/templates/rag-lantern/pyproject.toml
+++ b/templates/rag-lantern/pyproject.toml
@@ -9,7 +9,7 @@ readme = "README.md"
[tool.poetry.dependencies]
python = ">=3.8.1,<4.0"
-langchain = "^0.1"
+gigachain = "^0.1"
openai = "<2"
tiktoken = "^0.5.1"
rag-lantern = {path = "packages/rag-lantern", develop = true}
@@ -22,7 +22,7 @@ extras = [
]
version = "^1.0.0"
-[tool.langserve]
+[tool.gigaserve]
export_module = "rag_lantern.chain"
export_attr = "chain"
diff --git a/templates/rag-matching-engine/pyproject.toml b/templates/rag-matching-engine/pyproject.toml
index bff39de0eac74..7c23a7e907912 100644
--- a/templates/rag-matching-engine/pyproject.toml
+++ b/templates/rag-matching-engine/pyproject.toml
@@ -7,7 +7,7 @@ readme = "README.md"
[tool.poetry.dependencies]
python = ">=3.8.1,<4.0"
-gigachain = ">=0.0.325"
+gigachain = "^0.1"
google-cloud-aiplatform = "^1.35.0"
[tool.poetry.group.dev.dependencies]
diff --git a/templates/rag-milvus/.gitignore b/templates/rag-milvus/.gitignore
new file mode 100644
index 0000000000000..bee8a64b79a99
--- /dev/null
+++ b/templates/rag-milvus/.gitignore
@@ -0,0 +1 @@
+__pycache__
diff --git a/templates/rag-milvus/LICENSE b/templates/rag-milvus/LICENSE
new file mode 100644
index 0000000000000..fc0602feecdd6
--- /dev/null
+++ b/templates/rag-milvus/LICENSE
@@ -0,0 +1,21 @@
+MIT License
+
+Copyright (c) 2024 LangChain, Inc.
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
diff --git a/templates/rag-milvus/README.md b/templates/rag-milvus/README.md
new file mode 100644
index 0000000000000..c5c289817304a
--- /dev/null
+++ b/templates/rag-milvus/README.md
@@ -0,0 +1,68 @@
+# rag-milvus
+
+This template performs RAG using Milvus and OpenAI.
+
+## Environment Setup
+
+Start the milvus server instance, and get the host ip and port.
+
+Set the `OPENAI_API_KEY` environment variable to access the OpenAI models.
+
+## Usage
+
+To use this package, you should first have the LangChain CLI installed:
+
+```shell
+pip install -U langchain-cli
+```
+
+To create a new LangChain project and install this as the only package, you can do:
+
+```shell
+langchain app new my-app --package rag-milvus
+```
+
+If you want to add this to an existing project, you can just run:
+
+```shell
+langchain app add rag-milvus
+```
+
+And add the following code to your `server.py` file:
+```python
+from rag_milvus import chain as rag_milvus_chain
+
+add_routes(app, rag_milvus_chain, path="/rag-milvus")
+```
+
+(Optional) Let's now configure LangSmith.
+LangSmith will help us trace, monitor and debug LangChain applications.
+You can sign up for LangSmith [here](https://smith.langchain.com/).
+If you don't have access, you can skip this section
+
+
+```shell
+export LANGCHAIN_TRACING_V2=true
+export LANGCHAIN_API_KEY=
+export LANGCHAIN_PROJECT= # if not specified, defaults to "default"
+```
+
+If you are inside this directory, then you can spin up a LangServe instance directly by:
+
+```shell
+langchain serve
+```
+
+This will start the FastAPI app with a server is running locally at
+[http://localhost:8000](http://localhost:8000)
+
+We can see all templates at [http://127.0.0.1:8000/docs](http://127.0.0.1:8000/docs)
+We can access the playground at [http://127.0.0.1:8000/rag-milvus/playground](http://127.0.0.1:8000/rag-milvus/playground)
+
+We can access the template from code with:
+
+```python
+from langserve.client import RemoteRunnable
+
+runnable = RemoteRunnable("http://localhost:8000/rag-milvus")
+```
diff --git a/templates/rag-milvus/pyproject.toml b/templates/rag-milvus/pyproject.toml
new file mode 100644
index 0000000000000..f13a7135c6b7f
--- /dev/null
+++ b/templates/rag-milvus/pyproject.toml
@@ -0,0 +1,34 @@
+[tool.poetry]
+name = "rag-milvus"
+version = "0.1.1"
+description = "RAG using Milvus"
+authors = []
+readme = "README.md"
+
+[tool.poetry.dependencies]
+python = ">=3.8.1,<4.0"
+gigachain = "^0.1"
+gigachain-core = "^0.1"
+gigachain-openai = "^0.1"
+gigachain-community = "^0.0.30"
+pymilvus = "^2.4.3"
+scipy = "^1.9"
+
+[tool.poetry.group.dev.dependencies]
+gigachain-cli = ">=0.0.4"
+fastapi = "^0.104.0"
+sse-starlette = "^1.6.5"
+
+[tool.gigaserve]
+export_module = "rag_milvus"
+export_attr = "chain"
+
+[tool.templates-hub]
+use-case = "rag"
+author = "LangChain"
+integrations = ["OpenAI", "Milvus"]
+tags = ["vectordbs"]
+
+[build-system]
+requires = ["poetry-core"]
+build-backend = "poetry.core.masonry.api"
diff --git a/templates/rag-milvus/rag_milvus/__init__.py b/templates/rag-milvus/rag_milvus/__init__.py
new file mode 100644
index 0000000000000..cf9e1eac2677f
--- /dev/null
+++ b/templates/rag-milvus/rag_milvus/__init__.py
@@ -0,0 +1,3 @@
+from rag_milvus.chain import chain
+
+__all__ = ["chain"]
diff --git a/templates/rag-milvus/rag_milvus/chain.py b/templates/rag-milvus/rag_milvus/chain.py
new file mode 100644
index 0000000000000..57c5300694520
--- /dev/null
+++ b/templates/rag-milvus/rag_milvus/chain.py
@@ -0,0 +1,79 @@
+from langchain_core.output_parsers import StrOutputParser
+from langchain_core.prompts import ChatPromptTemplate
+from langchain_core.pydantic_v1 import BaseModel
+from langchain_core.runnables import RunnableParallel, RunnablePassthrough
+from langchain_milvus.vectorstores import Milvus
+from langchain_openai import ChatOpenAI, OpenAIEmbeddings
+
+# Example for document loading (from url), splitting, and creating vectorstore
+
+# Setting the URI as a local file, e.g.`./milvus.db`, is the most convenient method,
+# as it automatically utilizes Milvus Lite to store all data in this file.
+#
+# If you have large scale of data such as more than a million docs,
+# we recommend setting up a more performant Milvus server on docker or kubernetes.
+# (https://milvus.io/docs/quickstart.md)
+# When using this setup, please use the server URI,
+# e.g.`http://localhost:19530`, as your URI.
+
+URI = "./milvus.db"
+
+"""
+# Load
+from langchain_community.document_loaders import WebBaseLoader
+
+loader = WebBaseLoader("https://lilianweng.github.io/posts/2023-06-23-agent/")
+data = loader.load()
+
+# Split
+from langchain_text_splitters import RecursiveCharacterTextSplitter
+
+text_splitter = RecursiveCharacterTextSplitter(chunk_size=500, chunk_overlap=0)
+all_splits = text_splitter.split_documents(data)
+
+# Add to vectorDB
+vectorstore = Milvus.from_documents(documents=all_splits,
+ collection_name="rag_milvus",
+ embedding=OpenAIEmbeddings(),
+ drop_old=True,
+ connection_args={"uri": URI},
+ )
+retriever = vectorstore.as_retriever()
+"""
+
+# Embed a single document as a test
+vectorstore = Milvus.from_texts(
+ ["harrison worked at kensho"],
+ collection_name="rag_milvus",
+ embedding=OpenAIEmbeddings(),
+ drop_old=True,
+ connection_args={"uri": URI},
+)
+retriever = vectorstore.as_retriever()
+
+# RAG prompt
+template = """Answer the question based only on the following context:
+{context}
+
+Question: {question}
+"""
+prompt = ChatPromptTemplate.from_template(template)
+
+# LLM
+model = ChatOpenAI()
+
+# RAG chain
+chain = (
+ RunnableParallel({"context": retriever, "question": RunnablePassthrough()})
+ | prompt
+ | model
+ | StrOutputParser()
+)
+
+
+# Add typing for input
+class Question(BaseModel):
+ __root__: str
+
+
+chain = chain.with_types(input_type=Question)
diff --git a/templates/rag-milvus/tests/__init__.py b/templates/rag-milvus/tests/__init__.py
new file mode 100644
index 0000000000000..e69de29bb2d1d
diff --git a/templates/rag-momento-vector-index/pyproject.toml b/templates/rag-momento-vector-index/pyproject.toml
index 27d3bc77f768d..2202c15c5728b 100644
--- a/templates/rag-momento-vector-index/pyproject.toml
+++ b/templates/rag-momento-vector-index/pyproject.toml
@@ -7,11 +7,11 @@ readme = "README.md"
[tool.poetry.dependencies]
python = ">=3.8.1,<4.0"
-gigachain = ">=0.0.313, <0.1"
+gigachain = "^0.1"
momento = "^1.12.0"
openai = "<2"
tiktoken = "^0.5.1"
-langchain-text-splitters = ">=0.0.1,<0.1"
+gigachain-text-splitters = ">=0.0.1,<0.1"
[tool.poetry.group.dev.dependencies]
gigachain-cli = ">=0.0.21"
diff --git a/templates/rag-mongo/pyproject.toml b/templates/rag-mongo/pyproject.toml
index b7bdad776d1b5..89989ffc9d611 100644
--- a/templates/rag-mongo/pyproject.toml
+++ b/templates/rag-mongo/pyproject.toml
@@ -9,11 +9,11 @@ readme = "README.md"
[tool.poetry.dependencies]
python = ">=3.8.1,<4.0"
-gigachain = ">=0.0.325"
+gigachain = "^0.1"
openai = "<2"
tiktoken = ">=0.5.1"
pymongo = ">=4.5.0"
-langchain-text-splitters = ">=0.0.1,<0.1"
+gigachain-text-splitters = ">=0.0.1,<0.1"
[tool.poetry.group.dev.dependencies]
gigachain-cli = ">=0.0.21"
diff --git a/templates/rag-multi-index-fusion/pyproject.toml b/templates/rag-multi-index-fusion/pyproject.toml
index 542cfb99d8982..ab2fe58ee1126 100644
--- a/templates/rag-multi-index-fusion/pyproject.toml
+++ b/templates/rag-multi-index-fusion/pyproject.toml
@@ -7,7 +7,7 @@ readme = "README.md"
[tool.poetry.dependencies]
python = ">=3.8.1,<4.0"
-gigachain = ">=0.0.313, <0.1"
+gigachain = "^0.1"
openai = "<2"
xmltodict = "^0.13.0"
kay = "^0.1.2"
diff --git a/templates/rag-multi-index-router/pyproject.toml b/templates/rag-multi-index-router/pyproject.toml
index 4b60f0f6ace5a..1eaf7009e2afa 100644
--- a/templates/rag-multi-index-router/pyproject.toml
+++ b/templates/rag-multi-index-router/pyproject.toml
@@ -7,7 +7,7 @@ readme = "README.md"
[tool.poetry.dependencies]
python = ">=3.8.1,<4.0"
-gigachain = ">=0.0.313, <0.1"
+gigachain = "^0.1"
openai = "<2"
xmltodict = "^0.13.0"
kay = "^0.1.2"
diff --git a/templates/rag-multi-modal-local/pyproject.toml b/templates/rag-multi-modal-local/pyproject.toml
index 5558558f926ef..adb02af5ee602 100644
--- a/templates/rag-multi-modal-local/pyproject.toml
+++ b/templates/rag-multi-modal-local/pyproject.toml
@@ -9,7 +9,7 @@ readme = "README.md"
[tool.poetry.dependencies]
python = ">=3.8.1,<4.0"
-gigachain = ">=0.0.351"
+gigachain = ">=0.0.353,<0.2"
openai = "<2"
tiktoken = ">=0.5.1"
chromadb = ">=0.4.14"
@@ -21,7 +21,7 @@ gigachain-community = ">=0.0.4"
[tool.poetry.group.dev.dependencies]
gigachain-cli = ">=0.0.21"
-[tool.langserve]
+[tool.gigaserve]
export_module = "rag_multi_modal_local"
export_attr = "chain"
diff --git a/templates/rag-multi-modal-mv-local/pyproject.toml b/templates/rag-multi-modal-mv-local/pyproject.toml
index 9706948d5469f..d71375b66266e 100644
--- a/templates/rag-multi-modal-mv-local/pyproject.toml
+++ b/templates/rag-multi-modal-mv-local/pyproject.toml
@@ -9,7 +9,7 @@ readme = "README.md"
[tool.poetry.dependencies]
python = ">=3.8.1,<4.0"
-gigachain = ">=0.0.351"
+gigachain = ">=0.0.353,<0.2"
openai = "<2"
tiktoken = ">=0.5.1"
chromadb = ">=0.4.14"
@@ -21,7 +21,7 @@ gigachain-community = ">=0.0.4"
[tool.poetry.group.dev.dependencies]
gigachain-cli = ">=0.0.21"
-[tool.langserve]
+[tool.gigaserve]
export_module = "rag_multi_modal_mv_local"
export_attr = "chain"
diff --git a/templates/rag-ollama-multi-query/pyproject.toml b/templates/rag-ollama-multi-query/pyproject.toml
index 5b18ef76933d3..6880a371bb2ba 100644
--- a/templates/rag-ollama-multi-query/pyproject.toml
+++ b/templates/rag-ollama-multi-query/pyproject.toml
@@ -9,16 +9,16 @@ readme = "README.md"
[tool.poetry.dependencies]
python = ">=3.8.1,<4.0"
-gigachain = ">=0.0.325"
+gigachain = "^0.1"
openai = "<2"
tiktoken = ">=0.5.1"
chromadb = ">=0.4.14"
-langchain-text-splitters = ">=0.0.1,<0.1"
+gigachain-text-splitters = ">=0.0.1,<0.1"
[tool.poetry.group.dev.dependencies]
gigachain-cli = ">=0.0.21"
-[tool.langserve]
+[tool.gigaserve]
export_module = "rag_ollama_multi_query"
export_attr = "chain"
diff --git a/templates/rag-opensearch/pyproject.toml b/templates/rag-opensearch/pyproject.toml
index 7f47605397439..4fceb0f885b8e 100644
--- a/templates/rag-opensearch/pyproject.toml
+++ b/templates/rag-opensearch/pyproject.toml
@@ -7,7 +7,7 @@ readme = "README.md"
[tool.poetry.dependencies]
python = ">=3.8.1,<4.0"
-gigachain = ">=0.0.313, <0.1"
+gigachain = "^0.1"
openai = "^0.28.1"
opensearch-py = "^2.0.0"
tiktoken = "^0.5.1"
@@ -18,7 +18,7 @@ gigachain-cli = ">=0.0.21"
fastapi = ">=0.104.0,<1"
sse-starlette = "^1.6.5"
-[tool.langserve]
+[tool.gigaserve]
export_module = "rag_opensearch"
export_attr = "chain"
diff --git a/templates/rag-pinecone-multi-query/pyproject.toml b/templates/rag-pinecone-multi-query/pyproject.toml
index 39f20ef8cc192..00e401319ccd8 100644
--- a/templates/rag-pinecone-multi-query/pyproject.toml
+++ b/templates/rag-pinecone-multi-query/pyproject.toml
@@ -9,15 +9,19 @@ readme = "README.md"
[tool.poetry.dependencies]
python = ">=3.8.1,<4.0"
-gigachain = ">=0.0.325"
+gigachain = "^0.1"
openai = "<2"
tiktoken = ">=0.5.1"
pinecone-client = ">=2.2.4"
-langchain-text-splitters = ">=0.0.1,<0.1"
+gigachain-text-splitters = ">=0.0.1,<0.1"
[tool.poetry.group.dev.dependencies]
gigachain-cli = ">=0.0.21"
+[tool.gigaserve]
+export_module = "rag_pinecone_multi_query"
+export_attr = "chain"
+
[tool.templates-hub]
use-case = "rag"
author = "LangChain"
diff --git a/templates/rag-pinecone-rerank/pyproject.toml b/templates/rag-pinecone-rerank/pyproject.toml
index 7471bbdc68537..8c193961f8ada 100644
--- a/templates/rag-pinecone-rerank/pyproject.toml
+++ b/templates/rag-pinecone-rerank/pyproject.toml
@@ -9,16 +9,20 @@ readme = "README.md"
[tool.poetry.dependencies]
python = ">=3.8.1,<4.0"
-gigachain = ">=0.0.325"
+gigachain = "^0.1"
openai = "<2"
tiktoken = ">=0.5.1"
pinecone-client = ">=2.2.4"
cohere = ">=4.32"
-langchain-text-splitters = ">=0.0.1,<0.1"
+gigachain-text-splitters = ">=0.0.1,<0.1"
[tool.poetry.group.dev.dependencies]
gigachain-cli = ">=0.0.21"
+[tool.gigaserve]
+export_module = "rag_pinecone_rerank"
+export_attr = "chain"
+
[tool.templates-hub]
use-case = "rag"
author = "LangChain"
diff --git a/templates/rag-pinecone/pyproject.toml b/templates/rag-pinecone/pyproject.toml
index 14bc54cb4a93d..1cbccd34a8c06 100644
--- a/templates/rag-pinecone/pyproject.toml
+++ b/templates/rag-pinecone/pyproject.toml
@@ -9,15 +9,19 @@ readme = "README.md"
[tool.poetry.dependencies]
python = ">=3.8.1,<4.0"
-gigachain = ">=0.0.325"
+gigachain = "^0.1"
openai = "<2"
tiktoken = ">=0.5.1"
pinecone-client = ">=2.2.4"
-langchain-text-splitters = ">=0.0.1,<0.1"
+gigachain-text-splitters = ">=0.0.1,<0.1"
[tool.poetry.group.dev.dependencies]
gigachain-cli = ">=0.0.21"
+[tool.gigaserve]
+export_module = "rag_pinecone"
+export_attr = "chain"
+
[tool.templates-hub]
use-case = "rag"
author = "LangChain"
diff --git a/templates/rag-redis-multi-modal-multi-vector/pyproject.toml b/templates/rag-redis-multi-modal-multi-vector/pyproject.toml
index 0c2ed8bf4313c..d952ec1bd111a 100644
--- a/templates/rag-redis-multi-modal-multi-vector/pyproject.toml
+++ b/templates/rag-redis-multi-modal-multi-vector/pyproject.toml
@@ -20,7 +20,7 @@ gigachain-cli = ">=0.0.21"
fastapi = ">=0.104.0,<1"
sse-starlette = "^1.6.5"
-[tool.langserve]
+[tool.gigaserve]
export_module = "rag_redis_multi_modal_multi_vector"
export_attr = "chain"
diff --git a/templates/rag-redis/pyproject.toml b/templates/rag-redis/pyproject.toml
index a360cb143a315..c65b48365ce76 100644
--- a/templates/rag-redis/pyproject.toml
+++ b/templates/rag-redis/pyproject.toml
@@ -28,6 +28,10 @@ extras = ["pdf"]
poethepoet = "^0.24.1"
gigachain-cli = ">=0.0.21"
+[tool.gigaserve]
+export_module = "rag_redis.chain"
+export_attr = "chain"
+
[tool.templates-hub]
use-case = "rag"
author = "Redis"
@@ -35,7 +39,7 @@ integrations = ["OpenAI", "Redis", "HuggingFace"]
tags = ["vectordbs"]
[tool.poe.tasks.start]
-cmd = "uvicorn gigachain_cli.dev_scripts:create_demo_server --reload --port $port --host $host"
+cmd = "uvicorn langchain_cli.dev_scripts:create_demo_server --reload --port $port --host $host"
args = [
{ name = "port", help = "port to run on", default = "8000" },
{ name = "host", help = "host to run on", default = "127.0.0.1" },
diff --git a/templates/rag-self-query/pyproject.toml b/templates/rag-self-query/pyproject.toml
index c48b1ad433d9a..140231fb35281 100644
--- a/templates/rag-self-query/pyproject.toml
+++ b/templates/rag-self-query/pyproject.toml
@@ -20,6 +20,10 @@ gigachain-text-splitters = ">=0.0.1,<0.1"
[tool.poetry.group.dev.dependencies]
gigachain-cli = ">=0.0.21"
+[tool.gigaserve]
+export_module = "rag_self_query"
+export_attr = "chain"
+
[tool.templates-hub]
use-case = "rag"
author = "LangChain"
diff --git a/templates/rag-singlestoredb/pyproject.toml b/templates/rag-singlestoredb/pyproject.toml
index 9a57f4dde5061..38d94195107a9 100644
--- a/templates/rag-singlestoredb/pyproject.toml
+++ b/templates/rag-singlestoredb/pyproject.toml
@@ -7,7 +7,7 @@ readme = "README.md"
[tool.poetry.dependencies]
python = ">=3.8.1,<4.0"
-gigachain = ">=0.0.313, <0.1"
+gigachain = "^0.1"
openai = "<2"
singlestoredb = ">=0.8.1"
tiktoken = "^0.5.1"
@@ -18,7 +18,7 @@ gigachain-cli = ">=0.0.21"
fastapi = ">=0.104.0,<1"
sse-starlette = "^1.6.5"
-[tool.langserve]
+[tool.gigaserve]
export_module = "rag_singlestoredb"
export_attr = "chain"
diff --git a/templates/rag-supabase/pyproject.toml b/templates/rag-supabase/pyproject.toml
index 68214cde45800..99b65707fddef 100644
--- a/templates/rag-supabase/pyproject.toml
+++ b/templates/rag-supabase/pyproject.toml
@@ -9,13 +9,18 @@ readme = "README.md"
[tool.poetry.dependencies]
python = ">=3.8.1,<4.0"
-gigachain = ">=0.0.325"
+gigachain = "^0.1"
openai = "<2"
tiktoken = "^0.5.1"
supabase = "^1.2.0"
[tool.poetry.group.dev.dependencies]
gigachain-cli = ">=0.0.21"
+[tool.poetry.group.dev.dependencies.python-dotenv]
+extras = [
+ "cli",
+]
+version = "^1.0.0"
[tool.gigaserve]
export_module = "rag_supabase.chain"
diff --git a/templates/rag-timescale-conversation/pyproject.toml b/templates/rag-timescale-conversation/pyproject.toml
index a8f5955b4d9eb..56a6b419a3eb5 100644
--- a/templates/rag-timescale-conversation/pyproject.toml
+++ b/templates/rag-timescale-conversation/pyproject.toml
@@ -9,18 +9,22 @@ readme = "README.md"
[tool.poetry.dependencies]
python = ">=3.8.1,<4.0"
-gigachain = ">=0.0.335"
+gigachain = "^0.1"
openai = "<2"
tiktoken = ">=0.5.1"
pinecone-client = ">=2.2.4"
beautifulsoup4 = "^4.12.2"
python-dotenv = "^1.0.0"
timescale-vector = "^0.0.3"
-langchain-text-splitters = ">=0.0.1,<0.1"
+gigachain-text-splitters = ">=0.0.1,<0.1"
[tool.poetry.group.dev.dependencies]
gigachain-cli = ">=0.0.21"
+[tool.gigaserve]
+export_module = "rag_timescale_conversation"
+export_attr = "chain"
+
[tool.templates-hub]
use-case = "rag"
author = "Timescale"
diff --git a/templates/rag-timescale-hybrid-search-time/pyproject.toml b/templates/rag-timescale-hybrid-search-time/pyproject.toml
index 47636d9ff795d..e945bdcd0be40 100644
--- a/templates/rag-timescale-hybrid-search-time/pyproject.toml
+++ b/templates/rag-timescale-hybrid-search-time/pyproject.toml
@@ -7,18 +7,22 @@ readme = "README.md"
[tool.poetry.dependencies]
python = ">=3.8.1,<4.0"
-gigachain = ">=0.0.313, <0.1"
+gigachain = "^0.1"
openai = "<2"
fastapi = ">=0.104.0,<1"
sse-starlette = "^1.6.5"
timescale-vector = "^0.0.3"
lark = "^1.1.8"
tiktoken = "^0.5.1"
-langchain-text-splitters = ">=0.0.1,<0.1"
+gigachain-text-splitters = ">=0.0.1,<0.1"
[tool.poetry.group.dev.dependencies]
gigachain-cli = ">=0.0.21"
+[tool.gigaserve]
+export_module = "rag_timescale_hybrid_search_time.chain"
+export_attr = "chain"
+
[tool.templates-hub]
use-case = "rag"
author = "Timescale"
diff --git a/templates/rag-vectara-multiquery/pyproject.toml b/templates/rag-vectara-multiquery/pyproject.toml
index 5a904e32a3ee2..afaf4b17e3256 100644
--- a/templates/rag-vectara-multiquery/pyproject.toml
+++ b/templates/rag-vectara-multiquery/pyproject.toml
@@ -1,6 +1,6 @@
[tool.poetry]
name = "rag-vectara-multiquery"
-version = "0.1.0"
+version = "0.2.0"
description = "RAG using vectara with multiquery retriever"
authors = [
"Ofer Mendelevitch ",
@@ -9,12 +9,16 @@ readme = "README.md"
[tool.poetry.dependencies]
python = ">=3.8.1,<4.0"
-gigachain = ">=0.0.325"
+gigachain = "^0.1"
openai = "<2"
tiktoken = "^0.5.1"
[tool.poetry.group.dev.dependencies]
gigachain-cli = ">=0.0.21"
+[tool.poetry.group.dev.dependencies.python-dotenv]
+extras = [
+ "cli",
+]
version = "^1.0.0"
[tool.gigaserve]
diff --git a/templates/rag-vectara/pyproject.toml b/templates/rag-vectara/pyproject.toml
index 4540f7dc196cf..f085f7c93a3a8 100644
--- a/templates/rag-vectara/pyproject.toml
+++ b/templates/rag-vectara/pyproject.toml
@@ -1,6 +1,6 @@
[tool.poetry]
name = "rag-vectara"
-version = "0.1.0"
+version = "0.2.0"
description = "RAG using vectara retriever"
authors = [
"Ofer Mendelevitch ",
@@ -9,12 +9,17 @@ readme = "README.md"
[tool.poetry.dependencies]
python = ">=3.8.1,<4.0"
-gigachain = ">=0.0.325"
+gigachain = "^0.1"
openai = "<2"
tiktoken = "^0.5.1"
[tool.poetry.group.dev.dependencies]
gigachain-cli = ">=0.0.21"
+[tool.poetry.group.dev.dependencies.python-dotenv]
+extras = [
+ "cli",
+]
+version = "^1.0.0"
[tool.gigaserve]
export_module = "rag_vectara"
diff --git a/templates/rag-weaviate/pyproject.toml b/templates/rag-weaviate/pyproject.toml
index ab14d55d10a7a..6a1c55d8de640 100644
--- a/templates/rag-weaviate/pyproject.toml
+++ b/templates/rag-weaviate/pyproject.toml
@@ -9,14 +9,19 @@ readme = "README.md"
[tool.poetry.dependencies]
python = ">=3.8.1,<4.0"
-gigachain = ">=0.0.325"
+gigachain = "^0.1"
openai = "<2"
tiktoken = "^0.5.1"
weaviate-client = ">=3.24.2"
-langchain-text-splitters = ">=0.0.1,<0.1"
+gigachain-text-splitters = ">=0.0.1,<0.1"
[tool.poetry.group.dev.dependencies]
gigachain-cli = ">=0.0.21"
+[tool.poetry.group.dev.dependencies.python-dotenv]
+extras = [
+ "cli",
+]
+version = "^1.0.0"
[tool.gigaserve]
export_module = "rag_weaviate"
diff --git a/templates/research-assistant/pyproject.toml b/templates/research-assistant/pyproject.toml
index bdf89c38c048a..f2ad4c0926f59 100644
--- a/templates/research-assistant/pyproject.toml
+++ b/templates/research-assistant/pyproject.toml
@@ -7,7 +7,7 @@ readme = "README.md"
[tool.poetry.dependencies]
python = ">=3.8.1,<4.0"
-gigachain = ">=0.0.313, <0.1"
+gigachain = "^0.1"
openai = "<2"
beautifulsoup4 = "^4.12.2"
duckduckgo-search = "^3.9.5"
@@ -18,7 +18,7 @@ gigachain-cli = ">=0.0.21"
fastapi = ">=0.104.0,<1"
sse-starlette = "^1.6.5"
-[tool.langserve]
+[tool.gigaserve]
export_module = "research_assistant"
export_attr = "chain"
diff --git a/templates/retrieval-agent-fireworks/pyproject.toml b/templates/retrieval-agent-fireworks/pyproject.toml
index cdab12b7a6239..06f909b7e8749 100644
--- a/templates/retrieval-agent-fireworks/pyproject.toml
+++ b/templates/retrieval-agent-fireworks/pyproject.toml
@@ -7,9 +7,9 @@ readme = "README.md"
[tool.poetry.dependencies]
python = ">=3.8.1,<4.0"
-langchain = "^0.1"
+gigachain = "^0.1"
arxiv = "^2.0.0"
-langchain-community = ">=0.0.17,<0.2"
+gigachain-community = ">=0.0.17,<0.2"
langchainhub = "^0.1.14"
fireworks-ai = "^0.11.2"
@@ -19,7 +19,7 @@ gigachain-cli = ">=0.0.21"
fastapi = ">=0.104.0,<1"
sse-starlette = "^1.6.5"
-[tool.langserve]
+[tool.gigaserve]
export_module = "retrieval_agent_fireworks"
export_attr = "agent_executor"
diff --git a/templates/retrieval-agent/pyproject.toml b/templates/retrieval-agent/pyproject.toml
index 38588eed0462b..c78f802b3ff0f 100644
--- a/templates/retrieval-agent/pyproject.toml
+++ b/templates/retrieval-agent/pyproject.toml
@@ -7,7 +7,7 @@ readme = "README.md"
[tool.poetry.dependencies]
python = ">=3.8.1,<4.0"
-gigachain = ">=0.0.313, <0.1"
+gigachain = "^0.1"
openai = "<2"
arxiv = "^2.0.0"
gigachain-openai = "^0.0.2.post1"
@@ -17,7 +17,7 @@ gigachain-cli = ">=0.0.21"
fastapi = ">=0.104.0,<1"
sse-starlette = "^1.6.5"
-[tool.langserve]
+[tool.gigaserve]
export_module = "retrieval_agent"
export_attr = "agent_executor"
diff --git a/templates/rewrite-retrieve-read/pyproject.toml b/templates/rewrite-retrieve-read/pyproject.toml
index f822c7f2d07f8..4111df6143948 100644
--- a/templates/rewrite-retrieve-read/pyproject.toml
+++ b/templates/rewrite-retrieve-read/pyproject.toml
@@ -7,13 +7,17 @@ readme = "README.md"
[tool.poetry.dependencies]
python = ">=3.8.1,<4.0"
-gigachain = ">=0.0.325"
+gigachain = "^0.1"
duckduckgo-search = "^3.9.3"
openai = "<2"
[tool.poetry.group.dev.dependencies]
gigachain-cli = ">=0.0.21"
+[tool.gigaserve]
+export_module = "rewrite_retrieve_read.chain"
+export_attr = "chain"
+
[tool.templates-hub]
use-case = "rag"
author = "LangChain"
diff --git a/templates/robocorp-action-server/pyproject.toml b/templates/robocorp-action-server/pyproject.toml
index 4b79aef8d608a..3908a1bcc5613 100644
--- a/templates/robocorp-action-server/pyproject.toml
+++ b/templates/robocorp-action-server/pyproject.toml
@@ -9,14 +9,14 @@ readme = "README.md"
python = ">=3.8.1,<4.0"
gigachain = "^0.1"
gigachain-openai = ">=0.0.2,<0.2"
-langchain-robocorp = ">=0.0.3,<0.2"
+gigachain-robocorp = ">=0.0.3,<0.2"
[tool.poetry.group.dev.dependencies]
gigachain-cli = ">=0.0.21"
fastapi = ">=0.104.0,<1"
sse-starlette = "^1.6.5"
-[tool.langserve]
+[tool.gigaserve]
export_module = "robocorp_action_server"
export_attr = "agent_executor"
diff --git a/templates/self-query-supabase/pyproject.toml b/templates/self-query-supabase/pyproject.toml
index 5ef8d6e378840..8a69159aad32c 100644
--- a/templates/self-query-supabase/pyproject.toml
+++ b/templates/self-query-supabase/pyproject.toml
@@ -9,7 +9,7 @@ readme = "README.md"
[tool.poetry.dependencies]
python = ">=3.8.1,<4.0"
-gigachain = ">=0.0.325"
+gigachain = "^0.1"
openai = "<2"
tiktoken = "^0.5.1"
supabase = "^1.2.0"
@@ -17,6 +17,11 @@ lark = "^1.1.8"
[tool.poetry.group.dev.dependencies]
gigachain-cli = ">=0.0.21"
+[tool.poetry.group.dev.dependencies.python-dotenv]
+extras = [
+ "cli",
+]
+version = "^1.0.0"
[tool.gigaserve]
export_module = "self_query_supabase.chain"
diff --git a/templates/shopping-assistant/pyproject.toml b/templates/shopping-assistant/pyproject.toml
index ad7f74fcfcc05..139b2cb441067 100644
--- a/templates/shopping-assistant/pyproject.toml
+++ b/templates/shopping-assistant/pyproject.toml
@@ -7,16 +7,16 @@ readme = "README.md"
[tool.poetry.dependencies]
python = ">=3.8.12,<4.0"
-langchain = "^0.1"
+gigachain = "^0.1"
openai = "<2"
-ionic-langchain = "^0.2.2"
+ionic-gigachain = "^0.2.2"
gigachain-openai = "^0.0.5"
langchainhub = "^0.1"
[tool.poetry.group.dev.dependencies]
gigachain-cli = ">=0.0.21"
-[tool.langserve]
+[tool.gigaserve]
export_module = "shopping_assistant.agent"
export_attr = "agent_executor"
diff --git a/templates/skeleton-of-thought/pyproject.toml b/templates/skeleton-of-thought/pyproject.toml
index 7ed284719dbfe..f90da7f6ebe05 100644
--- a/templates/skeleton-of-thought/pyproject.toml
+++ b/templates/skeleton-of-thought/pyproject.toml
@@ -7,7 +7,7 @@ readme = "README.md"
[tool.poetry.dependencies]
python = ">=3.8.1,<4.0"
-gigachain = ">=0.0.313, <0.1"
+gigachain = "^0.1"
openai = "^0.28.1"
[tool.poetry.group.dev.dependencies]
@@ -15,7 +15,7 @@ gigachain-cli = ">=0.0.21"
fastapi = ">=0.104.0,<1"
sse-starlette = "^1.6.5"
-[tool.langserve]
+[tool.gigaserve]
export_module = "skeleton_of_thought"
export_attr = "chain"
diff --git a/templates/solo-performance-prompting-agent/pyproject.toml b/templates/solo-performance-prompting-agent/pyproject.toml
index 0f9c7cf1fab33..018679f515954 100644
--- a/templates/solo-performance-prompting-agent/pyproject.toml
+++ b/templates/solo-performance-prompting-agent/pyproject.toml
@@ -7,7 +7,7 @@ readme = "README.md"
[tool.poetry.dependencies]
python = ">=3.8.1,<4.0"
-gigachain = ">=0.0.313, <0.1"
+gigachain = "^0.1"
openai = "<2"
duckduckgo-search = "^3.9.3"
@@ -16,7 +16,7 @@ gigachain-cli = ">=0.0.21"
fastapi = ">=0.104.0,<1"
sse-starlette = "^1.6.5"
-[tool.langserve]
+[tool.gigaserve]
export_module = "solo_performance_prompting_agent.agent"
export_attr = "agent_executor"
diff --git a/templates/sql-llama2/pyproject.toml b/templates/sql-llama2/pyproject.toml
index a67a70aac974c..afd4e6a8b881a 100644
--- a/templates/sql-llama2/pyproject.toml
+++ b/templates/sql-llama2/pyproject.toml
@@ -9,12 +9,16 @@ readme = "README.md"
[tool.poetry.dependencies]
python = ">=3.8.1,<4.0"
-gigachain = ">=0.0.325"
+gigachain = "^0.1"
replicate = ">=0.15.4"
[tool.poetry.group.dev.dependencies]
gigachain-cli = ">=0.0.21"
+[tool.gigaserve]
+export_module = "sql_llama2"
+export_attr = "chain"
+
[tool.templates-hub]
use-case = "sql"
author = "LangChain"
diff --git a/templates/sql-llamacpp/pyproject.toml b/templates/sql-llamacpp/pyproject.toml
index 7f1a0a8f13de3..8e33180e021a4 100644
--- a/templates/sql-llamacpp/pyproject.toml
+++ b/templates/sql-llamacpp/pyproject.toml
@@ -9,12 +9,16 @@ readme = "README.md"
[tool.poetry.dependencies]
python = ">=3.8.1,<4.0"
-gigachain = ">=0.0.325"
+gigachain = "^0.1"
llama-cpp-python = ">=0.1.79"
[tool.poetry.group.dev.dependencies]
gigachain-cli = ">=0.0.21"
+[tool.gigaserve]
+export_module = "sql_llamacpp"
+export_attr = "chain"
+
[tool.templates-hub]
use-case = "sql"
author = "LangChain"
diff --git a/templates/sql-ollama/pyproject.toml b/templates/sql-ollama/pyproject.toml
index 2598c7fa4df65..b0997b69f3b2f 100644
--- a/templates/sql-ollama/pyproject.toml
+++ b/templates/sql-ollama/pyproject.toml
@@ -9,11 +9,15 @@ readme = "README.md"
[tool.poetry.dependencies]
python = ">=3.8.1,<4.0"
-gigachain = ">=0.0.325"
+gigachain = "^0.1"
[tool.poetry.group.dev.dependencies]
gigachain-cli = ">=0.0.21"
+[tool.gigaserve]
+export_module = "sql_ollama"
+export_attr = "chain"
+
[tool.templates-hub]
use-case = "sql"
author = "LangChain"
diff --git a/templates/sql-pgvector/pyproject.toml b/templates/sql-pgvector/pyproject.toml
index b564603cf26f5..b1d9c048494cf 100644
--- a/templates/sql-pgvector/pyproject.toml
+++ b/templates/sql-pgvector/pyproject.toml
@@ -7,7 +7,7 @@ readme = "README.md"
[tool.poetry.dependencies]
python = ">=3.8.1,<4.0"
-gigachain = ">=0.0.313, <0.1"
+gigachain = "^0.1"
openai = "<2"
psycopg2 = "^2.9.9"
tiktoken = "^0.5.1"
@@ -17,7 +17,7 @@ gigachain-cli = ">=0.0.21"
fastapi = ">=0.104.0,<1"
sse-starlette = "^1.6.5"
-[tool.langserve]
+[tool.gigaserve]
export_module = "sql_pgvector"
export_attr = "chain"
diff --git a/templates/sql-research-assistant/pyproject.toml b/templates/sql-research-assistant/pyproject.toml
index 02516560e5e16..3658dfcfc117a 100644
--- a/templates/sql-research-assistant/pyproject.toml
+++ b/templates/sql-research-assistant/pyproject.toml
@@ -7,7 +7,7 @@ readme = "README.md"
[tool.poetry.dependencies]
python = ">=3.8.1,<4.0"
-gigachain = ">=0.0.313, <0.1"
+gigachain = "^0.1"
openai = "^0.28.1"
bs4 = "^0.0.1"
duckduckgo-search = "^4.1.0"
@@ -17,7 +17,7 @@ gigachain-cli = ">=0.0.21"
fastapi = ">=0.104.0,<1"
sse-starlette = "^1.6.5"
-[tool.langserve]
+[tool.gigaserve]
export_module = "sql_research_assistant"
export_attr = "chain"
diff --git a/templates/stepback-qa-prompting/pyproject.toml b/templates/stepback-qa-prompting/pyproject.toml
index 47bdabe03c6c8..d714d837aaf00 100644
--- a/templates/stepback-qa-prompting/pyproject.toml
+++ b/templates/stepback-qa-prompting/pyproject.toml
@@ -7,13 +7,17 @@ readme = "README.md"
[tool.poetry.dependencies]
python = ">=3.8.1,<4.0"
-gigachain = ">=0.0.325"
+gigachain = "^0.1"
duckduckgo-search = "^3.9.3"
openai = "<2"
[tool.poetry.group.dev.dependencies]
gigachain-cli = ">=0.0.21"
+[tool.gigaserve]
+export_module = "stepback_qa_prompting.chain"
+export_attr = "chain"
+
[tool.templates-hub]
use-case = "rag"
author = "LangChain"
diff --git a/templates/summarize-anthropic/pyproject.toml b/templates/summarize-anthropic/pyproject.toml
index 9cb4db77a9ed5..663386d68e8ec 100644
--- a/templates/summarize-anthropic/pyproject.toml
+++ b/templates/summarize-anthropic/pyproject.toml
@@ -1,4 +1,3 @@
-
[tool.poetry]
name = "summarize-anthropic"
version = "0.1.0"
@@ -10,11 +9,15 @@ readme = "README.md"
python = ">=3.8.1,<4.0"
gigachain = "^0.1"
langchainhub = ">=0.1.13"
-gigachain-anthropic = "^0.1.4"
+langchain-anthropic = "^0.1.4"
[tool.poetry.group.dev.dependencies]
gigachain-cli = ">=0.0.21"
+[tool.gigaserve]
+export_module = "summarize_anthropic"
+export_attr = "chain"
+
[tool.templates-hub]
use-case = "summarization"
author = "LangChain"
@@ -24,4 +27,3 @@ tags = ["summarization"]
[build-system]
requires = ["poetry-core"]
build-backend = "poetry.core.masonry.api"
-
diff --git a/templates/vertexai-chuck-norris/pyproject.toml b/templates/vertexai-chuck-norris/pyproject.toml
index ae495f823aadc..7930a0a726849 100644
--- a/templates/vertexai-chuck-norris/pyproject.toml
+++ b/templates/vertexai-chuck-norris/pyproject.toml
@@ -7,7 +7,7 @@ readme = "README.md"
[tool.poetry.dependencies]
python = ">=3.8.1,<4.0"
-langchain = "^0.1"
+gigachain = "^0.1"
google-cloud-aiplatform = "^1.36.4"
[tool.poetry.group.dev.dependencies]
@@ -15,7 +15,7 @@ gigachain-cli = ">=0.0.21"
fastapi = ">=0.104.0,<1"
sse-starlette = "^1.6.5"
-[tool.langserve]
+[tool.gigaserve]
export_module = "vertexai_chuck_norris.chain"
export_attr = "chain"
diff --git a/templates/xml-agent/pyproject.toml b/templates/xml-agent/pyproject.toml
index d9f8030fe8487..0eeff517f749a 100644
--- a/templates/xml-agent/pyproject.toml
+++ b/templates/xml-agent/pyproject.toml
@@ -1,4 +1,3 @@
-
[tool.poetry]
name = "xml-agent"
version = "0.1.0"