Compare commits

...

9 Commits

Author SHA1 Message Date
amohamdy99
d59c60279f minor: addressing comments 2024-06-17 15:45:23 -07:00
amohamdy99
c98a5f9408 generate report button downloads file 2024-06-17 12:21:16 -07:00
amohamdy99
9eb7847912 cleanup: removd telemetry directory 2024-06-17 11:25:38 -07:00
amohamdy99
0a348f5ea6 zip reports 2024-06-17 11:14:41 -07:00
amohamdy99
76d0e8bbf9 minor: ux 2024-06-17 09:55:58 -07:00
amohamdy99
9a60b74a49 added chat sessions report generation 2024-06-17 00:37:02 -07:00
amohamdy99
ba9be884db save chat_messages into file_store as <report_id>_chat_messages 2024-06-16 22:04:10 -07:00
amohamdy99
9dceb511cb basic telemetry routes 2024-06-16 18:39:50 -07:00
amohamdy99
4fef8b74ad telemetry report page skeleton 2024-06-16 18:39:50 -07:00
6 changed files with 379 additions and 0 deletions

View File

@@ -0,0 +1,89 @@
import datetime
from pydantic import BaseModel
from sqlalchemy import select
from sqlalchemy.orm import Session
from danswer.configs.constants import MessageType
from danswer.db.models import ChatMessage
from danswer.db.models import ChatSession
class ChatMessageSkeleton(BaseModel):
message_id: int
chat_session_id: int
time_sent: datetime.datetime
def __init__(
self, message_id: int, chat_session_id: int, time_sent: datetime.datetime
) -> None:
self.message_id = message_id
self.chat_session_id = chat_session_id
self.time_sent = time_sent
# Gets skeletons of all message
# TODO: should change this to use a key index with dates
# TODO: Need to paginate as well
def get_empty_chat_messages_entries(
db_session: Session,
) -> list[ChatMessageSkeleton]:
stmt = select(
ChatMessage.id, ChatMessage.chat_session_id, ChatMessage.time_sent
).where(ChatMessage.message_type == MessageType.USER)
result = db_session.execute(stmt).all()
return [
ChatMessageSkeleton(
message_id=m.id, chat_session_id=m.chat_session_id, time_sent=m.time_sent
)
for m in result
]
class ChatSessionSkeleton(BaseModel):
session_id: int
user_id: int
one_shot: bool
time_created: datetime.datetime
time_updated: datetime.datetime
def __init__(
self,
session_id: int,
user_id: int,
one_shot: bool,
time_created: datetime.datetime,
time_updated: datetime.datetime,
) -> None:
self.session_id = session_id
self.user_id = user_id
self.one_shot = one_shot
self.time_created = time_created
self.time_updated = time_updated
def get_chat_sessions_skeleton(
db_session: Session,
) -> list[ChatSessionSkeleton]:
stmt = select(
ChatSession.id,
ChatSession.user_id,
ChatSession.one_shot,
ChatSession.time_created,
ChatSession.time_updated,
)
result = db_session.execute(stmt).all()
return [
ChatSessionSkeleton(
session_id=s.id,
user_id=s.user_id,
one_shot=s.one_shot,
time_created=s.time_created,
time_updated=s.time_updated,
)
for s in result
]

View File

@@ -71,6 +71,7 @@ from danswer.server.manage.llm.api import admin_router as llm_admin_router
from danswer.server.manage.llm.api import basic_router as llm_router
from danswer.server.manage.secondary_index import router as secondary_index_router
from danswer.server.manage.slack_bot import router as slack_bot_management_router
from danswer.server.manage.telemetry import router as telemetry_router
from danswer.server.manage.users import router as user_router
from danswer.server.middleware.latency_logging import add_latency_logging_middleware
from danswer.server.query_and_chat.chat_backend import router as chat_router
@@ -264,6 +265,7 @@ def get_application() -> FastAPI:
include_router_with_global_prefix_prepended(application, admin_query_router)
include_router_with_global_prefix_prepended(application, admin_router)
include_router_with_global_prefix_prepended(application, user_router)
include_router_with_global_prefix_prepended(application, telemetry_router)
include_router_with_global_prefix_prepended(application, connector_router)
include_router_with_global_prefix_prepended(application, credential_router)
include_router_with_global_prefix_prepended(application, cc_pair_router)

