Skip to content

Commit

Permalink
Add Backend Message and Frontend Banner for Service Maintenance (#294)
Browse files Browse the repository at this point in the history
### Description
- I am taking over this draft PR to implement display of service
maintenance banner selectively based on environment variables
- Added `health_status` endpoint on backend that reads from the env var
- Frontend displays maintenance banner if health status is `maintenance`
or if backend has no response coming back

### Tests
- When backend is down OR when backend has environment variable
`SERVICE_MAINTENANCE_BANNER=True`
<img width="1152" alt="image"
src="https://github.com/astronomer/ask-astro/assets/26350341/ca2fef7c-923e-43b1-b445-8003bbfe47f9">
- Other times (when env var is empty or if banner set to False)
<img width="1117" alt="image"
src="https://github.com/astronomer/ask-astro/assets/26350341/e4e70373-0a20-48ea-af97-b3160c5a2a6d">
- Slack
<img width="454" alt="image"
src="https://github.com/astronomer/ask-astro/assets/26350341/90c9874f-1e29-489b-98b0-b4b89228e098">


closes #302

---------

Co-authored-by: David Xue <xuegdxw@gmail.com>
  • Loading branch information
sunank200 and davidgxue authored Mar 1, 2024
1 parent 6fbe2d4 commit 3836ee8
Show file tree
Hide file tree
Showing 10 changed files with 114 additions and 9 deletions.
8 changes: 7 additions & 1 deletion api/ask_astro/models/request.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
from __future__ import annotations

from datetime import datetime
from typing import Any
from typing import Any, Literal
from uuid import UUID

from langchain.schema import AIMessage, BaseMessage, HumanMessage
Expand Down Expand Up @@ -109,3 +109,9 @@ def from_dict(cls, response_dict: dict[str, Any]) -> AskAstroRequest:
is_example=response_dict.get("is_example", False),
client=response_dict["client"],
)


class HealthStatus(BaseModel):
"""Represents the health status of the service."""

status: Literal["ok", "maintenance"] = Field(..., description="The health status of the service")
2 changes: 2 additions & 0 deletions api/ask_astro/rest/controllers/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
from sanic import Sanic, response

from ask_astro.rest.controllers.get_request import on_get_request
from ask_astro.rest.controllers.health_status import on_get_health_status
from ask_astro.rest.controllers.list_recent_requests import on_list_recent_requests
from ask_astro.rest.controllers.post_request import on_post_request
from ask_astro.rest.controllers.submit_feedback import on_submit_feedback
Expand All @@ -31,6 +32,7 @@ def register_routes(api: Sanic):
RouteConfig(on_get_request, "/requests/<request_id:uuid>", ["GET"], "get_request"),
RouteConfig(on_post_request, "/requests", ["POST"], "post_request"),
RouteConfig(on_submit_feedback, "/requests/<request_id:uuid>/feedback", ["POST"], "submit_feedback"),
RouteConfig(on_get_health_status, "/health_status", ["GET"], "health_status"),
]

for route_config in routes:
Expand Down
26 changes: 26 additions & 0 deletions api/ask_astro/rest/controllers/health_status.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
"""
Handles GET requests to the /ask/{question_id} endpoint.
"""
from __future__ import annotations

from logging import getLogger

from sanic import Request, json
from sanic_ext import openapi

from ask_astro import settings
from ask_astro.models.request import HealthStatus

logger = getLogger(__name__)


@openapi.definition(response=HealthStatus.schema_json())
async def on_get_health_status(request: Request) -> json:
"""
Handles GET requests to the /health_status endpoint.
:param request: The Sanic request object.
"""
if settings.SHOW_SERVICE_MAINTENANCE_BANNER:
return json({"status": "maintenance"}, status=200)
return json({"status": "healthy"}, status=200)
12 changes: 11 additions & 1 deletion api/ask_astro/services/questions.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
from ask_astro.clients.firestore import firestore_client
from ask_astro.config import FirestoreCollections, PromptPreprocessingConfig
from ask_astro.models.request import AskAstroRequest, Source
from ask_astro.settings import SHOW_SERVICE_MAINTENANCE_BANNER

logger = getLogger(__name__)

Expand All @@ -19,6 +20,10 @@ class InvalidRequestPromptError(Exception):
"""Exception raised when the prompt string in the request object is invalid"""


class RequestDuringMaintenanceException(Exception):
"""Exception raised when a request is still somehow received on the backend when server is in maintenance"""


class QuestionAnsweringError(Exception):
"""Exception raised when an error occurs during question answering"""

Expand All @@ -37,6 +42,10 @@ async def _update_firestore_request(request: AskAstroRequest) -> None:


