diff --git a/docs/ecosystem/atlas.md b/docs/ecosystem/atlas.md new file mode 100644 index 00000000..160cfd7b --- /dev/null +++ b/docs/ecosystem/atlas.md @@ -0,0 +1,25 @@ +# AtlasDB + +This page covers how to Nomic's Atlas ecosystem within LangChain. +It is broken into two parts: installation and setup, and then references to specific Atlas wrappers. + +## Installation and Setup +- Install the Python package with `pip install nomic` +- Nomic is also included in langchains poetry extras `poetry install -E all` +- +## Wrappers + +### VectorStore + +There exists a wrapper around the Atlas neural database, allowing you to use it as a vectorstore. +This vectorstore also gives you full access to the underlying AtlasProject object, which will allow you to use the full range of Atlas map interactions, such as bulk tagging and automatic topic modeling. +Please see [the Nomic docs](https://docs.nomic.ai/atlas_api.html) for more detailed information. + + + +To import this vectorstore: +```python +from langchain.vectorstores import AtlasDB +``` + +For a more detailed walkthrough of the Chroma wrapper, see [this notebook](../modules/indexes/examples/vectorstores.ipynb) diff --git a/docs/modules/indexes/vectorstore_examples/atlas.ipynb b/docs/modules/indexes/vectorstore_examples/atlas.ipynb new file mode 100644 index 00000000..bbb0d430 --- /dev/null +++ b/docs/modules/indexes/vectorstore_examples/atlas.ipynb @@ -0,0 +1,266 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": { + "pycharm": { + "name": "#%% md\n" + } + }, + "source": [ + "# AtlasDB\n", + "\n", + "This notebook shows you how to use functionality related to the AtlasDB" + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "metadata": { + "pycharm": { + "name": "#%%\n" + } + }, + "outputs": [], + "source": [ + "import time\n", + "from langchain.embeddings.openai import OpenAIEmbeddings\n", + "from langchain.text_splitter import SpacyTextSplitter\n", + "from langchain.vectorstores import AtlasDB\n", + "from langchain.document_loaders import TextLoader" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "metadata": { + "scrolled": true + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Collecting en-core-web-sm==3.5.0\n", + " Downloading https://github.com/explosion/spacy-models/releases/download/en_core_web_sm-3.5.0/en_core_web_sm-3.5.0-py3-none-any.whl (12.8 MB)\n", + "\u001B[2K \u001B[90m━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\u001B[0m \u001B[32m12.8/12.8 MB\u001B[0m \u001B[31m90.8 MB/s\u001B[0m eta \u001B[36m0:00:00\u001B[0m00:01\u001B[0m00:01\u001B[0m\n", + "\u001B[?25hRequirement already satisfied: spacy<3.6.0,>=3.5.0 in /home/ubuntu/langchain/.venv/lib/python3.9/site-packages (from en-core-web-sm==3.5.0) (3.5.0)\n", + "Requirement already satisfied: packaging>=20.0 in /home/ubuntu/langchain/.venv/lib/python3.9/site-packages (from spacy<3.6.0,>=3.5.0->en-core-web-sm==3.5.0) (23.0)\n", + "Requirement already satisfied: wasabi<1.2.0,>=0.9.1 in /home/ubuntu/langchain/.venv/lib/python3.9/site-packages (from spacy<3.6.0,>=3.5.0->en-core-web-sm==3.5.0) (1.1.1)\n", + "Requirement already satisfied: langcodes<4.0.0,>=3.2.0 in /home/ubuntu/langchain/.venv/lib/python3.9/site-packages (from spacy<3.6.0,>=3.5.0->en-core-web-sm==3.5.0) (3.3.0)\n", + "Requirement already satisfied: srsly<3.0.0,>=2.4.3 in /home/ubuntu/langchain/.venv/lib/python3.9/site-packages (from spacy<3.6.0,>=3.5.0->en-core-web-sm==3.5.0) (2.4.5)\n", + "Requirement already satisfied: pathy>=0.10.0 in /home/ubuntu/langchain/.venv/lib/python3.9/site-packages (from spacy<3.6.0,>=3.5.0->en-core-web-sm==3.5.0) (0.10.1)\n", + "Requirement already satisfied: setuptools in /home/ubuntu/langchain/.venv/lib/python3.9/site-packages (from spacy<3.6.0,>=3.5.0->en-core-web-sm==3.5.0) (67.4.0)\n", + "Requirement already satisfied: tqdm<5.0.0,>=4.38.0 in /home/ubuntu/langchain/.venv/lib/python3.9/site-packages (from spacy<3.6.0,>=3.5.0->en-core-web-sm==3.5.0) (4.64.1)\n", + "Requirement already satisfied: spacy-loggers<2.0.0,>=1.0.0 in /home/ubuntu/langchain/.venv/lib/python3.9/site-packages (from spacy<3.6.0,>=3.5.0->en-core-web-sm==3.5.0) (1.0.4)\n", + "Requirement already satisfied: smart-open<7.0.0,>=5.2.1 in /home/ubuntu/langchain/.venv/lib/python3.9/site-packages (from spacy<3.6.0,>=3.5.0->en-core-web-sm==3.5.0) (6.3.0)\n", + "Requirement already satisfied: thinc<8.2.0,>=8.1.0 in /home/ubuntu/langchain/.venv/lib/python3.9/site-packages (from spacy<3.6.0,>=3.5.0->en-core-web-sm==3.5.0) (8.1.7)\n", + "Requirement already satisfied: cymem<2.1.0,>=2.0.2 in /home/ubuntu/langchain/.venv/lib/python3.9/site-packages (from spacy<3.6.0,>=3.5.0->en-core-web-sm==3.5.0) (2.0.7)\n", + "Requirement already satisfied: typer<0.8.0,>=0.3.0 in /home/ubuntu/langchain/.venv/lib/python3.9/site-packages (from spacy<3.6.0,>=3.5.0->en-core-web-sm==3.5.0) (0.7.0)\n", + "Requirement already satisfied: requests<3.0.0,>=2.13.0 in /home/ubuntu/langchain/.venv/lib/python3.9/site-packages (from spacy<3.6.0,>=3.5.0->en-core-web-sm==3.5.0) (2.28.2)\n", + "Requirement already satisfied: jinja2 in /home/ubuntu/langchain/.venv/lib/python3.9/site-packages (from spacy<3.6.0,>=3.5.0->en-core-web-sm==3.5.0) (3.1.2)\n", + "Requirement already satisfied: pydantic!=1.8,!=1.8.1,<1.11.0,>=1.7.4 in /home/ubuntu/langchain/.venv/lib/python3.9/site-packages (from spacy<3.6.0,>=3.5.0->en-core-web-sm==3.5.0) (1.10.5)\n", + "Requirement already satisfied: catalogue<2.1.0,>=2.0.6 in /home/ubuntu/langchain/.venv/lib/python3.9/site-packages (from spacy<3.6.0,>=3.5.0->en-core-web-sm==3.5.0) (2.0.8)\n", + "Requirement already satisfied: spacy-legacy<3.1.0,>=3.0.11 in /home/ubuntu/langchain/.venv/lib/python3.9/site-packages (from spacy<3.6.0,>=3.5.0->en-core-web-sm==3.5.0) (3.0.12)\n", + "Requirement already satisfied: numpy>=1.15.0 in /home/ubuntu/langchain/.venv/lib/python3.9/site-packages (from spacy<3.6.0,>=3.5.0->en-core-web-sm==3.5.0) (1.24.2)\n", + "Requirement already satisfied: murmurhash<1.1.0,>=0.28.0 in /home/ubuntu/langchain/.venv/lib/python3.9/site-packages (from spacy<3.6.0,>=3.5.0->en-core-web-sm==3.5.0) (1.0.9)\n", + "Requirement already satisfied: preshed<3.1.0,>=3.0.2 in /home/ubuntu/langchain/.venv/lib/python3.9/site-packages (from spacy<3.6.0,>=3.5.0->en-core-web-sm==3.5.0) (3.0.8)\n", + "Requirement already satisfied: typing-extensions>=4.2.0 in /home/ubuntu/langchain/.venv/lib/python3.9/site-packages (from pydantic!=1.8,!=1.8.1,<1.11.0,>=1.7.4->spacy<3.6.0,>=3.5.0->en-core-web-sm==3.5.0) (4.5.0)\n", + "Requirement already satisfied: charset-normalizer<4,>=2 in /home/ubuntu/langchain/.venv/lib/python3.9/site-packages (from requests<3.0.0,>=2.13.0->spacy<3.6.0,>=3.5.0->en-core-web-sm==3.5.0) (3.0.1)\n", + "Requirement already satisfied: idna<4,>=2.5 in /home/ubuntu/langchain/.venv/lib/python3.9/site-packages (from requests<3.0.0,>=2.13.0->spacy<3.6.0,>=3.5.0->en-core-web-sm==3.5.0) (3.4)\n", + "Requirement already satisfied: certifi>=2017.4.17 in /home/ubuntu/langchain/.venv/lib/python3.9/site-packages (from requests<3.0.0,>=2.13.0->spacy<3.6.0,>=3.5.0->en-core-web-sm==3.5.0) (2022.12.7)\n", + "Requirement already satisfied: urllib3<1.27,>=1.21.1 in /home/ubuntu/langchain/.venv/lib/python3.9/site-packages (from requests<3.0.0,>=2.13.0->spacy<3.6.0,>=3.5.0->en-core-web-sm==3.5.0) (1.26.14)\n", + "Requirement already satisfied: blis<0.8.0,>=0.7.8 in /home/ubuntu/langchain/.venv/lib/python3.9/site-packages (from thinc<8.2.0,>=8.1.0->spacy<3.6.0,>=3.5.0->en-core-web-sm==3.5.0) (0.7.9)\n", + "Requirement already satisfied: confection<1.0.0,>=0.0.1 in /home/ubuntu/langchain/.venv/lib/python3.9/site-packages (from thinc<8.2.0,>=8.1.0->spacy<3.6.0,>=3.5.0->en-core-web-sm==3.5.0) (0.0.4)\n", + "Requirement already satisfied: click<9.0.0,>=7.1.1 in /home/ubuntu/langchain/.venv/lib/python3.9/site-packages (from typer<0.8.0,>=0.3.0->spacy<3.6.0,>=3.5.0->en-core-web-sm==3.5.0) (8.1.3)\n", + "Requirement already satisfied: MarkupSafe>=2.0 in /home/ubuntu/langchain/.venv/lib/python3.9/site-packages (from jinja2->spacy<3.6.0,>=3.5.0->en-core-web-sm==3.5.0) (2.1.2)\n", + "\n", + "\u001B[1m[\u001B[0m\u001B[34;49mnotice\u001B[0m\u001B[1;39;49m]\u001B[0m\u001B[39;49m A new release of pip is available: \u001B[0m\u001B[31;49m23.0\u001B[0m\u001B[39;49m -> \u001B[0m\u001B[32;49m23.0.1\u001B[0m\n", + "\u001B[1m[\u001B[0m\u001B[34;49mnotice\u001B[0m\u001B[1;39;49m]\u001B[0m\u001B[39;49m To update, run: \u001B[0m\u001B[32;49mpip install --upgrade pip\u001B[0m\n", + "\u001B[38;5;2m✔ Download and installation successful\u001B[0m\n", + "You can now load the package via spacy.load('en_core_web_sm')\n" + ] + } + ], + "source": [ + "!python -m spacy download en_core_web_sm" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "metadata": {}, + "outputs": [], + "source": [ + "ATLAS_TEST_API_KEY = '7xDPkYXSYDc1_ErdTPIcoAR9RNd8YDlkS3nVNXcVoIMZ6'" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "metadata": {}, + "outputs": [], + "source": [ + "loader = TextLoader('../../state_of_the_union.txt')\n", + "documents = loader.load()\n", + "text_splitter = SpacyTextSplitter(separator='|')\n", + "texts = []\n", + "for doc in text_splitter.split_documents(documents):\n", + " texts.extend(doc.page_content.split('|'))\n", + " \n", + "texts = [e.strip() for e in texts]" + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "metadata": {}, + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "2023-02-24 16:13:49.696 | INFO | nomic.project:_create_project:884 - Creating project `test_index_1677255228.136989` in organization `Atlas Demo`\n", + "2023-02-24 16:13:51.087 | INFO | nomic.project:wait_for_project_lock:993 - test_index_1677255228.136989: Project lock is released.\n", + "2023-02-24 16:13:51.225 | INFO | nomic.project:wait_for_project_lock:993 - test_index_1677255228.136989: Project lock is released.\n", + "2023-02-24 16:13:51.481 | INFO | nomic.project:add_text:1351 - Uploading text to Atlas.\n", + "1it [00:00, 1.20it/s]\n", + "2023-02-24 16:13:52.318 | INFO | nomic.project:add_text:1422 - Text upload succeeded.\n", + "2023-02-24 16:13:52.628 | INFO | nomic.project:wait_for_project_lock:993 - test_index_1677255228.136989: Project lock is released.\n", + "2023-02-24 16:13:53.380 | INFO | nomic.project:create_index:1192 - Created map `test_index_1677255228.136989_index` in project `test_index_1677255228.136989`: https://atlas.nomic.ai/map/ee2354a3-7f9a-4c6b-af43-b0cda09d7198/db996d77-8981-48a0-897a-ff2c22bbf541\n" + ] + } + ], + "source": [ + "db = AtlasDB.from_texts(texts=texts,\n", + " name='test_index_'+str(time.time()),\n", + " description='test_index',\n", + " api_key=ATLAS_TEST_API_KEY,\n", + " index_kwargs={'build_topic_model': True})" + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "metadata": { + "scrolled": false + }, + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "2023-02-24 16:14:09.106 | INFO | nomic.project:wait_for_project_lock:993 - test_index_1677255228.136989: Project lock is released.\n" + ] + } + ], + "source": [ + "with db.project.wait_for_project_lock():\n", + " time.sleep(1)" + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "\n", + " test_index_1677255228.136989\n", + "
\n", + " A description for your project 508 datums inserted.\n", + "
\n", + " 1 index built.\n", + "
Projections\n", + "
\n", + "\n", + "