View File

@@ -0,0 +1,105 @@
import csv
import io
import uuid
import zipfile
from fastapi import APIRouter
from fastapi import Depends
from fastapi import Response
from sqlalchemy.orm import Session
from danswer.auth.users import current_admin_user
from danswer.configs.constants import FileOrigin
from danswer.db.chat import get_chat_sessions_skeleton
from danswer.db.chat import get_empty_chat_messages_entries
from danswer.db.engine import get_session
from danswer.db.models import User
from danswer.file_store.file_store import FileStore
from danswer.file_store.file_store import get_default_file_store
router = APIRouter()
# TODO: can probably merge these helpers into one and use __annotations__ to get fields and headers
def generate_chat_messages_report(
db_session: Session, file_store: FileStore, report_id: str
) -> str:
file_name = f"{report_id}_chat_messages"
messages = get_empty_chat_messages_entries(db_session)
# write to memory buffer and store using pg_file_store
with io.StringIO() as csvbuf:
csvwriter = csv.writer(csvbuf, delimiter=",")
csvwriter.writerow(["message_id", "chat_session_id", "time_sent"])
for m in messages:
csvwriter.writerow([m.message_id, m.chat_session_id, m.time_sent])
# after writing seek to begining of buffer
csvbuf.seek(0)
file_store.save_file(
file_name=file_name,
content=csvbuf,
# content=csvbuf,
display_name=file_name,
file_origin=FileOrigin.OTHER,
file_type="text/csv",
)
return file_name
def generate_chat_sessions_report(
db_session: Session, file_store: FileStore, report_id: str
) -> str:
file_name = f"{report_id}_chat_sessions"
sessions = get_chat_sessions_skeleton(db_session)
# write to memory buffer and store using pg_file_store
with io.StringIO() as csvbuf:
csvwriter = csv.writer(csvbuf, delimiter=",")
csvwriter.writerow(
["session_id", "user_id", "one_shot", "time_created", "time_updated"]
)
for s in sessions:
csvwriter.writerow(
[s.session_id, s.user_id, s.one_shot, s.time_created, s.time_updated]
)
# after writing seek to begining of buffer
csvbuf.seek(0)
file_store.save_file(
file_name=file_name,
content=csvbuf,
display_name=file_name,
file_origin=FileOrigin.OTHER,
file_type="text/csv",
)
return file_name
@router.post("/admin/generate-usage-report")
async def generate_report(
_: User | None = Depends(current_admin_user),
db_session: Session = Depends(get_session),
) -> Response:
report_id = str(uuid.uuid4())
file_store = get_default_file_store(db_session)
messages_filename = generate_chat_messages_report(db_session, file_store, report_id)
sessions_filename = generate_chat_sessions_report(db_session, file_store, report_id)
zipBuffer = io.BytesIO()
with zipfile.ZipFile(zipBuffer, "a", zipfile.ZIP_DEFLATED) as zipFile:
# write messages
zipFile.writestr(
"chat_messages.csv",
file_store.read_file(messages_filename, mode="b").read(),
)
zipFile.writestr(
"chat_sessions.csv",
file_store.read_file(sessions_filename, mode="b").read(),
)
zipBuffer.seek(0)
return Response(zipBuffer.read(), media_type="application/zip")

View File

@@ -0,0 +1 @@
export interface UsageReport {}

View File

