< Blogs
Terra API

Terra API

February 12, 2024

How to Build an AI Nutritionist with Flask, Terra and ChatGPT

Terra is an API that delivers wearable data to your applications via Webhooks. In this article, you will learn how to use the Terra API and the ChatGPT API to create an application that acts as your personal AI nutritionist.

You will feed this app some personal data and nutrition data from a wearable, and AI will give you nutrition plans and advice based on your goals.

We will be using Python and Flask to consume sample health data from the Terra Webhook, send it to OpenAI's API, then display nutrition plans.

Here is the web application we are going to build:

image.png

Once you fill in information, AI will provide you with nutrition plans:

image.png

Step 1 - Obtain Your Credentials from The Terra Dashboard

To communicate with a wearable through the Terra API, you need the following:

  • Your API Key
  • Your Dev ID

Go to your Terra Dashboard, under Connections, you will find your API Key and Dev ID in the bottom right corner in the API Credentials Popup. They are used in virtually every interaction with the API - keep them safe!

Step 2 - Create an MVP App with Flask

Create a virtual environment and activate it:

python -m venv env

source env/bin/activate

Install the necessary packages:

pip install Flask terra-python openai

The terra-python package is a wrapper for the Terra endpoints and models. We'll use it to verify and authenticate incoming webhook data.

Create a Flask instance folder inside your project directory:

mkdir instance

Next, create a Flask app file called app.py:

import os
import json
import logging
from flask import Flask, render_template, request, redirect, url_for, Response
from datetime import datetime
from terra.base_client import Terra

logging.basicConfig(level=logging.INFO)

_LOGGER = logging.getLogger("app")

terra = Terra(api_key='<API-KEY>',
              dev_id='<DEV-ID>',
              secret='<SIGNING-SECRET>')

app = Flask(__name__)

def get_json_data():
    # Get today's date in YYYY-MM-DD format
    today_date = datetime.now().strftime("%Y-%m-%d")
    # Path to the JSON file for today's date
    instance_path = app.instance_path
    json_file_path = os.path.join(instance_path, f'{today_date}.json')

    if os.path.exists(json_file_path):
        # If JSON file exists, display its contents
        with open(json_file_path, 'r') as file:
            data = json.load(file)
            return data
    return None

@app.route('/', methods=["GET", "POST"])
def index():
    if request.method == 'POST':
        # Get data from the Terra Webhook
        body = request.get_json()

        # Log that the Webhook was recieved
        _LOGGER.info(
            "Received webhook for user %s of type %s",
            body.get("user", {}).get("user_id"),
            body["type"])

        # Just a health check, return 200
        if body["type"] == 'healthcheck':
            return Response(status=200)

        # Verify Terra Signature
        verified = terra.check_terra_signature(request.get_data().decode("utf-8"),
                                            request.headers['terra-signature'])

        # The data is verified
        if verified:
            return Response(status=200)

        else:
            return Response(status=403)

    # Handle GET requests
    if request.method == 'GET':
        data = get_json_data()
        return render_template('index.html', data=data)

@app.route('/submit', methods=['POST'])
def submit():
    # Get form data
    height = request.form.get('height')
    weight = request.form.get('weight')
    sex = request.form.get('sex')
    age = request.form.get('age')
    body_fat_percentage = request.form.get('body_fat_percentage')
    training_type = request.form.get('training_type')
    daily_meals = request.form.get('daily_meals')
    goals = request.form.get('goals')

    # Get today's date in YYYY-MM-DD format
    today_date = datetime.now().strftime("%Y-%m-%d")

    # Path to the JSON file for today's date
    json_file_path = os.path.join('instance', f'{today_date}.json')

    # Create a dictionary with form data
    data = {
        'height': height,
        'weight': weight,
        'sex': sex,
        'age': age,
        'body_fat_percentage': body_fat_percentage,
        'training_type': training_type,
        'daily_meals': daily_meals,
        'goals': goals
    }

    # Save the data to the JSON file
    with open(json_file_path, 'w') as file:
        json.dump(data, file, indent=4)

    return redirect(url_for('index'))

Note: Make sure to fill in <API-KEY> and <DEV-ID> with your API key and developer ID. The signing secret will be added later in this article.

