diff --git a/code-samples/eventing/bookstore-sample-app/ML-sentiment-analysis/.funcignore b/code-samples/eventing/bookstore-sample-app/ML-sentiment-analysis/.funcignore new file mode 100644 index 000000000..e8e281c66 --- /dev/null +++ b/code-samples/eventing/bookstore-sample-app/ML-sentiment-analysis/.funcignore @@ -0,0 +1,5 @@ + +# Use the .funcignore file to exclude files which should not be +# tracked in the image build. To instruct the system not to track +# files in the image build, add the regex pattern or file information +# to this file. diff --git a/code-samples/eventing/bookstore-sample-app/ML-sentiment-analysis/.gitignore b/code-samples/eventing/bookstore-sample-app/ML-sentiment-analysis/.gitignore new file mode 100644 index 000000000..965f0d4ef --- /dev/null +++ b/code-samples/eventing/bookstore-sample-app/ML-sentiment-analysis/.gitignore @@ -0,0 +1,5 @@ + +# Functions use the .func directory for local runtime data which should +# generally not be tracked in source control. To instruct the system to track +# .func in source control, comment the following line (prefix it with '# '). +/.func diff --git a/code-samples/eventing/bookstore-sample-app/ML-sentiment-analysis/Procfile b/code-samples/eventing/bookstore-sample-app/ML-sentiment-analysis/Procfile new file mode 100644 index 000000000..bb57f5a81 --- /dev/null +++ b/code-samples/eventing/bookstore-sample-app/ML-sentiment-analysis/Procfile @@ -0,0 +1 @@ +web: python -m parliament . diff --git a/code-samples/eventing/bookstore-sample-app/ML-sentiment-analysis/README.md b/code-samples/eventing/bookstore-sample-app/ML-sentiment-analysis/README.md new file mode 100644 index 000000000..115f9f6d5 --- /dev/null +++ b/code-samples/eventing/bookstore-sample-app/ML-sentiment-analysis/README.md @@ -0,0 +1,393 @@ +# Sentiment Analysis Service for Bookstore Reviews + +As a bookstore owner, you aim to receive instant notifications in a Slack channel whenever a customer submits a new **negative** review comment. By leveraging Knative Function, you can set up a serverless function that contains a simple sentiment analysis service to categorize review comments by sentiment. +## What Knative features will we learn about? + +- The easiness to use **Knative Function** to deploy your service, and make it be managed by **Knative Serving**, which give you the ability to auto-scale your service to zero, and scale up to handle the demand. + +## What does the final deliverable look like? +A running serverless Knative function that contains a python application that can receives the new review comments as CloudEvent and returns the sentiment classification of the input text as CloudEvent. + +The function's output will be only from +- Positive +- Neutral +- Negative +## Install Prerequisites + +--- +(Warning box: please make sure you have a running cluster with Knative Eventing and Serving installed. If not, click here. And you have the container registry ready.) + +--- + +### Prerequisite 1: Install Knative `func` CLI +Knative Function enables you to easily create, build, and deploy stateless, event-driven functions as [Knative Services](https://knative.dev/docs/serving/services/#:~:text=Knative%20Services%20are%20used%20to,the%20Service%20to%20be%20configured) by using the func CLI. + +In order to do so, you need to install the `func` CLI. +You can follow the [official documentation](https://knative.dev/docs/getting-started/install-func/) to install the `func` CLI. + +Running `func version` in your terminal to verify the installation, and you should see the version of the `func` CLI you installed. + +## Implementation +The process is straightforward: +- Begin by utilizing the `func create` command to generate your code template. +- Next, incorporate your unique code into this template. +- Finally, execute `func deploy` to deploy your application seamlessly to the Kubernetes cluster. + +This workflow ensures a smooth transition from development to deployment within the Knative Functions ecosystem. + +--- +a warning box: +(Troubleshooting: if you see `command not found`, you may need to add the `func` CLI to your PATH.) +--- +### Step 1: Create a Knative Function template + +Create a new function using the `func` CLI: + +```bash +func create -l +``` + +In this case, we are creating a python function, so the command will be: + +```bash +func create -l python sentiment-analysis +``` + +This command will create a new directory with the name `sentiment-analysis` and a bunch of files in it. The `func` CLI will generate a basic function template for you to start with. + +You can find all the supported languages templates [here](https://knative.dev/docs/functions/). + +The file tree will look like this: +```bash +sentiment-analysis +├── func.yaml +├── .funcignore +├── .gitignore +├── requirements.txt +├── app.sh +├── Procfile +└── func.py + +``` +### Step 2: Replace the generated code with the sentiment analysis logic +`func.py` is the file that contains the code for the function. You can replace the generated code with the sentiment analysis logic. You can use the following code as a starting point: + +```python title="func.py" +from parliament import Context +from flask import Request,request, jsonify +import json +from textblob import TextBlob +from time import sleep +from cloudevents.http import CloudEvent, to_structured + +# The function to convert the sentiment analysis result into a CloudEvent +def create_cloud_event(data): + attributes = { + "type": "knative.sampleapp.sentiment.response", + "source": "sentiment-analysis", + "datacontenttype": "application/json", + } + + # Put the sentiment analysis result into a dictionary + data = {"result": data} + + # Create a CloudEvent object + event = CloudEvent(attributes, data) + + return event + +def analyze_sentiment(text): + analysis = TextBlob(text) + sentiment = "Neutral" + if analysis.sentiment.polarity > 0: + sentiment = "Positive" + elif analysis.sentiment.polarity < 0: + sentiment = "Negative" + + # Convert the sentiment into a CloudEvent + sentiment = create_cloud_event(sentiment) + + # Sleep for 3 seconds to simulate a long-running process + sleep(3) + + return sentiment + +def main(context: Context): + """ + Function template + The context parameter contains the Flask request object and any + CloudEvent received with the request. + """ + + print("Received CloudEvent: ", context.cloud_event) + + # Add your business logic here + return analyze_sentiment(context.cloud_event.data) + +``` + +### Step 3: Configure the dependencies +The `requirements.txt` file contains the dependencies for the function. You can add the following dependencies to the `requirements.txt` file: + +```bash +Flask==3.0.2 +textblob==0.18.0.post0 +parliament-functions==0.1.0 +cloudevents==1.10.1 +``` +Knative function will automatically install the dependencies listed here when you build the function. + +### Step 4: Configre the pre-built environment +In order to properly use the `textblob` library, you need to download the corpora, which is a large collection of text data that is used to train the sentiment analysis model. You can do this by creating a new file called `setup.py`, +knative function will ensure that the `setup.py` file is executed after the dependencies have been installed. + + +The `setup.py` file should contain the following code for your bookstore: + + +```python +from setuptools import setup, find_packages +from setuptools.command.install import install +import subprocess + + +class PostInstallCommand(install): + """Post-installation for installation mode.""" + def run(self): + # Call the superclass run method + install.run(self) + # Run the command to download the TextBlob corpora + subprocess.call(['python', '-m', 'textblob.download_corpora', 'lite']) + + +setup( + name="download_corpora", + version="1.0", + packages=find_packages(), + cmdclass={ + 'install': PostInstallCommand, + } +) +``` + + + + +### Step 5: Try to build and run your Knative Function on your local machine + +In knative function, there are two ways to build: using the [pack build](https://github.com/knative/func/blob/8f3f718a5a036aa6b6eaa9f70c03aeea740015b9/docs/reference/func_build.md?plain=1#L46) or using the [source-to-image (s2i) build](https://github.com/knative/func/blob/4f48549c8ad4dad34bf750db243d81d503f0090f/docs/reference/func_build.md?plain=1#L43). + +Currently. only the **s2i** build is supported if you need to run setup.py. When building with s2i, the `setup.py` file will be executed automatically after the dependencies have been installed. + +Before we get started, configure the container registry to push the image to the container registry. You can use the following command to configure the container registry: + +```bash +export FUNC_REGISTRY= +``` + +In this case, we will use the s2i build by adding the flag `-b=s2i`, and `-v` to see the verbose output. + +```bash +func build -b=s2i -v +``` + +When the build is complete, you will see the following output: + +```bash +🙌 Function built: /sentiment-analysis-app:latest +``` + +This command will build the function and push the image to the container registry. After the build is complete, you can run the function using the following command: + +--- +An alert box +Issue you may experience: +``` +Error: '/home/Kuack/Documents/knative/docs/code-samples' does not contain an initialized function +``` +Solution: You may want to check whether you are in the correct directory. You can use the following command to check the current directory. + + +If you are in the right directory, and the error still occurs, try to check your func.yaml, + +as it has to contain the field `created` and the right time stamp to be treated as a valid knative function. + +--- + +```bash +func run -b=s2i -v +``` +In the future, you can **skip the step of `func build`**, because func run will automatically build the function for you. + +You will see the following output if the function is running successfully: + +``` +function up-to-date. Force rebuild with --build +Running on host port 8080 +---> Running application from script (app.sh) ... +```` + +Now you can test the function by sending a request to the function using the following command: + +```bash +curl -X POST http://localhost:8080 \ +-H "ce-id: 12345" \ +-H "ce-source: /your/source" \ +-H "ce-type: sentiment-analysis-request" \ +-H "ce-specversion: 1.0" \ +-H "Content-Type: application/json" \ +-d '{"input":"I love Knative so much!"}' +``` +where `-H` are the headers, and `-d` is the input text. The input text is a **sting**. Be careful with the quotes. + +If the function is running successfully, you will see the following output (the `data` field in the Response CloudEvent only): + +```bash +{ + "input":"I love Knative so much!", + "result": "Positive" +} +``` + +Knative function also have an easy way to simulate the CloudEvent, you can use the following command to simulate the CloudEvent: + +```bash +func invoke -f=cloudevent --data='{"input": "I love Knative so much"}' --content-type=application/json --type="new-comment" -v +``` +where the `-f` flag indicates the type of the data, is either `HTTP` or `cloudevent`, and the `--data` flag is the input text. +You can read more about `func invoke` [here](https://github.com/knative/func/blob/main/docs/reference/func_invoke.md). + +In this case, you will get the full CloudEvent response: + +```bash +Context Attributes, + specversion: 1.0 + type: knative.sampleapp.sentiment.response + source: sentiment-analysis + id: af0c0f59-9130-4a6c-96ef-6d72c2f4ce50 + time: 2024-02-31T18:48:00.232436Z + datacontenttype: application/json +Data, + { + "input":"I love Knative so much!", + "result": "Positive" + } + +``` + +### Step 6: Deploy the function to the cluster +After you have finished the code, you can deploy the function to the cluster using the following command: + +```bash +func deploy -b=s2i -v +``` + +When the deployment is complete, you will see the following output: + +```bash +✅ Function updated in namespace "default" and exposed at URL: + http://sentiment-analysis-app.default.10.99.46.8.sslip.io +``` + +You can also find the URL by running the following command: + +```bash +kubectl get kservice -A +``` + +You will see the URL in the output: + +```bash +NAMESPACE NAME URL LATESTCREATED LATESTREADY READY REASON +default sentiment-analysis-app http://sentiment-analysis-app.default.10.99.46.8.sslip.io sentiment-analysis-app-00002 sentiment-analysis-app-00002 True +``` + +Please note: if your URL ends with .svc.cluster.local, that means you can only access the function from within the cluster. You probably forget to configure the network or [start the tunnel](https://knative.dev/docs/getting-started/quickstart-install/#__tabbed_3_2) if you are using minikube. + +### Step 7: Verify the Deployment +After deployment, the `func` CLI provides a URL to access your function. You can verify the function's operation by sending a request with a sample review comment. + +Simply use Knative function's command `func invoke` to directly send a CloudEvent to the function on your cluster: + +```bash +func invoke -f=cloudevent --data="i love knative community so much" -v -t="http://sentiment-analysis-app.default.10.99.46.8.sslip.io" +``` + +- `-f` flag indicates the type of the data, is either `HTTP` or `cloudevent` +- `--data` flag is the input text +- `-t` flag is the URI to the Knative Function. + +If the function is running successfully, you will see the following output: + +```bash +Context Attributes, + specversion: 1.0 + type: knative.sampleapp.sentiment.response + source: sentiment-analysis + id: 6a246e0c-2b24-4f22-93c4-f1265c569b2d + time: 2024-02-31T19:03:50.434822Z + datacontenttype: application/json +Data, + { + "input":"I love Knative so much!", + "result": "Positive" + } +``` + +--- +Recall note box: you can get the URL to the function by running the following command: +```bash +kubectl get kservice -A +``` +--- +Another option is to use curl to send a CloudEvent to the function. +Using curl command to send a CloudEvent to the broker: +```bash +[root@curler:/]$ curl -v "http://sentiment-analysis-app.default.10.99.46.8.sslip.io" \ +-X POST \ +-H "ce-id: 12345" \ +-H "ce-source: my-local" \ +-H "ce-type: sentiment-analysis-request" \ +-H "ce-specversion: 1.0" \ +-H "Content-Type: application/json" \ +-d '"I love Knative so much! Sent to the cluster"' +``` + +Expect to receive a JSON response indicating the sentiment classification of the input text. + +```bash +{ + "input":"I love Knative so much!", + "result": "Positive" +} +``` +If you see the response, it means that the function is running successfully. + +--- +### The magic time about Serverless: autoscaling to zero +If you use the following command to query all the pods in the cluster, you will see that the pod is running: + +```bash +kubectl get pods -A +``` +where `-A` is the flag to query all the pods in all namespaces. + +And you will find that your sentiment analysis app is running: + +```bash +NAMESPACE NAME READY STATUS RESTARTS AGE +default sentiment-analysis-app-00002-deployment 2/2 Running 0 2m +``` + +But if you wait for a while without sending any CloudEvent to your function, and query the pods again, you will find that the pod that has your sentiment analysis app **disappeared**! + + +This is because **Knative Serving's autoscaler** will automatically scale down to zero if there is no request to the function! + +--- + +Congratulations! You have successfully set up the sentiment analysis service for your bookstore. + +## Conclusion + +In this tutorial, you learned how to create a serverless function that contains a simple sentiment analysis service with Knative function. \ No newline at end of file diff --git a/code-samples/eventing/bookstore-sample-app/ML-sentiment-analysis/app.sh b/code-samples/eventing/bookstore-sample-app/ML-sentiment-analysis/app.sh new file mode 100755 index 000000000..4da37d4d8 --- /dev/null +++ b/code-samples/eventing/bookstore-sample-app/ML-sentiment-analysis/app.sh @@ -0,0 +1,3 @@ +#!/bin/sh + +exec python -m parliament "$(dirname "$0")" diff --git a/code-samples/eventing/bookstore-sample-app/ML-sentiment-analysis/func.py b/code-samples/eventing/bookstore-sample-app/ML-sentiment-analysis/func.py new file mode 100644 index 000000000..7eaa5170f --- /dev/null +++ b/code-samples/eventing/bookstore-sample-app/ML-sentiment-analysis/func.py @@ -0,0 +1,50 @@ +from parliament import Context +from flask import Request,request, jsonify +import json +from textblob import TextBlob +from time import sleep +from cloudevents.http import CloudEvent, to_structured + +# The function to convert the sentiment analysis result into a CloudEvent +def create_cloud_event(inputText,data): + attributes = { + "type": "knative.sampleapp.sentiment.response", + "source": "sentiment-analysis", + "datacontenttype": "application/json", + } + + # Put the sentiment analysis result into a dictionary + data = { + "input": inputText, + "result": data + } + + # Create a CloudEvent object + event = CloudEvent(attributes, data) + + return event + +def analyze_sentiment(text): + analysis = TextBlob(text["input"]) + sentiment = "Neutral" + if analysis.sentiment.polarity > 0: + sentiment = "Positive" + elif analysis.sentiment.polarity < 0: + sentiment = "Negative" + + # Convert the sentiment into a CloudEvent + sentiment = create_cloud_event(text["input"],sentiment) + + return sentiment + +def main(context: Context): + """ + Function template + The context parameter contains the Flask request object and any + CloudEvent received with the request. + """ + + print("Received CloudEvent: ", context.cloud_event) + + # Add your business logic here + return analyze_sentiment(context.cloud_event.data) diff --git a/code-samples/eventing/bookstore-sample-app/ML-sentiment-analysis/func.yaml b/code-samples/eventing/bookstore-sample-app/ML-sentiment-analysis/func.yaml new file mode 100644 index 000000000..a3ac50d45 --- /dev/null +++ b/code-samples/eventing/bookstore-sample-app/ML-sentiment-analysis/func.yaml @@ -0,0 +1,11 @@ +specVersion: 0.35.0 +name: sentiment-analysis-app +runtime: python +registry: +image: /sentiment-analysis-app:latest +imageDigest: sha256:0bc*****d3c +created: 2024-02-31T00:18:00.06485162-04:00 +build: + builder: s2i +deploy: + namespace: default diff --git a/code-samples/eventing/bookstore-sample-app/ML-sentiment-analysis/requirements.txt b/code-samples/eventing/bookstore-sample-app/ML-sentiment-analysis/requirements.txt new file mode 100644 index 000000000..ab724e8a7 --- /dev/null +++ b/code-samples/eventing/bookstore-sample-app/ML-sentiment-analysis/requirements.txt @@ -0,0 +1,4 @@ +Flask==3.0.2 +textblob==0.18.0.post0 +parliament-functions==0.1.0 +cloudevents==1.10.1 \ No newline at end of file diff --git a/code-samples/eventing/bookstore-sample-app/ML-sentiment-analysis/setup.py b/code-samples/eventing/bookstore-sample-app/ML-sentiment-analysis/setup.py new file mode 100644 index 000000000..2cdd67ede --- /dev/null +++ b/code-samples/eventing/bookstore-sample-app/ML-sentiment-analysis/setup.py @@ -0,0 +1,22 @@ +from setuptools import setup, find_packages +from setuptools.command.install import install +import subprocess + + +class PostInstallCommand(install): + """Post-installation for installation mode.""" + def run(self): + # Call the superclass run method + install.run(self) + # Run the command to download the TextBlob corpora + subprocess.call(['python', '-m', 'textblob.download_corpora', 'lite']) + + +setup( + name="download_corpora", + version="1.0", + packages=find_packages(), + cmdclass={ + 'install': PostInstallCommand, + } +)