feat: 🎸 models (#1967)

updated available models

# Description

Please include a summary of the changes and the related issue. Please
also include relevant motivation and context.

## Checklist before requesting a review

Please delete options that are not relevant.

- [ ] My code follows the style guidelines of this project
- [ ] I have performed a self-review of my code
- [ ] I have commented hard-to-understand areas
- [ ] I have ideally added tests that prove my fix is effective or that
my feature works
- [ ] New and existing unit tests pass locally with my changes
- [ ] Any dependent changes have been merged

## Screenshots (if appropriate):
This commit is contained in:
Stan Girard 2024-01-04 01:14:03 +01:00 committed by GitHub
parent fae71f1935
commit a65eb5a9cd
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 233 additions and 83 deletions

1
.gitignore vendored
View File

@ -73,3 +73,4 @@ backend/slim.report.json
volumes/db/data/
volumes/storage/stub/stub/quivr/*
supabase/migrations/20240103191539_private.sql
supabase/20240103191539_private.sql

View File

@ -48,6 +48,7 @@ You can find the installation video [here](https://www.youtube.com/watch?v=cXBa6
```bash
git clone https://github.com/StanGirard/Quivr.git && cd Quivr
git checkout v0.0.152
```
- **Step 2**: Copy the `.env.example` files

View File

@ -1,4 +1,3 @@
import os
from datetime import datetime
from uuid import UUID
@ -12,7 +11,9 @@ class UserUsage(Repository):
def __init__(self, supabase_client):
self.db = supabase_client
def create_user_daily_usage(self, user_id: UUID, user_email: str, date: datetime):
def create_user_daily_usage(
self, user_id: UUID, user_email: str, date: datetime, number: int = 1
):
return (
self.db.table("user_daily_usage")
.insert(
@ -20,68 +21,138 @@ class UserUsage(Repository):
"user_id": str(user_id),
"email": user_email,
"date": date,
"daily_requests_count": 1,
"daily_requests_count": number,
}
)
.execute()
)
def check_subscription_validity(self, customer_id: str) -> bool:
"""
Check if the subscription of the user is still valid
"""
now = datetime.now()
# Format the datetime object as a string in the appropriate format for your Supabase database
now_str = now.strftime("%Y-%m-%d %H:%M:%S.%f")
subscription_still_valid = (
self.db.from_("subscriptions")
.select("*")
.filter(
"customer", "eq", customer_id
) # then check if current_period_end is greater than now with timestamp format
.filter("current_period_end", "gt", now_str)
.execute()
).data
if len(subscription_still_valid) > 0:
return True
def check_user_is_customer(self, user_id: UUID) -> (bool, str):
"""
Check if the user is a customer and return the customer id
"""
user_email_customer = (
self.db.from_("users")
.select("*")
.filter("id", "eq", str(user_id))
.execute()
).data
if len(user_email_customer) == 0:
return False, None
matching_customers = (
self.db.table("customers")
.select("email,id")
.filter("email", "eq", user_email_customer[0]["email"])
.execute()
).data
if len(matching_customers) == 0:
return False, None
return True, matching_customers[0]["id"]
def update_customer_settings_with_product_settings(
self, user_id: UUID, customer_id: str
):
"""
Check if the user is a customer and return the customer id
"""
matching_products = (
self.db.table("subscriptions")
.select("attrs")
.filter("customer", "eq", customer_id)
.execute()
).data
# Output object
# {"id":"sub_1OUZOgJglvQxkJ1H98TSY9bv","plan":{"id":"price_1NwMsXJglvQxkJ1Hbzs5JkTs","active":true,"amount":1900,"object":"plan","created":1696156081,"product":"prod_OjqZPhbBQwmsB8","currency":"usd","interval":"month","livemode":false,"metadata":{},"nickname":null,"tiers_mode":null,"usage_type":"licensed","amount_decimal":"1900","billing_scheme":"per_unit","interval_count":1,"aggregate_usage":null,"transform_usage":null,"trial_period_days":null},"items":{"url":"/v1/subscription_items?subscription=sub_1OUZOgJglvQxkJ1H98TSY9bv","data":[{"id":"si_PJBm1ciQlpaOA4","plan":{"id":"price_1NwMsXJglvQxkJ1Hbzs5JkTs","active":true,"amount":1900,"object":"plan","created":1696156081,"product":"prod_OjqZPhbBQwmsB8","currency":"usd","interval":"month","livemode":false,"metadata":{},"nickname":null,"tiers_mode":null,"usage_type":"licensed","amount_decimal":"1900","billing_scheme":"per_unit","interval_count":1,"aggregate_usage":null,"transform_usage":null,"trial_period_days":null},"price":{"id":"price_1NwMsXJglvQxkJ1Hbzs5JkTs","type":"recurring","active":true,"object":"price","created":1696156081,"product":"prod_OjqZPhbBQwmsB8","currency":"usd","livemode":false,"metadata":{},"nickname":null,"recurring":{"interval":"month","usage_type":"licensed","interval_count":1,"aggregate_usage":null,"trial_period_days":null},"lookup_key":null,"tiers_mode":null,"unit_amount":1900,"tax_behavior":"unspecified","billing_scheme":"per_unit","custom_unit_amount":null,"transform_quantity":null,"unit_amount_decimal":"1900"},"object":"subscription_item","created":1704307355,"metadata":{},"quantity":1,"tax_rates":[],"subscription":"sub_1OUZOgJglvQxkJ1H98TSY9bv","billing_thresholds":null}],"object":"list","has_more":false,"total_count":1},"object":"subscription","status":"active","created":1704307354,"currency":"usd","customer":"cus_PJBmxGOKfQgYDN","discount":null,"ended_at":null,"livemode":false,"metadata":{},"quantity":1,"schedule":null,"cancel_at":null,"trial_end":null,"start_date":1704307354,"test_clock":null,"application":null,"canceled_at":null,"description":null,"trial_start":null,"on_behalf_of":null,"automatic_tax":{"enabled":true},"transfer_data":null,"days_until_due":null,"default_source":null,"latest_invoice":"in_1OUZOgJglvQxkJ1HysujPh0b","pending_update":null,"trial_settings":{"end_behavior":{"missing_payment_method":"create_invoice"}},"pause_collection":null,"payment_settings":{"payment_method_types":null,"payment_method_options":null,"save_default_payment_method":"off"},"collection_method":"charge_automatically","default_tax_rates":[],"billing_thresholds":null,"current_period_end":1706985754,"billing_cycle_anchor":1704307354,"cancel_at_period_end":false,"cancellation_details":{"reason":null,"comment":null,"feedback":null},"current_period_start":1704307354,"pending_setup_intent":null,"default_payment_method":"pm_1OUZOfJglvQxkJ1HSHU0TTWW","application_fee_percent":null,"pending_invoice_item_interval":null,"next_pending_invoice_item_invoice":null}
# Now extract the product id from the object
if len(matching_products) == 0:
logger.info("No matching products found")
return
product_id = matching_products[0]["attrs"]["items"]["data"][0]["plan"][
"product"
]
# Now fetch the product settings
matching_product_settings = (
self.db.table("product_to_features")
.select("*")
.filter("stripe_product_id", "eq", product_id)
.execute()
).data
if len(matching_product_settings) == 0:
logger.info("No matching product settings found")
return
product_settings = matching_product_settings[0]
# Now update the user settings with the product settings
try:
self.db.table("user_settings").update(
{
"max_brains": product_settings["max_brains"],
"max_brain_size": product_settings["max_brain_size"],
"daily_chat_credit": product_settings["daily_chat_credit"],
"api_access": product_settings["api_access"],
"models": product_settings["models"],
}
).match({"user_id": str(user_id)}).execute()
except Exception as e:
logger.error(e)
logger.error("Error while updating user settings with product settings")
def check_if_is_premium_user(self, user_id: UUID):
"""
Check if the user is a premium user
"""
matching_customers = None
try:
is_premium_user = (
self.db.from_("user_settings")
.select("is_premium")
.filter("user_id", "eq", str(user_id))
.execute()
.data
)
user_is_customer, user_customer_id = self.check_user_is_customer(user_id)
logger.info("🔥🔥🔥")
logger.info(user_is_customer)
logger.info(user_customer_id)
if len(is_premium_user) > 0 and is_premium_user[0]["is_premium"]:
return True
if user_is_customer:
self.db.table("user_settings").update({"is_premium": True}).match(
{"user_id": str(user_id)}
).execute()
user_email_customer = (
self.db.from_("users")
.select("*")
.filter("id", "eq", str(user_id))
.execute()
).data
if len(user_email_customer) == 0:
return False
matching_customers = (
self.db.table("customers")
.select("email,id")
.filter("email", "eq", user_email_customer[0]["email"])
.execute()
).data
if len(matching_customers) == 0:
return False
now = datetime.now()
# Format the datetime object as a string in the appropriate format for your Supabase database
now_str = now.strftime("%Y-%m-%d %H:%M:%S.%f")
subscription_still_valid = (
self.db.from_("subscriptions")
.select("*")
.filter(
"customer", "eq", matching_customers[0]["id"]
) # then check if current_period_end is greater than now with timestamp format
.filter("current_period_end", "gt", now_str)
.execute()
).data
if len(subscription_still_valid) > 0:
if len(matching_customers) > 0:
self.db.table("user_settings").update({"is_premium": True}).match(
{"user_id": str(user_id)}
).execute()
if user_is_customer and self.check_subscription_validity(user_customer_id):
logger.info("User is a premium user")
self.update_customer_settings_with_product_settings(
user_id, user_customer_id
)
return True
else:
self.db.table("user_settings").update({"is_premium": False}).match(
@ -91,6 +162,7 @@ class UserUsage(Repository):
except Exception as e:
logger.info(matching_customers)
logger.error(e)
logger.error(
"Error while checking if user is a premium user. Stripe needs to be configured."
)
@ -125,18 +197,29 @@ class UserUsage(Repository):
check_is_premium = self.check_if_is_premium_user(user_id)
if check_is_premium:
user_settings["max_brains"] = int(
os.environ.get("PREMIUM_MAX_BRAIN_NUMBER", 12)
)
user_settings["max_brain_size"] = int(
os.environ.get("PREMIUM_MAX_BRAIN_SIZE", 50000000)
)
user_settings["daily_chat_credit"] = int(
os.environ.get("PREMIUM_DAILY_CHAT_CREDIT", 100)
)
# get the possibly updated user settings
user_settings_response = (
self.db.from_("user_settings")
.select("*")
.filter("user_id", "eq", str(user_id))
.execute()
).data
return user_settings_response[0]
return user_settings
def get_model_settings(self):
"""
Fetch the user settings from the database
"""
model_settings_response = (self.db.from_("models").select("*").execute()).data
if len(model_settings_response) == 0:
raise ValueError("An issue occured while fetching the model settings")
return model_settings_response
def get_user_usage(self, user_id):
"""
Fetch the user request stats from the database
@ -165,13 +248,15 @@ class UserUsage(Repository):
return response[0]["daily_requests_count"]
return None
def increment_user_request_count(self, user_id, date, current_requests_count: int):
def increment_user_request_count(
self, user_id, date, current_requests_count: int, number: int = 1
):
"""
Increment the user's requests count for a specific day
"""
self.update_user_request_count(
user_id, daily_requests_count=current_requests_count + 1, date=date
user_id, daily_requests_count=current_requests_count + number, date=date
)
def update_user_request_count(self, user_id, daily_requests_count, date):

View File

@ -24,6 +24,14 @@ class UserUsage(UserIdentity):
return request
def get_model_settings(self):
"""
Fetch the user request stats from the database
"""
request = self.supabase_db.get_model_settings()
return request
def get_user_settings(self):
"""
Fetch the user settings from the database
@ -32,7 +40,7 @@ class UserUsage(UserIdentity):
return request
def handle_increment_user_request_count(self, date):
def handle_increment_user_request_count(self, date, number=1):
"""
Increment the user request count in the database
"""
@ -44,15 +52,16 @@ class UserUsage(UserIdentity):
if self.email is None:
raise ValueError("User Email should be defined for daily usage table")
self.supabase_db.create_user_daily_usage(
user_id=self.id, date=date, user_email=self.email
user_id=self.id, date=date, user_email=self.email, number=number
)
self.daily_requests_count = 1
self.daily_requests_count = number
return
self.supabase_db.increment_user_request_count(
user_id=self.id,
date=date,
current_requests_count=current_requests_count,
number=number,
)
self.daily_requests_count = current_requests_count

View File

@ -21,21 +21,27 @@ class NullableUUID(UUID):
return None
def check_user_requests_limit(
user: UserIdentity,
):
def check_user_requests_limit(user: UserIdentity, model: str):
userDailyUsage = UserUsage(id=user.id, email=user.email)
userSettings = userDailyUsage.get_user_settings()
date = time.strftime("%Y%m%d")
userDailyUsage.handle_increment_user_request_count(date)
daily_chat_credit = userSettings.get("daily_chat_credit", 0)
models_price = userDailyUsage.get_model_settings()
user_choosen_model_price = 1000
for model_setting in models_price:
if model_setting["name"] == model:
user_choosen_model_price = model_setting["price"]
if int(userDailyUsage.daily_requests_count) >= int(daily_chat_credit):
raise HTTPException(
status_code=429, # pyright: ignore reportPrivateUsage=none
detail=f"You have reached your daily chat limit of {daily_chat_credit} requests per day. Please upgrade your plan to increase your daily chat limit.",
)
else:
userDailyUsage.handle_increment_user_request_count(
date, user_choosen_model_price
)
pass

View File

@ -155,7 +155,7 @@ async def create_question_handler(
chat_question.max_tokens = chat_question.max_tokens or fallback_max_tokens
try:
check_user_requests_limit(current_user)
check_user_requests_limit(current_user, chat_question.model)
is_model_ok = (chat_question).model in user_settings.get("models", ["gpt-3.5-turbo"]) # type: ignore
gpt_answer_generator = chat_instance.get_answer_generator(
chat_id=str(chat_id),
@ -229,7 +229,7 @@ async def create_stream_question_handler(
try:
logger.info(f"Streaming request for {chat_question.model}")
check_user_requests_limit(current_user)
check_user_requests_limit(current_user, chat_question.model)
# TODO check if model is in the list of models available for the user
is_model_ok = chat_question.model in user_settings.get("models", ["gpt-3.5-turbo"]) # type: ignore

View File

@ -1,4 +1,3 @@
import { useFeatureIsOn } from "@growthbook/growthbook-react";
import { useTranslation } from "react-i18next";
import { StripePricingModal } from "@/lib/components/Stripe";
@ -10,11 +9,6 @@ const MANAGE_PLAN_URL = process.env.NEXT_PUBLIC_STRIPE_MANAGE_PLAN_URL;
export const StripePricingOrManageButton = (): JSX.Element => {
const { t } = useTranslation("monetization");
const { userData } = useUserData();
const monetizationIsOn = useFeatureIsOn("monetization");
if (!monetizationIsOn) {
return <></>;
}
const is_premium = userData?.is_premium ?? false;
if (is_premium) {

View File

@ -1,4 +1,3 @@
import { useFeatureIsOn } from "@growthbook/growthbook-react";
import { useTranslation } from "react-i18next";
import { LuStar } from "react-icons/lu";
@ -9,10 +8,9 @@ import { useUserData } from "@/lib/hooks/useUserData";
export const UpgradeToPlus = (): JSX.Element => {
const { userData } = useUserData();
const is_premium = userData?.is_premium;
const featureIsOn = useFeatureIsOn("monetization");
const { t } = useTranslation("monetization");
if (!featureIsOn || is_premium === true) {
if (is_premium === true) {
return <></>;
}

View File

@ -1,4 +1,3 @@
import { useFeatureIsOn } from "@growthbook/growthbook-react";
import { useTranslation } from "react-i18next";
import { FiUser } from "react-icons/fi";
@ -10,10 +9,9 @@ import { SidebarFooterButton } from "./SidebarFooterButton";
export const UpgradeToPlus = (): JSX.Element => {
const { userData } = useUserData();
const is_premium = userData?.is_premium;
const featureIsOn = useFeatureIsOn("monetization");
const { t } = useTranslation("monetization");
if (!featureIsOn || is_premium === true) {
if (is_premium === true) {
return <></>;
}

View File

@ -0,0 +1,3 @@
alter table "public"."product_to_features" alter column "models" set not null;

View File

@ -0,0 +1,55 @@
create table "public"."models" (
"name" text not null,
"price" integer default 1,
"max_input" integer default 2000,
"max_output" integer default 1000
);
CREATE UNIQUE INDEX models_pkey ON public.models USING btree (name);
alter table "public"."models" add constraint "models_pkey" PRIMARY KEY using index "models_pkey";
grant delete on table "public"."models" to "anon";
grant insert on table "public"."models" to "anon";
grant references on table "public"."models" to "anon";
grant select on table "public"."models" to "anon";
grant trigger on table "public"."models" to "anon";
grant truncate on table "public"."models" to "anon";
grant update on table "public"."models" to "anon";
grant delete on table "public"."models" to "authenticated";
grant insert on table "public"."models" to "authenticated";
grant references on table "public"."models" to "authenticated";
grant select on table "public"."models" to "authenticated";
grant trigger on table "public"."models" to "authenticated";
grant truncate on table "public"."models" to "authenticated";
grant update on table "public"."models" to "authenticated";
grant delete on table "public"."models" to "service_role";
grant insert on table "public"."models" to "service_role";
grant references on table "public"."models" to "service_role";
grant select on table "public"."models" to "service_role";
grant trigger on table "public"."models" to "service_role";
grant truncate on table "public"."models" to "service_role";
grant update on table "public"."models" to "service_role";