Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
eb88c9bdf8 | ||
|
|
77e4791cbe | ||
|
|
5d922dfae7 | ||
|
|
c704c84451 | ||
|
|
6f23488438 | ||
|
|
ca50abb290 | ||
|
|
15067114a2 | ||
|
|
645f14535b | ||
|
|
250a5b03d2 | ||
|
|
5ee060dfc6 | ||
|
|
f08502cde4 | ||
|
|
90ecb88249 | ||
|
|
16e60f74da | ||
|
|
0ab252cc7c | ||
|
|
ebadbaf30d | ||
|
|
7ce6b6817e | ||
|
|
6d4040d5ed | ||
|
|
4a7a4c0add | ||
|
|
883e53d8b5 | ||
|
|
a9e6b9ae5c | ||
|
|
a61ee1f29f | ||
|
|
eefdf60e31 | ||
|
|
19a633a756 | ||
|
|
e84c1a3ab8 | ||
|
|
4d41aa7e53 | ||
|
|
a4c28cc324 | ||
|
|
187f2e7b1d | ||
|
|
b355825295 | ||
|
|
33dc18356a | ||
|
|
c804d90f6a | ||
|
|
09aa547de1 | ||
|
|
57839eae5c | ||
|
|
a43f5f38a1 | ||
|
|
8220d07057 | ||
|
|
89ffe20363 | ||
|
|
9a3adf54b1 | ||
|
|
459129118e | ||
|
|
0862370ad6 |
@@ -129,6 +129,7 @@ types:
|
||||
- optional<string>
|
||||
- optional<integer>
|
||||
- optional<boolean>
|
||||
- optional<list<string>>
|
||||
DatasetStatus:
|
||||
enum:
|
||||
- ACTIVE
|
||||
|
||||
@@ -1275,6 +1275,10 @@ components:
|
||||
nullable: true
|
||||
- type: boolean
|
||||
nullable: true
|
||||
- type: array
|
||||
items:
|
||||
type: string
|
||||
nullable: true
|
||||
DatasetStatus:
|
||||
title: DatasetStatus
|
||||
type: string
|
||||
|
||||
Generated
+2
-2
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "langfuse-core",
|
||||
"version": "1.31.2",
|
||||
"version": "1.33.8",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "langfuse-core",
|
||||
"version": "1.31.2",
|
||||
"version": "1.33.8",
|
||||
"hasInstallScript": true,
|
||||
"dependencies": {
|
||||
"@anthropic-ai/tokenizer": "^0.0.4",
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "langfuse-core",
|
||||
"version": "1.31.2",
|
||||
"version": "1.33.8",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"prebuild": "cp generated/openapi-client/openapi.yml public/openapi-client.yml && cp generated/openapi-server/openapi.yml public/openapi-server.yml",
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
-- DropIndex
|
||||
DROP INDEX "traces_project_id_external_id_key";
|
||||
@@ -198,7 +198,6 @@ model Trace {
|
||||
|
||||
scores Score[]
|
||||
|
||||
@@unique([projectId, externalId])
|
||||
@@index([projectId])
|
||||
@@index([sessionId])
|
||||
@@index([name])
|
||||
|
||||
+46
-39
@@ -127,10 +127,10 @@ async function main() {
|
||||
|
||||
for (let i = 0; i < TRACE_VOLUME; i++) {
|
||||
// print progress to console with a progress bar that refreshes every 10 iterations
|
||||
if (i % 10 === 0) {
|
||||
if ((i + 1) % 10 === 0 || i === TRACE_VOLUME - 1) {
|
||||
process.stdout.clearLine(0);
|
||||
process.stdout.cursorTo(0);
|
||||
process.stdout.write(`Seeding ${i} of ${TRACE_VOLUME}`);
|
||||
process.stdout.write(`Seeding ${i + 1} of ${TRACE_VOLUME}`);
|
||||
}
|
||||
// random date within last 90 days, with a linear bias towards more recent dates
|
||||
const traceTs = new Date(
|
||||
@@ -409,47 +409,54 @@ async function main() {
|
||||
}
|
||||
}
|
||||
|
||||
const dataset = await prisma.dataset.create({
|
||||
data: {
|
||||
name: "demo-dataset",
|
||||
projectId: project2.id,
|
||||
},
|
||||
});
|
||||
|
||||
const datasetRun = await prisma.datasetRuns.create({
|
||||
data: {
|
||||
name: "demo-dataset-run",
|
||||
datasetId: dataset.id,
|
||||
},
|
||||
});
|
||||
|
||||
for (let runNumber = 0; runNumber < 10; runNumber++) {
|
||||
//pick randomly from existingSpanIds
|
||||
const sourceObservationId =
|
||||
generationIds[Math.floor(Math.random() * generationIds.length)];
|
||||
const runObservationId =
|
||||
generationIds[Math.floor(Math.random() * generationIds.length)];
|
||||
|
||||
const datasetItem = await prisma.datasetItem.create({
|
||||
for (let datasetNumber = 0; datasetNumber < 2; datasetNumber++) {
|
||||
const dataset = await prisma.dataset.create({
|
||||
data: {
|
||||
datasetId: dataset.id,
|
||||
sourceObservationId:
|
||||
Math.random() > 0.5 ? sourceObservationId : undefined,
|
||||
input: [
|
||||
{ role: "user", content: "How can i create a React component?" },
|
||||
],
|
||||
expectedOutput:
|
||||
"Creating a React component can be done in two ways: as a functional component or as a class component. Let's start with a basic example of both.",
|
||||
name: `demo-dataset-${datasetNumber}`,
|
||||
projectId: project2.id,
|
||||
},
|
||||
});
|
||||
|
||||
await prisma.datasetRunItems.create({
|
||||
data: {
|
||||
datasetItemId: datasetItem.id,
|
||||
observationId: runObservationId!,
|
||||
datasetRunId: datasetRun.id,
|
||||
},
|
||||
});
|
||||
for (let datasetRunNumber = 0; datasetRunNumber < 2; datasetRunNumber++) {
|
||||
const datasetRun = await prisma.datasetRuns.create({
|
||||
data: {
|
||||
name: `demo-dataset-run-${datasetRunNumber}`,
|
||||
datasetId: dataset.id,
|
||||
},
|
||||
});
|
||||
|
||||
for (let runNumber = 0; runNumber < 10; runNumber++) {
|
||||
//pick randomly from existingSpanIds
|
||||
const sourceObservationId =
|
||||
generationIds[Math.floor(Math.random() * generationIds.length)];
|
||||
const runObservationId =
|
||||
generationIds[Math.floor(Math.random() * generationIds.length)];
|
||||
|
||||
const datasetItem = await prisma.datasetItem.create({
|
||||
data: {
|
||||
datasetId: dataset.id,
|
||||
sourceObservationId:
|
||||
Math.random() > 0.5 ? sourceObservationId : undefined,
|
||||
input: [
|
||||
{
|
||||
role: "user",
|
||||
content: "How can i create a React component?",
|
||||
},
|
||||
],
|
||||
expectedOutput:
|
||||
"Creating a React component can be done in two ways: as a functional component or as a class component. Let's start with a basic example of both.",
|
||||
},
|
||||
});
|
||||
|
||||
await prisma.datasetRunItems.create({
|
||||
data: {
|
||||
datasetItemId: datasetItem.id,
|
||||
observationId: runObservationId!,
|
||||
datasetRunId: datasetRun.id,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -300,6 +300,7 @@ describe("/api/public/ingestion API Endpoint", () => {
|
||||
id: generationId,
|
||||
traceId: traceId,
|
||||
parentObservationId: spanId,
|
||||
modelParameters: { someKey: ["user-1", "user-2"] },
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -376,6 +377,9 @@ describe("/api/public/ingestion API Endpoint", () => {
|
||||
expect(dbGeneration?.traceId).toBe(traceId);
|
||||
expect(dbGeneration?.name).toBe("generation-name");
|
||||
expect(dbGeneration?.parentObservationId).toBe(spanId);
|
||||
expect(dbGeneration?.modelParameters).toEqual({
|
||||
someKey: ["user-1", "user-2"],
|
||||
});
|
||||
|
||||
const dbEvent = await prisma.observation.findUnique({
|
||||
where: {
|
||||
@@ -790,6 +794,66 @@ describe("/api/public/ingestion API Endpoint", () => {
|
||||
expect(dbTrace[0]?.version).toBe("2.0.0");
|
||||
});
|
||||
|
||||
it("should not override a trace from a different project", async () => {
|
||||
const traceId = v4();
|
||||
const newProjectId = v4();
|
||||
|
||||
await prisma.project.create({
|
||||
data: {
|
||||
id: newProjectId,
|
||||
name: "another-project",
|
||||
},
|
||||
});
|
||||
|
||||
await prisma.trace.create({
|
||||
data: {
|
||||
id: traceId,
|
||||
project: { connect: { id: newProjectId } },
|
||||
},
|
||||
});
|
||||
|
||||
const responseOne = await makeAPICall("POST", "/api/public/ingestion", {
|
||||
batch: [
|
||||
{
|
||||
id: v4(),
|
||||
type: "trace-create",
|
||||
timestamp: new Date().toISOString(),
|
||||
body: {
|
||||
id: traceId,
|
||||
name: "trace-name",
|
||||
userId: "user-1",
|
||||
metadata: { key: "value" },
|
||||
release: "1.0.0",
|
||||
version: "2.0.0",
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
expect(responseOne.status).toBe(207);
|
||||
|
||||
console.log(responseOne.body);
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-member-access
|
||||
const errors = responseOne.body.errors;
|
||||
|
||||
expect(errors).toBeDefined();
|
||||
console.log(errors);
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
|
||||
expect(errors.length).toBe(1);
|
||||
|
||||
const dbTrace = await prisma.trace.findMany({
|
||||
where: {
|
||||
id: traceId,
|
||||
},
|
||||
});
|
||||
|
||||
expect(dbTrace.length).toEqual(1);
|
||||
expect(dbTrace[0]?.name).toBeNull();
|
||||
expect(dbTrace[0]?.release).toBeNull();
|
||||
expect(dbTrace[0]?.metadata).toBeNull();
|
||||
expect(dbTrace[0]?.version).toBeNull();
|
||||
});
|
||||
|
||||
[
|
||||
{
|
||||
inputs: [{ a: "a" }, { b: "b" }],
|
||||
|
||||
@@ -53,7 +53,7 @@ describe("Traces TRPC Router", () => {
|
||||
orderBy: null,
|
||||
});
|
||||
expect(traces).toBeDefined();
|
||||
expect(traces).toMatchObject([trace]);
|
||||
expect(traces).toMatchObject({ traces: [trace] });
|
||||
});
|
||||
|
||||
test("traces.all RPC orders traces by userId", async () => {
|
||||
@@ -88,7 +88,7 @@ describe("Traces TRPC Router", () => {
|
||||
order: "ASC",
|
||||
},
|
||||
});
|
||||
expect(tracesASC).toMatchObject([trace1, trace2]);
|
||||
expect(tracesASC).toMatchObject({ traces: [trace1, trace2] });
|
||||
|
||||
const tracesDESC = await caller.traces.all({
|
||||
page: 0,
|
||||
@@ -102,6 +102,6 @@ describe("Traces TRPC Router", () => {
|
||||
order: "DESC",
|
||||
},
|
||||
});
|
||||
expect(tracesDESC).toMatchObject([trace2, trace1]);
|
||||
expect(tracesDESC).toMatchObject({ traces: [trace2, trace1] });
|
||||
});
|
||||
});
|
||||
|
||||
+143
-113
@@ -141,7 +141,6 @@ export default function Layout(props: PropsWithChildren) {
|
||||
{props.children}
|
||||
</main>
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Head>
|
||||
@@ -505,120 +504,151 @@ type NestedNavigationItem = Omit<Route, "children"> & {
|
||||
const MainNavigation: React.FC<{
|
||||
nav: NavigationItem[];
|
||||
onNavitemClick?: () => void;
|
||||
}> = ({ nav, onNavitemClick }) => (
|
||||
<li>
|
||||
<ul role="list" className="-mx-2 space-y-1">
|
||||
{nav.map((item) => (
|
||||
<li key={item.name}>
|
||||
{(!item.children || item.children.length === 0) && item.href ? (
|
||||
<Link
|
||||
href={item.href}
|
||||
className={clsx(
|
||||
item.current
|
||||
? "bg-gray-50 text-indigo-600"
|
||||
: "text-gray-700 hover:bg-gray-50 hover:text-indigo-600",
|
||||
"group flex gap-x-3 rounded-md p-2 text-sm font-semibold leading-6",
|
||||
)}
|
||||
onClick={onNavitemClick}
|
||||
>
|
||||
{item.icon && (
|
||||
<item.icon
|
||||
className={clsx(
|
||||
item.current
|
||||
? "text-indigo-600"
|
||||
: "text-gray-400 group-hover:text-indigo-600",
|
||||
"h-6 w-6 shrink-0",
|
||||
)}
|
||||
aria-hidden="true"
|
||||
/>
|
||||
)}
|
||||
{item.name}
|
||||
{item.label && (
|
||||
<span
|
||||
className={cn(
|
||||
"self-center whitespace-nowrap break-keep rounded-sm border px-1 py-0.5 text-xs",
|
||||
item.current
|
||||
? "border-indigo-600 text-indigo-600"
|
||||
: "border-gray-200 text-gray-400 group-hover:border-indigo-600 group-hover:text-indigo-600",
|
||||
)}
|
||||
>
|
||||
{item.label}
|
||||
</span>
|
||||
)}
|
||||
</Link>
|
||||
) : item.children && item.children.length > 0 ? (
|
||||
<Disclosure
|
||||
as="div"
|
||||
defaultOpen={item.children.some((child) => child.current)}
|
||||
>
|
||||
{({ open }) => (
|
||||
<>
|
||||
<Disclosure.Button className="group flex w-full items-center gap-x-3 rounded-md p-2 text-left text-sm font-semibold leading-6 hover:bg-gray-50 hover:text-indigo-600">
|
||||
{item.icon && (
|
||||
<item.icon
|
||||
className="h-6 w-6 shrink-0 text-gray-400 group-hover:text-indigo-600"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
}> = ({ nav, onNavitemClick }) => {
|
||||
const STORAGE_KEY = "sidebar-tracing-default-open";
|
||||
const getDefaultOpen = () => {
|
||||
const savedState = localStorage.getItem(STORAGE_KEY);
|
||||
if (savedState !== null) {
|
||||
try {
|
||||
return JSON.parse(savedState) as boolean;
|
||||
} catch (e) {
|
||||
console.error("Error parsing saved state: ", e);
|
||||
}
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
const handleDropDownClick = () => {
|
||||
const savedState = localStorage.getItem(STORAGE_KEY);
|
||||
const isOpen =
|
||||
savedState !== null ? (JSON.parse(savedState) as boolean) : false;
|
||||
const newState = !isOpen;
|
||||
localStorage.setItem(
|
||||
"sidebar-tracing-default-open",
|
||||
JSON.stringify(newState),
|
||||
);
|
||||
};
|
||||
return (
|
||||
<li>
|
||||
<ul role="list" className="-mx-2 space-y-1">
|
||||
{nav.map((item) => (
|
||||
<li key={item.name}>
|
||||
{(!item.children || item.children.length === 0) && item.href ? (
|
||||
<Link
|
||||
href={item.href}
|
||||
className={clsx(
|
||||
item.current
|
||||
? "bg-gray-50 text-indigo-600"
|
||||
: "text-gray-700 hover:bg-gray-50 hover:text-indigo-600",
|
||||
"group flex gap-x-3 rounded-md p-2 text-sm font-semibold leading-6",
|
||||
)}
|
||||
onClick={onNavitemClick}
|
||||
>
|
||||
{item.icon && (
|
||||
<item.icon
|
||||
className={clsx(
|
||||
item.current
|
||||
? "text-indigo-600"
|
||||
: "text-gray-400 group-hover:text-indigo-600",
|
||||
"h-6 w-6 shrink-0",
|
||||
)}
|
||||
{item.name}
|
||||
{item.label && (
|
||||
<span
|
||||
className={cn(
|
||||
"self-center whitespace-nowrap break-keep rounded-sm border px-1 py-0.5 text-xs",
|
||||
item.current
|
||||
? "border-indigo-600 text-indigo-600"
|
||||
: "border-gray-200 text-gray-400 group-hover:border-indigo-600 group-hover:text-indigo-600",
|
||||
)}
|
||||
>
|
||||
{item.label}
|
||||
</span>
|
||||
aria-hidden="true"
|
||||
/>
|
||||
)}
|
||||
{item.name}
|
||||
{item.label && (
|
||||
<span
|
||||
className={cn(
|
||||
"self-center whitespace-nowrap break-keep rounded-sm border px-1 py-0.5 text-xs",
|
||||
item.current
|
||||
? "border-indigo-600 text-indigo-600"
|
||||
: "border-gray-200 text-gray-400 group-hover:border-indigo-600 group-hover:text-indigo-600",
|
||||
)}
|
||||
<ChevronRightIcon
|
||||
className={clsx(
|
||||
open ? "rotate-90 text-gray-500" : "text-gray-400",
|
||||
"ml-auto h-5 w-5 shrink-0",
|
||||
>
|
||||
{item.label}
|
||||
</span>
|
||||
)}
|
||||
</Link>
|
||||
) : item.children && item.children.length > 0 ? (
|
||||
<Disclosure
|
||||
as="div"
|
||||
defaultOpen={
|
||||
item.children.some((child) => child.current) ||
|
||||
getDefaultOpen()
|
||||
}
|
||||
>
|
||||
{({ open }) => (
|
||||
<>
|
||||
<Disclosure.Button
|
||||
className="group flex w-full items-center gap-x-3 rounded-md p-2 text-left text-sm font-semibold leading-6 hover:bg-gray-50 hover:text-indigo-600"
|
||||
onClick={handleDropDownClick}
|
||||
>
|
||||
{item.icon && (
|
||||
<item.icon
|
||||
className="h-6 w-6 shrink-0 text-gray-400 group-hover:text-indigo-600"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
)}
|
||||
aria-hidden="true"
|
||||
/>
|
||||
</Disclosure.Button>
|
||||
<Disclosure.Panel as="ul" className="mt-1 px-2">
|
||||
{item.children?.map((subItem) => (
|
||||
<li key={subItem.name}>
|
||||
{/* 44px */}
|
||||
<Link
|
||||
href={subItem.href ?? "#"}
|
||||
className={clsx(
|
||||
subItem.current
|
||||
? "bg-gray-50 text-indigo-600"
|
||||
: "text-gray-700 hover:bg-gray-50 hover:text-indigo-600",
|
||||
"flex w-full items-center gap-x-3 rounded-md py-2 pl-9 pr-2 text-sm leading-6",
|
||||
{item.name}
|
||||
{item.label && (
|
||||
<span
|
||||
className={cn(
|
||||
"self-center whitespace-nowrap break-keep rounded-sm border px-1 py-0.5 text-xs",
|
||||
item.current
|
||||
? "border-indigo-600 text-indigo-600"
|
||||
: "border-gray-200 text-gray-400 group-hover:border-indigo-600 group-hover:text-indigo-600",
|
||||
)}
|
||||
>
|
||||
{subItem.name}
|
||||
{subItem.label && (
|
||||
<span className="self-center whitespace-nowrap break-keep rounded-sm border border-gray-200 px-1 py-0.5 text-xs text-gray-400 group-hover:border-indigo-600 group-hover:text-indigo-600">
|
||||
{subItem.label}
|
||||
</span>
|
||||
)}
|
||||
</Link>
|
||||
</li>
|
||||
))}
|
||||
</Disclosure.Panel>
|
||||
</>
|
||||
)}
|
||||
</Disclosure>
|
||||
) : null}
|
||||
</li>
|
||||
))}
|
||||
<FeedbackButtonWrapper className="w-full">
|
||||
<li className="group flex cursor-pointer gap-x-3 rounded-md p-2 text-sm font-semibold leading-6 text-gray-700 hover:bg-gray-50 hover:text-indigo-600">
|
||||
<MessageSquarePlus
|
||||
className="h-6 w-6 shrink-0 text-gray-400 group-hover:text-indigo-600"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
Feedback
|
||||
</li>
|
||||
</FeedbackButtonWrapper>
|
||||
</ul>
|
||||
</li>
|
||||
);
|
||||
{item.label}
|
||||
</span>
|
||||
)}
|
||||
<ChevronRightIcon
|
||||
className={clsx(
|
||||
open ? "rotate-90 text-gray-500" : "text-gray-400",
|
||||
"ml-auto h-5 w-5 shrink-0",
|
||||
)}
|
||||
aria-hidden="true"
|
||||
/>
|
||||
</Disclosure.Button>
|
||||
<Disclosure.Panel as="ul" className="mt-1 px-2">
|
||||
{item.children?.map((subItem) => (
|
||||
<li key={subItem.name}>
|
||||
{/* 44px */}
|
||||
<Link
|
||||
href={subItem.href ?? "#"}
|
||||
className={clsx(
|
||||
subItem.current
|
||||
? "bg-gray-50 text-indigo-600"
|
||||
: "text-gray-700 hover:bg-gray-50 hover:text-indigo-600",
|
||||
"flex w-full items-center gap-x-3 rounded-md py-2 pl-9 pr-2 text-sm leading-6",
|
||||
)}
|
||||
>
|
||||
{subItem.name}
|
||||
{subItem.label && (
|
||||
<span className="self-center whitespace-nowrap break-keep rounded-sm border border-gray-200 px-1 py-0.5 text-xs text-gray-400 group-hover:border-indigo-600 group-hover:text-indigo-600">
|
||||
{subItem.label}
|
||||
</span>
|
||||
)}
|
||||
</Link>
|
||||
</li>
|
||||
))}
|
||||
</Disclosure.Panel>
|
||||
</>
|
||||
)}
|
||||
</Disclosure>
|
||||
) : null}
|
||||
</li>
|
||||
))}
|
||||
<FeedbackButtonWrapper className="w-full">
|
||||
<li className="group flex cursor-pointer gap-x-3 rounded-md p-2 text-sm font-semibold leading-6 text-gray-700 hover:bg-gray-50 hover:text-indigo-600">
|
||||
<MessageSquarePlus
|
||||
className="h-6 w-6 shrink-0 text-gray-400 group-hover:text-indigo-600"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
Feedback
|
||||
</li>
|
||||
</FeedbackButtonWrapper>
|
||||
</ul>
|
||||
</li>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -83,15 +83,20 @@ export function StarTraceToggle({
|
||||
utils.traces.all.setData(
|
||||
tracesFilter,
|
||||
(oldQueryData: RouterOutput["traces"]["all"] | undefined) => {
|
||||
return oldQueryData
|
||||
? oldQueryData.map((trace) => {
|
||||
return {
|
||||
...trace,
|
||||
bookmarked:
|
||||
trace.id === traceId ? !trace.bookmarked : trace.bookmarked,
|
||||
};
|
||||
})
|
||||
: [];
|
||||
return {
|
||||
totalCount: oldQueryData?.totalCount,
|
||||
traces: oldQueryData?.traces
|
||||
? oldQueryData.traces.map((trace) => {
|
||||
return {
|
||||
...trace,
|
||||
bookmarked:
|
||||
trace.id === traceId
|
||||
? !trace.bookmarked
|
||||
: trace.bookmarked,
|
||||
};
|
||||
})
|
||||
: [],
|
||||
};
|
||||
},
|
||||
);
|
||||
void utils.traces.all.invalidate();
|
||||
|
||||
@@ -122,10 +122,14 @@ export function DataTable<TData extends object, TValue>({
|
||||
}
|
||||
|
||||
if (orderBy?.column === header.column.columnDef.id) {
|
||||
setOrderBy({
|
||||
column: header.column.columnDef.id,
|
||||
order: orderBy.order === "ASC" ? "DESC" : "ASC",
|
||||
});
|
||||
if (orderBy.order === "DESC") {
|
||||
setOrderBy({
|
||||
column: header.column.columnDef.id,
|
||||
order: "ASC",
|
||||
});
|
||||
} else {
|
||||
setOrderBy(null);
|
||||
}
|
||||
} else {
|
||||
setOrderBy({
|
||||
column: header.column.columnDef.id,
|
||||
|
||||
@@ -106,13 +106,13 @@ export default function GenerationsTable({ projectId }: GenerationsTableProps) {
|
||||
isSuccess:
|
||||
generationsQueries[0].isSuccess || generationsQueries[1].isSuccess,
|
||||
data: [
|
||||
...(generationsQueries[0].data ?? []),
|
||||
...(generationsQueries[1].data ?? []),
|
||||
...(generationsQueries[0].data?.generations ?? []),
|
||||
...(generationsQueries[1].data?.generations ?? []),
|
||||
],
|
||||
error: generationsQueries[0].error ?? generationsQueries[1].error,
|
||||
};
|
||||
|
||||
const totalCount = generations.data.slice(1)[0]?.totalCount ?? 0;
|
||||
const totalCount = generationsQueries[0].data?.totalCount ?? 0;
|
||||
|
||||
const filterOptions = api.generations.filterOptions.useQuery({
|
||||
projectId,
|
||||
|
||||
@@ -6,6 +6,7 @@ import { TraceTableMultiSelectAction } from "@/src/components/table/data-table-m
|
||||
import { DataTableToolbar } from "@/src/components/table/data-table-toolbar";
|
||||
import TableLink from "@/src/components/table/table-link";
|
||||
import { type LangfuseColumnDef } from "@/src/components/table/types";
|
||||
import { TagTracePopver } from "@/src/features/tag/components/TagTracePopver";
|
||||
import { TokenUsageBadge } from "@/src/components/token-usage-badge";
|
||||
import { Checkbox } from "@/src/components/ui/checkbox";
|
||||
import { JSONView } from "@/src/components/ui/code";
|
||||
@@ -109,12 +110,12 @@ export default function TracesTable({
|
||||
};
|
||||
const traces = api.traces.all.useQuery(tracesAllQueryFilter);
|
||||
|
||||
const totalCount = traces.data?.slice(1)[0]?.totalCount ?? 0;
|
||||
const totalCount = traces.data?.totalCount ?? 0;
|
||||
useEffect(() => {
|
||||
if (traces.isSuccess) {
|
||||
setDetailPageList(
|
||||
"traces",
|
||||
traces.data.map((t) => t.id),
|
||||
traces.data.traces.map((t) => t.id),
|
||||
);
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
@@ -135,9 +136,8 @@ export default function TracesTable({
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
const convertToTableRow = (
|
||||
trace: RouterOutput["traces"]["all"][0],
|
||||
trace: RouterOutput["traces"]["all"]["traces"][0],
|
||||
): TracesTableRow => {
|
||||
return {
|
||||
bookmarked: trace.bookmarked,
|
||||
@@ -198,7 +198,6 @@ export default function TracesTable({
|
||||
cell: ({ row }) => {
|
||||
const bookmarked = row.getValue("bookmarked");
|
||||
const traceId = row.getValue("id");
|
||||
|
||||
return typeof traceId === "string" &&
|
||||
typeof bookmarked === "boolean" ? (
|
||||
<StarTraceToggle
|
||||
@@ -364,6 +363,21 @@ export default function TracesTable({
|
||||
accessorKey: "tags",
|
||||
id: "tags",
|
||||
header: "Tags",
|
||||
cell: ({ row }) => {
|
||||
const tags: string[] = row.getValue("tags");
|
||||
const traceId: string = row.getValue("id");
|
||||
const filterOptionTags = traceFilterOptions.data?.tags ?? [];
|
||||
const allTags = filterOptionTags.map((t) => t.value);
|
||||
return (
|
||||
<TagTracePopver
|
||||
tags={tags}
|
||||
availableTags={allTags}
|
||||
projectId={projectId}
|
||||
traceId={traceId}
|
||||
tracesFilter={tracesAllQueryFilter}
|
||||
/>
|
||||
);
|
||||
},
|
||||
enableHiding: true,
|
||||
},
|
||||
{
|
||||
@@ -403,7 +417,8 @@ export default function TracesTable({
|
||||
<TraceTableMultiSelectAction
|
||||
// Exclude traces that are not in the current page
|
||||
selectedTraceIds={Object.keys(selectedRows).filter(
|
||||
(traceId) => traces.data?.map((t) => t.id).includes(traceId),
|
||||
(traceId) =>
|
||||
traces.data?.traces.map((t) => t.id).includes(traceId),
|
||||
)}
|
||||
projectId={projectId}
|
||||
onDeleteSuccess={() => {
|
||||
@@ -428,11 +443,11 @@ export default function TracesTable({
|
||||
: {
|
||||
isLoading: false,
|
||||
isError: false,
|
||||
data: traces.data.map((t) => convertToTableRow(t)),
|
||||
data: traces.data.traces.map((t) => convertToTableRow(t)),
|
||||
}
|
||||
}
|
||||
pagination={{
|
||||
pageCount: Math.ceil(totalCount / paginationState.pageSize),
|
||||
pageCount: Math.ceil(Number(totalCount) / paginationState.pageSize),
|
||||
onChange: setPaginationState,
|
||||
state: paginationState,
|
||||
}}
|
||||
|
||||
@@ -23,6 +23,7 @@ import { api } from "@/src/utils/api";
|
||||
import { IOPreview } from "@/src/components/trace/IOPreview";
|
||||
import { formatInterval } from "@/src/utils/dates";
|
||||
import Link from "next/link";
|
||||
import { usdFormatter } from "@/src/utils/numbers";
|
||||
|
||||
export const ObservationPreview = (props: {
|
||||
observations: Array<ObservationReturnType>;
|
||||
@@ -61,8 +62,19 @@ export const ObservationPreview = (props: {
|
||||
projectId={preloadedObservation.projectId}
|
||||
/>
|
||||
) : undefined}
|
||||
{preloadedObservation.completionStartTime ? (
|
||||
<Badge variant="outline">
|
||||
Time to first token:{" "}
|
||||
{formatInterval(
|
||||
(preloadedObservation.completionStartTime.getTime() -
|
||||
preloadedObservation.startTime.getTime()) /
|
||||
1000,
|
||||
)}
|
||||
</Badge>
|
||||
) : null}
|
||||
{preloadedObservation.endTime ? (
|
||||
<Badge variant="outline">
|
||||
Latency:{" "}
|
||||
{formatInterval(
|
||||
(preloadedObservation.endTime.getTime() -
|
||||
preloadedObservation.startTime.getTime()) /
|
||||
@@ -87,7 +99,7 @@ export const ObservationPreview = (props: {
|
||||
) : null}
|
||||
{preloadedObservation.price ? (
|
||||
<Badge variant="outline">
|
||||
{preloadedObservation.price.toString()} USD
|
||||
{usdFormatter(preloadedObservation.price.toNumber())}
|
||||
</Badge>
|
||||
) : undefined}
|
||||
|
||||
|
||||
@@ -73,9 +73,6 @@ export const TracePreview = ({
|
||||
title="Metadata"
|
||||
json={trace.metadata}
|
||||
/>
|
||||
{trace.tags.length !== 0 && (
|
||||
<JSONView key={trace.id + "-tags"} title="Tags" json={trace.tags} />
|
||||
)}
|
||||
{scores.find((s) => s.observationId === null) ? (
|
||||
<div className="mt-5 flex flex-col gap-2">
|
||||
<h3>Scores</h3>
|
||||
|
||||
@@ -6,7 +6,6 @@ import { TracePreview } from "./TracePreview";
|
||||
import Header from "@/src/components/layouts/header";
|
||||
import { Badge } from "@/src/components/ui/badge";
|
||||
import { TraceAggUsageBadge } from "@/src/components/token-usage-badge";
|
||||
import Decimal from "decimal.js";
|
||||
import { StringParam, useQueryParam } from "use-query-params";
|
||||
import { PublishTraceSwitch } from "@/src/components/publish-object-switch";
|
||||
import { DetailPageNav } from "@/src/features/navigate-detail-pages/DetailPageNav";
|
||||
@@ -17,10 +16,13 @@ import { DeleteTrace } from "@/src/components/delete-trace";
|
||||
import { StarTraceDetailsToggle } from "@/src/components/star-toggle";
|
||||
import Link from "next/link";
|
||||
import { NoAccessError } from "@/src/components/no-access";
|
||||
import { TagTraceDetailsPopover } from "@/src/features/tag/components/TagTraceDetailsPopover";
|
||||
import useLocalStorage from "@/src/components/useLocalStorage";
|
||||
import { Toggle } from "@/src/components/ui/toggle";
|
||||
import { Award, ChevronsDownUp, ChevronsUpDown } from "lucide-react";
|
||||
import { ScrollArea } from "@/src/components/ui/scroll-area";
|
||||
import { usdFormatter } from "@/src/utils/numbers";
|
||||
import type Decimal from "decimal.js";
|
||||
|
||||
export function Trace(props: {
|
||||
observations: Array<ObservationReturnType>;
|
||||
@@ -114,18 +116,34 @@ export function TracePage({ traceId }: { traceId: string }) {
|
||||
},
|
||||
},
|
||||
);
|
||||
const totalCost = trace.data?.observations.reduce(
|
||||
(acc, o) => {
|
||||
if (!o.price) return acc;
|
||||
|
||||
return acc ? acc.plus(o.price) : new Decimal(0).plus(o.price);
|
||||
const traceFilterOptions = api.traces.filterOptions.useQuery(
|
||||
{
|
||||
projectId: trace.data?.projectId ?? "",
|
||||
},
|
||||
{
|
||||
trpc: {
|
||||
context: {
|
||||
skipBatch: true,
|
||||
},
|
||||
},
|
||||
enabled: !!trace.data?.projectId && trace.isSuccess,
|
||||
},
|
||||
undefined as Decimal | undefined,
|
||||
);
|
||||
|
||||
const filterOptionTags = traceFilterOptions.data?.tags ?? [];
|
||||
const allTags = filterOptionTags.map((t) => t.value);
|
||||
|
||||
const totalCost: Decimal | undefined = trace.data?.observations.reduce(
|
||||
(prev: Decimal | undefined, curr: ObservationReturnType) => {
|
||||
if (!curr.price) return prev;
|
||||
|
||||
return prev ? prev.plus(curr.price) : curr.price;
|
||||
},
|
||||
undefined,
|
||||
);
|
||||
if (trace.error?.data?.code === "UNAUTHORIZED") return <NoAccessError />;
|
||||
if (!trace.data) return <div>loading...</div>;
|
||||
|
||||
return (
|
||||
<div className="flex flex-col overflow-hidden xl:container md:h-[calc(100vh-2rem)]">
|
||||
<Header
|
||||
@@ -185,10 +203,21 @@ export function TracePage({ traceId }: { traceId: string }) {
|
||||
<TraceAggUsageBadge observations={trace.data.observations} />
|
||||
{totalCost ? (
|
||||
<Badge variant="outline">
|
||||
Total cost: {totalCost.toString()} USD
|
||||
Total cost: {usdFormatter(totalCost.toNumber())}
|
||||
</Badge>
|
||||
) : undefined}
|
||||
</div>
|
||||
<div className="mt-5 rounded-lg border bg-card font-semibold text-card-foreground shadow-sm">
|
||||
<div className="flex flex-row items-center gap-3 p-2.5">
|
||||
Tags
|
||||
<TagTraceDetailsPopover
|
||||
tags={trace.data.tags}
|
||||
availableTags={allTags}
|
||||
traceId={trace.data.id}
|
||||
projectId={trace.data.projectId}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="mt-5 flex-1 overflow-hidden border-t pt-5">
|
||||
<Trace
|
||||
key={trace.data.id}
|
||||
|
||||
@@ -1 +1 @@
|
||||
export const VERSION = "v1.31.2";
|
||||
export const VERSION = "v1.33.8";
|
||||
|
||||
@@ -61,7 +61,7 @@ export const MetricTable = ({
|
||||
</RightAlignedCell>,
|
||||
<RightAlignedCell key={`${i}-cost`}>
|
||||
{item.totalTokenCost
|
||||
? usdFormatter(item.totalTokenCost as number)
|
||||
? usdFormatter(item.totalTokenCost as number, 2, 2)
|
||||
: "$0"}
|
||||
</RightAlignedCell>,
|
||||
])
|
||||
@@ -83,7 +83,7 @@ export const MetricTable = ({
|
||||
collapse={{ collapsed: 5, expanded: 20 }}
|
||||
>
|
||||
<TotalMetric
|
||||
metric={totalTokens ? usdFormatter(totalTokens) : "$0"}
|
||||
metric={totalTokens ? usdFormatter(totalTokens, 2, 2) : "$0"}
|
||||
description="Total cost"
|
||||
>
|
||||
<DocPopup
|
||||
|
||||
@@ -94,13 +94,19 @@ export const ModelUsageChart = ({
|
||||
0,
|
||||
);
|
||||
|
||||
// had to add this function as tremor under the hodd adds more variables
|
||||
// to the function call which would break usdFormatter.
|
||||
const oneValueUsdFormatter = (value: number) => {
|
||||
return usdFormatter(value, 2, 2);
|
||||
};
|
||||
|
||||
const data = [
|
||||
{
|
||||
tabTitle: "Total cost",
|
||||
data: transformedModelCost,
|
||||
totalMetric: totalCost ? usdFormatter(totalCost) : usdFormatter(0),
|
||||
totalMetric: totalCost ? usdFormatter(totalCost, 2, 2) : usdFormatter(0),
|
||||
metricDescription: `Token cost`,
|
||||
formatter: usdFormatter,
|
||||
formatter: oneValueUsdFormatter,
|
||||
},
|
||||
{
|
||||
tabTitle: "Total tokens",
|
||||
@@ -115,7 +121,7 @@ export const ModelUsageChart = ({
|
||||
return (
|
||||
<DashboardCard
|
||||
className={className}
|
||||
title={"Model Usage"}
|
||||
title="Model Usage"
|
||||
isLoading={tokens.isLoading}
|
||||
>
|
||||
<TabComponent
|
||||
|
||||
@@ -114,15 +114,17 @@ export const UserChart = ({
|
||||
|
||||
const maxNumberOfEntries = { collapsed: 5, expanded: 20 } as const;
|
||||
|
||||
const localUsdFormatter = (value: number) => usdFormatter(value, 2, 2);
|
||||
|
||||
const data = [
|
||||
{
|
||||
tabTitle: "Token cost",
|
||||
data: isExpanded
|
||||
? transformedCost.slice(0, maxNumberOfEntries.expanded)
|
||||
: transformedCost.slice(0, maxNumberOfEntries.collapsed),
|
||||
totalMetric: totalCost ? usdFormatter(totalCost) : usdFormatter(0),
|
||||
totalMetric: totalCost ? usdFormatter(totalCost, 2, 2) : usdFormatter(0),
|
||||
metricDescription: "Total cost",
|
||||
formatter: usdFormatter,
|
||||
formatter: localUsdFormatter,
|
||||
},
|
||||
{
|
||||
tabTitle: "Count of Traces",
|
||||
|
||||
@@ -15,6 +15,8 @@ import { Button } from "@/src/components/ui/button";
|
||||
import { DatasetStatus, type DatasetItem } from "@prisma/client";
|
||||
import { cn } from "@/src/utils/tailwind";
|
||||
import { type LangfuseColumnDef } from "@/src/components/table/types";
|
||||
import { useDetailPageLists } from "@/src/features/navigate-detail-pages/context";
|
||||
import { useEffect } from "react";
|
||||
|
||||
type RowData = {
|
||||
id: string;
|
||||
@@ -31,12 +33,23 @@ export function DatasetItemsTable({
|
||||
projectId: string;
|
||||
datasetId: string;
|
||||
}) {
|
||||
const { setDetailPageList } = useDetailPageLists();
|
||||
const utils = api.useUtils();
|
||||
const items = api.datasets.itemsByDatasetId.useQuery({
|
||||
projectId,
|
||||
datasetId,
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (items.isSuccess) {
|
||||
setDetailPageList(
|
||||
"datasetItems",
|
||||
items.data.map((t) => t.id),
|
||||
);
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [items.isSuccess, items.data]);
|
||||
|
||||
const mutUpdate = api.datasets.updateDatasetItem.useMutation({
|
||||
onSuccess: () => utils.datasets.invalidate(),
|
||||
});
|
||||
|
||||
@@ -2,9 +2,11 @@ import { GroupedScoreBadges } from "@/src/components/grouped-score-badge";
|
||||
import { DataTable } from "@/src/components/table/data-table";
|
||||
import TableLink from "@/src/components/table/table-link";
|
||||
import { type LangfuseColumnDef } from "@/src/components/table/types";
|
||||
import { useDetailPageLists } from "@/src/features/navigate-detail-pages/context";
|
||||
import { api } from "@/src/utils/api";
|
||||
import { formatInterval } from "@/src/utils/dates";
|
||||
import { type RouterOutput } from "@/src/utils/types";
|
||||
import { useEffect } from "react";
|
||||
|
||||
type RowData = {
|
||||
key: {
|
||||
@@ -25,7 +27,16 @@ export function DatasetRunsTable(props: {
|
||||
projectId: props.projectId,
|
||||
datasetId: props.datasetId,
|
||||
});
|
||||
|
||||
const { setDetailPageList } = useDetailPageLists();
|
||||
useEffect(() => {
|
||||
if (runs.isSuccess) {
|
||||
setDetailPageList(
|
||||
"datasetRuns",
|
||||
runs.data.map((t) => t.id),
|
||||
);
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [runs.isSuccess, runs.data]);
|
||||
const columns: LangfuseColumnDef<RowData>[] = [
|
||||
{
|
||||
accessorKey: "key",
|
||||
|
||||
@@ -10,9 +10,11 @@ import {
|
||||
DropdownMenuTrigger,
|
||||
} from "@/src/components/ui/dropdown-menu";
|
||||
import { NewDatasetButton } from "@/src/features/datasets/components/NewDatasetButton";
|
||||
import { useDetailPageLists } from "@/src/features/navigate-detail-pages/context";
|
||||
import { api } from "@/src/utils/api";
|
||||
import { type RouterOutput } from "@/src/utils/types";
|
||||
import { MoreVertical, Trash } from "lucide-react";
|
||||
import { useEffect } from "react";
|
||||
|
||||
type RowData = {
|
||||
key: {
|
||||
@@ -26,6 +28,7 @@ type RowData = {
|
||||
};
|
||||
|
||||
export function DatasetsTable(props: { projectId: string }) {
|
||||
const { setDetailPageList } = useDetailPageLists();
|
||||
const utils = api.useUtils();
|
||||
const datasets = api.datasets.allDatasets.useQuery({
|
||||
projectId: props.projectId,
|
||||
@@ -33,6 +36,15 @@ export function DatasetsTable(props: { projectId: string }) {
|
||||
const mutDelete = api.datasets.deleteDataset.useMutation({
|
||||
onSuccess: () => utils.datasets.invalidate(),
|
||||
});
|
||||
useEffect(() => {
|
||||
if (datasets.isSuccess) {
|
||||
setDetailPageList(
|
||||
"datasets",
|
||||
datasets.data.map((t) => t.id),
|
||||
);
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [datasets.isSuccess, datasets.data]);
|
||||
|
||||
const columns: LangfuseColumnDef<RowData>[] = [
|
||||
{
|
||||
|
||||
@@ -57,13 +57,6 @@ export function tokenCount(p: {
|
||||
}
|
||||
|
||||
function openAiChatTokenCount(params: TokenCalculationParams) {
|
||||
let encoding: Tiktoken;
|
||||
try {
|
||||
encoding = encoding_for_model(params.model);
|
||||
} catch (KeyError) {
|
||||
console.log("Warning: model not found. Using cl100k_base encoding.");
|
||||
encoding = get_encoding("cl100k_base");
|
||||
}
|
||||
let tokens_per_message = 0;
|
||||
let tokens_per_name = 0;
|
||||
|
||||
@@ -100,7 +93,7 @@ function openAiChatTokenCount(params: TokenCalculationParams) {
|
||||
Object.keys(message).forEach((key) => {
|
||||
const value = message[key as keyof typeof message];
|
||||
if (value) {
|
||||
num_tokens += encoding.encode(value).length;
|
||||
num_tokens += getTokensByModel(params.model, value);
|
||||
}
|
||||
if (key === "name") {
|
||||
num_tokens += tokens_per_name;
|
||||
@@ -108,6 +101,7 @@ function openAiChatTokenCount(params: TokenCalculationParams) {
|
||||
});
|
||||
});
|
||||
num_tokens += 3; // every reply is primed with <| start |> assistant <| message |>
|
||||
|
||||
return num_tokens;
|
||||
}
|
||||
|
||||
@@ -132,10 +126,29 @@ const claudeStringTokenCount = (p: { model: string; text: string }) => {
|
||||
const getTokens = (name: TiktokenEncoding, text: string) => {
|
||||
const encoding = get_encoding(name);
|
||||
const tokens = encoding.encode(text);
|
||||
// https://github.com/dqbd/tiktoken/issues/72
|
||||
// we need to ensure to deallocate memory from the encoder
|
||||
encoding.free();
|
||||
return tokens.length;
|
||||
};
|
||||
|
||||
const getTokensByModel = (model: TiktokenModel, text: string) => {
|
||||
let encoding: Tiktoken;
|
||||
try {
|
||||
encoding = encoding_for_model(model);
|
||||
} catch (KeyError) {
|
||||
console.log("Warning: model not found. Using cl100k_base encoding.");
|
||||
encoding = get_encoding("cl100k_base");
|
||||
}
|
||||
|
||||
const length = encoding.encode(text).length;
|
||||
|
||||
// https://github.com/dqbd/tiktoken/issues/72
|
||||
// we need to ensure to deallocate memory from the encoder
|
||||
encoding.free();
|
||||
return length;
|
||||
};
|
||||
|
||||
function isString(value: unknown): value is string {
|
||||
return typeof value === "string";
|
||||
}
|
||||
|
||||
@@ -50,7 +50,7 @@ export const DetailPageNav = (props: {
|
||||
return () => window.removeEventListener("keydown", handleKeyDown);
|
||||
}, [previousPageId, nextPageId, router, props]);
|
||||
|
||||
if (ids.length > 0)
|
||||
if (ids.length > 1)
|
||||
return (
|
||||
<div>
|
||||
<Tooltip>
|
||||
|
||||
@@ -96,7 +96,9 @@ export const CreateGenerationBody = CreateSpanBody.extend({
|
||||
modelParameters: z
|
||||
.record(
|
||||
z.string(),
|
||||
z.union([z.string(), z.number(), z.boolean()]).nullish(),
|
||||
z
|
||||
.union([z.string(), z.number(), z.boolean(), z.array(z.string())])
|
||||
.nullish(),
|
||||
)
|
||||
.nullish(),
|
||||
usage: usage,
|
||||
@@ -116,7 +118,9 @@ export const UpdateGenerationBody = UpdateSpanBody.extend({
|
||||
modelParameters: z
|
||||
.record(
|
||||
z.string(),
|
||||
z.union([z.string(), z.number(), z.boolean()]).nullish(),
|
||||
z
|
||||
.union([z.string(), z.number(), z.boolean(), z.array(z.string())])
|
||||
.nullish(),
|
||||
)
|
||||
.nullish(),
|
||||
usage: usage,
|
||||
|
||||
@@ -11,6 +11,7 @@ const scopes = [
|
||||
|
||||
"objects:publish",
|
||||
"objects:bookmark",
|
||||
"objects:tag",
|
||||
|
||||
"traces:delete",
|
||||
|
||||
@@ -39,6 +40,7 @@ export const roleAccessRights: Record<MembershipRole, Scope[]> = {
|
||||
"apiKeys:delete",
|
||||
"objects:publish",
|
||||
"objects:bookmark",
|
||||
"objects:tag",
|
||||
"traces:delete",
|
||||
"scores:CUD",
|
||||
"project:delete",
|
||||
@@ -58,6 +60,7 @@ export const roleAccessRights: Record<MembershipRole, Scope[]> = {
|
||||
"apiKeys:delete",
|
||||
"objects:publish",
|
||||
"objects:bookmark",
|
||||
"objects:tag",
|
||||
"traces:delete",
|
||||
"scores:CUD",
|
||||
"datasets:CUD",
|
||||
@@ -68,6 +71,7 @@ export const roleAccessRights: Record<MembershipRole, Scope[]> = {
|
||||
"members:read",
|
||||
"objects:publish",
|
||||
"objects:bookmark",
|
||||
"objects:tag",
|
||||
"scores:CUD",
|
||||
"datasets:CUD",
|
||||
"prompts:CUD",
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
import { Button } from "@/src/components/ui/button";
|
||||
import React from "react";
|
||||
|
||||
export const TagButton: React.FC<{ tag: string; loading: boolean }> =
|
||||
React.memo(({ tag, loading }) => (
|
||||
<Button
|
||||
key={tag}
|
||||
variant="secondary"
|
||||
size="xs"
|
||||
className="text-xs font-semibold hover:bg-white"
|
||||
loading={loading}
|
||||
>
|
||||
{tag}
|
||||
</Button>
|
||||
));
|
||||
TagButton.displayName = "TagButton";
|
||||
@@ -0,0 +1,38 @@
|
||||
import { CommandItem } from "@/src/components/ui/command";
|
||||
import { cn } from "@/src/utils/tailwind";
|
||||
import { Check } from "lucide-react";
|
||||
import { Button } from "@/src/components/ui/button";
|
||||
|
||||
type TagCommandItemProps = {
|
||||
value: string;
|
||||
selectedTags: string[];
|
||||
setSelectedTags: (value: string[]) => void;
|
||||
};
|
||||
|
||||
const TagCommandItem = ({
|
||||
value,
|
||||
selectedTags,
|
||||
setSelectedTags,
|
||||
}: TagCommandItemProps) => {
|
||||
return (
|
||||
<CommandItem
|
||||
key={value}
|
||||
onSelect={() => {
|
||||
setSelectedTags([...selectedTags, value]);
|
||||
}}
|
||||
>
|
||||
<div
|
||||
className={cn(
|
||||
"mr-2 flex h-4 w-4 items-center justify-center rounded-sm border border-primary opacity-50 [&_svg]:invisible",
|
||||
)}
|
||||
>
|
||||
<Check className={cn("h-4 w-4")} />
|
||||
</div>
|
||||
<Button variant="secondary" size="xs">
|
||||
{value}
|
||||
</Button>
|
||||
</CommandItem>
|
||||
);
|
||||
};
|
||||
|
||||
export default TagCommandItem;
|
||||
@@ -0,0 +1,37 @@
|
||||
import React from "react";
|
||||
import { cn } from "@/src/utils/tailwind";
|
||||
import { CommandItem } from "cmdk";
|
||||
|
||||
type TagItemCreateProps = {
|
||||
inputValue: string;
|
||||
options: string[];
|
||||
onSelect: () => void;
|
||||
};
|
||||
|
||||
const TagItemCreate = ({
|
||||
inputValue,
|
||||
options,
|
||||
onSelect,
|
||||
}: TagItemCreateProps) => {
|
||||
const hasNoOption = !options
|
||||
.map((value) => value.toLowerCase())
|
||||
.includes(inputValue.toLowerCase());
|
||||
|
||||
const render = inputValue !== "" && hasNoOption;
|
||||
|
||||
if (!render) return null;
|
||||
|
||||
return (
|
||||
<CommandItem
|
||||
key={inputValue}
|
||||
value={inputValue}
|
||||
className="flex cursor-pointer items-center rounded-sm px-1 py-2 text-muted-foreground hover:bg-secondary/80"
|
||||
onSelect={onSelect}
|
||||
>
|
||||
<div className={cn("mr-2 h-4 w-4")} />
|
||||
Create new tag: "{inputValue}"
|
||||
</CommandItem>
|
||||
);
|
||||
};
|
||||
|
||||
export default TagItemCreate;
|
||||
@@ -0,0 +1,49 @@
|
||||
import React from "react";
|
||||
import { cn } from "@/src/utils/tailwind";
|
||||
import { X } from "lucide-react";
|
||||
import { Button } from "@/src/components/ui/button";
|
||||
import { Command as CommandPrimitive } from "cmdk";
|
||||
|
||||
type TagInputProps = React.ComponentPropsWithoutRef<
|
||||
typeof CommandPrimitive.Input
|
||||
> & {
|
||||
selectedTags: string[];
|
||||
setSelectedTags: (tags: string[]) => void;
|
||||
};
|
||||
|
||||
export const TagInput = React.forwardRef<
|
||||
React.ElementRef<typeof CommandPrimitive.Input>,
|
||||
TagInputProps
|
||||
>(({ className, selectedTags, setSelectedTags, ...props }, ref) => (
|
||||
<div
|
||||
className="flex flex-wrap items-center overflow-auto rounded-lg border px-2 pt-2"
|
||||
cmdk-input-wrapper=""
|
||||
>
|
||||
<div className="flex flex-wrap items-center gap-x-2 gap-y-1">
|
||||
{selectedTags.map((tag: string) => (
|
||||
<Button
|
||||
key={tag}
|
||||
variant="secondary"
|
||||
size="xs"
|
||||
onClick={() => {
|
||||
const newTags = selectedTags.filter((t) => t !== tag);
|
||||
setSelectedTags(newTags);
|
||||
}}
|
||||
>
|
||||
{tag}
|
||||
<X className="ml-1 h-3 w-3" />
|
||||
</Button>
|
||||
))}
|
||||
</div>
|
||||
<CommandPrimitive.Input
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"flex h-11 w-full rounded-md border-transparent bg-transparent px-1 text-sm outline-none placeholder:text-slate-500 focus:border-0 focus:border-none focus:border-transparent focus:ring-0 disabled:cursor-not-allowed disabled:opacity-50 dark:placeholder:text-slate-400 ",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
</div>
|
||||
));
|
||||
|
||||
TagInput.displayName = CommandPrimitive.Input.displayName;
|
||||
@@ -0,0 +1,25 @@
|
||||
import { Button } from "@/src/components/ui/button";
|
||||
import { TagButton } from "@/src/features/tag/components/TagButton";
|
||||
|
||||
type TagListProps = {
|
||||
selectedTags: string[];
|
||||
isLoading: boolean;
|
||||
};
|
||||
|
||||
const TagList = ({ selectedTags, isLoading }: TagListProps) => {
|
||||
return selectedTags.length > 0 ? (
|
||||
selectedTags.map((tag) => (
|
||||
<TagButton key={tag} tag={tag} loading={isLoading} />
|
||||
))
|
||||
) : (
|
||||
<Button
|
||||
variant="outline"
|
||||
size="xs"
|
||||
className="text-xs font-bold opacity-0 hover:bg-white hover:opacity-100"
|
||||
>
|
||||
Add tag
|
||||
</Button>
|
||||
);
|
||||
};
|
||||
|
||||
export default TagList;
|
||||
@@ -0,0 +1,88 @@
|
||||
import TagCommandItem from "@/src/features/tag/components/TagCommandItem";
|
||||
import TagCreateItem from "@/src/features/tag/components/TagCreateItem";
|
||||
import { TagInput } from "@/src/features/tag/components/TagInput";
|
||||
import TagList from "@/src/features/tag/components/TagList";
|
||||
import useTagManager from "@/src/features/tag/hooks/useTagManager";
|
||||
import {
|
||||
Popover,
|
||||
PopoverTrigger,
|
||||
PopoverContent,
|
||||
} from "@/src/components/ui/popover";
|
||||
import { Command, CommandList, CommandGroup } from "cmdk";
|
||||
|
||||
type TagManagerProps = {
|
||||
tags: string[];
|
||||
allTags: string[];
|
||||
hasAccess: boolean;
|
||||
isLoading: boolean;
|
||||
mutateTags: (value: string[]) => void;
|
||||
};
|
||||
|
||||
const TagManager = ({
|
||||
tags,
|
||||
allTags,
|
||||
hasAccess,
|
||||
isLoading,
|
||||
mutateTags,
|
||||
}: TagManagerProps) => {
|
||||
const {
|
||||
selectedTags,
|
||||
inputValue,
|
||||
availableTags,
|
||||
handleItemCreate,
|
||||
setInputValue,
|
||||
setSelectedTags,
|
||||
} = useTagManager({ initialTags: tags, allTags });
|
||||
|
||||
const handlePopoverChange = (open: boolean) => {
|
||||
if (!open && selectedTags !== tags) {
|
||||
setInputValue("");
|
||||
mutateTags(selectedTags);
|
||||
}
|
||||
};
|
||||
|
||||
if (!hasAccess) {
|
||||
return <TagList selectedTags={selectedTags} isLoading={isLoading} />;
|
||||
}
|
||||
|
||||
return (
|
||||
<Popover onOpenChange={(open) => handlePopoverChange(open)}>
|
||||
<PopoverTrigger className="select-none" asChild>
|
||||
<div className="flex flex-wrap gap-x-2 gap-y-1">
|
||||
<TagList selectedTags={selectedTags} isLoading={isLoading} />
|
||||
</div>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent>
|
||||
<Command>
|
||||
<TagInput
|
||||
value={inputValue}
|
||||
onValueChange={setInputValue}
|
||||
selectedTags={selectedTags}
|
||||
setSelectedTags={setSelectedTags}
|
||||
/>
|
||||
<CommandList
|
||||
className={availableTags.length > 0 ? "mt-2" : undefined}
|
||||
>
|
||||
<CommandGroup>
|
||||
{availableTags.map((value: string) => (
|
||||
<TagCommandItem
|
||||
key={value}
|
||||
value={value}
|
||||
selectedTags={selectedTags}
|
||||
setSelectedTags={setSelectedTags}
|
||||
/>
|
||||
))}
|
||||
<TagCreateItem
|
||||
onSelect={handleItemCreate}
|
||||
inputValue={inputValue}
|
||||
options={allTags}
|
||||
/>
|
||||
</CommandGroup>
|
||||
</CommandList>
|
||||
</Command>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
);
|
||||
};
|
||||
|
||||
export default TagManager;
|
||||
@@ -0,0 +1,74 @@
|
||||
import React, { useState } from "react";
|
||||
import { api } from "@/src/utils/api";
|
||||
import { useHasAccess } from "@/src/features/rbac/utils/checkAccess";
|
||||
import { type RouterOutput } from "@/src/utils/types";
|
||||
import TagManager from "@/src/features/tag/components/TagMananger";
|
||||
|
||||
type TagTraceDetailsPopoverProps = {
|
||||
tags: string[];
|
||||
availableTags: string[];
|
||||
projectId: string;
|
||||
traceId: string;
|
||||
};
|
||||
|
||||
export function TagTraceDetailsPopover({
|
||||
tags,
|
||||
availableTags,
|
||||
projectId,
|
||||
traceId,
|
||||
}: TagTraceDetailsPopoverProps) {
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const hasAccess = useHasAccess({ projectId, scope: "objects:tag" });
|
||||
|
||||
const utils = api.useUtils();
|
||||
const mutTags = api.traces.updateTags.useMutation({
|
||||
onMutate: async () => {
|
||||
await utils.traces.byId.cancel();
|
||||
setIsLoading(true);
|
||||
// Snapshot the previous value
|
||||
const prev = utils.traces.byId.getData({ traceId });
|
||||
|
||||
return { prev };
|
||||
},
|
||||
onError: (err, _newTags, context) => {
|
||||
setIsLoading(false);
|
||||
// Rollback to the previous value if mutation fails
|
||||
utils.traces.byId.setData({ traceId }, context?.prev);
|
||||
},
|
||||
onSettled: (data, error, { traceId, tags }) => {
|
||||
setIsLoading(false);
|
||||
utils.traces.byId.setData(
|
||||
{ traceId },
|
||||
(oldQueryData: RouterOutput["traces"]["byId"] | undefined) => {
|
||||
return oldQueryData
|
||||
? {
|
||||
...oldQueryData,
|
||||
tags: tags,
|
||||
}
|
||||
: undefined;
|
||||
},
|
||||
);
|
||||
void utils.traces.all.invalidate();
|
||||
void utils.traces.byId.invalidate();
|
||||
void utils.traces.filterOptions.invalidate();
|
||||
},
|
||||
});
|
||||
|
||||
function mutateTags(newTags: string[]) {
|
||||
void mutTags.mutateAsync({
|
||||
projectId,
|
||||
traceId,
|
||||
tags: newTags,
|
||||
});
|
||||
}
|
||||
|
||||
return (
|
||||
<TagManager
|
||||
tags={tags}
|
||||
allTags={availableTags}
|
||||
hasAccess={hasAccess}
|
||||
isLoading={isLoading}
|
||||
mutateTags={mutateTags}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
import React, { useState } from "react";
|
||||
import { api } from "@/src/utils/api";
|
||||
import { useHasAccess } from "@/src/features/rbac/utils/checkAccess";
|
||||
import { type RouterOutput, type RouterInput } from "@/src/utils/types";
|
||||
import TagManager from "@/src/features/tag/components/TagMananger";
|
||||
|
||||
type TagTracePopverProps = {
|
||||
tags: string[];
|
||||
availableTags: string[];
|
||||
projectId: string;
|
||||
traceId: string;
|
||||
tracesFilter: RouterInput["traces"]["all"];
|
||||
};
|
||||
|
||||
export function TagTracePopver({
|
||||
tags,
|
||||
availableTags,
|
||||
projectId,
|
||||
traceId,
|
||||
tracesFilter,
|
||||
}: TagTracePopverProps) {
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const hasAccess = useHasAccess({ projectId, scope: "objects:tag" });
|
||||
|
||||
const utils = api.useUtils();
|
||||
const mutTags = api.traces.updateTags.useMutation({
|
||||
onMutate: async () => {
|
||||
await utils.traces.all.cancel();
|
||||
setIsLoading(true);
|
||||
const prevTrace = utils.traces.all.getData(tracesFilter);
|
||||
return { prevTrace };
|
||||
},
|
||||
onError: (err, _newTags, context) => {
|
||||
utils.traces.all.setData(tracesFilter, context?.prevTrace);
|
||||
console.log("error", err);
|
||||
setIsLoading(false);
|
||||
},
|
||||
onSettled: (data, error, { traceId, tags }) => {
|
||||
utils.traces.all.setData(
|
||||
tracesFilter,
|
||||
(oldQueryData: RouterOutput["traces"]["all"] | undefined) => {
|
||||
return oldQueryData
|
||||
? {
|
||||
totalCount: oldQueryData.totalCount,
|
||||
traces: oldQueryData.traces.map((trace) => {
|
||||
return trace.id === traceId ? { ...trace, tags } : trace;
|
||||
}),
|
||||
}
|
||||
: { totalCount: undefined, traces: [] };
|
||||
},
|
||||
);
|
||||
setIsLoading(false);
|
||||
},
|
||||
});
|
||||
|
||||
function mutateTags(newTags: string[]) {
|
||||
void mutTags.mutateAsync({
|
||||
projectId,
|
||||
traceId,
|
||||
tags: newTags,
|
||||
});
|
||||
}
|
||||
|
||||
return (
|
||||
<TagManager
|
||||
tags={tags}
|
||||
allTags={availableTags}
|
||||
hasAccess={hasAccess}
|
||||
isLoading={isLoading}
|
||||
mutateTags={mutateTags}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
import { useState, useMemo } from "react";
|
||||
|
||||
type UseTagManagerProps = {
|
||||
initialTags: string[];
|
||||
allTags: string[];
|
||||
};
|
||||
|
||||
function useTagManager({ initialTags, allTags }: UseTagManagerProps) {
|
||||
const [selectedTags, setSelectedTags] = useState(initialTags);
|
||||
const [inputValue, setInputValue] = useState("");
|
||||
|
||||
const availableTags = useMemo(
|
||||
() => allTags.filter((value) => !selectedTags.includes(value)),
|
||||
[allTags, selectedTags],
|
||||
);
|
||||
const handleItemCreate = () => {
|
||||
setSelectedTags([...selectedTags, inputValue]);
|
||||
availableTags.push(inputValue);
|
||||
setInputValue("");
|
||||
};
|
||||
|
||||
return {
|
||||
selectedTags,
|
||||
inputValue,
|
||||
availableTags,
|
||||
handleItemCreate,
|
||||
setInputValue,
|
||||
setSelectedTags,
|
||||
};
|
||||
}
|
||||
|
||||
export default useTagManager;
|
||||
@@ -29,6 +29,7 @@ import "core-js/features/array/to-sorted";
|
||||
// Other CSS
|
||||
import "react18-json-view/src/style.css";
|
||||
import { DetailPageListsProvider } from "@/src/features/navigate-detail-pages/context";
|
||||
|
||||
const setProjectInPosthog = () => {
|
||||
// project
|
||||
const url = window.location.href;
|
||||
|
||||
@@ -25,6 +25,15 @@ import { ScoreProcessor } from "../../../server/api/services/EventProcessor";
|
||||
import { isNotNullOrUndefined } from "@/src/utils/types";
|
||||
import { telemetry } from "@/src/features/telemetry";
|
||||
import { jsonSchema } from "@/src/utils/zod";
|
||||
import * as Sentry from "@sentry/nextjs";
|
||||
|
||||
export const config = {
|
||||
api: {
|
||||
bodyParser: {
|
||||
sizeLimit: "3mb",
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export default async function handler(
|
||||
req: NextApiRequest,
|
||||
@@ -298,6 +307,9 @@ export const handleBatchResult = (
|
||||
error: error.error.message,
|
||||
});
|
||||
} else {
|
||||
if (process.env.NEXT_PUBLIC_SENTRY_DSN) {
|
||||
Sentry.captureException(error);
|
||||
}
|
||||
returnedErrors.push({
|
||||
id: error.id,
|
||||
status: 500,
|
||||
|
||||
@@ -4,6 +4,7 @@ import { api } from "@/src/utils/api";
|
||||
import { useRouter } from "next/router";
|
||||
import { Tabs, TabsList, TabsTrigger } from "@/src/components/ui/tabs";
|
||||
import Link from "next/link";
|
||||
import { DetailPageNav } from "@/src/features/navigate-detail-pages/DetailPageNav";
|
||||
|
||||
export default function Dataset() {
|
||||
const router = useRouter();
|
||||
@@ -23,6 +24,13 @@ export default function Dataset() {
|
||||
{ name: "Datasets", href: `/project/${projectId}/datasets` },
|
||||
{ name: dataset.data?.name ?? datasetId },
|
||||
]}
|
||||
actionButtons={
|
||||
<DetailPageNav
|
||||
currentId={datasetId}
|
||||
path={(id) => `/project/${projectId}/datasets/${id}`}
|
||||
listKey="datasets"
|
||||
/>
|
||||
}
|
||||
/>
|
||||
<Tabs value="runs" className="mb-3">
|
||||
<TabsList>
|
||||
|
||||
@@ -4,6 +4,7 @@ import { useRouter } from "next/router";
|
||||
import { Tabs, TabsList, TabsTrigger } from "@/src/components/ui/tabs";
|
||||
import Link from "next/link";
|
||||
import { DatasetItemsTable } from "@/src/features/datasets/components/DatasetItemsTable";
|
||||
import { DetailPageNav } from "@/src/features/navigate-detail-pages/DetailPageNav";
|
||||
|
||||
export default function DatasetItems() {
|
||||
const router = useRouter();
|
||||
@@ -23,6 +24,13 @@ export default function DatasetItems() {
|
||||
{ name: "Datasets", href: `/project/${projectId}/datasets` },
|
||||
{ name: dataset.data?.name ?? datasetId },
|
||||
]}
|
||||
actionButtons={
|
||||
<DetailPageNav
|
||||
currentId={datasetId}
|
||||
path={(id) => `/project/${projectId}/datasets/${id}/items/`}
|
||||
listKey="datasets"
|
||||
/>
|
||||
}
|
||||
/>
|
||||
<Tabs value="items" className="mb-3">
|
||||
<TabsList>
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import Header from "@/src/components/layouts/header";
|
||||
import { DatasetRunItemsTable } from "@/src/features/datasets/components/DatasetRunItemsTable";
|
||||
import { EditDatasetItem } from "@/src/features/datasets/components/EditDatasetItem";
|
||||
import { DetailPageNav } from "@/src/features/navigate-detail-pages/DetailPageNav";
|
||||
import { api } from "@/src/utils/api";
|
||||
import { useRouter } from "next/router";
|
||||
|
||||
@@ -27,6 +28,15 @@ export default function Dataset() {
|
||||
},
|
||||
{ name: "Item: " + itemId },
|
||||
]}
|
||||
actionButtons={
|
||||
<DetailPageNav
|
||||
currentId={itemId}
|
||||
path={(id) =>
|
||||
`/project/${projectId}/datasets/${datasetId}/items/${id}`
|
||||
}
|
||||
listKey="datasetItems"
|
||||
/>
|
||||
}
|
||||
/>
|
||||
<EditDatasetItem
|
||||
projectId={projectId}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import Header from "@/src/components/layouts/header";
|
||||
import { DatasetRunItemsTable } from "@/src/features/datasets/components/DatasetRunItemsTable";
|
||||
import { DetailPageNav } from "@/src/features/navigate-detail-pages/DetailPageNav";
|
||||
import { api } from "@/src/utils/api";
|
||||
import { useRouter } from "next/router";
|
||||
|
||||
@@ -26,6 +27,15 @@ export default function Dataset() {
|
||||
},
|
||||
{ name: "Run: " + runId },
|
||||
]}
|
||||
actionButtons={
|
||||
<DetailPageNav
|
||||
currentId={runId}
|
||||
path={(id) =>
|
||||
`/project/${projectId}/datasets/${datasetId}/runs/${id}`
|
||||
}
|
||||
listKey="datasetRuns"
|
||||
/>
|
||||
}
|
||||
/>
|
||||
<DatasetRunItemsTable
|
||||
projectId={projectId}
|
||||
|
||||
@@ -105,6 +105,7 @@ export default function Start() {
|
||||
projectId={projectId}
|
||||
globalFilterState={globalFilterState}
|
||||
/>
|
||||
|
||||
<MetricTable
|
||||
className="col-span-1 xl:col-span-2"
|
||||
projectId={projectId}
|
||||
|
||||
@@ -60,7 +60,7 @@ export const observationsTableCols: ColumnDefinition[] = [
|
||||
{
|
||||
name: "metadata",
|
||||
type: "stringObject",
|
||||
internal: 't."metadata"',
|
||||
internal: 'o."metadata"',
|
||||
},
|
||||
{
|
||||
name: "version",
|
||||
|
||||
@@ -6,7 +6,7 @@ import {
|
||||
} from "@/src/server/api/trpc";
|
||||
|
||||
import { type Observation, Prisma } from "@prisma/client";
|
||||
import { paginationZod } from "@/src/utils/zod";
|
||||
import { jsonSchema, paginationZod } from "@/src/utils/zod";
|
||||
import { singleFilter } from "@/src/server/api/interfaces/filters";
|
||||
import {
|
||||
datetimeFilterToPrismaSql,
|
||||
@@ -115,8 +115,7 @@ export const generationsRouter = createTRPCRouter({
|
||||
o.total_tokens as "totalTokens",
|
||||
o.level,
|
||||
o.status_message as "statusMessage",
|
||||
o.version,
|
||||
(count(*) OVER())::int AS "totalCount"
|
||||
o.version
|
||||
FROM observations_with_latency o
|
||||
JOIN traces t ON t.id = o.trace_id
|
||||
WHERE
|
||||
@@ -129,25 +128,52 @@ export const generationsRouter = createTRPCRouter({
|
||||
`,
|
||||
);
|
||||
|
||||
const pricings = await ctx.prisma.pricing.findMany();
|
||||
const totalGenerations = await ctx.prisma.$queryRaw<
|
||||
Array<{ count: bigint }>
|
||||
>(
|
||||
Prisma.sql`
|
||||
WITH observations_with_latency AS (
|
||||
SELECT
|
||||
o.*,
|
||||
CASE WHEN o.end_time IS NULL THEN NULL ELSE (EXTRACT(EPOCH FROM o."end_time") - EXTRACT(EPOCH FROM o."start_time"))::double precision END AS "latency"
|
||||
FROM observations o
|
||||
WHERE o.type = 'GENERATION'
|
||||
AND o.project_id = ${input.projectId}
|
||||
${datetimeFilter}
|
||||
)
|
||||
SELECT
|
||||
count(*)
|
||||
FROM observations_with_latency o
|
||||
JOIN traces t ON t.id = o.trace_id
|
||||
WHERE
|
||||
t.project_id = ${input.projectId}
|
||||
${searchCondition}
|
||||
${filterCondition}
|
||||
`,
|
||||
);
|
||||
|
||||
return generations.map(({ input, output, ...rest }) => {
|
||||
return {
|
||||
...rest,
|
||||
input,
|
||||
output,
|
||||
cost: rest.model
|
||||
? calculateTokenCost(pricings, {
|
||||
model: rest.model,
|
||||
totalTokens: new Decimal(rest.totalTokens),
|
||||
promptTokens: new Decimal(rest.promptTokens),
|
||||
completionTokens: new Decimal(rest.completionTokens),
|
||||
input: input,
|
||||
output: output,
|
||||
})
|
||||
: undefined,
|
||||
};
|
||||
});
|
||||
const pricings = await ctx.prisma.pricing.findMany();
|
||||
const count = totalGenerations[0]?.count;
|
||||
return {
|
||||
totalCount: count ? Number(count) : undefined,
|
||||
generations: generations.map(({ input, output, ...rest }) => {
|
||||
return {
|
||||
...rest,
|
||||
input,
|
||||
output,
|
||||
cost: rest.model
|
||||
? calculateTokenCost(pricings, {
|
||||
model: rest.model,
|
||||
totalTokens: new Decimal(rest.totalTokens),
|
||||
promptTokens: new Decimal(rest.promptTokens),
|
||||
completionTokens: new Decimal(rest.completionTokens),
|
||||
input: input,
|
||||
output: output,
|
||||
})
|
||||
: undefined,
|
||||
};
|
||||
}),
|
||||
};
|
||||
}),
|
||||
|
||||
export: protectedProjectProcedure
|
||||
@@ -253,7 +279,7 @@ export const generationsRouter = createTRPCRouter({
|
||||
generation.startTime.toISOString(),
|
||||
generation.endTime?.toISOString() ?? "",
|
||||
generation.cost
|
||||
? usdFormatter(generation.cost.toNumber())
|
||||
? usdFormatter(generation.cost.toNumber(), 2, 8)
|
||||
: "",
|
||||
JSON.stringify(generation.input),
|
||||
JSON.stringify(generation.output),
|
||||
@@ -277,9 +303,11 @@ export const generationsRouter = createTRPCRouter({
|
||||
content: z.string(),
|
||||
}),
|
||||
);
|
||||
const outputSchema = z.object({
|
||||
completion: z.string(),
|
||||
});
|
||||
const outputSchema = z
|
||||
.object({
|
||||
completion: jsonSchema,
|
||||
})
|
||||
.or(jsonSchema);
|
||||
output = enrichedGenerations
|
||||
.map((generation) => ({
|
||||
parsedInput: inputSchemaOpenAI.safeParse(generation.input),
|
||||
@@ -293,7 +321,14 @@ export const generationsRouter = createTRPCRouter({
|
||||
? [
|
||||
{
|
||||
role: "assistant",
|
||||
content: generation.parsedOutput.data.completion,
|
||||
content:
|
||||
typeof generation.parsedOutput.data ===
|
||||
"object" &&
|
||||
"completion" in generation.parsedOutput.data
|
||||
? JSON.stringify(
|
||||
generation.parsedOutput.data.completion,
|
||||
)
|
||||
: JSON.stringify(generation.parsedOutput.data),
|
||||
},
|
||||
]
|
||||
: [],
|
||||
@@ -303,6 +338,7 @@ export const generationsRouter = createTRPCRouter({
|
||||
// to jsonl
|
||||
.map((row) => JSON.stringify(row))
|
||||
.join("\n");
|
||||
console.log(output);
|
||||
break;
|
||||
default:
|
||||
throw new Error("Invalid export file format");
|
||||
|
||||
@@ -20,7 +20,6 @@ export const observationsRouter = createTRPCRouter({
|
||||
traceId: input.traceId,
|
||||
},
|
||||
});
|
||||
|
||||
const scores = generation.traceId
|
||||
? await ctx.prisma.score.findMany({
|
||||
where: {
|
||||
|
||||
@@ -22,6 +22,8 @@ import { throwIfNoAccess } from "@/src/features/rbac/utils/checkAccess";
|
||||
import { TRPCError } from "@trpc/server";
|
||||
import { orderBy } from "@/src/server/api/interfaces/orderBy";
|
||||
import { orderByToPrismaSql } from "@/src/features/orderBy/server/orderByToPrisma";
|
||||
import { type Sql } from "@prisma/client/runtime/library";
|
||||
import { instrumentAsync } from "@/src/utils/instrumentation";
|
||||
|
||||
const TraceFilterOptions = z.object({
|
||||
projectId: z.string(), // Required for protectedProjectProcedure
|
||||
@@ -70,58 +72,8 @@ export const traceRouter = createTRPCRouter({
|
||||
)`
|
||||
: Prisma.empty;
|
||||
|
||||
const query = Prisma.sql`
|
||||
WITH usage AS (
|
||||
SELECT
|
||||
trace_id,
|
||||
sum(prompt_tokens) AS "promptTokens",
|
||||
sum(completion_tokens) AS "completionTokens",
|
||||
sum(total_tokens) AS "totalTokens"
|
||||
FROM
|
||||
"observations"
|
||||
WHERE
|
||||
"trace_id" IS NOT NULL
|
||||
AND "type" = 'GENERATION'
|
||||
AND "project_id" = ${input.projectId}
|
||||
${observationTimeseriesFilter}
|
||||
GROUP BY
|
||||
trace_id
|
||||
),
|
||||
trace_latency AS (
|
||||
SELECT
|
||||
trace_id,
|
||||
EXTRACT(EPOCH FROM COALESCE(MAX("end_time"), MAX("start_time"))) - EXTRACT(EPOCH FROM MIN("start_time"))::double precision AS "latency"
|
||||
FROM
|
||||
"observations"
|
||||
WHERE
|
||||
"trace_id" IS NOT NULL
|
||||
AND "project_id" = ${input.projectId}
|
||||
${observationTimeseriesFilter}
|
||||
GROUP BY
|
||||
trace_id
|
||||
),
|
||||
-- used for filtering
|
||||
scores_avg AS (
|
||||
SELECT
|
||||
trace_id,
|
||||
jsonb_object_agg(name::text, avg_value::double precision) AS scores_avg
|
||||
FROM (
|
||||
SELECT
|
||||
trace_id,
|
||||
name,
|
||||
avg(value) avg_value
|
||||
FROM
|
||||
scores
|
||||
GROUP BY
|
||||
1,
|
||||
2
|
||||
ORDER BY
|
||||
1) tmp
|
||||
GROUP BY
|
||||
1
|
||||
)
|
||||
SELECT
|
||||
t.*,
|
||||
const tracesQuery = createTracesQuery(
|
||||
Prisma.sql`t.*,
|
||||
t."user_id" AS "userId",
|
||||
t."metadata" AS "metadata",
|
||||
t.session_id AS "sessionId",
|
||||
@@ -129,34 +81,48 @@ export const traceRouter = createTRPCRouter({
|
||||
COALESCE(u."promptTokens", 0)::int AS "promptTokens",
|
||||
COALESCE(u."completionTokens", 0)::int AS "completionTokens",
|
||||
COALESCE(u."totalTokens", 0)::int AS "totalTokens",
|
||||
tl.latency AS "latency",
|
||||
(count(*) OVER ())::int AS "totalCount"
|
||||
FROM
|
||||
"traces" AS t
|
||||
LEFT JOIN usage AS u ON u.trace_id = t.id
|
||||
-- used for filtering
|
||||
LEFT JOIN scores_avg AS s_avg ON s_avg.trace_id = t.id
|
||||
LEFT JOIN trace_latency AS tl ON tl.trace_id = t.id
|
||||
WHERE
|
||||
t."project_id" = ${input.projectId}
|
||||
${searchCondition}
|
||||
${filterCondition}
|
||||
${orderByCondition}
|
||||
LIMIT ${input.limit}
|
||||
OFFSET ${input.page * input.limit}
|
||||
`;
|
||||
tl.latency AS "latency"`,
|
||||
input.projectId,
|
||||
observationTimeseriesFilter,
|
||||
input.page,
|
||||
input.limit,
|
||||
searchCondition,
|
||||
filterCondition,
|
||||
orderByCondition,
|
||||
);
|
||||
|
||||
const traces = await ctx.prisma.$queryRaw<
|
||||
Array<
|
||||
Trace & {
|
||||
promptTokens: number;
|
||||
completionTokens: number;
|
||||
totalTokens: number;
|
||||
totalCount: number;
|
||||
latency: number | null;
|
||||
}
|
||||
>
|
||||
>(query);
|
||||
const traces = await instrumentAsync(
|
||||
{ name: "get-all-traces" },
|
||||
async () =>
|
||||
await ctx.prisma.$queryRaw<
|
||||
Array<
|
||||
Trace & {
|
||||
promptTokens: number;
|
||||
completionTokens: number;
|
||||
totalTokens: number;
|
||||
totalCount: number;
|
||||
latency: number | null;
|
||||
}
|
||||
>
|
||||
>(tracesQuery),
|
||||
);
|
||||
|
||||
const countQyery = createTracesQuery(
|
||||
Prisma.sql`count(*)`,
|
||||
input.projectId,
|
||||
observationTimeseriesFilter,
|
||||
0,
|
||||
1,
|
||||
searchCondition,
|
||||
filterCondition,
|
||||
Prisma.empty,
|
||||
);
|
||||
|
||||
const totalTraces = await instrumentAsync(
|
||||
{ name: "get-total-traces" },
|
||||
async () =>
|
||||
await ctx.prisma.$queryRaw<Array<{ count: bigint }>>(countQyery),
|
||||
);
|
||||
|
||||
// get scores for each trace individually to increase
|
||||
// performance of the query above
|
||||
@@ -170,10 +136,14 @@ export const traceRouter = createTRPCRouter({
|
||||
},
|
||||
},
|
||||
});
|
||||
return traces.map((trace) => {
|
||||
const filteredScores = scores.filter((s) => s.traceId === trace.id);
|
||||
return { ...trace, scores: filteredScores };
|
||||
});
|
||||
const totalTraceCount = totalTraces[0]?.count;
|
||||
return {
|
||||
traces: traces.map((trace) => {
|
||||
const filteredScores = scores.filter((s) => s.traceId === trace.id);
|
||||
return { ...trace, scores: filteredScores };
|
||||
}),
|
||||
totalCount: totalTraceCount ? Number(totalTraceCount) : undefined,
|
||||
};
|
||||
}),
|
||||
filterOptions: protectedProjectProcedure
|
||||
.input(z.object({ projectId: z.string() }))
|
||||
@@ -343,7 +313,6 @@ export const traceRouter = createTRPCRouter({
|
||||
bookmarked: input.bookmarked,
|
||||
},
|
||||
});
|
||||
|
||||
return trace;
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
@@ -404,4 +373,113 @@ export const traceRouter = createTRPCRouter({
|
||||
}
|
||||
}
|
||||
}),
|
||||
updateTags: protectedProjectProcedure
|
||||
.input(
|
||||
z.object({
|
||||
projectId: z.string(),
|
||||
traceId: z.string(),
|
||||
tags: z.array(z.string()),
|
||||
}),
|
||||
)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
throwIfNoAccess({
|
||||
session: ctx.session,
|
||||
projectId: input.projectId,
|
||||
scope: "objects:tag",
|
||||
});
|
||||
try {
|
||||
const trace = await ctx.prisma.trace.update({
|
||||
where: {
|
||||
id: input.traceId,
|
||||
projectId: input.projectId,
|
||||
},
|
||||
data: {
|
||||
tags: {
|
||||
set: input.tags,
|
||||
},
|
||||
},
|
||||
});
|
||||
return trace;
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
}
|
||||
}),
|
||||
});
|
||||
|
||||
function createTracesQuery(
|
||||
select: Sql,
|
||||
projectId: string,
|
||||
observationTimeseriesFilter: Sql,
|
||||
page: number,
|
||||
limit: number,
|
||||
searchCondition: Sql,
|
||||
filterCondition: Sql,
|
||||
orderByCondition: Sql,
|
||||
) {
|
||||
return Prisma.sql`
|
||||
WITH usage AS (
|
||||
SELECT
|
||||
trace_id,
|
||||
sum(prompt_tokens) AS "promptTokens",
|
||||
sum(completion_tokens) AS "completionTokens",
|
||||
sum(total_tokens) AS "totalTokens"
|
||||
FROM
|
||||
"observations"
|
||||
WHERE
|
||||
"trace_id" IS NOT NULL
|
||||
AND "type" = 'GENERATION'
|
||||
AND "project_id" = ${projectId}
|
||||
${observationTimeseriesFilter}
|
||||
GROUP BY
|
||||
trace_id
|
||||
),
|
||||
trace_latency AS (
|
||||
SELECT
|
||||
trace_id,
|
||||
EXTRACT(EPOCH FROM COALESCE(MAX("end_time"), MAX("start_time"))) - EXTRACT(EPOCH FROM MIN("start_time"))::double precision AS "latency"
|
||||
FROM
|
||||
"observations"
|
||||
WHERE
|
||||
"trace_id" IS NOT NULL
|
||||
AND "project_id" = ${projectId}
|
||||
${observationTimeseriesFilter}
|
||||
GROUP BY
|
||||
trace_id
|
||||
),
|
||||
-- used for filtering
|
||||
scores_avg AS (
|
||||
SELECT
|
||||
trace_id,
|
||||
jsonb_object_agg(name::text, avg_value::double precision) AS scores_avg
|
||||
FROM (
|
||||
SELECT
|
||||
trace_id,
|
||||
name,
|
||||
avg(value) avg_value
|
||||
FROM
|
||||
scores
|
||||
GROUP BY
|
||||
1,
|
||||
2
|
||||
ORDER BY
|
||||
1) tmp
|
||||
GROUP BY
|
||||
1
|
||||
)
|
||||
SELECT
|
||||
${select}
|
||||
FROM
|
||||
"traces" AS t
|
||||
LEFT JOIN usage AS u ON u.trace_id = t.id
|
||||
-- used for filtering
|
||||
LEFT JOIN scores_avg AS s_avg ON s_avg.trace_id = t.id
|
||||
LEFT JOIN trace_latency AS tl ON tl.trace_id = t.id
|
||||
WHERE
|
||||
t."project_id" = ${projectId}
|
||||
${searchCondition}
|
||||
${filterCondition}
|
||||
${orderByCondition}
|
||||
LIMIT ${limit}
|
||||
OFFSET ${page * limit}
|
||||
`;
|
||||
}
|
||||
|
||||
@@ -277,7 +277,9 @@ export class TraceProcessor implements EventProcessor {
|
||||
const { body } = this.event;
|
||||
|
||||
if (apiScope.accessLevel !== "all")
|
||||
throw new AuthenticationError("Access denied for trace creation");
|
||||
throw new AuthenticationError(
|
||||
`Access denied for trace creation, ${apiScope.accessLevel}`,
|
||||
);
|
||||
|
||||
const internalId = body.id ?? v4();
|
||||
|
||||
@@ -294,6 +296,16 @@ export class TraceProcessor implements EventProcessor {
|
||||
},
|
||||
});
|
||||
|
||||
// access rights note:
|
||||
// if trace exists, check if project id matches
|
||||
// if trace does not exist, insert the trace with the projectId from scope
|
||||
|
||||
if (existingTrace && existingTrace.projectId !== apiScope.projectId) {
|
||||
throw new AuthenticationError(
|
||||
`Access denied for trace creation ${existingTrace.projectId} `,
|
||||
);
|
||||
}
|
||||
|
||||
const mergedMetadata = mergeJson(
|
||||
existingTrace?.metadata
|
||||
? jsonSchema.parse(existingTrace.metadata)
|
||||
@@ -304,7 +316,6 @@ export class TraceProcessor implements EventProcessor {
|
||||
const upsertedTrace = await prisma.trace.upsert({
|
||||
where: {
|
||||
id: internalId,
|
||||
projectId: apiScope.projectId,
|
||||
},
|
||||
create: {
|
||||
id: internalId,
|
||||
|
||||
@@ -70,6 +70,9 @@ export const createTRPCContext = async (opts: CreateNextContextOptions) => {
|
||||
import { initTRPC, TRPCError } from "@trpc/server";
|
||||
import superjson from "superjson";
|
||||
import { ZodError } from "zod";
|
||||
import { setUpSuperjson } from "@/src/utils/superjson";
|
||||
|
||||
setUpSuperjson();
|
||||
|
||||
const t = initTRPC.context<typeof createTRPCContext>().create({
|
||||
transformer: superjson,
|
||||
|
||||
@@ -16,6 +16,9 @@ import { type inferRouterInputs, type inferRouterOutputs } from "@trpc/server";
|
||||
import superjson from "superjson";
|
||||
|
||||
import { type AppRouter } from "@/src/server/api/root";
|
||||
import { setUpSuperjson } from "@/src/utils/superjson";
|
||||
|
||||
setUpSuperjson();
|
||||
|
||||
const getBaseUrl = () => {
|
||||
if (typeof window !== "undefined") return ""; // browser should use relative url
|
||||
|
||||
+11
-5
@@ -14,12 +14,18 @@ export const numberFormatter = (number: number) => {
|
||||
}).format(number);
|
||||
};
|
||||
|
||||
export const usdFormatter = (number: number) =>
|
||||
new Intl.NumberFormat("en-US", {
|
||||
export const usdFormatter = (
|
||||
number: number,
|
||||
minimumFractionDigits: number = 2,
|
||||
maximumFractionDigits: number = 4,
|
||||
) => {
|
||||
return new Intl.NumberFormat("en-US", {
|
||||
style: "currency",
|
||||
currency: "USD",
|
||||
|
||||
// These options are needed to round to whole numbers if that's what you want.
|
||||
//minimumFractionDigits: 0, // (this suffices for whole numbers, but will print 2500.10 as $2,500.1)
|
||||
//maximumFractionDigits: 0, // (causes 2500.99 to be printed as $2,501)
|
||||
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/NumberFormat/NumberFormat#minimumfractiondigits
|
||||
minimumFractionDigits,
|
||||
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/NumberFormat/NumberFormat#maximumfractiondigits
|
||||
maximumFractionDigits,
|
||||
}).format(number);
|
||||
};
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
import Decimal from "decimal.js";
|
||||
import superjson from "superjson";
|
||||
|
||||
export const setUpSuperjson = () => {
|
||||
superjson.registerCustom<Decimal, string>(
|
||||
{
|
||||
isApplicable: (v): v is Decimal => Decimal.isDecimal(v),
|
||||
serialize: (v) => v.toJSON(),
|
||||
deserialize: (v) => new Decimal(v),
|
||||
},
|
||||
"decimal.js",
|
||||
);
|
||||
};
|
||||
Reference in New Issue
Block a user