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

285 file sharing #59

Merged
merged 20 commits into from
Nov 9, 2022
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
1 change: 1 addition & 0 deletions Rasa_Bot/actions/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ WORKDIR /app
# Copy actions requirements
COPY requirements-actions.txt ./

COPY tst.PNG ./
# Change to root user to install dependencies
USER root

Expand Down
63 changes: 60 additions & 3 deletions Rasa_Bot/actions/actions_common.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
import logging

from celery import Celery
from niceday_client import NicedayClient
from rasa_sdk import Action
from rasa_sdk.events import FollowupAction
from .definitions import REDIS_URL

from rasa_sdk.events import FollowupAction, SlotSet
from .definitions import REDIS_URL, NICEDAY_API_ENDPOINT

celery = Celery(broker=REDIS_URL)

Expand Down Expand Up @@ -37,3 +37,60 @@ async def run(self, dispatcher, tracker, domain):
logging.info("no celery error")

return []


class SendMetadata(Action):
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Might be overkill, but a few unrelated actions could all end up together in actions_common, so I would add a short doctring here making clear that the SendMetadata relates to the UploadFile and SetFilePath actions (at least, I assume they do)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Definitely a good suggestion. It will help in understanding how to use such actions in the future.

def name(self):
return "action_send_metadata"

async def run(self, dispatcher, tracker, domain):
"""
Sends the text message specified in the 'text' value of the json_message,
and sends the image identified by the id_file under the 'attachmentIds' key.

The id_file is obtained in the action_upload_file as a result of
a file uploaded to the NiceDay server. The id is stored in the
uploaded_file_id slot.

The uploaded file is a local one, and is uses the file indicated
by the action_set_file_path
"""
id_file = tracker.get_slot("uploaded_file_id")
dispatcher.utter_message(
json_message={"text": "image",
"attachmentIds": [id_file]},
)
return[]


class UploadFile(Action):
def name(self):
return "action_upload_file"

async def run(self, dispatcher, tracker, domain):
client = NicedayClient(NICEDAY_API_ENDPOINT)
user_id = int(tracker.current_state()['sender_id'])

filepath = tracker.get_slot('upload_file_path')
with open(filepath, 'rb') as content:
file = content.read()

response = client.upload_file(user_id, filepath, file)
file_id = response['id']
logging.info(response)
logging.info(file_id)

return[SlotSet("uploaded_file_id", file_id)]


class SetFilePath(Action):
def name(self):
return "action_set_file_path"

async def run(self, dispatcher, tracker, domain):

# TODO: This is hardcoded for testing. Needs to be set according to the use case

filepath = '/app/tst.PNG'
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

tst.PNG is a beautiful Italian Blue Italian Blue

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

not a random choice XD


