diff --git a/ionic_langchain/prompt.py b/ionic_langchain/prompt.py index 9159d3d..c71bce3 100644 --- a/ionic_langchain/prompt.py +++ b/ionic_langchain/prompt.py @@ -1,7 +1,9 @@ TOOL_PROMPT = """ - Ionic is an ecommerce shopping tool. Assistant uses the Ionic Commerce Shopping Tool to find, discover, and compare products from thousands of online retailers. Assistant should use the tool when the user is looking for a product recommendation or trying to find a specific product. + Ionic is an ecommerce shopping tool. Assistant uses the Ionic Commerce Shopping Tool to find, discover, and compare products from thousands of online retailers. Assistant should use the tool when the user is looking for a product recommendation or trying to find a specific product. - Ionic Tool input is a stringified List of each search value, for example `['coffee beans']` or `['coffee beans', 'coffee grinder']` and returns results for each search. + The user can specify the number of results, minimum price, and maximum price that they want to see results for. + Ionic Tool input is a comma seperated string of values: query (required), number of results (default to 5), minimum price in cents, and maximum price in cents. For example `coffee beans, 5, 500, 1000` or `coffee beans, 5` or `coffee beans, 5, 500` or `coffee beans, 5, , 1000`. If the user has not specified how many results they want, use 5 as the default. + Convert the price to cents before passing to the tool. For example, if the user asks for a product between $5 and $10, you would pass `500` and `1000` to the tool. If the human asks for what items are needed for an activity or project, you can provide product recommendations alongside your response about the required items instead of waiting for the user to follow up after. For example, if the user asks 'what do I need to go camping?', you would compile your list of product requirements for camping (e.g. 'tent', 'stove', 'sleeping bag'), and before responding to user you might use Ionic Tool for diff --git a/ionic_langchain/tool.py b/ionic_langchain/tool.py index b0acd09..2bc2b0b 100644 --- a/ionic_langchain/tool.py +++ b/ionic_langchain/tool.py @@ -1,10 +1,12 @@ +from __future__ import annotations + import dataclasses -from typing import Any, Optional +from typing import Any, Optional, Sequence from ionic import Ionic as IonicSDK -from ionic.models.components import QueryAPIRequest, Query -from ionic.models.operations import QuerySecurity, QueryResponse -from langchain.tools import Tool +from ionic.models.components import Query as SDKQuery, QueryAPIRequest +from ionic.models.operations import QueryResponse, QuerySecurity +from langchain_core.tools import Tool from ionic_langchain.prompt import TOOL_PROMPT @@ -18,20 +20,53 @@ def __init__(self, sdk: Optional[IonicSDK] = None): else: self._sdk = IonicSDK() - def query(self, queries: str) -> list: - """ - FIXME: handle non-200 responses - TODO: better typing in response - """ + def query( + self, + query_input: str, + ) -> Sequence[dict[str, Any]]: + if not query_input: + raise ValueError("query must not be empty") + + query, num_results, min_price, max_price = self.gen_query_request(query_input) request = QueryAPIRequest( - queries=[Query(query=query) for query in queries.split(", ")], + query=SDKQuery( + query=query, + num_results=num_results, + min_price=min_price, + max_price=max_price, + ) ) response: QueryResponse = self._sdk.query( request=request, security=QuerySecurity(), ) - return [dataclasses.asdict(r) for r in response.query_api_response.results] + return [ + dataclasses.asdict(result) for result in response.query_api_response.results + ] + + def _parse_number(self, value: str) -> int | None: + return int(value) if value and int(value) >= 0 else None + + def gen_query_request(self, query_input: str) -> tuple[str, int | None, int | None, int | None]: + if not query_input: + raise ValueError("query must not be empty") + + split_query = query_input.split(",") + len4_query = split_query + [None] * (4 - len(split_query)) # pad with None + + query, num_results, min_price, max_price, *rest = [ # *rest ignores extra values + item.strip() if item is not None else None for item in len4_query + ] + if not query: + raise ValueError("query must not be empty") + + return ( + str(query), + self._parse_number(num_results), + self._parse_number(min_price), + self._parse_number(max_price), + ) class IonicTool: @@ -44,7 +79,11 @@ def __init__(self, ionic: Optional[Ionic] = None): self._ionic = Ionic() def tool(self) -> Tool: - return Tool( + """ + - https://github.com/langchain-ai/langchain/issues/4197 + - https://python.langchain.com/docs/modules/agents/tools/multi_input_tool + """ + return Tool.from_function( func=self._ionic.query, name="Ionic Commerce Shopping Tool", description=TOOL_PROMPT, diff --git a/poetry.lock b/poetry.lock index 12f3a03..dd2237d 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1,4 +1,4 @@ -# This file is automatically @generated by Poetry 1.5.1 and should not be changed by hand. +# This file is automatically @generated by Poetry 1.7.1 and should not be changed by hand. [[package]] name = "aiohttp" @@ -175,6 +175,52 @@ docs = ["furo", "myst-parser", "sphinx", "sphinx-notfound-page", "sphinxcontrib- tests = ["attrs[tests-no-zope]", "zope-interface"] tests-no-zope = ["cloudpickle", "hypothesis", "mypy (>=1.1.1)", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins", "pytest-xdist[psutil]"] +[[package]] +name = "black" +version = "23.12.1" +description = "The uncompromising code formatter." +optional = false +python-versions = ">=3.8" +files = [ + {file = "black-23.12.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:e0aaf6041986767a5e0ce663c7a2f0e9eaf21e6ff87a5f95cbf3675bfd4c41d2"}, + {file = "black-23.12.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:c88b3711d12905b74206227109272673edce0cb29f27e1385f33b0163c414bba"}, + {file = "black-23.12.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a920b569dc6b3472513ba6ddea21f440d4b4c699494d2e972a1753cdc25df7b0"}, + {file = "black-23.12.1-cp310-cp310-win_amd64.whl", hash = "sha256:3fa4be75ef2a6b96ea8d92b1587dd8cb3a35c7e3d51f0738ced0781c3aa3a5a3"}, + {file = "black-23.12.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:8d4df77958a622f9b5a4c96edb4b8c0034f8434032ab11077ec6c56ae9f384ba"}, + {file = "black-23.12.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:602cfb1196dc692424c70b6507593a2b29aac0547c1be9a1d1365f0d964c353b"}, + {file = "black-23.12.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9c4352800f14be5b4864016882cdba10755bd50805c95f728011bcb47a4afd59"}, + {file = "black-23.12.1-cp311-cp311-win_amd64.whl", hash = "sha256:0808494f2b2df923ffc5723ed3c7b096bd76341f6213989759287611e9837d50"}, + {file = "black-23.12.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:25e57fd232a6d6ff3f4478a6fd0580838e47c93c83eaf1ccc92d4faf27112c4e"}, + {file = "black-23.12.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:2d9e13db441c509a3763a7a3d9a49ccc1b4e974a47be4e08ade2a228876500ec"}, + {file = "black-23.12.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6d1bd9c210f8b109b1762ec9fd36592fdd528485aadb3f5849b2740ef17e674e"}, + {file = "black-23.12.1-cp312-cp312-win_amd64.whl", hash = "sha256:ae76c22bde5cbb6bfd211ec343ded2163bba7883c7bc77f6b756a1049436fbb9"}, + {file = "black-23.12.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:1fa88a0f74e50e4487477bc0bb900c6781dbddfdfa32691e780bf854c3b4a47f"}, + {file = "black-23.12.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:a4d6a9668e45ad99d2f8ec70d5c8c04ef4f32f648ef39048d010b0689832ec6d"}, + {file = "black-23.12.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b18fb2ae6c4bb63eebe5be6bd869ba2f14fd0259bda7d18a46b764d8fb86298a"}, + {file = "black-23.12.1-cp38-cp38-win_amd64.whl", hash = "sha256:c04b6d9d20e9c13f43eee8ea87d44156b8505ca8a3c878773f68b4e4812a421e"}, + {file = "black-23.12.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:3e1b38b3135fd4c025c28c55ddfc236b05af657828a8a6abe5deec419a0b7055"}, + {file = "black-23.12.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:4f0031eaa7b921db76decd73636ef3a12c942ed367d8c3841a0739412b260a54"}, + {file = "black-23.12.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:97e56155c6b737854e60a9ab1c598ff2533d57e7506d97af5481141671abf3ea"}, + {file = "black-23.12.1-cp39-cp39-win_amd64.whl", hash = "sha256:dd15245c8b68fe2b6bd0f32c1556509d11bb33aec9b5d0866dd8e2ed3dba09c2"}, + {file = "black-23.12.1-py3-none-any.whl", hash = "sha256:78baad24af0f033958cad29731e27363183e140962595def56423e626f4bee3e"}, + {file = "black-23.12.1.tar.gz", hash = "sha256:4ce3ef14ebe8d9509188014d96af1c456a910d5b5cbf434a09fef7e024b3d0d5"}, +] + +[package.dependencies] +click = ">=8.0.0" +mypy-extensions = ">=0.4.3" +packaging = ">=22.0" +pathspec = ">=0.9.0" +platformdirs = ">=2" +tomli = {version = ">=1.1.0", markers = "python_version < \"3.11\""} +typing-extensions = {version = ">=4.0.1", markers = "python_version < \"3.11\""} + +[package.extras] +colorama = ["colorama (>=0.4.3)"] +d = ["aiohttp (>=3.7.4)", "aiohttp (>=3.7.4,!=3.9.0)"] +jupyter = ["ipython (>=7.8.0)", "tokenize-rt (>=3.2.0)"] +uvloop = ["uvloop (>=0.15.2)"] + [[package]] name = "certifi" version = "2023.11.17" @@ -285,6 +331,20 @@ files = [ {file = "charset_normalizer-3.3.2-py3-none-any.whl", hash = "sha256:3e4d1f6587322d2788836a99c69062fbb091331ec940e02d12d179c1d53e25fc"}, ] +[[package]] +name = "click" +version = "8.1.7" +description = "Composable command line interface toolkit" +optional = false +python-versions = ">=3.7" +files = [ + {file = "click-8.1.7-py3-none-any.whl", hash = "sha256:ae74fb96c20a0277a1d615f1e4d73c8414f5a98db8b799a7931d1582f3390c28"}, + {file = "click-8.1.7.tar.gz", hash = "sha256:ca9853ad459e787e2192211578cc907e7594e294c7ccc834310722b41b9ca6de"}, +] + +[package.dependencies] +colorama = {version = "*", markers = "platform_system == \"Windows\""} + [[package]] name = "colorama" version = "0.4.6" @@ -506,13 +566,13 @@ files = [ [[package]] name = "ionic-api-sdk" -version = "0.5.2" +version = "0.7.0" description = "Python Client SDK" optional = false python-versions = ">=3.8" files = [ - {file = "Ionic-API-SDK-0.5.2.tar.gz", hash = "sha256:c52df7e680628bafb685b8af8f1332eebaa6d3c1df749a82fe6930285a0efc65"}, - {file = "Ionic_API_SDK-0.5.2-py3-none-any.whl", hash = "sha256:9508ba1cb63ff8f366acac3455f73fd3a4d0afde639b48f24559b20e3f1aea15"}, + {file = "Ionic-API-SDK-0.7.0.tar.gz", hash = "sha256:c44d713152b26e659f278e07587e072fb9a3b1393f6b21af9ba763a2485a6ee6"}, + {file = "Ionic_API_SDK-0.7.0-py3-none-any.whl", hash = "sha256:25a4a3586cf7ba4337c48d17808bceb71b8ba41f329f2429090ef0af8dfe100f"}, ] [package.dependencies] @@ -837,6 +897,43 @@ files = [ {file = "packaging-23.2.tar.gz", hash = "sha256:048fb0e9405036518eaaf48a55953c750c11e1a1b68e0dd1a9d62ed0c092cfc5"}, ] +[[package]] +name = "pastel" +version = "0.2.1" +description = "Bring colors to your terminal." +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" +files = [ + {file = "pastel-0.2.1-py2.py3-none-any.whl", hash = "sha256:4349225fcdf6c2bb34d483e523475de5bb04a5c10ef711263452cb37d7dd4364"}, + {file = "pastel-0.2.1.tar.gz", hash = "sha256:e6581ac04e973cac858828c6202c1e1e81fee1dc7de7683f3e1ffe0bfd8a573d"}, +] + +[[package]] +name = "pathspec" +version = "0.12.1" +description = "Utility library for gitignore style pattern matching of file paths." +optional = false +python-versions = ">=3.8" +files = [ + {file = "pathspec-0.12.1-py3-none-any.whl", hash = "sha256:a0d503e138a4c123b27490a4f7beda6a01c6f288df0e4a8b79c7eb0dc7b4cc08"}, + {file = "pathspec-0.12.1.tar.gz", hash = "sha256:a482d51503a1ab33b1c67a6c3813a26953dbdc71c31dacaef9a838c4e29f5712"}, +] + +[[package]] +name = "platformdirs" +version = "4.1.0" +description = "A small Python package for determining appropriate platform-specific dirs, e.g. a \"user data dir\"." +optional = false +python-versions = ">=3.8" +files = [ + {file = "platformdirs-4.1.0-py3-none-any.whl", hash = "sha256:11c8f37bcca40db96d8144522d925583bdb7a31f7b0e37e3ed4318400a8e2380"}, + {file = "platformdirs-4.1.0.tar.gz", hash = "sha256:906d548203468492d432bcb294d4bc2fff751bf84971fbb2c10918cc206ee420"}, +] + +[package.extras] +docs = ["furo (>=2023.7.26)", "proselint (>=0.13)", "sphinx (>=7.1.1)", "sphinx-autodoc-typehints (>=1.24)"] +test = ["appdirs (==1.4.4)", "covdefaults (>=2.3)", "pytest (>=7.4)", "pytest-cov (>=4.1)", "pytest-mock (>=3.11.1)"] + [[package]] name = "pluggy" version = "1.3.0" @@ -852,6 +949,24 @@ files = [ dev = ["pre-commit", "tox"] testing = ["pytest", "pytest-benchmark"] +[[package]] +name = "poethepoet" +version = "0.24.4" +description = "A task runner that works well with poetry." +optional = false +python-versions = ">=3.8" +files = [ + {file = "poethepoet-0.24.4-py3-none-any.whl", hash = "sha256:fb4ea35d7f40fe2081ea917d2e4102e2310fda2cde78974050ca83896e229075"}, + {file = "poethepoet-0.24.4.tar.gz", hash = "sha256:ff4220843a87c888cbcb5312c8905214701d0af60ac7271795baa8369b428fef"}, +] + +[package.dependencies] +pastel = ">=0.2.1,<0.3.0" +tomli = ">=1.2.2" + +[package.extras] +poetry-plugin = ["poetry (>=1.0,<2.0)"] + [[package]] name = "pydantic" version = "2.5.2" @@ -1036,6 +1151,7 @@ files = [ {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"}, @@ -1043,8 +1159,16 @@ files = [ {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"}, @@ -1061,6 +1185,7 @@ files = [ {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"}, @@ -1068,6 +1193,7 @@ files = [ {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"}, @@ -1175,7 +1301,7 @@ files = [ ] [package.dependencies] -greenlet = {version = "!=0.4.17", markers = "platform_machine == \"win32\" or platform_machine == \"WIN32\" or platform_machine == \"AMD64\" or platform_machine == \"amd64\" or platform_machine == \"x86_64\" or platform_machine == \"ppc64le\" or platform_machine == \"aarch64\""} +greenlet = {version = "!=0.4.17", markers = "platform_machine == \"aarch64\" or platform_machine == \"ppc64le\" or platform_machine == \"x86_64\" or platform_machine == \"amd64\" or platform_machine == \"AMD64\" or platform_machine == \"win32\" or platform_machine == \"WIN32\""} typing-extensions = ">=4.2.0" [package.extras] @@ -1376,4 +1502,4 @@ multidict = ">=4.0" [metadata] lock-version = "2.0" python-versions = "^3.8.10" -content-hash = "d895f60bb14284bb061c4ff08b5316f6a34bc1bb9bedcb0c226e289cf1a7c20b" +content-hash = "5d060216469a7bb0354b4d326a2be986a2fa9e02325623258ea299b5138bcf2a" diff --git a/pyproject.toml b/pyproject.toml index 22dad75..f4d6414 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -9,9 +9,19 @@ packages = [{include = "ionic_langchain"}] [tool.poetry.dependencies] python = "^3.8.10" langchain = "~0.0.352" -ionic-api-sdk = "0.5.2" +ionic-api-sdk = "^0.7.0" pytest = "^7.4.4" +[tool.poetry.group.dev.dependencies] +pytest = "^7.4.3" +black = "^23.12.0" +poethepoet = "^0.24.4" + +[tool.poe.tasks.fmt] +cmd = "black ionic_langchain tests" + +[tool.poe.tasks.test] +cmd = "pytest tests/" [build-system] requires = ["poetry-core"] diff --git a/tests/test_get_query_request.py b/tests/test_get_query_request.py new file mode 100644 index 0000000..8083739 --- /dev/null +++ b/tests/test_get_query_request.py @@ -0,0 +1,60 @@ +import pytest + +from ionic_langchain.tool import Ionic + + +def test_none_input_raises_value_error(): + ionic = Ionic() + with pytest.raises(ValueError): + ionic.gen_query_request(None) + + +def test_only_spaces_input_raises_value_error(): + ionic = Ionic() + with pytest.raises(ValueError): + ionic.gen_query_request(" , , , ") + + +def test_extra_values_ignores_extra_values(): + ionic = Ionic() + result = ionic.gen_query_request("test query, 2, 1, 3, extra, values") + assert result == ("test query", 2, 1, 3) + + +def test_negative_number_values_raises_value_error(): + ionic = Ionic() + result = ionic.gen_query_request("test query, -2, -1, -3") + assert result == ("test query", None, None, None) + + +def test_valid_input_returns_expected_output(): + ionic = Ionic() + result = ionic.gen_query_request("test query, 2, 1, 3") + + assert result == ("test query", 2, 1, 3) + + +def test_missing_values_returns_expected_output(): + ionic = Ionic() + result = ionic.gen_query_request("test query, , , ") + + assert result == ("test query", None, None, None) + + +def test_extra_spaces_returns_expected_output(): + ionic = Ionic() + result = ionic.gen_query_request(" test query , 2 , 1 , 3 ") + + assert result == ("test query", 2, 1, 3) + + +def test_invalid_number_values_raises_value_error(): + ionic = Ionic() + with pytest.raises(ValueError): + ionic.gen_query_request("test query, not a number, not a number, not a number") + + +def test_float_raises_value_error(): + ionic = Ionic() + with pytest.raises(ValueError): + ionic.gen_query_request("test, 1.0, 2.0, 3.0") diff --git a/tests/test_ionic.py b/tests/test_ionic.py deleted file mode 100644 index dbaff34..0000000 --- a/tests/test_ionic.py +++ /dev/null @@ -1,51 +0,0 @@ -import pytest -from ionic import Ionic as IonicSdk -from ionic.models.errors import HTTPValidationError - -from ionic_langchain.tool import Ionic - - -def test_ionic_num_results(): - """ - requires server to be running (i.e. not compatible with CI) - """ - ionic = Ionic( - sdk=IonicSdk(server_url="http://localhost:8080"), - ) - results = ionic.query(queries="Reindeer Jerky, Salmon Jerky") - - assert len(results) == 2, "results are returned for each query" - reindeer_jerky_result = results[0] - assert ( - reindeer_jerky_result["query"]["query"] == "Reindeer Jerky" - ), "query should be included in response object" - assert reindeer_jerky_result["query"]["num_results"] is None - assert reindeer_jerky_result["query"]["max_price"] is None - assert reindeer_jerky_result["query"]["min_price"] is None - assert "products" in reindeer_jerky_result - assert ( - len(reindeer_jerky_result["products"]) == 5 - ), "num_results should be the server default" - - -@pytest.mark.skip("we aren't yet passing in the validated params") -def test_ionic_bad_input(): - """ - requires server to be running - """ - ionic = Ionic( - sdk=IonicSdk( - server_url="http://localhost:8080", - ), - ) - - with pytest.raises(HTTPValidationError) as exc_info: - ionic.query(queries="") - - problems = [det.loc[-1] for det in exc_info.value.detail] - assert len(problems) == 3, "all problems are included in error" - assert sorted(problems) == [ - "max_price", - "min_price", - "num_results", - ] diff --git a/tests/test_ionic_tool.py b/tests/test_ionic_tool.py index ecee650..9a513fc 100644 --- a/tests/test_ionic_tool.py +++ b/tests/test_ionic_tool.py @@ -10,4 +10,4 @@ def test_ionic_tool_is_valid(): try: IonicTool().tool() except Exception: - pytest.fail("unexpected exception %s initializing IonicTool#tool") \ No newline at end of file + pytest.fail("unexpected exception %s initializing IonicTool#tool") diff --git a/tests/test_query.py b/tests/test_query.py new file mode 100644 index 0000000..96ea4d4 --- /dev/null +++ b/tests/test_query.py @@ -0,0 +1,110 @@ +from ionic import Ionic as IonicSdk + +from ionic_langchain.tool import Ionic + + +def test_ionic(): + """ + requires server to be running (i.e. not compatible with CI) + """ + ionic = Ionic( + sdk=IonicSdk(server_url="http://localhost:8080"), + ) + results = ionic.query( + query_input="Reindeer Jerky", + ) + + assert len(results) == 1 + reindeer_jerky_result = results[0] + assert ( + reindeer_jerky_result["query"]["query"] == "Reindeer Jerky" + ), "query should be included in response object" + assert reindeer_jerky_result["query"]["num_results"] == 5 + assert reindeer_jerky_result["query"]["min_price"] is None + assert reindeer_jerky_result["query"]["max_price"] is None + assert "products" in reindeer_jerky_result + assert ( + len(reindeer_jerky_result["products"]) == 5 + ), "num_results should be respected" + + +def test_ionic_num_results(): + """ + requires server to be running (i.e. not compatible with CI) + """ + ionic = Ionic( + sdk=IonicSdk(server_url="http://localhost:8080"), + ) + results = ionic.query( + query_input="Reindeer Jerky, 2", + ) + + assert len(results) == 1 + reindeer_jerky_result = results[0] + assert ( + reindeer_jerky_result["query"]["query"] == "Reindeer Jerky" + ), "query should be included in response object" + assert reindeer_jerky_result["query"]["num_results"] == 2 + assert reindeer_jerky_result["query"]["min_price"] is None + assert reindeer_jerky_result["query"]["max_price"] is None + assert "products" in reindeer_jerky_result + assert ( + len(reindeer_jerky_result["products"]) == 2 + ), "num_results should be respected" + + +def test_ionic_price_range(): + """ + requires server to be running (i.e. not compatible with CI) + """ + ionic = Ionic( + sdk=IonicSdk(server_url="http://localhost:8080"), + ) + results = ionic.query(query_input="Reindeer Jerky, , 1000, 10000") + + assert len(results) == 1 + reindeer_jerky_result = results[0] + assert ( + reindeer_jerky_result["query"]["query"] == "Reindeer Jerky" + ), "query should be included in response object" + assert reindeer_jerky_result["query"]["num_results"] == 5 + assert reindeer_jerky_result["query"]["min_price"] == 1000 + assert reindeer_jerky_result["query"]["max_price"] == 10000 + + +def test_ionic_max_price(): + """ + requires server to be running (i.e. not compatible with CI) + """ + ionic = Ionic( + sdk=IonicSdk(server_url="http://localhost:8080"), + ) + results = ionic.query(query_input="Reindeer Jerky, , , 10000") + + assert len(results) == 1 + reindeer_jerky_result = results[0] + assert ( + reindeer_jerky_result["query"]["query"] == "Reindeer Jerky" + ), "query should be included in response object" + assert reindeer_jerky_result["query"]["num_results"] == 5 + assert reindeer_jerky_result["query"]["min_price"] is None + assert reindeer_jerky_result["query"]["max_price"] == 10000 + + +def test_ionic_min_price(): + """ + requires server to be running (i.e. not compatible with CI) + """ + ionic = Ionic( + sdk=IonicSdk(server_url="http://localhost:8080"), + ) + results = ionic.query(query_input="Reindeer Jerky, , 1000") + + assert len(results) == 1 + reindeer_jerky_result = results[0] + assert ( + reindeer_jerky_result["query"]["query"] == "Reindeer Jerky" + ), "query should be included in response object" + assert reindeer_jerky_result["query"]["num_results"] == 5 + assert reindeer_jerky_result["query"]["min_price"] == 1000 + assert reindeer_jerky_result["query"]["max_price"] is None