Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
fd37e2c35e | ||
|
|
0352e7fad6 | ||
|
|
6c4482ff56 | ||
|
|
cc62b32b82 | ||
|
|
b421c2dec5 | ||
|
|
3a67533ac8 | ||
|
|
21303a275a | ||
|
|
284494c62a | ||
|
|
b37ec03483 | ||
|
|
a49a410a04 | ||
|
|
1e9d8de305 | ||
|
|
0fa88aea57 | ||
|
|
61240d7c92 | ||
|
|
d54c888cd9 | ||
|
|
b87a00fa03 | ||
|
|
8b8d1086e5 | ||
|
|
d88902316b | ||
|
|
5dd5adf850 | ||
|
|
a56246059c | ||
|
|
833f4165ce | ||
|
|
ebe0c034fd | ||
|
|
64492f3002 | ||
|
|
27faebe244 | ||
|
|
5e681e5a6d | ||
|
|
8af88a0cec | ||
|
|
1770579fa6 | ||
|
|
273d29c383 | ||
|
|
0047a3dec6 | ||
|
|
b63aeeeae7 | ||
|
|
f5dc7eea07 | ||
|
|
8560a10e53 | ||
|
|
609a330c37 | ||
|
|
07ad7e54e3 | ||
|
|
4781b91e90 | ||
|
|
2c68223402 | ||
|
|
1211de3efb | ||
|
|
9b9fd35f13 | ||
|
|
407f5dd1ce | ||
|
|
d7832336dd | ||
|
|
6362d8bcc9 | ||
|
|
35057ea443 | ||
|
|
9adc3bee5b | ||
|
|
e57af47d84 | ||
|
|
8f0247d311 | ||
|
|
e96b2a4b0f | ||
|
|
c389d4f01a | ||
|
|
4d8bb0fb95 | ||
|
|
79068ef93f | ||
|
|
6c1c231ba7 | ||
|
|
b722892c00 | ||
|
|
4cd1d167da | ||
|
|
c0f38a8eda | ||
|
|
6be87994a9 | ||
|
|
e90bb60ec8 | ||
|
|
439a65b926 | ||
|
|
11af1f4167 | ||
|
|
2d265eb3f9 | ||
|
|
a93b8ff119 | ||
|
|
81cf48ddf0 | ||
|
|
816eec0f75 | ||
|
|
fa630d34de | ||
|
|
0eb129e186 | ||
|
|
2defaf273a | ||
|
|
b9305e76f5 | ||
|
|
241d1119f9 | ||
|
|
4a44adcedd | ||
|
|
9a861dff37 | ||
|
|
12ee00f273 | ||
|
|
f2513fbebf | ||
|
|
5be98906e3 | ||
|
|
4bac94e3fc | ||
|
|
86980da792 | ||
|
|
f9be079d62 | ||
|
|
d66fac5e29 | ||
|
|
83c01bb444 | ||
|
|
c937fc7280 | ||
|
|
c8aa4a2d70 | ||
|
|
5d459f0143 | ||
|
|
faa1b71f48 | ||
|
|
99e9825e66 | ||
|
|
11ad444632 | ||
|
|
f132aa3969 | ||
|
|
fa69d4dc75 | ||
|
|
6ef6fba4dc | ||
|
|
a5078ccbaf | ||
|
|
ba5c1cd70e | ||
|
|
19ea989b5a | ||
|
|
ec0bf37d0b | ||
|
|
3b4790270f | ||
|
|
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
-18
@@ -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,17 +35,18 @@ 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
|
||||
LANGFUSE_S3_MEDIA_UPLOAD_BUCKET=langfuse
|
||||
LANGFUSE_S3_MEDIA_UPLOAD_ACCESS_KEY_ID=devstoreaccount1
|
||||
LANGFUSE_S3_MEDIA_UPLOAD_SECRET_ACCESS_KEY=Eby8vdM02xNOcqFlqUwJPLlmEtlCDXJ1OUzFT50uSRZ6IFsuFq2UVErCz4I6tq/K1SZFPTOtr/KBHBeksoGMGw==
|
||||
@@ -57,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=devstoreaccount1
|
||||
LANGFUSE_S3_EVENT_UPLOAD_SECRET_ACCESS_KEY=Eby8vdM02xNOcqFlqUwJPLlmEtlCDXJ1OUzFT50uSRZ6IFsuFq2UVErCz4I6tq/K1SZFPTOtr/KBHBeksoGMGw==
|
||||
@@ -82,11 +82,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
-18
@@ -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,17 +34,18 @@ 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
|
||||
LANGFUSE_S3_MEDIA_UPLOAD_BUCKET=langfuse
|
||||
LANGFUSE_S3_MEDIA_UPLOAD_ACCESS_KEY_ID=minio
|
||||
LANGFUSE_S3_MEDIA_UPLOAD_SECRET_ACCESS_KEY=miniosecret
|
||||
@@ -56,7 +57,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 +79,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
-10
@@ -34,17 +34,18 @@ 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
|
||||
LANGFUSE_S3_MEDIA_UPLOAD_BUCKET=langfuse
|
||||
LANGFUSE_S3_MEDIA_UPLOAD_ACCESS_KEY_ID=minio
|
||||
LANGFUSE_S3_MEDIA_UPLOAD_SECRET_ACCESS_KEY=miniosecret
|
||||
@@ -56,7 +57,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 +78,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,8 @@ 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
|
||||
echo "LANGFUSE_POSTGRES_INGESTION_ENABLED=true" >> .env
|
||||
- name: Run + migrate
|
||||
run: |
|
||||
docker compose -f docker-compose.dev.yml up -d
|
||||
@@ -133,6 +140,8 @@ jobs:
|
||||
LANGFUSE_INIT_USER_PASSWORD: "password"
|
||||
- name: run test-sync
|
||||
run: pnpm --filter=web run test-sync
|
||||
- name: run test-client
|
||||
run: pnpm --filter=web run test-client
|
||||
|
||||
tests-web-async:
|
||||
timeout-minutes: 20
|
||||
@@ -158,6 +167,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 +184,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 +232,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 +292,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 +325,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 +349,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
|
||||
@@ -469,6 +480,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 +489,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
|
||||
|
||||
|
||||
@@ -2,8 +2,8 @@ Copyright (c) 2023--2024 Langfuse GmbH
|
||||
|
||||
Portions of this software are licensed as follows:
|
||||
|
||||
- All content that resides under the "ee/" and/or "web/src/ee" directories of this repository, if these directories exist, is licensed under the license defined in "ee/LICENSE".
|
||||
- All third party components incorporated into the Finto Technologies Software are licensed under the original license provided by the owner of the applicable component.
|
||||
- All content that resides under the "ee/", "web/src/ee/", and/or "worker/src/ee/" directories of this repository, if these directories exist, is licensed under the license defined in "ee/LICENSE".
|
||||
- All third party components incorporated into the Langfuse Software are licensed under the original license provided by the owner of the applicable component.
|
||||
- Content outside of the above mentioned directories or restrictions above is available under the "MIT Expat" license as defined below.
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
|
||||
@@ -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
|
||||
+133
-27
@@ -1,31 +1,131 @@
|
||||
services:
|
||||
langfuse-server:
|
||||
image: langfuse/langfuse:2
|
||||
depends_on:
|
||||
db:
|
||||
langfuse-worker:
|
||||
image: langfuse/langfuse-worker:3
|
||||
restart: always
|
||||
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:-auto}
|
||||
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_BUCKET: ${LANGFUSE_S3_MEDIA_UPLOAD_BUCKET:-langfuse}
|
||||
LANGFUSE_S3_MEDIA_UPLOAD_REGION: ${LANGFUSE_S3_MEDIA_UPLOAD_REGION:-auto}
|
||||
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:3
|
||||
restart: always
|
||||
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:-}
|
||||
LANGFUSE_SDK_CI_SYNC_PROCESSING_ENABLED: ${LANGFUSE_SDK_CI_SYNC_PROCESSING_ENABLED:-false}
|
||||
LANGFUSE_READ_FROM_POSTGRES_ONLY: ${LANGFUSE_READ_FROM_POSTGRES_ONLY:-false}
|
||||
LANGFUSE_READ_FROM_CLICKHOUSE_ONLY: ${LANGFUSE_READ_FROM_CLICKHOUSE_ONLY:-true}
|
||||
LANGFUSE_RETURN_FROM_CLICKHOUSE: ${LANGFUSE_RETURN_FROM_CLICKHOUSE:-true}
|
||||
|
||||
db:
|
||||
image: postgres
|
||||
clickhouse:
|
||||
image: clickhouse/clickhouse-server
|
||||
restart: always
|
||||
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
|
||||
restart: always
|
||||
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 +133,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
|
||||
|
||||
+1
-1
@@ -28,7 +28,7 @@
|
||||
"@opentelemetry/api": ">=1.0.0 <1.10.0",
|
||||
"axios": "^1.7.7",
|
||||
"next": "^14.2.15",
|
||||
"next-auth": "^4.24.7",
|
||||
"next-auth": "^4.24.11",
|
||||
"zod": "^3.23.8"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
||||
+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));
|
||||
|
||||
@@ -58,7 +58,7 @@ types:
|
||||
docs: The id of the object to attach the comment to. If this does not reference a valid existing object, an error will be thrown.
|
||||
content:
|
||||
type: string
|
||||
docs: The content of the comment. May include markdown. Currently limited to 500 characters.
|
||||
docs: The content of the comment. May include markdown. Currently limited to 3000 characters.
|
||||
authorUserId:
|
||||
type: optional<string>
|
||||
docs: The id of the user who created the comment.
|
||||
|
||||
@@ -126,7 +126,7 @@ types:
|
||||
docs: The output data of the observation
|
||||
usage:
|
||||
type: optional<Usage>
|
||||
docs: The usage data of the observation
|
||||
docs: (Deprecated. Use usageDetails and costDetails instead.) The usage data of the observation
|
||||
level:
|
||||
type: ObservationLevel
|
||||
docs: The level of the observation
|
||||
@@ -139,6 +139,12 @@ types:
|
||||
promptId:
|
||||
type: optional<string>
|
||||
docs: The prompt ID associated with the observation
|
||||
usageDetails:
|
||||
type: optional<map<string, integer>>
|
||||
docs: The usage details of the observation. Key is the name of the usage metric, value is the number of units consumed. The total key is the sum of all (non-total) usage metrics or the total value ingested.
|
||||
costDetails:
|
||||
type: optional<map<string, double>>
|
||||
docs: The cost details of the observation. Key is the name of the cost metric, value is the cost in USD. The total key is the sum of all (non-total) cost metrics or the total value ingested.
|
||||
|
||||
ObservationsView:
|
||||
extends: Observation
|
||||
@@ -163,13 +169,13 @@ types:
|
||||
docs: The total price in USD.
|
||||
calculatedInputCost:
|
||||
type: optional<double>
|
||||
docs: The calculated cost of the input in USD
|
||||
docs: (Deprecated. Use usageDetails and costDetails instead.) The calculated cost of the input in USD
|
||||
calculatedOutputCost:
|
||||
type: optional<double>
|
||||
docs: The calculated cost of the output in USD
|
||||
docs: (Deprecated. Use usageDetails and costDetails instead.) The calculated cost of the output in USD
|
||||
calculatedTotalCost:
|
||||
type: optional<double>
|
||||
docs: The calculated total cost in USD
|
||||
docs: (Deprecated. Use usageDetails and costDetails instead.) The calculated total cost in USD
|
||||
latency:
|
||||
type: optional<double>
|
||||
docs: The latency in seconds.
|
||||
@@ -178,7 +184,7 @@ types:
|
||||
docs: The time to the first token in seconds
|
||||
|
||||
Usage:
|
||||
docs: Standard interface for usage and cost
|
||||
docs: (Deprecated. Use usageDetails and costDetails instead.) Standard interface for usage and cost
|
||||
properties:
|
||||
input:
|
||||
docs: Number of input units (e.g. tokens)
|
||||
|
||||
@@ -126,6 +126,8 @@ types:
|
||||
model: optional<string>
|
||||
modelParameters: optional<map<string, commons.MapValue>>
|
||||
usage: optional<IngestionUsage>
|
||||
usageDetails: optional<UsageDetails>
|
||||
costDetails: optional<map<string, float>>
|
||||
promptName: optional<string>
|
||||
promptVersion: optional<integer>
|
||||
|
||||
@@ -137,6 +139,8 @@ types:
|
||||
modelParameters: optional<map<string, commons.MapValue>>
|
||||
usage: optional<IngestionUsage>
|
||||
promptName: optional<string>
|
||||
usageDetails: optional<UsageDetails>
|
||||
costDetails: optional<map<string, float>>
|
||||
promptVersion: optional<integer>
|
||||
|
||||
ObservationBody:
|
||||
@@ -312,3 +316,17 @@ types:
|
||||
properties:
|
||||
successes: list<IngestionSuccess>
|
||||
errors: list<IngestionError>
|
||||
|
||||
OpenAIUsageSchema:
|
||||
properties:
|
||||
prompt_tokens: integer
|
||||
completion_tokens: integer
|
||||
total_tokens: integer
|
||||
prompt_tokens_details: optional<map<string, integer>>
|
||||
completion_tokens_details: optional<map<string, integer>>
|
||||
|
||||
UsageDetails:
|
||||
discriminated: false
|
||||
union:
|
||||
- map<string, integer>
|
||||
- OpenAIUsageSchema
|
||||
|
||||
@@ -99,5 +99,63 @@ 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">
|
||||
enum:
|
||||
- value: image/png
|
||||
name: IMAGE_PNG
|
||||
- value: image/jpeg
|
||||
name: IMAGE_JPEG
|
||||
- value: image/jpg
|
||||
name: IMAGE_JPG
|
||||
- value: image/webp
|
||||
name: IMAGE_WEBP
|
||||
- value: image/gif
|
||||
name: IMAGE_GIF
|
||||
- value: image/svg+xml
|
||||
name: IMAGE_SVG_XML
|
||||
- value: image/tiff
|
||||
name: IMAGE_TIFF
|
||||
- value: image/bmp
|
||||
name: IMAGE_BMP
|
||||
- value: audio/mpeg
|
||||
name: AUDIO_MPEG
|
||||
- value: audio/mp3
|
||||
name: AUDIO_MP3
|
||||
- value: audio/wav
|
||||
name: AUDIO_WAV
|
||||
- value: audio/ogg
|
||||
name: AUDIO_OGG
|
||||
- value: audio/oga
|
||||
name: AUDIO_OGA
|
||||
- value: audio/aac
|
||||
name: AUDIO_AAC
|
||||
- value: audio/mp4
|
||||
name: AUDIO_MP4
|
||||
- value: audio/flac
|
||||
name: AUDIO_FLAC
|
||||
- value: video/mp4
|
||||
name: VIDEO_MP4
|
||||
- value: video/webm
|
||||
name: VIDEO_WEBM
|
||||
- value: text/plain
|
||||
name: TEXT_PLAIN
|
||||
- value: text/html
|
||||
name: TEXT_HTML
|
||||
- value: text/css
|
||||
name: TEXT_CSS
|
||||
- value: text/csv
|
||||
name: TEXT_CSV
|
||||
- value: application/pdf
|
||||
name: APPLICATION_PDF
|
||||
- value: application/msword
|
||||
name: APPLICATION_MSWORD
|
||||
- value: application/vnd.ms-excel
|
||||
name: APPLICATION_MS_EXCEL
|
||||
- value: application/zip
|
||||
name: APPLICATION_ZIP
|
||||
- value: application/json
|
||||
name: APPLICATION_JSON
|
||||
- value: application/xml
|
||||
name: APPLICATION_XML
|
||||
- value: application/octet-stream
|
||||
name: APPLICATION_OCTET_STREAM
|
||||
docs: The MIME type of the media record
|
||||
|
||||
+3
-3
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "langfuse",
|
||||
"version": "2.93.2",
|
||||
"version": "3.2.0",
|
||||
"author": "engineering@langfuse.com",
|
||||
"license": "MIT",
|
||||
"private": true,
|
||||
@@ -81,10 +81,10 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"packageManager": "pnpm@9.5.0",
|
||||
"pnpm": {
|
||||
"overrides": {
|
||||
"jsonpath-plus": "10.0.7"
|
||||
}
|
||||
},
|
||||
"packageManager": "pnpm@9.5.0"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
ALTER TABLE traces ON CLUSTER default DROP INDEX IF EXISTS idx_user_id;
|
||||
@@ -0,0 +1,2 @@
|
||||
ALTER TABLE traces ON CLUSTER default ADD INDEX IF NOT EXISTS idx_user_id user_id TYPE bloom_filter() GRANULARITY 1;
|
||||
ALTER TABLE traces ON CLUSTER default MATERIALIZE INDEX IF EXISTS idx_user_id;
|
||||
@@ -0,0 +1 @@
|
||||
ALTER TABLE traces ON CLUSTER default DROP INDEX IF EXISTS idx_user_id;
|
||||
@@ -0,0 +1,2 @@
|
||||
ALTER TABLE traces ADD INDEX IF NOT EXISTS idx_user_id user_id TYPE bloom_filter() GRANULARITY 1;
|
||||
ALTER TABLE traces MATERIALIZE INDEX IF EXISTS idx_user_id;
|
||||
@@ -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",
|
||||
@@ -81,7 +82,7 @@
|
||||
"langchain": "^0.3.6",
|
||||
"langfuse-langchain": "3.30.3",
|
||||
"lodash": "^4.17.21",
|
||||
"next-auth": "^4.24.7",
|
||||
"next-auth": "^4.24.11",
|
||||
"nodemailer": "^6.9.15",
|
||||
"prisma-extension-kysely": "^2.1.0",
|
||||
"uuid": "^9.0.1",
|
||||
|
||||
@@ -453,6 +453,7 @@ export type Project = {
|
||||
org_id: string;
|
||||
created_at: Generated<Timestamp>;
|
||||
updated_at: Generated<Timestamp>;
|
||||
deleted_at: Timestamp | null;
|
||||
name: string;
|
||||
};
|
||||
export type ProjectMembership = {
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
-- AlterTable
|
||||
ALTER TABLE "projects" ADD COLUMN "deleted_at" TIMESTAMP(3);
|
||||
+5
@@ -0,0 +1,5 @@
|
||||
-- DropForeignKey
|
||||
ALTER TABLE "job_executions" DROP CONSTRAINT "job_executions_job_output_score_id_fkey";
|
||||
|
||||
-- DropForeignKey
|
||||
ALTER TABLE "traces" DROP CONSTRAINT "traces_session_id_project_id_fkey";
|
||||
@@ -113,6 +113,7 @@ model Project {
|
||||
orgId String @map("org_id")
|
||||
createdAt DateTime @default(now()) @map("created_at")
|
||||
updatedAt DateTime @default(now()) @updatedAt @map("updated_at")
|
||||
deletedAt DateTime? @map("deleted_at")
|
||||
name String
|
||||
projectMembers ProjectMembership[]
|
||||
organization Organization @relation(fields: [orgId], references: [id], onUpdate: Cascade, onDelete: Cascade)
|
||||
@@ -468,25 +469,24 @@ enum ObservationLevel {
|
||||
}
|
||||
|
||||
model Score {
|
||||
id String @id @default(cuid())
|
||||
timestamp DateTime @default(now())
|
||||
projectId String @map("project_id")
|
||||
project Project @relation(fields: [projectId], references: [id], onDelete: Cascade)
|
||||
id String @id @default(cuid())
|
||||
timestamp DateTime @default(now())
|
||||
projectId String @map("project_id")
|
||||
project Project @relation(fields: [projectId], references: [id], onDelete: Cascade)
|
||||
name String
|
||||
value Float? // always defined if data type is NUMERIC or BOOLEAN, optional for CATEGORICAL
|
||||
source ScoreSource
|
||||
authorUserId String? @map("author_user_id")
|
||||
authorUserId String? @map("author_user_id")
|
||||
comment String?
|
||||
traceId String @map("trace_id")
|
||||
observationId String? @map("observation_id")
|
||||
configId String? @map("config_id")
|
||||
stringValue String? @map("string_value") // always defined if data type is CATEGORICAL or BOOLEAN, null for NUMERIC
|
||||
queueId String? @map("queue_id")
|
||||
createdAt DateTime @default(now()) @map("created_at")
|
||||
updatedAt DateTime @default(now()) @updatedAt @map("updated_at")
|
||||
dataType ScoreDataType @default(NUMERIC) @map("data_type")
|
||||
JobExecution JobExecution[]
|
||||
scoreConfig ScoreConfig? @relation(fields: [configId], references: [id], onDelete: SetNull)
|
||||
traceId String @map("trace_id")
|
||||
observationId String? @map("observation_id")
|
||||
configId String? @map("config_id")
|
||||
stringValue String? @map("string_value") // always defined if data type is CATEGORICAL or BOOLEAN, null for NUMERIC
|
||||
queueId String? @map("queue_id")
|
||||
createdAt DateTime @default(now()) @map("created_at")
|
||||
updatedAt DateTime @default(now()) @updatedAt @map("updated_at")
|
||||
dataType ScoreDataType @default(NUMERIC) @map("data_type")
|
||||
scoreConfig ScoreConfig? @relation(fields: [configId], references: [id], onDelete: SetNull)
|
||||
|
||||
@@unique([id, projectId]) // used for upserts via prisma
|
||||
@@index(timestamp)
|
||||
@@ -900,10 +900,9 @@ model JobExecution {
|
||||
|
||||
jobInputObservationId String? @map("job_input_observation_id") // no fk constraint - observations in ClickHouse, deletion handled via project cascade
|
||||
|
||||
jobInputDatasetItemId String? @map("job_input_dataset_item_id") // no fk constraint - job execution sensible standalone
|
||||
jobInputDatasetItemId String? @map("job_input_dataset_item_id") // no fk constraint - job execution sensible standalone
|
||||
|
||||
jobOutputScoreId String? @map("job_output_score_id")
|
||||
score Score? @relation(fields: [jobOutputScoreId], references: [id], onDelete: SetNull) // job remains when scores are deleted
|
||||
|
||||
@@index([projectId, status])
|
||||
@@index([projectId, id])
|
||||
|
||||
@@ -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,
|
||||
@@ -510,6 +513,9 @@ export async function createDatasets(
|
||||
Math.floor(Math.random() * relevantObservations.length)
|
||||
];
|
||||
|
||||
if (!observation) {
|
||||
continue;
|
||||
}
|
||||
await prisma.datasetRunItems.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,
|
||||
|
||||
@@ -5,6 +5,7 @@ const EnvSchema = z.object({
|
||||
NODE_ENV: z
|
||||
.enum(["development", "test", "production"])
|
||||
.default("development"),
|
||||
NEXTAUTH_URL: z.string().url().optional(),
|
||||
REDIS_HOST: z.string().nullish(),
|
||||
REDIS_PORT: z.coerce
|
||||
.number({
|
||||
@@ -27,15 +28,19 @@ 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_SDK_CI_SYNC_PROCESSING_ENABLED: z
|
||||
.enum(["true", "false"])
|
||||
.default("false"),
|
||||
LANGFUSE_ASYNC_INGESTION_PROCESSING: z
|
||||
LANGFUSE_CLICKHOUSE_INGESTION_ENABLED: z
|
||||
.enum(["true", "false"])
|
||||
.default("true"),
|
||||
LANGFUSE_POSTGRES_INGESTION_ENABLED: z
|
||||
.enum(["true", "false"])
|
||||
.default("false"),
|
||||
|
||||
LANGFUSE_INGESTION_QUEUE_DELAY_MS: z.coerce
|
||||
.number()
|
||||
.nonnegative()
|
||||
@@ -48,8 +53,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 +68,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));
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import { z } from "zod";
|
||||
|
||||
const MAX_COMMENT_LENGTH = 3000;
|
||||
|
||||
const COMMENT_OBJECT_TYPES = [
|
||||
"TRACE",
|
||||
"OBSERVATION",
|
||||
@@ -9,7 +11,7 @@ const COMMENT_OBJECT_TYPES = [
|
||||
|
||||
export const CreateCommentData = z.object({
|
||||
projectId: z.string(),
|
||||
content: z.string().trim().min(1).max(500),
|
||||
content: z.string().trim().min(1).max(MAX_COMMENT_LENGTH),
|
||||
objectId: z.string(),
|
||||
objectType: z.enum(COMMENT_OBJECT_TYPES),
|
||||
});
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
import { Prisma } from "../../db";
|
||||
|
||||
export const datasetItemMatchesVariable = (
|
||||
input: Prisma.JsonValue,
|
||||
variable: string,
|
||||
) => {
|
||||
if (
|
||||
input === null ||
|
||||
input === undefined ||
|
||||
typeof input !== "object" ||
|
||||
Array.isArray(input)
|
||||
)
|
||||
return false;
|
||||
return Object.keys(input).includes(variable);
|
||||
};
|
||||
@@ -6,7 +6,7 @@ import { isPresent } from "../../utils/typeChecks";
|
||||
import {
|
||||
jsonSchema,
|
||||
paginationMetaResponseZod,
|
||||
paginationZod,
|
||||
publicApiPaginationZod,
|
||||
} from "../../utils/zod";
|
||||
|
||||
/**
|
||||
@@ -17,7 +17,7 @@ export type ValidatedScoreConfig = z.infer<typeof ValidatedScoreConfigSchema>;
|
||||
|
||||
const validateCategories = (
|
||||
categories: ConfigCategory[],
|
||||
ctx: z.RefinementCtx
|
||||
ctx: z.RefinementCtx,
|
||||
) => {
|
||||
const uniqueNames = new Set<string>();
|
||||
const uniqueValues = new Set<number>();
|
||||
@@ -94,7 +94,7 @@ const BooleanScoreConfig = z.object({
|
||||
return categories.every(
|
||||
(category, index) =>
|
||||
category.label === expectedCategories[index].label &&
|
||||
category.value === expectedCategories[index].value
|
||||
category.value === expectedCategories[index].value,
|
||||
);
|
||||
}),
|
||||
});
|
||||
@@ -123,7 +123,7 @@ const ValidatedScoreConfigSchema = z
|
||||
minValue: z.undefined().nullish(),
|
||||
dataType: z.literal("CATEGORICAL"),
|
||||
categories: Categories.superRefine(validateCategories),
|
||||
})
|
||||
}),
|
||||
),
|
||||
ScoreConfigBase.merge(BooleanScoreConfig),
|
||||
])
|
||||
@@ -150,7 +150,7 @@ const ValidatedScoreConfigSchema = z
|
||||
*/
|
||||
export const filterAndValidateDbScoreConfigList = (
|
||||
scoreConfigs: ScoreConfigDbType[],
|
||||
onParseError?: (error: z.ZodError) => void
|
||||
onParseError?: (error: z.ZodError) => void,
|
||||
): ValidatedScoreConfig[] =>
|
||||
scoreConfigs.reduce((acc, ts) => {
|
||||
const result = ValidatedScoreConfigSchema.safeParse(ts);
|
||||
@@ -170,7 +170,7 @@ export const filterAndValidateDbScoreConfigList = (
|
||||
* @throws error if score fails validation
|
||||
*/
|
||||
export const validateDbScoreConfig = (
|
||||
scoreConfig: ScoreConfigDbType
|
||||
scoreConfig: ScoreConfigDbType,
|
||||
): ValidatedScoreConfig => ValidatedScoreConfigSchema.parse(scoreConfig);
|
||||
|
||||
/**
|
||||
@@ -205,7 +205,7 @@ export const PostScoreConfigBody = z
|
||||
z.object({
|
||||
dataType: z.literal("BOOLEAN"),
|
||||
categories: z.undefined().nullish(),
|
||||
})
|
||||
}),
|
||||
),
|
||||
])
|
||||
.superRefine((data, ctx) => {
|
||||
@@ -227,7 +227,7 @@ export const PostScoreConfigResponse = ValidatedScoreConfigSchema;
|
||||
|
||||
// GET /score-configs
|
||||
export const GetScoreConfigsQuery = z.object({
|
||||
...paginationZod,
|
||||
...publicApiPaginationZod,
|
||||
});
|
||||
|
||||
export const GetScoreConfigsResponse = z.object({
|
||||
|
||||
@@ -6,7 +6,7 @@ import { isPresent, stringDateTime } from "../../utils/typeChecks";
|
||||
import {
|
||||
NonEmptyString,
|
||||
paginationMetaResponseZod,
|
||||
paginationZod,
|
||||
publicApiPaginationZod,
|
||||
} from "../../utils/zod";
|
||||
import { Category as ConfigCategory } from "./scoreConfigTypes";
|
||||
|
||||
@@ -84,13 +84,13 @@ export const ScoreBodyWithoutConfig = z.discriminatedUnion("dataType", [
|
||||
z.object({
|
||||
value: z.number(),
|
||||
dataType: z.literal("NUMERIC"),
|
||||
})
|
||||
}),
|
||||
),
|
||||
BaseScoreBody.merge(
|
||||
z.object({
|
||||
value: z.string(),
|
||||
dataType: z.literal("CATEGORICAL"),
|
||||
})
|
||||
}),
|
||||
),
|
||||
BaseScoreBody.merge(
|
||||
z.object({
|
||||
@@ -98,7 +98,7 @@ export const ScoreBodyWithoutConfig = z.discriminatedUnion("dataType", [
|
||||
message: "Value must be either 0 or 1",
|
||||
}),
|
||||
dataType: z.literal("BOOLEAN"),
|
||||
})
|
||||
}),
|
||||
),
|
||||
]);
|
||||
|
||||
@@ -162,7 +162,7 @@ export const ScorePropsAgainstConfig = z.union([
|
||||
*/
|
||||
export const filterAndValidateDbScoreList = (
|
||||
scores: Score[],
|
||||
onParseError?: (error: z.ZodError) => void
|
||||
onParseError?: (error: z.ZodError) => void,
|
||||
): APIScore[] =>
|
||||
scores.reduce((acc, ts) => {
|
||||
const result = APIScoreSchema.safeParse(ts);
|
||||
@@ -199,14 +199,14 @@ export const PostScoresBody = z.discriminatedUnion("dataType", [
|
||||
value: z.number(),
|
||||
dataType: z.literal("NUMERIC"),
|
||||
configId: z.string().nullish(),
|
||||
})
|
||||
}),
|
||||
),
|
||||
BaseScoreBody.merge(
|
||||
z.object({
|
||||
value: z.string(),
|
||||
dataType: z.literal("CATEGORICAL"),
|
||||
configId: z.string().nullish(),
|
||||
})
|
||||
}),
|
||||
),
|
||||
BaseScoreBody.merge(
|
||||
z.object({
|
||||
@@ -216,14 +216,14 @@ export const PostScoresBody = z.discriminatedUnion("dataType", [
|
||||
}),
|
||||
dataType: z.literal("BOOLEAN"),
|
||||
configId: z.string().nullish(),
|
||||
})
|
||||
}),
|
||||
),
|
||||
BaseScoreBody.merge(
|
||||
z.object({
|
||||
value: z.union([z.string(), z.number()]),
|
||||
dataType: z.undefined(),
|
||||
configId: z.string().nullish(),
|
||||
})
|
||||
}),
|
||||
),
|
||||
]);
|
||||
|
||||
@@ -231,7 +231,7 @@ export const PostScoresResponse = z.object({ id: z.string() });
|
||||
|
||||
// GET /scores
|
||||
export const GetScoresQuery = z.object({
|
||||
...paginationZod,
|
||||
...publicApiPaginationZod,
|
||||
userId: z.string().nullish(),
|
||||
dataType: z.enum(ScoreDataType).nullish(),
|
||||
configId: z.string().nullish(),
|
||||
@@ -260,7 +260,7 @@ const LegacyGetScoreResponseDataV1 = z.intersection(
|
||||
userId: z.string().nullish(),
|
||||
tags: z.array(z.string()).nullish(),
|
||||
}),
|
||||
})
|
||||
}),
|
||||
);
|
||||
export const GetScoresResponse = z.object({
|
||||
data: z.array(LegacyGetScoreResponseDataV1),
|
||||
@@ -269,7 +269,7 @@ export const GetScoresResponse = z.object({
|
||||
|
||||
export const legacyFilterAndValidateV1GetScoreList = (
|
||||
scores: unknown[],
|
||||
onParseError?: (error: z.ZodError) => void
|
||||
onParseError?: (error: z.ZodError) => void,
|
||||
): z.infer<typeof LegacyGetScoreResponseDataV1>[] =>
|
||||
scores.reduce(
|
||||
(acc: z.infer<typeof LegacyGetScoreResponseDataV1>[], ts) => {
|
||||
@@ -282,7 +282,7 @@ export const legacyFilterAndValidateV1GetScoreList = (
|
||||
}
|
||||
return acc;
|
||||
},
|
||||
[] as z.infer<typeof LegacyGetScoreResponseDataV1>[]
|
||||
[] as z.infer<typeof LegacyGetScoreResponseDataV1>[],
|
||||
);
|
||||
|
||||
// GET /scores/{scoreId}
|
||||
|
||||
@@ -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";
|
||||
@@ -34,6 +32,9 @@ export * from "./features/scores";
|
||||
// comments
|
||||
export * from "./features/comments/types";
|
||||
|
||||
// experiments
|
||||
export * from "./features/experiments/utils";
|
||||
|
||||
// export db types only
|
||||
export * from "@prisma/client";
|
||||
export { type DB } from "../prisma/generated/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,12 +22,16 @@ 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/projectDelete";
|
||||
export * from "./redis/datasetRunItemUpsert";
|
||||
export * from "./redis/batchExport";
|
||||
export * from "./redis/legacyIngestion";
|
||||
export * from "./redis/ingestionQueue";
|
||||
export * from "./redis/postHogIntegrationQueue";
|
||||
export * from "./redis/postHogIntegrationProcessingQueue";
|
||||
export * from "./redis/experimentCreateQueue";
|
||||
export * from "./auth/types";
|
||||
export * from "./ingestion/legacy/index";
|
||||
@@ -39,3 +44,6 @@ export * from "./logger";
|
||||
export * from "./queries";
|
||||
export * from "./repositories";
|
||||
export * from "./redis/evalExecutionQueue";
|
||||
|
||||
// test utils
|
||||
export * from "./test-utils";
|
||||
|
||||
@@ -136,7 +136,7 @@ export class ObservationProcessor implements EventProcessor {
|
||||
|
||||
// Token counts
|
||||
const [newInputCount, newOutputCount] =
|
||||
"usage" in this.event.body
|
||||
"usage" in this.event.body || "usageDetails" in this.event.body
|
||||
? await this.calculateTokenCounts(
|
||||
apiScope.projectId,
|
||||
this.event.body,
|
||||
@@ -147,8 +147,9 @@ export class ObservationProcessor implements EventProcessor {
|
||||
: [undefined, undefined];
|
||||
|
||||
const newTotalCount =
|
||||
"usage" in this.event.body
|
||||
"usage" in this.event.body || "usageDetails" in this.event.body
|
||||
? (this.event.body.usage?.total ??
|
||||
this.event.body.usageDetails?.total ??
|
||||
(newInputCount != null || newOutputCount != null
|
||||
? (newInputCount ?? 0) + (newOutputCount ?? 0)
|
||||
: undefined))
|
||||
@@ -158,15 +159,24 @@ export class ObservationProcessor implements EventProcessor {
|
||||
inputCost:
|
||||
"usage" in this.event.body && this.event.body.usage?.inputCost != null // inputCost can be explicitly 0. Note only one equal sign to capture null AND undefined
|
||||
? new Decimal(this.event.body.usage?.inputCost)
|
||||
: existingObservation?.inputCost,
|
||||
: "costDetails" in this.event.body &&
|
||||
this.event.body.costDetails?.input != null
|
||||
? new Decimal(this.event.body.costDetails?.input)
|
||||
: existingObservation?.inputCost,
|
||||
outputCost:
|
||||
"usage" in this.event.body && this.event.body.usage?.outputCost != null // outputCost can be explicitly 0. Note only one equal sign to capture null AND undefined
|
||||
? new Decimal(this.event.body.usage?.outputCost)
|
||||
: existingObservation?.outputCost,
|
||||
: "costDetails" in this.event.body &&
|
||||
this.event.body.costDetails?.output != null
|
||||
? new Decimal(this.event.body.costDetails?.output)
|
||||
: existingObservation?.outputCost,
|
||||
totalCost:
|
||||
"usage" in this.event.body && this.event.body.usage?.totalCost != null // totalCost can be explicitly 0. Note only one equal sign to capture null AND undefined
|
||||
? new Decimal(this.event.body.usage?.totalCost)
|
||||
: existingObservation?.totalCost,
|
||||
: "costDetails" in this.event.body &&
|
||||
this.event.body.costDetails?.total != null
|
||||
? new Decimal(this.event.body.costDetails?.total)
|
||||
: existingObservation?.totalCost,
|
||||
};
|
||||
|
||||
const tokenCounts = {
|
||||
@@ -384,7 +394,7 @@ export class ObservationProcessor implements EventProcessor {
|
||||
model?: Model,
|
||||
existingObservation?: Omit<Observation, "input" | "output">,
|
||||
) {
|
||||
let newPromptTokens = body.usage?.input;
|
||||
let newPromptTokens = body.usage?.input || body.usageDetails?.input;
|
||||
if (newPromptTokens === undefined && model && model.tokenizerId) {
|
||||
if (body.input) {
|
||||
newPromptTokens = calculateTokenDelegate({
|
||||
@@ -409,7 +419,7 @@ export class ObservationProcessor implements EventProcessor {
|
||||
}
|
||||
}
|
||||
|
||||
let newCompletionTokens = body.usage?.output;
|
||||
let newCompletionTokens = body.usage?.output || body.usageDetails?.output;
|
||||
|
||||
if (newCompletionTokens === undefined && model && model.tokenizerId) {
|
||||
if (body.output) {
|
||||
|
||||
@@ -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,106 @@ 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,
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// This is a workaround to allow us to disable async ingestion processing for SDK CI testing
|
||||
// TODO: remove this block after SDKs are ready for V3 async ingestion processing
|
||||
if (env.LANGFUSE_SDK_CI_SYNC_PROCESSING_ENABLED === "true") {
|
||||
const result = await handleBatch(
|
||||
sortedBatch,
|
||||
authCheck,
|
||||
tokenCountDelegate,
|
||||
);
|
||||
|
||||
// in case we did not return early, we return the result here
|
||||
return aggregateBatchResult(
|
||||
[...validationErrors, ...authenticationErrors, ...result.errors],
|
||||
result.results,
|
||||
authCheck.scope.projectId,
|
||||
);
|
||||
}
|
||||
|
||||
// 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();
|
||||
// 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 (queue) {
|
||||
let addToQueueFailed = false;
|
||||
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,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const queuePayload: LegacyIngestionEventType =
|
||||
env.LANGFUSE_S3_EVENT_UPLOAD_ENABLED === "true" && !s3UploadErrored
|
||||
if (env.LANGFUSE_POSTGRES_INGESTION_ENABLED === "true") {
|
||||
// As part of the legacy processing we sent the entire batch to the worker.
|
||||
if (redis) {
|
||||
const queue = LegacyIngestionQueue.getInstance();
|
||||
|
||||
if (queue) {
|
||||
let addToQueueFailed = false;
|
||||
|
||||
const queuePayload: LegacyIngestionEventType = !s3UploadErrored
|
||||
? {
|
||||
data: Object.keys(sortedBatchByEventBodyId).map((id) => {
|
||||
const { key, type, eventBodyId } = sortedBatchByEventBodyId[id];
|
||||
@@ -255,44 +270,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 +362,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 +409,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) => {
|
||||
|
||||
@@ -56,10 +56,66 @@ export const usage = MixedUsage.nullish()
|
||||
// ensure output is always of new usage model
|
||||
.pipe(Usage.nullish());
|
||||
|
||||
const RawUsageOrCostDetails = z.record(
|
||||
z.string(),
|
||||
z.number().nonnegative().nullish(),
|
||||
);
|
||||
|
||||
const OpenAIUsageSchema = z
|
||||
.object({
|
||||
prompt_tokens: z.number().nonnegative(),
|
||||
completion_tokens: z.number().nonnegative(),
|
||||
total_tokens: z.number().nonnegative(),
|
||||
prompt_tokens_details: z.record(z.string(), z.number().nonnegative()),
|
||||
completion_tokens_details: z.record(z.string(), z.number().nonnegative()),
|
||||
})
|
||||
.strict()
|
||||
.transform((v) => {
|
||||
if (!v) return;
|
||||
|
||||
const {
|
||||
prompt_tokens,
|
||||
completion_tokens,
|
||||
total_tokens,
|
||||
prompt_tokens_details,
|
||||
completion_tokens_details,
|
||||
} = v;
|
||||
const result: z.infer<typeof RawUsageOrCostDetails> & {
|
||||
input: number;
|
||||
output: number;
|
||||
total: number;
|
||||
} = {
|
||||
input: prompt_tokens,
|
||||
output: completion_tokens,
|
||||
total: total_tokens,
|
||||
};
|
||||
|
||||
if (prompt_tokens_details) {
|
||||
for (const [key, value] of Object.entries(prompt_tokens_details)) {
|
||||
result[`input_${key}`] = value;
|
||||
result.input = Math.max(result.input - (value ?? 0), 0);
|
||||
}
|
||||
}
|
||||
|
||||
if (completion_tokens_details) {
|
||||
for (const [key, value] of Object.entries(completion_tokens_details)) {
|
||||
result[`output_${key}`] = value;
|
||||
result.output = Math.max(result.output - (value ?? 0), 0);
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
})
|
||||
.pipe(RawUsageOrCostDetails);
|
||||
|
||||
export const UsageOrCostDetails = z
|
||||
.union([OpenAIUsageSchema, RawUsageOrCostDetails])
|
||||
.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(),
|
||||
@@ -119,6 +175,8 @@ export const CreateGenerationBody = CreateSpanBody.extend({
|
||||
)
|
||||
.nullish(),
|
||||
usage: usage,
|
||||
usageDetails: UsageOrCostDetails,
|
||||
costDetails: UsageOrCostDetails,
|
||||
promptName: z.string().nullish(),
|
||||
promptVersion: z.number().int().nullish(),
|
||||
}).refine((value) => {
|
||||
@@ -147,6 +205,8 @@ export const UpdateGenerationBody = UpdateSpanBody.extend({
|
||||
)
|
||||
.nullish(),
|
||||
usage: usage,
|
||||
usageDetails: UsageOrCostDetails,
|
||||
costDetails: UsageOrCostDetails,
|
||||
promptName: z.string().nullish(),
|
||||
promptVersion: z.number().int().nullish(),
|
||||
}).refine((value) => {
|
||||
@@ -298,6 +358,8 @@ export const LegacyObservationBody = z.object({
|
||||
input: jsonSchema.nullish(),
|
||||
output: jsonSchema.nullish(),
|
||||
usage: usage,
|
||||
usageDetails: UsageOrCostDetails,
|
||||
costDetails: UsageOrCostDetails,
|
||||
metadata: jsonSchema.nullish(),
|
||||
parentObservationId: z.string().nullish(),
|
||||
level: z.nativeEnum(ObservationLevel).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,9 +3,11 @@ 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,
|
||||
BaseMessage,
|
||||
HumanMessage,
|
||||
SystemMessage,
|
||||
} from "@langchain/core/messages";
|
||||
@@ -15,7 +17,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";
|
||||
@@ -130,16 +132,28 @@ export async function fetchLLMCompletion(
|
||||
|
||||
finalCallbacks = finalCallbacks.length > 0 ? finalCallbacks : undefined;
|
||||
|
||||
const finalMessages = messages.map((message) => {
|
||||
if (message.role === ChatMessageRole.User)
|
||||
return new HumanMessage(message.content);
|
||||
if (message.role === ChatMessageRole.System)
|
||||
return new SystemMessage(message.content);
|
||||
let finalMessages: BaseMessage[];
|
||||
// VertexAI requires at least 1 user message
|
||||
if (modelParams.adapter === LLMAdapter.VertexAI && messages.length === 1) {
|
||||
finalMessages = [new HumanMessage(messages[0].content)];
|
||||
} else {
|
||||
finalMessages = messages.map((message) => {
|
||||
if (message.role === ChatMessageRole.User)
|
||||
return new HumanMessage(message.content);
|
||||
if (message.role === ChatMessageRole.System)
|
||||
return new SystemMessage(message.content);
|
||||
|
||||
return new AIMessage(message.content);
|
||||
});
|
||||
return new AIMessage(message.content);
|
||||
});
|
||||
}
|
||||
|
||||
let chatModel: ChatOpenAI | ChatAnthropic | ChatBedrockConverse;
|
||||
finalMessages = finalMessages.filter((m) => m.content.length > 0);
|
||||
|
||||
let chatModel:
|
||||
| ChatOpenAI
|
||||
| ChatAnthropic
|
||||
| ChatBedrockConverse
|
||||
| ChatVertexAI;
|
||||
if (modelParams.adapter === LLMAdapter.Anthropic) {
|
||||
chatModel = new ChatAnthropic({
|
||||
anthropicApiKey: apiKey,
|
||||
@@ -149,7 +163,7 @@ export async function fetchLLMCompletion(
|
||||
maxTokens: modelParams.max_tokens,
|
||||
topP: modelParams.top_p,
|
||||
callbacks: finalCallbacks,
|
||||
clientOptions: { maxRetries },
|
||||
clientOptions: { maxRetries, timeout: 1000 * 60 * 2 }, // 2 minutes timeout
|
||||
});
|
||||
} else if (modelParams.adapter === LLMAdapter.OpenAI) {
|
||||
chatModel = new ChatOpenAI({
|
||||
@@ -164,6 +178,7 @@ export async function fetchLLMCompletion(
|
||||
configuration: {
|
||||
baseURL,
|
||||
},
|
||||
timeout: 1000 * 60 * 2, // 2 minutes timeout
|
||||
});
|
||||
} else if (modelParams.adapter === LLMAdapter.Azure) {
|
||||
chatModel = new ChatOpenAI({
|
||||
@@ -176,6 +191,7 @@ export async function fetchLLMCompletion(
|
||||
topP: modelParams.top_p,
|
||||
callbacks: finalCallbacks,
|
||||
maxRetries,
|
||||
timeout: 1000 * 60 * 2, // 2 minutes timeout
|
||||
});
|
||||
} else if (modelParams.adapter === LLMAdapter.Bedrock) {
|
||||
const { region } = BedrockConfigSchema.parse(config);
|
||||
@@ -190,6 +206,24 @@ export async function fetchLLMCompletion(
|
||||
topP: modelParams.top_p,
|
||||
callbacks: finalCallbacks,
|
||||
maxRetries,
|
||||
timeout: 1000 * 60 * 2, // 2 minutes timeout
|
||||
});
|
||||
} else if (modelParams.adapter === LLMAdapter.VertexAI) {
|
||||
const credentials = GCPServiceAccountKeySchema.parse(JSON.parse(apiKey));
|
||||
|
||||
// Requests time out after 60 seconds for both public and private endpoints by default
|
||||
// Reference: https://cloud.google.com/vertex-ai/docs/predictions/get-online-predictions#send-request
|
||||
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
|
||||
@@ -237,6 +271,7 @@ export async function fetchLLMCompletion(
|
||||
configuration: {
|
||||
baseURL,
|
||||
},
|
||||
timeout: 1000 * 60 * 2, // 2 minutes timeout
|
||||
})
|
||||
.pipe(new StringOutputParser())
|
||||
.invoke(
|
||||
|
||||
@@ -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(", ")}`;
|
||||
}
|
||||
|
||||
@@ -10,6 +10,8 @@ import { TableFilters } from "./types";
|
||||
type AdditionalObservationFields = {
|
||||
traceName: string | null;
|
||||
traceTags: Array<string>;
|
||||
usageDetails: Record<string, number>;
|
||||
costDetails: Record<string, number>;
|
||||
};
|
||||
|
||||
export type FullObservation = AdditionalObservationFields & ObservationView;
|
||||
|
||||
@@ -20,3 +20,4 @@ export {
|
||||
NullFilter,
|
||||
type ClickhouseOperator,
|
||||
} from "./clickhouse-sql/clickhouse-filter";
|
||||
export { orderByToClickhouseSql } from "./clickhouse-sql/orderby-factory";
|
||||
|
||||
@@ -1,15 +1,6 @@
|
||||
import { z } from "zod";
|
||||
import { eventTypes, ingestionBatchEvent } from ".";
|
||||
|
||||
export enum EventName {
|
||||
TraceUpsert = "TraceUpsert",
|
||||
BatchExport = "BatchExport",
|
||||
EvaluationExecution = "EvaluationExecution",
|
||||
LegacyIngestion = "LegacyIngestion",
|
||||
CloudUsageMetering = "CloudUsageMetering",
|
||||
ExperimentCreate = "ExperimentCreate",
|
||||
}
|
||||
|
||||
export const LegacyIngestionEventFull = z.object({
|
||||
useS3EventStore: z.literal(false),
|
||||
data: ingestionBatchEvent,
|
||||
@@ -63,10 +54,18 @@ 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(),
|
||||
});
|
||||
export const TracesQueueEventSchema = z.object({
|
||||
projectId: z.string(),
|
||||
traceIds: z.array(z.string()),
|
||||
});
|
||||
export const ProjectQueueEventSchema = z.object({
|
||||
projectId: z.string(),
|
||||
orgId: z.string(),
|
||||
});
|
||||
export const DatasetRunItemUpsertEventSchema = z.object({
|
||||
projectId: z.string(),
|
||||
datasetItemId: z.string(),
|
||||
@@ -78,7 +77,9 @@ export const EvalExecutionEvent = z.object({
|
||||
jobExecutionId: z.string(),
|
||||
delay: z.number().nullish(),
|
||||
});
|
||||
|
||||
export const PostHogIntegrationProcessingEventSchema = z.object({
|
||||
projectId: z.string(),
|
||||
});
|
||||
export const ExperimentCreateEventSchema = z.object({
|
||||
projectId: z.string(),
|
||||
datasetId: z.string(),
|
||||
@@ -87,7 +88,9 @@ 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 TracesQueueEventType = z.infer<typeof TracesQueueEventSchema>;
|
||||
export type ProjectQueueEventType = z.infer<typeof ProjectQueueEventSchema>;
|
||||
export type DatasetRunItemUpsertEventType = z.infer<
|
||||
typeof DatasetRunItemUpsertEventSchema
|
||||
>;
|
||||
@@ -97,29 +100,14 @@ export type IngestionEventQueueType = z.infer<typeof IngestionEvent>;
|
||||
export type ExperimentCreateEventType = z.infer<
|
||||
typeof ExperimentCreateEventSchema
|
||||
>;
|
||||
|
||||
export const EventBodySchema = z.union([
|
||||
z.object({
|
||||
name: z.literal(EventName.TraceUpsert),
|
||||
payload: z.array(TraceUpsertEventSchema),
|
||||
}),
|
||||
z.object({
|
||||
name: z.literal(EventName.EvaluationExecution),
|
||||
payload: EvalExecutionEvent,
|
||||
}),
|
||||
z.object({
|
||||
name: z.literal(EventName.BatchExport),
|
||||
payload: BatchExportJobSchema,
|
||||
}),
|
||||
z.object({
|
||||
name: z.literal(EventName.ExperimentCreate),
|
||||
payload: ExperimentCreateEventSchema,
|
||||
}),
|
||||
]);
|
||||
export type EventBodyType = z.infer<typeof EventBodySchema>;
|
||||
export type PostHogIntegrationProcessingEventType = z.infer<
|
||||
typeof PostHogIntegrationProcessingEventSchema
|
||||
>;
|
||||
|
||||
export enum QueueName {
|
||||
TraceUpsert = "trace-upsert", // Ingestion pipeline adds events on each Trace upsert
|
||||
TraceDelete = "trace-delete",
|
||||
ProjectDelete = "project-delete",
|
||||
EvaluationExecution = "evaluation-execution-queue", // Worker executes Evals
|
||||
DatasetRunItemUpsert = "dataset-run-item-upsert-queue",
|
||||
BatchExport = "batch-export-queue",
|
||||
@@ -127,27 +115,44 @@ export enum QueueName {
|
||||
LegacyIngestionQueue = "legacy-ingestion-queue", // Used for batch processing of Ingestion
|
||||
CloudUsageMeteringQueue = "cloud-usage-metering-queue",
|
||||
ExperimentCreate = "experiment-create-queue",
|
||||
PostHogIntegrationQueue = "posthog-integration-queue",
|
||||
PostHogIntegrationProcessingQueue = "posthog-integration-processing-queue",
|
||||
}
|
||||
|
||||
export enum QueueJobs {
|
||||
TraceUpsert = "trace-upsert",
|
||||
TraceDelete = "trace-delete",
|
||||
ProjectDelete = "project-delete",
|
||||
DatasetRunItemUpsert = "dataset-run-item-upsert",
|
||||
EvaluationExecution = "evaluation-execution-job",
|
||||
BatchExportJob = "batch-export-job",
|
||||
EnqueueBatchExportJobs = "enqueue-batch-export-jobs",
|
||||
LegacyIngestionJob = "legacy-ingestion-job",
|
||||
CloudUsageMeteringJob = "cloud-usage-metering-job",
|
||||
IngestionJob = "ingestion-job",
|
||||
ExperimentCreateJob = "experiment-create-job",
|
||||
PostHogIntegrationJob = "posthog-integration-job",
|
||||
PostHogIntegrationProcessingJob = "posthog-integration-processing-job",
|
||||
}
|
||||
|
||||
export type TQueueJobTypes = {
|
||||
[QueueName.TraceUpsert]: {
|
||||
timestamp: Date;
|
||||
id: string;
|
||||
payload: TraceUpsertEventType;
|
||||
payload: TraceQueueEventType;
|
||||
name: QueueJobs.TraceUpsert;
|
||||
};
|
||||
[QueueName.TraceDelete]: {
|
||||
timestamp: Date;
|
||||
id: string;
|
||||
payload: TracesQueueEventType | TraceQueueEventType;
|
||||
name: QueueJobs.TraceDelete;
|
||||
};
|
||||
[QueueName.ProjectDelete]: {
|
||||
timestamp: Date;
|
||||
id: string;
|
||||
payload: ProjectQueueEventType;
|
||||
name: QueueJobs.ProjectDelete;
|
||||
};
|
||||
[QueueName.DatasetRunItemUpsert]: {
|
||||
timestamp: Date;
|
||||
id: string;
|
||||
@@ -184,4 +189,10 @@ export type TQueueJobTypes = {
|
||||
payload: ExperimentCreateEventType;
|
||||
name: QueueJobs.ExperimentCreateJob;
|
||||
};
|
||||
[QueueName.PostHogIntegrationProcessingQueue]: {
|
||||
timestamp: Date;
|
||||
id: string;
|
||||
payload: PostHogIntegrationProcessingEventType;
|
||||
name: QueueJobs.PostHogIntegrationProcessingJob;
|
||||
};
|
||||
};
|
||||
|
||||
+2
-2
@@ -1,8 +1,8 @@
|
||||
import { Queue } from "bullmq";
|
||||
import { env } from "../..";
|
||||
import { logger } from "@azure/storage-blob";
|
||||
import { env } from "../../env";
|
||||
import { QueueName, QueueJobs } from "../queues";
|
||||
import { createNewRedisInstance, redisQueueRetryOptions } from "./redis";
|
||||
import { logger } from "../logger";
|
||||
|
||||
export class CloudUsageMeteringQueue {
|
||||
private static instance: Queue | null = null;
|
||||
@@ -29,7 +29,7 @@ export class EvalExecutionQueue {
|
||||
attempts: 10,
|
||||
backoff: {
|
||||
type: "exponential",
|
||||
delay: 5000,
|
||||
delay: 1000,
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
@@ -26,10 +26,10 @@ export class ExperimentCreateQueue {
|
||||
defaultJobOptions: {
|
||||
removeOnComplete: true,
|
||||
removeOnFail: 10_000,
|
||||
attempts: 2,
|
||||
attempts: 10,
|
||||
backoff: {
|
||||
type: "exponential",
|
||||
delay: 5000,
|
||||
delay: 1000,
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
@@ -1,13 +1,17 @@
|
||||
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";
|
||||
import { ProjectDeleteQueue } from "./projectDelete";
|
||||
import { PostHogIntegrationQueue } from "./postHogIntegrationQueue";
|
||||
import { PostHogIntegrationProcessingQueue } from "./postHogIntegrationProcessingQueue";
|
||||
|
||||
export function getQueue(queueName: QueueName): Queue | null {
|
||||
switch (queueName) {
|
||||
@@ -25,8 +29,16 @@ 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();
|
||||
case QueueName.ProjectDelete:
|
||||
return ProjectDeleteQueue.getInstance();
|
||||
case QueueName.PostHogIntegrationQueue:
|
||||
return PostHogIntegrationQueue.getInstance();
|
||||
case QueueName.PostHogIntegrationProcessingQueue:
|
||||
return PostHogIntegrationProcessingQueue.getInstance();
|
||||
default:
|
||||
const exhaustiveCheckDefault: never = queueName;
|
||||
throw new Error(`Queue ${queueName} not found`);
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
import { Queue } from "bullmq";
|
||||
import { QueueName } from "../queues";
|
||||
import { createNewRedisInstance, redisQueueRetryOptions } from "./redis";
|
||||
import { logger } from "../logger";
|
||||
|
||||
export class PostHogIntegrationProcessingQueue {
|
||||
private static instance: Queue | null = null;
|
||||
|
||||
public static getInstance(): Queue | null {
|
||||
if (PostHogIntegrationProcessingQueue.instance) {
|
||||
return PostHogIntegrationProcessingQueue.instance;
|
||||
}
|
||||
|
||||
const newRedis = createNewRedisInstance({
|
||||
enableOfflineQueue: false,
|
||||
...redisQueueRetryOptions,
|
||||
});
|
||||
|
||||
PostHogIntegrationProcessingQueue.instance = newRedis
|
||||
? new Queue(QueueName.PostHogIntegrationProcessingQueue, {
|
||||
connection: newRedis,
|
||||
defaultJobOptions: {
|
||||
removeOnComplete: true,
|
||||
removeOnFail: 100_000,
|
||||
attempts: 5,
|
||||
backoff: {
|
||||
type: "exponential",
|
||||
delay: 5000,
|
||||
},
|
||||
},
|
||||
})
|
||||
: null;
|
||||
|
||||
PostHogIntegrationProcessingQueue.instance?.on("error", (err) => {
|
||||
logger.error("PostHogIntegrationProcessingQueue error", err);
|
||||
});
|
||||
|
||||
return PostHogIntegrationProcessingQueue.instance;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
import { Queue } from "bullmq";
|
||||
import { QueueName, QueueJobs } from "../queues";
|
||||
import { createNewRedisInstance, redisQueueRetryOptions } from "./redis";
|
||||
import { logger } from "../logger";
|
||||
|
||||
export class PostHogIntegrationQueue {
|
||||
private static instance: Queue | null = null;
|
||||
|
||||
public static getInstance(): Queue | null {
|
||||
if (PostHogIntegrationQueue.instance) {
|
||||
return PostHogIntegrationQueue.instance;
|
||||
}
|
||||
|
||||
const newRedis = createNewRedisInstance({
|
||||
enableOfflineQueue: false,
|
||||
...redisQueueRetryOptions,
|
||||
});
|
||||
|
||||
PostHogIntegrationQueue.instance = newRedis
|
||||
? new Queue(QueueName.PostHogIntegrationQueue, {
|
||||
connection: newRedis,
|
||||
defaultJobOptions: {
|
||||
removeOnComplete: true,
|
||||
removeOnFail: 100,
|
||||
attempts: 5,
|
||||
backoff: {
|
||||
type: "exponential",
|
||||
delay: 5000,
|
||||
},
|
||||
},
|
||||
})
|
||||
: null;
|
||||
|
||||
PostHogIntegrationQueue.instance?.on("error", (err) => {
|
||||
logger.error("PostHogIntegrationQueue error", err);
|
||||
});
|
||||
|
||||
if (PostHogIntegrationQueue.instance) {
|
||||
logger.debug("Scheduling jobs for PostHogIntegrationQueue");
|
||||
PostHogIntegrationQueue.instance
|
||||
.add(
|
||||
QueueJobs.PostHogIntegrationJob,
|
||||
{},
|
||||
{
|
||||
repeat: { pattern: "30 * * * *" }, // every hour at 30 minutes past
|
||||
},
|
||||
)
|
||||
.catch((err) => {
|
||||
logger.error("Error adding PostHogIntegrationJob schedule", err);
|
||||
});
|
||||
}
|
||||
|
||||
return PostHogIntegrationQueue.instance;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
import { QueueName, TQueueJobTypes } from "../queues";
|
||||
import { Queue } from "bullmq";
|
||||
import { createNewRedisInstance, redisQueueRetryOptions } from "./redis";
|
||||
import { logger } from "../logger";
|
||||
|
||||
export class ProjectDeleteQueue {
|
||||
private static instance: Queue<
|
||||
TQueueJobTypes[QueueName.ProjectDelete]
|
||||
> | null = null;
|
||||
|
||||
public static getInstance(): Queue<
|
||||
TQueueJobTypes[QueueName.ProjectDelete]
|
||||
> | null {
|
||||
if (ProjectDeleteQueue.instance) return ProjectDeleteQueue.instance;
|
||||
|
||||
const newRedis = createNewRedisInstance({
|
||||
enableOfflineQueue: false,
|
||||
...redisQueueRetryOptions,
|
||||
});
|
||||
|
||||
ProjectDeleteQueue.instance = newRedis
|
||||
? new Queue<TQueueJobTypes[QueueName.ProjectDelete]>(
|
||||
QueueName.ProjectDelete,
|
||||
{
|
||||
connection: newRedis,
|
||||
defaultJobOptions: {
|
||||
removeOnComplete: true,
|
||||
removeOnFail: 100_000,
|
||||
attempts: 5,
|
||||
backoff: {
|
||||
type: "exponential",
|
||||
delay: 5000,
|
||||
},
|
||||
},
|
||||
},
|
||||
)
|
||||
: null;
|
||||
|
||||
ProjectDeleteQueue.instance?.on("error", (err) => {
|
||||
logger.error("ProjectDeleteQueue error", err);
|
||||
});
|
||||
|
||||
return ProjectDeleteQueue.instance;
|
||||
}
|
||||
}
|
||||
@@ -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,10 +1,4 @@
|
||||
import { randomUUID } from "crypto";
|
||||
import {
|
||||
QueueJobs,
|
||||
QueueName,
|
||||
TQueueJobTypes,
|
||||
TraceUpsertEventType,
|
||||
} from "../queues";
|
||||
import { QueueName, TQueueJobTypes } from "../queues";
|
||||
import { Queue } from "bullmq";
|
||||
import { createNewRedisInstance, redisQueueRetryOptions } from "./redis";
|
||||
import { logger } from "../logger";
|
||||
@@ -32,7 +26,7 @@ export class TraceUpsertQueue {
|
||||
removeOnComplete: 100, // Important: If not true, new jobs for that ID would be ignored as jobs in the complete set are still considered as part of the queue
|
||||
removeOnFail: 100_000,
|
||||
attempts: 5,
|
||||
delay: 10_000, // 10 seconds
|
||||
delay: 15_000, // 15 seconds
|
||||
backoff: {
|
||||
type: "exponential",
|
||||
delay: 5000,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
import { queryClickhouse } from "./clickhouse";
|
||||
import {
|
||||
parseClickhouseUTCDateTimeFormat,
|
||||
queryClickhouse,
|
||||
} from "./clickhouse";
|
||||
import { createFilterFromFilterState } from "../queries/clickhouse-sql/factory";
|
||||
import { FilterState } from "../../types";
|
||||
import {
|
||||
@@ -200,8 +203,8 @@ export const getObservationUsageByTime = async (
|
||||
const query = `
|
||||
SELECT
|
||||
${selectTimeseriesColumn(groupBy, "start_time", "start_time")},
|
||||
sumMap(usage_details)['total'] as sum_usage_details,
|
||||
sumMap(cost_details)['total'] as sum_cost_details,
|
||||
sumMap(usage_details) as units,
|
||||
sumMap(cost_details) as cost,
|
||||
provided_model_name
|
||||
FROM observations o FINAL
|
||||
${tracesFilter ? "LEFT JOIN traces t ON o.trace_id = t.id AND o.project_id = t.project_id" : ""}
|
||||
@@ -214,8 +217,8 @@ export const getObservationUsageByTime = async (
|
||||
|
||||
const result = await queryClickhouse<{
|
||||
start_time: string;
|
||||
sum_usage_details: string;
|
||||
sum_cost_details: number;
|
||||
units: Record<string, number>;
|
||||
cost: Record<string, number>;
|
||||
provided_model_name: string;
|
||||
}>({
|
||||
query,
|
||||
@@ -230,8 +233,18 @@ export const getObservationUsageByTime = async (
|
||||
|
||||
return result.map((row) => ({
|
||||
start_time: new Date(row.start_time),
|
||||
sum_usage_details: Number(row.sum_usage_details),
|
||||
sum_cost_details: row.sum_cost_details,
|
||||
units: Object.fromEntries(
|
||||
Object.entries(row.units ?? {}).map(([key, value]) => [
|
||||
key,
|
||||
Number(value),
|
||||
]),
|
||||
),
|
||||
cost: Object.fromEntries(
|
||||
Object.entries(row.cost ?? {}).map(([key, value]) => [
|
||||
key,
|
||||
Number(value),
|
||||
]),
|
||||
),
|
||||
provided_model_name: row.provided_model_name,
|
||||
}));
|
||||
};
|
||||
@@ -616,6 +629,50 @@ export const getCategoricalScoreTimeSeries = async (
|
||||
});
|
||||
};
|
||||
|
||||
export const getObservationsStatusTimeSeries = async (
|
||||
projectId: string,
|
||||
filter: FilterState,
|
||||
groupBy: DateTrunc | undefined,
|
||||
) => {
|
||||
const chFilter = new FilterList(
|
||||
createFilterFromFilterState(filter, dashboardColumnDefinitions),
|
||||
);
|
||||
const chFilterRes = chFilter.apply();
|
||||
|
||||
const traceFilter = chFilter.find((f) => f.clickhouseTable === "traces");
|
||||
|
||||
const query = `
|
||||
SELECT
|
||||
${groupBy ? selectTimeseriesColumn(groupBy, "o.start_time", "start_time_bucket") + ", " : ""}
|
||||
count(*) as observation_count,
|
||||
level as level
|
||||
FROM observations o
|
||||
${traceFilter ? "JOIN traces t ON o.trace_id = t.id AND o.project_id = t.project_id" : ""}
|
||||
WHERE project_id = {projectId: String}
|
||||
AND o.level IS NOT NULL
|
||||
AND ${chFilterRes?.query}
|
||||
GROUP BY level ${groupBy ? ", start_time_bucket" : ""}
|
||||
${groupBy ? orderByTimeSeries(groupBy, "start_time_bucket") : ""}
|
||||
`;
|
||||
|
||||
const result = await queryClickhouse<{
|
||||
start_time_bucket?: string;
|
||||
observation_count: string;
|
||||
level: string;
|
||||
}>({
|
||||
query,
|
||||
params: { projectId, ...(chFilterRes ? chFilterRes.params : {}) },
|
||||
});
|
||||
|
||||
return result.map((row) => ({
|
||||
start_time_bucket: row.start_time_bucket
|
||||
? parseClickhouseUTCDateTimeFormat(row.start_time_bucket)
|
||||
: undefined,
|
||||
count: Number(row.observation_count),
|
||||
level: row.level,
|
||||
}));
|
||||
};
|
||||
|
||||
const orderByTimeSeries = (dateTrunc: DateTrunc, col: string) => {
|
||||
let interval;
|
||||
switch (dateTrunc) {
|
||||
|
||||
@@ -7,3 +7,5 @@ export * from "./traces_converters";
|
||||
export * from "./scores_converters";
|
||||
export * from "./observations_converters";
|
||||
export * from "./clickhouse";
|
||||
export * from "./constants";
|
||||
export * from "./trace-sessions";
|
||||
|
||||
@@ -38,6 +38,7 @@ import {
|
||||
OBSERVATIONS_TO_TRACE_INTERVAL,
|
||||
TRACE_TO_OBSERVATIONS_INTERVAL,
|
||||
} from "./constants";
|
||||
import { env } from "../../env";
|
||||
|
||||
export const checkObservationExists = async (
|
||||
projectId: string,
|
||||
@@ -210,11 +211,13 @@ export const getObservationById = async (
|
||||
id: string,
|
||||
projectId: string,
|
||||
fetchWithInputOutput: boolean = false,
|
||||
startTime?: Date,
|
||||
) => {
|
||||
const records = await getObservationByIdInternal(
|
||||
id,
|
||||
projectId,
|
||||
fetchWithInputOutput,
|
||||
startTime,
|
||||
);
|
||||
const mapped = records.map(convertObservation);
|
||||
|
||||
@@ -311,6 +314,7 @@ const getObservationByIdInternal = async (
|
||||
id: string,
|
||||
projectId: string,
|
||||
fetchWithInputOutput: boolean = false,
|
||||
startTime?: Date,
|
||||
) => {
|
||||
const query = `
|
||||
SELECT
|
||||
@@ -345,11 +349,18 @@ const getObservationByIdInternal = async (
|
||||
FROM observations
|
||||
WHERE id = {id: String}
|
||||
AND project_id = {projectId: String}
|
||||
${startTime ? `AND start_time = {startTime: DateTime64(3)}` : ""}
|
||||
ORDER BY event_ts desc
|
||||
LIMIT 1 by id, project_id`;
|
||||
return await queryClickhouse<ObservationRecordReadType>({
|
||||
query,
|
||||
params: { id, projectId },
|
||||
params: {
|
||||
id,
|
||||
projectId,
|
||||
...(startTime
|
||||
? { startTime: convertDateToClickhouseDateTime(startTime) }
|
||||
: {}),
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
@@ -389,7 +400,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 +418,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 +453,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 +510,45 @@ 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.project_id as "project_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 +575,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 +649,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 (
|
||||
@@ -907,6 +901,25 @@ export const deleteObservationsByTraceIds = async (
|
||||
projectId,
|
||||
traceIds,
|
||||
},
|
||||
clickhouseConfigs: {
|
||||
request_timeout: 120_000, // 2 minutes
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
export const deleteObservationsByProjectId = async (projectId: string) => {
|
||||
const query = `
|
||||
DELETE FROM observations
|
||||
WHERE project_id = {projectId: String};
|
||||
`;
|
||||
await commandClickhouse({
|
||||
query: query,
|
||||
params: {
|
||||
projectId,
|
||||
},
|
||||
clickhouseConfigs: {
|
||||
request_timeout: 120_000, // 2 minutes
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
@@ -964,8 +977,8 @@ export const getObservationMetricsForPrompts = async (
|
||||
prompt_version,
|
||||
min(start_time) AS first_observation,
|
||||
max(start_time) AS last_observation,
|
||||
medianExact(usage_details['input']) AS median_input_usage,
|
||||
medianExact(usage_details['output']) AS median_output_usage,
|
||||
medianExact(arraySum(mapValues(mapFilter(x -> positionCaseInsensitive(x.1, 'input') > 0, usage_details)))) AS median_input_usage,
|
||||
medianExact(arraySum(mapValues(mapFilter(x -> positionCaseInsensitive(x.1, 'output') > 0, usage_details)))) AS median_output_usage,
|
||||
medianExact(cost_details['total']) AS median_total_cost,
|
||||
medianExact(latency_ms) AS median_latency_ms
|
||||
FROM latencies
|
||||
@@ -1069,3 +1082,165 @@ 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);
|
||||
};
|
||||
|
||||
export const getTraceIdsForObservations = async (
|
||||
projectId: string,
|
||||
observationIds: string[],
|
||||
) => {
|
||||
const query = `
|
||||
SELECT
|
||||
trace_id,
|
||||
id
|
||||
FROM observations
|
||||
WHERE project_id = {projectId: String}
|
||||
AND id IN ({observationIds: Array(String)})
|
||||
`;
|
||||
|
||||
const rows = await queryClickhouse<{ id: string; trace_id: string }>({
|
||||
query,
|
||||
params: {
|
||||
projectId,
|
||||
observationIds,
|
||||
},
|
||||
});
|
||||
|
||||
return rows.map((row) => ({
|
||||
id: row.id,
|
||||
traceId: row.trace_id,
|
||||
}));
|
||||
};
|
||||
|
||||
export const getGenerationsForPostHog = async (
|
||||
projectId: string,
|
||||
minTimestamp: Date,
|
||||
maxTimestamp: Date,
|
||||
) => {
|
||||
const query = `
|
||||
SELECT
|
||||
o.name as name,
|
||||
o.start_time as start_time,
|
||||
o.id as id,
|
||||
o.total_cost as total_cost,
|
||||
if(isNull(completion_start_time), NULL, date_diff('milliseconds', start_time, completion_start_time)) as time_to_first_token,
|
||||
o.usage_details['total'] as input_tokens,
|
||||
o.usage_details['output'] as output_tokens,
|
||||
o.cost_details['total'] as total_tokens,
|
||||
o.project_id as project_id,
|
||||
if(isNull(end_time), NULL, date_diff('milliseconds', start_time, end_time) / 1000) as latency,
|
||||
o.provided_model_name as model,
|
||||
o.level as level,
|
||||
o.version as version,
|
||||
t.id as trace_id,
|
||||
t.name as trace_name,
|
||||
t.session_id as trace_session_id,
|
||||
t.user_id as trace_user_id,
|
||||
t.release as trace_release,
|
||||
t.tags as trace_tags,
|
||||
t.metadata['$posthog_session_id'] as posthog_session_id
|
||||
FROM observations o FINAL
|
||||
LEFT JOIN traces t FINAL ON o.trace_id = t.id AND o.project_id = t.project_id
|
||||
WHERE o.project_id = {projectId: String}
|
||||
AND t.project_id = {projectId: String}
|
||||
AND o.start_time >= {minTimestamp: DateTime64(3)}
|
||||
AND o.start_time <= {maxTimestamp: DateTime64(3)}
|
||||
AND t.timestamp >= {minTimestamp: DateTime64(3)} - INTERVAL 7 DAY
|
||||
AND t.timestamp <= {maxTimestamp: DateTime64(3)}
|
||||
AND o.type = 'GENERATION'
|
||||
`;
|
||||
|
||||
const records = await queryClickhouse<Record<string, unknown>>({
|
||||
query,
|
||||
params: {
|
||||
projectId,
|
||||
minTimestamp: convertDateToClickhouseDateTime(minTimestamp),
|
||||
maxTimestamp: convertDateToClickhouseDateTime(maxTimestamp),
|
||||
},
|
||||
});
|
||||
|
||||
const baseUrl = env.NEXTAUTH_URL?.replace("/api/auth", "");
|
||||
return records.map((record) => ({
|
||||
timestamp: record.start_time,
|
||||
langfuse_generation_name: record.name,
|
||||
langfuse_trace_name: record.trace_name,
|
||||
langfuse_url: `${baseUrl}/project/${projectId}/traces/${encodeURIComponent(record.trace_id as string)}?observation=${encodeURIComponent(record.id as string)}`,
|
||||
langfuse_id: record.id,
|
||||
langfuse_cost_usd: record.total_cost,
|
||||
langfuse_input_units: record.input_tokens,
|
||||
langfuse_output_units: record.output_tokens,
|
||||
langfuse_total_units: record.total_tokens,
|
||||
langfuse_session_id: record.trace_session_id,
|
||||
langfuse_project_id: projectId,
|
||||
langfuse_user_id: record.trace_user_id || "langfuse_unknown_user",
|
||||
langfuse_latency: record.latency,
|
||||
langfuse_time_to_first_token: record.time_to_first_token,
|
||||
langfuse_release: record.trace_release,
|
||||
langfuse_version: record.version,
|
||||
langfuse_model: record.model,
|
||||
langfuse_level: record.level,
|
||||
langfuse_tags: record.trace_tags,
|
||||
langfuse_event_version: "1.0.0",
|
||||
$session_id: record.posthog_session_id ?? null,
|
||||
$set: {
|
||||
langfuse_user_url: record.user_id
|
||||
? `${baseUrl}/project/${projectId}/users/${encodeURIComponent(record.user_id as string)}`
|
||||
: null,
|
||||
},
|
||||
}));
|
||||
};
|
||||
|
||||
@@ -13,7 +13,10 @@ import { jsonSchema } from "../../utils/zod";
|
||||
|
||||
export const convertObservationToView = (
|
||||
record: ObservationRecordReadType,
|
||||
): Omit<ObservationView, "inputPrice" | "outputPrice" | "totalPrice"> => {
|
||||
): Omit<ObservationView, "inputPrice" | "outputPrice" | "totalPrice"> & {
|
||||
usageDetails: Record<string, number>;
|
||||
costDetails: Record<string, number>;
|
||||
} => {
|
||||
// these cost are not used from the view. They are in the select statement but not in the
|
||||
// Prisma file. We will not clean this up but keep it as it is for now.
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
@@ -25,10 +28,7 @@ export const convertObservationToView = (
|
||||
? parseClickhouseUTCDateTimeFormat(record.end_time).getTime() -
|
||||
parseClickhouseUTCDateTimeFormat(record.start_time).getTime()
|
||||
: null,
|
||||
timeToFirstToken: record.completion_start_time
|
||||
? parseClickhouseUTCDateTimeFormat(record.start_time).getTime() -
|
||||
parseClickhouseUTCDateTimeFormat(record.completion_start_time).getTime()
|
||||
: null,
|
||||
|
||||
promptName: record.prompt_name ?? null,
|
||||
promptVersion: record.prompt_version ?? null,
|
||||
modelId: record.internal_model_id ?? null,
|
||||
@@ -42,7 +42,15 @@ export const convertObservation = (
|
||||
promptVersion: number | null;
|
||||
latency: number | null;
|
||||
timeToFirstToken: number | null;
|
||||
usageDetails: Record<string, number>;
|
||||
costDetails: Record<string, number>;
|
||||
} => {
|
||||
const reducedUsageDetails = reduceUsageOrCostDetails(record.usage_details);
|
||||
const reducedCostDetails = reduceUsageOrCostDetails(record.cost_details);
|
||||
const reducedProvidedCostDetails = reduceUsageOrCostDetails(
|
||||
record.provided_cost_details,
|
||||
);
|
||||
|
||||
return {
|
||||
id: record.id,
|
||||
traceId: record.trace_id ?? null,
|
||||
@@ -73,31 +81,41 @@ export const convertObservation = (
|
||||
promptId: record.prompt_id ?? null,
|
||||
createdAt: parseClickhouseUTCDateTimeFormat(record.created_at),
|
||||
updatedAt: parseClickhouseUTCDateTimeFormat(record.updated_at),
|
||||
promptTokens: record.usage_details?.input
|
||||
? Number(record.usage_details?.input)
|
||||
: 0,
|
||||
completionTokens: record.usage_details?.output
|
||||
? Number(record.usage_details?.output)
|
||||
: 0,
|
||||
totalTokens: record.usage_details?.total
|
||||
? Number(record.usage_details?.total)
|
||||
: 0,
|
||||
calculatedInputCost: record.cost_details?.input
|
||||
? new Decimal(record.cost_details.input)
|
||||
: null,
|
||||
calculatedOutputCost: record.cost_details?.output
|
||||
? new Decimal(record.cost_details.output)
|
||||
: null,
|
||||
promptTokens: reducedUsageDetails.input ?? 0,
|
||||
completionTokens: reducedUsageDetails.output ?? 0,
|
||||
totalTokens: reducedUsageDetails.total ?? 0,
|
||||
calculatedInputCost:
|
||||
reducedCostDetails.input != null
|
||||
? new Decimal(reducedCostDetails.input)
|
||||
: null,
|
||||
calculatedOutputCost:
|
||||
reducedCostDetails.output != null
|
||||
? new Decimal(reducedCostDetails.output)
|
||||
: null,
|
||||
calculatedTotalCost: record.cost_details?.total
|
||||
? new Decimal(record.cost_details.total)
|
||||
: null,
|
||||
inputCost: record.cost_details?.input
|
||||
? new Decimal(record.cost_details?.input)
|
||||
: null,
|
||||
outputCost: record.cost_details?.output
|
||||
? new Decimal(record.cost_details?.output)
|
||||
: null,
|
||||
inputCost:
|
||||
reducedProvidedCostDetails.input != null
|
||||
? new Decimal(reducedProvidedCostDetails.input)
|
||||
: null,
|
||||
outputCost:
|
||||
reducedProvidedCostDetails.output != null
|
||||
? new Decimal(reducedProvidedCostDetails.output)
|
||||
: null,
|
||||
totalCost: record.total_cost ? new Decimal(record.total_cost) : null,
|
||||
usageDetails: Object.fromEntries(
|
||||
Object.entries(record.usage_details ?? {}).map(([key, value]) => [
|
||||
key,
|
||||
Number(value),
|
||||
]),
|
||||
),
|
||||
costDetails: Object.fromEntries(
|
||||
Object.entries(record.cost_details ?? {}).map(([key, value]) => [
|
||||
key,
|
||||
Number(value),
|
||||
]),
|
||||
),
|
||||
model: record.provided_model_name ?? null,
|
||||
internalModelId: record.internal_model_id ?? null,
|
||||
unit: "TOKENS", // to be removed.
|
||||
@@ -108,8 +126,34 @@ export const convertObservation = (
|
||||
parseClickhouseUTCDateTimeFormat(record.start_time).getTime()
|
||||
: null,
|
||||
timeToFirstToken: record.completion_start_time
|
||||
? parseClickhouseUTCDateTimeFormat(record.start_time).getTime() -
|
||||
parseClickhouseUTCDateTimeFormat(record.completion_start_time).getTime()
|
||||
? parseClickhouseUTCDateTimeFormat(
|
||||
record.completion_start_time,
|
||||
).getTime() -
|
||||
parseClickhouseUTCDateTimeFormat(record.start_time).getTime()
|
||||
: null,
|
||||
};
|
||||
};
|
||||
|
||||
export const reduceUsageOrCostDetails = (
|
||||
details: Record<string, number> | null | undefined,
|
||||
): {
|
||||
input: number | null;
|
||||
output: number | null;
|
||||
total: number | null;
|
||||
} => {
|
||||
return {
|
||||
input: Object.entries(details ?? {})
|
||||
.filter(([usageType]) => usageType.startsWith("input"))
|
||||
.reduce(
|
||||
(acc, [_, value]) => (acc ?? 0) + Number(value),
|
||||
null as number | null, // default to null if no input usage is found
|
||||
),
|
||||
output: Object.entries(details ?? {})
|
||||
.filter(([usageType]) => usageType.startsWith("output"))
|
||||
.reduce(
|
||||
(acc, [_, value]) => (acc ?? 0) + Number(value),
|
||||
null as number | null, // default to null if no output usage is found
|
||||
),
|
||||
total: Number(details?.total ?? 0),
|
||||
};
|
||||
};
|
||||
|
||||
@@ -6,7 +6,7 @@ import {
|
||||
upsertClickhouse,
|
||||
} from "./clickhouse";
|
||||
import { FilterList } from "../queries/clickhouse-sql/clickhouse-filter";
|
||||
import { FilterState } from "../../types";
|
||||
import { FilterCondition, FilterState, TimeFilter } from "../../types";
|
||||
import {
|
||||
createFilterFromFilterState,
|
||||
getProjectIdDefaultFilter,
|
||||
@@ -25,6 +25,7 @@ import {
|
||||
import { SCORE_TO_TRACE_OBSERVATIONS_INTERVAL } from "./constants";
|
||||
import { convertDateToClickhouseDateTime } from "../clickhouse/client";
|
||||
import { ScoreRecordReadType } from "./definitions";
|
||||
import { env } from "../../env";
|
||||
|
||||
export const searchExistingAnnotationScore = async (
|
||||
projectId: string,
|
||||
@@ -93,6 +94,32 @@ export const getScoreById = async (
|
||||
return rows.map(convertToScore).shift();
|
||||
};
|
||||
|
||||
export const getScoresByIds = async (
|
||||
projectId: string,
|
||||
scoreId: string[],
|
||||
source?: ScoreSource,
|
||||
) => {
|
||||
const query = `
|
||||
SELECT *
|
||||
FROM scores s
|
||||
WHERE s.project_id = {projectId: String}
|
||||
AND s.id IN ({scoreId: Array(String)})
|
||||
${source ? `AND s.source = {source: String}` : ""}
|
||||
ORDER BY s.event_ts DESC
|
||||
LIMIT 1 BY s.id, s.project_id
|
||||
`;
|
||||
|
||||
const rows = await queryClickhouse<ScoreRecordReadType>({
|
||||
query,
|
||||
params: {
|
||||
projectId,
|
||||
scoreId,
|
||||
...(source !== undefined ? { source } : {}),
|
||||
},
|
||||
});
|
||||
return rows.map(convertToScore);
|
||||
};
|
||||
|
||||
/**
|
||||
* Accepts a score in a Clickhouse-ready format.
|
||||
* id, project_id, name, and timestamp must always be provided.
|
||||
@@ -476,6 +503,25 @@ export const deleteScoresByTraceIds = async (
|
||||
projectId,
|
||||
traceIds,
|
||||
},
|
||||
clickhouseConfigs: {
|
||||
request_timeout: 120_000, // 2 minutes
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
export const deleteScoresByProjectId = async (projectId: string) => {
|
||||
const query = `
|
||||
DELETE FROM scores
|
||||
WHERE project_id = {projectId: String};
|
||||
`;
|
||||
await commandClickhouse({
|
||||
query: query,
|
||||
params: {
|
||||
projectId,
|
||||
},
|
||||
clickhouseConfigs: {
|
||||
request_timeout: 120_000, // 2 minutes
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
@@ -489,10 +535,14 @@ export const getNumericScoreHistogram = async (
|
||||
);
|
||||
const chFilterRes = chFilter.apply();
|
||||
|
||||
const traceFilter = chFilter.find((f) => f.clickhouseTable === "traces");
|
||||
|
||||
const query = `
|
||||
select s.value
|
||||
from scores s
|
||||
${traceFilter ? `LEFT JOIN traces t ON s.trace_id = t.id AND t.project_id = s.project_id` : ""}
|
||||
WHERE s.project_id = {projectId: String}
|
||||
${traceFilter ? `AND t.project_id = {projectId: String}` : ""}
|
||||
${chFilterRes?.query ? `AND ${chFilterRes.query}` : ""}
|
||||
ORDER BY s.event_ts DESC
|
||||
LIMIT 1 BY s.id, s.project_id
|
||||
@@ -549,3 +599,129 @@ 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),
|
||||
}));
|
||||
};
|
||||
|
||||
export const getDistinctScoreNames = async (
|
||||
projectId: string,
|
||||
cutoffCreatedAt: Date,
|
||||
filter: FilterState,
|
||||
isTimestampFilter: (filter: FilterCondition) => filter is TimeFilter,
|
||||
) => {
|
||||
const scoreTimestampFilter = filter?.find(isTimestampFilter);
|
||||
|
||||
const query = `
|
||||
SELECT DISTINCT
|
||||
name
|
||||
FROM scores s
|
||||
WHERE s.project_id = {projectId: String}
|
||||
AND s.created_at <= {cutoffCreatedAt: DateTime64(3)}
|
||||
${scoreTimestampFilter ? `AND s.timestamp >= {filterTimestamp: DateTime64(3)}` : ""}
|
||||
`;
|
||||
|
||||
const rows = await queryClickhouse<{ name: string }>({
|
||||
query,
|
||||
params: {
|
||||
projectId,
|
||||
cutoffCreatedAt: convertDateToClickhouseDateTime(cutoffCreatedAt),
|
||||
...(scoreTimestampFilter
|
||||
? {
|
||||
filterTimestamp: convertDateToClickhouseDateTime(
|
||||
scoreTimestampFilter.value,
|
||||
),
|
||||
}
|
||||
: {}),
|
||||
},
|
||||
});
|
||||
|
||||
return rows.map((row) => row.name);
|
||||
};
|
||||
|
||||
export const getScoresForPostHog = async (
|
||||
projectId: string,
|
||||
minTimestamp: Date,
|
||||
maxTimestamp: Date,
|
||||
) => {
|
||||
const query = `
|
||||
SELECT
|
||||
s.id as id,
|
||||
s.timestamp as timestamp,
|
||||
s.name as name,
|
||||
s.value as value,
|
||||
s.comment as comment,
|
||||
t.name as trace_name,
|
||||
t.session_id as trace_session_id,
|
||||
t.user_id as trace_user_id,
|
||||
t.release as trace_release,
|
||||
t.tags as trace_tags,
|
||||
t.metadata['$posthog_session_id'] as posthog_session_id
|
||||
FROM scores s FINAL
|
||||
LEFT JOIN traces t FINAL ON s.trace_id = t.id AND s.project_id = t.project_id
|
||||
WHERE s.project_id = {projectId: String}
|
||||
AND t.project_id = {projectId: String}
|
||||
AND s.timestamp >= {minTimestamp: DateTime64(3)}
|
||||
AND s.timestamp <= {maxTimestamp: DateTime64(3)}
|
||||
AND t.timestamp >= {minTimestamp: DateTime64(3)} - INTERVAL 7 DAY
|
||||
AND t.timestamp <= {maxTimestamp: DateTime64(3)}
|
||||
`;
|
||||
|
||||
const records = await queryClickhouse<Record<string, unknown>>({
|
||||
query,
|
||||
params: {
|
||||
projectId,
|
||||
minTimestamp: convertDateToClickhouseDateTime(minTimestamp),
|
||||
maxTimestamp: convertDateToClickhouseDateTime(maxTimestamp),
|
||||
},
|
||||
});
|
||||
|
||||
const baseUrl = env.NEXTAUTH_URL?.replace("/api/auth", "");
|
||||
return records.map((record) => ({
|
||||
timestamp: record.timestamp,
|
||||
langfuse_score_name: record.name,
|
||||
langfuse_score_value: record.value,
|
||||
langfuse_score_comment: record.comment,
|
||||
langfuse_trace_name: record.trace_name,
|
||||
langfuse_id: record.id,
|
||||
langfuse_session_id: record.trace_session_id,
|
||||
langfuse_project_id: projectId,
|
||||
langfuse_user_id: record.trace_user_id || "langfuse_unknown_user",
|
||||
langfuse_release: record.trace_release,
|
||||
langfuse_tags: record.trace_tags,
|
||||
langfuse_event_version: "1.0.0",
|
||||
$session_id: record.posthog_session_id ?? null,
|
||||
$set: {
|
||||
langfuse_user_url: record.user_id
|
||||
? `${baseUrl}/project/${projectId}/users/${encodeURIComponent(record.user_id as string)}`
|
||||
: null,
|
||||
},
|
||||
}));
|
||||
};
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { ScoreSource, ScoreDataType } from "@prisma/client";
|
||||
import { ScoreSource, ScoreDataType, Score } from "@prisma/client";
|
||||
import { ScoreRecordReadType } from "./definitions";
|
||||
|
||||
export type ScoreAggregation = {
|
||||
@@ -11,7 +11,7 @@ export type ScoreAggregation = {
|
||||
comment: string | null;
|
||||
};
|
||||
|
||||
export const convertToScore = (row: ScoreRecordReadType) => {
|
||||
export const convertToScore = (row: ScoreRecordReadType): Score => {
|
||||
return {
|
||||
id: row.id,
|
||||
timestamp: new Date(row.timestamp),
|
||||
|
||||
@@ -0,0 +1,61 @@
|
||||
import z from "zod";
|
||||
import { prisma } from "../../db";
|
||||
import { singleFilter, stringOptionsFilter } from "../../interfaces/filters";
|
||||
|
||||
export const getPublicSessionsFilter = async (
|
||||
projectId: string,
|
||||
filter: z.infer<typeof singleFilter>[],
|
||||
) => {
|
||||
const sessionsBookmarkedFilter = filter?.find((f) => f.column === "⭐️");
|
||||
|
||||
// we are only fetching bookmarked sessions.
|
||||
// They need to be manipulated in the UI and should not be as many.
|
||||
const filteredSessions = sessionsBookmarkedFilter
|
||||
? await prisma.traceSession.findMany({
|
||||
where: {
|
||||
projectId: projectId,
|
||||
bookmarked: true,
|
||||
},
|
||||
select: {
|
||||
id: true,
|
||||
createdAt: true,
|
||||
bookmarked: true,
|
||||
public: true,
|
||||
},
|
||||
})
|
||||
: [];
|
||||
|
||||
const additionalBookmarkFilter: z.infer<typeof stringOptionsFilter>[] =
|
||||
sessionsBookmarkedFilter &&
|
||||
typeof sessionsBookmarkedFilter.value === "boolean" &&
|
||||
((sessionsBookmarkedFilter.value === true &&
|
||||
sessionsBookmarkedFilter.operator === "=") ||
|
||||
(sessionsBookmarkedFilter.value === false &&
|
||||
sessionsBookmarkedFilter.operator === "<>"))
|
||||
? [
|
||||
{
|
||||
column: "id",
|
||||
type: "stringOptions" as const,
|
||||
operator: "any of" as const,
|
||||
value: filteredSessions.map((s) => s.id),
|
||||
},
|
||||
]
|
||||
: sessionsBookmarkedFilter &&
|
||||
typeof sessionsBookmarkedFilter.value === "boolean" &&
|
||||
(sessionsBookmarkedFilter.value === false ||
|
||||
(sessionsBookmarkedFilter.value === true &&
|
||||
sessionsBookmarkedFilter.operator === "<>"))
|
||||
? [
|
||||
{
|
||||
column: "id",
|
||||
type: "stringOptions" as const,
|
||||
operator: "none of" as const,
|
||||
value: filteredSessions.map((s) => s.id),
|
||||
},
|
||||
]
|
||||
: [];
|
||||
|
||||
return filter
|
||||
? [...filter.filter((f) => f.column !== "⭐️"), ...additionalBookmarkFilter]
|
||||
: [...additionalBookmarkFilter];
|
||||
};
|
||||
@@ -23,8 +23,11 @@ import { sessionCols } from "../../tableDefinitions/mapSessionTable";
|
||||
import { convertDateToClickhouseDateTime } from "../clickhouse/client";
|
||||
import { convertClickhouseToDomain } from "./traces_converters";
|
||||
import { clickhouseSearchCondition } from "../queries/clickhouse-sql/search";
|
||||
import { TRACE_TO_OBSERVATIONS_INTERVAL } from "./constants";
|
||||
import { FetchTracesTableProps } from "../services/traces-ui-table-service";
|
||||
import {
|
||||
OBSERVATIONS_TO_TRACE_INTERVAL,
|
||||
TRACE_TO_OBSERVATIONS_INTERVAL,
|
||||
} from "./constants";
|
||||
import { env } from "../../env";
|
||||
|
||||
export const checkTraceExists = async (
|
||||
projectId: string,
|
||||
@@ -36,6 +39,11 @@ export const checkTraceExists = async (
|
||||
tracesPrefix: "t",
|
||||
});
|
||||
|
||||
const timeStampFilter = tracesFilter.find(
|
||||
(f) =>
|
||||
f.field === "timestamp" && (f.operator === ">=" || f.operator === ">"),
|
||||
) as DateTimeFilter | undefined;
|
||||
|
||||
tracesFilter.push(
|
||||
...createFilterFromFilterState(filter, tracesTableUiColumnDefinitions),
|
||||
new StringFilter({
|
||||
@@ -46,19 +54,46 @@ export const checkTraceExists = async (
|
||||
}),
|
||||
);
|
||||
|
||||
const observationFilter = tracesFilter.find(
|
||||
(f) => f.clickhouseTable === "observations",
|
||||
);
|
||||
const tracesFilterRes = tracesFilter.apply();
|
||||
const observationFilterRes = observationFilter?.apply();
|
||||
|
||||
const query = `
|
||||
SELECT id, project_id
|
||||
FROM traces t FINAL
|
||||
WITH observations_agg AS (
|
||||
SELECT
|
||||
|
||||
multiIf(
|
||||
arrayExists(x -> x = 'ERROR', groupArray(level)), 'ERROR',
|
||||
arrayExists(x -> x = 'WARNING', groupArray(level)), 'WARNING',
|
||||
arrayExists(x -> x = 'DEFAULT', groupArray(level)), 'DEFAULT',
|
||||
'DEBUG'
|
||||
) AS level,
|
||||
trace_id,
|
||||
project_id
|
||||
FROM observations o FINAL
|
||||
WHERE o.project_id = {projectId: String}
|
||||
${timeStampFilter ? `AND o.start_time >= {traceTimestamp: DateTime64(3)} - ${OBSERVATIONS_TO_TRACE_INTERVAL}` : ""}
|
||||
GROUP BY trace_id, project_id
|
||||
)
|
||||
SELECT
|
||||
t.id as id,
|
||||
t.project_id as project_id
|
||||
FROM traces t FINAL
|
||||
${observationFilterRes ? `INNER JOIN observations_agg o ON t.id = o.trace_id AND t.project_id = o.project_id` : ""}
|
||||
WHERE ${tracesFilterRes.query}
|
||||
AND t.project_id = {projectId: String}
|
||||
${timestamp ? `AND timestamp >= {timestamp: DateTime64(3)} - ${TRACE_TO_OBSERVATIONS_INTERVAL}` : ""}
|
||||
GROUP BY t.id, t.project_id
|
||||
`;
|
||||
|
||||
const rows = await queryClickhouse<{ id: string; project_id: string }>({
|
||||
query,
|
||||
params: {
|
||||
projectId,
|
||||
...tracesFilterRes.params,
|
||||
...(observationFilterRes ? observationFilterRes.params : {}),
|
||||
...(timestamp
|
||||
? { timestamp: convertDateToClickhouseDateTime(timestamp) }
|
||||
: {}),
|
||||
@@ -151,6 +186,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 +461,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 } =
|
||||
@@ -453,11 +529,11 @@ const getSessionsTableGeneric = async <T>(props: FetchTracesTableProps) => {
|
||||
date_diff('milliseconds', min(min_start_time), max(max_end_time)) as duration,
|
||||
sumMap(o.sum_usage_details) as session_usage_details,
|
||||
sumMap(o.sum_cost_details) as session_cost_details,
|
||||
sumMap(o.sum_cost_details)['input'] as session_input_cost,
|
||||
sumMap(o.sum_cost_details)['output'] as session_output_cost,
|
||||
sumMap(o.sum_cost_details)['total'] as session_total_cost,
|
||||
sumMap(o.sum_usage_details)['input'] as session_input_usage,
|
||||
sumMap(o.sum_usage_details)['output'] as session_output_usage,
|
||||
arraySum(mapValues(mapFilter(x -> positionCaseInsensitive(x.1, 'input') > 0, sumMap(o.sum_cost_details)))) as session_input_cost,
|
||||
arraySum(mapValues(mapFilter(x -> positionCaseInsensitive(x.1, 'output') > 0, sumMap(o.sum_cost_details)))) as session_output_cost,
|
||||
sumMap(o.sum_cost_details)['total'] as session_total_cost,
|
||||
arraySum(mapValues(mapFilter(x -> positionCaseInsensitive(x.1, 'input') > 0, sumMap(o.sum_usage_details)))) as session_input_usage,
|
||||
arraySum(mapValues(mapFilter(x -> positionCaseInsensitive(x.1, 'output') > 0, sumMap(o.sum_usage_details)))) as session_output_usage,
|
||||
sumMap(o.sum_usage_details)['total'] as session_total_usage
|
||||
FROM traces t FINAL
|
||||
LEFT JOIN observations_agg o
|
||||
@@ -548,6 +624,25 @@ export const deleteTraces = async (projectId: string, traceIds: string[]) => {
|
||||
projectId,
|
||||
traceIds,
|
||||
},
|
||||
clickhouseConfigs: {
|
||||
request_timeout: 120_000, // 2 minutes
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
export const deleteTracesByProjectId = async (projectId: string) => {
|
||||
const query = `
|
||||
DELETE FROM traces
|
||||
WHERE project_id = {projectId: String};
|
||||
`;
|
||||
await commandClickhouse({
|
||||
query: query,
|
||||
params: {
|
||||
projectId,
|
||||
},
|
||||
clickhouseConfigs: {
|
||||
request_timeout: 120_000, // 2 minutes
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
@@ -589,59 +684,95 @@ export const getUserMetrics = async (projectId: string, userIds: string[]) => {
|
||||
if (userIds.length === 0) {
|
||||
return [];
|
||||
}
|
||||
// this query uses window functions on observations + traces to always get only the first row and thereby remove deduplicates
|
||||
// we filter wherever possible by project id and user id
|
||||
const query = `
|
||||
WITH observations_agg AS (
|
||||
SELECT o.trace_id,
|
||||
count(*) as obs_count,
|
||||
sumMap(usage_details) as sum_usage_details,
|
||||
sum(total_cost) as sum_total_cost,
|
||||
anyLast(project_id) as project_id
|
||||
FROM observations o FINAL
|
||||
WHERE o.project_id = {projectId: String}
|
||||
GROUP BY o.trace_id
|
||||
),
|
||||
user_metric_data AS (
|
||||
SELECT t.user_id,
|
||||
max(t.timestamp) as max_timestamp,
|
||||
min(t.timestamp) as min_timestamp,
|
||||
count(*) as trace_count,
|
||||
sum(o.obs_count) as total_observations,
|
||||
sum(o.sum_total_cost) as session_total_cost,
|
||||
sumMap(o.sum_usage_details)['input'] as session_input_usage,
|
||||
sumMap(o.sum_usage_details)['output'] as session_output_usage,
|
||||
sumMap(o.sum_usage_details)['total'] as session_total_usage
|
||||
FROM traces t FINAL
|
||||
LEFT JOIN observations_agg o
|
||||
ON t.id = o.trace_id
|
||||
AND t.project_id = o.project_id
|
||||
WHERE t.user_id IS NOT NULL
|
||||
AND t.user_id != ''
|
||||
AND t.user_id IN ({userIds: Array(String)})
|
||||
AND t.project_id = {projectId: String}
|
||||
GROUP BY t.user_id
|
||||
WITH stats as (
|
||||
SELECT
|
||||
t.user_id as user_id,
|
||||
count(distinct o.id) as obs_count,
|
||||
sumMap(usage_details) as sum_usage_details,
|
||||
sum(total_cost) as sum_total_cost,
|
||||
max(t.timestamp) as max_timestamp,
|
||||
min(t.timestamp) as min_timestamp,
|
||||
count(distinct t.id) as trace_count
|
||||
FROM
|
||||
(
|
||||
SELECT
|
||||
o.project_id,
|
||||
o.trace_id,
|
||||
o.usage_details,
|
||||
o.total_cost,
|
||||
id,
|
||||
ROW_NUMBER() OVER (
|
||||
PARTITION BY id
|
||||
ORDER BY
|
||||
event_ts DESC
|
||||
) AS rn
|
||||
FROM
|
||||
observations o
|
||||
WHERE
|
||||
o.project_id = {projectId: String }
|
||||
AND o.trace_id in (
|
||||
SELECT
|
||||
distinct id
|
||||
from
|
||||
traces
|
||||
where
|
||||
user_id IN ({userIds: Array(String) })
|
||||
AND project_id = {projectId: String }
|
||||
)
|
||||
AND o.type = 'GENERATION'
|
||||
) as o
|
||||
JOIN (
|
||||
SELECT
|
||||
t.id,
|
||||
t.user_id,
|
||||
t.project_id,
|
||||
t.timestamp,
|
||||
ROW_NUMBER() OVER (
|
||||
PARTITION BY id
|
||||
ORDER BY
|
||||
event_ts DESC
|
||||
) AS rn
|
||||
FROM
|
||||
traces t
|
||||
WHERE
|
||||
t.user_id IN ({userIds: Array(String) })
|
||||
AND t.project_id = {projectId: String }
|
||||
) as t on t.id = o.trace_id
|
||||
and t.project_id = o.project_id
|
||||
WHERE
|
||||
o.rn = 1
|
||||
and t.rn = 1
|
||||
group by
|
||||
t.user_id
|
||||
)
|
||||
SELECT user_id AS userId,
|
||||
min_timestamp as firstTrace,
|
||||
max_timestamp as lastTrace,
|
||||
trace_count as totalTraces,
|
||||
total_observations as totalObservations,
|
||||
session_input_usage as totalPromptTokens,
|
||||
session_output_usage as totalCompletionTokens,
|
||||
session_total_usage as totalTokens,
|
||||
session_total_cost as sumCalculatedTotalCost
|
||||
FROM user_metric_data umd
|
||||
SELECT
|
||||
arraySum(mapValues(mapFilter(x -> positionCaseInsensitive(x.1, 'input') > 0, sum_usage_details))) as input_usage,
|
||||
arraySum(mapValues(mapFilter(x -> positionCaseInsensitive(x.1, 'output') > 0, sum_usage_details))) as output_usage,
|
||||
sum_usage_details [ 'total' ] as total_usage,
|
||||
obs_count,
|
||||
trace_count,
|
||||
user_id,
|
||||
sum_total_cost,
|
||||
max_timestamp,
|
||||
min_timestamp
|
||||
FROM
|
||||
stats
|
||||
|
||||
`;
|
||||
|
||||
return queryClickhouse<{
|
||||
userId: string;
|
||||
firstTrace: Date | null;
|
||||
lastTrace: Date | null;
|
||||
totalPromptTokens: bigint;
|
||||
totalCompletionTokens: bigint;
|
||||
totalTokens: bigint;
|
||||
totalObservations: bigint;
|
||||
totalTraces: bigint;
|
||||
sumCalculatedTotalCost: number;
|
||||
const rows = await queryClickhouse<{
|
||||
user_id: string;
|
||||
max_timestamp: string;
|
||||
min_timestamp: string;
|
||||
input_usage: string;
|
||||
output_usage: string;
|
||||
total_usage: string;
|
||||
obs_count: string;
|
||||
trace_count: string;
|
||||
sum_total_cost: string;
|
||||
}>({
|
||||
query,
|
||||
params: {
|
||||
@@ -649,4 +780,110 @@ export const getUserMetrics = async (projectId: string, userIds: string[]) => {
|
||||
userIds,
|
||||
},
|
||||
});
|
||||
return rows.map((row) => ({
|
||||
userId: row.user_id,
|
||||
maxTimestamp: parseClickhouseUTCDateTimeFormat(row.max_timestamp),
|
||||
minTimestamp: parseClickhouseUTCDateTimeFormat(row.min_timestamp),
|
||||
inputUsage: Number(row.input_usage),
|
||||
outputUsage: Number(row.output_usage),
|
||||
totalUsage: Number(row.total_usage),
|
||||
observationCount: Number(row.obs_count),
|
||||
traceCount: Number(row.trace_count),
|
||||
totalCost: Number(row.sum_total_cost),
|
||||
}));
|
||||
};
|
||||
|
||||
export const getTracesForPostHog = async (
|
||||
projectId: string,
|
||||
minTimestamp: Date,
|
||||
maxTimestamp: Date,
|
||||
) => {
|
||||
const query = `
|
||||
WITH observations_agg AS (
|
||||
SELECT o.project_id,
|
||||
o.trace_id,
|
||||
sum(total_cost) as total_cost,
|
||||
count(*) as observation_count,
|
||||
date_diff('milliseconds', least(min(start_time), min(end_time)), greatest(max(start_time), max(end_time))) as latency_milliseconds
|
||||
FROM observations o FINAL
|
||||
WHERE o.project_id = {projectId: String}
|
||||
AND o.start_time >= {minTimestamp: DateTime64(3)} - ${TRACE_TO_OBSERVATIONS_INTERVAL}
|
||||
GROUP BY o.project_id, o.trace_id
|
||||
)
|
||||
|
||||
SELECT
|
||||
t.id as id,
|
||||
t.timestamp as timestamp,
|
||||
t.name as name,
|
||||
t.session_id as session_id,
|
||||
t.user_id as user_id,
|
||||
t.release as release,
|
||||
t.version as version,
|
||||
t.tags as tags,
|
||||
t.metadata['$posthog_session_id'] as posthog_session_id,
|
||||
o.total_cost as total_cost,
|
||||
o.latency_milliseconds / 1000 as latency,
|
||||
o.observation_count as observation_count
|
||||
FROM traces t FINAL
|
||||
LEFT JOIN observations_agg o ON t.id = o.trace_id AND t.project_id = o.project_id
|
||||
WHERE t.project_id = {projectId: String}
|
||||
AND t.timestamp >= {minTimestamp: DateTime64(3)}
|
||||
AND t.timestamp <= {maxTimestamp: DateTime64(3)}
|
||||
`;
|
||||
|
||||
const records = await queryClickhouse<Record<string, unknown>>({
|
||||
query,
|
||||
params: {
|
||||
projectId,
|
||||
minTimestamp: convertDateToClickhouseDateTime(minTimestamp),
|
||||
maxTimestamp: convertDateToClickhouseDateTime(maxTimestamp),
|
||||
},
|
||||
});
|
||||
|
||||
const baseUrl = env.NEXTAUTH_URL?.replace("/api/auth", "");
|
||||
return records.map((record) => ({
|
||||
timestamp: record.timestamp,
|
||||
langfuse_id: record.id,
|
||||
langfuse_trace_name: record.name,
|
||||
langfuse_url: `${baseUrl}/project/${projectId}/traces/${encodeURIComponent(record.id as string)}`,
|
||||
langfuse_cost_usd: record.total_cost,
|
||||
langfuse_count_observations: record.observation_count,
|
||||
langfuse_session_id: record.session_id,
|
||||
langfuse_project_id: projectId,
|
||||
langfuse_user_id: record.user_id || "langfuse_unknown_user",
|
||||
langfuse_latency: record.latency,
|
||||
langfuse_release: record.release,
|
||||
langfuse_version: record.version,
|
||||
langfuse_tags: record.tags,
|
||||
langfuse_event_version: "1.0.0",
|
||||
$session_id: record.posthog_session_id ?? null,
|
||||
$set: {
|
||||
langfuse_user_url: record.user_id
|
||||
? `${baseUrl}/project/${projectId}/users/${encodeURIComponent(record.user_id as string)}`
|
||||
: null,
|
||||
},
|
||||
}));
|
||||
};
|
||||
|
||||
export const getTracesByIdsForAnyProject = async (traceIds: string[]) => {
|
||||
const query = `
|
||||
SELECT id, project_id
|
||||
FROM traces
|
||||
WHERE id IN ({traceIds: Array(String)})
|
||||
ORDER BY event_ts DESC
|
||||
LIMIT 1 by id, project_id;`;
|
||||
const records = await queryClickhouse<{
|
||||
id: string;
|
||||
project_id: string;
|
||||
}>({
|
||||
query,
|
||||
params: {
|
||||
traceIds,
|
||||
},
|
||||
});
|
||||
|
||||
return records.map((record) => ({
|
||||
id: record.id,
|
||||
projectId: record.project_id,
|
||||
}));
|
||||
};
|
||||
|
||||
@@ -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,18 @@ 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";
|
||||
import { reduceUsageOrCostDetails } from "../repositories";
|
||||
|
||||
export type TracesTableReturnType = Pick<
|
||||
TraceRecordReadType,
|
||||
@@ -30,7 +39,99 @@ 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;
|
||||
usageDetails: Record<string, number>;
|
||||
costDetails: Record<string, number>;
|
||||
};
|
||||
|
||||
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"> => {
|
||||
const usageDetails = reduceUsageOrCostDetails(row.usage_details);
|
||||
|
||||
return {
|
||||
id: row.id,
|
||||
projectId: row.project_id,
|
||||
latency: Number(row.latency),
|
||||
promptTokens: BigInt(usageDetails.input ?? 0),
|
||||
completionTokens: BigInt(usageDetails.output ?? 0),
|
||||
totalTokens: BigInt(usageDetails.total ?? 0),
|
||||
usageDetails: Object.fromEntries(
|
||||
Object.entries(row.usage_details).map(([key, value]) => [
|
||||
key,
|
||||
Number(value),
|
||||
]),
|
||||
),
|
||||
costDetails: Object.fromEntries(
|
||||
Object.entries(row.cost_details).map(([key, value]) => [
|
||||
key,
|
||||
Number(value),
|
||||
]),
|
||||
),
|
||||
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 +141,7 @@ export type TracesTableReturnType = Pick<
|
||||
};
|
||||
|
||||
export type FetchTracesTableProps = {
|
||||
select: string;
|
||||
select: "count" | "rows" | "metrics";
|
||||
projectId: string;
|
||||
filter: FilterState;
|
||||
searchQuery?: string;
|
||||
@@ -58,7 +159,7 @@ export const getTracesTableCount = async (props: {
|
||||
page?: number;
|
||||
}) => {
|
||||
const countRows = await getTracesTableGeneric<{ count: string }>({
|
||||
select: "count(*) as count",
|
||||
select: "count",
|
||||
...props,
|
||||
});
|
||||
|
||||
@@ -69,6 +170,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 +196,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 +205,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 +294,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 +315,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 +361,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 +377,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 +407,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,
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
import { clickhouseClient } from "../clickhouse/client";
|
||||
import {
|
||||
TraceRecordInsertType,
|
||||
ObservationRecordInsertType,
|
||||
ScoreRecordInsertType,
|
||||
} from "../repositories/definitions";
|
||||
|
||||
export const createTracesCh = async (trace: TraceRecordInsertType[]) => {
|
||||
return await clickhouseClient().insert({
|
||||
table: "traces",
|
||||
format: "JSONEachRow",
|
||||
values: trace,
|
||||
});
|
||||
};
|
||||
|
||||
export const createObservationsCh = async (
|
||||
observations: ObservationRecordInsertType[],
|
||||
) => {
|
||||
return await clickhouseClient().insert({
|
||||
table: "observations",
|
||||
format: "JSONEachRow",
|
||||
values: observations,
|
||||
});
|
||||
};
|
||||
|
||||
export const createScoresCh = async (scores: ScoreRecordInsertType[]) => {
|
||||
return await clickhouseClient().insert({
|
||||
table: "scores",
|
||||
format: "JSONEachRow",
|
||||
values: scores,
|
||||
});
|
||||
};
|
||||
@@ -0,0 +1,3 @@
|
||||
export * from "./tracing-factory";
|
||||
export * from "./clickhouse-helpers";
|
||||
export * from "./org-factory";
|
||||
@@ -0,0 +1,50 @@
|
||||
import { v4 } from "uuid";
|
||||
import { prisma } from "../../db";
|
||||
import { hashSecretKey, getDisplaySecretKey } from "../auth/apiKeys";
|
||||
|
||||
export function createBasicAuthHeader(
|
||||
username: string,
|
||||
password: string,
|
||||
): string {
|
||||
const base64Credentials = Buffer.from(`${username}:${password}`).toString(
|
||||
"base64",
|
||||
);
|
||||
return `Basic ${base64Credentials}`;
|
||||
}
|
||||
|
||||
export type CreateOrgProjectAndApiKeyOptions = {
|
||||
projectId?: string;
|
||||
};
|
||||
export const createOrgProjectAndApiKey = async (
|
||||
props?: CreateOrgProjectAndApiKeyOptions,
|
||||
) => {
|
||||
const projectId = props?.projectId ?? v4();
|
||||
const org = await prisma.organization.create({
|
||||
data: {
|
||||
id: v4(),
|
||||
name: v4(),
|
||||
},
|
||||
});
|
||||
await prisma.project.create({
|
||||
data: {
|
||||
id: projectId,
|
||||
name: v4(),
|
||||
orgId: org.id,
|
||||
},
|
||||
});
|
||||
const publicKey = v4();
|
||||
const secretKey = v4();
|
||||
|
||||
const auth = createBasicAuthHeader(publicKey, secretKey);
|
||||
await prisma.apiKey.create({
|
||||
data: {
|
||||
id: v4(),
|
||||
projectId: projectId,
|
||||
publicKey: publicKey,
|
||||
hashedSecretKey: await hashSecretKey(secretKey),
|
||||
displaySecretKey: getDisplaySecretKey(secretKey),
|
||||
},
|
||||
});
|
||||
|
||||
return { projectId, publicKey, secretKey, auth };
|
||||
};
|
||||
+5
-5
@@ -1,9 +1,9 @@
|
||||
import {
|
||||
type ScoreRecordInsertType,
|
||||
type ObservationRecordInsertType,
|
||||
type TraceRecordInsertType,
|
||||
} from "@langfuse/shared/src/server";
|
||||
import { v4 } from "uuid";
|
||||
import {
|
||||
TraceRecordInsertType,
|
||||
ObservationRecordInsertType,
|
||||
ScoreRecordInsertType,
|
||||
} from "../repositories/definitions";
|
||||
|
||||
export const createTrace = (trace: Partial<TraceRecordInsertType>) => {
|
||||
return {
|
||||
@@ -12,7 +12,7 @@ export function transformStreamToCsv(): Transform {
|
||||
transform(
|
||||
row: Record<string, any>,
|
||||
encoding: BufferEncoding,
|
||||
callback: TransformCallback
|
||||
callback: TransformCallback,
|
||||
): void {
|
||||
if (isFirstChunk) {
|
||||
// Extract headers from the first object
|
||||
@@ -23,7 +23,7 @@ export function transformStreamToCsv(): Transform {
|
||||
|
||||
// Convert the object to a CSV line and push it
|
||||
const csvRow = headers.map((header) => {
|
||||
const field = row[header];
|
||||
const field = row[header] ?? "";
|
||||
let str = stringify(field);
|
||||
|
||||
// escape and format fields that contain commas
|
||||
|
||||
@@ -85,4 +85,10 @@ export const dashboardColumnDefinitions: UiColumnMapping[] = [
|
||||
uiTableId: "version",
|
||||
uiTableName: "Version",
|
||||
},
|
||||
{
|
||||
clickhouseTableName: "observations",
|
||||
clickhouseSelect: "provided_model_name",
|
||||
uiTableId: "model",
|
||||
uiTableName: "Model",
|
||||
},
|
||||
];
|
||||
|
||||
@@ -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) / 1000)",
|
||||
// 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) / 1000)",
|
||||
// If we use the default of Decimal64(12), we cannot filter for more than ~40min due to an overflow
|
||||
clickhouseTypeOverwrite: "Decimal64(3)",
|
||||
},
|
||||
@@ -86,21 +86,21 @@ export const observationsTableUiColumnDefinitions: UiColumnMapping[] = [
|
||||
uiTableId: "tokensPerSecond",
|
||||
clickhouseTableName: "observations",
|
||||
clickhouseSelect:
|
||||
"usage_details['input'] / date_diff('seconds', start_time, end_time)",
|
||||
"(arraySum(mapValues(mapFilter(x -> positionCaseInsensitive(x.1, 'output') > 0, usage_details))) / (date_diff('milliseconds', start_time, end_time) / 1000))",
|
||||
},
|
||||
{
|
||||
uiTableName: "Input Cost ($)",
|
||||
uiTableId: "inputCost",
|
||||
clickhouseTableName: "observations",
|
||||
clickhouseSelect:
|
||||
"if(mapExists((k, v) -> (k = 'input'), cost_details), cost_details['input'], NULL)",
|
||||
"arraySum(mapValues(mapFilter(x -> positionCaseInsensitive(x.1, 'input') > 0, cost_details)))",
|
||||
},
|
||||
{
|
||||
uiTableName: "Output Cost ($)",
|
||||
uiTableId: "outputCost",
|
||||
clickhouseTableName: "observations",
|
||||
clickhouseSelect:
|
||||
"if(mapExists((k, v) -> (k = 'output'), cost_details), cost_details['output'], NULL)",
|
||||
"arraySum(mapValues(mapFilter(x -> positionCaseInsensitive(x.1, 'output') > 0, cost_details)))",
|
||||
},
|
||||
{
|
||||
uiTableName: "Total Cost ($)",
|
||||
@@ -132,14 +132,14 @@ export const observationsTableUiColumnDefinitions: UiColumnMapping[] = [
|
||||
uiTableId: "inputTokens",
|
||||
clickhouseTableName: "observations",
|
||||
clickhouseSelect:
|
||||
"if(mapExists((k, v) -> (k = 'input'), usage_details), usage_details['input'], NULL)",
|
||||
"arraySum(mapValues(mapFilter(x -> positionCaseInsensitive(x.1, 'input') > 0, usage_details)))",
|
||||
},
|
||||
{
|
||||
uiTableName: "Output Tokens",
|
||||
uiTableId: "outputTokens",
|
||||
clickhouseTableName: "observations",
|
||||
clickhouseSelect:
|
||||
"if(mapExists((k, v) -> (k = 'output'), usage_details), usage_details['output'], NULL)",
|
||||
"arraySum(mapValues(mapFilter(x -> positionCaseInsensitive(x.1, 'output') > 0, usage_details)))",
|
||||
},
|
||||
{
|
||||
uiTableName: "Total Tokens",
|
||||
|
||||
@@ -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)",
|
||||
"arraySum(mapValues(mapFilter(x -> positionCaseInsensitive(x.1, 'input') > 0, usage_details)))",
|
||||
},
|
||||
{
|
||||
uiTableName: "Output Tokens",
|
||||
uiTableId: "outputTokens",
|
||||
clickhouseTableName: "traces",
|
||||
clickhouseTableName: "observations",
|
||||
clickhouseSelect:
|
||||
"if(mapExists((k, v) -> (k = 'output'), usage_details), usage_details['output'], NULL)",
|
||||
"arraySum(mapValues(mapFilter(x -> positionCaseInsensitive(x.1, 'output') > 0, usage_details)))",
|
||||
},
|
||||
{
|
||||
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,21 @@ export const tracesTableUiColumnDefinitions: UiColumnMapping[] = [
|
||||
{
|
||||
uiTableName: "Input Cost ($)",
|
||||
uiTableId: "inputCost",
|
||||
clickhouseTableName: "traces",
|
||||
clickhouseSelect: "cost_details['input']",
|
||||
clickhouseTableName: "observations",
|
||||
clickhouseSelect:
|
||||
"arraySum(mapValues(mapFilter(x -> positionCaseInsensitive(x.1, 'input') > 0, cost_details)))",
|
||||
},
|
||||
{
|
||||
uiTableName: "Output Cost ($)",
|
||||
uiTableId: "outputCost",
|
||||
clickhouseTableName: "traces",
|
||||
clickhouseSelect: "cost_details['output']",
|
||||
clickhouseTableName: "observations",
|
||||
clickhouseSelect:
|
||||
"arraySum(mapValues(mapFilter(x -> positionCaseInsensitive(x.1, 'output') > 0, cost_details)))",
|
||||
},
|
||||
{
|
||||
uiTableName: "Total Cost ($)",
|
||||
uiTableId: "totalCost",
|
||||
clickhouseTableName: "traces",
|
||||
clickhouseTableName: "observations",
|
||||
clickhouseSelect: "cost_details['total']",
|
||||
},
|
||||
];
|
||||
|
||||
@@ -21,3 +21,16 @@ export function extractVariables(mustacheString: string): string[] {
|
||||
|
||||
return Array.from(uniqueVariables);
|
||||
}
|
||||
|
||||
export function stringifyValue(value: unknown) {
|
||||
switch (typeof value) {
|
||||
case "string":
|
||||
return value;
|
||||
case "number":
|
||||
return value.toString();
|
||||
case "boolean":
|
||||
return value.toString();
|
||||
default:
|
||||
return JSON.stringify(value);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,28 @@ 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),
|
||||
),
|
||||
};
|
||||
|
||||
export const publicApiPaginationZod = {
|
||||
page: z.preprocess(
|
||||
(x) => (x === "" ? undefined : x),
|
||||
z.coerce.number().gt(0).default(1),
|
||||
),
|
||||
limit: z.preprocess(
|
||||
(x) => (x === "" ? undefined : x),
|
||||
z.coerce.number().lte(100).default(50),
|
||||
),
|
||||
};
|
||||
|
||||
@@ -69,8 +80,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 +97,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
+230
-59
@@ -48,8 +48,8 @@ importers:
|
||||
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)
|
||||
next-auth:
|
||||
specifier: ^4.24.7
|
||||
version: 4.24.7(next@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))(nodemailer@6.9.15)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
|
||||
specifier: ^4.24.11
|
||||
version: 4.24.11(next@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))(nodemailer@6.9.15)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
|
||||
zod:
|
||||
specifier: ^3.23.8
|
||||
version: 3.23.8
|
||||
@@ -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,16 +202,16 @@ 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
|
||||
next-auth:
|
||||
specifier: ^4.24.7
|
||||
version: 4.24.7(next@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))(nodemailer@6.9.15)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
|
||||
specifier: ^4.24.11
|
||||
version: 4.24.11(next@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))(nodemailer@6.9.15)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
|
||||
nodemailer:
|
||||
specifier: ^6.9.15
|
||||
version: 6.9.15
|
||||
@@ -365,7 +368,7 @@ importers:
|
||||
version: 7.19.0(@emotion/react@11.11.4(@types/react@18.2.79)(react@18.2.0))(@emotion/styled@11.11.5(@emotion/react@11.11.4(@types/react@18.2.79)(react@18.2.0))(@types/react@18.2.79)(react@18.2.0))(@mui/material@5.15.19(@emotion/react@11.11.4(@types/react@18.2.79)(react@18.2.0))(@emotion/styled@11.11.5(@emotion/react@11.11.4(@types/react@18.2.79)(react@18.2.0))(@types/react@18.2.79)(react@18.2.0))(@types/react@18.2.79)(react-dom@18.2.0(react@18.2.0))(react@18.2.0))(@mui/system@5.16.7(@emotion/react@11.11.4(@types/react@18.2.79)(react@18.2.0))(@emotion/styled@11.11.5(@emotion/react@11.11.4(@types/react@18.2.79)(react@18.2.0))(@types/react@18.2.79)(react@18.2.0))(@types/react@18.2.79)(react@18.2.0))(@types/react@18.2.79)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
|
||||
'@next-auth/prisma-adapter':
|
||||
specifier: ^1.0.7
|
||||
version: 1.0.7(@prisma/client@5.22.0(prisma@5.22.0))(next-auth@4.24.7(next@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))(nodemailer@6.9.15)(react-dom@18.2.0(react@18.2.0))(react@18.2.0))
|
||||
version: 1.0.7(@prisma/client@5.22.0(prisma@5.22.0))(next-auth@4.24.11(next@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))(nodemailer@6.9.15)(react-dom@18.2.0(react@18.2.0))(react@18.2.0))
|
||||
'@opentelemetry/api':
|
||||
specifier: ^1.9.0
|
||||
version: 1.9.0
|
||||
@@ -578,19 +581,19 @@ 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)
|
||||
next-auth:
|
||||
specifier: ^4.24.7
|
||||
version: 4.24.7(next@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))(nodemailer@6.9.15)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
|
||||
specifier: ^4.24.11
|
||||
version: 4.24.11(next@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))(nodemailer@6.9.15)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
|
||||
next-query-params:
|
||||
specifier: ^5.0.1
|
||||
version: 5.0.1(next@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))(react@18.2.0)(use-query-params@2.2.1(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
|
||||
@@ -884,9 +887,12 @@ importers:
|
||||
pg:
|
||||
specifier: ^8.13.0
|
||||
version: 8.13.0
|
||||
posthog-node:
|
||||
specifier: ^4.3.1
|
||||
version: 4.3.1
|
||||
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
|
||||
@@ -966,6 +972,9 @@ importers:
|
||||
vitest:
|
||||
specifier: ^2.1.2
|
||||
version: 2.1.2(@types/node@20.11.29)(jsdom@20.0.3)(msw@2.6.5(@types/node@20.11.29)(typescript@5.4.5))(terser@5.36.0)
|
||||
wait-for-expect:
|
||||
specifier: ^3.0.2
|
||||
version: 3.0.2
|
||||
|
||||
packages:
|
||||
|
||||
@@ -2391,6 +2400,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 +5545,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 +5650,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 +5702,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==}
|
||||
|
||||
@@ -6016,10 +6053,6 @@ packages:
|
||||
cookie-signature@1.0.6:
|
||||
resolution: {integrity: sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==}
|
||||
|
||||
cookie@0.5.0:
|
||||
resolution: {integrity: sha512-YZ3GUyn/o8gfKJlnlX7g7xq4gyO6OSuhGPKaaGssGB2qgDUS0gPgtTvoyZLTt9Ab6dC4hfc9dV5arkvc/OCmrw==}
|
||||
engines: {node: '>= 0.6'}
|
||||
|
||||
cookie@0.6.0:
|
||||
resolution: {integrity: sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw==}
|
||||
engines: {node: '>= 0.6'}
|
||||
@@ -6616,6 +6649,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 +7134,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 +7317,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 +7458,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 +7492,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 +8267,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 +8315,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 +8619,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
|
||||
|
||||
@@ -8941,14 +9011,17 @@ packages:
|
||||
resolution: {integrity: sha512-NHDDGYudnvRutt/VhKFlX26IotXe1w0cmkDm6JGquh5bz/bDTw0LufSmH/GxTjEdpHEO+bVKFTwdrcGa/9XlKQ==}
|
||||
engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0}
|
||||
|
||||
next-auth@4.24.7:
|
||||
resolution: {integrity: sha512-iChjE8ov/1K/z98gdKbn2Jw+2vLgJtVV39X+rCP5SGnVQuco7QOr19FRNGMIrD8d3LYhHWV9j9sKLzq1aDWWQQ==}
|
||||
next-auth@4.24.11:
|
||||
resolution: {integrity: sha512-pCFXzIDQX7xmHFs4KVH4luCjaCbuPRtZ9oBUjUhOk84mZ9WVPf94n87TxYI4rSRf9HmfHEF8Yep3JrYDVOo3Cw==}
|
||||
peerDependencies:
|
||||
next: ^12.2.5 || ^13 || ^14
|
||||
'@auth/core': 0.34.2
|
||||
next: ^12.2.5 || ^13 || ^14 || ^15
|
||||
nodemailer: ^6.6.5
|
||||
react: ^17.0.2 || ^18
|
||||
react-dom: ^17.0.2 || ^18
|
||||
react: ^17.0.2 || ^18 || ^19
|
||||
react-dom: ^17.0.2 || ^18 || ^19
|
||||
peerDependenciesMeta:
|
||||
'@auth/core':
|
||||
optional: true
|
||||
nodemailer:
|
||||
optional: true
|
||||
|
||||
@@ -9027,6 +9100,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 +9661,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 +9882,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 +10637,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 +14107,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))
|
||||
@@ -14270,10 +14370,10 @@ snapshots:
|
||||
transitivePeerDependencies:
|
||||
- '@types/react'
|
||||
|
||||
'@next-auth/prisma-adapter@1.0.7(@prisma/client@5.22.0(prisma@5.22.0))(next-auth@4.24.7(next@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))(nodemailer@6.9.15)(react-dom@18.2.0(react@18.2.0))(react@18.2.0))':
|
||||
'@next-auth/prisma-adapter@1.0.7(@prisma/client@5.22.0(prisma@5.22.0))(next-auth@4.24.11(next@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))(nodemailer@6.9.15)(react-dom@18.2.0(react@18.2.0))(react@18.2.0))':
|
||||
dependencies:
|
||||
'@prisma/client': 5.22.0(prisma@5.22.0)
|
||||
next-auth: 4.24.7(next@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))(nodemailer@6.9.15)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
|
||||
next-auth: 4.24.11(next@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))(nodemailer@6.9.15)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
|
||||
|
||||
'@next/env@14.2.15': {}
|
||||
|
||||
@@ -15736,7 +15836,7 @@ snapshots:
|
||||
|
||||
'@radix-ui/react-use-callback-ref@1.0.1(@types/react@18.2.79)(react@18.2.0)':
|
||||
dependencies:
|
||||
'@babel/runtime': 7.25.7
|
||||
'@babel/runtime': 7.26.0
|
||||
react: 18.2.0
|
||||
optionalDependencies:
|
||||
'@types/react': 18.2.79
|
||||
@@ -15764,7 +15864,7 @@ snapshots:
|
||||
|
||||
'@radix-ui/react-use-escape-keydown@1.0.3(@types/react@18.2.79)(react@18.2.0)':
|
||||
dependencies:
|
||||
'@babel/runtime': 7.25.7
|
||||
'@babel/runtime': 7.26.0
|
||||
'@radix-ui/react-use-callback-ref': 1.0.1(@types/react@18.2.79)(react@18.2.0)
|
||||
react: 18.2.0
|
||||
optionalDependencies:
|
||||
@@ -15779,7 +15879,7 @@ snapshots:
|
||||
|
||||
'@radix-ui/react-use-layout-effect@1.0.1(@types/react@18.2.79)(react@18.2.0)':
|
||||
dependencies:
|
||||
'@babel/runtime': 7.25.7
|
||||
'@babel/runtime': 7.26.0
|
||||
react: 18.2.0
|
||||
optionalDependencies:
|
||||
'@types/react': 18.2.79
|
||||
@@ -17865,6 +17965,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 +18090,8 @@ snapshots:
|
||||
|
||||
before-after-hook@2.2.3: {}
|
||||
|
||||
bignumber.js@9.1.2: {}
|
||||
|
||||
binary-extensions@2.3.0: {}
|
||||
|
||||
bl@4.1.0:
|
||||
@@ -18066,6 +18170,8 @@ snapshots:
|
||||
|
||||
buffer-crc32@0.2.13: {}
|
||||
|
||||
buffer-equal-constant-time@1.0.1: {}
|
||||
|
||||
buffer-from@1.1.2: {}
|
||||
|
||||
buffer@5.6.0:
|
||||
@@ -18422,8 +18528,6 @@ snapshots:
|
||||
|
||||
cookie-signature@1.0.6: {}
|
||||
|
||||
cookie@0.5.0: {}
|
||||
|
||||
cookie@0.6.0: {}
|
||||
|
||||
cookie@0.7.2: {}
|
||||
@@ -19043,6 +19147,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
|
||||
@@ -19785,6 +19893,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 +20066,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 +20252,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 +20311,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 +21377,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 +21424,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 +21470,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 +21488,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 +21513,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 +21524,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 +21687,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
|
||||
|
||||
@@ -22175,11 +22348,11 @@ snapshots:
|
||||
dependencies:
|
||||
type-fest: 2.19.0
|
||||
|
||||
next-auth@4.24.7(next@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))(nodemailer@6.9.15)(react-dom@18.2.0(react@18.2.0))(react@18.2.0):
|
||||
next-auth@4.24.11(next@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))(nodemailer@6.9.15)(react-dom@18.2.0(react@18.2.0))(react@18.2.0):
|
||||
dependencies:
|
||||
'@babel/runtime': 7.24.5
|
||||
'@babel/runtime': 7.26.0
|
||||
'@panva/hkdf': 1.1.1
|
||||
cookie: 0.5.0
|
||||
cookie: 0.7.2
|
||||
jose: 4.15.5
|
||||
next: 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)
|
||||
oauth: 0.9.15
|
||||
@@ -22257,6 +22430,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 +23019,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 +23236,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 +24117,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: {}
|
||||
|
||||
|
||||
+4
-2
@@ -85,12 +85,14 @@ 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
|
||||
|
||||
ARG TARGETPLATFORM
|
||||
ARG BUILDPLATFORM
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
ENV NODE_ENV production
|
||||
|
||||
+7
-1
@@ -14,6 +14,12 @@ if [ -z "$DATABASE_URL" ]; then
|
||||
fi
|
||||
fi
|
||||
|
||||
# Check if CLICKHOUSE_URL is not set
|
||||
if [ -z "$CLICKHOUSE_URL" ]; then
|
||||
echo "Error: CLICKHOUSE_URL is not configured. Migrating from V2? Check out migration guide: https://langfuse.com/self-hosting/upgrade-guides/upgrade-v2-to-v3"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Set DIRECT_URL to the value of DATABASE_URL if it is not set, required for migrations
|
||||
if [ -z "$DIRECT_URL" ]; then
|
||||
export DIRECT_URL=$DATABASE_URL
|
||||
@@ -52,4 +58,4 @@ if [ $status -ne 0 ]; then
|
||||
fi
|
||||
|
||||
# Run the command passed to the docker image on start
|
||||
exec "$@"
|
||||
exec "$@"
|
||||
|
||||
+1
-1
@@ -24,7 +24,7 @@ const serverTestConfig = {
|
||||
const asyncServerTestConfig = {
|
||||
displayName: "async-server",
|
||||
testPathIgnorePatterns: ["__e2e__"],
|
||||
testMatch: ["/**/async/*.servertest.[jt]s?(x)"],
|
||||
testMatch: ["/**/async/**/*.servertest.[jt]s?(x)"],
|
||||
testEnvironment: "jest-environment-node",
|
||||
setupFilesAfterEnv: ["<rootDir>/src/__tests__/after-teardown.ts"],
|
||||
globalTeardown: "<rootDir>/src/__tests__/teardown.ts",
|
||||
|
||||
+21
-14
@@ -28,6 +28,9 @@ const cspHeader = `
|
||||
${env.SENTRY_CSP_REPORT_URI ? `report-uri ${env.SENTRY_CSP_REPORT_URI}; report-to csp-endpoint;` : ""}
|
||||
`;
|
||||
|
||||
// Match rules for Hugging Face
|
||||
const huggingFaceHosts = ["huggingface.co", ".*\\.hf\\.space$"];
|
||||
|
||||
const reportToHeader = {
|
||||
key: "Report-To",
|
||||
value: JSON.stringify({
|
||||
@@ -78,10 +81,6 @@ const nextConfig = {
|
||||
{
|
||||
source: "/:path*",
|
||||
headers: [
|
||||
{
|
||||
key: "x-frame-options",
|
||||
value: "SAMEORIGIN",
|
||||
},
|
||||
{
|
||||
key: "X-Content-Type-Options",
|
||||
value: "nosniff",
|
||||
@@ -97,6 +96,20 @@ const nextConfig = {
|
||||
...(env.SENTRY_CSP_REPORT_URI ? [reportToHeader] : []),
|
||||
],
|
||||
},
|
||||
{
|
||||
source: "/:path*",
|
||||
headers: [
|
||||
{
|
||||
key: "x-frame-options",
|
||||
value: "SAMEORIGIN",
|
||||
},
|
||||
],
|
||||
// Disable x-frame-options on Hugging Face to allow for embedded use of Langfuse
|
||||
missing: huggingFaceHosts.map((host) => ({
|
||||
type: "host",
|
||||
value: host,
|
||||
})),
|
||||
},
|
||||
// CSP header
|
||||
{
|
||||
source: "/:path((?!api).*)*",
|
||||
@@ -107,16 +120,10 @@ const nextConfig = {
|
||||
},
|
||||
],
|
||||
// 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
|
||||
},
|
||||
],
|
||||
missing: huggingFaceHosts.map((host) => ({
|
||||
type: "host",
|
||||
value: host,
|
||||
})),
|
||||
},
|
||||
// Required to check authentication status from langfuse.com
|
||||
...(env.NEXT_PUBLIC_LANGFUSE_CLOUD_REGION !== undefined
|
||||
|
||||
+6
-5
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "web",
|
||||
"version": "2.93.2",
|
||||
"version": "3.2.0",
|
||||
"private": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
@@ -16,6 +16,7 @@
|
||||
"start": "dotenv -e ../.env -- sh -c 'NEXT_MANUAL_SIG_HANDLE=true next start'",
|
||||
"test": "dotenv -e ../.env -- jest --silent false --verbose false --runInBand --detectOpenHandles --selectProjects async-server",
|
||||
"test-sync": "dotenv -e ../.env -- jest --silent false --verbose false --runInBand --detectOpenHandles --selectProjects sync-server",
|
||||
"test-client": "dotenv -e ../.env -- jest --silent false --verbose false --runInBand --detectOpenHandles --selectProjects client",
|
||||
"test:watch": "dotenv -e ../.env -- jest --watch --runInBand",
|
||||
"test:e2e": "dotenv -e ../.env -- playwright test",
|
||||
"test:e2e:server": "dotenv -e ../.env -- jest --runInBand --detectOpenHandles --verbose --selectProjects e2e-server",
|
||||
@@ -115,13 +116,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-auth": "^4.24.11",
|
||||
"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 +139,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",
|
||||
|
||||
File diff suppressed because one or more lines are too long
|
After Width: | Height: | Size: 34 KiB |
@@ -2389,7 +2389,7 @@ components:
|
||||
type: string
|
||||
description: >-
|
||||
The content of the comment. May include markdown. Currently limited
|
||||
to 500 characters.
|
||||
to 3000 characters.
|
||||
authorUserId:
|
||||
type: string
|
||||
nullable: true
|
||||
@@ -2627,7 +2627,9 @@ components:
|
||||
usage:
|
||||
$ref: '#/components/schemas/Usage'
|
||||
nullable: true
|
||||
description: The usage data of the observation
|
||||
description: >-
|
||||
(Deprecated. Use usageDetails and costDetails instead.) The usage
|
||||
data of the observation
|
||||
level:
|
||||
$ref: '#/components/schemas/ObservationLevel'
|
||||
description: The level of the observation
|
||||
@@ -2643,6 +2645,25 @@ components:
|
||||
type: string
|
||||
nullable: true
|
||||
description: The prompt ID associated with the observation
|
||||
usageDetails:
|
||||
type: object
|
||||
additionalProperties:
|
||||
type: integer
|
||||
nullable: true
|
||||
description: >-
|
||||
The usage details of the observation. Key is the name of the usage
|
||||
metric, value is the number of units consumed. The total key is the
|
||||
sum of all (non-total) usage metrics or the total value ingested.
|
||||
costDetails:
|
||||
type: object
|
||||
additionalProperties:
|
||||
type: number
|
||||
format: double
|
||||
nullable: true
|
||||
description: >-
|
||||
The cost details of the observation. Key is the name of the cost
|
||||
metric, value is the cost in USD. The total key is the sum of all
|
||||
(non-total) cost metrics or the total value ingested.
|
||||
required:
|
||||
- id
|
||||
- type
|
||||
@@ -2683,17 +2704,23 @@ components:
|
||||
type: number
|
||||
format: double
|
||||
nullable: true
|
||||
description: The calculated cost of the input in USD
|
||||
description: >-
|
||||
(Deprecated. Use usageDetails and costDetails instead.) The
|
||||
calculated cost of the input in USD
|
||||
calculatedOutputCost:
|
||||
type: number
|
||||
format: double
|
||||
nullable: true
|
||||
description: The calculated cost of the output in USD
|
||||
description: >-
|
||||
(Deprecated. Use usageDetails and costDetails instead.) The
|
||||
calculated cost of the output in USD
|
||||
calculatedTotalCost:
|
||||
type: number
|
||||
format: double
|
||||
nullable: true
|
||||
description: The calculated total cost in USD
|
||||
description: >-
|
||||
(Deprecated. Use usageDetails and costDetails instead.) The
|
||||
calculated total cost in USD
|
||||
latency:
|
||||
type: number
|
||||
format: double
|
||||
@@ -2709,7 +2736,9 @@ components:
|
||||
Usage:
|
||||
title: Usage
|
||||
type: object
|
||||
description: Standard interface for usage and cost
|
||||
description: >-
|
||||
(Deprecated. Use usageDetails and costDetails instead.) Standard
|
||||
interface for usage and cost
|
||||
properties:
|
||||
input:
|
||||
type: integer
|
||||
@@ -3608,6 +3637,15 @@ components:
|
||||
usage:
|
||||
$ref: '#/components/schemas/IngestionUsage'
|
||||
nullable: true
|
||||
usageDetails:
|
||||
$ref: '#/components/schemas/UsageDetails'
|
||||
nullable: true
|
||||
costDetails:
|
||||
type: object
|
||||
additionalProperties:
|
||||
type: number
|
||||
format: double
|
||||
nullable: true
|
||||
promptName:
|
||||
type: string
|
||||
nullable: true
|
||||
@@ -3638,6 +3676,15 @@ components:
|
||||
promptName:
|
||||
type: string
|
||||
nullable: true
|
||||
usageDetails:
|
||||
$ref: '#/components/schemas/UsageDetails'
|
||||
nullable: true
|
||||
costDetails:
|
||||
type: object
|
||||
additionalProperties:
|
||||
type: number
|
||||
format: double
|
||||
nullable: true
|
||||
promptVersion:
|
||||
type: integer
|
||||
nullable: true
|
||||
@@ -3955,6 +4002,37 @@ components:
|
||||
required:
|
||||
- successes
|
||||
- errors
|
||||
OpenAIUsageSchema:
|
||||
title: OpenAIUsageSchema
|
||||
type: object
|
||||
properties:
|
||||
prompt_tokens:
|
||||
type: integer
|
||||
completion_tokens:
|
||||
type: integer
|
||||
total_tokens:
|
||||
type: integer
|
||||
prompt_tokens_details:
|
||||
type: object
|
||||
additionalProperties:
|
||||
type: integer
|
||||
nullable: true
|
||||
completion_tokens_details:
|
||||
type: object
|
||||
additionalProperties:
|
||||
type: integer
|
||||
nullable: true
|
||||
required:
|
||||
- prompt_tokens
|
||||
- completion_tokens
|
||||
- total_tokens
|
||||
UsageDetails:
|
||||
title: UsageDetails
|
||||
oneOf:
|
||||
- type: object
|
||||
additionalProperties:
|
||||
type: integer
|
||||
- $ref: '#/components/schemas/OpenAIUsageSchema'
|
||||
GetMediaResponse:
|
||||
title: GetMediaResponse
|
||||
type: object
|
||||
@@ -4058,8 +4136,35 @@ components:
|
||||
title: MediaContentType
|
||||
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
|
||||
|
||||
@@ -689,7 +689,7 @@
|
||||
"auth": null,
|
||||
"body": {
|
||||
"mode": "raw",
|
||||
"raw": "{\n \"traceId\": \"example\",\n \"observationId\": \"example\",\n \"contentType\": \"example\",\n \"contentLength\": 0,\n \"sha256Hash\": \"example\",\n \"field\": \"example\"\n}",
|
||||
"raw": "{\n \"traceId\": \"example\",\n \"observationId\": \"example\",\n \"contentType\": \"image/png\",\n \"contentLength\": 0,\n \"sha256Hash\": \"example\",\n \"field\": \"example\"\n}",
|
||||
"options": {
|
||||
"raw": {
|
||||
"language": "json"
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import { v4 } from "uuid";
|
||||
import { JobExecutionStatus, Prisma, prisma } from "@langfuse/shared/src/db";
|
||||
import {
|
||||
clickhouseClient,
|
||||
getObservationById,
|
||||
getTraceById,
|
||||
OrgEnrichedApiKey,
|
||||
redis,
|
||||
} from "@langfuse/shared/src/server";
|
||||
@@ -15,6 +16,7 @@ const generateAuth = (username: string, password: string) => {
|
||||
const workerAdminAuth = generateAuth("admin", "myworkerpassword");
|
||||
|
||||
const userApiKeyAuth = generateAuth("pk-lf-1234567890", "sk-lf-1234567890");
|
||||
const projectId = "7a88fb47-b4e2-43b8-a06c-a5ce950dc53a";
|
||||
|
||||
describe("Health endpoints", () => {
|
||||
it("web container returns healthy", async () => {
|
||||
@@ -61,6 +63,7 @@ describe("Ingestion Pipeline", () => {
|
||||
timestamp: new Date().toISOString(),
|
||||
body: {
|
||||
name: "test trace",
|
||||
timestamp: new Date().toISOString(),
|
||||
id: traceId,
|
||||
userId: "user-1", // triggers the eval
|
||||
},
|
||||
@@ -73,6 +76,7 @@ describe("Ingestion Pipeline", () => {
|
||||
id: spanId,
|
||||
traceId: traceId,
|
||||
name: "test span",
|
||||
startTime: new Date().toISOString(),
|
||||
},
|
||||
},
|
||||
],
|
||||
@@ -103,32 +107,20 @@ describe("Ingestion Pipeline", () => {
|
||||
},
|
||||
});
|
||||
|
||||
const traces = await clickhouseClient().query({
|
||||
query: "SELECT * FROM traces",
|
||||
format: "JSONEachRow",
|
||||
});
|
||||
console.log("traces", await traces.text());
|
||||
expect(traceResponse.status).toBe(200);
|
||||
expect(traceResponse.body).not.toBeNull();
|
||||
expect((await traceResponse.json()).id).toBe(traceId);
|
||||
|
||||
const trace = await prisma.trace.findUnique({
|
||||
where: {
|
||||
id: traceId,
|
||||
},
|
||||
});
|
||||
const trace = await getTraceById(traceId, projectId);
|
||||
expect(trace).not.toBeNull();
|
||||
expect(trace?.name).toBe("test trace");
|
||||
|
||||
const observation = await prisma.observation.findUnique({
|
||||
where: {
|
||||
id: spanId,
|
||||
traceId: traceId,
|
||||
},
|
||||
});
|
||||
const observation = await getObservationById(spanId, projectId);
|
||||
expect(observation).not.toBeNull();
|
||||
expect(observation?.name).toBe("test span");
|
||||
|
||||
console.log("observationFounds", observation);
|
||||
|
||||
expect(redis).not.toBeNull();
|
||||
|
||||
const redisKeys = await redis?.keys(`api-key:*`);
|
||||
@@ -153,6 +145,7 @@ describe("Ingestion Pipeline", () => {
|
||||
projectId: "7a88fb47-b4e2-43b8-a06c-a5ce950dc53a",
|
||||
},
|
||||
});
|
||||
console.log("evalExecution", evalExecution);
|
||||
|
||||
expect(evalExecution).not.toBeNull();
|
||||
|
||||
@@ -163,12 +156,12 @@ describe("Ingestion Pipeline", () => {
|
||||
// failure due to missing openai key in the pipeline. Expected
|
||||
expect(evalExecution.status).toBe(JobExecutionStatus.ERROR);
|
||||
},
|
||||
50000,
|
||||
60000,
|
||||
10000,
|
||||
);
|
||||
|
||||
expect(response.status).toBe(207);
|
||||
}, 60000);
|
||||
}, 70000);
|
||||
|
||||
it("rate limit ingestion", async () => {
|
||||
// update the org in the database and set the rate limit to 1 for ingestion
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -54,6 +54,33 @@ describe("aggregateScores", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("should correctly aggregate multiple numeric scores with the same key and value", () => {
|
||||
const scores = [
|
||||
{
|
||||
name: "test",
|
||||
source: "API",
|
||||
dataType: "NUMERIC",
|
||||
value: 5,
|
||||
comment: "test comment",
|
||||
},
|
||||
{
|
||||
name: "test",
|
||||
source: "API",
|
||||
dataType: "NUMERIC",
|
||||
value: 5,
|
||||
comment: "another comment",
|
||||
},
|
||||
] as APIScore[];
|
||||
expect(aggregateScores(scores)).toEqual({
|
||||
"test-API-NUMERIC": {
|
||||
type: "NUMERIC",
|
||||
values: [5, 5],
|
||||
average: 5,
|
||||
comment: undefined,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it("should correctly aggregate scores with different keys", () => {
|
||||
const scores: APIScore[] = [
|
||||
{
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user