This code represents a basic Flask web application that connects with the Terra wearable API, and provides a simple input form for users to add their personal data. Let's break down the key components and functionality:

  1. Logging Setup:
    • Logging is configured with a basic setup, and a logger named _LOGGER is created.
  2. Terra API Configuration:
    • An instance of the Terra class is created with the Terra API key, developer ID, and signing secret.
  3. Flask Setup:
    • An instance of the Flask application is created (app).
  4. get_json_data() Function:
    • Retrieves JSON data from a file based on the current date.
  5. Routes:
    • The application defines two routes:
      • /: The main route that handles both GET and POST requests.
      • /submit: A route specifically for handling POST requests when the user submits a form.
  6. Index Route (/):
    • For GET requests, it calls the get_json_data() function and render the returned value.
    • For POST requests, it processes data received from the Terra webhook, logs the information, responds with a 200 status in case of a health check, and verifies the Terra signature. If the signature is verified, it returns a 200 response for now; otherwise, it returns a 403 response. The verification process will not work for now, as we haven't yet added our Terra signing secret, which will be added later in this article.
  7. Submit Route (/send):
    • Extract form data (height, weight, sex, etc.) from the request.
    • Create a dictionary with the form data.
    • Save the data to a JSON file named with the current date.
    • Redirect the user to the / (index) route after submitting the form.

Now, create a templates folder inside your Flask app's directory and create a new index.html file inside it:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Terra AI Nutritionist</title>
    <!-- Bootstrap CSS -->
    <link href="<https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0/css/bootstrap.min.css>" rel="stylesheet">
</head>
<body>
    <div class="container">
        <h1>Terra AI Nutritionist</h1>

        {% if data %}
            <pre>{{ data | tojson(indent=4) }}</pre>
        {% else %}
            <form action="/submit" method="post">
                <!-- Height input -->
                <div class="form-group">
                    <label for="height">Height (cm):</label>
                    <input type="number" class="form-control" id="height" name="height" required>
                </div>

                <!-- Weight input -->
                <div class="form-group">
                    <label for="weight">Weight (kg):</label>
                    <input type="number" class="form-control" id="weight" name="weight" required>
                </div>

                <!-- Sex input -->
                <div class="form-group">
                    <label>Sex:</label>
                    <div class="form-check">
                        <input type="radio" class="form-check-input" id="male" name="sex" value="Male" required>
                        <label class="form-check-label" for="male">Male</label>
                    </div>
                    <div class="form-check">
                        <input type="radio" class="form-check-input" id="female" name="sex" value="Female" required>
                        <label class="form-check-label" for="female">Female</label>
                    </div>
                </div>

                <!-- Age input -->
                <div class="form-group">
                    <label for="age">Age:</label>
                    <input type="number" class="form-control" id="age" name="age" required>
                </div>

                <!-- Body Fat Percentage input -->
                <div class="form-group">
                    <label for="body_fat_percentage">Body Fat Percentage:</label>
                    <input type="number" class="form-control" id="body_fat_percentage" name="body_fat_percentage" required>
                </div>

                <!-- Type of Training input -->
                <div class="form-group">
                    <label for="training_type">Type of Training:</label>
                    <select class="form-control" id="training_type" name="training_type" required>
                        <option value="strength_training">Strength Training</option>
                        <option value="cardio">Cardio</option>
                        <option value="both">Both</option>
                    </select>
                </div>

                <!-- Today's Meal input -->
                <div class="form-group">
                    <label for="daily_meals">Today's Meal:</label>
                    <textarea class="form-control" id="daily_meals" name="daily_meals" rows="3" required></textarea>
                </div>

                <!-- Goals input -->
                <div class="form-group">
                    <label for="goals">Goals:</label>
                    <select class="form-control" id="goals" name="goals" required>
                        <option value="weight_loss">Weight Loss</option>
                        <option value="muscle_gain">Muscle Gain</option>
                        <option value="overall_health">Overall Health</option>
                    </select>
                </div>

                <button type="submit" class="btn btn-primary">Submit</button>
            </form>
        {% endif %}
    </div>

    <!-- Bootstrap JS and dependencies -->
    <script src="<https://code.jquery.com/jquery-3.2.1.slim.min.js>"></script>
    <script src="<https://cdnjs.cloudflare.com/ajax/libs/popper.js/1.12.9/umd/popper.min.js>"></script>
    <script src="<https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0/js/bootstrap.min.js>"></script>
</body>
</html>

This HTML template either displays a web form for the user or the data the user has submitted if it is already stored.

Next Run the application on port 8080:

flask --app app run -p 8080

