diff --git a/.github/workflows/push-image.yml b/.github/workflows/push-image.yml index cb4c0ea..3b9b70d 100644 --- a/.github/workflows/push-image.yml +++ b/.github/workflows/push-image.yml @@ -11,11 +11,30 @@ on: options: - '05-assistive-chatbot' - '02-household-queries' + subdomain: + description: 'Subdomain of navalabs.co' + type: choice + required: true + default: 'chat' + options: + - 'chat' + - 'chatbot' + - 'chatbdt' + - 'chat-bdt' + - 'bdtbot' + - 'bdt-bot' + - 'bdt-chat' + - 'bdt-chatbot' + - 'chatbot-prototype' + - 'chat.zone' service_name: - description: 'Name of target AWS service. Leave blank if unsure.' + description: 'Name of target service. Leave blank if unsure' type: choice + default: '' options: - '' + - 'container-service-3' + - 'container-service-2' - 'chatbot-chainlit-svc' - 'secure-chatbot-svc' build_image: @@ -27,51 +46,148 @@ on: description: "Deploy image" required: true type: boolean + default: 'true' + # image_tag: + # description: 'Tag/Version of the image to push' + # required: true + # type: string + # default: '0.06' + create_new_svc: + description: "Create new Lightsail service" + required: true + type: boolean + default: 'false' + delete_images: + description: 'Delete previous images associated with service' + required: true + type: boolean default: 'false' env: + AWS_REGION: us-east-1 IMAGE_NAME: localimage jobs: publish-image: runs-on: ubuntu-latest steps: - - name: Check inputs - id: check_inputs - run: | - service_name="${{ inputs.service_name }}" - if [ "${service_name}" = "" ]; then - case "${{ inputs.dockerfile_folder }}" in - '02-household-queries') service_name='secure-chatbot-svc';; - '05-assistive-chatbot') service_name='chatbot-chainlit-svc';; - *) echo "Unknown dockerfile_folder: '${dockerfile_folder}'"; exit 2;; - esac - fi - echo "service_name=$service_name" >> $GITHUB_OUTPUT - - case "${service_name}" in - # The image_tag is specific to the `*-svc` service - 'secure-chatbot-svc') image_tag='0.01';; - 'chatbot-chainlit-svc') image_tag='chatbot-chainlit';; - *) echo "Unknown service_name: '${service_name}'"; exit 3;; - esac - echo "image_tag=$image_tag" >> $GITHUB_OUTPUT - - name: Configure AWS credentials uses: aws-actions/configure-aws-credentials@v4 with: - aws-region: us-east-1 + aws-region: ${{ env.AWS_REGION }} aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }} aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }} mask-aws-account-id: true # TODO: secure credentials: https://github.com/aws-actions/amazon-ecr-login?tab=readme-ov-file#ecr-private # https://github.com/docker/login-action?tab=readme-ov-file#aws-elastic-container-registry-ecr + # https://medium.com/@lukhee/automating-aws-lightsail-deployments-with-github-actions-53c73c9a1c1f - - name: Login to Amazon ECR - id: login-ecr - uses: aws-actions/amazon-ecr-login@v2 - with: - mask-password: true + + - name: "Upgrade AWS CLI version and setup lightsailctl" + run: | + # aws --version + # curl "https://awscli.amazonaws.com/awscli-exe-linux-x86_64.zip" -o "awscliv2.zip" + # unzip awscliv2.zip + # sudo ./aws/install --bin-dir /usr/local/bin --install-dir /usr/local/aws-cli --update + # which aws + aws --version + sudo curl "https://s3.us-west-2.amazonaws.com/lightsailctl/latest/linux-amd64/lightsailctl" -o "/usr/local/bin/lightsailctl" + sudo chmod +x /usr/local/bin/lightsailctl + aws lightsail push-container-image help + + + - name: Check inputs + id: check_inputs + run: | + service_name="${{ inputs.subdomain }}-svc" + # if [ "${service_name}" = "" ]; then + # case "${{ inputs.dockerfile_folder }}" in + # '02-household-queries') service_name='secure-chatbot-svc';; + # '05-assistive-chatbot') service_name='chatbot-chainlit-svc';; + # *) exit 1;; + # esac + # fi + echo "service_name=$service_name" >> $GITHUB_OUTPUT + + if [ ${{ inputs.create_new_svc }} == 'false' ]; then + echo "Since not creating new service, checking if service '$service_name' exists" + aws lightsail get-container-services --service-name "$service_name" + fi + + # image_tag="${{ inputs.image_tag }}" + # if [ "${image_tag}" = "" ]; then + # case "${service_name}" in + # # The image_tag is specific to the `*-svc` service + # 'secure-chatbot-svc') image_tag='0.01';; + # 'chatbot-chainlit-svc') image_tag='chatbot-chainlit';; + # container-service-*) image_tag='not-used';; + # *) echo "Unknown service_name: '${service_name}'"; exit 3;; + # esac + # fi + # echo "image_tag=$image_tag" >> $GITHUB_OUTPUT + + - name: "Create new Lightsail container service" + if: inputs.create_new_svc + env: + DOMAIN_NAME: navalabs.co + FULL_DOMAIN: ${{ inputs.subdomain }}.navalabs.co + SERVICE_NAME: ${{ steps.check_inputs.outputs.service_name }} + run: | + # check if service already exists + if aws lightsail get-container-services --service-name "$SERVICE_NAME" > /dev/null; then + echo "Already exists: $SERVICE_NAME!" + else + # `micro` power is needed for it's memory capacity; 60%+ memory is needed for the vector DB + aws lightsail create-container-service --service-name $SERVICE_NAME --power micro --scale 1 --public-domain-names navalabs-cert=$FULL_DOMAIN + + echo "Waiting for service to be ready before updating container service" + while true; do + sleep 15 + SVC_STATE=$(aws lightsail get-container-services --service-name "$SERVICE_NAME" | jq -r '.containerServices[0].state') + echo "service state: $SVC_STATE" + if [ "$SVC_STATE" == "READY" ]; then + break + fi + done + + SVC_URL=$(aws lightsail get-container-services --service-name "$SERVICE_NAME" | jq -r '.containerServices[0].url') + # Remove 'https://' prefix + URL_DOMAIN=${SVC_URL#https://} + # Remove '/' suffix + TARGET_DOMAIN=${URL_DOMAIN%/} + + # If domain entry exists, delete it + OLD_TARGET=$(aws lightsail get-domain --domain-name $DOMAIN_NAME | jq -r ".domain.domainEntries[] | select( .name == \"$FULL_DOMAIN\" ) | .target") + if [ "$OLD_TARGET" ] ; then + echo "Deleting existing '$FULL_DOMAIN' entry with target '$OLD_TARGET'" + aws lightsail delete-domain-entry --domain-name $DOMAIN_NAME --domain-entry "type=A,isAlias=true,name=$FULL_DOMAIN,target=$OLD_TARGET" + fi + + echo "Creating DNS assignment by adding a domain entry $FULL_DOMAIN to target $TARGET_DOMAIN" + aws lightsail create-domain-entry --domain-name $DOMAIN_NAME --domain-entry "type=A,isAlias=true,name=$FULL_DOMAIN,target=$TARGET_DOMAIN" + fi + + - name: "Delete previous container images" + if: inputs.delete_images + env: + SERVICE_NAME: ${{ steps.check_inputs.outputs.service_name }} + run: | + AWS_IMAGES=$(aws lightsail get-container-images --region "$AWS_REGION" --service-name "$SERVICE_NAME" --output text) + IMAGE_NAMES=$(echo $AWS_IMAGES | grep -Eo ':"$SERVICE_NAME"\.${{ inputs.image-name }}\.[0-9]+') + echo $IMAGE_NAMES + FIRST=0 + while read LINE; do + if [ "$FIRST" -ne 0 ]; then + aws lightsail delete-container-image --region "$AWS_REGION" --service-name "$SERVICE_NAME" --image $LINE; + fi + FIRST=1; + done <<< $IMAGE_NAMES + + # - name: "Login to Amazon ECR" + # id: login-ecr + # uses: aws-actions/amazon-ecr-login@v2 + # with: + # mask-password: true - name: "Checkout source code" if: inputs.build_image @@ -84,23 +200,75 @@ jobs: # TODO: make this more easily editable and secure # The DOT_ENV_FILE_CONTENTS contains LITERAL_API_KEY, OPENAI_API_KEY, RETRIEVE_K, LLM_MODEL_NAME, SUMMARIZER_LLM_MODEL_NAME echo "${{secrets.DOT_ENV_FILE_CONTENTS}}" > .env + echo "BUILD_DATE=$(date +%Y-%m-%d-%T)" >> .env + echo "GIT_SHA=${{ github.sha }}" >> .env docker build -t "$IMAGE_NAME" --build-arg GURU_CARDS_URL="https://docs.google.com/uc?export=download&id=${{ secrets.GURU_CARDS_URL_ID }}" . - - name: "Publish image to AWS ECR'" - id: publish_image + + - name: "Publish image to Lightsail" if: inputs.build_image + id: pub_image_to_ls env: - ECR_PATH: ${{ steps.login-ecr.outputs.registry }}/${{ secrets.ECR_REPO }} + ECR_PATH: ${{ steps.login-ecr.outputs.registry }}/${{ secrets.ECR_REPO }} + SERVICE_NAME: ${{ steps.check_inputs.outputs.service_name }} + # IMAGE_TAG: ${{ steps.check_inputs.outputs.image_tag }} + # LABEL must match regex ^(?:[a-z0-9]{1,2}|[a-z0-9][a-z0-9-]+[a-z0-9])$ + LABEL: git-push + IMAGE_SHA_TAG: ${{ github.sha }} run: | - image_tag="${{ steps.check_inputs.outputs.image_tag }}" - echo "# Publishing image ${image_tag} to $ECR_PATH" + echo "# Publishing image for $SERVICE_NAME" + aws lightsail push-container-image --region $AWS_REGION --service-name "$SERVICE_NAME" --label "$LABEL" --image "$IMAGE_NAME" - docker tag "$IMAGE_NAME" "$ECR_PATH:${image_tag}" - docker push "$ECR_PATH:${image_tag}" + LS_DOCKER_IMAGE=$(aws lightsail get-container-images --service-name "$SERVICE_NAME" | jq -r .containerImages[0].image) + echo "Image name: '$LS_DOCKER_IMAGE'" + echo "LS_DOCKER_IMAGE=$LS_DOCKER_IMAGE" >> $GITHUB_ENV - - name: "Update AWS Service" + - name: Deploy container on AWS Lightsail if: inputs.deploy_image env: - CLUSTER_NAME: genai-experiments + SERVICE_NAME: ${{ steps.check_inputs.outputs.service_name }} run: | - aws ecs update-service --force-new-deployment --cluster "$CLUSTER_NAME" --service "${{ steps.check_inputs.outputs.service_name }}" + TEMPLATE='{ + "serviceName": "$SERVICE_NAME", + "containers": { + "chatbot": { + "image": "$LS_DOCKER_IMAGE", + "command": [], + "environment": { + "ENV": "PROD", + "BUILD_DATE": "$BUILD_DATE" + }, + "ports": { + "8000": "HTTP" + } + } + }, + "publicEndpoint": { + "containerName": "chatbot", + "containerPort": 8000, + "healthCheck": { + "healthyThreshold": 2, + "unhealthyThreshold": 4, + "timeoutSeconds": 20, + "intervalSeconds": 60, + "path": "/healthcheck", + "successCodes": "200-499" + } + } + }' + echo "$TEMPLATE" | BUILD_DATE=$(date +%Y-%m-%d-%T%z) envsubst > config.json + cat config.json + aws lightsail create-container-service-deployment --cli-input-json file://config.json + + # aws lightsail create-container-service-deployment --region ${{ inputs.aws-region }} --cli-input-json '${{ inputs.aws-lightsail-service-config }}' > /dev/null + # aws lightsail update-container-service --service-name "$SERVICE_NAME" --no-is-disabled + + # TODO: Wait for deployment to complete + # TODO: warm up vector DB on startup + + # - name: "Update AWS Service" + # if: inputs.deploy_image + # env: + # CLUSTER_NAME: genai-experiments + # run: | + # aws ecs update-service --force-new-deployment --cluster "$CLUSTER_NAME" --service "${{ steps.check_inputs.outputs.service_name }}" diff --git a/05-assistive-chatbot/.env-DEV b/05-assistive-chatbot/.env-DEV new file mode 100644 index 0000000..f30e04c --- /dev/null +++ b/05-assistive-chatbot/.env-DEV @@ -0,0 +1,7 @@ + +ENABLE_CHATBOT_API=False + +CHAT_ENGINE='Summaries' +LLM_MODEL_NAME='openai :: gpt-3.5-turbo-instruct' +RETRIEVE_K=4 +SUMMARIZER_LLM_MODEL_NAME='openai :: gpt-3.5-turbo-instruct' diff --git a/05-assistive-chatbot/.env-PROD b/05-assistive-chatbot/.env-PROD new file mode 100644 index 0000000..f054aec --- /dev/null +++ b/05-assistive-chatbot/.env-PROD @@ -0,0 +1,8 @@ +ENABLE_CHATBOT_API=True + +CHATBOT_LOG_LEVEL='DEBUG' + +CHAT_ENGINE='Summaries' +LLM_MODEL_NAME='openai :: gpt-3.5-turbo-instruct' +RETRIEVE_K=4 +SUMMARIZER_LLM_MODEL_NAME='openai :: gpt-3.5-turbo-instruct' diff --git a/05-assistive-chatbot/chatbot-chainlit.py b/05-assistive-chatbot/chatbot-chainlit.py index 5449766..eec8120 100755 --- a/05-assistive-chatbot/chatbot-chainlit.py +++ b/05-assistive-chatbot/chatbot-chainlit.py @@ -6,6 +6,7 @@ """ import logging +import os import pprint import chainlit as cl @@ -23,13 +24,13 @@ logger.info("Chatbot API loaded: %s", chatbot_api.__name__) +## TODO: Enable users to log in so that they can be distinguished in GetLiteralAI feedback logs + @cl.on_chat_start async def init_chat(): - elements = [ - cl.Text(name="side-text", display="side", content="Side Text"), - ] - await cl.Message("Example of side-text", elements=elements).send() + build_date = os.environ.get("BUILD_DATE", "unknown") + await cl.Message(f"Welcome to the Assistive Chat prototype (built {build_date})").send() # https://docs.chainlit.io/api-reference/chat-settings chat_settings = cl.ChatSettings( @@ -124,6 +125,9 @@ async def message_submitted(message: cl.Message): if not cl.user_session.get("settings_applied", False): return + # TODO: Provide visual feedback that chatbot is working, e.g., add Chainlit spinner + # TODO: Send results as they are generated + chat_engine = cl.user_session.get("chat_engine") response = chat_engine.gen_response(message.content) diff --git a/05-assistive-chatbot/chatbot/__init__.py b/05-assistive-chatbot/chatbot/__init__.py index e9f951c..2edb8e0 100644 --- a/05-assistive-chatbot/chatbot/__init__.py +++ b/05-assistive-chatbot/chatbot/__init__.py @@ -1,5 +1,6 @@ import logging import os +from datetime import date import dotenv @@ -22,9 +23,11 @@ def configure_logging(): logging.info("Configured logging level: %s", log_level) +env = os.environ.get("ENV", "DEV") +print(f"Loading .env-{env}") +dotenv.load_dotenv(f".env-{env}") dotenv.load_dotenv() configure_logging() - logger = logging.getLogger(__name__) @@ -40,13 +43,15 @@ def configure_logging(): # Set to true to enable caching for faster responses and optimizing prompts using DSPy os.environ.setdefault("DSP_CACHEBOOL", "false") +os.environ.setdefault("BUILD_DATE", str(date.today())) + @utils.verbose_timer(logger) def _init_settings(): # Remember to update ChatSettings in chatbot-chainlit.py when adding new settings # and update chatbot/engines/__init.py:CHATBOT_SETTING_KEYS return { - "env": os.environ.get("ENV", "DEV"), + "env": env, "enable_api": is_true(os.environ.get("ENABLE_CHATBOT_API", "False")), "chat_engine": os.environ.get("CHAT_ENGINE", "Direct"), "model": os.environ.get("LLM_MODEL_NAME", "mock :: llm"), diff --git a/05-assistive-chatbot/chatbot_api.py b/05-assistive-chatbot/chatbot_api.py index 862c035..5117b6e 100755 --- a/05-assistive-chatbot/chatbot_api.py +++ b/05-assistive-chatbot/chatbot_api.py @@ -7,6 +7,7 @@ """ import logging +import os from functools import cached_property from typing import Dict @@ -51,7 +52,12 @@ def query(message: str | Dict): def healthcheck(request: Request): logger.info(request.headers) # TODO: Add a health check - https://pypi.org/project/fastapi-healthchecks/ - return HTMLResponse("Healthy") + + git_sha = os.environ.get("GIT_SHA", "") + build_date = os.environ.get("BUILD_DATE", "") + + logger.info("Returning: Healthy %s %s", build_date, git_sha) + return HTMLResponse(f"Healthy {build_date} {git_sha}") if __name__ == "__main__":