mirror of
https://github.com/danswer-ai/danswer.git
synced 2025-09-28 12:58:41 +02:00
Fix analytics + query history
This commit is contained in:
@@ -119,6 +119,11 @@ class AuthType(str, Enum):
|
|||||||
SAML = "saml"
|
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):
|
class SearchFeedbackType(str, Enum):
|
||||||
ENDORSE = "endorse" # boost this document for all future queries
|
ENDORSE = "endorse" # boost this document for all future queries
|
||||||
REJECT = "reject" # down-boost this document for all future queries
|
REJECT = "reject" # down-boost this document for all future queries
|
||||||
|
@@ -9,8 +9,10 @@ from sqlalchemy import func
|
|||||||
from sqlalchemy import select
|
from sqlalchemy import select
|
||||||
from sqlalchemy.orm import Session
|
from sqlalchemy.orm import Session
|
||||||
|
|
||||||
from danswer.configs.constants import QAFeedbackType
|
from danswer.configs.constants import MessageType
|
||||||
from danswer.db.models import QueryEvent
|
from danswer.db.models import ChatMessage
|
||||||
|
from danswer.db.models import ChatMessageFeedback
|
||||||
|
from danswer.db.models import ChatSession
|
||||||
|
|
||||||
|
|
||||||
def fetch_query_analytics(
|
def fetch_query_analytics(
|
||||||
@@ -20,19 +22,29 @@ def fetch_query_analytics(
|
|||||||
) -> Sequence[tuple[int, int, int, datetime.date]]:
|
) -> Sequence[tuple[int, int, int, datetime.date]]:
|
||||||
stmt = (
|
stmt = (
|
||||||
select(
|
select(
|
||||||
func.count(QueryEvent.id),
|
func.count(ChatMessage.id),
|
||||||
func.sum(case((QueryEvent.feedback == QAFeedbackType.LIKE, 1), else_=0)),
|
func.sum(case((ChatMessageFeedback.is_positive, 1), else_=0)),
|
||||||
func.sum(case((QueryEvent.feedback == QAFeedbackType.DISLIKE, 1), else_=0)),
|
func.sum(
|
||||||
cast(QueryEvent.time_created, Date),
|
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(
|
.where(
|
||||||
QueryEvent.time_created >= start,
|
ChatMessage.time_sent >= start,
|
||||||
)
|
)
|
||||||
.where(
|
.where(
|
||||||
QueryEvent.time_created <= end,
|
ChatMessage.time_sent <= end,
|
||||||
)
|
)
|
||||||
.group_by(cast(QueryEvent.time_created, Date))
|
.where(ChatMessage.message_type == MessageType.ASSISTANT)
|
||||||
.order_by(cast(QueryEvent.time_created, Date))
|
.group_by(cast(ChatMessage.time_sent, Date))
|
||||||
|
.order_by(cast(ChatMessage.time_sent, Date))
|
||||||
)
|
)
|
||||||
|
|
||||||
return db_session.execute(stmt).all() # type: ignore
|
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]]:
|
) -> Sequence[tuple[int, int, int, datetime.date, UUID]]:
|
||||||
stmt = (
|
stmt = (
|
||||||
select(
|
select(
|
||||||
func.count(QueryEvent.id),
|
func.count(ChatMessage.id),
|
||||||
func.sum(case((QueryEvent.feedback == QAFeedbackType.LIKE, 1), else_=0)),
|
func.sum(case((ChatMessageFeedback.is_positive, 1), else_=0)),
|
||||||
func.sum(case((QueryEvent.feedback == QAFeedbackType.DISLIKE, 1), else_=0)),
|
func.sum(
|
||||||
cast(QueryEvent.time_created, Date),
|
case(
|
||||||
QueryEvent.user_id,
|
(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(
|
.where(
|
||||||
QueryEvent.time_created >= start,
|
ChatMessage.time_sent <= end,
|
||||||
)
|
)
|
||||||
.where(
|
.where(ChatMessage.message_type == MessageType.ASSISTANT)
|
||||||
QueryEvent.time_created <= end,
|
.group_by(cast(ChatMessage.time_sent, Date), ChatSession.user_id)
|
||||||
)
|
.order_by(cast(ChatMessage.time_sent, Date), ChatSession.user_id)
|
||||||
.group_by(cast(QueryEvent.time_created, Date), QueryEvent.user_id)
|
|
||||||
.order_by(cast(QueryEvent.time_created, Date), QueryEvent.user_id)
|
|
||||||
)
|
)
|
||||||
|
|
||||||
return db_session.execute(stmt).all() # type: ignore
|
return db_session.execute(stmt).all() # type: ignore
|
||||||
|
@@ -10,34 +10,39 @@ from sqlalchemy.orm import aliased
|
|||||||
from sqlalchemy.orm import Session
|
from sqlalchemy.orm import Session
|
||||||
from sqlalchemy.orm.attributes import InstrumentedAttribute
|
from sqlalchemy.orm.attributes import InstrumentedAttribute
|
||||||
|
|
||||||
from danswer.configs.constants import QAFeedbackType
|
from danswer.configs.constants import MessageType
|
||||||
from danswer.db.models import QueryEvent
|
from danswer.db.models import ChatMessage
|
||||||
|
from danswer.db.models import ChatSession
|
||||||
from danswer.db.models import User
|
from danswer.db.models import User
|
||||||
|
|
||||||
SortByOptions = Literal["time_created", "feedback"]
|
SortByOptions = Literal["time_sent"]
|
||||||
|
|
||||||
|
|
||||||
def build_query_history_query(
|
def build_query_history_query(
|
||||||
start: datetime.datetime,
|
start: datetime.datetime,
|
||||||
end: datetime.datetime,
|
end: datetime.datetime,
|
||||||
query: str | None,
|
|
||||||
feedback_type: QAFeedbackType | None,
|
|
||||||
sort_by_field: SortByOptions,
|
sort_by_field: SortByOptions,
|
||||||
sort_by_direction: Literal["asc", "desc"],
|
sort_by_direction: Literal["asc", "desc"],
|
||||||
offset: int,
|
offset: int,
|
||||||
limit: int | None,
|
limit: int | None,
|
||||||
) -> Select[tuple[QueryEvent]]:
|
) -> Select[tuple[ChatMessage]]:
|
||||||
stmt = (
|
stmt = (
|
||||||
select(QueryEvent)
|
select(ChatMessage)
|
||||||
.where(
|
.where(
|
||||||
QueryEvent.time_created >= start,
|
ChatMessage.time_sent >= start,
|
||||||
)
|
)
|
||||||
.where(
|
.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":
|
if sort_by_direction == "asc":
|
||||||
stmt = stmt.order_by(order_by_field.asc())
|
stmt = stmt.order_by(order_by_field.asc())
|
||||||
else:
|
else:
|
||||||
@@ -48,17 +53,6 @@ def build_query_history_query(
|
|||||||
if limit:
|
if limit:
|
||||||
stmt = stmt.limit(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
|
return stmt
|
||||||
|
|
||||||
|
|
||||||
@@ -66,18 +60,14 @@ def fetch_query_history(
|
|||||||
db_session: Session,
|
db_session: Session,
|
||||||
start: datetime.datetime,
|
start: datetime.datetime,
|
||||||
end: datetime.datetime,
|
end: datetime.datetime,
|
||||||
query: str | None = None,
|
sort_by_field: SortByOptions = "time_sent",
|
||||||
feedback_type: QAFeedbackType | None = None,
|
|
||||||
sort_by_field: SortByOptions = "time_created",
|
|
||||||
sort_by_direction: Literal["asc", "desc"] = "desc",
|
sort_by_direction: Literal["asc", "desc"] = "desc",
|
||||||
offset: int = 0,
|
offset: int = 0,
|
||||||
limit: int | None = 500,
|
limit: int | None = 500,
|
||||||
) -> Sequence[QueryEvent]:
|
) -> Sequence[ChatMessage]:
|
||||||
stmt = build_query_history_query(
|
stmt = build_query_history_query(
|
||||||
start=start,
|
start=start,
|
||||||
end=end,
|
end=end,
|
||||||
query=query,
|
|
||||||
feedback_type=feedback_type,
|
|
||||||
sort_by_field=sort_by_field,
|
sort_by_field=sort_by_field,
|
||||||
sort_by_direction=sort_by_direction,
|
sort_by_direction=sort_by_direction,
|
||||||
offset=offset,
|
offset=offset,
|
||||||
@@ -91,27 +81,25 @@ def fetch_query_history_with_user_email(
|
|||||||
db_session: Session,
|
db_session: Session,
|
||||||
start: datetime.datetime,
|
start: datetime.datetime,
|
||||||
end: datetime.datetime,
|
end: datetime.datetime,
|
||||||
query: str | None = None,
|
sort_by_field: SortByOptions = "time_sent",
|
||||||
feedback_type: QAFeedbackType | None = None,
|
|
||||||
sort_by_field: SortByOptions = "time_created",
|
|
||||||
sort_by_direction: Literal["asc", "desc"] = "desc",
|
sort_by_direction: Literal["asc", "desc"] = "desc",
|
||||||
offset: int = 0,
|
offset: int = 0,
|
||||||
limit: int | None = 500,
|
limit: int | None = 500,
|
||||||
) -> Sequence[tuple[QueryEvent, str | None]]:
|
) -> Sequence[tuple[ChatMessage, str | None]]:
|
||||||
subquery = build_query_history_query(
|
subquery = build_query_history_query(
|
||||||
start=start,
|
start=start,
|
||||||
end=end,
|
end=end,
|
||||||
query=query,
|
|
||||||
feedback_type=feedback_type,
|
|
||||||
sort_by_field=sort_by_field,
|
sort_by_field=sort_by_field,
|
||||||
sort_by_direction=sort_by_direction,
|
sort_by_direction=sort_by_direction,
|
||||||
offset=offset,
|
offset=offset,
|
||||||
limit=limit,
|
limit=limit,
|
||||||
).subquery()
|
).subquery()
|
||||||
subquery_alias = aliased(QueryEvent, subquery)
|
subquery_alias = aliased(ChatMessage, subquery)
|
||||||
|
|
||||||
stmt_with_user_email = select(subquery_alias, User.email).join( # type: ignore
|
stmt_with_user_email = (
|
||||||
User, subquery_alias.user_id == User.id, isouter=True
|
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
|
return db_session.execute(stmt_with_user_email).all() # type: ignore
|
||||||
|
@@ -1,6 +1,6 @@
|
|||||||
import csv
|
import csv
|
||||||
import io
|
import io
|
||||||
from collections.abc import Iterable
|
from collections import defaultdict
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from datetime import timedelta
|
from datetime import timedelta
|
||||||
from datetime import timezone
|
from datetime import timezone
|
||||||
@@ -14,11 +14,11 @@ from sqlalchemy.orm import Session
|
|||||||
|
|
||||||
import danswer.db.models as db_models
|
import danswer.db.models as db_models
|
||||||
from danswer.auth.users import current_admin_user
|
from danswer.auth.users import current_admin_user
|
||||||
|
from danswer.configs.constants import MessageType
|
||||||
from danswer.configs.constants import QAFeedbackType
|
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.engine import get_session
|
||||||
from danswer.db.feedback import fetch_query_event_by_id
|
from danswer.db.models import ChatMessage
|
||||||
from danswer.db.models import Document
|
|
||||||
from ee.danswer.db.document import fetch_documents_from_ids
|
|
||||||
from ee.danswer.db.query_history import (
|
from ee.danswer.db.query_history import (
|
||||||
fetch_query_history_with_user_email,
|
fetch_query_history_with_user_email,
|
||||||
)
|
)
|
||||||
@@ -35,45 +35,112 @@ class AbridgedSearchDoc(BaseModel):
|
|||||||
link: str | None
|
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
|
id: int
|
||||||
user_email: str | None
|
user_email: str | None
|
||||||
query: str
|
name: str | None
|
||||||
llm_answer: str | None
|
messages: list[MessageSnapshot]
|
||||||
retrieved_documents: list[AbridgedSearchDoc]
|
|
||||||
feedback: QAFeedbackType | None
|
|
||||||
time_created: datetime
|
time_created: datetime
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def build(
|
def build(
|
||||||
cls,
|
cls,
|
||||||
query_event: db_models.QueryEvent,
|
messages: list[ChatMessage],
|
||||||
user_email: str | None,
|
) -> "ChatSessionSnapshot":
|
||||||
documents: Iterable[Document],
|
if len(messages) == 0:
|
||||||
) -> "QuerySnapshot":
|
raise ValueError("No messages provided")
|
||||||
|
|
||||||
|
chat_session = messages[0].chat_session
|
||||||
|
|
||||||
return cls(
|
return cls(
|
||||||
id=query_event.id,
|
id=chat_session.id,
|
||||||
user_email=user_email,
|
user_email=chat_session.user.email if chat_session.user else None,
|
||||||
query=query_event.query,
|
name=chat_session.description,
|
||||||
llm_answer=query_event.llm_answer,
|
messages=[
|
||||||
retrieved_documents=[
|
MessageSnapshot.build(message)
|
||||||
AbridgedSearchDoc(
|
for message in sorted(messages, key=lambda m: m.time_sent)
|
||||||
document_id=document.id,
|
if message.message_type != MessageType.SYSTEM
|
||||||
semantic_identifier=document.semantic_id,
|
|
||||||
link=document.link,
|
|
||||||
)
|
|
||||||
for document in documents
|
|
||||||
],
|
],
|
||||||
feedback=query_event.feedback,
|
time_created=chat_session.time_created,
|
||||||
time_created=query_event.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]:
|
def to_json(self) -> dict[str, str]:
|
||||||
return {
|
return {
|
||||||
"id": str(self.id),
|
"user_message": self.user_message,
|
||||||
"query": self.query,
|
"ai_response": self.ai_response,
|
||||||
"user_email": self.user_email or "",
|
|
||||||
"llm_answer": self.llm_answer or "",
|
|
||||||
"retrieved_documents": "|".join(
|
"retrieved_documents": "|".join(
|
||||||
[
|
[
|
||||||
doc.link or doc.semantic_identifier
|
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,
|
db_session: Session,
|
||||||
start: datetime | None,
|
start: datetime | None,
|
||||||
end: datetime | None,
|
end: datetime | None,
|
||||||
feedback_type: QAFeedbackType | None,
|
feedback_type: QAFeedbackType | None,
|
||||||
limit: int | None = 500,
|
limit: int | None = 500,
|
||||||
) -> list[QuerySnapshot]:
|
) -> list[ChatSessionSnapshot]:
|
||||||
query_history_with_user_email = fetch_query_history_with_user_email(
|
chat_messages_with_user_email = fetch_query_history_with_user_email(
|
||||||
db_session=db_session,
|
db_session=db_session,
|
||||||
start=start
|
start=start
|
||||||
or (
|
or (
|
||||||
datetime.now(tz=timezone.utc) - timedelta(days=30)
|
datetime.now(tz=timezone.utc) - timedelta(days=30)
|
||||||
), # default is 30d lookback
|
), # default is 30d lookback
|
||||||
end=end or datetime.now(tz=timezone.utc),
|
end=end or datetime.now(tz=timezone.utc),
|
||||||
feedback_type=feedback_type,
|
|
||||||
limit=limit,
|
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()
|
chat_session_snapshots = [
|
||||||
for query_event, _ in query_history_with_user_email:
|
ChatSessionSnapshot.build(messages)
|
||||||
all_relevant_document_ids = all_relevant_document_ids.union(
|
for messages in session_id_to_messages.values()
|
||||||
query_event.retrieved_document_ids or []
|
]
|
||||||
)
|
if feedback_type:
|
||||||
document_id_to_document = {
|
chat_session_snapshots = [
|
||||||
document.id: document
|
chat_session_snapshot
|
||||||
for document in fetch_documents_from_ids(
|
for chat_session_snapshot in chat_session_snapshots
|
||||||
db_session, list(all_relevant_document_ids)
|
if any(
|
||||||
)
|
message.feedback == feedback_type
|
||||||
}
|
for message in chat_session_snapshot.messages
|
||||||
|
|
||||||
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
|
|
||||||
)
|
)
|
||||||
)
|
]
|
||||||
return query_snapshots
|
|
||||||
|
return chat_session_snapshots
|
||||||
|
|
||||||
|
|
||||||
@router.get("/admin/query-history")
|
@router.get("/admin/chat-session-history")
|
||||||
def get_query_history(
|
def get_chat_session_history(
|
||||||
feedback_type: QAFeedbackType | None = None,
|
feedback_type: QAFeedbackType | None = None,
|
||||||
start: datetime | None = None,
|
start: datetime | None = None,
|
||||||
end: datetime | None = None,
|
end: datetime | None = None,
|
||||||
_: db_models.User | None = Depends(current_admin_user),
|
_: db_models.User | None = Depends(current_admin_user),
|
||||||
db_session: Session = Depends(get_session),
|
db_session: Session = Depends(get_session),
|
||||||
) -> list[QuerySnapshot]:
|
) -> list[ChatSessionSnapshot]:
|
||||||
return fetch_and_process_query_history(
|
return fetch_and_process_chat_session_history(
|
||||||
db_session=db_session,
|
db_session=db_session,
|
||||||
start=start,
|
start=start,
|
||||||
end=end,
|
end=end,
|
||||||
@@ -147,24 +205,22 @@ def get_query_history(
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@router.get("/admin/query-history/{query_id}")
|
@router.get("/admin/chat-session-history/{chat_session_id}")
|
||||||
def get_query(
|
def get_chat_session(
|
||||||
query_id: int,
|
chat_session_id: int,
|
||||||
_: db_models.User | None = Depends(current_admin_user),
|
_: db_models.User | None = Depends(current_admin_user),
|
||||||
db_session: Session = Depends(get_session),
|
db_session: Session = Depends(get_session),
|
||||||
) -> QuerySnapshot:
|
) -> ChatSessionSnapshot:
|
||||||
try:
|
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:
|
except ValueError:
|
||||||
raise HTTPException(400, f"Query event with id '{query_id}' does not exist.")
|
raise HTTPException(
|
||||||
documents = fetch_documents_from_ids(
|
400, f"Chat session with id '{chat_session_id}' does not exist."
|
||||||
db_session, query_event.retrieved_document_ids or []
|
)
|
||||||
)
|
|
||||||
return QuerySnapshot.build(
|
return ChatSessionSnapshot.build(messages=chat_session.messages)
|
||||||
query_event=query_event,
|
|
||||||
user_email=query_event.user.email if query_event.user else None,
|
|
||||||
documents=documents,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
@router.get("/admin/query-history-csv")
|
@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_models.User | None = Depends(current_admin_user),
|
||||||
db_session: Session = Depends(get_session),
|
db_session: Session = Depends(get_session),
|
||||||
) -> StreamingResponse:
|
) -> StreamingResponse:
|
||||||
complete_query_history = fetch_and_process_query_history(
|
complete_chat_session_history = fetch_and_process_chat_session_history(
|
||||||
db_session=db_session,
|
db_session=db_session,
|
||||||
start=datetime.fromtimestamp(0, tz=timezone.utc),
|
start=datetime.fromtimestamp(0, tz=timezone.utc),
|
||||||
end=datetime.now(tz=timezone.utc),
|
end=datetime.now(tz=timezone.utc),
|
||||||
@@ -180,11 +236,19 @@ def get_query_history_as_csv(
|
|||||||
limit=None,
|
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
|
# Create an in-memory text stream
|
||||||
stream = io.StringIO()
|
stream = io.StringIO()
|
||||||
writer = csv.DictWriter(stream, fieldnames=list(QuerySnapshot.__fields__.keys()))
|
writer = csv.DictWriter(
|
||||||
|
stream, fieldnames=list(QuestionAnswerPairSnapshot.__fields__.keys())
|
||||||
|
)
|
||||||
writer.writeheader()
|
writer.writeheader()
|
||||||
for row in complete_query_history:
|
for row in question_answer_pairs:
|
||||||
writer.writerow(row.to_json())
|
writer.writerow(row.to_json())
|
||||||
|
|
||||||
# Reset the stream's position to the start
|
# Reset the stream's position to the start
|
||||||
|
@@ -67,7 +67,9 @@ export const UserEditor = ({
|
|||||||
})}
|
})}
|
||||||
onSelect={(option) => {
|
onSelect={(option) => {
|
||||||
setSelectedUserIds([
|
setSelectedUserIds([
|
||||||
...Array.from(new Set([...selectedUserIds, option.value as string])),
|
...Array.from(
|
||||||
|
new Set([...selectedUserIds, option.value as string])
|
||||||
|
),
|
||||||
]);
|
]);
|
||||||
}}
|
}}
|
||||||
itemComponent={({ option }) => (
|
itemComponent={({ option }) => (
|
||||||
|
@@ -91,7 +91,10 @@ export const AddConnectorForm: React.FC<AddConnectorFormProps> = ({
|
|||||||
onSelect={(option) => {
|
onSelect={(option) => {
|
||||||
setSelectedCCPairIds([
|
setSelectedCCPairIds([
|
||||||
...Array.from(
|
...Array.from(
|
||||||
new Set([...selectedCCPairIds, parseInt(option.value as string)])
|
new Set([
|
||||||
|
...selectedCCPairIds,
|
||||||
|
parseInt(option.value as string),
|
||||||
|
])
|
||||||
),
|
),
|
||||||
]);
|
]);
|
||||||
}}
|
}}
|
||||||
|
@@ -2,6 +2,7 @@ import {
|
|||||||
DateRangePicker,
|
DateRangePicker,
|
||||||
DateRangePickerItem,
|
DateRangePickerItem,
|
||||||
DateRangePickerValue,
|
DateRangePickerValue,
|
||||||
|
Text,
|
||||||
} from "@tremor/react";
|
} from "@tremor/react";
|
||||||
import { getXDaysAgo } from "./dateUtils";
|
import { getXDaysAgo } from "./dateUtils";
|
||||||
|
|
||||||
@@ -16,9 +17,7 @@ export function DateRangeSelector({
|
|||||||
}) {
|
}) {
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<div className="text-sm my-auto mr-2 font-medium text-gray-200 mb-1">
|
<Text className="my-auto mr-2 font-medium mb-1">Date Range</Text>
|
||||||
Date Range
|
|
||||||
</div>
|
|
||||||
<DateRangePicker
|
<DateRangePicker
|
||||||
className="max-w-md"
|
className="max-w-md"
|
||||||
value={value}
|
value={value}
|
||||||
|
@@ -5,17 +5,15 @@ import { FeedbackChart } from "./FeedbackChart";
|
|||||||
import { QueryPerformanceChart } from "./QueryPerformanceChart";
|
import { QueryPerformanceChart } from "./QueryPerformanceChart";
|
||||||
import { BarChartIcon } from "@/components/icons/icons";
|
import { BarChartIcon } from "@/components/icons/icons";
|
||||||
import { useTimeRange } from "../lib";
|
import { useTimeRange } from "../lib";
|
||||||
|
import { AdminPageTitle } from "@/components/admin/Title";
|
||||||
|
|
||||||
export default function AnalyticsPage() {
|
export default function AnalyticsPage() {
|
||||||
const [timeRange, setTimeRange] = useTimeRange();
|
const [timeRange, setTimeRange] = useTimeRange();
|
||||||
|
|
||||||
return (
|
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 */}
|
{/* TODO: remove this `dark` once we have a mode selector */}
|
||||||
<div className="border-solid border-gray-600 border-b pb-2 mb-4 flex">
|
<AdminPageTitle title="Analytics" icon={<BarChartIcon size={32} />} />
|
||||||
<BarChartIcon size={32} />
|
|
||||||
<h1 className="text-3xl font-bold pl-2">Analytics</h1>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<DateRangeSelector value={timeRange} onValueChange={setTimeRange} />
|
<DateRangeSelector value={timeRange} onValueChange={setTimeRange} />
|
||||||
|
|
||||||
|
@@ -18,12 +18,18 @@ export interface AbridgedSearchDoc {
|
|||||||
link: string | null;
|
link: string | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface QuerySnapshot {
|
export interface MessageSnapshot {
|
||||||
id: number;
|
message: string;
|
||||||
query: string;
|
message_type: "user" | "assistant";
|
||||||
user_email: string | null;
|
documents: AbridgedSearchDoc[];
|
||||||
llm_answer: string;
|
|
||||||
retrieved_documents: AbridgedSearchDoc[];
|
|
||||||
time_created: string;
|
|
||||||
feedback: Feedback | null;
|
feedback: Feedback | null;
|
||||||
|
time_created: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ChatSessionSnapshot {
|
||||||
|
id: number;
|
||||||
|
user_email: string | null;
|
||||||
|
name: string | null;
|
||||||
|
messages: MessageSnapshot[];
|
||||||
|
time_created: string;
|
||||||
}
|
}
|
||||||
|
@@ -1,8 +1,8 @@
|
|||||||
import { errorHandlingFetcher } from "@/lib/fetcher";
|
import { errorHandlingFetcher } from "@/lib/fetcher";
|
||||||
import useSWR, { mutate } from "swr";
|
import useSWR, { mutate } from "swr";
|
||||||
import {
|
import {
|
||||||
|
ChatSessionSnapshot,
|
||||||
QueryAnalytics,
|
QueryAnalytics,
|
||||||
QuerySnapshot,
|
|
||||||
UserAnalytics,
|
UserAnalytics,
|
||||||
} from "./analytics/types";
|
} from "./analytics/types";
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
@@ -55,12 +55,12 @@ export const useQueryHistory = () => {
|
|||||||
useState<Feedback | null>(null);
|
useState<Feedback | null>(null);
|
||||||
const [timeRange, setTimeRange] = useTimeRange();
|
const [timeRange, setTimeRange] = useTimeRange();
|
||||||
|
|
||||||
const url = buildApiPath("/api/admin/query-history", {
|
const url = buildApiPath("/api/admin/chat-session-history", {
|
||||||
feedback_type: selectedFeedbackType,
|
feedback_type: selectedFeedbackType,
|
||||||
start: convertDateToStartOfDay(timeRange.from)?.toISOString(),
|
start: convertDateToStartOfDay(timeRange.from)?.toISOString(),
|
||||||
end: convertDateToEndOfDay(timeRange.to)?.toISOString(),
|
end: convertDateToEndOfDay(timeRange.to)?.toISOString(),
|
||||||
});
|
});
|
||||||
const swrResponse = useSWR<QuerySnapshot[]>(url, errorHandlingFetcher);
|
const swrResponse = useSWR<ChatSessionSnapshot[]>(url, errorHandlingFetcher);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
...swrResponse,
|
...swrResponse,
|
||||||
|
@@ -4,7 +4,7 @@ export function DownloadAsCSV() {
|
|||||||
return (
|
return (
|
||||||
<a
|
<a
|
||||||
href="/api/admin/query-history-csv"
|
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" />
|
<FiDownload className="my-auto mr-2" />
|
||||||
Download as CSV
|
Download as CSV
|
||||||
|
@@ -1,7 +1,11 @@
|
|||||||
import { Feedback } from "@/lib/types";
|
import { Feedback } from "@/lib/types";
|
||||||
import { Badge } from "@tremor/react";
|
import { Badge } from "@tremor/react";
|
||||||
|
|
||||||
export function FeedbackBadge({ feedback }: { feedback?: Feedback | null }) {
|
export function FeedbackBadge({
|
||||||
|
feedback,
|
||||||
|
}: {
|
||||||
|
feedback?: Feedback | "mixed" | null;
|
||||||
|
}) {
|
||||||
let feedbackBadge;
|
let feedbackBadge;
|
||||||
switch (feedback) {
|
switch (feedback) {
|
||||||
case "like":
|
case "like":
|
||||||
@@ -18,6 +22,13 @@ export function FeedbackBadge({ feedback }: { feedback?: Feedback | null }) {
|
|||||||
</Badge>
|
</Badge>
|
||||||
);
|
);
|
||||||
break;
|
break;
|
||||||
|
case "mixed":
|
||||||
|
feedbackBadge = (
|
||||||
|
<Badge color="purple" className="text-sm">
|
||||||
|
Mixed
|
||||||
|
</Badge>
|
||||||
|
);
|
||||||
|
break;
|
||||||
default:
|
default:
|
||||||
feedbackBadge = (
|
feedbackBadge = (
|
||||||
<Badge color="gray" className="text-sm">
|
<Badge color="gray" className="text-sm">
|
||||||
|
@@ -13,12 +13,9 @@ import {
|
|||||||
import { Divider } from "@tremor/react";
|
import { Divider } from "@tremor/react";
|
||||||
import { Select, SelectItem } from "@tremor/react";
|
import { Select, SelectItem } from "@tremor/react";
|
||||||
import { ThreeDotsLoader } from "@/components/Loading";
|
import { ThreeDotsLoader } from "@/components/Loading";
|
||||||
import { QuerySnapshot } from "../analytics/types";
|
import { ChatSessionSnapshot } from "../analytics/types";
|
||||||
import {
|
import { timestampToReadableDate } from "@/lib/dateUtils";
|
||||||
timestampToDateString,
|
import { FiFrown, FiMinus, FiSmile } from "react-icons/fi";
|
||||||
timestampToReadableDate,
|
|
||||||
} from "@/lib/dateUtils";
|
|
||||||
import { FiBook, FiFrown, FiMinus, FiSmile } from "react-icons/fi";
|
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { Feedback } from "@/lib/types";
|
import { Feedback } from "@/lib/types";
|
||||||
import { DateRangeSelector } from "../DateRangeSelector";
|
import { DateRangeSelector } from "../DateRangeSelector";
|
||||||
@@ -30,42 +27,47 @@ import { DownloadAsCSV } from "./DownloadAsCSV";
|
|||||||
const NUM_IN_PAGE = 20;
|
const NUM_IN_PAGE = 20;
|
||||||
|
|
||||||
function QueryHistoryTableRow({
|
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 (
|
return (
|
||||||
<TableRow
|
<TableRow
|
||||||
key={querySnapshot.id}
|
key={chatSessionSnapshot.id}
|
||||||
className="hover:bg-gradient-to-r hover:from-gray-800 hover:to-indigo-950 cursor-pointer relative"
|
className="hover:bg-hover-light cursor-pointer relative"
|
||||||
>
|
>
|
||||||
<TableCell>{querySnapshot.query}</TableCell>
|
|
||||||
<TableCell>
|
<TableCell>
|
||||||
<Text className="whitespace-normal line-clamp-5">
|
<Text className="whitespace-normal line-clamp-5">
|
||||||
{querySnapshot.llm_answer}
|
{chatSessionSnapshot.messages[0]?.message || "-"}
|
||||||
</Text>
|
</Text>
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell>
|
<TableCell>
|
||||||
{querySnapshot.retrieved_documents.slice(0, 5).map((document) => (
|
<Text className="whitespace-normal line-clamp-5">
|
||||||
<div className="flex" key={document.document_id}>
|
{chatSessionSnapshot.messages[1]?.message || "-"}
|
||||||
<FiBook className="my-auto mr-1" />{" "}
|
</Text>
|
||||||
<p className="max-w-xs text-ellipsis overflow-hidden">
|
|
||||||
{document.semantic_identifier}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell>
|
<TableCell>
|
||||||
<FeedbackBadge feedback={querySnapshot.feedback} />
|
<FeedbackBadge feedback={finalFeedback} />
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell>{querySnapshot.user_email || "-"}</TableCell>
|
<TableCell>{chatSessionSnapshot.user_email || "-"}</TableCell>
|
||||||
<TableCell>
|
<TableCell>
|
||||||
{timestampToReadableDate(querySnapshot.time_created)}
|
{timestampToReadableDate(chatSessionSnapshot.time_created)}
|
||||||
</TableCell>
|
</TableCell>
|
||||||
{/* Wrapping in <td> to avoid console warnings */}
|
{/* Wrapping in <td> to avoid console warnings */}
|
||||||
<td className="w-0 p-0">
|
<td className="w-0 p-0">
|
||||||
<Link
|
<Link
|
||||||
href={`/admin/performance/query-history/${querySnapshot.id}`}
|
href={`/admin/performance/query-history/${chatSessionSnapshot.id}`}
|
||||||
className="absolute w-full h-full left-0"
|
className="absolute w-full h-full left-0"
|
||||||
></Link>
|
></Link>
|
||||||
</td>
|
</td>
|
||||||
@@ -82,9 +84,7 @@ function SelectFeedbackType({
|
|||||||
}) {
|
}) {
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<div className="text-sm my-auto mr-2 font-medium text-gray-200 mb-1">
|
<Text className="my-auto mr-2 font-medium mb-1">Feedback Type</Text>
|
||||||
Feedback Type
|
|
||||||
</div>
|
|
||||||
<div className="max-w-sm space-y-6">
|
<div className="max-w-sm space-y-6">
|
||||||
<Select
|
<Select
|
||||||
value={value}
|
value={value}
|
||||||
@@ -108,7 +108,7 @@ function SelectFeedbackType({
|
|||||||
|
|
||||||
export function QueryHistoryTable() {
|
export function QueryHistoryTable() {
|
||||||
const {
|
const {
|
||||||
data: queryHistoryData,
|
data: chatSessionData,
|
||||||
selectedFeedbackType,
|
selectedFeedbackType,
|
||||||
setSelectedFeedbackType,
|
setSelectedFeedbackType,
|
||||||
timeRange,
|
timeRange,
|
||||||
@@ -119,7 +119,7 @@ export function QueryHistoryTable() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Card className="mt-8">
|
<Card className="mt-8">
|
||||||
{queryHistoryData ? (
|
{chatSessionData ? (
|
||||||
<>
|
<>
|
||||||
<div className="flex">
|
<div className="flex">
|
||||||
<div className="gap-y-3 flex flex-col">
|
<div className="gap-y-3 flex flex-col">
|
||||||
@@ -140,21 +140,20 @@ export function QueryHistoryTable() {
|
|||||||
<Table className="mt-5">
|
<Table className="mt-5">
|
||||||
<TableHead>
|
<TableHead>
|
||||||
<TableRow>
|
<TableRow>
|
||||||
<TableHeaderCell>Query</TableHeaderCell>
|
<TableHeaderCell>First User Message</TableHeaderCell>
|
||||||
<TableHeaderCell>LLM Answer</TableHeaderCell>
|
<TableHeaderCell>First AI Response</TableHeaderCell>
|
||||||
<TableHeaderCell>Retrieved Documents</TableHeaderCell>
|
|
||||||
<TableHeaderCell>Feedback</TableHeaderCell>
|
<TableHeaderCell>Feedback</TableHeaderCell>
|
||||||
<TableHeaderCell>User</TableHeaderCell>
|
<TableHeaderCell>User</TableHeaderCell>
|
||||||
<TableHeaderCell>Date</TableHeaderCell>
|
<TableHeaderCell>Date</TableHeaderCell>
|
||||||
</TableRow>
|
</TableRow>
|
||||||
</TableHead>
|
</TableHead>
|
||||||
<TableBody>
|
<TableBody>
|
||||||
{queryHistoryData
|
{chatSessionData
|
||||||
.slice(NUM_IN_PAGE * (page - 1), NUM_IN_PAGE * page)
|
.slice(NUM_IN_PAGE * (page - 1), NUM_IN_PAGE * page)
|
||||||
.map((querySnapshot) => (
|
.map((chatSessionSnapshot) => (
|
||||||
<QueryHistoryTableRow
|
<QueryHistoryTableRow
|
||||||
key={querySnapshot.id}
|
key={chatSessionSnapshot.id}
|
||||||
querySnapshot={querySnapshot}
|
chatSessionSnapshot={chatSessionSnapshot}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
</TableBody>
|
</TableBody>
|
||||||
@@ -163,7 +162,7 @@ export function QueryHistoryTable() {
|
|||||||
<div className="mt-3 flex">
|
<div className="mt-3 flex">
|
||||||
<div className="mx-auto">
|
<div className="mx-auto">
|
||||||
<PageSelector
|
<PageSelector
|
||||||
totalPages={Math.ceil(queryHistoryData.length / NUM_IN_PAGE)}
|
totalPages={Math.ceil(chatSessionData.length / NUM_IN_PAGE)}
|
||||||
currentPage={page}
|
currentPage={page}
|
||||||
onPageChange={(newPage) => {
|
onPageChange={(newPage) => {
|
||||||
setPage(newPage);
|
setPage(newPage);
|
||||||
|
@@ -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>
|
|
||||||
);
|
|
||||||
}
|
|
@@ -1,82 +1,81 @@
|
|||||||
import { Bold, Text, Card, Title, Divider, Italic } from "@tremor/react";
|
import { Bold, Text, Card, Title, Divider } from "@tremor/react";
|
||||||
import { QuerySnapshot } from "../../analytics/types";
|
import { ChatSessionSnapshot, MessageSnapshot } from "../../analytics/types";
|
||||||
import { buildUrl } from "@/lib/utilsSS";
|
|
||||||
import { BackButton } from "./BackButton";
|
|
||||||
import { FiBook } from "react-icons/fi";
|
import { FiBook } from "react-icons/fi";
|
||||||
import { processCookies } from "@/lib/userSS";
|
|
||||||
import { cookies } from "next/headers";
|
|
||||||
import { timestampToReadableDate } from "@/lib/dateUtils";
|
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({
|
export default async function QueryPage({
|
||||||
params,
|
params,
|
||||||
}: {
|
}: {
|
||||||
params: { id: string };
|
params: { id: string };
|
||||||
}) {
|
}) {
|
||||||
const response = await fetch(buildUrl(`/admin/query-history/${params.id}`), {
|
const response = await fetchSS(`/admin/chat-session-history/${params.id}`);
|
||||||
next: { revalidate: 0 },
|
const chatSessionSnapshot = (await response.json()) as ChatSessionSnapshot;
|
||||||
headers: {
|
|
||||||
cookie: processCookies(cookies()),
|
|
||||||
},
|
|
||||||
});
|
|
||||||
const queryEvent = (await response.json()) as QuerySnapshot;
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<main className="pt-4 mx-auto container dark">
|
<main className="pt-4 mx-auto container">
|
||||||
<BackButton />
|
<BackButton />
|
||||||
|
<SSRAutoRefresh />
|
||||||
|
|
||||||
<Card className="mt-4">
|
<Card className="mt-4">
|
||||||
<Title>Query Details</Title>
|
<Title>Chat Session Details</Title>
|
||||||
|
|
||||||
<Text className="flex flex-wrap whitespace-normal mt-1 text-xs">
|
<Text className="flex flex-wrap whitespace-normal mt-1 text-xs">
|
||||||
{queryEvent.user_email || "-"},{" "}
|
{chatSessionSnapshot.user_email || "-"},{" "}
|
||||||
{timestampToReadableDate(queryEvent.time_created)}
|
{timestampToReadableDate(chatSessionSnapshot.time_created)}
|
||||||
</Text>
|
</Text>
|
||||||
|
|
||||||
<Divider />
|
<Divider />
|
||||||
|
|
||||||
<div className="flex flex-col gap-y-3">
|
<div className="flex flex-col">
|
||||||
<div>
|
{chatSessionSnapshot.messages.map((message) => {
|
||||||
<Bold>Query</Bold>
|
return (
|
||||||
<Text className="flex flex-wrap whitespace-normal mt-1">
|
<MessageDisplay key={message.time_created} message={message} />
|
||||||
{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>
|
</div>
|
||||||
</Card>
|
</Card>
|
||||||
</main>
|
</main>
|
||||||
|
@@ -1,16 +1,15 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
|
import { AdminPageTitle } from "@/components/admin/Title";
|
||||||
import { QueryHistoryTable } from "./QueryHistoryTable";
|
import { QueryHistoryTable } from "./QueryHistoryTable";
|
||||||
import { DatabaseIcon } from "@/components/icons/icons";
|
import { DatabaseIcon } from "@/components/icons/icons";
|
||||||
|
|
||||||
export default function QueryHistoryPage() {
|
export default function QueryHistoryPage() {
|
||||||
return (
|
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">
|
<AdminPageTitle title="Query History" icon={<DatabaseIcon size={32} />} />
|
||||||
<DatabaseIcon size={32} />
|
|
||||||
<h1 className="text-3xl font-bold pl-2">Query History</h1>
|
|
||||||
</div>
|
|
||||||
<QueryHistoryTable />
|
<QueryHistoryTable />
|
||||||
</main>
|
</main>
|
||||||
);
|
);
|
||||||
|
Reference in New Issue
Block a user