Building an AI powered and Serverless meal planner with OpenAI, AWS Step functions, AWS Lambda and CDK
Tags: openai, serverless, stepfunctions, cdk
Published: 2023-03-13 08:16:41

OpenAI’s generative capabilities offer new possibilities when building applications. Combined with Serverless technologies, we can create applications faster while still maintaining the flexibility to iterate and to improve them over time.

In this article, I will show you how to build an application that sends emails containing generated weekly meal plans from a set of ingredients a user provides. We will use OpenAI’s APIs along with AWS Serverless services: Step Functions, AWS Lambda, and Amazon SES.

We will use NodeJs runtime and typescript for the Lambda code as well as CDK for IaC.

![logos of the services that were used](https://cdn-images-1.medium.com/max/2000/1*MLFWDhvi-YJnkegw9fQZ0g.png)

## What are we going to build ?

We will create an application that allows users to submit via an API a request containing a set of food ingredients and an email address. It will then, asynchronously, send to the user an email containing a meal plan with detailed recipes for a whole week:

![AI powered meal planner](https://cdn-images-1.medium.com/max/11034/1*I-kW3EmeHuj__0br6dsRlw.png)

Here is the architecture diagram of the application we are going to build:

![AI powered meal planner architecture overview](https://cdn-images-1.medium.com/max/5648/1*rE-5meExVJx0CBIaQz4bWQ.png)

The relevant parts of this solution:

* We use a step function to orchestrate the invocations of Lambda functions that send requests to OpenAI’s APIs to generate recipes from a prompt as well as an image for each recipe.

* We use the S3 bucket to store the generated recipe images. These images are served via a CloudFront distribution.

* The generated meal plan is then sent via email using SES. We use the [SES templates capability](https://docs.aws.amazon.com/ses/latest/dg/send-personalized-email-api.html) to send personalized emails for each user.

* The Rest API gateway has a POST route with an [integration](https://docs.aws.amazon.com/step-functions/latest/dg/tutorial-api-gateway.html) to the meal planner Step Function.

### TL;DR

You can find the full repository with its deployment pipeline here 👇
[**GitHub - ziedbentahar/serverless-meal-planner-with-aws-and-openai**](https://github.com/ziedbentahar/serverless-meal-planner-with-aws-and-openai)

## **Let’s deep dive into the code**

☝️ **Before starting**: In order to use OpenAI APIs, you will need to sign up and to create an API KEY. You can follow [this link](https://platform.openai.com/docs/quickstart/add-your-api-key) to get started. The use of this API [is not free](https://openai.com/pricing), however new accounts get free credits (tokens) to start experimenting.

![Creating API Keys on OpenAI account](https://cdn-images-1.medium.com/max/2000/1*fQFzKZ0z_-uDvDxavUnenA.png)

### Defining the state machine

The first step of this state machine is to generate a meal plan for a week. The second step involves generating a picture for each recipe and then saving it on a S3 bucket. The processing is done in parallel for each recipe using a Map state, this has the advantage to reduce the overall execution time of the state machine. And finally, the last important step is the sending of the email containing the meal plan:

![meal planner state machine](https://cdn-images-1.medium.com/max/2000/1*RH1w9ZORWgleOQysKFSUew.png)

Which translates to this fluent state machine definition in CDK:
{% gist https://gist.github.com/ziedbentahar/1d74c6f7f4bb61fa6b78bbac5b09a912.js %}

You can find the complete CDK definition of the state machine following [this link](https://github.com/ziedbentahar/serverless-meal-planner-with-aws-and-openai/blob/main/infrastructure/lib/state-machine.ts).

### Defining the Lambda functions

**1- Generate meal plan Lambda:**

The challenging part about this step was to find the best prompt that yields good and consistent results from OpenAI [completion API](https://platform.openai.com/docs/api-reference/completions). I used the `text-davinci-003` GPT model (also referred as GPT-3.5).

When I tried out different prompts, the suggestions were quite good for producing interesting meal plans given a list of coherent ingredients. I was even able to request a structured result in JSON format ready to be processed by the Lambda function. I also experimented with parameters such as [`temperature`](https://platform.openai.com/docs/api-reference/completions/create#completions/create-temperature), [`TopP`](https://platform.openai.com/docs/api-reference/completions/create#completions/create-top_p) and [`max_tokens`](https://platform.openai.com/docs/api-reference/completions/create#completions/create-max_tokens) searching for the sweet spot that gets satisfying results.

This prompt produces the best results given our use case:

    Generate a dinner meal plan for the whole week with these ingredients <a comma seperated list of ingredients> and with other random ingredients.
    Result must be in json format
    Each meal recipe contains a name, a five sentences for instructions and an array of ingredients

And here is the code of the Lambda function that handles the generation of the meal plan:
{% gist https://gist.github.com/ziedbentahar/36fc3ff3d0efad3500341643a9d084e6.js %}
☝️ **Some notes:**

* As depicted on the diagram above, The OpenAI API key is stored on a secret. In this example we use the [AWS parameters and secrets Lambda extension](https://docs.aws.amazon.com/secretsmanager/latest/userguide/retrieving-secrets_lambda.html) to read the secret value from the Lambda. You can learn more about this Lambda extension [here](https://levelup.gitconnected.com/using-aws-parameters-and-secrets-lambda-extension-e61dd6a41110).

* Even though the completion API was providing consistent response models in JSON, for some reason, the properties on the JSON object were not having a consistent casing as I was experimenting with the API. Hence the use of the getProperty helper function before returning the result; this function ensures getting a property value from an object regardless of its casing.

**2- Generate recipe image Lambda:**

This Lambda function is similar to the previous one. We use the recipe name that `createCompletion` API has generated in order to create an image from it by calling [createImage](https://platform.openai.com/docs/guides/images/introduction) (this API uses [DALL-E models](https://openai.com/research/dall-e) for image generation) :

{% gist https://gist.github.com/ziedbentahar/35298a5330f260ab785010289d67e9e5.js %}

`createImage` API returns an array of URLs, the size of this array depends on the [number of variation of the image](https://platform.openai.com/docs/api-reference/images/create-variation#images/create-variation-n)s we want to generate. In our example we are interested in only one single image. The image URL expires after one hour, here is why we pass it to the [upload-recipe-image-to-storage](https://github.com/ziedbentahar/serverless-meal-planner-with-aws-and-openai/blob/main/src/backend/lambdas/upload-recipe-image-to-storage.ts) Lambda that has the responsibility to download the image and to store it on a S3 Bucket.



**3- Send meal plan email Lambda:**

The sending of the email uses SES. But first, the Lambda function prepares a template data containing the necessary elements to generate the email:
{% gist https://gist.github.com/ziedbentahar/5d51474e0343091daa2b5cb95656d2e2.js %}
On the section below, we will see how to use CDK to create a new SES email identity as well as the email template that is used to send the mal plan.

**Note:** You can find the CDK definitions of these Lambda functions following [this link](https://github.com/ziedbentahar/serverless-meal-planner-with-aws-and-openai/blob/main/infrastructure/lib/lambdas.ts).

### Configuring SES

On this example, we use a domain that is already defined in the Route53 public hosted zone; The SES email identity DNS validation is then seamless. We also create the meal plan email template on this nested stack:
{% gist https://gist.github.com/ziedbentahar/3c3bbac90e991362e4e248a9fb94ab47.js %}

☝️ **Note:** By default, an SES account is in sandbox mode. You are allowed to send emails only to verified identities and you can only send a limited number of emails per 24-hour period. Follow [this link](https://docs.aws.amazon.com/ses/latest/dg/request-production-access.html) to understand the sandbox mode quotas and how to move out of it.



### Integrating the API Gateway with the Step function workflow

Creating the RestApi with the Step Function integration is quite easy with CDK, although a bit verbose:

{% gist https://gist.github.com/ziedbentahar/836c13bb2a26074c37fd7684e0d5b89c.js %}

We need to create a role that grants the Api Gateway to `states:StartExecution` the Step Function. Each request gets validated with the API gateway JSON schema [model validation](https://docs.aws.amazon.com/apigateway/latest/developerguide/models-mappings.html#models-mappings-models) before the execution of the step function.

## Wrapping up

In this post, we have seen how combining OpenAI APIs with serverless architecture can help building AI-powered applications with minimal setup and configuration. The capabilities of both of these two worlds are great enablers for building MVPs and iterating faster.

This application can be improved further by taking into account food restrictions or even by creating an AI powered weekly news letter.

You can find the full source code of this application here:
[**GitHub - ziedbentahar/serverless-meal-planner-with-aws-and-openai**](https://github.com/ziedbentahar/serverless-meal-planner-with-aws-and-openai)

### **Further readings**
[**OpenAI API Text Competion**](https://platform.openai.com/docs/guides/completion)
[**OpenAI API Image generation**](https://platform.openai.com/docs/guides/images)
[**Creating a Step Functions API Using API Gateway**](https://docs.aws.amazon.com/step-functions/latest/dg/tutorial-api-gateway.html)
[**Working with models and mapping templates**](https://docs.aws.amazon.com/apigateway/latest/developerguide/models-mappings.html)
[**Advanced email personalization**](https://docs.aws.amazon.com/ses/latest/dg/send-personalized-email-advanced.html)
[**aws-cdk-lib.aws_stepfunctions module · AWS CDK**](https://docs.aws.amazon.com/cdk/api/v2/docs/aws-cdk-lib.aws_stepfunctions-readme.html)