As Terra cannot send Webhook data to your local development server, you have to expose your server to the Internet using Ngrok. To install it, check out this page.

Once you install Ngrok, create an account, then obtain your authentication token from the Ngrok dashboard. Once you get your authtoken, add it to your Ngrok agent using the following command:

ngrok config add-authtoken <TOKEN>

This will allow you to access some features such as rendering HTML files.

Once you set up Ngrok, use it to expose your Flask application that is currently running on port 8080:

ngrok http 8080

This command will give you a URL under Forwarding. Use your browser to access this URL, you should see your index page.

image.png

Fill in your info, and you should see your personal data in JSON like so:

image.png

Note: Copy your Ngrok URL. This is your Server URL, and you will use it to connect to a Terra Webhook in the next step.

Step 3 - Connect a Terra Webhook with Your Server

You will now connect a test wearable with your Flask app.

Note: We will use FitBit in this demo.

Go to your Terra Dashboard, then in Connections. Under Sources, click Add Sources, and select Fitbit, then Save.

Next, under Destinations click Add Destination, then select Webhook, then Next.

Put your Ngrok Server URL under host.

The Connections dashboard should now look like so:

image.png

Now, you need to obtain your Signing secret. Click the three dots to the right of Webhook then Edit.

Copy the Signing secret. This is needed to authenticate and verify Webhook requests.

To use your Webhook's signing secret, modify the secret parameter in your Terra initiation inside your app.py Flask application:

terra = Terra(api_key='<API-KEY>',
              dev_id='<DEV-ID>',
              secret='<PASTE-SIGNING-SECRET-HERE>')

Once modified, remember to rerun your Flask app:

flask --app app run -p 8080

Note: Make sure Ngrok is still running. If you've stopped it and restarted it, your Server URL will change, so make sure to edit the Webhook host in your Terra Dashboard and replace the old Ngrok URL with the new one.

We will now test the Webhook connection. In the Terra Dashboard, go to Tools > Generate > Select Data Source > Fitbit.

Then click on Nutrition, then click Generate test data.

Once data is generated. Click Send to Destination.

Go back to your Flask server and wait for a few seconds. You should see the following message in the logs:

INFO:app:Received webhook for user <User-ID> of type nutrition
INFO:werkzeug:127.0.0.1 - - [06/Feb/2024 19:20:41] "POST / HTTP/1.1" 200 -

This means you have successfully connected a Terra Webhook with your Flask application and you are receiving data!

Next, you'll modify app.py to retrieve health data, feed it to OpenAI's API, then generate a nutrition plan based on it.

Step 4 - Create an AI Nutritionist

We will now use the OpenAI API (also known as ChatGPT API) to create our artificially intelligent nutritionist.

Inside your Flask project folder, open a new file called gpt.py then add the following code to it:

# gpt.py

import os
import json

from openai import OpenAI

client = OpenAI(
    api_key=os.environ.get("OPENAI_API_KEY"),
)

def ask(question):
    completion = client.chat.completions.create(
    model="gpt-3.5-turbo",
    messages=[
        {"role": "system", "content": """
         Act as an expert nutritionist.
         You provide custom nutrition plans based on personal data
         and data for consumed micros and macros.
         """},
        {"role": "user",
         "content": f"""{question}.
                     Respond in HTML with Bootstrap classes
                     for a well designed UI"""}
    ]
    )

    return completion.choices[0].message.content

Here, you set up an OpenAI client, passing your OpenAI API key as an environment variable.

In the ask() function, you set up ChatGPT as an expert nutritionist. The response will be in HTML, so you can display it inside your index.html template.

Now, set your Open API key in an environment variable.

For Linux & MacOS:

export OPENAI_API_KEY='your Open API key'

For Windows:

set OPENAI_API_KEY='your Open API key'

Next, inside your app.py file, add a gpt import at the top:

# imports ...
import gpt

Then, modify the POST request handler from this:

        # The data is verified
        if verified:
            return Response(status=200)

To this:

        # The data is verified
        if verified:
            personal_data = get_json_data()
            micros = body['data'][0]['meals'][0]['micros']
            macros = body['data'][0]['meals'][0]['macros']

            plan = gpt.ask(f"""Provide a nutrition plan based on the following
                               data:
                               - Personal: {personal_data}.
                               - micros consumed: {micros}.
                               - macros consumed: {macros}.
                            """)

            # Save the plan
            plan_file_path = os.path.join('instance', 'plan.txt')
            with open(plan_file_path, 'w') as file:
                file.write(plan)

            return Response(status=201)

