Compare commits

...
148 Commits
Author SHA1 Message Date
Hassieb Pakzad cf42dd726d chore: release v2.49.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-10 16:22:01 +02:00
Hassieb PakzadandGitHub 8aeec63c07 perf: use raw query for trace duration on dataset run table (#2286) 2024-06-10 16:19:31 +02:00
Hassieb Pakzad 0cd3fcfbeb chore: release v2.49.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-06-10 12:57:14 +02:00
Hassieb PakzadandGitHub a880bdb3b2 perf: boost observationCounts on prompts table (#2284) 2024-06-10 12:48:39 +02:00
Max DeichmannandGitHub 5c6d3e14f2 refactor: add tokenisation to worker (#2281) 2024-06-09 11:48:21 +00:00
Max DeichmannandGitHub 6ccc57312c fix: shared dependencies and improve docker ci (#2273) 2024-06-07 18:45:05 +02:00
Max DeichmannandGitHub c4831d1b25 fix: add lodash to worker (#2270) 2024-06-07 15:45:15 +02:00
Marc Klingen cc08408f0a fix(ui): show title when trying to deleting built-in models 2024-06-07 02:57:18 +02:00
6a41aadc98 fix(ui): handle nested menu states in sidebar (#2230)
* chore: update MainNavigation component to use nested navigation literals

* simplify

---------

Co-authored-by: Marc Klingen <git@marcklingen.com>
2024-06-06 22:42:51 +00:00
9d6efbae19 fix(ui): improve ux across delete buttons (#2258)
* improve ux across delete buttons

* updated delete button on models screen

* trpc errors are logged to console by default

* revert to pointer-events-none on disabled button

---------

Co-authored-by: Mish Ushakov <mishushakov@users.noreply.github.com>
Co-authored-by: Marc Klingen <git@marcklingen.com>
2024-06-07 00:26:04 +02:00
Max DeichmannandGitHub 74be1e87c1 refactor: move ingestion types to shared (#2264)
push
2024-06-06 17:56:35 +00:00
Hassieb Pakzad dd823da1da chore: release v2.49.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-06 17:52:18 +02:00
992f0affbc chore: remove trace fkey constraint from datasetrunitems (#2262)
Co-authored-by: Marc Klingen <git@marcklingen.com>
2024-06-06 17:49:17 +02:00
Mish UshakovandGitHub b8c192324c docs: Updated contributing guide (#2259) 2024-06-06 14:28:57 +02:00
Marc KlingenandGitHub 1416e760ff chore: remove dangling worker/generated/types.ts (#2257) 2024-06-06 14:11:01 +02:00
Marc KlingenandGitHub 4266b46dd9 chore(db): drop legacy pricings table, replaced by models (#2256) 2024-06-06 11:36:08 +02:00
Marc KlingenandGitHub 1933f8f201 feat: add token/character count support for gemini-1.5-flash and gemini-1.5-pro (#2255) 2024-06-06 11:21:42 +02:00
Marc KlingenandGitHub 1e35eb3a66 fix(ui): encode prompt name on link from traces (#2253) 2024-06-06 08:30:52 +00:00
Marc KlingenandGitHub 5478c5e317 feat(ui): use dropdownmenu for version modal (#2252) 2024-06-06 08:20:01 +00:00
Marc KlingenandGitHub 140452c89e feat(ui): add upgrade popover to version number (#2251) 2024-06-06 00:48:23 +00:00
Marc KlingenandGitHub 7ac92e83da feat(ui): use pagination instead of virtualization on sessions view (#2248) 2024-06-05 22:21:01 +00:00
Marc Klingen 8a65bad7d4 chore: release v2.48.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-06-05 23:04:25 +02:00
Marc Klingen 6c6348bfbe chore(rbac): grant new scoreConfigs:CUD scope to MEMBER role 2024-06-05 23:02:42 +02:00
Marlies Mayerhofer 9ff7fc942d fix: add maintainers heading in contributing file 2024-06-05 22:53:30 +02:00
Marlies Mayerhofer a3f59e7e95 feat: enhance robustness of multi select item search 2024-06-05 22:46:48 +02:00
Max DeichmannandGitHub 445bdcb65c chore: log level worker (#2246)
* push

* push
2024-06-05 20:34:12 +02:00
Max DeichmannandGitHub 8230aa79b3 chore: remove comments (#2245) 2024-06-05 20:11:10 +02:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
57ca7fb478 chore(deps-dev): bump @typescript-eslint/parser from 7.7.0 to 7.12.0 (#2222)
Bumps [@typescript-eslint/parser](https://github.com/typescript-eslint/typescript-eslint/tree/HEAD/packages/parser) from 7.7.0 to 7.12.0.
- [Release notes](https://github.com/typescript-eslint/typescript-eslint/releases)
- [Changelog](https://github.com/typescript-eslint/typescript-eslint/blob/main/packages/parser/CHANGELOG.md)
- [Commits](https://github.com/typescript-eslint/typescript-eslint/commits/v7.12.0/packages/parser)

---
updated-dependencies:
- dependency-name: "@typescript-eslint/parser"
  dependency-type: direct:development
  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-06-05 20:05:17 +02:00
marliessophieandGitHub 2db88101e1 feat: add score and score config user tracking events (#2240)
* feat: add score and score config user tracking events

* add: icon for annotation button
2024-06-05 13:12:22 +00:00
Hassieb PakzadandGitHub 669167002a fix: parse object type message content (#2239) 2024-06-05 12:48:30 +00:00
marliessophieandGitHub 2f6ade9354 fix(annotation): improve keyboard navigation experience in drawer (#2236) 2024-06-05 13:41:04 +02:00
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
Marc Klingen a7131dc422 chore: release v2.43.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-05-17 11:13:57 +02:00
Marc KlingenandGitHub bb73243a04 fix(ui): use svh for fullscreen pages on mobile (#2076) 2024-05-17 11:12:00 +02:00
Marc Klingen e543c7dc27 feat(auth): capture unexpected credential sign-in errors 2024-05-17 10:42:11 +02:00
Marc KlingenandGitHub a2b6b7a14b feat(cloud): data region select and info modal (#2085) 2024-05-17 10:17:37 +02:00
Johannes GoslarandGitHub e0930fbf46 intercept OpenAI requests in worker testruns (#2020) 2024-05-16 21:12:30 +00:00
Marc KlingenandGitHub 125aa6b2c4 fix(api): align get/v2/prompts meta response with api spec (#2079) 2024-05-16 18:49:20 +00:00
Marc Klingen 3caf70bd2b chore: release v2.43.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-16 17:19:49 +02:00
Marc Klingen 4139ccec98 fix: hide LlmApiKeyList in settings when not on langfuse cloud as related features are in beta right now and will be released in v3 2024-05-16 17:19:23 +02:00
Marc Klingen 07671c76b4 chore: release v2.43.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-16 15:46:26 +02:00
Max DeichmannandGitHub 317608421f fix: remove encryption key (#2074) 2024-05-16 15:37:44 +02:00
10d84a4094 perf(scores): remove fk constraint from traceId and observationId on scores, change to projectId (#2012)
---------

Co-authored-by: Marlies Mayerhofer <74332854+marliessophie@users.noreply.github.com>
2024-05-16 12:33:02 +00:00
f623bd9f79 feat: allows disabling model params in playground and evals (#2062)
Co-authored-by: Max Deichmann <m.deichmann@tum.de>
2024-05-16 09:15:04 +01:00
Max Deichmann 08fb5f4000 chore: release v2.42.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-16 09:53:08 +02:00
marliessophieandGitHub 03d00c405c feat: add sticky headings to table component (#2053)
* feat: add sticky header and footer to table component
2024-05-16 09:45:27 +02:00
Marc Klingen 783f7a205f fix: default to height m for single sessions 2024-05-16 02:50:32 +02:00
Marc KlingenandGitHub 83359e6fc6 perf(ui): add virtualized single session view (#2066) 2024-05-16 02:29:59 +02:00
Marc Klingen edd1484c5a chore: release v2.41.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-15 03:09:47 +02:00
Marc KlingenandGitHub 410c91b464 chore: add additional tests to dataset item route (#2059) 2024-05-15 03:09:25 +02:00
Marc KlingenandGitHub 8962090404 fix(ui): null values in prompt metrics table (#2058) 2024-05-15 01:02:23 +00:00
Marc Klingen 4f88b78c71 chore: consolidate api types 2024-05-15 02:19:14 +02:00
Marc Klingen 8995b30959 chore: release v2.41.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-15 02:09:42 +02:00
Marc KlingenandGitHub 321a25cf13 feat(datasets): make status of dataset item editable via API (#2057) 2024-05-15 00:02:50 +00:00
2990767c4a feat(prompts): add table to compare metrics across prompt versions, [promptName]/metrics (#1991)
---------

Co-authored-by: Marc Klingen <git@marcklingen.com>
2024-05-14 20:09:18 +00:00
321 changed files with 9138 additions and 4143 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"
+20 -15
View File
@@ -40,19 +40,27 @@ jobs:
- name: Checkout
uses: actions/checkout@v3
- name: Build Web Docker image
uses: docker/build-push-action@v4
with:
context: .
file: ./web/Dockerfile
push: false
- name: Build and run both images from compose
run: |
docker compose -f docker-compose.build.yml up -d
sleep 5 # Wait for PostgreSQL to accept connections
- name: Build Worker Docker image
uses: docker/build-push-action@v4
with:
context: .
file: ./worker/Dockerfile
push: false
- name: Check server health
run: |
timeout 10 bash -c 'until curl -f http://localhost:3000/api/public/health; do sleep 2; done'
- name: Check worker health
run: |
timeout 10 bash -c 'until curl -f http://localhost:3030/api/health; do sleep 2; done'
- name: Ensure no unhealthy status
run: |
if docker-compose ps | grep "(unhealthy)"; then
echo "One or more services are unhealthy"
exit 1
else
echo "All services are healthy"
fi
tests-web:
runs-on: ubuntu-latest
@@ -109,9 +117,6 @@ jobs:
strategy:
matrix:
node-version: [20]
env:
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
ENCRYPTION_KEY: ${{ secrets.ENCRYPTION_KEY }}
steps:
- name: Set Swap Space
uses: pierotofy/set-swap-space@master
+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 }}
+81 -1
View File
@@ -191,6 +191,16 @@ Requirements
pnpm run dev
```
7. Open the web app in the browser:
http://localhost:3000
8. Log in as a test user (after you ran `db:seed` command):
Username: demo@langfuse.com
Password: password
## Monorepo quickstart
- Available packages and their dependencies
@@ -290,7 +300,77 @@ 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.
## Using secrets stored in 1Password
## 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.
## Maintainers
### Using secrets stored in 1Password
When applying changes to non-local environments, you may need to use secrets stored in 1Password. We use the 1Password CLI for this purpose.
+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
+10
View File
@@ -20,6 +20,11 @@ services:
- LANGFUSE_WORKER_HOST=${LANGFUSE_WORKER_HOST:-worker}
- LANGFUSE_WORKER_PASSWORD=${LANGFUSE_WORKER_PASSWORD:-mybasicauthsecret}
restart: always
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:3000/api/public/health"]
interval: 30s
timeout: 10s
retries: 3
worker:
build:
@@ -41,6 +46,11 @@ services:
- REDIS_AUTH=${REDIS_AUTH:-myredissecret}
- LANGFUSE_WORKER_PASSWORD=${LANGFUSE_WORKER_PASSWORD:-mybasicauthsecret}
restart: always
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:3030/api/health"]
interval: 30s
timeout: 10s
retries: 3
redis:
image: redis:7.2.4
+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
+1 -1
View File
@@ -33,7 +33,7 @@
"@repo/eslint-config": "*",
"@repo/typescript-config": "*",
"@types/node": "^20.11.29",
"@typescript-eslint/parser": "^7.7.0",
"@typescript-eslint/parser": "^7.12.0",
"eslint": "^8.57.0",
"eslint-config-prettier": "^9.1.0",
"eslint-config-standard": "^17.1.0",
+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:
@@ -32,3 +32,6 @@ types:
id:
type: optional<string>
docs: Dataset items are upserted on their id
status:
type: optional<commons.DatasetStatus>
docs: Defaults to ACTIVE for newly created items
+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.40.0",
"version": "2.49.2",
"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"
+1 -1
View File
@@ -9,7 +9,7 @@
],
"devDependencies": {
"@typescript-eslint/eslint-plugin": "^7.1.0",
"@typescript-eslint/parser": "^7.7.0",
"@typescript-eslint/parser": "^7.12.0",
"@vercel/style-guide": "^6.0.0",
"eslint-config-next": "^14.2.3",
"eslint-config-prettier": "^9.1.0",
+6 -5
View File
@@ -44,14 +44,15 @@
"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",
"kysely": "^0.27.3",
"langchain": "^0.1.36",
"lodash": "^4.17.21",
"prisma-extension-kysely": "^2.1.0",
"zod": "^3.22.4",
"zod-to-json-schema": "^3.22.5"
@@ -59,11 +60,11 @@
"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",
"@typescript-eslint/parser": "^7.12.0",
"eslint": "^8.57.0",
"eslint-config-prettier": "^9.1.0",
"eslint-config-standard": "^17.1.0",
+29 -21
View File
@@ -25,22 +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 PricingUnit = {
PER_1000_TOKENS: "PER_1000_TOKENS",
PER_1000_CHARS: "PER_1000_CHARS"
export const ScoreDataType = {
CATEGORICAL: "CATEGORICAL",
NUMERIC: "NUMERIC",
BOOLEAN: "BOOLEAN"
} as const;
export type PricingUnit = (typeof PricingUnit)[keyof typeof PricingUnit];
export const TokenType = {
PROMPT: "PROMPT",
COMPLETION: "COMPLETION",
TOTAL: "TOTAL"
} as const;
export type TokenType = (typeof TokenType)[keyof typeof TokenType];
export type ScoreDataType = (typeof ScoreDataType)[keyof typeof ScoreDataType];
export const DatasetStatus = {
ACTIVE: "ACTIVE",
ARCHIVED: "ARCHIVED"
@@ -246,6 +241,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 +289,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;
@@ -302,14 +299,6 @@ export type PosthogIntegration = {
enabled: boolean;
created_at: Generated<Timestamp>;
};
export type Pricing = {
id: string;
model_name: string;
pricing_unit: Generated<PricingUnit>;
price: string;
currency: Generated<string>;
token_type: TokenType;
};
export type Project = {
id: string;
created_at: Generated<Timestamp>;
@@ -342,13 +331,32 @@ export type Prompt = {
export type Score = {
id: string;
timestamp: Generated<Timestamp>;
project_id: string | null;
project_id: string;
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;
@@ -444,10 +452,10 @@ export type DB = {
observations: Observation;
observations_view: ObservationView;
posthog_integrations: PosthogIntegration;
pricings: Pricing;
project_memberships: ProjectMembership;
projects: Project;
prompts: Prompt;
score_configs: ScoreConfig;
scores: Score;
Session: Session;
sso_configs: SsoConfig;
@@ -0,0 +1,5 @@
-- DropForeignKey
ALTER TABLE "scores" DROP CONSTRAINT "scores_observation_id_fkey";
-- DropForeignKey
ALTER TABLE "scores" DROP CONSTRAINT "scores_trace_id_fkey";
@@ -0,0 +1,11 @@
/*
Warnings:
- Made the column `project_id` on table `scores` required. This step will fail if there are existing NULL values in that column.
*/
-- AlterTable
ALTER TABLE "scores" ALTER COLUMN "project_id" SET NOT NULL;
-- AddForeignKey
ALTER TABLE "scores" ADD CONSTRAINT "scores_project_id_fkey" FOREIGN KEY ("project_id") REFERENCES "projects"("id") ON DELETE CASCADE ON UPDATE CASCADE;
@@ -0,0 +1,8 @@
/*
Warnings:
- A unique constraint covering the columns `[id,project_id]` on the table `scores` will be added. If there are existing duplicate values, this will fail.
*/
-- DropIndex
DROP INDEX "scores_id_trace_id_key";
@@ -0,0 +1,8 @@
/*
Warnings:
- A unique constraint covering the columns `[id,project_id]` on the table `scores` will be added. If there are existing duplicate values, this will fail.
*/
-- CreateIndex
CREATE UNIQUE INDEX CONCURRENTLY "scores_id_project_id_key" ON "scores"("id", "project_id");
@@ -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;
@@ -0,0 +1,5 @@
-- does not include pricing yet, will be added as soon as it is calculated at ingestion time
INSERT INTO "public"."models" ("id", "model_name", "match_pattern", "unit") VALUES ('clx30djsn0000w9mzebiv41we', 'gemini-1.5-flash', '(?i)^(gemini-1.5-flash)(@[a-zA-Z0-9]+)?$', 'CHARACTERS');
INSERT INTO "public"."models" ("id", "model_name", "match_pattern", "unit") VALUES ('clx30hkrx0000w9mz7lqi0ial', 'gemini-1.5-pro', '(?i)^(gemini-1.5-pro)(@[a-zA-Z0-9]+)?$', 'CHARACTERS');
@@ -0,0 +1,14 @@
/*
Warnings:
- You are about to drop the `pricings` table. If the table is not empty, all the data it contains will be lost.
*/
-- DropTable
DROP TABLE "pricings";
-- DropEnum
DROP TYPE "PricingUnit";
-- DropEnum
DROP TYPE "TokenType";
@@ -0,0 +1,3 @@
-- DropForeignKey
ALTER TABLE "dataset_run_items" DROP CONSTRAINT "dataset_run_items_trace_id_fkey";
ALTER TABLE "dataset_run_items" DROP CONSTRAINT "dataset_run_items_observation_id_fkey";
+88 -50
View File
@@ -113,6 +113,8 @@ model Project {
JobExecution JobExecution[]
LlmApiKeys LlmApiKeys[]
PosthogIntegration PosthogIntegration[]
Score Score[]
scoreConfig ScoreConfig[]
@@map("projects")
}
@@ -204,6 +206,7 @@ model TraceSession {
@@id([id, projectId])
@@index([projectId])
@@index([createdAt])
@@index([updatedAt])
@@map("trace_sessions")
}
@@ -230,10 +233,8 @@ model Trace {
createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime @default(now()) @updatedAt @map("updated_at")
scores Score[]
DatasetRunItems DatasetRunItems[]
DatasetItem DatasetItem[]
JobExecution JobExecution[]
DatasetItem DatasetItem[]
JobExecution JobExecution[]
@@index([projectId])
@@index([sessionId])
@@ -276,6 +277,8 @@ view TraceView {
}
// Update ObservationView below when making changes to this model!
// traceId is optional only due to timing during data injestion
// (traceId is not necessarily known at the time of observation creation)
model Observation {
id String @id @default(cuid())
traceId String? @map("trace_id")
@@ -290,25 +293,24 @@ 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?
internalModel String? @map("internal_model")
internalModel String? @map("internal_model")
modelParameters Json?
input Json?
output Json?
promptTokens Int @default(0) @map("prompt_tokens")
completionTokens Int @default(0) @map("completion_tokens")
totalTokens Int @default(0) @map("total_tokens")
promptTokens Int @default(0) @map("prompt_tokens")
completionTokens Int @default(0) @map("completion_tokens")
totalTokens Int @default(0) @map("total_tokens")
unit String?
inputCost Decimal? @map("input_cost")
outputCost Decimal? @map("output_cost")
totalCost Decimal? @map("total_cost")
completionStartTime DateTime? @map("completion_start_time")
scores Score[]
project Project @relation(fields: [projectId], references: [id], onDelete: Cascade)
inputCost Decimal? @map("input_cost")
outputCost Decimal? @map("output_cost")
totalCost Decimal? @map("total_cost")
completionStartTime DateTime? @map("completion_start_time")
project Project @relation(fields: [projectId], references: [id], onDelete: Cascade)
derivedDatasetItems DatasetItem[]
datasetRunItems DatasetRunItems[]
promptId String? @map("prompt_id")
prompt Prompt? @relation(fields: [promptId], onDelete: SetNull, references: [id])
@@ -322,6 +324,7 @@ model Observation {
@@index([type])
@@index(startTime)
@@index(createdAt)
@@index(updatedAt)
@@index(projectId)
@@index(parentObservationId)
@@index(model)
@@ -372,6 +375,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")
}
@@ -392,54 +396,73 @@ enum ObservationLevel {
model Score {
id String @id @default(cuid())
timestamp DateTime @default(now())
projectId String? @map("project_id")
projectId String @map("project_id")
project Project @relation(fields: [projectId], references: [id], onDelete: Cascade)
name String
value Float
source ScoreSource
authorUserId String? @map("author_user_id")
comment String?
traceId String @map("trace_id")
trace Trace @relation(fields: [traceId], references: [id], onDelete: Cascade)
observationId String? @map("observation_id")
observation Observation? @relation(fields: [observationId], references: [id], onDelete: SetNull)
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, traceId]) // used for upsert
@@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
}
enum PricingUnit {
PER_1000_TOKENS
PER_1000_CHARS
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 TokenType {
PROMPT
COMPLETION
TOTAL
}
model Pricing {
id String @id @default(cuid())
modelName String @map("model_name")
pricingUnit PricingUnit @default(PER_1000_TOKENS) @map("pricing_unit")
price Decimal
currency String @default("USD")
tokenType TokenType @map("token_type")
@@index(modelName)
@@map("pricings")
enum ScoreDataType {
CATEGORICAL
NUMERIC
BOOLEAN
}
model CronJobs {
@@ -465,6 +488,8 @@ model Dataset {
@@unique([projectId, name])
@@index([projectId], type: Hash)
@@index([createdAt])
@@index([updatedAt])
@@map("datasets")
}
@@ -484,8 +509,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,26 +535,28 @@ model DatasetRuns {
@@unique([datasetId, name])
@@index([datasetId], type: Hash)
@@index([createdAt])
@@index([updatedAt])
@@map("dataset_runs")
}
model DatasetRunItems {
id String @id @default(cuid())
datasetRunId String @map("dataset_run_id")
datasetRun DatasetRuns @relation(fields: [datasetRunId], references: [id], onDelete: Cascade)
datasetItemId String @map("dataset_item_id")
datasetItem DatasetItem @relation(fields: [datasetItemId], references: [id], onDelete: Cascade)
traceId String @map("trace_id")
trace Trace @relation(fields: [traceId], references: [id], onDelete: Cascade)
observationId String? @map("observation_id")
observation Observation? @relation(fields: [observationId], references: [id], onDelete: Cascade)
createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime @default(now()) @updatedAt @map("updated_at")
id String @id @default(cuid())
datasetRunId String @map("dataset_run_id")
datasetRun DatasetRuns @relation(fields: [datasetRunId], references: [id], onDelete: Cascade)
datasetItemId String @map("dataset_item_id")
datasetItem DatasetItem @relation(fields: [datasetItemId], references: [id], onDelete: Cascade)
traceId String @map("trace_id")
observationId String? @map("observation_id")
createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime @default(now()) @updatedAt @map("updated_at")
@@index([datasetRunId], type: Hash)
@@index([datasetItemId], type: Hash)
@@index([observationId], type: Hash)
@@index([traceId])
@@index([createdAt])
@@index([updatedAt])
@@map("dataset_run_items")
}
@@ -569,6 +599,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 +648,7 @@ model AuditLog {
@@index([projectId])
@@index([createdAt])
@@index([updatedAt])
@@map("audit_logs")
}
@@ -709,6 +742,11 @@ model JobExecution {
@@index([projectId, status])
@@index([projectId, id])
@@index([projectId])
@@index([jobConfigurationId])
@@index([jobOutputScoreId])
@@index([jobInputTraceId])
@@index([createdAt])
@@index([updatedAt])
@@map("job_executions")
}
+139 -8
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
@@ -459,7 +488,7 @@ function createObjects(
const session =
Math.random() > 0.3
? {
id: `session-${i % 10}`,
id: `session-${i % 3}`,
projectId: projectId,
}
: undefined;
@@ -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;
@@ -592,7 +641,9 @@ function createObjects(
const model = models[Math.floor(Math.random() * models.length)];
const promptId =
promptIds.get(projectId)![
Math.floor(Math.random() * promptIds.get(projectId)!.length)
Math.floor(
Math.random() * Math.floor(promptIds.get(projectId)!.length / 2)
)
];
const generation = {
@@ -600,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,
@@ -741,6 +794,7 @@ function createObjects(
traces,
observations,
scores,
configs,
sessions: uniqueSessions,
events,
};
@@ -923,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;
}
+21 -21
View File
@@ -1,5 +1,5 @@
import z from "zod";
import { ModelProvider } from "../..";
import { ModelProvider, UIModelParams } from "../..";
export const langfuseObjects = [
"trace",
@@ -77,30 +77,30 @@ export const availableEvalVariables = [
},
];
export const evalLLMModels = [
export const evalLLMModels: UIModelParams[] = [
{
provider: ModelProvider.OpenAI,
model: "gpt-3.5-turbo",
temperature: 1,
maxTemperature: 2,
max_tokens: 256,
top_p: 1,
provider: { value: ModelProvider.OpenAI, enabled: true },
model: { value: "gpt-3.5-turbo", enabled: true },
temperature: { value: 1, enabled: true },
maxTemperature: { value: 2, enabled: true },
max_tokens: { value: 256, enabled: true },
top_p: { value: 1, enabled: true },
},
{
provider: ModelProvider.OpenAI,
model: "gpt-4-turbo-preview",
temperature: 1,
maxTemperature: 2,
max_tokens: 256,
top_p: 1,
} as const,
provider: { value: ModelProvider.OpenAI, enabled: true },
model: { value: "gpt-4-turbo-preview", enabled: true },
temperature: { value: 1, enabled: true },
maxTemperature: { value: 2, enabled: true },
max_tokens: { value: 256, enabled: true },
top_p: { value: 1, enabled: true },
},
{
provider: ModelProvider.OpenAI,
model: "gpt-4o",
temperature: 1,
maxTemperature: 2,
max_tokens: 256,
top_p: 1,
provider: { value: ModelProvider.OpenAI, enabled: true },
model: { value: "gpt-4o", enabled: true },
temperature: { value: 1, enabled: true },
maxTemperature: { value: 2, enabled: true },
max_tokens: { value: 256, enabled: true },
top_p: { value: 1, enabled: true },
},
] as const;
@@ -1,9 +1,9 @@
import lodash from "lodash";
import { z } from "zod";
import { ModelUsageUnit } from "@langfuse/shared";
import { NonEmptyString, jsonSchema } from "@/src/utils/zod";
import { ObservationLevel } from "@langfuse/shared";
import { NonEmptyString, jsonSchema } from "../../utils/zod";
import { ModelUsageUnit } from "../../constants";
import { ObservationLevel } from "@prisma/client";
export const Usage = z.object({
input: z.number().int().nullish(),
@@ -109,7 +109,7 @@ export const CreateGenerationBody = CreateSpanBody.extend({
z.string(),
z
.union([z.string(), z.number(), z.boolean(), z.array(z.string())])
.nullish(),
.nullish()
)
.nullish(),
usage: usage,
@@ -131,7 +131,7 @@ export const UpdateGenerationBody = UpdateSpanBody.extend({
z.string(),
z
.union([z.string(), z.number(), z.boolean(), z.array(z.string())])
.nullish(),
.nullish()
)
.nullish(),
usage: usage,
@@ -195,7 +195,7 @@ export const LegacyGenerationsCreateSchema = z.object({
modelParameters: z
.record(
z.string(),
z.union([z.string(), z.number(), z.boolean()]).nullish(),
z.union([z.string(), z.number(), z.boolean()]).nullish()
)
.nullish(),
prompt: jsonSchema.nullish(),
@@ -219,7 +219,7 @@ export const LegacyGenerationPatchSchema = z.object({
modelParameters: z
.record(
z.string(),
z.union([z.string(), z.number(), z.boolean()]).nullish(),
z.union([z.string(), z.number(), z.boolean()]).nullish()
)
.nullish(),
prompt: jsonSchema.nullish(),
@@ -243,7 +243,7 @@ export const LegacyObservationBody = z.object({
modelParameters: z
.record(
z.string(),
z.union([z.string(), z.number(), z.boolean()]).nullish(),
z.union([z.string(), z.number(), z.boolean()]).nullish()
)
.nullish(),
input: jsonSchema.nullish(),
+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" ||
+3
View File
@@ -9,6 +9,9 @@ export * from "./filterToPrisma";
export * from "./tracesTable";
export * from "./server/auth";
export * from "./observationsTable";
export * from "./features/ingestion/types";
export * from "./utils/zod";
export * from "./utils/json";
export { env } from "./env";
// llm api
@@ -42,6 +42,7 @@ export const tableNames = [
"traces_parent_observation_scores",
"sessions",
"prompts",
"users",
] as const;
export type TableNames = (typeof tableNames)[number];
+11 -7
View File
@@ -20,19 +20,23 @@ export enum ChatMessageRole {
Assistant = "assistant",
}
export type ModelParams = AnthropicModelParams | OpenAIModelParams;
export type UIModelParams = Required<
AnthropicModelParams | OpenAIModelParams
> & {
maxTemperature: number;
type RecordWithEnabledFlag<T> = {
[K in keyof T]: { value: T[K]; enabled: boolean };
};
export type UIModelParams = RecordWithEnabledFlag<
Required<AnthropicModelParams | OpenAIModelParams> & {
maxTemperature: number;
}
>;
// Generic config
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";
@@ -1,6 +1,6 @@
import { type jsonSchema } from "@/src/utils/zod";
import { type z } from "zod";
import lodash from "lodash";
import { jsonSchema } from "./zod";
export const parseJson = (input: string) => {
try {
@@ -37,7 +37,7 @@ export function deepParseJson(json: unknown): unknown {
// Ensure we only iterate over the object's own properties
if (Object.prototype.hasOwnProperty.call(json, key)) {
(json as Record<string, unknown>)[key] = deepParseJson(
(json as Record<string, unknown>)[key],
(json as Record<string, unknown>)[key]
);
}
}
@@ -50,7 +50,7 @@ export function deepParseJson(json: unknown): unknown {
export const mergeJson = (
json1?: z.infer<typeof jsonSchema>,
json2?: z.infer<typeof jsonSchema>,
json2?: z.infer<typeof jsonSchema>
) => {
if (json1 === undefined) {
return json2;
@@ -26,7 +26,7 @@ const jsonSchemaNullable: z.ZodType<JsonNested> = z.lazy(() =>
nestedLiteralSchema,
z.array(jsonSchemaNullable),
z.record(jsonSchemaNullable),
]),
])
);
// Root schema that does not allow nulls at the root level
@@ -35,20 +35,29 @@ export const jsonSchema: z.ZodType<Json> = z.lazy(() =>
rootLiteralSchema,
z.array(jsonSchemaNullable),
z.record(jsonSchemaNullable),
]),
])
);
export const paginationZod = {
page: z.preprocess(
(x) => (x === "" ? undefined : x),
z.coerce.number().default(1),
z.coerce.number().default(1)
),
limit: z.preprocess(
(x) => (x === "" ? undefined : x),
z.coerce.number().lte(100).default(50),
z.coerce.number().lte(100).default(50)
),
};
export const optionalPaginationZod = {
page: z
.preprocess((x) => (x === "" ? undefined : x), z.coerce.number())
.optional(),
limit: z
.preprocess((x) => (x === "" ? undefined : x), z.coerce.number())
.optional(),
};
export const noHtmlRegex = /<[^>]*>/;
export const noHtmlCheck = (value: string) => !noHtmlRegex.test(value);
+764 -655
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
+25 -18
View File
@@ -1,6 +1,6 @@
{
"name": "web",
"version": "2.40.0",
"version": "2.49.2",
"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,26 +56,28 @@
"@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",
"@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",
@@ -84,7 +88,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",
@@ -94,6 +98,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",
@@ -101,8 +106,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",
@@ -111,29 +116,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",
"@typescript-eslint/parser": "^7.12.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",
+53 -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
@@ -1850,6 +1859,10 @@ components:
type: string
nullable: true
description: Dataset items are upserted on their id
status:
$ref: '#/components/schemas/DatasetStatus'
nullable: true
description: Defaults to ACTIVE for newly created items
required:
- datasetName
CreateDatasetRunItemRequest:
@@ -2495,6 +2508,7 @@ components:
totalCost:
type: number
format: double
description: Total model cost in USD
usage:
type: array
items:
@@ -2517,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
@@ -2650,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
@@ -2668,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
@@ -2709,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
+9 -4
View File
@@ -65,7 +65,7 @@
"auth": null,
"body": {
"mode": "raw",
"raw": "{\n \"datasetName\": \"example\",\n \"input\": \"UNKNOWN\",\n \"expectedOutput\": \"UNKNOWN\",\n \"metadata\": \"UNKNOWN\",\n \"sourceTraceId\": \"example\",\n \"sourceObservationId\": \"example\",\n \"id\": \"example\"\n}",
"raw": "{\n \"datasetName\": \"example\",\n \"input\": \"UNKNOWN\",\n \"expectedOutput\": \"UNKNOWN\",\n \"metadata\": \"UNKNOWN\",\n \"sourceTraceId\": \"example\",\n \"sourceObservationId\": \"example\",\n \"id\": \"example\",\n \"status\": \"ACTIVE\"\n}",
"options": {
"raw": {
"language": "json"
@@ -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": "",
+13 -4
View File
@@ -2,7 +2,7 @@ import { prisma } from "@langfuse/shared/src/db";
import * as Sentry from "@sentry/nextjs";
import { ProfilingIntegration } from "@sentry/profiling-node";
import type { SamplingContext, TransactionEvent } from "@sentry/types";
import { jsonSchema } from "@/src/utils/zod";
import { jsonSchema } from "@langfuse/shared";
if (process.env.NEXT_PUBLIC_SENTRY_DSN)
Sentry.init({
@@ -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(),
+4
View File
@@ -210,11 +210,14 @@ describe("/api/public/datasets and /api/public/dataset-items API Endpoints", ()
datasetName: "dataset-name",
input: { key: "value2" },
metadata: ["hello-world"],
status: "ARCHIVED",
});
expect(item2.status).toBe(200);
expect(item2.body).toMatchObject({
id: "dataset-item-id",
input: { key: "value2" },
metadata: ["hello-world"],
status: "ARCHIVED",
});
const dbDatasetItem = await prisma.datasetItem.findFirst({
@@ -223,6 +226,7 @@ describe("/api/public/datasets and /api/public/dataset-items API Endpoints", ()
expect(dbDatasetItem).not.toBeNull();
expect(dbDatasetItem?.input).toMatchObject({ key: "value2" });
expect(dbDatasetItem?.metadata).toMatchObject(["hello-world"]);
expect(dbDatasetItem?.status).toBe("ARCHIVED");
});
it("should create and get a dataset run, include special characters", async () => {
+5 -166
View File
@@ -281,6 +281,7 @@ describe("/api/public/ingestion API Endpoint", () => {
expect(dbScore?.name).toBe("score-name");
expect(dbScore?.value).toBe(100.5);
expect(dbScore?.observationId).toBeNull();
expect(dbScore?.projectId).toBe("7a88fb47-b4e2-43b8-a06c-a5ce950dc53a");
});
});
@@ -726,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"],
},
},
],
@@ -742,6 +744,7 @@ describe("/api/public/ingestion API Endpoint", () => {
id: traceId,
name: "trace-name",
userId: "user-2",
tags: ["tag-1", "tag-4", "tag-3"],
},
},
],
@@ -762,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 () => {
@@ -816,57 +821,6 @@ describe("/api/public/ingestion API Endpoint", () => {
expect(dbTrace.length).toBe(1);
});
it("should fail for auth errors", async () => {
const traceId = v4();
const scoreId = v4();
const responseOne = await makeAPICall("POST", "/api/public/ingestion", {
batch: [
{
id: v4(),
type: "trace-create",
timestamp: new Date().toISOString(),
body: {
id: traceId,
name: "trace-name",
userId: "user-1",
metadata: { key: "value" },
release: "1.0.0",
version: "2.0.0",
},
},
{
id: v4(),
type: "score-create",
timestamp: new Date().toISOString(),
body: {
id: scoreId,
name: "score-name",
value: 100.5,
traceId: "some-random-id",
},
},
],
});
expect(responseOne.status).toBe(207);
expect("errors" in responseOne.body).toBe(true);
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
expect(responseOne.body.errors.length).toBe(1);
expect("successes" in responseOne.body).toBe(true);
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
expect(responseOne.body.successes.length).toBe(1);
const dbTrace = await prisma.trace.findMany({
where: {
name: "trace-name",
},
});
expect(dbTrace.length).toBe(1);
});
it("should fail for resource not found", async () => {
const traceId = v4();
@@ -1653,121 +1607,6 @@ IB Home / . . . / News / News about the IB / Why ChatGPT is an o
expect(dbScore?.value).toBe(scoreValue);
});
it("should error on Bearer Auth for a trace from different project", async () => {
const otherProjectId = "other_project_id";
const traceId = "trace_id";
const bearerAuth = "Bearer pk-lf-1234567890";
const scoreId = "score_id";
const scoreEventId = "score_event_id";
const scoreName = "score-name";
const scoreValue = 100.5;
// Seed db with a trace to be scored
try {
await prisma.project.create({
data: {
id: otherProjectId,
name: "another-project",
},
});
await prisma.trace.create({
data: {
id: traceId,
name: "trace-name",
project: { connect: { id: otherProjectId } },
},
});
const response = await makeAPICall(
"POST",
"/api/public/ingestion",
{
batch: [
{
id: scoreEventId,
type: "score-create",
timestamp: new Date().toISOString(),
body: {
id: scoreId,
name: scoreName,
value: scoreValue,
traceId: traceId,
},
},
],
},
bearerAuth,
);
expect(response.status).toBe(207);
expect(response.body.successes.length).toBe(0);
expect(response.body).toHaveProperty("errors");
expect(response.body.errors.length).toEqual(1);
expect(response.body.errors[0]?.id).toEqual(scoreEventId);
expect(await prisma.trace.count()).toBe(1);
expect(await prisma.trace.count({ where: { id: traceId } })).toBe(1);
const dbScore = await prisma.score.findUnique({
where: {
id: scoreId,
},
});
expect(dbScore).toBeNull();
} finally {
await prisma.project.delete({ where: { id: otherProjectId } });
}
});
it("should error on Bearer Auth with a trace that does not exist", async () => {
const traceId = "trace_id";
const bearerAuth = "Bearer pk-lf-1234567890";
const scoreId = "score_id";
const scoreEventId = "score_event_id";
const scoreName = "score-name";
const scoreValue = 100.5;
const response = await makeAPICall(
"POST",
"/api/public/ingestion",
{
batch: [
{
id: scoreEventId,
type: "score-create",
timestamp: new Date().toISOString(),
body: {
id: scoreId,
name: scoreName,
value: scoreValue,
traceId: traceId,
},
},
],
},
bearerAuth,
);
expect(response.status).toBe(207);
expect(response.body.successes.length).toBe(0);
expect(response.body.errors.length).toBe(1);
expect(response.body.errors[0]?.id).toBe(scoreEventId);
expect(await prisma.trace.count()).toBe(0);
const dbScore = await prisma.score.findUnique({
where: {
id: scoreId,
},
});
expect(dbScore).toBeNull();
});
it("should error on wrong input", async () => {
const traceId = "trace_id";
const bearerAuth = "Bearer pk-lf-1234567890";
@@ -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,
},
]);
});
+100 -28
View File
@@ -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", () => {
@@ -784,10 +849,17 @@ describe("/api/public/v2/prompts API Endpoint", () => {
expect(promptMeta3.tags).toEqual(["tag-1"]);
// Validate pagination
expect(body.pagination.page).toBe(1);
expect(body.pagination.limit).toBe(10);
expect(body.pagination.totalPages).toBe(1);
expect(body.pagination.totalItems).toBe(3);
expect(body.meta.page).toBe(1);
expect(body.meta.limit).toBe(10);
expect(body.meta.totalPages).toBe(1);
expect(body.meta.totalItems).toBe(3);
// Validate pagination backwards compatibility
// https://github.com/langfuse/langfuse/issues/2068
expect(body.pagination?.page).toBe(1);
expect(body.pagination?.limit).toBe(10);
expect(body.pagination?.totalPages).toBe(1);
expect(body.pagination?.totalItems).toBe(3);
});
it("should fetch a prompt meta list with name filter", async () => {
@@ -802,10 +874,10 @@ describe("/api/public/v2/prompts API Endpoint", () => {
expect(body.data[0].tags).toEqual([]);
// Validate pagination
expect(body.pagination.page).toBe(1);
expect(body.pagination.limit).toBe(10);
expect(body.pagination.totalPages).toBe(1);
expect(body.pagination.totalItems).toBe(1);
expect(body.meta.page).toBe(1);
expect(body.meta.limit).toBe(10);
expect(body.meta.totalPages).toBe(1);
expect(body.meta.totalItems).toBe(1);
// Test with a different name
const response2 = await makeAPICall("GET", `${baseURI}?name=prompt-2`);
@@ -819,10 +891,10 @@ describe("/api/public/v2/prompts API Endpoint", () => {
expect(body2.data[0].tags).toEqual([]);
// Validate pagination
expect(body2.pagination.page).toBe(1);
expect(body2.pagination.limit).toBe(10);
expect(body2.pagination.totalPages).toBe(1);
expect(body2.pagination.totalItems).toBe(1);
expect(body2.meta.page).toBe(1);
expect(body2.meta.limit).toBe(10);
expect(body2.meta.totalPages).toBe(1);
expect(body2.meta.totalItems).toBe(1);
// Return 200 with empty list if name does not exist
const response3 = await makeAPICall(
@@ -846,10 +918,10 @@ describe("/api/public/v2/prompts API Endpoint", () => {
expect(body.data[0].tags).toEqual(["tag-1"]);
// Validate pagination
expect(body.pagination.page).toBe(1);
expect(body.pagination.limit).toBe(10);
expect(body.pagination.totalPages).toBe(1);
expect(body.pagination.totalItems).toBe(1);
expect(body.meta.page).toBe(1);
expect(body.meta.limit).toBe(10);
expect(body.meta.totalPages).toBe(1);
expect(body.meta.totalItems).toBe(1);
// Return 200 with empty list if tag does not exist
const response3 = await makeAPICall("GET", `${baseURI}?tag=non-existent`);
@@ -875,10 +947,10 @@ describe("/api/public/v2/prompts API Endpoint", () => {
).toBe(true);
// Validate pagination
expect(body.pagination.page).toBe(1);
expect(body.pagination.limit).toBe(10);
expect(body.pagination.totalPages).toBe(1);
expect(body.pagination.totalItems).toBe(3);
expect(body.meta.page).toBe(1);
expect(body.meta.limit).toBe(10);
expect(body.meta.totalPages).toBe(1);
expect(body.meta.totalItems).toBe(3);
// Test with a different label
const response2 = await makeAPICall("GET", `${baseURI}?label=dev`);
@@ -892,10 +964,10 @@ describe("/api/public/v2/prompts API Endpoint", () => {
expect(body2.data[0].tags).toEqual([]);
// Validate pagination
expect(body2.pagination.page).toBe(1);
expect(body2.pagination.limit).toBe(10);
expect(body2.pagination.totalPages).toBe(1);
expect(body2.pagination.totalItems).toBe(1);
expect(body2.meta.page).toBe(1);
expect(body2.meta.limit).toBe(10);
expect(body2.meta.totalPages).toBe(1);
expect(body2.meta.totalItems).toBe(1);
// Return 200 with empty list if label does not exist
const response3 = await makeAPICall(
@@ -914,10 +986,10 @@ describe("/api/public/v2/prompts API Endpoint", () => {
const body = response.body as unknown as PromptsMetaResponse;
expect(body.data).toHaveLength(1);
expect(body.pagination.page).toBe(1);
expect(body.pagination.limit).toBe(1);
expect(body.pagination.totalPages).toBe(3);
expect(body.pagination.totalItems).toBe(3);
expect(body.meta.page).toBe(1);
expect(body.meta.limit).toBe(1);
expect(body.meta.totalPages).toBe(3);
expect(body.meta.totalItems).toBe(3);
});
});
@@ -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 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,
+16 -15
View File
@@ -60,6 +60,7 @@ describe("/api/public/scores API Endpoint", () => {
expect(dbScore?.observationId).toBeNull();
expect(dbScore?.comment).toBe("comment");
expect(dbScore?.source).toBe("API");
expect(dbScore?.projectId).toBe("7a88fb47-b4e2-43b8-a06c-a5ce950dc53a");
});
it("should create score for a trace with int", async () => {
@@ -488,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 () => {
@@ -513,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 () => {
@@ -538,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);
});
});
+1 -1
View File
@@ -1,4 +1,4 @@
import { paginationZod } from "@/src/utils/zod";
import { paginationZod } from "@langfuse/shared";
import { ZodError } from "zod";
// Create test cases
+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}
+106 -19
View File
@@ -1,8 +1,105 @@
import { AlertTriangle, Check } from "lucide-react";
import {
AlertTriangle,
Check,
Github,
HardDriveDownload,
Newspaper,
} from "lucide-react";
import { VERSION } from "@/src/constants";
import { env } from "@/src/env.mjs";
import { cn } from "@/src/utils/tailwind";
import {
DropdownMenu,
DropdownMenuTrigger,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuLabel,
DropdownMenuSeparator,
} from "@/src/components/ui/dropdown-menu";
import { ArrowUp } from "lucide-react";
import { api } from "@/src/utils/api";
import { Button } from "@/src/components/ui/button";
import Link from "next/link";
const VersionLabel = ({ className }: { className?: string }) => {
const checkUpdate = api.public.checkUpdate.useQuery(undefined, {
refetchOnMount: false,
refetchOnWindowFocus: false,
refetchOnReconnect: false,
enabled: !env.NEXT_PUBLIC_LANGFUSE_CLOUD_REGION, // do not check for updates on Langfuse Cloud
onError: (error) => console.error("checkUpdate error", error), // do not render default error message
});
const hasUpdate =
!env.NEXT_PUBLIC_LANGFUSE_CLOUD_REGION &&
checkUpdate.data &&
checkUpdate.data.updateType;
const color =
checkUpdate.data?.updateType === "major"
? "text-dark-red"
: checkUpdate.data?.updateType === "minor"
? "text-dark-yellow"
: undefined;
return (
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="ghost" size="xs" className={className}>
{VERSION}
{hasUpdate && <ArrowUp className={`ml-1 h-3 w-3 ${color}`} />}
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent>
{hasUpdate ? (
<>
<DropdownMenuLabel>
New {checkUpdate.data?.updateType} version:{" "}
{checkUpdate.data?.latestRelease}
</DropdownMenuLabel>
<DropdownMenuSeparator />
</>
) : !env.NEXT_PUBLIC_LANGFUSE_CLOUD_REGION ? (
<>
<DropdownMenuLabel>This is the latest release</DropdownMenuLabel>
<DropdownMenuSeparator />
</>
) : null}
<DropdownMenuItem asChild>
<Link
href="https://github.com/langfuse/langfuse/releases"
target="_blank"
>
<Github size={16} className="mr-2" />
Releases
</Link>
</DropdownMenuItem>
<DropdownMenuItem asChild>
<Link href="https://langfuse.com/changelog" target="_blank">
<Newspaper size={16} className="mr-2" />
Changelog
</Link>
</DropdownMenuItem>
{hasUpdate && (
<>
<DropdownMenuSeparator />
<DropdownMenuItem asChild>
<Link
href="https://langfuse.com/docs/deployment/self-host#update"
target="_blank"
>
<HardDriveDownload size={16} className="mr-2" />
Update
</Link>
</DropdownMenuItem>
</>
)}
</DropdownMenuContent>
</DropdownMenu>
);
};
export const LangfuseIcon = ({
size = 32,
@@ -39,18 +136,18 @@ export const LangfuseLogo = ({
{showEnvLabel && env.NEXT_PUBLIC_LANGFUSE_CLOUD_REGION && (
<div
className={cn(
"flex items-center gap-2 self-stretch rounded-md px-3 py-2 text-xs ring-1 lg:-mx-2",
"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" ? (
<Check size={16} />
<Check size={16} className="hidden sm:block" />
) : (
<AlertTriangle size={16} />
<AlertTriangle size={16} className="hidden sm:block" />
)}
<span className="whitespace-nowrap">
{["EU", "US"].includes(env.NEXT_PUBLIC_LANGFUSE_CLOUD_REGION)
@@ -65,22 +162,12 @@ export const LangfuseLogo = ({
<span
className={cn(
"ml-2 font-mono font-semibold",
size === "sm" ? "text-sm" : "text-xl",
size === "sm" ? "text-sm" : "text-lg",
)}
>
Langfuse
</span>
{version && (
<a
href="https://github.com/langfuse/langfuse/releases"
target="_blank"
rel="noopener"
title="View releases on GitHub"
className="ml-2 text-xs text-gray-400"
>
{VERSION}
</a>
)}
{version && <VersionLabel className="ml-2" />}
</div>
</div>
);
@@ -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>
);
};
@@ -1,8 +1,3 @@
import {
supportedModels,
ModelProvider,
type UIModelParams,
} from "@langfuse/shared";
import { Input } from "@/src/components/ui/input";
import {
Select,
@@ -12,86 +7,111 @@ import {
SelectValue,
} 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,
supportedModels,
type UIModelParams,
} from "@langfuse/shared";
import { LLMApiKeyComponent } from "./LLMApiKeyComponent";
export type ModelParamsContext = {
modelParams: UIModelParams;
availableModels?: UIModelParams[];
updateModelParam: <Key extends keyof UIModelParams>(
updateModelParamValue: <Key extends keyof UIModelParams>(
key: Key,
value: UIModelParams[Key],
value: UIModelParams[Key]["value"],
) => void;
disabled?: boolean;
setModelParamEnabled?: (key: keyof UIModelParams, enabled: boolean) => void;
formDisabled?: boolean;
};
export const ModelParameters: React.FC<ModelParamsContext> = ({
modelParams,
availableModels,
updateModelParam,
disabled,
updateModelParamValue,
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"
disabled={disabled}
value={modelParams.provider}
disabled={formDisabled}
value={modelParams.provider.value}
options={
availableModels
? [...new Set(availableModels.map((m) => m.provider))]
? [...new Set(availableModels.map((m) => m.provider.value))]
: Object.values(ModelProvider)
}
updateModelParam={updateModelParam}
updateModelParam={updateModelParamValue}
/>
<ModelParamsSelect
title="Model name"
modelParamsKey="model"
disabled={disabled}
value={modelParams.model}
disabled={formDisabled}
value={modelParams.model.value}
options={Object.values(
availableModels
? availableModels
.filter((m) => m.provider === modelParams.provider)
.map((m) => m.model)
: supportedModels[modelParams.provider],
.filter(
(m) => m.provider.value === modelParams.provider.value,
)
.map((m) => m.model.value)
: supportedModels[modelParams.provider.value],
)}
updateModelParam={updateModelParam}
updateModelParam={updateModelParamValue}
/>
<ModelParamsSlider
title="Temperature"
modelParamsKey="temperature"
disabled={disabled}
value={modelParams.temperature}
formDisabled={formDisabled}
enabled={modelParams.temperature.enabled}
setModelParamEnabled={setModelParamEnabled}
value={modelParams.temperature.value}
min={0}
max={modelParams.maxTemperature}
max={modelParams.maxTemperature.value}
step={0.01}
tooltip="The sampling temperature. Higher values will make the output more random, while lower values like will make it more focused and deterministic."
updateModelParam={updateModelParam}
tooltip="The sampling temperature. Higher values will make the output more random, while lower values will make it more focused and deterministic."
updateModelParam={updateModelParamValue}
/>
<ModelParamsSlider
title="Output token limit"
modelParamsKey="max_tokens"
disabled={disabled}
value={modelParams.max_tokens}
formDisabled={formDisabled}
enabled={modelParams.max_tokens.enabled}
setModelParamEnabled={setModelParamEnabled}
value={modelParams.max_tokens.value}
min={1}
max={4096}
step={1}
tooltip="The maximum number of tokens that can be generated in the chat completion."
updateModelParam={updateModelParam}
updateModelParam={updateModelParamValue}
/>
<ModelParamsSlider
title="Top P"
modelParamsKey="top_p"
disabled={disabled}
value={modelParams.top_p}
formDisabled={formDisabled}
enabled={modelParams.top_p.enabled}
setModelParamEnabled={setModelParamEnabled}
value={modelParams.top_p.value}
min={0}
max={1}
step={0.01}
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={updateModelParam}
updateModelParam={updateModelParamValue}
/>
<LLMApiKeyComponent {...{ projectId, modelParams }} />
</div>
</div>
);
@@ -102,7 +122,7 @@ type ModelParamsSelectProps = {
modelParamsKey: keyof UIModelParams;
value: string;
options: string[];
updateModelParam: ModelParamsContext["updateModelParam"];
updateModelParam: ModelParamsContext["updateModelParamValue"];
disabled?: boolean;
};
const ModelParamsSelect = ({
@@ -115,7 +135,14 @@ const ModelParamsSelect = ({
}: ModelParamsSelectProps) => {
return (
<div className="space-y-2">
<p className="text-xs font-semibold">{title}</p>
<p
className={cn(
"text-xs font-semibold",
disabled && "text-muted-foreground",
)}
>
{title}
</p>
<Select
disabled={disabled}
onValueChange={(value) =>
@@ -149,8 +176,10 @@ type ModelParamsSliderProps = {
min: number;
max: number;
step: number;
updateModelParam: ModelParamsContext["updateModelParam"];
disabled?: boolean;
updateModelParam: ModelParamsContext["updateModelParamValue"];
enabled?: boolean;
formDisabled?: boolean;
setModelParamEnabled?: ModelParamsContext["setModelParamEnabled"];
};
const ModelParamsSlider = ({
title,
@@ -161,30 +190,51 @@ const ModelParamsSlider = ({
max,
step,
updateModelParam,
disabled,
setModelParamEnabled,
enabled,
formDisabled,
}: ModelParamsSliderProps) => {
return (
<div className="space-y-3" title={tooltip}>
<div className="flex flex-row">
<p className="flex-1 text-xs font-semibold">{title}</p>
<Input
className="h-6 w-14 appearance-none px-2 text-right"
type="number"
disabled={disabled}
min={min}
max={max}
step={step}
value={value}
onChange={(event) => {
updateModelParam(
modelParamsKey,
Math.max(Math.min(parseFloat(event.target.value), max), min),
);
}}
/>
<p
className={cn(
"flex-1 text-xs font-semibold",
(!enabled || formDisabled) && "text-muted-foreground",
)}
>
{title}
</p>
<div className="flex flex-row space-x-3">
<Input
className="h-6 w-14 appearance-none px-2 text-right"
type="number"
disabled={!enabled || formDisabled}
min={min}
max={max}
step={step}
value={value}
onChange={(event) => {
updateModelParam(
modelParamsKey,
Math.max(Math.min(parseFloat(event.target.value), max), min),
);
}}
/>
{setModelParamEnabled ? (
<Switch
title={`Control sending the ${title} parameter`}
disabled={formDisabled}
checked={enabled}
onCheckedChange={(checked) => {
setModelParamEnabled(modelParamsKey, checked);
}}
/>
) : null}
</div>
</div>
<Slider
disabled={disabled}
disabled={!enabled || formDisabled}
min={min}
max={max}
step={step}
+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>
);
+15 -22
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,24 +49,25 @@ 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,
title: "Slack Connect",
description: "Get a dedicated support channel for you and your team.",
href: "mailto:help@langfuse.com?subject=Slack%20Connect%20Request&body=I'd%20like%20to%20request%20a%20dedicated%20Slack%20Connect%20channel%20for%20me%20and%20my%20team.%0D%0A%0D%0AUsers%20(emails)%20to%20include%20besides%20mine%3A%0D%0A%0D%0A",
buttonText: "Request Slack Connect (via Email)",
buttonText: "Request via Email",
},
];
+1 -4
View File
@@ -53,16 +53,13 @@ export function DeleteButton({
},
});
if (!hasAccess) {
return null;
}
return (
<Popover key={itemId}>
<PopoverTrigger asChild>
<Button
variant={isTableAction ? "ghost" : "outline"}
size={isTableAction ? "xs" : "icon"}
disabled={!hasAccess}
onClick={() =>
type === "trace"
? capture("trace:delete_form_open", {

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