Skip to content

Commit

Permalink
Merge pull request #253 from fcampise/main
Browse files Browse the repository at this point in the history
Added Track 2, Sprint 5
  • Loading branch information
fcampise authored Apr 22, 2024
2 parents a127bc7 + e646a0d commit c77c712
Show file tree
Hide file tree
Showing 43 changed files with 1,689 additions and 1 deletion.

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
# User Story: Cache Recommendations in the Database - Step-by-Step
⏲️ _Est. time to complete: 30 min._ ⏲️

## User Story
*As a user, I want to be able to quickly go back and see the recommendations that were last presented for a given to-do item*

## 🎯Acceptance Criteria:
- The web app should store the recommendations in the database when they are generated.
- The recommendations should be displayed when the user clicks on the recommendations button for the task again.

## 🎓Know Before You Start
no resources at this time

## 📋Steps

In order to complete this user story you will need to complete the following tasks:

### Open Visual Studio Code
Open Visual Studio Code and open the source code the folder with your completed solution from the previous user story if you prefer you can use the starting reference application from [here](/Track_2_ToDo_App/Sprint-05%20-%20Advanced%20AI%20recommendations/src/app-s05-f01-us01/)

### Update the database to store the recommendations in the database

#### 1. Update the Database Model to Store recommendations
The first thing we will need to do is add the recommendations to the database. Open the `database.py` file and add the following code right under the `recommendations = []` instance variable to create the additional column

```python
recommendations_json = db.Column(db.JSON)
```

This code adds a new column to the `Todo` model called `recommendations_json` that will store the AI recommendations in JSON format. Note we will still keep the recommendations as a list in the `recommendations` instance variable so that it is easy for the UI to work with.

<br/>

### Update the Application Backend

#### 1. Update the `app.py` to handle saving and retrieving recommendations
We now need to update the `app.py` to handle saving and retrieving recommendations. We need to make two changes to this file. (A) import the json module and (B) Replace the recommend route in the `app.py` file with the following code:

A. At the top of the file add the following import statement:

```python
import json
```
B. We will then need to replace the `recommend` route with the following code:

```python
# Show AI recommendations
@app.route('/recommend/<int:id>', methods=['GET'])
async def recommend(id):
recommendation_engine = RecommendationEngine()
g.todo = db.session.query(Todo).filter_by(id=id).first()

if g.todo:
try:
#attempt to load any saved recommendation from the DB
if g.todo.recommendations_json is not None:
g.todo.recommendations = json.loads(g.todo.recommendations_json)
return render_template('index.html')
except ValueError as e:
print("Error:", e)

g.todo.recommendations = await recommendation_engine.get_recommendations(g.todo.name)

# Save the recommendations to the database
try:
g.todo.recommendations_json = json.dumps(g.todo.recommendations)
db.session.add(g.todo)
db.session.commit()
except Exception as e:
print(f"Error adding and committing todo: {e}")
return

return render_template('index.html')
```

This code updates the `/recommend/<int:id>` route to check if the task has saved recommendations in the database. If the recommendations are found, they are loaded from the database and displayed. If the recommendations are not found, the AI recommendations are generated and saved to the database.

#### 2. Delete the `todos.db` file
Before we can test this change, we will need to delete the `todos.db` file in your directory. This is because we have added a column to the database and to keep things simple we will just start fresh versus trying to update the database schema. This approach works fine when you are in development, but if this was a production system you would want to update the database schema and migrate the data. When you do this any items saved in the database will be lost.

> [!WARNING]
> If you do not delete the `todos.db` file you will get an error when you run the app. The error will be something like `sqlalchemy.exc.OperationalError: (sqlite3.OperationalError) no such column: todos.recommendations_json`. This error is because the database schema does not match the model.
<br/>

#### 3. Run the Application
Now let's see this functionality in action. Start the web app by running the following command in the terminal:

```bash
python app.py
```

<br/>
🎉 Congratulations! You have now saved the AI recommendations to the database. This will allow users to quickly go back and see the recommendations that were last presented for a given to-do item.

<br/>

> [!NOTE]
> 📄For the full source code for this exercise please see [here](/Track_2_ToDo_App/Sprint-05%20-%20Advanced%20AI%20recommendations/src/app-s05-f01-us02/).

<br/>

