diff --git a/backend/ee/onyx/db/analytics.py b/backend/ee/onyx/db/analytics.py index b19524f7e96..5e525fa624d 100644 --- a/backend/ee/onyx/db/analytics.py +++ b/backend/ee/onyx/db/analytics.py @@ -2,6 +2,7 @@ from collections.abc import Sequence from uuid import UUID +from sqlalchemy import and_ from sqlalchemy import case from sqlalchemy import cast from sqlalchemy import Date @@ -14,6 +15,9 @@ from onyx.db.models import ChatMessage from onyx.db.models import ChatMessageFeedback from onyx.db.models import ChatSession +from onyx.db.models import Persona +from onyx.db.models import User +from onyx.db.models import UserRole def fetch_query_analytics( @@ -234,3 +238,121 @@ def fetch_persona_unique_users( ) return [tuple(row) for row in db_session.execute(query).all()] + + +def fetch_assistant_message_analytics( + db_session: Session, + assistant_id: int, + start: datetime.datetime, + end: datetime.datetime, +) -> list[tuple[int, datetime.date]]: + """ + Gets the daily message counts for a specific assistant in the given time range. + """ + query = ( + select( + func.count(ChatMessage.id), + cast(ChatMessage.time_sent, Date), + ) + .join( + ChatSession, + ChatMessage.chat_session_id == ChatSession.id, + ) + .where( + or_( + ChatMessage.alternate_assistant_id == assistant_id, + ChatSession.persona_id == assistant_id, + ), + ChatMessage.time_sent >= start, + ChatMessage.time_sent <= end, + ChatMessage.message_type == MessageType.ASSISTANT, + ) + .group_by(cast(ChatMessage.time_sent, Date)) + .order_by(cast(ChatMessage.time_sent, Date)) + ) + + return [tuple(row) for row in db_session.execute(query).all()] + + +def fetch_assistant_unique_users( + db_session: Session, + assistant_id: int, + start: datetime.datetime, + end: datetime.datetime, +) -> list[tuple[int, datetime.date]]: + """ + Gets the daily unique user counts for a specific assistant in the given time range. + """ + query = ( + select( + func.count(func.distinct(ChatSession.user_id)), + cast(ChatMessage.time_sent, Date), + ) + .join( + ChatSession, + ChatMessage.chat_session_id == ChatSession.id, + ) + .where( + or_( + ChatMessage.alternate_assistant_id == assistant_id, + ChatSession.persona_id == assistant_id, + ), + ChatMessage.time_sent >= start, + ChatMessage.time_sent <= end, + ChatMessage.message_type == MessageType.ASSISTANT, + ) + .group_by(cast(ChatMessage.time_sent, Date)) + .order_by(cast(ChatMessage.time_sent, Date)) + ) + + return [tuple(row) for row in db_session.execute(query).all()] + + +def fetch_assistant_unique_users_total( + db_session: Session, + assistant_id: int, + start: datetime.datetime, + end: datetime.datetime, +) -> int: + """ + Gets the total number of distinct users who have sent or received messages from + the specified assistant in the given time range. + """ + query = ( + select(func.count(func.distinct(ChatSession.user_id))) + .select_from(ChatMessage) + .join( + ChatSession, + ChatMessage.chat_session_id == ChatSession.id, + ) + .where( + or_( + ChatMessage.alternate_assistant_id == assistant_id, + ChatSession.persona_id == assistant_id, + ), + ChatMessage.time_sent >= start, + ChatMessage.time_sent <= end, + ChatMessage.message_type == MessageType.ASSISTANT, + ) + ) + + result = db_session.execute(query).scalar() + return result if result else 0 + + +# Users can view assistant stats if they created the persona, +# or if they are an admin +def user_can_view_assistant_stats( + db_session: Session, user: User | None, assistant_id: int +) -> bool: + # If user is None, assume the user is an admin or auth is disabled + if user is None or user.role == UserRole.ADMIN: + return True + + # Check if the user created the persona + stmt = select(Persona).where( + and_(Persona.id == assistant_id, Persona.user_id == user.id) + ) + + persona = db_session.execute(stmt).scalar_one_or_none() + return persona is not None diff --git a/backend/ee/onyx/server/analytics/api.py b/backend/ee/onyx/server/analytics/api.py index 374bcb10dba..46f458c1748 100644 --- a/backend/ee/onyx/server/analytics/api.py +++ b/backend/ee/onyx/server/analytics/api.py @@ -1,17 +1,24 @@ import datetime from collections import defaultdict +from typing import List from fastapi import APIRouter from fastapi import Depends +from fastapi import HTTPException from pydantic import BaseModel from sqlalchemy.orm import Session +from ee.onyx.db.analytics import fetch_assistant_message_analytics +from ee.onyx.db.analytics import fetch_assistant_unique_users +from ee.onyx.db.analytics import fetch_assistant_unique_users_total from ee.onyx.db.analytics import fetch_onyxbot_analytics from ee.onyx.db.analytics import fetch_per_user_query_analytics from ee.onyx.db.analytics import fetch_persona_message_analytics from ee.onyx.db.analytics import fetch_persona_unique_users from ee.onyx.db.analytics import fetch_query_analytics +from ee.onyx.db.analytics import user_can_view_assistant_stats from onyx.auth.users import current_admin_user +from onyx.auth.users import current_user from onyx.db.engine import get_session from onyx.db.models import User @@ -191,3 +198,74 @@ def get_persona_unique_users( ) ) return unique_user_counts + + +class AssistantDailyUsageResponse(BaseModel): + date: datetime.date + total_messages: int + total_unique_users: int + + +class AssistantStatsResponse(BaseModel): + daily_stats: List[AssistantDailyUsageResponse] + total_messages: int + total_unique_users: int + + +@router.get("/assistant/{assistant_id}/stats") +def get_assistant_stats( + assistant_id: int, + start: datetime.datetime | None = None, + end: datetime.datetime | None = None, + user: User | None = Depends(current_user), + db_session: Session = Depends(get_session), +) -> AssistantStatsResponse: + """ + Returns daily message and unique user counts for a user's assistant, + along with the overall total messages and total distinct users. + """ + start = start or ( + datetime.datetime.utcnow() - datetime.timedelta(days=_DEFAULT_LOOKBACK_DAYS) + ) + end = end or datetime.datetime.utcnow() + + if not user_can_view_assistant_stats(db_session, user, assistant_id): + raise HTTPException( + status_code=403, detail="Not allowed to access this assistant's stats." + ) + + # Pull daily usage from the DB calls + messages_data = fetch_assistant_message_analytics( + db_session, assistant_id, start, end + ) + unique_users_data = fetch_assistant_unique_users( + db_session, assistant_id, start, end + ) + + # Map each day => (messages, unique_users). + daily_messages_map = {date: count for count, date in messages_data} + daily_unique_users_map = {date: count for count, date in unique_users_data} + all_dates = set(daily_messages_map.keys()) | set(daily_unique_users_map.keys()) + + # Merge both sets of metrics by date + daily_results: list[AssistantDailyUsageResponse] = [] + for date in sorted(all_dates): + daily_results.append( + AssistantDailyUsageResponse( + date=date, + total_messages=daily_messages_map.get(date, 0), + total_unique_users=daily_unique_users_map.get(date, 0), + ) + ) + + # Now pull a single total distinct user count across the entire time range + total_msgs = sum(d.total_messages for d in daily_results) + total_users = fetch_assistant_unique_users_total( + db_session, assistant_id, start, end + ) + + return AssistantStatsResponse( + daily_stats=daily_results, + total_messages=total_msgs, + total_unique_users=total_users, + ) diff --git a/backend/onyx/db/persona.py b/backend/onyx/db/persona.py index c20829d83fc..d092a4c3275 100644 --- a/backend/onyx/db/persona.py +++ b/backend/onyx/db/persona.py @@ -99,6 +99,9 @@ def _add_user_filters( return stmt.where(where_clause) +# fetch_persona_by_id is used to fetch a persona by its ID. It is used to fetch a persona by its ID. + + def fetch_persona_by_id( db_session: Session, persona_id: int, user: User | None, get_editable: bool = True ) -> Persona: diff --git a/web/src/app/assistants/mine/AssistantsList.tsx b/web/src/app/assistants/mine/AssistantsList.tsx index 0c9bb50ca28..b9a9760ba3d 100644 --- a/web/src/app/assistants/mine/AssistantsList.tsx +++ b/web/src/app/assistants/mine/AssistantsList.tsx @@ -6,6 +6,7 @@ import { Persona } from "@/app/admin/assistants/interfaces"; import { Button } from "@/components/ui/button"; import { Separator } from "@/components/ui/separator"; import { + FiBarChart, FiEdit2, FiList, FiMinus, @@ -59,6 +60,7 @@ import { MakePublicAssistantModal } from "@/app/chat/modal/MakePublicAssistantMo import { CustomTooltip } from "@/components/tooltip/CustomTooltip"; import { useAssistants } from "@/components/context/AssistantsContext"; import { useUser } from "@/components/user/UserProvider"; +import { usePaidEnterpriseFeaturesEnabled } from "@/components/settings/usePaidEnterpriseFeaturesEnabled"; function DraggableAssistantListItem({ ...props }: any) { const { @@ -116,7 +118,9 @@ function AssistantListItem({ const router = useRouter(); const [showSharingModal, setShowSharingModal] = useState(false); + const isEnterpriseEnabled = usePaidEnterpriseFeaturesEnabled(); const isOwnedByUser = checkUserOwnsAssistant(user, assistant); + const { isAdmin } = useUser(); return ( <> @@ -243,6 +247,18 @@ function AssistantListItem({ Add ), + + (isOwnedByUser || isAdmin) && isEnterpriseEnabled ? ( + + ) : null, isOwnedByUser ? (