Compare commits
78
Commits
v2.93.1
...
v3.0.0-rc.3
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ca3b95312b | ||
|
|
b9aaf05acb | ||
|
|
004aee9f35 | ||
|
|
0c9070f78f | ||
|
|
a91357a958 | ||
|
|
80050cee00 | ||
|
|
b533b1dccd | ||
|
|
25657d11e4 | ||
|
|
3dd616a3dc | ||
|
|
995ec498f4 | ||
|
|
12d7b7f753 | ||
|
|
2630327a86 | ||
|
|
4a1fa69af7 | ||
|
|
e53d101b49 | ||
|
|
39539300c1 | ||
|
|
59982db267 | ||
|
|
fe4299d30d | ||
|
|
304b290ab1 | ||
|
|
97423aa95d | ||
|
|
7aff8e0c26 | ||
|
|
22761d55c3 | ||
|
|
965cf1445d | ||
|
|
922bbb0ce1 | ||
|
|
d821423c6a | ||
|
|
84b0b7f680 | ||
|
|
fba7f4f0c3 | ||
|
|
a9405a2e8b | ||
|
|
2cb3529707 | ||
|
|
2f13204836 | ||
|
|
ae2a42bf80 | ||
|
|
039dee045b | ||
|
|
9831bb8e44 | ||
|
|
c6e3a12b08 | ||
|
|
9b01b562a8 | ||
|
|
e5a0cec7d6 | ||
|
|
9375250c5a | ||
|
|
82e5f1a932 | ||
|
|
64a62c9f6d | ||
|
|
44d856747a | ||
|
|
e17bd364c9 | ||
|
|
f38c756244 | ||
|
|
6c62f4b31a | ||
|
|
fb686d45ee | ||
|
|
1901c8cb39 | ||
|
|
66d925ee1f | ||
|
|
1b06385111 | ||
|
|
6e290b9ad4 | ||
|
|
8b8afff705 | ||
|
|
89fcf27f84 | ||
|
|
6717b16679 | ||
|
|
98e29cb512 | ||
|
|
c7b7a784d7 | ||
|
|
e778d04523 | ||
|
|
2a2f50e2d9 | ||
|
|
9d9820dacc | ||
|
|
6e4065eff3 | ||
|
|
993e2a8323 | ||
|
|
f0c117b35b | ||
|
|
b7d353a821 | ||
|
|
1bdc6a4068 | ||
|
|
851de49789 | ||
|
|
202a9b715d | ||
|
|
5fae362b29 | ||
|
|
38e5c3fac7 | ||
|
|
3d6d94fb16 | ||
|
|
cb7230ac41 | ||
|
|
44ba6eaddf | ||
|
|
e566dfa5a9 | ||
|
|
f9c3f60d49 | ||
|
|
aeb56ecde7 | ||
|
|
9ac84fbd61 | ||
|
|
fe9af426dc | ||
|
|
583e90321e | ||
|
|
e52530351a | ||
|
|
8a52c0b888 | ||
|
|
d02c9b4902 | ||
|
|
014c3d9dad | ||
|
|
1d3c74435b |
+10
-17
@@ -11,7 +11,7 @@ CLICKHOUSE_MIGRATION_URL="clickhouse://localhost:9000"
|
||||
CLICKHOUSE_URL="http://localhost:8123"
|
||||
CLICKHOUSE_USER="clickhouse"
|
||||
CLICKHOUSE_PASSWORD="clickhouse"
|
||||
CLICKHOUSE_MIGRATION_CLUSTER_DISABLED="true"
|
||||
CLICKHOUSE_CLUSTER_ENABLED="false"
|
||||
|
||||
# Next Auth
|
||||
# You can generate a new secret on the command line with:
|
||||
@@ -35,14 +35,16 @@ EMAIL_FROM_ADDRESS="" # Defines the email address to use as the from address.
|
||||
SMTP_CONNECTION_URL="" # Defines the connection url for smtp server.
|
||||
|
||||
# DON'T PANIC: The Azurite Secrets are well-known and meant to be hard-coded
|
||||
# S3 storage
|
||||
S3_ENDPOINT=http://localhost:10000/devstoreaccount1
|
||||
S3_ACCESS_KEY_ID=devstoreaccount1
|
||||
S3_SECRET_ACCESS_KEY=Eby8vdM02xNOcqFlqUwJPLlmEtlCDXJ1OUzFT50uSRZ6IFsuFq2UVErCz4I6tq/K1SZFPTOtr/KBHBeksoGMGw==
|
||||
S3_BUCKET_NAME=langfuse
|
||||
S3_REGION=auto
|
||||
# S3 Batch Exports
|
||||
LANGFUSE_S3_BATCH_EXPORT_ENABLED=true
|
||||
LANGFUSE_S3_BATCH_EXPORT_BUCKET=langfuse
|
||||
LANGFUSE_S3_BATCH_EXPORT_ACCESS_KEY_ID=devstoreaccount1
|
||||
LANGFUSE_S3_BATCH_EXPORT_SECRET_ACCESS_KEY=Eby8vdM02xNOcqFlqUwJPLlmEtlCDXJ1OUzFT50uSRZ6IFsuFq2UVErCz4I6tq/K1SZFPTOtr/KBHBeksoGMGw==
|
||||
LANGFUSE_S3_BATCH_EXPORT_REGION=auto
|
||||
LANGFUSE_S3_BATCH_EXPORT_ENDPOINT=http://localhost:10000/devstoreaccount1
|
||||
## Necessary for minio compatibility
|
||||
S3_FORCE_PATH_STYLE=true
|
||||
LANGFUSE_S3_BATCH_EXPORT_FORCE_PATH_STYLE=true
|
||||
LANGFUSE_S3_BATCH_EXPORT_PREFIX=exports/
|
||||
|
||||
# S3 Media Upload LOCAL
|
||||
LANGFUSE_S3_MEDIA_UPLOAD_ENABLED=true
|
||||
@@ -57,7 +59,6 @@ LANGFUSE_S3_MEDIA_UPLOAD_PREFIX=media/
|
||||
|
||||
# S3 Event Bucket Upload
|
||||
## Set to true to test uploading all events to S3
|
||||
LANGFUSE_S3_EVENT_UPLOAD_ENABLED=true
|
||||
LANGFUSE_S3_EVENT_UPLOAD_BUCKET=langfuse
|
||||
LANGFUSE_S3_EVENT_UPLOAD_ACCESS_KEY_ID=devstoreaccount1
|
||||
LANGFUSE_S3_EVENT_UPLOAD_SECRET_ACCESS_KEY=Eby8vdM02xNOcqFlqUwJPLlmEtlCDXJ1OUzFT50uSRZ6IFsuFq2UVErCz4I6tq/K1SZFPTOtr/KBHBeksoGMGw==
|
||||
@@ -82,11 +83,3 @@ 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=false
|
||||
LANGFUSE_RETURN_FROM_CLICKHOUSE=true
|
||||
LANGFUSE_READ_DASHBOARDS_FROM_CLICKHOUSE=true
|
||||
LANGFUSE_READ_FROM_CLICKHOUSE_ONLY=true
|
||||
|
||||
LANGFUSE_ASYNC_INGESTION_PROCESSING="true"
|
||||
LANGFUSE_ASYNC_CLICKHOUSE_INGESTION_PROCESSING="true"
|
||||
|
||||
+10
-17
@@ -11,7 +11,7 @@ CLICKHOUSE_MIGRATION_URL="clickhouse://localhost:9000"
|
||||
CLICKHOUSE_URL="http://localhost:8123"
|
||||
CLICKHOUSE_USER="clickhouse"
|
||||
CLICKHOUSE_PASSWORD="clickhouse"
|
||||
CLICKHOUSE_CLUSTER_DISABLED="true"
|
||||
CLICKHOUSE_CLUSTER_ENABLED="false"
|
||||
|
||||
# Next Auth
|
||||
# You can generate a new secret on the command line with:
|
||||
@@ -34,14 +34,16 @@ SALT="salt"
|
||||
EMAIL_FROM_ADDRESS="" # Defines the email address to use as the from address.
|
||||
SMTP_CONNECTION_URL="" # Defines the connection url for smtp server.
|
||||
|
||||
# S3 storage
|
||||
S3_ENDPOINT=http://localhost:9090
|
||||
S3_ACCESS_KEY_ID=minio
|
||||
S3_SECRET_ACCESS_KEY=miniosecret
|
||||
S3_BUCKET_NAME=langfuse
|
||||
S3_REGION=us-east-1
|
||||
# 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
|
||||
S3_FORCE_PATH_STYLE=true
|
||||
LANGFUSE_S3_BATCH_EXPORT_FORCE_PATH_STYLE=true
|
||||
LANGFUSE_S3_BATCH_EXPORT_PREFIX=exports/
|
||||
|
||||
# S3 Media Upload LOCAL
|
||||
LANGFUSE_S3_MEDIA_UPLOAD_ENABLED=true
|
||||
@@ -56,7 +58,6 @@ LANGFUSE_S3_MEDIA_UPLOAD_PREFIX=media/
|
||||
|
||||
# S3 Event Bucket Upload
|
||||
## Set to true to test uploading all events to S3
|
||||
LANGFUSE_S3_EVENT_UPLOAD_ENABLED=true
|
||||
LANGFUSE_S3_EVENT_UPLOAD_BUCKET=langfuse
|
||||
LANGFUSE_S3_EVENT_UPLOAD_ACCESS_KEY_ID=minio
|
||||
LANGFUSE_S3_EVENT_UPLOAD_SECRET_ACCESS_KEY=miniosecret
|
||||
@@ -79,11 +80,3 @@ 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=false
|
||||
LANGFUSE_RETURN_FROM_CLICKHOUSE=true
|
||||
LANGFUSE_READ_DASHBOARDS_FROM_CLICKHOUSE=true
|
||||
LANGFUSE_READ_FROM_CLICKHOUSE_ONLY=true
|
||||
|
||||
LANGFUSE_ASYNC_INGESTION_PROCESSING="true"
|
||||
LANGFUSE_ASYNC_CLICKHOUSE_INGESTION_PROCESSING="true"
|
||||
|
||||
+15
-9
@@ -34,14 +34,16 @@ SALT="salt"
|
||||
EMAIL_FROM_ADDRESS="" # Defines the email address to use as the from address.
|
||||
SMTP_CONNECTION_URL="" # Defines the connection url for smtp server.
|
||||
|
||||
# S3 storage
|
||||
S3_ENDPOINT=http://localhost:9090
|
||||
S3_ACCESS_KEY_ID=minio
|
||||
S3_SECRET_ACCESS_KEY=miniosecret
|
||||
S3_BUCKET_NAME=langfuse
|
||||
S3_REGION=us-east-1
|
||||
# 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
|
||||
S3_FORCE_PATH_STYLE=true
|
||||
LANGFUSE_S3_BATCH_EXPORT_FORCE_PATH_STYLE=true
|
||||
LANGFUSE_S3_BATCH_EXPORT_PREFIX=exports/
|
||||
|
||||
# # S3 Media Upload LOCAL
|
||||
LANGFUSE_S3_MEDIA_UPLOAD_ENABLED=true
|
||||
@@ -56,7 +58,6 @@ LANGFUSE_S3_MEDIA_UPLOAD_PREFIX=media/
|
||||
|
||||
# S3 Event Bucket Upload
|
||||
## Set to true to test uploading all events to S3
|
||||
LANGFUSE_S3_EVENT_UPLOAD_ENABLED=true
|
||||
LANGFUSE_S3_EVENT_UPLOAD_BUCKET=langfuse
|
||||
LANGFUSE_S3_EVENT_UPLOAD_ACCESS_KEY_ID=minio
|
||||
LANGFUSE_S3_EVENT_UPLOAD_SECRET_ACCESS_KEY=miniosecret
|
||||
@@ -78,4 +79,9 @@ REDIS_AUTH="myredissecret"
|
||||
ENCRYPTION_KEY=0000000000000000000000000000000000000000000000000000000000000000
|
||||
|
||||
# speeds up local development by not executing init scripts on server startup
|
||||
NEXT_PUBLIC_LANGFUSE_RUN_NEXT_INIT="false"
|
||||
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
|
||||
|
||||
@@ -1,22 +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@db:5432/postgres"
|
||||
DATABASE_URL="postgresql://postgres:postgres@db:5432/postgres"
|
||||
# Next Auth
|
||||
NEXTAUTH_SECRET="secret"
|
||||
NEXTAUTH_URL="http://localhost:3000"
|
||||
|
||||
# feature flag to enable experimental features locally
|
||||
LANGFUSE_ENABLE_EXPERIMENTAL_FEATURES="false"
|
||||
SALT="salt"
|
||||
|
||||
# Redis
|
||||
REDIS_HOST="127.0.0.1"
|
||||
REDIS_PORT=6379
|
||||
REDIS_AUTH="myredissecret"
|
||||
|
||||
# openssl rand -hex 32 used only here
|
||||
ENCRYPTION_KEY=0000000000000000000000000000000000000000000000000000000000000000
|
||||
+16
-9
@@ -67,6 +67,10 @@ OTEL_SERVICE_NAME="langfuse"
|
||||
# AUTH_GITHUB_CLIENT_ID=
|
||||
# AUTH_GITHUB_CLIENT_SECRET=
|
||||
# AUTH_GITHUB_ALLOW_ACCOUNT_LINKING=false
|
||||
# AUTH_GITHUB_ENTERPRISE_CLIENT_ID=
|
||||
# AUTH_GITHUB_ENTERPRISE_CLIENT_SECRET=
|
||||
# AUTH_GITHUB_ENTERPRISE_BASE_URL=
|
||||
# AUTH_GITHUB_ENTERPRISE_ALLOW_ACCOUNT_LINKING=false
|
||||
# AUTH_GITLAB_CLIENT_ID=
|
||||
# AUTH_GITLAB_CLIENT_SECRET=
|
||||
# AUTH_GITLAB_ALLOW_ACCOUNT_LINKING=false
|
||||
@@ -87,6 +91,10 @@ OTEL_SERVICE_NAME="langfuse"
|
||||
# AUTH_COGNITO_CLIENT_SECRET=
|
||||
# AUTH_COGNITO_ISSUER=
|
||||
# AUTH_COGNITO_ALLOW_ACCOUNT_LINKING=false
|
||||
# AUTH_KEYCLOAK_CLIENT_ID=
|
||||
# AUTH_KEYCLOAK_CLIENT_SECRET=
|
||||
# AUTH_KEYCLOAK_ISSUER=
|
||||
# AUTH_KEYCLOAK_ALLOW_ACCOUNT_LINKING=false
|
||||
# AUTH_CUSTOM_CLIENT_ID=
|
||||
# AUTH_CUSTOM_CLIENT_SECRET=
|
||||
# AUTH_CUSTOM_ISSUER=
|
||||
@@ -100,16 +108,16 @@ OTEL_SERVICE_NAME="langfuse"
|
||||
# Defines the connection url for smtp server.
|
||||
# SMTP_CONNECTION_URL=
|
||||
|
||||
# S3 storage, optional, used for exports from the UI
|
||||
# S3_ENDPOINT=
|
||||
# S3_ACCESS_KEY_ID=
|
||||
# S3_SECRET_ACCESS_KEY=
|
||||
# S3_BUCKET_NAME=
|
||||
# S3_REGION=
|
||||
# BATCH_EXPORT_DOWNLOAD_LINK_EXPIRATION_HOURS=
|
||||
# S3 Batch Exports
|
||||
# LANGFUSE_S3_BATCH_EXPORT_ENABLED=
|
||||
# LANGFUSE_S3_BATCH_EXPORT_BUCKET=
|
||||
# LANGFUSE_S3_BATCH_EXPORT_ACCESS_KEY_ID=
|
||||
# LANGFUSE_S3_BATCH_EXPORT_SECRET_ACCESS_KEY=
|
||||
# LANGFUSE_S3_BATCH_EXPORT_REGION=
|
||||
# LANGFUSE_S3_BATCH_EXPORT_ENDPOINT=
|
||||
# LANGFUSE_S3_BATCH_EXPORT_PREFIX=
|
||||
|
||||
# S3 storage for events, optional, used to persist all incoming events
|
||||
# LANGFUSE_S3_EVENT_UPLOAD_ENABLED="true"
|
||||
# LANGFUSE_S3_EVENT_UPLOAD_BUCKET=
|
||||
# Optional prefix to be used within the bucket. Must end with `/` if set
|
||||
# LANGFUSE_S3_EVENT_UPLOAD_PREFIX=events/
|
||||
@@ -242,7 +250,6 @@ OTEL_SERVICE_NAME="langfuse"
|
||||
# LANGFUSE_INGESTION_CLICKHOUSE_WRITE_INTERVAL_MS=
|
||||
# LANGFUSE_INGESTION_CLICKHOUSE_MAX_ATTEMPTS=
|
||||
# LANGFUSE_LEGACY_INGESTION_WORKER_CONCURRENCY=
|
||||
# LANGFUSE_ASYNC_INGESTION_PROCESSING="true"
|
||||
# QUEUE_CONSUMER_LEGACY_INGESTION_QUEUE_IS_ENABLED="true"
|
||||
|
||||
## END Langfuse V3 Ingestion
|
||||
@@ -58,7 +58,6 @@ jobs:
|
||||
--build-arg SENTRY_PROJECT=${{ vars.SENTRY_PROJECT }} \
|
||||
.
|
||||
docker push $REGISTRY/$REPOSITORY:$IMAGE_TAG
|
||||
|
||||
- name: Render AWS ECS Task Definition
|
||||
id: render-task-definition
|
||||
uses: aws-actions/amazon-ecs-render-task-definition@v1
|
||||
|
||||
@@ -6,6 +6,7 @@ on:
|
||||
branches: [main]
|
||||
pull_request:
|
||||
branches: [main]
|
||||
merge_group:
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
@@ -10,7 +10,11 @@ on:
|
||||
merge_group:
|
||||
pull_request:
|
||||
branches:
|
||||
- "*"
|
||||
- "**"
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref }}
|
||||
cancel-in-progress: ${{ github.event_name == 'pull_request' }}
|
||||
|
||||
jobs:
|
||||
lint:
|
||||
@@ -41,6 +45,7 @@ jobs:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
- name: Login to Docker Hub
|
||||
if: github.repository == 'langfuse/langfuse' && (github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository) && (github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository)
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME_READ }}
|
||||
@@ -89,6 +94,7 @@ jobs:
|
||||
with:
|
||||
version: 9.5.0
|
||||
- name: Login to Docker Hub
|
||||
if: github.repository == 'langfuse/langfuse' && (github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository)
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME_READ }}
|
||||
@@ -105,7 +111,7 @@ jobs:
|
||||
- name: Load default env
|
||||
run: |
|
||||
cp .env.dev.example .env
|
||||
grep -v -e '^S3_BUCKET_NAME=' -e '^REDIS_HOST=' -e '^NEXT_PUBLIC_LANGFUSE_RUN_NEXT_INIT=' .env.dev.legacy.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
|
||||
- name: Run + migrate
|
||||
run: |
|
||||
docker compose -f docker-compose.dev.yml up -d
|
||||
@@ -158,6 +164,7 @@ jobs:
|
||||
with:
|
||||
version: 9.5.0
|
||||
- name: Login to Docker Hub
|
||||
if: github.repository == 'langfuse/langfuse' && (github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository)
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME_READ }}
|
||||
@@ -174,7 +181,7 @@ jobs:
|
||||
- name: Load default env
|
||||
run: |
|
||||
cp .env.dev${{ matrix.blob-provider }}.example .env
|
||||
grep -v -e '^S3_BUCKET_NAME=' -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 '^REDIS_HOST=' -e '^NEXT_PUBLIC_LANGFUSE_RUN_NEXT_INIT=' .env.dev${{ matrix.blob-provider }}.example > .env
|
||||
- name: Run + migrate
|
||||
run: |
|
||||
docker compose -f docker-compose.dev${{ matrix.blob-provider }}.yml up -d
|
||||
@@ -222,6 +229,7 @@ jobs:
|
||||
with:
|
||||
version: 9.5.0
|
||||
- name: Login to Docker Hub
|
||||
if: github.repository == 'langfuse/langfuse' && (github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository)
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME_READ }}
|
||||
@@ -281,6 +289,7 @@ jobs:
|
||||
cache: "pnpm"
|
||||
cache-dependency-path: "pnpm-lock.yaml"
|
||||
- name: Login to Docker Hub
|
||||
if: github.repository == 'langfuse/langfuse' && (github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository)
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME_READ }}
|
||||
@@ -313,6 +322,7 @@ jobs:
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Login to Docker Hub
|
||||
if: github.repository == 'langfuse/langfuse' && (github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository)
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME_READ }}
|
||||
@@ -336,8 +346,6 @@ jobs:
|
||||
- name: Load default env
|
||||
run: |
|
||||
cp .env.dev.example .env
|
||||
echo "LANGFUSE_ASYNC_CLICKHOUSE_INGESTION_PROCESSING=true" >> .env
|
||||
echo "LANGFUSE_ASYNC_INGESTION_PROCESSING=true" >> .env
|
||||
echo "LANGFUSE_CACHE_API_KEY_ENABLED=true" >> .env
|
||||
echo "LANGFUSE_CACHE_PROMPT_ENABLED=true" >> .env
|
||||
- name: Run + migrate
|
||||
@@ -450,7 +458,7 @@ jobs:
|
||||
type=semver,pattern={{version}}
|
||||
type=semver,pattern={{major}}.{{minor}}
|
||||
type=semver,pattern={{major}}
|
||||
type=raw,value=latest,enable=${{ startsWith(github.ref, 'refs/tags/v3') }}
|
||||
# type=raw,value=latest,enable=${{ startsWith(github.ref, 'refs/tags/v3') }}
|
||||
- name: Build and push Docker image (web)
|
||||
uses: docker/build-push-action@v4
|
||||
with:
|
||||
@@ -469,6 +477,8 @@ jobs:
|
||||
images: |
|
||||
ghcr.io/langfuse/langfuse-worker # GitHub
|
||||
langfuse/langfuse-worker # Docker Hub
|
||||
flavor: |
|
||||
latest=false
|
||||
tags: |
|
||||
type=ref,event=branch
|
||||
type=ref,event=pr
|
||||
@@ -476,6 +486,7 @@ jobs:
|
||||
type=semver,pattern={{version}}
|
||||
type=semver,pattern={{major}}.{{minor}}
|
||||
type=semver,pattern={{major}}
|
||||
# type=raw,value=latest,enable=${{ startsWith(github.ref, 'refs/tags/v3') }}
|
||||
- name: Build and push Docker image (worker)
|
||||
uses: docker/build-push-action@v4
|
||||
with:
|
||||
|
||||
@@ -6,6 +6,11 @@ on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
merge_group:
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref }}
|
||||
cancel-in-progress: ${{ github.event_name == 'pull_request' }}
|
||||
|
||||
jobs:
|
||||
snyk:
|
||||
@@ -15,13 +20,13 @@ jobs:
|
||||
- name: Build a Docker image
|
||||
run: docker compose -f docker-compose.build.yml up -d
|
||||
|
||||
- name: Run Snyk to check Docker image for vulnerabilities (langfuse-server)
|
||||
- name: Run Snyk to check Docker image for vulnerabilities (langfuse-web)
|
||||
continue-on-error: true
|
||||
uses: snyk/actions/docker@master
|
||||
env:
|
||||
SNYK_TOKEN: ${{ secrets.SNYK_TOKEN }}
|
||||
with:
|
||||
image: langfuse-server
|
||||
image: langfuse-langfuse-web
|
||||
args: --file=web/Dockerfile
|
||||
|
||||
- name: Upload result to GitHub Code Scanning
|
||||
@@ -36,7 +41,7 @@ jobs:
|
||||
env:
|
||||
SNYK_TOKEN: ${{ secrets.SNYK_TOKEN }}
|
||||
with:
|
||||
image: langfuse-worker
|
||||
image: langfuse-langfuse-worker
|
||||
args: --file=worker/Dockerfile
|
||||
|
||||
- name: Upload result to GitHub Code Scanning
|
||||
|
||||
@@ -38,7 +38,6 @@ yarn-error.log*
|
||||
.env*
|
||||
!.env.dev.example
|
||||
!.env.dev-azure.example
|
||||
!.env.local.example
|
||||
!.env.prod.example
|
||||
!.env.dev.legacy.example
|
||||
|
||||
|
||||
@@ -191,3 +191,40 @@ You can opt-out by setting `TELEMETRY_ENABLED=false`.
|
||||
<img alt="Star History Chart" src="https://api.star-history.com/svg?repos=langfuse/langfuse&type=Date" />
|
||||
</picture>
|
||||
</a>
|
||||
|
||||
### Open Source Projects Using Langfuse
|
||||
|
||||
Top open-source Python projects that use Langfuse, ranked by stars ([Source](https://github.com/langfuse/langfuse-docs/blob/main/components-mdx/dependents/python.md)):
|
||||
|
||||
| Repository | Stars |
|
||||
| :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ----: |
|
||||
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/127165244?s=40&v=4" width="20" height="20" alt=""> [langgenius](https://github.com/langgenius) / [dify](https://github.com/langgenius/dify) | 53464 |
|
||||
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/158137808?s=40&v=4" width="20" height="20" alt=""> [open-webui](https://github.com/open-webui) / [open-webui](https://github.com/open-webui/open-webui) | 49387 |
|
||||
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/130722866?s=40&v=4" width="20" height="20" alt=""> [run-llama](https://github.com/run-llama) / [llama_index](https://github.com/run-llama/llama_index) | 37087 |
|
||||
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/85702467?s=40&v=4" width="20" height="20" alt=""> [langflow-ai](https://github.com/langflow-ai) / [langflow](https://github.com/langflow-ai/langflow) | 36690 |
|
||||
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/31035808?s=40&v=4" width="20" height="20" alt=""> [mindsdb](https://github.com/mindsdb) / [mindsdb](https://github.com/mindsdb/mindsdb) | 26871 |
|
||||
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/60330232?s=40&v=4" width="20" height="20" alt=""> [PostHog](https://github.com/PostHog) / [posthog](https://github.com/PostHog/posthog) | 22348 |
|
||||
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/121462774?s=40&v=4" width="20" height="20" alt=""> [BerriAI](https://github.com/BerriAI) / [litellm](https://github.com/BerriAI/litellm) | 14508 |
|
||||
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/78410652?s=40&v=4" width="20" height="20" alt=""> [GreyDGL](https://github.com/GreyDGL) / [PentestGPT](https://github.com/GreyDGL/PentestGPT) | 7319 |
|
||||
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/152537519?s=40&v=4" width="20" height="20" alt=""> [superagent-ai](https://github.com/superagent-ai) / [superagent](https://github.com/superagent-ai/superagent) | 5352 |
|
||||
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/7250217?s=40&v=4" width="20" height="20" alt=""> [Canner](https://github.com/Canner) / [WrenAI](https://github.com/Canner/WrenAI) | 2084 |
|
||||
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/11855343?s=40&v=4" width="20" height="20" alt=""> [pingcap](https://github.com/pingcap) / [autoflow](https://github.com/pingcap/autoflow) | 1936 |
|
||||
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/85268109?s=40&v=4" width="20" height="20" alt=""> [MLSysOps](https://github.com/MLSysOps) / [MLE-agent](https://github.com/MLSysOps/MLE-agent) | 1140 |
|
||||
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/158137808?s=40&v=4" width="20" height="20" alt=""> [open-webui](https://github.com/open-webui) / [pipelines](https://github.com/open-webui/pipelines) | 1044 |
|
||||
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/125468716?s=40&v=4" width="20" height="20" alt=""> [topoteretes](https://github.com/topoteretes) / [cognee](https://github.com/topoteretes/cognee) | 945 |
|
||||
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/188657705?s=40&v=4" width="20" height="20" alt=""> [bRAGAI](https://github.com/bRAGAI) / [bRAG-langchain](https://github.com/bRAGAI/bRAG-langchain) | 770 |
|
||||
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/169500408?s=40&v=4" width="20" height="20" alt=""> [opslane](https://github.com/opslane) / [opslane](https://github.com/opslane/opslane) | 675 |
|
||||
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/151867818?s=40&v=4" width="20" height="20" alt=""> [dynamiq-ai](https://github.com/dynamiq-ai) / [dynamiq](https://github.com/dynamiq-ai/dynamiq) | 583 |
|
||||
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/48585267?s=40&v=4" width="20" height="20" alt=""> [theopenconversationkit](https://github.com/theopenconversationkit) / [tock](https://github.com/theopenconversationkit/tock) | 505 |
|
||||
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/20493493?s=40&v=4" width="20" height="20" alt=""> [andysingal](https://github.com/andysingal) / [llm-course](https://github.com/andysingal/llm-course) | 383 |
|
||||
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/132396805?s=40&v=4" width="20" height="20" alt=""> [phospho-app](https://github.com/phospho-app) / [phospho](https://github.com/phospho-app/phospho) | 379 |
|
||||
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/178644984?s=40&v=4" width="20" height="20" alt=""> [sentient-engineering](https://github.com/sentient-engineering) / [agent-q](https://github.com/sentient-engineering/agent-q) | 357 |
|
||||
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/168552753?s=40&v=4" width="20" height="20" alt=""> [sql-agi](https://github.com/sql-agi) / [DB-GPT](https://github.com/sql-agi/DB-GPT) | 324 |
|
||||
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/60330232?s=40&v=4" width="20" height="20" alt=""> [PostHog](https://github.com/PostHog) / [posthog-foss](https://github.com/PostHog/posthog-foss) | 300 |
|
||||
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/609489?s=40&v=4" width="20" height="20" alt=""> [aorwall](https://github.com/aorwall) / [moatless-tools](https://github.com/aorwall/moatless-tools) | 278 |
|
||||
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/185116535?s=40&v=4" width="20" height="20" alt=""> [block](https://github.com/block) / [goose](https://github.com/block/goose) | 237 |
|
||||
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/66303003?s=40&v=4" width="20" height="20" alt=""> [RobotecAI](https://github.com/RobotecAI) / [rai](https://github.com/RobotecAI/rai) | 164 |
|
||||
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/171735272?s=40&v=4" width="20" height="20" alt=""> [8090-inc](https://github.com/8090-inc) / [xrx-sample-apps](https://github.com/8090-inc/xrx-sample-apps) | 133 |
|
||||
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/51827949?s=40&v=4" width="20" height="20" alt=""> [deepset-ai](https://github.com/deepset-ai) / [haystack-core-integrations](https://github.com/deepset-ai/haystack-core-integrations) | 122 |
|
||||
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/134601687?s=40&v=4" width="20" height="20" alt=""> [langfuse](https://github.com/langfuse) / [langfuse-python](https://github.com/langfuse/langfuse-python) | 108 |
|
||||
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/42655961?s=40&v=4" width="20" height="20" alt=""> [zozoheir](https://github.com/zozoheir) / [tinyllm](https://github.com/zozoheir/tinyllm) | 95 |
|
||||
|
||||
+114
-44
@@ -1,34 +1,53 @@
|
||||
version: "3.5"
|
||||
|
||||
services:
|
||||
server:
|
||||
langfuse-web:
|
||||
build:
|
||||
dockerfile: ./web/Dockerfile
|
||||
context: .
|
||||
args:
|
||||
- NEXT_PUBLIC_LANGFUSE_CLOUD_REGION=${NEXT_PUBLIC_LANGFUSE_CLOUD_REGION}
|
||||
depends_on:
|
||||
- db
|
||||
- redis
|
||||
depends_on: &langfuse-depends-on
|
||||
postgres:
|
||||
condition: service_healthy
|
||||
minio:
|
||||
condition: service_healthy
|
||||
redis:
|
||||
condition: service_healthy
|
||||
clickhouse:
|
||||
condition: service_healthy
|
||||
ports:
|
||||
- "3000:3000"
|
||||
environment:
|
||||
- DATABASE_URL=postgresql://postgres:postgres@db:5432/postgres
|
||||
- NEXTAUTH_SECRET=mysecret
|
||||
- SALT=mysalt
|
||||
- ENCRYPTION_KEY=0000000000000000000000000000000000000000000000000000000000000000 # generate via `openssl rand -hex 32`
|
||||
- NEXTAUTH_URL=http://localhost:3000
|
||||
- TELEMETRY_ENABLED=${TELEMETRY_ENABLED:-true}
|
||||
- LANGFUSE_ENABLE_EXPERIMENTAL_FEATURES=${LANGFUSE_ENABLE_EXPERIMENTAL_FEATURES:-false}
|
||||
- LANGFUSE_INIT_ORG_ID=${LANGFUSE_INIT_ORG_ID:-}
|
||||
- LANGFUSE_INIT_ORG_NAME=${LANGFUSE_INIT_ORG_NAME:-}
|
||||
- LANGFUSE_INIT_PROJECT_ID=${LANGFUSE_INIT_PROJECT_ID:-}
|
||||
- LANGFUSE_INIT_PROJECT_NAME=${LANGFUSE_INIT_PROJECT_NAME:-}
|
||||
- LANGFUSE_INIT_PROJECT_PUBLIC_KEY=${LANGFUSE_INIT_PROJECT_PUBLIC_KEY:-}
|
||||
- LANGFUSE_INIT_PROJECT_SECRET_KEY=${LANGFUSE_INIT_PROJECT_SECRET_KEY:-}
|
||||
- LANGFUSE_INIT_USER_EMAIL=${LANGFUSE_INIT_USER_EMAIL:-}
|
||||
- LANGFUSE_INIT_USER_NAME=${LANGFUSE_INIT_USER_NAME:-}
|
||||
- LANGFUSE_INIT_USER_PASSWORD=${LANGFUSE_INIT_USER_PASSWORD:-}
|
||||
environment: &langfuse-web-env
|
||||
DATABASE_URL: postgresql://postgres:postgres@postgres:5432/postgres
|
||||
NEXTAUTH_SECRET: mysecret
|
||||
SALT: mysalt
|
||||
ENCRYPTION_KEY: "0000000000000000000000000000000000000000000000000000000000000000" # generate via `openssl rand -hex 32`
|
||||
NEXTAUTH_URL: http://localhost:3000
|
||||
TELEMETRY_ENABLED: ${TELEMETRY_ENABLED:-true}
|
||||
LANGFUSE_ENABLE_EXPERIMENTAL_FEATURES: ${LANGFUSE_ENABLE_EXPERIMENTAL_FEATURES:-false}
|
||||
LANGFUSE_INIT_ORG_ID: ${LANGFUSE_INIT_ORG_ID:-}
|
||||
LANGFUSE_INIT_ORG_NAME: ${LANGFUSE_INIT_ORG_NAME:-}
|
||||
LANGFUSE_INIT_PROJECT_ID: ${LANGFUSE_INIT_PROJECT_ID:-}
|
||||
LANGFUSE_INIT_PROJECT_NAME: ${LANGFUSE_INIT_PROJECT_NAME:-}
|
||||
LANGFUSE_INIT_PROJECT_PUBLIC_KEY: ${LANGFUSE_INIT_PROJECT_PUBLIC_KEY:-}
|
||||
LANGFUSE_INIT_PROJECT_SECRET_KEY: ${LANGFUSE_INIT_PROJECT_SECRET_KEY:-}
|
||||
LANGFUSE_INIT_USER_EMAIL: ${LANGFUSE_INIT_USER_EMAIL:-}
|
||||
LANGFUSE_INIT_USER_NAME: ${LANGFUSE_INIT_USER_NAME:-}
|
||||
LANGFUSE_INIT_USER_PASSWORD: ${LANGFUSE_INIT_USER_PASSWORD:-}
|
||||
CLICKHOUSE_MIGRATION_URL: ${CLICKHOUSE_MIGRATION_URL:-clickhouse://clickhouse:9000}
|
||||
CLICKHOUSE_URL: ${CLICKHOUSE_URL:-http://clickhouse:8123}
|
||||
CLICKHOUSE_USER: ${CLICKHOUSE_USER:-clickhouse}
|
||||
CLICKHOUSE_PASSWORD: ${CLICKHOUSE_PASSWORD:-clickhouse}
|
||||
CLICKHOUSE_CLUSTER_ENABLED: ${CLICKHOUSE_CLUSTER_ENABLED:-false}
|
||||
LANGFUSE_S3_EVENT_UPLOAD_BUCKET: ${LANGFUSE_S3_EVENT_UPLOAD_BUCKET:-langfuse}
|
||||
LANGFUSE_S3_EVENT_UPLOAD_REGION: ${LANGFUSE_S3_EVENT_UPLOAD_REGION:-us-east-1}
|
||||
LANGFUSE_S3_EVENT_UPLOAD_ACCESS_KEY_ID: ${LANGFUSE_S3_EVENT_UPLOAD_ACCESS_KEY_ID:-minio}
|
||||
LANGFUSE_S3_EVENT_UPLOAD_SECRET_ACCESS_KEY: ${LANGFUSE_S3_EVENT_UPLOAD_SECRET_ACCESS_KEY:-miniosecret}
|
||||
LANGFUSE_S3_EVENT_UPLOAD_ENDPOINT: ${LANGFUSE_S3_EVENT_UPLOAD_ENDPOINT:-http://minio:9000}
|
||||
LANGFUSE_S3_EVENT_UPLOAD_FORCE_PATH_STYLE: ${LANGFUSE_S3_EVENT_UPLOAD_FORCE_PATH_STYLE:-true}
|
||||
LANGFUSE_S3_EVENT_UPLOAD_PREFIX: ${LANGFUSE_S3_EVENT_UPLOAD_PREFIX:-events/}
|
||||
REDIS_HOST: ${REDIS_HOST:-redis}
|
||||
REDIS_PORT: ${REDIS_PORT:-6379}
|
||||
REDIS_AUTH: ${REDIS_AUTH:-myredissecret}
|
||||
restart: always
|
||||
healthcheck:
|
||||
test: ["CMD", "curl", "-f", "http://localhost:3000/api/public/health"]
|
||||
@@ -36,26 +55,17 @@ services:
|
||||
timeout: 10s
|
||||
retries: 3
|
||||
|
||||
worker:
|
||||
langfuse-worker:
|
||||
build:
|
||||
dockerfile: ./worker/Dockerfile
|
||||
context: .
|
||||
args:
|
||||
- NEXT_PUBLIC_LANGFUSE_CLOUD_REGION=${NEXT_PUBLIC_LANGFUSE_CLOUD_REGION}
|
||||
depends_on:
|
||||
- db
|
||||
- redis
|
||||
depends_on: *langfuse-depends-on
|
||||
ports:
|
||||
- "3030:3030"
|
||||
environment:
|
||||
- DATABASE_URL=postgresql://postgres:postgres@db:5432/postgres
|
||||
- NEXTAUTH_SECRET=mysecret
|
||||
- TELEMETRY_ENABLED=${TELEMETRY_ENABLED:-true}
|
||||
- LANGFUSE_ENABLE_EXPERIMENTAL_FEATURES=${LANGFUSE_ENABLE_EXPERIMENTAL_FEATURES:-false}
|
||||
- PORT=${PORT:-3030}
|
||||
- REDIS_HOST=${REDIS_HOST:-redis}
|
||||
- REDIS_PORT=${REDIS_PORT:-6379}
|
||||
- REDIS_AUTH=${REDIS_AUTH:-myredissecret}
|
||||
<<: *langfuse-web-env
|
||||
restart: always
|
||||
healthcheck:
|
||||
test: ["CMD", "curl", "-f", "http://localhost:3030/api/health"]
|
||||
@@ -63,26 +73,86 @@ services:
|
||||
timeout: 10s
|
||||
retries: 3
|
||||
|
||||
clickhouse:
|
||||
image: clickhouse/clickhouse-server
|
||||
user: "101:101"
|
||||
container_name: clickhouse
|
||||
hostname: clickhouse
|
||||
environment:
|
||||
CLICKHOUSE_DB: default
|
||||
CLICKHOUSE_USER: clickhouse
|
||||
CLICKHOUSE_PASSWORD: clickhouse
|
||||
volumes:
|
||||
- langfuse_clickhouse_data:/var/lib/clickhouse
|
||||
- langfuse_clickhouse_logs:/var/log/clickhouse-server
|
||||
ports:
|
||||
- "8123:8123"
|
||||
- "9000:9000"
|
||||
healthcheck:
|
||||
test: wget --no-verbose --tries=1 --spider http://localhost:8123/ping || exit 1
|
||||
interval: 5s
|
||||
timeout: 5s
|
||||
retries: 10
|
||||
start_period: 1s
|
||||
|
||||
minio:
|
||||
image: minio/minio
|
||||
container_name: minio
|
||||
entrypoint: sh
|
||||
# create the 'langfuse' bucket before starting the service
|
||||
command: -c 'mkdir -p /data/langfuse && minio server --address ":9000" --console-address ":9001" /data'
|
||||
environment:
|
||||
MINIO_ROOT_USER: minio
|
||||
MINIO_ROOT_PASSWORD: miniosecret
|
||||
ports:
|
||||
- "9090:9000"
|
||||
- "9091:9001"
|
||||
volumes:
|
||||
- langfuse_minio_data:/data
|
||||
healthcheck:
|
||||
test: ["CMD", "mc", "ready", "local"]
|
||||
interval: 1s
|
||||
timeout: 5s
|
||||
retries: 5
|
||||
start_period: 1s
|
||||
|
||||
redis:
|
||||
image: redis:7.2.4
|
||||
image: redis:7
|
||||
restart: always
|
||||
command: >
|
||||
--requirepass ${REDIS_AUTH:-myredissecret}
|
||||
restart: always
|
||||
ports:
|
||||
- 6379:6379
|
||||
healthcheck:
|
||||
test: ["CMD", "redis-cli", "ping"]
|
||||
interval: 3s
|
||||
timeout: 10s
|
||||
retries: 10
|
||||
|
||||
db:
|
||||
image: postgres
|
||||
postgres:
|
||||
image: postgres:${POSTGRES_VERSION:-latest}
|
||||
restart: always
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "pg_isready -U postgres"]
|
||||
interval: 3s
|
||||
timeout: 3s
|
||||
retries: 10
|
||||
environment:
|
||||
- POSTGRES_USER=postgres
|
||||
- POSTGRES_PASSWORD=postgres
|
||||
- POSTGRES_DB=postgres
|
||||
POSTGRES_USER: postgres
|
||||
POSTGRES_PASSWORD: postgres
|
||||
POSTGRES_DB: postgres
|
||||
ports:
|
||||
- 5432:5432
|
||||
volumes:
|
||||
- database_data:/var/lib/postgresql/data
|
||||
- langfuse_postgres_data:/var/lib/postgresql/data
|
||||
|
||||
volumes:
|
||||
database_data:
|
||||
langfuse_postgres_data:
|
||||
driver: local
|
||||
langfuse_clickhouse_data:
|
||||
driver: local
|
||||
langfuse_clickhouse_logs:
|
||||
driver: local
|
||||
langfuse_minio_data:
|
||||
driver: local
|
||||
|
||||
|
||||
@@ -1,145 +0,0 @@
|
||||
services:
|
||||
langfuse-worker:
|
||||
image: langfuse/langfuse-worker:latest
|
||||
depends_on: &langfuse-depends-on
|
||||
postgres:
|
||||
condition: service_healthy
|
||||
minio:
|
||||
condition: service_healthy
|
||||
redis:
|
||||
condition: service_healthy
|
||||
ports:
|
||||
- "3030:3030"
|
||||
environment: &langfuse-worker-env
|
||||
DATABASE_URL: postgresql://postgres:postgres@postgres:5432/postgres
|
||||
SALT: "mysalt"
|
||||
ENCRYPTION_KEY: "0000000000000000000000000000000000000000000000000000000000000000" # generate via `openssl rand -hex 32`
|
||||
TELEMETRY_ENABLED: ${TELEMETRY_ENABLED:-true}
|
||||
LANGFUSE_ENABLE_EXPERIMENTAL_FEATURES: ${LANGFUSE_ENABLE_EXPERIMENTAL_FEATURES:-true}
|
||||
LANGFUSE_ASYNC_INGESTION_PROCESSING: ${LANGFUSE_ASYNC_INGESTION_PROCESSING:-true}
|
||||
LANGFUSE_ASYNC_CLICKHOUSE_INGESTION_PROCESSING: ${LANGFUSE_ASYNC_CLICKHOUSE_INGESTION_PROCESSING:-true}
|
||||
LANGFUSE_READ_DASHBOARDS_FROM_CLICKHOUSE: ${LANGFUSE_READ_DASHBOARDS_FROM_CLICKHOUSE:-true}
|
||||
LANGFUSE_READ_FROM_POSTGRES_ONLY: ${LANGFUSE_READ_FROM_POSTGRES_ONLY:-false}
|
||||
LANGFUSE_RETURN_FROM_CLICKHOUSE: ${LANGFUSE_RETURN_FROM_CLICKHOUSE:-true}
|
||||
CLICKHOUSE_MIGRATION_URL: ${CLICKHOUSE_MIGRATION_URL:-clickhouse://clickhouse:9000}
|
||||
CLICKHOUSE_URL: ${CLICKHOUSE_URL:-http://clickhouse:8123}
|
||||
CLICKHOUSE_USER: ${CLICKHOUSE_USER:-clickhouse}
|
||||
CLICKHOUSE_PASSWORD: ${CLICKHOUSE_PASSWORD:-clickhouse}
|
||||
CLICKHOUSE_CLUSTER_ENABLED: ${CLICKHOUSE_CLUSTER_ENABLED:-false}
|
||||
LANGFUSE_S3_EVENT_UPLOAD_ENABLED: ${LANGFUSE_S3_EVENT_UPLOAD_ENABLED:-true}
|
||||
LANGFUSE_S3_EVENT_UPLOAD_BUCKET: ${LANGFUSE_S3_EVENT_UPLOAD_BUCKET:-langfuse}
|
||||
LANGFUSE_S3_EVENT_UPLOAD_REGION: ${LANGFUSE_S3_EVENT_UPLOAD_REGION:-us-east-1}
|
||||
LANGFUSE_S3_EVENT_UPLOAD_ACCESS_KEY_ID: ${LANGFUSE_S3_EVENT_UPLOAD_ACCESS_KEY_ID:-minio}
|
||||
LANGFUSE_S3_EVENT_UPLOAD_SECRET_ACCESS_KEY: ${LANGFUSE_S3_EVENT_UPLOAD_SECRET_ACCESS_KEY:-miniosecret}
|
||||
LANGFUSE_S3_EVENT_UPLOAD_ENDPOINT: ${LANGFUSE_S3_EVENT_UPLOAD_ENDPOINT:-http://minio:9000}
|
||||
LANGFUSE_S3_EVENT_UPLOAD_FORCE_PATH_STYLE: ${LANGFUSE_S3_EVENT_UPLOAD_FORCE_PATH_STYLE:-true}
|
||||
LANGFUSE_S3_EVENT_UPLOAD_PREFIX: ${LANGFUSE_S3_EVENT_UPLOAD_PREFIX:-events/}
|
||||
LANGFUSE_S3_MEDIA_UPLOAD_ENABLED: ${LANGFUSE_S3_MEDIA_UPLOAD_ENABLED:-true}
|
||||
LANGFUSE_S3_MEDIA_UPLOAD_BUCKET: ${LANGFUSE_S3_MEDIA_UPLOAD_BUCKET:-langfuse}
|
||||
LANGFUSE_S3_MEDIA_UPLOAD_REGION: ${LANGFUSE_S3_MEDIA_UPLOAD_REGION:-us-east-1}
|
||||
LANGFUSE_S3_MEDIA_UPLOAD_ACCESS_KEY_ID: ${LANGFUSE_S3_MEDIA_UPLOAD_ACCESS_KEY_ID:-minio}
|
||||
LANGFUSE_S3_MEDIA_UPLOAD_SECRET_ACCESS_KEY: ${LANGFUSE_S3_MEDIA_UPLOAD_SECRET_ACCESS_KEY:-miniosecret}
|
||||
LANGFUSE_S3_MEDIA_UPLOAD_ENDPOINT: ${LANGFUSE_S3_MEDIA_UPLOAD_ENDPOINT:-http://minio:9000}
|
||||
LANGFUSE_S3_MEDIA_UPLOAD_FORCE_PATH_STYLE: ${LANGFUSE_S3_MEDIA_UPLOAD_FORCE_PATH_STYLE:-true}
|
||||
LANGFUSE_S3_MEDIA_UPLOAD_PREFIX: ${LANGFUSE_S3_MEDIA_UPLOAD_PREFIX:-media/}
|
||||
REDIS_HOST: ${REDIS_HOST:-redis}
|
||||
REDIS_PORT: ${REDIS_PORT:-6379}
|
||||
REDIS_AUTH: ${REDIS_AUTH:-myredissecret}
|
||||
|
||||
langfuse-web:
|
||||
image: langfuse/langfuse:latest
|
||||
depends_on: *langfuse-depends-on
|
||||
ports:
|
||||
- "3000:3000"
|
||||
environment:
|
||||
<<: *langfuse-worker-env
|
||||
NEXTAUTH_URL: http://localhost:3000
|
||||
NEXTAUTH_SECRET: mysecret
|
||||
LANGFUSE_INIT_ORG_ID: ${LANGFUSE_INIT_ORG_ID:-}
|
||||
LANGFUSE_INIT_ORG_NAME: ${LANGFUSE_INIT_ORG_NAME:-}
|
||||
LANGFUSE_INIT_PROJECT_ID: ${LANGFUSE_INIT_PROJECT_ID:-}
|
||||
LANGFUSE_INIT_PROJECT_NAME: ${LANGFUSE_INIT_PROJECT_NAME:-}
|
||||
LANGFUSE_INIT_PROJECT_PUBLIC_KEY: ${LANGFUSE_INIT_PROJECT_PUBLIC_KEY:-}
|
||||
LANGFUSE_INIT_PROJECT_SECRET_KEY: ${LANGFUSE_INIT_PROJECT_SECRET_KEY:-}
|
||||
LANGFUSE_INIT_USER_EMAIL: ${LANGFUSE_INIT_USER_EMAIL:-}
|
||||
LANGFUSE_INIT_USER_NAME: ${LANGFUSE_INIT_USER_NAME:-}
|
||||
LANGFUSE_INIT_USER_PASSWORD: ${LANGFUSE_INIT_USER_PASSWORD:-}
|
||||
|
||||
clickhouse:
|
||||
image: clickhouse/clickhouse-server
|
||||
user: "101:101"
|
||||
container_name: clickhouse
|
||||
hostname: clickhouse
|
||||
environment:
|
||||
CLICKHOUSE_DB: default
|
||||
CLICKHOUSE_USER: clickhouse
|
||||
CLICKHOUSE_PASSWORD: clickhouse
|
||||
volumes:
|
||||
- langfuse_clickhouse_data:/var/lib/clickhouse
|
||||
- langfuse_clickhouse_logs:/var/log/clickhouse-server
|
||||
ports:
|
||||
- "8123:8123"
|
||||
- "9000:9000"
|
||||
depends_on:
|
||||
- postgres
|
||||
|
||||
minio:
|
||||
image: minio/minio
|
||||
container_name: minio
|
||||
entrypoint: sh
|
||||
# create the 'langfuse' bucket before starting the service
|
||||
command: -c 'mkdir -p /data/langfuse && minio server --address ":9000" --console-address ":9001" /data'
|
||||
environment:
|
||||
MINIO_ROOT_USER: minio
|
||||
MINIO_ROOT_PASSWORD: miniosecret
|
||||
ports:
|
||||
- "9090:9000"
|
||||
- "9091:9001"
|
||||
volumes:
|
||||
- langfuse_minio_data:/data
|
||||
healthcheck:
|
||||
test: ["CMD", "mc", "ready", "local"]
|
||||
interval: 1s
|
||||
timeout: 5s
|
||||
retries: 5
|
||||
start_period: 1s
|
||||
|
||||
redis:
|
||||
image: redis:7
|
||||
restart: always
|
||||
command: >
|
||||
--requirepass ${REDIS_AUTH:-myredissecret}
|
||||
ports:
|
||||
- 6379:6379
|
||||
healthcheck:
|
||||
test: ["CMD", "redis-cli", "ping"]
|
||||
interval: 3s
|
||||
timeout: 10s
|
||||
retries: 10
|
||||
|
||||
postgres:
|
||||
image: postgres:${POSTGRES_VERSION:-latest}
|
||||
restart: always
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "pg_isready -U postgres"]
|
||||
interval: 3s
|
||||
timeout: 3s
|
||||
retries: 10
|
||||
environment:
|
||||
POSTGRES_USER: postgres
|
||||
POSTGRES_PASSWORD: postgres
|
||||
POSTGRES_DB: postgres
|
||||
ports:
|
||||
- 5432:5432
|
||||
volumes:
|
||||
- langfuse_postgres_data:/var/lib/postgresql/data
|
||||
|
||||
volumes:
|
||||
langfuse_postgres_data:
|
||||
driver: local
|
||||
langfuse_clickhouse_data:
|
||||
driver: local
|
||||
langfuse_clickhouse_logs:
|
||||
driver: local
|
||||
langfuse_minio_data:
|
||||
driver: local
|
||||
+118
-27
@@ -1,31 +1,116 @@
|
||||
services:
|
||||
langfuse-server:
|
||||
image: langfuse/langfuse:2
|
||||
depends_on:
|
||||
db:
|
||||
langfuse-worker:
|
||||
image: langfuse/langfuse-worker:main # TODO: Switch to :3 after go-live
|
||||
depends_on: &langfuse-depends-on
|
||||
postgres:
|
||||
condition: service_healthy
|
||||
minio:
|
||||
condition: service_healthy
|
||||
redis:
|
||||
condition: service_healthy
|
||||
clickhouse:
|
||||
condition: service_healthy
|
||||
ports:
|
||||
- "3030:3030"
|
||||
environment: &langfuse-worker-env
|
||||
DATABASE_URL: postgresql://postgres:postgres@postgres:5432/postgres
|
||||
SALT: "mysalt"
|
||||
ENCRYPTION_KEY: "0000000000000000000000000000000000000000000000000000000000000000" # generate via `openssl rand -hex 32`
|
||||
TELEMETRY_ENABLED: ${TELEMETRY_ENABLED:-true}
|
||||
LANGFUSE_ENABLE_EXPERIMENTAL_FEATURES: ${LANGFUSE_ENABLE_EXPERIMENTAL_FEATURES:-true}
|
||||
CLICKHOUSE_MIGRATION_URL: ${CLICKHOUSE_MIGRATION_URL:-clickhouse://clickhouse:9000}
|
||||
CLICKHOUSE_URL: ${CLICKHOUSE_URL:-http://clickhouse:8123}
|
||||
CLICKHOUSE_USER: ${CLICKHOUSE_USER:-clickhouse}
|
||||
CLICKHOUSE_PASSWORD: ${CLICKHOUSE_PASSWORD:-clickhouse}
|
||||
CLICKHOUSE_CLUSTER_ENABLED: ${CLICKHOUSE_CLUSTER_ENABLED:-false}
|
||||
LANGFUSE_S3_EVENT_UPLOAD_BUCKET: ${LANGFUSE_S3_EVENT_UPLOAD_BUCKET:-langfuse}
|
||||
LANGFUSE_S3_EVENT_UPLOAD_REGION: ${LANGFUSE_S3_EVENT_UPLOAD_REGION:-us-east-1}
|
||||
LANGFUSE_S3_EVENT_UPLOAD_ACCESS_KEY_ID: ${LANGFUSE_S3_EVENT_UPLOAD_ACCESS_KEY_ID:-minio}
|
||||
LANGFUSE_S3_EVENT_UPLOAD_SECRET_ACCESS_KEY: ${LANGFUSE_S3_EVENT_UPLOAD_SECRET_ACCESS_KEY:-miniosecret}
|
||||
LANGFUSE_S3_EVENT_UPLOAD_ENDPOINT: ${LANGFUSE_S3_EVENT_UPLOAD_ENDPOINT:-http://minio:9000}
|
||||
LANGFUSE_S3_EVENT_UPLOAD_FORCE_PATH_STYLE: ${LANGFUSE_S3_EVENT_UPLOAD_FORCE_PATH_STYLE:-true}
|
||||
LANGFUSE_S3_EVENT_UPLOAD_PREFIX: ${LANGFUSE_S3_EVENT_UPLOAD_PREFIX:-events/}
|
||||
REDIS_HOST: ${REDIS_HOST:-redis}
|
||||
REDIS_PORT: ${REDIS_PORT:-6379}
|
||||
REDIS_AUTH: ${REDIS_AUTH:-myredissecret}
|
||||
|
||||
langfuse-web:
|
||||
image: langfuse/langfuse:main # TODO: Switch to :3 after go-live
|
||||
depends_on: *langfuse-depends-on
|
||||
ports:
|
||||
- "3000:3000"
|
||||
environment:
|
||||
- DATABASE_URL=postgresql://postgres:postgres@db:5432/postgres
|
||||
- NEXTAUTH_SECRET=mysecret
|
||||
- SALT=mysalt
|
||||
- ENCRYPTION_KEY=0000000000000000000000000000000000000000000000000000000000000000 # generate via `openssl rand -hex 32`
|
||||
- NEXTAUTH_URL=http://localhost:3000
|
||||
- TELEMETRY_ENABLED=${TELEMETRY_ENABLED:-true}
|
||||
- LANGFUSE_ENABLE_EXPERIMENTAL_FEATURES=${LANGFUSE_ENABLE_EXPERIMENTAL_FEATURES:-false}
|
||||
- LANGFUSE_INIT_ORG_ID=${LANGFUSE_INIT_ORG_ID:-}
|
||||
- LANGFUSE_INIT_ORG_NAME=${LANGFUSE_INIT_ORG_NAME:-}
|
||||
- LANGFUSE_INIT_PROJECT_ID=${LANGFUSE_INIT_PROJECT_ID:-}
|
||||
- LANGFUSE_INIT_PROJECT_NAME=${LANGFUSE_INIT_PROJECT_NAME:-}
|
||||
- LANGFUSE_INIT_PROJECT_PUBLIC_KEY=${LANGFUSE_INIT_PROJECT_PUBLIC_KEY:-}
|
||||
- LANGFUSE_INIT_PROJECT_SECRET_KEY=${LANGFUSE_INIT_PROJECT_SECRET_KEY:-}
|
||||
- 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-worker-env
|
||||
NEXTAUTH_URL: http://localhost:3000
|
||||
NEXTAUTH_SECRET: mysecret
|
||||
LANGFUSE_INIT_ORG_ID: ${LANGFUSE_INIT_ORG_ID:-}
|
||||
LANGFUSE_INIT_ORG_NAME: ${LANGFUSE_INIT_ORG_NAME:-}
|
||||
LANGFUSE_INIT_PROJECT_ID: ${LANGFUSE_INIT_PROJECT_ID:-}
|
||||
LANGFUSE_INIT_PROJECT_NAME: ${LANGFUSE_INIT_PROJECT_NAME:-}
|
||||
LANGFUSE_INIT_PROJECT_PUBLIC_KEY: ${LANGFUSE_INIT_PROJECT_PUBLIC_KEY:-}
|
||||
LANGFUSE_INIT_PROJECT_SECRET_KEY: ${LANGFUSE_INIT_PROJECT_SECRET_KEY:-}
|
||||
LANGFUSE_INIT_USER_EMAIL: ${LANGFUSE_INIT_USER_EMAIL:-}
|
||||
LANGFUSE_INIT_USER_NAME: ${LANGFUSE_INIT_USER_NAME:-}
|
||||
LANGFUSE_INIT_USER_PASSWORD: ${LANGFUSE_INIT_USER_PASSWORD:-}
|
||||
|
||||
db:
|
||||
image: postgres
|
||||
clickhouse:
|
||||
image: clickhouse/clickhouse-server
|
||||
user: "101:101"
|
||||
container_name: clickhouse
|
||||
hostname: clickhouse
|
||||
environment:
|
||||
CLICKHOUSE_DB: default
|
||||
CLICKHOUSE_USER: clickhouse
|
||||
CLICKHOUSE_PASSWORD: clickhouse
|
||||
volumes:
|
||||
- langfuse_clickhouse_data:/var/lib/clickhouse
|
||||
- langfuse_clickhouse_logs:/var/log/clickhouse-server
|
||||
ports:
|
||||
- "8123:8123"
|
||||
- "9000:9000"
|
||||
healthcheck:
|
||||
test: wget --no-verbose --tries=1 --spider http://localhost:8123/ping || exit 1
|
||||
interval: 5s
|
||||
timeout: 5s
|
||||
retries: 10
|
||||
start_period: 1s
|
||||
|
||||
minio:
|
||||
image: minio/minio
|
||||
container_name: minio
|
||||
entrypoint: sh
|
||||
# create the 'langfuse' bucket before starting the service
|
||||
command: -c 'mkdir -p /data/langfuse && minio server --address ":9000" --console-address ":9001" /data'
|
||||
environment:
|
||||
MINIO_ROOT_USER: minio
|
||||
MINIO_ROOT_PASSWORD: miniosecret
|
||||
ports:
|
||||
- "9090:9000"
|
||||
- "9091:9001"
|
||||
volumes:
|
||||
- langfuse_minio_data:/data
|
||||
healthcheck:
|
||||
test: ["CMD", "mc", "ready", "local"]
|
||||
interval: 1s
|
||||
timeout: 5s
|
||||
retries: 5
|
||||
start_period: 1s
|
||||
|
||||
redis:
|
||||
image: redis:7
|
||||
restart: always
|
||||
command: >
|
||||
--requirepass ${REDIS_AUTH:-myredissecret}
|
||||
ports:
|
||||
- 6379:6379
|
||||
healthcheck:
|
||||
test: ["CMD", "redis-cli", "ping"]
|
||||
interval: 3s
|
||||
timeout: 10s
|
||||
retries: 10
|
||||
|
||||
postgres:
|
||||
image: postgres:${POSTGRES_VERSION:-latest}
|
||||
restart: always
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "pg_isready -U postgres"]
|
||||
@@ -33,14 +118,20 @@ services:
|
||||
timeout: 3s
|
||||
retries: 10
|
||||
environment:
|
||||
- POSTGRES_USER=postgres
|
||||
- POSTGRES_PASSWORD=postgres
|
||||
- POSTGRES_DB=postgres
|
||||
POSTGRES_USER: postgres
|
||||
POSTGRES_PASSWORD: postgres
|
||||
POSTGRES_DB: postgres
|
||||
ports:
|
||||
- 5432:5432
|
||||
volumes:
|
||||
- database_data:/var/lib/postgresql/data
|
||||
- langfuse_postgres_data:/var/lib/postgresql/data
|
||||
|
||||
volumes:
|
||||
database_data:
|
||||
langfuse_postgres_data:
|
||||
driver: local
|
||||
langfuse_clickhouse_data:
|
||||
driver: local
|
||||
langfuse_clickhouse_logs:
|
||||
driver: local
|
||||
langfuse_minio_data:
|
||||
driver: local
|
||||
|
||||
+2
-5
@@ -1,12 +1,9 @@
|
||||
import { z } from "zod";
|
||||
import { env as sharedEnv, removeEmptyEnvVariables } from "@langfuse/shared";
|
||||
import { removeEmptyEnvVariables } from "@langfuse/shared";
|
||||
|
||||
const EnvSchema = z.object({
|
||||
NEXT_PUBLIC_LANGFUSE_CLOUD_REGION: z.string().optional(),
|
||||
LANGFUSE_EE_LICENSE_KEY: z.string().optional(),
|
||||
});
|
||||
|
||||
export const env = {
|
||||
...sharedEnv,
|
||||
...EnvSchema.parse(removeEmptyEnvVariables(process.env)),
|
||||
};
|
||||
export const env = EnvSchema.parse(removeEmptyEnvVariables(process.env));
|
||||
|
||||
@@ -99,5 +99,5 @@ types:
|
||||
docs: The unique langfuse identifier of a media record
|
||||
|
||||
MediaContentType:
|
||||
type: literal<"image/png","image/jpeg","image/jpg","image/webp","audio/mpeg","audio/mp3","audio/wav","text/plain","application/pdf">
|
||||
type: literal<"image/png","image/jpeg","image/jpg","image/webp","image/gif","image/svg+xml","image/tiff","image/bmp","audio/mpeg","audio/mp3","audio/wav","audio/ogg","audio/oga","audio/aac","audio/mp4","audio/flac","video/mp4","video/webm","text/plain","text/html","text/css","text/csv","application/pdf","application/msword","application/vnd.ms-excel","application/zip","application/json","application/xml","application/octet-stream">
|
||||
docs: The MIME type of the media record
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "langfuse",
|
||||
"version": "2.92.0",
|
||||
"version": "3.0.0-rc.3",
|
||||
"author": "engineering@langfuse.com",
|
||||
"license": "MIT",
|
||||
"private": true,
|
||||
|
||||
@@ -19,16 +19,7 @@ then
|
||||
fi
|
||||
|
||||
# Construct the database URL
|
||||
if [ "$CLICKHOUSE_CLUSTER_ENABLED" == "true" ] ; then
|
||||
if [ "$CLICKHOUSE_MIGRATION_SSL" = true ] ; then
|
||||
DATABASE_URL="${CLICKHOUSE_MIGRATION_URL}?username=${CLICKHOUSE_USER}&password=${CLICKHOUSE_PASSWORD}&database=default&x-multi-statement=true&secure=true&skip_verify=true&x-cluster-name=default&x-migrations-table-engine=ReplicatedMergeTree"
|
||||
else
|
||||
DATABASE_URL="${CLICKHOUSE_MIGRATION_URL}?username=${CLICKHOUSE_USER}&password=${CLICKHOUSE_PASSWORD}&database=default&x-multi-statement=true&x-cluster-name=default&x-migrations-table-engine=ReplicatedMergeTree"
|
||||
fi
|
||||
|
||||
# Execute the up command
|
||||
migrate -source file://clickhouse/migrations/clustered -database "$DATABASE_URL" down
|
||||
else
|
||||
if [ "$CLICKHOUSE_CLUSTER_ENABLED" == "false" ] ; then
|
||||
if [ "$CLICKHOUSE_MIGRATION_SSL" = true ] ; then
|
||||
DATABASE_URL="${CLICKHOUSE_MIGRATION_URL}?username=${CLICKHOUSE_USER}&password=${CLICKHOUSE_PASSWORD}&database=default&x-multi-statement=true&secure=true&skip_verify=true&x-migrations-table-engine=MergeTree"
|
||||
else
|
||||
@@ -37,4 +28,13 @@ else
|
||||
|
||||
# Execute the up command
|
||||
migrate -source file://clickhouse/migrations/unclustered -database "$DATABASE_URL" down
|
||||
else
|
||||
if [ "$CLICKHOUSE_MIGRATION_SSL" = true ] ; then
|
||||
DATABASE_URL="${CLICKHOUSE_MIGRATION_URL}?username=${CLICKHOUSE_USER}&password=${CLICKHOUSE_PASSWORD}&database=default&x-multi-statement=true&secure=true&skip_verify=true&x-cluster-name=default&x-migrations-table-engine=ReplicatedMergeTree"
|
||||
else
|
||||
DATABASE_URL="${CLICKHOUSE_MIGRATION_URL}?username=${CLICKHOUSE_USER}&password=${CLICKHOUSE_PASSWORD}&database=default&x-multi-statement=true&x-cluster-name=default&x-migrations-table-engine=ReplicatedMergeTree"
|
||||
fi
|
||||
|
||||
# Execute the up command
|
||||
migrate -source file://clickhouse/migrations/clustered -database "$DATABASE_URL" down
|
||||
fi
|
||||
@@ -2,7 +2,7 @@ import {
|
||||
clickhouseClient,
|
||||
ObservationRecordReadType,
|
||||
} from "@langfuse/shared/src/server";
|
||||
import { Prisma, prisma } from "../../src/db";
|
||||
import { prisma } from "../../src/db";
|
||||
import { redis } from "@langfuse/shared/src/server";
|
||||
import { prepareClickhouse } from "../../scripts/prepareClickhouse";
|
||||
import { createDatasets } from "../../prisma/seed";
|
||||
|
||||
@@ -19,16 +19,7 @@ then
|
||||
fi
|
||||
|
||||
# Construct the database URL
|
||||
if [ "$CLICKHOUSE_CLUSTER_ENABLED" == "true" ] ; then
|
||||
if [ "$CLICKHOUSE_MIGRATION_SSL" = true ] ; then
|
||||
DATABASE_URL="${CLICKHOUSE_MIGRATION_URL}?username=${CLICKHOUSE_USER}&password=${CLICKHOUSE_PASSWORD}&database=default&x-multi-statement=true&secure=true&skip_verify=true&x-cluster-name=default&x-migrations-table-engine=ReplicatedMergeTree"
|
||||
else
|
||||
DATABASE_URL="${CLICKHOUSE_MIGRATION_URL}?username=${CLICKHOUSE_USER}&password=${CLICKHOUSE_PASSWORD}&database=default&x-multi-statement=true&x-cluster-name=default&x-migrations-table-engine=ReplicatedMergeTree"
|
||||
fi
|
||||
|
||||
# Execute the up command
|
||||
migrate -source file://clickhouse/migrations/clustered -database "$DATABASE_URL" up
|
||||
else
|
||||
if [ "$CLICKHOUSE_CLUSTER_ENABLED" == "false" ] ; then
|
||||
if [ "$CLICKHOUSE_MIGRATION_SSL" = true ] ; then
|
||||
DATABASE_URL="${CLICKHOUSE_MIGRATION_URL}?username=${CLICKHOUSE_USER}&password=${CLICKHOUSE_PASSWORD}&database=default&x-multi-statement=true&secure=true&skip_verify=true&x-migrations-table-engine=MergeTree"
|
||||
else
|
||||
@@ -37,4 +28,13 @@ else
|
||||
|
||||
# Execute the up command
|
||||
migrate -source file://clickhouse/migrations/unclustered -database "$DATABASE_URL" up
|
||||
else
|
||||
if [ "$CLICKHOUSE_MIGRATION_SSL" = true ] ; then
|
||||
DATABASE_URL="${CLICKHOUSE_MIGRATION_URL}?username=${CLICKHOUSE_USER}&password=${CLICKHOUSE_PASSWORD}&database=default&x-multi-statement=true&secure=true&skip_verify=true&x-cluster-name=default&x-migrations-table-engine=ReplicatedMergeTree"
|
||||
else
|
||||
DATABASE_URL="${CLICKHOUSE_MIGRATION_URL}?username=${CLICKHOUSE_USER}&password=${CLICKHOUSE_PASSWORD}&database=default&x-multi-statement=true&x-cluster-name=default&x-migrations-table-engine=ReplicatedMergeTree"
|
||||
fi
|
||||
|
||||
# Execute the up command
|
||||
migrate -source file://clickhouse/migrations/clustered -database "$DATABASE_URL" up
|
||||
fi
|
||||
|
||||
@@ -64,6 +64,7 @@
|
||||
"@langchain/anthropic": "^0.3.8",
|
||||
"@langchain/aws": "^0.1.2",
|
||||
"@langchain/core": "^0.3.18",
|
||||
"@langchain/google-vertexai": "^0.1.3",
|
||||
"@langchain/openai": "^0.3.14",
|
||||
"@opentelemetry/api": ">=1.0.0 <1.10.0",
|
||||
"@prisma/client": "^5.22.0",
|
||||
|
||||
@@ -450,6 +450,9 @@ export async function createDatasets(
|
||||
Math.random() > 0.3
|
||||
? observations[Math.floor(Math.random() * observations.length)]
|
||||
: undefined;
|
||||
if (!sourceObservation) {
|
||||
continue;
|
||||
}
|
||||
const datasetItem = await prisma.datasetItem.create({
|
||||
data: {
|
||||
projectId,
|
||||
|
||||
@@ -110,7 +110,7 @@ export const prepareClickhouse = async (
|
||||
map('input', toDecimal64(randUniform(0, 1000), 12), 'output', toDecimal64(randUniform(0, 1000), 12), 'total', toDecimal64(randUniform(0, 2000), 12)) AS provided_cost_details,
|
||||
map('input', toDecimal64(randUniform(0, 1000), 12), 'output', toDecimal64(randUniform(0, 1000), 12), 'total', toDecimal64(randUniform(0, 2000), 12)) AS cost_details,
|
||||
toDecimal64(randUniform(0, 2000), 12) AS total_cost,
|
||||
start_time AS completion_start_time,
|
||||
addMilliseconds(start_time, if(rand() < 0.6, floor(randUniform(0, 500)), floor(randUniform(0, 600)))) AS completion_start_time,
|
||||
array(${SEED_PROMPTS.map((p) => `concat('${p.id}',project_id)`).join(
|
||||
",",
|
||||
)})[(number % ${SEED_PROMPTS.length})+1] AS prompt_id,
|
||||
|
||||
+17
-10
@@ -27,15 +27,18 @@ const EnvSchema = z.object({
|
||||
.optional(),
|
||||
LANGFUSE_CACHE_PROMPT_ENABLED: z.enum(["true", "false"]).default("false"),
|
||||
LANGFUSE_CACHE_PROMPT_TTL_SECONDS: z.coerce.number().default(60 * 60),
|
||||
CLICKHOUSE_URL: z.string().url().optional(),
|
||||
CLICKHOUSE_USER: z.string().optional(),
|
||||
CLICKHOUSE_PASSWORD: z.string().optional(),
|
||||
LANGFUSE_ASYNC_CLICKHOUSE_INGESTION_PROCESSING: z
|
||||
CLICKHOUSE_URL: z.string().url(),
|
||||
CLICKHOUSE_USER: z.string(),
|
||||
CLICKHOUSE_PASSWORD: z.string(),
|
||||
|
||||
LANGFUSE_CLICKHOUSE_INGESTION_ENABLED: z
|
||||
.enum(["true", "false"])
|
||||
.default("false"),
|
||||
LANGFUSE_ASYNC_INGESTION_PROCESSING: z
|
||||
.default("true"),
|
||||
// TODO: Disable Postgres before go-live
|
||||
LANGFUSE_POSTGRES_INGESTION_ENABLED: z
|
||||
.enum(["true", "false"])
|
||||
.default("false"),
|
||||
.default("true"),
|
||||
|
||||
LANGFUSE_INGESTION_QUEUE_DELAY_MS: z.coerce
|
||||
.number()
|
||||
.nonnegative()
|
||||
@@ -48,8 +51,9 @@ const EnvSchema = z.object({
|
||||
ENABLE_AWS_CLOUDWATCH_METRIC_PUBLISHING: z
|
||||
.enum(["true", "false"])
|
||||
.default("false"),
|
||||
LANGFUSE_S3_EVENT_UPLOAD_ENABLED: z.enum(["true", "false"]).default("false"),
|
||||
LANGFUSE_S3_EVENT_UPLOAD_BUCKET: z.string().optional(),
|
||||
LANGFUSE_S3_EVENT_UPLOAD_BUCKET: z.string({
|
||||
required_error: "Langfuse requires a bucket name for S3 Event Uploads.",
|
||||
}),
|
||||
LANGFUSE_S3_EVENT_UPLOAD_PREFIX: z.string().default(""),
|
||||
LANGFUSE_S3_EVENT_UPLOAD_REGION: z.string().optional(),
|
||||
LANGFUSE_S3_EVENT_UPLOAD_ENDPOINT: z.string().optional(),
|
||||
@@ -62,4 +66,7 @@ const EnvSchema = z.object({
|
||||
STRIPE_SECRET_KEY: z.string().optional(),
|
||||
});
|
||||
|
||||
export const env = EnvSchema.parse(removeEmptyEnvVariables(process.env));
|
||||
export const env: z.infer<typeof EnvSchema> =
|
||||
process.env.DOCKER_BUILD === "1"
|
||||
? (process.env as any)
|
||||
: EnvSchema.parse(removeEmptyEnvVariables(process.env));
|
||||
|
||||
@@ -3,15 +3,17 @@ export const planLabels = {
|
||||
"cloud:hobby": "Hobby",
|
||||
"cloud:pro": "Pro",
|
||||
"cloud:team": "Team",
|
||||
"self-hosted:enterprise": "Enterprise",
|
||||
"self-hosted:pro": "Pro (self-hosted)",
|
||||
"self-hosted:enterprise": "Enterprise (self-hosted)",
|
||||
} as const;
|
||||
|
||||
export type Plan = keyof typeof planLabels;
|
||||
|
||||
export const plans = Object.keys(planLabels) as Plan[];
|
||||
|
||||
// This function is kept here to ensure consistency when updating plan names in the future.
|
||||
// These functions are kept here to ensure consistency when updating plan names in the future.
|
||||
export const isCloudPlan = (plan: Plan) => plan.startsWith("cloud");
|
||||
export const isSelfHostedPlan = (plan: Plan) => plan.startsWith("self-hosted");
|
||||
|
||||
export const isPlan = (value: string): value is Plan =>
|
||||
plans.includes(value as Plan);
|
||||
|
||||
@@ -7,7 +7,6 @@ export * from "./interfaces/customLLMProviderConfigSchemas";
|
||||
export * from "./tableDefinitions";
|
||||
export * from "./types";
|
||||
export * from "./tableDefinitions/tracesTable";
|
||||
export * from "./server/auth/apiKeys";
|
||||
export * from "./observationsTable";
|
||||
export * from "./utils/zod";
|
||||
export * from "./utils/json";
|
||||
@@ -16,7 +15,6 @@ export * from "./utils/objects";
|
||||
export * from "./utils/typeChecks";
|
||||
export * from "./features/entitlements/plans";
|
||||
export * from "./interfaces/rate-limits";
|
||||
export { env } from "./env";
|
||||
|
||||
// llm api
|
||||
export * from "./server/llm/types";
|
||||
|
||||
@@ -10,3 +10,19 @@ export const BedrockCredentialSchema = z
|
||||
})
|
||||
.optional();
|
||||
export type BedrockCredential = z.infer<typeof BedrockCredentialSchema>;
|
||||
|
||||
export const GCPServiceAccountKeySchema = z.object({
|
||||
type: z.literal("service_account"),
|
||||
project_id: z.string(),
|
||||
private_key_id: z.string(),
|
||||
private_key: z.string(),
|
||||
client_email: z.string(),
|
||||
client_id: z.string(),
|
||||
auth_uri: z.string(),
|
||||
token_uri: z.string(),
|
||||
auth_provider_x509_cert_url: z.string(),
|
||||
client_x509_cert_url: z.string(),
|
||||
});
|
||||
|
||||
export type GCPServiceAccountKey = z.infer<typeof GCPServiceAccountKeySchema>;
|
||||
export default GCPServiceAccountKeySchema;
|
||||
|
||||
@@ -0,0 +1,65 @@
|
||||
import type { OAuthConfig, OAuthUserConfig } from "next-auth/providers/oauth";
|
||||
import type { GithubProfile, GithubEmail } from "next-auth/providers/github";
|
||||
|
||||
export function GitHubEnterpriseProvider<P extends GithubProfile>(
|
||||
options: OAuthUserConfig<P> & {
|
||||
enterprise?: {
|
||||
baseUrl?: string;
|
||||
};
|
||||
}
|
||||
): OAuthConfig<P> {
|
||||
const baseUrl = options?.enterprise?.baseUrl ?? "https://github.com"
|
||||
const apiBaseUrl = options?.enterprise?.baseUrl
|
||||
? `${options?.enterprise?.baseUrl}/api/v3`
|
||||
: "https://api.github.com"
|
||||
|
||||
return {
|
||||
id: "github-enterprise",
|
||||
name: "GitHub Enterprise",
|
||||
type: "oauth",
|
||||
authorization: {
|
||||
url: `${baseUrl}/login/oauth/authorize`,
|
||||
params: { scope: "read:user user:email" },
|
||||
},
|
||||
token: `${baseUrl}/login/oauth/access_token`,
|
||||
userinfo: {
|
||||
url: `${apiBaseUrl}/user`,
|
||||
async request({ client, tokens }) {
|
||||
const profile = await client.userinfo(tokens.access_token!)
|
||||
|
||||
if (!profile.email) {
|
||||
// If the user does not have a public email, get another via the GitHub API
|
||||
// See https://docs.github.com/en/rest/users/emails#list-email-addresses-for-the-authenticated-user
|
||||
const res = await fetch(`${apiBaseUrl}/user/emails`, {
|
||||
headers: { Authorization: `token ${tokens.access_token}` },
|
||||
})
|
||||
|
||||
if (res.ok) {
|
||||
const emails: GithubEmail[] = await res.json()
|
||||
profile.email = (emails.find((e) => e.primary) ?? emails[0]).email
|
||||
}
|
||||
}
|
||||
|
||||
return profile
|
||||
},
|
||||
},
|
||||
profile(profile) {
|
||||
return {
|
||||
id: profile.id.toString(),
|
||||
name: profile.name ?? profile.login,
|
||||
email: profile.email,
|
||||
image: profile.avatar_url,
|
||||
}
|
||||
},
|
||||
style: {
|
||||
logo: "https://raw.githubusercontent.com/nextauthjs/next-auth/main/packages/next-auth/provider-logos/github.svg",
|
||||
logoDark:
|
||||
"https://raw.githubusercontent.com/nextauthjs/next-auth/main/packages/next-auth/provider-logos/github-dark.svg",
|
||||
bg: "#fff",
|
||||
bgDark: "#000",
|
||||
text: "#000",
|
||||
textDark: "#fff",
|
||||
},
|
||||
options,
|
||||
}
|
||||
}
|
||||
@@ -6,6 +6,7 @@ export * from "./services/PromptService";
|
||||
export * from "./services/traces-ui-table-service";
|
||||
export * from "./auth/apiKeys";
|
||||
export * from "./auth/customSsoProvider";
|
||||
export * from "./auth/gitHubEnterpriseProvider";
|
||||
export * from "./llm/fetchLLMCompletion";
|
||||
export * from "./llm/types";
|
||||
export * from "./utils/DatabaseReadStream";
|
||||
@@ -21,8 +22,9 @@ export * from "../server/ingestion/types";
|
||||
export * from "../server/ingestion/validateAndInflateScore";
|
||||
export * from "./redis/redis";
|
||||
export * from "./redis/traceUpsert";
|
||||
export * from "./redis/CloudUsageMeteringQueue";
|
||||
export * from "./redis/cloudUsageMeteringQueue";
|
||||
export * from "./redis/getQueue";
|
||||
export * from "./redis/traceDelete";
|
||||
export * from "./redis/datasetRunItemUpsert";
|
||||
export * from "./redis/batchExport";
|
||||
export * from "./redis/legacyIngestion";
|
||||
|
||||
@@ -83,7 +83,7 @@ export const handleBatch = async (
|
||||
|
||||
async function retry<T>(request: () => Promise<T>): Promise<T> {
|
||||
return await backOff(request, {
|
||||
numOfAttempts: env.LANGFUSE_ASYNC_INGESTION_PROCESSING === "true" ? 5 : 3,
|
||||
numOfAttempts: 5,
|
||||
retry: (e: Error, attemptNumber: number) => {
|
||||
if (e instanceof UnauthorizedError || e instanceof ForbiddenError) {
|
||||
logger.info("not retrying auth error");
|
||||
|
||||
@@ -156,91 +156,89 @@ export const processEventBatch = async (
|
||||
* ASYNC PROCESSING *
|
||||
********************/
|
||||
let s3UploadErrored = false;
|
||||
if (env.LANGFUSE_S3_EVENT_UPLOAD_ENABLED === "true") {
|
||||
await instrumentAsync({ name: "s3-upload-events" }, async () => {
|
||||
if (env.LANGFUSE_S3_EVENT_UPLOAD_BUCKET === undefined) {
|
||||
throw new Error("S3 event store is enabled but no bucket is set");
|
||||
}
|
||||
const s3Client = getS3StorageServiceClient(
|
||||
env.LANGFUSE_S3_EVENT_UPLOAD_BUCKET,
|
||||
);
|
||||
// S3 Event Upload is currently blocking, but non-failing.
|
||||
// If a promise rejects, we log it below, but do not throw an error.
|
||||
// In this case, we upload the full batch into the Redis queue.
|
||||
const results = await Promise.allSettled(
|
||||
Object.keys(sortedBatchByEventBodyId).map(async (id) => {
|
||||
// We upload the event in an array to the S3 bucket grouped by the eventBodyId.
|
||||
// That way we batch updates from the same invocation into a single file and reduce
|
||||
// write operations on S3.
|
||||
const { data, key, type, eventBodyId } = sortedBatchByEventBodyId[id];
|
||||
return s3Client.uploadJson(
|
||||
`${env.LANGFUSE_S3_EVENT_UPLOAD_PREFIX}${authCheck.scope.projectId}/${getClickhouseEntityType(type)}/${eventBodyId}/${key}.json`,
|
||||
data,
|
||||
);
|
||||
}),
|
||||
);
|
||||
results.forEach((result) => {
|
||||
if (result.status === "rejected") {
|
||||
s3UploadErrored = true;
|
||||
logger.error("Failed to upload event to S3", {
|
||||
error: result.reason,
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// Send each event individually to IngestionQueue for new processing
|
||||
if (
|
||||
env.LANGFUSE_ASYNC_INGESTION_PROCESSING === "true" &&
|
||||
env.LANGFUSE_S3_EVENT_UPLOAD_ENABLED === "true" &&
|
||||
env.LANGFUSE_ASYNC_CLICKHOUSE_INGESTION_PROCESSING === "true" &&
|
||||
redis &&
|
||||
!s3UploadErrored
|
||||
) {
|
||||
const queue = IngestionQueue.getInstance();
|
||||
await instrumentAsync({ name: "s3-upload-events" }, async () => {
|
||||
const s3Client = getS3StorageServiceClient(
|
||||
env.LANGFUSE_S3_EVENT_UPLOAD_BUCKET,
|
||||
);
|
||||
// S3 Event Upload is blocking, but non-failing.
|
||||
// If a promise rejects, we log it below, but do not throw an error.
|
||||
// In this case, we upload the full batch into the Redis queue.
|
||||
const results = await Promise.allSettled(
|
||||
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"),
|
||||
),
|
||||
Object.keys(sortedBatchByEventBodyId).map(async (id) => {
|
||||
// We upload the event in an array to the S3 bucket grouped by the eventBodyId.
|
||||
// That way we batch updates from the same invocation into a single file and reduce
|
||||
// write operations on S3.
|
||||
const { data, key, type, eventBodyId } = sortedBatchByEventBodyId[id];
|
||||
return s3Client.uploadJson(
|
||||
`${env.LANGFUSE_S3_EVENT_UPLOAD_PREFIX}${authCheck.scope.projectId}/${getClickhouseEntityType(type)}/${eventBodyId}/${key}.json`,
|
||||
data,
|
||||
);
|
||||
}),
|
||||
);
|
||||
results.forEach((result) => {
|
||||
if (result.status === "rejected") {
|
||||
logger.error("Failed to add event to IngestionQueue", {
|
||||
s3UploadErrored = true;
|
||||
logger.error("Failed to upload event to S3", {
|
||||
error: result.reason,
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// 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,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// As part of the legacy processing we sent the entire batch to the worker.
|
||||
if (env.LANGFUSE_ASYNC_INGESTION_PROCESSING === "true" && redis) {
|
||||
const queue = LegacyIngestionQueue.getInstance();
|
||||
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;
|
||||
if (queue) {
|
||||
let addToQueueFailed = false;
|
||||
|
||||
const queuePayload: LegacyIngestionEventType =
|
||||
env.LANGFUSE_S3_EVENT_UPLOAD_ENABLED === "true" && !s3UploadErrored
|
||||
const queuePayload: LegacyIngestionEventType = !s3UploadErrored
|
||||
? {
|
||||
data: Object.keys(sortedBatchByEventBodyId).map((id) => {
|
||||
const { key, type, eventBodyId } = sortedBatchByEventBodyId[id];
|
||||
@@ -255,44 +253,55 @@ export const processEventBatch = async (
|
||||
}
|
||||
: { 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;
|
||||
}
|
||||
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 })),
|
||||
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",
|
||||
);
|
||||
}
|
||||
} else {
|
||||
logger.error(
|
||||
"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,
|
||||
);
|
||||
}
|
||||
|
||||
/*******************
|
||||
* 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,
|
||||
throw new Error(
|
||||
"Either Clickhouse or Postgres ingestion (or both) must be enabled",
|
||||
);
|
||||
};
|
||||
|
||||
@@ -336,6 +345,7 @@ const sortBatch = (batch: Array<z.infer<typeof ingestionEvent>>) => {
|
||||
export const aggregateBatchResult = (
|
||||
errors: Array<{ id: string; error: unknown }>,
|
||||
results: Array<{ id: string; result: unknown }>,
|
||||
projectId?: string,
|
||||
) => {
|
||||
const returnedErrors: {
|
||||
id: string;
|
||||
@@ -382,7 +392,10 @@ export const aggregateBatchResult = (
|
||||
|
||||
if (returnedErrors.length > 0) {
|
||||
traceException(errors);
|
||||
logger.error("Error processing events", returnedErrors);
|
||||
logger.error("Error processing events", {
|
||||
errors: returnedErrors,
|
||||
"langfuse.project.id": projectId,
|
||||
});
|
||||
}
|
||||
|
||||
results.forEach((result) => {
|
||||
|
||||
@@ -59,7 +59,7 @@ export const usage = MixedUsage.nullish()
|
||||
export const TraceBody = z.object({
|
||||
id: z.string().nullish(),
|
||||
timestamp: stringDateTime,
|
||||
name: z.string().nullish(),
|
||||
name: z.string().max(1000).nullish(),
|
||||
externalId: z.string().nullish(),
|
||||
input: jsonSchema.nullish(),
|
||||
output: jsonSchema.nullish(),
|
||||
|
||||
@@ -17,7 +17,7 @@ type ValidateAndInflateScoreParams = {
|
||||
export async function validateAndInflateScore(
|
||||
params: ValidateAndInflateScoreParams,
|
||||
): Promise<Score> {
|
||||
const { body, projectId } = params;
|
||||
const { body, projectId, scoreId } = params;
|
||||
|
||||
if (body.configId) {
|
||||
const config = await prisma.scoreConfig.findFirst({
|
||||
@@ -32,10 +32,22 @@ export async function validateAndInflateScore(
|
||||
"The configId you provided does not match a valid config in this project",
|
||||
);
|
||||
|
||||
validateConfigAgainstBody(body, config as ValidatedScoreConfig);
|
||||
// Override some fields in the score body with config fields
|
||||
// We ignore the set fields in the body
|
||||
const bodyWithConfigOverrides = {
|
||||
...body,
|
||||
name: config.name,
|
||||
};
|
||||
|
||||
validateConfigAgainstBody(
|
||||
bodyWithConfigOverrides,
|
||||
config as ValidatedScoreConfig,
|
||||
);
|
||||
|
||||
return inflateScoreBody({
|
||||
...params,
|
||||
projectId,
|
||||
scoreId,
|
||||
body: bodyWithConfigOverrides,
|
||||
config: config as ValidatedScoreConfig,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ import type { ZodSchema } from "zod";
|
||||
import { CallbackHandler } from "langfuse-langchain";
|
||||
|
||||
import { ChatAnthropic } from "@langchain/anthropic";
|
||||
import { ChatVertexAI } from "@langchain/google-vertexai";
|
||||
import { ChatBedrockConverse } from "@langchain/aws";
|
||||
import {
|
||||
AIMessage,
|
||||
@@ -15,7 +16,7 @@ import {
|
||||
} from "@langchain/core/output_parsers";
|
||||
import { IterableReadableStream } from "@langchain/core/utils/stream";
|
||||
import { ChatOpenAI } from "@langchain/openai";
|
||||
import {
|
||||
import GCPServiceAccountKeySchema, {
|
||||
BedrockConfigSchema,
|
||||
BedrockCredentialSchema,
|
||||
} from "../../interfaces/customLLMProviderConfigSchemas";
|
||||
@@ -139,7 +140,11 @@ export async function fetchLLMCompletion(
|
||||
return new AIMessage(message.content);
|
||||
});
|
||||
|
||||
let chatModel: ChatOpenAI | ChatAnthropic | ChatBedrockConverse;
|
||||
let chatModel:
|
||||
| ChatOpenAI
|
||||
| ChatAnthropic
|
||||
| ChatBedrockConverse
|
||||
| ChatVertexAI;
|
||||
if (modelParams.adapter === LLMAdapter.Anthropic) {
|
||||
chatModel = new ChatAnthropic({
|
||||
anthropicApiKey: apiKey,
|
||||
@@ -191,6 +196,21 @@ export async function fetchLLMCompletion(
|
||||
callbacks: finalCallbacks,
|
||||
maxRetries,
|
||||
});
|
||||
} else if (modelParams.adapter === LLMAdapter.VertexAI) {
|
||||
const credentials = GCPServiceAccountKeySchema.parse(JSON.parse(apiKey));
|
||||
|
||||
chatModel = new ChatVertexAI({
|
||||
modelName: modelParams.model,
|
||||
temperature: modelParams.temperature,
|
||||
maxOutputTokens: modelParams.max_tokens,
|
||||
topP: modelParams.top_p,
|
||||
callbacks: finalCallbacks,
|
||||
maxRetries,
|
||||
authOptions: {
|
||||
projectId: credentials.project_id,
|
||||
credentials,
|
||||
},
|
||||
});
|
||||
} else {
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
const _exhaustiveCheck: never = modelParams.adapter;
|
||||
|
||||
@@ -16,6 +16,7 @@ export enum LLMAdapter {
|
||||
OpenAI = "openai",
|
||||
Azure = "azure",
|
||||
Bedrock = "bedrock",
|
||||
VertexAI = "vertex-ai",
|
||||
}
|
||||
|
||||
export enum ChatMessageRole {
|
||||
@@ -114,10 +115,18 @@ export const anthropicModels = [
|
||||
"claude-instant-1.2",
|
||||
] as const;
|
||||
|
||||
export const vertexAIModels = [
|
||||
"gemini-1.5-pro",
|
||||
"gemini-1.5-flash",
|
||||
"gemini-1.0-pro",
|
||||
] as const;
|
||||
|
||||
export type AnthropicModel = (typeof anthropicModels)[number];
|
||||
export type VertexAIModel = (typeof vertexAIModels)[number];
|
||||
export const supportedModels = {
|
||||
[LLMAdapter.Anthropic]: anthropicModels,
|
||||
[LLMAdapter.OpenAI]: openAIModels,
|
||||
[LLMAdapter.VertexAI]: vertexAIModels,
|
||||
[LLMAdapter.Azure]: [],
|
||||
[LLMAdapter.Bedrock]: [],
|
||||
} as const;
|
||||
|
||||
@@ -161,8 +161,8 @@ export class StringOptionsFilter implements Filter {
|
||||
return {
|
||||
query:
|
||||
this.operator === "any of"
|
||||
? `has({${varName}: Array(String)}, ${this.tablePrefix ? this.tablePrefix + "." : ""}${this.field}) = True`
|
||||
: `has({${varName}: Array(String)}, ${this.tablePrefix ? this.tablePrefix + "." : ""}${this.field}) = False`,
|
||||
? `${this.tablePrefix ? this.tablePrefix + "." : ""}${this.field} IN ({${varName}: Array(String)})`
|
||||
: `${this.tablePrefix ? this.tablePrefix + "." : ""}${this.field} NOT IN ({${varName}: Array(String)})`,
|
||||
params: { [varName]: this.values },
|
||||
};
|
||||
}
|
||||
|
||||
@@ -3,31 +3,53 @@ import { OrderByState } from "../../../interfaces/orderBy";
|
||||
import { UiColumnMapping } from "../../../tableDefinitions";
|
||||
import { logger } from "../../logger";
|
||||
|
||||
type OrderByStateNotNull = Exclude<OrderByState, null>;
|
||||
|
||||
export function orderByToClickhouseSql(
|
||||
orderBy: OrderByState,
|
||||
orderBy: OrderByState | OrderByState[] = [],
|
||||
tableColumns: UiColumnMapping[],
|
||||
): string {
|
||||
if (!orderBy) {
|
||||
if (
|
||||
!orderBy ||
|
||||
(Array.isArray(orderBy) && orderBy.filter(Boolean).length === 0)
|
||||
) {
|
||||
return "";
|
||||
}
|
||||
// Get column definition to map column to internal name, e.g. "t.id"
|
||||
const col = tableColumns.find(
|
||||
(c) => c.uiTableName === orderBy.column || c.uiTableId === orderBy.column,
|
||||
);
|
||||
|
||||
if (!col) {
|
||||
logger.warn("Invalid order by column", orderBy.column);
|
||||
throw new Error("Invalid order by column: " + orderBy.column);
|
||||
if (!Array.isArray(orderBy)) {
|
||||
orderBy = [orderBy];
|
||||
}
|
||||
// Initialize an array to hold order by clauses
|
||||
const orderByClauses: string[] = [];
|
||||
|
||||
// Loop through each orderBy entry
|
||||
for (const ob of orderBy.filter((o): o is OrderByStateNotNull =>
|
||||
Boolean(o),
|
||||
)) {
|
||||
// Get column definition to map column to internal name, e.g. "t.id"
|
||||
const col = tableColumns.find(
|
||||
(c) => c.uiTableName === ob.column || c.uiTableId === ob.column,
|
||||
);
|
||||
|
||||
if (!col) {
|
||||
logger.warn("Invalid order by column", ob.column);
|
||||
throw new Error("Invalid order by column: " + ob.column);
|
||||
}
|
||||
|
||||
// Assert that ob.order is either "asc" or "desc"
|
||||
const orderByOrder = z.enum(["ASC", "DESC"]);
|
||||
const order = orderByOrder.safeParse(ob.order);
|
||||
if (!order.success) {
|
||||
logger.warn("Invalid order", ob.order);
|
||||
throw new Error("Invalid order: " + ob.order);
|
||||
}
|
||||
|
||||
// Append the order by clause to the array
|
||||
orderByClauses.push(
|
||||
`${col.queryPrefix ? col.queryPrefix + "." : ""}${col.clickhouseSelect} ${order.data}`,
|
||||
);
|
||||
}
|
||||
|
||||
// Assert that orderBy.order is either "asc" or "desc"
|
||||
const orderByOrder = z.enum(["ASC", "DESC"]);
|
||||
const order = orderByOrder.safeParse(orderBy.order);
|
||||
if (!order.success) {
|
||||
logger.warn("Invalid order", orderBy.order);
|
||||
throw new Error("Invalid order: " + orderBy.order);
|
||||
}
|
||||
|
||||
// Both column and order are safe, can use raw SQL
|
||||
return `ORDER BY ${col.queryPrefix ? col.queryPrefix + "." : ""}${col.clickhouseSelect} ${order.data}`;
|
||||
// Join all order by clauses with a comma and return
|
||||
return `ORDER BY ${orderByClauses.join(", ")}`;
|
||||
}
|
||||
|
||||
@@ -20,3 +20,4 @@ export {
|
||||
NullFilter,
|
||||
type ClickhouseOperator,
|
||||
} from "./clickhouse-sql/clickhouse-filter";
|
||||
export { orderByToClickhouseSql } from "./clickhouse-sql/orderby-factory";
|
||||
|
||||
@@ -63,7 +63,7 @@ export const BatchExportJobSchema = z.object({
|
||||
projectId: z.string(),
|
||||
batchExportId: z.string(),
|
||||
});
|
||||
export const TraceUpsertEventSchema = z.object({
|
||||
export const TraceQueueEventSchema = z.object({
|
||||
projectId: z.string(),
|
||||
traceId: z.string(),
|
||||
});
|
||||
@@ -87,7 +87,7 @@ export const ExperimentCreateEventSchema = z.object({
|
||||
});
|
||||
|
||||
export type BatchExportJobType = z.infer<typeof BatchExportJobSchema>;
|
||||
export type TraceUpsertEventType = z.infer<typeof TraceUpsertEventSchema>;
|
||||
export type TraceQueueEventType = z.infer<typeof TraceQueueEventSchema>;
|
||||
export type DatasetRunItemUpsertEventType = z.infer<
|
||||
typeof DatasetRunItemUpsertEventSchema
|
||||
>;
|
||||
@@ -101,7 +101,7 @@ export type ExperimentCreateEventType = z.infer<
|
||||
export const EventBodySchema = z.union([
|
||||
z.object({
|
||||
name: z.literal(EventName.TraceUpsert),
|
||||
payload: z.array(TraceUpsertEventSchema),
|
||||
payload: z.array(TraceQueueEventSchema),
|
||||
}),
|
||||
z.object({
|
||||
name: z.literal(EventName.EvaluationExecution),
|
||||
@@ -120,6 +120,7 @@ export type EventBodyType = z.infer<typeof EventBodySchema>;
|
||||
|
||||
export enum QueueName {
|
||||
TraceUpsert = "trace-upsert", // Ingestion pipeline adds events on each Trace upsert
|
||||
TraceDelete = "trace-delete",
|
||||
EvaluationExecution = "evaluation-execution-queue", // Worker executes Evals
|
||||
DatasetRunItemUpsert = "dataset-run-item-upsert-queue",
|
||||
BatchExport = "batch-export-queue",
|
||||
@@ -131,6 +132,7 @@ export enum QueueName {
|
||||
|
||||
export enum QueueJobs {
|
||||
TraceUpsert = "trace-upsert",
|
||||
TraceDelete = "trace-delete",
|
||||
DatasetRunItemUpsert = "dataset-run-item-upsert",
|
||||
EvaluationExecution = "evaluation-execution-job",
|
||||
BatchExportJob = "batch-export-job",
|
||||
@@ -145,9 +147,15 @@ export type TQueueJobTypes = {
|
||||
[QueueName.TraceUpsert]: {
|
||||
timestamp: Date;
|
||||
id: string;
|
||||
payload: TraceUpsertEventType;
|
||||
payload: TraceQueueEventType;
|
||||
name: QueueJobs.TraceUpsert;
|
||||
};
|
||||
[QueueName.TraceDelete]: {
|
||||
timestamp: Date;
|
||||
id: string;
|
||||
payload: TraceQueueEventType;
|
||||
name: QueueJobs.TraceDelete;
|
||||
};
|
||||
[QueueName.DatasetRunItemUpsert]: {
|
||||
timestamp: Date;
|
||||
id: string;
|
||||
|
||||
+1
-1
@@ -1,5 +1,5 @@
|
||||
import { Queue } from "bullmq";
|
||||
import { env } from "../..";
|
||||
import { env } from "../../env";
|
||||
import { logger } from "@azure/storage-blob";
|
||||
import { QueueName, QueueJobs } from "../queues";
|
||||
import { createNewRedisInstance, redisQueueRetryOptions } from "./redis";
|
||||
@@ -1,13 +1,14 @@
|
||||
import { Queue } from "bullmq";
|
||||
import { QueueName } from "../queues";
|
||||
import { BatchExportQueue } from "./batchExport";
|
||||
import { CloudUsageMeteringQueue } from "./CloudUsageMeteringQueue";
|
||||
import { CloudUsageMeteringQueue } from "./cloudUsageMeteringQueue";
|
||||
import { DatasetRunItemUpsertQueue } from "./datasetRunItemUpsert";
|
||||
import { EvalExecutionQueue } from "./evalExecutionQueue";
|
||||
import { ExperimentCreateQueue } from "./experimentCreateQueue";
|
||||
import { IngestionQueue } from "./ingestionQueue";
|
||||
import { LegacyIngestionQueue } from "./legacyIngestion";
|
||||
import { TraceUpsertQueue } from "./traceUpsert";
|
||||
import { TraceDeleteQueue } from "./traceDelete";
|
||||
|
||||
export function getQueue(queueName: QueueName): Queue | null {
|
||||
switch (queueName) {
|
||||
@@ -25,6 +26,8 @@ export function getQueue(queueName: QueueName): Queue | null {
|
||||
return ExperimentCreateQueue.getInstance();
|
||||
case QueueName.TraceUpsert:
|
||||
return TraceUpsertQueue.getInstance();
|
||||
case QueueName.TraceDelete:
|
||||
return TraceDeleteQueue.getInstance();
|
||||
case QueueName.IngestionQueue:
|
||||
return IngestionQueue.getInstance();
|
||||
default:
|
||||
|
||||
@@ -0,0 +1,44 @@
|
||||
import { QueueName, TQueueJobTypes } from "../queues";
|
||||
import { Queue } from "bullmq";
|
||||
import { createNewRedisInstance, redisQueueRetryOptions } from "./redis";
|
||||
import { logger } from "../logger";
|
||||
|
||||
export class TraceDeleteQueue {
|
||||
private static instance: Queue<TQueueJobTypes[QueueName.TraceDelete]> | null =
|
||||
null;
|
||||
|
||||
public static getInstance(): Queue<
|
||||
TQueueJobTypes[QueueName.TraceDelete]
|
||||
> | null {
|
||||
if (TraceDeleteQueue.instance) return TraceDeleteQueue.instance;
|
||||
|
||||
const newRedis = createNewRedisInstance({
|
||||
enableOfflineQueue: false,
|
||||
...redisQueueRetryOptions,
|
||||
});
|
||||
|
||||
TraceDeleteQueue.instance = newRedis
|
||||
? new Queue<TQueueJobTypes[QueueName.TraceDelete]>(
|
||||
QueueName.TraceDelete,
|
||||
{
|
||||
connection: newRedis,
|
||||
defaultJobOptions: {
|
||||
removeOnComplete: true,
|
||||
removeOnFail: 100_000,
|
||||
attempts: 5,
|
||||
backoff: {
|
||||
type: "exponential",
|
||||
delay: 5000,
|
||||
},
|
||||
},
|
||||
},
|
||||
)
|
||||
: null;
|
||||
|
||||
TraceDeleteQueue.instance?.on("error", (err) => {
|
||||
logger.error("TraceDeleteQueue error", err);
|
||||
});
|
||||
|
||||
return TraceDeleteQueue.instance;
|
||||
}
|
||||
}
|
||||
@@ -1,9 +1,6 @@
|
||||
import { randomUUID } from "crypto";
|
||||
import {
|
||||
QueueJobs,
|
||||
QueueName,
|
||||
TQueueJobTypes,
|
||||
TraceUpsertEventType,
|
||||
} from "../queues";
|
||||
import { Queue } from "bullmq";
|
||||
import { createNewRedisInstance, redisQueueRetryOptions } from "./redis";
|
||||
|
||||
@@ -40,37 +40,31 @@ export async function upsertClickhouse<
|
||||
// https://opentelemetry.io/docs/specs/semconv/database/database-spans/
|
||||
span.setAttribute("ch.query.table", opts.table);
|
||||
|
||||
// If event upload is enabled, we store all rows in S3 to have a backup
|
||||
if (env.LANGFUSE_S3_EVENT_UPLOAD_ENABLED === "true") {
|
||||
if (env.LANGFUSE_S3_EVENT_UPLOAD_BUCKET === undefined) {
|
||||
throw new Error("S3 event store is enabled but no bucket is set");
|
||||
}
|
||||
const s3Client = getS3StorageServiceClient(
|
||||
env.LANGFUSE_S3_EVENT_UPLOAD_BUCKET,
|
||||
);
|
||||
await Promise.all(
|
||||
opts.records.map((record) => {
|
||||
// drop trailing s and pretend it's always a create.
|
||||
// Only applicable to scores and traces.
|
||||
let eventType = `${opts.table.slice(0, -1)}-create`;
|
||||
if (opts.table === "observations") {
|
||||
// @ts-ignore - If it's an observation we now that `type` is a string
|
||||
eventType = `${record["type"].toLowerCase()}-create`;
|
||||
}
|
||||
s3Client.uploadJson(
|
||||
`${env.LANGFUSE_S3_EVENT_UPLOAD_PREFIX}${record.project_id}/${getClickhouseEntityType(eventType)}/${record.id}/${randomUUID()}.json`,
|
||||
[
|
||||
{
|
||||
id: randomUUID(),
|
||||
timestamp: new Date().toISOString(),
|
||||
type: eventType,
|
||||
body: opts.eventBodyMapper(record),
|
||||
},
|
||||
],
|
||||
);
|
||||
}),
|
||||
);
|
||||
}
|
||||
const s3Client = getS3StorageServiceClient(
|
||||
env.LANGFUSE_S3_EVENT_UPLOAD_BUCKET,
|
||||
);
|
||||
await Promise.all(
|
||||
opts.records.map((record) => {
|
||||
// drop trailing s and pretend it's always a create.
|
||||
// Only applicable to scores and traces.
|
||||
let eventType = `${opts.table.slice(0, -1)}-create`;
|
||||
if (opts.table === "observations") {
|
||||
// @ts-ignore - If it's an observation we now that `type` is a string
|
||||
eventType = `${record["type"].toLowerCase()}-create`;
|
||||
}
|
||||
s3Client.uploadJson(
|
||||
`${env.LANGFUSE_S3_EVENT_UPLOAD_PREFIX}${record.project_id}/${getClickhouseEntityType(eventType)}/${record.id}/${randomUUID()}.json`,
|
||||
[
|
||||
{
|
||||
id: randomUUID(),
|
||||
timestamp: new Date().toISOString(),
|
||||
type: eventType,
|
||||
body: opts.eventBodyMapper(record),
|
||||
},
|
||||
],
|
||||
);
|
||||
}),
|
||||
);
|
||||
|
||||
const res = await clickhouseClient().insert({
|
||||
table: opts.table,
|
||||
|
||||
@@ -7,3 +7,4 @@ export * from "./traces_converters";
|
||||
export * from "./scores_converters";
|
||||
export * from "./observations_converters";
|
||||
export * from "./clickhouse";
|
||||
export * from "./constants";
|
||||
|
||||
@@ -389,7 +389,7 @@ export type ObservationsTableQueryResult = ObservationRecordReadType & {
|
||||
export const getObservationsTableCount = async (opts: ObservationTableQuery) =>
|
||||
getObservationsTableInternal<TableCount>({
|
||||
...opts,
|
||||
select: "count(*) as count",
|
||||
select: "count",
|
||||
});
|
||||
|
||||
export type ObservationsTableRow = Omit<
|
||||
@@ -407,29 +407,7 @@ export const getObservationsTable = async (
|
||||
>
|
||||
>({
|
||||
...opts,
|
||||
select: `
|
||||
o.id as id,
|
||||
o.name as name,
|
||||
o."model_parameters" as model_parameters,
|
||||
o.start_time as "start_time",
|
||||
o.end_time as "end_time",
|
||||
o.trace_id as "trace_id",
|
||||
o.completion_start_time as "completion_start_time",
|
||||
o.provided_usage_details as "provided_usage_details",
|
||||
o.usage_details as "usage_details",
|
||||
o.provided_cost_details as "provided_cost_details",
|
||||
o.cost_details as "cost_details",
|
||||
o.level as level,
|
||||
o.status_message as "status_message",
|
||||
o.version as version,
|
||||
o.parent_observation_id as "parent_observation_id",
|
||||
o.created_at as "created_at",
|
||||
o.updated_at as "updated_at",
|
||||
o.provided_model_name as "provided_model_name",
|
||||
o.total_cost as "total_cost",
|
||||
internal_model_id as "internal_model_id",
|
||||
if(isNull(end_time), NULL, date_diff('milliseconds', start_time, end_time)) as latency,
|
||||
if(isNull(completion_start_time), NULL, date_diff('milliseconds', start_time, completion_start_time)) as "time_to_first_token"`,
|
||||
select: "rows",
|
||||
});
|
||||
|
||||
const traces = await getTracesByIds(
|
||||
@@ -464,29 +442,7 @@ export const getObservationsTableWithModelData = async (
|
||||
>
|
||||
>({
|
||||
...opts,
|
||||
select: `
|
||||
o.id as id,
|
||||
o.name as name,
|
||||
o."model_parameters" as model_parameters,
|
||||
o.start_time as "start_time",
|
||||
o.end_time as "end_time",
|
||||
o.trace_id as "trace_id",
|
||||
o.completion_start_time as "completion_start_time",
|
||||
o.provided_usage_details as "provided_usage_details",
|
||||
o.usage_details as "usage_details",
|
||||
o.provided_cost_details as "provided_cost_details",
|
||||
o.cost_details as "cost_details",
|
||||
o.level as level,
|
||||
o.status_message as "status_message",
|
||||
o.version as version,
|
||||
o.parent_observation_id as "parent_observation_id",
|
||||
o.created_at as "created_at",
|
||||
o.updated_at as "updated_at",
|
||||
o.provided_model_name as "provided_model_name",
|
||||
o.total_cost as "total_cost",
|
||||
internal_model_id as "internal_model_id",
|
||||
if(isNull(end_time), NULL, date_diff('milliseconds', start_time, end_time)) as latency,
|
||||
if(isNull(completion_start_time), NULL, date_diff('milliseconds', start_time, completion_start_time)) as "time_to_first_token"`,
|
||||
select: "rows",
|
||||
});
|
||||
|
||||
const uniqueModels: string[] = Array.from(
|
||||
@@ -543,17 +499,44 @@ export const getObservationsTableWithModelData = async (
|
||||
};
|
||||
|
||||
const getObservationsTableInternal = async <T>(
|
||||
opts: ObservationTableQuery & { select: string },
|
||||
opts: ObservationTableQuery & { select: "count" | "rows" },
|
||||
): Promise<Array<T>> => {
|
||||
const select =
|
||||
opts.select === "count"
|
||||
? "count(*) as count"
|
||||
: `
|
||||
o.id as id,
|
||||
o.name as name,
|
||||
o."model_parameters" as model_parameters,
|
||||
o.start_time as "start_time",
|
||||
o.end_time as "end_time",
|
||||
o.trace_id as "trace_id",
|
||||
o.completion_start_time as "completion_start_time",
|
||||
o.provided_usage_details as "provided_usage_details",
|
||||
o.usage_details as "usage_details",
|
||||
o.provided_cost_details as "provided_cost_details",
|
||||
o.cost_details as "cost_details",
|
||||
o.level as level,
|
||||
o.status_message as "status_message",
|
||||
o.version as version,
|
||||
o.parent_observation_id as "parent_observation_id",
|
||||
o.created_at as "created_at",
|
||||
o.updated_at as "updated_at",
|
||||
o.provided_model_name as "provided_model_name",
|
||||
o.total_cost as "total_cost",
|
||||
internal_model_id as "internal_model_id",
|
||||
if(isNull(end_time), NULL, date_diff('milliseconds', start_time, end_time)) as latency,
|
||||
if(isNull(completion_start_time), NULL, date_diff('milliseconds', start_time, completion_start_time)) as "time_to_first_token"`;
|
||||
|
||||
const { projectId, filter, selectIOAndMetadata, limit, offset, orderBy } =
|
||||
opts;
|
||||
|
||||
const selectString = selectIOAndMetadata
|
||||
? `
|
||||
${opts.select},
|
||||
${select},
|
||||
${selectIOAndMetadata ? `o.input, o.output, o.metadata` : ""}
|
||||
`
|
||||
: opts.select;
|
||||
: select;
|
||||
|
||||
const scoresFilter = new FilterList([
|
||||
new StringFilter({
|
||||
@@ -580,6 +563,10 @@ const getObservationsTableInternal = async <T>(
|
||||
.includes(f.column),
|
||||
);
|
||||
|
||||
const hasScoresFilter = filter.some(
|
||||
(f) => f.column === "Scores" || f.column === "scores",
|
||||
);
|
||||
|
||||
const orderByTraces = opts.orderBy
|
||||
? observationsTableTraceUiColumnDefinitions
|
||||
.map((c) => c.uiTableId)
|
||||
@@ -650,63 +637,58 @@ const getObservationsTableInternal = async <T>(
|
||||
observation_id
|
||||
)`;
|
||||
|
||||
if (traceTableFilter.length > 0 || orderByTraces) {
|
||||
// joins with traces are very expensive. We need to filter by time as well.
|
||||
// We assume that a trace has to have been within the last 2 days to be relevant.
|
||||
// if we have default ordering by time, we order by toDate(o.start_time) first and then by
|
||||
// o.start_time. This way, clickhouse is able to read more efficiently directly from disk without ordering
|
||||
const newDefaultOrder =
|
||||
orderBy?.column === "startTime"
|
||||
? [{ column: "order_by_date", order: orderBy.order }, orderBy]
|
||||
: [orderBy ?? null];
|
||||
|
||||
const query = `
|
||||
const chOrderBy = orderByToClickhouseSql(newDefaultOrder, [
|
||||
...observationsTableUiColumnDefinitions,
|
||||
{
|
||||
uiTableName: "order_by_date",
|
||||
uiTableId: "order_by_date",
|
||||
clickhouseTableName: "observation",
|
||||
clickhouseSelect: "toDate(o.start_time)",
|
||||
},
|
||||
]);
|
||||
|
||||
// joins with traces are very expensive. We need to filter by time as well.
|
||||
// We assume that a trace has to have been within the last 2 days to be relevant.
|
||||
|
||||
const query = `
|
||||
${scoresCte}
|
||||
SELECT
|
||||
${selectString}
|
||||
FROM observations o FINAL
|
||||
LEFT JOIN traces t FINAL ON t.id = o.trace_id AND t.project_id = o.project_id
|
||||
LEFT JOIN scores_avg AS s_avg ON s_avg.trace_id = o.trace_id and s_avg.observation_id = o.id
|
||||
FROM observations o
|
||||
${traceTableFilter.length > 0 || orderByTraces || search.query ? "LEFT JOIN traces t FINAL ON t.id = o.trace_id AND t.project_id = o.project_id" : ""}
|
||||
${hasScoresFilter ? `LEFT JOIN scores_avg AS s_avg ON s_avg.trace_id = o.trace_id and s_avg.observation_id = o.id` : ""}
|
||||
WHERE ${appliedObservationsFilter.query}
|
||||
AND o.type = 'GENERATION'
|
||||
${timeFilter ? `AND t.timestamp > {tracesTimestampFilter: DateTime64(3)} - ${OBSERVATIONS_TO_TRACE_INTERVAL}` : ""}
|
||||
${timeFilter && (traceTableFilter.length > 0 || orderByTraces) ? `AND t.timestamp > {tracesTimestampFilter: DateTime64(3)} - ${OBSERVATIONS_TO_TRACE_INTERVAL}` : ""}
|
||||
${search.query}
|
||||
${orderByToClickhouseSql(orderBy ?? null, observationsTableUiColumnDefinitions)}
|
||||
${chOrderBy}
|
||||
${opts.select === "rows" ? "LIMIT 1 BY o.id, o.project_id" : ""}
|
||||
${limit !== undefined && offset !== undefined ? `LIMIT ${limit} OFFSET ${offset}` : ""};`;
|
||||
|
||||
const res = await queryClickhouse<T>({
|
||||
query,
|
||||
params: {
|
||||
...appliedScoresFilter.params,
|
||||
...appliedObservationsFilter.params,
|
||||
...(timeFilter
|
||||
? {
|
||||
tracesTimestampFilter: convertDateToClickhouseDateTime(
|
||||
timeFilter.value as Date,
|
||||
),
|
||||
}
|
||||
: {}),
|
||||
...search.params,
|
||||
},
|
||||
});
|
||||
const res = await queryClickhouse<T>({
|
||||
query,
|
||||
params: {
|
||||
...appliedScoresFilter.params,
|
||||
...appliedObservationsFilter.params,
|
||||
...(timeFilter
|
||||
? {
|
||||
tracesTimestampFilter: convertDateToClickhouseDateTime(
|
||||
timeFilter.value as Date,
|
||||
),
|
||||
}
|
||||
: {}),
|
||||
...search.params,
|
||||
},
|
||||
});
|
||||
|
||||
return res;
|
||||
} else {
|
||||
// we query by T, which could also be {count: string}.
|
||||
const query = `
|
||||
${scoresCte}
|
||||
SELECT
|
||||
${selectString}
|
||||
FROM observations o FINAL
|
||||
LEFT JOIN scores_avg AS s_avg ON s_avg.trace_id = o.trace_id and s_avg.observation_id = o.id
|
||||
WHERE ${appliedObservationsFilter.query}
|
||||
${orderByToClickhouseSql(orderBy ?? null, observationsTableUiColumnDefinitions)}
|
||||
${limit !== undefined && offset !== undefined ? `LIMIT ${limit} OFFSET ${offset}` : ""};`;
|
||||
|
||||
const res = await queryClickhouse<T>({
|
||||
query,
|
||||
params: {
|
||||
...appliedScoresFilter.params,
|
||||
...appliedObservationsFilter.params,
|
||||
},
|
||||
});
|
||||
|
||||
return res;
|
||||
}
|
||||
return res;
|
||||
};
|
||||
|
||||
export const getObservationsGroupedByModel = async (
|
||||
@@ -1069,3 +1051,60 @@ export const getLatencyAndTotalCostForObservationsByTraces = async (
|
||||
latency: Number(r.latency_ms) / 1000,
|
||||
}));
|
||||
};
|
||||
|
||||
export const getObservationCountsByProjectInCreationInterval = async ({
|
||||
start,
|
||||
end,
|
||||
}: {
|
||||
start: Date;
|
||||
end: Date;
|
||||
}) => {
|
||||
const query = `
|
||||
SELECT
|
||||
project_id,
|
||||
count(*) as count
|
||||
FROM observations
|
||||
WHERE created_at >= {start: DateTime64(3)}
|
||||
AND created_at < {end: DateTime64(3)}
|
||||
GROUP BY project_id
|
||||
`;
|
||||
|
||||
const rows = await queryClickhouse<{ project_id: string; count: string }>({
|
||||
query,
|
||||
params: {
|
||||
start: convertDateToClickhouseDateTime(start),
|
||||
end: convertDateToClickhouseDateTime(end),
|
||||
},
|
||||
});
|
||||
|
||||
return rows.map((row) => ({
|
||||
projectId: row.project_id,
|
||||
count: Number(row.count),
|
||||
}));
|
||||
};
|
||||
|
||||
export const getObservationCountOfProjectsSinceCreationDate = async ({
|
||||
projectIds,
|
||||
start,
|
||||
}: {
|
||||
projectIds: string[];
|
||||
start: Date;
|
||||
}) => {
|
||||
const query = `
|
||||
SELECT
|
||||
count(*) as count
|
||||
FROM observations
|
||||
WHERE project_id IN ({projectIds: Array(String)})
|
||||
AND created_at >= {start: DateTime64(3)}
|
||||
`;
|
||||
|
||||
const rows = await queryClickhouse<{ count: string }>({
|
||||
query,
|
||||
params: {
|
||||
projectIds,
|
||||
start: convertDateToClickhouseDateTime(start),
|
||||
},
|
||||
});
|
||||
|
||||
return Number(rows[0]?.count ?? 0);
|
||||
};
|
||||
|
||||
@@ -549,3 +549,34 @@ export const getAggregatedScoresForPrompts = async (
|
||||
promptId: row.prompt_id,
|
||||
}));
|
||||
};
|
||||
|
||||
export const getScoreCountsByProjectInCreationInterval = async ({
|
||||
start,
|
||||
end,
|
||||
}: {
|
||||
start: Date;
|
||||
end: Date;
|
||||
}) => {
|
||||
const query = `
|
||||
SELECT
|
||||
project_id,
|
||||
count(*) as count
|
||||
FROM scores
|
||||
WHERE created_at >= {start: DateTime64(3)}
|
||||
AND created_at < {end: DateTime64(3)}
|
||||
GROUP BY project_id
|
||||
`;
|
||||
|
||||
const rows = await queryClickhouse<{ project_id: string; count: string }>({
|
||||
query,
|
||||
params: {
|
||||
start: convertDateToClickhouseDateTime(start),
|
||||
end: convertDateToClickhouseDateTime(end),
|
||||
},
|
||||
});
|
||||
|
||||
return rows.map((row) => ({
|
||||
projectId: row.project_id,
|
||||
count: Number(row.count),
|
||||
}));
|
||||
};
|
||||
|
||||
@@ -151,6 +151,37 @@ export const hasAnyTrace = async (projectId: string) => {
|
||||
return rows.length > 0 && Number(rows[0].count) > 0;
|
||||
};
|
||||
|
||||
export const getTraceCountsByProjectInCreationInterval = async ({
|
||||
start,
|
||||
end,
|
||||
}: {
|
||||
start: Date;
|
||||
end: Date;
|
||||
}) => {
|
||||
const query = `
|
||||
SELECT
|
||||
project_id,
|
||||
count(*) as count
|
||||
FROM traces
|
||||
WHERE created_at >= {start: DateTime64(3)}
|
||||
AND created_at < {end: DateTime64(3)}
|
||||
GROUP BY project_id
|
||||
`;
|
||||
|
||||
const rows = await queryClickhouse<{ project_id: string; count: string }>({
|
||||
query,
|
||||
params: {
|
||||
start: convertDateToClickhouseDateTime(start),
|
||||
end: convertDateToClickhouseDateTime(end),
|
||||
},
|
||||
});
|
||||
|
||||
return rows.map((row) => ({
|
||||
projectId: row.project_id,
|
||||
count: Number(row.count),
|
||||
}));
|
||||
};
|
||||
|
||||
export const getTraceById = async (
|
||||
traceId: string,
|
||||
projectId: string,
|
||||
@@ -395,7 +426,17 @@ export const getSessionsTable = async (props: {
|
||||
return rows;
|
||||
};
|
||||
|
||||
const getSessionsTableGeneric = async <T>(props: FetchTracesTableProps) => {
|
||||
export type FetchSessionsTableProps = {
|
||||
select: string;
|
||||
projectId: string;
|
||||
filter: FilterState;
|
||||
searchQuery?: string;
|
||||
orderBy?: OrderByState;
|
||||
limit?: number;
|
||||
page?: number;
|
||||
};
|
||||
|
||||
const getSessionsTableGeneric = async <T>(props: FetchSessionsTableProps) => {
|
||||
const { select, projectId, filter, orderBy, limit, page } = props;
|
||||
|
||||
const { tracesFilter, scoresFilter, observationsFilter } =
|
||||
|
||||
@@ -1,10 +1,7 @@
|
||||
import { ObservationLevel, Trace } from "@prisma/client";
|
||||
import { Trace } from "@prisma/client";
|
||||
import { parseClickhouseUTCDateTimeFormat } from "./clickhouse";
|
||||
import { TraceRecordReadType } from "./definitions";
|
||||
import Decimal from "decimal.js";
|
||||
import { ScoreAggregate } from "../../features/scores";
|
||||
import { convertDateToClickhouseDateTime } from "../clickhouse/client";
|
||||
import { TracesTableReturnType } from "../services/traces-ui-table-service";
|
||||
|
||||
export const convertTraceDomainToClickhouse = (
|
||||
trace: Trace,
|
||||
@@ -54,53 +51,3 @@ export const convertClickhouseToDomain = (
|
||||
externalId: null,
|
||||
};
|
||||
};
|
||||
|
||||
export type TracesAllReturnType = {
|
||||
id: string;
|
||||
timestamp: Date;
|
||||
name: string | null;
|
||||
projectId: string;
|
||||
userId: string | null;
|
||||
release: string | null;
|
||||
version: string | null;
|
||||
public: boolean;
|
||||
bookmarked: boolean;
|
||||
sessionId: string | null;
|
||||
tags: string[];
|
||||
};
|
||||
|
||||
export const convertToDomain = (row: TracesTableReturnType) => {
|
||||
return {
|
||||
id: row.id,
|
||||
projectId: row.project_id,
|
||||
timestamp: parseClickhouseUTCDateTimeFormat(row.timestamp),
|
||||
tags: row.tags,
|
||||
bookmarked: row.bookmarked,
|
||||
name: row.name ?? null,
|
||||
release: row.release ?? null,
|
||||
version: row.version ?? null,
|
||||
userId: row.user_id ?? null,
|
||||
sessionId: row.session_id ?? null,
|
||||
latency: Number(row.latency),
|
||||
usageDetails: row.usage_details,
|
||||
costDetails: row.cost_details,
|
||||
level: row.level,
|
||||
observationCount: Number(row.observation_count),
|
||||
scoresAvg: row.scores_avg,
|
||||
public: row.public,
|
||||
};
|
||||
};
|
||||
|
||||
export type TracesMetricsReturnType = {
|
||||
id: string;
|
||||
promptTokens: bigint;
|
||||
completionTokens: bigint;
|
||||
totalTokens: bigint;
|
||||
latency: number | null;
|
||||
level: ObservationLevel;
|
||||
observationCount: bigint;
|
||||
calculatedTotalCost: Decimal | null;
|
||||
calculatedInputCost: Decimal | null;
|
||||
calculatedOutputCost: Decimal | null;
|
||||
scores: ScoreAggregate;
|
||||
};
|
||||
|
||||
@@ -13,9 +13,17 @@ import {
|
||||
} from "../queries/clickhouse-sql/factory";
|
||||
import { orderByToClickhouseSql } from "../queries/clickhouse-sql/orderby-factory";
|
||||
import { clickhouseSearchCondition } from "../queries/clickhouse-sql/search";
|
||||
import { convertToDomain } from "../repositories";
|
||||
import { queryClickhouse } from "../repositories/clickhouse";
|
||||
import {
|
||||
parseClickhouseUTCDateTimeFormat,
|
||||
queryClickhouse,
|
||||
} from "../repositories/clickhouse";
|
||||
import { TraceRecordReadType } from "../repositories/definitions";
|
||||
import {
|
||||
OBSERVATIONS_TO_TRACE_INTERVAL,
|
||||
SCORE_TO_TRACE_OBSERVATIONS_INTERVAL,
|
||||
} from "../repositories/constants";
|
||||
import Decimal from "decimal.js";
|
||||
import { ScoreAggregate } from "../../features/scores";
|
||||
|
||||
export type TracesTableReturnType = Pick<
|
||||
TraceRecordReadType,
|
||||
@@ -30,7 +38,83 @@ export type TracesTableReturnType = Pick<
|
||||
| "session_id"
|
||||
| "tags"
|
||||
| "public"
|
||||
> & {
|
||||
>;
|
||||
|
||||
export type TracesAllUiReturnType = {
|
||||
id: string;
|
||||
timestamp: Date;
|
||||
name: string | null;
|
||||
projectId: string;
|
||||
userId: string | null;
|
||||
release: string | null;
|
||||
version: string | null;
|
||||
public: boolean;
|
||||
bookmarked: boolean;
|
||||
sessionId: string | null;
|
||||
tags: string[];
|
||||
};
|
||||
|
||||
export type TracesMetricsUiReturnType = {
|
||||
id: string;
|
||||
projectId: string;
|
||||
promptTokens: bigint;
|
||||
completionTokens: bigint;
|
||||
totalTokens: bigint;
|
||||
latency: number | null;
|
||||
level: ObservationLevel;
|
||||
observationCount: bigint;
|
||||
calculatedTotalCost: Decimal | null;
|
||||
calculatedInputCost: Decimal | null;
|
||||
calculatedOutputCost: Decimal | null;
|
||||
scores: ScoreAggregate;
|
||||
};
|
||||
|
||||
export const convertToUiTableRows = (
|
||||
row: TracesTableReturnType,
|
||||
): TracesAllUiReturnType => {
|
||||
return {
|
||||
id: row.id,
|
||||
projectId: row.project_id,
|
||||
timestamp: parseClickhouseUTCDateTimeFormat(row.timestamp),
|
||||
tags: row.tags,
|
||||
bookmarked: row.bookmarked,
|
||||
name: row.name ?? null,
|
||||
release: row.release ?? null,
|
||||
version: row.version ?? null,
|
||||
userId: row.user_id ?? null,
|
||||
sessionId: row.session_id ?? null,
|
||||
public: row.public,
|
||||
};
|
||||
};
|
||||
|
||||
export const convertToUITableMetrics = (
|
||||
row: TracesTableMetricsClickhouseReturnType,
|
||||
): Omit<TracesMetricsUiReturnType, "scores"> => {
|
||||
return {
|
||||
id: row.id,
|
||||
projectId: row.project_id,
|
||||
latency: Number(row.latency),
|
||||
promptTokens: BigInt(row.usage_details?.input ?? 0),
|
||||
completionTokens: BigInt(row.usage_details?.output ?? 0),
|
||||
totalTokens: BigInt(row.usage_details?.total ?? 0),
|
||||
observationCount: BigInt(row.observation_count ?? 0),
|
||||
calculatedTotalCost: row.cost_details?.total
|
||||
? new Decimal(row.cost_details.total)
|
||||
: null,
|
||||
calculatedInputCost: row.cost_details?.input
|
||||
? new Decimal(row.cost_details.input)
|
||||
: null,
|
||||
calculatedOutputCost: row.cost_details?.output
|
||||
? new Decimal(row.cost_details.output)
|
||||
: null,
|
||||
level: row.level,
|
||||
};
|
||||
};
|
||||
|
||||
export type TracesTableMetricsClickhouseReturnType = {
|
||||
id: string;
|
||||
project_id: string;
|
||||
timestamp: Date;
|
||||
level: ObservationLevel;
|
||||
observation_count: number | null;
|
||||
latency: string | null;
|
||||
@@ -40,7 +124,7 @@ export type TracesTableReturnType = Pick<
|
||||
};
|
||||
|
||||
export type FetchTracesTableProps = {
|
||||
select: string;
|
||||
select: "count" | "rows" | "metrics";
|
||||
projectId: string;
|
||||
filter: FilterState;
|
||||
searchQuery?: string;
|
||||
@@ -58,7 +142,7 @@ export const getTracesTableCount = async (props: {
|
||||
page?: number;
|
||||
}) => {
|
||||
const countRows = await getTracesTableGeneric<{ count: string }>({
|
||||
select: "count(*) as count",
|
||||
select: "count",
|
||||
...props,
|
||||
});
|
||||
|
||||
@@ -69,6 +153,23 @@ export const getTracesTableCount = async (props: {
|
||||
return converted.length > 0 ? converted[0].count : 0;
|
||||
};
|
||||
|
||||
export const getTracesTableMetrics = async (props: {
|
||||
projectId: string;
|
||||
filter: FilterState;
|
||||
searchQuery?: string;
|
||||
orderBy?: OrderByState;
|
||||
limit?: number;
|
||||
page?: number;
|
||||
}): Promise<Array<Omit<TracesMetricsUiReturnType, "scores">>> => {
|
||||
const countRows =
|
||||
await getTracesTableGeneric<TracesTableMetricsClickhouseReturnType>({
|
||||
select: "metrics",
|
||||
...props,
|
||||
});
|
||||
|
||||
return countRows.map(convertToUITableMetrics);
|
||||
};
|
||||
|
||||
export const getTracesTable = async (
|
||||
projectId: string,
|
||||
filter: FilterState,
|
||||
@@ -78,24 +179,7 @@ export const getTracesTable = async (
|
||||
page?: number,
|
||||
) => {
|
||||
const rows = await getTracesTableGeneric<TracesTableReturnType>({
|
||||
select: `
|
||||
t.id,
|
||||
t.project_id as project_id,
|
||||
t.timestamp,
|
||||
t.tags,
|
||||
t.bookmarked,
|
||||
t.name,
|
||||
t.release,
|
||||
t.version,
|
||||
t.user_id,
|
||||
t.session_id,
|
||||
os.latency_milliseconds / 1000 as latency,
|
||||
os.cost_details as cost_details,
|
||||
os.usage_details as usage_details,
|
||||
os.level as level,
|
||||
os.observation_count as observation_count,
|
||||
s.scores_avg as scores_avg,
|
||||
t.public`,
|
||||
select: "rows",
|
||||
projectId,
|
||||
filter,
|
||||
searchQuery,
|
||||
@@ -104,13 +188,50 @@ export const getTracesTable = async (
|
||||
page,
|
||||
});
|
||||
|
||||
return rows.map(convertToDomain);
|
||||
return rows.map(convertToUiTableRows);
|
||||
};
|
||||
|
||||
const getTracesTableGeneric = async <T>(props: FetchTracesTableProps) => {
|
||||
const { select, projectId, filter, orderBy, limit, page, searchQuery } =
|
||||
props;
|
||||
|
||||
let sqlSelect: string;
|
||||
switch (select) {
|
||||
case "count":
|
||||
sqlSelect = "count(*) as count";
|
||||
break;
|
||||
case "metrics":
|
||||
sqlSelect = `
|
||||
t.id as id,
|
||||
t.project_id as project_id,
|
||||
t.timestamp as timestamp,
|
||||
os.latency_milliseconds / 1000 as latency,
|
||||
os.cost_details as cost_details,
|
||||
os.usage_details as usage_details,
|
||||
os.level as level,
|
||||
os.observation_count as observation_count,
|
||||
s.scores_avg as scores_avg,
|
||||
t.public as public`;
|
||||
break;
|
||||
case "rows":
|
||||
sqlSelect = `
|
||||
t.id as id,
|
||||
t.project_id as project_id,
|
||||
t.timestamp as timestamp,
|
||||
t.tags as tags,
|
||||
t.bookmarked as bookmarked,
|
||||
t.name as name,
|
||||
t.release as release,
|
||||
t.version as version,
|
||||
t.user_id as user_id,
|
||||
t.session_id as session_id,
|
||||
t.public as public`;
|
||||
break;
|
||||
default:
|
||||
const exhaustiveCheckDefault: never = select;
|
||||
throw new Error(`Unknown select type: ${select}`);
|
||||
}
|
||||
|
||||
const { tracesFilter, scoresFilter, observationsFilter } =
|
||||
getProjectIdDefaultFilter(projectId, { tracesPrefix: "t" });
|
||||
|
||||
@@ -156,27 +277,20 @@ const getTracesTableGeneric = async <T>(props: FetchTracesTableProps) => {
|
||||
f.field === "timestamp" && (f.operator === ">=" || f.operator === ">"),
|
||||
) as DateTimeFilter | undefined;
|
||||
|
||||
timeStampFilter
|
||||
? scoresFilter.push(
|
||||
new DateTimeFilter({
|
||||
clickhouseTable: "scores",
|
||||
field: "timestamp",
|
||||
operator: ">=",
|
||||
value: timeStampFilter.value,
|
||||
}),
|
||||
)
|
||||
: null;
|
||||
const requiresScoresJoin =
|
||||
tracesFilter.find((f) => f.clickhouseTable === "scores") !== undefined ||
|
||||
tracesTableUiColumnDefinitions.find(
|
||||
(c) =>
|
||||
c.uiTableName === orderBy?.column || c.uiTableId === orderBy?.column,
|
||||
)?.clickhouseTableName === "scores";
|
||||
|
||||
timeStampFilter
|
||||
? observationsFilter.push(
|
||||
new DateTimeFilter({
|
||||
clickhouseTable: "observations",
|
||||
field: "start_time",
|
||||
operator: ">=",
|
||||
value: timeStampFilter.value,
|
||||
}),
|
||||
)
|
||||
: null;
|
||||
const requiresObservationsJoin =
|
||||
tracesFilter.find((f) => f.clickhouseTable === "observations") !==
|
||||
undefined ||
|
||||
tracesTableUiColumnDefinitions.find(
|
||||
(c) =>
|
||||
c.uiTableName === orderBy?.column || c.uiTableId === orderBy?.column,
|
||||
)?.clickhouseTableName === "observations";
|
||||
|
||||
const tracesFilterRes = tracesFilter.apply();
|
||||
const scoresFilterRes = scoresFilter.apply();
|
||||
@@ -184,6 +298,36 @@ const getTracesTableGeneric = async <T>(props: FetchTracesTableProps) => {
|
||||
|
||||
const search = clickhouseSearchCondition(searchQuery);
|
||||
|
||||
const defaultOrder = orderBy?.order && orderBy?.column === "timestamp";
|
||||
const orderByCols = [
|
||||
...tracesTableUiColumnDefinitions,
|
||||
{
|
||||
clickhouseSelect: "toDate(t.timestamp)",
|
||||
uiTableName: "timestamp_to_date",
|
||||
uiTableId: "timestamp_to_date",
|
||||
clickhouseTableName: "observations",
|
||||
},
|
||||
];
|
||||
const chOrderBy = orderByToClickhouseSql(
|
||||
[
|
||||
defaultOrder
|
||||
? {
|
||||
column: "timestamp_to_date",
|
||||
order: orderBy.order,
|
||||
}
|
||||
: null,
|
||||
orderBy ?? null,
|
||||
],
|
||||
orderByCols,
|
||||
);
|
||||
|
||||
// complex query ahead:
|
||||
// - we only join scores and observations if we really need them to speed up default views
|
||||
// - we use FINAL on traces only in case we not need to order by something different than time. Otherwise we cannot guarantee correct reads.
|
||||
// - we filter the observations and scores as much as possible before joining them to traces.
|
||||
// - we order by todate(timestamp), timestamp per default and do not use FINAL.
|
||||
// In this case, CH is able to read the data only from the latest date from disk and filtering them in memory. No need to read all data e.g. for 1 month from disk.
|
||||
|
||||
const query = `
|
||||
WITH observations_stats AS (
|
||||
SELECT
|
||||
@@ -200,8 +344,10 @@ const getTracesTableGeneric = async <T>(props: FetchTracesTableProps) => {
|
||||
sumMap(cost_details) as cost_details,
|
||||
trace_id,
|
||||
project_id
|
||||
FROM observations FINAL
|
||||
WHERE ${observationFilterRes.query}
|
||||
FROM observations o FINAL
|
||||
WHERE o.project_id = {projectId: String}
|
||||
${timeStampFilter ? `AND o.start_time >= {traceTimestamp: DateTime64(3)} - ${OBSERVATIONS_TO_TRACE_INTERVAL}` : ""}
|
||||
${observationsFilter ? `AND ${observationFilterRes.query}` : ""}
|
||||
GROUP BY trace_id, project_id
|
||||
),
|
||||
scores_avg AS (
|
||||
@@ -214,21 +360,28 @@ const getTracesTableGeneric = async <T>(props: FetchTracesTableProps) => {
|
||||
trace_id,
|
||||
name,
|
||||
avg(value) avg_value
|
||||
FROM scores final
|
||||
WHERE ${scoresFilterRes.query}
|
||||
FROM scores s FINAL
|
||||
WHERE project_id = {projectId: String}
|
||||
${timeStampFilter ? `AND s.timestamp >= {traceTimestamp: DateTime64(3)} - ${SCORE_TO_TRACE_OBSERVATIONS_INTERVAL}` : ""}
|
||||
${scoresFilterRes ? `AND ${scoresFilterRes.query}` : ""}
|
||||
GROUP BY project_id,
|
||||
trace_id,
|
||||
name
|
||||
) tmp
|
||||
GROUP BY project_id, trace_id
|
||||
)
|
||||
SELECT ${select}
|
||||
FROM traces t final
|
||||
LEFT JOIN observations_stats os on os.project_id = t.project_id and os.trace_id = t.id
|
||||
LEFT JOIN scores_avg s on s.project_id = t.project_id and s.trace_id = t.id
|
||||
WHERE ${tracesFilterRes.query}
|
||||
SELECT ${sqlSelect}
|
||||
-- FINAL is used for non default ordering and count.
|
||||
FROM traces t ${["metrics", "rows"].includes(select) && defaultOrder ? "" : "FINAL"}
|
||||
${select === "metrics" || requiresObservationsJoin ? `LEFT JOIN observations_stats os on os.project_id = t.project_id and os.trace_id = t.id` : ""}
|
||||
${select === "metrics" || requiresScoresJoin ? `LEFT JOIN scores_avg s on s.project_id = t.project_id and s.trace_id = t.id` : ""}
|
||||
WHERE t.project_id = {projectId: String}
|
||||
${tracesFilterRes ? `AND ${tracesFilterRes.query}` : ""}
|
||||
${search.query}
|
||||
${orderByToClickhouseSql(orderBy ?? null, tracesTableUiColumnDefinitions)}
|
||||
${chOrderBy}
|
||||
-- This is used for metrics and row queries. Count has only one result.
|
||||
-- This is only used for default ordering. Otherwise, we use final.
|
||||
${["metrics", "rows"].includes(select) && defaultOrder ? "LIMIT 1 BY id, project_id" : ""}
|
||||
${limit !== undefined && page !== undefined ? `LIMIT {limit: Int32} OFFSET {offset: Int32}` : ""}
|
||||
`;
|
||||
|
||||
@@ -237,6 +390,8 @@ const getTracesTableGeneric = async <T>(props: FetchTracesTableProps) => {
|
||||
params: {
|
||||
limit: limit,
|
||||
offset: limit && page ? limit * page : 0,
|
||||
traceTimestamp: timeStampFilter?.value.getTime(),
|
||||
projectId: projectId,
|
||||
...tracesFilterRes.params,
|
||||
...observationFilterRes.params,
|
||||
...scoresFilterRes.params,
|
||||
|
||||
@@ -68,7 +68,7 @@ export const observationsTableUiColumnDefinitions: UiColumnMapping[] = [
|
||||
uiTableId: "timeToFirstToken",
|
||||
clickhouseTableName: "observations",
|
||||
clickhouseSelect:
|
||||
"if(isNull(completion_start_time), NULL, date_diff('seconds', start_time, completion_start_time))",
|
||||
"if(isNull(completion_start_time), NULL, date_diff('milliseconds', start_time, completion_start_time))",
|
||||
// If we use the default of Decimal64(12), we cannot filter for more than ~40min due to an overflow
|
||||
clickhouseTypeOverwrite: "Decimal64(3)",
|
||||
},
|
||||
@@ -77,7 +77,7 @@ export const observationsTableUiColumnDefinitions: UiColumnMapping[] = [
|
||||
uiTableId: "latency",
|
||||
clickhouseTableName: "observations",
|
||||
clickhouseSelect:
|
||||
"if(isNull(end_time), NULL, date_diff('seconds', start_time, end_time))",
|
||||
"if(isNull(end_time), NULL, date_diff('milliseconds', start_time, end_time))",
|
||||
// If we use the default of Decimal64(12), we cannot filter for more than ~40min due to an overflow
|
||||
clickhouseTypeOverwrite: "Decimal64(3)",
|
||||
},
|
||||
@@ -86,7 +86,7 @@ export const observationsTableUiColumnDefinitions: UiColumnMapping[] = [
|
||||
uiTableId: "tokensPerSecond",
|
||||
clickhouseTableName: "observations",
|
||||
clickhouseSelect:
|
||||
"usage_details['input'] / date_diff('seconds', start_time, end_time)",
|
||||
"(usage_details['input'] / date_diff('milliseconds', start_time, end_time))",
|
||||
},
|
||||
{
|
||||
uiTableName: "Input Cost ($)",
|
||||
|
||||
@@ -71,28 +71,28 @@ export const tracesTableUiColumnDefinitions: UiColumnMapping[] = [
|
||||
{
|
||||
uiTableName: "Input Tokens",
|
||||
uiTableId: "inputTokens",
|
||||
clickhouseTableName: "traces",
|
||||
clickhouseTableName: "observations",
|
||||
clickhouseSelect:
|
||||
"if(mapExists((k, v) -> (k = 'input'), usage_details), usage_details['input'], NULL)",
|
||||
},
|
||||
{
|
||||
uiTableName: "Output Tokens",
|
||||
uiTableId: "outputTokens",
|
||||
clickhouseTableName: "traces",
|
||||
clickhouseTableName: "observations",
|
||||
clickhouseSelect:
|
||||
"if(mapExists((k, v) -> (k = 'output'), usage_details), usage_details['output'], NULL)",
|
||||
},
|
||||
{
|
||||
uiTableName: "Total Tokens",
|
||||
uiTableId: "totalTokens",
|
||||
clickhouseTableName: "traces",
|
||||
clickhouseTableName: "observations",
|
||||
clickhouseSelect:
|
||||
"if(mapExists((k, v) -> (k = 'total'), usage_details), usage_details['total'], NULL)",
|
||||
},
|
||||
{
|
||||
uiTableName: "Usage",
|
||||
uiTableId: "usage",
|
||||
clickhouseTableName: "traces",
|
||||
clickhouseTableName: "observations",
|
||||
clickhouseSelect:
|
||||
"if(mapExists((k, v) -> (k = 'total'), usage_details), usage_details['total'], NULL)",
|
||||
},
|
||||
@@ -105,7 +105,7 @@ export const tracesTableUiColumnDefinitions: UiColumnMapping[] = [
|
||||
{
|
||||
uiTableName: "Latency (s)",
|
||||
uiTableId: "latency",
|
||||
clickhouseTableName: "traces",
|
||||
clickhouseTableName: "observations",
|
||||
clickhouseSelect: "latency_milliseconds / 1000",
|
||||
// If we use the default of Decimal64(12), we cannot filter for more than ~40min due to an overflow
|
||||
clickhouseTypeOverwrite: "Decimal64(3)",
|
||||
@@ -113,19 +113,19 @@ export const tracesTableUiColumnDefinitions: UiColumnMapping[] = [
|
||||
{
|
||||
uiTableName: "Input Cost ($)",
|
||||
uiTableId: "inputCost",
|
||||
clickhouseTableName: "traces",
|
||||
clickhouseTableName: "observations",
|
||||
clickhouseSelect: "cost_details['input']",
|
||||
},
|
||||
{
|
||||
uiTableName: "Output Cost ($)",
|
||||
uiTableId: "outputCost",
|
||||
clickhouseTableName: "traces",
|
||||
clickhouseTableName: "observations",
|
||||
clickhouseSelect: "cost_details['output']",
|
||||
},
|
||||
{
|
||||
uiTableName: "Total Cost ($)",
|
||||
uiTableId: "totalCost",
|
||||
clickhouseTableName: "traces",
|
||||
clickhouseTableName: "observations",
|
||||
clickhouseSelect: "cost_details['total']",
|
||||
},
|
||||
];
|
||||
|
||||
@@ -26,7 +26,7 @@ export const jsonSchemaNullable: z.ZodType<JsonNested> = z.lazy(() =>
|
||||
nestedLiteralSchema,
|
||||
z.array(jsonSchemaNullable),
|
||||
z.record(jsonSchemaNullable),
|
||||
])
|
||||
]),
|
||||
);
|
||||
|
||||
// Root schema that does not allow nulls at the root level
|
||||
@@ -35,17 +35,17 @@ export const jsonSchema: z.ZodType<Json> = z.lazy(() =>
|
||||
rootLiteralSchema,
|
||||
z.array(jsonSchemaNullable),
|
||||
z.record(jsonSchemaNullable),
|
||||
])
|
||||
]),
|
||||
);
|
||||
|
||||
export const paginationZod = {
|
||||
page: z.preprocess(
|
||||
(x) => (x === "" ? undefined : x),
|
||||
z.coerce.number().default(1)
|
||||
z.coerce.number().default(1),
|
||||
),
|
||||
limit: z.preprocess(
|
||||
(x) => (x === "" ? undefined : x),
|
||||
z.coerce.number().lte(100).default(50)
|
||||
z.coerce.number().lte(100).default(50),
|
||||
),
|
||||
};
|
||||
|
||||
@@ -69,8 +69,11 @@ export const paginationMetaResponseZod = z.object({
|
||||
totalPages: z.number().int().nonnegative(),
|
||||
});
|
||||
|
||||
export const noHtmlRegex = /<[^>]*>/;
|
||||
export const noHtmlCheck = (value: string) => !noHtmlRegex.test(value);
|
||||
export const htmlRegex = /<[^>]*>/;
|
||||
export const noHtmlCheck = (value: string) => !htmlRegex.test(value);
|
||||
|
||||
const urlRegex = /https?:\/\/[^\s/$.?#].[^\s]*/i;
|
||||
export const noUrlCheck = (value: string) => !urlRegex.test(value);
|
||||
|
||||
export const NonEmptyString = z.string().min(1);
|
||||
|
||||
@@ -83,7 +86,7 @@ export const NonEmptyString = z.string().min(1);
|
||||
*/
|
||||
export const validateZodSchema = <T extends z.ZodTypeAny>(
|
||||
schema: T,
|
||||
object: z.infer<T>
|
||||
object: z.infer<T>,
|
||||
): z.infer<T> => {
|
||||
return schema.parse(object);
|
||||
};
|
||||
|
||||
Generated
+227
-59
@@ -152,6 +152,9 @@ importers:
|
||||
'@langchain/core':
|
||||
specifier: ^0.3.18
|
||||
version: 0.3.18(openai@4.72.0(zod@3.23.8))
|
||||
'@langchain/google-vertexai':
|
||||
specifier: ^0.1.3
|
||||
version: 0.1.3(@langchain/core@0.3.18(openai@4.72.0(zod@3.23.8)))(zod@3.23.8)
|
||||
'@langchain/openai':
|
||||
specifier: ^0.3.14
|
||||
version: 0.3.14(@langchain/core@0.3.18(openai@4.72.0(zod@3.23.8)))
|
||||
@@ -199,10 +202,10 @@ 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(@aws-sdk/client-sso-oidc@3.668.0(@aws-sdk/client-sts@3.668.0))(@aws-sdk/client-sts@3.668.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)))(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-sso-oidc@3.668.0(@aws-sdk/client-sts@3.668.0))(@aws-sdk/client-sts@3.668.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))
|
||||
langfuse-langchain:
|
||||
specifier: 3.30.3
|
||||
version: 3.30.3(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-sso-oidc@3.668.0(@aws-sdk/client-sts@3.668.0))(@aws-sdk/client-sts@3.668.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)))(axios@1.7.7)(handlebars@4.7.8)(openai@4.72.0(zod@3.23.8)))
|
||||
version: 3.30.3(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-sso-oidc@3.668.0(@aws-sdk/client-sts@3.668.0))(@aws-sdk/client-sts@3.668.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
|
||||
@@ -578,13 +581,13 @@ 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(@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)))(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
|
||||
lucide-react:
|
||||
specifier: ^0.447.0
|
||||
version: 0.447.0(react@18.2.0)
|
||||
specifier: ^0.462.0
|
||||
version: 0.462.0(react@18.2.0)
|
||||
next:
|
||||
specifier: ^14.2.15
|
||||
version: 14.2.15(@babel/core@7.24.3)(@opentelemetry/api@1.9.0)(@playwright/test@1.47.2)(babel-plugin-macros@3.1.0)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
|
||||
@@ -601,8 +604,8 @@ importers:
|
||||
specifier: ^1.176.0
|
||||
version: 1.176.0
|
||||
posthog-node:
|
||||
specifier: ^3.6.3
|
||||
version: 3.6.3
|
||||
specifier: ^4.3.1
|
||||
version: 4.3.1
|
||||
prexit:
|
||||
specifier: ^2.2.0
|
||||
version: 2.2.0
|
||||
@@ -652,8 +655,8 @@ importers:
|
||||
specifier: ^1.4.41
|
||||
version: 1.4.41(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
|
||||
stripe:
|
||||
specifier: ^16.8.0
|
||||
version: 16.8.0
|
||||
specifier: ^17.4.0
|
||||
version: 17.4.0
|
||||
superjson:
|
||||
specifier: 2.2.1
|
||||
version: 2.2.1
|
||||
@@ -885,8 +888,8 @@ importers:
|
||||
specifier: ^8.13.0
|
||||
version: 8.13.0
|
||||
stripe:
|
||||
specifier: ^16.8.0
|
||||
version: 16.8.0
|
||||
specifier: ^17.4.0
|
||||
version: 17.4.0
|
||||
tiktoken:
|
||||
specifier: ^1.0.15
|
||||
version: 1.0.15
|
||||
@@ -2391,6 +2394,24 @@ packages:
|
||||
resolution: {integrity: sha512-IEZCrFs1Xd0J2FTH1D3Lnm3/Yk2r8LSpwDeLYwcCom3rNAK5k4mKQ2rwIpNq3YuqBdrTNMKRO+PopjkP1SB17A==}
|
||||
engines: {node: '>=18'}
|
||||
|
||||
'@langchain/google-common@0.1.3':
|
||||
resolution: {integrity: sha512-yry0taVC5AUwn55Gc6PTQX2XHLfbh6PAOheatkB77u22bYzLNaKG+DGQR8CRVRS9oZEXuNpXyAV7X2+2wv1w6Q==}
|
||||
engines: {node: '>=18'}
|
||||
peerDependencies:
|
||||
'@langchain/core': '>=0.2.21 <0.4.0'
|
||||
|
||||
'@langchain/google-gauth@0.1.3':
|
||||
resolution: {integrity: sha512-/FfF/5KNP78gUHLMhyqI9kMlYy7v+uyRddjx0lsiTOGWTZeFhhemmYw4Vbb8ltUSgfyx7/c9M8GryxY9vW5pqA==}
|
||||
engines: {node: '>=18'}
|
||||
peerDependencies:
|
||||
'@langchain/core': '>=0.2.21 <0.4.0'
|
||||
|
||||
'@langchain/google-vertexai@0.1.3':
|
||||
resolution: {integrity: sha512-AOvojzPiKKv1gZiH9tBYPBcZ3nOCuQAQsh4a/djYmlwjcA5bIb/teOa9eFZVTB83L2eM1U5steZF4nwQHnIIvw==}
|
||||
engines: {node: '>=18'}
|
||||
peerDependencies:
|
||||
'@langchain/core': '>=0.2.21 <0.4.0'
|
||||
|
||||
'@langchain/openai@0.3.14':
|
||||
resolution: {integrity: sha512-lNWjUo1tbvsss45IF7UQtMu1NJ6oUKvhgPYWXnX9f/d6OmuLu7D99HQ3Y88vLcUo9XjjOy417olYHignMduMjA==}
|
||||
engines: {node: '>=18'}
|
||||
@@ -5518,6 +5539,10 @@ packages:
|
||||
resolution: {integrity: sha512-bMxMKAjg13EBSVscxTaYA4mRc5t1UAXa2kXiGTNfZ079HIWXEkKmkgFrh/nJqamaLSrXO5H4WFFkPEaLJWbs3A==}
|
||||
engines: {node: '>= 0.4'}
|
||||
|
||||
arrify@2.0.1:
|
||||
resolution: {integrity: sha512-3duEwti880xqi4eAMN8AyR4a0ByT90zoYdLlevfrvU43vb0YZwZVfxOgxWrLXXXpyugL0hNZc9G6BiB5B3nUug==}
|
||||
engines: {node: '>=8'}
|
||||
|
||||
assertion-error@2.0.1:
|
||||
resolution: {integrity: sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==}
|
||||
engines: {node: '>=12'}
|
||||
@@ -5619,6 +5644,9 @@ packages:
|
||||
before-after-hook@2.2.3:
|
||||
resolution: {integrity: sha512-NzUnlZexiaH/46WDhANlyR2bXRopNg4F/zuSA3OpZnllCUgRaOF2znDioDWrmbNVsuZk6l9pMquQB38cfBZwkQ==}
|
||||
|
||||
bignumber.js@9.1.2:
|
||||
resolution: {integrity: sha512-2/mKyZH9K85bzOEfhXDBFZTGd1CTs+5IHpeFQo9luiBG7hghdC851Pj2WAhb6E3R6b9tZj/XKhbg4fum+Kepug==}
|
||||
|
||||
binary-extensions@2.3.0:
|
||||
resolution: {integrity: sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==}
|
||||
engines: {node: '>=8'}
|
||||
@@ -5668,6 +5696,9 @@ packages:
|
||||
buffer-crc32@0.2.13:
|
||||
resolution: {integrity: sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==}
|
||||
|
||||
buffer-equal-constant-time@1.0.1:
|
||||
resolution: {integrity: sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==}
|
||||
|
||||
buffer-from@1.1.2:
|
||||
resolution: {integrity: sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==}
|
||||
|
||||
@@ -6616,6 +6647,9 @@ packages:
|
||||
eastasianwidth@0.2.0:
|
||||
resolution: {integrity: sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==}
|
||||
|
||||
ecdsa-sig-formatter@1.0.11:
|
||||
resolution: {integrity: sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==}
|
||||
|
||||
editorconfig@1.0.4:
|
||||
resolution: {integrity: sha512-L9Qe08KWTlqYMVvMcTIvMAdl1cDUubzRNYL+WfA4bLDMHe4nemKkpmYzkznE1FwLKu0EEmy6obgQKzMJrg4x9Q==}
|
||||
engines: {node: '>=14'}
|
||||
@@ -7098,6 +7132,9 @@ packages:
|
||||
fast-levenshtein@2.0.6:
|
||||
resolution: {integrity: sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==}
|
||||
|
||||
fast-text-encoding@1.0.6:
|
||||
resolution: {integrity: sha512-VhXlQgj9ioXCqGstD37E/HBeqEGV/qOD/kmbVG8h5xKBYvM1L3lR1Zn4555cQ8GkYbJa8aJSipLPndE1k6zK2w==}
|
||||
|
||||
fast-xml-parser@4.4.1:
|
||||
resolution: {integrity: sha512-xkjOecfnKGkSsOwtZ5Pz7Us/T6mrbPQrq0nh+aCO5V9nk5NLWmasAHumTKjiPJPWANe+kAZ84Jc8ooJkzZ88Sw==}
|
||||
hasBin: true
|
||||
@@ -7278,6 +7315,14 @@ packages:
|
||||
functions-have-names@1.2.3:
|
||||
resolution: {integrity: sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==}
|
||||
|
||||
gaxios@5.1.3:
|
||||
resolution: {integrity: sha512-95hVgBRgEIRQQQHIbnxBXeHbW4TqFk4ZDJW7wmVtvYar72FdhRIo1UGOLS2eRAKCPEdPBWu+M7+A33D9CdX9rA==}
|
||||
engines: {node: '>=12'}
|
||||
|
||||
gcp-metadata@5.3.0:
|
||||
resolution: {integrity: sha512-FNTkdNEnBdlqF2oatizolQqNANMrcqJt6AAYt99B3y1aLLC8Hc5IOBb+ZnnzllodEEf6xMBp6wRcBbc16fa65w==}
|
||||
engines: {node: '>=12'}
|
||||
|
||||
gensync@1.0.0-beta.2:
|
||||
resolution: {integrity: sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==}
|
||||
engines: {node: '>=6.9.0'}
|
||||
@@ -7411,6 +7456,16 @@ packages:
|
||||
resolution: {integrity: sha512-jOMLD2Z7MAhyG8aJpNOpmziMOP4rPLcc95oQPKXBazW82z+CEgPFBQvEpRUa1KeIMUJo4Wsm+q6uzO/Q/4BksQ==}
|
||||
engines: {node: '>=18'}
|
||||
|
||||
google-auth-library@8.9.0:
|
||||
resolution: {integrity: sha512-f7aQCJODJFmYWN6PeNKzgvy9LI2tYmXnzpNDHEjG5sDNPgGb2FXQyTBnXeSH+PAtpKESFD+LmHw3Ox3mN7e1Fg==}
|
||||
engines: {node: '>=12'}
|
||||
|
||||
google-p12-pem@4.0.1:
|
||||
resolution: {integrity: sha512-WPkN4yGtz05WZ5EhtlxNDWPhC4JIic6G8ePitwUWy4l+XPVYec+a0j0Ts47PDtW59y3RwAhUd9/h9ZZ63px6RQ==}
|
||||
engines: {node: '>=12.0.0'}
|
||||
deprecated: Package is no longer maintained
|
||||
hasBin: true
|
||||
|
||||
gopd@1.0.1:
|
||||
resolution: {integrity: sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==}
|
||||
|
||||
@@ -7435,6 +7490,10 @@ packages:
|
||||
resolution: {integrity: sha512-GGTKBX4SD7Wdb8mqeDLni2oaRGYQWjWHGKPQ24ZMnUtKfcsVoiv4uX8+LJr1K6U5VW2Lu1BwJnj7uiori0YtRw==}
|
||||
engines: {node: ^12.22.0 || ^14.16.0 || ^16.0.0 || >=17.0.0}
|
||||
|
||||
gtoken@6.1.2:
|
||||
resolution: {integrity: sha512-4ccGpzz7YAr7lxrT2neugmXQ3hP9ho2gcaityLVkiUecAiwiy60Ii8gRbZeOsXV19fYaRjgBSshs8kXw+NKCPQ==}
|
||||
engines: {node: '>=12.0.0'}
|
||||
|
||||
hachure-fill@0.5.2:
|
||||
resolution: {integrity: sha512-3GKBOn+m2LX9iq+JC1064cSFprJY4jL1jCXTcpnfER5HYE2l/4EfWSGzkPa/ZDBmYI0ZOEj5VHV/eKnPGkHuOg==}
|
||||
|
||||
@@ -8206,6 +8265,9 @@ packages:
|
||||
engines: {node: '>=6'}
|
||||
hasBin: true
|
||||
|
||||
json-bigint@1.0.0:
|
||||
resolution: {integrity: sha512-SiPv/8VpZuWbvLSMtTDU8hEfrZWg/mH/nV/b4o0CYbSxu1UIQPLdwKOCIyLQX+VIPO5vrLX3i8qtqFyhdPSUSQ==}
|
||||
|
||||
json-buffer@3.0.1:
|
||||
resolution: {integrity: sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==}
|
||||
|
||||
@@ -8251,6 +8313,12 @@ packages:
|
||||
resolution: {integrity: sha512-ZZow9HBI5O6EPgSJLUb8n2NKgmVWTwCvHGwFuJlMjvLFqlGG6pjirPhtdsseaLZjSibD8eegzmYpUZwoIlj2cQ==}
|
||||
engines: {node: '>=4.0'}
|
||||
|
||||
jwa@2.0.0:
|
||||
resolution: {integrity: sha512-jrZ2Qx916EA+fq9cEAeCROWPTfCwi1IVHqT2tapuqLEVVDKFDENFw1oL+MwrTvH6msKxsd1YTDVw6uKEcsrLEA==}
|
||||
|
||||
jws@4.0.0:
|
||||
resolution: {integrity: sha512-KDncfTmOZoOMTFG4mBlG0qUIOlc03fmzH+ru6RgYVZhPkyiy/92Owlt/8UEN+a4TXR1FQetfIpJE8ApdvdVxTg==}
|
||||
|
||||
katex@0.16.11:
|
||||
resolution: {integrity: sha512-RQrI8rlHY92OLf3rho/Ts8i/XvjgguEjOkO1BEXcU3N8BqPpSzBNwV/G0Ukr+P/l3ivvJUE/Fa/CwbS6HesGNQ==}
|
||||
hasBin: true
|
||||
@@ -8549,8 +8617,8 @@ packages:
|
||||
resolution: {integrity: sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA==}
|
||||
engines: {node: '>=12'}
|
||||
|
||||
lucide-react@0.447.0:
|
||||
resolution: {integrity: sha512-SZ//hQmvi+kDKrNepArVkYK7/jfeZ5uFNEnYmd45RKZcbGD78KLnrcNXmgeg6m+xNHFvTG+CblszXCy4n6DN4w==}
|
||||
lucide-react@0.462.0:
|
||||
resolution: {integrity: sha512-NTL7EbAao9IFtuSivSZgrAh4fZd09Lr+6MTkqIxuHaH2nnYiYIzXPo06cOxHg9wKLdj6LL8TByG4qpePqwgx/g==}
|
||||
peerDependencies:
|
||||
react: ^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0-rc
|
||||
|
||||
@@ -9027,6 +9095,10 @@ packages:
|
||||
resolution: {integrity: sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA==}
|
||||
engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0}
|
||||
|
||||
node-forge@1.3.1:
|
||||
resolution: {integrity: sha512-dPEtOeMvF9VMcYV/1Wb8CPoVAXtp6MKMlcbAt4ddqmGqUJ6fQZFXkNZNkNlfevtNkGtaSoXf/vNNNSvgrdXwtA==}
|
||||
engines: {node: '>= 6.13.0'}
|
||||
|
||||
node-gyp-build-optional-packages@5.2.2:
|
||||
resolution: {integrity: sha512-s+w+rBWnpTMwSFbaE0UXsRlg7hU4FjekKU4eyAih5T8nJuNZT1nNsskXpxmeqSK9UzkBl6UgRlnKc8hz8IEqOw==}
|
||||
hasBin: true
|
||||
@@ -9584,8 +9656,8 @@ packages:
|
||||
posthog-js@1.176.0:
|
||||
resolution: {integrity: sha512-T5XKNtRzp7q6CGb7Vc7wAI76rWap9fiuDUPxPsyPBPDkreKya91x9RIsSapAVFafwD1AEin1QMczCmt9Le9BWw==}
|
||||
|
||||
posthog-node@3.6.3:
|
||||
resolution: {integrity: sha512-JB+ei0LkwE+rKHyW5z79Nd1jUaGxU6TvkfjFqY9vQaHxU5aU8dRl0UUaEmZdZbHwjp3WmXCBQQRNyimwbNQfCw==}
|
||||
posthog-node@4.3.1:
|
||||
resolution: {integrity: sha512-By9SEGZxBLC7GVyVb+HlJlpxM/xI4iLUgwtsBS8f4bZ0wqYKiNHoYcFEwMJnTM9xQcQZztr6dqLmsJ7jCv0vxA==}
|
||||
engines: {node: '>=15.0.0'}
|
||||
|
||||
pprof-format@2.1.0:
|
||||
@@ -9805,10 +9877,6 @@ packages:
|
||||
pure-rand@6.0.4:
|
||||
resolution: {integrity: sha512-LA0Y9kxMYv47GIPJy6MI84fqTd2HmYZI83W/kM/SkKfDlajnZYfmXFTxkbY+xSBPkLJxltMa9hIkmdc29eguMA==}
|
||||
|
||||
qs@6.11.0:
|
||||
resolution: {integrity: sha512-MvjoMCJwEarSbUYk5O+nmoSzSutSsTwF85zcHPQ9OrlFoZOYIjaqBAJIqIXjptyD5vThxGq52Xu/MaJzRkIk4Q==}
|
||||
engines: {node: '>=0.6'}
|
||||
|
||||
qs@6.13.0:
|
||||
resolution: {integrity: sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg==}
|
||||
engines: {node: '>=0.6'}
|
||||
@@ -10564,8 +10632,8 @@ packages:
|
||||
resolution: {integrity: sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==}
|
||||
engines: {node: '>=8'}
|
||||
|
||||
stripe@16.8.0:
|
||||
resolution: {integrity: sha512-6rOIcGOkxcc29jvhEyOYmpPFilekOBV+7vpemAoIAfbtCRW1yxzdDGM0/0vyekHglLL+wqGpP5ldrhO3dJ2JEQ==}
|
||||
stripe@17.4.0:
|
||||
resolution: {integrity: sha512-sQQGZguPxe7/QYXJKtDpfzT2OAH9F8nyE2SOsVdTU793iiU33/dpaKgWaJEGJm8396Yy/6NvTLblgdHlueGLhA==}
|
||||
engines: {node: '>=12.*'}
|
||||
|
||||
strnum@1.0.5:
|
||||
@@ -14034,6 +14102,33 @@ snapshots:
|
||||
transitivePeerDependencies:
|
||||
- openai
|
||||
|
||||
'@langchain/google-common@0.1.3(@langchain/core@0.3.18(openai@4.72.0(zod@3.23.8)))(zod@3.23.8)':
|
||||
dependencies:
|
||||
'@langchain/core': 0.3.18(openai@4.72.0(zod@3.23.8))
|
||||
uuid: 10.0.0
|
||||
zod-to-json-schema: 3.23.5(zod@3.23.8)
|
||||
transitivePeerDependencies:
|
||||
- zod
|
||||
|
||||
'@langchain/google-gauth@0.1.3(@langchain/core@0.3.18(openai@4.72.0(zod@3.23.8)))(zod@3.23.8)':
|
||||
dependencies:
|
||||
'@langchain/core': 0.3.18(openai@4.72.0(zod@3.23.8))
|
||||
'@langchain/google-common': 0.1.3(@langchain/core@0.3.18(openai@4.72.0(zod@3.23.8)))(zod@3.23.8)
|
||||
google-auth-library: 8.9.0
|
||||
transitivePeerDependencies:
|
||||
- encoding
|
||||
- supports-color
|
||||
- zod
|
||||
|
||||
'@langchain/google-vertexai@0.1.3(@langchain/core@0.3.18(openai@4.72.0(zod@3.23.8)))(zod@3.23.8)':
|
||||
dependencies:
|
||||
'@langchain/core': 0.3.18(openai@4.72.0(zod@3.23.8))
|
||||
'@langchain/google-gauth': 0.1.3(@langchain/core@0.3.18(openai@4.72.0(zod@3.23.8)))(zod@3.23.8)
|
||||
transitivePeerDependencies:
|
||||
- encoding
|
||||
- supports-color
|
||||
- zod
|
||||
|
||||
'@langchain/openai@0.3.14(@langchain/core@0.3.18(openai@4.72.0(zod@3.23.8)))':
|
||||
dependencies:
|
||||
'@langchain/core': 0.3.18(openai@4.72.0(zod@3.23.8))
|
||||
@@ -17378,10 +17473,10 @@ snapshots:
|
||||
'@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)
|
||||
'@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(@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-import-resolver-alias: 1.1.2(eslint-plugin-import@2.29.1)
|
||||
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-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(@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)(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(@types/node@20.14.8)(babel-plugin-macros@3.1.0))(typescript@5.4.5))(eslint@8.57.0)
|
||||
@@ -17865,6 +17960,8 @@ snapshots:
|
||||
is-array-buffer: 3.0.4
|
||||
is-shared-array-buffer: 1.0.3
|
||||
|
||||
arrify@2.0.1: {}
|
||||
|
||||
assertion-error@2.0.1: {}
|
||||
|
||||
ast-types-flow@0.0.8: {}
|
||||
@@ -17988,6 +18085,8 @@ snapshots:
|
||||
|
||||
before-after-hook@2.2.3: {}
|
||||
|
||||
bignumber.js@9.1.2: {}
|
||||
|
||||
binary-extensions@2.3.0: {}
|
||||
|
||||
bl@4.1.0:
|
||||
@@ -18066,6 +18165,8 @@ snapshots:
|
||||
|
||||
buffer-crc32@0.2.13: {}
|
||||
|
||||
buffer-equal-constant-time@1.0.1: {}
|
||||
|
||||
buffer-from@1.1.2: {}
|
||||
|
||||
buffer@5.6.0:
|
||||
@@ -19043,6 +19144,10 @@ snapshots:
|
||||
|
||||
eastasianwidth@0.2.0: {}
|
||||
|
||||
ecdsa-sig-formatter@1.0.11:
|
||||
dependencies:
|
||||
safe-buffer: 5.2.1
|
||||
|
||||
editorconfig@1.0.4:
|
||||
dependencies:
|
||||
'@one-ini/wasm': 0.1.1
|
||||
@@ -19286,7 +19391,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(@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)(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)
|
||||
@@ -19303,7 +19408,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-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)(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)
|
||||
|
||||
@@ -19312,9 +19417,9 @@ snapshots:
|
||||
eslint: 8.57.0
|
||||
eslint-plugin-turbo: 1.13.4(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-alias@1.1.2(eslint-plugin-import@2.29.1):
|
||||
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(@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)(eslint@8.57.0))(eslint@8.57.0)
|
||||
|
||||
eslint-import-resolver-node@0.3.9:
|
||||
dependencies:
|
||||
@@ -19329,8 +19434,8 @@ snapshots:
|
||||
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-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(@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-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)(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)(eslint@8.57.0))(eslint@8.57.0)
|
||||
fast-glob: 3.3.2
|
||||
get-tsconfig: 4.8.1
|
||||
is-core-module: 2.15.1
|
||||
@@ -19341,13 +19446,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(@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-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):
|
||||
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(@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)
|
||||
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(@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)
|
||||
fast-glob: 3.3.2
|
||||
get-tsconfig: 4.8.1
|
||||
is-core-module: 2.15.1
|
||||
@@ -19358,7 +19463,18 @@ snapshots:
|
||||
- eslint-import-resolver-webpack
|
||||
- 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-import-resolver-node@0.3.9)(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)(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)
|
||||
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)(eslint@8.57.0):
|
||||
dependencies:
|
||||
debug: 3.2.7
|
||||
optionalDependencies:
|
||||
@@ -19369,17 +19485,6 @@ 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(@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(@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
|
||||
|
||||
eslint-plugin-es-x@7.8.0(eslint@8.57.0):
|
||||
dependencies:
|
||||
'@eslint-community/eslint-utils': 4.4.1(eslint@8.57.0)
|
||||
@@ -19393,7 +19498,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(@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)(eslint@8.57.0))(eslint@8.57.0):
|
||||
dependencies:
|
||||
array-includes: 3.1.7
|
||||
array.prototype.findlastindex: 1.2.4
|
||||
@@ -19403,7 +19508,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(@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-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)
|
||||
hasown: 2.0.2
|
||||
is-core-module: 2.15.1
|
||||
is-glob: 4.0.3
|
||||
@@ -19785,6 +19890,8 @@ snapshots:
|
||||
|
||||
fast-levenshtein@2.0.6: {}
|
||||
|
||||
fast-text-encoding@1.0.6: {}
|
||||
|
||||
fast-xml-parser@4.4.1:
|
||||
dependencies:
|
||||
strnum: 1.0.5
|
||||
@@ -19956,6 +20063,24 @@ snapshots:
|
||||
|
||||
functions-have-names@1.2.3: {}
|
||||
|
||||
gaxios@5.1.3:
|
||||
dependencies:
|
||||
extend: 3.0.2
|
||||
https-proxy-agent: 5.0.1
|
||||
is-stream: 2.0.1
|
||||
node-fetch: 2.7.0
|
||||
transitivePeerDependencies:
|
||||
- encoding
|
||||
- supports-color
|
||||
|
||||
gcp-metadata@5.3.0:
|
||||
dependencies:
|
||||
gaxios: 5.1.3
|
||||
json-bigint: 1.0.0
|
||||
transitivePeerDependencies:
|
||||
- encoding
|
||||
- supports-color
|
||||
|
||||
gensync@1.0.0-beta.2: {}
|
||||
|
||||
get-caller-file@2.0.5: {}
|
||||
@@ -20124,6 +20249,25 @@ snapshots:
|
||||
slash: 5.1.0
|
||||
unicorn-magic: 0.1.0
|
||||
|
||||
google-auth-library@8.9.0:
|
||||
dependencies:
|
||||
arrify: 2.0.1
|
||||
base64-js: 1.5.1
|
||||
ecdsa-sig-formatter: 1.0.11
|
||||
fast-text-encoding: 1.0.6
|
||||
gaxios: 5.1.3
|
||||
gcp-metadata: 5.3.0
|
||||
gtoken: 6.1.2
|
||||
jws: 4.0.0
|
||||
lru-cache: 6.0.0
|
||||
transitivePeerDependencies:
|
||||
- encoding
|
||||
- supports-color
|
||||
|
||||
google-p12-pem@4.0.1:
|
||||
dependencies:
|
||||
node-forge: 1.3.1
|
||||
|
||||
gopd@1.0.1:
|
||||
dependencies:
|
||||
get-intrinsic: 1.2.4
|
||||
@@ -20164,6 +20308,15 @@ snapshots:
|
||||
|
||||
graphql@16.9.0: {}
|
||||
|
||||
gtoken@6.1.2:
|
||||
dependencies:
|
||||
gaxios: 5.1.3
|
||||
google-p12-pem: 4.0.1
|
||||
jws: 4.0.0
|
||||
transitivePeerDependencies:
|
||||
- encoding
|
||||
- supports-color
|
||||
|
||||
hachure-fill@0.5.2: {}
|
||||
|
||||
handlebars@4.7.8:
|
||||
@@ -21221,6 +21374,10 @@ snapshots:
|
||||
|
||||
jsesc@3.0.2: {}
|
||||
|
||||
json-bigint@1.0.0:
|
||||
dependencies:
|
||||
bignumber.js: 9.1.2
|
||||
|
||||
json-buffer@3.0.1: {}
|
||||
|
||||
json-parse-even-better-errors@2.3.1: {}
|
||||
@@ -21264,6 +21421,17 @@ snapshots:
|
||||
object.assign: 4.1.5
|
||||
object.values: 1.2.0
|
||||
|
||||
jwa@2.0.0:
|
||||
dependencies:
|
||||
buffer-equal-constant-time: 1.0.1
|
||||
ecdsa-sig-formatter: 1.0.11
|
||||
safe-buffer: 5.2.1
|
||||
|
||||
jws@4.0.0:
|
||||
dependencies:
|
||||
jwa: 2.0.0
|
||||
safe-buffer: 5.2.1
|
||||
|
||||
katex@0.16.11:
|
||||
dependencies:
|
||||
commander: 8.3.0
|
||||
@@ -21299,7 +21467,7 @@ snapshots:
|
||||
|
||||
kysely@0.27.4: {}
|
||||
|
||||
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-sso-oidc@3.668.0(@aws-sdk/client-sts@3.668.0))(@aws-sdk/client-sts@3.668.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)))(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-sso-oidc@3.668.0(@aws-sdk/client-sts@3.668.0))(@aws-sdk/client-sts@3.668.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)))
|
||||
@@ -21317,13 +21485,14 @@ snapshots:
|
||||
optionalDependencies:
|
||||
'@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-sso-oidc@3.668.0(@aws-sdk/client-sts@3.668.0))(@aws-sdk/client-sts@3.668.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
|
||||
transitivePeerDependencies:
|
||||
- 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(@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)))(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)))
|
||||
@@ -21341,6 +21510,7 @@ snapshots:
|
||||
optionalDependencies:
|
||||
'@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/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
|
||||
transitivePeerDependencies:
|
||||
@@ -21351,9 +21521,9 @@ snapshots:
|
||||
dependencies:
|
||||
mustache: 4.2.0
|
||||
|
||||
langfuse-langchain@3.30.3(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-sso-oidc@3.668.0(@aws-sdk/client-sts@3.668.0))(@aws-sdk/client-sts@3.668.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)))(axios@1.7.7)(handlebars@4.7.8)(openai@4.72.0(zod@3.23.8))):
|
||||
langfuse-langchain@3.30.3(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-sso-oidc@3.668.0(@aws-sdk/client-sts@3.668.0))(@aws-sdk/client-sts@3.668.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: 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-sso-oidc@3.668.0(@aws-sdk/client-sts@3.668.0))(@aws-sdk/client-sts@3.668.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)))(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-sso-oidc@3.668.0(@aws-sdk/client-sts@3.668.0))(@aws-sdk/client-sts@3.668.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))
|
||||
langfuse: 3.30.3
|
||||
langfuse-core: 3.30.3
|
||||
|
||||
@@ -21514,7 +21684,7 @@ snapshots:
|
||||
|
||||
lru-cache@7.18.3: {}
|
||||
|
||||
lucide-react@0.447.0(react@18.2.0):
|
||||
lucide-react@0.462.0(react@18.2.0):
|
||||
dependencies:
|
||||
react: 18.2.0
|
||||
|
||||
@@ -22257,6 +22427,8 @@ snapshots:
|
||||
fetch-blob: 3.2.0
|
||||
formdata-polyfill: 4.0.10
|
||||
|
||||
node-forge@1.3.1: {}
|
||||
|
||||
node-gyp-build-optional-packages@5.2.2:
|
||||
dependencies:
|
||||
detect-libc: 2.0.3
|
||||
@@ -22844,7 +23016,7 @@ snapshots:
|
||||
preact: 10.19.7
|
||||
web-vitals: 4.2.3
|
||||
|
||||
posthog-node@3.6.3:
|
||||
posthog-node@4.3.1:
|
||||
dependencies:
|
||||
axios: 1.7.7
|
||||
rusha: 0.8.14
|
||||
@@ -23061,10 +23233,6 @@ snapshots:
|
||||
|
||||
pure-rand@6.0.4: {}
|
||||
|
||||
qs@6.11.0:
|
||||
dependencies:
|
||||
side-channel: 1.0.6
|
||||
|
||||
qs@6.13.0:
|
||||
dependencies:
|
||||
side-channel: 1.0.6
|
||||
@@ -23946,10 +24114,10 @@ snapshots:
|
||||
|
||||
strip-json-comments@3.1.1: {}
|
||||
|
||||
stripe@16.8.0:
|
||||
stripe@17.4.0:
|
||||
dependencies:
|
||||
'@types/node': 20.14.8
|
||||
qs: 6.11.0
|
||||
qs: 6.13.0
|
||||
|
||||
strnum@1.0.5: {}
|
||||
|
||||
|
||||
+1
-2
@@ -85,8 +85,7 @@ ENV NEXT_MANUAL_SIG_HANDLE true
|
||||
# set the CI flag to true to get CI specific logs
|
||||
ENV CI true
|
||||
|
||||
|
||||
RUN NODE_OPTIONS='--max-old-space-size=4096' turbo run build --filter=web
|
||||
RUN NODE_OPTIONS='--max-old-space-size=4096' turbo run build --filter=web...
|
||||
|
||||
# Production image, copy all the files and run next
|
||||
FROM --platform=${TARGETPLATFORM:-linux/amd64} base AS runner
|
||||
|
||||
+28
-16
@@ -97,6 +97,7 @@ const nextConfig = {
|
||||
...(env.SENTRY_CSP_REPORT_URI ? [reportToHeader] : []),
|
||||
],
|
||||
},
|
||||
// CSP header
|
||||
{
|
||||
source: "/:path((?!api).*)*",
|
||||
headers: [
|
||||
@@ -105,26 +106,37 @@ const nextConfig = {
|
||||
value: cspHeader.replace(/\n/g, ""),
|
||||
},
|
||||
],
|
||||
// Disable CSP on Hugging Face to allow for embedded use of Langfuse
|
||||
missing: [
|
||||
{
|
||||
type: "host",
|
||||
value: "huggingface.co",
|
||||
},
|
||||
{
|
||||
type: "host",
|
||||
value: ".*\\.hf\\.space$", // *.hf.space
|
||||
},
|
||||
],
|
||||
},
|
||||
// Required to check authentication status from langfuse.com
|
||||
...(env.NEXT_PUBLIC_LANGFUSE_CLOUD_REGION !== undefined
|
||||
? [
|
||||
{
|
||||
source: "/api/auth/session",
|
||||
headers: [
|
||||
{
|
||||
key: "Access-Control-Allow-Origin",
|
||||
value: "https://langfuse.com",
|
||||
},
|
||||
{ key: "Access-Control-Allow-Credentials", value: "true" },
|
||||
{ key: "Access-Control-Allow-Methods", value: "GET,POST" },
|
||||
{
|
||||
key: "Access-Control-Allow-Headers",
|
||||
value: "Content-Type, Authorization",
|
||||
},
|
||||
],
|
||||
},
|
||||
]
|
||||
{
|
||||
source: "/api/auth/session",
|
||||
headers: [
|
||||
{
|
||||
key: "Access-Control-Allow-Origin",
|
||||
value: "https://langfuse.com",
|
||||
},
|
||||
{ key: "Access-Control-Allow-Credentials", value: "true" },
|
||||
{ key: "Access-Control-Allow-Methods", value: "GET,POST" },
|
||||
{
|
||||
key: "Access-Control-Allow-Headers",
|
||||
value: "Content-Type, Authorization",
|
||||
},
|
||||
],
|
||||
},
|
||||
]
|
||||
: []),
|
||||
// all files in /public/generated are public and can be accessed from any origin, e.g. to render an API reference based on our openapi schema
|
||||
{
|
||||
|
||||
+4
-4
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "web",
|
||||
"version": "2.92.0",
|
||||
"version": "3.0.0-rc.3",
|
||||
"private": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
@@ -115,13 +115,13 @@
|
||||
"kysely": "^0.27.4",
|
||||
"langchain": "^0.3.6",
|
||||
"lodash": "^4.17.21",
|
||||
"lucide-react": "^0.447.0",
|
||||
"lucide-react": "^0.462.0",
|
||||
"next": "^14.2.15",
|
||||
"next-auth": "^4.24.7",
|
||||
"next-query-params": "^5.0.1",
|
||||
"next-themes": "^0.3.0",
|
||||
"posthog-js": "^1.176.0",
|
||||
"posthog-node": "^3.6.3",
|
||||
"posthog-node": "^4.3.1",
|
||||
"prexit": "^2.2.0",
|
||||
"prisma": "^5.22.0",
|
||||
"rate-limiter-flexible": "^5.0.3",
|
||||
@@ -138,7 +138,7 @@
|
||||
"remark-gfm": "^4.0.0",
|
||||
"remark-math": "^6.0.0",
|
||||
"sonner": "^1.4.41",
|
||||
"stripe": "^16.8.0",
|
||||
"stripe": "^17.4.0",
|
||||
"superjson": "2.2.1",
|
||||
"tailwind-merge": "^2.5.2",
|
||||
"tailwindcss-animate": "^1.0.7",
|
||||
|
||||
@@ -4059,7 +4059,7 @@ components:
|
||||
type: string
|
||||
enum:
|
||||
- >-
|
||||
image/png","image/jpeg","image/jpg","image/webp","audio/mpeg","audio/mp3","audio/wav","text/plain","application/pdf
|
||||
image/png","image/jpeg","image/jpg","image/webp","image/gif","image/svg+xml","image/tiff","image/bmp","audio/mpeg","audio/mp3","audio/wav","audio/ogg","audio/oga","audio/aac","audio/mp4","audio/flac","video/mp4","video/webm","text/plain","text/html","text/css","text/csv","application/pdf","application/msword","application/vnd.ms-excel","application/zip","application/json","application/xml","application/octet-stream
|
||||
description: The MIME type of the media record
|
||||
DailyMetrics:
|
||||
title: DailyMetrics
|
||||
|
||||
@@ -22,7 +22,7 @@ test("Successfully sign up & able to go to homepage", async ({ page }) => {
|
||||
await page.goto("auth/sign-up");
|
||||
await page.fill('input[name="name"]', "demo lang");
|
||||
await page.fill('input[name="email"]', randomEmailAddress());
|
||||
await page.fill('input[type="password"]', "password2");
|
||||
await page.fill('input[type="password"]', "Password2#!");
|
||||
await page.click('button[data-testid="submit-email-password-sign-up-form"]');
|
||||
await page.waitForTimeout(2000);
|
||||
// see get started page
|
||||
@@ -35,7 +35,7 @@ test("Successfully sign up & able to go to homepage with uppercase email", async
|
||||
await page.goto("auth/sign-up");
|
||||
await page.fill('input[name="name"]', "demo lang");
|
||||
await page.fill('input[name="email"]', "A" + randomEmailAddress());
|
||||
await page.fill('input[type="password"]', "password3");
|
||||
await page.fill('input[type="password"]', "Password3#!");
|
||||
await page.click('button[data-testid="submit-email-password-sign-up-form"]');
|
||||
await page.waitForTimeout(2000);
|
||||
// see get started page
|
||||
@@ -50,7 +50,7 @@ test("Signup input validation", async ({ page }) => {
|
||||
await page.waitForTimeout(2000);
|
||||
await expect(page.getByText("Invalid email")).toBeVisible();
|
||||
await expect(
|
||||
page.getByText("Password must be at least 8 characters long"),
|
||||
page.getByText("Password must be at least 8 characters long."),
|
||||
).toBeVisible();
|
||||
await page.click('button[data-testid="submit-email-password-sign-up-form"]');
|
||||
await page.waitForTimeout(2000);
|
||||
|
||||
@@ -0,0 +1,75 @@
|
||||
import {
|
||||
createObservation,
|
||||
createTrace,
|
||||
} from "@/src/__tests__/fixtures/tracing-factory";
|
||||
import {
|
||||
createObservations,
|
||||
createTraces,
|
||||
} from "@/src/__tests__/server/repositories/clickhouse-helpers";
|
||||
import { makeZodVerifiedAPICall } from "@/src/__tests__/test-utils";
|
||||
import { randomUUID } from "crypto";
|
||||
import { GetMetricsDailyV1Response } from "@/src/features/public-api/types/metrics";
|
||||
|
||||
describe("/api/public/metrics/daily API Endpoint", () => {
|
||||
it("should get correct results from daily metrics", async () => {
|
||||
const traceName = randomUUID();
|
||||
const createdTrace = createTrace({
|
||||
name: traceName,
|
||||
user_id: "user-1",
|
||||
project_id: "7a88fb47-b4e2-43b8-a06c-a5ce950dc53a",
|
||||
metadata: { key: "value" },
|
||||
release: "1.0.0",
|
||||
version: "2.0.0",
|
||||
});
|
||||
|
||||
const observations = [
|
||||
createObservation({
|
||||
trace_id: createdTrace.id,
|
||||
project_id: createdTrace.project_id,
|
||||
name: "observation-name",
|
||||
end_time: new Date().getTime(),
|
||||
start_time: new Date().getTime() - 1000,
|
||||
input: "input",
|
||||
output: "output",
|
||||
provided_model_name: "model-1",
|
||||
}),
|
||||
createObservation({
|
||||
trace_id: createdTrace.id,
|
||||
project_id: createdTrace.project_id,
|
||||
name: "observation-name-2",
|
||||
end_time: new Date().getTime(),
|
||||
start_time: new Date().getTime() - 100000,
|
||||
input: "input-2",
|
||||
output: "output-2",
|
||||
provided_model_name: "model-2",
|
||||
}),
|
||||
];
|
||||
|
||||
await createTraces([createdTrace]);
|
||||
await createObservations(observations);
|
||||
|
||||
const metrics = await makeZodVerifiedAPICall(
|
||||
GetMetricsDailyV1Response,
|
||||
"GET",
|
||||
`/api/public/metrics/daily?traceName=${traceName}`,
|
||||
);
|
||||
|
||||
expect(metrics.body.meta.totalItems).toBe(1);
|
||||
expect(metrics.body.meta.totalPages).toBe(1);
|
||||
|
||||
const metric = metrics.body.data[0];
|
||||
expect(metric.countTraces).toBe(1);
|
||||
expect(metric.countObservations).toBe(2);
|
||||
expect(metric.usage).toHaveLength(2);
|
||||
expect(metric.totalCost).toBe(600);
|
||||
for (const usage of metric.usage) {
|
||||
expect(usage.model).toMatch(/model-\d/g);
|
||||
expect(usage.inputUsage).toBe(1234);
|
||||
expect(usage.outputUsage).toBe(5678);
|
||||
expect(usage.totalUsage).toBe(6912);
|
||||
expect(usage.countObservations).toBe(1);
|
||||
expect(usage.countTraces).toBe(1);
|
||||
expect(usage.totalCost).toBe(300);
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -7,7 +7,12 @@ import {
|
||||
createTraces,
|
||||
} from "@/src/__tests__/server/repositories/clickhouse-helpers";
|
||||
import { makeZodVerifiedAPICall } from "@/src/__tests__/test-utils";
|
||||
import { GetTraceV1Response } from "@/src/features/public-api/types/traces";
|
||||
import {
|
||||
GetTracesV1Response,
|
||||
GetTraceV1Response,
|
||||
} from "@/src/features/public-api/types/traces";
|
||||
import { randomUUID } from "crypto";
|
||||
import { snakeCase } from "lodash";
|
||||
|
||||
describe("/api/public/traces API Endpoint", () => {
|
||||
it("should create and get a trace via /traces", async () => {
|
||||
@@ -73,4 +78,182 @@ describe("/api/public/traces API Endpoint", () => {
|
||||
]),
|
||||
);
|
||||
});
|
||||
|
||||
it("should fetch all traces", async () => {
|
||||
const createdTrace = createTrace({
|
||||
name: "trace-name",
|
||||
user_id: "user-1",
|
||||
project_id: "7a88fb47-b4e2-43b8-a06c-a5ce950dc53a",
|
||||
metadata: { key: "value" },
|
||||
release: "1.0.0",
|
||||
version: "2.0.0",
|
||||
});
|
||||
|
||||
const observations = [
|
||||
createObservation({
|
||||
trace_id: createdTrace.id,
|
||||
project_id: createdTrace.project_id,
|
||||
name: "observation-name",
|
||||
end_time: new Date().getTime(),
|
||||
start_time: new Date().getTime() - 1000,
|
||||
input: "input",
|
||||
output: "output",
|
||||
}),
|
||||
createObservation({
|
||||
trace_id: createdTrace.id,
|
||||
project_id: createdTrace.project_id,
|
||||
name: "observation-name-2",
|
||||
end_time: new Date().getTime(),
|
||||
start_time: new Date().getTime() - 100000,
|
||||
input: "input-2",
|
||||
output: "output-2",
|
||||
}),
|
||||
];
|
||||
|
||||
await createTraces([createdTrace]);
|
||||
await createObservations(observations);
|
||||
|
||||
const traces = await makeZodVerifiedAPICall(
|
||||
GetTracesV1Response,
|
||||
"GET",
|
||||
"/api/public/traces",
|
||||
);
|
||||
|
||||
expect(traces.body.meta.totalItems).toBeGreaterThanOrEqual(1);
|
||||
expect(traces.body.data.length).toBeGreaterThanOrEqual(1);
|
||||
const trace = traces.body.data.find((t) => t.id === createdTrace.id);
|
||||
expect(trace).toBeTruthy();
|
||||
if (!trace) {
|
||||
return; // to satisfy TypeScript
|
||||
}
|
||||
expect(trace.name).toBe("trace-name");
|
||||
expect(trace.release).toBe("1.0.0");
|
||||
expect(trace.externalId).toBeNull();
|
||||
expect(trace.version).toBe("2.0.0");
|
||||
expect(trace.projectId).toBe("7a88fb47-b4e2-43b8-a06c-a5ce950dc53a");
|
||||
expect(trace.latency).toBe(100);
|
||||
expect(trace.observations.length).toBe(2);
|
||||
expect(trace.scores.length).toBe(0);
|
||||
});
|
||||
|
||||
it.each([
|
||||
["userId", randomUUID()],
|
||||
["sessionId", randomUUID()],
|
||||
["release", randomUUID()],
|
||||
["version", randomUUID()],
|
||||
["name", randomUUID()],
|
||||
])(
|
||||
"should fetch all traces filtered by a value (%s, %s)",
|
||||
async (prop: string, value: string) => {
|
||||
const createdTrace = createTrace({
|
||||
[snakeCase(prop)]: value,
|
||||
project_id: "7a88fb47-b4e2-43b8-a06c-a5ce950dc53a",
|
||||
metadata: { key: "value" },
|
||||
});
|
||||
|
||||
await createTraces([createdTrace]);
|
||||
|
||||
const traces = await makeZodVerifiedAPICall(
|
||||
GetTracesV1Response,
|
||||
"GET",
|
||||
`/api/public/traces?${prop}=${value}`,
|
||||
);
|
||||
|
||||
expect(traces.body.meta.totalItems).toBe(1);
|
||||
expect(traces.body.data.length).toBe(1);
|
||||
const trace = traces.body.data[0];
|
||||
expect(trace.projectId).toBe("7a88fb47-b4e2-43b8-a06c-a5ce950dc53a");
|
||||
expect((trace as any)[prop]).toBe(value);
|
||||
},
|
||||
);
|
||||
|
||||
it("should fetch all traces filtered by a tag", async () => {
|
||||
const tag = randomUUID();
|
||||
const createdTrace = createTrace({
|
||||
name: "trace-name",
|
||||
project_id: "7a88fb47-b4e2-43b8-a06c-a5ce950dc53a",
|
||||
metadata: { key: "value" },
|
||||
tags: [tag],
|
||||
});
|
||||
|
||||
await createTraces([createdTrace]);
|
||||
|
||||
const traces = await makeZodVerifiedAPICall(
|
||||
GetTracesV1Response,
|
||||
"GET",
|
||||
`/api/public/traces?tags=${[tag]}`,
|
||||
);
|
||||
|
||||
expect(traces.body.meta.totalItems).toBe(1);
|
||||
expect(traces.body.data.length).toBe(1);
|
||||
const trace = traces.body.data[0];
|
||||
expect(trace.projectId).toBe("7a88fb47-b4e2-43b8-a06c-a5ce950dc53a");
|
||||
});
|
||||
|
||||
it("should fetch all traces with pagination", async () => {
|
||||
const tag = randomUUID();
|
||||
const createdTrace1 = createTrace({
|
||||
name: "trace-name",
|
||||
project_id: "7a88fb47-b4e2-43b8-a06c-a5ce950dc53a",
|
||||
metadata: { key: "value" },
|
||||
tags: [tag],
|
||||
});
|
||||
const createdTrace2 = createTrace({
|
||||
name: "trace-name",
|
||||
project_id: "7a88fb47-b4e2-43b8-a06c-a5ce950dc53a",
|
||||
metadata: { key: "value" },
|
||||
tags: [tag],
|
||||
});
|
||||
const createdTrace3 = createTrace({
|
||||
name: "trace-name",
|
||||
project_id: "7a88fb47-b4e2-43b8-a06c-a5ce950dc53a",
|
||||
metadata: { key: "value" },
|
||||
tags: [tag],
|
||||
});
|
||||
|
||||
await createTraces([createdTrace1, createdTrace2, createdTrace3]);
|
||||
|
||||
const traces = await makeZodVerifiedAPICall(
|
||||
GetTracesV1Response,
|
||||
"GET",
|
||||
`/api/public/traces?tags=${[tag]}&limit=1&offset=1`,
|
||||
);
|
||||
|
||||
expect(traces.body.meta.totalItems).toBe(3);
|
||||
expect(traces.body.data.length).toBe(1);
|
||||
expect(traces.body.meta.totalPages).toBe(3);
|
||||
const trace = traces.body.data[0];
|
||||
expect(trace.projectId).toBe("7a88fb47-b4e2-43b8-a06c-a5ce950dc53a");
|
||||
});
|
||||
|
||||
it("should fetch all traces with custom order", async () => {
|
||||
const tag = randomUUID();
|
||||
const createdTrace1 = createTrace({
|
||||
name: "trace-name1",
|
||||
project_id: "7a88fb47-b4e2-43b8-a06c-a5ce950dc53a",
|
||||
metadata: { key: "value" },
|
||||
tags: [tag],
|
||||
});
|
||||
const createdTrace2 = createTrace({
|
||||
name: "trace-name2",
|
||||
project_id: "7a88fb47-b4e2-43b8-a06c-a5ce950dc53a",
|
||||
metadata: { key: "value" },
|
||||
tags: [tag],
|
||||
});
|
||||
|
||||
await createTraces([createdTrace1, createdTrace2]);
|
||||
|
||||
const traces = await makeZodVerifiedAPICall(
|
||||
GetTracesV1Response,
|
||||
"GET",
|
||||
`/api/public/traces?tags=${[tag]}&orderBy=name.desc`,
|
||||
);
|
||||
|
||||
expect(traces.body.meta.totalItems).toBe(2);
|
||||
expect(traces.body.data.length).toBe(2);
|
||||
const trace1 = traces.body.data[0];
|
||||
expect(trace1.name).toBe("trace-name2");
|
||||
const trace2 = traces.body.data[1];
|
||||
expect(trace2.name).toBe("trace-name1");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -9,9 +9,9 @@ import {
|
||||
} from "@/src/__tests__/fixtures/tracing-factory";
|
||||
import {
|
||||
getTracesTable,
|
||||
type TracesAllUiReturnType,
|
||||
type ObservationRecordInsertType,
|
||||
type TraceRecordInsertType,
|
||||
type TracesTableReturnType,
|
||||
} from "@langfuse/shared/src/server";
|
||||
import { type FilterState } from "@langfuse/shared";
|
||||
|
||||
@@ -43,12 +43,6 @@ describe("Traces table API test", () => {
|
||||
expect(tableRows[0].userId).toEqual(trace.user_id);
|
||||
expect(tableRows[0].sessionId).toEqual(trace.session_id);
|
||||
expect(tableRows[0].public).toEqual(trace.public);
|
||||
expect(tableRows[0].latency).toBeGreaterThanOrEqual(0);
|
||||
expect(tableRows[0].usageDetails).toEqual({});
|
||||
expect(tableRows[0].costDetails).toEqual({});
|
||||
expect(tableRows[0].level).toBeDefined();
|
||||
expect(tableRows[0].observationCount).toBeGreaterThanOrEqual(0);
|
||||
expect(tableRows[0].scoresAvg).toEqual([]);
|
||||
});
|
||||
|
||||
it("should get a correct trace with observations", async () => {
|
||||
@@ -82,29 +76,13 @@ describe("Traces table API test", () => {
|
||||
expect(tableRows[0].userId).toEqual(trace.user_id);
|
||||
expect(tableRows[0].sessionId).toEqual(trace.session_id);
|
||||
expect(tableRows[0].public).toEqual(trace.public);
|
||||
expect(tableRows[0].latency).toBeGreaterThanOrEqual(0);
|
||||
expect(tableRows[0].usageDetails).toEqual({
|
||||
input: (obs1.usage_details.input + obs2.usage_details.input).toString(),
|
||||
output: (
|
||||
obs1.usage_details.output + obs2.usage_details.output
|
||||
).toString(),
|
||||
total: (obs1.usage_details.total + obs2.usage_details.total).toString(),
|
||||
});
|
||||
expect(tableRows[0].costDetails).toEqual({
|
||||
input: obs1.cost_details.input + obs2.cost_details.input,
|
||||
output: obs1.cost_details.output + obs2.cost_details.output,
|
||||
total: obs1.cost_details.total + obs2.cost_details.total,
|
||||
});
|
||||
expect(tableRows[0].level).toBeDefined();
|
||||
expect(tableRows[0].observationCount).toBeGreaterThanOrEqual(0);
|
||||
expect(tableRows[0].scoresAvg).toEqual([]);
|
||||
});
|
||||
|
||||
type TestCase = {
|
||||
traceInput: Partial<TraceRecordInsertType>;
|
||||
observationInput: Partial<ObservationRecordInsertType>[];
|
||||
filterstate: FilterState;
|
||||
expected: Partial<TracesTableReturnType>[];
|
||||
expected: Partial<TracesAllUiReturnType>[];
|
||||
};
|
||||
|
||||
[
|
||||
@@ -247,30 +225,6 @@ describe("Traces table API test", () => {
|
||||
if (expectedTrace.public !== undefined) {
|
||||
expect(tableRows[index].public).toEqual(expectedTrace.public);
|
||||
}
|
||||
if (expectedTrace.latency !== undefined) {
|
||||
expect(tableRows[index].latency).toEqual(expectedTrace.latency);
|
||||
}
|
||||
if (expectedTrace.usage_details !== undefined) {
|
||||
expect(tableRows[index].usageDetails).toEqual(
|
||||
expectedTrace.usage_details,
|
||||
);
|
||||
}
|
||||
if (expectedTrace.cost_details !== undefined) {
|
||||
expect(tableRows[index].costDetails).toEqual(
|
||||
expectedTrace.cost_details,
|
||||
);
|
||||
}
|
||||
if (expectedTrace.level !== undefined) {
|
||||
expect(tableRows[index].level).toEqual(expectedTrace.level);
|
||||
}
|
||||
if (expectedTrace.observation_count !== undefined) {
|
||||
expect(tableRows[index].observationCount).toEqual(
|
||||
expectedTrace.observation_count,
|
||||
);
|
||||
}
|
||||
if (expectedTrace.scores_avg !== undefined) {
|
||||
expect(tableRows[index].scoresAvg).toEqual(expectedTrace.scores_avg);
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -14,7 +14,7 @@ describe("observations.export RPC", () => {
|
||||
|
||||
beforeAll(async () => {
|
||||
// Disable S3 upload
|
||||
process.env.S3_BUCKET_NAME = "";
|
||||
process.env.LANGFUSE_S3_BATCH_EXPORT_ENABLED = "false";
|
||||
|
||||
await pruneDatabase();
|
||||
const traceId = "trace-1";
|
||||
|
||||
@@ -978,6 +978,39 @@ describe("/api/public/ingestion API Endpoint", () => {
|
||||
expect(dbTrace.length).toBe(1);
|
||||
});
|
||||
|
||||
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");
|
||||
});
|
||||
|
||||
it("should update all token counts if update does not contain model name", async () => {
|
||||
const traceId = v4();
|
||||
const generationId = v4();
|
||||
|
||||
@@ -157,7 +157,7 @@ describe("UI Prompts Table", () => {
|
||||
firstObservation: expect.any(Date),
|
||||
lastObservation: expect.any(Date),
|
||||
medianInputUsage: 100,
|
||||
medianLatencyMs: 0,
|
||||
medianLatencyMs: expect.any(Number),
|
||||
medianOutputUsage: 5654,
|
||||
medianTotalCost: 0,
|
||||
promptId: "some-prompt-id",
|
||||
@@ -168,7 +168,7 @@ describe("UI Prompts Table", () => {
|
||||
firstObservation: expect.any(Date),
|
||||
lastObservation: expect.any(Date),
|
||||
medianInputUsage: 100,
|
||||
medianLatencyMs: 0,
|
||||
medianLatencyMs: expect.any(Number),
|
||||
medianOutputUsage: 200,
|
||||
medianTotalCost: 0,
|
||||
promptId: "some-prompt-id",
|
||||
|
||||
@@ -0,0 +1,151 @@
|
||||
import { Lock, AlertCircle, Sparkle } from "lucide-react";
|
||||
import { Button, type ButtonProps } from "@/src/components/ui/button";
|
||||
import {
|
||||
HoverCard,
|
||||
HoverCardContent,
|
||||
HoverCardTrigger,
|
||||
} from "@/src/components/ui/hover-card";
|
||||
import { HoverCardPortal } from "@radix-ui/react-hover-card";
|
||||
import Link from "next/link";
|
||||
|
||||
const BUTTON_STATE_MESSAGES = {
|
||||
limitReached: (current: number, max: number) =>
|
||||
`You have reached the limit (${current}/${max}) for this resource at your current plan. Upgrade your plan to increase the limit.`,
|
||||
noAccess:
|
||||
"You do not have access to this resource, please ask your admin to grant you access.",
|
||||
entitlement: "This feature is not available in your current plan.",
|
||||
} as const;
|
||||
|
||||
interface ActionButtonProps extends ButtonProps {
|
||||
icon?: React.ReactNode;
|
||||
loading?: boolean;
|
||||
hasAccess?: boolean;
|
||||
hasEntitlement?: boolean;
|
||||
limitValue?: number;
|
||||
limit?: number | false;
|
||||
children: React.ReactNode;
|
||||
className?: string;
|
||||
href?: string;
|
||||
}
|
||||
|
||||
export function ActionButton({
|
||||
loading = false,
|
||||
hasAccess = true,
|
||||
hasEntitlement = true,
|
||||
limitValue,
|
||||
limit = false,
|
||||
disabled = false,
|
||||
children,
|
||||
icon,
|
||||
className,
|
||||
href,
|
||||
...buttonProps
|
||||
}: ActionButtonProps) {
|
||||
const hasReachedLimit =
|
||||
typeof limit === "number" &&
|
||||
limitValue !== undefined &&
|
||||
limitValue >= limit;
|
||||
const isDisabled =
|
||||
disabled || !hasAccess || !hasEntitlement || hasReachedLimit;
|
||||
|
||||
const getMessage = () => {
|
||||
if (!hasAccess) return BUTTON_STATE_MESSAGES.noAccess;
|
||||
if (!hasEntitlement) return BUTTON_STATE_MESSAGES.entitlement;
|
||||
if (
|
||||
hasReachedLimit &&
|
||||
typeof limit === "number" &&
|
||||
limitValue !== undefined
|
||||
) {
|
||||
return BUTTON_STATE_MESSAGES.limitReached(limitValue, limit);
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
const message = getMessage();
|
||||
|
||||
const btnContent = (
|
||||
<ButtonContent
|
||||
icon={icon}
|
||||
isDisabled={isDisabled}
|
||||
loading={loading}
|
||||
hasAccess={hasAccess}
|
||||
hasEntitlement={hasEntitlement}
|
||||
hasReachedLimit={hasReachedLimit}
|
||||
className={className}
|
||||
buttonProps={buttonProps}
|
||||
href={href}
|
||||
>
|
||||
{children}
|
||||
</ButtonContent>
|
||||
);
|
||||
|
||||
if (isDisabled && message) {
|
||||
return (
|
||||
<HoverCard openDelay={200}>
|
||||
<HoverCardTrigger asChild>
|
||||
<span>{btnContent}</span>
|
||||
</HoverCardTrigger>
|
||||
<HoverCardPortal>
|
||||
<HoverCardContent className="w-80 text-sm">
|
||||
{message}
|
||||
</HoverCardContent>
|
||||
</HoverCardPortal>
|
||||
</HoverCard>
|
||||
);
|
||||
}
|
||||
|
||||
return btnContent;
|
||||
}
|
||||
|
||||
function ButtonContent({
|
||||
icon,
|
||||
isDisabled,
|
||||
loading,
|
||||
hasAccess,
|
||||
hasEntitlement,
|
||||
hasReachedLimit,
|
||||
className,
|
||||
buttonProps,
|
||||
children,
|
||||
href,
|
||||
}: {
|
||||
icon?: React.ReactNode;
|
||||
isDisabled: boolean;
|
||||
loading: boolean;
|
||||
hasAccess: boolean;
|
||||
hasEntitlement: boolean;
|
||||
hasReachedLimit: boolean;
|
||||
className?: string;
|
||||
buttonProps: Omit<ButtonProps, "disabled" | "loading" | "className">;
|
||||
children: React.ReactNode;
|
||||
href?: string;
|
||||
}) {
|
||||
const content = (
|
||||
<>
|
||||
{!hasAccess ? (
|
||||
<Lock className="mr-1 h-4 w-4" />
|
||||
) : !hasEntitlement ? (
|
||||
<AlertCircle className="mr-1 h-4 w-4" />
|
||||
) : hasReachedLimit ? (
|
||||
<Sparkle className="mr-1 h-4 w-4" />
|
||||
) : icon ? (
|
||||
<div className="mr-1">{icon}</div>
|
||||
) : null}
|
||||
{children}
|
||||
</>
|
||||
);
|
||||
|
||||
const renderLink = href && !isDisabled;
|
||||
|
||||
return (
|
||||
<Button
|
||||
disabled={isDisabled}
|
||||
loading={loading}
|
||||
className={className}
|
||||
{...buttonProps}
|
||||
asChild={renderLink ? true : undefined}
|
||||
>
|
||||
{renderLink ? <Link href={href}>{content}</Link> : content}
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
@@ -16,7 +16,6 @@ import React from "react";
|
||||
import { api } from "@/src/utils/api";
|
||||
import { showSuccessToast } from "@/src/features/notifications/showSuccessToast";
|
||||
import { useHasProjectAccess } from "@/src/features/rbac/utils/checkProjectAccess";
|
||||
import { useHasOrgEntitlement } from "@/src/features/entitlements/hooks";
|
||||
|
||||
export type BatchExportTableButtonProps = {
|
||||
projectId: string;
|
||||
@@ -41,7 +40,6 @@ export const BatchExportTableButton: React.FC<BatchExportTableButtonProps> = (
|
||||
});
|
||||
},
|
||||
});
|
||||
const entitled = useHasOrgEntitlement("batch-export");
|
||||
const hasAccess = useHasProjectAccess({
|
||||
projectId: props.projectId,
|
||||
scope: "batchExport:create",
|
||||
@@ -61,7 +59,7 @@ export const BatchExportTableButton: React.FC<BatchExportTableButtonProps> = (
|
||||
});
|
||||
};
|
||||
|
||||
if (!entitled || !hasAccess) return null;
|
||||
if (!hasAccess) return null;
|
||||
|
||||
return (
|
||||
<DropdownMenu>
|
||||
|
||||
@@ -19,9 +19,10 @@ import {
|
||||
import { ArrowUp } from "lucide-react";
|
||||
import { api } from "@/src/utils/api";
|
||||
import { Button } from "@/src/components/ui/button";
|
||||
import { useIsEeEnabled } from "@/src/ee/utils/useIsEeEnabled";
|
||||
import { env } from "@/src/env.mjs";
|
||||
import { cn } from "@/src/utils/tailwind";
|
||||
import { usePlan } from "@/src/features/entitlements/hooks";
|
||||
import { isSelfHostedPlan, planLabels } from "@langfuse/shared";
|
||||
|
||||
export const VersionLabel = ({ className }: { className?: string }) => {
|
||||
const checkUpdate = api.public.checkUpdate.useQuery(undefined, {
|
||||
@@ -31,9 +32,24 @@ export const VersionLabel = ({ className }: { className?: string }) => {
|
||||
enabled: !env.NEXT_PUBLIC_LANGFUSE_CLOUD_REGION, // do not check for updates on Langfuse Cloud
|
||||
onError: (error) => console.error("checkUpdate error", error), // do not render default error message
|
||||
});
|
||||
const isEeVersion = useIsEeEnabled();
|
||||
const plan = usePlan();
|
||||
const isLangfuseCloud = Boolean(env.NEXT_PUBLIC_LANGFUSE_CLOUD_REGION);
|
||||
|
||||
const selfHostedPlanLabel = !isLangfuseCloud
|
||||
? plan && isSelfHostedPlan(plan)
|
||||
? // self-host plan
|
||||
{
|
||||
short: plan === "self-hosted:pro" ? "Pro" : "EE",
|
||||
long: planLabels[plan],
|
||||
}
|
||||
: // no plan, oss
|
||||
{
|
||||
short: "OSS",
|
||||
long: "Open Source",
|
||||
}
|
||||
: // null on cloud
|
||||
null;
|
||||
|
||||
const hasUpdate =
|
||||
!env.NEXT_PUBLIC_LANGFUSE_CLOUD_REGION &&
|
||||
checkUpdate.data &&
|
||||
@@ -49,9 +65,9 @@ export const VersionLabel = ({ className }: { className?: string }) => {
|
||||
return (
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button variant="ghost" size="xs" className={cn("text-xs",className)}>
|
||||
<Button variant="ghost" size="xs" className={cn("text-xs", className)}>
|
||||
{VERSION}
|
||||
{!isLangfuseCloud && (isEeVersion ? " EE" : " OSS")}
|
||||
{selfHostedPlanLabel ? ` ${selfHostedPlanLabel.short}` : null}
|
||||
{hasUpdate && <ArrowUp className={`ml-1 h-3 w-3 ${color}`} />}
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
@@ -70,11 +86,11 @@ export const VersionLabel = ({ className }: { className?: string }) => {
|
||||
<DropdownMenuSeparator />
|
||||
</>
|
||||
) : null}
|
||||
{isEeVersion && (
|
||||
{selfHostedPlanLabel && (
|
||||
<>
|
||||
<DropdownMenuLabel className="flex items-center font-normal">
|
||||
<BadgeCheck size={16} className="mr-2" />
|
||||
Enterprise Edition
|
||||
{selfHostedPlanLabel.long}
|
||||
</DropdownMenuLabel>
|
||||
<DropdownMenuSeparator />
|
||||
</>
|
||||
@@ -100,12 +116,9 @@ export const VersionLabel = ({ className }: { className?: string }) => {
|
||||
Roadmap
|
||||
</Link>
|
||||
</DropdownMenuItem>
|
||||
{!isLangfuseCloud && !isEeVersion && (
|
||||
{!isLangfuseCloud && (
|
||||
<DropdownMenuItem asChild>
|
||||
<Link
|
||||
href="https://langfuse.com/docs/deployment/feature-overview"
|
||||
target="_blank"
|
||||
>
|
||||
<Link href="https://langfuse.com/pricing-self-host" target="_blank">
|
||||
<Info size={16} className="mr-2" />
|
||||
Compare Versions
|
||||
</Link>
|
||||
|
||||
@@ -33,7 +33,7 @@ export function AppSidebar({
|
||||
return (
|
||||
<Sidebar collapsible="icon" variant="sidebar" {...props}>
|
||||
<SidebarHeader>
|
||||
<div className="flex items-center gap-2 p-2">
|
||||
<div className="flex items-center gap-2 p-2 pr-0">
|
||||
<LangfuseLogo version />
|
||||
</div>
|
||||
<DemoBadge />
|
||||
|
||||
@@ -90,6 +90,7 @@ export type DatePickerWithRangeProps = {
|
||||
dateRange?: DashboardDateRange;
|
||||
className?: string;
|
||||
selectedOption: DashboardDateRangeOptions;
|
||||
disabled?: React.ComponentProps<typeof Calendar>["disabled"];
|
||||
setDateRangeAndOption: (
|
||||
option: DashboardDateRangeOptions,
|
||||
date?: DashboardDateRange,
|
||||
@@ -101,6 +102,7 @@ export function DatePickerWithRange({
|
||||
dateRange,
|
||||
selectedOption,
|
||||
setDateRangeAndOption,
|
||||
disabled,
|
||||
}: DatePickerWithRangeProps) {
|
||||
const [internalDateRange, setInternalDateRange] = useState<
|
||||
DateRange | undefined
|
||||
@@ -215,6 +217,7 @@ export function DatePickerWithRange({
|
||||
selected={internalDateRange}
|
||||
onSelect={onCalendarSelection}
|
||||
numberOfMonths={isSmallScreen ? 1 : 2}
|
||||
disabled={disabled}
|
||||
/>
|
||||
{!isSmallScreen && (
|
||||
<div className="flex flex-row border-t-2 py-1.5">
|
||||
|
||||
@@ -17,18 +17,30 @@ import {
|
||||
type DashboardDateRange,
|
||||
TABLE_AGGREGATION_OPTIONS,
|
||||
getDateFromOption,
|
||||
isTableDataRangeOptionAvailable,
|
||||
isDashboardDateRangeOptionAvailable,
|
||||
} from "@/src/utils/date-range-utils";
|
||||
import { Clock } from "lucide-react";
|
||||
import { useEntitlementLimit } from "@/src/features/entitlements/hooks";
|
||||
import { useMemo } from "react";
|
||||
import {
|
||||
HoverCard,
|
||||
HoverCardContent,
|
||||
HoverCardTrigger,
|
||||
} from "@/src/components/ui/hover-card";
|
||||
import { HoverCardPortal } from "@radix-ui/react-hover-card";
|
||||
|
||||
type BaseDateRangeDropdownProps<T> = {
|
||||
selectedOption: T;
|
||||
options: readonly T[];
|
||||
limitedOptions?: readonly T[];
|
||||
onSelectionChange: (value: T) => void;
|
||||
};
|
||||
|
||||
const BaseDateRangeDropdown = <T extends string>({
|
||||
selectedOption,
|
||||
options,
|
||||
limitedOptions,
|
||||
onSelectionChange,
|
||||
}: BaseDateRangeDropdownProps<T>) => {
|
||||
return (
|
||||
@@ -38,11 +50,33 @@ const BaseDateRangeDropdown = <T extends string>({
|
||||
<SelectValue placeholder="Select" />
|
||||
</SelectTrigger>
|
||||
<SelectContent position="popper" defaultValue={60}>
|
||||
{options.map((item) => (
|
||||
<SelectItem key={item} value={item}>
|
||||
{item}
|
||||
</SelectItem>
|
||||
))}
|
||||
{options.map((item) => {
|
||||
const itemObj = (
|
||||
<SelectItem
|
||||
key={item}
|
||||
value={item}
|
||||
disabled={limitedOptions?.includes(item)}
|
||||
>
|
||||
{item}
|
||||
</SelectItem>
|
||||
);
|
||||
const isLimited = limitedOptions?.includes(item);
|
||||
|
||||
return isLimited ? (
|
||||
<HoverCard openDelay={200}>
|
||||
<HoverCardTrigger asChild>
|
||||
<span>{itemObj}</span>
|
||||
</HoverCardTrigger>
|
||||
<HoverCardPortal>
|
||||
<HoverCardContent className="w-60 text-sm" side="right">
|
||||
This time range is not available in your current plan.
|
||||
</HoverCardContent>
|
||||
</HoverCardPortal>
|
||||
</HoverCard>
|
||||
) : (
|
||||
itemObj
|
||||
);
|
||||
})}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
);
|
||||
@@ -59,6 +93,17 @@ type DashboardDateRangeDropdownProps = {
|
||||
export const DashboardDateRangeDropdown: React.FC<
|
||||
DashboardDateRangeDropdownProps
|
||||
> = ({ selectedOption, setDateRangeAndOption }) => {
|
||||
const lookbackLimit = useEntitlementLimit("data-access-days");
|
||||
const disabledOptions = useMemo(() => {
|
||||
return DASHBOARD_AGGREGATION_OPTIONS.filter(
|
||||
(option) =>
|
||||
!isDashboardDateRangeOptionAvailable({
|
||||
option,
|
||||
limitDays: lookbackLimit,
|
||||
}),
|
||||
);
|
||||
}, [lookbackLimit]);
|
||||
|
||||
const onDropDownSelection = (value: DashboardDateRangeOptions) => {
|
||||
if (value === DASHBOARD_AGGREGATION_PLACEHOLDER) {
|
||||
setDateRangeAndOption(DASHBOARD_AGGREGATION_PLACEHOLDER, undefined);
|
||||
@@ -82,6 +127,7 @@ export const DashboardDateRangeDropdown: React.FC<
|
||||
<BaseDateRangeDropdown
|
||||
selectedOption={selectedOption}
|
||||
options={options}
|
||||
limitedOptions={disabledOptions}
|
||||
onSelectionChange={onDropDownSelection}
|
||||
/>
|
||||
);
|
||||
@@ -99,6 +145,14 @@ export const TableDateRangeDropdown: React.FC<TableDateRangeDropdownProps> = ({
|
||||
selectedOption,
|
||||
setDateRangeAndOption,
|
||||
}) => {
|
||||
const lookbackLimit = useEntitlementLimit("data-access-days");
|
||||
const disabledOptions = useMemo(() => {
|
||||
return TABLE_AGGREGATION_OPTIONS.filter(
|
||||
(option) =>
|
||||
!isTableDataRangeOptionAvailable({ option, limitDays: lookbackLimit }),
|
||||
);
|
||||
}, [lookbackLimit]);
|
||||
|
||||
const onDropDownSelection = (value: TableDateRangeOptions) => {
|
||||
const dateFromOption = getDateFromOption({
|
||||
filterSource: "TABLE",
|
||||
@@ -116,6 +170,7 @@ export const TableDateRangeDropdown: React.FC<TableDateRangeDropdownProps> = ({
|
||||
<BaseDateRangeDropdown
|
||||
selectedOption={selectedOption}
|
||||
options={TABLE_AGGREGATION_OPTIONS}
|
||||
limitedOptions={disabledOptions}
|
||||
onSelectionChange={onDropDownSelection}
|
||||
/>
|
||||
);
|
||||
|
||||
@@ -11,6 +11,8 @@ import { useHasProjectAccess } from "@/src/features/rbac/utils/checkProjectAcces
|
||||
import { type ProjectScope } from "@/src/features/rbac/constants/projectAccessRights";
|
||||
import { api } from "@/src/utils/api";
|
||||
import { usePostHogClientCapture } from "@/src/features/posthog-analytics/usePostHogClientCapture";
|
||||
import { Input } from "@/src/components/ui/input";
|
||||
import { Label } from "@/src/components/ui/label";
|
||||
|
||||
interface DeleteButtonProps {
|
||||
itemId: string;
|
||||
@@ -20,6 +22,7 @@ interface DeleteButtonProps {
|
||||
invalidateFunc: () => void;
|
||||
type: "trace" | "dataset";
|
||||
redirectUrl?: string;
|
||||
deleteConfirmation?: string;
|
||||
}
|
||||
|
||||
export function DeleteButton({
|
||||
@@ -30,10 +33,12 @@ export function DeleteButton({
|
||||
invalidateFunc,
|
||||
type,
|
||||
redirectUrl,
|
||||
deleteConfirmation,
|
||||
}: DeleteButtonProps) {
|
||||
const [isDeleted, setIsDeleted] = useState(false);
|
||||
const router = useRouter();
|
||||
const capture = usePostHogClientCapture();
|
||||
const [deleteConfirmationInput, setDeleteConfirmationInput] = useState("");
|
||||
|
||||
const hasAccess = useHasProjectAccess({ projectId, scope: scope });
|
||||
const traceMutation = api.traces.deleteMany.useMutation({
|
||||
@@ -79,6 +84,18 @@ export function DeleteButton({
|
||||
This action cannot be undone and removes all the data associated with
|
||||
this {type}.
|
||||
</p>
|
||||
{deleteConfirmation && (
|
||||
<div className="mb-4 grid w-full gap-1.5">
|
||||
<Label htmlFor="delete-confirmation">
|
||||
Type "{deleteConfirmation}" to confirm
|
||||
</Label>
|
||||
<Input
|
||||
id="delete-confirmation"
|
||||
value={deleteConfirmationInput}
|
||||
onChange={(e) => setDeleteConfirmationInput(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
<div className="flex justify-end space-x-4">
|
||||
{type === "trace" ? (
|
||||
<Button
|
||||
@@ -86,6 +103,10 @@ export function DeleteButton({
|
||||
variant="destructive"
|
||||
loading={traceMutation.isLoading || isDeleted}
|
||||
onClick={() => {
|
||||
if (deleteConfirmationInput !== deleteConfirmation) {
|
||||
alert("Please type the correct confirmation");
|
||||
return;
|
||||
}
|
||||
void traceMutation.mutateAsync({
|
||||
traceIds: [itemId],
|
||||
projectId,
|
||||
@@ -103,6 +124,10 @@ export function DeleteButton({
|
||||
variant="destructive"
|
||||
loading={datasetMutation.isLoading || isDeleted}
|
||||
onClick={() => {
|
||||
if (deleteConfirmationInput !== deleteConfirmation) {
|
||||
alert("Please type the correct confirmation");
|
||||
return;
|
||||
}
|
||||
void datasetMutation.mutateAsync({
|
||||
projectId,
|
||||
datasetId: itemId,
|
||||
|
||||
@@ -13,10 +13,15 @@ export const ErrorPage = ({
|
||||
}: {
|
||||
title?: string;
|
||||
message: string;
|
||||
additionalButton?: {
|
||||
label: string;
|
||||
href: string;
|
||||
};
|
||||
additionalButton?:
|
||||
| {
|
||||
label: string;
|
||||
href: string;
|
||||
}
|
||||
| {
|
||||
label: string;
|
||||
onClick: () => void;
|
||||
};
|
||||
}) => {
|
||||
const session = useSession();
|
||||
const router = useRouter();
|
||||
@@ -40,9 +45,15 @@ export const ErrorPage = ({
|
||||
</Button>
|
||||
) : null}
|
||||
{additionalButton ? (
|
||||
<Button variant="secondary" asChild>
|
||||
<Link href={additionalButton.href}>{additionalButton.label}</Link>
|
||||
</Button>
|
||||
"onClick" in additionalButton ? (
|
||||
<Button variant="secondary" onClick={additionalButton.onClick}>
|
||||
{additionalButton.label}
|
||||
</Button>
|
||||
) : (
|
||||
<Button variant="secondary" asChild>
|
||||
<Link href={additionalButton.href}>{additionalButton.label}</Link>
|
||||
</Button>
|
||||
)
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -10,7 +10,7 @@ import { Toaster } from "@/src/components/ui/sonner";
|
||||
import DOMPurify from "dompurify";
|
||||
import { ThemeToggle } from "@/src/features/theming/ThemeToggle";
|
||||
import { useQueryProjectOrOrganization } from "@/src/features/projects/hooks";
|
||||
import { useOrgEntitlements } from "@/src/features/entitlements/hooks";
|
||||
import { useEntitlements } from "@/src/features/entitlements/hooks";
|
||||
import { useUiCustomization } from "@/src/ee/features/ui-customization/useUiCustomization";
|
||||
import { hasOrganizationAccess } from "@/src/features/rbac/utils/checkOrganizationAccess";
|
||||
import { ClickhouseAdminToggle } from "@/src/components/layouts/ClickhouseAdminToggle";
|
||||
@@ -105,7 +105,7 @@ export default function Layout(props: PropsWithChildren) {
|
||||
const enableExperimentalFeatures =
|
||||
session.data?.environment.enableExperimentalFeatures ?? false;
|
||||
|
||||
const entitlements = useOrgEntitlements();
|
||||
const entitlements = useEntitlements();
|
||||
|
||||
const uiCustomization = useUiCustomization();
|
||||
|
||||
|
||||
@@ -37,7 +37,11 @@ export const SessionPage: React.FC<{
|
||||
},
|
||||
{
|
||||
retry(failureCount, error) {
|
||||
if (error.data?.code === "UNAUTHORIZED") return false;
|
||||
if (
|
||||
error.data?.code === "UNAUTHORIZED" ||
|
||||
error.data?.code === "NOT_FOUND"
|
||||
)
|
||||
return false;
|
||||
return failureCount < 3;
|
||||
},
|
||||
},
|
||||
@@ -68,6 +72,18 @@ export const SessionPage: React.FC<{
|
||||
if (session.error?.data?.code === "UNAUTHORIZED")
|
||||
return <ErrorPage message="You do not have access to this session." />;
|
||||
|
||||
if (session.error?.data?.code === "NOT_FOUND")
|
||||
return (
|
||||
<ErrorPage
|
||||
title="Session not found"
|
||||
message="The session is either still being processed or has been deleted."
|
||||
additionalButton={{
|
||||
label: "Retry",
|
||||
onClick: () => void window.location.reload(),
|
||||
}}
|
||||
/>
|
||||
);
|
||||
|
||||
return (
|
||||
<ScrollScreenPage>
|
||||
<Header
|
||||
@@ -118,7 +134,7 @@ export const SessionPage: React.FC<{
|
||||
userId ?? "",
|
||||
)}`}
|
||||
>
|
||||
<Badge>User ID: {userId}</Badge>
|
||||
<Badge className="max-w-[300px] truncate">User ID: {userId}</Badge>
|
||||
</Link>
|
||||
))}
|
||||
<Badge variant="outline">Traces: {session.data?.traces.length}</Badge>
|
||||
@@ -134,7 +150,9 @@ export const SessionPage: React.FC<{
|
||||
className="group grid gap-3 border-border p-2 shadow-none hover:border-ring md:grid-cols-3"
|
||||
key={trace.id}
|
||||
>
|
||||
<SessionIO traceId={trace.id} projectId={projectId} />
|
||||
<div className="col-span-2 overflow-hidden">
|
||||
<SessionIO traceId={trace.id} projectId={projectId} />
|
||||
</div>
|
||||
<div className="-mt-1 p-1 opacity-50 transition-opacity group-hover:opacity-100">
|
||||
<Link
|
||||
href={`/project/${projectId}/traces/${trace.id}`}
|
||||
@@ -195,11 +213,11 @@ const SessionIO = ({
|
||||
skipBatch: true,
|
||||
},
|
||||
},
|
||||
refetchOnMount: false, // prevents refetching loops
|
||||
refetchOnMount: false,
|
||||
},
|
||||
);
|
||||
return (
|
||||
<div className="col-span-2 flex flex-col gap-2 p-0">
|
||||
<div className="flex w-full flex-col gap-2 overflow-hidden p-0">
|
||||
{!trace.data ? (
|
||||
<JsonSkeleton
|
||||
className="h-full w-full overflow-hidden px-2 py-1"
|
||||
|
||||
+2
-2
@@ -39,7 +39,7 @@ import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { z } from "zod";
|
||||
import { useSession } from "next-auth/react";
|
||||
import { useHasOrgEntitlement } from "@/src/features/entitlements/hooks";
|
||||
import { useHasEntitlement } from "@/src/features/entitlements/hooks";
|
||||
|
||||
const addToQueueFormSchema = z.object({
|
||||
queueId: z.string(),
|
||||
@@ -71,7 +71,7 @@ export function TraceTableMultiSelectAction({
|
||||
},
|
||||
});
|
||||
|
||||
const hasEntitlement = useHasOrgEntitlement("annotation-queues");
|
||||
const hasEntitlement = useHasEntitlement("annotation-queues");
|
||||
const hasAddToQueueAccess = useHasProjectAccess({
|
||||
projectId,
|
||||
scope: "annotationQueues:CUD",
|
||||
|
||||
@@ -46,7 +46,7 @@ import { useIndividualScoreColumns } from "@/src/features/scores/hooks/useIndivi
|
||||
import TagList from "@/src/features/tag/components/TagList";
|
||||
import useColumnOrder from "@/src/features/column-visibility/hooks/useColumnOrder";
|
||||
import { BatchExportTableButton } from "@/src/components/BatchExportTableButton";
|
||||
import { useOrganizationPlan } from "@/src/features/entitlements/hooks";
|
||||
import { usePlan } from "@/src/features/entitlements/hooks";
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
@@ -104,7 +104,7 @@ export default function GenerationsTable({
|
||||
omittedFilter = [],
|
||||
}: GenerationsTableProps) {
|
||||
const capture = usePostHogClientCapture();
|
||||
const plan = useOrganizationPlan();
|
||||
const plan = usePlan();
|
||||
const [isExporting, setIsExporting] = useState(false);
|
||||
const [searchQuery, setSearchQuery] = useQueryParam(
|
||||
"search",
|
||||
|
||||
@@ -17,6 +17,7 @@ import {
|
||||
} from "@/src/components/schemas/ChatMlSchema";
|
||||
import { useMarkdownContext } from "@/src/features/theming/useMarkdownContext";
|
||||
import { type MediaReturnType } from "@/src/features/media/validation";
|
||||
import { LangfuseMediaView } from "@/src/components/ui/LangfuseMediaView";
|
||||
|
||||
const isSupportedMarkdownFormat = (
|
||||
content: unknown,
|
||||
@@ -67,6 +68,7 @@ export function MarkdownOrJsonView({
|
||||
className={className}
|
||||
customCodeHeaderClassName={customCodeHeaderClassName}
|
||||
audio={audio}
|
||||
media={media}
|
||||
/>
|
||||
) : (
|
||||
<JSONView
|
||||
@@ -148,6 +150,14 @@ export const IOPreview: React.FC<{
|
||||
const isPrettyViewAvailable =
|
||||
inChatMlArray.success || inMarkdown.success || outMarkdown.success;
|
||||
|
||||
// If there are additional input fields beyond the messages, render them
|
||||
const additionalInput =
|
||||
typeof input === "object"
|
||||
? Object.fromEntries(
|
||||
Object.entries(input as object).filter(([key]) => key !== "messages"),
|
||||
)
|
||||
: undefined;
|
||||
|
||||
// default I/O
|
||||
return (
|
||||
<>
|
||||
@@ -188,6 +198,12 @@ export const IOPreview: React.FC<{
|
||||
]),
|
||||
]}
|
||||
shouldRenderMarkdown
|
||||
additionalInput={
|
||||
Object.keys(additionalInput ?? {}).length > 0
|
||||
? additionalInput
|
||||
: undefined
|
||||
}
|
||||
media={media ?? []}
|
||||
/>
|
||||
) : (
|
||||
<>
|
||||
@@ -241,7 +257,15 @@ export const OpenAiMessageView: React.FC<{
|
||||
messages: z.infer<typeof ChatMlArraySchema>;
|
||||
title?: string;
|
||||
shouldRenderMarkdown?: boolean;
|
||||
}> = ({ title, messages, shouldRenderMarkdown = false }) => {
|
||||
media?: MediaReturnType[];
|
||||
additionalInput?: Record<string, unknown>;
|
||||
}> = ({
|
||||
title,
|
||||
messages,
|
||||
shouldRenderMarkdown = false,
|
||||
media,
|
||||
additionalInput,
|
||||
}) => {
|
||||
const COLLAPSE_THRESHOLD = 3;
|
||||
const [isCollapsed, setCollapsed] = useState(
|
||||
messages.length > COLLAPSE_THRESHOLD ? true : null,
|
||||
@@ -330,6 +354,27 @@ export const OpenAiMessageView: React.FC<{
|
||||
</Fragment>
|
||||
))}
|
||||
</div>
|
||||
{additionalInput && (
|
||||
<div className="p-3 pt-1">
|
||||
<JSONView title="Additional Input" json={additionalInput} />
|
||||
</div>
|
||||
)}
|
||||
{media && media.length > 0 && (
|
||||
<>
|
||||
<div className="mx-3 border-t px-2 py-1 text-xs text-muted-foreground">
|
||||
Media
|
||||
</div>
|
||||
<div className="flex flex-wrap gap-2 p-4 pt-1">
|
||||
{media.map((m) => (
|
||||
<LangfuseMediaView
|
||||
mediaAPIReturnValue={m}
|
||||
asFileIcon={true}
|
||||
key={m.mediaId}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -28,7 +28,7 @@ import { CommentDrawerButton } from "@/src/features/comments/CommentDrawerButton
|
||||
import { cn } from "@/src/utils/tailwind";
|
||||
import { NewDatasetItemFromTrace } from "@/src/features/datasets/components/NewDatasetItemFromObservationButton";
|
||||
import { CreateNewAnnotationQueueItem } from "@/src/ee/features/annotation-queues/components/CreateNewAnnotationQueueItem";
|
||||
import { useHasOrgEntitlement } from "@/src/features/entitlements/hooks";
|
||||
import { useHasEntitlement } from "@/src/features/entitlements/hooks";
|
||||
import { calculateDisplayTotalCost } from "@/src/components/trace/lib/helpers";
|
||||
import { useMemo } from "react";
|
||||
import { useIsAuthenticatedAndProjectMember } from "@/src/features/auth/hooks";
|
||||
@@ -65,7 +65,7 @@ export const ObservationPreview = ({
|
||||
const [emptySelectedConfigIds, setEmptySelectedConfigIds] = useLocalStorage<
|
||||
string[]
|
||||
>("emptySelectedConfigIds", []);
|
||||
const hasEntitlement = useHasOrgEntitlement("annotation-queues");
|
||||
const hasEntitlement = useHasEntitlement("annotation-queues");
|
||||
const isAuthenticatedAndProjectMember =
|
||||
useIsAuthenticatedAndProjectMember(projectId);
|
||||
|
||||
|
||||
@@ -28,7 +28,7 @@ import { api } from "@/src/utils/api";
|
||||
import { cn } from "@/src/utils/tailwind";
|
||||
import { NewDatasetItemFromTrace } from "@/src/features/datasets/components/NewDatasetItemFromObservationButton";
|
||||
import { CreateNewAnnotationQueueItem } from "@/src/ee/features/annotation-queues/components/CreateNewAnnotationQueueItem";
|
||||
import { useHasOrgEntitlement } from "@/src/features/entitlements/hooks";
|
||||
import { useHasEntitlement } from "@/src/features/entitlements/hooks";
|
||||
import { useMemo } from "react";
|
||||
import { usdFormatter } from "@/src/utils/numbers";
|
||||
import { calculateDisplayTotalCost } from "@/src/components/trace/lib/helpers";
|
||||
@@ -61,7 +61,7 @@ export const TracePreview = ({
|
||||
const [emptySelectedConfigIds, setEmptySelectedConfigIds] = useLocalStorage<
|
||||
string[]
|
||||
>("emptySelectedConfigIds", []);
|
||||
const hasEntitlement = useHasOrgEntitlement("annotation-queues");
|
||||
const hasEntitlement = useHasEntitlement("annotation-queues");
|
||||
const isAuthenticatedAndProjectMember = useIsAuthenticatedAndProjectMember(
|
||||
trace.projectId,
|
||||
);
|
||||
|
||||
@@ -138,7 +138,7 @@ function TreeItemInner({
|
||||
<PanelRightOpen className="h-4 w-4"></PanelRightOpen>
|
||||
</Button>
|
||||
</DrawerTrigger>
|
||||
<DrawerContent className="w-full overflow-hidden" size="md">
|
||||
<DrawerContent className="overflow-hidden" size="md">
|
||||
{children}
|
||||
</DrawerContent>
|
||||
</Drawer>
|
||||
|
||||
@@ -268,7 +268,11 @@ export function TracePage({
|
||||
},
|
||||
{
|
||||
retry(failureCount, error) {
|
||||
if (error.data?.code === "UNAUTHORIZED") return false;
|
||||
if (
|
||||
error.data?.code === "UNAUTHORIZED" ||
|
||||
error.data?.code === "NOT_FOUND"
|
||||
)
|
||||
return false;
|
||||
return failureCount < 3;
|
||||
},
|
||||
},
|
||||
@@ -309,7 +313,21 @@ export function TracePage({
|
||||
|
||||
if (trace.error?.data?.code === "UNAUTHORIZED")
|
||||
return <ErrorPage message="You do not have access to this trace." />;
|
||||
|
||||
if (trace.error?.data?.code === "NOT_FOUND")
|
||||
return (
|
||||
<ErrorPage
|
||||
title="Trace not found"
|
||||
message="The trace is either still being processed or has been deleted."
|
||||
additionalButton={{
|
||||
label: "Retry",
|
||||
onClick: () => void window.location.reload(),
|
||||
}}
|
||||
/>
|
||||
);
|
||||
|
||||
if (!trace.data) return <div>loading...</div>;
|
||||
|
||||
return (
|
||||
<FullScreenPage>
|
||||
<Header
|
||||
|
||||
@@ -8,7 +8,7 @@ import {
|
||||
import { ResizableImage } from "@/src/components/ui/resizable-image";
|
||||
import useProjectIdFromURL from "@/src/hooks/useProjectIdFromURL";
|
||||
import {
|
||||
MediaContentType,
|
||||
type MediaContentType,
|
||||
type MediaReturnType,
|
||||
} from "@/src/features/media/validation";
|
||||
import { FileIcon, ImageIcon, SpeakerLoudIcon } from "@radix-ui/react-icons";
|
||||
@@ -78,32 +78,22 @@ export const LangfuseMediaView = ({
|
||||
return <FileViewer src={mediaUrl} contentType={mediaData.type} />;
|
||||
}
|
||||
|
||||
switch (mediaData.type) {
|
||||
case MediaContentType.JPEG:
|
||||
case MediaContentType.PNG:
|
||||
case MediaContentType.WEBP:
|
||||
return (
|
||||
<div>
|
||||
<ResizableImage
|
||||
src={mediaUrl}
|
||||
isDefaultVisible={true}
|
||||
shouldValidateImageSource={false}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
case MediaContentType.MP3:
|
||||
case MediaContentType.MP3_LEGACY:
|
||||
case MediaContentType.WAV:
|
||||
return <AudioPlayer src={mediaUrl} />;
|
||||
|
||||
case MediaContentType.PDF:
|
||||
return <FileViewer src={mediaUrl} contentType={mediaData.type} />;
|
||||
|
||||
case MediaContentType.TXT:
|
||||
return <FileViewer src={mediaUrl} contentType={mediaData.type} />;
|
||||
|
||||
default:
|
||||
return null;
|
||||
if (mediaData.type.startsWith("image")) {
|
||||
return (
|
||||
<div>
|
||||
<ResizableImage
|
||||
src={mediaUrl}
|
||||
isDefaultVisible={true}
|
||||
shouldValidateImageSource={false}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
} else if (mediaData.type.startsWith("audio")) {
|
||||
return <AudioPlayer src={mediaUrl} />;
|
||||
} else if (mediaData.type.startsWith("video")) {
|
||||
return <VideoPlayer src={mediaUrl} />;
|
||||
} else {
|
||||
return <FileViewer src={mediaUrl} contentType={mediaData.type} />;
|
||||
}
|
||||
};
|
||||
|
||||
@@ -161,3 +151,14 @@ function AudioPlayer({ src }: { src?: string }) {
|
||||
</audio>
|
||||
);
|
||||
}
|
||||
|
||||
function VideoPlayer({ src }: { src?: string }) {
|
||||
if (!src) return null;
|
||||
|
||||
return (
|
||||
<video controls className="w-full" preload="metadata">
|
||||
<source src={src} />
|
||||
Your browser does not support the video element.
|
||||
</video>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -31,6 +31,7 @@ import {
|
||||
import { type z } from "zod";
|
||||
import { ResizableImage } from "@/src/components/ui/resizable-image";
|
||||
import { LangfuseMediaView } from "@/src/components/ui/LangfuseMediaView";
|
||||
import { type MediaReturnType } from "@/src/features/media/validation";
|
||||
|
||||
type ReactMarkdownNode = ReactMarkdownExtraProps["node"];
|
||||
type ReactMarkdownNodeChildren = Exclude<
|
||||
@@ -233,12 +234,14 @@ export function MarkdownView({
|
||||
className,
|
||||
customCodeHeaderClassName,
|
||||
audio,
|
||||
media,
|
||||
}: {
|
||||
markdown: string | z.infer<typeof OpenAIContentSchema>;
|
||||
title?: string;
|
||||
className?: string;
|
||||
customCodeHeaderClassName?: string;
|
||||
audio?: OpenAIOutputAudioType;
|
||||
media?: MediaReturnType[];
|
||||
}) {
|
||||
const [isCopied, setIsCopied] = useState(false);
|
||||
const capture = usePostHogClientCapture();
|
||||
@@ -364,6 +367,22 @@ export function MarkdownView({
|
||||
</>
|
||||
) : null}
|
||||
</div>
|
||||
{media && media.length > 0 && (
|
||||
<>
|
||||
<div className="mx-3 border-t px-2 py-1 text-xs text-muted-foreground">
|
||||
Media
|
||||
</div>
|
||||
<div className="flex flex-wrap gap-2 p-4 pt-1">
|
||||
{media.map((m) => (
|
||||
<LangfuseMediaView
|
||||
mediaAPIReturnValue={m}
|
||||
asFileIcon={true}
|
||||
key={m.mediaId}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -13,12 +13,14 @@ function Calendar({
|
||||
className,
|
||||
classNames,
|
||||
showOutsideDays = true,
|
||||
disabled,
|
||||
...props
|
||||
}: CalendarProps) {
|
||||
return (
|
||||
<DayPicker
|
||||
showOutsideDays={showOutsideDays}
|
||||
className={cn("p-3", className)}
|
||||
disabled={disabled}
|
||||
classNames={{
|
||||
months: "flex flex-col sm:flex-row space-y-4 sm:space-x-4 sm:space-y-0",
|
||||
month: "space-y-4",
|
||||
|
||||
@@ -1 +1 @@
|
||||
export const VERSION = "v2.92.0";
|
||||
export const VERSION = "v3.0.0-rc.3";
|
||||
|
||||
@@ -3,7 +3,7 @@ import Header from "@/src/components/layouts/header";
|
||||
import { Tabs, TabsList, TabsTrigger } from "@/src/components/ui/tabs";
|
||||
import useSessionStorage from "@/src/components/useSessionStorage";
|
||||
import { SupportOrUpgradePage } from "@/src/ee/features/billing/components/SupportOrUpgradePage";
|
||||
import { useHasOrgEntitlement } from "@/src/features/entitlements/hooks";
|
||||
import { useHasEntitlement } from "@/src/features/entitlements/hooks";
|
||||
import { useHasProjectAccess } from "@/src/features/rbac/utils/checkProjectAccess";
|
||||
import { AnnotationQueueItemPage } from "@/src/ee/features/annotation-queues/components/AnnotationQueueItemPage";
|
||||
import { api } from "@/src/utils/api";
|
||||
@@ -22,7 +22,7 @@ export const AnnotationQueuesItem = ({
|
||||
projectId,
|
||||
scope: "annotationQueues:read",
|
||||
});
|
||||
const hasEntitlement = useHasOrgEntitlement("annotation-queues");
|
||||
const hasEntitlement = useHasEntitlement("annotation-queues");
|
||||
|
||||
const queue = api.annotationQueues.byId.useQuery(
|
||||
{
|
||||
|
||||
+20
-63
@@ -19,7 +19,7 @@ import {
|
||||
import { Input } from "@/src/components/ui/input";
|
||||
import { useHasProjectAccess } from "@/src/features/rbac/utils/checkProjectAccess";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { CircleAlert, Edit, LockIcon, PlusIcon } from "lucide-react";
|
||||
import { Edit, PlusIcon } from "lucide-react";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { Form } from "@/src/components/ui/form";
|
||||
import { Textarea } from "@/src/components/ui/textarea";
|
||||
@@ -34,12 +34,11 @@ import { MultiSelectKeyValues } from "@/src/features/scores/components/multi-sel
|
||||
import { CommandItem } from "@/src/components/ui/command";
|
||||
import { useRouter } from "next/router";
|
||||
import { usePostHogClientCapture } from "@/src/features/posthog-analytics/usePostHogClientCapture";
|
||||
import { useOrganizationPlan } from "@/src/features/entitlements/hooks";
|
||||
import {
|
||||
HoverCard,
|
||||
HoverCardContent,
|
||||
HoverCardTrigger,
|
||||
} from "@/src/components/ui/hover-card";
|
||||
useHasEntitlement,
|
||||
useEntitlementLimit,
|
||||
} from "@/src/features/entitlements/hooks";
|
||||
import { ActionButton } from "@/src/components/ActionButton";
|
||||
|
||||
export const CreateOrEditAnnotationQueueButton = ({
|
||||
projectId,
|
||||
@@ -55,8 +54,9 @@ export const CreateOrEditAnnotationQueueButton = ({
|
||||
projectId: projectId,
|
||||
scope: "annotationQueues:CUD",
|
||||
});
|
||||
const hasEntitlement = useHasEntitlement("annotation-queues");
|
||||
const queueLimit = useEntitlementLimit("annotation-queue-count");
|
||||
const router = useRouter();
|
||||
const plan = useOrganizationPlan();
|
||||
const capture = usePostHogClientCapture();
|
||||
|
||||
const queueQuery = api.annotationQueues.byId.useQuery(
|
||||
@@ -119,55 +119,6 @@ export const CreateOrEditAnnotationQueueButton = ({
|
||||
|
||||
const configs = configsData.data?.configs ?? [];
|
||||
|
||||
if (!hasAccess) {
|
||||
return (
|
||||
<Button variant={variant} disabled={true} className="justify-start">
|
||||
<LockIcon className="-ml-0.5 mr-1.5 h-4 w-4" aria-hidden="true" />
|
||||
<span className="text-sm">{queueId ? "Edit" : "New queue"}</span>
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
|
||||
if (queueCountData.isLoading) return null;
|
||||
|
||||
// gate cloud hobby usage of annotation queue
|
||||
if (plan === "cloud:hobby" && !!queueCountData.data && !queueId) {
|
||||
return (
|
||||
<HoverCard>
|
||||
<HoverCardTrigger asChild>
|
||||
<Button
|
||||
variant={variant}
|
||||
className="relative grid grid-flow-row items-start justify-start overflow-hidden py-0 disabled:cursor-default"
|
||||
disabled
|
||||
>
|
||||
<div className="mt-2 flex h-6 flex-row items-center justify-center">
|
||||
<PlusIcon className="mr-1.5 h-4 w-4" aria-hidden="true" />
|
||||
<span className="text-sm">New queue</span>
|
||||
</div>
|
||||
<div className="absolute top-0 flex h-3 w-full items-center justify-center bg-primary-accent">
|
||||
<CircleAlert
|
||||
className="mr-1 h-2 w-2 text-white"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
<span className="text-xs text-white">At usage limit</span>
|
||||
</div>
|
||||
</Button>
|
||||
</HoverCardTrigger>
|
||||
<HoverCardContent className="w-80" align="start" side="right">
|
||||
<div className="flex justify-between space-x-4">
|
||||
<div className="space-y-1">
|
||||
<h4 className="text-sm font-semibold">Usage Limit Reached</h4>
|
||||
<p className="text-xs">
|
||||
You have reached the maximum number of annotation queues allowed
|
||||
on the Hobby plan. Upgrade your plan to create more queues.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</HoverCardContent>
|
||||
</HoverCard>
|
||||
);
|
||||
}
|
||||
|
||||
const onSubmit = (data: CreateQueue) => {
|
||||
if (queueId) {
|
||||
editQueueMutation.mutateAsync({
|
||||
@@ -202,20 +153,26 @@ export const CreateOrEditAnnotationQueueButton = ({
|
||||
return (
|
||||
<Dialog open={isOpen} onOpenChange={setIsOpen}>
|
||||
<DialogTrigger asChild>
|
||||
<Button
|
||||
<ActionButton
|
||||
variant={variant}
|
||||
onClick={() => setIsOpen(true)}
|
||||
className="justify-start"
|
||||
icon={
|
||||
queueId ? (
|
||||
<Edit className="h-4 w-4" aria-hidden="true" />
|
||||
) : (
|
||||
<PlusIcon className="h-4 w-4" aria-hidden="true" />
|
||||
)
|
||||
}
|
||||
hasAccess={hasAccess}
|
||||
hasEntitlement={hasEntitlement}
|
||||
limitValue={queueCountData.data}
|
||||
limit={queueLimit}
|
||||
>
|
||||
{queueId ? (
|
||||
<Edit className="-ml-0.5 mr-1.5 h-4 w-4" aria-hidden="true" />
|
||||
) : (
|
||||
<PlusIcon className="-ml-0.5 mr-1.5 h-4 w-4" aria-hidden="true" />
|
||||
)}
|
||||
<span className="ml-1 text-sm font-normal">
|
||||
{queueId ? "Edit" : "New queue"}
|
||||
</span>
|
||||
</Button>
|
||||
</ActionButton>
|
||||
</DialogTrigger>
|
||||
{configsData.data && (
|
||||
<DialogContent className="max-h-[90vh] overflow-y-auto">
|
||||
|
||||
@@ -12,7 +12,7 @@ import { getScoreDataTypeIcon } from "@/src/features/scores/components/ScoreDeta
|
||||
import Link from "next/link";
|
||||
import { CreateOrEditAnnotationQueueButton } from "@/src/ee/features/annotation-queues/components/CreateOrEditAnnotationQueueButton";
|
||||
import { useHasProjectAccess } from "@/src/features/rbac/utils/checkProjectAccess";
|
||||
import { useHasOrgEntitlement } from "@/src/features/entitlements/hooks";
|
||||
import { useHasEntitlement } from "@/src/features/entitlements/hooks";
|
||||
import { SupportOrUpgradePage } from "@/src/ee/features/billing/components/SupportOrUpgradePage";
|
||||
import { Skeleton } from "@/src/components/ui/skeleton";
|
||||
import { TableWithMetadataWrapper } from "@/src/components/table/TableWithMetadataWrapper";
|
||||
@@ -35,7 +35,7 @@ export default function QueueItems() {
|
||||
projectId,
|
||||
scope: "annotationQueues:CUD",
|
||||
});
|
||||
const hasEntitlement = useHasOrgEntitlement("annotation-queues");
|
||||
const hasEntitlement = useHasEntitlement("annotation-queues");
|
||||
if (!hasReadAccess || !hasEntitlement) return <SupportOrUpgradePage />;
|
||||
|
||||
return (
|
||||
|
||||
@@ -3,7 +3,7 @@ import { useRouter } from "next/router";
|
||||
import { FullScreenPage } from "@/src/components/layouts/full-screen-page";
|
||||
import { AnnotationQueuesTable } from "@/src/ee/features/annotation-queues/components/AnnotationQueuesTable";
|
||||
import { useHasProjectAccess } from "@/src/features/rbac/utils/checkProjectAccess";
|
||||
import { useHasOrgEntitlement } from "@/src/features/entitlements/hooks";
|
||||
import { useHasEntitlement } from "@/src/features/entitlements/hooks";
|
||||
import { SupportOrUpgradePage } from "@/src/ee/features/billing/components/SupportOrUpgradePage";
|
||||
|
||||
export default function AnnotationQueues() {
|
||||
@@ -13,7 +13,7 @@ export default function AnnotationQueues() {
|
||||
projectId: projectId,
|
||||
scope: "annotationQueues:read",
|
||||
});
|
||||
const hasEntitlement = useHasOrgEntitlement("annotation-queues");
|
||||
const hasEntitlement = useHasEntitlement("annotation-queues");
|
||||
if (!hasAccess || !hasEntitlement) return <SupportOrUpgradePage />;
|
||||
|
||||
return (
|
||||
|
||||
@@ -4,20 +4,12 @@ import { Button } from "@/src/components/ui/button";
|
||||
import { api } from "@/src/utils/api";
|
||||
import { Flex, MarkerBar, Metric, Text } from "@tremor/react";
|
||||
import Link from "next/link";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogHeader,
|
||||
DialogTrigger,
|
||||
} from "@/src/components/ui/dialog";
|
||||
import Header from "@/src/components/layouts/header";
|
||||
import { usePostHogClientCapture } from "@/src/features/posthog-analytics/usePostHogClientCapture";
|
||||
import { useQueryOrganization } from "@/src/features/organizations/hooks";
|
||||
import { Card } from "@/src/components/ui/card";
|
||||
import { numberFormatter, compactNumberFormatter } from "@/src/utils/numbers";
|
||||
import { useHasOrgEntitlement } from "@/src/features/entitlements/hooks";
|
||||
import { useHasEntitlement } from "@/src/features/entitlements/hooks";
|
||||
import { type Plan, planLabels } from "@langfuse/shared";
|
||||
import { stripeProducts } from "@/src/ee/features/billing/utils/stripeProducts";
|
||||
import { useRouter } from "next/router";
|
||||
import {
|
||||
chatAvailable,
|
||||
@@ -26,7 +18,15 @@ import {
|
||||
import { env } from "@/src/env.mjs";
|
||||
import { useHasOrganizationAccess } from "@/src/features/rbac/utils/checkOrganizationAccess";
|
||||
import { Alert, AlertDescription, AlertTitle } from "@/src/components/ui/alert";
|
||||
import { MAX_OBSERVATIONS_FREE_PLAN } from "@/src/ee/features/billing/constants";
|
||||
import { MAX_EVENTS_FREE_PLAN } from "@/src/ee/features/billing/constants";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogHeader,
|
||||
DialogTrigger,
|
||||
} from "@/src/components/ui/dialog";
|
||||
import { usePostHogClientCapture } from "@/src/features/posthog-analytics/usePostHogClientCapture";
|
||||
import { stripeProducts } from "@/src/ee/features/billing/utils/stripeProducts";
|
||||
|
||||
export const BillingSettings = () => {
|
||||
const router = useRouter();
|
||||
@@ -36,7 +36,7 @@ export const BillingSettings = () => {
|
||||
scope: "langfuseCloudBilling:CRUD",
|
||||
});
|
||||
|
||||
const entitled = useHasOrgEntitlement("cloud-billing");
|
||||
const entitled = useHasEntitlement("cloud-billing");
|
||||
if (!entitled) return null;
|
||||
|
||||
if (!hasAccess)
|
||||
@@ -72,11 +72,14 @@ const OrganizationUsageChart = () => {
|
||||
},
|
||||
},
|
||||
);
|
||||
const planLimit =
|
||||
organization?.cloudConfig?.monthlyObservationLimit ??
|
||||
MAX_OBSERVATIONS_FREE_PLAN;
|
||||
const hobbyPlanLimit =
|
||||
organization?.cloudConfig?.monthlyObservationLimit ?? MAX_EVENTS_FREE_PLAN;
|
||||
const plan: Plan = organization?.plan ?? "cloud:hobby";
|
||||
const planLabel = planLabels[plan];
|
||||
const usageType = usage.data?.usageType
|
||||
? usage.data.usageType.charAt(0).toUpperCase() +
|
||||
usage.data.usageType.slice(1)
|
||||
: "Events";
|
||||
|
||||
return (
|
||||
<div>
|
||||
@@ -85,19 +88,21 @@ const OrganizationUsageChart = () => {
|
||||
<>
|
||||
<Text>
|
||||
{usage.data.billingPeriod
|
||||
? `Observations in current billing period`
|
||||
: "Observations / last 30d"}
|
||||
? `${usageType} in current billing period`
|
||||
: `${usageType} / last 30d`}
|
||||
</Text>
|
||||
<Metric>{numberFormatter(usage.data.countObservations, 0)}</Metric>
|
||||
<Metric>{numberFormatter(usage.data.usageCount, 0)}</Metric>
|
||||
{plan === "cloud:hobby" && (
|
||||
<>
|
||||
<Flex className="mt-4">
|
||||
<Text>{`${numberFormatter((usage.data.countObservations / planLimit) * 100)}%`}</Text>
|
||||
<Text>Plan limit: {compactNumberFormatter(planLimit)}</Text>
|
||||
<Text>{`${numberFormatter((usage.data.usageCount / hobbyPlanLimit) * 100)}%`}</Text>
|
||||
<Text>
|
||||
Plan limit: {compactNumberFormatter(hobbyPlanLimit)}
|
||||
</Text>
|
||||
</Flex>
|
||||
<MarkerBar
|
||||
value={Math.min(
|
||||
(usage.data.countObservations / planLimit) * 100,
|
||||
(usage.data.usageCount / hobbyPlanLimit) * 100,
|
||||
100,
|
||||
)}
|
||||
className="mt-3"
|
||||
@@ -138,35 +143,35 @@ const OrganizationUsageChart = () => {
|
||||
|
||||
const BillingPortalOrPricingPageButton = () => {
|
||||
const organization = useQueryOrganization();
|
||||
const router = useRouter();
|
||||
const capture = usePostHogClientCapture();
|
||||
const billingPortalUrl = api.cloudBilling.getStripeCustomerPortalUrl.useQuery(
|
||||
{
|
||||
orgId: organization?.id as string,
|
||||
},
|
||||
{
|
||||
enabled: organization !== undefined,
|
||||
refetchOnMount: false,
|
||||
refetchOnReconnect: false,
|
||||
refetchOnWindowFocus: false,
|
||||
},
|
||||
);
|
||||
if (billingPortalUrl.isLoading) return null;
|
||||
if (!billingPortalUrl.data) return <PricingPageButton />;
|
||||
|
||||
return (
|
||||
<Button asChild>
|
||||
<Link href={billingPortalUrl.data}>Billing portal</Link>
|
||||
</Button>
|
||||
);
|
||||
};
|
||||
|
||||
const PricingPageButton = () => {
|
||||
const capture = usePostHogClientCapture();
|
||||
const organization = useQueryOrganization();
|
||||
const router = useRouter();
|
||||
const mutCreateCheckoutSession =
|
||||
api.cloudBilling.createStripeCheckoutSession.useMutation({
|
||||
onSuccess: (url) => {
|
||||
router.push(url);
|
||||
},
|
||||
});
|
||||
|
||||
if (!organization) return null;
|
||||
if (billingPortalUrl.isLoading) return null;
|
||||
if (billingPortalUrl.data)
|
||||
return (
|
||||
<Button asChild>
|
||||
<Link href={billingPortalUrl.data}>Billing portal</Link>
|
||||
</Button>
|
||||
);
|
||||
|
||||
// Do not show checkout or customer portal if manual plan is set in cloud config
|
||||
if (organization.cloudConfig?.plan) {
|
||||
@@ -186,6 +191,7 @@ const PricingPageButton = () => {
|
||||
else return null;
|
||||
}
|
||||
|
||||
// Show pricing page button
|
||||
return (
|
||||
<Dialog
|
||||
onOpenChange={(open) => {
|
||||
|
||||
@@ -1,11 +1,8 @@
|
||||
import { api } from "@/src/utils/api";
|
||||
import { useQueryProjectOrOrganization } from "@/src/features/projects/hooks";
|
||||
import { useHasOrganizationAccess } from "@/src/features/rbac/utils/checkOrganizationAccess";
|
||||
import { MAX_OBSERVATIONS_FREE_PLAN } from "@/src/ee/features/billing/constants";
|
||||
import {
|
||||
useHasOrgEntitlement,
|
||||
useOrganizationPlan,
|
||||
} from "@/src/features/entitlements/hooks";
|
||||
import { MAX_EVENTS_FREE_PLAN } from "@/src/ee/features/billing/constants";
|
||||
import { useHasEntitlement, usePlan } from "@/src/features/entitlements/hooks";
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
@@ -18,8 +15,8 @@ import Link from "next/link";
|
||||
|
||||
export const UsageTracker = () => {
|
||||
const { organization } = useQueryProjectOrOrganization();
|
||||
const hasEntitlement = useHasOrgEntitlement("cloud-billing");
|
||||
const plan = useOrganizationPlan();
|
||||
const hasEntitlement = useHasEntitlement("cloud-billing");
|
||||
const plan = usePlan();
|
||||
const hasAccess = useHasOrganizationAccess({
|
||||
organizationId: organization?.id,
|
||||
scope: "langfuseCloudBilling:CRUD",
|
||||
@@ -48,10 +45,11 @@ export const UsageTracker = () => {
|
||||
return null;
|
||||
}
|
||||
|
||||
const usage = usageQuery.data.countObservations || 0;
|
||||
const percentage = (usage / MAX_OBSERVATIONS_FREE_PLAN) * 100;
|
||||
const usage = usageQuery.data.usageCount || 0;
|
||||
const usageType = usageQuery.data.usageType;
|
||||
const percentage = (usage / MAX_EVENTS_FREE_PLAN) * 100;
|
||||
|
||||
if (percentage < 80) {
|
||||
if (percentage < 90) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -60,7 +58,7 @@ export const UsageTracker = () => {
|
||||
<CardHeader className="p-4 pb-0">
|
||||
<CardTitle className="text-sm">Hobby Plan Usage Limit</CardTitle>
|
||||
<CardDescription>
|
||||
{`${usage.toLocaleString()} / ${MAX_OBSERVATIONS_FREE_PLAN.toLocaleString()} (${percentage.toFixed(0)}%) observations in last 30 days. Please upgrade your plan to avoid interruptions.`}
|
||||
{`${usage.toLocaleString()} / ${MAX_EVENTS_FREE_PLAN.toLocaleString()} (${percentage.toFixed(0)}%) ${usageType} in last 30 days. Please upgrade your plan to avoid interruptions.`}
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="p-4 pt-2">
|
||||
|
||||
@@ -1 +1 @@
|
||||
export const MAX_OBSERVATIONS_FREE_PLAN = 50_000;
|
||||
export const MAX_EVENTS_FREE_PLAN = 50_000;
|
||||
|
||||
@@ -12,6 +12,7 @@ import { TRPCError } from "@trpc/server";
|
||||
import * as z from "zod";
|
||||
import { throwIfNoOrganizationAccess } from "@/src/features/rbac/utils/checkOrganizationAccess";
|
||||
import { auditLog } from "@/src/features/audit-logs/auditLog";
|
||||
import { getObservationCountOfProjectsSinceCreationDate } from "@langfuse/shared/src/server";
|
||||
|
||||
export const cloudBillingRouter = createTRPCRouter({
|
||||
createStripeCheckoutSession: protectedOrganizationProcedure
|
||||
@@ -221,6 +222,13 @@ export const cloudBillingRouter = createTRPCRouter({
|
||||
where: {
|
||||
id: input.orgId,
|
||||
},
|
||||
include: {
|
||||
projects: {
|
||||
select: {
|
||||
id: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
if (!organization) {
|
||||
throw new TRPCError({
|
||||
@@ -244,6 +252,7 @@ export const cloudBillingRouter = createTRPCRouter({
|
||||
start: new Date(subscription.current_period_start * 1000),
|
||||
end: new Date(subscription.current_period_end * 1000),
|
||||
};
|
||||
|
||||
const stripeInvoice = await stripeClient.invoices.retrieveUpcoming({
|
||||
subscription: parsedOrg.cloudConfig.stripe.activeSubscriptionId,
|
||||
});
|
||||
@@ -251,38 +260,78 @@ export const cloudBillingRouter = createTRPCRouter({
|
||||
usdAmount: stripeInvoice.amount_due / 100,
|
||||
date: new Date(stripeInvoice.period_end * 1000),
|
||||
};
|
||||
const usage = stripeInvoice.lines.data.reduce((acc, line) => {
|
||||
const usageInvoiceLines = stripeInvoice.lines.data.filter((line) =>
|
||||
Boolean(line.plan?.meter),
|
||||
);
|
||||
const usage = usageInvoiceLines.reduce((acc, line) => {
|
||||
if (line.quantity) {
|
||||
return acc + line.quantity;
|
||||
}
|
||||
return acc;
|
||||
}, 0);
|
||||
// get meter for usage type (events or observations)
|
||||
const meterId = usageInvoiceLines[0]?.plan?.meter;
|
||||
const meter = meterId
|
||||
? await stripeClient.billing.meters.retrieve(meterId)
|
||||
: undefined;
|
||||
|
||||
return {
|
||||
countObservations: usage,
|
||||
usageCount: usage,
|
||||
usageType: meter?.display_name.toLowerCase() ?? "events",
|
||||
billingPeriod,
|
||||
upcomingInvoice,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// For non-Stripe subscriptions, we can only get usage from the LangFuse API
|
||||
// Free plan, usage not tracked on Stripe
|
||||
const thirtyDaysAgo = new Date();
|
||||
thirtyDaysAgo.setDate(thirtyDaysAgo.getDate() - 30);
|
||||
thirtyDaysAgo.setHours(0, 0, 0, 0);
|
||||
const projectIds = organization.projects.map((p) => p.id);
|
||||
|
||||
const usage = await ctx.prisma.observation.count({
|
||||
where: {
|
||||
project: {
|
||||
orgId: input.orgId,
|
||||
},
|
||||
startTime: {
|
||||
gte: thirtyDaysAgo,
|
||||
},
|
||||
},
|
||||
});
|
||||
const countObservations =
|
||||
await getObservationCountOfProjectsSinceCreationDate({
|
||||
projectIds,
|
||||
start: thirtyDaysAgo,
|
||||
});
|
||||
|
||||
// const usageArr = await Promise.all([
|
||||
// ctx.prisma.observation.count({
|
||||
// where: {
|
||||
// project: {
|
||||
// orgId: input.orgId,
|
||||
// },
|
||||
// createdAt: {
|
||||
// gte: thirtyDaysAgo,
|
||||
// },
|
||||
// },
|
||||
// }),
|
||||
// ctx.prisma.trace.count({
|
||||
// where: {
|
||||
// project: {
|
||||
// orgId: input.orgId,
|
||||
// },
|
||||
// createdAt: {
|
||||
// gte: thirtyDaysAgo,
|
||||
// },
|
||||
// },
|
||||
// }),
|
||||
// ctx.prisma.score.count({
|
||||
// where: {
|
||||
// project: {
|
||||
// orgId: input.orgId,
|
||||
// },
|
||||
// createdAt: {
|
||||
// gte: thirtyDaysAgo,
|
||||
// },
|
||||
// },
|
||||
// }),
|
||||
// ]);
|
||||
|
||||
return {
|
||||
countObservations: usage,
|
||||
usageCount: countObservations,
|
||||
usageType: "observations",
|
||||
};
|
||||
}),
|
||||
});
|
||||
|
||||
@@ -104,6 +104,7 @@ async function handleSubscriptionChanged(
|
||||
});
|
||||
if (!checkoutSessionsResponse || checkoutSessionsResponse.data.length !== 1) {
|
||||
logger.error("[Stripe Webhook] No checkout session found");
|
||||
traceException("[Stripe Webhook] No checkout session found");
|
||||
return;
|
||||
}
|
||||
const checkoutSession = checkoutSessionsResponse.data[0];
|
||||
@@ -112,6 +113,7 @@ async function handleSubscriptionChanged(
|
||||
const clientReference = checkoutSession.client_reference_id;
|
||||
if (!clientReference) {
|
||||
logger.error("[Stripe Webhook] No client reference");
|
||||
traceException("[Stripe Webhook] No client reference");
|
||||
return NextResponse.json(
|
||||
{ message: "No client reference" },
|
||||
{ status: 400 },
|
||||
@@ -133,6 +135,7 @@ async function handleSubscriptionChanged(
|
||||
});
|
||||
if (!organization) {
|
||||
logger.error("[Stripe Webhook] Organization not found");
|
||||
traceException("[Stripe Webhook] Organization not found");
|
||||
return;
|
||||
}
|
||||
const parsedOrg = parseDbOrg(organization);
|
||||
@@ -140,14 +143,15 @@ async function handleSubscriptionChanged(
|
||||
// assert that no other stripe customer id is already set on the org
|
||||
const customerId = subscription.customer;
|
||||
if (!customerId || typeof customerId !== "string") {
|
||||
logger.error("[Stripe Webhook] Product ID not found");
|
||||
traceException("[Stripe Webhook] Product ID not found");
|
||||
logger.error("[Stripe Webhook] Customer ID not found");
|
||||
traceException("[Stripe Webhook] Customer ID not found");
|
||||
return;
|
||||
}
|
||||
if (
|
||||
parsedOrg.cloudConfig?.stripe?.customerId &&
|
||||
parsedOrg.cloudConfig?.stripe?.customerId !== customerId
|
||||
) {
|
||||
logger.error("[Stripe Webhook] Another customer id already set on org");
|
||||
traceException("[Stripe Webhook] Another customer id already set on org");
|
||||
return;
|
||||
}
|
||||
@@ -159,6 +163,9 @@ async function handleSubscriptionChanged(
|
||||
logger.error(
|
||||
"[Stripe Webhook] Subscription items not found or more than one",
|
||||
);
|
||||
traceException(
|
||||
"[Stripe Webhook] Subscription items not found or more than one",
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -180,6 +187,9 @@ async function handleSubscriptionChanged(
|
||||
traceException(
|
||||
"[Stripe Webhook] Another active product id already set on (one of the) org with this active subscription id",
|
||||
);
|
||||
logger.error(
|
||||
"[Stripe Webhook] Another active product id already set on (one of the) org with this active subscription id",
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { env } from "@/src/env.mjs";
|
||||
import Stripe from "stripe";
|
||||
|
||||
export const stripeClient = env.STRIPE_SECRET_KEY
|
||||
? new Stripe(env.STRIPE_SECRET_KEY)
|
||||
: undefined;
|
||||
export const stripeClient =
|
||||
env.STRIPE_SECRET_KEY && env.NEXT_PUBLIC_LANGFUSE_CLOUD_REGION
|
||||
? new Stripe(env.STRIPE_SECRET_KEY)
|
||||
: undefined;
|
||||
|
||||
@@ -24,7 +24,7 @@ export const stripeProducts: StripeProduct[] = [
|
||||
checkout: {
|
||||
title: "Pro",
|
||||
description:
|
||||
"For serious projects. Includes access to full history and higher usage.",
|
||||
"For serious projects. Includes access to full history, higher usage and access to features.",
|
||||
price: "$59 / month + $10/100k observations",
|
||||
},
|
||||
},
|
||||
|
||||
@@ -1,18 +1,26 @@
|
||||
import Header from "@/src/components/layouts/header";
|
||||
import { useRouter } from "next/router";
|
||||
import { Button } from "@/src/components/ui/button";
|
||||
import Link from "next/link";
|
||||
import { useHasProjectAccess } from "@/src/features/rbac/utils/checkProjectAccess";
|
||||
import { Lock, Plus } from "lucide-react";
|
||||
import { Plus } from "lucide-react";
|
||||
import EvaluatorTable from "@/src/ee/features/evals/components/evaluator-table";
|
||||
import { usePostHogClientCapture } from "@/src/features/posthog-analytics/usePostHogClientCapture";
|
||||
import { FullScreenPage } from "@/src/components/layouts/full-screen-page";
|
||||
import { Tabs, TabsList, TabsTrigger } from "@/src/components/ui/tabs";
|
||||
import { ActionButton } from "@/src/components/ActionButton";
|
||||
import { api } from "@/src/utils/api";
|
||||
import { useEntitlementLimit } from "@/src/features/entitlements/hooks";
|
||||
|
||||
export default function EvaluatorsPage() {
|
||||
const router = useRouter();
|
||||
const projectId = router.query.projectId as string;
|
||||
const capture = usePostHogClientCapture();
|
||||
const evaluatorCount = api.evals.allConfigs.useQuery({
|
||||
projectId,
|
||||
}).data?.totalCount;
|
||||
const evaluatorLimit = useEntitlementLimit(
|
||||
"model-based-evaluations-count-evaluators",
|
||||
);
|
||||
const hasWriteAccess = useHasProjectAccess({
|
||||
projectId,
|
||||
scope: "evalJob:CUD",
|
||||
@@ -37,23 +45,17 @@ export default function EvaluatorsPage() {
|
||||
href: "https://langfuse.com/docs/scores/model-based-evals",
|
||||
}}
|
||||
actionButtons={
|
||||
<Button
|
||||
disabled={!hasWriteAccess}
|
||||
onClick={() => capture("eval_config:new_form_open")}
|
||||
asChild
|
||||
<ActionButton
|
||||
hasAccess={hasWriteAccess}
|
||||
icon={<Plus className="h-4 w-4" />}
|
||||
variant="secondary"
|
||||
onClick={() => capture("eval_config:new_form_open")}
|
||||
href={`/project/${projectId}/evals/new`}
|
||||
limitValue={evaluatorCount ?? 0}
|
||||
limit={evaluatorLimit}
|
||||
>
|
||||
<Link
|
||||
href={hasWriteAccess ? `/project/${projectId}/evals/new` : "#"}
|
||||
>
|
||||
{hasWriteAccess ? (
|
||||
<Plus className="mr-2 h-4 w-4" />
|
||||
) : (
|
||||
<Lock className="mr-2 h-4 w-4" />
|
||||
)}
|
||||
New evaluator
|
||||
</Link>
|
||||
</Button>
|
||||
New evaluator
|
||||
</ActionButton>
|
||||
}
|
||||
/>
|
||||
<EvaluatorTable
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import { env } from "@/src/env.mjs";
|
||||
import { randomUUID } from "crypto";
|
||||
import {
|
||||
QueueJobs,
|
||||
@@ -17,7 +16,7 @@ export const addDatasetRunItemsToEvalQueue = async ({
|
||||
traceId: string;
|
||||
observationId?: string;
|
||||
}) => {
|
||||
if (redis && env.NEXT_PUBLIC_LANGFUSE_CLOUD_REGION) {
|
||||
if (redis) {
|
||||
const queue = DatasetRunItemUpsertQueue.getInstance();
|
||||
|
||||
if (queue) {
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { z } from "zod";
|
||||
|
||||
import {
|
||||
createTRPCRouter,
|
||||
protectedProjectProcedure,
|
||||
@@ -12,6 +11,7 @@ import {
|
||||
singleFilter,
|
||||
variableMapping,
|
||||
ChatMessageRole,
|
||||
paginationZod,
|
||||
type JobConfiguration,
|
||||
JobConfigState,
|
||||
JobType,
|
||||
@@ -111,8 +111,7 @@ export const evalRouter = createTRPCRouter({
|
||||
.input(
|
||||
z.object({
|
||||
projectId: z.string(),
|
||||
limit: z.number(),
|
||||
page: z.number(),
|
||||
...paginationZod,
|
||||
}),
|
||||
)
|
||||
.query(async ({ input, ctx }) => {
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user