@@ -0,0 +1,167 @@
"use client";
import { AdminPageTitle } from "@/components/admin/Title";
// import { UsageReport } from "./interfaces";
import { FiActivity, FiDownloadCloud } from "react-icons/fi";
import {
Callout,
DateRangePicker,
DateRangePickerItem,
Divider,
Table,
TableHead,
TableHeaderCell,
TableRow,
Text,
Title,
} from "@tremor/react";
import { Button } from "@tremor/react";
import { useTimeRange } from "@/lib/hooks";
import { useSWRConfig } from "swr";
function GenerateReportInput() {
const [_, setDateRange] = useTimeRange();
// const onSubmit = useEffect(
// const {
// data: usageReportData,
// isLoading: usageReportLoading,
// error: usageReportError,
// } = useSWR<UsageReport>(
// '/admin/generate-usage-report'
// )
// )
const requestReport = () => {
console.log("Requesting Report");
fetch("/api/admin/generate-usage-report", {
method: "POST",
credentials: "include",
})
.then((transfer) => {
return transfer.blob();
})
.then((bytes) => {
let elm = document.createElement("a");
elm.href = URL.createObjectURL(bytes);
elm.setAttribute("download", "usage_reports.zip");
elm.click();
})
.catch((err) => {
console.log(err);
});
};
const lastMonth = new Date();
lastMonth.setMonth(lastMonth.getMonth() - 1);
const lastYear = new Date();
lastYear.setFullYear(lastYear.getFullYear() - 1);
return (
<div className="mb-8">
<Title className="mb-6">Generate Usage Reports</Title>
<DateRangePicker
maxDate={new Date()}
defaultValue={{ selectValue: "allTime" }}
className="mb-3"
enableClear={false}
onValueChange={setDateRange}
>
<DateRangePickerItem
key="lastMonth"
value="lastMonth"
from={lastMonth}
to={new Date()}
>
Last Month
</DateRangePickerItem>
<DateRangePickerItem
key="lastYear"
value="lastYear"
from={lastYear}
to={new Date()}
>
Last Year
</DateRangePickerItem>
<DateRangePickerItem
key="allTime"
value="allTime"
from={new Date(1970, 0, 1)}
to={new Date()}
>
All time
</DateRangePickerItem>
</DateRangePicker>
<Button
color={"blue"}
icon={FiDownloadCloud}
size="xs"
onClick={() => requestReport()}
>
Generate Report
</Button>
<p className="mt-1 text-xs">This can take a few minutes.</p>
</div>
);
}
function DataDisclaimer() {
return (
<div className="mb-10">
<div className="mx-auto mt-2 mb-6">
<Callout title="We only see metadata" color="teal">
<Text>
<p>We don&apos;t collect ... DISCLAIMER DISCLAIMER DISCLAIMER</p>
<br />
<p>We collect ...</p>
<ul>
<li> Usage Volume </li>
<li> Number of Active Seats </li>
<li> Query Volume (No identifying data) </li>
</ul>
<br />
<p>We use this to ... INFO INFO INFO</p>
</Text>
</Callout>
</div>
</div>
);
}
function UsageReportsTable() {
return (
<div>
<Title className="mb-2 mt-6 mx-auto"> Previous Reports </Title>
<Table className="overflow-visible">
<TableHead>
<TableRow>
<TableHeaderCell>Report</TableHeaderCell>
<TableHeaderCell>Period</TableHeaderCell>
<TableHeaderCell>Time Generated</TableHeaderCell>
<TableHeaderCell>Download</TableHeaderCell>
</TableRow>
</TableHead>
</Table>
</div>
);
}
export default function Page() {
return (
<div className="mx-auto container">
<AdminPageTitle
title="Usage Reports"
icon={<FiActivity size={32} className="my-auto" />}
/>
<Text className="mb-8">
Generate usage statistics for all users in the workspace.
</Text>
<DataDisclaimer />
<GenerateReportInput />
<Divider />
<UsageReportsTable />
</div>
);
}

View File

@@ -20,6 +20,7 @@ import {
} from "@/lib/userSS";
import { redirect } from "next/navigation";
import {
FiActivity,
FiCpu,
FiLayers,
FiPackage,
@@ -181,6 +182,20 @@ export async function Layout({ children }: { children: React.ReactNode }) {
},
],
},
{
name: "Usage",
items: [
{
name: (
<div className="flex">
<FiActivity size={18} />
<div className="ml-1">Usage</div>
</div>
),
link: "/admin/usage",
},
],
},
{
name: "Settings",
items: [