Projection ID: db996d77-8981-48a0-897a-ff2c22bbf541

\n", + "
\n", + "
Hide embedded project
\n", + "
\n", + " Explore on atlas.nomic.ai\n", + "
\n", + "
\n", + " \n", + " \n", + "\n", + " \n", + " \n", + " \n", + " " + ], + "text/plain": [ + "AtlasProject: <{'id': 'ee2354a3-7f9a-4c6b-af43-b0cda09d7198', 'owner': '9c29afbb-a002-4d49-958e-ecf5ae1351ac', 'project_name': 'test_index_1677255228.136989', 'creator': 'auth0|63efc4b5462246f4d9a6ecf2', 'description': 'A description for your project', 'opensearch_index_id': 'f61fb8dd-0abf-4f31-9130-41870e443902', 'is_public': True, 'project_fields': ['atlas_id', 'text'], 'unique_id_field': 'atlas_id', 'modality': 'text', 'total_datums_in_project': 508, 'created_timestamp': '2023-02-24T16:13:50.313363+00:00', 'atlas_indices': [{'id': 'b1b01833-0964-4597-a4bc-a2d60700949d', 'project_id': 'ee2354a3-7f9a-4c6b-af43-b0cda09d7198', 'index_name': 'test_index_1677255228.136989_index', 'indexed_field': 'text', 'created_timestamp': '2023-02-24T16:13:52.957101+00:00', 'updated_timestamp': '2023-02-24T16:14:03.469621+00:00', 'atoms': ['charchunk', 'document'], 'colorable_fields': [], 'embedders': [{'id': '7ec0868a-4eed-4414-a482-25cce9803e1b', 'atlas_index_id': 'b1b01833-0964-4597-a4bc-a2d60700949d', 'ready': True, 'model_name': 'NomicEmbed', 'hyperparameters': {'norm': 'both', 'batch_size': 20, 'polymerize_by': 'charchunk', 'dataset_buffer_size': 1000}}], 'nearest_neighbor_indices': [{'id': '86f8e3ff-e07c-4678-a4d7-144db4b0301d', 'index_name': 'NomicOrganize', 'ready': True, 'hyperparameters': {'dim': 384, 'space': 'l2'}, 'atom_strategies': ['document']}], 'projections': [{'id': 'db996d77-8981-48a0-897a-ff2c22bbf541', 'projection_name': 'NomicProject', 'ready': True, 'hyperparameters': {'spread': 1.0, 'n_epochs': 50, 'n_neighbors': 15}, 'atom_strategies': ['document'], 'created_timestamp': '2023-02-24T16:13:52.979561+00:00', 'updated_timestamp': '2023-02-24T16:14:03.466309+00:00'}]}], 'insert_update_delete_lock': False}>" + ] + }, + "execution_count": 7, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "db.project" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3 (ipykernel)", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.9.4" + } + }, + "nbformat": 4, + "nbformat_minor": 1 +} \ No newline at end of file diff --git a/langchain/vectorstores/__init__.py b/langchain/vectorstores/__init__.py index d6c81f2a..d250980c 100644 --- a/langchain/vectorstores/__init__.py +++ b/langchain/vectorstores/__init__.py @@ -1,4 +1,5 @@ """Wrappers on top of vector stores.""" +from langchain.vectorstores.atlas import AtlasDB from langchain.vectorstores.base import VectorStore from langchain.vectorstores.chroma import Chroma from langchain.vectorstores.elastic_vector_search import ElasticVectorSearch @@ -19,4 +20,5 @@ __all__ = [ "Milvus", "Chroma", "OpenSearchVectorSearch", + "AtlasDB", ] diff --git a/langchain/vectorstores/atlas.py b/langchain/vectorstores/atlas.py new file mode 100644 index 00000000..78712f27 --- /dev/null +++ b/langchain/vectorstores/atlas.py @@ -0,0 +1,322 @@ +"""Wrapper around Atlas by Nomic.""" +from __future__ import annotations + +import logging +import uuid +from typing import Any, Iterable, List, Optional + +import numpy as np + +from langchain.docstore.document import Document +from langchain.embeddings.base import Embeddings +from langchain.vectorstores.base import VectorStore + +logger = logging.getLogger() + + +class AtlasDB(VectorStore): + """Wrapper around Atlas: Nomic's neural database and rhizomatic instrument. + + To use, you should have the ``nomic`` python package installed. + + Example: + .. code-block:: python + + from langchain.vectorstores import AtlasDB + from langchain.embeddings.openai import OpenAIEmbeddings + + embeddings = OpenAIEmbeddings() + vectorstore = AtlasDB("my_project", embeddings.embed_query) + """ + + _ATLAS_DEFAULT_ID_FIELD = "atlas_id" + + def __init__( + self, + name: str, + embedding_function: Optional[Embeddings] = None, + api_key: Optional[str] = None, + description: str = "A description for your project", + is_public: bool = True, + reset_project_if_exists: bool = False, + ) -> None: + """ + Initialize the Atlas Client + + Args: + name (str): The name of your project. If the project already exists, + it will be loaded. + embedding_function (Optional[Callable]): An optional function used for + embedding your data. If None, data will be embedded with + Nomic's embed model. + api_key (str): Your nomic API key + description (str): A description for your project. + is_public (bool): Whether your project is publicly accessible. + True by default. + reset_project_if_exists (bool): Whether to reset this project if it + already exists. Default False. + Generally userful during development and testing. + """ + try: + import nomic + from nomic import AtlasProject + except ImportError: + raise ValueError( + "Could not import nomic python package. " + "Please it install it with `pip install nomic`." + ) + + if api_key is None: + raise ValueError("No API key provided. Sign up at atlas.nomic.ai!") + nomic.login(api_key) + + self._embedding_function = embedding_function + modality = "text" + if self._embedding_function is not None: + modality = "embedding" + + # Check if the project exists, create it if not + self.project = AtlasProject( + name=name, + description=description, + modality=modality, + is_public=is_public, + reset_project_if_exists=reset_project_if_exists, + unique_id_field=AtlasDB._ATLAS_DEFAULT_ID_FIELD, + ) + self.project._latest_project_state() + + def add_texts( + self, + texts: Iterable[str], + metadatas: Optional[List[dict]] = None, + ids: Optional[List[str]] = None, + refresh: bool = True, + **kwargs: Any, + ) -> List[str]: + """Run more texts through the embeddings and add to the vectorstore. + + Args: + texts (Iterable[str]): Texts to add to the vectorstore. + metadatas (Optional[List[dict]], optional): Optional list of metadatas. + ids (Optional[List[str]]): An optional list of ids. + refresh(bool): Whether or not to refresh indices with the updated data. + Default True. + Returns: + List[str]: List of IDs of the added texts. + """ + + if ( + metadatas is not None + and len(metadatas) > 0 + and "text" in metadatas[0].keys() + ): + raise ValueError("Cannot accept key text in metadata!") + + texts = list(texts) + if ids is None: + ids = [str(uuid.uuid1()) for _ in texts] + + # Embedding upload case + if self._embedding_function is not None: + _embeddings = self._embedding_function.embed_documents(texts) + embeddings = np.stack(_embeddings) + if metadatas is None: + data = [ + {AtlasDB._ATLAS_DEFAULT_ID_FIELD: ids[i], "text": texts[i]} + for i, _ in enumerate(texts) + ] + else: + for i in range(len(metadatas)): + metadatas[i][AtlasDB._ATLAS_DEFAULT_ID_FIELD] = ids[i] + metadatas[i]["text"] = texts[i] + data = metadatas + + self.project._validate_map_data_inputs( + [], id_field=AtlasDB._ATLAS_DEFAULT_ID_FIELD, data=data + ) + with self.project.wait_for_project_lock(): + self.project.add_embeddings(embeddings=embeddings, data=data) + # Text upload case + else: + if metadatas is None: + data = [ + {"text": text, AtlasDB._ATLAS_DEFAULT_ID_FIELD: ids[i]} + for i, text in enumerate(texts) + ] + else: + for i, text in enumerate(texts): + metadatas[i]["text"] = texts + metadatas[i][AtlasDB._ATLAS_DEFAULT_ID_FIELD] = ids[i] + data = metadatas + + self.project._validate_map_data_inputs( + [], id_field=AtlasDB._ATLAS_DEFAULT_ID_FIELD, data=data + ) + + with self.project.wait_for_project_lock(): + self.project.add_text(data) + + if refresh: + if len(self.project.indices) > 0: + with self.project.wait_for_project_lock(): + self.project.rebuild_maps() + + return ids + + def create_index(self, **kwargs: Any) -> Any: + """Creates an index in your project. + + See + https://docs.nomic.ai/atlas_api.html#nomic.project.AtlasProject.create_index + for full detail. + """ + with self.project.wait_for_project_lock(): + return self.project.create_index(**kwargs) + + def similarity_search( + self, + query: str, + k: int = 4, + **kwargs: Any, + ) -> List[Document]: + """Run similarity search with AtlasDB + + Args: + query (str): Query text to search for. + k (int): Number of results to return. Defaults to 4. + + Returns: + List[Document]: List of documents most similar to the query text. + """ + if self._embedding_function is None: + raise NotImplementedError( + "AtlasDB requires an embedding_function for text similarity search!" + ) + + _embedding = self._embedding_function.embed_documents([query])[0] + embedding = np.array(_embedding).reshape(1, -1) + with self.project.wait_for_project_lock(): + neighbors, _ = self.project.projections[0].vector_search( + queries=embedding, k=k + ) + datas = self.project.get_data(ids=neighbors[0]) + + docs = [ + Document(page_content=datas[i]["text"], metadata=datas[i]) + for i, neighbor in enumerate(neighbors) + ] + return docs + + @classmethod + def from_texts( + cls, + texts: List[str], + embedding: Optional[Embeddings] = None, + metadatas: Optional[List[dict]] = None, + ids: Optional[List[str]] = None, + name: Optional[str] = None, + api_key: Optional[str] = None, + description: str = "A description for your project", + is_public: bool = True, + reset_project_if_exists: bool = False, + index_kwargs: Optional[dict] = None, + **kwargs: Any, + ) -> AtlasDB: + """Create an AtlasDB vectorstore from a raw documents. + + Args: + texts (List[str]): The list of texts to ingest. + name (str): Name of the project to create. + api_key (str): Your nomic API key, + embedding (Optional[Embeddings]): Embedding function. Defaults to None. + metadatas (Optional[List[dict]]): List of metadatas. Defaults to None. + ids (Optional[List[str]]): Optional list of document IDs. If None, + ids will be auto created + description (str): A description for your project. + is_public (bool): Whether your project is publicly accessible. + True by default. + reset_project_if_exists (bool): Whether to reset this project if it + already exists. Default False. + Generally userful during development and testing. + index_kwargs (Optional[dict]): Dict of kwargs for index creation. + See https://docs.nomic.ai/atlas_api.html + + Returns: + AtlasDB: Nomic's neural database and finest rhizomatic instrument + """ + if name is None or api_key is None: + raise ValueError("`name` and `api_key` cannot be None.") + + # Inject relevant kwargs + all_index_kwargs = {"name": name + "_index", "indexed_field": "text"} + if index_kwargs is not None: + for k, v in index_kwargs.items(): + all_index_kwargs[k] = v + + # Build project + atlasDB = cls( + name, + embedding_function=embedding, + api_key=api_key, + description="A description for your project", + is_public=is_public, + reset_project_if_exists=reset_project_if_exists, + ) + with atlasDB.project.wait_for_project_lock(): + atlasDB.add_texts(texts=texts, metadatas=metadatas, ids=ids) + atlasDB.create_index(**all_index_kwargs) + return atlasDB + + @classmethod + def from_documents( + cls, + documents: List[Document], + embedding: Optional[Embeddings] = None, + ids: Optional[List[str]] = None, + name: Optional[str] = None, + api_key: Optional[str] = None, + persist_directory: Optional[str] = None, + description: str = "A description for your project", + is_public: bool = True, + reset_project_if_exists: bool = False, + index_kwargs: Optional[dict] = None, + **kwargs: Any, + ) -> AtlasDB: + """Create an AtlasDB vectorstore from a list of documents. + + Args: + name (str): Name of the collection to create. + api_key (str): Your nomic API key, + documents (List[Document]): List of documents to add to the vectorstore. + embedding (Optional[Embeddings]): Embedding function. Defaults to None. + ids (Optional[List[str]]): Optional list of document IDs. If None, + ids will be auto created + description (str): A description for your project. + is_public (bool): Whether your project is publicly accessible. + True by default. + reset_project_if_exists (bool): Whether to reset this project if + it already exists. Default False. + Generally userful during development and testing. + index_kwargs (Optional[dict]): Dict of kwargs for index creation. + See https://docs.nomic.ai/atlas_api.html + + Returns: + AtlasDB: Nomic's neural database and finest rhizomatic instrument + """ + if name is None or api_key is None: + raise ValueError("`name` and `api_key` cannot be None.") + texts = [doc.page_content for doc in documents] + metadatas = [doc.metadata for doc in documents] + return cls.from_texts( + name=name, + api_key=api_key, + texts=texts, + embedding=embedding, + metadatas=metadatas, + ids=ids, + description=description, + is_public=is_public, + reset_project_if_exists=reset_project_if_exists, + index_kwargs=index_kwargs, + ) diff --git a/poetry.lock b/poetry.lock index b360a1b8..1eb608e5 100644 --- a/poetry.lock +++ b/poetry.lock @@ -2146,24 +2146,6 @@ files = [ [package.dependencies] arrow = ">=0.15.0" -[[package]] -name = "isort" -version = "5.12.0" -description = "A Python utility / library to sort Python imports." -category = "dev" -optional = false -python-versions = ">=3.8.0" -files = [ - {file = "isort-5.12.0-py3-none-any.whl", hash = "sha256:f84c2818376e66cf843d497486ea8fed8700b340f308f076c6fb1229dff318b6"}, - {file = "isort-5.12.0.tar.gz", hash = "sha256:8bef7dde241278824a6d83f44a544709b065191b95b6e50894bdc722fcba0504"}, -] - -[package.extras] -colors = ["colorama (>=0.4.3)"] -pipfile-deprecated-finder = ["pip-shims (>=0.5.2)", "pipreqs", "requirementslib"] -plugins = ["setuptools"] -requirements-deprecated-finder = ["pip-api", "pipreqs"] - [[package]] name = "jaraco-context" version = "4.3.0" @@ -2230,6 +2212,21 @@ files = [ {file = "joblib-1.2.0.tar.gz", hash = "sha256:e1cee4a79e4af22881164f218d4311f60074197fb707e082e803b61f6d137018"}, ] +[[package]] +name = "jsonlines" +version = "3.1.0" +description = "Library with helpers for the jsonlines file format" +category = "main" +optional = true +python-versions = ">=3.6" +files = [ + {file = "jsonlines-3.1.0-py3-none-any.whl", hash = "sha256:632f5e38f93dfcb1ac8c4e09780b92af3a55f38f26e7c47ae85109d420b6ad39"}, + {file = "jsonlines-3.1.0.tar.gz", hash = "sha256:2579cb488d96f815b0eb81629e3e6b0332da0962a18fa3532958f7ba14a5c37f"}, +] + +[package.dependencies] +attrs = ">=19.2.0" + [[package]] name = "jsonpointer" version = "2.3" @@ -2728,7 +2725,7 @@ testing = ["coverage", "pyyaml"] name = "markdown-it-py" version = "2.1.0" description = "Python port of markdown-it. Markdown parsing, done right!" -category = "dev" +category = "main" optional = false python-versions = ">=3.7" files = [ @@ -2884,7 +2881,7 @@ testing = ["coverage", "pytest", "pytest-cov", "pytest-regressions"] name = "mdurl" version = "0.1.2" description = "Markdown URL utilities" -category = "dev" +category = "main" optional = false python-versions = ">=3.7" files = [ @@ -3368,6 +3365,33 @@ plot = ["matplotlib"] tgrep = ["pyparsing"] twitter = ["twython"] +[[package]] +name = "nomic" +version = "1.0.43" +description = "The offical Nomic python client." +category = "main" +optional = true +python-versions = "*" +files = [ + {file = "nomic-1.0.43.tar.gz", hash = "sha256:275e52a734bc2421251ace99b148d7ac42f2b88b7aaa700e1c6a4fc1730ef987"}, +] + +[package.dependencies] +click = "*" +cohere = "*" +jsonlines = "*" +loguru = "*" +numpy = "*" +pyarrow = "*" +pydantic = "*" +requests = "*" +rich = "*" +tqdm = "*" +wonderwords = "*" + +[package.extras] +dev = ["black", "coverage", "mkautodoc", "mkdocs-material", "mkdocstrings[python]", "myst-parser", "pylint", "pytest", "twine"] + [[package]] name = "notebook" version = "6.5.2" @@ -4050,6 +4074,44 @@ files = [ [package.extras] tests = ["pytest"] +[[package]] +name = "pyarrow" +version = "11.0.0" +description = "Python library for Apache Arrow" +category = "main" +optional = true +python-versions = ">=3.7" +files = [ + {file = "pyarrow-11.0.0-cp310-cp310-macosx_10_14_x86_64.whl", hash = "sha256:40bb42afa1053c35c749befbe72f6429b7b5f45710e85059cdd534553ebcf4f2"}, + {file = "pyarrow-11.0.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:7c28b5f248e08dea3b3e0c828b91945f431f4202f1a9fe84d1012a761324e1ba"}, + {file = "pyarrow-11.0.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a37bc81f6c9435da3c9c1e767324ac3064ffbe110c4e460660c43e144be4ed85"}, + {file = "pyarrow-11.0.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ad7c53def8dbbc810282ad308cc46a523ec81e653e60a91c609c2233ae407689"}, + {file = "pyarrow-11.0.0-cp310-cp310-win_amd64.whl", hash = "sha256:25aa11c443b934078bfd60ed63e4e2d42461682b5ac10f67275ea21e60e6042c"}, + {file = "pyarrow-11.0.0-cp311-cp311-macosx_10_14_x86_64.whl", hash = "sha256:e217d001e6389b20a6759392a5ec49d670757af80101ee6b5f2c8ff0172e02ca"}, + {file = "pyarrow-11.0.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:ad42bb24fc44c48f74f0d8c72a9af16ba9a01a2ccda5739a517aa860fa7e3d56"}, + {file = "pyarrow-11.0.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2d942c690ff24a08b07cb3df818f542a90e4d359381fbff71b8f2aea5bf58841"}, + {file = "pyarrow-11.0.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f010ce497ca1b0f17a8243df3048055c0d18dcadbcc70895d5baf8921f753de5"}, + {file = "pyarrow-11.0.0-cp311-cp311-win_amd64.whl", hash = "sha256:2f51dc7ca940fdf17893227edb46b6784d37522ce08d21afc56466898cb213b2"}, + {file = "pyarrow-11.0.0-cp37-cp37m-macosx_10_14_x86_64.whl", hash = "sha256:1cbcfcbb0e74b4d94f0b7dde447b835a01bc1d16510edb8bb7d6224b9bf5bafc"}, + {file = "pyarrow-11.0.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:aaee8f79d2a120bf3e032d6d64ad20b3af6f56241b0ffc38d201aebfee879d00"}, + {file = "pyarrow-11.0.0-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:410624da0708c37e6a27eba321a72f29d277091c8f8d23f72c92bada4092eb5e"}, + {file = "pyarrow-11.0.0-cp37-cp37m-win_amd64.whl", hash = "sha256:2d53ba72917fdb71e3584ffc23ee4fcc487218f8ff29dd6df3a34c5c48fe8c06"}, + {file = "pyarrow-11.0.0-cp38-cp38-macosx_10_14_x86_64.whl", hash = "sha256:f12932e5a6feb5c58192209af1d2607d488cb1d404fbc038ac12ada60327fa34"}, + {file = "pyarrow-11.0.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:41a1451dd895c0b2964b83d91019e46f15b5564c7ecd5dcb812dadd3f05acc97"}, + {file = "pyarrow-11.0.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:becc2344be80e5dce4e1b80b7c650d2fc2061b9eb339045035a1baa34d5b8f1c"}, + {file = "pyarrow-11.0.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8f40be0d7381112a398b93c45a7e69f60261e7b0269cc324e9f739ce272f4f70"}, + {file = "pyarrow-11.0.0-cp38-cp38-win_amd64.whl", hash = "sha256:362a7c881b32dc6b0eccf83411a97acba2774c10edcec715ccaab5ebf3bb0835"}, + {file = "pyarrow-11.0.0-cp39-cp39-macosx_10_14_x86_64.whl", hash = "sha256:ccbf29a0dadfcdd97632b4f7cca20a966bb552853ba254e874c66934931b9841"}, + {file = "pyarrow-11.0.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:3e99be85973592051e46412accea31828da324531a060bd4585046a74ba45854"}, + {file = "pyarrow-11.0.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:69309be84dcc36422574d19c7d3a30a7ea43804f12552356d1ab2a82a713c418"}, + {file = "pyarrow-11.0.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:da93340fbf6f4e2a62815064383605b7ffa3e9eeb320ec839995b1660d69f89b"}, + {file = "pyarrow-11.0.0-cp39-cp39-win_amd64.whl", hash = "sha256:caad867121f182d0d3e1a0d36f197df604655d0b466f1bc9bafa903aa95083e4"}, + {file = "pyarrow-11.0.0.tar.gz", hash = "sha256:5461c57dbdb211a632a48facb9b39bbeb8a7905ec95d768078525283caef5f6d"}, +] + +[package.dependencies] +numpy = ">=1.16.6" + [[package]] name = "pyasn1" version = "0.4.8" @@ -4296,7 +4358,7 @@ typing-extensions = "*" name = "pygments" version = "2.14.0" description = "Pygments is a syntax highlighting package written in Python." -category = "dev" +category = "main" optional = false python-versions = ">=3.6" files = [ @@ -4996,6 +5058,26 @@ files = [ {file = "rfc3986_validator-0.1.1.tar.gz", hash = "sha256:3d44bde7921b3b9ec3ae4e3adca370438eccebc676456449b145d533b240d055"}, ] +[[package]] +name = "rich" +version = "13.3.1" +description = "Render rich text, tables, progress bars, syntax highlighting, markdown and more to the terminal" +category = "main" +optional = true +python-versions = ">=3.7.0" +files = [ + {file = "rich-13.3.1-py3-none-any.whl", hash = "sha256:8aa57747f3fc3e977684f0176a88e789be314a99f99b43b75d1e9cb5dc6db9e9"}, + {file = "rich-13.3.1.tar.gz", hash = "sha256:125d96d20c92b946b983d0d392b84ff945461e5a06d3867e9f9e575f8697b67f"}, +] + +[package.dependencies] +markdown-it-py = ">=2.1.0,<3.0.0" +pygments = ">=2.14.0,<3.0.0" +typing-extensions = {version = ">=4.0.0,<5.0", markers = "python_version < \"3.9\""} + +[package.extras] +jupyter = ["ipywidgets (>=7.5.1,<9)"] + [[package]] name = "rsa" version = "4.9" @@ -6935,6 +7017,21 @@ xmltodict = "*" docs = ["jaraco.packaging (>=8.2)", "rst.linker (>=1.9)", "sphinx"] testing = ["keyring", "pmxbot", "pytest (>=3.5,!=3.7.3)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=1.2.3)", "pytest-cov", "pytest-enabler", "pytest-flake8", "pytest-mypy"] +[[package]] +name = "wonderwords" +version = "2.2.0" +description = "A python package for random words and sentences in the english language" +category = "main" +optional = true +python-versions = ">=3.6" +files = [ + {file = "wonderwords-2.2.0-py3-none-any.whl", hash = "sha256:65fc665f1f5590e98f6d9259414ea036bf1b6dd83e51aa6ba44473c99ca92da1"}, + {file = "wonderwords-2.2.0.tar.gz", hash = "sha256:0b7ec6f591062afc55603bfea71463afbab06794b3064d9f7b04d0ce251a13d0"}, +] + +[package.extras] +cli = ["rich (==9.10.0)"] + [[package]] name = "wrapt" version = "1.14.1" @@ -7126,10 +7223,10 @@ docs = ["furo", "jaraco.packaging (>=9)", "jaraco.tidelift (>=1.4)", "rst.linker testing = ["flake8 (<5)", "func-timeout", "jaraco.functools", "jaraco.itertools", "more-itertools", "pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=1.3)", "pytest-flake8", "pytest-mypy (>=0.9.1)"] [extras] -all = ["anthropic", "cohere", "openai", "nlpcloud", "huggingface_hub", "manifest-ml", "elasticsearch", "opensearch-py", "google-search-results", "faiss-cpu", "sentence-transformers", "transformers", "spacy", "nltk", "wikipedia", "beautifulsoup4", "tiktoken", "torch", "jinja2", "pinecone-client", "weaviate-client", "redis", "google-api-python-client", "wolframalpha", "qdrant-client", "tensorflow-text", "pypdf", "networkx"] +all = ["anthropic", "cohere", "openai", "nlpcloud", "huggingface_hub", "manifest-ml", "elasticsearch", "opensearch-py", "google-search-results", "faiss-cpu", "sentence-transformers", "transformers", "spacy", "nltk", "wikipedia", "beautifulsoup4", "tiktoken", "torch", "jinja2", "pinecone-client", "weaviate-client", "redis", "google-api-python-client", "wolframalpha", "qdrant-client", "tensorflow-text", "pypdf", "networkx", "nomic"] llms = ["anthropic", "cohere", "openai", "nlpcloud", "huggingface_hub", "manifest-ml", "torch", "transformers"] [metadata] lock-version = "2.0" python-versions = ">=3.8.1,<4.0" -content-hash = "de052ebffaad7fa65dbb96eb900bbc28ea45dc280e3de7e857d4d4c72018d6f7" +content-hash = "2eb321893be595ceba7c7965ef3bc999dcc0941867c46bde6732eeb2c039a6d6" diff --git a/pyproject.toml b/pyproject.toml index 8e0aa2db..01d628d8 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -42,6 +42,7 @@ tenacity = "^8.1.0" cohere = {version = "^3", optional = true} openai = {version = "^0", optional = true} nlpcloud = {version = "^1", optional = true} +nomic = {version = "^1.0.43", optional = true} huggingface_hub = {version = "^0", optional = true} google-search-results = {version = "^2", optional = true} sentence-transformers = {version = "^2", optional = true} @@ -94,7 +95,7 @@ playwright = "^1.28.0" [tool.poetry.extras] llms = ["anthropic", "cohere", "openai", "nlpcloud", "huggingface_hub", "manifest-ml", "torch", "transformers"] -all = ["anthropic", "cohere", "openai", "nlpcloud", "huggingface_hub", "manifest-ml", "elasticsearch", "opensearch-py", "google-search-results", "faiss-cpu", "sentence_transformers", "transformers", "spacy", "nltk", "wikipedia", "beautifulsoup4", "tiktoken", "torch", "jinja2", "pinecone-client", "weaviate-client", "redis", "google-api-python-client", "wolframalpha", "qdrant-client", "tensorflow-text", "pypdf", "networkx"] +all = ["anthropic", "cohere", "openai", "nlpcloud", "huggingface_hub", "manifest-ml", "elasticsearch", "opensearch-py", "google-search-results", "faiss-cpu", "sentence_transformers", "transformers", "spacy", "nltk", "wikipedia", "beautifulsoup4", "tiktoken", "torch", "jinja2", "pinecone-client", "weaviate-client", "redis", "google-api-python-client", "wolframalpha", "qdrant-client", "tensorflow-text", "pypdf", "networkx", "nomic"] [tool.ruff] select = [ diff --git a/tests/integration_tests/vectorstores/test_atlas.py b/tests/integration_tests/vectorstores/test_atlas.py new file mode 100644 index 00000000..2a7c99e8 --- /dev/null +++ b/tests/integration_tests/vectorstores/test_atlas.py @@ -0,0 +1,40 @@ +"""Test Atlas functionality.""" +import time + +from langchain.vectorstores import AtlasDB +from tests.integration_tests.vectorstores.fake_embeddings import FakeEmbeddings + +ATLAS_TEST_API_KEY = "7xDPkYXSYDc1_ErdTPIcoAR9RNd8YDlkS3nVNXcVoIMZ6" + + +def test_atlas() -> None: + """Test end to end construction and search.""" + texts = ["foo", "bar", "baz"] + docsearch = AtlasDB.from_texts( + name="langchain_test_project" + str(time.time()), + texts=texts, + api_key=ATLAS_TEST_API_KEY, + embedding=FakeEmbeddings(), + ) + output = docsearch.similarity_search("foo", k=1) + assert len(output) == 1 + assert output[0].page_content == "foo" + + +def test_atlas_with_metadatas() -> None: + """Test end to end construction and search.""" + texts = ["foo", "bar", "baz"] + metadatas = [{"page": str(i)} for i in range(len(texts))] + docsearch = AtlasDB.from_texts( + name="langchain_test_project" + str(time.time()), + texts=texts, + api_key=ATLAS_TEST_API_KEY, + embedding=FakeEmbeddings(), + metadatas=metadatas, + reset_project_if_exists=True, + ) + + output = docsearch.similarity_search("foo", k=1) + assert len(output) == 1 + assert output[0].page_content == "foo" + assert output[0].metadata["page"] == "0"