Save and close the file.

Here, you get the user's personal data, their consumed micros and macros from Terra, then feed them to gpt.ask(). You save the resulting nutrition plan inside a text file called plan.txt.

You also respond with an HTTP 201 status instead of 200 to indicate that a new resource was created on the server.

Note: For a better data management approach, consider using a relational SQL database for all your data.

Next, edit the GET request handler from this:

# Handle GET requests
    if request.method == 'GET':
        data = get_json_data()
        return render_template('index.html', data=data)

To this:

    # Handle GET requests
    if request.method == 'GET':
        plan_file_path = os.path.join('instance', 'plan.txt')
        if os.path.exists(plan_file_path):
            with open(plan_file_path, 'r') as file:
                data = file.read()
        else:
            data = None
        return render_template('index.html', data=data)

This reads and renders the AI generated nutrition plan.

In the index.html template, change the following line:

<pre>{{ data | tojson(indent=4) }}</pre>

Into this:

{{ data | safe }}

This will render the AI generated HTML.

Restart your Flask server:

flask --app app run -p 8080

To test out this new code, go to your Terra Dashboard. Click Tools > Generate > Select Data Source > Fitbit.

Then click on Nutrition, then click Generate test data.

Once data is generated. Click Send to Destination.

Go back to your Flask server and wait for a few seconds. You should see the following message in the logs:

INFO:app:Received webhook for user <User-ID> of type nutrition
INFO:httpx:HTTP Request: POST <https://api.openai.com/v1/chat/completions> "HTTP/1.1 200 OK"
INFO:werkzeug:127.0.0.1 - - [06/Feb/2024 14:47:24] "POST / HTTP/1.1" 201 -

If you go to your index page, you will see your AI generated nutrition plan:

image.png

Conclusion

Congrats! You've created a personal nutritionist with Terra's API and OpenAI's API.

Note: This sample demo application will not cover everything accurately as it is only fed a small part of the overall health data of a user. To improve this application, here are some ideas:

  • Retrieve all data and metrics from a wearable using Terra, and feed this data to a custom GPT. This will provide your AI nutritionist with more context on the user's health and training.
  • Use Terra's Graph API to display graphs for different health and activity metrics. You can use this to display sleep analytics, historical heart rate data, and more.

To learn more about the awesome things you can build with Terra, check out the following articles:

More Topics

All Blogs
Team Spotlight
Startup Spotlight
How To
Blog
Podcast
Product Updates
Wearables
See All >
CEO and Co-Founder of Bioniq - Vadim Fedotov

CEO and Co-Founder of Bioniq - Vadim Fedotov

In this podcast with Kyriakos the CEO of Terra, Vadim Fedotov a former professional athlete turned entrepreneur, shares his journey in founding Bioniq.

Terra APITerra API
December 10, 2024
5 Lessons for Standing Out at HLTH

5 Lessons for Standing Out at HLTH

5 lessons from team Terra API for making a lasting impact at HLTH: from engaging senses to building real touch points, here’s what we learned from the HLTH event.

VanessaVanessa
December 5, 2024
November '24 Updates by Terra

November '24 Updates by Terra

Terra’s Latest Updates: Zepp Metrics, Support Revamp, and Teams API Enhancements 🚀✨

Alex VenetidisAlex Venetidis
December 1, 2024
Strava Pulls the Plug on their API: What This Means for Developers

Strava Pulls the Plug on their API: What This Means for Developers

Strava discontinued their API service, changing the ecosystem of third-party apps that have relied on their platform. How can developers react to this?

Terra APITerra API
November 21, 2024
Alternatives to the latest changes in the Strava API

Alternatives to the latest changes in the Strava API

Strava just introduced big changes to their API program. These changes will basically kill off a lot of apps. Use Terra API instead to avoid this

Kyriakos EleftheriouKyriakos Eleftheriou
November 19, 2024
next ventures
pioneer fund
samsung next
y combinator
general catalyst

Cookie Preferences

Essential CookiesAlways On
Advertisement Cookies
Analytics Cookies

Crunch Time: Embrace the Cookie Monster Within!

We use cookies to enhance your browsing experience and analyse our traffic. By clicking “Accept All”, you consent to our use of cookies according to our Cookie Policy. You can change your mind any time by visiting out cookie policy.