Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
05d28db250 | ||
|
|
c97ffde598 | ||
|
|
42c12b7ce7 | ||
|
|
602e4998d4 | ||
|
|
29da3162da | ||
|
|
747b7e9b7f | ||
|
|
966894cee6 | ||
|
|
9ba9098303 | ||
|
|
5561668b43 | ||
|
|
3fe5bf16d1 | ||
|
|
bb19675769 | ||
|
|
2fb70c207b | ||
|
|
0bbc27d53b | ||
|
|
ad7f273638 | ||
|
|
7637d45fd3 | ||
|
|
995b30b3d7 | ||
|
|
26d615842c | ||
|
|
61cf13a1db | ||
|
|
078f7d6f83 | ||
|
|
b4ce9a486b | ||
|
|
801090fdf1 | ||
|
|
4fd42b7b5b | ||
|
|
2591f0d7b7 | ||
|
|
95e8a2a793 | ||
|
|
8b367b1a10 | ||
|
|
2b80cba1fe | ||
|
|
0b7fd5e70e | ||
|
|
fcec58f976 | ||
|
|
62101ba598 | ||
|
|
5a6eba5242 | ||
|
|
20c2bb9dc2 | ||
|
|
4d0e431f2a | ||
|
|
3bed36e0e7 | ||
|
|
3196ee7d04 | ||
|
|
5219bb74ac | ||
|
|
555a16bce7 | ||
|
|
499a7b40f1 | ||
|
|
8e9acb2fe8 | ||
|
|
dda176f3f9 | ||
|
|
48aa358e99 | ||
|
|
6127f23758 | ||
|
|
bdfc51ca72 | ||
|
|
0bbf87abfd | ||
|
|
c7c934fbab | ||
|
|
3125f275e7 | ||
|
|
7cb27deb22 | ||
|
|
4b72fecb7f | ||
|
|
23efeb69cd | ||
|
|
479d85f4ac | ||
|
|
dfe9f802c8 | ||
|
|
6928d561b9 | ||
|
|
f97bd913b6 | ||
|
|
3bec8a535b | ||
|
|
5a9f47faef | ||
|
|
3ee6e936fb | ||
|
|
9346e6feb5 | ||
|
|
94387d1f6b | ||
|
|
e2bcc6bf4e | ||
|
|
defca401c9 | ||
|
|
d12c623a96 | ||
|
|
1eb945820c | ||
|
|
dcd1703668 | ||
|
|
42b0a49e2d | ||
|
|
983f34f49d | ||
|
|
3e66bb6dff | ||
|
|
2ea0b9bac4 | ||
|
|
2be49c2488 | ||
|
|
1aeb79b913 | ||
|
|
2084f69bec | ||
|
|
87fd20ec9e | ||
|
|
e028117453 | ||
|
|
8483ac749c | ||
|
|
d40635a860 | ||
|
|
4e27b6f0f1 | ||
|
|
cd3d6b0251 | ||
|
|
eea071c32f | ||
|
|
aebcac4b57 | ||
|
|
7e4836a345 | ||
|
|
67d37ec66b | ||
|
|
4b36304a55 | ||
|
|
d3abad82e9 | ||
|
|
dc672099dc | ||
|
|
5e2ab2e25f | ||
|
|
770d1ab7d4 | ||
|
|
b6a31f6146 | ||
|
|
33155deb83 | ||
|
|
65b4429272 | ||
|
|
2a30f5447d | ||
|
|
f147d2bde3 | ||
|
|
034fbac052 | ||
|
|
abbca987b4 | ||
|
|
f8080ca04c | ||
|
|
0396525f1b | ||
|
|
407f442b4f | ||
|
|
6b36836fd6 | ||
|
|
0a08f19ceb | ||
|
|
aff27ef922 | ||
|
|
7c65366eac | ||
|
|
f2152dd493 | ||
|
|
42e446ed55 | ||
|
|
77a65dcf78 | ||
|
|
e9d4594e80 | ||
|
|
a0cf26baec | ||
|
|
08f4e64eb2 | ||
|
|
788d1d67b0 | ||
|
|
01f95dac20 | ||
|
|
f6f84d9d39 | ||
|
|
ddfc42f739 | ||
|
|
22a391e315 | ||
|
|
46b19279c4 | ||
|
|
716ed04e60 | ||
|
|
87798edd95 | ||
|
|
7058e05ed5 | ||
|
|
fab0ba4f11 | ||
|
|
2094db0101 | ||
|
|
f23bea9272 | ||
|
|
adb67f23e6 | ||
|
|
ed087ae60b | ||
|
|
229320a3db | ||
|
|
38b3e8aaa4 | ||
|
|
2785aa6a32 | ||
|
|
06c0392e79 | ||
|
|
f9a3107149 | ||
|
|
d987c46fed | ||
|
|
8fdd4a7ca7 | ||
|
|
535e6ecb66 | ||
|
|
204691900d | ||
|
|
a1db67580d | ||
|
|
03094953b8 | ||
|
|
ba3e6eec47 | ||
|
|
93de36d25f | ||
|
|
0f66f37b32 | ||
|
|
b4e3634a14 | ||
|
|
29f1072da4 | ||
|
|
5673849f7b | ||
|
|
9cd7e68efc | ||
|
|
eb84df07bf | ||
|
|
b4ef5887de | ||
|
|
aff5e16351 | ||
|
|
00bb0ce5f8 | ||
|
|
eaf87083c6 | ||
|
|
bfe9269a95 | ||
|
|
54e7bc2298 | ||
|
|
a7720b0e8f | ||
|
|
e1ca51ec33 | ||
|
|
eb26a9861d | ||
|
|
b1692e1786 | ||
|
|
6b7709f8e7 | ||
|
|
6a5a233749 | ||
|
|
b262d7ff4a | ||
|
|
2b7aa21fa5 | ||
|
|
a60bf7ce55 | ||
|
|
0d1f948625 | ||
|
|
cf42dd726d | ||
|
|
8aeec63c07 | ||
|
|
0cd3fcfbeb | ||
|
|
a880bdb3b2 | ||
|
|
5c6d3e14f2 | ||
|
|
6ccc57312c | ||
|
|
c4831d1b25 | ||
|
|
cc08408f0a | ||
|
|
6a41aadc98 | ||
|
|
9d6efbae19 | ||
|
|
74be1e87c1 | ||
|
|
dd823da1da | ||
|
|
992f0affbc | ||
|
|
b8c192324c | ||
|
|
1416e760ff | ||
|
|
4266b46dd9 | ||
|
|
1933f8f201 | ||
|
|
1e35eb3a66 | ||
|
|
5478c5e317 | ||
|
|
140452c89e | ||
|
|
7ac92e83da | ||
|
|
8a65bad7d4 | ||
|
|
6c6348bfbe | ||
|
|
9ff7fc942d | ||
|
|
a3f59e7e95 | ||
|
|
445bdcb65c | ||
|
|
8230aa79b3 | ||
|
|
57ca7fb478 | ||
|
|
2db88101e1 | ||
|
|
669167002a | ||
|
|
2f6ade9354 | ||
|
|
86f44df5dc | ||
|
|
34da790d5e | ||
|
|
c16d86f7e5 | ||
|
|
1b2b8cc43a | ||
|
|
3903790086 | ||
|
|
6da1280c43 | ||
|
|
85343bf26c | ||
|
|
e3b4dc1928 | ||
|
|
a4939bacd8 | ||
|
|
4441942cf5 | ||
|
|
6597b02c09 | ||
|
|
0df3ad27dc | ||
|
|
696d458a27 | ||
|
|
fd18ab605d | ||
|
|
d47d14eecd | ||
|
|
24a81e1589 |
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"name": "langfuse dev",
|
||||
"image": "mcr.microsoft.com/devcontainers/javascript-node:20-bookworm",
|
||||
"features": {
|
||||
"ghcr.io/devcontainers/features/docker-in-docker:2": {
|
||||
"version": "latest",
|
||||
"moby": true
|
||||
}
|
||||
},
|
||||
"forwardPorts": [3000],
|
||||
|
||||
"onCreateCommand": "npm install -g pnpm@8.15.5",
|
||||
"postCreateCommand": "cp .env.dev.example .env && pnpm run dx-f"
|
||||
}
|
||||
@@ -34,10 +34,6 @@ S3_SECRET_ACCESS_KEY=
|
||||
S3_BUCKET_NAME=
|
||||
S3_REGION=
|
||||
|
||||
# Prompt playground
|
||||
OPENAI_API_KEY=""
|
||||
ANTHROPIC_API_KEY=""
|
||||
|
||||
# Set during docker build of application
|
||||
# Used to disable environment verification at build time
|
||||
# DOCKER_BUILD=1
|
||||
|
||||
@@ -13,11 +13,6 @@ NEXTAUTH_URL="http://localhost:3000"
|
||||
LANGFUSE_ENABLE_EXPERIMENTAL_FEATURES="false"
|
||||
SALT="salt"
|
||||
|
||||
|
||||
# Prompt playground
|
||||
OPENAI_API_KEY=""
|
||||
ANTHROPIC_API_KEY=""
|
||||
|
||||
# Redis
|
||||
REDIS_HOST="127.0.0.1"
|
||||
REDIS_PORT=6379
|
||||
|
||||
+16
-5
@@ -9,6 +9,8 @@
|
||||
DATABASE_URL="postgresql://postgres:postgres@db:5432/postgres"
|
||||
# DIRECT_URL="postgresql://postgres:postgres@db:5432/postgres"
|
||||
# SHADOW_DATABASE_URL=
|
||||
# optional, set to true to disable automated database migrations on Docker start
|
||||
# LANGFUSE_AUTO_POSTGRES_MIGRATION_DISABLED=
|
||||
|
||||
# Next Auth
|
||||
# NEXTAUTH_URL does not need to be set when deploying on Vercel
|
||||
@@ -23,6 +25,7 @@ SALT="salt"
|
||||
# Use CSP headers to enforce HTTPS, optional
|
||||
LANGFUSE_CSP_ENFORCE_HTTPS="true"
|
||||
|
||||
|
||||
# Docker only, optional
|
||||
# PORT=3000
|
||||
# HOSTNAME=localhost
|
||||
@@ -34,6 +37,9 @@ LANGFUSE_CSP_ENFORCE_HTTPS="true"
|
||||
# Enable experimental features, optional
|
||||
# LANGFUSE_ENABLE_EXPERIMENTAL_FEATURES=true
|
||||
|
||||
# Disable automated database migrations on Docker start, optional
|
||||
# LANFUSE_WEB_MIGRATION_DISABLED=true
|
||||
|
||||
# Auth, optional configuration
|
||||
# AUTH_DOMAINS_WITH_SSO_ENFORCEMENT=domain1.com,domain2.com
|
||||
# AUTH_DISABLE_USERNAME_PASSWORD=true
|
||||
@@ -43,6 +49,7 @@ LANGFUSE_CSP_ENFORCE_HTTPS="true"
|
||||
# AUTH_GOOGLE_CLIENT_ID=
|
||||
# AUTH_GOOGLE_CLIENT_SECRET=
|
||||
# AUTH_GOOGLE_ALLOW_ACCOUNT_LINKING=false
|
||||
# AUTH_GOOGLE_ALLOWED_DOMAINS=langfuse.com,google.com # optional allowlist of workspace domains that can sign in via Google
|
||||
# AUTH_GITHUB_CLIENT_ID=
|
||||
# AUTH_GITHUB_CLIENT_SECRET=
|
||||
# AUTH_GITHUB_ALLOW_ACCOUNT_LINKING=false
|
||||
@@ -62,7 +69,12 @@ LANGFUSE_CSP_ENFORCE_HTTPS="true"
|
||||
# AUTH_COGNITO_CLIENT_SECRET=
|
||||
# AUTH_COGNITO_ISSUER=
|
||||
# AUTH_COGNITO_ALLOW_ACCOUNT_LINKING=false
|
||||
|
||||
# AUTH_CUSTOM_CLIENT_ID=
|
||||
# AUTH_CUSTOM_CLIENT_SECRET=
|
||||
# AUTH_CUSTOM_ISSUER=
|
||||
# AUTH_CUSTOM_NAME=
|
||||
# AUTH_CUSTOM_SCOPE="openid email profile" # optional
|
||||
# AUTH_CUSTOM_ALLOW_ACCOUNT_LINKING=false
|
||||
|
||||
# Transactional email, optional
|
||||
# Defines the email address to use as the from address.
|
||||
@@ -76,6 +88,7 @@ LANGFUSE_CSP_ENFORCE_HTTPS="true"
|
||||
# S3_SECRET_ACCESS_KEY=
|
||||
# S3_BUCKET_NAME=
|
||||
# S3_REGION=
|
||||
# BATCH_EXPORT_DOWNLOAD_LINK_EXPIRATION_HOURS=
|
||||
|
||||
# Exports are streamed to S3 in pages to avoid memory issues
|
||||
# The page size can be adjusted if needed to optimize performance
|
||||
@@ -118,15 +131,13 @@ LANGFUSE_CSP_ENFORCE_HTTPS="true"
|
||||
# NEXT_SENTRY_ORG=
|
||||
# NEXT_SENTRY_PROJECT=
|
||||
# SENTRY_AUTH_TOKEN=
|
||||
# Used to determine the Sentry sample rate
|
||||
# LANGFUSE_TRACING_SAMPLE_RATE=
|
||||
|
||||
# Cloudflare Turnstile
|
||||
# NEXT_PUBLIC_TURNSTILE_SITE_KEY=
|
||||
# TURNSTILE_SECRET_KEY=
|
||||
|
||||
# Prompt playground
|
||||
# OPENAI_API_KEY=""
|
||||
# ANTHROPIC_API_KEY=""
|
||||
|
||||
# Betterstack
|
||||
# LANGFUSE_TEAM_BETTERSTACK_TOKEN=
|
||||
|
||||
|
||||
@@ -40,25 +40,35 @@ jobs:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: Build Web Docker image
|
||||
uses: docker/build-push-action@v4
|
||||
with:
|
||||
context: .
|
||||
file: ./web/Dockerfile
|
||||
push: false
|
||||
- name: Build and run both images from compose
|
||||
run: |
|
||||
docker compose -f docker-compose.build.yml up -d
|
||||
sleep 5 # Wait for PostgreSQL to accept connections
|
||||
|
||||
- name: Build Worker Docker image
|
||||
uses: docker/build-push-action@v4
|
||||
with:
|
||||
context: .
|
||||
file: ./worker/Dockerfile
|
||||
push: false
|
||||
- name: Check server health
|
||||
run: |
|
||||
timeout 10 bash -c 'until curl -f http://localhost:3000/api/public/health; do sleep 2; done'
|
||||
|
||||
- name: Check worker health
|
||||
run: |
|
||||
timeout 10 bash -c 'until curl -f http://localhost:3030/api/health; do sleep 2; done'
|
||||
|
||||
- name: Ensure no unhealthy status
|
||||
run: |
|
||||
if docker-compose ps | grep "(unhealthy)"; then
|
||||
echo "One or more services are unhealthy"
|
||||
exit 1
|
||||
else
|
||||
echo "All services are healthy"
|
||||
fi
|
||||
|
||||
tests-web:
|
||||
runs-on: ubuntu-latest
|
||||
name: tests-web (node${{ matrix.node-version }}, pg${{ matrix.postgres-version }})
|
||||
strategy:
|
||||
matrix:
|
||||
node-version: [20]
|
||||
postgres-version: [12, 15]
|
||||
steps:
|
||||
- name: Set Swap Space
|
||||
uses: pierotofy/set-swap-space@master
|
||||
@@ -89,6 +99,9 @@ jobs:
|
||||
run: |
|
||||
docker compose -f docker-compose.dev.yml up -d
|
||||
sleep 5 # Wait for PostgreSQL to accept connections
|
||||
docker compose ps
|
||||
env:
|
||||
POSTGRES_VERSION: ${{ matrix.postgres-version }}
|
||||
|
||||
- name: Seed DB
|
||||
run: |
|
||||
@@ -106,9 +119,11 @@ jobs:
|
||||
|
||||
tests-worker:
|
||||
runs-on: ubuntu-latest
|
||||
name: tests-worker (node${{ matrix.node-version }}, pg${{ matrix.postgres-version }})
|
||||
strategy:
|
||||
matrix:
|
||||
node-version: [20]
|
||||
postgres-version: [12, 15]
|
||||
steps:
|
||||
- name: Set Swap Space
|
||||
uses: pierotofy/set-swap-space@master
|
||||
@@ -139,6 +154,7 @@ jobs:
|
||||
run: |
|
||||
docker compose -f docker-compose.dev.yml up -d
|
||||
sleep 5 # Wait for PostgreSQL to accept connections
|
||||
docker compose ps
|
||||
|
||||
- name: Seed DB
|
||||
run: |
|
||||
@@ -176,6 +192,7 @@ jobs:
|
||||
- name: Run + migrate
|
||||
run: |
|
||||
docker compose -f docker-compose.dev.yml up -d
|
||||
docker compose ps
|
||||
sleep 5 # Wait for PostgreSQL to accept connections
|
||||
|
||||
- name: Seed DB
|
||||
@@ -242,6 +259,14 @@ jobs:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v3
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
with:
|
||||
driver-opts: network=host
|
||||
|
||||
- name: Extract metadata (tags, labels) for Docker
|
||||
id: meta-web
|
||||
uses: docker/metadata-action@v4
|
||||
@@ -265,6 +290,9 @@ jobs:
|
||||
push: true
|
||||
tags: ${{ steps.meta-web.outputs.tags }}
|
||||
labels: ${{ steps.meta-web.outputs.labels }}
|
||||
platforms: |
|
||||
linux/amd64
|
||||
linux/arm64
|
||||
|
||||
- name: Extract metadata (tags, labels) for Docker
|
||||
id: meta-worker
|
||||
@@ -289,3 +317,6 @@ jobs:
|
||||
push: true
|
||||
tags: ${{ steps.meta-worker.outputs.tags }}
|
||||
labels: ${{ steps.meta-worker.outputs.labels }}
|
||||
platforms: |
|
||||
linux/amd64
|
||||
linux/arm64
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
"on":
|
||||
push:
|
||||
branches:
|
||||
- production
|
||||
name: Deploy to web
|
||||
jobs:
|
||||
porter-deploy:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v3
|
||||
- name: Set Github tag
|
||||
id: vars
|
||||
run: echo "sha_short=$(git rev-parse --short HEAD)" >> $GITHUB_OUTPUT
|
||||
- name: Setup porter
|
||||
uses: porter-dev/setup-porter@v0.1.0
|
||||
- name: Deploy stack
|
||||
timeout-minutes: 30
|
||||
run: exec porter apply
|
||||
env:
|
||||
PORTER_CLUSTER: "4060"
|
||||
PORTER_DEPLOYMENT_TARGET_ID: 3be075c0-b121-4123-aec1-5dc571626b0c
|
||||
PORTER_HOST: https://dashboard.porter.run
|
||||
PORTER_PR_NUMBER: ${{ github.event.number }}
|
||||
PORTER_PROJECT: "12565"
|
||||
PORTER_REPO_NAME: ${{ github.event.repository.name }}
|
||||
PORTER_STACK_NAME: web
|
||||
PORTER_TAG: ${{ steps.vars.outputs.sha_short }}
|
||||
PORTER_TOKEN: ${{ secrets.PORTER_STACK_12565_4060 }}
|
||||
@@ -0,0 +1,29 @@
|
||||
"on":
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
name: Deploy to web (Staging)
|
||||
jobs:
|
||||
porter-deploy:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v3
|
||||
- name: Set Github tag
|
||||
id: vars
|
||||
run: echo "sha_short=$(git rev-parse --short HEAD)" >> $GITHUB_OUTPUT
|
||||
- name: Setup porter
|
||||
uses: porter-dev/setup-porter@v0.1.0
|
||||
- name: Deploy stack
|
||||
timeout-minutes: 30
|
||||
run: exec porter apply
|
||||
env:
|
||||
PORTER_CLUSTER: "4037"
|
||||
PORTER_DEPLOYMENT_TARGET_ID: c22d8fa2-f97c-4e1f-9cae-83cc6bfbad35
|
||||
PORTER_HOST: https://dashboard.getporter.dev
|
||||
PORTER_PR_NUMBER: ${{ github.event.number }}
|
||||
PORTER_PROJECT: "12565"
|
||||
PORTER_REPO_NAME: ${{ github.event.repository.name }}
|
||||
PORTER_STACK_NAME: web
|
||||
PORTER_TAG: ${{ steps.vars.outputs.sha_short }}
|
||||
PORTER_TOKEN: ${{ secrets.PORTER_STACK_12565_4037 }}
|
||||
@@ -0,0 +1,29 @@
|
||||
"on":
|
||||
push:
|
||||
branches:
|
||||
- production
|
||||
name: Deploy to web (US temp)
|
||||
jobs:
|
||||
porter-deploy:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v3
|
||||
- name: Set Github tag
|
||||
id: vars
|
||||
run: echo "sha_short=$(git rev-parse --short HEAD)" >> $GITHUB_OUTPUT
|
||||
- name: Setup porter
|
||||
uses: porter-dev/setup-porter@v0.1.0
|
||||
- name: Deploy stack
|
||||
timeout-minutes: 30
|
||||
run: exec porter apply
|
||||
env:
|
||||
PORTER_CLUSTER: "4054"
|
||||
PORTER_DEPLOYMENT_TARGET_ID: cddc0f2a-bb19-401a-9819-29f59f76b101
|
||||
PORTER_HOST: https://dashboard.porter.run
|
||||
PORTER_PR_NUMBER: ${{ github.event.number }}
|
||||
PORTER_PROJECT: "12565"
|
||||
PORTER_REPO_NAME: ${{ github.event.repository.name }}
|
||||
PORTER_STACK_NAME: web
|
||||
PORTER_TAG: ${{ steps.vars.outputs.sha_short }}
|
||||
PORTER_TOKEN: ${{ secrets.PORTER_STACK_12565_4054 }}
|
||||
+6
-6
@@ -1,8 +1,8 @@
|
||||
"on":
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
name: Deploy to worker
|
||||
- production
|
||||
name: Deploy to worker (EU temp)
|
||||
jobs:
|
||||
porter-deploy:
|
||||
runs-on: ubuntu-latest
|
||||
@@ -18,12 +18,12 @@ jobs:
|
||||
timeout-minutes: 30
|
||||
run: exec porter apply
|
||||
env:
|
||||
PORTER_CLUSTER: "3959"
|
||||
PORTER_DEPLOYMENT_TARGET_ID: d2bb23e4-1d77-48f2-a113-383d076959b3
|
||||
PORTER_HOST: https://dashboard.getporter.dev
|
||||
PORTER_CLUSTER: "4060"
|
||||
PORTER_DEPLOYMENT_TARGET_ID: 3be075c0-b121-4123-aec1-5dc571626b0c
|
||||
PORTER_HOST: https://dashboard.porter.run
|
||||
PORTER_PR_NUMBER: ${{ github.event.number }}
|
||||
PORTER_PROJECT: "12565"
|
||||
PORTER_REPO_NAME: ${{ github.event.repository.name }}
|
||||
PORTER_STACK_NAME: worker
|
||||
PORTER_TAG: ${{ steps.vars.outputs.sha_short }}
|
||||
PORTER_TOKEN: ${{ secrets.PORTER_STACK_12565_3959 }}
|
||||
PORTER_TOKEN: ${{ secrets.PORTER_STACK_12565_4060 }}
|
||||
@@ -0,0 +1,29 @@
|
||||
"on":
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
name: Deploy to worker (staging)
|
||||
jobs:
|
||||
porter-deploy:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v3
|
||||
- name: Set Github tag
|
||||
id: vars
|
||||
run: echo "sha_short=$(git rev-parse --short HEAD)" >> $GITHUB_OUTPUT
|
||||
- name: Setup porter
|
||||
uses: porter-dev/setup-porter@v0.1.0
|
||||
- name: Deploy stack
|
||||
timeout-minutes: 30
|
||||
run: exec porter apply
|
||||
env:
|
||||
PORTER_CLUSTER: "4037"
|
||||
PORTER_DEPLOYMENT_TARGET_ID: c22d8fa2-f97c-4e1f-9cae-83cc6bfbad35
|
||||
PORTER_HOST: https://dashboard.getporter.dev
|
||||
PORTER_PR_NUMBER: ${{ github.event.number }}
|
||||
PORTER_PROJECT: "12565"
|
||||
PORTER_REPO_NAME: ${{ github.event.repository.name }}
|
||||
PORTER_STACK_NAME: worker
|
||||
PORTER_TAG: ${{ steps.vars.outputs.sha_short }}
|
||||
PORTER_TOKEN: ${{ secrets.PORTER_STACK_12565_4037 }}
|
||||
@@ -0,0 +1,29 @@
|
||||
"on":
|
||||
push:
|
||||
branches:
|
||||
- production
|
||||
name: Deploy to worker (US)
|
||||
jobs:
|
||||
porter-deploy:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v3
|
||||
- name: Set Github tag
|
||||
id: vars
|
||||
run: echo "sha_short=$(git rev-parse --short HEAD)" >> $GITHUB_OUTPUT
|
||||
- name: Setup porter
|
||||
uses: porter-dev/setup-porter@v0.1.0
|
||||
- name: Deploy stack
|
||||
timeout-minutes: 30
|
||||
run: exec porter apply
|
||||
env:
|
||||
PORTER_CLUSTER: "4038"
|
||||
PORTER_DEPLOYMENT_TARGET_ID: 260cd04e-5dc8-4b22-a291-ad4453f981f7
|
||||
PORTER_HOST: https://dashboard.getporter.dev
|
||||
PORTER_PR_NUMBER: ${{ github.event.number }}
|
||||
PORTER_PROJECT: "12565"
|
||||
PORTER_REPO_NAME: ${{ github.event.repository.name }}
|
||||
PORTER_STACK_NAME: worker
|
||||
PORTER_TAG: ${{ steps.vars.outputs.sha_short }}
|
||||
PORTER_TOKEN: ${{ secrets.PORTER_STACK_12565_4038 }}
|
||||
@@ -0,0 +1,29 @@
|
||||
"on":
|
||||
push:
|
||||
branches:
|
||||
- production
|
||||
name: Deploy to worker (US temp)
|
||||
jobs:
|
||||
porter-deploy:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v3
|
||||
- name: Set Github tag
|
||||
id: vars
|
||||
run: echo "sha_short=$(git rev-parse --short HEAD)" >> $GITHUB_OUTPUT
|
||||
- name: Setup porter
|
||||
uses: porter-dev/setup-porter@v0.1.0
|
||||
- name: Deploy stack
|
||||
timeout-minutes: 30
|
||||
run: exec porter apply
|
||||
env:
|
||||
PORTER_CLUSTER: "4054"
|
||||
PORTER_DEPLOYMENT_TARGET_ID: cddc0f2a-bb19-401a-9819-29f59f76b101
|
||||
PORTER_HOST: https://dashboard.porter.run
|
||||
PORTER_PR_NUMBER: ${{ github.event.number }}
|
||||
PORTER_PROJECT: "12565"
|
||||
PORTER_REPO_NAME: ${{ github.event.repository.name }}
|
||||
PORTER_STACK_NAME: worker
|
||||
PORTER_TAG: ${{ steps.vars.outputs.sha_short }}
|
||||
PORTER_TOKEN: ${{ secrets.PORTER_STACK_12565_4054 }}
|
||||
@@ -0,0 +1,39 @@
|
||||
name: Snyk Container
|
||||
on: push
|
||||
jobs:
|
||||
snyk:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- 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)
|
||||
continue-on-error: true
|
||||
uses: snyk/actions/docker@master
|
||||
env:
|
||||
SNYK_TOKEN: ${{ secrets.SNYK_TOKEN }}
|
||||
with:
|
||||
image: langfuse-server
|
||||
args: --file=web/Dockerfile
|
||||
|
||||
- name: Upload result to GitHub Code Scanning
|
||||
uses: github/codeql-action/upload-sarif@v2
|
||||
with:
|
||||
sarif_file: snyk.sarif
|
||||
category: web
|
||||
|
||||
- name: Run Snyk to check Docker image for vulnerabilities (langfuse-worker)
|
||||
continue-on-error: true
|
||||
uses: snyk/actions/docker@master
|
||||
env:
|
||||
SNYK_TOKEN: ${{ secrets.SNYK_TOKEN }}
|
||||
with:
|
||||
image: langfuse-worker
|
||||
args: --file=worker/Dockerfile
|
||||
|
||||
- name: Upload result to GitHub Code Scanning
|
||||
uses: github/codeql-action/upload-sarif@v2
|
||||
with:
|
||||
sarif_file: snyk.sarif
|
||||
category: worker
|
||||
Executable
+22
@@ -0,0 +1,22 @@
|
||||
#!/bin/sh
|
||||
|
||||
# Get the current branch
|
||||
current_branch=$(git rev-parse --abbrev-ref HEAD)
|
||||
|
||||
# Define the protected branch
|
||||
protected_branch="main"
|
||||
|
||||
# Check if the current branch is the protected branch
|
||||
if [ "$current_branch" = "$protected_branch" ]; then
|
||||
echo "🚨 You are about to commit to the $protected_branch branch. Are you sure? (y/n)"
|
||||
read -r answer < /dev/tty
|
||||
if [ "$answer" != "${answer#[Yy]}" ]; then
|
||||
exit 0 # Commit will proceed
|
||||
else
|
||||
echo "Commit to $protected_branch branch has been canceled."
|
||||
exit 1 # Commit will be blocked
|
||||
fi
|
||||
fi
|
||||
|
||||
# If not the protected branch, proceed with the commit
|
||||
exit 0
|
||||
Executable
+22
@@ -0,0 +1,22 @@
|
||||
#!/bin/sh
|
||||
|
||||
# Get the current branch
|
||||
current_branch=$(git rev-parse --abbrev-ref HEAD)
|
||||
|
||||
# Define the protected branch
|
||||
protected_branch="main"
|
||||
|
||||
# Check if the current branch is the protected branch
|
||||
if [ "$current_branch" = "$protected_branch" ]; then
|
||||
echo "🚨 You are about to push to the $protected_branch branch. Are you sure? (y/n)"
|
||||
read -r answer < /dev/tty
|
||||
if [ "$answer" != "${answer#[Yy]}" ]; then
|
||||
exit 0 # Push will proceed
|
||||
else
|
||||
echo "Push to $protected_branch branch has been canceled."
|
||||
exit 1 # Push will be blocked
|
||||
fi
|
||||
fi
|
||||
|
||||
# If not the protected branch, proceed with the push
|
||||
exit 0
|
||||
+16
-2
@@ -55,7 +55,7 @@ A good first step is to search for open [issues](https://github.com/langfuse/lan
|
||||
|
||||
> [!NOTE]
|
||||
> Infrastructure will change in Langfuse version 3.0. More in the [GitHub Discussions](https://github.com/orgs/langfuse/discussions/1902).
|
||||
> `langfuse/langfuse/worker` is under active development and not recommended for production use in Langfuse 2.x
|
||||
> `langfuse/langfuse/worker` is under active development and not recommended for production use in Langfuse 2.x.
|
||||
|
||||
```mermaid
|
||||
flowchart TB
|
||||
@@ -151,6 +151,8 @@ Requirements
|
||||
- Node.js 20 as specified in the [.nvmrc](.nvmrc)
|
||||
- Docker to run the database locally
|
||||
|
||||
**Note:** You can also simply run Langfuse in a **GitHub Codespace** via the provided devcontainer. To do this, click on the green "Code" button in the top right corner of the repository and select "Open with Codespaces".
|
||||
|
||||
**Steps**
|
||||
|
||||
1. Fork the repository and clone it locally
|
||||
@@ -191,6 +193,16 @@ Requirements
|
||||
pnpm run dev
|
||||
```
|
||||
|
||||
7. Open the web app in the browser:
|
||||
|
||||
http://localhost:3000
|
||||
|
||||
8. Log in as a test user (after you ran `db:seed` command):
|
||||
|
||||
Username: demo@langfuse.com
|
||||
|
||||
Password: password
|
||||
|
||||
## Monorepo quickstart
|
||||
|
||||
- Available packages and their dependencies
|
||||
@@ -358,7 +370,9 @@ The background color of the following component will be `hsl(var(--primary))` an
|
||||
|
||||
By following these guidelines, you can ensure that any contributions to our theme are consistent, maintainable, and aligned with our design system.
|
||||
|
||||
## Using secrets stored in 1Password
|
||||
## Maintainers
|
||||
|
||||
### Using secrets stored in 1Password
|
||||
|
||||
When applying changes to non-local environments, you may need to use secrets stored in 1Password. We use the 1Password CLI for this purpose.
|
||||
|
||||
|
||||
@@ -24,6 +24,8 @@
|
||||
</div>
|
||||
<span>Langfuse uses <a href="https://github.com/orgs/langfuse/discussions"><strong>Github Discussions</strong></a> for Support and Feature Requests.</span>
|
||||
<br/>
|
||||
<span>We're hiring. <a href="https://langfuse.com/careers"><strong>Join us</strong></a> in Backend Engineering, Product Engineering, and Developer Relations.</span>
|
||||
<br/>
|
||||
<br/>
|
||||
<div>
|
||||
<a href="https://github.com/langfuse/langfuse/blob/main/LICENSE"><img src="https://img.shields.io/badge/License-MIT-red.svg?style=flat-square" alt="MIT License"></a>
|
||||
|
||||
@@ -5,6 +5,8 @@ services:
|
||||
build:
|
||||
dockerfile: ./web/Dockerfile
|
||||
context: .
|
||||
args:
|
||||
- NEXT_PUBLIC_LANGFUSE_CLOUD_REGION=${NEXT_PUBLIC_LANGFUSE_CLOUD_REGION}
|
||||
depends_on:
|
||||
- db
|
||||
- redis
|
||||
@@ -20,11 +22,18 @@ services:
|
||||
- LANGFUSE_WORKER_HOST=${LANGFUSE_WORKER_HOST:-worker}
|
||||
- LANGFUSE_WORKER_PASSWORD=${LANGFUSE_WORKER_PASSWORD:-mybasicauthsecret}
|
||||
restart: always
|
||||
healthcheck:
|
||||
test: ["CMD", "curl", "-f", "http://localhost:3000/api/public/health"]
|
||||
interval: 30s
|
||||
timeout: 10s
|
||||
retries: 3
|
||||
|
||||
worker:
|
||||
build:
|
||||
dockerfile: ./worker/Dockerfile
|
||||
context: .
|
||||
args:
|
||||
- NEXT_PUBLIC_LANGFUSE_CLOUD_REGION=${NEXT_PUBLIC_LANGFUSE_CLOUD_REGION}
|
||||
depends_on:
|
||||
- db
|
||||
- redis
|
||||
@@ -41,6 +50,11 @@ services:
|
||||
- REDIS_AUTH=${REDIS_AUTH:-myredissecret}
|
||||
- LANGFUSE_WORKER_PASSWORD=${LANGFUSE_WORKER_PASSWORD:-mybasicauthsecret}
|
||||
restart: always
|
||||
healthcheck:
|
||||
test: ["CMD", "curl", "-f", "http://localhost:3030/api/health"]
|
||||
interval: 30s
|
||||
timeout: 10s
|
||||
retries: 3
|
||||
|
||||
redis:
|
||||
image: redis:7.2.4
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
version: "3.5"
|
||||
|
||||
services:
|
||||
redis:
|
||||
image: redis:7.2.4
|
||||
@@ -10,7 +8,7 @@ services:
|
||||
- 6379:6379
|
||||
|
||||
db:
|
||||
image: postgres
|
||||
image: postgres:${POSTGRES_VERSION:-latest}
|
||||
restart: always
|
||||
command: ["postgres", "-c", "log_statement=all"]
|
||||
environment:
|
||||
|
||||
+1
-3
@@ -1,8 +1,6 @@
|
||||
version: "3.5"
|
||||
|
||||
services:
|
||||
langfuse-server:
|
||||
image: ghcr.io/langfuse/langfuse:latest
|
||||
image: langfuse/langfuse:2
|
||||
depends_on:
|
||||
db:
|
||||
condition: service_healthy
|
||||
|
||||
+1
-1
@@ -33,7 +33,7 @@
|
||||
"@repo/eslint-config": "*",
|
||||
"@repo/typescript-config": "*",
|
||||
"@types/node": "^20.11.29",
|
||||
"@typescript-eslint/parser": "^7.7.0",
|
||||
"@typescript-eslint/parser": "^7.12.0",
|
||||
"eslint": "^8.57.0",
|
||||
"eslint-config-prettier": "^9.1.0",
|
||||
"eslint-config-standard": "^17.1.0",
|
||||
|
||||
@@ -11,6 +11,7 @@ import { encrypt, decrypt } from "@langfuse/shared/encryption";
|
||||
import { SsoProviderSchema } from "./types";
|
||||
import { type NextApiRequest, type NextApiResponse } from "next";
|
||||
import { env } from "../env";
|
||||
import { CustomSSOProvider } from "@langfuse/shared/src/server";
|
||||
|
||||
// Local cache for SSO configurations
|
||||
let cachedSsoConfigs: {
|
||||
@@ -173,6 +174,15 @@ const dbToNextAuthProvider = (provider: SsoProviderSchema): Provider | null => {
|
||||
...provider.authConfig,
|
||||
clientSecret: decrypt(provider.authConfig.clientSecret),
|
||||
});
|
||||
else if (provider.authProvider === "custom")
|
||||
return CustomSSOProvider({
|
||||
id: getAuthProviderIdForSsoConfig(provider), // use the domain as the provider id as we use domain-specific credentials
|
||||
...provider.authConfig,
|
||||
clientSecret: decrypt(provider.authConfig.clientSecret),
|
||||
authorization: {
|
||||
params: { scope: provider.authConfig.scope ?? "openid email profile" },
|
||||
},
|
||||
});
|
||||
else {
|
||||
// Type check to ensure we handle all providers
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
|
||||
@@ -76,12 +76,27 @@ export const CognitoProviderSchema = base.extend({
|
||||
.nullish(),
|
||||
});
|
||||
|
||||
export const CustomProviderSchema = base.extend({
|
||||
authProvider: z.literal("custom"),
|
||||
authConfig: z
|
||||
.object({
|
||||
name: z.string(),
|
||||
clientId: z.string(),
|
||||
clientSecret: z.string(),
|
||||
issuer: z.string(),
|
||||
scope: z.string().nullish(),
|
||||
allowDangerousEmailAccountLinking: z.boolean().optional().default(false),
|
||||
})
|
||||
.nullish(),
|
||||
});
|
||||
|
||||
export type GoogleProviderSchema = z.infer<typeof GoogleProviderSchema>;
|
||||
export type GithubProviderSchema = z.infer<typeof GithubProviderSchema>;
|
||||
export type Auth0ProviderSchema = z.infer<typeof Auth0ProviderSchema>;
|
||||
export type OktaProviderSchema = z.infer<typeof OktaProviderSchema>;
|
||||
export type AzureAdProviderSchema = z.infer<typeof AzureAdProviderSchema>;
|
||||
export type CognitoProviderSchema = z.infer<typeof CognitoProviderSchema>;
|
||||
export type CustomProviderSchema = z.infer<typeof CustomProviderSchema>;
|
||||
|
||||
export const SsoProviderSchema = z.discriminatedUnion("authProvider", [
|
||||
GoogleProviderSchema,
|
||||
@@ -90,6 +105,7 @@ export const SsoProviderSchema = z.discriminatedUnion("authProvider", [
|
||||
OktaProviderSchema,
|
||||
AzureAdProviderSchema,
|
||||
CognitoProviderSchema,
|
||||
CustomProviderSchema,
|
||||
]);
|
||||
|
||||
export type SsoProviderSchema = z.infer<typeof SsoProviderSchema>;
|
||||
|
||||
@@ -111,7 +111,24 @@ types:
|
||||
totalCost:
|
||||
docs: USD total cost, defaults to input+output
|
||||
type: optional<double>
|
||||
|
||||
ScoreConfig:
|
||||
docs: Configuration for a score
|
||||
properties:
|
||||
id: string
|
||||
name: string
|
||||
createdAt: datetime
|
||||
updatedAt: datetime
|
||||
projectId: string
|
||||
dataType: ScoreDataType
|
||||
isArchived: boolean
|
||||
minValue: optional<double>
|
||||
maxValue: optional<double>
|
||||
categories: optional<list<ConfigCategory>>
|
||||
description: optional<string>
|
||||
ConfigCategory:
|
||||
properties:
|
||||
value: double
|
||||
label: string
|
||||
Score:
|
||||
properties:
|
||||
id: string
|
||||
@@ -122,7 +139,7 @@ types:
|
||||
observationId: optional<string>
|
||||
timestamp: datetime
|
||||
comment: optional<string>
|
||||
DatasetCore:
|
||||
Dataset:
|
||||
properties:
|
||||
id: string
|
||||
name: string
|
||||
@@ -131,21 +148,6 @@ types:
|
||||
projectId: string
|
||||
createdAt: datetime
|
||||
updatedAt: datetime
|
||||
DatasetWithReferences: # GET datasets
|
||||
extends: DatasetCore
|
||||
properties:
|
||||
items:
|
||||
type: list<string>
|
||||
docs: list of dataset item ids
|
||||
runs:
|
||||
type: list<string>
|
||||
docs: list of dataset run names
|
||||
Dataset: # GET datasets/[name]
|
||||
docs: Dataset including all items
|
||||
extends: DatasetCore
|
||||
properties:
|
||||
items: list<DatasetItem>
|
||||
runs: list<string>
|
||||
DatasetItem:
|
||||
properties:
|
||||
id: string
|
||||
@@ -179,7 +181,43 @@ types:
|
||||
datasetName: string
|
||||
createdAt: datetime
|
||||
updatedAt: datetime
|
||||
DatasetRunWithItems:
|
||||
extends: DatasetRun
|
||||
properties:
|
||||
datasetRunItems: list<DatasetRunItem>
|
||||
Model:
|
||||
docs: Model definition used for transforming usage into USD cost and/or tokenization.
|
||||
properties:
|
||||
id: string
|
||||
modelName:
|
||||
docs: "Name of the model definition. If multiple with the same name exist, they are applied in the following order: (1) custom over built-in, (2) newest according to startTime where model.startTime<observation.startTime"
|
||||
type: string
|
||||
matchPattern:
|
||||
docs: "Regex pattern which matches this model definition to generation.model. Useful in case of fine-tuned models. If you want to exact match, use `(?i)^modelname$`"
|
||||
type: string
|
||||
startDate:
|
||||
docs: Apply only to generations which are newer than this ISO date.
|
||||
type: optional<date>
|
||||
unit:
|
||||
docs: Unit used by this model.
|
||||
type: ModelUsageUnit
|
||||
inputPrice:
|
||||
docs: Price (USD) per input unit
|
||||
type: optional<double>
|
||||
outputPrice:
|
||||
docs: Price (USD) per output unit
|
||||
type: optional<double>
|
||||
totalPrice:
|
||||
docs: Price (USD) per total unit. Cannot be set if input or output price is set.
|
||||
type: optional<double>
|
||||
tokenizerId:
|
||||
docs: Optional. Tokenizer to be applied to observations which match to this model. See docs for more details.
|
||||
type: optional<string>
|
||||
tokenizerConfig:
|
||||
docs: Optional. Configuration for the selected tokenizer. Needs to be JSON. See docs for more details.
|
||||
type: optional<unknown>
|
||||
isLangfuseManaged:
|
||||
type: boolean
|
||||
|
||||
# Utilities
|
||||
ModelUsageUnit:
|
||||
@@ -212,6 +250,11 @@ types:
|
||||
- ANNOTATION
|
||||
- API
|
||||
- EVAL
|
||||
ScoreDataType:
|
||||
enum:
|
||||
- NUMERIC
|
||||
- BOOLEAN
|
||||
- CATEGORICAL
|
||||
|
||||
errors:
|
||||
Error:
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
# yaml-language-server: $schema=https://raw.githubusercontent.com/fern-api/fern/main/fern.schema.json
|
||||
imports:
|
||||
commons: ./commons.yml
|
||||
pagination: ./utils/pagination.yml
|
||||
service:
|
||||
auth: true
|
||||
base-path: /api/public
|
||||
@@ -19,6 +20,23 @@ service:
|
||||
id:
|
||||
type: string
|
||||
response: commons.DatasetItem
|
||||
list:
|
||||
docs: Get dataset items
|
||||
method: GET
|
||||
path: /dataset-items
|
||||
request:
|
||||
name: GetDatasetItemsRequest
|
||||
query-parameters:
|
||||
datasetName: optional<string>
|
||||
sourceTraceId: optional<string>
|
||||
sourceObservationId: optional<string>
|
||||
page:
|
||||
type: optional<integer>
|
||||
docs: page number, starts at 1
|
||||
limit:
|
||||
type: optional<integer>
|
||||
docs: limit of items per page
|
||||
response: PaginatedDatasetItems
|
||||
|
||||
types:
|
||||
CreateDatasetItemRequest:
|
||||
@@ -31,7 +49,11 @@ types:
|
||||
sourceObservationId: optional<string>
|
||||
id:
|
||||
type: optional<string>
|
||||
docs: Dataset items are upserted on their id
|
||||
docs: Dataset items are upserted on their id. Id needs to be globally unique and cannot be reused across datasets.
|
||||
status:
|
||||
type: optional<commons.DatasetStatus>
|
||||
docs: Defaults to ACTIVE for newly created items
|
||||
PaginatedDatasetItems:
|
||||
properties:
|
||||
data: list<commons.DatasetItem>
|
||||
meta: pagination.MetaResponse
|
||||
|
||||
@@ -9,42 +9,66 @@ service:
|
||||
list:
|
||||
method: GET
|
||||
docs: Get all datasets
|
||||
path: /datasets
|
||||
path: /v2/datasets
|
||||
request:
|
||||
name: GetDatasetsRequest
|
||||
query-parameters:
|
||||
page: optional<integer>
|
||||
limit: optional<integer>
|
||||
page:
|
||||
type: optional<integer>
|
||||
docs: page number, starts at 1
|
||||
limit:
|
||||
type: optional<integer>
|
||||
docs: limit of items per page
|
||||
response: PaginatedDatasets
|
||||
get:
|
||||
method: GET
|
||||
docs: Get a dataset and its items
|
||||
path: /datasets/{datasetName}
|
||||
docs: Get a dataset
|
||||
path: /v2/datasets/{datasetName}
|
||||
path-parameters:
|
||||
datasetName: string
|
||||
response: commons.Dataset
|
||||
create:
|
||||
method: POST
|
||||
docs: Create a dataset
|
||||
path: /datasets
|
||||
path: /v2/datasets
|
||||
request: CreateDatasetRequest
|
||||
response: commons.Dataset
|
||||
getRuns:
|
||||
getRun:
|
||||
method: GET
|
||||
docs: Get a dataset run and its items
|
||||
path: /datasets/{datasetName}/runs/{runName}
|
||||
path-parameters:
|
||||
datasetName: string
|
||||
runName: string
|
||||
response: commons.DatasetRun
|
||||
response: commons.DatasetRunWithItems
|
||||
getRuns:
|
||||
method: GET
|
||||
docs: Get dataset runs
|
||||
path: /datasets/{datasetName}/runs
|
||||
path-parameters:
|
||||
datasetName: string
|
||||
request:
|
||||
name: GetDatasetRunsRequest
|
||||
query-parameters:
|
||||
page:
|
||||
type: optional<integer>
|
||||
docs: page number, starts at 1
|
||||
limit:
|
||||
type: optional<integer>
|
||||
docs: limit of items per page
|
||||
response: PaginatedDatasetRuns
|
||||
|
||||
types:
|
||||
PaginatedDatasets:
|
||||
properties:
|
||||
data: list<commons.DatasetWithReferences>
|
||||
data: list<commons.Dataset>
|
||||
meta: pagination.MetaResponse
|
||||
CreateDatasetRequest:
|
||||
properties:
|
||||
name: string
|
||||
description: optional<string>
|
||||
metadata: optional<unknown>
|
||||
PaginatedDatasetRuns:
|
||||
properties:
|
||||
data: list<commons.DatasetRun>
|
||||
meta: pagination.MetaResponse
|
||||
|
||||
@@ -7,28 +7,57 @@ service:
|
||||
base-path: /api/public
|
||||
endpoints:
|
||||
batch:
|
||||
docs: Batched ingestion for Langfuse Tracing
|
||||
docs: |
|
||||
Batched ingestion for Langfuse Tracing. If you want to use tracing via the API, such as to build your own Langfuse client implementation, this is the only API route you need to implement.
|
||||
|
||||
Notes:
|
||||
- Batch sizes are limited to 3.5 MB in total. You need to adjust the number of events per batch accordingly.
|
||||
- The API does not return a 4xx status code for input errors. Instead, it responds with a 207 status code, which includes a list of the encountered errors.
|
||||
method: POST
|
||||
path: /ingestion
|
||||
request:
|
||||
name: IngestionRequest
|
||||
body:
|
||||
properties:
|
||||
batch: list<IngestionEvent>
|
||||
response: IngestionResponse # will be reportet as 200 response, but endpoint returns 207
|
||||
batch:
|
||||
type: list<IngestionEvent>
|
||||
docs: "Batch of tracing events to be ingested. Discriminated by attribute `type`."
|
||||
metadata:
|
||||
type: optional<unknown>
|
||||
docs: Optional. Metadata field used by the Langfuse SDKs for debugging.
|
||||
response:
|
||||
type: IngestionResponse
|
||||
status-code: 207
|
||||
|
||||
types:
|
||||
IngestionEvent:
|
||||
discriminant: "type"
|
||||
union:
|
||||
trace-create: TraceEvent
|
||||
score-create: ScoreEvent
|
||||
event-create: CreateEventEvent
|
||||
generation-create: CreateGenerationEvent
|
||||
generation-update: UpdateGenerationEvent
|
||||
span-create: CreateSpanEvent
|
||||
span-update: UpdateSpanEvent
|
||||
sdk-log: SDKLogEvent
|
||||
trace-create:
|
||||
type: TraceEvent
|
||||
docs: Creates a new trace. Upserts on id for updates if trace with id exists.
|
||||
score-create:
|
||||
type: ScoreEvent
|
||||
docs: Creates a new score. Upserts on id for updates if score with id exists.
|
||||
span-create:
|
||||
type: CreateSpanEvent
|
||||
docs: Creates a new span.
|
||||
span-update:
|
||||
type: UpdateSpanEvent
|
||||
docs: Updates span based on id.
|
||||
generation-create:
|
||||
type: CreateGenerationEvent
|
||||
docs: Creates a new generation.
|
||||
generation-update:
|
||||
type: UpdateGenerationEvent
|
||||
docs: Updates a generation based on id.
|
||||
event-create:
|
||||
type: CreateEventEvent
|
||||
docs: Creates an event.
|
||||
|
||||
sdk-log:
|
||||
type: SDKLogEvent
|
||||
docs: Langfuse SDKs only, used for debugging purposes.
|
||||
|
||||
# both are legacy
|
||||
observation-create:
|
||||
@@ -162,9 +191,15 @@ types:
|
||||
|
||||
BaseEvent:
|
||||
properties:
|
||||
id: string
|
||||
timestamp: string
|
||||
metadata: unknown
|
||||
id:
|
||||
type: string
|
||||
docs: UUID v4 that identifies the event
|
||||
timestamp:
|
||||
type: string
|
||||
docs: "Datetime (ISO 8601) of event creation in client. Should be as close to actual event creation in client as possible, this timestamp will be used for ordering of events in future release. Resolution: milliseconds (required), microseconds (optimal)."
|
||||
metadata:
|
||||
type: optional<unknown>
|
||||
docs: Optional. Metadata field used by the Langfuse SDKs for debugging.
|
||||
|
||||
TraceEvent:
|
||||
extends: BaseEvent
|
||||
|
||||
@@ -48,12 +48,25 @@ types:
|
||||
date: date
|
||||
countTraces: integer
|
||||
countObservations: integer
|
||||
totalCost: double
|
||||
totalCost:
|
||||
type: double
|
||||
docs: Total model cost in USD
|
||||
usage: list<UsageByModel>
|
||||
UsageByModel:
|
||||
docs: Daily usage of a given model. Usage corresponds to the unit set for the specific model (e.g. tokens).
|
||||
properties:
|
||||
model: optional<string>
|
||||
inputUsage: integer
|
||||
outputUsage: integer
|
||||
totalUsage: integer
|
||||
inputUsage:
|
||||
type: integer
|
||||
docs: Total number of generation input units (e.g. tokens)
|
||||
outputUsage:
|
||||
type: integer
|
||||
docs: Total number of generation output units (e.g. tokens)
|
||||
totalUsage:
|
||||
type: integer
|
||||
docs: Total number of generation total units (e.g. tokens)
|
||||
countTraces: integer
|
||||
countObservations: integer
|
||||
totalCost:
|
||||
type: double
|
||||
docs: Total model cost in USD
|
||||
|
||||
@@ -0,0 +1,76 @@
|
||||
# yaml-language-server: $schema=https://raw.githubusercontent.com/fern-api/fern/main/fern.schema.json
|
||||
imports:
|
||||
commons: ./commons.yml
|
||||
pagination: ./utils/pagination.yml
|
||||
service:
|
||||
auth: true
|
||||
base-path: /api/public
|
||||
endpoints:
|
||||
create:
|
||||
method: POST
|
||||
docs: Create a model
|
||||
path: /models
|
||||
request: CreateModelRequest
|
||||
response: commons.Model
|
||||
list:
|
||||
method: GET
|
||||
docs: Get all models
|
||||
path: /models
|
||||
request:
|
||||
name: GetModelsRequest
|
||||
query-parameters:
|
||||
page:
|
||||
type: optional<integer>
|
||||
docs: page number, starts at 1
|
||||
limit:
|
||||
type: optional<integer>
|
||||
docs: limit of items per page
|
||||
response: PaginatedModels
|
||||
get:
|
||||
method: GET
|
||||
docs: Get a model
|
||||
path: /models/{id}
|
||||
path-parameters:
|
||||
id: string
|
||||
response: commons.Model
|
||||
delete:
|
||||
method: DELETE
|
||||
docs: Create a model. Cannot delete models managed by Langfuse. You can create your own definition with the same modelName to override the definition though.
|
||||
path: /models/{id}
|
||||
path-parameters:
|
||||
id: string
|
||||
|
||||
types:
|
||||
PaginatedModels:
|
||||
properties:
|
||||
data: list<commons.Model>
|
||||
meta: pagination.MetaResponse
|
||||
CreateModelRequest:
|
||||
properties:
|
||||
modelName:
|
||||
docs: "Name of the model definition. If multiple with the same name exist, they are applied in the following order: (1) custom over built-in, (2) newest according to startTime where model.startTime<observation.startTime"
|
||||
type: string
|
||||
matchPattern:
|
||||
docs: "Regex pattern which matches this model definition to generation.model. Useful in case of fine-tuned models. If you want to exact match, use `(?i)^modelname$`"
|
||||
type: string
|
||||
startDate:
|
||||
docs: Apply only to generations which are newer than this ISO date.
|
||||
type: optional<date>
|
||||
unit:
|
||||
docs: Unit used by this model.
|
||||
type: commons.ModelUsageUnit
|
||||
inputPrice:
|
||||
docs: Price (USD) per input unit
|
||||
type: optional<double>
|
||||
outputPrice:
|
||||
docs: Price (USD) per output unit
|
||||
type: optional<double>
|
||||
totalPrice:
|
||||
docs: Price (USD) per total units. Cannot be set if input or output price is set.
|
||||
type: optional<double>
|
||||
tokenizerId:
|
||||
docs: Optional. Tokenizer to be applied to observations which match to this model. See docs for more details.
|
||||
type: optional<string>
|
||||
tokenizerConfig:
|
||||
docs: Optional. Configuration for the selected tokenizer. Needs to be JSON. See docs for more details.
|
||||
type: optional<unknown>
|
||||
@@ -35,8 +35,12 @@ service:
|
||||
name: optional<string>
|
||||
label: optional<string>
|
||||
tag: optional<string>
|
||||
page: optional<integer>
|
||||
limit: optional<integer>
|
||||
page:
|
||||
type: optional<integer>
|
||||
docs: page number, starts at 1
|
||||
limit:
|
||||
type: optional<integer>
|
||||
docs: limit of items per page
|
||||
|
||||
response: PromptMetaListResponse
|
||||
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
# yaml-language-server: $schema=https://raw.githubusercontent.com/fern-api/fern/main/fern.schema.json
|
||||
imports:
|
||||
pagination: ./utils/pagination.yml
|
||||
commons: ./commons.yml
|
||||
service:
|
||||
auth: true
|
||||
base-path: /api/public
|
||||
endpoints:
|
||||
get:
|
||||
docs: Get all score configs
|
||||
method: GET
|
||||
path: /score-configs
|
||||
request:
|
||||
name: GetScoreConfigsRequest
|
||||
query-parameters:
|
||||
page:
|
||||
type: optional<integer>
|
||||
docs: Page number, starts at 1.
|
||||
limit:
|
||||
type: optional<integer>
|
||||
docs: Limit of items per page. If you encounter api issues due to too large page sizes, try to reduce the limit.
|
||||
response: ScoreConfigs
|
||||
get-by-id:
|
||||
docs: Get a score config
|
||||
method: GET
|
||||
path: /score-configs/{configId}
|
||||
path-parameters:
|
||||
configId:
|
||||
type: string
|
||||
docs: The unique langfuse identifier of a score config
|
||||
response: commons.ScoreConfig
|
||||
types:
|
||||
ScoreConfigs:
|
||||
properties:
|
||||
data: list<commons.ScoreConfig>
|
||||
meta: pagination.MetaResponse
|
||||
@@ -31,7 +31,7 @@ service:
|
||||
type: optional<datetime>
|
||||
docs: Retrieve only scores newer than this datetime (ISO 8601).
|
||||
source:
|
||||
type: commons.ScoreSource
|
||||
type: optional<commons.ScoreSource>
|
||||
docs: Retrieve only scores from a specific source.
|
||||
operator:
|
||||
type: optional<string>
|
||||
@@ -39,6 +39,9 @@ service:
|
||||
value:
|
||||
type: optional<double>
|
||||
docs: Retrieve only scores with <operator> value.
|
||||
scoreIds:
|
||||
type: optional<string>
|
||||
docs: Comma-separated list of score IDs to limit the results to.
|
||||
response: Scores
|
||||
get-by-id:
|
||||
docs: Get a score
|
||||
|
||||
@@ -7,7 +7,7 @@ service:
|
||||
base-path: /api/public
|
||||
endpoints:
|
||||
get:
|
||||
docs: Get a session
|
||||
docs: Get a session. Please note that `traces` on this endpoint are not paginated, if you plan to fetch large sessions, consider `GET /api/public/traces?sessionId=<sessionId>`
|
||||
method: GET
|
||||
path: /sessions/{sessionId}
|
||||
path-parameters:
|
||||
|
||||
@@ -30,6 +30,7 @@ service:
|
||||
docs: Limit of items per page. If you encounter api issues due to too large page sizes, try to reduce the limit.
|
||||
userId: optional<string>
|
||||
name: optional<string>
|
||||
sessionId: optional<string>
|
||||
fromTimestamp:
|
||||
type: optional<datetime>
|
||||
docs: Retrieve only traces newer than this datetime (ISO 8601).
|
||||
|
||||
@@ -1,4 +1,13 @@
|
||||
types:
|
||||
# to be used as copy/paste template for query strings
|
||||
# PaginationRequest:
|
||||
# properties:
|
||||
# page:
|
||||
# type: optional<integer>
|
||||
# docs: page number, starts at 1
|
||||
# limit:
|
||||
# type: optional<integer>
|
||||
# docs: limit of items per page
|
||||
MetaResponse:
|
||||
properties:
|
||||
page:
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
{
|
||||
"organization": "finto",
|
||||
"version": "0.21.0"
|
||||
"version": "0.30.3"
|
||||
}
|
||||
+15
-5
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "langfuse",
|
||||
"version": "2.47.4",
|
||||
"version": "2.59.0",
|
||||
"author": "engineering@langfuse.com",
|
||||
"license": "MIT",
|
||||
"private": true,
|
||||
@@ -17,26 +17,36 @@
|
||||
"db:seed": "turbo run db:seed",
|
||||
"db:seed:examples": "turbo run db:seed:examples",
|
||||
"nuke": "bash ./scripts/nuke.sh",
|
||||
"dx": "pnpm i && pnpm --filter=shared run db:reset && pnpm --filter=shared run db:seed:examples && pnpm run dev",
|
||||
"dx": "pnpm i && pnpm run infra:dev:up && pnpm --filter=shared run db:reset && pnpm --filter=shared run db:seed:examples && pnpm run dev",
|
||||
"dx-f": "pnpm i && pnpm run infra:dev:up && pnpm --filter=shared run db:reset -f && pnpm --filter=shared run db:seed:examples && pnpm run dev",
|
||||
"build": "turbo run build",
|
||||
"start": "turbo run start",
|
||||
"dev": "turbo run dev",
|
||||
"lint": "turbo run lint",
|
||||
"test": "turbo run test",
|
||||
"models:migrate": "turbo run models:migrate",
|
||||
"release": "dotenv -e ../.env -- release-it"
|
||||
"release": "dotenv -e ../.env -- release-it",
|
||||
"prepare": "husky"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@release-it/bumper": "^6.0.1",
|
||||
"braces": "3.0.3",
|
||||
"dotenv-cli": "^7.4.2",
|
||||
"husky": "^9.0.11",
|
||||
"prettier": "^3.2.5",
|
||||
"release-it": "^17.2.1",
|
||||
"release-it": "^17.3.0",
|
||||
"turbo": "^1.13.3"
|
||||
},
|
||||
"release-it": {
|
||||
"git": {
|
||||
"commitMessage": "chore: release v${version}",
|
||||
"tagName": "v${version}"
|
||||
"tagName": "v${version}",
|
||||
"commitArgs": [
|
||||
"--no-verify"
|
||||
],
|
||||
"pushArgs": [
|
||||
"--no-verify"
|
||||
]
|
||||
},
|
||||
"plugins": {
|
||||
"@release-it/bumper": {
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
],
|
||||
"devDependencies": {
|
||||
"@typescript-eslint/eslint-plugin": "^7.1.0",
|
||||
"@typescript-eslint/parser": "^7.7.0",
|
||||
"@typescript-eslint/parser": "^7.12.0",
|
||||
"@vercel/style-guide": "^6.0.0",
|
||||
"eslint-config-next": "^14.2.3",
|
||||
"eslint-config-prettier": "^9.1.0",
|
||||
|
||||
@@ -17,9 +17,9 @@
|
||||
"import": "./dist/src/db.js",
|
||||
"require": "./dist/src/db.js"
|
||||
},
|
||||
"./src/server/auth": {
|
||||
"import": "./dist/src/server/auth.js",
|
||||
"require": "./dist/src/server/auth.js"
|
||||
"./src/server": {
|
||||
"import": "./dist/src/server/index.js",
|
||||
"require": "./dist/src/server/index.js"
|
||||
},
|
||||
"./encryption": {
|
||||
"import": "./dist/src/encryption/index.js",
|
||||
@@ -44,14 +44,22 @@
|
||||
"seed": "ts-node -r tsconfig-paths/register -r dotenv/config --compiler-options {\"module\":\"CommonJS\"} prisma/seed.ts"
|
||||
},
|
||||
"dependencies": {
|
||||
"@aws-sdk/client-s3": "^3.550.0",
|
||||
"@aws-sdk/lib-storage": "^3.568.0",
|
||||
"@aws-sdk/s3-request-presigner": "^3.554.0",
|
||||
"@langchain/anthropic": "^0.1.21",
|
||||
"@langchain/core": "^0.1.61",
|
||||
"@langchain/openai": "^0.0.33",
|
||||
"@prisma/client": "^5.13.0",
|
||||
"@react-email/components": "^0.0.19",
|
||||
"@react-email/render": "^0.0.15",
|
||||
"@types/bcryptjs": "^2.4.6",
|
||||
"bcryptjs": "^2.4.3",
|
||||
"kysely": "^0.27.3",
|
||||
"langchain": "^0.1.36",
|
||||
"langchain": "^0.2.6",
|
||||
"lodash": "^4.17.21",
|
||||
"next-auth": "^4.24.7",
|
||||
"nodemailer": "^6.9.13",
|
||||
"prisma-extension-kysely": "^2.1.0",
|
||||
"zod": "^3.22.4",
|
||||
"zod-to-json-schema": "^3.22.5"
|
||||
@@ -59,17 +67,18 @@
|
||||
"devDependencies": {
|
||||
"@repo/eslint-config": "*",
|
||||
"@repo/typescript-config": "*",
|
||||
"@types/lodash": "^4.17.4",
|
||||
"@types/lodash": "^4.17.5",
|
||||
"@types/node": "^20.11.29",
|
||||
"@types/nodemailer": "^6.4.15",
|
||||
"@types/pg": "^8.11.6",
|
||||
"@types/uuid": "^9.0.8",
|
||||
"@typescript-eslint/parser": "^7.7.0",
|
||||
"@typescript-eslint/parser": "^7.12.0",
|
||||
"eslint": "^8.57.0",
|
||||
"eslint-config-prettier": "^9.1.0",
|
||||
"eslint-config-standard": "^17.1.0",
|
||||
"eslint-plugin-prettier": "^5.1.3",
|
||||
"kysely-codegen": "^0.11.0",
|
||||
"nodemon": "^3.0.3",
|
||||
"nodemon": "^3.1.3",
|
||||
"prettier": "^3.2.5",
|
||||
"prisma": "^5.13.0",
|
||||
"prisma-erd-generator": "^1.11.2",
|
||||
@@ -78,5 +87,9 @@
|
||||
"tsc-watch": "^6.2.0",
|
||||
"typescript": "^5.4.5",
|
||||
"vitest": "^1.5.3"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/react": "^18.2.79",
|
||||
"react": "^18.0.0"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -32,20 +32,10 @@ export const ScoreSource = {
|
||||
export type ScoreSource = (typeof ScoreSource)[keyof typeof ScoreSource];
|
||||
export const ScoreDataType = {
|
||||
CATEGORICAL: "CATEGORICAL",
|
||||
NUMERIC: "NUMERIC"
|
||||
NUMERIC: "NUMERIC",
|
||||
BOOLEAN: "BOOLEAN"
|
||||
} as const;
|
||||
export type ScoreDataType = (typeof ScoreDataType)[keyof typeof ScoreDataType];
|
||||
export const PricingUnit = {
|
||||
PER_1000_TOKENS: "PER_1000_TOKENS",
|
||||
PER_1000_CHARS: "PER_1000_CHARS"
|
||||
} as const;
|
||||
export type PricingUnit = (typeof PricingUnit)[keyof typeof PricingUnit];
|
||||
export const TokenType = {
|
||||
PROMPT: "PROMPT",
|
||||
COMPLETION: "COMPLETION",
|
||||
TOTAL: "TOTAL"
|
||||
} as const;
|
||||
export type TokenType = (typeof TokenType)[keyof typeof TokenType];
|
||||
export const DatasetStatus = {
|
||||
ACTIVE: "ACTIVE",
|
||||
ARCHIVED: "ARCHIVED"
|
||||
@@ -108,6 +98,21 @@ export type AuditLog = {
|
||||
before: string | null;
|
||||
after: string | null;
|
||||
};
|
||||
export type BatchExport = {
|
||||
id: string;
|
||||
created_at: Generated<Timestamp>;
|
||||
updated_at: Generated<Timestamp>;
|
||||
project_id: string;
|
||||
user_id: string;
|
||||
finished_at: Timestamp | null;
|
||||
expires_at: Timestamp | null;
|
||||
name: string;
|
||||
status: string;
|
||||
query: unknown;
|
||||
format: string;
|
||||
url: string | null;
|
||||
log: string | null;
|
||||
};
|
||||
export type CronJobs = {
|
||||
name: string;
|
||||
last_run: Timestamp | null;
|
||||
@@ -162,6 +167,7 @@ export type EvalTemplate = {
|
||||
version: number;
|
||||
prompt: string;
|
||||
model: string;
|
||||
provider: string;
|
||||
model_params: unknown;
|
||||
vars: Generated<string[]>;
|
||||
output_schema: unknown;
|
||||
@@ -209,8 +215,12 @@ export type LlmApiKeys = {
|
||||
created_at: Generated<Timestamp>;
|
||||
updated_at: Generated<Timestamp>;
|
||||
provider: string;
|
||||
adapter: string;
|
||||
display_secret_key: string;
|
||||
secret_key: string;
|
||||
base_url: string | null;
|
||||
custom_models: Generated<string[]>;
|
||||
with_default_models: Generated<boolean>;
|
||||
project_id: string;
|
||||
};
|
||||
export type MembershipInvitation = {
|
||||
@@ -254,6 +264,7 @@ export type Observation = {
|
||||
updated_at: Generated<Timestamp>;
|
||||
model: string | null;
|
||||
internal_model: string | null;
|
||||
internal_model_id: string | null;
|
||||
modelParameters: unknown | null;
|
||||
input: unknown | null;
|
||||
output: unknown | null;
|
||||
@@ -264,6 +275,9 @@ export type Observation = {
|
||||
input_cost: string | null;
|
||||
output_cost: string | null;
|
||||
total_cost: string | null;
|
||||
calculated_input_cost: string | null;
|
||||
calculated_output_cost: string | null;
|
||||
calculated_total_cost: string | null;
|
||||
completion_start_time: Timestamp | null;
|
||||
prompt_id: string | null;
|
||||
};
|
||||
@@ -309,14 +323,6 @@ export type PosthogIntegration = {
|
||||
enabled: boolean;
|
||||
created_at: Generated<Timestamp>;
|
||||
};
|
||||
export type Pricing = {
|
||||
id: string;
|
||||
model_name: string;
|
||||
pricing_unit: Generated<PricingUnit>;
|
||||
price: string;
|
||||
currency: Generated<string>;
|
||||
token_type: TokenType;
|
||||
};
|
||||
export type Project = {
|
||||
id: string;
|
||||
created_at: Generated<Timestamp>;
|
||||
@@ -455,6 +461,7 @@ export type DB = {
|
||||
Account: Account;
|
||||
api_keys: ApiKey;
|
||||
audit_logs: AuditLog;
|
||||
batch_exports: BatchExport;
|
||||
cron_jobs: CronJobs;
|
||||
dataset_items: DatasetItem;
|
||||
dataset_run_items: DatasetRunItems;
|
||||
@@ -470,7 +477,6 @@ export type DB = {
|
||||
observations: Observation;
|
||||
observations_view: ObservationView;
|
||||
posthog_integrations: PosthogIntegration;
|
||||
pricings: Pricing;
|
||||
project_memberships: ProjectMembership;
|
||||
projects: Project;
|
||||
prompts: Prompt;
|
||||
|
||||
-7
@@ -1,7 +0,0 @@
|
||||
-- Google Vertex uses @ to separate model name and version
|
||||
|
||||
UPDATE "public"."models" SET "match_pattern" = '(?i)^(claude-3-haiku(-|@)?20240307)$' WHERE "id" = 'cltr0w45b000008k1407o9qv1';
|
||||
|
||||
UPDATE "public"."models" SET "match_pattern" = '(?i)^(claude-3-opus(-|@)?20240229)$' WHERE "id" = 'cltgy0iuw000008le3vod1hhy';
|
||||
|
||||
UPDATE "public"."models" SET "match_pattern" = '(?i)^(claude-3-sonnet(-|@)?20240229)$' WHERE "id" = 'cltgy0pp6000108le56se7bl3';
|
||||
+2
@@ -0,0 +1,2 @@
|
||||
-- CreateIndex
|
||||
CREATE INDEX CONCURRENTLY "dataset_items_source_trace_id_idx" ON "dataset_items" USING HASH ("source_trace_id");
|
||||
@@ -0,0 +1,2 @@
|
||||
-- CreateIndex
|
||||
CREATE INDEX "scores_project_id_name_idx" ON "scores"("project_id", "name");
|
||||
+2
@@ -0,0 +1,2 @@
|
||||
-- AlterEnum
|
||||
ALTER TYPE "ScoreDataType" ADD VALUE 'BOOLEAN';
|
||||
+14
@@ -0,0 +1,14 @@
|
||||
/*
|
||||
Warnings:
|
||||
|
||||
- You are about to drop the `pricings` table. If the table is not empty, all the data it contains will be lost.
|
||||
|
||||
*/
|
||||
-- DropTable
|
||||
DROP TABLE "pricings";
|
||||
|
||||
-- DropEnum
|
||||
DROP TYPE "PricingUnit";
|
||||
|
||||
-- DropEnum
|
||||
DROP TYPE "TokenType";
|
||||
+3
@@ -0,0 +1,3 @@
|
||||
-- DropForeignKey
|
||||
ALTER TABLE "dataset_run_items" DROP CONSTRAINT "dataset_run_items_trace_id_fkey";
|
||||
ALTER TABLE "dataset_run_items" DROP CONSTRAINT "dataset_run_items_observation_id_fkey";
|
||||
+8
@@ -0,0 +1,8 @@
|
||||
-- does not include pricing yet, will be added as soon as it is calculated at ingestion time
|
||||
|
||||
-- remove model if added by faulty migration, context: https://github.com/langfuse/langfuse/issues/2266
|
||||
DELETE FROM models WHERE id in ('clx30djsn0000w9mzebiv41we', 'clx30hkrx0000w9mz7lqi0ial');
|
||||
|
||||
INSERT INTO "models" ("id", "model_name", "match_pattern", "unit") VALUES ('clx30djsn0000w9mzebiv41we', 'gemini-1.5-flash', '(?i)^(gemini-1.5-flash)(@[a-zA-Z0-9]+)?$', 'CHARACTERS');
|
||||
|
||||
INSERT INTO "models" ("id", "model_name", "match_pattern", "unit") VALUES ('clx30hkrx0000w9mz7lqi0ial', 'gemini-1.5-pro', '(?i)^(gemini-1.5-pro)(@[a-zA-Z0-9]+)?$', 'CHARACTERS');
|
||||
+7
@@ -0,0 +1,7 @@
|
||||
-- Google Vertex uses @ to separate model name and version
|
||||
|
||||
UPDATE "models" SET "match_pattern" = '(?i)^(claude-3-haiku(-|@)?20240307)$' WHERE "id" = 'cltr0w45b000008k1407o9qv1';
|
||||
|
||||
UPDATE "models" SET "match_pattern" = '(?i)^(claude-3-opus(-|@)?20240229)$' WHERE "id" = 'cltgy0iuw000008le3vod1hhy';
|
||||
|
||||
UPDATE "models" SET "match_pattern" = '(?i)^(claude-3-sonnet(-|@)?20240229)$' WHERE "id" = 'cltgy0pp6000108le56se7bl3';
|
||||
+24
@@ -0,0 +1,24 @@
|
||||
BEGIN;
|
||||
|
||||
ALTER TABLE "llm_api_keys"
|
||||
ADD COLUMN "base_url" TEXT,
|
||||
ADD COLUMN "adapter" TEXT,
|
||||
ADD COLUMN "custom_models" TEXT[] NOT NULL DEFAULT '{}'::TEXT[],
|
||||
ADD COLUMN "with_default_models" BOOLEAN NOT NULL DEFAULT true;
|
||||
|
||||
UPDATE "llm_api_keys"
|
||||
SET "adapter" = "provider";
|
||||
|
||||
ALTER TABLE "llm_api_keys"
|
||||
ALTER COLUMN "adapter" SET NOT NULL;
|
||||
|
||||
ALTER TABLE "eval_templates"
|
||||
ADD COLUMN "provider" TEXT;
|
||||
|
||||
UPDATE "eval_templates"
|
||||
SET "provider" = 'openai';
|
||||
|
||||
ALTER TABLE "eval_templates"
|
||||
ALTER COLUMN "provider" SET NOT NULL;
|
||||
|
||||
COMMIT;
|
||||
+51
@@ -0,0 +1,51 @@
|
||||
-- Create manual-score config for any project with manual scores and link config_id to scores
|
||||
BEGIN;
|
||||
WITH project_configs AS (
|
||||
INSERT INTO score_configs (id,
|
||||
project_id,
|
||||
name,
|
||||
data_type,
|
||||
is_archived,
|
||||
min_value,
|
||||
max_value,
|
||||
description)
|
||||
SELECT
|
||||
md5(random()::text || clock_timestamp()::text || s.project_id::text)::uuid AS id,
|
||||
s.project_id,
|
||||
'manual-score',
|
||||
'NUMERIC',
|
||||
FALSE,
|
||||
- 1,
|
||||
1,
|
||||
'Langfuse legacy annotation score.'
|
||||
FROM ( SELECT DISTINCT
|
||||
project_id
|
||||
FROM
|
||||
scores
|
||||
WHERE
|
||||
name = 'manual-score'
|
||||
AND config_id IS NULL
|
||||
AND source = 'ANNOTATION') s
|
||||
WHERE
|
||||
NOT EXISTS (
|
||||
SELECT
|
||||
1
|
||||
FROM
|
||||
score_configs sc
|
||||
WHERE
|
||||
sc.name = 'manual-score'
|
||||
AND sc.project_id = s.project_id)
|
||||
RETURNING
|
||||
id,
|
||||
project_id
|
||||
)
|
||||
UPDATE
|
||||
scores
|
||||
SET
|
||||
config_id = pc.id
|
||||
FROM
|
||||
project_configs pc
|
||||
WHERE
|
||||
scores.project_id = pc.project_id
|
||||
AND scores.name = 'manual-score';
|
||||
COMMIT;
|
||||
+2
@@ -0,0 +1,2 @@
|
||||
-- CreateIndex
|
||||
CREATE INDEX CONCURRENTLY "observations_project_id_prompt_id_idx" ON "observations"("project_id", "prompt_id");
|
||||
+2
@@ -0,0 +1,2 @@
|
||||
-- DropForeignKey
|
||||
ALTER TABLE "observations" DROP CONSTRAINT "observations_prompt_id_fkey";
|
||||
+27
@@ -0,0 +1,27 @@
|
||||
-- CreateTable
|
||||
CREATE TABLE "batch_exports" (
|
||||
"id" TEXT NOT NULL,
|
||||
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updated_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"project_id" TEXT NOT NULL,
|
||||
"user_id" TEXT NOT NULL,
|
||||
"finished_at" TIMESTAMP(3),
|
||||
"expires_at" TIMESTAMP(3),
|
||||
"name" TEXT NOT NULL,
|
||||
"status" TEXT NOT NULL,
|
||||
"query" JSONB NOT NULL,
|
||||
"format" TEXT NOT NULL,
|
||||
"url" TEXT,
|
||||
"log" TEXT,
|
||||
|
||||
CONSTRAINT "batch_exports_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "batch_exports_project_id_user_id_idx" ON "batch_exports"("project_id", "user_id");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "batch_exports_status_idx" ON "batch_exports"("status");
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "batch_exports" ADD CONSTRAINT "batch_exports_project_id_fkey" FOREIGN KEY ("project_id") REFERENCES "projects"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
+2
@@ -0,0 +1,2 @@
|
||||
-- DropIndex
|
||||
DROP INDEX CONCURRENTLY IF EXISTS "observations_parent_observation_id_idx";
|
||||
+2
@@ -0,0 +1,2 @@
|
||||
-- DropIndex
|
||||
DROP INDEX CONCURRENTLY IF EXISTS "observations_updated_at_idx";
|
||||
+2
@@ -0,0 +1,2 @@
|
||||
-- DropIndex
|
||||
DROP INDEX CONCURRENTLY IF EXISTS "scores_updated_at_idx";
|
||||
+2
@@ -0,0 +1,2 @@
|
||||
-- DropIndex
|
||||
DROP INDEX CONCURRENTLY IF EXISTS "traces_external_id_idx";
|
||||
@@ -0,0 +1,2 @@
|
||||
-- DropIndex
|
||||
DROP INDEX CONCURRENTLY IF EXISTS "traces_release_idx";
|
||||
+2
@@ -0,0 +1,2 @@
|
||||
-- DropIndex
|
||||
DROP INDEX CONCURRENTLY IF EXISTS "traces_updated_at_idx";
|
||||
+2
@@ -0,0 +1,2 @@
|
||||
-- CreateIndex
|
||||
CREATE INDEX CONCURRENTLY "traces_project_id_timestamp_idx" ON "traces"("project_id", "timestamp");
|
||||
+15
@@ -0,0 +1,15 @@
|
||||
INSERT INTO models (
|
||||
id,
|
||||
project_id,
|
||||
model_name,
|
||||
match_pattern,
|
||||
start_date,
|
||||
input_price,
|
||||
output_price,
|
||||
total_price,
|
||||
unit,
|
||||
tokenizer_id
|
||||
)
|
||||
VALUES
|
||||
-- add 3.5 sonnet model
|
||||
('clxt0n0m60000pumz1j5b7zsf', NULL, 'claude-3-5-sonnet-20240620', '(?i)^(claude-3-5-sonnet(-|@)?20240620)$', NULL, 0.000003, 0.000015, NULL, 'TOKENS', 'claude')
|
||||
+5
@@ -0,0 +1,5 @@
|
||||
-- AlterTable
|
||||
ALTER TABLE "observations" ADD COLUMN "calculated_input_cost" DECIMAL(65,30),
|
||||
ADD COLUMN "calculated_output_cost" DECIMAL(65,30),
|
||||
ADD COLUMN "calculated_total_cost" DECIMAL(65,30),
|
||||
ADD COLUMN "internal_model_id" TEXT;
|
||||
+17
@@ -0,0 +1,17 @@
|
||||
/*
|
||||
Previously, the pattern did not allow for model specifications like "1106" or any other 4-digit block.
|
||||
This led to missing models on the generation-update call where the exact model name that was used was provided (including the 4-digit block).
|
||||
The new pattern allows for:
|
||||
|
||||
- "gpt-4-vision-preview" as set on generation-create
|
||||
- "gpt-4-1106-vision-preview" as set on generation-update
|
||||
|
||||
*/
|
||||
|
||||
UPDATE
|
||||
"models"
|
||||
SET
|
||||
"match_pattern" = '(?i)^(gpt-4(-\d{4})?-vision-preview)$'
|
||||
WHERE
|
||||
"id" = 'clrkvx5gp000108juaogs54ea'
|
||||
AND "model_name" = 'gpt-4-turbo-vision';
|
||||
@@ -115,6 +115,7 @@ model Project {
|
||||
PosthogIntegration PosthogIntegration[]
|
||||
Score Score[]
|
||||
scoreConfig ScoreConfig[]
|
||||
BatchExport BatchExport[]
|
||||
|
||||
@@map("projects")
|
||||
}
|
||||
@@ -144,9 +145,13 @@ model LlmApiKeys {
|
||||
createdAt DateTime @default(now()) @map("created_at")
|
||||
updatedAt DateTime @default(now()) @updatedAt @map("updated_at")
|
||||
|
||||
provider String
|
||||
displaySecretKey String @map("display_secret_key")
|
||||
secretKey String @map("secret_key")
|
||||
provider String
|
||||
adapter String // This controls the interface that is used to connect with the LLM, e.g. 'openai' or 'anthropic'
|
||||
displaySecretKey String @map("display_secret_key")
|
||||
secretKey String @map("secret_key")
|
||||
baseURL String? @map("base_url")
|
||||
customModels String[] @default([]) @map("custom_models")
|
||||
withDefaultModels Boolean @default(true) @map("with_default_models")
|
||||
|
||||
projectId String @map("project_id")
|
||||
project Project @relation(fields: [projectId], references: [id], onDelete: Cascade)
|
||||
@@ -233,19 +238,16 @@ model Trace {
|
||||
createdAt DateTime @default(now()) @map("created_at")
|
||||
updatedAt DateTime @default(now()) @updatedAt @map("updated_at")
|
||||
|
||||
DatasetRunItems DatasetRunItems[]
|
||||
DatasetItem DatasetItem[]
|
||||
JobExecution JobExecution[]
|
||||
DatasetItem DatasetItem[]
|
||||
JobExecution JobExecution[]
|
||||
|
||||
@@index([projectId])
|
||||
@@index([projectId, timestamp])
|
||||
@@index([sessionId])
|
||||
@@index([name])
|
||||
@@index([userId])
|
||||
@@index([id, userId])
|
||||
@@index([externalId])
|
||||
@@index(timestamp)
|
||||
@@index(release)
|
||||
@@index(updatedAt)
|
||||
@@index(createdAt)
|
||||
@@index([tags(ops: ArrayOps)], type: Gin)
|
||||
@@map("traces")
|
||||
@@ -297,25 +299,33 @@ model Observation {
|
||||
updatedAt DateTime @default(now()) @updatedAt @map("updated_at")
|
||||
|
||||
// GENERATION ONLY
|
||||
model String?
|
||||
internalModel String? @map("internal_model")
|
||||
modelParameters Json?
|
||||
input Json?
|
||||
output Json?
|
||||
promptTokens Int @default(0) @map("prompt_tokens")
|
||||
completionTokens Int @default(0) @map("completion_tokens")
|
||||
totalTokens Int @default(0) @map("total_tokens")
|
||||
unit String?
|
||||
inputCost Decimal? @map("input_cost")
|
||||
outputCost Decimal? @map("output_cost")
|
||||
totalCost Decimal? @map("total_cost")
|
||||
completionStartTime DateTime? @map("completion_start_time")
|
||||
project Project @relation(fields: [projectId], references: [id], onDelete: Cascade)
|
||||
derivedDatasetItems DatasetItem[]
|
||||
datasetRunItems DatasetRunItems[]
|
||||
model String?
|
||||
internalModel String? @map("internal_model")
|
||||
internalModelId String? @map("internal_model_id")
|
||||
|
||||
promptId String? @map("prompt_id")
|
||||
prompt Prompt? @relation(fields: [promptId], onDelete: SetNull, references: [id])
|
||||
modelParameters Json?
|
||||
input Json?
|
||||
output Json?
|
||||
promptTokens Int @default(0) @map("prompt_tokens")
|
||||
completionTokens Int @default(0) @map("completion_tokens")
|
||||
totalTokens Int @default(0) @map("total_tokens")
|
||||
unit String?
|
||||
|
||||
// User provided cost at ingestion
|
||||
inputCost Decimal? @map("input_cost")
|
||||
outputCost Decimal? @map("output_cost")
|
||||
totalCost Decimal? @map("total_cost")
|
||||
|
||||
// Calculated cost
|
||||
calculatedInputCost Decimal? @map("calculated_input_cost")
|
||||
calculatedOutputCost Decimal? @map("calculated_output_cost")
|
||||
calculatedTotalCost Decimal? @map("calculated_total_cost")
|
||||
|
||||
completionStartTime DateTime? @map("completion_start_time")
|
||||
project Project @relation(fields: [projectId], references: [id], onDelete: Cascade)
|
||||
derivedDatasetItems DatasetItem[]
|
||||
|
||||
promptId String? @map("prompt_id") // no fk constraint, prompt can be deleted
|
||||
|
||||
@@unique([id, projectId])
|
||||
@@index([projectId, internalModel, startTime, unit])
|
||||
@@ -326,11 +336,10 @@ model Observation {
|
||||
@@index([type])
|
||||
@@index(startTime)
|
||||
@@index(createdAt)
|
||||
@@index(updatedAt)
|
||||
@@index(projectId)
|
||||
@@index(parentObservationId)
|
||||
@@index(model)
|
||||
@@index(internalModel)
|
||||
@@index([projectId, promptId])
|
||||
@@index(promptId)
|
||||
@@index([projectId, startTime, type])
|
||||
@@map("observations")
|
||||
@@ -419,13 +428,13 @@ model Score {
|
||||
@@index(timestamp)
|
||||
@@index([value])
|
||||
@@index([projectId])
|
||||
@@index([projectId, name])
|
||||
@@index([authorUserId])
|
||||
@@index([configId])
|
||||
@@index([traceId], type: Hash)
|
||||
@@index([observationId], type: Hash)
|
||||
@@index([source])
|
||||
@@index([createdAt])
|
||||
@@index([updatedAt])
|
||||
@@map("scores")
|
||||
}
|
||||
|
||||
@@ -463,29 +472,7 @@ model ScoreConfig {
|
||||
enum ScoreDataType {
|
||||
CATEGORICAL
|
||||
NUMERIC
|
||||
}
|
||||
|
||||
enum PricingUnit {
|
||||
PER_1000_TOKENS
|
||||
PER_1000_CHARS
|
||||
}
|
||||
|
||||
enum TokenType {
|
||||
PROMPT
|
||||
COMPLETION
|
||||
TOTAL
|
||||
}
|
||||
|
||||
model Pricing {
|
||||
id String @id @default(cuid())
|
||||
modelName String @map("model_name")
|
||||
pricingUnit PricingUnit @default(PER_1000_TOKENS) @map("pricing_unit")
|
||||
price Decimal
|
||||
currency String @default("USD")
|
||||
tokenType TokenType @map("token_type")
|
||||
|
||||
@@index(modelName)
|
||||
@@map("pricings")
|
||||
BOOLEAN
|
||||
}
|
||||
|
||||
model CronJobs {
|
||||
@@ -532,6 +519,7 @@ model DatasetItem {
|
||||
updatedAt DateTime @default(now()) @updatedAt @map("updated_at")
|
||||
datasetRunItems DatasetRunItems[]
|
||||
|
||||
@@index([sourceTraceId], type: Hash)
|
||||
@@index([sourceObservationId], type: Hash)
|
||||
@@index([datasetId], type: Hash)
|
||||
@@index([createdAt])
|
||||
@@ -563,17 +551,15 @@ model DatasetRuns {
|
||||
}
|
||||
|
||||
model DatasetRunItems {
|
||||
id String @id @default(cuid())
|
||||
datasetRunId String @map("dataset_run_id")
|
||||
datasetRun DatasetRuns @relation(fields: [datasetRunId], references: [id], onDelete: Cascade)
|
||||
datasetItemId String @map("dataset_item_id")
|
||||
datasetItem DatasetItem @relation(fields: [datasetItemId], references: [id], onDelete: Cascade)
|
||||
traceId String @map("trace_id")
|
||||
trace Trace @relation(fields: [traceId], references: [id], onDelete: Cascade)
|
||||
observationId String? @map("observation_id")
|
||||
observation Observation? @relation(fields: [observationId], references: [id], onDelete: Cascade)
|
||||
createdAt DateTime @default(now()) @map("created_at")
|
||||
updatedAt DateTime @default(now()) @updatedAt @map("updated_at")
|
||||
id String @id @default(cuid())
|
||||
datasetRunId String @map("dataset_run_id")
|
||||
datasetRun DatasetRuns @relation(fields: [datasetRunId], references: [id], onDelete: Cascade)
|
||||
datasetItemId String @map("dataset_item_id")
|
||||
datasetItem DatasetItem @relation(fields: [datasetItemId], references: [id], onDelete: Cascade)
|
||||
traceId String @map("trace_id")
|
||||
observationId String? @map("observation_id")
|
||||
createdAt DateTime @default(now()) @map("created_at")
|
||||
updatedAt DateTime @default(now()) @updatedAt @map("updated_at")
|
||||
|
||||
@@index([datasetRunId], type: Hash)
|
||||
@@index([datasetItemId], type: Hash)
|
||||
@@ -609,15 +595,14 @@ model Prompt {
|
||||
|
||||
createdBy String @map("created_by")
|
||||
|
||||
prompt Json
|
||||
name String
|
||||
version Int
|
||||
type String @default("text")
|
||||
isActive Boolean? @map("is_active") // Deprecated. To be removed once 'production' labels work as expected.
|
||||
config Json @default("{}")
|
||||
tags String[] @default([])
|
||||
labels String[] @default([])
|
||||
Observation Observation[]
|
||||
prompt Json
|
||||
name String
|
||||
version Int
|
||||
type String @default("text")
|
||||
isActive Boolean? @map("is_active") // Deprecated. To be removed once 'production' labels work as expected.
|
||||
config Json @default("{}")
|
||||
tags String[] @default([])
|
||||
labels String[] @default([])
|
||||
|
||||
@@unique([projectId, name, version])
|
||||
@@index([projectId, name, version])
|
||||
@@ -688,6 +673,7 @@ model EvalTemplate {
|
||||
version Int
|
||||
prompt String
|
||||
model String
|
||||
provider String
|
||||
modelParams Json @map("model_params")
|
||||
vars String[] @default([])
|
||||
outputSchema Json @map("output_schema")
|
||||
@@ -801,3 +787,27 @@ model PosthogIntegration {
|
||||
@@index([projectId])
|
||||
@@map("posthog_integrations")
|
||||
}
|
||||
|
||||
model BatchExport {
|
||||
id String @id @default(cuid())
|
||||
createdAt DateTime @default(now()) @map("created_at")
|
||||
updatedAt DateTime @default(now()) @updatedAt @map("updated_at")
|
||||
|
||||
projectId String @map("project_id")
|
||||
project Project @relation(fields: [projectId], references: [id], onDelete: Cascade)
|
||||
userId String @map("user_id")
|
||||
|
||||
finishedAt DateTime? @map("finished_at")
|
||||
expiresAt DateTime? @map("expires_at")
|
||||
|
||||
name String
|
||||
status String
|
||||
query Json
|
||||
format String
|
||||
url String?
|
||||
log String?
|
||||
|
||||
@@index([projectId, userId])
|
||||
@@index([status])
|
||||
@@map("batch_exports")
|
||||
}
|
||||
|
||||
+126
-29
@@ -4,6 +4,7 @@ import {
|
||||
type Prisma,
|
||||
ObservationType,
|
||||
ScoreSource,
|
||||
ScoreDataType,
|
||||
} from "../src/index";
|
||||
import { hash } from "bcryptjs";
|
||||
import { parseArgs } from "node:util";
|
||||
@@ -11,7 +12,7 @@ import { parseArgs } from "node:util";
|
||||
import { chunk } from "lodash";
|
||||
import { v4 } from "uuid";
|
||||
import { ModelUsageUnit } from "../src";
|
||||
import { getDisplaySecretKey, hashSecretKey } from "../src/server/auth";
|
||||
import { getDisplaySecretKey, hashSecretKey } from "../src/server";
|
||||
import { encrypt } from "../src/encryption";
|
||||
|
||||
const LOAD_TRACE_VOLUME = 10_000;
|
||||
@@ -39,6 +40,7 @@ async function main() {
|
||||
name: "Demo User",
|
||||
email: "demo@langfuse.com",
|
||||
password: await hash("password", 12),
|
||||
image: "https://static.langfuse.com/langfuse-dev%2Fexample-avatar.png",
|
||||
},
|
||||
});
|
||||
|
||||
@@ -144,6 +146,11 @@ async function main() {
|
||||
});
|
||||
}
|
||||
|
||||
const configIdsAndNames = await generateConfigsForProject([
|
||||
project1,
|
||||
project2,
|
||||
]);
|
||||
|
||||
const promptIds = await generatePromptsForProject([project1, project2]);
|
||||
|
||||
const envTags = [null, "development", "staging", "production"];
|
||||
@@ -157,7 +164,8 @@ async function main() {
|
||||
colorTags,
|
||||
project1,
|
||||
project2,
|
||||
promptIds
|
||||
promptIds,
|
||||
configIdsAndNames
|
||||
);
|
||||
|
||||
console.log(
|
||||
@@ -176,6 +184,7 @@ async function main() {
|
||||
secretKey: encrypt(OPENAI_API_KEY),
|
||||
displaySecretKey: getDisplaySecretKey(OPENAI_API_KEY),
|
||||
provider: "openai",
|
||||
adapter: "openai",
|
||||
},
|
||||
});
|
||||
} else {
|
||||
@@ -201,6 +210,7 @@ async function main() {
|
||||
"Please evaluate the toxicity of the following text {{input}} {{output}}",
|
||||
model: "gpt-3.5-turbo",
|
||||
vars: ["input", "output"],
|
||||
provider: "openai",
|
||||
outputSchema: {
|
||||
score: "provide a score between 0 and 1",
|
||||
reasoning: "one sentence reasoning for the score",
|
||||
@@ -367,11 +377,10 @@ async function uploadObjects(
|
||||
});
|
||||
|
||||
for (let i = 0; i < promises.length; i++) {
|
||||
process.stdout.clearLine(0);
|
||||
process.stdout.cursorTo(0);
|
||||
process.stdout.write(
|
||||
`Seeding of Sessions ${(i / promises.length) * 100}% complete`
|
||||
);
|
||||
if (i + 1 >= promises.length || i % Math.ceil(promises.length / 10) === 0)
|
||||
console.log(
|
||||
`Seeding of Sessions ${((i + 1) / promises.length) * 100}% complete`
|
||||
);
|
||||
await promises[i];
|
||||
}
|
||||
|
||||
@@ -385,11 +394,10 @@ async function uploadObjects(
|
||||
);
|
||||
});
|
||||
for (let i = 0; i < promises.length; i++) {
|
||||
process.stdout.clearLine(0);
|
||||
process.stdout.cursorTo(0);
|
||||
process.stdout.write(
|
||||
`Seeding of Traces ${(i / promises.length) * 100}% complete`
|
||||
);
|
||||
if (i + 1 >= promises.length || i % Math.ceil(promises.length / 10) === 0)
|
||||
console.log(
|
||||
`Seeding of Traces ${((i + 1) / promises.length) * 100}% complete`
|
||||
);
|
||||
await promises[i];
|
||||
}
|
||||
|
||||
@@ -403,11 +411,10 @@ async function uploadObjects(
|
||||
});
|
||||
|
||||
for (let i = 0; i < promises.length; i++) {
|
||||
process.stdout.clearLine(0);
|
||||
process.stdout.cursorTo(0);
|
||||
process.stdout.write(
|
||||
`Seeding of Observations ${(i / promises.length) * 100}% complete`
|
||||
);
|
||||
if (i + 1 >= promises.length || i % Math.ceil(promises.length / 10) === 0)
|
||||
console.log(
|
||||
`Seeding of Observations ${((i + 1) / promises.length) * 100}% complete`
|
||||
);
|
||||
await promises[i];
|
||||
}
|
||||
|
||||
@@ -421,11 +428,10 @@ async function uploadObjects(
|
||||
});
|
||||
|
||||
for (let i = 0; i < promises.length; i++) {
|
||||
process.stdout.clearLine(0);
|
||||
process.stdout.cursorTo(0);
|
||||
process.stdout.write(
|
||||
`Seeding of Events ${(i / promises.length) * 100}% complete`
|
||||
);
|
||||
if (i + 1 >= promises.length || i % Math.ceil(promises.length / 10) === 0)
|
||||
console.log(
|
||||
`Seeding of Events ${((i + 1) / promises.length) * 100}% complete`
|
||||
);
|
||||
await promises[i];
|
||||
}
|
||||
|
||||
@@ -438,11 +444,10 @@ async function uploadObjects(
|
||||
);
|
||||
});
|
||||
for (let i = 0; i < promises.length; i++) {
|
||||
process.stdout.clearLine(0);
|
||||
process.stdout.cursorTo(0);
|
||||
process.stdout.write(
|
||||
`Seeding of Scores ${(i / promises.length) * 100}% complete`
|
||||
);
|
||||
if (i + 1 >= promises.length || i % Math.ceil(promises.length / 10) === 0)
|
||||
console.log(
|
||||
`Seeding of Scores ${((i + 1) / promises.length) * 100}% complete`
|
||||
);
|
||||
await promises[i];
|
||||
}
|
||||
}
|
||||
@@ -453,13 +458,15 @@ function createObjects(
|
||||
colorTags: (string | null)[],
|
||||
project1: Project,
|
||||
project2: Project,
|
||||
promptIds: Map<string, string[]>
|
||||
promptIds: Map<string, string[]>,
|
||||
configIdsAndNames: Map<string, { name: string; id: string }[]>
|
||||
) {
|
||||
const traces: Prisma.TraceCreateManyInput[] = [];
|
||||
const observations: Prisma.ObservationCreateManyInput[] = [];
|
||||
const scores: Prisma.ScoreCreateManyInput[] = [];
|
||||
const sessions: Prisma.TraceSessionCreateManyInput[] = [];
|
||||
const events: Prisma.ObservationCreateManyInput[] = [];
|
||||
const configs: Prisma.ScoreConfigCreateManyInput[] = [];
|
||||
|
||||
for (let i = 0; i < traceVolume; i++) {
|
||||
// print progress to console with a progress bar that refreshes every 10 iterations
|
||||
@@ -511,17 +518,28 @@ function createObjects(
|
||||
|
||||
traces.push(trace);
|
||||
|
||||
const configArray = configIdsAndNames.get(projectId) ?? [];
|
||||
const randomIndex = Math.floor(Math.random() * 3);
|
||||
const config =
|
||||
configArray.length >= randomIndex - 1 && configArray[randomIndex];
|
||||
const { name: annotationScoreName, id: configId } = config || {
|
||||
name: "manual-score",
|
||||
id: undefined,
|
||||
};
|
||||
|
||||
const traceScores = [
|
||||
...(Math.random() > 0.5
|
||||
? [
|
||||
{
|
||||
traceId: trace.id,
|
||||
name: "manual-score",
|
||||
name: annotationScoreName,
|
||||
value: Math.floor(Math.random() * 3) - 1,
|
||||
timestamp: traceTs,
|
||||
source: ScoreSource.ANNOTATION,
|
||||
projectId,
|
||||
authorUserId: `user-${i}`,
|
||||
dataType: ScoreDataType.NUMERIC,
|
||||
...(configId ? { configId } : {}),
|
||||
},
|
||||
]
|
||||
: []),
|
||||
@@ -534,6 +552,7 @@ function createObjects(
|
||||
timestamp: traceTs,
|
||||
source: ScoreSource.API,
|
||||
projectId,
|
||||
dataType: ScoreDataType.NUMERIC,
|
||||
},
|
||||
]
|
||||
: []),
|
||||
@@ -772,6 +791,7 @@ function createObjects(
|
||||
traces,
|
||||
observations,
|
||||
scores,
|
||||
configs,
|
||||
sessions: uniqueSessions,
|
||||
events,
|
||||
};
|
||||
@@ -954,3 +974,80 @@ async function generatePrompts(project: Project) {
|
||||
}
|
||||
return promptIds;
|
||||
}
|
||||
|
||||
async function generateConfigsForProject(projects: Project[]) {
|
||||
const projectIdsToConfigs: Map<string, { name: string; id: string }[]> =
|
||||
new Map();
|
||||
|
||||
await Promise.all(
|
||||
projects.map(async (project) => {
|
||||
const configNameAndId = await generateConfigs(project);
|
||||
projectIdsToConfigs.set(project.id, configNameAndId);
|
||||
})
|
||||
);
|
||||
return projectIdsToConfigs;
|
||||
}
|
||||
|
||||
async function generateConfigs(project: Project) {
|
||||
const configNameAndId: { name: string; id: string }[] = [];
|
||||
|
||||
const configs = [
|
||||
{
|
||||
id: `config-${v4()}`,
|
||||
name: "manual-score",
|
||||
dataType: ScoreDataType.NUMERIC,
|
||||
projectId: project.id,
|
||||
isArchived: false,
|
||||
},
|
||||
{
|
||||
id: `config-${v4()}`,
|
||||
projectId: project.id,
|
||||
name: "Accuracy",
|
||||
dataType: ScoreDataType.CATEGORICAL,
|
||||
categories: [
|
||||
{ label: "Incorrect", value: 0 },
|
||||
{ label: "Partially Correct", value: 1 },
|
||||
{ label: "Correct", value: 2 },
|
||||
],
|
||||
isArchived: false,
|
||||
},
|
||||
{
|
||||
id: `config-${v4()}`,
|
||||
projectId: project.id,
|
||||
name: "Toxicity",
|
||||
dataType: ScoreDataType.BOOLEAN,
|
||||
categories: [
|
||||
{ label: "True", value: 1 },
|
||||
{ label: "False", value: 0 },
|
||||
],
|
||||
description:
|
||||
"Used to indicate if text was harmful or offensive in nature.",
|
||||
isArchived: false,
|
||||
},
|
||||
];
|
||||
|
||||
for (const config of configs) {
|
||||
await prisma.scoreConfig.upsert({
|
||||
where: {
|
||||
id_projectId: {
|
||||
projectId: config.projectId,
|
||||
id: config.id,
|
||||
},
|
||||
},
|
||||
create: {
|
||||
id: config.id,
|
||||
projectId: config.projectId,
|
||||
name: config.name,
|
||||
dataType: config.dataType,
|
||||
categories: config.categories,
|
||||
isArchived: config.isArchived,
|
||||
},
|
||||
update: {
|
||||
id: config.id,
|
||||
},
|
||||
});
|
||||
configNameAndId.push({ name: config.name, id: config.id });
|
||||
}
|
||||
|
||||
return configNameAndId;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
DO $$
|
||||
BEGIN
|
||||
IF EXISTS (SELECT 1 FROM information_schema.tables WHERE table_name = '_prisma_migrations') THEN
|
||||
DELETE FROM _prisma_migrations
|
||||
WHERE migration_name IN ('20240606090858_pricings_add_latest_gemini_models', '20240530212419_model_price_anthropic_via_google_vertex', '20240604133340_backfill_manual_scores');
|
||||
END IF;
|
||||
END $$;
|
||||
@@ -0,0 +1,7 @@
|
||||
import { BaseError } from "./BaseError";
|
||||
|
||||
export class InvalidRequestError extends BaseError {
|
||||
constructor(description = "Invalid Request Error") {
|
||||
super("InvalidRequestError", 400, description, true);
|
||||
}
|
||||
}
|
||||
@@ -1,7 +0,0 @@
|
||||
import { BaseError } from "./BaseError";
|
||||
|
||||
export class ValidationError extends BaseError {
|
||||
constructor(description = "Validation Error") {
|
||||
super("ValidationError", 400, description, true);
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
export { BaseError } from "./BaseError";
|
||||
export { LangfuseNotFoundError } from "./NotFoundError";
|
||||
export { ValidationError } from "./ValidationError";
|
||||
export { InvalidRequestError } from "./InvalidRequestError";
|
||||
export { UnauthorizedError } from "./UnauthorizedError";
|
||||
export { ForbiddenError } from "./ForbiddenError";
|
||||
export { MethodNotAllowedError } from "./MethodNotAllowedError";
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
import z from "zod";
|
||||
import { ScoreConfig } from "../../db";
|
||||
|
||||
const configCategory = z.object({
|
||||
label: z.string().min(1),
|
||||
value: z.number(),
|
||||
});
|
||||
|
||||
export const categoriesList = z.array(configCategory);
|
||||
|
||||
export type ConfigCategory = z.infer<typeof configCategory>;
|
||||
|
||||
export type CastedConfig = Omit<ScoreConfig, "categories"> & {
|
||||
categories: ConfigCategory[] | null;
|
||||
};
|
||||
@@ -0,0 +1,73 @@
|
||||
import z from "zod";
|
||||
|
||||
import { BatchExport } from "@prisma/client";
|
||||
|
||||
import { singleFilter } from "../../interfaces/filters";
|
||||
import { orderBy } from "../../interfaces/orderBy";
|
||||
|
||||
export enum BatchExportStatus {
|
||||
QUEUED = "QUEUED",
|
||||
PROCESSING = "PROCESSING",
|
||||
COMPLETED = "COMPLETED",
|
||||
FAILED = "FAILED",
|
||||
}
|
||||
|
||||
export enum BatchExportFileFormat {
|
||||
JSON = "JSON",
|
||||
CSV = "CSV",
|
||||
}
|
||||
|
||||
export enum BatchExportTableName {
|
||||
Sessions = "sessions",
|
||||
}
|
||||
|
||||
export const exportOptions: Record<
|
||||
BatchExportFileFormat,
|
||||
{
|
||||
label: string;
|
||||
extension: string;
|
||||
fileType: string;
|
||||
}
|
||||
> = {
|
||||
CSV: { label: "CSV", extension: "csv", fileType: "text/csv" },
|
||||
JSON: { label: "JSON", extension: "json", fileType: "application/json" },
|
||||
} as const;
|
||||
|
||||
export const BatchExportQuerySchema = z.object({
|
||||
tableName: z.nativeEnum(BatchExportTableName),
|
||||
filter: z.array(singleFilter).nullable(),
|
||||
orderBy,
|
||||
limit: z.number().optional(),
|
||||
page: z.number().optional(),
|
||||
});
|
||||
|
||||
export type BatchExportQueryType = z.infer<typeof BatchExportQuerySchema>;
|
||||
|
||||
export const CreateBatchExportSchema = z.object({
|
||||
projectId: z.string(),
|
||||
name: z.string(),
|
||||
query: BatchExportQuerySchema,
|
||||
format: z.nativeEnum(BatchExportFileFormat),
|
||||
});
|
||||
|
||||
export const BatchExportSchema = z.object({
|
||||
id: z.string(),
|
||||
createdAt: z.date(),
|
||||
updatedAt: z.date(),
|
||||
projectId: z.string(),
|
||||
userId: z.string(),
|
||||
finishedAt: z.date().nullable(),
|
||||
expiresAt: z.date().nullable(),
|
||||
name: z.string(),
|
||||
status: z.nativeEnum(BatchExportStatus),
|
||||
query: BatchExportQuerySchema,
|
||||
format: z.nativeEnum(BatchExportFileFormat),
|
||||
url: z.string().nullable(),
|
||||
log: z.string().nullable(),
|
||||
});
|
||||
|
||||
// Ensure that zod type matches the Prisma type
|
||||
export type BatchExportType =
|
||||
z.infer<typeof BatchExportSchema> extends BatchExport
|
||||
? z.infer<typeof BatchExportSchema>
|
||||
: never;
|
||||
@@ -1,5 +1,5 @@
|
||||
import z from "zod";
|
||||
import { ModelProvider, UIModelParams } from "../..";
|
||||
import { LLMAdapter } from "../..";
|
||||
|
||||
export const langfuseObjects = [
|
||||
"trace",
|
||||
@@ -77,38 +77,15 @@ export const availableEvalVariables = [
|
||||
},
|
||||
];
|
||||
|
||||
export const evalLLMModels: UIModelParams[] = [
|
||||
{
|
||||
provider: { value: ModelProvider.OpenAI, enabled: true },
|
||||
model: { value: "gpt-3.5-turbo", enabled: true },
|
||||
temperature: { value: 1, enabled: true },
|
||||
maxTemperature: { value: 2, enabled: true },
|
||||
max_tokens: { value: 256, enabled: true },
|
||||
top_p: { value: 1, enabled: true },
|
||||
},
|
||||
{
|
||||
provider: { value: ModelProvider.OpenAI, enabled: true },
|
||||
model: { value: "gpt-4-turbo-preview", enabled: true },
|
||||
temperature: { value: 1, enabled: true },
|
||||
maxTemperature: { value: 2, enabled: true },
|
||||
max_tokens: { value: 256, enabled: true },
|
||||
top_p: { value: 1, enabled: true },
|
||||
},
|
||||
{
|
||||
provider: { value: ModelProvider.OpenAI, enabled: true },
|
||||
model: { value: "gpt-4o", enabled: true },
|
||||
temperature: { value: 1, enabled: true },
|
||||
maxTemperature: { value: 2, enabled: true },
|
||||
max_tokens: { value: 256, enabled: true },
|
||||
top_p: { value: 1, enabled: true },
|
||||
},
|
||||
] as const;
|
||||
|
||||
export const EvalModelNames = z.enum([
|
||||
"gpt-3.5-turbo",
|
||||
"gpt-4-turbo-preview",
|
||||
"gpt-4o",
|
||||
]);
|
||||
export const evalModelsByAdapter: Record<LLMAdapter, string[]> = {
|
||||
[LLMAdapter.OpenAI]: [
|
||||
"gpt-4o",
|
||||
"gpt-4-turbo-preview",
|
||||
"gpt-3.5-turbo",
|
||||
] as const,
|
||||
[LLMAdapter.Anthropic]: [] as const,
|
||||
[LLMAdapter.Azure]: [] as const,
|
||||
};
|
||||
|
||||
export const OutputSchema = z.object({
|
||||
reasoning: z.string(),
|
||||
|
||||
+8
-8
@@ -1,9 +1,9 @@
|
||||
import lodash from "lodash";
|
||||
import { z } from "zod";
|
||||
|
||||
import { ModelUsageUnit } from "@langfuse/shared";
|
||||
import { NonEmptyString, jsonSchema } from "@/src/utils/zod";
|
||||
import { ObservationLevel } from "@langfuse/shared";
|
||||
import { NonEmptyString, jsonSchema } from "../../utils/zod";
|
||||
import { ModelUsageUnit } from "../../constants";
|
||||
import { ObservationLevel } from "@prisma/client";
|
||||
|
||||
export const Usage = z.object({
|
||||
input: z.number().int().nullish(),
|
||||
@@ -109,7 +109,7 @@ export const CreateGenerationBody = CreateSpanBody.extend({
|
||||
z.string(),
|
||||
z
|
||||
.union([z.string(), z.number(), z.boolean(), z.array(z.string())])
|
||||
.nullish(),
|
||||
.nullish()
|
||||
)
|
||||
.nullish(),
|
||||
usage: usage,
|
||||
@@ -131,7 +131,7 @@ export const UpdateGenerationBody = UpdateSpanBody.extend({
|
||||
z.string(),
|
||||
z
|
||||
.union([z.string(), z.number(), z.boolean(), z.array(z.string())])
|
||||
.nullish(),
|
||||
.nullish()
|
||||
)
|
||||
.nullish(),
|
||||
usage: usage,
|
||||
@@ -195,7 +195,7 @@ export const LegacyGenerationsCreateSchema = z.object({
|
||||
modelParameters: z
|
||||
.record(
|
||||
z.string(),
|
||||
z.union([z.string(), z.number(), z.boolean()]).nullish(),
|
||||
z.union([z.string(), z.number(), z.boolean()]).nullish()
|
||||
)
|
||||
.nullish(),
|
||||
prompt: jsonSchema.nullish(),
|
||||
@@ -219,7 +219,7 @@ export const LegacyGenerationPatchSchema = z.object({
|
||||
modelParameters: z
|
||||
.record(
|
||||
z.string(),
|
||||
z.union([z.string(), z.number(), z.boolean()]).nullish(),
|
||||
z.union([z.string(), z.number(), z.boolean()]).nullish()
|
||||
)
|
||||
.nullish(),
|
||||
prompt: jsonSchema.nullish(),
|
||||
@@ -243,7 +243,7 @@ export const LegacyObservationBody = z.object({
|
||||
modelParameters: z
|
||||
.record(
|
||||
z.string(),
|
||||
z.union([z.string(), z.number(), z.boolean()]).nullish(),
|
||||
z.union([z.string(), z.number(), z.boolean()]).nullish()
|
||||
)
|
||||
.nullish(),
|
||||
input: jsonSchema.nullish(),
|
||||
@@ -1,8 +1,5 @@
|
||||
import { Prisma } from "@prisma/client";
|
||||
import {
|
||||
ColumnDefinition,
|
||||
type TableNames as TableName,
|
||||
} from "./interfaces/tableDefinition";
|
||||
import { ColumnDefinition, type TableNames } from "./tableDefinitions";
|
||||
import { FilterState } from "./types";
|
||||
import { filterOperators } from "./interfaces/filters";
|
||||
|
||||
@@ -27,7 +24,7 @@ const arrayOperatorReplacements = {
|
||||
export function tableColumnsToSqlFilterAndPrefix(
|
||||
filters: FilterState,
|
||||
tableColumns: ColumnDefinition[],
|
||||
table: TableName
|
||||
table: TableNames
|
||||
): Prisma.Sql {
|
||||
const sql = tableColumnsToSqlFilter(filters, tableColumns, table);
|
||||
if (sql === Prisma.empty) {
|
||||
@@ -43,7 +40,7 @@ export function tableColumnsToSqlFilterAndPrefix(
|
||||
export function tableColumnsToSqlFilter(
|
||||
filters: FilterState,
|
||||
tableColumns: ColumnDefinition[],
|
||||
table: TableName
|
||||
table: TableNames
|
||||
): Prisma.Sql {
|
||||
const internalFilters = filters.map((filter) => {
|
||||
// Get column definition to map column to internal name, e.g. "t.id"
|
||||
@@ -153,7 +150,7 @@ export function tableColumnsToSqlFilter(
|
||||
|
||||
const castValueToPostgresTypes = (
|
||||
column: ColumnDefinition,
|
||||
table: TableName
|
||||
table: TableNames
|
||||
) => {
|
||||
return column.name === "type" &&
|
||||
(table === "observations" ||
|
||||
|
||||
@@ -1,14 +1,18 @@
|
||||
export * from "./constants";
|
||||
export * from "./queries";
|
||||
export * from "./queues";
|
||||
export * from "./interfaces/exportTypes";
|
||||
export * from "./interfaces/filters";
|
||||
export * from "./interfaces/orderBy";
|
||||
export * from "./interfaces/tableDefinition";
|
||||
export * from "./tableDefinitions";
|
||||
export * from "./types";
|
||||
export * from "./filterToPrisma";
|
||||
export * from "./orderByToPrisma";
|
||||
export * from "./tracesTable";
|
||||
export * from "./server/auth";
|
||||
export * from "./observationsTable";
|
||||
export * from "./features/ingestion/types";
|
||||
export * from "./utils/zod";
|
||||
export * from "./utils/json";
|
||||
export { env } from "./env";
|
||||
|
||||
// llm api
|
||||
@@ -17,6 +21,10 @@ export * from "./server/llm/fetchLLMCompletion";
|
||||
|
||||
// evals
|
||||
export * from "./features/evals/types";
|
||||
export * from "./features/batchExport/types";
|
||||
|
||||
// annotation
|
||||
export * from "./features/annotation/types";
|
||||
|
||||
// export db types only
|
||||
export * from "@prisma/client";
|
||||
|
||||
@@ -1,19 +0,0 @@
|
||||
export const exportFileFormats = ["CSV", "JSON", "OPENAI-JSONL"] as const;
|
||||
export type ExportFileFormats = (typeof exportFileFormats)[number];
|
||||
|
||||
export const exportOptions: Record<
|
||||
ExportFileFormats,
|
||||
{
|
||||
label: string;
|
||||
extension: string;
|
||||
fileType: string;
|
||||
}
|
||||
> = {
|
||||
CSV: { label: "CSV", extension: "csv", fileType: "text/csv" },
|
||||
JSON: { label: "JSON", extension: "json", fileType: "application/json" },
|
||||
"OPENAI-JSONL": {
|
||||
label: "OpenAI JSONL (fine-tuning)",
|
||||
extension: "jsonl",
|
||||
fileType: "application/json",
|
||||
},
|
||||
} as const;
|
||||
@@ -6,3 +6,5 @@ export const orderBy = z
|
||||
order: z.enum(["ASC", "DESC"]),
|
||||
})
|
||||
.nullable();
|
||||
|
||||
export type OrderByState = z.infer<typeof orderBy>;
|
||||
|
||||
@@ -2,7 +2,7 @@ import { ObservationLevel } from "@prisma/client";
|
||||
import {
|
||||
type OptionsDefinition,
|
||||
type ColumnDefinition,
|
||||
} from "./interfaces/tableDefinition";
|
||||
} from "./tableDefinitions/types";
|
||||
|
||||
// to be used server side
|
||||
export const observationsTableCols: ColumnDefinition[] = [
|
||||
|
||||
+8
-6
@@ -1,8 +1,10 @@
|
||||
import { type OrderByState } from "@/src/features/orderBy/types";
|
||||
import { type ColumnDefinition } from "@langfuse/shared";
|
||||
import { Prisma } from "@langfuse/shared";
|
||||
import { z } from "zod";
|
||||
|
||||
import { Prisma } from "@prisma/client";
|
||||
|
||||
import type { ColumnDefinition } from "./tableDefinitions/types";
|
||||
import type { OrderByState } from "./interfaces/orderBy";
|
||||
|
||||
/**
|
||||
* Convert orderBy to SQL ORDER BY clause
|
||||
* @param orderBy orderBy state
|
||||
@@ -11,7 +13,7 @@ import { z } from "zod";
|
||||
*/
|
||||
export function orderByToPrismaSql(
|
||||
orderBy: OrderByState,
|
||||
tableColumns: ColumnDefinition[],
|
||||
tableColumns: ColumnDefinition[]
|
||||
): Prisma.Sql {
|
||||
if (!orderBy) {
|
||||
return Prisma.sql`ORDER BY t.timestamp DESC NULLS LAST`;
|
||||
@@ -20,7 +22,7 @@ export function orderByToPrismaSql(
|
||||
const col = tableColumns.find(
|
||||
// TODO: Only use id instead of name.
|
||||
// It's less error-prone & decouples data fetching from the human-readable UI labels
|
||||
(c) => c.name === orderBy.column || c.id === orderBy.column,
|
||||
(c) => c.name === orderBy.column || c.id === orderBy.column
|
||||
);
|
||||
|
||||
if (!col) {
|
||||
@@ -38,6 +40,6 @@ export function orderByToPrismaSql(
|
||||
|
||||
// Both column and order are safe, can use raw SQL
|
||||
return Prisma.raw(
|
||||
`ORDER BY ${col.internal} ${order.data} ${orderBy.order === "DESC" ? "NULLS LAST" : "NULLS FIRST"}`,
|
||||
`ORDER BY ${col.internal} ${order.data} ${orderBy.order === "DESC" ? "NULLS LAST" : "NULLS FIRST"}`
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,88 @@
|
||||
import { z } from "zod";
|
||||
|
||||
import { Prisma } from "@prisma/client";
|
||||
import { tableColumnsToSqlFilterAndPrefix } from "../filterToPrisma";
|
||||
import { singleFilter } from "../interfaces/filters";
|
||||
import { orderBy } from "../interfaces/orderBy";
|
||||
import { orderByToPrismaSql } from "../orderByToPrisma";
|
||||
import { sessionsViewCols } from "../tableDefinitions/index";
|
||||
|
||||
const GetSessionTableSQLParamsSchema = z.object({
|
||||
projectId: z.string(),
|
||||
filter: z.array(singleFilter).nullable(),
|
||||
orderBy: orderBy,
|
||||
page: z.number(),
|
||||
limit: z.number(),
|
||||
});
|
||||
type GetSessionTableSQLParams = z.infer<typeof GetSessionTableSQLParamsSchema>;
|
||||
|
||||
export const getSessionTableSQL = (
|
||||
params: GetSessionTableSQLParams
|
||||
): Prisma.Sql => {
|
||||
const { projectId, filter, orderBy, page, limit } =
|
||||
GetSessionTableSQLParamsSchema.parse(params);
|
||||
|
||||
const filterCondition = tableColumnsToSqlFilterAndPrefix(
|
||||
filter ?? [],
|
||||
sessionsViewCols,
|
||||
"sessions"
|
||||
);
|
||||
const orderByCondition = orderByToPrismaSql(orderBy, sessionsViewCols);
|
||||
|
||||
const sql = Prisma.sql`
|
||||
SELECT
|
||||
s.id,
|
||||
s. "created_at" AS "createdAt",
|
||||
s.bookmarked,
|
||||
s.public,
|
||||
t. "userIds",
|
||||
t. "countTraces",
|
||||
o. "sessionDuration",
|
||||
o. "totalCost" AS "totalCost",
|
||||
o. "inputCost" AS "inputCost",
|
||||
o. "outputCost" AS "outputCost",
|
||||
o. "promptTokens" AS "promptTokens",
|
||||
o. "completionTokens" AS "completionTokens",
|
||||
o. "totalTokens" AS "totalTokens",
|
||||
(count(*) OVER ())::int AS "totalCount"
|
||||
FROM
|
||||
trace_sessions AS s
|
||||
LEFT JOIN LATERAL (
|
||||
SELECT
|
||||
t.session_id,
|
||||
MAX(t. "timestamp") AS "max_timestamp",
|
||||
MIN(t. "timestamp") AS "min_timestamp",
|
||||
array_agg(t.id) AS "traceIds",
|
||||
array_agg(DISTINCT t.user_id) AS "userIds",
|
||||
count(t.id)::int AS "countTraces"
|
||||
FROM
|
||||
traces t
|
||||
WHERE
|
||||
t.project_id = ${projectId}
|
||||
AND t.session_id = s.id
|
||||
GROUP BY
|
||||
t.session_id) AS t ON TRUE
|
||||
LEFT JOIN LATERAL (
|
||||
SELECT
|
||||
EXTRACT(EPOCH FROM COALESCE(MAX(o. "end_time"), MAX(o. "start_time"), t. "max_timestamp")) - EXTRACT(EPOCH FROM COALESCE(MIN(o. "start_time"), t. "min_timestamp"))::double precision AS "sessionDuration",
|
||||
SUM(COALESCE(o. "calculated_input_cost", 0)) AS "inputCost",
|
||||
SUM(COALESCE(o. "calculated_output_cost", 0)) AS "outputCost",
|
||||
SUM(COALESCE(o. "calculated_total_cost", 0)) AS "totalCost",
|
||||
SUM(o.prompt_tokens) AS "promptTokens",
|
||||
SUM(o.completion_tokens) AS "completionTokens",
|
||||
SUM(o.total_tokens) AS "totalTokens"
|
||||
FROM
|
||||
observations_view o
|
||||
WHERE
|
||||
o.project_id = ${projectId}
|
||||
AND o.trace_id = ANY (t. "traceIds")) AS o ON TRUE
|
||||
WHERE
|
||||
s. "project_id" = ${projectId}
|
||||
${filterCondition}
|
||||
${orderByCondition}
|
||||
LIMIT ${limit}
|
||||
OFFSET ${page * limit}
|
||||
`;
|
||||
|
||||
return sql;
|
||||
};
|
||||
@@ -0,0 +1 @@
|
||||
export { getSessionTableSQL } from "./getSessionTableSQL";
|
||||
@@ -1,36 +1,75 @@
|
||||
import { z } from "zod";
|
||||
|
||||
export const TraceUpsertEvent = z.object({
|
||||
export enum EventName {
|
||||
TraceUpsert = "TraceUpsert",
|
||||
BatchExport = "BatchExport",
|
||||
EvaluationExecution = "EvaluationExecution",
|
||||
}
|
||||
|
||||
export const BatchExportJobSchema = z.object({
|
||||
projectId: z.string(),
|
||||
batchExportId: z.string(),
|
||||
});
|
||||
export const TraceUpsertEventSchema = z.object({
|
||||
projectId: z.string(),
|
||||
traceId: z.string(),
|
||||
});
|
||||
|
||||
export const EvalExecutionEvent = z.object({
|
||||
projectId: z.string(),
|
||||
jobExecutionId: z.string(),
|
||||
});
|
||||
|
||||
export type BatchExportJobType = z.infer<typeof BatchExportJobSchema>;
|
||||
export type TraceUpsertEventType = z.infer<typeof TraceUpsertEventSchema>;
|
||||
export type EvalExecutionEventType = z.infer<typeof EvalExecutionEvent>;
|
||||
|
||||
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,
|
||||
}),
|
||||
]);
|
||||
export type EventBodyType = z.infer<typeof EventBodySchema>;
|
||||
|
||||
export enum QueueName {
|
||||
TraceUpsert = "trace-upsert", // Ingestion pipeline adds events on each Trace upsert
|
||||
EvaluationExecution = "evaluation-execution-queue", // Worker executes Evals
|
||||
BatchExport = "batch-export-queue",
|
||||
RepeatQueue = "repeat-queue",
|
||||
}
|
||||
|
||||
export enum QueueJobs {
|
||||
TraceUpsert = "trace-upsert",
|
||||
EvaluationExecution = "evaluation-execution-job",
|
||||
BatchExportJob = "batch-export-job",
|
||||
EnqueueBatchExportJobs = "enqueue-batch-export-jobs",
|
||||
}
|
||||
|
||||
export type TQueueJobTypes = {
|
||||
[QueueName.TraceUpsert]: {
|
||||
timestamp: Date;
|
||||
id: string;
|
||||
payload: z.infer<typeof TraceUpsertEvent>;
|
||||
payload: TraceUpsertEventType;
|
||||
name: QueueJobs.TraceUpsert;
|
||||
};
|
||||
[QueueName.EvaluationExecution]: {
|
||||
timestamp: Date;
|
||||
id: string;
|
||||
payload: z.infer<typeof EvalExecutionEvent>;
|
||||
payload: EvalExecutionEventType;
|
||||
name: QueueJobs.EvaluationExecution;
|
||||
};
|
||||
[QueueName.BatchExport]: {
|
||||
timestamp: Date;
|
||||
id: string;
|
||||
payload: BatchExportJobType;
|
||||
name: QueueJobs.BatchExportJob;
|
||||
};
|
||||
};
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { compare, hash } from "bcryptjs";
|
||||
import { randomUUID } from "crypto";
|
||||
import * as crypto from "crypto";
|
||||
import type { OAuthConfig, OAuthUserConfig } from "next-auth/providers/oauth";
|
||||
|
||||
export function generateSecretKey() {
|
||||
return `sk-lf-${randomUUID()}`;
|
||||
@@ -48,3 +49,33 @@ export function createShaHash(privateKey: string, salt: string): string {
|
||||
|
||||
return hash;
|
||||
}
|
||||
|
||||
export interface CustomSSOUser extends Record<string, any> {
|
||||
email: string;
|
||||
id: string;
|
||||
name: string;
|
||||
verified: boolean;
|
||||
}
|
||||
|
||||
export function CustomSSOProvider<P extends CustomSSOUser>(
|
||||
options: OAuthUserConfig<P>
|
||||
): OAuthConfig<P> {
|
||||
return {
|
||||
id: "custom",
|
||||
name: "CustomSSOProvider",
|
||||
type: "oauth",
|
||||
wellKnown: `${options.issuer}/.well-known/openid-configuration`,
|
||||
authorization: { params: { scope: "openid email profile" } }, // overridden by options.authorization to be able to set custom scopes, deep merged with this default
|
||||
checks: ["pkce", "state"],
|
||||
idToken: true,
|
||||
profile(profile) {
|
||||
return {
|
||||
id: profile.sub,
|
||||
name: profile.name,
|
||||
email: profile.email,
|
||||
image: null,
|
||||
};
|
||||
},
|
||||
options,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
export * from "./services/S3StorageService";
|
||||
export * from "./services/email/projectInvitation/sendProjectInvitationEmail";
|
||||
export * from "./services/email/batchExportSuccess/sendBatchExportSuccessEmail";
|
||||
export * from "./services/email/passwordReset/sendResetPasswordVerificationRequest";
|
||||
export * from "./auth";
|
||||
export * from "./llm/fetchLLMCompletion";
|
||||
export * from "./llm/types";
|
||||
export * from "./utils/DatabaseReadStream";
|
||||
export * from "./utils/transforms";
|
||||
@@ -17,7 +17,7 @@ import {
|
||||
ChatMessageRole,
|
||||
LLMFunctionCall,
|
||||
ModelParams,
|
||||
ModelProvider,
|
||||
LLMAdapter,
|
||||
} from "./types";
|
||||
import zodToJsonSchema from "zod-to-json-schema";
|
||||
import { JsonOutputFunctionsParser } from "langchain/output_parsers";
|
||||
@@ -27,7 +27,9 @@ type LLMCompletionParams = {
|
||||
modelParams: ModelParams;
|
||||
functionCall?: LLMFunctionCall;
|
||||
callbacks?: BaseCallbackHandler[];
|
||||
baseURL?: string;
|
||||
apiKey?: string;
|
||||
maxRetries?: number;
|
||||
};
|
||||
|
||||
type FetchLLMCompletionParams = LLMCompletionParams & {
|
||||
@@ -57,7 +59,16 @@ export async function fetchLLMCompletion(
|
||||
params: FetchLLMCompletionParams
|
||||
): Promise<string | IterableReadableStream<Uint8Array> | unknown> {
|
||||
// the apiKey must never be printed to the console
|
||||
const { messages, modelParams, streaming, callbacks, apiKey } = params;
|
||||
const {
|
||||
messages,
|
||||
modelParams,
|
||||
streaming,
|
||||
callbacks,
|
||||
apiKey,
|
||||
baseURL,
|
||||
maxRetries,
|
||||
} = params;
|
||||
|
||||
const finalMessages = messages.map((message) => {
|
||||
if (message.role === ChatMessageRole.User)
|
||||
return new HumanMessage(message.content);
|
||||
@@ -68,16 +79,18 @@ export async function fetchLLMCompletion(
|
||||
});
|
||||
|
||||
let chatModel: ChatOpenAI | ChatAnthropic;
|
||||
if (modelParams.provider === ModelProvider.Anthropic) {
|
||||
if (modelParams.adapter === LLMAdapter.Anthropic) {
|
||||
chatModel = new ChatAnthropic({
|
||||
anthropicApiKey: apiKey,
|
||||
anthropicApiUrl: baseURL,
|
||||
modelName: modelParams.model,
|
||||
temperature: modelParams.temperature,
|
||||
maxTokens: modelParams.max_tokens,
|
||||
topP: modelParams.top_p,
|
||||
callbacks,
|
||||
clientOptions: { maxRetries },
|
||||
});
|
||||
} else if (modelParams.provider === ModelProvider.OpenAI) {
|
||||
} else if (modelParams.adapter === LLMAdapter.OpenAI) {
|
||||
chatModel = new ChatOpenAI({
|
||||
openAIApiKey: apiKey,
|
||||
modelName: modelParams.model,
|
||||
@@ -85,15 +98,29 @@ export async function fetchLLMCompletion(
|
||||
maxTokens: modelParams.max_tokens,
|
||||
topP: modelParams.top_p,
|
||||
callbacks,
|
||||
maxRetries,
|
||||
configuration: {
|
||||
baseURL,
|
||||
},
|
||||
});
|
||||
} else if (modelParams.adapter === LLMAdapter.Azure) {
|
||||
chatModel = new ChatOpenAI({
|
||||
azureOpenAIApiKey: apiKey,
|
||||
azureOpenAIBasePath: baseURL,
|
||||
azureOpenAIApiDeploymentName: modelParams.model,
|
||||
azureOpenAIApiVersion: "2024-02-01",
|
||||
temperature: modelParams.temperature,
|
||||
maxTokens: modelParams.max_tokens,
|
||||
topP: modelParams.top_p,
|
||||
callbacks,
|
||||
maxRetries,
|
||||
});
|
||||
} else {
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
const _exhaustiveCheck: never = modelParams;
|
||||
const _exhaustiveCheck: never = modelParams.adapter;
|
||||
throw new Error("This model provider is not supported.");
|
||||
}
|
||||
|
||||
console.log("Making LLM call with params: ", modelParams);
|
||||
|
||||
if (params.functionCall) {
|
||||
const functionCallingModel = chatModel.bind({
|
||||
functions: [
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { LlmApiKeys } from "@prisma/client";
|
||||
import z from "zod";
|
||||
|
||||
export type PromptVariable = { name: string; value: string; isUsed: boolean };
|
||||
@@ -9,9 +10,10 @@ export type ChatMessage = {
|
||||
|
||||
export type ChatMessageWithId = ChatMessage & { id: string };
|
||||
|
||||
export enum ModelProvider {
|
||||
export enum LLMAdapter {
|
||||
Anthropic = "anthropic",
|
||||
OpenAI = "openai",
|
||||
Azure = "azure",
|
||||
}
|
||||
|
||||
export enum ChatMessageRole {
|
||||
@@ -19,32 +21,30 @@ export enum ChatMessageRole {
|
||||
User = "user",
|
||||
Assistant = "assistant",
|
||||
}
|
||||
export type ModelParams = AnthropicModelParams | OpenAIModelParams;
|
||||
|
||||
export type ModelParams = {
|
||||
provider: string;
|
||||
adapter: LLMAdapter;
|
||||
model: string;
|
||||
} & ModelConfig;
|
||||
|
||||
type RecordWithEnabledFlag<T> = {
|
||||
[K in keyof T]: { value: T[K]; enabled: boolean };
|
||||
};
|
||||
export type UIModelParams = RecordWithEnabledFlag<
|
||||
Required<AnthropicModelParams | OpenAIModelParams> & {
|
||||
Required<ModelParams> & {
|
||||
maxTemperature: number;
|
||||
}
|
||||
>;
|
||||
|
||||
// Generic config
|
||||
export type ModelConfig = z.infer<typeof ZodModelConfig>;
|
||||
|
||||
export const ZodModelConfig = z.object({
|
||||
max_tokens: z.coerce.number().optional(),
|
||||
temperature: z.coerce.number().optional(),
|
||||
top_p: z.coerce.number().optional(),
|
||||
});
|
||||
|
||||
// OpenAI
|
||||
export type OpenAIModelParams = {
|
||||
provider: ModelProvider.OpenAI;
|
||||
model: OpenAIModel;
|
||||
} & ModelConfig;
|
||||
|
||||
export const openAIModels = [
|
||||
"gpt-4o",
|
||||
"gpt-4o-2024-05-13",
|
||||
@@ -64,13 +64,8 @@ export const openAIModels = [
|
||||
|
||||
export type OpenAIModel = (typeof openAIModels)[number];
|
||||
|
||||
// Anthropic
|
||||
export type AnthropicModelParams = {
|
||||
provider: ModelProvider.Anthropic;
|
||||
model: AnthropicModel;
|
||||
} & ModelConfig;
|
||||
|
||||
export const anthropicModels = [
|
||||
"claude-3-5-sonnet-20240620",
|
||||
"claude-3-opus-20240229",
|
||||
"claude-3-sonnet-20240229",
|
||||
"claude-3-haiku-20240307",
|
||||
@@ -81,8 +76,9 @@ export const anthropicModels = [
|
||||
|
||||
export type AnthropicModel = (typeof anthropicModels)[number];
|
||||
export const supportedModels = {
|
||||
[ModelProvider.Anthropic]: anthropicModels,
|
||||
[ModelProvider.OpenAI]: openAIModels,
|
||||
[LLMAdapter.Anthropic]: anthropicModels,
|
||||
[LLMAdapter.OpenAI]: openAIModels,
|
||||
[LLMAdapter.Azure]: [],
|
||||
} as const;
|
||||
|
||||
export type LLMFunctionCall = {
|
||||
@@ -90,3 +86,26 @@ export type LLMFunctionCall = {
|
||||
description: string;
|
||||
parameters: z.ZodTypeAny; // this has to be a json schema for OpenAI
|
||||
};
|
||||
|
||||
export const LLMApiKeySchema = z
|
||||
.object({
|
||||
id: z.string(),
|
||||
projectId: z.string(),
|
||||
createdAt: z.date(),
|
||||
updatedAt: z.date(),
|
||||
adapter: z.nativeEnum(LLMAdapter),
|
||||
provider: z.string(),
|
||||
displaySecretKey: z.string(),
|
||||
secretKey: z.string(),
|
||||
baseURL: z.string().nullable(),
|
||||
customModels: z.array(z.string()),
|
||||
withDefaultModels: z.boolean(),
|
||||
})
|
||||
// strict mode to prevent extra keys. Thorws error otherwise
|
||||
// https://github.com/colinhacks/zod?tab=readme-ov-file#strict
|
||||
.strict();
|
||||
|
||||
export type LLMApiKey =
|
||||
z.infer<typeof LLMApiKeySchema> extends LlmApiKeys
|
||||
? z.infer<typeof LLMApiKeySchema>
|
||||
: never;
|
||||
|
||||
+23
-38
@@ -1,5 +1,4 @@
|
||||
import type { Readable } from "stream";
|
||||
import { env } from "@/src/env.mjs";
|
||||
import { GetObjectCommand, S3Client } from "@aws-sdk/client-s3";
|
||||
import { Upload } from "@aws-sdk/lib-storage";
|
||||
import { getSignedUrl } from "@aws-sdk/s3-request-presigner";
|
||||
@@ -8,44 +7,52 @@ type UploadFile = {
|
||||
fileName: string;
|
||||
fileType: string;
|
||||
data: Readable | string;
|
||||
expiresInSeconds: number;
|
||||
};
|
||||
|
||||
class S3StorageService {
|
||||
export class S3StorageService {
|
||||
private client: S3Client;
|
||||
private bucketName: string;
|
||||
|
||||
constructor() {
|
||||
if (!S3StorageService.getIsS3StorageConfigured(env)) {
|
||||
throw new Error("S3 bucket is not configured");
|
||||
}
|
||||
constructor(params: {
|
||||
accessKeyId: string;
|
||||
secretAccessKey: string;
|
||||
bucketName: string;
|
||||
endpoint: string;
|
||||
region: string;
|
||||
}) {
|
||||
const { accessKeyId, secretAccessKey, bucketName, endpoint, region } =
|
||||
params;
|
||||
|
||||
this.client = new S3Client({
|
||||
credentials: {
|
||||
accessKeyId: env.S3_ACCESS_KEY_ID,
|
||||
secretAccessKey: env.S3_SECRET_ACCESS_KEY,
|
||||
accessKeyId,
|
||||
secretAccessKey,
|
||||
},
|
||||
endpoint: env.S3_ENDPOINT,
|
||||
region: env.S3_REGION,
|
||||
endpoint,
|
||||
region,
|
||||
});
|
||||
this.bucketName = bucketName;
|
||||
}
|
||||
|
||||
public async uploadFile({
|
||||
fileName,
|
||||
fileType,
|
||||
data,
|
||||
expiresInSeconds,
|
||||
}: UploadFile): Promise<{ signedUrl: string }> {
|
||||
try {
|
||||
await new Upload({
|
||||
client: this.client,
|
||||
params: {
|
||||
Bucket: env.S3_BUCKET_NAME,
|
||||
Bucket: this.bucketName,
|
||||
Key: fileName,
|
||||
Body: data,
|
||||
ContentType: fileType,
|
||||
},
|
||||
}).done();
|
||||
|
||||
const expiresInOneHour = 60 * 60;
|
||||
const signedUrl = await this.getSignedUrl(fileName, expiresInOneHour);
|
||||
const signedUrl = await this.getSignedUrl(fileName, expiresInSeconds);
|
||||
|
||||
return { signedUrl };
|
||||
} catch (err) {
|
||||
@@ -57,42 +64,20 @@ class S3StorageService {
|
||||
|
||||
private async getSignedUrl(
|
||||
fileName: string,
|
||||
ttlSeconds: number,
|
||||
ttlSeconds: number
|
||||
): Promise<string> {
|
||||
try {
|
||||
return await getSignedUrl(
|
||||
this.client,
|
||||
new GetObjectCommand({
|
||||
Bucket: env.S3_BUCKET_NAME,
|
||||
Bucket: this.bucketName,
|
||||
Key: fileName,
|
||||
ResponseContentDisposition: `attachment; filename="${fileName}"`,
|
||||
}),
|
||||
{ expiresIn: ttlSeconds },
|
||||
{ expiresIn: ttlSeconds }
|
||||
);
|
||||
} catch (err) {
|
||||
throw Error("Failed to generate signed URL");
|
||||
}
|
||||
}
|
||||
|
||||
static getIsS3StorageConfigured(
|
||||
currentEnv: Env,
|
||||
): currentEnv is S3ConfiguredEnv {
|
||||
return Boolean(
|
||||
currentEnv.S3_BUCKET_NAME &&
|
||||
currentEnv.S3_ACCESS_KEY_ID &&
|
||||
currentEnv.S3_SECRET_ACCESS_KEY &&
|
||||
currentEnv.S3_ENDPOINT &&
|
||||
currentEnv.S3_REGION,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export { S3StorageService };
|
||||
|
||||
type Env = typeof env;
|
||||
type S3ConfiguredEnv = Env & {
|
||||
S3_ACCESS_KEY_ID: string;
|
||||
S3_SECRET_ACCESS_KEY: string;
|
||||
S3_ENDPOINT: string;
|
||||
S3_REGION: string;
|
||||
};
|
||||
+83
@@ -0,0 +1,83 @@
|
||||
import * as React from "react";
|
||||
import {
|
||||
Body,
|
||||
Button,
|
||||
Container,
|
||||
Head,
|
||||
Heading,
|
||||
Hr,
|
||||
Html,
|
||||
Img,
|
||||
Preview,
|
||||
Section,
|
||||
Tailwind,
|
||||
Text,
|
||||
} from "@react-email/components";
|
||||
|
||||
interface BatchExportSuccessTemplateProps {
|
||||
userName: string;
|
||||
receiverEmail: string;
|
||||
downloadLink: string;
|
||||
batchExportName: string;
|
||||
expiresInHours: number;
|
||||
}
|
||||
|
||||
export const BatchExportSuccessEmailTemplate = ({
|
||||
receiverEmail,
|
||||
downloadLink,
|
||||
userName,
|
||||
batchExportName,
|
||||
expiresInHours,
|
||||
}: BatchExportSuccessTemplateProps) => {
|
||||
const previewText = `Download your data export from Langfuse`;
|
||||
|
||||
return (
|
||||
<Html>
|
||||
<Head />
|
||||
<Preview>{previewText}</Preview>
|
||||
<Tailwind>
|
||||
<Body className="mx-auto my-auto bg-background font-sans">
|
||||
<Container className="mx-auto my-10 w-[465px] rounded border border-solid border-[#eaeaea] p-5">
|
||||
<Section className="mt-8">
|
||||
<Img
|
||||
src="https://static.langfuse.com/langfuse_logo_transactional_email.png"
|
||||
width="40"
|
||||
height="40"
|
||||
alt="Langfuse"
|
||||
className="mx-auto my-0"
|
||||
/>
|
||||
</Section>
|
||||
<Heading className="mx-0 my-[30px] p-0 text-center text-2xl font-normal text-black">
|
||||
Your data export is ready
|
||||
</Heading>
|
||||
<Text className="text-sm leading-6 text-black">
|
||||
Hello <strong>{userName}</strong>
|
||||
</Text>
|
||||
<Text className="text-sm leading-6 text-black">
|
||||
Your data export{" "}
|
||||
<span className="font-mono">{batchExportName}</span> is ready to
|
||||
download. The download link will expire in{" "}
|
||||
<strong>{expiresInHours} hours</strong>.
|
||||
</Text>
|
||||
<Section className="mb-4 mt-8 text-center">
|
||||
<Button
|
||||
className="rounded bg-black px-5 py-3 text-center text-xs font-semibold text-white no-underline"
|
||||
href={downloadLink}
|
||||
>
|
||||
Download Export
|
||||
</Button>
|
||||
</Section>
|
||||
<Hr className="mx-0 my-[26px] w-full border border-solid border-[#eaeaea]" />
|
||||
<Text className="text-xs leading-6 text-[#666666]">
|
||||
This email was intended for{" "}
|
||||
<span className="text-black">{receiverEmail}</span>. If you were
|
||||
not expecting this email, please delete it.
|
||||
</Text>
|
||||
</Container>
|
||||
</Body>
|
||||
</Tailwind>
|
||||
</Html>
|
||||
);
|
||||
};
|
||||
|
||||
export default BatchExportSuccessEmailTemplate;
|
||||
+56
@@ -0,0 +1,56 @@
|
||||
import { createTransport } from "nodemailer";
|
||||
import { parseConnectionUrl } from "nodemailer/lib/shared/index.js";
|
||||
import { render } from "@react-email/render";
|
||||
|
||||
import { BatchExportSuccessEmailTemplate } from "./BatchExportSuccessEmailTemplate";
|
||||
|
||||
type SendBatchExportSuccessParams = {
|
||||
env: Partial<
|
||||
Record<"EMAIL_FROM_ADDRESS" | "SMTP_CONNECTION_URL", string | undefined>
|
||||
>;
|
||||
receiverEmail: string;
|
||||
downloadLink: string;
|
||||
userName: string;
|
||||
batchExportName: string;
|
||||
expiresInHours: number;
|
||||
};
|
||||
|
||||
export const sendBatchExportSuccessEmail = async ({
|
||||
env,
|
||||
receiverEmail,
|
||||
downloadLink,
|
||||
userName,
|
||||
batchExportName,
|
||||
expiresInHours,
|
||||
}: SendBatchExportSuccessParams) => {
|
||||
if (!env.EMAIL_FROM_ADDRESS || !env.SMTP_CONNECTION_URL) {
|
||||
console.error("Missing environment variables for sending email.");
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const mailer = createTransport(parseConnectionUrl(env.SMTP_CONNECTION_URL));
|
||||
const htmlTemplate = render(
|
||||
BatchExportSuccessEmailTemplate({
|
||||
receiverEmail,
|
||||
downloadLink,
|
||||
userName,
|
||||
batchExportName,
|
||||
expiresInHours,
|
||||
})
|
||||
);
|
||||
|
||||
await mailer.sendMail({
|
||||
to: receiverEmail,
|
||||
from: {
|
||||
address: env.EMAIL_FROM_ADDRESS,
|
||||
name: "Langfuse",
|
||||
},
|
||||
subject: "Your data export is ready",
|
||||
html: htmlTemplate,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
}
|
||||
};
|
||||
+88
@@ -0,0 +1,88 @@
|
||||
/*
|
||||
* To be used in the `sendVerificationRequest` function of the `email` provider of NextAuth.js.
|
||||
*/
|
||||
|
||||
import * as React from "react";
|
||||
import {
|
||||
Body,
|
||||
Button,
|
||||
Container,
|
||||
Head,
|
||||
Heading,
|
||||
Html,
|
||||
Img,
|
||||
Preview,
|
||||
Section,
|
||||
Tailwind,
|
||||
Text,
|
||||
} from "@react-email/components";
|
||||
import { createTransport } from "nodemailer";
|
||||
import { render } from "@react-email/render";
|
||||
import { type SendVerificationRequestParams } from "next-auth/providers/email";
|
||||
|
||||
interface ResetPasswordTemplateProps {
|
||||
url: string;
|
||||
}
|
||||
|
||||
const ResetPasswordTemplate = ({ url }: ResetPasswordTemplateProps) => {
|
||||
const previewText = "Reset your Langfuse password";
|
||||
return (
|
||||
<Html>
|
||||
<Head />
|
||||
<Preview>{previewText}</Preview>
|
||||
<Tailwind>
|
||||
<Body className="mx-auto my-auto bg-background font-sans">
|
||||
<Container className="mx-auto my-10 w-[465px] rounded border border-solid border-[#eaeaea] p-5">
|
||||
<Section className="mt-8">
|
||||
<Img
|
||||
src="https://static.langfuse.com/langfuse_logo_transactional_email.png"
|
||||
width="40"
|
||||
height="40"
|
||||
alt="Langfuse"
|
||||
className="mx-auto my-0"
|
||||
/>
|
||||
</Section>
|
||||
<Heading className="mx-0 my-[30px] p-0 text-center text-xl font-normal text-black">
|
||||
Forgot your Langfuse password?
|
||||
<br />
|
||||
It happens to the best of us.
|
||||
</Heading>
|
||||
<Section className="mb-8 mt-8 text-center">
|
||||
<Button
|
||||
className="rounded bg-black px-5 py-3 text-center text-xs font-semibold text-white no-underline"
|
||||
href={url}
|
||||
>
|
||||
Reset your password
|
||||
</Button>
|
||||
</Section>
|
||||
<Text className="text-center text-xs leading-6 text-[#666666]">
|
||||
If you do not want to change your password or didn't request
|
||||
a reset, you can ignore and delete this email.
|
||||
</Text>
|
||||
</Container>
|
||||
</Body>
|
||||
</Tailwind>
|
||||
</Html>
|
||||
);
|
||||
};
|
||||
|
||||
export async function sendResetPasswordVerificationRequest(
|
||||
params: SendVerificationRequestParams
|
||||
) {
|
||||
const { identifier, url, provider } = params;
|
||||
const transport = createTransport(provider.server);
|
||||
const htmlTemplate = render(<ResetPasswordTemplate url={url} />);
|
||||
const result = await transport.sendMail({
|
||||
to: identifier,
|
||||
from: provider.from,
|
||||
subject: `Forgot your password?`,
|
||||
text: `To reset your Langfuse password, please confirm your email:\n${url}\n\nIf you do not want to change your password or didn't request a reset, you can ignore and delete this email.`,
|
||||
html: htmlTemplate,
|
||||
});
|
||||
const failed = result.rejected.concat(result.pending).filter(Boolean);
|
||||
if (failed.length) {
|
||||
throw new Error(`Email(s) (${failed.join(", ")}) could not be sent`);
|
||||
}
|
||||
}
|
||||
|
||||
export default ResetPasswordTemplate;
|
||||
+8
-8
@@ -15,14 +15,13 @@ import {
|
||||
Text,
|
||||
} from "@react-email/components";
|
||||
|
||||
import { env } from "@/src/env.mjs";
|
||||
|
||||
interface ProjectInvitationTemplateProps {
|
||||
invitedByUsername: string;
|
||||
invitedByUserEmail: string;
|
||||
projectName: string;
|
||||
recieverEmail: string;
|
||||
receiverEmail: string;
|
||||
inviteLink: string;
|
||||
emailFromAddress: string;
|
||||
langfuseCloudRegion?: string;
|
||||
}
|
||||
|
||||
@@ -30,8 +29,9 @@ export const ProjectInvitationTemplate = ({
|
||||
invitedByUsername,
|
||||
invitedByUserEmail,
|
||||
projectName,
|
||||
recieverEmail,
|
||||
receiverEmail,
|
||||
inviteLink,
|
||||
emailFromAddress,
|
||||
langfuseCloudRegion,
|
||||
}: ProjectInvitationTemplateProps) => {
|
||||
const previewText = `Join ${invitedByUsername} on Langfuse`;
|
||||
@@ -72,7 +72,7 @@ export const ProjectInvitationTemplate = ({
|
||||
</Text>
|
||||
<Section className="mb-4 mt-8 text-center">
|
||||
<Button
|
||||
className="rounded bg-black px-5 py-3 text-center text-xs font-semibold text-background no-underline"
|
||||
className="rounded bg-black px-5 py-3 text-center text-xs font-semibold text-white no-underline"
|
||||
href={inviteLink}
|
||||
>
|
||||
Accept Invitation
|
||||
@@ -90,10 +90,10 @@ export const ProjectInvitationTemplate = ({
|
||||
<Hr className="mx-0 my-[26px] w-full border border-solid border-[#eaeaea]" />
|
||||
<Text className="text-xs leading-6 text-[#666666]">
|
||||
This invitation was intended for{" "}
|
||||
<span className="text-black">{recieverEmail}</span>. This invite
|
||||
<span className="text-black">{receiverEmail}</span>. This invite
|
||||
was sent from{" "}
|
||||
<span className="text-black">{env.EMAIL_FROM_ADDRESS}</span>. If
|
||||
you were not expecting this invitation, you can ignore this email.
|
||||
<span className="text-black">{emailFromAddress}</span>. If you
|
||||
were not expecting this invitation, you can ignore this email.
|
||||
</Text>
|
||||
</Container>
|
||||
</Body>
|
||||
+85
@@ -0,0 +1,85 @@
|
||||
import { createTransport } from "nodemailer";
|
||||
import { parseConnectionUrl } from "nodemailer/lib/shared/index.js";
|
||||
import { render } from "@react-email/render";
|
||||
|
||||
import ProjectInvitationTemplate from "./ProjectInvitationEmailTemplate";
|
||||
|
||||
const langfuseUrls = {
|
||||
US: "https://us.cloud.langfuse.com",
|
||||
EU: "https://cloud.langfuse.com",
|
||||
STAGING: "https://staging.langfuse.com",
|
||||
};
|
||||
|
||||
type SendProjectInvitationParams = {
|
||||
env: Partial<
|
||||
Record<
|
||||
| "EMAIL_FROM_ADDRESS"
|
||||
| "SMTP_CONNECTION_URL"
|
||||
| "NEXT_PUBLIC_LANGFUSE_CLOUD_REGION"
|
||||
| "NEXTAUTH_URL",
|
||||
string | undefined
|
||||
>
|
||||
>;
|
||||
to: string;
|
||||
inviterName: string;
|
||||
inviterEmail: string;
|
||||
projectName: string;
|
||||
};
|
||||
|
||||
export const sendProjectInvitationEmail = async ({
|
||||
env,
|
||||
to,
|
||||
inviterName,
|
||||
inviterEmail,
|
||||
projectName,
|
||||
}: SendProjectInvitationParams) => {
|
||||
if (!env.EMAIL_FROM_ADDRESS || !env.SMTP_CONNECTION_URL) {
|
||||
console.error(
|
||||
"Missing environment variables for sending project invitation email."
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
const getAuthURL = () =>
|
||||
env.NEXT_PUBLIC_LANGFUSE_CLOUD_REGION === "US" ||
|
||||
env.NEXT_PUBLIC_LANGFUSE_CLOUD_REGION === "EU" ||
|
||||
env.NEXT_PUBLIC_LANGFUSE_CLOUD_REGION === "STAGING"
|
||||
? langfuseUrls[env.NEXT_PUBLIC_LANGFUSE_CLOUD_REGION]
|
||||
: env.NEXTAUTH_URL;
|
||||
|
||||
const authUrl = getAuthURL();
|
||||
if (!authUrl) {
|
||||
console.error(
|
||||
"Missing NEXTAUTH_URL or NEXT_PUBLIC_LANGFUSE_CLOUD_REGION environment variable."
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const mailer = createTransport(parseConnectionUrl(env.SMTP_CONNECTION_URL));
|
||||
|
||||
const htmlTemplate = render(
|
||||
ProjectInvitationTemplate({
|
||||
invitedByUsername: inviterName,
|
||||
invitedByUserEmail: inviterEmail,
|
||||
projectName: projectName,
|
||||
receiverEmail: to,
|
||||
inviteLink: authUrl,
|
||||
emailFromAddress: env.EMAIL_FROM_ADDRESS,
|
||||
langfuseCloudRegion: env.NEXT_PUBLIC_LANGFUSE_CLOUD_REGION,
|
||||
})
|
||||
);
|
||||
|
||||
await mailer.sendMail({
|
||||
to,
|
||||
from: {
|
||||
address: env.EMAIL_FROM_ADDRESS,
|
||||
name: "Langfuse",
|
||||
},
|
||||
subject: `${inviterName} invited you to join "${projectName}"`,
|
||||
html: htmlTemplate,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
}
|
||||
};
|
||||
+12
-2
@@ -26,9 +26,10 @@ export class DatabaseReadStream<EntityType> extends Readable {
|
||||
// the delegate function takes care of querying the database in a paginated manner
|
||||
private queryDelegate: (
|
||||
pageSize: number,
|
||||
offset: number,
|
||||
offset: number
|
||||
) => Promise<Array<EntityType>>,
|
||||
private pageSize: number,
|
||||
private maxRecords?: number
|
||||
) {
|
||||
super({ objectMode: true }); // Set object mode to true to allow pushing objects to the stream rather than strings or buffers
|
||||
|
||||
@@ -43,10 +44,19 @@ export class DatabaseReadStream<EntityType> extends Readable {
|
||||
this.isReading = true;
|
||||
|
||||
try {
|
||||
// Stop reading if the maximum number of records has been reached
|
||||
if (this.maxRecords && this.offset >= this.maxRecords) {
|
||||
this.hasNextPage = false;
|
||||
this.push(null); // Signal end of stream
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
const rows = await this.queryDelegate(this.pageSize, this.offset);
|
||||
|
||||
if (rows.length > 0) {
|
||||
rows.forEach((row) => this.push(row));
|
||||
this.offset += rows.length;
|
||||
this.offset += this.pageSize;
|
||||
} else {
|
||||
this.hasNextPage = false;
|
||||
this.push(null); // Signal end of stream
|
||||
@@ -0,0 +1,13 @@
|
||||
import { Transform } from "stream";
|
||||
|
||||
import { BatchExportFileFormat } from "../../../features/batchExport/types";
|
||||
import { transformStreamToCsv } from "./transformStreamToCsv";
|
||||
import { transformStreamToJson } from "./transformStreamToJson";
|
||||
|
||||
export const streamTransformations: Record<
|
||||
BatchExportFileFormat,
|
||||
() => Transform
|
||||
> = {
|
||||
CSV: transformStreamToCsv,
|
||||
JSON: transformStreamToJson,
|
||||
};
|
||||
@@ -0,0 +1,5 @@
|
||||
export const stringify = (data: any): string => {
|
||||
return JSON.stringify(data, (key, value) =>
|
||||
typeof value === "bigint" ? Number.parseInt(value.toString()) : value
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,39 @@
|
||||
import { Transform, type TransformCallback } from "stream";
|
||||
import { stringify } from "./stringify";
|
||||
|
||||
export function transformStreamToCsv(): Transform {
|
||||
let isFirstChunk = true;
|
||||
let headers: string[] = [];
|
||||
|
||||
return new Transform({
|
||||
objectMode: true,
|
||||
transform(
|
||||
row: Record<string, any>,
|
||||
encoding: BufferEncoding,
|
||||
callback: TransformCallback
|
||||
): void {
|
||||
if (isFirstChunk) {
|
||||
// Extract headers from the first object
|
||||
headers = Object.keys(row);
|
||||
this.push(headers.join(",") + "\n");
|
||||
isFirstChunk = false;
|
||||
}
|
||||
|
||||
// Convert the object to a CSV line and push it
|
||||
const csvRow = headers.map((header) => {
|
||||
const field = row[header];
|
||||
const str = stringify(field);
|
||||
|
||||
if (str.startsWith('"') && str.endsWith('"')) {
|
||||
return str;
|
||||
} else {
|
||||
return `"${str?.replace(/"/g, '""') ?? ""}"`;
|
||||
}
|
||||
});
|
||||
|
||||
this.push(csvRow.join(",") + "\n");
|
||||
|
||||
callback();
|
||||
},
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
import { Transform, type TransformCallback } from "stream";
|
||||
import { stringify } from "./stringify";
|
||||
|
||||
export function transformStreamToJson(): Transform {
|
||||
let isFirstElement = true;
|
||||
|
||||
return new Transform({
|
||||
objectMode: true,
|
||||
|
||||
transform(
|
||||
row: any,
|
||||
encoding: BufferEncoding,
|
||||
callback: TransformCallback
|
||||
): void {
|
||||
if (isFirstElement) {
|
||||
this.push("["); // Push the opening bracket for the first element
|
||||
isFirstElement = false; // Reset the flag after the first element
|
||||
} else {
|
||||
this.push(","); // For subsequent elements, prepend a comma
|
||||
}
|
||||
|
||||
this.push(stringify(row)); // Push the current row as a JSON string
|
||||
|
||||
callback();
|
||||
},
|
||||
|
||||
// 'final' is called when there is no more data to be consumed, but before the stream is finished.
|
||||
final(callback: TransformCallback): void {
|
||||
if (isFirstElement) {
|
||||
// If no rows were processed, the opening bracket has not been pushed yet.
|
||||
this.push("[]"); // Push an empty array to ensure valid JSON.
|
||||
} else {
|
||||
this.push("]"); // Close JSON array
|
||||
}
|
||||
|
||||
callback();
|
||||
},
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
export * from "./sessionsView";
|
||||
export * from "./types";
|
||||
+2
-2
@@ -1,7 +1,7 @@
|
||||
import {
|
||||
type OptionsDefinition,
|
||||
type ColumnDefinition,
|
||||
} from "@langfuse/shared";
|
||||
} from "../tableDefinitions/types";
|
||||
|
||||
export const sessionsViewCols: ColumnDefinition[] = [
|
||||
{ name: "⭐️", id: "bookmarked", type: "boolean", internal: "s.bookmarked" },
|
||||
@@ -85,7 +85,7 @@ export type SessionOptions = {
|
||||
};
|
||||
|
||||
export function sessionsTableColsWithOptions(
|
||||
options?: SessionOptions,
|
||||
options?: SessionOptions
|
||||
): ColumnDefinition[] {
|
||||
return sessionsViewCols.map((col) => {
|
||||
if (col.id === "userIds") {
|
||||
+1
@@ -42,6 +42,7 @@ export const tableNames = [
|
||||
"traces_parent_observation_scores",
|
||||
"sessions",
|
||||
"prompts",
|
||||
"users",
|
||||
] as const;
|
||||
|
||||
export type TableNames = (typeof tableNames)[number];
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user