Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6aa5b20cce | ||
|
|
fa0011f80f | ||
|
|
82cf939be6 | ||
|
|
ba02577ace | ||
|
|
474634b632 | ||
|
|
2441af3981 | ||
|
|
1f9b2f600c | ||
|
|
68fe3357b6 | ||
|
|
fc007fd0fa | ||
|
|
e850ac6578 | ||
|
|
c65f877063 | ||
|
|
3fb7ec55fb | ||
|
|
ddf90217bc | ||
|
|
63968392cc | ||
|
|
0f76b010df | ||
|
|
9b718c556f | ||
|
|
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 |
@@ -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
|
||||
@@ -66,6 +73,7 @@ LANGFUSE_CSP_ENFORCE_HTTPS="true"
|
||||
# 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
|
||||
@@ -80,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
|
||||
@@ -122,6 +131,8 @@ 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=
|
||||
|
||||
@@ -64,9 +64,11 @@ jobs:
|
||||
|
||||
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
|
||||
@@ -97,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: |
|
||||
@@ -114,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
|
||||
@@ -147,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: |
|
||||
@@ -184,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
|
||||
@@ -250,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
|
||||
@@ -273,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
|
||||
@@ -297,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 }}
|
||||
+1
-1
@@ -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
|
||||
|
||||
@@ -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>
|
||||
@@ -157,7 +159,7 @@ This repository is MIT licensed, except for the `ee` folders. See [LICENSE](LICE
|
||||
|
||||
### GET API to export your data
|
||||
|
||||
[**GET routes**](https://langfuse.com/docs/integrations/api) to use data in downstream applications (e.g. embedded analytics).
|
||||
[**GET routes**](https://langfuse.com/docs/integrations/api) to use data in downstream applications (e.g. embedded analytics). You can also access them conveniently via the SDKs ([docs](https://langfuse.com/docs/query-traces)).
|
||||
|
||||
### Security & Privacy
|
||||
|
||||
|
||||
@@ -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
|
||||
@@ -30,6 +32,8 @@ services:
|
||||
build:
|
||||
dockerfile: ./worker/Dockerfile
|
||||
context: .
|
||||
args:
|
||||
- NEXT_PUBLIC_LANGFUSE_CLOUD_REGION=${NEXT_PUBLIC_LANGFUSE_CLOUD_REGION}
|
||||
depends_on:
|
||||
- db
|
||||
- redis
|
||||
|
||||
@@ -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,5 +1,3 @@
|
||||
version: "3.5"
|
||||
|
||||
services:
|
||||
langfuse-server:
|
||||
image: langfuse/langfuse:2
|
||||
|
||||
+4
-1
@@ -11,7 +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/auth";
|
||||
import { CustomSSOProvider } from "@langfuse/shared/src/server";
|
||||
|
||||
// Local cache for SSO configurations
|
||||
let cachedSsoConfigs: {
|
||||
@@ -179,6 +179,9 @@ const dbToNextAuthProvider = (provider: SsoProviderSchema): Provider | null => {
|
||||
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
|
||||
|
||||
@@ -84,6 +84,7 @@ export const CustomProviderSchema = base.extend({
|
||||
clientId: z.string(),
|
||||
clientSecret: z.string(),
|
||||
issuer: z.string(),
|
||||
scope: z.string().nullish(),
|
||||
allowDangerousEmailAccountLinking: z.boolean().optional().default(false),
|
||||
})
|
||||
.nullish(),
|
||||
|
||||
@@ -17,15 +17,131 @@ types:
|
||||
id: optional<string>
|
||||
traceId: string
|
||||
name: string
|
||||
value: double
|
||||
value:
|
||||
type: CreateScoreValue
|
||||
docs: The value of the score. Must be passed as string for categorical scores, and numeric for boolean and numeric scores. Boolean score values must equal either 1 or 0 (true or false)
|
||||
observationId: optional<string>
|
||||
comment: optional<string>
|
||||
Score:
|
||||
dataType:
|
||||
type: optional<ScoreDataType>
|
||||
docs: When set, must match the score value's type. If not set, will be inferred from the score value or config
|
||||
configId:
|
||||
type: optional<string>
|
||||
docs: Reference a score config on a score. When set, the score name must equal the config name and scores must comply with the config's range and data type. For categorical scores, the value must map to a config category. Numeric scores might be constrained by the score config's max and min values
|
||||
examples:
|
||||
- value:
|
||||
name: "novelty"
|
||||
value: 0.9
|
||||
traceId: "cdef-1234-5678-90ab"
|
||||
- value:
|
||||
name: "consistency"
|
||||
value: 1.2
|
||||
dataType: "NUMERIC"
|
||||
traceId: "cdef-1234-5678-90ab"
|
||||
- value:
|
||||
name: "accuracy"
|
||||
value: 0.9
|
||||
dataType: "NUMERIC"
|
||||
configId: "9203-4567-89ab-cdef"
|
||||
traceId: "cdef-1234-5678-90ab"
|
||||
- value:
|
||||
name: "toxicity"
|
||||
value: "not toxic"
|
||||
traceId: "cdef-1234-5678-90ab"
|
||||
- value:
|
||||
name: "correctness"
|
||||
value: "partially correct"
|
||||
dataType: "CATEGORICAL"
|
||||
configId: "1234-5678-90ab-cdef"
|
||||
traceId: "cdef-1234-5678-90ab"
|
||||
- value:
|
||||
name: "hallucination"
|
||||
value: 0
|
||||
dataType: "BOOLEAN"
|
||||
traceId: "cdef-1234-5678-90ab"
|
||||
- value:
|
||||
name: "helpfulness"
|
||||
value: 1
|
||||
dataType: "BOOLEAN"
|
||||
configId: "1234-5678-90ab-cdef"
|
||||
traceId: "cdef-1234-5678-90ab"
|
||||
NumericScore:
|
||||
properties:
|
||||
id: string
|
||||
traceId: string
|
||||
name: string
|
||||
value: double
|
||||
value:
|
||||
type: double
|
||||
docs: The numeric value of the score
|
||||
source: ScoreSource
|
||||
observationId: optional<string>
|
||||
timestamp: datetime
|
||||
comment: optional<string>
|
||||
configId:
|
||||
type: optional<string>
|
||||
docs: Reference a score config on a score. When set, config and score name must be equal and value must comply to optionally defined numerical range
|
||||
BooleanScore:
|
||||
properties:
|
||||
id: string
|
||||
traceId: string
|
||||
name: string
|
||||
value:
|
||||
type: double
|
||||
docs: The numeric value of the score. Equals 1 for "True" and 0 for "False"
|
||||
stringValue:
|
||||
type: string
|
||||
docs: The string representation of the score value. Is inferred from the numeric value and equals "True" or "False"
|
||||
source: ScoreSource
|
||||
observationId: optional<string>
|
||||
timestamp: datetime
|
||||
comment: optional<string>
|
||||
configId:
|
||||
type: optional<string>
|
||||
docs: Reference a score config on a score. When set, config and score name must be equal
|
||||
CategoricalScore:
|
||||
properties:
|
||||
id: string
|
||||
traceId: string
|
||||
name: string
|
||||
value:
|
||||
type: optional<double>
|
||||
docs: Only defined if a config is linked. Represents the numeric category mapping of the stringValue
|
||||
stringValue:
|
||||
type: string
|
||||
docs: The string representation of the score value. If no config is linked, can be any string. Otherwise, must map to a config category
|
||||
source: ScoreSource
|
||||
observationId: optional<string>
|
||||
timestamp: datetime
|
||||
comment: optional<string>
|
||||
configId:
|
||||
type: optional<string>
|
||||
docs: Reference a score config on a score. When set, config and score name must be equal and stringValue must map to a config category
|
||||
Score:
|
||||
discriminant: "dataType"
|
||||
union:
|
||||
NUMERIC:
|
||||
type: NumericScore
|
||||
docs: "Score with NUMERIC data type"
|
||||
CATEGORICAL:
|
||||
type: CategoricalScore
|
||||
docs: "Score with CATEGORICAL data type"
|
||||
BOOLEAN:
|
||||
type: BooleanScore
|
||||
docs: "Score with BOOLEAN data type"
|
||||
ScoreSource:
|
||||
enum:
|
||||
- ANNOTATION
|
||||
- API
|
||||
- EVAL
|
||||
ScoreDataType:
|
||||
enum:
|
||||
- NUMERIC
|
||||
- CATEGORICAL
|
||||
- BOOLEAN
|
||||
|
||||
CreateScoreValue:
|
||||
discriminated: false
|
||||
union:
|
||||
- string
|
||||
- double
|
||||
docs: The value of the score. Must be passed as string for categorical scores, and numeric for boolean and numeric scores
|
||||
|
||||
@@ -111,18 +111,103 @@ types:
|
||||
totalCost:
|
||||
docs: USD total cost, defaults to input+output
|
||||
type: optional<double>
|
||||
|
||||
Score:
|
||||
ScoreConfig:
|
||||
docs: Configuration for a score
|
||||
properties:
|
||||
id: string
|
||||
name: string
|
||||
createdAt: datetime
|
||||
updatedAt: datetime
|
||||
projectId: string
|
||||
dataType: ScoreDataType
|
||||
isArchived:
|
||||
type: boolean
|
||||
docs: Whether the score config is archived. Defaults to false
|
||||
minValue:
|
||||
type: optional<double>
|
||||
docs: Sets minimum value for numerical scores. If not set, the minimum value defaults to -∞
|
||||
maxValue:
|
||||
type: optional<double>
|
||||
docs: Sets maximum value for numerical scores. If not set, the maximum value defaults to +∞
|
||||
categories:
|
||||
type: optional<list<ConfigCategory>>
|
||||
docs: Configures custom categories for categorical scores
|
||||
description: optional<string>
|
||||
ConfigCategory:
|
||||
properties:
|
||||
value: double
|
||||
label: string
|
||||
NumericScore:
|
||||
properties:
|
||||
id: string
|
||||
traceId: string
|
||||
name: string
|
||||
value: double
|
||||
value:
|
||||
type: double
|
||||
docs: The numeric value of the score
|
||||
source: ScoreSource
|
||||
observationId: optional<string>
|
||||
timestamp: datetime
|
||||
comment: optional<string>
|
||||
DatasetCore:
|
||||
configId:
|
||||
type: optional<string>
|
||||
docs: Reference a score config on a score. When set, config and score name must be equal and value must comply to optionally defined numerical range
|
||||
BooleanScore:
|
||||
properties:
|
||||
id: string
|
||||
traceId: string
|
||||
name: string
|
||||
value:
|
||||
type: double
|
||||
docs: The numeric value of the score. Equals 1 for "True" and 0 for "False"
|
||||
stringValue:
|
||||
type: string
|
||||
docs: The string representation of the score value. Is inferred from the numeric value and equals "True" or "False"
|
||||
source: ScoreSource
|
||||
observationId: optional<string>
|
||||
timestamp: datetime
|
||||
comment: optional<string>
|
||||
configId:
|
||||
type: optional<string>
|
||||
docs: Reference a score config on a score. When set, config and score name must be equal
|
||||
CategoricalScore:
|
||||
properties:
|
||||
id: string
|
||||
traceId: string
|
||||
name: string
|
||||
value:
|
||||
type: optional<double>
|
||||
docs: Only defined if a config is linked. Represents the numeric category mapping of the stringValue
|
||||
stringValue:
|
||||
type: string
|
||||
docs: The string representation of the score value. If no config is linked, can be any string. Otherwise, must map to a config category
|
||||
source: ScoreSource
|
||||
observationId: optional<string>
|
||||
timestamp: datetime
|
||||
comment: optional<string>
|
||||
configId:
|
||||
type: optional<string>
|
||||
docs: Reference a score config on a score. When set, config and score name must be equal and stringValue must map to a config category
|
||||
Score:
|
||||
discriminant: "dataType"
|
||||
union:
|
||||
NUMERIC:
|
||||
type: NumericScore
|
||||
docs: "Score with NUMERIC data type"
|
||||
CATEGORICAL:
|
||||
type: CategoricalScore
|
||||
docs: "Score with CATEGORICAL data type"
|
||||
BOOLEAN:
|
||||
type: BooleanScore
|
||||
docs: "Score with BOOLEAN data type"
|
||||
|
||||
CreateScoreValue:
|
||||
discriminated: false
|
||||
union:
|
||||
- string
|
||||
- double
|
||||
docs: The value of the score. Must be passed as string for categorical scores, and numeric for boolean and numeric scores
|
||||
Dataset:
|
||||
properties:
|
||||
id: string
|
||||
name: string
|
||||
@@ -131,21 +216,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 +249,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 +318,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,7 +7,12 @@ service:
|
||||
base-path: /api/public
|
||||
endpoints:
|
||||
batch:
|
||||
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.
|
||||
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:
|
||||
@@ -180,9 +185,54 @@ types:
|
||||
id: optional<string>
|
||||
traceId: string
|
||||
name: string
|
||||
value: double
|
||||
value:
|
||||
type: commons.CreateScoreValue
|
||||
docs: The value of the score. Must be passed as string for categorical scores, and numeric for boolean and numeric scores. Boolean score values must equal either 1 or 0 (true or false)
|
||||
observationId: optional<string>
|
||||
comment: optional<string>
|
||||
dataType:
|
||||
type: optional<commons.ScoreDataType>
|
||||
docs: When set, must match the score value's type. If not set, will be inferred from the score value or config
|
||||
configId:
|
||||
type: optional<string>
|
||||
docs: Reference a score config on a score. When set, the score name must equal the config name and scores must comply with the config's range and data type. For categorical scores, the value must map to a config category. Numeric scores might be constrained by the score config's max and min values
|
||||
examples:
|
||||
- value:
|
||||
name: "novelty"
|
||||
value: 0.9
|
||||
traceId: "cdef-1234-5678-90ab"
|
||||
- value:
|
||||
name: "consistency"
|
||||
value: 1.2
|
||||
dataType: "NUMERIC"
|
||||
traceId: "cdef-1234-5678-90ab"
|
||||
- value:
|
||||
name: "accuracy"
|
||||
value: 0.9
|
||||
dataType: "NUMERIC"
|
||||
configId: "9203-4567-89ab-cdef"
|
||||
traceId: "cdef-1234-5678-90ab"
|
||||
- value:
|
||||
name: "toxicity"
|
||||
value: "not toxic"
|
||||
traceId: "cdef-1234-5678-90ab"
|
||||
- value:
|
||||
name: "correctness"
|
||||
value: "partially correct"
|
||||
dataType: "CATEGORICAL"
|
||||
configId: "1234-5678-90ab-cdef"
|
||||
traceId: "cdef-1234-5678-90ab"
|
||||
- value:
|
||||
name: "hallucination"
|
||||
value: 0
|
||||
dataType: "BOOLEAN"
|
||||
traceId: "cdef-1234-5678-90ab"
|
||||
- value:
|
||||
name: "helpfulness"
|
||||
value: 1
|
||||
dataType: "BOOLEAN"
|
||||
configId: "1234-5678-90ab-cdef"
|
||||
traceId: "cdef-1234-5678-90ab"
|
||||
|
||||
BaseEvent:
|
||||
properties:
|
||||
|
||||
@@ -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: Delete 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,58 @@
|
||||
# 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:
|
||||
create:
|
||||
docs: Create a score configuration (config). Score configs are used to define the structure of scores
|
||||
method: POST
|
||||
path: /score-configs
|
||||
request: CreateScoreConfigRequest
|
||||
response: commons.ScoreConfig
|
||||
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
|
||||
CreateScoreConfigRequest:
|
||||
properties:
|
||||
name: string
|
||||
dataType: commons.ScoreDataType
|
||||
categories:
|
||||
type: optional<list<commons.ConfigCategory>>
|
||||
docs: Configure custom categories for categorical scores. Pass a list of objects with `label` and `value` properties. Categories are autogenerated for boolean configs and cannot be passed
|
||||
minValue:
|
||||
type: optional<double>
|
||||
docs: Configure a minimum value for numerical scores. If not set, the minimum value defaults to -∞
|
||||
maxValue:
|
||||
type: optional<double>
|
||||
docs: Configure a maximum value for numerical scores. If not set, the maximum value defaults to +∞
|
||||
description:
|
||||
type: optional<string>
|
||||
docs: Description is shown across the Langfuse UI and can be used to e.g. explain the config categories in detail, why a numeric range was set, or provide additional context on config name or usage
|
||||
@@ -25,8 +25,12 @@ service:
|
||||
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.
|
||||
userId: optional<string>
|
||||
name: optional<string>
|
||||
userId:
|
||||
type: optional<string>
|
||||
docs: Retrieve only scores with this userId associated to the trace.
|
||||
name:
|
||||
type: optional<string>
|
||||
docs: Retrieve only scores with this name.
|
||||
fromTimestamp:
|
||||
type: optional<datetime>
|
||||
docs: Retrieve only scores newer than this datetime (ISO 8601).
|
||||
@@ -42,6 +46,12 @@ service:
|
||||
scoreIds:
|
||||
type: optional<string>
|
||||
docs: Comma-separated list of score IDs to limit the results to.
|
||||
configId:
|
||||
type: optional<string>
|
||||
docs: Retrieve only scores with a specific configId.
|
||||
dataType:
|
||||
type: optional<commons.ScoreDataType>
|
||||
docs: Retrieve only scores with a specific dataType.
|
||||
response: Scores
|
||||
get-by-id:
|
||||
docs: Get a score
|
||||
@@ -66,9 +76,55 @@ types:
|
||||
id: optional<string>
|
||||
traceId: string
|
||||
name: string
|
||||
value: double
|
||||
value:
|
||||
type: commons.CreateScoreValue
|
||||
docs: The value of the score. Must be passed as string for categorical scores, and numeric for boolean and numeric scores. Boolean score values must equal either 1 or 0 (true or false)
|
||||
observationId: optional<string>
|
||||
comment: optional<string>
|
||||
dataType:
|
||||
type: optional<commons.ScoreDataType>
|
||||
docs: The data type of the score. When passing a configId this field is inferred. Otherwise, this field must be passed or will default to numeric.
|
||||
configId:
|
||||
type: optional<string>
|
||||
docs: Reference a score config on a score. The unique langfuse identifier of a score config. When passing this field, the dataType and stringValue fields are automatically populated.
|
||||
examples:
|
||||
- value:
|
||||
name: "novelty"
|
||||
value: 0.9
|
||||
traceId: "cdef-1234-5678-90ab"
|
||||
- value:
|
||||
name: "consistency"
|
||||
value: 1.2
|
||||
dataType: "NUMERIC"
|
||||
traceId: "cdef-1234-5678-90ab"
|
||||
- value:
|
||||
name: "accuracy"
|
||||
value: 0.9
|
||||
dataType: "NUMERIC"
|
||||
configId: "9203-4567-89ab-cdef"
|
||||
traceId: "cdef-1234-5678-90ab"
|
||||
- value:
|
||||
name: "toxicity"
|
||||
value: "not toxic"
|
||||
traceId: "cdef-1234-5678-90ab"
|
||||
- value:
|
||||
name: "correctness"
|
||||
value: "partially correct"
|
||||
dataType: "CATEGORICAL"
|
||||
configId: "1234-5678-90ab-cdef"
|
||||
traceId: "cdef-1234-5678-90ab"
|
||||
- value:
|
||||
name: "hallucination"
|
||||
value: 0
|
||||
dataType: "BOOLEAN"
|
||||
traceId: "cdef-1234-5678-90ab"
|
||||
- value:
|
||||
name: "helpfulness"
|
||||
value: 1
|
||||
dataType: "BOOLEAN"
|
||||
configId: "1234-5678-90ab-cdef"
|
||||
traceId: "cdef-1234-5678-90ab"
|
||||
|
||||
Scores:
|
||||
properties:
|
||||
data: list<commons.Score>
|
||||
|
||||
@@ -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
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "langfuse",
|
||||
"version": "2.53.0",
|
||||
"version": "2.60.0",
|
||||
"author": "engineering@langfuse.com",
|
||||
"license": "MIT",
|
||||
"private": true,
|
||||
|
||||
@@ -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,16 +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"
|
||||
@@ -63,6 +69,7 @@
|
||||
"@repo/typescript-config": "*",
|
||||
"@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.12.0",
|
||||
@@ -80,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"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -98,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;
|
||||
@@ -204,7 +219,7 @@ export type LlmApiKeys = {
|
||||
display_secret_key: string;
|
||||
secret_key: string;
|
||||
base_url: string | null;
|
||||
custom_models: string[];
|
||||
custom_models: Generated<string[]>;
|
||||
with_default_models: Generated<boolean>;
|
||||
project_id: string;
|
||||
};
|
||||
@@ -249,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;
|
||||
@@ -259,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;
|
||||
};
|
||||
@@ -338,7 +357,7 @@ export type Score = {
|
||||
timestamp: Generated<Timestamp>;
|
||||
project_id: string;
|
||||
name: string;
|
||||
value: number;
|
||||
value: number | null;
|
||||
source: ScoreSource;
|
||||
author_user_id: string | null;
|
||||
comment: string | null;
|
||||
@@ -442,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;
|
||||
|
||||
-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';
|
||||
-5
@@ -1,5 +0,0 @@
|
||||
-- does not include pricing yet, will be added as soon as it is calculated at ingestion time
|
||||
|
||||
INSERT INTO "public"."models" ("id", "model_name", "match_pattern", "unit") VALUES ('clx30djsn0000w9mzebiv41we', 'gemini-1.5-flash', '(?i)^(gemini-1.5-flash)(@[a-zA-Z0-9]+)?$', 'CHARACTERS');
|
||||
|
||||
INSERT INTO "public"."models" ("id", "model_name", "match_pattern", "unit") VALUES ('clx30hkrx0000w9mz7lqi0ial', 'gemini-1.5-pro', '(?i)^(gemini-1.5-pro)(@[a-zA-Z0-9]+)?$', 'CHARACTERS');
|
||||
+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';
|
||||
+3
-2
@@ -10,7 +10,7 @@ INSERT INTO score_configs (id,
|
||||
max_value,
|
||||
description)
|
||||
SELECT
|
||||
gen_random_uuid () AS id,
|
||||
md5(random()::text || clock_timestamp()::text || s.project_id::text)::uuid AS id,
|
||||
s.project_id,
|
||||
'manual-score',
|
||||
'NUMERIC',
|
||||
@@ -24,6 +24,7 @@ FROM ( SELECT DISTINCT
|
||||
scores
|
||||
WHERE
|
||||
name = 'manual-score'
|
||||
AND config_id IS NULL
|
||||
AND source = 'ANNOTATION') s
|
||||
WHERE
|
||||
NOT EXISTS (
|
||||
@@ -47,4 +48,4 @@ FROM
|
||||
WHERE
|
||||
scores.project_id = pc.project_id
|
||||
AND scores.name = 'manual-score';
|
||||
COMMIT;
|
||||
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';
|
||||
+9
@@ -0,0 +1,9 @@
|
||||
-- Add AWS Bedrock model names for Anthropic models
|
||||
|
||||
UPDATE "models" SET "match_pattern" = '(?i)^(claude-3-haiku-20240307|anthropic\.claude-3-haiku-20240307-v1:0|claude-3-haiku@20240307)$' WHERE "id" = 'cltr0w45b000008k1407o9qv1';
|
||||
|
||||
UPDATE "models" SET "match_pattern" = '(?i)^(claude-3-sonnet-20240229|anthropic\.claude-3-sonnet-20240229-v1:0|claude-3-sonnet@20240229)$' WHERE "id" = 'cltgy0pp6000108le56se7bl3';
|
||||
|
||||
UPDATE "models" SET "match_pattern" = '(?i)^(claude-3-opus-20240229|anthropic\.claude-3-opus-20240229-v1:0|claude-3-opus@20240229)$' WHERE "id" = 'cltgy0iuw000008le3vod1hhy';
|
||||
|
||||
UPDATE "models" SET "match_pattern" = '(?i)^(claude-3-5-sonnet-20240620|anthropic\.claude-3-5-sonnet-20240620-v1:0|claude-3-5-sonnet@20240620)$' WHERE "id" = 'clxt0n0m60000pumz1j5b7zsf';
|
||||
+88
@@ -0,0 +1,88 @@
|
||||
DROP VIEW IF EXISTS "observations_view"; -- Drop view as column was added in 20240528214726_add_cursor_new_columns_observations and update view must have same columns
|
||||
CREATE VIEW "observations_view" AS -- Specify the columns that should be returned in the view, as calculated columns are added but exist in the observations table already
|
||||
SELECT
|
||||
o.id,
|
||||
o.name,
|
||||
o.start_time,
|
||||
o.end_time,
|
||||
o.parent_observation_id,
|
||||
o.type,
|
||||
o.trace_id,
|
||||
o.metadata,
|
||||
o.model,
|
||||
o."modelParameters",
|
||||
o.input,
|
||||
o.output,
|
||||
o.level,
|
||||
o.status_message,
|
||||
o.completion_start_time,
|
||||
o.completion_tokens,
|
||||
o.prompt_tokens,
|
||||
o.total_tokens,
|
||||
o.version,
|
||||
o.project_id,
|
||||
o.created_at,
|
||||
o.unit,
|
||||
o.prompt_id,
|
||||
o.input_cost,
|
||||
o.output_cost,
|
||||
o.total_cost,
|
||||
o.internal_model,
|
||||
m.id AS "model_id",
|
||||
m.start_date AS "model_start_date",
|
||||
m.input_price,
|
||||
m.output_price,
|
||||
m.total_price,
|
||||
m.tokenizer_config AS "tokenizer_config",
|
||||
CASE
|
||||
WHEN o.calculated_input_cost IS NULL AND o.input_cost IS NULL AND o.output_cost IS NULL AND o.total_cost IS NULL THEN
|
||||
o.prompt_tokens::decimal * m.input_price
|
||||
ELSE
|
||||
COALESCE(o.calculated_input_cost, o.input_cost)
|
||||
END AS "calculated_input_cost",
|
||||
CASE
|
||||
WHEN o.calculated_output_cost IS NULL AND o.input_cost IS NULL AND o.output_cost IS NULL AND o.total_cost IS NULL THEN
|
||||
o.completion_tokens::decimal * m.output_price
|
||||
ELSE
|
||||
COALESCE(o.calculated_output_cost, o.output_cost)
|
||||
END AS "calculated_output_cost",
|
||||
CASE
|
||||
WHEN o.calculated_total_cost IS NULL AND o.input_cost IS NULL AND o.output_cost IS NULL AND o.total_cost IS NULL THEN
|
||||
CASE
|
||||
WHEN m.total_price IS NOT NULL AND o.total_tokens IS NOT NULL THEN
|
||||
m.total_price * o.total_tokens
|
||||
ELSE
|
||||
o.prompt_tokens::decimal * m.input_price +
|
||||
o.completion_tokens::decimal * m.output_price
|
||||
END
|
||||
ELSE
|
||||
COALESCE(o.calculated_total_cost, o.total_cost)
|
||||
END AS "calculated_total_cost",
|
||||
CASE WHEN o.end_time IS NULL THEN NULL ELSE (EXTRACT(EPOCH FROM o."end_time") - EXTRACT(EPOCH FROM o."start_time"))::double precision END AS "latency",
|
||||
CASE WHEN o.completion_start_time IS NOT NULL AND o.start_time IS NOT NULL THEN EXTRACT(EPOCH FROM (completion_start_time - start_time))::double precision ELSE NULL END as "time_to_first_token"
|
||||
|
||||
FROM
|
||||
observations o
|
||||
LEFT JOIN LATERAL (
|
||||
SELECT
|
||||
models.*
|
||||
FROM
|
||||
models
|
||||
WHERE (models.project_id = o.project_id OR models.project_id IS NULL)
|
||||
AND models.model_name = o.internal_model
|
||||
AND (models.start_date < o.start_time OR models.start_date IS NULL)
|
||||
AND o.unit::TEXT = models.unit
|
||||
ORDER BY
|
||||
models.project_id ASC, -- in postgres, NULLs are sorted last when ordering ASC
|
||||
models.start_date DESC NULLS LAST -- now, NULLs are sorted last when ordering DESC as well
|
||||
LIMIT 1
|
||||
) m ON TRUE
|
||||
|
||||
|
||||
-- requirements:
|
||||
-- 1. The view should return all columns from the observations table
|
||||
-- 2. The view should match with only one model for each observation if:
|
||||
-- a. The model has the same project_id as the observation, otherwise the model without project_id.
|
||||
-- b. The model has the same model_name as the observation
|
||||
-- c. The model has a start_date that is less than the observation start_time, otherwise the model without start_date
|
||||
-- d. The model has the same unit as the observation
|
||||
+2
@@ -0,0 +1,2 @@
|
||||
-- AlterTable
|
||||
ALTER TABLE "scores" ALTER COLUMN "value" DROP NOT NULL;
|
||||
@@ -115,6 +115,7 @@ model Project {
|
||||
PosthogIntegration PosthogIntegration[]
|
||||
Score Score[]
|
||||
scoreConfig ScoreConfig[]
|
||||
BatchExport BatchExport[]
|
||||
|
||||
@@map("projects")
|
||||
}
|
||||
@@ -149,7 +150,7 @@ model LlmApiKeys {
|
||||
displaySecretKey String @map("display_secret_key")
|
||||
secretKey String @map("secret_key")
|
||||
baseURL String? @map("base_url")
|
||||
customModels String[] @map("custom_models")
|
||||
customModels String[] @default([]) @map("custom_models")
|
||||
withDefaultModels Boolean @default(true) @map("with_default_models")
|
||||
|
||||
projectId String @map("project_id")
|
||||
@@ -241,14 +242,12 @@ model Trace {
|
||||
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")
|
||||
@@ -300,24 +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")
|
||||
model String?
|
||||
internalModel String? @map("internal_model")
|
||||
internalModelId String? @map("internal_model_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")
|
||||
prompt Prompt? @relation(fields: [promptId], onDelete: SetNull, references: [id])
|
||||
promptId String? @map("prompt_id") // no fk constraint, prompt can be deleted
|
||||
|
||||
@@unique([id, projectId])
|
||||
@@index([projectId, internalModel, startTime, unit])
|
||||
@@ -328,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")
|
||||
@@ -403,14 +410,14 @@ model Score {
|
||||
projectId String @map("project_id")
|
||||
project Project @relation(fields: [projectId], references: [id], onDelete: Cascade)
|
||||
name String
|
||||
value Float
|
||||
value Float? // always defined if data type is NUMERIC or BOOLEAN, optional for CATEGORICAL
|
||||
source ScoreSource
|
||||
authorUserId String? @map("author_user_id")
|
||||
comment String?
|
||||
traceId String @map("trace_id")
|
||||
observationId String? @map("observation_id")
|
||||
configId String? @map("config_id")
|
||||
stringValue String? @map("string_value")
|
||||
stringValue String? @map("string_value") // always defined if data type is CATEGORICAL or BOOLEAN, null for NUMERIC
|
||||
createdAt DateTime @default(now()) @map("created_at")
|
||||
updatedAt DateTime @default(now()) @updatedAt @map("updated_at")
|
||||
dataType ScoreDataType @default(NUMERIC) @map("data_type")
|
||||
@@ -428,7 +435,6 @@ model Score {
|
||||
@@index([observationId], type: Hash)
|
||||
@@index([source])
|
||||
@@index([createdAt])
|
||||
@@index([updatedAt])
|
||||
@@map("scores")
|
||||
}
|
||||
|
||||
@@ -589,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])
|
||||
@@ -782,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")
|
||||
}
|
||||
|
||||
@@ -12,11 +12,16 @@ 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;
|
||||
|
||||
type ConfigCategory = {
|
||||
label: string;
|
||||
value: number;
|
||||
};
|
||||
|
||||
const options = {
|
||||
environment: { type: "string" },
|
||||
} as const;
|
||||
@@ -377,11 +382,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];
|
||||
}
|
||||
|
||||
@@ -395,11 +399,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];
|
||||
}
|
||||
|
||||
@@ -413,11 +416,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];
|
||||
}
|
||||
|
||||
@@ -431,11 +433,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];
|
||||
}
|
||||
|
||||
@@ -448,11 +449,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];
|
||||
}
|
||||
}
|
||||
@@ -464,7 +464,15 @@ function createObjects(
|
||||
project1: Project,
|
||||
project2: Project,
|
||||
promptIds: Map<string, string[]>,
|
||||
configIdsAndNames: Map<string, { name: string; id: string }[]>
|
||||
configParams: Map<
|
||||
string,
|
||||
{
|
||||
name: string;
|
||||
id: string;
|
||||
dataType: ScoreDataType;
|
||||
categories: ConfigCategory[] | null;
|
||||
}[]
|
||||
>
|
||||
) {
|
||||
const traces: Prisma.TraceCreateManyInput[] = [];
|
||||
const observations: Prisma.ObservationCreateManyInput[] = [];
|
||||
@@ -523,13 +531,34 @@ function createObjects(
|
||||
|
||||
traces.push(trace);
|
||||
|
||||
const configArray = configIdsAndNames.get(projectId) ?? [];
|
||||
const configArray = configParams.get(projectId) ?? [];
|
||||
const randomIndex = Math.floor(Math.random() * 3);
|
||||
const config =
|
||||
configArray.length >= randomIndex - 1 && configArray[randomIndex];
|
||||
const { name: annotationScoreName, id: configId } = config || {
|
||||
const {
|
||||
name: annotationScoreName,
|
||||
id: configId,
|
||||
dataType,
|
||||
categories,
|
||||
} = config || {
|
||||
name: "manual-score",
|
||||
id: undefined,
|
||||
dataType: ScoreDataType.NUMERIC,
|
||||
categories: null,
|
||||
};
|
||||
|
||||
const value = Math.floor(Math.random() * 2);
|
||||
const scoreNumericAndStringValue = {
|
||||
...(dataType === ScoreDataType.NUMERIC && { value }),
|
||||
...(dataType === ScoreDataType.CATEGORICAL && {
|
||||
value,
|
||||
stringValue: categories?.find((category) => category.value === value)
|
||||
?.label,
|
||||
}),
|
||||
...(dataType === ScoreDataType.BOOLEAN && {
|
||||
value,
|
||||
stringValue: value === 1 ? "True" : "False",
|
||||
}),
|
||||
};
|
||||
|
||||
const traceScores = [
|
||||
@@ -538,12 +567,12 @@ function createObjects(
|
||||
{
|
||||
traceId: trace.id,
|
||||
name: annotationScoreName,
|
||||
value: Math.floor(Math.random() * 3) - 1,
|
||||
timestamp: traceTs,
|
||||
source: ScoreSource.ANNOTATION,
|
||||
projectId,
|
||||
authorUserId: `user-${i}`,
|
||||
dataType: ScoreDataType.NUMERIC,
|
||||
dataType,
|
||||
...scoreNumericAndStringValue,
|
||||
...(configId ? { configId } : {}),
|
||||
},
|
||||
]
|
||||
@@ -561,6 +590,20 @@ function createObjects(
|
||||
},
|
||||
]
|
||||
: []),
|
||||
...(Math.random() < 0.8
|
||||
? [
|
||||
{
|
||||
traceId: trace.id,
|
||||
name: "Completeness",
|
||||
timestamp: traceTs,
|
||||
source: ScoreSource.API,
|
||||
projectId,
|
||||
dataType: ScoreDataType.CATEGORICAL,
|
||||
stringValue:
|
||||
Math.floor(Math.random() * 2) === 1 ? "Fully" : "Partially",
|
||||
},
|
||||
]
|
||||
: []),
|
||||
];
|
||||
|
||||
scores.push(...traceScores);
|
||||
@@ -981,8 +1024,15 @@ async function generatePrompts(project: Project) {
|
||||
}
|
||||
|
||||
async function generateConfigsForProject(projects: Project[]) {
|
||||
const projectIdsToConfigs: Map<string, { name: string; id: string }[]> =
|
||||
new Map();
|
||||
const projectIdsToConfigs: Map<
|
||||
string,
|
||||
{
|
||||
name: string;
|
||||
id: string;
|
||||
dataType: ScoreDataType;
|
||||
categories: ConfigCategory[] | null;
|
||||
}[]
|
||||
> = new Map();
|
||||
|
||||
await Promise.all(
|
||||
projects.map(async (project) => {
|
||||
@@ -994,7 +1044,12 @@ async function generateConfigsForProject(projects: Project[]) {
|
||||
}
|
||||
|
||||
async function generateConfigs(project: Project) {
|
||||
const configNameAndId: { name: string; id: string }[] = [];
|
||||
const configNameAndId: {
|
||||
name: string;
|
||||
id: string;
|
||||
dataType: ScoreDataType;
|
||||
categories: ConfigCategory[] | null;
|
||||
}[] = [];
|
||||
|
||||
const configs = [
|
||||
{
|
||||
@@ -1051,7 +1106,12 @@ async function generateConfigs(project: Project) {
|
||||
id: config.id,
|
||||
},
|
||||
});
|
||||
configNameAndId.push({ name: config.name, id: config.id });
|
||||
configNameAndId.push({
|
||||
name: config.name,
|
||||
id: config.id,
|
||||
dataType: config.dataType,
|
||||
categories: config.categories ?? null,
|
||||
});
|
||||
}
|
||||
|
||||
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 InternalServerError extends BaseError {
|
||||
constructor(description = "Internal Server Error") {
|
||||
super("InternalServerError", 500, description, true);
|
||||
}
|
||||
}
|
||||
@@ -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,7 +1,8 @@
|
||||
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";
|
||||
export { ApiError } from "./ApiError";
|
||||
export { InternalServerError } from "./InternalServerError";
|
||||
|
||||
@@ -0,0 +1,59 @@
|
||||
import z from "zod";
|
||||
import { type ScoreDataType } from "../../db";
|
||||
|
||||
const NUMERIC: ScoreDataType = "NUMERIC";
|
||||
const CATEGORICAL: ScoreDataType = "CATEGORICAL";
|
||||
const BOOLEAN: ScoreDataType = "BOOLEAN";
|
||||
|
||||
export const availableDataTypes = [NUMERIC, CATEGORICAL, BOOLEAN] as const;
|
||||
|
||||
const NumericData = z.object({
|
||||
value: z.number(),
|
||||
stringValue: z.undefined().nullish(),
|
||||
dataType: z.literal("NUMERIC"),
|
||||
});
|
||||
|
||||
const CategoricalData = z.object({
|
||||
value: z.number().optional().nullish(),
|
||||
stringValue: z.string(),
|
||||
dataType: z.literal("CATEGORICAL"),
|
||||
});
|
||||
|
||||
const BooleanData = z.object({
|
||||
value: z.number(),
|
||||
stringValue: z.string(),
|
||||
dataType: z.literal("BOOLEAN"),
|
||||
});
|
||||
|
||||
const CreateAnnotationScoreBase = z.object({
|
||||
name: z.string(),
|
||||
projectId: z.string(),
|
||||
traceId: z.string(),
|
||||
configId: z.string().optional(),
|
||||
observationId: z.string().optional(),
|
||||
comment: z.string().optional().nullish(),
|
||||
});
|
||||
|
||||
const UpdateAnnotationScoreBase = CreateAnnotationScoreBase.extend({
|
||||
id: z.string(),
|
||||
});
|
||||
|
||||
/**
|
||||
* CreateAnnotationScoreData is only used for annotation scores created via the UI.
|
||||
* For langfuse score types please refer to `web/src/features/public-api/types/scores.ts`
|
||||
*/
|
||||
export const CreateAnnotationScoreData = z.discriminatedUnion("dataType", [
|
||||
CreateAnnotationScoreBase.merge(NumericData),
|
||||
CreateAnnotationScoreBase.merge(CategoricalData),
|
||||
CreateAnnotationScoreBase.merge(BooleanData),
|
||||
]);
|
||||
|
||||
/**
|
||||
* UpdateAnnotationScoreData is only used for annotation scores updated via the UI
|
||||
* For langfuse score types please refer to `web/src/features/public-api/types/scores.ts`
|
||||
*/
|
||||
export const UpdateAnnotationScoreData = z.discriminatedUnion("dataType", [
|
||||
UpdateAnnotationScoreBase.merge(NumericData),
|
||||
UpdateAnnotationScoreBase.merge(CategoricalData),
|
||||
UpdateAnnotationScoreBase.merge(BooleanData),
|
||||
]);
|
||||
@@ -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;
|
||||
@@ -145,15 +145,75 @@ export const UpdateGenerationBody = UpdateSpanBody.extend({
|
||||
return false;
|
||||
});
|
||||
|
||||
export const ScoreBody = z.object({
|
||||
const BaseScoreBody = z.object({
|
||||
id: z.string().nullish(),
|
||||
name: NonEmptyString,
|
||||
value: z.number(),
|
||||
traceId: z.string(),
|
||||
observationId: z.string().nullish(),
|
||||
comment: z.string().nullish(),
|
||||
});
|
||||
|
||||
/**
|
||||
* ScoreBody exactly mirrors `PostScoresBody` in the public API. Please refer there for source of truth.
|
||||
*/
|
||||
export const ScoreBody = z
|
||||
.discriminatedUnion("dataType", [
|
||||
BaseScoreBody.merge(
|
||||
z.object({
|
||||
value: z.number(),
|
||||
dataType: z.literal("NUMERIC"),
|
||||
configId: z.string().nullish(),
|
||||
})
|
||||
),
|
||||
BaseScoreBody.merge(
|
||||
z.object({
|
||||
value: z.string(),
|
||||
dataType: z.literal("CATEGORICAL"),
|
||||
configId: z.string().nullish(),
|
||||
})
|
||||
),
|
||||
BaseScoreBody.merge(
|
||||
z.object({
|
||||
value: z.number().refine((val) => val === 0 || val === 1, {
|
||||
message: "Value must be either 0 or 1",
|
||||
}),
|
||||
dataType: z.literal("BOOLEAN"),
|
||||
configId: z.string().nullish(),
|
||||
})
|
||||
),
|
||||
BaseScoreBody.merge(
|
||||
z.object({
|
||||
value: z.union([z.string(), z.number()]),
|
||||
dataType: z.undefined(),
|
||||
configId: z.string().nullish(),
|
||||
})
|
||||
),
|
||||
])
|
||||
.superRefine((data, ctx) => {
|
||||
if (data.dataType) {
|
||||
if (typeof data.value === "number") {
|
||||
if (data.dataType === "CATEGORICAL") {
|
||||
ctx.addIssue({
|
||||
code: z.ZodIssueCode.custom,
|
||||
message: `Value must be a string for data type ${data.dataType}`,
|
||||
});
|
||||
}
|
||||
} else if (typeof data.value === "string") {
|
||||
if (data.dataType === "NUMERIC") {
|
||||
ctx.addIssue({
|
||||
code: z.ZodIssueCode.custom,
|
||||
message: `Value must be a number for data type ${data.dataType}`,
|
||||
});
|
||||
} else if (data.dataType === "BOOLEAN") {
|
||||
ctx.addIssue({
|
||||
code: z.ZodIssueCode.custom,
|
||||
message: `Value must number equal to either 0 or 1 for data type ${data.dataType}`,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// LEGACY, only required for backwards compatibility
|
||||
export const LegacySpanPostSchema = z.object({
|
||||
id: z.string().nullish(),
|
||||
|
||||
@@ -1,8 +1,5 @@
|
||||
import { Prisma } from "@prisma/client";
|
||||
import {
|
||||
ColumnDefinition,
|
||||
type TableNames,
|
||||
} from "./interfaces/tableDefinition";
|
||||
import { ColumnDefinition, type TableNames } from "./tableDefinitions";
|
||||
import { FilterState } from "./types";
|
||||
import { filterOperators } from "./interfaces/filters";
|
||||
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
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";
|
||||
@@ -20,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;
|
||||
};
|
||||
};
|
||||
|
||||
@@ -65,7 +65,7 @@ export function CustomSSOProvider<P extends CustomSSOUser>(
|
||||
name: "CustomSSOProvider",
|
||||
type: "oauth",
|
||||
wellKnown: `${options.issuer}/.well-known/openid-configuration`,
|
||||
authorization: { params: { scope: "openid email profile" } },
|
||||
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) {
|
||||
|
||||
@@ -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";
|
||||
@@ -65,6 +65,7 @@ export const openAIModels = [
|
||||
export type OpenAIModel = (typeof openAIModels)[number];
|
||||
|
||||
export const anthropicModels = [
|
||||
"claude-3-5-sonnet-20240620",
|
||||
"claude-3-opus-20240229",
|
||||
"claude-3-sonnet-20240229",
|
||||
"claude-3-haiku-20240307",
|
||||
|
||||
+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") {
|
||||
@@ -58,7 +58,32 @@ export const optionalPaginationZod = {
|
||||
.optional(),
|
||||
};
|
||||
|
||||
export const queryStringZod = z
|
||||
.string()
|
||||
.transform((val) => decodeURIComponent(val));
|
||||
|
||||
export const paginationMetaResponseZod = z.object({
|
||||
page: z.number().int().positive(),
|
||||
limit: z.number().int().positive(),
|
||||
totalItems: z.number().int().nonnegative(),
|
||||
totalPages: z.number().int().nonnegative(),
|
||||
});
|
||||
|
||||
export const noHtmlRegex = /<[^>]*>/;
|
||||
export const noHtmlCheck = (value: string) => !noHtmlRegex.test(value);
|
||||
|
||||
export const NonEmptyString = z.string().min(1);
|
||||
|
||||
/**
|
||||
* Validates an object against a Zod schema and helps with IDE type warnings.
|
||||
*
|
||||
* @param schema - The Zod schema to validate against.
|
||||
* @param object - The object to be validated.
|
||||
* @returns The parsed object if validation is successful.
|
||||
*/
|
||||
export const validateZodSchema = <T extends z.ZodTypeAny>(
|
||||
schema: T,
|
||||
object: z.infer<T>
|
||||
): z.infer<T> => {
|
||||
return schema.parse(object);
|
||||
};
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
{
|
||||
"extends": "@repo/typescript-config/base.json",
|
||||
"compilerOptions": {
|
||||
"jsx": "react",
|
||||
"moduleResolution": "NodeNext",
|
||||
"module": "NodeNext",
|
||||
"lib": ["ES2020"],
|
||||
|
||||
Generated
+1808
-1289
File diff suppressed because it is too large
Load Diff
+3
-1
@@ -9,7 +9,9 @@
|
||||
"start": {
|
||||
"dependsOn": ["^start"]
|
||||
},
|
||||
"db:migrate": {},
|
||||
"db:migrate": {
|
||||
"cache": false
|
||||
},
|
||||
"db:deploy": {},
|
||||
"db:reset": {},
|
||||
"db:push": {},
|
||||
|
||||
+15
-5
@@ -1,10 +1,10 @@
|
||||
FROM node:20-alpine3.20 AS alpine
|
||||
FROM --platform=${BUILDPLATFORM:-linux/amd64} node:20-alpine3.20 AS alpine
|
||||
|
||||
# It's important to update the index before installing packages to ensure you're getting the latest versions.
|
||||
# Check https://github.com/nodejs/docker-node/tree/b4117f9333da4138b03a546ec926ef50a31506c3#nodealpine to understand why libc6-compat might be needed.
|
||||
RUN apk update && apk upgrade --no-cache libcrypto3 libssl3 libc6-compat busybox ssl_client
|
||||
|
||||
FROM alpine AS base
|
||||
FROM --platform=${BUILDPLATFORM:-linux/amd64} alpine AS base
|
||||
RUN npm install turbo@^1.13.3 --global
|
||||
ENV PNPM_HOME="/pnpm"
|
||||
ENV PATH="$PNPM_HOME:$PATH"
|
||||
@@ -12,7 +12,7 @@ RUN corepack enable
|
||||
RUN corepack prepare pnpm@8.15.5 --activate
|
||||
|
||||
|
||||
FROM base AS pruner
|
||||
FROM --platform=${BUILDPLATFORM:-linux/amd64} base AS pruner
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
@@ -20,7 +20,7 @@ COPY . .
|
||||
RUN turbo prune --scope=web --docker
|
||||
|
||||
|
||||
FROM base AS builder
|
||||
FROM --platform=${BUILDPLATFORM:-linux/amd64} base AS builder
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
@@ -33,6 +33,15 @@ RUN pnpm install --frozen-lockfile
|
||||
|
||||
ENV DOCKER_BUILD 1
|
||||
|
||||
# pass public variables in build step
|
||||
ARG NEXT_PUBLIC_LANGFUSE_CLOUD_REGION
|
||||
ARG NEXT_PUBLIC_DEMO_PROJECT_ID
|
||||
ARG NEXT_PUBLIC_SIGN_UP_DISABLED
|
||||
ARG NEXT_PUBLIC_TURNSTILE_SITE_KEY
|
||||
ARG NEXT_PUBLIC_POSTHOG_KEY
|
||||
ARG NEXT_PUBLIC_POSTHOG_HOST
|
||||
ARG NEXT_PUBLIC_CRISP_WEBSITE_ID
|
||||
ARG LANFUSE_WEB_MIGRATION_DISABLED
|
||||
|
||||
# Copy source code of isolated subworkspace
|
||||
COPY --from=pruner /app/out/full/ .
|
||||
@@ -48,7 +57,7 @@ ENV NEXT_TELEMETRY_DISABLED 1
|
||||
RUN turbo run build --filter=web
|
||||
|
||||
# Production image, copy all the files and run next
|
||||
FROM base AS runner
|
||||
FROM --platform=${TARGETPLATFORM:-linux/amd64} base AS runner
|
||||
|
||||
RUN apk add --no-cache dumb-init
|
||||
|
||||
@@ -78,6 +87,7 @@ COPY --from=builder --chown=nextjs:nodejs /app/web/public ./web/public
|
||||
COPY --from=builder --chown=nextjs:nodejs /app/packages/shared/prisma ./packages/shared/prisma
|
||||
|
||||
COPY --chown=nextjs:nodejs ./web/entrypoint.sh ./web/entrypoint.sh
|
||||
COPY --chown=nextjs:nodejs ./packages/shared/scripts/cleanup.sql ./packages/shared/scripts/cleanup.sql
|
||||
RUN chmod +x ./web/entrypoint.sh
|
||||
|
||||
USER nextjs
|
||||
|
||||
+8
-2
@@ -1,5 +1,6 @@
|
||||
#!/bin/sh
|
||||
|
||||
# Run cleanup script before running migrations
|
||||
# Check if DATABASE_URL is not set
|
||||
if [ -z "$DATABASE_URL" ]; then
|
||||
# Check if all required variables are provided
|
||||
@@ -18,8 +19,13 @@ if [ -z "$DIRECT_URL" ]; then
|
||||
export DIRECT_URL=$DATABASE_URL
|
||||
fi
|
||||
|
||||
# Apply migrations
|
||||
prisma migrate deploy --schema=./packages/shared/prisma/schema.prisma
|
||||
# Always execute the scripts, except when disabled.
|
||||
if [ "$LANGFUSE_AUTO_POSTGRES_MIGRATION_DISABLED" != "true" ]; then
|
||||
prisma db execute --url "$DIRECT_URL" --file "./packages/shared/scripts/cleanup.sql"
|
||||
|
||||
# Apply migrations
|
||||
prisma migrate deploy --schema=./packages/shared/prisma/schema.prisma
|
||||
fi
|
||||
status=$?
|
||||
|
||||
# If migration fails (returns non-zero exit status), exit script with that status
|
||||
|
||||
+39
-37
@@ -11,8 +11,8 @@ import { env } from "./src/env.mjs";
|
||||
* img-src https to allow loading images from SSO providers
|
||||
*/
|
||||
const cspHeader = `
|
||||
default-src 'self' https://ph.langfuse.com https://*.posthog.com wss://*.crisp.chat https://*.crisp.chat;
|
||||
script-src 'self' 'unsafe-eval' https://*.crisp.chat https://challenges.cloudflare.com https://ph.langfuse.com https://static.cloudflareinsights.com https://*.stripe.com;
|
||||
default-src 'self' https://*.langfuse.com https://*.posthog.com https://*.sentry.io wss://*.crisp.chat https://*.crisp.chat;
|
||||
script-src 'self' 'unsafe-eval' https://*.langfuse.com https://*.crisp.chat https://challenges.cloudflare.com https://*.sentry.io https://ph.langfuse.com https://static.cloudflareinsights.com https://*.stripe.com;
|
||||
style-src 'self' 'unsafe-inline' https://*.crisp.chat;
|
||||
img-src 'self' https: blob: data:;
|
||||
font-src 'self' https://*.crisp.chat;
|
||||
@@ -29,6 +29,9 @@ const cspHeader = `
|
||||
const nextConfig = {
|
||||
transpilePackages: ["@langfuse/shared"],
|
||||
reactStrictMode: true,
|
||||
experimental: {
|
||||
instrumentationHook: true,
|
||||
},
|
||||
|
||||
/**
|
||||
* If you have `experimental: { appDir: true }` set, then you must comment the below `i18n` config
|
||||
@@ -77,22 +80,22 @@ const nextConfig = {
|
||||
// Required to check authentication status from langfuse.com
|
||||
...(env.NEXT_PUBLIC_LANGFUSE_CLOUD_REGION !== undefined
|
||||
? [
|
||||
{
|
||||
source: "/api/auth/session",
|
||||
headers: [
|
||||
{
|
||||
key: "Access-Control-Allow-Origin",
|
||||
value: "https://langfuse.com",
|
||||
},
|
||||
{ key: "Access-Control-Allow-Credentials", value: "true" },
|
||||
{ key: "Access-Control-Allow-Methods", value: "GET,POST" },
|
||||
{
|
||||
key: "Access-Control-Allow-Headers",
|
||||
value: "Content-Type, Authorization",
|
||||
},
|
||||
],
|
||||
},
|
||||
]
|
||||
{
|
||||
source: "/api/auth/session",
|
||||
headers: [
|
||||
{
|
||||
key: "Access-Control-Allow-Origin",
|
||||
value: "https://langfuse.com",
|
||||
},
|
||||
{ key: "Access-Control-Allow-Credentials", value: "true" },
|
||||
{ key: "Access-Control-Allow-Methods", value: "GET,POST" },
|
||||
{
|
||||
key: "Access-Control-Allow-Headers",
|
||||
value: "Content-Type, Authorization",
|
||||
},
|
||||
],
|
||||
},
|
||||
]
|
||||
: []),
|
||||
];
|
||||
},
|
||||
@@ -106,26 +109,9 @@ const nextConfig = {
|
||||
|
||||
return config;
|
||||
},
|
||||
sentry: {
|
||||
// See the sections below for information on the following options:
|
||||
// 'Configure Source Maps':
|
||||
// - disableServerWebpackPlugin
|
||||
// - disableClientWebpackPlugin
|
||||
// - hideSourceMaps
|
||||
hideSourceMaps: true,
|
||||
// - widenClientFileUpload
|
||||
// 'Configure Legacy Browser Support':
|
||||
// - transpileClientSDK
|
||||
// 'Configure Serverside Auto-instrumentation':
|
||||
// - autoInstrumentServerFunctions
|
||||
// - excludeServerRoutes
|
||||
// 'Configure Tunneling':
|
||||
// - tunnelRoute
|
||||
tunnelRoute: "/api/monitoring-tunnel",
|
||||
},
|
||||
};
|
||||
|
||||
const sentryWebpackPluginOptions = {
|
||||
const sentryOptions = {
|
||||
// Additional config options for the Sentry Webpack plugin. Keep in mind that
|
||||
// the following options are set automatically, and overriding them is not
|
||||
// recommended:
|
||||
@@ -139,6 +125,22 @@ const sentryWebpackPluginOptions = {
|
||||
|
||||
// For all available options, see:
|
||||
// https://github.com/getsentry/sentry-webpack-plugin#options.
|
||||
|
||||
// See the sections below for information on the following options:
|
||||
// 'Configure Source Maps':
|
||||
// - disableServerWebpackPlugin
|
||||
// - disableClientWebpackPlugin
|
||||
// - hideSourceMaps
|
||||
hideSourceMaps: true,
|
||||
// - widenClientFileUpload
|
||||
// 'Configure Legacy Browser Support':
|
||||
// - transpileClientSDK
|
||||
// 'Configure Serverside Auto-instrumentation':
|
||||
// - autoInstrumentServerFunctions
|
||||
// - excludeServerRoutes
|
||||
// 'Configure Tunneling':
|
||||
// - tunnelRoute
|
||||
tunnelRoute: "/api/monitoring-tunnel",
|
||||
};
|
||||
|
||||
export default withSentryConfig(nextConfig, sentryWebpackPluginOptions);
|
||||
export default withSentryConfig(nextConfig, sentryOptions);
|
||||
|
||||
+8
-14
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "web",
|
||||
"version": "2.53.0",
|
||||
"version": "2.60.0",
|
||||
"private": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
@@ -17,13 +17,11 @@
|
||||
"test": "dotenv -e ../.env -- jest --runInBand",
|
||||
"test:watch": "dotenv -e ../.env -- jest --watch --runInBand",
|
||||
"test:e2e": "dotenv -e ../.env -- playwright test",
|
||||
"models:migrate": "dotenv -e ../.env -- tsx scripts/model-match.ts"
|
||||
"models:migrate": "dotenv -e ../.env -- tsx scripts/model-match.ts",
|
||||
"generationCost:backfill": "dotenv -e ../.env -- tsx scripts/observations-backfill-calculated-cost.ts"
|
||||
},
|
||||
"dependencies": {
|
||||
"@anthropic-ai/tokenizer": "^0.0.4",
|
||||
"@aws-sdk/client-s3": "^3.550.0",
|
||||
"@aws-sdk/lib-storage": "^3.568.0",
|
||||
"@aws-sdk/s3-request-presigner": "^3.554.0",
|
||||
"@codemirror/lang-json": "^6.0.1",
|
||||
"@codemirror/lint": "^6.8.0",
|
||||
"@headlessui/react": "1.7.18",
|
||||
@@ -59,15 +57,13 @@
|
||||
"@radix-ui/react-toggle": "^1.0.3",
|
||||
"@radix-ui/react-toggle-group": "^1.0.4",
|
||||
"@radix-ui/react-tooltip": "^1.0.7",
|
||||
"@react-email/components": "^0.0.19",
|
||||
"@react-email/render": "^0.0.15",
|
||||
"@remixicon/react": "^4.2.0",
|
||||
"@repo/eslint-config": "workspace:*",
|
||||
"@repo/typescript-config": "workspace:*",
|
||||
"@sentry/nextjs": "^8.8.0",
|
||||
"@sentry/node": "^8.8.0",
|
||||
"@sentry/profiling-node": "^8.8.0",
|
||||
"@sentry/types": "^8.8.0",
|
||||
"@sentry/nextjs": "^8.13.0",
|
||||
"@sentry/node": "^8.13.0",
|
||||
"@sentry/profiling-node": "^8.13.0",
|
||||
"@sentry/types": "^8.13.0",
|
||||
"@t3-oss/env-nextjs": "^0.10.1",
|
||||
"@tailwindcss/container-queries": "^0.1.1",
|
||||
"@tanstack/react-query": "^4.36.1",
|
||||
@@ -93,14 +89,13 @@
|
||||
"exponential-backoff": "^3.1.1",
|
||||
"js-tiktoken": "^1.0.12",
|
||||
"kysely": "^0.27.3",
|
||||
"langchain": "^0.1.36",
|
||||
"langchain": "^0.2.6",
|
||||
"lodash": "^4.17.21",
|
||||
"lucide-react": "^0.364.0",
|
||||
"next": "^14.2.3",
|
||||
"next-auth": "^4.24.7",
|
||||
"next-query-params": "^5.0.0",
|
||||
"next-themes": "^0.3.0",
|
||||
"nodemailer": "^6.9.13",
|
||||
"posthog-js": "^1.122.0",
|
||||
"posthog-node": "^3.6.3",
|
||||
"prisma": "^5.13.0",
|
||||
@@ -134,7 +129,6 @@
|
||||
"@types/jest": "^29.5.12",
|
||||
"@types/lodash": "^4.17.5",
|
||||
"@types/node": "20.10.5",
|
||||
"@types/nodemailer": "^6.4.15",
|
||||
"@types/react": "^18.2.79",
|
||||
"@types/react-dom": "^18.2.25",
|
||||
"@types/uuid": "^9.0.8",
|
||||
|
||||
@@ -59,21 +59,39 @@ components:
|
||||
type: string
|
||||
traceId:
|
||||
type: string
|
||||
example: cdef-1234-5678-90ab
|
||||
name:
|
||||
type: string
|
||||
example: novelty
|
||||
value:
|
||||
type: number
|
||||
format: double
|
||||
$ref: '#/components/schemas/CreateScoreValue'
|
||||
description: >-
|
||||
The value of the score. Must be passed as string for categorical
|
||||
scores, and numeric for boolean and numeric scores. Boolean score
|
||||
values must equal either 1 or 0 (true or false)
|
||||
observationId:
|
||||
type: string
|
||||
comment:
|
||||
type: string
|
||||
dataType:
|
||||
$ref: '#/components/schemas/ScoreDataType'
|
||||
description: >-
|
||||
When set, must match the score value's type. If not set, will be
|
||||
inferred from the score value or config
|
||||
configId:
|
||||
type: string
|
||||
description: >-
|
||||
Reference a score config on a score. When set, the score name must
|
||||
equal the config name and scores must comply with the config's range
|
||||
and data type. For categorical scores, the value must map to a
|
||||
config category. Numeric scores might be constrained by the score
|
||||
config's max and min values
|
||||
required:
|
||||
- traceId
|
||||
- name
|
||||
- value
|
||||
Score:
|
||||
title: Score
|
||||
NumericScore:
|
||||
title: NumericScore
|
||||
type: object
|
||||
properties:
|
||||
id:
|
||||
@@ -85,6 +103,9 @@ components:
|
||||
value:
|
||||
type: number
|
||||
format: double
|
||||
description: The numeric value of the score
|
||||
source:
|
||||
$ref: '#/components/schemas/ScoreSource'
|
||||
observationId:
|
||||
type: string
|
||||
timestamp:
|
||||
@@ -92,12 +113,163 @@ components:
|
||||
format: date-time
|
||||
comment:
|
||||
type: string
|
||||
configId:
|
||||
type: string
|
||||
description: >-
|
||||
Reference a score config on a score. When set, config and score name
|
||||
must be equal and value must comply to optionally defined numerical
|
||||
range
|
||||
required:
|
||||
- id
|
||||
- traceId
|
||||
- name
|
||||
- value
|
||||
- source
|
||||
- timestamp
|
||||
BooleanScore:
|
||||
title: BooleanScore
|
||||
type: object
|
||||
properties:
|
||||
id:
|
||||
type: string
|
||||
traceId:
|
||||
type: string
|
||||
name:
|
||||
type: string
|
||||
value:
|
||||
type: number
|
||||
format: double
|
||||
description: >-
|
||||
The numeric value of the score. Equals 1 for "True" and 0 for
|
||||
"False"
|
||||
stringValue:
|
||||
type: string
|
||||
description: >-
|
||||
The string representation of the score value. Is inferred from the
|
||||
numeric value and equals "True" or "False"
|
||||
source:
|
||||
$ref: '#/components/schemas/ScoreSource'
|
||||
observationId:
|
||||
type: string
|
||||
timestamp:
|
||||
type: string
|
||||
format: date-time
|
||||
comment:
|
||||
type: string
|
||||
configId:
|
||||
type: string
|
||||
description: >-
|
||||
Reference a score config on a score. When set, config and score name
|
||||
must be equal
|
||||
required:
|
||||
- id
|
||||
- traceId
|
||||
- name
|
||||
- value
|
||||
- stringValue
|
||||
- source
|
||||
- timestamp
|
||||
CategoricalScore:
|
||||
title: CategoricalScore
|
||||
type: object
|
||||
properties:
|
||||
id:
|
||||
type: string
|
||||
traceId:
|
||||
type: string
|
||||
name:
|
||||
type: string
|
||||
value:
|
||||
type: number
|
||||
format: double
|
||||
description: >-
|
||||
Only defined if a config is linked. Represents the numeric category
|
||||
mapping of the stringValue
|
||||
stringValue:
|
||||
type: string
|
||||
description: >-
|
||||
The string representation of the score value. If no config is
|
||||
linked, can be any string. Otherwise, must map to a config category
|
||||
source:
|
||||
$ref: '#/components/schemas/ScoreSource'
|
||||
observationId:
|
||||
type: string
|
||||
timestamp:
|
||||
type: string
|
||||
format: date-time
|
||||
comment:
|
||||
type: string
|
||||
configId:
|
||||
type: string
|
||||
description: >-
|
||||
Reference a score config on a score. When set, config and score name
|
||||
must be equal and stringValue must map to a config category
|
||||
required:
|
||||
- id
|
||||
- traceId
|
||||
- name
|
||||
- stringValue
|
||||
- source
|
||||
- timestamp
|
||||
Score:
|
||||
title: Score
|
||||
oneOf:
|
||||
- type: object
|
||||
allOf:
|
||||
- type: object
|
||||
properties:
|
||||
dataType:
|
||||
type: string
|
||||
enum:
|
||||
- NUMERIC
|
||||
- $ref: '#/components/schemas/NumericScore'
|
||||
required:
|
||||
- dataType
|
||||
- type: object
|
||||
allOf:
|
||||
- type: object
|
||||
properties:
|
||||
dataType:
|
||||
type: string
|
||||
enum:
|
||||
- CATEGORICAL
|
||||
- $ref: '#/components/schemas/CategoricalScore'
|
||||
required:
|
||||
- dataType
|
||||
- type: object
|
||||
allOf:
|
||||
- type: object
|
||||
properties:
|
||||
dataType:
|
||||
type: string
|
||||
enum:
|
||||
- BOOLEAN
|
||||
- $ref: '#/components/schemas/BooleanScore'
|
||||
required:
|
||||
- dataType
|
||||
ScoreSource:
|
||||
title: ScoreSource
|
||||
type: string
|
||||
enum:
|
||||
- ANNOTATION
|
||||
- API
|
||||
- EVAL
|
||||
ScoreDataType:
|
||||
title: ScoreDataType
|
||||
type: string
|
||||
enum:
|
||||
- NUMERIC
|
||||
- CATEGORICAL
|
||||
- BOOLEAN
|
||||
CreateScoreValue:
|
||||
title: CreateScoreValue
|
||||
oneOf:
|
||||
- type: string
|
||||
- type: number
|
||||
format: double
|
||||
description: >-
|
||||
The value of the score. Must be passed as string for categorical scores,
|
||||
and numeric for boolean and numeric scores
|
||||
securitySchemes:
|
||||
BearerAuth:
|
||||
type: http
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -106,6 +106,57 @@
|
||||
"body": null
|
||||
},
|
||||
"response": []
|
||||
},
|
||||
{
|
||||
"_type": "endpoint",
|
||||
"name": "List",
|
||||
"request": {
|
||||
"description": "Get dataset items",
|
||||
"url": {
|
||||
"raw": "{{baseUrl}}/api/public/dataset-items?datasetName=&sourceTraceId=&sourceObservationId=&page=&limit=",
|
||||
"host": [
|
||||
"{{baseUrl}}"
|
||||
],
|
||||
"path": [
|
||||
"api",
|
||||
"public",
|
||||
"dataset-items"
|
||||
],
|
||||
"query": [
|
||||
{
|
||||
"key": "datasetName",
|
||||
"value": "",
|
||||
"description": null
|
||||
},
|
||||
{
|
||||
"key": "sourceTraceId",
|
||||
"value": "",
|
||||
"description": null
|
||||
},
|
||||
{
|
||||
"key": "sourceObservationId",
|
||||
"value": "",
|
||||
"description": null
|
||||
},
|
||||
{
|
||||
"key": "page",
|
||||
"value": "",
|
||||
"description": "page number, starts at 1"
|
||||
},
|
||||
{
|
||||
"key": "limit",
|
||||
"value": "",
|
||||
"description": "limit of items per page"
|
||||
}
|
||||
],
|
||||
"variable": []
|
||||
},
|
||||
"header": [],
|
||||
"method": "GET",
|
||||
"auth": null,
|
||||
"body": null
|
||||
},
|
||||
"response": []
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -160,25 +211,26 @@
|
||||
"request": {
|
||||
"description": "Get all datasets",
|
||||
"url": {
|
||||
"raw": "{{baseUrl}}/api/public/datasets?page=&limit=",
|
||||
"raw": "{{baseUrl}}/api/public/v2/datasets?page=&limit=",
|
||||
"host": [
|
||||
"{{baseUrl}}"
|
||||
],
|
||||
"path": [
|
||||
"api",
|
||||
"public",
|
||||
"v2",
|
||||
"datasets"
|
||||
],
|
||||
"query": [
|
||||
{
|
||||
"key": "page",
|
||||
"value": "",
|
||||
"description": null
|
||||
"description": "page number, starts at 1"
|
||||
},
|
||||
{
|
||||
"key": "limit",
|
||||
"value": "",
|
||||
"description": null
|
||||
"description": "limit of items per page"
|
||||
}
|
||||
],
|
||||
"variable": []
|
||||
@@ -194,15 +246,16 @@
|
||||
"_type": "endpoint",
|
||||
"name": "Get",
|
||||
"request": {
|
||||
"description": "Get a dataset and its items",
|
||||
"description": "Get a dataset",
|
||||
"url": {
|
||||
"raw": "{{baseUrl}}/api/public/datasets/:datasetName",
|
||||
"raw": "{{baseUrl}}/api/public/v2/datasets/:datasetName",
|
||||
"host": [
|
||||
"{{baseUrl}}"
|
||||
],
|
||||
"path": [
|
||||
"api",
|
||||
"public",
|
||||
"v2",
|
||||
"datasets",
|
||||
":datasetName"
|
||||
],
|
||||
@@ -228,13 +281,14 @@
|
||||
"request": {
|
||||
"description": "Create a dataset",
|
||||
"url": {
|
||||
"raw": "{{baseUrl}}/api/public/datasets",
|
||||
"raw": "{{baseUrl}}/api/public/v2/datasets",
|
||||
"host": [
|
||||
"{{baseUrl}}"
|
||||
],
|
||||
"path": [
|
||||
"api",
|
||||
"public",
|
||||
"v2",
|
||||
"datasets"
|
||||
],
|
||||
"query": [],
|
||||
@@ -257,7 +311,7 @@
|
||||
},
|
||||
{
|
||||
"_type": "endpoint",
|
||||
"name": "Get Runs",
|
||||
"name": "Get Run",
|
||||
"request": {
|
||||
"description": "Get a dataset run and its items",
|
||||
"url": {
|
||||
@@ -293,6 +347,50 @@
|
||||
"body": null
|
||||
},
|
||||
"response": []
|
||||
},
|
||||
{
|
||||
"_type": "endpoint",
|
||||
"name": "Get Runs",
|
||||
"request": {
|
||||
"description": "Get dataset runs",
|
||||
"url": {
|
||||
"raw": "{{baseUrl}}/api/public/datasets/:datasetName/runs?page=&limit=",
|
||||
"host": [
|
||||
"{{baseUrl}}"
|
||||
],
|
||||
"path": [
|
||||
"api",
|
||||
"public",
|
||||
"datasets",
|
||||
":datasetName",
|
||||
"runs"
|
||||
],
|
||||
"query": [
|
||||
{
|
||||
"key": "page",
|
||||
"value": "",
|
||||
"description": "page number, starts at 1"
|
||||
},
|
||||
{
|
||||
"key": "limit",
|
||||
"value": "",
|
||||
"description": "limit of items per page"
|
||||
}
|
||||
],
|
||||
"variable": [
|
||||
{
|
||||
"key": "datasetName",
|
||||
"value": "",
|
||||
"description": null
|
||||
}
|
||||
]
|
||||
},
|
||||
"header": [],
|
||||
"method": "GET",
|
||||
"auth": null,
|
||||
"body": null
|
||||
},
|
||||
"response": []
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -337,7 +435,7 @@
|
||||
"_type": "endpoint",
|
||||
"name": "Batch",
|
||||
"request": {
|
||||
"description": "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.",
|
||||
"description": "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.\n\nNotes:\n\n- Batch sizes are limited to 3.5 MB in total. You need to adjust the number of events per batch accordingly.\n- 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.",
|
||||
"url": {
|
||||
"raw": "{{baseUrl}}/api/public/ingestion",
|
||||
"host": [
|
||||
@@ -437,6 +535,146 @@
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"_type": "container",
|
||||
"description": null,
|
||||
"name": "Models",
|
||||
"item": [
|
||||
{
|
||||
"_type": "endpoint",
|
||||
"name": "Create",
|
||||
"request": {
|
||||
"description": "Create a model",
|
||||
"url": {
|
||||
"raw": "{{baseUrl}}/api/public/models",
|
||||
"host": [
|
||||
"{{baseUrl}}"
|
||||
],
|
||||
"path": [
|
||||
"api",
|
||||
"public",
|
||||
"models"
|
||||
],
|
||||
"query": [],
|
||||
"variable": []
|
||||
},
|
||||
"header": [],
|
||||
"method": "POST",
|
||||
"auth": null,
|
||||
"body": {
|
||||
"mode": "raw",
|
||||
"raw": "{\n \"modelName\": \"example\",\n \"matchPattern\": \"example\",\n \"startDate\": \"1994-11-05\",\n \"unit\": \"CHARACTERS\",\n \"inputPrice\": 0,\n \"outputPrice\": 0,\n \"totalPrice\": 0,\n \"tokenizerId\": \"example\",\n \"tokenizerConfig\": \"UNKNOWN\"\n}",
|
||||
"options": {
|
||||
"raw": {
|
||||
"language": "json"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"response": []
|
||||
},
|
||||
{
|
||||
"_type": "endpoint",
|
||||
"name": "List",
|
||||
"request": {
|
||||
"description": "Get all models",
|
||||
"url": {
|
||||
"raw": "{{baseUrl}}/api/public/models?page=&limit=",
|
||||
"host": [
|
||||
"{{baseUrl}}"
|
||||
],
|
||||
"path": [
|
||||
"api",
|
||||
"public",
|
||||
"models"
|
||||
],
|
||||
"query": [
|
||||
{
|
||||
"key": "page",
|
||||
"value": "",
|
||||
"description": "page number, starts at 1"
|
||||
},
|
||||
{
|
||||
"key": "limit",
|
||||
"value": "",
|
||||
"description": "limit of items per page"
|
||||
}
|
||||
],
|
||||
"variable": []
|
||||
},
|
||||
"header": [],
|
||||
"method": "GET",
|
||||
"auth": null,
|
||||
"body": null
|
||||
},
|
||||
"response": []
|
||||
},
|
||||
{
|
||||
"_type": "endpoint",
|
||||
"name": "Get",
|
||||
"request": {
|
||||
"description": "Get a model",
|
||||
"url": {
|
||||
"raw": "{{baseUrl}}/api/public/models/:id",
|
||||
"host": [
|
||||
"{{baseUrl}}"
|
||||
],
|
||||
"path": [
|
||||
"api",
|
||||
"public",
|
||||
"models",
|
||||
":id"
|
||||
],
|
||||
"query": [],
|
||||
"variable": [
|
||||
{
|
||||
"key": "id",
|
||||
"value": "",
|
||||
"description": null
|
||||
}
|
||||
]
|
||||
},
|
||||
"header": [],
|
||||
"method": "GET",
|
||||
"auth": null,
|
||||
"body": null
|
||||
},
|
||||
"response": []
|
||||
},
|
||||
{
|
||||
"_type": "endpoint",
|
||||
"name": "Delete",
|
||||
"request": {
|
||||
"description": "Delete a model. Cannot delete models managed by Langfuse. You can create your own definition with the same modelName to override the definition though.",
|
||||
"url": {
|
||||
"raw": "{{baseUrl}}/api/public/models/:id",
|
||||
"host": [
|
||||
"{{baseUrl}}"
|
||||
],
|
||||
"path": [
|
||||
"api",
|
||||
"public",
|
||||
"models",
|
||||
":id"
|
||||
],
|
||||
"query": [],
|
||||
"variable": [
|
||||
{
|
||||
"key": "id",
|
||||
"value": "",
|
||||
"description": null
|
||||
}
|
||||
]
|
||||
},
|
||||
"header": [],
|
||||
"method": "DELETE",
|
||||
"auth": null,
|
||||
"body": null
|
||||
},
|
||||
"response": []
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"_type": "container",
|
||||
"description": null,
|
||||
@@ -658,12 +896,12 @@
|
||||
{
|
||||
"key": "page",
|
||||
"value": "",
|
||||
"description": null
|
||||
"description": "page number, starts at 1"
|
||||
},
|
||||
{
|
||||
"key": "limit",
|
||||
"value": "",
|
||||
"description": null
|
||||
"description": "limit of items per page"
|
||||
}
|
||||
],
|
||||
"variable": []
|
||||
@@ -711,6 +949,114 @@
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"_type": "container",
|
||||
"description": null,
|
||||
"name": "Score Configs",
|
||||
"item": [
|
||||
{
|
||||
"_type": "endpoint",
|
||||
"name": "Create",
|
||||
"request": {
|
||||
"description": "Create a score configuration (config). Score configs are used to define the structure of scores",
|
||||
"url": {
|
||||
"raw": "{{baseUrl}}/api/public/score-configs",
|
||||
"host": [
|
||||
"{{baseUrl}}"
|
||||
],
|
||||
"path": [
|
||||
"api",
|
||||
"public",
|
||||
"score-configs"
|
||||
],
|
||||
"query": [],
|
||||
"variable": []
|
||||
},
|
||||
"header": [],
|
||||
"method": "POST",
|
||||
"auth": null,
|
||||
"body": {
|
||||
"mode": "raw",
|
||||
"raw": "{\n \"name\": \"example\",\n \"dataType\": \"NUMERIC\",\n \"categories\": [\n {\n \"value\": 0,\n \"label\": \"example\"\n }\n ],\n \"minValue\": 0,\n \"maxValue\": 0,\n \"description\": \"example\"\n}",
|
||||
"options": {
|
||||
"raw": {
|
||||
"language": "json"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"response": []
|
||||
},
|
||||
{
|
||||
"_type": "endpoint",
|
||||
"name": "Get",
|
||||
"request": {
|
||||
"description": "Get all score configs",
|
||||
"url": {
|
||||
"raw": "{{baseUrl}}/api/public/score-configs?page=&limit=",
|
||||
"host": [
|
||||
"{{baseUrl}}"
|
||||
],
|
||||
"path": [
|
||||
"api",
|
||||
"public",
|
||||
"score-configs"
|
||||
],
|
||||
"query": [
|
||||
{
|
||||
"key": "page",
|
||||
"value": "",
|
||||
"description": "Page number, starts at 1."
|
||||
},
|
||||
{
|
||||
"key": "limit",
|
||||
"value": "",
|
||||
"description": "Limit of items per page. If you encounter api issues due to too large page sizes, try to reduce the limit"
|
||||
}
|
||||
],
|
||||
"variable": []
|
||||
},
|
||||
"header": [],
|
||||
"method": "GET",
|
||||
"auth": null,
|
||||
"body": null
|
||||
},
|
||||
"response": []
|
||||
},
|
||||
{
|
||||
"_type": "endpoint",
|
||||
"name": "Get By Id",
|
||||
"request": {
|
||||
"description": "Get a score config",
|
||||
"url": {
|
||||
"raw": "{{baseUrl}}/api/public/score-configs/:configId",
|
||||
"host": [
|
||||
"{{baseUrl}}"
|
||||
],
|
||||
"path": [
|
||||
"api",
|
||||
"public",
|
||||
"score-configs",
|
||||
":configId"
|
||||
],
|
||||
"query": [],
|
||||
"variable": [
|
||||
{
|
||||
"key": "configId",
|
||||
"value": "",
|
||||
"description": "The unique langfuse identifier of a score config"
|
||||
}
|
||||
]
|
||||
},
|
||||
"header": [],
|
||||
"method": "GET",
|
||||
"auth": null,
|
||||
"body": null
|
||||
},
|
||||
"response": []
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"_type": "container",
|
||||
"description": null,
|
||||
@@ -739,7 +1085,7 @@
|
||||
"auth": null,
|
||||
"body": {
|
||||
"mode": "raw",
|
||||
"raw": "{\n \"id\": \"example\",\n \"traceId\": \"example\",\n \"name\": \"example\",\n \"value\": 0,\n \"observationId\": \"example\",\n \"comment\": \"example\"\n}",
|
||||
"raw": "{\n \"name\": \"novelty\",\n \"value\": 0.9,\n \"traceId\": \"cdef-1234-5678-90ab\"\n}",
|
||||
"options": {
|
||||
"raw": {
|
||||
"language": "json"
|
||||
@@ -755,7 +1101,7 @@
|
||||
"request": {
|
||||
"description": "Get a list of scores",
|
||||
"url": {
|
||||
"raw": "{{baseUrl}}/api/public/scores?page=&limit=&userId=&name=&fromTimestamp=&source=&operator=&value=&scoreIds=",
|
||||
"raw": "{{baseUrl}}/api/public/scores?page=&limit=&userId=&name=&fromTimestamp=&source=&operator=&value=&scoreIds=&configId=&dataType=",
|
||||
"host": [
|
||||
"{{baseUrl}}"
|
||||
],
|
||||
@@ -778,12 +1124,12 @@
|
||||
{
|
||||
"key": "userId",
|
||||
"value": "",
|
||||
"description": null
|
||||
"description": "Retrieve only scores with this userId associated to the trace."
|
||||
},
|
||||
{
|
||||
"key": "name",
|
||||
"value": "",
|
||||
"description": null
|
||||
"description": "Retrieve only scores with this name."
|
||||
},
|
||||
{
|
||||
"key": "fromTimestamp",
|
||||
@@ -809,6 +1155,16 @@
|
||||
"key": "scoreIds",
|
||||
"value": "",
|
||||
"description": "Comma-separated list of score IDs to limit the results to."
|
||||
},
|
||||
{
|
||||
"key": "configId",
|
||||
"value": "",
|
||||
"description": "Retrieve only scores with a specific configId."
|
||||
},
|
||||
{
|
||||
"key": "dataType",
|
||||
"value": "",
|
||||
"description": "Retrieve only scores with a specific dataType."
|
||||
}
|
||||
],
|
||||
"variable": []
|
||||
|
||||
@@ -0,0 +1,249 @@
|
||||
import "dotenv/config";
|
||||
|
||||
import { z } from "zod";
|
||||
import { prisma, Prisma } from "@langfuse/shared/src/db";
|
||||
|
||||
const BackfillCalculatedGenerationArgsSchema = z
|
||||
.object({
|
||||
batchSize: z.coerce.number().optional().default(5_000),
|
||||
maxRowsToProcess: z.coerce.number().optional().default(Infinity), // Default to process all rows
|
||||
maxDate: z.coerce.date().optional().default(new Date()), // Default to today
|
||||
})
|
||||
.strict();
|
||||
|
||||
const backfillCalculatedGenerationCost = async () => {
|
||||
let previousTimeout;
|
||||
try {
|
||||
const args = parseArgs(process.argv.slice(2));
|
||||
const { batchSize, maxRowsToProcess, maxDate } = args;
|
||||
|
||||
log("Starting backfillCalculatedGenerationCost with params", args);
|
||||
|
||||
// Set the statement timeout
|
||||
const newTimeout = "19min";
|
||||
previousTimeout = await updateStatementTimeout(newTimeout, previousTimeout);
|
||||
|
||||
// Drop column if it exists and add temporary column
|
||||
await addTemporaryColumnIfNotExists();
|
||||
|
||||
let currentDateCutoff = maxDate.toISOString();
|
||||
let totalRowsProcessed = 0;
|
||||
|
||||
log("Starting batch update loop...");
|
||||
|
||||
// Step 3: Batch update in a loop
|
||||
while (true) {
|
||||
log(`Starting batch update for generations before: ${currentDateCutoff}`);
|
||||
const startDate = Date.now();
|
||||
const batchUpdate = await prisma.$queryRaw<
|
||||
{ start_time: Date }[]
|
||||
>(Prisma.sql`
|
||||
WITH batch AS (
|
||||
SELECT o.id,
|
||||
o.start_time,
|
||||
o.prompt_tokens,
|
||||
o.completion_tokens,
|
||||
o.total_tokens,
|
||||
o.input_cost,
|
||||
o.output_cost,
|
||||
o.total_cost,
|
||||
m.id AS model_id,
|
||||
m.input_price,
|
||||
m.output_price,
|
||||
m.total_price
|
||||
FROM observations o
|
||||
LEFT JOIN LATERAL (
|
||||
SELECT models.id,
|
||||
models.input_price,
|
||||
models.output_price,
|
||||
models.total_price
|
||||
FROM models
|
||||
WHERE (models.project_id = o.project_id OR models.project_id IS NULL)
|
||||
AND models.model_name = o.internal_model
|
||||
AND (models.start_date < o.start_time OR models.start_date IS NULL)
|
||||
AND o.unit = models.unit
|
||||
ORDER BY models.project_id, models.start_date DESC NULLS LAST
|
||||
LIMIT 1
|
||||
) m ON true
|
||||
WHERE
|
||||
start_time <= ${currentDateCutoff}::TIMESTAMP WITH TIME ZONE AT TIME ZONE 'UTC'
|
||||
AND (internal_model IS NOT NULL
|
||||
OR input_cost IS NOT NULL
|
||||
OR output_cost IS NOT NULL
|
||||
OR total_cost IS NOT NULL)
|
||||
ORDER BY
|
||||
start_time DESC
|
||||
LIMIT ${batchSize}
|
||||
),
|
||||
updated_batch AS (
|
||||
UPDATE observations o
|
||||
SET calculated_input_cost =
|
||||
CASE
|
||||
WHEN batch.input_cost IS NULL AND batch.output_cost IS NULL AND batch.total_cost IS NULL
|
||||
THEN batch.prompt_tokens::numeric * batch.input_price
|
||||
ELSE batch.input_cost
|
||||
END,
|
||||
calculated_output_cost =
|
||||
CASE
|
||||
WHEN batch.input_cost IS NULL AND batch.output_cost IS NULL AND batch.total_cost IS NULL
|
||||
THEN batch.completion_tokens::numeric * batch.output_price
|
||||
ELSE batch.output_cost
|
||||
END,
|
||||
calculated_total_cost =
|
||||
CASE
|
||||
WHEN batch.input_cost IS NULL AND batch.output_cost IS NULL AND batch.total_cost IS NULL
|
||||
THEN
|
||||
CASE
|
||||
WHEN batch.total_price IS NOT NULL AND batch.total_tokens IS NOT NULL THEN batch.total_price * batch.total_tokens::numeric
|
||||
ELSE batch.prompt_tokens::numeric * batch.input_price + batch.completion_tokens::numeric * batch.output_price
|
||||
END
|
||||
ELSE batch.total_cost
|
||||
END,
|
||||
internal_model_id = batch.model_id,
|
||||
tmp_has_calculated_cost = TRUE
|
||||
FROM batch
|
||||
WHERE o.id = batch.id
|
||||
RETURNING o.id
|
||||
)
|
||||
-- Get the last id of the updated batch
|
||||
SELECT start_time FROM batch LIMIT 1 OFFSET ${batchSize - 1};
|
||||
`);
|
||||
|
||||
log(`Batch update completed in ${Date.now() - startDate} ms`);
|
||||
|
||||
if (!batchUpdate[0]?.start_time) {
|
||||
log(
|
||||
`No more rows to process, breaking loop after ${totalRowsProcessed.toLocaleString()} rows processed.`,
|
||||
);
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
currentDateCutoff = batchUpdate[0]?.start_time.toISOString();
|
||||
totalRowsProcessed += batchSize;
|
||||
|
||||
log(
|
||||
`Total rows processed after increment: ${totalRowsProcessed.toLocaleString()} rows`,
|
||||
);
|
||||
if (maxRowsToProcess && totalRowsProcessed >= maxRowsToProcess) {
|
||||
log(
|
||||
`Max rows to process reached: ${maxRowsToProcess.toLocaleString()}, breaking loop.`,
|
||||
);
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
log("✅ Finished batch update loop.");
|
||||
|
||||
// Drop the temporary column
|
||||
log("Dropping temporary column...");
|
||||
await prisma.$executeRaw`ALTER TABLE observations DROP COLUMN IF EXISTS tmp_has_calculated_cost;`;
|
||||
log("✅ Dropped temporary column");
|
||||
|
||||
log("✅ Finished backfillCalculatedGenerationCost");
|
||||
} catch (err) {
|
||||
console.error("Error executing script", err);
|
||||
} finally {
|
||||
// Reset the statement timeout to two minutes
|
||||
await prisma.$executeRawUnsafe(
|
||||
`SET statement_timeout = '${previousTimeout}';`,
|
||||
);
|
||||
log(
|
||||
`Reset statement_timeout to ${previousTimeout}. Current statement_timeout: ${JSON.stringify(
|
||||
await prisma.$queryRaw(Prisma.sql`SHOW statement_timeout;`),
|
||||
)}`,
|
||||
);
|
||||
|
||||
// Disconnect from the database
|
||||
await prisma.$disconnect();
|
||||
log("Disconnected from the database.");
|
||||
}
|
||||
};
|
||||
|
||||
function parseArgs(args: string[]) {
|
||||
try {
|
||||
const namedArgs: Record<string, string | boolean> = {};
|
||||
for (let i = 0; i < args.length; i++) {
|
||||
if (args[i].startsWith("--")) {
|
||||
const key = args[i].slice(2);
|
||||
const value =
|
||||
args[i + 1] && !args[i + 1].startsWith("--") ? args[i + 1] : true;
|
||||
namedArgs[key] = value;
|
||||
if (value !== true) i++; // Skip the next argument if it was used as a value
|
||||
}
|
||||
}
|
||||
|
||||
return BackfillCalculatedGenerationArgsSchema.parse(namedArgs);
|
||||
} catch (error) {
|
||||
if (error instanceof z.ZodError) {
|
||||
console.error("Validation error:", error.errors);
|
||||
} else {
|
||||
console.error("An unexpected error occurred:", error);
|
||||
}
|
||||
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
async function addTemporaryColumnIfNotExists() {
|
||||
const columnExists = await prisma.$queryRaw<{ column_exists: boolean }[]>(
|
||||
Prisma.sql`
|
||||
SELECT EXISTS (
|
||||
SELECT 1
|
||||
FROM information_schema.columns
|
||||
WHERE table_name = 'observations'
|
||||
AND column_name = 'tmp_has_calculated_cost'
|
||||
) AS column_exists;
|
||||
`,
|
||||
);
|
||||
if (!columnExists[0]?.column_exists) {
|
||||
await prisma.$executeRaw`ALTER TABLE observations ADD COLUMN tmp_has_calculated_cost BOOLEAN DEFAULT FALSE;`;
|
||||
log("✅ Added temporary column tmp_has_calculated_cost");
|
||||
} else {
|
||||
log(
|
||||
"⚠️ Temporary column tmp_has_calculated_cost already exists. Continuing...",
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
type StatementTimeout = {
|
||||
statement_timeout: string;
|
||||
};
|
||||
|
||||
async function updateStatementTimeout(
|
||||
newTimeout: string,
|
||||
previousTimeout: any,
|
||||
) {
|
||||
const [{ statement_timeout: previousTimeoutRead }] = await prisma.$queryRaw<
|
||||
StatementTimeout[]
|
||||
>(Prisma.sql`SHOW statement_timeout;`);
|
||||
|
||||
log(`Current statement_timeout read from DB: ${previousTimeoutRead}`);
|
||||
|
||||
if (!previousTimeoutRead || previousTimeoutRead === newTimeout) {
|
||||
// If the statement_timeout is already set to 19 minutes, assume it was set by this script and reset it to 2 minutes
|
||||
previousTimeout = "2min";
|
||||
} else {
|
||||
previousTimeout = previousTimeoutRead;
|
||||
}
|
||||
|
||||
log(`Setting statement_timeout to ${newTimeout} minutes...`);
|
||||
|
||||
await prisma.$executeRawUnsafe(`SET statement_timeout = '${newTimeout}';`);
|
||||
|
||||
log(
|
||||
`Updated statement_timeout. Current statement_timeout: ${JSON.stringify(
|
||||
await prisma.$queryRaw(Prisma.sql`SHOW statement_timeout;`),
|
||||
)}`,
|
||||
);
|
||||
|
||||
return previousTimeout;
|
||||
}
|
||||
|
||||
function log(message: string, ...args: any[]) {
|
||||
console.log(new Date().toISOString(), " - ", message, ...args);
|
||||
}
|
||||
|
||||
// Execute the script
|
||||
backfillCalculatedGenerationCost();
|
||||
@@ -1,7 +1,7 @@
|
||||
import {
|
||||
getDisplaySecretKey,
|
||||
hashSecretKey,
|
||||
} from "@langfuse/shared/src/server/auth";
|
||||
} from "@langfuse/shared/src/server";
|
||||
import { verifyAuthHeaderAndReturnScope } from "@/src/features/public-api/server/apiAuth";
|
||||
import { prisma } from "@langfuse/shared/src/db";
|
||||
|
||||
|
||||
@@ -1,8 +1,26 @@
|
||||
/** @jest-environment node */
|
||||
|
||||
import { prisma } from "@langfuse/shared/src/db";
|
||||
import { makeAPICall, pruneDatabase } from "@/src/__tests__/test-utils";
|
||||
import {
|
||||
makeAPICall,
|
||||
makeZodVerifiedAPICall,
|
||||
pruneDatabase,
|
||||
} from "@/src/__tests__/test-utils";
|
||||
import { v4 } from "uuid";
|
||||
import {
|
||||
GetDatasetItemV1Response,
|
||||
GetDatasetItemsV1Response,
|
||||
GetDatasetRunV1Response,
|
||||
GetDatasetRunsV1Response,
|
||||
GetDatasetV1Response,
|
||||
GetDatasetV2Response,
|
||||
GetDatasetsV1Response,
|
||||
GetDatasetsV2Response,
|
||||
PostDatasetItemsV1Response,
|
||||
PostDatasetRunItemsV1Response,
|
||||
PostDatasetsV1Response,
|
||||
PostDatasetsV2Response,
|
||||
} from "@/src/features/public-api/types/datasets";
|
||||
|
||||
describe("/api/public/datasets and /api/public/dataset-items API Endpoints", () => {
|
||||
const traceId = v4();
|
||||
@@ -49,11 +67,24 @@ describe("/api/public/datasets and /api/public/dataset-items API Endpoints", ()
|
||||
});
|
||||
afterEach(async () => await pruneDatabase());
|
||||
|
||||
it("should create and get a dataset, include special characters", async () => {
|
||||
await makeAPICall("POST", "/api/public/datasets", {
|
||||
it("should create and get a dataset (v1), include special characters", async () => {
|
||||
const createRes = await makeZodVerifiedAPICall(
|
||||
PostDatasetsV1Response,
|
||||
"POST",
|
||||
"/api/public/datasets",
|
||||
{
|
||||
name: "dataset + name",
|
||||
description: "dataset-description",
|
||||
metadata: { foo: "bar" },
|
||||
},
|
||||
);
|
||||
expect(createRes.status).toBe(200);
|
||||
expect(createRes.body).toMatchObject({
|
||||
name: "dataset + name",
|
||||
description: "dataset-description",
|
||||
metadata: { foo: "bar" },
|
||||
items: [],
|
||||
runs: [],
|
||||
});
|
||||
|
||||
const dbDataset = await prisma.dataset.findMany({
|
||||
@@ -61,62 +92,131 @@ describe("/api/public/datasets and /api/public/dataset-items API Endpoints", ()
|
||||
name: "dataset + name",
|
||||
},
|
||||
});
|
||||
|
||||
expect(dbDataset.length).toBeGreaterThan(0);
|
||||
|
||||
const getDataset = await makeAPICall(
|
||||
// get dataset (v1) excluding items and runs
|
||||
const getDatasetV1 = await makeAPICall(
|
||||
"GET",
|
||||
`/api/public/datasets/${encodeURIComponent("dataset + name")}`,
|
||||
);
|
||||
|
||||
expect(getDataset.status).toBe(200);
|
||||
expect(getDataset.body).toMatchObject({
|
||||
expect(getDatasetV1.status).toBe(200);
|
||||
expect(getDatasetV1.body).toMatchObject({
|
||||
name: "dataset + name",
|
||||
description: "dataset-description",
|
||||
metadata: { foo: "bar" },
|
||||
items: [],
|
||||
runs: [],
|
||||
});
|
||||
});
|
||||
|
||||
it("GET datasets", async () => {
|
||||
await makeAPICall("POST", "/api/public/datasets", {
|
||||
name: "dataset-name-1",
|
||||
it("should create and get a dataset (v2), include special characters", async () => {
|
||||
const createRes = await makeZodVerifiedAPICall(
|
||||
PostDatasetsV2Response,
|
||||
"POST",
|
||||
"/api/public/v2/datasets",
|
||||
{
|
||||
name: "dataset + name + v2",
|
||||
description: "dataset-description",
|
||||
metadata: { foo: "bar" },
|
||||
},
|
||||
);
|
||||
expect(createRes.status).toBe(200);
|
||||
expect(createRes.body).toMatchObject({
|
||||
name: "dataset + name + v2",
|
||||
description: "dataset-description",
|
||||
metadata: { foo: "bar" },
|
||||
});
|
||||
|
||||
await makeAPICall("POST", "/api/public/datasets", {
|
||||
name: "dataset-name-2",
|
||||
const dbDataset = await prisma.dataset.findMany({
|
||||
where: {
|
||||
name: "dataset + name + v2",
|
||||
},
|
||||
});
|
||||
expect(dbDataset.length).toBeGreaterThan(0);
|
||||
|
||||
// get dataset (v2) excluding items and runs
|
||||
const getDatasetV2 = await makeZodVerifiedAPICall(
|
||||
GetDatasetV2Response,
|
||||
"GET",
|
||||
`/api/public/v2/datasets/${encodeURIComponent("dataset + name + v2")}`,
|
||||
);
|
||||
expect(getDatasetV2.status).toBe(200);
|
||||
expect(getDatasetV2.body).toMatchObject({
|
||||
name: "dataset + name + v2",
|
||||
description: "dataset-description",
|
||||
metadata: { foo: "bar" },
|
||||
});
|
||||
expect(getDatasetV2.body).not.toHaveProperty("items");
|
||||
expect(getDatasetV2.body).not.toHaveProperty("runs");
|
||||
});
|
||||
|
||||
it("GET datasets (v1 & v2)", async () => {
|
||||
// v1 post
|
||||
await makeZodVerifiedAPICall(
|
||||
PostDatasetsV1Response,
|
||||
"POST",
|
||||
"/api/public/datasets",
|
||||
{
|
||||
name: "dataset-name-1",
|
||||
description: "dataset-description-1",
|
||||
metadata: { key: "value" },
|
||||
},
|
||||
);
|
||||
// v2 post
|
||||
await makeZodVerifiedAPICall(
|
||||
PostDatasetsV2Response,
|
||||
"POST",
|
||||
"/api/public/v2/datasets",
|
||||
{
|
||||
name: "dataset-name-2",
|
||||
},
|
||||
);
|
||||
|
||||
const datasetItemId = v4();
|
||||
|
||||
const createItemRes = await makeAPICall<{
|
||||
datasetName: string; // field that can break if the API changes as it is not a db column
|
||||
}>("POST", "/api/public/dataset-items", {
|
||||
datasetName: "dataset-name-2",
|
||||
input: { key: "value" },
|
||||
expectedOutput: { key: "value" },
|
||||
metadata: { key: "value-dataset-item" },
|
||||
id: datasetItemId,
|
||||
});
|
||||
const createItemRes = await makeZodVerifiedAPICall(
|
||||
PostDatasetItemsV1Response,
|
||||
"POST",
|
||||
"/api/public/dataset-items",
|
||||
{
|
||||
datasetName: "dataset-name-2",
|
||||
input: { key: "value" },
|
||||
expectedOutput: { key: "value" },
|
||||
metadata: { key: "value-dataset-item" },
|
||||
id: datasetItemId,
|
||||
},
|
||||
);
|
||||
|
||||
expect(createItemRes.status).toBe(200);
|
||||
expect(createItemRes.body).toMatchObject({
|
||||
datasetName: "dataset-name-2", // not included in db table
|
||||
});
|
||||
|
||||
await makeAPICall("POST", "/api/public/dataset-run-items", {
|
||||
datasetItemId: datasetItemId,
|
||||
observationId: observationId,
|
||||
runName: "test-run",
|
||||
metadata: { key: "value" },
|
||||
});
|
||||
await makeZodVerifiedAPICall(
|
||||
PostDatasetRunItemsV1Response,
|
||||
"POST",
|
||||
"/api/public/dataset-run-items",
|
||||
{
|
||||
datasetItemId: datasetItemId,
|
||||
observationId: observationId,
|
||||
runName: "test-run",
|
||||
metadata: { key: "value" },
|
||||
},
|
||||
);
|
||||
|
||||
const getDatasets = await makeAPICall("GET", `/api/public/datasets`);
|
||||
const getDatasetsV1 = await makeZodVerifiedAPICall(
|
||||
GetDatasetsV1Response,
|
||||
"GET",
|
||||
`/api/public/datasets`,
|
||||
);
|
||||
|
||||
expect(getDatasets.status).toBe(200);
|
||||
expect(getDatasets.body).toMatchObject({
|
||||
expect(getDatasetsV1.status).toBe(200);
|
||||
expect(getDatasetsV1.body).toMatchObject({
|
||||
data: expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
name: "dataset-name-1",
|
||||
description: "dataset-description-1",
|
||||
metadata: { key: "value" },
|
||||
items: [],
|
||||
runs: [],
|
||||
}),
|
||||
@@ -131,87 +231,257 @@ describe("/api/public/datasets and /api/public/dataset-items API Endpoints", ()
|
||||
page: 1,
|
||||
}),
|
||||
});
|
||||
|
||||
const getDatasetsV2 = await makeZodVerifiedAPICall(
|
||||
GetDatasetsV2Response,
|
||||
"GET",
|
||||
`/api/public/v2/datasets`,
|
||||
);
|
||||
|
||||
expect(getDatasetsV2.status).toBe(200);
|
||||
expect(getDatasetsV2.body).toMatchObject({
|
||||
data: expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
name: "dataset-name-1",
|
||||
description: "dataset-description-1",
|
||||
metadata: { key: "value" },
|
||||
}),
|
||||
expect.objectContaining({
|
||||
name: "dataset-name-2",
|
||||
description: null,
|
||||
metadata: null,
|
||||
}),
|
||||
]),
|
||||
meta: expect.objectContaining({
|
||||
totalItems: 2,
|
||||
page: 1,
|
||||
}),
|
||||
});
|
||||
});
|
||||
|
||||
it("should create and get a dataset item (via datasets and individually)", async () => {
|
||||
await makeAPICall("POST", "/api/public/datasets", {
|
||||
name: "dataset-name",
|
||||
});
|
||||
await makeAPICall("POST", "/api/public/dataset-items", {
|
||||
datasetName: "dataset-name",
|
||||
input: { key: "value" },
|
||||
expectedOutput: { key: "value" },
|
||||
metadata: { key: "value-dataset-item" },
|
||||
sourceTraceId: traceId,
|
||||
sourceObservationId: observationId,
|
||||
});
|
||||
const dbDatasetItem = await prisma.datasetItem.findFirst({
|
||||
it("should create and get a dataset items (via datasets (v1), individually, and as a list)", async () => {
|
||||
await makeZodVerifiedAPICall(
|
||||
PostDatasetsV1Response,
|
||||
"POST",
|
||||
"/api/public/datasets",
|
||||
{
|
||||
name: "dataset-name",
|
||||
},
|
||||
);
|
||||
for (let i = 0; i < 5; i++) {
|
||||
await makeZodVerifiedAPICall(
|
||||
PostDatasetItemsV1Response,
|
||||
"POST",
|
||||
"/api/public/dataset-items",
|
||||
{
|
||||
datasetName: "dataset-name",
|
||||
input: { key: "value" },
|
||||
expectedOutput: { key: "value" },
|
||||
metadata: { key: "value-dataset-item" },
|
||||
sourceTraceId: i % 2 === 0 ? traceId : undefined,
|
||||
sourceObservationId: i % 2 === 0 ? observationId : undefined,
|
||||
},
|
||||
);
|
||||
}
|
||||
const dbDatasetItems = await prisma.datasetItem.findMany({
|
||||
where: {
|
||||
dataset: {
|
||||
name: "dataset-name",
|
||||
},
|
||||
},
|
||||
orderBy: {
|
||||
createdAt: "desc",
|
||||
},
|
||||
});
|
||||
expect(dbDatasetItems.length).toBe(5);
|
||||
const dbDatasetItemsApiResponseFormat = dbDatasetItems.map((item) => ({
|
||||
...item,
|
||||
createdAt: item.createdAt.toISOString(),
|
||||
updatedAt: item.updatedAt.toISOString(),
|
||||
datasetName: "dataset-name",
|
||||
}));
|
||||
|
||||
expect(dbDatasetItem).not.toBeNull();
|
||||
// add another dataset to test the list endpoint
|
||||
await makeZodVerifiedAPICall(
|
||||
PostDatasetsV1Response,
|
||||
"POST",
|
||||
"/api/public/datasets",
|
||||
{
|
||||
name: "dataset-name-other",
|
||||
},
|
||||
);
|
||||
await makeZodVerifiedAPICall(
|
||||
PostDatasetItemsV1Response,
|
||||
"POST",
|
||||
"/api/public/dataset-items",
|
||||
{
|
||||
datasetName: "dataset-name-other",
|
||||
input: { key: "value" },
|
||||
expectedOutput: { key: "value" },
|
||||
},
|
||||
);
|
||||
const dbDatasetItemsOther = await prisma.datasetItem.findMany({
|
||||
where: {
|
||||
dataset: {
|
||||
name: "dataset-name-other",
|
||||
},
|
||||
},
|
||||
orderBy: {
|
||||
createdAt: "desc",
|
||||
},
|
||||
});
|
||||
expect(dbDatasetItemsOther.length).toBe(1);
|
||||
const dbDatasetItemsOtherApiResponseFormat = dbDatasetItemsOther.map(
|
||||
(item) => ({
|
||||
...item,
|
||||
createdAt: item.createdAt.toISOString(),
|
||||
updatedAt: item.updatedAt.toISOString(),
|
||||
datasetName: "dataset-name-other",
|
||||
}),
|
||||
);
|
||||
const dbDatasetItemsAllApiResponseFormat = [
|
||||
...dbDatasetItemsApiResponseFormat,
|
||||
...dbDatasetItemsOtherApiResponseFormat,
|
||||
].sort((a, b) => b.createdAt.localeCompare(a.createdAt)); // createdAt desc
|
||||
|
||||
const getDataset = await makeAPICall(
|
||||
// Get dataset (v1) includes list of items
|
||||
const getDataset = await makeZodVerifiedAPICall(
|
||||
GetDatasetV1Response,
|
||||
"GET",
|
||||
`/api/public/datasets/dataset-name`,
|
||||
);
|
||||
expect(getDataset.status).toBe(200);
|
||||
expect(getDataset.body).toMatchObject({
|
||||
name: "dataset-name",
|
||||
items: [
|
||||
{
|
||||
id: dbDatasetItem!.id,
|
||||
input: { key: "value" },
|
||||
expectedOutput: { key: "value" },
|
||||
metadata: { key: "value-dataset-item" },
|
||||
datasetName: "dataset-name", // not included in db table
|
||||
},
|
||||
],
|
||||
items: dbDatasetItemsApiResponseFormat,
|
||||
});
|
||||
|
||||
const getDatasetItem = await makeAPICall(
|
||||
// Get List
|
||||
const getDatasetItemsAll = await makeZodVerifiedAPICall(
|
||||
GetDatasetItemsV1Response,
|
||||
"GET",
|
||||
`/api/public/dataset-items/${dbDatasetItem!.id}`,
|
||||
`/api/public/dataset-items`,
|
||||
);
|
||||
expect(getDatasetItemsAll.status).toBe(200);
|
||||
expect(getDatasetItemsAll.body).toMatchObject({
|
||||
data: dbDatasetItemsAllApiResponseFormat,
|
||||
meta: expect.objectContaining({
|
||||
totalItems: 6,
|
||||
page: 1,
|
||||
}),
|
||||
});
|
||||
// Get List, check pagination
|
||||
const getDatasetItemsAllPage2 = await makeZodVerifiedAPICall(
|
||||
GetDatasetItemsV1Response,
|
||||
"GET",
|
||||
`/api/public/dataset-items?page=2&limit=1`,
|
||||
);
|
||||
expect(getDatasetItemsAllPage2.status).toBe(200);
|
||||
expect(getDatasetItemsAllPage2.body).toMatchObject({
|
||||
data: dbDatasetItemsAllApiResponseFormat.slice(1, 2),
|
||||
meta: expect.objectContaining({
|
||||
totalItems: 6,
|
||||
page: 2,
|
||||
totalPages: 6,
|
||||
limit: 1,
|
||||
}),
|
||||
});
|
||||
// Get filtered list by datasetName
|
||||
const getDatasetItems = await makeZodVerifiedAPICall(
|
||||
GetDatasetItemsV1Response,
|
||||
"GET",
|
||||
`/api/public/dataset-items?datasetName=dataset-name`,
|
||||
);
|
||||
expect(getDatasetItems.status).toBe(200);
|
||||
expect(getDatasetItems.body).toMatchObject({
|
||||
data: dbDatasetItemsApiResponseFormat,
|
||||
meta: expect.objectContaining({
|
||||
totalItems: 5,
|
||||
page: 1,
|
||||
}),
|
||||
});
|
||||
// Get filtered list by sourceTraceId
|
||||
const getDatasetItemsTrace = await makeZodVerifiedAPICall(
|
||||
GetDatasetItemsV1Response,
|
||||
"GET",
|
||||
`/api/public/dataset-items?sourceTraceId=${traceId}`,
|
||||
);
|
||||
expect(getDatasetItemsTrace.status).toBe(200);
|
||||
expect(getDatasetItemsTrace.body).toMatchObject({
|
||||
data: dbDatasetItemsApiResponseFormat.filter(
|
||||
(item) => item.sourceTraceId === traceId,
|
||||
),
|
||||
meta: expect.objectContaining({
|
||||
totalItems: 3,
|
||||
page: 1,
|
||||
}),
|
||||
});
|
||||
// Get filtered list by sourceObservationId
|
||||
const getDatasetItemsObservation = await makeZodVerifiedAPICall(
|
||||
GetDatasetItemsV1Response,
|
||||
"GET",
|
||||
`/api/public/dataset-items?sourceObservationId=${observationId}`,
|
||||
);
|
||||
expect(getDatasetItemsObservation.status).toBe(200);
|
||||
expect(getDatasetItemsObservation.body).toMatchObject({
|
||||
data: dbDatasetItemsApiResponseFormat.filter(
|
||||
(item) => item.sourceObservationId === observationId,
|
||||
),
|
||||
meta: expect.objectContaining({
|
||||
totalItems: 3,
|
||||
page: 1,
|
||||
}),
|
||||
});
|
||||
|
||||
// Get single item
|
||||
const singleItem = dbDatasetItemsApiResponseFormat[0];
|
||||
const getDatasetItem = await makeZodVerifiedAPICall(
|
||||
GetDatasetItemV1Response,
|
||||
"GET",
|
||||
`/api/public/dataset-items/${singleItem.id}`,
|
||||
);
|
||||
expect(getDatasetItem.status).toBe(200);
|
||||
expect(getDatasetItem.body).toMatchObject({
|
||||
id: dbDatasetItem!.id,
|
||||
input: { key: "value" },
|
||||
expectedOutput: { key: "value" },
|
||||
metadata: { key: "value-dataset-item" },
|
||||
datasetName: "dataset-name", // not included in db table
|
||||
sourceTraceId: traceId,
|
||||
sourceObservationId: observationId,
|
||||
});
|
||||
expect(getDatasetItem.body).toMatchObject(singleItem);
|
||||
});
|
||||
|
||||
it("should upsert a dataset item", async () => {
|
||||
await makeAPICall("POST", "/api/public/datasets", {
|
||||
name: "dataset-name",
|
||||
});
|
||||
await makeZodVerifiedAPICall(
|
||||
PostDatasetsV1Response,
|
||||
"POST",
|
||||
"/api/public/datasets",
|
||||
{
|
||||
name: "dataset-name",
|
||||
},
|
||||
);
|
||||
|
||||
const item1 = await makeAPICall("POST", "/api/public/dataset-items", {
|
||||
id: "dataset-item-id",
|
||||
datasetName: "dataset-name",
|
||||
input: { key: "value" },
|
||||
metadata: { key: "value-dataset-item" },
|
||||
});
|
||||
const item1 = await makeZodVerifiedAPICall(
|
||||
PostDatasetItemsV1Response,
|
||||
"POST",
|
||||
"/api/public/dataset-items",
|
||||
{
|
||||
id: "dataset-item-id",
|
||||
datasetName: "dataset-name",
|
||||
input: { key: "value" },
|
||||
metadata: { key: "value-dataset-item" },
|
||||
},
|
||||
);
|
||||
expect(item1.status).toBe(200);
|
||||
expect(item1.body).toMatchObject({
|
||||
id: "dataset-item-id",
|
||||
});
|
||||
|
||||
const item2 = await makeAPICall("POST", "/api/public/dataset-items", {
|
||||
id: "dataset-item-id",
|
||||
datasetName: "dataset-name",
|
||||
input: { key: "value2" },
|
||||
metadata: ["hello-world"],
|
||||
status: "ARCHIVED",
|
||||
});
|
||||
const item2 = await makeZodVerifiedAPICall(
|
||||
PostDatasetItemsV1Response,
|
||||
"POST",
|
||||
"/api/public/dataset-items",
|
||||
{
|
||||
id: "dataset-item-id",
|
||||
datasetName: "dataset-name",
|
||||
input: { key: "value2" },
|
||||
metadata: ["hello-world"],
|
||||
status: "ARCHIVED",
|
||||
},
|
||||
);
|
||||
expect(item2.status).toBe(200);
|
||||
expect(item2.body).toMatchObject({
|
||||
id: "dataset-item-id",
|
||||
@@ -230,7 +500,8 @@ describe("/api/public/datasets and /api/public/dataset-items API Endpoints", ()
|
||||
});
|
||||
|
||||
it("should create and get a dataset run, include special characters", async () => {
|
||||
const dataset = await makeAPICall<{ id: string }>(
|
||||
const dataset = await makeZodVerifiedAPICall(
|
||||
PostDatasetsV1Response,
|
||||
"POST",
|
||||
"/api/public/datasets",
|
||||
{
|
||||
@@ -241,12 +512,17 @@ describe("/api/public/datasets and /api/public/dataset-items API Endpoints", ()
|
||||
expect(dataset.body).toMatchObject({
|
||||
name: "dataset name",
|
||||
});
|
||||
await makeAPICall("POST", "/api/public/dataset-items", {
|
||||
datasetName: "dataset name",
|
||||
id: "dataset-item-id",
|
||||
input: { key: "value" },
|
||||
expectedOutput: { key: "value" },
|
||||
});
|
||||
await makeZodVerifiedAPICall(
|
||||
PostDatasetItemsV1Response,
|
||||
"POST",
|
||||
"/api/public/dataset-items",
|
||||
{
|
||||
datasetName: "dataset name",
|
||||
id: "dataset-item-id",
|
||||
input: { key: "value" },
|
||||
expectedOutput: { key: "value" },
|
||||
},
|
||||
);
|
||||
const traceId = v4();
|
||||
const observationId = v4();
|
||||
const response = await makeAPICall("POST", "/api/public/ingestion", {
|
||||
@@ -285,7 +561,8 @@ describe("/api/public/datasets and /api/public/dataset-items API Endpoints", ()
|
||||
});
|
||||
expect(response.status).toBe(207);
|
||||
|
||||
const runItemObservation = await makeAPICall(
|
||||
const runItemObservation = await makeZodVerifiedAPICall(
|
||||
PostDatasetRunItemsV1Response,
|
||||
"POST",
|
||||
"/api/public/dataset-run-items",
|
||||
{
|
||||
@@ -315,9 +592,9 @@ describe("/api/public/datasets and /api/public/dataset-items API Endpoints", ()
|
||||
traceId: traceId,
|
||||
});
|
||||
|
||||
const getRunAPI = await makeAPICall(
|
||||
const getRunAPI = await makeZodVerifiedAPICall(
|
||||
GetDatasetRunV1Response,
|
||||
"GET",
|
||||
|
||||
`/api/public/datasets/${encodeURIComponent("dataset name")}/runs/${encodeURIComponent("run + only + observation")}`,
|
||||
);
|
||||
expect(getRunAPI.status).toBe(200);
|
||||
@@ -337,14 +614,17 @@ describe("/api/public/datasets and /api/public/dataset-items API Endpoints", ()
|
||||
]),
|
||||
});
|
||||
|
||||
const runItemTrace = await makeAPICall<{
|
||||
datasetRunName: string; // field that can break if the API changes as it is not a db column
|
||||
}>("POST", "/api/public/dataset-run-items", {
|
||||
datasetItemId: "dataset-item-id",
|
||||
traceId: traceId,
|
||||
runName: "run-only-trace",
|
||||
metadata: { key: "value" },
|
||||
});
|
||||
const runItemTrace = await makeZodVerifiedAPICall(
|
||||
PostDatasetRunItemsV1Response,
|
||||
"POST",
|
||||
"/api/public/dataset-run-items",
|
||||
{
|
||||
datasetItemId: "dataset-item-id",
|
||||
traceId: traceId,
|
||||
runName: "run-only-trace",
|
||||
metadata: { key: "value" },
|
||||
},
|
||||
);
|
||||
|
||||
expect(runItemTrace.status).toBe(200);
|
||||
expect(runItemTrace.body).toMatchObject({
|
||||
@@ -369,7 +649,8 @@ describe("/api/public/datasets and /api/public/dataset-items API Endpoints", ()
|
||||
observationId: null,
|
||||
});
|
||||
|
||||
const runItemBoth = await makeAPICall(
|
||||
const runItemBoth = await makeZodVerifiedAPICall(
|
||||
PostDatasetRunItemsV1Response,
|
||||
"POST",
|
||||
"/api/public/dataset-run-items",
|
||||
{
|
||||
@@ -399,6 +680,149 @@ describe("/api/public/datasets and /api/public/dataset-items API Endpoints", ()
|
||||
});
|
||||
});
|
||||
|
||||
it("GET /api/public/datasets/{datasetName}/runs", async () => {
|
||||
// create multiple runs
|
||||
await makeZodVerifiedAPICall(
|
||||
PostDatasetsV1Response,
|
||||
"POST",
|
||||
"/api/public/datasets",
|
||||
{
|
||||
name: "dataset-name",
|
||||
},
|
||||
);
|
||||
await makeZodVerifiedAPICall(
|
||||
PostDatasetItemsV1Response,
|
||||
"POST",
|
||||
"/api/public/dataset-items",
|
||||
{
|
||||
datasetName: "dataset-name",
|
||||
id: "dataset-item-id",
|
||||
input: { key: "value" },
|
||||
expectedOutput: { key: "value" },
|
||||
},
|
||||
);
|
||||
const traceId = v4();
|
||||
const observationId = v4();
|
||||
const response = await makeAPICall("POST", "/api/public/ingestion", {
|
||||
batch: [
|
||||
{
|
||||
id: v4(),
|
||||
type: "trace-create",
|
||||
timestamp: new Date().toISOString(),
|
||||
body: {
|
||||
id: traceId,
|
||||
name: "trace-name",
|
||||
userId: "user-1",
|
||||
metadata: { key: "value" },
|
||||
release: "1.0.0",
|
||||
version: "2.0.0",
|
||||
},
|
||||
},
|
||||
{
|
||||
id: v4(),
|
||||
type: "observation-create",
|
||||
timestamp: new Date().toISOString(),
|
||||
body: {
|
||||
id: observationId,
|
||||
traceId: traceId,
|
||||
type: "GENERATION",
|
||||
name: "generation-name",
|
||||
startTime: "2021-01-01T00:00:00.000Z",
|
||||
endTime: "2021-01-01T00:00:00.000Z",
|
||||
modelParameters: { key: "value" },
|
||||
input: { key: "value" },
|
||||
metadata: { key: "value" },
|
||||
version: "2.0.0",
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
expect(response.status).toBe(207);
|
||||
|
||||
await makeZodVerifiedAPICall(
|
||||
PostDatasetRunItemsV1Response,
|
||||
"POST",
|
||||
"/api/public/dataset-run-items",
|
||||
{
|
||||
datasetItemId: "dataset-item-id",
|
||||
traceId: traceId,
|
||||
observationId: observationId,
|
||||
runName: "run-1",
|
||||
},
|
||||
);
|
||||
await makeZodVerifiedAPICall(
|
||||
PostDatasetRunItemsV1Response,
|
||||
"POST",
|
||||
"/api/public/dataset-run-items",
|
||||
{
|
||||
datasetItemId: "dataset-item-id",
|
||||
traceId: traceId,
|
||||
observationId: observationId,
|
||||
runName: "run-2",
|
||||
},
|
||||
);
|
||||
await makeZodVerifiedAPICall(
|
||||
PostDatasetRunItemsV1Response,
|
||||
"POST",
|
||||
"/api/public/dataset-run-items",
|
||||
{
|
||||
datasetItemId: "dataset-item-id",
|
||||
traceId: traceId,
|
||||
observationId: observationId,
|
||||
runName: "run-3",
|
||||
},
|
||||
);
|
||||
|
||||
// check runs in db
|
||||
const dbRuns = await prisma.datasetRuns.findMany({
|
||||
where: {
|
||||
dataset: { name: "dataset-name" },
|
||||
},
|
||||
orderBy: {
|
||||
createdAt: "desc",
|
||||
},
|
||||
});
|
||||
expect(dbRuns.length).toBe(3);
|
||||
const dbRunsApiResponseFormat = dbRuns.map((run) => ({
|
||||
...run,
|
||||
createdAt: run.createdAt.toISOString(),
|
||||
updatedAt: run.updatedAt.toISOString(),
|
||||
datasetName: "dataset-name",
|
||||
}));
|
||||
|
||||
// test get runs
|
||||
const getRuns = await makeZodVerifiedAPICall(
|
||||
GetDatasetRunsV1Response,
|
||||
"GET",
|
||||
`/api/public/datasets/dataset-name/runs`,
|
||||
);
|
||||
expect(getRuns.status).toBe(200);
|
||||
expect(getRuns.body).toMatchObject({
|
||||
data: dbRunsApiResponseFormat,
|
||||
meta: expect.objectContaining({
|
||||
totalItems: 3,
|
||||
page: 1,
|
||||
}),
|
||||
});
|
||||
|
||||
// test runs with pagination
|
||||
const getRunsPage2 = await makeZodVerifiedAPICall(
|
||||
GetDatasetRunsV1Response,
|
||||
"GET",
|
||||
`/api/public/datasets/dataset-name/runs?page=2&limit=1`,
|
||||
);
|
||||
expect(getRunsPage2.status).toBe(200);
|
||||
expect(getRunsPage2.body).toMatchObject({
|
||||
data: dbRunsApiResponseFormat.slice(1, 2),
|
||||
meta: expect.objectContaining({
|
||||
totalItems: 3,
|
||||
page: 2,
|
||||
totalPages: 3,
|
||||
limit: 1,
|
||||
}),
|
||||
});
|
||||
});
|
||||
|
||||
it("dataset-run-items should fail when neither trace nor observation provided", async () => {
|
||||
const response = await makeAPICall(
|
||||
"POST",
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user