Add persona stats (#3282)

* Added a chart to display persona message stats

* polish

* k

* hope this works

* cleanup
This commit is contained in:
hagen-danswer
2024-12-05 09:15:56 -08:00
committed by GitHub
parent c81e704c95
commit 14772dee71
5 changed files with 442 additions and 3 deletions

View File

@@ -170,3 +170,67 @@ 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 [tuple(row) for row in db_session.execute(query).all()]
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 [tuple(row) for row in db_session.execute(query).all()]

View File

@@ -11,11 +11,16 @@ from danswer.db.engine import get_session
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_persona_unique_users
from ee.danswer.db.analytics import fetch_query_analytics
router = APIRouter(prefix="/analytics")
_DEFAULT_LOOKBACK_DAYS = 30
class QueryAnalyticsResponse(BaseModel):
total_queries: int
total_likes: int
@@ -33,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,
@@ -64,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,
@@ -98,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,
@@ -115,3 +120,74 @@ 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_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 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()
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
class PersonaUniqueUsersResponse(BaseModel):
unique_users: int
date: datetime.date
persona_id: int
@router.get("/admin/persona/unique-users")
def get_persona_unique_users(
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 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 unique_user_counts

View File

@@ -97,3 +97,69 @@ 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 usePersonaMessages = (
personaId: number | undefined,
timeRange: DateRangePickerValue
) => {
const url = buildApiPath(`/api/analytics/admin/persona/messages`, {
persona_id: personaId?.toString(),
start: convertDateToStartOfDay(timeRange.from)?.toISOString(),
end: convertDateToEndOfDay(timeRange.to)?.toISOString(),
});
const { data, error, isLoading } = useSWR<PersonaMessageAnalytics[]>(
personaId !== undefined ? url : null,
errorHandlingFetcher
);
return {
data,
error,
isLoading,
refreshPersonaMessages: () => mutate(url),
};
};
export interface PersonaUniqueUserAnalytics {
unique_users: number;
date: string;
persona_id: number;
}
export const usePersonaUniqueUsers = (
personaId: number | undefined,
timeRange: DateRangePickerValue
) => {
const url = buildApiPath(`/api/analytics/admin/persona/unique-users`, {
persona_id: personaId?.toString(),
start: convertDateToStartOfDay(timeRange.from)?.toISOString(),
end: convertDateToEndOfDay(timeRange.to)?.toISOString(),
});
const { data, error, isLoading } = useSWR<PersonaUniqueUserAnalytics[]>(
personaId !== undefined ? url : null,
errorHandlingFetcher
);
return {
data,
error,
isLoading,
refreshPersonaUniqueUsers: () => mutate(url),
};
};

View File

@@ -0,0 +1,231 @@
import { ThreeDotsLoader } from "@/components/Loading";
import { X, Search } from "lucide-react";
import {
getDatesList,
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";
import CardSection from "@/components/admin/CardSection";
import { AreaChartDisplay } from "@/components/ui/areaChart";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import { useState, useMemo, useEffect } from "react";
export function PersonaMessagesChart({
timeRange,
}: {
timeRange: DateRangePickerValue;
}) {
const [selectedPersonaId, setSelectedPersonaId] = useState<
number | undefined
>(undefined);
const [searchQuery, setSearchQuery] = useState("");
const [highlightedIndex, setHighlightedIndex] = useState(-1);
const { allAssistants: personaList } = useAssistants();
const {
data: personaMessagesData,
isLoading: isPersonaMessagesLoading,
error: personaMessagesError,
} = usePersonaMessages(selectedPersonaId, timeRange);
const {
data: personaUniqueUsersData,
isLoading: isPersonaUniqueUsersLoading,
error: personaUniqueUsersError,
} = usePersonaUniqueUsers(selectedPersonaId, timeRange);
const isLoading = isPersonaMessagesLoading || isPersonaUniqueUsersLoading;
const hasError = 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 ||
!personaUniqueUsersData?.length ||
selectedPersonaId === undefined
) {
return null;
}
const initialDate =
timeRange.from ||
new Date(
Math.min(
...personaMessagesData.map((entry) => new Date(entry.date).getTime())
)
);
const dateRange = getDatesList(initialDate);
// 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 messageData = messagesMap.get(dateStr);
const uniqueUserData = uniqueUsersMap.get(dateStr);
return {
Day: dateStr,
Messages: messageData?.total_messages || 0,
"Unique Users": uniqueUserData?.unique_users || 0,
};
});
}, [
personaMessagesData,
personaUniqueUsersData,
timeRange.from,
selectedPersonaId,
]);
let content;
if (isLoading) {
content = (
<div className="h-80 flex flex-col">
<ThreeDotsLoader />
</div>
);
} else if (!personaList || hasError) {
content = (
<div className="h-80 text-red-600 text-bold flex flex-col">
<p className="m-auto">Failed to fetch data...</p>
</div>
);
} else if (selectedPersonaId === undefined) {
content = (
<div className="h-80 text-gray-500 flex flex-col">
<p className="m-auto">Select a persona to view analytics</p>
</div>
);
} else if (!personaMessagesData?.length) {
content = (
<div className="h-80 text-gray-500 flex flex-col">
<p className="m-auto">
No data found for selected persona in the selected time range
</p>
</div>
);
} else if (chartData) {
content = (
<AreaChartDisplay
className="mt-4"
data={chartData}
categories={["Messages", "Unique Users"]}
index="Day"
colors={["indigo", "fuchsia"]}
yAxisWidth={60}
/>
);
}
const selectedPersona = personaList?.find((p) => p.id === selectedPersonaId);
return (
<CardSection className="mt-8">
<Title>Persona Analytics</Title>
<div className="flex flex-col gap-4">
<Text>Messages and unique users per day for selected persona</Text>
<div className="flex items-center gap-4">
<Select
value={selectedPersonaId?.toString() ?? ""}
onValueChange={(value) => {
setSelectedPersonaId(parseInt(value));
}}
>
<SelectTrigger className="flex w-full max-w-xs">
<SelectValue placeholder="Select a persona to display" />
</SelectTrigger>
<SelectContent>
<div className="flex items-center px-2 pb-2 sticky top-0 bg-background border-b">
<Search className="h-4 w-4 mr-2 shrink-0 opacity-50" />
<input
className="flex h-8 w-full rounded-sm bg-transparent py-3 text-sm outline-none placeholder:text-muted-foreground disabled:cursor-not-allowed disabled:opacity-50"
placeholder="Search personas..."
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
onClick={(e) => e.stopPropagation()}
onMouseDown={(e) => e.stopPropagation()}
onKeyDown={handleKeyDown}
/>
{searchQuery && (
<X
className="h-4 w-4 shrink-0 opacity-50 cursor-pointer hover:opacity-100"
onClick={() => {
setSearchQuery("");
setHighlightedIndex(-1);
}}
/>
)}
</div>
{filteredPersonaList.map((persona, index) => (
<SelectItem
key={persona.id}
value={persona.id.toString()}
className={`${highlightedIndex === index ? "hover" : ""}`}
onMouseEnter={() => setHighlightedIndex(index)}
>
{persona.name}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
</div>
{content}
</CardSection>
);
}

View File

@@ -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() {
<QueryPerformanceChart timeRange={timeRange} />
<FeedbackChart timeRange={timeRange} />
<DanswerBotChart timeRange={timeRange} />
<PersonaMessagesChart timeRange={timeRange} />
<Separator />
<UsageReports />
</main>