Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
63df788908 | ||
|
|
fb463cbf98 | ||
|
|
cfd99efd48 | ||
|
|
3b2c5f8bf3 | ||
|
|
d08a276b67 | ||
|
|
23bc2cd39c | ||
|
|
a31eda8c68 | ||
|
|
d044ae51d1 | ||
|
|
552578afc3 | ||
|
|
00997db325 | ||
|
|
550464cf0b | ||
|
|
e43568a3d2 | ||
|
|
4869e8a093 | ||
|
|
d9b307ec87 | ||
|
|
3abfbef4c3 | ||
|
|
86aa1eb34f | ||
|
|
59edc9851a | ||
|
|
0e31dcef99 | ||
|
|
a67235ab8a | ||
|
|
eb88c9bdf8 | ||
|
|
77e4791cbe | ||
|
|
5d922dfae7 | ||
|
|
c704c84451 | ||
|
|
6f23488438 | ||
|
|
ca50abb290 | ||
|
|
15067114a2 | ||
|
|
645f14535b | ||
|
|
250a5b03d2 | ||
|
|
5ee060dfc6 | ||
|
|
f08502cde4 | ||
|
|
90ecb88249 | ||
|
|
16e60f74da |
@@ -129,6 +129,7 @@ types:
|
||||
- optional<string>
|
||||
- optional<integer>
|
||||
- optional<boolean>
|
||||
- optional<list<string>>
|
||||
DatasetStatus:
|
||||
enum:
|
||||
- ACTIVE
|
||||
|
||||
@@ -26,6 +26,9 @@ service:
|
||||
limit: optional<integer>
|
||||
userId: optional<string>
|
||||
name: optional<string>
|
||||
orderBy:
|
||||
type: string
|
||||
docs: Format of the string sort_by=timestamp.asc (id, timestamp, name, userId, release, version, public, bookmarked, sessionId)
|
||||
tags:
|
||||
type: optional<string>
|
||||
allow-multiple: true
|
||||
@@ -37,3 +40,6 @@ types:
|
||||
properties:
|
||||
data: list<commons.TraceWithDetails>
|
||||
meta: pagination.MetaResponse
|
||||
Sort:
|
||||
properties:
|
||||
id: string
|
||||
|
||||
@@ -884,6 +884,14 @@ paths:
|
||||
schema:
|
||||
type: string
|
||||
nullable: true
|
||||
- name: orderBy
|
||||
in: query
|
||||
description: >-
|
||||
Format of the string sort_by=timestamp.asc (id, timestamp, name,
|
||||
userId, release, version, public, bookmarked, sessionId)
|
||||
required: true
|
||||
schema:
|
||||
type: string
|
||||
- name: tags
|
||||
in: query
|
||||
description: Only traces that include all of these tags will be returned.
|
||||
@@ -1275,6 +1283,10 @@ components:
|
||||
nullable: true
|
||||
- type: boolean
|
||||
nullable: true
|
||||
- type: array
|
||||
items:
|
||||
type: string
|
||||
nullable: true
|
||||
DatasetStatus:
|
||||
title: DatasetStatus
|
||||
type: string
|
||||
@@ -1988,6 +2000,14 @@ components:
|
||||
required:
|
||||
- data
|
||||
- meta
|
||||
Sort:
|
||||
title: Sort
|
||||
type: object
|
||||
properties:
|
||||
id:
|
||||
type: string
|
||||
required:
|
||||
- id
|
||||
utilsMetaResponse:
|
||||
title: utilsMetaResponse
|
||||
type: object
|
||||
|
||||
@@ -708,7 +708,7 @@
|
||||
"request": {
|
||||
"description": "Get list of traces",
|
||||
"url": {
|
||||
"raw": "{{baseUrl}}/api/public/traces?page=&limit=&userId=&name=&tags=",
|
||||
"raw": "{{baseUrl}}/api/public/traces?page=&limit=&userId=&name=&orderBy=&tags=",
|
||||
"host": [
|
||||
"{{baseUrl}}"
|
||||
],
|
||||
@@ -738,6 +738,11 @@
|
||||
"value": "",
|
||||
"description": null
|
||||
},
|
||||
{
|
||||
"key": "orderBy",
|
||||
"value": "",
|
||||
"description": "Format of the string sort_by=timestamp.asc (id, timestamp, name, userId, release, version, public, bookmarked, sessionId)"
|
||||
},
|
||||
{
|
||||
"key": "tags",
|
||||
"value": "",
|
||||
|
||||
Generated
+342
-309
File diff suppressed because it is too large
Load Diff
+11
-11
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "langfuse-core",
|
||||
"version": "1.33.3",
|
||||
"version": "1.34.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"prebuild": "cp generated/openapi-client/openapi.yml public/openapi-client.yml && cp generated/openapi-server/openapi.yml public/openapi-server.yml",
|
||||
@@ -33,7 +33,7 @@
|
||||
"@heroicons/react": "^2.1.1",
|
||||
"@hookform/resolvers": "^3.3.4",
|
||||
"@next-auth/prisma-adapter": "^1.0.7",
|
||||
"@prisma/client": "^5.7.1",
|
||||
"@prisma/client": "^5.8.1",
|
||||
"@radix-ui/react-accordion": "^1.1.2",
|
||||
"@radix-ui/react-alert-dialog": "^1.0.5",
|
||||
"@radix-ui/react-avatar": "^1.0.4",
|
||||
@@ -56,8 +56,8 @@
|
||||
"@radix-ui/react-tooltip": "^1.0.7",
|
||||
"@react-email/components": "^0.0.12",
|
||||
"@react-email/render": "^0.0.10",
|
||||
"@sentry/nextjs": "^7.92.0",
|
||||
"@sentry/profiling-node": "^1.3.2",
|
||||
"@sentry/nextjs": "^7.95.0",
|
||||
"@sentry/profiling-node": "^1.3.5",
|
||||
"@sentry/types": "^7.88.0",
|
||||
"@t3-oss/env-nextjs": "^0.7.1",
|
||||
"@tailwindcss/forms": "^0.5.7",
|
||||
@@ -75,16 +75,16 @@
|
||||
"cmdk": "^0.2.0",
|
||||
"core-js": "^3.35.0",
|
||||
"cors": "^2.8.5",
|
||||
"date-fns": "^3.1.0",
|
||||
"date-fns": "^3.3.1",
|
||||
"decimal.js": "^10.4.3",
|
||||
"exponential-backoff": "^3.1.1",
|
||||
"lodash": "^4.17.21",
|
||||
"lucide-react": "^0.306.0",
|
||||
"next": "^14.0.4",
|
||||
"next": "^14.1.0",
|
||||
"next-auth": "^4.24.5",
|
||||
"next-query-params": "^5.0.0",
|
||||
"nodemailer": "^6.9.8",
|
||||
"posthog-js": "^1.96.1",
|
||||
"posthog-js": "^1.100.0",
|
||||
"posthog-node": "^3.2.1",
|
||||
"react": "18.2.0",
|
||||
"react-day-picker": "^8.10.0",
|
||||
@@ -118,18 +118,18 @@
|
||||
"@types/react": "^18.2.46",
|
||||
"@types/react-dom": "^18.2.18",
|
||||
"@types/uuid": "^9.0.7",
|
||||
"@typescript-eslint/eslint-plugin": "^6.17.0",
|
||||
"@typescript-eslint/parser": "^6.16.0",
|
||||
"@typescript-eslint/eslint-plugin": "^6.19.0",
|
||||
"@typescript-eslint/parser": "^6.19.1",
|
||||
"autoprefixer": "^10.4.16",
|
||||
"dotenv-cli": "^7.3.0",
|
||||
"eslint": "^8.56.0",
|
||||
"eslint-config-next": "^14.0.4",
|
||||
"eslint-config-next": "^14.1.0",
|
||||
"jest": "^29.7.0",
|
||||
"jest-environment-jsdom": "^29.7.0",
|
||||
"postcss": "^8.4.33",
|
||||
"prettier": "^3.1.1",
|
||||
"prettier-plugin-tailwindcss": "^0.5.11",
|
||||
"prisma": "^5.7.1",
|
||||
"prisma": "^5.8.1",
|
||||
"prisma-erd-generator": "^1.11.2",
|
||||
"release-it": "^17.0.1",
|
||||
"tailwindcss": "^3.4.1",
|
||||
|
||||
@@ -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])
|
||||
|
||||
@@ -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" }],
|
||||
|
||||
+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>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -34,6 +34,7 @@ import {
|
||||
exportOptions,
|
||||
type ExportFileFormats,
|
||||
} from "@/src/server/api/interfaces/exportTypes";
|
||||
import { useOrderByState } from "@/src/features/orderBy/hooks/useOrderByState";
|
||||
|
||||
export type GenerationsTableRow = {
|
||||
id: string;
|
||||
@@ -82,37 +83,21 @@ export default function GenerationsTable({ projectId }: GenerationsTableProps) {
|
||||
},
|
||||
]);
|
||||
|
||||
const generationsQueries = api.useQueries((t) => [
|
||||
t.generations.all({
|
||||
page: paginationState.pageIndex,
|
||||
limit: paginationState.pageSize / 2,
|
||||
projectId,
|
||||
filter: filterState,
|
||||
searchQuery,
|
||||
}),
|
||||
t.generations.all({
|
||||
page: paginationState.pageIndex + paginationState.pageSize / 2,
|
||||
limit: paginationState.pageSize / 2,
|
||||
projectId,
|
||||
filter: filterState,
|
||||
searchQuery,
|
||||
}),
|
||||
]);
|
||||
const [orderByState, setOrderByState] = useOrderByState({
|
||||
column: "startTime",
|
||||
order: "DESC",
|
||||
});
|
||||
|
||||
const generations = {
|
||||
isLoading:
|
||||
generationsQueries[0].isLoading || generationsQueries[1].isLoading,
|
||||
isError: generationsQueries[0].isError || generationsQueries[1].isError,
|
||||
isSuccess:
|
||||
generationsQueries[0].isSuccess || generationsQueries[1].isSuccess,
|
||||
data: [
|
||||
...(generationsQueries[0].data?.generations ?? []),
|
||||
...(generationsQueries[1].data?.generations ?? []),
|
||||
],
|
||||
error: generationsQueries[0].error ?? generationsQueries[1].error,
|
||||
};
|
||||
const generations = api.generations.all.useQuery({
|
||||
page: paginationState.pageIndex,
|
||||
limit: paginationState.pageSize,
|
||||
projectId,
|
||||
filter: filterState,
|
||||
orderBy: orderByState,
|
||||
searchQuery,
|
||||
});
|
||||
|
||||
const totalCount = generationsQueries[0].data?.totalCount ?? 0;
|
||||
const totalCount = generations.data?.totalCount ?? 0;
|
||||
|
||||
const filterOptions = api.generations.filterOptions.useQuery({
|
||||
projectId,
|
||||
@@ -166,6 +151,7 @@ export default function GenerationsTable({ projectId }: GenerationsTableProps) {
|
||||
const columns: LangfuseColumnDef<GenerationsTableRow>[] = [
|
||||
{
|
||||
accessorKey: "id",
|
||||
id: "id",
|
||||
header: "ID",
|
||||
cell: ({ row }) => {
|
||||
const observationId = row.getValue("id");
|
||||
@@ -178,13 +164,17 @@ export default function GenerationsTable({ projectId }: GenerationsTableProps) {
|
||||
/>
|
||||
) : null;
|
||||
},
|
||||
enableSorting: true,
|
||||
},
|
||||
{
|
||||
accessorKey: "name",
|
||||
id: "name",
|
||||
header: "name",
|
||||
enableSorting: true,
|
||||
},
|
||||
{
|
||||
accessorKey: "traceId",
|
||||
id: "traceId",
|
||||
header: "Trace ID",
|
||||
cell: ({ row }) => {
|
||||
const value = row.getValue("traceId");
|
||||
@@ -195,19 +185,25 @@ export default function GenerationsTable({ projectId }: GenerationsTableProps) {
|
||||
/>
|
||||
) : undefined;
|
||||
},
|
||||
enableSorting: true,
|
||||
},
|
||||
{
|
||||
accessorKey: "traceName",
|
||||
id: "traceName",
|
||||
header: "Trace Name",
|
||||
enableHiding: true,
|
||||
enableSorting: true,
|
||||
},
|
||||
{
|
||||
accessorKey: "startTime",
|
||||
id: "startTime",
|
||||
header: "Start Time",
|
||||
enableHiding: true,
|
||||
enableSorting: true,
|
||||
},
|
||||
{
|
||||
accessorKey: "latency",
|
||||
id: "latency",
|
||||
header: "Latency",
|
||||
cell: ({ row }) => {
|
||||
const value: number | undefined = row.getValue("latency");
|
||||
@@ -216,6 +212,7 @@ export default function GenerationsTable({ projectId }: GenerationsTableProps) {
|
||||
) : undefined;
|
||||
},
|
||||
enableHiding: true,
|
||||
enableSorting: true,
|
||||
},
|
||||
{
|
||||
accessorKey: "cost",
|
||||
@@ -231,6 +228,7 @@ export default function GenerationsTable({ projectId }: GenerationsTableProps) {
|
||||
},
|
||||
{
|
||||
accessorKey: "level",
|
||||
id: "level",
|
||||
header: "Level",
|
||||
enableHiding: true,
|
||||
cell({ row }) {
|
||||
@@ -247,6 +245,7 @@ export default function GenerationsTable({ projectId }: GenerationsTableProps) {
|
||||
</span>
|
||||
) : undefined;
|
||||
},
|
||||
enableSorting: true,
|
||||
},
|
||||
{
|
||||
accessorKey: "statusMessage",
|
||||
@@ -256,8 +255,10 @@ export default function GenerationsTable({ projectId }: GenerationsTableProps) {
|
||||
},
|
||||
{
|
||||
accessorKey: "model",
|
||||
id: "model",
|
||||
header: "Model",
|
||||
enableHiding: true,
|
||||
enableSorting: true,
|
||||
},
|
||||
{
|
||||
accessorKey: "usage",
|
||||
@@ -311,8 +312,10 @@ export default function GenerationsTable({ projectId }: GenerationsTableProps) {
|
||||
},
|
||||
{
|
||||
accessorKey: "version",
|
||||
id: "version",
|
||||
header: "Version",
|
||||
enableHiding: true,
|
||||
enableSorting: true,
|
||||
},
|
||||
];
|
||||
const [columnVisibility, setColumnVisibility] =
|
||||
@@ -322,7 +325,7 @@ export default function GenerationsTable({ projectId }: GenerationsTableProps) {
|
||||
);
|
||||
|
||||
const rows: GenerationsTableRow[] = generations.isSuccess
|
||||
? generations.data.map((generation) => {
|
||||
? generations.data.generations.map((generation) => {
|
||||
return {
|
||||
id: generation.id,
|
||||
traceId: generation.traceId,
|
||||
@@ -420,6 +423,8 @@ export default function GenerationsTable({ projectId }: GenerationsTableProps) {
|
||||
onChange: setPaginationState,
|
||||
state: paginationState,
|
||||
}}
|
||||
setOrderBy={setOrderByState}
|
||||
orderBy={orderByState}
|
||||
columnVisibility={columnVisibility}
|
||||
onColumnVisibilityChange={setColumnVisibility}
|
||||
/>
|
||||
|
||||
@@ -7,6 +7,7 @@ import useColumnVisibility from "@/src/features/column-visibility/hooks/useColum
|
||||
import { useQueryFilterState } from "@/src/features/filters/hooks/useFilterState";
|
||||
import { type FilterState } from "@/src/features/filters/types";
|
||||
import { useDetailPageLists } from "@/src/features/navigate-detail-pages/context";
|
||||
import { useOrderByState } from "@/src/features/orderBy/hooks/useOrderByState";
|
||||
import { sessionsViewCols } from "@/src/server/api/definitions/sessionsView";
|
||||
import { api } from "@/src/utils/api";
|
||||
import { formatInterval, utcDateOffsetByDays } from "@/src/utils/dates";
|
||||
@@ -63,11 +64,17 @@ export default function SessionsTable({
|
||||
pageSize: withDefault(NumberParam, 50),
|
||||
});
|
||||
|
||||
const [orderByState, setOrderByState] = useOrderByState({
|
||||
column: "createdAt",
|
||||
order: "DESC",
|
||||
});
|
||||
|
||||
const sessions = api.sessions.all.useQuery({
|
||||
page: paginationState.pageIndex,
|
||||
limit: paginationState.pageSize,
|
||||
projectId,
|
||||
filter: filterState,
|
||||
orderBy: orderByState,
|
||||
});
|
||||
|
||||
const totalCount = sessions.data?.slice(1)[0]?.totalCount ?? 0;
|
||||
@@ -97,6 +104,7 @@ export default function SessionsTable({
|
||||
const columns: LangfuseColumnDef<SessionTableRow>[] = [
|
||||
{
|
||||
accessorKey: "bookmarked",
|
||||
id: "bookmarked",
|
||||
header: undefined,
|
||||
cell: ({ row }) => {
|
||||
const bookmarked = row.getValue("bookmarked");
|
||||
@@ -112,9 +120,11 @@ export default function SessionsTable({
|
||||
/>
|
||||
) : undefined;
|
||||
},
|
||||
enableSorting: true,
|
||||
},
|
||||
{
|
||||
accessorKey: "id",
|
||||
id: "id",
|
||||
header: "ID",
|
||||
cell: ({ row }) => {
|
||||
const value = row.getValue("id");
|
||||
@@ -125,14 +135,18 @@ export default function SessionsTable({
|
||||
/>
|
||||
) : undefined;
|
||||
},
|
||||
enableSorting: true,
|
||||
},
|
||||
{
|
||||
accessorKey: "createdAt",
|
||||
id: "createdAt",
|
||||
header: "Created At",
|
||||
enableHiding: true,
|
||||
enableSorting: true,
|
||||
},
|
||||
{
|
||||
accessorKey: "sessionDuration",
|
||||
id: "sessionDuration",
|
||||
header: "Duration",
|
||||
enableHiding: true,
|
||||
cell: ({ row }) => {
|
||||
@@ -141,6 +155,7 @@ export default function SessionsTable({
|
||||
? formatInterval(value)
|
||||
: undefined;
|
||||
},
|
||||
enableSorting: true,
|
||||
},
|
||||
{
|
||||
accessorKey: "userIds",
|
||||
@@ -165,8 +180,10 @@ export default function SessionsTable({
|
||||
},
|
||||
{
|
||||
accessorKey: "countTraces",
|
||||
id: "countTraces",
|
||||
header: "Traces",
|
||||
enableHiding: true,
|
||||
enableSorting: true,
|
||||
},
|
||||
];
|
||||
|
||||
@@ -205,6 +222,8 @@ export default function SessionsTable({
|
||||
onChange: setPaginationState,
|
||||
state: paginationState,
|
||||
}}
|
||||
setOrderBy={setOrderByState}
|
||||
orderBy={orderByState}
|
||||
columnVisibility={columnVisibility}
|
||||
onColumnVisibilityChange={setColumnVisibility}
|
||||
help={{
|
||||
|
||||
@@ -1 +1 @@
|
||||
export const VERSION = "v1.33.3";
|
||||
export const VERSION = "v1.34.0";
|
||||
|
||||
@@ -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,9 +101,7 @@ function openAiChatTokenCount(params: TokenCalculationParams) {
|
||||
});
|
||||
});
|
||||
num_tokens += 3; // every reply is primed with <| start |> assistant <| message |>
|
||||
// https://github.com/dqbd/tiktoken/issues/72
|
||||
// we need to ensure to deallocate memory from the encoder
|
||||
encoding.free();
|
||||
|
||||
return num_tokens;
|
||||
}
|
||||
|
||||
@@ -141,6 +132,23 @@ const getTokens = (name: TiktokenEncoding, text: string) => {
|
||||
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";
|
||||
}
|
||||
|
||||
@@ -5,7 +5,6 @@ import {
|
||||
} from "@/src/features/public-api/lib/apiKeys";
|
||||
import { type ApiAccessScope } from "@/src/features/public-api/server/types";
|
||||
import { prisma } from "@/src/server/db";
|
||||
import { instrumentAsync } from "@/src/utils/instrumentation";
|
||||
|
||||
export type AuthHeaderVerificationResult =
|
||||
| {
|
||||
@@ -20,93 +19,85 @@ export type AuthHeaderVerificationResult =
|
||||
export async function verifyAuthHeaderAndReturnScope(
|
||||
authHeader: string | undefined,
|
||||
): Promise<AuthHeaderVerificationResult> {
|
||||
return instrumentAsync(
|
||||
{ name: "verifyAuthHeaderAndReturnScope" },
|
||||
async () => {
|
||||
if (!authHeader) {
|
||||
console.error("No authorization header");
|
||||
return {
|
||||
validKey: false,
|
||||
error: "No authorization header",
|
||||
};
|
||||
if (!authHeader) {
|
||||
console.error("No authorization header");
|
||||
return {
|
||||
validKey: false,
|
||||
error: "No authorization header",
|
||||
};
|
||||
}
|
||||
|
||||
try {
|
||||
// Basic auth, full scope, needs secret key and public key
|
||||
if (authHeader.startsWith("Basic ")) {
|
||||
const { username: publicKey, password: secretKey } =
|
||||
extractBasicAuthCredentials(authHeader);
|
||||
|
||||
const salt = env.SALT;
|
||||
const hashFromProvidedKey = createShaHash(secretKey, salt);
|
||||
const apiKey = await prisma.apiKey.findUnique({
|
||||
where: { fastHashedSecretKey: hashFromProvidedKey },
|
||||
});
|
||||
let projectId = apiKey?.projectId;
|
||||
|
||||
if (!apiKey || !apiKey.fastHashedSecretKey) {
|
||||
const dbKey = await findDbKeyOrThrow(publicKey);
|
||||
const isValid = await verifySecretKey(secretKey, dbKey.hashedSecretKey);
|
||||
|
||||
if (!isValid) {
|
||||
console.log("Old key is invalid", publicKey);
|
||||
throw new Error("Invalid credentials");
|
||||
}
|
||||
|
||||
const shaKey = createShaHash(secretKey, salt);
|
||||
|
||||
await prisma.apiKey.update({
|
||||
where: { publicKey },
|
||||
data: {
|
||||
fastHashedSecretKey: shaKey,
|
||||
},
|
||||
});
|
||||
projectId = dbKey.projectId;
|
||||
}
|
||||
|
||||
try {
|
||||
// Basic auth, full scope, needs secret key and public key
|
||||
if (authHeader.startsWith("Basic ")) {
|
||||
const { username: publicKey, password: secretKey } =
|
||||
extractBasicAuthCredentials(authHeader);
|
||||
|
||||
const salt = env.SALT;
|
||||
const hashFromProvidedKey = createShaHash(secretKey, salt);
|
||||
const apiKey = await prisma.apiKey.findUnique({
|
||||
where: { fastHashedSecretKey: hashFromProvidedKey },
|
||||
});
|
||||
let projectId = apiKey?.projectId;
|
||||
|
||||
if (!apiKey || !apiKey.fastHashedSecretKey) {
|
||||
const dbKey = await findDbKeyOrThrow(publicKey);
|
||||
const isValid = await verifySecretKey(
|
||||
secretKey,
|
||||
dbKey.hashedSecretKey,
|
||||
);
|
||||
|
||||
if (!isValid) {
|
||||
console.log("Old key is invalid", publicKey);
|
||||
throw new Error("Invalid credentials");
|
||||
}
|
||||
|
||||
const shaKey = createShaHash(secretKey, salt);
|
||||
|
||||
await prisma.apiKey.update({
|
||||
where: { publicKey },
|
||||
data: {
|
||||
fastHashedSecretKey: shaKey,
|
||||
},
|
||||
});
|
||||
projectId = dbKey.projectId;
|
||||
}
|
||||
|
||||
if (!projectId) {
|
||||
console.log("No project id found for key", publicKey);
|
||||
throw new Error("Invalid credentials");
|
||||
}
|
||||
|
||||
return {
|
||||
validKey: true,
|
||||
scope: {
|
||||
projectId: projectId,
|
||||
accessLevel: "all",
|
||||
},
|
||||
};
|
||||
}
|
||||
// Bearer auth, limited scope, only needs public key
|
||||
if (authHeader.startsWith("Bearer ")) {
|
||||
const publicKey = authHeader.replace("Bearer ", "");
|
||||
|
||||
const dbKey = await findDbKeyOrThrow(publicKey);
|
||||
|
||||
return {
|
||||
validKey: true,
|
||||
scope: {
|
||||
projectId: dbKey.projectId,
|
||||
accessLevel: "scores",
|
||||
},
|
||||
};
|
||||
}
|
||||
} catch (error: unknown) {
|
||||
console.error("Error verifying auth header: ", error);
|
||||
return {
|
||||
validKey: false,
|
||||
error: error instanceof Error ? error.message : "Authorization error",
|
||||
};
|
||||
if (!projectId) {
|
||||
console.log("No project id found for key", publicKey);
|
||||
throw new Error("Invalid credentials");
|
||||
}
|
||||
|
||||
return {
|
||||
validKey: false,
|
||||
error: "Invalid authorization header",
|
||||
validKey: true,
|
||||
scope: {
|
||||
projectId: projectId,
|
||||
accessLevel: "all",
|
||||
},
|
||||
};
|
||||
},
|
||||
);
|
||||
}
|
||||
// Bearer auth, limited scope, only needs public key
|
||||
if (authHeader.startsWith("Bearer ")) {
|
||||
const publicKey = authHeader.replace("Bearer ", "");
|
||||
|
||||
const dbKey = await findDbKeyOrThrow(publicKey);
|
||||
|
||||
return {
|
||||
validKey: true,
|
||||
scope: {
|
||||
projectId: dbKey.projectId,
|
||||
accessLevel: "scores",
|
||||
},
|
||||
};
|
||||
}
|
||||
} catch (error: unknown) {
|
||||
console.error("Error verifying auth header: ", error);
|
||||
return {
|
||||
validKey: false,
|
||||
error: error instanceof Error ? error.message : "Authorization error",
|
||||
};
|
||||
}
|
||||
return {
|
||||
validKey: false,
|
||||
error: "Invalid authorization header",
|
||||
};
|
||||
}
|
||||
|
||||
function extractBasicAuthCredentials(basicAuthHeader: string): {
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import React from "react";
|
||||
import { cn } from "@/src/utils/tailwind";
|
||||
import { CommandItem } from "cmdk";
|
||||
|
||||
type TagItemCreateProps = {
|
||||
@@ -25,10 +24,9 @@ const TagItemCreate = ({
|
||||
<CommandItem
|
||||
key={inputValue}
|
||||
value={inputValue}
|
||||
className="flex cursor-pointer items-center rounded-sm px-1 py-2 text-muted-foreground hover:bg-secondary/80"
|
||||
className="flex cursor-pointer items-center rounded-sm px-3 py-2 text-sm text-muted-foreground hover:bg-secondary/80"
|
||||
onSelect={onSelect}
|
||||
>
|
||||
<div className={cn("mr-2 h-4 w-4")} />
|
||||
Create new tag: "{inputValue}"
|
||||
</CommandItem>
|
||||
);
|
||||
|
||||
@@ -16,31 +16,34 @@ export const TagInput = React.forwardRef<
|
||||
TagInputProps
|
||||
>(({ className, selectedTags, setSelectedTags, ...props }, ref) => (
|
||||
<div
|
||||
className="flex flex-wrap items-center overflow-auto rounded-lg border px-2 pt-2"
|
||||
className="flex flex-wrap items-center overflow-auto rounded-lg border px-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>
|
||||
{selectedTags.length > 0 && (
|
||||
<div className="flex flex-wrap items-center gap-x-2 gap-y-1 pt-2">
|
||||
{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,
|
||||
)}
|
||||
autoFocus
|
||||
{...props}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -64,7 +64,7 @@ const TagManager = ({
|
||||
className={availableTags.length > 0 ? "mt-2" : undefined}
|
||||
>
|
||||
<CommandGroup>
|
||||
{availableTags.map((value: string) => (
|
||||
{availableTags.slice(0, 5).map((value: string) => (
|
||||
<TagCommandItem
|
||||
key={value}
|
||||
value={value}
|
||||
|
||||
@@ -25,6 +25,7 @@ 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: {
|
||||
@@ -306,6 +307,9 @@ export const handleBatchResult = (
|
||||
error: error.error.message,
|
||||
});
|
||||
} else {
|
||||
if (process.env.NEXT_PUBLIC_SENTRY_DSN) {
|
||||
Sentry.captureException(error.error);
|
||||
}
|
||||
returnedErrors.push({
|
||||
id: error.id,
|
||||
status: 500,
|
||||
|
||||
@@ -15,12 +15,24 @@ import {
|
||||
} from "@/src/features/public-api/server/ingestion-api-schema";
|
||||
import { v4 } from "uuid";
|
||||
import { telemetry } from "@/src/features/telemetry";
|
||||
import { orderByToPrismaSql } from "@/src/features/orderBy/server/orderByToPrisma";
|
||||
import { tracesTableCols } from "@/src/server/api/definitions/tracesTable";
|
||||
import { orderBy } from "@/src/server/api/interfaces/orderBy";
|
||||
|
||||
const GetTracesSchema = z.object({
|
||||
...paginationZod,
|
||||
userId: z.string().nullish(),
|
||||
name: z.string().nullish(),
|
||||
tags: z.union([z.array(z.string()), z.string()]).nullish(),
|
||||
orderBy: z
|
||||
.string() // orderBy=timestamp.asc
|
||||
.nullish()
|
||||
.transform((v) => {
|
||||
if (!v) return null;
|
||||
const [column, order] = v.split(".");
|
||||
return { column, order: order?.toUpperCase() };
|
||||
})
|
||||
.pipe(orderBy.nullish()),
|
||||
});
|
||||
|
||||
export default async function handler(
|
||||
@@ -92,6 +104,11 @@ export default async function handler(
|
||||
)}] <@ t."tags"`
|
||||
: Prisma.empty;
|
||||
|
||||
const orderByCondition = orderByToPrismaSql(
|
||||
obj.orderBy ?? null,
|
||||
tracesTableCols,
|
||||
);
|
||||
|
||||
const traces = await prisma.$queryRaw<
|
||||
Array<Trace & { observations: string[]; scores: string[] }>
|
||||
>(Prisma.sql`
|
||||
@@ -116,7 +133,7 @@ export default async function handler(
|
||||
${nameCondition}
|
||||
${tagsCondition}
|
||||
GROUP BY t.id
|
||||
ORDER BY t."timestamp" DESC
|
||||
${orderByCondition}
|
||||
LIMIT ${obj.limit} OFFSET ${skipValue}
|
||||
`);
|
||||
const totalItems = await prisma.trace.count({
|
||||
|
||||
@@ -8,62 +8,73 @@ import { ObservationLevel } from "@prisma/client";
|
||||
export const observationsTableCols: ColumnDefinition[] = [
|
||||
{
|
||||
name: "id",
|
||||
id: "id",
|
||||
type: "string",
|
||||
internal: 'o."id"',
|
||||
},
|
||||
{
|
||||
name: "name",
|
||||
id: "name",
|
||||
type: "stringOptions",
|
||||
internal: 'o."name"',
|
||||
options: [], // to be added at runtime
|
||||
},
|
||||
{ name: "traceId", type: "string", internal: 't."id"' },
|
||||
{ name: "traceId", id: "traceId", type: "string", internal: 't."id"' },
|
||||
{
|
||||
name: "traceName",
|
||||
id: "traceName",
|
||||
type: "stringOptions",
|
||||
internal: 't."name"',
|
||||
options: [], // to be added at runtime
|
||||
},
|
||||
{ name: "userId", type: "string", internal: 't."user_id"' },
|
||||
{ name: "userId", id: "userId", type: "string", internal: 't."user_id"' },
|
||||
{
|
||||
name: "start_time",
|
||||
id: "startTime",
|
||||
type: "datetime",
|
||||
internal: 'o."start_time"',
|
||||
},
|
||||
{
|
||||
name: "end_time",
|
||||
id: "endTime",
|
||||
type: "datetime",
|
||||
internal: 'o."end_time"',
|
||||
},
|
||||
{
|
||||
name: "latency (s)",
|
||||
id: "latency",
|
||||
type: "number",
|
||||
internal: '"latency"',
|
||||
},
|
||||
{
|
||||
name: "level",
|
||||
id: "level",
|
||||
type: "stringOptions",
|
||||
internal: 'o."level"::text',
|
||||
options: Object.values(ObservationLevel).map((value) => ({ value })),
|
||||
},
|
||||
{
|
||||
name: "Status Message",
|
||||
id: "statusMessage",
|
||||
type: "string",
|
||||
internal: 'o."status_message"',
|
||||
},
|
||||
{
|
||||
name: "model",
|
||||
id: "model",
|
||||
type: "stringOptions",
|
||||
internal: 'o."model"',
|
||||
options: [], // to be added at runtime
|
||||
},
|
||||
{
|
||||
name: "metadata",
|
||||
id: "metadata",
|
||||
type: "stringObject",
|
||||
internal: 't."metadata"',
|
||||
internal: 'o."metadata"',
|
||||
},
|
||||
{
|
||||
name: "version",
|
||||
id: "version",
|
||||
type: "string",
|
||||
internal: 'o."version"',
|
||||
},
|
||||
|
||||
@@ -1,7 +1,13 @@
|
||||
import { type ColumnDefinition } from "@/src/server/api/interfaces/tableDefinition";
|
||||
|
||||
export const sessionsViewCols: ColumnDefinition[] = [
|
||||
{ name: "⭐️", type: "boolean", internal: "s.bookmarked" },
|
||||
{ name: "⭐️", id: "bookmarked", type: "boolean", internal: "s.bookmarked" },
|
||||
{
|
||||
name: "id",
|
||||
id: "id",
|
||||
type: "string",
|
||||
internal: 's."id"',
|
||||
},
|
||||
{
|
||||
name: "userId",
|
||||
type: "string",
|
||||
@@ -9,12 +15,20 @@ export const sessionsViewCols: ColumnDefinition[] = [
|
||||
},
|
||||
{
|
||||
name: "Session duration (s)",
|
||||
id: "sessionDuration",
|
||||
type: "number",
|
||||
internal: 'o."sessionDuration"',
|
||||
},
|
||||
{
|
||||
name: "createdAt",
|
||||
id: "createdAt",
|
||||
type: "datetime",
|
||||
internal: 's."created_at"',
|
||||
},
|
||||
{
|
||||
name: "countTraces",
|
||||
id: "countTraces",
|
||||
type: "number",
|
||||
internal: 't."countTraces"',
|
||||
},
|
||||
];
|
||||
|
||||
@@ -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,
|
||||
@@ -30,6 +30,8 @@ import {
|
||||
exportFileFormats,
|
||||
exportOptions,
|
||||
} from "@/src/server/api/interfaces/exportTypes";
|
||||
import { orderBy } from "@/src/server/api/interfaces/orderBy";
|
||||
import { orderByToPrismaSql } from "@/src/features/orderBy/server/orderByToPrisma";
|
||||
|
||||
const GenerationFilterOptions = z.object({
|
||||
projectId: z.string(), // Required for protectedProjectProcedure
|
||||
@@ -38,6 +40,7 @@ const GenerationFilterOptions = z.object({
|
||||
});
|
||||
|
||||
const ListInputs = GenerationFilterOptions.extend({
|
||||
orderBy: orderBy,
|
||||
...paginationZod,
|
||||
});
|
||||
|
||||
@@ -64,6 +67,11 @@ export const generationsRouter = createTRPCRouter({
|
||||
observationsTableCols,
|
||||
);
|
||||
|
||||
const orderByCondition = orderByToPrismaSql(
|
||||
input.orderBy,
|
||||
observationsTableCols,
|
||||
);
|
||||
|
||||
// to improve query performance, add timeseries filter to observation queries as well
|
||||
const startTimeFilter = input.filter.find(
|
||||
(f) => f.column === "start_time" && f.type === "datetime",
|
||||
@@ -122,7 +130,7 @@ export const generationsRouter = createTRPCRouter({
|
||||
t.project_id = ${input.projectId}
|
||||
${searchCondition}
|
||||
${filterCondition}
|
||||
ORDER BY o.start_time DESC
|
||||
${orderByCondition}
|
||||
LIMIT ${input.limit}
|
||||
OFFSET ${input.page * input.limit}
|
||||
`,
|
||||
@@ -303,9 +311,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),
|
||||
@@ -319,7 +329,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),
|
||||
},
|
||||
]
|
||||
: [],
|
||||
@@ -329,6 +346,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");
|
||||
|
||||
@@ -12,10 +12,13 @@ import { singleFilter } from "@/src/server/api/interfaces/filters";
|
||||
import { paginationZod } from "@/src/utils/zod";
|
||||
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";
|
||||
|
||||
const SessionFilterOptions = z.object({
|
||||
projectId: z.string(), // Required for protectedProjectProcedure
|
||||
filter: z.array(singleFilter).nullable(),
|
||||
orderBy: orderBy,
|
||||
...paginationZod,
|
||||
});
|
||||
|
||||
@@ -28,6 +31,10 @@ export const sessionRouter = createTRPCRouter({
|
||||
input.filter ?? [],
|
||||
sessionsViewCols,
|
||||
);
|
||||
const orderByCondition = orderByToPrismaSql(
|
||||
input.orderBy,
|
||||
sessionsViewCols,
|
||||
);
|
||||
|
||||
const sessions = await ctx.prisma.$queryRaw<
|
||||
Array<{
|
||||
@@ -79,7 +86,7 @@ export const sessionRouter = createTRPCRouter({
|
||||
WHERE
|
||||
s."project_id" = ${input.projectId}
|
||||
${filterCondition}
|
||||
ORDER BY 2 desc
|
||||
${orderByCondition}
|
||||
LIMIT ${input.limit}
|
||||
OFFSET ${input.page * input.limit}
|
||||
`);
|
||||
|
||||
@@ -23,7 +23,6 @@ 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
|
||||
@@ -91,21 +90,17 @@ export const traceRouter = createTRPCRouter({
|
||||
orderByCondition,
|
||||
);
|
||||
|
||||
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 traces = await ctx.prisma.$queryRaw<
|
||||
Array<
|
||||
Trace & {
|
||||
promptTokens: number;
|
||||
completionTokens: number;
|
||||
totalTokens: number;
|
||||
totalCount: number;
|
||||
latency: number | null;
|
||||
}
|
||||
>
|
||||
>(tracesQuery);
|
||||
|
||||
const countQyery = createTracesQuery(
|
||||
Prisma.sql`count(*)`,
|
||||
@@ -118,11 +113,8 @@ export const traceRouter = createTRPCRouter({
|
||||
Prisma.empty,
|
||||
);
|
||||
|
||||
const totalTraces = await instrumentAsync(
|
||||
{ name: "get-total-traces" },
|
||||
async () =>
|
||||
await ctx.prisma.$queryRaw<Array<{ count: bigint }>>(countQyery),
|
||||
);
|
||||
const totalTraces =
|
||||
await ctx.prisma.$queryRaw<Array<{ count: bigint }>>(countQyery);
|
||||
|
||||
// get scores for each trace individually to increase
|
||||
// performance of the query above
|
||||
|
||||
@@ -57,10 +57,13 @@ export class ObservationProcessor implements EventProcessor {
|
||||
this.event = event;
|
||||
}
|
||||
|
||||
async convertToObservation(apiScope: ApiAccessScope): Promise<{
|
||||
async convertToObservation(
|
||||
apiScope: ApiAccessScope,
|
||||
existingObservation: Observation | null,
|
||||
): Promise<{
|
||||
id: string;
|
||||
create: Prisma.ObservationCreateInput;
|
||||
update: Prisma.ObservationUpdateInput;
|
||||
create: Prisma.ObservationUncheckedCreateInput;
|
||||
update: Prisma.ObservationUncheckedUpdateInput;
|
||||
}> {
|
||||
const { body } = this.event;
|
||||
|
||||
@@ -85,12 +88,6 @@ export class ObservationProcessor implements EventProcessor {
|
||||
|
||||
const { id, traceId, name, startTime, metadata } = body;
|
||||
|
||||
const existingObservation = id
|
||||
? await prisma.observation.findUnique({
|
||||
where: { id, projectId: apiScope.projectId },
|
||||
})
|
||||
: null;
|
||||
|
||||
if (
|
||||
this.event.type === eventTypes.OBSERVATION_UPDATE &&
|
||||
!existingObservation
|
||||
@@ -124,20 +121,26 @@ export class ObservationProcessor implements EventProcessor {
|
||||
metadata ?? undefined,
|
||||
);
|
||||
|
||||
const prompts =
|
||||
const prompt =
|
||||
"promptName" in this.event.body &&
|
||||
typeof this.event.body.promptName === "string" &&
|
||||
"promptVersion" in this.event.body &&
|
||||
typeof this.event.body.promptVersion === "number"
|
||||
? await prisma.prompt.findMany({
|
||||
? await prisma.prompt.findUnique({
|
||||
where: {
|
||||
projectId: apiScope.projectId,
|
||||
name: this.event.body.promptName,
|
||||
version: this.event.body.promptVersion,
|
||||
projectId_name_version: {
|
||||
projectId: apiScope.projectId,
|
||||
name: this.event.body.promptName,
|
||||
version: this.event.body.promptVersion,
|
||||
},
|
||||
},
|
||||
})
|
||||
: undefined;
|
||||
|
||||
// Only null if promptName and promptVersion are set but prompt is not found
|
||||
if (prompt === null)
|
||||
console.warn("Prompt not found for observation", this.event.body);
|
||||
|
||||
const observationId = id ?? v4();
|
||||
return {
|
||||
id: observationId,
|
||||
@@ -174,10 +177,8 @@ export class ObservationProcessor implements EventProcessor {
|
||||
statusMessage: body.statusMessage ?? undefined,
|
||||
parentObservationId: body.parentObservationId ?? undefined,
|
||||
version: body.version ?? undefined,
|
||||
project: { connect: { id: apiScope.projectId } },
|
||||
...(prompts && prompts.length === 1
|
||||
? { prompt: { connect: { id: prompts[0]?.id } } }
|
||||
: undefined),
|
||||
projectId: apiScope.projectId,
|
||||
promptId: prompt ? prompt.id : undefined,
|
||||
},
|
||||
update: {
|
||||
name,
|
||||
@@ -209,9 +210,7 @@ export class ObservationProcessor implements EventProcessor {
|
||||
statusMessage: body.statusMessage ?? undefined,
|
||||
parentObservationId: body.parentObservationId ?? undefined,
|
||||
version: body.version ?? undefined,
|
||||
...(prompts && prompts.length === 1
|
||||
? { prompt: { connect: { id: prompts[0]?.id } } }
|
||||
: undefined),
|
||||
promptId: prompt ? prompt.id : undefined,
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -250,14 +249,28 @@ export class ObservationProcessor implements EventProcessor {
|
||||
if (apiScope.accessLevel !== "all")
|
||||
throw new AuthenticationError("Access denied for observation creation");
|
||||
|
||||
const obs = await this.convertToObservation(apiScope);
|
||||
const existingObservation = this.event.body.id
|
||||
? await prisma.observation.findFirst({
|
||||
where: { id: this.event.body.id },
|
||||
})
|
||||
: null;
|
||||
|
||||
if (
|
||||
existingObservation &&
|
||||
existingObservation.projectId !== apiScope.projectId
|
||||
) {
|
||||
throw new AuthenticationError(
|
||||
`Access denied for observation creation ${existingObservation.projectId} `,
|
||||
);
|
||||
}
|
||||
|
||||
const obs = await this.convertToObservation(apiScope, existingObservation);
|
||||
|
||||
// Do not use nested upserts or multiple where conditions as this should be a single native database upsert
|
||||
// https://www.prisma.io/docs/orm/reference/prisma-client-reference#database-upserts
|
||||
return await prisma.observation.upsert({
|
||||
where: {
|
||||
id_projectId: {
|
||||
id: obs.id,
|
||||
projectId: apiScope.projectId,
|
||||
},
|
||||
id: obs.id,
|
||||
},
|
||||
create: obs.create,
|
||||
update: obs.update,
|
||||
@@ -277,7 +290,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();
|
||||
|
||||
@@ -288,12 +303,18 @@ export class TraceProcessor implements EventProcessor {
|
||||
body,
|
||||
);
|
||||
|
||||
const existingTrace = await prisma.trace.findUnique({
|
||||
const existingTrace = await prisma.trace.findFirst({
|
||||
where: {
|
||||
id: internalId,
|
||||
},
|
||||
});
|
||||
|
||||
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)
|
||||
@@ -301,10 +322,27 @@ export class TraceProcessor implements EventProcessor {
|
||||
body.metadata ?? undefined,
|
||||
);
|
||||
|
||||
if (body.sessionId) {
|
||||
await prisma.traceSession.upsert({
|
||||
where: {
|
||||
id_projectId: {
|
||||
id: body.sessionId,
|
||||
projectId: apiScope.projectId,
|
||||
},
|
||||
},
|
||||
create: {
|
||||
id: body.sessionId,
|
||||
projectId: apiScope.projectId,
|
||||
},
|
||||
update: {},
|
||||
});
|
||||
}
|
||||
|
||||
// Do not use nested upserts or multiple where conditions as this should be a single native database upsert
|
||||
// https://www.prisma.io/docs/orm/reference/prisma-client-reference#database-upserts
|
||||
const upsertedTrace = await prisma.trace.upsert({
|
||||
where: {
|
||||
id: internalId,
|
||||
projectId: apiScope.projectId,
|
||||
},
|
||||
create: {
|
||||
id: internalId,
|
||||
@@ -315,16 +353,9 @@ export class TraceProcessor implements EventProcessor {
|
||||
metadata: mergedMetadata ?? body.metadata ?? undefined,
|
||||
release: body.release ?? undefined,
|
||||
version: body.version ?? undefined,
|
||||
session: body.sessionId
|
||||
? {
|
||||
connectOrCreate: {
|
||||
where: { id: body.sessionId, projectId: apiScope.projectId },
|
||||
create: { id: body.sessionId, projectId: apiScope.projectId },
|
||||
},
|
||||
}
|
||||
: undefined,
|
||||
sessionId: body.sessionId ?? undefined,
|
||||
public: body.public ?? undefined,
|
||||
project: { connect: { id: apiScope.projectId } },
|
||||
projectId: apiScope.projectId,
|
||||
tags: body.tags ?? undefined,
|
||||
},
|
||||
update: {
|
||||
@@ -335,14 +366,7 @@ export class TraceProcessor implements EventProcessor {
|
||||
metadata: mergedMetadata ?? body.metadata ?? undefined,
|
||||
release: body.release ?? undefined,
|
||||
version: body.version ?? undefined,
|
||||
session: body.sessionId
|
||||
? {
|
||||
connectOrCreate: {
|
||||
where: { id: body.sessionId, projectId: apiScope.projectId },
|
||||
create: { id: body.sessionId, projectId: apiScope.projectId },
|
||||
},
|
||||
}
|
||||
: undefined,
|
||||
sessionId: body.sessionId ?? undefined,
|
||||
public: body.public ?? undefined,
|
||||
tags: body.tags ?? undefined,
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user