[🔼 Back **Workshop** Instructions ](/Track_2_ToDo_App/Workshop-Format.md) | [🔼 Back to **Hackathon** Sprint 5 ](/Track_2_ToDo_App/Sprint-05%20-%20Advanced%20AI%20recommendations/README.md) | [**◀ Previous user story**](User%20Story%201%20-%20Get%20Gen%20AI%20recommendation.md) | [**Next user story**](User%20Story%203%20-%20Refresh%20Recommendations.md)
Original file line number Diff line number Diff line change
@@ -0,0 +1,183 @@
# User Story: Allow the user to refresh the recommendations - Step-by-Step
⏲️ _Est. time to complete: 30 min._ ⏲️

## User Story
*As a user, I want to be able to tell the system to re-generate the recommendations on how to complete a task when I click on it*

## 🎯Acceptance Criteria:
- The web app should provide a user interface element to refresh the recommendations for a task.
- The system should re-generate the recommendations when the user clicks on the refresh button.
- The new recommendations should be displayed in the recommendations section.
- The new recommendations should be stored in the database and overwrite the old recommendations.
- The AI recommendation engine should not have any reuse old recommendations in the new recommendation list.
- The interface should look something like this:

![Refresh Recommendations](/Track_2_ToDo_App/Sprint-05%20-%20Advanced%20AI%20recommendations/images/outcome-S05-F01-US03.png)

## 🎓Know Before You Start
no resources at this time

## 📋Steps

In order to complete this user story you will need to complete the following tasks:

### Open Visual Studio Code
Open Visual Studio Code and open the source code the folder with your completed solution from the previous user story if you prefer you can use the starting reference application from [here](/Track_2_ToDo_App/Sprint-05%20-%20Advanced%20AI%20recommendations/src/app-s05-f01-us02/)

<br/>

### Update the recommendation engine to allow for refreshing recommendations

#### 1. Update get_recommendations method
The first thing that we will need to do is update the recommendation engine to enable it to re-generate new recommendations. Open the `recommendation_engine.py` file in the source folder of your application. We will need to replace the `get_recommendations` method with the following code:

```python
async def get_recommendations(self, keyword_phrase, previous_links_str=None):
prompt = f"""Please return 5 recommendations based on the input string: '{keyword_phrase}' using correct JSON syntax that contains a title and a hyperlink back to the supporting website. RETURN ONLY JSON AND NOTHING ELSE"""
system_prompt = """You are an administrative assistant bot who is good at giving
recommendations for tasks that need to be done by referencing website links that can provide
assistance to helping complete the task.
If there are not any recommendations simply return an empty collection.
EXPECTED OUTPUT:
Provide your response as a JSON object with the following schema:
[{"title": "...", "link": "..."},
{"title": "...", "link": "..."},
{"title": "...", "link": "..."}]
"""

if previous_links_str is not None:
prompt = prompt + f". EXCLUDE the following links from your recommendations: {previous_links_str}"

message_text = [{"role":"system","content":system_prompt},
{"role":"user","content":prompt},]

response = self.client.chat.completions.create(
model= deployment,
messages = message_text,
temperature=0.14,
max_tokens=800,
top_p=0.17,
frequency_penalty=0,
presence_penalty=0,
stop=None
)

result = response.choices[0].message.content
print(result)

try:
recommendation = json.loads(result)
except Exception as e:
print(f"Error loading recommendations: {e}")
recommendation = [{"title": "Sorry, unable to recommendation at this time", "link": ""}]

return recommendation
```

The only changes that we made to this function are as follows:
![Recommendation Engine Changes](/Track_2_ToDo_App/Sprint-05%20-%20Advanced%20AI%20recommendations/images/Recommendation_engine_changes-S05-F01-US03.png)
- We added the `previous_links_str` parameter to the function. This parameter will be used to pass in any previous recommendations that we want to exclude from the new recommendations.
- We updated the prompt to include the `previous_links_str` if it is passed in. You will notice that we simply append some additional prompt text to the exiting prompt to exclude previous recommendations where `{previous_links_str}` is replaced with the string of the previous recommendations.

