Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

LLM Suggestion local runs #625

Merged
merged 5 commits into from
Dec 10, 2024
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 (personal one ATM)
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)
lukemun marked this conversation as resolved.
Show resolved Hide resolved
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.
17 changes: 16 additions & 1 deletion env-config/example.env
Original file line number Diff line number Diff line change
@@ -1,5 +1,20 @@
SENTRY_ORG=

NEXT_PUBLIC_DSN=
NEXT_PUBLIC_ENVIRONMENT=
NEXT_RELEASE_PACKAGE_NAME=
NEXT_SENTRY_PROJECT=
NEXT_SOURCEMAPS_DIR=build/static/js
NEXT_SOURCEMAPS_URL_PREFIX=~/static/js
NEXT_PUBLIC_FLASK_BACKEND=
NEXT_PUBLIC_EXPRESS_BACKEND=
NEXT_PUBLIC_SPRINGBOOT_BACKEND=
NEXT_PUBLIC_ASPNETCORE_BACKEND=
NEXT_PUBLIC_LARAVEL_BACKEND=
NEXT_PUBLIC_RUBY_BACKEND=
NEXT_PUBLIC_RUBYONRAILS_BACKEND=
NEXT_LOCAL_PORT=3000 # only needed in local.env

REACT_APP_DSN=
REACT_APP_ENGINE_SERVICE=
REACT_APP_ENVIRONMENT=
Expand Down Expand Up @@ -78,4 +93,4 @@ CRONSPYTHON_APP_DSN=
CRONSPYTHON_MONITOR_SLUG=
CRONSPYTHON_DEPLOY_HOST=
CRONSPYTHON_DEPLOY_DIR=
CRONSPYTHON_CRONTAB_USER= # if change this must clear crontab for previus user or will have 2 running simult.
CRONSPYTHON_CRONTAB_USER= # if change this must clear crontab for previus user or will have 2 running simult.
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')
realkosty marked this conversation as resolved.
Show resolved Hide resolved
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"])
lukemun marked this conversation as resolved.
Show resolved Hide resolved


@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");
realkosty marked this conversation as resolved.
Show resolved Hide resolved
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
Loading