diff --git a/poetry.lock b/poetry.lock index 0bd0101..33ca411 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1,4 +1,4 @@ -# This file is automatically @generated by Poetry 2.1.1 and should not be changed by hand. +# This file is automatically @generated by Poetry 2.0.1 and should not be changed by hand. [[package]] name = "alabaster" @@ -25,7 +25,7 @@ files = [ ] [package.extras] -dev = ["backports.zoneinfo ; python_version < \"3.9\"", "freezegun (>=1.0,<2.0)", "jinja2 (>=3.0)", "pytest (>=6.0)", "pytest-cov", "pytz", "setuptools", "tzdata ; sys_platform == \"win32\""] +dev = ["backports.zoneinfo", "freezegun (>=1.0,<2.0)", "jinja2 (>=3.0)", "pytest (>=6.0)", "pytest-cov", "pytz", "setuptools", "tzdata"] [[package]] name = "blinker" @@ -255,7 +255,7 @@ files = [ ] [package.extras] -toml = ["tomli ; python_full_version <= \"3.11.0a6\""] +toml = ["tomli"] [[package]] name = "docutils" @@ -514,6 +514,21 @@ tomli = {version = ">=1", markers = "python_version < \"3.11\""} [package.extras] dev = ["argcomplete", "attrs (>=19.2)", "hypothesis (>=3.56)", "mock", "pygments (>=2.7.2)", "requests", "setuptools", "xmlschema"] +[[package]] +name = "pytest-httpserver" +version = "1.1.2" +description = "pytest-httpserver is a httpserver for pytest" +optional = false +python-versions = ">=3.9" +groups = ["main"] +files = [ + {file = "pytest_httpserver-1.1.2-py3-none-any.whl", hash = "sha256:93009d79574fc982301e8494fdea0884f21bb0caf3bcc719151dfbd1e3a943ea"}, + {file = "pytest_httpserver-1.1.2.tar.gz", hash = "sha256:38d0b726580d05c47cbd0ced1ecb36a51668ef1596cdc6d70a9cfa2b3cc00ebd"}, +] + +[package.dependencies] +Werkzeug = ">=2.0.0" + [[package]] name = "requests" version = "2.32.3" @@ -752,7 +767,7 @@ files = [ ] [package.extras] -brotli = ["brotli (>=1.0.9) ; platform_python_implementation == \"CPython\"", "brotlicffi (>=0.8.0) ; platform_python_implementation != \"CPython\""] +brotli = ["brotli (>=1.0.9)", "brotlicffi (>=0.8.0)"] h2 = ["h2 (>=4,<5)"] socks = ["pysocks (>=1.5.6,!=1.5.7,<2.0)"] zstd = ["zstandard (>=0.18.0)"] @@ -778,4 +793,4 @@ watchdog = ["watchdog (>=2.3)"] [metadata] lock-version = "2.1" python-versions = ">=3.10" -content-hash = "ce9cac7092447dc5d6b3920853bb10fcc166018bc4f48c50925ef09db74e7891" +content-hash = "76f74a5c2443d5c8e9db9433f0bdcc94a1d12c469f8b49dd325f3cc5a1b06f4e" diff --git a/pyproject.toml b/pyproject.toml index ec97022..688e449 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -12,6 +12,7 @@ dependencies = [ "tinydb (>=3.1.0,<4.0.0)", "requests (>=2.32.3,<3.0.0)", "coverage (>=7.6.12,<8.0.0)", + "pytest-httpserver (>=1.1.2,<2.0.0)", ] diff --git a/senju/haiku.py b/senju/haiku.py index 0ff3b24..292c55a 100644 --- a/senju/haiku.py +++ b/senju/haiku.py @@ -86,15 +86,11 @@ class Haiku: return json.dumps(self.lines) @staticmethod - def request_haiku(seed: str) -> 'Haiku': - """ - Generates a haiku using an AI model based on the provided seed text. - - This function prompts the AI to generate a haiku based on the user input. - It validates that the response contains exactly 3 lines. - The function will retry until a valid haiku is generated. - + def request_haiku(seed: str, url=AI_BASE_URL + AI_GEN_ENDPOINT) -> Haiku: + """This function prompts the ai to generate + the hauku based on the user input :param seed: The input text used to inspire the haiku generation. + :param url: The URL to the AI endpoint :type seed: str :return: A new Haiku object containing the generated three lines. :rtype: Haiku @@ -107,19 +103,35 @@ class Haiku: "stream": False, "eval_count": 20 } + + tries = 0 + while True: + tries += 1 try: - r = requests.post(url=AI_BASE_URL + AI_GEN_ENDPOINT, + r = requests.post(url=url, json=ai_gen_request) ai_response = str(r.json()["response"]) - logging.warning(ai_response) + + logging.debug(f"ai response: {ai_response}") + lines = ai_response.split("\n") while len(lines) != 3: lines.pop() - logging.warning(lines) - if len(lines) != 3: - continue + + logging.info(f"lines for haiku: {lines}") + + if len(lines) < 3: + if tries < 20: + logging.warning("too few lines, trying again") + logging.debug(lines) + continue + else: + logging.warning("too many tries, aborting") + raise Exception( + "Generating the haiku took too many tries") + haiku = Haiku( [ lines[0], @@ -127,8 +139,11 @@ class Haiku: lines[2] ]) break - except json.JSONDecodeError: - continue + + except json.JSONDecodeError as e: + logging.error(f"error while reading json from LLM: {e}") + raise e + return haiku diff --git a/tests/test_haiku.py b/tests/test_haiku.py new file mode 100644 index 0000000..b970751 --- /dev/null +++ b/tests/test_haiku.py @@ -0,0 +1,68 @@ +# I do not trust python and it's tests, so I'm testing them. May not be worth +# much, but at least it shows me a few things. + +from __future__ import annotations + +import json +from pytest_httpserver import HTTPServer +import requests + +# do not remove this import. This is needed for +# pytest fixtures to work +import pytest # noqa: F401 + +from senju.haiku import Haiku # noqa: F401 + +# Note: these weird arguments are an indicator of what should be dome +# before. For example, `temp_data_dir` is a function in `conftest.py`. If we +# put it in the arguments, it seems to run before our test, and the return +# value becomes a local. This is all very confusing for someone used to +# Rust's libtest + + +def test_create_haiku(): + haiku = Haiku(["line number 1", "line number 2", "line number 3"]) + assert haiku.lines[0] == "line number 1" + assert haiku.lines[1] == "line number 2" + assert haiku.lines[2] == "line number 3" + assert len(haiku.lines) == 3 + + +def test_get_haiku_json(): + haiku = Haiku(["line number 1", "line number 2", "line number 3"]) + data_raw: str = haiku.get_json() + assert data_raw == '["line number 1", "line number 2", "line number 3"]' + data = json.loads(data_raw) + assert haiku.lines[0] == "line number 1" + assert haiku.lines[1] == "line number 2" + assert haiku.lines[2] == "line number 3" + assert len(haiku.lines) == 3 + assert data == ['line number 1', 'line number 2', 'line number 3'] + + +def test_request_haiku(httpserver: HTTPServer): + + httpserver.expect_request( + "/testhaiku").respond_with_json({"response": + "The apparition of these\n" + "faces in a crowd; Petal\n" + "on a wet, black bough." + }) + + haiku = Haiku.request_haiku( + "apple banana papaya", url=httpserver.url_for("/testhaiku")) + assert haiku.lines[0] == "The apparition of these" + assert haiku.lines[1] == "faces in a crowd; Petal" + assert haiku.lines[2] == "on a wet, black bough." + assert len(haiku.lines) == 3 + + +def test_request_haiku_respondse_bad(httpserver: HTTPServer): + with pytest.raises(requests.exceptions.JSONDecodeError): + + httpserver.expect_request( + "/testhaiku").respond_with_data( + "this is completely wrong" + ("A" * 50 + "\n") * 20) + + Haiku.request_haiku( + "apple banana papaya", url=httpserver.url_for("/testhaiku")) diff --git a/tests/test_store.py b/tests/test_store.py index ac8aac4..05d03b1 100644 --- a/tests/test_store.py +++ b/tests/test_store.py @@ -3,11 +3,21 @@ from __future__ import annotations import pytest # noqa: F401 +import os from senju.haiku import DEFAULT_HAIKU, Haiku from senju.store_manager import StoreManager # noqa: F401 +def test_temp_data_dir(temp_data_dir): + print(temp_data_dir) + testpath = temp_data_dir / "__test" + with open(testpath, "w") as f: + f.write("that dir actually works") + os.remove(testpath) + assert not os.path.exists(testpath) + + def test_save_and_load_any(store_manager: StoreManager): thing = { "color": "blue", @@ -58,3 +68,28 @@ def test_load_latest_or_default_with_non_empty(temp_data_dir): haiku = store.load_haiku(store.get_id_of_latest_haiku()) assert haiku != DEFAULT_HAIKU assert haiku == nonsense_test_haiku + + +def test_load_latest_with_non_empty_store(temp_data_dir): + store = StoreManager(temp_data_dir / "empty_store.json") + store.save_haiku(Haiku(["hello", "world", "bananenkrokodil"])) + h = store.get_id_of_latest_haiku() + assert h is not None + assert h > 0 + + +def test_create_store_with_bad_file(temp_data_dir): + with pytest.raises(Exception): + testpath = temp_data_dir / "non_empty.json" + with open(testpath, "w") as f: + f.write("BUT IT DOES NOT ACTUALLY HAVE JSON") + store = StoreManager(testpath) + store._save({"hello": 19}) + + +def test_create_store_with_non_empty(temp_data_dir): + testpath = temp_data_dir / "non_empty.json" + with open(testpath, "w") as f: + f.write('{"this": ["is","valid","json"]}') + store = StoreManager(testpath) + store._save({"hello": 19}) diff --git a/tests/test_tests.py b/tests/test_tests.py deleted file mode 100644 index 58ab3fa..0000000 --- a/tests/test_tests.py +++ /dev/null @@ -1,30 +0,0 @@ -# I do not trust python and it's tests, so I'm testing them. May not be worth -# much, but at least it shows me a few things. - -from __future__ import annotations - -import os - -# do not remove this import. This is needed for -# pytest fixtures to work -import pytest # noqa: F401 - -import senju # noqa: F401 - -# Note: these weird arguments are an indicator of what should be dome -# before. For example, `temp_data_dir` is a function in `conftest.py`. If we -# put it in the arguments, it seems to run before our test, and the return -# value becomes a local. This is all very confusing for someone used to -# Rust's libtest - - -def test_tests_are_loaded(): - assert True # if we make it here, they are - - -def test_temp_data_dir(temp_data_dir): - print(temp_data_dir) - testpath = temp_data_dir / "__test" - with open(testpath, "w") as f: - f.write("that dir actually works") - os.remove(testpath)