return[SlotSet("upload_file_path", filepath)]
38 changes: 37 additions & 1 deletion Rasa_Bot/actions/actions_minimum_functional_product.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,16 @@
"""
import datetime
import logging
from typing import Text, Dict, Any

from dateutil.relativedelta import relativedelta
from dateutil.rrule import rrule, DAILY
from niceday_client import NicedayClient, definitions
from paalgorithms import weekly_kilometers
from rasa_sdk import Action
from rasa_sdk import Action, Tracker
from rasa_sdk.events import SlotSet
from rasa_sdk.executor import CollectingDispatcher
from rasa_sdk.forms import FormValidationAction
from virtual_coach_db.dbschema.models import Users
from virtual_coach_db.helper.helper_functions import get_db_session

Expand Down Expand Up @@ -175,3 +178,36 @@ async def run(self, dispatcher, tracker, domain):
"This is a tracker",
recursive_rule)
return[]


class ValidateActivityGetFileForm(FormValidationAction):
def name(self) -> Text:
return 'validate_activity_get_file_form'

def validate_received_file_text(
self, slot_value: Text, dispatcher: CollectingDispatcher,
tracker: Tracker, domain: Dict[Text, Any]) -> Dict[Text, Any]:
# pylint: disable=unused-argument
"""Validate the presence of an attached file"""

# here the message metadata are retrieved
events = tracker.current_state()['events']

user_events = [e for e in events if e['event'] == 'user']
# we defined the metadata key name 'attachmentIds' is defined in the broker
received_file_ids_list = user_events[-1]['metadata']['attachmentIds']
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So the last event always contains the list of received files? Is there an async problem possible where two images are sent simultaneously and the user_events list contains a mess of events for the two sends? (apologies if I've misunderstood what's happening here)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a validation action that runs on user text input and the new message structure created in the custom channels always contains the metadata (even if the files list is empty). So, in theory we always get this. But theory and reality do not always match. It is a good idea to surround this with some verification.

This action runs on a single message, so the is no possibility to mess with this action.


if not self._is_valid_input(received_file_ids_list):
dispatcher.utter_message(response="utter_no_attachment")
return {"received_file_text": None}

# At this point the file ID should be saved in the DB for
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Are we the ones sending these images, or will the user ever send images to us? Will the images ever be sensitive data that we need to be extra careful with in the db?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I guess what I'm wondering is, can anyone with the file ID fetch the file? Is it stored on Niceday servers?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So, the files are stored in the NiceDay server, and for accessing them the id and the access token are needed.
The images that we will send will not contain sensitive data, and the files sent by the users will not be stored in anyway on our system, so we should be on the safe side.

# re-accessing the shared file

return {"received_file_text": slot_value}

@staticmethod
def _is_valid_input(value):
if len(value) == 0:
return False
return True
Binary file added Rasa_Bot/actions/tst.PNG
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
58 changes: 40 additions & 18 deletions Rasa_Bot/custom_channels.py
Original file line number Diff line number Diff line change
@@ -1,33 +1,51 @@
import inspect
from typing import Text, Callable, Awaitable, Any
import os
import typing
from typing import Text, Callable, Awaitable, Any, Dict, List

from rasa.core.channels.channel import InputChannel, OutputChannel, UserMessage
from rasa.core.channels.channel import InputChannel, UserMessage, CollectingOutputChannel
from sanic import Blueprint, response
from sanic.request import Request
from sanic.response import HTTPResponse

from niceday_client import NicedayClient

NICEDAY_API_URL = 'http://niceday_api:8080/'
NICEDAY_API_URL = os.getenv('NICEDAY_API_ENDPOINT')


class NicedayOutputChannel(OutputChannel):
class NicedayOutputChannel(CollectingOutputChannel):
"""
Output channel that sends messages to Niceday server
"""
def __init__(self):
self.niceday_client = NicedayClient(niceday_api_uri=NICEDAY_API_URL)

@classmethod
def name(cls) -> Text:
return "niceday_output_channel"

async def send_text_message(
self, recipient_id: Text, text: Text, **kwargs: Any
) -> None:
"""Send a message through this channel."""
for message_part in text.strip().split("\n\n"):
self.niceday_client.post_message(int(recipient_id), message_part)
def _message(self, # pylint: disable=too-many-arguments, arguments-renamed
recipient_id: typing.Optional[str],
text: typing.Optional[str] = None,
image: typing.Optional[str] = None,
buttons: typing.Optional[List[Dict[str, Any]]] = None,
attachment: typing.Optional[str] = None,
custom: typing.Optional[Dict[str, Any]] = None
) -> Dict:
msg_metadata = None
if custom is not None:
text = custom["text"]
msg_metadata = custom["attachmentIds"]
custom = None
obj = {
"recipient_id": recipient_id,
"text": text,
"image": image,
"buttons": buttons,
"attachment": attachment,
"custom": custom,
"metadata": msg_metadata
}

# filter out any values that are `None`
return {k: v for k, v in obj.items() if v is not None}


class NicedayInputChannel(InputChannel):
Expand Down Expand Up @@ -57,18 +75,22 @@ async def health(request: Request) -> HTTPResponse: # pylint: disable=unused-ar
async def receive(request: Request) -> HTTPResponse:
sender_id = request.json.get("sender") # method to get sender_id
text = request.json.get("message") # method to fetch text

metadata = request.json.get("metadata")
collector = self.get_output_channel()
await on_new_message(
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does this ever timeout (or fail in some way)? Does something cause the app to retry if that happens? (this is just out of curiosity)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This contacts the Rasa server, that has a timeout. But if it fails for whatever reason, at the moment we do nothing, you are right

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This contacts the Rasa server, that has a timeout. But if it fails for whatever reason, at the moment we do nothing, you are right

UserMessage(text, collector, sender_id, input_channel=self.name())
UserMessage(text,
collector,
sender_id,
input_channel=self.name(),
metadata=metadata)
)
return response.text("success")
return response.json(collector.messages)

return custom_webhook

def get_output_channel(self) -> OutputChannel:
def get_output_channel(self) -> CollectingOutputChannel:
"""
Register output channel. This is the output channel that is used when calling the
'trigger_intent' endpoint.
"""
return self.output_channel
return NicedayOutputChannel()
12 changes: 12 additions & 0 deletions Rasa_Bot/data/nlu.yml
Original file line number Diff line number Diff line change
Expand Up @@ -98,3 +98,15 @@ nlu:
- "Oke"
- "Ok"

- intent: request_metadata
examples: |
- "Metadata"
- "Ik wil metadata"
- "metadata"
- "Dit is metadata"

- intent: user_shares_image
examples: |
- "file"
- "File"
- "Start file"
11 changes: 10 additions & 1 deletion Rasa_Bot/data/rules.yml
Original file line number Diff line number Diff line change
Expand Up @@ -52,4 +52,13 @@ rules:
- requested_slot: null
- action: utter_confirm
- action: action_store_user_preferences_to_db
- action: utter_user_preferences
- action: utter_user_preferences

- rule: Test metadata
steps:
- intent: request_metadata
- action: action_set_file_path
- action: action_upload_file
- action: utter_file_sent
- action: action_send_metadata
- action: action_end_dialog
13 changes: 12 additions & 1 deletion Rasa_Bot/data/stories_minimum_functional_product.yml
Original file line number Diff line number Diff line change
Expand Up @@ -60,4 +60,15 @@ stories:
- action: utter_ask_foreseen_hrs
- intent: deny
- action: utter_be_careful_for_hrs
- action: action_end_dialog
- action: action_end_dialog

# Example to get user file
- story: example get file
steps:
- intent: user_shares_image
- action: utter_send_me_a_file
- action: activity_get_file_form
- active_loop: activity_get_file_form
- active_loop: null
- action: utter_file_received
- action: action_end_dialog
4 changes: 2 additions & 2 deletions Rasa_Bot/docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ services:
rasa_server:
build:
context: .
dockerfile: Dockerfile.rasa_server
dockerfile: Dockerfile
ports:
- 5005:5005
volumes:
Expand All @@ -12,7 +12,7 @@ services:
rasa_actions:
build:
context: ./actions/
dockerfile: Dockerfile.rasa_actions
dockerfile: Dockerfile
expose: ["5055"]
environment:
- DB_HOST=host.docker.internal:5432
Expand Down
24 changes: 23 additions & 1 deletion Rasa_Bot/domain/domain_common.yml
Original file line number Diff line number Diff line change
@@ -1,13 +1,35 @@
intents:
- urgent
- request_metadata
slots:
### Id obtained from NiceDay when uploading a file
uploaded_file_id:
type: float
initial_value: 0
influence_conversation: false
mappings:
- type: custom

### Id obtained from NiceDay when uploading a file
upload_file_path:
type: text
influence_conversation: false
mappings:
- type: custom
actions:
- action_end_dialog
- mark_dialog_as_completed
- action_handle_urgent_intent
- action_upload_file
- action_send_metadata
- action_set_file_path


responses:
# To refer a user to a human
utter_refer:
- text: "Volgens mij heb je een probleem waar een mens je beter mee kan helpen. Ik raad je aan om contact op te nemen met iemand die je vertrouwt, zoals je huisarts."
- text: "Volgens mij heb je een probleem waar een mens je beter mee kan helpen. Ik raad je aan om contact op te nemen met iemand die je vertrouwt, zoals je huisarts."
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this for when the user shares a really horrible image?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we could absolutely use it in this way!


## send a file
utter_file_sent:
- text: "Here is your file"
32 changes: 32 additions & 0 deletions Rasa_Bot/domain/domain_get_file.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
intents:
- user_shares_image

slots:
received_file_text:
type: float
influence_conversation: true
mappings:
- type: from_text
conditions:
- active_loop: activity_get_file_form


responses:
utter_send_me_a_file:
- text: "Now I ask for a file"
utter_ask_activity_get_file_form_received_file_text:
- text: "Send me a file"
utter_file_received:
- text: "I received a file"
utter_no_attachment:
- text: "I see no attached files"

actions:
- validate_activity_get_file_form

forms:
activity_get_file_form:
ignored_intents:
- user_shares_image
required_slots:
- received_file_text
2 changes: 2 additions & 0 deletions Rasa_Bot/domain/domain_minimum_functional_product.yml
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ slots:
mappings:
- type: custom


responses:
# General
utter_greet:
Expand Down Expand Up @@ -75,6 +76,7 @@ responses:
utter_reminder_is_set:
- text: "The reminder is set. ..."


actions:
- action_get_plan_week
- action_get_age_from_database
Expand Down
Binary file not shown.
Binary file not shown.