Compare commits

...
47 Commits
Author SHA1 Message Date
Hassieb Pakzad cf42dd726d chore: release v2.49.2
CI/CD / lint (push) Waiting to run
CI/CD / test-docker-build (push) Waiting to run
CI/CD / tests-web (20) (push) Waiting to run
CI/CD / tests-worker (20) (push) Waiting to run
CI/CD / e2e-tests (push) Waiting to run
CI/CD / all-ci-passed (push) Blocked by required conditions
CI/CD / push-docker-image (push) Blocked by required conditions
release.yml / release (push) Waiting to run
2024-06-10 16:22:01 +02:00
Hassieb PakzadandGitHub 8aeec63c07 perf: use raw query for trace duration on dataset run table (#2286) 2024-06-10 16:19:31 +02:00
Hassieb Pakzad 0cd3fcfbeb chore: release v2.49.1
CI/CD / lint (push) Waiting to run
CI/CD / test-docker-build (push) Waiting to run
CI/CD / tests-web (20) (push) Waiting to run
CI/CD / tests-worker (20) (push) Waiting to run
CI/CD / e2e-tests (push) Waiting to run
CI/CD / all-ci-passed (push) Blocked by required conditions
CI/CD / push-docker-image (push) Blocked by required conditions
release.yml / release (push) Waiting to run
2024-06-10 12:57:14 +02:00
Hassieb PakzadandGitHub a880bdb3b2 perf: boost observationCounts on prompts table (#2284) 2024-06-10 12:48:39 +02:00
Max DeichmannandGitHub 5c6d3e14f2 refactor: add tokenisation to worker (#2281) 2024-06-09 11:48:21 +00:00
Max DeichmannandGitHub 6ccc57312c fix: shared dependencies and improve docker ci (#2273) 2024-06-07 18:45:05 +02:00
Max DeichmannandGitHub c4831d1b25 fix: add lodash to worker (#2270) 2024-06-07 15:45:15 +02:00
Marc Klingen cc08408f0a fix(ui): show title when trying to deleting built-in models 2024-06-07 02:57:18 +02:00
6a41aadc98 fix(ui): handle nested menu states in sidebar (#2230)
* chore: update MainNavigation component to use nested navigation literals

* simplify

---------

Co-authored-by: Marc Klingen <git@marcklingen.com>
2024-06-06 22:42:51 +00:00
9d6efbae19 fix(ui): improve ux across delete buttons (#2258)
* improve ux across delete buttons

* updated delete button on models screen

* trpc errors are logged to console by default

* revert to pointer-events-none on disabled button

---------

Co-authored-by: Mish Ushakov <mishushakov@users.noreply.github.com>
Co-authored-by: Marc Klingen <git@marcklingen.com>
2024-06-07 00:26:04 +02:00
Max DeichmannandGitHub 74be1e87c1 refactor: move ingestion types to shared (#2264)
push
2024-06-06 17:56:35 +00:00
Hassieb Pakzad dd823da1da chore: release v2.49.0
CI/CD / lint (push) Waiting to run
CI/CD / test-docker-build (push) Waiting to run
CI/CD / tests-web (20) (push) Waiting to run
CI/CD / tests-worker (20) (push) Waiting to run
CI/CD / e2e-tests (push) Waiting to run
CI/CD / all-ci-passed (push) Blocked by required conditions
CI/CD / push-docker-image (push) Blocked by required conditions
release.yml / release (push) Waiting to run
2024-06-06 17:52:18 +02:00
992f0affbc chore: remove trace fkey constraint from datasetrunitems (#2262)
Co-authored-by: Marc Klingen <git@marcklingen.com>
2024-06-06 17:49:17 +02:00
Mish UshakovandGitHub b8c192324c docs: Updated contributing guide (#2259) 2024-06-06 14:28:57 +02:00
Marc KlingenandGitHub 1416e760ff chore: remove dangling worker/generated/types.ts (#2257) 2024-06-06 14:11:01 +02:00
Marc KlingenandGitHub 4266b46dd9 chore(db): drop legacy pricings table, replaced by models (#2256) 2024-06-06 11:36:08 +02:00
Marc KlingenandGitHub 1933f8f201 feat: add token/character count support for gemini-1.5-flash and gemini-1.5-pro (#2255) 2024-06-06 11:21:42 +02:00
Marc KlingenandGitHub 1e35eb3a66 fix(ui): encode prompt name on link from traces (#2253) 2024-06-06 08:30:52 +00:00
Marc KlingenandGitHub 5478c5e317 feat(ui): use dropdownmenu for version modal (#2252) 2024-06-06 08:20:01 +00:00
Marc KlingenandGitHub 140452c89e feat(ui): add upgrade popover to version number (#2251) 2024-06-06 00:48:23 +00:00
Marc KlingenandGitHub 7ac92e83da feat(ui): use pagination instead of virtualization on sessions view (#2248) 2024-06-05 22:21:01 +00:00
Marc Klingen 8a65bad7d4 chore: release v2.48.1
CI/CD / lint (push) Waiting to run
CI/CD / test-docker-build (push) Waiting to run
CI/CD / tests-web (20) (push) Waiting to run
CI/CD / tests-worker (20) (push) Waiting to run
CI/CD / e2e-tests (push) Waiting to run
CI/CD / all-ci-passed (push) Blocked by required conditions
CI/CD / push-docker-image (push) Blocked by required conditions
release.yml / release (push) Waiting to run
2024-06-05 23:04:25 +02:00
Marc Klingen 6c6348bfbe chore(rbac): grant new scoreConfigs:CUD scope to MEMBER role 2024-06-05 23:02:42 +02:00
Marlies Mayerhofer 9ff7fc942d fix: add maintainers heading in contributing file 2024-06-05 22:53:30 +02:00
Marlies Mayerhofer a3f59e7e95 feat: enhance robustness of multi select item search 2024-06-05 22:46:48 +02:00
Max DeichmannandGitHub 445bdcb65c chore: log level worker (#2246)
* push

* push
2024-06-05 20:34:12 +02:00
Max DeichmannandGitHub 8230aa79b3 chore: remove comments (#2245) 2024-06-05 20:11:10 +02:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
57ca7fb478 chore(deps-dev): bump @typescript-eslint/parser from 7.7.0 to 7.12.0 (#2222)
Bumps [@typescript-eslint/parser](https://github.com/typescript-eslint/typescript-eslint/tree/HEAD/packages/parser) from 7.7.0 to 7.12.0.
- [Release notes](https://github.com/typescript-eslint/typescript-eslint/releases)
- [Changelog](https://github.com/typescript-eslint/typescript-eslint/blob/main/packages/parser/CHANGELOG.md)
- [Commits](https://github.com/typescript-eslint/typescript-eslint/commits/v7.12.0/packages/parser)

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-06-05 20:05:17 +02:00
marliessophieandGitHub 2db88101e1 feat: add score and score config user tracking events (#2240)
* feat: add score and score config user tracking events

* add: icon for annotation button
2024-06-05 13:12:22 +00:00
Hassieb PakzadandGitHub 669167002a fix: parse object type message content (#2239) 2024-06-05 12:48:30 +00:00
marliessophieandGitHub 2f6ade9354 fix(annotation): improve keyboard navigation experience in drawer (#2236) 2024-06-05 13:41:04 +02:00
Marc Klingen 86f44df5dc chore: release v2.48.0
CI/CD / lint (push) Waiting to run
CI/CD / test-docker-build (push) Waiting to run
CI/CD / tests-web (20) (push) Waiting to run
CI/CD / tests-worker (20) (push) Waiting to run
CI/CD / e2e-tests (push) Waiting to run
CI/CD / all-ci-passed (push) Blocked by required conditions
CI/CD / push-docker-image (push) Blocked by required conditions
release.yml / release (push) Waiting to run
2024-06-05 00:32:21 +02:00
marliessophieandGitHub 34da790d5e chore(scores): backfill manual scores with config_id (#2233) 2024-06-05 00:25:10 +02:00
c16d86f7e5 feat(scores): add annotation slider and score config settings for categorical annotation (#2157)
---------

Co-authored-by: Marc Klingen <git@marcklingen.com>
2024-06-05 00:06:05 +02:00
Marc KlingenandGitHub 1b2b8cc43a ci: fix flaky docker test build step by pinning turbo v1 (#2234) 2024-06-04 23:48:00 +02:00
Marc Klingen 3903790086 docs(api): refer to GET /traces endpoint to fetch large sessions with pagination 2024-06-04 20:22:23 +02:00
6da1280c43 perf(user-table): add timestamp filter to user table (#2231)
* feat: add timestamp filter to user table

* users table definition instead of reusing traces

* add filter to total as well to make table state consistent

---------

Co-authored-by: Marc Klingen <git@marcklingen.com>
2024-06-04 19:08:00 +02:00
Marc Klingen 85343bf26c chore: release v2.47.7
CI/CD / lint (push) Waiting to run
CI/CD / test-docker-build (push) Waiting to run
CI/CD / tests-web (20) (push) Waiting to run
CI/CD / tests-worker (20) (push) Waiting to run
CI/CD / e2e-tests (push) Waiting to run
CI/CD / all-ci-passed (push) Blocked by required conditions
CI/CD / push-docker-image (push) Blocked by required conditions
release.yml / release (push) Waiting to run
2024-06-04 16:56:48 +02:00
Richard KrümmelandGitHub e3b4dc1928 feat(api): add sessionId filter for traces endpoint (#2227) 2024-06-04 14:38:51 +00:00
Marc KlingenandGitHub a4939bacd8 perf: add index on scores.name (#2228) 2024-06-04 15:35:21 +02:00
Hassieb Pakzad 4441942cf5 chore: release v2.47.6
CI/CD / lint (push) Waiting to run
CI/CD / test-docker-build (push) Waiting to run
CI/CD / tests-web (20) (push) Waiting to run
CI/CD / tests-worker (20) (push) Waiting to run
CI/CD / e2e-tests (push) Waiting to run
CI/CD / all-ci-passed (push) Blocked by required conditions
CI/CD / push-docker-image (push) Blocked by required conditions
release.yml / release (push) Waiting to run
2024-06-04 14:02:23 +02:00
Hassieb PakzadandGitHub 6597b02c09 perf: boost sessions table performance (#2225) 2024-06-04 13:42:09 +02:00
Marc KlingenandGitHub 0df3ad27dc perf: add index on dataset_items.source_trace_id (#2220) 2024-06-04 01:57:19 +02:00
Marc KlingenandGitHub 696d458a27 fix(cloud): handle posthog settings rbac correctly in frontend (#2223) 2024-06-03 23:35:49 +00:00
Marc KlingenandGitHub fd18ab605d feat(api): add count and cost to daily metrics api (#2219) 2024-06-03 22:41:01 +02:00
Marc Klingen d47d14eecd chore: release v2.47.5
CI/CD / lint (push) Waiting to run
CI/CD / test-docker-build (push) Waiting to run
CI/CD / tests-web (20) (push) Waiting to run
CI/CD / tests-worker (20) (push) Waiting to run
CI/CD / e2e-tests (push) Waiting to run
CI/CD / all-ci-passed (push) Blocked by required conditions
CI/CD / push-docker-image (push) Blocked by required conditions
release.yml / release (push) Waiting to run
2024-06-03 19:56:18 +02:00
Richard KrümmelandGitHub 24a81e1589 fix: update validation schema for prompt tags (#2217) 2024-06-03 18:51:39 +02:00
124 changed files with 3832 additions and 1819 deletions
+20 -12
View File
@@ -40,19 +40,27 @@ jobs:
- name: Checkout
uses: actions/checkout@v3
- name: Build Web Docker image
uses: docker/build-push-action@v4
with:
context: .
file: ./web/Dockerfile
push: false
- name: Build and run both images from compose
run: |
docker compose -f docker-compose.build.yml up -d
sleep 5 # Wait for PostgreSQL to accept connections
- name: Build Worker Docker image
uses: docker/build-push-action@v4
with:
context: .
file: ./worker/Dockerfile
push: false
- name: Check server health
run: |
timeout 10 bash -c 'until curl -f http://localhost:3000/api/public/health; do sleep 2; done'
- name: Check worker health
run: |
timeout 10 bash -c 'until curl -f http://localhost:3030/api/health; do sleep 2; done'
- name: Ensure no unhealthy status
run: |
if docker-compose ps | grep "(unhealthy)"; then
echo "One or more services are unhealthy"
exit 1
else
echo "All services are healthy"
fi
tests-web:
runs-on: ubuntu-latest
+13 -1
View File
@@ -191,6 +191,16 @@ Requirements
pnpm run dev
```
7. Open the web app in the browser:
http://localhost:3000
8. Log in as a test user (after you ran `db:seed` command):
Username: demo@langfuse.com
Password: password
## Monorepo quickstart
- Available packages and their dependencies
@@ -358,7 +368,9 @@ The background color of the following component will be `hsl(var(--primary))` an
By following these guidelines, you can ensure that any contributions to our theme are consistent, maintainable, and aligned with our design system.
## Using secrets stored in 1Password
## Maintainers
### Using secrets stored in 1Password
When applying changes to non-local environments, you may need to use secrets stored in 1Password. We use the 1Password CLI for this purpose.
+10
View File
@@ -20,6 +20,11 @@ services:
- LANGFUSE_WORKER_HOST=${LANGFUSE_WORKER_HOST:-worker}
- LANGFUSE_WORKER_PASSWORD=${LANGFUSE_WORKER_PASSWORD:-mybasicauthsecret}
restart: always
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:3000/api/public/health"]
interval: 30s
timeout: 10s
retries: 3
worker:
build:
@@ -41,6 +46,11 @@ services:
- REDIS_AUTH=${REDIS_AUTH:-myredissecret}
- LANGFUSE_WORKER_PASSWORD=${LANGFUSE_WORKER_PASSWORD:-mybasicauthsecret}
restart: always
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:3030/api/health"]
interval: 30s
timeout: 10s
retries: 3
redis:
image: redis:7.2.4
+1 -1
View File
@@ -33,7 +33,7 @@
"@repo/eslint-config": "*",
"@repo/typescript-config": "*",
"@types/node": "^20.11.29",
"@typescript-eslint/parser": "^7.7.0",
"@typescript-eslint/parser": "^7.12.0",
"eslint": "^8.57.0",
"eslint-config-prettier": "^9.1.0",
"eslint-config-standard": "^17.1.0",
+17 -4
View File
@@ -48,12 +48,25 @@ types:
date: date
countTraces: integer
countObservations: integer
totalCost: double
totalCost:
type: double
docs: Total model cost in USD
usage: list<UsageByModel>
UsageByModel:
docs: Daily usage of a given model. Usage corresponds to the unit set for the specific model (e.g. tokens).
properties:
model: optional<string>
inputUsage: integer
outputUsage: integer
totalUsage: integer
inputUsage:
type: integer
docs: Total number of generation input units (e.g. tokens)
outputUsage:
type: integer
docs: Total number of generation output units (e.g. tokens)
totalUsage:
type: integer
docs: Total number of generation total units (e.g. tokens)
countTraces: integer
countObservations: integer
totalCost:
type: double
docs: Total model cost in USD
+1 -1
View File
@@ -7,7 +7,7 @@ service:
base-path: /api/public
endpoints:
get:
docs: Get a session
docs: Get a session. Please note that `traces` on this endpoint are not paginated, if you plan to fetch large sessions, consider `GET /api/public/traces?sessionId=<sessionId>`
method: GET
path: /sessions/{sessionId}
path-parameters:
+1
View File
@@ -30,6 +30,7 @@ service:
docs: Limit of items per page. If you encounter api issues due to too large page sizes, try to reduce the limit.
userId: optional<string>
name: optional<string>
sessionId: optional<string>
fromTimestamp:
type: optional<datetime>
docs: Retrieve only traces newer than this datetime (ISO 8601).
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "langfuse",
"version": "2.47.4",
"version": "2.49.2",
"author": "engineering@langfuse.com",
"license": "MIT",
"private": true,
+1 -1
View File
@@ -9,7 +9,7 @@
],
"devDependencies": {
"@typescript-eslint/eslint-plugin": "^7.1.0",
"@typescript-eslint/parser": "^7.7.0",
"@typescript-eslint/parser": "^7.12.0",
"@vercel/style-guide": "^6.0.0",
"eslint-config-next": "^14.2.3",
"eslint-config-prettier": "^9.1.0",
+2 -1
View File
@@ -52,6 +52,7 @@
"bcryptjs": "^2.4.3",
"kysely": "^0.27.3",
"langchain": "^0.1.36",
"lodash": "^4.17.21",
"prisma-extension-kysely": "^2.1.0",
"zod": "^3.22.4",
"zod-to-json-schema": "^3.22.5"
@@ -63,7 +64,7 @@
"@types/node": "^20.11.29",
"@types/pg": "^8.11.6",
"@types/uuid": "^9.0.8",
"@typescript-eslint/parser": "^7.7.0",
"@typescript-eslint/parser": "^7.12.0",
"eslint": "^8.57.0",
"eslint-config-prettier": "^9.1.0",
"eslint-config-standard": "^17.1.0",
+2 -21
View File
@@ -32,20 +32,10 @@ export const ScoreSource = {
export type ScoreSource = (typeof ScoreSource)[keyof typeof ScoreSource];
export const ScoreDataType = {
CATEGORICAL: "CATEGORICAL",
NUMERIC: "NUMERIC"
NUMERIC: "NUMERIC",
BOOLEAN: "BOOLEAN"
} as const;
export type ScoreDataType = (typeof ScoreDataType)[keyof typeof ScoreDataType];
export const PricingUnit = {
PER_1000_TOKENS: "PER_1000_TOKENS",
PER_1000_CHARS: "PER_1000_CHARS"
} as const;
export type PricingUnit = (typeof PricingUnit)[keyof typeof PricingUnit];
export const TokenType = {
PROMPT: "PROMPT",
COMPLETION: "COMPLETION",
TOTAL: "TOTAL"
} as const;
export type TokenType = (typeof TokenType)[keyof typeof TokenType];
export const DatasetStatus = {
ACTIVE: "ACTIVE",
ARCHIVED: "ARCHIVED"
@@ -309,14 +299,6 @@ export type PosthogIntegration = {
enabled: boolean;
created_at: Generated<Timestamp>;
};
export type Pricing = {
id: string;
model_name: string;
pricing_unit: Generated<PricingUnit>;
price: string;
currency: Generated<string>;
token_type: TokenType;
};
export type Project = {
id: string;
created_at: Generated<Timestamp>;
@@ -470,7 +452,6 @@ export type DB = {
observations: Observation;
observations_view: ObservationView;
posthog_integrations: PosthogIntegration;
pricings: Pricing;
project_memberships: ProjectMembership;
projects: Project;
prompts: Prompt;
@@ -0,0 +1,2 @@
-- CreateIndex
CREATE INDEX CONCURRENTLY "dataset_items_source_trace_id_idx" ON "dataset_items" USING HASH ("source_trace_id");
@@ -0,0 +1,2 @@
-- CreateIndex
CREATE INDEX "scores_project_id_name_idx" ON "scores"("project_id", "name");
@@ -0,0 +1,2 @@
-- AlterEnum
ALTER TYPE "ScoreDataType" ADD VALUE 'BOOLEAN';
@@ -0,0 +1,50 @@
-- Create manual-score config for any project with manual scores and link config_id to scores
BEGIN;
WITH project_configs AS (
INSERT INTO score_configs (id,
project_id,
name,
data_type,
is_archived,
min_value,
max_value,
description)
SELECT
gen_random_uuid () AS id,
s.project_id,
'manual-score',
'NUMERIC',
FALSE,
- 1,
1,
'Langfuse legacy annotation score.'
FROM ( SELECT DISTINCT
project_id
FROM
scores
WHERE
name = 'manual-score'
AND source = 'ANNOTATION') s
WHERE
NOT EXISTS (
SELECT
1
FROM
score_configs sc
WHERE
sc.name = 'manual-score'
AND sc.project_id = s.project_id)
RETURNING
id,
project_id
)
UPDATE
scores
SET
config_id = pc.id
FROM
project_configs pc
WHERE
scores.project_id = pc.project_id
AND scores.name = 'manual-score';
COMMIT;
@@ -0,0 +1,5 @@
-- does not include pricing yet, will be added as soon as it is calculated at ingestion time
INSERT INTO "public"."models" ("id", "model_name", "match_pattern", "unit") VALUES ('clx30djsn0000w9mzebiv41we', 'gemini-1.5-flash', '(?i)^(gemini-1.5-flash)(@[a-zA-Z0-9]+)?$', 'CHARACTERS');
INSERT INTO "public"."models" ("id", "model_name", "match_pattern", "unit") VALUES ('clx30hkrx0000w9mz7lqi0ial', 'gemini-1.5-pro', '(?i)^(gemini-1.5-pro)(@[a-zA-Z0-9]+)?$', 'CHARACTERS');
@@ -0,0 +1,14 @@
/*
Warnings:
- You are about to drop the `pricings` table. If the table is not empty, all the data it contains will be lost.
*/
-- DropTable
DROP TABLE "pricings";
-- DropEnum
DROP TYPE "PricingUnit";
-- DropEnum
DROP TYPE "TokenType";
@@ -0,0 +1,3 @@
-- DropForeignKey
ALTER TABLE "dataset_run_items" DROP CONSTRAINT "dataset_run_items_trace_id_fkey";
ALTER TABLE "dataset_run_items" DROP CONSTRAINT "dataset_run_items_observation_id_fkey";
+23 -47
View File
@@ -233,9 +233,8 @@ model Trace {
createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime @default(now()) @updatedAt @map("updated_at")
DatasetRunItems DatasetRunItems[]
DatasetItem DatasetItem[]
JobExecution JobExecution[]
DatasetItem DatasetItem[]
JobExecution JobExecution[]
@@index([projectId])
@@index([sessionId])
@@ -298,21 +297,20 @@ model Observation {
// GENERATION ONLY
model String?
internalModel String? @map("internal_model")
internalModel String? @map("internal_model")
modelParameters Json?
input Json?
output Json?
promptTokens Int @default(0) @map("prompt_tokens")
completionTokens Int @default(0) @map("completion_tokens")
totalTokens Int @default(0) @map("total_tokens")
promptTokens Int @default(0) @map("prompt_tokens")
completionTokens Int @default(0) @map("completion_tokens")
totalTokens Int @default(0) @map("total_tokens")
unit String?
inputCost Decimal? @map("input_cost")
outputCost Decimal? @map("output_cost")
totalCost Decimal? @map("total_cost")
completionStartTime DateTime? @map("completion_start_time")
project Project @relation(fields: [projectId], references: [id], onDelete: Cascade)
inputCost Decimal? @map("input_cost")
outputCost Decimal? @map("output_cost")
totalCost Decimal? @map("total_cost")
completionStartTime DateTime? @map("completion_start_time")
project Project @relation(fields: [projectId], references: [id], onDelete: Cascade)
derivedDatasetItems DatasetItem[]
datasetRunItems DatasetRunItems[]
promptId String? @map("prompt_id")
prompt Prompt? @relation(fields: [promptId], onDelete: SetNull, references: [id])
@@ -419,6 +417,7 @@ model Score {
@@index(timestamp)
@@index([value])
@@index([projectId])
@@index([projectId, name])
@@index([authorUserId])
@@index([configId])
@@index([traceId], type: Hash)
@@ -463,29 +462,7 @@ model ScoreConfig {
enum ScoreDataType {
CATEGORICAL
NUMERIC
}
enum PricingUnit {
PER_1000_TOKENS
PER_1000_CHARS
}
enum TokenType {
PROMPT
COMPLETION
TOTAL
}
model Pricing {
id String @id @default(cuid())
modelName String @map("model_name")
pricingUnit PricingUnit @default(PER_1000_TOKENS) @map("pricing_unit")
price Decimal
currency String @default("USD")
tokenType TokenType @map("token_type")
@@index(modelName)
@@map("pricings")
BOOLEAN
}
model CronJobs {
@@ -532,6 +509,7 @@ model DatasetItem {
updatedAt DateTime @default(now()) @updatedAt @map("updated_at")
datasetRunItems DatasetRunItems[]
@@index([sourceTraceId], type: Hash)
@@index([sourceObservationId], type: Hash)
@@index([datasetId], type: Hash)
@@index([createdAt])
@@ -563,17 +541,15 @@ model DatasetRuns {
}
model DatasetRunItems {
id String @id @default(cuid())
datasetRunId String @map("dataset_run_id")
datasetRun DatasetRuns @relation(fields: [datasetRunId], references: [id], onDelete: Cascade)
datasetItemId String @map("dataset_item_id")
datasetItem DatasetItem @relation(fields: [datasetItemId], references: [id], onDelete: Cascade)
traceId String @map("trace_id")
trace Trace @relation(fields: [traceId], references: [id], onDelete: Cascade)
observationId String? @map("observation_id")
observation Observation? @relation(fields: [observationId], references: [id], onDelete: Cascade)
createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime @default(now()) @updatedAt @map("updated_at")
id String @id @default(cuid())
datasetRunId String @map("dataset_run_id")
datasetRun DatasetRuns @relation(fields: [datasetRunId], references: [id], onDelete: Cascade)
datasetItemId String @map("dataset_item_id")
datasetItem DatasetItem @relation(fields: [datasetItemId], references: [id], onDelete: Cascade)
traceId String @map("trace_id")
observationId String? @map("observation_id")
createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime @default(now()) @updatedAt @map("updated_at")
@@index([datasetRunId], type: Hash)
@@index([datasetItemId], type: Hash)
+103 -3
View File
@@ -4,6 +4,7 @@ import {
type Prisma,
ObservationType,
ScoreSource,
ScoreDataType,
} from "../src/index";
import { hash } from "bcryptjs";
import { parseArgs } from "node:util";
@@ -39,6 +40,7 @@ async function main() {
name: "Demo User",
email: "demo@langfuse.com",
password: await hash("password", 12),
image: "https://static.langfuse.com/langfuse-dev%2Fexample-avatar.png",
},
});
@@ -144,6 +146,11 @@ async function main() {
});
}
const configIdsAndNames = await generateConfigsForProject([
project1,
project2,
]);
const promptIds = await generatePromptsForProject([project1, project2]);
const envTags = [null, "development", "staging", "production"];
@@ -157,7 +164,8 @@ async function main() {
colorTags,
project1,
project2,
promptIds
promptIds,
configIdsAndNames
);
console.log(
@@ -453,13 +461,15 @@ function createObjects(
colorTags: (string | null)[],
project1: Project,
project2: Project,
promptIds: Map<string, string[]>
promptIds: Map<string, string[]>,
configIdsAndNames: Map<string, { name: string; id: string }[]>
) {
const traces: Prisma.TraceCreateManyInput[] = [];
const observations: Prisma.ObservationCreateManyInput[] = [];
const scores: Prisma.ScoreCreateManyInput[] = [];
const sessions: Prisma.TraceSessionCreateManyInput[] = [];
const events: Prisma.ObservationCreateManyInput[] = [];
const configs: Prisma.ScoreConfigCreateManyInput[] = [];
for (let i = 0; i < traceVolume; i++) {
// print progress to console with a progress bar that refreshes every 10 iterations
@@ -511,17 +521,28 @@ function createObjects(
traces.push(trace);
const configArray = configIdsAndNames.get(projectId) ?? [];
const randomIndex = Math.floor(Math.random() * 3);
const config =
configArray.length >= randomIndex - 1 && configArray[randomIndex];
const { name: annotationScoreName, id: configId } = config || {
name: "manual-score",
id: undefined,
};
const traceScores = [
...(Math.random() > 0.5
? [
{
traceId: trace.id,
name: "manual-score",
name: annotationScoreName,
value: Math.floor(Math.random() * 3) - 1,
timestamp: traceTs,
source: ScoreSource.ANNOTATION,
projectId,
authorUserId: `user-${i}`,
dataType: ScoreDataType.NUMERIC,
...(configId ? { configId } : {}),
},
]
: []),
@@ -534,6 +555,7 @@ function createObjects(
timestamp: traceTs,
source: ScoreSource.API,
projectId,
dataType: ScoreDataType.NUMERIC,
},
]
: []),
@@ -772,6 +794,7 @@ function createObjects(
traces,
observations,
scores,
configs,
sessions: uniqueSessions,
events,
};
@@ -954,3 +977,80 @@ async function generatePrompts(project: Project) {
}
return promptIds;
}
async function generateConfigsForProject(projects: Project[]) {
const projectIdsToConfigs: Map<string, { name: string; id: string }[]> =
new Map();
await Promise.all(
projects.map(async (project) => {
const configNameAndId = await generateConfigs(project);
projectIdsToConfigs.set(project.id, configNameAndId);
})
);
return projectIdsToConfigs;
}
async function generateConfigs(project: Project) {
const configNameAndId: { name: string; id: string }[] = [];
const configs = [
{
id: `config-${v4()}`,
name: "manual-score",
dataType: ScoreDataType.NUMERIC,
projectId: project.id,
isArchived: false,
},
{
id: `config-${v4()}`,
projectId: project.id,
name: "Accuracy",
dataType: ScoreDataType.CATEGORICAL,
categories: [
{ label: "Incorrect", value: 0 },
{ label: "Partially Correct", value: 1 },
{ label: "Correct", value: 2 },
],
isArchived: false,
},
{
id: `config-${v4()}`,
projectId: project.id,
name: "Toxicity",
dataType: ScoreDataType.BOOLEAN,
categories: [
{ label: "True", value: 1 },
{ label: "False", value: 0 },
],
description:
"Used to indicate if text was harmful or offensive in nature.",
isArchived: false,
},
];
for (const config of configs) {
await prisma.scoreConfig.upsert({
where: {
id_projectId: {
projectId: config.projectId,
id: config.id,
},
},
create: {
id: config.id,
projectId: config.projectId,
name: config.name,
dataType: config.dataType,
categories: config.categories,
isArchived: config.isArchived,
},
update: {
id: config.id,
},
});
configNameAndId.push({ name: config.name, id: config.id });
}
return configNameAndId;
}
@@ -1,9 +1,9 @@
import lodash from "lodash";
import { z } from "zod";
import { ModelUsageUnit } from "@langfuse/shared";
import { NonEmptyString, jsonSchema } from "@/src/utils/zod";
import { ObservationLevel } from "@langfuse/shared";
import { NonEmptyString, jsonSchema } from "../../utils/zod";
import { ModelUsageUnit } from "../../constants";
import { ObservationLevel } from "@prisma/client";
export const Usage = z.object({
input: z.number().int().nullish(),
@@ -109,7 +109,7 @@ export const CreateGenerationBody = CreateSpanBody.extend({
z.string(),
z
.union([z.string(), z.number(), z.boolean(), z.array(z.string())])
.nullish(),
.nullish()
)
.nullish(),
usage: usage,
@@ -131,7 +131,7 @@ export const UpdateGenerationBody = UpdateSpanBody.extend({
z.string(),
z
.union([z.string(), z.number(), z.boolean(), z.array(z.string())])
.nullish(),
.nullish()
)
.nullish(),
usage: usage,
@@ -195,7 +195,7 @@ export const LegacyGenerationsCreateSchema = z.object({
modelParameters: z
.record(
z.string(),
z.union([z.string(), z.number(), z.boolean()]).nullish(),
z.union([z.string(), z.number(), z.boolean()]).nullish()
)
.nullish(),
prompt: jsonSchema.nullish(),
@@ -219,7 +219,7 @@ export const LegacyGenerationPatchSchema = z.object({
modelParameters: z
.record(
z.string(),
z.union([z.string(), z.number(), z.boolean()]).nullish(),
z.union([z.string(), z.number(), z.boolean()]).nullish()
)
.nullish(),
prompt: jsonSchema.nullish(),
@@ -243,7 +243,7 @@ export const LegacyObservationBody = z.object({
modelParameters: z
.record(
z.string(),
z.union([z.string(), z.number(), z.boolean()]).nullish(),
z.union([z.string(), z.number(), z.boolean()]).nullish()
)
.nullish(),
input: jsonSchema.nullish(),
+4 -4
View File
@@ -1,7 +1,7 @@
import { Prisma } from "@prisma/client";
import {
ColumnDefinition,
type TableNames as TableName,
type TableNames,
} from "./interfaces/tableDefinition";
import { FilterState } from "./types";
import { filterOperators } from "./interfaces/filters";
@@ -27,7 +27,7 @@ const arrayOperatorReplacements = {
export function tableColumnsToSqlFilterAndPrefix(
filters: FilterState,
tableColumns: ColumnDefinition[],
table: TableName
table: TableNames
): Prisma.Sql {
const sql = tableColumnsToSqlFilter(filters, tableColumns, table);
if (sql === Prisma.empty) {
@@ -43,7 +43,7 @@ export function tableColumnsToSqlFilterAndPrefix(
export function tableColumnsToSqlFilter(
filters: FilterState,
tableColumns: ColumnDefinition[],
table: TableName
table: TableNames
): Prisma.Sql {
const internalFilters = filters.map((filter) => {
// Get column definition to map column to internal name, e.g. "t.id"
@@ -153,7 +153,7 @@ export function tableColumnsToSqlFilter(
const castValueToPostgresTypes = (
column: ColumnDefinition,
table: TableName
table: TableNames
) => {
return column.name === "type" &&
(table === "observations" ||
+3
View File
@@ -9,6 +9,9 @@ export * from "./filterToPrisma";
export * from "./tracesTable";
export * from "./server/auth";
export * from "./observationsTable";
export * from "./features/ingestion/types";
export * from "./utils/zod";
export * from "./utils/json";
export { env } from "./env";
// llm api
@@ -42,6 +42,7 @@ export const tableNames = [
"traces_parent_observation_scores",
"sessions",
"prompts",
"users",
] as const;
export type TableNames = (typeof tableNames)[number];
+2 -1
View File
@@ -30,4 +30,5 @@ export type TableName =
| "sessions"
| "scores"
| "prompts"
| "dashboard";
| "dashboard"
| "users";
@@ -1,6 +1,6 @@
import { type jsonSchema } from "@/src/utils/zod";
import { type z } from "zod";
import lodash from "lodash";
import { jsonSchema } from "./zod";
export const parseJson = (input: string) => {
try {
@@ -37,7 +37,7 @@ export function deepParseJson(json: unknown): unknown {
// Ensure we only iterate over the object's own properties
if (Object.prototype.hasOwnProperty.call(json, key)) {
(json as Record<string, unknown>)[key] = deepParseJson(
(json as Record<string, unknown>)[key],
(json as Record<string, unknown>)[key]
);
}
}
@@ -50,7 +50,7 @@ export function deepParseJson(json: unknown): unknown {
export const mergeJson = (
json1?: z.infer<typeof jsonSchema>,
json2?: z.infer<typeof jsonSchema>,
json2?: z.infer<typeof jsonSchema>
) => {
if (json1 === undefined) {
return json2;
@@ -26,7 +26,7 @@ const jsonSchemaNullable: z.ZodType<JsonNested> = z.lazy(() =>
nestedLiteralSchema,
z.array(jsonSchemaNullable),
z.record(jsonSchemaNullable),
]),
])
);
// Root schema that does not allow nulls at the root level
@@ -35,17 +35,17 @@ export const jsonSchema: z.ZodType<Json> = z.lazy(() =>
rootLiteralSchema,
z.array(jsonSchemaNullable),
z.record(jsonSchemaNullable),
]),
])
);
export const paginationZod = {
page: z.preprocess(
(x) => (x === "" ? undefined : x),
z.coerce.number().default(1),
z.coerce.number().default(1)
),
limit: z.preprocess(
(x) => (x === "" ? undefined : x),
z.coerce.number().lte(100).default(50),
z.coerce.number().lte(100).default(50)
),
};
+204 -412
View File
@@ -49,8 +49,8 @@ importers:
specifier: ^20.11.29
version: 20.11.29
'@typescript-eslint/parser':
specifier: ^7.7.0
version: 7.7.0(eslint@8.57.0)(typescript@5.4.5)
specifier: ^7.12.0
version: 7.12.0(eslint@8.57.0)(typescript@5.4.5)
eslint:
specifier: ^8.57.0
version: 8.57.0
@@ -80,10 +80,10 @@ importers:
devDependencies:
'@typescript-eslint/eslint-plugin':
specifier: ^7.1.0
version: 7.3.1(@typescript-eslint/parser@7.7.0)(eslint@8.57.0)(typescript@5.4.5)
version: 7.3.1(@typescript-eslint/parser@7.12.0)(eslint@8.57.0)(typescript@5.4.5)
'@typescript-eslint/parser':
specifier: ^7.7.0
version: 7.7.0(eslint@8.57.0)(typescript@5.4.5)
specifier: ^7.12.0
version: 7.12.0(eslint@8.57.0)(typescript@5.4.5)
'@vercel/style-guide':
specifier: ^6.0.0
version: 6.0.0(eslint@8.57.0)(prettier@3.2.5)(typescript@5.4.5)
@@ -130,7 +130,10 @@ importers:
version: 0.27.3
langchain:
specifier: ^0.1.36
version: 0.1.36
version: 0.1.36(@aws-sdk/client-s3@3.550.0)(@aws-sdk/credential-provider-node@3.549.0)(lodash@4.17.21)
lodash:
specifier: ^4.17.21
version: 4.17.21
prisma-extension-kysely:
specifier: ^2.1.0
version: 2.1.0(@prisma/client@5.13.0)
@@ -160,8 +163,8 @@ importers:
specifier: ^9.0.8
version: 9.0.8
'@typescript-eslint/parser':
specifier: ^7.7.0
version: 7.7.0(eslint@8.57.0)(typescript@5.4.5)
specifier: ^7.12.0
version: 7.12.0(eslint@8.57.0)(typescript@5.4.5)
eslint:
specifier: ^8.57.0
version: 8.57.0
@@ -315,6 +318,9 @@ importers:
'@radix-ui/react-toggle':
specifier: ^1.0.3
version: 1.0.3(@types/react-dom@18.2.25)(@types/react@18.2.79)(react-dom@18.2.0)(react@18.2.0)
'@radix-ui/react-toggle-group':
specifier: ^1.0.4
version: 1.0.4(@types/react-dom@18.2.25)(@types/react@18.2.79)(react-dom@18.2.0)(react@18.2.0)
'@radix-ui/react-tooltip':
specifier: ^1.0.7
version: 1.0.7(@types/react-dom@18.2.25)(@types/react@18.2.79)(react-dom@18.2.0)(react@18.2.0)
@@ -357,9 +363,6 @@ importers:
'@tanstack/react-table':
specifier: ^8.11.8
version: 8.13.2(react-dom@18.2.0)(react@18.2.0)
'@tanstack/react-virtual':
specifier: ^3.5.0
version: 3.5.0(react-dom@18.2.0)(react@18.2.0)
'@tremor/react':
specifier: 3.16.2
version: 3.16.2(react-dom@18.2.0)(react@18.2.0)(tailwindcss@3.4.3)
@@ -495,6 +498,9 @@ importers:
uuid:
specifier: ^9.0.1
version: 9.0.1
vaul:
specifier: ^0.9.1
version: 0.9.1(@types/react-dom@18.2.25)(@types/react@18.2.79)(react-dom@18.2.0)(react@18.2.0)
zod:
specifier: ^3.22.4
version: 3.22.4
@@ -556,10 +562,10 @@ importers:
version: 9.0.8
'@typescript-eslint/eslint-plugin':
specifier: ^6.21.0
version: 6.21.0(@typescript-eslint/parser@7.7.0)(eslint@8.57.0)(typescript@5.4.5)
version: 6.21.0(@typescript-eslint/parser@7.12.0)(eslint@8.57.0)(typescript@5.4.5)
'@typescript-eslint/parser':
specifier: ^7.7.0
version: 7.7.0(eslint@8.57.0)(typescript@5.4.5)
specifier: ^7.12.0
version: 7.12.0(eslint@8.57.0)(typescript@5.4.5)
autoprefixer:
specifier: ^10.4.19
version: 10.4.19(postcss@8.4.38)
@@ -608,6 +614,9 @@ importers:
worker:
dependencies:
'@anthropic-ai/tokenizer':
specifier: ^0.0.4
version: 0.0.4
'@langfuse/shared':
specifier: workspace:*
version: link:../packages/shared
@@ -647,6 +656,9 @@ importers:
ioredis:
specifier: ^5.4.1
version: 5.4.1
js-tiktoken:
specifier: ^1.0.12
version: 1.0.12
kysely:
specifier: ^0.27.3
version: 0.27.3
@@ -665,6 +677,9 @@ importers:
pino-pretty:
specifier: ^10.3.1
version: 10.3.1
tiktoken:
specifier: ^1.0.15
version: 1.0.15
uuid:
specifier: ^9.0.1
version: 9.0.1
@@ -697,8 +712,8 @@ importers:
specifier: ^8.11.6
version: 8.11.6
'@typescript-eslint/parser':
specifier: ^7.7.0
version: 7.7.0(eslint@8.57.0)(typescript@5.4.5)
specifier: ^7.12.0
version: 7.12.0(eslint@8.57.0)(typescript@5.4.5)
eslint:
specifier: ^8.57.0
version: 8.57.0
@@ -824,7 +839,7 @@ packages:
resolution: {integrity: sha512-EHRKbxlxlc8W4KCBEseByJ7YwyYCmgu9OyN59H9+IYIGPoKv8tXyQXinkeGDI+cI8Tiuz9wk2jZb/kK7AyvL7g==}
dependencies:
'@types/node': 18.19.25
tiktoken: 1.0.13
tiktoken: 1.0.15
dev: false
/@aws-crypto/crc32@3.0.0:
@@ -2079,14 +2094,6 @@ packages:
style-mod: 4.1.2
dev: false
/@codemirror/lint@6.7.1:
resolution: {integrity: sha512-rELba6QJD20/bNXWP/cKTGLrwVEcpa2ViwULCV03ONcY1Je85++7sczVRUlnE4TJMjatx3IJTz6HX4NXi+moXw==}
dependencies:
'@codemirror/state': 6.4.1
'@codemirror/view': 6.26.3
crelt: 1.0.6
dev: false
/@codemirror/lint@6.8.0:
resolution: {integrity: sha512-lsFofvaw0lnPRJlQylNsC4IRt/1lI4OD/yYslrSGVndOJfStc58v+8p9dgGiD90ktOfL7OhBWns1ZETYgz0EJA==}
dependencies:
@@ -3346,7 +3353,7 @@ packages:
ansi-styles: 5.2.0
camelcase: 6.3.0
decamelize: 1.2.0
js-tiktoken: 1.0.11
js-tiktoken: 1.0.12
langsmith: 0.1.14
ml-distance: 4.0.1
mustache: 4.2.0
@@ -4256,7 +4263,7 @@ packages:
'@types/react-dom':
optional: true
dependencies:
'@babel/runtime': 7.24.0
'@babel/runtime': 7.24.5
'@radix-ui/primitive': 1.0.1
'@radix-ui/react-compose-refs': 1.0.1(@types/react@18.2.79)(react@18.2.0)
'@radix-ui/react-context': 1.0.1(@types/react@18.2.79)(react@18.2.0)
@@ -4841,6 +4848,33 @@ packages:
react-dom: 18.2.0(react@18.2.0)
dev: false
/@radix-ui/react-toggle-group@1.0.4(@types/react-dom@18.2.25)(@types/react@18.2.79)(react-dom@18.2.0)(react@18.2.0):
resolution: {integrity: sha512-Uaj/M/cMyiyT9Bx6fOZO0SAG4Cls0GptBWiBmBxofmDbNVnYYoyRWj/2M/6VCi/7qcXFWnHhRUfdfZFvvkuu8A==}
peerDependencies:
'@types/react': '*'
'@types/react-dom': '*'
react: ^16.8 || ^17.0 || ^18.0
react-dom: ^16.8 || ^17.0 || ^18.0
peerDependenciesMeta:
'@types/react':
optional: true
'@types/react-dom':
optional: true
dependencies:
'@babel/runtime': 7.24.5
'@radix-ui/primitive': 1.0.1
'@radix-ui/react-context': 1.0.1(@types/react@18.2.79)(react@18.2.0)
'@radix-ui/react-direction': 1.0.1(@types/react@18.2.79)(react@18.2.0)
'@radix-ui/react-primitive': 1.0.3(@types/react-dom@18.2.25)(@types/react@18.2.79)(react-dom@18.2.0)(react@18.2.0)
'@radix-ui/react-roving-focus': 1.0.4(@types/react-dom@18.2.25)(@types/react@18.2.79)(react-dom@18.2.0)(react@18.2.0)
'@radix-ui/react-toggle': 1.0.3(@types/react-dom@18.2.25)(@types/react@18.2.79)(react-dom@18.2.0)(react@18.2.0)
'@radix-ui/react-use-controllable-state': 1.0.1(@types/react@18.2.79)(react@18.2.0)
'@types/react': 18.2.79
'@types/react-dom': 18.2.25
react: 18.2.0
react-dom: 18.2.0(react@18.2.0)
dev: false
/@radix-ui/react-toggle@1.0.3(@types/react-dom@18.2.25)(@types/react@18.2.79)(react-dom@18.2.0)(react@18.2.0):
resolution: {integrity: sha512-Pkqg3+Bc98ftZGsl60CLANXQBBQ4W3mTFS9EJvNxKMZ7magklKV69/id1mlAlOFDDfHvlCms0fx8fA4CMKDJHg==}
peerDependencies:
@@ -6710,7 +6744,7 @@ packages:
dev: true
optional: true
/@typescript-eslint/eslint-plugin@6.21.0(@typescript-eslint/parser@7.7.0)(eslint@8.57.0)(typescript@5.4.5):
/@typescript-eslint/eslint-plugin@6.21.0(@typescript-eslint/parser@7.12.0)(eslint@8.57.0)(typescript@5.4.5):
resolution: {integrity: sha512-oy9+hTPCUFpngkEZUSzbf9MxI65wbKFoQYsgPdILTfbUldp5ovUuphZVe4i30emU9M/kP+T64Di0mxl7dSw3MA==}
engines: {node: ^16.0.0 || >=18.0.0}
peerDependencies:
@@ -6722,7 +6756,7 @@ packages:
optional: true
dependencies:
'@eslint-community/regexpp': 4.10.0
'@typescript-eslint/parser': 7.7.0(eslint@8.57.0)(typescript@5.4.5)
'@typescript-eslint/parser': 7.12.0(eslint@8.57.0)(typescript@5.4.5)
'@typescript-eslint/scope-manager': 6.21.0
'@typescript-eslint/type-utils': 6.21.0(eslint@8.57.0)(typescript@5.4.5)
'@typescript-eslint/utils': 6.21.0(eslint@8.57.0)(typescript@5.4.5)
@@ -6739,7 +6773,7 @@ packages:
- supports-color
dev: true
/@typescript-eslint/eslint-plugin@7.3.1(@typescript-eslint/parser@7.7.0)(eslint@8.57.0)(typescript@5.4.5):
/@typescript-eslint/eslint-plugin@7.3.1(@typescript-eslint/parser@7.12.0)(eslint@8.57.0)(typescript@5.4.5):
resolution: {integrity: sha512-STEDMVQGww5lhCuNXVSQfbfuNII5E08QWkvAw5Qwf+bj2WT+JkG1uc+5/vXA3AOYMDHVOSpL+9rcbEUiHIm2dw==}
engines: {node: ^18.18.0 || >=20.0.0}
peerDependencies:
@@ -6751,7 +6785,7 @@ packages:
optional: true
dependencies:
'@eslint-community/regexpp': 4.10.0
'@typescript-eslint/parser': 7.7.0(eslint@8.57.0)(typescript@5.4.5)
'@typescript-eslint/parser': 7.12.0(eslint@8.57.0)(typescript@5.4.5)
'@typescript-eslint/scope-manager': 7.3.1
'@typescript-eslint/type-utils': 7.3.1(eslint@8.57.0)(typescript@5.4.5)
'@typescript-eslint/utils': 7.3.1(eslint@8.57.0)(typescript@5.4.5)
@@ -6768,6 +6802,27 @@ packages:
- supports-color
dev: true
/@typescript-eslint/parser@7.12.0(eslint@8.57.0)(typescript@5.4.5):
resolution: {integrity: sha512-dm/J2UDY3oV3TKius2OUZIFHsomQmpHtsV0FTh1WO8EKgHLQ1QCADUqscPgTpU+ih1e21FQSRjXckHn3txn6kQ==}
engines: {node: ^18.18.0 || >=20.0.0}
peerDependencies:
eslint: ^8.56.0
typescript: '*'
peerDependenciesMeta:
typescript:
optional: true
dependencies:
'@typescript-eslint/scope-manager': 7.12.0
'@typescript-eslint/types': 7.12.0
'@typescript-eslint/typescript-estree': 7.12.0(typescript@5.4.5)
'@typescript-eslint/visitor-keys': 7.12.0
debug: 4.3.4(supports-color@5.5.0)
eslint: 8.57.0
typescript: 5.4.5
transitivePeerDependencies:
- supports-color
dev: true
/@typescript-eslint/parser@7.2.0(eslint@8.57.0)(typescript@5.4.5):
resolution: {integrity: sha512-5FKsVcHTk6TafQKQbuIVkXq58Fnbkd2wDL4LB7AURN7RUOu1utVP+G8+6u3ZhEroW3DF6hyo3ZEXxgKgp4KeCg==}
engines: {node: ^16.0.0 || >=18.0.0}
@@ -6789,27 +6844,6 @@ packages:
- supports-color
dev: true
/@typescript-eslint/parser@7.7.0(eslint@8.57.0)(typescript@5.4.5):
resolution: {integrity: sha512-fNcDm3wSwVM8QYL4HKVBggdIPAy9Q41vcvC/GtDobw3c4ndVT3K6cqudUmjHPw8EAp4ufax0o58/xvWaP2FmTg==}
engines: {node: ^18.18.0 || >=20.0.0}
peerDependencies:
eslint: ^8.56.0
typescript: '*'
peerDependenciesMeta:
typescript:
optional: true
dependencies:
'@typescript-eslint/scope-manager': 7.7.0
'@typescript-eslint/types': 7.7.0
'@typescript-eslint/typescript-estree': 7.7.0(typescript@5.4.5)
'@typescript-eslint/visitor-keys': 7.7.0
debug: 4.3.4(supports-color@5.5.0)
eslint: 8.57.0
typescript: 5.4.5
transitivePeerDependencies:
- supports-color
dev: true
/@typescript-eslint/scope-manager@5.62.0:
resolution: {integrity: sha512-VXuvVvZeQCQb5Zgf4HAxc04q5j+WrNAtNh9OwCsCgpKqESMTu3tF/jhZ3xG6T4NZwWl65Bg8KuS2uEvhSfLl0w==}
engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0}
@@ -6826,6 +6860,14 @@ packages:
'@typescript-eslint/visitor-keys': 6.21.0
dev: true
/@typescript-eslint/scope-manager@7.12.0:
resolution: {integrity: sha512-itF1pTnN6F3unPak+kutH9raIkL3lhH1YRPGgt7QQOh43DQKVJXmWkpb+vpc/TiDHs6RSd9CTbDsc/Y+Ygq7kg==}
engines: {node: ^18.18.0 || >=20.0.0}
dependencies:
'@typescript-eslint/types': 7.12.0
'@typescript-eslint/visitor-keys': 7.12.0
dev: true
/@typescript-eslint/scope-manager@7.2.0:
resolution: {integrity: sha512-Qh976RbQM/fYtjx9hs4XkayYujB/aPwglw2choHmf3zBjB4qOywWSdt9+KLRdHubGcoSwBnXUH2sR3hkyaERRg==}
engines: {node: ^16.0.0 || >=18.0.0}
@@ -6842,14 +6884,6 @@ packages:
'@typescript-eslint/visitor-keys': 7.3.1
dev: true
/@typescript-eslint/scope-manager@7.7.0:
resolution: {integrity: sha512-/8INDn0YLInbe9Wt7dK4cXLDYp0fNHP5xKLHvZl3mOT5X17rK/YShXaiNmorl+/U4VKCVIjJnx4Ri5b0y+HClw==}
engines: {node: ^18.18.0 || >=20.0.0}
dependencies:
'@typescript-eslint/types': 7.7.0
'@typescript-eslint/visitor-keys': 7.7.0
dev: true
/@typescript-eslint/type-utils@6.21.0(eslint@8.57.0)(typescript@5.4.5):
resolution: {integrity: sha512-rZQI7wHfao8qMX3Rd3xqeYSMCL3SoiSQLBATSiVKARdFGCYSRvmViieZjqc58jKgs8Y8i9YvVVhRbHSTA4VBag==}
engines: {node: ^16.0.0 || >=18.0.0}
@@ -6900,6 +6934,11 @@ packages:
engines: {node: ^16.0.0 || >=18.0.0}
dev: true
/@typescript-eslint/types@7.12.0:
resolution: {integrity: sha512-o+0Te6eWp2ppKY3mLCU+YA9pVJxhUJE15FV7kxuD9jgwIAa+w/ycGJBMrYDTpVGUM/tgpa9SeMOugSabWFq7bg==}
engines: {node: ^18.18.0 || >=20.0.0}
dev: true
/@typescript-eslint/types@7.2.0:
resolution: {integrity: sha512-XFtUHPI/abFhm4cbCDc5Ykc8npOKBSJePY3a3s+lwumt7XWJuzP5cZcfZ610MIPHjQjNsOLlYK8ASPaNG8UiyA==}
engines: {node: ^16.0.0 || >=18.0.0}
@@ -6910,11 +6949,6 @@ packages:
engines: {node: ^18.18.0 || >=20.0.0}
dev: true
/@typescript-eslint/types@7.7.0:
resolution: {integrity: sha512-G01YPZ1Bd2hn+KPpIbrAhEWOn5lQBrjxkzHkWvP6NucMXFtfXoevK82hzQdpfuQYuhkvFDeQYbzXCjR1z9Z03w==}
engines: {node: ^18.18.0 || >=20.0.0}
dev: true
/@typescript-eslint/typescript-estree@5.62.0(typescript@5.4.5):
resolution: {integrity: sha512-CmcQ6uY7b9y694lKdRB8FEel7JbU/40iSAPomu++SjLMntB+2Leay2LO6i8VnJk58MtE9/nQSFIH6jpyRWyYzA==}
engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0}
@@ -6958,6 +6992,28 @@ packages:
- supports-color
dev: true
/@typescript-eslint/typescript-estree@7.12.0(typescript@5.4.5):
resolution: {integrity: sha512-5bwqLsWBULv1h6pn7cMW5dXX/Y2amRqLaKqsASVwbBHMZSnHqE/HN4vT4fE0aFsiwxYvr98kqOWh1a8ZKXalCQ==}
engines: {node: ^18.18.0 || >=20.0.0}
peerDependencies:
typescript: '*'
peerDependenciesMeta:
typescript:
optional: true
dependencies:
'@typescript-eslint/types': 7.12.0
'@typescript-eslint/visitor-keys': 7.12.0
debug: 4.3.4(supports-color@5.5.0)
globby: 11.1.0
is-glob: 4.0.3
minimatch: 9.0.4
semver: 7.6.2
ts-api-utils: 1.3.0(typescript@5.4.5)
typescript: 5.4.5
transitivePeerDependencies:
- supports-color
dev: true
/@typescript-eslint/typescript-estree@7.2.0(typescript@5.4.5):
resolution: {integrity: sha512-cyxS5WQQCoBwSakpMrvMXuMDEbhOo9bNHHrNcEWis6XHx6KF518tkF1wBvKIn/tpq5ZpUYK7Bdklu8qY0MsFIA==}
engines: {node: ^16.0.0 || >=18.0.0}
@@ -7002,28 +7058,6 @@ packages:
- supports-color
dev: true
/@typescript-eslint/typescript-estree@7.7.0(typescript@5.4.5):
resolution: {integrity: sha512-8p71HQPE6CbxIBy2kWHqM1KGrC07pk6RJn40n0DSc6bMOBBREZxSDJ+BmRzc8B5OdaMh1ty3mkuWRg4sCFiDQQ==}
engines: {node: ^18.18.0 || >=20.0.0}
peerDependencies:
typescript: '*'
peerDependenciesMeta:
typescript:
optional: true
dependencies:
'@typescript-eslint/types': 7.7.0
'@typescript-eslint/visitor-keys': 7.7.0
debug: 4.3.4(supports-color@5.5.0)
globby: 11.1.0
is-glob: 4.0.3
minimatch: 9.0.4
semver: 7.6.0
ts-api-utils: 1.3.0(typescript@5.4.5)
typescript: 5.4.5
transitivePeerDependencies:
- supports-color
dev: true
/@typescript-eslint/utils@5.62.0(eslint@8.57.0)(typescript@5.4.5):
resolution: {integrity: sha512-n8oxjeb5aIbPFEtmQxQYOLI0i9n5ySBEY/ZEHHZqKQSFnxio1rv6dthascc9dLuwrL0RC5mPCxB7vnAVGAYWAQ==}
engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0}
@@ -7098,6 +7132,14 @@ packages:
eslint-visitor-keys: 3.4.3
dev: true
/@typescript-eslint/visitor-keys@7.12.0:
resolution: {integrity: sha512-uZk7DevrQLL3vSnfFl5bj4sL75qC9D6EdjemIdbtkuUmIheWpuiiylSY01JxJE7+zGrOWDZrp1WxOuDntvKrHQ==}
engines: {node: ^18.18.0 || >=20.0.0}
dependencies:
'@typescript-eslint/types': 7.12.0
eslint-visitor-keys: 3.4.3
dev: true
/@typescript-eslint/visitor-keys@7.2.0:
resolution: {integrity: sha512-c6EIQRHhcpl6+tO8EMR+kjkkV+ugUNXOmeASA1rlzkd8EPIriavpWoiEz1HR/VLhbVIdhqnV6E7JZm00cBDx2A==}
engines: {node: ^16.0.0 || >=18.0.0}
@@ -7114,14 +7156,6 @@ packages:
eslint-visitor-keys: 3.4.3
dev: true
/@typescript-eslint/visitor-keys@7.7.0:
resolution: {integrity: sha512-h0WHOj8MhdhY8YWkzIF30R379y0NqyOHExI9N9KCzvmu05EgG4FumeYa3ccfKUSphyWkWQE1ybVrgz/Pbam6YA==}
engines: {node: ^18.18.0 || >=20.0.0}
dependencies:
'@typescript-eslint/types': 7.7.0
eslint-visitor-keys: 3.4.3
dev: true
/@uiw/codemirror-extensions-basic-setup@4.21.25(@codemirror/autocomplete@6.16.0)(@codemirror/commands@6.3.3)(@codemirror/language@6.10.1)(@codemirror/lint@6.8.0)(@codemirror/search@6.5.6)(@codemirror/state@6.4.1)(@codemirror/view@6.26.3):
resolution: {integrity: sha512-eeUKlmEE8aSoSgelS8OR2elcPGntpRo669XinAqPCLa0eKorT2B0d3ts+AE+njAeGk744tiyAEbHb2n+6OQmJw==}
peerDependencies:
@@ -7238,14 +7272,14 @@ packages:
'@babel/core': 7.24.3
'@babel/eslint-parser': 7.24.1(@babel/core@7.24.3)(eslint@8.57.0)
'@rushstack/eslint-patch': 1.7.2
'@typescript-eslint/eslint-plugin': 7.3.1(@typescript-eslint/parser@7.7.0)(eslint@8.57.0)(typescript@5.4.5)
'@typescript-eslint/parser': 7.7.0(eslint@8.57.0)(typescript@5.4.5)
'@typescript-eslint/eslint-plugin': 7.3.1(@typescript-eslint/parser@7.12.0)(eslint@8.57.0)(typescript@5.4.5)
'@typescript-eslint/parser': 7.12.0(eslint@8.57.0)(typescript@5.4.5)
eslint: 8.57.0
eslint-config-prettier: 9.1.0(eslint@8.57.0)
eslint-import-resolver-alias: 1.1.2(eslint-plugin-import@2.29.1)
eslint-import-resolver-typescript: 3.6.1(@typescript-eslint/parser@7.7.0)(eslint-plugin-import@2.29.1)(eslint@8.57.0)
eslint-import-resolver-typescript: 3.6.1(@typescript-eslint/parser@7.12.0)(eslint-plugin-import@2.29.1)(eslint@8.57.0)
eslint-plugin-eslint-comments: 3.2.0(eslint@8.57.0)
eslint-plugin-import: 2.29.1(@typescript-eslint/parser@7.7.0)(eslint-import-resolver-typescript@3.6.1)(eslint@8.57.0)
eslint-plugin-import: 2.29.1(@typescript-eslint/parser@7.12.0)(eslint-import-resolver-typescript@3.6.1)(eslint@8.57.0)
eslint-plugin-jest: 27.9.0(@typescript-eslint/eslint-plugin@7.3.1)(eslint@8.57.0)(typescript@5.4.5)
eslint-plugin-jsx-a11y: 6.8.0(eslint@8.57.0)
eslint-plugin-playwright: 1.5.4(eslint-plugin-jest@27.9.0)(eslint@8.57.0)
@@ -8420,7 +8454,7 @@ packages:
'@codemirror/autocomplete': 6.16.0(@codemirror/language@6.10.1)(@codemirror/state@6.4.1)(@codemirror/view@6.26.3)(@lezer/common@1.2.1)
'@codemirror/commands': 6.5.0
'@codemirror/language': 6.10.1
'@codemirror/lint': 6.7.1
'@codemirror/lint': 6.8.0
'@codemirror/search': 6.5.6
'@codemirror/state': 6.4.1
'@codemirror/view': 6.26.3
@@ -9521,7 +9555,7 @@ packages:
eslint: 8.57.0
eslint-import-resolver-node: 0.3.9
eslint-import-resolver-typescript: 3.6.1(@typescript-eslint/parser@7.2.0)(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.29.1)(eslint@8.57.0)
eslint-plugin-import: 2.29.1(@typescript-eslint/parser@7.7.0)(eslint-import-resolver-typescript@3.6.1)(eslint@8.57.0)
eslint-plugin-import: 2.29.1(@typescript-eslint/parser@7.12.0)(eslint-import-resolver-typescript@3.6.1)(eslint@8.57.0)
eslint-plugin-jsx-a11y: 6.8.0(eslint@8.57.0)
eslint-plugin-react: 7.34.1(eslint@8.57.0)
eslint-plugin-react-hooks: 4.6.0(eslint@8.57.0)
@@ -9550,7 +9584,7 @@ packages:
eslint-plugin-promise: ^6.0.0
dependencies:
eslint: 8.57.0
eslint-plugin-import: 2.29.1(@typescript-eslint/parser@7.7.0)(eslint-import-resolver-typescript@3.6.1)(eslint@8.57.0)
eslint-plugin-import: 2.29.1(@typescript-eslint/parser@7.12.0)(eslint-import-resolver-typescript@3.6.1)(eslint@8.57.0)
eslint-plugin-n: 16.6.2(eslint@8.57.0)
eslint-plugin-promise: 6.1.1(eslint@8.57.0)
dev: true
@@ -9570,7 +9604,7 @@ packages:
peerDependencies:
eslint-plugin-import: '>=1.4.0'
dependencies:
eslint-plugin-import: 2.29.1(@typescript-eslint/parser@7.7.0)(eslint-import-resolver-typescript@3.6.1)(eslint@8.57.0)
eslint-plugin-import: 2.29.1(@typescript-eslint/parser@7.12.0)(eslint-import-resolver-typescript@3.6.1)(eslint@8.57.0)
dev: true
/eslint-import-resolver-node@0.3.9:
@@ -9583,6 +9617,29 @@ packages:
- supports-color
dev: true
/eslint-import-resolver-typescript@3.6.1(@typescript-eslint/parser@7.12.0)(eslint-plugin-import@2.29.1)(eslint@8.57.0):
resolution: {integrity: sha512-xgdptdoi5W3niYeuQxKmzVDTATvLYqhpwmykwsh7f6HIOStGWEIL9iqZgQDF9u9OEzrRwR8no5q2VT+bjAujTg==}
engines: {node: ^14.18.0 || >=16.0.0}
peerDependencies:
eslint: '*'
eslint-plugin-import: '*'
dependencies:
debug: 4.3.4(supports-color@5.5.0)
enhanced-resolve: 5.16.0
eslint: 8.57.0
eslint-module-utils: 2.8.1(@typescript-eslint/parser@7.12.0)(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.1)(eslint@8.57.0)
eslint-plugin-import: 2.29.1(@typescript-eslint/parser@7.12.0)(eslint-import-resolver-typescript@3.6.1)(eslint@8.57.0)
fast-glob: 3.3.2
get-tsconfig: 4.7.3
is-core-module: 2.13.1
is-glob: 4.0.3
transitivePeerDependencies:
- '@typescript-eslint/parser'
- eslint-import-resolver-node
- eslint-import-resolver-webpack
- supports-color
dev: true
/eslint-import-resolver-typescript@3.6.1(@typescript-eslint/parser@7.2.0)(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.29.1)(eslint@8.57.0):
resolution: {integrity: sha512-xgdptdoi5W3niYeuQxKmzVDTATvLYqhpwmykwsh7f6HIOStGWEIL9iqZgQDF9u9OEzrRwR8no5q2VT+bjAujTg==}
engines: {node: ^14.18.0 || >=16.0.0}
@@ -9594,7 +9651,7 @@ packages:
enhanced-resolve: 5.16.0
eslint: 8.57.0
eslint-module-utils: 2.8.1(@typescript-eslint/parser@7.2.0)(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.1)(eslint@8.57.0)
eslint-plugin-import: 2.29.1(@typescript-eslint/parser@7.7.0)(eslint-import-resolver-typescript@3.6.1)(eslint@8.57.0)
eslint-plugin-import: 2.29.1(@typescript-eslint/parser@7.12.0)(eslint-import-resolver-typescript@3.6.1)(eslint@8.57.0)
fast-glob: 3.3.2
get-tsconfig: 4.7.3
is-core-module: 2.13.1
@@ -9606,26 +9663,33 @@ packages:
- supports-color
dev: true
/eslint-import-resolver-typescript@3.6.1(@typescript-eslint/parser@7.7.0)(eslint-plugin-import@2.29.1)(eslint@8.57.0):
resolution: {integrity: sha512-xgdptdoi5W3niYeuQxKmzVDTATvLYqhpwmykwsh7f6HIOStGWEIL9iqZgQDF9u9OEzrRwR8no5q2VT+bjAujTg==}
engines: {node: ^14.18.0 || >=16.0.0}
/eslint-module-utils@2.8.1(@typescript-eslint/parser@7.12.0)(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.1)(eslint@8.57.0):
resolution: {integrity: sha512-rXDXR3h7cs7dy9RNpUlQf80nX31XWJEyGq1tRMo+6GsO5VmTe4UTwtmonAD4ZkAsrfMVDA2wlGJ3790Ys+D49Q==}
engines: {node: '>=4'}
peerDependencies:
'@typescript-eslint/parser': '*'
eslint: '*'
eslint-plugin-import: '*'
eslint-import-resolver-node: '*'
eslint-import-resolver-typescript: '*'
eslint-import-resolver-webpack: '*'
peerDependenciesMeta:
'@typescript-eslint/parser':
optional: true
eslint:
optional: true
eslint-import-resolver-node:
optional: true
eslint-import-resolver-typescript:
optional: true
eslint-import-resolver-webpack:
optional: true
dependencies:
debug: 4.3.4(supports-color@5.5.0)
enhanced-resolve: 5.16.0
'@typescript-eslint/parser': 7.12.0(eslint@8.57.0)(typescript@5.4.5)
debug: 3.2.7
eslint: 8.57.0
eslint-module-utils: 2.8.1(@typescript-eslint/parser@7.7.0)(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.1)(eslint@8.57.0)
eslint-plugin-import: 2.29.1(@typescript-eslint/parser@7.7.0)(eslint-import-resolver-typescript@3.6.1)(eslint@8.57.0)
fast-glob: 3.3.2
get-tsconfig: 4.7.3
is-core-module: 2.13.1
is-glob: 4.0.3
eslint-import-resolver-node: 0.3.9
eslint-import-resolver-typescript: 3.6.1(@typescript-eslint/parser@7.12.0)(eslint-plugin-import@2.29.1)(eslint@8.57.0)
transitivePeerDependencies:
- '@typescript-eslint/parser'
- eslint-import-resolver-node
- eslint-import-resolver-webpack
- supports-color
dev: true
@@ -9659,36 +9723,6 @@ packages:
- supports-color
dev: true
/eslint-module-utils@2.8.1(@typescript-eslint/parser@7.7.0)(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.1)(eslint@8.57.0):
resolution: {integrity: sha512-rXDXR3h7cs7dy9RNpUlQf80nX31XWJEyGq1tRMo+6GsO5VmTe4UTwtmonAD4ZkAsrfMVDA2wlGJ3790Ys+D49Q==}
engines: {node: '>=4'}
peerDependencies:
'@typescript-eslint/parser': '*'
eslint: '*'
eslint-import-resolver-node: '*'
eslint-import-resolver-typescript: '*'
eslint-import-resolver-webpack: '*'
peerDependenciesMeta:
'@typescript-eslint/parser':
optional: true
eslint:
optional: true
eslint-import-resolver-node:
optional: true
eslint-import-resolver-typescript:
optional: true
eslint-import-resolver-webpack:
optional: true
dependencies:
'@typescript-eslint/parser': 7.7.0(eslint@8.57.0)(typescript@5.4.5)
debug: 3.2.7
eslint: 8.57.0
eslint-import-resolver-node: 0.3.9
eslint-import-resolver-typescript: 3.6.1(@typescript-eslint/parser@7.7.0)(eslint-plugin-import@2.29.1)(eslint@8.57.0)
transitivePeerDependencies:
- supports-color
dev: true
/eslint-plugin-es-x@7.6.0(eslint@8.57.0):
resolution: {integrity: sha512-I0AmeNgevgaTR7y2lrVCJmGYF0rjoznpDvqV/kIkZSZbZ8Rw3eu4cGlvBBULScfkSOCzqKbff5LR4CNrV7mZHA==}
engines: {node: ^14.18.0 || >=16.0.0}
@@ -9712,7 +9746,7 @@ packages:
ignore: 5.3.1
dev: true
/eslint-plugin-import@2.29.1(@typescript-eslint/parser@7.7.0)(eslint-import-resolver-typescript@3.6.1)(eslint@8.57.0):
/eslint-plugin-import@2.29.1(@typescript-eslint/parser@7.12.0)(eslint-import-resolver-typescript@3.6.1)(eslint@8.57.0):
resolution: {integrity: sha512-BbPC0cuExzhiMo4Ff1BTVwHpjjv28C5R+btTOGaCRC7UEz801up0JadwkeSk5Ued6TG34uaczuVuH6qyy5YUxw==}
engines: {node: '>=4'}
peerDependencies:
@@ -9722,7 +9756,7 @@ packages:
'@typescript-eslint/parser':
optional: true
dependencies:
'@typescript-eslint/parser': 7.7.0(eslint@8.57.0)(typescript@5.4.5)
'@typescript-eslint/parser': 7.12.0(eslint@8.57.0)(typescript@5.4.5)
array-includes: 3.1.7
array.prototype.findlastindex: 1.2.4
array.prototype.flat: 1.3.2
@@ -9731,7 +9765,7 @@ packages:
doctrine: 2.1.0
eslint: 8.57.0
eslint-import-resolver-node: 0.3.9
eslint-module-utils: 2.8.1(@typescript-eslint/parser@7.7.0)(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.1)(eslint@8.57.0)
eslint-module-utils: 2.8.1(@typescript-eslint/parser@7.12.0)(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.1)(eslint@8.57.0)
hasown: 2.0.2
is-core-module: 2.13.1
is-glob: 4.0.3
@@ -9760,7 +9794,7 @@ packages:
jest:
optional: true
dependencies:
'@typescript-eslint/eslint-plugin': 7.3.1(@typescript-eslint/parser@7.7.0)(eslint@8.57.0)(typescript@5.4.5)
'@typescript-eslint/eslint-plugin': 7.3.1(@typescript-eslint/parser@7.12.0)(eslint@8.57.0)(typescript@5.4.5)
'@typescript-eslint/utils': 5.62.0(eslint@8.57.0)(typescript@5.4.5)
eslint: 8.57.0
transitivePeerDependencies:
@@ -9968,7 +10002,7 @@ packages:
vitest:
optional: true
dependencies:
'@typescript-eslint/eslint-plugin': 7.3.1(@typescript-eslint/parser@7.7.0)(eslint@8.57.0)(typescript@5.4.5)
'@typescript-eslint/eslint-plugin': 7.3.1(@typescript-eslint/parser@7.12.0)(eslint@8.57.0)(typescript@5.4.5)
'@typescript-eslint/utils': 7.3.1(eslint@8.57.0)(typescript@5.4.5)
eslint: 8.57.0
transitivePeerDependencies:
@@ -12207,12 +12241,6 @@ packages:
engines: {node: '>=14'}
dev: false
/js-tiktoken@1.0.11:
resolution: {integrity: sha512-PajXFLq2vx7/8jllQZ43vzNpAai/0MOVdJjW/UrNyJorNQRTjHrqdGJG/mjHVy7h9M6dW6CaG43eNLMYFkTh6w==}
dependencies:
base64-js: 1.5.1
dev: false
/js-tiktoken@1.0.12:
resolution: {integrity: sha512-L7wURW1fH9Qaext0VzaUDpFGVQgjkdE3Dgsy9/+yXyGEpBKnylTd0mU0bfbNkKDlXRb6TEsZkwuflu1B8uQbJQ==}
dependencies:
@@ -12418,256 +12446,6 @@ packages:
resolution: {integrity: sha512-lG03Ru+XyOJFsjH3OMY6R/9U38IjDPfnOfDgO3ynhbDr+Dz8fak+X6L62vqu3iybQnj+lG84OttBuU9KY3L9kA==}
engines: {node: '>=14.0.0'}
/langchain@0.1.36:
resolution: {integrity: sha512-NTbnCL/jKWIeEI//Nm1oG8nhW3vkYWvEMr1MPotmTThTfeKfO87eV/OAzAyh6Ruy6GFs/qofRgQZGIe6XvXTNQ==}
engines: {node: '>=18'}
peerDependencies:
'@aws-sdk/client-s3': ^3.310.0
'@aws-sdk/client-sagemaker-runtime': ^3.310.0
'@aws-sdk/client-sfn': ^3.310.0
'@aws-sdk/credential-provider-node': ^3.388.0
'@azure/storage-blob': ^12.15.0
'@gomomento/sdk': ^1.51.1
'@gomomento/sdk-core': ^1.51.1
'@gomomento/sdk-web': ^1.51.1
'@google-ai/generativelanguage': ^0.2.1
'@google-cloud/storage': ^6.10.1 || ^7.7.0
'@mendable/firecrawl-js': ^0.0.13
'@notionhq/client': ^2.2.10
'@pinecone-database/pinecone': '*'
'@supabase/supabase-js': ^2.10.0
'@vercel/kv': ^0.2.3
'@xata.io/client': ^0.28.0
apify-client: ^2.7.1
assemblyai: ^4.0.0
axios: '*'
cheerio: ^1.0.0-rc.12
chromadb: '*'
convex: ^1.3.1
couchbase: ^4.3.0
d3-dsv: ^2.0.0
epub2: ^3.0.1
faiss-node: '*'
fast-xml-parser: '*'
google-auth-library: ^8.9.0
handlebars: ^4.7.8
html-to-text: ^9.0.5
ignore: ^5.2.0
ioredis: ^5.3.2
jsdom: '*'
mammoth: ^1.6.0
mongodb: '>=5.2.0'
node-llama-cpp: '*'
notion-to-md: ^3.1.0
officeparser: ^4.0.4
pdf-parse: 1.1.1
peggy: ^3.0.2
playwright: ^1.32.1
puppeteer: ^19.7.2
pyodide: ^0.24.1
redis: ^4.6.4
sonix-speech-recognition: ^2.1.1
srt-parser-2: ^1.2.3
typeorm: ^0.3.12
weaviate-ts-client: '*'
web-auth-library: ^1.0.3
ws: ^8.14.2
youtube-transcript: ^1.0.6
youtubei.js: ^9.1.0
peerDependenciesMeta:
'@aws-sdk/client-s3':
optional: true
'@aws-sdk/client-sagemaker-runtime':
optional: true
'@aws-sdk/client-sfn':
optional: true
'@aws-sdk/credential-provider-node':
optional: true
'@azure/storage-blob':
optional: true
'@gomomento/sdk':
optional: true
'@gomomento/sdk-core':
optional: true
'@gomomento/sdk-web':
optional: true
'@google-ai/generativelanguage':
optional: true
'@google-cloud/storage':
optional: true
'@mendable/firecrawl-js':
optional: true
'@notionhq/client':
optional: true
'@pinecone-database/pinecone':
optional: true
'@supabase/supabase-js':
optional: true
'@vercel/kv':
optional: true
'@xata.io/client':
optional: true
apify-client:
optional: true
assemblyai:
optional: true
axios:
optional: true
cheerio:
optional: true
chromadb:
optional: true
convex:
optional: true
couchbase:
optional: true
d3-dsv:
optional: true
epub2:
optional: true
faiss-node:
optional: true
fast-xml-parser:
optional: true
google-auth-library:
optional: true
handlebars:
optional: true
html-to-text:
optional: true
ignore:
optional: true
ioredis:
optional: true
jsdom:
optional: true
mammoth:
optional: true
mongodb:
optional: true
node-llama-cpp:
optional: true
notion-to-md:
optional: true
officeparser:
optional: true
pdf-parse:
optional: true
peggy:
optional: true
playwright:
optional: true
puppeteer:
optional: true
pyodide:
optional: true
redis:
optional: true
sonix-speech-recognition:
optional: true
srt-parser-2:
optional: true
typeorm:
optional: true
weaviate-ts-client:
optional: true
web-auth-library:
optional: true
ws:
optional: true
youtube-transcript:
optional: true
youtubei.js:
optional: true
dependencies:
'@anthropic-ai/sdk': 0.9.1
'@langchain/community': 0.0.47(@aws-sdk/credential-provider-node@3.549.0)(lodash@4.17.21)
'@langchain/core': 0.1.61
'@langchain/openai': 0.0.33
'@langchain/textsplitters': 0.0.0
binary-extensions: 2.3.0
js-tiktoken: 1.0.11
js-yaml: 4.1.0
jsonpointer: 5.0.1
langchainhub: 0.0.8
langsmith: 0.1.14
ml-distance: 4.0.1
openapi-types: 12.1.3
p-retry: 4.6.2
uuid: 9.0.1
yaml: 2.4.1
zod: 3.22.4
zod-to-json-schema: 3.22.5(zod@3.22.4)
transitivePeerDependencies:
- '@aws-crypto/sha256-js'
- '@aws-sdk/client-bedrock-agent-runtime'
- '@aws-sdk/client-bedrock-runtime'
- '@aws-sdk/client-dynamodb'
- '@aws-sdk/client-kendra'
- '@aws-sdk/client-lambda'
- '@azure/search-documents'
- '@clickhouse/client'
- '@cloudflare/ai'
- '@datastax/astra-db-ts'
- '@elastic/elasticsearch'
- '@getmetal/metal-sdk'
- '@getzep/zep-js'
- '@gradientai/nodejs-sdk'
- '@huggingface/inference'
- '@mozilla/readability'
- '@opensearch-project/opensearch'
- '@planetscale/database'
- '@premai/prem-sdk'
- '@qdrant/js-client-rest'
- '@raycast/api'
- '@rockset/client'
- '@smithy/eventstream-codec'
- '@smithy/protocol-http'
- '@smithy/signature-v4'
- '@smithy/util-utf8'
- '@supabase/postgrest-js'
- '@tensorflow-models/universal-sentence-encoder'
- '@tensorflow/tfjs-converter'
- '@tensorflow/tfjs-core'
- '@upstash/redis'
- '@upstash/vector'
- '@vercel/postgres'
- '@writerai/writer-sdk'
- '@xenova/transformers'
- '@zilliz/milvus2-sdk-node'
- better-sqlite3
- cassandra-driver
- cborg
- closevector-common
- closevector-node
- closevector-web
- cohere-ai
- discord.js
- dria
- duck-duck-scrape
- encoding
- firebase-admin
- googleapis
- hnswlib-node
- interface-datastore
- it-all
- jsonwebtoken
- llmonitor
- lodash
- lunary
- mysql2
- neo4j-driver
- pg
- pg-copy-streams
- pickleparser
- portkey-ai
- replicate
- typesense
- usearch
- vectordb
- voy-search
dev: false
/langchain@0.1.36(@aws-sdk/client-s3@3.550.0)(@aws-sdk/credential-provider-node@3.549.0)(lodash@4.17.21):
resolution: {integrity: sha512-NTbnCL/jKWIeEI//Nm1oG8nhW3vkYWvEMr1MPotmTThTfeKfO87eV/OAzAyh6Ruy6GFs/qofRgQZGIe6XvXTNQ==}
engines: {node: '>=18'}
@@ -16430,8 +16208,8 @@ packages:
resolution: {integrity: sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg==}
dev: true
/tiktoken@1.0.13:
resolution: {integrity: sha512-JaL9ZnvTbGFMDIBeGdVkLt4qWTeCPw+n7Ock+wceAGRenuHA6nOOvMJFliNDyXsjg2osGKJWsXtO2xc74VxyDw==}
/tiktoken@1.0.15:
resolution: {integrity: sha512-sCsrq/vMWUSEW29CJLNmPvWxlVp7yh2tlkAjpJltIKqp5CKf98ZNpdeHRmAlPVFlGEbswDc6SmI8vz64W/qErw==}
dev: false
/tiny-invariant@1.3.3:
@@ -17050,6 +16828,20 @@ packages:
engines: {node: '>= 0.8'}
dev: false
/vaul@0.9.1(@types/react-dom@18.2.25)(@types/react@18.2.79)(react-dom@18.2.0)(react@18.2.0):
resolution: {integrity: sha512-fAhd7i4RNMinx+WEm6pF3nOl78DFkAazcN04ElLPFF9BMCNGbY/kou8UMhIcicm0rJCNePJP0Yyza60gGOD0Jw==}
peerDependencies:
react: ^16.8 || ^17.0 || ^18.0
react-dom: ^16.8 || ^17.0 || ^18.0
dependencies:
'@radix-ui/react-dialog': 1.0.5(@types/react-dom@18.2.25)(@types/react@18.2.79)(react-dom@18.2.0)(react@18.2.0)
react: 18.2.0
react-dom: 18.2.0(react@18.2.0)
transitivePeerDependencies:
- '@types/react'
- '@types/react-dom'
dev: false
/victory-vendor@36.9.2:
resolution: {integrity: sha512-PnpQQMuxlwYdocC8fIJqVXvkeViHYzotI+NJrCuav0ZYFoq912ZHBk3mCeuj+5/VpodOjPe1z0Fk2ihgzlXqjQ==}
dependencies:
+1 -1
View File
@@ -5,7 +5,7 @@ FROM node:20-alpine AS alpine
RUN apk update && apk upgrade --no-cache libcrypto3 libssl3 libc6-compat
FROM alpine AS base
RUN npm install turbo --global
RUN npm install turbo@^1.13.3 --global
ENV PNPM_HOME="/pnpm"
ENV PATH="$PNPM_HOME:$PATH"
RUN corepack enable
+4 -3
View File
@@ -1,6 +1,6 @@
{
"name": "web",
"version": "2.47.4",
"version": "2.49.2",
"private": true,
"license": "MIT",
"engines": {
@@ -56,6 +56,7 @@
"@radix-ui/react-switch": "^1.0.3",
"@radix-ui/react-tabs": "^1.0.4",
"@radix-ui/react-toggle": "^1.0.3",
"@radix-ui/react-toggle-group": "^1.0.4",
"@radix-ui/react-tooltip": "^1.0.7",
"@react-email/components": "^0.0.18",
"@react-email/render": "^0.0.14",
@@ -70,7 +71,6 @@
"@tailwindcss/container-queries": "^0.1.1",
"@tanstack/react-query": "^4.36.1",
"@tanstack/react-table": "^8.11.8",
"@tanstack/react-virtual": "^3.5.0",
"@tremor/react": "3.16.2",
"@trpc/client": "^10.45.0",
"@trpc/next": "^10.45.0",
@@ -116,6 +116,7 @@
"tailwindcss-animate": "^1.0.7",
"use-query-params": "^2.2.1",
"uuid": "^9.0.1",
"vaul": "^0.9.1",
"zod": "^3.22.4"
},
"devDependencies": {
@@ -137,7 +138,7 @@
"@types/react-dom": "^18.2.25",
"@types/uuid": "^9.0.8",
"@typescript-eslint/eslint-plugin": "^6.21.0",
"@typescript-eslint/parser": "^7.7.0",
"@typescript-eslint/parser": "^7.12.0",
"autoprefixer": "^10.4.19",
"dotenv-cli": "^7.4.2",
"eslint": "^8.56.0",
+25 -1
View File
@@ -1137,7 +1137,10 @@ paths:
security: *ref_0
/api/public/sessions/{sessionId}:
get:
description: Get a session
description: >-
Get a session. Please note that `traces` on this endpoint are not
paginated, if you plan to fetch large sessions, consider `GET
/api/public/traces?sessionId=<sessionId>`
operationId: sessions_get
tags:
- Sessions
@@ -1262,6 +1265,12 @@ paths:
schema:
type: string
nullable: true
- name: sessionId
in: query
required: false
schema:
type: string
nullable: true
- name: fromTimestamp
in: query
description: Retrieve only traces newer than this datetime (ISO 8601).
@@ -2499,6 +2508,7 @@ components:
totalCost:
type: number
format: double
description: Total model cost in USD
usage:
type: array
items:
@@ -2521,14 +2531,28 @@ components:
nullable: true
inputUsage:
type: integer
description: Total number of generation input units (e.g. tokens)
outputUsage:
type: integer
description: Total number of generation output units (e.g. tokens)
totalUsage:
type: integer
description: Total number of generation total units (e.g. tokens)
countTraces:
type: integer
countObservations:
type: integer
totalCost:
type: number
format: double
description: Total model cost in USD
required:
- inputUsage
- outputUsage
- totalUsage
- countTraces
- countObservations
- totalCost
Observations:
title: Observations
type: object
+7 -2
View File
@@ -890,7 +890,7 @@
"_type": "endpoint",
"name": "Get",
"request": {
"description": "Get a session",
"description": "Get a session. Please note that `traces` on this endpoint are not paginated, if you plan to fetch large sessions, consider `GET /api/public/traces?sessionId=<sessionId>`",
"url": {
"raw": "{{baseUrl}}/api/public/sessions/:sessionId",
"host": [
@@ -963,7 +963,7 @@
"request": {
"description": "Get list of traces.",
"url": {
"raw": "{{baseUrl}}/api/public/traces?page=&limit=&userId=&name=&fromTimestamp=&orderBy=&tags=",
"raw": "{{baseUrl}}/api/public/traces?page=&limit=&userId=&name=&sessionId=&fromTimestamp=&orderBy=&tags=",
"host": [
"{{baseUrl}}"
],
@@ -993,6 +993,11 @@
"value": "",
"description": null
},
{
"key": "sessionId",
"value": "",
"description": null
},
{
"key": "fromTimestamp",
"value": "",
+1 -1
View File
@@ -2,7 +2,7 @@ import { prisma } from "@langfuse/shared/src/db";
import * as Sentry from "@sentry/nextjs";
import { ProfilingIntegration } from "@sentry/profiling-node";
import type { SamplingContext, TransactionEvent } from "@sentry/types";
import { jsonSchema } from "@/src/utils/zod";
import { jsonSchema } from "@langfuse/shared";
if (process.env.NEXT_PUBLIC_SENTRY_DSN)
Sentry.init({
@@ -68,12 +68,18 @@ describe("/api/public/metrics/daily API Endpoint", () => {
inputUsage: 333,
outputUsage: 0,
totalUsage: 333,
countObservations: 1,
countTraces: 1,
totalCost: 0,
},
{
model: "modelC",
inputUsage: 666,
outputUsage: 777,
totalUsage: 1443,
countObservations: 1,
countTraces: 1,
totalCost: 1024.22,
},
]);
@@ -88,6 +94,9 @@ describe("/api/public/metrics/daily API Endpoint", () => {
inputUsage: 100,
outputUsage: 200,
totalUsage: 300,
countObservations: 1,
countTraces: 1,
totalCost: 0,
},
]);
});
@@ -50,7 +50,7 @@ describe("Build valid SQL queries", () => {
table: "traces_scores",
values: ["project-id"],
strings: [
' FROM traces t JOIN scores s ON t.id = s.trace_id AND t.project_id = s.project_id WHERE t."project_id" = ',
` FROM traces t JOIN scores s ON t.id = s.trace_id AND s.data_type != 'CATEGORICAL' AND t.project_id = s.project_id WHERE t."project_id" = `,
";",
],
} as const,
+58
View File
@@ -238,4 +238,62 @@ describe("/api/public/traces API Endpoint", () => {
expect(trace.body.htmlPath).toContain(`/traces/${traceId}`);
expect(trace.body.htmlPath).toContain(`/project/`); // do not know the projectId
});
it("should filter traces by session ID", async () => {
const sessionId = "test-session-id";
const anotherSessionId = "another-session-id";
// Create traces with different session IDs
await makeAPICall("POST", "/api/public/traces", {
id: "trace-1",
name: "test-trace-1",
sessionId,
userId: "user-1",
projectId: "project-1",
metadata: { key: "value" },
release: "1.0.0",
version: "1.0.0",
});
await makeAPICall("POST", "/api/public/traces", {
id: "trace-2",
name: "test-trace-2",
sessionId: anotherSessionId,
userId: "user-2",
projectId: "project-1",
metadata: { key: "value" },
release: "1.0.0",
version: "1.0.0",
});
// Filter by session ID
const tracesBySessionId = await makeAPICall<GetTracesAPIResponse>(
"GET",
`/api/public/traces?sessionId=${sessionId}`,
);
expect(tracesBySessionId.status).toBe(200);
expect(tracesBySessionId.body.data).toHaveLength(1);
expect(tracesBySessionId.body.data[0].id).toBe("trace-1");
// Filter by another session ID
const tracesByAnotherSessionId = await makeAPICall<GetTracesAPIResponse>(
"GET",
`/api/public/traces?sessionId=${anotherSessionId}`,
);
expect(tracesByAnotherSessionId.status).toBe(200);
expect(tracesByAnotherSessionId.body.data).toHaveLength(1);
expect(tracesByAnotherSessionId.body.data[0].id).toBe("trace-2");
// Filter by non-existent session ID
const tracesByNonExistentSessionId =
await makeAPICall<GetTracesAPIResponse>(
"GET",
`/api/public/traces?sessionId=non-existent-session-id`,
);
expect(tracesByNonExistentSessionId.status).toBe(200);
expect(tracesByNonExistentSessionId.body.data).toHaveLength(0);
});
});
+1 -1
View File
@@ -1,4 +1,4 @@
import { paginationZod } from "@/src/utils/zod";
import { paginationZod } from "@langfuse/shared";
import { ZodError } from "zod";
// Create test cases
+100 -13
View File
@@ -1,8 +1,105 @@
import { AlertTriangle, Check } from "lucide-react";
import {
AlertTriangle,
Check,
Github,
HardDriveDownload,
Newspaper,
} from "lucide-react";
import { VERSION } from "@/src/constants";
import { env } from "@/src/env.mjs";
import { cn } from "@/src/utils/tailwind";
import {
DropdownMenu,
DropdownMenuTrigger,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuLabel,
DropdownMenuSeparator,
} from "@/src/components/ui/dropdown-menu";
import { ArrowUp } from "lucide-react";
import { api } from "@/src/utils/api";
import { Button } from "@/src/components/ui/button";
import Link from "next/link";
const VersionLabel = ({ className }: { className?: string }) => {
const checkUpdate = api.public.checkUpdate.useQuery(undefined, {
refetchOnMount: false,
refetchOnWindowFocus: false,
refetchOnReconnect: false,
enabled: !env.NEXT_PUBLIC_LANGFUSE_CLOUD_REGION, // do not check for updates on Langfuse Cloud
onError: (error) => console.error("checkUpdate error", error), // do not render default error message
});
const hasUpdate =
!env.NEXT_PUBLIC_LANGFUSE_CLOUD_REGION &&
checkUpdate.data &&
checkUpdate.data.updateType;
const color =
checkUpdate.data?.updateType === "major"
? "text-dark-red"
: checkUpdate.data?.updateType === "minor"
? "text-dark-yellow"
: undefined;
return (
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="ghost" size="xs" className={className}>
{VERSION}
{hasUpdate && <ArrowUp className={`ml-1 h-3 w-3 ${color}`} />}
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent>
{hasUpdate ? (
<>
<DropdownMenuLabel>
New {checkUpdate.data?.updateType} version:{" "}
{checkUpdate.data?.latestRelease}
</DropdownMenuLabel>
<DropdownMenuSeparator />
</>
) : !env.NEXT_PUBLIC_LANGFUSE_CLOUD_REGION ? (
<>
<DropdownMenuLabel>This is the latest release</DropdownMenuLabel>
<DropdownMenuSeparator />
</>
) : null}
<DropdownMenuItem asChild>
<Link
href="https://github.com/langfuse/langfuse/releases"
target="_blank"
>
<Github size={16} className="mr-2" />
Releases
</Link>
</DropdownMenuItem>
<DropdownMenuItem asChild>
<Link href="https://langfuse.com/changelog" target="_blank">
<Newspaper size={16} className="mr-2" />
Changelog
</Link>
</DropdownMenuItem>
{hasUpdate && (
<>
<DropdownMenuSeparator />
<DropdownMenuItem asChild>
<Link
href="https://langfuse.com/docs/deployment/self-host#update"
target="_blank"
>
<HardDriveDownload size={16} className="mr-2" />
Update
</Link>
</DropdownMenuItem>
</>
)}
</DropdownMenuContent>
</DropdownMenu>
);
};
export const LangfuseIcon = ({
size = 32,
@@ -65,22 +162,12 @@ export const LangfuseLogo = ({
<span
className={cn(
"ml-2 font-mono font-semibold",
size === "sm" ? "text-sm" : "text-xl",
size === "sm" ? "text-sm" : "text-lg",
)}
>
Langfuse
</span>
{version && (
<a
href="https://github.com/langfuse/langfuse/releases"
target="_blank"
rel="noopener"
title="View releases on GitHub"
className="ml-2 text-xs text-muted-foreground"
>
{VERSION}
</a>
)}
{version && <VersionLabel className="ml-2" />}
</div>
</div>
);
+1 -1
View File
@@ -67,7 +67,7 @@ const supportChannels = [
title: "Slack Connect",
description: "Get a dedicated support channel for you and your team.",
href: "mailto:help@langfuse.com?subject=Slack%20Connect%20Request&body=I'd%20like%20to%20request%20a%20dedicated%20Slack%20Connect%20channel%20for%20me%20and%20my%20team.%0D%0A%0D%0AUsers%20(emails)%20to%20include%20besides%20mine%3A%0D%0A%0D%0A",
buttonText: "Request Slack Connect (via Email)",
buttonText: "Request via Email",
},
];
+1 -4
View File
@@ -53,16 +53,13 @@ export function DeleteButton({
},
});
if (!hasAccess) {
return null;
}
return (
<Popover key={itemId}>
<PopoverTrigger asChild>
<Button
variant={isTableAction ? "ghost" : "outline"}
size={isTableAction ? "xs" : "icon"}
disabled={!hasAccess}
onClick={() =>
type === "trace"
? capture("trace:delete_form_open", {
+14 -5
View File
@@ -4,9 +4,13 @@ import {
HoverCardContent,
HoverCardTrigger,
} from "@/src/components/ui/hover-card";
import {
isBooleanDataType,
isCategoricalDataType,
} from "@/src/features/manual-scoring/lib/helpers";
import { type ScoreSimplified } from "@/src/server/api/routers/generations/getAllQuery";
import { cn } from "@/src/utils/tailwind";
import { MessageCircle } from "lucide-react";
import { MessageCircleMore } from "lucide-react";
export const GroupedScoreBadges = ({
scores,
@@ -34,11 +38,13 @@ export const GroupedScoreBadges = ({
<div className={cn("text-xs", props.className)}>
{props.scores.map((s, i) => (
<span key={i} className="group/score ml-1 first:ml-0">
{s.value.toFixed(2)}
{isCategoricalDataType(s.dataType) || isBooleanDataType(s.dataType)
? s.stringValue
: s.value.toFixed(2)}
{s.comment && (
<HoverCard>
<HoverCardTrigger className="ml-1 inline-block cursor-pointer">
<MessageCircle size={12} />
<MessageCircleMore size={12} />
</HoverCardTrigger>
<HoverCardContent className="overflow-hidden whitespace-normal break-normal">
<p>{s.comment}</p>
@@ -73,9 +79,12 @@ export const GroupedScoreBadges = ({
<Badge
variant="outline"
key={name}
className="break-all font-normal"
className="grid grid-cols-[1fr,auto] gap-1 font-normal"
>
{name}: <ScoresOfGroup scores={scores} className="ml-2" />
<p className="truncate" title={name}>
{name}:
</p>
<ScoresOfGroup scores={scores} />
</Badge>
))}
</>
+11 -5
View File
@@ -521,9 +521,9 @@ const MainNavigation: React.FC<{
onNavitemClick?: () => void;
className?: string;
}> = ({ nav, onNavitemClick, className }) => {
const [isOpen, setIsOpen] = useLocalStorage(
"sidebar-tracing-default-open",
false,
const [isOpen, setIsOpen] = useLocalStorage<Record<string, boolean>>(
"sidebar-item-default-open",
{},
);
return (
@@ -572,14 +572,20 @@ const MainNavigation: React.FC<{
<Disclosure
as="div"
defaultOpen={
item.children.some((child) => child.current) || isOpen
item.children.some((child) => child.current) ||
isOpen[item.name]
}
>
{({ open }) => (
<>
<Disclosure.Button
className="group flex w-full items-center gap-x-3 rounded-md p-2 text-left text-sm font-semibold hover:bg-primary-foreground hover:text-primary-accent"
onClick={() => setIsOpen(!isOpen)}
onClick={() =>
setIsOpen((prev) => ({
...prev,
[item.name]: !prev[item.name],
}))
}
>
{item.icon && (
<item.icon
+50 -118
View File
@@ -7,38 +7,24 @@ import { IOPreview } from "@/src/components/trace/IOPreview";
import { JsonSkeleton } from "@/src/components/ui/CodeJsonViewer";
import { Badge } from "@/src/components/ui/badge";
import { Card } from "@/src/components/ui/card";
import { ManualScoreButton } from "@/src/features/manual-scoring/components/ManualScoreButton";
import { DetailPageNav } from "@/src/features/navigate-detail-pages/DetailPageNav";
import { useDetailPageLists } from "@/src/features/navigate-detail-pages/context";
import { api } from "@/src/utils/api";
import { usdFormatter } from "@/src/utils/numbers";
import Link from "next/link";
import { useEffect, useRef } from "react";
import { useWindowVirtualizer } from "@tanstack/react-virtual";
import { type RouterOutput } from "@/src/utils/types";
import {
DataTableRowHeightSwitch,
type RowHeight,
useRowHeightLocalStorage,
} from "@/src/components/table/data-table-row-height-switch";
import { ScrollArea } from "@/src/components/ui/scroll-area";
import { useEffect, useState } from "react";
import { AnnotateButton } from "@/src/features/manual-scoring/components/AnnotateButton";
import { Button } from "@/src/components/ui/button";
// do not use the usual table row heights here
const rowHeightMapping: Record<RowHeight, number> = {
s: 200,
m: 350,
l: 700,
};
// some projects have thousands of traces in a sessions, paginate to avoid rendering all at once
const PAGE_SIZE = 50;
export const SessionPage: React.FC<{
sessionId: string;
projectId: string;
}> = ({ sessionId, projectId }) => {
const { setDetailPageList } = useDetailPageLists();
const [rowHeight, setRowHeight] = useRowHeightLocalStorage(
"single-session",
"m",
);
const [visibleTraces, setVisibleTraces] = useState(PAGE_SIZE);
const session = api.sessions.byId.useQuery(
{
sessionId,
@@ -96,11 +82,6 @@ export const SessionPage: React.FC<{
}
listKey="sessions"
/>,
<DataTableRowHeightSwitch
rowHeight={rowHeight}
setRowHeight={setRowHeight}
key="height"
/>,
]}
/>
<div className="flex flex-wrap gap-2">
@@ -121,97 +102,49 @@ export const SessionPage: React.FC<{
</Badge>
)}
</div>
{session.data && (
<TraceCardList
session={session.data}
projectId={projectId}
rowHeight={rowHeightMapping[rowHeight]}
/>
)}
</div>
);
};
const TraceCardList = ({
session,
projectId,
rowHeight,
}: {
session: RouterOutput["sessions"]["byId"];
projectId: string;
rowHeight: number;
}) => {
const listVirtualizationRef = useRef<HTMLDivElement | null>(null);
const virtualizer = useWindowVirtualizer({
count: session.traces.length,
estimateSize: () => rowHeight,
overscan: 5,
scrollMargin: listVirtualizationRef.current?.offsetTop ?? 0,
gap: 10,
});
useEffect(() => {
// re-measure when rowHeight changes to update the virtualizer
virtualizer.measure();
}, [rowHeight, virtualizer]);
return (
<div className="mt-5 border-t pt-5">
<div
ref={listVirtualizationRef}
style={{
height: `${virtualizer.getTotalSize()}px`,
position: "relative",
}}
>
{virtualizer
.getVirtualItems()
.map((virtualItem) => ({
virtualItem,
trace: session.traces[virtualItem.index],
}))
.map(({ virtualItem, trace }) => (
<Card
className="group grid w-full gap-3 overflow-hidden border-border p-2 shadow-none hover:border-ring md:grid-cols-3"
key={virtualItem.key}
data-index={virtualItem.index}
style={{
position: "absolute",
top: 0,
left: 0,
height: `${rowHeight}px`,
transform: `translateY(${virtualItem.start - virtualizer.options.scrollMargin}px)`,
}}
>
<ScrollArea className="col-span-2 pr-3">
<SessionIO traceId={trace.id} />
</ScrollArea>
<ScrollArea>
<div className="-mt-1 overflow-y-auto p-1 opacity-50 transition-opacity group-hover:opacity-100">
<Link
href={`/project/${projectId}/traces/${trace.id}`}
className="text-xs hover:underline"
>
Trace: {trace.name} ({trace.id})&nbsp;
</Link>
<div className="text-xs text-muted-foreground">
{trace.timestamp.toLocaleString()}
</div>
<div className="mb-1 mt-2 text-xs text-muted-foreground">
Scores
</div>
<div className="flex flex-wrap content-start items-start gap-1">
<GroupedScoreBadges scores={trace.scores} />
</div>
<ManualScoreButton
projectId={projectId}
traceId={trace.id}
scores={trace.scores}
variant="badge"
/>
</div>
</ScrollArea>
</Card>
))}
<div className="mt-5 flex flex-col gap-2 border-t pt-5">
{session.data?.traces.slice(0, visibleTraces).map((trace) => (
<Card
className="group grid gap-3 border-border p-2 shadow-none hover:border-ring md:grid-cols-3"
key={trace.id}
>
<SessionIO traceId={trace.id} />
<div className="-mt-1 p-1 opacity-50 transition-opacity group-hover:opacity-100">
<Link
href={`/project/${projectId}/traces/${trace.id}`}
className="text-xs hover:underline"
>
Trace: {trace.name} ({trace.id})&nbsp;
</Link>
<div className="text-xs text-muted-foreground">
{trace.timestamp.toLocaleString()}
</div>
<div className="mb-1 mt-2 text-xs text-muted-foreground">
Scores
</div>
<div className="mb-1 flex flex-wrap content-start items-start gap-1">
<GroupedScoreBadges scores={trace.scores} />
</div>
<AnnotateButton
projectId={projectId}
traceId={trace.id}
scores={trace.scores}
variant="badge"
type="session"
source="SessionDetail"
/>
</div>
</Card>
))}
{session.data?.traces && session.data.traces.length > visibleTraces && (
<Button
onClick={() => setVisibleTraces((prev) => prev + PAGE_SIZE)}
variant="ghost"
className="self-center"
>
{`Load ${Math.min(session.data.traces.length - visibleTraces, PAGE_SIZE)} More`}
</Button>
)}
</div>
</div>
);
@@ -230,9 +163,8 @@ const SessionIO = ({ traceId }: { traceId: string }) => {
refetchOnMount: false, // prevents refetching loops
},
);
return (
<div className="flex flex-col gap-2 overflow-x-hidden overflow-y-scroll p-0">
<div className="col-span-2 flex flex-col gap-2 p-0">
{!trace.data ? (
<JsonSkeleton
className="h-full w-full overflow-hidden px-2 py-1"
+23 -3
View File
@@ -50,6 +50,8 @@ interface DataTableProps<TData, TValue> {
help?: { description: string; href: string };
rowHeight?: RowHeight;
className?: string;
paginationClassName?: string;
isBorderless?: boolean;
}
export interface AsyncTableData<T> {
@@ -71,6 +73,9 @@ export function DataTable<TData extends object, TValue>({
orderBy,
setOrderBy,
rowHeight,
className,
paginationClassName,
isBorderless = false,
}: DataTableProps<TData, TValue>) {
const [columnFilters, setColumnFilters] = useState<ColumnFiltersState>([]);
const rowheighttw = getRowHeightTailwindClass(rowHeight);
@@ -105,8 +110,18 @@ export function DataTable<TData extends object, TValue>({
return (
<>
<div className="flex w-full max-w-full flex-1 flex-col gap-1 overflow-auto">
<div className="w-full overflow-auto rounded-md border">
<div
className={cn(
"flex w-full max-w-full flex-1 flex-col gap-1 overflow-auto",
className,
)}
>
<div
className={cn(
"w-full overflow-auto",
isBorderless ? "" : "rounded-md border",
)}
>
<Table>
<TableHeader>
{table.getHeaderGroups().map((headerGroup) => (
@@ -242,7 +257,12 @@ export function DataTable<TData extends object, TValue>({
<div className="grow"></div>
</div>
{pagination !== undefined ? (
<div className="bg:background sticky bottom-0 z-10 flex w-full justify-end font-medium">
<div
className={cn(
"sticky bottom-0 z-10 flex w-full justify-end bg-background font-medium",
paginationClassName,
)}
>
<DataTablePagination
table={table}
paginationOptions={pagination.options}
+54 -29
View File
@@ -1,6 +1,12 @@
import { DataTable } from "@/src/components/table/data-table";
import { type LangfuseColumnDef } from "@/src/components/table/types";
import { Button } from "@/src/components/ui/button";
import {
Popover,
PopoverContent,
PopoverTrigger,
} from "@/src/components/ui/popover";
import { useState } from "react";
import useColumnVisibility from "@/src/features/column-visibility/hooks/useColumnVisibility";
import { usePostHogClientCapture } from "@/src/features/posthog-analytics/usePostHogClientCapture";
import { useHasAccess } from "@/src/features/rbac/utils/checkAccess";
@@ -10,6 +16,7 @@ import { type Prisma, type Model } from "@langfuse/shared/src/db";
import Decimal from "decimal.js";
import { Trash } from "lucide-react";
import { useQueryParams, withDefault, NumberParam } from "use-query-params";
import { cn } from "@/src/utils/tailwind";
export type ModelTableRow = {
modelId: string;
@@ -222,13 +229,12 @@ export default function ModelTable({ projectId }: { projectId: string }) {
accessorKey: "actions",
header: "Actions",
cell: ({ row }) => {
return row.original.maintainer === "User" ? (
return (
<DeleteModelButton
projectId={projectId}
modelId={row.original.modelId}
isBuiltIn={row.original.maintainer === "Langfuse"}
/>
) : (
<div className="h-6" />
);
},
},
@@ -287,10 +293,13 @@ export default function ModelTable({ projectId }: { projectId: string }) {
const DeleteModelButton = ({
modelId,
projectId,
isBuiltIn,
}: {
modelId: string;
projectId: string;
isBuiltIn?: boolean;
}) => {
const [isOpen, setIsOpen] = useState(false);
const utils = api.useUtils();
const capture = usePostHogClientCapture();
const mut = api.models.delete.useMutation({
@@ -304,32 +313,48 @@ const DeleteModelButton = ({
scope: "models:CUD",
});
if (!hasAccess) {
return null;
}
return (
<Button
size="xs"
variant="ghost"
onClick={() => {
const confirmDelete = window.confirm(
"Are you sure you want to delete this model?",
);
if (confirmDelete) {
capture("models:delete_button_click");
mut
.mutateAsync({
projectId,
modelId,
})
.catch((error) => {
console.error(error);
});
}
}}
>
<Trash size={14} />
</Button>
<Popover open={isOpen} onOpenChange={() => setIsOpen(!isOpen)}>
<PopoverTrigger asChild>
<Button
variant="ghost"
size="xs"
disabled={!hasAccess || isBuiltIn}
title={
isBuiltIn ? "Built-in models cannot be deleted" : "Delete model"
}
className={cn(
isBuiltIn &&
"disabled:pointer-events-auto disabled:cursor-not-allowed",
)}
>
<Trash className="h-4 w-4" />
</Button>
</PopoverTrigger>
<PopoverContent>
<h2 className="text-md mb-3 font-semibold">Please confirm</h2>
<p className="mb-3 text-sm">
This action permanently deletes this model definition.
</p>
<div className="flex justify-end space-x-4">
<Button
type="button"
variant="destructive"
loading={mut.isLoading}
onClick={() => {
capture("models:delete_button_click");
mut.mutateAsync({
projectId,
modelId,
});
setIsOpen(false);
}}
>
Delete Model
</Button>
</div>
</PopoverContent>
</Popover>
);
};
@@ -0,0 +1,174 @@
import React from "react";
import { Card } from "@/src/components/ui/card";
import { useRowHeightLocalStorage } from "@/src/components/table/data-table-row-height-switch";
import useColumnVisibility from "@/src/features/column-visibility/hooks/useColumnVisibility";
import { api } from "@/src/utils/api";
import { type LangfuseColumnDef } from "@/src/components/table/types";
import { DataTableToolbar } from "@/src/components/table/data-table-toolbar";
import { DataTable } from "@/src/components/table/data-table";
import { type ScoreDataType, type Prisma } from "@langfuse/shared";
import { IOTableCell } from "@/src/components/ui/CodeJsonViewer";
import { NumberParam, useQueryParams, withDefault } from "use-query-params";
import { isNumericDataType } from "@/src/features/manual-scoring/lib/helpers";
type ScoreConfigTableRow = {
id: string;
name: string;
dataType: ScoreDataType;
createdAt: string;
updatedAt: string;
range: {
maxValue?: number | null;
minValue?: number | null;
categories?: Prisma.JsonValue | null;
};
description?: string | null;
};
function getConfigRange(
originalRow: ScoreConfigTableRow,
): Prisma.JsonValue | undefined {
const { range, dataType } = originalRow;
if (isNumericDataType(dataType)) {
return [
{ minValue: range.minValue ?? "-∞", maxValue: range.maxValue ?? "∞" },
];
}
return range.categories;
}
export function ScoreConfigsTable({ projectId }: { projectId: string }) {
const [paginationState, setPaginationState] = useQueryParams({
pageIndex: withDefault(NumberParam, 0),
pageSize: withDefault(NumberParam, 50),
});
const [rowHeight, setRowHeight] = useRowHeightLocalStorage(
"scoreConfigs",
"s",
);
const configs = api.scoreConfigs.all.useQuery({
projectId,
page: paginationState.pageIndex,
limit: paginationState.pageSize,
});
const totalCount = configs.data?.totalCount ?? 0;
const columns: LangfuseColumnDef<ScoreConfigTableRow>[] = [
{
accessorKey: "name",
id: "name",
header: "Name",
enableHiding: true,
},
{
accessorKey: "dataType",
id: "dataType",
header: "Data Type",
enableHiding: true,
},
{
accessorKey: "range",
id: "range",
header: "Range",
enableHiding: true,
cell: ({ row }) => {
const range = getConfigRange(row.original);
return !!range ? (
<IOTableCell data={range} singleLine={rowHeight === "s"} />
) : null;
},
},
{
accessorKey: "description",
id: "description",
header: "Description",
enableHiding: true,
cell: ({ row }) => {
const value = row.original.description;
return !!value ? (
<IOTableCell data={value} singleLine={rowHeight === "s"} />
) : null;
},
},
{
accessorKey: "id",
id: "id",
header: "Config ID",
enableHiding: true,
defaultHidden: true,
},
{
accessorKey: "createdAt",
id: "createdAt",
header: "Created At",
enableHiding: true,
defaultHidden: true,
},
];
const [columnVisibility, setColumnVisibility] =
useColumnVisibility<ScoreConfigTableRow>(
"scoreConfigsColumnVisibility",
columns,
);
return (
<>
<DataTableToolbar
columns={columns}
columnVisibility={columnVisibility}
setColumnVisibility={setColumnVisibility}
rowHeight={rowHeight}
setRowHeight={setRowHeight}
/>
<Card className="mb-4 flex max-h-[calc(100dvh-40rem)] flex-col overflow-hidden">
<DataTable
columns={columns}
data={
configs.isLoading
? { isLoading: true, isError: false }
: configs.isError
? {
isLoading: false,
isError: true,
error: configs.error.message,
}
: {
isLoading: false,
isError: false,
data: configs.data?.configs.map((config) => ({
id: config.id,
name: config.name,
dataType: config.dataType,
description: config.description,
createdAt: config.createdAt.toLocaleString(),
updatedAt: config.updatedAt.toLocaleString(),
range: {
maxValue: config.maxValue,
minValue: config.minValue,
categories: config.categories,
},
})),
}
}
pagination={{
pageCount: Math.ceil(totalCount / paginationState.pageSize),
onChange: setPaginationState,
state: paginationState,
}}
columnVisibility={columnVisibility}
onColumnVisibilityChange={setColumnVisibility}
rowHeight={rowHeight}
className="gap-2"
paginationClassName="-mx-2 mb-2"
isBorderless
/>
</Card>
</>
);
}
+67 -27
View File
@@ -4,8 +4,10 @@ import { DataTableToolbar } from "@/src/components/table/data-table-toolbar";
import TableLink from "@/src/components/table/table-link";
import { type LangfuseColumnDef } from "@/src/components/table/types";
import { IOTableCell } from "@/src/components/ui/CodeJsonViewer";
import { Avatar, AvatarImage } from "@/src/components/ui/avatar";
import useColumnVisibility from "@/src/features/column-visibility/hooks/useColumnVisibility";
import { useQueryFilterState } from "@/src/features/filters/hooks/useFilterState";
import { isNumericDataType } from "@/src/features/manual-scoring/lib/helpers";
import { useOrderByState } from "@/src/features/orderBy/hooks/useOrderByState";
import {
type ScoreOptions,
@@ -14,7 +16,7 @@ import {
import { api } from "@/src/utils/api";
import { utcDateOffsetByDays } from "@/src/utils/dates";
import type { RouterOutput, RouterInput } from "@/src/utils/types";
import type { FilterState } from "@langfuse/shared";
import type { FilterState, ScoreDataType } from "@langfuse/shared";
import { useSession } from "next-auth/react";
import { useQueryParams, withDefault, NumberParam } from "use-query-params";
@@ -24,7 +26,12 @@ export type ScoresTableRow = {
timestamp: string;
source: string;
name: string;
value: number;
dataType: ScoreDataType;
value: string;
author: {
image?: string;
name?: string;
};
comment?: string;
observationId?: string;
traceName?: string;
@@ -172,6 +179,29 @@ export default function ScoresTable({
) : undefined;
},
},
{
accessorKey: "userId",
header: "Trace User ID",
id: "userId",
headerTooltip: {
description: "The user ID associated with the trace.",
href: "https://langfuse.com/docs/tracing-features/users",
},
enableHiding: true,
enableSorting: true,
cell: ({ row }) => {
const value = row.getValue("userId");
return typeof value === "string" ? (
<>
<TableLink
path={`/project/${projectId}/users/${value}`}
value={value}
truncateAt={40}
/>
</>
) : undefined;
},
},
{
accessorKey: "timestamp",
header: "Timestamp",
@@ -193,38 +223,39 @@ export default function ScoresTable({
enableHiding: true,
enableSorting: true,
},
{
accessorKey: "dataType",
header: "Data Type",
id: "dataType",
enableHiding: true,
enableSorting: true,
},
{
accessorKey: "value",
header: "Value",
id: "value",
enableHiding: true,
enableSorting: true,
cell: ({ row }) => {
const value: number = row.getValue("value");
return value % 1 === 0 ? value : value.toFixed(4);
},
},
{
accessorKey: "userId",
header: "User ID",
id: "userId",
headerTooltip: {
description: "The user ID associated with the trace.",
href: "https://langfuse.com/docs/tracing-features/users",
},
enableHiding: true,
enableSorting: true,
accessorKey: "author",
id: "author",
header: "Author",
cell: ({ row }) => {
const value = row.getValue("userId");
return typeof value === "string" ? (
<>
<TableLink
path={`/project/${projectId}/users/${value}`}
value={value}
truncateAt={40}
/>
</>
) : undefined;
const { name, image } = row.getValue(
"author",
) as ScoresTableRow["author"];
return (
<div className="flex items-center space-x-2">
<Avatar className="h-7 w-7">
<AvatarImage
src={image ?? undefined}
alt={name ?? "User Avatar"}
/>
</Avatar>
<span>{name}</span>
</div>
);
},
},
{
@@ -279,12 +310,21 @@ export default function ScoresTable({
timestamp: score.timestamp.toLocaleString(),
source: score.source,
name: score.name,
value: score.value,
dataType: score.dataType,
value: isNumericDataType(score.dataType)
? score.value % 1 === 0
? String(score.value)
: score.value.toFixed(4)
: score.stringValue ?? "",
author: {
image: score.authorUserImage ?? undefined,
name: score.authorUserName ?? undefined,
},
comment: score.comment ?? undefined,
observationId: score.observationId ?? undefined,
traceId: score.traceId,
traceName: score.traceName ?? undefined,
userId: score.userId ?? undefined,
userId: score.traceUserId ?? undefined,
jobConfigurationId: score.jobConfigurationId ?? undefined,
};
};
+1 -1
View File
@@ -1,6 +1,6 @@
import { JSONView } from "@/src/components/ui/CodeJsonViewer";
import { z } from "zod";
import { deepParseJson } from "@/src/utils/json";
import { deepParseJson } from "@langfuse/shared";
import { cn } from "@/src/utils/tailwind";
import { useState } from "react";
import { Button } from "@/src/components/ui/button";
@@ -8,7 +8,6 @@ import {
CardTitle,
} from "@/src/components/ui/card";
import { Badge } from "@/src/components/ui/badge";
import { ManualScoreButton } from "@/src/features/manual-scoring/components/ManualScoreButton";
import { NewDatasetItemFromTrace } from "@/src/features/datasets/components/NewDatasetItemFromObservationButton";
import { type ObservationReturnType } from "@/src/server/api/routers/traces";
import { api } from "@/src/utils/api";
@@ -22,6 +21,7 @@ import { withDefault, StringParam, useQueryParam } from "use-query-params";
import ScoresTable from "@/src/components/table/use-cases/scores";
import { ScoresPreview } from "@/src/components/trace/ScoresPreview";
import { JumpToPlaygroundButton } from "@/src/ee/features/playground/page/components/JumpToPlaygroundButton";
import { AnnotateButton } from "@/src/features/manual-scoring/components/AnnotateButton";
export const ObservationPreview = (props: {
observations: Array<ObservationReturnType>;
@@ -154,11 +154,12 @@ export const ObservationPreview = (props: {
</div>
</div>
<div className="flex flex-wrap gap-2">
<ManualScoreButton
<AnnotateButton
projectId={props.projectId}
traceId={preloadedObservation.traceId}
observationId={preloadedObservation.id}
scores={props.scores}
type="observation"
/>
{observationWithInputAndOutput.data?.type === "GENERATION" && (
<JumpToPlaygroundButton
@@ -237,7 +238,7 @@ const PromptBadge = (props: { promptId: string; projectId: string }) => {
if (prompt.isLoading || !prompt.data) return null;
return (
<Link
href={`/project/${props.projectId}/prompts/${prompt.data.name}?version=${prompt.data.version}`}
href={`/project/${props.projectId}/prompts/${encodeURIComponent(prompt.data.name)}?version=${prompt.data.version}`}
>
<Badge>
Prompt: {prompt.data.name}
+4 -4
View File
@@ -9,7 +9,6 @@ import {
CardTitle,
} from "@/src/components/ui/card";
import { TraceAggUsageBadge } from "@/src/components/token-usage-badge";
import { ManualScoreButton } from "@/src/features/manual-scoring/components/ManualScoreButton";
import { Badge } from "@/src/components/ui/badge";
import { type ObservationReturnType } from "@/src/server/api/routers/traces";
import { IOPreview } from "@/src/components/trace/IOPreview";
@@ -19,6 +18,7 @@ import { Tabs, TabsList, TabsTrigger } from "@/src/components/ui/tabs";
import { withDefault, StringParam, useQueryParam } from "use-query-params";
import ScoresTable from "@/src/components/table/use-cases/scores";
import { ScoresPreview } from "@/src/components/trace/ScoresPreview";
import { AnnotateButton } from "@/src/features/manual-scoring/components/AnnotateButton";
export const TracePreview = ({
trace,
@@ -54,13 +54,13 @@ export const TracePreview = ({
<TabsList className="bg-background py-0">
<TabsTrigger
value="preview"
className="data-[state=active]:border-primary-accent h-full rounded-none border-b-4 border-transparent data-[state=active]:shadow-none"
className="h-full rounded-none border-b-4 border-transparent data-[state=active]:border-primary-accent data-[state=active]:shadow-none"
>
Preview
</TabsTrigger>
<TabsTrigger
value="scores"
className="data-[state=active]:border-primary-accent h-full rounded-none border-b-4 border-transparent data-[state=active]:shadow-none"
className="h-full rounded-none border-b-4 border-transparent data-[state=active]:border-primary-accent data-[state=active]:shadow-none"
>
Scores
</TabsTrigger>
@@ -95,7 +95,7 @@ export const TracePreview = ({
</div>
</div>
<div className="flex flex-wrap gap-2">
<ManualScoreButton
<AnnotateButton
projectId={trace.projectId}
traceId={trace.id}
scores={scores}
+1 -1
View File
@@ -4,7 +4,7 @@ import { Check, ChevronsDownUp, ChevronsUpDown, Copy } from "lucide-react";
import { cn } from "@/src/utils/tailwind";
import { default as React18JsonView } from "react18-json-view";
import "react18-json-view/src/dark.css";
import { deepParseJson } from "@/src/utils/json";
import { deepParseJson } from "@langfuse/shared";
import { Skeleton } from "@/src/components/ui/skeleton";
import { useTheme } from "next-themes";
+129
View File
@@ -0,0 +1,129 @@
"use client";
import * as React from "react";
import { Drawer as DrawerPrimitive } from "vaul";
import { cn } from "@/src/utils/tailwind";
import { useMediaQuery } from "react-responsive";
// https://tailwindcss.com/docs/responsive-design
const TAILWIND_MD_MEDIA_QUERY = 768;
const Drawer = ({
shouldScaleBackground = true,
...props
}: React.ComponentProps<typeof DrawerPrimitive.Root>) => {
const isMediumScreen = useMediaQuery({
query: `(min-width: ${TAILWIND_MD_MEDIA_QUERY}px)`,
});
const direction = isMediumScreen ? "right" : "bottom";
return (
<DrawerPrimitive.Root
shouldScaleBackground={shouldScaleBackground}
direction={direction}
{...props}
/>
);
};
Drawer.displayName = "Drawer";
const DrawerTrigger = DrawerPrimitive.Trigger;
const DrawerPortal = DrawerPrimitive.Portal;
const DrawerClose = DrawerPrimitive.Close;
const DrawerOverlay = React.forwardRef<
React.ElementRef<typeof DrawerPrimitive.Overlay>,
React.ComponentPropsWithoutRef<typeof DrawerPrimitive.Overlay>
>(({ className, ...props }, ref) => (
<DrawerPrimitive.Overlay
ref={ref}
className={cn("fixed inset-0 z-50 bg-primary/20", className)}
{...props}
/>
));
DrawerOverlay.displayName = DrawerPrimitive.Overlay.displayName;
const DrawerContent = React.forwardRef<
React.ElementRef<typeof DrawerPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof DrawerPrimitive.Content>
>(({ className, children, ...props }, ref) => (
<DrawerPortal>
<DrawerOverlay />
<DrawerPrimitive.Content
ref={ref}
className={cn(
"fixed inset-x-0 bottom-0 z-50 mt-24 flex h-auto flex-col rounded-t-[10px] border bg-background md:inset-x-auto md:inset-y-0 md:right-0 md:mt-0 md:h-full md:w-2/5 md:rounded-l-[10px] md:rounded-t-[0px] lg:w-1/3 xl:w-1/4 2xl:w-1/4",
className,
)}
{...props}
>
{children}
</DrawerPrimitive.Content>
</DrawerPortal>
));
DrawerContent.displayName = "DrawerContent";
const DrawerHeader = ({
className,
...props
}: React.HTMLAttributes<HTMLDivElement>) => (
<div
className={cn("grid gap-1.5 p-4 text-center sm:text-left", className)}
{...props}
/>
);
DrawerHeader.displayName = "DrawerHeader";
const DrawerFooter = ({
className,
...props
}: React.HTMLAttributes<HTMLDivElement>) => (
<div
className={cn("mt-auto flex flex-col gap-2 p-4", className)}
{...props}
/>
);
DrawerFooter.displayName = "DrawerFooter";
const DrawerTitle = React.forwardRef<
React.ElementRef<typeof DrawerPrimitive.Title>,
React.ComponentPropsWithoutRef<typeof DrawerPrimitive.Title>
>(({ className, ...props }, ref) => (
<DrawerPrimitive.Title
ref={ref}
className={cn(
"text-lg font-semibold leading-none tracking-tight",
className,
)}
{...props}
/>
));
DrawerTitle.displayName = DrawerPrimitive.Title.displayName;
const DrawerDescription = React.forwardRef<
React.ElementRef<typeof DrawerPrimitive.Description>,
React.ComponentPropsWithoutRef<typeof DrawerPrimitive.Description>
>(({ className, ...props }, ref) => (
<DrawerPrimitive.Description
ref={ref}
className={cn("text-sm text-muted-foreground", className)}
{...props}
/>
));
DrawerDescription.displayName = DrawerPrimitive.Description.displayName;
export {
Drawer,
DrawerPortal,
DrawerOverlay,
DrawerTrigger,
DrawerClose,
DrawerContent,
DrawerHeader,
DrawerFooter,
DrawerTitle,
DrawerDescription,
};
+3 -1
View File
@@ -28,4 +28,6 @@ const PopoverContent = React.forwardRef<
));
PopoverContent.displayName = PopoverPrimitive.Content.displayName;
export { Popover, PopoverTrigger, PopoverContent };
const PopoverClose = PopoverPrimitive.Close;
export { Popover, PopoverTrigger, PopoverContent, PopoverClose };
+60
View File
@@ -0,0 +1,60 @@
"use client";
import * as React from "react";
import * as ToggleGroupPrimitive from "@radix-ui/react-toggle-group";
import { type VariantProps } from "class-variance-authority";
import { cn } from "@/src/utils/tailwind";
import { toggleVariants } from "@/src/components/ui/toggle";
const ToggleGroupContext = React.createContext<
VariantProps<typeof toggleVariants>
>({
size: "default",
variant: "default",
});
const ToggleGroup = React.forwardRef<
React.ElementRef<typeof ToggleGroupPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof ToggleGroupPrimitive.Root> &
VariantProps<typeof toggleVariants>
>(({ className, variant, size, children, ...props }, ref) => (
<ToggleGroupPrimitive.Root
ref={ref}
className={cn("flex items-center justify-center gap-1", className)}
{...props}
>
<ToggleGroupContext.Provider value={{ variant, size }}>
{children}
</ToggleGroupContext.Provider>
</ToggleGroupPrimitive.Root>
));
ToggleGroup.displayName = ToggleGroupPrimitive.Root.displayName;
const ToggleGroupItem = React.forwardRef<
React.ElementRef<typeof ToggleGroupPrimitive.Item>,
React.ComponentPropsWithoutRef<typeof ToggleGroupPrimitive.Item> &
VariantProps<typeof toggleVariants>
>(({ className, children, variant, size, ...props }, ref) => {
const context = React.useContext(ToggleGroupContext);
return (
<ToggleGroupPrimitive.Item
ref={ref}
className={cn(
toggleVariants({
variant: context.variant || variant,
size: context.size || size,
}),
className,
)}
{...props}
>
{children}
</ToggleGroupPrimitive.Item>
);
});
ToggleGroupItem.displayName = ToggleGroupPrimitive.Item.displayName;
export { ToggleGroup, ToggleGroupItem };
+1 -1
View File
@@ -1 +1 @@
export const VERSION = "v2.47.4";
export const VERSION = "v2.49.2";
@@ -11,7 +11,7 @@ import {
PopoverTrigger,
} from "@/src/components/ui/popover";
import { useState } from "react";
import { Trash2 } from "lucide-react";
import { Trash } from "lucide-react";
import {
Tabs,
TabsContent,
@@ -156,7 +156,7 @@ export function DeactivateConfig({
disabled={!hasAccess || config?.status !== "ACTIVE"}
loading={isLoading}
>
<Trash2 className="h-5 w-5" />
<Trash className="h-5 w-5" />
</Button>
</PopoverTrigger>
<PopoverContent>
@@ -1,24 +1,24 @@
import { Terminal } from "lucide-react";
import Link from "next/link";
import { useEffect, useState } from "react";
import { z } from "zod";
import { createEmptyMessage } from "@/src/components/ChatMessages/utils/createEmptyMessage";
import { Button } from "@/src/components/ui/button";
import usePlaygroundCache from "@/src/ee/features/playground/page/hooks/usePlaygroundCache";
import { type PlaygroundCache } from "@/src/ee/features/playground/page/types";
import { getIsCloudEnvironment } from "@/src/ee/utils/getIsCloudEnvironment";
import { usePostHogClientCapture } from "@/src/features/posthog-analytics/usePostHogClientCapture";
import { PromptType } from "@/src/features/prompts/server/utils/validation";
import useProjectIdFromURL from "@/src/hooks/useProjectIdFromURL";
import {
ChatMessageRole,
supportedModels as playgroundSupportedModels,
type Observation,
type Prompt,
supportedModels as playgroundSupportedModels,
type UIModelParams,
ZodModelConfig,
} from "@langfuse/shared";
import { useEffect, useState } from "react";
import { PromptType } from "@/src/features/prompts/server/utils/validation";
import { ChatMessageListSchema } from "@/src/features/prompts/components/NewPromptForm/validation";
import { createEmptyMessage } from "@/src/components/ChatMessages/utils/createEmptyMessage";
import { getIsCloudEnvironment } from "@/src/ee/utils/getIsCloudEnvironment";
type JumpToPlaygroundButtonProps = (
| {
@@ -76,9 +76,19 @@ export const JumpToPlaygroundButton: React.FC<JumpToPlaygroundButtonProps> = (
);
};
const ParsedChatMessageListSchema = z.array(
z.object({
role: z.nativeEnum(ChatMessageRole),
content: z.union([
z.string(),
z.any().transform((v) => JSON.stringify(v, null, 2)),
]),
}),
);
const parsePrompt = (prompt: Prompt): PlaygroundCache => {
if (prompt.type === PromptType.Chat) {
const parsedMessages = ChatMessageListSchema.safeParse(prompt.prompt);
const parsedMessages = ParsedChatMessageListSchema.safeParse(prompt.prompt);
return parsedMessages.success ? { messages: parsedMessages.data } : null;
} else {
@@ -109,14 +119,16 @@ const parseGeneration = (generation: Observation): PlaygroundCache => {
}
if (typeof input === "object") {
const parsedMessages = ChatMessageListSchema.safeParse(input);
const parsedMessages = ParsedChatMessageListSchema.safeParse(input);
if (parsedMessages.success)
return { messages: parsedMessages.data, modelParams };
}
if (typeof input === "object" && "messages" in input) {
const parsedMessages = ChatMessageListSchema.safeParse(input["messages"]);
const parsedMessages = ParsedChatMessageListSchema.safeParse(
input["messages"],
);
if (parsedMessages.success)
return { messages: parsedMessages.data, modelParams };
@@ -1,5 +1,5 @@
import { type ChangeEvent } from "react";
import { CheckCircle2, Circle, Trash2Icon } from "lucide-react";
import { CheckCircle2, Circle, TrashIcon } from "lucide-react";
import { Button } from "@/src/components/ui/button";
import { Textarea } from "@/src/components/ui/textarea";
import { type PromptVariable } from "@langfuse/shared";
@@ -46,7 +46,7 @@ export const PromptVariableComponent: React.FC<{
onClick={handleDeleteVariable}
className="p-0"
>
{!isUsed && <Trash2Icon size={16} />}
{!isUsed && <TrashIcon size={16} />}
</Button>
</div>
<Textarea
@@ -1,4 +1,4 @@
import { noHtmlCheck } from "@/src/utils/zod";
import { noHtmlCheck } from "@langfuse/shared";
import * as z from "zod";
export const projectNameSchema = z.object({
+1 -1
View File
@@ -1,4 +1,4 @@
import { noHtmlCheck } from "@/src/utils/zod";
import { noHtmlCheck } from "@langfuse/shared";
import * as z from "zod";
export const signupSchema = z.object({
@@ -12,7 +12,7 @@ import { useEffect } from "react";
import { usdFormatter } from "../../../utils/numbers";
import { DataTableToolbar } from "@/src/components/table/data-table-toolbar";
import useColumnVisibility from "@/src/features/column-visibility/hooks/useColumnVisibility";
import { type Prisma } from "@langfuse/shared";
import { ScoreDataType, type Prisma } from "@langfuse/shared";
import { useRowHeightLocalStorage } from "@/src/components/table/data-table-row-height-switch";
import { IOTableCell } from "@/src/components/ui/CodeJsonViewer";
@@ -120,6 +120,7 @@ export function DatasetRunsTable(props: {
scores={Object.entries(scores).map(([k, v]) => ({
name: k,
value: v,
dataType: ScoreDataType.NUMERIC, // numeric and boolean values treated as numeric
}))}
variant="headings"
/>
@@ -12,7 +12,7 @@ import {
import { throwIfNoAccess } from "@/src/features/rbac/utils/checkAccess";
import { auditLog } from "@/src/features/audit-logs/auditLog";
import { DB } from "@/src/server/db";
import { paginationZod } from "@/src/utils/zod";
import { paginationZod } from "@langfuse/shared";
export const datasetRouter = createTRPCRouter({
allDatasetMeta: protectedProjectProcedure
@@ -167,6 +167,7 @@ export const datasetRouter = createTRPCRouter({
JOIN traces t ON t.id = s.trace_id
WHERE
t.project_id = ${input.projectId}
AND s.data_type != 'CATEGORICAL'
AND ri.dataset_run_id = runs.id
GROUP BY s.name
) s
@@ -529,18 +530,6 @@ export const datasetRouter = createTRPCRouter({
},
},
},
include: {
observation: {
select: {
id: true,
},
},
trace: {
select: {
id: true,
},
},
},
orderBy: {
createdAt: "desc",
},
@@ -597,18 +586,37 @@ export const datasetRouter = createTRPCRouter({
},
});
const traces = await ctx.prisma.traceView.findMany({
where: {
id: {
in: runItems.map((ri) => ri.traceId).filter(Boolean) as string[],
},
projectId: ctx.session.projectId,
},
select: {
id: true,
duration: true,
},
});
// Directly access 'traces' table and calculate duration via lateral join
// Previously used 'traces_view' was not performant enough
const traceIdsSQL = Prisma.sql`ARRAY[${Prisma.join(runItems.map((ri) => ri.traceId))}]`;
const traces = await ctx.prisma.$queryRaw<
{
id: string;
duration: number;
}[]
>(
Prisma.sql`
SELECT
t.id,
o.duration
FROM
traces t
LEFT JOIN LATERAL (
SELECT
EXTRACT(epoch FROM COALESCE(max(o1.end_time), max(o1.start_time)))::double precision - EXTRACT(epoch FROM min(o1.start_time))::double precision AS duration
FROM
observations o1
WHERE
o1.project_id = t.project_id
AND o1.trace_id = t.id
GROUP BY
o1.project_id,
o1.trace_id) o ON TRUE
WHERE
t.project_id = ${input.projectId}
AND t.id = ANY(${traceIdsSQL})
`,
);
const items = runItems.map((ri) => {
return {
@@ -25,7 +25,7 @@ import {
filterOperators,
singleFilter,
} from "@langfuse/shared";
import { NonEmptyString } from "@/src/utils/zod";
import { NonEmptyString } from "@langfuse/shared";
import { cn } from "@/src/utils/tailwind";
import { usePostHogClientCapture } from "@/src/features/posthog-analytics/usePostHogClientCapture";
@@ -104,7 +104,6 @@ export function MultiSelect({
selectedValues.add(option.value);
}
const filterValues = Array.from(selectedValues);
onValueChange(filterValues.length ? filterValues : []);
}}
>
@@ -15,6 +15,7 @@ import {
withDefault,
} from "use-query-params";
import { promptsTableCols } from "@/src/server/api/definitions/promptsTable";
import { usersTableCols } from "@/src/server/api/definitions/usersTable";
const DEBUG_QUERY_STATE = false;
@@ -107,6 +108,7 @@ const tableCols = {
sessions: sessionsViewCols,
scores: scoresTableCols,
prompts: promptsTableCols,
users: usersTableCols,
dashboard: [
{ id: "traceName", name: "Trace Name" },
{ id: "tags", name: "Tags" },
@@ -0,0 +1,807 @@
import { useHasAccess } from "@/src/features/rbac/utils/checkAccess";
import React from "react";
import { Button } from "@/src/components/ui/button";
import {
LockIcon,
MessageCircleMore,
MessageCircle,
X,
SquarePen,
} from "lucide-react";
import {
type ControllerRenderProps,
useFieldArray,
useForm,
} from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";
import {
Form,
FormControl,
FormField,
FormItem,
FormLabel,
FormMessage,
} from "@/src/components/ui/form";
import {
Drawer,
DrawerContent,
DrawerHeader,
DrawerTrigger,
} from "@/src/components/ui/drawer";
import { ScoreDataType, type Score, type ScoreConfig } from "@langfuse/shared";
import { z } from "zod";
import { Input } from "@/src/components/ui/input";
import {
Popover,
PopoverClose,
PopoverContent,
PopoverTrigger,
} from "@/src/components/ui/popover";
import { api } from "@/src/utils/api";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/src/components/ui/select";
import { Textarea } from "@/src/components/ui/textarea";
import { HoverCardContent } from "@radix-ui/react-hover-card";
import { HoverCard, HoverCardTrigger } from "@/src/components/ui/hover-card";
import { ScoreConfigDetails } from "@/src/features/manual-scoring/components/ScoreConfigDetails";
import { trpcErrorToast } from "@/src/utils/trpcErrorToast";
import {
isNumericDataType,
isPresent,
isScoreUnsaved,
} from "@/src/features/manual-scoring/lib/helpers";
import { getDefaultScoreData } from "@/src/features/manual-scoring/lib/getDefaultScoreData";
import { ToggleGroup, ToggleGroupItem } from "@/src/components/ui/toggle-group";
import Header from "@/src/components/layouts/header";
import { MultiSelectKeyValues } from "@/src/features/manual-scoring/components/multi-select-key-values";
import { CommandItem } from "@/src/components/ui/command";
import { useRouter } from "next/router";
import useLocalStorage from "@/src/components/useLocalStorage";
import { usePostHogClientCapture } from "@/src/features/posthog-analytics/usePostHogClientCapture";
const AnnotationScoreDataSchema = z.object({
name: z.string(),
scoreId: z.string().optional(),
value: z.number().nullable().optional(),
stringValue: z.string().optional(),
dataType: z.nativeEnum(ScoreDataType),
configId: z.string().optional(),
comment: z.string().optional(),
});
const AnnotateFormSchema = z.object({
scoreData: z.array(AnnotationScoreDataSchema),
});
type AnnotateFormSchemaType = z.infer<typeof AnnotateFormSchema>;
type AnnotationScoreSchemaType = z.infer<typeof AnnotationScoreDataSchema>;
type ConfigCategory = {
label: string;
value: string;
};
export function AnnotateButton({
traceId,
scores,
observationId,
projectId,
variant = "button",
type = "trace",
source = "TraceDetail",
}: {
traceId: string;
scores: Score[];
observationId?: string;
projectId: string;
variant?: "button" | "badge";
type?: "trace" | "observation" | "session";
source?: "TraceDetail" | "SessionDetail";
}) {
const capture = usePostHogClientCapture();
const hasAccess = useHasAccess({
projectId,
scope: "scores:CUD",
});
const configsData = api.scoreConfigs.all.useQuery({
projectId,
});
const configs = configsData.data?.configs ?? [];
const [emptySelectedConfigIds, setEmptySelectedConfigIds] = useLocalStorage<
string[]
>("emptySelectedConfigIds", []);
const form = useForm<z.infer<typeof AnnotateFormSchema>>({
resolver: zodResolver(AnnotateFormSchema),
defaultValues: {
scoreData: getDefaultScoreData({
scores,
traceId,
observationId,
emptySelectedConfigIds,
configs,
}),
},
});
const router = useRouter();
const { fields, remove, update, replace } = useFieldArray({
control: form.control,
name: "scoreData",
});
const mutDeleteScore = api.scores.deleteAnnotationScore.useMutation({
onError: (error) => {
trpcErrorToast(error);
},
onSettled: async (data, error) => {
if (!data || error) return;
const { id, name, dataType, configId } = data;
const updatedScoreIndex = fields.findIndex(
(field) => field.scoreId === id,
);
update(updatedScoreIndex, {
name,
dataType,
configId: configId ?? undefined,
value: null,
scoreId: undefined,
stringValue: undefined,
comment: undefined,
});
await Promise.all([
utils.scores.invalidate(),
utils.traces.invalidate(),
utils.sessions.invalidate(),
]);
},
});
const utils = api.useUtils();
const onSettledUpsert = async (data?: Score, error?: unknown) => {
if (!data || error) return;
const { id, value, stringValue, name, dataType, configId, comment } = data;
const updatedScoreIndex = fields.findIndex(
(field) => field.configId === configId,
);
update(updatedScoreIndex, {
value,
name,
dataType,
scoreId: id,
stringValue: stringValue ?? undefined,
configId: configId ?? undefined,
comment: comment ?? undefined,
});
await Promise.all([
utils.scores.invalidate(),
utils.traces.invalidate(),
utils.sessions.invalidate(),
]);
};
const mutCreateScores = api.scores.createAnnotationScore.useMutation({
onError: (error) => {
trpcErrorToast(error);
},
onSettled: onSettledUpsert,
});
const mutUpdateScores = api.scores.updateAnnotationScore.useMutation({
onError: (error) => {
trpcErrorToast(error);
},
onSettled: onSettledUpsert,
});
if (!hasAccess && variant === "badge") return null;
function handleOnCheckedChange(
values: Record<string, string>[],
changedValueId?: string,
) {
if (values.length === 0) {
const populatedScoreFields = fields.filter(({ scoreId }) => !!scoreId);
replace(populatedScoreFields);
setEmptySelectedConfigIds(
populatedScoreFields
.filter(({ configId }) => !!configId)
.map(({ configId }) => configId as string),
);
return;
}
if (!changedValueId) return;
const configToChange = configs.find(({ id }) => id === changedValueId);
if (!configToChange) return;
const { id, name, dataType } = configToChange;
const index = fields.findIndex(({ configId }) => configId === id);
if (index === -1) {
replace([
...fields,
{
name,
dataType,
configId: id,
},
]);
setEmptySelectedConfigIds([...emptySelectedConfigIds, changedValueId]);
} else {
remove(index);
setEmptySelectedConfigIds(
emptySelectedConfigIds.filter((id) => id !== changedValueId),
);
}
}
function handleOnValueChange(
score: AnnotationScoreSchemaType,
index: number,
configCategories: ConfigCategory[],
): ((value: string) => void) | undefined {
return async (stringValue) => {
const selectedCategory = configCategories.find(
({ label }) => label === stringValue,
);
if (selectedCategory) {
const newValue = Number(selectedCategory.value);
update(index, {
...score,
value: newValue,
});
if (!!stringValue) {
if (!!score.scoreId) {
await mutUpdateScores.mutateAsync({
projectId,
...score,
id: score.scoreId,
value: newValue,
stringValue,
});
capture("score:update", {
type: type,
source: source,
dataType: score.dataType,
});
} else {
await mutCreateScores.mutateAsync({
projectId,
traceId,
...score,
observationId,
value: newValue,
stringValue,
});
capture("score:create", {
type: type,
source: source,
dataType: score.dataType,
});
}
}
}
};
}
function handleCommentUpdate({
field,
score,
comment,
}: {
field: ControllerRenderProps<
AnnotateFormSchemaType,
`scoreData.${number}.comment`
>;
score: AnnotationScoreSchemaType;
comment?: string | null;
}): React.MouseEventHandler<HTMLButtonElement> | undefined {
return async () => {
const { value, scoreId } = score;
if (!!field.value && !!scoreId && isPresent(value)) {
await mutUpdateScores.mutateAsync({
projectId,
...score,
value,
id: scoreId,
comment,
});
capture(comment ? "score:update_comment" : "score:delete_comment", {
type: type,
source: source,
});
}
};
}
function handleOnBlur({
config,
field,
index,
score,
}: {
config: ScoreConfig;
field: ControllerRenderProps<
AnnotateFormSchemaType,
`scoreData.${number}.value`
>;
index: number;
score: AnnotationScoreSchemaType;
}): React.FocusEventHandler<HTMLInputElement> | undefined {
return async () => {
const { maxValue, minValue, dataType } = config;
if (isNumericDataType(dataType)) {
if (
(isPresent(maxValue) && Number(field.value) > maxValue) ||
(isPresent(minValue) && Number(field.value) < minValue)
) {
form.setError(`scoreData.${index}.value`, {
type: "custom",
message: `Not in range: [${minValue ?? "-∞"},${maxValue ?? "∞"}]`,
});
return;
}
}
form.clearErrors(`scoreData.${index}.value`);
if (isPresent(field.value)) {
if (!!score.scoreId) {
await mutUpdateScores.mutateAsync({
projectId,
...score,
value: Number(field.value),
id: score.scoreId,
});
capture("score:update", {
type: type,
source: source,
dataType: score.dataType,
});
} else {
await mutCreateScores.mutateAsync({
projectId,
traceId,
...score,
observationId,
value: Number(field.value),
});
capture("score:create", {
type: type,
source: source,
dataType: score.dataType,
});
}
}
};
}
return (
<Drawer>
<DrawerTrigger asChild>
{variant === "button" ? (
<Button
variant="secondary"
disabled={!hasAccess}
onClick={() =>
capture(
Boolean(scores.length)
? "score:update_form_open"
: "score:create_form_open",
{
type: type,
source: source,
},
)
}
>
{!hasAccess ? (
<LockIcon className="mr-2 h-3 w-3" />
) : (
<SquarePen className="mr-2 h-5 w-5" />
)}
<span>Annotate</span>
</Button>
) : (
<Button
className="h-6 rounded-full px-3 text-xs"
onClick={() =>
capture(
Boolean(scores.length)
? "score:update_form_open"
: "score:create_form_open",
{
type: type,
source: source,
},
)
}
>
Annotate
</Button>
)}
</DrawerTrigger>
<DrawerContent className="h-1/3">
<div className="mx-auto w-full overflow-y-auto md:max-h-full">
<DrawerHeader className="sticky top-0 z-10 bg-background">
<Header
title="Annotate"
level="h3"
help={{
description: `Annotate ${observationId ? "observation" : "trace"} with scores to capture human evaluation across different dimensions.`,
href: "https://langfuse.com/docs/scores/manually",
}}
></Header>
<div className="grid grid-flow-col items-center">
<MultiSelectKeyValues
title="Value"
align="end"
items="empty scores"
className="grid grid-cols-[auto,1fr,auto,auto] gap-2"
onValueChange={handleOnCheckedChange}
options={configs.map((config) => ({
key: config.id,
value: config.name,
disabled: fields.some(
(field) => !!field.scoreId && field.configId === config.id,
),
}))}
values={fields
.filter((field) => !!field.configId)
.map((field) => ({
value: field.name,
key: field.configId as string,
}))}
controlButtons={
<CommandItem
onSelect={() => {
capture("score_configs:manage_configs_item_click", {
type: type,
source: source,
});
router.push(`/project/${projectId}/settings`);
}}
>
Manage score configs
</CommandItem>
}
/>
</div>
</DrawerHeader>
<Form {...form}>
<form className="flex flex-col gap-4">
<div className="grid grid-flow-row gap-2 px-4">
<FormField
control={form.control}
name="scoreData"
render={() => (
<>
{fields.map((score, index) => {
const config = configs.find(
(config) => config.id === score.configId,
);
if (!config) return null;
return (
<div
key={score.id}
className="grid w-full grid-cols-[1fr,2fr] items-center gap-8 text-left"
>
<div className="grid h-full grid-cols-[1fr,auto] items-center">
{config.description ||
isPresent(config.maxValue) ||
isPresent(config.minValue) ? (
<HoverCard>
<HoverCardTrigger asChild>
<span className="line-clamp-2 break-words text-xs font-medium underline decoration-muted-gray decoration-dashed underline-offset-2">
{score.name}
</span>
</HoverCardTrigger>
<HoverCardContent>
<ScoreConfigDetails config={config} />
</HoverCardContent>
</HoverCard>
) : (
<span
className="line-clamp-2 break-words text-xs font-medium"
title={score.name}
>
{score.name}
</span>
)}
<Popover>
<PopoverTrigger asChild>
<Button
variant="link"
type="button"
size="xs"
title="Add or view score comment"
className="h-full items-start px-0 pl-1 disabled:text-primary/50 disabled:opacity-100"
disabled={isScoreUnsaved(score.scoreId)}
>
{score.comment ? (
<MessageCircleMore className="h-4 w-4" />
) : (
<MessageCircle className="h-4 w-4" />
)}
</Button>
</PopoverTrigger>
<PopoverContent>
<FormField
control={form.control}
name={`scoreData.${index}.comment`}
render={({ field }) => (
<FormItem className="space-y-4">
<FormLabel className="text-sm">
Comment (optional)
</FormLabel>
{!!field.value &&
field.value !== score.comment && (
<HoverCard>
<HoverCardTrigger asChild>
<span className="ml-2 mr-2 rounded-sm bg-input p-1 text-xs">
Draft
</span>
</HoverCardTrigger>
<HoverCardContent side="top">
{!!score.comment && (
<div className="mb-4 max-w-48 rounded border bg-background p-2 shadow-sm">
<p className="text-xs">
Saved comment:{" "}
{score.comment}
</p>
</div>
)}
</HoverCardContent>
</HoverCard>
)}
<FormControl>
<>
<Textarea
{...field}
className="text-xs"
value={field.value || ""}
/>
{field.value !== score.comment && (
<div className="grid w-full grid-cols-[1fr,1fr] gap-2">
<Button
variant="secondary"
type="button"
size="sm"
className="text-xs"
disabled={!field.value}
loading={
mutUpdateScores.isLoading
}
onClick={handleCommentUpdate({
field,
score,
comment: field.value,
})}
>
Save
</Button>
<PopoverClose asChild>
<Button
variant="secondary"
type="button"
size="sm"
className="text-xs"
disabled={!field.value}
onClick={() => {
form.setValue(
`scoreData.${index}.comment`,
score.comment ?? "",
);
}}
>
Discard
</Button>
</PopoverClose>
</div>
)}
{field.value === score.comment && (
<div className="flex justify-end">
<Button
variant="destructive"
type="button"
size="sm"
className="text-xs"
disabled={
!field.value ||
!score.comment
}
loading={
mutUpdateScores.isLoading
}
onClick={handleCommentUpdate({
field,
score,
comment: null,
})}
>
Delete
</Button>
</div>
)}
</>
</FormControl>
<FormMessage className="text-xs" />
</FormItem>
)}
/>
</PopoverContent>
</Popover>
</div>
<div className="grid grid-cols-[11fr,1fr]">
<FormField
control={form.control}
name={`scoreData.${index}.value`}
render={({ field }) => (
<FormItem>
<FormControl>
{isNumericDataType(score.dataType) ? (
<Input
{...field}
value={field.value ?? undefined}
type="number"
className="text-xs"
onBlur={handleOnBlur({
config,
field,
index,
score,
})}
onKeyDown={handleOnKeyDown}
/>
) : config.categories &&
(
(config.categories as ConfigCategory[]) ??
[]
).length > 3 ? (
<Select
defaultValue={score.stringValue}
onValueChange={handleOnValueChange(
score,
index,
(config.categories as ConfigCategory[]) ??
[],
)}
>
<SelectTrigger>
<div className="text-xs">
<SelectValue placeholder="Select category" />
</div>
</SelectTrigger>
<SelectContent>
{(
(config.categories as ConfigCategory[]) ??
[]
).map(
(category: ConfigCategory) => (
<SelectItem
key={category.value}
value={category.label}
className="text-xs"
>
{category.label}
</SelectItem>
),
)}
</SelectContent>
</Select>
) : (
<ToggleGroup
type="single"
defaultValue={score.stringValue}
className={`grid grid-cols-${((config.categories as ConfigCategory[]) ?? [])?.length}`}
onValueChange={handleOnValueChange(
score,
index,
(config.categories as ConfigCategory[]) ??
[],
)}
>
{(
(config.categories as ConfigCategory[]) ??
[]
).map((category: ConfigCategory) => (
<ToggleGroupItem
key={category.value}
value={category.label}
variant="outline"
className="grid grid-flow-col gap-1 text-nowrap px-1 text-xs font-normal"
>
<span
className="truncate"
title={category.label}
>
{category.label}
</span>
<span className="text-primary/60">{`(${category.value})`}</span>
</ToggleGroupItem>
))}
</ToggleGroup>
)}
</FormControl>
<FormMessage className="text-xs" />
</FormItem>
)}
/>
<Button
variant="link"
type="button"
className="px-0 pl-1"
title="Delete score from trace/observation"
disabled={isScoreUnsaved(score.scoreId)}
loading={mutDeleteScore.isLoading}
onClick={async () => {
if (score.scoreId) {
await mutDeleteScore.mutateAsync({
id: score.scoreId,
projectId,
});
capture("score:delete", {
type: type,
source: source,
});
form.clearErrors(
`scoreData.${index}.value`,
);
}
}}
>
<X className="h-4 w-4" />
</Button>
</div>
</div>
);
})}
</>
)}
/>
</div>
</form>
</Form>
</div>
</DrawerContent>
</Drawer>
);
}
function handleOnKeyDown(e: React.KeyboardEvent<HTMLInputElement>) {
if (e.key === "Enter") {
e.preventDefault();
const form = e.currentTarget.form;
if (!form) return;
const currentTabIndex = e.currentTarget.tabIndex;
const nextElement = form.querySelector(
`[tabindex="${currentTabIndex + 1}"]`,
);
if (nextElement instanceof HTMLElement) {
nextElement.focus();
} else {
e.currentTarget.blur();
}
}
}
@@ -0,0 +1,423 @@
import React, { useState } from "react";
import { Button } from "@/src/components/ui/button";
import { useHasAccess } from "@/src/features/rbac/utils/checkAccess";
import { z } from "zod";
import {
Dialog,
DialogContent,
DialogHeader,
DialogTitle,
DialogTrigger,
} from "@/src/components/ui/dialog";
import { PlusIcon, Trash } from "lucide-react";
import { useFieldArray, useForm } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";
import {
Form,
FormControl,
FormField,
FormItem,
FormLabel,
FormMessage,
} from "@/src/components/ui/form";
import { Input } from "@/src/components/ui/input";
import { ScoreDataType } from "@langfuse/shared";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/src/components/ui/select";
import { api } from "@/src/utils/api";
import { Textarea } from "@/src/components/ui/textarea";
import {
isBooleanDataType,
isCategoricalDataType,
isNumericDataType,
} from "@/src/features/manual-scoring/lib/helpers";
import DocPopup from "@/src/components/layouts/doc-popup";
import { usePostHogClientCapture } from "@/src/features/posthog-analytics/usePostHogClientCapture";
const availableDataTypes = [
ScoreDataType.NUMERIC,
ScoreDataType.CATEGORICAL,
ScoreDataType.BOOLEAN,
] as const;
const category = z.object({
label: z.string().min(1),
value: z.coerce.number(),
});
const formSchema = z.object({
name: z.string().min(1).max(35),
dataType: z.enum(availableDataTypes),
minValue: z.coerce.number().optional(),
maxValue: z.coerce.number().optional(),
categories: z.array(category).optional(),
description: z.string().optional(),
});
export function CreateScoreConfigButton({ projectId }: { projectId: string }) {
const [open, setOpen] = useState(false);
const [formError, setFormError] = useState<string | null>(null);
const capture = usePostHogClientCapture();
const hasAccess = useHasAccess({
projectId: projectId,
scope: "scoreConfigs:CUD",
});
const utils = api.useUtils();
const createScoreConfig = api.scoreConfigs.create.useMutation({
onSuccess: () => utils.scoreConfigs.invalidate(),
onError: (error) =>
setFormError(error.message ?? "An error occurred while creating config."),
});
const form = useForm<z.infer<typeof formSchema>>({
resolver: zodResolver(formSchema),
defaultValues: {
dataType: ScoreDataType.NUMERIC,
minValue: undefined,
maxValue: undefined,
name: "",
},
});
const { fields, append, remove, replace } = useFieldArray({
control: form.control,
name: "categories",
});
if (!hasAccess) return null;
function onSubmit(values: z.infer<typeof formSchema>) {
const error = validateForm(values);
setFormError(error);
if (error) return;
return createScoreConfig
.mutateAsync({
projectId,
...values,
})
.then(() => {
capture("score_configs:create_form_submit", {
dataType: values.dataType,
});
form.reset();
setOpen(false);
})
.catch((error) => {
console.error(error);
});
}
return (
<>
<Dialog
open={open}
onOpenChange={(v) => {
setOpen(v);
form.reset();
}}
>
<DialogTrigger asChild>
<Button variant="secondary" loading={createScoreConfig.isLoading}>
<PlusIcon className="-ml-0.5 mr-1.5 h-5 w-5" aria-hidden="true" />
Add new score config
</Button>
</DialogTrigger>
<DialogContent>
<DialogHeader>
<DialogTitle>Add new score config</DialogTitle>
</DialogHeader>
<Form {...form}>
<form
className="space-y-6"
// eslint-disable-next-line @typescript-eslint/no-misused-promises
onSubmit={form.handleSubmit(onSubmit)}
>
<FormField
control={form.control}
name="name"
render={({ field }) => (
<FormItem>
<FormLabel>Name</FormLabel>
<FormControl>
<Input
{...field}
type="text"
onBlur={(e) => field.onChange(e.target.value.trimEnd())}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="dataType"
render={({ field }) => (
<FormItem>
<FormLabel>Data type</FormLabel>
<Select
defaultValue={field.value}
onValueChange={(value) => {
field.onChange(
value as (typeof availableDataTypes)[number],
);
form.clearErrors();
if (isNumericDataType(value as ScoreDataType)) {
remove();
} else {
form.setValue("minValue", undefined);
form.setValue("maxValue", undefined);
if (isBooleanDataType(value as ScoreDataType)) {
replace([
{ label: "True", value: 1 },
{ label: "False", value: 0 },
]);
} else {
replace([{ label: "", value: 0 }]);
}
}
}}
>
<FormControl>
<SelectTrigger>
<SelectValue placeholder="Select a data type" />
</SelectTrigger>
</FormControl>
<SelectContent>
{availableDataTypes.map((role) => (
<SelectItem value={role} key={role}>
{role}
</SelectItem>
))}
</SelectContent>
</Select>
<FormMessage />
</FormItem>
)}
/>
{isNumericDataType(form.getValues("dataType")) ? (
<>
<FormField
control={form.control}
name="minValue"
render={({ field }) => (
<FormItem>
<FormLabel>Minimum (optional)</FormLabel>
<FormControl>
<Input {...field} type="number" />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="maxValue"
render={({ field }) => (
<FormItem>
<FormLabel>Maximum (optional)</FormLabel>
<FormControl>
<Input {...field} type="number" />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
</>
) : (
<div className="grid grid-flow-row gap-2">
<FormField
control={form.control}
name="categories"
render={() => (
<>
{fields.length > 0 && (
<div className="mb-2 grid grid-cols-[1fr,3fr] items-center gap-2 text-left sm:grid-cols-[1fr,7fr]">
<FormLabel className="grid grid-flow-col">
Value
<DocPopup
description={`This is how the ${
isCategoricalDataType(
form.getValues("dataType"),
)
? "category"
: "boolean"
} label is mapped to an integer value internally.`}
size="xs"
></DocPopup>
</FormLabel>
<FormLabel>Label</FormLabel>
</div>
)}
{fields.map((category, index) => (
<div
key={`${category.id}-langfuseObject`}
className="items-top mb-2 grid grid-cols-[1fr,3fr] gap-2 text-left sm:grid-cols-[1fr,7fr]"
>
<FormField
control={form.control}
name={`categories.${index}.value`}
render={({ field }) => (
<FormItem>
<FormControl>
<Input
{...field}
readOnly
disabled
inputMode="numeric"
className="text-center"
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<div className="grid grid-cols-[1fr,auto] gap-2">
<FormField
control={form.control}
name={`categories.${index}.label`}
render={({ field }) => (
<FormItem>
<FormControl>
<Input
{...field}
type="text"
onBlur={(e) =>
field.onChange(
e.target.value.trimEnd(),
)
}
readOnly={isBooleanDataType(
form.getValues("dataType"),
)}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
{isCategoricalDataType(
form.getValues("dataType"),
) && (
<Button
onClick={() => remove(index)}
variant="outline"
size="icon"
disabled={
index === 0 || index !== fields.length - 1
}
>
<Trash className="h-4 w-4" />
</Button>
)}
</div>
</div>
))}
{isCategoricalDataType(form.getValues("dataType")) && (
<div className="grid-cols-auto grid">
<Button
type="button"
variant="secondary"
disabled={
isBooleanDataType(form.getValues("dataType")) &&
fields.length === 2
}
onClick={() =>
append({ label: "", value: fields.length })
}
>
Add category
</Button>
</div>
)}
</>
)}
/>
</div>
)}
<FormField
control={form.control}
name="description"
render={({ field }) => (
<>
<FormItem>
<FormLabel>Description (optional)</FormLabel>
<FormControl>
<Textarea
{...field}
placeholder="Provide an optional description of the score config..."
/>
</FormControl>
<FormMessage />
</FormItem>
</>
)}
/>
<Button
type="submit"
className="w-full"
loading={form.formState.isSubmitting}
>
Submit
</Button>
</form>
{formError ? (
<p className="text-red text-center">
<span className="font-bold">Error:</span> {formError}
</p>
) : null}
</Form>
</DialogContent>
</Dialog>
</>
);
}
function validateForm(values: z.infer<typeof formSchema>): string | null {
if (isNumericDataType(values.dataType)) {
if (
!!values.maxValue &&
!!values.minValue &&
values.maxValue <= values.minValue
) {
return "Maximum value must be greater than Minimum value.";
}
} else if (isCategoricalDataType(values.dataType)) {
if (!values.categories || values.categories.length === 0) {
return "At least one category is required for categorical data types.";
}
} else if (isBooleanDataType(values.dataType)) {
if (values.categories?.length !== 2)
return "Boolean data type must have exactly 2 categories.";
const isBooleanCategoryInvalid = values.categories?.some(
(category) => category.value !== 0 && category.value !== 1,
);
if (isBooleanCategoryInvalid)
return "Boolean data type must have categories with values 0 and 1.";
}
const uniqueNames = new Set<string>();
const uniqueValues = new Set<number>();
for (const category of values.categories || []) {
if (uniqueNames.has(category.label)) {
return "Category names must be unique.";
}
uniqueNames.add(category.label);
if (uniqueValues.has(category.value)) {
return "Category values must be unique.";
}
uniqueValues.add(category.value);
}
return null;
}
@@ -1,248 +0,0 @@
import { Button } from "@/src/components/ui/button";
import {
Dialog,
DialogContent,
DialogHeader,
DialogTitle,
DialogTrigger,
} from "@/src/components/ui/dialog";
import {
Form,
FormField,
FormItem,
FormLabel,
FormDescription,
FormControl,
FormMessage,
} from "@/src/components/ui/form";
import { Textarea } from "@/src/components/ui/textarea";
import { api } from "@/src/utils/api";
import { zodResolver } from "@hookform/resolvers/zod";
import { type Score } from "@langfuse/shared/src/db";
import { useState } from "react";
import * as z from "zod";
import { useForm } from "react-hook-form";
import { Slider } from "@/src/components/ui/slider";
import { useHasAccess } from "@/src/features/rbac/utils/checkAccess";
import { LockIcon } from "lucide-react";
import { usePostHogClientCapture } from "@/src/features/posthog-analytics/usePostHogClientCapture";
const SCORE_NAME = "manual-score";
const formSchema = z.object({
score: z.number(),
comment: z.string().optional(),
});
export function ManualScoreButton({
traceId,
scores,
observationId,
projectId,
variant = "button",
}: {
traceId: string;
scores: Score[];
observationId?: string;
projectId: string;
variant?: "button" | "badge";
}) {
const capture = usePostHogClientCapture();
const hasAccess = useHasAccess({
projectId,
scope: "scores:CUD",
});
const score = scores.find(
(s) =>
s.name === SCORE_NAME &&
s.traceId === traceId &&
(observationId !== undefined
? s.observationId === observationId
: s.observationId === null),
);
// variant === "badge" type is session
// if observationId is defined type is observation
// else type is trace
const type =
variant === "badge" ? "session" : observationId ? "observation" : "trace";
const source = type === "session" ? "SessionDetail" : "TraceDetail";
const utils = api.useUtils();
const onSuccess = async () => {
await Promise.all([
utils.scores.invalidate(),
utils.traces.invalidate(),
utils.sessions.invalidate(),
]);
};
const mutCreateScore = api.scores.createAnnotationScore.useMutation({
onSuccess,
});
const mutUpdateScore = api.scores.updateAnnotationScore.useMutation({
onSuccess,
});
const mutDeleteScore = api.scores.deleteAnnotationScore.useMutation({
onSuccess,
});
const handleDelete = async () => {
if (score) {
await mutDeleteScore.mutateAsync({ id: score.id, projectId });
capture("score:delete", {
type: type,
source: source,
});
onOpenChange(false);
}
};
const [open, setOpen] = useState(false);
const form = useForm<z.infer<typeof formSchema>>({
resolver: zodResolver(formSchema),
defaultValues: {
score: 0,
comment: "",
},
});
const onOpenChange = (value: boolean) => {
if (!hasAccess) return;
if (!value) {
form.reset();
setOpen(false);
} else {
capture(score ? "score:update_form_open" : "score:create_form_open", {
type: type,
source: source,
});
form.setValue("score", score?.value ?? 0);
form.setValue("comment", score?.comment ?? "");
setOpen(true);
}
};
const onSubmit = async (values: z.infer<typeof formSchema>) => {
if (score) {
await mutUpdateScore.mutateAsync({
id: score.id,
value: values.score,
comment: values.comment,
projectId,
});
capture("score:update_form_submit", {
type: type,
source: source,
});
} else {
await mutCreateScore.mutateAsync({
name: SCORE_NAME,
value: values.score,
comment: values.comment,
traceId,
observationId,
projectId,
});
capture("score:create_form_submit", {
type: type,
source: source,
});
}
onOpenChange(false);
};
if (!hasAccess && variant === "badge") return null;
return (
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogTrigger asChild>
{variant === "button" ? (
<Button variant="secondary" disabled={!hasAccess}>
<span>{score ? `Update score: ${score.value}` : "Add score"}</span>
{!hasAccess ? <LockIcon className="ml-2 h-3 w-3" /> : null}
</Button>
) : (
<Button className="h-6 rounded-full px-3 text-xs">
{score ? "Update score" : "Add score"}
</Button>
)}
</DialogTrigger>
<DialogContent>
<DialogHeader>
<DialogTitle className="mb-5">
{score ? "Update Score" : "Create Score"}
</DialogTitle>
</DialogHeader>
<Form {...form}>
<form
// eslint-disable-next-line @typescript-eslint/no-misused-promises
onSubmit={form.handleSubmit(onSubmit)}
className="space-y-8"
>
<FormField
control={form.control}
name="score"
render={({ field }) => (
<FormItem>
<FormLabel>Score</FormLabel>
<FormControl>
<Slider
{...field}
min={-1}
max={1}
step={0.01}
onValueChange={(value) => {
if (value[0] !== undefined) field.onChange(value[0]);
}}
value={[field.value]}
onChange={undefined}
/>
</FormControl>
<FormDescription className="flex justify-between">
<span>-1 (bad)</span>
<span>1 (good)</span>
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="comment"
render={({ field }) => (
<FormItem>
<FormLabel>Comment (optional)</FormLabel>
<FormControl>
<Textarea {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<div className="flex justify-end space-x-4">
<Button type="submit" loading={form.formState.isSubmitting}>
{form.formState.isSubmitting
? "Loading ..."
: score
? "Update"
: "Create"}
</Button>
{score && (
<Button
type="button"
variant="destructive"
onClick={() => void handleDelete()}
loading={mutDeleteScore.isLoading}
>
Delete
</Button>
)}
</div>
</form>
</Form>
</DialogContent>
</Dialog>
);
}
@@ -0,0 +1,23 @@
import {
isNumericDataType,
isPresent,
} from "@/src/features/manual-scoring/lib/helpers";
import { type ScoreConfig } from "@langfuse/shared";
import React from "react";
export function ScoreConfigDetails({ config }: { config: ScoreConfig }) {
const { name, description, minValue, maxValue, dataType } = config;
if (!description && !minValue && !maxValue) return null;
const isNameTruncated = name.length > 20;
return (
<div className="max-w-48 overflow-hidden text-wrap rounded border bg-background p-2 text-xs font-light">
{!!description && <p>{`Description: ${description}`}</p>}
{isNumericDataType(dataType) &&
(isPresent(minValue) || isPresent(maxValue)) ? (
<p>{`Range: [${minValue ?? "-∞"}, ${maxValue ?? "∞"}]`}</p>
) : null}
{isNameTruncated && <p>{`Full name: ${name}`}</p>}
</div>
);
}
@@ -0,0 +1,34 @@
import React from "react";
import Header from "@/src/components/layouts/header";
import { useHasAccess } from "@/src/features/rbac/utils/checkAccess";
import { CreateScoreConfigButton } from "@/src/features/manual-scoring/components/CreateScoreConfigButton";
import { ScoreConfigsTable } from "@/src/components/table/use-cases/score-configs";
export function ScoreConfigSettings({ projectId }: { projectId: string }) {
const hasReadAccess = useHasAccess({
projectId: projectId,
scope: "scoreConfigs:read",
});
if (!hasReadAccess) return null;
return (
<div>
<Header title="Score Configs" level="h3" />
<p className="mb-4 text-sm">
Score configs define which scores are available for{" "}
<a
href="https://langfuse.com/docs/scores/manually"
className="underline"
target="_blank"
rel="noopener noreferrer"
>
annotation
</a>{" "}
in your project. Please note that all score configs are immutable.
</p>
<ScoreConfigsTable projectId={projectId} />
<CreateScoreConfigButton projectId={projectId} />
</div>
);
}
@@ -0,0 +1,190 @@
import * as React from "react";
import { Check, ChevronDown } from "lucide-react";
import { cn } from "@/src/utils/tailwind";
import { Badge } from "@/src/components/ui/badge";
import { Button } from "@/src/components/ui/button";
import {
Command,
CommandEmpty,
CommandGroup,
CommandInput,
CommandItem,
CommandList,
CommandSeparator,
} from "@/src/components/ui/command";
import {
Popover,
PopoverContent,
PopoverTrigger,
} from "@/src/components/ui/popover";
import { Separator } from "@/src/components/ui/separator";
type MultiSelectOptions = {
value: string;
key?: string;
count?: number;
disabled?: boolean;
};
export function MultiSelectKeyValues<
T extends { key: string; value: string } | string,
>({
title,
values,
onValueChange,
options,
className,
disabled,
items = "items",
align = "center",
controlButtons,
}: {
title?: string;
values: T[];
onValueChange: (values: T[], changedValue?: string) => void;
options: MultiSelectOptions[] | readonly MultiSelectOptions[];
className?: string;
disabled?: boolean;
items?: string;
align?: "center" | "end" | "start";
controlButtons?: React.ReactNode;
}) {
const selectedValueKeys = new Set(
values.map((value) => (typeof value === "string" ? value : value.key)),
);
const showClearItems = selectedValueKeys.size > 0;
function formatFilterValues(): T[] {
if (values.length > 0 && typeof values[0] === "string") {
return Array.from(selectedValueKeys) as T[];
}
return options
.filter((option) => !!option.key && selectedValueKeys.has(option.key))
.map((option) => ({
key: option.key as string,
value: option.value,
})) as T[];
}
return (
<Popover modal>
<PopoverTrigger asChild>
<Button
variant="outline"
className={cn(
"flex h-10 w-full items-center justify-between rounded-md border border-input bg-transparent px-3 py-2 text-sm ring-offset-background placeholder:text-muted-foreground focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50",
className,
)}
disabled={disabled}
>
Select
<ChevronDown className="h-4 w-4 opacity-50" />
{selectedValueKeys.size > 0 && (
<>
<Separator orientation="vertical" className="mx-2 h-4" />
<Badge
variant="secondary"
className="rounded-sm px-1 font-normal lg:hidden"
>
{selectedValueKeys.size}
</Badge>
<div className="hidden space-x-1 overflow-x-auto lg:flex">
{selectedValueKeys.size > 2 ? (
<Badge
variant="secondary"
className="rounded-sm px-1 font-normal"
>
{selectedValueKeys.size} selected
</Badge>
) : (
options
.filter((option) =>
selectedValueKeys.has(option.key ?? option.value),
)
.map((option) => (
<Badge
variant="secondary"
key={option.key}
className="rounded-sm px-1 font-normal"
>
{option.value}
</Badge>
))
)}
</div>
</>
)}
</Button>
</PopoverTrigger>
<PopoverContent className="w-[200px] p-0" align={align}>
<Command>
<CommandInput placeholder={title} />
<CommandList>
<CommandEmpty>No results found.</CommandEmpty>
<CommandGroup>
{options.map((option) => {
const isSelected = selectedValueKeys.has(
option.key ?? option.value,
);
return (
<CommandItem
key={option.key ?? option.value}
value={option.key ?? option.value}
keywords={[option.value]}
onSelect={(value) => {
if (isSelected) {
selectedValueKeys.delete(value);
} else {
selectedValueKeys.add(value);
}
const filterValues = formatFilterValues();
onValueChange(
filterValues.length ? filterValues : [],
value,
);
}}
disabled={option.disabled}
>
<div
className={cn(
"mr-2 flex h-4 w-4 items-center justify-center rounded-sm border border-primary",
isSelected
? "bg-primary text-primary-foreground"
: "opacity-50 [&_svg]:invisible",
option.disabled ? "opacity-50" : null,
)}
>
<Check className={cn("h-4 w-4")} />
</div>
<span className="overflow-x-scroll">{option.value}</span>
{option.count !== undefined ? (
<span className="ml-auto flex h-4 w-4 items-center justify-center pl-1 font-mono text-xs">
{option.count}
</span>
) : null}
</CommandItem>
);
})}
</CommandGroup>
{controlButtons || showClearItems ? (
<>
<CommandSeparator />
<CommandGroup heading="Controls">
{showClearItems && (
<CommandItem onSelect={() => onValueChange([])}>
Clear {items}
</CommandItem>
)}
{controlButtons}
</CommandGroup>
</>
) : null}
</CommandList>
</Command>
</PopoverContent>
</Popover>
);
}
@@ -0,0 +1,56 @@
import { type Score, ScoreSource, type ScoreConfig } from "@langfuse/shared";
export const getDefaultScoreData = ({
scores,
emptySelectedConfigIds,
configs,
traceId,
observationId,
}: {
scores: Score[];
emptySelectedConfigIds: string[];
configs: ScoreConfig[];
traceId: string;
observationId?: string;
}) => {
const populatedScores = scores
.filter(
(s) =>
s.source === ScoreSource.ANNOTATION &&
s.traceId === traceId &&
(observationId !== undefined
? s.observationId === observationId
: s.observationId === null),
)
.map(({ id, name, value, dataType, stringValue, configId, comment }) => ({
scoreId: id,
name,
value,
dataType,
stringValue: stringValue ?? undefined,
configId: configId ?? undefined,
comment: comment ?? undefined,
}));
const populatedScoresConfigIds = new Set(
populatedScores.map((s) => s.configId),
);
const emptyScores = configs
.filter(
(c) =>
!populatedScoresConfigIds.has(c.id) &&
emptySelectedConfigIds.includes(c.id),
)
.map(({ name, dataType, id }) => ({
scoreId: undefined,
name,
value: undefined,
dataType,
stringValue: undefined,
configId: id,
comment: undefined,
}));
return [...populatedScores, ...emptyScores];
};
@@ -0,0 +1,15 @@
import { ScoreDataType } from "@langfuse/shared";
export const isNumericDataType = (dataType: ScoreDataType) =>
dataType === ScoreDataType.NUMERIC;
export const isCategoricalDataType = (dataType: ScoreDataType) =>
dataType === ScoreDataType.CATEGORICAL;
export const isBooleanDataType = (dataType: ScoreDataType) =>
dataType === ScoreDataType.BOOLEAN;
export const isPresent = <T>(value: T): value is NonNullable<T> =>
value !== null && value !== undefined && value !== "";
export const isScoreUnsaved = (scoreId?: string): boolean => !scoreId;
@@ -28,12 +28,15 @@ const events = {
],
generations: ["export"],
score: [
"create",
"update",
"delete",
"update_form_open",
"create_form_open",
"update_form_submit",
"create_form_submit",
"update_comment",
"delete_comment",
],
score_configs: ["create_form_submit", "manage_configs_item_click"],
models: ["delete_button_click", "new_form_submit", "new_form_open"],
prompts: [
"new_form_submit",
@@ -1,7 +1,7 @@
import { Button } from "@/src/components/ui/button";
import { useHasAccess } from "@/src/features/rbac/utils/checkAccess";
import { api } from "@/src/utils/api";
import { Trash2 } from "lucide-react";
import { Trash } from "lucide-react";
import { useState } from "react";
import {
Popover,
@@ -46,10 +46,6 @@ export function DeletePromptVersion({
},
});
if (!hasAccess) {
return null;
}
return (
<Popover
key={promptVersionId}
@@ -62,8 +58,13 @@ export function DeletePromptVersion({
}}
>
<PopoverTrigger asChild>
<Button variant="outline" type="button" size="icon">
<Trash2 className="h-5 w-5" />
<Button
variant="outline"
type="button"
size="icon"
disabled={!hasAccess}
>
<Trash className="h-5 w-5" />
</Button>
</PopoverTrigger>
<PopoverContent>
@@ -1,7 +1,7 @@
import { Button } from "@/src/components/ui/button";
import { useHasAccess } from "@/src/features/rbac/utils/checkAccess";
import { api } from "@/src/utils/api";
import { Trash2 } from "lucide-react";
import { Trash } from "lucide-react";
import { useState } from "react";
import {
Popover,
@@ -22,15 +22,11 @@ export function DeletePrompt({ promptName }: { promptName: string }) {
},
});
if (!hasAccess) {
return null;
}
return (
<Popover open={isOpen} onOpenChange={() => setIsOpen(!isOpen)}>
<PopoverTrigger asChild>
<Button variant="ghost" size="xs">
<Trash2 className="h-4 w-4" />
<Button variant="ghost" size="xs" disabled={!hasAccess}>
<Trash className="h-4 w-4" />
</Button>
</PopoverTrigger>
<PopoverContent>
@@ -247,37 +247,3 @@ export const PromptDetail = () => {
</div>
);
};
export function UpdatePrompt({
projectId,
prompt,
isLoading,
}: {
projectId: string;
prompt: Prompt | undefined;
isLoading: boolean;
}) {
const hasAccess = useHasAccess({ projectId, scope: "prompts:CUD" });
const handlePromptEdit = () => {
void router.push(
`/project/${projectId}/prompts/${prompt?.id}/edit`,
undefined,
{
shallow: true,
},
);
};
return (
<Button
variant="outline"
size="icon"
onClick={() => handlePromptEdit()}
disabled={!hasAccess}
loading={isLoading}
>
<Pencil className="h-5 w-5" />
</Button>
);
}
@@ -3,7 +3,7 @@ import {
PromptType,
} from "@/src/features/prompts/server/utils/validation";
import { ValidationError } from "@langfuse/shared";
import { jsonSchema } from "@/src/utils/zod";
import { jsonSchema } from "@langfuse/shared";
import { type PrismaClient } from "@langfuse/shared/src/db";
import { LATEST_PROMPT_LABEL } from "@/src/features/prompts/constants";
import { removeLabelsFromPreviousPromptVersions } from "@/src/features/prompts/server/utils/updatePromptLabels";
@@ -13,7 +13,7 @@ import { type Prompt, Prisma } from "@langfuse/shared/src/db";
import { createPrompt } from "../actions/createPrompt";
import { orderByToPrismaSql } from "@/src/features/orderBy/server/orderByToPrisma";
import { promptsTableCols } from "@/src/server/api/definitions/promptsTable";
import { optionalPaginationZod, paginationZod } from "@/src/utils/zod";
import { optionalPaginationZod, paginationZod } from "@langfuse/shared";
import {
orderBy,
singleFilter,
@@ -93,31 +93,47 @@ export const promptRouter = createTRPCRouter({
};
}
const observationCountQuery = 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)
.where("prompts.name", "in", promptNames)
.groupBy("prompts.name");
const compiledQuery = observationCountQuery.compile();
const promptCounts = await ctx.prisma.$queryRawUnsafe<
Array<{
name: string;
count: bigint;
}>
>(compiledQuery.sql, ...compiledQuery.parameters);
const promptCounts = (
await ctx.prisma.$queryRaw<
{
promptName: string;
observationCount: bigint;
}[]
>(
Prisma.sql`
WITH aggregated_prompts AS (
SELECT
ARRAY_AGG(p.id) AS prompt_ids,
p.name
FROM
prompts p
WHERE
p.project_id = ${input.projectId}
GROUP BY
p. "name"
)
SELECT
ap.name AS "promptName",
oc.observation_count AS "observationCount"
FROM
aggregated_prompts ap
LEFT JOIN LATERAL (
SELECT
count(*) AS observation_count
FROM
observations o
WHERE
o.project_id = ${input.projectId}
AND o.prompt_id = ANY (ap.prompt_ids)) AS oc ON TRUE
`,
)
).map((v) => ({ ...v, observationCount: Number(v.observationCount) }));
const joinedPromptsAndCounts = prompts.map((p) => {
const matchedCount = promptCounts.find((c) => c.name === p.name);
const matchedCount = promptCounts.find((c) => c.promptName === p.name);
return {
...p,
observationCount: Number(matchedCount?.count ?? 0),
observationCount: matchedCount?.observationCount ?? 0,
};
});
@@ -626,6 +642,7 @@ export const promptRouter = createTRPCRouter({
LEFT JOIN scores s ON o.trace_id = s.trace_id AND s.observation_id = o.id AND s.project_id = ${input.projectId}
WHERE
o.type = 'GENERATION'
AND s.data_type != 'CATEGORICAL'
AND o.prompt_id IS NOT NULL
AND o.project_id = ${input.projectId}
AND p.id IN (${Prisma.join(input.promptIds)})
@@ -681,6 +698,8 @@ export const promptRouter = createTRPCRouter({
traces_by_prompt_id tp
JOIN prompts AS p ON tp.prompt_id = p.id AND p.project_id = ${input.projectId}
LEFT JOIN scores s ON tp.trace_id = s.trace_id AND s.observation_id IS NULL AND s.project_id = ${input.projectId}
WHERE
s.data_type != 'CATEGORICAL'
), average_scores_by_prompt AS (
SELECT
prompt_id,
@@ -1,5 +1,5 @@
import { z } from "zod";
import { jsonSchema } from "@/src/utils/zod";
import { jsonSchema } from "@langfuse/shared";
import type { Prompt } from "@langfuse/shared";
export const ChatMessageSchema = z.object({
@@ -27,7 +27,7 @@ export const CreateTextPromptSchema = z.object({
type: z.literal(PromptType.Text).optional(),
prompt: z.string(),
config: jsonSchema.nullable().default({}),
tags: z.array(z.string()).optional(),
tags: z.array(z.string()).nullish(),
});
export const CreateChatPromptSchema = z.object({
@@ -36,7 +36,7 @@ export const CreateChatPromptSchema = z.object({
type: z.literal(PromptType.Chat),
prompt: z.array(ChatMessageSchema),
config: jsonSchema.nullable().default({}),
tags: z.array(z.string()).optional(),
tags: z.array(z.string()).nullish(),
});
export const CreatePromptSchema = z.union([
@@ -17,6 +17,9 @@ const scopes = [
"scores:CUD",
"scoreConfigs:CUD",
"scoreConfigs:read",
"project:delete",
"project:update",
"project:transfer",
@@ -57,6 +60,8 @@ export const roleAccessRights: Record<ProjectRole, Scope[]> = {
"objects:tag",
"traces:delete",
"scores:CUD",
"scoreConfigs:CUD",
"scoreConfigs:read",
"project:delete",
"project:update",
"project:transfer",
@@ -87,6 +92,8 @@ export const roleAccessRights: Record<ProjectRole, Scope[]> = {
"objects:tag",
"traces:delete",
"scores:CUD",
"scoreConfigs:CUD",
"scoreConfigs:read",
"datasets:CUD",
"prompts:CUD",
"prompts:read",
@@ -107,6 +114,8 @@ export const roleAccessRights: Record<ProjectRole, Scope[]> = {
"objects:bookmark",
"objects:tag",
"scores:CUD",
"scoreConfigs:CUD",
"scoreConfigs:read",
"datasets:CUD",
"prompts:CUD",
"prompts:read",
@@ -120,6 +129,7 @@ export const roleAccessRights: Record<ProjectRole, Scope[]> = {
VIEWER: [
"prompts:read",
"evalTemplate:read",
"scoreConfigs:read",
"evalJob:read",
"evalJobExecution:read",
],
+1 -1
View File
@@ -3,7 +3,7 @@ import { z } from "zod";
import { cors, runMiddleware } from "@/src/features/public-api/server/cors";
import { DatasetStatus, prisma } from "@langfuse/shared/src/db";
import { verifyAuthHeaderAndReturnScope } from "@/src/features/public-api/server/apiAuth";
import { jsonSchema } from "@/src/utils/zod";
import { jsonSchema } from "@langfuse/shared";
import { v4 as uuidv4 } from "uuid";
import { isPrismaException } from "@/src/utils/exceptions";
+25 -32
View File
@@ -4,7 +4,7 @@ import { z } from "zod";
import { cors, runMiddleware } from "@/src/features/public-api/server/cors";
import { verifyAuthHeaderAndReturnScope } from "@/src/features/public-api/server/apiAuth";
import { isPrismaException } from "@/src/utils/exceptions";
import { jsonSchema } from "@/src/utils/zod";
import { jsonSchema } from "@langfuse/shared";
const DatasetRunItemPostSchema = z
.object({
@@ -16,7 +16,7 @@ const DatasetRunItemPostSchema = z
traceId: z.string().nullish(),
})
.refine((data) => data.observationId || data.traceId, {
message: "ObservationId or traceId must be provided",
message: "observationId or traceId must be provided",
path: ["observationId", "traceId"], // Specify the path of the error
});
@@ -77,39 +77,32 @@ export default async function handler(
});
}
const observation = observationId
? await prisma.observation.findUnique({
where: {
id: observationId,
projectId: authCheck.scope.projectId,
},
})
: undefined;
if (observationId && !observation) {
console.error("Observation not found");
return res.status(404).json({
message: "Observation not found",
});
}
let finalTraceId = traceId;
const trace = traceId
? await prisma.trace.findUnique({
where: { id: traceId, projectId: authCheck.scope.projectId },
})
: undefined;
if (traceId && !trace) {
console.error("Trace not found");
return res.status(404).json({
message: "Trace not found",
});
// Backwards compatibility: historically, dataset run items were linked to observations, not traces
if (!traceId && observationId) {
const observation = observationId
? await prisma.observation.findUnique({
where: {
id: observationId,
projectId: authCheck.scope.projectId,
},
})
: undefined;
if (observationId && !observation) {
console.error("Observation not found");
return res.status(404).json({
message: "Observation not found",
});
}
finalTraceId = observation?.traceId;
}
// double check, should not be necessary due to zod schema + validations above
const saveTraceId = trace?.id ?? observation?.traceId;
if (!!!saveTraceId) {
console.error("Observation or Trace not found");
if (!finalTraceId) {
console.error("No traceId set");
return res.status(404).json({
message: "Observation or Trace not found",
message: "No traceId set",
});
}
@@ -135,8 +128,8 @@ export default async function handler(
const runItem = await prisma.datasetRunItems.create({
data: {
datasetItemId: datasetItemId,
traceId: saveTraceId,
observationId: observation?.id ?? undefined,
traceId: finalTraceId,
observationId,
datasetRunId: run.id,
},
});
+1 -1
View File
@@ -4,7 +4,7 @@ import { cors, runMiddleware } from "@/src/features/public-api/server/cors";
import { prisma } from "@langfuse/shared/src/db";
import { verifyAuthHeaderAndReturnScope } from "@/src/features/public-api/server/apiAuth";
import { isPrismaException } from "@/src/utils/exceptions";
import { jsonSchema, paginationZod } from "@/src/utils/zod";
import { jsonSchema, paginationZod } from "@langfuse/shared";
const CreateDatasetSchema = z.object({
name: z.string(),
+1 -1
View File
@@ -6,7 +6,7 @@ import {
CreateEventEvent,
eventTypes,
ingestionBatchEvent,
} from "@/src/features/public-api/server/ingestion-api-schema";
} from "@langfuse/shared";
import {
handleBatch,
handleBatchResultLegacy,
+1 -1
View File
@@ -8,7 +8,7 @@ import {
LegacyGenerationsCreateSchema,
eventTypes,
ingestionBatchEvent,
} from "@/src/features/public-api/server/ingestion-api-schema";
} from "@langfuse/shared";
import {
handleBatch,
handleBatchResultLegacy,
+2 -2
View File
@@ -10,7 +10,7 @@ import {
type ingestionApiSchema,
eventTypes,
ingestionEvent,
} from "@/src/features/public-api/server/ingestion-api-schema";
} from "@langfuse/shared";
import { type ApiAccessScope } from "@/src/features/public-api/server/types";
import { persistEventMiddleware } from "@/src/server/api/services/event-service";
import { backOff } from "exponential-backoff";
@@ -24,7 +24,7 @@ import { ObservationProcessor } from "../../../server/api/services/EventProcesso
import { ScoreProcessor } from "../../../server/api/services/EventProcessor";
import { isNotNullOrUndefined } from "@/src/utils/types";
import { telemetry } from "@/src/features/telemetry";
import { jsonSchema } from "@/src/utils/zod";
import { jsonSchema } from "@langfuse/shared";
import * as Sentry from "@sentry/nextjs";
import { isPrismaException } from "@/src/utils/exceptions";
import { env } from "@/src/env.mjs";
+18 -9
View File
@@ -3,9 +3,9 @@ import { z } from "zod";
import { cors, runMiddleware } from "@/src/features/public-api/server/cors";
import { Prisma, prisma } from "@langfuse/shared/src/db";
import { verifyAuthHeaderAndReturnScope } from "@/src/features/public-api/server/apiAuth";
import { paginationZod } from "@/src/utils/zod";
import { paginationZod } from "@langfuse/shared";
import { isPrismaException } from "@/src/utils/exceptions";
import { stringDate } from "@/src/features/public-api/server/ingestion-api-schema";
import { stringDate } from "@langfuse/shared";
const GetUsageSchema = z.object({
...paginationZod,
@@ -68,12 +68,15 @@ export default async function handler(
DATE_TRUNC('DAY',
o.start_time) "date",
o.model,
SUM(o.prompt_tokens) inputUsage,
SUM(o.completion_tokens) outputUsage,
SUM(o.total_tokens) totalUsage
count(distinct o.id)::integer as "countObservations",
count(distinct t.id)::integer as "countTraces",
SUM(o.prompt_tokens) "inputUsage",
SUM(o.completion_tokens) "outputUsage",
SUM(o.total_tokens) "totalUsage",
COALESCE(SUM(o.calculated_total_cost), 0)::DOUBLE PRECISION as "totalCost"
FROM
traces t
LEFT JOIN observations o ON o.trace_id = t.id AND o.project_id = t.project_id
LEFT JOIN observations_view o ON o.trace_id = t.id AND o.project_id = t.project_id
WHERE o.start_time IS NOT NULL
AND t.project_id = ${authCheck.scope.projectId}
${traceNameCondition}
@@ -94,11 +97,17 @@ export default async function handler(
json_agg(json_build_object('model',
model,
'inputUsage',
inputUsage,
"inputUsage",
'outputUsage',
outputUsage,
"outputUsage",
'totalUsage',
totalUsage)) daily_usage_json
"totalUsage",
'totalCost',
"totalCost",
'countObservations',
"countObservations",
'countTraces',
"countTraces")) daily_usage_json
FROM
model_usage
GROUP BY
@@ -2,7 +2,7 @@ import { verifyAuthHeaderAndReturnScope } from "@/src/features/public-api/server
import { cors, runMiddleware } from "@/src/features/public-api/server/cors";
import { mapUsageOutput } from "@/src/features/public-api/server/outputSchemaConversion";
import { prisma } from "@langfuse/shared/src/db";
import { paginationZod } from "@/src/utils/zod";
import { paginationZod } from "@langfuse/shared";
import {
Prisma,
type PrismaClient,
@@ -11,7 +11,7 @@ import {
import { type NextApiRequest, type NextApiResponse } from "next";
import { z } from "zod";
import { isPrismaException } from "@/src/utils/exceptions";
import { stringDate } from "@/src/features/public-api/server/ingestion-api-schema";
import { stringDate } from "@langfuse/shared";
const ObservationsGetSchema = z.object({
...paginationZod,
+2 -2
View File
@@ -4,13 +4,13 @@ import { type NextApiRequest, type NextApiResponse } from "next";
import { z } from "zod";
import { cors, runMiddleware } from "@/src/features/public-api/server/cors";
import { verifyAuthHeaderAndReturnScope } from "@/src/features/public-api/server/apiAuth";
import { paginationZod } from "@/src/utils/zod";
import { paginationZod } from "@langfuse/shared";
import {
ScoreBody,
eventTypes,
ingestionBatchEvent,
stringDate,
} from "@/src/features/public-api/server/ingestion-api-schema";
} from "@langfuse/shared";
import { v4 } from "uuid";
import {
handleBatch,
+1 -1
View File
@@ -8,7 +8,7 @@ import {
LegacySpanPostSchema,
eventTypes,
ingestionBatchEvent,
} from "@/src/features/public-api/server/ingestion-api-schema";
} from "@langfuse/shared";
import {
handleBatch,
handleBatchResultLegacy,
+8 -6
View File
@@ -4,16 +4,12 @@ import { cors, runMiddleware } from "@/src/features/public-api/server/cors";
import { prisma } from "@langfuse/shared/src/db";
import { verifyAuthHeaderAndReturnScope } from "@/src/features/public-api/server/apiAuth";
import { Prisma, type Trace } from "@langfuse/shared/src/db";
import { paginationZod } from "@/src/utils/zod";
import { paginationZod } from "@langfuse/shared";
import {
handleBatch,
handleBatchResultLegacy,
} from "@/src/pages/api/public/ingestion";
import {
TraceBody,
eventTypes,
stringDate,
} from "@/src/features/public-api/server/ingestion-api-schema";
import { TraceBody, eventTypes, stringDate } from "@langfuse/shared";
import { v4 } from "uuid";
import { telemetry } from "@/src/features/telemetry";
import { orderByToPrismaSql } from "@/src/features/orderBy/server/orderByToPrisma";
@@ -25,6 +21,7 @@ const GetTracesSchema = z.object({
userId: z.string().nullish(),
name: z.string().nullish(),
tags: z.union([z.array(z.string()), z.string()]).nullish(),
sessionId: z.string().nullish(),
fromTimestamp: stringDate,
orderBy: z
.string() // orderBy=timestamp.asc
@@ -105,6 +102,9 @@ export default async function handler(
", ",
)}] <@ t."tags"`
: Prisma.empty;
const sessionCondition = obj.sessionId
? Prisma.sql`AND t."session_id" = ${obj.sessionId}`
: Prisma.empty;
const fromTimestampCondition = obj.fromTimestamp
? Prisma.sql`AND t."timestamp" >= ${obj.fromTimestamp}::timestamp with time zone at time zone 'UTC'`
: Prisma.empty;
@@ -145,6 +145,7 @@ export default async function handler(
${userCondition}
${nameCondition}
${tagsCondition}
${sessionCondition}
${orderByCondition}
LIMIT ${obj.limit} OFFSET ${skipValue}
) AS t
@@ -168,6 +169,7 @@ export default async function handler(
projectId: authCheck.scope.projectId,
name: obj.name ? obj.name : undefined,
userId: obj.userId ? obj.userId : undefined,
sessionId: obj.sessionId ? obj.sessionId : undefined,
timestamp: obj.fromTimestamp
? { gte: new Date(obj.fromTimestamp) }
: undefined,
+1 -1
View File
@@ -3,7 +3,7 @@ import { z } from "zod";
import { cors, runMiddleware } from "@/src/features/public-api/server/cors";
import { prisma } from "@langfuse/shared/src/db";
import { verifyAuthHeaderAndReturnScope } from "@/src/features/public-api/server/apiAuth";
import { paginationZod } from "@/src/utils/zod";
import { paginationZod } from "@langfuse/shared";
import { isPrismaException } from "@/src/utils/exceptions";
const GetUsersSchema = z.object({
@@ -16,6 +16,7 @@ import { formatIntervalSeconds } from "@/src/utils/dates";
import { GroupedScoreBadges } from "@/src/components/grouped-score-badge";
import useColumnVisibility from "@/src/features/column-visibility/hooks/useColumnVisibility";
import { Skeleton } from "@/src/components/ui/skeleton";
import { ScoreDataType } from "@langfuse/shared";
type PromptVersionTableRow = {
version: number;
@@ -247,6 +248,7 @@ export default function PromptVersionTable() {
scores={Object.entries(scores).map(([k, v]) => ({
name: k,
value: v,
dataType: ScoreDataType.NUMERIC, // numeric and boolean values treated as numeric
}))}
variant="headings"
/>
@@ -273,6 +275,7 @@ export default function PromptVersionTable() {
scores={Object.entries(scores).map(([k, v]) => ({
name: k,
value: v,
dataType: ScoreDataType.NUMERIC, // numeric and boolean values treated as numeric
}))}
variant="headings"
/>
@@ -16,6 +16,7 @@ import { Card } from "@tremor/react";
import { Button } from "@/src/components/ui/button";
import Link from "next/link";
import { LlmApiKeyList } from "@/src/features/public-api/components/LLMApiKeyList";
import { ScoreConfigSettings } from "@/src/features/manual-scoring/components/ScoreConfigSettings";
export default function SettingsPage() {
const router = useRouter();
@@ -29,6 +30,7 @@ export default function SettingsPage() {
<LlmApiKeyList projectId={projectId} />
<ProjectMembersTable projectId={projectId} />
<ProjectUsageChart projectId={projectId} />
<ScoreConfigSettings projectId={projectId} />
<Integrations projectId={projectId} />
<Instructions />
<RenameProject projectId={projectId} />
@@ -107,7 +109,7 @@ function Instructions() {
<li key={itemIdx}>
<div className="group relative flex items-start space-x-3 py-4">
<div className="flex-shrink-0">
<span className="group-hover:border-primary-accent group-hover:text-primary-accent inline-flex h-10 w-10 items-center justify-center rounded-lg border border-border text-muted-foreground">
<span className="inline-flex h-10 w-10 items-center justify-center rounded-lg border border-border text-muted-foreground group-hover:border-primary-accent group-hover:text-primary-accent">
<item.icon className="h-6 w-6" aria-hidden="true" />
</span>
</div>
@@ -15,6 +15,7 @@ import { Switch } from "@/src/components/ui/switch";
import { env } from "@/src/env.mjs";
import { usePostHogClientCapture } from "@/src/features/posthog-analytics/usePostHogClientCapture";
import { posthogIntegrationFormSchema } from "@/src/features/posthog-integration/types";
import { useHasAccess } from "@/src/features/rbac/utils/checkAccess";
import { api } from "@/src/utils/api";
import { type RouterOutput } from "@/src/utils/types";
import { zodResolver } from "@hookform/resolvers/zod";
@@ -28,7 +29,13 @@ import { type z } from "zod";
export default function PosthogIntegrationSettings() {
const router = useRouter();
const projectId = router.query.projectId as string;
const state = api.posthogIntegration.get.useQuery({ projectId });
const hasAccess = useHasAccess({ projectId, scope: "integrations:CRUD" });
const state = api.posthogIntegration.get.useQuery(
{ projectId },
{
enabled: hasAccess,
},
);
if (env.NEXT_PUBLIC_LANGFUSE_CLOUD_REGION === undefined) return null;
return (
@@ -46,7 +53,7 @@ export default function PosthogIntegrationSettings() {
</Button>
}
status={
state.isInitialLoading
state.isInitialLoading || !hasAccess
? undefined
: state.data?.enabled
? "active"
@@ -63,15 +70,20 @@ export default function PosthogIntegrationSettings() {
on a daily schedule to PostHog. When first activated, it will sync all
historical data from the beginning of your project.
</p>
<div className="flex flex-col gap-10"></div>
{!state.isInitialLoading && (
{!hasAccess && (
<p className="text-sm">
You current role does not grant you access to these settings, please
reach out to your project admin or owner.
</p>
)}
{hasAccess && (
<>
<Header level="h3" title="Configuration" />
<Card className="p-4">
<PostHogIntegrationSettings
state={state.data}
projectId={projectId}
isLoading={state.isLoading}
/>
</Card>
</>
@@ -97,9 +109,11 @@ export default function PosthogIntegrationSettings() {
const PostHogIntegrationSettings = ({
state,
projectId,
isLoading,
}: {
state?: RouterOutput["posthogIntegration"]["get"];
projectId: string;
isLoading: boolean;
}) => {
const capture = usePostHogClientCapture();
const posthogForm = useForm<z.infer<typeof posthogIntegrationFormSchema>>({
@@ -109,6 +123,7 @@ const PostHogIntegrationSettings = ({
posthogProjectApiKey: state?.posthogApiKey ?? "",
enabled: state?.enabled ?? false,
},
disabled: isLoading,
});
useEffect(() => {
@@ -205,12 +220,14 @@ const PostHogIntegrationSettings = ({
<Button
loading={mut.isLoading}
onClick={posthogForm.handleSubmit(onSubmit)}
disabled={isLoading}
>
Save
</Button>
<Button
variant="ghost"
loading={mutDelete.isLoading}
disabled={isLoading || !!!state}
onClick={() => {
if (
confirm(
+39 -15
View File
@@ -1,19 +1,24 @@
import Header from "@/src/components/layouts/header";
import { api } from "@/src/utils/api";
import { type RouterOutput, type RouterInput } from "@/src/utils/types";
import { useEffect, useState } from "react";
import TableLink from "@/src/components/table/table-link";
import { DataTable } from "@/src/components/table/data-table";
import { useRouter } from "next/router";
import { compactNumberFormatter, usdFormatter } from "@/src/utils/numbers";
import { useEffect } from "react";
import { NumberParam, useQueryParams, withDefault } from "use-query-params";
import { DataTableToolbar } from "@/src/components/table/data-table-toolbar";
import { GroupedScoreBadges } from "@/src/components/grouped-score-badge";
import { type Score } from "@langfuse/shared";
import { useQueryParams, withDefault, NumberParam } from "use-query-params";
import { useDetailPageLists } from "@/src/features/navigate-detail-pages/context";
import { FullScreenPage } from "@/src/components/layouts/full-screen-page";
import Header from "@/src/components/layouts/header";
import { DataTable } from "@/src/components/table/data-table";
import TableLink from "@/src/components/table/table-link";
import { type LangfuseColumnDef } from "@/src/components/table/types";
import { Skeleton } from "@/src/components/ui/skeleton";
import { FullScreenPage } from "@/src/components/layouts/full-screen-page";
import { useQueryFilterState } from "@/src/features/filters/hooks/useFilterState";
import { useDetailPageLists } from "@/src/features/navigate-detail-pages/context";
import { api } from "@/src/utils/api";
import { compactNumberFormatter, usdFormatter } from "@/src/utils/numbers";
import { type RouterInput, type RouterOutput } from "@/src/utils/types";
import { type Score } from "@langfuse/shared";
import { utcDateOffsetByDays } from "@/src/utils/dates";
import { useSession } from "next-auth/react";
import { usersTableCols } from "@/src/server/api/definitions/usersTable";
export type ScoreFilterInput = Omit<RouterInput["users"]["all"], "projectId">;
@@ -26,8 +31,22 @@ type RowData = {
export default function UsersPage() {
const router = useRouter();
const session = useSession();
const projectId = router.query.projectId as string;
const [queryOptions] = useState<ScoreFilterInput>({});
const [userFilterState, setUserFilterState] = useQueryFilterState(
[
{
column: "timestamp",
type: "datetime",
operator: ">",
value: utcDateOffsetByDays(
session.data?.environment.defaultTableDateTimeOffset ?? -7,
),
},
],
"users",
);
const { setDetailPageList } = useDetailPageLists();
@@ -37,7 +56,7 @@ export default function UsersPage() {
});
const users = api.users.all.useQuery({
...queryOptions,
filter: userFilterState,
page: paginationState.pageIndex,
limit: paginationState.pageSize,
projectId,
@@ -212,7 +231,12 @@ export default function UsersPage() {
href: "https://langfuse.com/docs/user-explorer",
}}
/>
<DataTableToolbar
filterColumnDefinition={usersTableCols}
filterState={userFilterState}
setFilterState={setUserFilterState}
columns={columns}
/>
<DataTable
columns={columns}
data={
@@ -2,6 +2,7 @@ import {
type OptionsDefinition,
type ColumnDefinition,
ScoreSource,
ScoreDataType,
} from "@langfuse/shared";
export const scoresTableCols: ColumnDefinition[] = [
@@ -36,6 +37,13 @@ export const scoresTableCols: ColumnDefinition[] = [
internal: 's."source"::text',
options: Object.values(ScoreSource).map((value) => ({ value })),
},
{
name: "Data Type",
id: "dataType",
type: "stringOptions",
internal: 's."data_type"::text',
options: Object.values(ScoreDataType).map((value) => ({ value })),
},
{
name: "Name",
id: "name",
@@ -0,0 +1,10 @@
import { type ColumnDefinition } from "@langfuse/shared";
export const usersTableCols: ColumnDefinition[] = [
{
name: "Timestamp",
id: "timestamp",
type: "datetime",
internal: 't."timestamp"',
},
];
+4
View File
@@ -16,6 +16,8 @@ import { modelRouter } from "@/src/server/api/routers/models";
import { evalRouter } from "@/src/ee/features/evals/server/router";
import { posthogIntegrationRouter } from "@/src/features/posthog-integration/posthog-integration-router";
import { llmApiKeyRouter } from "@/src/features/llm-api-key/server/router";
import { scoreConfigsRouter } from "@/src/server/api/routers/scoreConfigs";
import { publicRouter } from "@/src/server/api/routers/public";
/**
* This is the primary router for your server.
@@ -27,6 +29,7 @@ export const appRouter = createTRPCRouter({
sessions: sessionRouter,
generations: generationsRouter,
scores: scoresRouter,
scoreConfigs: scoreConfigsRouter,
dashboard: dashboardRouter,
projects: projectsRouter,
users: userRouter,
@@ -40,6 +43,7 @@ export const appRouter = createTRPCRouter({
evals: evalRouter,
posthogIntegration: posthogIntegrationRouter,
llmApiKey: llmApiKeyRouter,
public: publicRouter,
});
// export type definition of API

Some files were not shown because too many files have changed in this diff Show More