Skip to content

Dockerfile for Flask bot #374

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

Merged
merged 3 commits into from
Oct 29, 2019
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
27 changes: 27 additions & 0 deletions libraries/functional-tests/functionaltestbot/Dockfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
# Copyright (c) Microsoft Corporation. All rights reserved.
# Licensed under the MIT License.

FROM python:3.7-slim as pkg_holder

ARG EXTRA_INDEX_URL
RUN pip config set global.extra-index-url "${EXTRA_INDEX_URL}"

COPY requirements.txt .
RUN pip download -r requirements.txt -d packages

FROM python:3.7-slim

ENV VIRTUAL_ENV=/opt/venv
RUN python3.7 -m venv $VIRTUAL_ENV
ENV PATH="$VIRTUAL_ENV/bin:$PATH"

COPY . /app
WORKDIR /app

COPY --from=pkg_holder packages packages

RUN pip install -r requirements.txt --no-index --find-links=packages && rm -rf packages

ENTRYPOINT ["python"]
EXPOSE 3978
CMD ["runserver.py"]
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
# Copyright (c) Microsoft Corporation. All rights reserved.
# Licensed under the MIT License.

from .app import APP

__all__ = ["APP"]
21 changes: 21 additions & 0 deletions libraries/functional-tests/functionaltestbot/flask_bot_app/app.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
# Copyright (c) Microsoft Corporation. All rights reserved.
# Licensed under the MIT License.

"""Bot app with Flask routing."""

from flask import Response

from .bot_app import BotApp


APP = BotApp()


@APP.flask.route("/api/messages", methods=["POST"])
def messages() -> Response:
return APP.messages()


@APP.flask.route("/api/test", methods=["GET"])
def test() -> Response:
return APP.test()
108 changes: 108 additions & 0 deletions libraries/functional-tests/functionaltestbot/flask_bot_app/bot_app.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
# Copyright (c) Microsoft Corporation. All rights reserved.
# Licensed under the MIT License.

import asyncio
import sys
from types import MethodType
from flask import Flask, Response, request

from botbuilder.core import (
BotFrameworkAdapter,
BotFrameworkAdapterSettings,
MessageFactory,
TurnContext,
)
from botbuilder.schema import Activity, InputHints

from .default_config import DefaultConfig
from .my_bot import MyBot


class BotApp:
"""A Flask echo bot."""

def __init__(self):
# Create the loop and Flask app
self.loop = asyncio.get_event_loop()
self.flask = Flask(__name__, instance_relative_config=True)
self.flask.config.from_object(DefaultConfig)

# Create adapter.
# See https://aka.ms/about-bot-adapter to learn more about how bots work.
self.settings = BotFrameworkAdapterSettings(
self.flask.config["APP_ID"], self.flask.config["APP_PASSWORD"]
)
self.adapter = BotFrameworkAdapter(self.settings)

# Catch-all for errors.
async def on_error(adapter, context: TurnContext, error: Exception):
# This check writes out errors to console log .vs. app insights.
# NOTE: In production environment, you should consider logging this to Azure
# application insights.
print(f"\n [on_turn_error]: {error}", file=sys.stderr)

# Send a message to the user
error_message_text = "Sorry, it looks like something went wrong."
error_message = MessageFactory.text(
error_message_text, error_message_text, InputHints.expecting_input
)
await context.send_activity(error_message)

# pylint: disable=protected-access
if adapter._conversation_state:
# If state was defined, clear it.
await adapter._conversation_state.delete(context)

self.adapter.on_turn_error = MethodType(on_error, self.adapter)

# Create the main dialog
self.bot = MyBot()

def messages(self) -> Response:
"""Main bot message handler that listens for incoming requests."""

if "application/json" in request.headers["Content-Type"]:
body = request.json
else:
return Response(status=415)

activity = Activity().deserialize(body)
auth_header = (
request.headers["Authorization"]
if "Authorization" in request.headers
else ""
)

async def aux_func(turn_context):
await self.bot.on_turn(turn_context)

try:
task = self.loop.create_task(
self.adapter.process_activity(activity, auth_header, aux_func)
)
self.loop.run_until_complete(task)
return Response(status=201)
except Exception as exception:
raise exception

@staticmethod
def test() -> Response:
"""
For test only - verify if the flask app works locally - e.g. with:
```bash
curl http://127.0.0.1:3978/api/test
```
You shall get:
```
test
```
"""
return Response(status=200, response="test\n")

def run(self, host=None) -> None:
try:
self.flask.run(
host=host, debug=False, port=self.flask.config["PORT"]
) # nosec debug
except Exception as exception:
raise exception
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
# Copyright (c) Microsoft Corporation. All rights reserved.
# Licensed under the MIT License.

from os import environ


class DefaultConfig:
""" Bot Configuration """