```text
. EXCLUDE the following links from your recommendations: {previous_links_str}
<br/>
### Updating Web Application Backend to handle refreshing recommendations
#### 1. Update Recommend Route
We now need to update the backend route to handle the refresh of the recommendations. Open the `app.py` file and replace the `recommend` route with the following code:
```python
@app.route('/recommend/<int:id>', methods=['GET'])
@app.route('/recommend/<int:id>/<refresh>', methods=['GET'])
async def recommend(id, refresh=False):
recommendation_engine = RecommendationEngine()
g.todo = db.session.query(Todo).filter_by(id=id).first()
if g.todo and not refresh:
try:
#attempt to load any saved recommendation from the DB
if g.todo.recommendations_json is not None:
g.todo.recommendations = json.loads(g.todo.recommendations_json)
return render_template('index.html')
except ValueError as e:
print("Error:", e)
previous_links_str = None
if refresh:
g.todo.recommendations = json.loads(g.todo.recommendations_json)
# Extract links
links = [item["link"] for item in g.todo.recommendations]
# Convert list of links to a single string
previous_links_str = ", ".join(links)
g.todo.recommendations = await recommendation_engine.get_recommendations(g.todo.name, previous_links_str)
# Save the recommendations to the database
try:
g.todo.recommendations_json = json.dumps(g.todo.recommendations)
db.session.add(g.todo)
db.session.commit()
except Exception as e:
print(f"Error adding and committing todo: {e}")
return
return render_template('index.html')
```

This code adds a new route `/refresh/<int:id>/refresh` to the existing recommend function that will handle the refresh of the recommendations. It also updates the parameter to the `recommend` route to include a `refresh` parameter that will be used to determine if the user is requesting a refresh of the recommendations. If the user is requesting a refresh, the existing recommendations are loaded from the database and passed to the `get_recommendations` method along with the `previous_links_str` that is generated from the existing recommendations. The new recommendations are then saved to the database and displayed on the UI.

<br/>

### Update the User Interface to include a "refresh" button

#### 1. Update the `index.html` file
Finally we will need to update the UI to include a "refresh" button. Open the `index.html` file in the `templates` folder of your application. Replace the `<div id="recommendations-div"...>`section with the following changes:

```html
<div id="recommendations-div" class="card">
<div class="card-body">
<div class="list-group" id="list-of-recommendations">
<h5>AI Recommendations for "{{ g.todo.name }}"</h5>
{% for recommend in g.todo.recommendations %}
<a href="{{ recommend.link }}" class="list-group-item list-group-item-action"> {{ recommend.title }} </a>
{% endfor %}
</div>
<br />
Don't like recommendations?
<a href="{{ url_for('recommend', id=g.todo.id, refresh=true) }}" class="btn btn-info btn-fixed-width"> Refresh </a>
</div>
</div>
```

This code adds a "Refresh Recommendations" button to the UI that will call the `/recommend/<int:id>/refresh` route when clicked. This will allow the user to refresh the recommendations for a task and see new recommendations generated by the AI recommendation engine.

<br/>

#### 2. Run the Web Application
Now let's see this functionality in action. Start the web app by running the following command in the terminal:

```bash
python app.py
```

You should now see a "Refresh" button within the recommendations section of the task details page. It should look something like this:
![Refresh Recommendations](/Track_2_ToDo_App/Sprint-05%20-%20Advanced%20AI%20recommendations/images/outcome-S05-F01-US03.png)


<br/>
🎉 Congratulations! You have now have the ability to refresh your AI recommendations.

<br/>

> [!NOTE]
> 📄For the full source code for this exercise please see [here](/Track_2_ToDo_App/Sprint-05%20-%20Advanced%20AI%20recommendations/src/app-s05-f01-us03/).

<br/>

[🔼 Back **Workshop** Instructions ](/Track_2_ToDo_App/Workshop-Format.md) | [🔼 Back to **Hackathon** Sprint 5 ](/Track_2_ToDo_App/Sprint-05%20-%20Advanced%20AI%20recommendations/README.md) | [**◀ Previous user story**](User%20Story%202%20-%20Cache%20recommendations%20in%20DB.md) | [**Next user story** (in next sprint) ▶](/Track_2_ToDo_App/Sprint-06%20-%20Advanced%20To-Do%20Details/Feature%201%20-%20Add%20Additional%20To-Do%20Details/User%20Story%201%20-%20Add%20additional%20details%20to%20to-do%20item.md)
13 changes: 13 additions & 0 deletions Track_2_ToDo_App/Sprint-05 - Advanced AI recommendations/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
# Sprint 5: Advanced AI Recommendations
⏲️ _Est. time to complete: 60 min._ ⏲️

In this sprint you will be taking the application that you built in Sprint 4 and adding in AI recommendations to help users complete their tasks. This will be done by leveraging the Azure OpenAI API to generate recommendations based on the task name.

**📕Feature: Advanced AI Recommendations**
1. [**📖 Get Recommendations from Generative AI based on To-Do name**](/Track_2_ToDo_App/Sprint-05%20-%20Advanced%20AI%20recommendations/Feature%201%20-%20Get%20Generative%20AI%20recommendation/User%20Story%201%20-%20Get%20Gen%20AI%20recommendation.md)
2. [**📖 Store the recommendations in the DB for a task**](/Track_2_ToDo_App/Sprint-05%20-%20Advanced%20AI%20recommendations/Feature%201%20-%20Get%20Generative%20AI%20recommendation/User%20Story%202%20-%20Cache%20recommendations%20in%20DB.md)
3. [**📖 Allow the user to refresh the recommendations**](/Track_2_ToDo_App/Sprint-05%20-%20Advanced%20AI%20recommendations/Feature%201%20-%20Get%20Generative%20AI%20recommendation/User%20Story%203%20-%20Refresh%20Recommendations.md)

<br/>

[🔼 Hackathon Home Page ](/Track_2_ToDo_App/README.md) | [◀ Previous Sprint](/Track_2_ToDo_App/Sprint-04%20-%20Voice%20To%20Text/README.md) | [Next sprint ▶](/Track_2_ToDo_App/Sprint-06%20-%20Advanced%20To-Do%20Details/README.md)
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
# Sprint 5 - Source Code Directory
This directory contains the completed source code after the end of each user story. The directory is structured as `app-s05-f01-us02` where
- `s05` - represents the sprint number, in this case sprint 5
- `f01` - represents the feature number, in this case feature 1
- `us02` - represents the user story number, in this case user story 2

> [!NOTE]
> The code in the directory is the completed solution after the completion of that user story. Like many other things in life, there are many ways to solve a problem. The source code in these directories is just one solution to the problem and does not necessarily represent best practices for a given solution. In many cases we chose simplicity or readability over efficiency.
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
DEBUG_APP="True"
USE_AZURE_OPENAI="True"
OPENAI_API_KEY="<get the key from https://platform.openai.com/api-keys>"
OPENAI_ORG_ID="<get the ord-id from https://platform.openai.com/account/organization>"
OPEN_AI_DEPLOYMENT_NAME="gpt-3.5-turbo"
AZURE_OPENAI_DEPLOYMENT_NAME="gpt-35-turbo"
AZURE_OPENAI_ENDPOINT=""
AZURE_OPENAI_API_KEY=""
AZURE_OPENAI_VERSION="2023-05-15"
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
###############################################################################
## Sprint 5: Advanced AI Recommendations
## Feature 1: Get Gen AI Recommendation
## User Story 1: Get Gen AI Recommendation
############################################################################
import os
from flask import Flask, render_template, request, redirect, url_for, g
from database import db, Todo
from recommendation_engine import RecommendationEngine

app = Flask(__name__)
basedir = os.path.abspath(os.path.dirname(__file__)) # Get the directory of the this file
todo_file = os.path.join(basedir, 'todo_list.txt') # Create the path to the to-do list file using the directory
app.config["SQLALCHEMY_DATABASE_URI"] = 'sqlite:///' + os.path.join(basedir, 'todos.db')
app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False

db.init_app(app)

with app.app_context():
db.create_all()

@app.before_request
def load_data_to_g():
todos = Todo.query.all()
g.todos = todos
g.todo = None

@app.route("/")
def index():
return render_template("index.html")

@app.route("/add", methods=["POST"])
def add_todo():
# Get the data from the form
todo = Todo(
name=request.form["todo"],
)
# Add the new ToDo to the list
db.session.add(todo)
db.session.commit()

# Add the new ToDo to the list
return redirect(url_for('index'))

# Delete a ToDo
@app.route('/remove/<int:id>', methods=["POST"])
def remove_todo(id):
db.session.delete(Todo.query.filter_by(id=id).first())
db.session.commit()
return redirect(url_for('index'))

# Show AI recommendations
@app.route('/recommend/<int:id>', methods=['GET'])
async def recommend(id):
recommendation_engine = RecommendationEngine()
g.todo = db.session.query(Todo).filter_by(id=id).first()
g.todo.recommendations = await recommendation_engine.get_recommendations(g.todo.name)

return render_template('index.html')


if __name__ == "__main__":
app.run(debug=True)
Loading

0 comments on commit c77c712

Please sign in to comment.