diff --git a/app/backend/__init__.py b/app/backend/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/app/backend/init_app.py b/app/backend/init_app.py index 0fd8cf3d..0980cb72 100644 --- a/app/backend/init_app.py +++ b/app/backend/init_app.py @@ -119,7 +119,7 @@ async def initApp() -> AppConfig: password=DB_PASSWORD ) # read enviornment config - config_helper = ConfigHelper(base_path=os.getcwd()+"/ressources/", env=CONFIG_NAME, base_config_name="base") + config_helper = ConfigHelper(base_path=os.path.dirname(os.path.realpath(__file__))+"/ressources/", env=CONFIG_NAME, base_config_name="base") cfg = config_helper.loadData() model_info = AzureChatGPTConfig( diff --git a/app/backend/ressources/test.json b/app/backend/ressources/test.json new file mode 100644 index 00000000..ae81a7ab --- /dev/null +++ b/app/backend/ressources/test.json @@ -0,0 +1,21 @@ +{ + "frontend": { + "labels": { + "env_name": "MUCGPT-Test" + }, + "alternative_logo": false + }, + "backend": { + "enable_auth": false, + "enable_database": false, + "chat":{ + "log_tokens": false + }, + "brainstorm": { + "log_tokens": false + }, + "sum": { + "log_tokens": false + } + } +} \ No newline at end of file diff --git a/pyproject.toml b/pyproject.toml index 7a135e4c..c2fe8dcf 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -10,6 +10,12 @@ line-length = 300 [tool.pytest.ini_options] addopts = "-ra --cov" pythonpath = ["app/backend"] +testpaths = [ + "tests"] +markers = [ + "integration: mark a test as a integration test", + "unit: mark test as a unit test" +] [tool.coverage.paths] source = ["scripts", "app"] diff --git a/requirements-dev.txt b/requirements-dev.txt index b207cc34..385e3dd5 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -5,6 +5,7 @@ black pytest pytest-asyncio pytest-snapshot +pytest-mock coverage pytest-cov pre-commit diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/tests/conftest.py b/tests/conftest.py deleted file mode 100644 index dd11980e..00000000 --- a/tests/conftest.py +++ /dev/null @@ -1,109 +0,0 @@ -from collections import namedtuple -from unittest import mock - -import openai -import pytest -import pytest_asyncio -from azure.search.documents.aio import SearchClient - -import app - -MockToken = namedtuple("MockToken", ["token", "expires_on"]) - - -class MockAzureCredential: - async def get_token(self, uri): - return MockToken("mock_token", 9999999999) - - -@pytest.fixture -def mock_openai_embedding(monkeypatch): - async def mock_acreate(*args, **kwargs): - return {"data": [{"embedding": [0.1, 0.2, 0.3]}]} - - monkeypatch.setattr(openai.Embedding, "acreate", mock_acreate) - - -@pytest.fixture -def mock_openai_chatcompletion(monkeypatch): - class AsyncChatCompletionIterator: - def __init__(self, answer): - self.num = 1 - self.answer = answer - - def __aiter__(self): - return self - - async def __anext__(self): - if self.num == 1: - self.num = 0 - return openai.util.convert_to_openai_object({"choices": [{"delta": {"content": self.answer}}]}) - else: - raise StopAsyncIteration - - async def mock_acreate(*args, **kwargs): - messages = kwargs["messages"] - if messages[-1]["content"] == "Generate search query for: What is the capital of France?": - answer = "capital of France" - else: - answer = "The capital of France is Paris." - if "stream" in kwargs and kwargs["stream"] is True: - return AsyncChatCompletionIterator(answer) - else: - return openai.util.convert_to_openai_object({"choices": [{"message": {"content": answer}}]}) - - monkeypatch.setattr(openai.ChatCompletion, "acreate", mock_acreate) - - -@pytest.fixture -def mock_acs_search(monkeypatch): - class Caption: - def __init__(self, text): - self.text = text - - class AsyncSearchResultsIterator: - def __init__(self): - self.num = 1 - - def __aiter__(self): - return self - - async def __anext__(self): - if self.num == 1: - self.num = 0 - return { - "sourcepage": "Benefit_Options-2.pdf", - "sourcefile": "Benefit_Options.pdf", - "content": "There is a whistleblower policy.", - "embeddings": [], - "category": None, - "id": "file-Benefit_Options_pdf-42656E656669745F4F7074696F6E732E706466-page-2", - "@search.score": 0.03279569745063782, - "@search.reranker_score": 3.4577205181121826, - "@search.highlights": None, - "@search.captions": [Caption("Caption: A whistleblower policy.")], - } - else: - raise StopAsyncIteration - - async def mock_search(*args, **kwargs): - return AsyncSearchResultsIterator() - - monkeypatch.setattr(SearchClient, "search", mock_search) - - -@pytest_asyncio.fixture -async def client(monkeypatch, mock_openai_chatcompletion, mock_openai_embedding, mock_acs_search): - monkeypatch.setenv("AZURE_OPENAI_SERVICE", "test-openai-service") - monkeypatch.setenv("AZURE_OPENAI_CHATGPT_DEPLOYMENT", "test-chatgpt") - monkeypatch.setenv("AZURE_OPENAI_CHATGPT_MODEL", "gpt-35-turbo") - monkeypatch.setenv("AZURE_OPENAI_EMB_DEPLOYMENT", "test-ada") - - with mock.patch("app.DefaultAzureCredential") as mock_default_azure_credential: - mock_default_azure_credential.return_value = MockAzureCredential() - quart_app = app.create_app() - - async with quart_app.test_app() as test_app: - quart_app.config.update({"TESTING": True}) - - yield test_app.test_client() diff --git a/tests/integration/__init__.py b/tests/integration/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/tests/integration/conftest.py b/tests/integration/conftest.py new file mode 100644 index 00000000..e2b33198 --- /dev/null +++ b/tests/integration/conftest.py @@ -0,0 +1,72 @@ +from collections import namedtuple + +import openai +import pytest +import pytest_asyncio + +import app + +MockToken = namedtuple("MockToken", ["token", "expires_on"]) + + +class MockAzureCredential: + async def get_token(self, uri): + return MockToken("mock_token", 9999999999) + + + +@pytest.fixture +def mock_openai_chatcompletion(monkeypatch): + class AsyncChatCompletionIterator: + def __init__(self, answer): + self.num = 1 + self.answer = answer + + def __aiter__(self): + return self + + async def __anext__(self): + if self.num == 1: + self.num = 0 + return openai.util.convert_to_openai_object({"choices": [{"delta": {"content": self.answer}}]}) + else: + raise StopAsyncIteration + + async def mock_acreate(*args, **kwargs): + messages = kwargs["messages"] + if messages[-1]["content"] == "Generate search query for: What is the capital of France?": + answer = "capital of France" + else: + answer = "The capital of France is Paris." + if "stream" in kwargs and kwargs["stream"] is True: + return AsyncChatCompletionIterator(answer) + else: + return openai.util.convert_to_openai_object({"choices": [{"message": {"content": answer}}]}) + + monkeypatch.setattr(openai.ChatCompletion, "acreate", mock_acreate) + + + +@pytest_asyncio.fixture +async def client(monkeypatch, mock_openai_chatcompletion): + monkeypatch.setenv("AZURE_OPENAI_SERVICE", "test-openai-service") + monkeypatch.setenv("AZURE_OPENAI_CHATGPT_DEPLOYMENT", "test-chatgpt") + monkeypatch.setenv("AZURE_OPENAI_CHATGPT_MODEL", "gpt-35-turbo") + monkeypatch.setenv("AZURE_OPENAI_EMB_DEPLOYMENT", "test-ada") + monkeypatch.setenv("SSO_ISSUER", "testissuer.de") + monkeypatch.setenv("CONFIG_NAME", "test") + monkeypatch.setenv("DB_HOST", "not used") + monkeypatch.setenv("DB_NAME", "not used") + monkeypatch.setenv("DB_PASSWORD", "not used") + monkeypatch.setenv("DB_USER", "not used") + + + #with mock.patch("app.DefaultAzureCredential") as mock_default_azure_credential: + #mock_default_azure_credential.return_value = MockAzureCredential() + quart_app = app.create_app() + + async with quart_app.test_app() as test_app: + quart_app.config.update({"TESTING": True}) + + yield test_app.test_client() + diff --git a/tests/integration/test_app.py b/tests/integration/test_app.py new file mode 100644 index 00000000..223c37f3 --- /dev/null +++ b/tests/integration/test_app.py @@ -0,0 +1,201 @@ +from io import BytesIO +from unittest import mock +import pytest +import quart.testing.app +from httpx import Request, Response +from openai import BadRequestError +from quart.datastructures import FileStorage +import json +import app +import PyPDF2 +from core.types.Chunk import Chunk +from brainstorm.brainstormresult import BrainstormResult +from summarize.summarizeresult import SummarizeResult + + + +def fake_response(http_code): + return Response(http_code, request=Request(method="get", url="https://foo.bar/")) + + +# See https://learn.microsoft.com/en-us/azure/ai-services/openai/concepts/content-filter +filtered_response = BadRequestError( + message="The response was filtered", + body={ + "message": "The response was filtered", + "type": None, + "param": "prompt", + "code": "content_filter", + "status": 400, + }, + response=Response( + 400, request=Request(method="get", url="https://foo.bar/"), json={"error": {"code": "content_filter"}} + ), +) + +contextlength_response = BadRequestError( + message="This model's maximum context length is 4096 tokens. However, your messages resulted in 5069 tokens. Please reduce the length of the messages.", + body={ + "message": "This model's maximum context length is 4096 tokens. However, your messages resulted in 5069 tokens. Please reduce the length of the messages.", + "code": "context_length_exceeded", + "status": 400, + }, + response=Response(400, request=Request(method="get", url="https://foo.bar/"), json={"error": {"code": "429"}}), +) + + + +@pytest.mark.asyncio +@pytest.mark.integration +async def test_missing_env_vars(): + quart_app = app.create_app() + + with pytest.raises(quart.testing.app.LifespanError) as exc_info: + async with quart_app.test_app() as test_app: + test_app.test_client() + assert str(exc_info.value) == "Lifespan failure in startup. ''AZURE_OPENAI_EMB_DEPLOYMENT''" + +@pytest.mark.asyncio +@pytest.mark.integration +async def test_index(client): + response = await client.get('/') + assert response.status_code == 200 + + +@pytest.mark.asyncio +@pytest.mark.integration +async def test_unknown_endpoint(client): + response = await client.post("/unknownendpoint") + assert response.status_code == 404 + +@pytest.mark.asyncio +@pytest.mark.integration +async def test_favicon(client): + response = await client.get("/favicon.ico") + assert response.status_code == 200 + assert response.content_type.startswith("image") + assert response.content_type.endswith("icon") + + +@pytest.mark.asyncio +@pytest.mark.integration +@pytest.mark.skip(reason="TODO implement better error handling.") +async def test_brainstorm_exception(client, monkeypatch,caplog): + monkeypatch.setattr( + "brainstorm.brainstorm.Brainstorm.brainstorm", + mock.Mock(side_effect=ZeroDivisionError("something bad happened")), + ) + data = { + "topic": "München", + "language": "Deutsch", + + } + response = await client.post('/brainstorm', json=data) + assert response.status_code == 500 + result = await response.get_json() + assert "Exception in /error: something bad happened" in caplog.text + +@pytest.mark.asyncio +@pytest.mark.integration +async def test_brainstorm_must_be_json(client): + response = await client.post('/brainstorm') + assert response.status_code == 415 + result = await response.get_json() + assert result["error"] == "request must be json" + +@pytest.mark.asyncio +@pytest.mark.integration +async def test_brainstorm(client, mocker): + mock_result = BrainstormResult(answer= "result of brainstorming.") + mocker.patch("brainstorm.brainstorm.Brainstorm.brainstorm", mock.AsyncMock(return_value=mock_result)) + data = { + "topic": "München", + "language": "Deutsch", + + } + response = await client.post('/brainstorm', json=data) + assert response.status_code == 200 + result = await response.get_json() + assert result == mock_result + + +@pytest.mark.asyncio +@pytest.mark.integration +async def test_sum_text(client, mocker): + mock_result = SummarizeResult(answer= ["sum1", "sum2", "sum3"]) + mocker.patch("summarize.summarize.Summarize.summarize", mock.AsyncMock(return_value=mock_result)) + data = { + "detaillevel": "short", + "text": "To be summarized", + "language": "Deutsch" + } + response = await client.post('/sum', form={"body": json.dumps(data)}) + assert response.status_code == 200 + result = await response.get_json() + assert result == mock_result + +@pytest.mark.asyncio +@pytest.mark.integration +async def test_sum_pdf(client, mocker): + mock_result = SummarizeResult(answer= ["sum1", "sum2", "sum3"]) + mocker.patch("summarize.summarize.Summarize.summarize", mock.AsyncMock(return_value=mock_result)) + + data = { + "detaillevel": "short", + "language": "Deutsch" + } + + tmp = BytesIO() + writer = PyPDF2.PdfWriter() + writer.add_blank_page(219, 297) + page = writer.pages[0] + writer.add_page(page) + # create text + annotation = PyPDF2.generic.AnnotationBuilder.free_text( + "Hello World\nThis is the second line!", + rect=(50, 550, 200, 650), + font="Arial", + bold=True, + italic=True, + font_size="20pt", + font_color="00ff00", + border_color="0000ff", + background_color="cdcdcd", + ) + writer.add_annotation(page_number=0, annotation=annotation) + writer.write(tmp) + tmp.seek(0) + + response = await client.post('/sum', form={"body": json.dumps(data)}, files={"file": FileStorage(tmp, filename="file")}) + assert response.status_code == 200 + result = await response.get_json() + assert result == mock_result + +@pytest.mark.asyncio +@pytest.mark.integration +async def test_chat_stream_must_be_json(client): + response = await client.post('/chat_stream') + assert response.status_code == 415 + result = await response.get_json() + assert result["error"] == "request must be json" + +async def streaming_generator(): + yield Chunk(type="C", message= "Hello", order=0) + + yield Chunk(type="C", message= "World", order=1) + + yield Chunk(type="C", message= "!", order=2) + +@pytest.mark.asyncio +@pytest.mark.integration +async def test_chatstream(client, mocker): + mocker.patch("chat.chat.Chat.run_without_streaming", mock.AsyncMock(return_value=streaming_generator)) + data = { + "temperature": 0.1, + "max_tokens": 2400, + "system_message": "", + "history": [{"user": "hi"}] + + } + response = await client.post('/chat_stream', json=data) + assert response.status_code == 200 \ No newline at end of file diff --git a/tests/test_app.py b/tests/test_app.py deleted file mode 100644 index 8315f16d..00000000 --- a/tests/test_app.py +++ /dev/null @@ -1,253 +0,0 @@ -import json - -import pytest -import quart.testing.app - -import app - - -@pytest.mark.asyncio -async def test_missing_env_vars(): - quart_app = app.create_app() - - with pytest.raises(quart.testing.app.LifespanError) as exc_info: - async with quart_app.test_app() as test_app: - test_app.test_client() - assert str(exc_info.value) == "Lifespan failure in startup. ''AZURE_OPENAI_EMB_DEPLOYMENT''" - -@pytest.mark.asyncio -async def test_index(client): - response = await client.get("/") - assert response.status_code == 200 - - -@pytest.mark.asyncio -async def test_ask_request_must_be_json(client): - response = await client.post("/ask") - assert response.status_code == 415 - result = await response.get_json() - assert result["error"] == "request must be json" - - -@pytest.mark.asyncio -async def test_ask_with_unknown_approach(client): - response = await client.post("/ask", json={"approach": "test"}) - assert response.status_code == 400 - - -@pytest.mark.asyncio -async def test_ask_rtr_text(client, snapshot): - response = await client.post( - "/ask", - json={ - "approach": "rtr", - "question": "What is the capital of France?", - "overrides": {"retrieval_mode": "text"}, - }, - ) - assert response.status_code == 200 - result = await response.get_json() - snapshot.assert_match(json.dumps(result, indent=4), "result.json") - - -@pytest.mark.asyncio -async def test_ask_rtr_text_semanticranker(client, snapshot): - response = await client.post( - "/ask", - json={ - "approach": "rtr", - "question": "What is the capital of France?", - "overrides": {"retrieval_mode": "text", "semantic_ranker": True}, - }, - ) - assert response.status_code == 200 - result = await response.get_json() - snapshot.assert_match(json.dumps(result, indent=4), "result.json") - - -@pytest.mark.asyncio -async def test_ask_rtr_text_semanticcaptions(client, snapshot): - response = await client.post( - "/ask", - json={ - "approach": "rtr", - "question": "What is the capital of France?", - "overrides": {"retrieval_mode": "text", "semantic_captions": True}, - }, - ) - assert response.status_code == 200 - result = await response.get_json() - snapshot.assert_match(json.dumps(result, indent=4), "result.json") - - -@pytest.mark.asyncio -async def test_ask_rtr_hybrid(client, snapshot): - response = await client.post( - "/ask", - json={ - "approach": "rtr", - "question": "What is the capital of France?", - "overrides": {"retrieval_mode": "hybrid"}, - }, - ) - assert response.status_code == 200 - result = await response.get_json() - snapshot.assert_match(json.dumps(result, indent=4), "result.json") - - -@pytest.mark.asyncio -async def test_chat_request_must_be_json(client): - response = await client.post("/chat") - assert response.status_code == 415 - result = await response.get_json() - assert result["error"] == "request must be json" - - -@pytest.mark.asyncio -async def test_chat_with_unknown_approach(client): - response = await client.post("/chat", json={"approach": "test"}) - assert response.status_code == 400 - - -@pytest.mark.asyncio -async def test_chat_text(client, snapshot): - response = await client.post( - "/chat", - json={ - "approach": "rrr", - "history": [{"user": "What is the capital of France?"}], - "overrides": {"retrieval_mode": "text"}, - }, - ) - assert response.status_code == 200 - result = await response.get_json() - snapshot.assert_match(json.dumps(result, indent=4), "result.json") - - -@pytest.mark.asyncio -async def test_chat_text_semanticranker(client, snapshot): - response = await client.post( - "/chat", - json={ - "approach": "rrr", - "history": [{"user": "What is the capital of France?"}], - "overrides": {"retrieval_mode": "text", "semantic_ranker": True}, - }, - ) - assert response.status_code == 200 - result = await response.get_json() - snapshot.assert_match(json.dumps(result, indent=4), "result.json") - - -@pytest.mark.asyncio -async def test_chat_text_semanticcaptions(client, snapshot): - response = await client.post( - "/chat", - json={ - "approach": "rrr", - "history": [{"user": "What is the capital of France?"}], - "overrides": {"retrieval_mode": "text", "semantic_captions": True}, - }, - ) - assert response.status_code == 200 - result = await response.get_json() - snapshot.assert_match(json.dumps(result, indent=4), "result.json") - - -@pytest.mark.asyncio -async def test_chat_prompt_template(client, snapshot): - response = await client.post( - "/chat", - json={ - "approach": "rrr", - "history": [{"user": "What is the capital of France?"}], - "overrides": {"retrieval_mode": "text", "prompt_template": "You are a cat."}, - }, - ) - assert response.status_code == 200 - result = await response.get_json() - snapshot.assert_match(json.dumps(result, indent=4), "result.json") - - -@pytest.mark.asyncio -async def test_chat_prompt_template_concat(client, snapshot): - response = await client.post( - "/chat", - json={ - "approach": "rrr", - "history": [{"user": "What is the capital of France?"}], - "overrides": {"retrieval_mode": "text", "prompt_template": ">>> Meow like a cat."}, - }, - ) - assert response.status_code == 200 - result = await response.get_json() - snapshot.assert_match(json.dumps(result, indent=4), "result.json") - - -@pytest.mark.asyncio -async def test_chat_hybrid(client, snapshot): - response = await client.post( - "/chat", - json={ - "approach": "rrr", - "history": [{"user": "What is the capital of France?"}], - "overrides": {"retrieval_mode": "hybrid"}, - }, - ) - assert response.status_code == 200 - result = await response.get_json() - snapshot.assert_match(json.dumps(result, indent=4), "result.json") - - -@pytest.mark.asyncio -async def test_chat_vector(client, snapshot): - response = await client.post( - "/chat", - json={ - "approach": "rrr", - "history": [{"user": "What is the capital of France?"}], - "overrides": {"retrieval_mode": "vector"}, - }, - ) - assert response.status_code == 200 - result = await response.get_json() - snapshot.assert_match(json.dumps(result, indent=4), "result.json") - - -@pytest.mark.asyncio -async def test_chat_stream_request_must_be_json(client): - response = await client.post("/chat_stream") - assert response.status_code == 415 - result = await response.get_json() - assert result["error"] == "request must be json" - - -@pytest.mark.asyncio -async def test_chat_stream_with_unknown_approach(client): - response = await client.post("/chat_stream", json={"approach": "test"}) - assert response.status_code == 400 - - -@pytest.mark.asyncio -async def test_chat_stream_text(client, snapshot): - response = await client.post( - "/chat_stream", - json={ - "approach": "rrr", - "history": [{"user": "What is the capital of France?"}], - "overrides": {"retrieval_mode": "text"}, - }, - ) - assert response.status_code == 200 - result = await response.get_data() - snapshot.assert_match(result, "result.jsonlines") - - -@pytest.mark.asyncio -async def test_format_as_ndjson(): - async def gen(): - yield {"a": "I ❤️ 🐍"} - yield {"b": "Newlines inside \n strings are fine"} - - result = [line async for line in app.format_as_ndjson(gen())] - assert result == ['{"a": "I ❤️ 🐍"}\n', '{"b": "Newlines inside \\n strings are fine"}\n'] diff --git a/tests/test_messagebuilder.py b/tests/test_messagebuilder.py deleted file mode 100644 index 6eaaad28..00000000 --- a/tests/test_messagebuilder.py +++ /dev/null @@ -1,24 +0,0 @@ -from core.messagebuilder import MessageBuilder - - -def test_messagebuilder(): - builder = MessageBuilder("You are a bot.", "gpt-35-turbo") - assert builder.messages == [ - # 1 token, 1 token, 1 token, 5 tokens - {"role": "system", "content": "You are a bot."} - ] - assert builder.model == "gpt-35-turbo" - assert builder.token_length == 8 - - -def test_messagebuilder_append(): - builder = MessageBuilder("You are a bot.", "gpt-35-turbo") - builder.append_message("user", "Hello, how are you?") - assert builder.messages == [ - # 1 token, 1 token, 1 token, 5 tokens - {"role": "system", "content": "You are a bot."}, - # 1 token, 1 token, 1 token, 6 tokens - {"role": "user", "content": "Hello, how are you?"}, - ] - assert builder.model == "gpt-35-turbo" - assert builder.token_length == 17 diff --git a/tests/test_modelhelper.py b/tests/test_modelhelper.py deleted file mode 100644 index 28072fda..00000000 --- a/tests/test_modelhelper.py +++ /dev/null @@ -1,61 +0,0 @@ -import pytest - -from core.modelhelper import ( - get_oai_chatmodel_tiktok, - get_token_limit, - num_tokens_from_messages, -) - - -def test_get_token_limit(): - assert get_token_limit("gpt-35-turbo") == 4000 - assert get_token_limit("gpt-3.5-turbo") == 4000 - assert get_token_limit("gpt-35-turbo-16k") == 16000 - assert get_token_limit("gpt-3.5-turbo-16k") == 16000 - assert get_token_limit("gpt-4") == 8100 - assert get_token_limit("gpt-4-32k") == 32000 - - -def test_get_token_limit_error(): - with pytest.raises(ValueError, match="Expected model gpt-35-turbo and above"): - get_token_limit("gpt-3") - - -def test_num_tokens_from_messages(): - message = { - # 1 token : 1 token - "role": "user", - # 1 token : 5 tokens - "content": "Hello, how are you?", - } - model = "gpt-35-turbo" - assert num_tokens_from_messages(message, model) == 9 - - -def test_num_tokens_from_messages_gpt4(): - message = { - # 1 token : 1 token - "role": "user", - # 1 token : 5 tokens - "content": "Hello, how are you?", - } - model = "gpt-4" - assert num_tokens_from_messages(message, model) == 9 - - -def test_get_oai_chatmodel_tiktok_mapped(): - assert get_oai_chatmodel_tiktok("gpt-35-turbo") == "gpt-3.5-turbo" - assert get_oai_chatmodel_tiktok("gpt-35-turbo-16k") == "gpt-3.5-turbo-16k" - - -def test_get_oai_chatmodel_tiktok_unmapped(): - assert get_oai_chatmodel_tiktok("gpt-4") == "gpt-4" - - -def test_get_oai_chatmodel_tiktok_error(): - with pytest.raises(ValueError, match="Expected Azure OpenAI ChatGPT model name"): - get_oai_chatmodel_tiktok("") - with pytest.raises(ValueError, match="Expected Azure OpenAI ChatGPT model name"): - get_oai_chatmodel_tiktok(None) - with pytest.raises(ValueError, match="Expected Azure OpenAI ChatGPT model name"): - get_oai_chatmodel_tiktok("gpt-3") diff --git a/tests/unit/__init__.py b/tests/unit/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/tests/unit/test_confighelper.py b/tests/unit/test_confighelper.py new file mode 100644 index 00000000..b4da3ce6 --- /dev/null +++ b/tests/unit/test_confighelper.py @@ -0,0 +1,49 @@ +import unittest + +import pytest + +from core.confighelper import ConfigHelper +import os + + +class Test_Confighelper(unittest.TestCase): + @pytest.mark.asyncio + @pytest.mark.unit + def test_confighelper_create(self): + path=r"app\\backend\\ressources\\" + env="dev" + helper = ConfigHelper(path, env) + self.assertEqual(helper.base_config_name, "base") + self.assertEqual(helper.env, env) + self.assertEqual(helper.base_path, path) + helper = ConfigHelper(path, env, "basis") + self.assertEqual(helper.base_config_name, "basis") + self.assertEqual(helper.env, env) + self.assertEqual(helper.base_path, path) + + + @pytest.mark.asyncio + @pytest.mark.unit + def test_confighelper_loadData(self): + path=r"app\\backend\\ressources\\" + env="dev" + helper = ConfigHelper(path, env) + data = helper.loadData() + self.assertIn("version", data) + self.assertIn("frontend", data) + self.assertIn("backend", data) + + @pytest.mark.asyncio + @pytest.mark.unit + def test_confighelper_loadData_fail(self): + path=r"app\\backend\\ressources\\" + env="super" + filename = path + env + ".json" + with open(filename, "w") as file: + file.write('{"frontend": {"labels": {"env_name": "MUC tschibidi-C"},"alternative_logo": true}}') + helper = ConfigHelper(path, env) + self.assertEqual(helper.base_config_name, "base") + self.assertEqual(helper.env, env) + self.assertEqual(helper.base_path, path) + self.assertRaises(ValueError, helper.loadData) + os.remove(filename) diff --git a/tests/unit/test_datahelper.py b/tests/unit/test_datahelper.py new file mode 100644 index 00000000..7f7ea49f --- /dev/null +++ b/tests/unit/test_datahelper.py @@ -0,0 +1,18 @@ +import unittest + +import pytest + +from core.datahelper import Requestinfo + + +class Test_Datahelper(unittest.TestCase): + @pytest.mark.asyncio + @pytest.mark.unit + def test_requestinfo_creation(self): + request = Requestinfo(tokencount=100, department='IT', messagecount=50, method='GET') + self.assertIsInstance(request, Requestinfo) + self.assertEqual(request.tokencount, 100) + self.assertEqual(request.department, 'IT') + self.assertEqual(request.messagecount, 50) + self.assertEqual(request.method, 'GET') + self.assertEqual(str(request), ' ') \ No newline at end of file diff --git a/tests/unit/test_helper.py b/tests/unit/test_helper.py new file mode 100644 index 00000000..d5a71afa --- /dev/null +++ b/tests/unit/test_helper.py @@ -0,0 +1,20 @@ +import unittest +import pytest +from core.helper import format_as_ndjson + + +class Test_Helper(unittest.TestCase): + + @pytest.mark.asyncio + @pytest.mark.unit + async def test_format_as_ndjson(self): + async def gen(): + yield {"a": "I ❤️ 🐍"} + yield {"b": "Newlines inside \n strings are fine"} + + result = [line async for line in format_as_ndjson(gen())] + assert result == ['{"a": "I ❤️ 🐍"}\n', '{"b": "Newlines inside \\n strings are fine"}\n'] + + +if __name__ == '__main__': + unittest.main() \ No newline at end of file diff --git a/tests/unit/test_llmhelper.py b/tests/unit/test_llmhelper.py new file mode 100644 index 00000000..4cd51807 --- /dev/null +++ b/tests/unit/test_llmhelper.py @@ -0,0 +1,76 @@ +import unittest + +import pytest + +from core.llmhelper import getModel +from langchain_core.runnables.base import RunnableSerializable + + +class Test_LLMhelper(unittest.TestCase): + + def setUp(self): + self.api_key = "test_api_key" + self.api_base = "test_api_base" + self.api_version = "test_api_version" + self.api_type = "test_api_type" + + @pytest.mark.asyncio + @pytest.mark.unit + def test_getModel_returns_llm(self): + model = getModel(chatgpt_model="test_model", + max_tokens=10, + n=1, + api_key=self.api_key, + api_base=self.api_base, + api_version=self.api_version, + api_type=self.api_type, + temperature=0.5, + streaming=True) + self.assertIsInstance(model, RunnableSerializable) + + @pytest.mark.asyncio + @pytest.mark.unit + def test_getModel_configurable_fields(self): + model = getModel(chatgpt_model="test_model", + max_tokens=10, + n=1, + api_key=self.api_key, + api_base=self.api_base, + api_version=self.api_version, + api_type=self.api_type, + temperature=0.5, + streaming=True) + self.assertIn("temperature", model.fields) + self.assertIn("max_tokens", model.fields) + self.assertIn("openai_api_key", model.fields) + self.assertIn("streaming", model.fields) + self.assertIn("callbacks", model.fields) + + @pytest.mark.asyncio + @pytest.mark.unit + def test_getModel_configurable_alternatives(self): + model = getModel(chatgpt_model="test_model", + max_tokens=10, + n=1, + api_key=self.api_key, + api_base=self.api_base, + api_version=self.api_version, + api_type=self.api_type, + temperature=0.5, + streaming=True) + self.assertIn("fake", model.alternatives) + + @pytest.mark.asyncio + @pytest.mark.unit + def test_getModel_fake_llm(self): + model = getModel(chatgpt_model="test_model", + max_tokens=10, + n=1, + api_key=self.api_key, + api_base=self.api_base, + api_version=self.api_version, + api_type=self.api_type, + temperature=0.5, + streaming=True) + print(model.alternatives["fake"]) + self.assertEqual(model.alternatives["fake"].responses, ["Hi diggi"]) \ No newline at end of file diff --git a/tests/unit/test_modelhelper.py b/tests/unit/test_modelhelper.py new file mode 100644 index 00000000..33ea16da --- /dev/null +++ b/tests/unit/test_modelhelper.py @@ -0,0 +1,30 @@ +import unittest + +import pytest + +from core.modelhelper import get_token_limit, num_tokens_from_messages + + +class Test_Modelhelper(unittest.TestCase): + @pytest.mark.asyncio + @pytest.mark.unit + def test_get_token_limit(self): + self.assertEqual(get_token_limit("gpt-35-turbo"), 4000) + self.assertEqual(get_token_limit("gpt-3.5-turbo"), 4000) + self.assertEqual(get_token_limit("gpt-35-turbo-16k"), 16000) + self.assertEqual(get_token_limit("gpt-3.5-turbo-16k"), 16000) + self.assertEqual(get_token_limit("gpt-4"), 8100) + self.assertEqual(get_token_limit("gpt-4-32k"), 32000) + self.assertRaises(ValueError, get_token_limit, "gpt-2") + + @pytest.mark.asyncio + @pytest.mark.unit + def test_num_tokens_from_messages(self): + messages = [ + {"user": "Hello, I have a problem with my computer.", "bot": "Hi there! What seems to be the issue?"}, + {"user": "My computer won't turn on.", "bot": "Okay, let's try a few troubleshooting steps. Have you checked to make sure it's plugged in and the power outlet?"}] + self.assertEqual(num_tokens_from_messages(messages,"gpt-35-turbo" ), 64) + self.assertRaises(ValueError,num_tokens_from_messages,messages,"" ) + self.assertRaises(ValueError,num_tokens_from_messages,messages,"gpt-2" ) + + \ No newline at end of file diff --git a/tests/unit/test_textsplit.py b/tests/unit/test_textsplit.py new file mode 100644 index 00000000..cc0ee6c3 --- /dev/null +++ b/tests/unit/test_textsplit.py @@ -0,0 +1,34 @@ +import unittest + +import pytest + +import PyPDF2 + +from core.textsplit import textToDocs, PDFtoDocs, splitText, splitPDF + + +class Test_Testsplit(unittest.TestCase): + + @pytest.mark.asyncio + @pytest.mark.unit + def test_textToDocs(self): + text = "Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet." + self.assertEqual(len(textToDocs(text, 100, 10)), 3) + + @pytest.mark.asyncio + @pytest.mark.unit + def test_PDFtoDocs(self): + path= r"app\frontend\src\assets\mucgpt_cheatsheet.pdf" + self.assertEqual(len(PDFtoDocs(path)), 2) + + @pytest.mark.asyncio + @pytest.mark.unit + def test_splitText(self): + text = "Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet." + self.assertEqual(len(splitText(text, 100, 10)), 3) + + @pytest.mark.asyncio + @pytest.mark.unit + def test_splitPDF(self): + path= r"app\frontend\src\assets\mucgpt_cheatsheet.pdf" + self.assertEqual(len(splitPDF(path)), 2) \ No newline at end of file