Skip to content

Commit

Permalink
LLM Suggestion local runs
Browse files Browse the repository at this point in the history
- Adds LLM feature to next
- Creates endpoints in flask
- Adds conditional logic checking for OPEN_API_KEY
- Update READEME.md
  • Loading branch information
lukemun committed Nov 12, 2024
1 parent 4082b48 commit 254323c
Show file tree
Hide file tree
Showing 8 changed files with 223 additions and 11 deletions.
11 changes: 11 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -175,3 +175,14 @@ gcloud config list, to display current account
```

`gcloud app deploy` does not support `--update-env-vars RELEASE=$RELEASE` like `gcloud run deploy` does with Cloud Run

## Local Run with AI Suggestions

1. Add your OPENAI_API_KEY= to flask .env
2. Start flask locally (./deploy.sh --env=local flask)
3. Take note of where it's running (likey http://127.0.0.1:8080)
4. Change NEXT_PUBLIC_FLASK_BACKEND to the url from Step 3
5. Run the next server (npm run dev)
6. Get suggestion should show. Clicking it will go next client -> next server -> flask

On main page load, next will check with flask if it has the OPEN_API_KEY and conditionally show the get suggestion input.
4 changes: 3 additions & 1 deletion flask/requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -8,5 +8,7 @@ pg8000==1.16.6
psycopg2-binary==2.9.9
python-dotenv==0.12.0
pytz==2020.4
sentry-sdk==2.14.0
sentry-sdk==2.17.0
sqlalchemy==1.4.49
openai==1.52.2
tiktoken==0.8.0
48 changes: 44 additions & 4 deletions flask/src/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,16 @@
import random
import requests
import time
from flask import Flask, json, request, make_response, send_from_directory
from flask import Flask, json, request, make_response, send_from_directory, jsonify
from flask_cors import CORS
from openai import OpenAI
import dotenv
from .db import get_products, get_products_join, get_inventory
from .utils import parseHeaders, get_iterator
import sentry_sdk
from sentry_sdk.integrations.flask import FlaskIntegration
from sentry_sdk.integrations.sqlalchemy import SqlalchemyIntegration
from sentry_sdk.ai.monitoring import ai_track

RUBY_CUSTOM_HEADERS = ['se', 'customerType', 'email']
pests = ["aphids", "thrips", "spider mites", "lead miners", "scale", "whiteflies", "earwigs", "cutworms", "mealybugs",
Expand Down Expand Up @@ -40,7 +42,7 @@ def before_send(event, hint):
# Now that TDA puts platform/browser and test path into SE tag we want to prevent
# creating separate issues for those. See https://github.com/sentry-demos/empower/pull/332
se_fingerprint = prefix[0]

if se.startswith('prod-tda-'):
event['fingerprint'] = ['{{ default }}', se_fingerprint, RELEASE]
else:
Expand All @@ -51,7 +53,8 @@ def before_send(event, hint):

def traces_sampler(sampling_context):
sentry_sdk.set_context("sampling_context", sampling_context)
REQUEST_METHOD = sampling_context['wsgi_environ']['REQUEST_METHOD']
wsgi_environ = sampling_context.get('wsgi_environ', {})
REQUEST_METHOD = wsgi_environ.get('REQUEST_METHOD', 'GET')
if REQUEST_METHOD == 'OPTIONS':
return 0.0
else:
Expand Down Expand Up @@ -96,6 +99,39 @@ def __init__(self, import_name, *args, **kwargs):
app = MyFlask(__name__)
CORS(app)

client = OpenAI(api_key= os.environ["OPENAI_API_KEY"])


@app.route('/suggestion', methods=['GET'])
def suggestion():
print("got request")
sentry_sdk.metrics.incr(
key="endpoint_call",
value=1,
tags={"endpoint": "/suggestion", "method": "GET"},
)

catalog = request.args.get('catalog')
prompt = f'''You are witty plant salesman. Here is your catalog of plants: {catalog}.
Provide a suggestion based on the user\'s location. Pick one plant from the catalog provided.
Keep your response short and concise. Try to incorporate the weather and current season.'''
geo = request.args.get('geo')

@ai_track("Suggestion Pipeline")
def suggestion_pipeline():
with sentry_sdk.start_transaction(op="Suggestion AI", description="Suggestion ai pipeline"):
response = client.chat.completions.create(
model="gpt-4o-mini",
messages=
[
{ "role" : "system", "content": prompt },
{ "role": "user", "content": geo }
]).choices[0].message.content
return response

response = suggestion_pipeline()
return jsonify({"suggestion": response}), 200


@app.route('/checkout', methods=['POST'])
def checkout():
Expand Down Expand Up @@ -154,7 +190,7 @@ def products():
value=1,
tags={"endpoint": "/products", "method": "GET"},
)

product_inventory = None
fetch_promotions = request.args.get('fetch_promotions')
timeout_seconds = (EXTREMELY_SLOW_PROFILE if fetch_promotions else NORMAL_SLOW_PROFILE)
Expand Down Expand Up @@ -255,6 +291,10 @@ def connect():
return "flask /connect"


@app.route('/showSuggestion', methods=['GET'])
def showSuggestion():
return jsonify({"response":os.environ["OPENAI_API_KEY"] is not None}), 200

@app.route('/product/0/info', methods=['GET'])
def product_info():
time.sleep(.55)
Expand Down
16 changes: 14 additions & 2 deletions next/lib/data.js
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,18 @@ export default async function getProducts() {
}
}

export async function getProductsOnly() {
try {
console.log("Fetching products...");
const products = await prisma.products.findMany();

return products;
} catch (error) {
console.error("Database Error:", error)
// do sentry stuff
}
}

export async function getProduct(index) {
const i = Number(index);
try {
Expand Down Expand Up @@ -75,7 +87,7 @@ export async function checkoutAction(cart) {
}
}

return {status : 200, message: "success"}
return { status: 200, message: "success" }
}


Expand All @@ -92,7 +104,7 @@ export async function getInventory(cart) {
let inventory;
try {
inventory = await prisma.inventory.findMany({
where: { id : { in : productIds } }
where: { id: { in: productIds } }
});
} catch (error) {
console.log("Database Error:", error);
Expand Down
35 changes: 35 additions & 0 deletions next/src/app/api/showSuggestion/route.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@

import { NextResponse } from "next/server";
import { getProductsOnly } from '@/lib/data.js';
import {
determineBackendUrl,
} from '@/src/utils/backendrouter';


export async function GET(request) {
const backendUrl = determineBackendUrl('flask');

const resp = await fetch(backendUrl + `/showSuggestion`, {
method: 'GET',
headers: { 'Content-Type': 'application/json' },
})
.then((result) => {
if (!result.ok) {
// Sentry.setContext('err', {
// status: result.status,
// statusText: result.statusText,
// });
return Promise.reject();
} else {
return result.json();
}
});

return NextResponse.json({ response: resp.response }, { status: 200 })
}

function extractRelevantDataAsString(items) {
return items.map(item =>
`Title: ${item.title}, Description: ${item.description}, Price: ${item.price}`
).join('; ');
}
40 changes: 40 additions & 0 deletions next/src/app/api/suggestion/route.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@

import { NextResponse } from "next/server";
import { getProductsOnly } from '@/lib/data.js';
import {
determineBackendUrl,
} from '@/src/utils/backendrouter';


export async function GET(request) {
console.log("Sending ai suggeestion request...")
const geo = request.nextUrl.searchParams.get("geo");
const productsFull = await getProductsOnly();
const products = extractRelevantDataAsString(productsFull);

const backendUrl = determineBackendUrl('flask');

const resp = await fetch(backendUrl + `/suggestion?catalog=${products}&geo=${geo}`, {
method: 'GET',
headers: { 'Content-Type': 'application/json' },
})
.then((result) => {
if (!result.ok) {
// Sentry.setContext('err', {
// status: result.status,
// statusText: result.statusText,
// });
return Promise.reject();
} else {
return result.json();
}
});

return NextResponse.json({ suggestion: resp.suggestion }, { status: 200 })
}

function extractRelevantDataAsString(items) {
return items.map(item =>
`Title: ${item.title}, Description: ${item.description}, Price: ${item.price}`
).join('; ');
}
61 changes: 57 additions & 4 deletions next/src/app/page.jsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
'use client'

import * as Sentry from '@sentry/nextjs';
import plantsBackground from '@/public/plants-background-img.jpg';
import ButtonLink from '@/src/ui/ButtonLink';
import plantsBackground from '/public/plants-background-img.jpg';
import ButtonLink from '/src/ui/ButtonLink';
import { useSearchParams } from 'next/navigation';

import { useEffect } from 'react';
import { useEffect, useState } from 'react';
import { useRouter } from 'next/navigation';
import {
determineBackendType,
Expand All @@ -17,15 +17,49 @@ const divStyle = {
};


export default function Page() {
export default function Page(props) {

console.log("in home page");
const router = useRouter();
const { backend, frontendSlowdown } = useSearchParams();
const backendType = determineBackendType(backend);
const backendUrl = determineBackendUrl(backendType);
console.log('backend is ' + backendUrl);
const [showSuggestionFeature, setShowSuggestionFeature] = useState(false);

const [suggestion, setSuggestion] = useState("");
const [city, setCity] = useState("");

const handleInputChange = (e) => {
setCity(e.target.value);
}

const getShowSuggestionFeature = async () => {
try {
let resp = await fetch(`/api/showSuggestion`);
let data = await resp.json()
setShowSuggestionFeature(data.response)
} catch (err) {
console.error("Error checking for suggestion feature");
}
}


const getSuggestion = async () => {
console.log("Fetching suggestion...")
try {
let resp = await fetch(`/api/suggestion?geo=${city}`);
console.log(resp);
let data = await resp.json();
setSuggestion(data.suggestion);
console.log(data.suggestion);
const ele = document.getElementById('hero-suggestion');
ele.classList.add("fade-in");

} catch (err) {
console.error("Error fetching suggestion", err);
}
}

useEffect(() => {
try {
Expand All @@ -36,6 +70,8 @@ export default function Page() {
'Content-Type': 'application/json',
},
});

getShowSuggestionFeature();
} catch (err) {
Sentry.captureException(err);
}
Expand All @@ -50,6 +86,23 @@ export default function Page() {
<ButtonLink to={'/products'} params={router.query}>
Browse products
</ButtonLink>
{showSuggestionFeature &&
<div>
<button onClick={getSuggestion}>
Get Suggestion
</button>

{!suggestion &&
<input
className="city-input"
name="city"
placeholder="Your City"
onChange={handleInputChange}
/>}
<div id="hero-suggestion">
<p>{suggestion}</p>
</div>
</div>}
</div>
</div>
);
Expand Down
19 changes: 19 additions & 0 deletions next/src/styles/index.css
Original file line number Diff line number Diff line change
Expand Up @@ -155,6 +155,25 @@ input[type='text'] {
margin-top: 0.5rem;
}


.city-input {
width: 200px;
}

.fade-in {
animation: fadeIn 1s;
}

@keyframes fadeIn {
0% {
opacity: 0;
}

100% {
opacity: 1;
}
}

.star {
/* lighter */
/* color: #F1B71C; */
Expand Down

0 comments on commit 254323c

Please sign in to comment.