Compare commits

...
94 Commits
Author SHA1 Message Date
Marc Klingen 86f44df5dc chore: release v2.48.0
CI/CD / lint (push) Waiting to run
CI/CD / test-docker-build (push) Waiting to run
CI/CD / tests-web (20) (push) Waiting to run
CI/CD / tests-worker (20) (push) Waiting to run
CI/CD / e2e-tests (push) Waiting to run
CI/CD / all-ci-passed (push) Blocked by required conditions
CI/CD / push-docker-image (push) Blocked by required conditions
release.yml / release (push) Waiting to run
2024-06-05 00:32:21 +02:00
marliessophieandGitHub 34da790d5e chore(scores): backfill manual scores with config_id (#2233) 2024-06-05 00:25:10 +02:00
c16d86f7e5 feat(scores): add annotation slider and score config settings for categorical annotation (#2157)
---------

Co-authored-by: Marc Klingen <git@marcklingen.com>
2024-06-05 00:06:05 +02:00
Marc KlingenandGitHub 1b2b8cc43a ci: fix flaky docker test build step by pinning turbo v1 (#2234) 2024-06-04 23:48:00 +02:00
Marc Klingen 3903790086 docs(api): refer to GET /traces endpoint to fetch large sessions with pagination 2024-06-04 20:22:23 +02:00
6da1280c43 perf(user-table): add timestamp filter to user table (#2231)
* feat: add timestamp filter to user table

* users table definition instead of reusing traces

* add filter to total as well to make table state consistent

---------

Co-authored-by: Marc Klingen <git@marcklingen.com>
2024-06-04 19:08:00 +02:00
Marc Klingen 85343bf26c chore: release v2.47.7
CI/CD / lint (push) Waiting to run
CI/CD / test-docker-build (push) Waiting to run
CI/CD / tests-web (20) (push) Waiting to run
CI/CD / tests-worker (20) (push) Waiting to run
CI/CD / e2e-tests (push) Waiting to run
CI/CD / all-ci-passed (push) Blocked by required conditions
CI/CD / push-docker-image (push) Blocked by required conditions
release.yml / release (push) Waiting to run
2024-06-04 16:56:48 +02:00
Richard KrümmelandGitHub e3b4dc1928 feat(api): add sessionId filter for traces endpoint (#2227) 2024-06-04 14:38:51 +00:00
Marc KlingenandGitHub a4939bacd8 perf: add index on scores.name (#2228) 2024-06-04 15:35:21 +02:00
Hassieb Pakzad 4441942cf5 chore: release v2.47.6
CI/CD / lint (push) Waiting to run
CI/CD / test-docker-build (push) Waiting to run
CI/CD / tests-web (20) (push) Waiting to run
CI/CD / tests-worker (20) (push) Waiting to run
CI/CD / e2e-tests (push) Waiting to run
CI/CD / all-ci-passed (push) Blocked by required conditions
CI/CD / push-docker-image (push) Blocked by required conditions
release.yml / release (push) Waiting to run
2024-06-04 14:02:23 +02:00
Hassieb PakzadandGitHub 6597b02c09 perf: boost sessions table performance (#2225) 2024-06-04 13:42:09 +02:00
Marc KlingenandGitHub 0df3ad27dc perf: add index on dataset_items.source_trace_id (#2220) 2024-06-04 01:57:19 +02:00
Marc KlingenandGitHub 696d458a27 fix(cloud): handle posthog settings rbac correctly in frontend (#2223) 2024-06-03 23:35:49 +00:00
Marc KlingenandGitHub fd18ab605d feat(api): add count and cost to daily metrics api (#2219) 2024-06-03 22:41:01 +02:00
Marc Klingen d47d14eecd chore: release v2.47.5
CI/CD / lint (push) Waiting to run
CI/CD / test-docker-build (push) Waiting to run
CI/CD / tests-web (20) (push) Waiting to run
CI/CD / tests-worker (20) (push) Waiting to run
CI/CD / e2e-tests (push) Waiting to run
CI/CD / all-ci-passed (push) Blocked by required conditions
CI/CD / push-docker-image (push) Blocked by required conditions
release.yml / release (push) Waiting to run
2024-06-03 19:56:18 +02:00
Richard KrümmelandGitHub 24a81e1589 fix: update validation schema for prompt tags (#2217) 2024-06-03 18:51:39 +02:00
Max Deichmann ac0c13f3e5 chore: release v2.47.4
CI/CD / lint (push) Waiting to run
CI/CD / test-docker-build (push) Waiting to run
CI/CD / tests-web (20) (push) Waiting to run
CI/CD / tests-worker (20) (push) Waiting to run
CI/CD / e2e-tests (push) Waiting to run
CI/CD / all-ci-passed (push) Blocked by required conditions
CI/CD / push-docker-image (push) Blocked by required conditions
release.yml / release (push) Waiting to run
2024-06-03 18:07:58 +02:00
Marc KlingenandGitHub a7b90fa32c feat(ui): preview model cost per 1k, 100k, and 1M units (#2216) 2024-06-03 13:45:10 +00:00
Marc Klingen 9eaf2a2e0f docs(api): add tags to prompt api response type 2024-06-03 14:24:48 +02:00
Max DeichmannandGitHub 61e3cc33bf fix: do not log expected eval errors as sentry errors (#2213) 2024-06-03 12:22:41 +00:00
2be198400d feat(api): add prompt tags via API (#2180)
---------

Co-authored-by: Marc Klingen <git@marcklingen.com>
2024-06-03 12:10:15 +00:00
Max Deichmann 830df682f9 chore: release v2.47.3
CI/CD / lint (push) Waiting to run
CI/CD / test-docker-build (push) Waiting to run
CI/CD / tests-web (20) (push) Waiting to run
CI/CD / tests-worker (20) (push) Waiting to run
CI/CD / e2e-tests (push) Waiting to run
CI/CD / all-ci-passed (push) Blocked by required conditions
CI/CD / push-docker-image (push) Blocked by required conditions
release.yml / release (push) Waiting to run
2024-06-03 12:19:27 +02:00
Max DeichmannandGitHub 2a420b2e90 chore: reduce sentry sampling (#2212) 2024-06-03 11:06:56 +02:00
Marc Klingen 53127c34ab chore: add posthog ui_host 2024-06-02 21:18:35 +02:00
Marc Klingen 9c57ff4853 chore: reduce cloud telemetry to 4 times a day 2024-06-02 20:18:55 +02:00
Marc Klingen a71176eda4 chore: disable posthog autocapture 2024-06-02 20:12:54 +02:00
Marc KlingenandGitHub f59a85f412 fix(ui): full prompt name on hover when saving as new version from playground (#2208) 2024-06-02 14:31:01 +00:00
Marc Klingen ec59436522 chore: release v2.47.2
CI/CD / lint (push) Waiting to run
CI/CD / test-docker-build (push) Waiting to run
CI/CD / tests-web (20) (push) Waiting to run
CI/CD / tests-worker (20) (push) Waiting to run
CI/CD / e2e-tests (push) Waiting to run
CI/CD / all-ci-passed (push) Blocked by required conditions
CI/CD / push-docker-image (push) Blocked by required conditions
release.yml / release (push) Waiting to run
2024-06-02 01:37:09 +02:00
Marc KlingenandGitHub 53a3d58a2f fix(ui): do not run llm api key query when it is not available (#2205) 2024-06-02 01:35:41 +02:00
Marc KlingenandGitHub c1162a3a8b chore: update support page to include GH discussions (#2204) 2024-06-01 15:59:34 +00:00
Marc Klingen 43d8c4ec99 chore: release v2.47.1
CI/CD / lint (push) Waiting to run
CI/CD / test-docker-build (push) Waiting to run
CI/CD / tests-web (20) (push) Waiting to run
CI/CD / tests-worker (20) (push) Waiting to run
CI/CD / e2e-tests (push) Waiting to run
CI/CD / all-ci-passed (push) Blocked by required conditions
CI/CD / push-docker-image (push) Blocked by required conditions
release.yml / release (push) Waiting to run
2024-05-31 16:15:00 +02:00
Marc KlingenandGitHub 3a88315ac5 perf: add date range filter to scores, default to 7 days across core tables (#2198) 2024-05-31 14:12:16 +00:00
Marc KlingenandGitHub e06b96a5ee fix(auth): add custom timeout to sso config query (#2192) 2024-05-31 10:21:03 +00:00
Marc KlingenandGitHub 307f54ef8a chore(cloud): update usage & billing copy (#2193) 2024-05-30 23:38:43 +00:00
Marc KlingenandGitHub 196dc1bf00 feat: support claude-3 pricing on google vertex (#2191) 2024-05-30 23:36:11 +02:00
Marc Klingen ee2c0fb27b chore: release v2.47.0
CI/CD / lint (push) Waiting to run
CI/CD / test-docker-build (push) Waiting to run
CI/CD / tests-web (20) (push) Waiting to run
CI/CD / tests-worker (20) (push) Waiting to run
CI/CD / e2e-tests (push) Waiting to run
CI/CD / all-ci-passed (push) Blocked by required conditions
CI/CD / push-docker-image (push) Blocked by required conditions
release.yml / release (push) Waiting to run
2024-05-30 19:26:01 +02:00
Marc KlingenandGitHub 27623de650 fix(ui): do not wrap tags within tables (#2189) 2024-05-30 14:05:39 +00:00
Marc Klingen 61c8f6c269 docs update integrations in readme 2024-05-30 15:40:02 +02:00
Marc KlingenandGitHub cc9e21ccb3 perf(datasets): lateral join on runsByDatasetId, remove unused include on runitemsByRunIdOrItemId (#2183) 2024-05-29 23:59:48 +00:00
078ceb7ff1 feat(ui): add client-side error notifications (#2166)
---------

Co-authored-by: Marc Klingen <git@marcklingen.com>
2024-05-29 14:41:41 +00:00
Marc KlingenandGitHub 9436c33025 fix(ui): json views should use resolve theme instead of the literal theme which can be system (#2168) 2024-05-29 14:14:38 +00:00
Marc KlingenandGitHub 88b3e1bea6 chore(cloud): remove posthog ingestion metrics as we moved to airbyte (#2179) 2024-05-29 13:22:02 +00:00
Richard KrümmelandGitHub da042d8284 fix(ui): allow for long model names in select filter (#2173) 2024-05-29 13:42:51 +02:00
Max DeichmannandGitHub 50264701f6 chore: reduce logging (#2178)
push
2024-05-29 09:14:33 +00:00
Marc KlingenandGitHub 3ee8a2e026 perf(db): add cursors and indicies on cursors (#2174) 2024-05-29 00:06:40 +02:00
Marc KlingenandGitHub ed04357815 feat(ui): add linter to json inputs (#2171) 2024-05-28 14:40:13 +00:00
Max Deichmann fa26275634 chore: release v2.46.0
CI/CD / lint (push) Waiting to run
CI/CD / test-docker-build (push) Waiting to run
CI/CD / tests-web (20) (push) Waiting to run
CI/CD / tests-worker (20) (push) Waiting to run
CI/CD / e2e-tests (push) Waiting to run
CI/CD / all-ci-passed (push) Blocked by required conditions
CI/CD / push-docker-image (push) Blocked by required conditions
release.yml / release (push) Waiting to run
2024-05-28 16:04:42 +02:00
Max DeichmannandGitHub 7587c75459 chore: do not log io (#2164) 2024-05-28 11:27:12 +00:00
ClemoandGitHub a956d0115c docs: update 2024-05-27 19:14:33 -07:00
Marc KlingenandGitHub cb82835489 fix(ui): overflow behavior of tag manager (#2156) 2024-05-27 17:13:32 +02:00
Hassieb PakzadandGitHub d1c1e60eca feat: use custom keys for playground (#2139) 2024-05-27 14:29:05 +02:00
Marc Klingen 81b612273a chore: release v2.45.1
CI/CD / lint (push) Waiting to run
CI/CD / test-docker-build (push) Waiting to run
CI/CD / tests-web (20) (push) Waiting to run
CI/CD / tests-worker (20) (push) Waiting to run
CI/CD / e2e-tests (push) Waiting to run
CI/CD / all-ci-passed (push) Blocked by required conditions
CI/CD / push-docker-image (push) Blocked by required conditions
release.yml / release (push) Waiting to run
2024-05-27 13:10:28 +02:00
Marc KlingenandGitHub 2c1abf2f6e feat(auth): add AWS Cognito provider (#2151)
Fixes #2107
2024-05-27 10:21:04 +00:00
Marc KlingenandGitHub db1a662f7f fix(ui): default hide version and release on trace table (#2150) 2024-05-27 10:06:22 +00:00
Richard KrümmelandGitHub 1b0e3de709 fix(ui): table header tooltips (#2148) 2024-05-27 11:20:48 +02:00
Richard KrümmelandGitHub be8aec2f8f feat(theme): add posthog (#2147) 2024-05-27 11:20:11 +02:00
adc01014c0 fix(scores): table source filter (#2144)
Co-authored-by: Marc Klingen <git@marcklingen.com>
2024-05-24 19:29:39 +00:00
Marc KlingenandGitHub 215af995fb perf: add indicies to foreign keys on job executions table (#2143) 2024-05-24 21:12:52 +02:00
marliessophieandGitHub 74ff237145 feat(scores): rename source REVIEW to ANNOTATION (#2141)
* feat(scores): add 'ANNOTATION' to `ScoreSource`

* feat: adjust seeder

* feat: adjust trpc router

* add: backfill scores source `REVIEW` to `ANNOTATION`

* add: drop `REVIEW` from scores source enum

* update yml

* refactor: rename trpc procedures
2024-05-24 19:58:30 +02:00
porter-deployment-app[bot]GitHubporter-deployment-app[bot] <87230664+porter-deployment-app[bot]@users.noreply.github.com>
5c2ae0e678 Enable Porter Application worker (#2138)
Create porter_stack_worker.yml file

Co-authored-by: porter-deployment-app[bot] <87230664+porter-deployment-app[bot]@users.noreply.github.com>
2024-05-24 14:42:46 +00:00
marliessophieandGitHub a6d8ce8c96 feat(score_config): add table via migration (#2126)
* feat: add author user id column to scores table

* feat: add author user id to scores created via trpc router

* feat: add author user id to scores created in seeder

* fixup

* add: index to author user id on scores

* feat: annotation config schema

* add: migration

* add: migrations in order

* fixup after merge

* fix: replicate to worker/types

* fix: address feedback

* rn: to scoreConfig

* push

* refactor: rn `ScoreDataType` to `ScoreConfigDataType`

* fix

* fix: scores server test

* context: order of returned data can vary based on how data is retrieved. Likely re-ordered due to new index on configId in scores table

* fix: re-generate types for correct alphabetical order

* fix: remodel migrations to match new schema

* feat: rm backfill migration for data_type on scores

* make data_type column optional on scores

* push: regenerate types

* push

* feat: make dataType on scores required again

* rename: scores add data type migration

* re-run migrations

* push
2024-05-23 19:26:16 +02:00
Marc Klingen fa4ba016b5 chore: release v2.45.0
CI/CD / lint (push) Waiting to run
CI/CD / test-docker-build (push) Waiting to run
CI/CD / tests-web (20) (push) Waiting to run
CI/CD / tests-worker (20) (push) Waiting to run
CI/CD / e2e-tests (push) Waiting to run
CI/CD / all-ci-passed (push) Blocked by required conditions
CI/CD / push-docker-image (push) Blocked by required conditions
release.yml / release (push) Waiting to run
2024-05-23 15:49:07 +02:00
Hassieb PakzadandGitHub 9b7e0abfbc feat: adds links in and out of playground (#2123) 2024-05-23 11:35:39 +00:00
dependabot[bot]andGitHub 5556772388 chore(deps): bump the patches group across 1 directory with 14 updates (#2120) 2024-05-23 09:46:10 +02:00
Richard KrümmelandGitHub 68e0270051 fix(ui): improve orange hue (#2128)
Improve dark:muted-orange hue
2024-05-22 23:12:34 +02:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
5bd7d879ad chore(deps): bump @t3-oss/env-nextjs from 0.8.0 to 0.10.1 (#1962)
Bumps [@t3-oss/env-nextjs](https://github.com/t3-oss/t3-env/tree/HEAD/packages/nextjs) from 0.8.0 to 0.10.1.
- [Release notes](https://github.com/t3-oss/t3-env/releases)
- [Changelog](https://github.com/t3-oss/t3-env/blob/main/packages/nextjs/CHANGELOG.md)
- [Commits](https://github.com/t3-oss/t3-env/commits/@t3-oss/env-nextjs@0.10.1/packages/nextjs)

---
updated-dependencies:
- dependency-name: "@t3-oss/env-nextjs"
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-05-22 17:17:57 +00:00
Richard KrümmelandGitHub 87d34c05d9 feat(ui): add dark mode improvements (#2127) 2024-05-22 18:26:21 +02:00
ae27144346 feat(scores): add nullable authorUserId to scores (#2121)
* feat: add author user id column to scores table

* feat: add author user id to scores created via trpc router

* feat: add author user id to scores created in seeder

* fixup

* add: index to author user id on scores

* replicate to worker/types

---------

Co-authored-by: Marc Klingen <git@marcklingen.com>
2024-05-22 15:47:56 +02:00
marliessophieandGitHub f5101083a2 fix: prompt metrics router add early return if no valid promptIds array input (#2125)
fix: metric router add early return if no valid promptIds array input
2024-05-22 12:16:00 +00:00
124c9e8107 feat: move scores table on trace and observation preview into seperate tab (#2078)
* feat: move scores on trace and observation preview to different tab

* refactor: extract current table logic into `ScoresTablePreview`

* fix: rm unused footer

* refactor: scores preview table

* fix: tab state needs to be reset upon changing currentObservationId

* refactor: reorder for readability

* refactor: rm column visibility as not needed

* fix: rm pagination

* refactor: rn details to preview

* feat: restyle tabs

* feat: reuse scores table

* feat: hide columns

* feat: add source column to scores table

* feat: add user name and avatar to scores table

* adjust seeder

* feat: show score value in preview tab

* fix: don't show comment if empty string

* feat: add score source to preview UI

* feat: add tooltip

* feat: don't use user image and name in scores table

* feat: hide userId column in trace and observation preview

* adjust seeder to reuse image in bucket

* fix: pass table name to save column visibility in local storage

* revert: changes to seed file

* feat: misc UI improvements

* fixup: first draft add `is not` filter to table

* feat: show all nested scores for trace scores

* fix: misc UI

* ui: use `primary-accent` color

* adjust padding/margins

* adjust paddings

---------

Co-authored-by: Marc Klingen <git@marcklingen.com>
2024-05-22 09:57:24 +00:00
Marc KlingenandGitHub fabbb75e40 fix(api): dedupe and sort trace tags (#2122) 2024-05-22 09:00:34 +00:00
Richard KrümmelandGitHub 857507195b feat(ui): add dark mode (#1998) 2024-05-22 10:52:53 +02:00
Marc KlingenandGitHub cd7864cad4 fix(ui): hide playground button if not available (#2118) 2024-05-21 14:43:07 +02:00
Marc KlingenandGitHub cd9fd110e4 perf(api): optimize get traces api & fix totalCost (#2117) 2024-05-21 14:40:21 +02:00
Marc Klingen 3c35e5e9c5 chore: release v2.44.0
CI/CD / lint (push) Waiting to run
CI/CD / test-docker-build (push) Waiting to run
CI/CD / tests-web (20) (push) Waiting to run
CI/CD / tests-worker (20) (push) Waiting to run
CI/CD / e2e-tests (push) Waiting to run
CI/CD / all-ci-passed (push) Blocked by required conditions
CI/CD / push-docker-image (push) Blocked by required conditions
release.yml / release (push) Waiting to run
2024-05-21 12:32:58 +02:00
Marc KlingenandGitHub fe75e4c5f1 refactor: add time to first token to observations_view (#2092) 2024-05-21 12:30:27 +02:00
6435ce1a79 fix(docker-compose): Add healthcheck for db service (#2113)
---------

Co-authored-by: Marc Klingen <git@marcklingen.com>
2024-05-21 10:55:15 +02:00
marliessophieandGitHub d270143f0e fix: rename careers file to match hackernews post (#2115) 2024-05-21 09:54:21 +02:00
Marc Klingen 4041550d5e fix: link of help link of eval log 2024-05-21 00:35:46 +02:00
Max DeichmannandGitHub fc5ff52d1e docs: careers file extension (#2105) 2024-05-20 19:19:17 +00:00
ClemoandGitHub 6f6bef722b docs: create careers 2024-05-20 12:11:25 -07:00
Marc KlingenandGitHub 496ac68008 docs: Update .env.prod.example (#2103) 2024-05-20 17:12:36 +02:00
Marc KlingenandGitHub 595f9f1e5d fix(ui): parse chatml messages where content is an array (#2100) 2024-05-20 12:53:00 +00:00
Marc Klingen 70bca50ad0 fix(ui): spacing of user menu 2024-05-17 23:56:15 +02:00
Marc Klingen 99791458b4 fix(ui): padding of second-level menu 2024-05-17 23:54:33 +02:00
Marc KlingenandGitHub 60dc78b498 feat(ui): less padding in main menu (#2095) 2024-05-17 21:36:00 +00:00
Marc KlingenandGitHub 43e5887361 fix(api): merge tags when upserting a trace (#2091) 2024-05-17 18:43:10 +02:00
marliessophieandGitHub c009d9602d revert: fix(cloud) remove custom cookie management" (#2090)
Revert "fix(cloud): remove custom cookie management (#2087)"

This reverts commit 976f743841.
2024-05-17 14:32:54 +00:00
Max DeichmannandGitHub 830ebf6fff fix: set cache control explicitly (#2088) 2024-05-17 16:08:19 +02:00
Marc KlingenandGitHub 976f743841 fix(cloud): remove custom cookie management (#2087) 2024-05-17 14:12:11 +02:00
Max DeichmannandGitHub 89c1e46c55 chore: adjust sentry (#2086) 2024-05-17 10:21:51 +00:00
Marc Klingen 832e5d85ff fix(ee): capture exceptions when loading dynamic sso providers 2024-05-17 12:14:14 +02:00
Marc Klingen 9b3a1f3af3 feat(cloud): include data region in project invite emails 2024-05-17 12:08:40 +02:00
Marc Klingen c2515f7e6a feat(auth): improve auth error messages and logging 2024-05-17 11:37:45 +02:00
251 changed files with 6179 additions and 2142 deletions
+1 -1
View File
@@ -47,5 +47,5 @@ REDIS_PORT=6379
REDIS_AUTH="myredissecret"
LANGFUSE_WORKER_PASSWORD=mybasicauthsecret
# openssl rand -base64 32 used only here
# openssl rand -hex 32 used only here
ENCRYPTION_KEY=6c16874e5c0f0cc74ddec00425fa99fbe9ffbe412b7d5d906a4b00005df91403
+6 -1
View File
@@ -58,6 +58,11 @@ LANGFUSE_CSP_ENFORCE_HTTPS="true"
# AUTH_AUTH0_CLIENT_SECRET=
# AUTH_AUTH0_ISSUER=
# AUTH_AUTH0_ALLOW_ACCOUNT_LINKING=false
# AUTH_COGNITO_CLIENT_ID=
# AUTH_COGNITO_CLIENT_SECRET=
# AUTH_COGNITO_ISSUER=
# AUTH_COGNITO_ALLOW_ACCOUNT_LINKING=false
# Transactional email, optional
# Defines the email address to use as the from address.
@@ -96,7 +101,7 @@ LANGFUSE_CSP_ENFORCE_HTTPS="true"
### START Langfuse Cloud Config
# Used for Langfuse Cloud deployments
# Not recommended for self-hosted deployments as these are NOT COVERED BY SEMVER
# Not recommended for self-hosted deployments as these are NOT COVERED BY SEMANTIC VERSIONING
# NEXT_PUBLIC_LANGFUSE_CLOUD_REGION="US"
# NEXTAUTH_COOKIE_DOMAIN=".langfuse.com"
+29
View File
@@ -0,0 +1,29 @@
"on":
push:
branches:
- main
name: Deploy to worker
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: "3959"
PORTER_DEPLOYMENT_TARGET_ID: d2bb23e4-1d77-48f2-a113-383d076959b3
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_3959 }}
+68
View File
@@ -290,6 +290,74 @@ When a new release is tagged on the `main` branch (excluding prereleases), it tr
1. The Docker image is published to GitHub Packages with the version number and `latest` tag.
2. The deployment is carried out on Langfuse Cloud. This is done by force pushing the `main` branch to the `production` branch during every release, using the [`release.yml`](.github/workflows/release.yml) GitHub Action.
## Theming
At Langfuse, we utilize CSS variables to manage our theme settings across the platform.
Our approach leverages separate CSS variables for backgrounds (--background) and foregrounds (--foreground), fully adhering to the [shadcn/ui](https://ui.shadcn.com/docs/theming) color conventions. The background suffix can be omitted if the variable is used for the background color of the component. We recommend using HSL values for these colors to enhance consistency and customization. There is no need to manually handle dark mode styling with "dark:" prefixes, as next-themes automatically manages the theme switching.
Given the following CSS variables:
```
--primary: 222.2 47.4% 11.2%; // e.g. background-color
--primary-foreground: 210 40% 98%; // e.g. text-color
```
The background color of the following component will be `hsl(var(--primary))` and the foreground color will be `hsl(var(--primary-foreground))`.
```
<div class="bg-primary text-primary-foreground">Hello</div>
```
### Color Variables
| Variable | Description | Examples |
| ------------------------ | ------------------------------------------------------------------ | -------------------------------- |
| --background | Background color | Default background color of body |
| --foreground | Foreground color | Default text color of body |
| --muted | Muted background color | TabsList, Skeleton and Switch |
| --muted-foreground | Muted foreground color | |
| --popover | Popover background color | DropdownMenu, HoverCard, Popover |
| --popover-foreground | Popover foreground color | |
| --card | Card background color | Card |
| --card-foreground | Card foreground color | |
| --border | Border color | Default border color |
| --input | Input field border color | Input, Select, Textarea |
| --primary | Primary button background colors | Button variant="primary" |
| --primary-foreground | Primary button foreground color | |
| --secondary | Secondary button background color | Button variant="secondary" |
| --secondary-foreground | Secondary button foreground color | |
| --accent | Used for accents such as hover effects | DropdownMenuItem, SelectItem |
| --accent-foreground | Used for texts on hover effects | DropdownMenuItem, SelectItem |
| --destructive | Destructive action color for background | Button variant="destructive" |
| --destructive-foreground | Destructive action color for text | |
| --ring | Focus ring color | MultiSelect |
| --primary-accent | Primary accent color used for branding | Layout |
| --hover-primary-accent | Primary accent color used for hover effects for links | SignIn and AuthCloudRegionSwitch |
| --muted-green | Muted green for Event label | ObservationTree |
| --muted-orange | Muted orange for Generation label | ObservationTree |
| --muted-blue | Muted blue for Span label | ObservationTree |
| --muted-gray | Muted gray for disabled status badges | StatusBadge |
| --accent-light-green | Light green accent for background of output and assistant messages | IOPreview, Generations, Traces |
| --accent-dark-green | Dark green accent for border of output and assistant messages | CodeJsonViewer and IOPReview |
| --light-red | Light red for error background | level-color and StatusBadge |
| --dark-red | Dark red for error text and error badge dot color | level-color and ErrorPage |
| --light-yellow | Light yellow for warning background | LevelColor |
| --dark-yellow | Dark yellow for warning text | LevelColor |
| --light-green | Light green for success status badge background | StatusBadge |
| --dark-green | Dark green for success status badge text and dot | StatusBadge |
| --light-blue | Light blue for background of Staging label | LangfuseLogo |
| --dark-blue | Dark blue for text and border of Staging label | LangfuseLogo |
| --accent-light-blue | Light blue accent for table link hover effect | TableLink |
| --accent-dark-blue | Dark blue accent for table link text | TableLink |
### Adding New Colors
1. Global Definitions: Add new CSS variable definitions in the global.css file.
2. Tailwind Configuration: Reflect these new colors in the tailwind.config.js to maintain alignment with Tailwind's utility classes.
By following these guidelines, you can ensure that any contributions to our theme are consistent, maintainable, and aligned with our design system.
## Using secrets stored in 1Password
When applying changes to non-local environments, you may need to use secrets stored in 1Password. We use the 1Password CLI for this purpose.
+18 -14
View File
@@ -106,22 +106,26 @@ See the [→ Quickstart](https://langfuse.com/docs/get-started) to integrate Lan
### Integrations
| Integration | Supports | Description |
| ---------------------------------------------------------------- | ------------- | ----------------------------------------------------------- |
| [**SDK** - _recommended_](https://langfuse.com/docs/sdk) | Python, JS/TS | Manual instrumentation using the SDKs for full flexibility. |
| [OpenAI SDK](https://langfuse.com/docs/integrations/openai) | Python, JS/TS | Automated instrumentation of OpenAI SDK. |
| [Langchain](https://langfuse.com/docs/integrations/langchain) | Python, JS/TS | Instrumentation via Langchain callbacks. |
| [LlamaIndex](https://langfuse.com/docs/integrations/llama-index) | Python | Automated instrumentation via LlamaIndex callback system. |
| [API](https://langfuse.com/docs/api) | | Directly call the public API. OpenAPI spec available. |
| 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. |
External projects/packages that integrate with Langfuse:
Packages that integrate with Langfuse:
| Name | Description |
| --------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------ |
| [LiteLLM](https://langfuse.com/docs/integrations/litellm) | Use any LLM as a drop in replacement for GPT. Use Azure, OpenAI, Cohere, Anthropic, Ollama, VLLM, Sagemaker, HuggingFace, Replicate (100+ LLMs). |
| [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. |
| [Superagent](https://langfuse.com/docs/integrations/superagent) | Open Source AI Assistant Framework & API for prototyping and deployment of agents. |
| 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. |
| [Superagent](/docs/integrations/superagent) | Open Source AI Assistant Framework & API for prototyping and deployment of agents. |
## Questions and feedback
+14
View File
@@ -0,0 +1,14 @@
_We are Hiring_
Join us in scaling Langfuse in Berlin, Germany. We are an open source company, we hire in person, we are only hiring technical talent.
_Open Roles_
- Backend Engineer, 70-130k EUR, 0.25-0.75% Equity, https://www.ycombinator.com/companies/langfuse/jobs/mnrdwla-backend-engineer
- Product Engineer, 70-130k EUR, 0.25-0.75% Equity, https://www.ycombinator.com/companies/langfuse/jobs/aAvmoFB-product-engineer
- Developer Advocate, 60-110k EUR, 0.25-0.5% Equity, https://www.ycombinator.com/companies/langfuse/jobs/uHysbKH-developer-advocate-devrel
_More Info_
- https://langfuse.com/careers
- https://langfuse.com/docs
- https://langfuse.com/changelog
+7 -1
View File
@@ -4,7 +4,8 @@ services:
langfuse-server:
image: ghcr.io/langfuse/langfuse:latest
depends_on:
- db
db:
condition: service_healthy
ports:
- "3000:3000"
environment:
@@ -18,6 +19,11 @@ services:
db:
image: postgres
restart: always
healthcheck:
test: ["CMD-SHELL", "pg_isready -U postgres"]
interval: 3s
timeout: 3s
retries: 10
environment:
- POSTGRES_USER=postgres
- POSTGRES_PASSWORD=postgres
+31 -2
View File
@@ -2,10 +2,11 @@ import { type Provider } from "next-auth/providers/index";
import GoogleProvider from "next-auth/providers/google";
import GitHubProvider from "next-auth/providers/github";
import OktaProvider from "next-auth/providers/okta";
import CognitoProvider from "next-auth/providers/cognito";
import Auth0Provider from "next-auth/providers/auth0";
import AzureADProvider from "next-auth/providers/azure-ad";
import { isEeAvailable } from "..";
import { prisma } from "@langfuse/shared/src/db";
import { type SsoConfig, prisma } from "@langfuse/shared/src/db";
import { encrypt, decrypt } from "@langfuse/shared/encryption";
import { SsoProviderSchema } from "./types";
import { type NextApiRequest, type NextApiResponse } from "next";
@@ -25,14 +26,36 @@ let cachedSsoConfigs: {
async function getSsoConfigs(): Promise<SsoProviderSchema[]> {
if (!isEeAvailable) return [];
const CACHE_TTL = 60 * 1000; // 1 minute
const DB_MAX_WAIT = 2000; // 2 seconds
const DB_TIMEOUT = 3000; // 3 seconds
// Set/refresh the cache if it's empty or expired
if (
cachedSsoConfigs === null ||
Date.now() - cachedSsoConfigs.timestamp > CACHE_TTL
) {
// findMany with custom timeout via $transaction
let dbConfigs: SsoConfig[] = [];
try {
dbConfigs = await prisma.$transaction(
async (prisma) => prisma.ssoConfig.findMany(),
{
maxWait: DB_MAX_WAIT,
timeout: DB_TIMEOUT,
}
);
} catch (e) {
// cache empty array to prevent repeated DB calls on error
cachedSsoConfigs = {
data: [],
timestamp: Date.now(),
};
// caught and logged in the caller
throw e;
}
// transform into zod object
const dbConfigs = await prisma.ssoConfig.findMany();
const parsedSsoConfigs = dbConfigs
.map((v) => {
try {
@@ -144,6 +167,12 @@ const dbToNextAuthProvider = (provider: SsoProviderSchema): Provider | null => {
...provider.authConfig,
clientSecret: decrypt(provider.authConfig.clientSecret),
});
else if (provider.authProvider === "cognito")
return CognitoProvider({
id: getAuthProviderIdForSsoConfig(provider), // use the domain as the provider id as we use domain-specific credentials
...provider.authConfig,
clientSecret: decrypt(provider.authConfig.clientSecret),
});
else {
// Type check to ensure we handle all providers
// eslint-disable-next-line no-unused-vars
+14
View File
@@ -64,11 +64,24 @@ export const AzureAdProviderSchema = base.extend({
.nullish(),
});
export const CognitoProviderSchema = base.extend({
authProvider: z.literal("cognito"),
authConfig: z
.object({
clientId: z.string(),
clientSecret: z.string(),
issuer: z.string(),
allowDangerousEmailAccountLinking: z.boolean().optional().default(false),
})
.nullish(),
});
export type GoogleProviderSchema = z.infer<typeof GoogleProviderSchema>;
export type GithubProviderSchema = z.infer<typeof GithubProviderSchema>;
export type Auth0ProviderSchema = z.infer<typeof Auth0ProviderSchema>;
export type OktaProviderSchema = z.infer<typeof OktaProviderSchema>;
export type AzureAdProviderSchema = z.infer<typeof AzureAdProviderSchema>;
export type CognitoProviderSchema = z.infer<typeof CognitoProviderSchema>;
export const SsoProviderSchema = z.discriminatedUnion("authProvider", [
GoogleProviderSchema,
@@ -76,6 +89,7 @@ export const SsoProviderSchema = z.discriminatedUnion("authProvider", [
Auth0ProviderSchema,
OktaProviderSchema,
AzureAdProviderSchema,
CognitoProviderSchema,
]);
export type SsoProviderSchema = z.infer<typeof SsoProviderSchema>;
+1 -1
View File
@@ -209,8 +209,8 @@ types:
- ARCHIVED
ScoreSource:
enum:
- ANNOTATION
- API
- REVIEW
- EVAL
errors:
+17 -4
View File
@@ -48,12 +48,25 @@ types:
date: date
countTraces: integer
countObservations: integer
totalCost: double
totalCost:
type: double
docs: Total model cost in USD
usage: list<UsageByModel>
UsageByModel:
docs: Daily usage of a given model. Usage corresponds to the unit set for the specific model (e.g. tokens).
properties:
model: optional<string>
inputUsage: integer
outputUsage: integer
totalUsage: integer
inputUsage:
type: integer
docs: Total number of generation input units (e.g. tokens)
outputUsage:
type: integer
docs: Total number of generation output units (e.g. tokens)
totalUsage:
type: integer
docs: Total number of generation total units (e.g. tokens)
countTraces: integer
countObservations: integer
totalCost:
type: double
docs: Total model cost in USD
+18 -3
View File
@@ -70,14 +70,24 @@ types:
name: string
prompt: list<ChatMessage>
config: optional<unknown>
labels: optional<list<string>>
labels:
type: optional<list<string>>
docs: List of deployment labels of this prompt version.
tags:
type: optional<list<string>>
docs: List of tags to apply to all versions of this prompt.
CreateTextPromptRequest:
properties:
name: string
prompt: string
config: optional<unknown>
labels: optional<list<string>>
labels:
type: optional<list<string>>
docs: List of deployment labels of this prompt version.
tags:
type: optional<list<string>>
docs: List of tags to apply to all versions of this prompt.
Prompt:
union:
@@ -89,7 +99,12 @@ types:
name: string
version: integer
config: unknown
labels: list<string>
labels:
type: list<string>
docs: List of deployment labels of this prompt version.
tags:
type: list<string>
docs: List of tags. Used to filter via UI and API. The same across versions of a prompt.
ChatMessage:
properties:
+1 -1
View File
@@ -7,7 +7,7 @@ service:
base-path: /api/public
endpoints:
get:
docs: Get a session
docs: Get a session. Please note that `traces` on this endpoint are not paginated, if you plan to fetch large sessions, consider `GET /api/public/traces?sessionId=<sessionId>`
method: GET
path: /sessions/{sessionId}
path-parameters:
+1
View File
@@ -30,6 +30,7 @@ service:
docs: Limit of items per page. If you encounter api issues due to too large page sizes, try to reduce the limit.
userId: optional<string>
name: optional<string>
sessionId: optional<string>
fromTimestamp:
type: optional<datetime>
docs: Retrieve only traces newer than this datetime (ISO 8601).
+2 -2
View File
@@ -1,6 +1,6 @@
{
"name": "langfuse",
"version": "2.43.2",
"version": "2.48.0",
"author": "engineering@langfuse.com",
"license": "MIT",
"private": true,
@@ -28,7 +28,7 @@
},
"devDependencies": {
"@release-it/bumper": "^6.0.1",
"dotenv-cli": "^7.4.1",
"dotenv-cli": "^7.4.2",
"prettier": "^3.2.5",
"release-it": "^17.2.1",
"turbo": "^1.13.3"
+4 -4
View File
@@ -44,9 +44,9 @@
"seed": "ts-node -r tsconfig-paths/register -r dotenv/config --compiler-options {\"module\":\"CommonJS\"} prisma/seed.ts"
},
"dependencies": {
"@langchain/anthropic": "^0.1.16",
"@langchain/anthropic": "^0.1.21",
"@langchain/core": "^0.1.61",
"@langchain/openai": "^0.0.28",
"@langchain/openai": "^0.0.33",
"@prisma/client": "^5.13.0",
"@types/bcryptjs": "^2.4.6",
"bcryptjs": "^2.4.3",
@@ -59,9 +59,9 @@
"devDependencies": {
"@repo/eslint-config": "*",
"@repo/typescript-config": "*",
"@types/lodash": "^4.14.202",
"@types/lodash": "^4.17.4",
"@types/node": "^20.11.29",
"@types/pg": "^8.11.5",
"@types/pg": "^8.11.6",
"@types/uuid": "^9.0.8",
"@typescript-eslint/parser": "^7.7.0",
"eslint": "^8.57.0",
+29 -1
View File
@@ -25,11 +25,17 @@ export const ObservationLevel = {
} as const;
export type ObservationLevel = (typeof ObservationLevel)[keyof typeof ObservationLevel];
export const ScoreSource = {
ANNOTATION: "ANNOTATION",
API: "API",
REVIEW: "REVIEW",
EVAL: "EVAL"
} as const;
export type ScoreSource = (typeof ScoreSource)[keyof typeof ScoreSource];
export const ScoreDataType = {
CATEGORICAL: "CATEGORICAL",
NUMERIC: "NUMERIC",
BOOLEAN: "BOOLEAN"
} as const;
export type ScoreDataType = (typeof ScoreDataType)[keyof typeof ScoreDataType];
export const PricingUnit = {
PER_1000_TOKENS: "PER_1000_TOKENS",
PER_1000_CHARS: "PER_1000_CHARS"
@@ -246,6 +252,7 @@ export type Observation = {
status_message: string | null;
version: string | null;
created_at: Generated<Timestamp>;
updated_at: Generated<Timestamp>;
model: string | null;
internal_model: string | null;
modelParameters: unknown | null;
@@ -293,6 +300,7 @@ export type ObservationView = {
calculated_output_cost: string | null;
calculated_total_cost: string | null;
latency: number | null;
time_to_first_token: number | null;
};
export type PosthogIntegration = {
project_id: string;
@@ -346,9 +354,28 @@ export type Score = {
name: string;
value: number;
source: ScoreSource;
author_user_id: string | null;
comment: string | null;
trace_id: string;
observation_id: string | null;
config_id: string | null;
string_value: string | null;
created_at: Generated<Timestamp>;
updated_at: Generated<Timestamp>;
data_type: Generated<ScoreDataType>;
};
export type ScoreConfig = {
id: string;
created_at: Generated<Timestamp>;
updated_at: Generated<Timestamp>;
project_id: string;
name: string;
data_type: ScoreDataType;
is_archived: Generated<boolean>;
min_value: number | null;
max_value: number | null;
categories: unknown | null;
description: string | null;
};
export type Session = {
id: string;
@@ -448,6 +475,7 @@ export type DB = {
project_memberships: ProjectMembership;
projects: Project;
prompts: Prompt;
score_configs: ScoreConfig;
scores: Score;
Session: Session;
sso_configs: SsoConfig;
@@ -0,0 +1,61 @@
CREATE OR REPLACE VIEW "observations_view" AS
SELECT
o.*,
m.id AS "model_id",
m.start_date AS "model_start_date",
m.input_price,
m.output_price,
m.total_price,
m.tokenizer_config AS "tokenizer_config",
CASE
WHEN o.input_cost IS NULL AND o.output_cost IS NULL AND o.total_cost IS NULL THEN
o.prompt_tokens::decimal * m.input_price
ELSE
o.input_cost
END AS "calculated_input_cost",
CASE
WHEN o.input_cost IS NULL AND o.output_cost IS NULL AND o.total_cost IS NULL THEN
o.completion_tokens::decimal * m.output_price
ELSE
o.output_cost
END AS "calculated_output_cost",
CASE
WHEN o.input_cost IS NULL AND o.output_cost IS NULL AND o.total_cost IS NULL THEN
CASE
WHEN m.total_price IS NOT NULL AND o.total_tokens IS NOT NULL THEN
m.total_price * o.total_tokens
ELSE
o.prompt_tokens::decimal * m.input_price +
o.completion_tokens::decimal * m.output_price
END
ELSE
o.total_cost
END AS "calculated_total_cost",
CASE WHEN o.end_time IS NULL THEN NULL ELSE (EXTRACT(EPOCH FROM o."end_time") - EXTRACT(EPOCH FROM o."start_time"))::double precision END AS "latency",
CASE WHEN o.completion_start_time IS NOT NULL AND o.start_time IS NOT NULL THEN EXTRACT(EPOCH FROM (completion_start_time - start_time))::double precision ELSE NULL END as "time_to_first_token"
FROM
observations o
LEFT JOIN LATERAL (
SELECT
models.*
FROM
models
WHERE (models.project_id = o.project_id OR models.project_id IS NULL)
AND models.model_name = o.internal_model
AND (models.start_date < o.start_time OR models.start_date IS NULL)
AND o.unit::TEXT = models.unit
ORDER BY
models.project_id ASC, -- in postgres, NULLs are sorted last when ordering ASC
models.start_date DESC NULLS LAST -- now, NULLs are sorted last when ordering DESC as well
LIMIT 1
) m ON TRUE
-- requirements:
-- 1. The view should return all columns from the observations table
-- 2. The view should match with only one model for each observation if:
-- a. The model has the same project_id as the observation, otherwise the model without project_id.
-- b. The model has the same model_name as the observation
-- c. The model has a start_date that is less than the observation start_time, otherwise the model without start_date
-- d. The model has the same unit as the observation
@@ -0,0 +1,2 @@
-- AlterTable
ALTER TABLE "scores" ADD COLUMN "author_user_id" TEXT;
@@ -0,0 +1,2 @@
-- CreateIndex
CREATE INDEX CONCURRENTLY "scores_author_user_id_idx" ON "scores"("author_user_id");
@@ -0,0 +1,42 @@
-- CreateEnum
CREATE TYPE "ScoreDataType" AS ENUM ('CATEGORICAL', 'NUMERIC');
-- AlterTable
ALTER TABLE "scores" ADD COLUMN "config_id" TEXT,
ADD COLUMN "data_type" "ScoreDataType" NOT NULL DEFAULT 'NUMERIC',
ADD COLUMN "string_value" TEXT;
-- CreateTable
CREATE TABLE "score_configs" (
"id" TEXT NOT NULL,
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updated_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"project_id" TEXT NOT NULL,
"name" TEXT NOT NULL,
"data_type" "ScoreDataType" NOT NULL,
"is_archived" BOOLEAN NOT NULL DEFAULT false,
"min_value" DOUBLE PRECISION,
"max_value" DOUBLE PRECISION,
"categories" JSONB,
"description" TEXT,
CONSTRAINT "score_configs_pkey" PRIMARY KEY ("id")
);
-- CreateIndex
CREATE INDEX "score_configs_data_type_idx" ON "score_configs"("data_type");
-- CreateIndex
CREATE INDEX "score_configs_is_archived_idx" ON "score_configs"("is_archived");
-- CreateIndex
CREATE INDEX "score_configs_project_id_idx" ON "score_configs"("project_id");
-- CreateIndex
CREATE INDEX "score_configs_categories_idx" ON "score_configs"("categories");
-- CreateIndex
CREATE UNIQUE INDEX "score_configs_id_project_id_key" ON "score_configs"("id", "project_id");
-- AddForeignKey
ALTER TABLE "score_configs" ADD CONSTRAINT "score_configs_project_id_fkey" FOREIGN KEY ("project_id") REFERENCES "projects"("id") ON DELETE CASCADE ON UPDATE CASCADE;
@@ -0,0 +1,2 @@
-- CreateIndex
CREATE INDEX CONCURRENTLY "scores_config_id_idx" ON "scores"("config_id");
@@ -0,0 +1,2 @@
-- AddForeignKey
ALTER TABLE "scores" ADD CONSTRAINT "scores_config_id_fkey" FOREIGN KEY ("config_id") REFERENCES "score_configs"("id") ON DELETE SET NULL ON UPDATE CASCADE;
@@ -0,0 +1,2 @@
-- AlterEnum
ALTER TYPE "ScoreSource" ADD VALUE 'ANNOTATION';
@@ -0,0 +1,4 @@
-- Backfill the scores source for 'REVIEW' to be 'ANNOTATION'
UPDATE "scores"
SET "source" = 'ANNOTATION'::"ScoreSource"
WHERE "source" = 'REVIEW'::"ScoreSource";
@@ -0,0 +1,14 @@
/*
Warnings:
- The values [REVIEW] on the enum `ScoreSource` will be removed. If these variants are still used in the database, this will fail.
*/
-- AlterEnum
BEGIN;
CREATE TYPE "ScoreSource_new" AS ENUM ('ANNOTATION', 'API', 'EVAL');
ALTER TABLE "scores" ALTER COLUMN "source" TYPE "ScoreSource_new" USING ("source"::text::"ScoreSource_new");
ALTER TYPE "ScoreSource" RENAME TO "ScoreSource_old";
ALTER TYPE "ScoreSource_new" RENAME TO "ScoreSource";
DROP TYPE "ScoreSource_old";
COMMIT;
@@ -0,0 +1,2 @@
-- CreateIndex
CREATE INDEX CONCURRENTLY "job_executions_job_configuration_id_idx" ON "job_executions"("job_configuration_id");
@@ -0,0 +1,2 @@
-- CreateIndex
CREATE INDEX CONCURRENTLY "job_executions_job_output_score_id_idx" ON "job_executions"("job_output_score_id");
@@ -0,0 +1,2 @@
-- CreateIndex
CREATE INDEX CONCURRENTLY "job_executions_job_input_trace_id_idx" ON "job_executions"("job_input_trace_id");
@@ -0,0 +1,2 @@
-- CreateIndex
CREATE INDEX CONCURRENTLY "job_executions_created_at_idx" ON "job_executions"("created_at");
@@ -0,0 +1,2 @@
-- AlterTable
ALTER TABLE "observations" ADD COLUMN "updated_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP;
@@ -0,0 +1,4 @@
-- AlterTable
ALTER TABLE "scores" ADD COLUMN "created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
ADD COLUMN "updated_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP;
@@ -0,0 +1,2 @@
-- CreateIndex
CREATE INDEX CONCURRENTLY "audit_logs_updated_at_idx" ON "audit_logs"("updated_at");
@@ -0,0 +1,3 @@
-- CreateIndex
CREATE INDEX CONCURRENTLY "dataset_items_created_at_idx" ON "dataset_items"("created_at");
@@ -0,0 +1,3 @@
-- CreateIndex
CREATE INDEX CONCURRENTLY "dataset_items_updated_at_idx" ON "dataset_items"("updated_at");
@@ -0,0 +1,3 @@
-- CreateIndex
CREATE INDEX CONCURRENTLY "dataset_run_items_created_at_idx" ON "dataset_run_items"("created_at");
@@ -0,0 +1,3 @@
-- CreateIndex
CREATE INDEX CONCURRENTLY "dataset_run_items_updated_at_idx" ON "dataset_run_items"("updated_at");
@@ -0,0 +1,3 @@
-- CreateIndex
CREATE INDEX CONCURRENTLY "dataset_runs_created_at_idx" ON "dataset_runs"("created_at");
@@ -0,0 +1,3 @@
-- CreateIndex
CREATE INDEX CONCURRENTLY "dataset_runs_updated_at_idx" ON "dataset_runs"("updated_at");
@@ -0,0 +1,3 @@
-- CreateIndex
CREATE INDEX CONCURRENTLY "datasets_created_at_idx" ON "datasets"("created_at");
@@ -0,0 +1,3 @@
-- CreateIndex
CREATE INDEX CONCURRENTLY "datasets_updated_at_idx" ON "datasets"("updated_at");
@@ -0,0 +1,3 @@
-- CreateIndex
CREATE INDEX CONCURRENTLY "job_executions_updated_at_idx" ON "job_executions"("updated_at");
@@ -0,0 +1,3 @@
-- CreateIndex
CREATE INDEX CONCURRENTLY "observations_updated_at_idx" ON "observations"("updated_at");
@@ -0,0 +1,3 @@
-- CreateIndex
CREATE INDEX CONCURRENTLY "prompts_created_at_idx" ON "prompts"("created_at");
@@ -0,0 +1,3 @@
-- CreateIndex
CREATE INDEX CONCURRENTLY "prompts_updated_at_idx" ON "prompts"("updated_at");
@@ -0,0 +1,3 @@
-- CreateIndex
CREATE INDEX CONCURRENTLY "score_configs_created_at_idx" ON "score_configs"("created_at");
@@ -0,0 +1,3 @@
-- CreateIndex
CREATE INDEX CONCURRENTLY "score_configs_updated_at_idx" ON "score_configs"("updated_at");
@@ -0,0 +1,3 @@
-- CreateIndex
CREATE INDEX CONCURRENTLY "scores_created_at_idx" ON "scores"("created_at");
@@ -0,0 +1,3 @@
-- CreateIndex
CREATE INDEX CONCURRENTLY "scores_updated_at_idx" ON "scores"("updated_at");
@@ -0,0 +1,3 @@
-- CreateIndex
CREATE INDEX CONCURRENTLY "trace_sessions_updated_at_idx" ON "trace_sessions"("updated_at");
@@ -0,0 +1,7 @@
-- Google Vertex uses @ to separate model name and version
UPDATE "public"."models" SET "match_pattern" = '(?i)^(claude-3-haiku(-|@)?20240307)$' WHERE "id" = 'cltr0w45b000008k1407o9qv1';
UPDATE "public"."models" SET "match_pattern" = '(?i)^(claude-3-opus(-|@)?20240229)$' WHERE "id" = 'cltgy0iuw000008le3vod1hhy';
UPDATE "public"."models" SET "match_pattern" = '(?i)^(claude-3-sonnet(-|@)?20240229)$' WHERE "id" = 'cltgy0pp6000108le56se7bl3';
@@ -0,0 +1,2 @@
-- CreateIndex
CREATE INDEX CONCURRENTLY "dataset_items_source_trace_id_idx" ON "dataset_items" USING HASH ("source_trace_id");
@@ -0,0 +1,2 @@
-- CreateIndex
CREATE INDEX "scores_project_id_name_idx" ON "scores"("project_id", "name");
@@ -0,0 +1,2 @@
-- AlterEnum
ALTER TYPE "ScoreDataType" ADD VALUE 'BOOLEAN';
@@ -0,0 +1,50 @@
-- Create manual-score config for any project with manual scores and link config_id to scores
BEGIN;
WITH project_configs AS (
INSERT INTO score_configs (id,
project_id,
name,
data_type,
is_archived,
min_value,
max_value,
description)
SELECT
gen_random_uuid () AS id,
s.project_id,
'manual-score',
'NUMERIC',
FALSE,
- 1,
1,
'Langfuse legacy annotation score.'
FROM ( SELECT DISTINCT
project_id
FROM
scores
WHERE
name = 'manual-score'
AND source = 'ANNOTATION') s
WHERE
NOT EXISTS (
SELECT
1
FROM
score_configs sc
WHERE
sc.name = 'manual-score'
AND sc.project_id = s.project_id)
RETURNING
id,
project_id
)
UPDATE
scores
SET
config_id = pc.id
FROM
project_configs pc
WHERE
scores.project_id = pc.project_id
AND scores.name = 'manual-score';
COMMIT;
+66 -1
View File
@@ -114,6 +114,7 @@ model Project {
LlmApiKeys LlmApiKeys[]
PosthogIntegration PosthogIntegration[]
Score Score[]
scoreConfig ScoreConfig[]
@@map("projects")
}
@@ -205,6 +206,7 @@ model TraceSession {
@@id([id, projectId])
@@index([projectId])
@@index([createdAt])
@@index([updatedAt])
@@map("trace_sessions")
}
@@ -292,6 +294,7 @@ model Observation {
statusMessage String? @map("status_message")
version String?
createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime @default(now()) @updatedAt @map("updated_at")
// GENERATION ONLY
model String?
@@ -323,6 +326,7 @@ model Observation {
@@index([type])
@@index(startTime)
@@index(createdAt)
@@index(updatedAt)
@@index(projectId)
@@index(parentObservationId)
@@index(model)
@@ -373,6 +377,7 @@ view ObservationView {
calculatedOutputCost Decimal? @map("calculated_output_cost")
calculatedTotalCost Decimal? @map("calculated_total_cost")
latency Float? @map("latency")
timeToFirstToken Float? @map("time_to_first_token")
@@map("observations_view")
}
@@ -398,27 +403,70 @@ model Score {
name String
value Float
source ScoreSource
authorUserId String? @map("author_user_id")
comment String?
traceId String @map("trace_id")
observationId String? @map("observation_id")
configId String? @map("config_id")
stringValue String? @map("string_value")
createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime @default(now()) @updatedAt @map("updated_at")
dataType ScoreDataType @default(NUMERIC) @map("data_type")
JobExecution JobExecution[]
scoreConfig ScoreConfig? @relation(fields: [configId], references: [id], onDelete: SetNull)
@@unique([id, projectId]) // used for upserts via prisma
@@index(timestamp)
@@index([value])
@@index([projectId])
@@index([projectId, name])
@@index([authorUserId])
@@index([configId])
@@index([traceId], type: Hash)
@@index([observationId], type: Hash)
@@index([source])
@@index([createdAt])
@@index([updatedAt])
@@map("scores")
}
enum ScoreSource {
ANNOTATION
API
REVIEW
EVAL
}
model ScoreConfig {
id String @id @default(cuid())
createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime @default(now()) @updatedAt @map("updated_at")
projectId String @map("project_id")
project Project @relation(fields: [projectId], references: [id], onDelete: Cascade)
name String
dataType ScoreDataType @map("data_type")
isArchived Boolean @default(false) @map("is_archived")
minValue Float? @map("min_value")
maxValue Float? @map("max_value")
categories Json? @map("categories")
description String?
score Score[]
@@unique([id, projectId]) // used for upserts via prisma
@@index([dataType])
@@index([isArchived])
@@index([projectId])
@@index([categories])
@@index([createdAt])
@@index([updatedAt])
@@map("score_configs")
}
enum ScoreDataType {
CATEGORICAL
NUMERIC
BOOLEAN
}
enum PricingUnit {
PER_1000_TOKENS
PER_1000_CHARS
@@ -465,6 +513,8 @@ model Dataset {
@@unique([projectId, name])
@@index([projectId], type: Hash)
@@index([createdAt])
@@index([updatedAt])
@@map("datasets")
}
@@ -484,8 +534,11 @@ model DatasetItem {
updatedAt DateTime @default(now()) @updatedAt @map("updated_at")
datasetRunItems DatasetRunItems[]
@@index([sourceTraceId], type: Hash)
@@index([sourceObservationId], type: Hash)
@@index([datasetId], type: Hash)
@@index([createdAt])
@@index([updatedAt])
@@map("dataset_items")
}
@@ -507,6 +560,8 @@ model DatasetRuns {
@@unique([datasetId, name])
@@index([datasetId], type: Hash)
@@index([createdAt])
@@index([updatedAt])
@@map("dataset_runs")
}
@@ -527,6 +582,8 @@ model DatasetRunItems {
@@index([datasetItemId], type: Hash)
@@index([observationId], type: Hash)
@@index([traceId])
@@index([createdAt])
@@index([updatedAt])
@@map("dataset_run_items")
}
@@ -569,6 +626,8 @@ model Prompt {
@@index([projectId, name, version])
@@index([projectId, id])
@@index([projectId])
@@index([createdAt])
@@index([updatedAt])
@@index([tags(ops: ArrayOps)], type: Gin)
@@map("prompts")
}
@@ -616,6 +675,7 @@ model AuditLog {
@@index([projectId])
@@index([createdAt])
@@index([updatedAt])
@@map("audit_logs")
}
@@ -709,6 +769,11 @@ model JobExecution {
@@index([projectId, status])
@@index([projectId, id])
@@index([projectId])
@@index([jobConfigurationId])
@@index([jobOutputScoreId])
@@index([jobInputTraceId])
@@index([createdAt])
@@index([updatedAt])
@@map("job_executions")
}
+135 -6
View File
@@ -4,6 +4,7 @@ import {
type Prisma,
ObservationType,
ScoreSource,
ScoreDataType,
} from "../src/index";
import { hash } from "bcryptjs";
import { parseArgs } from "node:util";
@@ -12,6 +13,7 @@ import { chunk } from "lodash";
import { v4 } from "uuid";
import { ModelUsageUnit } from "../src";
import { getDisplaySecretKey, hashSecretKey } from "../src/server/auth";
import { encrypt } from "../src/encryption";
const LOAD_TRACE_VOLUME = 10_000;
@@ -38,6 +40,7 @@ async function main() {
name: "Demo User",
email: "demo@langfuse.com",
password: await hash("password", 12),
image: "https://static.langfuse.com/langfuse-dev%2Fexample-avatar.png",
},
});
@@ -143,6 +146,11 @@ async function main() {
});
}
const configIdsAndNames = await generateConfigsForProject([
project1,
project2,
]);
const promptIds = await generatePromptsForProject([project1, project2]);
const envTags = [null, "development", "staging", "production"];
@@ -156,7 +164,8 @@ async function main() {
colorTags,
project1,
project2,
promptIds
promptIds,
configIdsAndNames
);
console.log(
@@ -165,6 +174,24 @@ async function main() {
await uploadObjects(traces, observations, scores, sessions, events);
// If openai key is in environment, add it to the projects LLM API keys
const OPENAI_API_KEY = process.env.OPENAI_API_KEY;
if (OPENAI_API_KEY) {
await prisma.llmApiKeys.create({
data: {
projectId: project1.id,
secretKey: encrypt(OPENAI_API_KEY),
displaySecretKey: getDisplaySecretKey(OPENAI_API_KEY),
provider: "openai",
},
});
} else {
console.warn(
"No OPENAI_API_KEY found in environment. Skipping seeding LLM API key."
);
}
// add eval objects
const evalTemplate = await prisma.evalTemplate.upsert({
where: {
@@ -434,13 +461,15 @@ function createObjects(
colorTags: (string | null)[],
project1: Project,
project2: Project,
promptIds: Map<string, string[]>
promptIds: Map<string, string[]>,
configIdsAndNames: Map<string, { name: string; id: string }[]>
) {
const traces: Prisma.TraceCreateManyInput[] = [];
const observations: Prisma.ObservationCreateManyInput[] = [];
const scores: Prisma.ScoreCreateManyInput[] = [];
const sessions: Prisma.TraceSessionCreateManyInput[] = [];
const events: Prisma.ObservationCreateManyInput[] = [];
const configs: Prisma.ScoreConfigCreateManyInput[] = [];
for (let i = 0; i < traceVolume; i++) {
// print progress to console with a progress bar that refreshes every 10 iterations
@@ -492,16 +521,28 @@ function createObjects(
traces.push(trace);
const configArray = configIdsAndNames.get(projectId) ?? [];
const randomIndex = Math.floor(Math.random() * 3);
const config =
configArray.length >= randomIndex - 1 && configArray[randomIndex];
const { name: annotationScoreName, id: configId } = config || {
name: "manual-score",
id: undefined,
};
const traceScores = [
...(Math.random() > 0.5
? [
{
traceId: trace.id,
name: "manual-score",
name: annotationScoreName,
value: Math.floor(Math.random() * 3) - 1,
timestamp: traceTs,
source: ScoreSource.REVIEW,
source: ScoreSource.ANNOTATION,
projectId,
authorUserId: `user-${i}`,
dataType: ScoreDataType.NUMERIC,
...(configId ? { configId } : {}),
},
]
: []),
@@ -514,6 +555,7 @@ function createObjects(
timestamp: traceTs,
source: ScoreSource.API,
projectId,
dataType: ScoreDataType.NUMERIC,
},
]
: []),
@@ -528,9 +570,9 @@ function createObjects(
const spanTsStart = new Date(
traceTs.getTime() + Math.floor(Math.random() * 30)
);
// random duration of upto 30ms
// random duration of upto 5000ms
const spanTsEnd = new Date(
spanTsStart.getTime() + Math.floor(Math.random() * 30)
spanTsStart.getTime() + Math.floor(Math.random() * 5000)
);
const span = {
@@ -574,6 +616,13 @@ function createObjects(
(spanTsEnd.getTime() - generationTsStart.getTime())
)
);
// somewhere in the middle
const generationTsCompletionStart = new Date(
generationTsStart.getTime() +
Math.floor(
(generationTsEnd.getTime() - generationTsStart.getTime()) / 3
)
);
const promptTokens = Math.floor(Math.random() * 1000) + 300;
const completionTokens = Math.floor(Math.random() * 500) + 100;
@@ -602,6 +651,8 @@ function createObjects(
id: `generation-${v4()}`,
startTime: generationTsStart,
endTime: generationTsEnd,
completionStartTime:
Math.random() > 0.5 ? generationTsCompletionStart : undefined,
name: `generation-${i}-${j}-${k}`,
projectId: trace.projectId,
promptId: promptId,
@@ -743,6 +794,7 @@ function createObjects(
traces,
observations,
scores,
configs,
sessions: uniqueSessions,
events,
};
@@ -925,3 +977,80 @@ async function generatePrompts(project: Project) {
}
return promptIds;
}
async function generateConfigsForProject(projects: Project[]) {
const projectIdsToConfigs: Map<string, { name: string; id: string }[]> =
new Map();
await Promise.all(
projects.map(async (project) => {
const configNameAndId = await generateConfigs(project);
projectIdsToConfigs.set(project.id, configNameAndId);
})
);
return projectIdsToConfigs;
}
async function generateConfigs(project: Project) {
const configNameAndId: { name: string; id: string }[] = [];
const configs = [
{
id: `config-${v4()}`,
name: "manual-score",
dataType: ScoreDataType.NUMERIC,
projectId: project.id,
isArchived: false,
},
{
id: `config-${v4()}`,
projectId: project.id,
name: "Accuracy",
dataType: ScoreDataType.CATEGORICAL,
categories: [
{ label: "Incorrect", value: 0 },
{ label: "Partially Correct", value: 1 },
{ label: "Correct", value: 2 },
],
isArchived: false,
},
{
id: `config-${v4()}`,
projectId: project.id,
name: "Toxicity",
dataType: ScoreDataType.BOOLEAN,
categories: [
{ label: "True", value: 1 },
{ label: "False", value: 0 },
],
description:
"Used to indicate if text was harmful or offensive in nature.",
isArchived: false,
},
];
for (const config of configs) {
await prisma.scoreConfig.upsert({
where: {
id_projectId: {
projectId: config.projectId,
id: config.id,
},
},
create: {
id: config.id,
projectId: config.projectId,
name: config.name,
dataType: config.dataType,
categories: config.categories,
isArchived: config.isArchived,
},
update: {
id: config.id,
},
});
configNameAndId.push({ name: config.name, id: config.id });
}
return configNameAndId;
}
+4 -4
View File
@@ -1,7 +1,7 @@
import { Prisma } from "@prisma/client";
import {
ColumnDefinition,
type TableNames as TableName,
type TableNames,
} from "./interfaces/tableDefinition";
import { FilterState } from "./types";
import { filterOperators } from "./interfaces/filters";
@@ -27,7 +27,7 @@ const arrayOperatorReplacements = {
export function tableColumnsToSqlFilterAndPrefix(
filters: FilterState,
tableColumns: ColumnDefinition[],
table: TableName
table: TableNames
): Prisma.Sql {
const sql = tableColumnsToSqlFilter(filters, tableColumns, table);
if (sql === Prisma.empty) {
@@ -43,7 +43,7 @@ export function tableColumnsToSqlFilterAndPrefix(
export function tableColumnsToSqlFilter(
filters: FilterState,
tableColumns: ColumnDefinition[],
table: TableName
table: TableNames
): Prisma.Sql {
const internalFilters = filters.map((filter) => {
// Get column definition to map column to internal name, e.g. "t.id"
@@ -153,7 +153,7 @@ export function tableColumnsToSqlFilter(
const castValueToPostgresTypes = (
column: ColumnDefinition,
table: TableName
table: TableNames
) => {
return column.name === "type" &&
(table === "observations" ||
@@ -42,6 +42,7 @@ export const tableNames = [
"traces_parent_observation_scores",
"sessions",
"prompts",
"users",
] as const;
export type TableNames = (typeof tableNames)[number];
+3 -3
View File
@@ -34,9 +34,9 @@ export type UIModelParams = RecordWithEnabledFlag<
export type ModelConfig = z.infer<typeof ZodModelConfig>;
export const ZodModelConfig = z.object({
max_tokens: z.number().optional(),
temperature: z.number().optional(),
top_p: z.number().optional(),
max_tokens: z.coerce.number().optional(),
temperature: z.coerce.number().optional(),
top_p: z.coerce.number().optional(),
});
// OpenAI
+2 -1
View File
@@ -30,4 +30,5 @@ export type TableName =
| "sessions"
| "scores"
| "prompts"
| "dashboard";
| "dashboard"
| "users";
+441 -250
View File
File diff suppressed because it is too large Load Diff
+1 -1
View File
@@ -5,7 +5,7 @@ FROM node:20-alpine AS alpine
RUN apk update && apk upgrade --no-cache libcrypto3 libssl3 libc6-compat
FROM alpine AS base
RUN npm install turbo --global
RUN npm install turbo@^1.13.3 --global
ENV PNPM_HOME="/pnpm"
ENV PATH="$PNPM_HOME:$PATH"
RUN corepack enable
+24 -17
View File
@@ -1,6 +1,6 @@
{
"name": "web",
"version": "2.43.2",
"version": "2.48.0",
"private": true,
"license": "MIT",
"engines": {
@@ -25,12 +25,14 @@
"@aws-sdk/lib-storage": "^3.568.0",
"@aws-sdk/s3-request-presigner": "^3.554.0",
"@codemirror/lang-json": "^6.0.1",
"@headlessui/react": "^1.7.19",
"@codemirror/lint": "^6.8.0",
"@headlessui/react": "1.7.18",
"@headlessui/tailwindcss": "0.2.0",
"@heroicons/react": "^2.1.3",
"@hookform/resolvers": "^3.3.4",
"@langchain/anthropic": "^0.1.16",
"@langchain/anthropic": "^0.1.21",
"@langchain/core": "^0.1.61",
"@langchain/openai": "^0.0.28",
"@langchain/openai": "^0.0.33",
"@langfuse/ee": "workspace:*",
"@langfuse/shared": "workspace:*",
"@marsidev/react-turnstile": "^0.5.4",
@@ -54,27 +56,29 @@
"@radix-ui/react-switch": "^1.0.3",
"@radix-ui/react-tabs": "^1.0.4",
"@radix-ui/react-toggle": "^1.0.3",
"@radix-ui/react-toggle-group": "^1.0.4",
"@radix-ui/react-tooltip": "^1.0.7",
"@react-email/components": "^0.0.17",
"@react-email/render": "^0.0.13",
"@react-email/components": "^0.0.18",
"@react-email/render": "^0.0.14",
"@remixicon/react": "^4.2.0",
"@repo/eslint-config": "workspace:*",
"@repo/typescript-config": "workspace:*",
"@sentry/nextjs": "^7.113.0",
"@sentry/node": "^7.113.0",
"@sentry/profiling-node": "^7.113.0",
"@sentry/types": "^7.113.0",
"@t3-oss/env-nextjs": "^0.8.0",
"@t3-oss/env-nextjs": "^0.10.1",
"@tailwindcss/container-queries": "^0.1.1",
"@tailwindcss/forms": "^0.5.7",
"@tanstack/react-query": "^4.36.1",
"@tanstack/react-table": "^8.11.8",
"@tanstack/react-virtual": "^3.5.0",
"@tremor/react": "3.11.1",
"@tremor/react": "3.16.2",
"@trpc/client": "^10.45.0",
"@trpc/next": "^10.45.0",
"@trpc/react-query": "^10.45.0",
"@trpc/server": "^10.45.0",
"@uiw/codemirror-theme-github": "^4.21.25",
"@uiw/codemirror-theme-tokyo-night": "^4.22.1",
"@uiw/react-codemirror": "^4.21.25",
"ai": "^3.0.23",
"bcryptjs": "^2.4.3",
@@ -85,7 +89,7 @@
"cors": "^2.8.5",
"date-fns": "^3.3.1",
"decimal.js": "^10.4.3",
"dompurify": "^3.1.2",
"dompurify": "^3.1.4",
"exponential-backoff": "^3.1.1",
"js-tiktoken": "^1.0.12",
"kysely": "^0.27.3",
@@ -95,6 +99,7 @@
"next": "^14.2.3",
"next-auth": "^4.24.7",
"next-query-params": "^5.0.0",
"next-themes": "^0.3.0",
"nodemailer": "^6.9.13",
"posthog-js": "^1.122.0",
"posthog-node": "^3.6.3",
@@ -102,8 +107,8 @@
"react": "18.2.0",
"react-day-picker": "^8.10.1",
"react-dom": "18.2.0",
"react-hook-form": "^7.51.3",
"react-icons": "^5.0.1",
"react-hook-form": "^7.51.5",
"react-icons": "^5.2.1",
"react-responsive": "^10.0.0",
"react18-json-view": "^0.2.8-canary.6",
"sonner": "^1.4.41",
@@ -112,29 +117,31 @@
"tailwindcss-animate": "^1.0.7",
"use-query-params": "^2.2.1",
"uuid": "^9.0.1",
"vaul": "^0.9.1",
"zod": "^3.22.4"
},
"devDependencies": {
"@jedmao/location": "^3.0.0",
"@mermaid-js/mermaid-cli": "^10.7.0",
"@playwright/test": "^1.43.1",
"@testing-library/jest-dom": "^6.4.2",
"@testing-library/react": "^15.0.6",
"@tailwindcss/forms": "^0.5.7",
"@testing-library/jest-dom": "^6.4.5",
"@testing-library/react": "^15.0.7",
"@types/bcryptjs": "^2.4.6",
"@types/cors": "^2.8.17",
"@types/dompurify": "^3.0.5",
"@types/eslint": "^8.56.7",
"@types/jest": "^29.5.12",
"@types/lodash": "^4.14.202",
"@types/lodash": "^4.17.4",
"@types/node": "20.10.5",
"@types/nodemailer": "^6.4.14",
"@types/nodemailer": "^6.4.15",
"@types/react": "^18.2.79",
"@types/react-dom": "^18.2.25",
"@types/uuid": "^9.0.8",
"@typescript-eslint/eslint-plugin": "^6.21.0",
"@typescript-eslint/parser": "^7.7.0",
"autoprefixer": "^10.4.19",
"dotenv-cli": "^7.3.0",
"dotenv-cli": "^7.4.2",
"eslint": "^8.56.0",
"eslint-config-next": "^14.2.3",
"jest": "^29.7.0",
+49 -2
View File
@@ -1137,7 +1137,10 @@ paths:
security: *ref_0
/api/public/sessions/{sessionId}:
get:
description: Get a session
description: >-
Get a session. Please note that `traces` on this endpoint are not
paginated, if you plan to fetch large sessions, consider `GET
/api/public/traces?sessionId=<sessionId>`
operationId: sessions_get
tags:
- Sessions
@@ -1262,6 +1265,12 @@ paths:
schema:
type: string
nullable: true
- name: sessionId
in: query
required: false
schema:
type: string
nullable: true
- name: fromTimestamp
in: query
description: Retrieve only traces newer than this datetime (ISO 8601).
@@ -1825,8 +1834,8 @@ components:
title: ScoreSource
type: string
enum:
- ANNOTATION
- API
- REVIEW
- EVAL
CreateDatasetItemRequest:
title: CreateDatasetItemRequest
@@ -2499,6 +2508,7 @@ components:
totalCost:
type: number
format: double
description: Total model cost in USD
usage:
type: array
items:
@@ -2521,14 +2531,28 @@ components:
nullable: true
inputUsage:
type: integer
description: Total number of generation input units (e.g. tokens)
outputUsage:
type: integer
description: Total number of generation output units (e.g. tokens)
totalUsage:
type: integer
description: Total number of generation total units (e.g. tokens)
countTraces:
type: integer
countObservations:
type: integer
totalCost:
type: number
format: double
description: Total model cost in USD
required:
- inputUsage
- outputUsage
- totalUsage
- countTraces
- countObservations
- totalCost
Observations:
title: Observations
type: object
@@ -2654,6 +2678,13 @@ components:
items:
type: string
nullable: true
description: List of deployment labels of this prompt version.
tags:
type: array
items:
type: string
nullable: true
description: List of tags to apply to all versions of this prompt.
required:
- name
- prompt
@@ -2672,6 +2703,13 @@ components:
items:
type: string
nullable: true
description: List of deployment labels of this prompt version.
tags:
type: array
items:
type: string
nullable: true
description: List of tags to apply to all versions of this prompt.
required:
- name
- prompt
@@ -2713,11 +2751,20 @@ components:
type: array
items:
type: string
description: List of deployment labels of this prompt version.
tags:
type: array
items:
type: string
description: >-
List of tags. Used to filter via UI and API. The same across
versions of a prompt.
required:
- name
- version
- config
- labels
- tags
ChatMessage:
title: ChatMessage
type: object
+8 -3
View File
@@ -699,7 +699,7 @@
"auth": null,
"body": {
"mode": "raw",
"raw": "{\n \"type\": \"chat\",\n \"name\": \"example\",\n \"prompt\": [\n {\n \"role\": \"example\",\n \"content\": \"example\"\n }\n ],\n \"config\": \"UNKNOWN\",\n \"labels\": [\n \"example\"\n ]\n}",
"raw": "{\n \"type\": \"chat\",\n \"name\": \"example\",\n \"prompt\": [\n {\n \"role\": \"example\",\n \"content\": \"example\"\n }\n ],\n \"config\": \"UNKNOWN\",\n \"labels\": [\n \"example\"\n ],\n \"tags\": [\n \"example\"\n ]\n}",
"options": {
"raw": {
"language": "json"
@@ -890,7 +890,7 @@
"_type": "endpoint",
"name": "Get",
"request": {
"description": "Get a session",
"description": "Get a session. Please note that `traces` on this endpoint are not paginated, if you plan to fetch large sessions, consider `GET /api/public/traces?sessionId=<sessionId>`",
"url": {
"raw": "{{baseUrl}}/api/public/sessions/:sessionId",
"host": [
@@ -963,7 +963,7 @@
"request": {
"description": "Get list of traces.",
"url": {
"raw": "{{baseUrl}}/api/public/traces?page=&limit=&userId=&name=&fromTimestamp=&orderBy=&tags=",
"raw": "{{baseUrl}}/api/public/traces?page=&limit=&userId=&name=&sessionId=&fromTimestamp=&orderBy=&tags=",
"host": [
"{{baseUrl}}"
],
@@ -993,6 +993,11 @@
"value": "",
"description": null
},
{
"key": "sessionId",
"value": "",
"description": null
},
{
"key": "fromTimestamp",
"value": "",
+12 -3
View File
@@ -18,12 +18,21 @@ if (process.env.NEXT_PUBLIC_SENTRY_DSN)
samplingContext.request.url &&
samplingContext.request.url.includes("api/trpc")
) {
return 0.3;
return 0.1;
}
return 0.1;
if (
samplingContext.request &&
samplingContext.request.url &&
samplingContext.request.url.includes("api/auth") &&
samplingContext.transactionContext.status !== "ok" &&
samplingContext.transactionContext.status !== "unauthenticated"
) {
return 0.1;
}
return 0.01;
},
profilesSampleRate: 0.2, // Profiling sample rate is relative to tracesSampleRate
profilesSampleRate: 0.1,
integrations: [
// Add profiling integration to list of integrations
new ProfilingIntegration(),
@@ -727,6 +727,7 @@ describe("/api/public/ingestion API Endpoint", () => {
metadata: { key: "value" },
release: "1.0.0",
version: "2.0.0",
tags: ["tag-1", "tag-2", "tag-2"],
},
},
],
@@ -743,6 +744,7 @@ describe("/api/public/ingestion API Endpoint", () => {
id: traceId,
name: "trace-name",
userId: "user-2",
tags: ["tag-1", "tag-4", "tag-3"],
},
},
],
@@ -763,6 +765,8 @@ describe("/api/public/ingestion API Endpoint", () => {
expect(dbTrace[0]?.externalId).toBeNull();
expect(dbTrace[0]?.version).toBe("2.0.0");
expect(dbTrace[0]?.projectId).toBe("7a88fb47-b4e2-43b8-a06c-a5ce950dc53a");
expect(dbTrace[0]?.tags).toEqual(["tag-1", "tag-2", "tag-3", "tag-4"]);
expect(dbTrace[0]?.tags.length).toBe(4);
});
it("should fail for wrong event formats", async () => {
@@ -68,12 +68,18 @@ describe("/api/public/metrics/daily API Endpoint", () => {
inputUsage: 333,
outputUsage: 0,
totalUsage: 333,
countObservations: 1,
countTraces: 1,
totalCost: 0,
},
{
model: "modelC",
inputUsage: 666,
outputUsage: 777,
totalUsage: 1443,
countObservations: 1,
countTraces: 1,
totalCost: 1024.22,
},
]);
@@ -88,6 +94,9 @@ describe("/api/public/metrics/daily API Endpoint", () => {
inputUsage: 100,
outputUsage: 200,
totalUsage: 300,
countObservations: 1,
countTraces: 1,
totalCost: 0,
},
]);
});
@@ -680,6 +680,71 @@ describe("/api/public/v2/prompts API Endpoint", () => {
expect(fetchedPrompt.body.createdBy).toBe("API");
expect(fetchedPrompt.body.config).toEqual({});
});
it("should update tags across versions", async () => {
const promptName = "prompt-name" + nanoid();
const createPromptVersion = async (tags?: string[]) => {
await makeAPICall("POST", baseURI, {
name: promptName,
prompt: "This is a test prompt",
type: PromptType.Text,
...(tags !== undefined && { tags: tags }),
});
};
const fetchPromptVersion = async (version: number) => {
const fetchedPrompt = await makeAPICall(
"GET",
`${baseURI}/${promptName}?version=${version}`,
undefined,
);
expect(fetchedPrompt.status).toBe(200);
if (!isPrompt(fetchedPrompt.body)) {
throw new Error("Expected body to be a prompt");
}
return fetchedPrompt.body;
};
// Create version 1 with ["tag"]
await createPromptVersion(["tag"]);
let fetchedPrompt1 = await fetchPromptVersion(1);
expect(fetchedPrompt1.tags).toEqual(["tag"]);
expect(fetchedPrompt1.version).toBe(1);
// Create version 2 with no tags provided (should use tags from version 1)
await createPromptVersion();
let fetchedPrompt2 = await fetchPromptVersion(2);
expect(fetchedPrompt2.tags).toEqual(["tag"]);
expect(fetchedPrompt2.version).toBe(2);
// Create version 3 with ["tag1", "tag2", "tag3"] (should update tags across versions)
await createPromptVersion(["tag1", "tag2", "tag3"]);
fetchedPrompt1 = await fetchPromptVersion(1);
fetchedPrompt2 = await fetchPromptVersion(2);
let fetchedPrompt3 = await fetchPromptVersion(3);
expect(fetchedPrompt1.tags).toEqual(["tag1", "tag2", "tag3"]);
expect(fetchedPrompt1.version).toBe(1);
expect(fetchedPrompt2.tags).toEqual(["tag1", "tag2", "tag3"]);
expect(fetchedPrompt2.version).toBe(2);
expect(fetchedPrompt3.tags).toEqual(["tag1", "tag2", "tag3"]);
expect(fetchedPrompt3.version).toBe(3);
// remove tags
await createPromptVersion([]);
fetchedPrompt1 = await fetchPromptVersion(1);
fetchedPrompt2 = await fetchPromptVersion(2);
fetchedPrompt3 = await fetchPromptVersion(3);
let fetchedPrompt4 = await fetchPromptVersion(4);
expect(fetchedPrompt1.tags).toEqual([]);
expect(fetchedPrompt1.version).toBe(1);
expect(fetchedPrompt2.tags).toEqual([]);
expect(fetchedPrompt2.version).toBe(2);
expect(fetchedPrompt3.tags).toEqual([]);
expect(fetchedPrompt3.version).toBe(3);
expect(fetchedPrompt4.tags).toEqual([]);
expect(fetchedPrompt4.version).toBe(4);
});
});
describe("when fetching a prompt list", () => {
@@ -50,7 +50,7 @@ describe("Build valid SQL queries", () => {
table: "traces_scores",
values: ["project-id"],
strings: [
' FROM traces t JOIN scores s ON t.id = s.trace_id AND t.project_id = s.project_id WHERE t."project_id" = ',
` FROM traces t JOIN scores s ON t.id = s.trace_id AND s.data_type != 'CATEGORICAL' AND t.project_id = s.project_id WHERE t."project_id" = `,
";",
],
} as const,
+15 -15
View File
@@ -489,16 +489,16 @@ describe("/api/public/scores API Endpoint", () => {
totalPages: 1,
});
expect(getScore.body.data).toMatchObject([
{
id: scoreId_1,
name: scoreName,
value: 10.5,
},
{
id: scoreId_2,
name: scoreName,
value: 50.5,
},
{
id: scoreId_1,
name: scoreName,
value: 10.5,
},
]);
});
it("test operator >=", async () => {
@@ -514,16 +514,16 @@ describe("/api/public/scores API Endpoint", () => {
totalPages: 1,
});
expect(getScore.body.data).toMatchObject([
{
id: scoreId_2,
name: scoreName,
value: 50.5,
},
{
id: scoreId_3,
name: scoreName,
value: 100.8,
},
{
id: scoreId_2,
name: scoreName,
value: 50.5,
},
]);
});
it("test operator !=", async () => {
@@ -539,16 +539,16 @@ describe("/api/public/scores API Endpoint", () => {
totalPages: 1,
});
expect(getScore.body.data).toMatchObject([
{
id: scoreId_1,
name: scoreName,
value: 10.5,
},
{
id: scoreId_3,
name: scoreName,
value: 100.8,
},
{
id: scoreId_1,
name: scoreName,
value: 10.5,
},
]);
});
it("test operator =", async () => {
+84
View File
@@ -182,6 +182,32 @@ describe("/api/public/traces API Endpoint", () => {
endTime: "2021-01-01T00:20:00.000Z",
});
// Simulate scores on the trace
const scoreId1 = uuidv4();
await makeAPICall("POST", "/api/public/scores", {
id: scoreId1,
name: "score-1",
value: 75.0,
traceId: traceId,
comment: "First score",
});
const scoreId2 = uuidv4();
await makeAPICall("POST", "/api/public/scores", {
id: scoreId2,
name: "score-2",
value: 85.5,
traceId: traceId,
comment: "Second score",
});
const scoreId3 = uuidv4();
await makeAPICall("POST", "/api/public/scores", {
id: scoreId3,
name: "score-3",
value: 95.0,
traceId: traceId,
comment: "Third score",
});
// GET traces
// Retrieve the trace with totalCost and latency
const traces = await makeAPICall<GetTracesAPIResponse>(
@@ -212,4 +238,62 @@ describe("/api/public/traces API Endpoint", () => {
expect(trace.body.htmlPath).toContain(`/traces/${traceId}`);
expect(trace.body.htmlPath).toContain(`/project/`); // do not know the projectId
});
it("should filter traces by session ID", async () => {
const sessionId = "test-session-id";
const anotherSessionId = "another-session-id";
// Create traces with different session IDs
await makeAPICall("POST", "/api/public/traces", {
id: "trace-1",
name: "test-trace-1",
sessionId,
userId: "user-1",
projectId: "project-1",
metadata: { key: "value" },
release: "1.0.0",
version: "1.0.0",
});
await makeAPICall("POST", "/api/public/traces", {
id: "trace-2",
name: "test-trace-2",
sessionId: anotherSessionId,
userId: "user-2",
projectId: "project-1",
metadata: { key: "value" },
release: "1.0.0",
version: "1.0.0",
});
// Filter by session ID
const tracesBySessionId = await makeAPICall<GetTracesAPIResponse>(
"GET",
`/api/public/traces?sessionId=${sessionId}`,
);
expect(tracesBySessionId.status).toBe(200);
expect(tracesBySessionId.body.data).toHaveLength(1);
expect(tracesBySessionId.body.data[0].id).toBe("trace-1");
// Filter by another session ID
const tracesByAnotherSessionId = await makeAPICall<GetTracesAPIResponse>(
"GET",
`/api/public/traces?sessionId=${anotherSessionId}`,
);
expect(tracesByAnotherSessionId.status).toBe(200);
expect(tracesByAnotherSessionId.body.data).toHaveLength(1);
expect(tracesByAnotherSessionId.body.data[0].id).toBe("trace-2");
// Filter by non-existent session ID
const tracesByNonExistentSessionId =
await makeAPICall<GetTracesAPIResponse>(
"GET",
`/api/public/traces?sessionId=non-existent-session-id`,
);
expect(tracesByNonExistentSessionId.status).toBe(200);
expect(tracesByNonExistentSessionId.body.data).toHaveLength(0);
});
});
+6
View File
@@ -0,0 +1,6 @@
import chatCompletionHandler from "@/src/ee/features/playground/server/chatCompletionHandler";
export const dynamic = "force-dynamic";
export const maxDuration = 60;
export const POST = chatCompletionHandler;
@@ -63,6 +63,7 @@ export const ChatMessageComponent: React.FC<ChatMessageProps> = ({
<Textarea
ref={textAreaRef}
id={message.id}
className="height-[auto] min-h-8 w-full pt-3 font-mono text-xs focus:outline-none"
placeholder={placeholder}
value={message.content}
+4 -4
View File
@@ -41,10 +41,10 @@ export const LangfuseLogo = ({
className={cn(
"flex items-center gap-2 self-stretch rounded-md px-1 py-1 text-xs ring-1 sm:px-3 sm:py-2 lg:-mx-2",
env.NEXT_PUBLIC_LANGFUSE_CLOUD_REGION === "STAGING"
? "bg-blue-100 text-blue-500 ring-blue-500"
? "bg-light-blue text-dark-blue ring-dark-blue"
: env.NEXT_PUBLIC_LANGFUSE_CLOUD_REGION === "DEV"
? "bg-green-100 text-green-500 ring-green-500"
: "bg-red-100 text-red-500 ring-red-500",
? "bg-light-green text-dark-green ring-dark-green"
: "bg-light-red text-dark-red ring-dark-red",
)}
>
{env.NEXT_PUBLIC_LANGFUSE_CLOUD_REGION === "DEV" ? (
@@ -76,7 +76,7 @@ export const LangfuseLogo = ({
target="_blank"
rel="noopener"
title="View releases on GitHub"
className="ml-2 text-xs text-gray-400"
className="ml-2 text-xs text-muted-foreground"
>
{VERSION}
</a>
@@ -0,0 +1,77 @@
import Link from "next/link";
import { Label } from "@/src/components/ui/label";
import { useHasAccess } from "@/src/features/rbac/utils/checkAccess";
import { api } from "@/src/utils/api";
import { supportedModels, type UIModelParams } from "@langfuse/shared";
import { ArrowTopRightIcon } from "@radix-ui/react-icons";
export const LLMApiKeyComponent = (p: {
projectId: string;
modelParams: UIModelParams;
}) => {
const hasAccess = useHasAccess({
projectId: p.projectId,
scope: "llmApiKeys:read",
});
if (!hasAccess) {
return (
<div>
<Label className="text-xs font-semibold">API key</Label>
<p className="text-sm text-muted-foreground">
LLM API Key only visible to Owner and Admin roles.
</p>
</div>
);
}
const apiKeys = api.llmApiKey.all.useQuery({
projectId: p.projectId,
});
if (apiKeys.isLoading) {
return (
<div>
<Label className="text-xs font-semibold">API key</Label>
<p className="text-sm text-muted-foreground">Loading...</p>
</div>
);
}
const model = p.modelParams.model.value;
const modelProvider = Object.entries(supportedModels).find((providerData) =>
(providerData[1] as any as string[]).includes(model),
)?.[0];
const apiKey = apiKeys.data?.data.find((k) => k.provider === modelProvider);
return (
<div className="space-y-2 text-xs">
<Label className="text-xs font-semibold">API key</Label>
<div>
{apiKey ? (
<span className="mr-2 rounded-sm bg-input p-1 text-xs">
{apiKey.displaySecretKey}
</span>
) : undefined}
</div>
{/* Custom form message to include a link to the already existing prompt */}
{!apiKey ? (
<div className="flex flex-col font-medium text-destructive">
{`No LLM API key found for provider ${modelProvider}.`}
<Link
href={`/project/${p.projectId}/settings`}
className="flex flex-row"
>
Create a new LLM API key here. <ArrowTopRightIcon />
</Link>
</div>
) : undefined}
<p className="text-muted-foreground">
The LLM API key is used for each execution and will incur costs.
</p>
</div>
);
};
@@ -8,6 +8,7 @@ import {
} from "@/src/components/ui/select";
import { Slider } from "@/src/components/ui/slider";
import { Switch } from "@/src/components/ui/switch";
import useProjectIdFromURL from "@/src/hooks/useProjectIdFromURL";
import { cn } from "@/src/utils/tailwind";
import {
ModelProvider,
@@ -15,6 +16,8 @@ import {
type UIModelParams,
} from "@langfuse/shared";
import { LLMApiKeyComponent } from "./LLMApiKeyComponent";
export type ModelParamsContext = {
modelParams: UIModelParams;
availableModels?: UIModelParams[];
@@ -33,10 +36,14 @@ export const ModelParameters: React.FC<ModelParamsContext> = ({
setModelParamEnabled,
formDisabled = false,
}) => {
const projectId = useProjectIdFromURL();
if (!projectId) return null;
return (
<div className="flex flex-col space-y-4">
<p className="font-semibold">Model</p>
<div className="space-y-6">
<div className="space-y-4">
<ModelParamsSelect
title="Provider"
modelParamsKey="provider"
@@ -104,6 +111,7 @@ export const ModelParameters: React.FC<ModelParamsContext> = ({
tooltip="An alternative to sampling with temperature, called nucleus sampling, where the model considers the results of the tokens with top_p probability mass. So 0.1 means only the tokens comprising the top 10% probability mass are considered. We generally recommend altering this or temperature but not both."
updateModelParam={updateModelParamValue}
/>
<LLMApiKeyComponent {...{ projectId, modelParams }} />
</div>
</div>
);
@@ -127,7 +135,12 @@ const ModelParamsSelect = ({
}: ModelParamsSelectProps) => {
return (
<div className="space-y-2">
<p className={cn("text-xs font-semibold", disabled && "text-gray-400")}>
<p
className={cn(
"text-xs font-semibold",
disabled && "text-muted-foreground",
)}
>
{title}
</p>
<Select
@@ -187,7 +200,7 @@ const ModelParamsSlider = ({
<p
className={cn(
"flex-1 text-xs font-semibold",
(!enabled || formDisabled) && "text-gray-400",
(!enabled || formDisabled) && "text-muted-foreground",
)}
>
{title}
+18 -22
View File
@@ -7,27 +7,23 @@ export const Slider = (props: {
loading?: boolean;
onChecked?: (checked: boolean) => void;
isChecked?: boolean; // whether the slider is active
}) => {
console.log("props", props);
return (
<Switch
checked={props.isChecked}
disabled={props.loading || props.disabled}
onChange={props.onChecked}
}) => (
<Switch
checked={props.isChecked}
disabled={props.loading || props.disabled}
onChange={props.onChecked}
className={cn(
props.isChecked ? "bg-background" : "bg-input",
"relative inline-flex h-6 w-11 flex-shrink-0 cursor-pointer rounded-full border-2 border-transparent transition-colors duration-200 ease-in-out focus:outline-none focus:ring-2 focus:ring-foreground focus:ring-offset-2",
)}
>
<span className="sr-only">Use setting</span>
<span
aria-hidden="true"
className={cn(
props.isChecked ? "bg-black" : "bg-gray-200",
"relative inline-flex h-6 w-11 flex-shrink-0 cursor-pointer rounded-full border-2 border-transparent transition-colors duration-200 ease-in-out focus:outline-none focus:ring-2 focus:ring-black focus:ring-offset-2",
props.isChecked ? "translate-x-5" : "translate-x-0",
"pointer-events-none inline-block h-5 w-5 transform rounded-full bg-background shadow ring-0 transition duration-200 ease-in-out",
)}
>
<span className="sr-only">Use setting</span>
<span
aria-hidden="true"
className={cn(
props.isChecked ? "translate-x-5" : "translate-x-0",
"pointer-events-none inline-block h-5 w-5 transform rounded-full bg-white shadow ring-0 transition duration-200 ease-in-out",
)}
/>
</Switch>
);
};
/>
</Switch>
);
+14 -21
View File
@@ -8,14 +8,7 @@ import {
CardTitle,
} from "@/src/components/ui/card";
import { chatAvailable, openChat } from "@/src/features/support-chat/chat";
import {
Book,
Calendar,
Github,
Mail,
MessageSquare,
Slack,
} from "lucide-react";
import { Book, Github, Mail, MessageSquare, Slack } from "lucide-react";
import Link from "next/link";
import { SiDiscord } from "react-icons/si";
@@ -30,7 +23,7 @@ const supportChannels = [
},
{
icon: Github,
title: "GitHub",
title: "GitHub Issues",
description:
"Create an issue on Github to report bugs or request new features.",
href: "https://github.com/langfuse/langfuse/issues/new/choose",
@@ -38,12 +31,11 @@ const supportChannels = [
primary: true,
},
{
icon: SiDiscord,
title: "Discord",
description:
"Get support from community and maintainers. Follow announcements to stay up to date with new features.",
href: "https://langfuse.com/discord",
buttonText: "Join Discord",
icon: Github,
title: "GitHub Support",
description: "Create a support ticket via GitHub discussions.",
href: "https://github.com/orgs/langfuse/discussions/categories/support",
buttonText: "Submit question",
primary: true,
},
{
@@ -57,17 +49,18 @@ const supportChannels = [
{
icon: MessageSquare,
title: "Chat",
description: "Get quick support directly from the team.",
description: "Get support directly from the team.",
onClick: () => openChat(),
available: chatAvailable,
buttonText: "Launch Chat",
},
{
icon: Calendar,
title: "Schedule call",
description: "Schedule a call with one of the founders.",
href: "https://cal.com/marc-kl/office-hours",
buttonText: "Schedule Call",
icon: SiDiscord,
title: "Discord",
description:
"Get support from community. Follow announcements to stay up to date with new features.",
href: "https://langfuse.com/discord",
buttonText: "Join Discord",
},
{
icon: Slack,
+1 -1
View File
@@ -18,7 +18,7 @@ export const ErrorPage = ({
return (
<div className="flex h-screen flex-col items-center justify-center">
<AlertCircle className="mb-4 h-12 w-12 text-red-500" />
<AlertCircle className="text-dark-red mb-4 h-12 w-12" />
<h1 className="mb-4 text-xl font-bold">{title}</h1>
<p className="mb-8 text-center">{message}</p>
{session.status === "unauthenticated" ? (
+15 -6
View File
@@ -4,9 +4,13 @@ import {
HoverCardContent,
HoverCardTrigger,
} from "@/src/components/ui/hover-card";
import {
isBooleanDataType,
isCategoricalDataType,
} from "@/src/features/manual-scoring/lib/helpers";
import { type ScoreSimplified } from "@/src/server/api/routers/generations/getAllQuery";
import { cn } from "@/src/utils/tailwind";
import { MessageCircle } from "lucide-react";
import { MessageCircleMore } from "lucide-react";
export const GroupedScoreBadges = ({
scores,
@@ -34,11 +38,13 @@ export const GroupedScoreBadges = ({
<div className={cn("text-xs", props.className)}>
{props.scores.map((s, i) => (
<span key={i} className="group/score ml-1 first:ml-0">
{s.value.toFixed(2)}
{isCategoricalDataType(s.dataType) || isBooleanDataType(s.dataType)
? s.stringValue
: s.value.toFixed(2)}
{s.comment && (
<HoverCard>
<HoverCardTrigger className="ml-1 inline-block cursor-pointer">
<MessageCircle size={12} />
<MessageCircleMore size={12} />
</HoverCardTrigger>
<HoverCardContent className="overflow-hidden whitespace-normal break-normal">
<p>{s.comment}</p>
@@ -58,7 +64,7 @@ export const GroupedScoreBadges = ({
.sort(([a], [b]) => (a < b ? -1 : 1))
.map(([name, scores]) => (
<div key={name}>
<div className="text-xs text-gray-500">{name}</div>
<div className="text-xs text-muted-foreground">{name}</div>
<ScoresOfGroup scores={scores} />
</div>
))}
@@ -73,9 +79,12 @@ export const GroupedScoreBadges = ({
<Badge
variant="outline"
key={name}
className="break-all font-normal"
className="grid grid-cols-[1fr,auto] gap-1 font-normal"
>
{name}: <ScoresOfGroup scores={scores} className="ml-2" />
<p className="truncate" title={name}>
{name}:
</p>
<ScoresOfGroup scores={scores} />
</Badge>
))}
</>
+22 -4
View File
@@ -1,7 +1,11 @@
import CodeMirror, { EditorView } from "@uiw/react-codemirror";
import { githubLight } from "@uiw/codemirror-theme-github";
import { json } from "@codemirror/lang-json";
import { tokyoNight } from "@uiw/codemirror-theme-tokyo-night";
import { json, jsonParseLinter } from "@codemirror/lang-json";
import { linter } from "@codemirror/lint";
import { useTheme } from "next-themes";
import { cn } from "@/src/utils/tailwind";
import { useState } from "react";
// todo: add json linting
@@ -18,17 +22,31 @@ export function JsonEditor({
lineWrapping?: boolean;
className?: string;
}) {
const { resolvedTheme } = useTheme();
const codeMirrorTheme = resolvedTheme === "dark" ? tokyoNight : githubLight;
// used to disable linter when field is empty
const [linterEnabled, setLinterEnabled] = useState<boolean>(
!!defaultValue && defaultValue !== "",
);
return (
<CodeMirror
value={defaultValue}
theme={githubLight}
theme={codeMirrorTheme}
basicSetup={{
foldGutter: true,
}}
lang={"json"}
extensions={[json(), ...(lineWrapping ? [EditorView.lineWrapping] : [])]}
extensions={[
json(),
...(linterEnabled ? [linter(jsonParseLinter())] : []),
...(lineWrapping ? [EditorView.lineWrapping] : []),
]}
defaultValue={defaultValue}
onChange={onChange}
onChange={(c) => {
if (onChange) onChange(c);
setLinterEnabled(c !== "");
}}
className={cn("overflow-hidden rounded-md border", className)}
editable={editable}
/>
+9 -5
View File
@@ -4,6 +4,7 @@ import {
HoverCardTrigger,
} from "@/src/components/ui/hover-card";
import { usePostHogClientCapture } from "@/src/features/posthog-analytics/usePostHogClientCapture";
import { cn } from "@/src/utils/tailwind";
import { HelpCircle, Info } from "lucide-react";
import Link from "next/link";
@@ -40,13 +41,16 @@ export default function DocPopup({
}
}}
>
<HoverCardTrigger className="mx-1 cursor-pointer" asChild>
<HoverCardTrigger
className={cn("mx-1", href ? "cursor-pointer" : "cursor-default")}
asChild
>
{href ? (
<Link
href={href}
rel="noopener"
target="_blank"
className="inline-block whitespace-nowrap text-gray-500 sm:pl-0"
className="inline-block whitespace-nowrap text-muted-foreground sm:pl-0"
onClick={() => {
capture("help_popup:href_clicked", {
href: href,
@@ -62,7 +66,7 @@ export default function DocPopup({
}
</Link>
) : (
<div className="inline-block whitespace-nowrap text-gray-500 sm:pl-0">
<div className="inline-block whitespace-nowrap text-muted-foreground sm:pl-0">
{
{
question: <HelpCircle className={sizes[size]} />,
@@ -74,7 +78,7 @@ export default function DocPopup({
</HoverCardTrigger>
<HoverCardContent>
{typeof description === "string" ? (
<div className="whitespace-break-spaces text-xs font-normal text-gray-800 sm:pl-0">
<div className="whitespace-break-spaces text-xs font-normal text-primary sm:pl-0">
{description}
</div>
) : (
@@ -98,7 +102,7 @@ export function Popup({ triggerContent, description }: PopupProps) {
</HoverCardTrigger>
<HoverCardContent>
{typeof description === "string" ? (
<div className="whitespace-break-spaces text-xs font-normal text-gray-800 sm:pl-0">
<div className="whitespace-break-spaces text-xs font-normal text-primary sm:pl-0">
{description}
</div>
) : (
+7 -7
View File
@@ -49,10 +49,10 @@ export default function Header({
<nav className="sm:hidden" aria-label="Back">
<Link
href={backHref}
className="flex items-center text-sm font-medium text-gray-500 hover:text-gray-700"
className="flex items-center text-sm font-medium text-muted-foreground hover:text-primary"
>
<ChevronLeftIcon
className="-ml-1 mr-1 h-5 w-5 flex-shrink-0 text-gray-400"
className="-ml-1 mr-1 h-5 w-5 flex-shrink-0 text-muted-foreground"
aria-hidden="true"
/>
Back
@@ -67,19 +67,19 @@ export default function Header({
<div className="flex items-center">
{index !== 0 && (
<ChevronRightIcon
className="mr-4 h-5 w-5 flex-shrink-0 text-gray-400"
className="mr-4 h-5 w-5 flex-shrink-0 text-muted-foreground"
aria-hidden="true"
/>
)}
{href ? (
<Link
href={href}
className="text-sm font-medium text-gray-500 hover:text-gray-700"
className="text-sm font-medium text-muted-foreground hover:text-primary"
>
{name}
</Link>
) : (
<div className="text-sm font-medium text-gray-500">
<div className="text-sm font-medium text-muted-foreground">
{name}
</div>
)}
@@ -94,11 +94,11 @@ export default function Header({
<div className="flex items-center gap-3 md:gap-5">
<div className="flex min-w-0 flex-row justify-center align-middle">
{level === "h2" ? (
<h2 className="text-2xl font-bold leading-7 text-gray-900 sm:truncate sm:text-3xl sm:tracking-tight">
<h2 className="text-2xl font-bold leading-7 sm:truncate sm:text-3xl sm:tracking-tight">
{props.title}
</h2>
) : (
<h3 className="text-lg font-bold leading-7 text-gray-900 sm:truncate sm:text-xl sm:tracking-tight">
<h3 className="text-lg font-bold leading-7 sm:truncate sm:text-xl sm:tracking-tight">
{props.title}
</h3>
)}
+70 -55
View File
@@ -31,14 +31,26 @@ import { ChevronDownIcon } from "@heroicons/react/20/solid";
import useLocalStorage from "@/src/components/useLocalStorage";
import { ProjectNavigation } from "@/src/components/projectNavigation";
import DOMPurify from "dompurify";
import { ThemeToggle } from "@/src/features/theming/ThemeToggle";
const signOutUser = async () => {
localStorage.clear();
sessionStorage.clear();
await signOut({
callbackUrl: "/auth/sign-in",
});
};
const userNavigation = [
{
name: "Theme",
onClick: () => {},
content: <ThemeToggle />,
},
{
name: "Sign out",
onClick: () =>
signOut({
callbackUrl: "/auth/sign-in",
}),
onClick: signOutUser,
},
];
@@ -135,9 +147,8 @@ export default function Layout(props: PropsWithChildren) {
!publishablePaths.includes(router.pathname) &&
!router.pathname.startsWith("/public/")
) {
void signOut({
callbackUrl: "/auth/sign-in",
});
signOutUser();
return <Spinner message="Redirecting" />;
}
@@ -178,7 +189,7 @@ export default function Layout(props: PropsWithChildren) {
router.pathname.startsWith("/public/");
if (hideNavigation)
return (
<main className="min-h-screen bg-gray-50 px-4 py-4 sm:px-6 lg:px-8">
<main className="min-h-screen bg-primary-foreground px-4 py-4 sm:px-6 lg:px-8">
{props.children}
</main>
);
@@ -222,7 +233,7 @@ export default function Layout(props: PropsWithChildren) {
leaveFrom="opacity-100"
leaveTo="opacity-0"
>
<div className="fixed inset-0 bg-gray-900/80" />
<div className="fixed inset-0 bg-primary/80" />
</Transition.Child>
<div className="fixed inset-0 flex">
@@ -253,14 +264,14 @@ export default function Layout(props: PropsWithChildren) {
>
<span className="sr-only">Close sidebar</span>
<XMarkIcon
className="h-6 w-6 text-white"
className="h-5 w-5 text-background"
aria-hidden="true"
/>
</button>
</div>
</Transition.Child>
{/* Sidebar component, swap this element with another sidebar if you like */}
<div className="flex grow flex-col gap-y-5 overflow-y-auto bg-white px-6 py-4">
<div className="flex grow flex-col gap-y-5 overflow-y-auto bg-background px-6 py-4">
<LangfuseLogo
version
size="xl"
@@ -273,7 +284,7 @@ export default function Layout(props: PropsWithChildren) {
<MainNavigation nav={navigation} />
</ul>
<div className="mb-2 flex flex-row place-content-between items-center">
<div className="text-xs font-semibold leading-6 text-gray-400">
<div className="text-xs font-semibold text-muted-foreground">
Project
</div>
<NewProjectButton size="xs" />
@@ -291,9 +302,9 @@ export default function Layout(props: PropsWithChildren) {
</Transition.Root>
{/* Static sidebar for desktop */}
<div className="hidden lg:fixed lg:inset-y-0 lg:z-50 lg:flex lg:w-60 lg:flex-col">
<div className="hidden lg:fixed lg:inset-y-0 lg:z-50 lg:flex lg:w-56 lg:flex-col">
{/* Sidebar component, swap this element with another sidebar if you like */}
<div className="flex h-screen grow flex-col border-r border-gray-200 bg-white pt-7">
<div className="flex h-screen grow flex-col border-r border-border bg-background pt-7">
<LangfuseLogo
version
size="xl"
@@ -312,16 +323,16 @@ export default function Layout(props: PropsWithChildren) {
description="What do you think about this project? What can be improved?"
type="feedback"
>
<li className="group -mx-2 my-1 flex cursor-pointer gap-x-3 rounded-md p-2 text-sm font-semibold leading-6 text-gray-700 hover:bg-gray-50 hover:text-indigo-600">
<li className="group -mx-2 my-1 flex cursor-pointer gap-x-3 rounded-md p-1.5 text-sm font-semibold text-primary hover:bg-primary-foreground hover:text-primary-accent">
<MessageSquarePlus
className="h-6 w-6 shrink-0 text-gray-400 group-hover:text-indigo-600"
className="h-5 w-5 shrink-0 text-muted-foreground group-hover:text-primary-accent"
aria-hidden="true"
/>
Feedback
</li>
</FeedbackButtonWrapper>
<div className="mb-2 flex flex-row place-content-between items-center">
<div className="text-xs font-semibold leading-6 text-gray-400">
<div className="text-xs font-semibold text-muted-foreground">
Project
</div>
<NewProjectButton size="xs" />
@@ -334,9 +345,9 @@ export default function Layout(props: PropsWithChildren) {
</nav>
<Menu as="div" className="relative">
<Menu.Button className="flex w-full items-center gap-x-4 overflow-hidden p-1.5 py-3 pl-6 pr-10 text-sm font-semibold leading-6 text-gray-900 hover:bg-gray-50">
<Menu.Button className="flex w-full items-center gap-x-2 overflow-hidden p-1.5 py-3 pl-6 pr-8 text-sm font-semibold text-primary hover:bg-primary-foreground">
<span className="sr-only">Open user menu</span>
<Avatar className="h-8 w-8">
<Avatar className="h-7 w-7">
<AvatarImage src={session.data?.user?.image ?? undefined} />
<AvatarFallback>
{session.data?.user?.name
@@ -348,12 +359,12 @@ export default function Layout(props: PropsWithChildren) {
: null}
</AvatarFallback>
</Avatar>
<span className="flex-shrink truncate text-sm font-semibold leading-6 text-gray-900">
<span className="flex-shrink truncate text-sm font-semibold text-primary">
{session.data?.user?.name}
</span>
<div className="flex-1" />
<ChevronDownIcon
className="h-5 w-5 text-gray-400"
className="h-5 w-5 text-muted-foreground"
aria-hidden="true"
/>
</Menu.Button>
@@ -366,8 +377,8 @@ export default function Layout(props: PropsWithChildren) {
leaveFrom="transform opacity-100 scale-100"
leaveTo="transform opacity-0 scale-95"
>
<Menu.Items className="absolute -top-full right-0 z-10 mt-2.5 rounded-md bg-white py-2 shadow-lg ring-1 ring-gray-900/5 focus:outline-none">
<span className="mb-1 block border-b px-3 pb-2 text-sm leading-6 text-gray-500">
<Menu.Items className="absolute -top-full bottom-1 right-0 z-10 overflow-hidden rounded-md bg-background py-2 shadow-lg ring-1 ring-border focus:outline-none">
<span className="block border-b px-3 pb-2 text-sm leading-6 text-muted-foreground">
{session.data?.user?.email}
</span>
{userNavigation.map((item) => (
@@ -376,11 +387,12 @@ export default function Layout(props: PropsWithChildren) {
<a
onClick={() => void item.onClick()}
className={cn(
active ? "bg-gray-50" : "",
"block cursor-pointer px-3 py-1 text-sm leading-6 text-gray-900",
active ? "bg-primary-foreground" : "",
"flex cursor-pointer items-center justify-between px-2 py-0.5 text-sm leading-6 text-primary",
)}
>
{item.name}
{item.content}
</a>
)}
</Menu.Item>
@@ -391,14 +403,14 @@ export default function Layout(props: PropsWithChildren) {
</div>
</div>
<div className="sticky top-0 z-40 flex items-center gap-x-6 bg-white px-4 py-4 shadow-sm sm:px-6 lg:hidden">
<div className="sticky top-0 z-40 flex items-center gap-x-6 bg-background px-4 py-4 shadow-sm sm:px-6 lg:hidden">
<button
type="button"
className="-m-2.5 p-2.5 text-gray-700 lg:hidden"
className="-m-2.5 p-2.5 text-primary lg:hidden"
onClick={() => setSidebarOpen(true)}
>
<span className="sr-only">Open sidebar</span>
<Bars3Icon className="h-6 w-6" aria-hidden="true" />
<Bars3Icon className="h-5 w-5" aria-hidden="true" />
</button>
<LangfuseLogo
version
@@ -406,7 +418,7 @@ export default function Layout(props: PropsWithChildren) {
showEnvLabel={session.data?.user?.email?.endsWith("@langfuse.com")}
/>
<Menu as="div" className="relative">
<Menu.Button className="flex items-center gap-x-4 text-sm font-semibold leading-6 text-gray-900">
<Menu.Button className="flex items-center gap-x-4 text-sm font-semibold text-primary">
<span className="sr-only">Open user menu</span>
<Avatar className="h-8 w-8">
<AvatarImage src={session.data?.user?.image ?? undefined} />
@@ -430,8 +442,8 @@ export default function Layout(props: PropsWithChildren) {
leaveFrom="transform opacity-100 scale-100"
leaveTo="transform opacity-0 scale-95"
>
<Menu.Items className="absolute right-0 z-10 mt-2.5 rounded-md bg-white py-2 shadow-lg ring-1 ring-gray-900/5 focus:outline-none">
<span className="mb-1 block border-b px-3 pb-2 text-sm leading-6 text-gray-500">
<Menu.Items className="absolute right-0 z-10 mt-2.5 rounded-md bg-background py-2 pb-1 shadow-lg ring-1 ring-border focus:outline-none">
<span className="mb-1 block border-b px-3 pb-2 text-sm leading-6 text-muted-foreground">
{session.data?.user?.email}
</span>
{userNavigation.map((item) => (
@@ -440,11 +452,12 @@ export default function Layout(props: PropsWithChildren) {
<a
onClick={() => void item.onClick()}
className={cn(
active ? "bg-gray-50" : "",
"block cursor-pointer px-3 py-1 text-sm leading-6 text-gray-900",
active ? "bg-primary-foreground" : "",
"flex cursor-pointer items-center justify-between px-2 py-1 text-sm leading-6 text-primary",
)}
>
{item.name}
{item.content}
</a>
)}
</Menu.Item>
@@ -453,13 +466,13 @@ export default function Layout(props: PropsWithChildren) {
</Transition>
</Menu>
</div>
<div className="lg:pl-60">
<div className="lg:pl-56">
{env.NEXT_PUBLIC_DEMO_PROJECT_ID &&
projectId === env.NEXT_PUBLIC_DEMO_PROJECT_ID &&
(env.NEXT_PUBLIC_LANGFUSE_CLOUD_REGION === "STAGING" ||
env.NEXT_PUBLIC_LANGFUSE_CLOUD_REGION === "EU") &&
!session.data?.user?.email?.endsWith("@langfuse.com") ? (
<div className="flex w-full items-center border-b border-yellow-500 bg-yellow-100 px-4 py-2 lg:sticky lg:top-0 lg:z-40">
<div className="flex w-full items-center border-b border-dark-yellow bg-light-yellow px-4 py-2 lg:sticky lg:top-0 lg:z-40">
<div className="flex flex-1 flex-wrap gap-1">
<div className="flex items-center gap-1">
<Info className="h-4 w-4" />
@@ -523,9 +536,9 @@ const MainNavigation: React.FC<{
href={item.href}
className={clsx(
item.current
? "bg-gray-50 text-indigo-600"
: "text-gray-700 hover:bg-gray-50 hover:text-indigo-600",
"group flex gap-x-3 rounded-md p-2 text-sm font-semibold leading-6",
? "bg-primary-foreground text-primary-accent"
: "text-primary hover:bg-primary-foreground hover:text-primary-accent",
"group flex gap-x-3 rounded-md p-2 text-sm font-semibold",
)}
onClick={onNavitemClick}
target={item.newTab ? "_blank" : undefined}
@@ -534,9 +547,9 @@ const MainNavigation: React.FC<{
<item.icon
className={clsx(
item.current
? "text-indigo-600"
: "text-gray-400 group-hover:text-indigo-600",
"h-6 w-6 shrink-0",
? "text-primary-accent"
: "text-muted-foreground group-hover:text-primary-accent",
"h-5 w-5 shrink-0",
)}
aria-hidden="true"
/>
@@ -545,10 +558,10 @@ const MainNavigation: React.FC<{
{item.label && (
<span
className={cn(
"self-center whitespace-nowrap break-keep rounded-sm border px-1 py-0.5 text-xs",
"-my-0.5 self-center whitespace-nowrap break-keep rounded-sm border px-1 py-0.5 text-xs",
item.current
? "border-indigo-600 text-indigo-600"
: "border-gray-200 text-gray-400 group-hover:border-indigo-600 group-hover:text-indigo-600",
? "border-primary-accent text-primary-accent"
: "border-border text-muted-foreground group-hover:border-primary-accent group-hover:text-primary-accent",
)}
>
{item.label}
@@ -565,12 +578,12 @@ const MainNavigation: React.FC<{
{({ open }) => (
<>
<Disclosure.Button
className="group flex w-full items-center gap-x-3 rounded-md p-2 text-left text-sm font-semibold leading-6 hover:bg-gray-50 hover:text-indigo-600"
className="group flex w-full items-center gap-x-3 rounded-md p-2 text-left text-sm font-semibold hover:bg-primary-foreground hover:text-primary-accent"
onClick={() => setIsOpen(!isOpen)}
>
{item.icon && (
<item.icon
className="h-6 w-6 shrink-0 text-gray-400 group-hover:text-indigo-600"
className="h-5 w-5 shrink-0 text-muted-foreground group-hover:text-primary-accent"
aria-hidden="true"
/>
)}
@@ -578,10 +591,10 @@ const MainNavigation: React.FC<{
{item.label && (
<span
className={cn(
"self-center whitespace-nowrap break-keep rounded-sm border px-1 py-0.5 text-xs",
"-my-0.5 self-center whitespace-nowrap break-keep rounded-sm border px-1 py-0.5 text-xs",
item.current
? "border-indigo-600 text-indigo-600"
: "border-gray-200 text-gray-400 group-hover:border-indigo-600 group-hover:text-indigo-600",
? "border-primary-accent text-primary-accent"
: "border-border text-muted-foreground group-hover:border-primary-accent group-hover:text-primary-accent",
)}
>
{item.label}
@@ -589,13 +602,15 @@ const MainNavigation: React.FC<{
)}
<ChevronRightIcon
className={clsx(
open ? "rotate-90 text-gray-500" : "text-gray-400",
open
? "rotate-90 text-muted-foreground"
: "text-muted-foreground",
"ml-auto h-5 w-5 shrink-0",
)}
aria-hidden="true"
/>
</Disclosure.Button>
<Disclosure.Panel as="ul" className="mt-1 px-2">
<Disclosure.Panel as="ul" className="mt-1 space-y-1 px-2">
{item.children?.map((subItem) => (
<li key={subItem.name}>
{/* 44px */}
@@ -603,15 +618,15 @@ const MainNavigation: React.FC<{
href={subItem.href ?? "#"}
className={clsx(
subItem.current
? "bg-gray-50 text-indigo-600"
: "text-gray-700 hover:bg-gray-50 hover:text-indigo-600",
"flex w-full items-center gap-x-3 rounded-md py-2 pl-9 pr-2 text-sm leading-6",
? "bg-primary-foreground text-primary-accent"
: "text-primary hover:bg-primary-foreground hover:text-primary-accent",
"ml-0.5 flex w-full items-center gap-x-3 rounded-md p-1.5 pl-7 pr-2 text-sm",
)}
target={subItem.newTab ? "_blank" : undefined}
>
{subItem.name}
{subItem.label && (
<span className="self-center whitespace-nowrap break-keep rounded-sm border border-gray-200 px-1 py-0.5 text-xs text-gray-400 group-hover:border-indigo-600 group-hover:text-indigo-600">
<span className="self-center whitespace-nowrap break-keep rounded-sm border border-border px-1 py-0.5 text-xs text-muted-foreground group-hover:border-primary-accent group-hover:text-primary-accent">
{subItem.label}
</span>
)}
+1 -1
View File
@@ -5,7 +5,7 @@ export function Spinner(props: { message: string }) {
<div className="flex min-h-full flex-1 flex-col justify-center py-12 sm:px-6 lg:px-8">
<div className="sm:mx-auto sm:w-full sm:max-w-md">
<LangfuseIcon className="mx-auto motion-safe:animate-spin" size={42} />
<h2 className="mt-5 text-center text-2xl font-bold leading-9 tracking-tight text-gray-900">
<h2 className="mt-5 text-center text-2xl font-bold leading-9 tracking-tight text-primary">
{props.message} ...
</h2>
</div>
+10 -10
View File
@@ -11,22 +11,22 @@ export type Status =
(typeof statusCategories)[keyof typeof statusCategories][number];
export const StatusBadge = (props: { className?: string; type: Status }) => {
let badgeColor = "bg-gray-100 text-gray-800";
let dotColor = "bg-gray-500";
let dotPingColor = "bg-gray-600";
let badgeColor = "bg-muted-gray text-primary";
let dotColor = "bg-muted-foreground";
let dotPingColor = "bg-muted-foreground";
let showDot = true;
if (statusCategories.active.includes(props.type)) {
badgeColor = "bg-green-100 text-green-600";
dotColor = "animate-ping bg-green-500";
dotPingColor = "bg-green-600";
badgeColor = "bg-light-green text-dark-green";
dotColor = "animate-ping bg-dark-green";
dotPingColor = "bg-dark-green";
} else if (statusCategories.error.includes(props.type)) {
badgeColor = "bg-red-100 text-red-600";
dotColor = "bg-red-500";
dotPingColor = "bg-red-600";
badgeColor = "bg-light-red text-dark-red";
dotColor = "animate-ping bg-dark-red";
dotPingColor = "bg-dark-red";
showDot = false;
} else if (statusCategories.completed.includes(props.type)) {
badgeColor = "bg-green-100 text-green-600";
badgeColor = "bg-light-green text-dark-green";
showDot = false;
}
+3 -3
View File
@@ -1,6 +1,6 @@
export const LevelColors = {
DEFAULT: { text: "", bg: "" },
DEBUG: { text: "text-gray-500", bg: "bg-gray-50" },
WARNING: { text: "text-yellow-800", bg: "bg-yellow-50" },
ERROR: { text: "text-red-800", bg: "bg-red-50" },
DEBUG: { text: "text-muted-foreground", bg: "bg-primary-foreground" },
WARNING: { text: "text-dark-yellow", bg: "bg-light-yellow" },
ERROR: { text: "text-dark-red", bg: "bg-light-red" },
};
+6 -6
View File
@@ -32,9 +32,9 @@ export const ProjectNavigation: React.FC<ProjectNavigationProps> = ({
router.push(`/project/${value}`);
}}
>
<SelectTrigger className="text-gray-700 ring-transparent focus:ring-0 focus:ring-offset-0">
<SelectTrigger className="h-8 text-primary ring-transparent focus:ring-0 focus:ring-offset-0">
<SelectValue
className="text-sm font-semibold text-gray-700"
className="text-sm font-semibold text-primary"
placeholder={currentProjectId}
/>
</SelectTrigger>
@@ -46,8 +46,8 @@ export const ProjectNavigation: React.FC<ProjectNavigationProps> = ({
className={cn(
"truncate",
currentProjectId === project.id
? "border-gray-700 text-gray-700"
: "text-gray-400",
? "border-primary text-primary"
: "text-muted-foreground",
)}
>
{project.name}
@@ -57,8 +57,8 @@ export const ProjectNavigation: React.FC<ProjectNavigationProps> = ({
className={cn(
"self-center whitespace-nowrap break-keep rounded-sm border px-1 py-0.5 text-xs",
currentProjectId === project.id
? "border-gray-700 text-gray-700"
: "border-gray-200 text-gray-400 group-hover:border-gray-700 group-hover:text-gray-700",
? "border-primary text-primary"
: "border-border text-muted-foreground group-hover:border-primary group-hover:text-primary",
)}
>
view-only
+1 -1
View File
@@ -93,7 +93,7 @@ const Base = (props: {
"Loading.."
) : props.isPublic ? (
<div
className="flex cursor-pointer items-center gap-1 text-green-800"
className="text-dark-green flex cursor-pointer items-center gap-1"
onClick={() => copyUrl()}
>
{isCopied ? "Link copied ..." : "Public"}
+8 -6
View File
@@ -7,7 +7,6 @@ import { IOPreview } from "@/src/components/trace/IOPreview";
import { JsonSkeleton } from "@/src/components/ui/CodeJsonViewer";
import { Badge } from "@/src/components/ui/badge";
import { Card } from "@/src/components/ui/card";
import { ManualScoreButton } from "@/src/features/manual-scoring/components/ManualScoreButton";
import { DetailPageNav } from "@/src/features/navigate-detail-pages/DetailPageNav";
import { useDetailPageLists } from "@/src/features/navigate-detail-pages/context";
import { api } from "@/src/utils/api";
@@ -22,6 +21,7 @@ import {
useRowHeightLocalStorage,
} from "@/src/components/table/data-table-row-height-switch";
import { ScrollArea } from "@/src/components/ui/scroll-area";
import { AnnotateButton } from "@/src/features/manual-scoring/components/AnnotateButton";
// do not use the usual table row heights here
const rowHeightMapping: Record<RowHeight, number> = {
@@ -171,7 +171,7 @@ const TraceCardList = ({
}))
.map(({ virtualItem, trace }) => (
<Card
className="border-border-gray-150 group grid w-full gap-3 overflow-hidden p-2 shadow-none hover:border-gray-300 md:grid-cols-3"
className="group grid w-full gap-3 overflow-hidden border-border p-2 shadow-none hover:border-ring md:grid-cols-3"
key={virtualItem.key}
data-index={virtualItem.index}
style={{
@@ -193,14 +193,16 @@ const TraceCardList = ({
>
Trace: {trace.name} ({trace.id})&nbsp;
</Link>
<div className="text-xs text-gray-500">
<div className="text-xs text-muted-foreground">
{trace.timestamp.toLocaleString()}
</div>
<div className="mb-1 mt-2 text-xs text-gray-500">Scores</div>
<div className="mb-1 mt-2 text-xs text-muted-foreground">
Scores
</div>
<div className="flex flex-wrap content-start items-start gap-1">
<GroupedScoreBadges scores={trace.scores} />
</div>
<ManualScoreButton
<AnnotateButton
projectId={projectId}
traceId={trace.id}
scores={trace.scores}
@@ -244,7 +246,7 @@ const SessionIO = ({ traceId }: { traceId: string }) => {
hideIfNull
/>
) : (
<div className="p-2 text-xs text-gray-500">
<div className="p-2 text-xs text-muted-foreground">
This trace has no input or output.
</div>
)}
+4 -3
View File
@@ -7,6 +7,7 @@ import { cn } from "@/src/utils/tailwind";
import { type RouterOutput, type RouterInput } from "@/src/utils/types";
import { useState } from "react";
import { usePostHogClientCapture } from "@/src/features/posthog-analytics/usePostHogClientCapture";
import { trpcErrorToast } from "@/src/utils/trpcErrorToast";
export function StarToggle({
value,
@@ -32,7 +33,7 @@ export function StarToggle({
<StarIcon
className={cn(
"h-4 w-4",
value ? "fill-current text-yellow-500" : "text-gray-500",
value ? "fill-current text-yellow-500" : "text-muted-foreground",
)}
/>
</Button>
@@ -76,7 +77,7 @@ export function StarTraceToggle({
onError: (err, _newTodo, context) => {
setIsLoading(false);
// Rollback to the previous value if mutation fails
console.log("error", err);
trpcErrorToast(err);
utils.traces.all.setData(tracesFilter, context?.prev);
},
onSettled: () => {
@@ -157,7 +158,7 @@ export function StarTraceDetailsToggle({
},
onError: (err, _newTodo, context) => {
setIsLoading(false);
console.log("error", err);
trpcErrorToast(err);
// Rollback to the previous value if mutation fails
utils.traces.byId.setData({ traceId }, context?.prevData);
},
+4 -4
View File
@@ -6,19 +6,19 @@ interface Statistic {
export default function StatsCards({ stats }: { stats: Statistic[] }) {
return (
<div>
<h3 className="text-base font-semibold leading-6 text-gray-900">
<h3 className="text-base font-semibold leading-6 text-primary">
Model configuration
</h3>
<dl className="mt-5 grid grid-cols-1 gap-5 sm:grid-cols-4">
{stats.map((item) => (
<div
key={item.name}
className="overflow-hidden rounded-lg bg-white px-4 py-5 shadow sm:p-6"
className="overflow-hidden rounded-lg bg-background px-4 py-5 shadow sm:p-6"
>
<dt className="truncate text-sm font-medium text-gray-500">
<dt className="truncate text-sm font-medium text-muted-foreground">
{item.name}
</dt>
<dd className="mt-1 text-3xl font-semibold tracking-tight text-gray-900">
<dd className="mt-1 text-3xl font-semibold tracking-tight text-primary">
{item.stat}
</dd>
</div>
@@ -83,7 +83,7 @@ export function DataTableColumnVisibilityFilter<TData, TValue>({
>
<Button variant="outline" title="Show/hide columns">
<Columns className="mr-2 h-4 w-4" />
<span className="text-xs text-gray-500">{`(${count}/${total})`}</span>
<span className="text-xs text-muted-foreground">{`(${count}/${total})`}</span>
<ChevronDown className="ml-2 h-4 w-4" />
</Button>
</DropdownMenuTrigger>
@@ -55,7 +55,7 @@ export const DataTableRowHeightSwitch = ({
<TabsTrigger
key={id}
value={id}
className="px-2 shadow-none data-[state=active]:bg-slate-200 data-[state=active]:ring-border"
className="px-2 shadow-none data-[state=active]:bg-input data-[state=active]:ring-border"
>
<span role="img" aria-label={`${label} size`}>
{icon}
+23 -4
View File
@@ -12,7 +12,6 @@ import {
Table,
TableBody,
TableCell,
TableFooter,
TableHead,
TableHeader,
TableRow,
@@ -51,6 +50,8 @@ interface DataTableProps<TData, TValue> {
help?: { description: string; href: string };
rowHeight?: RowHeight;
className?: string;
paginationClassName?: string;
isBorderless?: boolean;
}
export interface AsyncTableData<T> {
@@ -72,6 +73,9 @@ export function DataTable<TData extends object, TValue>({
orderBy,
setOrderBy,
rowHeight,
className,
paginationClassName,
isBorderless = false,
}: DataTableProps<TData, TValue>) {
const [columnFilters, setColumnFilters] = useState<ColumnFiltersState>([]);
const rowheighttw = getRowHeightTailwindClass(rowHeight);
@@ -106,8 +110,18 @@ export function DataTable<TData extends object, TValue>({
return (
<>
<div className="flex w-full max-w-full flex-1 flex-col gap-1 overflow-auto">
<div className="w-full overflow-auto rounded-md border">
<div
className={cn(
"flex w-full max-w-full flex-1 flex-col gap-1 overflow-auto",
className,
)}
>
<div
className={cn(
"w-full overflow-auto",
isBorderless ? "" : "rounded-md border",
)}
>
<Table>
<TableHeader>
{table.getHeaderGroups().map((headerGroup) => (
@@ -243,7 +257,12 @@ export function DataTable<TData extends object, TValue>({
<div className="grow"></div>
</div>
{pagination !== undefined ? (
<div className="sticky bottom-0 z-10 flex w-full justify-end bg-white font-medium">
<div
className={cn(
"sticky bottom-0 z-10 flex w-full justify-end bg-background font-medium",
paginationClassName,
)}
>
<DataTablePagination
table={table}
paginationOptions={pagination.options}
+2 -1
View File
@@ -19,7 +19,8 @@ export default function TableLink({
: value;
return (
<Link
className="inline-block rounded bg-indigo-50 px-2 py-1 text-xs font-semibold text-blue-600 shadow-sm hover:bg-indigo-100"
className="inline-block rounded bg-primary-accent/20 px-2
py-1 text-xs font-semibold text-accent-dark-blue shadow-sm hover:bg-accent-light-blue/45"
href={path}
title={value}
>
@@ -21,11 +21,7 @@ import {
withDefault,
} from "use-query-params";
import { useQueryFilterState } from "@/src/features/filters/hooks/useFilterState";
import {
formatIntervalSeconds,
intervalInSeconds,
utcDateOffsetByDays,
} from "@/src/utils/dates";
import { formatIntervalSeconds, utcDateOffsetByDays } from "@/src/utils/dates";
import useColumnVisibility from "@/src/features/column-visibility/hooks/useColumnVisibility";
import { type LangfuseColumnDef } from "@/src/components/table/types";
import {
@@ -59,6 +55,7 @@ export type GenerationsTableRow = {
endTime?: string;
completionStartTime?: Date;
latency?: number;
timeToFirstToken?: number;
name?: string;
model?: string;
// i/o not set explicitly, but fetched from the server from the cell
@@ -118,7 +115,7 @@ export default function GenerationsTable({
type: "datetime",
operator: ">",
value: utcDateOffsetByDays(
session.data?.environment.defaultTableDateTimeOffset ?? -14,
session.data?.environment.defaultTableDateTimeOffset ?? -7,
),
},
],
@@ -310,21 +307,12 @@ export default function GenerationsTable({
enableHiding: true,
enableSorting: true,
cell: ({ row }) => {
const startTime: Date = row.getValue("startTime");
const completionStartTime: Date | undefined =
const timeToFirstToken: number | undefined =
row.getValue("timeToFirstToken");
if (!completionStartTime) {
return undefined;
}
const latencyInSeconds =
intervalInSeconds(startTime, completionStartTime) || "-";
return (
<span>
{typeof latencyInSeconds === "number"
? formatIntervalSeconds(latencyInSeconds)
: latencyInSeconds}
{timeToFirstToken ? formatIntervalSeconds(timeToFirstToken) : "-"}
</span>
);
},
@@ -623,7 +611,7 @@ export default function GenerationsTable({
traceName: generation.traceName ?? "",
startTime: generation.startTime,
endTime: generation.endTime?.toLocaleString() ?? undefined,
timeToFirstToken: generation.completionStartTime ?? undefined,
timeToFirstToken: generation.timeToFirstToken ?? undefined,
latency: generation.latency ?? undefined,
totalCost: generation.calculatedTotalCost ?? undefined,
inputCost: generation.calculatedInputCost ?? undefined,
@@ -758,7 +746,7 @@ const GenerationsIOCell = ({
data={
io === "output" ? observation.data?.output : observation.data?.input
}
className={cn(io === "output" && "bg-green-50")}
className={cn(io === "output" && "bg-accent-light-green")}
singleLine={singleLine}
/>
);
@@ -111,7 +111,7 @@ export default function ModelTable({ projectId }: { projectId: string }) {
return (
<>
Input Price{" "}
<span className="text-xs text-gray-400">/ 1k units</span>
<span className="text-xs text-muted-foreground">/ 1k units</span>
</>
);
},
@@ -140,7 +140,7 @@ export default function ModelTable({ projectId }: { projectId: string }) {
return (
<>
Output Price{" "}
<span className="text-xs text-gray-400">/ 1k units</span>
<span className="text-xs text-muted-foreground">/ 1k units</span>
</>
);
},
@@ -163,7 +163,7 @@ export default function ModelTable({ projectId }: { projectId: string }) {
return (
<>
Total Price{" "}
<span className="text-xs text-gray-400">/ 1k units</span>
<span className="text-xs text-muted-foreground">/ 1k units</span>
</>
);
},

Some files were not shown because too many files have changed in this diff Show More