Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
cf42dd726d | ||
|
|
8aeec63c07 | ||
|
|
0cd3fcfbeb | ||
|
|
a880bdb3b2 | ||
|
|
5c6d3e14f2 | ||
|
|
6ccc57312c | ||
|
|
c4831d1b25 | ||
|
|
cc08408f0a | ||
|
|
6a41aadc98 | ||
|
|
9d6efbae19 | ||
|
|
74be1e87c1 | ||
|
|
dd823da1da | ||
|
|
992f0affbc | ||
|
|
b8c192324c | ||
|
|
1416e760ff | ||
|
|
4266b46dd9 | ||
|
|
1933f8f201 | ||
|
|
1e35eb3a66 | ||
|
|
5478c5e317 | ||
|
|
140452c89e | ||
|
|
7ac92e83da | ||
|
|
8a65bad7d4 | ||
|
|
6c6348bfbe | ||
|
|
9ff7fc942d | ||
|
|
a3f59e7e95 | ||
|
|
445bdcb65c | ||
|
|
8230aa79b3 | ||
|
|
57ca7fb478 | ||
|
|
2db88101e1 | ||
|
|
669167002a | ||
|
|
2f6ade9354 | ||
|
|
86f44df5dc | ||
|
|
34da790d5e | ||
|
|
c16d86f7e5 | ||
|
|
1b2b8cc43a | ||
|
|
3903790086 | ||
|
|
6da1280c43 | ||
|
|
85343bf26c | ||
|
|
e3b4dc1928 | ||
|
|
a4939bacd8 | ||
|
|
4441942cf5 | ||
|
|
6597b02c09 | ||
|
|
0df3ad27dc | ||
|
|
696d458a27 | ||
|
|
fd18ab605d | ||
|
|
d47d14eecd | ||
|
|
24a81e1589 |
@@ -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
@@ -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.
|
||||
|
||||
|
||||
@@ -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
@@ -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",
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "langfuse",
|
||||
"version": "2.47.4",
|
||||
"version": "2.49.2",
|
||||
"author": "engineering@langfuse.com",
|
||||
"license": "MIT",
|
||||
"private": true,
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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;
|
||||
|
||||
+2
@@ -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");
|
||||
+2
@@ -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;
|
||||
+5
@@ -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');
|
||||
+14
@@ -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";
|
||||
+3
@@ -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";
|
||||
@@ -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)
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
+8
-8
@@ -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(),
|
||||
@@ -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" ||
|
||||
|
||||
@@ -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];
|
||||
|
||||
@@ -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)
|
||||
),
|
||||
};
|
||||
|
||||
Generated
+204
-412
@@ -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
@@ -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
@@ -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",
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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": "",
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,4 +1,4 @@
|
||||
import { paginationZod } from "@/src/utils/zod";
|
||||
import { paginationZod } from "@langfuse/shared";
|
||||
import { ZodError } from "zod";
|
||||
|
||||
// Create test cases
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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",
|
||||
},
|
||||
];
|
||||
|
||||
|
||||
@@ -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", {
|
||||
|
||||
@@ -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>
|
||||
))}
|
||||
</>
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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}) ↗
|
||||
</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}) ↗
|
||||
</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"
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -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,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}
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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";
|
||||
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
@@ -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 };
|
||||
|
||||
@@ -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 @@
|
||||
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,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",
|
||||
],
|
||||
|
||||
@@ -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";
|
||||
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
});
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -6,7 +6,7 @@ import {
|
||||
CreateEventEvent,
|
||||
eventTypes,
|
||||
ingestionBatchEvent,
|
||||
} from "@/src/features/public-api/server/ingestion-api-schema";
|
||||
} from "@langfuse/shared";
|
||||
import {
|
||||
handleBatch,
|
||||
handleBatchResultLegacy,
|
||||
|
||||
@@ -8,7 +8,7 @@ import {
|
||||
LegacyGenerationsCreateSchema,
|
||||
eventTypes,
|
||||
ingestionBatchEvent,
|
||||
} from "@/src/features/public-api/server/ingestion-api-schema";
|
||||
} from "@langfuse/shared";
|
||||
import {
|
||||
handleBatch,
|
||||
handleBatchResultLegacy,
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -8,7 +8,7 @@ import {
|
||||
LegacySpanPostSchema,
|
||||
eventTypes,
|
||||
ingestionBatchEvent,
|
||||
} from "@/src/features/public-api/server/ingestion-api-schema";
|
||||
} from "@langfuse/shared";
|
||||
import {
|
||||
handleBatch,
|
||||
handleBatchResultLegacy,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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"',
|
||||
},
|
||||
];
|
||||
@@ -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
Reference in New Issue
Block a user