PORT: int = 3978
APP_ID: str = environ.get("MicrosoftAppId", "")
APP_PASSWORD: str = environ.get("MicrosoftAppPassword", "")
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
# Copyright (c) Microsoft Corporation. All rights reserved.
# Licensed under the MIT License.

from botbuilder.core import ActivityHandler, TurnContext
from botbuilder.schema import ChannelAccount


class MyBot(ActivityHandler):
"""See https://aka.ms/about-bot-activity-message to learn more about the message and other activity types."""

async def on_message_activity(self, turn_context: TurnContext):
await turn_context.send_activity(f"You said '{ turn_context.activity.text }'")

async def on_members_added_activity(
self, members_added: ChannelAccount, turn_context: TurnContext
):
for member_added in members_added:
if member_added.id != turn_context.activity.recipient.id:
await turn_context.send_activity("Hello and welcome!")
5 changes: 5 additions & 0 deletions libraries/functional-tests/functionaltestbot/requirements.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
# Copyright (c) Microsoft Corporation. All rights reserved.
# Licensed under the MIT License.

botbuilder-core>=4.5.0.b4
flask>=1.0.3
16 changes: 16 additions & 0 deletions libraries/functional-tests/functionaltestbot/runserver.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
# Copyright (c) Microsoft Corporation. All rights reserved.
# Licensed under the MIT License.

"""
To run the Flask bot app, in a py virtual environment,
```bash
pip install -r requirements.txt
python runserver.py
```
"""

from flask_bot_app import APP


if __name__ == "__main__":
APP.run(host="0.0.0.0")
92 changes: 92 additions & 0 deletions libraries/functional-tests/tests/direct_line_client.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
# Copyright (c) Microsoft Corporation. All rights reserved.
# Licensed under the MIT License.

from typing import Tuple

import requests
from requests import Response


class DirectLineClient:
"""A direct line client that sends and receives messages."""

def __init__(self, direct_line_secret: str):
self._direct_line_secret: str = direct_line_secret
self._base_url: str = "https://directline.botframework.com/v3/directline"
self._set_headers()
self._start_conversation()
self._watermark: str = ""

def send_message(self, text: str, retry_count: int = 3) -> Response:
"""Send raw text to bot framework using direct line api"""

url = "/".join(
[self._base_url, "conversations", self._conversation_id, "activities"]
)
json_payload = {
"conversationId": self._conversation_id,
"type": "message",
"from": {"id": "user1"},
"text": text,
}

success = False
current_retry = 0
bot_response = None
while not success and current_retry < retry_count:
bot_response = requests.post(url, headers=self._headers, json=json_payload)
current_retry += 1
if bot_response.status_code == 200:
success = True

return bot_response

def get_message(self, retry_count: int = 3) -> Tuple[Response, str]:
"""Get a response message back from the bot framework using direct line api"""

url = "/".join(
[self._base_url, "conversations", self._conversation_id, "activities"]
)
url = url + "?watermark=" + self._watermark

success = False
current_retry = 0
bot_response = None
while not success and current_retry < retry_count:
bot_response = requests.get(
url,
headers=self._headers,
json={"conversationId": self._conversation_id},
)
current_retry += 1
if bot_response.status_code == 200:
success = True
json_response = bot_response.json()

if "watermark" in json_response:
self._watermark = json_response["watermark"]

if "activities" in json_response:
activities_count = len(json_response["activities"])
if activities_count > 0:
return (
bot_response,
json_response["activities"][activities_count - 1]["text"],
)
return bot_response, "No new messages"
return bot_response, "error contacting bot for response"

def _set_headers(self) -> None:
headers = {"Content-Type": "application/json"}
value = " ".join(["Bearer", self._direct_line_secret])
headers.update({"Authorization": value})
self._headers = headers

def _start_conversation(self) -> None:
# Start conversation and get us a conversationId to use
url = "/".join([self._base_url, "conversations"])
bot_response = requests.post(url, headers=self._headers)

# Extract the conversationID for sending messages to bot
json_response = bot_response.json()
self._conversation_id = json_response["conversationId"]
26 changes: 26 additions & 0 deletions libraries/functional-tests/tests/test_py_bot.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
# Copyright (c) Microsoft Corporation. All rights reserved.
# Licensed under the MIT License.

import os
from unittest import TestCase

from direct_line_client import DirectLineClient


class PyBotTest(TestCase):
def test_deployed_bot_answer(self):
direct_line_secret = os.environ.get("DIRECT_LINE_KEY", "")
if direct_line_secret == "":
return

client = DirectLineClient(direct_line_secret)
user_message: str = "Contoso"

send_result = client.send_message(user_message)
self.assertIsNotNone(send_result)
self.assertEqual(200, send_result.status_code)

response, text = client.get_message()
self.assertIsNotNone(response)
self.assertEqual(200, response.status_code)
self.assertEqual(f"You said '{user_message}'", text)