def _preprocess_request(request: AskAstroRequest) -> None:
if SHOW_SERVICE_MAINTENANCE_BANNER:
error_msg = "Ask Astro is currently undergoing maintenance and will be back shortly. We apologize for any inconvenience this may cause!"
request.response = error_msg
raise RequestDuringMaintenanceException(error_msg)
if len(request.prompt) > PromptPreprocessingConfig.max_char:
error_msg = "Question text is too long. Please try making a new thread and shortening your question."
request.response = error_msg
Expand All @@ -62,6 +71,7 @@ def _preprocess_request(request: AskAstroRequest) -> None:
retry=retry_if_not_exception_type(InvalidRequestPromptError),
stop=stop_after_attempt(5),
wait=wait_exponential(multiplier=1, max=10),
reraise=True,
)
async def answer_question(request: AskAstroRequest) -> None:
"""
Expand Down Expand Up @@ -110,7 +120,7 @@ async def answer_question(request: AskAstroRequest) -> None:
except Exception as e:
# If there's an error, mark the request as errored and add it to the database
request.status = "error"
if not isinstance(e, InvalidRequestPromptError):
if not isinstance(e, (InvalidRequestPromptError, RequestDuringMaintenanceException)):
request.response = "Sorry, something went wrong. Please try again later."
raise QuestionAnsweringError("An error occurred during question answering.") from e
else:
Expand Down
2 changes: 2 additions & 0 deletions api/ask_astro/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,3 +20,5 @@
CONVERSATIONAL_RETRIEVAL_LOAD_QA_CHAIN_DEPLOYMENT_NAME = os.environ.get(
"CONVERSATIONAL_RETRIEVAL_LOAD_QA_CHAIN_DEPLOYMENT_NAME", "gpt-4-32k"
)

SHOW_SERVICE_MAINTENANCE_BANNER = os.environ.get("SHOW_SERVICE_MAINTENANCE_BANNER", "False").upper() == "TRUE"
7 changes: 5 additions & 2 deletions api/ask_astro/slack/controllers/mention.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
from slack_sdk.web.async_client import AsyncWebClient

from ask_astro.models.request import AskAstroRequest
from ask_astro.services.questions import answer_question
from ask_astro.services.questions import InvalidRequestPromptError, RequestDuringMaintenanceException, answer_question
from ask_astro.slack.utils import get_blocks, markdown_to_slack

logger = getLogger(__name__)
Expand Down Expand Up @@ -121,6 +121,9 @@ async def on_mention(body: dict[str, Any], ack: AsyncAck, say: AsyncSay, client:
except Exception as e:
await client.reactions_remove(name=THOUGHT_BALLOON_REACTION, channel=channel_id, timestamp=ts)
await try_add_reaction(client, FAILURE_REACTION, channel_id, ts)
await say(text=FAILURE_MESSAGE, thread_ts=ts)
if not isinstance(e, (InvalidRequestPromptError, RequestDuringMaintenanceException)):
await say(text=FAILURE_MESSAGE, thread_ts=ts)
else:
await say(text=str(request.response), thread_ts=ts)

raise e
24 changes: 24 additions & 0 deletions tests/api/ask_astro/rest/controllers/test_health_status.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
from unittest.mock import patch

import pytest


@pytest.mark.asyncio
@pytest.mark.parametrize(
"show_maintenance_banner, expected_response",
[
(True, {"status": "maintenance"}),
(False, {"status": "healthy"}),
],
)
async def test_health_status(app, show_maintenance_banner, expected_response):
"""
Test the /health_status endpoint by mocking banner status environment variable
"""

with patch("ask_astro.settings.SHOW_SERVICE_MAINTENANCE_BANNER", new=show_maintenance_banner):
_, response = await app.asgi_client.get("/health_status")

# Validating the response status code and content
assert response.status == 200
assert response.json == expected_response
3 changes: 2 additions & 1 deletion ui/src/routes/+layout.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,7 @@
</div>
{/if}
{#if !$page.data.publicServiceAnnouncement}
<form
method="post"
action="/?/submitPrompt"
Expand Down Expand Up @@ -115,8 +116,8 @@
{/if}
</div>
</form>
{/if}
</section>
<slot />
<div class="footer">
Expand Down
23 changes: 19 additions & 4 deletions ui/src/routes/+page.server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,19 +21,34 @@ const limiter = new RateLimiter({
}
});

const publicServiceAnnouncement = "Public Service Announcement: Ask Astro is currently undergoing maintenance and will be back shortly. We apologize for any inconvenience this may cause!";

export const load: PageServerLoad = async (event) => {
await limiter.cookieLimiter?.preflight(event);

let health_status;
try {
const requests = await fetch(`${ASK_ASTRO_API_URL}/requests`);
health_status = await fetch(`${ASK_ASTRO_API_URL}/health_status`);
health_status = await health_status.json();
} catch (err) {
}

return requests.json();
try {
const requests = await fetch(`${ASK_ASTRO_API_URL}/requests`);
return {
requests: await requests.json(),
publicServiceAnnouncement: (health_status?.status === "maintenance" || !health_status) ? publicServiceAnnouncement : null,
};
} catch (err) {
console.error(err);

return { requests: [] };
return {
requests: [],
publicServiceAnnouncement: (health_status?.status === "maintenance" || !health_status) ? publicServiceAnnouncement : null,
};
}
};


export const actions = {
submitPrompt: async (event: RequestEvent) => {

Expand Down
16 changes: 16 additions & 0 deletions ui/src/routes/+page.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,13 @@
<title>Ask Astro</title>
</svelte:head>

<!-- Display the PSA banner if the message exists -->
{#if data.publicServiceAnnouncement}
<div class="psa-banner">
{data.publicServiceAnnouncement}
</div>
{/if}

<p class="previously-asked pt-4">Previously asked questions</p>

<div class="grid gap-2 pt-2 pb-12 home-grid-cols">
Expand All @@ -35,4 +42,13 @@
grid-template-columns: repeat(auto-fit, minmax(350px, 1fr));
width: 100%;
}
.psa-banner {
background-color: #ffcc00;
padding: 15px;
margin-bottom: 20px;
text-align: center;
color: black; /* Text color */
font-weight: bold;
}
</style>

0 comments on commit 3836ee8

Please sign in to comment.