Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
bc323772c1 | ||
|
|
fcef737dc0 | ||
|
|
4bf77d68ed | ||
|
|
d7ec924ed8 | ||
|
|
cefaf56613 | ||
|
|
6aff9b0f4a | ||
|
|
815de149aa | ||
|
|
bc8923485e | ||
|
|
1a60425692 | ||
|
|
1d3ac98123 | ||
|
|
de67ec1a02 | ||
|
|
9510995a67 | ||
|
|
4623d76155 | ||
|
|
6a6abc6bd8 | ||
|
|
ddee827e22 | ||
|
|
e327db8bbd | ||
|
|
9229d759e1 | ||
|
|
d8d8ab7060 | ||
|
|
b851359923 | ||
|
|
b36ea74314 | ||
|
|
b2f103549a | ||
|
|
f660b80438 | ||
|
|
5bbfffef50 | ||
|
|
cb6f95dc93 | ||
|
|
8345ce48fc | ||
|
|
f71d28900a | ||
|
|
14eaf62e45 | ||
|
|
f8e61c2659 | ||
|
|
3b205b45ce | ||
|
|
7260c44486 | ||
|
|
92ec2b60e2 | ||
|
|
a856488f2d | ||
|
|
a18f09fd8d | ||
|
|
2a95dd1da0 | ||
|
|
04ebd5994f | ||
|
|
268b741a3a | ||
|
|
8b686e9b31 | ||
|
|
e802a5c23a | ||
|
|
5180e2382e | ||
|
|
2cae1253d3 | ||
|
|
be2414a82c | ||
|
|
95e63c6f78 | ||
|
|
32b6502d13 | ||
|
|
1bc33c7b92 | ||
|
|
4967b20478 | ||
|
|
1130894cdc | ||
|
|
af276e9e78 | ||
|
|
d69dbd81a2 | ||
|
|
7e807c0de8 | ||
|
|
cfa2ac6da9 | ||
|
|
952a550847 | ||
|
|
6199903117 | ||
|
|
88cdb8999d | ||
|
|
9ad2ef1f91 | ||
|
|
7a9717cb5e | ||
|
|
ce31a806c7 | ||
|
|
e7e6cf1b99 | ||
|
|
9f76457f22 |
@@ -1,86 +0,0 @@
|
||||
# When adding additional environment variables, the schema in "/src/env.mjs"
|
||||
# should be updated accordingly.
|
||||
|
||||
# Prisma
|
||||
# https://www.prisma.io/docs/reference/database-reference/connection-urls#env
|
||||
DIRECT_URL="postgresql://postgres:postgres@localhost:5432/postgres"
|
||||
DATABASE_URL="postgresql://postgres:postgres@localhost:5432/postgres"
|
||||
|
||||
# Clickhouse
|
||||
CLICKHOUSE_MIGRATION_URL="clickhouse://localhost:9000"
|
||||
CLICKHOUSE_URL="http://localhost:8123"
|
||||
CLICKHOUSE_USER="clickhouse"
|
||||
CLICKHOUSE_PASSWORD="clickhouse"
|
||||
CLICKHOUSE_CLUSTER_ENABLED="false"
|
||||
|
||||
# Next Auth
|
||||
# You can generate a new secret on the command line with:
|
||||
# openssl rand -base64 32
|
||||
# https://next-auth.js.org/configuration/options#secret
|
||||
# NEXTAUTH_SECRET=""
|
||||
NEXTAUTH_URL="http://localhost:3000"
|
||||
NEXTAUTH_SECRET="secret"
|
||||
|
||||
# Langfuse Cloud Environment
|
||||
NEXT_PUBLIC_LANGFUSE_CLOUD_REGION="DEV"
|
||||
|
||||
# Langfuse experimental features
|
||||
LANGFUSE_ENABLE_EXPERIMENTAL_FEATURES="true"
|
||||
|
||||
# Salt for API key hashing
|
||||
SALT="salt"
|
||||
|
||||
# Email
|
||||
EMAIL_FROM_ADDRESS="" # Defines the email address to use as the from address.
|
||||
SMTP_CONNECTION_URL="" # Defines the connection url for smtp server.
|
||||
|
||||
# S3 Batch Exports
|
||||
LANGFUSE_S3_BATCH_EXPORT_ENABLED=true
|
||||
LANGFUSE_S3_BATCH_EXPORT_BUCKET=langfuse
|
||||
LANGFUSE_S3_BATCH_EXPORT_ACCESS_KEY_ID=minio
|
||||
LANGFUSE_S3_BATCH_EXPORT_SECRET_ACCESS_KEY=miniosecret
|
||||
LANGFUSE_S3_BATCH_EXPORT_REGION=us-east-1
|
||||
LANGFUSE_S3_BATCH_EXPORT_ENDPOINT=http://localhost:9090
|
||||
## Necessary for minio compatibility
|
||||
LANGFUSE_S3_BATCH_EXPORT_FORCE_PATH_STYLE=true
|
||||
LANGFUSE_S3_BATCH_EXPORT_PREFIX=exports/
|
||||
|
||||
# # S3 Media Upload LOCAL
|
||||
LANGFUSE_S3_MEDIA_UPLOAD_BUCKET=langfuse
|
||||
LANGFUSE_S3_MEDIA_UPLOAD_ACCESS_KEY_ID=minio
|
||||
LANGFUSE_S3_MEDIA_UPLOAD_SECRET_ACCESS_KEY=miniosecret
|
||||
LANGFUSE_S3_MEDIA_UPLOAD_REGION=us-east-1
|
||||
LANGFUSE_S3_MEDIA_UPLOAD_ENDPOINT=http://localhost:9090
|
||||
## Necessary for minio compatibility
|
||||
LANGFUSE_S3_MEDIA_UPLOAD_FORCE_PATH_STYLE=true
|
||||
LANGFUSE_S3_MEDIA_UPLOAD_PREFIX=media/
|
||||
|
||||
# S3 Event Bucket Upload
|
||||
## Set to true to test uploading all events to S3
|
||||
LANGFUSE_S3_EVENT_UPLOAD_BUCKET=langfuse
|
||||
LANGFUSE_S3_EVENT_UPLOAD_ACCESS_KEY_ID=minio
|
||||
LANGFUSE_S3_EVENT_UPLOAD_SECRET_ACCESS_KEY=miniosecret
|
||||
LANGFUSE_S3_EVENT_UPLOAD_REGION=us-east-1
|
||||
LANGFUSE_S3_EVENT_UPLOAD_ENDPOINT=http://localhost:9090
|
||||
## Necessary for minio compatibility
|
||||
LANGFUSE_S3_EVENT_UPLOAD_FORCE_PATH_STYLE=true
|
||||
LANGFUSE_S3_EVENT_UPLOAD_PREFIX=events/
|
||||
|
||||
# Set during docker build of application
|
||||
# Used to disable environment verification at build time
|
||||
# DOCKER_BUILD=1
|
||||
|
||||
REDIS_HOST="127.0.0.1"
|
||||
REDIS_PORT=6379
|
||||
REDIS_AUTH="myredissecret"
|
||||
|
||||
# openssl rand -hex 32 used only here
|
||||
ENCRYPTION_KEY=0000000000000000000000000000000000000000000000000000000000000000
|
||||
|
||||
# speeds up local development by not executing init scripts on server startup
|
||||
NEXT_PUBLIC_LANGFUSE_RUN_NEXT_INIT="false"
|
||||
|
||||
LANGFUSE_READ_FROM_POSTGRES_ONLY=true
|
||||
LANGFUSE_RETURN_FROM_CLICKHOUSE=false
|
||||
LANGFUSE_READ_DASHBOARDS_FROM_CLICKHOUSE=false
|
||||
LANGFUSE_READ_FROM_CLICKHOUSE_ONLY=false
|
||||
@@ -111,8 +111,9 @@ jobs:
|
||||
- name: Load default env
|
||||
run: |
|
||||
cp .env.dev.example .env
|
||||
grep -v -e '^LANGFUSE_S3_BATCH_EXPORT_ENABLED=' -e '^REDIS_HOST=' -e '^NEXT_PUBLIC_LANGFUSE_RUN_NEXT_INIT=' .env.dev.legacy.example > .env
|
||||
echo "LANGFUSE_POSTGRES_INGESTION_ENABLED=true" >> .env
|
||||
grep -v -e '^LANGFUSE_S3_BATCH_EXPORT_ENABLED=' -e '^NEXT_PUBLIC_LANGFUSE_RUN_NEXT_INIT=' .env.dev.example > .env
|
||||
echo "LANGFUSE_INGESTION_QUEUE_DELAY_MS=1" >> .env
|
||||
echo "LANGFUSE_INGESTION_CLICKHOUSE_WRITE_INTERVAL_MS=1" >> .env
|
||||
- name: Run + migrate
|
||||
run: |
|
||||
docker compose -f docker-compose.dev.yml up -d
|
||||
@@ -184,7 +185,9 @@ jobs:
|
||||
- name: Load default env
|
||||
run: |
|
||||
cp .env.dev${{ matrix.blob-provider }}.example .env
|
||||
grep -v -e '^LANGFUSE_S3_BATCH_EXPORT_ENABLED=' -e '^REDIS_HOST=' -e '^NEXT_PUBLIC_LANGFUSE_RUN_NEXT_INIT=' .env.dev${{ matrix.blob-provider }}.example > .env
|
||||
grep -v -e '^LANGFUSE_S3_BATCH_EXPORT_ENABLED=' -e '^NEXT_PUBLIC_LANGFUSE_RUN_NEXT_INIT=' .env.dev${{ matrix.blob-provider }}.example > .env
|
||||
echo "LANGFUSE_INGESTION_QUEUE_DELAY_MS=1" >> .env
|
||||
echo "LANGFUSE_INGESTION_CLICKHOUSE_WRITE_INTERVAL_MS=1" >> .env
|
||||
- name: Run + migrate
|
||||
run: |
|
||||
docker compose -f docker-compose.dev${{ matrix.blob-provider }}.yml up -d
|
||||
|
||||
@@ -39,7 +39,6 @@ yarn-error.log*
|
||||
!.env.dev.example
|
||||
!.env.dev-azure.example
|
||||
!.env.prod.example
|
||||
!.env.dev.legacy.example
|
||||
|
||||
# vercel
|
||||
.vercel
|
||||
|
||||
+33
-29
@@ -121,36 +121,40 @@ flowchart TB
|
||||
end
|
||||
end
|
||||
|
||||
DB[Postgres Database]
|
||||
Redis[Redis Cache/Queue]
|
||||
Clickhouse[Clickhouse Database]
|
||||
subgraph s9 ["VPC (US and EU separated)"]
|
||||
DB[Postgres Database]
|
||||
Redis[Redis Cache/Queue]
|
||||
Clickhouse[Clickhouse Database]
|
||||
|
||||
subgraph s1["Application (langfuse/langfuse/web)"]
|
||||
API[Public HTTP API]
|
||||
G[TRPC API]
|
||||
I[NextAuth]
|
||||
H[React Frontend]
|
||||
ORM
|
||||
H --> G
|
||||
H --> I
|
||||
G --> I
|
||||
G --- ORM
|
||||
API --- ORM
|
||||
I --- ORM
|
||||
end
|
||||
|
||||
subgraph s5["Application (langfuse/langfuse/worker)"]
|
||||
Worker
|
||||
end
|
||||
|
||||
Worker --- DB
|
||||
Worker --- Redis
|
||||
Worker --- Clickhouse
|
||||
|
||||
ORM --- DB
|
||||
ORM --- Redis
|
||||
ORM --- Clickhouse
|
||||
|
||||
|
||||
subgraph s1["Application (langfuse/langfuse/web)"]
|
||||
API[Public HTTP API]
|
||||
G[TRPC API]
|
||||
I[NextAuth]
|
||||
H[React Frontend]
|
||||
ORM
|
||||
H --> G
|
||||
H --> I
|
||||
G --> I
|
||||
G --- ORM
|
||||
API --- ORM
|
||||
I --- ORM
|
||||
end
|
||||
|
||||
subgraph s5["Application (langfuse/langfuse/worker)"]
|
||||
Worker
|
||||
end
|
||||
|
||||
Worker --- DB
|
||||
Worker --- Redis
|
||||
Worker --- Clickhouse
|
||||
|
||||
ORM --- DB
|
||||
ORM --- Redis
|
||||
ORM --- Clickhouse
|
||||
|
||||
JS --- API
|
||||
Python --- API
|
||||
```
|
||||
@@ -397,7 +401,7 @@ The background color of the following component will be `hsl(var(--primary))` an
|
||||
| --primary-accent | Primary accent color used for branding | Layout |
|
||||
| --hover-primary-accent | Primary accent color used for hover effects for links | SignIn and AuthCloudRegionSwitch |
|
||||
| --muted-green | Muted green for Event label | ObservationTree |
|
||||
| --muted-orange | Muted orange for Generation label | ObservationTree |
|
||||
| --muted-magenta | Muted magenta for Generation label | ObservationTree |
|
||||
| --muted-blue | Muted blue for Span label | ObservationTree |
|
||||
| --muted-gray | Muted gray for disabled status badges | StatusBadge |
|
||||
| --accent-light-green | Light green accent for background of output and assistant messages | IOPreview, Generations, Traces |
|
||||
@@ -439,7 +443,7 @@ You can update the default AI models and prices by adding or updating an entry i
|
||||
Please note that
|
||||
|
||||
- prices are in USD
|
||||
- the list is ordered by ID, so make sure to keep this order
|
||||
- the list is ordered by ID, so make sure to keep this order and insert new models at the end of the list
|
||||
- the `updated_at` field must be updated with the current date in ISO 8601 format. Otherwise, the change will be ignored.
|
||||
|
||||
### Transition period until V3 release
|
||||
|
||||
@@ -1,13 +1,19 @@
|
||||
_We are Hiring_
|
||||
Join us in scaling Langfuse in Berlin, Germany. We are an open source company, we hire in person, we are only hiring technical talent.
|
||||
Join us in building out Langfuse in Berlin, Germany. Langfuse is the open source LLM engineering platform: we build tooling to help developers [build & improve LLM applications](https://langfuse.com/docs).
|
||||
|
||||
We are an open source company, we hire in person (4+ days a week), we only hire excellent technical talent. Find more information on our [careers page](https://langfuse.com/careers)
|
||||
|
||||
_Open Roles_
|
||||
|
||||
- Product Engineer, 70-130k EUR, 0.25-0.75% Equity, https://www.ycombinator.com/companies/langfuse/jobs/aAvmoFB-product-engineer
|
||||
- Developer Advocate, 60-110k EUR, 0.25-0.5% Equity, https://www.ycombinator.com/companies/langfuse/jobs/uHysbKH-developer-advocate-devrel
|
||||
- Product Engineer, 70-130k EUR, 0.1-0.35% Equity, https://www.ycombinator.com/companies/langfuse/jobs/aAvmoFB-product-engineer
|
||||
- Backend Engineer, 70-130k EUR, 0.1-0.35% Equity, https://www.ycombinator.com/companies/langfuse/jobs/1bO16H6-backend-engineer
|
||||
- Design Engineer, 70-130k EUR, 0.1-0.35% Equity, https://www.ycombinator.com/companies/langfuse/jobs/mDquP95-design-engineer
|
||||
- Developer Advocate, 70-130k EUR, 0.1-0.35% Equity, https://www.ycombinator.com/companies/langfuse/jobs/uHysbKH-developer-advocate-devrel
|
||||
|
||||
|
||||
_More Info_
|
||||
|
||||
- https://langfuse.com/careers
|
||||
- https://langfuse.com/docs
|
||||
- https://langfuse.com/why
|
||||
- https://langfuse.com/changelog
|
||||
|
||||
@@ -63,10 +63,6 @@ services:
|
||||
LANGFUSE_INIT_USER_EMAIL: ${LANGFUSE_INIT_USER_EMAIL:-}
|
||||
LANGFUSE_INIT_USER_NAME: ${LANGFUSE_INIT_USER_NAME:-}
|
||||
LANGFUSE_INIT_USER_PASSWORD: ${LANGFUSE_INIT_USER_PASSWORD:-}
|
||||
LANGFUSE_SDK_CI_SYNC_PROCESSING_ENABLED: ${LANGFUSE_SDK_CI_SYNC_PROCESSING_ENABLED:-false}
|
||||
LANGFUSE_READ_FROM_POSTGRES_ONLY: ${LANGFUSE_READ_FROM_POSTGRES_ONLY:-false}
|
||||
LANGFUSE_READ_FROM_CLICKHOUSE_ONLY: ${LANGFUSE_READ_FROM_CLICKHOUSE_ONLY:-true}
|
||||
LANGFUSE_RETURN_FROM_CLICKHOUSE: ${LANGFUSE_RETURN_FROM_CLICKHOUSE:-true}
|
||||
|
||||
clickhouse:
|
||||
image: clickhouse/clickhouse-server
|
||||
|
||||
@@ -378,10 +378,10 @@ types:
|
||||
type: string
|
||||
startDate:
|
||||
docs: Apply only to generations which are newer than this ISO date.
|
||||
type: optional<date>
|
||||
type: optional<datetime>
|
||||
unit:
|
||||
docs: Unit used by this model.
|
||||
type: ModelUsageUnit
|
||||
type: optional<ModelUsageUnit>
|
||||
inputPrice:
|
||||
docs: Price (USD) per input unit
|
||||
type: optional<double>
|
||||
|
||||
@@ -58,7 +58,7 @@ types:
|
||||
type: optional<datetime>
|
||||
unit:
|
||||
docs: Unit used by this model.
|
||||
type: commons.ModelUsageUnit
|
||||
type: optional<commons.ModelUsageUnit>
|
||||
inputPrice:
|
||||
docs: Price (USD) per input unit
|
||||
type: optional<double>
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
{
|
||||
"organization": "finto",
|
||||
"organization": "langfuse",
|
||||
"version": "0.43.7"
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "langfuse",
|
||||
"version": "3.6.2",
|
||||
"version": "3.9.0",
|
||||
"author": "engineering@langfuse.com",
|
||||
"license": "MIT",
|
||||
"private": true,
|
||||
|
||||
@@ -73,7 +73,7 @@
|
||||
"@types/bcryptjs": "^2.4.6",
|
||||
"axios": "^1.7.7",
|
||||
"bcryptjs": "^2.4.3",
|
||||
"bullmq": "^5.12.10",
|
||||
"bullmq": "^5.34.10",
|
||||
"dd-trace": "^5.23.1",
|
||||
"decimal.js": "^10.4.3",
|
||||
"exponential-backoff": "^3.1.1",
|
||||
|
||||
@@ -170,6 +170,17 @@ export type BatchExport = {
|
||||
url: string | null;
|
||||
log: string | null;
|
||||
};
|
||||
export type BillingMeterBackup = {
|
||||
stripe_customer_id: string;
|
||||
meter_id: string;
|
||||
start_time: Timestamp;
|
||||
end_time: Timestamp;
|
||||
aggregated_value: number;
|
||||
event_name: string;
|
||||
org_id: string;
|
||||
created_at: Generated<Timestamp>;
|
||||
updated_at: Generated<Timestamp>;
|
||||
};
|
||||
export type Comment = {
|
||||
id: string;
|
||||
project_id: string;
|
||||
@@ -611,6 +622,7 @@ export type DB = {
|
||||
audit_logs: AuditLog;
|
||||
background_migrations: BackgroundMigration;
|
||||
batch_exports: BatchExport;
|
||||
billing_meter_backups: BillingMeterBackup;
|
||||
comments: Comment;
|
||||
cron_jobs: CronJobs;
|
||||
dataset_items: DatasetItem;
|
||||
|
||||
+21
@@ -0,0 +1,21 @@
|
||||
-- CreateTable
|
||||
CREATE TABLE "billing_meter_backups" (
|
||||
"stripe_customer_id" TEXT NOT NULL,
|
||||
"meter_id" TEXT NOT NULL,
|
||||
"start_time" TIMESTAMP(3) NOT NULL,
|
||||
"end_time" TIMESTAMP(3) NOT NULL,
|
||||
|
||||
"aggregated_value" INTEGER NOT NULL,
|
||||
|
||||
"event_name" TEXT NOT NULL,
|
||||
"org_id" TEXT NOT NULL,
|
||||
|
||||
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updated_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "billing_meter_backups_stripe_customer_id_meter_id_start_tim_idx" ON "billing_meter_backups"("stripe_customer_id", "meter_id", "start_time", "end_time");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "billing_meter_backups_stripe_customer_id_meter_id_start_tim_key" ON "billing_meter_backups"("stripe_customer_id", "meter_id", "start_time", "end_time");
|
||||
@@ -1026,3 +1026,26 @@ model QueueBackUp {
|
||||
|
||||
@@map("queue_backups")
|
||||
}
|
||||
|
||||
model BillingMeterBackup {
|
||||
// unique
|
||||
stripeCustomerId String @map("stripe_customer_id")
|
||||
meterId String @map("meter_id")
|
||||
startTime DateTime @map("start_time")
|
||||
endTime DateTime @map("end_time")
|
||||
|
||||
// value
|
||||
aggregatedValue Int @map("aggregated_value")
|
||||
|
||||
// labels
|
||||
eventName String @map("event_name")
|
||||
orgId String @map("org_id")
|
||||
|
||||
// ts
|
||||
createdAt DateTime @default(now()) @map("created_at")
|
||||
updatedAt DateTime @default(now()) @updatedAt @map("updated_at")
|
||||
|
||||
@@unique([stripeCustomerId, meterId, startTime, endTime])
|
||||
@@index([stripeCustomerId, meterId, startTime, endTime])
|
||||
@@map("billing_meter_backups")
|
||||
}
|
||||
|
||||
@@ -33,15 +33,6 @@ const EnvSchema = z.object({
|
||||
CLICKHOUSE_DB: z.string().default("default"),
|
||||
CLICKHOUSE_USER: z.string(),
|
||||
CLICKHOUSE_PASSWORD: z.string(),
|
||||
LANGFUSE_SDK_CI_SYNC_PROCESSING_ENABLED: z
|
||||
.enum(["true", "false"])
|
||||
.default("false"),
|
||||
LANGFUSE_CLICKHOUSE_INGESTION_ENABLED: z
|
||||
.enum(["true", "false"])
|
||||
.default("true"),
|
||||
LANGFUSE_POSTGRES_INGESTION_ENABLED: z
|
||||
.enum(["true", "false"])
|
||||
.default("false"),
|
||||
|
||||
LANGFUSE_INGESTION_QUEUE_DELAY_MS: z.coerce
|
||||
.number()
|
||||
@@ -72,6 +63,9 @@ const EnvSchema = z.object({
|
||||
LANGFUSE_S3_CORE_DATA_EXPORT_IS_ENABLED: z
|
||||
.enum(["true", "false"])
|
||||
.default("false"),
|
||||
LANGFUSE_POSTGRES_METERING_DATA_EXPORT_IS_ENABLED: z
|
||||
.enum(["true", "false"])
|
||||
.default("false"),
|
||||
});
|
||||
|
||||
export const env: z.infer<typeof EnvSchema> =
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { BaseError } from "./BaseError";
|
||||
|
||||
export class ApiError extends BaseError {
|
||||
constructor(description = "Api call failed") {
|
||||
super("ApiError", 500, description, true);
|
||||
constructor(description = "Api call failed", status = 500) {
|
||||
super("ApiError", status, description, true);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -33,6 +33,7 @@ export * from "./redis/ingestionQueue";
|
||||
export * from "./redis/postHogIntegrationQueue";
|
||||
export * from "./redis/postHogIntegrationProcessingQueue";
|
||||
export * from "./redis/coreDataS3ExportQueue";
|
||||
export * from "./redis/meteringDataPostgresExportQueue";
|
||||
export * from "./redis/experimentCreateQueue";
|
||||
export * from "./auth/types";
|
||||
export * from "./ingestion/legacy/index";
|
||||
|
||||
@@ -18,11 +18,9 @@ import {
|
||||
traceException,
|
||||
} from "../instrumentation";
|
||||
import { logger } from "../logger";
|
||||
import { LegacyIngestionEventType, QueueJobs } from "../queues";
|
||||
import { QueueJobs } from "../queues";
|
||||
import { IngestionQueue } from "../redis/ingestionQueue";
|
||||
import { LegacyIngestionQueue } from "../redis/legacyIngestion";
|
||||
import { redis } from "../redis/redis";
|
||||
import { handleBatch } from "./legacy";
|
||||
import {
|
||||
StorageService,
|
||||
StorageServiceFactory,
|
||||
@@ -185,140 +183,47 @@ export const processEventBatch = async (
|
||||
});
|
||||
});
|
||||
|
||||
// This is a workaround to allow us to disable async ingestion processing for SDK CI testing
|
||||
// TODO: remove this block after SDKs are ready for V3 async ingestion processing
|
||||
if (env.LANGFUSE_SDK_CI_SYNC_PROCESSING_ENABLED === "true") {
|
||||
const result = await handleBatch(
|
||||
sortedBatch,
|
||||
authCheck,
|
||||
tokenCountDelegate,
|
||||
);
|
||||
|
||||
// in case we did not return early, we return the result here
|
||||
return aggregateBatchResult(
|
||||
[...validationErrors, ...authenticationErrors, ...result.errors],
|
||||
result.results,
|
||||
authCheck.scope.projectId,
|
||||
);
|
||||
}
|
||||
|
||||
// Send each event individually to IngestionQueue for ClickHouse processing
|
||||
if (env.LANGFUSE_CLICKHOUSE_INGESTION_ENABLED === "true") {
|
||||
if (s3UploadErrored) {
|
||||
throw new Error(
|
||||
"Failed to upload events to blob storage, aborting event processing",
|
||||
);
|
||||
}
|
||||
|
||||
if (redis) {
|
||||
const queue = IngestionQueue.getInstance();
|
||||
await Promise.all(
|
||||
Object.keys(sortedBatchByEventBodyId).map(async (id) =>
|
||||
queue
|
||||
? queue.add(
|
||||
QueueJobs.IngestionJob,
|
||||
{
|
||||
id: randomUUID(),
|
||||
timestamp: new Date(),
|
||||
name: QueueJobs.IngestionJob as const,
|
||||
payload: {
|
||||
data: {
|
||||
type: sortedBatchByEventBodyId[id].type,
|
||||
eventBodyId: sortedBatchByEventBodyId[id].eventBodyId,
|
||||
},
|
||||
authCheck,
|
||||
},
|
||||
},
|
||||
{
|
||||
delay: env.LANGFUSE_INGESTION_QUEUE_DELAY_MS,
|
||||
},
|
||||
)
|
||||
: Promise.reject("Failed to instantiate queue"),
|
||||
),
|
||||
);
|
||||
if (env.LANGFUSE_POSTGRES_INGESTION_ENABLED !== "true") {
|
||||
// If postgres ingestion is disabled, we return early
|
||||
return aggregateBatchResult(
|
||||
[...validationErrors, ...authenticationErrors],
|
||||
sortedBatch.map((event) => ({ id: event.id, result: event })),
|
||||
authCheck.scope.projectId,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (env.LANGFUSE_POSTGRES_INGESTION_ENABLED === "true") {
|
||||
// As part of the legacy processing we sent the entire batch to the worker.
|
||||
if (redis) {
|
||||
const queue = LegacyIngestionQueue.getInstance();
|
||||
|
||||
if (queue) {
|
||||
let addToQueueFailed = false;
|
||||
|
||||
const queuePayload: LegacyIngestionEventType = !s3UploadErrored
|
||||
? {
|
||||
data: Object.keys(sortedBatchByEventBodyId).map((id) => {
|
||||
const { key, type, eventBodyId } = sortedBatchByEventBodyId[id];
|
||||
return {
|
||||
type,
|
||||
eventBodyId,
|
||||
eventId: key,
|
||||
};
|
||||
}),
|
||||
authCheck,
|
||||
useS3EventStore: true,
|
||||
}
|
||||
: { data: sortedBatch, authCheck, useS3EventStore: false };
|
||||
|
||||
try {
|
||||
await queue.add(QueueJobs.LegacyIngestionJob, {
|
||||
payload: queuePayload,
|
||||
id: randomUUID(),
|
||||
timestamp: new Date(),
|
||||
name: QueueJobs.LegacyIngestionJob as const,
|
||||
});
|
||||
} catch (e: unknown) {
|
||||
logger.warn(
|
||||
"Failed to add batch to queue, falling back to sync processing",
|
||||
e,
|
||||
);
|
||||
addToQueueFailed = true;
|
||||
}
|
||||
|
||||
if (!addToQueueFailed) {
|
||||
return aggregateBatchResult(
|
||||
// we are not sending additional server errors to the client in case of early return
|
||||
[...validationErrors, ...authenticationErrors],
|
||||
sortedBatch.map((event) => ({ id: event.id, result: event })),
|
||||
authCheck.scope.projectId,
|
||||
);
|
||||
}
|
||||
} else {
|
||||
logger.warn(
|
||||
"Ingestion queue not initialized, falling back to sync processing",
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/*******************
|
||||
* SYNC PROCESSING *
|
||||
*******************/
|
||||
const result = await handleBatch(
|
||||
sortedBatch,
|
||||
authCheck,
|
||||
tokenCountDelegate,
|
||||
);
|
||||
|
||||
// in case we did not return early, we return the result here
|
||||
return aggregateBatchResult(
|
||||
[...validationErrors, ...authenticationErrors, ...result.errors],
|
||||
result.results,
|
||||
authCheck.scope.projectId,
|
||||
if (s3UploadErrored) {
|
||||
throw new Error(
|
||||
"Failed to upload events to blob storage, aborting event processing",
|
||||
);
|
||||
}
|
||||
|
||||
throw new Error(
|
||||
"Either Clickhouse or Postgres ingestion (or both) must be enabled",
|
||||
if (!redis) {
|
||||
throw new Error("Redis not initialized, aborting event processing");
|
||||
}
|
||||
|
||||
const queue = IngestionQueue.getInstance();
|
||||
await Promise.all(
|
||||
Object.keys(sortedBatchByEventBodyId).map(async (id) =>
|
||||
queue
|
||||
? queue.add(
|
||||
QueueJobs.IngestionJob,
|
||||
{
|
||||
id: randomUUID(),
|
||||
timestamp: new Date(),
|
||||
name: QueueJobs.IngestionJob as const,
|
||||
payload: {
|
||||
data: {
|
||||
type: sortedBatchByEventBodyId[id].type,
|
||||
eventBodyId: sortedBatchByEventBodyId[id].eventBodyId,
|
||||
},
|
||||
authCheck,
|
||||
},
|
||||
},
|
||||
{
|
||||
delay: env.LANGFUSE_INGESTION_QUEUE_DELAY_MS,
|
||||
},
|
||||
)
|
||||
: Promise.reject("Failed to instantiate queue"),
|
||||
),
|
||||
);
|
||||
|
||||
return aggregateBatchResult(
|
||||
[...validationErrors, ...authenticationErrors],
|
||||
sortedBatch.map((event) => ({ id: event.id, result: event })),
|
||||
authCheck.scope.projectId,
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -112,13 +112,16 @@ export const UsageOrCostDetails = z
|
||||
.union([OpenAIUsageSchema, RawUsageOrCostDetails])
|
||||
.nullish();
|
||||
|
||||
// Using z.any instead of jsonSchema for input/output as we saw huge CPU overhead for large numeric arrays.
|
||||
// With this setup parsing should be more lightweight and doesn't block other requests.
|
||||
// As we allow plain values, arrays, and objects the JSON parse via bodyParser should suffice.
|
||||
export const TraceBody = z.object({
|
||||
id: z.string().nullish(),
|
||||
timestamp: stringDateTime,
|
||||
name: z.string().max(1000).nullish(),
|
||||
externalId: z.string().nullish(),
|
||||
input: jsonSchema.nullish(),
|
||||
output: jsonSchema.nullish(),
|
||||
input: z.any().nullish(),
|
||||
output: z.any().nullish(),
|
||||
sessionId: z.string().nullish(),
|
||||
userId: z.string().nullish(),
|
||||
metadata: jsonSchema.nullish(),
|
||||
@@ -133,8 +136,8 @@ export const OptionalObservationBody = z.object({
|
||||
name: z.string().nullish(),
|
||||
startTime: stringDateTime,
|
||||
metadata: jsonSchema.nullish(),
|
||||
input: jsonSchema.nullish(),
|
||||
output: jsonSchema.nullish(),
|
||||
input: z.any().nullish(),
|
||||
output: z.any().nullish(),
|
||||
level: z.nativeEnum(ObservationLevel).nullish(),
|
||||
statusMessage: z.string().nullish(),
|
||||
parentObservationId: z.string().nullish(),
|
||||
|
||||
@@ -119,6 +119,7 @@ export enum QueueName {
|
||||
PostHogIntegrationQueue = "posthog-integration-queue",
|
||||
PostHogIntegrationProcessingQueue = "posthog-integration-processing-queue",
|
||||
CoreDataS3ExportQueue = "core-data-s3-export-queue",
|
||||
MeteringDataPostgresExportQueue = "metering-data-postgres-export-queue",
|
||||
}
|
||||
|
||||
export enum QueueJobs {
|
||||
@@ -136,6 +137,7 @@ export enum QueueJobs {
|
||||
PostHogIntegrationJob = "posthog-integration-job",
|
||||
PostHogIntegrationProcessingJob = "posthog-integration-processing-job",
|
||||
CoreDataS3ExportJob = "core-data-s3-export-job",
|
||||
MeteringDataPostgresExportJob = "metering-data-postgres-export-job",
|
||||
}
|
||||
|
||||
export type TQueueJobTypes = {
|
||||
|
||||
@@ -45,6 +45,7 @@ export class CloudUsageMeteringQueue {
|
||||
QueueJobs.CloudUsageMeteringJob,
|
||||
{},
|
||||
{
|
||||
// Run at minute 5 of every hour (e.g. 1:05, 2:05, 3:05, etc)
|
||||
repeat: { pattern: "5 * * * *" },
|
||||
},
|
||||
);
|
||||
|
||||
@@ -13,6 +13,7 @@ import { ProjectDeleteQueue } from "./projectDelete";
|
||||
import { PostHogIntegrationQueue } from "./postHogIntegrationQueue";
|
||||
import { PostHogIntegrationProcessingQueue } from "./postHogIntegrationProcessingQueue";
|
||||
import { CoreDataS3ExportQueue } from "./coreDataS3ExportQueue";
|
||||
import { MeteringDataPostgresExportQueue } from "./meteringDataPostgresExportQueue";
|
||||
|
||||
export function getQueue(queueName: QueueName): Queue | null {
|
||||
switch (queueName) {
|
||||
@@ -44,6 +45,8 @@ export function getQueue(queueName: QueueName): Queue | null {
|
||||
return SecondaryIngestionQueue.getInstance();
|
||||
case QueueName.CoreDataS3ExportQueue:
|
||||
return CoreDataS3ExportQueue.getInstance();
|
||||
case QueueName.MeteringDataPostgresExportQueue:
|
||||
return MeteringDataPostgresExportQueue.getInstance();
|
||||
default:
|
||||
const exhaustiveCheckDefault: never = queueName;
|
||||
throw new Error(`Queue ${queueName} not found`);
|
||||
|
||||
@@ -0,0 +1,64 @@
|
||||
import { Queue } from "bullmq";
|
||||
import { QueueName, QueueJobs } from "../queues";
|
||||
import { createNewRedisInstance, redisQueueRetryOptions } from "./redis";
|
||||
import { logger } from "../logger";
|
||||
import { env } from "../../env";
|
||||
|
||||
export class MeteringDataPostgresExportQueue {
|
||||
private static instance: Queue | null = null;
|
||||
|
||||
public static getInstance(): Queue | null {
|
||||
if (env.LANGFUSE_POSTGRES_METERING_DATA_EXPORT_IS_ENABLED !== "true") {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (MeteringDataPostgresExportQueue.instance) {
|
||||
return MeteringDataPostgresExportQueue.instance;
|
||||
}
|
||||
|
||||
const newRedis = createNewRedisInstance({
|
||||
enableOfflineQueue: false,
|
||||
...redisQueueRetryOptions,
|
||||
});
|
||||
|
||||
MeteringDataPostgresExportQueue.instance = newRedis
|
||||
? new Queue(QueueName.MeteringDataPostgresExportQueue, {
|
||||
connection: newRedis,
|
||||
defaultJobOptions: {
|
||||
removeOnComplete: true,
|
||||
removeOnFail: 100,
|
||||
attempts: 5,
|
||||
backoff: {
|
||||
type: "exponential",
|
||||
delay: 5000,
|
||||
},
|
||||
},
|
||||
})
|
||||
: null;
|
||||
|
||||
MeteringDataPostgresExportQueue.instance?.on("error", (err) => {
|
||||
logger.error("MeteringDataPostgresExportQueue error", err);
|
||||
});
|
||||
|
||||
if (MeteringDataPostgresExportQueue.instance) {
|
||||
logger.debug("Scheduling jobs for MeteringDataPostgresExportQueue");
|
||||
MeteringDataPostgresExportQueue.instance
|
||||
.add(
|
||||
QueueJobs.MeteringDataPostgresExportJob,
|
||||
{},
|
||||
{
|
||||
// repeat: { pattern: "30 3 * * *" }, // every day at 3:30am UTC
|
||||
repeat: { pattern: "0 * * * *" }, // initially, run every hour
|
||||
},
|
||||
)
|
||||
.catch((err) => {
|
||||
logger.error(
|
||||
"Error adding MeteringDataPostgresExportJob schedule",
|
||||
err,
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
return MeteringDataPostgresExportQueue.instance;
|
||||
}
|
||||
}
|
||||
@@ -4,7 +4,7 @@ import {
|
||||
convertDateToClickhouseDateTime,
|
||||
} from "../clickhouse/client";
|
||||
import { logger } from "../logger";
|
||||
import { instrumentAsync } from "../instrumentation";
|
||||
import { getTracer, instrumentAsync } from "../instrumentation";
|
||||
import {
|
||||
StorageService,
|
||||
StorageServiceFactory,
|
||||
@@ -12,6 +12,7 @@ import {
|
||||
import { randomUUID } from "crypto";
|
||||
import { getClickhouseEntityType } from "../clickhouse/schemaUtils";
|
||||
import { NodeClickHouseClientConfigOptions } from "@clickhouse/client/dist/config";
|
||||
import { context, trace } from "@opentelemetry/api";
|
||||
|
||||
let s3StorageServiceClient: StorageService;
|
||||
|
||||
@@ -101,6 +102,64 @@ export async function upsertClickhouse<
|
||||
});
|
||||
}
|
||||
|
||||
export async function* queryClickhouseStream<T>(opts: {
|
||||
query: string;
|
||||
params?: Record<string, unknown> | undefined;
|
||||
clickhouseConfigs?: NodeClickHouseClientConfigOptions;
|
||||
}): AsyncGenerator<T> {
|
||||
const tracer = getTracer("clickhouse-query-stream");
|
||||
const span = tracer.startSpan("clickhouse-query-stream");
|
||||
|
||||
try {
|
||||
const res = await context.with(
|
||||
trace.setSpan(context.active(), span),
|
||||
async () => {
|
||||
// https://opentelemetry.io/docs/specs/semconv/database/database-spans/
|
||||
span.setAttribute("ch.query.text", opts.query);
|
||||
|
||||
const res = await clickhouseClient(opts.clickhouseConfigs).query({
|
||||
query: opts.query,
|
||||
format: "JSONEachRow",
|
||||
query_params: opts.params,
|
||||
});
|
||||
// same logic as for prisma. we want to see queries in development
|
||||
if (env.NODE_ENV === "development") {
|
||||
logger.info(`clickhouse:query ${res.query_id} ${opts.query}`);
|
||||
}
|
||||
|
||||
span.setAttribute("ch.queryId", res.query_id);
|
||||
|
||||
// add summary headers to the span. Helps to tune performance
|
||||
const summaryHeader = res.response_headers["x-clickhouse-summary"];
|
||||
if (summaryHeader) {
|
||||
try {
|
||||
const summary = Array.isArray(summaryHeader)
|
||||
? JSON.parse(summaryHeader[0])
|
||||
: JSON.parse(summaryHeader);
|
||||
for (const key in summary) {
|
||||
span.setAttribute(`ch.${key}`, summary[key]);
|
||||
}
|
||||
} catch (error) {
|
||||
logger.debug(
|
||||
`Failed to parse clickhouse summary header ${summaryHeader}`,
|
||||
error,
|
||||
);
|
||||
}
|
||||
}
|
||||
return res;
|
||||
},
|
||||
);
|
||||
|
||||
for await (const rows of res.stream<T>()) {
|
||||
for (const row of rows) {
|
||||
yield row.json();
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
span.end();
|
||||
}
|
||||
}
|
||||
|
||||
export async function queryClickhouse<T>(opts: {
|
||||
query: string;
|
||||
params?: Record<string, unknown> | undefined;
|
||||
|
||||
@@ -2,6 +2,7 @@ import {
|
||||
commandClickhouse,
|
||||
parseClickhouseUTCDateTimeFormat,
|
||||
queryClickhouse,
|
||||
queryClickhouseStream,
|
||||
upsertClickhouse,
|
||||
} from "./clickhouse";
|
||||
import { ObservationLevel } from "@prisma/client";
|
||||
@@ -1214,11 +1215,11 @@ export const getTraceIdsForObservations = async (
|
||||
}));
|
||||
};
|
||||
|
||||
export const getGenerationsForPostHog = async (
|
||||
export const getGenerationsForPostHog = async function* (
|
||||
projectId: string,
|
||||
minTimestamp: Date,
|
||||
maxTimestamp: Date,
|
||||
) => {
|
||||
) {
|
||||
const query = `
|
||||
SELECT
|
||||
o.name as name,
|
||||
@@ -1252,7 +1253,7 @@ export const getGenerationsForPostHog = async (
|
||||
AND o.type = 'GENERATION'
|
||||
`;
|
||||
|
||||
const records = await queryClickhouse<Record<string, unknown>>({
|
||||
const records = queryClickhouseStream<Record<string, unknown>>({
|
||||
query,
|
||||
params: {
|
||||
projectId,
|
||||
@@ -1262,32 +1263,34 @@ export const getGenerationsForPostHog = async (
|
||||
});
|
||||
|
||||
const baseUrl = env.NEXTAUTH_URL?.replace("/api/auth", "");
|
||||
return records.map((record) => ({
|
||||
timestamp: record.start_time,
|
||||
langfuse_generation_name: record.name,
|
||||
langfuse_trace_name: record.trace_name,
|
||||
langfuse_url: `${baseUrl}/project/${projectId}/traces/${encodeURIComponent(record.trace_id as string)}?observation=${encodeURIComponent(record.id as string)}`,
|
||||
langfuse_id: record.id,
|
||||
langfuse_cost_usd: record.total_cost,
|
||||
langfuse_input_units: record.input_tokens,
|
||||
langfuse_output_units: record.output_tokens,
|
||||
langfuse_total_units: record.total_tokens,
|
||||
langfuse_session_id: record.trace_session_id,
|
||||
langfuse_project_id: projectId,
|
||||
langfuse_user_id: record.trace_user_id || "langfuse_unknown_user",
|
||||
langfuse_latency: record.latency,
|
||||
langfuse_time_to_first_token: record.time_to_first_token,
|
||||
langfuse_release: record.trace_release,
|
||||
langfuse_version: record.version,
|
||||
langfuse_model: record.model,
|
||||
langfuse_level: record.level,
|
||||
langfuse_tags: record.trace_tags,
|
||||
langfuse_event_version: "1.0.0",
|
||||
$session_id: record.posthog_session_id ?? null,
|
||||
$set: {
|
||||
langfuse_user_url: record.user_id
|
||||
? `${baseUrl}/project/${projectId}/users/${encodeURIComponent(record.user_id as string)}`
|
||||
: null,
|
||||
},
|
||||
}));
|
||||
for await (const record of records) {
|
||||
yield {
|
||||
timestamp: record.start_time,
|
||||
langfuse_generation_name: record.name,
|
||||
langfuse_trace_name: record.trace_name,
|
||||
langfuse_url: `${baseUrl}/project/${projectId}/traces/${encodeURIComponent(record.trace_id as string)}?observation=${encodeURIComponent(record.id as string)}`,
|
||||
langfuse_id: record.id,
|
||||
langfuse_cost_usd: record.total_cost,
|
||||
langfuse_input_units: record.input_tokens,
|
||||
langfuse_output_units: record.output_tokens,
|
||||
langfuse_total_units: record.total_tokens,
|
||||
langfuse_session_id: record.trace_session_id,
|
||||
langfuse_project_id: projectId,
|
||||
langfuse_user_id: record.trace_user_id || "langfuse_unknown_user",
|
||||
langfuse_latency: record.latency,
|
||||
langfuse_time_to_first_token: record.time_to_first_token,
|
||||
langfuse_release: record.trace_release,
|
||||
langfuse_version: record.version,
|
||||
langfuse_model: record.model,
|
||||
langfuse_level: record.level,
|
||||
langfuse_tags: record.trace_tags,
|
||||
langfuse_event_version: "1.0.0",
|
||||
$session_id: record.posthog_session_id ?? null,
|
||||
$set: {
|
||||
langfuse_user_url: record.user_id
|
||||
? `${baseUrl}/project/${projectId}/users/${encodeURIComponent(record.user_id as string)}`
|
||||
: null,
|
||||
},
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
@@ -3,6 +3,7 @@ import {
|
||||
commandClickhouse,
|
||||
parseClickhouseUTCDateTimeFormat,
|
||||
queryClickhouse,
|
||||
queryClickhouseStream,
|
||||
upsertClickhouse,
|
||||
} from "./clickhouse";
|
||||
import { FilterList } from "../queries/clickhouse-sql/clickhouse-filter";
|
||||
@@ -203,20 +204,24 @@ export const getScoresForObservations = async (
|
||||
return rows.map(convertToScore);
|
||||
};
|
||||
|
||||
export const getScoresGroupedByNameSourceType = async (projectId: string) => {
|
||||
export const getScoresGroupedByNameSourceType = async (
|
||||
projectId: string,
|
||||
timestamp: Date | undefined,
|
||||
) => {
|
||||
// We mainly use queries like this to retrieve filter options.
|
||||
// Therefore, we can skip final as some inaccuracy in count is acceptable.
|
||||
const query = `
|
||||
select
|
||||
name,
|
||||
source,
|
||||
data_type
|
||||
from scores s
|
||||
WHERE s.project_id = {projectId: String}
|
||||
GROUP BY name, source, data_type
|
||||
ORDER BY count() desc
|
||||
LIMIT 1000;
|
||||
`;
|
||||
select
|
||||
name,
|
||||
source,
|
||||
data_type
|
||||
from scores s
|
||||
WHERE s.project_id = {projectId: String}
|
||||
${timestamp ? `AND s.timestamp >= {timestamp: DateTime64(3)}` : ""}
|
||||
GROUP BY name, source, data_type
|
||||
ORDER BY count() desc
|
||||
LIMIT 1000;
|
||||
`;
|
||||
|
||||
const rows = await queryClickhouse<{
|
||||
name: string;
|
||||
@@ -226,6 +231,9 @@ export const getScoresGroupedByNameSourceType = async (projectId: string) => {
|
||||
query: query,
|
||||
params: {
|
||||
projectId: projectId,
|
||||
...(timestamp
|
||||
? { timestamp: convertDateToClickhouseDateTime(timestamp) }
|
||||
: {}),
|
||||
},
|
||||
});
|
||||
|
||||
@@ -669,11 +677,11 @@ export const getDistinctScoreNames = async (
|
||||
return rows.map((row) => row.name);
|
||||
};
|
||||
|
||||
export const getScoresForPostHog = async (
|
||||
export const getScoresForPostHog = async function* (
|
||||
projectId: string,
|
||||
minTimestamp: Date,
|
||||
maxTimestamp: Date,
|
||||
) => {
|
||||
) {
|
||||
const query = `
|
||||
SELECT
|
||||
s.id as id,
|
||||
@@ -697,7 +705,7 @@ export const getScoresForPostHog = async (
|
||||
AND t.timestamp <= {maxTimestamp: DateTime64(3)}
|
||||
`;
|
||||
|
||||
const records = await queryClickhouse<Record<string, unknown>>({
|
||||
const records = queryClickhouseStream<Record<string, unknown>>({
|
||||
query,
|
||||
params: {
|
||||
projectId,
|
||||
@@ -707,24 +715,26 @@ export const getScoresForPostHog = async (
|
||||
});
|
||||
|
||||
const baseUrl = env.NEXTAUTH_URL?.replace("/api/auth", "");
|
||||
return records.map((record) => ({
|
||||
timestamp: record.timestamp,
|
||||
langfuse_score_name: record.name,
|
||||
langfuse_score_value: record.value,
|
||||
langfuse_score_comment: record.comment,
|
||||
langfuse_trace_name: record.trace_name,
|
||||
langfuse_id: record.id,
|
||||
langfuse_session_id: record.trace_session_id,
|
||||
langfuse_project_id: projectId,
|
||||
langfuse_user_id: record.trace_user_id || "langfuse_unknown_user",
|
||||
langfuse_release: record.trace_release,
|
||||
langfuse_tags: record.trace_tags,
|
||||
langfuse_event_version: "1.0.0",
|
||||
$session_id: record.posthog_session_id ?? null,
|
||||
$set: {
|
||||
langfuse_user_url: record.user_id
|
||||
? `${baseUrl}/project/${projectId}/users/${encodeURIComponent(record.user_id as string)}`
|
||||
: null,
|
||||
},
|
||||
}));
|
||||
for await (const record of records) {
|
||||
yield {
|
||||
timestamp: record.timestamp,
|
||||
langfuse_score_name: record.name,
|
||||
langfuse_score_value: record.value,
|
||||
langfuse_score_comment: record.comment,
|
||||
langfuse_trace_name: record.trace_name,
|
||||
langfuse_id: record.id,
|
||||
langfuse_session_id: record.trace_session_id,
|
||||
langfuse_project_id: projectId,
|
||||
langfuse_user_id: record.trace_user_id || "langfuse_unknown_user",
|
||||
langfuse_release: record.trace_release,
|
||||
langfuse_tags: record.trace_tags,
|
||||
langfuse_event_version: "1.0.0",
|
||||
$session_id: record.posthog_session_id ?? null,
|
||||
$set: {
|
||||
langfuse_user_url: record.user_id
|
||||
? `${baseUrl}/project/${projectId}/users/${encodeURIComponent(record.user_id as string)}`
|
||||
: null,
|
||||
},
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
@@ -2,6 +2,7 @@ import {
|
||||
commandClickhouse,
|
||||
parseClickhouseUTCDateTimeFormat,
|
||||
queryClickhouse,
|
||||
queryClickhouseStream,
|
||||
upsertClickhouse,
|
||||
} from "./clickhouse";
|
||||
import {
|
||||
@@ -627,11 +628,11 @@ export const getUserMetrics = async (
|
||||
}));
|
||||
};
|
||||
|
||||
export const getTracesForPostHog = async (
|
||||
export const getTracesForPostHog = async function* (
|
||||
projectId: string,
|
||||
minTimestamp: Date,
|
||||
maxTimestamp: Date,
|
||||
) => {
|
||||
) {
|
||||
const query = `
|
||||
WITH observations_agg AS (
|
||||
SELECT o.project_id,
|
||||
@@ -665,7 +666,7 @@ export const getTracesForPostHog = async (
|
||||
AND t.timestamp <= {maxTimestamp: DateTime64(3)}
|
||||
`;
|
||||
|
||||
const records = await queryClickhouse<Record<string, unknown>>({
|
||||
const records = queryClickhouseStream<Record<string, unknown>>({
|
||||
query,
|
||||
params: {
|
||||
projectId,
|
||||
@@ -675,28 +676,30 @@ export const getTracesForPostHog = async (
|
||||
});
|
||||
|
||||
const baseUrl = env.NEXTAUTH_URL?.replace("/api/auth", "");
|
||||
return records.map((record) => ({
|
||||
timestamp: record.timestamp,
|
||||
langfuse_id: record.id,
|
||||
langfuse_trace_name: record.name,
|
||||
langfuse_url: `${baseUrl}/project/${projectId}/traces/${encodeURIComponent(record.id as string)}`,
|
||||
langfuse_cost_usd: record.total_cost,
|
||||
langfuse_count_observations: record.observation_count,
|
||||
langfuse_session_id: record.session_id,
|
||||
langfuse_project_id: projectId,
|
||||
langfuse_user_id: record.user_id || "langfuse_unknown_user",
|
||||
langfuse_latency: record.latency,
|
||||
langfuse_release: record.release,
|
||||
langfuse_version: record.version,
|
||||
langfuse_tags: record.tags,
|
||||
langfuse_event_version: "1.0.0",
|
||||
$session_id: record.posthog_session_id ?? null,
|
||||
$set: {
|
||||
langfuse_user_url: record.user_id
|
||||
? `${baseUrl}/project/${projectId}/users/${encodeURIComponent(record.user_id as string)}`
|
||||
: null,
|
||||
},
|
||||
}));
|
||||
for await (const record of records) {
|
||||
yield {
|
||||
timestamp: record.timestamp,
|
||||
langfuse_id: record.id,
|
||||
langfuse_trace_name: record.name,
|
||||
langfuse_url: `${baseUrl}/project/${projectId}/traces/${encodeURIComponent(record.id as string)}`,
|
||||
langfuse_cost_usd: record.total_cost,
|
||||
langfuse_count_observations: record.observation_count,
|
||||
langfuse_session_id: record.session_id,
|
||||
langfuse_project_id: projectId,
|
||||
langfuse_user_id: record.user_id || "langfuse_unknown_user",
|
||||
langfuse_latency: record.latency,
|
||||
langfuse_release: record.release,
|
||||
langfuse_version: record.version,
|
||||
langfuse_tags: record.tags,
|
||||
langfuse_event_version: "1.0.0",
|
||||
$session_id: record.posthog_session_id ?? null,
|
||||
$set: {
|
||||
langfuse_user_url: record.user_id
|
||||
? `${baseUrl}/project/${projectId}/users/${encodeURIComponent(record.user_id as string)}`
|
||||
: null,
|
||||
},
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
export const getTracesByIdsForAnyProject = async (traceIds: string[]) => {
|
||||
|
||||
@@ -189,17 +189,28 @@ const getSessionsTableGeneric = async <T>(props: FetchSessionsTableProps) => {
|
||||
const singleTraceFilter =
|
||||
filters.length > 0 ? new FilterList(filters).apply() : undefined;
|
||||
|
||||
const hasMetricsFilter = tracesFilter.find((f) =>
|
||||
[
|
||||
"session_total_cost",
|
||||
"session_input_cost",
|
||||
"session_output_cost",
|
||||
"duration",
|
||||
"session_total_usage",
|
||||
"session_output_usage",
|
||||
"session_input_usage",
|
||||
].includes(f.field),
|
||||
);
|
||||
const hasMetricsFilter =
|
||||
tracesFilter.find((f) =>
|
||||
[
|
||||
"session_total_cost",
|
||||
"session_input_cost",
|
||||
"session_output_cost",
|
||||
"duration",
|
||||
"session_total_usage",
|
||||
"session_output_usage",
|
||||
"session_input_usage",
|
||||
].includes(f.field),
|
||||
) ||
|
||||
(orderBy &&
|
||||
[
|
||||
"totalCost",
|
||||
"inputCost",
|
||||
"outputCost",
|
||||
"sessionDuration",
|
||||
"totalUsage",
|
||||
"outputUsage",
|
||||
"inputUsage",
|
||||
].includes(orderBy?.column));
|
||||
|
||||
const selectMetrics = select === "metrics" || hasMetricsFilter;
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { z } from "zod";
|
||||
import { merge } from "lodash";
|
||||
import { JsonNested, jsonSchema, jsonSchemaNullable } from "./zod";
|
||||
import { JsonNested, jsonSchema } from "./zod";
|
||||
|
||||
/**
|
||||
* Deeply parses a JSON string or object for nested stringified JSON
|
||||
@@ -52,13 +52,11 @@ export const mergeJson = (
|
||||
|
||||
export const parseJsonPrioritised = (
|
||||
json: string,
|
||||
): z.infer<typeof jsonSchema> | string | undefined => {
|
||||
): JsonNested | string | undefined => {
|
||||
try {
|
||||
const parsedJson = JSON.parse(json);
|
||||
return jsonSchema.parse(parsedJson);
|
||||
return JSON.parse(json);
|
||||
} catch (error) {
|
||||
const parsed = jsonSchema.safeParse(json);
|
||||
return parsed.success ? parsed.data : json;
|
||||
return json;
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
Generated
+337
-221
@@ -102,7 +102,7 @@ importers:
|
||||
version: 7.12.0(eslint@8.57.0)(typescript@5.4.5)
|
||||
'@vercel/style-guide':
|
||||
specifier: ^6.0.0
|
||||
version: 6.0.0(@next/eslint-plugin-next@14.2.15)(eslint@8.57.0)(jest@29.7.0)(prettier@3.3.3)(typescript@5.4.5)(vitest@2.1.2)
|
||||
version: 6.0.0(@next/eslint-plugin-next@14.2.15)(eslint@8.57.0)(jest@29.7.0(@types/node@20.14.8)(babel-plugin-macros@3.1.0))(prettier@3.3.3)(typescript@5.4.5)(vitest@2.1.2(@types/node@20.14.8)(jsdom@20.0.3)(msw@2.6.5(@types/node@20.14.8)(typescript@5.4.5))(terser@5.36.0))
|
||||
eslint-config-next:
|
||||
specifier: ^14.2.15
|
||||
version: 14.2.15(eslint@8.57.0)(typescript@5.4.5)
|
||||
@@ -184,8 +184,8 @@ importers:
|
||||
specifier: ^2.4.3
|
||||
version: 2.4.3
|
||||
bullmq:
|
||||
specifier: ^5.12.10
|
||||
version: 5.12.10
|
||||
specifier: ^5.34.10
|
||||
version: 5.34.10
|
||||
dd-trace:
|
||||
specifier: ^5.23.1
|
||||
version: 5.23.1
|
||||
@@ -312,7 +312,7 @@ importers:
|
||||
version: 0.0.4
|
||||
'@appsignal/opentelemetry-instrumentation-bullmq':
|
||||
specifier: ^0.7.3
|
||||
version: 0.7.3(bullmq@5.12.10)
|
||||
version: 0.7.3(bullmq@5.34.10)
|
||||
'@baselime/trpc-opentelemetry-middleware':
|
||||
specifier: ^0.1.2
|
||||
version: 0.1.2(@opentelemetry/api@1.9.0)(@trpc/server@10.45.2)
|
||||
@@ -536,8 +536,8 @@ importers:
|
||||
specifier: ^2.4.3
|
||||
version: 2.4.3
|
||||
bullmq:
|
||||
specifier: ^5.12.10
|
||||
version: 5.12.10
|
||||
specifier: ^5.34.10
|
||||
version: 5.34.10
|
||||
class-variance-authority:
|
||||
specifier: ^0.7.0
|
||||
version: 0.7.0
|
||||
@@ -582,7 +582,7 @@ importers:
|
||||
version: 0.27.4
|
||||
langchain:
|
||||
specifier: ^0.3.6
|
||||
version: 0.3.6(@langchain/anthropic@0.3.8(@langchain/core@0.3.18(openai@4.72.0(zod@3.23.8))))(@langchain/aws@0.1.2(@langchain/core@0.3.18(openai@4.72.0(zod@3.23.8))))(@langchain/core@0.3.18(openai@4.72.0(zod@3.23.8)))(@langchain/google-vertexai@0.1.3(@langchain/core@0.3.18(openai@4.72.0(zod@3.23.8)))(zod@3.23.8))(axios@1.7.7)(handlebars@4.7.8)(openai@4.72.0(zod@3.23.8))
|
||||
version: 0.3.6(@langchain/anthropic@0.3.8(@langchain/core@0.3.18(openai@4.72.0(zod@3.23.8))))(@langchain/aws@0.1.2(@aws-sdk/client-sts@3.675.0)(@langchain/core@0.3.18(openai@4.72.0(zod@3.23.8))))(@langchain/core@0.3.18(openai@4.72.0(zod@3.23.8)))(@langchain/google-vertexai@0.1.3(@langchain/core@0.3.18(openai@4.72.0(zod@3.23.8)))(zod@3.23.8))(axios@1.7.7)(handlebars@4.7.8)(openai@4.72.0(zod@3.23.8))
|
||||
lodash:
|
||||
specifier: ^4.17.21
|
||||
version: 4.17.21
|
||||
@@ -698,7 +698,7 @@ importers:
|
||||
version: 0.5.7(tailwindcss@3.4.17(ts-node@10.9.2(@types/node@20.10.5)(typescript@5.4.5)))
|
||||
'@testing-library/jest-dom':
|
||||
specifier: ^6.4.6
|
||||
version: 6.4.6(@jest/globals@29.7.0)(@types/jest@29.5.12)(jest@29.7.0(@types/node@20.10.5)(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@types/node@20.10.5)(typescript@5.4.5)))(vitest@2.1.2(@types/node@20.10.5))
|
||||
version: 6.4.6(@jest/globals@29.7.0)(@types/jest@29.5.12)(jest@29.7.0(@types/node@20.10.5)(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@types/node@20.10.5)(typescript@5.4.5)))(vitest@2.1.2(@types/node@20.10.5)(jsdom@20.0.3)(msw@2.6.5(@types/node@20.10.5)(typescript@5.4.5))(terser@5.36.0))
|
||||
'@testing-library/react':
|
||||
specifier: ^15.0.7
|
||||
version: 15.0.7(@types/react@18.2.79)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
|
||||
@@ -794,7 +794,7 @@ importers:
|
||||
version: 0.0.4
|
||||
'@appsignal/opentelemetry-instrumentation-bullmq':
|
||||
specifier: ^0.7.3
|
||||
version: 0.7.3(bullmq@5.12.10)
|
||||
version: 0.7.3(bullmq@5.34.10)
|
||||
'@clickhouse/client':
|
||||
specifier: ^1.4.0
|
||||
version: 1.4.0
|
||||
@@ -844,11 +844,14 @@ importers:
|
||||
specifier: ^2.5.0
|
||||
version: 2.5.0
|
||||
bullmq:
|
||||
specifier: ^5.12.10
|
||||
version: 5.12.10
|
||||
specifier: ^5.34.10
|
||||
version: 5.34.10
|
||||
cors:
|
||||
specifier: ^2.8.5
|
||||
version: 2.8.5
|
||||
csv-parse:
|
||||
specifier: ^5.6.0
|
||||
version: 5.6.0
|
||||
dd-trace:
|
||||
specifier: ^5.23.1
|
||||
version: 5.23.1
|
||||
@@ -5719,8 +5722,8 @@ packages:
|
||||
builtins@5.1.0:
|
||||
resolution: {integrity: sha512-SW9lzGTLvWTP1AY8xeAMZimqDrIaSdLQUcVr9DMef51niJ022Ri87SwRRKYm4A6iHfkPaiVUu/Duw2Wc4J7kKg==}
|
||||
|
||||
bullmq@5.12.10:
|
||||
resolution: {integrity: sha512-lXH8Caj+FvYHiBS0QBEpQOq57RcVuEPziBC5cBlWguCVNfn1UMSri22bRrynDKuof8o9XB43ctmYASUpoa0DeQ==}
|
||||
bullmq@5.34.10:
|
||||
resolution: {integrity: sha512-ia6EzpQm1ZPq6GUBSLyfvzJrhdBTd1f3Gn2g9pFtLX4hBOob6QHmcmBzGgPlSCyr/i2Qfe4OdjS21bRd02srbw==}
|
||||
|
||||
bundle-name@4.1.0:
|
||||
resolution: {integrity: sha512-tjwM5exMg6BGRI+kNmTntNsvdZS1X8BFYS6tnJ2hdH0kVxM6/eVZ2xy+FqStSWvYmtfFMDLIxurorHwDKfDz5Q==}
|
||||
@@ -6173,6 +6176,9 @@ packages:
|
||||
csstype@3.1.3:
|
||||
resolution: {integrity: sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==}
|
||||
|
||||
csv-parse@5.6.0:
|
||||
resolution: {integrity: sha512-l3nz3euub2QMg5ouu5U09Ew9Wf6/wQ8I++ch1loQ0ljmzhmfZYrH9fflS22i/PQEvsPvxCwxgz5q7UB8K1JO4Q==}
|
||||
|
||||
cytoscape-cose-bilkent@4.1.0:
|
||||
resolution: {integrity: sha512-wgQlVIUJF13Quxiv5e1gstZ08rnZj2XaLHGoFMYXz7SkNfCDOOteKBE6SYRfA9WxxI/iBc3ajfDoc6hb/MRAHQ==}
|
||||
peerDependencies:
|
||||
@@ -8946,8 +8952,8 @@ packages:
|
||||
resolution: {integrity: sha512-P0efT1C9jIdVRefqjzOQ9Xml57zpOXnIuS+csaB4MdZbTdmGDLo8XhzBG1N7aO11gKDDkJvBLULeFTo46wwreA==}
|
||||
hasBin: true
|
||||
|
||||
msgpackr@1.10.1:
|
||||
resolution: {integrity: sha512-r5VRLv9qouXuLiIBrLpl2d5ZvPt8svdQTl5/vMvE4nzDMyEX4sgW5yWhuBBj5UmgwOTWj8CIdSXn5sAfsHAWIQ==}
|
||||
msgpackr@1.11.2:
|
||||
resolution: {integrity: sha512-F9UngXRlPyWCDEASDpTf6c9uNhGPTqnTeLVt7bN+bU1eajoR/8V9ys2BRaV5C/e5ihE6sJ9uPIKaYt6bFuO32g==}
|
||||
|
||||
msw@2.6.5:
|
||||
resolution: {integrity: sha512-PnlnTpUlOrj441kYQzzFhzMzMCGFT6a2jKUBG7zSpLkYS5oh8Arrbc0dL8/rNAtxaoBy0EVs2mFqj2qdmWK7lQ==}
|
||||
@@ -11651,14 +11657,14 @@ snapshots:
|
||||
'@types/node': 18.19.25
|
||||
tiktoken: 1.0.15
|
||||
|
||||
'@appsignal/opentelemetry-instrumentation-bullmq@0.7.3(bullmq@5.12.10)':
|
||||
'@appsignal/opentelemetry-instrumentation-bullmq@0.7.3(bullmq@5.34.10)':
|
||||
dependencies:
|
||||
'@opentelemetry/api': 1.9.0
|
||||
'@opentelemetry/instrumentation': 0.52.1(@opentelemetry/api@1.9.0)
|
||||
'@opentelemetry/semantic-conventions': 1.27.0
|
||||
flat: 5.0.2
|
||||
optionalDependencies:
|
||||
bullmq: 5.12.10
|
||||
bullmq: 5.34.10
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
@@ -12338,6 +12344,26 @@ snapshots:
|
||||
- '@aws-sdk/client-sso-oidc'
|
||||
- aws-crt
|
||||
|
||||
'@aws-sdk/credential-provider-ini@3.675.0(@aws-sdk/client-sts@3.675.0)':
|
||||
dependencies:
|
||||
'@aws-sdk/client-sts': 3.675.0
|
||||
'@aws-sdk/core': 3.667.0
|
||||
'@aws-sdk/credential-provider-env': 3.667.0
|
||||
'@aws-sdk/credential-provider-http': 3.667.0
|
||||
'@aws-sdk/credential-provider-process': 3.667.0
|
||||
'@aws-sdk/credential-provider-sso': 3.675.0(@aws-sdk/client-sso-oidc@3.668.0(@aws-sdk/client-sts@3.668.0))
|
||||
'@aws-sdk/credential-provider-web-identity': 3.667.0(@aws-sdk/client-sts@3.675.0)
|
||||
'@aws-sdk/types': 3.667.0
|
||||
'@smithy/credential-provider-imds': 3.2.4
|
||||
'@smithy/property-provider': 3.1.7
|
||||
'@smithy/shared-ini-file-loader': 3.1.8
|
||||
'@smithy/types': 3.5.0
|
||||
tslib: 2.6.3
|
||||
transitivePeerDependencies:
|
||||
- '@aws-sdk/client-sso-oidc'
|
||||
- aws-crt
|
||||
optional: true
|
||||
|
||||
'@aws-sdk/credential-provider-node@3.668.0(@aws-sdk/client-sso-oidc@3.668.0(@aws-sdk/client-sts@3.668.0))(@aws-sdk/client-sts@3.668.0)':
|
||||
dependencies:
|
||||
'@aws-sdk/credential-provider-env': 3.667.0
|
||||
@@ -12357,26 +12383,6 @@ snapshots:
|
||||
- '@aws-sdk/client-sts'
|
||||
- aws-crt
|
||||
|
||||
'@aws-sdk/credential-provider-node@3.675.0':
|
||||
dependencies:
|
||||
'@aws-sdk/credential-provider-env': 3.667.0
|
||||
'@aws-sdk/credential-provider-http': 3.667.0
|
||||
'@aws-sdk/credential-provider-ini': 3.675.0(@aws-sdk/client-sso-oidc@3.668.0(@aws-sdk/client-sts@3.668.0))(@aws-sdk/client-sts@3.668.0)
|
||||
'@aws-sdk/credential-provider-process': 3.667.0
|
||||
'@aws-sdk/credential-provider-sso': 3.675.0(@aws-sdk/client-sso-oidc@3.668.0(@aws-sdk/client-sts@3.668.0))
|
||||
'@aws-sdk/credential-provider-web-identity': 3.667.0(@aws-sdk/client-sts@3.668.0)
|
||||
'@aws-sdk/types': 3.667.0
|
||||
'@smithy/credential-provider-imds': 3.2.4
|
||||
'@smithy/property-provider': 3.1.7
|
||||
'@smithy/shared-ini-file-loader': 3.1.8
|
||||
'@smithy/types': 3.5.0
|
||||
tslib: 2.6.3
|
||||
transitivePeerDependencies:
|
||||
- '@aws-sdk/client-sso-oidc'
|
||||
- '@aws-sdk/client-sts'
|
||||
- aws-crt
|
||||
optional: true
|
||||
|
||||
'@aws-sdk/credential-provider-node@3.675.0(@aws-sdk/client-sso-oidc@3.668.0(@aws-sdk/client-sts@3.668.0))(@aws-sdk/client-sts@3.668.0)':
|
||||
dependencies:
|
||||
'@aws-sdk/credential-provider-env': 3.667.0
|
||||
@@ -12415,6 +12421,26 @@ snapshots:
|
||||
- '@aws-sdk/client-sts'
|
||||
- aws-crt
|
||||
|
||||
'@aws-sdk/credential-provider-node@3.675.0(@aws-sdk/client-sts@3.675.0)':
|
||||
dependencies:
|
||||
'@aws-sdk/credential-provider-env': 3.667.0
|
||||
'@aws-sdk/credential-provider-http': 3.667.0
|
||||
'@aws-sdk/credential-provider-ini': 3.675.0(@aws-sdk/client-sts@3.675.0)
|
||||
'@aws-sdk/credential-provider-process': 3.667.0
|
||||
'@aws-sdk/credential-provider-sso': 3.675.0(@aws-sdk/client-sso-oidc@3.668.0(@aws-sdk/client-sts@3.668.0))
|
||||
'@aws-sdk/credential-provider-web-identity': 3.667.0(@aws-sdk/client-sts@3.675.0)
|
||||
'@aws-sdk/types': 3.667.0
|
||||
'@smithy/credential-provider-imds': 3.2.4
|
||||
'@smithy/property-provider': 3.1.7
|
||||
'@smithy/shared-ini-file-loader': 3.1.8
|
||||
'@smithy/types': 3.5.0
|
||||
tslib: 2.6.3
|
||||
transitivePeerDependencies:
|
||||
- '@aws-sdk/client-sso-oidc'
|
||||
- '@aws-sdk/client-sts'
|
||||
- aws-crt
|
||||
optional: true
|
||||
|
||||
'@aws-sdk/credential-provider-process@3.667.0':
|
||||
dependencies:
|
||||
'@aws-sdk/core': 3.667.0
|
||||
@@ -13711,12 +13737,41 @@ snapshots:
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
'@inquirer/confirm@5.0.2(@types/node@20.10.5)':
|
||||
dependencies:
|
||||
'@inquirer/core': 10.1.0(@types/node@20.10.5)
|
||||
'@inquirer/type': 3.0.1(@types/node@20.10.5)
|
||||
'@types/node': 20.10.5
|
||||
optional: true
|
||||
|
||||
'@inquirer/confirm@5.0.2(@types/node@20.11.29)':
|
||||
dependencies:
|
||||
'@inquirer/core': 10.1.0(@types/node@20.11.29)
|
||||
'@inquirer/type': 3.0.1(@types/node@20.11.29)
|
||||
'@types/node': 20.11.29
|
||||
|
||||
'@inquirer/confirm@5.0.2(@types/node@20.14.8)':
|
||||
dependencies:
|
||||
'@inquirer/core': 10.1.0(@types/node@20.14.8)
|
||||
'@inquirer/type': 3.0.1(@types/node@20.14.8)
|
||||
'@types/node': 20.14.8
|
||||
optional: true
|
||||
|
||||
'@inquirer/core@10.1.0(@types/node@20.10.5)':
|
||||
dependencies:
|
||||
'@inquirer/figures': 1.0.8
|
||||
'@inquirer/type': 3.0.1(@types/node@20.10.5)
|
||||
ansi-escapes: 4.3.2
|
||||
cli-width: 4.1.0
|
||||
mute-stream: 2.0.0
|
||||
signal-exit: 4.1.0
|
||||
strip-ansi: 6.0.1
|
||||
wrap-ansi: 6.2.0
|
||||
yoctocolors-cjs: 2.1.2
|
||||
transitivePeerDependencies:
|
||||
- '@types/node'
|
||||
optional: true
|
||||
|
||||
'@inquirer/core@10.1.0(@types/node@20.11.29)':
|
||||
dependencies:
|
||||
'@inquirer/figures': 1.0.8
|
||||
@@ -13731,14 +13786,39 @@ snapshots:
|
||||
transitivePeerDependencies:
|
||||
- '@types/node'
|
||||
|
||||
'@inquirer/core@10.1.0(@types/node@20.14.8)':
|
||||
dependencies:
|
||||
'@inquirer/figures': 1.0.8
|
||||
'@inquirer/type': 3.0.1(@types/node@20.14.8)
|
||||
ansi-escapes: 4.3.2
|
||||
cli-width: 4.1.0
|
||||
mute-stream: 2.0.0
|
||||
signal-exit: 4.1.0
|
||||
strip-ansi: 6.0.1
|
||||
wrap-ansi: 6.2.0
|
||||
yoctocolors-cjs: 2.1.2
|
||||
transitivePeerDependencies:
|
||||
- '@types/node'
|
||||
optional: true
|
||||
|
||||
'@inquirer/figures@1.0.3': {}
|
||||
|
||||
'@inquirer/figures@1.0.8': {}
|
||||
|
||||
'@inquirer/type@3.0.1(@types/node@20.10.5)':
|
||||
dependencies:
|
||||
'@types/node': 20.10.5
|
||||
optional: true
|
||||
|
||||
'@inquirer/type@3.0.1(@types/node@20.11.29)':
|
||||
dependencies:
|
||||
'@types/node': 20.11.29
|
||||
|
||||
'@inquirer/type@3.0.1(@types/node@20.14.8)':
|
||||
dependencies:
|
||||
'@types/node': 20.14.8
|
||||
optional: true
|
||||
|
||||
'@ioredis/commands@1.2.0': {}
|
||||
|
||||
'@isaacs/cliui@8.0.2':
|
||||
@@ -13771,42 +13851,6 @@ snapshots:
|
||||
jest-util: 29.7.0
|
||||
slash: 3.0.0
|
||||
|
||||
'@jest/core@29.7.0':
|
||||
dependencies:
|
||||
'@jest/console': 29.7.0
|
||||
'@jest/reporters': 29.7.0
|
||||
'@jest/test-result': 29.7.0
|
||||
'@jest/transform': 29.7.0
|
||||
'@jest/types': 29.6.3
|
||||
'@types/node': 20.14.8
|
||||
ansi-escapes: 4.3.2
|
||||
chalk: 4.1.2
|
||||
ci-info: 3.9.0
|
||||
exit: 0.1.2
|
||||
graceful-fs: 4.2.11
|
||||
jest-changed-files: 29.7.0
|
||||
jest-config: 29.7.0(@types/node@20.14.8)
|
||||
jest-haste-map: 29.7.0
|
||||
jest-message-util: 29.7.0
|
||||
jest-regex-util: 29.6.3
|
||||
jest-resolve: 29.7.0
|
||||
jest-resolve-dependencies: 29.7.0
|
||||
jest-runner: 29.7.0
|
||||
jest-runtime: 29.7.0
|
||||
jest-snapshot: 29.7.0
|
||||
jest-util: 29.7.0
|
||||
jest-validate: 29.7.0
|
||||
jest-watcher: 29.7.0
|
||||
micromatch: 4.0.8
|
||||
pretty-format: 29.7.0
|
||||
slash: 3.0.0
|
||||
strip-ansi: 6.0.1
|
||||
transitivePeerDependencies:
|
||||
- babel-plugin-macros
|
||||
- supports-color
|
||||
- ts-node
|
||||
optional: true
|
||||
|
||||
'@jest/core@29.7.0(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@types/node@20.10.5)(typescript@5.4.5))':
|
||||
dependencies:
|
||||
'@jest/console': 29.7.0
|
||||
@@ -14021,12 +14065,12 @@ snapshots:
|
||||
- '@aws-sdk/client-sts'
|
||||
- aws-crt
|
||||
|
||||
'@langchain/aws@0.1.2(@langchain/core@0.3.18(openai@4.72.0(zod@3.23.8)))':
|
||||
'@langchain/aws@0.1.2(@aws-sdk/client-sts@3.675.0)(@langchain/core@0.3.18(openai@4.72.0(zod@3.23.8)))':
|
||||
dependencies:
|
||||
'@aws-sdk/client-bedrock-agent-runtime': 3.668.0
|
||||
'@aws-sdk/client-bedrock-runtime': 3.668.0
|
||||
'@aws-sdk/client-kendra': 3.668.0
|
||||
'@aws-sdk/credential-provider-node': 3.675.0
|
||||
'@aws-sdk/credential-provider-node': 3.675.0(@aws-sdk/client-sts@3.675.0)
|
||||
'@langchain/core': 0.3.18(openai@4.72.0(zod@3.23.8))
|
||||
zod: 3.23.8
|
||||
zod-to-json-schema: 3.23.5(zod@3.23.8)
|
||||
@@ -16751,7 +16795,7 @@ snapshots:
|
||||
lz-string: 1.5.0
|
||||
pretty-format: 27.5.1
|
||||
|
||||
'@testing-library/jest-dom@6.4.6(@jest/globals@29.7.0)(@types/jest@29.5.12)(jest@29.7.0(@types/node@20.10.5)(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@types/node@20.10.5)(typescript@5.4.5)))(vitest@2.1.2(@types/node@20.10.5))':
|
||||
'@testing-library/jest-dom@6.4.6(@jest/globals@29.7.0)(@types/jest@29.5.12)(jest@29.7.0(@types/node@20.10.5)(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@types/node@20.10.5)(typescript@5.4.5)))(vitest@2.1.2(@types/node@20.10.5)(jsdom@20.0.3)(msw@2.6.5(@types/node@20.10.5)(typescript@5.4.5))(terser@5.36.0))':
|
||||
dependencies:
|
||||
'@adobe/css-tools': 4.4.0
|
||||
'@babel/runtime': 7.24.7
|
||||
@@ -16765,7 +16809,7 @@ snapshots:
|
||||
'@jest/globals': 29.7.0
|
||||
'@types/jest': 29.5.12
|
||||
jest: 29.7.0(@types/node@20.10.5)(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@types/node@20.10.5)(typescript@5.4.5))
|
||||
vitest: 2.1.2(@types/node@20.10.5)
|
||||
vitest: 2.1.2(@types/node@20.10.5)(jsdom@20.0.3)(msw@2.6.5(@types/node@20.10.5)(typescript@5.4.5))(terser@5.36.0)
|
||||
|
||||
'@testing-library/react@15.0.7(@types/react@18.2.79)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)':
|
||||
dependencies:
|
||||
@@ -17415,7 +17459,7 @@ snapshots:
|
||||
|
||||
'@ungap/structured-clone@1.2.0': {}
|
||||
|
||||
'@vercel/style-guide@6.0.0(@next/eslint-plugin-next@14.2.15)(eslint@8.57.0)(jest@29.7.0)(prettier@3.3.3)(typescript@5.4.5)(vitest@2.1.2)':
|
||||
'@vercel/style-guide@6.0.0(@next/eslint-plugin-next@14.2.15)(eslint@8.57.0)(jest@29.7.0(@types/node@20.14.8)(babel-plugin-macros@3.1.0))(prettier@3.3.3)(typescript@5.4.5)(vitest@2.1.2(@types/node@20.14.8)(jsdom@20.0.3)(msw@2.6.5(@types/node@20.14.8)(typescript@5.4.5))(terser@5.36.0))':
|
||||
dependencies:
|
||||
'@babel/core': 7.24.3
|
||||
'@babel/eslint-parser': 7.24.1(@babel/core@7.24.3)(eslint@8.57.0)
|
||||
@@ -17424,18 +17468,18 @@ snapshots:
|
||||
'@typescript-eslint/parser': 7.12.0(eslint@8.57.0)(typescript@5.4.5)
|
||||
eslint-config-prettier: 9.1.0(eslint@8.57.0)
|
||||
eslint-import-resolver-alias: 1.1.2(eslint-plugin-import@2.29.1(@typescript-eslint/parser@7.12.0(eslint@8.57.0)(typescript@5.4.5))(eslint-import-resolver-typescript@3.6.1)(eslint@8.57.0))
|
||||
eslint-import-resolver-typescript: 3.6.1(@typescript-eslint/parser@7.12.0(eslint@8.57.0)(typescript@5.4.5))(eslint-plugin-import@2.29.1)(eslint@8.57.0)
|
||||
eslint-import-resolver-typescript: 3.6.1(@typescript-eslint/parser@7.12.0(eslint@8.57.0)(typescript@5.4.5))(eslint-plugin-import@2.29.1(@typescript-eslint/parser@7.12.0(eslint@8.57.0)(typescript@5.4.5))(eslint-import-resolver-typescript@3.6.1)(eslint@8.57.0))(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.12.0(eslint@8.57.0)(typescript@5.4.5))(eslint-import-resolver-typescript@3.6.1)(eslint@8.57.0)
|
||||
eslint-plugin-jest: 27.9.0(@typescript-eslint/eslint-plugin@7.3.1(@typescript-eslint/parser@7.12.0(eslint@8.57.0)(typescript@5.4.5))(eslint@8.57.0)(typescript@5.4.5))(eslint@8.57.0)(jest@29.7.0)(typescript@5.4.5)
|
||||
eslint-plugin-import: 2.29.1(@typescript-eslint/parser@7.12.0(eslint@8.57.0)(typescript@5.4.5))(eslint-import-resolver-typescript@3.6.1(@typescript-eslint/parser@7.12.0(eslint@8.57.0)(typescript@5.4.5))(eslint-plugin-import@2.29.1(@typescript-eslint/parser@7.12.0(eslint@8.57.0)(typescript@5.4.5))(eslint-import-resolver-typescript@3.6.1)(eslint@8.57.0))(eslint@8.57.0))(eslint@8.57.0)
|
||||
eslint-plugin-jest: 27.9.0(@typescript-eslint/eslint-plugin@7.3.1(@typescript-eslint/parser@7.12.0(eslint@8.57.0)(typescript@5.4.5))(eslint@8.57.0)(typescript@5.4.5))(eslint@8.57.0)(jest@29.7.0(@types/node@20.14.8)(babel-plugin-macros@3.1.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(@typescript-eslint/eslint-plugin@7.3.1(@typescript-eslint/parser@7.12.0(eslint@8.57.0)(typescript@5.4.5))(eslint@8.57.0)(typescript@5.4.5))(eslint@8.57.0)(jest@29.7.0)(typescript@5.4.5))(eslint@8.57.0)
|
||||
eslint-plugin-playwright: 1.5.4(eslint-plugin-jest@27.9.0(@typescript-eslint/eslint-plugin@7.3.1(@typescript-eslint/parser@7.12.0(eslint@8.57.0)(typescript@5.4.5))(eslint@8.57.0)(typescript@5.4.5))(eslint@8.57.0)(jest@29.7.0(@types/node@20.14.8)(babel-plugin-macros@3.1.0))(typescript@5.4.5))(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)
|
||||
eslint-plugin-testing-library: 6.2.0(eslint@8.57.0)(typescript@5.4.5)
|
||||
eslint-plugin-tsdoc: 0.2.17
|
||||
eslint-plugin-unicorn: 51.0.1(eslint@8.57.0)
|
||||
eslint-plugin-vitest: 0.3.26(@typescript-eslint/eslint-plugin@7.3.1(@typescript-eslint/parser@7.12.0(eslint@8.57.0)(typescript@5.4.5))(eslint@8.57.0)(typescript@5.4.5))(eslint@8.57.0)(typescript@5.4.5)(vitest@2.1.2)
|
||||
eslint-plugin-vitest: 0.3.26(@typescript-eslint/eslint-plugin@7.3.1(@typescript-eslint/parser@7.12.0(eslint@8.57.0)(typescript@5.4.5))(eslint@8.57.0)(typescript@5.4.5))(eslint@8.57.0)(typescript@5.4.5)(vitest@2.1.2(@types/node@20.14.8)(jsdom@20.0.3)(msw@2.6.5(@types/node@20.14.8)(typescript@5.4.5))(terser@5.36.0))
|
||||
prettier-plugin-packagejson: 2.4.12(prettier@3.3.3)
|
||||
optionalDependencies:
|
||||
'@next/eslint-plugin-next': 14.2.15
|
||||
@@ -17456,6 +17500,16 @@ snapshots:
|
||||
chai: 5.1.1
|
||||
tinyrainbow: 1.2.0
|
||||
|
||||
'@vitest/mocker@2.1.2(@vitest/spy@2.1.2)(msw@2.6.5(@types/node@20.10.5)(typescript@5.4.5))(vite@5.2.14(@types/node@20.10.5)(terser@5.36.0))':
|
||||
dependencies:
|
||||
'@vitest/spy': 2.1.2
|
||||
estree-walker: 3.0.3
|
||||
magic-string: 0.30.11
|
||||
optionalDependencies:
|
||||
msw: 2.6.5(@types/node@20.10.5)(typescript@5.4.5)
|
||||
vite: 5.2.14(@types/node@20.10.5)(terser@5.36.0)
|
||||
optional: true
|
||||
|
||||
'@vitest/mocker@2.1.2(@vitest/spy@2.1.2)(msw@2.6.5(@types/node@20.11.29)(typescript@5.4.5))(vite@5.2.14(@types/node@20.11.29)(terser@5.36.0))':
|
||||
dependencies:
|
||||
'@vitest/spy': 2.1.2
|
||||
@@ -17465,13 +17519,14 @@ snapshots:
|
||||
msw: 2.6.5(@types/node@20.11.29)(typescript@5.4.5)
|
||||
vite: 5.2.14(@types/node@20.11.29)(terser@5.36.0)
|
||||
|
||||
'@vitest/mocker@2.1.2(@vitest/spy@2.1.2)(vite@5.2.14(@types/node@20.10.5))':
|
||||
'@vitest/mocker@2.1.2(@vitest/spy@2.1.2)(msw@2.6.5(@types/node@20.14.8)(typescript@5.4.5))(vite@5.2.14(@types/node@20.14.8)(terser@5.36.0))':
|
||||
dependencies:
|
||||
'@vitest/spy': 2.1.2
|
||||
estree-walker: 3.0.3
|
||||
magic-string: 0.30.11
|
||||
optionalDependencies:
|
||||
vite: 5.2.14(@types/node@20.10.5)
|
||||
msw: 2.6.5(@types/node@20.14.8)(typescript@5.4.5)
|
||||
vite: 5.2.14(@types/node@20.14.8)(terser@5.36.0)
|
||||
optional: true
|
||||
|
||||
'@vitest/pretty-format@2.1.2':
|
||||
@@ -18124,11 +18179,11 @@ snapshots:
|
||||
dependencies:
|
||||
semver: 7.6.3
|
||||
|
||||
bullmq@5.12.10:
|
||||
bullmq@5.34.10:
|
||||
dependencies:
|
||||
cron-parser: 4.9.0
|
||||
ioredis: 5.4.1
|
||||
msgpackr: 1.10.1
|
||||
msgpackr: 1.11.2
|
||||
node-abort-controller: 3.1.1
|
||||
semver: 7.6.3
|
||||
tslib: 2.6.3
|
||||
@@ -18521,22 +18576,6 @@ snapshots:
|
||||
crc-32: 1.2.2
|
||||
readable-stream: 3.6.2
|
||||
|
||||
create-jest@29.7.0:
|
||||
dependencies:
|
||||
'@jest/types': 29.6.3
|
||||
chalk: 4.1.2
|
||||
exit: 0.1.2
|
||||
graceful-fs: 4.2.11
|
||||
jest-config: 29.7.0(@types/node@20.14.8)
|
||||
jest-util: 29.7.0
|
||||
prompts: 2.4.2
|
||||
transitivePeerDependencies:
|
||||
- '@types/node'
|
||||
- babel-plugin-macros
|
||||
- supports-color
|
||||
- ts-node
|
||||
optional: true
|
||||
|
||||
create-jest@29.7.0(@types/node@20.10.5)(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@types/node@20.10.5)(typescript@5.4.5)):
|
||||
dependencies:
|
||||
'@jest/types': 29.6.3
|
||||
@@ -18552,6 +18591,22 @@ snapshots:
|
||||
- supports-color
|
||||
- ts-node
|
||||
|
||||
create-jest@29.7.0(@types/node@20.14.8)(babel-plugin-macros@3.1.0):
|
||||
dependencies:
|
||||
'@jest/types': 29.6.3
|
||||
chalk: 4.1.2
|
||||
exit: 0.1.2
|
||||
graceful-fs: 4.2.11
|
||||
jest-config: 29.7.0(@types/node@20.14.8)(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@types/node@20.10.5)(typescript@5.4.5))
|
||||
jest-util: 29.7.0
|
||||
prompts: 2.4.2
|
||||
transitivePeerDependencies:
|
||||
- '@types/node'
|
||||
- babel-plugin-macros
|
||||
- supports-color
|
||||
- ts-node
|
||||
optional: true
|
||||
|
||||
create-require@1.1.1: {}
|
||||
|
||||
crelt@1.0.6: {}
|
||||
@@ -18610,6 +18665,8 @@ snapshots:
|
||||
|
||||
csstype@3.1.3: {}
|
||||
|
||||
csv-parse@5.6.0: {}
|
||||
|
||||
cytoscape-cose-bilkent@4.1.0(cytoscape@3.30.2):
|
||||
dependencies:
|
||||
cose-base: 1.0.3
|
||||
@@ -19326,7 +19383,7 @@ snapshots:
|
||||
eslint: 8.57.0
|
||||
eslint-import-resolver-node: 0.3.9
|
||||
eslint-import-resolver-typescript: 3.6.1(@typescript-eslint/parser@7.12.0(eslint@8.57.0)(typescript@5.4.5))(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.12.0(eslint@8.57.0)(typescript@5.4.5))(eslint-import-resolver-typescript@3.6.1)(eslint@8.57.0)
|
||||
eslint-plugin-import: 2.29.1(@typescript-eslint/parser@7.12.0(eslint@8.57.0)(typescript@5.4.5))(eslint-import-resolver-typescript@3.6.1(@typescript-eslint/parser@7.12.0(eslint@8.57.0)(typescript@5.4.5))(eslint-plugin-import@2.29.1(@typescript-eslint/parser@7.12.0(eslint@8.57.0)(typescript@5.4.5))(eslint-import-resolver-typescript@3.6.1)(eslint@8.57.0))(eslint@8.57.0))(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)
|
||||
@@ -19343,7 +19400,7 @@ snapshots:
|
||||
eslint-config-standard@17.1.0(eslint-plugin-import@2.29.1(@typescript-eslint/parser@7.12.0(eslint@8.57.0)(typescript@5.4.5))(eslint@8.57.0))(eslint-plugin-n@16.6.2(eslint@8.57.0))(eslint-plugin-promise@6.4.0(eslint@8.57.0))(eslint@8.57.0):
|
||||
dependencies:
|
||||
eslint: 8.57.0
|
||||
eslint-plugin-import: 2.29.1(@typescript-eslint/parser@7.12.0(eslint@8.57.0)(typescript@5.4.5))(eslint@8.57.0)
|
||||
eslint-plugin-import: 2.29.1(@typescript-eslint/parser@7.12.0(eslint@8.57.0)(typescript@5.4.5))(eslint-import-resolver-typescript@3.6.1(@typescript-eslint/parser@7.12.0(eslint@8.57.0)(typescript@5.4.5))(eslint-plugin-import@2.29.1(@typescript-eslint/parser@7.12.0(eslint@8.57.0)(typescript@5.4.5))(eslint-import-resolver-typescript@3.6.1)(eslint@8.57.0))(eslint@8.57.0))(eslint@8.57.0)
|
||||
eslint-plugin-n: 16.6.2(eslint@8.57.0)
|
||||
eslint-plugin-promise: 6.4.0(eslint@8.57.0)
|
||||
|
||||
@@ -19354,7 +19411,7 @@ snapshots:
|
||||
|
||||
eslint-import-resolver-alias@1.1.2(eslint-plugin-import@2.29.1(@typescript-eslint/parser@7.12.0(eslint@8.57.0)(typescript@5.4.5))(eslint-import-resolver-typescript@3.6.1)(eslint@8.57.0)):
|
||||
dependencies:
|
||||
eslint-plugin-import: 2.29.1(@typescript-eslint/parser@7.12.0(eslint@8.57.0)(typescript@5.4.5))(eslint-import-resolver-typescript@3.6.1)(eslint@8.57.0)
|
||||
eslint-plugin-import: 2.29.1(@typescript-eslint/parser@7.12.0(eslint@8.57.0)(typescript@5.4.5))(eslint-import-resolver-typescript@3.6.1(@typescript-eslint/parser@7.12.0(eslint@8.57.0)(typescript@5.4.5))(eslint-plugin-import@2.29.1(@typescript-eslint/parser@7.12.0(eslint@8.57.0)(typescript@5.4.5))(eslint-import-resolver-typescript@3.6.1)(eslint@8.57.0))(eslint@8.57.0))(eslint@8.57.0)
|
||||
|
||||
eslint-import-resolver-node@0.3.9:
|
||||
dependencies:
|
||||
@@ -19370,7 +19427,7 @@ snapshots:
|
||||
enhanced-resolve: 5.17.1
|
||||
eslint: 8.57.0
|
||||
eslint-module-utils: 2.8.1(@typescript-eslint/parser@7.12.0(eslint@8.57.0)(typescript@5.4.5))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.1(@typescript-eslint/parser@7.12.0(eslint@8.57.0)(typescript@5.4.5))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.29.1)(eslint@8.57.0))(eslint@8.57.0)
|
||||
eslint-plugin-import: 2.29.1(@typescript-eslint/parser@7.12.0(eslint@8.57.0)(typescript@5.4.5))(eslint-import-resolver-typescript@3.6.1)(eslint@8.57.0)
|
||||
eslint-plugin-import: 2.29.1(@typescript-eslint/parser@7.12.0(eslint@8.57.0)(typescript@5.4.5))(eslint-import-resolver-typescript@3.6.1(@typescript-eslint/parser@7.12.0(eslint@8.57.0)(typescript@5.4.5))(eslint-plugin-import@2.29.1(@typescript-eslint/parser@7.12.0(eslint@8.57.0)(typescript@5.4.5))(eslint-import-resolver-typescript@3.6.1)(eslint@8.57.0))(eslint@8.57.0))(eslint@8.57.0)
|
||||
fast-glob: 3.3.2
|
||||
get-tsconfig: 4.8.1
|
||||
is-core-module: 2.15.1
|
||||
@@ -19381,13 +19438,13 @@ snapshots:
|
||||
- eslint-import-resolver-webpack
|
||||
- supports-color
|
||||
|
||||
eslint-import-resolver-typescript@3.6.1(@typescript-eslint/parser@7.12.0(eslint@8.57.0)(typescript@5.4.5))(eslint-plugin-import@2.29.1)(eslint@8.57.0):
|
||||
eslint-import-resolver-typescript@3.6.1(@typescript-eslint/parser@7.12.0(eslint@8.57.0)(typescript@5.4.5))(eslint-plugin-import@2.29.1(@typescript-eslint/parser@7.12.0(eslint@8.57.0)(typescript@5.4.5))(eslint-import-resolver-typescript@3.6.1)(eslint@8.57.0))(eslint@8.57.0):
|
||||
dependencies:
|
||||
debug: 4.3.7(supports-color@5.5.0)
|
||||
enhanced-resolve: 5.17.1
|
||||
eslint: 8.57.0
|
||||
eslint-module-utils: 2.8.1(@typescript-eslint/parser@7.12.0(eslint@8.57.0)(typescript@5.4.5))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.1(@typescript-eslint/parser@7.12.0(eslint@8.57.0)(typescript@5.4.5))(eslint-plugin-import@2.29.1)(eslint@8.57.0))(eslint@8.57.0)
|
||||
eslint-plugin-import: 2.29.1(@typescript-eslint/parser@7.12.0(eslint@8.57.0)(typescript@5.4.5))(eslint-import-resolver-typescript@3.6.1)(eslint@8.57.0)
|
||||
eslint-module-utils: 2.8.1(@typescript-eslint/parser@7.12.0(eslint@8.57.0)(typescript@5.4.5))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.1(@typescript-eslint/parser@7.12.0(eslint@8.57.0)(typescript@5.4.5))(eslint-plugin-import@2.29.1(@typescript-eslint/parser@7.12.0(eslint@8.57.0)(typescript@5.4.5))(eslint-import-resolver-typescript@3.6.1)(eslint@8.57.0))(eslint@8.57.0))(eslint@8.57.0)
|
||||
eslint-plugin-import: 2.29.1(@typescript-eslint/parser@7.12.0(eslint@8.57.0)(typescript@5.4.5))(eslint-import-resolver-typescript@3.6.1(@typescript-eslint/parser@7.12.0(eslint@8.57.0)(typescript@5.4.5))(eslint-plugin-import@2.29.1(@typescript-eslint/parser@7.12.0(eslint@8.57.0)(typescript@5.4.5))(eslint-import-resolver-typescript@3.6.1)(eslint@8.57.0))(eslint@8.57.0))(eslint@8.57.0)
|
||||
fast-glob: 3.3.2
|
||||
get-tsconfig: 4.8.1
|
||||
is-core-module: 2.15.1
|
||||
@@ -19409,14 +19466,14 @@ snapshots:
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
eslint-module-utils@2.8.1(@typescript-eslint/parser@7.12.0(eslint@8.57.0)(typescript@5.4.5))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.1(@typescript-eslint/parser@7.12.0(eslint@8.57.0)(typescript@5.4.5))(eslint-plugin-import@2.29.1)(eslint@8.57.0))(eslint@8.57.0):
|
||||
eslint-module-utils@2.8.1(@typescript-eslint/parser@7.12.0(eslint@8.57.0)(typescript@5.4.5))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.1(@typescript-eslint/parser@7.12.0(eslint@8.57.0)(typescript@5.4.5))(eslint-plugin-import@2.29.1(@typescript-eslint/parser@7.12.0(eslint@8.57.0)(typescript@5.4.5))(eslint-import-resolver-typescript@3.6.1)(eslint@8.57.0))(eslint@8.57.0))(eslint@8.57.0):
|
||||
dependencies:
|
||||
debug: 3.2.7
|
||||
optionalDependencies:
|
||||
'@typescript-eslint/parser': 7.12.0(eslint@8.57.0)(typescript@5.4.5)
|
||||
eslint: 8.57.0
|
||||
eslint-import-resolver-node: 0.3.9
|
||||
eslint-import-resolver-typescript: 3.6.1(@typescript-eslint/parser@7.12.0(eslint@8.57.0)(typescript@5.4.5))(eslint-plugin-import@2.29.1)(eslint@8.57.0)
|
||||
eslint-import-resolver-typescript: 3.6.1(@typescript-eslint/parser@7.12.0(eslint@8.57.0)(typescript@5.4.5))(eslint-plugin-import@2.29.1(@typescript-eslint/parser@7.12.0(eslint@8.57.0)(typescript@5.4.5))(eslint-import-resolver-typescript@3.6.1)(eslint@8.57.0))(eslint@8.57.0)
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
@@ -19433,7 +19490,7 @@ snapshots:
|
||||
eslint: 8.57.0
|
||||
ignore: 5.3.1
|
||||
|
||||
eslint-plugin-import@2.29.1(@typescript-eslint/parser@7.12.0(eslint@8.57.0)(typescript@5.4.5))(eslint-import-resolver-typescript@3.6.1)(eslint@8.57.0):
|
||||
eslint-plugin-import@2.29.1(@typescript-eslint/parser@7.12.0(eslint@8.57.0)(typescript@5.4.5))(eslint-import-resolver-typescript@3.6.1(@typescript-eslint/parser@7.12.0(eslint@8.57.0)(typescript@5.4.5))(eslint-plugin-import@2.29.1(@typescript-eslint/parser@7.12.0(eslint@8.57.0)(typescript@5.4.5))(eslint-import-resolver-typescript@3.6.1)(eslint@8.57.0))(eslint@8.57.0))(eslint@8.57.0):
|
||||
dependencies:
|
||||
array-includes: 3.1.7
|
||||
array.prototype.findlastindex: 1.2.4
|
||||
@@ -19443,7 +19500,7 @@ snapshots:
|
||||
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.12.0(eslint@8.57.0)(typescript@5.4.5))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.1(@typescript-eslint/parser@7.12.0(eslint@8.57.0)(typescript@5.4.5))(eslint-plugin-import@2.29.1)(eslint@8.57.0))(eslint@8.57.0)
|
||||
eslint-module-utils: 2.8.1(@typescript-eslint/parser@7.12.0(eslint@8.57.0)(typescript@5.4.5))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.1(@typescript-eslint/parser@7.12.0(eslint@8.57.0)(typescript@5.4.5))(eslint-plugin-import@2.29.1(@typescript-eslint/parser@7.12.0(eslint@8.57.0)(typescript@5.4.5))(eslint-import-resolver-typescript@3.6.1)(eslint@8.57.0))(eslint@8.57.0))(eslint@8.57.0)
|
||||
hasown: 2.0.2
|
||||
is-core-module: 2.15.1
|
||||
is-glob: 4.0.3
|
||||
@@ -19460,40 +19517,13 @@ snapshots:
|
||||
- eslint-import-resolver-webpack
|
||||
- supports-color
|
||||
|
||||
eslint-plugin-import@2.29.1(@typescript-eslint/parser@7.12.0(eslint@8.57.0)(typescript@5.4.5))(eslint@8.57.0):
|
||||
dependencies:
|
||||
array-includes: 3.1.7
|
||||
array.prototype.findlastindex: 1.2.4
|
||||
array.prototype.flat: 1.3.2
|
||||
array.prototype.flatmap: 1.3.2
|
||||
debug: 3.2.7
|
||||
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.12.0(eslint@8.57.0)(typescript@5.4.5))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.1(@typescript-eslint/parser@7.12.0(eslint@8.57.0)(typescript@5.4.5))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.29.1)(eslint@8.57.0))(eslint@8.57.0)
|
||||
hasown: 2.0.2
|
||||
is-core-module: 2.15.1
|
||||
is-glob: 4.0.3
|
||||
minimatch: 3.1.2
|
||||
object.fromentries: 2.0.8
|
||||
object.groupby: 1.0.3
|
||||
object.values: 1.2.0
|
||||
semver: 6.3.1
|
||||
tsconfig-paths: 3.15.0
|
||||
optionalDependencies:
|
||||
'@typescript-eslint/parser': 7.12.0(eslint@8.57.0)(typescript@5.4.5)
|
||||
transitivePeerDependencies:
|
||||
- eslint-import-resolver-typescript
|
||||
- eslint-import-resolver-webpack
|
||||
- supports-color
|
||||
|
||||
eslint-plugin-jest@27.9.0(@typescript-eslint/eslint-plugin@7.3.1(@typescript-eslint/parser@7.12.0(eslint@8.57.0)(typescript@5.4.5))(eslint@8.57.0)(typescript@5.4.5))(eslint@8.57.0)(jest@29.7.0)(typescript@5.4.5):
|
||||
eslint-plugin-jest@27.9.0(@typescript-eslint/eslint-plugin@7.3.1(@typescript-eslint/parser@7.12.0(eslint@8.57.0)(typescript@5.4.5))(eslint@8.57.0)(typescript@5.4.5))(eslint@8.57.0)(jest@29.7.0(@types/node@20.14.8)(babel-plugin-macros@3.1.0))(typescript@5.4.5):
|
||||
dependencies:
|
||||
'@typescript-eslint/utils': 5.62.0(eslint@8.57.0)(typescript@5.4.5)
|
||||
eslint: 8.57.0
|
||||
optionalDependencies:
|
||||
'@typescript-eslint/eslint-plugin': 7.3.1(@typescript-eslint/parser@7.12.0(eslint@8.57.0)(typescript@5.4.5))(eslint@8.57.0)(typescript@5.4.5)
|
||||
jest: 29.7.0
|
||||
jest: 29.7.0(@types/node@20.14.8)(babel-plugin-macros@3.1.0)
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
- typescript
|
||||
@@ -19535,12 +19565,12 @@ snapshots:
|
||||
|
||||
eslint-plugin-only-warn@1.1.0: {}
|
||||
|
||||
eslint-plugin-playwright@1.5.4(eslint-plugin-jest@27.9.0(@typescript-eslint/eslint-plugin@7.3.1(@typescript-eslint/parser@7.12.0(eslint@8.57.0)(typescript@5.4.5))(eslint@8.57.0)(typescript@5.4.5))(eslint@8.57.0)(jest@29.7.0)(typescript@5.4.5))(eslint@8.57.0):
|
||||
eslint-plugin-playwright@1.5.4(eslint-plugin-jest@27.9.0(@typescript-eslint/eslint-plugin@7.3.1(@typescript-eslint/parser@7.12.0(eslint@8.57.0)(typescript@5.4.5))(eslint@8.57.0)(typescript@5.4.5))(eslint@8.57.0)(jest@29.7.0(@types/node@20.14.8)(babel-plugin-macros@3.1.0))(typescript@5.4.5))(eslint@8.57.0):
|
||||
dependencies:
|
||||
eslint: 8.57.0
|
||||
globals: 13.24.0
|
||||
optionalDependencies:
|
||||
eslint-plugin-jest: 27.9.0(@typescript-eslint/eslint-plugin@7.3.1(@typescript-eslint/parser@7.12.0(eslint@8.57.0)(typescript@5.4.5))(eslint@8.57.0)(typescript@5.4.5))(eslint@8.57.0)(jest@29.7.0)(typescript@5.4.5)
|
||||
eslint-plugin-jest: 27.9.0(@typescript-eslint/eslint-plugin@7.3.1(@typescript-eslint/parser@7.12.0(eslint@8.57.0)(typescript@5.4.5))(eslint@8.57.0)(typescript@5.4.5))(eslint@8.57.0)(jest@29.7.0(@types/node@20.14.8)(babel-plugin-macros@3.1.0))(typescript@5.4.5)
|
||||
|
||||
eslint-plugin-prettier@5.1.3(@types/eslint@8.56.12)(eslint-config-prettier@9.1.0(eslint@8.57.0))(eslint@8.57.0)(prettier@3.3.3):
|
||||
dependencies:
|
||||
@@ -19622,13 +19652,13 @@ snapshots:
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
eslint-plugin-vitest@0.3.26(@typescript-eslint/eslint-plugin@7.3.1(@typescript-eslint/parser@7.12.0(eslint@8.57.0)(typescript@5.4.5))(eslint@8.57.0)(typescript@5.4.5))(eslint@8.57.0)(typescript@5.4.5)(vitest@2.1.2):
|
||||
eslint-plugin-vitest@0.3.26(@typescript-eslint/eslint-plugin@7.3.1(@typescript-eslint/parser@7.12.0(eslint@8.57.0)(typescript@5.4.5))(eslint@8.57.0)(typescript@5.4.5))(eslint@8.57.0)(typescript@5.4.5)(vitest@2.1.2(@types/node@20.14.8)(jsdom@20.0.3)(msw@2.6.5(@types/node@20.14.8)(typescript@5.4.5))(terser@5.36.0)):
|
||||
dependencies:
|
||||
'@typescript-eslint/utils': 7.3.1(eslint@8.57.0)(typescript@5.4.5)
|
||||
eslint: 8.57.0
|
||||
optionalDependencies:
|
||||
'@typescript-eslint/eslint-plugin': 7.3.1(@typescript-eslint/parser@7.12.0(eslint@8.57.0)(typescript@5.4.5))(eslint@8.57.0)(typescript@5.4.5)
|
||||
vitest: 2.1.2(@types/node@20.11.29)(jsdom@20.0.3)(msw@2.6.5(@types/node@20.11.29)(typescript@5.4.5))(terser@5.36.0)
|
||||
vitest: 2.1.2(@types/node@20.14.8)(jsdom@20.0.3)(msw@2.6.5(@types/node@20.14.8)(typescript@5.4.5))(terser@5.36.0)
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
- typescript
|
||||
@@ -20900,26 +20930,6 @@ snapshots:
|
||||
- babel-plugin-macros
|
||||
- supports-color
|
||||
|
||||
jest-cli@29.7.0:
|
||||
dependencies:
|
||||
'@jest/core': 29.7.0
|
||||
'@jest/test-result': 29.7.0
|
||||
'@jest/types': 29.6.3
|
||||
chalk: 4.1.2
|
||||
create-jest: 29.7.0
|
||||
exit: 0.1.2
|
||||
import-local: 3.1.0
|
||||
jest-config: 29.7.0(@types/node@20.14.8)
|
||||
jest-util: 29.7.0
|
||||
jest-validate: 29.7.0
|
||||
yargs: 17.7.2
|
||||
transitivePeerDependencies:
|
||||
- '@types/node'
|
||||
- babel-plugin-macros
|
||||
- supports-color
|
||||
- ts-node
|
||||
optional: true
|
||||
|
||||
jest-cli@29.7.0(@types/node@20.10.5)(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@types/node@20.10.5)(typescript@5.4.5)):
|
||||
dependencies:
|
||||
'@jest/core': 29.7.0(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@types/node@20.10.5)(typescript@5.4.5))
|
||||
@@ -20939,6 +20949,26 @@ snapshots:
|
||||
- supports-color
|
||||
- ts-node
|
||||
|
||||
jest-cli@29.7.0(@types/node@20.14.8)(babel-plugin-macros@3.1.0):
|
||||
dependencies:
|
||||
'@jest/core': 29.7.0(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@types/node@20.10.5)(typescript@5.4.5))
|
||||
'@jest/test-result': 29.7.0
|
||||
'@jest/types': 29.6.3
|
||||
chalk: 4.1.2
|
||||
create-jest: 29.7.0(@types/node@20.14.8)(babel-plugin-macros@3.1.0)
|
||||
exit: 0.1.2
|
||||
import-local: 3.1.0
|
||||
jest-config: 29.7.0(@types/node@20.14.8)(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@types/node@20.10.5)(typescript@5.4.5))
|
||||
jest-util: 29.7.0
|
||||
jest-validate: 29.7.0
|
||||
yargs: 17.7.2
|
||||
transitivePeerDependencies:
|
||||
- '@types/node'
|
||||
- babel-plugin-macros
|
||||
- supports-color
|
||||
- ts-node
|
||||
optional: true
|
||||
|
||||
jest-config@29.7.0(@types/node@20.10.5)(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@types/node@20.10.5)(typescript@5.4.5)):
|
||||
dependencies:
|
||||
'@babel/core': 7.24.3
|
||||
@@ -20970,37 +21000,6 @@ snapshots:
|
||||
- babel-plugin-macros
|
||||
- supports-color
|
||||
|
||||
jest-config@29.7.0(@types/node@20.14.8):
|
||||
dependencies:
|
||||
'@babel/core': 7.24.3
|
||||
'@jest/test-sequencer': 29.7.0
|
||||
'@jest/types': 29.6.3
|
||||
babel-jest: 29.7.0(@babel/core@7.24.3)
|
||||
chalk: 4.1.2
|
||||
ci-info: 3.9.0
|
||||
deepmerge: 4.3.1
|
||||
glob: 7.2.3
|
||||
graceful-fs: 4.2.11
|
||||
jest-circus: 29.7.0(babel-plugin-macros@3.1.0)
|
||||
jest-environment-node: 29.7.0
|
||||
jest-get-type: 29.6.3
|
||||
jest-regex-util: 29.6.3
|
||||
jest-resolve: 29.7.0
|
||||
jest-runner: 29.7.0
|
||||
jest-util: 29.7.0
|
||||
jest-validate: 29.7.0
|
||||
micromatch: 4.0.8
|
||||
parse-json: 5.2.0
|
||||
pretty-format: 29.7.0
|
||||
slash: 3.0.0
|
||||
strip-json-comments: 3.1.1
|
||||
optionalDependencies:
|
||||
'@types/node': 20.14.8
|
||||
transitivePeerDependencies:
|
||||
- babel-plugin-macros
|
||||
- supports-color
|
||||
optional: true
|
||||
|
||||
jest-config@29.7.0(@types/node@20.14.8)(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@types/node@20.10.5)(typescript@5.4.5)):
|
||||
dependencies:
|
||||
'@babel/core': 7.24.3
|
||||
@@ -21268,19 +21267,6 @@ snapshots:
|
||||
merge-stream: 2.0.0
|
||||
supports-color: 8.1.1
|
||||
|
||||
jest@29.7.0:
|
||||
dependencies:
|
||||
'@jest/core': 29.7.0
|
||||
'@jest/types': 29.6.3
|
||||
import-local: 3.1.0
|
||||
jest-cli: 29.7.0
|
||||
transitivePeerDependencies:
|
||||
- '@types/node'
|
||||
- babel-plugin-macros
|
||||
- supports-color
|
||||
- ts-node
|
||||
optional: true
|
||||
|
||||
jest@29.7.0(@types/node@20.10.5)(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@types/node@20.10.5)(typescript@5.4.5)):
|
||||
dependencies:
|
||||
'@jest/core': 29.7.0(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@types/node@20.10.5)(typescript@5.4.5))
|
||||
@@ -21293,6 +21279,19 @@ snapshots:
|
||||
- supports-color
|
||||
- ts-node
|
||||
|
||||
jest@29.7.0(@types/node@20.14.8)(babel-plugin-macros@3.1.0):
|
||||
dependencies:
|
||||
'@jest/core': 29.7.0(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@types/node@20.10.5)(typescript@5.4.5))
|
||||
'@jest/types': 29.6.3
|
||||
import-local: 3.1.0
|
||||
jest-cli: 29.7.0(@types/node@20.14.8)(babel-plugin-macros@3.1.0)
|
||||
transitivePeerDependencies:
|
||||
- '@types/node'
|
||||
- babel-plugin-macros
|
||||
- supports-color
|
||||
- ts-node
|
||||
optional: true
|
||||
|
||||
jiti@1.21.7: {}
|
||||
|
||||
jju@1.4.0: {}
|
||||
@@ -21485,7 +21484,7 @@ snapshots:
|
||||
- encoding
|
||||
- openai
|
||||
|
||||
langchain@0.3.6(@langchain/anthropic@0.3.8(@langchain/core@0.3.18(openai@4.72.0(zod@3.23.8))))(@langchain/aws@0.1.2(@langchain/core@0.3.18(openai@4.72.0(zod@3.23.8))))(@langchain/core@0.3.18(openai@4.72.0(zod@3.23.8)))(@langchain/google-vertexai@0.1.3(@langchain/core@0.3.18(openai@4.72.0(zod@3.23.8)))(zod@3.23.8))(axios@1.7.7)(handlebars@4.7.8)(openai@4.72.0(zod@3.23.8)):
|
||||
langchain@0.3.6(@langchain/anthropic@0.3.8(@langchain/core@0.3.18(openai@4.72.0(zod@3.23.8))))(@langchain/aws@0.1.2(@aws-sdk/client-sts@3.675.0)(@langchain/core@0.3.18(openai@4.72.0(zod@3.23.8))))(@langchain/core@0.3.18(openai@4.72.0(zod@3.23.8)))(@langchain/google-vertexai@0.1.3(@langchain/core@0.3.18(openai@4.72.0(zod@3.23.8)))(zod@3.23.8))(axios@1.7.7)(handlebars@4.7.8)(openai@4.72.0(zod@3.23.8)):
|
||||
dependencies:
|
||||
'@langchain/core': 0.3.18(openai@4.72.0(zod@3.23.8))
|
||||
'@langchain/openai': 0.3.14(@langchain/core@0.3.18(openai@4.72.0(zod@3.23.8)))
|
||||
@@ -21502,7 +21501,7 @@ snapshots:
|
||||
zod-to-json-schema: 3.23.5(zod@3.23.8)
|
||||
optionalDependencies:
|
||||
'@langchain/anthropic': 0.3.8(@langchain/core@0.3.18(openai@4.72.0(zod@3.23.8)))
|
||||
'@langchain/aws': 0.1.2(@langchain/core@0.3.18(openai@4.72.0(zod@3.23.8)))
|
||||
'@langchain/aws': 0.1.2(@aws-sdk/client-sts@3.675.0)(@langchain/core@0.3.18(openai@4.72.0(zod@3.23.8)))
|
||||
'@langchain/google-vertexai': 0.1.3(@langchain/core@0.3.18(openai@4.72.0(zod@3.23.8)))(zod@3.23.8)
|
||||
axios: 1.7.7
|
||||
handlebars: 4.7.8
|
||||
@@ -22227,10 +22226,36 @@ snapshots:
|
||||
'@msgpackr-extract/msgpackr-extract-win32-x64': 3.0.3
|
||||
optional: true
|
||||
|
||||
msgpackr@1.10.1:
|
||||
msgpackr@1.11.2:
|
||||
optionalDependencies:
|
||||
msgpackr-extract: 3.0.3
|
||||
|
||||
msw@2.6.5(@types/node@20.10.5)(typescript@5.4.5):
|
||||
dependencies:
|
||||
'@bundled-es-modules/cookie': 2.0.1
|
||||
'@bundled-es-modules/statuses': 1.0.1
|
||||
'@bundled-es-modules/tough-cookie': 0.1.6
|
||||
'@inquirer/confirm': 5.0.2(@types/node@20.10.5)
|
||||
'@mswjs/interceptors': 0.37.1
|
||||
'@open-draft/deferred-promise': 2.2.0
|
||||
'@open-draft/until': 2.1.0
|
||||
'@types/cookie': 0.6.0
|
||||
'@types/statuses': 2.0.5
|
||||
chalk: 4.1.2
|
||||
graphql: 16.9.0
|
||||
headers-polyfill: 4.0.3
|
||||
is-node-process: 1.2.0
|
||||
outvariant: 1.4.3
|
||||
path-to-regexp: 6.3.0
|
||||
strict-event-emitter: 0.5.1
|
||||
type-fest: 4.27.0
|
||||
yargs: 17.7.2
|
||||
optionalDependencies:
|
||||
typescript: 5.4.5
|
||||
transitivePeerDependencies:
|
||||
- '@types/node'
|
||||
optional: true
|
||||
|
||||
msw@2.6.5(@types/node@20.11.29)(typescript@5.4.5):
|
||||
dependencies:
|
||||
'@bundled-es-modules/cookie': 2.0.1
|
||||
@@ -22256,6 +22281,32 @@ snapshots:
|
||||
transitivePeerDependencies:
|
||||
- '@types/node'
|
||||
|
||||
msw@2.6.5(@types/node@20.14.8)(typescript@5.4.5):
|
||||
dependencies:
|
||||
'@bundled-es-modules/cookie': 2.0.1
|
||||
'@bundled-es-modules/statuses': 1.0.1
|
||||
'@bundled-es-modules/tough-cookie': 0.1.6
|
||||
'@inquirer/confirm': 5.0.2(@types/node@20.14.8)
|
||||
'@mswjs/interceptors': 0.37.1
|
||||
'@open-draft/deferred-promise': 2.2.0
|
||||
'@open-draft/until': 2.1.0
|
||||
'@types/cookie': 0.6.0
|
||||
'@types/statuses': 2.0.5
|
||||
chalk: 4.1.2
|
||||
graphql: 16.9.0
|
||||
headers-polyfill: 4.0.3
|
||||
is-node-process: 1.2.0
|
||||
outvariant: 1.4.3
|
||||
path-to-regexp: 6.3.0
|
||||
strict-event-emitter: 0.5.1
|
||||
type-fest: 4.27.0
|
||||
yargs: 17.7.2
|
||||
optionalDependencies:
|
||||
typescript: 5.4.5
|
||||
transitivePeerDependencies:
|
||||
- '@types/node'
|
||||
optional: true
|
||||
|
||||
mustache@4.2.0: {}
|
||||
|
||||
mute-stream@1.0.0: {}
|
||||
@@ -24700,12 +24751,12 @@ snapshots:
|
||||
d3-time: 3.1.0
|
||||
d3-timer: 3.0.1
|
||||
|
||||
vite-node@2.1.2(@types/node@20.10.5):
|
||||
vite-node@2.1.2(@types/node@20.10.5)(terser@5.36.0):
|
||||
dependencies:
|
||||
cac: 6.7.14
|
||||
debug: 4.3.7(supports-color@5.5.0)
|
||||
pathe: 1.1.2
|
||||
vite: 5.2.14(@types/node@20.10.5)
|
||||
vite: 5.2.14(@types/node@20.10.5)(terser@5.36.0)
|
||||
transitivePeerDependencies:
|
||||
- '@types/node'
|
||||
- less
|
||||
@@ -24733,7 +24784,24 @@ snapshots:
|
||||
- supports-color
|
||||
- terser
|
||||
|
||||
vite@5.2.14(@types/node@20.10.5):
|
||||
vite-node@2.1.2(@types/node@20.14.8)(terser@5.36.0):
|
||||
dependencies:
|
||||
cac: 6.7.14
|
||||
debug: 4.3.7(supports-color@5.5.0)
|
||||
pathe: 1.1.2
|
||||
vite: 5.2.14(@types/node@20.14.8)(terser@5.36.0)
|
||||
transitivePeerDependencies:
|
||||
- '@types/node'
|
||||
- less
|
||||
- lightningcss
|
||||
- sass
|
||||
- stylus
|
||||
- sugarss
|
||||
- supports-color
|
||||
- terser
|
||||
optional: true
|
||||
|
||||
vite@5.2.14(@types/node@20.10.5)(terser@5.36.0):
|
||||
dependencies:
|
||||
esbuild: 0.20.2
|
||||
postcss: 8.4.49
|
||||
@@ -24741,6 +24809,7 @@ snapshots:
|
||||
optionalDependencies:
|
||||
'@types/node': 20.10.5
|
||||
fsevents: 2.3.3
|
||||
terser: 5.36.0
|
||||
optional: true
|
||||
|
||||
vite@5.2.14(@types/node@20.11.29)(terser@5.36.0):
|
||||
@@ -24753,10 +24822,21 @@ snapshots:
|
||||
fsevents: 2.3.3
|
||||
terser: 5.36.0
|
||||
|
||||
vitest@2.1.2(@types/node@20.10.5):
|
||||
vite@5.2.14(@types/node@20.14.8)(terser@5.36.0):
|
||||
dependencies:
|
||||
esbuild: 0.20.2
|
||||
postcss: 8.4.49
|
||||
rollup: 4.13.0
|
||||
optionalDependencies:
|
||||
'@types/node': 20.14.8
|
||||
fsevents: 2.3.3
|
||||
terser: 5.36.0
|
||||
optional: true
|
||||
|
||||
vitest@2.1.2(@types/node@20.10.5)(jsdom@20.0.3)(msw@2.6.5(@types/node@20.10.5)(typescript@5.4.5))(terser@5.36.0):
|
||||
dependencies:
|
||||
'@vitest/expect': 2.1.2
|
||||
'@vitest/mocker': 2.1.2(@vitest/spy@2.1.2)(vite@5.2.14(@types/node@20.10.5))
|
||||
'@vitest/mocker': 2.1.2(@vitest/spy@2.1.2)(msw@2.6.5(@types/node@20.10.5)(typescript@5.4.5))(vite@5.2.14(@types/node@20.10.5)(terser@5.36.0))
|
||||
'@vitest/pretty-format': 2.1.2
|
||||
'@vitest/runner': 2.1.2
|
||||
'@vitest/snapshot': 2.1.2
|
||||
@@ -24771,11 +24851,12 @@ snapshots:
|
||||
tinyexec: 0.3.0
|
||||
tinypool: 1.0.1
|
||||
tinyrainbow: 1.2.0
|
||||
vite: 5.2.14(@types/node@20.10.5)
|
||||
vite-node: 2.1.2(@types/node@20.10.5)
|
||||
vite: 5.2.14(@types/node@20.10.5)(terser@5.36.0)
|
||||
vite-node: 2.1.2(@types/node@20.10.5)(terser@5.36.0)
|
||||
why-is-node-running: 2.3.0
|
||||
optionalDependencies:
|
||||
'@types/node': 20.10.5
|
||||
jsdom: 20.0.3
|
||||
transitivePeerDependencies:
|
||||
- less
|
||||
- lightningcss
|
||||
@@ -24821,6 +24902,41 @@ snapshots:
|
||||
- supports-color
|
||||
- terser
|
||||
|
||||
vitest@2.1.2(@types/node@20.14.8)(jsdom@20.0.3)(msw@2.6.5(@types/node@20.14.8)(typescript@5.4.5))(terser@5.36.0):
|
||||
dependencies:
|
||||
'@vitest/expect': 2.1.2
|
||||
'@vitest/mocker': 2.1.2(@vitest/spy@2.1.2)(msw@2.6.5(@types/node@20.14.8)(typescript@5.4.5))(vite@5.2.14(@types/node@20.14.8)(terser@5.36.0))
|
||||
'@vitest/pretty-format': 2.1.2
|
||||
'@vitest/runner': 2.1.2
|
||||
'@vitest/snapshot': 2.1.2
|
||||
'@vitest/spy': 2.1.2
|
||||
'@vitest/utils': 2.1.2
|
||||
chai: 5.1.1
|
||||
debug: 4.3.7(supports-color@5.5.0)
|
||||
magic-string: 0.30.11
|
||||
pathe: 1.1.2
|
||||
std-env: 3.7.0
|
||||
tinybench: 2.9.0
|
||||
tinyexec: 0.3.0
|
||||
tinypool: 1.0.1
|
||||
tinyrainbow: 1.2.0
|
||||
vite: 5.2.14(@types/node@20.14.8)(terser@5.36.0)
|
||||
vite-node: 2.1.2(@types/node@20.14.8)(terser@5.36.0)
|
||||
why-is-node-running: 2.3.0
|
||||
optionalDependencies:
|
||||
'@types/node': 20.14.8
|
||||
jsdom: 20.0.3
|
||||
transitivePeerDependencies:
|
||||
- less
|
||||
- lightningcss
|
||||
- msw
|
||||
- sass
|
||||
- stylus
|
||||
- sugarss
|
||||
- supports-color
|
||||
- terser
|
||||
optional: true
|
||||
|
||||
vscode-jsonrpc@8.2.0: {}
|
||||
|
||||
vscode-languageserver-protocol@3.17.5:
|
||||
|
||||
+3
-3
@@ -12,8 +12,8 @@ import { env } from "./src/env.mjs";
|
||||
*/
|
||||
const cspHeader = `
|
||||
default-src 'self' https://*.langfuse.com https://*.langfuse.dev https://*.posthog.com https://*.sentry.io wss://*.crisp.chat https://*.crisp.chat;
|
||||
script-src 'self' 'unsafe-eval' 'unsafe-inline' https://*.langfuse.com https://*.langfuse.dev https://client.crisp.chat https://settings.crisp.chat https://challenges.cloudflare.com https://*.sentry.io https://static.cloudflareinsights.com https://*.stripe.com;
|
||||
style-src 'self' 'unsafe-inline' https://client.crisp.chat;
|
||||
script-src 'self' 'unsafe-eval' 'unsafe-inline' https://*.langfuse.com https://*.langfuse.dev https://client.crisp.chat https://settings.crisp.chat https://challenges.cloudflare.com https://*.sentry.io https://static.cloudflareinsights.com https://*.stripe.com https://uptime.betterstack.com;
|
||||
style-src 'self' 'unsafe-inline' https://client.crisp.chat https://uptime.betterstack.com;
|
||||
img-src 'self' https: blob: data: http://localhost:* https://client.crisp.chat https://image.crisp.chat https://storage.crisp.chat;
|
||||
font-src 'self' https://client.crisp.chat;
|
||||
frame-src 'self' https://challenges.cloudflare.com https://*.stripe.com https://game.crisp.chat;
|
||||
@@ -22,7 +22,7 @@ const cspHeader = `
|
||||
base-uri 'self';
|
||||
form-action 'self';
|
||||
frame-ancestors 'none';
|
||||
connect-src 'self' https://*.langfuse.com https://*.langfuse.dev https://client.crisp.chat https://storage.crisp.chat wss://client.relay.crisp.chat wss://stream.relay.crisp.chat https://*.ingest.us.sentry.io;
|
||||
connect-src 'self' https://*.langfuse.com https://*.langfuse.dev https://client.crisp.chat https://storage.crisp.chat wss://client.relay.crisp.chat wss://stream.relay.crisp.chat https://*.ingest.us.sentry.io https://uptime.betterstack.com;
|
||||
media-src 'self' https: http://localhost:* https://client.crisp.chat;
|
||||
${env.LANGFUSE_CSP_ENFORCE_HTTPS === "true" ? "upgrade-insecure-requests; block-all-mixed-content;" : ""}
|
||||
${env.SENTRY_CSP_REPORT_URI ? `report-uri ${env.SENTRY_CSP_REPORT_URI}; report-to csp-endpoint;` : ""}
|
||||
|
||||
+2
-2
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "web",
|
||||
"version": "3.6.2",
|
||||
"version": "3.9.0",
|
||||
"private": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
@@ -99,7 +99,7 @@
|
||||
"@uiw/react-codemirror": "^4.21.25",
|
||||
"ai": "^3.4.9",
|
||||
"bcryptjs": "^2.4.3",
|
||||
"bullmq": "^5.12.10",
|
||||
"bullmq": "^5.34.10",
|
||||
"class-variance-authority": "^0.7.0",
|
||||
"clsx": "^2.1.1",
|
||||
"cmdk": "^1.0.0",
|
||||
|
||||
@@ -3186,11 +3186,12 @@ components:
|
||||
to exact match, use `(?i)^modelname$`
|
||||
startDate:
|
||||
type: string
|
||||
format: date
|
||||
format: date-time
|
||||
nullable: true
|
||||
description: Apply only to generations which are newer than this ISO date.
|
||||
unit:
|
||||
$ref: '#/components/schemas/ModelUsageUnit'
|
||||
nullable: true
|
||||
description: Unit used by this model.
|
||||
inputPrice:
|
||||
type: number
|
||||
@@ -3226,7 +3227,6 @@ components:
|
||||
- id
|
||||
- modelName
|
||||
- matchPattern
|
||||
- unit
|
||||
- isLangfuseManaged
|
||||
ModelUsageUnit:
|
||||
title: ModelUsageUnit
|
||||
@@ -4279,6 +4279,7 @@ components:
|
||||
description: Apply only to generations which are newer than this ISO date.
|
||||
unit:
|
||||
$ref: '#/components/schemas/ModelUsageUnit'
|
||||
nullable: true
|
||||
description: Unit used by this model.
|
||||
inputPrice:
|
||||
type: number
|
||||
@@ -4311,7 +4312,6 @@ components:
|
||||
required:
|
||||
- modelName
|
||||
- matchPattern
|
||||
- unit
|
||||
Observations:
|
||||
title: Observations
|
||||
type: object
|
||||
|
||||
@@ -123,7 +123,7 @@ test("Unauthenticated user should not be redirected to non-relative URLs after l
|
||||
test("Unauthenticated user should be redirected to relative URL after login", async ({
|
||||
page,
|
||||
}) => {
|
||||
const relativeUrl = "/support";
|
||||
const relativeUrl = "/setup";
|
||||
await page.goto(
|
||||
`/auth/sign-in?targetPath=${encodeURIComponent(relativeUrl)}`,
|
||||
);
|
||||
|
||||
@@ -0,0 +1,219 @@
|
||||
import { randomUUID } from "crypto";
|
||||
import { makeAPICall } from "@/src/__tests__/test-utils";
|
||||
import waitForExpect from "wait-for-expect";
|
||||
import {
|
||||
getObservationById,
|
||||
getScoreById,
|
||||
getTraceById,
|
||||
} from "@langfuse/shared/src/server";
|
||||
import { v4 } from "uuid";
|
||||
|
||||
const projectId = "7a88fb47-b4e2-43b8-a06c-a5ce950dc53a";
|
||||
|
||||
describe("/api/public/ingestion API Endpoint", () => {
|
||||
it.each([
|
||||
[
|
||||
"plain",
|
||||
{
|
||||
id: randomUUID(),
|
||||
type: "trace-create",
|
||||
timestamp: new Date().toISOString(),
|
||||
body: {
|
||||
id: randomUUID(),
|
||||
timestamp: new Date().toISOString(),
|
||||
},
|
||||
},
|
||||
],
|
||||
[
|
||||
"metadata+io",
|
||||
{
|
||||
id: randomUUID(),
|
||||
type: "trace-create",
|
||||
timestamp: new Date().toISOString(),
|
||||
body: {
|
||||
id: randomUUID(),
|
||||
timestamp: new Date().toISOString(),
|
||||
metadata: { hello: "world" },
|
||||
input: "input",
|
||||
output: "output",
|
||||
},
|
||||
},
|
||||
],
|
||||
[
|
||||
"complex-io",
|
||||
{
|
||||
id: randomUUID(),
|
||||
type: "trace-create",
|
||||
timestamp: new Date().toISOString(),
|
||||
body: {
|
||||
id: randomUUID(),
|
||||
timestamp: new Date().toISOString(),
|
||||
metadata: { hello: "world" },
|
||||
input: ["hello", { world: "world" }, [1, 2, 3]],
|
||||
},
|
||||
},
|
||||
],
|
||||
])(
|
||||
"should create traces via the ingestion API (%s)",
|
||||
async (_name: string, entity: any) => {
|
||||
const response = await makeAPICall("POST", "/api/public/ingestion", {
|
||||
batch: [entity],
|
||||
});
|
||||
|
||||
expect(response.status).toBe(207);
|
||||
|
||||
await waitForExpect(async () => {
|
||||
const trace = await getTraceById(entity.body.id, projectId);
|
||||
expect(trace).toBeDefined();
|
||||
expect(trace!.id).toBe(entity.body.id);
|
||||
expect(trace!.projectId).toBe(projectId);
|
||||
expect(trace!.metadata).toEqual(entity.body?.metadata ?? {});
|
||||
expect(trace!.input).toEqual(entity.body?.input ?? null);
|
||||
expect(trace!.output).toEqual(entity.body?.output ?? null);
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
it.each([
|
||||
[
|
||||
"generation",
|
||||
"GENERATION",
|
||||
{
|
||||
id: randomUUID(),
|
||||
type: "generation-create",
|
||||
timestamp: new Date().toISOString(),
|
||||
body: {
|
||||
id: randomUUID(),
|
||||
traceId: randomUUID(),
|
||||
parentObservationId: randomUUID(),
|
||||
startTime: new Date().toISOString(),
|
||||
model: "gpt-4",
|
||||
input: { text: "input" },
|
||||
output: { text: "output" },
|
||||
},
|
||||
},
|
||||
],
|
||||
[
|
||||
"span",
|
||||
"SPAN",
|
||||
{
|
||||
id: randomUUID(),
|
||||
type: "span-create",
|
||||
timestamp: new Date().toISOString(),
|
||||
body: {
|
||||
id: randomUUID(),
|
||||
traceId: randomUUID(),
|
||||
startTime: new Date().toISOString(),
|
||||
},
|
||||
},
|
||||
],
|
||||
[
|
||||
"span-complex-io",
|
||||
"SPAN",
|
||||
{
|
||||
id: randomUUID(),
|
||||
type: "span-create",
|
||||
timestamp: new Date().toISOString(),
|
||||
body: {
|
||||
id: randomUUID(),
|
||||
traceId: randomUUID(),
|
||||
startTime: new Date().toISOString(),
|
||||
input: ["hello", { world: "world" }, [1, 2, 3]],
|
||||
output: ["hello", { world: [2, 3, "test"] }, [1, 2, 3]],
|
||||
},
|
||||
},
|
||||
],
|
||||
])(
|
||||
"should create observations via the ingestion API (%s)",
|
||||
async (_name: string, type: string, entity: any) => {
|
||||
const response = await makeAPICall("POST", "/api/public/ingestion", {
|
||||
batch: [entity],
|
||||
});
|
||||
|
||||
expect(response.status).toBe(207);
|
||||
|
||||
await waitForExpect(async () => {
|
||||
const observation = await getObservationById(
|
||||
entity.body.id,
|
||||
projectId,
|
||||
true,
|
||||
);
|
||||
expect(observation).toBeDefined();
|
||||
expect(observation!.id).toBe(entity.body.id);
|
||||
expect(observation!.projectId).toBe(projectId);
|
||||
expect(observation!.metadata).toEqual(entity.body?.metadata ?? {});
|
||||
expect(observation!.input).toEqual(entity.body?.input ?? null);
|
||||
expect(observation!.output).toEqual(entity.body?.output ?? null);
|
||||
expect(observation!.type).toBe(type);
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
it.each([
|
||||
[
|
||||
"plain",
|
||||
{
|
||||
id: randomUUID(),
|
||||
type: "score-create",
|
||||
timestamp: new Date().toISOString(),
|
||||
body: {
|
||||
id: randomUUID(),
|
||||
name: "score-name",
|
||||
traceId: randomUUID(),
|
||||
value: 100.5,
|
||||
observationId: randomUUID(),
|
||||
},
|
||||
},
|
||||
],
|
||||
])(
|
||||
"should create scores via the ingestion API (%s)",
|
||||
async (_name: string, entity: any) => {
|
||||
const response = await makeAPICall("POST", "/api/public/ingestion", {
|
||||
batch: [entity],
|
||||
});
|
||||
|
||||
expect(response.status).toBe(207);
|
||||
|
||||
await waitForExpect(async () => {
|
||||
const score = await getScoreById(projectId, entity.body.id);
|
||||
expect(score).toBeDefined();
|
||||
expect(score!.id).toBe(entity.body.id);
|
||||
expect(score!.projectId).toBe(projectId);
|
||||
expect(score!.value).toEqual(100.5);
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
it("should fail for long trace name", async () => {
|
||||
const traceId = v4();
|
||||
|
||||
const baseString =
|
||||
"Lorem ipsum dolor sit amet, consectetur adipiscing elit. ";
|
||||
const repeatCount = Math.ceil(1500 / baseString.length);
|
||||
const name = baseString.repeat(repeatCount);
|
||||
|
||||
const response = await makeAPICall("POST", "/api/public/ingestion", {
|
||||
batch: [
|
||||
{
|
||||
id: v4(),
|
||||
type: "trace-create",
|
||||
timestamp: new Date().toISOString(),
|
||||
body: {
|
||||
id: traceId,
|
||||
name,
|
||||
userId: "user-1",
|
||||
metadata: { key: "value" },
|
||||
release: "1.0.0",
|
||||
version: "2.0.0",
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
expect(response.status).toBe(207);
|
||||
expect("errors" in response.body).toBe(true);
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
|
||||
expect(response.body.errors.length).toBe(1);
|
||||
expect(response.body.errors[0].message).toBe("Invalid request data");
|
||||
});
|
||||
});
|
||||
@@ -92,6 +92,139 @@ describe("trpc.sessions", () => {
|
||||
expect(uiSessions[0].user_ids).toEqual(["user1"]);
|
||||
});
|
||||
|
||||
it("should GET sessions ordered by total cost", async () => {
|
||||
const { projectId } = await createOrgProjectAndApiKey();
|
||||
const sessionId1 = v4();
|
||||
const sessionId2 = v4();
|
||||
|
||||
await prisma.traceSession.createMany({
|
||||
data: [
|
||||
{
|
||||
id: sessionId1,
|
||||
projectId: projectId,
|
||||
},
|
||||
{
|
||||
id: sessionId2,
|
||||
projectId: projectId,
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
const traces = [
|
||||
createTrace({
|
||||
session_id: sessionId1,
|
||||
project_id: projectId,
|
||||
}),
|
||||
createTrace({
|
||||
session_id: sessionId2,
|
||||
project_id: projectId,
|
||||
}),
|
||||
];
|
||||
|
||||
const observations = [
|
||||
createObservation({
|
||||
trace_id: traces[0].id,
|
||||
project_id: projectId,
|
||||
cost_details: {
|
||||
input: 0.1,
|
||||
output: 0.2,
|
||||
total: 0.3,
|
||||
},
|
||||
total_cost: 0.3,
|
||||
}),
|
||||
createObservation({
|
||||
trace_id: traces[1].id,
|
||||
project_id: projectId,
|
||||
cost_details: {
|
||||
input: 0.3,
|
||||
output: 0.4,
|
||||
total: 0.7,
|
||||
},
|
||||
total_cost: 0.7,
|
||||
}),
|
||||
];
|
||||
|
||||
await createTracesCh(traces);
|
||||
await createObservationsCh(observations);
|
||||
|
||||
const uiSessions = await getSessionsTable({
|
||||
projectId: projectId,
|
||||
filter: [],
|
||||
orderBy: {
|
||||
column: "totalCost",
|
||||
order: "DESC" as const,
|
||||
},
|
||||
limit: 10000,
|
||||
page: 0,
|
||||
});
|
||||
|
||||
expect(uiSessions.length).toBe(2);
|
||||
expect(uiSessions[0].session_id).toBe(sessionId2);
|
||||
expect(uiSessions[1].session_id).toBe(sessionId1);
|
||||
});
|
||||
|
||||
it("should GET sessions ordered by duration", async () => {
|
||||
const { projectId } = await createOrgProjectAndApiKey();
|
||||
const sessionId1 = v4();
|
||||
const sessionId2 = v4();
|
||||
|
||||
await prisma.traceSession.createMany({
|
||||
data: [
|
||||
{
|
||||
id: sessionId1,
|
||||
projectId: projectId,
|
||||
},
|
||||
{
|
||||
id: sessionId2,
|
||||
projectId: projectId,
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
const traces = [
|
||||
createTrace({
|
||||
session_id: sessionId1,
|
||||
project_id: projectId,
|
||||
}),
|
||||
createTrace({
|
||||
session_id: sessionId2,
|
||||
project_id: projectId,
|
||||
}),
|
||||
];
|
||||
const observations = [
|
||||
createObservation({
|
||||
trace_id: traces[0].id,
|
||||
project_id: projectId,
|
||||
start_time: new Date("2024-01-01T00:00:00Z").getTime(),
|
||||
end_time: new Date("2024-01-01T00:00:10Z").getTime(), // 10 second duration
|
||||
}),
|
||||
createObservation({
|
||||
trace_id: traces[1].id,
|
||||
project_id: projectId,
|
||||
start_time: new Date("2024-01-01T00:00:00Z").getTime(),
|
||||
end_time: new Date("2024-01-01T00:00:20Z").getTime(), // 20 second duration
|
||||
}),
|
||||
];
|
||||
|
||||
await createTracesCh(traces);
|
||||
await createObservationsCh(observations);
|
||||
|
||||
const uiSessions = await getSessionsTable({
|
||||
projectId: projectId,
|
||||
filter: [],
|
||||
orderBy: {
|
||||
column: "sessionDuration",
|
||||
order: "DESC" as const,
|
||||
},
|
||||
limit: 10000,
|
||||
page: 0,
|
||||
});
|
||||
|
||||
expect(uiSessions.length).toBe(2);
|
||||
expect(uiSessions[0].session_id).toBe(sessionId2);
|
||||
expect(uiSessions[1].session_id).toBe(sessionId1);
|
||||
});
|
||||
|
||||
it("should GET metrics for a list of sessions", async () => {
|
||||
const { projectId } = await createOrgProjectAndApiKey();
|
||||
const sessionId1 = v4();
|
||||
|
||||
@@ -1,297 +0,0 @@
|
||||
/** @jest-environment node */
|
||||
|
||||
import { prisma } from "@langfuse/shared/src/db";
|
||||
import {
|
||||
makeZodVerifiedAPICall,
|
||||
pruneDatabase,
|
||||
} from "@/src/__tests__/test-utils";
|
||||
import { v4 as uuidv4 } from "uuid";
|
||||
import { PostTracesV1Response } from "@/src/features/public-api/types/traces";
|
||||
import { PostEventsV1Response } from "@/src/features/public-api/types/events";
|
||||
|
||||
describe("/api/public/events API Endpoint", () => {
|
||||
beforeEach(async () => await pruneDatabase());
|
||||
|
||||
it("should create event after trace", async () => {
|
||||
await pruneDatabase();
|
||||
|
||||
const traceId = uuidv4();
|
||||
|
||||
await makeZodVerifiedAPICall(
|
||||
PostTracesV1Response,
|
||||
"POST",
|
||||
"/api/public/traces",
|
||||
{
|
||||
id: traceId,
|
||||
name: "trace-name",
|
||||
userId: "user-1",
|
||||
projectId: "7a88fb47-b4e2-43b8-a06c-a5ce950dc53a",
|
||||
metadata: { key: "value" },
|
||||
release: "1.0.0",
|
||||
version: "2.0.0",
|
||||
},
|
||||
);
|
||||
|
||||
const dbTrace = await prisma.trace.findMany({
|
||||
where: {
|
||||
id: traceId,
|
||||
},
|
||||
});
|
||||
|
||||
expect(dbTrace.length).toBeGreaterThan(0);
|
||||
expect(dbTrace[0]?.id).toBe(traceId);
|
||||
|
||||
const eventId = uuidv4();
|
||||
const createEvent = await makeZodVerifiedAPICall(
|
||||
PostEventsV1Response,
|
||||
"POST",
|
||||
"/api/public/events",
|
||||
{
|
||||
id: eventId,
|
||||
traceId: traceId,
|
||||
name: "event-name",
|
||||
startTime: "2021-01-01T00:00:00.000Z",
|
||||
input: { input: "value" },
|
||||
output: { output: "value" },
|
||||
metadata: { meta: "value" },
|
||||
version: "2.0.0",
|
||||
},
|
||||
);
|
||||
|
||||
expect(createEvent.status).toBe(200);
|
||||
const dbEvent = await prisma.observation.findUnique({
|
||||
where: {
|
||||
id: eventId,
|
||||
},
|
||||
});
|
||||
|
||||
expect(dbEvent?.id).toBe(eventId);
|
||||
expect(dbEvent?.traceId).toBe(traceId);
|
||||
expect(dbEvent?.name).toBe("event-name");
|
||||
expect(dbEvent?.startTime).toEqual(new Date("2021-01-01T00:00:00.000Z"));
|
||||
expect(dbEvent?.input).toEqual({ input: "value" });
|
||||
expect(dbEvent?.output).toEqual({ output: "value" });
|
||||
expect(dbEvent?.metadata).toEqual({ meta: "value" });
|
||||
expect(dbEvent?.version).toBe("2.0.0");
|
||||
});
|
||||
|
||||
it("should create event before trace", async () => {
|
||||
await pruneDatabase();
|
||||
|
||||
const traceId = uuidv4();
|
||||
const eventId = uuidv4();
|
||||
|
||||
const createEvent = await makeZodVerifiedAPICall(
|
||||
PostEventsV1Response,
|
||||
"POST",
|
||||
"/api/public/events",
|
||||
{
|
||||
id: eventId,
|
||||
traceId: traceId,
|
||||
name: "event-name",
|
||||
startTime: "2021-01-01T00:00:00.000Z",
|
||||
input: { input: "value" },
|
||||
output: { output: "value" },
|
||||
metadata: { meta: "value" },
|
||||
version: "2.0.0",
|
||||
},
|
||||
);
|
||||
|
||||
expect(createEvent.status).toBe(200);
|
||||
const dbEvent = await prisma.observation.findUnique({
|
||||
where: {
|
||||
id: eventId,
|
||||
},
|
||||
});
|
||||
|
||||
expect(dbEvent?.id).toBe(eventId);
|
||||
expect(dbEvent?.traceId).toBe(traceId);
|
||||
expect(dbEvent?.name).toBe("event-name");
|
||||
expect(dbEvent?.startTime).toEqual(new Date("2021-01-01T00:00:00.000Z"));
|
||||
expect(dbEvent?.input).toEqual({ input: "value" });
|
||||
expect(dbEvent?.output).toEqual({ output: "value" });
|
||||
expect(dbEvent?.metadata).toEqual({ meta: "value" });
|
||||
expect(dbEvent?.version).toBe("2.0.0");
|
||||
|
||||
await makeZodVerifiedAPICall(
|
||||
PostTracesV1Response,
|
||||
"POST",
|
||||
"/api/public/traces",
|
||||
{
|
||||
id: traceId,
|
||||
name: "trace-name",
|
||||
userId: "user-1",
|
||||
projectId: "7a88fb47-b4e2-43b8-a06c-a5ce950dc53a",
|
||||
metadata: { key: "value" },
|
||||
release: "1.0.0",
|
||||
version: "2.0.0",
|
||||
},
|
||||
);
|
||||
|
||||
const dbTrace = await prisma.trace.findMany({
|
||||
where: {
|
||||
id: traceId,
|
||||
},
|
||||
});
|
||||
|
||||
expect(dbTrace.length).toBeGreaterThan(0);
|
||||
expect(dbTrace[0]?.id).toBe(traceId);
|
||||
});
|
||||
|
||||
it("should create trace and ignore externalId and create event afterwards", async () => {
|
||||
await pruneDatabase();
|
||||
|
||||
const traceId = uuidv4();
|
||||
|
||||
const response = await makeZodVerifiedAPICall(
|
||||
PostTracesV1Response,
|
||||
"POST",
|
||||
"/api/public/traces",
|
||||
{
|
||||
externalId: traceId,
|
||||
name: "trace-name",
|
||||
userId: "user-1",
|
||||
projectId: "7a88fb47-b4e2-43b8-a06c-a5ce950dc53a",
|
||||
metadata: { key: "value" },
|
||||
release: "1.0.0",
|
||||
version: "2.0.0",
|
||||
},
|
||||
);
|
||||
|
||||
expect(response.status).toBe(200);
|
||||
|
||||
const dbTrace = await prisma.trace.findMany({
|
||||
where: {
|
||||
name: "trace-name",
|
||||
},
|
||||
});
|
||||
|
||||
expect(dbTrace.length).toBeGreaterThan(0);
|
||||
expect(dbTrace[0]?.externalId).toBeNull();
|
||||
expect(dbTrace[0]?.id).not.toBe(traceId);
|
||||
|
||||
const eventId = uuidv4();
|
||||
const createEvent = await makeZodVerifiedAPICall(
|
||||
PostEventsV1Response,
|
||||
"POST",
|
||||
"/api/public/events",
|
||||
{
|
||||
id: eventId,
|
||||
traceIdType: "EXTERNAL",
|
||||
traceId: dbTrace[0]?.id,
|
||||
name: "event-name",
|
||||
startTime: "2021-01-01T00:00:00.000Z",
|
||||
input: { input: "value" },
|
||||
output: { output: "value" },
|
||||
metadata: { meta: "value" },
|
||||
version: "2.0.0",
|
||||
},
|
||||
);
|
||||
|
||||
expect(createEvent.status).toBe(200);
|
||||
const dbEvent = await prisma.observation.findUnique({
|
||||
where: {
|
||||
id: eventId,
|
||||
},
|
||||
});
|
||||
|
||||
expect(dbEvent?.id).toBe(eventId);
|
||||
expect(dbEvent?.traceId).toBe(dbTrace[0]?.id);
|
||||
expect(dbEvent?.name).toBe("event-name");
|
||||
expect(dbEvent?.startTime).toEqual(new Date("2021-01-01T00:00:00.000Z"));
|
||||
expect(dbEvent?.input).toEqual({ input: "value" });
|
||||
expect(dbEvent?.output).toEqual({ output: "value" });
|
||||
expect(dbEvent?.metadata).toEqual({ meta: "value" });
|
||||
expect(dbEvent?.version).toBe("2.0.0");
|
||||
});
|
||||
|
||||
it("should create trace when creating span without existing trace", async () => {
|
||||
const eventName = uuidv4();
|
||||
|
||||
const spanId = uuidv4();
|
||||
const createEvent = await makeZodVerifiedAPICall(
|
||||
PostEventsV1Response,
|
||||
"POST",
|
||||
"/api/public/events",
|
||||
{
|
||||
id: spanId,
|
||||
name: eventName,
|
||||
startTime: "2021-01-01T00:00:00.000Z",
|
||||
endTime: "2021-01-01T00:00:00.000Z",
|
||||
input: { input: "value" },
|
||||
output: { output: "value" },
|
||||
metadata: { meta: "value" },
|
||||
version: "2.0.0",
|
||||
},
|
||||
);
|
||||
|
||||
const dbTrace = await prisma.trace.findMany({
|
||||
where: {
|
||||
name: eventName,
|
||||
},
|
||||
});
|
||||
|
||||
expect(dbTrace.length).toBe(1);
|
||||
expect(dbTrace[0]?.name).toBe(eventName);
|
||||
|
||||
expect(createEvent.status).toBe(200);
|
||||
const dbEvent = await prisma.observation.findUnique({
|
||||
where: {
|
||||
id: spanId,
|
||||
},
|
||||
});
|
||||
|
||||
expect(dbEvent?.id).toBe(spanId);
|
||||
expect(dbEvent?.traceId).toBe(dbTrace[0]?.id);
|
||||
expect(dbEvent?.name).toBe(eventName);
|
||||
expect(dbEvent?.startTime).toEqual(new Date("2021-01-01T00:00:00.000Z"));
|
||||
expect(dbEvent?.input).toEqual({ input: "value" });
|
||||
expect(dbEvent?.output).toEqual({ output: "value" });
|
||||
expect(dbEvent?.metadata).toEqual({ meta: "value" });
|
||||
expect(dbEvent?.version).toBe("2.0.0");
|
||||
});
|
||||
|
||||
it("should create event when creating generation without existing trace without traceId", async () => {
|
||||
const generationName = uuidv4();
|
||||
|
||||
const spanId = uuidv4();
|
||||
const createSpan = await makeZodVerifiedAPICall(
|
||||
PostEventsV1Response,
|
||||
"POST",
|
||||
"/api/public/events",
|
||||
{
|
||||
id: spanId,
|
||||
name: generationName,
|
||||
startTime: "2021-01-01T00:00:00.000Z",
|
||||
input: { key: "value" },
|
||||
metadata: { key: "value" },
|
||||
version: "2.0.0",
|
||||
},
|
||||
);
|
||||
|
||||
const dbEvent = await prisma.observation.findFirstOrThrow({
|
||||
where: {
|
||||
name: generationName,
|
||||
},
|
||||
});
|
||||
|
||||
const dbTrace = await prisma.trace.findMany({
|
||||
where: {
|
||||
id: dbEvent.traceId!,
|
||||
},
|
||||
});
|
||||
|
||||
expect(dbTrace.length).toBe(1);
|
||||
expect(dbTrace[0]?.name).toBe(generationName);
|
||||
|
||||
expect(createSpan.status).toBe(200);
|
||||
|
||||
expect(dbEvent.id).toBe(spanId);
|
||||
expect(dbEvent.traceId).toBe(dbTrace[0]?.id);
|
||||
expect(dbEvent.name).toBe(generationName);
|
||||
expect(dbEvent.startTime).toEqual(new Date("2021-01-01T00:00:00.000Z"));
|
||||
expect(dbEvent.input).toEqual({ key: "value" });
|
||||
expect(dbEvent.metadata).toEqual({ key: "value" });
|
||||
expect(dbEvent.version).toBe("2.0.0");
|
||||
});
|
||||
});
|
||||
@@ -1,643 +0,0 @@
|
||||
/** @jest-environment node */
|
||||
|
||||
import { v4 as uuidv4 } from "uuid";
|
||||
|
||||
import {
|
||||
makeAPICall,
|
||||
makeZodVerifiedAPICall,
|
||||
pruneDatabase,
|
||||
} from "@/src/__tests__/test-utils";
|
||||
import { ModelUsageUnit } from "@langfuse/shared";
|
||||
import { prisma } from "@langfuse/shared/src/db";
|
||||
import { PostTracesV1Response } from "@/src/features/public-api/types/traces";
|
||||
import {
|
||||
PatchGenerationsV1Response,
|
||||
PostGenerationsV1Body,
|
||||
PostGenerationsV1Response,
|
||||
} from "@/src/features/public-api/types/generations";
|
||||
|
||||
describe("/api/public/generations API Endpoint", () => {
|
||||
beforeEach(async () => await pruneDatabase());
|
||||
|
||||
[
|
||||
{
|
||||
usage: {
|
||||
input: 100,
|
||||
output: 200,
|
||||
total: 100,
|
||||
unit: ModelUsageUnit.Characters,
|
||||
},
|
||||
expectedUnit: ModelUsageUnit.Characters,
|
||||
expectedPromptTokens: 100,
|
||||
expectedCompletionTokens: 200,
|
||||
expectedTotalTokens: 100,
|
||||
},
|
||||
{
|
||||
usage: {
|
||||
total: 100,
|
||||
unit: ModelUsageUnit.Characters,
|
||||
},
|
||||
expectedUnit: ModelUsageUnit.Characters,
|
||||
expectedPromptTokens: 0,
|
||||
expectedCompletionTokens: 0,
|
||||
expectedTotalTokens: 100,
|
||||
},
|
||||
{
|
||||
usage: {
|
||||
total: 100,
|
||||
},
|
||||
expectedUnit: null,
|
||||
expectedPromptTokens: 0,
|
||||
expectedCompletionTokens: 0,
|
||||
expectedTotalTokens: 100,
|
||||
},
|
||||
{
|
||||
usage: {
|
||||
promptTokens: 100,
|
||||
completionTokens: 200,
|
||||
totalTokens: 100,
|
||||
},
|
||||
expectedPromptTokens: 100,
|
||||
expectedCompletionTokens: 200,
|
||||
expectedTotalTokens: 100,
|
||||
expectedUnit: ModelUsageUnit.Tokens,
|
||||
},
|
||||
{
|
||||
usage: {
|
||||
totalTokens: 100,
|
||||
},
|
||||
expectedPromptTokens: 0,
|
||||
expectedCompletionTokens: 0,
|
||||
expectedTotalTokens: 100,
|
||||
expectedUnit: ModelUsageUnit.Tokens,
|
||||
},
|
||||
{
|
||||
usage: undefined,
|
||||
expectedPromptTokens: 0,
|
||||
expectedCompletionTokens: 0,
|
||||
expectedTotalTokens: 0,
|
||||
expectedUnit: null,
|
||||
},
|
||||
{
|
||||
usage: null,
|
||||
expectedPromptTokens: 0,
|
||||
expectedCompletionTokens: 0,
|
||||
expectedTotalTokens: 0,
|
||||
expectedUnit: null,
|
||||
},
|
||||
{
|
||||
usage: {},
|
||||
expectedPromptTokens: 0,
|
||||
expectedCompletionTokens: 0,
|
||||
expectedTotalTokens: 0,
|
||||
expectedUnit: null,
|
||||
},
|
||||
].forEach((testConfig) => {
|
||||
it(`should create generation after trace 1 ${JSON.stringify(
|
||||
testConfig,
|
||||
)}`, async () => {
|
||||
await pruneDatabase();
|
||||
|
||||
const traceId = uuidv4();
|
||||
|
||||
await makeZodVerifiedAPICall(
|
||||
PostTracesV1Response,
|
||||
"POST",
|
||||
"/api/public/traces",
|
||||
{
|
||||
id: traceId,
|
||||
name: "trace-name",
|
||||
userId: "user-1",
|
||||
projectId: "7a88fb47-b4e2-43b8-a06c-a5ce950dc53a",
|
||||
metadata: { key: "value" },
|
||||
release: "1.0.0",
|
||||
version: "2.0.0",
|
||||
},
|
||||
);
|
||||
|
||||
const dbTrace = await prisma.trace.findMany({
|
||||
where: {
|
||||
id: traceId,
|
||||
},
|
||||
});
|
||||
|
||||
expect(dbTrace.length).toBeGreaterThan(0);
|
||||
expect(dbTrace[0]?.id).toBe(traceId);
|
||||
|
||||
const generationId = uuidv4();
|
||||
const createGeneration = await makeZodVerifiedAPICall(
|
||||
PostGenerationsV1Response,
|
||||
"POST",
|
||||
"/api/public/generations",
|
||||
{
|
||||
id: generationId,
|
||||
traceId: traceId,
|
||||
name: "generation-name",
|
||||
startTime: "2021-01-01T00:00:00.000Z",
|
||||
endTime: "2021-01-01T00:00:00.000Z",
|
||||
model: "model-name",
|
||||
modelParameters: { key: "value" },
|
||||
prompt: { key: "value" },
|
||||
metadata: { key: "value" },
|
||||
version: "2.0.0",
|
||||
usage: testConfig.usage,
|
||||
},
|
||||
);
|
||||
|
||||
expect(createGeneration.status).toBe(200);
|
||||
const dbGeneration = await prisma.observation.findUnique({
|
||||
where: {
|
||||
id: generationId,
|
||||
},
|
||||
});
|
||||
|
||||
expect(dbGeneration?.id).toBe(generationId);
|
||||
expect(dbGeneration?.traceId).toBe(traceId);
|
||||
expect(dbGeneration?.name).toBe("generation-name");
|
||||
expect(dbGeneration?.startTime).toEqual(
|
||||
new Date("2021-01-01T00:00:00.000Z"),
|
||||
);
|
||||
expect(dbGeneration?.endTime).toEqual(
|
||||
new Date("2021-01-01T00:00:00.000Z"),
|
||||
);
|
||||
expect(dbGeneration?.model).toBe("model-name");
|
||||
expect(dbGeneration?.modelParameters).toEqual({ key: "value" });
|
||||
expect(dbGeneration?.input).toEqual({ key: "value" });
|
||||
expect(dbGeneration?.metadata).toEqual({ key: "value" });
|
||||
expect(dbGeneration?.version).toBe("2.0.0");
|
||||
expect(dbGeneration?.unit).toBe(testConfig.expectedUnit);
|
||||
expect(dbGeneration?.promptTokens).toBe(testConfig.expectedPromptTokens);
|
||||
expect(dbGeneration?.completionTokens).toBe(
|
||||
testConfig.expectedCompletionTokens,
|
||||
);
|
||||
expect(dbGeneration?.totalTokens).toBe(testConfig.expectedTotalTokens);
|
||||
});
|
||||
});
|
||||
|
||||
it("should create generation before trace", async () => {
|
||||
await pruneDatabase();
|
||||
|
||||
const traceId = uuidv4();
|
||||
|
||||
const generationId = uuidv4();
|
||||
const createGeneration = await makeZodVerifiedAPICall(
|
||||
PostGenerationsV1Body,
|
||||
"POST",
|
||||
"/api/public/generations",
|
||||
{
|
||||
id: generationId,
|
||||
traceId: traceId,
|
||||
name: "generation-name",
|
||||
startTime: "2021-01-01T00:00:00.000Z",
|
||||
endTime: "2021-01-01T00:00:00.000Z",
|
||||
model: "model-name",
|
||||
modelParameters: { key: "value" },
|
||||
prompt: { key: "value" },
|
||||
metadata: { key: "value" },
|
||||
version: "2.0.0",
|
||||
},
|
||||
);
|
||||
|
||||
expect(createGeneration.status).toBe(200);
|
||||
const dbGeneration = await prisma.observation.findUnique({
|
||||
where: {
|
||||
id: generationId,
|
||||
},
|
||||
});
|
||||
|
||||
expect(dbGeneration?.id).toBe(generationId);
|
||||
expect(dbGeneration?.traceId).toBe(traceId);
|
||||
expect(dbGeneration?.name).toBe("generation-name");
|
||||
expect(dbGeneration?.startTime).toEqual(
|
||||
new Date("2021-01-01T00:00:00.000Z"),
|
||||
);
|
||||
expect(dbGeneration?.endTime).toEqual(new Date("2021-01-01T00:00:00.000Z"));
|
||||
expect(dbGeneration?.model).toBe("model-name");
|
||||
expect(dbGeneration?.modelParameters).toEqual({ key: "value" });
|
||||
expect(dbGeneration?.input).toEqual({ key: "value" });
|
||||
expect(dbGeneration?.metadata).toEqual({ key: "value" });
|
||||
expect(dbGeneration?.version).toBe("2.0.0");
|
||||
|
||||
await makeZodVerifiedAPICall(
|
||||
PostTracesV1Response,
|
||||
"POST",
|
||||
"/api/public/traces",
|
||||
{
|
||||
id: traceId,
|
||||
name: "trace-name",
|
||||
userId: "user-1",
|
||||
projectId: "7a88fb47-b4e2-43b8-a06c-a5ce950dc53a",
|
||||
metadata: { key: "value" },
|
||||
release: "1.0.0",
|
||||
version: "2.0.0",
|
||||
},
|
||||
);
|
||||
|
||||
const dbTrace = await prisma.trace.findMany({
|
||||
where: {
|
||||
id: traceId,
|
||||
},
|
||||
});
|
||||
|
||||
expect(dbTrace.length).toBeGreaterThan(0);
|
||||
expect(dbTrace[0]?.id).toBe(traceId);
|
||||
});
|
||||
|
||||
it("should create generation after trace ignoring externalId", async () => {
|
||||
const traceId = uuidv4();
|
||||
|
||||
const response = await makeZodVerifiedAPICall(
|
||||
PostTracesV1Response,
|
||||
"POST",
|
||||
"/api/public/traces",
|
||||
{
|
||||
externalId: traceId,
|
||||
name: "trace-name",
|
||||
userId: "user-1",
|
||||
projectId: "7a88fb47-b4e2-43b8-a06c-a5ce950dc53a",
|
||||
metadata: { key: "value" },
|
||||
release: "1.0.0",
|
||||
version: "2.0.0",
|
||||
},
|
||||
);
|
||||
|
||||
expect(response.status).toBe(200);
|
||||
|
||||
const dbTrace = await prisma.trace.findMany({
|
||||
where: {
|
||||
name: "trace-name",
|
||||
},
|
||||
});
|
||||
|
||||
expect(dbTrace.length).toBeGreaterThan(0);
|
||||
expect(dbTrace[0]?.externalId).toBeNull();
|
||||
expect(dbTrace[0]?.id).not.toBe(traceId);
|
||||
|
||||
const generationId = uuidv4();
|
||||
const createGeneration = await makeZodVerifiedAPICall(
|
||||
PostGenerationsV1Body,
|
||||
"POST",
|
||||
"/api/public/generations",
|
||||
{
|
||||
id: generationId,
|
||||
traceIdType: "EXTERNAL",
|
||||
traceId: traceId,
|
||||
name: "generation-name",
|
||||
startTime: "2021-01-01T00:00:00.000Z",
|
||||
endTime: "2021-01-01T00:00:00.000Z",
|
||||
model: "model-name",
|
||||
modelParameters: { key: "value" },
|
||||
prompt: { key: "value" },
|
||||
metadata: { key: "value" },
|
||||
version: "2.0.0",
|
||||
},
|
||||
);
|
||||
|
||||
expect(createGeneration.status).toBe(200);
|
||||
const dbGeneration = await prisma.observation.findUnique({
|
||||
where: {
|
||||
id: generationId,
|
||||
},
|
||||
});
|
||||
|
||||
expect(dbGeneration?.id).toBe(generationId);
|
||||
expect(dbGeneration?.traceId).toBe(traceId);
|
||||
expect(dbGeneration?.name).toBe("generation-name");
|
||||
expect(dbGeneration?.startTime).toEqual(
|
||||
new Date("2021-01-01T00:00:00.000Z"),
|
||||
);
|
||||
expect(dbGeneration?.endTime).toEqual(new Date("2021-01-01T00:00:00.000Z"));
|
||||
expect(dbGeneration?.model).toBe("model-name");
|
||||
expect(dbGeneration?.modelParameters).toEqual({ key: "value" });
|
||||
expect(dbGeneration?.input).toEqual({ key: "value" });
|
||||
expect(dbGeneration?.metadata).toEqual({ key: "value" });
|
||||
expect(dbGeneration?.version).toBe("2.0.0");
|
||||
});
|
||||
|
||||
it("should create trace when creating generation without existing trace", async () => {
|
||||
const generationName = uuidv4();
|
||||
|
||||
const generationId = uuidv4();
|
||||
const createGeneration = await makeZodVerifiedAPICall(
|
||||
PostGenerationsV1Body,
|
||||
"POST",
|
||||
"/api/public/generations",
|
||||
{
|
||||
id: generationId,
|
||||
name: generationName,
|
||||
startTime: "2021-01-01T00:00:00.000Z",
|
||||
endTime: "2021-01-01T00:00:00.000Z",
|
||||
model: "model-name",
|
||||
modelParameters: { key: "value" },
|
||||
prompt: { key: "value" },
|
||||
metadata: { key: "value" },
|
||||
version: "2.0.0",
|
||||
},
|
||||
);
|
||||
|
||||
const dbTrace = await prisma.trace.findMany({
|
||||
where: {
|
||||
name: generationName,
|
||||
},
|
||||
});
|
||||
|
||||
expect(dbTrace.length).toBe(1);
|
||||
expect(dbTrace[0]?.name).toBe(generationName);
|
||||
|
||||
expect(createGeneration.status).toBe(200);
|
||||
const dbGeneration = await prisma.observation.findUnique({
|
||||
where: {
|
||||
id: generationId,
|
||||
},
|
||||
});
|
||||
|
||||
expect(dbGeneration?.id).toBe(generationId);
|
||||
expect(dbGeneration?.traceId).toBe(dbTrace[0]?.id);
|
||||
expect(dbGeneration?.name).toBe(generationName);
|
||||
expect(dbGeneration?.startTime).toEqual(
|
||||
new Date("2021-01-01T00:00:00.000Z"),
|
||||
);
|
||||
expect(dbGeneration?.endTime).toEqual(new Date("2021-01-01T00:00:00.000Z"));
|
||||
expect(dbGeneration?.model).toBe("model-name");
|
||||
expect(dbGeneration?.modelParameters).toEqual({ key: "value" });
|
||||
expect(dbGeneration?.input).toEqual({ key: "value" });
|
||||
expect(dbGeneration?.metadata).toEqual({ key: "value" });
|
||||
expect(dbGeneration?.version).toBe("2.0.0");
|
||||
});
|
||||
|
||||
it("should create nested generations", async () => {
|
||||
const generationName = uuidv4();
|
||||
|
||||
const generationId = uuidv4();
|
||||
const createGeneration = await makeZodVerifiedAPICall(
|
||||
PostGenerationsV1Body,
|
||||
"POST",
|
||||
"/api/public/generations",
|
||||
{
|
||||
id: generationId,
|
||||
name: generationName,
|
||||
startTime: "2021-01-01T00:00:00.000Z",
|
||||
endTime: "2021-01-01T00:00:00.000Z",
|
||||
model: "model-name",
|
||||
modelParameters: { key: "value" },
|
||||
prompt: { key: "value" },
|
||||
metadata: { key: "value" },
|
||||
version: "2.0.0",
|
||||
},
|
||||
);
|
||||
|
||||
expect(createGeneration.status).toBe(200);
|
||||
const dbGeneration = await prisma.observation.findUnique({
|
||||
where: {
|
||||
id: generationId,
|
||||
},
|
||||
});
|
||||
|
||||
expect(dbGeneration?.id).toBe(generationId);
|
||||
|
||||
const generationId2 = uuidv4();
|
||||
const generationName2 = uuidv4();
|
||||
|
||||
const createGeneration2 = await makeZodVerifiedAPICall(
|
||||
PostGenerationsV1Body,
|
||||
"POST",
|
||||
"/api/public/generations",
|
||||
{
|
||||
id: generationId2,
|
||||
name: generationName2,
|
||||
startTime: "2021-01-01T00:00:00.000Z",
|
||||
endTime: "2021-01-01T00:00:00.000Z",
|
||||
model: "model-name",
|
||||
modelParameters: { key: "value" },
|
||||
prompt: { key: "value" },
|
||||
metadata: { key: "value" },
|
||||
version: "2.0.0",
|
||||
parentObservationId: generationId,
|
||||
},
|
||||
);
|
||||
expect(createGeneration2.status).toBe(200);
|
||||
|
||||
const dbGeneration2 = await prisma.observation.findUnique({
|
||||
where: {
|
||||
id: generationId2,
|
||||
},
|
||||
});
|
||||
|
||||
expect(dbGeneration2?.id).toBe(generationId2);
|
||||
expect(dbGeneration2?.parentObservationId).toBe(generationId);
|
||||
});
|
||||
|
||||
it("should not create trace when creating generation without existing trace with externalId", async () => {
|
||||
const generationName = uuidv4();
|
||||
|
||||
const generationId = uuidv4();
|
||||
const externalTraceId = uuidv4();
|
||||
const createGeneration = await makeZodVerifiedAPICall(
|
||||
PostGenerationsV1Body,
|
||||
"POST",
|
||||
"/api/public/generations",
|
||||
{
|
||||
id: generationId,
|
||||
traceIdType: "EXTERNAL",
|
||||
traceId: externalTraceId,
|
||||
name: generationName,
|
||||
startTime: "2021-01-01T00:00:00.000Z",
|
||||
endTime: "2021-01-01T00:00:00.000Z",
|
||||
model: "model-name",
|
||||
modelParameters: { key: "value" },
|
||||
prompt: { key: "value" },
|
||||
metadata: { key: "value" },
|
||||
version: "2.0.0",
|
||||
},
|
||||
);
|
||||
|
||||
expect(createGeneration.status).toBe(200);
|
||||
|
||||
const dbGeneration = await prisma.observation.findFirstOrThrow({
|
||||
where: {
|
||||
name: generationName,
|
||||
},
|
||||
});
|
||||
expect(dbGeneration.id).toBe(generationId);
|
||||
expect(dbGeneration.traceId).toBe(externalTraceId);
|
||||
|
||||
const dbTraces = await prisma.trace.findMany();
|
||||
expect(dbTraces.length).toBe(0);
|
||||
});
|
||||
|
||||
it("should create trace when creating generation without existing trace without traceId", async () => {
|
||||
const generationName = uuidv4();
|
||||
|
||||
const generationId = uuidv4();
|
||||
const createGeneration = await makeZodVerifiedAPICall(
|
||||
PostGenerationsV1Body,
|
||||
"POST",
|
||||
"/api/public/generations",
|
||||
{
|
||||
id: generationId,
|
||||
name: generationName,
|
||||
startTime: "2021-01-01T00:00:00.000Z",
|
||||
endTime: "2021-01-01T00:00:00.000Z",
|
||||
model: "model-name",
|
||||
modelParameters: { key: "value" },
|
||||
prompt: { key: "value" },
|
||||
metadata: { key: "value" },
|
||||
version: "2.0.0",
|
||||
},
|
||||
);
|
||||
|
||||
const dbGeneration = await prisma.observation.findFirstOrThrow({
|
||||
where: {
|
||||
name: generationName,
|
||||
},
|
||||
});
|
||||
|
||||
const dbTrace = await prisma.trace.findMany({
|
||||
where: {
|
||||
id: dbGeneration.traceId!,
|
||||
},
|
||||
});
|
||||
|
||||
expect(dbTrace.length).toBe(1);
|
||||
expect(dbTrace[0]?.name).toBe(generationName);
|
||||
|
||||
expect(createGeneration.status).toBe(200);
|
||||
|
||||
expect(dbGeneration.id).toBe(generationId);
|
||||
expect(dbGeneration.traceId).toBe(dbTrace[0]?.id);
|
||||
expect(dbGeneration.name).toBe(generationName);
|
||||
expect(dbGeneration.startTime).toEqual(
|
||||
new Date("2021-01-01T00:00:00.000Z"),
|
||||
);
|
||||
expect(dbGeneration.endTime).toEqual(new Date("2021-01-01T00:00:00.000Z"));
|
||||
expect(dbGeneration.model).toBe("model-name");
|
||||
expect(dbGeneration.modelParameters).toEqual({ key: "value" });
|
||||
expect(dbGeneration.input).toEqual({ key: "value" });
|
||||
expect(dbGeneration.metadata).toEqual({ key: "value" });
|
||||
expect(dbGeneration.version).toBe("2.0.0");
|
||||
});
|
||||
|
||||
it("should update generation", async () => {
|
||||
const generationName = uuidv4();
|
||||
|
||||
const generationId = uuidv4();
|
||||
const createGeneration = await makeZodVerifiedAPICall(
|
||||
PostGenerationsV1Body,
|
||||
"POST",
|
||||
"/api/public/generations",
|
||||
{
|
||||
id: generationId,
|
||||
name: generationName,
|
||||
startTime: "2021-01-01T00:00:00.000Z",
|
||||
endTime: "2021-01-01T00:00:00.000Z",
|
||||
model: "model-name",
|
||||
modelParameters: { key: "value" },
|
||||
prompt: { key: "value" },
|
||||
metadata: { key: "value" },
|
||||
version: "2.0.0",
|
||||
},
|
||||
);
|
||||
|
||||
expect(createGeneration.status).toBe(200);
|
||||
|
||||
const updateGeneration = await makeZodVerifiedAPICall(
|
||||
PatchGenerationsV1Response,
|
||||
"PATCH",
|
||||
"/api/public/generations",
|
||||
{
|
||||
generationId: generationId,
|
||||
completion: "this is a great gpt response",
|
||||
},
|
||||
);
|
||||
expect(updateGeneration.status).toBe(200);
|
||||
|
||||
const dbGeneration = await prisma.observation.findUnique({
|
||||
where: {
|
||||
id: generationId,
|
||||
},
|
||||
});
|
||||
|
||||
expect(dbGeneration?.id).toBe(generationId);
|
||||
expect(dbGeneration?.name).toBe(generationName);
|
||||
expect(dbGeneration?.startTime).toEqual(
|
||||
new Date("2021-01-01T00:00:00.000Z"),
|
||||
);
|
||||
expect(dbGeneration?.endTime).toEqual(new Date("2021-01-01T00:00:00.000Z"));
|
||||
expect(dbGeneration?.model).toBe("model-name");
|
||||
expect(dbGeneration?.modelParameters).toEqual({ key: "value" });
|
||||
expect(dbGeneration?.input).toEqual({ key: "value" });
|
||||
expect(dbGeneration?.output).toEqual("this is a great gpt response");
|
||||
expect(dbGeneration?.metadata).toEqual({ key: "value" });
|
||||
expect(dbGeneration?.version).toBe("2.0.0");
|
||||
});
|
||||
|
||||
it("should accept objects as i/o", async () => {
|
||||
const generationName = uuidv4();
|
||||
|
||||
const generationId = uuidv4();
|
||||
const createGeneration = await makeZodVerifiedAPICall(
|
||||
PostGenerationsV1Body,
|
||||
"POST",
|
||||
"/api/public/generations",
|
||||
{
|
||||
id: generationId,
|
||||
name: generationName,
|
||||
prompt: { key: "value" },
|
||||
completion: [
|
||||
{
|
||||
foo: "bar",
|
||||
},
|
||||
],
|
||||
metadata: [
|
||||
{
|
||||
tags: ["example tag", "second tag"],
|
||||
},
|
||||
],
|
||||
},
|
||||
);
|
||||
|
||||
expect(createGeneration.status).toBe(200);
|
||||
|
||||
const dbGeneration = await prisma.observation.findUnique({
|
||||
where: {
|
||||
id: generationId,
|
||||
},
|
||||
});
|
||||
|
||||
expect(dbGeneration?.id).toBe(generationId);
|
||||
expect(dbGeneration?.name).toBe(generationName);
|
||||
expect(dbGeneration?.input).toEqual({ key: "value" });
|
||||
expect(dbGeneration?.output).toEqual([
|
||||
{
|
||||
foo: "bar",
|
||||
},
|
||||
]);
|
||||
expect(dbGeneration?.metadata).toEqual([
|
||||
{
|
||||
tags: ["example tag", "second tag"],
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it("should not succeed update if generation does not exist", async () => {
|
||||
const generationId = uuidv4();
|
||||
|
||||
const updateGeneration = await makeAPICall(
|
||||
"PATCH",
|
||||
"/api/public/generations",
|
||||
{
|
||||
generationId: generationId,
|
||||
completion: "this is a great gpt response",
|
||||
},
|
||||
);
|
||||
expect(updateGeneration.status).toBe(404);
|
||||
|
||||
const dbGeneration = await prisma.observation.findUnique({
|
||||
where: {
|
||||
id: generationId,
|
||||
},
|
||||
});
|
||||
|
||||
expect(dbGeneration).toBeNull();
|
||||
});
|
||||
});
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,198 +0,0 @@
|
||||
import {
|
||||
makeZodVerifiedAPICall,
|
||||
pruneDatabase,
|
||||
} from "@/src/__tests__/test-utils";
|
||||
import { PostGenerationsV1Response } from "@/src/features/public-api/types/generations";
|
||||
import { GetMetricsDailyV1Response } from "@/src/features/public-api/types/metrics";
|
||||
import { PostTracesV1Response } from "@/src/features/public-api/types/traces";
|
||||
import { v4 as uuidv4 } from "uuid";
|
||||
|
||||
describe("/api/public/metrics/daily API Endpoint", () => {
|
||||
beforeEach(async () => await pruneDatabase());
|
||||
afterEach(async () => await pruneDatabase());
|
||||
|
||||
it("should handle daily metrics correctly", async () => {
|
||||
// Create traces with observations on different days
|
||||
const traceId1 = uuidv4();
|
||||
const traceId2 = uuidv4();
|
||||
await makeZodVerifiedAPICall(
|
||||
PostTracesV1Response,
|
||||
"POST",
|
||||
"/api/public/traces",
|
||||
{
|
||||
id: traceId1,
|
||||
timestamp: "2021-01-01T00:00:00.000Z",
|
||||
name: "trace-day-1",
|
||||
userId: "user-daily-metrics",
|
||||
projectId: "project-daily-metrics",
|
||||
},
|
||||
);
|
||||
await makeZodVerifiedAPICall(
|
||||
PostTracesV1Response,
|
||||
"POST",
|
||||
"/api/public/traces",
|
||||
{
|
||||
id: traceId2,
|
||||
timestamp: "2021-01-02T00:00:00.000Z",
|
||||
name: "trace-day-2",
|
||||
userId: "user-daily-metrics",
|
||||
projectId: "project-daily-metrics",
|
||||
},
|
||||
);
|
||||
|
||||
// Simulate observations with usage metrics on different days
|
||||
await makeZodVerifiedAPICall(
|
||||
PostGenerationsV1Response,
|
||||
"POST",
|
||||
"/api/public/generations",
|
||||
{
|
||||
traceId: traceId1,
|
||||
model: "modelA",
|
||||
usage: { input: 100, output: 200, total: 300 },
|
||||
startTime: "2021-01-01T00:00:00.000Z",
|
||||
endTime: "2021-01-01T00:01:00.000Z",
|
||||
},
|
||||
);
|
||||
await makeZodVerifiedAPICall(
|
||||
PostGenerationsV1Response,
|
||||
"POST",
|
||||
"/api/public/generations",
|
||||
{
|
||||
traceId: traceId2,
|
||||
model: "modelB",
|
||||
usage: { input: 333 },
|
||||
startTime: "2021-01-02T00:00:00.000Z",
|
||||
endTime: "2021-01-02T00:02:00.000Z",
|
||||
},
|
||||
);
|
||||
await makeZodVerifiedAPICall(
|
||||
PostGenerationsV1Response,
|
||||
"POST",
|
||||
"/api/public/generations",
|
||||
{
|
||||
traceId: traceId2,
|
||||
model: "modelC",
|
||||
usage: { input: 666, output: 777, totalCost: 1024.22 },
|
||||
startTime: "2021-01-02T00:00:00.000Z",
|
||||
endTime: "2021-01-02T00:04:00.000Z",
|
||||
},
|
||||
);
|
||||
await makeZodVerifiedAPICall(
|
||||
PostGenerationsV1Response,
|
||||
"POST",
|
||||
"/api/public/generations",
|
||||
{
|
||||
traceId: traceId2,
|
||||
usage: { output: 300 },
|
||||
startTime: "2021-01-02T00:00:00.000Z",
|
||||
endTime: "2021-01-02T00:04:00.000Z",
|
||||
},
|
||||
);
|
||||
|
||||
// Retrieve the daily metrics
|
||||
const dailyMetricsResponse = await makeZodVerifiedAPICall(
|
||||
GetMetricsDailyV1Response,
|
||||
"GET",
|
||||
`/api/public/metrics/daily`,
|
||||
);
|
||||
const dailyMetricsData = dailyMetricsResponse.body.data;
|
||||
|
||||
// Check if the daily metrics are calculated correctly
|
||||
expect(dailyMetricsData).toHaveLength(2); // Two days of data
|
||||
if (!dailyMetricsData[0])
|
||||
throw new Error("dailyMetricsData[0] is undefined");
|
||||
expect(dailyMetricsData[0].date).toBe("2021-01-02"); // Latest date first
|
||||
expect(dailyMetricsData[0].countTraces).toBe(1);
|
||||
expect(dailyMetricsData[0].totalCost).toEqual(1024.22);
|
||||
expect(dailyMetricsData[0].usage).toEqual([
|
||||
{
|
||||
model: "modelB",
|
||||
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,
|
||||
},
|
||||
{
|
||||
model: null,
|
||||
countObservations: 1,
|
||||
countTraces: 1,
|
||||
inputUsage: 0,
|
||||
outputUsage: 300,
|
||||
totalCost: 0,
|
||||
totalUsage: 300,
|
||||
},
|
||||
]);
|
||||
|
||||
if (!dailyMetricsData[1])
|
||||
throw new Error("dailyMetricsData[1] is undefined");
|
||||
expect(dailyMetricsData[1].date).toBe("2021-01-01");
|
||||
expect(dailyMetricsData[1].countTraces).toBe(1);
|
||||
expect(dailyMetricsData[1].totalCost).toEqual(0);
|
||||
expect(dailyMetricsData[1].usage).toEqual([
|
||||
{
|
||||
model: "modelA",
|
||||
inputUsage: 100,
|
||||
outputUsage: 200,
|
||||
totalUsage: 300,
|
||||
countObservations: 1,
|
||||
countTraces: 1,
|
||||
totalCost: 0,
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it("should handle daily metrics correctly when there is no data", async () => {
|
||||
// Retrieve the daily metrics
|
||||
const dailyMetricsResponse = await makeZodVerifiedAPICall(
|
||||
GetMetricsDailyV1Response,
|
||||
"GET",
|
||||
`/api/public/metrics/daily`,
|
||||
);
|
||||
const dailyMetricsData = dailyMetricsResponse.body.data;
|
||||
|
||||
// Check if the daily metrics are calculated correctly
|
||||
expect(dailyMetricsData).toHaveLength(0); // No data
|
||||
});
|
||||
|
||||
it("should handle daily metrics correctly when there is just a trace", async () => {
|
||||
const traceId1 = uuidv4();
|
||||
await makeZodVerifiedAPICall(
|
||||
PostTracesV1Response,
|
||||
"POST",
|
||||
"/api/public/traces",
|
||||
{
|
||||
id: traceId1,
|
||||
timestamp: "2021-01-01T00:00:00.000Z",
|
||||
name: "trace-day-1",
|
||||
userId: "user-daily-metrics",
|
||||
projectId: "project-daily-metrics",
|
||||
},
|
||||
);
|
||||
|
||||
// Retrieve the daily metrics
|
||||
const dailyMetricsResponse = await makeZodVerifiedAPICall(
|
||||
GetMetricsDailyV1Response,
|
||||
"GET",
|
||||
`/api/public/metrics/daily`,
|
||||
);
|
||||
const dailyMetricsData = dailyMetricsResponse.body.data;
|
||||
|
||||
// Check if the daily metrics are calculated correctly
|
||||
expect(dailyMetricsData).toHaveLength(1);
|
||||
expect(dailyMetricsData[0].date).toBe("2021-01-01");
|
||||
expect(dailyMetricsData[0].countTraces).toBe(1);
|
||||
expect(dailyMetricsData[0].totalCost).toEqual(0);
|
||||
expect(dailyMetricsData[0].usage).toEqual([]);
|
||||
});
|
||||
});
|
||||
@@ -9,6 +9,7 @@ import {
|
||||
PromptType,
|
||||
type LegacyValidatedPrompt,
|
||||
} from "@/src/features/prompts/server/utils/validation";
|
||||
import { getObservationById } from "@langfuse/shared/src/server";
|
||||
|
||||
describe("/api/public/prompts API Endpoint", () => {
|
||||
beforeEach(async () => await pruneDatabase());
|
||||
@@ -411,6 +412,7 @@ describe("/api/public/prompts API Endpoint", () => {
|
||||
const generationId = v4();
|
||||
|
||||
const promptId = uuidv4();
|
||||
const projectId = "7a88fb47-b4e2-43b8-a06c-a5ce950dc53a";
|
||||
|
||||
await prisma.prompt.create({
|
||||
data: {
|
||||
@@ -420,7 +422,7 @@ describe("/api/public/prompts API Endpoint", () => {
|
||||
labels: ["production"],
|
||||
version: 1,
|
||||
project: {
|
||||
connect: { id: "7a88fb47-b4e2-43b8-a06c-a5ce950dc53a" },
|
||||
connect: { id: projectId },
|
||||
},
|
||||
createdBy: "user-1",
|
||||
},
|
||||
@@ -459,11 +461,10 @@ describe("/api/public/prompts API Endpoint", () => {
|
||||
|
||||
expect(response.status).toBe(207);
|
||||
|
||||
const dbGeneration = await prisma.observation.findUnique({
|
||||
where: {
|
||||
id: generationId,
|
||||
},
|
||||
});
|
||||
// Delay to allow for async processing
|
||||
await new Promise((resolve) => setTimeout(resolve, 500));
|
||||
|
||||
const dbGeneration = await getObservationById(generationId, projectId);
|
||||
|
||||
expect(dbGeneration?.id).toBe(generationId);
|
||||
expect(dbGeneration?.promptId).toBe(promptId);
|
||||
|
||||
@@ -13,6 +13,7 @@ import {
|
||||
import { nanoid } from "ai";
|
||||
|
||||
import { type PromptsMetaResponse } from "@/src/features/prompts/server/actions/getPromptsMeta";
|
||||
import { getObservationById } from "@langfuse/shared/src/server";
|
||||
|
||||
const projectId = "7a88fb47-b4e2-43b8-a06c-a5ce950dc53a";
|
||||
const baseURI = "/api/public/v2/prompts";
|
||||
@@ -395,11 +396,10 @@ describe("/api/public/v2/prompts API Endpoint", () => {
|
||||
|
||||
expect(response.status).toBe(207);
|
||||
|
||||
const dbGeneration = await prisma.observation.findUnique({
|
||||
where: {
|
||||
id: generationId,
|
||||
},
|
||||
});
|
||||
// Delay to allow for async processing
|
||||
await new Promise((resolve) => setTimeout(resolve, 500));
|
||||
|
||||
const dbGeneration = await getObservationById(generationId, projectId);
|
||||
|
||||
expect(dbGeneration?.id).toBe(generationId);
|
||||
expect(dbGeneration?.promptId).toBe(promptId);
|
||||
|
||||
@@ -1,791 +0,0 @@
|
||||
/** @jest-environment node */
|
||||
|
||||
import { prisma } from "@langfuse/shared/src/db";
|
||||
import {
|
||||
makeAPICall,
|
||||
makeZodVerifiedAPICall,
|
||||
pruneDatabase,
|
||||
} from "@/src/__tests__/test-utils";
|
||||
import { v4 as uuidv4 } from "uuid";
|
||||
import { DeleteScoreResponse, GetScoreResponse } from "@langfuse/shared";
|
||||
import { PostTracesV1Response } from "@/src/features/public-api/types/traces";
|
||||
|
||||
const traceId = "de98afa2-89dc-47e9-9924-33f1490fdaf4";
|
||||
|
||||
describe("/api/public/scores API Endpoint", () => {
|
||||
let should_prune_db = true;
|
||||
beforeEach(async () => {
|
||||
if (should_prune_db) await pruneDatabase();
|
||||
|
||||
await makeZodVerifiedAPICall(
|
||||
PostTracesV1Response,
|
||||
"POST",
|
||||
"/api/public/traces",
|
||||
{
|
||||
id: traceId,
|
||||
name: "trace-name",
|
||||
userId: "user-1",
|
||||
projectId: "7a88fb47-b4e2-43b8-a06c-a5ce950dc53a",
|
||||
metadata: { key: "value" },
|
||||
release: "1.0.0",
|
||||
version: "2.0.0",
|
||||
},
|
||||
);
|
||||
}, 10000);
|
||||
afterEach(async () => {
|
||||
if (should_prune_db) await pruneDatabase();
|
||||
});
|
||||
|
||||
it("should create score for a trace", async () => {
|
||||
const dbTrace = await prisma.trace.findMany({
|
||||
where: {
|
||||
id: traceId,
|
||||
},
|
||||
});
|
||||
|
||||
expect(dbTrace.length).toBeGreaterThan(0);
|
||||
expect(dbTrace[0]?.id).toBe(traceId);
|
||||
|
||||
const scoreId = uuidv4();
|
||||
const createScore = await makeAPICall("POST", "/api/public/scores", {
|
||||
id: scoreId,
|
||||
name: "score-name",
|
||||
value: 100.5,
|
||||
traceId: traceId,
|
||||
comment: "comment",
|
||||
});
|
||||
|
||||
expect(createScore.status).toBe(200);
|
||||
const fetchedScore = await makeZodVerifiedAPICall(
|
||||
GetScoreResponse,
|
||||
"GET",
|
||||
`/api/public/scores/${scoreId}`,
|
||||
);
|
||||
|
||||
expect(fetchedScore.body?.id).toBe(scoreId);
|
||||
expect(fetchedScore.body?.traceId).toBe(traceId);
|
||||
expect(fetchedScore.body?.name).toBe("score-name");
|
||||
expect(fetchedScore.body?.value).toBe(100.5);
|
||||
expect(fetchedScore.body?.observationId).toBeNull();
|
||||
expect(fetchedScore.body?.comment).toBe("comment");
|
||||
expect(fetchedScore.body?.source).toBe("API");
|
||||
expect(fetchedScore.body?.projectId).toBe(
|
||||
"7a88fb47-b4e2-43b8-a06c-a5ce950dc53a",
|
||||
);
|
||||
});
|
||||
|
||||
it("should create score for a trace with int", async () => {
|
||||
const dbTrace = await prisma.trace.findMany({
|
||||
where: {
|
||||
id: traceId,
|
||||
},
|
||||
});
|
||||
|
||||
expect(dbTrace.length).toBeGreaterThan(0);
|
||||
expect(dbTrace[0]?.id).toBe(traceId);
|
||||
|
||||
const scoreId = uuidv4();
|
||||
const createScore = await makeAPICall("POST", "/api/public/scores", {
|
||||
id: scoreId,
|
||||
name: "score-name",
|
||||
value: 100,
|
||||
traceId: traceId,
|
||||
});
|
||||
|
||||
expect(createScore.status).toBe(200);
|
||||
const fetchedScore = await makeZodVerifiedAPICall(
|
||||
GetScoreResponse,
|
||||
"GET",
|
||||
`/api/public/scores/${scoreId}`,
|
||||
);
|
||||
|
||||
expect(fetchedScore.body?.id).toBe(scoreId);
|
||||
expect(fetchedScore.body?.traceId).toBe(traceId);
|
||||
expect(fetchedScore.body?.name).toBe("score-name");
|
||||
expect(fetchedScore.body?.value).toBe(100);
|
||||
expect(fetchedScore.body?.observationId).toBeNull();
|
||||
});
|
||||
|
||||
it("should create score for a generation", async () => {
|
||||
await pruneDatabase();
|
||||
|
||||
const generationId = uuidv4();
|
||||
|
||||
await makeZodVerifiedAPICall(
|
||||
PostTracesV1Response,
|
||||
"POST",
|
||||
"/api/public/generations",
|
||||
{
|
||||
id: generationId,
|
||||
name: "generation-name",
|
||||
startTime: "2021-01-01T00:00:00.000Z",
|
||||
endTime: "2021-01-01T00:00:00.000Z",
|
||||
model: "model-name",
|
||||
modelParameters: { key: "value" },
|
||||
prompt: { key: "value" },
|
||||
metadata: { key: "value" },
|
||||
version: "2.0.0",
|
||||
},
|
||||
);
|
||||
|
||||
const dbGeneration = await prisma.observation.findMany({
|
||||
where: {
|
||||
id: generationId,
|
||||
},
|
||||
});
|
||||
|
||||
expect(dbGeneration.length).toBeGreaterThan(0);
|
||||
expect(dbGeneration[0]?.id).toBe(generationId);
|
||||
|
||||
const scoreId = uuidv4();
|
||||
const scoreData = {
|
||||
id: scoreId,
|
||||
name: "score-name",
|
||||
value: 100,
|
||||
traceId: dbGeneration[0]!.traceId!,
|
||||
observationId: dbGeneration[0]!.id,
|
||||
};
|
||||
const createScore = await makeAPICall(
|
||||
"POST",
|
||||
"/api/public/scores",
|
||||
scoreData,
|
||||
);
|
||||
|
||||
expect(createScore.status).toBe(200);
|
||||
const fetchedScore = await makeZodVerifiedAPICall(
|
||||
GetScoreResponse,
|
||||
"GET",
|
||||
`/api/public/scores/${scoreId}`,
|
||||
);
|
||||
|
||||
expect(fetchedScore.body).toMatchObject(scoreData);
|
||||
});
|
||||
|
||||
it("should create numeric score if value is integer and no data type is passed", async () => {
|
||||
const dbTrace = await prisma.trace.findMany({
|
||||
where: {
|
||||
id: traceId,
|
||||
},
|
||||
});
|
||||
|
||||
expect(dbTrace.length).toBeGreaterThan(0);
|
||||
expect(dbTrace[0]?.id).toBe(traceId);
|
||||
|
||||
const scoreId = uuidv4();
|
||||
const scoreData = {
|
||||
id: scoreId,
|
||||
name: "accuracy",
|
||||
value: 1,
|
||||
traceId,
|
||||
};
|
||||
const createScore = await makeAPICall(
|
||||
"POST",
|
||||
"/api/public/scores",
|
||||
scoreData,
|
||||
);
|
||||
|
||||
expect(createScore.status).toBe(200);
|
||||
const fetchedScore = await makeZodVerifiedAPICall(
|
||||
GetScoreResponse,
|
||||
"GET",
|
||||
`/api/public/scores/${scoreId}`,
|
||||
);
|
||||
|
||||
expect(fetchedScore.body).toMatchObject({
|
||||
...scoreData,
|
||||
dataType: "NUMERIC",
|
||||
});
|
||||
});
|
||||
|
||||
it("should create categorical score if value is string and no data type is passed", async () => {
|
||||
const dbTrace = await prisma.trace.findMany({
|
||||
where: {
|
||||
id: traceId,
|
||||
},
|
||||
});
|
||||
|
||||
expect(dbTrace.length).toBeGreaterThan(0);
|
||||
expect(dbTrace[0]?.id).toBe(traceId);
|
||||
|
||||
const scoreId = uuidv4();
|
||||
const scoreData = {
|
||||
id: scoreId,
|
||||
name: "accuracy",
|
||||
value: "Good",
|
||||
traceId,
|
||||
};
|
||||
const createScore = await makeAPICall(
|
||||
"POST",
|
||||
"/api/public/scores",
|
||||
scoreData,
|
||||
);
|
||||
|
||||
expect(createScore.status).toBe(200);
|
||||
const fetchedScore = await makeZodVerifiedAPICall(
|
||||
GetScoreResponse,
|
||||
"GET",
|
||||
`/api/public/scores/${scoreId}`,
|
||||
);
|
||||
|
||||
expect(fetchedScore.body).toMatchObject({
|
||||
...scoreData,
|
||||
value: null,
|
||||
stringValue: "Good",
|
||||
dataType: "CATEGORICAL",
|
||||
});
|
||||
});
|
||||
|
||||
it("should create boolean score if boolean data type is passed", async () => {
|
||||
const dbTrace = await prisma.trace.findMany({
|
||||
where: {
|
||||
id: traceId,
|
||||
},
|
||||
});
|
||||
|
||||
expect(dbTrace.length).toBeGreaterThan(0);
|
||||
expect(dbTrace[0]?.id).toBe(traceId);
|
||||
|
||||
const scoreId = uuidv4();
|
||||
const scoreData = {
|
||||
id: scoreId,
|
||||
name: "score-name",
|
||||
value: 1,
|
||||
dataType: "BOOLEAN",
|
||||
traceId,
|
||||
};
|
||||
const createScore = await makeAPICall(
|
||||
"POST",
|
||||
"/api/public/scores",
|
||||
scoreData,
|
||||
);
|
||||
|
||||
expect(createScore.status).toBe(200);
|
||||
|
||||
const fetchedScore = await makeZodVerifiedAPICall(
|
||||
GetScoreResponse,
|
||||
"GET",
|
||||
`/api/public/scores/${scoreId}`,
|
||||
);
|
||||
|
||||
expect(fetchedScore.body).toMatchObject({
|
||||
...scoreData,
|
||||
stringValue: "True",
|
||||
});
|
||||
});
|
||||
|
||||
it("should infer boolean data type from boolean score config", async () => {
|
||||
const dbTrace = await prisma.trace.findMany({
|
||||
where: {
|
||||
id: traceId,
|
||||
},
|
||||
});
|
||||
|
||||
expect(dbTrace.length).toBeGreaterThan(0);
|
||||
expect(dbTrace[0]?.id).toBe(traceId);
|
||||
|
||||
await makeAPICall("POST", "/api/public/score-configs", {
|
||||
name: "accuracy",
|
||||
dataType: "BOOLEAN",
|
||||
});
|
||||
|
||||
const dbScoreConfig = await prisma.scoreConfig.findMany({
|
||||
where: {
|
||||
name: "accuracy",
|
||||
},
|
||||
});
|
||||
|
||||
expect(dbScoreConfig.length).toBeGreaterThan(0);
|
||||
expect(dbScoreConfig[0]?.name).toBe("accuracy");
|
||||
|
||||
const scoreId = uuidv4();
|
||||
const scoreData = {
|
||||
id: scoreId,
|
||||
name: "accuracy",
|
||||
value: 1,
|
||||
configId: dbScoreConfig[0].id,
|
||||
traceId,
|
||||
};
|
||||
const createScore = await makeAPICall(
|
||||
"POST",
|
||||
"/api/public/scores",
|
||||
scoreData,
|
||||
);
|
||||
|
||||
expect(createScore.status).toBe(200);
|
||||
const fetchedScore = await makeZodVerifiedAPICall(
|
||||
GetScoreResponse,
|
||||
"GET",
|
||||
`/api/public/scores/${scoreId}`,
|
||||
);
|
||||
|
||||
expect(fetchedScore.body).toMatchObject({
|
||||
...scoreData,
|
||||
stringValue: "True",
|
||||
dataType: "BOOLEAN",
|
||||
});
|
||||
});
|
||||
|
||||
it("should NOT create categorical score if numeric data type is passed", async () => {
|
||||
const dbTrace = await prisma.trace.findMany({
|
||||
where: {
|
||||
id: traceId,
|
||||
},
|
||||
});
|
||||
|
||||
expect(dbTrace.length).toBeGreaterThan(0);
|
||||
expect(dbTrace[0]?.id).toBe(traceId);
|
||||
|
||||
const scoreId = uuidv4();
|
||||
const scoreData = {
|
||||
id: scoreId,
|
||||
name: "accuracy",
|
||||
value: 1,
|
||||
dataType: "CATEGORICAL",
|
||||
traceId,
|
||||
};
|
||||
const createScore = await makeAPICall(
|
||||
"POST",
|
||||
"/api/public/scores",
|
||||
scoreData,
|
||||
);
|
||||
|
||||
expect(createScore.status).toBe(400);
|
||||
expect(createScore.body).toMatchObject({
|
||||
message: "Invalid request data",
|
||||
error: [
|
||||
{
|
||||
code: "invalid_type",
|
||||
expected: "string",
|
||||
message: "Expected string, received number",
|
||||
path: ["value"],
|
||||
received: "number",
|
||||
},
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
it("should NOT create numeric score if categorical data type is passed incl numeric config", async () => {
|
||||
const dbTrace = await prisma.trace.findMany({
|
||||
where: {
|
||||
id: traceId,
|
||||
},
|
||||
});
|
||||
|
||||
expect(dbTrace.length).toBeGreaterThan(0);
|
||||
expect(dbTrace[0]?.id).toBe(traceId);
|
||||
|
||||
await makeAPICall("POST", "/api/public/score-configs", {
|
||||
name: "accuracy",
|
||||
dataType: "NUMERIC",
|
||||
});
|
||||
|
||||
const dbScoreConfig = await prisma.scoreConfig.findMany({
|
||||
where: {
|
||||
name: "accuracy",
|
||||
},
|
||||
});
|
||||
|
||||
expect(dbScoreConfig.length).toBeGreaterThan(0);
|
||||
expect(dbScoreConfig[0]?.name).toBe("accuracy");
|
||||
|
||||
const scoreId = uuidv4();
|
||||
const scoreData = {
|
||||
id: scoreId,
|
||||
name: "accuracy",
|
||||
value: "Good",
|
||||
configId: dbScoreConfig[0].id,
|
||||
dataType: "CATEGORICAL",
|
||||
traceId,
|
||||
};
|
||||
const createScore = await makeAPICall(
|
||||
"POST",
|
||||
"/api/public/scores",
|
||||
scoreData,
|
||||
);
|
||||
|
||||
expect(createScore.status).toBe(400);
|
||||
expect(createScore.body).toMatchObject({
|
||||
message:
|
||||
"Data type mismatch based on config: expected NUMERIC, got CATEGORICAL",
|
||||
});
|
||||
});
|
||||
|
||||
it("should NOT create numeric score if config and passed data type mismatch", async () => {
|
||||
const dbTrace = await prisma.trace.findMany({
|
||||
where: {
|
||||
id: traceId,
|
||||
},
|
||||
});
|
||||
|
||||
expect(dbTrace.length).toBeGreaterThan(0);
|
||||
expect(dbTrace[0]?.id).toBe(traceId);
|
||||
|
||||
await makeAPICall("POST", "/api/public/score-configs", {
|
||||
name: "accuracy",
|
||||
dataType: "CATEGORICAL",
|
||||
categories: [
|
||||
{ label: "One", value: 1 },
|
||||
{ label: "Zero", value: 0 },
|
||||
],
|
||||
});
|
||||
|
||||
const dbScoreConfig = await prisma.scoreConfig.findMany({
|
||||
where: {
|
||||
name: "accuracy",
|
||||
},
|
||||
});
|
||||
|
||||
expect(dbScoreConfig.length).toBeGreaterThan(0);
|
||||
expect(dbScoreConfig[0]?.name).toBe("accuracy");
|
||||
|
||||
const scoreId = uuidv4();
|
||||
const scoreData = {
|
||||
id: scoreId,
|
||||
name: "accuracy",
|
||||
value: 1,
|
||||
configId: dbScoreConfig[0].id,
|
||||
dataType: "NUMERIC",
|
||||
traceId,
|
||||
};
|
||||
const createScore = await makeAPICall(
|
||||
"POST",
|
||||
"/api/public/scores",
|
||||
scoreData,
|
||||
);
|
||||
|
||||
expect(createScore.status).toBe(400);
|
||||
expect(createScore.body).toMatchObject({
|
||||
message:
|
||||
"Data type mismatch based on config: expected CATEGORICAL, got NUMERIC",
|
||||
});
|
||||
});
|
||||
|
||||
it("should NOT create boolean score if string value is passed", async () => {
|
||||
const dbTrace = await prisma.trace.findMany({
|
||||
where: {
|
||||
id: traceId,
|
||||
},
|
||||
});
|
||||
|
||||
expect(dbTrace.length).toBeGreaterThan(0);
|
||||
expect(dbTrace[0]?.id).toBe(traceId);
|
||||
|
||||
await makeAPICall("POST", "/api/public/score-configs", {
|
||||
name: "accuracy",
|
||||
dataType: "BOOLEAN",
|
||||
});
|
||||
|
||||
const dbScoreConfig = await prisma.scoreConfig.findMany({
|
||||
where: {
|
||||
name: "accuracy",
|
||||
},
|
||||
});
|
||||
|
||||
expect(dbScoreConfig.length).toBeGreaterThan(0);
|
||||
expect(dbScoreConfig[0]?.name).toBe("accuracy");
|
||||
|
||||
const scoreId = uuidv4();
|
||||
const scoreData = {
|
||||
id: scoreId,
|
||||
name: "accuracy",
|
||||
value: "True",
|
||||
configId: dbScoreConfig[0].id,
|
||||
dataType: "BOOLEAN",
|
||||
traceId,
|
||||
};
|
||||
const createScore = await makeAPICall(
|
||||
"POST",
|
||||
"/api/public/scores",
|
||||
scoreData,
|
||||
);
|
||||
|
||||
expect(createScore.status).toBe(400);
|
||||
expect(createScore.body).toMatchObject({
|
||||
message: "Invalid request data",
|
||||
error: [
|
||||
{
|
||||
code: "invalid_type",
|
||||
expected: "number",
|
||||
message: "Expected number, received string",
|
||||
path: ["value"],
|
||||
received: "string",
|
||||
},
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
it("should NOT create boolean score with value other than 1 | 0", async () => {
|
||||
const dbTrace = await prisma.trace.findMany({
|
||||
where: {
|
||||
id: traceId,
|
||||
},
|
||||
});
|
||||
|
||||
expect(dbTrace.length).toBeGreaterThan(0);
|
||||
expect(dbTrace[0]?.id).toBe(traceId);
|
||||
|
||||
await makeAPICall("POST", "/api/public/score-configs", {
|
||||
name: "accuracy",
|
||||
dataType: "BOOLEAN",
|
||||
});
|
||||
|
||||
const dbScoreConfig = await prisma.scoreConfig.findMany({
|
||||
where: {
|
||||
name: "accuracy",
|
||||
},
|
||||
});
|
||||
|
||||
expect(dbScoreConfig.length).toBeGreaterThan(0);
|
||||
expect(dbScoreConfig[0]?.name).toBe("accuracy");
|
||||
|
||||
const scoreId = uuidv4();
|
||||
const scoreData = {
|
||||
id: scoreId,
|
||||
name: "accuracy",
|
||||
value: 0.5,
|
||||
configId: dbScoreConfig[0].id,
|
||||
dataType: "BOOLEAN",
|
||||
traceId,
|
||||
};
|
||||
const createScore = await makeAPICall(
|
||||
"POST",
|
||||
"/api/public/scores",
|
||||
scoreData,
|
||||
);
|
||||
|
||||
expect(createScore.status).toBe(400);
|
||||
expect(createScore.body).toMatchObject({
|
||||
message: "Invalid request data",
|
||||
error: [
|
||||
{
|
||||
code: "custom",
|
||||
message:
|
||||
"Value must be a number equal to either 0 or 1 for data type BOOLEAN",
|
||||
path: ["value"],
|
||||
},
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
it("should NOT create categorical score with value not defined in config.categories", async () => {
|
||||
const dbTrace = await prisma.trace.findMany({
|
||||
where: {
|
||||
id: traceId,
|
||||
},
|
||||
});
|
||||
|
||||
expect(dbTrace.length).toBeGreaterThan(0);
|
||||
expect(dbTrace[0]?.id).toBe(traceId);
|
||||
|
||||
await makeAPICall("POST", "/api/public/score-configs", {
|
||||
name: "accuracy",
|
||||
dataType: "CATEGORICAL",
|
||||
categories: [
|
||||
{ label: "One", value: 1 },
|
||||
{ label: "Zero", value: 0 },
|
||||
],
|
||||
});
|
||||
|
||||
const dbScoreConfig = await prisma.scoreConfig.findMany({
|
||||
where: {
|
||||
name: "accuracy",
|
||||
},
|
||||
});
|
||||
|
||||
expect(dbScoreConfig.length).toBeGreaterThan(0);
|
||||
expect(dbScoreConfig[0]?.name).toBe("accuracy");
|
||||
|
||||
const scoreId = uuidv4();
|
||||
const scoreData = {
|
||||
id: scoreId,
|
||||
name: "accuracy",
|
||||
value: 1,
|
||||
configId: dbScoreConfig[0].id,
|
||||
dataType: "CATEGORICAL",
|
||||
traceId,
|
||||
};
|
||||
const createScore = await makeAPICall(
|
||||
"POST",
|
||||
"/api/public/scores",
|
||||
scoreData,
|
||||
);
|
||||
|
||||
expect(createScore.status).toBe(400);
|
||||
expect(createScore.body).toMatchObject({
|
||||
message: "Invalid request data",
|
||||
error: [
|
||||
{
|
||||
code: "invalid_type",
|
||||
expected: "string",
|
||||
message: "Expected string, received number",
|
||||
path: ["value"],
|
||||
received: "number",
|
||||
},
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
it("should NOT create numeric score outside of defined range", async () => {
|
||||
const dbTrace = await prisma.trace.findMany({
|
||||
where: {
|
||||
id: traceId,
|
||||
},
|
||||
});
|
||||
|
||||
expect(dbTrace.length).toBeGreaterThan(0);
|
||||
expect(dbTrace[0]?.id).toBe(traceId);
|
||||
|
||||
await makeAPICall("POST", "/api/public/score-configs", {
|
||||
name: "accuracy",
|
||||
dataType: "NUMERIC",
|
||||
maxValue: 0,
|
||||
});
|
||||
|
||||
const dbScoreConfig = await prisma.scoreConfig.findMany({
|
||||
where: {
|
||||
name: "accuracy",
|
||||
},
|
||||
});
|
||||
|
||||
expect(dbScoreConfig.length).toBeGreaterThan(0);
|
||||
expect(dbScoreConfig[0]?.name).toBe("accuracy");
|
||||
|
||||
const scoreId = uuidv4();
|
||||
const scoreData = {
|
||||
id: scoreId,
|
||||
name: "accuracy",
|
||||
value: 0.5,
|
||||
configId: dbScoreConfig[0].id,
|
||||
dataType: "NUMERIC",
|
||||
traceId,
|
||||
};
|
||||
const createScore = await makeAPICall(
|
||||
"POST",
|
||||
"/api/public/scores",
|
||||
scoreData,
|
||||
);
|
||||
|
||||
expect(createScore.status).toBe(400);
|
||||
expect(createScore.body).toMatchObject({
|
||||
message:
|
||||
"Ingested score body not valid against provided config: - Value exceeds maximum value of 0 defined in config",
|
||||
});
|
||||
});
|
||||
|
||||
it("should upsert a score", async () => {
|
||||
const dbTrace = await prisma.trace.findMany({
|
||||
where: {
|
||||
id: traceId,
|
||||
},
|
||||
});
|
||||
|
||||
expect(dbTrace.length).toBeGreaterThan(0);
|
||||
expect(dbTrace[0]?.id).toBe(traceId);
|
||||
|
||||
const generationId = uuidv4();
|
||||
await makeZodVerifiedAPICall(
|
||||
PostTracesV1Response,
|
||||
"POST",
|
||||
"/api/public/generations",
|
||||
{
|
||||
id: generationId,
|
||||
name: "generation-name",
|
||||
traceId,
|
||||
startTime: "2021-01-01T00:00:00.000Z",
|
||||
endTime: "2021-01-01T00:00:00.000Z",
|
||||
model: "model-name",
|
||||
modelParameters: { key: "value" },
|
||||
prompt: { key: "value" },
|
||||
metadata: { key: "value" },
|
||||
version: "2.0.0",
|
||||
},
|
||||
);
|
||||
|
||||
const dbGeneration = await prisma.observation.findMany({
|
||||
where: {
|
||||
id: generationId,
|
||||
},
|
||||
});
|
||||
|
||||
expect(dbGeneration.length).toBeGreaterThan(0);
|
||||
expect(dbGeneration[0]?.id).toBe(generationId);
|
||||
|
||||
const scoreId = uuidv4();
|
||||
const createScore = await makeAPICall("POST", "/api/public/scores", {
|
||||
id: scoreId,
|
||||
name: "score-name",
|
||||
value: 100,
|
||||
traceId: traceId,
|
||||
comment: "comment",
|
||||
});
|
||||
expect(createScore.status).toBe(200);
|
||||
|
||||
const upsertScore = await makeAPICall("POST", "/api/public/scores", {
|
||||
id: scoreId,
|
||||
traceId: traceId,
|
||||
name: "score-name-updated",
|
||||
value: 200,
|
||||
comment: "comment-updated",
|
||||
observationId: dbGeneration[0]!.id,
|
||||
});
|
||||
expect(upsertScore.status).toBe(200);
|
||||
|
||||
const fetchedScore = await makeZodVerifiedAPICall(
|
||||
GetScoreResponse,
|
||||
"GET",
|
||||
`/api/public/scores/${scoreId}`,
|
||||
);
|
||||
|
||||
expect(fetchedScore.body?.id).toBe(scoreId);
|
||||
expect(fetchedScore.body?.traceId).toBe(traceId);
|
||||
expect(fetchedScore.body?.name).toBe("score-name-updated");
|
||||
expect(fetchedScore.body?.value).toBe(200);
|
||||
expect(fetchedScore.body?.comment).toBe("comment-updated");
|
||||
expect(fetchedScore.body?.observationId).toBe(dbGeneration[0]!.id);
|
||||
});
|
||||
|
||||
it("should delete a score", async () => {
|
||||
await pruneDatabase();
|
||||
|
||||
const traceId = uuidv4();
|
||||
|
||||
await makeZodVerifiedAPICall(
|
||||
PostTracesV1Response,
|
||||
"POST",
|
||||
"/api/public/traces",
|
||||
{
|
||||
id: traceId,
|
||||
},
|
||||
);
|
||||
|
||||
const scoreId = uuidv4();
|
||||
const createScore = await makeAPICall("POST", "/api/public/scores", {
|
||||
id: scoreId,
|
||||
name: "score-name",
|
||||
value: 100.5,
|
||||
traceId: traceId,
|
||||
comment: "comment",
|
||||
});
|
||||
|
||||
expect(createScore.status).toBe(200);
|
||||
const fetchedScore = await makeZodVerifiedAPICall(
|
||||
GetScoreResponse,
|
||||
"GET",
|
||||
`/api/public/scores/${scoreId}`,
|
||||
);
|
||||
expect(fetchedScore.body.id).toBe(scoreId);
|
||||
|
||||
const deleteScore = await makeZodVerifiedAPICall(
|
||||
DeleteScoreResponse,
|
||||
"DELETE",
|
||||
`/api/public/scores/${scoreId}`,
|
||||
);
|
||||
expect(deleteScore.status).toBe(200);
|
||||
const deletedScore = await prisma.score.findUnique({
|
||||
where: {
|
||||
id: scoreId,
|
||||
},
|
||||
});
|
||||
expect(deletedScore).toBeNull();
|
||||
});
|
||||
});
|
||||
@@ -1,145 +0,0 @@
|
||||
/** @jest-environment node */
|
||||
import { pruneDatabase } from "@/src/__tests__/test-utils";
|
||||
import { appRouter } from "@/src/server/api/root";
|
||||
import { createInnerTRPCContext } from "@/src/server/api/trpc";
|
||||
import { prisma } from "@langfuse/shared/src/db";
|
||||
import type { Session } from "next-auth";
|
||||
|
||||
describe("Traces TRPC Router", () => {
|
||||
beforeEach(async () => await pruneDatabase());
|
||||
afterEach(async () => await pruneDatabase());
|
||||
|
||||
const session: Session = {
|
||||
expires: "1",
|
||||
user: {
|
||||
id: "clgb17vnp000008jjere5g15i",
|
||||
name: "John Doe",
|
||||
organizations: [
|
||||
{
|
||||
id: "seed-org-id",
|
||||
role: "OWNER",
|
||||
plan: "cloud:hobby",
|
||||
cloudConfig: undefined,
|
||||
projects: [
|
||||
{
|
||||
id: "7a88fb47-b4e2-43b8-a06c-a5ce950dc53a",
|
||||
role: "ADMIN",
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
featureFlags: {
|
||||
templateFlag: true,
|
||||
},
|
||||
admin: true,
|
||||
},
|
||||
environment: {} as any,
|
||||
};
|
||||
|
||||
const ctx = createInnerTRPCContext({ session });
|
||||
const caller = appRouter.createCaller({ ...ctx, prisma });
|
||||
|
||||
test("traces.all RPC returns an array of traces", async () => {
|
||||
const trace = {
|
||||
name: "trace-name",
|
||||
userId: "user-1",
|
||||
release: "1.0.0",
|
||||
version: "2.0.0",
|
||||
};
|
||||
await prisma.trace.create({
|
||||
data: { ...trace, projectId: "7a88fb47-b4e2-43b8-a06c-a5ce950dc53a" },
|
||||
});
|
||||
|
||||
const traces = await caller.traces.all({
|
||||
page: 0,
|
||||
limit: 10,
|
||||
// projectId from `seed.ts`
|
||||
projectId: "7a88fb47-b4e2-43b8-a06c-a5ce950dc53a",
|
||||
filter: null,
|
||||
searchQuery: "",
|
||||
orderBy: null,
|
||||
});
|
||||
expect(traces).toBeDefined();
|
||||
expect(traces).toMatchObject({ traces: [trace] });
|
||||
});
|
||||
|
||||
test("traces.all RPC must not return input, output, metadata", async () => {
|
||||
const trace = {
|
||||
name: "trace-name",
|
||||
userId: "user-1",
|
||||
input: { a: 1 },
|
||||
output: { b: 2 },
|
||||
metadata: { c: 3 },
|
||||
release: "1.0.0",
|
||||
version: "2.0.0",
|
||||
};
|
||||
await prisma.trace.create({
|
||||
data: { ...trace, projectId: "7a88fb47-b4e2-43b8-a06c-a5ce950dc53a" },
|
||||
});
|
||||
|
||||
const traces = await caller.traces.all({
|
||||
page: 0,
|
||||
limit: 10,
|
||||
// projectId from `seed.ts`
|
||||
projectId: "7a88fb47-b4e2-43b8-a06c-a5ce950dc53a",
|
||||
filter: null,
|
||||
searchQuery: "",
|
||||
orderBy: null,
|
||||
});
|
||||
expect(traces.traces).toBeDefined();
|
||||
expect(traces.traces).toHaveLength(1);
|
||||
|
||||
const returnedTrace = traces.traces[0];
|
||||
expect(returnedTrace).not.toHaveProperty("input");
|
||||
expect(returnedTrace).not.toHaveProperty("output");
|
||||
expect(returnedTrace).not.toHaveProperty("metadata");
|
||||
});
|
||||
|
||||
test("traces.all RPC orders traces by userId", async () => {
|
||||
const traceTmpl = {
|
||||
name: "trace-name",
|
||||
userId: "user-1",
|
||||
release: "1.0.0",
|
||||
version: "2.0.0",
|
||||
};
|
||||
const trace1 = traceTmpl;
|
||||
const trace2 = { ...traceTmpl, userId: "user-2" };
|
||||
await prisma.trace.create({
|
||||
data: { ...traceTmpl, projectId: "7a88fb47-b4e2-43b8-a06c-a5ce950dc53a" },
|
||||
});
|
||||
await prisma.trace.create({
|
||||
data: {
|
||||
...trace2,
|
||||
projectId: "7a88fb47-b4e2-43b8-a06c-a5ce950dc53a",
|
||||
},
|
||||
});
|
||||
|
||||
const tracesASC = await caller.traces.all({
|
||||
page: 0,
|
||||
limit: 10,
|
||||
// projectId from `seed.ts`
|
||||
projectId: "7a88fb47-b4e2-43b8-a06c-a5ce950dc53a",
|
||||
filter: null,
|
||||
searchQuery: "",
|
||||
orderBy: {
|
||||
column: "userId",
|
||||
order: "ASC",
|
||||
},
|
||||
});
|
||||
expect(tracesASC).toMatchObject({ traces: [trace1, trace2] });
|
||||
|
||||
const tracesDESC = await caller.traces.all({
|
||||
page: 0,
|
||||
limit: 10,
|
||||
// projectId from `seed.ts`
|
||||
projectId: "7a88fb47-b4e2-43b8-a06c-a5ce950dc53a",
|
||||
filter: null,
|
||||
searchQuery: "",
|
||||
orderBy: {
|
||||
column: "userId",
|
||||
order: "DESC",
|
||||
},
|
||||
});
|
||||
expect(tracesDESC).toMatchObject({ traces: [trace2, trace1] });
|
||||
});
|
||||
});
|
||||
@@ -25,6 +25,9 @@ describe("Create and get sessions", () => {
|
||||
},
|
||||
);
|
||||
|
||||
// Delay to allow for async processing
|
||||
await new Promise((resolve) => setTimeout(resolve, 500));
|
||||
|
||||
const dbSession = await prisma.traceSession.findFirst({
|
||||
where: {
|
||||
id: "session-id",
|
||||
|
||||
@@ -1,376 +0,0 @@
|
||||
/** @jest-environment node */
|
||||
|
||||
import { prisma } from "@langfuse/shared/src/db";
|
||||
import {
|
||||
makeAPICall,
|
||||
makeZodVerifiedAPICall,
|
||||
pruneDatabase,
|
||||
} from "@/src/__tests__/test-utils";
|
||||
import { v4 as uuidv4 } from "uuid";
|
||||
import { PostTracesV1Response } from "@/src/features/public-api/types/traces";
|
||||
import {
|
||||
PatchSpansV1Response,
|
||||
PostSpansV1Response,
|
||||
} from "@/src/features/public-api/types/spans";
|
||||
|
||||
describe("/api/public/spans API Endpoint", () => {
|
||||
beforeEach(async () => await pruneDatabase());
|
||||
afterEach(async () => await pruneDatabase());
|
||||
|
||||
it("should create span after trace", async () => {
|
||||
const traceId = uuidv4();
|
||||
|
||||
await makeZodVerifiedAPICall(
|
||||
PostTracesV1Response,
|
||||
"POST",
|
||||
"/api/public/traces",
|
||||
{
|
||||
id: traceId,
|
||||
name: "trace-name",
|
||||
userId: "user-1",
|
||||
projectId: "7a88fb47-b4e2-43b8-a06c-a5ce950dc53a",
|
||||
metadata: { key: "value" },
|
||||
release: "1.0.0",
|
||||
version: "2.0.0",
|
||||
},
|
||||
);
|
||||
|
||||
const dbTrace = await prisma.trace.findMany({
|
||||
where: {
|
||||
id: traceId,
|
||||
},
|
||||
});
|
||||
|
||||
expect(dbTrace.length).toBeGreaterThan(0);
|
||||
expect(dbTrace[0]?.id).toBe(traceId);
|
||||
|
||||
const spanId = uuidv4();
|
||||
const createSpan = await makeZodVerifiedAPICall(
|
||||
PostSpansV1Response,
|
||||
"POST",
|
||||
"/api/public/spans",
|
||||
{
|
||||
id: spanId,
|
||||
traceId: traceId,
|
||||
name: "span-name",
|
||||
startTime: "2021-01-01T00:00:00.000Z",
|
||||
endTime: "2021-01-01T00:00:00.000Z",
|
||||
input: { input: "value" },
|
||||
metadata: { meta: "value" },
|
||||
version: "2.0.0",
|
||||
},
|
||||
);
|
||||
|
||||
expect(createSpan.status).toBe(200);
|
||||
const dbSpan = await prisma.observation.findUnique({
|
||||
where: {
|
||||
id: spanId,
|
||||
},
|
||||
});
|
||||
|
||||
expect(dbSpan?.id).toBe(spanId);
|
||||
expect(dbSpan?.traceId).toBe(traceId);
|
||||
expect(dbSpan?.name).toBe("span-name");
|
||||
expect(dbSpan?.startTime).toEqual(new Date("2021-01-01T00:00:00.000Z"));
|
||||
expect(dbSpan?.endTime).toEqual(new Date("2021-01-01T00:00:00.000Z"));
|
||||
expect(dbSpan?.input).toEqual({ input: "value" });
|
||||
expect(dbSpan?.metadata).toEqual({ meta: "value" });
|
||||
expect(dbSpan?.version).toBe("2.0.0");
|
||||
});
|
||||
|
||||
it("should create span before trace", async () => {
|
||||
const traceId = uuidv4();
|
||||
const spanId = uuidv4();
|
||||
|
||||
const createSpan = await makeZodVerifiedAPICall(
|
||||
PostSpansV1Response,
|
||||
"POST",
|
||||
"/api/public/spans",
|
||||
{
|
||||
id: spanId,
|
||||
traceId: traceId,
|
||||
name: "span-name",
|
||||
startTime: "2021-01-01T00:00:00.000Z",
|
||||
endTime: "2021-01-01T00:00:00.000Z",
|
||||
input: { input: "value" },
|
||||
metadata: { meta: "value" },
|
||||
version: "2.0.0",
|
||||
},
|
||||
);
|
||||
|
||||
expect(createSpan.status).toBe(200);
|
||||
const dbSpan = await prisma.observation.findUnique({
|
||||
where: {
|
||||
id: spanId,
|
||||
},
|
||||
});
|
||||
|
||||
expect(dbSpan?.id).toBe(spanId);
|
||||
expect(dbSpan?.traceId).toBe(traceId);
|
||||
expect(dbSpan?.name).toBe("span-name");
|
||||
expect(dbSpan?.startTime).toEqual(new Date("2021-01-01T00:00:00.000Z"));
|
||||
expect(dbSpan?.endTime).toEqual(new Date("2021-01-01T00:00:00.000Z"));
|
||||
expect(dbSpan?.input).toEqual({ input: "value" });
|
||||
expect(dbSpan?.metadata).toEqual({ meta: "value" });
|
||||
expect(dbSpan?.version).toBe("2.0.0");
|
||||
|
||||
await makeZodVerifiedAPICall(
|
||||
PostTracesV1Response,
|
||||
"POST",
|
||||
"/api/public/traces",
|
||||
{
|
||||
id: traceId,
|
||||
name: "trace-name",
|
||||
userId: "user-1",
|
||||
projectId: "7a88fb47-b4e2-43b8-a06c-a5ce950dc53a",
|
||||
metadata: { key: "value" },
|
||||
release: "1.0.0",
|
||||
version: "2.0.0",
|
||||
},
|
||||
);
|
||||
|
||||
const dbTrace = await prisma.trace.findMany({
|
||||
where: {
|
||||
id: traceId,
|
||||
},
|
||||
});
|
||||
|
||||
expect(dbTrace.length).toBeGreaterThan(0);
|
||||
expect(dbTrace[0]?.id).toBe(traceId);
|
||||
});
|
||||
|
||||
it("should create span after trace ignoring externalId", async () => {
|
||||
const traceId = uuidv4();
|
||||
const spanId = uuidv4();
|
||||
|
||||
const response = await makeZodVerifiedAPICall(
|
||||
PostTracesV1Response,
|
||||
"POST",
|
||||
"/api/public/traces",
|
||||
{
|
||||
externalId: uuidv4(),
|
||||
id: traceId,
|
||||
name: "trace-name",
|
||||
userId: "user-1",
|
||||
projectId: "7a88fb47-b4e2-43b8-a06c-a5ce950dc53a",
|
||||
},
|
||||
);
|
||||
expect(response.status).toBe(200);
|
||||
const createSpan = await makeZodVerifiedAPICall(
|
||||
PostSpansV1Response,
|
||||
"POST",
|
||||
"/api/public/spans",
|
||||
{
|
||||
id: spanId,
|
||||
name: "spanName",
|
||||
projectId: "7a88fb47-b4e2-43b8-a06c-a5ce950dc53a",
|
||||
traceId: traceId,
|
||||
externalTraceIdType: "EXTERNAL",
|
||||
},
|
||||
);
|
||||
expect(createSpan.status).toBe(200);
|
||||
|
||||
const dbSpan = await prisma.observation.findFirstOrThrow({
|
||||
where: {
|
||||
name: "spanName",
|
||||
},
|
||||
});
|
||||
expect(dbSpan.id).toBe(spanId);
|
||||
expect(dbSpan.traceId).toBe(traceId);
|
||||
});
|
||||
|
||||
it("should create trace when creating span without existing trace", async () => {
|
||||
const spanName = uuidv4();
|
||||
|
||||
const spanId = uuidv4();
|
||||
const createSpan = await makeZodVerifiedAPICall(
|
||||
PostSpansV1Response,
|
||||
"POST",
|
||||
"/api/public/spans",
|
||||
{
|
||||
id: spanId,
|
||||
name: spanName,
|
||||
startTime: "2021-01-01T00:00:00.000Z",
|
||||
endTime: "2021-01-01T00:00:00.000Z",
|
||||
input: { input: "value" },
|
||||
metadata: { meta: "value" },
|
||||
version: "2.0.0",
|
||||
},
|
||||
);
|
||||
|
||||
const dbTrace = await prisma.trace.findMany({
|
||||
where: {
|
||||
name: spanName,
|
||||
},
|
||||
});
|
||||
|
||||
expect(dbTrace.length).toBe(1);
|
||||
expect(dbTrace[0]?.name).toBe(spanName);
|
||||
|
||||
expect(createSpan.status).toBe(200);
|
||||
const dbSpan = await prisma.observation.findUnique({
|
||||
where: {
|
||||
id: spanId,
|
||||
},
|
||||
});
|
||||
|
||||
expect(dbSpan?.id).toBe(spanId);
|
||||
expect(dbSpan?.traceId).toBe(dbTrace[0]?.id);
|
||||
expect(dbSpan?.name).toBe(spanName);
|
||||
expect(dbSpan?.startTime).toEqual(new Date("2021-01-01T00:00:00.000Z"));
|
||||
expect(dbSpan?.endTime).toEqual(new Date("2021-01-01T00:00:00.000Z"));
|
||||
expect(dbSpan?.input).toEqual({ input: "value" });
|
||||
expect(dbSpan?.metadata).toEqual({ meta: "value" });
|
||||
expect(dbSpan?.version).toBe("2.0.0");
|
||||
});
|
||||
|
||||
it("should create trace when creating span without existing trace with externalId", async () => {
|
||||
const spanName = uuidv4();
|
||||
|
||||
const spanId = uuidv4();
|
||||
const traceId = uuidv4();
|
||||
const createSpan = await makeZodVerifiedAPICall(
|
||||
PostSpansV1Response,
|
||||
"POST",
|
||||
"/api/public/spans",
|
||||
{
|
||||
id: spanId,
|
||||
traceIdType: "EXTERNAL",
|
||||
traceId: traceId,
|
||||
name: spanName,
|
||||
startTime: "2021-01-01T00:00:00.000Z",
|
||||
endTime: "2021-01-01T00:00:00.000Z",
|
||||
input: { input: "value" },
|
||||
metadata: { meta: "value" },
|
||||
version: "2.0.0",
|
||||
},
|
||||
);
|
||||
|
||||
expect(createSpan.status).toBe(200);
|
||||
|
||||
const dbSpan = await prisma.observation.findMany({
|
||||
where: {
|
||||
name: spanName,
|
||||
},
|
||||
});
|
||||
expect(dbSpan.length).toBe(1);
|
||||
expect(dbSpan[0]?.name).toBe(spanName);
|
||||
expect(dbSpan[0]?.id).toBe(spanId);
|
||||
expect(dbSpan[0]?.traceId).toBe(traceId);
|
||||
|
||||
const dbTraces = await prisma.trace.findMany();
|
||||
expect(dbTraces.length).toBe(0);
|
||||
});
|
||||
|
||||
it("should create trace when creating span without existing trace without traceId", async () => {
|
||||
const generationName = uuidv4();
|
||||
|
||||
const spanId = uuidv4();
|
||||
const createSpan = await makeZodVerifiedAPICall(
|
||||
PostSpansV1Response,
|
||||
"POST",
|
||||
"/api/public/spans",
|
||||
{
|
||||
id: spanId,
|
||||
name: generationName,
|
||||
startTime: "2021-01-01T00:00:00.000Z",
|
||||
endTime: "2021-01-01T00:00:00.000Z",
|
||||
input: { key: "value" },
|
||||
metadata: { key: "value" },
|
||||
version: "2.0.0",
|
||||
},
|
||||
);
|
||||
|
||||
const dbSpan = await prisma.observation.findFirstOrThrow({
|
||||
where: {
|
||||
name: generationName,
|
||||
},
|
||||
});
|
||||
|
||||
const dbTrace = await prisma.trace.findMany({
|
||||
where: {
|
||||
id: dbSpan.traceId!,
|
||||
},
|
||||
});
|
||||
|
||||
expect(dbTrace.length).toBe(1);
|
||||
expect(dbTrace[0]?.name).toBe(generationName);
|
||||
|
||||
expect(createSpan.status).toBe(200);
|
||||
|
||||
expect(dbSpan.id).toBe(spanId);
|
||||
expect(dbSpan.traceId).toBe(dbTrace[0]?.id);
|
||||
expect(dbSpan.name).toBe(generationName);
|
||||
expect(dbSpan.startTime).toEqual(new Date("2021-01-01T00:00:00.000Z"));
|
||||
expect(dbSpan.endTime).toEqual(new Date("2021-01-01T00:00:00.000Z"));
|
||||
expect(dbSpan.input).toEqual({ key: "value" });
|
||||
expect(dbSpan.metadata).toEqual({ key: "value" });
|
||||
expect(dbSpan.version).toBe("2.0.0");
|
||||
});
|
||||
|
||||
it("should update span", async () => {
|
||||
const spanName = uuidv4();
|
||||
|
||||
const spanId = uuidv4();
|
||||
const createSpan = await makeZodVerifiedAPICall(
|
||||
PostSpansV1Response,
|
||||
"POST",
|
||||
"/api/public/spans",
|
||||
{
|
||||
id: spanId,
|
||||
name: spanName,
|
||||
startTime: "2021-01-01T00:00:00.000Z",
|
||||
endTime: "2021-01-01T00:00:00.000Z",
|
||||
input: { input: "value" },
|
||||
metadata: { meta: "value" },
|
||||
version: "2.0.0",
|
||||
},
|
||||
);
|
||||
|
||||
expect(createSpan.status).toBe(200);
|
||||
|
||||
const updatedSpan = await makeZodVerifiedAPICall(
|
||||
PatchSpansV1Response,
|
||||
"PATCH",
|
||||
"/api/public/spans",
|
||||
{
|
||||
spanId: spanId,
|
||||
output: { key: "this is a great gpt output" },
|
||||
},
|
||||
);
|
||||
expect(updatedSpan.status).toBe(200);
|
||||
|
||||
const dbSpan = await prisma.observation.findUnique({
|
||||
where: {
|
||||
id: spanId,
|
||||
},
|
||||
});
|
||||
|
||||
expect(dbSpan?.id).toBe(spanId);
|
||||
expect(dbSpan?.name).toBe(spanName);
|
||||
expect(dbSpan?.startTime).toEqual(new Date("2021-01-01T00:00:00.000Z"));
|
||||
expect(dbSpan?.endTime).toEqual(new Date("2021-01-:00:00.000Z"));
|
||||
expect(dbSpan?.input).toEqual({ input: "value" });
|
||||
expect(dbSpan?.output).toEqual({ key: "this is a great gpt output" });
|
||||
expect(dbSpan?.metadata).toEqual({ meta: "value" });
|
||||
expect(dbSpan?.version).toBe("2.0.0");
|
||||
});
|
||||
|
||||
it("should not upsert span if span does not exist", async () => {
|
||||
const spanId = uuidv4();
|
||||
|
||||
const updatedSpan = await makeAPICall("PATCH", "/api/public/spans", {
|
||||
spanId: spanId,
|
||||
output: { key: "this is a great gpt output" },
|
||||
});
|
||||
expect(updatedSpan.status).toBe(404);
|
||||
|
||||
const dbSpan = await prisma.observation.findUnique({
|
||||
where: {
|
||||
id: spanId,
|
||||
},
|
||||
});
|
||||
|
||||
expect(dbSpan).toBeNull();
|
||||
});
|
||||
});
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,358 +0,0 @@
|
||||
/** @jest-environment node */
|
||||
|
||||
import { v4 as uuidv4 } from "uuid";
|
||||
|
||||
import {
|
||||
makeZodVerifiedAPICall,
|
||||
pruneDatabase,
|
||||
} from "@/src/__tests__/test-utils";
|
||||
import { PostGenerationsV1Response } from "@/src/features/public-api/types/generations";
|
||||
import {
|
||||
GetTracesV1Response,
|
||||
GetTraceV1Response,
|
||||
PostTracesV1Response,
|
||||
} from "@/src/features/public-api/types/traces";
|
||||
import { PostScoresResponse } from "@langfuse/shared";
|
||||
import { prisma } from "@langfuse/shared/src/db";
|
||||
|
||||
describe("/api/public/traces API Endpoint", () => {
|
||||
beforeEach(async () => await pruneDatabase());
|
||||
afterEach(async () => await pruneDatabase());
|
||||
|
||||
it("should upsert second trace", async () => {
|
||||
await pruneDatabase();
|
||||
|
||||
await makeZodVerifiedAPICall(
|
||||
PostTracesV1Response,
|
||||
"POST",
|
||||
"/api/public/traces",
|
||||
{
|
||||
id: "trace-id",
|
||||
name: "trace-name",
|
||||
userId: "user-1",
|
||||
metadata: { key: "value" },
|
||||
release: "1.0.0",
|
||||
version: "2.0.0",
|
||||
public: true,
|
||||
},
|
||||
);
|
||||
|
||||
const dbTrace1 = await prisma.trace.findFirst({
|
||||
where: {
|
||||
id: "trace-id",
|
||||
},
|
||||
});
|
||||
|
||||
expect(dbTrace1).not.toBeNull();
|
||||
expect(dbTrace1).toMatchObject({
|
||||
name: "trace-name",
|
||||
release: "1.0.0",
|
||||
externalId: null,
|
||||
version: "2.0.0",
|
||||
public: true,
|
||||
userId: "user-1",
|
||||
});
|
||||
|
||||
await makeZodVerifiedAPICall(
|
||||
PostTracesV1Response,
|
||||
"POST",
|
||||
"/api/public/traces",
|
||||
{
|
||||
id: "trace-id",
|
||||
metadata: { key: "value" },
|
||||
timestamp: "2021-01-01T00:00:00.000Z",
|
||||
release: "1.0.0",
|
||||
version: "5.0.0",
|
||||
public: false,
|
||||
},
|
||||
);
|
||||
|
||||
const dbTrace2 = await prisma.trace.findFirst({
|
||||
where: {
|
||||
name: "trace-name",
|
||||
},
|
||||
});
|
||||
|
||||
expect(dbTrace2).not.toBeNull();
|
||||
expect(dbTrace2).toMatchObject({
|
||||
name: "trace-name",
|
||||
release: "1.0.0",
|
||||
externalId: null,
|
||||
version: "5.0.0",
|
||||
public: false,
|
||||
userId: "user-1",
|
||||
timestamp: new Date("2021-01-01T00:00:00.000Z"),
|
||||
});
|
||||
});
|
||||
|
||||
it("should use tags correctly on POST and GET", async () => {
|
||||
await pruneDatabase();
|
||||
|
||||
await makeZodVerifiedAPICall(
|
||||
PostTracesV1Response,
|
||||
"POST",
|
||||
"/api/public/traces",
|
||||
{
|
||||
id: "trace-1",
|
||||
tags: ["tag-1", "tag-2", "tag-3"],
|
||||
},
|
||||
);
|
||||
|
||||
await makeZodVerifiedAPICall(
|
||||
PostTracesV1Response,
|
||||
"POST",
|
||||
"/api/public/traces",
|
||||
{
|
||||
id: "trace-2",
|
||||
tags: ["tag-1"],
|
||||
},
|
||||
);
|
||||
|
||||
await makeZodVerifiedAPICall(
|
||||
PostTracesV1Response,
|
||||
"POST",
|
||||
"/api/public/traces",
|
||||
{
|
||||
id: "trace-3",
|
||||
tags: ["tag-2", "tag-3"],
|
||||
},
|
||||
);
|
||||
|
||||
// multiple tags
|
||||
const traces = await makeZodVerifiedAPICall(
|
||||
GetTracesV1Response,
|
||||
"GET",
|
||||
"/api/public/traces?tags=tag-2&tags=tag-3",
|
||||
);
|
||||
const traceIds = traces.body.data.map((t) => t.id);
|
||||
// check for equality ok as ordered by timestamp
|
||||
expect(traceIds).toEqual(["trace-3", "trace-1"]);
|
||||
|
||||
// single tag
|
||||
const traces2 = await makeZodVerifiedAPICall(
|
||||
GetTracesV1Response,
|
||||
"GET",
|
||||
"/api/public/traces?tags=tag-1",
|
||||
);
|
||||
const traceIds2 = traces2.body.data.map((t) => t.id);
|
||||
// check for equality ok as ordered by timestamp
|
||||
expect(traceIds2).toEqual(["trace-2", "trace-1"]);
|
||||
|
||||
// wrong tag
|
||||
const traces3 = await makeZodVerifiedAPICall(
|
||||
GetTracesV1Response,
|
||||
"GET",
|
||||
"/api/public/traces?tags=tag-10",
|
||||
);
|
||||
const traceIds3 = traces3.body.data.map((t) => t.id);
|
||||
// check for equality ok as ordered by timestamp
|
||||
expect(traceIds3).toEqual([]);
|
||||
|
||||
// no tag
|
||||
const traces4 = await makeZodVerifiedAPICall(
|
||||
GetTracesV1Response,
|
||||
"GET",
|
||||
"/api/public/traces?tags=",
|
||||
);
|
||||
const traceIds4 = traces4.body.data.map((t) => t.id);
|
||||
// check for equality ok as ordered by timestamp
|
||||
expect(traceIds4).toEqual(["trace-3", "trace-2", "trace-1"]);
|
||||
});
|
||||
|
||||
it("should handle metrics correctly on GET traces and GET trace", async () => {
|
||||
await pruneDatabase();
|
||||
|
||||
// Create a trace with some observations that have costs and latencies
|
||||
const traceId = uuidv4();
|
||||
await makeZodVerifiedAPICall(
|
||||
PostTracesV1Response,
|
||||
"POST",
|
||||
"/api/public/traces",
|
||||
{
|
||||
id: traceId,
|
||||
name: "trace-with-costs",
|
||||
userId: "user-costs",
|
||||
projectId: "project-costs",
|
||||
metadata: { key: "value" },
|
||||
release: "1.0.0",
|
||||
version: "2.0.0",
|
||||
},
|
||||
);
|
||||
console.log(traceId);
|
||||
|
||||
// Simulate observations with costs and latencies
|
||||
const generationId = uuidv4();
|
||||
await makeZodVerifiedAPICall(
|
||||
PostGenerationsV1Response,
|
||||
"POST",
|
||||
"/api/public/generations",
|
||||
{
|
||||
traceId: traceId,
|
||||
id: generationId,
|
||||
name: "Generation1",
|
||||
usage: { totalCost: 10.5 },
|
||||
startTime: "2021-01-01T00:00:00.000Z",
|
||||
endTime: "2021-01-01T00:10:00.000Z",
|
||||
},
|
||||
);
|
||||
await makeZodVerifiedAPICall(
|
||||
PostGenerationsV1Response,
|
||||
"POST",
|
||||
"/api/public/generations",
|
||||
{
|
||||
traceId: traceId,
|
||||
usage: { totalCost: 5.25 },
|
||||
startTime: "2021-01-01T00:10:00.000Z",
|
||||
endTime: "2021-01-01T00:20:00.000Z",
|
||||
},
|
||||
);
|
||||
|
||||
// Simulate scores on the trace
|
||||
const scoreId1 = uuidv4();
|
||||
await makeZodVerifiedAPICall(
|
||||
PostScoresResponse,
|
||||
"POST",
|
||||
"/api/public/scores",
|
||||
{
|
||||
id: scoreId1,
|
||||
name: "score-1",
|
||||
value: 75.0,
|
||||
traceId: traceId,
|
||||
comment: "First score",
|
||||
},
|
||||
);
|
||||
const scoreId2 = uuidv4();
|
||||
await makeZodVerifiedAPICall(
|
||||
PostScoresResponse,
|
||||
"POST",
|
||||
"/api/public/scores",
|
||||
{
|
||||
id: scoreId2,
|
||||
name: "score-2",
|
||||
value: 85.5,
|
||||
traceId: traceId,
|
||||
comment: "Second score",
|
||||
},
|
||||
);
|
||||
const scoreId3 = uuidv4();
|
||||
await makeZodVerifiedAPICall(
|
||||
PostScoresResponse,
|
||||
"POST",
|
||||
"/api/public/scores",
|
||||
{
|
||||
id: scoreId3,
|
||||
name: "score-3",
|
||||
value: 95.0,
|
||||
traceId: traceId,
|
||||
comment: "Third score",
|
||||
},
|
||||
);
|
||||
|
||||
// GET traces
|
||||
// Retrieve the trace with totalCost and latency
|
||||
const traces = await makeZodVerifiedAPICall(
|
||||
GetTracesV1Response,
|
||||
"GET",
|
||||
`/api/public/traces`,
|
||||
);
|
||||
const traceData = traces.body.data[0];
|
||||
if (!traceData) throw new Error("traceData is undefined");
|
||||
|
||||
// Check if the totalCost and latency are calculated correctly
|
||||
expect(traceData.totalCost).toBeCloseTo(15.75); // Sum of costs
|
||||
expect(traceData.latency).toBeCloseTo(1200); // Difference in seconds between min startTime and max endTime
|
||||
expect(traceData.id).toBe(traceId);
|
||||
expect(traceData.htmlPath).toContain(`/traces/${traceId}`);
|
||||
expect(traceData.htmlPath).toContain(`/project/`); // do not know the projectId
|
||||
|
||||
// GET trace
|
||||
// Retrieve the trace with total
|
||||
const trace = await makeZodVerifiedAPICall(
|
||||
GetTraceV1Response,
|
||||
"GET",
|
||||
`/api/public/traces/${traceId}`,
|
||||
);
|
||||
console.log(trace.body);
|
||||
expect(trace.body.totalCost).toBeCloseTo(15.75);
|
||||
expect(trace.body.id).toBe(traceId);
|
||||
expect(trace.body.id).toBe(traceId);
|
||||
expect(trace.body.htmlPath).toContain(`/traces/${traceId}`);
|
||||
expect(trace.body.htmlPath).toContain(`/project/`); // do not know the projectId
|
||||
expect(trace.body.scores).toHaveLength(3);
|
||||
expect(trace.body.scores[0].id).toBe(scoreId3);
|
||||
expect(trace.body.scores[0].name).toBe("score-3");
|
||||
expect(trace.body.observations).toHaveLength(2);
|
||||
expect(trace.body.observations[0].id).toBe(generationId);
|
||||
expect(trace.body.observations[0].name).toBe("Generation1");
|
||||
});
|
||||
|
||||
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 makeZodVerifiedAPICall(
|
||||
PostTracesV1Response,
|
||||
"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 makeZodVerifiedAPICall(
|
||||
PostTracesV1Response,
|
||||
"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 makeZodVerifiedAPICall(
|
||||
GetTracesV1Response,
|
||||
"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 makeZodVerifiedAPICall(
|
||||
GetTracesV1Response,
|
||||
"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 makeZodVerifiedAPICall(
|
||||
GetTracesV1Response,
|
||||
"GET",
|
||||
`/api/public/traces?sessionId=non-existent-session-id`,
|
||||
);
|
||||
|
||||
expect(tracesByNonExistentSessionId.status).toBe(200);
|
||||
expect(tracesByNonExistentSessionId.body.data).toHaveLength(0);
|
||||
});
|
||||
});
|
||||
@@ -37,9 +37,10 @@ describe("parseJsonPrioritised", () => {
|
||||
["42", 42], // Number
|
||||
["true", true], // Boolean true
|
||||
["false", false], // Boolean false
|
||||
["null", "null"], // Null
|
||||
["null", null], // Null
|
||||
["", ""], // Empty string
|
||||
["invalid{json}", "invalid{json}"], // Invalid JSON string
|
||||
['{"hello": "world"', '{"hello": "world"'], // Invalid JSON string
|
||||
['[null, 123, "abc"]', [null, 123, "abc"]], // Mixed array
|
||||
[
|
||||
'{"array": [1, 2], "nested": {"key": "value"}}',
|
||||
|
||||
@@ -1,109 +0,0 @@
|
||||
import { Button } from "@/src/components/ui/button";
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
CardDescription,
|
||||
CardFooter,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from "@/src/components/ui/card";
|
||||
import { chatAvailable, openChat } from "@/src/features/support-chat/chat";
|
||||
import { Book, Github, Mail, MessageSquare, Slack } from "lucide-react";
|
||||
import Link from "next/link";
|
||||
import { SiDiscord } from "react-icons/si";
|
||||
|
||||
const supportChannels = [
|
||||
{
|
||||
icon: Book,
|
||||
title: "Documentation",
|
||||
description: "Find answers in the documentation.",
|
||||
href: "https://docs.langfuse.com",
|
||||
buttonText: "Visit Docs",
|
||||
primary: true,
|
||||
},
|
||||
{
|
||||
icon: Github,
|
||||
title: "GitHub Issues",
|
||||
description:
|
||||
"Create an issue on Github to report bugs or request new features.",
|
||||
href: "https://github.com/langfuse/langfuse/issues/new/choose",
|
||||
buttonText: "Create Issue",
|
||||
primary: true,
|
||||
},
|
||||
{
|
||||
icon: Github,
|
||||
title: "GitHub Support",
|
||||
description: "Create a support ticket via GitHub discussions.",
|
||||
href: "https://github.com/orgs/langfuse/discussions/categories/support",
|
||||
buttonText: "Submit Question",
|
||||
primary: true,
|
||||
},
|
||||
{
|
||||
icon: Mail,
|
||||
title: "Email",
|
||||
description: "Send email to our shared inbox: help@langfuse.com",
|
||||
href: "mailto:help@langfuse.com",
|
||||
buttonText: "Send Email",
|
||||
available: !chatAvailable,
|
||||
},
|
||||
{
|
||||
icon: MessageSquare,
|
||||
title: "Chat",
|
||||
description: "Get support directly from the team.",
|
||||
onClick: () => openChat(),
|
||||
available: chatAvailable,
|
||||
buttonText: "Launch Chat",
|
||||
},
|
||||
{
|
||||
icon: SiDiscord,
|
||||
title: "Discord",
|
||||
description:
|
||||
"Get support from community. Follow announcements to stay up to date with new features.",
|
||||
href: "https://langfuse.com/discord",
|
||||
buttonText: "Join Discord",
|
||||
},
|
||||
{
|
||||
icon: Slack,
|
||||
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 via Email",
|
||||
},
|
||||
];
|
||||
|
||||
export const SupportChannels = () => (
|
||||
<div className="mt-10 grid grid-cols-1 gap-5 md:grid-cols-2 lg:grid-cols-3">
|
||||
{supportChannels
|
||||
.filter((channel) => channel.available === undefined || channel.available)
|
||||
.map((channel) => (
|
||||
<Card key={channel.title} className="flex flex-col">
|
||||
<CardHeader>
|
||||
<CardTitle className="flex items-center gap-2">
|
||||
<channel.icon size={20} />
|
||||
{channel.title}
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="flex-1">
|
||||
<CardDescription>{channel.description}</CardDescription>
|
||||
</CardContent>
|
||||
<CardFooter>
|
||||
{channel.href ? (
|
||||
<Button
|
||||
asChild
|
||||
variant={channel.primary ? "default" : "secondary"}
|
||||
>
|
||||
<Link href={channel.href}>{channel.buttonText}</Link>
|
||||
</Button>
|
||||
) : (
|
||||
<Button
|
||||
onClick={channel.onClick}
|
||||
variant={channel.primary ? "default" : "secondary"}
|
||||
>
|
||||
{channel.buttonText}
|
||||
</Button>
|
||||
)}
|
||||
</CardFooter>
|
||||
</Card>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
@@ -15,7 +15,7 @@ import { useUiCustomization } from "@/src/ee/features/ui-customization/useUiCust
|
||||
import { hasOrganizationAccess } from "@/src/features/rbac/utils/checkOrganizationAccess";
|
||||
import { ClickhouseAdminToggle } from "@/src/components/layouts/ClickhouseAdminToggle";
|
||||
import { SidebarInset, SidebarProvider } from "@/src/components/ui/sidebar";
|
||||
import { AppSidebar } from "@/src/components/app-sidebar";
|
||||
import { AppSidebar } from "@/src/components/nav/app-sidebar";
|
||||
|
||||
const signOutUser = async () => {
|
||||
localStorage.clear();
|
||||
|
||||
@@ -8,7 +8,6 @@ import {
|
||||
type LucideIcon,
|
||||
Settings,
|
||||
UsersIcon,
|
||||
LibraryBig,
|
||||
TerminalIcon,
|
||||
Lightbulb,
|
||||
Grid2X2,
|
||||
@@ -20,9 +19,11 @@ import { type Entitlement } from "@/src/features/entitlements/constants/entitlem
|
||||
import { type UiCustomizationOption } from "@/src/ee/features/ui-customization/useUiCustomization";
|
||||
import { type User } from "next-auth";
|
||||
import { type OrganizationScope } from "@/src/features/rbac/constants/organizationAccessRights";
|
||||
import { SupportMenuDropdown } from "@/src/components/nav/support-menu-dropdown";
|
||||
|
||||
export type Route = {
|
||||
title: string;
|
||||
menuNode?: ReactNode;
|
||||
featureFlag?: Flag;
|
||||
label?: string | ReactNode;
|
||||
projectRbacScopes?: ProjectScope[]; // array treated as OR
|
||||
@@ -156,19 +157,11 @@ export const ROUTES: Route[] = [
|
||||
icon: Settings,
|
||||
bottom: true,
|
||||
},
|
||||
{
|
||||
title: "Docs",
|
||||
pathname: "https://langfuse.com/docs",
|
||||
icon: LibraryBig,
|
||||
bottom: true,
|
||||
newTab: true,
|
||||
customizableHref: "documentationHref",
|
||||
},
|
||||
{
|
||||
title: "Support",
|
||||
pathname: "/support",
|
||||
icon: LifeBuoy,
|
||||
bottom: true,
|
||||
customizableHref: "supportHref",
|
||||
pathname: "", // Empty pathname since this is a dropdown
|
||||
menuNode: <SupportMenuDropdown />,
|
||||
},
|
||||
];
|
||||
|
||||
@@ -1,8 +1,11 @@
|
||||
"use client";
|
||||
|
||||
import * as React from "react";
|
||||
import { NavMain, type NavMainItem } from "@/src/components/nav-main";
|
||||
import { NavUser, type UserNavigationProps } from "@/src/components/nav-user";
|
||||
import { NavMain, type NavMainItem } from "@/src/components/nav/nav-main";
|
||||
import {
|
||||
NavUser,
|
||||
type UserNavigationProps,
|
||||
} from "@/src/components/nav/nav-user";
|
||||
import {
|
||||
Sidebar,
|
||||
SidebarContent,
|
||||
@@ -15,7 +18,7 @@ import { useRouter } from "next/router";
|
||||
import Link from "next/link";
|
||||
import { Alert, AlertDescription } from "@/src/components/ui/alert";
|
||||
import { LangfuseLogo } from "@/src/components/LangfuseLogo";
|
||||
import { SidebarNotifications } from "@/src/components/sidebar-notifications";
|
||||
import { SidebarNotifications } from "@/src/components/nav/sidebar-notifications";
|
||||
import { UsageTracker } from "@/src/ee/features/billing/components/UsageTracker";
|
||||
|
||||
type AppSidebarProps = {
|
||||
@@ -45,7 +48,7 @@ export function AppSidebar({
|
||||
<UsageTracker />
|
||||
<SidebarNotifications />
|
||||
</div>
|
||||
<NavMain items={secondaryNavItems} showFeedbackButton />
|
||||
<NavMain items={secondaryNavItems} />
|
||||
</SidebarContent>
|
||||
<SidebarFooter>
|
||||
<NavUser {...userNavProps} />
|
||||
@@ -1,6 +1,6 @@
|
||||
"use client";
|
||||
|
||||
import { ChevronRight, MessageSquarePlus, type LucideIcon } from "lucide-react";
|
||||
import { ChevronRight, type LucideIcon } from "lucide-react";
|
||||
|
||||
import {
|
||||
Collapsible,
|
||||
@@ -20,18 +20,20 @@ import {
|
||||
import Link from "next/link";
|
||||
import { type ReactNode } from "react";
|
||||
import { cn } from "@/src/utils/tailwind";
|
||||
import { FeedbackButtonWrapper } from "@/src/features/feedback/component/FeedbackButton";
|
||||
|
||||
export type NavMainItem = {
|
||||
title: string;
|
||||
menuNode?: ReactNode;
|
||||
url: string;
|
||||
icon?: LucideIcon;
|
||||
isActive?: boolean;
|
||||
label?: string | ReactNode;
|
||||
newTab?: boolean;
|
||||
items?: {
|
||||
title: string;
|
||||
url: string;
|
||||
isActive?: boolean;
|
||||
newTab?: boolean;
|
||||
}[];
|
||||
};
|
||||
|
||||
@@ -57,27 +59,11 @@ function NavItemContent({ item }: { item: NavMainItem }) {
|
||||
);
|
||||
}
|
||||
|
||||
export function NavMain({
|
||||
items,
|
||||
showFeedbackButton,
|
||||
}: {
|
||||
items: NavMainItem[];
|
||||
showFeedbackButton?: boolean;
|
||||
}) {
|
||||
export function NavMain({ items }: { items: NavMainItem[] }) {
|
||||
const { open, setOpen } = useSidebar();
|
||||
return (
|
||||
<SidebarGroup>
|
||||
<SidebarMenu>
|
||||
{showFeedbackButton && (
|
||||
<FeedbackButtonWrapper>
|
||||
<SidebarMenuItem>
|
||||
<SidebarMenuButton tooltip="Feedback">
|
||||
<MessageSquarePlus className="h-5 w-5" aria-hidden="true" />
|
||||
Feedback
|
||||
</SidebarMenuButton>
|
||||
</SidebarMenuItem>
|
||||
</FeedbackButtonWrapper>
|
||||
)}
|
||||
{items.map((item) =>
|
||||
item.items && item.items.length > 0 ? (
|
||||
<Collapsible
|
||||
@@ -111,7 +97,10 @@ export function NavMain({
|
||||
asChild
|
||||
isActive={subItem.isActive}
|
||||
>
|
||||
<Link href={subItem.url}>
|
||||
<Link
|
||||
href={subItem.url}
|
||||
target={subItem.newTab ? "_blank" : undefined}
|
||||
>
|
||||
<span>{subItem.title}</span>
|
||||
</Link>
|
||||
</SidebarMenuSubButton>
|
||||
@@ -123,15 +112,20 @@ export function NavMain({
|
||||
</Collapsible>
|
||||
) : (
|
||||
<SidebarMenuItem key={item.title}>
|
||||
<SidebarMenuButton
|
||||
asChild
|
||||
tooltip={item.title}
|
||||
isActive={item.isActive}
|
||||
>
|
||||
<Link href={item.url}>
|
||||
<NavItemContent item={item} />
|
||||
</Link>
|
||||
</SidebarMenuButton>
|
||||
{item.menuNode || (
|
||||
<SidebarMenuButton
|
||||
asChild
|
||||
tooltip={item.title}
|
||||
isActive={item.isActive}
|
||||
>
|
||||
<Link
|
||||
href={item.url}
|
||||
target={item.newTab ? "blank" : undefined}
|
||||
>
|
||||
<NavItemContent item={item} />
|
||||
</Link>
|
||||
</SidebarMenuButton>
|
||||
)}
|
||||
</SidebarMenuItem>
|
||||
),
|
||||
)}
|
||||
+1
-37
@@ -8,7 +8,7 @@ import {
|
||||
} from "@/src/components/ui/card";
|
||||
import { useEffect, useState } from "react";
|
||||
import { X } from "lucide-react";
|
||||
import useLocalStorage from "./useLocalStorage";
|
||||
import useLocalStorage from "../useLocalStorage";
|
||||
import Link from "next/link";
|
||||
import { usePostHogClientCapture } from "@/src/features/posthog-analytics/usePostHogClientCapture";
|
||||
|
||||
@@ -23,42 +23,6 @@ type SidebarNotification = {
|
||||
};
|
||||
|
||||
const notifications: SidebarNotification[] = [
|
||||
{
|
||||
id: "lw2-5",
|
||||
title: "Launch Week 2 – Day 5",
|
||||
description: "Introducing Prompt Experiments to test prompts on datasets",
|
||||
link: "https://langfuse.com/changelog/2024-11-22-prompt-experimentation",
|
||||
linkTitle: "Changelog",
|
||||
},
|
||||
{
|
||||
id: "lw2-4",
|
||||
title: "Launch Week 2 – Day 4",
|
||||
description: "All new docs for datasets, experiments, and evaluations",
|
||||
link: "https://langfuse.com/changelog/2024-11-21-all-new-datasets-and-evals-documentation",
|
||||
linkTitle: "Changelog",
|
||||
},
|
||||
{
|
||||
id: "lw2-3",
|
||||
title: "Launch Week 2 – Day 3",
|
||||
description:
|
||||
"Full multi-modal support including images, audio, and attachments",
|
||||
link: "https://langfuse.com/changelog/2024-11-20-full-multi-modal-images-audio-attachments",
|
||||
linkTitle: "Changelog",
|
||||
},
|
||||
{
|
||||
id: "lw2-2",
|
||||
title: "Launch Week 2 – Day 2",
|
||||
description: "LLM-as-a-Judge Evaluators for Dataset Experiments",
|
||||
link: "https://langfuse.com/changelog/2024-11-19-llm-as-a-judge-for-datasets",
|
||||
linkTitle: "Changelog",
|
||||
},
|
||||
{
|
||||
id: "lw2-1",
|
||||
title: "Launch Week 2 – Day 1",
|
||||
description: "New side-by-side comparison view for dataset experiment runs",
|
||||
link: "https://langfuse.com/changelog/2024-11-18-dataset-runs-comparison-view",
|
||||
linkTitle: "Changelog",
|
||||
},
|
||||
{
|
||||
id: "github-star",
|
||||
title: "Star Langfuse",
|
||||
@@ -0,0 +1,182 @@
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuSeparator,
|
||||
DropdownMenuTrigger,
|
||||
} from "@/src/components/ui/dropdown-menu";
|
||||
import {
|
||||
ArrowUpRight,
|
||||
Bug,
|
||||
Github,
|
||||
LibraryBig,
|
||||
LifeBuoy,
|
||||
Lightbulb,
|
||||
MessageCircle,
|
||||
MessageSquarePlus,
|
||||
Sparkles,
|
||||
type LucideIcon,
|
||||
} from "lucide-react";
|
||||
import { useMemo, type ReactNode } from "react";
|
||||
import {
|
||||
type UiCustomizationOption,
|
||||
useUiCustomization,
|
||||
} from "@/src/ee/features/ui-customization/useUiCustomization";
|
||||
import { SidebarMenuButton, useSidebar } from "@/src/components/ui/sidebar";
|
||||
import {
|
||||
chatAvailable,
|
||||
chatIsVisible,
|
||||
showChat,
|
||||
hideChat,
|
||||
} from "@/src/features/support-chat/chat";
|
||||
import { Switch } from "@/src/components/ui/switch";
|
||||
import { SiDiscord } from "react-icons/si";
|
||||
|
||||
type SupportMenuItem = {
|
||||
title: string;
|
||||
pathname: string;
|
||||
icon: LucideIcon | React.ElementType;
|
||||
menuNode?: ReactNode;
|
||||
customizableHref?: UiCustomizationOption;
|
||||
};
|
||||
|
||||
export const SupportMenuDropdown = () => {
|
||||
const uiCustomization = useUiCustomization();
|
||||
|
||||
const supportMenuItems: (SupportMenuItem | "separator")[] = useMemo(() => {
|
||||
const items: (SupportMenuItem | "separator")[] = [
|
||||
{
|
||||
title: "Ask AI",
|
||||
pathname: "https://langfuse.com/docs/ask-ai",
|
||||
icon: Sparkles,
|
||||
},
|
||||
];
|
||||
|
||||
const chatVisible = chatIsVisible();
|
||||
if (uiCustomization?.supportHref) {
|
||||
items.push({
|
||||
title: "Support",
|
||||
pathname: uiCustomization.supportHref,
|
||||
icon: LifeBuoy,
|
||||
});
|
||||
} else {
|
||||
if (chatAvailable) {
|
||||
items.push({
|
||||
title: "Chat",
|
||||
pathname: "#",
|
||||
menuNode: (
|
||||
<div className="flex items-center gap-2">
|
||||
<MessageCircle className="h-4 w-4" />
|
||||
<span>Chat</span>
|
||||
<Switch
|
||||
defaultChecked={chatVisible}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
onCheckedChange={(checked) => {
|
||||
if (checked) {
|
||||
showChat();
|
||||
} else {
|
||||
hideChat();
|
||||
}
|
||||
}}
|
||||
className="ml-auto"
|
||||
/>
|
||||
</div>
|
||||
),
|
||||
icon: MessageCircle,
|
||||
});
|
||||
}
|
||||
items.push("separator");
|
||||
items.push({
|
||||
title: "GitHub Support",
|
||||
pathname: "https://langfuse.com/gh-support",
|
||||
icon: Github,
|
||||
});
|
||||
items.push({
|
||||
title: "Discord",
|
||||
pathname: "https://langfuse.com/discord",
|
||||
icon: SiDiscord,
|
||||
});
|
||||
}
|
||||
|
||||
items.push("separator");
|
||||
items.push({
|
||||
title: "Docs",
|
||||
pathname: "https://langfuse.com/docs",
|
||||
icon: LibraryBig,
|
||||
customizableHref: "documentationHref",
|
||||
});
|
||||
items.push("separator");
|
||||
|
||||
if (uiCustomization?.feedbackHref) {
|
||||
items.push({
|
||||
title: "Feedback",
|
||||
pathname: uiCustomization.feedbackHref,
|
||||
icon: MessageSquarePlus,
|
||||
});
|
||||
} else {
|
||||
items.push(
|
||||
...[
|
||||
{
|
||||
title: "Feature Request",
|
||||
pathname: "https://langfuse.com/ideas",
|
||||
icon: Lightbulb,
|
||||
},
|
||||
{
|
||||
title: "Report a Bug",
|
||||
pathname: "https://langfuse.com/issues",
|
||||
icon: Bug,
|
||||
},
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
return items;
|
||||
}, [uiCustomization]);
|
||||
|
||||
const { isMobile } = useSidebar();
|
||||
|
||||
return (
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<SidebarMenuButton>
|
||||
<LifeBuoy className="h-4 w-4" />
|
||||
Support
|
||||
</SidebarMenuButton>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent
|
||||
side={isMobile ? "bottom" : "right"}
|
||||
align="end"
|
||||
sideOffset={4}
|
||||
>
|
||||
{supportMenuItems.map((item, index) => {
|
||||
if (item === "separator") {
|
||||
return <DropdownMenuSeparator key={`separator-${index}`} />;
|
||||
}
|
||||
const url = item.customizableHref
|
||||
? (uiCustomization?.[item.customizableHref] ?? item.pathname)
|
||||
: item.pathname;
|
||||
return (
|
||||
<DropdownMenuItem key={item.title}>
|
||||
{item.menuNode ?? (
|
||||
<a
|
||||
href={url}
|
||||
target={url.startsWith("http") ? "_blank" : undefined}
|
||||
rel={
|
||||
url.startsWith("http") ? "noopener noreferrer" : undefined
|
||||
}
|
||||
className="flex cursor-pointer items-center"
|
||||
>
|
||||
<item.icon className="mr-2 h-4 w-4" />
|
||||
{item.title}
|
||||
{url.startsWith("http") && (
|
||||
<ArrowUpRight className="ml-1 h-3 w-3" />
|
||||
)}
|
||||
</a>
|
||||
)}
|
||||
</DropdownMenuItem>
|
||||
);
|
||||
})}
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
);
|
||||
};
|
||||
@@ -160,8 +160,12 @@ function ColumnVisibilityDropdownItem<TData, TValue>({
|
||||
|
||||
function GroupVisibilityDropdownHeader<TData, TValue>({
|
||||
column,
|
||||
groupTotalCount,
|
||||
groupVisibleCount,
|
||||
}: {
|
||||
column: LangfuseColumnDef<TData, TValue>;
|
||||
groupTotalCount: number;
|
||||
groupVisibleCount: number;
|
||||
}) {
|
||||
const { attributes, isDragging, listeners, setNodeRef, transform } =
|
||||
useSortable({
|
||||
@@ -189,6 +193,9 @@ function GroupVisibilityDropdownHeader<TData, TValue>({
|
||||
? column.header
|
||||
: column.accessorKey}
|
||||
</span>
|
||||
<span className="ml-1.5 text-xs text-muted-foreground">
|
||||
({groupVisibleCount}/{groupTotalCount})
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex items-center">
|
||||
<Button
|
||||
@@ -352,7 +359,11 @@ export function DataTableColumnVisibilityFilter<TData, TValue>({
|
||||
return (
|
||||
<DropdownMenuSub key={index}>
|
||||
{isColumnOrderingEnabled ? (
|
||||
<GroupVisibilityDropdownHeader column={column} />
|
||||
<GroupVisibilityDropdownHeader
|
||||
column={column}
|
||||
groupTotalCount={groupTotalCount}
|
||||
groupVisibleCount={groupVisibleCount}
|
||||
/>
|
||||
) : (
|
||||
<DropdownMenuSubTrigger hasCustomIcon>
|
||||
<Component className="mr-2 h-4 w-4 opacity-50" />
|
||||
|
||||
@@ -422,9 +422,9 @@ function TableBodyComponent<TData>({
|
||||
</TableRow>
|
||||
))
|
||||
) : (
|
||||
<TableRow>
|
||||
<TableCell colSpan={columns.length} className="h-24 text-center">
|
||||
<div>
|
||||
<TableRow className="hover:bg-transparent">
|
||||
<TableCell colSpan={columns.length} className="relative h-24">
|
||||
<div className="pointer-events-none fixed left-[50%] flex -translate-y-1/2 items-center justify-center">
|
||||
No results.{" "}
|
||||
{help && (
|
||||
<DocPopup description={help.description} href={help.href} />
|
||||
|
||||
@@ -249,7 +249,7 @@ export function ScoreConfigsTable({ projectId }: { projectId: string }) {
|
||||
rowHeight={rowHeight}
|
||||
setRowHeight={setRowHeight}
|
||||
/>
|
||||
<Card className="mb-4 flex max-h-[calc(100dvh-30rem)] flex-col overflow-hidden">
|
||||
<Card className="mb-4 flex max-h-[60dvh] flex-col overflow-hidden">
|
||||
<DataTable
|
||||
columns={columns}
|
||||
data={
|
||||
|
||||
@@ -257,6 +257,7 @@ export const OpenAiMessageView: React.FC<{
|
||||
messages: z.infer<typeof ChatMlArraySchema>;
|
||||
title?: string;
|
||||
shouldRenderMarkdown?: boolean;
|
||||
collapseLongHistory?: boolean;
|
||||
media?: MediaReturnType[];
|
||||
additionalInput?: Record<string, unknown>;
|
||||
}> = ({
|
||||
@@ -264,11 +265,12 @@ export const OpenAiMessageView: React.FC<{
|
||||
messages,
|
||||
shouldRenderMarkdown = false,
|
||||
media,
|
||||
collapseLongHistory = true,
|
||||
additionalInput,
|
||||
}) => {
|
||||
const COLLAPSE_THRESHOLD = 3;
|
||||
const [isCollapsed, setCollapsed] = useState(
|
||||
messages.length > COLLAPSE_THRESHOLD ? true : null,
|
||||
collapseLongHistory && messages.length > COLLAPSE_THRESHOLD ? true : null,
|
||||
);
|
||||
|
||||
return (
|
||||
|
||||
@@ -152,8 +152,8 @@ export const ObservationPreview = ({
|
||||
<div className="flex w-full flex-col overflow-y-auto">
|
||||
<CardHeader className="flex flex-row flex-wrap justify-between gap-2">
|
||||
<div className="flex flex-col gap-1">
|
||||
<CardTitle>
|
||||
<span className="mr-2 rounded-sm bg-input p-1 text-xs">
|
||||
<CardTitle className="flex flex-row items-center gap-2">
|
||||
<span className="rounded-sm bg-input p-1 text-xs">
|
||||
{preloadedObservation.type}
|
||||
</span>
|
||||
<span>{preloadedObservation.name}</span>
|
||||
@@ -290,55 +290,58 @@ export const ObservationPreview = ({
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
{viewType === "detailed" && (
|
||||
<div className="flex flex-wrap gap-2">
|
||||
<CommentDrawerButton
|
||||
projectId={preloadedObservation.projectId}
|
||||
objectId={preloadedObservation.id}
|
||||
objectType="OBSERVATION"
|
||||
count={commentCounts?.get(preloadedObservation.id)}
|
||||
/>
|
||||
<div className="flex items-start">
|
||||
<AnnotateDrawer
|
||||
key={"annotation-drawer" + preloadedObservation.id}
|
||||
projectId={projectId}
|
||||
traceId={traceId}
|
||||
observationId={preloadedObservation.id}
|
||||
scores={scores}
|
||||
emptySelectedConfigIds={emptySelectedConfigIds}
|
||||
setEmptySelectedConfigIds={setEmptySelectedConfigIds}
|
||||
type="observation"
|
||||
hasGroupedButton={hasEntitlement}
|
||||
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{viewType === "detailed" && (
|
||||
<>
|
||||
<CommentDrawerButton
|
||||
projectId={preloadedObservation.projectId}
|
||||
objectId={preloadedObservation.id}
|
||||
objectType="OBSERVATION"
|
||||
count={commentCounts?.get(preloadedObservation.id)}
|
||||
/>
|
||||
{hasEntitlement && (
|
||||
<CreateNewAnnotationQueueItem
|
||||
<div className="flex items-start">
|
||||
<AnnotateDrawer
|
||||
key={"annotation-drawer" + preloadedObservation.id}
|
||||
projectId={projectId}
|
||||
objectId={preloadedObservation.id}
|
||||
objectType={AnnotationQueueObjectType.OBSERVATION}
|
||||
traceId={traceId}
|
||||
observationId={preloadedObservation.id}
|
||||
scores={scores}
|
||||
emptySelectedConfigIds={emptySelectedConfigIds}
|
||||
setEmptySelectedConfigIds={setEmptySelectedConfigIds}
|
||||
type="observation"
|
||||
hasGroupedButton={hasEntitlement}
|
||||
/>
|
||||
{hasEntitlement && (
|
||||
<CreateNewAnnotationQueueItem
|
||||
projectId={projectId}
|
||||
objectId={preloadedObservation.id}
|
||||
objectType={AnnotationQueueObjectType.OBSERVATION}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{observationWithInputAndOutput.data?.type === "GENERATION" && (
|
||||
<JumpToPlaygroundButton
|
||||
source="generation"
|
||||
generation={observationWithInputAndOutput.data}
|
||||
analyticsEventName="trace_detail:test_in_playground_button_click"
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{observationWithInputAndOutput.data?.type === "GENERATION" && (
|
||||
<JumpToPlaygroundButton
|
||||
source="generation"
|
||||
generation={observationWithInputAndOutput.data}
|
||||
analyticsEventName="trace_detail:test_in_playground_button_click"
|
||||
/>
|
||||
)}
|
||||
{observationWithInputAndOutput.data ? (
|
||||
<NewDatasetItemFromTrace
|
||||
traceId={preloadedObservation.traceId}
|
||||
observationId={preloadedObservation.id}
|
||||
projectId={projectId}
|
||||
input={observationWithInputAndOutput.data.input}
|
||||
output={observationWithInputAndOutput.data.output}
|
||||
metadata={observationWithInputAndOutput.data.metadata}
|
||||
key={preloadedObservation.id}
|
||||
/>
|
||||
) : null}
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
{observationWithInputAndOutput.data ? (
|
||||
<NewDatasetItemFromTrace
|
||||
traceId={preloadedObservation.traceId}
|
||||
observationId={preloadedObservation.id}
|
||||
projectId={projectId}
|
||||
input={observationWithInputAndOutput.data.input}
|
||||
output={observationWithInputAndOutput.data.output}
|
||||
metadata={observationWithInputAndOutput.data.metadata}
|
||||
key={preloadedObservation.id}
|
||||
/>
|
||||
) : null}
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent className="flex flex-col gap-4">
|
||||
{selectedTab === "preview" && (
|
||||
|
||||
@@ -1,12 +1,23 @@
|
||||
import { type NestedObservation } from "@/src/utils/types";
|
||||
import { cn } from "@/src/utils/tailwind";
|
||||
import { type APIScore, type Trace, type $Enums } from "@langfuse/shared";
|
||||
import {
|
||||
type APIScore,
|
||||
type Trace,
|
||||
type $Enums,
|
||||
ObservationLevel,
|
||||
} from "@langfuse/shared";
|
||||
import { GroupedScoreBadges } from "@/src/components/grouped-score-badge";
|
||||
import { Fragment, useMemo } from "react";
|
||||
import { type ObservationReturnType } from "@/src/server/api/routers/traces";
|
||||
import { LevelColors } from "@/src/components/level-colors";
|
||||
import { formatIntervalSeconds } from "@/src/utils/dates";
|
||||
import { MinusCircle, MinusIcon, PlusCircleIcon, PlusIcon } from "lucide-react";
|
||||
import {
|
||||
InfoIcon,
|
||||
MinusCircle,
|
||||
MinusIcon,
|
||||
PlusCircleIcon,
|
||||
PlusIcon,
|
||||
} from "lucide-react";
|
||||
import { Toggle } from "@/src/components/ui/toggle";
|
||||
import { Button } from "@/src/components/ui/button";
|
||||
import { usePostHogClientCapture } from "@/src/features/posthog-analytics/usePostHogClientCapture";
|
||||
@@ -39,10 +50,12 @@ export const ObservationTree = ({
|
||||
traceCommentCounts?: Map<string, number>;
|
||||
className?: string;
|
||||
showExpandControls?: boolean;
|
||||
minLevel?: ObservationLevel;
|
||||
setMinLevel?: React.Dispatch<React.SetStateAction<ObservationLevel>>;
|
||||
}) => {
|
||||
const nestedObservations = useMemo(
|
||||
() => nestObservations(props.observations),
|
||||
[props.observations],
|
||||
const { nestedObservations, hiddenObservationsCount } = useMemo(
|
||||
() => nestObservations(props.observations, props.minLevel),
|
||||
[props.observations, props.minLevel],
|
||||
);
|
||||
const totalCost = useMemo(() => {
|
||||
return calculateDisplayTotalCost({
|
||||
@@ -82,6 +95,23 @@ export const ObservationTree = ({
|
||||
props.trace.latency ? props.trace.latency * 1000 : undefined
|
||||
}
|
||||
/>
|
||||
{props.minLevel && hiddenObservationsCount > 0 ? (
|
||||
<span className="flex items-center gap-1 p-2 py-4">
|
||||
<InfoIcon className="h-4 w-4 text-muted-foreground" />
|
||||
<span className="flex flex-row gap-1 text-sm text-muted-foreground">
|
||||
<p>
|
||||
{hiddenObservationsCount} observations below {props.minLevel}{" "}
|
||||
level are hidden.
|
||||
</p>
|
||||
<p
|
||||
className="cursor-pointer underline"
|
||||
onClick={() => props.setMinLevel?.(ObservationLevel.DEBUG)}
|
||||
>
|
||||
Show all
|
||||
</p>
|
||||
</span>
|
||||
</span>
|
||||
) : null}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -223,6 +253,7 @@ const ObservationTreeNode = (props: {
|
||||
{props.comments ? (
|
||||
<CommentCountIcon
|
||||
count={props.comments.get(observation.id)}
|
||||
className={treeItemColors.get(observation.type)}
|
||||
/>
|
||||
) : null}
|
||||
</div>
|
||||
|
||||
@@ -169,42 +169,45 @@ export const TracePreview = ({
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
{viewType === "detailed" && (
|
||||
<div className="flex flex-wrap gap-2">
|
||||
<CommentDrawerButton
|
||||
projectId={trace.projectId}
|
||||
objectId={trace.id}
|
||||
objectType="TRACE"
|
||||
count={commentCounts?.get(trace.id)}
|
||||
/>
|
||||
<div className="flex items-start">
|
||||
<AnnotateDrawer
|
||||
key={"annotation-drawer" + trace.id}
|
||||
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{viewType === "detailed" && (
|
||||
<>
|
||||
<CommentDrawerButton
|
||||
projectId={trace.projectId}
|
||||
traceId={trace.id}
|
||||
scores={scores}
|
||||
emptySelectedConfigIds={emptySelectedConfigIds}
|
||||
setEmptySelectedConfigIds={setEmptySelectedConfigIds}
|
||||
hasGroupedButton={hasEntitlement}
|
||||
objectId={trace.id}
|
||||
objectType="TRACE"
|
||||
count={commentCounts?.get(trace.id)}
|
||||
/>
|
||||
{hasEntitlement && (
|
||||
<CreateNewAnnotationQueueItem
|
||||
<div className="flex items-start">
|
||||
<AnnotateDrawer
|
||||
key={"annotation-drawer" + trace.id}
|
||||
projectId={trace.projectId}
|
||||
objectId={trace.id}
|
||||
objectType={AnnotationQueueObjectType.TRACE}
|
||||
traceId={trace.id}
|
||||
scores={scores}
|
||||
emptySelectedConfigIds={emptySelectedConfigIds}
|
||||
setEmptySelectedConfigIds={setEmptySelectedConfigIds}
|
||||
hasGroupedButton={hasEntitlement}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
<NewDatasetItemFromTrace
|
||||
traceId={trace.id}
|
||||
projectId={trace.projectId}
|
||||
input={trace.input}
|
||||
output={trace.output}
|
||||
metadata={trace.metadata}
|
||||
key={trace.id}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
{hasEntitlement && (
|
||||
<CreateNewAnnotationQueueItem
|
||||
projectId={trace.projectId}
|
||||
objectId={trace.id}
|
||||
objectType={AnnotationQueueObjectType.TRACE}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
<NewDatasetItemFromTrace
|
||||
traceId={trace.id}
|
||||
projectId={trace.projectId}
|
||||
input={trace.input}
|
||||
output={trace.output}
|
||||
metadata={trace.metadata}
|
||||
key={trace.id}
|
||||
/>
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent className="flex flex-col gap-4">
|
||||
{selectedTab === "preview" && (
|
||||
|
||||
@@ -304,7 +304,7 @@ export function TraceTimelineView({
|
||||
};
|
||||
}, [parentRef]);
|
||||
|
||||
const nestedObservations = useMemo(
|
||||
const { nestedObservations } = useMemo(
|
||||
() => nestObservations(observations),
|
||||
[observations],
|
||||
);
|
||||
|
||||
@@ -5,6 +5,14 @@ import { TracePreview } from "./TracePreview";
|
||||
|
||||
import Header from "@/src/components/layouts/header";
|
||||
import { Badge } from "@/src/components/ui/badge";
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectLabel,
|
||||
SelectGroup,
|
||||
} from "@/src/components/ui/select";
|
||||
import { AggUsageBadge } from "@/src/components/token-usage-badge";
|
||||
import { StringParam, useQueryParam, withDefault } from "use-query-params";
|
||||
import { PublishTraceSwitch } from "@/src/components/publish-object-switch";
|
||||
@@ -22,6 +30,7 @@ import {
|
||||
Award,
|
||||
ChevronsDownUp,
|
||||
ChevronsUpDown,
|
||||
FilterIcon,
|
||||
ListTree,
|
||||
Network,
|
||||
Percent,
|
||||
@@ -31,7 +40,7 @@ import { useCallback, useState } from "react";
|
||||
import { DeleteButton } from "@/src/components/deleteButton";
|
||||
import { usePostHogClientCapture } from "@/src/features/posthog-analytics/usePostHogClientCapture";
|
||||
import { TraceTimelineView } from "@/src/components/trace/TraceTimelineView";
|
||||
import { type APIScore } from "@langfuse/shared";
|
||||
import { type APIScore, ObservationLevel } from "@langfuse/shared";
|
||||
import { FullScreenPage } from "@/src/components/layouts/full-screen-page";
|
||||
import { calculateDisplayTotalCost } from "@/src/components/trace/lib/helpers";
|
||||
import { useIsAuthenticatedAndProjectMember } from "@/src/features/auth/hooks";
|
||||
@@ -74,6 +83,9 @@ export function Trace(props: {
|
||||
[],
|
||||
);
|
||||
|
||||
const [minObservationLevel, setMinObservationLevel] =
|
||||
useState<ObservationLevel>(ObservationLevel.DEFAULT);
|
||||
|
||||
const isAuthenticatedAndProjectMember = useIsAuthenticatedAndProjectMember(
|
||||
props.projectId,
|
||||
);
|
||||
@@ -223,6 +235,36 @@ export function Trace(props: {
|
||||
>
|
||||
<Percent className="h-4 w-4" />
|
||||
</Toggle>
|
||||
<Select
|
||||
onValueChange={(v: ObservationLevel) => setMinObservationLevel(v)}
|
||||
value={minObservationLevel}
|
||||
>
|
||||
<SelectTrigger
|
||||
hideDownIcon
|
||||
className="focus:ring-none h-auto w-auto border-none px-0 py-0 text-sm focus:outline-none focus:ring-0 focus:ring-offset-0"
|
||||
>
|
||||
<Toggle
|
||||
pressed={colorCodeMetricsOnObservationTree}
|
||||
onPressedChange={(e) => setColorCodeMetricsOnObservationTree(e)}
|
||||
size="xs"
|
||||
title="Color code metrics (>50% yellow, >75% red)"
|
||||
>
|
||||
<FilterIcon className="h-4 w-4" />
|
||||
</Toggle>
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectGroup>
|
||||
<SelectLabel className="py-1 text-sm font-semibold">
|
||||
Min. Level
|
||||
</SelectLabel>
|
||||
{Object.values(ObservationLevel).map((level) => (
|
||||
<SelectItem value={level} key={level}>
|
||||
{level}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectGroup>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
<ObservationTree
|
||||
@@ -240,6 +282,8 @@ export function Trace(props: {
|
||||
colorCodeMetrics={colorCodeMetricsOnObservationTree}
|
||||
observationCommentCounts={observationCommentCounts.data}
|
||||
traceCommentCounts={traceCommentCounts.data}
|
||||
minLevel={minObservationLevel}
|
||||
setMinLevel={setMinObservationLevel}
|
||||
className="flex w-full flex-col overflow-y-auto"
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { ObservationType } from "@langfuse/shared";
|
||||
import { ObservationLevel, ObservationType } from "@langfuse/shared";
|
||||
import { type NestedObservation } from "@/src/utils/types";
|
||||
import { type ObservationReturnType } from "@/src/server/api/routers/traces";
|
||||
import Decimal from "decimal.js";
|
||||
@@ -7,18 +7,29 @@ export type TreeItemType = ObservationType | "TRACE";
|
||||
|
||||
export const treeItemColors: Map<TreeItemType, string> = new Map([
|
||||
[ObservationType.SPAN, "bg-muted-blue"],
|
||||
[ObservationType.GENERATION, "bg-muted-orange"],
|
||||
[ObservationType.GENERATION, "bg-muted-magenta"],
|
||||
[ObservationType.EVENT, "bg-muted-green"],
|
||||
["TRACE", "bg-input"],
|
||||
]);
|
||||
|
||||
export function nestObservations(
|
||||
list: ObservationReturnType[],
|
||||
): NestedObservation[] {
|
||||
if (list.length === 0) return [];
|
||||
minLevel?: ObservationLevel,
|
||||
): {
|
||||
nestedObservations: NestedObservation[];
|
||||
hiddenObservationsCount: number;
|
||||
} {
|
||||
if (list.length === 0)
|
||||
return { nestedObservations: [], hiddenObservationsCount: 0 };
|
||||
|
||||
// Data prep:
|
||||
// - Filter for observations with minimum level
|
||||
// - Remove parentObservationId attribute from observations if the id does not exist in the list of observations
|
||||
const mutableList = list.filter((o) =>
|
||||
getObservationLevels(minLevel).includes(o.level),
|
||||
);
|
||||
const hiddenObservationsCount = list.length - mutableList.length;
|
||||
|
||||
// Data prep: Remove parentObservationId attribute from observations if the id does not exist in the list of observations
|
||||
const mutableList = list.map((o) => ({ ...o }));
|
||||
mutableList.forEach((observation) => {
|
||||
if (
|
||||
observation.parentObservationId &&
|
||||
@@ -61,7 +72,10 @@ export function nestObservations(
|
||||
}
|
||||
|
||||
// Step 5: Return the roots.
|
||||
return Array.from(roots.values());
|
||||
return {
|
||||
nestedObservations: Array.from(roots.values()),
|
||||
hiddenObservationsCount,
|
||||
};
|
||||
}
|
||||
|
||||
export function calculateDisplayTotalCost(p: {
|
||||
@@ -114,7 +128,9 @@ export function calculateDisplayTotalCost(p: {
|
||||
curr.calculatedOutputCost ?? new Decimal(0),
|
||||
),
|
||||
)
|
||||
: curr.calculatedInputCost ?? curr.calculatedOutputCost ?? undefined;
|
||||
: (curr.calculatedInputCost ??
|
||||
curr.calculatedOutputCost ??
|
||||
undefined);
|
||||
}
|
||||
|
||||
if (!curr.calculatedTotalCost) return prev;
|
||||
@@ -129,3 +145,18 @@ export function calculateDisplayTotalCost(p: {
|
||||
|
||||
return totalCost;
|
||||
}
|
||||
|
||||
function getObservationLevels(minLevel: ObservationLevel | undefined) {
|
||||
const ascendingLevels = [
|
||||
ObservationLevel.DEBUG,
|
||||
ObservationLevel.DEFAULT,
|
||||
ObservationLevel.WARNING,
|
||||
ObservationLevel.ERROR,
|
||||
];
|
||||
|
||||
if (!minLevel) return ascendingLevels;
|
||||
|
||||
const minLevelIndex = ascendingLevels.indexOf(minLevel);
|
||||
|
||||
return ascendingLevels.slice(minLevelIndex);
|
||||
}
|
||||
|
||||
@@ -14,7 +14,9 @@ const SelectValue = SelectPrimitive.Value;
|
||||
|
||||
const SelectTrigger = React.forwardRef<
|
||||
React.ElementRef<typeof SelectPrimitive.Trigger>,
|
||||
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Trigger>
|
||||
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Trigger> & {
|
||||
hideDownIcon?: boolean;
|
||||
}
|
||||
>(({ className, children, ...props }, ref) => (
|
||||
<SelectPrimitive.Trigger
|
||||
ref={ref}
|
||||
@@ -25,9 +27,11 @@ const SelectTrigger = React.forwardRef<
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
<SelectPrimitive.Icon asChild>
|
||||
<ChevronDown className="h-4 w-4 opacity-50" />
|
||||
</SelectPrimitive.Icon>
|
||||
{props.hideDownIcon ? null : (
|
||||
<SelectPrimitive.Icon asChild>
|
||||
<ChevronDown className="h-4 w-4 opacity-50" />
|
||||
</SelectPrimitive.Icon>
|
||||
)}
|
||||
</SelectPrimitive.Trigger>
|
||||
));
|
||||
SelectTrigger.displayName = SelectPrimitive.Trigger.displayName;
|
||||
|
||||
@@ -1 +1 @@
|
||||
export const VERSION = "v3.6.2";
|
||||
export const VERSION = "v3.9.0";
|
||||
|
||||
@@ -5,7 +5,6 @@ import {
|
||||
createTRPCRouter,
|
||||
protectedProjectProcedure,
|
||||
} from "@/src/server/api/trpc";
|
||||
import { measureAndReturnApi } from "@/src/server/utils/checkClickhouseAccess";
|
||||
import {
|
||||
type AnnotationQueueItem,
|
||||
AnnotationQueueObjectType,
|
||||
@@ -79,38 +78,14 @@ export const queueItemRouter = createTRPCRouter({
|
||||
};
|
||||
|
||||
if (item.objectType === AnnotationQueueObjectType.OBSERVATION) {
|
||||
await measureAndReturnApi({
|
||||
input: { projectId: input.projectId, queryClickhouse: false },
|
||||
operation: "annotations.byId",
|
||||
user: ctx.session.user,
|
||||
pgExecution: async () => {
|
||||
const observation = await ctx.prisma.observation.findUnique({
|
||||
where: {
|
||||
id: item.objectId,
|
||||
projectId: input.projectId,
|
||||
},
|
||||
select: {
|
||||
id: true,
|
||||
traceId: true,
|
||||
},
|
||||
});
|
||||
|
||||
return {
|
||||
...inflatedItem,
|
||||
parentTraceId: observation?.traceId,
|
||||
};
|
||||
},
|
||||
clickhouseExecution: async () => {
|
||||
const clickhouseObservation = await getObservationById(
|
||||
item.objectId,
|
||||
input.projectId,
|
||||
);
|
||||
return {
|
||||
...inflatedItem,
|
||||
parentTraceId: clickhouseObservation?.traceId,
|
||||
};
|
||||
},
|
||||
});
|
||||
const clickhouseObservation = await getObservationById(
|
||||
item.objectId,
|
||||
input.projectId,
|
||||
);
|
||||
return {
|
||||
...inflatedItem,
|
||||
parentTraceId: clickhouseObservation?.traceId,
|
||||
};
|
||||
}
|
||||
|
||||
return inflatedItem;
|
||||
@@ -147,131 +122,68 @@ export const queueItemRouter = createTRPCRouter({
|
||||
scope: "annotationQueues:read",
|
||||
});
|
||||
|
||||
return await measureAndReturnApi({
|
||||
input: { projectId: input.projectId, queryClickhouse: false },
|
||||
operation: "annotationQueueItems.itemsByQueue",
|
||||
user: ctx.session.user,
|
||||
pgExecution: async () => {
|
||||
const [queueItems, totalItems] = await Promise.all([
|
||||
// queueItems
|
||||
ctx.prisma.$queryRaw<
|
||||
Array<{
|
||||
id: string;
|
||||
status: AnnotationQueueStatus;
|
||||
objectId: string;
|
||||
objectType: AnnotationQueueObjectType;
|
||||
parentTraceId: string | null;
|
||||
completedAt: string | null;
|
||||
annotatorUserId: string | null;
|
||||
annotatorUserImage: string | null;
|
||||
annotatorUserName: string | null;
|
||||
}>
|
||||
>(Prisma.sql`
|
||||
SELECT
|
||||
aqi.id,
|
||||
aqi.status,
|
||||
aqi.object_id AS "objectId",
|
||||
aqi.object_type AS "objectType",
|
||||
o.trace_id AS "parentTraceId",
|
||||
aqi.completed_at AS "completedAt",
|
||||
aqi.annotator_user_id AS "annotatorUserId",
|
||||
u.image AS "annotatorUserImage",
|
||||
u.name AS "annotatorUserName"
|
||||
FROM
|
||||
annotation_queue_items aqi
|
||||
LEFT JOIN
|
||||
observations o ON o.id = aqi.object_id AND aqi.object_type = 'OBSERVATION' AND o.project_id = ${input.projectId}
|
||||
LEFT JOIN
|
||||
users u ON u.id = aqi.annotator_user_id AND u.id in (SELECT user_id FROM organization_memberships WHERE org_id = ${ctx.session.orgId})
|
||||
WHERE
|
||||
aqi.project_id = ${input.projectId} AND aqi.queue_id = ${input.queueId}
|
||||
ORDER BY
|
||||
aqi.created_at ASC,
|
||||
aqi.object_id ASC,
|
||||
aqi.object_type ASC
|
||||
${input.limit ? Prisma.sql`LIMIT ${input.limit}` : Prisma.empty}
|
||||
${input.page && input.limit ? Prisma.sql`OFFSET ${input.page * input.limit}` : Prisma.empty}
|
||||
`),
|
||||
// totalItems
|
||||
ctx.prisma.annotationQueueItem.count({
|
||||
where: {
|
||||
queueId: input.queueId,
|
||||
projectId: input.projectId,
|
||||
},
|
||||
}),
|
||||
]);
|
||||
const [queueItems, totalItems] = await Promise.all([
|
||||
await ctx.prisma.$queryRaw<
|
||||
Array<{
|
||||
id: string;
|
||||
status: AnnotationQueueStatus;
|
||||
objectId: string;
|
||||
objectType: AnnotationQueueObjectType;
|
||||
completedAt: string | null;
|
||||
annotatorUserId: string | null;
|
||||
annotatorUserImage: string | null;
|
||||
annotatorUserName: string | null;
|
||||
}>
|
||||
>(Prisma.sql`
|
||||
SELECT
|
||||
aqi.id,
|
||||
aqi.status,
|
||||
aqi.object_id AS "objectId",
|
||||
aqi.object_type AS "objectType",
|
||||
aqi.completed_at AS "completedAt",
|
||||
aqi.annotator_user_id AS "annotatorUserId",
|
||||
u.image AS "annotatorUserImage",
|
||||
u.name AS "annotatorUserName"
|
||||
FROM
|
||||
annotation_queue_items aqi
|
||||
LEFT JOIN
|
||||
users u ON u.id = aqi.annotator_user_id AND u.id in (SELECT user_id FROM organization_memberships WHERE org_id = ${ctx.session.orgId})
|
||||
WHERE
|
||||
aqi.project_id = ${input.projectId} AND aqi.queue_id = ${input.queueId}
|
||||
ORDER BY
|
||||
aqi.created_at ASC,
|
||||
aqi.object_id ASC,
|
||||
aqi.object_type ASC
|
||||
${input.limit ? Prisma.sql`LIMIT ${input.limit}` : Prisma.empty}
|
||||
${input.page && input.limit ? Prisma.sql`OFFSET ${input.page * input.limit}` : Prisma.empty}
|
||||
`),
|
||||
ctx.prisma.annotationQueueItem.count({
|
||||
where: {
|
||||
queueId: input.queueId,
|
||||
projectId: input.projectId,
|
||||
},
|
||||
}),
|
||||
]);
|
||||
|
||||
return { queueItems, totalItems };
|
||||
},
|
||||
clickhouseExecution: async () => {
|
||||
const [queueItems, totalItems] = await Promise.all([
|
||||
await ctx.prisma.$queryRaw<
|
||||
Array<{
|
||||
id: string;
|
||||
status: AnnotationQueueStatus;
|
||||
objectId: string;
|
||||
objectType: AnnotationQueueObjectType;
|
||||
completedAt: string | null;
|
||||
annotatorUserId: string | null;
|
||||
annotatorUserImage: string | null;
|
||||
annotatorUserName: string | null;
|
||||
}>
|
||||
>(Prisma.sql`
|
||||
SELECT
|
||||
aqi.id,
|
||||
aqi.status,
|
||||
aqi.object_id AS "objectId",
|
||||
aqi.object_type AS "objectType",
|
||||
aqi.completed_at AS "completedAt",
|
||||
aqi.annotator_user_id AS "annotatorUserId",
|
||||
u.image AS "annotatorUserImage",
|
||||
u.name AS "annotatorUserName"
|
||||
FROM
|
||||
annotation_queue_items aqi
|
||||
LEFT JOIN
|
||||
users u ON u.id = aqi.annotator_user_id AND u.id in (SELECT user_id FROM organization_memberships WHERE org_id = ${ctx.session.orgId})
|
||||
WHERE
|
||||
aqi.project_id = ${input.projectId} AND aqi.queue_id = ${input.queueId}
|
||||
ORDER BY
|
||||
aqi.created_at ASC,
|
||||
aqi.object_id ASC,
|
||||
aqi.object_type ASC
|
||||
${input.limit ? Prisma.sql`LIMIT ${input.limit}` : Prisma.empty}
|
||||
${input.page && input.limit ? Prisma.sql`OFFSET ${input.page * input.limit}` : Prisma.empty}
|
||||
`),
|
||||
ctx.prisma.annotationQueueItem.count({
|
||||
where: {
|
||||
queueId: input.queueId,
|
||||
projectId: input.projectId,
|
||||
},
|
||||
}),
|
||||
]);
|
||||
const observationIds = queueItems
|
||||
.filter(
|
||||
(item) => item.objectType === AnnotationQueueObjectType.OBSERVATION,
|
||||
)
|
||||
.map((item) => item.objectId);
|
||||
|
||||
const observationIds = queueItems
|
||||
.filter(
|
||||
(item) =>
|
||||
item.objectType === AnnotationQueueObjectType.OBSERVATION,
|
||||
)
|
||||
.map((item) => item.objectId);
|
||||
const traceIds =
|
||||
observationIds.length > 0
|
||||
? await getTraceIdsForObservations(input.projectId, observationIds)
|
||||
: [];
|
||||
|
||||
const traceIds =
|
||||
observationIds.length > 0
|
||||
? await getTraceIdsForObservations(
|
||||
input.projectId,
|
||||
observationIds,
|
||||
)
|
||||
: [];
|
||||
|
||||
return {
|
||||
queueItems: queueItems.map((item) => ({
|
||||
...item,
|
||||
parentTraceId:
|
||||
traceIds.find((t) => t.id === item.objectId)?.traceId || null,
|
||||
})),
|
||||
totalItems,
|
||||
};
|
||||
},
|
||||
});
|
||||
return {
|
||||
queueItems: queueItems.map((item) => ({
|
||||
...item,
|
||||
parentTraceId:
|
||||
traceIds.find((t) => t.id === item.objectId)?.traceId || null,
|
||||
})),
|
||||
totalItems,
|
||||
};
|
||||
} catch (error) {
|
||||
logger.error(error);
|
||||
if (error instanceof TRPCError) {
|
||||
|
||||
@@ -5,7 +5,6 @@ import {
|
||||
createTRPCRouter,
|
||||
protectedProjectProcedure,
|
||||
} from "@/src/server/api/trpc";
|
||||
import { measureAndReturnApi } from "@/src/server/utils/checkClickhouseAccess";
|
||||
import {
|
||||
AnnotationQueueObjectType,
|
||||
AnnotationQueueStatus,
|
||||
@@ -530,38 +529,14 @@ export const queueRouter = createTRPCRouter({
|
||||
};
|
||||
|
||||
if (item.objectType === AnnotationQueueObjectType.OBSERVATION) {
|
||||
await measureAndReturnApi({
|
||||
input: { projectId: input.projectId, queryClickhouse: false },
|
||||
operation: "fetchAndLockNext",
|
||||
user: ctx.session.user,
|
||||
pgExecution: async () => {
|
||||
const observation = await ctx.prisma.observation.findUnique({
|
||||
where: {
|
||||
id: item.objectId,
|
||||
projectId: input.projectId,
|
||||
},
|
||||
select: {
|
||||
id: true,
|
||||
traceId: true,
|
||||
},
|
||||
});
|
||||
|
||||
return {
|
||||
...inflatedUpdatedItem,
|
||||
parentTraceId: observation?.traceId,
|
||||
};
|
||||
},
|
||||
clickhouseExecution: async () => {
|
||||
const clickhouseObservation = await getObservationById(
|
||||
item.objectId,
|
||||
input.projectId,
|
||||
);
|
||||
return {
|
||||
...inflatedUpdatedItem,
|
||||
parentTraceId: clickhouseObservation?.traceId,
|
||||
};
|
||||
},
|
||||
});
|
||||
const clickhouseObservation = await getObservationById(
|
||||
item.objectId,
|
||||
input.projectId,
|
||||
);
|
||||
return {
|
||||
...inflatedUpdatedItem,
|
||||
parentTraceId: clickhouseObservation?.traceId,
|
||||
};
|
||||
}
|
||||
|
||||
return inflatedUpdatedItem;
|
||||
|
||||
@@ -30,7 +30,6 @@ import { EvalReferencedEvaluators } from "@/src/ee/features/evals/types";
|
||||
import { EvaluatorStatus } from "../types";
|
||||
import { traceException } from "@langfuse/shared/src/server";
|
||||
import { isNotNullOrUndefined } from "@/src/utils/types";
|
||||
import { measureAndReturnApi } from "@/src/server/utils/checkClickhouseAccess";
|
||||
|
||||
const APIEvaluatorSchema = z.object({
|
||||
id: z.string(),
|
||||
@@ -730,22 +729,7 @@ export const evalRouter = createTRPCRouter({
|
||||
|
||||
const scores =
|
||||
scoreIds.length > 0
|
||||
? await measureAndReturnApi({
|
||||
input: { projectId: input.projectId, queryClickhouse: false },
|
||||
operation: "get-scores-eval-log",
|
||||
user: ctx.session.user,
|
||||
pgExecution: async () => {
|
||||
return await ctx.prisma.score.findMany({
|
||||
where: {
|
||||
projectId: input.projectId,
|
||||
id: { in: scoreIds },
|
||||
},
|
||||
});
|
||||
},
|
||||
clickhouseExecution: async () => {
|
||||
return await getScoresByIds(input.projectId, scoreIds);
|
||||
},
|
||||
})
|
||||
? await getScoresByIds(input.projectId, scoreIds)
|
||||
: [];
|
||||
|
||||
return {
|
||||
|
||||
@@ -42,7 +42,8 @@ export const JumpToPlaygroundButton: React.FC<JumpToPlaygroundButtonProps> = (
|
||||
const projectId = useProjectIdFromURL();
|
||||
const { setPlaygroundCache } = usePlaygroundCache();
|
||||
const [capturedState, setCapturedState] = useState<PlaygroundCache>(null);
|
||||
const available = useHasEntitlement("playground");
|
||||
const [isAvailable, setIsAvailable] = useState<boolean>(false);
|
||||
const isEntitled = useHasEntitlement("playground");
|
||||
|
||||
useEffect(() => {
|
||||
if (props.source === "prompt") {
|
||||
@@ -52,26 +53,33 @@ export const JumpToPlaygroundButton: React.FC<JumpToPlaygroundButtonProps> = (
|
||||
}
|
||||
}, [props]);
|
||||
|
||||
useEffect(() => {
|
||||
if (capturedState && isEntitled) {
|
||||
setIsAvailable(true);
|
||||
} else {
|
||||
setIsAvailable(false);
|
||||
}
|
||||
}, [capturedState, isEntitled, setIsAvailable]);
|
||||
|
||||
const handleClick = () => {
|
||||
capture(props.analyticsEventName);
|
||||
setPlaygroundCache(capturedState);
|
||||
};
|
||||
|
||||
if (!available) return null;
|
||||
if (!isAvailable) return null;
|
||||
|
||||
return (
|
||||
<Button
|
||||
variant={props.variant ?? "secondary"}
|
||||
size={props.source === "prompt" ? "icon" : "default"}
|
||||
title="Test in LLM playground"
|
||||
onClick={handleClick}
|
||||
asChild
|
||||
>
|
||||
<Link href={`/project/${projectId}/playground`}>
|
||||
<Terminal className="h-4 w-4" />
|
||||
{props.source === "generation" && (
|
||||
<span className="ml-2">Test in playground</span>
|
||||
)}
|
||||
<span className="ml-2">
|
||||
{props.source === "generation" ? "Test in playground" : "Playground"}
|
||||
</span>
|
||||
</Link>
|
||||
</Button>
|
||||
);
|
||||
|
||||
@@ -232,24 +232,6 @@ export const env = createEnv({
|
||||
);
|
||||
}, "LANGFUSE_ALLOWED_ORGANIZATION_CREATORS must be a comma separated list of valid email addresses"),
|
||||
|
||||
// TODO: Remove entire block during V3 clean up
|
||||
// Settings to toggle Clickhouse vs Postgres behaviour
|
||||
LANGFUSE_READ_FROM_POSTGRES_ONLY: z
|
||||
.enum(["true", "false"])
|
||||
.default("false"),
|
||||
LANGFUSE_RETURN_FROM_CLICKHOUSE: z.enum(["true", "false"]).default("true"),
|
||||
LANGFUSE_EXPERIMENT_EXCLUDED_PROJECT_IDS: z.string().optional(),
|
||||
LANGFUSE_EXPERIMENT_EXCLUDED_OPERATIONS: z.string().optional(),
|
||||
LANGFUSE_READ_DASHBOARDS_FROM_CLICKHOUSE: z
|
||||
.enum(["true", "false"])
|
||||
.default("true"),
|
||||
LANGFUSE_READ_FROM_CLICKHOUSE_ONLY: z
|
||||
.enum(["true", "false"])
|
||||
.default("true"),
|
||||
LANGFUSE_POSTGRES_INGESTION_ENABLED: z
|
||||
.enum(["true", "false"])
|
||||
.default("false"),
|
||||
|
||||
STRIPE_SECRET_KEY: z.string().optional(),
|
||||
STRIPE_WEBHOOK_SIGNING_SECRET: z.string().optional(),
|
||||
SENTRY_AUTH_TOKEN: z.string().optional(),
|
||||
@@ -314,8 +296,6 @@ export const env = createEnv({
|
||||
NEXT_PUBLIC_SIGN_UP_DISABLED: process.env.NEXT_PUBLIC_SIGN_UP_DISABLED,
|
||||
LANGFUSE_ENABLE_EXPERIMENTAL_FEATURES:
|
||||
process.env.LANGFUSE_ENABLE_EXPERIMENTAL_FEATURES,
|
||||
LANGFUSE_EXPERIMENT_EXCLUDED_OPERATIONS:
|
||||
process.env.LANGFUSE_EXPERIMENT_EXCLUDED_OPERATIONS,
|
||||
LANGFUSE_DISABLE_EXPENSIVE_POSTGRES_QUERIES:
|
||||
process.env.LANGFUSE_DISABLE_EXPENSIVE_POSTGRES_QUERIES,
|
||||
LANGFUSE_TEAM_SLACK_WEBHOOK: process.env.LANGFUSE_TEAM_SLACK_WEBHOOK,
|
||||
@@ -484,18 +464,6 @@ export const env = createEnv({
|
||||
process.env.LANGFUSE_CACHE_API_KEY_TTL_SECONDS,
|
||||
LANGFUSE_ALLOWED_ORGANIZATION_CREATORS:
|
||||
process.env.LANGFUSE_ALLOWED_ORGANIZATION_CREATORS,
|
||||
LANGFUSE_READ_FROM_POSTGRES_ONLY:
|
||||
process.env.LANGFUSE_READ_FROM_POSTGRES_ONLY,
|
||||
LANGFUSE_POSTGRES_INGESTION_ENABLED:
|
||||
process.env.LANGFUSE_POSTGRES_INGESTION_ENABLED,
|
||||
LANGFUSE_READ_FROM_CLICKHOUSE_ONLY:
|
||||
process.env.LANGFUSE_READ_FROM_CLICKHOUSE_ONLY,
|
||||
LANGFUSE_RETURN_FROM_CLICKHOUSE:
|
||||
process.env.LANGFUSE_RETURN_FROM_CLICKHOUSE,
|
||||
LANGFUSE_EXPERIMENT_EXCLUDED_PROJECT_IDS:
|
||||
process.env.LANGFUSE_EXPERIMENT_EXCLUDED_PROJECT_IDS,
|
||||
LANGFUSE_READ_DASHBOARDS_FROM_CLICKHOUSE:
|
||||
process.env.LANGFUSE_READ_DASHBOARDS_FROM_CLICKHOUSE,
|
||||
STRIPE_SECRET_KEY: process.env.STRIPE_SECRET_KEY,
|
||||
STRIPE_WEBHOOK_SIGNING_SECRET: process.env.STRIPE_WEBHOOK_SIGNING_SECRET,
|
||||
SENTRY_AUTH_TOKEN: process.env.SENTRY_AUTH_TOKEN,
|
||||
|
||||
@@ -59,8 +59,8 @@ export function CloudRegionSwitch({
|
||||
);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className="mb-6 flex w-full flex-col gap-2">
|
||||
<div className="-mb-10 mt-8 rounded-lg bg-card px-6 py-6 text-sm sm:mx-auto sm:w-full sm:max-w-[480px] sm:rounded-lg sm:px-10">
|
||||
<div className="flex w-full flex-col gap-2">
|
||||
<div>
|
||||
<span className="text-sm font-medium leading-none">
|
||||
Data Region
|
||||
@@ -126,8 +126,12 @@ const DataRegionInfo = () => (
|
||||
<div className="flex flex-col gap-2">
|
||||
<p>Langfuse Cloud is available in two data regions:</p>
|
||||
<ul className="list-disc pl-5">
|
||||
<li>US: Northern California (AWS us-west-1) & Virginia (AWS us-east-1)</li>
|
||||
<li>EU: Germany/Frankfurt (AWS eu-central-1) & Ireland (AWS eu-west-1)</li>
|
||||
<li>
|
||||
US: Oregon (AWS us-west-2)
|
||||
</li>
|
||||
<li>
|
||||
EU: Ireland (AWS eu-west-1)
|
||||
</li>
|
||||
</ul>
|
||||
<p>
|
||||
Regions are strictly separated, and no data is shared across regions.
|
||||
|
||||
@@ -1,12 +1,24 @@
|
||||
import { cn } from "@/src/utils/tailwind";
|
||||
import { MessageCircleMore } from "lucide-react";
|
||||
|
||||
export function CommentCountIcon({ count }: { count?: number }) {
|
||||
export function CommentCountIcon({
|
||||
count,
|
||||
className,
|
||||
}: {
|
||||
count?: number;
|
||||
className?: string;
|
||||
}) {
|
||||
if (!count) return null;
|
||||
|
||||
return (
|
||||
<span className="relative mr-1 text-xs">
|
||||
<MessageCircleMore className="h-4 w-4" />
|
||||
<span className="absolute -top-0.5 left-2.5 flex max-h-[0.8rem] min-w-[0.8rem] items-center justify-center rounded-full border border-muted-foreground bg-accent-light-blue px-[0.2rem] text-[8px]">
|
||||
<span
|
||||
className={cn(
|
||||
"absolute -top-0.5 left-2.5 flex max-h-[0.8rem] min-w-[0.8rem] items-center justify-center rounded-full border border-muted-foreground bg-accent-light-blue px-[0.2rem] text-[8px]",
|
||||
className,
|
||||
)}
|
||||
>
|
||||
{count > 99 ? "99+" : count}
|
||||
</span>
|
||||
</span>
|
||||
|
||||
@@ -4,7 +4,7 @@ import { DashboardCard } from "@/src/features/dashboard/components/cards/Dashboa
|
||||
import { type FilterState } from "@langfuse/shared";
|
||||
import { type DashboardDateRangeAggregationOption } from "@/src/utils/date-range-utils";
|
||||
import { MultiSelectKeyValues } from "@/src/features/scores/components/multi-select-key-values";
|
||||
import React, { useMemo, useState } from "react";
|
||||
import React, { useMemo } from "react";
|
||||
import { Separator } from "@/src/components/ui/separator";
|
||||
import {
|
||||
isBooleanDataType,
|
||||
@@ -20,6 +20,7 @@ import DocPopup from "@/src/components/layouts/doc-popup";
|
||||
import { NoDataOrLoading } from "@/src/components/NoDataOrLoading";
|
||||
import { Flex, Text } from "@tremor/react";
|
||||
import { useClickhouse } from "@/src/components/layouts/ClickhouseAdminToggle";
|
||||
import useLocalStorage from "@/src/components/useLocalStorage";
|
||||
|
||||
export function ScoreAnalytics(props: {
|
||||
className?: string;
|
||||
@@ -27,9 +28,12 @@ export function ScoreAnalytics(props: {
|
||||
globalFilterState: FilterState;
|
||||
projectId: string;
|
||||
}) {
|
||||
const [selectedDashboardScoreKeys, setSelectedDashboardScoreKeys] = useState<
|
||||
string[]
|
||||
>([]);
|
||||
// Stale score selections in localStorage are ignored as we only show scores that exist in scoreAnalyticsOptions
|
||||
const [selectedDashboardScoreKeys, setSelectedDashboardScoreKeys] =
|
||||
useLocalStorage<string[]>(
|
||||
`selectedDashboardScoreKeys-${props.projectId}`,
|
||||
[],
|
||||
);
|
||||
|
||||
const scoreKeysAndProps = api.scores.getScoreKeysAndProps.useQuery(
|
||||
{
|
||||
|
||||
@@ -1,10 +1,8 @@
|
||||
import { z } from "zod";
|
||||
import { isClickhouseAdminEligible } from "@/src/server/utils/checkClickhouseAccess";
|
||||
import {
|
||||
createTRPCRouter,
|
||||
protectedProjectProcedure,
|
||||
} from "@/src/server/api/trpc";
|
||||
import { executeQuery } from "@/src/server/api/services/queryBuilder";
|
||||
import {
|
||||
filterInterface,
|
||||
type groupByInterface,
|
||||
@@ -33,7 +31,6 @@ import {
|
||||
} from "@langfuse/shared/src/server";
|
||||
import { type DatabaseRow } from "@/src/server/api/services/queryBuilder";
|
||||
import { dashboardColumnDefinitions } from "@langfuse/shared";
|
||||
import { env } from "@/src/env.mjs";
|
||||
|
||||
export const dashboardRouter = createTRPCRouter({
|
||||
chart: protectedProjectProcedure
|
||||
@@ -64,243 +61,227 @@ export const dashboardRouter = createTRPCRouter({
|
||||
.nullish(),
|
||||
}),
|
||||
)
|
||||
.query(async ({ input, ctx }) => {
|
||||
if (
|
||||
input.queryClickhouse &&
|
||||
!isClickhouseAdminEligible(ctx.session.user)
|
||||
) {
|
||||
throw new TRPCError({
|
||||
code: "UNAUTHORIZED",
|
||||
message: "Not eligible to query clickhouse",
|
||||
});
|
||||
}
|
||||
|
||||
if (
|
||||
input.queryClickhouse ||
|
||||
env.LANGFUSE_READ_DASHBOARDS_FROM_CLICKHOUSE === "true"
|
||||
) {
|
||||
switch (input.queryName) {
|
||||
case "traces-total":
|
||||
const count = await getTotalTraces(
|
||||
.query(async ({ input }) => {
|
||||
switch (input.queryName) {
|
||||
case "traces-total":
|
||||
const count = await getTotalTraces(
|
||||
input.projectId,
|
||||
input.filter ?? [],
|
||||
);
|
||||
return count as DatabaseRow[];
|
||||
case "traces-grouped-by-name":
|
||||
return (
|
||||
await getTracesGroupedByName(
|
||||
input.projectId,
|
||||
input.filter ?? [],
|
||||
);
|
||||
return count as DatabaseRow[];
|
||||
case "traces-grouped-by-name":
|
||||
return (
|
||||
await getTracesGroupedByName(
|
||||
input.projectId,
|
||||
dashboardColumnDefinitions,
|
||||
input.filter,
|
||||
)
|
||||
).map(
|
||||
(row) =>
|
||||
({
|
||||
traceName: row.name,
|
||||
countTraceId: row.count,
|
||||
}) as DatabaseRow,
|
||||
);
|
||||
case "observations-model-cost":
|
||||
const cost = await getObservationsCostGroupedByName(
|
||||
input.projectId,
|
||||
input.filter ?? [],
|
||||
);
|
||||
|
||||
return cost.map((row) => ({
|
||||
model: row.name,
|
||||
sumCalculatedTotalCost: row.sum_cost_details,
|
||||
sumTotalTokens: row.sum_usage_details,
|
||||
})) as DatabaseRow[];
|
||||
case "score-aggregate":
|
||||
const scores = await getScoreAggregate(
|
||||
input.projectId,
|
||||
input.filter ?? [],
|
||||
);
|
||||
|
||||
return scores.map((row) => ({
|
||||
scoreName: row.name,
|
||||
scoreSource: row.source,
|
||||
scoreDataType: row.data_type,
|
||||
avgValue: row.avg_value,
|
||||
countScoreId: Number(row.count),
|
||||
})) as DatabaseRow[];
|
||||
|
||||
case "traces-timeseries":
|
||||
const dateTrunc = extractTimeSeries(input.groupBy);
|
||||
if (!dateTrunc) {
|
||||
return [];
|
||||
}
|
||||
const rows = await groupTracesByTime(
|
||||
input.projectId,
|
||||
input.filter ?? [],
|
||||
dateTrunc,
|
||||
);
|
||||
|
||||
return rows as DatabaseRow[];
|
||||
case "observations-usage-timeseries":
|
||||
const dateTruncObs = extractTimeSeries(input.groupBy);
|
||||
if (!dateTruncObs) {
|
||||
return [];
|
||||
}
|
||||
const rowsObs = await getObservationUsageByTime(
|
||||
input.projectId,
|
||||
input.filter ?? [],
|
||||
dateTruncObs,
|
||||
);
|
||||
|
||||
return rowsObs.map((row) => ({
|
||||
startTime: row.start_time,
|
||||
units: row.units,
|
||||
cost: row.cost,
|
||||
model: row.provided_model_name,
|
||||
})) as DatabaseRow[];
|
||||
|
||||
case "distinct-models":
|
||||
const models = await getDistinctModels(
|
||||
input.projectId,
|
||||
input.filter ?? [],
|
||||
);
|
||||
return models as DatabaseRow[];
|
||||
|
||||
case "scores-aggregate-timeseries":
|
||||
const dateTruncScores = extractTimeSeries(input.groupBy);
|
||||
if (!dateTruncScores) {
|
||||
return [];
|
||||
}
|
||||
const aggregatedScores = await getScoresAggregateOverTime(
|
||||
input.projectId,
|
||||
input.filter ?? [],
|
||||
dateTruncScores,
|
||||
);
|
||||
|
||||
return aggregatedScores as DatabaseRow[];
|
||||
|
||||
case "observations-usage-by-users":
|
||||
const rowsUsers = await getModelUsageByUser(
|
||||
input.projectId,
|
||||
input.filter ?? [],
|
||||
);
|
||||
|
||||
return rowsUsers.map((row) => ({
|
||||
sumTotalTokens: row.sumUsageDetails,
|
||||
sumCalculatedTotalCost: row.sumCostDetails,
|
||||
user: row.userId,
|
||||
})) as DatabaseRow[];
|
||||
|
||||
case "traces-grouped-by-user":
|
||||
const traces = await getTracesGroupedByUsers(
|
||||
input.projectId,
|
||||
input.filter ?? [],
|
||||
undefined,
|
||||
1000,
|
||||
0,
|
||||
dashboardColumnDefinitions,
|
||||
);
|
||||
input.filter,
|
||||
)
|
||||
).map(
|
||||
(row) =>
|
||||
({
|
||||
traceName: row.name,
|
||||
countTraceId: row.count,
|
||||
}) as DatabaseRow,
|
||||
);
|
||||
case "observations-model-cost":
|
||||
const cost = await getObservationsCostGroupedByName(
|
||||
input.projectId,
|
||||
input.filter ?? [],
|
||||
);
|
||||
|
||||
return traces.map((row) => ({
|
||||
user: row.user,
|
||||
countTraceId: Number(row.count),
|
||||
})) as DatabaseRow[];
|
||||
case "observation-latencies-aggregated":
|
||||
const latencies = await getObservationLatencies(
|
||||
return cost.map((row) => ({
|
||||
model: row.name,
|
||||
sumCalculatedTotalCost: row.sum_cost_details,
|
||||
sumTotalTokens: row.sum_usage_details,
|
||||
})) as DatabaseRow[];
|
||||
case "score-aggregate":
|
||||
const scores = await getScoreAggregate(
|
||||
input.projectId,
|
||||
input.filter ?? [],
|
||||
);
|
||||
|
||||
return scores.map((row) => ({
|
||||
scoreName: row.name,
|
||||
scoreSource: row.source,
|
||||
scoreDataType: row.data_type,
|
||||
avgValue: row.avg_value,
|
||||
countScoreId: Number(row.count),
|
||||
})) as DatabaseRow[];
|
||||
|
||||
case "traces-timeseries":
|
||||
const dateTrunc = extractTimeSeries(input.groupBy);
|
||||
if (!dateTrunc) {
|
||||
return [];
|
||||
}
|
||||
const rows = await groupTracesByTime(
|
||||
input.projectId,
|
||||
input.filter ?? [],
|
||||
dateTrunc,
|
||||
);
|
||||
|
||||
return rows as DatabaseRow[];
|
||||
case "observations-usage-timeseries":
|
||||
const dateTruncObs = extractTimeSeries(input.groupBy);
|
||||
if (!dateTruncObs) {
|
||||
return [];
|
||||
}
|
||||
const rowsObs = await getObservationUsageByTime(
|
||||
input.projectId,
|
||||
input.filter ?? [],
|
||||
dateTruncObs,
|
||||
);
|
||||
|
||||
return rowsObs.map((row) => ({
|
||||
startTime: row.start_time,
|
||||
units: row.units,
|
||||
cost: row.cost,
|
||||
model: row.provided_model_name,
|
||||
})) as DatabaseRow[];
|
||||
|
||||
case "distinct-models":
|
||||
const models = await getDistinctModels(
|
||||
input.projectId,
|
||||
input.filter ?? [],
|
||||
);
|
||||
return models as DatabaseRow[];
|
||||
|
||||
case "scores-aggregate-timeseries":
|
||||
const dateTruncScores = extractTimeSeries(input.groupBy);
|
||||
if (!dateTruncScores) {
|
||||
return [];
|
||||
}
|
||||
const aggregatedScores = await getScoresAggregateOverTime(
|
||||
input.projectId,
|
||||
input.filter ?? [],
|
||||
dateTruncScores,
|
||||
);
|
||||
|
||||
return aggregatedScores as DatabaseRow[];
|
||||
|
||||
case "observations-usage-by-users":
|
||||
const rowsUsers = await getModelUsageByUser(
|
||||
input.projectId,
|
||||
input.filter ?? [],
|
||||
);
|
||||
|
||||
return rowsUsers.map((row) => ({
|
||||
sumTotalTokens: row.sumUsageDetails,
|
||||
sumCalculatedTotalCost: row.sumCostDetails,
|
||||
user: row.userId,
|
||||
})) as DatabaseRow[];
|
||||
|
||||
case "traces-grouped-by-user":
|
||||
const traces = await getTracesGroupedByUsers(
|
||||
input.projectId,
|
||||
input.filter ?? [],
|
||||
undefined,
|
||||
1000,
|
||||
0,
|
||||
dashboardColumnDefinitions,
|
||||
);
|
||||
|
||||
return traces.map((row) => ({
|
||||
user: row.user,
|
||||
countTraceId: Number(row.count),
|
||||
})) as DatabaseRow[];
|
||||
case "observation-latencies-aggregated":
|
||||
const latencies = await getObservationLatencies(
|
||||
input.projectId,
|
||||
input.filter ?? [],
|
||||
);
|
||||
|
||||
return latencies.map((row) => ({
|
||||
name: row.name,
|
||||
percentile50Duration: row.p50,
|
||||
percentile90Duration: row.p90,
|
||||
percentile95Duration: row.p95,
|
||||
percentile99Duration: row.p99,
|
||||
})) as DatabaseRow[];
|
||||
case "model-latencies-over-time":
|
||||
const dateTruncModels = extractTimeSeries(input.groupBy);
|
||||
if (!dateTruncModels) {
|
||||
return [];
|
||||
}
|
||||
const modelLatencies = await getModelLatenciesOverTime(
|
||||
input.projectId,
|
||||
input.filter ?? [],
|
||||
dateTruncModels,
|
||||
);
|
||||
|
||||
return modelLatencies.map((row) => ({
|
||||
model: row.model,
|
||||
startTime: row.start_time,
|
||||
percentile50Duration: row.p50,
|
||||
percentile75Duration: row.p75,
|
||||
percentile90Duration: row.p90,
|
||||
percentile95Duration: row.p95,
|
||||
percentile99Duration: row.p99,
|
||||
})) as DatabaseRow[];
|
||||
case "traces-latencies-aggregated":
|
||||
const traceLatencies = await getTracesLatencies(
|
||||
input.projectId,
|
||||
input.filter ?? [],
|
||||
);
|
||||
|
||||
return traceLatencies.map((row) => ({
|
||||
traceName: row.name,
|
||||
percentile50Duration: row.p50,
|
||||
percentile90Duration: row.p90,
|
||||
percentile95Duration: row.p95,
|
||||
percentile99Duration: row.p99,
|
||||
})) as DatabaseRow[];
|
||||
case "numeric-score-time-series":
|
||||
const dateTruncNumericScoreTimeSeries = extractTimeSeries(
|
||||
input.groupBy,
|
||||
);
|
||||
if (!dateTruncNumericScoreTimeSeries) {
|
||||
return [];
|
||||
}
|
||||
const numericScoreTimeSeries = await getNumericScoreTimeSeries(
|
||||
input.projectId,
|
||||
input.filter ?? [],
|
||||
dateTruncNumericScoreTimeSeries,
|
||||
);
|
||||
return numericScoreTimeSeries.map((row) => ({
|
||||
scoreTimestamp: row.score_timestamp,
|
||||
scoreName: row.score_name,
|
||||
avgValue: row.avg_value,
|
||||
})) as DatabaseRow[];
|
||||
case "categorical-score-chart":
|
||||
const dateTruncCategoricalScoreTimeSeries = extractTimeSeries(
|
||||
input.groupBy,
|
||||
);
|
||||
const categoricalScoreTimeSeries =
|
||||
await getCategoricalScoreTimeSeries(
|
||||
input.projectId,
|
||||
input.filter ?? [],
|
||||
dateTruncCategoricalScoreTimeSeries,
|
||||
);
|
||||
return categoricalScoreTimeSeries.map((row) => ({
|
||||
...(row.score_timestamp
|
||||
? { scoreTimestamp: row.score_timestamp }
|
||||
: {}),
|
||||
scoreName: row.score_name || null,
|
||||
scoreDataType: row.score_data_type || null,
|
||||
scoreSource: row.score_source || null,
|
||||
stringValue: row.score_value || null,
|
||||
countStringValue: Number(row.count) || 0,
|
||||
})) as DatabaseRow[];
|
||||
case "observations-status-timeseries":
|
||||
const timeSeriesGroupBy = extractTimeSeries(input.groupBy);
|
||||
|
||||
return latencies.map((row) => ({
|
||||
name: row.name,
|
||||
percentile50Duration: row.p50,
|
||||
percentile90Duration: row.p90,
|
||||
percentile95Duration: row.p95,
|
||||
percentile99Duration: row.p99,
|
||||
})) as DatabaseRow[];
|
||||
case "model-latencies-over-time":
|
||||
const dateTruncModels = extractTimeSeries(input.groupBy);
|
||||
if (!dateTruncModels) {
|
||||
return [];
|
||||
}
|
||||
const modelLatencies = await getModelLatenciesOverTime(
|
||||
input.projectId,
|
||||
input.filter ?? [],
|
||||
dateTruncModels,
|
||||
);
|
||||
return (await getObservationsStatusTimeSeries(
|
||||
input.projectId,
|
||||
input.filter ?? [],
|
||||
timeSeriesGroupBy,
|
||||
)) as DatabaseRow[];
|
||||
|
||||
return modelLatencies.map((row) => ({
|
||||
model: row.model,
|
||||
startTime: row.start_time,
|
||||
percentile50Duration: row.p50,
|
||||
percentile75Duration: row.p75,
|
||||
percentile90Duration: row.p90,
|
||||
percentile95Duration: row.p95,
|
||||
percentile99Duration: row.p99,
|
||||
})) as DatabaseRow[];
|
||||
case "traces-latencies-aggregated":
|
||||
const traceLatencies = await getTracesLatencies(
|
||||
input.projectId,
|
||||
input.filter ?? [],
|
||||
);
|
||||
|
||||
return traceLatencies.map((row) => ({
|
||||
traceName: row.name,
|
||||
percentile50Duration: row.p50,
|
||||
percentile90Duration: row.p90,
|
||||
percentile95Duration: row.p95,
|
||||
percentile99Duration: row.p99,
|
||||
})) as DatabaseRow[];
|
||||
case "numeric-score-time-series":
|
||||
const dateTruncNumericScoreTimeSeries = extractTimeSeries(
|
||||
input.groupBy,
|
||||
);
|
||||
if (!dateTruncNumericScoreTimeSeries) {
|
||||
return [];
|
||||
}
|
||||
const numericScoreTimeSeries = await getNumericScoreTimeSeries(
|
||||
input.projectId,
|
||||
input.filter ?? [],
|
||||
dateTruncNumericScoreTimeSeries,
|
||||
);
|
||||
return numericScoreTimeSeries.map((row) => ({
|
||||
scoreTimestamp: row.score_timestamp,
|
||||
scoreName: row.score_name,
|
||||
avgValue: row.avg_value,
|
||||
})) as DatabaseRow[];
|
||||
case "categorical-score-chart":
|
||||
const dateTruncCategoricalScoreTimeSeries = extractTimeSeries(
|
||||
input.groupBy,
|
||||
);
|
||||
const categoricalScoreTimeSeries =
|
||||
await getCategoricalScoreTimeSeries(
|
||||
input.projectId,
|
||||
input.filter ?? [],
|
||||
dateTruncCategoricalScoreTimeSeries,
|
||||
);
|
||||
return categoricalScoreTimeSeries.map((row) => ({
|
||||
...(row.score_timestamp
|
||||
? { scoreTimestamp: row.score_timestamp }
|
||||
: {}),
|
||||
scoreName: row.score_name || null,
|
||||
scoreDataType: row.score_data_type || null,
|
||||
scoreSource: row.score_source || null,
|
||||
stringValue: row.score_value || null,
|
||||
countStringValue: Number(row.count) || 0,
|
||||
})) as DatabaseRow[];
|
||||
case "observations-status-timeseries":
|
||||
const timeSeriesGroupBy = extractTimeSeries(input.groupBy);
|
||||
|
||||
return (await getObservationsStatusTimeSeries(
|
||||
input.projectId,
|
||||
input.filter ?? [],
|
||||
timeSeriesGroupBy,
|
||||
)) as DatabaseRow[];
|
||||
|
||||
default:
|
||||
throw new TRPCError({
|
||||
code: "NOT_FOUND",
|
||||
message: "Query not found",
|
||||
});
|
||||
}
|
||||
default:
|
||||
throw new TRPCError({
|
||||
code: "NOT_FOUND",
|
||||
message: "Query not found",
|
||||
});
|
||||
}
|
||||
return await executeQuery(ctx.prisma, input.projectId, input);
|
||||
}),
|
||||
scoreHistogram: protectedProjectProcedure
|
||||
.input(
|
||||
@@ -310,30 +291,12 @@ export const dashboardRouter = createTRPCRouter({
|
||||
queryClickhouse: z.boolean().default(false),
|
||||
}),
|
||||
)
|
||||
.query(async ({ input, ctx }) => {
|
||||
if (
|
||||
input.queryClickhouse &&
|
||||
!isClickhouseAdminEligible(ctx.session.user)
|
||||
) {
|
||||
throw new TRPCError({
|
||||
code: "UNAUTHORIZED",
|
||||
message: "Not eligible to query clickhouse",
|
||||
});
|
||||
}
|
||||
|
||||
if (
|
||||
input.queryClickhouse ||
|
||||
env.LANGFUSE_READ_DASHBOARDS_FROM_CLICKHOUSE === "true"
|
||||
) {
|
||||
const data = await getNumericScoreHistogram(
|
||||
input.projectId,
|
||||
input.filter ?? [],
|
||||
input.limit ?? 10000,
|
||||
);
|
||||
return createHistogramData(data);
|
||||
}
|
||||
|
||||
const data = await executeQuery(ctx.prisma, input.projectId, input);
|
||||
.query(async ({ input }) => {
|
||||
const data = await getNumericScoreHistogram(
|
||||
input.projectId,
|
||||
input.filter ?? [],
|
||||
input.limit ?? 10000,
|
||||
);
|
||||
return createHistogramData(data);
|
||||
}),
|
||||
});
|
||||
|
||||
@@ -4,23 +4,11 @@ import {
|
||||
createTRPCRouter,
|
||||
protectedProjectProcedure,
|
||||
} from "@/src/server/api/trpc";
|
||||
import {
|
||||
type DatasetRuns,
|
||||
Prisma,
|
||||
type Dataset,
|
||||
} from "@langfuse/shared/src/db";
|
||||
import { Prisma, type Dataset } from "@langfuse/shared/src/db";
|
||||
import { throwIfNoProjectAccess } from "@/src/features/rbac/utils/checkProjectAccess";
|
||||
import { auditLog } from "@/src/features/audit-logs/auditLog";
|
||||
import { DB } from "@/src/server/db";
|
||||
import {
|
||||
type PrismaClient,
|
||||
type ScoreAggregate,
|
||||
type ScoreSimplified,
|
||||
filterAndValidateDbScoreList,
|
||||
paginationZod,
|
||||
} from "@langfuse/shared";
|
||||
import { aggregateScores } from "@/src/features/scores/lib/aggregateScores";
|
||||
import { measureAndReturnApi } from "@/src/server/utils/checkClickhouseAccess";
|
||||
import { paginationZod } from "@langfuse/shared";
|
||||
import {
|
||||
createDatasetRunsTable,
|
||||
createDatasetRunsTableWithoutMetrics,
|
||||
@@ -28,7 +16,6 @@ import {
|
||||
fetchDatasetItems,
|
||||
getRunItemsByRunIdOrItemId,
|
||||
} from "@/src/features/datasets/server/service";
|
||||
import { traceException } from "@langfuse/shared/src/server";
|
||||
|
||||
export const datasetRouter = createTRPCRouter({
|
||||
allDatasetMeta: protectedProjectProcedure
|
||||
@@ -163,64 +150,42 @@ export const datasetRouter = createTRPCRouter({
|
||||
runsByDatasetId: protectedProjectProcedure
|
||||
.input(datasetRunsTableSchema)
|
||||
.query(async ({ input, ctx }) => {
|
||||
return await measureAndReturnApi({
|
||||
input,
|
||||
operation: "datasets.runsByDatasetId",
|
||||
user: ctx.session.user,
|
||||
pgExecution: async () => {
|
||||
return await runsByDatasetIdPg(ctx.prisma, input);
|
||||
},
|
||||
clickhouseExecution: async () => {
|
||||
// we cannot easily join all the tracing data with the dataset run items
|
||||
// hence, we pull the trace_ids and observation_ids separately for all run items
|
||||
// afterwards, we aggregate them per run
|
||||
// we cannot easily join all the tracing data with the dataset run items
|
||||
// hence, we pull the trace_ids and observation_ids separately for all run items
|
||||
// afterwards, we aggregate them per run
|
||||
const runs = await createDatasetRunsTableWithoutMetrics(input);
|
||||
|
||||
const runs = await createDatasetRunsTableWithoutMetrics(input);
|
||||
|
||||
const totalRuns = await ctx.prisma.datasetRuns.count({
|
||||
where: {
|
||||
datasetId: input.datasetId,
|
||||
projectId: input.projectId,
|
||||
},
|
||||
});
|
||||
|
||||
return {
|
||||
totalRuns,
|
||||
runs,
|
||||
};
|
||||
const totalRuns = await ctx.prisma.datasetRuns.count({
|
||||
where: {
|
||||
datasetId: input.datasetId,
|
||||
projectId: input.projectId,
|
||||
},
|
||||
});
|
||||
|
||||
return {
|
||||
totalRuns,
|
||||
runs,
|
||||
};
|
||||
}),
|
||||
runsByDatasetIdMetrics: protectedProjectProcedure
|
||||
.input(datasetRunsTableSchema)
|
||||
.query(async ({ input, ctx }) => {
|
||||
return await measureAndReturnApi({
|
||||
input,
|
||||
operation: "datasets.runsByDatasetId",
|
||||
user: ctx.session.user,
|
||||
pgExecution: async () => {
|
||||
return await runsByDatasetIdPg(ctx.prisma, input);
|
||||
},
|
||||
clickhouseExecution: async () => {
|
||||
// we cannot easily join all the tracing data with the dataset run items
|
||||
// hence, we pull the trace_ids and observation_ids separately for all run items
|
||||
// afterwards, we aggregate them per run
|
||||
// we cannot easily join all the tracing data with the dataset run items
|
||||
// hence, we pull the trace_ids and observation_ids separately for all run items
|
||||
// afterwards, we aggregate them per run
|
||||
const runs = await createDatasetRunsTable(input);
|
||||
|
||||
const runs = await createDatasetRunsTable(input);
|
||||
|
||||
const totalRuns = await ctx.prisma.datasetRuns.count({
|
||||
where: {
|
||||
datasetId: input.datasetId,
|
||||
projectId: input.projectId,
|
||||
},
|
||||
});
|
||||
|
||||
return {
|
||||
totalRuns,
|
||||
runs,
|
||||
};
|
||||
const totalRuns = await ctx.prisma.datasetRuns.count({
|
||||
where: {
|
||||
datasetId: input.datasetId,
|
||||
projectId: input.projectId,
|
||||
},
|
||||
});
|
||||
|
||||
return {
|
||||
totalRuns,
|
||||
runs,
|
||||
};
|
||||
}),
|
||||
itemById: protectedProjectProcedure
|
||||
.input(
|
||||
@@ -686,107 +651,11 @@ export const datasetRouter = createTRPCRouter({
|
||||
},
|
||||
});
|
||||
|
||||
return await measureAndReturnApi({
|
||||
input,
|
||||
operation: "datasets.runitemsByRunIdOrItemId",
|
||||
user: ctx.session.user,
|
||||
pgExecution: async () => {
|
||||
const traceScores = await ctx.prisma.score.findMany({
|
||||
where: {
|
||||
projectId: ctx.session.projectId,
|
||||
traceId: {
|
||||
in: runItems.map((ri) => ri.traceId),
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const observations = await ctx.prisma.observationView.findMany({
|
||||
where: {
|
||||
id: {
|
||||
in: runItems
|
||||
.map((ri) => ri.observationId)
|
||||
.filter(Boolean) as string[],
|
||||
},
|
||||
projectId: ctx.session.projectId,
|
||||
},
|
||||
select: {
|
||||
id: true,
|
||||
latency: true,
|
||||
calculatedTotalCost: 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;
|
||||
totalCost: number;
|
||||
}[]
|
||||
>(
|
||||
Prisma.sql`
|
||||
SELECT
|
||||
t.id,
|
||||
o.duration,
|
||||
o.total_cost as "totalCost"
|
||||
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,
|
||||
SUM(COALESCE(o1.calculated_total_cost, 0))::double precision AS total_cost
|
||||
FROM
|
||||
-- Use observations_view as cost are not backfilled for self-hosters. Once V3 is migration is done, we can use observations instead
|
||||
observations_view o1
|
||||
WHERE
|
||||
o1.project_id = ${input.projectId}
|
||||
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 validatedTraceScores = filterAndValidateDbScoreList(
|
||||
traceScores,
|
||||
traceException,
|
||||
);
|
||||
|
||||
const items = runItems.map((ri) => {
|
||||
return {
|
||||
id: ri.id,
|
||||
createdAt: ri.createdAt,
|
||||
datasetItemId: ri.datasetItemId,
|
||||
observation: observations.find((o) => o.id === ri.observationId),
|
||||
trace: traces.find((t) => t.id === ri.traceId),
|
||||
scores: aggregateScores(
|
||||
validatedTraceScores.filter((s) => s.traceId === ri.traceId),
|
||||
),
|
||||
};
|
||||
});
|
||||
|
||||
// Note: We early return in case of no run items, when adding parameters here, make sure to update the early return above
|
||||
return {
|
||||
totalRunItems,
|
||||
runItems: items,
|
||||
};
|
||||
},
|
||||
clickhouseExecution: async () => {
|
||||
// Note: We early return in case of no run items, when adding parameters here, make sure to update the early return above
|
||||
return {
|
||||
totalRunItems,
|
||||
runItems: await getRunItemsByRunIdOrItemId(
|
||||
input.projectId,
|
||||
runItems,
|
||||
),
|
||||
};
|
||||
},
|
||||
});
|
||||
// Note: We early return in case of no run items, when adding parameters here, make sure to update the early return above
|
||||
return {
|
||||
totalRunItems,
|
||||
runItems: await getRunItemsByRunIdOrItemId(input.projectId, runItems),
|
||||
};
|
||||
}),
|
||||
datasetItemsBasedOnTraceOrObservation: protectedProjectProcedure
|
||||
.input(
|
||||
@@ -851,161 +720,3 @@ export const datasetRouter = createTRPCRouter({
|
||||
return deletedDatasetRun;
|
||||
}),
|
||||
});
|
||||
|
||||
async function runsByDatasetIdPg(
|
||||
prisma: PrismaClient,
|
||||
input: {
|
||||
projectId: string;
|
||||
datasetId: string;
|
||||
queryClickhouse: boolean;
|
||||
page?: number;
|
||||
limit?: number;
|
||||
runIds?: string[];
|
||||
},
|
||||
) {
|
||||
const scoresByRunId = await prisma.$queryRaw<
|
||||
Array<{ scores: Array<ScoreSimplified>; runId: string }>
|
||||
>(Prisma.sql`
|
||||
SELECT
|
||||
runs.id "runId",
|
||||
array_agg(s.score) AS "scores"
|
||||
FROM
|
||||
dataset_runs runs
|
||||
JOIN datasets ON datasets.id = runs.dataset_id AND datasets.project_id = ${input.projectId}
|
||||
LEFT JOIN LATERAL (
|
||||
SELECT
|
||||
jsonb_build_object ('name', s.name, 'stringValue', s.string_value, 'value', s.value, 'source', s."source", 'dataType', s.data_type, 'comment', s.comment) AS "score"
|
||||
FROM
|
||||
dataset_run_items ri
|
||||
JOIN scores s
|
||||
ON s.trace_id = ri.trace_id
|
||||
AND (ri.observation_id IS NULL OR s.observation_id = ri.observation_id)
|
||||
AND s.project_id = ${input.projectId}
|
||||
JOIN traces t ON t.id = s.trace_id AND t.project_id = ${input.projectId}
|
||||
WHERE
|
||||
ri.project_id = ${input.projectId}
|
||||
AND ri.dataset_run_id = runs.id
|
||||
) s ON true
|
||||
WHERE
|
||||
runs.dataset_id = ${input.datasetId}
|
||||
AND runs.project_id = ${input.projectId}
|
||||
AND s.score IS NOT NULL
|
||||
${input.runIds ? Prisma.sql`AND runs.id IN (${Prisma.join(input.runIds)})` : Prisma.empty}
|
||||
GROUP BY
|
||||
runs.id
|
||||
${input.limit ? Prisma.sql`LIMIT ${input.limit}` : Prisma.empty}
|
||||
${input.page && input.limit ? Prisma.sql`OFFSET ${input.page * input.limit}` : Prisma.empty}
|
||||
`);
|
||||
|
||||
const runs = await prisma.$queryRaw<
|
||||
Array<
|
||||
DatasetRuns & {
|
||||
avgLatency: number | undefined;
|
||||
avgTotalCost: Prisma.Decimal | undefined;
|
||||
countRunItems: number;
|
||||
}
|
||||
>
|
||||
>(Prisma.sql`
|
||||
SELECT
|
||||
runs.id,
|
||||
runs.name,
|
||||
runs.description,
|
||||
runs.metadata,
|
||||
runs.created_at "createdAt",
|
||||
runs.updated_at "updatedAt",
|
||||
COALESCE(o_latency_and_total_cost. "o_avgLatency", t_latency_and_total_cost."t_avgLatency", 0) "avgLatency",
|
||||
COALESCE(o_latency_and_total_cost. "o_avgTotalCost", t_latency_and_total_cost."t_avgTotalCost", 0) "avgTotalCost",
|
||||
COALESCE(run_items_count.count, 0)::int "countRunItems"
|
||||
FROM
|
||||
dataset_runs runs
|
||||
JOIN datasets ON datasets.id = runs.dataset_id
|
||||
AND datasets.project_id = ${input.projectId}
|
||||
|
||||
-- Add average latency and cost if a run's items are linked to observations
|
||||
-- LIMITATION: this will only work if all items for a given run are linked to either observations or traces
|
||||
-- If a run has items linked to both observations and traces, the average latency and cost will be incorrect as only those from the observations will be used
|
||||
LEFT JOIN LATERAL (
|
||||
SELECT
|
||||
AVG(o.latency) AS "o_avgLatency",
|
||||
AVG(COALESCE(o.calculated_total_cost, 0)) AS "o_avgTotalCost"
|
||||
FROM
|
||||
dataset_run_items ri
|
||||
JOIN observations_view o ON o.id = ri.observation_id
|
||||
AND o.project_id = ${input.projectId}
|
||||
WHERE
|
||||
ri.project_id = ${input.projectId}
|
||||
AND ri.dataset_run_id = runs.id) o_latency_and_total_cost ON TRUE
|
||||
|
||||
-- Add average latency and cost if run's items are linked to traces
|
||||
LEFT JOIN LATERAL (
|
||||
-- Average across run items. One run has many items
|
||||
SELECT
|
||||
AVG(trace_latency_cost.duration) AS "t_avgLatency",
|
||||
AVG(trace_latency_cost.total_cost) AS "t_avgTotalCost"
|
||||
FROM
|
||||
dataset_run_items ri
|
||||
LEFT JOIN LATERAL (
|
||||
-- Latency and cost for a run item's trace
|
||||
SELECT
|
||||
t.id,
|
||||
o.duration,
|
||||
o.total_cost
|
||||
FROM
|
||||
traces t
|
||||
LEFT JOIN LATERAL (
|
||||
-- Latency and cost across a trace's observations
|
||||
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,
|
||||
SUM(COALESCE(o1.calculated_total_cost, 0)) AS total_cost
|
||||
FROM
|
||||
-- Use observations_view as cost are not backfilled for self-hosters. Once V3 is migration is done, we can use observations instead
|
||||
observations_view o1
|
||||
WHERE
|
||||
o1.project_id = ${input.projectId}
|
||||
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 = ri.trace_id) trace_latency_cost ON TRUE
|
||||
WHERE
|
||||
ri.project_id = ${input.projectId}
|
||||
AND ri.dataset_run_id = runs.id) t_latency_and_total_cost ON TRUE
|
||||
|
||||
-- Add run item counts
|
||||
LEFT JOIN LATERAL (
|
||||
SELECT
|
||||
count(*) AS count
|
||||
FROM
|
||||
dataset_run_items ri
|
||||
WHERE
|
||||
ri.dataset_run_id = runs.id
|
||||
AND ri.project_id = ${input.projectId}) run_items_count ON TRUE
|
||||
WHERE
|
||||
runs.dataset_id = ${input.datasetId}
|
||||
AND runs.project_id = ${input.projectId}
|
||||
${input.runIds ? Prisma.sql`AND runs.id IN (${Prisma.join(input.runIds)})` : Prisma.empty}
|
||||
ORDER BY
|
||||
runs.created_at DESC
|
||||
${input.limit ? Prisma.sql`LIMIT ${input.limit}` : Prisma.empty}
|
||||
${input.page && input.limit ? Prisma.sql`OFFSET ${input.page * input.limit}` : Prisma.empty}
|
||||
`);
|
||||
|
||||
const totalRuns = await prisma.datasetRuns.count({
|
||||
where: {
|
||||
datasetId: input.datasetId,
|
||||
projectId: input.projectId,
|
||||
},
|
||||
});
|
||||
|
||||
return {
|
||||
totalRuns,
|
||||
runs: runs.map((run) => ({
|
||||
...run,
|
||||
scores: aggregateScores(
|
||||
scoresByRunId.flatMap((s) => (s.runId === run.id ? s.scores : [])),
|
||||
) as ScoreAggregate | undefined,
|
||||
})),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -25,7 +25,6 @@ import {
|
||||
} from "@langfuse/shared/src/server";
|
||||
import { aggregateScores } from "@/src/features/scores/lib/aggregateScores";
|
||||
import Decimal from "decimal.js";
|
||||
import { measureAndReturnApi } from "@/src/server/utils/checkClickhouseAccess";
|
||||
import { env } from "@/src/env.mjs";
|
||||
|
||||
export const datasetRunsTableSchema = z.object({
|
||||
@@ -71,8 +70,8 @@ export const createDatasetRunsTableWithoutMetrics = async (
|
||||
createdAt: run.run_created_at,
|
||||
updatedAt: run.run_updated_at,
|
||||
// return metric fields as undefined
|
||||
avgTotalCost: undefined,
|
||||
avgLatency: undefined,
|
||||
avgTotalCost: undefined as Decimal | undefined,
|
||||
avgLatency: undefined as number | undefined,
|
||||
scores: undefined,
|
||||
}));
|
||||
};
|
||||
@@ -187,8 +186,6 @@ export const createTempTableInClickhouse = async (
|
||||
)
|
||||
ENGINE = ${env.CLICKHOUSE_CLUSTER_ENABLED === "true" ? "ReplicatedMergeTree()" : "MergeTree()"}
|
||||
PRIMARY KEY (project_id, dataset_id, run_id, trace_id)
|
||||
|
||||
|
||||
`;
|
||||
await commandClickhouse({
|
||||
query,
|
||||
@@ -202,7 +199,7 @@ export const deleteTempTableInClickhouse = async (
|
||||
sessionId: string,
|
||||
) => {
|
||||
const query = `
|
||||
DROP TABLE IF EXISTS ${tableName}
|
||||
DROP TABLE IF EXISTS ${tableName} ${env.CLICKHOUSE_CLUSTER_ENABLED === "true" ? "ON CLUSTER " + env.CLICKHOUSE_CLUSTER_NAME : ""}
|
||||
`;
|
||||
await commandClickhouse({
|
||||
query,
|
||||
@@ -425,65 +422,27 @@ export const fetchDatasetItems = async (input: DatasetRunItemsTableInput) => {
|
||||
});
|
||||
|
||||
// check in clickhouse if the traces already exist. They arrive delayed.
|
||||
const tracingData = await measureAndReturnApi({
|
||||
input: { queryClickhouse: false, projectId: input.projectId },
|
||||
operation: "datasets.itemsByDatasetId",
|
||||
user: null,
|
||||
pgExecution: async () => {
|
||||
const traces = await input.prisma.trace.findMany({
|
||||
where: {
|
||||
id: {
|
||||
in: datasetItems
|
||||
.map((item) => item.sourceTraceId)
|
||||
.filter((id): id is string => Boolean(id)),
|
||||
},
|
||||
projectId: input.projectId,
|
||||
},
|
||||
});
|
||||
const traces = await getTracesByIds(
|
||||
datasetItems
|
||||
.map((item) => item.sourceTraceId)
|
||||
.filter((id): id is string => Boolean(id)),
|
||||
input.projectId,
|
||||
);
|
||||
|
||||
const observations = await input.prisma.observation.findMany({
|
||||
where: {
|
||||
id: {
|
||||
in: datasetItems
|
||||
.map((item) => item.sourceObservationId)
|
||||
.filter((id): id is string => Boolean(id)),
|
||||
},
|
||||
projectId: input.projectId,
|
||||
},
|
||||
});
|
||||
const observations = await getObservationsById(
|
||||
datasetItems
|
||||
.map((item) => item.sourceObservationId)
|
||||
.filter((id): id is string => Boolean(id)),
|
||||
input.projectId,
|
||||
);
|
||||
|
||||
return {
|
||||
traceIds: traces.map((t) => t.id),
|
||||
observationIds: observations.map((o) => ({
|
||||
id: o.id,
|
||||
traceId: o.traceId,
|
||||
})),
|
||||
};
|
||||
},
|
||||
clickhouseExecution: async () => {
|
||||
const traces = await getTracesByIds(
|
||||
datasetItems
|
||||
.map((item) => item.sourceTraceId)
|
||||
.filter((id): id is string => Boolean(id)),
|
||||
input.projectId,
|
||||
);
|
||||
|
||||
const observations = await getObservationsById(
|
||||
datasetItems
|
||||
.map((item) => item.sourceObservationId)
|
||||
.filter((id): id is string => Boolean(id)),
|
||||
input.projectId,
|
||||
);
|
||||
|
||||
return {
|
||||
traceIds: traces.map((t) => t.id),
|
||||
observationIds: observations.map((o) => ({
|
||||
id: o.id,
|
||||
traceId: o.traceId,
|
||||
})),
|
||||
};
|
||||
},
|
||||
});
|
||||
const tracingData = {
|
||||
traceIds: traces.map((t) => t.id),
|
||||
observationIds: observations.map((o) => ({
|
||||
id: o.id,
|
||||
traceId: o.traceId,
|
||||
})),
|
||||
};
|
||||
|
||||
return {
|
||||
totalDatasetItems,
|
||||
|
||||
@@ -6,10 +6,11 @@ import {
|
||||
DialogTitle,
|
||||
DialogTrigger,
|
||||
DialogDescription,
|
||||
DialogPortal,
|
||||
} from "@/src/components/ui/dialog";
|
||||
import { useState } from "react";
|
||||
import Link from "next/link";
|
||||
import { Bug, LifeBuoy, Sparkles } from "lucide-react";
|
||||
import { Bug, Sparkles } from "lucide-react";
|
||||
|
||||
interface FeedbackDialogProps {
|
||||
className?: string;
|
||||
@@ -28,32 +29,33 @@ export function FeedbackButtonWrapper({
|
||||
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={setOpen}>
|
||||
<DialogTrigger className={className} asChild>
|
||||
<DialogTrigger
|
||||
className={className}
|
||||
asChild
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
{children}
|
||||
</DialogTrigger>
|
||||
<DialogContent>
|
||||
<DialogHeader>
|
||||
<DialogTitle>{title}</DialogTitle>
|
||||
<DialogDescription>{description}</DialogDescription>
|
||||
</DialogHeader>
|
||||
<div className="flex flex-row flex-wrap items-center justify-center gap-3 sm:justify-start">
|
||||
<Link href="https://langfuse.com/ideas" target="_blank">
|
||||
<Button variant="secondary">
|
||||
<Sparkles className="mr-2 h-4 w-4" /> Submit Feature Request
|
||||
</Button>
|
||||
</Link>
|
||||
<Link href="https://langfuse.com/issues" target="_blank">
|
||||
<Button variant="secondary">
|
||||
<Bug className="mr-2 h-4 w-4" /> Report a Bug
|
||||
</Button>
|
||||
</Link>
|
||||
<Link href="/support" target="_blank">
|
||||
<Button variant="outline">
|
||||
<LifeBuoy className="mr-2 h-4 w-4" /> Support
|
||||
</Button>
|
||||
</Link>
|
||||
</div>
|
||||
</DialogContent>
|
||||
<DialogPortal>
|
||||
<DialogContent>
|
||||
<DialogHeader>
|
||||
<DialogTitle>{title}</DialogTitle>
|
||||
<DialogDescription>{description}</DialogDescription>
|
||||
</DialogHeader>
|
||||
<div className="flex flex-row flex-wrap items-center justify-center gap-3 sm:justify-start">
|
||||
<Link href="https://langfuse.com/ideas" target="_blank">
|
||||
<Button variant="secondary">
|
||||
<Sparkles className="mr-2 h-4 w-4" /> Submit Feature Request
|
||||
</Button>
|
||||
</Link>
|
||||
<Link href="https://langfuse.com/issues" target="_blank">
|
||||
<Button variant="secondary">
|
||||
<Bug className="mr-2 h-4 w-4" /> Report a Bug
|
||||
</Button>
|
||||
</Link>
|
||||
</div>
|
||||
</DialogContent>
|
||||
</DialogPortal>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -227,6 +227,14 @@ export function InlineFilterBuilder({
|
||||
);
|
||||
}
|
||||
|
||||
const getOperator = (
|
||||
type: NonNullable<WipFilterCondition["type"]>,
|
||||
): WipFilterCondition["operator"] => {
|
||||
return filterOperators[type]?.length > 0
|
||||
? filterOperators[type][0]
|
||||
: undefined;
|
||||
};
|
||||
|
||||
function FilterBuilderForm({
|
||||
columns,
|
||||
filterState,
|
||||
@@ -312,19 +320,18 @@ function FilterBuilderForm({
|
||||
const col = columns.find(
|
||||
(c) => c.id === value,
|
||||
);
|
||||
const defaultOperator = col?.type
|
||||
? getOperator(col.type)
|
||||
: undefined;
|
||||
|
||||
handleFilterChange(
|
||||
{
|
||||
column: col?.name,
|
||||
type: col?.type,
|
||||
operator:
|
||||
// does not work as expected on eval-template form when embedded into form via InlineFilterBuilder
|
||||
// col?.type !== undefined &&
|
||||
// filterOperators[col.type]?.length > 0
|
||||
// ? (filterOperators[col.type][0] as any) // operator matches type
|
||||
undefined,
|
||||
operator: defaultOperator,
|
||||
value: undefined,
|
||||
key: undefined,
|
||||
},
|
||||
} as WipFilterCondition,
|
||||
i,
|
||||
);
|
||||
}}
|
||||
@@ -391,6 +398,8 @@ function FilterBuilderForm({
|
||||
<Select
|
||||
disabled={!filter.column || disabled}
|
||||
onValueChange={(value) => {
|
||||
// protect against invalid empty operator values
|
||||
if (value === "") return;
|
||||
handleFilterChange(
|
||||
{
|
||||
...filter,
|
||||
|
||||
@@ -1,4 +1,10 @@
|
||||
import { Building2, LifeBuoy, LockIcon, Settings, Users } from "lucide-react";
|
||||
import {
|
||||
BookOpen,
|
||||
LockIcon,
|
||||
MessageSquareText,
|
||||
Settings,
|
||||
Users,
|
||||
} from "lucide-react";
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
@@ -13,7 +19,6 @@ import { PlusIcon } from "lucide-react";
|
||||
import Link from "next/link";
|
||||
import { StringParam, useQueryParams } from "use-query-params";
|
||||
import { Input } from "@/src/components/ui/input";
|
||||
import { Alert, AlertTitle, AlertDescription } from "@/src/components/ui/alert";
|
||||
import { useHasOrganizationAccess } from "@/src/features/rbac/utils/checkOrganizationAccess";
|
||||
import { env } from "@/src/env.mjs";
|
||||
import { Divider } from "@tremor/react";
|
||||
@@ -178,7 +183,12 @@ export const OrganizationProjectOverview = () => {
|
||||
{!queryOrgId && (
|
||||
<>
|
||||
<Header
|
||||
title="Home"
|
||||
title="Organizations"
|
||||
help={{
|
||||
description:
|
||||
"Organizations help you manage access to projects. Each organization can have multiple projects and team members with different roles.",
|
||||
href: "https://langfuse.com/docs/rbac",
|
||||
}}
|
||||
actionButtons={
|
||||
<>
|
||||
<Input
|
||||
@@ -197,7 +207,6 @@ export const OrganizationProjectOverview = () => {
|
||||
</>
|
||||
}
|
||||
/>
|
||||
{!showOnboarding && <IntroducingOrganizations />}
|
||||
</>
|
||||
)}
|
||||
{showOnboarding && <Onboarding />}
|
||||
@@ -227,29 +236,11 @@ export const OrganizationProjectOverview = () => {
|
||||
);
|
||||
};
|
||||
|
||||
const IntroducingOrganizations = () => (
|
||||
<Alert className="mb-10 mt-5">
|
||||
<Building2 className="h-4 w-4" />
|
||||
<AlertTitle>Introducing Organizations</AlertTitle>
|
||||
<AlertDescription>
|
||||
Organizations are a way to group projects and manage access to them. See{" "}
|
||||
<Link
|
||||
href="https://langfuse.com/changelog/2024-08-13-organizations"
|
||||
className="underline"
|
||||
target="_blank"
|
||||
>
|
||||
changelog
|
||||
</Link>{" "}
|
||||
to learn more.
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
);
|
||||
|
||||
const Onboarding = () => {
|
||||
const session = useSession();
|
||||
const canCreateOrgs = session.data?.user?.canCreateOrganizations;
|
||||
return (
|
||||
<Card>
|
||||
<Card className="mt-5">
|
||||
<CardHeader>
|
||||
<CardTitle data-testid="create-new-project-title">
|
||||
Get Started
|
||||
@@ -272,9 +263,15 @@ const Onboarding = () => {
|
||||
</Button>
|
||||
)}
|
||||
<Button variant="secondary" asChild>
|
||||
<Link href="/support">
|
||||
<LifeBuoy className="mr-2 h-4 w-4" aria-hidden="true" />
|
||||
Support
|
||||
<Link href="https://langfuse.com/docs" target="_blank">
|
||||
<BookOpen className="mr-2 h-4 w-4" aria-hidden="true" />
|
||||
Docs
|
||||
</Link>
|
||||
</Button>
|
||||
<Button variant="secondary" asChild>
|
||||
<Link href="https://langfuse.com/docs/ask-ai" target="_blank">
|
||||
<MessageSquareText className="mr-2 h-4 w-4" aria-hidden="true" />
|
||||
Ask AI
|
||||
</Link>
|
||||
</Button>
|
||||
</CardFooter>
|
||||
|
||||
@@ -159,6 +159,12 @@ export const PromptDetail = () => {
|
||||
]}
|
||||
actionButtons={
|
||||
<>
|
||||
<JumpToPlaygroundButton
|
||||
source="prompt"
|
||||
prompt={prompt}
|
||||
analyticsEventName="prompt_detail:test_in_playground_button_click"
|
||||
variant="outline"
|
||||
/>
|
||||
{hasAccess ? (
|
||||
<>
|
||||
{hasEntitlement && (
|
||||
@@ -168,11 +174,11 @@ export const PromptDetail = () => {
|
||||
>
|
||||
<DialogTrigger asChild disabled={!hasExperimentWriteAccess}>
|
||||
<Button
|
||||
variant="secondary"
|
||||
variant="outline"
|
||||
disabled={!hasExperimentWriteAccess}
|
||||
>
|
||||
<FlaskConical className="h-4 w-4" />
|
||||
<span className="ml-2">New experiment</span>
|
||||
<span className="ml-2">Experiment</span>
|
||||
</Button>
|
||||
</DialogTrigger>
|
||||
<DialogContent className="max-h-[90vh] overflow-y-auto">
|
||||
@@ -194,7 +200,7 @@ export const PromptDetail = () => {
|
||||
)}
|
||||
|
||||
<Button
|
||||
variant="secondary"
|
||||
variant="default"
|
||||
onClick={() => {
|
||||
capture("prompts:update_form_open");
|
||||
}}
|
||||
@@ -217,12 +223,6 @@ export const PromptDetail = () => {
|
||||
</div>
|
||||
</Button>
|
||||
)}
|
||||
<JumpToPlaygroundButton
|
||||
source="prompt"
|
||||
prompt={prompt}
|
||||
analyticsEventName="prompt_detail:test_in_playground_button_click"
|
||||
variant="outline"
|
||||
/>
|
||||
<DetailPageNav
|
||||
key="nav"
|
||||
currentId={promptName}
|
||||
@@ -262,7 +262,11 @@ export const PromptDetail = () => {
|
||||
</div>
|
||||
<div className="col-span-2 md:h-full">
|
||||
{prompt.type === PromptType.Chat && chatMessages ? (
|
||||
<OpenAiMessageView title="Chat prompt" messages={chatMessages} />
|
||||
<OpenAiMessageView
|
||||
title="Chat prompt"
|
||||
messages={chatMessages}
|
||||
collapseLongHistory={false}
|
||||
/>
|
||||
) : typeof prompt.prompt === "string" ? (
|
||||
<CodeView content={prompt.prompt} title="Text prompt" />
|
||||
) : (
|
||||
|
||||
@@ -12,7 +12,6 @@ import {
|
||||
} from "@/src/server/api/trpc";
|
||||
import { type Prompt, Prisma } from "@langfuse/shared/src/db";
|
||||
import { createPrompt } from "../actions/createPrompt";
|
||||
import { type ScoreSimplified } from "@langfuse/shared";
|
||||
import { promptsTableCols } from "@/src/server/api/definitions/promptsTable";
|
||||
import { optionalPaginationZod, paginationZod } from "@langfuse/shared";
|
||||
import { orderBy, singleFilter } from "@langfuse/shared";
|
||||
@@ -28,7 +27,6 @@ import {
|
||||
getAggregatedScoresForPrompts,
|
||||
} from "@langfuse/shared/src/server";
|
||||
import { aggregateScores } from "@/src/features/scores/lib/aggregateScores";
|
||||
import { measureAndReturnApi } from "@/src/server/utils/checkClickhouseAccess";
|
||||
|
||||
const PromptFilterOptions = z.object({
|
||||
projectId: z.string(), // Required for protectedProjectProcedure
|
||||
@@ -107,63 +105,16 @@ export const promptRouter = createTRPCRouter({
|
||||
queryClickhouse: z.boolean().default(false),
|
||||
}),
|
||||
)
|
||||
.query(async ({ input, ctx }) => {
|
||||
.query(async ({ input }) => {
|
||||
if (input.promptNames.length === 0) return [];
|
||||
|
||||
return await measureAndReturnApi({
|
||||
input,
|
||||
operation: "prompts.metrics",
|
||||
user: ctx.session.user,
|
||||
pgExecution: async () => {
|
||||
const promptCounts = await ctx.prisma.$queryRaw<
|
||||
{
|
||||
promptName: string;
|
||||
observationCount: bigint;
|
||||
}[]
|
||||
>(
|
||||
Prisma.sql`
|
||||
WITH prompt_ids AS (
|
||||
SELECT
|
||||
p.id,
|
||||
p.name
|
||||
FROM
|
||||
prompts p
|
||||
WHERE
|
||||
p.project_id = ${input.projectId}
|
||||
AND p.name IN (${Prisma.join(input.promptNames)})
|
||||
)
|
||||
SELECT
|
||||
p.name AS "promptName", SUM(oc.observation_count) AS "observationCount"
|
||||
FROM
|
||||
prompt_ids p
|
||||
LEFT JOIN LATERAL (
|
||||
SELECT
|
||||
COUNT(*) AS observation_count
|
||||
FROM
|
||||
observations o
|
||||
WHERE
|
||||
o.project_id = ${input.projectId}
|
||||
AND o.prompt_id = p.id) oc ON TRUE
|
||||
GROUP BY
|
||||
p.name
|
||||
`,
|
||||
);
|
||||
return promptCounts.map(({ promptName, observationCount }) => ({
|
||||
promptName,
|
||||
observationCount: Number(observationCount),
|
||||
}));
|
||||
},
|
||||
clickhouseExecution: async () => {
|
||||
const res = await getObservationsWithPromptName(
|
||||
input.projectId,
|
||||
input.promptNames,
|
||||
);
|
||||
return res.map(({ promptName, count }) => ({
|
||||
promptName,
|
||||
observationCount: count,
|
||||
}));
|
||||
},
|
||||
});
|
||||
const res = await getObservationsWithPromptName(
|
||||
input.projectId,
|
||||
input.promptNames,
|
||||
);
|
||||
return res.map(({ promptName, count }) => ({
|
||||
promptName,
|
||||
observationCount: count,
|
||||
}));
|
||||
}),
|
||||
byId: protectedProjectProcedure
|
||||
.input(
|
||||
@@ -708,171 +659,34 @@ export const promptRouter = createTRPCRouter({
|
||||
scope: "prompts:read",
|
||||
});
|
||||
|
||||
return await measureAndReturnApi({
|
||||
input,
|
||||
operation: "prompts.versionMetrics",
|
||||
user: ctx.session.user,
|
||||
pgExecution: async () => {
|
||||
if (input.promptIds.length === 0) return [];
|
||||
const [metrics, generationScores, traceScores] = await Promise.all([
|
||||
// metrics
|
||||
ctx.prisma.$queryRaw<
|
||||
Array<{
|
||||
id: string;
|
||||
observationCount: bigint;
|
||||
firstUsed: Date | null;
|
||||
lastUsed: Date | null;
|
||||
medianOutputTokens: number | null;
|
||||
medianInputTokens: number | null;
|
||||
medianTotalCost: number | null;
|
||||
medianLatency: number | null;
|
||||
}>
|
||||
>(
|
||||
Prisma.sql`
|
||||
select p.id, p.version, observation_metrics.* from prompts p
|
||||
LEFT JOIN LATERAL (
|
||||
SELECT
|
||||
count(*) AS "observationCount",
|
||||
MIN(o.start_time) AS "firstUsed",
|
||||
MAX(o.start_time) AS "lastUsed",
|
||||
PERCENTILE_CONT(0.5) WITHIN GROUP(ORDER BY o.completion_tokens) AS "medianOutputTokens",
|
||||
PERCENTILE_CONT(0.5) WITHIN GROUP(ORDER BY o.prompt_tokens) AS "medianInputTokens",
|
||||
PERCENTILE_CONT(0.5) WITHIN GROUP(ORDER BY o.calculated_total_cost) AS "medianTotalCost",
|
||||
PERCENTILE_CONT(0.5) WITHIN GROUP(ORDER BY o.latency) AS "medianLatency"
|
||||
FROM
|
||||
"observations_view" o
|
||||
WHERE
|
||||
o.prompt_id = p.id
|
||||
AND "type" = 'GENERATION'
|
||||
AND "project_id" = ${input.projectId}
|
||||
) AS observation_metrics ON true
|
||||
WHERE "project_id" = ${input.projectId}
|
||||
AND p.id in (${Prisma.join(input.promptIds)})
|
||||
ORDER BY version DESC
|
||||
`,
|
||||
),
|
||||
// generationScores
|
||||
ctx.prisma.$queryRaw<
|
||||
Array<{
|
||||
promptId: string;
|
||||
scores: Array<ScoreSimplified>;
|
||||
}>
|
||||
>(Prisma.sql`
|
||||
SELECT
|
||||
p.id AS "promptId",
|
||||
array_agg(s.score) AS "scores"
|
||||
FROM
|
||||
prompts p
|
||||
LEFT JOIN LATERAL (
|
||||
SELECT
|
||||
jsonb_build_object ('name', s.name, 'stringValue', s.string_value, 'value', s.value, 'source', s."source", 'dataType', s.data_type, 'comment', s.comment) AS "score"
|
||||
FROM
|
||||
observations AS o
|
||||
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.prompt_id IS NOT NULL
|
||||
AND o.type = 'GENERATION'
|
||||
AND o.prompt_id = p.id
|
||||
AND o.project_id = ${input.projectId}
|
||||
AND s.name IS NOT NULL
|
||||
AND p.id IN (${Prisma.join(input.promptIds)})
|
||||
) s ON TRUE
|
||||
WHERE
|
||||
p.project_id = ${input.projectId}
|
||||
AND s.score IS NOT NULL
|
||||
GROUP BY
|
||||
p.id
|
||||
`),
|
||||
// traceScores
|
||||
ctx.prisma.$queryRaw<
|
||||
Array<{
|
||||
promptId: string;
|
||||
scores: Array<ScoreSimplified>;
|
||||
}>
|
||||
>(Prisma.sql`
|
||||
SELECT
|
||||
p.id AS "promptId",
|
||||
array_agg(s.score) AS "scores"
|
||||
FROM
|
||||
prompts p
|
||||
LEFT JOIN LATERAL (
|
||||
SELECT
|
||||
jsonb_build_object ('name', s.name, 'stringValue', s.string_value, 'value', s.value, 'source', s."source", 'dataType', s.data_type, 'comment', s.comment) AS "score"
|
||||
FROM
|
||||
scores s
|
||||
WHERE
|
||||
s.trace_id IN (
|
||||
SELECT o.trace_id
|
||||
FROM observations o
|
||||
WHERE
|
||||
o.prompt_id IS NOT NULL
|
||||
AND o.prompt_id = p.id
|
||||
AND o.type = 'GENERATION'
|
||||
AND o.project_id = ${input.projectId}
|
||||
AND o.prompt_id IN (${Prisma.join(input.promptIds)})
|
||||
)
|
||||
AND s.observation_id IS NULL
|
||||
AND s.project_id = ${input.projectId}
|
||||
) s ON TRUE
|
||||
WHERE
|
||||
p.project_id = ${input.projectId}
|
||||
AND s.score IS NOT NULL
|
||||
GROUP BY
|
||||
p.id
|
||||
`),
|
||||
]);
|
||||
const [observations, observationScores, traceScores] = await Promise.all([
|
||||
getObservationMetricsForPrompts(input.projectId, input.promptIds),
|
||||
getScoresForPromptIds(input.projectId, input.promptIds, "observation"),
|
||||
getScoresForPromptIds(input.projectId, input.promptIds, "trace"),
|
||||
]);
|
||||
|
||||
return metrics.map((metric) => ({
|
||||
...metric,
|
||||
observationScores: aggregateScores(
|
||||
generationScores.find((score) => score.promptId === metric.id)
|
||||
?.scores ?? [],
|
||||
),
|
||||
traceScores: aggregateScores(
|
||||
traceScores.find((score) => score.promptId === metric.id)
|
||||
?.scores ?? [],
|
||||
),
|
||||
}));
|
||||
},
|
||||
clickhouseExecution: async () => {
|
||||
console.log("clickhouseExecution", input.promptIds);
|
||||
const [observations, observationScores, traceScores] =
|
||||
await Promise.all([
|
||||
getObservationMetricsForPrompts(input.projectId, input.promptIds),
|
||||
getScoresForPromptIds(
|
||||
input.projectId,
|
||||
input.promptIds,
|
||||
"observation",
|
||||
),
|
||||
getScoresForPromptIds(input.projectId, input.promptIds, "trace"),
|
||||
]);
|
||||
return observations.map((r) => {
|
||||
const promptObservationScores = observationScores.find(
|
||||
(score) => score.promptId === r.promptId,
|
||||
);
|
||||
const promptTraceScores = traceScores.find(
|
||||
(score) => score.promptId === r.promptId,
|
||||
);
|
||||
|
||||
return observations.map((r) => {
|
||||
const promptObservationScores = observationScores.find(
|
||||
(score) => score.promptId === r.promptId,
|
||||
);
|
||||
const promptTraceScores = traceScores.find(
|
||||
(score) => score.promptId === r.promptId,
|
||||
);
|
||||
|
||||
return {
|
||||
id: r.promptId,
|
||||
observationCount: BigInt(r.count),
|
||||
firstUsed: r.firstObservation,
|
||||
lastUsed: r.lastObservation,
|
||||
medianOutputTokens: r.medianOutputUsage,
|
||||
medianInputTokens: r.medianInputUsage,
|
||||
medianTotalCost: r.medianTotalCost,
|
||||
medianLatency: r.medianLatencyMs,
|
||||
observationScores: aggregateScores(
|
||||
promptObservationScores?.scores ?? [],
|
||||
),
|
||||
traceScores: aggregateScores(promptTraceScores?.scores ?? []),
|
||||
};
|
||||
});
|
||||
},
|
||||
return {
|
||||
id: r.promptId,
|
||||
observationCount: BigInt(r.count),
|
||||
firstUsed: r.firstObservation,
|
||||
lastUsed: r.lastObservation,
|
||||
medianOutputTokens: r.medianOutputUsage,
|
||||
medianInputTokens: r.medianInputUsage,
|
||||
medianTotalCost: r.medianTotalCost,
|
||||
medianLatency: r.medianLatencyMs,
|
||||
observationScores: aggregateScores(
|
||||
promptObservationScores?.scores ?? [],
|
||||
),
|
||||
traceScores: aggregateScores(promptTraceScores?.scores ?? []),
|
||||
};
|
||||
});
|
||||
}),
|
||||
});
|
||||
|
||||
@@ -256,8 +256,22 @@ export const QuickstartExamples = ({
|
||||
>
|
||||
FAQ post
|
||||
</a>{" "}
|
||||
for common resolutions or{" "}
|
||||
<Link className="underline" href="/support">
|
||||
for common resolutions,{" "}
|
||||
<Link
|
||||
className="underline"
|
||||
href="https://langfuse.com/docs/ask-ai"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
Ask AI
|
||||
</Link>{" "}
|
||||
or{" "}
|
||||
<Link
|
||||
className="underline"
|
||||
href="https://langfuse.com/support"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
get support
|
||||
</Link>
|
||||
.
|
||||
|
||||
@@ -100,9 +100,9 @@ export class RateLimitService {
|
||||
isFirstInDuration: err.isFirstInDuration,
|
||||
};
|
||||
} else {
|
||||
// Some other error occurred, rethrow it
|
||||
// Some other error occurred, return undefined to fail open
|
||||
logger.error("Internal Rate limit error", err);
|
||||
throw err;
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -66,7 +66,7 @@ export const generateObservationsForPublicApi = async (props: QueryType) => {
|
||||
${props.limit !== undefined && props.page !== undefined ? `LIMIT {limit: Int32} OFFSET {offset: Int32}` : ""}
|
||||
`;
|
||||
|
||||
const records = await queryClickhouse<ObservationRecordReadType>({
|
||||
const result = await queryClickhouse<ObservationRecordReadType>({
|
||||
query,
|
||||
params: {
|
||||
...appliedFilter.params,
|
||||
@@ -77,7 +77,7 @@ export const generateObservationsForPublicApi = async (props: QueryType) => {
|
||||
: {}),
|
||||
},
|
||||
});
|
||||
return records.map(convertObservationToView);
|
||||
return result.map(convertObservationToView);
|
||||
};
|
||||
|
||||
export const getObservationsCountForPublicApi = async (props: QueryType) => {
|
||||
|
||||
@@ -481,6 +481,7 @@ export function AnnotateDrawerContent({
|
||||
...score,
|
||||
scoreId: createdScore.id,
|
||||
value: createdScore.value,
|
||||
stringValue: createdScore.stringValue ?? undefined,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -80,7 +80,7 @@ export function SetupPage() {
|
||||
"Create a new organization. This will be used to manage your projects and teams.",
|
||||
}}
|
||||
/>
|
||||
<div className="md:container md:mx-auto">
|
||||
<div className="mt-3 md:container md:mx-auto">
|
||||
<Breadcrumb className="mb-3">
|
||||
<BreadcrumbList>
|
||||
<BreadcrumbItem>
|
||||
@@ -142,11 +142,17 @@ export function SetupPage() {
|
||||
{
|
||||
// 1. Create Org
|
||||
stepInt === 1 && (
|
||||
<NewOrganizationForm
|
||||
onSuccess={(orgId) => {
|
||||
router.push(inviteMembersRoute(orgId));
|
||||
}}
|
||||
/>
|
||||
<div>
|
||||
<Header title="New Organization" level="h3" />
|
||||
<p className="mb-4 text-sm text-muted-foreground">
|
||||
Organizations are used to manage your projects and teams.
|
||||
</p>
|
||||
<NewOrganizationForm
|
||||
onSuccess={(orgId) => {
|
||||
router.push(inviteMembersRoute(orgId));
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
{
|
||||
@@ -155,6 +161,10 @@ export function SetupPage() {
|
||||
<div className="flex flex-col gap-10">
|
||||
<div>
|
||||
<Header title="Organization Members" level="h3" />
|
||||
<p className="mb-4 text-sm text-muted-foreground">
|
||||
Invite members to your organization to collaborate on
|
||||
projects. You can always add more members later.
|
||||
</p>
|
||||
<MembersTable orgId={organization.id} />
|
||||
</div>
|
||||
<div>
|
||||
@@ -166,12 +176,20 @@ export function SetupPage() {
|
||||
{
|
||||
// 3. Create Project
|
||||
stepInt === 3 && organization && (
|
||||
<NewProjectForm
|
||||
orgId={organization.id}
|
||||
onSuccess={(projectId) =>
|
||||
router.push(setupTracingRoute(projectId))
|
||||
}
|
||||
/>
|
||||
<div>
|
||||
<Header title="New Project" level="h3" />
|
||||
<p className="mb-4 text-sm text-muted-foreground">
|
||||
Projects are used to group traces, datasets, evals and
|
||||
prompts. Multiple environments are best separated via tags
|
||||
within a project.
|
||||
</p>
|
||||
<NewProjectForm
|
||||
orgId={organization.id}
|
||||
onSuccess={(projectId) =>
|
||||
router.push(setupTracingRoute(projectId))
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
{
|
||||
@@ -180,6 +198,10 @@ export function SetupPage() {
|
||||
<div className="space-y-8">
|
||||
<div>
|
||||
<Header title="API Keys" level="h3" />
|
||||
<p className="mb-4 text-sm text-muted-foreground">
|
||||
These keys are used to authenticate your API requests. You
|
||||
can create more keys later in the project settings.
|
||||
</p>
|
||||
<TracingSetup
|
||||
projectId={project.id}
|
||||
hasAnyTrace={hasAnyTrace ?? false}
|
||||
@@ -266,6 +288,10 @@ const TracingSetup = ({
|
||||
level="h3"
|
||||
status={hasAnyTrace ? "active" : "pending"}
|
||||
/>
|
||||
<p className="mb-4 text-sm text-muted-foreground">
|
||||
Tracing is used to track and analyze your LLM calls. You can always
|
||||
skip this step and setup tracing later.
|
||||
</p>
|
||||
<QuickstartExamples
|
||||
secretKey={apiKeys.secretKey}
|
||||
publicKey={apiKeys.publicKey}
|
||||
|
||||
@@ -115,4 +115,9 @@ export const showChat = () => {
|
||||
}
|
||||
};
|
||||
|
||||
export const chatIsVisible = () => {
|
||||
if (!chatAvailable) return false;
|
||||
return sessionStorage.getItem("supportChatVisible") === "true";
|
||||
};
|
||||
|
||||
export const chatAvailable = !!process.env.NEXT_PUBLIC_CRISP_WEBSITE_ID;
|
||||
|
||||
@@ -11,7 +11,7 @@ import {
|
||||
import { env } from "@/src/env.mjs";
|
||||
|
||||
// Interval between jobs in minutes
|
||||
const JOB_INTERVAL_MINUTES = Prisma.raw("24 * 60"); // 24 hours
|
||||
const JOB_INTERVAL_MINUTES = Prisma.raw("720"); // 12 hours
|
||||
|
||||
// Timeout for job in minutes, if job is not finished in this time, it will be retried
|
||||
const JOB_TIMEOUT_MINUTES = Prisma.raw("10"); // 10 minutes
|
||||
|
||||
@@ -109,6 +109,7 @@ const MyApp: AppType<{ session: Session | null }> = ({
|
||||
<Component {...pageProps} />
|
||||
<UserTracking />
|
||||
</Layout>
|
||||
<BetterStackUptimeStatusMessage />
|
||||
</ThemeProvider>
|
||||
</MarkdownContextProvider>
|
||||
<CrispWidget />
|
||||
@@ -248,3 +249,15 @@ if (
|
||||
return await shutdown(signal);
|
||||
});
|
||||
}
|
||||
|
||||
function BetterStackUptimeStatusMessage() {
|
||||
if (!env.NEXT_PUBLIC_LANGFUSE_CLOUD_REGION) return null;
|
||||
return (
|
||||
<script
|
||||
src="https://uptime.betterstack.com/widgets/announcement.js"
|
||||
data-id="189328"
|
||||
async={true}
|
||||
type="text/javascript"
|
||||
></script>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -9,7 +9,6 @@ import {
|
||||
import { LangfuseNotFoundError, InvalidRequestError } from "@langfuse/shared";
|
||||
import { addDatasetRunItemsToEvalQueue } from "@/src/ee/features/evals/server/addDatasetRunItemsToEvalQueue";
|
||||
import { getObservationById } from "@langfuse/shared/src/server";
|
||||
import { env } from "@/src/env.mjs";
|
||||
|
||||
export default withMiddlewares({
|
||||
POST: createAuthedAPIRoute({
|
||||
@@ -51,20 +50,11 @@ export default withMiddlewares({
|
||||
|
||||
// Backwards compatibility: historically, dataset run items were linked to observations, not traces
|
||||
if (!traceId && observationId) {
|
||||
const observation =
|
||||
env.LANGFUSE_RETURN_FROM_CLICKHOUSE === "true"
|
||||
? await getObservationById(
|
||||
observationId,
|
||||
auth.scope.projectId,
|
||||
true,
|
||||
)
|
||||
: await prisma.observation.findUnique({
|
||||
where: {
|
||||
id: observationId,
|
||||
projectId: auth.scope.projectId,
|
||||
},
|
||||
});
|
||||
|
||||
const observation = await getObservationById(
|
||||
observationId,
|
||||
auth.scope.projectId,
|
||||
true,
|
||||
);
|
||||
if (observationId && !observation) {
|
||||
throw new LangfuseNotFoundError("Observation not found");
|
||||
}
|
||||
|
||||
@@ -1,13 +1,9 @@
|
||||
import { Prisma, prisma } from "@langfuse/shared/src/db";
|
||||
import {
|
||||
GetMetricsDailyV1Query,
|
||||
GetMetricsDailyV1Response,
|
||||
} from "@/src/features/public-api/types/metrics";
|
||||
import { withMiddlewares } from "@/src/features/public-api/server/withMiddlewares";
|
||||
import { createAuthedAPIRoute } from "@/src/features/public-api/server/createAuthedAPIRoute";
|
||||
import { type z } from "zod";
|
||||
import { env } from "@/src/env.mjs";
|
||||
import { measureAndReturnApi } from "@/src/server/utils/checkClickhouseAccess";
|
||||
import {
|
||||
generateDailyMetrics,
|
||||
getDailyMetricsCount,
|
||||
@@ -20,162 +16,32 @@ export default withMiddlewares({
|
||||
responseSchema: GetMetricsDailyV1Response,
|
||||
rateLimitResource: "public-api-metrics",
|
||||
fn: async ({ query, auth }) => {
|
||||
return measureAndReturnApi({
|
||||
input: { projectId: auth.scope.projectId, queryClickhouse: false },
|
||||
operation: "api/public/metrics/daily",
|
||||
user: null,
|
||||
pgExecution: async () => {
|
||||
const traceNameCondition = query.traceName
|
||||
? Prisma.sql`AND t.name = ${query.traceName}`
|
||||
: Prisma.empty;
|
||||
const userCondition = query.userId
|
||||
? Prisma.sql`AND t."user_id" = ${query.userId}`
|
||||
: Prisma.empty;
|
||||
const tagsCondition = query.tags
|
||||
? Prisma.sql`AND ARRAY[${Prisma.join(
|
||||
(Array.isArray(query.tags)
|
||||
? query.tags
|
||||
: query.tags.split(",")
|
||||
).map((v) => Prisma.sql`${v}`),
|
||||
", ",
|
||||
)}] <@ t."tags"`
|
||||
: Prisma.empty;
|
||||
const fromTimestampCondition = query.fromTimestamp
|
||||
? Prisma.sql`AND t."timestamp" >= ${query.fromTimestamp}::timestamp with time zone at time zone 'UTC'`
|
||||
: Prisma.empty;
|
||||
const toTimestampCondition = query.toTimestamp
|
||||
? Prisma.sql`AND t."timestamp" < ${query.toTimestamp}::timestamp with time zone at time zone 'UTC'`
|
||||
: Prisma.empty;
|
||||
const fromObservationStartTimeCondition = query.fromTimestamp
|
||||
? Prisma.sql`AND o."start_time" >= ${query.fromTimestamp}::timestamp with time zone at time zone 'UTC' - INTERVAL '1 day'`
|
||||
: Prisma.empty;
|
||||
const toObservationStartTimeCondition = query.toTimestamp
|
||||
? Prisma.sql`AND o."start_time" < ${query.toTimestamp}::timestamp with time zone at time zone 'UTC' + INTERVAL '1 day'`
|
||||
: Prisma.empty;
|
||||
const filterProps = {
|
||||
projectId: auth.scope.projectId,
|
||||
page: query.page ?? undefined,
|
||||
limit: query.limit ?? undefined,
|
||||
traceName: query.traceName ?? undefined,
|
||||
userId: query.userId ?? undefined,
|
||||
tags: query.tags ?? undefined,
|
||||
fromTimestamp: query.fromTimestamp ?? undefined,
|
||||
toTimestamp: query.toTimestamp ?? undefined,
|
||||
};
|
||||
|
||||
// TODO: We can use observations as soon as we compute costs on write and backfill self-hosters
|
||||
const observationtable =
|
||||
env.NEXT_PUBLIC_LANGFUSE_CLOUD_REGION === undefined
|
||||
? Prisma.sql`observations_view`
|
||||
: Prisma.sql`observations`;
|
||||
const [usage, count] = await Promise.all([
|
||||
generateDailyMetrics(filterProps),
|
||||
getDailyMetricsCount(filterProps),
|
||||
]);
|
||||
|
||||
const [usage, totalItemsRes] = await Promise.all([
|
||||
prisma.$queryRaw`
|
||||
WITH model_usage AS (
|
||||
SELECT DATE_TRUNC('DAY', o.start_time) "date",
|
||||
o.model,
|
||||
count(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 ${observationtable} 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 = ${auth.scope.projectId}
|
||||
${traceNameCondition}
|
||||
${userCondition}
|
||||
${tagsCondition}
|
||||
${fromTimestampCondition}
|
||||
${toTimestampCondition}
|
||||
${fromObservationStartTimeCondition}
|
||||
${toObservationStartTimeCondition}
|
||||
GROUP BY 1, 2),
|
||||
daily_model_usage AS (
|
||||
SELECT "date",
|
||||
sum("countObservations")::integer "countObservations",
|
||||
sum("totalCost")::DOUBLE PRECISION "totalCost",
|
||||
json_agg(json_build_object(
|
||||
'model', "model",
|
||||
'inputUsage', "inputUsage",
|
||||
'outputUsage', "outputUsage",
|
||||
'totalUsage', "totalUsage",
|
||||
'totalCost', "totalCost",
|
||||
'countObservations', "countObservations",
|
||||
'countTraces', "countTraces"
|
||||
)) daily_usage_json
|
||||
FROM model_usage
|
||||
GROUP BY 1),
|
||||
trace_usage AS (
|
||||
SELECT DATE_TRUNC('DAY', t.timestamp) "date",
|
||||
count(t.id)::integer as "countTraces"
|
||||
FROM traces t
|
||||
WHERE t.project_id = ${auth.scope.projectId}
|
||||
${traceNameCondition}
|
||||
${userCondition}
|
||||
${tagsCondition}
|
||||
${fromTimestampCondition}
|
||||
${toTimestampCondition}
|
||||
GROUP BY 1)
|
||||
|
||||
SELECT TO_CHAR(COALESCE(trace_usage.date, daily_model_usage.date), 'YYYY-MM-DD') AS "date",
|
||||
COALESCE(trace_usage."countTraces", 0) "countTraces",
|
||||
COALESCE("countObservations", 0) "countObservations",
|
||||
COALESCE("totalCost", 0) "totalCost",
|
||||
COALESCE(daily_usage_json, '[]'::JSON) usage
|
||||
FROM daily_model_usage
|
||||
FULL OUTER JOIN trace_usage
|
||||
ON daily_model_usage."date" = trace_usage."date"
|
||||
ORDER BY 1 DESC
|
||||
LIMIT ${query.limit} OFFSET ${(query.page - 1) * query.limit}
|
||||
`,
|
||||
prisma.$queryRaw<{ count: number }[]>`
|
||||
SELECT COUNT(DISTINCT DATE_TRUNC('DAY', t.timestamp))::integer
|
||||
FROM traces t
|
||||
WHERE t.project_id = ${auth.scope.projectId}
|
||||
${traceNameCondition}
|
||||
${userCondition}
|
||||
${tagsCondition}
|
||||
${fromTimestampCondition}
|
||||
${toTimestampCondition}
|
||||
`,
|
||||
]);
|
||||
|
||||
const totalItems = totalItemsRes[0]?.count ?? 0;
|
||||
|
||||
return {
|
||||
// cannot use type warnings due to query raw, covered by tests
|
||||
data: usage as z.infer<typeof GetMetricsDailyV1Response>["data"],
|
||||
meta: {
|
||||
page: query.page,
|
||||
limit: query.limit,
|
||||
totalItems,
|
||||
totalPages: Math.ceil(totalItems / query.limit),
|
||||
},
|
||||
};
|
||||
const finalCount = count || 0;
|
||||
return {
|
||||
data: usage,
|
||||
meta: {
|
||||
page: query.page,
|
||||
limit: query.limit,
|
||||
totalItems: finalCount,
|
||||
totalPages: Math.ceil(finalCount / query.limit),
|
||||
},
|
||||
clickhouseExecution: async () => {
|
||||
const filterProps = {
|
||||
projectId: auth.scope.projectId,
|
||||
page: query.page ?? undefined,
|
||||
limit: query.limit ?? undefined,
|
||||
traceName: query.traceName ?? undefined,
|
||||
userId: query.userId ?? undefined,
|
||||
tags: query.tags ?? undefined,
|
||||
fromTimestamp: query.fromTimestamp ?? undefined,
|
||||
toTimestamp: query.toTimestamp ?? undefined,
|
||||
};
|
||||
|
||||
const [usage, count] = await Promise.all([
|
||||
generateDailyMetrics(filterProps),
|
||||
getDailyMetricsCount(filterProps),
|
||||
]);
|
||||
|
||||
const finalCount = count || 0;
|
||||
return {
|
||||
data: usage,
|
||||
meta: {
|
||||
page: query.page,
|
||||
limit: query.limit,
|
||||
totalItems: finalCount,
|
||||
totalPages: Math.ceil(finalCount / query.limit),
|
||||
},
|
||||
};
|
||||
},
|
||||
});
|
||||
};
|
||||
},
|
||||
}),
|
||||
});
|
||||
|
||||
@@ -7,7 +7,6 @@ import {
|
||||
import { withMiddlewares } from "@/src/features/public-api/server/withMiddlewares";
|
||||
import { createAuthedAPIRoute } from "@/src/features/public-api/server/createAuthedAPIRoute";
|
||||
import { LangfuseNotFoundError } from "@langfuse/shared";
|
||||
import { measureAndReturnApi } from "@/src/server/utils/checkClickhouseAccess";
|
||||
import { getObservationViewById } from "@langfuse/shared/src/server";
|
||||
|
||||
export default withMiddlewares({
|
||||
@@ -16,77 +15,58 @@ export default withMiddlewares({
|
||||
querySchema: GetObservationV1Query,
|
||||
responseSchema: GetObservationV1Response,
|
||||
fn: async ({ query, auth }) => {
|
||||
const { observationId } = query;
|
||||
const observation = await measureAndReturnApi({
|
||||
input: { projectId: auth.scope.projectId, queryClickhouse: false },
|
||||
operation: "api/public/observations/[observationId]",
|
||||
user: null,
|
||||
pgExecution: async () => {
|
||||
return await prisma.observationView.findFirst({
|
||||
where: {
|
||||
id: observationId,
|
||||
projectId: auth.scope.projectId,
|
||||
},
|
||||
});
|
||||
},
|
||||
clickhouseExecution: async () => {
|
||||
const observation = await getObservationViewById(
|
||||
observationId,
|
||||
auth.scope.projectId,
|
||||
true,
|
||||
);
|
||||
if (!observation) {
|
||||
throw new LangfuseNotFoundError(
|
||||
"Observation not found within authorized project",
|
||||
);
|
||||
}
|
||||
const clickhouseObservation = await getObservationViewById(
|
||||
query.observationId,
|
||||
auth.scope.projectId,
|
||||
true,
|
||||
);
|
||||
if (!clickhouseObservation) {
|
||||
throw new LangfuseNotFoundError(
|
||||
"Observation not found within authorized project",
|
||||
);
|
||||
}
|
||||
|
||||
const model = observation.modelId
|
||||
? await prisma.model.findFirst({
|
||||
where: {
|
||||
AND: [
|
||||
const model = clickhouseObservation.modelId
|
||||
? await prisma.model.findFirst({
|
||||
where: {
|
||||
AND: [
|
||||
{
|
||||
id: clickhouseObservation.modelId,
|
||||
},
|
||||
{
|
||||
OR: [
|
||||
{
|
||||
id: observation.modelId,
|
||||
projectId: auth.scope.projectId,
|
||||
},
|
||||
{
|
||||
OR: [
|
||||
{
|
||||
projectId: auth.scope.projectId,
|
||||
},
|
||||
{
|
||||
projectId: null,
|
||||
},
|
||||
],
|
||||
projectId: null,
|
||||
},
|
||||
],
|
||||
},
|
||||
include: {
|
||||
Price: true,
|
||||
},
|
||||
orderBy: {
|
||||
projectId: {
|
||||
sort: "desc",
|
||||
nulls: "last",
|
||||
},
|
||||
},
|
||||
})
|
||||
: undefined;
|
||||
],
|
||||
},
|
||||
include: {
|
||||
Price: true,
|
||||
},
|
||||
orderBy: {
|
||||
projectId: {
|
||||
sort: "desc",
|
||||
nulls: "last",
|
||||
},
|
||||
},
|
||||
})
|
||||
: undefined;
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
|
||||
return {
|
||||
...observation,
|
||||
modelId: model?.id ?? null,
|
||||
inputPrice:
|
||||
model?.Price?.find((m) => m.usageType === "input")?.price ?? null,
|
||||
outputPrice:
|
||||
model?.Price?.find((m) => m.usageType === "output")?.price ??
|
||||
null,
|
||||
totalPrice:
|
||||
model?.Price?.find((m) => m.usageType === "total")?.price ?? null,
|
||||
};
|
||||
},
|
||||
});
|
||||
const observation = {
|
||||
...clickhouseObservation,
|
||||
modelId: model?.id ?? null,
|
||||
inputPrice:
|
||||
model?.Price?.find((m) => m.usageType === "input")?.price ?? null,
|
||||
outputPrice:
|
||||
model?.Price?.find((m) => m.usageType === "output")?.price ?? null,
|
||||
totalPrice:
|
||||
model?.Price?.find((m) => m.usageType === "total")?.price ?? null,
|
||||
};
|
||||
|
||||
if (!observation) {
|
||||
throw new LangfuseNotFoundError(
|
||||
|
||||
@@ -2,15 +2,12 @@ import { prisma } from "@langfuse/shared/src/db";
|
||||
|
||||
import { withMiddlewares } from "@/src/features/public-api/server/withMiddlewares";
|
||||
import { createAuthedAPIRoute } from "@/src/features/public-api/server/createAuthedAPIRoute";
|
||||
import { Prisma, type ObservationView } from "@langfuse/shared/src/db";
|
||||
|
||||
import { InternalServerError } from "@langfuse/shared";
|
||||
import {
|
||||
GetObservationsV1Query,
|
||||
GetObservationsV1Response,
|
||||
transformDbToApiObservation,
|
||||
} from "@/src/features/public-api/types/observations";
|
||||
import { measureAndReturnApi } from "@/src/server/utils/checkClickhouseAccess";
|
||||
import {
|
||||
generateObservationsForPublicApi,
|
||||
getObservationsCountForPublicApi,
|
||||
@@ -22,194 +19,71 @@ export default withMiddlewares({
|
||||
querySchema: GetObservationsV1Query,
|
||||
responseSchema: GetObservationsV1Response,
|
||||
fn: async ({ query, auth }) => {
|
||||
return await measureAndReturnApi({
|
||||
input: { projectId: auth.scope.projectId, queryClickhouse: false },
|
||||
operation: "api/public/observations",
|
||||
user: null,
|
||||
pgExecution: async () => {
|
||||
const userIdCondition = query.userId
|
||||
? Prisma.sql`AND traces."user_id" = ${query.userId}`
|
||||
: Prisma.empty;
|
||||
const filterProps = {
|
||||
projectId: auth.scope.projectId,
|
||||
page: query.page ?? undefined,
|
||||
limit: query.limit ?? undefined,
|
||||
traceId: query.traceId ?? undefined,
|
||||
userId: query.userId ?? undefined,
|
||||
name: query.name ?? undefined,
|
||||
type: query.type ?? undefined,
|
||||
parentObservationId: query.parentObservationId ?? undefined,
|
||||
fromStartTime: query.fromStartTime ?? undefined,
|
||||
toStartTime: query.toStartTime ?? undefined,
|
||||
version: query.version ?? undefined,
|
||||
};
|
||||
const [items, count] = await Promise.all([
|
||||
generateObservationsForPublicApi(filterProps),
|
||||
getObservationsCountForPublicApi(filterProps),
|
||||
]);
|
||||
const uniqueModels: string[] = Array.from(
|
||||
new Set(
|
||||
items.map((r) => r.modelId).filter((r): r is string => Boolean(r)),
|
||||
),
|
||||
);
|
||||
|
||||
const nameCondition = query.name
|
||||
? Prisma.sql`AND o."name" = ${query.name}`
|
||||
: Prisma.empty;
|
||||
const models =
|
||||
uniqueModels.length > 0
|
||||
? await prisma.model.findMany({
|
||||
where: {
|
||||
id: {
|
||||
in: uniqueModels,
|
||||
},
|
||||
OR: [{ projectId: auth.scope.projectId }, { projectId: null }],
|
||||
},
|
||||
include: {
|
||||
Price: true,
|
||||
},
|
||||
})
|
||||
: [];
|
||||
const finalCount = count ? count : 0;
|
||||
|
||||
const observationTypeCondition = query.type
|
||||
? Prisma.sql`AND o."type" = ${query.type}::"ObservationType"`
|
||||
: Prisma.empty;
|
||||
|
||||
const traceIdCondition = query.traceId
|
||||
? Prisma.sql`AND o."trace_id" = ${query.traceId}`
|
||||
: Prisma.empty;
|
||||
|
||||
const parentObservationIdCondition = query.parentObservationId
|
||||
? Prisma.sql`AND o."parent_observation_id" = ${query.parentObservationId}`
|
||||
: Prisma.empty;
|
||||
|
||||
const fromStartTimeCondition = query.fromStartTime
|
||||
? Prisma.sql`AND o."start_time" >= ${query.fromStartTime}::timestamp with time zone at time zone 'UTC'`
|
||||
: Prisma.empty;
|
||||
|
||||
const toStartTimeCondition = query.toStartTime
|
||||
? Prisma.sql`AND o."start_time" < ${query.toStartTime}::timestamp with time zone at time zone 'UTC'`
|
||||
: Prisma.empty;
|
||||
|
||||
const versionCondition = query.version
|
||||
? Prisma.sql`AND o."version" = ${query.version}`
|
||||
: Prisma.empty;
|
||||
|
||||
const observations = await prisma.$queryRaw<ObservationView[]>`
|
||||
SELECT
|
||||
o."id",
|
||||
o."name",
|
||||
o."start_time" AS "startTime",
|
||||
o."end_time" AS "endTime",
|
||||
o."parent_observation_id" AS "parentObservationId",
|
||||
o."type",
|
||||
o."metadata",
|
||||
o."model",
|
||||
o."input",
|
||||
o."output",
|
||||
o."level",
|
||||
o."status_message" AS "statusMessage",
|
||||
o."completion_start_time" AS "completionStartTime",
|
||||
o."completion_tokens" AS "completionTokens",
|
||||
o."prompt_tokens" AS "promptTokens",
|
||||
o."total_tokens" AS "totalTokens",
|
||||
o."unit" AS "unit",
|
||||
o."version",
|
||||
o."project_id" AS "projectId",
|
||||
o."trace_id" AS "traceId",
|
||||
o."modelParameters" AS "modelParameters",
|
||||
o."model_id" as "modelId",
|
||||
o."input_price" as "inputPrice",
|
||||
o."output_price" as "outputPrice",
|
||||
o."total_price" as "totalPrice",
|
||||
o."calculated_input_cost" as "calculatedInputCost",
|
||||
o."calculated_output_cost" as "calculatedOutputCost",
|
||||
o."calculated_total_cost" as "calculatedTotalCost",
|
||||
o."latency",
|
||||
o."prompt_id" as "promptId",
|
||||
o."prompt_name" as "promptName",
|
||||
o."prompt_version" as "promptVersion",
|
||||
o."created_at" as "createdAt",
|
||||
o."updated_at" as "updatedAt",
|
||||
o."time_to_first_token" as "timeToFirstToken"
|
||||
FROM observations_view o LEFT JOIN traces ON o."trace_id" = traces."id" AND traces."project_id" = o."project_id"
|
||||
WHERE o."project_id" = ${auth.scope.projectId}
|
||||
${nameCondition}
|
||||
${userIdCondition}
|
||||
${observationTypeCondition}
|
||||
${traceIdCondition}
|
||||
${versionCondition}
|
||||
${parentObservationIdCondition}
|
||||
${fromStartTimeCondition}
|
||||
${toStartTimeCondition}
|
||||
ORDER by o."start_time" DESC
|
||||
OFFSET ${(query.page - 1) * query.limit}
|
||||
LIMIT ${query.limit}
|
||||
`;
|
||||
const countRes = await prisma.$queryRaw<{ count: bigint }[]>`
|
||||
SELECT COUNT(*) FROM observations o LEFT JOIN traces ON o."trace_id" = traces."id"
|
||||
WHERE o."project_id" = ${auth.scope.projectId}
|
||||
${observationTypeCondition}
|
||||
${nameCondition}
|
||||
${userIdCondition}
|
||||
${traceIdCondition}
|
||||
${versionCondition}
|
||||
${parentObservationIdCondition}
|
||||
${fromStartTimeCondition}
|
||||
${toStartTimeCondition}
|
||||
`;
|
||||
if (countRes.length !== 1) {
|
||||
throw new InternalServerError("Unexpected totalItems result");
|
||||
}
|
||||
const totalItems = Number(countRes[0].count);
|
||||
|
||||
return {
|
||||
data: observations.map(transformDbToApiObservation),
|
||||
meta: {
|
||||
page: query.page,
|
||||
limit: query.limit,
|
||||
totalItems,
|
||||
totalPages: Math.ceil(totalItems / query.limit),
|
||||
},
|
||||
};
|
||||
return {
|
||||
data: items
|
||||
.map((i) => {
|
||||
const model = models.find((m) => m.id === i.modelId);
|
||||
return {
|
||||
...i,
|
||||
modelId: model?.id ?? null,
|
||||
inputPrice:
|
||||
model?.Price?.find((m) => m.usageType === "input")?.price ??
|
||||
null,
|
||||
outputPrice:
|
||||
model?.Price?.find((m) => m.usageType === "output")?.price ??
|
||||
null,
|
||||
totalPrice:
|
||||
model?.Price?.find((m) => m.usageType === "total")?.price ??
|
||||
null,
|
||||
};
|
||||
})
|
||||
.map(transformDbToApiObservation),
|
||||
meta: {
|
||||
page: query.page,
|
||||
limit: query.limit,
|
||||
totalItems: finalCount,
|
||||
totalPages: Math.ceil(finalCount / query.limit),
|
||||
},
|
||||
clickhouseExecution: async () => {
|
||||
const filterProps = {
|
||||
projectId: auth.scope.projectId,
|
||||
page: query.page ?? undefined,
|
||||
limit: query.limit ?? undefined,
|
||||
traceId: query.traceId ?? undefined,
|
||||
userId: query.userId ?? undefined,
|
||||
name: query.name ?? undefined,
|
||||
type: query.type ?? undefined,
|
||||
parentObservationId: query.parentObservationId ?? undefined,
|
||||
fromStartTime: query.fromStartTime ?? undefined,
|
||||
toStartTime: query.toStartTime ?? undefined,
|
||||
version: query.version ?? undefined,
|
||||
};
|
||||
const [items, count] = await Promise.all([
|
||||
generateObservationsForPublicApi(filterProps),
|
||||
getObservationsCountForPublicApi(filterProps),
|
||||
]);
|
||||
const uniqueModels: string[] = Array.from(
|
||||
new Set(
|
||||
items
|
||||
.map((r) => r.modelId)
|
||||
.filter((r): r is string => Boolean(r)),
|
||||
),
|
||||
);
|
||||
|
||||
const models =
|
||||
uniqueModels.length > 0
|
||||
? await prisma.model.findMany({
|
||||
where: {
|
||||
id: {
|
||||
in: uniqueModels,
|
||||
},
|
||||
OR: [
|
||||
{ projectId: auth.scope.projectId },
|
||||
{ projectId: null },
|
||||
],
|
||||
},
|
||||
include: {
|
||||
Price: true,
|
||||
},
|
||||
})
|
||||
: [];
|
||||
const finalCount = count ? count : 0;
|
||||
|
||||
return {
|
||||
data: items
|
||||
.map((i) => {
|
||||
const model = models.find((m) => m.id === i.modelId);
|
||||
return {
|
||||
...i,
|
||||
modelId: model?.id ?? null,
|
||||
inputPrice:
|
||||
model?.Price?.find((m) => m.usageType === "input")?.price ??
|
||||
null,
|
||||
outputPrice:
|
||||
model?.Price?.find((m) => m.usageType === "output")
|
||||
?.price ?? null,
|
||||
totalPrice:
|
||||
model?.Price?.find((m) => m.usageType === "total")?.price ??
|
||||
null,
|
||||
};
|
||||
})
|
||||
.map(transformDbToApiObservation),
|
||||
meta: {
|
||||
page: query.page,
|
||||
limit: query.limit,
|
||||
totalItems: finalCount,
|
||||
totalPages: Math.ceil(finalCount / query.limit),
|
||||
},
|
||||
};
|
||||
},
|
||||
});
|
||||
};
|
||||
},
|
||||
}),
|
||||
});
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { createAuthedAPIRoute } from "@/src/features/public-api/server/createAuthedAPIRoute";
|
||||
import { withMiddlewares } from "@/src/features/public-api/server/withMiddlewares";
|
||||
import { measureAndReturnApi } from "@/src/server/utils/checkClickhouseAccess";
|
||||
import {
|
||||
DeleteScoreQuery,
|
||||
DeleteScoreResponse,
|
||||
@@ -9,7 +8,6 @@ import {
|
||||
InternalServerError,
|
||||
LangfuseNotFoundError,
|
||||
} from "@langfuse/shared";
|
||||
import { prisma } from "@langfuse/shared/src/db";
|
||||
import {
|
||||
deleteScore,
|
||||
getScoreById,
|
||||
@@ -23,24 +21,7 @@ export default withMiddlewares({
|
||||
querySchema: GetScoreQuery,
|
||||
responseSchema: GetScoreResponse,
|
||||
fn: async ({ query, auth }) => {
|
||||
const { scoreId } = query;
|
||||
|
||||
const score = await measureAndReturnApi({
|
||||
input: { projectId: auth.scope.projectId, queryClickhouse: false },
|
||||
operation: "api/public/scores/[scoreId]",
|
||||
user: null,
|
||||
pgExecution: async () => {
|
||||
return await prisma.score.findUnique({
|
||||
where: {
|
||||
id: scoreId,
|
||||
projectId: auth.scope.projectId,
|
||||
},
|
||||
});
|
||||
},
|
||||
clickhouseExecution: async () => {
|
||||
return await getScoreById(auth.scope.projectId, scoreId);
|
||||
},
|
||||
});
|
||||
const score = await getScoreById(auth.scope.projectId, query.scoreId);
|
||||
|
||||
if (!score) {
|
||||
throw new LangfuseNotFoundError("Score not found");
|
||||
@@ -63,17 +44,7 @@ export default withMiddlewares({
|
||||
responseSchema: DeleteScoreResponse,
|
||||
fn: async ({ query, auth }) => {
|
||||
const { scoreId } = query;
|
||||
|
||||
await Promise.all([
|
||||
prisma.score.deleteMany({
|
||||
where: {
|
||||
id: scoreId,
|
||||
projectId: auth.scope.projectId,
|
||||
},
|
||||
}),
|
||||
deleteScore(auth.scope.projectId, scoreId),
|
||||
]);
|
||||
|
||||
await deleteScore(auth.scope.projectId, scoreId);
|
||||
return { message: "Score deleted successfully" };
|
||||
},
|
||||
}),
|
||||
|
||||
@@ -9,14 +9,12 @@ import {
|
||||
PostScoresBody,
|
||||
PostScoresResponse,
|
||||
} from "@langfuse/shared";
|
||||
import { prisma, Prisma } from "@langfuse/shared/src/db";
|
||||
import {
|
||||
eventTypes,
|
||||
logger,
|
||||
processEventBatch,
|
||||
} from "@langfuse/shared/src/server";
|
||||
import { tokenCount } from "@/src/features/ingest/usage";
|
||||
import { measureAndReturnApi } from "@/src/server/utils/checkClickhouseAccess";
|
||||
import {
|
||||
generateScoresForPublicApi,
|
||||
getScoresCountForPublicApi,
|
||||
@@ -57,193 +55,54 @@ export default withMiddlewares({
|
||||
querySchema: GetScoresQuery,
|
||||
responseSchema: GetScoresResponse,
|
||||
fn: async ({ query, auth }) => {
|
||||
const {
|
||||
page,
|
||||
limit,
|
||||
configId,
|
||||
queueId,
|
||||
traceTags,
|
||||
userId,
|
||||
name,
|
||||
fromTimestamp,
|
||||
toTimestamp,
|
||||
source,
|
||||
operator,
|
||||
value,
|
||||
scoreIds,
|
||||
dataType,
|
||||
} = query;
|
||||
const [items, count] = await Promise.all([
|
||||
generateScoresForPublicApi({
|
||||
projectId: auth.scope.projectId,
|
||||
page: query.page ?? undefined,
|
||||
limit: query.limit ?? undefined,
|
||||
userId: query.userId ?? undefined,
|
||||
name: query.name ?? undefined,
|
||||
configId: query.configId ?? undefined,
|
||||
queueId: query.queueId ?? undefined,
|
||||
traceTags: query.traceTags ?? undefined,
|
||||
dataType: query.dataType ?? undefined,
|
||||
fromTimestamp: query.fromTimestamp ?? undefined,
|
||||
toTimestamp: query.toTimestamp ?? undefined,
|
||||
source: query.source ?? undefined,
|
||||
value: query.value ?? undefined,
|
||||
operator: query.operator ?? undefined,
|
||||
scoreIds: query.scoreIds ?? undefined,
|
||||
}),
|
||||
getScoresCountForPublicApi({
|
||||
projectId: auth.scope.projectId,
|
||||
page: query.page ?? undefined,
|
||||
limit: query.limit ?? undefined,
|
||||
userId: query.userId ?? undefined,
|
||||
name: query.name ?? undefined,
|
||||
configId: query.configId ?? undefined,
|
||||
queueId: query.queueId ?? undefined,
|
||||
traceTags: query.traceTags ?? undefined,
|
||||
dataType: query.dataType ?? undefined,
|
||||
fromTimestamp: query.fromTimestamp ?? undefined,
|
||||
toTimestamp: query.toTimestamp ?? undefined,
|
||||
source: query.source ?? undefined,
|
||||
value: query.value ?? undefined,
|
||||
operator: query.operator ?? undefined,
|
||||
scoreIds: query.scoreIds ?? undefined,
|
||||
}),
|
||||
]);
|
||||
|
||||
return await measureAndReturnApi({
|
||||
input: { projectId: auth.scope.projectId, queryClickhouse: false },
|
||||
operation: "api/public/scores",
|
||||
user: null,
|
||||
pgExecution: async () => {
|
||||
const skipValue = (page - 1) * limit;
|
||||
const configCondition = configId
|
||||
? Prisma.sql`AND s."config_id" = ${configId}`
|
||||
: Prisma.empty;
|
||||
const queueCondition = queueId
|
||||
? Prisma.sql`AND s."queue_id" = ${queueId}`
|
||||
: Prisma.empty;
|
||||
const traceTagsCondition = traceTags
|
||||
? Prisma.sql`AND ARRAY[${Prisma.join(
|
||||
(Array.isArray(traceTags)
|
||||
? traceTags
|
||||
: traceTags.split(",")
|
||||
).map((v) => Prisma.sql`${v}`),
|
||||
", ",
|
||||
)}] <@ t."tags"`
|
||||
: Prisma.empty;
|
||||
const dataTypeCondition = dataType
|
||||
? Prisma.sql`AND s."data_type" = ${dataType}::"ScoreDataType"`
|
||||
: Prisma.empty;
|
||||
const userCondition = userId
|
||||
? Prisma.sql`AND t."user_id" = ${userId}`
|
||||
: Prisma.empty;
|
||||
const nameCondition = name
|
||||
? Prisma.sql`AND s."name" = ${name}`
|
||||
: Prisma.empty;
|
||||
const fromTimestampCondition = fromTimestamp
|
||||
? Prisma.sql`AND s."timestamp" >= ${fromTimestamp}::timestamp with time zone at time zone 'UTC'`
|
||||
: Prisma.empty;
|
||||
const toTimestampCondition = toTimestamp
|
||||
? Prisma.sql`AND s."timestamp" < ${toTimestamp}::timestamp with time zone at time zone 'UTC'`
|
||||
: Prisma.empty;
|
||||
const sourceCondition = source
|
||||
? Prisma.sql`AND s."source" = ${source}::"ScoreSource"`
|
||||
: Prisma.empty;
|
||||
const valueCondition =
|
||||
operator && value !== null && value !== undefined
|
||||
? Prisma.sql`AND s."value" ${Prisma.raw(`${operator}`)} ${value}`
|
||||
: Prisma.empty;
|
||||
const scoreIdCondition = scoreIds
|
||||
? Prisma.sql`AND s."id" = ANY(${scoreIds})`
|
||||
: Prisma.empty;
|
||||
const finalCount = count ? count : 0;
|
||||
|
||||
const scores = await prisma.$queryRaw<Array<unknown>>(Prisma.sql`
|
||||
SELECT
|
||||
s.id,
|
||||
s.timestamp,
|
||||
s.name,
|
||||
s.value,
|
||||
s.string_value as "stringValue",
|
||||
s.author_user_id as "authorUserId",
|
||||
s.project_id as "projectId",
|
||||
s.created_at as "createdAt",
|
||||
s.updated_at as "updatedAt",
|
||||
s.source,
|
||||
s.comment,
|
||||
s.data_type as "dataType",
|
||||
s.config_id as "configId",
|
||||
s.queue_id as "queueId",
|
||||
s.trace_id as "traceId",
|
||||
s.observation_id as "observationId",
|
||||
json_build_object('userId', t.user_id, 'tags', t.tags) as "trace"
|
||||
FROM "scores" AS s
|
||||
LEFT JOIN "traces" AS t ON t.id = s.trace_id AND t.project_id = ${auth.scope.projectId}
|
||||
WHERE s.project_id = ${auth.scope.projectId}
|
||||
${configCondition}
|
||||
${queueCondition}
|
||||
${traceTagsCondition}
|
||||
${dataTypeCondition}
|
||||
${userCondition}
|
||||
${nameCondition}
|
||||
${sourceCondition}
|
||||
${fromTimestampCondition}
|
||||
${toTimestampCondition}
|
||||
${valueCondition}
|
||||
${scoreIdCondition}
|
||||
ORDER BY s."timestamp" DESC
|
||||
LIMIT ${limit} OFFSET ${skipValue}
|
||||
`);
|
||||
|
||||
const totalItemsRes = await prisma.$queryRaw<{ count: bigint }[]>(
|
||||
Prisma.sql`
|
||||
SELECT COUNT(*) as count
|
||||
FROM "scores" AS s
|
||||
LEFT JOIN "traces" AS t ON t.id = s.trace_id AND t.project_id = ${auth.scope.projectId}
|
||||
WHERE s.project_id = ${auth.scope.projectId}
|
||||
${configCondition}
|
||||
${queueCondition}
|
||||
${traceTagsCondition}
|
||||
${dataTypeCondition}
|
||||
${userCondition}
|
||||
${nameCondition}
|
||||
${sourceCondition}
|
||||
${fromTimestampCondition}
|
||||
${toTimestampCondition}
|
||||
${valueCondition}
|
||||
${scoreIdCondition}
|
||||
`,
|
||||
);
|
||||
|
||||
const validatedScores = legacyFilterAndValidateV1GetScoreList(scores);
|
||||
|
||||
const totalItems =
|
||||
totalItemsRes[0] !== undefined ? Number(totalItemsRes[0].count) : 0;
|
||||
|
||||
return {
|
||||
data: validatedScores,
|
||||
meta: {
|
||||
page: page,
|
||||
limit: limit,
|
||||
totalItems,
|
||||
totalPages: Math.ceil(totalItems / limit),
|
||||
},
|
||||
};
|
||||
return {
|
||||
data: legacyFilterAndValidateV1GetScoreList(items),
|
||||
meta: {
|
||||
page: query.page,
|
||||
limit: query.limit,
|
||||
totalItems: finalCount,
|
||||
totalPages: Math.ceil(finalCount / query.limit),
|
||||
},
|
||||
clickhouseExecution: async () => {
|
||||
const [items, count] = await Promise.all([
|
||||
generateScoresForPublicApi({
|
||||
projectId: auth.scope.projectId,
|
||||
page: query.page ?? undefined,
|
||||
limit: query.limit ?? undefined,
|
||||
userId: query.userId ?? undefined,
|
||||
name: query.name ?? undefined,
|
||||
configId: query.configId ?? undefined,
|
||||
queueId: query.queueId ?? undefined,
|
||||
traceTags: query.traceTags ?? undefined,
|
||||
dataType: query.dataType ?? undefined,
|
||||
fromTimestamp: query.fromTimestamp ?? undefined,
|
||||
toTimestamp: query.toTimestamp ?? undefined,
|
||||
source: query.source ?? undefined,
|
||||
value: query.value ?? undefined,
|
||||
operator: query.operator ?? undefined,
|
||||
scoreIds: query.scoreIds ?? undefined,
|
||||
}),
|
||||
getScoresCountForPublicApi({
|
||||
projectId: auth.scope.projectId,
|
||||
page: query.page ?? undefined,
|
||||
limit: query.limit ?? undefined,
|
||||
userId: query.userId ?? undefined,
|
||||
name: query.name ?? undefined,
|
||||
configId: query.configId ?? undefined,
|
||||
queueId: query.queueId ?? undefined,
|
||||
traceTags: query.traceTags ?? undefined,
|
||||
dataType: query.dataType ?? undefined,
|
||||
fromTimestamp: query.fromTimestamp ?? undefined,
|
||||
toTimestamp: query.toTimestamp ?? undefined,
|
||||
source: query.source ?? undefined,
|
||||
value: query.value ?? undefined,
|
||||
operator: query.operator ?? undefined,
|
||||
scoreIds: query.scoreIds ?? undefined,
|
||||
}),
|
||||
]);
|
||||
|
||||
const finalCount = count ? count : 0;
|
||||
|
||||
return {
|
||||
data: legacyFilterAndValidateV1GetScoreList(items),
|
||||
meta: {
|
||||
page: query.page,
|
||||
limit: query.limit,
|
||||
totalItems: finalCount,
|
||||
totalPages: Math.ceil(finalCount / query.limit),
|
||||
},
|
||||
};
|
||||
},
|
||||
});
|
||||
};
|
||||
},
|
||||
}),
|
||||
});
|
||||
|
||||
@@ -6,7 +6,6 @@ import {
|
||||
import { withMiddlewares } from "@/src/features/public-api/server/withMiddlewares";
|
||||
import { createAuthedAPIRoute } from "@/src/features/public-api/server/createAuthedAPIRoute";
|
||||
import { LangfuseNotFoundError } from "@langfuse/shared";
|
||||
import { measureAndReturnApi } from "@/src/server/utils/checkClickhouseAccess";
|
||||
import { getTracesBySessionId } from "@langfuse/shared/src/server";
|
||||
|
||||
export default withMiddlewares({
|
||||
@@ -16,47 +15,34 @@ export default withMiddlewares({
|
||||
responseSchema: GetSessionV1Response,
|
||||
fn: async ({ query, auth }) => {
|
||||
const { sessionId } = query;
|
||||
|
||||
return await measureAndReturnApi({
|
||||
input: { projectId: auth.scope.projectId, queryClickhouse: false },
|
||||
operation: "scores.countAll",
|
||||
user: null,
|
||||
pgExecution: async () => {
|
||||
throw new LangfuseNotFoundError(
|
||||
"Session not found within authorized project",
|
||||
);
|
||||
const session = await prisma.traceSession.findUnique({
|
||||
where: {
|
||||
id_projectId: {
|
||||
id: sessionId,
|
||||
projectId: auth.scope.projectId,
|
||||
},
|
||||
},
|
||||
clickhouseExecution: async () => {
|
||||
const session = await prisma.traceSession.findUnique({
|
||||
where: {
|
||||
id_projectId: {
|
||||
id: sessionId,
|
||||
projectId: auth.scope.projectId,
|
||||
},
|
||||
},
|
||||
select: {
|
||||
id: true,
|
||||
createdAt: true,
|
||||
projectId: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (!session) {
|
||||
throw new LangfuseNotFoundError(
|
||||
"Session not found within authorized project",
|
||||
);
|
||||
}
|
||||
|
||||
const traces = await getTracesBySessionId(auth.scope.projectId, [
|
||||
sessionId,
|
||||
]);
|
||||
|
||||
return {
|
||||
...session,
|
||||
traces,
|
||||
};
|
||||
select: {
|
||||
id: true,
|
||||
createdAt: true,
|
||||
projectId: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (!session) {
|
||||
throw new LangfuseNotFoundError(
|
||||
"Session not found within authorized project",
|
||||
);
|
||||
}
|
||||
|
||||
const traces = await getTracesBySessionId(auth.scope.projectId, [
|
||||
sessionId,
|
||||
]);
|
||||
|
||||
return {
|
||||
...session,
|
||||
traces,
|
||||
};
|
||||
},
|
||||
}),
|
||||
});
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user