Compare commits

...
41 Commits
Author SHA1 Message Date
Marc Klingen af789fb3ac chore: release v2.11.2
CI/CD / tests (18) (push) Failing after 58s
CI/CD / tests (20) (push) Failing after 49s
release.yml / release (push) Failing after 2s
CI/CD / e2e-tests (push) Failing after 47s
CI/CD / test-docker-build (push) Successful in 4m8s
CI/CD / all-ci-passed (push) Canceled after 0s
CI/CD / push-docker-image (push) Canceled after 0s
CI/CD / lint (push) Failing after 14m43s
2024-03-16 19:43:51 +01:00
Marc Klingen 7fcf9e98ae chore: add LANGFUSE_ENABLE_EXPERIMENTAL_FEATURES to env prod example 2024-03-15 16:21:44 +01:00
Marc Klingen 45c7a95a9c chore(ui): always display hostname 2024-03-14 19:33:10 +01:00
Marc KlingenandGitHub 75009cf83d feat(sso): toggle account linking via AUTH_GOOGLE_ALLOW_ACCOUNT_LINKING and AUTH_GITHUB_ALLOW_ACCOUNT_LINKING (#1416) 2024-03-14 17:02:14 +00:00
Max Deichmann 70642da8ab chore: release v2.11.1
CI/CD / push-docker-image (push) Blocked by required conditions
CI/CD / tests (18) (push) Failing after 1m6s
CI/CD / e2e-tests (push) Failing after 43s
release.yml / release (push) Failing after 2s
CI/CD / tests (20) (push) Failing after 55s
CI/CD / test-docker-build (push) Successful in 2m55s
CI/CD / lint (push) Successful in 4m23s
CI/CD / all-ci-passed (push) Waiting to run
2024-03-14 10:41:53 +01:00
Max DeichmannandGitHub 6754733aac feat: add haiku model (#1414)
push
2024-03-14 09:41:07 +00:00
Richard KrümmelandGitHub e45bd75afa feat(traces): add level to table (#1359) 2024-03-13 16:39:01 +01:00
Marc KlingenandGitHub 9b271f0b78 chore: no default score source in db (#1403) 2024-03-12 20:02:26 +00:00
Marc KlingenandGitHub e98565ee9c feat: add score source (#1365) 2024-03-12 19:27:51 +01:00
Marc KlingenandGitHub 62d1f31527 docs: update readme 2024-03-12 18:25:59 +01:00
Max DeichmannandGitHub c1190d70da refactor: add nuke command (#1400) 2024-03-12 17:00:42 +00:00
Max DeichmannandGitHub bf9badb711 fix: Add additional projectid condition (#1393) 2024-03-12 08:20:48 +00:00
8155749b2b feat(prompts): extend prompts table (#1346)
Co-authored-by: Max Deichmann <m.deichmann@tum.de>
2024-03-12 09:15:14 +01:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
84c0ba6487 chore(deps): bump the patches group in /worker with 3 updates (#1390)
Bumps the patches group in /worker with 3 updates: [fastify](https://github.com/fastify/fastify), [kysely](https://github.com/kysely-org/kysely) and [@types/pg](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/pg).


Updates `fastify` from 4.26.1 to 4.26.2
- [Release notes](https://github.com/fastify/fastify/releases)
- [Commits](https://github.com/fastify/fastify/compare/v4.26.1...v4.26.2)

Updates `kysely` from 0.27.2 to 0.27.3
- [Release notes](https://github.com/kysely-org/kysely/releases)
- [Commits](https://github.com/kysely-org/kysely/compare/0.27.2...0.27.3)

Updates `@types/pg` from 8.11.0 to 8.11.2
- [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases)
- [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/pg)

---
updated-dependencies:
- dependency-name: fastify
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: patches
- dependency-name: kysely
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: patches
- dependency-name: "@types/pg"
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: patches
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-03-12 01:40:29 +01:00
Marc Klingen 6320f48737 chore: release v2.11.0
CI/CD / push-docker-image (push) Blocked by required conditions
CI/CD / tests (18) (push) Failing after 51s
CI/CD / tests (20) (push) Failing after 47s
CI/CD / e2e-tests (push) Failing after 47s
CI/CD / test-docker-build (push) Successful in 2m54s
release.yml / release (push) Failing after 14s
CI/CD / lint (push) Successful in 4m35s
CI/CD / all-ci-passed (push) Waiting to run
2024-03-11 22:44:39 +01:00
Marc KlingenandGitHub fe053405ea feat(ui): extend ChatML message rendering to additional json for tools/functions (#1388)
* feat(ui): extend ChatML message rendering to additional json for tools/functions

* fix typing
2024-03-11 21:29:26 +00:00
Marc KlingenandGitHub 63d1abffb2 fix(ui): score table pagination (#1387) 2024-03-11 20:15:25 +00:00
Marc Klingen 3050ed756f chore: release v2.10.10
CI/CD / push-docker-image (push) Blocked by required conditions
CI/CD / tests (20) (push) Failing after 46s
CI/CD / tests (18) (push) Failing after 58s
CI/CD / e2e-tests (push) Failing after 44s
release.yml / release (push) Failing after 2s
CI/CD / lint (push) Successful in 4m23s
CI/CD / test-docker-build (push) Successful in 7m14s
CI/CD / all-ci-passed (push) Waiting to run
2024-03-11 20:01:01 +01:00
Marc Klingen 6eebf0b49d docs: update readme 2024-03-11 19:58:16 +01:00
Marc KlingenandGitHub bbc57779c3 feat(sso): add env AUTH_AZURE_ALLOW_ACCOUNT_LINKING to trust azuread emails (#1386) 2024-03-11 18:55:10 +00:00
Max Deichmann ccc9161967 chore: release v2.10.9
CI/CD / push-docker-image (push) Blocked by required conditions
CI/CD / e2e-tests (push) Failing after 47s
CI/CD / tests (20) (push) Failing after 1m59s
CI/CD / lint (push) Successful in 4m17s
CI/CD / test-docker-build (push) Successful in 3m51s
release.yml / release (push) Failing after 3s
CI/CD / tests (18) (push) Failing after 11m33s
CI/CD / all-ci-passed (push) Waiting to run
2024-03-11 16:55:39 +01:00
Max DeichmannandGitHub 6df7a2c716 fix: users table pagination (#1383) 2024-03-11 15:51:36 +00:00
Marc KlingenandGitHub 88e4f210d0 feat: add llamaindex copyable snippet to api popup and fix scroll behavior (#1381) 2024-03-09 02:04:39 +00:00
Max Deichmann 011c1452a0 chore: release v2.10.8
CI/CD / push-docker-image (push) Blocked by required conditions
CI/CD / tests (18) (push) Failing after 53s
CI/CD / tests (20) (push) Failing after 49s
CI/CD / e2e-tests (push) Failing after 50s
CI/CD / test-docker-build (push) Successful in 3m9s
release.yml / release (push) Failing after 3s
CI/CD / lint (push) Successful in 4m35s
CI/CD / all-ci-passed (push) Waiting to run
2024-03-08 10:59:06 +01:00
Max DeichmannandGitHub 4a3025dbae fix: fix generations table sorting (#1370) 2024-03-08 09:58:20 +00:00
Marc Klingen 367e8c8a61 chore: release v2.10.7
CI/CD / push-docker-image (push) Blocked by required conditions
CI/CD / tests (18) (push) Failing after 1m6s
CI/CD / tests (20) (push) Failing after 55s
CI/CD / e2e-tests (push) Failing after 56s
CI/CD / test-docker-build (push) Successful in 3m12s
release.yml / release (push) Failing after 3s
CI/CD / lint (push) Successful in 4m14s
CI/CD / all-ci-passed (push) Waiting to run
2024-03-08 00:27:14 +01:00
Max DeichmannandGitHub 84edae6468 refactor: rename dashboard class (#1366) 2024-03-07 19:03:10 +00:00
Max DeichmannandGitHub 87e04a3482 feat: load generations table i/o individually (#1364) 2024-03-07 18:26:41 +00:00
Max DeichmannandGitHub 90abf7fb34 feat: add claude 3 models (#1354) 2024-03-07 17:53:51 +00:00
Marc KlingenandGitHub 027c3791a0 fix: render records type content as chat message (#1362)
fix: also render records `content` as openai chatmessage
2024-03-07 15:20:27 +00:00
4023e7fd4a feat: add cost to dataset run table (#1344)
---------

Co-authored-by: Marc Klingen <git@marcklingen.com>
2024-03-07 15:30:48 +01:00
Marc Klingen 3d178caa17 chore: release v2.10.6
CI/CD / push-docker-image (push) Blocked by required conditions
CI/CD / tests (18) (push) Failing after 58s
CI/CD / tests (20) (push) Failing after 50s
CI/CD / e2e-tests (push) Failing after 48s
CI/CD / test-docker-build (push) Successful in 3m31s
CI/CD / lint (push) Successful in 4m13s
CI/CD / all-ci-passed (push) Waiting to run
release.yml / release (push) Failing after 3s
2024-03-07 15:03:31 +01:00
Hassieb PakzadandGitHub 128b03bec0 fix: allow null values returned for cost values in sessions table (#1360)
fix: allow null values returned from sessions table
2024-03-07 15:02:29 +01:00
Max Deichmann 73889fcc96 chore: release v2.10.5
CI/CD / push-docker-image (push) Blocked by required conditions
CI/CD / tests (20) (push) Failing after 44s
CI/CD / tests (18) (push) Failing after 1m23s
release.yml / release (push) Failing after 3s
CI/CD / e2e-tests (push) Failing after 43s
CI/CD / test-docker-build (push) Successful in 3m43s
CI/CD / lint (push) Successful in 4m21s
CI/CD / all-ci-passed (push) Waiting to run
2024-03-07 10:02:28 +01:00
Max DeichmannandGitHub d88a295f8d fix: only sort supported columns (#1356) 2024-03-07 09:46:46 +01:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>Max Deichmann
15c5fdc2a9 chore(deps-dev): bump @typescript-eslint/eslint-plugin from 6.21.0 to 7.1.1 in /worker (#1333)
chore(deps-dev): bump @typescript-eslint/eslint-plugin in /worker

Bumps [@typescript-eslint/eslint-plugin](https://github.com/typescript-eslint/typescript-eslint/tree/HEAD/packages/eslint-plugin) from 6.21.0 to 7.1.1.
- [Release notes](https://github.com/typescript-eslint/typescript-eslint/releases)
- [Changelog](https://github.com/typescript-eslint/typescript-eslint/blob/main/packages/eslint-plugin/CHANGELOG.md)
- [Commits](https://github.com/typescript-eslint/typescript-eslint/commits/v7.1.1/packages/eslint-plugin)

---
updated-dependencies:
- dependency-name: "@typescript-eslint/eslint-plugin"
  dependency-type: direct:development
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Max Deichmann <m.deichmann@tum.de>
2024-03-07 08:02:13 +00:00
Max Deichmann ca9c0e07d1 chore: release v2.10.4
CI/CD / push-docker-image (push) Blocked by required conditions
CI/CD / tests (18) (push) Failing after 54s
CI/CD / tests (20) (push) Failing after 51s
release.yml / release (push) Failing after 2s
CI/CD / e2e-tests (push) Failing after 48s
CI/CD / lint (push) Successful in 4m41s
CI/CD / test-docker-build (push) Successful in 5m44s
CI/CD / all-ci-passed (push) Waiting to run
2024-03-07 08:56:32 +01:00
Max DeichmannandGitHub bf0eab424f fix: fix correct reuse of db connections (#1348) 2024-03-07 08:55:38 +01:00
Marc Klingen 89d38ff6ad chore: release v2.10.3
CI/CD / push-docker-image (push) Blocked by required conditions
CI/CD / tests (18) (push) Failing after 58s
CI/CD / tests (20) (push) Failing after 49s
CI/CD / e2e-tests (push) Failing after 48s
CI/CD / test-docker-build (push) Successful in 2m53s
release.yml / release (push) Failing after 3s
CI/CD / lint (push) Successful in 4m51s
CI/CD / all-ci-passed (push) Waiting to run
2024-03-07 00:52:54 +01:00
Marc KlingenandGitHub 8d7b334521 refactor: remove separate identify() to reduce number of posthog events (#1263) 2024-03-06 23:27:21 +00:00
Marc KlingenandGitHub a374a378a0 fix: dashboard state management (#1349) 2024-03-06 23:23:11 +00:00
58 changed files with 865 additions and 409 deletions
+4 -1
View File
@@ -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.
+8 -16
View File
@@ -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`.
+5
View File
@@ -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:
+1
View File
@@ -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": "",
+9
View File
@@ -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 '{}' \;
+9
View File
@@ -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
+2 -2
View File
@@ -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
View File
@@ -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",
+6
View File
@@ -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)
+7
View File
@@ -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
View File
@@ -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"),
+3
View File
@@ -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,
});
-9
View File
@@ -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;
},
},
{
+25 -1
View File
@@ -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",
+81 -43
View File
@@ -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
View File
@@ -1 +1 @@
export const VERSION = "v2.10.2";
export const VERSION = "v2.11.2";
+9
View File
@@ -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;
+6 -9
View File
@@ -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,
},
},
});
+24 -18
View File
@@ -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"
+1 -1
View File
@@ -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"
+8 -5
View File
@@ -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: {
+6 -6
View File
@@ -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
+16 -4
View File
@@ -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
-2
View File
@@ -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 } },
}),
+6 -2
View File
@@ -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
View File
@@ -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(),
+4
View File
@@ -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);
}
+6
View File
@@ -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;
+63 -63
View File
@@ -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
View File
@@ -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",