Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b6e49b440e | ||
|
|
ff6e0648f3 | ||
|
|
c4fa31be9f | ||
|
|
e68394abc6 | ||
|
|
c24032391d | ||
|
|
6c7faf61f8 | ||
|
|
43bedf2afe | ||
|
|
674bdbd976 | ||
|
|
ed1f6d8563 | ||
|
|
b49d335eb0 | ||
|
|
e9bc76fe89 | ||
|
|
7cdbc7f464 | ||
|
|
237e539c4c | ||
|
|
b1357a0bae | ||
|
|
fa51789ac2 | ||
|
|
894ffdda69 | ||
|
|
0c2e6a5b5d | ||
|
|
0d7e5bfcca | ||
|
|
7cc5a9e229 | ||
|
|
629b30e42d | ||
|
|
3e98df566c | ||
|
|
0a88294202 | ||
|
|
6e9f5ace41 | ||
|
|
1b662c95e5 | ||
|
|
87e94289ff | ||
|
|
ed08331927 | ||
|
|
02e5c0119a | ||
|
|
adc39d58e4 | ||
|
|
19987e9816 | ||
|
|
746299431f | ||
|
|
74224a04cd | ||
|
|
874d574a9e | ||
|
|
13655515a2 | ||
|
|
204ce02bd1 | ||
|
|
b87f84bf67 | ||
|
|
e139b40167 | ||
|
|
f3596fac10 | ||
|
|
f1d15ea268 | ||
|
|
5183cf4859 | ||
|
|
604ff3203e | ||
|
|
ca8ba2f233 | ||
|
|
f9c5534a20 | ||
|
|
f652c75304 | ||
|
|
879a72b821 | ||
|
|
d3cb8469c0 | ||
|
|
b5b93cf710 | ||
|
|
27caeb1536 | ||
|
|
3b40c67aae | ||
|
|
79a6bc115f | ||
|
|
8aa1e0e1b8 | ||
|
|
217926b049 | ||
|
|
8ed92de662 | ||
|
|
2a9421674b | ||
|
|
0b2bf8535d | ||
|
|
b89e2423c3 | ||
|
|
43340dd6ff | ||
|
|
e529d887ba | ||
|
|
d7d79df5cf | ||
|
|
59de947865 | ||
|
|
c5ab61b356 | ||
|
|
13db6781e2 | ||
|
|
5442429f47 | ||
|
|
c01600b2b8 | ||
|
|
19101723b3 | ||
|
|
989522b22a | ||
|
|
8b4a2893d5 | ||
|
|
6719dd4158 | ||
|
|
94d87f2a42 | ||
|
|
e06272c4df | ||
|
|
f6512d7a8e | ||
|
|
5e67d897b7 | ||
|
|
9adcfde4cc | ||
|
|
bc8f8656a1 | ||
|
|
85a327ef26 | ||
|
|
46b20f4107 | ||
|
|
fb8f26a00e | ||
|
|
3d7ba475e2 | ||
|
|
03028a92b4 | ||
|
|
8a96290f03 | ||
|
|
bca09bdefe | ||
|
|
2d314250e9 | ||
|
|
4a87f402ec | ||
|
|
800b2136b9 | ||
|
|
278df5ab3c | ||
|
|
c7f9e18768 | ||
|
|
2121b40cca | ||
|
|
5ba440f2de | ||
|
|
657a3275de | ||
|
|
56bb9c8dcf | ||
|
|
66e15bcf0d | ||
|
|
5f94ca8e82 | ||
|
|
e8561fb105 | ||
|
|
18e84c6498 | ||
|
|
05152ac8d6 | ||
|
|
6cb9e24ba2 | ||
|
|
484fa424fb | ||
|
|
555aebe8c7 | ||
|
|
af29424b48 | ||
|
|
42f1c63bbf | ||
|
|
663ded0743 | ||
|
|
d7ced99d16 | ||
|
|
c979cae620 | ||
|
|
c483293b10 | ||
|
|
932913cd76 | ||
|
|
5875537132 | ||
|
|
1a53981a7a | ||
|
|
0e6b676b19 | ||
|
|
955e25555f | ||
|
|
2a11f1e8fb | ||
|
|
143a461f0c | ||
|
|
c8544690f7 | ||
|
|
05a533dd7e | ||
|
|
f3d15bb36e | ||
|
|
24c321f632 | ||
|
|
0e9cf548b2 | ||
|
|
7158a41fe9 | ||
|
|
bc384a525a | ||
|
|
3f70828df6 | ||
|
|
0988635427 | ||
|
|
41ed6fed5e | ||
|
|
eb40659e44 | ||
|
|
a22bcb1188 | ||
|
|
392fbe6c8c | ||
|
|
48d5a8f5a1 | ||
|
|
44428870f7 | ||
|
|
724a4f5d9a | ||
|
|
2358264b66 | ||
|
|
65e3b4c5ca | ||
|
|
62c413faf5 | ||
|
|
8ea14ac537 | ||
|
|
c32b84663c | ||
|
|
23ab79ba42 | ||
|
|
6ffec3337f | ||
|
|
34a2a9cddf | ||
|
|
0854429850 | ||
|
|
05fd690486 | ||
|
|
bc3f39272b | ||
|
|
96ace71f9a | ||
|
|
70415dcab5 | ||
|
|
6a18f249aa | ||
|
|
cc60d506a7 | ||
|
|
a31a7f238e | ||
|
|
41ebc77f86 | ||
|
|
cdc41cf573 | ||
|
|
c86e32f756 | ||
|
|
85576c162a | ||
|
|
93ad1e6880 | ||
|
|
97b17fc8bb | ||
|
|
04e2d9249f | ||
|
|
cb94a54f0c | ||
|
|
7c90bbed0c | ||
|
|
f133526ba6 | ||
|
|
dc014de54c | ||
|
|
c6beec1ad5 | ||
|
|
a810f7750f | ||
|
|
2b34c82e98 | ||
|
|
fe283284ef | ||
|
|
307f3d18ce | ||
|
|
46fff7148e | ||
|
|
9cce111be6 | ||
|
|
dbd7f810c1 | ||
|
|
abc3ea2b5a | ||
|
|
3d8a4f9d75 | ||
|
|
7b4bef7c2e | ||
|
|
b10353115b | ||
|
|
f3235ceccb | ||
|
|
92455b4e5a | ||
|
|
217b92d548 | ||
|
|
c6ddd3822f | ||
|
|
67d6c32ec5 | ||
|
|
d89379e693 | ||
|
|
4b8d3a764e | ||
|
|
5757fa6e38 | ||
|
|
34c3e5af08 | ||
|
|
c68ec2fca0 | ||
|
|
03dda153e8 | ||
|
|
63641521d8 | ||
|
|
29d11642c4 | ||
|
|
fb98a404d2 | ||
|
|
032fce56c4 | ||
|
|
518b8df2da | ||
|
|
70761f7ab0 | ||
|
|
a85d4ccf07 | ||
|
|
1b42eae7dd | ||
|
|
898cca56f9 | ||
|
|
83e052e769 | ||
|
|
b5f19771fa | ||
|
|
644390183b | ||
|
|
4d8e9003fe | ||
|
|
651c46a270 | ||
|
|
6b127bced0 | ||
|
|
e3a837b561 | ||
|
|
11c04f02c8 | ||
|
|
536aa138bc | ||
|
|
61b596c46a | ||
|
|
ddf43c0836 | ||
|
|
d5ab03892a | ||
|
|
918b6643d2 | ||
|
|
937b4c0f0f |
+1
-1
@@ -1,4 +1,4 @@
|
||||
[codespell]
|
||||
skip = .git,*.pdf,*.svg,package-lock.json,*.prisma,pnpm-lock.yaml
|
||||
ignore-words-list = afterall,vertx
|
||||
ignore-words-list = afterall,vertx,notIn
|
||||
|
||||
|
||||
+22
-5
@@ -34,11 +34,25 @@ EMAIL_FROM_ADDRESS="" # Defines the email address to use as the from address.
|
||||
SMTP_CONNECTION_URL="" # Defines the connection url for smtp server.
|
||||
|
||||
# S3 storage
|
||||
S3_ENDPOINT=
|
||||
S3_ACCESS_KEY_ID=
|
||||
S3_SECRET_ACCESS_KEY=
|
||||
S3_BUCKET_NAME=
|
||||
S3_REGION=
|
||||
S3_ENDPOINT=http://localhost:9090
|
||||
S3_ACCESS_KEY_ID=minio
|
||||
S3_SECRET_ACCESS_KEY=miniosecret
|
||||
S3_BUCKET_NAME=mybucket
|
||||
S3_REGION=us-east-1
|
||||
## Necessary for minio compatibility
|
||||
S3_FORCE_PATH_STYLE=true
|
||||
|
||||
# S3 Event Bucket Upload
|
||||
## Set to true to test uploading all events to S3
|
||||
LANGFUSE_S3_EVENT_UPLOAD_ENABLED=false
|
||||
LANGFUSE_S3_EVENT_UPLOAD_BUCKET=mybucket
|
||||
LANGFUSE_S3_EVENT_UPLOAD_ACCESS_KEY_ID=minio
|
||||
LANGFUSE_S3_EVENT_UPLOAD_SECRET_ACCESS_KEY=miniosecret
|
||||
LANGFUSE_S3_EVENT_UPLOAD_REGION=us-east-1
|
||||
LANGFUSE_S3_EVENT_UPLOAD_ENDPOINT=http://localhost:9090
|
||||
## Necessary for minio compatibility
|
||||
LANGFUSE_S3_EVENT_UPLOAD_FORCE_PATH_STYLE=true
|
||||
LANGFUSE_S3_EVENT_UPLOAD_PREFIX=events/
|
||||
|
||||
# Set during docker build of application
|
||||
# Used to disable environment verification at build time
|
||||
@@ -51,3 +65,6 @@ REDIS_AUTH="myredissecret"
|
||||
LANGFUSE_WORKER_PASSWORD=mybasicauthsecret
|
||||
# openssl rand -hex 32 used only here
|
||||
ENCRYPTION_KEY=0000000000000000000000000000000000000000000000000000000000000000
|
||||
|
||||
# speeds up local development by not executing init scripts on server startup
|
||||
NEXT_PUBLIC_LANGFUSE_RUN_NEXT_INIT="false"
|
||||
+44
-2
@@ -28,17 +28,28 @@ ENCRYPTION_KEY="0000000000000000000000000000000000000000000000000000000000000000
|
||||
# Use CSP headers to enforce HTTPS, optional
|
||||
# LANGFUSE_CSP_ENFORCE_HTTPS="true"
|
||||
|
||||
# Configure base path for self-hosting, optional
|
||||
# Note: You need to build the docker image with the base path set and cannot use the pre-built docker image if you set this.
|
||||
# NEXT_PUBLIC_BASE_PATH="/app"
|
||||
|
||||
# Docker only, optional
|
||||
# PORT=3000
|
||||
# HOSTNAME=localhost
|
||||
|
||||
# Opentelemetry, optional
|
||||
OTEL_EXPORTER_OTLP_ENDPOINT="http://localhost:4318"
|
||||
OTEL_SERVICE_NAME="langfuse"
|
||||
|
||||
# Default role for users who sign up, optional, can be org or org+project
|
||||
# LANGFUSE_DEFAULT_ORG_ID=
|
||||
# LANGFUSE_DEFAULT_ORG_ROLE=
|
||||
# LANGFUSE_DEFAULT_PROJECT_ID=
|
||||
# LANGFUSE_DEFAULT_PROJECT_ROLE=
|
||||
|
||||
# Logging, optional
|
||||
# LANGFUSE_LOG_LEVEL=info
|
||||
# LANGFUSE_LOG_FORMAT=text
|
||||
|
||||
# Enable experimental features, optional
|
||||
# LANGFUSE_ENABLE_EXPERIMENTAL_FEATURES=true
|
||||
|
||||
@@ -56,6 +67,10 @@ ENCRYPTION_KEY="0000000000000000000000000000000000000000000000000000000000000000
|
||||
# AUTH_GITHUB_CLIENT_ID=
|
||||
# AUTH_GITHUB_CLIENT_SECRET=
|
||||
# AUTH_GITHUB_ALLOW_ACCOUNT_LINKING=false
|
||||
# AUTH_GITLAB_CLIENT_ID=
|
||||
# AUTH_GITLAB_CLIENT_SECRET=
|
||||
# AUTH_GITLAB_ALLOW_ACCOUNT_LINKING=false
|
||||
# AUTH_GITLAB_ISSUER=
|
||||
# AUTH_AZURE_AD_CLIENT_ID=
|
||||
# AUTH_AZURE_AD_CLIENT_SECRET=
|
||||
# AUTH_AZURE_AD_TENANT_ID=
|
||||
@@ -93,10 +108,33 @@ ENCRYPTION_KEY="0000000000000000000000000000000000000000000000000000000000000000
|
||||
# S3_REGION=
|
||||
# BATCH_EXPORT_DOWNLOAD_LINK_EXPIRATION_HOURS=
|
||||
|
||||
# S3 storage for events, optional, used to persist all incoming events
|
||||
# LANGFUSE_S3_EVENT_UPLOAD_ENABLED="true"
|
||||
# LANGFUSE_S3_EVENT_UPLOAD_BUCKET=
|
||||
# Optional prefix to be used within the bucket. Must end with `/` if set
|
||||
# LANGFUSE_S3_EVENT_UPLOAD_PREFIX=events/
|
||||
# The following four options are optional and fallback to the normal SDK credential provider chain if omitted
|
||||
# See https://docs.aws.amazon.com/sdk-for-javascript/v3/developer-guide/setting-credentials-node.html
|
||||
# LANGFUSE_S3_EVENT_UPLOAD_ENDPOINT=
|
||||
# LANGFUSE_S3_EVENT_UPLOAD_REGION=
|
||||
# LANGFUSE_S3_EVENT_UPLOAD_ACCESS_KEY_ID=
|
||||
# LANGFUSE_S3_EVENT_UPLOAD_SECRET_ACCESS_KEY=
|
||||
|
||||
# Exports are streamed to S3 in pages to avoid memory issues
|
||||
# The page size can be adjusted if needed to optimize performance
|
||||
# DB_EXPORT_PAGE_SIZE=1000
|
||||
|
||||
# Automated provisioning of default resources
|
||||
# LANGFUSE_INIT_ORG_ID=org-id
|
||||
# LANGFUSE_INIT_ORG_NAME=org-name
|
||||
# LANGFUSE_INIT_PROJECT_ID=project-id
|
||||
# LANGFUSE_INIT_PROJECT_NAME=project-name
|
||||
# LANGFUSE_INIT_PROJECT_PUBLIC_KEY=pk-1234567890
|
||||
# LANGFUSE_INIT_PROJECT_SECRET_KEY=sk-1234567890
|
||||
# LANGFUSE_INIT_USER_EMAIL=user@example.com
|
||||
# LANGFUSE_INIT_USER_NAME=User Name
|
||||
# LANGFUSE_INIT_USER_PASSWORD=password
|
||||
|
||||
|
||||
|
||||
### START Enterprise Edition Configuration
|
||||
@@ -109,6 +147,12 @@ ENCRYPTION_KEY="0000000000000000000000000000000000000000000000000000000000000000
|
||||
# LANGFUSE_UI_DOCUMENTATION_HREF=https://docs.example.com
|
||||
# LANGFUSE_UI_SUPPORT_HREF=https://support.example.com
|
||||
# LANGFUSE_UI_FEEDBACK_HREF=https://feedback.example.com
|
||||
# LANGFUSE_UI_LOGO_LIGHT_MODE_HREF=https://static.langfuse.com/langfuse-dev/example-logo-light-mode.png
|
||||
# LANGFUSE_UI_LOGO_DARK_MODE_HREF=https://static.langfuse.com/langfuse-dev/example-logo-dark-mode.png
|
||||
# LANGFUSE_UI_DEFAULT_MODEL_ADAPTER=Anthropic # OpenAI, Anthropic, Azure
|
||||
# LANGFUSE_UI_DEFAULT_BASE_URL_OPENAI=https://api.openai.com/v1
|
||||
# LANGFUSE_UI_DEFAULT_BASE_URL_ANTHROPIC=https://api.anthropic.com
|
||||
# LANGFUSE_UI_DEFAULT_BASE_URL_AZURE_OPENAI=https://{instanceName}.openai.azure.com/openai/deployments
|
||||
|
||||
### END Enterprise Edition Configuration
|
||||
|
||||
@@ -145,7 +189,6 @@ ENCRYPTION_KEY="0000000000000000000000000000000000000000000000000000000000000000
|
||||
# NEXT_SENTRY_PROJECT=
|
||||
# SENTRY_AUTH_TOKEN=
|
||||
# SENTRY_CSP_REPORT_URI=
|
||||
# LANGFUSE_WORKER_BETTERSTACK_TOKEN=
|
||||
|
||||
|
||||
# Cloudflare Turnstile
|
||||
@@ -201,7 +244,6 @@ ENCRYPTION_KEY="0000000000000000000000000000000000000000000000000000000000000000
|
||||
# LANGFUSE_INGESTION_CLICKHOUSE_WRITE_BATCH_SIZE=
|
||||
# LANGFUSE_INGESTION_CLICKHOUSE_WRITE_INTERVAL_MS=
|
||||
# LANGFUSE_INGESTION_CLICKHOUSE_MAX_ATTEMPTS=
|
||||
# LANGFUSE_LOG_LEVEL=
|
||||
# LANGFUSE_LEGACY_INGESTION_WORKER_CONCURRENCY=
|
||||
# LANGFUSE_ASYNC_INGESTION_PROCESSING="true"
|
||||
# QUEUE_CONSUMER_LEGACY_INGESTION_QUEUE_IS_ENABLED="true"
|
||||
|
||||
@@ -32,6 +32,10 @@ updates:
|
||||
patterns:
|
||||
- "lodash"
|
||||
- "@types/lodash"
|
||||
express:
|
||||
patterns:
|
||||
- "express"
|
||||
- "@types/express"
|
||||
observability:
|
||||
patterns:
|
||||
- "dd-trace"
|
||||
|
||||
@@ -0,0 +1,75 @@
|
||||
name: WorkflowCall - Deploy to ECS Service
|
||||
on:
|
||||
workflow_call:
|
||||
inputs:
|
||||
environment:
|
||||
type: string
|
||||
description: Deployment environment
|
||||
required: true
|
||||
service:
|
||||
type: string
|
||||
description: Name of the service to be deployed, e.g. web-ingestion, web, or worker.
|
||||
required: true
|
||||
jobs:
|
||||
ecs-deploy:
|
||||
runs-on: ubuntu-latest
|
||||
environment: ${{ inputs.environment }}
|
||||
steps:
|
||||
- name: Get app name
|
||||
uses: winterjung/split@v2
|
||||
id: split
|
||||
with:
|
||||
msg: ${{ inputs.service }}
|
||||
separator: "-"
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
- name: Authenticate with AWS
|
||||
# GitHub/AWS recommend to use OIDC here: https://github.com/aws-actions/configure-aws-credentials?tab=readme-ov-file#oidc
|
||||
# Probably more painful to configure, but would remove all long-lived credentials.
|
||||
uses: aws-actions/configure-aws-credentials@v4
|
||||
with:
|
||||
aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
|
||||
aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
|
||||
aws-region: ${{ vars.AWS_REGION }}
|
||||
- name: Login to AWS ECR
|
||||
id: login-ecr
|
||||
uses: aws-actions/amazon-ecr-login@v2
|
||||
- name: Build, tag, and push Docker image
|
||||
env:
|
||||
REGISTRY: ${{ steps.login-ecr.outputs.registry }}
|
||||
REPOSITORY: ${{ steps.split.outputs._0 }}
|
||||
IMAGE_TAG: ${{ github.sha }}
|
||||
run: |
|
||||
docker build \
|
||||
-t $REGISTRY/$REPOSITORY:$IMAGE_TAG \
|
||||
-f ./${{ steps.split.outputs._0 }}/Dockerfile \
|
||||
--build-arg NEXT_PUBLIC_LANGFUSE_CLOUD_REGION=${{ vars.NEXT_PUBLIC_LANGFUSE_CLOUD_REGION }} \
|
||||
--build-arg NEXT_LANGFUSE_TRACING_SAMPLE_RATE=${{ vars.NEXT_LANGFUSE_TRACING_SAMPLE_RATE }} \
|
||||
--build-arg NEXT_PUBLIC_SENTRY_ENVIRONMENT=${{ vars.NEXT_PUBLIC_SENTRY_ENVIRONMENT }} \
|
||||
--build-arg NEXT_PUBLIC_DEMO_ORG_ID=${{ vars.NEXT_PUBLIC_DEMO_ORG_ID }} \
|
||||
--build-arg NEXT_PUBLIC_DEMO_PROJECT_ID=${{ vars.NEXT_PUBLIC_DEMO_PROJECT_ID }} \
|
||||
--build-arg NEXT_PUBLIC_SENTRY_DSN=${{ vars.NEXT_PUBLIC_SENTRY_DSN }} \
|
||||
--build-arg NEXT_PUBLIC_BUILD_ID=${{ github.sha }} \
|
||||
--build-arg NEXT_PUBLIC_POSTHOG_KEY=${{ vars.NEXT_PUBLIC_POSTHOG_KEY }} \
|
||||
--build-arg NEXT_PUBLIC_POSTHOG_HOST=${{ vars.NEXT_PUBLIC_POSTHOG_HOST }} \
|
||||
--build-arg NEXT_PUBLIC_CRISP_WEBSITE_ID=${{ vars.NEXT_PUBLIC_CRISP_WEBSITE_ID }} \
|
||||
--build-arg SENTRY_AUTH_TOKEN=${{ secrets.SENTRY_AUTH_TOKEN }} \
|
||||
--build-arg SENTRY_ORG=${{ vars.SENTRY_ORG }} \
|
||||
--build-arg SENTRY_PROJECT=${{ vars.SENTRY_PROJECT }} \
|
||||
.
|
||||
docker push $REGISTRY/$REPOSITORY:$IMAGE_TAG
|
||||
|
||||
- name: Render AWS ECS Task Definition
|
||||
id: render-task-definition
|
||||
uses: aws-actions/amazon-ecs-render-task-definition@v1
|
||||
with:
|
||||
container-name: ${{ inputs.service }}
|
||||
image: ${{ steps.login-ecr.outputs.registry }}/${{ steps.split.outputs._0 }}:${{ github.sha }}
|
||||
task-definition-family: ${{ inputs.environment }}-${{ inputs.service }}
|
||||
- name: Update AWS ECS Service
|
||||
uses: aws-actions/amazon-ecs-deploy-task-definition@v2
|
||||
with:
|
||||
task-definition: ${{ steps.render-task-definition.outputs.task-definition }}
|
||||
service: ${{ inputs.environment }}-${{ inputs.service }}
|
||||
cluster: ${{ inputs.environment }}-cluster
|
||||
wait-for-service-stability: true
|
||||
@@ -22,7 +22,7 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout repo
|
||||
uses: actions/checkout@v3
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Start containers
|
||||
run: docker compose -f "docker-compose.yml" up -d --build
|
||||
@@ -31,7 +31,7 @@ jobs:
|
||||
uses: pnpm/action-setup@v2.2.4
|
||||
|
||||
- name: Setup Node 18
|
||||
uses: actions/setup-node@v3
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 18
|
||||
|
||||
|
||||
@@ -0,0 +1,92 @@
|
||||
# For most projects, this workflow file will not need changing; you simply need
|
||||
# to commit it to your repository.
|
||||
#
|
||||
# You may wish to alter this file to override the set of languages analyzed,
|
||||
# or to provide custom queries or build logic.
|
||||
#
|
||||
# ******** NOTE ********
|
||||
# We have attempted to detect the languages in your repository. Please check
|
||||
# the `language` matrix defined below to confirm you have the correct set of
|
||||
# supported CodeQL languages.
|
||||
#
|
||||
name: "CodeQL"
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: ["main", "production", "v*"]
|
||||
pull_request:
|
||||
branches: ["main", "production", "v*"]
|
||||
schedule:
|
||||
- cron: "31 13 * * 0"
|
||||
|
||||
jobs:
|
||||
analyze:
|
||||
name: Analyze (${{ matrix.language }})
|
||||
# Runner size impacts CodeQL analysis time. To learn more, please see:
|
||||
# - https://gh.io/recommended-hardware-resources-for-running-codeql
|
||||
# - https://gh.io/supported-runners-and-hardware-resources
|
||||
# - https://gh.io/using-larger-runners (GitHub.com only)
|
||||
# Consider using larger runners or machines with greater resources for possible analysis time improvements.
|
||||
runs-on: ${{ (matrix.language == 'swift' && 'macos-latest') || 'ubuntu-latest' }}
|
||||
permissions:
|
||||
# required for all workflows
|
||||
security-events: write
|
||||
|
||||
# required to fetch internal or private CodeQL packs
|
||||
packages: read
|
||||
|
||||
# only required for workflows in private repositories
|
||||
actions: read
|
||||
contents: read
|
||||
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
include:
|
||||
- language: javascript-typescript
|
||||
build-mode: none
|
||||
# CodeQL supports the following values keywords for 'language': 'c-cpp', 'csharp', 'go', 'java-kotlin', 'javascript-typescript', 'python', 'ruby', 'swift'
|
||||
# Use `c-cpp` to analyze code written in C, C++ or both
|
||||
# Use 'java-kotlin' to analyze code written in Java, Kotlin or both
|
||||
# Use 'javascript-typescript' to analyze code written in JavaScript, TypeScript or both
|
||||
# To learn more about changing the languages that are analyzed or customizing the build mode for your analysis,
|
||||
# see https://docs.github.com/en/code-security/code-scanning/creating-an-advanced-setup-for-code-scanning/customizing-your-advanced-setup-for-code-scanning.
|
||||
# If you are analyzing a compiled language, you can modify the 'build-mode' for that language to customize how
|
||||
# your codebase is analyzed, see https://docs.github.com/en/code-security/code-scanning/creating-an-advanced-setup-for-code-scanning/codeql-code-scanning-for-compiled-languages
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
|
||||
# Initializes the CodeQL tools for scanning.
|
||||
- name: Initialize CodeQL
|
||||
uses: github/codeql-action/init@v3
|
||||
with:
|
||||
languages: ${{ matrix.language }}
|
||||
build-mode: ${{ matrix.build-mode }}
|
||||
# If you wish to specify custom queries, you can do so here or in a config file.
|
||||
# By default, queries listed here will override any specified in a config file.
|
||||
# Prefix the list here with "+" to use these queries and those in the config file.
|
||||
|
||||
# For more details on CodeQL's query packs, refer to: https://docs.github.com/en/code-security/code-scanning/automatically-scanning-your-code-for-vulnerabilities-and-errors/configuring-code-scanning#using-queries-in-ql-packs
|
||||
# queries: security-extended,security-and-quality
|
||||
|
||||
# If the analyze step fails for one of the languages you are analyzing with
|
||||
# "We were unable to automatically build your code", modify the matrix above
|
||||
# to set the build mode to "manual" for that language. Then modify this step
|
||||
# to build your code.
|
||||
# ℹ️ Command-line programs to run using the OS shell.
|
||||
# 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun
|
||||
- if: matrix.build-mode == 'manual'
|
||||
shell: bash
|
||||
run: |
|
||||
echo 'If you are using a "manual" build mode for one or more of the' \
|
||||
'languages you are analyzing, replace this with the commands to build' \
|
||||
'your code, for example:'
|
||||
echo ' make bootstrap'
|
||||
echo ' make release'
|
||||
exit 1
|
||||
|
||||
- name: Perform CodeQL Analysis
|
||||
uses: github/codeql-action/analyze@v3
|
||||
with:
|
||||
category: "/language:${{matrix.language}}"
|
||||
@@ -0,0 +1,105 @@
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
- production
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
service:
|
||||
description: 'Service to be deployed'
|
||||
type: choice
|
||||
options:
|
||||
- all
|
||||
- web
|
||||
- web-ingestion
|
||||
- worker
|
||||
required: true
|
||||
environment:
|
||||
description: 'Environment to deploy to'
|
||||
type: choice
|
||||
options:
|
||||
- staging
|
||||
- prod-eu-temp
|
||||
- prod-us
|
||||
required: true
|
||||
|
||||
concurrency:
|
||||
# Support concurrent `push` and `workflow_dispatch`` actions
|
||||
group: deploy-${{ github.event_name }}-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
name: Deploy to ECS
|
||||
jobs:
|
||||
affected-services:
|
||||
runs-on: ubuntu-latest
|
||||
outputs:
|
||||
services: ${{ steps.affected-services.outputs.result }}
|
||||
steps:
|
||||
- name: Get affected services
|
||||
id: affected-services
|
||||
uses: actions/github-script@v7
|
||||
with:
|
||||
script: |
|
||||
if (context.eventName === "workflow_dispatch") {
|
||||
if (context.payload.inputs.service === "all") {
|
||||
return `["web", "web-ingestion", "worker"]`
|
||||
}
|
||||
return `["${context.payload.inputs.service}"]`
|
||||
}
|
||||
if (context.eventName === "push") {
|
||||
return `["web", "web-ingestion", "worker"]`
|
||||
}
|
||||
return "[]"
|
||||
result-encoding: string
|
||||
- name: Print services to build
|
||||
uses: actions/github-script@v7
|
||||
env:
|
||||
services: ${{ steps.affected-services.outputs.result }}
|
||||
with:
|
||||
result-encoding: string
|
||||
script: |
|
||||
console.log('Services', `${process.env.services}` ?? 'n/a');
|
||||
|
||||
affected-environments:
|
||||
runs-on: ubuntu-latest
|
||||
outputs:
|
||||
environments: ${{ steps.affected-environments.outputs.result }}
|
||||
steps:
|
||||
- name: Get affected environments
|
||||
id: affected-environments
|
||||
uses: actions/github-script@v7
|
||||
with:
|
||||
script: |
|
||||
if (context.eventName === "workflow_dispatch") {
|
||||
return `["${context.payload.inputs.environment}"]`
|
||||
}
|
||||
if (context.eventName === "push") {
|
||||
if (context.ref === "refs/heads/main") {
|
||||
return `["staging"]`
|
||||
}
|
||||
if (context.ref === "refs/heads/production") {
|
||||
return `["prod-eu-temp", "prod-us"]`
|
||||
}
|
||||
}
|
||||
return "[]"
|
||||
result-encoding: string
|
||||
- name: Print environments to build
|
||||
uses: actions/github-script@v7
|
||||
env:
|
||||
environments: ${{ steps.affected-environments.outputs.result }}
|
||||
with:
|
||||
result-encoding: string
|
||||
script: |
|
||||
console.log('Environments', `${process.env.environments}` ?? 'n/a');
|
||||
|
||||
ecs-deploy:
|
||||
uses: ./.github/workflows/_deploy_ecs_service.yml
|
||||
needs: [affected-services, affected-environments]
|
||||
secrets: inherit
|
||||
strategy:
|
||||
matrix:
|
||||
service: ${{ fromJson(needs.affected-services.outputs.services) }}
|
||||
environment: ${{ fromJson(needs.affected-environments.outputs.environments) }}
|
||||
with:
|
||||
service: ${{ matrix.service }}
|
||||
environment: ${{ matrix.environment }}
|
||||
@@ -14,10 +14,10 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v2
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Setup node
|
||||
uses: actions/setup-node@v2
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 18
|
||||
|
||||
|
||||
@@ -16,11 +16,11 @@ jobs:
|
||||
lint:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/checkout@v4
|
||||
- uses: pnpm/action-setup@v3
|
||||
with:
|
||||
version: 9.5.0
|
||||
- uses: actions/setup-node@v3
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 20
|
||||
cache: "pnpm"
|
||||
@@ -35,24 +35,20 @@ jobs:
|
||||
run: pnpm run lint
|
||||
|
||||
test-docker-build:
|
||||
timeout-minutes: 20
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v3
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Set NEXT_PUBLIC_BUILD_ID
|
||||
run: echo "NEXT_PUBLIC_BUILD_ID=$(git rev-parse --short HEAD)" >> $GITHUB_ENV
|
||||
|
||||
- name: Build and run both images from compose
|
||||
run: |
|
||||
docker compose -f docker-compose.build.yml up -d
|
||||
sleep 5 # Wait for PostgreSQL to accept connections
|
||||
|
||||
- name: Check server health
|
||||
run: |
|
||||
timeout 10 bash -c 'until curl -f http://localhost:3000/api/public/health; do sleep 2; done'
|
||||
|
||||
- name: Check worker health
|
||||
run: |
|
||||
timeout 10 bash -c 'until curl -f http://localhost:3030/api/health; do sleep 2; done'
|
||||
|
||||
- name: Ensure no unhealthy status
|
||||
run: |
|
||||
if docker-compose ps | grep "(unhealthy)"; then
|
||||
@@ -62,6 +58,14 @@ jobs:
|
||||
echo "All services are healthy"
|
||||
fi
|
||||
|
||||
- name: Check worker health
|
||||
run: |
|
||||
timeout 10 bash -c 'until curl -f http://localhost:3030/api/health; do sleep 2; done'
|
||||
|
||||
- name: Check server health
|
||||
run: |
|
||||
timeout 10 bash -c 'until curl -f http://localhost:3000/api/public/health; do sleep 2; done'
|
||||
|
||||
tests-web:
|
||||
timeout-minutes: 20
|
||||
runs-on: ubuntu-latest
|
||||
@@ -76,12 +80,12 @@ jobs:
|
||||
with:
|
||||
swap-size-gb: 10
|
||||
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/checkout@v4
|
||||
- uses: pnpm/action-setup@v3
|
||||
with:
|
||||
version: 9.5.0
|
||||
- name: Use Node.js ${{ matrix.node-version }}
|
||||
uses: actions/setup-node@v3
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: ${{ matrix.node-version }}
|
||||
cache: "pnpm"
|
||||
@@ -94,7 +98,7 @@ jobs:
|
||||
- name: Load default env
|
||||
run: |
|
||||
cp .env.dev.example .env
|
||||
grep -v -e '^REDIS_HOST=' .env.dev.example > .env
|
||||
grep -v -e '^S3_BUCKET_NAME=' -e '^REDIS_HOST=' -e '^NEXT_PUBLIC_LANGFUSE_RUN_NEXT_INIT=' .env.dev.example > .env
|
||||
|
||||
- name: Run + migrate
|
||||
run: |
|
||||
@@ -107,17 +111,22 @@ jobs:
|
||||
- name: Seed DB
|
||||
run: |
|
||||
pnpm run db:migrate
|
||||
pnpm run db:seed
|
||||
|
||||
- name: Build
|
||||
run: pnpm run build
|
||||
env:
|
||||
SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}
|
||||
SENTRY_ORG: ${{ vars.SENTRY_ORG }}
|
||||
SENTRY_PROJECT: ${{ vars.SENTRY_PROJECT }}
|
||||
|
||||
- name: Start Langfuse
|
||||
run: (pnpm run start&)
|
||||
env:
|
||||
LANGFUSE_INIT_ORG_ID: "seed-org-id"
|
||||
LANGFUSE_INIT_ORG_NAME: "Seed Org"
|
||||
LANGFUSE_INIT_PROJECT_ID: "7a88fb47-b4e2-43b8-a06c-a5ce950dc53a"
|
||||
LANGFUSE_INIT_PROJECT_NAME: "Seed Project"
|
||||
LANGFUSE_INIT_PROJECT_PUBLIC_KEY: "pk-lf-1234567890"
|
||||
LANGFUSE_INIT_PROJECT_SECRET_KEY: "sk-lf-1234567890"
|
||||
LANGFUSE_INIT_USER_EMAIL: "demo@langfuse.com"
|
||||
LANGFUSE_INIT_USER_NAME: "Demo User"
|
||||
LANGFUSE_INIT_USER_PASSWORD: "password"
|
||||
|
||||
- name: run tests
|
||||
run: pnpm --filter=web run test
|
||||
@@ -136,13 +145,13 @@ jobs:
|
||||
with:
|
||||
swap-size-gb: 10
|
||||
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/checkout@v4
|
||||
- uses: pnpm/action-setup@v3
|
||||
with:
|
||||
version: 9.5.0
|
||||
|
||||
- name: Use Node.js ${{ matrix.node-version }}
|
||||
uses: actions/setup-node@v3
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: ${{ matrix.node-version }}
|
||||
cache: "pnpm"
|
||||
@@ -193,11 +202,11 @@ jobs:
|
||||
e2e-tests:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/checkout@v4
|
||||
- uses: pnpm/action-setup@v3
|
||||
with:
|
||||
version: 9.5.0
|
||||
- uses: actions/setup-node@v3
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 20
|
||||
cache: "pnpm"
|
||||
@@ -235,11 +244,11 @@ jobs:
|
||||
e2e-server-tests:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/checkout@v4
|
||||
- uses: pnpm/action-setup@v3
|
||||
with:
|
||||
version: 9.5.0
|
||||
- uses: actions/setup-node@v3
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 20
|
||||
cache: "pnpm"
|
||||
@@ -313,13 +322,16 @@ jobs:
|
||||
with:
|
||||
version: 9.5.0
|
||||
- name: Setup node
|
||||
uses: actions/setup-node@v3
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 20
|
||||
cache-dependency-path: "pnpm-lock.yaml"
|
||||
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v3
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Set NEXT_PUBLIC_BUILD_ID
|
||||
run: echo "NEXT_PUBLIC_BUILD_ID=$(git rev-parse --short HEAD)" >> $GITHUB_ENV
|
||||
|
||||
- name: Log in to the GitHub Container registry
|
||||
uses: docker/login-action@v2
|
||||
|
||||
@@ -1,29 +0,0 @@
|
||||
"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 }}
|
||||
@@ -1,29 +0,0 @@
|
||||
"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 }}
|
||||
@@ -1,29 +0,0 @@
|
||||
"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 }}
|
||||
@@ -1,29 +0,0 @@
|
||||
"on":
|
||||
push:
|
||||
branches:
|
||||
- production
|
||||
name: Deploy to worker (EU 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: "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_4060 }}
|
||||
@@ -1,29 +0,0 @@
|
||||
"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 }}
|
||||
@@ -1,29 +0,0 @@
|
||||
"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 }}
|
||||
Vendored
+2
-2
@@ -2,10 +2,10 @@
|
||||
"version": "0.2.0",
|
||||
"configurations": [
|
||||
{
|
||||
"name": "Next.js: debug server-side",
|
||||
"name": "Web and Worker: server side",
|
||||
"type": "node-terminal",
|
||||
"request": "launch",
|
||||
"command": "npm run dev"
|
||||
"command": "pnpm run dev"
|
||||
},
|
||||
{
|
||||
"name": "Next.js: debug client-side",
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||

|
||||

|
||||
|
||||
<div align="center"><h1> Langfuse: Open Source LLM Observability & Engineering Platform</h4></div>
|
||||
<div align="center"><h2> Debug and improve your LLM app</h4></div>
|
||||
<div align="center"><h3> LLM Observability, Prompt Management, LLM Evaluations, Datasets, LLM Metrics and Prompt Playground</h4></div>
|
||||
<div align="center"><h1>Langfuse: Open Source LLM Engineering Platform</h1></div>
|
||||
<div align="center"><h4>LLM Observability, Prompt Management, LLM Evaluations,<br/>Datasets, LLM Metrics, and Prompt Playground</h4></div>
|
||||
|
||||
<div align="center">
|
||||
<div>
|
||||
@@ -28,15 +27,15 @@
|
||||
</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>
|
||||
<span>We're hiring. <a href="https://langfuse.com/careers"><strong>Join us</strong></a> in 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>
|
||||
<a href="https://www.ycombinator.com/companies/langfuse"><img src="https://img.shields.io/badge/Y%20Combinator-W23-orange?style=flat-square" alt="Y Combinator W23"></a>
|
||||
<a href="https://github.com/langfuse/langfuse/pkgs/container/langfuse"><img alt="Docker Image" src="https://img.shields.io/badge/docker-langfuse-blue?logo=Docker&logoColor=white&style=flat-square"></a>
|
||||
<a href="https://www.npmjs.com/package/langfuse"><img src="https://img.shields.io/npm/v/langfuse?style=flat-square&label=npm+langfuse" alt="langfuse npm package"></a>
|
||||
<a href="https://pypi.python.org/pypi/langfuse"><img src="https://img.shields.io/pypi/v/langfuse.svg?style=flat-square&label=pypi+langfuse" alt="langfuse Python package on PyPi"></a>
|
||||
<a href="https://pypi.python.org/pypi/langfuse"><img src="https://img.shields.io/pypi/dm/langfuse?style=flat-square&logo=python&logoColor=white&label=pypi%20langfuse&color=blue" alt="langfuse Python package on PyPi"></a>
|
||||
<a href="https://www.npmjs.com/package/langfuse"><img src="https://img.shields.io/npm/dm/langfuse?style=flat-square&logo=npm&logoColor=white&label=npm%20langfuse&color=blue" alt="langfuse npm package"></a>
|
||||
</div>
|
||||
</div>
|
||||
</br>
|
||||
@@ -93,10 +92,10 @@ docker compose up -d
|
||||
|
||||
### Self-host (docker)
|
||||
|
||||
Langfuse is simple to self-host and keep updated. It currently requires only a single docker container.
|
||||
Langfuse is simple to self-host and keep updated. It currently requires only a single docker container and a postgres database.
|
||||
[→ Self Hosting Instructions](https://langfuse.com/docs/deployment/self-host)
|
||||
|
||||
Templated deployments: [Railway, GCP Cloud Run, AWS Fargate, Kubernetes and others](https://langfuse.com/docs/deployment/self-host#platform-specific-information)
|
||||
Templated deployments: [Railway, GCP, AWS, Azure, Kubernetes and others](https://langfuse.com/docs/deployment/self-host#platform-specific-information)
|
||||
|
||||
## Get Started
|
||||
|
||||
@@ -108,31 +107,31 @@ You need a Langfuse public and secret key to get started. Sign up [here](https:/
|
||||
|
||||
Note: We recommend using our fully async, typed [SDKs](https://langfuse.com/docs/sdk) that allow you to instrument any LLM application with any underlying model. They are available in [Python (Decorators)](https://langfuse.com/docs/sdk/python) & [JS/TS](https://langfuse.com/docs/sdk/typescript). The SDKs will always be the most fully featured and stable way to ingest data into Langfuse.
|
||||
|
||||
You may want to use another integration to get started quickly or implement a use case that we do not yet support. However, we recommend to migrate to the Langfuse SDKs over time to ensure performance and stability.
|
||||
|
||||
See the [→ Quickstart](https://langfuse.com/docs/get-started) to integrate Langfuse.
|
||||
|
||||
### LLM Observability Integrations
|
||||
|
||||
| Integration | Supports | Description |
|
||||
| -------------------------------------------------------- | -------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------ |
|
||||
| [SDK](/docs/sdk) | Python, JS/TS | Manual instrumentation using the SDKs for full flexibility. |
|
||||
| [OpenAI](/docs/integrations/openai) | Python, JS/TS | Automated instrumentation using drop-in replacement of OpenAI SDK. |
|
||||
| [Langchain](/docs/integrations/langchain) | Python, JS/TS | Automated instrumentation by passing callback handler to Langchain application. |
|
||||
| [LlamaIndex](/docs/integrations/llama-index/get-started) | Python | Automated instrumentation via LlamaIndex callback system. |
|
||||
| [Haystack](/docs/integrations/haystack) | Python | Automated instrumentation via Haystack content tracing system. |
|
||||
| [LiteLLM](/docs/integrations/litellm) | Python, JS/TS (proxy only) | Use any LLM as a drop in replacement for GPT. Use Azure, OpenAI, Cohere, Anthropic, Ollama, VLLM, Sagemaker, HuggingFace, Replicate (100+ LLMs). |
|
||||
| [API](/docs/api) | | Directly call the public API. OpenAPI spec available. |
|
||||
| Integration | Supports | Description |
|
||||
| ---------------------------------------------------------------------------- | -------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------ |
|
||||
| [SDK](https://langfuse.com/docs/sdk) | Python, JS/TS | Manual instrumentation using the SDKs for full flexibility. |
|
||||
| [OpenAI](https://langfuse.com/docs/integrations/openai) | Python, JS/TS | Automated instrumentation using drop-in replacement of OpenAI SDK. |
|
||||
| [Langchain](https://langfuse.com/docs/integrations/langchain) | Python, JS/TS | Automated instrumentation by passing callback handler to Langchain application. |
|
||||
| [LlamaIndex](https://langfuse.com/docs/integrations/llama-index/get-started) | Python | Automated instrumentation via LlamaIndex callback system. |
|
||||
| [Haystack](https://langfuse.com/docs/integrations/haystack) | Python | Automated instrumentation via Haystack content tracing system. |
|
||||
| [LiteLLM](https://langfuse.com/docs/integrations/litellm) | Python, JS/TS (proxy only) | Use any LLM as a drop in replacement for GPT. Use Azure, OpenAI, Cohere, Anthropic, Ollama, VLLM, Sagemaker, HuggingFace, Replicate (100+ LLMs). |
|
||||
| [Vercel AI SDK](https://langfuse.com/docs/integrations/vercel-ai-sdk) | JS/TS | TypeScript toolkit designed to help developers build AI-powered applications with React, Next.js, Vue, Svelte, Node.js. |
|
||||
| [API](https://langfuse.com/docs/api) | | Directly call the public API. OpenAPI spec available. |
|
||||
|
||||
Packages that integrate with Langfuse:
|
||||
Packages integrated with Langfuse:
|
||||
|
||||
| Name | Description |
|
||||
| ---------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------- |
|
||||
| [Instructor](/docs/integrations/instructor) | Library to get structured LLM outputs (JSON, Pydantic) |
|
||||
| [Mirascope](/docs/integrations/mirascope) | Python toolkit for building LLM applications. |
|
||||
| [AI SDK by Vercel](/docs/sdk/typescript/example-vercel-ai) | Typescript SDK that makes streaming LLM outputs super easy. |
|
||||
| [Flowise](/docs/integrations/flowise) | JS/TS no-code builder for customized LLM flows. |
|
||||
| [Langflow](/docs/integrations/langflow) | Python-based UI for LangChain, designed with react-flow to provide an effortless way to experiment and prototype flows. |
|
||||
| Name | Description |
|
||||
| --------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------- |
|
||||
| [Instructor](https://langfuse.com/docs/integrations/instructor) | Library to get structured LLM outputs (JSON, Pydantic) |
|
||||
| [Dify](https://langfuse.com/docs/integrations/dify) | Open source LLM app development platform with no-code builder. |
|
||||
| [Ollama](https://langfuse.com/docs/integrations/ollama) | Easily run open source LLMs on your own machine. |
|
||||
| [Mirascope](https://langfuse.com/docs/integrations/mirascope) | Python toolkit for building LLM applications. |
|
||||
| [Flowise](https://langfuse.com/docs/integrations/flowise) | JS/TS no-code builder for customized LLM flows. |
|
||||
| [Langflow](https://langfuse.com/docs/integrations/langflow) | Python-based UI for LangChain, designed with react-flow to provide an effortless way to experiment and prototype flows. |
|
||||
|
||||
## Questions and feedback
|
||||
|
||||
@@ -140,13 +139,13 @@ Packages that integrate with Langfuse:
|
||||
|
||||
- [Roadmap](https://langfuse.com/roadmap)
|
||||
- [GitHub Discussions](https://github.com/orgs/langfuse/discussions)
|
||||
- [Feature Requests](https://langfuse.com/idea
|
||||
- [Feature Requests](https://langfuse.com/ideas)
|
||||
|
||||
### Support and feedback
|
||||
|
||||
In order of preference the best way to communicate with us:
|
||||
|
||||
- [GitHub Discussions](https://github.com/orgs/langfuse/discussions): Contribute [ideas](https://langfuse.com/idea) [support requests](https://github.com/orgs/langfuse/discussions/categories/support) and [report bugs](https://github.com/langfuse/langfuse/issues/new?labels=%F0%9F%90%9E%E2%9D%94+unconfirmed+bug&projects=&template=bug_report.yml&title=bug%3A+) (preferred as we create a permanent, indexed artifact for other community members)
|
||||
- [GitHub Discussions](https://github.com/orgs/langfuse/discussions) (preferred): Contribute [ideas](https://langfuse.com/ideas), [support requests](https://langfuse.com/gh-support) and [report bugs](https://langfuse.com/issues)
|
||||
- [Discord](https://langfuse.com/discord): community support
|
||||
- Privately: contact at langfuse dot com
|
||||
|
||||
|
||||
@@ -22,6 +22,15 @@ services:
|
||||
- LANGFUSE_ENABLE_EXPERIMENTAL_FEATURES=${LANGFUSE_ENABLE_EXPERIMENTAL_FEATURES:-false}
|
||||
- LANGFUSE_WORKER_HOST=${LANGFUSE_WORKER_HOST:-worker}
|
||||
- LANGFUSE_WORKER_PASSWORD=${LANGFUSE_WORKER_PASSWORD:-mybasicauthsecret}
|
||||
- LANGFUSE_INIT_ORG_ID=${LANGFUSE_INIT_ORG_ID:-}
|
||||
- LANGFUSE_INIT_ORG_NAME=${LANGFUSE_INIT_ORG_NAME:-}
|
||||
- LANGFUSE_INIT_PROJECT_ID=${LANGFUSE_INIT_PROJECT_ID:-}
|
||||
- LANGFUSE_INIT_PROJECT_NAME=${LANGFUSE_INIT_PROJECT_NAME:-}
|
||||
- LANGFUSE_INIT_PROJECT_PUBLIC_KEY=${LANGFUSE_INIT_PROJECT_PUBLIC_KEY:-}
|
||||
- LANGFUSE_INIT_PROJECT_SECRET_KEY=${LANGFUSE_INIT_PROJECT_SECRET_KEY:-}
|
||||
- LANGFUSE_INIT_USER_EMAIL=${LANGFUSE_INIT_USER_EMAIL:-}
|
||||
- LANGFUSE_INIT_USER_NAME=${LANGFUSE_INIT_USER_NAME:-}
|
||||
- LANGFUSE_INIT_USER_PASSWORD=${LANGFUSE_INIT_USER_PASSWORD:-}
|
||||
restart: always
|
||||
healthcheck:
|
||||
test: ["CMD", "curl", "-f", "http://localhost:3000/api/public/health"]
|
||||
|
||||
@@ -17,6 +17,38 @@ services:
|
||||
depends_on:
|
||||
- postgres
|
||||
|
||||
minio:
|
||||
image: minio/minio
|
||||
container_name: minio
|
||||
command: server /data --console-address ":9001"
|
||||
environment:
|
||||
MINIO_ACCESS_KEY: minio
|
||||
MINIO_SECRET_KEY: miniosecret
|
||||
ports:
|
||||
- "9090:9000"
|
||||
- "9091:9001"
|
||||
volumes:
|
||||
- langfuse_minio_data:/data
|
||||
healthcheck:
|
||||
test: ["CMD", "mc", "ready", "local"]
|
||||
interval: 10s
|
||||
timeout: 5s
|
||||
retries: 5
|
||||
start_period: 5s
|
||||
|
||||
miniocreatebucket:
|
||||
image: minio/mc
|
||||
container_name: miniocreatebucket
|
||||
entrypoint: ["/bin/sh", "-c"]
|
||||
command: >
|
||||
"mc alias set minio http://minio:9000 minio miniosecret &&
|
||||
mc rm -r --force minio/mybucket || true &&
|
||||
mc mb minio/mybucket &&
|
||||
mc policy set download minio/mybucket"
|
||||
depends_on:
|
||||
minio:
|
||||
condition: service_healthy
|
||||
|
||||
redis:
|
||||
image: redis:7.2.4
|
||||
restart: always
|
||||
@@ -45,3 +77,5 @@ volumes:
|
||||
driver: local
|
||||
langfuse_clickhouse_logs:
|
||||
driver: local
|
||||
langfuse_minio_data:
|
||||
driver: local
|
||||
|
||||
@@ -14,6 +14,15 @@ services:
|
||||
- NEXTAUTH_URL=http://localhost:3000
|
||||
- TELEMETRY_ENABLED=${TELEMETRY_ENABLED:-true}
|
||||
- LANGFUSE_ENABLE_EXPERIMENTAL_FEATURES=${LANGFUSE_ENABLE_EXPERIMENTAL_FEATURES:-false}
|
||||
- LANGFUSE_INIT_ORG_ID=${LANGFUSE_INIT_ORG_ID:-}
|
||||
- LANGFUSE_INIT_ORG_NAME=${LANGFUSE_INIT_ORG_NAME:-}
|
||||
- LANGFUSE_INIT_PROJECT_ID=${LANGFUSE_INIT_PROJECT_ID:-}
|
||||
- LANGFUSE_INIT_PROJECT_NAME=${LANGFUSE_INIT_PROJECT_NAME:-}
|
||||
- LANGFUSE_INIT_PROJECT_PUBLIC_KEY=${LANGFUSE_INIT_PROJECT_PUBLIC_KEY:-}
|
||||
- LANGFUSE_INIT_PROJECT_SECRET_KEY=${LANGFUSE_INIT_PROJECT_SECRET_KEY:-}
|
||||
- LANGFUSE_INIT_USER_EMAIL=${LANGFUSE_INIT_USER_EMAIL:-}
|
||||
- LANGFUSE_INIT_USER_NAME=${LANGFUSE_INIT_USER_NAME:-}
|
||||
- LANGFUSE_INIT_USER_PASSWORD=${LANGFUSE_INIT_USER_PASSWORD:-}
|
||||
|
||||
db:
|
||||
image: postgres
|
||||
|
||||
+4
-3
@@ -25,8 +25,9 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@langfuse/shared": "workspace:*",
|
||||
"@opentelemetry/api": ">=1.0.0 <1.9.0",
|
||||
"next": "^14.2.6",
|
||||
"@opentelemetry/api": ">=1.0.0 <1.10.0",
|
||||
"axios": "^1.7.7",
|
||||
"next": "^14.2.15",
|
||||
"next-auth": "^4.24.7",
|
||||
"zod": "^3.23.8"
|
||||
},
|
||||
@@ -39,7 +40,7 @@
|
||||
"eslint-config-prettier": "^9.1.0",
|
||||
"eslint-config-standard": "^17.1.0",
|
||||
"eslint-plugin-prettier": "^5.1.3",
|
||||
"prettier": "^3.2.5",
|
||||
"prettier": "^3.3.3",
|
||||
"ts-node": "^10.9.2",
|
||||
"tsc-watch": "^6.2.0",
|
||||
"typescript": "^5.4.5"
|
||||
|
||||
+5
-2
@@ -1,9 +1,12 @@
|
||||
import { z } from "zod";
|
||||
import { env as sharedEnv } from "@langfuse/shared";
|
||||
import { env as sharedEnv, removeEmptyEnvVariables } from "@langfuse/shared";
|
||||
|
||||
const EnvSchema = z.object({
|
||||
NEXT_PUBLIC_LANGFUSE_CLOUD_REGION: z.string().optional(),
|
||||
LANGFUSE_EE_LICENSE_KEY: z.string().optional(),
|
||||
});
|
||||
|
||||
export const env = { ...sharedEnv, ...EnvSchema.parse(process.env) };
|
||||
export const env = {
|
||||
...sharedEnv,
|
||||
...EnvSchema.parse(removeEmptyEnvVariables(process.env)),
|
||||
};
|
||||
|
||||
@@ -31,10 +31,10 @@ service:
|
||||
docs: Optional filter for metrics where traces include all of these tags
|
||||
fromTimestamp:
|
||||
type: optional<datetime>
|
||||
docs: Optional filter to only include traces on or after a certain datetime (ISO 8601)
|
||||
docs: Optional filter to only include traces and observations on or after a certain datetime (ISO 8601)
|
||||
toTimestamp:
|
||||
type: optional<datetime>
|
||||
docs: Optional filter to only include traces before a certain datetime (ISO 8601)
|
||||
docs: Optional filter to only include traces and observations before a certain datetime (ISO 8601)
|
||||
response: DailyMetrics
|
||||
types:
|
||||
DailyMetrics:
|
||||
|
||||
@@ -3,32 +3,22 @@ groups:
|
||||
local:
|
||||
generators:
|
||||
- name: fernapi/fern-openapi
|
||||
version: 0.0.28
|
||||
version: 0.0.31
|
||||
output:
|
||||
location: local-file-system
|
||||
path: ../../../web/public/generated/api
|
||||
config:
|
||||
namespaceExport: Langfuse
|
||||
allowCustomFetcher: true
|
||||
- name: fernapi/fern-python-sdk
|
||||
version: 1.1.0-rc2
|
||||
version: 2.16.0
|
||||
output:
|
||||
location: local-file-system
|
||||
path: ../../../generated/python
|
||||
config:
|
||||
client_class_name: FernLangfuse
|
||||
improved_imports: false
|
||||
inline_request_params: false
|
||||
pydantic_config:
|
||||
require_optional_fields: false
|
||||
use_str_enums: false
|
||||
- name: fernapi/fern-typescript-node-sdk
|
||||
version: 0.7.1
|
||||
output:
|
||||
location: local-file-system
|
||||
path: ../../../generated/typescript-server
|
||||
config:
|
||||
namespaceExport: Langfuse
|
||||
allowCustomFetcher: true
|
||||
- name: fernapi/fern-postman
|
||||
version: 0.0.45
|
||||
output:
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
{
|
||||
"organization": "finto",
|
||||
"version": "0.30.3"
|
||||
"version": "0.43.7"
|
||||
}
|
||||
+2
-2
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "langfuse",
|
||||
"version": "2.76.0",
|
||||
"version": "2.84.0",
|
||||
"author": "engineering@langfuse.com",
|
||||
"license": "MIT",
|
||||
"private": true,
|
||||
@@ -33,7 +33,7 @@
|
||||
"braces": "3.0.3",
|
||||
"dotenv-cli": "^7.4.2",
|
||||
"husky": "^9.0.11",
|
||||
"prettier": "^3.2.5",
|
||||
"prettier": "^3.3.3",
|
||||
"release-it": "^17.3.0",
|
||||
"turbo": "^1.13.4"
|
||||
},
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
"@typescript-eslint/eslint-plugin": "^7.1.0",
|
||||
"@typescript-eslint/parser": "^7.12.0",
|
||||
"@vercel/style-guide": "^6.0.0",
|
||||
"eslint-config-next": "^14.2.6",
|
||||
"eslint-config-next": "^14.2.14",
|
||||
"eslint-config-prettier": "^9.1.0",
|
||||
"eslint-config-turbo": "^1.13.4",
|
||||
"eslint-plugin-only-warn": "^1.1.0",
|
||||
|
||||
@@ -21,6 +21,10 @@
|
||||
"import": "./dist/src/server/index.js",
|
||||
"require": "./dist/src/server/index.js"
|
||||
},
|
||||
"./src/server/auth/apiKeys": {
|
||||
"import": "./dist/src/server/auth/apiKeys.js",
|
||||
"require": "./dist/src/server/auth/apiKeys.js"
|
||||
},
|
||||
"./encryption": {
|
||||
"import": "./dist/src/encryption/index.js",
|
||||
"require": "./dist/src/encryption/index.js"
|
||||
@@ -51,59 +55,61 @@
|
||||
"dependencies": {
|
||||
"@anthropic-ai/tokenizer": "^0.0.4",
|
||||
"@aws-sdk/client-s3": "^3.627.0",
|
||||
"@aws-sdk/lib-storage": "^3.568.0",
|
||||
"@aws-sdk/lib-storage": "^3.667.0",
|
||||
"@aws-sdk/s3-request-presigner": "^3.554.0",
|
||||
"@clickhouse/client": "^1.4.0",
|
||||
"@langchain/anthropic": "^0.1.21",
|
||||
"@langchain/core": "^0.1.61",
|
||||
"@langchain/openai": "^0.0.33",
|
||||
"@opentelemetry/api": ">=1.0.0 <1.9.0",
|
||||
"@prisma/client": "^5.17.0",
|
||||
"@langchain/anthropic": "^0.3.1",
|
||||
"@langchain/core": "^0.3.7",
|
||||
"@langchain/openai": "^0.3.0",
|
||||
"@opentelemetry/api": ">=1.0.0 <1.10.0",
|
||||
"@prisma/client": "^5.20.0",
|
||||
"@react-email/components": "^0.0.19",
|
||||
"@react-email/render": "^0.0.15",
|
||||
"@types/bcryptjs": "^2.4.6",
|
||||
"axios": "^1.7.7",
|
||||
"bcryptjs": "^2.4.3",
|
||||
"bullmq": "^5.12.10",
|
||||
"dd-trace": "^5.19.0",
|
||||
"dd-trace": "^5.23.1",
|
||||
"decimal.js": "^10.4.3",
|
||||
"exponential-backoff": "^3.1.1",
|
||||
"ioredis": "^5.4.1",
|
||||
"kysely": "^0.27.4",
|
||||
"langchain": "^0.2.6",
|
||||
"langchain": "^0.3.2",
|
||||
"lodash": "^4.17.21",
|
||||
"next-auth": "^4.24.7",
|
||||
"nodemailer": "^6.9.13",
|
||||
"nodemailer": "^6.9.15",
|
||||
"prisma-extension-kysely": "^2.1.0",
|
||||
"uuid": "^9.0.1",
|
||||
"winston": "^3.14.2",
|
||||
"zod": "^3.23.8",
|
||||
"zod-to-json-schema": "^3.23.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@repo/eslint-config": "workspace:*",
|
||||
"@repo/typescript-config": "workspace:*",
|
||||
"@types/lodash": "^4.17.7",
|
||||
"@types/lodash": "^4.17.10",
|
||||
"@types/node": "^20.11.29",
|
||||
"@types/nodemailer": "^6.4.15",
|
||||
"@types/pg": "^8.11.6",
|
||||
"@types/nodemailer": "^6.4.16",
|
||||
"@types/pg": "^8.11.10",
|
||||
"@types/uuid": "^9.0.8",
|
||||
"@typescript-eslint/parser": "^7.12.0",
|
||||
"eslint": "^8.57.0",
|
||||
"eslint-config-prettier": "^9.1.0",
|
||||
"eslint-config-standard": "^17.1.0",
|
||||
"eslint-plugin-prettier": "^5.1.3",
|
||||
"kysely-codegen": "^0.11.0",
|
||||
"nodemon": "^3.1.3",
|
||||
"prettier": "^3.2.5",
|
||||
"prisma": "^5.17.0",
|
||||
"kysely-codegen": "^0.16.8",
|
||||
"nodemon": "^3.1.7",
|
||||
"prettier": "^3.3.3",
|
||||
"prisma": "^5.20.0",
|
||||
"prisma-erd-generator": "^1.11.2",
|
||||
"prisma-kysely": "^1.8.0",
|
||||
"ts-node": "^10.9.2",
|
||||
"tsc-watch": "^6.2.0",
|
||||
"typescript": "^5.4.5",
|
||||
"vitest": "^1.5.3"
|
||||
"vitest": "^2.1.2"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/react": "^18.2.79",
|
||||
"react": "^18.0.0"
|
||||
"@types/react": "~18.2.79",
|
||||
"react": "~18.2.0"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -37,6 +37,16 @@ export const ScoreDataType = {
|
||||
BOOLEAN: "BOOLEAN"
|
||||
} as const;
|
||||
export type ScoreDataType = (typeof ScoreDataType)[keyof typeof ScoreDataType];
|
||||
export const AnnotationQueueStatus = {
|
||||
PENDING: "PENDING",
|
||||
COMPLETED: "COMPLETED"
|
||||
} as const;
|
||||
export type AnnotationQueueStatus = (typeof AnnotationQueueStatus)[keyof typeof AnnotationQueueStatus];
|
||||
export const AnnotationQueueObjectType = {
|
||||
TRACE: "TRACE",
|
||||
OBSERVATION: "OBSERVATION"
|
||||
} as const;
|
||||
export type AnnotationQueueObjectType = (typeof AnnotationQueueObjectType)[keyof typeof AnnotationQueueObjectType];
|
||||
export const DatasetStatus = {
|
||||
ACTIVE: "ACTIVE",
|
||||
ARCHIVED: "ARCHIVED"
|
||||
@@ -80,6 +90,31 @@ export type Account = {
|
||||
scope: string | null;
|
||||
id_token: string | null;
|
||||
session_state: string | null;
|
||||
refresh_token_expires_in: number | null;
|
||||
created_at: number | null;
|
||||
};
|
||||
export type AnnotationQueue = {
|
||||
id: string;
|
||||
name: string;
|
||||
description: string | null;
|
||||
score_config_ids: Generated<string[]>;
|
||||
project_id: string;
|
||||
created_at: Generated<Timestamp>;
|
||||
updated_at: Generated<Timestamp>;
|
||||
};
|
||||
export type AnnotationQueueItem = {
|
||||
id: string;
|
||||
queue_id: string;
|
||||
object_id: string;
|
||||
object_type: AnnotationQueueObjectType;
|
||||
status: Generated<AnnotationQueueStatus>;
|
||||
locked_at: Timestamp | null;
|
||||
locked_by_user_id: string | null;
|
||||
annotator_user_id: string | null;
|
||||
completed_at: Timestamp | null;
|
||||
project_id: string;
|
||||
created_at: Generated<Timestamp>;
|
||||
updated_at: Generated<Timestamp>;
|
||||
};
|
||||
export type ApiKey = {
|
||||
id: string;
|
||||
@@ -409,6 +444,7 @@ export type Score = {
|
||||
observation_id: string | null;
|
||||
config_id: string | null;
|
||||
string_value: string | null;
|
||||
queue_id: string | null;
|
||||
created_at: Generated<Timestamp>;
|
||||
updated_at: Generated<Timestamp>;
|
||||
data_type: Generated<ScoreDataType>;
|
||||
@@ -505,6 +541,8 @@ export type VerificationToken = {
|
||||
};
|
||||
export type DB = {
|
||||
Account: Account;
|
||||
annotation_queue_items: AnnotationQueueItem;
|
||||
annotation_queues: AnnotationQueue;
|
||||
api_keys: ApiKey;
|
||||
audit_logs: AuditLog;
|
||||
batch_exports: BatchExport;
|
||||
|
||||
+30
@@ -0,0 +1,30 @@
|
||||
-- For OpenAI reasoning models, manually tokenizing output string does not account for reasoning tokens, thus would lead to incorrect cost
|
||||
-- So no manual tokenizer config is provided
|
||||
-- Reference: https://platform.openai.com/docs/guides/reasoning/how-reasoning-works
|
||||
|
||||
INSERT INTO models (
|
||||
id,
|
||||
project_id,
|
||||
model_name,
|
||||
match_pattern,
|
||||
start_date,
|
||||
input_price,
|
||||
output_price,
|
||||
total_price,
|
||||
unit,
|
||||
tokenizer_id,
|
||||
tokenizer_config
|
||||
)
|
||||
VALUES
|
||||
-- o1-preview
|
||||
('cm10ivcdp0000gix7lelmbw80', NULL, 'o1-preview', '(?i)^(o1-preview)$', NULL, 0.000015, 0.00006, NULL, 'TOKENS', NULL, NULL),
|
||||
|
||||
-- o1-preview-2024-09-12
|
||||
('cm10ivo130000n8x7qopcjjcg', NULL, 'o1-preview-2024-09-12', '(?i)^(o1-preview-2024-09-12)$', NULL, 0.000015, 0.00006, NULL, 'TOKENS', NULL, NULL),
|
||||
|
||||
-- o1-mini
|
||||
('cm10ivwo40000r1x7gg3syjq0', NULL, 'o1-mini', '(?i)^(o1-mini)$', NULL, 0.000003, 0.000012, NULL, 'TOKENS', NULL, NULL),
|
||||
|
||||
-- o1-mini-2024-09-12
|
||||
('cm10iw6p20000wgx7it1hlb22', NULL, 'o1-mini-2024-09-12', '(?i)^(o1-mini-2024-09-12)$', NULL, 0.000003, 0.000012, NULL, 'TOKENS', NULL, NULL)
|
||||
|
||||
+2
@@ -0,0 +1,2 @@
|
||||
-- AlterTable
|
||||
ALTER TABLE "Account" ADD COLUMN "refresh_token_expires_in" INTEGER;
|
||||
+3
@@ -0,0 +1,3 @@
|
||||
-- DropIndex
|
||||
-- Removes indexes that are covered by other indexes on the same table, e.g. (project_id) if (project_id, timestamp) exists.
|
||||
DROP INDEX CONCURRENTLY IF EXISTS "traces_project_id_idx"; -- covered by traces_project_id_timestamp_idx
|
||||
+3
@@ -0,0 +1,3 @@
|
||||
-- DropIndex
|
||||
-- Removes indexes that are covered by other indexes on the same table, e.g. (project_id) if (project_id, timestamp) exists.
|
||||
DROP INDEX CONCURRENTLY IF EXISTS "datasets_project_id_idx"; -- covered by datasets_project_id_name_key (unique)
|
||||
+3
@@ -0,0 +1,3 @@
|
||||
-- DropIndex
|
||||
-- Removes indexes that are covered by other indexes on the same table, e.g. (project_id) if (project_id, timestamp) exists.
|
||||
DROP INDEX CONCURRENTLY IF EXISTS "eval_templates_project_id_idx"; -- covered by eval_templates_project_id_id_idx
|
||||
+3
@@ -0,0 +1,3 @@
|
||||
-- DropIndex
|
||||
-- Removes indexes that are covered by other indexes on the same table, e.g. (project_id) if (project_id, timestamp) exists.
|
||||
DROP INDEX CONCURRENTLY IF EXISTS "job_configurations_project_id_idx"; -- covered by job_configurations_project_id_id_idx
|
||||
+3
@@ -0,0 +1,3 @@
|
||||
-- DropIndex
|
||||
-- Removes indexes that are covered by other indexes on the same table, e.g. (project_id) if (project_id, timestamp) exists.
|
||||
DROP INDEX CONCURRENTLY IF EXISTS "job_executions_project_id_idx"; -- covered by job_executions_project_id_id_idx
|
||||
+3
@@ -0,0 +1,3 @@
|
||||
-- DropIndex
|
||||
-- Removes indexes that are covered by other indexes on the same table, e.g. (project_id) if (project_id, timestamp) exists.
|
||||
DROP INDEX CONCURRENTLY IF EXISTS "llm_api_keys_project_id_provider_idx"; -- covered by llm_api_keys_project_id_provider_key (unique)
|
||||
+3
@@ -0,0 +1,3 @@
|
||||
-- DropIndex
|
||||
-- Removes indexes that are covered by other indexes on the same table, e.g. (project_id) if (project_id, timestamp) exists.
|
||||
DROP INDEX CONCURRENTLY IF EXISTS "models_project_id_model_name_idx"; -- covered by models_project_id_model_name_start_date_unit_key (unique)
|
||||
+3
@@ -0,0 +1,3 @@
|
||||
-- DropIndex
|
||||
-- Removes indexes that are covered by other indexes on the same table, e.g. (project_id) if (project_id, timestamp) exists.
|
||||
DROP INDEX CONCURRENTLY IF EXISTS "models_project_id_model_name_start_date_unit_idx"; -- covered by models_project_id_model_name_start_date_unit_key (unique)
|
||||
+3
@@ -0,0 +1,3 @@
|
||||
-- DropIndex
|
||||
-- Removes indexes that are covered by other indexes on the same table, e.g. (project_id) if (project_id, timestamp) exists.
|
||||
DROP INDEX CONCURRENTLY IF EXISTS "observations_project_id_idx"; -- covered by observations_project_id_start_time_type_idx
|
||||
+3
@@ -0,0 +1,3 @@
|
||||
-- DropIndex
|
||||
-- Removes indexes that are covered by other indexes on the same table, e.g. (project_id) if (project_id, timestamp) exists.
|
||||
DROP INDEX CONCURRENTLY IF EXISTS "observations_trace_id_idx"; -- covered by observations_trace_id_project_id_start_time_idx
|
||||
+3
@@ -0,0 +1,3 @@
|
||||
-- DropIndex
|
||||
-- Removes indexes that are covered by other indexes on the same table, e.g. (project_id) if (project_id, timestamp) exists.
|
||||
DROP INDEX CONCURRENTLY IF EXISTS "observations_trace_id_project_id_idx"; -- covered by observations_trace_id_project_id_start_time_idx
|
||||
+3
@@ -0,0 +1,3 @@
|
||||
-- DropIndex
|
||||
-- Removes indexes that are covered by other indexes on the same table, e.g. (project_id) if (project_id, timestamp) exists.
|
||||
DROP INDEX CONCURRENTLY IF EXISTS "organization_memberships_org_id_idx"; -- covered by organization_memberships_org_id_user_id_key (unique)
|
||||
+3
@@ -0,0 +1,3 @@
|
||||
-- DropIndex
|
||||
-- Removes indexes that are covered by other indexes on the same table, e.g. (project_id) if (project_id, timestamp) exists.
|
||||
DROP INDEX CONCURRENTLY IF EXISTS "posthog_integrations_project_id_idx"; -- covered by posthog_integrations_pkey (unique)
|
||||
+3
@@ -0,0 +1,3 @@
|
||||
-- DropIndex
|
||||
-- Removes indexes that are covered by other indexes on the same table, e.g. (project_id) if (project_id, timestamp) exists.
|
||||
DROP INDEX CONCURRENTLY IF EXISTS "prompts_project_id_idx"; -- covered by prompts_project_id_name_version_key (unique)
|
||||
+3
@@ -0,0 +1,3 @@
|
||||
-- DropIndex
|
||||
-- Removes indexes that are covered by other indexes on the same table, e.g. (project_id) if (project_id, timestamp) exists.
|
||||
DROP INDEX CONCURRENTLY IF EXISTS "prompts_project_id_name_version_idx"; -- covered by prompts_project_id_name_version_key (unique)
|
||||
+3
@@ -0,0 +1,3 @@
|
||||
-- DropIndex
|
||||
-- Removes indexes that are covered by other indexes on the same table, e.g. (project_id) if (project_id, timestamp) exists.
|
||||
DROP INDEX CONCURRENTLY IF EXISTS "scores_project_id_idx"; -- covered by scores_project_id_name_idx
|
||||
+2
@@ -0,0 +1,2 @@
|
||||
-- AlterTable
|
||||
ALTER TABLE "Account" ADD COLUMN "created_at" INTEGER;
|
||||
+2
@@ -0,0 +1,2 @@
|
||||
-- AlterTable
|
||||
ALTER TABLE "scores" ADD COLUMN "queue_id" TEXT;
|
||||
@@ -0,0 +1,75 @@
|
||||
-- CreateEnum
|
||||
CREATE TYPE "AnnotationQueueStatus" AS ENUM ('PENDING', 'COMPLETED');
|
||||
|
||||
-- CreateEnum
|
||||
CREATE TYPE "AnnotationQueueObjectType" AS ENUM ('TRACE', 'OBSERVATION');
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "annotation_queues" (
|
||||
"id" TEXT NOT NULL,
|
||||
"name" TEXT NOT NULL,
|
||||
"description" TEXT,
|
||||
"score_config_ids" TEXT[] DEFAULT ARRAY[]::TEXT[],
|
||||
"project_id" TEXT NOT NULL,
|
||||
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updated_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
|
||||
CONSTRAINT "annotation_queues_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "annotation_queue_items" (
|
||||
"id" TEXT NOT NULL,
|
||||
"queue_id" TEXT NOT NULL,
|
||||
"object_id" TEXT NOT NULL,
|
||||
"object_type" "AnnotationQueueObjectType" NOT NULL,
|
||||
"status" "AnnotationQueueStatus" NOT NULL DEFAULT 'PENDING',
|
||||
"locked_at" TIMESTAMP(3),
|
||||
"locked_by_user_id" TEXT,
|
||||
"annotator_user_id" TEXT,
|
||||
"completed_at" TIMESTAMP(3),
|
||||
"project_id" TEXT NOT NULL,
|
||||
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updated_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
|
||||
CONSTRAINT "annotation_queue_items_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "annotation_queues_id_project_id_idx" ON "annotation_queues"("id", "project_id");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "annotation_queues_project_id_created_at_idx" ON "annotation_queues"("project_id", "created_at");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "annotation_queues_project_id_name_key" ON "annotation_queues"("project_id", "name");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "annotation_queue_items_id_project_id_idx" ON "annotation_queue_items"("id", "project_id");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "annotation_queue_items_project_id_queue_id_status_idx" ON "annotation_queue_items"("project_id", "queue_id", "status");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "annotation_queue_items_object_id_object_type_project_id_que_idx" ON "annotation_queue_items"("object_id", "object_type", "project_id", "queue_id");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "annotation_queue_items_annotator_user_id_idx" ON "annotation_queue_items"("annotator_user_id");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "annotation_queue_items_created_at_idx" ON "annotation_queue_items"("created_at");
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "annotation_queues" ADD CONSTRAINT "annotation_queues_project_id_fkey" FOREIGN KEY ("project_id") REFERENCES "projects"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "annotation_queue_items" ADD CONSTRAINT "annotation_queue_items_queue_id_fkey" FOREIGN KEY ("queue_id") REFERENCES "annotation_queues"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "annotation_queue_items" ADD CONSTRAINT "annotation_queue_items_locked_by_user_id_fkey" FOREIGN KEY ("locked_by_user_id") REFERENCES "users"("id") ON DELETE SET NULL ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "annotation_queue_items" ADD CONSTRAINT "annotation_queue_items_annotator_user_id_fkey" FOREIGN KEY ("annotator_user_id") REFERENCES "users"("id") ON DELETE SET NULL ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "annotation_queue_items" ADD CONSTRAINT "annotation_queue_items_project_id_fkey" FOREIGN KEY ("project_id") REFERENCES "projects"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
@@ -34,21 +34,23 @@ generator kysely {
|
||||
|
||||
// Necessary for Next auth
|
||||
model Account {
|
||||
id String @id @default(cuid())
|
||||
userId String @map("user_id")
|
||||
type String
|
||||
provider String
|
||||
providerAccountId String
|
||||
refresh_token String? // @db.Text
|
||||
access_token String? // @db.Text
|
||||
expires_at Int?
|
||||
expires_in Int?
|
||||
ext_expires_in Int?
|
||||
token_type String?
|
||||
scope String?
|
||||
id_token String? // @db.Text
|
||||
session_state String?
|
||||
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
||||
id String @id @default(cuid())
|
||||
userId String @map("user_id")
|
||||
type String
|
||||
provider String
|
||||
providerAccountId String
|
||||
refresh_token String? // @db.Text
|
||||
access_token String? // @db.Text
|
||||
expires_at Int?
|
||||
expires_in Int?
|
||||
ext_expires_in Int?
|
||||
token_type String?
|
||||
scope String?
|
||||
id_token String? // @db.Text
|
||||
session_state String?
|
||||
refresh_token_expires_in Int?
|
||||
created_at Int? // GitLab
|
||||
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
||||
|
||||
@@unique([provider, providerAccountId])
|
||||
@@index([userId])
|
||||
@@ -78,6 +80,8 @@ model User {
|
||||
createdAt DateTime @default(now()) @map("created_at")
|
||||
updatedAt DateTime @default(now()) @updatedAt @map("updated_at")
|
||||
featureFlags String[] @default([]) @map("feature_flags")
|
||||
annotatedLockedItem AnnotationQueueItem[] @relation("LockedByUser")
|
||||
annotatedCompletedItem AnnotationQueueItem[] @relation("AnnotatorUser")
|
||||
|
||||
@@map("users")
|
||||
}
|
||||
@@ -105,31 +109,33 @@ model Organization {
|
||||
}
|
||||
|
||||
model Project {
|
||||
id String @id @default(cuid())
|
||||
orgId String @map("org_id")
|
||||
createdAt DateTime @default(now()) @map("created_at")
|
||||
updatedAt DateTime @default(now()) @updatedAt @map("updated_at")
|
||||
name String
|
||||
projectMembers ProjectMembership[]
|
||||
organization Organization @relation(fields: [orgId], references: [id], onUpdate: Cascade, onDelete: Cascade)
|
||||
traces Trace[]
|
||||
observations Observation[]
|
||||
apiKeys ApiKey[]
|
||||
dataset Dataset[]
|
||||
RawEvents Events[]
|
||||
invitations MembershipInvitation[]
|
||||
sessions TraceSession[]
|
||||
Prompt Prompt[]
|
||||
Model Model[]
|
||||
EvalTemplate EvalTemplate[]
|
||||
JobConfiguration JobConfiguration[]
|
||||
JobExecution JobExecution[]
|
||||
LlmApiKeys LlmApiKeys[]
|
||||
PosthogIntegration PosthogIntegration[]
|
||||
Score Score[]
|
||||
scoreConfig ScoreConfig[]
|
||||
BatchExport BatchExport[]
|
||||
comment Comment[]
|
||||
id String @id @default(cuid())
|
||||
orgId String @map("org_id")
|
||||
createdAt DateTime @default(now()) @map("created_at")
|
||||
updatedAt DateTime @default(now()) @updatedAt @map("updated_at")
|
||||
name String
|
||||
projectMembers ProjectMembership[]
|
||||
organization Organization @relation(fields: [orgId], references: [id], onUpdate: Cascade, onDelete: Cascade)
|
||||
traces Trace[]
|
||||
observations Observation[]
|
||||
apiKeys ApiKey[]
|
||||
dataset Dataset[]
|
||||
RawEvents Events[]
|
||||
invitations MembershipInvitation[]
|
||||
sessions TraceSession[]
|
||||
Prompt Prompt[]
|
||||
Model Model[]
|
||||
EvalTemplate EvalTemplate[]
|
||||
JobConfiguration JobConfiguration[]
|
||||
JobExecution JobExecution[]
|
||||
LlmApiKeys LlmApiKeys[]
|
||||
PosthogIntegration PosthogIntegration[]
|
||||
Score Score[]
|
||||
scoreConfig ScoreConfig[]
|
||||
BatchExport BatchExport[]
|
||||
comment Comment[]
|
||||
annotationQueue AnnotationQueue[]
|
||||
annotationQueueItem AnnotationQueueItem[]
|
||||
|
||||
@@index([orgId])
|
||||
@@map("projects")
|
||||
@@ -172,7 +178,6 @@ model LlmApiKeys {
|
||||
project Project @relation(fields: [projectId], references: [id], onDelete: Cascade)
|
||||
|
||||
@@unique([projectId, provider])
|
||||
@@index([projectId, provider])
|
||||
@@map("llm_api_keys")
|
||||
}
|
||||
|
||||
@@ -189,7 +194,6 @@ model OrganizationMembership {
|
||||
|
||||
@@unique([orgId, userId])
|
||||
@@index([userId])
|
||||
@@index([orgId])
|
||||
@@map("organization_memberships")
|
||||
}
|
||||
|
||||
@@ -282,7 +286,6 @@ model Trace {
|
||||
|
||||
JobExecution JobExecution[]
|
||||
|
||||
@@index([projectId])
|
||||
@@index([projectId, timestamp])
|
||||
@@index([sessionId])
|
||||
@@index([name])
|
||||
@@ -373,12 +376,9 @@ model Observation {
|
||||
@@index([projectId, internalModel, startTime, unit])
|
||||
@@index([traceId, projectId, type, startTime])
|
||||
@@index([traceId, projectId, startTime])
|
||||
@@index([traceId, projectId])
|
||||
@@index([traceId])
|
||||
@@index([type])
|
||||
@@index(startTime)
|
||||
@@index(createdAt)
|
||||
@@index(projectId)
|
||||
@@index(model)
|
||||
@@index(internalModel)
|
||||
@@index([projectId, promptId])
|
||||
@@ -464,6 +464,7 @@ model Score {
|
||||
observationId String? @map("observation_id")
|
||||
configId String? @map("config_id")
|
||||
stringValue String? @map("string_value") // always defined if data type is CATEGORICAL or BOOLEAN, null for NUMERIC
|
||||
queueId String? @map("queue_id")
|
||||
createdAt DateTime @default(now()) @map("created_at")
|
||||
updatedAt DateTime @default(now()) @updatedAt @map("updated_at")
|
||||
dataType ScoreDataType @default(NUMERIC) @map("data_type")
|
||||
@@ -473,7 +474,6 @@ model Score {
|
||||
@@unique([id, projectId]) // used for upserts via prisma
|
||||
@@index(timestamp)
|
||||
@@index([value])
|
||||
@@index([projectId])
|
||||
@@index([projectId, name])
|
||||
@@index([authorUserId])
|
||||
@@index([configId])
|
||||
@@ -521,6 +521,59 @@ enum ScoreDataType {
|
||||
BOOLEAN
|
||||
}
|
||||
|
||||
model AnnotationQueue {
|
||||
id String @id @default(cuid())
|
||||
name String
|
||||
description String?
|
||||
scoreConfigIds String[] @default([]) @map("score_config_ids")
|
||||
projectId String @map("project_id")
|
||||
project Project @relation(fields: [projectId], references: [id], onDelete: Cascade)
|
||||
createdAt DateTime @default(now()) @map("created_at")
|
||||
updatedAt DateTime @default(now()) @updatedAt @map("updated_at")
|
||||
annotationQueueItem AnnotationQueueItem[]
|
||||
|
||||
@@unique([projectId, name])
|
||||
@@index([id, projectId])
|
||||
@@index([projectId, createdAt])
|
||||
@@map("annotation_queues")
|
||||
}
|
||||
|
||||
model AnnotationQueueItem {
|
||||
id String @id @default(cuid())
|
||||
queueId String @map("queue_id")
|
||||
queue AnnotationQueue @relation(fields: [queueId], references: [id], onDelete: Cascade)
|
||||
objectId String @map("object_id")
|
||||
objectType AnnotationQueueObjectType @map("object_type")
|
||||
status AnnotationQueueStatus @default(PENDING)
|
||||
lockedAt DateTime? @map("locked_at")
|
||||
lockedByUserId String? @map("locked_by_user_id")
|
||||
lockedByUser User? @relation("LockedByUser", fields: [lockedByUserId], references: [id], onDelete: SetNull)
|
||||
annotatorUserId String? @map("annotator_user_id")
|
||||
annotatorUser User? @relation("AnnotatorUser", fields: [annotatorUserId], references: [id], onDelete: SetNull)
|
||||
completedAt DateTime? @map("completed_at")
|
||||
projectId String @map("project_id")
|
||||
project Project @relation(fields: [projectId], references: [id], onDelete: Cascade)
|
||||
createdAt DateTime @default(now()) @map("created_at")
|
||||
updatedAt DateTime @default(now()) @updatedAt @map("updated_at")
|
||||
|
||||
@@index([id, projectId])
|
||||
@@index([projectId, queueId, status])
|
||||
@@index([objectId, objectType, projectId, queueId])
|
||||
@@index([annotatorUserId])
|
||||
@@index([createdAt])
|
||||
@@map("annotation_queue_items")
|
||||
}
|
||||
|
||||
enum AnnotationQueueStatus {
|
||||
PENDING
|
||||
COMPLETED
|
||||
}
|
||||
|
||||
enum AnnotationQueueObjectType {
|
||||
TRACE
|
||||
OBSERVATION
|
||||
}
|
||||
|
||||
model CronJobs {
|
||||
name String @id
|
||||
lastRun DateTime? @map("last_run")
|
||||
@@ -544,7 +597,6 @@ model Dataset {
|
||||
|
||||
@@id([id, projectId])
|
||||
@@unique([projectId, name])
|
||||
@@index([projectId], type: Hash)
|
||||
@@index([createdAt])
|
||||
@@index([updatedAt])
|
||||
@@map("datasets")
|
||||
@@ -678,9 +730,7 @@ model Prompt {
|
||||
labels String[] @default([])
|
||||
|
||||
@@unique([projectId, name, version])
|
||||
@@index([projectId, name, version])
|
||||
@@index([projectId, id])
|
||||
@@index([projectId])
|
||||
@@index([createdAt])
|
||||
@@index([updatedAt])
|
||||
@@index([tags(ops: ArrayOps)], type: Gin)
|
||||
@@ -707,8 +757,6 @@ model Model {
|
||||
tokenizerConfig Json? @map("tokenizer_config")
|
||||
|
||||
@@unique([projectId, modelName, startDate, unit])
|
||||
@@index([projectId, modelName, startDate, unit])
|
||||
@@index([projectId, modelName])
|
||||
@@index(modelName)
|
||||
@@map("models")
|
||||
}
|
||||
@@ -757,7 +805,6 @@ model EvalTemplate {
|
||||
|
||||
@@unique([projectId, name, version])
|
||||
@@index([projectId, id])
|
||||
@@index([projectId])
|
||||
@@map("eval_templates")
|
||||
}
|
||||
|
||||
@@ -792,7 +839,6 @@ model JobConfiguration {
|
||||
JobExecution JobExecution[]
|
||||
|
||||
@@index([projectId, id])
|
||||
@@index([projectId])
|
||||
@@map("job_configurations")
|
||||
}
|
||||
|
||||
@@ -827,7 +873,6 @@ model JobExecution {
|
||||
|
||||
@@index([projectId, status])
|
||||
@@index([projectId, id])
|
||||
@@index([projectId])
|
||||
@@index([jobConfigurationId])
|
||||
@@index([jobOutputScoreId])
|
||||
@@index([jobInputTraceId])
|
||||
@@ -860,7 +905,6 @@ model PosthogIntegration {
|
||||
enabled Boolean
|
||||
createdAt DateTime @default(now()) @map("created_at")
|
||||
|
||||
@@index([projectId])
|
||||
@@map("posthog_integrations")
|
||||
}
|
||||
|
||||
|
||||
+154
-24
@@ -5,6 +5,7 @@ import {
|
||||
ObservationType,
|
||||
ScoreSource,
|
||||
ScoreDataType,
|
||||
AnnotationQueueObjectType,
|
||||
} from "../src/index";
|
||||
import { hash } from "bcryptjs";
|
||||
import { parseArgs } from "node:util";
|
||||
@@ -12,7 +13,7 @@ import { parseArgs } from "node:util";
|
||||
import { chunk } from "lodash";
|
||||
import { v4 } from "uuid";
|
||||
import { ModelUsageUnit } from "../src";
|
||||
import { getDisplaySecretKey, hashSecretKey } from "../src/server";
|
||||
import { getDisplaySecretKey, hashSecretKey, logger } from "../src/server";
|
||||
import { encrypt } from "../src/encryption";
|
||||
import { redis } from "../src/server/redis/redis";
|
||||
|
||||
@@ -77,6 +78,9 @@ async function main() {
|
||||
create: {
|
||||
id: seedOrgId,
|
||||
name: "Seed Org",
|
||||
cloudConfig: {
|
||||
plan: "Team",
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
@@ -250,6 +254,11 @@ async function main() {
|
||||
project2,
|
||||
]);
|
||||
|
||||
const queueIds = await generateQueuesForProject(
|
||||
[project1, project2],
|
||||
configIdsAndNames
|
||||
);
|
||||
|
||||
const promptIds = await generatePromptsForProject([project1, project2]);
|
||||
|
||||
const envTags = [null, "development", "staging", "production"];
|
||||
@@ -257,18 +266,26 @@ async function main() {
|
||||
|
||||
const traceVolume = environment === "load" ? LOAD_TRACE_VOLUME : 100;
|
||||
|
||||
const { traces, observations, scores, sessions, events, comments } =
|
||||
createObjects(
|
||||
traceVolume,
|
||||
envTags,
|
||||
colorTags,
|
||||
project1,
|
||||
project2,
|
||||
promptIds,
|
||||
configIdsAndNames
|
||||
);
|
||||
const {
|
||||
traces,
|
||||
observations,
|
||||
scores,
|
||||
sessions,
|
||||
events,
|
||||
comments,
|
||||
queueItems,
|
||||
} = createObjects(
|
||||
traceVolume,
|
||||
envTags,
|
||||
colorTags,
|
||||
project1,
|
||||
project2,
|
||||
promptIds,
|
||||
queueIds,
|
||||
configIdsAndNames
|
||||
);
|
||||
|
||||
console.log(
|
||||
logger.info(
|
||||
`Seeding ${traces.length} traces, ${observations.length} observations, and ${scores.length} scores`
|
||||
);
|
||||
|
||||
@@ -278,7 +295,8 @@ async function main() {
|
||||
scores,
|
||||
sessions,
|
||||
events,
|
||||
comments
|
||||
comments,
|
||||
queueItems
|
||||
);
|
||||
|
||||
// If openai key is in environment, add it to the projects LLM API keys
|
||||
@@ -295,7 +313,7 @@ async function main() {
|
||||
},
|
||||
});
|
||||
} else {
|
||||
console.warn(
|
||||
logger.warn(
|
||||
"No OPENAI_API_KEY found in environment. Skipping seeding LLM API key."
|
||||
);
|
||||
}
|
||||
@@ -457,13 +475,13 @@ main()
|
||||
.then(async () => {
|
||||
await prisma.$disconnect();
|
||||
redis?.disconnect();
|
||||
console.log("Disconnected from postgres and redis");
|
||||
logger.info("Disconnected from postgres and redis");
|
||||
})
|
||||
.catch(async (e) => {
|
||||
console.error(e);
|
||||
logger.error(e);
|
||||
await prisma.$disconnect();
|
||||
redis?.disconnect();
|
||||
console.log("Disconnected from postgres and redis");
|
||||
logger.info("Disconnected from postgres and redis");
|
||||
process.exit(1);
|
||||
});
|
||||
|
||||
@@ -473,7 +491,8 @@ async function uploadObjects(
|
||||
scores: Prisma.ScoreCreateManyInput[],
|
||||
sessions: Prisma.TraceSessionCreateManyInput[],
|
||||
events: Prisma.ObservationCreateManyInput[],
|
||||
comments: Prisma.CommentCreateManyInput[]
|
||||
comments: Prisma.CommentCreateManyInput[],
|
||||
queueItems: Prisma.AnnotationQueueItemCreateManyInput[]
|
||||
) {
|
||||
let promises: Prisma.PrismaPromise<unknown>[] = [];
|
||||
|
||||
@@ -493,7 +512,7 @@ async function uploadObjects(
|
||||
|
||||
for (let i = 0; i < promises.length; i++) {
|
||||
if (i + 1 >= promises.length || i % Math.ceil(promises.length / 10) === 0)
|
||||
console.log(
|
||||
logger.info(
|
||||
`Seeding of Sessions ${((i + 1) / promises.length) * 100}% complete`
|
||||
);
|
||||
await promises[i];
|
||||
@@ -510,7 +529,7 @@ async function uploadObjects(
|
||||
});
|
||||
for (let i = 0; i < promises.length; i++) {
|
||||
if (i + 1 >= promises.length || i % Math.ceil(promises.length / 10) === 0)
|
||||
console.log(
|
||||
logger.info(
|
||||
`Seeding of Traces ${((i + 1) / promises.length) * 100}% complete`
|
||||
);
|
||||
await promises[i];
|
||||
@@ -527,7 +546,7 @@ async function uploadObjects(
|
||||
|
||||
for (let i = 0; i < promises.length; i++) {
|
||||
if (i + 1 >= promises.length || i % Math.ceil(promises.length / 10) === 0)
|
||||
console.log(
|
||||
logger.info(
|
||||
`Seeding of Observations ${((i + 1) / promises.length) * 100}% complete`
|
||||
);
|
||||
await promises[i];
|
||||
@@ -544,7 +563,7 @@ async function uploadObjects(
|
||||
|
||||
for (let i = 0; i < promises.length; i++) {
|
||||
if (i + 1 >= promises.length || i % Math.ceil(promises.length / 10) === 0)
|
||||
console.log(
|
||||
logger.info(
|
||||
`Seeding of Events ${((i + 1) / promises.length) * 100}% complete`
|
||||
);
|
||||
await promises[i];
|
||||
@@ -560,7 +579,7 @@ async function uploadObjects(
|
||||
});
|
||||
for (let i = 0; i < promises.length; i++) {
|
||||
if (i + 1 >= promises.length || i % Math.ceil(promises.length / 10) === 0)
|
||||
console.log(
|
||||
logger.info(
|
||||
`Seeding of Scores ${((i + 1) / promises.length) * 100}% complete`
|
||||
);
|
||||
await promises[i];
|
||||
@@ -576,11 +595,27 @@ async function uploadObjects(
|
||||
});
|
||||
for (let i = 0; i < promises.length; i++) {
|
||||
if (i + 1 >= promises.length || i % Math.ceil(promises.length / 10) === 0)
|
||||
console.log(
|
||||
logger.info(
|
||||
`Seeding of Comments ${((i + 1) / promises.length) * 100}% complete`
|
||||
);
|
||||
await promises[i];
|
||||
}
|
||||
|
||||
promises = [];
|
||||
chunk(queueItems, chunkSize).forEach((chunk) => {
|
||||
promises.push(
|
||||
prisma.annotationQueueItem.createMany({
|
||||
data: chunk,
|
||||
})
|
||||
);
|
||||
});
|
||||
for (let i = 0; i < promises.length; i++) {
|
||||
if (i + 1 >= promises.length || i % Math.ceil(promises.length / 10) === 0)
|
||||
logger.info(
|
||||
`Seeding of Annotation Queue Items ${((i + 1) / promises.length) * 100}% complete`
|
||||
);
|
||||
await promises[i];
|
||||
}
|
||||
}
|
||||
|
||||
function createObjects(
|
||||
@@ -590,6 +625,7 @@ function createObjects(
|
||||
project1: Project,
|
||||
project2: Project,
|
||||
promptIds: Map<string, string[]>,
|
||||
queueIds: Map<string, string[]>,
|
||||
configParams: Map<
|
||||
string,
|
||||
{
|
||||
@@ -607,6 +643,7 @@ function createObjects(
|
||||
const events: Prisma.ObservationCreateManyInput[] = [];
|
||||
const configs: Prisma.ScoreConfigCreateManyInput[] = [];
|
||||
const comments: Prisma.CommentCreateManyInput[] = [];
|
||||
const queueItems: Prisma.AnnotationQueueItemCreateManyInput[] = [];
|
||||
|
||||
for (let i = 0; i < traceVolume; i++) {
|
||||
// print progress to console with a progress bar that refreshes every 10 iterations
|
||||
@@ -689,6 +726,23 @@ function createObjects(
|
||||
}),
|
||||
};
|
||||
|
||||
const queueItem = [
|
||||
...(Math.random() > 0.9
|
||||
? [
|
||||
{
|
||||
queueId: queueIds.get(projectId)?.[
|
||||
Math.floor(Math.random() * 2)
|
||||
] as string,
|
||||
objectId: trace.id,
|
||||
objectType: AnnotationQueueObjectType.TRACE,
|
||||
projectId,
|
||||
},
|
||||
]
|
||||
: []),
|
||||
];
|
||||
|
||||
queueItems.push(...queueItem);
|
||||
|
||||
const traceScores = [
|
||||
...(Math.random() > 0.5
|
||||
? [
|
||||
@@ -941,6 +995,7 @@ function createObjects(
|
||||
observations,
|
||||
scores,
|
||||
configs,
|
||||
queueItems,
|
||||
sessions: uniqueSessions,
|
||||
events,
|
||||
comments,
|
||||
@@ -1218,6 +1273,7 @@ async function generateConfigs(project: Project) {
|
||||
|
||||
return configNameAndId;
|
||||
}
|
||||
|
||||
function getGenerationInputOutput(): {
|
||||
input: Prisma.InputJsonValue;
|
||||
output: Prisma.InputJsonValue;
|
||||
@@ -1278,3 +1334,77 @@ function getGenerationInputOutput(): {
|
||||
|
||||
return { input, output };
|
||||
}
|
||||
|
||||
async function generateQueuesForProject(
|
||||
projects: Project[],
|
||||
configIdsAndNames: Map<
|
||||
string,
|
||||
{
|
||||
name: string;
|
||||
id: string;
|
||||
dataType: ScoreDataType;
|
||||
categories: ConfigCategory[] | null;
|
||||
}[]
|
||||
>
|
||||
) {
|
||||
const projectIdsToQueues: Map<string, string[]> = new Map();
|
||||
|
||||
await Promise.all(
|
||||
projects.map(async (project) => {
|
||||
const queueIds = await generateQueues(
|
||||
project,
|
||||
configIdsAndNames.get(project.id) ?? []
|
||||
);
|
||||
projectIdsToQueues.set(project.id, queueIds);
|
||||
})
|
||||
);
|
||||
return projectIdsToQueues;
|
||||
}
|
||||
|
||||
async function generateQueues(
|
||||
project: Project,
|
||||
configIdsAndNames: {
|
||||
name: string;
|
||||
id: string;
|
||||
dataType: ScoreDataType;
|
||||
categories: ConfigCategory[] | null;
|
||||
}[]
|
||||
) {
|
||||
const queueIds: string[] = [];
|
||||
|
||||
const queues = [
|
||||
{
|
||||
id: `queue-${v4()}`,
|
||||
name: "Default",
|
||||
description: "Default queue",
|
||||
scoreConfigIds: configIdsAndNames.map((config) => config.id),
|
||||
projectId: project.id,
|
||||
},
|
||||
{
|
||||
id: `queue-${v4()}`,
|
||||
name: "Correction",
|
||||
scoreConfigIds: [configIdsAndNames[0].id],
|
||||
projectId: project.id,
|
||||
},
|
||||
];
|
||||
|
||||
for (const queue of queues) {
|
||||
await prisma.annotationQueue.upsert({
|
||||
where: {
|
||||
projectId_name: {
|
||||
projectId: queue.projectId,
|
||||
name: queue.name,
|
||||
},
|
||||
},
|
||||
create: {
|
||||
...queue,
|
||||
},
|
||||
update: {
|
||||
id: queue.id,
|
||||
},
|
||||
});
|
||||
queueIds.push(queue.id);
|
||||
}
|
||||
|
||||
return queueIds;
|
||||
}
|
||||
|
||||
+79
-22
@@ -1,7 +1,7 @@
|
||||
// This file exports the prisma db connection, the Prisma Object, and the Typescript types.
|
||||
// This is not imported in the index.ts file of this package, as we must not import this into FE code.
|
||||
|
||||
import { PrismaClient } from "@prisma/client";
|
||||
import { Prisma, PrismaClient } from "@prisma/client";
|
||||
import { env } from "process";
|
||||
import kyselyExtension from "prisma-extension-kysely";
|
||||
import {
|
||||
@@ -11,21 +11,83 @@ import {
|
||||
PostgresQueryCompiler,
|
||||
} from "kysely";
|
||||
import { DB } from ".";
|
||||
import { logger } from "./server";
|
||||
|
||||
// Instantiated according to the Prisma documentation
|
||||
// https://www.prisma.io/docs/orm/more/help-and-troubleshooting/help-articles/nextjs-prisma-client-dev-practices
|
||||
export class PrismaClientSingleton {
|
||||
private static instance: PrismaClient;
|
||||
|
||||
const prismaClientSingleton = () => {
|
||||
return new PrismaClient({
|
||||
log:
|
||||
env.NODE_ENV === "development"
|
||||
? ["query", "error", "warn"]
|
||||
: ["error", "warn"],
|
||||
});
|
||||
};
|
||||
public static getInstance(): PrismaClient {
|
||||
if (PrismaClientSingleton.instance) {
|
||||
return PrismaClientSingleton.instance;
|
||||
}
|
||||
|
||||
const kyselySingleton = (prismaClient: PrismaClient) => {
|
||||
return prismaClient.$extends(
|
||||
const client = new PrismaClient<
|
||||
Prisma.PrismaClientOptions,
|
||||
"warn" | "error" | "query"
|
||||
>({
|
||||
log: [
|
||||
{ emit: "event", level: "query" },
|
||||
{ emit: "event", level: "error" },
|
||||
{ emit: "event", level: "warn" },
|
||||
],
|
||||
});
|
||||
|
||||
if (env.NODE_ENV === "development") {
|
||||
client.$on("query", (event) => {
|
||||
logger.info(`prisma:query ${event.query}, ${event.duration}ms`);
|
||||
});
|
||||
}
|
||||
|
||||
client.$on("warn", (event) => {
|
||||
logger.warn(`prisma:warn ${event.message}`);
|
||||
});
|
||||
|
||||
client.$on("error", (event) => {
|
||||
logger.error(`prisma:error ${event.message}`);
|
||||
});
|
||||
|
||||
PrismaClientSingleton.instance = client;
|
||||
|
||||
return PrismaClientSingleton.instance;
|
||||
}
|
||||
}
|
||||
|
||||
export class KyselySingleton {
|
||||
private static instance: { $kysely: Kysely<DB> };
|
||||
|
||||
public static getInstance() {
|
||||
if (KyselySingleton.instance) {
|
||||
return KyselySingleton.instance;
|
||||
}
|
||||
|
||||
KyselySingleton.instance = PrismaClientSingleton.getInstance().$extends(
|
||||
kyselyExtension({
|
||||
kysely: (driver) =>
|
||||
new Kysely<DB>({
|
||||
dialect: {
|
||||
// This is where the magic happens!
|
||||
createDriver: () => driver,
|
||||
// Don't forget to customize these to match your database!
|
||||
createAdapter: () => new PostgresAdapter(),
|
||||
createIntrospector: (db) => new PostgresIntrospector(db),
|
||||
createQueryCompiler: () => new PostgresQueryCompiler(),
|
||||
},
|
||||
}),
|
||||
})
|
||||
);
|
||||
|
||||
return KyselySingleton.instance;
|
||||
}
|
||||
}
|
||||
|
||||
declare const globalThis: {
|
||||
prismaGlobal: PrismaClient | undefined;
|
||||
kyselyPrismaGlobal: { $kysely: Kysely<DB> } | undefined;
|
||||
} & typeof global;
|
||||
|
||||
if (process.env.NODE_ENV === "development") {
|
||||
globalThis.prismaGlobal ??= new PrismaClient(); // regular instantiation
|
||||
globalThis.kyselyPrismaGlobal ??= globalThis.prismaGlobal.$extends(
|
||||
kyselyExtension({
|
||||
kysely: (driver) =>
|
||||
new Kysely<DB>({
|
||||
@@ -41,15 +103,10 @@ const kyselySingleton = (prismaClient: PrismaClient) => {
|
||||
})
|
||||
);
|
||||
};
|
||||
declare global {
|
||||
// eslint-disable-next-line no-var
|
||||
var prisma: undefined | ReturnType<typeof prismaClientSingleton>;
|
||||
var kyselyPrisma: undefined | ReturnType<typeof kyselySingleton>;
|
||||
}
|
||||
|
||||
export const prisma = globalThis.prisma ?? prismaClientSingleton();
|
||||
export const kyselyPrisma = globalThis.kyselyPrisma ?? kyselySingleton(prisma);
|
||||
export const prisma =
|
||||
globalThis.prismaGlobal ?? PrismaClientSingleton.getInstance();
|
||||
export const kyselyPrisma =
|
||||
globalThis.kyselyPrismaGlobal ?? KyselySingleton.getInstance();
|
||||
|
||||
export * from "@prisma/client";
|
||||
|
||||
if (process.env.NODE_ENV !== "production") globalThis.prisma = prisma;
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { z } from "zod";
|
||||
import { removeEmptyEnvVariables } from "./utils/environment";
|
||||
|
||||
const EnvSchema = z.object({
|
||||
NODE_ENV: z
|
||||
@@ -38,6 +39,11 @@ const EnvSchema = z.object({
|
||||
.number()
|
||||
.positive()
|
||||
.default(60 * 10),
|
||||
SALT: z.string().optional(), // used by components imported by web package
|
||||
LANGFUSE_LOG_LEVEL: z
|
||||
.enum(["trace", "debug", "info", "warn", "error", "fatal"])
|
||||
.optional(),
|
||||
LANGFUSE_LOG_FORMAT: z.enum(["text", "json"]).default("text"),
|
||||
});
|
||||
|
||||
export const env = EnvSchema.parse(process.env);
|
||||
export const env = EnvSchema.parse(removeEmptyEnvVariables(process.env));
|
||||
|
||||
@@ -32,6 +32,7 @@ const CreateAnnotationScoreBase = z.object({
|
||||
configId: z.string().optional(),
|
||||
observationId: z.string().optional(),
|
||||
comment: z.string().nullish(),
|
||||
queueId: z.string().nullish(),
|
||||
});
|
||||
|
||||
const UpdateAnnotationScoreBase = CreateAnnotationScoreBase.extend({
|
||||
@@ -57,3 +58,15 @@ export const UpdateAnnotationScoreData = z.discriminatedUnion("dataType", [
|
||||
UpdateAnnotationScoreBase.merge(CategoricalData),
|
||||
UpdateAnnotationScoreBase.merge(BooleanData),
|
||||
]);
|
||||
|
||||
// annotation queues
|
||||
|
||||
export const CreateQueueData = z.object({
|
||||
name: z.string().min(1).max(35),
|
||||
description: z.string().max(1000).optional(),
|
||||
scoreConfigIds: z.array(z.string()).min(1, {
|
||||
message: "At least 1 score config must be selected",
|
||||
}),
|
||||
});
|
||||
|
||||
export type CreateQueue = z.infer<typeof CreateQueueData>;
|
||||
|
||||
@@ -19,6 +19,8 @@ export enum BatchExportFileFormat {
|
||||
|
||||
export enum BatchExportTableName {
|
||||
Sessions = "sessions",
|
||||
Traces = "traces",
|
||||
Generations = "generations",
|
||||
}
|
||||
|
||||
export const exportOptions: Record<
|
||||
|
||||
@@ -83,13 +83,13 @@ export const ScoreBodyWithoutConfig = z.discriminatedUnion("dataType", [
|
||||
z.object({
|
||||
value: z.number(),
|
||||
dataType: z.literal("NUMERIC"),
|
||||
})
|
||||
}),
|
||||
),
|
||||
BaseScoreBody.merge(
|
||||
z.object({
|
||||
value: z.string(),
|
||||
dataType: z.literal("CATEGORICAL"),
|
||||
})
|
||||
}),
|
||||
),
|
||||
BaseScoreBody.merge(
|
||||
z.object({
|
||||
@@ -97,7 +97,7 @@ export const ScoreBodyWithoutConfig = z.discriminatedUnion("dataType", [
|
||||
message: "Value must be either 0 or 1",
|
||||
}),
|
||||
dataType: z.literal("BOOLEAN"),
|
||||
})
|
||||
}),
|
||||
),
|
||||
]);
|
||||
|
||||
@@ -161,7 +161,7 @@ export const ScorePropsAgainstConfig = z.union([
|
||||
*/
|
||||
export const filterAndValidateDbScoreList = (
|
||||
scores: Score[],
|
||||
onParseError?: (error: z.ZodError) => void
|
||||
onParseError?: (error: z.ZodError) => void,
|
||||
): APIScore[] =>
|
||||
scores.reduce((acc, ts) => {
|
||||
const result = APIScoreSchema.safeParse(ts);
|
||||
@@ -198,14 +198,14 @@ export const PostScoresBody = z.discriminatedUnion("dataType", [
|
||||
value: z.number(),
|
||||
dataType: z.literal("NUMERIC"),
|
||||
configId: z.string().nullish(),
|
||||
})
|
||||
}),
|
||||
),
|
||||
BaseScoreBody.merge(
|
||||
z.object({
|
||||
value: z.string(),
|
||||
dataType: z.literal("CATEGORICAL"),
|
||||
configId: z.string().nullish(),
|
||||
})
|
||||
}),
|
||||
),
|
||||
BaseScoreBody.merge(
|
||||
z.object({
|
||||
@@ -215,14 +215,14 @@ export const PostScoresBody = z.discriminatedUnion("dataType", [
|
||||
}),
|
||||
dataType: z.literal("BOOLEAN"),
|
||||
configId: z.string().nullish(),
|
||||
})
|
||||
}),
|
||||
),
|
||||
BaseScoreBody.merge(
|
||||
z.object({
|
||||
value: z.union([z.string(), z.number()]),
|
||||
dataType: z.undefined(),
|
||||
configId: z.string().nullish(),
|
||||
})
|
||||
}),
|
||||
),
|
||||
]);
|
||||
|
||||
@@ -256,7 +256,7 @@ const LegacyGetScoreResponseDataV1 = z.intersection(
|
||||
trace: z.object({
|
||||
userId: z.string().nullish(),
|
||||
}),
|
||||
})
|
||||
}),
|
||||
);
|
||||
export const GetScoresResponse = z.object({
|
||||
data: z.array(LegacyGetScoreResponseDataV1),
|
||||
@@ -265,7 +265,7 @@ export const GetScoresResponse = z.object({
|
||||
|
||||
export const legacyFilterAndValidateV1GetScoreList = (
|
||||
scores: unknown[],
|
||||
onParseError?: (error: z.ZodError) => void
|
||||
onParseError?: (error: z.ZodError) => void,
|
||||
): z.infer<typeof LegacyGetScoreResponseDataV1>[] =>
|
||||
scores.reduce(
|
||||
(acc: z.infer<typeof LegacyGetScoreResponseDataV1>[], ts) => {
|
||||
@@ -278,7 +278,7 @@ export const legacyFilterAndValidateV1GetScoreList = (
|
||||
}
|
||||
return acc;
|
||||
},
|
||||
[] as z.infer<typeof LegacyGetScoreResponseDataV1>[]
|
||||
[] as z.infer<typeof LegacyGetScoreResponseDataV1>[],
|
||||
);
|
||||
|
||||
// GET /scores/{scoreId}
|
||||
|
||||
@@ -1,15 +1,12 @@
|
||||
export * from "./constants";
|
||||
export * from "./queries";
|
||||
export * from "./interfaces/filters";
|
||||
export * from "./interfaces/orderBy";
|
||||
export * from "./interfaces/cloudConfigSchema";
|
||||
export * from "./interfaces/parseDbOrg";
|
||||
export * from "./tableDefinitions";
|
||||
export * from "./types";
|
||||
export * from "./filterToPrisma";
|
||||
export * from "./orderByToPrisma";
|
||||
export * from "./tracesTable";
|
||||
export * from "./server/auth/auth";
|
||||
export * from "./server/auth/apiKeys";
|
||||
export * from "./observationsTable";
|
||||
export * from "./utils/zod";
|
||||
export * from "./utils/json";
|
||||
@@ -43,3 +40,5 @@ export { type DB } from "../prisma/generated/types";
|
||||
|
||||
// errors
|
||||
export * from "./errors/index";
|
||||
|
||||
export * from "./utils/environment";
|
||||
|
||||
@@ -6,6 +6,7 @@ export const RateLimitResource = z.enum([
|
||||
"public-api",
|
||||
"public-api-metrics",
|
||||
"prompts",
|
||||
"legacy-ingestion",
|
||||
]);
|
||||
|
||||
// result of the rate limit check.
|
||||
|
||||
@@ -52,7 +52,8 @@ export const observationsTableCols: ColumnDefinition[] = [
|
||||
name: "Time To First Token (s)",
|
||||
id: "timeToFirstToken",
|
||||
type: "number",
|
||||
internal: 'o."completion_start_time" - o."start_time"',
|
||||
internal:
|
||||
'EXTRACT(EPOCH FROM (o."completion_start_time" - o."start_time"))',
|
||||
nullable: true,
|
||||
},
|
||||
{
|
||||
@@ -62,10 +63,10 @@ export const observationsTableCols: ColumnDefinition[] = [
|
||||
internal: '"latency"',
|
||||
},
|
||||
{
|
||||
name: "Time Per Output Token (s)",
|
||||
id: "timePerOutputToken",
|
||||
name: "Tokens per second",
|
||||
id: "tokensPerSecond",
|
||||
type: "number",
|
||||
internal: '"latency" / o."completion_tokens"',
|
||||
internal: 'o."completion_tokens" / "latency"',
|
||||
nullable: true,
|
||||
},
|
||||
{
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
export { createSessionsAllQuery } from "./createSessionsAllQuery";
|
||||
@@ -0,0 +1,81 @@
|
||||
import { PrismaClient } from "@prisma/client";
|
||||
import { compare, hash } from "bcryptjs";
|
||||
import { randomUUID } from "crypto";
|
||||
import * as crypto from "crypto";
|
||||
import { env } from "../../env";
|
||||
|
||||
export function getDisplaySecretKey(secretKey: string) {
|
||||
return secretKey.slice(0, 6) + "..." + secretKey.slice(-4);
|
||||
}
|
||||
|
||||
export async function hashSecretKey(key: string) {
|
||||
// legacy, uses bcrypt, transformed into hashed key upon first use
|
||||
const hashedKey = await hash(key, 11);
|
||||
return hashedKey;
|
||||
}
|
||||
|
||||
async function generateKeySet() {
|
||||
return {
|
||||
pk: `pk-lf-${randomUUID()}`,
|
||||
sk: `sk-lf-${randomUUID()}`,
|
||||
};
|
||||
}
|
||||
|
||||
export async function verifySecretKey(key: string, hashedKey: string) {
|
||||
const isValid = await compare(key, hashedKey);
|
||||
return isValid;
|
||||
}
|
||||
|
||||
export function createShaHash(privateKey: string, salt: string): string {
|
||||
const hash = crypto
|
||||
.createHash("sha256")
|
||||
.update(privateKey)
|
||||
.update(crypto.createHash("sha256").update(salt, "utf8").digest("hex"))
|
||||
.digest("hex");
|
||||
|
||||
return hash;
|
||||
}
|
||||
|
||||
export async function createAndAddApiKeysToDb(p: {
|
||||
prisma: PrismaClient;
|
||||
projectId: string;
|
||||
note?: string;
|
||||
predefinedKeys?: {
|
||||
secretKey: string;
|
||||
publicKey: string;
|
||||
};
|
||||
}) {
|
||||
const salt = env.SALT;
|
||||
if (!salt) {
|
||||
throw new Error("SALT is not set");
|
||||
}
|
||||
|
||||
const { pk, sk } = p.predefinedKeys
|
||||
? { pk: p.predefinedKeys.publicKey, sk: p.predefinedKeys.secretKey }
|
||||
: await generateKeySet();
|
||||
|
||||
const hashedSk = await hashSecretKey(sk);
|
||||
const displaySk = getDisplaySecretKey(sk);
|
||||
|
||||
const hashFromProvidedKey = createShaHash(sk, salt);
|
||||
|
||||
const apiKey = await p.prisma.apiKey.create({
|
||||
data: {
|
||||
projectId: p.projectId,
|
||||
publicKey: pk,
|
||||
hashedSecretKey: hashedSk,
|
||||
displaySecretKey: displaySk,
|
||||
fastHashedSecretKey: hashFromProvidedKey,
|
||||
note: p.note,
|
||||
},
|
||||
});
|
||||
|
||||
return {
|
||||
id: apiKey.id,
|
||||
createdAt: apiKey.createdAt,
|
||||
note: apiKey.note,
|
||||
publicKey: apiKey.publicKey,
|
||||
secretKey: sk,
|
||||
displaySecretKey: displaySk,
|
||||
};
|
||||
}
|
||||
@@ -1,81 +0,0 @@
|
||||
import { compare, hash } from "bcryptjs";
|
||||
import { randomUUID } from "crypto";
|
||||
import * as crypto from "crypto";
|
||||
import type { OAuthConfig, OAuthUserConfig } from "next-auth/providers/oauth";
|
||||
|
||||
export function generateSecretKey() {
|
||||
return `sk-lf-${randomUUID()}`;
|
||||
}
|
||||
|
||||
export function generatePublicKey() {
|
||||
return `pk-lf-${randomUUID()}`;
|
||||
}
|
||||
|
||||
export function getDisplaySecretKey(secretKey: string) {
|
||||
return secretKey.slice(0, 6) + "..." + secretKey.slice(-4);
|
||||
}
|
||||
|
||||
export async function hashSecretKey(key: string) {
|
||||
// legacy, uses bcrypt, transformed into hashed key upon first use
|
||||
const hashedKey = await hash(key, 11);
|
||||
return hashedKey;
|
||||
}
|
||||
|
||||
export async function generateKeySet() {
|
||||
const pk = generatePublicKey();
|
||||
const sk = generateSecretKey();
|
||||
const hashedSk = await hashSecretKey(sk);
|
||||
const displaySk = getDisplaySecretKey(sk);
|
||||
|
||||
return {
|
||||
pk,
|
||||
sk,
|
||||
hashedSk,
|
||||
displaySk,
|
||||
};
|
||||
}
|
||||
|
||||
export async function verifySecretKey(key: string, hashedKey: string) {
|
||||
const isValid = await compare(key, hashedKey);
|
||||
return isValid;
|
||||
}
|
||||
|
||||
export function createShaHash(privateKey: string, salt: string): string {
|
||||
const hash = crypto
|
||||
.createHash("sha256")
|
||||
.update(privateKey)
|
||||
.update(crypto.createHash("sha256").update(salt, "utf8").digest("hex"))
|
||||
.digest("hex");
|
||||
|
||||
return hash;
|
||||
}
|
||||
|
||||
export interface CustomSSOUser extends Record<string, any> {
|
||||
email: string;
|
||||
id: string;
|
||||
name: string;
|
||||
verified: boolean;
|
||||
}
|
||||
|
||||
export function CustomSSOProvider<P extends CustomSSOUser>(
|
||||
options: OAuthUserConfig<P>
|
||||
): OAuthConfig<P> {
|
||||
return {
|
||||
id: "custom",
|
||||
name: "CustomSSOProvider",
|
||||
type: "oauth",
|
||||
wellKnown: `${options.issuer}/.well-known/openid-configuration`,
|
||||
authorization: { params: { scope: "openid email profile" } }, // overridden by options.authorization to be able to set custom scopes, deep merged with this default
|
||||
checks: ["pkce", "state"],
|
||||
idToken: true,
|
||||
profile(profile) {
|
||||
return {
|
||||
id: profile.sub,
|
||||
name: profile.name,
|
||||
email: profile.email,
|
||||
image: null,
|
||||
};
|
||||
},
|
||||
options,
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
import type { OAuthConfig, OAuthUserConfig } from "next-auth/providers/oauth";
|
||||
|
||||
interface CustomSSOUser extends Record<string, any> {
|
||||
email: string;
|
||||
id: string;
|
||||
name: string;
|
||||
verified: boolean;
|
||||
}
|
||||
|
||||
export function CustomSSOProvider<P extends CustomSSOUser>(
|
||||
options: OAuthUserConfig<P>
|
||||
): OAuthConfig<P> {
|
||||
return {
|
||||
id: "custom",
|
||||
name: "CustomSSOProvider",
|
||||
type: "oauth",
|
||||
wellKnown: `${options.issuer}/.well-known/openid-configuration`,
|
||||
authorization: { params: { scope: "openid email profile" } }, // overridden by options.authorization to be able to set custom scopes, deep merged with this default
|
||||
checks: ["pkce", "state"],
|
||||
idToken: true,
|
||||
profile(profile) {
|
||||
return {
|
||||
id: profile.sub,
|
||||
name: profile.name,
|
||||
email: profile.email,
|
||||
image: null,
|
||||
};
|
||||
},
|
||||
options,
|
||||
};
|
||||
}
|
||||
+5
-4
@@ -1,8 +1,9 @@
|
||||
import { Prisma } from "@prisma/client";
|
||||
import { ColumnDefinition, type TableNames } from "./tableDefinitions";
|
||||
import { FilterState } from "./types";
|
||||
import { filterOperators, timeFilter } from "./interfaces/filters";
|
||||
import { ColumnDefinition, type TableNames } from "../tableDefinitions";
|
||||
import { FilterState } from "../types";
|
||||
import { filterOperators, timeFilter } from "../interfaces/filters";
|
||||
import { z } from "zod";
|
||||
import { logger } from "./index";
|
||||
|
||||
const operatorReplacements = {
|
||||
"any of": "IN",
|
||||
@@ -51,7 +52,7 @@ export function tableColumnsToSqlFilter(
|
||||
c.name === filter.column || c.id === filter.column
|
||||
);
|
||||
if (!col) {
|
||||
console.error("Invalid filter column", filter.column);
|
||||
logger.error("Invalid filter column", filter.column);
|
||||
throw new Error("Invalid filter column: " + filter.column);
|
||||
}
|
||||
const colPrisma = Prisma.raw(col.internal);
|
||||
@@ -3,7 +3,8 @@ export * from "./services/email/organizationInvitation/sendMembershipInvitationE
|
||||
export * from "./services/email/batchExportSuccess/sendBatchExportSuccessEmail";
|
||||
export * from "./services/email/passwordReset/sendResetPasswordVerificationRequest";
|
||||
export * from "./services/PromptService";
|
||||
export * from "./auth/auth";
|
||||
export * from "./auth/apiKeys";
|
||||
export * from "./auth/customSsoProvider";
|
||||
export * from "./llm/fetchLLMCompletion";
|
||||
export * from "./llm/types";
|
||||
export * from "./utils/DatabaseReadStream";
|
||||
@@ -24,4 +25,8 @@ export * from "./auth/types";
|
||||
export * from "./ingestion/legacy/index";
|
||||
export * from "./queues";
|
||||
export * from "./ingestion/legacy/EventProcessor";
|
||||
export * from "./orderByToPrisma";
|
||||
export * from "./filterToPrisma";
|
||||
export * from "./instrumentation";
|
||||
export * from "./logger";
|
||||
export * from "./queries";
|
||||
|
||||
@@ -10,6 +10,7 @@ import {
|
||||
traceEvent,
|
||||
scoreEvent,
|
||||
sdkLogEvent,
|
||||
ingestionEvent,
|
||||
} from "../types";
|
||||
import { validateAndInflateScore } from "../validateAndInflateScore";
|
||||
import { Trace, Observation, Score, Prisma, Model } from "@prisma/client";
|
||||
@@ -18,15 +19,42 @@ import { mergeJson } from "../../../utils/json";
|
||||
import { jsonSchema } from "../../../utils/zod";
|
||||
import { prisma } from "../../../db";
|
||||
import { LegacyIngestionAccessScope } from ".";
|
||||
import { logger } from "../../logger";
|
||||
|
||||
export interface EventProcessor {
|
||||
auth(apiScope: LegacyIngestionAccessScope): void;
|
||||
|
||||
process(
|
||||
apiScope: LegacyIngestionAccessScope
|
||||
apiScope: LegacyIngestionAccessScope,
|
||||
): Promise<Trace | Observation | Score> | undefined;
|
||||
}
|
||||
|
||||
export const getProcessorForEvent = (
|
||||
event: z.infer<typeof ingestionEvent>,
|
||||
calculateTokenDelegate: (p: {
|
||||
model: Model;
|
||||
text: unknown;
|
||||
}) => number | undefined,
|
||||
): EventProcessor => {
|
||||
switch (event.type) {
|
||||
case eventTypes.TRACE_CREATE:
|
||||
return new TraceProcessor(event);
|
||||
case eventTypes.OBSERVATION_CREATE:
|
||||
case eventTypes.OBSERVATION_UPDATE:
|
||||
case eventTypes.EVENT_CREATE:
|
||||
case eventTypes.SPAN_CREATE:
|
||||
case eventTypes.SPAN_UPDATE:
|
||||
case eventTypes.GENERATION_CREATE:
|
||||
case eventTypes.GENERATION_UPDATE:
|
||||
return new ObservationProcessor(event, calculateTokenDelegate);
|
||||
case eventTypes.SCORE_CREATE: {
|
||||
return new ScoreProcessor(event);
|
||||
}
|
||||
case eventTypes.SDK_LOG:
|
||||
return new SdkLogProcessor(event);
|
||||
}
|
||||
};
|
||||
|
||||
export class ObservationProcessor implements EventProcessor {
|
||||
event: ObservationEvent;
|
||||
calculateTokenDelegate: (p: {
|
||||
@@ -39,7 +67,7 @@ export class ObservationProcessor implements EventProcessor {
|
||||
calculateTokenDelegate: (p: {
|
||||
model: Model;
|
||||
text: unknown;
|
||||
}) => number | undefined
|
||||
}) => number | undefined,
|
||||
) {
|
||||
this.event = event;
|
||||
this.calculateTokenDelegate = calculateTokenDelegate;
|
||||
@@ -47,7 +75,7 @@ export class ObservationProcessor implements EventProcessor {
|
||||
|
||||
async convertToObservation(
|
||||
apiScope: LegacyIngestionAccessScope,
|
||||
existingObservation: Observation | null
|
||||
existingObservation: Omit<Observation, "input" | "output"> | null,
|
||||
): Promise<{
|
||||
id: string;
|
||||
create: Prisma.ObservationUncheckedCreateInput;
|
||||
@@ -77,7 +105,7 @@ export class ObservationProcessor implements EventProcessor {
|
||||
!existingObservation
|
||||
) {
|
||||
throw new LangfuseNotFoundError(
|
||||
`Observation with id ${this.event.id} not found`
|
||||
`Observation with id ${this.event.id} not found`,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -119,11 +147,12 @@ export class ObservationProcessor implements EventProcessor {
|
||||
// Token counts
|
||||
const [newInputCount, newOutputCount] =
|
||||
"usage" in this.event.body
|
||||
? this.calculateTokenCounts(
|
||||
? await this.calculateTokenCounts(
|
||||
apiScope.projectId,
|
||||
this.event.body,
|
||||
this.calculateTokenDelegate,
|
||||
internalModel ?? undefined,
|
||||
existingObservation ?? undefined
|
||||
existingObservation ?? undefined,
|
||||
)
|
||||
: [undefined, undefined];
|
||||
|
||||
@@ -159,7 +188,7 @@ export class ObservationProcessor implements EventProcessor {
|
||||
const calculatedCosts = ObservationProcessor.calculateTokenCosts(
|
||||
internalModel,
|
||||
userProvidedTokenCosts,
|
||||
tokenCounts
|
||||
tokenCounts,
|
||||
);
|
||||
|
||||
// merge metadata from existingObservation.metadata and metadata
|
||||
@@ -167,7 +196,7 @@ export class ObservationProcessor implements EventProcessor {
|
||||
existingObservation?.metadata
|
||||
? jsonSchema.parse(existingObservation.metadata)
|
||||
: undefined,
|
||||
this.event.body.metadata ?? undefined
|
||||
this.event.body.metadata ?? undefined,
|
||||
);
|
||||
|
||||
const prompt =
|
||||
@@ -187,8 +216,9 @@ export class ObservationProcessor implements EventProcessor {
|
||||
: undefined;
|
||||
|
||||
// Only null if promptName and promptVersion are set but prompt is not found
|
||||
if (prompt === null)
|
||||
console.warn("Prompt not found for observation", this.event.body);
|
||||
if (prompt === null) {
|
||||
logger.warn("Prompt not found for observation", this.event.body);
|
||||
}
|
||||
|
||||
const observationId = this.event.body.id ?? v4();
|
||||
|
||||
@@ -309,7 +339,8 @@ export class ObservationProcessor implements EventProcessor {
|
||||
};
|
||||
}
|
||||
|
||||
calculateTokenCounts(
|
||||
async calculateTokenCounts(
|
||||
projectId: string,
|
||||
body:
|
||||
| z.infer<typeof legacyObservationCreateEvent>["body"]
|
||||
| z.infer<typeof generationCreateEvent>["body"],
|
||||
@@ -318,29 +349,59 @@ export class ObservationProcessor implements EventProcessor {
|
||||
text: unknown;
|
||||
}) => number | undefined,
|
||||
model?: Model,
|
||||
existingObservation?: Observation
|
||||
existingObservation?: Omit<Observation, "input" | "output">,
|
||||
) {
|
||||
const newPromptTokens =
|
||||
body.usage?.input ??
|
||||
((body.input || existingObservation?.input) && model && model.tokenizerId
|
||||
? calculateTokenDelegate({
|
||||
model: model,
|
||||
text: body.input ?? existingObservation?.input,
|
||||
})
|
||||
: undefined);
|
||||
let newPromptTokens = body.usage?.input;
|
||||
if (newPromptTokens === undefined && model && model.tokenizerId) {
|
||||
if (body.input) {
|
||||
newPromptTokens = calculateTokenDelegate({
|
||||
model: model,
|
||||
text: body.input,
|
||||
});
|
||||
} else {
|
||||
logger.info(
|
||||
`No input provided, trying to calculate for id: ${existingObservation?.id}`,
|
||||
);
|
||||
const observationInput = await prisma.observation.findFirst({
|
||||
where: { id: existingObservation?.id, projectId: projectId },
|
||||
select: {
|
||||
input: true,
|
||||
},
|
||||
});
|
||||
|
||||
const newCompletionTokens =
|
||||
body.usage?.output ??
|
||||
((body.output || existingObservation?.output) &&
|
||||
model &&
|
||||
model.tokenizerId
|
||||
? calculateTokenDelegate({
|
||||
model: model,
|
||||
text: body.output ?? existingObservation?.output,
|
||||
})
|
||||
: undefined);
|
||||
newPromptTokens = calculateTokenDelegate({
|
||||
model: model,
|
||||
text: observationInput?.input,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return [newPromptTokens, newCompletionTokens];
|
||||
let newCompletionTokens = body.usage?.output;
|
||||
|
||||
if (newCompletionTokens === undefined && model && model.tokenizerId) {
|
||||
if (body.output) {
|
||||
newCompletionTokens = calculateTokenDelegate({
|
||||
model: model,
|
||||
text: body.output,
|
||||
});
|
||||
} else {
|
||||
logger.info(
|
||||
`No output provided, trying to calculate for id: ${existingObservation?.id}`,
|
||||
);
|
||||
const observationOutput = await prisma.observation.findFirst({
|
||||
where: { id: existingObservation?.id, projectId: projectId },
|
||||
select: {
|
||||
output: true,
|
||||
},
|
||||
});
|
||||
newCompletionTokens = calculateTokenDelegate({
|
||||
model: model,
|
||||
text: observationOutput?.output,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return [newPromptTokens ?? undefined, newCompletionTokens ?? undefined];
|
||||
}
|
||||
|
||||
static calculateTokenCosts(
|
||||
@@ -350,7 +411,7 @@ export class ObservationProcessor implements EventProcessor {
|
||||
outputCost?: Decimal | null;
|
||||
totalCost?: Decimal | null;
|
||||
},
|
||||
tokenCounts: { input?: number; output?: number; total?: number }
|
||||
tokenCounts: { input?: number; output?: number; total?: number },
|
||||
): {
|
||||
inputCost?: Decimal | null;
|
||||
outputCost?: Decimal | null;
|
||||
@@ -367,7 +428,7 @@ export class ObservationProcessor implements EventProcessor {
|
||||
totalCost:
|
||||
userProvidedCosts.totalCost ??
|
||||
(userProvidedCosts.inputCost ?? new Decimal(0)).add(
|
||||
userProvidedCosts.outputCost ?? new Decimal(0)
|
||||
userProvidedCosts.outputCost ?? new Decimal(0),
|
||||
),
|
||||
};
|
||||
}
|
||||
@@ -408,7 +469,43 @@ export class ObservationProcessor implements EventProcessor {
|
||||
|
||||
const existingObservation = this.event.body.id
|
||||
? await prisma.observation.findFirst({
|
||||
where: { id: this.event.body.id },
|
||||
select: {
|
||||
// do not select I/O to spare our db
|
||||
input: false,
|
||||
output: false,
|
||||
|
||||
id: true,
|
||||
traceId: true,
|
||||
projectId: true,
|
||||
type: true,
|
||||
startTime: true,
|
||||
endTime: true,
|
||||
name: true,
|
||||
metadata: true,
|
||||
parentObservationId: true,
|
||||
level: true,
|
||||
statusMessage: true,
|
||||
version: true,
|
||||
createdAt: true,
|
||||
updatedAt: true,
|
||||
model: true,
|
||||
internalModelId: true,
|
||||
modelParameters: true,
|
||||
promptTokens: true,
|
||||
completionTokens: true,
|
||||
totalTokens: true,
|
||||
unit: true,
|
||||
inputCost: true,
|
||||
outputCost: true,
|
||||
totalCost: true,
|
||||
calculatedInputCost: true,
|
||||
calculatedOutputCost: true,
|
||||
calculatedTotalCost: true,
|
||||
completionStartTime: true,
|
||||
promptId: true,
|
||||
internalModel: true,
|
||||
},
|
||||
where: { id: this.event.body.id, projectId: apiScope.projectId },
|
||||
})
|
||||
: null;
|
||||
|
||||
@@ -417,7 +514,7 @@ export class ObservationProcessor implements EventProcessor {
|
||||
existingObservation.projectId !== apiScope.projectId
|
||||
) {
|
||||
throw new ForbiddenError(
|
||||
`Access denied for observation creation ${existingObservation.projectId} `
|
||||
`Access denied for observation creation ${existingObservation.projectId} `,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -434,6 +531,7 @@ export class ObservationProcessor implements EventProcessor {
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export class TraceProcessor implements EventProcessor {
|
||||
event: z.infer<typeof traceEvent>;
|
||||
|
||||
@@ -447,7 +545,7 @@ export class TraceProcessor implements EventProcessor {
|
||||
}
|
||||
|
||||
async process(
|
||||
apiScope: LegacyIngestionAccessScope
|
||||
apiScope: LegacyIngestionAccessScope,
|
||||
): Promise<Trace | Observation | Score> {
|
||||
const { body } = this.event;
|
||||
|
||||
@@ -455,11 +553,8 @@ export class TraceProcessor implements EventProcessor {
|
||||
|
||||
const internalId = body.id ?? v4();
|
||||
|
||||
console.log(
|
||||
"Trying to create trace, project ",
|
||||
apiScope.projectId,
|
||||
", id:",
|
||||
internalId
|
||||
logger.debug(
|
||||
`Trying to create trace, project ${apiScope.projectId}, id: ${internalId}`,
|
||||
);
|
||||
|
||||
const existingTrace = await prisma.trace.findFirst({
|
||||
@@ -470,7 +565,7 @@ export class TraceProcessor implements EventProcessor {
|
||||
|
||||
if (existingTrace && existingTrace.projectId !== apiScope.projectId) {
|
||||
throw new ForbiddenError(
|
||||
`Access denied for trace creation ${existingTrace.projectId}`
|
||||
`Access denied for trace creation ${existingTrace.projectId}`,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -478,7 +573,7 @@ export class TraceProcessor implements EventProcessor {
|
||||
existingTrace?.metadata
|
||||
? jsonSchema.parse(existingTrace.metadata)
|
||||
: undefined,
|
||||
body.metadata ?? undefined
|
||||
body.metadata ?? undefined,
|
||||
);
|
||||
|
||||
const mergedTags =
|
||||
@@ -546,6 +641,7 @@ export class TraceProcessor implements EventProcessor {
|
||||
return upsertedTrace;
|
||||
}
|
||||
}
|
||||
|
||||
export class ScoreProcessor implements EventProcessor {
|
||||
event: z.infer<typeof scoreEvent>;
|
||||
|
||||
@@ -556,12 +652,12 @@ export class ScoreProcessor implements EventProcessor {
|
||||
auth(apiScope: LegacyIngestionAccessScope) {
|
||||
if (apiScope.accessLevel !== "scores" && apiScope.accessLevel !== "all")
|
||||
throw new ForbiddenError(
|
||||
`Access denied for score creation, ${apiScope.accessLevel}`
|
||||
`Access denied for score creation, ${apiScope.accessLevel}`,
|
||||
);
|
||||
}
|
||||
|
||||
async process(
|
||||
apiScope: LegacyIngestionAccessScope
|
||||
apiScope: LegacyIngestionAccessScope,
|
||||
): Promise<Trace | Observation | Score> {
|
||||
const { body } = this.event;
|
||||
|
||||
@@ -579,7 +675,7 @@ export class ScoreProcessor implements EventProcessor {
|
||||
});
|
||||
if (existingScore && existingScore.projectId !== apiScope.projectId) {
|
||||
throw new ForbiddenError(
|
||||
`Access denied for score creation ${existingScore.projectId}`
|
||||
`Access denied for score creation ${existingScore.projectId}`,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -619,7 +715,7 @@ export class SdkLogProcessor implements EventProcessor {
|
||||
|
||||
process() {
|
||||
try {
|
||||
console.log("SDK Log", this.event);
|
||||
logger.info("SDK Log", this.event);
|
||||
return undefined;
|
||||
} catch (error) {
|
||||
return undefined;
|
||||
|
||||
@@ -8,10 +8,11 @@ import { IngestionUtils } from "../IngestionUtils";
|
||||
import { IngestionEventType } from "../types";
|
||||
import { redis } from "../../redis/redis";
|
||||
import { env } from "../../../env";
|
||||
import { logger } from "../../logger";
|
||||
|
||||
export async function enqueueIngestionEvents(
|
||||
projectId: string,
|
||||
events: IngestionEventType[]
|
||||
events: IngestionEventType[],
|
||||
) {
|
||||
const ingestionFlushQueue = getIngestionFlushQueue();
|
||||
|
||||
@@ -31,8 +32,8 @@ export async function enqueueIngestionEvents(
|
||||
event,
|
||||
redis,
|
||||
ingestionFlushQueue,
|
||||
batchTimestamp
|
||||
)
|
||||
batchTimestamp,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -44,11 +45,11 @@ async function enqueueSingleIngestionEvent(
|
||||
event: IngestionEventType,
|
||||
redis: Redis,
|
||||
ingestionFlushQueue: IngestionFlushQueue,
|
||||
batchTimestamp: string
|
||||
batchTimestamp: string,
|
||||
): Promise<void> {
|
||||
if (!("id" in event.body && event.body.id)) {
|
||||
console.warn(
|
||||
`Received ingestion event without id: ${JSON.stringify(event)}`
|
||||
logger.warn(
|
||||
`Received ingestion event without id: ${JSON.stringify(event)}`,
|
||||
);
|
||||
|
||||
return;
|
||||
|
||||
@@ -2,14 +2,8 @@ import { env } from "node:process";
|
||||
import z from "zod";
|
||||
import { ForbiddenError, UnauthorizedError } from "../../../errors";
|
||||
import { eventTypes, ingestionApiSchema, ingestionEvent } from "../types";
|
||||
import {
|
||||
EventProcessor,
|
||||
TraceProcessor,
|
||||
ObservationProcessor,
|
||||
ScoreProcessor,
|
||||
SdkLogProcessor,
|
||||
} from "./EventProcessor";
|
||||
import { EventBodyType, EventName, TraceUpsertEventType } from "../../queues";
|
||||
import { getProcessorForEvent } from "./EventProcessor";
|
||||
import { TraceUpsertEventType } from "../../queues";
|
||||
import {
|
||||
convertTraceUpsertEventsToRedisEvents,
|
||||
getTraceUpsertQueue,
|
||||
@@ -19,6 +13,7 @@ import { redis } from "../../redis/redis";
|
||||
import { backOff } from "exponential-backoff";
|
||||
import { Model } from "../../..";
|
||||
import { enqueueIngestionEvents } from "./enqueueIngestionEvents";
|
||||
import { logger } from "../../logger";
|
||||
|
||||
export type BatchResult = {
|
||||
result: unknown;
|
||||
@@ -49,9 +44,9 @@ type LegacyIngestionAuthHeaderVerificationResult =
|
||||
export const handleBatch = async (
|
||||
events: z.infer<typeof ingestionApiSchema>["batch"],
|
||||
authCheck: LegacyIngestionAuthHeaderVerificationResult,
|
||||
calculateTokenDelegate: (p: TokenCountInput) => number | undefined
|
||||
calculateTokenDelegate: (p: TokenCountInput) => number | undefined,
|
||||
) => {
|
||||
console.log(`handling ingestion ${events.length} events`);
|
||||
logger.debug(`handling ingestion ${events.length} events`);
|
||||
|
||||
if (!authCheck.validKey) throw new UnauthorizedError(authCheck.error);
|
||||
|
||||
@@ -69,7 +64,7 @@ export const handleBatch = async (
|
||||
return await handleSingleEvent(
|
||||
singleEvent,
|
||||
authCheck.scope,
|
||||
calculateTokenDelegate
|
||||
calculateTokenDelegate,
|
||||
);
|
||||
});
|
||||
results.push({
|
||||
@@ -79,7 +74,7 @@ export const handleBatch = async (
|
||||
}); // Push each result into the array
|
||||
} catch (error) {
|
||||
// Handle or log the error if `handleSingleEvent` fails
|
||||
console.error("Error handling event:", error);
|
||||
logger.error("Error handling event:", error);
|
||||
// Decide how to handle the error: rethrow, continue, or push an error object to results
|
||||
// For example, push an error object:
|
||||
errors.push({
|
||||
@@ -93,9 +88,9 @@ export const handleBatch = async (
|
||||
if (env.CLICKHOUSE_URL) {
|
||||
try {
|
||||
await enqueueIngestionEvents(authCheck.scope.projectId, events);
|
||||
console.log(`Added ${events.length} ingestion events to queue`);
|
||||
logger.info(`Added ${events.length} ingestion events to queue`);
|
||||
} catch (err) {
|
||||
console.error("Error adding ingestion events to queue", err);
|
||||
logger.error("Error adding ingestion events to queue", err);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -107,10 +102,10 @@ async function retry<T>(request: () => Promise<T>): Promise<T> {
|
||||
numOfAttempts: env.LANGFUSE_ASYNC_INGESTION_PROCESSING === "true" ? 5 : 3,
|
||||
retry: (e: Error, attemptNumber: number) => {
|
||||
if (e instanceof UnauthorizedError || e instanceof ForbiddenError) {
|
||||
console.log("not retrying auth error");
|
||||
logger.info("not retrying auth error");
|
||||
return false;
|
||||
}
|
||||
console.log(`retrying processing events ${attemptNumber}`);
|
||||
logger.info(`retrying processing events ${attemptNumber}`);
|
||||
return true;
|
||||
},
|
||||
});
|
||||
@@ -122,7 +117,7 @@ const handleSingleEvent = async (
|
||||
calculateTokenDelegate: (p: {
|
||||
model: Model;
|
||||
text: unknown;
|
||||
}) => number | undefined
|
||||
}) => number | undefined,
|
||||
) => {
|
||||
const { body } = event;
|
||||
let restEvent = body;
|
||||
@@ -137,46 +132,24 @@ const handleSingleEvent = async (
|
||||
restEvent = rest;
|
||||
}
|
||||
|
||||
console.log(
|
||||
`handling single event ${event.id} of type ${event.type}: ${JSON.stringify({ body: restEvent })}`
|
||||
logger.info(
|
||||
`handling single event ${event.id} of type ${event.type}: ${JSON.stringify({ body: restEvent })}`,
|
||||
);
|
||||
|
||||
const cleanedEvent = ingestionEvent.parse(cleanEvent(event));
|
||||
|
||||
const { type } = cleanedEvent;
|
||||
|
||||
let processor: EventProcessor;
|
||||
switch (type) {
|
||||
case eventTypes.TRACE_CREATE:
|
||||
processor = new TraceProcessor(cleanedEvent);
|
||||
break;
|
||||
case eventTypes.OBSERVATION_CREATE:
|
||||
case eventTypes.OBSERVATION_UPDATE:
|
||||
case eventTypes.EVENT_CREATE:
|
||||
case eventTypes.SPAN_CREATE:
|
||||
case eventTypes.SPAN_UPDATE:
|
||||
case eventTypes.GENERATION_CREATE:
|
||||
case eventTypes.GENERATION_UPDATE:
|
||||
processor = new ObservationProcessor(
|
||||
cleanedEvent,
|
||||
calculateTokenDelegate
|
||||
);
|
||||
break;
|
||||
case eventTypes.SCORE_CREATE: {
|
||||
processor = new ScoreProcessor(cleanedEvent);
|
||||
break;
|
||||
}
|
||||
case eventTypes.SDK_LOG:
|
||||
processor = new SdkLogProcessor(cleanedEvent);
|
||||
}
|
||||
|
||||
// Deny access to non-score events if the access level is not "all"
|
||||
// This is an additional safeguard to auth checks in EventProcessor
|
||||
if (apiScope.accessLevel !== "all" && type !== eventTypes.SCORE_CREATE) {
|
||||
if (
|
||||
apiScope.accessLevel !== "all" &&
|
||||
cleanedEvent.type !== eventTypes.SCORE_CREATE
|
||||
) {
|
||||
throw new ForbiddenError("Access denied. Event type not allowed.");
|
||||
}
|
||||
|
||||
return await processor.process(apiScope);
|
||||
return getProcessorForEvent(cleanedEvent, calculateTokenDelegate).process(
|
||||
apiScope,
|
||||
);
|
||||
};
|
||||
|
||||
// cleans NULL characters from the event
|
||||
@@ -201,15 +174,15 @@ export function cleanEvent(obj: unknown): unknown {
|
||||
}
|
||||
|
||||
export const isNotNullOrUndefined = <T>(
|
||||
val?: T | null
|
||||
val?: T | null,
|
||||
): val is Exclude<T, null | undefined> => !isUndefinedOrNull(val);
|
||||
|
||||
export const isUndefinedOrNull = <T>(val?: T | null): val is undefined | null =>
|
||||
val === undefined || val === null;
|
||||
|
||||
export const sendToWorkerIfEnvironmentConfigured = async (
|
||||
export const addTracesToTraceUpsertQueue = async (
|
||||
batchResults: BatchResult[],
|
||||
projectId: string
|
||||
projectId: string,
|
||||
): Promise<void> => {
|
||||
const traceEvents: TraceUpsertEventType[] = batchResults
|
||||
.filter((result) => result.type === eventTypes.TRACE_CREATE) // we only have create, no update.
|
||||
@@ -219,49 +192,23 @@ export const sendToWorkerIfEnvironmentConfigured = async (
|
||||
"id" in result.result
|
||||
? // ingestion API only gets traces for one projectId
|
||||
{ traceId: result.result.id as string, projectId }
|
||||
: null
|
||||
: null,
|
||||
)
|
||||
.filter(isNotNullOrUndefined);
|
||||
|
||||
try {
|
||||
if (env.NEXT_PUBLIC_LANGFUSE_CLOUD_REGION && redis) {
|
||||
console.log(`Sending ${traceEvents.length} events to worker via Redis`);
|
||||
logger.info(`Sending ${traceEvents.length} events to worker via Redis`);
|
||||
|
||||
const queue = getTraceUpsertQueue();
|
||||
if (!queue) {
|
||||
console.error("TraceUpsertQueue not initialized");
|
||||
logger.error("TraceUpsertQueue not initialized");
|
||||
return;
|
||||
}
|
||||
|
||||
await queue.addBulk(convertTraceUpsertEventsToRedisEvents(traceEvents));
|
||||
} else if (
|
||||
env.LANGFUSE_WORKER_HOST &&
|
||||
env.LANGFUSE_WORKER_PASSWORD &&
|
||||
env.NEXT_PUBLIC_LANGFUSE_CLOUD_REGION
|
||||
) {
|
||||
console.log(`Sending ${traceEvents.length} events to worker via HTTP`);
|
||||
const body: EventBodyType = {
|
||||
name: EventName.TraceUpsert,
|
||||
payload: traceEvents,
|
||||
};
|
||||
|
||||
if (traceEvents.length > 0) {
|
||||
await fetch(`${env.LANGFUSE_WORKER_HOST}/api/events`, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
Authorization:
|
||||
"Basic " +
|
||||
Buffer.from(
|
||||
"admin" + ":" + env.LANGFUSE_WORKER_PASSWORD
|
||||
).toString("base64"),
|
||||
},
|
||||
body: JSON.stringify(body),
|
||||
signal: AbortSignal.timeout(8 * 1000),
|
||||
});
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error sending events to worker", error);
|
||||
logger.error("Error sending events to worker", error);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -8,7 +8,7 @@ export async function findModel(p: {
|
||||
unit?: string;
|
||||
startTime?: Date;
|
||||
};
|
||||
existingDbObservation?: Observation;
|
||||
existingDbObservation?: Omit<Observation, "input" | "output">;
|
||||
}): Promise<Model | null> {
|
||||
const { event, existingDbObservation } = p;
|
||||
// either get the model from the existing observation
|
||||
|
||||
@@ -115,7 +115,7 @@ export const CreateGenerationBody = CreateSpanBody.extend({
|
||||
z.array(z.string()),
|
||||
z.record(z.string()),
|
||||
])
|
||||
.nullish()
|
||||
.nullish(),
|
||||
)
|
||||
.nullish(),
|
||||
usage: usage,
|
||||
@@ -143,7 +143,7 @@ export const UpdateGenerationBody = UpdateSpanBody.extend({
|
||||
z.array(z.string()),
|
||||
z.record(z.string()),
|
||||
])
|
||||
.nullish()
|
||||
.nullish(),
|
||||
)
|
||||
.nullish(),
|
||||
usage: usage,
|
||||
@@ -174,14 +174,14 @@ export const ScoreBody = z.discriminatedUnion("dataType", [
|
||||
value: z.number(),
|
||||
dataType: z.literal("NUMERIC"),
|
||||
configId: z.string().nullish(),
|
||||
})
|
||||
}),
|
||||
),
|
||||
BaseScoreBody.merge(
|
||||
z.object({
|
||||
value: z.string(),
|
||||
dataType: z.literal("CATEGORICAL"),
|
||||
configId: z.string().nullish(),
|
||||
})
|
||||
}),
|
||||
),
|
||||
BaseScoreBody.merge(
|
||||
z.object({
|
||||
@@ -191,14 +191,14 @@ export const ScoreBody = z.discriminatedUnion("dataType", [
|
||||
}),
|
||||
dataType: z.literal("BOOLEAN"),
|
||||
configId: z.string().nullish(),
|
||||
})
|
||||
}),
|
||||
),
|
||||
BaseScoreBody.merge(
|
||||
z.object({
|
||||
value: z.union([z.string(), z.number()]),
|
||||
dataType: z.undefined(),
|
||||
configId: z.string().nullish(),
|
||||
})
|
||||
}),
|
||||
),
|
||||
]);
|
||||
|
||||
@@ -243,7 +243,7 @@ export const LegacyGenerationsCreateSchema = z.object({
|
||||
modelParameters: z
|
||||
.record(
|
||||
z.string(),
|
||||
z.union([z.string(), z.number(), z.boolean()]).nullish()
|
||||
z.union([z.string(), z.number(), z.boolean()]).nullish(),
|
||||
)
|
||||
.nullish(),
|
||||
prompt: jsonSchema.nullish(),
|
||||
@@ -267,7 +267,7 @@ export const LegacyGenerationPatchSchema = z.object({
|
||||
modelParameters: z
|
||||
.record(
|
||||
z.string(),
|
||||
z.union([z.string(), z.number(), z.boolean()]).nullish()
|
||||
z.union([z.string(), z.number(), z.boolean()]).nullish(),
|
||||
)
|
||||
.nullish(),
|
||||
prompt: jsonSchema.nullish(),
|
||||
@@ -291,7 +291,7 @@ export const LegacyObservationBody = z.object({
|
||||
modelParameters: z
|
||||
.record(
|
||||
z.string(),
|
||||
z.union([z.string(), z.number(), z.boolean()]).nullish()
|
||||
z.union([z.string(), z.number(), z.boolean()]).nullish(),
|
||||
)
|
||||
.nullish(),
|
||||
input: jsonSchema.nullish(),
|
||||
@@ -306,6 +306,7 @@ export const LegacyObservationBody = z.object({
|
||||
|
||||
export const SdkLogEvent = z.object({
|
||||
log: jsonSchema,
|
||||
id: z.string().nullish(), // Not used, but makes downstream processing easier.
|
||||
});
|
||||
|
||||
// definitions for the ingestion API
|
||||
@@ -402,7 +403,7 @@ export const ingestionBatchEvent = z.array(ingestionEvent);
|
||||
export type IngestionBatchEventType = z.infer<typeof ingestionBatchEvent>;
|
||||
|
||||
export const ingestionEventWithProjectId = ingestionEvent.and(
|
||||
z.object({ projectId: z.string() })
|
||||
z.object({ projectId: z.string() }),
|
||||
);
|
||||
export type IngestionEventWithProjectIdType = z.infer<
|
||||
typeof ingestionEventWithProjectId
|
||||
|
||||
@@ -15,7 +15,7 @@ type ValidateAndInflateScoreParams = {
|
||||
};
|
||||
|
||||
export async function validateAndInflateScore(
|
||||
params: ValidateAndInflateScoreParams
|
||||
params: ValidateAndInflateScoreParams,
|
||||
): Promise<Score> {
|
||||
const { body, projectId } = params;
|
||||
|
||||
@@ -29,7 +29,7 @@ export async function validateAndInflateScore(
|
||||
|
||||
if (!config || !validateDbScoreConfigSafe(config).success)
|
||||
throw new LangfuseNotFoundError(
|
||||
"The configId you provided does not match a valid config in this project"
|
||||
"The configId you provided does not match a valid config in this project",
|
||||
);
|
||||
|
||||
validateConfigAgainstBody(body, config as ValidatedScoreConfig);
|
||||
@@ -47,7 +47,7 @@ export async function validateAndInflateScore(
|
||||
|
||||
if (!validation.success) {
|
||||
throw new InvalidRequestError(
|
||||
`Ingested score value type not valid against provided data type. Provide numeric values for numeric and boolean scores, and string values for categorical scores.`
|
||||
`Ingested score value type not valid against provided data type. Provide numeric values for numeric and boolean scores, and string values for categorical scores.`,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -62,7 +62,7 @@ function inferDataType(value: string | number): ScoreDataType {
|
||||
|
||||
function mapStringValueToNumericValue(
|
||||
config: ValidatedScoreConfig,
|
||||
label: string
|
||||
label: string,
|
||||
): number | null {
|
||||
return (
|
||||
config.categories?.find((category) => category.label === label)?.value ??
|
||||
@@ -71,7 +71,7 @@ function mapStringValueToNumericValue(
|
||||
}
|
||||
|
||||
function inflateScoreBody(
|
||||
params: ValidateAndInflateScoreParams & { config?: ValidatedScoreConfig }
|
||||
params: ValidateAndInflateScoreParams & { config?: ValidatedScoreConfig },
|
||||
): Score {
|
||||
const { body, projectId, scoreId, config } = params;
|
||||
|
||||
@@ -105,25 +105,25 @@ function inflateScoreBody(
|
||||
|
||||
function validateConfigAgainstBody(
|
||||
body: any,
|
||||
config: ValidatedScoreConfig
|
||||
config: ValidatedScoreConfig,
|
||||
): void {
|
||||
const { maxValue, minValue, categories, dataType: configDataType } = config;
|
||||
|
||||
if (body.dataType && body.dataType !== configDataType) {
|
||||
throw new InvalidRequestError(
|
||||
`Data type mismatch based on config: expected ${configDataType}, got ${body.dataType}`
|
||||
`Data type mismatch based on config: expected ${configDataType}, got ${body.dataType}`,
|
||||
);
|
||||
}
|
||||
|
||||
if (config.isArchived) {
|
||||
throw new InvalidRequestError(
|
||||
"Config is archived and cannot be used to create new scores. Please restore the config first."
|
||||
"Config is archived and cannot be used to create new scores. Please restore the config first.",
|
||||
);
|
||||
}
|
||||
|
||||
if (config.name !== body.name) {
|
||||
throw new InvalidRequestError(
|
||||
`Name mismatch based on config: expected ${config.name}, got ${body.name}`
|
||||
`Name mismatch based on config: expected ${config.name}, got ${body.name}`,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -136,7 +136,7 @@ function validateConfigAgainstBody(
|
||||
|
||||
if (!dataTypeValidation.success) {
|
||||
throw new InvalidRequestError(
|
||||
`Ingested score body not valid against provided config data type.`
|
||||
`Ingested score body not valid against provided config data type.`,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -154,7 +154,7 @@ function validateConfigAgainstBody(
|
||||
.join(", ");
|
||||
|
||||
throw new InvalidRequestError(
|
||||
`Ingested score body not valid against provided config: ${errorDetails}`
|
||||
`Ingested score body not valid against provided config: ${errorDetails}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,13 +12,3 @@ Throughout our applications we want to use as much Otel as possible. This helps
|
||||
- `web` and `worker` have an instrumentation.ts file, which configures otel for the application.
|
||||
- For trpc, we use `@baselime/trpc-opentelemetry-middleware` to enrich spans with trpc inputs and outputs.
|
||||
- When building adding new infrastructure, we should search for auto instumentations for our code base.
|
||||
|
||||
## Library support
|
||||
|
||||
- `dd-trace` has a direct dependency of `@opentelemetry/api` version < `1.9` [GH](https://github.com/DataDog/dd-trace-js/blob/ed9b0b30f7b0283579a9bf8c18e1f9deab18fecf/package.json#L81).
|
||||
- `@prisma/instrumentation` requires at least `@opentelemetry/api` version `1.8` [GH](https://github.com/prisma/prisma/blob/d780290b13754420abcfa5d7592f02049c6cc005/packages/instrumentation/package.json#L25)
|
||||
- We need to ensure that all other libraries we use are compatible with `@opentelemetry/api` version `1.8`. If we have a package using a later version, this will break the instrumentation and crash the container. We can check the versions with the following command:
|
||||
|
||||
```bash
|
||||
pnpm --filter=web why @opentelemetry/api
|
||||
```
|
||||
|
||||
@@ -3,54 +3,42 @@ import * as dd from "dd-trace";
|
||||
|
||||
// type CallbackFn<T> = () => T;
|
||||
|
||||
export type TCarrier = {
|
||||
traceparent?: string;
|
||||
tracestate?: string;
|
||||
};
|
||||
|
||||
export type SpanCtx = {
|
||||
name: string;
|
||||
spanKind?: opentelemetry.SpanKind; // https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/trace/api.md#spankind
|
||||
rootSpan?: boolean; // https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/overview.md#traces
|
||||
traceScope?: string;
|
||||
traceContext?: TCarrier;
|
||||
};
|
||||
|
||||
type AsyncCallbackFn<T> = () => Promise<T>;
|
||||
type AsyncCallbackFn<T> = (span: opentelemetry.Span) => Promise<T>;
|
||||
|
||||
export async function instrumentAsync<T>(
|
||||
ctx: SpanCtx,
|
||||
callback: AsyncCallbackFn<T>
|
||||
callback: AsyncCallbackFn<T>,
|
||||
): Promise<T> {
|
||||
return await getTracer(ctx.traceScope ?? callback.name).startActiveSpan(
|
||||
ctx.name,
|
||||
{
|
||||
root: ctx.rootSpan,
|
||||
kind: ctx.spanKind,
|
||||
},
|
||||
async (span) => {
|
||||
try {
|
||||
const result = await callback();
|
||||
span.end();
|
||||
return result;
|
||||
} catch (ex) {
|
||||
traceException(ex as opentelemetry.Exception, span);
|
||||
span.end();
|
||||
throw ex;
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
const activeContext = ctx.traceContext
|
||||
? opentelemetry.propagation.extract(
|
||||
opentelemetry.context.active(),
|
||||
ctx.traceContext,
|
||||
)
|
||||
: opentelemetry.context.active();
|
||||
|
||||
type SyncCallbackFn<T> = () => T;
|
||||
|
||||
export function instrumentSync<T>(
|
||||
ctx: SpanCtx,
|
||||
callback: SyncCallbackFn<T>
|
||||
): T {
|
||||
return getTracer(ctx.traceScope ?? callback.name).startActiveSpan(
|
||||
ctx.name,
|
||||
{
|
||||
root: ctx.rootSpan,
|
||||
root: !Boolean(ctx.traceContext) && ctx.rootSpan,
|
||||
kind: ctx.spanKind,
|
||||
},
|
||||
(span) => {
|
||||
activeContext,
|
||||
async (span) => {
|
||||
try {
|
||||
const result = callback();
|
||||
const result = await callback(span);
|
||||
span.end();
|
||||
return result;
|
||||
} catch (ex) {
|
||||
@@ -58,7 +46,41 @@ export function instrumentSync<T>(
|
||||
span.end();
|
||||
throw ex;
|
||||
}
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
type SyncCallbackFn<T> = (span: opentelemetry.Span) => T;
|
||||
|
||||
export function instrumentSync<T>(
|
||||
ctx: SpanCtx,
|
||||
callback: SyncCallbackFn<T>,
|
||||
): T {
|
||||
const activeContext = ctx.traceContext
|
||||
? opentelemetry.propagation.extract(
|
||||
opentelemetry.context.active(),
|
||||
ctx.traceContext,
|
||||
)
|
||||
: opentelemetry.context.active();
|
||||
|
||||
return getTracer(ctx.traceScope ?? callback.name).startActiveSpan(
|
||||
ctx.name,
|
||||
{
|
||||
root: !Boolean(ctx.traceContext) && ctx.rootSpan,
|
||||
kind: ctx.spanKind,
|
||||
},
|
||||
activeContext,
|
||||
(span) => {
|
||||
try {
|
||||
const result = callback(span);
|
||||
span.end();
|
||||
return result;
|
||||
} catch (ex) {
|
||||
traceException(ex as opentelemetry.Exception, span);
|
||||
span.end();
|
||||
throw ex;
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
@@ -67,7 +89,7 @@ export const getCurrentSpan = () => opentelemetry.trace.getActiveSpan();
|
||||
export const traceException = (
|
||||
ex: unknown,
|
||||
span?: opentelemetry.Span,
|
||||
code?: string
|
||||
code?: string,
|
||||
) => {
|
||||
const activeSpan = span ?? getCurrentSpan();
|
||||
|
||||
@@ -115,7 +137,7 @@ export const traceException = (
|
||||
|
||||
export const addUserToSpan = (
|
||||
attributes: { userId?: string; projectId?: string; email?: string },
|
||||
span?: opentelemetry.Span
|
||||
span?: opentelemetry.Span,
|
||||
) => {
|
||||
const activeSpan = span ?? getCurrentSpan();
|
||||
|
||||
@@ -138,7 +160,7 @@ export const recordGauge = (
|
||||
| {
|
||||
[tag: string]: string | number;
|
||||
}
|
||||
| undefined
|
||||
| undefined,
|
||||
) => {
|
||||
dd.dogstatsd.gauge(stat, value, tags);
|
||||
};
|
||||
@@ -146,7 +168,7 @@ export const recordGauge = (
|
||||
export const recordIncrement = (
|
||||
stat: string,
|
||||
value?: number | undefined,
|
||||
tags?: { [tag: string]: string | number } | undefined
|
||||
tags?: { [tag: string]: string | number } | undefined,
|
||||
) => {
|
||||
dd.dogstatsd.increment(stat, value, tags);
|
||||
};
|
||||
@@ -154,7 +176,19 @@ export const recordIncrement = (
|
||||
export const recordHistogram = (
|
||||
stat: string,
|
||||
value?: number | undefined,
|
||||
tags?: { [tag: string]: string | number } | undefined
|
||||
tags?: { [tag: string]: string | number } | undefined,
|
||||
) => {
|
||||
dd.dogstatsd.histogram(stat, value, tags);
|
||||
};
|
||||
|
||||
/**
|
||||
* Converts a queue name to the matching datadog metric name.
|
||||
* Consumer only needs to append the relevant suffix.
|
||||
*
|
||||
* Example: `legacy-ingestion-queue` -> `langfuse.queue.legacy_ingestion`
|
||||
*/
|
||||
export const convertQueueNameToMetricName = (queueName: string): string => {
|
||||
return (
|
||||
"langfuse.queue." + queueName.replace(/-/g, "_").replace(/_queue$/, "")
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import { type ZodSchema } from "zod";
|
||||
|
||||
import { ChatAnthropic } from "@langchain/anthropic";
|
||||
import {
|
||||
AIMessage,
|
||||
@@ -12,20 +14,12 @@ import {
|
||||
import { IterableReadableStream } from "@langchain/core/utils/stream";
|
||||
import { ChatOpenAI } from "@langchain/openai";
|
||||
|
||||
import {
|
||||
ChatMessage,
|
||||
ChatMessageRole,
|
||||
LLMFunctionCall,
|
||||
ModelParams,
|
||||
LLMAdapter,
|
||||
} from "./types";
|
||||
import zodToJsonSchema from "zod-to-json-schema";
|
||||
import { JsonOutputFunctionsParser } from "langchain/output_parsers";
|
||||
import { ChatMessage, ChatMessageRole, ModelParams, LLMAdapter } from "./types";
|
||||
|
||||
type LLMCompletionParams = {
|
||||
messages: ChatMessage[];
|
||||
modelParams: ModelParams;
|
||||
functionCall?: LLMFunctionCall;
|
||||
structuredOutputSchema?: ZodSchema;
|
||||
callbacks?: BaseCallbackHandler[];
|
||||
baseURL?: string;
|
||||
apiKey?: string;
|
||||
@@ -51,7 +45,7 @@ export async function fetchLLMCompletion(
|
||||
export async function fetchLLMCompletion(
|
||||
params: LLMCompletionParams & {
|
||||
streaming: false;
|
||||
functionCall: LLMFunctionCall;
|
||||
structuredOutputSchema: ZodSchema;
|
||||
}
|
||||
): Promise<unknown>;
|
||||
|
||||
@@ -121,18 +115,41 @@ export async function fetchLLMCompletion(
|
||||
throw new Error("This model provider is not supported.");
|
||||
}
|
||||
|
||||
if (params.functionCall) {
|
||||
const functionCallingModel = chatModel.bind({
|
||||
functions: [
|
||||
{
|
||||
...params.functionCall,
|
||||
parameters: zodToJsonSchema(params.functionCall.parameters),
|
||||
},
|
||||
],
|
||||
function_call: { name: params.functionCall.name },
|
||||
});
|
||||
const outputParser = new JsonOutputFunctionsParser();
|
||||
return await functionCallingModel.pipe(outputParser).invoke(finalMessages);
|
||||
if (params.structuredOutputSchema) {
|
||||
return await (chatModel as ChatOpenAI) // Typecast necessary due to https://github.com/langchain-ai/langchainjs/issues/6795
|
||||
.withStructuredOutput(params.structuredOutputSchema)
|
||||
.invoke(finalMessages);
|
||||
}
|
||||
|
||||
/*
|
||||
Workaround OpenAI o1 while in beta:
|
||||
|
||||
This is a temporary workaround to avoid sending system messages to OpenAI's O1 models.
|
||||
O1 models do not support in beta:
|
||||
- system messages
|
||||
- top_p
|
||||
- max_tokens at all, one has to use max_completion_tokens instead
|
||||
- temperature different than 1
|
||||
|
||||
Reference: https://platform.openai.com/docs/guides/reasoning/beta-limitations
|
||||
*/
|
||||
if (modelParams.model.startsWith("o1-")) {
|
||||
return await new ChatOpenAI({
|
||||
openAIApiKey: apiKey,
|
||||
modelName: modelParams.model,
|
||||
temperature: 1,
|
||||
maxTokens: undefined,
|
||||
topP: undefined,
|
||||
callbacks,
|
||||
maxRetries,
|
||||
configuration: {
|
||||
baseURL,
|
||||
},
|
||||
})
|
||||
.pipe(new StringOutputParser())
|
||||
.invoke(
|
||||
finalMessages.filter((message) => message._getType() !== "system")
|
||||
);
|
||||
}
|
||||
|
||||
if (streaming) {
|
||||
|
||||
@@ -4,7 +4,7 @@ import z from "zod";
|
||||
export type PromptVariable = { name: string; value: string; isUsed: boolean };
|
||||
|
||||
export type ChatMessage = {
|
||||
role: ChatMessageRole;
|
||||
role: ChatMessageRole | string; // Users may ingest any string as role via API/SDK
|
||||
content: string;
|
||||
};
|
||||
|
||||
@@ -22,6 +22,8 @@ export enum ChatMessageRole {
|
||||
Assistant = "assistant",
|
||||
}
|
||||
|
||||
export const ChatMessageDefaultRoleSchema = z.nativeEnum(ChatMessageRole);
|
||||
|
||||
export type ModelParams = {
|
||||
provider: string;
|
||||
adapter: LLMAdapter;
|
||||
@@ -52,6 +54,10 @@ export const openAIModels = [
|
||||
"gpt-4o-2024-05-13",
|
||||
"gpt-4o-mini",
|
||||
"gpt-4o-mini-2024-07-18",
|
||||
"o1-preview",
|
||||
"o1-preview-2024-09-12",
|
||||
"o1-mini",
|
||||
"o1-mini-2024-09-12",
|
||||
"gpt-4-turbo-preview",
|
||||
"gpt-4-1106-preview",
|
||||
"gpt-4-0613",
|
||||
|
||||
@@ -0,0 +1,50 @@
|
||||
import { env } from "../env";
|
||||
import winston from "winston";
|
||||
import { getCurrentSpan } from "./instrumentation";
|
||||
|
||||
const tracingFormat = function () {
|
||||
return winston.format((info) => {
|
||||
const span = getCurrentSpan();
|
||||
if (span) {
|
||||
const { spanId, traceId } = span.spanContext();
|
||||
const traceIdEnd = traceId.slice(traceId.length / 2);
|
||||
info["dd.trace_id"] = BigInt(`0x${traceIdEnd}`).toString();
|
||||
info["dd.span_id"] = BigInt(`0x${spanId}`).toString();
|
||||
info["trace_id"] = traceId;
|
||||
info["span_id"] = spanId;
|
||||
}
|
||||
return info;
|
||||
})();
|
||||
};
|
||||
|
||||
const getWinstonLogger = (
|
||||
nodeEnv: "development" | "production" | "test",
|
||||
minLevel = "info",
|
||||
) => {
|
||||
const textLoggerFormat = winston.format.combine(
|
||||
winston.format.errors({ stack: true }),
|
||||
winston.format.timestamp(),
|
||||
winston.format.align(),
|
||||
winston.format.printf((info) => {
|
||||
const logMessage = `${info.timestamp} ${info.level} ${info.message}`;
|
||||
return info.stack ? `${logMessage}\n${info.stack}` : logMessage;
|
||||
}),
|
||||
);
|
||||
|
||||
const jsonLoggerFormat = winston.format.combine(
|
||||
winston.format.errors({ stack: true }),
|
||||
winston.format.timestamp(),
|
||||
tracingFormat(),
|
||||
winston.format.json(),
|
||||
);
|
||||
|
||||
const format =
|
||||
env.LANGFUSE_LOG_FORMAT === "text" ? textLoggerFormat : jsonLoggerFormat;
|
||||
return winston.createLogger({
|
||||
level: minLevel,
|
||||
format: format,
|
||||
transports: [new winston.transports.Console()],
|
||||
});
|
||||
};
|
||||
|
||||
export const logger = getWinstonLogger(env.NODE_ENV, env.LANGFUSE_LOG_LEVEL);
|
||||
+8
-7
@@ -2,8 +2,9 @@ import { z } from "zod";
|
||||
|
||||
import { Prisma } from "@prisma/client";
|
||||
|
||||
import type { ColumnDefinition } from "./tableDefinitions/types";
|
||||
import type { OrderByState } from "./interfaces/orderBy";
|
||||
import type { ColumnDefinition } from "../tableDefinitions/types";
|
||||
import type { OrderByState } from "../interfaces/orderBy";
|
||||
import { logger } from "./logger";
|
||||
|
||||
/**
|
||||
* Convert orderBy to SQL ORDER BY clause
|
||||
@@ -13,7 +14,7 @@ import type { OrderByState } from "./interfaces/orderBy";
|
||||
*/
|
||||
export function orderByToPrismaSql(
|
||||
orderBy: OrderByState,
|
||||
tableColumns: ColumnDefinition[]
|
||||
tableColumns: ColumnDefinition[],
|
||||
): Prisma.Sql {
|
||||
if (!orderBy) {
|
||||
return Prisma.sql`ORDER BY t.timestamp DESC`;
|
||||
@@ -22,11 +23,11 @@ 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) {
|
||||
console.log("Invalid filter column", orderBy.column);
|
||||
logger.warn("Invalid filter column", orderBy.column);
|
||||
throw new Error("Invalid filter column: " + orderBy.column);
|
||||
}
|
||||
|
||||
@@ -34,12 +35,12 @@ export function orderByToPrismaSql(
|
||||
const orderByOrder = z.enum(["ASC", "DESC"]);
|
||||
const order = orderByOrder.safeParse(orderBy.order);
|
||||
if (!order.success) {
|
||||
console.log("Invalid order", orderBy.order);
|
||||
logger.warn("Invalid order", orderBy.order);
|
||||
throw new Error("Invalid order: " + orderBy.order);
|
||||
}
|
||||
|
||||
// Both column and order are safe, can use raw SQL
|
||||
return Prisma.raw(
|
||||
`ORDER BY ${col.internal} ${order.data} ${col.nullable ? (orderBy.order === "DESC" ? "NULLS LAST" : "NULLS FIRST") : ""}`
|
||||
`ORDER BY ${col.internal} ${order.data} ${col.nullable ? (orderBy.order === "DESC" ? "NULLS LAST" : "NULLS FIRST") : ""}`,
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,172 @@
|
||||
import { ObservationView, Prisma } from "@prisma/client";
|
||||
import {
|
||||
datetimeFilterToPrismaSql,
|
||||
tableColumnsToSqlFilterAndPrefix,
|
||||
} from "../filterToPrisma";
|
||||
import { orderByToPrismaSql } from "../orderByToPrisma";
|
||||
import { observationsTableCols } from "../../observationsTable";
|
||||
import { TableFilters } from "./types";
|
||||
|
||||
type AdditionalObservationFields = {
|
||||
traceName: string | null;
|
||||
promptName: string | null;
|
||||
promptVersion: string | null;
|
||||
traceTags: Array<string>;
|
||||
};
|
||||
|
||||
type FullObservation = AdditionalObservationFields & ObservationView;
|
||||
|
||||
export type FullObservations = Array<FullObservation>;
|
||||
|
||||
export type FullObservationsWithScores = Array<
|
||||
FullObservation & { scores?: Record<string, string[] | number[]> | null }
|
||||
>;
|
||||
|
||||
export type IOAndMetadataOmittedObservations = Array<
|
||||
Omit<ObservationView, "input" | "output" | "metadata"> &
|
||||
AdditionalObservationFields
|
||||
>;
|
||||
|
||||
export function parseGetAllGenerationsInput(filters: TableFilters) {
|
||||
const searchCondition = filters.searchQuery
|
||||
? Prisma.sql`AND (
|
||||
o."id" ILIKE ${`%${filters.searchQuery}%`} OR
|
||||
o."name" ILIKE ${`%${filters.searchQuery}%`} OR
|
||||
o."model" ILIKE ${`%${filters.searchQuery}%`} OR
|
||||
t."name" ILIKE ${`%${filters.searchQuery}%`}
|
||||
)`
|
||||
: Prisma.empty;
|
||||
|
||||
const filterCondition = tableColumnsToSqlFilterAndPrefix(
|
||||
filters.filter ?? [],
|
||||
observationsTableCols,
|
||||
"observations"
|
||||
);
|
||||
|
||||
const orderByCondition = orderByToPrismaSql(
|
||||
filters.orderBy,
|
||||
observationsTableCols
|
||||
);
|
||||
|
||||
// to improve query performance, add timeseries filter to observation queries as well
|
||||
const startTimeFilter = filters.filter?.find(
|
||||
(f) => f.column === "Start Time" && f.type === "datetime"
|
||||
);
|
||||
|
||||
const datetimeFilter =
|
||||
startTimeFilter && startTimeFilter.type === "datetime"
|
||||
? datetimeFilterToPrismaSql(
|
||||
"start_time",
|
||||
startTimeFilter.operator,
|
||||
startTimeFilter.value
|
||||
)
|
||||
: Prisma.empty;
|
||||
|
||||
return {
|
||||
searchCondition,
|
||||
filterCondition,
|
||||
orderByCondition,
|
||||
datetimeFilter,
|
||||
};
|
||||
}
|
||||
|
||||
export function createGenerationsQuery({
|
||||
projectId,
|
||||
datetimeFilter = Prisma.empty,
|
||||
page,
|
||||
limit,
|
||||
searchCondition = Prisma.empty,
|
||||
filterCondition = Prisma.empty,
|
||||
orderByCondition = Prisma.empty,
|
||||
selectIOAndMetadata = false,
|
||||
selectScoreValues = false,
|
||||
}: {
|
||||
projectId: string;
|
||||
datetimeFilter?: Prisma.Sql;
|
||||
page?: number;
|
||||
limit?: number;
|
||||
searchCondition?: Prisma.Sql;
|
||||
filterCondition?: Prisma.Sql;
|
||||
orderByCondition?: Prisma.Sql;
|
||||
selectIOAndMetadata?: boolean;
|
||||
selectScoreValues?: boolean;
|
||||
}) {
|
||||
return Prisma.sql`
|
||||
WITH scores_avg AS (
|
||||
SELECT
|
||||
trace_id,
|
||||
observation_id,
|
||||
${selectScoreValues ? Prisma.sql`jsonb_object_agg(name::text, "values") AS "scores_values",` : Prisma.empty}
|
||||
jsonb_object_agg(name::text, avg_value::double precision) AS "scores_avg"
|
||||
FROM (
|
||||
SELECT
|
||||
trace_id,
|
||||
observation_id,
|
||||
name,
|
||||
${selectScoreValues ? Prisma.sql`array_agg(COALESCE(string_value, value::text)) AS "values",` : Prisma.empty}
|
||||
avg(value) avg_value,
|
||||
comment
|
||||
FROM
|
||||
scores
|
||||
WHERE
|
||||
project_id = ${projectId}
|
||||
${selectScoreValues ? Prisma.empty : Prisma.sql`AND scores."data_type" IN ('NUMERIC', 'BOOLEAN')`}
|
||||
GROUP BY
|
||||
trace_id,
|
||||
observation_id,
|
||||
name,
|
||||
comment
|
||||
ORDER BY
|
||||
trace_id
|
||||
) tmp
|
||||
GROUP BY
|
||||
trace_id,
|
||||
observation_id
|
||||
)
|
||||
SELECT
|
||||
${selectScoreValues ? Prisma.sql`s_avg."scores_values" AS "scores",` : Prisma.empty}
|
||||
o.id,
|
||||
o.name,
|
||||
o.model,
|
||||
o."modelParameters",
|
||||
o.start_time as "startTime",
|
||||
o.end_time as "endTime",
|
||||
${selectIOAndMetadata ? Prisma.sql`o.input, o.output, o.metadata,` : Prisma.empty}
|
||||
o.trace_id as "traceId",
|
||||
t.name as "traceName",
|
||||
o.completion_start_time as "completionStartTime",
|
||||
o.time_to_first_token as "timeToFirstToken",
|
||||
o.prompt_tokens as "promptTokens",
|
||||
o.completion_tokens as "completionTokens",
|
||||
o.total_tokens as "totalTokens",
|
||||
o.unit,
|
||||
o.level,
|
||||
o.status_message as "statusMessage",
|
||||
o.version,
|
||||
o.model_id as "modelId",
|
||||
o.input_price as "inputPrice",
|
||||
o.output_price as "outputPrice",
|
||||
o.total_price as "totalPrice",
|
||||
o.calculated_input_cost as "calculatedInputCost",
|
||||
o.calculated_output_cost as "calculatedOutputCost",
|
||||
o.calculated_total_cost as "calculatedTotalCost",
|
||||
o."latency",
|
||||
o.prompt_id as "promptId",
|
||||
p.name as "promptName",
|
||||
p.version as "promptVersion",
|
||||
t.tags as "traceTags"
|
||||
FROM observations_view o
|
||||
JOIN traces t ON t.id = o.trace_id AND t.project_id = ${projectId}
|
||||
LEFT JOIN scores_avg AS s_avg ON s_avg.trace_id = t.id and s_avg.observation_id = o.id
|
||||
LEFT JOIN prompts p ON p.id = o.prompt_id AND p.project_id = ${projectId}
|
||||
WHERE
|
||||
o.project_id = ${projectId}
|
||||
AND o.type = 'GENERATION'
|
||||
${datetimeFilter}
|
||||
${searchCondition}
|
||||
${filterCondition}
|
||||
${orderByCondition}
|
||||
${limit ? Prisma.sql`LIMIT ${limit}` : Prisma.empty}
|
||||
${page && limit ? Prisma.sql`OFFSET ${page * limit}` : Prisma.empty}
|
||||
`;
|
||||
}
|
||||
+5
-5
@@ -2,10 +2,10 @@ import { z } from "zod";
|
||||
|
||||
import { Prisma } from "@prisma/client";
|
||||
import { tableColumnsToSqlFilterAndPrefix } from "../filterToPrisma";
|
||||
import { singleFilter } from "../interfaces/filters";
|
||||
import { orderBy } from "../interfaces/orderBy";
|
||||
import { singleFilter } from "../../interfaces/filters";
|
||||
import { orderBy } from "../../interfaces/orderBy";
|
||||
import { orderByToPrismaSql } from "../orderByToPrisma";
|
||||
import { sessionsViewCols } from "../tableDefinitions/index";
|
||||
import { sessionsViewCols } from "../../tableDefinitions";
|
||||
|
||||
const GetSessionTableSQLParamsSchema = z.object({
|
||||
projectId: z.string(),
|
||||
@@ -22,7 +22,7 @@ export const createSessionsAllQuery = (
|
||||
options?: {
|
||||
ignoreOrderBy?: boolean; // used by session.metrics and session.all.totalCount
|
||||
sessionIdList?: string[]; // used by session.metrics
|
||||
}
|
||||
},
|
||||
): Prisma.Sql => {
|
||||
const { projectId, filter, orderBy, page, limit } =
|
||||
GetSessionTableSQLParamsSchema.parse(params);
|
||||
@@ -30,7 +30,7 @@ export const createSessionsAllQuery = (
|
||||
const filterCondition = tableColumnsToSqlFilterAndPrefix(
|
||||
filter ?? [],
|
||||
sessionsViewCols,
|
||||
"sessions"
|
||||
"sessions",
|
||||
);
|
||||
const orderByCondition = orderByToPrismaSql(orderBy, sessionsViewCols);
|
||||
|
||||
@@ -0,0 +1,135 @@
|
||||
import { Prisma } from "@prisma/client";
|
||||
import { TableFilters } from "./types";
|
||||
import {
|
||||
datetimeFilterToPrismaSql,
|
||||
tableColumnsToSqlFilterAndPrefix,
|
||||
} from "../filterToPrisma";
|
||||
import { tracesTableCols } from "../../tracesTable";
|
||||
import { orderByToPrismaSql } from "../orderByToPrisma";
|
||||
|
||||
export function parseTraceAllFilters(input: TableFilters) {
|
||||
const filterCondition = tableColumnsToSqlFilterAndPrefix(
|
||||
input.filter ?? [],
|
||||
tracesTableCols,
|
||||
"traces"
|
||||
);
|
||||
const orderByCondition = orderByToPrismaSql(input.orderBy, tracesTableCols);
|
||||
|
||||
// to improve query performance, add timeseries filter to observation queries as well
|
||||
const timeseriesFilter = input.filter?.find(
|
||||
(f) => f.column === "Timestamp" && f.type === "datetime"
|
||||
);
|
||||
|
||||
const observationTimeseriesFilter =
|
||||
timeseriesFilter && timeseriesFilter.type === "datetime"
|
||||
? datetimeFilterToPrismaSql(
|
||||
"start_time",
|
||||
timeseriesFilter.operator,
|
||||
timeseriesFilter.value
|
||||
)
|
||||
: Prisma.empty;
|
||||
|
||||
const searchCondition = input.searchQuery
|
||||
? Prisma.sql`AND (
|
||||
t."id" ILIKE ${`%${input.searchQuery}%`} OR
|
||||
t."external_id" ILIKE ${`%${input.searchQuery}%`} OR
|
||||
t."user_id" ILIKE ${`%${input.searchQuery}%`} OR
|
||||
t."name" ILIKE ${`%${input.searchQuery}%`}
|
||||
)`
|
||||
: Prisma.empty;
|
||||
|
||||
return {
|
||||
filterCondition,
|
||||
orderByCondition,
|
||||
observationTimeseriesFilter,
|
||||
searchCondition,
|
||||
};
|
||||
}
|
||||
|
||||
export function createTracesQuery({
|
||||
select,
|
||||
projectId,
|
||||
observationTimeseriesFilter = Prisma.empty,
|
||||
page,
|
||||
limit,
|
||||
searchCondition = Prisma.empty,
|
||||
filterCondition = Prisma.empty,
|
||||
orderByCondition = Prisma.empty,
|
||||
selectScoreValues = false,
|
||||
}: {
|
||||
select: Prisma.Sql;
|
||||
projectId: string;
|
||||
observationTimeseriesFilter?: Prisma.Sql;
|
||||
page?: number;
|
||||
limit?: number;
|
||||
searchCondition?: Prisma.Sql;
|
||||
filterCondition?: Prisma.Sql;
|
||||
orderByCondition?: Prisma.Sql;
|
||||
selectScoreValues?: boolean;
|
||||
}) {
|
||||
return Prisma.sql`
|
||||
SELECT
|
||||
${select}
|
||||
FROM
|
||||
"traces" AS t
|
||||
LEFT JOIN LATERAL (
|
||||
SELECT
|
||||
SUM(prompt_tokens) AS "promptTokens",
|
||||
SUM(completion_tokens) AS "completionTokens",
|
||||
SUM(total_tokens) AS "totalTokens",
|
||||
SUM(calculated_total_cost) AS "calculatedTotalCost",
|
||||
SUM(calculated_input_cost) AS "calculatedInputCost",
|
||||
SUM(calculated_output_cost) AS "calculatedOutputCost"
|
||||
FROM
|
||||
"observations_view"
|
||||
WHERE
|
||||
trace_id = t.id
|
||||
AND "type" = 'GENERATION'
|
||||
AND "project_id" = ${projectId}
|
||||
${observationTimeseriesFilter}
|
||||
) AS generation_metrics ON true
|
||||
LEFT JOIN LATERAL (
|
||||
SELECT
|
||||
COUNT(*) AS "observationCount",
|
||||
EXTRACT(EPOCH FROM COALESCE(MAX("end_time"), MAX("start_time"))) - EXTRACT(EPOCH FROM MIN("start_time"))::double precision AS "latency",
|
||||
COALESCE(
|
||||
MAX(CASE WHEN level = 'ERROR' THEN 'ERROR' END),
|
||||
MAX(CASE WHEN level = 'WARNING' THEN 'WARNING' END),
|
||||
MAX(CASE WHEN level = 'DEFAULT' THEN 'DEFAULT' END),
|
||||
'DEBUG'
|
||||
) AS "level"
|
||||
FROM
|
||||
"observations"
|
||||
WHERE
|
||||
trace_id = t.id
|
||||
AND "project_id" = ${projectId}
|
||||
${observationTimeseriesFilter}
|
||||
) AS observation_metrics ON true
|
||||
LEFT JOIN LATERAL (
|
||||
SELECT
|
||||
${selectScoreValues ? Prisma.sql`jsonb_object_agg(name::text, "values") AS "scores_values",` : Prisma.empty}
|
||||
jsonb_object_agg(name::text, avg_value::double precision) AS "scores_avg"
|
||||
FROM (
|
||||
SELECT
|
||||
name,
|
||||
${selectScoreValues ? Prisma.sql`array_agg(COALESCE(string_value, value::text)) AS "values",` : Prisma.empty}
|
||||
AVG(value) avg_value
|
||||
FROM
|
||||
scores
|
||||
WHERE
|
||||
trace_id = t.id
|
||||
AND t."project_id" = ${projectId}
|
||||
${selectScoreValues ? Prisma.empty : Prisma.sql`AND scores."data_type" IN ('NUMERIC', 'BOOLEAN')`}
|
||||
GROUP BY
|
||||
name
|
||||
) tmp
|
||||
) AS s_avg ON true
|
||||
WHERE
|
||||
t."project_id" = ${projectId}
|
||||
${searchCondition}
|
||||
${filterCondition}
|
||||
${orderByCondition}
|
||||
${limit ? Prisma.sql`LIMIT ${limit}` : Prisma.empty}
|
||||
${page && limit ? Prisma.sql`OFFSET ${page * limit}` : Prisma.empty}
|
||||
`;
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
export { createSessionsAllQuery } from "./createSessionsAllQuery";
|
||||
export { createTracesQuery, parseTraceAllFilters } from "./createTracesQuery";
|
||||
export {
|
||||
createGenerationsQuery,
|
||||
parseGetAllGenerationsInput,
|
||||
type FullObservations,
|
||||
type FullObservationsWithScores,
|
||||
type IOAndMetadataOmittedObservations,
|
||||
} from "./createGenerationsQuery";
|
||||
@@ -0,0 +1,14 @@
|
||||
import z from "zod";
|
||||
import { singleFilter } from "../../interfaces/filters";
|
||||
import { orderBy } from "../../interfaces/orderBy";
|
||||
import { optionalPaginationZod } from "../../utils/zod";
|
||||
|
||||
const TableFilterSchema = z.object({
|
||||
projectId: z.string(),
|
||||
filter: z.array(singleFilter).nullish(),
|
||||
searchQuery: z.string().nullish(),
|
||||
orderBy: orderBy,
|
||||
...optionalPaginationZod,
|
||||
});
|
||||
|
||||
export type TableFilters = z.infer<typeof TableFilterSchema>;
|
||||
@@ -1,5 +1,5 @@
|
||||
import { z } from "zod";
|
||||
import { ingestionBatchEvent } from ".";
|
||||
import { eventTypes, ingestionBatchEvent, TCarrier } from ".";
|
||||
|
||||
export enum EventName {
|
||||
TraceUpsert = "TraceUpsert",
|
||||
@@ -9,7 +9,8 @@ export enum EventName {
|
||||
CloudUsageMetering = "CloudUsageMetering",
|
||||
}
|
||||
|
||||
export const LegacyIngestionEvent = z.object({
|
||||
export const LegacyIngestionEventFull = z.object({
|
||||
useS3EventStore: z.literal(false),
|
||||
data: ingestionBatchEvent,
|
||||
authCheck: z.object({
|
||||
validKey: z.literal(true),
|
||||
@@ -20,6 +21,29 @@ export const LegacyIngestionEvent = z.object({
|
||||
}),
|
||||
});
|
||||
|
||||
export const LegacyIngestionEventMeta = z.object({
|
||||
useS3EventStore: z.literal(true),
|
||||
data: z.array(
|
||||
z.object({
|
||||
type: z.nativeEnum(eventTypes),
|
||||
eventBodyId: z.string(),
|
||||
eventId: z.string(),
|
||||
})
|
||||
),
|
||||
authCheck: z.object({
|
||||
validKey: z.literal(true),
|
||||
scope: z.object({
|
||||
projectId: z.string(),
|
||||
accessLevel: z.enum(["all", "scores"]),
|
||||
}),
|
||||
}),
|
||||
});
|
||||
|
||||
export const LegacyIngestionEvent = z.discriminatedUnion("useS3EventStore", [
|
||||
LegacyIngestionEventFull,
|
||||
LegacyIngestionEventMeta,
|
||||
]);
|
||||
|
||||
export const BatchExportJobSchema = z.object({
|
||||
projectId: z.string(),
|
||||
batchExportId: z.string(),
|
||||
@@ -31,6 +55,7 @@ export const TraceUpsertEventSchema = z.object({
|
||||
export const EvalExecutionEvent = z.object({
|
||||
projectId: z.string(),
|
||||
jobExecutionId: z.string(),
|
||||
delay: z.number().nullish(),
|
||||
});
|
||||
|
||||
export type BatchExportJobType = z.infer<typeof BatchExportJobSchema>;
|
||||
@@ -80,23 +105,27 @@ export type TQueueJobTypes = {
|
||||
id: string;
|
||||
payload: TraceUpsertEventType;
|
||||
name: QueueJobs.TraceUpsert;
|
||||
_tracecontext?: TCarrier;
|
||||
};
|
||||
[QueueName.EvaluationExecution]: {
|
||||
timestamp: Date;
|
||||
id: string;
|
||||
payload: EvalExecutionEventType;
|
||||
name: QueueJobs.EvaluationExecution;
|
||||
_tracecontext?: TCarrier;
|
||||
};
|
||||
[QueueName.BatchExport]: {
|
||||
timestamp: Date;
|
||||
id: string;
|
||||
payload: BatchExportJobType;
|
||||
name: QueueJobs.BatchExportJob;
|
||||
_tracecontext?: TCarrier;
|
||||
};
|
||||
[QueueName.LegacyIngestionQueue]: {
|
||||
timestamp: Date;
|
||||
id: string;
|
||||
payload: LegacyIngestionEventType;
|
||||
name: QueueJobs.LegacyIngestionJob;
|
||||
_tracecontext?: TCarrier;
|
||||
};
|
||||
};
|
||||
|
||||
@@ -15,8 +15,12 @@ export const getBatchExportQueue = () => {
|
||||
connection: connection,
|
||||
defaultJobOptions: {
|
||||
removeOnComplete: true,
|
||||
removeOnFail: 100,
|
||||
removeOnFail: 10_000,
|
||||
attempts: 2,
|
||||
backoff: {
|
||||
type: "exponential",
|
||||
delay: 5000,
|
||||
},
|
||||
},
|
||||
})
|
||||
: null;
|
||||
|
||||
@@ -18,9 +18,13 @@ export const getIngestionFlushQueue = () => {
|
||||
connection: connection,
|
||||
defaultJobOptions: {
|
||||
removeOnComplete: true, // Important: If not true, new jobs for that ID would be ignored as jobs in the complete set are still considered as part of the queue
|
||||
removeOnFail: 1_000,
|
||||
removeOnFail: 100_000,
|
||||
delay: env.LANGFUSE_INGESTION_FLUSH_DELAY_MS,
|
||||
attempts: env.LANGFUSE_INGESTION_FLUSH_ATTEMPTS,
|
||||
backoff: {
|
||||
type: "exponential",
|
||||
delay: 5000,
|
||||
},
|
||||
},
|
||||
})
|
||||
: null;
|
||||
|
||||
@@ -21,8 +21,12 @@ export class LegacyIngestionQueue {
|
||||
connection: newRedis,
|
||||
defaultJobOptions: {
|
||||
removeOnComplete: true,
|
||||
removeOnFail: 100,
|
||||
removeOnFail: 100_000,
|
||||
attempts: 5,
|
||||
backoff: {
|
||||
type: "exponential",
|
||||
delay: 5000,
|
||||
},
|
||||
},
|
||||
},
|
||||
)
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import Redis, { RedisOptions } from "ioredis";
|
||||
import { env } from "../../env";
|
||||
import { logger } from "../logger";
|
||||
|
||||
export const createNewRedisInstance = (
|
||||
additionalOptions: Partial<RedisOptions> = {},
|
||||
@@ -26,7 +27,7 @@ const createRedisClient = () => {
|
||||
try {
|
||||
return createNewRedisInstance();
|
||||
} catch (e) {
|
||||
console.error(e, "Failed to connect to redis");
|
||||
logger.error("Failed to connect to redis", e);
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
@@ -21,7 +21,12 @@ export const getTraceUpsertQueue = () => {
|
||||
connection: connection,
|
||||
defaultJobOptions: {
|
||||
removeOnComplete: 100, // Important: If not true, new jobs for that ID would be ignored as jobs in the complete set are still considered as part of the queue
|
||||
removeOnFail: 1_000,
|
||||
removeOnFail: 100_000,
|
||||
attempts: 5,
|
||||
backoff: {
|
||||
type: "exponential",
|
||||
delay: 5000,
|
||||
},
|
||||
},
|
||||
})
|
||||
: null;
|
||||
@@ -30,7 +35,7 @@ export const getTraceUpsertQueue = () => {
|
||||
};
|
||||
|
||||
export function convertTraceUpsertEventsToRedisEvents(
|
||||
events: TraceUpsertEventType[]
|
||||
events: TraceUpsertEventType[],
|
||||
) {
|
||||
const uniqueTracesPerProject = events.reduce((acc, event) => {
|
||||
if (!acc.get(event.projectId)) {
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { Prompt, PrismaClient } from "@prisma/client";
|
||||
import { Redis } from "ioredis";
|
||||
import { env } from "../../env";
|
||||
import { logger } from "../logger";
|
||||
|
||||
export class PromptService {
|
||||
private cacheEnabled: boolean;
|
||||
@@ -11,7 +12,7 @@ export class PromptService {
|
||||
private redis: Redis | null,
|
||||
private metricIncrementer?: // used for otel metrics
|
||||
(name: string, value?: number) => void,
|
||||
cacheEnabled?: boolean // used for testing
|
||||
cacheEnabled?: boolean, // used for testing
|
||||
) {
|
||||
this.cacheEnabled =
|
||||
Boolean(redis) &&
|
||||
@@ -25,7 +26,7 @@ export class PromptService {
|
||||
const cachedPrompt = await this.getCachedPrompt(params);
|
||||
|
||||
this.incrementMetric(
|
||||
cachedPrompt ? Metrics.PromptCacheHit : Metrics.PromptCacheMiss
|
||||
cachedPrompt ? Metrics.PromptCacheHit : Metrics.PromptCacheMiss,
|
||||
);
|
||||
|
||||
if (cachedPrompt) {
|
||||
@@ -117,7 +118,7 @@ export class PromptService {
|
||||
}
|
||||
|
||||
public async lockCache(
|
||||
params: Pick<PromptParams, "projectId" | "promptName">
|
||||
params: Pick<PromptParams, "projectId" | "promptName">,
|
||||
): Promise<void> {
|
||||
if (!this.cacheEnabled) return;
|
||||
|
||||
@@ -133,7 +134,7 @@ export class PromptService {
|
||||
}
|
||||
|
||||
public async unlockCache(
|
||||
params: Pick<PromptParams, "projectId" | "promptName">
|
||||
params: Pick<PromptParams, "projectId" | "promptName">,
|
||||
): Promise<void> {
|
||||
if (!this.cacheEnabled) return;
|
||||
|
||||
@@ -149,7 +150,7 @@ export class PromptService {
|
||||
}
|
||||
|
||||
private async isCacheLocked(
|
||||
params: Pick<PromptParams, "projectId" | "promptName">
|
||||
params: Pick<PromptParams, "projectId" | "promptName">,
|
||||
): Promise<boolean> {
|
||||
const lockKey = this.getLockKey(params);
|
||||
|
||||
@@ -163,14 +164,14 @@ export class PromptService {
|
||||
}
|
||||
|
||||
private getLockKey(
|
||||
params: Pick<PromptParams, "projectId" | "promptName">
|
||||
params: Pick<PromptParams, "projectId" | "promptName">,
|
||||
): string {
|
||||
// Important to *pre*fix LOCK as otherwise it would be deleted by deleteKeysByPrefix
|
||||
return `LOCK:${this.getCacheKeyPrefix(params)}`;
|
||||
}
|
||||
|
||||
public async invalidateCache(
|
||||
params: Pick<PromptParams, "projectId" | "promptName">
|
||||
params: Pick<PromptParams, "projectId" | "promptName">,
|
||||
): Promise<void> {
|
||||
if (!this.cacheEnabled) return;
|
||||
|
||||
@@ -187,7 +188,7 @@ export class PromptService {
|
||||
await this.redis?.del([...(keys ?? []), keyIndexKey]);
|
||||
|
||||
this.logInfo(
|
||||
`Cache invalidated for prefix ${cacheKeyPrefix} in ${Date.now() - startTime}ms`
|
||||
`Cache invalidated for prefix ${cacheKeyPrefix} in ${Date.now() - startTime}ms`,
|
||||
);
|
||||
} catch (e) {
|
||||
this.logError("Error deleting keys for prefix", cacheKeyPrefix, e);
|
||||
@@ -203,23 +204,23 @@ export class PromptService {
|
||||
}
|
||||
|
||||
private getCacheKeyPrefix(
|
||||
params: Pick<PromptParams, "projectId" | "promptName">
|
||||
params: Pick<PromptParams, "projectId" | "promptName">,
|
||||
): string {
|
||||
return `prompt:${params.projectId}:${params.promptName}`;
|
||||
}
|
||||
|
||||
private getKeyIndexKey(
|
||||
params: Pick<PromptParams, "projectId" | "promptName">
|
||||
params: Pick<PromptParams, "projectId" | "promptName">,
|
||||
): string {
|
||||
return `prompt_key_index:${params.projectId}:${params.promptName}`;
|
||||
}
|
||||
|
||||
private logError(message: string, ...args: any[]) {
|
||||
console.error(`[PromptService] ${message}`, ...args);
|
||||
logger.error(`[PromptService] ${message}`, ...args);
|
||||
}
|
||||
|
||||
private logInfo(message: string, ...args: any[]) {
|
||||
console.log(`[PromptService] ${message}`, ...args);
|
||||
logger.info(`[PromptService] ${message}`, ...args);
|
||||
}
|
||||
|
||||
private incrementMetric(name: Metrics, value: number = 1) {
|
||||
|
||||
@@ -1,7 +1,12 @@
|
||||
import type { Readable } from "stream";
|
||||
import { GetObjectCommand, S3Client } from "@aws-sdk/client-s3";
|
||||
import {
|
||||
GetObjectCommand,
|
||||
PutObjectCommand,
|
||||
S3Client,
|
||||
} from "@aws-sdk/client-s3";
|
||||
import { Upload } from "@aws-sdk/lib-storage";
|
||||
import { getSignedUrl } from "@aws-sdk/s3-request-presigner";
|
||||
import { logger } from "../logger";
|
||||
|
||||
type UploadFile = {
|
||||
fileName: string;
|
||||
@@ -15,24 +20,30 @@ export class S3StorageService {
|
||||
private bucketName: string;
|
||||
|
||||
constructor(params: {
|
||||
accessKeyId: string;
|
||||
secretAccessKey: string;
|
||||
accessKeyId: string | undefined;
|
||||
secretAccessKey: string | undefined;
|
||||
bucketName: string;
|
||||
endpoint: string;
|
||||
region: string;
|
||||
endpoint: string | undefined;
|
||||
region: string | undefined;
|
||||
forcePathStyle: boolean;
|
||||
}) {
|
||||
const { accessKeyId, secretAccessKey, bucketName, endpoint, region } =
|
||||
params;
|
||||
// Use accessKeyId and secretAccessKey if provided or fallback to default credentials
|
||||
const { accessKeyId, secretAccessKey } = params;
|
||||
const credentials =
|
||||
accessKeyId !== undefined && secretAccessKey !== undefined
|
||||
? {
|
||||
accessKeyId,
|
||||
secretAccessKey,
|
||||
}
|
||||
: undefined;
|
||||
|
||||
this.client = new S3Client({
|
||||
credentials: {
|
||||
accessKeyId,
|
||||
secretAccessKey,
|
||||
},
|
||||
endpoint,
|
||||
region,
|
||||
credentials,
|
||||
endpoint: params.endpoint,
|
||||
region: params.region,
|
||||
forcePathStyle: params.forcePathStyle,
|
||||
});
|
||||
this.bucketName = bucketName;
|
||||
this.bucketName = params.bucketName;
|
||||
}
|
||||
|
||||
public async uploadFile({
|
||||
@@ -56,15 +67,45 @@ export class S3StorageService {
|
||||
|
||||
return { signedUrl };
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
|
||||
logger.error(`Failed to upload file to ${fileName}`, err);
|
||||
throw new Error("Failed to upload to S3 or generate signed URL");
|
||||
}
|
||||
}
|
||||
|
||||
public async uploadJson(path: string, body: Record<string, unknown>[]) {
|
||||
const putCommand = new PutObjectCommand({
|
||||
Bucket: this.bucketName,
|
||||
Key: path,
|
||||
Body: JSON.stringify(body),
|
||||
ContentType: "application/json",
|
||||
});
|
||||
|
||||
try {
|
||||
await this.client.send(putCommand);
|
||||
} catch (err) {
|
||||
logger.error(`Failed to upload JSON to S3 ${path}`, err);
|
||||
throw Error("Failed to upload JSON to S3");
|
||||
}
|
||||
}
|
||||
|
||||
public async download(path: string): Promise<string> {
|
||||
const getCommand = new GetObjectCommand({
|
||||
Bucket: this.bucketName,
|
||||
Key: path,
|
||||
});
|
||||
|
||||
try {
|
||||
const response = await this.client.send(getCommand);
|
||||
return (await response.Body?.transformToString()) ?? "";
|
||||
} catch (err) {
|
||||
logger.error(`Failed to download file from S3 ${path}`, err);
|
||||
throw Error("Failed to download file from S3");
|
||||
}
|
||||
}
|
||||
|
||||
private async getSignedUrl(
|
||||
fileName: string,
|
||||
ttlSeconds: number
|
||||
ttlSeconds: number,
|
||||
): Promise<string> {
|
||||
try {
|
||||
return await getSignedUrl(
|
||||
@@ -74,9 +115,10 @@ export class S3StorageService {
|
||||
Key: fileName,
|
||||
ResponseContentDisposition: `attachment; filename="${fileName}"`,
|
||||
}),
|
||||
{ expiresIn: ttlSeconds }
|
||||
{ expiresIn: ttlSeconds },
|
||||
);
|
||||
} catch (err) {
|
||||
logger.error(`Failed to generate presigned URL for ${fileName}`, err);
|
||||
throw Error("Failed to generate signed URL");
|
||||
}
|
||||
}
|
||||
|
||||
+5
-4
@@ -19,7 +19,6 @@ interface BatchExportSuccessTemplateProps {
|
||||
receiverEmail: string;
|
||||
downloadLink: string;
|
||||
batchExportName: string;
|
||||
expiresInHours: number;
|
||||
}
|
||||
|
||||
export const BatchExportSuccessEmailTemplate = ({
|
||||
@@ -27,7 +26,6 @@ export const BatchExportSuccessEmailTemplate = ({
|
||||
downloadLink,
|
||||
userName,
|
||||
batchExportName,
|
||||
expiresInHours,
|
||||
}: BatchExportSuccessTemplateProps) => {
|
||||
const previewText = `Download your data export from Langfuse`;
|
||||
|
||||
@@ -56,8 +54,11 @@ export const BatchExportSuccessEmailTemplate = ({
|
||||
<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>.
|
||||
download. The download link is valid for a few hours.
|
||||
</Text>
|
||||
<Text className="text-sm leading-6 text-black">
|
||||
Please note data exports do not reflect custom column ordering or
|
||||
visibility.
|
||||
</Text>
|
||||
<Section className="mb-4 mt-8 text-center">
|
||||
<Button
|
||||
|
||||
+3
-6
@@ -3,6 +3,7 @@ import { parseConnectionUrl } from "nodemailer/lib/shared/index.js";
|
||||
import { render } from "@react-email/render";
|
||||
|
||||
import { BatchExportSuccessEmailTemplate } from "./BatchExportSuccessEmailTemplate";
|
||||
import { logger } from "../../../logger";
|
||||
|
||||
type SendBatchExportSuccessParams = {
|
||||
env: Partial<
|
||||
@@ -12,7 +13,6 @@ type SendBatchExportSuccessParams = {
|
||||
downloadLink: string;
|
||||
userName: string;
|
||||
batchExportName: string;
|
||||
expiresInHours: number;
|
||||
};
|
||||
|
||||
export const sendBatchExportSuccessEmail = async ({
|
||||
@@ -21,11 +21,9 @@ export const sendBatchExportSuccessEmail = async ({
|
||||
downloadLink,
|
||||
userName,
|
||||
batchExportName,
|
||||
expiresInHours,
|
||||
}: SendBatchExportSuccessParams) => {
|
||||
if (!env.EMAIL_FROM_ADDRESS || !env.SMTP_CONNECTION_URL) {
|
||||
console.error("Missing environment variables for sending email.");
|
||||
|
||||
logger.error("Missing environment variables for sending email.");
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -37,7 +35,6 @@ export const sendBatchExportSuccessEmail = async ({
|
||||
downloadLink,
|
||||
userName,
|
||||
batchExportName,
|
||||
expiresInHours,
|
||||
})
|
||||
);
|
||||
|
||||
@@ -51,6 +48,6 @@ export const sendBatchExportSuccessEmail = async ({
|
||||
html: htmlTemplate,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
logger.error(error);
|
||||
}
|
||||
};
|
||||
|
||||
+4
-3
@@ -3,6 +3,7 @@ import { parseConnectionUrl } from "nodemailer/lib/shared/index.js";
|
||||
import { render } from "@react-email/render";
|
||||
|
||||
import MembershipInvitationTemplate from "./MembershipInvitationEmailTemplate";
|
||||
import { logger } from "../../../logger";
|
||||
|
||||
const langfuseUrls = {
|
||||
US: "https://us.cloud.langfuse.com",
|
||||
@@ -34,7 +35,7 @@ export const sendMembershipInvitationEmail = async ({
|
||||
orgName,
|
||||
}: SendMembershipInvitationParams) => {
|
||||
if (!env.EMAIL_FROM_ADDRESS || !env.SMTP_CONNECTION_URL) {
|
||||
console.error(
|
||||
logger.error(
|
||||
"Missing environment variables for sending membership invitation email."
|
||||
);
|
||||
return;
|
||||
@@ -49,7 +50,7 @@ export const sendMembershipInvitationEmail = async ({
|
||||
|
||||
const authUrl = getAuthURL();
|
||||
if (!authUrl) {
|
||||
console.error(
|
||||
logger.error(
|
||||
"Missing NEXTAUTH_URL or NEXT_PUBLIC_LANGFUSE_CLOUD_REGION environment variable."
|
||||
);
|
||||
return;
|
||||
@@ -77,6 +78,6 @@ export const sendMembershipInvitationEmail = async ({
|
||||
html: htmlTemplate,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
logger.error(error);
|
||||
}
|
||||
};
|
||||
|
||||
+4
-3
@@ -56,8 +56,9 @@ const ResetPasswordTemplate = ({ url }: ResetPasswordTemplateProps) => {
|
||||
</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.
|
||||
The link is valid for 10 minutes. 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>
|
||||
@@ -76,7 +77,7 @@ export async function sendResetPasswordVerificationRequest(
|
||||
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.`,
|
||||
text: `To reset your Langfuse password, please confirm your email:\n${url}\n\nThe link is valid for 10 minutes. If 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);
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import { Transform, type TransformCallback } from "stream";
|
||||
import { stringify } from "./stringify";
|
||||
|
||||
const DELIMITER = ",";
|
||||
|
||||
export function transformStreamToCsv(): Transform {
|
||||
let isFirstChunk = true;
|
||||
let headers: string[] = [];
|
||||
@@ -15,15 +17,19 @@ export function transformStreamToCsv(): Transform {
|
||||
if (isFirstChunk) {
|
||||
// Extract headers from the first object
|
||||
headers = Object.keys(row);
|
||||
this.push(headers.join(",") + "\n");
|
||||
this.push(headers.join(DELIMITER) + "\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);
|
||||
let str = stringify(field);
|
||||
|
||||
// escape and format fields that contain commas
|
||||
if (str.includes(",")) {
|
||||
str = `"${str.replace(/"/g, '""')}"`;
|
||||
}
|
||||
if (str.startsWith('"') && str.endsWith('"')) {
|
||||
return str;
|
||||
} else {
|
||||
@@ -31,7 +37,7 @@ export function transformStreamToCsv(): Transform {
|
||||
}
|
||||
});
|
||||
|
||||
this.push(csvRow.join(",") + "\n");
|
||||
this.push(csvRow.join(DELIMITER) + "\n");
|
||||
|
||||
callback();
|
||||
},
|
||||
|
||||
@@ -78,28 +78,28 @@ export const tracesTableCols: ColumnDefinition[] = [
|
||||
name: "Input Tokens",
|
||||
id: "inputTokens",
|
||||
type: "number",
|
||||
internal: 'tm."promptTokens"',
|
||||
internal: 'generation_metrics."promptTokens"',
|
||||
nullable: true,
|
||||
},
|
||||
{
|
||||
name: "Output Tokens",
|
||||
id: "outputTokens",
|
||||
type: "number",
|
||||
internal: 'tm."completionTokens"',
|
||||
internal: 'generation_metrics."completionTokens"',
|
||||
nullable: true,
|
||||
},
|
||||
{
|
||||
name: "Total Tokens",
|
||||
id: "totalTokens",
|
||||
type: "number",
|
||||
internal: 'tm."totalTokens"',
|
||||
internal: 'generation_metrics."totalTokens"',
|
||||
nullable: true,
|
||||
},
|
||||
{
|
||||
name: "Usage",
|
||||
id: "usage",
|
||||
type: "number",
|
||||
internal: 'tm."totalTokens"',
|
||||
internal: 'generation_metrics."totalTokens"',
|
||||
nullable: true,
|
||||
},
|
||||
|
||||
@@ -113,7 +113,7 @@ export const tracesTableCols: ColumnDefinition[] = [
|
||||
name: "Latency (s)",
|
||||
id: "latency",
|
||||
type: "number",
|
||||
internal: "tl.latency",
|
||||
internal: "observation_metrics.latency",
|
||||
},
|
||||
{
|
||||
name: "Input Cost ($)",
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import { type z } from "zod";
|
||||
import { singleFilter } from "./interfaces/filters";
|
||||
import { singleFilter, timeFilter } from "./interfaces/filters";
|
||||
|
||||
// to be sent to the server
|
||||
export type TimeFilter = z.infer<typeof timeFilter>;
|
||||
export type FilterCondition = z.infer<typeof singleFilter>;
|
||||
export type FilterState = FilterCondition[];
|
||||
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
// https://github.com/t3-oss/t3-env/blob/e7e21095e00a477e37608783defda5a6a99586d0/packages/core/src/index.ts#L228
|
||||
// unfortunately, we are not able to install t3-env in all our packaging as some rely on commonjs.
|
||||
export const removeEmptyEnvVariables = (runtimeEnv: any) => {
|
||||
for (const [key, value] of Object.entries(runtimeEnv)) {
|
||||
if (value === "") {
|
||||
delete runtimeEnv[key];
|
||||
}
|
||||
}
|
||||
return runtimeEnv;
|
||||
};
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user