From 23f28670fc436d76adf91dc5da7c575129918344 Mon Sep 17 00:00:00 2001 From: Krishnakanth Alagiri Date: Sun, 12 May 2024 03:21:38 -0400 Subject: [PATCH 1/2] =?UTF-8?q?=F0=9F=93=A6=20NEW:=20Wrapped=20REST=20API?= =?UTF-8?q?=20endpoint=20around=20Meeseeks=20core.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .env.example | 1 + meeseeks-api/backend.py | 116 +++++++++++++++++++++++++++++++++------- 2 files changed, 99 insertions(+), 18 deletions(-) diff --git a/.env.example b/.env.example index 8267d1d..0113502 100644 --- a/.env.example +++ b/.env.example @@ -11,6 +11,7 @@ VERSION=1.0.0 ENVMODE=dev LOG_LEVEL=DEBUG CACHE_DIR='/path/to/cache/directory' +MASTER_API_TOKEN='xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx' # * Home Assistant Configuration diff --git a/meeseeks-api/backend.py b/meeseeks-api/backend.py index ac58ef9..8e83d74 100644 --- a/meeseeks-api/backend.py +++ b/meeseeks-api/backend.py @@ -1,33 +1,113 @@ #!/usr/bin/env python3 -# TODO: Complete the API submodule by wrapping around the Meeseeks core. +""" +Meeseeks API + +This module implements a REST API for Meeseeks using Flask-RESTX. +It provides a single endpoint to interact with the Meeseeks core, +allowing users to submit queries and receive the executed action plan +as a JSON response. +""" +# TODO: API key authentication and rate limiting not implemented yet. +# Standard library modules import os import sys -from flask import Flask, request -from flask_restx import Api, Resource -# TODO: Need to package the application and import it as module +from typing import Dict + +# Third-party modules +from flask import Flask, request, jsonify +from flask_restx import Api, Resource, fields +from dotenv import load_dotenv + # Adding the parent directory to the path before importing the custom modules sys.path.append(os.path.join(os.path.dirname(os.path.abspath(__file__)), '..')) + # Custom imports - Meeseeks core modules -from core.task_master import generate_action_plan, run_action_plan +if True: + from core.task_master import generate_action_plan, run_action_plan + from core.classes import TaskQueue + from core.common import get_logger +# Load environment variables +load_dotenv() + +# Initialize logger +logging = get_logger(name="meeseeks-api") + +# Create Flask application app = Flask(__name__) -api = Api(app) +# Create API instance with Swagger documentation +api = Api(app, version='1.0', title='Meeseeks API', + description='Interact with Meeseeks through a REST API', + doc='/swagger-ui/') + +# Define API namespace +ns = api.namespace('api', description='Meeseeks operations') + +# Define API model for request and response +task_queue_model = api.model('TaskQueue', { + 'human_message': fields.String( + required=True, description='The original user query'), + 'action_steps': fields.List(fields.Nested(api.model('ActionStep', { + 'action_consumer': fields.String( + required=True, + description='The tool responsible for executing the action'), + 'action_type': fields.String( + required=True, + description='The type of action to be performed (get/set)'), + 'action_argument': fields.String( + required=True, + description='The specific argument for the action'), + 'result': fields.String( + description='The result of the executed action') + }))), +}) + + +@ns.route('/query') +class MeeseeksQuery(Resource): + """ + Endpoint to submit a query to Meeseeks and receive the executed + action plan as a JSON response. + """ + + @api.doc(security='apiKey') + @api.expect(api.model('Query', {'query': fields.String( + required=True, description='The user query')})) + @api.response(200, 'Success', task_queue_model) + @api.response(400, 'Invalid input') + @api.response(401, 'Unauthorized') + def post(self) -> Dict: + """ + Process a user query, generate and execute the action plan, + and return the result as a JSON. + + Requires a valid API token for authorization. + """ + # Get API token from headers + api_token = request.headers.get('X-API-Key') + + # Validate API token + if api_token != os.getenv("MASTER_API_TOKEN"): + logging.warning( + "Unauthorized API call attempt with token: %s", api_token) + return {"message": "Unauthorized"}, 401 + + # Get user query from request data + user_query = request.json.get('query') + if not user_query: + return {"message": "Invalid input: 'query' is required"}, 400 + logging.info("Received user query: %s", user_query) -@api.route('/generate_action_plan') -class GenerateActionPlanResource(Resource): - def post(self): - user_input = request.json.get('user_input') - action_plan_list, task_queue = generate_action_plan(user_input) - return {'action_plan_list': action_plan_list, 'task_queue': task_queue} + # Generate action plan from user query + task_queue: TaskQueue = generate_action_plan(user_query=user_query) + # Execute action plan + task_queue = run_action_plan(task_queue) -@api.route('/run_action_plan') -class RunActionPlanResource(Resource): - def post(self): - task_queue = request.json.get('task_queue') - ai_response = run_action_plan(task_queue) - return {'ai_response': ai_response} + # Return TaskQueue as JSON + logging.info("Returning executed action plan.") + return task_queue.dict(), 200 if __name__ == '__main__': From e5ab51f8fc2146f5838f750575611b0980392e99 Mon Sep 17 00:00:00 2001 From: Krishnakanth Alagiri Date: Sun, 12 May 2024 18:16:05 -0400 Subject: [PATCH 2/2] =?UTF-8?q?=F0=9F=93=A6=20NEW:=20Docker=20image=20for?= =?UTF-8?q?=20API=20&=20Build=20action=20update?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/docker-buildx.yml | 114 ++++++++++++++++++++-------- Dockerfile.api | 22 ++++++ Dockerfile => Dockerfile.base | 21 +---- Dockerfile.chat | 22 ++++++ meeseeks-api/README.md | 3 +- meeseeks-api/backend.py | 28 +++++-- 6 files changed, 153 insertions(+), 57 deletions(-) create mode 100644 Dockerfile.api rename Dockerfile => Dockerfile.base (72%) create mode 100644 Dockerfile.chat diff --git a/.github/workflows/docker-buildx.yml b/.github/workflows/docker-buildx.yml index 9048b0b..d8228cb 100644 --- a/.github/workflows/docker-buildx.yml +++ b/.github/workflows/docker-buildx.yml @@ -14,9 +14,9 @@ # If the branch name is 'release/1.0.2-dev', the image is tagged as '1.0.2-dev'. # # * The 'latest' and 'stable' tags allow us to easily switch between different versions. -# * The 'dev' tag allows you to have a separate version for development. +# * The 'dev' tag allows you to have a separate version for development. -name: Build Meseeks Chat Docker Image +name: Build and Push Docker Images on: workflow_dispatch: @@ -25,48 +25,98 @@ on: - "release/*" jobs: - build: + docker: runs-on: ubuntu-latest steps: - name: Checkout code - uses: actions/checkout@v2 + uses: actions/checkout@v4 + + - name: Extract version and release type + id: extract_version + run: | + BRANCH_NAME=${{ github.ref_name }} + VERSION=$(echo $BRANCH_NAME | cut -d'/' -f 2 | cut -d'-' -f 1) + RELEASE_TYPE=$(echo $BRANCH_NAME | cut -d'/' -f 2 | cut -d'-' -f 2) + echo "version=$VERSION" >> $GITHUB_OUTPUT + echo "release_type=$RELEASE_TYPE" >> $GITHUB_OUTPUT + + - name: Docker meta for meeseeks-base + id: meta_base + uses: docker/metadata-action@v5 + with: + images: ghcr.io/bearlike/meeseeks-base + tags: | + type=raw,value=${{ steps.extract_version.outputs.version }}${{ steps.extract_version.outputs.release_type == 'dev' && '-dev' || '' }} + type=raw,value=latest,enable=${{ steps.extract_version.outputs.release_type == 'latest' }} + type=raw,value=stable,enable=${{ steps.extract_version.outputs.release_type == 'stable' }} + type=raw,value=dev,enable=${{ steps.extract_version.outputs.release_type == 'dev' }} + + - name: Docker meta for meeseeks-chat + id: meta_chat + uses: docker/metadata-action@v5 + with: + images: ghcr.io/bearlike/meeseeks-chat + tags: | + type=raw,value=${{ steps.extract_version.outputs.version }}${{ steps.extract_version.outputs.release_type == 'dev' && '-dev' || '' }} + type=raw,value=latest,enable=${{ steps.extract_version.outputs.release_type == 'latest' }} + type=raw,value=stable,enable=${{ steps.extract_version.outputs.release_type == 'stable' }} + type=raw,value=dev,enable=${{ steps.extract_version.outputs.release_type == 'dev' }} + + - name: Docker meta for meeseeks-api + id: meta_api + uses: docker/metadata-action@v5 + with: + images: ghcr.io/bearlike/meeseeks-api + tags: | + type=raw,value=${{ steps.extract_version.outputs.version }}${{ steps.extract_version.outputs.release_type == 'dev' && '-dev' || '' }} + type=raw,value=latest,enable=${{ steps.extract_version.outputs.release_type == 'latest' }} + type=raw,value=stable,enable=${{ steps.extract_version.outputs.release_type == 'stable' }} + type=raw,value=dev,enable=${{ steps.extract_version.outputs.release_type == 'dev' }} + + - name: Set up QEMU + uses: docker/setup-qemu-action@v3 - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v1 + uses: docker/setup-buildx-action@v3 - - name: Login to DockerHub - uses: docker/login-action@v1 + - name: Login to GHCR + if: github.event_name != 'pull_request' + uses: docker/login-action@v3 with: registry: ghcr.io - username: ${{ github.actor }} + username: ${{ github.repository_owner }} password: ${{ secrets.GITHUB_TOKEN }} - - name: Extract branch name - id: extract_branch - shell: bash - run: | - BRANCH_NAME=$(echo ${{ github.ref }} | sed 's/refs\/heads\///') - echo "branch=$BRANCH_NAME" >> $GITHUB_ENV - echo "Extracted branch name: $BRANCH_NAME" + - name: Build and push meeseeks-base + uses: docker/build-push-action@v5 + with: + context: . + file: Dockerfile.base + platforms: linux/amd64,linux/arm64 + push: ${{ github.event_name != 'pull_request' }} + tags: ${{ steps.meta_base.outputs.tags }} + labels: ${{ steps.meta_base.outputs.labels }} - - name: Set version and channel - id: version_channel - run: | - BRANCH_NAME=${{ env.branch }} - VERSION=$(echo $BRANCH_NAME | cut -d'/' -f 2 | cut -d'-' -f 1) - CHANNEL=$(echo $BRANCH_NAME | cut -d'/' -f 2 | cut -d'-' -f 2) - echo "version=$VERSION" >> $GITHUB_ENV - echo "channel=$CHANNEL" >> $GITHUB_ENV - echo "Extracted version: $VERSION" - echo "Extracted channel: $CHANNEL" - echo "Extracted branch name: $BRANCH_NAME" + - name: Build and push meeseeks-chat + uses: docker/build-push-action@v5 + with: + context: . + file: Dockerfile.chat + platforms: linux/amd64,linux/arm64 + push: ${{ github.event_name != 'pull_request' }} + tags: ${{ steps.meta_chat.outputs.tags }} + labels: ${{ steps.meta_chat.outputs.labels }} + build-args: | + BASE_IMAGE=ghcr.io/bearlike/meeseeks-base:${{ steps.extract_version.outputs.version }}${{ steps.extract_version.outputs.release_type == 'dev' && '-dev' || '' }} - - name: Build and push - uses: docker/build-push-action@v2 + - name: Build and push meeseeks-api + uses: docker/build-push-action@v5 with: context: . - push: true - tags: | - ghcr.io/bearlike/meeseeks-chat:${{ env.version }}${{ env.channel == 'dev' && '-dev' || '' }} - ghcr.io/bearlike/meeseeks-chat:${{ env.channel == 'latest' && 'latest' || env.channel == 'stable' && 'stable' || 'dev' }} + file: Dockerfile.api platforms: linux/amd64,linux/arm64 + push: ${{ github.event_name != 'pull_request' }} + tags: ${{ steps.meta_api.outputs.tags }} + labels: ${{ steps.meta_api.outputs.labels }} + build-args: | + BASE_IMAGE=ghcr.io/bearlike/meeseeks-base:${{ steps.extract_version.outputs.version }}${{ steps.extract_version.outputs.release_type == 'dev' && '-dev' || '' }} diff --git a/Dockerfile.api b/Dockerfile.api new file mode 100644 index 0000000..33828ef --- /dev/null +++ b/Dockerfile.api @@ -0,0 +1,22 @@ +# syntax=docker/dockerfile:1 + +# Dockerfile.api +ARG BASE_IMAGE="ghcr.io/bearlike/meeseeks-base:latest" +FROM $BASE_IMAGE + +ARG TITLE="Meeseeks API: Personal Assistant" + +LABEL title=$TITLE + +# Install the meeseeks-api dependencies +WORKDIR /app/meeseeks-api +RUN poetry install + +# Set API specific environment variable +ENV MASTER_API_TOKEN='msk-strong-password' + +# Expose port 5123 for the API +EXPOSE 5123 + +# Run the API application +ENTRYPOINT ["poetry", "run", "python", "backend.py"] diff --git a/Dockerfile b/Dockerfile.base similarity index 72% rename from Dockerfile rename to Dockerfile.base index 0a9fc6b..0581542 100644 --- a/Dockerfile +++ b/Dockerfile.base @@ -1,9 +1,10 @@ -# Dockerfile to build meeseeks-chat with core dependencies. +# syntax=docker/dockerfile:1 +# Dockerfile.base FROM python:3.11-buster # Set the title, GitHub repo URL, version, and author -ARG TITLE="Meeseeks Chat: Personal Assistant" \ +ARG TITLE="Meeseeks Base" \ VERSION="1.0.0" \ AUTHOR="Krishnakanth Alagiri" @@ -43,11 +44,7 @@ RUN pip install 'poetry>=1.8,<1.9' # Install the core dependencies RUN poetry install -# Install the meeseeks-chat dependencies -WORKDIR /app/meeseeks-chat -RUN poetry install - -# Set default environment variablesfor Meeseeks +# Set default environment variables for Meeseeks (common ones) ENV CACHE_DIR='/tmp/meeseeks_cache' \ DEFAULT_MODEL='gpt-3.5-turbo' \ LOG_LEVEL=DEBUG \ @@ -56,13 +53,3 @@ ENV CACHE_DIR='/tmp/meeseeks_cache' \ COLOREDLOGS_FIELD_STYLES='asctime=color=240;name=45,inverse' \ COLOREDLOGS_LEVEL_STYLES='info=220;spam=22;debug=34;verbose=34;notice=220;warning=202;success=118,bold;error=124;critical=background=red' \ COLOREDLOGS_LOG_FORMAT='%(asctime)s [%(name)s] %(levelname)s %(message)s' - - -# Expose port 8501 for Streamlit -EXPOSE 8502 - -# Healthcheck to ensure the Streamlit server is running -HEALTHCHECK CMD curl --fail http://localhost:8502/_stcore/health - -# Run the Streamlit application -ENTRYPOINT ["poetry", "run", "python", "-m", "streamlit", "run", "chat_master.py", "--server.port=8502", "--server.address=0.0.0.0"] diff --git a/Dockerfile.chat b/Dockerfile.chat new file mode 100644 index 0000000..c0af248 --- /dev/null +++ b/Dockerfile.chat @@ -0,0 +1,22 @@ +# syntax=docker/dockerfile:1 + +# Dockerfile.chat +ARG BASE_IMAGE="ghcr.io/bearlike/meeseeks-base:latest" +FROM $BASE_IMAGE + +ARG TITLE="Meeseeks Chat: Personal Assistant" + +LABEL title=$TITLE + +# Install the meeseeks-chat dependencies +WORKDIR /app/meeseeks-chat +RUN poetry install + +# Expose port 8502 for Streamlit +EXPOSE 8502 + +# Healthcheck to ensure the Streamlit server is running +HEALTHCHECK CMD curl --fail http://localhost:8502/_stcore/health + +# Run the Streamlit application +ENTRYPOINT ["poetry", "run", "python", "-m", "streamlit", "run", "chat_master.py", "--server.port=8502", "--server.address=0.0.0.0"] diff --git a/meeseeks-api/README.md b/meeseeks-api/README.md index a3bbbbd..e2c77a1 100644 --- a/meeseeks-api/README.md +++ b/meeseeks-api/README.md @@ -1,5 +1,6 @@ # meeseeks-api -REST API Engine wrapped around the meeseeks-core +- REST API Engine wrapped around the meeseeks-core. +- No components are explicitly tested for safety or security. Use with caution in a production environment. [Link to GitHub Repository](https://github.com/bearlike/Personal-Assistant/edit/main/README.md) diff --git a/meeseeks-api/backend.py b/meeseeks-api/backend.py index 8e83d74..61b4279 100644 --- a/meeseeks-api/backend.py +++ b/meeseeks-api/backend.py @@ -29,16 +29,28 @@ # Load environment variables load_dotenv() +# Get the API token from the environment variables +# The default API token is "msk-strong-password" +MASTER_API_TOKEN = os.getenv("MASTER_API_TOKEN", "msk-strong-password") # Initialize logger logging = get_logger(name="meeseeks-api") - +logging.debug("Starting API server with API token: %s", MASTER_API_TOKEN) # Create Flask application app = Flask(__name__) + +authorizations = { + 'apikey': { + 'type': 'apiKey', + 'in': 'header', + 'name': 'X-API-KEY' + } +} +VERSION = os.getenv("VERSION", "(Dev)") # Create API instance with Swagger documentation -api = Api(app, version='1.0', title='Meeseeks API', +api = Api(app, version=VERSION, title='Meeseeks API', description='Interact with Meeseeks through a REST API', - doc='/swagger-ui/') + doc='/swagger-ui/', authorizations=authorizations, security='apikey') # Define API namespace ns = api.namespace('api', description='Meeseeks operations') @@ -70,7 +82,7 @@ class MeeseeksQuery(Resource): action plan as a JSON response. """ - @api.doc(security='apiKey') + @api.doc(security='apikey') @api.expect(api.model('Query', {'query': fields.String( required=True, description='The user query')})) @api.response(200, 'Success', task_queue_model) @@ -84,10 +96,12 @@ def post(self) -> Dict: Requires a valid API token for authorization. """ # Get API token from headers - api_token = request.headers.get('X-API-Key') + api_token = request.headers.get('X-API-Key', None) # Validate API token - if api_token != os.getenv("MASTER_API_TOKEN"): + if api_token is None: + return {"message": "API token is not provided."}, 401 + if api_token != MASTER_API_TOKEN: logging.warning( "Unauthorized API call attempt with token: %s", api_token) return {"message": "Unauthorized"}, 401 @@ -111,4 +125,4 @@ def post(self) -> Dict: if __name__ == '__main__': - app.run(debug=True) + app.run(debug=True, host='0.0.0.0', port=5123)