Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
af789fb3ac | ||
|
|
7fcf9e98ae | ||
|
|
45c7a95a9c | ||
|
|
75009cf83d | ||
|
|
70642da8ab | ||
|
|
6754733aac | ||
|
|
e45bd75afa | ||
|
|
9b271f0b78 | ||
|
|
e98565ee9c | ||
|
|
62d1f31527 | ||
|
|
c1190d70da | ||
|
|
bf9badb711 | ||
|
|
8155749b2b | ||
|
|
84c0ba6487 | ||
|
|
6320f48737 | ||
|
|
fe053405ea | ||
|
|
63d1abffb2 | ||
|
|
3050ed756f | ||
|
|
6eebf0b49d | ||
|
|
bbc57779c3 | ||
|
|
ccc9161967 | ||
|
|
6df7a2c716 | ||
|
|
88e4f210d0 | ||
|
|
011c1452a0 | ||
|
|
4a3025dbae | ||
|
|
367e8c8a61 | ||
|
|
84edae6468 | ||
|
|
87e04a3482 | ||
|
|
90abf7fb34 | ||
|
|
027c3791a0 | ||
|
|
4023e7fd4a | ||
|
|
3d178caa17 | ||
|
|
128b03bec0 | ||
|
|
73889fcc96 | ||
|
|
d88a295f8d | ||
|
|
15c5fdc2a9 | ||
|
|
ca9c0e07d1 | ||
|
|
bf0eab424f | ||
|
|
89d38ff6ad | ||
|
|
8d7b334521 | ||
|
|
a374a378a0 |
+4
-1
@@ -29,7 +29,7 @@ SALT="salt"
|
||||
# LANGFUSE_DEFAULT_PROJECT_ROLE=
|
||||
|
||||
# Enable experimental features, optional
|
||||
# NEXT_PUBLIC_ENABLE_EXPERIMENTAL_FEATURES
|
||||
# LANGFUSE_ENABLE_EXPERIMENTAL_FEATURES=true
|
||||
|
||||
# Auth, optional configuration
|
||||
# AUTH_DOMAINS_WITH_SSO_ENFORCEMENT=domain1.com,domain2.com
|
||||
@@ -38,11 +38,14 @@ SALT="salt"
|
||||
# SSO, each group is optional
|
||||
# AUTH_GOOGLE_CLIENT_ID=
|
||||
# AUTH_GOOGLE_CLIENT_SECRET=
|
||||
# AUTH_GOOGLE_ALLOW_ACCOUNT_LINKING=false
|
||||
# AUTH_GITHUB_CLIENT_ID=
|
||||
# AUTH_GITHUB_CLIENT_SECRET=
|
||||
# AUTH_GITHUB_ALLOW_ACCOUNT_LINKING=false
|
||||
# AUTH_AZURE_AD_CLIENT_ID=
|
||||
# AUTH_AZURE_AD_CLIENT_SECRET=
|
||||
# AUTH_AZURE_AD_TENANT_ID=
|
||||
# AUTH_AZURE_ALLOW_ACCOUNT_LINKING=false
|
||||
|
||||
# Transactional email, optional
|
||||
# Defines the email address to use as the from address.
|
||||
|
||||
@@ -21,21 +21,12 @@
|
||||
</div>
|
||||
</br>
|
||||
<div>
|
||||
<a href="https://langfuse.com/docs">
|
||||
<strong>Docs</strong>
|
||||
</a> ·
|
||||
<a href="https://langfuse.com/issue">
|
||||
<strong>Report Bug</strong>
|
||||
</a> ·
|
||||
<a href="https://langfuse.com/idea">
|
||||
<strong>Feature Request</strong>
|
||||
</a> ·
|
||||
<a href="https://langfuse.com/changelog">
|
||||
<strong>Changelog</strong>
|
||||
</a> ·
|
||||
<a href="https://langfuse.com/discord">
|
||||
<strong>Discord</strong>
|
||||
</a>
|
||||
<a href="https://langfuse.com/docs"><strong>Docs</strong></a> ·
|
||||
<a href="https://langfuse.com/issues"><strong>Report Bug</strong></a> ·
|
||||
<a href="https://langfuse.com/ideas"><strong>Feature Request</strong></a> ·
|
||||
<a href="https://langfuse.com/changelog"><strong>Changelog</strong></a> ·
|
||||
<a href="https://langfuse.com/roadmap"><strong>Roadmap</strong></a> ·
|
||||
<a href="https://langfuse.com/discord"><strong>Discord</strong></a>
|
||||
</div>
|
||||
</br>
|
||||
<img src="https://img.shields.io/badge/License-MIT-red.svg?style=flat-square" alt="MIT License">
|
||||
@@ -142,6 +133,7 @@ External projects/packages that integrate with Langfuse:
|
||||
|
||||
- [GitHub Discussions](https://github.com/orgs/langfuse/discussions)
|
||||
- [Feature Requests](https://langfuse.com/idea)
|
||||
- [Roadmap](https://langfuse.com/roadmap)
|
||||
|
||||
### Support and feedback
|
||||
|
||||
@@ -180,6 +172,6 @@ This helps us to:
|
||||
1. Understand how Langfuse is used and improve the most relevant features.
|
||||
2. Track overall usage for internal and external (e.g. fundraising) reporting.
|
||||
|
||||
None of the data is shared with third parties and does not include any sensitive information. We want to be super transparent about this and you can find the exact data we collect [here](/src/features/telemetry/index.ts).
|
||||
None of the data is shared with third parties and does not include any sensitive information. We want to be super transparent about this and you can find the exact data we collect [here](/web/src/features/telemetry/index.ts).
|
||||
|
||||
You can opt-out by setting `TELEMETRY_ENABLED=false`.
|
||||
|
||||
@@ -118,6 +118,7 @@ types:
|
||||
traceId: string
|
||||
name: string
|
||||
value: double
|
||||
source: ScoreSource
|
||||
observationId: optional<string>
|
||||
timestamp: datetime
|
||||
comment: optional<string>
|
||||
@@ -183,6 +184,10 @@ types:
|
||||
enum:
|
||||
- ACTIVE
|
||||
- ARCHIVED
|
||||
ScoreSource:
|
||||
enum:
|
||||
- API
|
||||
- REVIEW
|
||||
|
||||
errors:
|
||||
Error:
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
"infra:dev:down": "docker-compose -f ./docker-compose.dev.yml down",
|
||||
"dev": "npm run dev --prefix worker/ & npm run dev --prefix web/",
|
||||
"postinstall": "cd worker && npm install && cd ../web && npm install",
|
||||
"nuke": "bash ./scripts/nuke.sh",
|
||||
"dx": "dotenv -e .env -- npm i && npm run infra:dev:up && cd web && npm run db:reset && npm run db:seed:examples && cd .. && npm run dev"
|
||||
},
|
||||
"author": "",
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
|
||||
# used to nuke the dev environment for engineers
|
||||
|
||||
find . -name 'node_modules' -type d -prune -print -exec rm -rf '{}' \;
|
||||
find . -name '.next' -type d -prune -print -exec rm -rf '{}' \;
|
||||
find . -iname "bin" -type d -prune -print -exec rm -rf '{}' \;
|
||||
find . -iname "dist" -type d -prune -print -exec rm -rf '{}' \;
|
||||
find . -iname "out" -type d -prune -print -exec rm -rf '{}' \;
|
||||
find . -iname ".turbo" -type d -prune -print -exec rm -rf '{}' \;
|
||||
@@ -1376,6 +1376,8 @@ components:
|
||||
value:
|
||||
type: number
|
||||
format: double
|
||||
source:
|
||||
$ref: '#/components/schemas/ScoreSource'
|
||||
observationId:
|
||||
type: string
|
||||
nullable: true
|
||||
@@ -1390,6 +1392,7 @@ components:
|
||||
- traceId
|
||||
- name
|
||||
- value
|
||||
- source
|
||||
- timestamp
|
||||
Dataset:
|
||||
title: Dataset
|
||||
@@ -1541,6 +1544,12 @@ components:
|
||||
enum:
|
||||
- ACTIVE
|
||||
- ARCHIVED
|
||||
ScoreSource:
|
||||
title: ScoreSource
|
||||
type: string
|
||||
enum:
|
||||
- API
|
||||
- REVIEW
|
||||
CreateDatasetItemRequest:
|
||||
title: CreateDatasetItemRequest
|
||||
type: object
|
||||
|
||||
Generated
+2
-2
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "langfuse-core",
|
||||
"version": "2.10.2",
|
||||
"version": "2.11.2",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "langfuse-core",
|
||||
"version": "2.10.2",
|
||||
"version": "2.11.2",
|
||||
"hasInstallScript": true,
|
||||
"dependencies": {
|
||||
"@anthropic-ai/tokenizer": "^0.0.4",
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "langfuse-core",
|
||||
"version": "2.10.2",
|
||||
"version": "2.11.2",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"prebuild": "cp generated/openapi-client/openapi.yml public/openapi-client.yml && cp generated/openapi-server/openapi.yml public/openapi-server.yml",
|
||||
|
||||
@@ -24,6 +24,11 @@ export const ObservationLevel = {
|
||||
ERROR: "ERROR"
|
||||
} as const;
|
||||
export type ObservationLevel = (typeof ObservationLevel)[keyof typeof ObservationLevel];
|
||||
export const ScoreSource = {
|
||||
API: "API",
|
||||
REVIEW: "REVIEW"
|
||||
} as const;
|
||||
export type ScoreSource = (typeof ScoreSource)[keyof typeof ScoreSource];
|
||||
export const PricingUnit = {
|
||||
PER_1000_TOKENS: "PER_1000_TOKENS",
|
||||
PER_1000_CHARS: "PER_1000_CHARS"
|
||||
@@ -259,6 +264,7 @@ export type Score = {
|
||||
timestamp: Generated<Timestamp>;
|
||||
name: string;
|
||||
value: number;
|
||||
source: ScoreSource;
|
||||
comment: string | null;
|
||||
trace_id: string;
|
||||
observation_id: string | null;
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
INSERT INTO models (
|
||||
id,
|
||||
project_id,
|
||||
model_name,
|
||||
match_pattern,
|
||||
start_date,
|
||||
input_price,
|
||||
output_price,
|
||||
total_price,
|
||||
unit,
|
||||
tokenizer_id,
|
||||
tokenizer_config
|
||||
)
|
||||
VALUES
|
||||
-- https://docs.anthropic.com/claude/docs/models-overview, https://docs.anthropic.com/claude/docs/quickstart-guide
|
||||
('cltgy0iuw000008le3vod1hhy', NULL, 'claude-3-opus-20240229', '(?i)^(claude-3-opus-20240229)$', NULL, 0.000015, 0.000075, NULL, 'TOKENS', 'claude', NULL),
|
||||
('cltgy0pp6000108le56se7bl3', NULL, 'claude-3-sonnet-20240229', '(?i)^(claude-3-sonnet-20240229)$', NULL, 0.000003, 0.000015, NULL, 'TOKENS', 'claude', NULL)
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
-- CreateEnum
|
||||
CREATE TYPE "ScoreSource" AS ENUM ('API', 'REVIEW');
|
||||
|
||||
-- AlterTable
|
||||
ALTER TABLE "scores" ADD COLUMN "source" "ScoreSource" NOT NULL DEFAULT 'API';
|
||||
@@ -0,0 +1,2 @@
|
||||
-- CreateIndex
|
||||
CREATE INDEX CONCURRENTLY "scores_source_idx" ON "scores"("source");
|
||||
@@ -0,0 +1,4 @@
|
||||
-- scores previously created in the Langfuse UI all had the name 'manual-score'
|
||||
UPDATE "scores"
|
||||
SET "source" = 'REVIEW'::"ScoreSource"
|
||||
WHERE "name" = 'manual-score';
|
||||
@@ -0,0 +1,2 @@
|
||||
-- AlterTable
|
||||
ALTER TABLE "scores" ALTER COLUMN "source" DROP DEFAULT;
|
||||
@@ -0,0 +1,17 @@
|
||||
INSERT INTO models (
|
||||
id,
|
||||
project_id,
|
||||
model_name,
|
||||
match_pattern,
|
||||
start_date,
|
||||
input_price,
|
||||
output_price,
|
||||
total_price,
|
||||
unit,
|
||||
tokenizer_id,
|
||||
tokenizer_config
|
||||
)
|
||||
VALUES
|
||||
-- https://docs.anthropic.com/claude/docs/models-overview, https://docs.anthropic.com/claude/docs/quickstart-guide
|
||||
('cltr0w45b000008k1407o9qv1', NULL, 'claude-3-haiku-20240307', '(?i)^(claude-3-haiku-20240307)$', NULL, 0.00000025, 0.00000125, NULL, 'TOKENS', 'claude', NULL)
|
||||
|
||||
@@ -296,6 +296,7 @@ model Score {
|
||||
timestamp DateTime @default(now())
|
||||
name String
|
||||
value Float
|
||||
source ScoreSource
|
||||
comment String?
|
||||
traceId String @map("trace_id")
|
||||
trace Trace @relation(fields: [traceId], references: [id], onDelete: Cascade)
|
||||
@@ -307,9 +308,15 @@ model Score {
|
||||
@@index([value])
|
||||
@@index([traceId], type: Hash)
|
||||
@@index([observationId], type: Hash)
|
||||
@@index([source])
|
||||
@@map("scores")
|
||||
}
|
||||
|
||||
enum ScoreSource {
|
||||
API
|
||||
REVIEW
|
||||
}
|
||||
|
||||
enum PricingUnit {
|
||||
PER_1000_TOKENS
|
||||
PER_1000_CHARS
|
||||
|
||||
+7
-2
@@ -3,6 +3,7 @@ import {
|
||||
type Project,
|
||||
type Prisma,
|
||||
ObservationType,
|
||||
ScoreSource,
|
||||
} from "@prisma/client";
|
||||
import {
|
||||
hashSecretKey,
|
||||
@@ -386,7 +387,7 @@ function createObjects(
|
||||
more: "1,2,3;4?6",
|
||||
},
|
||||
tags: tags as string[],
|
||||
userId: `user-${i % 10}`,
|
||||
userId: `user-${i % 60}`,
|
||||
input:
|
||||
Math.random() > 0.3 ? "I'm looking for a React component" : undefined,
|
||||
output:
|
||||
@@ -403,9 +404,10 @@ function createObjects(
|
||||
? [
|
||||
{
|
||||
traceId: trace.id,
|
||||
name: "feedback",
|
||||
name: "manual-score",
|
||||
value: Math.floor(Math.random() * 3) - 1,
|
||||
timestamp: traceTs,
|
||||
source: ScoreSource.REVIEW,
|
||||
},
|
||||
]
|
||||
: []),
|
||||
@@ -416,6 +418,7 @@ function createObjects(
|
||||
name: "sentiment",
|
||||
value: Math.floor(Math.random() * 10) - 5,
|
||||
timestamp: traceTs,
|
||||
source: ScoreSource.API,
|
||||
},
|
||||
]
|
||||
: []),
|
||||
@@ -596,6 +599,7 @@ function createObjects(
|
||||
value: Math.random() * 2 - 1,
|
||||
observationId: generation.id,
|
||||
traceId: trace.id,
|
||||
source: ScoreSource.API,
|
||||
});
|
||||
if (Math.random() > 0.6)
|
||||
scores.push({
|
||||
@@ -603,6 +607,7 @@ function createObjects(
|
||||
value: Math.random() * 2 - 1,
|
||||
observationId: generation.id,
|
||||
traceId: trace.id,
|
||||
source: ScoreSource.API,
|
||||
});
|
||||
|
||||
generationIds.push(generation.id);
|
||||
|
||||
@@ -95,7 +95,7 @@ describe("observations.export RPC", () => {
|
||||
orderBy: { column: "id", order: "ASC" },
|
||||
filter: [
|
||||
{
|
||||
column: "start_time",
|
||||
column: "Start Time",
|
||||
type: "datetime",
|
||||
operator: ">",
|
||||
value: new Date("1990-01-01"),
|
||||
@@ -123,7 +123,7 @@ describe("observations.export RPC", () => {
|
||||
orderBy: { column: "id", order: "ASC" },
|
||||
filter: [
|
||||
{
|
||||
column: "start_time",
|
||||
column: "Start Time",
|
||||
type: "datetime",
|
||||
operator: ">",
|
||||
value: new Date("1990-01-01"),
|
||||
@@ -150,7 +150,7 @@ describe("observations.export RPC", () => {
|
||||
orderBy: { column: "id", order: "ASC" },
|
||||
filter: [
|
||||
{
|
||||
column: "start_time",
|
||||
column: "Start Time",
|
||||
type: "datetime",
|
||||
operator: ">",
|
||||
value: new Date("1990-01-01"),
|
||||
@@ -179,7 +179,7 @@ describe("observations.export RPC", () => {
|
||||
orderBy: { column: "id", order: "ASC" },
|
||||
filter: [
|
||||
{
|
||||
column: "start_time",
|
||||
column: "Start Time",
|
||||
type: "datetime",
|
||||
operator: ">",
|
||||
value: new Date("1990-01-01"),
|
||||
|
||||
@@ -54,6 +54,7 @@ describe("/api/public/scores API Endpoint", () => {
|
||||
expect(dbScore?.value).toBe(100.5);
|
||||
expect(dbScore?.observationId).toBeNull();
|
||||
expect(dbScore?.comment).toBe("comment");
|
||||
expect(dbScore?.source).toBe("API");
|
||||
});
|
||||
|
||||
it("should create score for a trace with int", async () => {
|
||||
@@ -301,6 +302,7 @@ describe("/api/public/scores API Endpoint", () => {
|
||||
comment: string;
|
||||
traceId: string;
|
||||
observationId: string;
|
||||
source: string;
|
||||
}>("GET", `/api/public/scores/${scoreId}`);
|
||||
|
||||
expect(getScore.status).toBe(200);
|
||||
@@ -309,6 +311,7 @@ describe("/api/public/scores API Endpoint", () => {
|
||||
name: "score-name",
|
||||
value: 100.5,
|
||||
comment: "comment",
|
||||
source: "API",
|
||||
traceId,
|
||||
observationId: generationId,
|
||||
});
|
||||
|
||||
@@ -22,7 +22,6 @@ import {
|
||||
import { useEffect, useState } from "react";
|
||||
import {
|
||||
type DateTimeAggregationOption,
|
||||
findClosestInterval,
|
||||
dateTimeAggregationSettings,
|
||||
dateTimeAggregationOptions,
|
||||
} from "@/src/features/dashboard/lib/timeseries-aggregation";
|
||||
@@ -89,7 +88,6 @@ export function DatePicker({
|
||||
export type DatePickerWithRangeProps = {
|
||||
dateRange?: DashboardDateRange;
|
||||
className?: string;
|
||||
setAgg: (agg: DateTimeAggregationOption) => void;
|
||||
selectedOption: AvailableDateRangeSelections;
|
||||
setDateRangeAndOption: (
|
||||
option: AvailableDateRangeSelections,
|
||||
@@ -101,7 +99,6 @@ export function DatePickerWithRange({
|
||||
className,
|
||||
dateRange,
|
||||
selectedOption,
|
||||
setAgg,
|
||||
setDateRangeAndOption,
|
||||
}: DatePickerWithRangeProps) {
|
||||
const [internalDateRange, setInternalDateRange] = useState<
|
||||
@@ -112,12 +109,6 @@ export function DatePickerWithRange({
|
||||
setInternalDateRange(dateRange);
|
||||
}, [dateRange]);
|
||||
|
||||
const closestInterval = dateRange
|
||||
? findClosestInterval(dateRange)
|
||||
: undefined;
|
||||
|
||||
closestInterval ? setAgg(closestInterval) : null;
|
||||
|
||||
const onDropDownSelection = (value: string) => {
|
||||
if (isValidOption(value)) {
|
||||
const setting = dateTimeAggregationSettings[value];
|
||||
|
||||
@@ -34,7 +34,7 @@ import { type LangfuseColumnDef } from "@/src/components/table/types";
|
||||
import { type ObservationLevel } from "@prisma/client";
|
||||
import { cn } from "@/src/utils/tailwind";
|
||||
import { LevelColors } from "@/src/components/level-colors";
|
||||
import { usdFormatter } from "@/src/utils/numbers";
|
||||
import { randomIntFromInterval, usdFormatter } from "@/src/utils/numbers";
|
||||
import {
|
||||
exportOptions,
|
||||
type ExportFileFormats,
|
||||
@@ -42,6 +42,8 @@ import {
|
||||
import { useOrderByState } from "@/src/features/orderBy/hooks/useOrderByState";
|
||||
import type Decimal from "decimal.js";
|
||||
import { type ScoreSimplified } from "@/src/server/api/routers/generations/getAllQuery";
|
||||
import { Skeleton } from "@/src/components/ui/skeleton";
|
||||
import React from "react";
|
||||
|
||||
export type GenerationsTableRow = {
|
||||
id: string;
|
||||
@@ -91,7 +93,7 @@ export default function GenerationsTable({ projectId }: GenerationsTableProps) {
|
||||
|
||||
const [filterState, setFilterState] = useQueryFilterState([
|
||||
{
|
||||
column: "start_time",
|
||||
column: "Start Time",
|
||||
type: "datetime",
|
||||
operator: ">",
|
||||
value: utcDateOffsetByDays(-14),
|
||||
@@ -410,8 +412,11 @@ export default function GenerationsTable({ projectId }: GenerationsTableProps) {
|
||||
accessorKey: "input",
|
||||
header: "Input",
|
||||
cell: ({ row }) => {
|
||||
const value: unknown = row.getValue("input");
|
||||
return <JSONView json={value} className="w-[500px]" />;
|
||||
const observationId: string = row.getValue("id");
|
||||
const traceId: string = row.getValue("traceId");
|
||||
return (
|
||||
<IOCell observationId={observationId} traceId={traceId} io="input" />
|
||||
);
|
||||
},
|
||||
enableHiding: true,
|
||||
defaultHidden: true,
|
||||
@@ -420,8 +425,11 @@ export default function GenerationsTable({ projectId }: GenerationsTableProps) {
|
||||
accessorKey: "output",
|
||||
header: "Output",
|
||||
cell: ({ row }) => {
|
||||
const value: unknown = row.getValue("output");
|
||||
return <JSONView json={value} className="w-[500px] bg-green-50" />;
|
||||
const observationId: string = row.getValue("id");
|
||||
const traceId: string = row.getValue("traceId");
|
||||
return (
|
||||
<IOCell observationId={observationId} traceId={traceId} io="output" />
|
||||
);
|
||||
},
|
||||
enableHiding: true,
|
||||
defaultHidden: true,
|
||||
@@ -448,7 +456,7 @@ export default function GenerationsTable({ projectId }: GenerationsTableProps) {
|
||||
id: "prompt",
|
||||
header: "Prompt",
|
||||
enableHiding: true,
|
||||
enableSorting: true,
|
||||
enableSorting: false,
|
||||
cell: ({ row }) => {
|
||||
const promptName = row.original.promptName;
|
||||
const promptVersion = row.original.promptVersion;
|
||||
@@ -501,7 +509,7 @@ export default function GenerationsTable({ projectId }: GenerationsTableProps) {
|
||||
version: generation.version ?? "",
|
||||
model: generation.model ?? "",
|
||||
input: generation.input,
|
||||
scores: generation.scores ?? undefined,
|
||||
scores: generation.scores,
|
||||
output: generation.output,
|
||||
level: generation.level,
|
||||
metadata: generation.metadata
|
||||
@@ -600,3 +608,73 @@ export default function GenerationsTable({ projectId }: GenerationsTableProps) {
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const IOCell = ({
|
||||
traceId,
|
||||
observationId,
|
||||
io,
|
||||
}: {
|
||||
traceId: string;
|
||||
observationId: string;
|
||||
io: "input" | "output";
|
||||
}) => {
|
||||
const observation = api.observations.byId.useQuery(
|
||||
{
|
||||
observationId: observationId,
|
||||
traceId: traceId,
|
||||
},
|
||||
{
|
||||
enabled: typeof traceId === "string" && typeof observationId === "string",
|
||||
trpc: {
|
||||
context: {
|
||||
skipBatch: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
);
|
||||
return (
|
||||
<>
|
||||
{observation.isLoading || !observation.data ? (
|
||||
<JsonSkeleton className="h-[250px] w-[500px] px-3 py-1" />
|
||||
) : (
|
||||
<JSONView
|
||||
json={
|
||||
io === "output" ? observation.data.output : observation.data.input
|
||||
}
|
||||
className="h-[250px] w-[500px] overflow-y-auto"
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export const JsonSkeleton = ({
|
||||
className,
|
||||
numRows = 10,
|
||||
}: {
|
||||
numRows?: number;
|
||||
className?: string;
|
||||
}) => {
|
||||
const sizingOptions = [
|
||||
"h-5 w-full",
|
||||
"h-5 w-[400px]",
|
||||
"h-5 w-[450px]",
|
||||
"h-5 w-[475px]",
|
||||
];
|
||||
|
||||
const generateRandomSize = () =>
|
||||
sizingOptions[randomIntFromInterval(0, sizingOptions.length - 1)];
|
||||
|
||||
return (
|
||||
<div className={cn("w-[500px] rounded-md border", className)}>
|
||||
<div className="flex flex-col gap-1">
|
||||
{[...Array<number>(numRows)].map((_) => (
|
||||
<>
|
||||
<Skeleton className={generateRandomSize()} />
|
||||
</>
|
||||
))}
|
||||
<br />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -208,8 +208,8 @@ export default function SessionsTable({
|
||||
enableHiding: true,
|
||||
defaultHidden: true,
|
||||
cell: ({ row }) => {
|
||||
const value: Decimal | undefined = row.getValue("inputCost");
|
||||
return value !== undefined ? (
|
||||
const value: Decimal | null | undefined = row.getValue("inputCost");
|
||||
return value ? (
|
||||
<span>{usdFormatter(value.toNumber())}</span>
|
||||
) : undefined;
|
||||
},
|
||||
@@ -221,9 +221,9 @@ export default function SessionsTable({
|
||||
enableHiding: true,
|
||||
defaultHidden: true,
|
||||
cell: ({ row }) => {
|
||||
const value: Decimal | undefined = row.getValue("outputCost");
|
||||
const value: Decimal | null | undefined = row.getValue("outputCost");
|
||||
|
||||
return value !== undefined ? (
|
||||
return value ? (
|
||||
<span>{usdFormatter(value.toNumber())}</span>
|
||||
) : undefined;
|
||||
},
|
||||
@@ -235,9 +235,9 @@ export default function SessionsTable({
|
||||
enableHiding: true,
|
||||
enableSorting: true,
|
||||
cell: ({ row }) => {
|
||||
const value: Decimal | undefined = row.getValue("totalCost");
|
||||
const value: Decimal | null | undefined = row.getValue("totalCost");
|
||||
|
||||
return value !== undefined ? (
|
||||
return value ? (
|
||||
<span>{usdFormatter(value.toNumber())}</span>
|
||||
) : undefined;
|
||||
},
|
||||
@@ -251,7 +251,7 @@ export default function SessionsTable({
|
||||
cell: ({ row }) => {
|
||||
const value: number | undefined = row.getValue("inputTokens");
|
||||
|
||||
return value !== undefined ? <span>{Number(value)}</span> : undefined;
|
||||
return value ? <span>{Number(value)}</span> : undefined;
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -263,7 +263,7 @@ export default function SessionsTable({
|
||||
cell: ({ row }) => {
|
||||
const value = row.getValue("outputTokens");
|
||||
|
||||
return value !== undefined ? <span>{Number(value)}</span> : undefined;
|
||||
return value ? <span>{Number(value)}</span> : undefined;
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -274,7 +274,7 @@ export default function SessionsTable({
|
||||
defaultHidden: true,
|
||||
cell: ({ row }) => {
|
||||
const value = row.getValue("totalTokens");
|
||||
return value !== undefined ? <span>{Number(value)}</span> : undefined;
|
||||
return value ? <span>{Number(value)}</span> : undefined;
|
||||
},
|
||||
},
|
||||
{
|
||||
|
||||
@@ -18,7 +18,7 @@ import { tracesTableColsWithOptions } from "@/src/server/api/definitions/tracesT
|
||||
import { api } from "@/src/utils/api";
|
||||
import { formatIntervalSeconds, utcDateOffsetByDays } from "@/src/utils/dates";
|
||||
import { type RouterInput, type RouterOutput } from "@/src/utils/types";
|
||||
import { type Score } from "@prisma/client";
|
||||
import { type ObservationLevel, type Score } from "@prisma/client";
|
||||
import { type RowSelectionState } from "@tanstack/react-table";
|
||||
import { useEffect, useState } from "react";
|
||||
import {
|
||||
@@ -31,6 +31,8 @@ import {
|
||||
import type Decimal from "decimal.js";
|
||||
import { usdFormatter } from "@/src/utils/numbers";
|
||||
import { DeleteButton } from "@/src/components/deleteButton";
|
||||
import { LevelColors } from "@/src/components/level-colors";
|
||||
import { cn } from "@/src/utils/tailwind";
|
||||
|
||||
export type TracesTableRow = {
|
||||
bookmarked: boolean;
|
||||
@@ -39,6 +41,7 @@ export type TracesTableRow = {
|
||||
name: string;
|
||||
userId: string;
|
||||
metadata?: string;
|
||||
level: ObservationLevel;
|
||||
latency?: number;
|
||||
release?: string;
|
||||
version?: string;
|
||||
@@ -150,6 +153,7 @@ export default function TracesTable({
|
||||
id: trace.id,
|
||||
timestamp: trace.timestamp.toLocaleString(),
|
||||
name: trace.name ?? "",
|
||||
level: trace.level,
|
||||
metadata: JSON.stringify(trace.metadata),
|
||||
release: trace.release ?? undefined,
|
||||
version: trace.version ?? undefined,
|
||||
@@ -458,6 +462,26 @@ export default function TracesTable({
|
||||
},
|
||||
enableHiding: true,
|
||||
},
|
||||
{
|
||||
accessorKey: "level",
|
||||
header: "Level",
|
||||
cell: ({ row }) => {
|
||||
const value: ObservationLevel = row.getValue("level");
|
||||
return (
|
||||
<span
|
||||
className={cn(
|
||||
"rounded-sm p-0.5 text-xs",
|
||||
LevelColors[value].bg,
|
||||
LevelColors[value].text,
|
||||
)}
|
||||
>
|
||||
{value}
|
||||
</span>
|
||||
);
|
||||
},
|
||||
enableHiding: true,
|
||||
defaultHidden: true,
|
||||
},
|
||||
{
|
||||
accessorKey: "version",
|
||||
id: "version",
|
||||
|
||||
@@ -30,38 +30,34 @@ export const IOPreview: React.FC<{
|
||||
? outLegacyCompletionSchemaParsed.data
|
||||
: props.output ?? null;
|
||||
|
||||
// OpenAI messages
|
||||
let inOpenAiMessageArray = OpenAiMessageArraySchema.safeParse(input);
|
||||
if (!inOpenAiMessageArray.success) {
|
||||
// check if input is an array of length 1 including an array of OpenAiMessageSchema
|
||||
// ChatML format
|
||||
let inChatMlArray = ChatMlArraySchema.safeParse(input);
|
||||
if (!inChatMlArray.success) {
|
||||
// check if input is an array of length 1 including an array of ChatMlMessageSchema
|
||||
// this is the case for some integrations
|
||||
// e.g. [[OpenAiMessageSchema, ...]]
|
||||
const inputArray = z.array(OpenAiMessageArraySchema).safeParse(input);
|
||||
// e.g. [[ChatMlMessageSchema, ...]]
|
||||
const inputArray = z.array(ChatMlArraySchema).safeParse(input);
|
||||
if (inputArray.success && inputArray.data.length === 1) {
|
||||
inOpenAiMessageArray = OpenAiMessageArraySchema.safeParse(
|
||||
inputArray.data[0],
|
||||
);
|
||||
inChatMlArray = ChatMlArraySchema.safeParse(inputArray.data[0]);
|
||||
} else {
|
||||
// check if input is an object with a messages key
|
||||
// this is the case for some integrations
|
||||
// e.g. { messages: [OpenAiMessageSchema, ...] }
|
||||
// e.g. { messages: [ChatMlMessageSchema, ...] }
|
||||
const inputObject = z
|
||||
.object({
|
||||
messages: OpenAiMessageArraySchema,
|
||||
messages: ChatMlArraySchema,
|
||||
})
|
||||
.safeParse(input);
|
||||
|
||||
if (inputObject.success) {
|
||||
inOpenAiMessageArray = OpenAiMessageArraySchema.safeParse(
|
||||
inputObject.data.messages,
|
||||
);
|
||||
inChatMlArray = ChatMlArraySchema.safeParse(inputObject.data.messages);
|
||||
}
|
||||
}
|
||||
}
|
||||
const outOpenAiMessage = OpenAiMessageSchema.safeParse(output);
|
||||
const outChatMlMessage = ChatMlMessageSchema.safeParse(output);
|
||||
|
||||
// Pretty view available
|
||||
const isPrettyViewAvailable = inOpenAiMessageArray.success;
|
||||
const isPrettyViewAvailable = inChatMlArray.success;
|
||||
|
||||
// default I/O
|
||||
return (
|
||||
@@ -79,16 +75,16 @@ export const IOPreview: React.FC<{
|
||||
) : null}
|
||||
{isPrettyViewAvailable && currentView === "pretty" ? (
|
||||
<OpenAiMessageView
|
||||
messages={inOpenAiMessageArray.data.concat(
|
||||
outOpenAiMessage.success
|
||||
messages={inChatMlArray.data.concat(
|
||||
outChatMlMessage.success
|
||||
? {
|
||||
...outOpenAiMessage.data,
|
||||
role: outOpenAiMessage.data.role ?? "assistant",
|
||||
...outChatMlMessage.data,
|
||||
role: outChatMlMessage.data.role ?? "assistant",
|
||||
}
|
||||
: {
|
||||
: ChatMlMessageSchema.parse({
|
||||
role: "assistant",
|
||||
content: outputClean ? JSON.stringify(outputClean) : null,
|
||||
},
|
||||
}),
|
||||
)}
|
||||
/>
|
||||
) : null}
|
||||
@@ -116,34 +112,54 @@ export const IOPreview: React.FC<{
|
||||
);
|
||||
};
|
||||
|
||||
const OpenAiMessageSchema = z
|
||||
const ChatMlMessageSchema = z
|
||||
.object({
|
||||
role: z.enum(["system", "user", "assistant", "function"]).optional(),
|
||||
name: z.string().optional(),
|
||||
content: z.union([z.record(z.any()).array(), z.string()]).nullable(),
|
||||
function_call: z
|
||||
.object({
|
||||
name: z.string(),
|
||||
arguments: z.record(z.any()),
|
||||
})
|
||||
role: z
|
||||
.enum(["system", "user", "assistant", "function", "tool"])
|
||||
.optional(),
|
||||
name: z.string().optional(),
|
||||
content: z
|
||||
.union([z.record(z.any()), z.record(z.any()).array(), z.string()])
|
||||
.nullish(),
|
||||
additional_kwargs: z.record(z.any()).optional(),
|
||||
})
|
||||
.strict() // no additional properties
|
||||
.refine((value) => value.content !== null || value.role !== undefined);
|
||||
.passthrough()
|
||||
|
||||
const OpenAiMessageArraySchema = z.array(OpenAiMessageSchema).min(1);
|
||||
.refine((value) => value.content !== null || value.role !== undefined)
|
||||
.transform(({ additional_kwargs, ...other }) => ({
|
||||
...other,
|
||||
...additional_kwargs,
|
||||
}))
|
||||
.transform(({ role, name, content, ...other }) => ({
|
||||
role,
|
||||
name,
|
||||
content,
|
||||
json: Object.keys(other).length === 0 ? undefined : other,
|
||||
}));
|
||||
const ChatMlArraySchema = z.array(ChatMlMessageSchema).min(1);
|
||||
|
||||
const OpenAiMessageView: React.FC<{
|
||||
messages: z.infer<typeof OpenAiMessageArraySchema>;
|
||||
messages: z.infer<typeof ChatMlArraySchema>;
|
||||
}> = ({ messages }) => {
|
||||
const COLLAPSE_THRESHOLD = 3;
|
||||
const [isCollapsed, setCollapsed] = useState(
|
||||
messages.length > COLLAPSE_THRESHOLD ? true : null,
|
||||
);
|
||||
|
||||
const transformedMessages = messages;
|
||||
console.log(transformedMessages);
|
||||
// const transformedMessages = messages.map(
|
||||
// ({ role, name, content, ...rest }) => ({
|
||||
// role,
|
||||
// name,
|
||||
// content,
|
||||
// json: rest,
|
||||
// }),
|
||||
// );
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-2 rounded-md border p-3">
|
||||
{messages
|
||||
{transformedMessages
|
||||
.filter(
|
||||
(_, i) =>
|
||||
// show all if not collapsed or null; show first and last n if collapsed
|
||||
@@ -151,14 +167,36 @@ const OpenAiMessageView: React.FC<{
|
||||
)
|
||||
.map((message, index) => (
|
||||
<Fragment key={index}>
|
||||
<JSONView
|
||||
title={message.name ?? message.role}
|
||||
json={message.function_call ?? message.content}
|
||||
className={cn(
|
||||
message.role === "system" && "bg-gray-100",
|
||||
message.role === "assistant" && "bg-green-50",
|
||||
<div>
|
||||
{!!message.content && (
|
||||
<JSONView
|
||||
title={message.name ?? message.role}
|
||||
json={message.content}
|
||||
className={cn(
|
||||
"bg-gray-100",
|
||||
message.role === "system" && "bg-gray-100",
|
||||
message.role === "assistant" && "bg-green-50",
|
||||
message.role === "user" && "bg-white",
|
||||
!!message.json && "rounded-b-none",
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
{!!message.json && (
|
||||
<JSONView
|
||||
title={
|
||||
message.content ? undefined : message.name ?? message.role
|
||||
}
|
||||
json={message.json}
|
||||
className={cn(
|
||||
"bg-gray-100",
|
||||
message.role === "system" && "bg-gray-100",
|
||||
message.role === "assistant" && "bg-green-50",
|
||||
message.role === "user" && "bg-white",
|
||||
!!message.content && "rounded-t-none border-t-0",
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
{isCollapsed !== null && index === 0 ? (
|
||||
<Button
|
||||
variant="ghost"
|
||||
|
||||
@@ -1 +1 @@
|
||||
export const VERSION = "v2.10.2";
|
||||
export const VERSION = "v2.11.2";
|
||||
|
||||
@@ -42,11 +42,14 @@ export const env = createEnv({
|
||||
// AUTH
|
||||
AUTH_GOOGLE_CLIENT_ID: z.string().optional(),
|
||||
AUTH_GOOGLE_CLIENT_SECRET: z.string().optional(),
|
||||
AUTH_GOOGLE_ALLOW_ACCOUNT_LINKING: z.enum(["true", "false"]).optional(),
|
||||
AUTH_GITHUB_CLIENT_ID: z.string().optional(),
|
||||
AUTH_GITHUB_CLIENT_SECRET: z.string().optional(),
|
||||
AUTH_GITHUB_ALLOW_ACCOUNT_LINKING: z.enum(["true", "false"]).optional(),
|
||||
AUTH_AZURE_AD_CLIENT_ID: z.string().optional(),
|
||||
AUTH_AZURE_AD_CLIENT_SECRET: z.string().optional(),
|
||||
AUTH_AZURE_AD_TENANT_ID: z.string().optional(),
|
||||
AUTH_AZURE_ALLOW_ACCOUNT_LINKING: z.enum(["true", "false"]).optional(),
|
||||
AUTH_DOMAINS_WITH_SSO_ENFORCEMENT: z.string().optional(),
|
||||
AUTH_DISABLE_USERNAME_PASSWORD: z.enum(["true", "false"]).optional(),
|
||||
// EMAIL
|
||||
@@ -105,11 +108,17 @@ export const env = createEnv({
|
||||
// AUTH
|
||||
AUTH_GOOGLE_CLIENT_ID: process.env.AUTH_GOOGLE_CLIENT_ID,
|
||||
AUTH_GOOGLE_CLIENT_SECRET: process.env.AUTH_GOOGLE_CLIENT_SECRET,
|
||||
AUTH_GOOGLE_ALLOW_ACCOUNT_LINKING:
|
||||
process.env.AUTH_GOOGLE_ALLOW_ACCOUNT_LINKING,
|
||||
AUTH_GITHUB_CLIENT_ID: process.env.AUTH_GITHUB_CLIENT_ID,
|
||||
AUTH_GITHUB_CLIENT_SECRET: process.env.AUTH_GITHUB_CLIENT_SECRET,
|
||||
AUTH_GITHUB_ALLOW_ACCOUNT_LINKING:
|
||||
process.env.AUTH_GITHUB_ALLOW_ACCOUNT_LINKING,
|
||||
AUTH_AZURE_AD_CLIENT_ID: process.env.AUTH_AZURE_AD_CLIENT_ID,
|
||||
AUTH_AZURE_AD_CLIENT_SECRET: process.env.AUTH_AZURE_AD_CLIENT_SECRET,
|
||||
AUTH_AZURE_AD_TENANT_ID: process.env.AUTH_AZURE_AD_TENANT_ID,
|
||||
AUTH_AZURE_ALLOW_ACCOUNT_LINKING:
|
||||
process.env.AUTH_AZURE_ALLOW_ACCOUNT_LINKING,
|
||||
AUTH_DOMAINS_WITH_SSO_ENFORCEMENT:
|
||||
process.env.AUTH_DOMAINS_WITH_SSO_ENFORCEMENT,
|
||||
AUTH_DISABLE_USERNAME_PASSWORD: process.env.AUTH_DISABLE_USERNAME_PASSWORD,
|
||||
|
||||
@@ -127,7 +127,6 @@ export const ModelUsageChart = ({
|
||||
},
|
||||
];
|
||||
|
||||
console.log(data, "data", totalCost);
|
||||
return (
|
||||
<DashboardCard
|
||||
className={className}
|
||||
|
||||
@@ -6,6 +6,7 @@ import { api } from "@/src/utils/api";
|
||||
import { formatIntervalSeconds, intervalInSeconds } from "@/src/utils/dates";
|
||||
import { type RouterOutput } from "@/src/utils/types";
|
||||
import { type Score } from "@prisma/client";
|
||||
import { usdFormatter } from "../../../utils/numbers";
|
||||
|
||||
type RowData = {
|
||||
id: string;
|
||||
@@ -14,6 +15,7 @@ type RowData = {
|
||||
observation: { id: string; traceId: string };
|
||||
scores: Score[];
|
||||
latency: number;
|
||||
totalCost: string;
|
||||
};
|
||||
|
||||
export function DatasetRunItemsTable(
|
||||
@@ -72,6 +74,14 @@ export function DatasetRunItemsTable(
|
||||
return <>{formatIntervalSeconds(latency)}</>;
|
||||
},
|
||||
},
|
||||
{
|
||||
accessorKey: "totalCost",
|
||||
header: "Total Cost",
|
||||
cell: ({ row }) => {
|
||||
const totalCost: RowData["totalCost"] = row.getValue("totalCost");
|
||||
return <>{totalCost}</>;
|
||||
},
|
||||
},
|
||||
{
|
||||
accessorKey: "scores",
|
||||
header: "Scores",
|
||||
@@ -94,6 +104,9 @@ export function DatasetRunItemsTable(
|
||||
traceId: item.observation.traceId ?? "", // never actually null, just not enforced by db
|
||||
},
|
||||
scores: item.observation.scores,
|
||||
totalCost: usdFormatter(
|
||||
item.observation.calculatedTotalCost?.toNumber() ?? 0,
|
||||
),
|
||||
latency: intervalInSeconds(
|
||||
item.observation.startTime,
|
||||
item.observation.endTime,
|
||||
|
||||
@@ -7,6 +7,7 @@ import { api } from "@/src/utils/api";
|
||||
import { formatIntervalSeconds } from "@/src/utils/dates";
|
||||
import { type RouterOutput } from "@/src/utils/types";
|
||||
import { useEffect } from "react";
|
||||
import { usdFormatter } from "../../../utils/numbers";
|
||||
|
||||
type RowData = {
|
||||
key: {
|
||||
@@ -16,6 +17,7 @@ type RowData = {
|
||||
createdAt: string;
|
||||
countRunItems: string;
|
||||
avgLatency: number;
|
||||
avgTotalCost: string;
|
||||
scores: RouterOutput["datasets"]["runsByDatasetId"][number]["scores"];
|
||||
};
|
||||
|
||||
@@ -68,6 +70,15 @@ export function DatasetRunsTable(props: {
|
||||
return <>{formatIntervalSeconds(avgLatency)}</>;
|
||||
},
|
||||
},
|
||||
{
|
||||
accessorKey: "avgTotalCost",
|
||||
header: "Total Cost (avg)",
|
||||
cell: ({ row }) => {
|
||||
const avgTotalCost: RowData["avgTotalCost"] =
|
||||
row.getValue("avgTotalCost");
|
||||
return <>{avgTotalCost}</>;
|
||||
},
|
||||
},
|
||||
{
|
||||
accessorKey: "scores",
|
||||
header: "Scores (avg)",
|
||||
@@ -94,6 +105,7 @@ export function DatasetRunsTable(props: {
|
||||
createdAt: item.createdAt.toISOString(),
|
||||
countRunItems: item.countRunItems.toString(),
|
||||
avgLatency: item.avgLatency,
|
||||
avgTotalCost: usdFormatter(item.avgTotalCost.toNumber()),
|
||||
scores: item.scores,
|
||||
};
|
||||
};
|
||||
|
||||
@@ -79,6 +79,7 @@ export const datasetRouter = createTRPCRouter({
|
||||
countRunItems: number;
|
||||
scores: Record<string, number>;
|
||||
avgLatency: number;
|
||||
avgTotalCost: Prisma.Decimal;
|
||||
}
|
||||
>
|
||||
>(Prisma.sql`
|
||||
@@ -111,13 +112,14 @@ export const datasetRouter = createTRPCRouter({
|
||||
ORDER BY
|
||||
run_id
|
||||
),
|
||||
latency_by_run_id AS (
|
||||
latency_and_total_cost_by_run_id AS (
|
||||
SELECT
|
||||
ri.dataset_run_id run_id,
|
||||
AVG(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 "avgLatency"
|
||||
AVG(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 "avgLatency",
|
||||
AVG(COALESCE(o.calculated_total_cost, 0)) AS "avgTotalCost"
|
||||
FROM
|
||||
dataset_run_items ri
|
||||
JOIN observations o ON o.id = ri.observation_id
|
||||
JOIN observations_view o ON o.id = ri.observation_id
|
||||
WHERE o.project_id = ${input.projectId}
|
||||
group by 1
|
||||
)
|
||||
@@ -128,14 +130,15 @@ export const datasetRouter = createTRPCRouter({
|
||||
runs.created_at "createdAt",
|
||||
runs.updated_at "updatedAt",
|
||||
COALESCE(avg_scores.scores, '[]'::jsonb) scores,
|
||||
COALESCE(latency."avgLatency", 0) "avgLatency",
|
||||
COALESCE(latency_and_total_cost."avgLatency", 0) "avgLatency",
|
||||
COALESCE(latency_and_total_cost."avgTotalCost", 0) "avgTotalCost",
|
||||
count(DISTINCT ri.id)::int "countRunItems"
|
||||
FROM
|
||||
dataset_runs runs
|
||||
JOIN datasets ON datasets.id = runs.dataset_id
|
||||
LEFT JOIN dataset_run_items ri ON ri.dataset_run_id = runs.id
|
||||
LEFT JOIN json_avg_scores_by_run_id avg_scores ON avg_scores.run_id = runs.id
|
||||
LEFT JOIN latency_by_run_id latency ON latency.run_id = runs.id
|
||||
LEFT JOIN latency_and_total_cost_by_run_id latency_and_total_cost ON latency_and_total_cost.run_id = runs.id
|
||||
WHERE runs.dataset_id = ${input.datasetId}
|
||||
AND datasets.project_id = ${input.projectId}
|
||||
GROUP BY
|
||||
@@ -144,7 +147,8 @@ export const datasetRouter = createTRPCRouter({
|
||||
3,
|
||||
4,
|
||||
5,
|
||||
6
|
||||
6,
|
||||
7
|
||||
ORDER BY
|
||||
runs.created_at DESC
|
||||
`);
|
||||
@@ -395,7 +399,7 @@ export const datasetRouter = createTRPCRouter({
|
||||
),
|
||||
)
|
||||
.query(async ({ input, ctx }) => {
|
||||
return ctx.prisma.datasetRunItems.findMany({
|
||||
const runItems = await ctx.prisma.datasetRunItems.findMany({
|
||||
where: {
|
||||
datasetRunId: input.datasetRunId,
|
||||
datasetItemId: input.datasetItemId,
|
||||
@@ -417,6 +421,31 @@ export const datasetRouter = createTRPCRouter({
|
||||
createdAt: "desc",
|
||||
},
|
||||
});
|
||||
|
||||
const observationIds = runItems.map((ri) => ri.observationId);
|
||||
const observations = await ctx.prisma.observationView.findMany({
|
||||
select: {
|
||||
id: true,
|
||||
calculatedTotalCost: true,
|
||||
},
|
||||
where: {
|
||||
id: {
|
||||
in: observationIds,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
return runItems.map((ri) => {
|
||||
return {
|
||||
id: ri.id,
|
||||
createdAt: ri.createdAt,
|
||||
datasetItemId: ri.datasetItemId,
|
||||
observation: {
|
||||
...ri.observation,
|
||||
...observations.find((o) => o.id === ri.observationId),
|
||||
},
|
||||
};
|
||||
});
|
||||
}),
|
||||
observationInDatasets: protectedProjectProcedure
|
||||
.input(
|
||||
|
||||
@@ -69,9 +69,15 @@ export function ManualScoreButton({
|
||||
utils.sessions.invalidate(),
|
||||
]);
|
||||
};
|
||||
const mutCreateScore = api.scores.create.useMutation({ onSuccess });
|
||||
const mutUpdateScore = api.scores.update.useMutation({ onSuccess });
|
||||
const mutDeleteScore = api.scores.delete.useMutation({ onSuccess });
|
||||
const mutCreateScore = api.scores.createReviewScore.useMutation({
|
||||
onSuccess,
|
||||
});
|
||||
const mutUpdateScore = api.scores.updateReviewScore.useMutation({
|
||||
onSuccess,
|
||||
});
|
||||
const mutDeleteScore = api.scores.deleteReviewScore.useMutation({
|
||||
onSuccess,
|
||||
});
|
||||
|
||||
const handleDelete = async () => {
|
||||
if (score) {
|
||||
|
||||
@@ -1,10 +1,7 @@
|
||||
import { Card } from "@tremor/react";
|
||||
import { CodeView } from "@/src/components/ui/code";
|
||||
import { env } from "@/src/env.mjs";
|
||||
|
||||
export function HostNameProject() {
|
||||
if (env.NEXT_PUBLIC_LANGFUSE_CLOUD_REGION === "EU") return null;
|
||||
|
||||
return (
|
||||
<div>
|
||||
<h2 className="mb-5 text-base font-semibold leading-6 text-gray-900">
|
||||
|
||||
@@ -18,6 +18,7 @@ type RowData = {
|
||||
id: string;
|
||||
createdAt: Date;
|
||||
isActive: boolean;
|
||||
numberOfObservations: number;
|
||||
};
|
||||
|
||||
export function PromptTable(props: { projectId: string }) {
|
||||
@@ -72,6 +73,25 @@ export function PromptTable(props: { projectId: string }) {
|
||||
return createdAt.toLocaleString();
|
||||
},
|
||||
},
|
||||
{
|
||||
accessorKey: "numberOfObservations",
|
||||
header: "Number of Generations",
|
||||
cell: ({ row }) => {
|
||||
const numberOfObservations: number = row.getValue(
|
||||
"numberOfObservations",
|
||||
);
|
||||
const name: string = row.getValue("name");
|
||||
const filter = encodeURIComponent(
|
||||
`Prompt Name;stringOptions;;any of;${name}`,
|
||||
);
|
||||
return (
|
||||
<TableLink
|
||||
path={`/project/${props.projectId}/generations?filter=${numberOfObservations ? filter : ""}`}
|
||||
value={numberOfObservations.toLocaleString()}
|
||||
/>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
accessorKey: "actions",
|
||||
header: "Actions",
|
||||
@@ -95,6 +115,7 @@ export function PromptTable(props: { projectId: string }) {
|
||||
version: item.version,
|
||||
createdAt: item.createdAt,
|
||||
isActive: item.isActive,
|
||||
numberOfObservations: Number(item.observationCount),
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
@@ -8,6 +8,7 @@ import { throwIfNoAccess } from "@/src/features/rbac/utils/checkAccess";
|
||||
import { type Prompt, type PrismaClient } from "@prisma/client";
|
||||
import { jsonSchema } from "@/src/utils/zod";
|
||||
import { auditLog } from "@/src/features/audit-logs/auditLog";
|
||||
import { DB } from "@/src/server/db";
|
||||
|
||||
export const CreatePrompt = z.object({
|
||||
projectId: z.string(),
|
||||
@@ -30,8 +31,17 @@ export const promptRouter = createTRPCRouter({
|
||||
projectId: input.projectId,
|
||||
scope: "prompts:read",
|
||||
});
|
||||
|
||||
const prompts = await ctx.prisma.$queryRaw<Array<Prompt>>`
|
||||
SELECT id, name, version, project_id as "projectId", prompt, updated_at as "updatedAt", created_at AS "createdAt", is_active AS "isActive"
|
||||
SELECT
|
||||
id,
|
||||
name,
|
||||
version,
|
||||
project_id AS "projectId",
|
||||
prompt,
|
||||
updated_at AS "updatedAt",
|
||||
created_at AS "createdAt",
|
||||
is_active AS "isActive"
|
||||
FROM prompts
|
||||
WHERE (name, version) IN (
|
||||
SELECT name, MAX(version)
|
||||
@@ -41,7 +51,35 @@ export const promptRouter = createTRPCRouter({
|
||||
)
|
||||
AND "project_id" = ${input.projectId}
|
||||
ORDER BY name ASC`;
|
||||
return prompts;
|
||||
|
||||
const promptCountQuery = DB.selectFrom("observations")
|
||||
.fullJoin("prompts", "prompts.id", "observations.prompt_id")
|
||||
.select(({ fn }) => [
|
||||
"prompts.name",
|
||||
fn.count("observations.id").as("count"),
|
||||
])
|
||||
.where("prompts.project_id", "=", input.projectId)
|
||||
.where("observations.project_id", "=", input.projectId)
|
||||
.groupBy("prompts.name");
|
||||
|
||||
const compiledQuery = promptCountQuery.compile();
|
||||
|
||||
const promptCounts = await ctx.prisma.$queryRawUnsafe<
|
||||
Array<{
|
||||
name: string;
|
||||
count: bigint;
|
||||
}>
|
||||
>(compiledQuery.sql, ...compiledQuery.parameters);
|
||||
|
||||
const joinedPromptsAndCounts = prompts.map((p) => {
|
||||
const marchedCount = promptCounts.find((c) => c.name === p.name);
|
||||
return {
|
||||
...p,
|
||||
observationCount: marchedCount?.count ?? 0,
|
||||
};
|
||||
});
|
||||
|
||||
return joinedPromptsAndCounts;
|
||||
}),
|
||||
byId: protectedProjectProcedure
|
||||
.input(
|
||||
|
||||
@@ -67,34 +67,37 @@ export function CreateApiKeyButton(props: { projectId: string }) {
|
||||
</DialogTrigger>
|
||||
<DialogContent
|
||||
onPointerDownOutside={(e) => e.preventDefault()}
|
||||
className="max-w-full md:max-w-xl"
|
||||
className="flex max-h-screen w-full flex-col md:max-w-xl"
|
||||
>
|
||||
<DialogTitle>API Keys</DialogTitle>
|
||||
<div className="mb-2">
|
||||
<div className="text-md font-semibold">Secret Key</div>
|
||||
<div className="my-2">
|
||||
This key can only be viewed once. You can always generate a new key.
|
||||
</div>
|
||||
<CodeView content={generatedKeys?.secretKey ?? "Loading ..."} />
|
||||
</div>
|
||||
<div>
|
||||
<div className="text-md mb-2 font-semibold">Public Key</div>
|
||||
<CodeView content={generatedKeys?.publicKey ?? "Loading ..."} />
|
||||
</div>
|
||||
<div>
|
||||
<div className="text-md mb-2 font-semibold">Host</div>
|
||||
<CodeView content={hostname} />
|
||||
</div>
|
||||
{generatedKeys && (
|
||||
<div className="shrink overflow-x-hidden overflow-y-scroll">
|
||||
<div className="mb-2">
|
||||
<div className="text-md my-2 font-semibold">Usage</div>
|
||||
<QuickstartExamples
|
||||
secretKey={generatedKeys.secretKey}
|
||||
publicKey={generatedKeys.publicKey}
|
||||
host={hostname}
|
||||
/>
|
||||
<div className="text-md font-semibold">Secret Key</div>
|
||||
<div className="my-2">
|
||||
This key can only be viewed once. You can always generate a new
|
||||
key.
|
||||
</div>
|
||||
<CodeView content={generatedKeys?.secretKey ?? "Loading ..."} />
|
||||
</div>
|
||||
)}
|
||||
<div>
|
||||
<div className="text-md mb-2 font-semibold">Public Key</div>
|
||||
<CodeView content={generatedKeys?.publicKey ?? "Loading ..."} />
|
||||
</div>
|
||||
<div>
|
||||
<div className="text-md mb-2 font-semibold">Host</div>
|
||||
<CodeView content={hostname} />
|
||||
</div>
|
||||
{generatedKeys && (
|
||||
<div className="mb-2 max-w-full">
|
||||
<div className="text-md my-2 font-semibold">Usage</div>
|
||||
<QuickstartExamples
|
||||
secretKey={generatedKeys.secretKey}
|
||||
publicKey={generatedKeys.publicKey}
|
||||
host={hostname}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
|
||||
@@ -15,15 +15,18 @@ export const QuickstartExamples = ({
|
||||
publicKey: string;
|
||||
host: string;
|
||||
}) => (
|
||||
<Tabs defaultValue="python">
|
||||
<TabsList>
|
||||
<TabsTrigger value="python">Python</TabsTrigger>
|
||||
<TabsTrigger value="js">JS/TS</TabsTrigger>
|
||||
<TabsTrigger value="openai">OpenAI</TabsTrigger>
|
||||
<TabsTrigger value="langchain">Langchain</TabsTrigger>
|
||||
<TabsTrigger value="langchain-js">Langchain JS</TabsTrigger>
|
||||
<TabsTrigger value="other">Other</TabsTrigger>
|
||||
</TabsList>
|
||||
<Tabs defaultValue="python" className="relative max-w-full">
|
||||
<div className="overflow-x-scroll">
|
||||
<TabsList>
|
||||
<TabsTrigger value="python">Python</TabsTrigger>
|
||||
<TabsTrigger value="js">JS/TS</TabsTrigger>
|
||||
<TabsTrigger value="openai">OpenAI</TabsTrigger>
|
||||
<TabsTrigger value="langchain">Langchain</TabsTrigger>
|
||||
<TabsTrigger value="langchain-js">Langchain JS</TabsTrigger>
|
||||
<TabsTrigger value="llamaindex">LlamaIndex</TabsTrigger>
|
||||
<TabsTrigger value="other">Other</TabsTrigger>
|
||||
</TabsList>
|
||||
</div>
|
||||
<TabsContent value="python">
|
||||
<CodeView content="pip install langfuse" className="mb-2 bg-blue-50" />
|
||||
<CodeView
|
||||
@@ -49,7 +52,7 @@ export const QuickstartExamples = ({
|
||||
>
|
||||
Python docs
|
||||
</a>{" "}
|
||||
for more details.
|
||||
for more details and an end-to-end example.
|
||||
</p>
|
||||
</TabsContent>
|
||||
<TabsContent value="js">
|
||||
@@ -77,7 +80,7 @@ export const QuickstartExamples = ({
|
||||
>
|
||||
JS/TS docs
|
||||
</a>{" "}
|
||||
for more details.
|
||||
for more details and an end-to-end example.
|
||||
</p>
|
||||
</TabsContent>
|
||||
<TabsContent value="openai">
|
||||
@@ -89,7 +92,7 @@ export const QuickstartExamples = ({
|
||||
<CodeView content="pip install langfuse" className="my-2 bg-blue-50" />
|
||||
<CodeView
|
||||
title=".env"
|
||||
content={`LANGFUSE_SECRET_KEY=${secretKey};\nLANGFUSE_PUBLIC_KEY=${publicKey};\nLANGFUSE_HOST="${host}";`}
|
||||
content={`LANGFUSE_SECRET_KEY=${secretKey}\nLANGFUSE_PUBLIC_KEY=${publicKey}\nLANGFUSE_HOST="${host}"`}
|
||||
className="my-2 bg-blue-50"
|
||||
/>
|
||||
<CodeView
|
||||
@@ -106,7 +109,7 @@ export const QuickstartExamples = ({
|
||||
>
|
||||
OpenAI Integration docs
|
||||
</a>{" "}
|
||||
for more details.
|
||||
for more details and an end-to-end example.
|
||||
</p>
|
||||
</TabsContent>
|
||||
<TabsContent value="langchain">
|
||||
@@ -116,11 +119,9 @@ export const QuickstartExamples = ({
|
||||
</p>
|
||||
<CodeView content="pip install langfuse" className="my-2 bg-blue-50" />
|
||||
<CodeView
|
||||
title=".env"
|
||||
content={`LANGFUSE_SECRET_KEY=${secretKey};\nLANGFUSE_PUBLIC_KEY=${publicKey};\nLANGFUSE_HOST="${host}";`}
|
||||
content={LANGCHAIN_PYTHON_CODE({ publicKey, secretKey, host })}
|
||||
className="my-2 bg-blue-50"
|
||||
/>
|
||||
<CodeView content={LANGCHAIN_PYTHON_CODE} className="my-2 bg-blue-50" />
|
||||
<p className="mt-2 text-xs text-gray-600">
|
||||
See the{" "}
|
||||
<a
|
||||
@@ -131,7 +132,7 @@ export const QuickstartExamples = ({
|
||||
>
|
||||
Langchain Integration docs
|
||||
</a>{" "}
|
||||
for more details.
|
||||
for more details and an end-to-end example.
|
||||
</p>
|
||||
</TabsContent>
|
||||
<TabsContent value="langchain-js">
|
||||
@@ -144,11 +145,9 @@ export const QuickstartExamples = ({
|
||||
className="my-2 bg-blue-50"
|
||||
/>
|
||||
<CodeView
|
||||
title=".env"
|
||||
content={`LANGFUSE_SECRET_KEY=${secretKey};\nLANGFUSE_PUBLIC_KEY=${publicKey};\LANGFUSE_BASEURL="${host}";`}
|
||||
content={LANGCHAIN_JS_CODE({ publicKey, secretKey, host })}
|
||||
className="my-2 bg-blue-50"
|
||||
/>
|
||||
<CodeView content={LANGCHAIN_JS_CODE} className="my-2 bg-blue-50" />
|
||||
<p className="mt-2 text-xs text-gray-600">
|
||||
See the{" "}
|
||||
<a
|
||||
@@ -159,7 +158,33 @@ export const QuickstartExamples = ({
|
||||
>
|
||||
Langchain Integration docs
|
||||
</a>{" "}
|
||||
for more details.
|
||||
for more details and an end-to-end example.
|
||||
</p>
|
||||
</TabsContent>
|
||||
<TabsContent value="llamaindex">
|
||||
<p className="mt-2 text-xs text-gray-600">
|
||||
The integration uses the LlamaIndex callback system to automatically
|
||||
capture detailed traces of your LlamaIndex executions.
|
||||
</p>
|
||||
<CodeView
|
||||
content="pip install langfuse llama-index"
|
||||
className="my-2 bg-blue-50"
|
||||
/>
|
||||
<CodeView
|
||||
content={LLAMA_INDEX_CODE({ publicKey, secretKey, host })}
|
||||
className="my-2 bg-blue-50"
|
||||
/>
|
||||
<p className="mt-2 text-xs text-gray-600">
|
||||
See the{" "}
|
||||
<a
|
||||
href="https://langfuse.com/docs/integrations/llama-index"
|
||||
className="underline"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
LlamaIndex Integration docs
|
||||
</a>{" "}
|
||||
for more details and an end-to-end example.
|
||||
</p>
|
||||
</TabsContent>
|
||||
<TabsContent value="other">
|
||||
@@ -188,23 +213,55 @@ export const QuickstartExamples = ({
|
||||
</Tabs>
|
||||
);
|
||||
|
||||
const LANGCHAIN_PYTHON_CODE = `from langfuse.callback import CallbackHandler
|
||||
const LANGCHAIN_PYTHON_CODE = (p: {
|
||||
publicKey: string;
|
||||
secretKey: string;
|
||||
host: string;
|
||||
}) => `from langfuse.callback import CallbackHandler
|
||||
langfuse_handler = CallbackHandler(
|
||||
public_key="${p.publicKey}",
|
||||
secret_key="${p.secretKey}",
|
||||
host="${p.host}"
|
||||
)
|
||||
|
||||
# <Your Langchain code here>
|
||||
|
||||
# Add handler to run/invoke/call/chat
|
||||
agent.run("<user_input>", callbacks=[handler])`;
|
||||
chain.invoke({"input": "<user_input>"}, config={"callbacks": [langfuse_handler]})`;
|
||||
|
||||
const LANGCHAIN_JS_CODE = `import { CallbackHandler } from "langfuse-langchain";
|
||||
const LANGCHAIN_JS_CODE = (p: {
|
||||
publicKey: string;
|
||||
secretKey: string;
|
||||
host: string;
|
||||
}) => `import { CallbackHandler } from "langfuse-langchain";
|
||||
|
||||
// Initialize Langfuse callback handler
|
||||
const handler = new CallbackHandler();
|
||||
const langfuseHandler = new CallbackHandler({
|
||||
publicKey: "${p.publicKey}",
|
||||
secretKey: "${p.secretKey}",
|
||||
baseUrl: "${p.host}"
|
||||
});
|
||||
|
||||
// Your Langchain implementation
|
||||
const chain = new LLMChain(...);
|
||||
|
||||
// Add handler as callback when running the Langchain agent
|
||||
await chain.call(
|
||||
await chain.invoke(
|
||||
{ input: "<user_input>" },
|
||||
{ callbacks: [handler] }
|
||||
{ callbacks: [langfuseHandler] }
|
||||
);`;
|
||||
|
||||
const LLAMA_INDEX_CODE = (p: {
|
||||
publicKey: string;
|
||||
secretKey: string;
|
||||
host: string;
|
||||
}) => `from llama_index.core import Settings
|
||||
from llama_index.core.callbacks import CallbackManager
|
||||
from langfuse.llama_index import LlamaIndexCallbackHandler
|
||||
|
||||
langfuse_callback_handler = LlamaIndexCallbackHandler(
|
||||
public_key="${p.publicKey}",
|
||||
secret_key="${p.secretKey}",
|
||||
host="${p.host}"
|
||||
)
|
||||
Settings.callback_manager = CallbackManager([langfuse_callback_handler])"`;
|
||||
|
||||
@@ -35,4 +35,4 @@ const TagCommandItem = ({
|
||||
);
|
||||
};
|
||||
|
||||
export default TagCommandItem;
|
||||
export default TagCommandItem;
|
||||
|
||||
@@ -238,15 +238,6 @@ async function posthogTelemetry({
|
||||
LIMIT 30
|
||||
`;
|
||||
|
||||
posthog.identify({
|
||||
distinctId: "docker:" + clientId,
|
||||
properties: {
|
||||
environment: process.env.NODE_ENV,
|
||||
userDomains: domains,
|
||||
docker: true,
|
||||
langfuseVersion: VERSION,
|
||||
},
|
||||
});
|
||||
posthog.capture({
|
||||
distinctId: "docker:" + clientId,
|
||||
event: "telemetry",
|
||||
@@ -263,6 +254,12 @@ async function posthogTelemetry({
|
||||
datasetRunItems: countDatasetRunItems,
|
||||
startTimeframe: startTimeframe?.toISOString(),
|
||||
endTimeframe: endTimeframe.toISOString(),
|
||||
$set: {
|
||||
environment: process.env.NODE_ENV,
|
||||
userDomains: domains,
|
||||
docker: true,
|
||||
langfuseVersion: VERSION,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { useState } from "react";
|
||||
import Header from "@/src/components/layouts/header";
|
||||
import { type DateTimeAggregationOption } from "@/src/features/dashboard/lib/timeseries-aggregation";
|
||||
import { findClosestInterval } from "@/src/features/dashboard/lib/timeseries-aggregation";
|
||||
import { useRouter } from "next/router";
|
||||
import { GenerationLatencyChart } from "@/src/features/dashboard/components/LatencyChart";
|
||||
import { ChartScores } from "@/src/features/dashboard/components/ChartScores";
|
||||
@@ -32,7 +31,9 @@ import { FilterBuilder } from "@/src/features/filters/components/filter-builder"
|
||||
import { type FilterState } from "@/src/features/filters/types";
|
||||
import { type ColumnDefinition } from "@/src/server/api/interfaces/tableDefinition";
|
||||
import { useQueryFilterState } from "@/src/features/filters/hooks/useFilterState";
|
||||
import { LatencyTables } from "@/src/features/dashboard/components/SpanLatencyTable";
|
||||
import { LatencyTables } from "@/src/features/dashboard/components/LatencyTables";
|
||||
import { useMemo } from "react";
|
||||
import { useSession } from "next-auth/react";
|
||||
|
||||
export type DashboardDateRange = {
|
||||
from: Date;
|
||||
@@ -40,29 +41,30 @@ export type DashboardDateRange = {
|
||||
};
|
||||
|
||||
export default function Start() {
|
||||
const [agg, setAgg] = useState<DateTimeAggregationOption>("7 days");
|
||||
const router = useRouter();
|
||||
const projectId = router.query.projectId as string;
|
||||
const posthog = usePostHog();
|
||||
|
||||
const projects = api.projects.all.useQuery();
|
||||
const project = projects.data?.find((p) => p.id === projectId);
|
||||
const session = useSession();
|
||||
const project = session.data?.user?.projects.find(
|
||||
(project) => project.id === projectId,
|
||||
);
|
||||
|
||||
const currDate = new Date();
|
||||
const FromParam = withDefault(NumberParam, addDays(currDate, -7).getTime());
|
||||
const ToParam = withDefault(NumberParam, currDate.getTime());
|
||||
const SelectParam = withDefault(StringParam, "Select a date range");
|
||||
const memoizedDate = useMemo(() => new Date(), []);
|
||||
|
||||
const [urlParams, setUrlParams] = useQueryParams({
|
||||
from: FromParam,
|
||||
to: ToParam,
|
||||
select: SelectParam,
|
||||
from: withDefault(NumberParam, addDays(memoizedDate, -7).getTime()),
|
||||
to: withDefault(NumberParam, memoizedDate.getTime()),
|
||||
select: withDefault(StringParam, "Select a date range"),
|
||||
});
|
||||
|
||||
const dateRange =
|
||||
urlParams.from && urlParams.to
|
||||
? { from: new Date(urlParams.from), to: new Date(urlParams.to) }
|
||||
: undefined;
|
||||
const dateRange = useMemo(
|
||||
() =>
|
||||
urlParams.from && urlParams.to
|
||||
? { from: new Date(urlParams.from), to: new Date(urlParams.to) }
|
||||
: undefined,
|
||||
[urlParams.from, urlParams.to],
|
||||
);
|
||||
|
||||
const selectedOption = isValidOption(urlParams.select)
|
||||
? urlParams.select
|
||||
@@ -105,6 +107,11 @@ export default function Start() {
|
||||
|
||||
const [userFilterState, setUserFilterState] = useQueryFilterState([]);
|
||||
|
||||
const agg = useMemo(
|
||||
() => (dateRange ? findClosestInterval(dateRange) ?? "7 days" : "7 days"),
|
||||
[dateRange],
|
||||
);
|
||||
|
||||
const timeFilter = dateRange
|
||||
? [
|
||||
{
|
||||
@@ -131,7 +138,6 @@ export default function Start() {
|
||||
<div className=" flex flex-col gap-2 lg:flex-row">
|
||||
<DatePickerWithRange
|
||||
dateRange={dateRange}
|
||||
setAgg={setAgg}
|
||||
setDateRangeAndOption={setDateRangeAndOption}
|
||||
selectedOption={selectedOption}
|
||||
className="my-0 max-w-full overflow-x-auto"
|
||||
|
||||
@@ -66,7 +66,7 @@ export default function UsersPage() {
|
||||
})),
|
||||
);
|
||||
|
||||
const totalCount = userRowData.rows?.length ?? 0;
|
||||
const totalCount = users.data?.totalUsers ?? 0;
|
||||
|
||||
useEffect(() => {
|
||||
if (users.isSuccess) {
|
||||
|
||||
@@ -7,13 +7,13 @@ import { ObservationLevel } from "@prisma/client";
|
||||
// to be used server side
|
||||
export const observationsTableCols: ColumnDefinition[] = [
|
||||
{
|
||||
name: "id",
|
||||
name: "Id",
|
||||
id: "id",
|
||||
type: "string",
|
||||
internal: 'o."id"',
|
||||
},
|
||||
{
|
||||
name: "name",
|
||||
name: "Name",
|
||||
id: "name",
|
||||
type: "stringOptions",
|
||||
internal: 'o."name"',
|
||||
@@ -21,7 +21,7 @@ export const observationsTableCols: ColumnDefinition[] = [
|
||||
},
|
||||
{ name: "traceId", id: "traceId", type: "string", internal: 't."id"' },
|
||||
{
|
||||
name: "traceName",
|
||||
name: "Trace Name",
|
||||
id: "traceName",
|
||||
type: "stringOptions",
|
||||
internal: 't."name"',
|
||||
@@ -29,30 +29,30 @@ export const observationsTableCols: ColumnDefinition[] = [
|
||||
},
|
||||
{ name: "userId", id: "userId", type: "string", internal: 't."user_id"' },
|
||||
{
|
||||
name: "start_time",
|
||||
name: "Start Time",
|
||||
id: "startTime",
|
||||
type: "datetime",
|
||||
internal: 'o."start_time"',
|
||||
},
|
||||
{
|
||||
name: "end_time",
|
||||
name: "End Time",
|
||||
id: "endTime",
|
||||
type: "datetime",
|
||||
internal: 'o."end_time"',
|
||||
},
|
||||
{
|
||||
name: "latency (s)",
|
||||
name: "Latency (s)",
|
||||
id: "latency",
|
||||
type: "number",
|
||||
internal: '"latency"',
|
||||
},
|
||||
{
|
||||
name: "Cost ($)",
|
||||
name: "Total Cost ($)",
|
||||
type: "number",
|
||||
internal: 'o."calculated_total_cost"',
|
||||
},
|
||||
{
|
||||
name: "level",
|
||||
name: "Level",
|
||||
id: "level",
|
||||
type: "stringOptions",
|
||||
internal: 'o."level"::text',
|
||||
@@ -65,30 +65,37 @@ export const observationsTableCols: ColumnDefinition[] = [
|
||||
internal: 'o."status_message"',
|
||||
},
|
||||
{
|
||||
name: "model",
|
||||
name: "Model",
|
||||
id: "model",
|
||||
type: "stringOptions",
|
||||
internal: 'o."model"',
|
||||
options: [], // to be added at runtime
|
||||
},
|
||||
{
|
||||
name: "metadata",
|
||||
name: "Metadata",
|
||||
id: "metadata",
|
||||
type: "stringObject",
|
||||
internal: 'o."metadata"',
|
||||
},
|
||||
{
|
||||
name: "scores_avg",
|
||||
name: "Scores",
|
||||
id: "scores_avg",
|
||||
type: "numberObject",
|
||||
internal: "scores_avg",
|
||||
},
|
||||
{
|
||||
name: "version",
|
||||
name: "Version",
|
||||
id: "version",
|
||||
type: "string",
|
||||
internal: 'o."version"',
|
||||
},
|
||||
{
|
||||
name: "Prompt Name",
|
||||
id: "promptName",
|
||||
type: "stringOptions",
|
||||
internal: "p.name",
|
||||
options: [], // to be added at runtime
|
||||
},
|
||||
];
|
||||
|
||||
// to be used client side, insert options for use in filter-builder
|
||||
@@ -98,24 +105,28 @@ export type ObservationOptions = {
|
||||
name: Array<OptionsDefinition>;
|
||||
traceName: Array<OptionsDefinition>;
|
||||
scores_avg: Array<string>;
|
||||
promptName: Array<OptionsDefinition>;
|
||||
};
|
||||
|
||||
export function observationsTableColsWithOptions(
|
||||
options?: ObservationOptions,
|
||||
): ColumnDefinition[] {
|
||||
return observationsTableCols.map((col) => {
|
||||
if (col.name === "model") {
|
||||
if (col.id === "model") {
|
||||
return { ...col, options: options?.model ?? [] };
|
||||
}
|
||||
if (col.name === "name") {
|
||||
if (col.id === "name") {
|
||||
return { ...col, options: options?.name ?? [] };
|
||||
}
|
||||
if (col.name === "traceName") {
|
||||
if (col.id === "traceName") {
|
||||
return { ...col, options: options?.traceName ?? [] };
|
||||
}
|
||||
if (col.name === "scores_avg") {
|
||||
if (col.id === "scores_avg") {
|
||||
return { ...col, keyOptions: options?.scores_avg ?? [] };
|
||||
}
|
||||
if (col.id === "promptName") {
|
||||
return { ...col, options: options?.promptName ?? [] };
|
||||
}
|
||||
return col;
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
import { Readable } from "stream";
|
||||
|
||||
import { type Prisma, type PrismaClient } from "@prisma/client";
|
||||
|
||||
/**
|
||||
* DatabaseReadStream fetches and streams database records in paginated batches,
|
||||
* simulating a streaming behavior. This class is designed for efficient, memory-optimized chunking of
|
||||
@@ -25,8 +23,11 @@ export class DatabaseReadStream<EntityType> extends Readable {
|
||||
private isReading: boolean;
|
||||
|
||||
constructor(
|
||||
private prisma: PrismaClient,
|
||||
private queryBuilder: (limit?: number, offset?: number) => Prisma.Sql,
|
||||
// the delegate function takes care of querying the database in a paginated manner
|
||||
private queryDelegate: (
|
||||
pageSize: number,
|
||||
offset: number,
|
||||
) => Promise<Array<EntityType>>,
|
||||
private pageSize: number,
|
||||
) {
|
||||
super({ objectMode: true }); // Set object mode to true to allow pushing objects to the stream rather than strings or buffers
|
||||
@@ -42,10 +43,7 @@ export class DatabaseReadStream<EntityType> extends Readable {
|
||||
this.isReading = true;
|
||||
|
||||
try {
|
||||
const rows = await this.prisma.$queryRaw<EntityType[]>(
|
||||
this.queryBuilder(this.pageSize, this.offset),
|
||||
);
|
||||
|
||||
const rows = await this.queryDelegate(this.pageSize, this.offset);
|
||||
if (rows.length > 0) {
|
||||
rows.forEach((row) => this.push(row));
|
||||
this.offset += rows.length;
|
||||
|
||||
@@ -4,22 +4,17 @@ import {
|
||||
} from "@/src/features/filters/server/filterToPrisma";
|
||||
import { orderByToPrismaSql } from "@/src/features/orderBy/server/orderByToPrisma";
|
||||
import { observationsTableCols } from "@/src/server/api/definitions/observationsTable";
|
||||
import { Prisma } from "@prisma/client";
|
||||
|
||||
import { type GenerationsExportInput } from "../exportQuery";
|
||||
import { type ObservationView, Prisma } from "@prisma/client";
|
||||
import { prisma } from "@/src/server/db";
|
||||
import { type GetAllGenerationsInput } from "../getAllQuery";
|
||||
|
||||
type GetSqlFromInputParams =
|
||||
| {
|
||||
input: GenerationsExportInput;
|
||||
type: "export";
|
||||
}
|
||||
| { input: Omit<GetAllGenerationsInput, "limit, page">; type: "paginate" };
|
||||
|
||||
export function getAllGenerationsSqlQuery({
|
||||
export async function getAllGenerations({
|
||||
input,
|
||||
type,
|
||||
}: GetSqlFromInputParams) {
|
||||
selectIO,
|
||||
}: {
|
||||
input: GetAllGenerationsInput;
|
||||
selectIO: boolean;
|
||||
}) {
|
||||
const searchCondition = input.searchQuery
|
||||
? Prisma.sql`AND (
|
||||
o."id" ILIKE ${`%${input.searchQuery}%`} OR
|
||||
@@ -53,18 +48,7 @@ export function getAllGenerationsSqlQuery({
|
||||
)
|
||||
: Prisma.empty;
|
||||
|
||||
// For exports: use a date cutoff filter to ignore newly ingested rows
|
||||
const dateCutoffFilter =
|
||||
type === "export"
|
||||
? datetimeFilterToPrismaSql("start_time", "<", new Date())
|
||||
: Prisma.empty;
|
||||
|
||||
const queryBuilder = (limit?: number, offset?: number) => {
|
||||
const pagination =
|
||||
limit !== undefined && offset !== undefined
|
||||
? Prisma.sql`LIMIT ${limit} OFFSET ${offset}`
|
||||
: Prisma.empty;
|
||||
return Prisma.sql`
|
||||
const query = Prisma.sql`
|
||||
WITH scores_avg AS (
|
||||
SELECT
|
||||
trace_id,
|
||||
@@ -88,7 +72,7 @@ export function getAllGenerationsSqlQuery({
|
||||
1) tmp
|
||||
GROUP BY
|
||||
1, 2
|
||||
), observations_agg as (
|
||||
)
|
||||
SELECT
|
||||
o.id,
|
||||
o.name,
|
||||
@@ -96,8 +80,7 @@ export function getAllGenerationsSqlQuery({
|
||||
o."modelParameters",
|
||||
o.start_time as "startTime",
|
||||
o.end_time as "endTime",
|
||||
o.input,
|
||||
o.output,
|
||||
${selectIO ? Prisma.sql`o.input, o.output,` : Prisma.empty}
|
||||
o.metadata,
|
||||
o.trace_id as "traceId",
|
||||
t.name as "traceName",
|
||||
@@ -129,60 +112,47 @@ export function getAllGenerationsSqlQuery({
|
||||
AND t.project_id = ${input.projectId}
|
||||
AND o.type = 'GENERATION'
|
||||
${datetimeFilter}
|
||||
${dateCutoffFilter}
|
||||
${searchCondition}
|
||||
${filterCondition}
|
||||
${orderByCondition}
|
||||
${pagination}
|
||||
)
|
||||
SELECT
|
||||
o.*,
|
||||
COALESCE(jsonb_agg(
|
||||
CASE
|
||||
WHEN s.name IS NOT NULL THEN jsonb_build_object(
|
||||
'name', s.name,
|
||||
'value', s.value,
|
||||
'comment', s.comment
|
||||
)
|
||||
ELSE NULL
|
||||
END
|
||||
) FILTER (WHERE s.name IS NOT NULL), '[]'::jsonb) AS "scores"
|
||||
FROM
|
||||
observations_agg o
|
||||
LEFT JOIN scores s ON s.trace_id = o. "traceId" AND s.observation_id = o.id
|
||||
GROUP BY
|
||||
o.id,
|
||||
o.name,
|
||||
o.model,
|
||||
o. "modelParameters",
|
||||
o. "startTime",
|
||||
o. "endTime",
|
||||
o.input,
|
||||
o.output,
|
||||
o.metadata,
|
||||
o."traceId",
|
||||
o."traceName",
|
||||
o."completionStartTime",
|
||||
o."promptTokens",
|
||||
o."completionTokens",
|
||||
o."totalTokens",
|
||||
o.unit,
|
||||
o.level,
|
||||
o."statusMessage",
|
||||
o.version,
|
||||
o."modelId",
|
||||
o."inputPrice",
|
||||
o."outputPrice",
|
||||
o."totalPrice",
|
||||
o."calculatedInputCost",
|
||||
o."calculatedOutputCost",
|
||||
o."calculatedTotalCost",
|
||||
o."latency",
|
||||
o."promptId",
|
||||
o."promptName",
|
||||
o."promptVersion";
|
||||
LIMIT ${input.limit} OFFSET ${input.page * input.limit}
|
||||
`;
|
||||
};
|
||||
|
||||
return { queryBuilder, datetimeFilter, searchCondition, filterCondition };
|
||||
const generations = await prisma.$queryRaw<
|
||||
Array<
|
||||
ObservationView & {
|
||||
traceName: string | null;
|
||||
promptName: string | null;
|
||||
promptVersion: string | null;
|
||||
}
|
||||
>
|
||||
>(query);
|
||||
|
||||
const scores = await prisma.score.findMany({
|
||||
where: {
|
||||
trace: {
|
||||
projectId: input.projectId,
|
||||
},
|
||||
observationId: {
|
||||
in: generations.map((gen) => gen.id),
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const fullGenerations = generations.map((generation) => {
|
||||
const filteredScores = scores.filter(
|
||||
(s) => s.observationId === generation.id,
|
||||
);
|
||||
return {
|
||||
...generation,
|
||||
scores: filteredScores,
|
||||
};
|
||||
});
|
||||
|
||||
return {
|
||||
generations: fullGenerations,
|
||||
datetimeFilter,
|
||||
searchCondition,
|
||||
filterCondition,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -11,7 +11,7 @@ import { protectedProjectProcedure } from "@/src/server/api/trpc";
|
||||
import { type ObservationView } from "@prisma/client";
|
||||
|
||||
import { DatabaseReadStream } from "../db/DatabaseReadStream";
|
||||
import { getAllGenerationsSqlQuery } from "../db/getAllGenerationsSqlQuery";
|
||||
import { getAllGenerations as getAllGenerations } from "../db/getAllGenerationsSqlQuery";
|
||||
import { GenerationTableOptions } from "../utils/GenerationTableOptions";
|
||||
import { transformStreamToCsv } from "./transforms/transformStreamToCsv";
|
||||
import { transformStreamToJson } from "./transforms/transformStreamToJson";
|
||||
@@ -35,16 +35,29 @@ export type GenerationsExportResult =
|
||||
|
||||
export const generationsExportQuery = protectedProjectProcedure
|
||||
.input(generationsExportInput)
|
||||
.query<GenerationsExportResult>(async ({ input, ctx }) => {
|
||||
const { queryBuilder } = getAllGenerationsSqlQuery({
|
||||
input,
|
||||
type: "export",
|
||||
});
|
||||
|
||||
.query<GenerationsExportResult>(async ({ input }) => {
|
||||
const queryPageSize = env.DB_EXPORT_PAGE_SIZE ?? 1000;
|
||||
|
||||
const dateCutoffFilter = {
|
||||
column: "Start Time",
|
||||
operator: "<" as const,
|
||||
value: new Date(),
|
||||
type: "datetime" as const,
|
||||
};
|
||||
|
||||
const dbReadStream = new DatabaseReadStream<ObservationView>(
|
||||
ctx.prisma,
|
||||
queryBuilder,
|
||||
async (pageSize: number, offset: number) => {
|
||||
const dbReturn = await getAllGenerations({
|
||||
input: {
|
||||
...input,
|
||||
filter: [...input.filter, dateCutoffFilter],
|
||||
page: offset / pageSize,
|
||||
limit: pageSize,
|
||||
},
|
||||
selectIO: true,
|
||||
});
|
||||
return dbReturn.generations;
|
||||
},
|
||||
queryPageSize,
|
||||
);
|
||||
|
||||
|
||||
@@ -31,6 +31,23 @@ export const filterOptionsQuery = protectedProjectProcedure
|
||||
where: queryFilter,
|
||||
_count: { _all: true },
|
||||
});
|
||||
const promptNames = await ctx.prisma.$queryRaw<
|
||||
Array<{
|
||||
promptName: string | null;
|
||||
count: number;
|
||||
}>
|
||||
>(Prisma.sql`
|
||||
SELECT
|
||||
p.name "promptName",
|
||||
count(*)::int AS count
|
||||
FROM prompts p
|
||||
JOIN observations o ON o.prompt_id = p.id
|
||||
WHERE o.type = 'GENERATION'
|
||||
AND o.project_id = ${input.projectId}
|
||||
AND o.prompt_id IS NOT NULL
|
||||
AND p.project_id = ${input.projectId}
|
||||
GROUP BY 1
|
||||
`);
|
||||
const traceName = await ctx.prisma.$queryRaw<
|
||||
Array<{
|
||||
traceName: string | null;
|
||||
@@ -70,6 +87,12 @@ export const filterOptionsQuery = protectedProjectProcedure
|
||||
count: i.count,
|
||||
})),
|
||||
scores_avg: scores.map((score) => score.name),
|
||||
promptName: promptNames
|
||||
.filter((i) => i.promptName !== null)
|
||||
.map((i) => ({
|
||||
value: i.promptName as string,
|
||||
count: i.count,
|
||||
})),
|
||||
};
|
||||
|
||||
return res;
|
||||
|
||||
@@ -5,7 +5,7 @@ import { paginationZod } from "@/src/utils/zod";
|
||||
import { type ObservationView, Prisma } from "@prisma/client";
|
||||
|
||||
import { GenerationTableOptions } from "./utils/GenerationTableOptions";
|
||||
import { getAllGenerationsSqlQuery } from "@/src/server/api/routers/generations/db/getAllGenerationsSqlQuery";
|
||||
import { getAllGenerations } from "@/src/server/api/routers/generations/db/getAllGenerationsSqlQuery";
|
||||
|
||||
const getAllGenerationsInput = GenerationTableOptions.extend({
|
||||
...paginationZod,
|
||||
@@ -30,12 +30,8 @@ export type ObservationViewWithScores = ObservationView & {
|
||||
export const getAllQuery = protectedProjectProcedure
|
||||
.input(getAllGenerationsInput)
|
||||
.query(async ({ input, ctx }) => {
|
||||
const { queryBuilder, datetimeFilter, filterCondition, searchCondition } =
|
||||
getAllGenerationsSqlQuery({ input, type: "paginate" });
|
||||
|
||||
const query = queryBuilder(input.limit, input.page * input.limit);
|
||||
const generations =
|
||||
await ctx.prisma.$queryRaw<ObservationViewWithScores[]>(query);
|
||||
const { generations, datetimeFilter, filterCondition, searchCondition } =
|
||||
await getAllGenerations({ input, selectIO: false });
|
||||
|
||||
const totalGenerations = await ctx.prisma.$queryRaw<
|
||||
Array<{ count: bigint }>
|
||||
@@ -45,6 +41,7 @@ export const getAllQuery = protectedProjectProcedure
|
||||
count(*)
|
||||
FROM observations_view o
|
||||
JOIN traces t ON t.id = o.trace_id AND t.project_id = o.project_id
|
||||
LEFT JOIN prompts p ON p.id = o.prompt_id
|
||||
LEFT JOIN LATERAL (
|
||||
SELECT
|
||||
jsonb_object_agg(name::text, avg_value::double precision) AS "scores_avg"
|
||||
|
||||
@@ -72,8 +72,8 @@ export const scoresRouter = createTRPCRouter({
|
||||
input.projectId,
|
||||
filterCondition,
|
||||
Prisma.empty,
|
||||
input.limit,
|
||||
input.page,
|
||||
1, // limit
|
||||
0, // page
|
||||
),
|
||||
);
|
||||
|
||||
@@ -128,7 +128,7 @@ export const scoresRouter = createTRPCRouter({
|
||||
},
|
||||
}),
|
||||
),
|
||||
create: protectedProcedure
|
||||
createReviewScore: protectedProcedure
|
||||
.input(
|
||||
z.object({
|
||||
traceId: z.string(),
|
||||
@@ -176,6 +176,7 @@ export const scoresRouter = createTRPCRouter({
|
||||
value: input.value,
|
||||
name: input.name,
|
||||
comment: input.comment,
|
||||
source: "REVIEW",
|
||||
},
|
||||
});
|
||||
await auditLog({
|
||||
@@ -191,7 +192,7 @@ export const scoresRouter = createTRPCRouter({
|
||||
});
|
||||
return score;
|
||||
}),
|
||||
update: protectedProcedure
|
||||
updateReviewScore: protectedProcedure
|
||||
.input(
|
||||
z.object({
|
||||
id: z.string(),
|
||||
@@ -203,6 +204,7 @@ export const scoresRouter = createTRPCRouter({
|
||||
const score = await ctx.prisma.score.findFirstOrThrow({
|
||||
where: {
|
||||
id: input.id,
|
||||
source: "REVIEW",
|
||||
trace: {
|
||||
project: {
|
||||
members: {
|
||||
@@ -252,12 +254,13 @@ export const scoresRouter = createTRPCRouter({
|
||||
},
|
||||
});
|
||||
}),
|
||||
delete: protectedProcedure
|
||||
deleteReviewScore: protectedProcedure
|
||||
.input(z.string())
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
const score = await ctx.prisma.score.findFirstOrThrow({
|
||||
where: {
|
||||
id: input,
|
||||
source: "REVIEW",
|
||||
trace: {
|
||||
project: {
|
||||
members: {
|
||||
|
||||
@@ -96,12 +96,12 @@ export const sessionRouter = createTRPCRouter({
|
||||
t."userIds",
|
||||
t."countTraces",
|
||||
o."sessionDuration",
|
||||
o."totalCost",
|
||||
o."inputCost",
|
||||
o."outputCost",
|
||||
o."promptTokens",
|
||||
o."completionTokens",
|
||||
o."totalTokens",
|
||||
COALESCE(o."totalCost", 0) AS "totalCost",
|
||||
COALESCE(o."inputCost", 0) AS "inputCost",
|
||||
COALESCE(o."outputCost", 0) AS "outputCost",
|
||||
COALESCE(o."promptTokens", 0) AS "promptTokens",
|
||||
COALESCE(o."completionTokens", 0) AS "completionTokens",
|
||||
COALESCE(o."totalTokens", 0) AS "totalTokens",
|
||||
(count(*) OVER ())::int AS "totalCount"
|
||||
FROM trace_sessions s
|
||||
LEFT JOIN trace_metrics t ON t.session_id = s.id
|
||||
|
||||
@@ -5,7 +5,12 @@ import {
|
||||
protectedGetTraceProcedure,
|
||||
protectedProjectProcedure,
|
||||
} from "@/src/server/api/trpc";
|
||||
import { Prisma, type Trace, type ObservationView } from "@prisma/client";
|
||||
import {
|
||||
Prisma,
|
||||
type Trace,
|
||||
type ObservationView,
|
||||
type ObservationLevel,
|
||||
} from "@prisma/client";
|
||||
import { paginationZod } from "@/src/utils/zod";
|
||||
import { singleFilter } from "@/src/server/api/interfaces/filters";
|
||||
import {
|
||||
@@ -88,7 +93,8 @@ export const traceRouter = createTRPCRouter({
|
||||
tl.latency AS "latency",
|
||||
COALESCE(tm."calculatedTotalCost", 0)::numeric AS "calculatedTotalCost",
|
||||
COALESCE(tm."calculatedInputCost", 0)::numeric AS "calculatedInputCost",
|
||||
COALESCE(tm."calculatedOutputCost", 0)::numeric AS "calculatedOutputCost"
|
||||
COALESCE(tm."calculatedOutputCost", 0)::numeric AS "calculatedOutputCost",
|
||||
tm."level" AS "level"
|
||||
`,
|
||||
|
||||
input.projectId,
|
||||
@@ -111,6 +117,7 @@ export const traceRouter = createTRPCRouter({
|
||||
totalTokens: number;
|
||||
totalCount: number;
|
||||
latency: number | null;
|
||||
level: ObservationLevel;
|
||||
calculatedTotalCost: Decimal | null;
|
||||
calculatedInputCost: Decimal | null;
|
||||
calculatedOutputCost: Decimal | null;
|
||||
@@ -205,7 +212,6 @@ export const traceRouter = createTRPCRouter({
|
||||
byId: protectedGetTraceProcedure
|
||||
.input(z.object({ traceId: z.string() }))
|
||||
.query(async ({ input, ctx }) => {
|
||||
console.log("input", input);
|
||||
const trace = await ctx.prisma.trace.findFirstOrThrow({
|
||||
where: {
|
||||
id: input.traceId,
|
||||
@@ -479,7 +485,13 @@ function createTracesQuery(
|
||||
SUM(total_tokens) AS "totalTokens",
|
||||
SUM(calculated_total_cost) AS "calculatedTotalCost",
|
||||
SUM(calculated_input_cost) AS "calculatedInputCost",
|
||||
SUM(calculated_output_cost) AS "calculatedOutputCost"
|
||||
SUM(calculated_output_cost) AS "calculatedOutputCost",
|
||||
COALESCE(
|
||||
MAX(CASE WHEN level = 'ERROR' THEN 'ERROR' END),
|
||||
MAX(CASE WHEN level = 'WARNING' THEN 'WARNING' END),
|
||||
MAX(CASE WHEN level = 'DEFAULT' THEN 'DEFAULT' END),
|
||||
'DEBUG'
|
||||
) AS "level"
|
||||
FROM
|
||||
"observations_view"
|
||||
WHERE
|
||||
|
||||
@@ -133,8 +133,6 @@ export const userRouter = createTRPCRouter({
|
||||
1;
|
||||
`;
|
||||
|
||||
console.log(users);
|
||||
|
||||
if (users.length === 0) {
|
||||
return [];
|
||||
}
|
||||
|
||||
@@ -544,6 +544,7 @@ export class ScoreProcessor implements EventProcessor {
|
||||
timestamp: new Date(),
|
||||
value: body.value,
|
||||
name: body.name,
|
||||
source: "API",
|
||||
comment: body.comment,
|
||||
...(body.observationId && {
|
||||
observation: { connect: { id: body.observationId } },
|
||||
@@ -554,6 +555,7 @@ export class ScoreProcessor implements EventProcessor {
|
||||
value: body.value,
|
||||
name: body.name,
|
||||
comment: body.comment,
|
||||
source: "API",
|
||||
...(body.observationId && {
|
||||
observation: { connect: { id: body.observationId } },
|
||||
}),
|
||||
|
||||
@@ -103,7 +103,8 @@ if (env.AUTH_GOOGLE_CLIENT_ID && env.AUTH_GOOGLE_CLIENT_SECRET)
|
||||
GoogleProvider({
|
||||
clientId: env.AUTH_GOOGLE_CLIENT_ID,
|
||||
clientSecret: env.AUTH_GOOGLE_CLIENT_SECRET,
|
||||
allowDangerousEmailAccountLinking: true,
|
||||
allowDangerousEmailAccountLinking:
|
||||
env.AUTH_GOOGLE_ALLOW_ACCOUNT_LINKING === "true",
|
||||
}),
|
||||
);
|
||||
|
||||
@@ -112,7 +113,8 @@ if (env.AUTH_GITHUB_CLIENT_ID && env.AUTH_GITHUB_CLIENT_SECRET)
|
||||
GitHubProvider({
|
||||
clientId: env.AUTH_GITHUB_CLIENT_ID,
|
||||
clientSecret: env.AUTH_GITHUB_CLIENT_SECRET,
|
||||
allowDangerousEmailAccountLinking: true,
|
||||
allowDangerousEmailAccountLinking:
|
||||
env.AUTH_GITHUB_ALLOW_ACCOUNT_LINKING === "true",
|
||||
}),
|
||||
);
|
||||
|
||||
@@ -126,6 +128,8 @@ if (
|
||||
clientId: env.AUTH_AZURE_AD_CLIENT_ID,
|
||||
clientSecret: env.AUTH_AZURE_AD_CLIENT_SECRET,
|
||||
tenantId: env.AUTH_AZURE_AD_TENANT_ID,
|
||||
allowDangerousEmailAccountLinking:
|
||||
env.AUTH_AZURE_ALLOW_ACCOUNT_LINKING === "true",
|
||||
}),
|
||||
);
|
||||
|
||||
|
||||
+20
-11
@@ -1,6 +1,3 @@
|
||||
import { PrismaClient } from "@prisma/client";
|
||||
|
||||
import { env } from "@/src/env.mjs";
|
||||
import {
|
||||
DummyDriver,
|
||||
Kysely,
|
||||
@@ -10,17 +7,29 @@ import {
|
||||
} from "kysely";
|
||||
import { type DB as Database } from "@/prisma/generated/types";
|
||||
|
||||
const globalForPrisma = globalThis as unknown as {
|
||||
prisma: PrismaClient | undefined;
|
||||
import { PrismaClient } from "@prisma/client";
|
||||
import { env } from "@/src/env.mjs";
|
||||
|
||||
// Instantiated according to the Prisma documentation
|
||||
// https://www.prisma.io/docs/orm/more/help-and-troubleshooting/help-articles/nextjs-prisma-client-dev-practices
|
||||
|
||||
const prismaClientSingleton = () => {
|
||||
return new PrismaClient({
|
||||
log:
|
||||
env.NODE_ENV === "development"
|
||||
? ["query", "error", "warn"]
|
||||
: ["error", "warn"],
|
||||
});
|
||||
};
|
||||
|
||||
export const prisma =
|
||||
globalForPrisma.prisma ??
|
||||
new PrismaClient({
|
||||
log:
|
||||
env.NODE_ENV === "development" ? ["query", "error", "warn"] : ["error"],
|
||||
});
|
||||
declare global {
|
||||
// eslint-disable-next-line no-var
|
||||
var prisma: undefined | ReturnType<typeof prismaClientSingleton>;
|
||||
}
|
||||
|
||||
export const prisma = globalThis.prisma ?? prismaClientSingleton();
|
||||
|
||||
if (process.env.NODE_ENV !== "production") globalThis.prisma = prisma;
|
||||
export const DB = new Kysely<Database>({
|
||||
dialect: {
|
||||
createAdapter: () => new PostgresAdapter(),
|
||||
|
||||
@@ -29,3 +29,7 @@ export const usdFormatter = (
|
||||
maximumFractionDigits,
|
||||
}).format(number);
|
||||
};
|
||||
|
||||
export function randomIntFromInterval(min: number, max: number) {
|
||||
return Math.floor(Math.random() * (max - min + 1) + min);
|
||||
}
|
||||
|
||||
@@ -24,6 +24,11 @@ export const ObservationLevel = {
|
||||
ERROR: "ERROR"
|
||||
} as const;
|
||||
export type ObservationLevel = (typeof ObservationLevel)[keyof typeof ObservationLevel];
|
||||
export const ScoreSource = {
|
||||
API: "API",
|
||||
REVIEW: "REVIEW"
|
||||
} as const;
|
||||
export type ScoreSource = (typeof ScoreSource)[keyof typeof ScoreSource];
|
||||
export const PricingUnit = {
|
||||
PER_1000_TOKENS: "PER_1000_TOKENS",
|
||||
PER_1000_CHARS: "PER_1000_CHARS"
|
||||
@@ -259,6 +264,7 @@ export type Score = {
|
||||
timestamp: Generated<Timestamp>;
|
||||
name: string;
|
||||
value: number;
|
||||
source: ScoreSource;
|
||||
comment: string | null;
|
||||
trace_id: string;
|
||||
observation_id: string | null;
|
||||
|
||||
Generated
+63
-63
@@ -11,16 +11,16 @@
|
||||
"dependencies": {
|
||||
"@fastify/redis": "^6.1.1",
|
||||
"backoff": "^2.5.0",
|
||||
"fastify": "^4.26.1",
|
||||
"kysely": "^0.27.2",
|
||||
"fastify": "^4.26.2",
|
||||
"kysely": "^0.27.3",
|
||||
"node": "^21.6.2",
|
||||
"pg": "^8.11.3",
|
||||
"zod": "^3.22.4"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^20.11.19",
|
||||
"@types/pg": "^8.11.0",
|
||||
"@typescript-eslint/eslint-plugin": "^6.21.0",
|
||||
"@types/pg": "^8.11.2",
|
||||
"@typescript-eslint/eslint-plugin": "^7.1.1",
|
||||
"eslint": "^8.56.0",
|
||||
"eslint-config-prettier": "^9.1.0",
|
||||
"eslint-plugin-import": "^2.29.1",
|
||||
@@ -879,9 +879,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@types/pg": {
|
||||
"version": "8.11.0",
|
||||
"resolved": "https://registry.npmjs.org/@types/pg/-/pg-8.11.0.tgz",
|
||||
"integrity": "sha512-sDAlRiBNthGjNFfvt0k6mtotoVYVQ63pA8R4EMWka7crawSR60waVYR0HAgmPRs/e2YaeJTD/43OoZ3PFw80pw==",
|
||||
"version": "8.11.2",
|
||||
"resolved": "https://registry.npmjs.org/@types/pg/-/pg-8.11.2.tgz",
|
||||
"integrity": "sha512-G2Mjygf2jFMU/9hCaTYxJrwdObdcnuQde1gndooZSOHsNSaCehAuwc7EIuSA34Do8Jx2yZ19KtvW8P0j4EuUXw==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@types/node": "*",
|
||||
@@ -947,22 +947,22 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@types/semver": {
|
||||
"version": "7.5.7",
|
||||
"resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.5.7.tgz",
|
||||
"integrity": "sha512-/wdoPq1QqkSj9/QOeKkFquEuPzQbHTWAMPH/PaUMB+JuR31lXhlWXRZ52IpfDYVlDOUBvX09uBrPwxGT1hjNBg==",
|
||||
"version": "7.5.8",
|
||||
"resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.5.8.tgz",
|
||||
"integrity": "sha512-I8EUhyrgfLrcTkzV3TSsGyl1tSuPrEDzr0yd5m90UgNxQkyDXULk3b6MlQqTCpZpNtWe1K0hzclnZkTcLBe2UQ==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/@typescript-eslint/eslint-plugin": {
|
||||
"version": "6.21.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-6.21.0.tgz",
|
||||
"integrity": "sha512-oy9+hTPCUFpngkEZUSzbf9MxI65wbKFoQYsgPdILTfbUldp5ovUuphZVe4i30emU9M/kP+T64Di0mxl7dSw3MA==",
|
||||
"version": "7.1.1",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-7.1.1.tgz",
|
||||
"integrity": "sha512-zioDz623d0RHNhvx0eesUmGfIjzrk18nSBC8xewepKXbBvN/7c1qImV7Hg8TI1URTxKax7/zxfxj3Uph8Chcuw==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@eslint-community/regexpp": "^4.5.1",
|
||||
"@typescript-eslint/scope-manager": "6.21.0",
|
||||
"@typescript-eslint/type-utils": "6.21.0",
|
||||
"@typescript-eslint/utils": "6.21.0",
|
||||
"@typescript-eslint/visitor-keys": "6.21.0",
|
||||
"@typescript-eslint/scope-manager": "7.1.1",
|
||||
"@typescript-eslint/type-utils": "7.1.1",
|
||||
"@typescript-eslint/utils": "7.1.1",
|
||||
"@typescript-eslint/visitor-keys": "7.1.1",
|
||||
"debug": "^4.3.4",
|
||||
"graphemer": "^1.4.0",
|
||||
"ignore": "^5.2.4",
|
||||
@@ -978,8 +978,8 @@
|
||||
"url": "https://opencollective.com/typescript-eslint"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@typescript-eslint/parser": "^6.0.0 || ^6.0.0-alpha",
|
||||
"eslint": "^7.0.0 || ^8.0.0"
|
||||
"@typescript-eslint/parser": "^7.0.0",
|
||||
"eslint": "^8.56.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"typescript": {
|
||||
@@ -988,16 +988,16 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/parser": {
|
||||
"version": "6.21.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-6.21.0.tgz",
|
||||
"integrity": "sha512-tbsV1jPne5CkFQCgPBcDOt30ItF7aJoZL997JSF7MhGQqOeT3svWRYxiqlfA5RUdlHN6Fi+EI9bxqbdyAUZjYQ==",
|
||||
"version": "7.1.1",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-7.1.1.tgz",
|
||||
"integrity": "sha512-ZWUFyL0z04R1nAEgr9e79YtV5LbafdOtN7yapNbn1ansMyaegl2D4bL7vHoJ4HPSc4CaLwuCVas8CVuneKzplQ==",
|
||||
"dev": true,
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@typescript-eslint/scope-manager": "6.21.0",
|
||||
"@typescript-eslint/types": "6.21.0",
|
||||
"@typescript-eslint/typescript-estree": "6.21.0",
|
||||
"@typescript-eslint/visitor-keys": "6.21.0",
|
||||
"@typescript-eslint/scope-manager": "7.1.1",
|
||||
"@typescript-eslint/types": "7.1.1",
|
||||
"@typescript-eslint/typescript-estree": "7.1.1",
|
||||
"@typescript-eslint/visitor-keys": "7.1.1",
|
||||
"debug": "^4.3.4"
|
||||
},
|
||||
"engines": {
|
||||
@@ -1008,7 +1008,7 @@
|
||||
"url": "https://opencollective.com/typescript-eslint"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"eslint": "^7.0.0 || ^8.0.0"
|
||||
"eslint": "^8.56.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"typescript": {
|
||||
@@ -1017,13 +1017,13 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/scope-manager": {
|
||||
"version": "6.21.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-6.21.0.tgz",
|
||||
"integrity": "sha512-OwLUIWZJry80O99zvqXVEioyniJMa+d2GrqpUTqi5/v5D5rOrppJVBPa0yKCblcigC0/aYAzxxqQ1B+DS2RYsg==",
|
||||
"version": "7.1.1",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-7.1.1.tgz",
|
||||
"integrity": "sha512-cirZpA8bJMRb4WZ+rO6+mnOJrGFDd38WoXCEI57+CYBqta8Yc8aJym2i7vyqLL1vVYljgw0X27axkUXz32T8TA==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@typescript-eslint/types": "6.21.0",
|
||||
"@typescript-eslint/visitor-keys": "6.21.0"
|
||||
"@typescript-eslint/types": "7.1.1",
|
||||
"@typescript-eslint/visitor-keys": "7.1.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^16.0.0 || >=18.0.0"
|
||||
@@ -1034,13 +1034,13 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/type-utils": {
|
||||
"version": "6.21.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-6.21.0.tgz",
|
||||
"integrity": "sha512-rZQI7wHfao8qMX3Rd3xqeYSMCL3SoiSQLBATSiVKARdFGCYSRvmViieZjqc58jKgs8Y8i9YvVVhRbHSTA4VBag==",
|
||||
"version": "7.1.1",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-7.1.1.tgz",
|
||||
"integrity": "sha512-5r4RKze6XHEEhlZnJtR3GYeCh1IueUHdbrukV2KSlLXaTjuSfeVF8mZUVPLovidCuZfbVjfhi4c0DNSa/Rdg5g==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@typescript-eslint/typescript-estree": "6.21.0",
|
||||
"@typescript-eslint/utils": "6.21.0",
|
||||
"@typescript-eslint/typescript-estree": "7.1.1",
|
||||
"@typescript-eslint/utils": "7.1.1",
|
||||
"debug": "^4.3.4",
|
||||
"ts-api-utils": "^1.0.1"
|
||||
},
|
||||
@@ -1052,7 +1052,7 @@
|
||||
"url": "https://opencollective.com/typescript-eslint"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"eslint": "^7.0.0 || ^8.0.0"
|
||||
"eslint": "^8.56.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"typescript": {
|
||||
@@ -1061,9 +1061,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/types": {
|
||||
"version": "6.21.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-6.21.0.tgz",
|
||||
"integrity": "sha512-1kFmZ1rOm5epu9NZEZm1kckCDGj5UJEf7P1kliH4LKu/RkwpsfqqGmY2OOcUs18lSlQBKLDYBOGxRVtrMN5lpg==",
|
||||
"version": "7.1.1",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-7.1.1.tgz",
|
||||
"integrity": "sha512-KhewzrlRMrgeKm1U9bh2z5aoL4s7K3tK5DwHDn8MHv0yQfWFz/0ZR6trrIHHa5CsF83j/GgHqzdbzCXJ3crx0Q==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": "^16.0.0 || >=18.0.0"
|
||||
@@ -1074,13 +1074,13 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/typescript-estree": {
|
||||
"version": "6.21.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-6.21.0.tgz",
|
||||
"integrity": "sha512-6npJTkZcO+y2/kr+z0hc4HwNfrrP4kNYh57ek7yCNlrBjWQ1Y0OS7jiZTkgumrvkX5HkEKXFZkkdFNkaW2wmUQ==",
|
||||
"version": "7.1.1",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-7.1.1.tgz",
|
||||
"integrity": "sha512-9ZOncVSfr+sMXVxxca2OJOPagRwT0u/UHikM2Rd6L/aB+kL/QAuTnsv6MeXtjzCJYb8PzrXarypSGIPx3Jemxw==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@typescript-eslint/types": "6.21.0",
|
||||
"@typescript-eslint/visitor-keys": "6.21.0",
|
||||
"@typescript-eslint/types": "7.1.1",
|
||||
"@typescript-eslint/visitor-keys": "7.1.1",
|
||||
"debug": "^4.3.4",
|
||||
"globby": "^11.1.0",
|
||||
"is-glob": "^4.0.3",
|
||||
@@ -1126,17 +1126,17 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/utils": {
|
||||
"version": "6.21.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-6.21.0.tgz",
|
||||
"integrity": "sha512-NfWVaC8HP9T8cbKQxHcsJBY5YE1O33+jpMwN45qzWWaPDZgLIbo12toGMWnmhvCpd3sIxkpDw3Wv1B3dYrbDQQ==",
|
||||
"version": "7.1.1",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-7.1.1.tgz",
|
||||
"integrity": "sha512-thOXM89xA03xAE0lW7alstvnyoBUbBX38YtY+zAUcpRPcq9EIhXPuJ0YTv948MbzmKh6e1AUszn5cBFK49Umqg==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@eslint-community/eslint-utils": "^4.4.0",
|
||||
"@types/json-schema": "^7.0.12",
|
||||
"@types/semver": "^7.5.0",
|
||||
"@typescript-eslint/scope-manager": "6.21.0",
|
||||
"@typescript-eslint/types": "6.21.0",
|
||||
"@typescript-eslint/typescript-estree": "6.21.0",
|
||||
"@typescript-eslint/scope-manager": "7.1.1",
|
||||
"@typescript-eslint/types": "7.1.1",
|
||||
"@typescript-eslint/typescript-estree": "7.1.1",
|
||||
"semver": "^7.5.4"
|
||||
},
|
||||
"engines": {
|
||||
@@ -1147,16 +1147,16 @@
|
||||
"url": "https://opencollective.com/typescript-eslint"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"eslint": "^7.0.0 || ^8.0.0"
|
||||
"eslint": "^8.56.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/visitor-keys": {
|
||||
"version": "6.21.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-6.21.0.tgz",
|
||||
"integrity": "sha512-JJtkDduxLi9bivAB+cYOVMtbkqdPOhZ+ZI5LC47MIRrDV4Yn2o+ZnW10Nkmr28xRpSpdJ6Sm42Hjf2+REYXm0A==",
|
||||
"version": "7.1.1",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-7.1.1.tgz",
|
||||
"integrity": "sha512-yTdHDQxY7cSoCcAtiBzVzxleJhkGB9NncSIyMYe2+OGON1ZsP9zOPws/Pqgopa65jvknOjlk/w7ulPlZ78PiLQ==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@typescript-eslint/types": "6.21.0",
|
||||
"@typescript-eslint/types": "7.1.1",
|
||||
"eslint-visitor-keys": "^3.4.1"
|
||||
},
|
||||
"engines": {
|
||||
@@ -2737,9 +2737,9 @@
|
||||
"integrity": "sha512-eel5UKGn369gGEWOqBShmFJWfq/xSJvsgDzgLYC845GneayWvXBf0lJCBn5qTABfewy1ZDPoaR5OZCP+kssfuw=="
|
||||
},
|
||||
"node_modules/fastify": {
|
||||
"version": "4.26.1",
|
||||
"resolved": "https://registry.npmjs.org/fastify/-/fastify-4.26.1.tgz",
|
||||
"integrity": "sha512-tznA/G55dsxzM5XChBfcvVSloG2ejeeotfPPJSFaWmHyCDVGMpvf3nRNbsCb/JTBF9RmQFBfuujWt3Nphjesng==",
|
||||
"version": "4.26.2",
|
||||
"resolved": "https://registry.npmjs.org/fastify/-/fastify-4.26.2.tgz",
|
||||
"integrity": "sha512-90pjTuPGrfVKtdpLeLzND5nyC4woXZN5VadiNQCicj/iJU4viNHKhsAnb7jmv1vu2IzkLXyBiCzdWuzeXgQ5Ug==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
@@ -3756,9 +3756,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/kysely": {
|
||||
"version": "0.27.2",
|
||||
"resolved": "https://registry.npmjs.org/kysely/-/kysely-0.27.2.tgz",
|
||||
"integrity": "sha512-DmRvEfiR/NLpgsTbSxma2ldekhsdcd65+MNiKXyd/qj7w7X5e3cLkXxcj+MypsRDjPhHQ/CD5u3Eq1sBYzX0bw==",
|
||||
"version": "0.27.3",
|
||||
"resolved": "https://registry.npmjs.org/kysely/-/kysely-0.27.3.tgz",
|
||||
"integrity": "sha512-lG03Ru+XyOJFsjH3OMY6R/9U38IjDPfnOfDgO3ynhbDr+Dz8fak+X6L62vqu3iybQnj+lG84OttBuU9KY3L9kA==",
|
||||
"engines": {
|
||||
"node": ">=14.0.0"
|
||||
}
|
||||
|
||||
+4
-4
@@ -18,16 +18,16 @@
|
||||
"dependencies": {
|
||||
"@fastify/redis": "^6.1.1",
|
||||
"backoff": "^2.5.0",
|
||||
"fastify": "^4.26.1",
|
||||
"kysely": "^0.27.2",
|
||||
"fastify": "^4.26.2",
|
||||
"kysely": "^0.27.3",
|
||||
"node": "^21.6.2",
|
||||
"pg": "^8.11.3",
|
||||
"zod": "^3.22.4"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^20.11.19",
|
||||
"@types/pg": "^8.11.0",
|
||||
"@typescript-eslint/eslint-plugin": "^6.21.0",
|
||||
"@types/pg": "^8.11.2",
|
||||
"@typescript-eslint/eslint-plugin": "^7.1.1",
|
||||
"eslint": "^8.56.0",
|
||||
"eslint-config-prettier": "^9.1.0",
|
||||
"eslint-plugin-import": "^2.29.1",
|
||||
|
||||
Reference in New Issue
Block a user