mirror of
https://github.com/open-webui/open-webui
synced 2025-04-07 22:25:05 +00:00
Implement Usage Statistics, Bug: No Chat ID On First Message
This commit is contained in:
parent
2b2faa1433
commit
b227bc7a97
@ -6,7 +6,7 @@ export PYTHONPATH="/Users/philszalay/Documents/code/beyond-the-loop/backend:$PYT
|
||||
PORT="${PORT:-8080}"
|
||||
|
||||
# Start the LiteLLM container in the background
|
||||
cd /Users/philszalay/Documents/code/beyond-the-loop && docker-compose up -d litellm
|
||||
cd /Users/philszalay/Documents/code/beyond-the-loop && docker-compose -f docker-compose-prod.yaml up -d litellm
|
||||
|
||||
# Start the uvicorn server
|
||||
cd /Users/philszalay/Documents/code/beyond-the-loop/backend && uvicorn open_webui.main:app --port $PORT --host 0.0.0.0 --forwarded-allow-ips '*' --reload
|
||||
|
@ -1,6 +1,6 @@
|
||||
from pydantic import BaseModel, ConfigDict
|
||||
from typing import Optional
|
||||
from sqlalchemy import String, Column, BigInteger, Integer, Text, ForeignKey
|
||||
from sqlalchemy import String, Column, BigInteger, Integer, Text, ForeignKey, Float
|
||||
|
||||
import uuid
|
||||
import time
|
||||
@ -23,6 +23,7 @@ class Completion(Base):
|
||||
model = Column(Text)
|
||||
credits_used = Column(Integer)
|
||||
created_at = Column(BigInteger)
|
||||
time_saved_in_seconds = Column(Float)
|
||||
|
||||
class CompletionModel(BaseModel):
|
||||
id: str
|
||||
@ -31,12 +32,13 @@ class CompletionModel(BaseModel):
|
||||
model: str
|
||||
credits_used: int
|
||||
created_at: int # timestamp in epoch
|
||||
time_saved_in_seconds: float
|
||||
|
||||
model_config = ConfigDict(from_attributes=True)
|
||||
|
||||
|
||||
class CompletionTable:
|
||||
def insert_new_completion(self, user_id: str, chat_id: str, model: str, credits_used: int) -> Optional[CompletionModel]:
|
||||
def insert_new_completion(self, user_id: str, chat_id: str, model: str, credits_used: int, time_saved_in_seconds: float) -> Optional[CompletionModel]:
|
||||
id = str(uuid.uuid4())
|
||||
completion = CompletionModel(
|
||||
**{
|
||||
@ -45,7 +47,8 @@ class CompletionTable:
|
||||
"chat_id": chat_id,
|
||||
"created_at": int(time.time()),
|
||||
"model": model,
|
||||
"credits_used": credits_used
|
||||
"credits_used": credits_used,
|
||||
"time_saved_in_seconds": time_saved_in_seconds
|
||||
}
|
||||
)
|
||||
try:
|
||||
@ -63,5 +66,21 @@ class CompletionTable:
|
||||
print(f"Error creating completion: {e}")
|
||||
return None
|
||||
|
||||
def calculate_saved_time_in_seconds(last_message, response_message):
|
||||
print(last_message + " ----- " + response_message)
|
||||
|
||||
writing_speed_per_word = 600 / 500 # 500 words in 600 seconds = 1.2 sec per word
|
||||
reading_speed_per_word = 400 / 500 # 500 words in 400 seconds = 0.8 sec per word
|
||||
|
||||
# Now prompt is a string (the last message), not a list of messages
|
||||
num_words_prompt = len(last_message.split())
|
||||
num_words_output = len(response_message.split())
|
||||
|
||||
prompt_time = num_words_prompt * writing_speed_per_word
|
||||
writing_time = num_words_output * writing_speed_per_word
|
||||
reading_time = num_words_output * reading_speed_per_word
|
||||
|
||||
total_time = writing_time - (prompt_time + reading_time)
|
||||
return total_time
|
||||
|
||||
Completions = CompletionTable()
|
||||
|
@ -383,3 +383,88 @@ async def get_total_chats(
|
||||
raise HTTPException(status_code=400, detail="Invalid date format. Use YYYY-MM-DD.")
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=500, detail=f"Error fetching chat stats: {e}")
|
||||
|
||||
|
||||
@router.get("/stats/saved-time-in-seconds")
|
||||
async def get_saved_time_in_seconds(
|
||||
start_date: str = Query(None, description="Start date in YYYY-MM-DD format (optional)"),
|
||||
end_date: str = Query(None, description="End date in YYYY-MM-DD format (optional)"),
|
||||
user=Depends(get_verified_user)
|
||||
):
|
||||
"""
|
||||
Returns total saved time in seconds for the last 12 months or within a specified time frame,
|
||||
filtered by the user's completions.
|
||||
"""
|
||||
try:
|
||||
current_date = datetime.now()
|
||||
one_year_ago = current_date.replace(day=1) - timedelta(days=365)
|
||||
|
||||
# Parse start_date
|
||||
if start_date:
|
||||
start_date_dt = datetime.strptime(start_date, "%Y-%m-%d")
|
||||
else:
|
||||
start_date_dt = one_year_ago # Default to one year ago
|
||||
|
||||
# Parse end_date
|
||||
if end_date:
|
||||
end_date_dt = datetime.strptime(end_date, "%Y-%m-%d")
|
||||
else:
|
||||
end_date_dt = current_date # Default to current date
|
||||
|
||||
if start_date_dt > end_date_dt:
|
||||
raise HTTPException(status_code=400, detail="Start date must be before end date.")
|
||||
|
||||
# Ensure end_date includes the entire day
|
||||
end_date_dt = end_date_dt.replace(hour=23, minute=59, second=59)
|
||||
|
||||
with get_db() as db:
|
||||
query = db.query(
|
||||
func.strftime('%Y-%m', func.datetime(Completion.created_at, 'unixepoch')).label("month"),
|
||||
func.sum(Completion.time_saved_in_seconds).label("total_saved_time")
|
||||
).filter(
|
||||
func.datetime(Completion.created_at, 'unixepoch') >= start_date_dt.strftime('%Y-%m-%d 00:00:00'),
|
||||
func.datetime(Completion.created_at, 'unixepoch') <= end_date_dt.strftime('%Y-%m-%d %H:%M:%S')
|
||||
)
|
||||
|
||||
query = query.filter(Completion.user_id == user.id)
|
||||
|
||||
# Execute the query and fetch results
|
||||
results = query.group_by("month").order_by("month").all()
|
||||
|
||||
# Convert results to a dictionary
|
||||
monthly_saved_time = {row[0]: int(row[1]) if row[1] is not None else 0 for row in results}
|
||||
|
||||
# Generate all months within the specified range
|
||||
months = []
|
||||
current_month = start_date_dt.replace(day=1)
|
||||
end_month = end_date_dt.replace(day=1)
|
||||
|
||||
while current_month <= end_month:
|
||||
months.append(current_month.strftime('%Y-%m'))
|
||||
# Move to the first day of next month
|
||||
if current_month.month == 12:
|
||||
current_month = current_month.replace(year=current_month.year + 1, month=1)
|
||||
else:
|
||||
current_month = current_month.replace(month=current_month.month + 1)
|
||||
|
||||
saved_time_data = {month: monthly_saved_time.get(month, 0) for month in months}
|
||||
|
||||
# Calculate percentage changes month-over-month
|
||||
percentage_changes = {}
|
||||
previous_value = None
|
||||
for month, value in saved_time_data.items():
|
||||
if previous_value is not None:
|
||||
change = ((value - previous_value) / previous_value) * 100 if previous_value != 0 else None
|
||||
percentage_changes[month] = round(change, 2) if change is not None else "N/A"
|
||||
else:
|
||||
percentage_changes[month] = "N/A"
|
||||
previous_value = value
|
||||
|
||||
return {
|
||||
"monthly_saved_time_in_seconds": saved_time_data,
|
||||
"percentage_changes": percentage_changes
|
||||
}
|
||||
except ValueError:
|
||||
raise HTTPException(status_code=400, detail="Invalid date format. Use YYYY-MM-DD.")
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=500, detail=f"Error fetching saved time stats: {e}")
|
||||
|
@ -22,6 +22,7 @@ from beyond_the_loop.models.companies import Companies
|
||||
from beyond_the_loop.models.companies import EIGHTY_PERCENT_CREDIT_LIMIT
|
||||
from beyond_the_loop.models.completions import Completions
|
||||
from beyond_the_loop.services.email_service import EmailService
|
||||
from beyond_the_loop.models.completions import calculate_saved_time_in_seconds
|
||||
|
||||
from open_webui.config import (
|
||||
CACHE_DIR,
|
||||
@ -564,47 +565,50 @@ async def generate_chat_completion(
|
||||
model_id = form_data.get("model")
|
||||
model_info = Models.get_model_by_id(model_id)
|
||||
|
||||
# Subtract credits from the company's balance (if possible)
|
||||
model_message_credit_cost = ModelMessageCreditCosts.get_cost_by_model(model_id)
|
||||
chat_id = metadata.get("chat_id", "no_chat_id")
|
||||
has_chat_id = "chat_id" in metadata and metadata["chat_id"] is not None
|
||||
|
||||
# Get current credit balance once
|
||||
current_balance = Companies.get_credit_balance(user.company_id)
|
||||
|
||||
# Check if company has sufficient credits
|
||||
if current_balance < model_message_credit_cost:
|
||||
email_service = EmailService()
|
||||
email_service.send_budget_mail_100(to_email=user.email, recipient_name=user.name)
|
||||
# Initialize the credit cost variable
|
||||
model_message_credit_cost = 0
|
||||
|
||||
raise HTTPException(
|
||||
status_code=402, # 402 Payment Required
|
||||
detail=f"Insufficient credits. This operation requires {model_message_credit_cost} credits.",
|
||||
)
|
||||
if has_chat_id:
|
||||
model_message_credit_cost = ModelMessageCreditCosts.get_cost_by_model(model_id)
|
||||
|
||||
# Check 80% threshold
|
||||
if current_balance - model_message_credit_cost < EIGHTY_PERCENT_CREDIT_LIMIT: # If balance is less than 125% of required (which means we're below 80%)
|
||||
email_service = EmailService()
|
||||
|
||||
should_send_budget_email_80 = True # Default to sending email
|
||||
# Get current credit balance
|
||||
current_balance = Companies.get_credit_balance(user.company_id)
|
||||
|
||||
if Companies.get_auto_recharge(user.company_id):
|
||||
try:
|
||||
# Trigger auto-recharge using the charge_customer endpoint
|
||||
await charge_customer(user)
|
||||
# Note: The webhook will handle adding the credits when payment succeeds
|
||||
should_send_budget_email_80 = False # Don't send email if auto-recharge succeeded
|
||||
except HTTPException as e:
|
||||
print(f"Auto-recharge failed: {str(e)}")
|
||||
except Exception as e:
|
||||
print(f"Unexpected error during auto-recharge: {str(e)}")
|
||||
# Check if company has sufficient credits
|
||||
if current_balance < model_message_credit_cost:
|
||||
email_service = EmailService()
|
||||
email_service.send_budget_mail_100(to_email=user.email, recipient_name=user.name)
|
||||
|
||||
if should_send_budget_email_80:
|
||||
email_service.send_budget_mail_80(to_email=user.email, recipient_name=user.name)
|
||||
raise HTTPException(
|
||||
status_code=402, # 402 Payment Required
|
||||
detail=f"Insufficient credits. This operation requires {model_message_credit_cost} credits.",
|
||||
)
|
||||
|
||||
# Subtract credits from balance
|
||||
Companies.subtract_credit_balance(user.company_id, model_message_credit_cost)
|
||||
# Check 80% threshold
|
||||
if current_balance - model_message_credit_cost < EIGHTY_PERCENT_CREDIT_LIMIT: # If balance is less than 125% of required (which means we're below 80%)
|
||||
email_service = EmailService()
|
||||
|
||||
# Add completion to completion table
|
||||
Completions.insert_new_completion(user.id, metadata["chat_id"], model_id, model_message_credit_cost)
|
||||
should_send_budget_email_80 = True # Default to sending email
|
||||
|
||||
if Companies.get_auto_recharge(user.company_id):
|
||||
try:
|
||||
# Trigger auto-recharge using the charge_customer endpoint
|
||||
await charge_customer(user)
|
||||
# Note: The webhook will handle adding the credits when payment succeeds
|
||||
should_send_budget_email_80 = False # Don't send email if auto-recharge succeeded
|
||||
except HTTPException as e:
|
||||
print(f"Auto-recharge failed: {str(e)}")
|
||||
except Exception as e:
|
||||
print(f"Unexpected error during auto-recharge: {str(e)}")
|
||||
|
||||
if should_send_budget_email_80:
|
||||
email_service.send_budget_mail_80(to_email=user.email, recipient_name=user.name)
|
||||
|
||||
# Subtract credits from balance
|
||||
Companies.subtract_credit_balance(user.company_id, model_message_credit_cost)
|
||||
|
||||
# Check model info and override the payload
|
||||
if model_info:
|
||||
@ -689,6 +693,11 @@ async def generate_chat_completion(
|
||||
streaming = False
|
||||
response = None
|
||||
|
||||
# Parse payload once for both streaming and non-streaming cases
|
||||
payload_dict = json.loads(payload)
|
||||
last_user_message = next((msg['content'] for msg in reversed(payload_dict['messages'])
|
||||
if msg['role'] == 'user'), '')
|
||||
|
||||
try:
|
||||
session = aiohttp.ClientSession(
|
||||
trust_env=True, timeout=aiohttp.ClientTimeout(total=AIOHTTP_CLIENT_TIMEOUT)
|
||||
@ -725,8 +734,28 @@ async def generate_chat_completion(
|
||||
# Check if response is SSE
|
||||
if "text/event-stream" in r.headers.get("Content-Type", ""):
|
||||
streaming = True
|
||||
|
||||
async def insert_completion_if_streaming_is_done():
|
||||
full_response = ""
|
||||
async for chunk in r.content:
|
||||
chunk_str = chunk.decode()
|
||||
if chunk_str.startswith('data: '):
|
||||
try:
|
||||
data = json.loads(chunk_str[6:])
|
||||
delta = data.get('choices', [{}])[0].get('delta', {}).get('content', '')
|
||||
if delta: # Only use it if there's actual content
|
||||
full_response += delta
|
||||
elif data.get('choices', [{}])[0].get('finish_reason'):
|
||||
# End of stream
|
||||
# Add completion to completion table if it's a chat message from the user
|
||||
if has_chat_id:
|
||||
Completions.insert_new_completion(user.id, metadata["chat_id"], model_id, model_message_credit_cost, calculate_saved_time_in_seconds(last_user_message, full_response))
|
||||
except json.JSONDecodeError:
|
||||
print(f"\n{chunk_str}")
|
||||
yield chunk
|
||||
|
||||
return StreamingResponse(
|
||||
r.content,
|
||||
insert_completion_if_streaming_is_done(),
|
||||
status_code=r.status,
|
||||
headers=dict(r.headers),
|
||||
background=BackgroundTask(
|
||||
@ -741,6 +770,13 @@ async def generate_chat_completion(
|
||||
response = await r.text()
|
||||
|
||||
r.raise_for_status()
|
||||
|
||||
if has_chat_id:
|
||||
# Add completion to completion table
|
||||
response_content = response.get('choices', [{}])[0].get('message', {}).get('content', '')
|
||||
|
||||
Completions.insert_new_completion(user.id, metadata["chat_id"], model_id, model_message_credit_cost, calculate_saved_time_in_seconds(last_user_message, response_content))
|
||||
|
||||
return response
|
||||
except Exception as e:
|
||||
log.exception(e)
|
||||
@ -826,7 +862,7 @@ async def proxy(path: str, request: Request, user=Depends(get_verified_user)):
|
||||
res = await r.json()
|
||||
print(res)
|
||||
if "error" in res:
|
||||
detail = f"External: {res['error']['message'] if 'message' in res['error'] else res['error']}"
|
||||
detail = f"External Error: {res['error']['message'] if 'message' in res['error'] else res['error']}"
|
||||
except Exception:
|
||||
detail = f"External: {e}"
|
||||
raise HTTPException(
|
||||
|
@ -839,7 +839,7 @@ async def chat_completion(
|
||||
user=Depends(get_verified_user),
|
||||
):
|
||||
if not request.app.state.MODELS:
|
||||
await get_all_models(request)
|
||||
await get_all_models(request, user)
|
||||
|
||||
tasks = form_data.pop("background_tasks", None)
|
||||
try:
|
||||
|
Loading…
Reference in New Issue
Block a user