Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

issue #1: ailab search endpoint and tests #8

Merged
merged 2 commits into from
Nov 29, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
40 changes: 40 additions & 0 deletions .env.template
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,9 @@ FINESSE_BACKEND_GITHUB_STATIC_FILE_URL=https://api.github.com/repos/ai-cfia/fine
# Message for Finesse data search failures. Optional.
# FINESSE_BACKEND_ERROR_FINESSE_DATA_FAILED="finesse-data static search failed"

# Message for ailab-db search failures. Optional.
# FINESSE_BACKEND_ERROR_AILAB_FAILED="Ailab-db search failed."

# Message for unexpected errors. Optional.
# FINESSE_BACKEND_ERROR_UNEXPECTED="Unexpected error."

Expand All @@ -35,3 +38,40 @@ FINESSE_BACKEND_GITHUB_STATIC_FILE_URL=https://api.github.com/repos/ai-cfia/fine

# Regular expression pattern used for sanitizing input to prevent log injection. Optional.
# FINESSE_BACKEND_SANITIZE_PATTERN="[^\w \d\"#\$%&'\(\)\*\+,-\.\/:;?@\^_`{\|}~]+|\%\w+|;|/|\(|\)"

# API key for OpenAI, used for authentication when making requests.
# Obtain from: https://portal.azure.com/#home
OPENAI_API_KEY=<OpenAI-API-Key>

# The version of the OpenAI API being used.
# Example: 2023-05-15
OPENAI_API_VERSION=<OpenAI-API-Version>

# Deployment name for GPT-based models in Azure OpenAI.
# Example: davinci
AZURE_OPENAI_GPT_DEPLOYMENT=<Azure-OpenAI-GPT-Deployment>

# Deployment name for ChatGPT models in Azure OpenAI.
# Example: chat
AZURE_OPENAI_CHATGPT_DEPLOYMENT=<Azure-OpenAI-ChatGPT-Deployment>

# Data Source Name (DSN) for configuring a database connection in Louis's system.
# Format: postgresql://PGUSER:PGPASSWORD@DB_SERVER_CONTAINER_NAME/PGBASE
LOUIS_DSN=<Louis-DSN>

# Schema within the Louis database system.
# Example: louis_0.0.5
LOUIS_SCHEMA=<Louis-Schema>

# Endpoint URL for making requests to the OpenAI API.
# Obtain along with the OpenAI API Key.
OPENAI_ENDPOINT=<OpenAI-Endpoint>

# File containing the weights for the search.
# Example: finesse-weights.json
FINESSE_WEIGHTS=<Finesse-Weights>

# Specific OpenAI API model engine to be used.
# Example: ada
OPENAI_API_ENGINE=<OpenAI-API-Engine>

26 changes: 26 additions & 0 deletions app/ailab_db/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import logging

from ailab.db import DBError, connect_db
from ailab.db.api import match_documents_from_text_query


class AilabDbSearchException(DBError):
"""Custom exception for errors in searching from ailab-db."""


class EmptyQueryError(DBError):
"Raised when the search query is empty."


def ailab_db_search(query):
if not query:
logging.error("Empty search query received")
raise EmptyQueryError("Search query cannot be empty")

try:
with connect_db().cursor() as cursor:
return match_documents_from_text_query(cursor, query)
# TODO: handle specific exceptions
except Exception as e:
logging.error(f"finesse-data fetch failed: {e}", exc_info=True)
raise AilabDbSearchException(f"finesse-data fetch failed: {e}") from e
16 changes: 15 additions & 1 deletion app/blueprints/search.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import re
from functools import wraps

from flask import Blueprint, current_app, jsonify, request
from index_search import AzureIndexSearchQueryError, search

from app.ailab_db import DBError, ailab_db_search
from app.finesse_data import FinesseDataFetchException, fetch_data
from app.utils import sanitize

Expand Down Expand Up @@ -49,3 +49,17 @@ def search_static():
return jsonify({"error": current_app.config["ERROR_FINESSE_DATA_FAILED"]}), 500
except Exception:
return jsonify({"error": current_app.config["ERROR_UNEXPECTED"]}), 500


