From 150aedd58c3c435cec6a9085bc884d7e0c391053 Mon Sep 17 00:00:00 2001 From: hagen-danswer Date: Wed, 27 Nov 2024 16:24:32 -0800 Subject: [PATCH 1/5] Added a chart to display persona message stats --- .../tasks/external_group_syncing/tasks.py | 4 +- backend/ee/danswer/db/analytics.py | 32 ++ backend/ee/danswer/server/analytics/api.py | 42 +++ web/src/app/ee/admin/performance/lib.ts | 50 ++++ .../usage/PersonaMessagesChart.tsx | 279 ++++++++++++++++++ .../app/ee/admin/performance/usage/page.tsx | 2 + 6 files changed, 408 insertions(+), 1 deletion(-) create mode 100644 web/src/app/ee/admin/performance/usage/PersonaMessagesChart.tsx diff --git a/backend/danswer/background/celery/tasks/external_group_syncing/tasks.py b/backend/danswer/background/celery/tasks/external_group_syncing/tasks.py index 8381656ee17..8ec10052192 100644 --- a/backend/danswer/background/celery/tasks/external_group_syncing/tasks.py +++ b/backend/danswer/background/celery/tasks/external_group_syncing/tasks.py @@ -230,7 +230,9 @@ def connector_external_group_sync_generator_task( if ext_group_sync_func is None: raise ValueError(f"No external group sync func found for {source_type}") - logger.info(f"Syncing docs for {source_type}") + logger.info( + f"Syncing external groups for {source_type} with cc_pair={cc_pair_id}" + ) external_user_groups: list[ExternalUserGroup] = ext_group_sync_func(cc_pair) diff --git a/backend/ee/danswer/db/analytics.py b/backend/ee/danswer/db/analytics.py index e0eff7850e4..e99ef1e25e3 100644 --- a/backend/ee/danswer/db/analytics.py +++ b/backend/ee/danswer/db/analytics.py @@ -170,3 +170,35 @@ def fetch_danswerbot_analytics( ) return results + + +def fetch_persona_message_analytics( + db_session: Session, + persona_id: int, + start: datetime.datetime, + end: datetime.datetime, +) -> list[tuple[int, datetime.date]]: + """Gets the daily message counts for a specific persona within 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 == persona_id, + ChatSession.persona_id == persona_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 list(db_session.execute(query).all()) # type: ignore diff --git a/backend/ee/danswer/server/analytics/api.py b/backend/ee/danswer/server/analytics/api.py index f79199323f5..8b8742bf9e0 100644 --- a/backend/ee/danswer/server/analytics/api.py +++ b/backend/ee/danswer/server/analytics/api.py @@ -3,6 +3,7 @@ from fastapi import APIRouter from fastapi import Depends +from fastapi import Query from pydantic import BaseModel from sqlalchemy.orm import Session @@ -11,6 +12,7 @@ from danswer.db.models import User from ee.danswer.db.analytics import fetch_danswerbot_analytics from ee.danswer.db.analytics import fetch_per_user_query_analytics +from ee.danswer.db.analytics import fetch_persona_message_analytics from ee.danswer.db.analytics import fetch_query_analytics router = APIRouter(prefix="/analytics") @@ -115,3 +117,43 @@ def get_danswerbot_analytics( ] return resolution_results + + +class PersonaMessageAnalyticsResponse(BaseModel): + total_messages: int + date: datetime.date + persona_id: int + + +@router.get("/admin/persona/messages") +def get_persona_messages( + persona_ids: str = Query(...), # ... means this parameter is required + start: datetime.datetime | None = None, + end: datetime.datetime | None = None, + _: User | None = Depends(current_admin_user), + db_session: Session = Depends(get_session), +) -> list[PersonaMessageAnalyticsResponse]: + """Fetch daily message counts for multiple personas within the given time range.""" + # Convert comma-separated string to list of integers + parsed_persona_ids = [ + int(id.strip()) for id in persona_ids.split(",") if id.strip() + ] + persona_message_counts = [] + start = start or (datetime.datetime.utcnow() - datetime.timedelta(days=30)) + end = end or datetime.datetime.utcnow() + for persona_id in parsed_persona_ids: + for count, date in fetch_persona_message_analytics( + db_session=db_session, + persona_id=int(persona_id), + start=start, + end=end, + ): + persona_message_counts.append( + PersonaMessageAnalyticsResponse( + total_messages=count, + date=date, + persona_id=persona_id, + ) + ) + + return persona_message_counts diff --git a/web/src/app/ee/admin/performance/lib.ts b/web/src/app/ee/admin/performance/lib.ts index 0837df1dea0..b0eb30ad942 100644 --- a/web/src/app/ee/admin/performance/lib.ts +++ b/web/src/app/ee/admin/performance/lib.ts @@ -97,3 +97,53 @@ export function getDatesList(startDate: Date): string[] { return datesList; } + +export interface PersonaMessageAnalytics { + total_messages: number; + date: string; + persona_id: number; +} + +export interface PersonaSnapshot { + id: number; + name: string; + description: string; + is_visible: boolean; + is_public: boolean; +} + +export const usePersonaList = () => { + const { data, error, isLoading } = useSWR( + "/api/persona", + errorHandlingFetcher + ); + + return { + data, + error, + isLoading, + }; +}; + +export const usePersonaMessages = ( + personaIds: number[], + timeRange: DateRangePickerValue +) => { + const url = buildApiPath(`/api/analytics/admin/persona/messages`, { + persona_ids: personaIds.join(","), + start: convertDateToStartOfDay(timeRange.from)?.toISOString(), + end: convertDateToEndOfDay(timeRange.to)?.toISOString(), + }); + + const { data, error, isLoading } = useSWR( + personaIds.length > 0 ? url : null, + errorHandlingFetcher + ); + + return { + data, + error, + isLoading, + refreshPersonaMessages: () => mutate(url), + }; +}; diff --git a/web/src/app/ee/admin/performance/usage/PersonaMessagesChart.tsx b/web/src/app/ee/admin/performance/usage/PersonaMessagesChart.tsx new file mode 100644 index 00000000000..9766efea6dc --- /dev/null +++ b/web/src/app/ee/admin/performance/usage/PersonaMessagesChart.tsx @@ -0,0 +1,279 @@ +import { ThreeDotsLoader } from "@/components/Loading"; +import { getDatesList, usePersonaList, usePersonaMessages } from "../lib"; +import { DateRangePickerValue } from "@/app/ee/admin/performance/DateRangeSelector"; +import Text from "@/components/ui/text"; +import Title from "@/components/ui/title"; +import CardSection from "@/components/admin/CardSection"; +import { AreaChartDisplay } from "@/components/ui/areaChart"; +import { Badge } from "@/components/ui/badge"; +import { X, Search } from "lucide-react"; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "@/components/ui/select"; +import { useState, useMemo } from "react"; +import { cn } from "@/lib/utils"; + +export function PersonaMessagesChart({ + timeRange, +}: { + timeRange: DateRangePickerValue; +}) { + const [selectedPersonaIds, setSelectedPersonaIds] = useState([]); + const [isOpen, setIsOpen] = useState(false); + const { + data: personaList, + isLoading: isPersonaListLoading, + error: personaListError, + } = usePersonaList(); + + const { + data: personaMessagesData, + isLoading: isPersonaMessagesLoading, + error: personaMessagesError, + } = usePersonaMessages(selectedPersonaIds, timeRange); + + const isLoading = isPersonaListLoading || isPersonaMessagesLoading; + const hasError = personaListError || personaMessagesError; + + const colors = useMemo( + () => [ + "#10B981", + "#3B82F6", + "#9333EA", + "#F59E0B", + "#F43F5E", + "#6366F1", + "#06B6D4", + "#EC4899", + ], + [] + ); + + const getPersonaColor = useMemo( + () => (index: number) => colors[index % colors.length], + [colors] + ); + + // Define color classes for badges + const colorClasses = useMemo( + () => ({ + "#10B981": "bg-emerald-100 hover:bg-emerald-200 text-emerald-700", + "#3B82F6": "bg-blue-100 hover:bg-blue-200 text-blue-700", + "#9333EA": "bg-purple-100 hover:bg-purple-200 text-purple-700", + "#F59E0B": "bg-amber-100 hover:bg-amber-200 text-amber-700", + "#F43F5E": "bg-rose-100 hover:bg-rose-200 text-rose-700", + "#6366F1": "bg-indigo-100 hover:bg-indigo-200 text-indigo-700", + "#06B6D4": "bg-cyan-100 hover:bg-cyan-200 text-cyan-700", + "#EC4899": "bg-pink-100 hover:bg-pink-200 text-pink-700", + }), + [] + ); + + const chartData = useMemo(() => { + if ( + !personaMessagesData?.length || + !personaList || + selectedPersonaIds.length === 0 + ) { + return null; + } + + const initialDate = + timeRange.from || + new Date( + Math.min( + ...personaMessagesData.map((entry) => new Date(entry.date).getTime()) + ) + ); + const dateRange = getDatesList(initialDate); + + // Create a map for each persona's data + const personaDataMaps = selectedPersonaIds.map((personaId) => { + const personaData = personaMessagesData.filter( + (d) => d.persona_id === personaId + ); + return { + personaId, + dataMap: new Map(personaData.map((entry) => [entry.date, entry])), + }; + }); + + return dateRange.map((dateStr) => { + const dataPoint: any = { Day: dateStr }; + personaDataMaps.forEach(({ personaId, dataMap }) => { + const persona = personaList.find((p) => p.id === personaId); + const messageData = dataMap.get(dateStr); + dataPoint[persona?.name || `Persona ${personaId}`] = + messageData?.total_messages || 0; + }); + return dataPoint; + }); + }, [personaMessagesData, timeRange.from]); + + const categories = useMemo( + () => + selectedPersonaIds.map( + (id) => personaList?.find((p) => p.id === id)?.name || `Persona ${id}` + ), + [selectedPersonaIds, personaList] + ); + + let content; + if (isLoading) { + content = ( +
+ +
+ ); + } else if (!personaList || hasError) { + content = ( +
+

Failed to fetch data...

+
+ ); + } else if (selectedPersonaIds.length === 0) { + content = ( +
+

Select personas to view message analytics

+
+ ); + } else if (!personaMessagesData?.length) { + content = ( +
+

+ No messages found for selected personas in the selected time range +

+
+ ); + } else if (chartData) { + content = ( + getPersonaColor(i))} + yAxisWidth={60} + /> + ); + } + + const selectedPersonas = + personaList?.filter((p) => selectedPersonaIds.includes(p.id)) || []; + + return ( + + Persona Messages +
+ Messages per day for selected personas +
+ e.stopPropagation()} + onKeyDown={(e) => e.stopPropagation()} + onFocus={(e) => e.stopPropagation()} + onChange={(e) => { + e.preventDefault(); + e.stopPropagation(); + const input = e.target.value.toLowerCase(); + const items = document.querySelectorAll('[role="option"]'); + items.forEach((item) => { + const text = item.textContent?.toLowerCase() || ""; + (item as HTMLElement).style.display = text.includes(input) + ? "" + : "none"; + }); + }} + /> +
+ {personaList?.map((persona) => ( + + {persona.name} + + ))} + + +
+ + {selectedPersonas.length > 0 && ( +
+ {selectedPersonas.map((persona) => { + const index = selectedPersonaIds.indexOf(persona.id); + return ( + + {persona.name} + + setSelectedPersonaIds( + selectedPersonaIds.filter((id) => id !== persona.id) + ) + } + /> + + ); + })} +
+ )} + + {content} +
+ ); +} diff --git a/web/src/app/ee/admin/performance/usage/page.tsx b/web/src/app/ee/admin/performance/usage/page.tsx index e1fffc323a2..967f16a377e 100644 --- a/web/src/app/ee/admin/performance/usage/page.tsx +++ b/web/src/app/ee/admin/performance/usage/page.tsx @@ -4,6 +4,7 @@ import { DateRangeSelector } from "../DateRangeSelector"; import { DanswerBotChart } from "./DanswerBotChart"; import { FeedbackChart } from "./FeedbackChart"; import { QueryPerformanceChart } from "./QueryPerformanceChart"; +import { PersonaMessagesChart } from "./PersonaMessagesChart"; import { useTimeRange } from "../lib"; import { AdminPageTitle } from "@/components/admin/Title"; import { FiActivity } from "react-icons/fi"; @@ -26,6 +27,7 @@ export default function AnalyticsPage() { + From d02296c5b930b40967b888ee5118be8262087b89 Mon Sep 17 00:00:00 2001 From: hagen-danswer Date: Wed, 27 Nov 2024 17:40:52 -0800 Subject: [PATCH 2/5] polish --- backend/ee/danswer/db/analytics.py | 32 ++ backend/ee/danswer/server/analytics/api.py | 31 ++ web/src/app/ee/admin/performance/lib.ts | 29 ++ .../usage/PersonaMessagesChart.tsx | 282 ++++++++---------- 4 files changed, 216 insertions(+), 158 deletions(-) diff --git a/backend/ee/danswer/db/analytics.py b/backend/ee/danswer/db/analytics.py index e99ef1e25e3..1b068099153 100644 --- a/backend/ee/danswer/db/analytics.py +++ b/backend/ee/danswer/db/analytics.py @@ -202,3 +202,35 @@ def fetch_persona_message_analytics( ) return list(db_session.execute(query).all()) # type: ignore + + +def fetch_persona_unique_users( + db_session: Session, + persona_id: int, + start: datetime.datetime, + end: datetime.datetime, +) -> list[tuple[int, datetime.date]]: + """Gets the daily unique user counts for a specific persona within 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 == persona_id, + ChatSession.persona_id == persona_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 list(db_session.execute(query).all()) # type: ignore diff --git a/backend/ee/danswer/server/analytics/api.py b/backend/ee/danswer/server/analytics/api.py index 8b8742bf9e0..382df90f6ad 100644 --- a/backend/ee/danswer/server/analytics/api.py +++ b/backend/ee/danswer/server/analytics/api.py @@ -1,5 +1,6 @@ import datetime from collections import defaultdict +from typing import Any from fastapi import APIRouter from fastapi import Depends @@ -13,6 +14,7 @@ from ee.danswer.db.analytics import fetch_danswerbot_analytics from ee.danswer.db.analytics import fetch_per_user_query_analytics from ee.danswer.db.analytics import fetch_persona_message_analytics +from ee.danswer.db.analytics import fetch_persona_unique_users from ee.danswer.db.analytics import fetch_query_analytics router = APIRouter(prefix="/analytics") @@ -157,3 +159,32 @@ def get_persona_messages( ) return persona_message_counts + + +@router.get("/admin/persona/unique-users") +def get_persona_unique_users( + persona_ids: str, + start: datetime.datetime, + end: datetime.datetime, + _: User | None = Depends(current_admin_user), + db_session: Session = Depends(get_session), +) -> list[dict[str, Any]]: + """Get unique users per day for each persona.""" + persona_id_list = [int(pid) for pid in persona_ids.split(",")] + results = [] + for persona_id in persona_id_list: + daily_counts = fetch_persona_unique_users( + db_session=db_session, + persona_id=persona_id, + start=start, + end=end, + ) + for count, date in daily_counts: + results.append( + { + "unique_users": count, + "date": date.isoformat(), + "persona_id": persona_id, + } + ) + return results diff --git a/web/src/app/ee/admin/performance/lib.ts b/web/src/app/ee/admin/performance/lib.ts index b0eb30ad942..951a8e34267 100644 --- a/web/src/app/ee/admin/performance/lib.ts +++ b/web/src/app/ee/admin/performance/lib.ts @@ -147,3 +147,32 @@ export const usePersonaMessages = ( refreshPersonaMessages: () => mutate(url), }; }; + +export interface PersonaUniqueUserAnalytics { + unique_users: number; + date: string; + persona_id: number; +} + +export const usePersonaUniqueUsers = ( + personaIds: number[], + timeRange: DateRangePickerValue +) => { + const url = buildApiPath(`/api/analytics/admin/persona/unique-users`, { + persona_ids: personaIds.join(","), + start: convertDateToStartOfDay(timeRange.from)?.toISOString(), + end: convertDateToEndOfDay(timeRange.to)?.toISOString(), + }); + + const { data, error, isLoading } = useSWR( + personaIds.length > 0 ? url : null, + errorHandlingFetcher + ); + + return { + data, + error, + isLoading, + refreshPersonaUniqueUsers: () => mutate(url), + }; +}; diff --git a/web/src/app/ee/admin/performance/usage/PersonaMessagesChart.tsx b/web/src/app/ee/admin/performance/usage/PersonaMessagesChart.tsx index 9766efea6dc..668404c6998 100644 --- a/web/src/app/ee/admin/performance/usage/PersonaMessagesChart.tsx +++ b/web/src/app/ee/admin/performance/usage/PersonaMessagesChart.tsx @@ -1,12 +1,16 @@ import { ThreeDotsLoader } from "@/components/Loading"; -import { getDatesList, usePersonaList, usePersonaMessages } from "../lib"; +import { X, Search } from "lucide-react"; +import { + getDatesList, + usePersonaList, + usePersonaMessages, + usePersonaUniqueUsers, +} from "../lib"; import { DateRangePickerValue } from "@/app/ee/admin/performance/DateRangeSelector"; import Text from "@/components/ui/text"; import Title from "@/components/ui/title"; import CardSection from "@/components/admin/CardSection"; import { AreaChartDisplay } from "@/components/ui/areaChart"; -import { Badge } from "@/components/ui/badge"; -import { X, Search } from "lucide-react"; import { Select, SelectContent, @@ -14,16 +18,18 @@ import { SelectTrigger, SelectValue, } from "@/components/ui/select"; -import { useState, useMemo } from "react"; -import { cn } from "@/lib/utils"; +import { useState, useMemo, useEffect } from "react"; export function PersonaMessagesChart({ timeRange, }: { timeRange: DateRangePickerValue; }) { - const [selectedPersonaIds, setSelectedPersonaIds] = useState([]); - const [isOpen, setIsOpen] = useState(false); + const [selectedPersonaId, setSelectedPersonaId] = useState< + number | undefined + >(undefined); + const [searchQuery, setSearchQuery] = useState(""); + const [highlightedIndex, setHighlightedIndex] = useState(-1); const { data: personaList, isLoading: isPersonaListLoading, @@ -34,50 +40,75 @@ export function PersonaMessagesChart({ data: personaMessagesData, isLoading: isPersonaMessagesLoading, error: personaMessagesError, - } = usePersonaMessages(selectedPersonaIds, timeRange); - - const isLoading = isPersonaListLoading || isPersonaMessagesLoading; - const hasError = personaListError || personaMessagesError; - - const colors = useMemo( - () => [ - "#10B981", - "#3B82F6", - "#9333EA", - "#F59E0B", - "#F43F5E", - "#6366F1", - "#06B6D4", - "#EC4899", - ], - [] + } = usePersonaMessages( + selectedPersonaId !== undefined ? [selectedPersonaId] : [], + timeRange ); - const getPersonaColor = useMemo( - () => (index: number) => colors[index % colors.length], - [colors] + const { + data: personaUniqueUsersData, + isLoading: isPersonaUniqueUsersLoading, + error: personaUniqueUsersError, + } = usePersonaUniqueUsers( + selectedPersonaId !== undefined ? [selectedPersonaId] : [], + timeRange ); - // Define color classes for badges - const colorClasses = useMemo( - () => ({ - "#10B981": "bg-emerald-100 hover:bg-emerald-200 text-emerald-700", - "#3B82F6": "bg-blue-100 hover:bg-blue-200 text-blue-700", - "#9333EA": "bg-purple-100 hover:bg-purple-200 text-purple-700", - "#F59E0B": "bg-amber-100 hover:bg-amber-200 text-amber-700", - "#F43F5E": "bg-rose-100 hover:bg-rose-200 text-rose-700", - "#6366F1": "bg-indigo-100 hover:bg-indigo-200 text-indigo-700", - "#06B6D4": "bg-cyan-100 hover:bg-cyan-200 text-cyan-700", - "#EC4899": "bg-pink-100 hover:bg-pink-200 text-pink-700", - }), - [] - ); + const isLoading = + isPersonaListLoading || + isPersonaMessagesLoading || + isPersonaUniqueUsersLoading; + const hasError = + personaListError || personaMessagesError || personaUniqueUsersError; + + const filteredPersonaList = useMemo(() => { + if (!personaList) return []; + return personaList.filter((persona) => + persona.name.toLowerCase().includes(searchQuery.toLowerCase()) + ); + }, [personaList, searchQuery]); + + const handleKeyDown = (e: React.KeyboardEvent) => { + e.stopPropagation(); + + switch (e.key) { + case "ArrowDown": + e.preventDefault(); + setHighlightedIndex((prev) => + prev < filteredPersonaList.length - 1 ? prev + 1 : prev + ); + break; + case "ArrowUp": + e.preventDefault(); + setHighlightedIndex((prev) => (prev > 0 ? prev - 1 : prev)); + break; + case "Enter": + if ( + highlightedIndex >= 0 && + highlightedIndex < filteredPersonaList.length + ) { + setSelectedPersonaId(filteredPersonaList[highlightedIndex].id); + setSearchQuery(""); + setHighlightedIndex(-1); + } + break; + case "Escape": + setSearchQuery(""); + setHighlightedIndex(-1); + break; + } + }; + + // Reset highlight when search query changes + useEffect(() => { + setHighlightedIndex(-1); + }, [searchQuery]); const chartData = useMemo(() => { if ( !personaMessagesData?.length || - !personaList || - selectedPersonaIds.length === 0 + !personaUniqueUsersData?.length || + selectedPersonaId === undefined ) { return null; } @@ -91,36 +122,29 @@ export function PersonaMessagesChart({ ); const dateRange = getDatesList(initialDate); - // Create a map for each persona's data - const personaDataMaps = selectedPersonaIds.map((personaId) => { - const personaData = personaMessagesData.filter( - (d) => d.persona_id === personaId - ); - return { - personaId, - dataMap: new Map(personaData.map((entry) => [entry.date, entry])), - }; - }); + // Create maps for messages and unique users data + const messagesMap = new Map( + personaMessagesData.map((entry) => [entry.date, entry]) + ); + const uniqueUsersMap = new Map( + personaUniqueUsersData.map((entry) => [entry.date, entry]) + ); return dateRange.map((dateStr) => { - const dataPoint: any = { Day: dateStr }; - personaDataMaps.forEach(({ personaId, dataMap }) => { - const persona = personaList.find((p) => p.id === personaId); - const messageData = dataMap.get(dateStr); - dataPoint[persona?.name || `Persona ${personaId}`] = - messageData?.total_messages || 0; - }); - return dataPoint; + const messageData = messagesMap.get(dateStr); + const uniqueUserData = uniqueUsersMap.get(dateStr); + return { + Day: dateStr, + Messages: messageData?.total_messages || 0, + "Unique Users": uniqueUserData?.unique_users || 0, + }; }); - }, [personaMessagesData, timeRange.from]); - - const categories = useMemo( - () => - selectedPersonaIds.map( - (id) => personaList?.find((p) => p.id === id)?.name || `Persona ${id}` - ), - [selectedPersonaIds, personaList] - ); + }, [ + personaMessagesData, + personaUniqueUsersData, + timeRange.from, + selectedPersonaId, + ]); let content; if (isLoading) { @@ -135,17 +159,17 @@ export function PersonaMessagesChart({

Failed to fetch data...

); - } else if (selectedPersonaIds.length === 0) { + } else if (selectedPersonaId === undefined) { content = (
-

Select personas to view message analytics

+

Select a persona to view analytics

); } else if (!personaMessagesData?.length) { content = (

- No messages found for selected personas in the selected time range + No data found for selected persona in the selected time range

); @@ -154,87 +178,59 @@ export function PersonaMessagesChart({ getPersonaColor(i))} + colors={["indigo", "fuchsia"]} yAxisWidth={60} /> ); } - const selectedPersonas = - personaList?.filter((p) => selectedPersonaIds.includes(p.id)) || []; + const selectedPersona = personaList?.find((p) => p.id === selectedPersonaId); return ( - Persona Messages + Persona Analytics
- Messages per day for selected personas + Messages and unique users per day for selected persona
setSearchQuery(e.target.value)} + onClick={(e) => e.stopPropagation()} onMouseDown={(e) => e.stopPropagation()} - onKeyDown={(e) => e.stopPropagation()} - onFocus={(e) => e.stopPropagation()} - onChange={(e) => { - e.preventDefault(); - e.stopPropagation(); - const input = e.target.value.toLowerCase(); - const items = document.querySelectorAll('[role="option"]'); - items.forEach((item) => { - const text = item.textContent?.toLowerCase() || ""; - (item as HTMLElement).style.display = text.includes(input) - ? "" - : "none"; - }); - }} + onKeyDown={handleKeyDown} /> + {searchQuery && ( + { + setSearchQuery(""); + setHighlightedIndex(-1); + }} + /> + )}
- {personaList?.map((persona) => ( + {filteredPersonaList.map((persona, index) => ( setHighlightedIndex(index)} > {persona.name} @@ -242,36 +238,6 @@ export function PersonaMessagesChart({
- - {selectedPersonas.length > 0 && ( -
- {selectedPersonas.map((persona) => { - const index = selectedPersonaIds.indexOf(persona.id); - return ( - - {persona.name} - - setSelectedPersonaIds( - selectedPersonaIds.filter((id) => id !== persona.id) - ) - } - /> - - ); - })} -
- )} {content}
From 4a24d8a6f316fad604725706b0530d39bcb77521 Mon Sep 17 00:00:00 2001 From: hagen-danswer Date: Wed, 27 Nov 2024 17:44:58 -0800 Subject: [PATCH 3/5] k --- backend/ee/danswer/server/analytics/api.py | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/backend/ee/danswer/server/analytics/api.py b/backend/ee/danswer/server/analytics/api.py index 382df90f6ad..60a45cf733d 100644 --- a/backend/ee/danswer/server/analytics/api.py +++ b/backend/ee/danswer/server/analytics/api.py @@ -1,6 +1,5 @@ import datetime from collections import defaultdict -from typing import Any from fastapi import APIRouter from fastapi import Depends @@ -161,6 +160,12 @@ def get_persona_messages( return persona_message_counts +class PersonaUniqueUsersResponse(BaseModel): + unique_users: int + date: datetime.date + persona_id: int + + @router.get("/admin/persona/unique-users") def get_persona_unique_users( persona_ids: str, @@ -168,7 +173,7 @@ def get_persona_unique_users( end: datetime.datetime, _: User | None = Depends(current_admin_user), db_session: Session = Depends(get_session), -) -> list[dict[str, Any]]: +) -> list[PersonaUniqueUsersResponse]: """Get unique users per day for each persona.""" persona_id_list = [int(pid) for pid in persona_ids.split(",")] results = [] @@ -181,10 +186,10 @@ def get_persona_unique_users( ) for count, date in daily_counts: results.append( - { - "unique_users": count, - "date": date.isoformat(), - "persona_id": persona_id, - } + PersonaUniqueUsersResponse( + unique_users=count, + date=date, + persona_id=persona_id, + ) ) return results From ec2fbedb3a9260b4fe3750b2df15ad76044e0604 Mon Sep 17 00:00:00 2001 From: hagen-danswer Date: Wed, 27 Nov 2024 17:52:48 -0800 Subject: [PATCH 4/5] hope this works --- .../background/celery/tasks/external_group_syncing/tasks.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/backend/danswer/background/celery/tasks/external_group_syncing/tasks.py b/backend/danswer/background/celery/tasks/external_group_syncing/tasks.py index 8ec10052192..8e91b14b7b4 100644 --- a/backend/danswer/background/celery/tasks/external_group_syncing/tasks.py +++ b/backend/danswer/background/celery/tasks/external_group_syncing/tasks.py @@ -228,11 +228,9 @@ def connector_external_group_sync_generator_task( ext_group_sync_func = GROUP_PERMISSIONS_FUNC_MAP.get(source_type) if ext_group_sync_func is None: - raise ValueError(f"No external group sync func found for {source_type}") + raise ValueError(f"No doc sync func found for {source_type}") - logger.info( - f"Syncing external groups for {source_type} with cc_pair={cc_pair_id}" - ) + logger.info(f"Syncing docs for {source_type}") external_user_groups: list[ExternalUserGroup] = ext_group_sync_func(cc_pair) From 947c1f7d1fa57329f16a0beab1a9886ceddd01de Mon Sep 17 00:00:00 2001 From: hagen-danswer Date: Tue, 3 Dec 2024 11:17:39 -0800 Subject: [PATCH 5/5] cleanup --- backend/ee/danswer/db/analytics.py | 4 +- backend/ee/danswer/server/analytics/api.py | 86 +++++++++---------- web/src/app/ee/admin/performance/lib.ts | 25 ++---- .../usage/PersonaMessagesChart.tsx | 26 ++---- 4 files changed, 56 insertions(+), 85 deletions(-) diff --git a/backend/ee/danswer/db/analytics.py b/backend/ee/danswer/db/analytics.py index 1b068099153..8d27af06899 100644 --- a/backend/ee/danswer/db/analytics.py +++ b/backend/ee/danswer/db/analytics.py @@ -201,7 +201,7 @@ def fetch_persona_message_analytics( .order_by(cast(ChatMessage.time_sent, Date)) ) - return list(db_session.execute(query).all()) # type: ignore + return [tuple(row) for row in db_session.execute(query).all()] def fetch_persona_unique_users( @@ -233,4 +233,4 @@ def fetch_persona_unique_users( .order_by(cast(ChatMessage.time_sent, Date)) ) - return list(db_session.execute(query).all()) # type: ignore + return [tuple(row) for row in db_session.execute(query).all()] diff --git a/backend/ee/danswer/server/analytics/api.py b/backend/ee/danswer/server/analytics/api.py index 60a45cf733d..2963dc2134c 100644 --- a/backend/ee/danswer/server/analytics/api.py +++ b/backend/ee/danswer/server/analytics/api.py @@ -3,7 +3,6 @@ from fastapi import APIRouter from fastapi import Depends -from fastapi import Query from pydantic import BaseModel from sqlalchemy.orm import Session @@ -19,6 +18,9 @@ router = APIRouter(prefix="/analytics") +_DEFAULT_LOOKBACK_DAYS = 30 + + class QueryAnalyticsResponse(BaseModel): total_queries: int total_likes: int @@ -36,7 +38,7 @@ def get_query_analytics( daily_query_usage_info = fetch_query_analytics( start=start or ( - datetime.datetime.utcnow() - datetime.timedelta(days=30) + datetime.datetime.utcnow() - datetime.timedelta(days=_DEFAULT_LOOKBACK_DAYS) ), # default is 30d lookback end=end or datetime.datetime.utcnow(), db_session=db_session, @@ -67,7 +69,7 @@ def get_user_analytics( daily_query_usage_info_per_user = fetch_per_user_query_analytics( start=start or ( - datetime.datetime.utcnow() - datetime.timedelta(days=30) + datetime.datetime.utcnow() - datetime.timedelta(days=_DEFAULT_LOOKBACK_DAYS) ), # default is 30d lookback end=end or datetime.datetime.utcnow(), db_session=db_session, @@ -101,7 +103,7 @@ def get_danswerbot_analytics( daily_danswerbot_info = fetch_danswerbot_analytics( start=start or ( - datetime.datetime.utcnow() - datetime.timedelta(days=30) + datetime.datetime.utcnow() - datetime.timedelta(days=_DEFAULT_LOOKBACK_DAYS) ), # default is 30d lookback end=end or datetime.datetime.utcnow(), db_session=db_session, @@ -128,34 +130,32 @@ class PersonaMessageAnalyticsResponse(BaseModel): @router.get("/admin/persona/messages") def get_persona_messages( - persona_ids: str = Query(...), # ... means this parameter is required + persona_id: int, start: datetime.datetime | None = None, end: datetime.datetime | None = None, _: User | None = Depends(current_admin_user), db_session: Session = Depends(get_session), ) -> list[PersonaMessageAnalyticsResponse]: - """Fetch daily message counts for multiple personas within the given time range.""" - # Convert comma-separated string to list of integers - parsed_persona_ids = [ - int(id.strip()) for id in persona_ids.split(",") if id.strip() - ] - persona_message_counts = [] - start = start or (datetime.datetime.utcnow() - datetime.timedelta(days=30)) + """Fetch daily message counts for a single persona within the given time range.""" + start = start or ( + datetime.datetime.utcnow() - datetime.timedelta(days=_DEFAULT_LOOKBACK_DAYS) + ) end = end or datetime.datetime.utcnow() - for persona_id in parsed_persona_ids: - for count, date in fetch_persona_message_analytics( - db_session=db_session, - persona_id=int(persona_id), - start=start, - end=end, - ): - persona_message_counts.append( - PersonaMessageAnalyticsResponse( - total_messages=count, - date=date, - persona_id=persona_id, - ) + + persona_message_counts = [] + for count, date in fetch_persona_message_analytics( + db_session=db_session, + persona_id=persona_id, + start=start, + end=end, + ): + persona_message_counts.append( + PersonaMessageAnalyticsResponse( + total_messages=count, + date=date, + persona_id=persona_id, ) + ) return persona_message_counts @@ -168,28 +168,26 @@ class PersonaUniqueUsersResponse(BaseModel): @router.get("/admin/persona/unique-users") def get_persona_unique_users( - persona_ids: str, + persona_id: int, start: datetime.datetime, end: datetime.datetime, _: User | None = Depends(current_admin_user), db_session: Session = Depends(get_session), ) -> list[PersonaUniqueUsersResponse]: - """Get unique users per day for each persona.""" - persona_id_list = [int(pid) for pid in persona_ids.split(",")] - results = [] - for persona_id in persona_id_list: - daily_counts = fetch_persona_unique_users( - db_session=db_session, - persona_id=persona_id, - start=start, - end=end, - ) - for count, date in daily_counts: - results.append( - PersonaUniqueUsersResponse( - unique_users=count, - date=date, - persona_id=persona_id, - ) + """Get unique users per day for a single persona.""" + unique_user_counts = [] + daily_counts = fetch_persona_unique_users( + db_session=db_session, + persona_id=persona_id, + start=start, + end=end, + ) + for count, date in daily_counts: + unique_user_counts.append( + PersonaUniqueUsersResponse( + unique_users=count, + date=date, + persona_id=persona_id, ) - return results + ) + return unique_user_counts diff --git a/web/src/app/ee/admin/performance/lib.ts b/web/src/app/ee/admin/performance/lib.ts index 951a8e34267..59042a38766 100644 --- a/web/src/app/ee/admin/performance/lib.ts +++ b/web/src/app/ee/admin/performance/lib.ts @@ -112,31 +112,18 @@ export interface PersonaSnapshot { is_public: boolean; } -export const usePersonaList = () => { - const { data, error, isLoading } = useSWR( - "/api/persona", - errorHandlingFetcher - ); - - return { - data, - error, - isLoading, - }; -}; - export const usePersonaMessages = ( - personaIds: number[], + personaId: number | undefined, timeRange: DateRangePickerValue ) => { const url = buildApiPath(`/api/analytics/admin/persona/messages`, { - persona_ids: personaIds.join(","), + persona_id: personaId?.toString(), start: convertDateToStartOfDay(timeRange.from)?.toISOString(), end: convertDateToEndOfDay(timeRange.to)?.toISOString(), }); const { data, error, isLoading } = useSWR( - personaIds.length > 0 ? url : null, + personaId !== undefined ? url : null, errorHandlingFetcher ); @@ -155,17 +142,17 @@ export interface PersonaUniqueUserAnalytics { } export const usePersonaUniqueUsers = ( - personaIds: number[], + personaId: number | undefined, timeRange: DateRangePickerValue ) => { const url = buildApiPath(`/api/analytics/admin/persona/unique-users`, { - persona_ids: personaIds.join(","), + persona_id: personaId?.toString(), start: convertDateToStartOfDay(timeRange.from)?.toISOString(), end: convertDateToEndOfDay(timeRange.to)?.toISOString(), }); const { data, error, isLoading } = useSWR( - personaIds.length > 0 ? url : null, + personaId !== undefined ? url : null, errorHandlingFetcher ); diff --git a/web/src/app/ee/admin/performance/usage/PersonaMessagesChart.tsx b/web/src/app/ee/admin/performance/usage/PersonaMessagesChart.tsx index 668404c6998..593ab6ba4de 100644 --- a/web/src/app/ee/admin/performance/usage/PersonaMessagesChart.tsx +++ b/web/src/app/ee/admin/performance/usage/PersonaMessagesChart.tsx @@ -2,10 +2,10 @@ import { ThreeDotsLoader } from "@/components/Loading"; import { X, Search } from "lucide-react"; import { getDatesList, - usePersonaList, usePersonaMessages, usePersonaUniqueUsers, } from "../lib"; +import { useAssistants } from "@/components/context/AssistantsContext"; import { DateRangePickerValue } from "@/app/ee/admin/performance/DateRangeSelector"; import Text from "@/components/ui/text"; import Title from "@/components/ui/title"; @@ -30,36 +30,22 @@ export function PersonaMessagesChart({ >(undefined); const [searchQuery, setSearchQuery] = useState(""); const [highlightedIndex, setHighlightedIndex] = useState(-1); - const { - data: personaList, - isLoading: isPersonaListLoading, - error: personaListError, - } = usePersonaList(); + const { allAssistants: personaList } = useAssistants(); const { data: personaMessagesData, isLoading: isPersonaMessagesLoading, error: personaMessagesError, - } = usePersonaMessages( - selectedPersonaId !== undefined ? [selectedPersonaId] : [], - timeRange - ); + } = usePersonaMessages(selectedPersonaId, timeRange); const { data: personaUniqueUsersData, isLoading: isPersonaUniqueUsersLoading, error: personaUniqueUsersError, - } = usePersonaUniqueUsers( - selectedPersonaId !== undefined ? [selectedPersonaId] : [], - timeRange - ); + } = usePersonaUniqueUsers(selectedPersonaId, timeRange); - const isLoading = - isPersonaListLoading || - isPersonaMessagesLoading || - isPersonaUniqueUsersLoading; - const hasError = - personaListError || personaMessagesError || personaUniqueUsersError; + const isLoading = isPersonaMessagesLoading || isPersonaUniqueUsersLoading; + const hasError = personaMessagesError || personaUniqueUsersError; const filteredPersonaList = useMemo(() => { if (!personaList) return [];