Fix analytics + query history

This commit is contained in:
Weves
2023-12-16 16:47:01 -08:00
committed by Chris Weaver
parent 67a4eb6f6f
commit a52711967f
16 changed files with 353 additions and 280 deletions

View File

@@ -119,6 +119,11 @@ class AuthType(str, Enum):
SAML = "saml"
class QAFeedbackType(str, Enum):
LIKE = "like" # User likes the answer, used for metrics
DISLIKE = "dislike" # User dislikes the answer, used for metrics
class SearchFeedbackType(str, Enum):
ENDORSE = "endorse" # boost this document for all future queries
REJECT = "reject" # down-boost this document for all future queries

View File

@@ -9,8 +9,10 @@ from sqlalchemy import func
from sqlalchemy import select
from sqlalchemy.orm import Session
from danswer.configs.constants import QAFeedbackType
from danswer.db.models import QueryEvent
from danswer.configs.constants import MessageType
from danswer.db.models import ChatMessage
from danswer.db.models import ChatMessageFeedback
from danswer.db.models import ChatSession
def fetch_query_analytics(
@@ -20,19 +22,29 @@ def fetch_query_analytics(
) -> Sequence[tuple[int, int, int, datetime.date]]:
stmt = (
select(
func.count(QueryEvent.id),
func.sum(case((QueryEvent.feedback == QAFeedbackType.LIKE, 1), else_=0)),
func.sum(case((QueryEvent.feedback == QAFeedbackType.DISLIKE, 1), else_=0)),
cast(QueryEvent.time_created, Date),
func.count(ChatMessage.id),
func.sum(case((ChatMessageFeedback.is_positive, 1), else_=0)),
func.sum(
case(
(ChatMessageFeedback.is_positive == False, 1), else_=0 # noqa: E712
)
),
cast(ChatMessage.time_sent, Date),
)
.join(
ChatMessageFeedback,
ChatMessageFeedback.chat_message_id == ChatMessage.id,
isouter=True,
)
.where(
QueryEvent.time_created >= start,
ChatMessage.time_sent >= start,
)
.where(
QueryEvent.time_created <= end,
ChatMessage.time_sent <= end,
)
.group_by(cast(QueryEvent.time_created, Date))
.order_by(cast(QueryEvent.time_created, Date))
.where(ChatMessage.message_type == MessageType.ASSISTANT)
.group_by(cast(ChatMessage.time_sent, Date))
.order_by(cast(ChatMessage.time_sent, Date))
)
return db_session.execute(stmt).all() # type: ignore
@@ -45,20 +57,26 @@ def fetch_per_user_query_analytics(
) -> Sequence[tuple[int, int, int, datetime.date, UUID]]:
stmt = (
select(
func.count(QueryEvent.id),
func.sum(case((QueryEvent.feedback == QAFeedbackType.LIKE, 1), else_=0)),
func.sum(case((QueryEvent.feedback == QAFeedbackType.DISLIKE, 1), else_=0)),
cast(QueryEvent.time_created, Date),
QueryEvent.user_id,
func.count(ChatMessage.id),
func.sum(case((ChatMessageFeedback.is_positive, 1), else_=0)),
func.sum(
case(
(ChatMessageFeedback.is_positive == False, 1), else_=0 # noqa: E712
)
),
cast(ChatMessage.time_sent, Date),
ChatSession.user_id,
)
.join(ChatSession, ChatSession.id == ChatMessage.chat_session_id)
.where(
ChatMessage.time_sent >= start,
)
.where(
QueryEvent.time_created >= start,
ChatMessage.time_sent <= end,
)
.where(
QueryEvent.time_created <= end,
)
.group_by(cast(QueryEvent.time_created, Date), QueryEvent.user_id)
.order_by(cast(QueryEvent.time_created, Date), QueryEvent.user_id)
.where(ChatMessage.message_type == MessageType.ASSISTANT)
.group_by(cast(ChatMessage.time_sent, Date), ChatSession.user_id)
.order_by(cast(ChatMessage.time_sent, Date), ChatSession.user_id)
)
return db_session.execute(stmt).all() # type: ignore

View File

@@ -10,34 +10,39 @@ from sqlalchemy.orm import aliased
from sqlalchemy.orm import Session
from sqlalchemy.orm.attributes import InstrumentedAttribute
from danswer.configs.constants import QAFeedbackType
from danswer.db.models import QueryEvent
from danswer.configs.constants import MessageType
from danswer.db.models import ChatMessage
from danswer.db.models import ChatSession
from danswer.db.models import User
SortByOptions = Literal["time_created", "feedback"]
SortByOptions = Literal["time_sent"]
def build_query_history_query(
start: datetime.datetime,
end: datetime.datetime,
query: str | None,
feedback_type: QAFeedbackType | None,
sort_by_field: SortByOptions,
sort_by_direction: Literal["asc", "desc"],
offset: int,
limit: int | None,
) -> Select[tuple[QueryEvent]]:
) -> Select[tuple[ChatMessage]]:
stmt = (
select(QueryEvent)
select(ChatMessage)
.where(
QueryEvent.time_created >= start,
ChatMessage.time_sent >= start,
)
.where(
QueryEvent.time_created <= end,
ChatMessage.time_sent <= end,
)
.where(
or_(
ChatMessage.message_type == MessageType.ASSISTANT,
ChatMessage.message_type == MessageType.USER,
),
)
)
order_by_field = cast(InstrumentedAttribute, getattr(QueryEvent, sort_by_field))
order_by_field = cast(InstrumentedAttribute, getattr(ChatMessage, sort_by_field))
if sort_by_direction == "asc":
stmt = stmt.order_by(order_by_field.asc())
else:
@@ -48,17 +53,6 @@ def build_query_history_query(
if limit:
stmt = stmt.limit(limit)
if query:
stmt = stmt.where(
or_(
QueryEvent.llm_answer.ilike(f"%{query}%"),
QueryEvent.query.ilike(f"%{query}%"),
)
)
if feedback_type:
stmt = stmt.where(QueryEvent.feedback == feedback_type)
return stmt
@@ -66,18 +60,14 @@ def fetch_query_history(
db_session: Session,
start: datetime.datetime,
end: datetime.datetime,
query: str | None = None,
feedback_type: QAFeedbackType | None = None,
sort_by_field: SortByOptions = "time_created",
sort_by_field: SortByOptions = "time_sent",
sort_by_direction: Literal["asc", "desc"] = "desc",
offset: int = 0,
limit: int | None = 500,
) -> Sequence[QueryEvent]:
) -> Sequence[ChatMessage]:
stmt = build_query_history_query(
start=start,
end=end,
query=query,
feedback_type=feedback_type,
sort_by_field=sort_by_field,
sort_by_direction=sort_by_direction,
offset=offset,
@@ -91,27 +81,25 @@ def fetch_query_history_with_user_email(
db_session: Session,
start: datetime.datetime,
end: datetime.datetime,
query: str | None = None,
feedback_type: QAFeedbackType | None = None,
sort_by_field: SortByOptions = "time_created",
sort_by_field: SortByOptions = "time_sent",
sort_by_direction: Literal["asc", "desc"] = "desc",
offset: int = 0,
limit: int | None = 500,
) -> Sequence[tuple[QueryEvent, str | None]]:
) -> Sequence[tuple[ChatMessage, str | None]]:
subquery = build_query_history_query(
start=start,
end=end,
query=query,
feedback_type=feedback_type,
sort_by_field=sort_by_field,
sort_by_direction=sort_by_direction,
offset=offset,
limit=limit,
).subquery()
subquery_alias = aliased(QueryEvent, subquery)
subquery_alias = aliased(ChatMessage, subquery)
stmt_with_user_email = select(subquery_alias, User.email).join( # type: ignore
User, subquery_alias.user_id == User.id, isouter=True
stmt_with_user_email = (
select(subquery_alias, User.email) # type: ignore
.join(ChatSession, subquery_alias.chat_session_id == ChatSession.id)
.join(User, ChatSession.user_id == User.id, isouter=True)
)
return db_session.execute(stmt_with_user_email).all() # type: ignore

View File

@@ -1,6 +1,6 @@
import csv
import io
from collections.abc import Iterable
from collections import defaultdict
from datetime import datetime
from datetime import timedelta
from datetime import timezone
@@ -14,11 +14,11 @@ from sqlalchemy.orm import Session
import danswer.db.models as db_models
from danswer.auth.users import current_admin_user
from danswer.configs.constants import MessageType
from danswer.configs.constants import QAFeedbackType
from danswer.db.chat import get_chat_session_by_id
from danswer.db.engine import get_session
from danswer.db.feedback import fetch_query_event_by_id
from danswer.db.models import Document
from ee.danswer.db.document import fetch_documents_from_ids
from danswer.db.models import ChatMessage
from ee.danswer.db.query_history import (
fetch_query_history_with_user_email,
)
@@ -35,45 +35,112 @@ class AbridgedSearchDoc(BaseModel):
link: str | None
class QuerySnapshot(BaseModel):
class MessageSnapshot(BaseModel):
message: str
message_type: MessageType
documents: list[AbridgedSearchDoc]
feedback: QAFeedbackType | None
time_created: datetime
@classmethod
def build(cls, message: ChatMessage) -> "MessageSnapshot":
latest_messages_feedback_obj = (
message.chat_message_feedbacks[-1]
if len(message.chat_message_feedbacks) > 0
else None
)
message_feedback = (
(
QAFeedbackType.LIKE
if latest_messages_feedback_obj.is_positive
else QAFeedbackType.DISLIKE
)
if latest_messages_feedback_obj
else None
)
return cls(
message=message.message,
message_type=message.message_type,
documents=[
AbridgedSearchDoc(
document_id=document.document_id,
semantic_identifier=document.semantic_id,
link=document.link,
)
for document in message.search_docs
],
feedback=message_feedback,
time_created=message.time_sent,
)
class ChatSessionSnapshot(BaseModel):
id: int
user_email: str | None
query: str
llm_answer: str | None
retrieved_documents: list[AbridgedSearchDoc]
feedback: QAFeedbackType | None
name: str | None
messages: list[MessageSnapshot]
time_created: datetime
@classmethod
def build(
cls,
query_event: db_models.QueryEvent,
user_email: str | None,
documents: Iterable[Document],
) -> "QuerySnapshot":
messages: list[ChatMessage],
) -> "ChatSessionSnapshot":
if len(messages) == 0:
raise ValueError("No messages provided")
chat_session = messages[0].chat_session
return cls(
id=query_event.id,
user_email=user_email,
query=query_event.query,
llm_answer=query_event.llm_answer,
retrieved_documents=[
AbridgedSearchDoc(
document_id=document.id,
semantic_identifier=document.semantic_id,
link=document.link,
)
for document in documents
id=chat_session.id,
user_email=chat_session.user.email if chat_session.user else None,
name=chat_session.description,
messages=[
MessageSnapshot.build(message)
for message in sorted(messages, key=lambda m: m.time_sent)
if message.message_type != MessageType.SYSTEM
],
feedback=query_event.feedback,
time_created=query_event.time_created,
time_created=chat_session.time_created,
)
class QuestionAnswerPairSnapshot(BaseModel):
user_message: str
ai_response: str
retrieved_documents: list[AbridgedSearchDoc]
feedback: QAFeedbackType | None
time_created: datetime
@classmethod
def from_chat_session_snapshot(
cls,
chat_session_snapshot: ChatSessionSnapshot,
) -> list["QuestionAnswerPairSnapshot"]:
message_pairs: list[tuple[MessageSnapshot, MessageSnapshot]] = []
for ind in range(1, len(chat_session_snapshot.messages), 2):
message_pairs.append(
(
chat_session_snapshot.messages[ind - 1],
chat_session_snapshot.messages[ind],
)
)
return [
cls(
user_message=user_message.message,
ai_response=ai_message.message,
retrieved_documents=ai_message.documents,
feedback=ai_message.feedback,
time_created=user_message.time_created,
)
for user_message, ai_message in message_pairs
]
def to_json(self) -> dict[str, str]:
return {
"id": str(self.id),
"query": self.query,
"user_email": self.user_email or "",
"llm_answer": self.llm_answer or "",
"user_message": self.user_message,
"ai_response": self.ai_response,
"retrieved_documents": "|".join(
[
doc.link or doc.semantic_identifier
@@ -85,61 +152,52 @@ class QuerySnapshot(BaseModel):
}
def fetch_and_process_query_history(
def fetch_and_process_chat_session_history(
db_session: Session,
start: datetime | None,
end: datetime | None,
feedback_type: QAFeedbackType | None,
limit: int | None = 500,
) -> list[QuerySnapshot]:
query_history_with_user_email = fetch_query_history_with_user_email(
) -> list[ChatSessionSnapshot]:
chat_messages_with_user_email = fetch_query_history_with_user_email(
db_session=db_session,
start=start
or (
datetime.now(tz=timezone.utc) - timedelta(days=30)
), # default is 30d lookback
end=end or datetime.now(tz=timezone.utc),
feedback_type=feedback_type,
limit=limit,
)
session_id_to_messages: dict[int, list[ChatMessage]] = defaultdict(list)
for message, _ in chat_messages_with_user_email:
session_id_to_messages[message.chat_session_id].append(message)
all_relevant_document_ids: set[str] = set()
for query_event, _ in query_history_with_user_email:
all_relevant_document_ids = all_relevant_document_ids.union(
query_event.retrieved_document_ids or []
)
document_id_to_document = {
document.id: document
for document in fetch_documents_from_ids(
db_session, list(all_relevant_document_ids)
)
}
query_snapshots: list[QuerySnapshot] = []
for query_event, user_email in query_history_with_user_email:
unique_document_ids = set(query_event.retrieved_document_ids or [])
documents = [
document_id_to_document[doc_id]
for doc_id in unique_document_ids
if doc_id in document_id_to_document
]
query_snapshots.append(
QuerySnapshot.build(
query_event=query_event, user_email=user_email, documents=documents
chat_session_snapshots = [
ChatSessionSnapshot.build(messages)
for messages in session_id_to_messages.values()
]
if feedback_type:
chat_session_snapshots = [
chat_session_snapshot
for chat_session_snapshot in chat_session_snapshots
if any(
message.feedback == feedback_type
for message in chat_session_snapshot.messages
)
)
return query_snapshots
]
return chat_session_snapshots
@router.get("/admin/query-history")
def get_query_history(
@router.get("/admin/chat-session-history")
def get_chat_session_history(
feedback_type: QAFeedbackType | None = None,
start: datetime | None = None,
end: datetime | None = None,
_: db_models.User | None = Depends(current_admin_user),
db_session: Session = Depends(get_session),
) -> list[QuerySnapshot]:
return fetch_and_process_query_history(
) -> list[ChatSessionSnapshot]:
return fetch_and_process_chat_session_history(
db_session=db_session,
start=start,
end=end,
@@ -147,24 +205,22 @@ def get_query_history(
)
@router.get("/admin/query-history/{query_id}")
def get_query(
query_id: int,
@router.get("/admin/chat-session-history/{chat_session_id}")
def get_chat_session(
chat_session_id: int,
_: db_models.User | None = Depends(current_admin_user),
db_session: Session = Depends(get_session),
) -> QuerySnapshot:
) -> ChatSessionSnapshot:
try:
query_event = fetch_query_event_by_id(query_id=query_id, db_session=db_session)
chat_session = get_chat_session_by_id(
chat_session_id=chat_session_id, user_id=None, db_session=db_session
)
except ValueError:
raise HTTPException(400, f"Query event with id '{query_id}' does not exist.")
documents = fetch_documents_from_ids(
db_session, query_event.retrieved_document_ids or []
)
return QuerySnapshot.build(
query_event=query_event,
user_email=query_event.user.email if query_event.user else None,
documents=documents,
)
raise HTTPException(
400, f"Chat session with id '{chat_session_id}' does not exist."
)
return ChatSessionSnapshot.build(messages=chat_session.messages)
@router.get("/admin/query-history-csv")
@@ -172,7 +228,7 @@ def get_query_history_as_csv(
_: db_models.User | None = Depends(current_admin_user),
db_session: Session = Depends(get_session),
) -> StreamingResponse:
complete_query_history = fetch_and_process_query_history(
complete_chat_session_history = fetch_and_process_chat_session_history(
db_session=db_session,
start=datetime.fromtimestamp(0, tz=timezone.utc),
end=datetime.now(tz=timezone.utc),
@@ -180,11 +236,19 @@ def get_query_history_as_csv(
limit=None,
)
question_answer_pairs: list[QuestionAnswerPairSnapshot] = []
for chat_session_snapshot in complete_chat_session_history:
question_answer_pairs.extend(
QuestionAnswerPairSnapshot.from_chat_session_snapshot(chat_session_snapshot)
)
# Create an in-memory text stream
stream = io.StringIO()
writer = csv.DictWriter(stream, fieldnames=list(QuerySnapshot.__fields__.keys()))
writer = csv.DictWriter(
stream, fieldnames=list(QuestionAnswerPairSnapshot.__fields__.keys())
)
writer.writeheader()
for row in complete_query_history:
for row in question_answer_pairs:
writer.writerow(row.to_json())
# Reset the stream's position to the start

View File

@@ -67,7 +67,9 @@ export const UserEditor = ({
})}
onSelect={(option) => {
setSelectedUserIds([
...Array.from(new Set([...selectedUserIds, option.value as string])),
...Array.from(
new Set([...selectedUserIds, option.value as string])
),
]);
}}
itemComponent={({ option }) => (

View File

@@ -91,7 +91,10 @@ export const AddConnectorForm: React.FC<AddConnectorFormProps> = ({
onSelect={(option) => {
setSelectedCCPairIds([
...Array.from(
new Set([...selectedCCPairIds, parseInt(option.value as string)])
new Set([
...selectedCCPairIds,
parseInt(option.value as string),
])
),
]);
}}

View File

@@ -2,6 +2,7 @@ import {
DateRangePicker,
DateRangePickerItem,
DateRangePickerValue,
Text,
} from "@tremor/react";
import { getXDaysAgo } from "./dateUtils";
@@ -16,9 +17,7 @@ export function DateRangeSelector({
}) {
return (
<div>
<div className="text-sm my-auto mr-2 font-medium text-gray-200 mb-1">
Date Range
</div>
<Text className="my-auto mr-2 font-medium mb-1">Date Range</Text>
<DateRangePicker
className="max-w-md"
value={value}

View File

@@ -5,17 +5,15 @@ import { FeedbackChart } from "./FeedbackChart";
import { QueryPerformanceChart } from "./QueryPerformanceChart";
import { BarChartIcon } from "@/components/icons/icons";
import { useTimeRange } from "../lib";
import { AdminPageTitle } from "@/components/admin/Title";
export default function AnalyticsPage() {
const [timeRange, setTimeRange] = useTimeRange();
return (
<main className="pt-4 mx-auto container dark">
<main className="pt-4 mx-auto container">
{/* TODO: remove this `dark` once we have a mode selector */}
<div className="border-solid border-gray-600 border-b pb-2 mb-4 flex">
<BarChartIcon size={32} />
<h1 className="text-3xl font-bold pl-2">Analytics</h1>
</div>
<AdminPageTitle title="Analytics" icon={<BarChartIcon size={32} />} />
<DateRangeSelector value={timeRange} onValueChange={setTimeRange} />

View File

@@ -18,12 +18,18 @@ export interface AbridgedSearchDoc {
link: string | null;
}
export interface QuerySnapshot {
id: number;
query: string;
user_email: string | null;
llm_answer: string;
retrieved_documents: AbridgedSearchDoc[];
time_created: string;
export interface MessageSnapshot {
message: string;
message_type: "user" | "assistant";
documents: AbridgedSearchDoc[];
feedback: Feedback | null;
time_created: string;
}
export interface ChatSessionSnapshot {
id: number;
user_email: string | null;
name: string | null;
messages: MessageSnapshot[];
time_created: string;
}

View File

@@ -1,8 +1,8 @@
import { errorHandlingFetcher } from "@/lib/fetcher";
import useSWR, { mutate } from "swr";
import {
ChatSessionSnapshot,
QueryAnalytics,
QuerySnapshot,
UserAnalytics,
} from "./analytics/types";
import { useState } from "react";
@@ -55,12 +55,12 @@ export const useQueryHistory = () => {
useState<Feedback | null>(null);
const [timeRange, setTimeRange] = useTimeRange();
const url = buildApiPath("/api/admin/query-history", {
const url = buildApiPath("/api/admin/chat-session-history", {
feedback_type: selectedFeedbackType,
start: convertDateToStartOfDay(timeRange.from)?.toISOString(),
end: convertDateToEndOfDay(timeRange.to)?.toISOString(),
});
const swrResponse = useSWR<QuerySnapshot[]>(url, errorHandlingFetcher);
const swrResponse = useSWR<ChatSessionSnapshot[]>(url, errorHandlingFetcher);
return {
...swrResponse,

View File

@@ -4,7 +4,7 @@ export function DownloadAsCSV() {
return (
<a
href="/api/admin/query-history-csv"
className="text-gray-300 flex ml-auto py-2 px-4 border border-gray-800 h-fit cursor-pointer hover:bg-gray-800 text-sm"
className="flex ml-auto py-2 px-4 border border-border h-fit cursor-pointer hover:bg-hover-light text-sm"
>
<FiDownload className="my-auto mr-2" />
Download as CSV

View File

@@ -1,7 +1,11 @@
import { Feedback } from "@/lib/types";
import { Badge } from "@tremor/react";
export function FeedbackBadge({ feedback }: { feedback?: Feedback | null }) {
export function FeedbackBadge({
feedback,
}: {
feedback?: Feedback | "mixed" | null;
}) {
let feedbackBadge;
switch (feedback) {
case "like":
@@ -18,6 +22,13 @@ export function FeedbackBadge({ feedback }: { feedback?: Feedback | null }) {
</Badge>
);
break;
case "mixed":
feedbackBadge = (
<Badge color="purple" className="text-sm">
Mixed
</Badge>
);
break;
default:
feedbackBadge = (
<Badge color="gray" className="text-sm">

View File

@@ -13,12 +13,9 @@ import {
import { Divider } from "@tremor/react";
import { Select, SelectItem } from "@tremor/react";
import { ThreeDotsLoader } from "@/components/Loading";
import { QuerySnapshot } from "../analytics/types";
import {
timestampToDateString,
timestampToReadableDate,
} from "@/lib/dateUtils";
import { FiBook, FiFrown, FiMinus, FiSmile } from "react-icons/fi";
import { ChatSessionSnapshot } from "../analytics/types";
import { timestampToReadableDate } from "@/lib/dateUtils";
import { FiFrown, FiMinus, FiSmile } from "react-icons/fi";
import { useState } from "react";
import { Feedback } from "@/lib/types";
import { DateRangeSelector } from "../DateRangeSelector";
@@ -30,42 +27,47 @@ import { DownloadAsCSV } from "./DownloadAsCSV";
const NUM_IN_PAGE = 20;
function QueryHistoryTableRow({
querySnapshot,
chatSessionSnapshot,
}: {
querySnapshot: QuerySnapshot;
chatSessionSnapshot: ChatSessionSnapshot;
}) {
let finalFeedback: Feedback | "mixed" | null = null;
for (const message of chatSessionSnapshot.messages) {
if (message.feedback) {
if (finalFeedback === null) {
finalFeedback = message.feedback;
} else if (finalFeedback !== message.feedback) {
finalFeedback = "mixed";
}
}
}
return (
<TableRow
key={querySnapshot.id}
className="hover:bg-gradient-to-r hover:from-gray-800 hover:to-indigo-950 cursor-pointer relative"
key={chatSessionSnapshot.id}
className="hover:bg-hover-light cursor-pointer relative"
>
<TableCell>{querySnapshot.query}</TableCell>
<TableCell>
<Text className="whitespace-normal line-clamp-5">
{querySnapshot.llm_answer}
{chatSessionSnapshot.messages[0]?.message || "-"}
</Text>
</TableCell>
<TableCell>
{querySnapshot.retrieved_documents.slice(0, 5).map((document) => (
<div className="flex" key={document.document_id}>
<FiBook className="my-auto mr-1" />{" "}
<p className="max-w-xs text-ellipsis overflow-hidden">
{document.semantic_identifier}
</p>
</div>
))}
<Text className="whitespace-normal line-clamp-5">
{chatSessionSnapshot.messages[1]?.message || "-"}
</Text>
</TableCell>
<TableCell>
<FeedbackBadge feedback={querySnapshot.feedback} />
<FeedbackBadge feedback={finalFeedback} />
</TableCell>
<TableCell>{querySnapshot.user_email || "-"}</TableCell>
<TableCell>{chatSessionSnapshot.user_email || "-"}</TableCell>
<TableCell>
{timestampToReadableDate(querySnapshot.time_created)}
{timestampToReadableDate(chatSessionSnapshot.time_created)}
</TableCell>
{/* Wrapping in <td> to avoid console warnings */}
<td className="w-0 p-0">
<Link
href={`/admin/performance/query-history/${querySnapshot.id}`}
href={`/admin/performance/query-history/${chatSessionSnapshot.id}`}
className="absolute w-full h-full left-0"
></Link>
</td>
@@ -82,9 +84,7 @@ function SelectFeedbackType({
}) {
return (
<div>
<div className="text-sm my-auto mr-2 font-medium text-gray-200 mb-1">
Feedback Type
</div>
<Text className="my-auto mr-2 font-medium mb-1">Feedback Type</Text>
<div className="max-w-sm space-y-6">
<Select
value={value}
@@ -108,7 +108,7 @@ function SelectFeedbackType({
export function QueryHistoryTable() {
const {
data: queryHistoryData,
data: chatSessionData,
selectedFeedbackType,
setSelectedFeedbackType,
timeRange,
@@ -119,7 +119,7 @@ export function QueryHistoryTable() {
return (
<Card className="mt-8">
{queryHistoryData ? (
{chatSessionData ? (
<>
<div className="flex">
<div className="gap-y-3 flex flex-col">
@@ -140,21 +140,20 @@ export function QueryHistoryTable() {
<Table className="mt-5">
<TableHead>
<TableRow>
<TableHeaderCell>Query</TableHeaderCell>
<TableHeaderCell>LLM Answer</TableHeaderCell>
<TableHeaderCell>Retrieved Documents</TableHeaderCell>
<TableHeaderCell>First User Message</TableHeaderCell>
<TableHeaderCell>First AI Response</TableHeaderCell>
<TableHeaderCell>Feedback</TableHeaderCell>
<TableHeaderCell>User</TableHeaderCell>
<TableHeaderCell>Date</TableHeaderCell>
</TableRow>
</TableHead>
<TableBody>
{queryHistoryData
{chatSessionData
.slice(NUM_IN_PAGE * (page - 1), NUM_IN_PAGE * page)
.map((querySnapshot) => (
.map((chatSessionSnapshot) => (
<QueryHistoryTableRow
key={querySnapshot.id}
querySnapshot={querySnapshot}
key={chatSessionSnapshot.id}
chatSessionSnapshot={chatSessionSnapshot}
/>
))}
</TableBody>
@@ -163,7 +162,7 @@ export function QueryHistoryTable() {
<div className="mt-3 flex">
<div className="mx-auto">
<PageSelector
totalPages={Math.ceil(queryHistoryData.length / NUM_IN_PAGE)}
totalPages={Math.ceil(chatSessionData.length / NUM_IN_PAGE)}
currentPage={page}
onPageChange={(newPage) => {
setPage(newPage);

View File

@@ -1,18 +0,0 @@
"use client";
/* TODO: bring this out of EE */
import { useRouter } from "next/navigation";
import { FiChevronLeft } from "react-icons/fi";
export function BackButton() {
const router = useRouter();
return (
<div
className="my-auto flex mb-1 hover:bg-gray-800 w-fit pr-2 cursor-pointer rounded-lg"
onClick={() => router.back()}
>
<FiChevronLeft className="mr-1 my-auto" />
Back
</div>
);
}

View File

@@ -1,82 +1,81 @@
import { Bold, Text, Card, Title, Divider, Italic } from "@tremor/react";
import { QuerySnapshot } from "../../analytics/types";
import { buildUrl } from "@/lib/utilsSS";
import { BackButton } from "./BackButton";
import { Bold, Text, Card, Title, Divider } from "@tremor/react";
import { ChatSessionSnapshot, MessageSnapshot } from "../../analytics/types";
import { FiBook } from "react-icons/fi";
import { processCookies } from "@/lib/userSS";
import { cookies } from "next/headers";
import { timestampToReadableDate } from "@/lib/dateUtils";
import { BackButton } from "@/components/BackButton";
import { SSRAutoRefresh } from "@/components/SSRAutoRefresh";
import { FeedbackBadge } from "../FeedbackBadge";
import { fetchSS } from "@/lib/utilsSS";
function MessageDisplay({ message }: { message: MessageSnapshot }) {
return (
<div>
<Bold className="text-xs mb-1">
{message.message_type === "user" ? "User" : "AI"}
</Bold>
<Text>{message.message}</Text>
{message.documents.length > 0 && (
<div className="flex flex-col gap-y-2 mt-2">
<Bold className="font-bold text-xs">Reference Documents</Bold>
{message.documents.slice(0, 5).map((document) => {
return (
<Text className="flex" key={document.document_id}>
<FiBook
className={
"my-auto mr-1" + (document.link ? " text-link" : " ")
}
/>
{document.link ? (
<a href={document.link} target="_blank" className="text-link">
{document.semantic_identifier}
</a>
) : (
document.semantic_identifier
)}
</Text>
);
})}
</div>
)}
{message.feedback && (
<div className="mt-2">
<FeedbackBadge feedback={message.feedback} />
</div>
)}
<Divider />
</div>
);
}
export default async function QueryPage({
params,
}: {
params: { id: string };
}) {
const response = await fetch(buildUrl(`/admin/query-history/${params.id}`), {
next: { revalidate: 0 },
headers: {
cookie: processCookies(cookies()),
},
});
const queryEvent = (await response.json()) as QuerySnapshot;
const response = await fetchSS(`/admin/chat-session-history/${params.id}`);
const chatSessionSnapshot = (await response.json()) as ChatSessionSnapshot;
return (
<main className="pt-4 mx-auto container dark">
<main className="pt-4 mx-auto container">
<BackButton />
<SSRAutoRefresh />
<Card className="mt-4">
<Title>Query Details</Title>
<Title>Chat Session Details</Title>
<Text className="flex flex-wrap whitespace-normal mt-1 text-xs">
{queryEvent.user_email || "-"},{" "}
{timestampToReadableDate(queryEvent.time_created)}
{chatSessionSnapshot.user_email || "-"},{" "}
{timestampToReadableDate(chatSessionSnapshot.time_created)}
</Text>
<Divider />
<div className="flex flex-col gap-y-3">
<div>
<Bold>Query</Bold>
<Text className="flex flex-wrap whitespace-normal mt-1">
{queryEvent.query}
</Text>
</div>
<div>
<Bold>Answer</Bold>
<Text className="flex flex-wrap whitespace-normal mt-1">
{queryEvent.llm_answer}
</Text>
</div>
<div>
<Bold>Retrieved Documents</Bold>
<div className="flex flex-col gap-y-2 mt-1">
{queryEvent.retrieved_documents?.map((document) => {
return (
<Text className="flex" key={document.document_id}>
<FiBook
className={
"my-auto mr-1" +
(document.link ? " text-blue-500" : " ")
}
/>
{document.link ? (
<a
href={document.link}
target="_blank"
className="text-blue-500"
>
{document.semantic_identifier}
</a>
) : (
document.semantic_identifier
)}
</Text>
);
})}
</div>
</div>
<div className="flex flex-col">
{chatSessionSnapshot.messages.map((message) => {
return (
<MessageDisplay key={message.time_created} message={message} />
);
})}
</div>
</Card>
</main>

View File

@@ -1,16 +1,15 @@
"use client";
import { AdminPageTitle } from "@/components/admin/Title";
import { QueryHistoryTable } from "./QueryHistoryTable";
import { DatabaseIcon } from "@/components/icons/icons";
export default function QueryHistoryPage() {
return (
<main className="pt-4 mx-auto container dark">
{/* TODO: remove this `dark` once we have a mode selector */}
<div className="border-solid border-gray-600 border-b pb-2 mb-4 flex">
<DatabaseIcon size={32} />
<h1 className="text-3xl font-bold pl-2">Query History</h1>
</div>
<main className="pt-4 mx-auto container">
<AdminPageTitle title="Query History" icon={<DatabaseIcon size={32} />} />
<QueryHistoryTable />
</main>
);