@search_blueprint.route("/ailab", methods=["POST"])
@require_non_empty_query
def search_ailab_db():
query = request.json["query"]
query = sanitize(query, current_app.config["SANITIZE_PATTERN"])
try:
results = ailab_db_search(query)
return jsonify(results)
except DBError:
return jsonify({"error": current_app.config["ERROR_AILAB_FAILED"]}), 500
except Exception:
return jsonify({"error": current_app.config["ERROR_UNEXPECTED"]}), 500
8 changes: 6 additions & 2 deletions app/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@
DEFAULT_ERROR_FINESSE_DATA_FAILED = "finesse-data static search failed"
DEFAULT_ERROR_UNEXPECTED = "Unexpected error."
DEFAULT_FUZZY_MATCH_THRESHOLD = 90
DEFAULT_ERROR_AILAB_FAILED = "Ailab-db search failed."
DEFAULT_ERROR_UNEXPECTED = "Unexpected error."
DEFAULT_SANITIZE_PATTERN = (
"[^\w \d\"#\$%&'\(\)\*\+,-\.\/:;?@\^_`{\|}~]+|\%\w+|;|/|\(|\)"
)
Expand Down Expand Up @@ -42,8 +44,10 @@ class Config:
"FINESSE_BACKEND_ERROR_AZURE_FAILED", DEFAULT_ERROR_AZURE_FAILED
)
ERROR_FINESSE_DATA_FAILED = os.getenv(
"FINESSE_BACKEND_ERROR_FINESSE_DATA_FAILED",
DEFAULT_ERROR_FINESSE_DATA_FAILED,
"FINESSE_BACKEND_ERROR_FINESSE_DATA_FAILED", DEFAULT_ERROR_FINESSE_DATA_FAILED
)
ERROR_AILAB_FAILED = os.getenv(
"FINESSE_BACKEND_ERROR_AILAB_FAILED", DEFAULT_ERROR_AILAB_FAILED
)
ERROR_UNEXPECTED = os.getenv(
"FINESSE_BACKEND_ERROR_UNEXPECTED", DEFAULT_ERROR_UNEXPECTED
Expand Down
7 changes: 7 additions & 0 deletions finesse-weights.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"recency": 1,
"traffic": 1,
"current": 0.5,
"typicality": 0.2,
"similarity": 1
}
1 change: 1 addition & 0 deletions requirements-production.txt
Original file line number Diff line number Diff line change
Expand Up @@ -6,3 +6,4 @@ python-dotenv==1.0.0 # Released: 2023-02-24
git+https://github.com/ai-cfia/azure-db.git@main#subdirectory=azure-ai-search
fuzzywuzzy==0.18.0
python-Levenshtein== 0.23.0
git+https://github.com/ai-cfia/ailab-db@main
1 change: 1 addition & 0 deletions requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -6,3 +6,4 @@ python-dotenv
git+https://github.com/ai-cfia/azure-db.git@main#subdirectory=azure-ai-search
fuzzywuzzy
python-Levenshtein
git+https://github.com/ai-cfia/ailab-db@main
2 changes: 1 addition & 1 deletion tests/common.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,5 +16,5 @@ class TestAzureSearchConfig:
class TestConfig(Config):
AZURE_CONFIG = TestAzureSearchConfig()
FINESSE_DATA_URL = ""
DEBUG = ""
DEBUG = True
TESTING = True
36 changes: 36 additions & 0 deletions tests/test_ailab_search.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import unittest
from unittest.mock import patch

from app.ailab_db import DBError
from app.app_creator import create_app
from tests.common import TestConfig


class TestAilabSearch(unittest.TestCase):
def setUp(self):
self.config = TestConfig()
self.app = create_app(self.config)
self.client = self.app.test_client()

def test_search_ailab_success(self):
with patch("app.blueprints.search.ailab_db_search") as mock_search:
mock_search.return_value = {"some": "ailab data"}
response = self.client.post("/search/ailab", json={"query": "ailab query"})
self.assertEqual(response.status_code, 200)
self.assertEqual(response.json, {"some": "ailab data"})

def test_search_ailab_no_query(self):
response = self.client.post("/search/ailab", json={})
self.assertEqual(response.status_code, 400)

def test_search_ailab_db_error(self):
with patch("app.blueprints.search.ailab_db_search") as mock_search:
mock_search.side_effect = DBError("Ailab DB failed")
response = self.client.post("/search/ailab", json={"query": "ailab query"})
self.assertEqual(response.status_code, 500)

def test_search_ailab_unexpected_error(self):
with patch("app.blueprints.search.ailab_db_search") as mock_search:
mock_search.side_effect = Exception("Unexpected error")
response = self.client.post("/search/ailab", json={"query": "ailab query"})
self.assertEqual(response.status_code, 500)
29 changes: 29 additions & 0 deletions tests/test_azure_search.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import unittest
from unittest.mock import patch

from app.app_creator import create_app
from tests.common import TestConfig


class TestAzureSearch(unittest.TestCase):
def setUp(self):
self.config = TestConfig()
self.app = create_app(self.config)
self.client = self.app.test_client()

def test_search_azure_success(self):
with patch("app.blueprints.search.search") as mock_search:
mock_search.return_value = {"some": "azure data"}
response = self.client.post("/search/azure", json={"query": "azure query"})
self.assertEqual(response.status_code, 200)
self.assertEqual(response.json, {"some": "azure data"})

def test_search_azure_no_query(self):
response = self.client.post("/search/azure", json={})
self.assertEqual(response.status_code, 400)

def test_search_azure_error(self):
with patch("app.blueprints.search.search") as mock_search:
mock_search.side_effect = Exception("Azure search failed")
response = self.client.post("/search/azure", json={"query": "azure query"})
self.assertEqual(response.status_code, 500)