Repository: Azure-Samples/azure-search-openai-demo
Pull request #495 (2023-08-02 20:39:38)

Use an app factory pattern to enable app testing
## Purpose

This PR changes app.py to use an app factory pattern (create_app) along with a Flask Blueprint to organize the code. This makes testing much easier and is a general best practice.

This PR is based off the previous PR that ruff-linted the files, so that needs to be merged first to isolate the changes. Then I'll turn the draft into an actual PR.


## Does this introduce a breaking change?
<!-- Mark one with an "x". -->
```
[ ] Yes
[X] No
```

## Pull Request Type
What kind of change does this Pull Request introduce?

<!-- Please check the one that applies to this PR using "x". -->
```
[ ] Bugfix
[ ] Feature
[ ] Code style update (formatting, local variables)
[X] Refactoring (no functional changes, no api changes)
[ ] Documentation content changes
[ ] Other... Please describe:
```

## How to Test

* python3 -m pytest
* Run start.sh in app
* Run azd up for prod


tonybaloney: Great improvement, this will cause a few merge conflicts with existing PRs, but that can't be avoided

Patch:
diff --git a/README.md b/README.md
index 2b2b058..872b736 100644
--- a/README.md
+++ b/README.md
@@ -29,7 +29,7 @@ The repo includes sample data so it's ready to try end to end. In this sample ap
 #### To Run Locally
 
 * [Azure Developer CLI](https://aka.ms/azure-dev/install)
-* [Python 3.8+](https://www.python.org/downloads/)
+* [Python 3.9+](https://www.python.org/downloads/)
   * **Important**: Python and the pip package manager must be in the path in Windows for the setup scripts to work.
   * **Important**: Ensure you can run `python --version` from console. On Ubuntu, you might need to run `sudo apt install python-is-python3` to link `python` to `python3`.
 * [Node.js 14+](https://nodejs.org/en/download/)
diff --git a/app/backend/app.py b/app/backend/app.py
index 177af48..97dff7d 100644
--- a/app/backend/app.py
+++ b/app/backend/app.py
@@ -5,85 +5,67 @@ import os
 import time
 
 import openai
+from azure.identity import DefaultAzureCredential
+from azure.search.documents import SearchClient
+from azure.storage.blob import BlobServiceClient
+from flask import (
+    Blueprint,
+    Flask,
+    abort,
+    current_app,
+    jsonify,
+    request,
+    send_file,
+    send_from_directory,
+)
+
 from approaches.chatreadretrieveread import ChatReadRetrieveReadApproach
 from approaches.readdecomposeask import ReadDecomposeAsk
 from approaches.readretrieveread import ReadRetrieveReadApproach
 from approaches.retrievethenread import RetrieveThenReadApproach
-from azure.identity import DefaultAzureCredential
-from azure.search.documents import SearchClient
-from azure.storage.blob import BlobServiceClient
-from flask import Flask, abort, jsonify, request, send_file
 
 # Replace these with your own values, either in environment variables or directly here
-AZURE_STORAGE_ACCOUNT = os.environ.get("AZURE_STORAGE_ACCOUNT") or "mystorageaccount"
-AZURE_STORAGE_CONTAINER = os.environ.get("AZURE_STORAGE_CONTAINER") or "content"
-AZURE_SEARCH_SERVICE = os.environ.get("AZURE_SEARCH_SERVICE") or "gptkb"
-AZURE_SEARCH_INDEX = os.environ.get("AZURE_SEARCH_INDEX") or "gptkbindex"
-AZURE_OPENAI_SERVICE = os.environ.get("AZURE_OPENAI_SERVICE") or "myopenai"
-AZURE_OPENAI_GPT_DEPLOYMENT = os.environ.get("AZURE_OPENAI_GPT_DEPLOYMENT") or "davinci"
-AZURE_OPENAI_CHATGPT_DEPLOYMENT = os.environ.get("AZURE_OPENAI_CHATGPT_DEPLOYMENT") or "chat"
-AZURE_OPENAI_CHATGPT_MODEL = os.environ.get("AZURE_OPENAI_CHATGPT_MODEL") or "gpt-35-turbo"
-AZURE_OPENAI_EMB_DEPLOYMENT = os.environ.get("AZURE_OPENAI_EMB_DEPLOYMENT") or "embedding"
-
-KB_FIELDS_CONTENT = os.environ.get("KB_FIELDS_CONTENT") or "content"
-KB_FIELDS_CATEGORY = os.environ.get("KB_FIELDS_CATEGORY") or "category"
-KB_FIELDS_SOURCEPAGE = os.environ.get("KB_FIELDS_SOURCEPAGE") or "sourcepage"
-
-# Use the current user identity to authenticate with Azure OpenAI, Cognitive Search and Blob Storage (no secrets needed,
-# just use 'az login' locally, and managed identity when deployed on Azure). If you need to use keys, use separate AzureKeyCredential instances with the
-# keys for each service
-# If you encounter a blocking error during a DefaultAzureCredntial resolution, you can exclude the problematic credential by using a parameter (ex. exclude_shared_token_cache_credential=True)
-azure_credential = DefaultAzureCredential(exclude_shared_token_cache_credential = True)
-
-# Used by the OpenAI SDK
-openai.api_type = "azure"
-openai.api_base = f"https://{AZURE_OPENAI_SERVICE}.openai.azure.com"
-openai.api_version = "2023-05-15"
-
-# Comment these two lines out if using keys, set your API key in the OPENAI_API_KEY environment variable instead
-openai.api_type = "azure_ad"
-openai_token = azure_credential.get_token("https://cognitiveservices.azure.com/.default")
-openai.api_key = openai_token.token
-
-# Set up clients for Cognitive Search and Storage
-search_client = SearchClient(
-    endpoint=f"https://{AZURE_SEARCH_SERVICE}.search.windows.net",
-    index_name=AZURE_SEARCH_INDEX,
-    credential=azure_credential)
-blob_client = BlobServiceClient(
-    account_url=f"https://{AZURE_STORAGE_ACCOUNT}.blob.core.windows.net",
-    credential=azure_credential)
-blob_container = blob_client.get_container_client(AZURE_STORAGE_CONTAINER)
-
-# Various approaches to integrate GPT and external knowledge, most applications will use a single one of these patterns
-# or some derivative, here we include several for exploration purposes
-ask_approaches = {
-    "rtr": RetrieveThenReadApproach(search_client, AZURE_OPENAI_CHATGPT_DEPLOYMENT, AZURE_OPENAI_CHATGPT_MODEL, AZURE_OPENAI_EMB_DEPLOYMENT, KB_FIELDS_SOURCEPAGE, KB_FIELDS_CONTENT),
-    "rrr": ReadRetrieveReadApproach(search_client, AZURE_OPENAI_GPT_DEPLOYMENT, AZURE_OPENAI_EMB_DEPLOYMENT, KB_FIELDS_SOURCEPAGE, KB_FIELDS_CONTENT),
-    "rda": ReadDecomposeAsk(search_client, AZURE_OPENAI_GPT_DEPLOYMENT, AZURE_OPENAI_EMB_DEPLOYMENT, KB_FIELDS_SOURCEPAGE, KB_FIELDS_CONTENT)
-}
-
-chat_approaches = {
-    "rrr": ChatReadRetrieveReadApproach(search_client,
-                                        AZURE_OPENAI_CHATGPT_DEPLOYMENT,
-                                        AZURE_OPENAI_CHATGPT_MODEL,
-                                        AZURE_OPENAI_EMB_DEPLOYMENT,
-                                        KB_FIELDS_SOURCEPAGE,
-                                        KB_FIELDS_CONTENT)
-}
-
-app = Flask(__name__)
-
-@app.route("/", defaults={"path": "index.html"})
-@app.route("/<path:path>")
-def static_file(path):
-    return app.send_static_file(path)
+AZURE_STORAGE_ACCOUNT = os.getenv("AZURE_STORAGE_ACCOUNT", "mystorageaccount")
+AZURE_STORAGE_CONTAINER = os.getenv("AZURE_STORAGE_CONTAINER", "content")
+AZURE_SEARCH_SERVICE = os.getenv("AZURE_SEARCH_SERVICE", "gptkb")
+AZURE_SEARCH_INDEX = os.getenv("AZURE_SEARCH_INDEX", "gptkbindex")
+AZURE_OPENAI_SERVICE = os.getenv("AZURE_OPENAI_SERVICE", "myopenai")
+AZURE_OPENAI_GPT_DEPLOYMENT = os.getenv("AZURE_OPENAI_GPT_DEPLOYMENT", "davinci")
+AZURE_OPENAI_CHATGPT_DEPLOYMENT = os.getenv("AZURE_OPENAI_CHATGPT_DEPLOYMENT", "chat")
+AZURE_OPENAI_CHATGPT_MODEL = os.getenv("AZURE_OPENAI_CHATGPT_MODEL", "gpt-35-turbo")
+AZURE_OPENAI_EMB_DEPLOYMENT = os.getenv("AZURE_OPENAI_EMB_DEPLOYMENT", "embedding")
+
+KB_FIELDS_CONTENT = os.getenv("KB_FIELDS_CONTENT", "content")
+KB_FIELDS_CATEGORY = os.getenv("KB_FIELDS_CATEGORY", "category")
+KB_FIELDS_SOURCEPAGE = os.getenv("KB_FIELDS_SOURCEPAGE", "sourcepage")
+
+CONFIG_OPENAI_TOKEN = "openai_token"
+CONFIG_CREDENTIAL = "azure_credential"
+CONFIG_ASK_APPROACHES = "ask_approaches"
+CONFIG_CHAT_APPROACHES = "chat_approaches"
+CONFIG_BLOB_CLIENT = "blob_client"
+
+
+bp = Blueprint("routes", __name__, static_folder='static')
+
+@bp.route("/")
+def index():
+    return bp.send_static_file("index.html")
+
+@bp.route("/favicon.ico")
+def favicon():
+    return bp.send_static_file("favicon.ico")
+
+@bp.route("/assets/<path:path>")
+def assets(path):
+    return send_from_directory("static/assets", path)
 
 # Serve content files from blob storage from within the app to keep the example self-contained.
 # *** NOTE *** this assumes that the content files are public, or at least that all users of the app
 # can access all the files. This is also slow and memory hungry.
-@app.route("/content/<path>")
+@bp.route("/content/<path>")
 def content_file(path):
+    blob_container = current_app.config[CONFIG_BLOB_CLIENT].get_container_client(AZURE_STORAGE_CONTAINER)
     blob = blob_container.get_blob_client(path).download_blob()
     if not blob.properties or not blob.properties.has_key("content_settings"):
         abort(404)
@@ -95,13 +77,13 @@ def content_file(path):
     blob_file.seek(0)
     return send_file(blob_file, mimetype=mime_type, as_attachment=False, download_name=path)
 
-@app.route("/ask", methods=["POST"])
+@bp.route("/ask", methods=["POST"])
 def ask():
-    if not request.json:
-        return jsonify({"error": "request must be json"}), 400
+    if not request.is_json:
+        return jsonify({"error": "request must be json"}), 415
     approach = request.json["approach"]
     try:
-        impl = ask_approaches.get(approach)
+        impl = current_app.config[CONFIG_ASK_APPROACHES].get(approach)
         if not impl:
             return jsonify({"error": "unknown approach"}), 400
         r = impl.run(request.json["question"], request.json.get("overrides") or {})
@@ -110,13 +92,13 @@ def ask():
         logging.exception("Exception in /ask")
         return jsonify({"error": str(e)}), 500
 
-@app.route("/chat", methods=["POST"])
+@bp.route("/chat", methods=["POST"])
 def chat():
-    if not request.json:
-        return jsonify({"error": "request must be json"}), 400
+    if not request.is_json:
+        return jsonify({"error": "request must be json"}), 415
     approach = request.json["approach"]
     try:
-        impl = chat_approaches.get(approach)
+        impl = current_app.config[CONFIG_CHAT_APPROACHES].get(approach)
         if not impl:
             return jsonify({"error": "unknown approach"}), 400
         r = impl.run(request.json["history"], request.json.get("overrides") or {})
@@ -125,12 +107,89 @@ def chat():
         logging.exception("Exception in /chat")
         return jsonify({"error": str(e)}), 500
 
-@app.before_request
+@bp.before_request
 def ensure_openai_token():
-    global openai_token
+    openai_token = current_app.config[CONFIG_OPENAI_TOKEN]
     if openai_token.expires_on < time.time() + 60:
-        openai_token = azure_credential.get_token("https://cognitiveservices.azure.com/.default")
+        openai_token = current_app.config[CONFIG_CREDENTIAL].get_token("https://cognitiveservices.azure.com/.default")
+        current_app.config[CONFIG_OPENAI_TOKEN] = openai_token
         openai.api_key = openai_token.token
 
+
+def create_app():
+    app = Flask(__name__)
+
+    # Use the current user identity to authenticate with Azure OpenAI, Cognitive Search and Blob Storage (no secrets needed,
+    # just use 'az login' locally, and managed identity when deployed on Azure). If you need to use keys, use separate AzureKeyCredential instances with the
+    # keys for each service
+    # If you encounter a blocking error during a DefaultAzureCredntial resolution, you can exclude the problematic credential by using a parameter (ex. exclude_shared_token_cache_credential=True)
+    azure_credential = DefaultAzureCredential(exclude_shared_token_cache_credential = True)
+
+    # Set up clients for Cognitive Search and Storage
+    search_client = SearchClient(
+        endpoint=f"https://{AZURE_SEARCH_SERVICE}.search.windows.net",
+        index_name=AZURE_SEARCH_INDEX,
+        credential=azure_credential)
+    blob_client = BlobServiceClient(
+        account_url=f"https://{AZURE_STORAGE_ACCOUNT}.blob.core.windows.net",
+        credential=azure_credential)
+
+    # Used by the OpenAI SDK
+    openai.api_type = "azure"
+    openai.api_base = f"https://{AZURE_OPENAI_SERVICE}.openai.azure.com"
+    openai.api_version = "2023-05-15"
+
+    # Comment these two lines out if using keys, set your API key in the OPENAI_API_KEY environment variable instead
+    openai.api_type = "azure_ad"
+    openai_token = azure_credential.get_token(
+        "https://cognitiveservices.azure.com/.default"
+    )
+    openai.api_key = openai_token.token
+
+    # Store on app.config for later use inside requests
+    app.config[CONFIG_OPENAI_TOKEN] = openai_token
+    app.config[CONFIG_CREDENTIAL] = azure_credential
+    app.config[CONFIG_BLOB_CLIENT] = blob_client
+    # Various approaches to integrate GPT and external knowledge, most applications will use a single one of these patterns
+    # or some derivative, here we include several for exploration purposes
+    app.config[CONFIG_ASK_APPROACHES] = {
+        "rtr": RetrieveThenReadApproach(
+            search_client,
+            AZURE_OPENAI_CHATGPT_DEPLOYMENT,
+            AZURE_OPENAI_CHATGPT_MODEL,
+            AZURE_OPENAI_EMB_DEPLOYMENT,
+            KB_FIELDS_SOURCEPAGE,
+            KB_FIELDS_CONTENT
+        ),
+        "rrr": ReadRetrieveReadApproach(
+            search_client,
+            AZURE_OPENAI_GPT_DEPLOYMENT,
+            AZURE_OPENAI_EMB_DEPLOYMENT,
+            KB_FIELDS_SOURCEPAGE,
+            KB_FIELDS_CONTENT
+        ),
+        "rda": ReadDecomposeAsk(search_client,
+            AZURE_OPENAI_GPT_DEPLOYMENT,
+            AZURE_OPENAI_EMB_DEPLOYMENT,
+            KB_FIELDS_SOURCEPAGE,
+            KB_FIELDS_CONTENT
+        )
+    }
+    app.config[CONFIG_CHAT_APPROACHES] = {
+        "rrr": ChatReadRetrieveReadApproach(
+            search_client,
+            AZURE_OPENAI_CHATGPT_DEPLOYMENT,
+            AZURE_OPENAI_CHATGPT_MODEL,
+            AZURE_OPENAI_EMB_DEPLOYMENT,
+            KB_FIELDS_SOURCEPAGE,
+            KB_FIELDS_CONTENT,
+        )
+    }
+
+    app.register_blueprint(bp)
+
+    return app
+
 if __name__ == "__main__":
+    app = create_app()
     app.run()
diff --git a/app/backend/approaches/chatreadretrieveread.py b/app/backend/approaches/chatreadretrieveread.py
index 9be21f5..5c5d733 100644
--- a/app/backend/approaches/chatreadretrieveread.py
+++ b/app/backend/approaches/chatreadretrieveread.py
@@ -1,9 +1,10 @@
 from typing import Any, Sequence
 
 import openai
-from approaches.approach import Approach
 from azure.search.documents import SearchClient
 from azure.search.documents.models import QueryType
+
+from approaches.approach import Approach
 from core.messagebuilder import MessageBuilder
 from core.modelhelper import get_token_limit
 from text import nonewlines
diff --git a/app/backend/approaches/readdecomposeask.py b/app/backend/approaches/readdecomposeask.py
index 7e18b22..4c22cef 100644
--- a/app/backend/approaches/readdecomposeask.py
+++ b/app/backend/approaches/readdecomposeask.py
@@ -2,7 +2,6 @@ import re
 from typing import Any, List, Optional
 
 import openai
-from approaches.approach import Approach
 from azure.search.documents import SearchClient
 from azure.search.documents.models import QueryType
 from langchain.agents import AgentExecutor, Tool
@@ -10,6 +9,8 @@ from langchain.agents.react.base import ReActDocstoreAgent
 from langchain.callbacks.manager import CallbackManager
 from langchain.llms.openai import AzureOpenAI
 from langchain.prompts import BasePromptTemplate, PromptTemplate
+
+from approaches.approach import Approach
 from langchainadapters import HtmlCallbackHandler
 from text import nonewlines
 
diff --git a/app/backend/approaches/readretrieveread.py b/app/backend/approaches/readretrieveread.py
index 1285660..de453e0 100644
--- a/app/backend/approaches/readretrieveread.py
+++ b/app/backend/approaches/readretrieveread.py
@@ -1,13 +1,14 @@
 from typing import Any
 
 import openai
-from approaches.approach import Approach
 from azure.search.documents import SearchClient
 from azure.search.documents.models import QueryType
 from langchain.agents import AgentExecutor, Tool, ZeroShotAgent
 from langchain.callbacks.manager import CallbackManager, Callbacks
 from langchain.chains import LLMChain
 from langchain.llms.openai import AzureOpenAI
+
+from approaches.approach import Approach
 from langchainadapters import HtmlCallbackHandler
 from lookuptool import CsvLookupTool
 from text import nonewlines
diff --git a/app/backend/approaches/retrievethenread.py b/app/backend/approaches/retrievethenread.py
index be53dfa..2ce52ee 100644
--- a/app/backend/approaches/retrievethenread.py
+++ b/app/backend/approaches/retrievethenread.py
@@ -1,9 +1,10 @@
 from typing import Any
 
 import openai
-from approaches.approach import Approach
 from azure.search.documents import SearchClient
 from azure.search.documents.models import QueryType
+
+from approaches.approach import Approach
 from core.messagebuilder import MessageBuilder
 from text import nonewlines
 
diff --git a/app/backend/gunicorn.conf.py b/app/backend/gunicorn.conf.py
index 98b33af..e9fef7e 100644
--- a/app/backend/gunicorn.conf.py
+++ b/app/backend/gunicorn.conf.py
@@ -9,3 +9,4 @@ num_cpus = multiprocessing.cpu_count()
 workers = (num_cpus * 2) + 1
 threads = 1 if num_cpus == 1 else 2
 timeout = 600
+worker_class = "gthread"
diff --git a/app/backend/start_appservice.sh b/app/backend/start_appservice.sh
deleted file mode 100644
index 3ea1ed1..0000000
--- a/app/backend/start_appservice.sh
+++ /dev/null
@@ -1,1 +0,0 @@
-python3 -m gunicorn app:app
diff --git a/infra/main.bicep b/infra/main.bicep
index 47a835a..0e9d63c 100644
--- a/infra/main.bicep
+++ b/infra/main.bicep
@@ -111,7 +111,7 @@ module backend 'core/host/appservice.bicep' = {
     appServicePlanId: appServicePlan.outputs.id
     runtimeName: 'python'
     runtimeVersion: '3.10'
-    appCommandLine: 'start_appservice.sh'
+    appCommandLine: 'python3 -m gunicorn "app:create_app()"'
     scmDoBuildDuringDeployment: true
     managedIdentity: true
     appSettings: {
diff --git a/pyproject.toml b/pyproject.toml
index 1fb5881..7a135e4 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -2,12 +2,14 @@
 target-version = "py38"
 select = ["E", "F", "I", "UP"]
 ignore = ["E501", "E701"] # line too long, multiple statements on one line
+src = ["app/backend"]
 
 [tool.black]
 line-length = 300
 
 [tool.pytest.ini_options]
 addopts = "-ra --cov"
+pythonpath = ["app/backend"]
 
 [tool.coverage.paths]
 source = ["scripts", "app"]


Test patch:
diff --git a/.github/workflows/python-test.yaml b/.github/workflows/python-test.yaml
index d86400c..dd96622 100644
--- a/.github/workflows/python-test.yaml
+++ b/.github/workflows/python-test.yaml
@@ -14,7 +14,7 @@ jobs:
       fail-fast: false
       matrix:
         os: ["ubuntu-20.04"]
-        python_version: ["3.8", "3.9", "3.10", "3.11"]
+        python_version: ["3.9", "3.10", "3.11"]
     steps:
         - uses: actions/checkout@v3
         - name: Setup python
@@ -22,6 +22,15 @@ jobs:
           with:
             python-version: ${{ matrix.python_version }}
             architecture: x64
+        - name: Setup node
+          uses: actions/setup-node@v2
+          with:
+            node-version: 18
+        - name: Build frontend
+          run: |
+            cd ./app/frontend
+            npm install
+            npm run build
         - name: Install dependencies
           run: |
             python -m pip install --upgrade pip
diff --git a/tests/conftest.py b/tests/conftest.py
new file mode 100644
index 0000000..acfc828
--- /dev/null
+++ b/tests/conftest.py
@@ -0,0 +1,61 @@
+from collections import namedtuple
+from unittest import mock
+
+import pytest
+
+import app as backend_app
+from approaches.approach import Approach
+from approaches.chatreadretrieveread import ChatReadRetrieveReadApproach
+
+
+class MockedAskApproach(Approach):
+    def run(self, question, overrides):
+        assert question == "What is the capital of France?"
+        return {"answer": "Paris"}
+
+
+class MockedChatApproach(ChatReadRetrieveReadApproach):
+    def __init__(self):
+        pass
+
+    def run(self, history, overrides):
+        messages = ChatReadRetrieveReadApproach.get_messages_from_history(self, ChatReadRetrieveReadApproach.query_prompt_template, "gpt-3.5-turbo", history, "Generate search query")
+        assert messages[0]["role"] == "system"
+        assert messages[1]["content"] == "Generate search query"
+        assert messages[1]["role"] == "user"
+        return {"answer": "Paris", "data_points": [], "thoughts": ""}
+
+
+MockToken = namedtuple("MockToken", ["token", "expires_on"])
+
+
+class MockAzureCredential:
+    def get_token(self, uri):
+        return MockToken("mock_token", 9999999999)
+
+
+@pytest.fixture()
+def app():
+    # mock the DefaultAzureCredential
+    with mock.patch("app.DefaultAzureCredential") as mock_default_azure_credential:
+        mock_default_azure_credential.return_value = MockAzureCredential()
+        _app = backend_app.create_app()
+    _app.config.update(
+        {
+            "TESTING": True,
+            backend_app.CONFIG_ASK_APPROACHES: {"mock": MockedAskApproach()},
+            backend_app.CONFIG_CHAT_APPROACHES: {"mock": MockedChatApproach()},
+        }
+    )
+
+    yield _app
+
+
+@pytest.fixture()
+def client(app):
+    return app.test_client()
+
+
+@pytest.fixture()
+def runner(app):
+    return app.test_cli_runner()
diff --git a/tests/test_app.py b/tests/test_app.py
new file mode 100644
index 0000000..20a32b9
--- /dev/null
+++ b/tests/test_app.py
@@ -0,0 +1,43 @@
+def test_index(client):
+    response = client.get("/")
+    assert response.status_code == 200
+
+
+def test_ask_request_must_be_json(client):
+    response = client.post("/ask")
+    assert response.status_code == 415
+    assert response.json["error"] == "request must be json"
+
+
+def test_ask_with_unknown_approach(client):
+    response = client.post("/ask", json={"approach": "test"})
+    assert response.status_code == 400
+
+
+def test_ask_mock_approach(client):
+    response = client.post("/ask", json={"approach": "mock", "question": "What is the capital of France?"})
+    assert response.status_code == 200
+    assert response.json["answer"] == "Paris"
+
+
+def test_chat_request_must_be_json(client):
+    response = client.post("/chat")
+    assert response.status_code == 415
+    assert response.json["error"] == "request must be json"
+
+
+def test_chat_with_unknown_approach(client):
+    response = client.post("/chat", json={"approach": "test"})
+    assert response.status_code == 400
+
+
+def test_chat_mock_approach(client):
+    response = client.post(
+        "/chat",
+        json={
+            "approach": "mock",
+            "history": [{"user": "What is the capital of France?"}],
+        },
+    )
+    assert response.status_code == 200
+    assert response.json["answer"] == "Paris"
diff --git a/tests/test_messagebuilder.py b/tests/test_messagebuilder.py
index 91cd677..6eaaad2 100644
--- a/tests/test_messagebuilder.py
+++ b/tests/test_messagebuilder.py
@@ -1,4 +1,4 @@
-from app.backend.core.messagebuilder import MessageBuilder
+from core.messagebuilder import MessageBuilder
 
 
 def test_messagebuilder():
diff --git a/tests/test_modelhelper.py b/tests/test_modelhelper.py
index 33fd67d..28072fd 100644
--- a/tests/test_modelhelper.py
+++ b/tests/test_modelhelper.py
@@ -1,6 +1,6 @@
 import pytest
 
-from app.backend.core.modelhelper import (
+from core.modelhelper import (
     get_oai_chatmodel_tiktok,
     get_token_limit,
     num_tokens_from_messages,