From dd1c233b4f88664c4ceaf997f3738f16fbd80911 Mon Sep 17 00:00:00 2001 From: Evan Date: Thu, 17 Apr 2025 20:10:44 -0600 Subject: [PATCH 01/11] add pytest-cov and pytest-mock --- poetry.lock | 120 +++++++++++++++++++++++++++++++++++++++++- pyproject.toml | 138 +++++++++++++++++++++++++------------------------ 2 files changed, 188 insertions(+), 70 deletions(-) diff --git a/poetry.lock b/poetry.lock index c66a98fa..ac5a65e3 100644 --- a/poetry.lock +++ b/poetry.lock @@ -653,6 +653,85 @@ traitlets = ">=4" [package.extras] test = ["pytest"] +[[package]] +name = "coverage" +version = "7.8.0" +description = "Code coverage measurement for Python" +optional = false +python-versions = ">=3.9" +groups = ["dev"] +files = [ + {file = "coverage-7.8.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:2931f66991175369859b5fd58529cd4b73582461877ecfd859b6549869287ffe"}, + {file = "coverage-7.8.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:52a523153c568d2c0ef8826f6cc23031dc86cffb8c6aeab92c4ff776e7951b28"}, + {file = "coverage-7.8.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5c8a5c139aae4c35cbd7cadca1df02ea8cf28a911534fc1b0456acb0b14234f3"}, + {file = "coverage-7.8.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5a26c0c795c3e0b63ec7da6efded5f0bc856d7c0b24b2ac84b4d1d7bc578d676"}, + {file = "coverage-7.8.0-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:821f7bcbaa84318287115d54becb1915eece6918136c6f91045bb84e2f88739d"}, + {file = "coverage-7.8.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:a321c61477ff8ee705b8a5fed370b5710c56b3a52d17b983d9215861e37b642a"}, + {file = "coverage-7.8.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:ed2144b8a78f9d94d9515963ed273d620e07846acd5d4b0a642d4849e8d91a0c"}, + {file = "coverage-7.8.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:042e7841a26498fff7a37d6fda770d17519982f5b7d8bf5278d140b67b61095f"}, + {file = "coverage-7.8.0-cp310-cp310-win32.whl", hash = "sha256:f9983d01d7705b2d1f7a95e10bbe4091fabc03a46881a256c2787637b087003f"}, + {file = "coverage-7.8.0-cp310-cp310-win_amd64.whl", hash = "sha256:5a570cd9bd20b85d1a0d7b009aaf6c110b52b5755c17be6962f8ccd65d1dbd23"}, + {file = "coverage-7.8.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:e7ac22a0bb2c7c49f441f7a6d46c9c80d96e56f5a8bc6972529ed43c8b694e27"}, + {file = "coverage-7.8.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:bf13d564d310c156d1c8e53877baf2993fb3073b2fc9f69790ca6a732eb4bfea"}, + {file = "coverage-7.8.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a5761c70c017c1b0d21b0815a920ffb94a670c8d5d409d9b38857874c21f70d7"}, + {file = "coverage-7.8.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e5ff52d790c7e1628241ffbcaeb33e07d14b007b6eb00a19320c7b8a7024c040"}, + {file = "coverage-7.8.0-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d39fc4817fd67b3915256af5dda75fd4ee10621a3d484524487e33416c6f3543"}, + {file = "coverage-7.8.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:b44674870709017e4b4036e3d0d6c17f06a0e6d4436422e0ad29b882c40697d2"}, + {file = "coverage-7.8.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:8f99eb72bf27cbb167b636eb1726f590c00e1ad375002230607a844d9e9a2318"}, + {file = "coverage-7.8.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:b571bf5341ba8c6bc02e0baeaf3b061ab993bf372d982ae509807e7f112554e9"}, + {file = "coverage-7.8.0-cp311-cp311-win32.whl", hash = "sha256:e75a2ad7b647fd8046d58c3132d7eaf31b12d8a53c0e4b21fa9c4d23d6ee6d3c"}, + {file = "coverage-7.8.0-cp311-cp311-win_amd64.whl", hash = "sha256:3043ba1c88b2139126fc72cb48574b90e2e0546d4c78b5299317f61b7f718b78"}, + {file = "coverage-7.8.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:bbb5cc845a0292e0c520656d19d7ce40e18d0e19b22cb3e0409135a575bf79fc"}, + {file = "coverage-7.8.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:4dfd9a93db9e78666d178d4f08a5408aa3f2474ad4d0e0378ed5f2ef71640cb6"}, + {file = "coverage-7.8.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f017a61399f13aa6d1039f75cd467be388d157cd81f1a119b9d9a68ba6f2830d"}, + {file = "coverage-7.8.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0915742f4c82208ebf47a2b154a5334155ed9ef9fe6190674b8a46c2fb89cb05"}, + {file = "coverage-7.8.0-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8a40fcf208e021eb14b0fac6bdb045c0e0cab53105f93ba0d03fd934c956143a"}, + {file = "coverage-7.8.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:a1f406a8e0995d654b2ad87c62caf6befa767885301f3b8f6f73e6f3c31ec3a6"}, + {file = "coverage-7.8.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:77af0f6447a582fdc7de5e06fa3757a3ef87769fbb0fdbdeba78c23049140a47"}, + {file = "coverage-7.8.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:f2d32f95922927186c6dbc8bc60df0d186b6edb828d299ab10898ef3f40052fe"}, + {file = "coverage-7.8.0-cp312-cp312-win32.whl", hash = "sha256:769773614e676f9d8e8a0980dd7740f09a6ea386d0f383db6821df07d0f08545"}, + {file = "coverage-7.8.0-cp312-cp312-win_amd64.whl", hash = "sha256:e5d2b9be5b0693cf21eb4ce0ec8d211efb43966f6657807f6859aab3814f946b"}, + {file = "coverage-7.8.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:5ac46d0c2dd5820ce93943a501ac5f6548ea81594777ca585bf002aa8854cacd"}, + {file = "coverage-7.8.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:771eb7587a0563ca5bb6f622b9ed7f9d07bd08900f7589b4febff05f469bea00"}, + {file = "coverage-7.8.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:42421e04069fb2cbcbca5a696c4050b84a43b05392679d4068acbe65449b5c64"}, + {file = "coverage-7.8.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:554fec1199d93ab30adaa751db68acec2b41c5602ac944bb19187cb9a41a8067"}, + {file = "coverage-7.8.0-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5aaeb00761f985007b38cf463b1d160a14a22c34eb3f6a39d9ad6fc27cb73008"}, + {file = "coverage-7.8.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:581a40c7b94921fffd6457ffe532259813fc68eb2bdda60fa8cc343414ce3733"}, + {file = "coverage-7.8.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:f319bae0321bc838e205bf9e5bc28f0a3165f30c203b610f17ab5552cff90323"}, + {file = "coverage-7.8.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:04bfec25a8ef1c5f41f5e7e5c842f6b615599ca8ba8391ec33a9290d9d2db3a3"}, + {file = "coverage-7.8.0-cp313-cp313-win32.whl", hash = "sha256:dd19608788b50eed889e13a5d71d832edc34fc9dfce606f66e8f9f917eef910d"}, + {file = "coverage-7.8.0-cp313-cp313-win_amd64.whl", hash = "sha256:a9abbccd778d98e9c7e85038e35e91e67f5b520776781d9a1e2ee9d400869487"}, + {file = "coverage-7.8.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:18c5ae6d061ad5b3e7eef4363fb27a0576012a7447af48be6c75b88494c6cf25"}, + {file = "coverage-7.8.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:95aa6ae391a22bbbce1b77ddac846c98c5473de0372ba5c463480043a07bff42"}, + {file = "coverage-7.8.0-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e013b07ba1c748dacc2a80e69a46286ff145935f260eb8c72df7185bf048f502"}, + {file = "coverage-7.8.0-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d766a4f0e5aa1ba056ec3496243150698dc0481902e2b8559314368717be82b1"}, + {file = "coverage-7.8.0-cp313-cp313t-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ad80e6b4a0c3cb6f10f29ae4c60e991f424e6b14219d46f1e7d442b938ee68a4"}, + {file = "coverage-7.8.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:b87eb6fc9e1bb8f98892a2458781348fa37e6925f35bb6ceb9d4afd54ba36c73"}, + {file = "coverage-7.8.0-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:d1ba00ae33be84066cfbe7361d4e04dec78445b2b88bdb734d0d1cbab916025a"}, + {file = "coverage-7.8.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:f3c38e4e5ccbdc9198aecc766cedbb134b2d89bf64533973678dfcf07effd883"}, + {file = "coverage-7.8.0-cp313-cp313t-win32.whl", hash = "sha256:379fe315e206b14e21db5240f89dc0774bdd3e25c3c58c2c733c99eca96f1ada"}, + {file = "coverage-7.8.0-cp313-cp313t-win_amd64.whl", hash = "sha256:2e4b6b87bb0c846a9315e3ab4be2d52fac905100565f4b92f02c445c8799e257"}, + {file = "coverage-7.8.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:fa260de59dfb143af06dcf30c2be0b200bed2a73737a8a59248fcb9fa601ef0f"}, + {file = "coverage-7.8.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:96121edfa4c2dfdda409877ea8608dd01de816a4dc4a0523356067b305e4e17a"}, + {file = "coverage-7.8.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6b8af63b9afa1031c0ef05b217faa598f3069148eeee6bb24b79da9012423b82"}, + {file = "coverage-7.8.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:89b1f4af0d4afe495cd4787a68e00f30f1d15939f550e869de90a86efa7e0814"}, + {file = "coverage-7.8.0-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:94ec0be97723ae72d63d3aa41961a0b9a6f5a53ff599813c324548d18e3b9e8c"}, + {file = "coverage-7.8.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:8a1d96e780bdb2d0cbb297325711701f7c0b6f89199a57f2049e90064c29f6bd"}, + {file = "coverage-7.8.0-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:f1d8a2a57b47142b10374902777e798784abf400a004b14f1b0b9eaf1e528ba4"}, + {file = "coverage-7.8.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:cf60dd2696b457b710dd40bf17ad269d5f5457b96442f7f85722bdb16fa6c899"}, + {file = "coverage-7.8.0-cp39-cp39-win32.whl", hash = "sha256:be945402e03de47ba1872cd5236395e0f4ad635526185a930735f66710e1bd3f"}, + {file = "coverage-7.8.0-cp39-cp39-win_amd64.whl", hash = "sha256:90e7fbc6216ecaffa5a880cdc9c77b7418c1dcb166166b78dbc630d07f278cc3"}, + {file = "coverage-7.8.0-pp39.pp310.pp311-none-any.whl", hash = "sha256:b8194fb8e50d556d5849753de991d390c5a1edeeba50f68e3a9253fbd8bf8ccd"}, + {file = "coverage-7.8.0-py3-none-any.whl", hash = "sha256:dbf364b4c5e7bae9250528167dfe40219b62e2d573c854d74be213e1e52069f7"}, + {file = "coverage-7.8.0.tar.gz", hash = "sha256:7a3d62b3b03b4b6fd41a085f3574874cf946cb4604d2b4d3e8dca8cd570ca501"}, +] + +[package.dependencies] +tomli = {version = "*", optional = true, markers = "python_full_version <= \"3.11.0a6\" and extra == \"toml\""} + +[package.extras] +toml = ["tomli ; python_full_version <= \"3.11.0a6\""] + [[package]] name = "debugpy" version = "1.8.13" @@ -3381,6 +3460,43 @@ pytest = ">=8.2,<9" docs = ["sphinx (>=5.3)", "sphinx-rtd-theme (>=1)"] testing = ["coverage (>=6.2)", "hypothesis (>=5.7.1)"] +[[package]] +name = "pytest-cov" +version = "6.1.1" +description = "Pytest plugin for measuring coverage." +optional = false +python-versions = ">=3.9" +groups = ["dev"] +files = [ + {file = "pytest_cov-6.1.1-py3-none-any.whl", hash = "sha256:bddf29ed2d0ab6f4df17b4c55b0a657287db8684af9c42ea546b21b1041b3dde"}, + {file = "pytest_cov-6.1.1.tar.gz", hash = "sha256:46935f7aaefba760e716c2ebfbe1c216240b9592966e7da99ea8292d4d3e2a0a"}, +] + +[package.dependencies] +coverage = {version = ">=7.5", extras = ["toml"]} +pytest = ">=4.6" + +[package.extras] +testing = ["fields", "hunter", "process-tests", "pytest-xdist", "virtualenv"] + +[[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" +groups = ["dev"] +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-xdist" version = "3.6.1" @@ -4479,7 +4595,7 @@ description = "A lil' TOML parser" optional = false python-versions = ">=3.8" groups = ["dev"] -markers = "python_version == \"3.10\"" +markers = "python_full_version <= \"3.11.0a6\"" files = [ {file = "tomli-2.2.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:678e4fa69e4575eb77d103de3df8a895e1591b48e740211bd1067378c69e8249"}, {file = "tomli-2.2.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:023aa114dd824ade0100497eb2318602af309e5a55595f76b626d6d9f3b7b0a6"}, @@ -5328,4 +5444,4 @@ groq = ["groq"] [metadata] lock-version = "2.1" python-versions = ">=3.10,<4" -content-hash = "fa0608494d973b528ccad56576f42c04cff04a66c1325796d224dedab292aa16" +content-hash = "96fe3386c3d71524696848d12384d82e9edb3d684c72baea53db97e8d703d791" diff --git a/pyproject.toml b/pyproject.toml index c23170fe..92190242 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,87 +1,89 @@ [project] -name = "graphiti-core" -description = "A temporal graph building library" -version = "0.10.3" -authors = [ - { "name" = "Paul Paliychuk", "email" = "paul@getzep.com" }, - { "name" = "Preston Rasmussen", "email" = "preston@getzep.com" }, - { "name" = "Daniel Chalef", "email" = "daniel@getzep.com" }, -] -readme = "README.md" -license = "Apache-2.0" -requires-python = ">=3.10,<4" -packages = [{ include = "graphiti_core", from = "." }] -dependencies = [ - "pydantic>=2.8.2", - "neo4j>=5.23.0", - "diskcache>=5.6.3", - "openai>=1.53.0", - "tenacity>=9.0.0", - "numpy>=1.0.0", - "python-dotenv>=1.0.1", - "graph-service (>=1.0.0.7,<2.0.0.0)", -] + authors = [ + { "name" = "Daniel Chalef", "email" = "daniel@getzep.com" }, + { "name" = "Paul Paliychuk", "email" = "paul@getzep.com" }, + { "name" = "Preston Rasmussen", "email" = "preston@getzep.com" }, + ] + dependencies = [ + "diskcache>=5.6.3", + "graph-service (>=1.0.0.7,<2.0.0.0)", + "neo4j>=5.23.0", + "numpy>=1.0.0", + "openai>=1.53.0", + "pydantic>=2.8.2", + "python-dotenv>=1.0.1", + "tenacity>=9.0.0", + ] + description = "A temporal graph building library" + license = "Apache-2.0" + name = "graphiti-core" + packages = [{ include = "graphiti_core", from = "." }] + readme = "README.md" + requires-python = ">=3.10,<4" + version = "0.10.3" [project.urls] -Homepage = "https://help.getzep.com/graphiti/graphiti/overview" -Repository = "https://github.com/getzep/graphiti" + Homepage = "https://help.getzep.com/graphiti/graphiti/overview" + Repository = "https://github.com/getzep/graphiti" [project.optional-dependencies] -anthropic = ["anthropic>=0.49.0"] -groq = ["groq>=0.2.0"] -google-genai = ["google-genai>=1.8.0"] + anthropic = ["anthropic>=0.49.0"] + google-genai = ["google-genai>=1.8.0"] + groq = ["groq>=0.2.0"] [tool.poetry.group.dev.dependencies] -mypy = ">=1.11.1" -groq = ">=0.2.0" -anthropic = ">=0.49.0" -google-genai = ">=1.8.0" -ipykernel = ">=6.29.5" -jupyterlab = ">=4.2.4" -diskcache-stubs = ">=5.6.3.6.20240818" -langgraph = ">=0.2.15" -langchain-anthropic = ">=0.2.4" -langsmith = ">=0.1.108" -langchain-openai = ">=0.2.6" -sentence-transformers = ">=3.2.1" -transformers = ">=4.45.2" -voyageai = ">=0.2.3" -pytest = ">=8.3.3" -pytest-asyncio = ">=0.24.0" -pytest-xdist = ">=3.6.1" -ruff = ">=0.7.1" + anthropic = ">=0.49.0" + diskcache-stubs = ">=5.6.3.6.20240818" + google-genai = ">=1.8.0" + groq = ">=0.2.0" + ipykernel = ">=6.29.5" + jupyterlab = ">=4.2.4" + langchain-anthropic = ">=0.2.4" + langchain-openai = ">=0.2.6" + langgraph = ">=0.2.15" + langsmith = ">=0.1.108" + mypy = ">=1.11.1" + pytest = ">=8.3.3" + pytest-asyncio = ">=0.24.0" + pytest-xdist = ">=3.6.1" + ruff = ">=0.7.1" + sentence-transformers = ">=3.2.1" + transformers = ">=4.45.2" + voyageai = ">=0.2.3" +pytest-cov = "^6.1.1" +pytest-mock = "^3.14.0" [build-system] -requires = ["poetry-core"] -build-backend = "poetry.core.masonry.api" + build-backend = "poetry.core.masonry.api" + requires = ["poetry-core"] [tool.poetry] -requires-poetry = ">=2.0" + requires-poetry = ">=2.0" [tool.pytest.ini_options] -pythonpath = ["."] + pythonpath = ["."] [tool.ruff] -line-length = 100 + line-length = 100 [tool.ruff.lint] -select = [ - # pycodestyle - "E", - # Pyflakes - "F", - # pyupgrade - "UP", - # flake8-bugbear - "B", - # flake8-simplify - "SIM", - # isort - "I", -] -ignore = ["E501"] + ignore = ["E501"] + select = [ + # pycodestyle + "E", + # Pyflakes + "F", + # pyupgrade + "UP", + # flake8-bugbear + "B", + # flake8-simplify + "SIM", + # isort + "I", + ] [tool.ruff.format] -quote-style = "single" -indent-style = "space" -docstring-code-format = true + docstring-code-format = true + indent-style = "space" + quote-style = "single" From 0a238b5868da673f0a853eeef9af592b70dda9eb Mon Sep 17 00:00:00 2001 From: Evan Date: Thu, 17 Apr 2025 20:11:22 -0600 Subject: [PATCH 02/11] add coverage configuration --- .coveragerc | 7 +++++++ pytest.ini | 26 +++++++++++++++++++++++++- 2 files changed, 32 insertions(+), 1 deletion(-) create mode 100644 .coveragerc diff --git a/.coveragerc b/.coveragerc new file mode 100644 index 00000000..67f23c78 --- /dev/null +++ b/.coveragerc @@ -0,0 +1,7 @@ +[report] +exclude_lines = + pragma: no cover + def __repr__ + if __name__ == .__main__.: + @abstractmethod + pass \ No newline at end of file diff --git a/pytest.ini b/pytest.ini index 7c95c298..68d8b2fc 100644 --- a/pytest.ini +++ b/pytest.ini @@ -1,3 +1,27 @@ [pytest] +# Test selection and discovery markers = - integration: marks tests as integration tests \ No newline at end of file + integration: marks tests as integration tests +testpaths = tests +python_files = test_*.py + +# Async configuration +asyncio_mode = strict +asyncio_default_fixture_loop_scope = function + +# Warning filters +filterwarnings = + ignore::UserWarning:neo4j._optional_deps.*: + +# Coverage configuration +[coverage:report] +exclude_lines = + pragma: no cover + def __repr__ + if __name__ == .__main__.: + @abstractmethod + pass + +# Tool-specific settings +[tool:pytest] +addopts = --cov=graphiti_core --cov-report=term-missing \ No newline at end of file From cbbd4cbce0ab22b3c409b018d891a7134ae88b7b Mon Sep 17 00:00:00 2001 From: Evan Date: Thu, 17 Apr 2025 21:29:10 -0600 Subject: [PATCH 03/11] add embedder tests --- tests/embedder/conftest.py | 28 +++++ tests/embedder/test_client.py | 67 ++++++++++++ tests/embedder/test_gemini.py | 150 ++++++++++++++++++++++++++ tests/embedder/test_openai.py | 192 ++++++++++++++++++++++++++++++++++ tests/embedder/test_voyage.py | 181 ++++++++++++++++++++++++++++++++ 5 files changed, 618 insertions(+) create mode 100644 tests/embedder/conftest.py create mode 100644 tests/embedder/test_client.py create mode 100644 tests/embedder/test_gemini.py create mode 100644 tests/embedder/test_openai.py create mode 100644 tests/embedder/test_voyage.py diff --git a/tests/embedder/conftest.py b/tests/embedder/conftest.py new file mode 100644 index 00000000..72a882a8 --- /dev/null +++ b/tests/embedder/conftest.py @@ -0,0 +1,28 @@ +""" +Copyright 2024, Zep Software, Inc. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +""" + +import pytest + +from graphiti_core.embedder.client import EMBEDDING_DIM + + +@pytest.fixture +def mock_embedding_values() -> list[float]: + """Returns a list of mock embedding values with the default dimension. + + This can be used across different embedder tests to create consistent mock responses. + """ + return [0.1] * EMBEDDING_DIM diff --git a/tests/embedder/test_client.py b/tests/embedder/test_client.py new file mode 100644 index 00000000..21ce13a0 --- /dev/null +++ b/tests/embedder/test_client.py @@ -0,0 +1,67 @@ +""" +Copyright 2024, Zep Software, Inc. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +""" + +# Running tests: pytest -xvs tests/embedder/test_client.py +# Running tests with coverage: pytest -xvs tests/embedder/test_client.py --cov=graphiti_core.embedder.client --cov-report=term-missing + +from typing import Any + +import pytest + +from graphiti_core.embedder.client import EMBEDDING_DIM, EmbedderClient, EmbedderConfig + + +def test_embedder_config_defaults() -> None: + """Test that EmbedderConfig defaults are set correctly""" + config = EmbedderConfig() + assert config.embedding_dim == EMBEDDING_DIM + + +def test_embedder_config_frozen_field() -> None: + """Test that embedding_dim is frozen and can't be changed after initialization""" + config = EmbedderConfig(embedding_dim=512) + assert config.embedding_dim == 512 + + # Attempting to change the frozen field should raise an error + with pytest.raises(ValueError): + config.embedding_dim = 256 + + +def test_embedder_client_is_abstract() -> None: + """Test that EmbedderClient cannot be instantiated directly""" + with pytest.raises(TypeError): + EmbedderClient() # Should fail as it's an abstract class # type: ignore[abstract] + + +class MinimalEmbedderClient(EmbedderClient): + """Minimal implementation of EmbedderClient for testing inheritance""" + + def __init__(self, mock_values: list[float]) -> None: + self.mock_values = mock_values + + async def create(self, input_data: Any) -> list[float]: + return self.mock_values + + +@pytest.mark.asyncio +async def test_embedder_client_can_be_subclassed(mock_embedding_values: list[float]) -> None: + """Test that EmbedderClient can be subclassed with a concrete implementation""" + client = MinimalEmbedderClient(mock_embedding_values) + result = await client.create('test') + + assert isinstance(result, list) + assert len(result) == EMBEDDING_DIM + assert all(isinstance(x, float) for x in result) diff --git a/tests/embedder/test_gemini.py b/tests/embedder/test_gemini.py new file mode 100644 index 00000000..343cde86 --- /dev/null +++ b/tests/embedder/test_gemini.py @@ -0,0 +1,150 @@ +""" +Copyright 2024, Zep Software, Inc. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +""" + +# Running tests: poetry run pytest -xvs tests/embedder/test_gemini.py +# Running tests with coverage: poetry run pytest -xvs tests/embedder/test_gemini.py --cov=graphiti_core.embedder.gemini --cov-report=term-missing + +import pytest +from pytest_mock import MockerFixture + +from graphiti_core.embedder.client import EMBEDDING_DIM +from graphiti_core.embedder.gemini import ( + DEFAULT_EMBEDDING_MODEL, + GeminiEmbedder, + GeminiEmbedderConfig, +) + + +@pytest.mark.asyncio +async def test_gemini_embedder_initialization(mocker: MockerFixture) -> None: + """Test that GeminiEmbedder initializes correctly with API key""" + mock_genai = mocker.patch('graphiti_core.embedder.gemini.genai') + mock_client = mocker.MagicMock() + mock_genai.Client.return_value = mock_client + + embedder = GeminiEmbedder(config=GeminiEmbedderConfig(api_key='test_key')) + + # Verify client initialization + mock_genai.Client.assert_called_once_with(api_key='test_key') + assert embedder.config.embedding_model == DEFAULT_EMBEDDING_MODEL + assert embedder.config.embedding_dim == EMBEDDING_DIM + + +@pytest.mark.asyncio +async def test_gemini_embedder_custom_config(mocker: MockerFixture) -> None: + """Test GeminiEmbedder with custom configuration""" + mock_genai = mocker.patch('graphiti_core.embedder.gemini.genai') + mock_client = mocker.MagicMock() + mock_genai.Client.return_value = mock_client + + custom_config = GeminiEmbedderConfig( + api_key='test_key', + embedding_model='custom-model', + embedding_dim=512, + ) + embedder = GeminiEmbedder(config=custom_config) + + # Verify client initialization with custom config + assert embedder.config.embedding_model == 'custom-model' + assert embedder.config.embedding_dim == 512 + + +@pytest.mark.asyncio +async def test_gemini_embedder_create_with_string( + mocker: MockerFixture, + mock_embedding_values: list[float], +) -> None: + """Test creating embeddings with a string input""" + mock_genai = mocker.patch('graphiti_core.embedder.gemini.genai') + mock_client = mocker.MagicMock() + mock_genai.Client.return_value = mock_client + + # Set up mock response + mock_result = mocker.MagicMock() + mock_embedding = mocker.MagicMock() + mock_embedding.values = mock_embedding_values + mock_result.embeddings = [mock_embedding] + + # Set up async mock for embed_content + mock_aio = mocker.MagicMock() + mock_models = mocker.MagicMock() + mock_embed_content = mocker.AsyncMock(return_value=mock_result) + mock_models.embed_content = mock_embed_content + mock_aio.models = mock_models + mock_client.aio = mock_aio + + # Create embedder + embedder = GeminiEmbedder(config=GeminiEmbedderConfig(api_key='test_key')) + + # Call create method + result = await embedder.create('test input') + + # Verify API call + mock_embed_content.assert_called_once() + _, kwargs = mock_embed_content.call_args + assert kwargs['model'] == DEFAULT_EMBEDDING_MODEL + assert kwargs['contents'] == ['test input'] + assert 'config' in kwargs + + # Verify result + assert len(result) == EMBEDDING_DIM + assert all(x == 0.1 for x in result) + + +@pytest.mark.asyncio +async def test_gemini_embedder_create_with_list( + mocker: MockerFixture, + mock_embedding_values: list[float], +) -> None: + """Test creating embeddings with a list input""" + mock_genai = mocker.patch('graphiti_core.embedder.gemini.genai') + mock_client = mocker.MagicMock() + mock_genai.Client.return_value = mock_client + + # Set up mock response + mock_result = mocker.MagicMock() + mock_embedding = mocker.MagicMock() + mock_embedding.values = mock_embedding_values + mock_result.embeddings = [mock_embedding] + + # Set up async mock for embed_content + mock_aio = mocker.MagicMock() + mock_models = mocker.MagicMock() + mock_embed_content = mocker.AsyncMock(return_value=mock_result) + mock_models.embed_content = mock_embed_content + mock_aio.models = mock_models + mock_client.aio = mock_aio + + # Create embedder with custom model + embedder = GeminiEmbedder( + config=GeminiEmbedderConfig( + api_key='test_key', + embedding_model='custom-model', + ) + ) + + # Call create method with list + test_input = ['input1', 'input2'] + result = await embedder.create(test_input) + + # Verify API call with custom model + mock_embed_content.assert_called_once() + _, kwargs = mock_embed_content.call_args + assert kwargs['model'] == 'custom-model' + assert kwargs['contents'] == [test_input] # The list is wrapped in another list + + # Verify result + assert len(result) == EMBEDDING_DIM diff --git a/tests/embedder/test_openai.py b/tests/embedder/test_openai.py new file mode 100644 index 00000000..0967447c --- /dev/null +++ b/tests/embedder/test_openai.py @@ -0,0 +1,192 @@ +""" +Copyright 2024, Zep Software, Inc. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +""" + +# Running tests: poetry run pytest -xvs tests/embedder/test_openai.py +# Running tests with coverage: poetry run pytest -xvs tests/embedder/test_openai.py --cov=graphiti_core.embedder.openai --cov-report=term-missing + +from unittest.mock import AsyncMock + +import pytest +from pytest_mock import MockerFixture + +from graphiti_core.embedder.client import EMBEDDING_DIM +from graphiti_core.embedder.openai import ( + DEFAULT_EMBEDDING_MODEL, + OpenAIEmbedder, + OpenAIEmbedderConfig, +) + + +@pytest.fixture +def mock_openai_client(mocker: MockerFixture, mock_embedding_values: list[float]) -> AsyncMock: + mock_client = mocker.AsyncMock() + mock_response = mocker.MagicMock() + mock_response.data = [mocker.MagicMock()] + mock_response.data[0].embedding = mock_embedding_values + mock_client.embeddings.create.return_value = mock_response + return mock_client + + +@pytest.mark.asyncio +async def test_openai_embedder_create_with_string( + mocker: MockerFixture, + mock_embedding_values: list[float], +) -> None: + """Test creating embeddings with a string input""" + mock_openai = mocker.patch('graphiti_core.embedder.openai.AsyncOpenAI') + mock_client = mocker.AsyncMock() + mock_response = mocker.MagicMock() + mock_response.data = [mocker.MagicMock()] + mock_response.data[0].embedding = mock_embedding_values + mock_client.embeddings.create.return_value = mock_response + mock_openai.return_value = mock_client + + # Test with explicit model (testing behavior) + embedder = OpenAIEmbedder( + config=OpenAIEmbedderConfig(api_key='test_key', embedding_model='text-embedding-3-small') + ) + + result = await embedder.create('test input') + + # Check result + assert len(result) == EMBEDDING_DIM # Should be truncated to embedding_dim + assert all(isinstance(x, float) for x in result) + + # Verify API call + mock_client.embeddings.create.assert_called_once_with( + input='test input', model='text-embedding-3-small' + ) + + +@pytest.mark.asyncio +async def test_openai_embedder_create_with_list( + mocker: MockerFixture, + mock_embedding_values: list[float], +) -> None: + """Test creating embeddings with a list input""" + mock_openai = mocker.patch('graphiti_core.embedder.openai.AsyncOpenAI') + mock_client = mocker.AsyncMock() + mock_response = mocker.MagicMock() + mock_response.data = [mocker.MagicMock()] + mock_response.data[0].embedding = mock_embedding_values + mock_client.embeddings.create.return_value = mock_response + mock_openai.return_value = mock_client + + # Only specifying API key to test default model behavior + embedder = OpenAIEmbedder(config=OpenAIEmbedderConfig(api_key='test_key')) + + result = await embedder.create(['test input 1', 'test input 2']) + + # Check result + assert len(result) == EMBEDDING_DIM + + # Verify API call uses the default model from imported constant + mock_client.embeddings.create.assert_called_once_with( + input=['test input 1', 'test input 2'], model=DEFAULT_EMBEDDING_MODEL + ) + + +@pytest.mark.asyncio +async def test_openai_embedder_with_custom_client( + mocker: MockerFixture, + mock_embedding_values: list[float], +) -> None: + """Test using a custom OpenAI client""" + mock_client = mocker.AsyncMock() + mock_response = mocker.MagicMock() + mock_response.data = [mocker.MagicMock()] + mock_response.data[0].embedding = mock_embedding_values + mock_client.embeddings.create.return_value = mock_response + + # Test with default config to ensure it uses default model + embedder = OpenAIEmbedder(config=OpenAIEmbedderConfig(), client=mock_client) + + result = await embedder.create('test input') + + # Check client was used + mock_client.embeddings.create.assert_called_once_with( + input='test input', model=DEFAULT_EMBEDDING_MODEL + ) + assert len(result) == EMBEDDING_DIM + + +@pytest.mark.asyncio +async def test_openai_embedder_default_config( + mocker: MockerFixture, + mock_embedding_values: list[float], +) -> None: + """Test that the default configuration works as expected""" + mock_openai = mocker.patch('graphiti_core.embedder.openai.AsyncOpenAI') + mock_client = mocker.AsyncMock() + mock_response = mocker.MagicMock() + mock_response.data = [mocker.MagicMock()] + mock_response.data[0].embedding = mock_embedding_values + mock_client.embeddings.create.return_value = mock_response + mock_openai.return_value = mock_client + + # Create with completely default config + embedder = OpenAIEmbedder() + + result = await embedder.create('test input') + + # Verify default model is used + mock_client.embeddings.create.assert_called_once_with( + input='test input', model=DEFAULT_EMBEDDING_MODEL + ) + assert len(result) == EMBEDDING_DIM + + +@pytest.mark.asyncio +async def test_openai_embedder_with_env_api_key( + mocker: MockerFixture, + mock_embedding_values: list[float], +) -> None: + """Test that the embedder falls back to environment variables for API key""" + # Mock the environment variable being set + env_api_key = 'env_test_key' + mocker.patch.dict('os.environ', {'OPENAI_API_KEY': env_api_key}) + + # Import the actual OpenAI client here + from openai import AsyncOpenAI + + # Create a direct instance of OpenAI client to verify env var behavior + real_client = AsyncOpenAI(api_key=None) + + # Verify the client picked up the env var + assert real_client.api_key == env_api_key + + # Now patch the embeddings.create method to avoid actual API calls + mock_create = mocker.AsyncMock() + mock_response = mocker.MagicMock() + mock_response.data = [mocker.MagicMock()] + mock_response.data[0].embedding = mock_embedding_values + mock_create.return_value = mock_response + mocker.patch.object(real_client.embeddings, 'create', mock_create) + + # Patch the AsyncOpenAI constructor to return our real client with mocked create method + mocker.patch('graphiti_core.embedder.openai.AsyncOpenAI', return_value=real_client) + + # Create the embedder with None api_key + embedder = OpenAIEmbedder(config=OpenAIEmbedderConfig(api_key=None)) + + # Verify the embedder's client has the correct API key + assert embedder.client.api_key == env_api_key + + # Test that it works when creating embeddings + await embedder.create('test input') + + # Verify the API call was made with the right parameters + mock_create.assert_called_once_with(input='test input', model=DEFAULT_EMBEDDING_MODEL) diff --git a/tests/embedder/test_voyage.py b/tests/embedder/test_voyage.py new file mode 100644 index 00000000..4ee0e373 --- /dev/null +++ b/tests/embedder/test_voyage.py @@ -0,0 +1,181 @@ +""" +Copyright 2024, Zep Software, Inc. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +""" + +# Running tests: poetry run pytest -xvs tests/embedder/test_voyage.py +# Running tests with coverage: poetry run pytest -xvs tests/embedder/test_voyage.py --cov=graphiti_core.embedder.voyage --cov-report=term-missing + +from typing import NamedTuple + +import pytest +from pytest_mock import MockerFixture + +from graphiti_core.embedder.client import EMBEDDING_DIM +from graphiti_core.embedder.voyage import ( + DEFAULT_EMBEDDING_MODEL, + VoyageAIEmbedder, + VoyageAIEmbedderConfig, +) + + +class EmbeddingResponse(NamedTuple): + embeddings: list[list[float]] + + +@pytest.mark.asyncio +async def test_voyageai_embedder_initialization(mocker: MockerFixture) -> None: + """Test that VoyageAIEmbedder initializes correctly""" + mock_voyage = mocker.patch('graphiti_core.embedder.voyage.voyageai') + mock_client = mocker.AsyncMock() + mock_voyage.AsyncClient.return_value = mock_client + + # Create embedder + embedder = VoyageAIEmbedder(config=VoyageAIEmbedderConfig(api_key='test_key')) + + # Verify client initialization + mock_voyage.AsyncClient.assert_called_once_with(api_key='test_key') + assert embedder.config.embedding_model == DEFAULT_EMBEDDING_MODEL + assert embedder.config.embedding_dim == EMBEDDING_DIM + + +@pytest.mark.asyncio +async def test_voyageai_embedder_custom_config(mocker: MockerFixture) -> None: + """Test VoyageAIEmbedder with custom configuration""" + mock_voyage = mocker.patch('graphiti_core.embedder.voyage.voyageai') + mock_client = mocker.AsyncMock() + mock_voyage.AsyncClient.return_value = mock_client + + # Create custom config + custom_config = VoyageAIEmbedderConfig( + api_key='test_key', + embedding_model='custom-model', + embedding_dim=512, + ) + + # Create embedder with custom config + embedder = VoyageAIEmbedder(config=custom_config) + + # Verify config is used + assert embedder.config.embedding_model == 'custom-model' + assert embedder.config.embedding_dim == 512 + + +@pytest.mark.asyncio +async def test_voyageai_embedder_create_with_string( + mocker: MockerFixture, + mock_embedding_values: list[float], +) -> None: + """Test creating embeddings with a string input""" + mock_voyage = mocker.patch('graphiti_core.embedder.voyage.voyageai') + mock_client = mocker.AsyncMock() + mock_voyage.AsyncClient.return_value = mock_client + + # Create mock response + embeddings: list[list[float]] = [mock_embedding_values] + mock_response = EmbeddingResponse(embeddings=embeddings) + mock_client.embed.return_value = mock_response + + # Create embedder + embedder = VoyageAIEmbedder(config=VoyageAIEmbedderConfig(api_key='test_key')) + + # Call create method + result = await embedder.create('test input') + + # Verify API call + mock_client.embed.assert_called_once_with(['test input'], model=DEFAULT_EMBEDDING_MODEL) + + # Verify result + assert len(result) == EMBEDDING_DIM + assert all(x == 0.1 for x in result) + + +@pytest.mark.asyncio +async def test_voyageai_embedder_create_with_list( + mocker: MockerFixture, + mock_embedding_values: list[float], +) -> None: + """Test creating embeddings with a list input""" + mock_voyage = mocker.patch('graphiti_core.embedder.voyage.voyageai') + mock_client = mocker.AsyncMock() + mock_voyage.AsyncClient.return_value = mock_client + + # Create mock response + embeddings: list[list[float]] = [mock_embedding_values] + mock_response = EmbeddingResponse(embeddings=embeddings) + mock_client.embed.return_value = mock_response + + # Create embedder + embedder = VoyageAIEmbedder( + config=VoyageAIEmbedderConfig( + api_key='test_key', + embedding_model='custom-model', + ) + ) + + # Call create method with list + result = await embedder.create(['input1', 'input2']) + + # Verify API call with correct inputs + mock_client.embed.assert_called_once_with(['input1', 'input2'], model='custom-model') + + # Verify result + assert len(result) == EMBEDDING_DIM + assert all(x == 0.1 for x in result) + + +@pytest.mark.asyncio +async def test_voyageai_embedder_with_empty_input(mocker: MockerFixture) -> None: + """Test behavior with empty input""" + mock_voyage = mocker.patch('graphiti_core.embedder.voyage.voyageai') + mock_client = mocker.AsyncMock() + mock_voyage.AsyncClient.return_value = mock_client + + # Create embedder + embedder = VoyageAIEmbedder(config=VoyageAIEmbedderConfig(api_key='test_key')) + + # Call create method with empty list + result = await embedder.create([]) + + # Should return empty list without API call + assert result == [] + mock_client.embed.assert_not_called() + + +@pytest.mark.asyncio +async def test_voyageai_embedder_with_mixed_types( + mocker: MockerFixture, + mock_embedding_values: list[float], +) -> None: + """Test handling of mixed input types""" + mock_voyage = mocker.patch('graphiti_core.embedder.voyage.voyageai') + mock_client = mocker.AsyncMock() + mock_voyage.AsyncClient.return_value = mock_client + + # Create mock response + embeddings: list[list[float]] = [mock_embedding_values] + mock_response = EmbeddingResponse(embeddings=embeddings) + mock_client.embed.return_value = mock_response + + # Create embedder + embedder = VoyageAIEmbedder(config=VoyageAIEmbedderConfig(api_key='test_key')) + + # Call create method with mixed types + result = await embedder.create([123, 'text', None, '']) # type: ignore[arg-type] + + # Should filter out None and empty strings + mock_client.embed.assert_called_once_with(['123', 'text'], model=DEFAULT_EMBEDDING_MODEL) + + # Verify result + assert len(result) == EMBEDDING_DIM From 8c2f2341d5abc4333ba293081143de97cf759e7a Mon Sep 17 00:00:00 2001 From: Evan Date: Thu, 17 Apr 2025 21:36:07 -0600 Subject: [PATCH 04/11] rename test files to prevent name collision errors for tests in other dirs --- tests/embedder/{test_client.py => test_embedder_client.py} | 0 tests/embedder/{test_gemini.py => test_embedder_gemini.py} | 0 tests/embedder/{test_openai.py => test_embedder_openai.py} | 0 tests/embedder/{test_voyage.py => test_embedder_voyage.py} | 0 4 files changed, 0 insertions(+), 0 deletions(-) rename tests/embedder/{test_client.py => test_embedder_client.py} (100%) rename tests/embedder/{test_gemini.py => test_embedder_gemini.py} (100%) rename tests/embedder/{test_openai.py => test_embedder_openai.py} (100%) rename tests/embedder/{test_voyage.py => test_embedder_voyage.py} (100%) diff --git a/tests/embedder/test_client.py b/tests/embedder/test_embedder_client.py similarity index 100% rename from tests/embedder/test_client.py rename to tests/embedder/test_embedder_client.py diff --git a/tests/embedder/test_gemini.py b/tests/embedder/test_embedder_gemini.py similarity index 100% rename from tests/embedder/test_gemini.py rename to tests/embedder/test_embedder_gemini.py diff --git a/tests/embedder/test_openai.py b/tests/embedder/test_embedder_openai.py similarity index 100% rename from tests/embedder/test_openai.py rename to tests/embedder/test_embedder_openai.py diff --git a/tests/embedder/test_voyage.py b/tests/embedder/test_embedder_voyage.py similarity index 100% rename from tests/embedder/test_voyage.py rename to tests/embedder/test_embedder_voyage.py From 680e7360cd60e88ef78363b4cf82fd706dbecfbb Mon Sep 17 00:00:00 2001 From: Evan Date: Thu, 17 Apr 2025 22:12:09 -0600 Subject: [PATCH 05/11] improve env var handling --- tests/embedder/test_embedder_openai.py | 28 ++++++++++++++++++++------ 1 file changed, 22 insertions(+), 6 deletions(-) diff --git a/tests/embedder/test_embedder_openai.py b/tests/embedder/test_embedder_openai.py index 0967447c..4007cb14 100644 --- a/tests/embedder/test_embedder_openai.py +++ b/tests/embedder/test_embedder_openai.py @@ -17,6 +17,7 @@ # Running tests: poetry run pytest -xvs tests/embedder/test_openai.py # Running tests with coverage: poetry run pytest -xvs tests/embedder/test_openai.py --cov=graphiti_core.embedder.openai --cov-report=term-missing +import os from unittest.mock import AsyncMock import pytest @@ -40,6 +41,24 @@ def mock_openai_client(mocker: MockerFixture, mock_embedding_values: list[float] return mock_client +@pytest.fixture +def mock_env_api_key(): + """Fixture to safely manage environment variables for testing. + + This fixture ensures thread-safe environment variable handling by: + 1. Storing the original environment + 2. Setting the test environment variable + 3. Yielding the test value + 4. Restoring the original environment + """ + original_env = dict(os.environ) + test_key = 'env_test_key' + os.environ['OPENAI_API_KEY'] = test_key + yield test_key + os.environ.clear() + os.environ.update(original_env) + + @pytest.mark.asyncio async def test_openai_embedder_create_with_string( mocker: MockerFixture, @@ -153,12 +172,9 @@ async def test_openai_embedder_default_config( async def test_openai_embedder_with_env_api_key( mocker: MockerFixture, mock_embedding_values: list[float], + mock_env_api_key: str, ) -> None: """Test that the embedder falls back to environment variables for API key""" - # Mock the environment variable being set - env_api_key = 'env_test_key' - mocker.patch.dict('os.environ', {'OPENAI_API_KEY': env_api_key}) - # Import the actual OpenAI client here from openai import AsyncOpenAI @@ -166,7 +182,7 @@ async def test_openai_embedder_with_env_api_key( real_client = AsyncOpenAI(api_key=None) # Verify the client picked up the env var - assert real_client.api_key == env_api_key + assert real_client.api_key == mock_env_api_key # Now patch the embeddings.create method to avoid actual API calls mock_create = mocker.AsyncMock() @@ -183,7 +199,7 @@ async def test_openai_embedder_with_env_api_key( embedder = OpenAIEmbedder(config=OpenAIEmbedderConfig(api_key=None)) # Verify the embedder's client has the correct API key - assert embedder.client.api_key == env_api_key + assert embedder.client.api_key == mock_env_api_key # Test that it works when creating embeddings await embedder.create('test input') From a21bcd58cedad74cd3e3f855000877b8eb6e48df Mon Sep 17 00:00:00 2001 From: Evan Date: Thu, 17 Apr 2025 22:12:15 -0600 Subject: [PATCH 06/11] improve test assertions and docs --- tests/embedder/test_embedder_gemini.py | 6 +++++- tests/embedder/test_embedder_voyage.py | 11 ++++++++--- 2 files changed, 13 insertions(+), 4 deletions(-) diff --git a/tests/embedder/test_embedder_gemini.py b/tests/embedder/test_embedder_gemini.py index 343cde86..75f8fa20 100644 --- a/tests/embedder/test_embedder_gemini.py +++ b/tests/embedder/test_embedder_gemini.py @@ -109,7 +109,11 @@ async def test_gemini_embedder_create_with_list( mocker: MockerFixture, mock_embedding_values: list[float], ) -> None: - """Test creating embeddings with a list input""" + """Test creating embeddings with a list input + + Note: The input list is intentionally wrapped in another list as the Gemini API's + embed_content method expects a list of contents, even for single inputs. + """ mock_genai = mocker.patch('graphiti_core.embedder.gemini.genai') mock_client = mocker.MagicMock() mock_genai.Client.return_value = mock_client diff --git a/tests/embedder/test_embedder_voyage.py b/tests/embedder/test_embedder_voyage.py index 4ee0e373..bdf6bf28 100644 --- a/tests/embedder/test_embedder_voyage.py +++ b/tests/embedder/test_embedder_voyage.py @@ -98,7 +98,7 @@ async def test_voyageai_embedder_create_with_string( # Verify result assert len(result) == EMBEDDING_DIM - assert all(x == 0.1 for x in result) + assert result == mock_embedding_values @pytest.mark.asyncio @@ -132,7 +132,7 @@ async def test_voyageai_embedder_create_with_list( # Verify result assert len(result) == EMBEDDING_DIM - assert all(x == 0.1 for x in result) + assert result == mock_embedding_values @pytest.mark.asyncio @@ -158,7 +158,11 @@ async def test_voyageai_embedder_with_mixed_types( mocker: MockerFixture, mock_embedding_values: list[float], ) -> None: - """Test handling of mixed input types""" + """Test handling of mixed input types + + Note: All non-string inputs are converted to strings (e.g., integers like 123 become '123'). + None values and empty strings are filtered out. + """ mock_voyage = mocker.patch('graphiti_core.embedder.voyage.voyageai') mock_client = mocker.AsyncMock() mock_voyage.AsyncClient.return_value = mock_client @@ -179,3 +183,4 @@ async def test_voyageai_embedder_with_mixed_types( # Verify result assert len(result) == EMBEDDING_DIM + assert result == mock_embedding_values From 29d1a949400905986e0396854cf0463fcd818812 Mon Sep 17 00:00:00 2001 From: Evan Schultz Date: Thu, 17 Apr 2025 22:19:47 -0600 Subject: [PATCH 07/11] update test command comment Co-authored-by: ellipsis-dev[bot] <65095814+ellipsis-dev[bot]@users.noreply.github.com> --- tests/embedder/test_embedder_openai.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/embedder/test_embedder_openai.py b/tests/embedder/test_embedder_openai.py index 4007cb14..94a3c459 100644 --- a/tests/embedder/test_embedder_openai.py +++ b/tests/embedder/test_embedder_openai.py @@ -14,7 +14,7 @@ limitations under the License. """ -# Running tests: poetry run pytest -xvs tests/embedder/test_openai.py +# Running tests: poetry run pytest -xvs tests/embedder/test_embedder_openai.py # Running tests with coverage: poetry run pytest -xvs tests/embedder/test_openai.py --cov=graphiti_core.embedder.openai --cov-report=term-missing import os From 098affde9408a1373b8ebb7eb740ea2b80f23f22 Mon Sep 17 00:00:00 2001 From: Evan Schultz Date: Thu, 17 Apr 2025 22:20:09 -0600 Subject: [PATCH 08/11] update test command comment in tests/embedder/test_embedder_voyage.py Co-authored-by: ellipsis-dev[bot] <65095814+ellipsis-dev[bot]@users.noreply.github.com> --- tests/embedder/test_embedder_voyage.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/embedder/test_embedder_voyage.py b/tests/embedder/test_embedder_voyage.py index bdf6bf28..5b51606e 100644 --- a/tests/embedder/test_embedder_voyage.py +++ b/tests/embedder/test_embedder_voyage.py @@ -14,7 +14,7 @@ limitations under the License. """ -# Running tests: poetry run pytest -xvs tests/embedder/test_voyage.py +# Running tests: poetry run pytest -xvs tests/embedder/test_embedder_voyage.py # Running tests with coverage: poetry run pytest -xvs tests/embedder/test_voyage.py --cov=graphiti_core.embedder.voyage --cov-report=term-missing from typing import NamedTuple From 02239c42143ba6d682fcc59ebafc606cd59700de Mon Sep 17 00:00:00 2001 From: Evan Schultz Date: Thu, 17 Apr 2025 22:20:43 -0600 Subject: [PATCH 09/11] update .coveragerc Co-authored-by: ellipsis-dev[bot] <65095814+ellipsis-dev[bot]@users.noreply.github.com> --- .coveragerc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.coveragerc b/.coveragerc index 67f23c78..ba20bf35 100644 --- a/.coveragerc +++ b/.coveragerc @@ -2,6 +2,6 @@ exclude_lines = pragma: no cover def __repr__ - if __name__ == .__main__.: + if __name__ == "__main__": @abstractmethod pass \ No newline at end of file From 6169b99698bb4788cfcb979f6f4de31839c2ecd2 Mon Sep 17 00:00:00 2001 From: Evan Schultz Date: Thu, 17 Apr 2025 22:20:54 -0600 Subject: [PATCH 10/11] update pytest.ini Co-authored-by: ellipsis-dev[bot] <65095814+ellipsis-dev[bot]@users.noreply.github.com> --- pytest.ini | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pytest.ini b/pytest.ini index 68d8b2fc..18184c61 100644 --- a/pytest.ini +++ b/pytest.ini @@ -18,7 +18,7 @@ filterwarnings = exclude_lines = pragma: no cover def __repr__ - if __name__ == .__main__.: + if __name__ == "__main__": @abstractmethod pass From dcf12c025bb9f9c04bc882249b69777919ae307e Mon Sep 17 00:00:00 2001 From: Evan Schultz Date: Thu, 17 Apr 2025 22:21:09 -0600 Subject: [PATCH 11/11] update test command in tests/embedder/test_embedder_gemini.py Co-authored-by: ellipsis-dev[bot] <65095814+ellipsis-dev[bot]@users.noreply.github.com> --- tests/embedder/test_embedder_gemini.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/embedder/test_embedder_gemini.py b/tests/embedder/test_embedder_gemini.py index 75f8fa20..318869d7 100644 --- a/tests/embedder/test_embedder_gemini.py +++ b/tests/embedder/test_embedder_gemini.py @@ -14,7 +14,7 @@ limitations under the License. """ -# Running tests: poetry run pytest -xvs tests/embedder/test_gemini.py +# Running tests: poetry run pytest -xvs tests/embedder/test_embedder_gemini.py # Running tests with coverage: poetry run pytest -xvs tests/embedder/test_gemini.py --cov=graphiti_core.embedder.gemini --cov-report=term-missing import pytest