Compare commits

...
351 Commits
Author SHA1 Message Date
steffen911 f10e896545 chore: release v3.13.0 2025-01-21 17:10:01 +01:00
0047cbb367 fix: use unix timestamp for batch export file name to avoid invalid characters (#5092)
Co-authored-by: hopkinx <hopkinx@yanxihuadeMac-mini.local>
2025-01-21 17:08:33 +01:00
Marc KlingenandGitHub 8df120c186 feat(ee): add audit log view to project settings (#5150)
* add audit logs table

* push

* fix pagination zod schema

* copy

* add check that user is in org

* simplify
2025-01-21 15:10:22 +00:00
Max DeichmannandGitHub a12b5b5006 fix: correctly upgrade katex (#5151) 2025-01-21 14:57:48 +00:00
marliessophieandGitHub 91f360ae29 chore(models-ui): move models into settings (#5124)
* chore(models-ui): move models into settings

* style: add notification to manage models in settings

* chore: mv button
2025-01-21 14:54:58 +00:00
Steffen SchmitzandGitHub 026f7f1b4b chore: allow skipping ingestion CH reads for projects created after cutoff date (#5116)
* chore: allow skipping ingestion CH reads for projects created after cutoff date

* chore: add in memory cache for project skips
2025-01-21 14:08:36 +00:00
Max DeichmannandGitHub 29ad2965e6 security: upgrade katex (#5149) 2025-01-21 14:01:37 +00:00
Steffen SchmitzandGitHub ae3f84125b chore: remove legacy/batched ingestion pipeline code (#5121)
* chore: remove legacy/batched ingestion pipeline code

* chore: remove js-tiktoken dependency

* chore: partially revert updates

* chore: typing in gitHubEnterpriseProvider.ts

* chore: remove model-match test
2025-01-21 12:57:43 +00:00
Steffen SchmitzandGitHub 507a0eb7f4 chore: skip re-builds for same tree hash (#5135)
* chore: skip re-builds for same tree hash

* dummy

* chore: include do not skip

* chore: skip push
2025-01-21 11:58:10 +00:00
Max DeichmannandGitHub 64b29bce24 fix: observation table type alignment (#5146) 2025-01-21 11:27:50 +00:00
marliessophieandGitHub 60d6c5359b feat(dataset-runs): add product analytics events (#5145)
* feat(dataset-runs): add product analytics events

* push

* push
2025-01-21 11:25:45 +00:00
Steffen SchmitzandGitHub 7f94a29c48 chore: reduce default write interval for CH queue to 1000 (#5144) 2025-01-21 11:19:44 +00:00
steffen911 a26f16b090 chore: release v3.12.0
CI/CD / lint (push) Waiting to run
CI/CD / test-docker-build (push) Waiting to run
CI/CD / tests-web-sync (node20, pg12) (push) Waiting to run
CI/CD / tests-web-sync (node20, pg15) (push) Waiting to run
CI/CD / tests-web-async (node20, pg12, mode) (push) Waiting to run
CI/CD / tests-web-async (node20, pg15, mode) (push) Waiting to run
CI/CD / tests-web-async (node20, pg12, mode-azure) (push) Waiting to run
CI/CD / tests-web-async (node20, pg15, mode-azure) (push) Waiting to run
CI/CD / tests-worker (node20, pg12, mode) (push) Waiting to run
CI/CD / tests-worker (node20, pg15, mode) (push) Waiting to run
CI/CD / tests-worker (node20, pg12, mode-azure) (push) Waiting to run
CI/CD / tests-worker (node20, pg15, mode-azure) (push) Waiting to run
CI/CD / e2e-tests (push) Waiting to run
CI/CD / e2e-server-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
2025-01-21 12:28:38 +01:00
Steffen SchmitzandGitHub b34da5a0de feat: add support for DATABASE_ARGS config (#5143) 2025-01-21 11:14:39 +00:00
marliessophieandGitHub 9684768346 chore(traces): remove unsued code pulling trace data from prisma [deprecated] (#5139) 2025-01-21 10:24:27 +00:00
Steffen SchmitzandGitHub 1c51a319fd chore: remove postgres fallback for generations table calls (#5137) 2025-01-21 10:55:54 +01:00
Steffen SchmitzandGitHub 9dd7aa833b chore: remove queryClickhouse toggles (#5123)
* chore: remove queryClickhouse toggles

* chore: linting
2025-01-21 08:01:57 +00:00
Marc KlingenandGitHub e14f8587af revert: ask for github star during onboarding (#5127)
Revert "feat(onboarding): add GitHub star prompt with badge (#5125)"

This reverts commit ed743c1f76.
2025-01-20 21:04:24 +00:00
ed743c1f76 feat(onboarding): add GitHub star prompt with badge (#5125)
* feat(onboarding): add GitHub star prompt with badge

* ui improvements

* add posthog tracking

---------

Co-authored-by: Marc Klingen <git@marcklingen.com>
2025-01-20 19:27:46 +00:00
Hassieb PakzadandGitHub 0ee21fb98e fix(llm-api-keys): improve form dialog for bedrock (#5122) 2025-01-20 18:28:09 +01:00
Hassieb PakzadandGitHub 78d74ccb4e fix(trace-view): show disabled jump to playground instead of hiding (#5120) 2025-01-20 18:05:38 +01:00
Hassieb PakzadandGitHub 4cb5af71d3 fix(ingestion): allow nullish tokens_details in openai usage schema (#5117) 2025-01-20 18:03:51 +01:00
steffen911 7111ebfbeb chore: release v3.11.1
CI/CD / lint (push) Waiting to run
CI/CD / test-docker-build (push) Waiting to run
CI/CD / tests-web-sync (node20, pg12) (push) Waiting to run
CI/CD / tests-web-sync (node20, pg15) (push) Waiting to run
CI/CD / tests-web-async (node20, pg12, mode) (push) Waiting to run
CI/CD / tests-web-async (node20, pg15, mode) (push) Waiting to run
CI/CD / tests-web-async (node20, pg12, mode-azure) (push) Waiting to run
CI/CD / tests-web-async (node20, pg15, mode-azure) (push) Waiting to run
CI/CD / tests-worker (node20, pg12, mode) (push) Waiting to run
CI/CD / tests-worker (node20, pg15, mode) (push) Waiting to run
CI/CD / tests-worker (node20, pg12, mode-azure) (push) Waiting to run
CI/CD / tests-worker (node20, pg15, mode-azure) (push) Waiting to run
CI/CD / e2e-tests (push) Waiting to run
CI/CD / e2e-server-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
2025-01-20 17:02:28 +01:00
Steffen SchmitzandGitHub a9506b40ab fix: allow field overwrites with null in ingestion api (#5114)
* fix: allow field overwrites with `null` in ingestion api

* chore: add undefined check

* chore: check undefined does not overwrite value

* chore: explicit undefined check
2025-01-20 15:24:09 +00:00
Max Deichmann 3cd82c97cf chore: release v3.11.0
CI/CD / lint (push) Waiting to run
CI/CD / test-docker-build (push) Waiting to run
CI/CD / tests-web-sync (node20, pg12) (push) Waiting to run
CI/CD / tests-web-sync (node20, pg15) (push) Waiting to run
CI/CD / tests-web-async (node20, pg12, mode) (push) Waiting to run
CI/CD / tests-web-async (node20, pg15, mode) (push) Waiting to run
CI/CD / tests-web-async (node20, pg12, mode-azure) (push) Waiting to run
CI/CD / tests-web-async (node20, pg15, mode-azure) (push) Waiting to run
CI/CD / tests-worker (node20, pg12, mode) (push) Waiting to run
CI/CD / tests-worker (node20, pg15, mode) (push) Waiting to run
CI/CD / tests-worker (node20, pg12, mode-azure) (push) Waiting to run
CI/CD / tests-worker (node20, pg15, mode-azure) (push) Waiting to run
CI/CD / e2e-tests (push) Waiting to run
CI/CD / e2e-server-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
2025-01-20 15:49:51 +01:00
Max DeichmannandGitHub f55bee2d36 fix: route all links to new observations table (#5112) 2025-01-20 15:32:54 +01:00
Max DeichmannandGitHub 5391dd90f4 fix: correct the rate limiter header return types (#5111) 2025-01-20 15:16:56 +01:00
Max DeichmannandGitHub a1e670a729 feat: convert generations to observations table (#5099)
* feat: convert generations to observations table

* feat: add type col to generations table

* feat: add type col to generations table

* feat: add type col to generations table

* feat: add type col to generations table

* feat: add type col to generations table

* fix: fix
2025-01-20 13:40:41 +00:00
Max DeichmannandGitHub b6e21fc44b fix: return 404 error instead of 500 for not found observations trpc … (#5105)
fix: return 404 error instead of 500 for not found observations trpc route
2025-01-19 14:23:15 +01:00
Max DeichmannandGitHub eb59082bf7 fix: flaky session export test (#5103) 2025-01-18 19:03:57 +00:00
Max Deichmann 950f36b87d chore: release v3.10.0
CI/CD / lint (push) Waiting to run
CI/CD / test-docker-build (push) Waiting to run
CI/CD / tests-web-sync (node20, pg12) (push) Waiting to run
CI/CD / tests-web-sync (node20, pg15) (push) Waiting to run
CI/CD / tests-web-async (node20, pg12, mode) (push) Waiting to run
CI/CD / tests-web-async (node20, pg15, mode) (push) Waiting to run
CI/CD / tests-web-async (node20, pg12, mode-azure) (push) Waiting to run
CI/CD / tests-web-async (node20, pg15, mode-azure) (push) Waiting to run
CI/CD / tests-worker (node20, pg12, mode) (push) Waiting to run
CI/CD / tests-worker (node20, pg15, mode) (push) Waiting to run
CI/CD / tests-worker (node20, pg12, mode-azure) (push) Waiting to run
CI/CD / tests-worker (node20, pg15, mode-azure) (push) Waiting to run
CI/CD / e2e-tests (push) Waiting to run
CI/CD / e2e-server-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
2025-01-18 19:55:56 +01:00
Max DeichmannandGitHub c55b70d119 fix: fix session ui table ordering (#5102) 2025-01-18 19:55:26 +01:00
Marc KlingenandGitHub c1109c91f1 feat(ui): single user view, simpler metrics, link to dashboard (#5097) 2025-01-17 21:14:42 +00:00
Marc KlingenandGitHub 3b737934f9 chore(cloud): run analytics jobs daily (#5096) 2025-01-17 18:14:22 +00:00
Steffen SchmitzandGitHub a12f3f675e fix: update ordering for get traces calls to avoid stale results (#5090)
* fix: update ordering for get traces calls to avoid stale results

* chore: explanation

* chore: remove stale test

* chore: lint

* chore: update comment

* chore: cleanup
2025-01-17 16:30:07 +00:00
Marc KlingenandGitHub 30d5755c24 fix(ui): support menu button width (#5094)
push
2025-01-17 15:04:49 +00:00
Marc KlingenandGitHub 39762cb1c0 feat(cloud): export billing meter backup to s3 (#5091) 2025-01-17 15:22:41 +01:00
marliessophieandGitHub 8ac4176aaf feat(datasets): upload dataset via csv import (#4656)
* fixup: add csv import for datasets

* feat: include DnD logic

* add: fastest-levenshtein dep

* fixup: upload & parse .csv file

* fixup: uploading file

* fix: allow uploading .csv file

* chore: add csv-parse

* eslint

* chore: include project id in temp storage logic

* chore: access checks

* push

* chore: use s3 signed url to upload .csv file temporarily

fixup: env credentials

* deps: fix circular dep

* fixup

* create import bucket for uploads

* push

* push

* push

* remove s3 config for import

* remove server side processing

* fix: metadata display for items

* style: fix css styling and add progress bar

* push

* push

* push

* refactor: extract helper function

* wrap in try catch block

* fix: button logic in case of no access

* analytics: add posthog events
2025-01-17 13:51:25 +00:00
marliessophieandGitHub f87af4f675 feat(model): add playground and eval support for gemini-2.0-flash-exp (#5087) 2025-01-17 13:19:06 +00:00
Max Deichmann bc323772c1 chore: release v3.9.0 2025-01-17 12:26:06 +01:00
Marc KlingenandGitHub fcef737dc0 feat(cloud): snapshot usage metering to postgres db (#5075) 2025-01-17 12:13:46 +01:00
marliessophieandGitHub 4bf77d68ed feat(model): add support for o1 model costs (#5073)
* feat(model): add support for o1 model costs

* order cost list by ID

* push

* update contributing

* push
2025-01-17 10:23:25 +00:00
Steffen SchmitzandGitHub d7ec924ed8 chore: remove outdated postgres executions (#5069) 2025-01-17 11:10:20 +01:00
Marc KlingenandGitHub cefaf56613 chore(cloud): region details (#5080) 2025-01-17 00:18:00 +01:00
Max DeichmannandGitHub 6aff9b0f4a fix: quickly recover from metering queue errors (#5077)
fix: quickly reciver from metering queue errors
2025-01-16 22:11:55 +01:00
Max DeichmannandGitHub 815de149aa fix: retry stripe apis (#5076)
push
2025-01-16 21:57:05 +01:00
Max DeichmannandGitHub bc8923485e fix: add race condition check to billing queue (#5074) 2025-01-16 20:40:59 +01:00
Max DeichmannandGitHub 1a60425692 fix: retry stripe in metering logic and meter again if error ocurred (#5072) 2025-01-16 19:42:10 +01:00
marliessophieandGitHub 1d3ac98123 style(ui): improve observation type label colors for readbility, accessibility (#5071)
* style(ui): adjust muted colors for readbility, accessibility

* style(ui): adjust comment labels given type

* margins

* push
2025-01-16 16:27:47 +00:00
Max DeichmannandGitHub de67ec1a02 chore: add cloud meter event trigger (#5068)
* push

* push

* feat: add metering event
2025-01-16 15:22:59 +00:00
marliessophieandGitHub 9510995a67 fix(ui-tables): populate filter operator by default (#5067) 2025-01-16 14:21:48 +00:00
Marc KlingenandGitHub 4623d76155 fix(cloud): remove race condition check in metering cron job (#5066) 2025-01-16 11:31:00 +01:00
Steffen SchmitzandGitHub 6a6abc6bd8 chore: skip zod parse for json parsing (#5063)
* chore: skip zod parse for json parsing

* chore: lint

* chore: add JsonNested type

* chore: add one more test for invalid json
2025-01-16 09:32:41 +00:00
Max DeichmannandGitHub ddee827e22 chore: update architecture diagram (#5055) 2025-01-15 22:07:16 +00:00
Max DeichmannandGitHub e327db8bbd chore: correct data region (#5054) 2025-01-15 22:01:58 +00:00
Max DeichmannandGitHub 9229d759e1 chore: add project_id to get clickhouse query ingestion (#5051) 2025-01-15 20:09:53 +01:00
Hassieb PakzadandGitHub d8d8ab7060 fix(jump-to-playground): hide button if message parsing will fail (#5048) 2025-01-15 16:55:58 +01:00
Steffen SchmitzandGitHub b851359923 chore: remove legacy v3-migration env variables (#5041)
* chore: remove legacy v3-migration env variables

* chore: linting

* chore: move legacy files to current env

* chore: remove outdated tests

* chore: add async ingestion test

* chore: reduce delay for tests

* chore: lint

* chore: skip scores and enable redis on CI

* chore: remove outdated test classes
2025-01-15 15:39:20 +00:00
marliessophieandGitHub b36ea74314 fix(ui-annoation): bug when re-submitting score comments on categorical scores (#5045)
fix(ui-annoation): re-submit score comments on categorical scores
2025-01-15 15:12:46 +00:00
Hassieb PakzadandGitHub b2f103549a feat(trace-tree-iu): add hint for hidden obs (#5044) 2025-01-15 14:47:39 +01:00
marliessophieandGitHub f660b80438 style(ui-tables): fix position of No results. text if table has no data (#5043)
* style(ui-tables): fix position of `No results.` text if table has no data

* fix: dom nesting
2025-01-15 13:18:23 +00:00
Steffen SchmitzandGitHub 5bbfffef50 chore: allow any value for input/output in ingestion endpoint (#5040)
* chore: allow any value for input/output in ingestion endpoint

* chore: add explanation
2025-01-15 10:36:13 +00:00
Hassieb PakzadandGitHub cb6f95dc93 feat(trace-detail-ui): filter by minObservationlvl (#5039) 2025-01-15 11:40:57 +01:00
Max Deichmann 8345ce48fc chore: release v3.8.0 2025-01-15 10:26:15 +01:00
Max DeichmannandGitHub f71d28900a perf: use streaming for observation and trace list endpoints (#5038)
push
2025-01-15 10:25:28 +01:00
Hassieb PakzadandGitHub 14eaf62e45 fix(ingestion): overwriting of observation level (#5027) 2025-01-15 09:43:41 +01:00
Steffen SchmitzandGitHub f8e61c2659 chore: fallback to error.status for eval error handling (#5037) 2025-01-15 07:40:36 +00:00
marliessophieandGitHub 3b205b45ce chore(ui-dashboard): save selected score graphs in the dashboard to localStorage (project-bound) (#5029)
* chore(ui-dashboard): save selected score graphs in the dashboard to `localStorage` (project-bound)

* add comment

* push
2025-01-14 19:14:17 +00:00
Max DeichmannandGitHub 7260c44486 fix: correctly order sessions by time (#5028)
* fix: correctly order sessions by time

* fix: correctly order sessions by time

* fix: correctly order sessions by time
2025-01-14 19:00:53 +00:00
marliessophieandGitHub 92ec2b60e2 feat(ui-tabels): Count of group selection on column visibility (#5024) 2025-01-14 16:26:01 +00:00
Steffen SchmitzandGitHub a856488f2d chore: upgrade bullmq to 5.34.10 (#5022)
* chore: upgrade bullmq to 5.34.10

* chore: adjust tests
2025-01-14 13:59:55 +00:00
Steffen SchmitzandGitHub a18f09fd8d fix: process status codes on eval execution errors (#5013)
* fix: process status codes on eval execution errors

* chore: error handling
2025-01-14 12:29:44 +00:00
Hassieb PakzadandGitHub 2a95dd1da0 fix(openapi-models): modelusageunit to optional (#5015) 2025-01-14 10:25:07 +01:00
Hassieb PakzadandGitHub 04ebd5994f fix(fern-models): unit to optional commons.model (#5014) 2025-01-14 10:22:32 +01:00
Hassieb PakzadandGitHub 268b741a3a chore(models): update fern types startDate unit (#5004) 2025-01-13 19:53:41 +01:00
Steffen SchmitzandGitHub 8b686e9b31 fix: fail open on redis errors in ratelimitservice (#5001) 2025-01-13 17:29:17 +00:00
Max Deichmann e802a5c23a chore: release v3.7.1 2025-01-13 14:39:24 +01:00
Max DeichmannandGitHub 5180e2382e fix: drop dataset run item temp tables from all CH nodes (#4999) 2025-01-13 14:38:11 +01:00
Max DeichmannandGitHub 2cae1253d3 fix: fix session table sorting (#4995)
* fix: fix session table sorting

* push
2025-01-13 12:18:37 +00:00
ClemoandGitHub be2414a82c docs: fix typo 2025-01-13 10:54:43 +01:00
ClemoandGitHub 95e63c6f78 docs: Update careers 2025-01-13 10:53:54 +01:00
Marc Klingen 32b6502d13 chore: release v3.7.0 2025-01-12 09:59:41 +01:00
Marc KlingenandGitHub 1bc33c7b92 fix: telemetry error "invalid input syntax for type interval" (#4991) 2025-01-12 09:57:46 +01:00
Marc KlingenandGitHub 4967b20478 fix(ui): support menu forwardref error, memo list of options, keys for separators (#4989)
fix(ui): support menu forwardref error
2025-01-12 03:33:37 +00:00
Marc KlingenandGitHub 1130894cdc chore: remove old launch week notifications (#4986) 2025-01-12 03:18:13 +00:00
Marc KlingenandGitHub af276e9e78 feat(cloud): add banner for status page announcement (#4988)
* wip

* fix(ui): "chatisvisble not a function"

* csp

* feat(cloud): add banner for status page announcement
2025-01-12 03:17:45 +00:00
Marc KlingenandGitHub d69dbd81a2 fix(ui): chatIsVisible rendering issues (#4987)
fix(ui): "chatisvisble not a function"
2025-01-12 02:43:25 +00:00
Marc KlingenandGitHub 7e807c0de8 feat(annotation-queue): show "add to dataset" button on trace preview (#4985) 2025-01-12 02:24:57 +00:00
Marc KlingenandGitHub cfa2ac6da9 chore(ui): improve button labels for playground and experiments on prompt view (#4984) 2025-01-12 02:11:49 +00:00
Marc KlingenandGitHub 952a550847 feat(ui): add support dropdown menu to replace /support page (#4983) 2025-01-12 02:05:46 +00:00
Marc KlingenandGitHub 6199903117 chore(ui): add descriptions to the setup onboarding flow (#4982) 2025-01-12 00:00:19 +00:00
Marc KlingenandGitHub 88cdb8999d chore(ui): do not collapse chat messages on prompt view (#4981) 2025-01-11 23:48:35 +00:00
Marc KlingenGitHubellipsis-dev[bot] <65095814+ellipsis-dev[bot]@users.noreply.github.com>
9ad2ef1f91 fix(ui): docs menu item should open in new tab (#4980)
* fix(ui): docs menu item should open in new tab

* Update web/src/components/nav-main.tsx

Co-authored-by: ellipsis-dev[bot] <65095814+ellipsis-dev[bot]@users.noreply.github.com>

---------

Co-authored-by: ellipsis-dev[bot] <65095814+ellipsis-dev[bot]@users.noreply.github.com>
2025-01-11 21:11:37 +00:00
Marc KlingenandGitHub 7a9717cb5e chore(ui): remove introducing orgs banner (#4979) 2025-01-11 20:59:51 +00:00
Marc KlingenandGitHub ce31a806c7 chore(cloud): improve ui of cloud region selection, remove login error notice (#4978)
chore(cloud): imrpove ui of cloud region selection, remove login error notice
2025-01-11 18:03:20 +00:00
Max DeichmannandGitHub e7e6cf1b99 feat: script to replay missing events (#4966)
* feat: script to replay missing events

* feat: script to replay missing events
2025-01-10 15:40:49 +00:00
Steffen SchmitzandGitHub 9f76457f22 chore: stream clickhouse results to posthog to reduce memory consumption (#4967)
* chore: stream clickhouse results to posthog to reduce memory consumption

* chore: end span after streaming all results

* chore: tracing for clickhouse streaming
2025-01-10 13:42:03 +00:00
Marc Klingen c79c84ea96 chore: release v3.6.2 2025-01-10 14:49:19 +01:00
Hassieb PakzadandGitHub 4634669fcd chore(docker-compose): add ingestion env vars (#4969) 2025-01-10 14:47:25 +01:00
Marc KlingenandGitHub 549e0eb629 chore: daily instead of hourly telemetry (#4968) 2025-01-10 14:10:51 +01:00
Steffen SchmitzandGitHub 80adf63fe6 fix: adjust deletion order for apikey invalidation on project delete (#4964) 2025-01-10 10:24:26 +00:00
Hassieb Pakzad 6c1d7534bb chore: release v3.6.1 2025-01-10 09:15:57 +01:00
Max DeichmannandGitHub 23a4bbd2cb fix: fix trace exports (#4958)
* fix: fix trace exports

* push

* fix: fix trace exports
2025-01-09 19:36:07 +00:00
Steffen SchmitzandGitHub 07d048d46f chore: enable clickhouse cluster mode by default (#4956) 2025-01-09 16:32:55 +00:00
Hassieb PakzadandGitHub bd7226bbb8 fix(pubic-api): observations and traces FINAL (#4951) 2025-01-09 17:41:35 +01:00
Steffen SchmitzandGitHub fe70d92211 chore: set created_at in ClickHouse based on min S3 timestamp (#4955)
* chore: set created_at in ClickHouse based on min S3 timestamp

* chore: fix worker tests
2025-01-09 16:12:23 +00:00
Max DeichmannandGitHub bb3621f31d fix: sessions metrics api performance (#4954) 2025-01-09 17:15:30 +01:00
Steffen SchmitzandGitHub 63225a9fd6 chore: accept projectId list which skips clickhouse reads during ingestion (#4952) 2025-01-09 14:13:23 +00:00
Max DeichmannandGitHub 947da9cc84 fix: sessions table id filter (#4953) 2025-01-09 14:33:09 +01:00
Max DeichmannandGitHub 93a0ab6f3a fix: remove trace tracesession fk (#4946)
* fix: remove trace tracesession fk

* push

* push

* push

* fix: remove trace tracesession fk

* fix: remove trace tracesession fk

* fix: remove trace tracesession fk
2025-01-09 09:50:58 +00:00
Hassieb PakzadandGitHub 68ad079d8c fix(models-table): sorting across pages (#4947) 2025-01-09 10:30:00 +01:00
Max DeichmannandGitHub cf29c6b7e4 feat: backup redis queues to postgres (#4943)
feat: backup redis queuer
2025-01-08 23:15:03 +00:00
Max DeichmannandGitHub e4d2a5ec6c perf: improve session all time search performance (#4926)
* push

* fix: correctly export categorical scores for trace exports

* fixes

* push

* Merge branch 'max/lfe-3345-fix-session-table-all-time-search' of https://github.com/langfuse/langfuse into max/lfe-3345-fix-session-table-all-time-search

* fix

* fixes
2025-01-08 16:54:01 +00:00
Steffen SchmitzandGitHub 35f3092a1c feat: retry 429 errors for eval executions for up to 24h (#4933)
* feat: retry 429 errors for eval executions for up to 24h

* chore: skip type error retries for eval execution
2025-01-08 14:51:36 +00:00
Hassieb PakzadandGitHub 7b222318e1 chore(cost): add langchain-compatible price keys (#4932) 2025-01-08 15:49:21 +01:00
Steffen SchmitzandGitHub d9c077ea56 chore: add BUILD_ID to otel setup and exclude health from tracing (#4930)
* chore: add BUILD_ID to otel setup

* chore: remove comment
2025-01-08 13:17:00 +00:00
Hassieb PakzadandGitHub 1ec7325b85 feat(models): lastUsed column to full lookback (#4931) 2025-01-08 14:21:07 +01:00
Steffen SchmitzandGitHub 96b2127f36 chore: reduce number of parsing iterations for observations and traces (#4927)
* chore: reduce number of parsing iterations for observations and traces

* chore: lint
2025-01-08 12:36:37 +00:00
Max DeichmannandGitHub d3c2b8cf27 fix: correctly export categorical scores for trace exports (#4928)
* fix: correctly export categorical scores for trace exports

* push
2025-01-08 11:31:50 +00:00
Hassieb PakzadandGitHub beeef222e4 feat(models): sort models table by lastUsed (#4925) 2025-01-08 12:06:57 +01:00
steffen911 ee376f9cad chore: release v3.6.0 2025-01-07 14:43:35 +01:00
c8936ec68d feat: add CLICKHOUSE_DB support (#4911)
* take CLICKHOUSE_DB environment variable into account

Fixes: #4888
Signed-off-by: Aleksandar Pesic <peske.nis@gmail.com>

* chore: cleanup env usage

* chore: extend env.prod.example

---------

Signed-off-by: Aleksandar Pesic <peske.nis@gmail.com>
Co-authored-by: Aleksandar Pesic <peske.nis@gmail.com>
2025-01-07 13:18:03 +00:00
Max DeichmannandGitHub 8d8170c62c security: prevent prototype pollution in dataset compare view (#4910) 2025-01-07 12:15:33 +00:00
Max DeichmannandGitHub 72e1a43192 security: upgrade nanoid (#4909) 2025-01-07 11:47:31 +00:00
Hassieb PakzadandGitHub a842625bd1 chore(media): increase rate-limit to use ingestion budget (#4907) 2025-01-07 11:42:07 +01:00
Max DeichmannandGitHub 890a542759 fix: do not error on 403 by model provider for eval executions (#4905)
* fix: do not error on 403 by model provider

* push
2025-01-07 10:23:58 +00:00
Steffen SchmitzandGitHub 1e8af5b821 chore: move sessions, traces, comments router logs to logger (#4902)
* chore: move sessions router logs to logger

* chore: adjust traces and comments
2025-01-07 07:45:44 +00:00
Hassieb PakzadandGitHub d3f3f354db fix(models): allow string tokenizer config (#4898) 2025-01-06 16:24:49 +01:00
steffen911 18a7cb6684 chore: release v3.5.3 2025-01-06 13:43:19 +01:00
Steffen SchmitzandGitHub 2d70ca574e chore: skip existence check for score deletion (#4896)
* chore: skip existence check for score deletion

* chore: lint
2025-01-06 11:03:51 +00:00
Steffen SchmitzandGitHub 1c980c78dd fix: use singular unit in clickhouse datediff for 23.x support (#4894) 2025-01-06 10:35:53 +00:00
Marc KlingenGitHubMax Deichmanndependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>Steffen SchmitzHassieb Pakzad
5170718aad fix(prompt-experiments): do not restrict to evalModelsOnly in order to use anthropic, bedrock, vertex (#4872)
* fix(prompt-experiments): do not restrict to evalModelsOnly in order to use anthropic, bedrock, vertex

* chore(cloud): disable deletion of traces via UI as it caused performance issues (#4873)

chore(cloud): disable deletion of traces via the UI as it caused performance issues

* docs: add link to data model documentation to api reference of ingestion endpoint (#4877)

* fix: use database upserts correctly (#4884)

* fix: use database upserts correctly

* fix: use database upserts correctly

* push

* fix: use database upserts correctly

* fix: use database upserts correctly

* push

* fix: use database upserts correctly

* chore(deps): bump next from 14.2.15 to 14.2.21 (#4887)

Bumps [next](https://github.com/vercel/next.js) from 14.2.15 to 14.2.21.
- [Release notes](https://github.com/vercel/next.js/releases)
- [Changelog](https://github.com/vercel/next.js/blob/canary/release.js)
- [Commits](https://github.com/vercel/next.js/compare/v14.2.15...v14.2.21)

---
updated-dependencies:
- dependency-name: next
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>

* chore: release v3.5.2

* chore: remove postgres from ingestion merge (#4467)

* chore: remove postgres from ingestion merge

* chore: remove pg tests

* chore: remove postgres tracing

* remove evalModelsOnly

---------

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: Max Deichmann <m.deichmann@tum.de>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Steffen Schmitz <steffen@langfuse.com>
Co-authored-by: Hassieb Pakzad <68423100+hassiebp@users.noreply.github.com>
2025-01-06 10:29:57 +00:00
Steffen SchmitzandGitHub a01c27f4cc chore: remove postgres from ingestion merge (#4467)
* chore: remove postgres from ingestion merge

* chore: remove pg tests

* chore: remove postgres tracing
2025-01-06 09:29:26 +00:00
Max Deichmann 51354e6c6d chore: release v3.5.2 2025-01-06 10:34:55 +01:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
10dd50cff0 chore(deps): bump next from 14.2.15 to 14.2.21 (#4887)
Bumps [next](https://github.com/vercel/next.js) from 14.2.15 to 14.2.21.
- [Release notes](https://github.com/vercel/next.js/releases)
- [Changelog](https://github.com/vercel/next.js/blob/canary/release.js)
- [Commits](https://github.com/vercel/next.js/compare/v14.2.15...v14.2.21)

---
updated-dependencies:
- dependency-name: next
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-01-06 09:19:47 +00:00
Max DeichmannandGitHub d8783f8698 fix: use database upserts correctly (#4884)
* fix: use database upserts correctly

* fix: use database upserts correctly

* push

* fix: use database upserts correctly

* fix: use database upserts correctly

* push

* fix: use database upserts correctly
2025-01-03 21:45:56 +00:00
Marc KlingenandGitHub 16a572f51d docs: add link to data model documentation to api reference of ingestion endpoint (#4877) 2025-01-02 21:10:26 +00:00
Marc KlingenandGitHub 947641cbbf chore(cloud): disable deletion of traces via UI as it caused performance issues (#4873)
chore(cloud): disable deletion of traces via the UI as it caused performance issues
2025-01-02 19:03:40 +01:00
Marc KlingenandGitHub 249e161473 chore: remove beta label from evaluation menu (#4871) 2025-01-02 15:14:32 +00:00
Marc KlingenandGitHub 90bab4386f fix(ui): improve /trace/:id redirect when trace is accessed while still being processed (#4868) 2025-01-02 13:51:56 +00:00
Max DeichmannandGitHub 33fddb1344 fix: support large amount of traces for sessions in session.byid (#4847) 2024-12-29 21:42:50 +00:00
Hassieb PakzadandGitHub c24dcb068a fix(legacy-ingestion): usageDetails support (#4841) 2024-12-27 10:11:24 +01:00
Max DeichmannandGitHub 2986723254 fix: ensure idemptence of project deletion queue (#4831) 2024-12-24 11:49:29 +00:00
Max DeichmannandGitHub b45b2fd8f8 chore: clean up error message bullmq endpoint (#4826) 2024-12-23 21:09:57 +00:00
Marc Klingen 8936fb4ad9 chore: release v3.5.0 2024-12-23 17:39:13 +01:00
Marc KlingenandGitHub 7d7e25b39c fix: use clickhouse for tracing usage stats (#4825) 2024-12-23 17:07:50 +01:00
Marc KlingenandGitHub 138d2f1c09 feat(ui): switch to github discussions and issues for in-product feedback (#4822) 2024-12-21 14:16:27 +01:00
Steffen SchmitzandGitHub 51595eb15d chore: export core information tables to S3 daily (#4821) 2024-12-21 12:23:10 +00:00
Marc KlingenandGitHub 6ee68cf508 chore: posthog integration out of beta (#4820)
chore: Posthog integration out of Beta
2024-12-21 12:03:30 +00:00
Marc KlingenandGitHub 308513e644 chore(ui): remove dateRangeFilter on prompt metrics table as no longer necessary on CH (#4818)
* chore(ui): remove dateRangeFilter on prompt metrics table as no longer necessary on CH

* fix lint
2024-12-21 11:49:03 +00:00
Marc KlingenandGitHub 155543bc93 feat(ui): enable the creation of a new LLM connection from the LLM selection dropdown (#4819)
feat(ui): allow to create new llm connection from llm selection dropdown
2024-12-21 11:46:34 +00:00
Marc KlingenandGitHub 8f961925d5 feat(ui): add export status table to project settings (#4816) 2024-12-21 04:38:03 +01:00
Marc KlingenandGitHub 7ddc844d7d fix(ui): clarify what applying producation label to a new prompt does (#4812)
* fix(ui): clarify what applying producation label to a new prompt does

* fix lint and copy
2024-12-21 01:54:52 +00:00
Marc KlingenandGitHub 5381c5f19b feat(cloud): improve open/close state of support chat (#4815)
chore(cloud): hide chat by default, show when opened manually or on incoming/unread messages, persist in session storage
2024-12-21 01:40:39 +00:00
Marc Klingen dcd6060665 chore: release v3.4.0 2024-12-20 22:41:08 +01:00
Chris TranandGitHub 0bfba7d690 feat(auth): add AUTH_CUSTOM_CLIENT_AUTH_METHOD to support PKCE code flow (#4810) 2024-12-20 22:34:02 +01:00
Hassieb Pakzad 25b363e1bc chore: fix eslint error 2024-12-20 18:54:34 +01:00
Hassieb PakzadandGitHub 0de37b0437 feat(models): add UI to add all usage types (#4802) 2024-12-20 18:49:44 +01:00
Max DeichmannandGitHub ab5febb9c7 fix: include time filter into user metrics query (#4808)
* fixes: fix user table sorting and filters

* fixes: fix user table sorting and filters

* fixes: fix user table sorting and filters
2024-12-20 15:59:11 +00:00
marliessophieandGitHub 0e5cade9e7 chore(ui): toggle json editor for text prompts (#4807) 2024-12-20 15:22:28 +00:00
Max DeichmannandGitHub 1438e052df fix: fix json parsing of trace / observation i/o (#4805)
* remove only

* push

* remove only
2024-12-20 12:54:47 +00:00
Max DeichmannandGitHub 9917aff8cf fix: add latencies to trace exports (#4804)
* fix: add latencies to trace exports

* remove only
2024-12-20 12:23:52 +00:00
marliessophieandGitHub c915c2b678 chore(ui): move dataset details into popover on runs table (#4801)
* chore(ui): move dataset details into popover on runs table

* push
2024-12-20 11:41:38 +00:00
marliessophieandGitHub 50379bd4b5 feat(datasets): support charts for scores and resource metrics (#4754)
* fixup: dataset run analytics; compare

* refactor: extract common timeseries component

* push

* fix: proper limit and offset settings

* chore: extend route to accept run ids

* eslint

* chore: order chart data ASC

* styles

* push

* push

* style: adjust multi-select component

* push

* push

* push
2024-12-20 10:14:37 +00:00
Marc KlingenandGitHub 8fd93ecff9 fix(cloud): remove unnecessary posthog events (#4798) 2024-12-20 01:59:39 +01:00
Max DeichmannandGitHub 046c6c6125 fix: fix tests (#4797) 2024-12-20 00:04:31 +01:00
Max DeichmannandGitHub ba750a2d55 fix: fix time to first token in trace detail view (#4796) 2024-12-19 23:41:40 +01:00
Marc Klingen 485120806e chore: release v3.3.0 2024-12-19 22:02:50 +01:00
87acc4f7f3 feat: redirect ingestion traffic for specific projects to secondary queue (#4792)
* feat: redirect specific projects to secondary ingestion queue

* feat: redirect specific projects to secondary ingestion queue

* push

* Update worker/src/queues/ingestionQueue.ts

Co-authored-by: Steffen Schmitz <steffen@langfuse.com>

* push

* push

---------

Co-authored-by: Steffen Schmitz <steffen@langfuse.com>
2024-12-19 16:06:20 +00:00
Steffen SchmitzandGitHub 21b1faa21b fix: adjust clickhouse type mapping for session duration filter (#4791) 2024-12-19 13:04:39 +00:00
Max DeichmannandGitHub 76cfaf1069 chore: measure lookup hits in ingestion pipeline (#4787) 2024-12-19 10:20:10 +01:00
dbe808d520 feat(auth): add AUTH_IGNORE_ACCOUNT_FIELDS to sanitize IDP fields before creating an account (#4728)
* feat: Field sanitization before creating an Account

* add comments

---------

Co-authored-by: Marc Klingen <git@marcklingen.com>
2024-12-18 23:26:56 +00:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
6c977429c2 chore(deps-dev): bump tailwindcss from 3.4.13 to 3.4.17 (#4784)
Bumps [tailwindcss](https://github.com/tailwindlabs/tailwindcss) from 3.4.13 to 3.4.17.
- [Release notes](https://github.com/tailwindlabs/tailwindcss/releases)
- [Changelog](https://github.com/tailwindlabs/tailwindcss/blob/v3.4.17/CHANGELOG.md)
- [Commits](https://github.com/tailwindlabs/tailwindcss/compare/v3.4.13...v3.4.17)

---
updated-dependencies:
- dependency-name: tailwindcss
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-12-18 23:24:43 +00:00
Marc KlingenandGitHub c42c6a0f91 chore: do not add latest, major, and major.minor tags to docker images of -rc versions (#4781) 2024-12-18 21:37:46 +01:00
Max DeichmannandGitHub f14b69f68d chore: add attributes to ingestion queue processing (#4779)
git push
2024-12-18 19:54:32 +00:00
marliessophieandGitHub dd892459a4 chore(ui): order score names alphabetically in dropdowns (#4776)
* chore(ui): order score names alphabetically in dropdowns

* push
2024-12-18 16:40:04 +00:00
Marc KlingenandGitHub 3488b5c443 docs: Update README.md 2024-12-18 03:51:09 +01:00
Max DeichmannandGitHub 6e0f4483b4 chore: increase rate limits (#4751) 2024-12-17 15:55:18 +00:00
Marc KlingenandGitHub b5d9fb3e63 chore(cloud): remove posthog metrics cron job (#4753) 2024-12-17 15:52:42 +00:00
Marc KlingenandGitHub e087da6b6e docs: update README.md 2024-12-17 12:53:21 +01:00
Marc KlingenandGitHub ea8734d709 docs: update readme (#4749) 2024-12-17 12:49:14 +01:00
Hassieb PakzadandGitHub 9cf4e57da1 fix(dashboard-modelUsage): none model name handling (#4748) 2024-12-17 10:52:36 +01:00
Marc KlingenandGitHub 292501b6dd feat(ui): add organizations menu item link for all global pages (#4741) 2024-12-16 20:46:57 +00:00
Hassieb PakzadandGitHub 2a1424e4d0 fix(dashboard-modelUsage): NaN handling on totals (#4738) 2024-12-16 18:28:19 +01:00
steffen911 fd37e2c35e chore: release v3.2.0 2024-12-16 18:04:14 +01:00
marliessophieandGitHub 0352e7fad6 chore(annotation): return previous score props in delete method (#4737)
chore: return previous annotation score in delete method
2024-12-16 16:47:37 +00:00
Hassieb PakzadandGitHub 6c4482ff56 feat(cost-tracking): allow additional cost and usage keys (#4536) 2024-12-16 17:30:24 +01:00
marliessophieandGitHub cc62b32b82 fix(annotation): fail postgres upsert, delete silentley while Clickhouse is in lead (#4733)
* fix(annotation): fail postgres upsert, delete silentley while Clickhouse is in lead

* chore: disable deletion of score while update operation is in progress
2024-12-16 14:40:04 +00:00
Steffen SchmitzandGitHub b421c2dec5 feat: add posthog integration processing (#4685)
* feat: add posthog integration processing

* chore: add times for query optimization

* chore: cleanups

* chore: remove unused join

* chore: run posthog integration every hour

* chore: rename to langfuse_event_version

* chore: add date workaround for initial sync

* chore: query updates

* chore: remove scheduling info
2024-12-16 13:43:07 +00:00
Marc KlingenandGitHub 3a67533ac8 feat(ui): new experiment modal to highlight prompt and custom experiments (#4731)
* feat(ui): new experiment modal to highlight prompt and custom experiments

* push

* fix lint

* fix lint
2024-12-16 12:00:22 +00:00
steffen911 21303a275a chore: release v3.1.1
CI/CD / lint (push) Waiting to run
CI/CD / test-docker-build (push) Waiting to run
CI/CD / tests-web-sync (node20, pg12) (push) Waiting to run
CI/CD / tests-web-sync (node20, pg15) (push) Waiting to run
CI/CD / tests-web-async (node20, pg12, mode) (push) Waiting to run
CI/CD / tests-web-async (node20, pg15, mode) (push) Waiting to run
CI/CD / tests-web-async (node20, pg12, mode-azure) (push) Waiting to run
CI/CD / tests-web-async (node20, pg15, mode-azure) (push) Waiting to run
CI/CD / tests-worker (node20, pg12, mode) (push) Waiting to run
CI/CD / tests-worker (node20, pg15, mode) (push) Waiting to run
CI/CD / tests-worker (node20, pg12, mode-azure) (push) Waiting to run
CI/CD / tests-worker (node20, pg15, mode-azure) (push) Waiting to run
CI/CD / e2e-tests (push) Waiting to run
CI/CD / e2e-server-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-12-16 11:03:21 +01:00
Steffen SchmitzandGitHub 284494c62a fix: use correct golang-migrate in build (#4729)
* fix: use correct golang-migrate in build

* chore: skip docker push

* chore: run docker build right away

* chore: add args for targetplatform and buildplatform

* chore: add TARGETPLATFORM args
2024-12-16 09:44:58 +00:00
Marc KlingenandGitHub b37ec03483 fix(ui): redirect after org/project deletion when using custom base path (#4727) 2024-12-16 09:18:53 +00:00
Steffen SchmitzandGitHub a49a410a04 chore: remove filterlist console log (#4718) 2024-12-13 15:48:09 +00:00
Steffen SchmitzandGitHub 1e9d8de305 fix: rate limit deletions globally to 1 per 3s (#4715) 2024-12-13 10:09:53 +00:00
Marc KlingenandGitHub 0fa88aea57 feat: in HF Spaces, prompt opening in new tab when running in iframe (#4713) 2024-12-13 02:41:37 +01:00
Max DeichmannandGitHub 61240d7c92 fix: fix pagination on public api (#4710)
* fix: fix pagination

* fix: fix pagination

* fix: fix pagination

* fix: fix pagination

* fix: fix pagination

* fix: fix pagination
2024-12-12 23:02:46 +00:00
marliessophieandGitHub d54c888cd9 chore(evals/experiments): support retries via bullmq; set 2 minute LLM call completion timeouts (#4709)
* chore(eval/experiments): support job retries via bullmq instead of within worker

* chore: timeout model completion calls after 2 minutes

* chore: set retries on llm completion calls to 1

* chore: do not propogate expected error cases in experiment creation

* eslint
2024-12-12 20:53:56 +00:00
Max DeichmannandGitHub b87a00fa03 fix: do not fetch dataset run items which are not active (#4708)
* fix

* fix

* push

* fix
2024-12-12 20:31:43 +00:00
marliessophieandGitHub 8b8d1086e5 fix(organizations): block org deletion in case of active projects (#4707)
* fix(organizations): block org deletion in case of active projects

* chore: count soft and hard deleted projects as
2024-12-12 19:23:27 +00:00
Max DeichmannandGitHub d88902316b fix: fix level filter on trace upsert queue (#4704) 2024-12-12 15:40:47 +01:00
Steffen SchmitzandGitHub 5dd5adf850 fix: use clickhouse for failIfRecentEvent condition in healthcheck (#4701) 2024-12-12 12:22:19 +00:00
Max Deichmann a56246059c chore: release v3.1.0
CI/CD / lint (push) Waiting to run
CI/CD / test-docker-build (push) Waiting to run
CI/CD / tests-web-sync (node20, pg12) (push) Waiting to run
CI/CD / tests-web-sync (node20, pg15) (push) Waiting to run
CI/CD / tests-web-async (node20, pg12, mode) (push) Waiting to run
CI/CD / tests-web-async (node20, pg15, mode) (push) Waiting to run
CI/CD / tests-web-async (node20, pg12, mode-azure) (push) Waiting to run
CI/CD / tests-web-async (node20, pg15, mode-azure) (push) Waiting to run
CI/CD / tests-worker (node20, pg12, mode) (push) Waiting to run
CI/CD / tests-worker (node20, pg15, mode) (push) Waiting to run
CI/CD / tests-worker (node20, pg12, mode-azure) (push) Waiting to run
CI/CD / tests-worker (node20, pg15, mode-azure) (push) Waiting to run
CI/CD / e2e-tests (push) Waiting to run
CI/CD / e2e-server-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-12-12 13:34:26 +01:00
Max DeichmannandGitHub 833f4165ce chore: improve naming for observations by level chart (#4700)
* feat: add observations status chart

* fix: improve naming for observations by level chart
2024-12-12 12:20:18 +00:00
Max DeichmannandGitHub ebe0c034fd security: upgrade next-auth (#4690)
* upgrade next-auth

* push

* eval execution time
2024-12-12 12:14:33 +00:00
Max DeichmannandGitHub 64492f3002 feat: add observations status chart (#4697) 2024-12-12 12:56:53 +01:00
Max DeichmannandGitHub 27faebe244 chore: improve trace export tests (#4694) 2024-12-12 10:15:37 +00:00
Marc KlingenandGitHub 5e681e5a6d feat(ui): add editable note for each api key (#4686)
* feat(ui): add editable note for each api key

* fix access scopes
2024-12-11 16:51:17 +00:00
marliessophieandGitHub 8af88a0cec fix(playground): Model side menu in playground not scrollable (#4687) 2024-12-11 16:48:18 +00:00
marliessophieandGitHub 1770579fa6 feat(annotation): optimistic updates on annotation form (#3859)
* wip: optimistic updates on annotation form

* refactor to handle sequence of create, update operations correctly

* fix: don't check postgres for score existence when running on clickhouse

* disable annotation button while score updates are saving

* fix: await for all mutations to complete before opening modal again

* rm console log

* fix: update api response clickhouse
2024-12-11 12:39:05 +00:00
Max DeichmannandGitHub 273d29c383 fix: export i/o for traces (#4673) 2024-12-10 16:21:08 +01:00
marliessophieandGitHub 0047a3dec6 fix(experiments): don't require name in experiment creation (#4659)
* fix(experiments): don't require name in experiment creation

* push

* push

* push

* push
2024-12-10 15:00:15 +00:00
Hassieb PakzadandGitHub b63aeeeae7 fix(vertex): at least 1 user message always (#4666) 2024-12-10 10:46:17 +01:00
Marc KlingenandGitHub f5dc7eea07 docs: update upgrade guide url in entrypoint (#4655) 2024-12-09 13:36:27 +00:00
Steffen SchmitzandGitHub 8560a10e53 fix: handle non-existent projects gracefully on delete (#4654)
* fix: handle non-existent projects gracefully on delete

* chore: return early on project does not exist
2024-12-09 13:14:20 +00:00
Marc KlingenandGitHub 609a330c37 chore: update default docker compose to use 3 docker tag (#4653) 2024-12-09 13:31:58 +01:00
Hassieb PakzadandGitHub 07ad7e54e3 fix(model-form): increase max token limit to 16384 (#4651) 2024-12-09 11:52:18 +01:00
Max DeichmannandGitHub 4781b91e90 fix: always restart all docker services (#4649)
fix
2024-12-09 10:10:27 +00:00
marliessophieandGitHub 2c68223402 fix(experiments): ensure experiment runs on all valid inputs (#4643)
* fix(experiments): ensure experiment runs on all valid inputs

* refactor: extract common function
2024-12-09 10:01:18 +00:00
Max Deichmann 1211de3efb chore: release v3.0.0
CI/CD / lint (push) Waiting to run
CI/CD / test-docker-build (push) Waiting to run
CI/CD / tests-web-sync (node20, pg12) (push) Waiting to run
CI/CD / tests-web-sync (node20, pg15) (push) Waiting to run
CI/CD / tests-web-async (node20, pg12, mode) (push) Waiting to run
CI/CD / tests-web-async (node20, pg15, mode) (push) Waiting to run
CI/CD / tests-web-async (node20, pg12, mode-azure) (push) Waiting to run
CI/CD / tests-web-async (node20, pg15, mode-azure) (push) Waiting to run
CI/CD / tests-worker (node20, pg12, mode) (push) Waiting to run
CI/CD / tests-worker (node20, pg15, mode) (push) Waiting to run
CI/CD / tests-worker (node20, pg12, mode-azure) (push) Waiting to run
CI/CD / tests-worker (node20, pg15, mode-azure) (push) Waiting to run
CI/CD / e2e-tests (push) Waiting to run
CI/CD / e2e-server-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-12-09 11:15:37 +01:00
Zhiqiang ZHOUandGitHub 9b9fd35f13 fix(openapi): MediaContentType to enum (#4627)
Signed-off-by: STRRL <im@strrl.dev>
2024-12-09 11:13:00 +01:00
Steffen SchmitzandGitHub 407f5dd1ce build: add latest tags to v3 tags (#4648) 2024-12-09 09:59:32 +00:00
Steffen SchmitzandGitHub d7832336dd fix: allow userId filter on /api/public/observations again (#4644)
* fix: allow userId filter on /api/public/observations again

* Update observation-api.servertest.ts
2024-12-09 09:46:51 +00:00
Max DeichmannandGitHub 6362d8bcc9 fix: always pull the project_id for generations queries (#4641)
push
2024-12-09 08:19:15 +00:00
Max Deichmann 35057ea443 chore: release v3.0.0-rc.6
CI/CD / lint (push) Waiting to run
CI/CD / test-docker-build (push) Waiting to run
CI/CD / tests-web-sync (node20, pg12) (push) Waiting to run
CI/CD / tests-web-sync (node20, pg15) (push) Waiting to run
CI/CD / tests-web-async (node20, pg12, mode) (push) Waiting to run
CI/CD / tests-web-async (node20, pg15, mode) (push) Waiting to run
CI/CD / tests-web-async (node20, pg12, mode-azure) (push) Waiting to run
CI/CD / tests-web-async (node20, pg15, mode-azure) (push) Waiting to run
CI/CD / tests-worker (node20, pg12, mode) (push) Waiting to run
CI/CD / tests-worker (node20, pg15, mode) (push) Waiting to run
CI/CD / tests-worker (node20, pg12, mode-azure) (push) Waiting to run
CI/CD / tests-worker (node20, pg15, mode-azure) (push) Waiting to run
CI/CD / e2e-tests (push) Waiting to run
CI/CD / e2e-server-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
2024-12-09 08:52:46 +01:00
Max DeichmannandGitHub 9adc3bee5b fix: default to empty string for csv export in case of null (#4640) 2024-12-08 22:56:48 +01:00
Max DeichmannandGitHub e57af47d84 fix: fix trace tagging without postgres (#4639)
* fix: fix trace tagging without postgres

* fix: fix trace tagging without postgres
2024-12-08 21:43:00 +00:00
Max DeichmannandGitHub 8f0247d311 fix: fix trace bookmarking (#4638)
fix: fix trace publishing
2024-12-08 22:22:07 +01:00
Max DeichmannandGitHub e96b2a4b0f feat: move exports to ch (#4634) 2024-12-08 22:07:33 +01:00
Max DeichmannandGitHub c389d4f01a feat: move session tests to async suite (#4636)
* feat: move session tests to async suite

* feat: move session tests to async suite

* feat: move session tests to async suite
2024-12-08 13:21:14 +00:00
Max Deichmann 4d8bb0fb95 chore: release v3.0.0-rc.5
CI/CD / lint (push) Waiting to run
CI/CD / test-docker-build (push) Waiting to run
CI/CD / tests-web-sync (node20, pg12) (push) Waiting to run
CI/CD / tests-web-sync (node20, pg15) (push) Waiting to run
CI/CD / tests-web-async (node20, pg12, mode) (push) Waiting to run
CI/CD / tests-web-async (node20, pg15, mode) (push) Waiting to run
CI/CD / tests-web-async (node20, pg12, mode-azure) (push) Waiting to run
CI/CD / tests-web-async (node20, pg15, mode-azure) (push) Waiting to run
CI/CD / tests-worker (node20, pg12, mode) (push) Waiting to run
CI/CD / tests-worker (node20, pg15, mode) (push) Waiting to run
CI/CD / tests-worker (node20, pg12, mode-azure) (push) Waiting to run
CI/CD / tests-worker (node20, pg15, mode-azure) (push) Waiting to run
CI/CD / e2e-tests (push) Waiting to run
CI/CD / e2e-server-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
2024-12-08 10:52:56 +01:00
Marc KlingenandGitHub 79068ef93f chore: use s3 region auto in docker-compose.yml (#4630) 2024-12-07 17:26:43 +00:00
6c1c231ba7 fix: support reading observations from clickhouse for observations in annotation queue (#4621)
Co-authored-by: Max Deichmann <m.deichmann@tum.de>
2024-12-07 18:10:20 +01:00
Max DeichmannandGitHub b722892c00 fix: check postgres negative flag (#4624)
* fix: check postgres negative flag

* improve dashboard

* improve dashboard

* improve dashboard

* improve dashboard

* perf: fetch observations using timestamps

* perf: fetch observations using timestamps

* perf: fetch observations using timestamps

* perf: fetch observations using timestamps
2024-12-07 14:52:55 +00:00
Max DeichmannandGitHub 4cd1d167da perf: fetch observations using timestamps (#4629) 2024-12-07 13:35:45 +00:00
Steffen SchmitzandGitHub c0f38a8eda chore: hide background migrations on cloud and show queued status (#4628) 2024-12-07 07:59:02 +00:00
marliessophieandGitHub 6be87994a9 fix: only read trace and score from prisma if LANGFUSE_POSTGRES_INGESTION_ENABLED (#4618)
* chore: remove reading trace and score from prisma, only read from clickhouse

* fixup: keep create logic behind flag

* chore: only read from postgres for updating annoation score if `LANGFUSE_POSTGRES_INGESTION_ENABLED`

* chore: only read from postgres for deleting annoation score if `LANGFUSE_POSTGRES_INGESTION_ENABLED`

* chore: throw errors if scores not found in CH

* chore: add audit logs in clickhouse scenario

* only create audit log once on cloud
2024-12-06 18:42:01 +00:00
Max DeichmannandGitHub e90bb60ec8 fix: fix traces publishing in V3 without postgres (#4620)
* make sleep great again

* make sleep great again
2024-12-06 18:22:53 +00:00
Max DeichmannandGitHub 439a65b926 fix: fix numeric chart for dashboards (#4615) 2024-12-06 18:15:25 +00:00
Max DeichmannandGitHub 11af1f4167 feat: increase trace upsert delay (#4619)
* fix: increase trace upsert delay

* fix: increase trace upsert delay
2024-12-06 17:15:23 +00:00
marliessophieandGitHub 2d265eb3f9 feat(comments): add support for comment button on each trace preview in session view (#3848)
* feat(comments): add support for comment button on each trace preview in single session view

* perf: manually timeout comment count query at 500ms without failing overall operation

* push

* chore: query comment reference objects in clickhouse if `env.clickhouse_url`

* remove: deprecated code

* chore: support fetching traces per session from clickhouse

* push

* refactor: adjust tests to populate traces and observations in clickhouse

* refactor: adjust tests and remove redundant code

* push
2024-12-06 16:35:15 +00:00
Max DeichmannandGitHub a93b8ff119 chore: move eval to correct ee directory (#4616)
* chore: move eval to correct ee directory

* chore: move eval to correct ee directory

* chore: move eval to correct ee directory
2024-12-06 16:31:10 +00:00
Max DeichmannandGitHub 81cf48ddf0 feat: set v3 default variables (#4613) 2024-12-06 16:46:09 +01:00
816eec0f75 chore: drop FK v3 constraints (#4612)
* chore: drop FK v3 constraints

* make sleep great again

* make sleep great again

* make sleep great again

* make sleep great again

---------

Co-authored-by: steffen911 <steffen@langfuse.com>
2024-12-06 13:02:54 +00:00
steffen911 fa630d34de chore: release v3.0.0-rc.4
CI/CD / lint (push) Waiting to run
CI/CD / test-docker-build (push) Waiting to run
CI/CD / tests-web-sync (node20, pg12) (push) Waiting to run
CI/CD / tests-web-sync (node20, pg15) (push) Waiting to run
CI/CD / tests-web-async (node20, pg12, mode) (push) Waiting to run
CI/CD / tests-web-async (node20, pg15, mode) (push) Waiting to run
CI/CD / tests-web-async (node20, pg12, mode-azure) (push) Waiting to run
CI/CD / tests-web-async (node20, pg15, mode-azure) (push) Waiting to run
CI/CD / tests-worker (node20, pg12, mode) (push) Waiting to run
CI/CD / tests-worker (node20, pg15, mode) (push) Waiting to run
CI/CD / tests-worker (node20, pg12, mode-azure) (push) Waiting to run
CI/CD / tests-worker (node20, pg15, mode-azure) (push) Waiting to run
CI/CD / e2e-tests (push) Waiting to run
CI/CD / e2e-server-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
2024-12-06 14:01:36 +01:00
0eb129e186 feat: add ui for background migrations (#4602)
* feat: add ui for background migrations

* chore: add retry for background migrations

* chore: remove redundant import

* chore: invalidate correct state

* chore: unused import

* chore: update the popup text

* chore: add status icon

* chore: move to update menu

---------

Co-authored-by: steffen911 <steffen@langfuse.com>
2024-12-06 12:49:26 +00:00
marliessophieandGitHub 2defaf273a fix(experiments): don't create experiment run items for archived dataset items (#4611)
* fix(experiments): don't create experiment run items for archived dataset items

* fix: type check for input, `typeof null === "object"` in js
2024-12-06 12:02:46 +00:00
Max DeichmannandGitHub b9305e76f5 fix: fix front end confirmations to delete traces (#4601) 2024-12-05 16:55:28 +01:00
Max DeichmannandGitHub 241d1119f9 feat: adjust trace redirect to uniqueness of trace ids to projects (#4598) 2024-12-05 15:24:11 +00:00
Steffen SchmitzandGitHub 4a44adcedd chore: add wait time during validation of clickhouse background migra… (#4596)
chore: add wait time during validation of clickhouse background migrations
2024-12-05 13:53:09 +00:00
Steffen SchmitzandGitHub 9a861dff37 chore: make project deletion asynchronous (#4399)
* chore: make project deletion asynchronous

* chore: delete projects on the worker

* chore: indicate in UI overview that project is being deleted

* chore: remove API keys from Postgres in project deletion call
2024-12-05 13:13:22 +00:00
Steffen SchmitzandGitHub 12ee00f273 chore: check early whether clickhouse url is configured (#4595)
* chore: check early whether clickhouse url is configured

* Update entrypoint.sh
2024-12-05 12:51:23 +00:00
Steffen SchmitzandGitHub f2513fbebf fix: return timestamps in API response as ISO strings (#4593) 2024-12-05 10:49:22 +00:00
marliessophieandGitHub 5be98906e3 feat(url): support AWS S3 signed images in trace/observation preview (#4575)
* feat(url): support AWS S3 signed images in trace/observation preview

* push

* push

* chore: also validate content-type of pre-signed urls
2024-12-05 09:35:20 +00:00
marliessophieandGitHub 4bac94e3fc feat(comments): increase content character limit to 3,000 (#4577)
* feat(comments): increase content character limit to 3,000

* push

* push

* push
2024-12-05 08:38:43 +00:00
Max DeichmannandGitHub 86980da792 fix: show correct scores if dataset runs link same traces (#4582)
* fix: fix scores when dataset runs are linked to the same traces

* fix: fix scores when dataset runs are linked to the same traces
2024-12-04 22:14:18 +00:00
Max DeichmannandGitHub f9be079d62 chore: add aggregateScores tests (#4580)
* chore: add aggregateScores tests

* chore: add aggregateScores tests
2024-12-04 21:06:43 +00:00
Marc KlingenandGitHub d66fac5e29 chore: explicitly mention worker/src/ee (#4581) 2024-12-04 21:02:51 +00:00
Hassieb PakzadandGitHub 83c01bb444 fix(media): add self host env vars (#4578) 2024-12-04 19:07:37 +01:00
Hassieb PakzadandGitHub c937fc7280 chore(cost-tracking): add gpt-4o-2024-11-20 prices (#4576) 2024-12-04 17:10:36 +00:00
Hassieb PakzadandGitHub c8aa4a2d70 chore(ingestion): add back async ingestion processing flag (#4573) 2024-12-04 17:28:18 +01:00
Marc KlingenandGitHub 5d459f0143 fix(cloud): only active evals count towards free plan limit (#4572)
* fix(cloud): only active evals count towards free plan limit

* remove log
2024-12-04 15:22:45 +00:00
Steffen SchmitzandGitHub faa1b71f48 fix: perform batch trace deletion on clickhouse (#4570) 2024-12-04 14:17:09 +00:00
Max DeichmannandGitHub 99e9825e66 perf: improve user metrics for large projects (#4568)
* feat: improve user metrics

* feat: improve user metrics

* feat: improve user metrics

* feat: improve user metrics

* feat: improve user metrics
2024-12-04 14:11:28 +00:00
marliessophieandGitHub 11ad444632 fix(ui): show scrollbar for scores in column visibility (#4569) 2024-12-04 13:56:56 +00:00
Steffen SchmitzGitHubellipsis-dev[bot] <65095814+ellipsis-dev[bot]@users.noreply.github.com>
f132aa3969 fix: convert json strings to json in traces api metadata field (#4564)
* fix: convert json strings to json in traces api metadata field

* Update web/src/__tests__/async/traces-api.servertest.ts

Co-authored-by: ellipsis-dev[bot] <65095814+ellipsis-dev[bot]@users.noreply.github.com>

* chore: stupid ai

* chore: typing

* chore: lint

---------

Co-authored-by: ellipsis-dev[bot] <65095814+ellipsis-dev[bot]@users.noreply.github.com>
2024-12-04 11:11:43 +00:00
Steffen SchmitzandGitHub fa69d4dc75 chore: increase timeout for daily metrics query (#4565) 2024-12-04 10:46:52 +00:00
Max DeichmannandGitHub 6ef6fba4dc chore: double ingestion API rate limits (#4544)
feat
2024-12-04 09:31:44 +00:00
Max DeichmannandGitHub a5078ccbaf chore: remove public users api (#4553) 2024-12-04 09:20:09 +00:00
Marc KlingenandGitHub ba5c1cd70e fix: disable x frame options header on Hugging Face (#4558) 2024-12-04 00:06:19 +01:00
Max DeichmannandGitHub 19ea989b5a perf: add a user_id bloom filter (#4554)
* perf: add a user_id bloom filter

* push

* perf: add a user_id bloom filter
2024-12-03 21:28:52 +00:00
Max DeichmannandGitHub ec0bf37d0b chore: refactor tests async (#4552)
* chore: refactor tests

* chore: refactor tests
2024-12-03 20:03:57 +00:00
Max DeichmannandGitHub 3b4790270f fix: fix time to first token (#4549)
* fix: fix time to first token

* fix: fix time to first token

* fix: fix time to first token

* push

* fix: fix time to first token
2024-12-03 18:05:40 +00:00
Marc Klingen ca3b95312b chore: release v3.0.0-rc.3
CI/CD / lint (push) Waiting to run
CI/CD / test-docker-build (push) Waiting to run
CI/CD / tests-web-sync (node20, pg12) (push) Waiting to run
CI/CD / tests-web-sync (node20, pg15) (push) Waiting to run
CI/CD / tests-web-async (node20, pg12, mode) (push) Waiting to run
CI/CD / tests-web-async (node20, pg15, mode) (push) Waiting to run
CI/CD / tests-web-async (node20, pg12, mode-azure) (push) Waiting to run
CI/CD / tests-web-async (node20, pg15, mode-azure) (push) Waiting to run
CI/CD / tests-worker (node20, pg12, mode) (push) Waiting to run
CI/CD / tests-worker (node20, pg15, mode) (push) Waiting to run
CI/CD / tests-worker (node20, pg12, mode-azure) (push) Waiting to run
CI/CD / tests-worker (node20, pg15, mode-azure) (push) Waiting to run
CI/CD / e2e-tests (push) Waiting to run
CI/CD / e2e-server-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
2024-12-03 18:53:59 +01:00
b9aaf05acb feat(auth): add KeyCloak authentication option (#2866)
---------

Co-authored-by: RTae <natthanan.bhu@doctorasa.co>
Co-authored-by: Marc Klingen <git@marcklingen.com>
2024-12-03 18:46:09 +01:00
Marc KlingenandGitHub 004aee9f35 feat(ui): add "not found" error page for unknown traces and sessions (#4548)
* feat(ui): add "not found" page for unknown trace ids

* also apply to session
2024-12-03 16:56:03 +00:00
Steffen SchmitzandGitHub 0c9070f78f chore: migrate /metrics/daily API route to Clickhouse (#4546)
* chore: migrate /metrics/daily API route to Clickhouse

* chore: apply final
2024-12-03 16:35:09 +00:00
Marc KlingenandGitHub a91357a958 fix: remove LANGFUSE_CSP_DISABLE (did not work) and disable csp headers on HF Spaces (#4545) 2024-12-03 17:31:18 +01:00
Hassieb PakzadandGitHub 80050cee00 fix(media): add wait time before retries (#4547) 2024-12-03 17:24:43 +01:00
Steffen SchmitzandGitHub b533b1dccd chore: migrate traces all API route to Clickhouse (#4542)
* chore: migrate traces all API route to Clickhouse

* chore: lint

* chore: remove console logs

* chore: naming
2024-12-03 14:04:15 +00:00
Max DeichmannandGitHub 25657d11e4 perf: improve count query performance for scores public api (#4538)
feat
2024-12-03 10:38:41 +00:00
Max DeichmannandGitHub 3dd616a3dc perf: improve single trace public API performance (#4537)
feat
2024-12-03 10:11:04 +00:00
Steffen SchmitzandGitHub 995ec498f4 chore: include projectId in event processing error message (#4535) 2024-12-03 09:22:56 +00:00
Max DeichmannandGitHub 12d7b7f753 perf: improve scores API query performance (#4534)
* feat

* feat

* feat

* push

* feat

* feat

* feat
2024-12-03 09:17:55 +00:00
Marc KlingenandGitHub 2630327a86 chore: add LANGFUSE_CSP_DISABLE to .env.prod.example (#4533) 2024-12-03 01:23:15 +01:00
Marc KlingenandGitHub 4a1fa69af7 feat: optionally disable csp headers via LANGFUSE_CSP_DISABLE=true (#4529) 2024-12-03 01:04:23 +01:00
Max DeichmannandGitHub e53d101b49 fix: show dataset run if not run item exists yet (#4528)
feat
2024-12-02 21:46:54 +00:00
Marc KlingenandGitHub 39539300c1 fix: on cache schema change, delete cached api keys (#4527) 2024-12-02 20:36:12 +00:00
Hassieb PakzadandGitHub 59982db267 fix(io-ui): padding additional input (#4526) 2024-12-02 19:29:37 +01:00
Hassieb PakzadandGitHub fe4299d30d fix(IO-ui): render additional input in pretty view (#4524) 2024-12-02 18:58:25 +01:00
Marc KlingenandGitHub 304b290ab1 fix: no need to decode nextjs path variables, causes issues with special characters (#4522) 2024-12-02 17:09:06 +00:00
Hassieb PakzadandGitHub 97423aa95d fix(media): use db upserts instead of prisma (#4521) 2024-12-02 17:42:53 +01:00
Max DeichmannandGitHub 7aff8e0c26 fix: fix score source public api filter (#4520)
feat
2024-12-02 15:46:59 +00:00
Max DeichmannandGitHub 22761d55c3 chore: add logging for datasets router (#4519)
CI/CD / lint (push) Waiting to run
CI/CD / test-docker-build (push) Waiting to run
CI/CD / tests-web-sync (node20, pg12) (push) Waiting to run
CI/CD / tests-web-sync (node20, pg15) (push) Waiting to run
CI/CD / tests-web-async (node20, pg12, mode) (push) Waiting to run
CI/CD / tests-web-async (node20, pg15, mode) (push) Waiting to run
CI/CD / tests-web-async (node20, pg12, mode-azure) (push) Waiting to run
CI/CD / tests-web-async (node20, pg15, mode-azure) (push) Waiting to run
CI/CD / tests-worker (node20, pg12, mode) (push) Waiting to run
CI/CD / tests-worker (node20, pg15, mode) (push) Waiting to run
CI/CD / tests-worker (node20, pg12, mode-azure) (push) Waiting to run
CI/CD / tests-worker (node20, pg15, mode-azure) (push) Waiting to run
CI/CD / e2e-tests (push) Waiting to run
CI/CD / e2e-server-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
2024-12-02 16:29:40 +01:00
steffen911 965cf1445d chore: release v3.0.0-rc.2 2024-12-02 16:28:36 +01:00
Marc KlingenandGitHub 922bbb0ce1 docs: add list of dependents to readme (#4518)
* docs: add list of dependents to readme

* push
2024-12-02 15:05:56 +00:00
Max DeichmannandGitHub d821423c6a perf: improve dataset run item performance (#4514) 2024-12-02 15:57:20 +01:00
Steffen SchmitzandGitHub 84b0b7f680 chore: remove backend cloud version checks (#4512)
* chore: remove backend cloud version checks

* chore: lint

* chore: adjust clickhouse healthcheck
2024-12-02 14:41:18 +00:00
Steffen SchmitzandGitHub fba7f4f0c3 build: cancel concurrent PR jobs on new commit (#4517)
* build: cancel concurrent PR jobs on new commit

* chore: adjust pattern

* chore: adjust pattern
2024-12-02 14:31:58 +00:00
Steffen SchmitzandGitHub a9405a2e8b chore: ignore latency for prompts-ui.servertest.ts (#4513) 2024-12-02 14:20:05 +00:00
Marc KlingenandGitHub 2cb3529707 fix(ui): disallow creation of projects if project name already exists in the org (#4516) 2024-12-02 14:12:46 +00:00
Marc KlingenandGitHub 2f13204836 fix(ui): overflow of session view on mobile (#4515) 2024-12-02 14:12:20 +00:00
Steffen SchmitzandGitHub ae2a42bf80 chore: move batch export s3 env variable names into new naming pattern (#4511)
* chore: move batch export s3 env variable names into new naming pattern

* chore: add prefixes correctly

* chore: overwrite batch export feature in CI
2024-12-02 13:49:15 +00:00
Marc KlingenandGitHub 039dee045b feat(cloud): update entitlements to account for limits, add self-hosted pro plan (#3913)
* wip

* push

* add migration

* render correct usage event type in settings

* render number of seats in settings

* wip

* add entitlement limits

* add new actionbutton

* migrate annotation queue create

* add to table date range selectors

* add limit to dashboard date range

* move migration

* fix usage preview in billing settings

* get seats from subscription instead of next invoice

* remove old console.log

* ad pro plan self-hosted

* fix

* readme

* rename / fixes

* push

* only alert on over 90% usage

* push

* push

* push

* push

* add new self host entitlements for v3

* drop batch-export entitlement

* push

* move logic to clickhouse

* push

* fix padding

* fix clickhouse logic
2024-12-02 13:39:02 +00:00
Max DeichmannandGitHub 9831bb8e44 fix: increase attempts for evals (#4487)
feat
2024-12-02 13:37:00 +00:00
Steffen SchmitzandGitHub c6e3a12b08 chore: apply v3 defaults to environment variables (#4498) 2024-12-02 13:50:44 +01:00
Marc KlingenandGitHub 9b01b562a8 fix(ui): account for prereleases in update check (#4503) 2024-11-29 18:34:12 +00:00
Hassieb Pakzad e5a0cec7d6 chore: add sha256 hash to media url span 2024-11-29 18:33:26 +01:00
Steffen SchmitzandGitHub 9375250c5a chore: fix typo in ingestion queue concurrency and adjust default (#4495) 2024-11-29 10:30:11 +00:00
Hassieb PakzadandGitHub 82e5f1a932 feat(playground+evals): add gemini and vertex support (#4485) 2024-11-29 10:07:33 +01:00
Max DeichmannandGitHub 64a62c9f6d perf: improve datasets view (#4486) 2024-11-28 21:21:05 +01:00
44d856747a feat(auth): add GitHub Enterprise Authentication Provider (#4463)
feat: Add GitHub Enterprise Authentication Provider

Co-authored-by: Marc Klingen <git@marcklingen.com>
2024-11-28 01:43:43 +00:00
Marc Klingen e17bd364c9 chore: fix ci on fork prs 2024-11-28 02:30:08 +01:00
Marc KlingenandGitHub f38c756244 fix: do not authenticate with Docker Hub for pull request CI (#4477) 2024-11-28 02:20:29 +01:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>Marc Klingen
6c62f4b31a chore(deps): bump posthog-node from 3.6.3 to 4.3.1 (#4450)
* chore(deps): bump posthog-node from 3.6.3 to 4.3.1

Bumps [posthog-node](https://github.com/PostHog/posthog-js-lite/tree/HEAD/posthog-node) from 3.6.3 to 4.3.1.
- [Release notes](https://github.com/PostHog/posthog-js-lite/releases)
- [Changelog](https://github.com/PostHog/posthog-js-lite/blob/main/posthog-node/CHANGELOG.md)
- [Commits](https://github.com/PostHog/posthog-js-lite/commits/posthog-node-v4.3.1/posthog-node)

---
updated-dependencies:
- dependency-name: posthog-node
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>

* fix flush

* shutdown instead of shutdownasync

---------

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Marc Klingen <git@marcklingen.com>
2024-11-28 00:07:51 +00:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
fb686d45ee chore(deps): bump stripe from 16.8.0 to 17.4.0 (#4377)
Bumps [stripe](https://github.com/stripe/stripe-node) from 16.8.0 to 17.4.0.
- [Release notes](https://github.com/stripe/stripe-node/releases)
- [Changelog](https://github.com/stripe/stripe-node/blob/master/CHANGELOG.md)
- [Commits](https://github.com/stripe/stripe-node/compare/v16.8.0...v17.4.0)

---
updated-dependencies:
- dependency-name: stripe
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-11-27 23:47:54 +00:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
1901c8cb39 chore(deps): bump lucide-react from 0.447.0 to 0.462.0 (#4475)
Bumps [lucide-react](https://github.com/lucide-icons/lucide/tree/HEAD/packages/lucide-react) from 0.447.0 to 0.462.0.
- [Release notes](https://github.com/lucide-icons/lucide/releases)
- [Commits](https://github.com/lucide-icons/lucide/commits/0.462.0/packages/lucide-react)

---
updated-dependencies:
- dependency-name: lucide-react
  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-11-27 23:47:05 +00:00
Marc Klingen 66d925ee1f chore: do not run codeql on merge queue 2024-11-28 01:05:07 +01:00
Marc Klingen 1b06385111 chore: run ci on merge groups 2024-11-28 00:23:08 +01:00
Hassieb PakzadandGitHub 6e290b9ad4 fix(media): increase retries (#4474) 2024-11-27 22:57:53 +00:00
Max DeichmannandGitHub 8b8afff705 fix: fix generations search in clickhouse (#4472) 2024-11-27 23:20:21 +01:00
Max DeichmannandGitHub 89fcf27f84 chore: add logs for media API (#4471) 2024-11-27 22:46:44 +01:00
Max DeichmannandGitHub 6717b16679 perf: add improved default performance to traces table (#4469)
* feat

* feat
2024-11-27 21:07:29 +00:00
Cole WinstanleyandGitHub 98e29cb512 fix(auth): add nonce check for Cognito NextAuth provider (#4401) 2024-11-27 17:59:31 +01:00
Marc KlingenandGitHub c7b7a784d7 feat(ui): add more explicit confirmation prompt before deleting a dataset (#4424) 2024-11-27 16:51:44 +00:00
Marc KlingenandGitHub e778d04523 perf: single clickhouse query for cloud usage job across all orgs/projects (#4453) 2024-11-27 17:39:31 +01:00
Hassieb PakzadandGitHub 2a2f50e2d9 feat(media): broaden content type support (#4442)
* feat(media): broaden content type support

* fix file extensions

* add media attachment view in pretty mode
2024-11-27 17:19:54 +01:00
Max DeichmannandGitHub 9d9820dacc chore: remove log line (#4466) 2024-11-27 15:54:39 +01:00
Max DeichmannandGitHub 6e4065eff3 perf: extract traces table metrics from clickhouse traces.all query (#4460) 2024-11-27 14:24:15 +00:00
Marc KlingenandGitHub 993e2a8323 fix(ui): trace timeline peek drawer should not be full-width (#4462) 2024-11-27 14:04:45 +00:00
Steffen SchmitzandGitHub f0c117b35b chore: expand error logging for queue worker errors (#4457) 2024-11-27 12:29:59 +00:00
Max DeichmannandGitHub b7d353a821 perf: increase performance for traces default table (#4459) 2024-11-27 11:36:14 +00:00
Hassieb PakzadandGitHub 1bdc6a4068 fix(media): increase isolationLevel on concurrent upserts (#4458) 2024-11-27 12:23:08 +01:00
Marc KlingenandGitHub 851de49789 perf: prevent duplicated parallel executions of cloud metering job (#4455) 2024-11-27 01:27:07 +00:00
Max DeichmannandGitHub 202a9b715d perf: improve clickhouse traces table perfromance (#4451) 2024-11-26 23:25:45 +00:00
Marc KlingenandGitHub 5fae362b29 refactor(cloud): move usage metering job to clickhouse (#4448) 2024-11-26 21:27:04 +00:00
Max DeichmannandGitHub 38e5c3fac7 Max/lfe 2988 adjust generations table default again (#4446) 2024-11-26 21:01:10 +00:00
Marc KlingenandGitHub 3d6d94fb16 chore(cloud): improve logs of usage metering job (#4447)
chore: improve logs of usage metering job
2024-11-26 20:27:14 +00:00
Marc KlingenandGitHub cb7230ac41 fix: override score name when config is set (#4445) 2024-11-26 20:08:36 +00:00
Steffen SchmitzandGitHub 44ba6eaddf chore: restrict trace.name length to 1000 characters (#4441) 2024-11-26 19:47:54 +01:00
Max DeichmannandGitHub e566dfa5a9 feat: improve generations table default query time (#4440) 2024-11-26 18:12:13 +00:00
Steffen SchmitzandGitHub f9c3f60d49 chore: make trace deletion asynchronous (#4398) 2024-11-26 16:26:31 +00:00
Steffen SchmitzandGitHub aeb56ecde7 chore: explicitly cast names for traces table (#4433) 2024-11-26 17:15:32 +01:00
Marc KlingenandGitHub 9ac84fbd61 fix: do not log stack trace in trpc checkupdate when no internet access (#4436) 2024-11-26 16:33:33 +01:00
Hassieb PakzadandGitHub fe9af426dc fix(playground): unique prompt names in save to prompt (#4437) 2024-11-26 16:22:44 +01:00
Marc KlingenandGitHub 583e90321e fix(auth): improve name and password verification (#4434)
* fix(auth): improve name and password verification

* password complexity
* no url in name

* e2e test
2024-11-26 13:28:43 +00:00
Hassieb Pakzad e52530351a chore: update v3preview docker compose to main tagged images 2024-11-26 13:49:28 +01:00
Hassieb Pakzad 8a52c0b888 chore: update v3preview docker compose to 3 tagged images 2024-11-26 13:32:16 +01:00
Hassieb Pakzad d02c9b4902 chore: release v3.0.0-rc.1
CI/CD / lint (push) Waiting to run
CI/CD / test-docker-build (push) Waiting to run
CI/CD / tests-web-sync (node20, pg12) (push) Waiting to run
CI/CD / tests-web-sync (node20, pg15) (push) Waiting to run
CI/CD / tests-web-async (node20, pg12, mode) (push) Waiting to run
CI/CD / tests-web-async (node20, pg15, mode) (push) Waiting to run
CI/CD / tests-web-async (node20, pg12, mode-azure) (push) Waiting to run
CI/CD / tests-web-async (node20, pg15, mode-azure) (push) Waiting to run
CI/CD / tests-worker (node20, pg12, mode) (push) Waiting to run
CI/CD / tests-worker (node20, pg15, mode) (push) Waiting to run
CI/CD / tests-worker (node20, pg12, mode-azure) (push) Waiting to run
CI/CD / tests-worker (node20, pg15, mode-azure) (push) Waiting to run
CI/CD / e2e-tests (push) Waiting to run
CI/CD / e2e-server-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
2024-11-26 12:20:20 +01:00
Hassieb Pakzad 014c3d9dad chore: do not tag v3 tags as latest image 2024-11-26 12:17:51 +01:00
Max DeichmannandGitHub 1d3c74435b fix: change the default of the migration script (#4419) 2024-11-26 09:48:37 +00:00
482 changed files with 22681 additions and 21595 deletions
+10 -18
View File
@@ -11,7 +11,7 @@ CLICKHOUSE_MIGRATION_URL="clickhouse://localhost:9000"
CLICKHOUSE_URL="http://localhost:8123"
CLICKHOUSE_USER="clickhouse"
CLICKHOUSE_PASSWORD="clickhouse"
CLICKHOUSE_MIGRATION_CLUSTER_DISABLED="true"
CLICKHOUSE_CLUSTER_ENABLED="false"
# Next Auth
# You can generate a new secret on the command line with:
@@ -35,17 +35,18 @@ EMAIL_FROM_ADDRESS="" # Defines the email address to use as the from address.
SMTP_CONNECTION_URL="" # Defines the connection url for smtp server.
# DON'T PANIC: The Azurite Secrets are well-known and meant to be hard-coded
# S3 storage
S3_ENDPOINT=http://localhost:10000/devstoreaccount1
S3_ACCESS_KEY_ID=devstoreaccount1
S3_SECRET_ACCESS_KEY=Eby8vdM02xNOcqFlqUwJPLlmEtlCDXJ1OUzFT50uSRZ6IFsuFq2UVErCz4I6tq/K1SZFPTOtr/KBHBeksoGMGw==
S3_BUCKET_NAME=langfuse
S3_REGION=auto
# S3 Batch Exports
LANGFUSE_S3_BATCH_EXPORT_ENABLED=true
LANGFUSE_S3_BATCH_EXPORT_BUCKET=langfuse
LANGFUSE_S3_BATCH_EXPORT_ACCESS_KEY_ID=devstoreaccount1
LANGFUSE_S3_BATCH_EXPORT_SECRET_ACCESS_KEY=Eby8vdM02xNOcqFlqUwJPLlmEtlCDXJ1OUzFT50uSRZ6IFsuFq2UVErCz4I6tq/K1SZFPTOtr/KBHBeksoGMGw==
LANGFUSE_S3_BATCH_EXPORT_REGION=auto
LANGFUSE_S3_BATCH_EXPORT_ENDPOINT=http://localhost:10000/devstoreaccount1
## Necessary for minio compatibility
S3_FORCE_PATH_STYLE=true
LANGFUSE_S3_BATCH_EXPORT_FORCE_PATH_STYLE=true
LANGFUSE_S3_BATCH_EXPORT_PREFIX=exports/
# S3 Media Upload LOCAL
LANGFUSE_S3_MEDIA_UPLOAD_ENABLED=true
LANGFUSE_S3_MEDIA_UPLOAD_BUCKET=langfuse
LANGFUSE_S3_MEDIA_UPLOAD_ACCESS_KEY_ID=devstoreaccount1
LANGFUSE_S3_MEDIA_UPLOAD_SECRET_ACCESS_KEY=Eby8vdM02xNOcqFlqUwJPLlmEtlCDXJ1OUzFT50uSRZ6IFsuFq2UVErCz4I6tq/K1SZFPTOtr/KBHBeksoGMGw==
@@ -57,7 +58,6 @@ LANGFUSE_S3_MEDIA_UPLOAD_PREFIX=media/
# S3 Event Bucket Upload
## Set to true to test uploading all events to S3
LANGFUSE_S3_EVENT_UPLOAD_ENABLED=true
LANGFUSE_S3_EVENT_UPLOAD_BUCKET=langfuse
LANGFUSE_S3_EVENT_UPLOAD_ACCESS_KEY_ID=devstoreaccount1
LANGFUSE_S3_EVENT_UPLOAD_SECRET_ACCESS_KEY=Eby8vdM02xNOcqFlqUwJPLlmEtlCDXJ1OUzFT50uSRZ6IFsuFq2UVErCz4I6tq/K1SZFPTOtr/KBHBeksoGMGw==
@@ -82,11 +82,3 @@ ENCRYPTION_KEY=0000000000000000000000000000000000000000000000000000000000000000
# speeds up local development by not executing init scripts on server startup
NEXT_PUBLIC_LANGFUSE_RUN_NEXT_INIT="false"
LANGFUSE_READ_FROM_POSTGRES_ONLY=false
LANGFUSE_RETURN_FROM_CLICKHOUSE=true
LANGFUSE_READ_DASHBOARDS_FROM_CLICKHOUSE=true
LANGFUSE_READ_FROM_CLICKHOUSE_ONLY=true
LANGFUSE_ASYNC_INGESTION_PROCESSING="true"
LANGFUSE_ASYNC_CLICKHOUSE_INGESTION_PROCESSING="true"
+10 -18
View File
@@ -11,7 +11,7 @@ CLICKHOUSE_MIGRATION_URL="clickhouse://localhost:9000"
CLICKHOUSE_URL="http://localhost:8123"
CLICKHOUSE_USER="clickhouse"
CLICKHOUSE_PASSWORD="clickhouse"
CLICKHOUSE_CLUSTER_DISABLED="true"
CLICKHOUSE_CLUSTER_ENABLED="false"
# Next Auth
# You can generate a new secret on the command line with:
@@ -34,17 +34,18 @@ SALT="salt"
EMAIL_FROM_ADDRESS="" # Defines the email address to use as the from address.
SMTP_CONNECTION_URL="" # Defines the connection url for smtp server.
# S3 storage
S3_ENDPOINT=http://localhost:9090
S3_ACCESS_KEY_ID=minio
S3_SECRET_ACCESS_KEY=miniosecret
S3_BUCKET_NAME=langfuse
S3_REGION=us-east-1
# S3 Batch Exports
LANGFUSE_S3_BATCH_EXPORT_ENABLED=true
LANGFUSE_S3_BATCH_EXPORT_BUCKET=langfuse
LANGFUSE_S3_BATCH_EXPORT_ACCESS_KEY_ID=minio
LANGFUSE_S3_BATCH_EXPORT_SECRET_ACCESS_KEY=miniosecret
LANGFUSE_S3_BATCH_EXPORT_REGION=us-east-1
LANGFUSE_S3_BATCH_EXPORT_ENDPOINT=http://localhost:9090
## Necessary for minio compatibility
S3_FORCE_PATH_STYLE=true
LANGFUSE_S3_BATCH_EXPORT_FORCE_PATH_STYLE=true
LANGFUSE_S3_BATCH_EXPORT_PREFIX=exports/
# S3 Media Upload LOCAL
LANGFUSE_S3_MEDIA_UPLOAD_ENABLED=true
LANGFUSE_S3_MEDIA_UPLOAD_BUCKET=langfuse
LANGFUSE_S3_MEDIA_UPLOAD_ACCESS_KEY_ID=minio
LANGFUSE_S3_MEDIA_UPLOAD_SECRET_ACCESS_KEY=miniosecret
@@ -56,7 +57,6 @@ LANGFUSE_S3_MEDIA_UPLOAD_PREFIX=media/
# S3 Event Bucket Upload
## Set to true to test uploading all events to S3
LANGFUSE_S3_EVENT_UPLOAD_ENABLED=true
LANGFUSE_S3_EVENT_UPLOAD_BUCKET=langfuse
LANGFUSE_S3_EVENT_UPLOAD_ACCESS_KEY_ID=minio
LANGFUSE_S3_EVENT_UPLOAD_SECRET_ACCESS_KEY=miniosecret
@@ -79,11 +79,3 @@ ENCRYPTION_KEY=0000000000000000000000000000000000000000000000000000000000000000
# speeds up local development by not executing init scripts on server startup
NEXT_PUBLIC_LANGFUSE_RUN_NEXT_INIT="false"
LANGFUSE_READ_FROM_POSTGRES_ONLY=false
LANGFUSE_RETURN_FROM_CLICKHOUSE=true
LANGFUSE_READ_DASHBOARDS_FROM_CLICKHOUSE=true
LANGFUSE_READ_FROM_CLICKHOUSE_ONLY=true
LANGFUSE_ASYNC_INGESTION_PROCESSING="true"
LANGFUSE_ASYNC_CLICKHOUSE_INGESTION_PROCESSING="true"
-81
View File
@@ -1,81 +0,0 @@
# When adding additional environment variables, the schema in "/src/env.mjs"
# should be updated accordingly.
# Prisma
# https://www.prisma.io/docs/reference/database-reference/connection-urls#env
DIRECT_URL="postgresql://postgres:postgres@localhost:5432/postgres"
DATABASE_URL="postgresql://postgres:postgres@localhost:5432/postgres"
# Clickhouse
CLICKHOUSE_MIGRATION_URL="clickhouse://localhost:9000"
CLICKHOUSE_URL="http://localhost:8123"
CLICKHOUSE_USER="clickhouse"
CLICKHOUSE_PASSWORD="clickhouse"
CLICKHOUSE_CLUSTER_ENABLED="false"
# Next Auth
# You can generate a new secret on the command line with:
# openssl rand -base64 32
# https://next-auth.js.org/configuration/options#secret
# NEXTAUTH_SECRET=""
NEXTAUTH_URL="http://localhost:3000"
NEXTAUTH_SECRET="secret"
# Langfuse Cloud Environment
NEXT_PUBLIC_LANGFUSE_CLOUD_REGION="DEV"
# Langfuse experimental features
LANGFUSE_ENABLE_EXPERIMENTAL_FEATURES="true"
# Salt for API key hashing
SALT="salt"
# Email
EMAIL_FROM_ADDRESS="" # Defines the email address to use as the from address.
SMTP_CONNECTION_URL="" # Defines the connection url for smtp server.
# S3 storage
S3_ENDPOINT=http://localhost:9090
S3_ACCESS_KEY_ID=minio
S3_SECRET_ACCESS_KEY=miniosecret
S3_BUCKET_NAME=langfuse
S3_REGION=us-east-1
## Necessary for minio compatibility
S3_FORCE_PATH_STYLE=true
# # S3 Media Upload LOCAL
LANGFUSE_S3_MEDIA_UPLOAD_ENABLED=true
LANGFUSE_S3_MEDIA_UPLOAD_BUCKET=langfuse
LANGFUSE_S3_MEDIA_UPLOAD_ACCESS_KEY_ID=minio
LANGFUSE_S3_MEDIA_UPLOAD_SECRET_ACCESS_KEY=miniosecret
LANGFUSE_S3_MEDIA_UPLOAD_REGION=us-east-1
LANGFUSE_S3_MEDIA_UPLOAD_ENDPOINT=http://localhost:9090
## Necessary for minio compatibility
LANGFUSE_S3_MEDIA_UPLOAD_FORCE_PATH_STYLE=true
LANGFUSE_S3_MEDIA_UPLOAD_PREFIX=media/
# S3 Event Bucket Upload
## Set to true to test uploading all events to S3
LANGFUSE_S3_EVENT_UPLOAD_ENABLED=true
LANGFUSE_S3_EVENT_UPLOAD_BUCKET=langfuse
LANGFUSE_S3_EVENT_UPLOAD_ACCESS_KEY_ID=minio
LANGFUSE_S3_EVENT_UPLOAD_SECRET_ACCESS_KEY=miniosecret
LANGFUSE_S3_EVENT_UPLOAD_REGION=us-east-1
LANGFUSE_S3_EVENT_UPLOAD_ENDPOINT=http://localhost:9090
## Necessary for minio compatibility
LANGFUSE_S3_EVENT_UPLOAD_FORCE_PATH_STYLE=true
LANGFUSE_S3_EVENT_UPLOAD_PREFIX=events/
# Set during docker build of application
# Used to disable environment verification at build time
# DOCKER_BUILD=1
REDIS_HOST="127.0.0.1"
REDIS_PORT=6379
REDIS_AUTH="myredissecret"
# openssl rand -hex 32 used only here
ENCRYPTION_KEY=0000000000000000000000000000000000000000000000000000000000000000
# speeds up local development by not executing init scripts on server startup
NEXT_PUBLIC_LANGFUSE_RUN_NEXT_INIT="false"
-22
View File
@@ -1,22 +0,0 @@
# When adding additional environment variables, the schema in "/src/env.mjs"
# should be updated accordingly.
# Prisma
# https://www.prisma.io/docs/reference/database-reference/connection-urls#env
DIRECT_URL="postgresql://postgres:postgres@db:5432/postgres"
DATABASE_URL="postgresql://postgres:postgres@db:5432/postgres"
# Next Auth
NEXTAUTH_SECRET="secret"
NEXTAUTH_URL="http://localhost:3000"
# feature flag to enable experimental features locally
LANGFUSE_ENABLE_EXPERIMENTAL_FEATURES="false"
SALT="salt"
# Redis
REDIS_HOST="127.0.0.1"
REDIS_PORT=6379
REDIS_AUTH="myredissecret"
# openssl rand -hex 32 used only here
ENCRYPTION_KEY=0000000000000000000000000000000000000000000000000000000000000000
+19 -11
View File
@@ -55,6 +55,7 @@ OTEL_SERVICE_NAME="langfuse"
# Auth, optional configuration
# AUTH_DOMAINS_WITH_SSO_ENFORCEMENT=domain1.com,domain2.com
# AUTH_IGNORE_ACCOUNT_FIELDS=foo,bar
# AUTH_DISABLE_USERNAME_PASSWORD=true
# AUTH_DISABLE_SIGNUP=true
# AUTH_SESSION_MAX_AGE=43200 # 30 days in minutes (default)
@@ -67,6 +68,10 @@ OTEL_SERVICE_NAME="langfuse"
# AUTH_GITHUB_CLIENT_ID=
# AUTH_GITHUB_CLIENT_SECRET=
# AUTH_GITHUB_ALLOW_ACCOUNT_LINKING=false
# AUTH_GITHUB_ENTERPRISE_CLIENT_ID=
# AUTH_GITHUB_ENTERPRISE_CLIENT_SECRET=
# AUTH_GITHUB_ENTERPRISE_BASE_URL=
# AUTH_GITHUB_ENTERPRISE_ALLOW_ACCOUNT_LINKING=false
# AUTH_GITLAB_CLIENT_ID=
# AUTH_GITLAB_CLIENT_SECRET=
# AUTH_GITLAB_ALLOW_ACCOUNT_LINKING=false
@@ -87,11 +92,16 @@ OTEL_SERVICE_NAME="langfuse"
# AUTH_COGNITO_CLIENT_SECRET=
# AUTH_COGNITO_ISSUER=
# AUTH_COGNITO_ALLOW_ACCOUNT_LINKING=false
# AUTH_KEYCLOAK_CLIENT_ID=
# AUTH_KEYCLOAK_CLIENT_SECRET=
# AUTH_KEYCLOAK_ISSUER=
# AUTH_KEYCLOAK_ALLOW_ACCOUNT_LINKING=false
# AUTH_CUSTOM_CLIENT_ID=
# AUTH_CUSTOM_CLIENT_SECRET=
# AUTH_CUSTOM_ISSUER=
# AUTH_CUSTOM_NAME=
# AUTH_CUSTOM_SCOPE="openid email profile" # optional
# AUTH_CUSTOM_CLIENT_AUTH_METHOD="client_secret_basic" # optional
# AUTH_CUSTOM_ALLOW_ACCOUNT_LINKING=false
# Transactional email, optional
@@ -100,16 +110,16 @@ OTEL_SERVICE_NAME="langfuse"
# Defines the connection url for smtp server.
# SMTP_CONNECTION_URL=
# S3 storage, optional, used for exports from the UI
# S3_ENDPOINT=
# S3_ACCESS_KEY_ID=
# S3_SECRET_ACCESS_KEY=
# S3_BUCKET_NAME=
# S3_REGION=
# BATCH_EXPORT_DOWNLOAD_LINK_EXPIRATION_HOURS=
# S3 Batch Exports
# LANGFUSE_S3_BATCH_EXPORT_ENABLED=
# LANGFUSE_S3_BATCH_EXPORT_BUCKET=
# LANGFUSE_S3_BATCH_EXPORT_ACCESS_KEY_ID=
# LANGFUSE_S3_BATCH_EXPORT_SECRET_ACCESS_KEY=
# LANGFUSE_S3_BATCH_EXPORT_REGION=
# LANGFUSE_S3_BATCH_EXPORT_ENDPOINT=
# LANGFUSE_S3_BATCH_EXPORT_PREFIX=
# S3 storage for events, optional, used to persist all incoming events
# LANGFUSE_S3_EVENT_UPLOAD_ENABLED="true"
# LANGFUSE_S3_EVENT_UPLOAD_BUCKET=
# Optional prefix to be used within the bucket. Must end with `/` if set
# LANGFUSE_S3_EVENT_UPLOAD_PREFIX=events/
@@ -235,14 +245,12 @@ OTEL_SERVICE_NAME="langfuse"
# CLICKHOUSE_URL=
# CLICKHOUSE_USER=
# CLICKHOUSE_PASSWORD=
# CLICKHOUSE_DB=
# Ingestion
# LANGFUSE_INGESTION_QUEUE_DELAY_MS=
# LANGFUSE_INGESTION_CLICKHOUSE_WRITE_BATCH_SIZE=
# LANGFUSE_INGESTION_CLICKHOUSE_WRITE_INTERVAL_MS=
# LANGFUSE_INGESTION_CLICKHOUSE_MAX_ATTEMPTS=
# LANGFUSE_LEGACY_INGESTION_WORKER_CONCURRENCY=
# LANGFUSE_ASYNC_INGESTION_PROCESSING="true"
# QUEUE_CONSUMER_LEGACY_INGESTION_QUEUE_IS_ENABLED="true"
## END Langfuse V3 Ingestion
@@ -58,7 +58,6 @@ jobs:
--build-arg SENTRY_PROJECT=${{ vars.SENTRY_PROJECT }} \
.
docker push $REGISTRY/$REPOSITORY:$IMAGE_TAG
- name: Render AWS ECS Task Definition
id: render-task-definition
uses: aws-actions/amazon-ecs-render-task-definition@v1
+1
View File
@@ -6,6 +6,7 @@ on:
branches: [main]
pull_request:
branches: [main]
merge_group:
permissions:
contents: read
+59 -10
View File
@@ -10,11 +10,29 @@ on:
merge_group:
pull_request:
branches:
- "*"
- "**"
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: ${{ github.event_name == 'pull_request' }}
jobs:
pre-job:
runs-on: ubuntu-latest
outputs:
should_skip: ${{ steps.skip_check.outputs.should_skip }}
timeout-minutes: 15
steps:
- id: skip_check
uses: fkirc/skip-duplicate-actions@v5
with:
do_not_skip: '["workflow_dispatch"]'
lint:
runs-on: ubuntu-latest
needs:
- pre-job
if: needs.pre-job.outputs.should_skip != 'true'
steps:
- uses: actions/checkout@v4
- uses: pnpm/action-setup@v3
@@ -37,10 +55,14 @@ jobs:
test-docker-build:
timeout-minutes: 20
runs-on: ubuntu-latest
needs:
- pre-job
if: needs.pre-job.outputs.should_skip != 'true'
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Login to Docker Hub
if: github.repository == 'langfuse/langfuse' && (github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository) && (github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository)
uses: docker/login-action@v3
with:
username: ${{ secrets.DOCKERHUB_USERNAME_READ }}
@@ -69,6 +91,9 @@ jobs:
tests-web-sync:
timeout-minutes: 20
runs-on: ubuntu-latest
needs:
- pre-job
if: needs.pre-job.outputs.should_skip != 'true'
name: tests-web-sync (node${{ matrix.node-version }}, pg${{ matrix.postgres-version }})
strategy:
matrix:
@@ -89,6 +114,7 @@ jobs:
with:
version: 9.5.0
- name: Login to Docker Hub
if: github.repository == 'langfuse/langfuse' && (github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository)
uses: docker/login-action@v3
with:
username: ${{ secrets.DOCKERHUB_USERNAME_READ }}
@@ -105,7 +131,9 @@ jobs:
- name: Load default env
run: |
cp .env.dev.example .env
grep -v -e '^S3_BUCKET_NAME=' -e '^REDIS_HOST=' -e '^NEXT_PUBLIC_LANGFUSE_RUN_NEXT_INIT=' .env.dev.legacy.example > .env
grep -v -e '^LANGFUSE_S3_BATCH_EXPORT_ENABLED=' -e '^NEXT_PUBLIC_LANGFUSE_RUN_NEXT_INIT=' .env.dev.example > .env
echo "LANGFUSE_INGESTION_QUEUE_DELAY_MS=1" >> .env
echo "LANGFUSE_INGESTION_CLICKHOUSE_WRITE_INTERVAL_MS=1" >> .env
- name: Run + migrate
run: |
docker compose -f docker-compose.dev.yml up -d
@@ -133,10 +161,15 @@ jobs:
LANGFUSE_INIT_USER_PASSWORD: "password"
- name: run test-sync
run: pnpm --filter=web run test-sync
- name: run test-client
run: pnpm --filter=web run test-client
tests-web-async:
timeout-minutes: 20
runs-on: ubuntu-latest
needs:
- pre-job
if: needs.pre-job.outputs.should_skip != 'true'
name: tests-web-async (node${{ matrix.node-version }}, pg${{ matrix.postgres-version }}, mode${{ matrix.blob-provider }})
strategy:
matrix:
@@ -158,6 +191,7 @@ jobs:
with:
version: 9.5.0
- name: Login to Docker Hub
if: github.repository == 'langfuse/langfuse' && (github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository)
uses: docker/login-action@v3
with:
username: ${{ secrets.DOCKERHUB_USERNAME_READ }}
@@ -174,7 +208,9 @@ jobs:
- name: Load default env
run: |
cp .env.dev${{ matrix.blob-provider }}.example .env
grep -v -e '^S3_BUCKET_NAME=' -e '^REDIS_HOST=' -e '^NEXT_PUBLIC_LANGFUSE_RUN_NEXT_INIT=' .env.dev${{ matrix.blob-provider }}.example > .env
grep -v -e '^LANGFUSE_S3_BATCH_EXPORT_ENABLED=' -e '^NEXT_PUBLIC_LANGFUSE_RUN_NEXT_INIT=' .env.dev${{ matrix.blob-provider }}.example > .env
echo "LANGFUSE_INGESTION_QUEUE_DELAY_MS=1" >> .env
echo "LANGFUSE_INGESTION_CLICKHOUSE_WRITE_INTERVAL_MS=1" >> .env
- name: Run + migrate
run: |
docker compose -f docker-compose.dev${{ matrix.blob-provider }}.yml up -d
@@ -206,6 +242,9 @@ jobs:
tests-worker:
timeout-minutes: 20
runs-on: ubuntu-latest
needs:
- pre-job
if: needs.pre-job.outputs.should_skip != 'true'
name: tests-worker (node${{ matrix.node-version }}, pg${{ matrix.postgres-version }}, mode${{ matrix.blob-provider }})
strategy:
matrix:
@@ -222,6 +261,7 @@ jobs:
with:
version: 9.5.0
- name: Login to Docker Hub
if: github.repository == 'langfuse/langfuse' && (github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository)
uses: docker/login-action@v3
with:
username: ${{ secrets.DOCKERHUB_USERNAME_READ }}
@@ -270,6 +310,9 @@ jobs:
e2e-tests:
runs-on: ubuntu-latest
needs:
- pre-job
if: needs.pre-job.outputs.should_skip != 'true'
steps:
- uses: actions/checkout@v4
- uses: pnpm/action-setup@v3
@@ -281,6 +324,7 @@ jobs:
cache: "pnpm"
cache-dependency-path: "pnpm-lock.yaml"
- name: Login to Docker Hub
if: github.repository == 'langfuse/langfuse' && (github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository)
uses: docker/login-action@v3
with:
username: ${{ secrets.DOCKERHUB_USERNAME_READ }}
@@ -310,9 +354,13 @@ jobs:
e2e-server-tests:
runs-on: ubuntu-latest
needs:
- pre-job
if: needs.pre-job.outputs.should_skip != 'true'
steps:
- uses: actions/checkout@v4
- name: Login to Docker Hub
if: github.repository == 'langfuse/langfuse' && (github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository)
uses: docker/login-action@v3
with:
username: ${{ secrets.DOCKERHUB_USERNAME_READ }}
@@ -336,8 +384,6 @@ jobs:
- name: Load default env
run: |
cp .env.dev.example .env
echo "LANGFUSE_ASYNC_CLICKHOUSE_INGESTION_PROCESSING=true" >> .env
echo "LANGFUSE_ASYNC_INGESTION_PROCESSING=true" >> .env
echo "LANGFUSE_CACHE_API_KEY_ENABLED=true" >> .env
echo "LANGFUSE_CACHE_PROMPT_ENABLED=true" >> .env
- name: Run + migrate
@@ -448,9 +494,9 @@ jobs:
type=ref,event=pr
type=sha
type=semver,pattern={{version}}
type=semver,pattern={{major}}.{{minor}}
type=semver,pattern={{major}}
type=raw,value=latest,enable=${{ startsWith(github.ref, 'refs/tags/v3') }}
type=semver,pattern={{major}}.{{minor}},enable=${{ !contains(github.ref, '-rc') }}
type=semver,pattern={{major}},enable=${{ !contains(github.ref, '-rc') }}
type=raw,value=latest,enable=${{ startsWith(github.ref, 'refs/tags/v3') && !contains(github.ref, '-rc') }}
- name: Build and push Docker image (web)
uses: docker/build-push-action@v4
with:
@@ -469,13 +515,16 @@ jobs:
images: |
ghcr.io/langfuse/langfuse-worker # GitHub
langfuse/langfuse-worker # Docker Hub
flavor: |
latest=false
tags: |
type=ref,event=branch
type=ref,event=pr
type=sha
type=semver,pattern={{version}}
type=semver,pattern={{major}}.{{minor}}
type=semver,pattern={{major}}
type=semver,pattern={{major}}.{{minor}},enable=${{ !contains(github.ref, '-rc') }}
type=semver,pattern={{major}},enable=${{ !contains(github.ref, '-rc') }}
type=raw,value=latest,enable=${{ startsWith(github.ref, 'refs/tags/v3') && !contains(github.ref, '-rc') }}
- name: Build and push Docker image (worker)
uses: docker/build-push-action@v4
with:
+8 -3
View File
@@ -6,6 +6,11 @@ on:
push:
branches:
- main
merge_group:
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: ${{ github.event_name == 'pull_request' }}
jobs:
snyk:
@@ -15,13 +20,13 @@ jobs:
- name: Build a Docker image
run: docker compose -f docker-compose.build.yml up -d
- name: Run Snyk to check Docker image for vulnerabilities (langfuse-server)
- name: Run Snyk to check Docker image for vulnerabilities (langfuse-web)
continue-on-error: true
uses: snyk/actions/docker@master
env:
SNYK_TOKEN: ${{ secrets.SNYK_TOKEN }}
with:
image: langfuse-server
image: langfuse-langfuse-web
args: --file=web/Dockerfile
- name: Upload result to GitHub Code Scanning
@@ -36,7 +41,7 @@ jobs:
env:
SNYK_TOKEN: ${{ secrets.SNYK_TOKEN }}
with:
image: langfuse-worker
image: langfuse-langfuse-worker
args: --file=worker/Dockerfile
- name: Upload result to GitHub Code Scanning
-2
View File
@@ -38,9 +38,7 @@ yarn-error.log*
.env*
!.env.dev.example
!.env.dev-azure.example
!.env.local.example
!.env.prod.example
!.env.dev.legacy.example
# vercel
.vercel
+33 -29
View File
@@ -121,36 +121,40 @@ flowchart TB
end
end
DB[Postgres Database]
Redis[Redis Cache/Queue]
Clickhouse[Clickhouse Database]
subgraph s9 ["VPC (US and EU separated)"]
DB[Postgres Database]
Redis[Redis Cache/Queue]
Clickhouse[Clickhouse Database]
subgraph s1["Application (langfuse/langfuse/web)"]
API[Public HTTP API]
G[TRPC API]
I[NextAuth]
H[React Frontend]
ORM
H --> G
H --> I
G --> I
G --- ORM
API --- ORM
I --- ORM
end
subgraph s5["Application (langfuse/langfuse/worker)"]
Worker
end
Worker --- DB
Worker --- Redis
Worker --- Clickhouse
ORM --- DB
ORM --- Redis
ORM --- Clickhouse
subgraph s1["Application (langfuse/langfuse/web)"]
API[Public HTTP API]
G[TRPC API]
I[NextAuth]
H[React Frontend]
ORM
H --> G
H --> I
G --> I
G --- ORM
API --- ORM
I --- ORM
end
subgraph s5["Application (langfuse/langfuse/worker)"]
Worker
end
Worker --- DB
Worker --- Redis
Worker --- Clickhouse
ORM --- DB
ORM --- Redis
ORM --- Clickhouse
JS --- API
Python --- API
```
@@ -397,7 +401,7 @@ The background color of the following component will be `hsl(var(--primary))` an
| --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-magenta | Muted magenta 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 |
@@ -439,7 +443,7 @@ You can update the default AI models and prices by adding or updating an entry i
Please note that
- prices are in USD
- the list is ordered by ID, so make sure to keep this order
- the list is ordered by ID, so make sure to keep this order and insert new models at the end of the list
- the `updated_at` field must be updated with the current date in ISO 8601 format. Otherwise, the change will be ignored.
### Transition period until V3 release
+2 -2
View File
@@ -2,8 +2,8 @@ Copyright (c) 2023--2024 Langfuse GmbH
Portions of this software are licensed as follows:
- All content that resides under the "ee/" and/or "web/src/ee" directories of this repository, if these directories exist, is licensed under the license defined in "ee/LICENSE".
- All third party components incorporated into the Finto Technologies Software are licensed under the original license provided by the owner of the applicable component.
- All content that resides under the "ee/", "web/src/ee/", and/or "worker/src/ee/" directories of this repository, if these directories exist, is licensed under the license defined in "ee/LICENSE".
- All third party components incorporated into the Langfuse Software are licensed under the original license provided by the owner of the applicable component.
- Content outside of the above mentioned directories or restrictions above is available under the "MIT Expat" license as defined below.
Permission is hereby granted, free of charge, to any person obtaining a copy
+52 -3
View File
@@ -42,9 +42,7 @@
## Langfuse Overview
_Unmute video for voice-over_
https://github.com/langfuse/langfuse/assets/2834609/a94062e9-c782-4ee9-af59-dee6370149a8
[![Langfuse Overview Video](https://github.com/user-attachments/assets/3926b288-ff61-4b95-8aa1-45d041c70866)](https://langfuse.com/watch-demo)
### Develop
@@ -191,3 +189,54 @@ You can opt-out by setting `TELEMETRY_ENABLED=false`.
<img alt="Star History Chart" src="https://api.star-history.com/svg?repos=langfuse/langfuse&type=Date" />
</picture>
</a>
### Open Source Projects Using Langfuse
Top open-source Python projects that use Langfuse, ranked by stars ([Source](https://github.com/langfuse/langfuse-docs/blob/main/components-mdx/dependents)):
| Repository | Stars |
| :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ----: |
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/127165244?s=40&v=4" width="20" height="20" alt=""> &nbsp; [langgenius](https://github.com/langgenius) / [dify](https://github.com/langgenius/dify) | 54865 |
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/158137808?s=40&v=4" width="20" height="20" alt=""> &nbsp; [open-webui](https://github.com/open-webui) / [open-webui](https://github.com/open-webui/open-webui) | 51531 |
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/131470832?s=40&v=4" width="20" height="20" alt=""> &nbsp; [lobehub](https://github.com/lobehub) / [lobe-chat](https://github.com/lobehub/lobe-chat) | 49003 |
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/85702467?s=40&v=4" width="20" height="20" alt=""> &nbsp; [langflow-ai](https://github.com/langflow-ai) / [langflow](https://github.com/langflow-ai/langflow) | 39093 |
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/130722866?s=40&v=4" width="20" height="20" alt=""> &nbsp; [run-llama](https://github.com/run-llama) / [llama_index](https://github.com/run-llama/llama_index) | 37368 |
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/139558948?s=40&v=4" width="20" height="20" alt=""> &nbsp; [chatchat-space](https://github.com/chatchat-space) / [Langchain-Chatchat](https://github.com/chatchat-space/Langchain-Chatchat) | 32486 |
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/128289781?s=40&v=4" width="20" height="20" alt=""> &nbsp; [FlowiseAI](https://github.com/FlowiseAI) / [Flowise](https://github.com/FlowiseAI/Flowise) | 32448 |
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/31035808?s=40&v=4" width="20" height="20" alt=""> &nbsp; [mindsdb](https://github.com/mindsdb) / [mindsdb](https://github.com/mindsdb/mindsdb) | 26931 |
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/119600397?s=40&v=4" width="20" height="20" alt=""> &nbsp; [twentyhq](https://github.com/twentyhq) / [twenty](https://github.com/twentyhq/twenty) | 24195 |
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/60330232?s=40&v=4" width="20" height="20" alt=""> &nbsp; [PostHog](https://github.com/PostHog) / [posthog](https://github.com/PostHog/posthog) | 22618 |
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/121462774?s=40&v=4" width="20" height="20" alt=""> &nbsp; [BerriAI](https://github.com/BerriAI) / [litellm](https://github.com/BerriAI/litellm) | 15151 |
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/179202840?s=40&v=4" width="20" height="20" alt=""> &nbsp; [mediar-ai](https://github.com/mediar-ai) / [screenpipe](https://github.com/mediar-ai/screenpipe) | 11037 |
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/105877416?s=40&v=4" width="20" height="20" alt=""> &nbsp; [formbricks](https://github.com/formbricks) / [formbricks](https://github.com/formbricks/formbricks) | 9386 |
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/76263028?s=40&v=4" width="20" height="20" alt=""> &nbsp; [anthropics](https://github.com/anthropics) / [courses](https://github.com/anthropics/courses) | 8385 |
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/78410652?s=40&v=4" width="20" height="20" alt=""> &nbsp; [GreyDGL](https://github.com/GreyDGL) / [PentestGPT](https://github.com/GreyDGL/PentestGPT) | 7374 |
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/152537519?s=40&v=4" width="20" height="20" alt=""> &nbsp; [superagent-ai](https://github.com/superagent-ai) / [superagent](https://github.com/superagent-ai/superagent) | 5391 |
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/137907881?s=40&v=4" width="20" height="20" alt=""> &nbsp; [promptfoo](https://github.com/promptfoo) / [promptfoo](https://github.com/promptfoo/promptfoo) | 4976 |
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/157326433?s=40&v=4" width="20" height="20" alt=""> &nbsp; [onlook-dev](https://github.com/onlook-dev) / [onlook](https://github.com/onlook-dev/onlook) | 4141 |
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/7250217?s=40&v=4" width="20" height="20" alt=""> &nbsp; [Canner](https://github.com/Canner) / [WrenAI](https://github.com/Canner/WrenAI) | 2526 |
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/11855343?s=40&v=4" width="20" height="20" alt=""> &nbsp; [pingcap](https://github.com/pingcap) / [autoflow](https://github.com/pingcap/autoflow) | 2061 |
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/85268109?s=40&v=4" width="20" height="20" alt=""> &nbsp; [MLSysOps](https://github.com/MLSysOps) / [MLE-agent](https://github.com/MLSysOps/MLE-agent) | 1161 |
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/158137808?s=40&v=4" width="20" height="20" alt=""> &nbsp; [open-webui](https://github.com/open-webui) / [pipelines](https://github.com/open-webui/pipelines) | 1100 |
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/18422723?s=40&v=4" width="20" height="20" alt=""> &nbsp; [alishobeiri](https://github.com/alishobeiri) / [thread](https://github.com/alishobeiri/thread) | 1074 |
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/125468716?s=40&v=4" width="20" height="20" alt=""> &nbsp; [topoteretes](https://github.com/topoteretes) / [cognee](https://github.com/topoteretes/cognee) | 971 |
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/188657705?s=40&v=4" width="20" height="20" alt=""> &nbsp; [bRAGAI](https://github.com/bRAGAI) / [bRAG-langchain](https://github.com/bRAGAI/bRAG-langchain) | 823 |
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/169500408?s=40&v=4" width="20" height="20" alt=""> &nbsp; [opslane](https://github.com/opslane) / [opslane](https://github.com/opslane/opslane) | 677 |
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/151867818?s=40&v=4" width="20" height="20" alt=""> &nbsp; [dynamiq-ai](https://github.com/dynamiq-ai) / [dynamiq](https://github.com/dynamiq-ai/dynamiq) | 639 |
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/48585267?s=40&v=4" width="20" height="20" alt=""> &nbsp; [theopenconversationkit](https://github.com/theopenconversationkit) / [tock](https://github.com/theopenconversationkit/tock) | 514 |
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/20493493?s=40&v=4" width="20" height="20" alt=""> &nbsp; [andysingal](https://github.com/andysingal) / [llm-course](https://github.com/andysingal/llm-course) | 394 |
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/132396805?s=40&v=4" width="20" height="20" alt=""> &nbsp; [phospho-app](https://github.com/phospho-app) / [phospho](https://github.com/phospho-app/phospho) | 384 |
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/178644984?s=40&v=4" width="20" height="20" alt=""> &nbsp; [sentient-engineering](https://github.com/sentient-engineering) / [agent-q](https://github.com/sentient-engineering/agent-q) | 370 |
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/168552753?s=40&v=4" width="20" height="20" alt=""> &nbsp; [sql-agi](https://github.com/sql-agi) / [DB-GPT](https://github.com/sql-agi/DB-GPT) | 324 |
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/60330232?s=40&v=4" width="20" height="20" alt=""> &nbsp; [PostHog](https://github.com/PostHog) / [posthog-foss](https://github.com/PostHog/posthog-foss) | 305 |
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/154247157?s=40&v=4" width="20" height="20" alt=""> &nbsp; [vespperhq](https://github.com/vespperhq) / [vespper](https://github.com/vespperhq/vespper) | 304 |
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/185116535?s=40&v=4" width="20" height="20" alt=""> &nbsp; [block](https://github.com/block) / [goose](https://github.com/block/goose) | 295 |
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/609489?s=40&v=4" width="20" height="20" alt=""> &nbsp; [aorwall](https://github.com/aorwall) / [moatless-tools](https://github.com/aorwall/moatless-tools) | 291 |
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/2357342?s=40&v=4" width="20" height="20" alt=""> &nbsp; [dmayboroda](https://github.com/dmayboroda) / [minima](https://github.com/dmayboroda/minima) | 221 |
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/66303003?s=40&v=4" width="20" height="20" alt=""> &nbsp; [RobotecAI](https://github.com/RobotecAI) / [rai](https://github.com/RobotecAI/rai) | 172 |
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/148684274?s=40&v=4" width="20" height="20" alt=""> &nbsp; [i-am-alice](https://github.com/i-am-alice) / [3rd-devs](https://github.com/i-am-alice/3rd-devs) | 148 |
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/171735272?s=40&v=4" width="20" height="20" alt=""> &nbsp; [8090-inc](https://github.com/8090-inc) / [xrx-sample-apps](https://github.com/8090-inc/xrx-sample-apps) | 138 |
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/104478511?s=40&v=4" width="20" height="20" alt=""> &nbsp; [babelcloud](https://github.com/babelcloud) / [LLM-RGB](https://github.com/babelcloud/LLM-RGB) | 135 |
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/15125613?s=40&v=4" width="20" height="20" alt=""> &nbsp; [souzatharsis](https://github.com/souzatharsis) / [tamingLLMs](https://github.com/souzatharsis/tamingLLMs) | 129 |
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/169401942?s=40&v=4" width="20" height="20" alt=""> &nbsp; [LibreChat-AI](https://github.com/LibreChat-AI) / [librechat.ai](https://github.com/LibreChat-AI/librechat.ai) | 128 |
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/51827949?s=40&v=4" width="20" height="20" alt=""> &nbsp; [deepset-ai](https://github.com/deepset-ai) / [haystack-core-integrations](https://github.com/deepset-ai/haystack-core-integrations) | 126 |
+9 -3
View File
@@ -1,13 +1,19 @@
_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.
Join us in building out Langfuse in Berlin, Germany. Langfuse is the open source LLM engineering platform: we build tooling to help developers [build & improve LLM applications](https://langfuse.com/docs).
We are an open source company, we hire in person (4+ days a week), we only hire excellent technical talent. Find more information on our [careers page](https://langfuse.com/careers)
_Open Roles_
- 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
- Product Engineer, 70-130k EUR, 0.1-0.35% Equity, https://www.ycombinator.com/companies/langfuse/jobs/aAvmoFB-product-engineer
- Backend Engineer, 70-130k EUR, 0.1-0.35% Equity, https://www.ycombinator.com/companies/langfuse/jobs/1bO16H6-backend-engineer
- Design Engineer, 70-130k EUR, 0.1-0.35% Equity, https://www.ycombinator.com/companies/langfuse/jobs/mDquP95-design-engineer
- Developer Advocate, 70-130k EUR, 0.1-0.35% 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/why
- https://langfuse.com/changelog
+113 -44
View File
@@ -1,34 +1,53 @@
version: "3.5"
services:
server:
langfuse-web:
build:
dockerfile: ./web/Dockerfile
context: .
args:
- NEXT_PUBLIC_LANGFUSE_CLOUD_REGION=${NEXT_PUBLIC_LANGFUSE_CLOUD_REGION}
depends_on:
- db
- redis
depends_on: &langfuse-depends-on
postgres:
condition: service_healthy
minio:
condition: service_healthy
redis:
condition: service_healthy
clickhouse:
condition: service_healthy
ports:
- "3000:3000"
environment:
- DATABASE_URL=postgresql://postgres:postgres@db:5432/postgres
- NEXTAUTH_SECRET=mysecret
- SALT=mysalt
- ENCRYPTION_KEY=0000000000000000000000000000000000000000000000000000000000000000 # generate via `openssl rand -hex 32`
- NEXTAUTH_URL=http://localhost:3000
- TELEMETRY_ENABLED=${TELEMETRY_ENABLED:-true}
- LANGFUSE_ENABLE_EXPERIMENTAL_FEATURES=${LANGFUSE_ENABLE_EXPERIMENTAL_FEATURES:-false}
- LANGFUSE_INIT_ORG_ID=${LANGFUSE_INIT_ORG_ID:-}
- LANGFUSE_INIT_ORG_NAME=${LANGFUSE_INIT_ORG_NAME:-}
- LANGFUSE_INIT_PROJECT_ID=${LANGFUSE_INIT_PROJECT_ID:-}
- LANGFUSE_INIT_PROJECT_NAME=${LANGFUSE_INIT_PROJECT_NAME:-}
- LANGFUSE_INIT_PROJECT_PUBLIC_KEY=${LANGFUSE_INIT_PROJECT_PUBLIC_KEY:-}
- LANGFUSE_INIT_PROJECT_SECRET_KEY=${LANGFUSE_INIT_PROJECT_SECRET_KEY:-}
- LANGFUSE_INIT_USER_EMAIL=${LANGFUSE_INIT_USER_EMAIL:-}
- LANGFUSE_INIT_USER_NAME=${LANGFUSE_INIT_USER_NAME:-}
- LANGFUSE_INIT_USER_PASSWORD=${LANGFUSE_INIT_USER_PASSWORD:-}
environment: &langfuse-web-env
DATABASE_URL: postgresql://postgres:postgres@postgres:5432/postgres
NEXTAUTH_SECRET: mysecret
SALT: mysalt
ENCRYPTION_KEY: "0000000000000000000000000000000000000000000000000000000000000000" # generate via `openssl rand -hex 32`
NEXTAUTH_URL: http://localhost:3000
TELEMETRY_ENABLED: ${TELEMETRY_ENABLED:-true}
LANGFUSE_ENABLE_EXPERIMENTAL_FEATURES: ${LANGFUSE_ENABLE_EXPERIMENTAL_FEATURES:-false}
LANGFUSE_INIT_ORG_ID: ${LANGFUSE_INIT_ORG_ID:-}
LANGFUSE_INIT_ORG_NAME: ${LANGFUSE_INIT_ORG_NAME:-}
LANGFUSE_INIT_PROJECT_ID: ${LANGFUSE_INIT_PROJECT_ID:-}
LANGFUSE_INIT_PROJECT_NAME: ${LANGFUSE_INIT_PROJECT_NAME:-}
LANGFUSE_INIT_PROJECT_PUBLIC_KEY: ${LANGFUSE_INIT_PROJECT_PUBLIC_KEY:-}
LANGFUSE_INIT_PROJECT_SECRET_KEY: ${LANGFUSE_INIT_PROJECT_SECRET_KEY:-}
LANGFUSE_INIT_USER_EMAIL: ${LANGFUSE_INIT_USER_EMAIL:-}
LANGFUSE_INIT_USER_NAME: ${LANGFUSE_INIT_USER_NAME:-}
LANGFUSE_INIT_USER_PASSWORD: ${LANGFUSE_INIT_USER_PASSWORD:-}
CLICKHOUSE_MIGRATION_URL: ${CLICKHOUSE_MIGRATION_URL:-clickhouse://clickhouse:9000}
CLICKHOUSE_URL: ${CLICKHOUSE_URL:-http://clickhouse:8123}
CLICKHOUSE_USER: ${CLICKHOUSE_USER:-clickhouse}
CLICKHOUSE_PASSWORD: ${CLICKHOUSE_PASSWORD:-clickhouse}
CLICKHOUSE_CLUSTER_ENABLED: ${CLICKHOUSE_CLUSTER_ENABLED:-false}
LANGFUSE_S3_EVENT_UPLOAD_BUCKET: ${LANGFUSE_S3_EVENT_UPLOAD_BUCKET:-langfuse}
LANGFUSE_S3_EVENT_UPLOAD_REGION: ${LANGFUSE_S3_EVENT_UPLOAD_REGION:-us-east-1}
LANGFUSE_S3_EVENT_UPLOAD_ACCESS_KEY_ID: ${LANGFUSE_S3_EVENT_UPLOAD_ACCESS_KEY_ID:-minio}
LANGFUSE_S3_EVENT_UPLOAD_SECRET_ACCESS_KEY: ${LANGFUSE_S3_EVENT_UPLOAD_SECRET_ACCESS_KEY:-miniosecret}
LANGFUSE_S3_EVENT_UPLOAD_ENDPOINT: ${LANGFUSE_S3_EVENT_UPLOAD_ENDPOINT:-http://minio:9000}
LANGFUSE_S3_EVENT_UPLOAD_FORCE_PATH_STYLE: ${LANGFUSE_S3_EVENT_UPLOAD_FORCE_PATH_STYLE:-true}
LANGFUSE_S3_EVENT_UPLOAD_PREFIX: ${LANGFUSE_S3_EVENT_UPLOAD_PREFIX:-events/}
REDIS_HOST: ${REDIS_HOST:-redis}
REDIS_PORT: ${REDIS_PORT:-6379}
REDIS_AUTH: ${REDIS_AUTH:-myredissecret}
restart: always
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:3000/api/public/health"]
@@ -36,26 +55,17 @@ services:
timeout: 10s
retries: 3
worker:
langfuse-worker:
build:
dockerfile: ./worker/Dockerfile
context: .
args:
- NEXT_PUBLIC_LANGFUSE_CLOUD_REGION=${NEXT_PUBLIC_LANGFUSE_CLOUD_REGION}
depends_on:
- db
- redis
depends_on: *langfuse-depends-on
ports:
- "3030:3030"
environment:
- DATABASE_URL=postgresql://postgres:postgres@db:5432/postgres
- NEXTAUTH_SECRET=mysecret
- TELEMETRY_ENABLED=${TELEMETRY_ENABLED:-true}
- LANGFUSE_ENABLE_EXPERIMENTAL_FEATURES=${LANGFUSE_ENABLE_EXPERIMENTAL_FEATURES:-false}
- PORT=${PORT:-3030}
- REDIS_HOST=${REDIS_HOST:-redis}
- REDIS_PORT=${REDIS_PORT:-6379}
- REDIS_AUTH=${REDIS_AUTH:-myredissecret}
<<: *langfuse-web-env
restart: always
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:3030/api/health"]
@@ -63,26 +73,85 @@ services:
timeout: 10s
retries: 3
clickhouse:
image: clickhouse/clickhouse-server
user: "101:101"
container_name: clickhouse
hostname: clickhouse
environment:
CLICKHOUSE_DB: default
CLICKHOUSE_USER: clickhouse
CLICKHOUSE_PASSWORD: clickhouse
volumes:
- langfuse_clickhouse_data:/var/lib/clickhouse
- langfuse_clickhouse_logs:/var/log/clickhouse-server
ports:
- "8123:8123"
- "9000:9000"
healthcheck:
test: wget --no-verbose --tries=1 --spider http://localhost:8123/ping || exit 1
interval: 5s
timeout: 5s
retries: 10
start_period: 1s
minio:
image: minio/minio
container_name: minio
entrypoint: sh
# create the 'langfuse' bucket before starting the service
command: -c 'mkdir -p /data/langfuse && minio server --address ":9000" --console-address ":9001" /data'
environment:
MINIO_ROOT_USER: minio
MINIO_ROOT_PASSWORD: miniosecret
ports:
- "9090:9000"
- "9091:9001"
volumes:
- langfuse_minio_data:/data
healthcheck:
test: ["CMD", "mc", "ready", "local"]
interval: 1s
timeout: 5s
retries: 5
start_period: 1s
redis:
image: redis:7.2.4
image: redis:7
restart: always
command: >
--requirepass ${REDIS_AUTH:-myredissecret}
restart: always
ports:
- 6379:6379
healthcheck:
test: ["CMD", "redis-cli", "ping"]
interval: 3s
timeout: 10s
retries: 10
db:
image: postgres
postgres:
image: postgres:${POSTGRES_VERSION:-latest}
restart: always
healthcheck:
test: ["CMD-SHELL", "pg_isready -U postgres"]
interval: 3s
timeout: 3s
retries: 10
environment:
- POSTGRES_USER=postgres
- POSTGRES_PASSWORD=postgres
- POSTGRES_DB=postgres
POSTGRES_USER: postgres
POSTGRES_PASSWORD: postgres
POSTGRES_DB: postgres
ports:
- 5432:5432
volumes:
- database_data:/var/lib/postgresql/data
- langfuse_postgres_data:/var/lib/postgresql/data
volumes:
database_data:
langfuse_postgres_data:
driver: local
langfuse_clickhouse_data:
driver: local
langfuse_clickhouse_logs:
driver: local
langfuse_minio_data:
driver: local
-145
View File
@@ -1,145 +0,0 @@
services:
langfuse-worker:
image: langfuse/langfuse-worker:latest
depends_on: &langfuse-depends-on
postgres:
condition: service_healthy
minio:
condition: service_healthy
redis:
condition: service_healthy
ports:
- "3030:3030"
environment: &langfuse-worker-env
DATABASE_URL: postgresql://postgres:postgres@postgres:5432/postgres
SALT: "mysalt"
ENCRYPTION_KEY: "0000000000000000000000000000000000000000000000000000000000000000" # generate via `openssl rand -hex 32`
TELEMETRY_ENABLED: ${TELEMETRY_ENABLED:-true}
LANGFUSE_ENABLE_EXPERIMENTAL_FEATURES: ${LANGFUSE_ENABLE_EXPERIMENTAL_FEATURES:-true}
LANGFUSE_ASYNC_INGESTION_PROCESSING: ${LANGFUSE_ASYNC_INGESTION_PROCESSING:-true}
LANGFUSE_ASYNC_CLICKHOUSE_INGESTION_PROCESSING: ${LANGFUSE_ASYNC_CLICKHOUSE_INGESTION_PROCESSING:-true}
LANGFUSE_READ_DASHBOARDS_FROM_CLICKHOUSE: ${LANGFUSE_READ_DASHBOARDS_FROM_CLICKHOUSE:-true}
LANGFUSE_READ_FROM_POSTGRES_ONLY: ${LANGFUSE_READ_FROM_POSTGRES_ONLY:-false}
LANGFUSE_RETURN_FROM_CLICKHOUSE: ${LANGFUSE_RETURN_FROM_CLICKHOUSE:-true}
CLICKHOUSE_MIGRATION_URL: ${CLICKHOUSE_MIGRATION_URL:-clickhouse://clickhouse:9000}
CLICKHOUSE_URL: ${CLICKHOUSE_URL:-http://clickhouse:8123}
CLICKHOUSE_USER: ${CLICKHOUSE_USER:-clickhouse}
CLICKHOUSE_PASSWORD: ${CLICKHOUSE_PASSWORD:-clickhouse}
CLICKHOUSE_CLUSTER_ENABLED: ${CLICKHOUSE_CLUSTER_ENABLED:-false}
LANGFUSE_S3_EVENT_UPLOAD_ENABLED: ${LANGFUSE_S3_EVENT_UPLOAD_ENABLED:-true}
LANGFUSE_S3_EVENT_UPLOAD_BUCKET: ${LANGFUSE_S3_EVENT_UPLOAD_BUCKET:-langfuse}
LANGFUSE_S3_EVENT_UPLOAD_REGION: ${LANGFUSE_S3_EVENT_UPLOAD_REGION:-us-east-1}
LANGFUSE_S3_EVENT_UPLOAD_ACCESS_KEY_ID: ${LANGFUSE_S3_EVENT_UPLOAD_ACCESS_KEY_ID:-minio}
LANGFUSE_S3_EVENT_UPLOAD_SECRET_ACCESS_KEY: ${LANGFUSE_S3_EVENT_UPLOAD_SECRET_ACCESS_KEY:-miniosecret}
LANGFUSE_S3_EVENT_UPLOAD_ENDPOINT: ${LANGFUSE_S3_EVENT_UPLOAD_ENDPOINT:-http://minio:9000}
LANGFUSE_S3_EVENT_UPLOAD_FORCE_PATH_STYLE: ${LANGFUSE_S3_EVENT_UPLOAD_FORCE_PATH_STYLE:-true}
LANGFUSE_S3_EVENT_UPLOAD_PREFIX: ${LANGFUSE_S3_EVENT_UPLOAD_PREFIX:-events/}
LANGFUSE_S3_MEDIA_UPLOAD_ENABLED: ${LANGFUSE_S3_MEDIA_UPLOAD_ENABLED:-true}
LANGFUSE_S3_MEDIA_UPLOAD_BUCKET: ${LANGFUSE_S3_MEDIA_UPLOAD_BUCKET:-langfuse}
LANGFUSE_S3_MEDIA_UPLOAD_REGION: ${LANGFUSE_S3_MEDIA_UPLOAD_REGION:-us-east-1}
LANGFUSE_S3_MEDIA_UPLOAD_ACCESS_KEY_ID: ${LANGFUSE_S3_MEDIA_UPLOAD_ACCESS_KEY_ID:-minio}
LANGFUSE_S3_MEDIA_UPLOAD_SECRET_ACCESS_KEY: ${LANGFUSE_S3_MEDIA_UPLOAD_SECRET_ACCESS_KEY:-miniosecret}
LANGFUSE_S3_MEDIA_UPLOAD_ENDPOINT: ${LANGFUSE_S3_MEDIA_UPLOAD_ENDPOINT:-http://minio:9000}
LANGFUSE_S3_MEDIA_UPLOAD_FORCE_PATH_STYLE: ${LANGFUSE_S3_MEDIA_UPLOAD_FORCE_PATH_STYLE:-true}
LANGFUSE_S3_MEDIA_UPLOAD_PREFIX: ${LANGFUSE_S3_MEDIA_UPLOAD_PREFIX:-media/}
REDIS_HOST: ${REDIS_HOST:-redis}
REDIS_PORT: ${REDIS_PORT:-6379}
REDIS_AUTH: ${REDIS_AUTH:-myredissecret}
langfuse-web:
image: langfuse/langfuse:latest
depends_on: *langfuse-depends-on
ports:
- "3000:3000"
environment:
<<: *langfuse-worker-env
NEXTAUTH_URL: http://localhost:3000
NEXTAUTH_SECRET: mysecret
LANGFUSE_INIT_ORG_ID: ${LANGFUSE_INIT_ORG_ID:-}
LANGFUSE_INIT_ORG_NAME: ${LANGFUSE_INIT_ORG_NAME:-}
LANGFUSE_INIT_PROJECT_ID: ${LANGFUSE_INIT_PROJECT_ID:-}
LANGFUSE_INIT_PROJECT_NAME: ${LANGFUSE_INIT_PROJECT_NAME:-}
LANGFUSE_INIT_PROJECT_PUBLIC_KEY: ${LANGFUSE_INIT_PROJECT_PUBLIC_KEY:-}
LANGFUSE_INIT_PROJECT_SECRET_KEY: ${LANGFUSE_INIT_PROJECT_SECRET_KEY:-}
LANGFUSE_INIT_USER_EMAIL: ${LANGFUSE_INIT_USER_EMAIL:-}
LANGFUSE_INIT_USER_NAME: ${LANGFUSE_INIT_USER_NAME:-}
LANGFUSE_INIT_USER_PASSWORD: ${LANGFUSE_INIT_USER_PASSWORD:-}
clickhouse:
image: clickhouse/clickhouse-server
user: "101:101"
container_name: clickhouse
hostname: clickhouse
environment:
CLICKHOUSE_DB: default
CLICKHOUSE_USER: clickhouse
CLICKHOUSE_PASSWORD: clickhouse
volumes:
- langfuse_clickhouse_data:/var/lib/clickhouse
- langfuse_clickhouse_logs:/var/log/clickhouse-server
ports:
- "8123:8123"
- "9000:9000"
depends_on:
- postgres
minio:
image: minio/minio
container_name: minio
entrypoint: sh
# create the 'langfuse' bucket before starting the service
command: -c 'mkdir -p /data/langfuse && minio server --address ":9000" --console-address ":9001" /data'
environment:
MINIO_ROOT_USER: minio
MINIO_ROOT_PASSWORD: miniosecret
ports:
- "9090:9000"
- "9091:9001"
volumes:
- langfuse_minio_data:/data
healthcheck:
test: ["CMD", "mc", "ready", "local"]
interval: 1s
timeout: 5s
retries: 5
start_period: 1s
redis:
image: redis:7
restart: always
command: >
--requirepass ${REDIS_AUTH:-myredissecret}
ports:
- 6379:6379
healthcheck:
test: ["CMD", "redis-cli", "ping"]
interval: 3s
timeout: 10s
retries: 10
postgres:
image: postgres:${POSTGRES_VERSION:-latest}
restart: always
healthcheck:
test: ["CMD-SHELL", "pg_isready -U postgres"]
interval: 3s
timeout: 3s
retries: 10
environment:
POSTGRES_USER: postgres
POSTGRES_PASSWORD: postgres
POSTGRES_DB: postgres
ports:
- 5432:5432
volumes:
- langfuse_postgres_data:/var/lib/postgresql/data
volumes:
langfuse_postgres_data:
driver: local
langfuse_clickhouse_data:
driver: local
langfuse_clickhouse_logs:
driver: local
langfuse_minio_data:
driver: local
+131 -27
View File
@@ -1,31 +1,129 @@
services:
langfuse-server:
image: langfuse/langfuse:2
depends_on:
db:
langfuse-worker:
image: langfuse/langfuse-worker:3
restart: always
depends_on: &langfuse-depends-on
postgres:
condition: service_healthy
minio:
condition: service_healthy
redis:
condition: service_healthy
clickhouse:
condition: service_healthy
ports:
- "3030:3030"
environment: &langfuse-worker-env
DATABASE_URL: postgresql://postgres:postgres@postgres:5432/postgres
SALT: "mysalt"
ENCRYPTION_KEY: "0000000000000000000000000000000000000000000000000000000000000000" # generate via `openssl rand -hex 32`
TELEMETRY_ENABLED: ${TELEMETRY_ENABLED:-true}
LANGFUSE_ENABLE_EXPERIMENTAL_FEATURES: ${LANGFUSE_ENABLE_EXPERIMENTAL_FEATURES:-true}
CLICKHOUSE_MIGRATION_URL: ${CLICKHOUSE_MIGRATION_URL:-clickhouse://clickhouse:9000}
CLICKHOUSE_URL: ${CLICKHOUSE_URL:-http://clickhouse:8123}
CLICKHOUSE_USER: ${CLICKHOUSE_USER:-clickhouse}
CLICKHOUSE_PASSWORD: ${CLICKHOUSE_PASSWORD:-clickhouse}
CLICKHOUSE_CLUSTER_ENABLED: ${CLICKHOUSE_CLUSTER_ENABLED:-false}
LANGFUSE_S3_EVENT_UPLOAD_BUCKET: ${LANGFUSE_S3_EVENT_UPLOAD_BUCKET:-langfuse}
LANGFUSE_S3_EVENT_UPLOAD_REGION: ${LANGFUSE_S3_EVENT_UPLOAD_REGION:-auto}
LANGFUSE_S3_EVENT_UPLOAD_ACCESS_KEY_ID: ${LANGFUSE_S3_EVENT_UPLOAD_ACCESS_KEY_ID:-minio}
LANGFUSE_S3_EVENT_UPLOAD_SECRET_ACCESS_KEY: ${LANGFUSE_S3_EVENT_UPLOAD_SECRET_ACCESS_KEY:-miniosecret}
LANGFUSE_S3_EVENT_UPLOAD_ENDPOINT: ${LANGFUSE_S3_EVENT_UPLOAD_ENDPOINT:-http://minio:9000}
LANGFUSE_S3_EVENT_UPLOAD_FORCE_PATH_STYLE: ${LANGFUSE_S3_EVENT_UPLOAD_FORCE_PATH_STYLE:-true}
LANGFUSE_S3_EVENT_UPLOAD_PREFIX: ${LANGFUSE_S3_EVENT_UPLOAD_PREFIX:-events/}
LANGFUSE_S3_MEDIA_UPLOAD_BUCKET: ${LANGFUSE_S3_MEDIA_UPLOAD_BUCKET:-langfuse}
LANGFUSE_S3_MEDIA_UPLOAD_REGION: ${LANGFUSE_S3_MEDIA_UPLOAD_REGION:-auto}
LANGFUSE_S3_MEDIA_UPLOAD_ACCESS_KEY_ID: ${LANGFUSE_S3_MEDIA_UPLOAD_ACCESS_KEY_ID:-minio}
LANGFUSE_S3_MEDIA_UPLOAD_SECRET_ACCESS_KEY: ${LANGFUSE_S3_MEDIA_UPLOAD_SECRET_ACCESS_KEY:-miniosecret}
LANGFUSE_S3_MEDIA_UPLOAD_ENDPOINT: ${LANGFUSE_S3_MEDIA_UPLOAD_ENDPOINT:-http://minio:9000}
LANGFUSE_S3_MEDIA_UPLOAD_FORCE_PATH_STYLE: ${LANGFUSE_S3_MEDIA_UPLOAD_FORCE_PATH_STYLE:-true}
LANGFUSE_S3_MEDIA_UPLOAD_PREFIX: ${LANGFUSE_S3_MEDIA_UPLOAD_PREFIX:-media/}
LANGFUSE_INGESTION_QUEUE_DELAY_MS: ${LANGFUSE_INGESTION_QUEUE_DELAY_MS:-}
LANGFUSE_INGESTION_CLICKHOUSE_WRITE_INTERVAL_MS: ${LANGFUSE_INGESTION_CLICKHOUSE_WRITE_INTERVAL_MS:-}
REDIS_HOST: ${REDIS_HOST:-redis}
REDIS_PORT: ${REDIS_PORT:-6379}
REDIS_AUTH: ${REDIS_AUTH:-myredissecret}
langfuse-web:
image: langfuse/langfuse:3
restart: always
depends_on: *langfuse-depends-on
ports:
- "3000:3000"
environment:
- DATABASE_URL=postgresql://postgres:postgres@db:5432/postgres
- NEXTAUTH_SECRET=mysecret
- SALT=mysalt
- ENCRYPTION_KEY=0000000000000000000000000000000000000000000000000000000000000000 # generate via `openssl rand -hex 32`
- NEXTAUTH_URL=http://localhost:3000
- TELEMETRY_ENABLED=${TELEMETRY_ENABLED:-true}
- LANGFUSE_ENABLE_EXPERIMENTAL_FEATURES=${LANGFUSE_ENABLE_EXPERIMENTAL_FEATURES:-false}
- LANGFUSE_INIT_ORG_ID=${LANGFUSE_INIT_ORG_ID:-}
- LANGFUSE_INIT_ORG_NAME=${LANGFUSE_INIT_ORG_NAME:-}
- LANGFUSE_INIT_PROJECT_ID=${LANGFUSE_INIT_PROJECT_ID:-}
- LANGFUSE_INIT_PROJECT_NAME=${LANGFUSE_INIT_PROJECT_NAME:-}
- LANGFUSE_INIT_PROJECT_PUBLIC_KEY=${LANGFUSE_INIT_PROJECT_PUBLIC_KEY:-}
- LANGFUSE_INIT_PROJECT_SECRET_KEY=${LANGFUSE_INIT_PROJECT_SECRET_KEY:-}
- LANGFUSE_INIT_USER_EMAIL=${LANGFUSE_INIT_USER_EMAIL:-}
- LANGFUSE_INIT_USER_NAME=${LANGFUSE_INIT_USER_NAME:-}
- LANGFUSE_INIT_USER_PASSWORD=${LANGFUSE_INIT_USER_PASSWORD:-}
<<: *langfuse-worker-env
NEXTAUTH_URL: http://localhost:3000
NEXTAUTH_SECRET: mysecret
LANGFUSE_INIT_ORG_ID: ${LANGFUSE_INIT_ORG_ID:-}
LANGFUSE_INIT_ORG_NAME: ${LANGFUSE_INIT_ORG_NAME:-}
LANGFUSE_INIT_PROJECT_ID: ${LANGFUSE_INIT_PROJECT_ID:-}
LANGFUSE_INIT_PROJECT_NAME: ${LANGFUSE_INIT_PROJECT_NAME:-}
LANGFUSE_INIT_PROJECT_PUBLIC_KEY: ${LANGFUSE_INIT_PROJECT_PUBLIC_KEY:-}
LANGFUSE_INIT_PROJECT_SECRET_KEY: ${LANGFUSE_INIT_PROJECT_SECRET_KEY:-}
LANGFUSE_INIT_USER_EMAIL: ${LANGFUSE_INIT_USER_EMAIL:-}
LANGFUSE_INIT_USER_NAME: ${LANGFUSE_INIT_USER_NAME:-}
LANGFUSE_INIT_USER_PASSWORD: ${LANGFUSE_INIT_USER_PASSWORD:-}
db:
image: postgres
clickhouse:
image: clickhouse/clickhouse-server
restart: always
user: "101:101"
container_name: clickhouse
hostname: clickhouse
environment:
CLICKHOUSE_DB: default
CLICKHOUSE_USER: clickhouse
CLICKHOUSE_PASSWORD: clickhouse
volumes:
- langfuse_clickhouse_data:/var/lib/clickhouse
- langfuse_clickhouse_logs:/var/log/clickhouse-server
ports:
- "8123:8123"
- "9000:9000"
healthcheck:
test: wget --no-verbose --tries=1 --spider http://localhost:8123/ping || exit 1
interval: 5s
timeout: 5s
retries: 10
start_period: 1s
minio:
image: minio/minio
restart: always
container_name: minio
entrypoint: sh
# create the 'langfuse' bucket before starting the service
command: -c 'mkdir -p /data/langfuse && minio server --address ":9000" --console-address ":9001" /data'
environment:
MINIO_ROOT_USER: minio
MINIO_ROOT_PASSWORD: miniosecret
ports:
- "9090:9000"
- "9091:9001"
volumes:
- langfuse_minio_data:/data
healthcheck:
test: ["CMD", "mc", "ready", "local"]
interval: 1s
timeout: 5s
retries: 5
start_period: 1s
redis:
image: redis:7
restart: always
command: >
--requirepass ${REDIS_AUTH:-myredissecret}
ports:
- 6379:6379
healthcheck:
test: ["CMD", "redis-cli", "ping"]
interval: 3s
timeout: 10s
retries: 10
postgres:
image: postgres:${POSTGRES_VERSION:-latest}
restart: always
healthcheck:
test: ["CMD-SHELL", "pg_isready -U postgres"]
@@ -33,14 +131,20 @@ services:
timeout: 3s
retries: 10
environment:
- POSTGRES_USER=postgres
- POSTGRES_PASSWORD=postgres
- POSTGRES_DB=postgres
POSTGRES_USER: postgres
POSTGRES_PASSWORD: postgres
POSTGRES_DB: postgres
ports:
- 5432:5432
volumes:
- database_data:/var/lib/postgresql/data
- langfuse_postgres_data:/var/lib/postgresql/data
volumes:
database_data:
langfuse_postgres_data:
driver: local
langfuse_clickhouse_data:
driver: local
langfuse_clickhouse_logs:
driver: local
langfuse_minio_data:
driver: local
+4 -3
View File
@@ -27,8 +27,8 @@
"@langfuse/shared": "workspace:*",
"@opentelemetry/api": ">=1.0.0 <1.10.0",
"axios": "^1.7.7",
"next": "^14.2.15",
"next-auth": "^4.24.7",
"next": "^14.2.21",
"next-auth": "^4.24.11",
"zod": "^3.23.8"
},
"devDependencies": {
@@ -47,7 +47,8 @@
},
"pnpm": {
"overrides": {
"jsonpath-plus": "10.0.7"
"jsonpath-plus": "10.0.7",
"nanoid": "^3.3.8"
}
}
}
+2 -5
View File
@@ -1,12 +1,9 @@
import { z } from "zod";
import { env as sharedEnv, removeEmptyEnvVariables } from "@langfuse/shared";
import { removeEmptyEnvVariables } from "@langfuse/shared";
const EnvSchema = z.object({
NEXT_PUBLIC_LANGFUSE_CLOUD_REGION: z.string().optional(),
LANGFUSE_EE_LICENSE_KEY: z.string().optional(),
});
export const env = {
...sharedEnv,
...EnvSchema.parse(removeEmptyEnvVariables(process.env)),
};
export const env = EnvSchema.parse(removeEmptyEnvVariables(process.env));
+1 -1
View File
@@ -58,7 +58,7 @@ types:
docs: The id of the object to attach the comment to. If this does not reference a valid existing object, an error will be thrown.
content:
type: string
docs: The content of the comment. May include markdown. Currently limited to 500 characters.
docs: The content of the comment. May include markdown. Currently limited to 3000 characters.
authorUserId:
type: optional<string>
docs: The id of the user who created the comment.
+13 -7
View File
@@ -126,7 +126,7 @@ types:
docs: The output data of the observation
usage:
type: optional<Usage>
docs: The usage data of the observation
docs: (Deprecated. Use usageDetails and costDetails instead.) The usage data of the observation
level:
type: ObservationLevel
docs: The level of the observation
@@ -139,6 +139,12 @@ types:
promptId:
type: optional<string>
docs: The prompt ID associated with the observation
usageDetails:
type: optional<map<string, integer>>
docs: The usage details of the observation. Key is the name of the usage metric, value is the number of units consumed. The total key is the sum of all (non-total) usage metrics or the total value ingested.
costDetails:
type: optional<map<string, double>>
docs: The cost details of the observation. Key is the name of the cost metric, value is the cost in USD. The total key is the sum of all (non-total) cost metrics or the total value ingested.
ObservationsView:
extends: Observation
@@ -163,13 +169,13 @@ types:
docs: The total price in USD.
calculatedInputCost:
type: optional<double>
docs: The calculated cost of the input in USD
docs: (Deprecated. Use usageDetails and costDetails instead.) The calculated cost of the input in USD
calculatedOutputCost:
type: optional<double>
docs: The calculated cost of the output in USD
docs: (Deprecated. Use usageDetails and costDetails instead.) The calculated cost of the output in USD
calculatedTotalCost:
type: optional<double>
docs: The calculated total cost in USD
docs: (Deprecated. Use usageDetails and costDetails instead.) The calculated total cost in USD
latency:
type: optional<double>
docs: The latency in seconds.
@@ -178,7 +184,7 @@ types:
docs: The time to the first token in seconds
Usage:
docs: Standard interface for usage and cost
docs: (Deprecated. Use usageDetails and costDetails instead.) Standard interface for usage and cost
properties:
input:
docs: Number of input units (e.g. tokens)
@@ -372,10 +378,10 @@ types:
type: string
startDate:
docs: Apply only to generations which are newer than this ISO date.
type: optional<date>
type: optional<datetime>
unit:
docs: Unit used by this model.
type: ModelUsageUnit
type: optional<ModelUsageUnit>
inputPrice:
docs: Price (USD) per input unit
type: optional<double>
+19
View File
@@ -11,6 +11,7 @@ service:
Batched ingestion for Langfuse Tracing. If you want to use tracing via the API, such as to build your own Langfuse client implementation, this is the only API route you need to implement.
Notes:
- Introduction to data model: https://langfuse.com/docs/tracing-data-model
- Batch sizes are limited to 3.5 MB in total. You need to adjust the number of events per batch accordingly.
- The API does not return a 4xx status code for input errors. Instead, it responds with a 207 status code, which includes a list of the encountered errors.
method: POST
@@ -126,6 +127,8 @@ types:
model: optional<string>
modelParameters: optional<map<string, commons.MapValue>>
usage: optional<IngestionUsage>
usageDetails: optional<UsageDetails>
costDetails: optional<map<string, float>>
promptName: optional<string>
promptVersion: optional<integer>
@@ -137,6 +140,8 @@ types:
modelParameters: optional<map<string, commons.MapValue>>
usage: optional<IngestionUsage>
promptName: optional<string>
usageDetails: optional<UsageDetails>
costDetails: optional<map<string, float>>
promptVersion: optional<integer>
ObservationBody:
@@ -312,3 +317,17 @@ types:
properties:
successes: list<IngestionSuccess>
errors: list<IngestionError>
OpenAIUsageSchema:
properties:
prompt_tokens: integer
completion_tokens: integer
total_tokens: integer
prompt_tokens_details: optional<map<string, integer>>
completion_tokens_details: optional<map<string, integer>>
UsageDetails:
discriminated: false
union:
- map<string, integer>
- OpenAIUsageSchema
+59 -1
View File
@@ -99,5 +99,63 @@ types:
docs: The unique langfuse identifier of a media record
MediaContentType:
type: literal<"image/png","image/jpeg","image/jpg","image/webp","audio/mpeg","audio/mp3","audio/wav","text/plain","application/pdf">
enum:
- value: image/png
name: IMAGE_PNG
- value: image/jpeg
name: IMAGE_JPEG
- value: image/jpg
name: IMAGE_JPG
- value: image/webp
name: IMAGE_WEBP
- value: image/gif
name: IMAGE_GIF
- value: image/svg+xml
name: IMAGE_SVG_XML
- value: image/tiff
name: IMAGE_TIFF
- value: image/bmp
name: IMAGE_BMP
- value: audio/mpeg
name: AUDIO_MPEG
- value: audio/mp3
name: AUDIO_MP3
- value: audio/wav
name: AUDIO_WAV
- value: audio/ogg
name: AUDIO_OGG
- value: audio/oga
name: AUDIO_OGA
- value: audio/aac
name: AUDIO_AAC
- value: audio/mp4
name: AUDIO_MP4
- value: audio/flac
name: AUDIO_FLAC
- value: video/mp4
name: VIDEO_MP4
- value: video/webm
name: VIDEO_WEBM
- value: text/plain
name: TEXT_PLAIN
- value: text/html
name: TEXT_HTML
- value: text/css
name: TEXT_CSS
- value: text/csv
name: TEXT_CSV
- value: application/pdf
name: APPLICATION_PDF
- value: application/msword
name: APPLICATION_MSWORD
- value: application/vnd.ms-excel
name: APPLICATION_MS_EXCEL
- value: application/zip
name: APPLICATION_ZIP
- value: application/json
name: APPLICATION_JSON
- value: application/xml
name: APPLICATION_XML
- value: application/octet-stream
name: APPLICATION_OCTET_STREAM
docs: The MIME type of the media record
+1 -1
View File
@@ -58,7 +58,7 @@ types:
type: optional<datetime>
unit:
docs: Unit used by this model.
type: commons.ModelUsageUnit
type: optional<commons.ModelUsageUnit>
inputPrice:
docs: Price (USD) per input unit
type: optional<double>
+3 -2
View File
@@ -1,4 +1,5 @@
{
"organization": "finto",
"organization": "langfuse",
"version": "0.43.7"
}
}
+6 -5
View File
@@ -1,6 +1,6 @@
{
"name": "langfuse",
"version": "2.92.0",
"version": "3.13.0",
"author": "engineering@langfuse.com",
"license": "MIT",
"private": true,
@@ -25,7 +25,6 @@
"dev": "turbo run dev",
"lint": "turbo run lint",
"test": "turbo run test",
"models:migrate": "turbo run models:migrate",
"release": "dotenv -e ../.env -- release-it",
"prepare": "husky"
},
@@ -81,10 +80,12 @@
}
}
},
"packageManager": "pnpm@9.5.0",
"pnpm": {
"overrides": {
"jsonpath-plus": "10.0.7"
"jsonpath-plus": "10.0.7",
"nanoid": "^3.3.8",
"katex": "^0.16.21"
}
},
"packageManager": "pnpm@9.5.0"
}
}
@@ -0,0 +1 @@
ALTER TABLE traces ON CLUSTER default DROP INDEX IF EXISTS idx_user_id;
@@ -0,0 +1,2 @@
ALTER TABLE traces ON CLUSTER default ADD INDEX IF NOT EXISTS idx_user_id user_id TYPE bloom_filter() GRANULARITY 1;
ALTER TABLE traces ON CLUSTER default MATERIALIZE INDEX IF EXISTS idx_user_id;
@@ -0,0 +1 @@
ALTER TABLE traces ON CLUSTER default DROP INDEX IF EXISTS idx_user_id;
@@ -0,0 +1,2 @@
ALTER TABLE traces ADD INDEX IF NOT EXISTS idx_user_id user_id TYPE bloom_filter() GRANULARITY 1;
ALTER TABLE traces MATERIALIZE INDEX IF EXISTS idx_user_id;
+22 -12
View File
@@ -18,23 +18,33 @@ then
exit 1
fi
# Construct the database URL
if [ "$CLICKHOUSE_CLUSTER_ENABLED" == "true" ] ; then
if [ "$CLICKHOUSE_MIGRATION_SSL" = true ] ; then
DATABASE_URL="${CLICKHOUSE_MIGRATION_URL}?username=${CLICKHOUSE_USER}&password=${CLICKHOUSE_PASSWORD}&database=default&x-multi-statement=true&secure=true&skip_verify=true&x-cluster-name=default&x-migrations-table-engine=ReplicatedMergeTree"
else
DATABASE_URL="${CLICKHOUSE_MIGRATION_URL}?username=${CLICKHOUSE_USER}&password=${CLICKHOUSE_PASSWORD}&database=default&x-multi-statement=true&x-cluster-name=default&x-migrations-table-engine=ReplicatedMergeTree"
fi
# Ensure CLICKHOUSE_DB is set
if [ -z "${CLICKHOUSE_DB}" ]; then
export CLICKHOUSE_DB="default"
fi
# Execute the up command
migrate -source file://clickhouse/migrations/clustered -database "$DATABASE_URL" down
else
# Ensure CLICKHOUSE_CLUSTER_NAME is set
if [ -z "${CLICKHOUSE_CLUSTER_NAME}" ]; then
export CLICKHOUSE_CLUSTER_NAME="default"
fi
# Construct the database URL
if [ "$CLICKHOUSE_CLUSTER_ENABLED" == "false" ] ; then
if [ "$CLICKHOUSE_MIGRATION_SSL" = true ] ; then
DATABASE_URL="${CLICKHOUSE_MIGRATION_URL}?username=${CLICKHOUSE_USER}&password=${CLICKHOUSE_PASSWORD}&database=default&x-multi-statement=true&secure=true&skip_verify=true&x-migrations-table-engine=MergeTree"
DATABASE_URL="${CLICKHOUSE_MIGRATION_URL}?username=${CLICKHOUSE_USER}&password=${CLICKHOUSE_PASSWORD}&database=${CLICKHOUSE_DB}&x-multi-statement=true&secure=true&skip_verify=true&x-migrations-table-engine=MergeTree"
else
DATABASE_URL="${CLICKHOUSE_MIGRATION_URL}?username=${CLICKHOUSE_USER}&password=${CLICKHOUSE_PASSWORD}&database=default&x-multi-statement=true&x-migrations-table-engine=MergeTree"
DATABASE_URL="${CLICKHOUSE_MIGRATION_URL}?username=${CLICKHOUSE_USER}&password=${CLICKHOUSE_PASSWORD}&database=${CLICKHOUSE_DB}&x-multi-statement=true&x-migrations-table-engine=MergeTree"
fi
# Execute the up command
migrate -source file://clickhouse/migrations/unclustered -database "$DATABASE_URL" down
else
if [ "$CLICKHOUSE_MIGRATION_SSL" = true ] ; then
DATABASE_URL="${CLICKHOUSE_MIGRATION_URL}?username=${CLICKHOUSE_USER}&password=${CLICKHOUSE_PASSWORD}&database=${CLICKHOUSE_DB}&x-multi-statement=true&secure=true&skip_verify=true&x-cluster-name=${CLICKHOUSE_CLUSTER_NAME}&x-migrations-table-engine=ReplicatedMergeTree"
else
DATABASE_URL="${CLICKHOUSE_MIGRATION_URL}?username=${CLICKHOUSE_USER}&password=${CLICKHOUSE_PASSWORD}&database=${CLICKHOUSE_DB}&x-multi-statement=true&x-cluster-name=${CLICKHOUSE_CLUSTER_NAME}&x-migrations-table-engine=ReplicatedMergeTree"
fi
# Execute the up command
migrate -source file://clickhouse/migrations/clustered -database "$DATABASE_URL" down
fi
+7 -2
View File
@@ -12,11 +12,16 @@ then
exit 1
fi
# Ensure CLICKHOUSE_DB is set
if [ -z "${CLICKHOUSE_DB}" ]; then
export CLICKHOUSE_DB="default"
fi
# Construct the database URL
if [ "$CLICKHOUSE_MIGRATION_SSL" = true ] ; then
DATABASE_URL="${CLICKHOUSE_MIGRATION_URL}?username=${CLICKHOUSE_USER}&password=${CLICKHOUSE_PASSWORD}&database=default&x-multi-statement=true&secure=true&skip_verify=true&x-migrations-table-engine=MergeTree"
DATABASE_URL="${CLICKHOUSE_MIGRATION_URL}?username=${CLICKHOUSE_USER}&password=${CLICKHOUSE_PASSWORD}&database=${CLICKHOUSE_DB}&x-multi-statement=true&secure=true&skip_verify=true&x-migrations-table-engine=MergeTree"
else
DATABASE_URL="${CLICKHOUSE_MIGRATION_URL}?username=${CLICKHOUSE_USER}&password=${CLICKHOUSE_PASSWORD}&database=default&x-multi-statement=true&x-migrations-table-engine=MergeTree"
DATABASE_URL="${CLICKHOUSE_MIGRATION_URL}?username=${CLICKHOUSE_USER}&password=${CLICKHOUSE_PASSWORD}&database=${CLICKHOUSE_DB}&x-multi-statement=true&x-migrations-table-engine=MergeTree"
fi
# Execute the drop command
migrate -source file://clickhouse/migrations -database "$DATABASE_URL" drop
+1 -1
View File
@@ -2,7 +2,7 @@ import {
clickhouseClient,
ObservationRecordReadType,
} from "@langfuse/shared/src/server";
import { Prisma, prisma } from "../../src/db";
import { prisma } from "../../src/db";
import { redis } from "@langfuse/shared/src/server";
import { prepareClickhouse } from "../../scripts/prepareClickhouse";
import { createDatasets } from "../../prisma/seed";
+22 -12
View File
@@ -18,23 +18,33 @@ then
exit 1
fi
# Construct the database URL
if [ "$CLICKHOUSE_CLUSTER_ENABLED" == "true" ] ; then
if [ "$CLICKHOUSE_MIGRATION_SSL" = true ] ; then
DATABASE_URL="${CLICKHOUSE_MIGRATION_URL}?username=${CLICKHOUSE_USER}&password=${CLICKHOUSE_PASSWORD}&database=default&x-multi-statement=true&secure=true&skip_verify=true&x-cluster-name=default&x-migrations-table-engine=ReplicatedMergeTree"
else
DATABASE_URL="${CLICKHOUSE_MIGRATION_URL}?username=${CLICKHOUSE_USER}&password=${CLICKHOUSE_PASSWORD}&database=default&x-multi-statement=true&x-cluster-name=default&x-migrations-table-engine=ReplicatedMergeTree"
fi
# Ensure CLICKHOUSE_DB is set
if [ -z "${CLICKHOUSE_DB}" ]; then
export CLICKHOUSE_DB="default"
fi
# Execute the up command
migrate -source file://clickhouse/migrations/clustered -database "$DATABASE_URL" up
else
# Ensure CLICKHOUSE_CLUSTER_NAME is set
if [ -z "${CLICKHOUSE_CLUSTER_NAME}" ]; then
export CLICKHOUSE_CLUSTER_NAME="default"
fi
# Construct the database URL
if [ "$CLICKHOUSE_CLUSTER_ENABLED" == "false" ] ; then
if [ "$CLICKHOUSE_MIGRATION_SSL" = true ] ; then
DATABASE_URL="${CLICKHOUSE_MIGRATION_URL}?username=${CLICKHOUSE_USER}&password=${CLICKHOUSE_PASSWORD}&database=default&x-multi-statement=true&secure=true&skip_verify=true&x-migrations-table-engine=MergeTree"
DATABASE_URL="${CLICKHOUSE_MIGRATION_URL}?username=${CLICKHOUSE_USER}&password=${CLICKHOUSE_PASSWORD}&database=${CLICKHOUSE_DB}&x-multi-statement=true&secure=true&skip_verify=true&x-migrations-table-engine=MergeTree"
else
DATABASE_URL="${CLICKHOUSE_MIGRATION_URL}?username=${CLICKHOUSE_USER}&password=${CLICKHOUSE_PASSWORD}&database=default&x-multi-statement=true&x-migrations-table-engine=MergeTree"
DATABASE_URL="${CLICKHOUSE_MIGRATION_URL}?username=${CLICKHOUSE_USER}&password=${CLICKHOUSE_PASSWORD}&database=${CLICKHOUSE_DB}&x-multi-statement=true&x-migrations-table-engine=MergeTree"
fi
# Execute the up command
migrate -source file://clickhouse/migrations/unclustered -database "$DATABASE_URL" up
else
if [ "$CLICKHOUSE_MIGRATION_SSL" = true ] ; then
DATABASE_URL="${CLICKHOUSE_MIGRATION_URL}?username=${CLICKHOUSE_USER}&password=${CLICKHOUSE_PASSWORD}&database=${CLICKHOUSE_DB}&x-multi-statement=true&secure=true&skip_verify=true&x-cluster-name=${CLICKHOUSE_CLUSTER_NAME}&x-migrations-table-engine=ReplicatedMergeTree"
else
DATABASE_URL="${CLICKHOUSE_MIGRATION_URL}?username=${CLICKHOUSE_USER}&password=${CLICKHOUSE_PASSWORD}&database=${CLICKHOUSE_DB}&x-multi-statement=true&x-cluster-name=${CLICKHOUSE_CLUSTER_NAME}&x-migrations-table-engine=ReplicatedMergeTree"
fi
# Execute the up command
migrate -source file://clickhouse/migrations/clustered -database "$DATABASE_URL" up
fi
+5 -3
View File
@@ -64,6 +64,7 @@
"@langchain/anthropic": "^0.3.8",
"@langchain/aws": "^0.1.2",
"@langchain/core": "^0.3.18",
"@langchain/google-vertexai": "^0.1.3",
"@langchain/openai": "^0.3.14",
"@opentelemetry/api": ">=1.0.0 <1.10.0",
"@prisma/client": "^5.22.0",
@@ -72,7 +73,7 @@
"@types/bcryptjs": "^2.4.6",
"axios": "^1.7.7",
"bcryptjs": "^2.4.3",
"bullmq": "^5.12.10",
"bullmq": "^5.34.10",
"dd-trace": "^5.23.1",
"decimal.js": "^10.4.3",
"exponential-backoff": "^3.1.1",
@@ -81,7 +82,7 @@
"langchain": "^0.3.6",
"langfuse-langchain": "3.30.3",
"lodash": "^4.17.21",
"next-auth": "^4.24.7",
"next-auth": "^4.24.11",
"nodemailer": "^6.9.15",
"prisma-extension-kysely": "^2.1.0",
"uuid": "^9.0.1",
@@ -120,7 +121,8 @@
},
"pnpm": {
"overrides": {
"jsonpath-plus": "10.0.7"
"jsonpath-plus": "10.0.7",
"nanoid": "^3.3.8"
}
}
}
+21
View File
@@ -170,6 +170,17 @@ export type BatchExport = {
url: string | null;
log: string | null;
};
export type BillingMeterBackup = {
stripe_customer_id: string;
meter_id: string;
start_time: Timestamp;
end_time: Timestamp;
aggregated_value: number;
event_name: string;
org_id: string;
created_at: Generated<Timestamp>;
updated_at: Generated<Timestamp>;
};
export type Comment = {
id: string;
project_id: string;
@@ -453,6 +464,7 @@ export type Project = {
org_id: string;
created_at: Generated<Timestamp>;
updated_at: Generated<Timestamp>;
deleted_at: Timestamp | null;
name: string;
};
export type ProjectMembership = {
@@ -478,6 +490,13 @@ export type Prompt = {
tags: Generated<string[]>;
labels: Generated<string[]>;
};
export type QueueBackUp = {
id: string;
project_id: string | null;
queue_name: string;
content: unknown;
created_at: Generated<Timestamp>;
};
export type Score = {
id: string;
timestamp: Generated<Timestamp>;
@@ -603,6 +622,7 @@ export type DB = {
audit_logs: AuditLog;
background_migrations: BackgroundMigration;
batch_exports: BatchExport;
billing_meter_backups: BillingMeterBackup;
comments: Comment;
cron_jobs: CronJobs;
dataset_items: DatasetItem;
@@ -627,6 +647,7 @@ export type DB = {
project_memberships: ProjectMembership;
projects: Project;
prompts: Prompt;
queue_backups: QueueBackUp;
score_configs: ScoreConfig;
scores: Score;
Session: Session;
@@ -0,0 +1,2 @@
-- AlterTable
ALTER TABLE "projects" ADD COLUMN "deleted_at" TIMESTAMP(3);
@@ -0,0 +1,5 @@
-- DropForeignKey
ALTER TABLE "job_executions" DROP CONSTRAINT "job_executions_job_output_score_id_fkey";
-- DropForeignKey
ALTER TABLE "traces" DROP CONSTRAINT "traces_session_id_project_id_fkey";
@@ -0,0 +1,13 @@
-- CreateTable
CREATE TABLE "queue_backups" (
"id" TEXT NOT NULL,
"project_id" TEXT,
"queue_name" TEXT NOT NULL,
"content" JSONB NOT NULL,
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "queue_backups_pkey" PRIMARY KEY ("id")
);
-- AddForeignKey
ALTER TABLE "traces" ADD CONSTRAINT "traces_session_id_project_id_fkey" FOREIGN KEY ("session_id", "project_id") REFERENCES "trace_sessions"("id", "project_id") ON DELETE RESTRICT ON UPDATE CASCADE;
@@ -0,0 +1,2 @@
-- DropForeignKey
ALTER TABLE "traces" DROP CONSTRAINT "traces_session_id_project_id_fkey";
@@ -0,0 +1,21 @@
-- CreateTable
CREATE TABLE "billing_meter_backups" (
"stripe_customer_id" TEXT NOT NULL,
"meter_id" TEXT NOT NULL,
"start_time" TIMESTAMP(3) NOT NULL,
"end_time" TIMESTAMP(3) NOT NULL,
"aggregated_value" INTEGER NOT NULL,
"event_name" TEXT NOT NULL,
"org_id" TEXT NOT NULL,
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updated_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP
);
-- CreateIndex
CREATE INDEX "billing_meter_backups_stripe_customer_id_meter_id_start_tim_idx" ON "billing_meter_backups"("stripe_customer_id", "meter_id", "start_time", "end_time");
-- CreateIndex
CREATE UNIQUE INDEX "billing_meter_backups_stripe_customer_id_meter_id_start_tim_key" ON "billing_meter_backups"("stripe_customer_id", "meter_id", "start_time", "end_time");
+62 -31
View File
@@ -113,6 +113,7 @@ model Project {
orgId String @map("org_id")
createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime @default(now()) @updatedAt @map("updated_at")
deletedAt DateTime? @map("deleted_at")
name String
projectMembers ProjectMembership[]
organization Organization @relation(fields: [orgId], references: [id], onUpdate: Cascade, onDelete: Cascade)
@@ -271,7 +272,6 @@ model TraceSession {
project Project @relation(fields: [projectId], references: [id], onDelete: Cascade)
bookmarked Boolean @default(false)
public Boolean @default(false)
traces Trace[]
@@id([id, projectId])
@@index([projectId])
@@ -283,25 +283,24 @@ model TraceSession {
// Update TraceView below when making changes to this model!
model Trace {
id String @id @default(cuid())
externalId String? @map("external_id")
timestamp DateTime @default(now())
id String @id @default(cuid())
externalId String? @map("external_id")
timestamp DateTime @default(now())
name String?
userId String? @map("user_id")
userId String? @map("user_id")
metadata Json?
release String?
version String?
projectId String @map("project_id")
project Project @relation(fields: [projectId], references: [id], onDelete: Cascade)
public Boolean @default(false)
bookmarked Boolean @default(false)
tags String[] @default([])
projectId String @map("project_id")
project Project @relation(fields: [projectId], references: [id], onDelete: Cascade)
public Boolean @default(false)
bookmarked Boolean @default(false)
tags String[] @default([])
input Json?
output Json?
sessionId String? @map("session_id")
session TraceSession? @relation(fields: [sessionId, projectId], references: [id, projectId])
createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime @default(now()) @updatedAt @map("updated_at")
sessionId String? @map("session_id")
createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime @default(now()) @updatedAt @map("updated_at")
@@index([projectId, timestamp])
@@index([sessionId])
@@ -468,25 +467,24 @@ enum ObservationLevel {
}
model Score {
id String @id @default(cuid())
timestamp DateTime @default(now())
projectId String @map("project_id")
project Project @relation(fields: [projectId], references: [id], onDelete: Cascade)
id String @id @default(cuid())
timestamp DateTime @default(now())
projectId String @map("project_id")
project Project @relation(fields: [projectId], references: [id], onDelete: Cascade)
name String
value Float? // always defined if data type is NUMERIC or BOOLEAN, optional for CATEGORICAL
source ScoreSource
authorUserId String? @map("author_user_id")
authorUserId String? @map("author_user_id")
comment String?
traceId String @map("trace_id")
observationId String? @map("observation_id")
configId String? @map("config_id")
stringValue String? @map("string_value") // always defined if data type is CATEGORICAL or BOOLEAN, null for NUMERIC
queueId String? @map("queue_id")
createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime @default(now()) @updatedAt @map("updated_at")
dataType ScoreDataType @default(NUMERIC) @map("data_type")
JobExecution JobExecution[]
scoreConfig ScoreConfig? @relation(fields: [configId], references: [id], onDelete: SetNull)
traceId String @map("trace_id")
observationId String? @map("observation_id")
configId String? @map("config_id")
stringValue String? @map("string_value") // always defined if data type is CATEGORICAL or BOOLEAN, null for NUMERIC
queueId String? @map("queue_id")
createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime @default(now()) @updatedAt @map("updated_at")
dataType ScoreDataType @default(NUMERIC) @map("data_type")
scoreConfig ScoreConfig? @relation(fields: [configId], references: [id], onDelete: SetNull)
@@unique([id, projectId]) // used for upserts via prisma
@@index(timestamp)
@@ -900,10 +898,9 @@ model JobExecution {
jobInputObservationId String? @map("job_input_observation_id") // no fk constraint - observations in ClickHouse, deletion handled via project cascade
jobInputDatasetItemId String? @map("job_input_dataset_item_id") // no fk constraint - job execution sensible standalone
jobInputDatasetItemId String? @map("job_input_dataset_item_id") // no fk constraint - job execution sensible standalone
jobOutputScoreId String? @map("job_output_score_id")
score Score? @relation(fields: [jobOutputScoreId], references: [id], onDelete: SetNull) // job remains when scores are deleted
@@index([projectId, status])
@@index([projectId, id])
@@ -1018,3 +1015,37 @@ model ObservationMedia {
@@index([projectId, observationId])
@@map("observation_media")
}
model QueueBackUp {
id String @id @default(cuid())
projectId String? @map("project_id")
queueName String @map("queue_name")
content Json
createdAt DateTime @default(now()) @map("created_at")
@@map("queue_backups")
}
model BillingMeterBackup {
// unique
stripeCustomerId String @map("stripe_customer_id")
meterId String @map("meter_id")
startTime DateTime @map("start_time")
endTime DateTime @map("end_time")
// value
aggregatedValue Int @map("aggregated_value")
// labels
eventName String @map("event_name")
orgId String @map("org_id")
// ts
createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime @default(now()) @updatedAt @map("updated_at")
@@unique([stripeCustomerId, meterId, startTime, endTime])
@@index([stripeCustomerId, meterId, startTime, endTime])
@@map("billing_meter_backups")
}
+6
View File
@@ -450,6 +450,9 @@ export async function createDatasets(
Math.random() > 0.3
? observations[Math.floor(Math.random() * observations.length)]
: undefined;
if (!sourceObservation) {
continue;
}
const datasetItem = await prisma.datasetItem.create({
data: {
projectId,
@@ -510,6 +513,9 @@ export async function createDatasets(
Math.floor(Math.random() * relevantObservations.length)
];
if (!observation) {
continue;
}
await prisma.datasetRunItems.create({
data: {
projectId,
+31 -15
View File
@@ -96,21 +96,37 @@ export const prepareClickhouse = async (
'version' AS version,
repeat('input', toInt64(randExponential(1 / 100))) AS input,
repeat('output', toInt64(randExponential(1 / 100))) AS output,
case
when number % 2 = 0 then 'claude-3-haiku-20240307'
else 'gpt-4'
end as provided_model_name,
case
when number % 2 = 0 then 'cltr0w45b000008k1407o9qv1'
else 'clrntkjgy000f08jx79v9g1xj'
end as internal_model_id,
'{"temperature": 0.7, "max_tokens": 150}' AS model_parameters,
map('input', toUInt64(randUniform(0, 1000)), 'output', toUInt64(randUniform(0, 1000)), 'total', toUInt64(randUniform(0, 2000))) AS provided_usage_details,
map('input', toUInt64(randUniform(0, 1000)), 'output', toUInt64(randUniform(0, 1000)), 'total', toUInt64(randUniform(0, 2000))) AS usage_details,
map('input', toDecimal64(randUniform(0, 1000), 12), 'output', toDecimal64(randUniform(0, 1000), 12), 'total', toDecimal64(randUniform(0, 2000), 12)) AS provided_cost_details,
map('input', toDecimal64(randUniform(0, 1000), 12), 'output', toDecimal64(randUniform(0, 1000), 12), 'total', toDecimal64(randUniform(0, 2000), 12)) AS cost_details,
toDecimal64(randUniform(0, 2000), 12) AS total_cost,
start_time AS completion_start_time,
if("type" = 'GENERATION',
case
when number % 2 = 0 then 'claude-3-haiku-20240307'
else 'gpt-4'
end,
NULL) as provided_model_name,
if("type" = 'GENERATION',
case
when number % 2 = 0 then 'cltr0w45b000008k1407o9qv1'
else 'clrntkjgy000f08jx79v9g1xj'
end,
NULL) as internal_model_id,
if("type" = 'GENERATION',
'{"temperature": 0.7, "max_tokens": 150}',
'{}') AS model_parameters,
if("type" = 'GENERATION',
map('input', toUInt64(randUniform(0, 1000)), 'output', toUInt64(randUniform(0, 1000)), 'total', toUInt64(randUniform(0, 2000))),
map()) AS provided_usage_details,
if("type" = 'GENERATION',
map('input', toUInt64(randUniform(0, 1000)), 'output', toUInt64(randUniform(0, 1000)), 'total', toUInt64(randUniform(0, 2000))),
map()) AS usage_details,
if("type" = 'GENERATION',
map('input', toDecimal64(randUniform(0, 1000), 12), 'output', toDecimal64(randUniform(0, 1000), 12), 'total', toDecimal64(randUniform(0, 2000), 12)),
map()) AS provided_cost_details,
if("type" = 'GENERATION',
map('input', toDecimal64(randUniform(0, 1000), 12), 'output', toDecimal64(randUniform(0, 1000), 12), 'total', toDecimal64(randUniform(0, 2000), 12)),
map()) AS cost_details,
if("type" = 'GENERATION',
toDecimal64(randUniform(0, 2000), 12),
NULL) AS total_cost,
addMilliseconds(start_time, if(rand() < 0.6, floor(randUniform(0, 500)), floor(randUniform(0, 600)))) AS completion_start_time,
array(${SEED_PROMPTS.map((p) => `concat('${p.id}',project_id)`).join(
",",
)})[(number % ${SEED_PROMPTS.length})+1] AS prompt_id,
+21 -12
View File
@@ -5,6 +5,7 @@ const EnvSchema = z.object({
NODE_ENV: z
.enum(["development", "test", "production"])
.default("development"),
NEXTAUTH_URL: z.string().url().optional(),
REDIS_HOST: z.string().nullish(),
REDIS_PORT: z.coerce
.number({
@@ -27,15 +28,12 @@ const EnvSchema = z.object({
.optional(),
LANGFUSE_CACHE_PROMPT_ENABLED: z.enum(["true", "false"]).default("false"),
LANGFUSE_CACHE_PROMPT_TTL_SECONDS: z.coerce.number().default(60 * 60),
CLICKHOUSE_URL: z.string().url().optional(),
CLICKHOUSE_USER: z.string().optional(),
CLICKHOUSE_PASSWORD: z.string().optional(),
LANGFUSE_ASYNC_CLICKHOUSE_INGESTION_PROCESSING: z
.enum(["true", "false"])
.default("false"),
LANGFUSE_ASYNC_INGESTION_PROCESSING: z
.enum(["true", "false"])
.default("false"),
CLICKHOUSE_URL: z.string().url(),
CLICKHOUSE_CLUSTER_NAME: z.string().default("default"),
CLICKHOUSE_DB: z.string().default("default"),
CLICKHOUSE_USER: z.string(),
CLICKHOUSE_PASSWORD: z.string(),
LANGFUSE_INGESTION_QUEUE_DELAY_MS: z.coerce
.number()
.nonnegative()
@@ -48,8 +46,9 @@ const EnvSchema = z.object({
ENABLE_AWS_CLOUDWATCH_METRIC_PUBLISHING: z
.enum(["true", "false"])
.default("false"),
LANGFUSE_S3_EVENT_UPLOAD_ENABLED: z.enum(["true", "false"]).default("false"),
LANGFUSE_S3_EVENT_UPLOAD_BUCKET: z.string().optional(),
LANGFUSE_S3_EVENT_UPLOAD_BUCKET: z.string({
required_error: "Langfuse requires a bucket name for S3 Event Uploads.",
}),
LANGFUSE_S3_EVENT_UPLOAD_PREFIX: z.string().default(""),
LANGFUSE_S3_EVENT_UPLOAD_REGION: z.string().optional(),
LANGFUSE_S3_EVENT_UPLOAD_ENDPOINT: z.string().optional(),
@@ -60,6 +59,16 @@ const EnvSchema = z.object({
.default("false"),
LANGFUSE_USE_AZURE_BLOB: z.enum(["true", "false"]).default("false"),
STRIPE_SECRET_KEY: z.string().optional(),
LANGFUSE_S3_CORE_DATA_EXPORT_IS_ENABLED: z
.enum(["true", "false"])
.default("false"),
LANGFUSE_POSTGRES_METERING_DATA_EXPORT_IS_ENABLED: z
.enum(["true", "false"])
.default("false"),
});
export const env = EnvSchema.parse(removeEmptyEnvVariables(process.env));
export const env: z.infer<typeof EnvSchema> =
process.env.DOCKER_BUILD === "1"
? (process.env as any)
: EnvSchema.parse(removeEmptyEnvVariables(process.env));
+2 -2
View File
@@ -1,7 +1,7 @@
import { BaseError } from "./BaseError";
export class ApiError extends BaseError {
constructor(description = "Api call failed") {
super("ApiError", 500, description, true);
constructor(description = "Api call failed", status = 500) {
super("ApiError", status, description, true);
}
}
@@ -1,5 +1,7 @@
import { z } from "zod";
const MAX_COMMENT_LENGTH = 3000;
const COMMENT_OBJECT_TYPES = [
"TRACE",
"OBSERVATION",
@@ -9,7 +11,7 @@ const COMMENT_OBJECT_TYPES = [
export const CreateCommentData = z.object({
projectId: z.string(),
content: z.string().trim().min(1).max(500),
content: z.string().trim().min(1).max(MAX_COMMENT_LENGTH),
objectId: z.string(),
objectType: z.enum(COMMENT_OBJECT_TYPES),
});
@@ -3,15 +3,17 @@ export const planLabels = {
"cloud:hobby": "Hobby",
"cloud:pro": "Pro",
"cloud:team": "Team",
"self-hosted:enterprise": "Enterprise",
"self-hosted:pro": "Pro (self-hosted)",
"self-hosted:enterprise": "Enterprise (self-hosted)",
} as const;
export type Plan = keyof typeof planLabels;
export const plans = Object.keys(planLabels) as Plan[];
// This function is kept here to ensure consistency when updating plan names in the future.
// These functions are kept here to ensure consistency when updating plan names in the future.
export const isCloudPlan = (plan: Plan) => plan.startsWith("cloud");
export const isSelfHostedPlan = (plan: Plan) => plan.startsWith("self-hosted");
export const isPlan = (value: string): value is Plan =>
plans.includes(value as Plan);
@@ -0,0 +1,15 @@
import { Prisma } from "../../db";
export const datasetItemMatchesVariable = (
input: Prisma.JsonValue,
variable: string,
) => {
if (
input === null ||
input === undefined ||
typeof input !== "object" ||
Array.isArray(input)
)
return false;
return Object.keys(input).includes(variable);
};
@@ -6,7 +6,7 @@ import { isPresent } from "../../utils/typeChecks";
import {
jsonSchema,
paginationMetaResponseZod,
paginationZod,
publicApiPaginationZod,
} from "../../utils/zod";
/**
@@ -17,7 +17,7 @@ export type ValidatedScoreConfig = z.infer<typeof ValidatedScoreConfigSchema>;
const validateCategories = (
categories: ConfigCategory[],
ctx: z.RefinementCtx
ctx: z.RefinementCtx,
) => {
const uniqueNames = new Set<string>();
const uniqueValues = new Set<number>();
@@ -94,7 +94,7 @@ const BooleanScoreConfig = z.object({
return categories.every(
(category, index) =>
category.label === expectedCategories[index].label &&
category.value === expectedCategories[index].value
category.value === expectedCategories[index].value,
);
}),
});
@@ -123,7 +123,7 @@ const ValidatedScoreConfigSchema = z
minValue: z.undefined().nullish(),
dataType: z.literal("CATEGORICAL"),
categories: Categories.superRefine(validateCategories),
})
}),
),
ScoreConfigBase.merge(BooleanScoreConfig),
])
@@ -150,7 +150,7 @@ const ValidatedScoreConfigSchema = z
*/
export const filterAndValidateDbScoreConfigList = (
scoreConfigs: ScoreConfigDbType[],
onParseError?: (error: z.ZodError) => void
onParseError?: (error: z.ZodError) => void,
): ValidatedScoreConfig[] =>
scoreConfigs.reduce((acc, ts) => {
const result = ValidatedScoreConfigSchema.safeParse(ts);
@@ -170,7 +170,7 @@ export const filterAndValidateDbScoreConfigList = (
* @throws error if score fails validation
*/
export const validateDbScoreConfig = (
scoreConfig: ScoreConfigDbType
scoreConfig: ScoreConfigDbType,
): ValidatedScoreConfig => ValidatedScoreConfigSchema.parse(scoreConfig);
/**
@@ -205,7 +205,7 @@ export const PostScoreConfigBody = z
z.object({
dataType: z.literal("BOOLEAN"),
categories: z.undefined().nullish(),
})
}),
),
])
.superRefine((data, ctx) => {
@@ -227,7 +227,7 @@ export const PostScoreConfigResponse = ValidatedScoreConfigSchema;
// GET /score-configs
export const GetScoreConfigsQuery = z.object({
...paginationZod,
...publicApiPaginationZod,
});
export const GetScoreConfigsResponse = z.object({
@@ -6,7 +6,7 @@ import { isPresent, stringDateTime } from "../../utils/typeChecks";
import {
NonEmptyString,
paginationMetaResponseZod,
paginationZod,
publicApiPaginationZod,
} from "../../utils/zod";
import { Category as ConfigCategory } from "./scoreConfigTypes";
@@ -84,13 +84,13 @@ export const ScoreBodyWithoutConfig = z.discriminatedUnion("dataType", [
z.object({
value: z.number(),
dataType: z.literal("NUMERIC"),
})
}),
),
BaseScoreBody.merge(
z.object({
value: z.string(),
dataType: z.literal("CATEGORICAL"),
})
}),
),
BaseScoreBody.merge(
z.object({
@@ -98,7 +98,7 @@ export const ScoreBodyWithoutConfig = z.discriminatedUnion("dataType", [
message: "Value must be either 0 or 1",
}),
dataType: z.literal("BOOLEAN"),
})
}),
),
]);
@@ -162,7 +162,7 @@ export const ScorePropsAgainstConfig = z.union([
*/
export const filterAndValidateDbScoreList = (
scores: Score[],
onParseError?: (error: z.ZodError) => void
onParseError?: (error: z.ZodError) => void,
): APIScore[] =>
scores.reduce((acc, ts) => {
const result = APIScoreSchema.safeParse(ts);
@@ -199,14 +199,14 @@ export const PostScoresBody = z.discriminatedUnion("dataType", [
value: z.number(),
dataType: z.literal("NUMERIC"),
configId: z.string().nullish(),
})
}),
),
BaseScoreBody.merge(
z.object({
value: z.string(),
dataType: z.literal("CATEGORICAL"),
configId: z.string().nullish(),
})
}),
),
BaseScoreBody.merge(
z.object({
@@ -216,14 +216,14 @@ export const PostScoresBody = z.discriminatedUnion("dataType", [
}),
dataType: z.literal("BOOLEAN"),
configId: z.string().nullish(),
})
}),
),
BaseScoreBody.merge(
z.object({
value: z.union([z.string(), z.number()]),
dataType: z.undefined(),
configId: z.string().nullish(),
})
}),
),
]);
@@ -231,7 +231,7 @@ export const PostScoresResponse = z.object({ id: z.string() });
// GET /scores
export const GetScoresQuery = z.object({
...paginationZod,
...publicApiPaginationZod,
userId: z.string().nullish(),
dataType: z.enum(ScoreDataType).nullish(),
configId: z.string().nullish(),
@@ -260,7 +260,7 @@ const LegacyGetScoreResponseDataV1 = z.intersection(
userId: z.string().nullish(),
tags: z.array(z.string()).nullish(),
}),
})
}),
);
export const GetScoresResponse = z.object({
data: z.array(LegacyGetScoreResponseDataV1),
@@ -269,7 +269,7 @@ export const GetScoresResponse = z.object({
export const legacyFilterAndValidateV1GetScoreList = (
scores: unknown[],
onParseError?: (error: z.ZodError) => void
onParseError?: (error: z.ZodError) => void,
): z.infer<typeof LegacyGetScoreResponseDataV1>[] =>
scores.reduce(
(acc: z.infer<typeof LegacyGetScoreResponseDataV1>[], ts) => {
@@ -282,7 +282,7 @@ export const legacyFilterAndValidateV1GetScoreList = (
}
return acc;
},
[] as z.infer<typeof LegacyGetScoreResponseDataV1>[]
[] as z.infer<typeof LegacyGetScoreResponseDataV1>[],
);
// GET /scores/{scoreId}
+3 -2
View File
@@ -7,7 +7,6 @@ export * from "./interfaces/customLLMProviderConfigSchemas";
export * from "./tableDefinitions";
export * from "./types";
export * from "./tableDefinitions/tracesTable";
export * from "./server/auth/apiKeys";
export * from "./observationsTable";
export * from "./utils/zod";
export * from "./utils/json";
@@ -16,7 +15,6 @@ export * from "./utils/objects";
export * from "./utils/typeChecks";
export * from "./features/entitlements/plans";
export * from "./interfaces/rate-limits";
export { env } from "./env";
// llm api
export * from "./server/llm/types";
@@ -34,6 +32,9 @@ export * from "./features/scores";
// comments
export * from "./features/comments/types";
// experiments
export * from "./features/experiments/utils";
// export db types only
export * from "@prisma/client";
export { type DB } from "../prisma/generated/types";
@@ -10,3 +10,19 @@ export const BedrockCredentialSchema = z
})
.optional();
export type BedrockCredential = z.infer<typeof BedrockCredentialSchema>;
export const GCPServiceAccountKeySchema = z.object({
type: z.literal("service_account"),
project_id: z.string(),
private_key_id: z.string(),
private_key: z.string(),
client_email: z.string(),
client_id: z.string(),
auth_uri: z.string(),
token_uri: z.string(),
auth_provider_x509_cert_url: z.string(),
client_x509_cert_url: z.string(),
});
export type GCPServiceAccountKey = z.infer<typeof GCPServiceAccountKeySchema>;
export default GCPServiceAccountKeySchema;
+24 -1
View File
@@ -20,6 +20,13 @@ export const observationsTableCols: ColumnDefinition[] = [
options: [], // to be added at runtime
nullable: true,
},
{
name: "type",
id: "type",
type: "stringOptions",
options: [],
internal: 'o."type"',
},
{ name: "Trace ID", id: "traceId", type: "string", internal: 't."id"' },
{
name: "Trace Name",
@@ -112,6 +119,14 @@ export const observationsTableCols: ColumnDefinition[] = [
options: [], // to be added at runtime
nullable: true,
},
{
name: "Model ID",
id: "modelId",
type: "stringOptions",
internal: 'o."internal_model_id"',
options: [], // to be added at runtime
nullable: true,
},
{
name: "Input Tokens",
id: "inputTokens",
@@ -186,20 +201,25 @@ export const observationsTableCols: ColumnDefinition[] = [
// allows for undefined options, to offer filters while options are still loading
export type ObservationOptions = {
model: Array<OptionsDefinition>;
modelId: Array<OptionsDefinition>;
name: Array<OptionsDefinition>;
traceName: Array<OptionsDefinition>;
scores_avg: Array<string>;
promptName: Array<OptionsDefinition>;
tags: Array<OptionsDefinition>;
type: Array<OptionsDefinition>;
};
export function observationsTableColsWithOptions(
options?: ObservationOptions
options?: ObservationOptions,
): ColumnDefinition[] {
return observationsTableCols.map((col) => {
if (col.id === "model") {
return { ...col, options: options?.model ?? [] };
}
if (col.id === "modelId") {
return { ...col, options: options?.modelId ?? [] };
}
if (col.id === "name") {
return { ...col, options: options?.name ?? [] };
}
@@ -215,6 +235,9 @@ export function observationsTableColsWithOptions(
if (col.id === "tags") {
return { ...col, options: options?.tags ?? [] };
}
if (col.id === "type") {
return { ...col, options: options?.type ?? [] };
}
return col;
});
}
@@ -0,0 +1,65 @@
import type { OAuthConfig, OAuthUserConfig } from "next-auth/providers/oauth";
import type { GithubProfile, GithubEmail } from "next-auth/providers/github";
export function GitHubEnterpriseProvider<P extends GithubProfile>(
options: OAuthUserConfig<P> & {
enterprise?: {
baseUrl?: string;
};
},
): OAuthConfig<P> {
const baseUrl = options?.enterprise?.baseUrl ?? "https://github.com";
const apiBaseUrl = options?.enterprise?.baseUrl
? `${options?.enterprise?.baseUrl}/api/v3`
: "https://api.github.com";
return {
id: "github-enterprise",
name: "GitHub Enterprise",
type: "oauth",
authorization: {
url: `${baseUrl}/login/oauth/authorize`,
params: { scope: "read:user user:email" },
},
token: `${baseUrl}/login/oauth/access_token`,
userinfo: {
url: `${apiBaseUrl}/user`,
async request({ client, tokens }) {
const profile = await client.userinfo(tokens.access_token!);
if (!profile.email) {
// If the user does not have a public email, get another via the GitHub API
// See https://docs.github.com/en/rest/users/emails#list-email-addresses-for-the-authenticated-user
const res = await fetch(`${apiBaseUrl}/user/emails`, {
headers: { Authorization: `token ${tokens.access_token}` },
});
if (res.ok) {
const emails = (await res.json()) as GithubEmail[];
profile.email = (emails.find((e) => e.primary) ?? emails[0]).email;
}
}
return profile;
},
},
profile(profile) {
return {
id: profile.id.toString(),
name: profile.name ?? profile.login,
email: profile.email,
image: profile.avatar_url,
};
},
style: {
logo: "https://raw.githubusercontent.com/nextauthjs/next-auth/main/packages/next-auth/provider-logos/github.svg",
logoDark:
"https://raw.githubusercontent.com/nextauthjs/next-auth/main/packages/next-auth/provider-logos/github-dark.svg",
bg: "#fff",
bgDark: "#000",
text: "#000",
textDark: "#fff",
},
options,
};
}
@@ -10,7 +10,7 @@ export const clickhouseClient = (opts?: NodeClickHouseClientConfigOptions) =>
url: env.CLICKHOUSE_URL,
username: env.CLICKHOUSE_USER,
password: env.CLICKHOUSE_PASSWORD,
database: "default",
database: env.CLICKHOUSE_DB,
clickhouse_settings: {
async_insert: 1,
wait_for_async_insert: 1, // if disabled, we won't get errors from clickhouse
+12 -4
View File
@@ -6,6 +6,7 @@ export * from "./services/PromptService";
export * from "./services/traces-ui-table-service";
export * from "./auth/apiKeys";
export * from "./auth/customSsoProvider";
export * from "./auth/gitHubEnterpriseProvider";
export * from "./llm/fetchLLMCompletion";
export * from "./llm/types";
export * from "./utils/DatabaseReadStream";
@@ -21,17 +22,20 @@ export * from "../server/ingestion/types";
export * from "../server/ingestion/validateAndInflateScore";
export * from "./redis/redis";
export * from "./redis/traceUpsert";
export * from "./redis/CloudUsageMeteringQueue";
export * from "./redis/cloudUsageMeteringQueue";
export * from "./redis/getQueue";
export * from "./redis/traceDelete";
export * from "./redis/projectDelete";
export * from "./redis/datasetRunItemUpsert";
export * from "./redis/batchExport";
export * from "./redis/legacyIngestion";
export * from "./redis/ingestionQueue";
export * from "./redis/postHogIntegrationQueue";
export * from "./redis/postHogIntegrationProcessingQueue";
export * from "./redis/coreDataS3ExportQueue";
export * from "./redis/meteringDataPostgresExportQueue";
export * from "./redis/experimentCreateQueue";
export * from "./auth/types";
export * from "./ingestion/legacy/index";
export * from "./queues";
export * from "./ingestion/legacy/EventProcessor";
export * from "./orderByToPrisma";
export * from "./filterToPrisma";
export * from "./instrumentation";
@@ -39,3 +43,7 @@ export * from "./logger";
export * from "./queries";
export * from "./repositories";
export * from "./redis/evalExecutionQueue";
export * from "./services/sessions-ui-table-service";
// test utils
export * from "./test-utils";
@@ -1,786 +0,0 @@
import { v4 } from "uuid";
import { type z } from "zod";
import Decimal from "decimal.js";
import { findModel } from "../modelMatch";
import {
ObservationEvent,
eventTypes,
legacyObservationCreateEvent,
generationCreateEvent,
traceEvent,
scoreEvent,
sdkLogEvent,
ingestionEvent,
} from "../types";
import { validateAndInflateScore } from "../validateAndInflateScore";
import { Trace, Observation, Score, Prisma, Model } from "@prisma/client";
import { ForbiddenError, LangfuseNotFoundError } from "../../../errors";
import { mergeJson } from "../../../utils/json";
import { jsonSchema } from "../../../utils/zod";
import { prisma } from "../../../db";
import { LegacyIngestionAccessScope } from ".";
import { logger } from "../../logger";
import { env } from "../../../env";
import { upsertTrace } from "../../repositories";
import { convertDateToClickhouseDateTime } from "../../clickhouse/client";
export interface EventProcessor {
auth(apiScope: LegacyIngestionAccessScope): void;
process(
apiScope: LegacyIngestionAccessScope,
): Promise<Trace | Observation | Score> | undefined;
}
export const getProcessorForEvent = (
event: z.infer<typeof ingestionEvent>,
calculateTokenDelegate: (p: {
model: Model;
text: unknown;
}) => number | undefined,
): EventProcessor => {
switch (event.type) {
case eventTypes.TRACE_CREATE:
return new TraceProcessor(event);
case eventTypes.OBSERVATION_CREATE:
case eventTypes.OBSERVATION_UPDATE:
case eventTypes.EVENT_CREATE:
case eventTypes.SPAN_CREATE:
case eventTypes.SPAN_UPDATE:
case eventTypes.GENERATION_CREATE:
case eventTypes.GENERATION_UPDATE:
return new ObservationProcessor(event, calculateTokenDelegate);
case eventTypes.SCORE_CREATE: {
return new ScoreProcessor(event);
}
case eventTypes.SDK_LOG:
return new SdkLogProcessor(event);
}
};
export class ObservationProcessor implements EventProcessor {
event: ObservationEvent;
calculateTokenDelegate: (p: {
model: Model;
text: unknown;
}) => number | undefined;
constructor(
event: ObservationEvent,
calculateTokenDelegate: (p: {
model: Model;
text: unknown;
}) => number | undefined,
) {
this.event = event;
this.calculateTokenDelegate = calculateTokenDelegate;
}
async convertToObservation(
apiScope: LegacyIngestionAccessScope,
existingObservation: Omit<Observation, "input" | "output"> | null,
): Promise<{
id: string;
create: Prisma.ObservationUncheckedCreateInput;
update: Prisma.ObservationUncheckedUpdateInput;
}> {
let type: "EVENT" | "SPAN" | "GENERATION";
switch (this.event.type) {
case eventTypes.OBSERVATION_CREATE:
case eventTypes.OBSERVATION_UPDATE:
type = this.event.body.type;
break;
case eventTypes.EVENT_CREATE:
type = "EVENT" as const;
break;
case eventTypes.SPAN_CREATE:
case eventTypes.SPAN_UPDATE:
type = "SPAN" as const;
break;
case eventTypes.GENERATION_CREATE:
case eventTypes.GENERATION_UPDATE:
type = "GENERATION" as const;
break;
}
if (
this.event.type === eventTypes.OBSERVATION_UPDATE &&
!existingObservation
) {
throw new LangfuseNotFoundError(
`Observation with id ${this.event.id} not found`,
);
}
// find matching model definition based on event and existing observation in db
const internalModel: Model | undefined | null =
type === "GENERATION"
? await findModel({
event: {
projectId: apiScope.projectId,
model:
"model" in this.event.body
? (this.event.body.model ?? undefined)
: undefined,
unit:
"usage" in this.event.body
? (this.event.body.usage?.unit ?? undefined)
: undefined,
startTime: this.event.body.startTime
? new Date(this.event.body.startTime)
: undefined,
},
existingDbObservation: existingObservation ?? undefined,
})
: undefined;
// Token counts
const [newInputCount, newOutputCount] =
"usage" in this.event.body
? await this.calculateTokenCounts(
apiScope.projectId,
this.event.body,
this.calculateTokenDelegate,
internalModel ?? undefined,
existingObservation ?? undefined,
)
: [undefined, undefined];
const newTotalCount =
"usage" in this.event.body
? (this.event.body.usage?.total ??
(newInputCount != null || newOutputCount != null
? (newInputCount ?? 0) + (newOutputCount ?? 0)
: undefined))
: undefined;
const userProvidedTokenCosts = {
inputCost:
"usage" in this.event.body && this.event.body.usage?.inputCost != null // inputCost can be explicitly 0. Note only one equal sign to capture null AND undefined
? new Decimal(this.event.body.usage?.inputCost)
: existingObservation?.inputCost,
outputCost:
"usage" in this.event.body && this.event.body.usage?.outputCost != null // outputCost can be explicitly 0. Note only one equal sign to capture null AND undefined
? new Decimal(this.event.body.usage?.outputCost)
: existingObservation?.outputCost,
totalCost:
"usage" in this.event.body && this.event.body.usage?.totalCost != null // totalCost can be explicitly 0. Note only one equal sign to capture null AND undefined
? new Decimal(this.event.body.usage?.totalCost)
: existingObservation?.totalCost,
};
const tokenCounts = {
input: newInputCount ?? existingObservation?.promptTokens,
output: newOutputCount ?? existingObservation?.completionTokens,
total: newTotalCount || existingObservation?.totalTokens,
};
const calculatedCosts = ObservationProcessor.calculateTokenCosts(
internalModel,
userProvidedTokenCosts,
tokenCounts,
);
// merge metadata from existingObservation.metadata and metadata
const mergedMetadata = mergeJson(
existingObservation?.metadata
? jsonSchema.parse(existingObservation.metadata)
: undefined,
this.event.body.metadata ?? undefined,
);
const prompt =
"promptName" in this.event.body &&
typeof this.event.body.promptName === "string" &&
"promptVersion" in this.event.body &&
typeof this.event.body.promptVersion === "number"
? await prisma.prompt.findUnique({
where: {
projectId_name_version: {
projectId: apiScope.projectId,
name: this.event.body.promptName,
version: this.event.body.promptVersion,
},
},
})
: undefined;
// Only null if promptName and promptVersion are set but prompt is not found
if (prompt === null) {
logger.warn("Prompt not found for observation", this.event.body);
}
const observationId =
this.event.body.id ??
(() => {
const newId = v4();
logger.info(
`observation.id is null. Generating for projectId: ${apiScope.projectId}, id: ${newId}`,
);
return newId;
})();
let traceId = this.event.body?.traceId;
if (!this.event.body.traceId && !existingObservation) {
// Create trace if no traceId
traceId = observationId;
// Insert trace into postgres
await prisma.trace.upsert({
where: {
id: observationId,
},
create: {
projectId: apiScope.projectId,
name: this.event.body.name,
id: observationId,
timestamp: this.event.body.startTime || new Date(),
},
update: {},
});
if (env.CLICKHOUSE_URL) {
// Insert trace into clickhouse if enabled
await upsertTrace({
id: observationId,
project_id: apiScope.projectId,
timestamp: convertDateToClickhouseDateTime(
this.event.body.startTime
? new Date(this.event.body.startTime)
: new Date(),
),
created_at: convertDateToClickhouseDateTime(new Date()),
updated_at: convertDateToClickhouseDateTime(new Date()),
});
}
}
return {
id: observationId,
create: {
id: observationId,
traceId,
type: type,
name: this.event.body.name,
startTime: this.event.body.startTime
? new Date(this.event.body.startTime)
: undefined,
endTime:
"endTime" in this.event.body && this.event.body.endTime
? new Date(this.event.body.endTime)
: undefined,
completionStartTime:
"completionStartTime" in this.event.body &&
this.event.body.completionStartTime
? new Date(this.event.body.completionStartTime)
: undefined,
metadata: mergedMetadata ?? this.event.body.metadata ?? undefined,
model: "model" in this.event.body ? this.event.body.model : undefined,
modelParameters:
"modelParameters" in this.event.body
? (this.event.body.modelParameters ?? undefined)
: undefined,
input: this.event.body.input ?? undefined,
output: this.event.body.output ?? undefined,
promptTokens: newInputCount,
completionTokens: newOutputCount,
totalTokens: newTotalCount,
unit:
"usage" in this.event.body
? (this.event.body.usage?.unit ?? internalModel?.unit)
: internalModel?.unit,
level: this.event.body.level ?? undefined,
statusMessage: this.event.body.statusMessage ?? undefined,
parentObservationId: this.event.body.parentObservationId ?? undefined,
version: this.event.body.version ?? undefined,
projectId: apiScope.projectId,
promptId: prompt ? prompt.id : undefined,
...(internalModel
? { internalModel: internalModel.modelName }
: undefined),
inputCost:
"usage" in this.event.body
? this.event.body.usage?.inputCost
: undefined,
outputCost:
"usage" in this.event.body
? this.event.body.usage?.outputCost
: undefined,
totalCost:
"usage" in this.event.body
? this.event.body.usage?.totalCost
: undefined,
calculatedInputCost: calculatedCosts?.inputCost,
calculatedOutputCost: calculatedCosts?.outputCost,
calculatedTotalCost: calculatedCosts?.totalCost,
internalModelId: internalModel?.id,
},
update: {
name: this.event.body.name ?? undefined,
startTime: this.event.body.startTime
? new Date(this.event.body.startTime)
: undefined,
endTime:
"endTime" in this.event.body && this.event.body.endTime
? new Date(this.event.body.endTime)
: undefined,
completionStartTime:
"completionStartTime" in this.event.body &&
this.event.body.completionStartTime
? new Date(this.event.body.completionStartTime)
: undefined,
metadata: mergedMetadata ?? this.event.body.metadata ?? undefined,
model: "model" in this.event.body ? this.event.body.model : undefined,
modelParameters:
"modelParameters" in this.event.body
? (this.event.body.modelParameters ?? undefined)
: undefined,
input: this.event.body.input ?? undefined,
output: this.event.body.output ?? undefined,
promptTokens: newInputCount,
completionTokens: newOutputCount,
totalTokens: newTotalCount,
unit:
"usage" in this.event.body
? (this.event.body.usage?.unit ?? internalModel?.unit)
: internalModel?.unit,
level: this.event.body.level ?? undefined,
statusMessage: this.event.body.statusMessage ?? undefined,
parentObservationId: this.event.body.parentObservationId ?? undefined,
version: this.event.body.version ?? undefined,
promptId: prompt ? prompt.id : undefined,
...(internalModel
? { internalModel: internalModel.modelName }
: undefined),
inputCost:
"usage" in this.event.body
? this.event.body.usage?.inputCost
: undefined,
outputCost:
"usage" in this.event.body
? this.event.body.usage?.outputCost
: undefined,
totalCost:
"usage" in this.event.body
? this.event.body.usage?.totalCost
: undefined,
calculatedInputCost: calculatedCosts?.inputCost,
calculatedOutputCost: calculatedCosts?.outputCost,
calculatedTotalCost: calculatedCosts?.totalCost,
internalModelId: internalModel?.id,
},
};
}
async calculateTokenCounts(
projectId: string,
body:
| z.infer<typeof legacyObservationCreateEvent>["body"]
| z.infer<typeof generationCreateEvent>["body"],
calculateTokenDelegate: (p: {
model: Model;
text: unknown;
}) => number | undefined,
model?: Model,
existingObservation?: Omit<Observation, "input" | "output">,
) {
let newPromptTokens = body.usage?.input;
if (newPromptTokens === undefined && model && model.tokenizerId) {
if (body.input) {
newPromptTokens = calculateTokenDelegate({
model: model,
text: body.input,
});
} else {
logger.debug(
`No input provided, trying to calculate for id: ${existingObservation?.id}`,
);
const observationInput = await prisma.observation.findFirst({
where: { id: existingObservation?.id, projectId: projectId },
select: {
input: true,
},
});
newPromptTokens = calculateTokenDelegate({
model: model,
text: observationInput?.input,
});
}
}
let newCompletionTokens = body.usage?.output;
if (newCompletionTokens === undefined && model && model.tokenizerId) {
if (body.output) {
newCompletionTokens = calculateTokenDelegate({
model: model,
text: body.output,
});
} else {
logger.debug(
`No output provided, trying to calculate for id: ${existingObservation?.id}`,
);
const observationOutput = await prisma.observation.findFirst({
where: { id: existingObservation?.id, projectId: projectId },
select: {
output: true,
},
});
newCompletionTokens = calculateTokenDelegate({
model: model,
text: observationOutput?.output,
});
}
}
return [newPromptTokens ?? undefined, newCompletionTokens ?? undefined];
}
static calculateTokenCosts(
model: Model | null | undefined,
userProvidedCosts: {
inputCost?: Decimal | null;
outputCost?: Decimal | null;
totalCost?: Decimal | null;
},
tokenCounts: { input?: number; output?: number; total?: number },
): {
inputCost?: Decimal | null;
outputCost?: Decimal | null;
totalCost?: Decimal | null;
} {
// If user has provided any cost point, do not calculate anything else
if (
userProvidedCosts.inputCost ||
userProvidedCosts.outputCost ||
userProvidedCosts.totalCost
) {
return {
...userProvidedCosts,
totalCost:
userProvidedCosts.totalCost ??
(userProvidedCosts.inputCost ?? new Decimal(0)).add(
userProvidedCosts.outputCost ?? new Decimal(0),
),
};
}
const finalInputCost =
tokenCounts.input !== undefined && model?.inputPrice
? model.inputPrice.mul(tokenCounts.input)
: undefined;
const finalOutputCost =
tokenCounts.output !== undefined && model?.outputPrice
? model.outputPrice.mul(tokenCounts.output)
: finalInputCost
? new Decimal(0)
: undefined;
const finalTotalCost =
tokenCounts.total !== undefined && model?.totalPrice
? model.totalPrice.mul(tokenCounts.total)
: (finalInputCost ?? finalOutputCost)
? new Decimal(finalInputCost ?? 0).add(finalOutputCost ?? 0)
: undefined;
return {
inputCost: finalInputCost,
outputCost: finalOutputCost,
totalCost: finalTotalCost,
};
}
auth(apiScope: LegacyIngestionAccessScope): void {
if (apiScope.accessLevel !== "all")
throw new ForbiddenError("Access denied for observation creation");
}
async process(apiScope: LegacyIngestionAccessScope): Promise<Observation> {
this.auth(apiScope);
const existingObservation = this.event.body.id
? await prisma.observation.findFirst({
select: {
// do not select I/O to spare our db
input: false,
output: false,
id: true,
traceId: true,
projectId: true,
type: true,
startTime: true,
endTime: true,
name: true,
metadata: true,
parentObservationId: true,
level: true,
statusMessage: true,
version: true,
createdAt: true,
updatedAt: true,
model: true,
internalModelId: true,
modelParameters: true,
promptTokens: true,
completionTokens: true,
totalTokens: true,
unit: true,
inputCost: true,
outputCost: true,
totalCost: true,
calculatedInputCost: true,
calculatedOutputCost: true,
calculatedTotalCost: true,
completionStartTime: true,
promptId: true,
internalModel: true,
},
where: { id: this.event.body.id, projectId: apiScope.projectId },
})
: null;
if (
existingObservation &&
existingObservation.projectId !== apiScope.projectId
) {
throw new ForbiddenError(
`Access denied for observation creation ${existingObservation.projectId} `,
);
}
const obs = await this.convertToObservation(apiScope, existingObservation);
// Do not use nested upserts or multiple where conditions as this should be a single native database upsert
// https://www.prisma.io/docs/orm/reference/prisma-client-reference#database-upserts
return await prisma.observation.upsert({
where: {
id: obs.id,
},
create: obs.create,
update: obs.update,
});
}
}
export class TraceProcessor implements EventProcessor {
event: z.infer<typeof traceEvent>;
constructor(event: z.infer<typeof traceEvent>) {
this.event = event;
}
auth(apiScope: LegacyIngestionAccessScope): void {
if (apiScope.accessLevel !== "all")
throw new ForbiddenError("Access denied for trace creation");
}
async process(
apiScope: LegacyIngestionAccessScope,
): Promise<Trace | Observation | Score> {
const { body } = this.event;
this.auth(apiScope);
const internalId =
body.id ??
(() => {
const newId = v4();
logger.info(
`trace.id is null. Generating for projectId: ${apiScope.projectId}, id: ${newId}`,
);
return newId;
})();
logger.debug(
`Trying to create trace, project ${apiScope.projectId}, id: ${internalId}`,
);
const existingTrace = await prisma.trace.findFirst({
where: {
id: internalId,
},
});
if (existingTrace && existingTrace.projectId !== apiScope.projectId) {
throw new ForbiddenError(
`Access denied for trace creation ${existingTrace.projectId}`,
);
}
const mergedMetadata = mergeJson(
existingTrace?.metadata
? jsonSchema.parse(existingTrace.metadata)
: undefined,
body.metadata ?? undefined,
);
const mergedTags =
existingTrace?.tags && body.tags
? Array.from(new Set(existingTrace.tags.concat(body.tags ?? []))).sort()
: body.tags
? Array.from(new Set(body.tags)).sort()
: undefined;
if (body.sessionId) {
try {
await prisma.traceSession.upsert({
where: {
id_projectId: {
id: body.sessionId,
projectId: apiScope.projectId,
},
},
create: {
id: body.sessionId,
projectId: apiScope.projectId,
},
update: {},
});
} catch (e) {
if (
e instanceof Prisma.PrismaClientKnownRequestError &&
e.code === "P2002"
) {
logger.warn(
`Failed to upsert session. Session ${body.sessionId} in project ${apiScope.projectId} already exists`,
);
} else {
throw e;
}
}
}
// Do not use nested upserts or multiple where conditions as this should be a single native database upsert
// https://www.prisma.io/docs/orm/reference/prisma-client-reference#database-upserts
const upsertedTrace = await prisma.trace.upsert({
where: {
id: internalId,
},
create: {
id: internalId,
timestamp: this.event.body.timestamp
? new Date(this.event.body.timestamp)
: undefined,
name: body.name ?? undefined,
userId: body.userId ?? undefined,
input: body.input ?? undefined,
output: body.output ?? undefined,
metadata: mergedMetadata ?? body.metadata ?? undefined,
release: body.release ?? undefined,
version: body.version ?? undefined,
sessionId: body.sessionId ?? undefined,
public: body.public ?? undefined,
projectId: apiScope.projectId,
tags: mergedTags ?? undefined,
},
update: {
name: body.name ?? undefined,
timestamp: this.event.body.timestamp
? new Date(this.event.body.timestamp)
: undefined,
userId: body.userId ?? undefined,
input: body.input ?? undefined,
output: body.output ?? undefined,
metadata: mergedMetadata ?? body.metadata ?? undefined,
release: body.release ?? undefined,
version: body.version ?? undefined,
sessionId: body.sessionId ?? undefined,
public: body.public ?? undefined,
tags: mergedTags ?? undefined,
},
});
return upsertedTrace;
}
}
export class ScoreProcessor implements EventProcessor {
event: z.infer<typeof scoreEvent>;
constructor(event: z.infer<typeof scoreEvent>) {
this.event = event;
}
auth(apiScope: LegacyIngestionAccessScope) {
if (apiScope.accessLevel !== "scores" && apiScope.accessLevel !== "all")
throw new ForbiddenError(
`Access denied for score creation, ${apiScope.accessLevel}`,
);
}
async process(
apiScope: LegacyIngestionAccessScope,
): Promise<Trace | Observation | Score> {
const { body } = this.event;
this.auth(apiScope);
const id =
body.id ??
(() => {
const newId = v4();
logger.info(
`score.id is null. Generating for projectId: ${apiScope.projectId}, id: ${newId}`,
);
return newId;
})();
const existingScore = await prisma.score.findFirst({
where: {
id: id,
},
select: {
projectId: true,
},
});
if (existingScore && existingScore.projectId !== apiScope.projectId) {
throw new ForbiddenError(
`Access denied for score creation ${existingScore.projectId}`,
);
}
const validatedScore = await validateAndInflateScore({
body,
scoreId: id,
projectId: apiScope.projectId,
});
return await prisma.score.upsert({
where: {
id_projectId: {
id,
projectId: apiScope.projectId,
},
},
create: {
...validatedScore,
},
update: {
...validatedScore,
},
});
}
}
export class SdkLogProcessor implements EventProcessor {
event: z.infer<typeof sdkLogEvent>;
constructor(event: z.infer<typeof sdkLogEvent>) {
this.event = event;
}
auth(apiScope: LegacyIngestionAccessScope) {
return;
}
process() {
try {
logger.info("SDK Log", this.event);
return undefined;
} catch (error) {
return undefined;
}
}
}
@@ -1,161 +0,0 @@
import { env } from "node:process";
import z from "zod";
import { ForbiddenError, UnauthorizedError } from "../../../errors";
import { eventTypes, ingestionApiSchema, IngestionEventType } from "../types";
import { getProcessorForEvent } from "./EventProcessor";
import { ApiAccessScope } from "../../auth/types";
import { backOff } from "exponential-backoff";
import { Model } from "../../..";
import { logger } from "../../logger";
export type BatchResult = {
result: unknown;
id: string;
type: string;
};
type TokenCountInput = {
model: Model;
text: unknown;
};
export type LegacyIngestionAccessScope = Omit<
ApiAccessScope,
"orgId" | "plan" | "rateLimitOverrides"
>;
type LegacyIngestionAuthHeaderVerificationResult =
| {
validKey: true;
scope: LegacyIngestionAccessScope;
}
| {
validKey: false;
error: string;
};
export const handleBatch = async (
events: z.infer<typeof ingestionApiSchema>["batch"],
authCheck: LegacyIngestionAuthHeaderVerificationResult,
calculateTokenDelegate: (p: TokenCountInput) => number | undefined,
) => {
logger.debug(`handling ingestion ${events.length} events`);
if (!authCheck.validKey) throw new UnauthorizedError(authCheck.error);
const results: BatchResult[] = []; // Array to store the results
const errors: {
error: unknown;
id: string;
type: string;
}[] = []; // Array to store the errors
for (const singleEvent of events) {
try {
const result = await retry(async () => {
return await handleSingleEvent(
singleEvent,
authCheck.scope,
calculateTokenDelegate,
);
});
results.push({
result: result,
id: singleEvent.id,
type: singleEvent.type,
}); // Push each result into the array
} catch (error) {
// Handle or log the error if `handleSingleEvent` fails
logger.error("Error handling event:", error);
// Decide how to handle the error: rethrow, continue, or push an error object to results
// For example, push an error object:
errors.push({
error,
id: singleEvent.id,
type: singleEvent.type,
});
}
}
return { results, errors };
};
async function retry<T>(request: () => Promise<T>): Promise<T> {
return await backOff(request, {
numOfAttempts: env.LANGFUSE_ASYNC_INGESTION_PROCESSING === "true" ? 5 : 3,
retry: (e: Error, attemptNumber: number) => {
if (e instanceof UnauthorizedError || e instanceof ForbiddenError) {
logger.info("not retrying auth error");
return false;
}
logger.info(`retrying processing events ${attemptNumber}`);
return true;
},
});
}
const handleSingleEvent = async (
event: IngestionEventType,
apiScope: LegacyIngestionAccessScope,
calculateTokenDelegate: (p: {
model: Model;
text: unknown;
}) => number | undefined,
) => {
const { body } = event;
let restEvent = body;
if ("input" in body) {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const { input, ...rest } = body;
restEvent = rest;
}
if ("output" in restEvent) {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const { output, ...rest } = restEvent;
restEvent = rest;
}
logger.debug(
`handling single event ${event.id} of type ${event.type}: ${JSON.stringify({ body: restEvent })}`,
);
const cleanedEvent = cleanEvent(event) as IngestionEventType;
// Deny access to non-score events if the access level is not "all"
// This is an additional safeguard to auth checks in EventProcessor
if (
apiScope.accessLevel !== "all" &&
cleanedEvent.type !== eventTypes.SCORE_CREATE
) {
throw new ForbiddenError("Access denied. Event type not allowed.");
}
return getProcessorForEvent(cleanedEvent, calculateTokenDelegate).process(
apiScope,
);
};
// cleans NULL characters from the event
export function cleanEvent(obj: unknown): unknown {
if (typeof obj === "string") {
return obj.replace(/\u0000/g, "");
} else if (typeof obj === "object" && obj !== null) {
if (Array.isArray(obj)) {
return obj.map(cleanEvent);
} else {
// Here we assert that obj is a Record<string, unknown>
const objAsRecord = obj as Record<string, unknown>;
const newObj: Record<string, unknown> = {};
for (const key in objAsRecord) {
newObj[key] = cleanEvent(objAsRecord[key]);
}
return newObj;
}
} else {
return obj;
}
}
export const isUndefinedOrNull = <T>(val?: T | null): val is undefined | null =>
val === undefined || val === null;
@@ -4,6 +4,7 @@ import { z } from "zod";
import { type Model } from "../../db";
import { env } from "../../env";
import {
ForbiddenError,
InvalidRequestError,
LangfuseNotFoundError,
UnauthorizedError,
@@ -18,16 +19,13 @@ import {
traceException,
} from "../instrumentation";
import { logger } from "../logger";
import { LegacyIngestionEventType, QueueJobs } from "../queues";
import { QueueJobs } from "../queues";
import { IngestionQueue } from "../redis/ingestionQueue";
import { LegacyIngestionQueue } from "../redis/legacyIngestion";
import { redis } from "../redis/redis";
import { handleBatch } from "./legacy";
import {
StorageService,
StorageServiceFactory,
} from "../services/StorageService";
import { getProcessorForEvent } from "./legacy/EventProcessor";
import { eventTypes, ingestionEvent, IngestionEventType } from "./types";
export type TokenCountDelegate = (p: {
@@ -54,7 +52,6 @@ const getS3StorageServiceClient = (bucketName: string): StorageService => {
export const processEventBatch = async (
input: unknown[],
authCheck: AuthHeaderValidVerificationResult,
tokenCountDelegate: TokenCountDelegate,
): Promise<{
successes: { id: string; status: number }[];
errors: {
@@ -99,7 +96,7 @@ export const processEventBatch = async (
});
return [];
}
if (!isAuthorized(parsed.data, authCheck, tokenCountDelegate)) {
if (!isAuthorized(parsed.data, authCheck)) {
authenticationErrors.push({
id: parsed.data.id,
error: new UnauthorizedError("Access Scope Denied"),
@@ -156,157 +153,95 @@ export const processEventBatch = async (
* ASYNC PROCESSING *
********************/
let s3UploadErrored = false;
if (env.LANGFUSE_S3_EVENT_UPLOAD_ENABLED === "true") {
await instrumentAsync({ name: "s3-upload-events" }, async () => {
if (env.LANGFUSE_S3_EVENT_UPLOAD_BUCKET === undefined) {
throw new Error("S3 event store is enabled but no bucket is set");
}
const s3Client = getS3StorageServiceClient(
env.LANGFUSE_S3_EVENT_UPLOAD_BUCKET,
);
// S3 Event Upload is currently blocking, but non-failing.
// If a promise rejects, we log it below, but do not throw an error.
// In this case, we upload the full batch into the Redis queue.
const results = await Promise.allSettled(
Object.keys(sortedBatchByEventBodyId).map(async (id) => {
// We upload the event in an array to the S3 bucket grouped by the eventBodyId.
// That way we batch updates from the same invocation into a single file and reduce
// write operations on S3.
const { data, key, type, eventBodyId } = sortedBatchByEventBodyId[id];
return s3Client.uploadJson(
`${env.LANGFUSE_S3_EVENT_UPLOAD_PREFIX}${authCheck.scope.projectId}/${getClickhouseEntityType(type)}/${eventBodyId}/${key}.json`,
data,
);
}),
);
results.forEach((result) => {
if (result.status === "rejected") {
s3UploadErrored = true;
logger.error("Failed to upload event to S3", {
error: result.reason,
});
}
});
});
}
// Send each event individually to IngestionQueue for new processing
if (
env.LANGFUSE_ASYNC_INGESTION_PROCESSING === "true" &&
env.LANGFUSE_S3_EVENT_UPLOAD_ENABLED === "true" &&
env.LANGFUSE_ASYNC_CLICKHOUSE_INGESTION_PROCESSING === "true" &&
redis &&
!s3UploadErrored
) {
const queue = IngestionQueue.getInstance();
await instrumentAsync({ name: "s3-upload-events" }, async () => {
const s3Client = getS3StorageServiceClient(
env.LANGFUSE_S3_EVENT_UPLOAD_BUCKET,
);
// S3 Event Upload is blocking, but non-failing.
// If a promise rejects, we log it below, but do not throw an error.
// In this case, we upload the full batch into the Redis queue.
const results = await Promise.allSettled(
Object.keys(sortedBatchByEventBodyId).map(async (id) =>
queue
? queue.add(
QueueJobs.IngestionJob,
{
id: randomUUID(),
timestamp: new Date(),
name: QueueJobs.IngestionJob as const,
payload: {
data: {
type: sortedBatchByEventBodyId[id].type,
eventBodyId: sortedBatchByEventBodyId[id].eventBodyId,
},
authCheck,
},
},
{
delay: env.LANGFUSE_INGESTION_QUEUE_DELAY_MS,
},
)
: Promise.reject("Failed to instantiate queue"),
),
Object.keys(sortedBatchByEventBodyId).map(async (id) => {
// We upload the event in an array to the S3 bucket grouped by the eventBodyId.
// That way we batch updates from the same invocation into a single file and reduce
// write operations on S3.
const { data, key, type, eventBodyId } = sortedBatchByEventBodyId[id];
return s3Client.uploadJson(
`${env.LANGFUSE_S3_EVENT_UPLOAD_PREFIX}${authCheck.scope.projectId}/${getClickhouseEntityType(type)}/${eventBodyId}/${key}.json`,
data,
);
}),
);
results.forEach((result) => {
if (result.status === "rejected") {
logger.error("Failed to add event to IngestionQueue", {
s3UploadErrored = true;
logger.error("Failed to upload event to S3", {
error: result.reason,
});
}
});
});
// Send each event individually to IngestionQueue for ClickHouse processing
if (s3UploadErrored) {
throw new Error(
"Failed to upload events to blob storage, aborting event processing",
);
}
// As part of the legacy processing we sent the entire batch to the worker.
if (env.LANGFUSE_ASYNC_INGESTION_PROCESSING === "true" && redis) {
const queue = LegacyIngestionQueue.getInstance();
if (queue) {
let addToQueueFailed = false;
const queuePayload: LegacyIngestionEventType =
env.LANGFUSE_S3_EVENT_UPLOAD_ENABLED === "true" && !s3UploadErrored
? {
data: Object.keys(sortedBatchByEventBodyId).map((id) => {
const { key, type, eventBodyId } = sortedBatchByEventBodyId[id];
return {
type,
eventBodyId,
eventId: key,
};
}),
authCheck,
useS3EventStore: true,
}
: { data: sortedBatch, authCheck, useS3EventStore: false };
try {
await queue.add(QueueJobs.LegacyIngestionJob, {
payload: queuePayload,
id: randomUUID(),
timestamp: new Date(),
name: QueueJobs.LegacyIngestionJob as const,
});
} catch (e: unknown) {
logger.warn(
"Failed to add batch to queue, falling back to sync processing",
e,
);
addToQueueFailed = true;
}
if (!addToQueueFailed) {
return aggregateBatchResult(
// we are not sending additional server errors to the client in case of early return
[...validationErrors, ...authenticationErrors],
sortedBatch.map((event) => ({ id: event.id, result: event })),
);
}
} else {
logger.error(
"Ingestion queue not initialized, falling back to sync processing",
);
}
if (!redis) {
throw new Error("Redis not initialized, aborting event processing");
}
/*******************
* SYNC PROCESSING *
*******************/
const result = await handleBatch(sortedBatch, authCheck, tokenCountDelegate);
const queue = IngestionQueue.getInstance();
await Promise.all(
Object.keys(sortedBatchByEventBodyId).map(async (id) =>
queue
? queue.add(
QueueJobs.IngestionJob,
{
id: randomUUID(),
timestamp: new Date(),
name: QueueJobs.IngestionJob as const,
payload: {
data: {
type: sortedBatchByEventBodyId[id].type,
eventBodyId: sortedBatchByEventBodyId[id].eventBodyId,
},
authCheck,
},
},
{
delay: env.LANGFUSE_INGESTION_QUEUE_DELAY_MS,
},
)
: Promise.reject("Failed to instantiate queue"),
),
);
// in case we did not return early, we return the result here
return aggregateBatchResult(
[...validationErrors, ...authenticationErrors, ...result.errors],
result.results,
[...validationErrors, ...authenticationErrors],
sortedBatch.map((event) => ({ id: event.id, result: event })),
authCheck.scope.projectId,
);
};
const isAuthorized = (
event: IngestionEventType,
authScope: AuthHeaderValidVerificationResult,
tokenCountDelegate: TokenCountDelegate,
): boolean => {
try {
getProcessorForEvent(event, tokenCountDelegate).auth(authScope.scope);
if (event.type === eventTypes.SDK_LOG) {
return true;
} catch (error) {
return false;
}
if (event.type === eventTypes.SCORE_CREATE) {
return (
authScope.scope.accessLevel === "scores" ||
authScope.scope.accessLevel === "all"
);
}
return authScope.scope.accessLevel === "all";
};
/**
@@ -336,6 +271,7 @@ const sortBatch = (batch: Array<z.infer<typeof ingestionEvent>>) => {
export const aggregateBatchResult = (
errors: Array<{ id: string; error: unknown }>,
results: Array<{ id: string; result: unknown }>,
projectId?: string,
) => {
const returnedErrors: {
id: string;
@@ -382,7 +318,10 @@ export const aggregateBatchResult = (
if (returnedErrors.length > 0) {
traceException(errors);
logger.error("Error processing events", returnedErrors);
logger.error("Error processing events", {
errors: returnedErrors,
"langfuse.project.id": projectId,
});
}
results.forEach((result) => {
+74 -5
View File
@@ -56,13 +56,76 @@ export const usage = MixedUsage.nullish()
// ensure output is always of new usage model
.pipe(Usage.nullish());
const RawUsageOrCostDetails = z.record(
z.string(),
z.number().nonnegative().nullish(),
);
const OpenAIUsageSchema = z
.object({
prompt_tokens: z.number().nonnegative(),
completion_tokens: z.number().nonnegative(),
total_tokens: z.number().nonnegative(),
prompt_tokens_details: z
.record(z.string(), z.number().nonnegative())
.nullish(),
completion_tokens_details: z
.record(z.string(), z.number().nonnegative())
.nullish(),
})
.strict()
.transform((v) => {
if (!v) return;
const {
prompt_tokens,
completion_tokens,
total_tokens,
prompt_tokens_details,
completion_tokens_details,
} = v;
const result: z.infer<typeof RawUsageOrCostDetails> & {
input: number;
output: number;
total: number;
} = {
input: prompt_tokens,
output: completion_tokens,
total: total_tokens,
};
if (prompt_tokens_details) {
for (const [key, value] of Object.entries(prompt_tokens_details)) {
result[`input_${key}`] = value;
result.input = Math.max(result.input - (value ?? 0), 0);
}
}
if (completion_tokens_details) {
for (const [key, value] of Object.entries(completion_tokens_details)) {
result[`output_${key}`] = value;
result.output = Math.max(result.output - (value ?? 0), 0);
}
}
return result;
})
.pipe(RawUsageOrCostDetails);
export const UsageOrCostDetails = z
.union([OpenAIUsageSchema, RawUsageOrCostDetails])
.nullish();
// Using z.any instead of jsonSchema for input/output as we saw huge CPU overhead for large numeric arrays.
// With this setup parsing should be more lightweight and doesn't block other requests.
// As we allow plain values, arrays, and objects the JSON parse via bodyParser should suffice.
export const TraceBody = z.object({
id: z.string().nullish(),
timestamp: stringDateTime,
name: z.string().nullish(),
name: z.string().max(1000).nullish(),
externalId: z.string().nullish(),
input: jsonSchema.nullish(),
output: jsonSchema.nullish(),
input: z.any().nullish(),
output: z.any().nullish(),
sessionId: z.string().nullish(),
userId: z.string().nullish(),
metadata: jsonSchema.nullish(),
@@ -77,8 +140,8 @@ export const OptionalObservationBody = z.object({
name: z.string().nullish(),
startTime: stringDateTime,
metadata: jsonSchema.nullish(),
input: jsonSchema.nullish(),
output: jsonSchema.nullish(),
input: z.any().nullish(),
output: z.any().nullish(),
level: z.nativeEnum(ObservationLevel).nullish(),
statusMessage: z.string().nullish(),
parentObservationId: z.string().nullish(),
@@ -119,6 +182,8 @@ export const CreateGenerationBody = CreateSpanBody.extend({
)
.nullish(),
usage: usage,
usageDetails: UsageOrCostDetails,
costDetails: UsageOrCostDetails,
promptName: z.string().nullish(),
promptVersion: z.number().int().nullish(),
}).refine((value) => {
@@ -147,6 +212,8 @@ export const UpdateGenerationBody = UpdateSpanBody.extend({
)
.nullish(),
usage: usage,
usageDetails: UsageOrCostDetails,
costDetails: UsageOrCostDetails,
promptName: z.string().nullish(),
promptVersion: z.number().int().nullish(),
}).refine((value) => {
@@ -298,6 +365,8 @@ export const LegacyObservationBody = z.object({
input: jsonSchema.nullish(),
output: jsonSchema.nullish(),
usage: usage,
usageDetails: UsageOrCostDetails,
costDetails: UsageOrCostDetails,
metadata: jsonSchema.nullish(),
parentObservationId: z.string().nullish(),
level: z.nativeEnum(ObservationLevel).nullish(),
@@ -17,7 +17,7 @@ type ValidateAndInflateScoreParams = {
export async function validateAndInflateScore(
params: ValidateAndInflateScoreParams,
): Promise<Score> {
const { body, projectId } = params;
const { body, projectId, scoreId } = params;
if (body.configId) {
const config = await prisma.scoreConfig.findFirst({
@@ -32,10 +32,22 @@ export async function validateAndInflateScore(
"The configId you provided does not match a valid config in this project",
);
validateConfigAgainstBody(body, config as ValidatedScoreConfig);
// Override some fields in the score body with config fields
// We ignore the set fields in the body
const bodyWithConfigOverrides = {
...body,
name: config.name,
};
validateConfigAgainstBody(
bodyWithConfigOverrides,
config as ValidatedScoreConfig,
);
return inflateScoreBody({
...params,
projectId,
scoreId,
body: bodyWithConfigOverrides,
config: config as ValidatedScoreConfig,
});
}
@@ -3,9 +3,11 @@ import type { ZodSchema } from "zod";
import { CallbackHandler } from "langfuse-langchain";
import { ChatAnthropic } from "@langchain/anthropic";
import { ChatVertexAI } from "@langchain/google-vertexai";
import { ChatBedrockConverse } from "@langchain/aws";
import {
AIMessage,
BaseMessage,
HumanMessage,
SystemMessage,
} from "@langchain/core/messages";
@@ -15,7 +17,7 @@ import {
} from "@langchain/core/output_parsers";
import { IterableReadableStream } from "@langchain/core/utils/stream";
import { ChatOpenAI } from "@langchain/openai";
import {
import GCPServiceAccountKeySchema, {
BedrockConfigSchema,
BedrockCredentialSchema,
} from "../../interfaces/customLLMProviderConfigSchemas";
@@ -120,7 +122,6 @@ export async function fetchLLMCompletion(
await processEventBatch(
JSON.parse(JSON.stringify(events)), // stringify to emulate network event batch from network call
traceParams.authCheck,
traceParams.tokenCountDelegate,
);
} catch (e) {
logger.error("Failed to process traced events", { error: e });
@@ -130,16 +131,28 @@ export async function fetchLLMCompletion(
finalCallbacks = finalCallbacks.length > 0 ? finalCallbacks : undefined;
const finalMessages = messages.map((message) => {
if (message.role === ChatMessageRole.User)
return new HumanMessage(message.content);
if (message.role === ChatMessageRole.System)
return new SystemMessage(message.content);
let finalMessages: BaseMessage[];
// VertexAI requires at least 1 user message
if (modelParams.adapter === LLMAdapter.VertexAI && messages.length === 1) {
finalMessages = [new HumanMessage(messages[0].content)];
} else {
finalMessages = messages.map((message) => {
if (message.role === ChatMessageRole.User)
return new HumanMessage(message.content);
if (message.role === ChatMessageRole.System)
return new SystemMessage(message.content);
return new AIMessage(message.content);
});
return new AIMessage(message.content);
});
}
let chatModel: ChatOpenAI | ChatAnthropic | ChatBedrockConverse;
finalMessages = finalMessages.filter((m) => m.content.length > 0);
let chatModel:
| ChatOpenAI
| ChatAnthropic
| ChatBedrockConverse
| ChatVertexAI;
if (modelParams.adapter === LLMAdapter.Anthropic) {
chatModel = new ChatAnthropic({
anthropicApiKey: apiKey,
@@ -149,7 +162,7 @@ export async function fetchLLMCompletion(
maxTokens: modelParams.max_tokens,
topP: modelParams.top_p,
callbacks: finalCallbacks,
clientOptions: { maxRetries },
clientOptions: { maxRetries, timeout: 1000 * 60 * 2 }, // 2 minutes timeout
});
} else if (modelParams.adapter === LLMAdapter.OpenAI) {
chatModel = new ChatOpenAI({
@@ -164,6 +177,7 @@ export async function fetchLLMCompletion(
configuration: {
baseURL,
},
timeout: 1000 * 60 * 2, // 2 minutes timeout
});
} else if (modelParams.adapter === LLMAdapter.Azure) {
chatModel = new ChatOpenAI({
@@ -176,6 +190,7 @@ export async function fetchLLMCompletion(
topP: modelParams.top_p,
callbacks: finalCallbacks,
maxRetries,
timeout: 1000 * 60 * 2, // 2 minutes timeout
});
} else if (modelParams.adapter === LLMAdapter.Bedrock) {
const { region } = BedrockConfigSchema.parse(config);
@@ -190,6 +205,24 @@ export async function fetchLLMCompletion(
topP: modelParams.top_p,
callbacks: finalCallbacks,
maxRetries,
timeout: 1000 * 60 * 2, // 2 minutes timeout
});
} else if (modelParams.adapter === LLMAdapter.VertexAI) {
const credentials = GCPServiceAccountKeySchema.parse(JSON.parse(apiKey));
// Requests time out after 60 seconds for both public and private endpoints by default
// Reference: https://cloud.google.com/vertex-ai/docs/predictions/get-online-predictions#send-request
chatModel = new ChatVertexAI({
modelName: modelParams.model,
temperature: modelParams.temperature,
maxOutputTokens: modelParams.max_tokens,
topP: modelParams.top_p,
callbacks: finalCallbacks,
maxRetries,
authOptions: {
projectId: credentials.project_id,
credentials,
},
});
} else {
// eslint-disable-next-line no-unused-vars
@@ -237,6 +270,7 @@ export async function fetchLLMCompletion(
configuration: {
baseURL,
},
timeout: 1000 * 60 * 2, // 2 minutes timeout
})
.pipe(new StringOutputParser())
.invoke(
+10
View File
@@ -16,6 +16,7 @@ export enum LLMAdapter {
OpenAI = "openai",
Azure = "azure",
Bedrock = "bedrock",
VertexAI = "vertex-ai",
}
export enum ChatMessageRole {
@@ -114,10 +115,19 @@ export const anthropicModels = [
"claude-instant-1.2",
] as const;
export const vertexAIModels = [
"gemini-2.0-flash-exp",
"gemini-1.5-pro",
"gemini-1.5-flash",
"gemini-1.0-pro",
] as const;
export type AnthropicModel = (typeof anthropicModels)[number];
export type VertexAIModel = (typeof vertexAIModels)[number];
export const supportedModels = {
[LLMAdapter.Anthropic]: anthropicModels,
[LLMAdapter.OpenAI]: openAIModels,
[LLMAdapter.VertexAI]: vertexAIModels,
[LLMAdapter.Azure]: [],
[LLMAdapter.Bedrock]: [],
} as const;
@@ -161,8 +161,8 @@ export class StringOptionsFilter implements Filter {
return {
query:
this.operator === "any of"
? `has({${varName}: Array(String)}, ${this.tablePrefix ? this.tablePrefix + "." : ""}${this.field}) = True`
: `has({${varName}: Array(String)}, ${this.tablePrefix ? this.tablePrefix + "." : ""}${this.field}) = False`,
? `${this.tablePrefix ? this.tablePrefix + "." : ""}${this.field} IN ({${varName}: Array(String)})`
: `${this.tablePrefix ? this.tablePrefix + "." : ""}${this.field} NOT IN ({${varName}: Array(String)})`,
params: { [varName]: this.values },
};
}
@@ -3,31 +3,53 @@ import { OrderByState } from "../../../interfaces/orderBy";
import { UiColumnMapping } from "../../../tableDefinitions";
import { logger } from "../../logger";
type OrderByStateNotNull = Exclude<OrderByState, null>;
export function orderByToClickhouseSql(
orderBy: OrderByState,
orderBy: OrderByState | OrderByState[] = [],
tableColumns: UiColumnMapping[],
): string {
if (!orderBy) {
if (
!orderBy ||
(Array.isArray(orderBy) && orderBy.filter(Boolean).length === 0)
) {
return "";
}
// Get column definition to map column to internal name, e.g. "t.id"
const col = tableColumns.find(
(c) => c.uiTableName === orderBy.column || c.uiTableId === orderBy.column,
);
if (!col) {
logger.warn("Invalid order by column", orderBy.column);
throw new Error("Invalid order by column: " + orderBy.column);
if (!Array.isArray(orderBy)) {
orderBy = [orderBy];
}
// Initialize an array to hold order by clauses
const orderByClauses: string[] = [];
// Loop through each orderBy entry
for (const ob of orderBy.filter((o): o is OrderByStateNotNull =>
Boolean(o),
)) {
// Get column definition to map column to internal name, e.g. "t.id"
const col = tableColumns.find(
(c) => c.uiTableName === ob.column || c.uiTableId === ob.column,
);
if (!col) {
logger.warn("Invalid order by column", ob.column);
throw new Error("Invalid order by column: " + ob.column);
}
// Assert that ob.order is either "asc" or "desc"
const orderByOrder = z.enum(["ASC", "DESC"]);
const order = orderByOrder.safeParse(ob.order);
if (!order.success) {
logger.warn("Invalid order", ob.order);
throw new Error("Invalid order: " + ob.order);
}
// Append the order by clause to the array
orderByClauses.push(
`${col.queryPrefix ? col.queryPrefix + "." : ""}${col.clickhouseSelect} ${order.data}`,
);
}
// Assert that orderBy.order is either "asc" or "desc"
const orderByOrder = z.enum(["ASC", "DESC"]);
const order = orderByOrder.safeParse(orderBy.order);
if (!order.success) {
logger.warn("Invalid order", orderBy.order);
throw new Error("Invalid order: " + orderBy.order);
}
// Both column and order are safe, can use raw SQL
return `ORDER BY ${col.queryPrefix ? col.queryPrefix + "." : ""}${col.clickhouseSelect} ${order.data}`;
// Join all order by clauses with a comma and return
return `ORDER BY ${orderByClauses.join(", ")}`;
}
@@ -1,15 +1,10 @@
import { ObservationView, Prisma } from "@prisma/client";
import {
datetimeFilterToPrismaSql,
tableColumnsToSqlFilterAndPrefix,
} from "../filterToPrisma";
import { orderByToPrismaSql } from "../orderByToPrisma";
import { observationsTableCols } from "../../observationsTable";
import { TableFilters } from "./types";
import { ObservationView } from "@prisma/client";
type AdditionalObservationFields = {
traceName: string | null;
traceTags: Array<string>;
usageDetails: Record<string, number>;
costDetails: Record<string, number>;
};
export type FullObservation = AdditionalObservationFields & ObservationView;
@@ -24,147 +19,3 @@ export type IOAndMetadataOmittedObservations = Array<
Omit<ObservationView, "input" | "output" | "metadata"> &
AdditionalObservationFields
>;
export function parseGetAllGenerationsInput(filters: TableFilters) {
const searchCondition = filters.searchQuery
? Prisma.sql`AND (
o."id" ILIKE ${`%${filters.searchQuery}%`} OR
o."name" ILIKE ${`%${filters.searchQuery}%`} OR
o."model" ILIKE ${`%${filters.searchQuery}%`} OR
t."name" ILIKE ${`%${filters.searchQuery}%`}
)`
: Prisma.empty;
const filterCondition = tableColumnsToSqlFilterAndPrefix(
filters.filter ?? [],
observationsTableCols,
"observations",
);
const orderByCondition = orderByToPrismaSql(
filters.orderBy,
observationsTableCols,
);
// to improve query performance, add timeseries filter to observation queries as well
const startTimeFilter = filters.filter?.find(
(f) => f.column === "Start Time" && f.type === "datetime",
);
const datetimeFilter =
startTimeFilter && startTimeFilter.type === "datetime"
? datetimeFilterToPrismaSql(
"start_time",
startTimeFilter.operator,
startTimeFilter.value,
)
: Prisma.empty;
return {
searchCondition,
filterCondition,
orderByCondition,
datetimeFilter,
};
}
export function createGenerationsQuery({
projectId,
datetimeFilter = Prisma.empty,
page,
limit,
searchCondition = Prisma.empty,
filterCondition = Prisma.empty,
orderByCondition = Prisma.empty,
selectIOAndMetadata = false,
selectScoreValues = false,
}: {
projectId: string;
datetimeFilter?: Prisma.Sql;
page?: number;
limit?: number;
searchCondition?: Prisma.Sql;
filterCondition?: Prisma.Sql;
orderByCondition?: Prisma.Sql;
selectIOAndMetadata?: boolean;
selectScoreValues?: boolean;
}) {
return Prisma.sql`
WITH scores_avg AS (
SELECT
trace_id,
observation_id,
${selectScoreValues ? Prisma.sql`jsonb_object_agg(name::text, "values") AS "scores_values",` : Prisma.empty}
jsonb_object_agg(name::text, avg_value::double precision) AS "scores_avg"
FROM (
SELECT
trace_id,
observation_id,
name,
${selectScoreValues ? Prisma.sql`array_agg(COALESCE(string_value, value::text)) AS "values",` : Prisma.empty}
avg(value) avg_value,
comment
FROM
scores
WHERE
project_id = ${projectId}
${selectScoreValues ? Prisma.empty : Prisma.sql`AND scores."data_type" IN ('NUMERIC', 'BOOLEAN')`}
GROUP BY
trace_id,
observation_id,
name,
comment
ORDER BY
trace_id
) tmp
GROUP BY
trace_id,
observation_id
)
SELECT
${selectScoreValues ? Prisma.sql`s_avg."scores_values" AS "scores",` : Prisma.empty}
o.id,
o.name,
o.model,
o."modelParameters",
o.start_time as "startTime",
o.end_time as "endTime",
${selectIOAndMetadata ? Prisma.sql`o.input, o.output, o.metadata,` : Prisma.empty}
o.trace_id as "traceId",
t.name as "traceName",
o.completion_start_time as "completionStartTime",
o.time_to_first_token as "timeToFirstToken",
o.prompt_tokens as "promptTokens",
o.completion_tokens as "completionTokens",
o.total_tokens as "totalTokens",
o.unit,
o.level,
o.status_message as "statusMessage",
o.version,
o.model_id as "modelId",
o.input_price as "inputPrice",
o.output_price as "outputPrice",
o.total_price as "totalPrice",
o.calculated_input_cost as "calculatedInputCost",
o.calculated_output_cost as "calculatedOutputCost",
o.calculated_total_cost as "calculatedTotalCost",
o."latency",
o.prompt_id as "promptId",
p.name as "promptName",
p.version as "promptVersion",
t.tags as "traceTags"
FROM observations_view o
JOIN traces t ON t.id = o.trace_id AND t.project_id = ${projectId}
LEFT JOIN scores_avg AS s_avg ON s_avg.trace_id = t.id and s_avg.observation_id = o.id
LEFT JOIN prompts p ON p.id = o.prompt_id AND p.project_id = ${projectId}
WHERE
o.project_id = ${projectId}
AND o.type = 'GENERATION'
${datetimeFilter}
${searchCondition}
${filterCondition}
${orderByCondition}
${limit ? Prisma.sql`LIMIT ${limit}` : Prisma.empty}
${page && limit ? Prisma.sql`OFFSET ${page * limit}` : Prisma.empty}
`;
}
@@ -1,135 +0,0 @@
import { Prisma } from "@prisma/client";
import { TableFilters } from "./types";
import {
datetimeFilterToPrismaSql,
tableColumnsToSqlFilterAndPrefix,
} from "../filterToPrisma";
import { tracesTableCols } from "../../tableDefinitions/tracesTable";
import { orderByToPrismaSql } from "../orderByToPrisma";
export function parseTraceAllFilters(input: TableFilters) {
const filterCondition = tableColumnsToSqlFilterAndPrefix(
input.filter ?? [],
tracesTableCols,
"traces",
);
const orderByCondition = orderByToPrismaSql(input.orderBy, tracesTableCols);
// to improve query performance, add timeseries filter to observation queries as well
const timeseriesFilter = input.filter?.find(
(f) => f.column === "Timestamp" && f.type === "datetime",
);
const observationTimeseriesFilter =
timeseriesFilter && timeseriesFilter.type === "datetime"
? datetimeFilterToPrismaSql(
"start_time",
timeseriesFilter.operator,
timeseriesFilter.value,
)
: Prisma.empty;
const searchCondition = input.searchQuery
? Prisma.sql`AND (
t."id" ILIKE ${`%${input.searchQuery}%`} OR
t."external_id" ILIKE ${`%${input.searchQuery}%`} OR
t."user_id" ILIKE ${`%${input.searchQuery}%`} OR
t."name" ILIKE ${`%${input.searchQuery}%`}
)`
: Prisma.empty;
return {
filterCondition,
orderByCondition,
observationTimeseriesFilter,
searchCondition,
};
}
export function createTracesQuery({
select,
projectId,
observationTimeseriesFilter = Prisma.empty,
page,
limit,
searchCondition = Prisma.empty,
filterCondition = Prisma.empty,
orderByCondition = Prisma.empty,
selectScoreValues = false,
}: {
select: Prisma.Sql;
projectId: string;
observationTimeseriesFilter?: Prisma.Sql;
page?: number;
limit?: number;
searchCondition?: Prisma.Sql;
filterCondition?: Prisma.Sql;
orderByCondition?: Prisma.Sql;
selectScoreValues?: boolean;
}) {
return Prisma.sql`
SELECT
${select}
FROM
"traces" AS t
LEFT JOIN LATERAL (
SELECT
SUM(prompt_tokens) AS "promptTokens",
SUM(completion_tokens) AS "completionTokens",
SUM(total_tokens) AS "totalTokens",
SUM(calculated_total_cost) AS "calculatedTotalCost",
SUM(calculated_input_cost) AS "calculatedInputCost",
SUM(calculated_output_cost) AS "calculatedOutputCost"
FROM
"observations_view"
WHERE
trace_id = t.id
AND "type" = 'GENERATION'
AND "project_id" = ${projectId}
${observationTimeseriesFilter}
) AS generation_metrics ON true
LEFT JOIN LATERAL (
SELECT
COUNT(*) AS "observationCount",
EXTRACT(EPOCH FROM COALESCE(MAX("end_time"), MAX("start_time"))) - EXTRACT(EPOCH FROM MIN("start_time"))::double precision AS "latency",
COALESCE(
MAX(CASE WHEN level = 'ERROR' THEN 'ERROR' END),
MAX(CASE WHEN level = 'WARNING' THEN 'WARNING' END),
MAX(CASE WHEN level = 'DEFAULT' THEN 'DEFAULT' END),
'DEBUG'
) AS "level"
FROM
"observations"
WHERE
trace_id = t.id
AND "project_id" = ${projectId}
${observationTimeseriesFilter}
) AS observation_metrics ON true
LEFT JOIN LATERAL (
SELECT
${selectScoreValues ? Prisma.sql`jsonb_object_agg(name::text, "values") AS "scores_values",` : Prisma.empty}
jsonb_object_agg(name::text, avg_value::double precision) AS "scores_avg"
FROM (
SELECT
name,
${selectScoreValues ? Prisma.sql`array_agg(COALESCE(string_value, value::text)) AS "values",` : Prisma.empty}
AVG(value) avg_value
FROM
scores
WHERE
trace_id = t.id
AND t."project_id" = ${projectId}
${selectScoreValues ? Prisma.empty : Prisma.sql`AND scores."data_type" IN ('NUMERIC', 'BOOLEAN')`}
GROUP BY
name
) tmp
) AS s_avg ON true
WHERE
t."project_id" = ${projectId}
${searchCondition}
${filterCondition}
${orderByCondition}
${limit ? Prisma.sql`LIMIT ${limit}` : Prisma.empty}
${page !== undefined && limit !== undefined ? Prisma.sql`OFFSET ${page * limit}` : Prisma.empty}
`;
}
+1 -3
View File
@@ -1,8 +1,5 @@
export { createSessionsAllQuery } from "./createSessionsAllQuery";
export { createTracesQuery, parseTraceAllFilters } from "./createTracesQuery";
export {
createGenerationsQuery,
parseGetAllGenerationsInput,
type FullObservations,
type FullObservationsWithScores,
type IOAndMetadataOmittedObservations,
@@ -20,3 +17,4 @@ export {
NullFilter,
type ClickhouseOperator,
} from "./clickhouse-sql/clickhouse-filter";
export { orderByToClickhouseSql } from "./clickhouse-sql/orderby-factory";
+55 -76
View File
@@ -1,50 +1,6 @@
import { z } from "zod";
import { eventTypes, ingestionBatchEvent } from ".";
export enum EventName {
TraceUpsert = "TraceUpsert",
BatchExport = "BatchExport",
EvaluationExecution = "EvaluationExecution",
LegacyIngestion = "LegacyIngestion",
CloudUsageMetering = "CloudUsageMetering",
ExperimentCreate = "ExperimentCreate",
}
export const LegacyIngestionEventFull = z.object({
useS3EventStore: z.literal(false),
data: ingestionBatchEvent,
authCheck: z.object({
validKey: z.literal(true),
scope: z.object({
projectId: z.string(),
accessLevel: z.enum(["all", "scores"]),
}),
}),
});
export const LegacyIngestionEventMeta = z.object({
useS3EventStore: z.literal(true),
data: z.array(
z.object({
type: z.nativeEnum(eventTypes),
eventBodyId: z.string(),
eventId: z.string(),
}),
),
authCheck: z.object({
validKey: z.literal(true),
scope: z.object({
projectId: z.string(),
accessLevel: z.enum(["all", "scores"]),
}),
}),
});
export const LegacyIngestionEvent = z.discriminatedUnion("useS3EventStore", [
LegacyIngestionEventFull,
LegacyIngestionEventMeta,
]);
export const IngestionEvent = z.object({
data: z.object({
type: z.nativeEnum(eventTypes),
@@ -63,10 +19,18 @@ export const BatchExportJobSchema = z.object({
projectId: z.string(),
batchExportId: z.string(),
});
export const TraceUpsertEventSchema = z.object({
export const TraceQueueEventSchema = z.object({
projectId: z.string(),
traceId: z.string(),
});
export const TracesQueueEventSchema = z.object({
projectId: z.string(),
traceIds: z.array(z.string()),
});
export const ProjectQueueEventSchema = z.object({
projectId: z.string(),
orgId: z.string(),
});
export const DatasetRunItemUpsertEventSchema = z.object({
projectId: z.string(),
datasetItemId: z.string(),
@@ -78,7 +42,9 @@ export const EvalExecutionEvent = z.object({
jobExecutionId: z.string(),
delay: z.number().nullish(),
});
export const PostHogIntegrationProcessingEventSchema = z.object({
projectId: z.string(),
});
export const ExperimentCreateEventSchema = z.object({
projectId: z.string(),
datasetId: z.string(),
@@ -87,67 +53,74 @@ export const ExperimentCreateEventSchema = z.object({
});
export type BatchExportJobType = z.infer<typeof BatchExportJobSchema>;
export type TraceUpsertEventType = z.infer<typeof TraceUpsertEventSchema>;
export type TraceQueueEventType = z.infer<typeof TraceQueueEventSchema>;
export type TracesQueueEventType = z.infer<typeof TracesQueueEventSchema>;
export type ProjectQueueEventType = z.infer<typeof ProjectQueueEventSchema>;
export type DatasetRunItemUpsertEventType = z.infer<
typeof DatasetRunItemUpsertEventSchema
>;
export type EvalExecutionEventType = z.infer<typeof EvalExecutionEvent>;
export type LegacyIngestionEventType = z.infer<typeof LegacyIngestionEvent>;
export type IngestionEventQueueType = z.infer<typeof IngestionEvent>;
export type ExperimentCreateEventType = z.infer<
typeof ExperimentCreateEventSchema
>;
export const EventBodySchema = z.union([
z.object({
name: z.literal(EventName.TraceUpsert),
payload: z.array(TraceUpsertEventSchema),
}),
z.object({
name: z.literal(EventName.EvaluationExecution),
payload: EvalExecutionEvent,
}),
z.object({
name: z.literal(EventName.BatchExport),
payload: BatchExportJobSchema,
}),
z.object({
name: z.literal(EventName.ExperimentCreate),
payload: ExperimentCreateEventSchema,
}),
]);
export type EventBodyType = z.infer<typeof EventBodySchema>;
export type PostHogIntegrationProcessingEventType = z.infer<
typeof PostHogIntegrationProcessingEventSchema
>;
export enum QueueName {
TraceUpsert = "trace-upsert", // Ingestion pipeline adds events on each Trace upsert
TraceDelete = "trace-delete",
ProjectDelete = "project-delete",
EvaluationExecution = "evaluation-execution-queue", // Worker executes Evals
DatasetRunItemUpsert = "dataset-run-item-upsert-queue",
BatchExport = "batch-export-queue",
IngestionQueue = "ingestion-queue", // Process single events with S3-merge
LegacyIngestionQueue = "legacy-ingestion-queue", // Used for batch processing of Ingestion
IngestionSecondaryQueue = "secondary-ingestion-queue", // Separates high priority + high throughput projects from other projects.
CloudUsageMeteringQueue = "cloud-usage-metering-queue",
ExperimentCreate = "experiment-create-queue",
PostHogIntegrationQueue = "posthog-integration-queue",
PostHogIntegrationProcessingQueue = "posthog-integration-processing-queue",
CoreDataS3ExportQueue = "core-data-s3-export-queue",
MeteringDataPostgresExportQueue = "metering-data-postgres-export-queue",
}
export enum QueueJobs {
TraceUpsert = "trace-upsert",
TraceDelete = "trace-delete",
ProjectDelete = "project-delete",
DatasetRunItemUpsert = "dataset-run-item-upsert",
EvaluationExecution = "evaluation-execution-job",
BatchExportJob = "batch-export-job",
EnqueueBatchExportJobs = "enqueue-batch-export-jobs",
LegacyIngestionJob = "legacy-ingestion-job",
CloudUsageMeteringJob = "cloud-usage-metering-job",
IngestionJob = "ingestion-job",
IngestionSecondaryJob = "secondary-ingestion-job",
ExperimentCreateJob = "experiment-create-job",
PostHogIntegrationJob = "posthog-integration-job",
PostHogIntegrationProcessingJob = "posthog-integration-processing-job",
CoreDataS3ExportJob = "core-data-s3-export-job",
MeteringDataPostgresExportJob = "metering-data-postgres-export-job",
}
export type TQueueJobTypes = {
[QueueName.TraceUpsert]: {
timestamp: Date;
id: string;
payload: TraceUpsertEventType;
payload: TraceQueueEventType;
name: QueueJobs.TraceUpsert;
};
[QueueName.TraceDelete]: {
timestamp: Date;
id: string;
payload: TracesQueueEventType | TraceQueueEventType;
name: QueueJobs.TraceDelete;
};
[QueueName.ProjectDelete]: {
timestamp: Date;
id: string;
payload: ProjectQueueEventType;
name: QueueJobs.ProjectDelete;
};
[QueueName.DatasetRunItemUpsert]: {
timestamp: Date;
id: string;
@@ -166,13 +139,13 @@ export type TQueueJobTypes = {
payload: BatchExportJobType;
name: QueueJobs.BatchExportJob;
};
[QueueName.LegacyIngestionQueue]: {
[QueueName.IngestionQueue]: {
timestamp: Date;
id: string;
payload: LegacyIngestionEventType;
name: QueueJobs.LegacyIngestionJob;
payload: IngestionEventQueueType;
name: QueueJobs.IngestionJob;
};
[QueueName.IngestionQueue]: {
[QueueName.IngestionSecondaryQueue]: {
timestamp: Date;
id: string;
payload: IngestionEventQueueType;
@@ -184,4 +157,10 @@ export type TQueueJobTypes = {
payload: ExperimentCreateEventType;
name: QueueJobs.ExperimentCreateJob;
};
[QueueName.PostHogIntegrationProcessingQueue]: {
timestamp: Date;
id: string;
payload: PostHogIntegrationProcessingEventType;
name: QueueJobs.PostHogIntegrationProcessingJob;
};
};
@@ -1,8 +1,8 @@
import { Queue } from "bullmq";
import { env } from "../..";
import { logger } from "@azure/storage-blob";
import { env } from "../../env";
import { QueueName, QueueJobs } from "../queues";
import { createNewRedisInstance, redisQueueRetryOptions } from "./redis";
import { logger } from "../logger";
export class CloudUsageMeteringQueue {
private static instance: Queue | null = null;
@@ -45,6 +45,7 @@ export class CloudUsageMeteringQueue {
QueueJobs.CloudUsageMeteringJob,
{},
{
// Run at minute 5 of every hour (e.g. 1:05, 2:05, 3:05, etc)
repeat: { pattern: "5 * * * *" },
},
);
@@ -0,0 +1,60 @@
import { Queue } from "bullmq";
import { QueueName, QueueJobs } from "../queues";
import { createNewRedisInstance, redisQueueRetryOptions } from "./redis";
import { logger } from "../logger";
import { env } from "../../env";
export class CoreDataS3ExportQueue {
private static instance: Queue | null = null;
public static getInstance(): Queue | null {
if (env.LANGFUSE_S3_CORE_DATA_EXPORT_IS_ENABLED !== "true") {
return null;
}
if (CoreDataS3ExportQueue.instance) {
return CoreDataS3ExportQueue.instance;
}
const newRedis = createNewRedisInstance({
enableOfflineQueue: false,
...redisQueueRetryOptions,
});
CoreDataS3ExportQueue.instance = newRedis
? new Queue(QueueName.CoreDataS3ExportQueue, {
connection: newRedis,
defaultJobOptions: {
removeOnComplete: true,
removeOnFail: 100,
attempts: 5,
backoff: {
type: "exponential",
delay: 5000,
},
},
})
: null;
CoreDataS3ExportQueue.instance?.on("error", (err) => {
logger.error("CoreDataS3ExportQueue error", err);
});
if (CoreDataS3ExportQueue.instance) {
logger.debug("Scheduling jobs for CoreDataS3ExportQueue");
CoreDataS3ExportQueue.instance
.add(
QueueJobs.CoreDataS3ExportJob,
{},
{
repeat: { pattern: "15 3 * * *" }, // every day at 3:15am
},
)
.catch((err) => {
logger.error("Error adding CoreDataS3ExportJob schedule", err);
});
}
return CoreDataS3ExportQueue.instance;
}
}
@@ -29,7 +29,7 @@ export class EvalExecutionQueue {
attempts: 10,
backoff: {
type: "exponential",
delay: 5000,
delay: 1000,
},
},
},
@@ -26,10 +26,10 @@ export class ExperimentCreateQueue {
defaultJobOptions: {
removeOnComplete: true,
removeOnFail: 10_000,
attempts: 2,
attempts: 10,
backoff: {
type: "exponential",
delay: 5000,
delay: 1000,
},
},
},
+22 -5
View File
@@ -1,18 +1,21 @@
import { Queue } from "bullmq";
import { QueueName } from "../queues";
import { BatchExportQueue } from "./batchExport";
import { CloudUsageMeteringQueue } from "./CloudUsageMeteringQueue";
import { CloudUsageMeteringQueue } from "./cloudUsageMeteringQueue";
import { DatasetRunItemUpsertQueue } from "./datasetRunItemUpsert";
import { EvalExecutionQueue } from "./evalExecutionQueue";
import { ExperimentCreateQueue } from "./experimentCreateQueue";
import { IngestionQueue } from "./ingestionQueue";
import { LegacyIngestionQueue } from "./legacyIngestion";
import { IngestionQueue, SecondaryIngestionQueue } from "./ingestionQueue";
import { TraceUpsertQueue } from "./traceUpsert";
import { TraceDeleteQueue } from "./traceDelete";
import { ProjectDeleteQueue } from "./projectDelete";
import { PostHogIntegrationQueue } from "./postHogIntegrationQueue";
import { PostHogIntegrationProcessingQueue } from "./postHogIntegrationProcessingQueue";
import { CoreDataS3ExportQueue } from "./coreDataS3ExportQueue";
import { MeteringDataPostgresExportQueue } from "./meteringDataPostgresExportQueue";
export function getQueue(queueName: QueueName): Queue | null {
switch (queueName) {
case QueueName.LegacyIngestionQueue:
return LegacyIngestionQueue.getInstance();
case QueueName.BatchExport:
return BatchExportQueue.getInstance();
case QueueName.CloudUsageMeteringQueue:
@@ -25,8 +28,22 @@ export function getQueue(queueName: QueueName): Queue | null {
return ExperimentCreateQueue.getInstance();
case QueueName.TraceUpsert:
return TraceUpsertQueue.getInstance();
case QueueName.TraceDelete:
return TraceDeleteQueue.getInstance();
case QueueName.IngestionQueue:
return IngestionQueue.getInstance();
case QueueName.ProjectDelete:
return ProjectDeleteQueue.getInstance();
case QueueName.PostHogIntegrationQueue:
return PostHogIntegrationQueue.getInstance();
case QueueName.PostHogIntegrationProcessingQueue:
return PostHogIntegrationProcessingQueue.getInstance();
case QueueName.IngestionSecondaryQueue:
return SecondaryIngestionQueue.getInstance();
case QueueName.CoreDataS3ExportQueue:
return CoreDataS3ExportQueue.getInstance();
case QueueName.MeteringDataPostgresExportQueue:
return MeteringDataPostgresExportQueue.getInstance();
default:
const exhaustiveCheckDefault: never = queueName;
throw new Error(`Queue ${queueName} not found`);
@@ -43,3 +43,45 @@ export class IngestionQueue {
return IngestionQueue.instance;
}
}
export class SecondaryIngestionQueue {
private static instance: Queue<
TQueueJobTypes[QueueName.IngestionSecondaryQueue]
> | null = null;
public static getInstance(): Queue<
TQueueJobTypes[QueueName.IngestionSecondaryQueue]
> | null {
if (SecondaryIngestionQueue.instance)
return SecondaryIngestionQueue.instance;
const newRedis = createNewRedisInstance({
enableOfflineQueue: false,
...redisQueueRetryOptions,
});
SecondaryIngestionQueue.instance = newRedis
? new Queue<TQueueJobTypes[QueueName.IngestionSecondaryQueue]>(
QueueName.IngestionSecondaryQueue,
{
connection: newRedis,
defaultJobOptions: {
removeOnComplete: true,
removeOnFail: 100_000,
attempts: 5,
backoff: {
type: "exponential",
delay: 5000,
},
},
},
)
: null;
SecondaryIngestionQueue.instance?.on("error", (err) => {
logger.error("SecondaryIngestionQueue error", err);
});
return SecondaryIngestionQueue.instance;
}
}
@@ -0,0 +1,63 @@
import { Queue } from "bullmq";
import { QueueName, QueueJobs } from "../queues";
import { createNewRedisInstance, redisQueueRetryOptions } from "./redis";
import { logger } from "../logger";
import { env } from "../../env";
export class MeteringDataPostgresExportQueue {
private static instance: Queue | null = null;
public static getInstance(): Queue | null {
if (env.LANGFUSE_POSTGRES_METERING_DATA_EXPORT_IS_ENABLED !== "true") {
return null;
}
if (MeteringDataPostgresExportQueue.instance) {
return MeteringDataPostgresExportQueue.instance;
}
const newRedis = createNewRedisInstance({
enableOfflineQueue: false,
...redisQueueRetryOptions,
});
MeteringDataPostgresExportQueue.instance = newRedis
? new Queue(QueueName.MeteringDataPostgresExportQueue, {
connection: newRedis,
defaultJobOptions: {
removeOnComplete: true,
removeOnFail: 100,
attempts: 5,
backoff: {
type: "exponential",
delay: 5000,
},
},
})
: null;
MeteringDataPostgresExportQueue.instance?.on("error", (err) => {
logger.error("MeteringDataPostgresExportQueue error", err);
});
if (MeteringDataPostgresExportQueue.instance) {
logger.debug("Scheduling jobs for MeteringDataPostgresExportQueue");
MeteringDataPostgresExportQueue.instance
.add(
QueueJobs.MeteringDataPostgresExportJob,
{},
{
repeat: { pattern: "30 2 * * *" }, // every day at 2:30am UTC
},
)
.catch((err) => {
logger.error(
"Error adding MeteringDataPostgresExportJob schedule",
err,
);
});
}
return MeteringDataPostgresExportQueue.instance;
}
}
@@ -0,0 +1,40 @@
import { Queue } from "bullmq";
import { QueueName } from "../queues";
import { createNewRedisInstance, redisQueueRetryOptions } from "./redis";
import { logger } from "../logger";
export class PostHogIntegrationProcessingQueue {
private static instance: Queue | null = null;
public static getInstance(): Queue | null {
if (PostHogIntegrationProcessingQueue.instance) {
return PostHogIntegrationProcessingQueue.instance;
}
const newRedis = createNewRedisInstance({
enableOfflineQueue: false,
...redisQueueRetryOptions,
});
PostHogIntegrationProcessingQueue.instance = newRedis
? new Queue(QueueName.PostHogIntegrationProcessingQueue, {
connection: newRedis,
defaultJobOptions: {
removeOnComplete: true,
removeOnFail: 100_000,
attempts: 5,
backoff: {
type: "exponential",
delay: 5000,
},
},
})
: null;
PostHogIntegrationProcessingQueue.instance?.on("error", (err) => {
logger.error("PostHogIntegrationProcessingQueue error", err);
});
return PostHogIntegrationProcessingQueue.instance;
}
}
@@ -0,0 +1,55 @@
import { Queue } from "bullmq";
import { QueueName, QueueJobs } from "../queues";
import { createNewRedisInstance, redisQueueRetryOptions } from "./redis";
import { logger } from "../logger";
export class PostHogIntegrationQueue {
private static instance: Queue | null = null;
public static getInstance(): Queue | null {
if (PostHogIntegrationQueue.instance) {
return PostHogIntegrationQueue.instance;
}
const newRedis = createNewRedisInstance({
enableOfflineQueue: false,
...redisQueueRetryOptions,
});
PostHogIntegrationQueue.instance = newRedis
? new Queue(QueueName.PostHogIntegrationQueue, {
connection: newRedis,
defaultJobOptions: {
removeOnComplete: true,
removeOnFail: 100,
attempts: 5,
backoff: {
type: "exponential",
delay: 5000,
},
},
})
: null;
PostHogIntegrationQueue.instance?.on("error", (err) => {
logger.error("PostHogIntegrationQueue error", err);
});
if (PostHogIntegrationQueue.instance) {
logger.debug("Scheduling jobs for PostHogIntegrationQueue");
PostHogIntegrationQueue.instance
.add(
QueueJobs.PostHogIntegrationJob,
{},
{
repeat: { pattern: "30 * * * *" }, // every hour at 30 minutes past
},
)
.catch((err) => {
logger.error("Error adding PostHogIntegrationJob schedule", err);
});
}
return PostHogIntegrationQueue.instance;
}
}
@@ -1,31 +1,31 @@
import { Queue } from "bullmq";
import { QueueName, TQueueJobTypes } from "../queues";
import { Queue } from "bullmq";
import { createNewRedisInstance, redisQueueRetryOptions } from "./redis";
import { logger } from "../logger";
export class LegacyIngestionQueue {
export class ProjectDeleteQueue {
private static instance: Queue<
TQueueJobTypes[QueueName.LegacyIngestionQueue]
TQueueJobTypes[QueueName.ProjectDelete]
> | null = null;
public static getInstance(): Queue<
TQueueJobTypes[QueueName.LegacyIngestionQueue]
TQueueJobTypes[QueueName.ProjectDelete]
> | null {
if (LegacyIngestionQueue.instance) return LegacyIngestionQueue.instance;
if (ProjectDeleteQueue.instance) return ProjectDeleteQueue.instance;
const newRedis = createNewRedisInstance({
enableOfflineQueue: false,
...redisQueueRetryOptions,
});
LegacyIngestionQueue.instance = newRedis
? new Queue<TQueueJobTypes[QueueName.LegacyIngestionQueue]>(
QueueName.LegacyIngestionQueue,
ProjectDeleteQueue.instance = newRedis
? new Queue<TQueueJobTypes[QueueName.ProjectDelete]>(
QueueName.ProjectDelete,
{
connection: newRedis,
defaultJobOptions: {
removeOnComplete: true,
removeOnFail: 500_000,
removeOnFail: 100_000,
attempts: 5,
backoff: {
type: "exponential",
@@ -36,10 +36,10 @@ export class LegacyIngestionQueue {
)
: null;
LegacyIngestionQueue.instance?.on("error", (err) => {
logger.error("LegacyIngestionQueue error", err);
ProjectDeleteQueue.instance?.on("error", (err) => {
logger.error("ProjectDeleteQueue error", err);
});
return LegacyIngestionQueue.instance;
return ProjectDeleteQueue.instance;
}
}
@@ -0,0 +1,44 @@
import { QueueName, TQueueJobTypes } from "../queues";
import { Queue } from "bullmq";
import { createNewRedisInstance, redisQueueRetryOptions } from "./redis";
import { logger } from "../logger";
export class TraceDeleteQueue {
private static instance: Queue<TQueueJobTypes[QueueName.TraceDelete]> | null =
null;
public static getInstance(): Queue<
TQueueJobTypes[QueueName.TraceDelete]
> | null {
if (TraceDeleteQueue.instance) return TraceDeleteQueue.instance;
const newRedis = createNewRedisInstance({
enableOfflineQueue: false,
...redisQueueRetryOptions,
});
TraceDeleteQueue.instance = newRedis
? new Queue<TQueueJobTypes[QueueName.TraceDelete]>(
QueueName.TraceDelete,
{
connection: newRedis,
defaultJobOptions: {
removeOnComplete: true,
removeOnFail: 100_000,
attempts: 5,
backoff: {
type: "exponential",
delay: 5000,
},
},
},
)
: null;
TraceDeleteQueue.instance?.on("error", (err) => {
logger.error("TraceDeleteQueue error", err);
});
return TraceDeleteQueue.instance;
}
}
@@ -1,10 +1,4 @@
import { randomUUID } from "crypto";
import {
QueueJobs,
QueueName,
TQueueJobTypes,
TraceUpsertEventType,
} from "../queues";
import { QueueName, TQueueJobTypes } from "../queues";
import { Queue } from "bullmq";
import { createNewRedisInstance, redisQueueRetryOptions } from "./redis";
import { logger } from "../logger";
@@ -32,7 +26,7 @@ export class TraceUpsertQueue {
removeOnComplete: 100, // Important: If not true, new jobs for that ID would be ignored as jobs in the complete set are still considered as part of the queue
removeOnFail: 100_000,
attempts: 5,
delay: 10_000, // 10 seconds
delay: 15_000, // 15 seconds
backoff: {
type: "exponential",
delay: 5000,
@@ -4,7 +4,7 @@ import {
convertDateToClickhouseDateTime,
} from "../clickhouse/client";
import { logger } from "../logger";
import { instrumentAsync } from "../instrumentation";
import { getTracer, instrumentAsync } from "../instrumentation";
import {
StorageService,
StorageServiceFactory,
@@ -12,6 +12,7 @@ import {
import { randomUUID } from "crypto";
import { getClickhouseEntityType } from "../clickhouse/schemaUtils";
import { NodeClickHouseClientConfigOptions } from "@clickhouse/client/dist/config";
import { context, trace } from "@opentelemetry/api";
let s3StorageServiceClient: StorageService;
@@ -40,37 +41,31 @@ export async function upsertClickhouse<
// https://opentelemetry.io/docs/specs/semconv/database/database-spans/
span.setAttribute("ch.query.table", opts.table);
// If event upload is enabled, we store all rows in S3 to have a backup
if (env.LANGFUSE_S3_EVENT_UPLOAD_ENABLED === "true") {
if (env.LANGFUSE_S3_EVENT_UPLOAD_BUCKET === undefined) {
throw new Error("S3 event store is enabled but no bucket is set");
}
const s3Client = getS3StorageServiceClient(
env.LANGFUSE_S3_EVENT_UPLOAD_BUCKET,
);
await Promise.all(
opts.records.map((record) => {
// drop trailing s and pretend it's always a create.
// Only applicable to scores and traces.
let eventType = `${opts.table.slice(0, -1)}-create`;
if (opts.table === "observations") {
// @ts-ignore - If it's an observation we now that `type` is a string
eventType = `${record["type"].toLowerCase()}-create`;
}
s3Client.uploadJson(
`${env.LANGFUSE_S3_EVENT_UPLOAD_PREFIX}${record.project_id}/${getClickhouseEntityType(eventType)}/${record.id}/${randomUUID()}.json`,
[
{
id: randomUUID(),
timestamp: new Date().toISOString(),
type: eventType,
body: opts.eventBodyMapper(record),
},
],
);
}),
);
}
const s3Client = getS3StorageServiceClient(
env.LANGFUSE_S3_EVENT_UPLOAD_BUCKET,
);
await Promise.all(
opts.records.map((record) => {
// drop trailing s and pretend it's always a create.
// Only applicable to scores and traces.
let eventType = `${opts.table.slice(0, -1)}-create`;
if (opts.table === "observations") {
// @ts-ignore - If it's an observation we now that `type` is a string
eventType = `${record["type"].toLowerCase()}-create`;
}
s3Client.uploadJson(
`${env.LANGFUSE_S3_EVENT_UPLOAD_PREFIX}${record.project_id}/${getClickhouseEntityType(eventType)}/${record.id}/${randomUUID()}.json`,
[
{
id: randomUUID(),
timestamp: new Date().toISOString(),
type: eventType,
body: opts.eventBodyMapper(record),
},
],
);
}),
);
const res = await clickhouseClient().insert({
table: opts.table,
@@ -107,6 +102,64 @@ export async function upsertClickhouse<
});
}
export async function* queryClickhouseStream<T>(opts: {
query: string;
params?: Record<string, unknown> | undefined;
clickhouseConfigs?: NodeClickHouseClientConfigOptions;
}): AsyncGenerator<T> {
const tracer = getTracer("clickhouse-query-stream");
const span = tracer.startSpan("clickhouse-query-stream");
try {
const res = await context.with(
trace.setSpan(context.active(), span),
async () => {
// https://opentelemetry.io/docs/specs/semconv/database/database-spans/
span.setAttribute("ch.query.text", opts.query);
const res = await clickhouseClient(opts.clickhouseConfigs).query({
query: opts.query,
format: "JSONEachRow",
query_params: opts.params,
});
// same logic as for prisma. we want to see queries in development
if (env.NODE_ENV === "development") {
logger.info(`clickhouse:query ${res.query_id} ${opts.query}`);
}
span.setAttribute("ch.queryId", res.query_id);
// add summary headers to the span. Helps to tune performance
const summaryHeader = res.response_headers["x-clickhouse-summary"];
if (summaryHeader) {
try {
const summary = Array.isArray(summaryHeader)
? JSON.parse(summaryHeader[0])
: JSON.parse(summaryHeader);
for (const key in summary) {
span.setAttribute(`ch.${key}`, summary[key]);
}
} catch (error) {
logger.debug(
`Failed to parse clickhouse summary header ${summaryHeader}`,
error,
);
}
}
return res;
},
);
for await (const rows of res.stream<T>()) {
for (const row of rows) {
yield row.json();
}
}
} finally {
span.end();
}
}
export async function queryClickhouse<T>(opts: {
query: string;
params?: Record<string, unknown> | undefined;
@@ -1,4 +1,7 @@
import { queryClickhouse } from "./clickhouse";
import {
parseClickhouseUTCDateTimeFormat,
queryClickhouse,
} from "./clickhouse";
import { createFilterFromFilterState } from "../queries/clickhouse-sql/factory";
import { FilterState } from "../../types";
import {
@@ -200,8 +203,8 @@ export const getObservationUsageByTime = async (
const query = `
SELECT
${selectTimeseriesColumn(groupBy, "start_time", "start_time")},
sumMap(usage_details)['total'] as sum_usage_details,
sumMap(cost_details)['total'] as sum_cost_details,
sumMap(usage_details) as units,
sumMap(cost_details) as cost,
provided_model_name
FROM observations o FINAL
${tracesFilter ? "LEFT JOIN traces t ON o.trace_id = t.id AND o.project_id = t.project_id" : ""}
@@ -214,8 +217,8 @@ export const getObservationUsageByTime = async (
const result = await queryClickhouse<{
start_time: string;
sum_usage_details: string;
sum_cost_details: number;
units: Record<string, number>;
cost: Record<string, number>;
provided_model_name: string;
}>({
query,
@@ -230,8 +233,18 @@ export const getObservationUsageByTime = async (
return result.map((row) => ({
start_time: new Date(row.start_time),
sum_usage_details: Number(row.sum_usage_details),
sum_cost_details: row.sum_cost_details,
units: Object.fromEntries(
Object.entries(row.units ?? {}).map(([key, value]) => [
key,
Number(value),
]),
),
cost: Object.fromEntries(
Object.entries(row.cost ?? {}).map(([key, value]) => [
key,
Number(value),
]),
),
provided_model_name: row.provided_model_name,
}));
};
@@ -405,7 +418,7 @@ export const getObservationLatencies = async (
// Skipping FINAL here, as the quantiles are approximate to begin with.
const query = `
SELECT
quantiles(0.5, 0.9, 0.95, 0.99)(date_diff('milliseconds', o.start_time, o.end_time)) as quantiles,
quantiles(0.5, 0.9, 0.95, 0.99)(date_diff('millisecond', o.start_time, o.end_time)) as quantiles,
name
FROM observations o
${chFilter.find((f) => f.clickhouseTable === "traces") ? "LEFT JOIN traces t ON o.trace_id = t.id AND o.project_id = t.project_id" : ""}
@@ -452,7 +465,7 @@ export const getTracesLatencies = async (
select o.trace_id,
t.name,
o.project_id,
date_diff('milliseconds', min(o.start_time), coalesce(max(o.end_time), max(o.start_time))) as duration
date_diff('millisecond', min(o.start_time), coalesce(max(o.end_time), max(o.start_time))) as duration
FROM traces t
JOIN observations o
ON o.trace_id = t.id AND o.project_id = t.project_id
@@ -508,7 +521,7 @@ export const getModelLatenciesOverTime = async (
SELECT
${selectTimeseriesColumn(groupBy, "o.start_time", "start_time_bucket")},
provided_model_name,
quantiles(0.5, 0.75, 0.9, 0.95, 0.99)(date_diff('milliseconds', o.start_time, o.end_time)) as quantiles
quantiles(0.5, 0.75, 0.9, 0.95, 0.99)(date_diff('millisecond', o.start_time, o.end_time)) as quantiles
FROM observations o
${traceFilter ? "JOIN traces t ON o.trace_id = t.id AND o.project_id = t.project_id" : ""}
WHERE project_id = {projectId: String}
@@ -616,6 +629,50 @@ export const getCategoricalScoreTimeSeries = async (
});
};
export const getObservationsStatusTimeSeries = async (
projectId: string,
filter: FilterState,
groupBy: DateTrunc | undefined,
) => {
const chFilter = new FilterList(
createFilterFromFilterState(filter, dashboardColumnDefinitions),
);
const chFilterRes = chFilter.apply();
const traceFilter = chFilter.find((f) => f.clickhouseTable === "traces");
const query = `
SELECT
${groupBy ? selectTimeseriesColumn(groupBy, "o.start_time", "start_time_bucket") + ", " : ""}
count(*) as observation_count,
level as level
FROM observations o
${traceFilter ? "JOIN traces t ON o.trace_id = t.id AND o.project_id = t.project_id" : ""}
WHERE project_id = {projectId: String}
AND o.level IS NOT NULL
AND ${chFilterRes?.query}
GROUP BY level ${groupBy ? ", start_time_bucket" : ""}
${groupBy ? orderByTimeSeries(groupBy, "start_time_bucket") : ""}
`;
const result = await queryClickhouse<{
start_time_bucket?: string;
observation_count: string;
level: string;
}>({
query,
params: { projectId, ...(chFilterRes ? chFilterRes.params : {}) },
});
return result.map((row) => ({
start_time_bucket: row.start_time_bucket
? parseClickhouseUTCDateTimeFormat(row.start_time_bucket)
: undefined,
count: Number(row.observation_count),
level: row.level,
}));
};
const orderByTimeSeries = (dateTrunc: DateTrunc, col: string) => {
let interval;
switch (dateTrunc) {
@@ -7,3 +7,5 @@ export * from "./traces_converters";
export * from "./scores_converters";
export * from "./observations_converters";
export * from "./clickhouse";
export * from "./constants";
export * from "./trace-sessions";
@@ -2,6 +2,7 @@ import {
commandClickhouse,
parseClickhouseUTCDateTimeFormat,
queryClickhouse,
queryClickhouseStream,
upsertClickhouse,
} from "./clickhouse";
import { ObservationLevel } from "@prisma/client";
@@ -38,6 +39,7 @@ import {
OBSERVATIONS_TO_TRACE_INTERVAL,
TRACE_TO_OBSERVATIONS_INTERVAL,
} from "./constants";
import { env } from "../../env";
export const checkObservationExists = async (
projectId: string,
@@ -210,11 +212,13 @@ export const getObservationById = async (
id: string,
projectId: string,
fetchWithInputOutput: boolean = false,
startTime?: Date,
) => {
const records = await getObservationByIdInternal(
id,
projectId,
fetchWithInputOutput,
startTime,
);
const mapped = records.map(convertObservation);
@@ -311,6 +315,7 @@ const getObservationByIdInternal = async (
id: string,
projectId: string,
fetchWithInputOutput: boolean = false,
startTime?: Date,
) => {
const query = `
SELECT
@@ -345,11 +350,18 @@ const getObservationByIdInternal = async (
FROM observations
WHERE id = {id: String}
AND project_id = {projectId: String}
${startTime ? `AND start_time = {startTime: DateTime64(3)}` : ""}
ORDER BY event_ts desc
LIMIT 1 by id, project_id`;
return await queryClickhouse<ObservationRecordReadType>({
query,
params: { id, projectId },
params: {
id,
projectId,
...(startTime
? { startTime: convertDateToClickhouseDateTime(startTime) }
: {}),
},
});
};
@@ -389,7 +401,7 @@ export type ObservationsTableQueryResult = ObservationRecordReadType & {
export const getObservationsTableCount = async (opts: ObservationTableQuery) =>
getObservationsTableInternal<TableCount>({
...opts,
select: "count(*) as count",
select: "count",
});
export type ObservationsTableRow = Omit<
@@ -403,33 +415,11 @@ export const getObservationsTable = async (
const observationRecords = await getObservationsTableInternal<
Omit<
ObservationsTableQueryResult,
"trace_tags" | "trace_name" | "trace_user_id" | "type"
"trace_tags" | "trace_name" | "trace_user_id"
>
>({
...opts,
select: `
o.id as id,
o.name as name,
o."model_parameters" as model_parameters,
o.start_time as "start_time",
o.end_time as "end_time",
o.trace_id as "trace_id",
o.completion_start_time as "completion_start_time",
o.provided_usage_details as "provided_usage_details",
o.usage_details as "usage_details",
o.provided_cost_details as "provided_cost_details",
o.cost_details as "cost_details",
o.level as level,
o.status_message as "status_message",
o.version as version,
o.parent_observation_id as "parent_observation_id",
o.created_at as "created_at",
o.updated_at as "updated_at",
o.provided_model_name as "provided_model_name",
o.total_cost as "total_cost",
internal_model_id as "internal_model_id",
if(isNull(end_time), NULL, date_diff('milliseconds', start_time, end_time)) as latency,
if(isNull(completion_start_time), NULL, date_diff('milliseconds', start_time, completion_start_time)) as "time_to_first_token"`,
select: "rows",
});
const traces = await getTracesByIds(
@@ -442,7 +432,7 @@ export const getObservationsTable = async (
return observationRecords.map((o) => {
const trace = traces.find((t) => t.id === o.trace_id);
return {
...convertObservationToView({ ...o, type: "GENERATION" }),
...convertObservationToView(o),
latency: o.latency ? Number(o.latency) / 1000 : null,
timeToFirstToken: o.time_to_first_token
? Number(o.time_to_first_token) / 1000
@@ -460,33 +450,11 @@ export const getObservationsTableWithModelData = async (
const observationRecords = await getObservationsTableInternal<
Omit<
ObservationsTableQueryResult,
"trace_tags" | "trace_name" | "trace_user_id" | "type"
"trace_tags" | "trace_name" | "trace_user_id"
>
>({
...opts,
select: `
o.id as id,
o.name as name,
o."model_parameters" as model_parameters,
o.start_time as "start_time",
o.end_time as "end_time",
o.trace_id as "trace_id",
o.completion_start_time as "completion_start_time",
o.provided_usage_details as "provided_usage_details",
o.usage_details as "usage_details",
o.provided_cost_details as "provided_cost_details",
o.cost_details as "cost_details",
o.level as level,
o.status_message as "status_message",
o.version as version,
o.parent_observation_id as "parent_observation_id",
o.created_at as "created_at",
o.updated_at as "updated_at",
o.provided_model_name as "provided_model_name",
o.total_cost as "total_cost",
internal_model_id as "internal_model_id",
if(isNull(end_time), NULL, date_diff('milliseconds', start_time, end_time)) as latency,
if(isNull(completion_start_time), NULL, date_diff('milliseconds', start_time, completion_start_time)) as "time_to_first_token"`,
select: "rows",
});
const uniqueModels: string[] = Array.from(
@@ -523,7 +491,7 @@ export const getObservationsTableWithModelData = async (
const trace = traces.find((t) => t.id === o.trace_id);
const model = models.find((m) => m.id === o.internal_model_id);
return {
...convertObservationToView({ ...o, type: "GENERATION" }),
...convertObservationToView(o),
latency: o.latency ? Number(o.latency) / 1000 : null,
timeToFirstToken: o.time_to_first_token
? Number(o.time_to_first_token) / 1000
@@ -543,17 +511,49 @@ export const getObservationsTableWithModelData = async (
};
const getObservationsTableInternal = async <T>(
opts: ObservationTableQuery & { select: string },
opts: ObservationTableQuery & { select: "count" | "rows" },
): Promise<Array<T>> => {
const select =
opts.select === "count"
? "count(*) as count"
: `
o.id as id,
o.type as type,
o.project_id as "project_id",
o.name as name,
o."model_parameters" as model_parameters,
o.start_time as "start_time",
o.end_time as "end_time",
o.trace_id as "trace_id",
o.completion_start_time as "completion_start_time",
o.provided_usage_details as "provided_usage_details",
o.usage_details as "usage_details",
o.provided_cost_details as "provided_cost_details",
o.cost_details as "cost_details",
o.level as level,
o.status_message as "status_message",
o.version as version,
o.parent_observation_id as "parent_observation_id",
o.created_at as "created_at",
o.updated_at as "updated_at",
o.provided_model_name as "provided_model_name",
o.total_cost as "total_cost",
o.prompt_id as "prompt_id",
o.prompt_name as "prompt_name",
o.prompt_version as "prompt_version",
internal_model_id as "internal_model_id",
if(isNull(end_time), NULL, date_diff('millisecond', start_time, end_time)) as latency,
if(isNull(completion_start_time), NULL, date_diff('millisecond', start_time, completion_start_time)) as "time_to_first_token"`;
const { projectId, filter, selectIOAndMetadata, limit, offset, orderBy } =
opts;
const selectString = selectIOAndMetadata
? `
${opts.select},
${select},
${selectIOAndMetadata ? `o.input, o.output, o.metadata` : ""}
`
: opts.select;
: select;
const scoresFilter = new FilterList([
new StringFilter({
@@ -580,6 +580,10 @@ const getObservationsTableInternal = async <T>(
.includes(f.column),
);
const hasScoresFilter = filter.some(
(f) => f.column === "Scores" || f.column === "scores",
);
const orderByTraces = opts.orderBy
? observationsTableTraceUiColumnDefinitions
.map((c) => c.uiTableId)
@@ -650,63 +654,58 @@ const getObservationsTableInternal = async <T>(
observation_id
)`;
if (traceTableFilter.length > 0 || orderByTraces) {
// joins with traces are very expensive. We need to filter by time as well.
// We assume that a trace has to have been within the last 2 days to be relevant.
// if we have default ordering by time, we order by toDate(o.start_time) first and then by
// o.start_time. This way, clickhouse is able to read more efficiently directly from disk without ordering
const newDefaultOrder =
orderBy?.column === "startTime"
? [{ column: "order_by_date", order: orderBy.order }, orderBy]
: [orderBy ?? null];
const query = `
const chOrderBy = orderByToClickhouseSql(newDefaultOrder, [
...observationsTableUiColumnDefinitions,
{
uiTableName: "order_by_date",
uiTableId: "order_by_date",
clickhouseTableName: "observation",
clickhouseSelect: "toDate(o.start_time)",
},
]);
// joins with traces are very expensive. We need to filter by time as well.
// We assume that a trace has to have been within the last 2 days to be relevant.
const query = `
${scoresCte}
SELECT
${selectString}
FROM observations o FINAL
LEFT JOIN traces t FINAL ON t.id = o.trace_id AND t.project_id = o.project_id
LEFT JOIN scores_avg AS s_avg ON s_avg.trace_id = o.trace_id and s_avg.observation_id = o.id
FROM observations o
${traceTableFilter.length > 0 || orderByTraces || search.query ? "LEFT JOIN traces t FINAL ON t.id = o.trace_id AND t.project_id = o.project_id" : ""}
${hasScoresFilter ? `LEFT JOIN scores_avg AS s_avg ON s_avg.trace_id = o.trace_id and s_avg.observation_id = o.id` : ""}
WHERE ${appliedObservationsFilter.query}
AND o.type = 'GENERATION'
${timeFilter ? `AND t.timestamp > {tracesTimestampFilter: DateTime64(3)} - ${OBSERVATIONS_TO_TRACE_INTERVAL}` : ""}
${timeFilter && (traceTableFilter.length > 0 || orderByTraces) ? `AND t.timestamp > {tracesTimestampFilter: DateTime64(3)} - ${OBSERVATIONS_TO_TRACE_INTERVAL}` : ""}
${search.query}
${orderByToClickhouseSql(orderBy ?? null, observationsTableUiColumnDefinitions)}
${chOrderBy}
${opts.select === "rows" ? "LIMIT 1 BY o.id, o.project_id" : ""}
${limit !== undefined && offset !== undefined ? `LIMIT ${limit} OFFSET ${offset}` : ""};`;
const res = await queryClickhouse<T>({
query,
params: {
...appliedScoresFilter.params,
...appliedObservationsFilter.params,
...(timeFilter
? {
tracesTimestampFilter: convertDateToClickhouseDateTime(
timeFilter.value as Date,
),
}
: {}),
...search.params,
},
});
const res = await queryClickhouse<T>({
query,
params: {
...appliedScoresFilter.params,
...appliedObservationsFilter.params,
...(timeFilter
? {
tracesTimestampFilter: convertDateToClickhouseDateTime(
timeFilter.value as Date,
),
}
: {}),
...search.params,
},
});
return res;
} else {
// we query by T, which could also be {count: string}.
const query = `
${scoresCte}
SELECT
${selectString}
FROM observations o FINAL
LEFT JOIN scores_avg AS s_avg ON s_avg.trace_id = o.trace_id and s_avg.observation_id = o.id
WHERE ${appliedObservationsFilter.query}
${orderByToClickhouseSql(orderBy ?? null, observationsTableUiColumnDefinitions)}
${limit !== undefined && offset !== undefined ? `LIMIT ${limit} OFFSET ${offset}` : ""};`;
const res = await queryClickhouse<T>({
query,
params: {
...appliedScoresFilter.params,
...appliedObservationsFilter.params,
},
});
return res;
}
return res;
};
export const getObservationsGroupedByModel = async (
@@ -753,6 +752,50 @@ export const getObservationsGroupedByModel = async (
return res.map((r) => ({ model: r.name }));
};
export const getObservationsGroupedByModelId = async (
projectId: string,
filter: FilterState,
) => {
const observationsFilter = new FilterList([
new StringFilter({
clickhouseTable: "observations",
field: "project_id",
operator: "=",
value: projectId,
tablePrefix: "o",
}),
]);
observationsFilter.push(
...createFilterFromFilterState(
filter,
observationsTableUiColumnDefinitions,
),
);
const appliedObservationsFilter = observationsFilter.apply();
// We mainly use queries like this to retrieve filter options.
// Therefore, we can skip final as some inaccuracy in count is acceptable.
const query = `
SELECT o.internal_model_id as modelId
FROM observations o
WHERE ${appliedObservationsFilter.query}
AND o.type = 'GENERATION'
GROUP BY o.internal_model_id
ORDER BY count() DESC
LIMIT 1000;
`;
const res = await queryClickhouse<{ modelId: string }>({
query,
params: {
...appliedObservationsFilter.params,
},
});
return res.map((r) => ({ modelId: r.modelId }));
};
export const getObservationsGroupedByName = async (
projectId: string,
filter: FilterState,
@@ -907,6 +950,25 @@ export const deleteObservationsByTraceIds = async (
projectId,
traceIds,
},
clickhouseConfigs: {
request_timeout: 120_000, // 2 minutes
},
});
};
export const deleteObservationsByProjectId = async (projectId: string) => {
const query = `
DELETE FROM observations
WHERE project_id = {projectId: String};
`;
await commandClickhouse({
query: query,
params: {
projectId,
},
clickhouseConfigs: {
request_timeout: 120_000, // 2 minutes
},
});
};
@@ -950,7 +1012,7 @@ export const getObservationMetricsForPrompts = async (
end_time,
usage_details,
cost_details,
dateDiff('milliseconds', start_time, end_time) AS latency_ms
dateDiff('millisecond', start_time, end_time) AS latency_ms
FROM observations
FINAL
WHERE (type = 'GENERATION')
@@ -964,8 +1026,8 @@ export const getObservationMetricsForPrompts = async (
prompt_version,
min(start_time) AS first_observation,
max(start_time) AS last_observation,
medianExact(usage_details['input']) AS median_input_usage,
medianExact(usage_details['output']) AS median_output_usage,
medianExact(arraySum(mapValues(mapFilter(x -> positionCaseInsensitive(x.1, 'input') > 0, usage_details)))) AS median_input_usage,
medianExact(arraySum(mapValues(mapFilter(x -> positionCaseInsensitive(x.1, 'output') > 0, usage_details)))) AS median_output_usage,
medianExact(cost_details['total']) AS median_total_cost,
medianExact(latency_ms) AS median_latency_ms
FROM latencies
@@ -1013,7 +1075,7 @@ export const getLatencyAndTotalCostForObservations = async (
SELECT
id,
cost_details['total'] AS total_cost,
dateDiff('milliseconds', start_time, end_time) AS latency_ms
dateDiff('millisecond', start_time, end_time) AS latency_ms
FROM observations FINAL
WHERE project_id = {projectId: String}
AND id IN ({observationIds: Array(String)})
@@ -1045,7 +1107,7 @@ export const getLatencyAndTotalCostForObservationsByTraces = async (
SELECT
trace_id,
sumMap(cost_details)['total'] AS total_cost,
dateDiff('milliseconds', min(start_time), max(end_time)) AS latency_ms
dateDiff('millisecond', min(start_time), max(end_time)) AS latency_ms
FROM observations FINAL
WHERE project_id = {projectId: String}
AND trace_id IN ({traceIds: Array(String)})
@@ -1069,3 +1131,167 @@ export const getLatencyAndTotalCostForObservationsByTraces = async (
latency: Number(r.latency_ms) / 1000,
}));
};
export const getObservationCountsByProjectInCreationInterval = async ({
start,
end,
}: {
start: Date;
end: Date;
}) => {
const query = `
SELECT
project_id,
count(*) as count
FROM observations
WHERE created_at >= {start: DateTime64(3)}
AND created_at < {end: DateTime64(3)}
GROUP BY project_id
`;
const rows = await queryClickhouse<{ project_id: string; count: string }>({
query,
params: {
start: convertDateToClickhouseDateTime(start),
end: convertDateToClickhouseDateTime(end),
},
});
return rows.map((row) => ({
projectId: row.project_id,
count: Number(row.count),
}));
};
export const getObservationCountOfProjectsSinceCreationDate = async ({
projectIds,
start,
}: {
projectIds: string[];
start: Date;
}) => {
const query = `
SELECT
count(*) as count
FROM observations
WHERE project_id IN ({projectIds: Array(String)})
AND created_at >= {start: DateTime64(3)}
`;
const rows = await queryClickhouse<{ count: string }>({
query,
params: {
projectIds,
start: convertDateToClickhouseDateTime(start),
},
});
return Number(rows[0]?.count ?? 0);
};
export const getTraceIdsForObservations = async (
projectId: string,
observationIds: string[],
) => {
const query = `
SELECT
trace_id,
id
FROM observations
WHERE project_id = {projectId: String}
AND id IN ({observationIds: Array(String)})
`;
const rows = await queryClickhouse<{ id: string; trace_id: string }>({
query,
params: {
projectId,
observationIds,
},
});
return rows.map((row) => ({
id: row.id,
traceId: row.trace_id,
}));
};
export const getGenerationsForPostHog = async function* (
projectId: string,
minTimestamp: Date,
maxTimestamp: Date,
) {
const query = `
SELECT
o.name as name,
o.start_time as start_time,
o.id as id,
o.total_cost as total_cost,
if(isNull(completion_start_time), NULL, date_diff('millisecond', start_time, completion_start_time)) as time_to_first_token,
o.usage_details['total'] as input_tokens,
o.usage_details['output'] as output_tokens,
o.cost_details['total'] as total_tokens,
o.project_id as project_id,
if(isNull(end_time), NULL, date_diff('millisecond', start_time, end_time) / 1000) as latency,
o.provided_model_name as model,
o.level as level,
o.version as version,
t.id as trace_id,
t.name as trace_name,
t.session_id as trace_session_id,
t.user_id as trace_user_id,
t.release as trace_release,
t.tags as trace_tags,
t.metadata['$posthog_session_id'] as posthog_session_id
FROM observations o FINAL
LEFT JOIN traces t FINAL ON o.trace_id = t.id AND o.project_id = t.project_id
WHERE o.project_id = {projectId: String}
AND t.project_id = {projectId: String}
AND o.start_time >= {minTimestamp: DateTime64(3)}
AND o.start_time <= {maxTimestamp: DateTime64(3)}
AND t.timestamp >= {minTimestamp: DateTime64(3)} - INTERVAL 7 DAY
AND t.timestamp <= {maxTimestamp: DateTime64(3)}
AND o.type = 'GENERATION'
`;
const records = queryClickhouseStream<Record<string, unknown>>({
query,
params: {
projectId,
minTimestamp: convertDateToClickhouseDateTime(minTimestamp),
maxTimestamp: convertDateToClickhouseDateTime(maxTimestamp),
},
});
const baseUrl = env.NEXTAUTH_URL?.replace("/api/auth", "");
for await (const record of records) {
yield {
timestamp: record.start_time,
langfuse_generation_name: record.name,
langfuse_trace_name: record.trace_name,
langfuse_url: `${baseUrl}/project/${projectId}/traces/${encodeURIComponent(record.trace_id as string)}?observation=${encodeURIComponent(record.id as string)}`,
langfuse_id: record.id,
langfuse_cost_usd: record.total_cost,
langfuse_input_units: record.input_tokens,
langfuse_output_units: record.output_tokens,
langfuse_total_units: record.total_tokens,
langfuse_session_id: record.trace_session_id,
langfuse_project_id: projectId,
langfuse_user_id: record.trace_user_id || "langfuse_unknown_user",
langfuse_latency: record.latency,
langfuse_time_to_first_token: record.time_to_first_token,
langfuse_release: record.trace_release,
langfuse_version: record.version,
langfuse_model: record.model,
langfuse_level: record.level,
langfuse_tags: record.trace_tags,
langfuse_event_version: "1.0.0",
$session_id: record.posthog_session_id ?? null,
$set: {
langfuse_user_url: record.user_id
? `${baseUrl}/project/${projectId}/users/${encodeURIComponent(record.user_id as string)}`
: null,
},
};
}
};
@@ -9,11 +9,13 @@ import Decimal from "decimal.js";
import { parseClickhouseUTCDateTimeFormat } from "./clickhouse";
import { ObservationRecordReadType } from "./definitions";
import { parseJsonPrioritised } from "../../utils/json";
import { jsonSchema } from "../../utils/zod";
export const convertObservationToView = (
record: ObservationRecordReadType,
): Omit<ObservationView, "inputPrice" | "outputPrice" | "totalPrice"> => {
): Omit<ObservationView, "inputPrice" | "outputPrice" | "totalPrice"> & {
usageDetails: Record<string, number>;
costDetails: Record<string, number>;
} => {
// these cost are not used from the view. They are in the select statement but not in the
// Prisma file. We will not clean this up but keep it as it is for now.
// eslint-disable-next-line no-unused-vars
@@ -25,10 +27,7 @@ export const convertObservationToView = (
? parseClickhouseUTCDateTimeFormat(record.end_time).getTime() -
parseClickhouseUTCDateTimeFormat(record.start_time).getTime()
: null,
timeToFirstToken: record.completion_start_time
? parseClickhouseUTCDateTimeFormat(record.start_time).getTime() -
parseClickhouseUTCDateTimeFormat(record.completion_start_time).getTime()
: null,
promptName: record.prompt_name ?? null,
promptVersion: record.prompt_version ?? null,
modelId: record.internal_model_id ?? null,
@@ -42,7 +41,15 @@ export const convertObservation = (
promptVersion: number | null;
latency: number | null;
timeToFirstToken: number | null;
usageDetails: Record<string, number>;
costDetails: Record<string, number>;
} => {
const reducedUsageDetails = reduceUsageOrCostDetails(record.usage_details);
const reducedCostDetails = reduceUsageOrCostDetails(record.cost_details);
const reducedProvidedCostDetails = reduceUsageOrCostDetails(
record.provided_cost_details,
);
return {
id: record.id,
traceId: record.trace_id ?? null,
@@ -54,15 +61,22 @@ export const convertObservation = (
? parseClickhouseUTCDateTimeFormat(record.end_time)
: null,
name: record.name ?? null,
metadata: record.metadata,
metadata:
record.metadata &&
Object.fromEntries(
Object.entries(record.metadata ?? {}).map(([key, val]) => [
key,
val && parseJsonPrioritised(val),
]),
),
level: record.level as ObservationLevel,
statusMessage: record.status_message ?? null,
version: record.version ?? null,
input: (record.input
? jsonSchema.parse(parseJsonPrioritised(record.input))
? parseJsonPrioritised(record.input)
: null) as Prisma.JsonValue | null,
output: (record.output
? jsonSchema.parse(parseJsonPrioritised(record.output))
? parseJsonPrioritised(record.output)
: null) as Prisma.JsonValue | null,
modelParameters: record.model_parameters
? JSON.parse(record.model_parameters)
@@ -73,31 +87,41 @@ export const convertObservation = (
promptId: record.prompt_id ?? null,
createdAt: parseClickhouseUTCDateTimeFormat(record.created_at),
updatedAt: parseClickhouseUTCDateTimeFormat(record.updated_at),
promptTokens: record.usage_details?.input
? Number(record.usage_details?.input)
: 0,
completionTokens: record.usage_details?.output
? Number(record.usage_details?.output)
: 0,
totalTokens: record.usage_details?.total
? Number(record.usage_details?.total)
: 0,
calculatedInputCost: record.cost_details?.input
? new Decimal(record.cost_details.input)
: null,
calculatedOutputCost: record.cost_details?.output
? new Decimal(record.cost_details.output)
: null,
promptTokens: reducedUsageDetails.input ?? 0,
completionTokens: reducedUsageDetails.output ?? 0,
totalTokens: reducedUsageDetails.total ?? 0,
calculatedInputCost:
reducedCostDetails.input != null
? new Decimal(reducedCostDetails.input)
: null,
calculatedOutputCost:
reducedCostDetails.output != null
? new Decimal(reducedCostDetails.output)
: null,
calculatedTotalCost: record.cost_details?.total
? new Decimal(record.cost_details.total)
: null,
inputCost: record.cost_details?.input
? new Decimal(record.cost_details?.input)
: null,
outputCost: record.cost_details?.output
? new Decimal(record.cost_details?.output)
: null,
inputCost:
reducedProvidedCostDetails.input != null
? new Decimal(reducedProvidedCostDetails.input)
: null,
outputCost:
reducedProvidedCostDetails.output != null
? new Decimal(reducedProvidedCostDetails.output)
: null,
totalCost: record.total_cost ? new Decimal(record.total_cost) : null,
usageDetails: Object.fromEntries(
Object.entries(record.usage_details ?? {}).map(([key, value]) => [
key,
Number(value),
]),
),
costDetails: Object.fromEntries(
Object.entries(record.cost_details ?? {}).map(([key, value]) => [
key,
Number(value),
]),
),
model: record.provided_model_name ?? null,
internalModelId: record.internal_model_id ?? null,
unit: "TOKENS", // to be removed.
@@ -108,8 +132,35 @@ export const convertObservation = (
parseClickhouseUTCDateTimeFormat(record.start_time).getTime()
: null,
timeToFirstToken: record.completion_start_time
? parseClickhouseUTCDateTimeFormat(record.start_time).getTime() -
parseClickhouseUTCDateTimeFormat(record.completion_start_time).getTime()
? (parseClickhouseUTCDateTimeFormat(
record.completion_start_time,
).getTime() -
parseClickhouseUTCDateTimeFormat(record.start_time).getTime()) /
1000
: null,
};
};
export const reduceUsageOrCostDetails = (
details: Record<string, number> | null | undefined,
): {
input: number | null;
output: number | null;
total: number | null;
} => {
return {
input: Object.entries(details ?? {})
.filter(([usageType]) => usageType.startsWith("input"))
.reduce(
(acc, [_, value]) => (acc ?? 0) + Number(value),
null as number | null, // default to null if no input usage is found
),
output: Object.entries(details ?? {})
.filter(([usageType]) => usageType.startsWith("output"))
.reduce(
(acc, [_, value]) => (acc ?? 0) + Number(value),
null as number | null, // default to null if no output usage is found
),
total: Number(details?.total ?? 0),
};
};
+208 -19
View File
@@ -3,10 +3,11 @@ import {
commandClickhouse,
parseClickhouseUTCDateTimeFormat,
queryClickhouse,
queryClickhouseStream,
upsertClickhouse,
} from "./clickhouse";
import { FilterList } from "../queries/clickhouse-sql/clickhouse-filter";
import { FilterState } from "../../types";
import { FilterCondition, FilterState, TimeFilter } from "../../types";
import {
createFilterFromFilterState,
getProjectIdDefaultFilter,
@@ -25,6 +26,7 @@ import {
import { SCORE_TO_TRACE_OBSERVATIONS_INTERVAL } from "./constants";
import { convertDateToClickhouseDateTime } from "../clickhouse/client";
import { ScoreRecordReadType } from "./definitions";
import { env } from "../../env";
export const searchExistingAnnotationScore = async (
projectId: string,
@@ -93,6 +95,32 @@ export const getScoreById = async (
return rows.map(convertToScore).shift();
};
export const getScoresByIds = async (
projectId: string,
scoreId: string[],
source?: ScoreSource,
) => {
const query = `
SELECT *
FROM scores s
WHERE s.project_id = {projectId: String}
AND s.id IN ({scoreId: Array(String)})
${source ? `AND s.source = {source: String}` : ""}
ORDER BY s.event_ts DESC
LIMIT 1 BY s.id, s.project_id
`;
const rows = await queryClickhouse<ScoreRecordReadType>({
query,
params: {
projectId,
scoreId,
...(source !== undefined ? { source } : {}),
},
});
return rows.map(convertToScore);
};
/**
* Accepts a score in a Clickhouse-ready format.
* id, project_id, name, and timestamp must always be provided.
@@ -108,13 +136,16 @@ export const upsertScore = async (score: Partial<ScoreRecordReadType>) => {
});
};
export const getScoresForTraces = async (
projectId: string,
traceIds: string[],
timestamp?: Date,
limit?: number,
offset?: number,
) => {
export type GetScoresForTracesProps = {
projectId: string;
traceIds: string[];
timestamp?: Date;
limit?: number;
offset?: number;
};
export const getScoresForTraces = async (props: GetScoresForTracesProps) => {
const { projectId, traceIds, timestamp, limit, offset } = props;
const query = `
select
*
@@ -173,20 +204,24 @@ export const getScoresForObservations = async (
return rows.map(convertToScore);
};
export const getScoresGroupedByNameSourceType = async (projectId: string) => {
export const getScoresGroupedByNameSourceType = async (
projectId: string,
timestamp: Date | undefined,
) => {
// We mainly use queries like this to retrieve filter options.
// Therefore, we can skip final as some inaccuracy in count is acceptable.
const query = `
select
name,
source,
data_type
from scores s
WHERE s.project_id = {projectId: String}
GROUP BY name, source, data_type
ORDER BY count() desc
LIMIT 1000;
`;
select
name,
source,
data_type
from scores s
WHERE s.project_id = {projectId: String}
${timestamp ? `AND s.timestamp >= {timestamp: DateTime64(3)}` : ""}
GROUP BY name, source, data_type
ORDER BY count() desc
LIMIT 1000;
`;
const rows = await queryClickhouse<{
name: string;
@@ -196,6 +231,9 @@ export const getScoresGroupedByNameSourceType = async (projectId: string) => {
query: query,
params: {
projectId: projectId,
...(timestamp
? { timestamp: convertDateToClickhouseDateTime(timestamp) }
: {}),
},
});
@@ -476,6 +514,25 @@ export const deleteScoresByTraceIds = async (
projectId,
traceIds,
},
clickhouseConfigs: {
request_timeout: 120_000, // 2 minutes
},
});
};
export const deleteScoresByProjectId = async (projectId: string) => {
const query = `
DELETE FROM scores
WHERE project_id = {projectId: String};
`;
await commandClickhouse({
query: query,
params: {
projectId,
},
clickhouseConfigs: {
request_timeout: 120_000, // 2 minutes
},
});
};
@@ -489,10 +546,14 @@ export const getNumericScoreHistogram = async (
);
const chFilterRes = chFilter.apply();
const traceFilter = chFilter.find((f) => f.clickhouseTable === "traces");
const query = `
select s.value
from scores s
${traceFilter ? `LEFT JOIN traces t ON s.trace_id = t.id AND t.project_id = s.project_id` : ""}
WHERE s.project_id = {projectId: String}
${traceFilter ? `AND t.project_id = {projectId: String}` : ""}
${chFilterRes?.query ? `AND ${chFilterRes.query}` : ""}
ORDER BY s.event_ts DESC
LIMIT 1 BY s.id, s.project_id
@@ -549,3 +610,131 @@ export const getAggregatedScoresForPrompts = async (
promptId: row.prompt_id,
}));
};
export const getScoreCountsByProjectInCreationInterval = async ({
start,
end,
}: {
start: Date;
end: Date;
}) => {
const query = `
SELECT
project_id,
count(*) as count
FROM scores
WHERE created_at >= {start: DateTime64(3)}
AND created_at < {end: DateTime64(3)}
GROUP BY project_id
`;
const rows = await queryClickhouse<{ project_id: string; count: string }>({
query,
params: {
start: convertDateToClickhouseDateTime(start),
end: convertDateToClickhouseDateTime(end),
},
});
return rows.map((row) => ({
projectId: row.project_id,
count: Number(row.count),
}));
};
export const getDistinctScoreNames = async (
projectId: string,
cutoffCreatedAt: Date,
filter: FilterState,
isTimestampFilter: (filter: FilterCondition) => filter is TimeFilter,
) => {
const scoreTimestampFilter = filter?.find(isTimestampFilter);
const query = `
SELECT DISTINCT
name
FROM scores s
WHERE s.project_id = {projectId: String}
AND s.created_at <= {cutoffCreatedAt: DateTime64(3)}
${scoreTimestampFilter ? `AND s.timestamp >= {filterTimestamp: DateTime64(3)}` : ""}
`;
const rows = await queryClickhouse<{ name: string }>({
query,
params: {
projectId,
cutoffCreatedAt: convertDateToClickhouseDateTime(cutoffCreatedAt),
...(scoreTimestampFilter
? {
filterTimestamp: convertDateToClickhouseDateTime(
scoreTimestampFilter.value,
),
}
: {}),
},
});
return rows.map((row) => row.name);
};
export const getScoresForPostHog = async function* (
projectId: string,
minTimestamp: Date,
maxTimestamp: Date,
) {
const query = `
SELECT
s.id as id,
s.timestamp as timestamp,
s.name as name,
s.value as value,
s.comment as comment,
t.name as trace_name,
t.session_id as trace_session_id,
t.user_id as trace_user_id,
t.release as trace_release,
t.tags as trace_tags,
t.metadata['$posthog_session_id'] as posthog_session_id
FROM scores s FINAL
LEFT JOIN traces t FINAL ON s.trace_id = t.id AND s.project_id = t.project_id
WHERE s.project_id = {projectId: String}
AND t.project_id = {projectId: String}
AND s.timestamp >= {minTimestamp: DateTime64(3)}
AND s.timestamp <= {maxTimestamp: DateTime64(3)}
AND t.timestamp >= {minTimestamp: DateTime64(3)} - INTERVAL 7 DAY
AND t.timestamp <= {maxTimestamp: DateTime64(3)}
`;
const records = queryClickhouseStream<Record<string, unknown>>({
query,
params: {
projectId,
minTimestamp: convertDateToClickhouseDateTime(minTimestamp),
maxTimestamp: convertDateToClickhouseDateTime(maxTimestamp),
},
});
const baseUrl = env.NEXTAUTH_URL?.replace("/api/auth", "");
for await (const record of records) {
yield {
timestamp: record.timestamp,
langfuse_score_name: record.name,
langfuse_score_value: record.value,
langfuse_score_comment: record.comment,
langfuse_trace_name: record.trace_name,
langfuse_id: record.id,
langfuse_session_id: record.trace_session_id,
langfuse_project_id: projectId,
langfuse_user_id: record.trace_user_id || "langfuse_unknown_user",
langfuse_release: record.trace_release,
langfuse_tags: record.trace_tags,
langfuse_event_version: "1.0.0",
$session_id: record.posthog_session_id ?? null,
$set: {
langfuse_user_url: record.user_id
? `${baseUrl}/project/${projectId}/users/${encodeURIComponent(record.user_id as string)}`
: null,
},
};
}
};
@@ -1,4 +1,4 @@
import { ScoreSource, ScoreDataType } from "@prisma/client";
import { ScoreSource, ScoreDataType, Score } from "@prisma/client";
import { ScoreRecordReadType } from "./definitions";
export type ScoreAggregation = {
@@ -11,7 +11,7 @@ export type ScoreAggregation = {
comment: string | null;
};
export const convertToScore = (row: ScoreRecordReadType) => {
export const convertToScore = (row: ScoreRecordReadType): Score => {
return {
id: row.id,
timestamp: new Date(row.timestamp),
@@ -0,0 +1,61 @@
import z from "zod";
import { prisma } from "../../db";
import { singleFilter, stringOptionsFilter } from "../../interfaces/filters";
export const getPublicSessionsFilter = async (
projectId: string,
filter: z.infer<typeof singleFilter>[],
) => {
const sessionsBookmarkedFilter = filter?.find((f) => f.column === "⭐️");
// we are only fetching bookmarked sessions.
// They need to be manipulated in the UI and should not be as many.
const filteredSessions = sessionsBookmarkedFilter
? await prisma.traceSession.findMany({
where: {
projectId: projectId,
bookmarked: true,
},
select: {
id: true,
createdAt: true,
bookmarked: true,
public: true,
},
})
: [];
const additionalBookmarkFilter: z.infer<typeof stringOptionsFilter>[] =
sessionsBookmarkedFilter &&
typeof sessionsBookmarkedFilter.value === "boolean" &&
((sessionsBookmarkedFilter.value === true &&
sessionsBookmarkedFilter.operator === "=") ||
(sessionsBookmarkedFilter.value === false &&
sessionsBookmarkedFilter.operator === "<>"))
? [
{
column: "id",
type: "stringOptions" as const,
operator: "any of" as const,
value: filteredSessions.map((s) => s.id),
},
]
: sessionsBookmarkedFilter &&
typeof sessionsBookmarkedFilter.value === "boolean" &&
(sessionsBookmarkedFilter.value === false ||
(sessionsBookmarkedFilter.value === true &&
sessionsBookmarkedFilter.operator === "<>"))
? [
{
column: "id",
type: "stringOptions" as const,
operator: "none of" as const,
value: filteredSessions.map((s) => s.id),
},
]
: [];
return filter
? [...filter.filter((f) => f.column !== "⭐️"), ...additionalBookmarkFilter]
: [...additionalBookmarkFilter];
};
+311 -237
View File
@@ -2,6 +2,7 @@ import {
commandClickhouse,
parseClickhouseUTCDateTimeFormat,
queryClickhouse,
queryClickhouseStream,
upsertClickhouse,
} from "./clickhouse";
import {
@@ -16,15 +17,15 @@ import {
} from "../queries/clickhouse-sql/clickhouse-filter";
import { TraceRecordReadType } from "./definitions";
import { tracesTableUiColumnDefinitions } from "../../tableDefinitions/mapTracesTable";
import { OrderByState } from "../../interfaces/orderBy";
import { orderByToClickhouseSql } from "../queries/clickhouse-sql/orderby-factory";
import { UiColumnMapping } from "../../tableDefinitions";
import { sessionCols } from "../../tableDefinitions/mapSessionTable";
import { convertDateToClickhouseDateTime } from "../clickhouse/client";
import { convertClickhouseToDomain } from "./traces_converters";
import { clickhouseSearchCondition } from "../queries/clickhouse-sql/search";
import { TRACE_TO_OBSERVATIONS_INTERVAL } from "./constants";
import { FetchTracesTableProps } from "../services/traces-ui-table-service";
import {
OBSERVATIONS_TO_TRACE_INTERVAL,
TRACE_TO_OBSERVATIONS_INTERVAL,
} from "./constants";
import { env } from "../../env";
export const checkTraceExists = async (
projectId: string,
@@ -36,6 +37,11 @@ export const checkTraceExists = async (
tracesPrefix: "t",
});
const timeStampFilter = tracesFilter.find(
(f) =>
f.field === "timestamp" && (f.operator === ">=" || f.operator === ">"),
) as DateTimeFilter | undefined;
tracesFilter.push(
...createFilterFromFilterState(filter, tracesTableUiColumnDefinitions),
new StringFilter({
@@ -46,19 +52,46 @@ export const checkTraceExists = async (
}),
);
const observationFilter = tracesFilter.find(
(f) => f.clickhouseTable === "observations",
);
const tracesFilterRes = tracesFilter.apply();
const observationFilterRes = observationFilter?.apply();
const query = `
SELECT id, project_id
FROM traces t FINAL
WITH observations_agg AS (
SELECT
multiIf(
arrayExists(x -> x = 'ERROR', groupArray(level)), 'ERROR',
arrayExists(x -> x = 'WARNING', groupArray(level)), 'WARNING',
arrayExists(x -> x = 'DEFAULT', groupArray(level)), 'DEFAULT',
'DEBUG'
) AS level,
trace_id,
project_id
FROM observations o FINAL
WHERE o.project_id = {projectId: String}
${timeStampFilter ? `AND o.start_time >= {traceTimestamp: DateTime64(3)} - ${OBSERVATIONS_TO_TRACE_INTERVAL}` : ""}
GROUP BY trace_id, project_id
)
SELECT
t.id as id,
t.project_id as project_id
FROM traces t FINAL
${observationFilterRes ? `INNER JOIN observations_agg o ON t.id = o.trace_id AND t.project_id = o.project_id` : ""}
WHERE ${tracesFilterRes.query}
AND t.project_id = {projectId: String}
${timestamp ? `AND timestamp >= {timestamp: DateTime64(3)} - ${TRACE_TO_OBSERVATIONS_INTERVAL}` : ""}
GROUP BY t.id, t.project_id
`;
const rows = await queryClickhouse<{ id: string; project_id: string }>({
query,
params: {
projectId,
...tracesFilterRes.params,
...(observationFilterRes ? observationFilterRes.params : {}),
...(timestamp
? { timestamp: convertDateToClickhouseDateTime(timestamp) }
: {}),
@@ -151,6 +184,37 @@ export const hasAnyTrace = async (projectId: string) => {
return rows.length > 0 && Number(rows[0].count) > 0;
};
export const getTraceCountsByProjectInCreationInterval = async ({
start,
end,
}: {
start: Date;
end: Date;
}) => {
const query = `
SELECT
project_id,
count(*) as count
FROM traces
WHERE created_at >= {start: DateTime64(3)}
AND created_at < {end: DateTime64(3)}
GROUP BY project_id
`;
const rows = await queryClickhouse<{ project_id: string; count: string }>({
query,
params: {
start: convertDateToClickhouseDateTime(start),
end: convertDateToClickhouseDateTime(end),
},
});
return rows.map((row) => ({
projectId: row.project_id,
count: Number(row.count),
}));
};
export const getTraceById = async (
traceId: string,
projectId: string,
@@ -317,186 +381,6 @@ export const getTracesGroupedByTags = async (props: GroupedTracesQueryProp) => {
return rows;
};
export type SessionDataReturnType = {
session_id: string;
max_timestamp: string;
min_timestamp: string;
trace_ids: string[];
user_ids: string[];
trace_count: number;
trace_tags: string[];
total_observations: number;
duration: number;
session_usage_details: Record<string, number>;
session_cost_details: Record<string, number>;
session_input_cost: string;
session_output_cost: string;
session_total_cost: string;
session_input_usage: string;
session_output_usage: string;
session_total_usage: string;
};
export const getSessionsTableCount = async (props: {
projectId: string;
filter: FilterState;
orderBy?: OrderByState;
limit?: number;
page?: number;
}) => {
const rows = await getSessionsTableGeneric<{ count: string }>({
select: `
count(session_id) as count
`,
projectId: props.projectId,
filter: props.filter,
orderBy: props.orderBy,
limit: props.limit,
page: props.page,
});
return rows.length > 0 ? Number(rows[0].count) : 0;
};
export const getSessionsTable = async (props: {
projectId: string;
filter: FilterState;
orderBy?: OrderByState;
limit?: number;
page?: number;
}) => {
const rows = await getSessionsTableGeneric<SessionDataReturnType>({
select: `
session_id,
max_timestamp,
min_timestamp,
trace_ids,
user_ids,
trace_count,
trace_tags,
total_observations,
duration,
session_usage_details,
session_cost_details,
session_input_cost,
session_output_cost,
session_total_cost,
session_input_usage,
session_output_usage,
session_total_usage
`,
projectId: props.projectId,
filter: props.filter,
orderBy: props.orderBy,
limit: props.limit,
page: props.page,
});
return rows;
};
const getSessionsTableGeneric = async <T>(props: FetchTracesTableProps) => {
const { select, projectId, filter, orderBy, limit, page } = props;
const { tracesFilter, scoresFilter, observationsFilter } =
getProjectIdDefaultFilter(projectId, { tracesPrefix: "s" });
tracesFilter.push(...createFilterFromFilterState(filter, sessionCols));
const tracesFilterRes = tracesFilter.apply();
const scoresAvgFilterRes = scoresFilter.apply();
const observationsStatsRes = observationsFilter.apply();
const traceTimestampFilter: DateTimeFilter | undefined = tracesFilter.find(
(f) =>
f.field === "min_timestamp" &&
(f.operator === ">=" || f.operator === ">"),
) as DateTimeFilter | undefined;
const singleTraceFilter = traceTimestampFilter
? new FilterList([
new DateTimeFilter({
clickhouseTable: "traces",
field: "timestamp",
operator: traceTimestampFilter.operator,
value: traceTimestampFilter.value,
}),
]).apply()
: undefined;
const query = `
WITH observations_agg AS (
SELECT o.trace_id,
count(*) as obs_count,
min(o.start_time) as min_start_time,
max(o.end_time) as max_end_time,
sumMap(usage_details) as sum_usage_details,
sumMap(cost_details) as sum_cost_details,
anyLast(project_id) as project_id
FROM observations o FINAL
WHERE o.project_id = {projectId: String}
${traceTimestampFilter ? `AND o.start_time >= {observationsStartTime: DateTime64(3)} - ${TRACE_TO_OBSERVATIONS_INTERVAL}` : ""}
GROUP BY o.trace_id
),
session_data AS (
SELECT
t.session_id,
anyLast(t.project_id) as project_id,
max(t.timestamp) as max_timestamp,
min(t.timestamp) as min_timestamp,
groupArray(t.id) AS trace_ids,
groupUniqArray(t.user_id) AS user_ids,
count(*) as trace_count,
groupUniqArrayArray(t.tags) as trace_tags,
-- Aggregate observations data at session level
sum(o.obs_count) as total_observations,
date_diff('milliseconds', min(min_start_time), max(max_end_time)) as duration,
sumMap(o.sum_usage_details) as session_usage_details,
sumMap(o.sum_cost_details) as session_cost_details,
sumMap(o.sum_cost_details)['input'] as session_input_cost,
sumMap(o.sum_cost_details)['output'] as session_output_cost,
sumMap(o.sum_cost_details)['total'] as session_total_cost,
sumMap(o.sum_usage_details)['input'] as session_input_usage,
sumMap(o.sum_usage_details)['output'] as session_output_usage,
sumMap(o.sum_usage_details)['total'] as session_total_usage
FROM traces t FINAL
LEFT JOIN observations_agg o
ON t.id = o.trace_id AND t.project_id = o.project_id
WHERE t.session_id IS NOT NULL
AND t.project_id = {projectId: String}
${singleTraceFilter?.query ? ` AND ${singleTraceFilter.query}` : ""}
GROUP BY t.session_id
)
SELECT ${select}
FROM session_data s
WHERE ${tracesFilterRes.query ? tracesFilterRes.query : ""}
${orderByToClickhouseSql(orderBy ?? null, sessionCols)}
${limit !== undefined && page !== undefined ? `LIMIT {limit: Int32} OFFSET {offset: Int32}` : ""}
`;
const obsStartTimeValue = traceTimestampFilter
? convertDateToClickhouseDateTime(traceTimestampFilter.value)
: null;
const res = await queryClickhouse<T>({
query: query,
params: {
projectId,
limit: limit,
offset: limit && page ? limit * page : 0,
...tracesFilterRes.params,
...observationsStatsRes.params,
...scoresAvgFilterRes.params,
...singleTraceFilter?.params,
...(obsStartTimeValue
? { observationsStartTime: obsStartTimeValue }
: {}),
},
});
return res;
};
export const getTracesIdentifierForSession = async (
projectId: string,
sessionId: string,
@@ -548,6 +432,25 @@ export const deleteTraces = async (projectId: string, traceIds: string[]) => {
projectId,
traceIds,
},
clickhouseConfigs: {
request_timeout: 120_000, // 2 minutes
},
});
};
export const deleteTracesByProjectId = async (projectId: string) => {
const query = `
DELETE FROM traces
WHERE project_id = {projectId: String};
`;
await commandClickhouse({
query: query,
params: {
projectId,
},
clickhouseConfigs: {
request_timeout: 120_000, // 2 minutes
},
});
};
@@ -585,68 +488,239 @@ export const getTotalUserCount = async (
});
};
export const getUserMetrics = async (projectId: string, userIds: string[]) => {
export const getUserMetrics = async (
projectId: string,
userIds: string[],
filter: FilterState,
) => {
if (userIds.length === 0) {
return [];
}
// filter state contains date range filter for traces so far.
const chFilter = new FilterList(
createFilterFromFilterState(filter, tracesTableUiColumnDefinitions),
);
const chFilterRes = chFilter.apply();
const timestampFilter = chFilter.find(
(f) => f.field === "timestamp" && f.operator === ">=",
);
// this query uses window functions on observations + traces to always get only the first row and thereby remove deduplicates
// we filter wherever possible by project id and user id
const query = `
WITH observations_agg AS (
SELECT o.trace_id,
count(*) as obs_count,
sumMap(usage_details) as sum_usage_details,
sum(total_cost) as sum_total_cost,
anyLast(project_id) as project_id
FROM observations o FINAL
WHERE o.project_id = {projectId: String}
GROUP BY o.trace_id
),
user_metric_data AS (
SELECT t.user_id,
max(t.timestamp) as max_timestamp,
min(t.timestamp) as min_timestamp,
count(*) as trace_count,
sum(o.obs_count) as total_observations,
sum(o.sum_total_cost) as session_total_cost,
sumMap(o.sum_usage_details)['input'] as session_input_usage,
sumMap(o.sum_usage_details)['output'] as session_output_usage,
sumMap(o.sum_usage_details)['total'] as session_total_usage
FROM traces t FINAL
LEFT JOIN observations_agg o
ON t.id = o.trace_id
AND t.project_id = o.project_id
WHERE t.user_id IS NOT NULL
AND t.user_id != ''
AND t.user_id IN ({userIds: Array(String)})
AND t.project_id = {projectId: String}
GROUP BY t.user_id
WITH stats as (
SELECT
t.user_id as user_id,
count(distinct o.id) as obs_count,
sumMap(usage_details) as sum_usage_details,
sum(total_cost) as sum_total_cost,
max(t.timestamp) as max_timestamp,
min(t.timestamp) as min_timestamp,
count(distinct t.id) as trace_count
FROM
(
SELECT
o.project_id,
o.trace_id,
o.usage_details,
o.total_cost,
id,
ROW_NUMBER() OVER (
PARTITION BY id
ORDER BY
event_ts DESC
) AS rn
FROM
observations o
WHERE
o.project_id = {projectId: String }
${timestampFilter ? `AND o.start_time >= {traceTimestamp: DateTime64(3)} - ${OBSERVATIONS_TO_TRACE_INTERVAL}` : ""}
AND o.trace_id in (
SELECT
distinct id
from
traces
where
user_id IN ({userIds: Array(String) })
AND project_id = {projectId: String }
${filter.length > 0 ? `AND ${chFilterRes.query}` : ""}
)
AND o.type = 'GENERATION'
) as o
JOIN (
SELECT
t.id,
t.user_id,
t.project_id,
t.timestamp,
ROW_NUMBER() OVER (
PARTITION BY id
ORDER BY
event_ts DESC
) AS rn
FROM
traces t
WHERE
t.user_id IN ({userIds: Array(String) })
AND t.project_id = {projectId: String }
${filter.length > 0 ? `AND ${chFilterRes.query}` : ""}
) as t on t.id = o.trace_id
and t.project_id = o.project_id
WHERE
o.rn = 1
and t.rn = 1
group by
t.user_id
)
SELECT user_id AS userId,
min_timestamp as firstTrace,
max_timestamp as lastTrace,
trace_count as totalTraces,
total_observations as totalObservations,
session_input_usage as totalPromptTokens,
session_output_usage as totalCompletionTokens,
session_total_usage as totalTokens,
session_total_cost as sumCalculatedTotalCost
FROM user_metric_data umd
SELECT
arraySum(mapValues(mapFilter(x -> positionCaseInsensitive(x.1, 'input') > 0, sum_usage_details))) as input_usage,
arraySum(mapValues(mapFilter(x -> positionCaseInsensitive(x.1, 'output') > 0, sum_usage_details))) as output_usage,
sum_usage_details [ 'total' ] as total_usage,
obs_count,
trace_count,
user_id,
sum_total_cost,
max_timestamp,
min_timestamp
FROM
stats
`;
return queryClickhouse<{
userId: string;
firstTrace: Date | null;
lastTrace: Date | null;
totalPromptTokens: bigint;
totalCompletionTokens: bigint;
totalTokens: bigint;
totalObservations: bigint;
totalTraces: bigint;
sumCalculatedTotalCost: number;
const rows = await queryClickhouse<{
user_id: string;
max_timestamp: string;
min_timestamp: string;
input_usage: string;
output_usage: string;
total_usage: string;
obs_count: string;
trace_count: string;
sum_total_cost: string;
}>({
query,
params: {
projectId,
userIds,
...chFilterRes.params,
...(timestampFilter
? {
traceTimestamp: convertDateToClickhouseDateTime(
(timestampFilter as DateTimeFilter).value,
),
}
: {}),
},
});
return rows.map((row) => ({
userId: row.user_id,
maxTimestamp: parseClickhouseUTCDateTimeFormat(row.max_timestamp),
minTimestamp: parseClickhouseUTCDateTimeFormat(row.min_timestamp),
inputUsage: Number(row.input_usage),
outputUsage: Number(row.output_usage),
totalUsage: Number(row.total_usage),
observationCount: Number(row.obs_count),
traceCount: Number(row.trace_count),
totalCost: Number(row.sum_total_cost),
}));
};
export const getTracesForPostHog = async function* (
projectId: string,
minTimestamp: Date,
maxTimestamp: Date,
) {
const query = `
WITH observations_agg AS (
SELECT o.project_id,
o.trace_id,
sum(total_cost) as total_cost,
count(*) as observation_count,
date_diff('millisecond', least(min(start_time), min(end_time)), greatest(max(start_time), max(end_time))) as latency_milliseconds
FROM observations o FINAL
WHERE o.project_id = {projectId: String}
AND o.start_time >= {minTimestamp: DateTime64(3)} - ${TRACE_TO_OBSERVATIONS_INTERVAL}
GROUP BY o.project_id, o.trace_id
)
SELECT
t.id as id,
t.timestamp as timestamp,
t.name as name,
t.session_id as session_id,
t.user_id as user_id,
t.release as release,
t.version as version,
t.tags as tags,
t.metadata['$posthog_session_id'] as posthog_session_id,
o.total_cost as total_cost,
o.latency_milliseconds / 1000 as latency,
o.observation_count as observation_count
FROM traces t FINAL
LEFT JOIN observations_agg o ON t.id = o.trace_id AND t.project_id = o.project_id
WHERE t.project_id = {projectId: String}
AND t.timestamp >= {minTimestamp: DateTime64(3)}
AND t.timestamp <= {maxTimestamp: DateTime64(3)}
`;
const records = queryClickhouseStream<Record<string, unknown>>({
query,
params: {
projectId,
minTimestamp: convertDateToClickhouseDateTime(minTimestamp),
maxTimestamp: convertDateToClickhouseDateTime(maxTimestamp),
},
});
const baseUrl = env.NEXTAUTH_URL?.replace("/api/auth", "");
for await (const record of records) {
yield {
timestamp: record.timestamp,
langfuse_id: record.id,
langfuse_trace_name: record.name,
langfuse_url: `${baseUrl}/project/${projectId}/traces/${encodeURIComponent(record.id as string)}`,
langfuse_cost_usd: record.total_cost,
langfuse_count_observations: record.observation_count,
langfuse_session_id: record.session_id,
langfuse_project_id: projectId,
langfuse_user_id: record.user_id || "langfuse_unknown_user",
langfuse_latency: record.latency,
langfuse_release: record.release,
langfuse_version: record.version,
langfuse_tags: record.tags,
langfuse_event_version: "1.0.0",
$session_id: record.posthog_session_id ?? null,
$set: {
langfuse_user_url: record.user_id
? `${baseUrl}/project/${projectId}/users/${encodeURIComponent(record.user_id as string)}`
: null,
},
};
}
};
export const getTracesByIdsForAnyProject = async (traceIds: string[]) => {
const query = `
SELECT id, project_id
FROM traces
WHERE id IN ({traceIds: Array(String)})
ORDER BY event_ts DESC
LIMIT 1 by id, project_id;`;
const records = await queryClickhouse<{
id: string;
project_id: string;
}>({
query,
params: {
traceIds,
},
});
return records.map((record) => ({
id: record.id,
projectId: record.project_id,
}));
};
@@ -1,10 +1,8 @@
import { ObservationLevel, Trace } from "@prisma/client";
import { Prisma, Trace } from "@prisma/client";
import { parseClickhouseUTCDateTimeFormat } from "./clickhouse";
import { TraceRecordReadType } from "./definitions";
import Decimal from "decimal.js";
import { ScoreAggregate } from "../../features/scores";
import { convertDateToClickhouseDateTime } from "../clickhouse/client";
import { TracesTableReturnType } from "../services/traces-ui-table-service";
import { parseJsonPrioritised } from "../../utils/json";
export const convertTraceDomainToClickhouse = (
trace: Trace,
@@ -46,61 +44,22 @@ export const convertClickhouseToDomain = (
userId: record.user_id ?? null,
sessionId: record.session_id ?? null,
public: record.public,
input: record.input ?? null,
output: record.output ?? null,
metadata: record.metadata,
input: (record.input
? parseJsonPrioritised(record.input)
: null) as Prisma.JsonValue | null,
output: (record.output
? parseJsonPrioritised(record.output)
: null) as Prisma.JsonValue | null,
metadata:
record.metadata &&
Object.fromEntries(
Object.entries(record.metadata ?? {}).map(([key, val]) => [
key,
val && parseJsonPrioritised(val),
]),
),
createdAt: parseClickhouseUTCDateTimeFormat(record.created_at),
updatedAt: parseClickhouseUTCDateTimeFormat(record.updated_at),
externalId: null,
};
};
export type TracesAllReturnType = {
id: string;
timestamp: Date;
name: string | null;
projectId: string;
userId: string | null;
release: string | null;
version: string | null;
public: boolean;
bookmarked: boolean;
sessionId: string | null;
tags: string[];
};
export const convertToDomain = (row: TracesTableReturnType) => {
return {
id: row.id,
projectId: row.project_id,
timestamp: parseClickhouseUTCDateTimeFormat(row.timestamp),
tags: row.tags,
bookmarked: row.bookmarked,
name: row.name ?? null,
release: row.release ?? null,
version: row.version ?? null,
userId: row.user_id ?? null,
sessionId: row.session_id ?? null,
latency: Number(row.latency),
usageDetails: row.usage_details,
costDetails: row.cost_details,
level: row.level,
observationCount: Number(row.observation_count),
scoresAvg: row.scores_avg,
public: row.public,
};
};
export type TracesMetricsReturnType = {
id: string;
promptTokens: bigint;
completionTokens: bigint;
totalTokens: bigint;
latency: number | null;
level: ObservationLevel;
observationCount: bigint;
calculatedTotalCost: Decimal | null;
calculatedInputCost: Decimal | null;
calculatedOutputCost: Decimal | null;
scores: ScoreAggregate;
};
@@ -30,7 +30,7 @@ export interface StorageService {
download(path: string): Promise<string>;
listFiles(prefix: string): Promise<string[]>;
listFiles(prefix: string): Promise<{ file: string; createdAt: Date }[]>;
getSignedUrl(
fileName: string,
@@ -201,7 +201,9 @@ class AzureBlobStorageService implements StorageService {
}
}
public async listFiles(prefix: string): Promise<string[]> {
public async listFiles(
prefix: string,
): Promise<{ file: string; createdAt: Date }[]> {
try {
await this.createContainerIfNotExists();
@@ -209,7 +211,10 @@ class AzureBlobStorageService implements StorageService {
const files = [];
for await (const blob of result) {
if (blob.name.startsWith(prefix)) {
files.push(blob.name);
files.push({
file: blob.name,
createdAt: blob?.properties?.createdOn ?? new Date(),
});
}
}
return files;
@@ -364,7 +369,9 @@ class S3StorageService implements StorageService {
}
}
public async listFiles(prefix: string): Promise<string[]> {
public async listFiles(
prefix: string,
): Promise<{ file: string; createdAt: Date }[]> {
const listCommand = new ListObjectsV2Command({
Bucket: this.bucketName,
Prefix: prefix,
@@ -373,7 +380,11 @@ class S3StorageService implements StorageService {
try {
const response = await this.client.send(listCommand);
return (
response.Contents?.flatMap((file) => (file.Key ? [file.Key] : [])) ?? []
response.Contents?.flatMap((file) =>
file.Key
? [{ file: file.Key, createdAt: file.LastModified ?? new Date() }]
: [],
) ?? []
);
} catch (err) {
logger.error(`Failed to list files from S3 ${prefix}`, err);
@@ -0,0 +1,319 @@
import { OrderByState } from "../../interfaces/orderBy";
import { sessionCols } from "../../tableDefinitions/mapSessionTable";
import { FilterState } from "../../types";
import { convertDateToClickhouseDateTime } from "../clickhouse/client";
import { DateTimeFilter, FilterList, orderByToClickhouseSql } from "../queries";
import {
getProjectIdDefaultFilter,
createFilterFromFilterState,
} from "../queries/clickhouse-sql/factory";
import {
TRACE_TO_OBSERVATIONS_INTERVAL,
queryClickhouse,
} from "../repositories";
export type SessionDataReturnType = {
session_id: string;
max_timestamp: string;
min_timestamp: string;
trace_ids: string[];
user_ids: string[];
trace_count: number;
trace_tags: string[];
};
export type SessionWithMetricsReturnType = SessionDataReturnType & {
total_observations: number;
duration: number;
session_usage_details: Record<string, number>;
session_cost_details: Record<string, number>;
session_input_cost: string;
session_output_cost: string;
session_total_cost: string;
session_input_usage: string;
session_output_usage: string;
session_total_usage: string;
};
export const getSessionsTableCount = async (props: {
projectId: string;
filter: FilterState;
orderBy?: OrderByState;
limit?: number;
page?: number;
}) => {
const rows = await getSessionsTableGeneric<{ count: string }>({
select: "count",
projectId: props.projectId,
filter: props.filter,
orderBy: props.orderBy,
limit: props.limit,
page: props.page,
});
return rows.length > 0 ? Number(rows[0].count) : 0;
};
export const getSessionsTable = async (props: {
projectId: string;
filter: FilterState;
orderBy?: OrderByState;
limit?: number;
page?: number;
}) => {
const rows = await getSessionsTableGeneric<SessionDataReturnType>({
select: "rows",
projectId: props.projectId,
filter: props.filter,
orderBy: props.orderBy,
limit: props.limit,
page: props.page,
});
return rows.map((row) => ({
...row,
trace_count: Number(row.trace_count),
}));
};
export const getSessionsWithMetrics = async (props: {
projectId: string;
filter: FilterState;
orderBy?: OrderByState;
limit?: number;
page?: number;
}) => {
const rows = await getSessionsTableGeneric<SessionWithMetricsReturnType>({
select: "metrics",
projectId: props.projectId,
filter: props.filter,
orderBy: props.orderBy,
limit: props.limit,
page: props.page,
});
return rows.map((row) => ({
...row,
trace_count: Number(row.trace_count),
total_observations: Number(row.total_observations),
}));
};
export type FetchSessionsTableProps = {
select: "count" | "rows" | "metrics";
projectId: string;
filter: FilterState;
searchQuery?: string;
orderBy?: OrderByState;
limit?: number;
page?: number;
};
const getSessionsTableGeneric = async <T>(props: FetchSessionsTableProps) => {
const { select, projectId, filter, orderBy, limit, page } = props;
let sqlSelect: string;
switch (select) {
case "count":
sqlSelect = "count(session_id) as count";
break;
case "rows":
sqlSelect = `
session_id,
max_timestamp,
min_timestamp,
trace_ids,
user_ids,
trace_count,
trace_tags`;
break;
case "metrics":
sqlSelect = `
session_id,
max_timestamp,
min_timestamp,
trace_ids,
user_ids,
trace_count,
trace_tags,
total_observations,
duration,
session_usage_details,
session_cost_details,
session_input_cost,
session_output_cost,
session_total_cost,
session_input_usage,
session_output_usage,
session_total_usage`;
break;
default:
const exhaustiveCheckDefault: never = select;
throw new Error(`Unknown select type: ${select}`);
}
const { tracesFilter } = getProjectIdDefaultFilter(projectId, {
tracesPrefix: "s",
});
tracesFilter.push(...createFilterFromFilterState(filter, sessionCols));
const tracesFilterRes = tracesFilter.apply();
const traceTimestampFilter: DateTimeFilter | undefined = tracesFilter.find(
(f) =>
f.field === "min_timestamp" &&
(f.operator === ">=" || f.operator === ">"),
) as DateTimeFilter | undefined;
const filters = [];
if (traceTimestampFilter) {
filters.push(
new DateTimeFilter({
clickhouseTable: "traces",
field: "timestamp",
operator: traceTimestampFilter.operator,
value: traceTimestampFilter.value,
}),
);
}
const additionalSingleTraceFilter = tracesFilter.find(
(f) => f.field === "bookmarked" || f.field === "session_id",
);
if (additionalSingleTraceFilter) {
filters.push(additionalSingleTraceFilter);
}
const singleTraceFilter =
filters.length > 0 ? new FilterList(filters).apply() : undefined;
const hasMetricsFilter =
tracesFilter.find((f) =>
[
"session_total_cost",
"session_input_cost",
"session_output_cost",
"duration",
"session_total_usage",
"session_output_usage",
"session_input_usage",
].includes(f.field),
) ||
(orderBy &&
[
"totalCost",
"inputCost",
"outputCost",
"sessionDuration",
"totalTokens",
"outputTokens",
"inputTokens",
"usage",
].includes(orderBy?.column));
const selectMetrics = select === "metrics" || hasMetricsFilter;
// We use deduplicated traces and observations CTEs instead of final to be able to use Skip indices in Clickhouse.
const query = `
WITH deduplicated_traces AS (
SELECT * EXCEPT input, output, metadata
FROM traces t
WHERE t.session_id IS NOT NULL
AND t.project_id = {projectId: String}
${singleTraceFilter?.query ? ` AND ${singleTraceFilter.query}` : ""}
ORDER BY event_ts DESC
LIMIT 1 BY id, project_id
),
deduplicated_observations AS (
SELECT *
FROM observations o
WHERE o.project_id = {projectId: String}
${traceTimestampFilter ? `AND o.start_time >= {observationsStartTime: DateTime64(3)} - ${TRACE_TO_OBSERVATIONS_INTERVAL}` : ""}
AND o.trace_id IN (
SELECT id
FROM deduplicated_traces
)
ORDER BY event_ts DESC
LIMIT 1 BY id, project_id
),
observations_agg AS (
SELECT o.trace_id,
count(*) as obs_count,
min(o.start_time) as min_start_time,
max(o.end_time) as max_end_time,
sumMap(usage_details) as sum_usage_details,
sumMap(cost_details) as sum_cost_details,
anyLast(project_id) as project_id
FROM deduplicated_observations o
WHERE o.project_id = {projectId: String}
${traceTimestampFilter ? `AND o.start_time >= {observationsStartTime: DateTime64(3)} - ${TRACE_TO_OBSERVATIONS_INTERVAL}` : ""}
GROUP BY o.trace_id
),
session_data AS (
SELECT
t.session_id,
anyLast(t.project_id) as project_id,
max(t.timestamp) as max_timestamp,
min(t.timestamp) as min_timestamp,
groupArray(t.id) AS trace_ids,
groupUniqArray(t.user_id) AS user_ids,
count(*) as trace_count,
groupUniqArrayArray(t.tags) as trace_tags
-- Aggregate observations data at session level
${
selectMetrics
? `
,
sum(o.obs_count) as total_observations,
date_diff('millisecond', min(min_start_time), max(max_end_time)) as duration,
sumMap(o.sum_usage_details) as session_usage_details,
sumMap(o.sum_cost_details) as session_cost_details,
arraySum(mapValues(mapFilter(x -> positionCaseInsensitive(x.1, 'input') > 0, sumMap(o.sum_cost_details)))) as session_input_cost,
arraySum(mapValues(mapFilter(x -> positionCaseInsensitive(x.1, 'output') > 0, sumMap(o.sum_cost_details)))) as session_output_cost,
sumMap(o.sum_cost_details)['total'] as session_total_cost,
arraySum(mapValues(mapFilter(x -> positionCaseInsensitive(x.1, 'input') > 0, sumMap(o.sum_usage_details)))) as session_input_usage,
arraySum(mapValues(mapFilter(x -> positionCaseInsensitive(x.1, 'output') > 0, sumMap(o.sum_usage_details)))) as session_output_usage,
sumMap(o.sum_usage_details)['total'] as session_total_usage`
: ""
}
FROM deduplicated_traces t
${
selectMetrics
? `LEFT JOIN observations_agg o
ON t.id = o.trace_id AND t.project_id = o.project_id`
: ""
}
WHERE t.session_id IS NOT NULL
AND t.project_id = {projectId: String}
${singleTraceFilter?.query ? ` AND ${singleTraceFilter.query}` : ""}
GROUP BY t.session_id
)
SELECT ${sqlSelect}
FROM session_data s
WHERE ${tracesFilterRes.query ? tracesFilterRes.query : ""}
${orderByToClickhouseSql(orderBy ?? null, sessionCols)}
${limit !== undefined && page !== undefined ? `LIMIT {limit: Int32} OFFSET {offset: Int32}` : ""}
`;
const obsStartTimeValue = traceTimestampFilter
? convertDateToClickhouseDateTime(traceTimestampFilter.value)
: null;
const res = await queryClickhouse<T>({
query: query,
params: {
projectId,
limit: limit,
offset: limit && page ? limit * page : 0,
...tracesFilterRes.params,
...singleTraceFilter?.params,
...(obsStartTimeValue
? { observationsStartTime: obsStartTimeValue }
: {}),
},
});
return res;
};
@@ -13,9 +13,18 @@ import {
} from "../queries/clickhouse-sql/factory";
import { orderByToClickhouseSql } from "../queries/clickhouse-sql/orderby-factory";
import { clickhouseSearchCondition } from "../queries/clickhouse-sql/search";
import { convertToDomain } from "../repositories";
import { queryClickhouse } from "../repositories/clickhouse";
import {
parseClickhouseUTCDateTimeFormat,
queryClickhouse,
} from "../repositories/clickhouse";
import { TraceRecordReadType } from "../repositories/definitions";
import {
OBSERVATIONS_TO_TRACE_INTERVAL,
SCORE_TO_TRACE_OBSERVATIONS_INTERVAL,
} from "../repositories/constants";
import Decimal from "decimal.js";
import { ScoreAggregate } from "../../features/scores";
import { reduceUsageOrCostDetails } from "../repositories";
export type TracesTableReturnType = Pick<
TraceRecordReadType,
@@ -30,7 +39,99 @@ export type TracesTableReturnType = Pick<
| "session_id"
| "tags"
| "public"
> & {
>;
export type TracesAllUiReturnType = {
id: string;
timestamp: Date;
name: string | null;
projectId: string;
userId: string | null;
release: string | null;
version: string | null;
public: boolean;
bookmarked: boolean;
sessionId: string | null;
tags: string[];
};
export type TracesMetricsUiReturnType = {
id: string;
projectId: string;
promptTokens: bigint;
completionTokens: bigint;
totalTokens: bigint;
latency: number | null;
level: ObservationLevel;
observationCount: bigint;
calculatedTotalCost: Decimal | null;
calculatedInputCost: Decimal | null;
calculatedOutputCost: Decimal | null;
scores: ScoreAggregate;
usageDetails: Record<string, number>;
costDetails: Record<string, number>;
};
export const convertToUiTableRows = (
row: TracesTableReturnType,
): TracesAllUiReturnType => {
return {
id: row.id,
projectId: row.project_id,
timestamp: parseClickhouseUTCDateTimeFormat(row.timestamp),
tags: row.tags,
bookmarked: row.bookmarked,
name: row.name ?? null,
release: row.release ?? null,
version: row.version ?? null,
userId: row.user_id ?? null,
sessionId: row.session_id ?? null,
public: row.public,
};
};
export const convertToUITableMetrics = (
row: TracesTableMetricsClickhouseReturnType,
): Omit<TracesMetricsUiReturnType, "scores"> => {
const usageDetails = reduceUsageOrCostDetails(row.usage_details);
return {
id: row.id,
projectId: row.project_id,
latency: Number(row.latency),
promptTokens: BigInt(usageDetails.input ?? 0),
completionTokens: BigInt(usageDetails.output ?? 0),
totalTokens: BigInt(usageDetails.total ?? 0),
usageDetails: Object.fromEntries(
Object.entries(row.usage_details).map(([key, value]) => [
key,
Number(value),
]),
),
costDetails: Object.fromEntries(
Object.entries(row.cost_details).map(([key, value]) => [
key,
Number(value),
]),
),
observationCount: BigInt(row.observation_count ?? 0),
calculatedTotalCost: row.cost_details?.total
? new Decimal(row.cost_details.total)
: null,
calculatedInputCost: row.cost_details?.input
? new Decimal(row.cost_details.input)
: null,
calculatedOutputCost: row.cost_details?.output
? new Decimal(row.cost_details.output)
: null,
level: row.level,
};
};
export type TracesTableMetricsClickhouseReturnType = {
id: string;
project_id: string;
timestamp: Date;
level: ObservationLevel;
observation_count: number | null;
latency: string | null;
@@ -40,7 +141,7 @@ export type TracesTableReturnType = Pick<
};
export type FetchTracesTableProps = {
select: string;
select: "count" | "rows" | "metrics";
projectId: string;
filter: FilterState;
searchQuery?: string;
@@ -58,7 +159,7 @@ export const getTracesTableCount = async (props: {
page?: number;
}) => {
const countRows = await getTracesTableGeneric<{ count: string }>({
select: "count(*) as count",
select: "count",
...props,
});
@@ -69,6 +170,23 @@ export const getTracesTableCount = async (props: {
return converted.length > 0 ? converted[0].count : 0;
};
export const getTracesTableMetrics = async (props: {
projectId: string;
filter: FilterState;
searchQuery?: string;
orderBy?: OrderByState;
limit?: number;
page?: number;
}): Promise<Array<Omit<TracesMetricsUiReturnType, "scores">>> => {
const countRows =
await getTracesTableGeneric<TracesTableMetricsClickhouseReturnType>({
select: "metrics",
...props,
});
return countRows.map(convertToUITableMetrics);
};
export const getTracesTable = async (
projectId: string,
filter: FilterState,
@@ -78,24 +196,7 @@ export const getTracesTable = async (
page?: number,
) => {
const rows = await getTracesTableGeneric<TracesTableReturnType>({
select: `
t.id,
t.project_id as project_id,
t.timestamp,
t.tags,
t.bookmarked,
t.name,
t.release,
t.version,
t.user_id,
t.session_id,
os.latency_milliseconds / 1000 as latency,
os.cost_details as cost_details,
os.usage_details as usage_details,
os.level as level,
os.observation_count as observation_count,
s.scores_avg as scores_avg,
t.public`,
select: "rows",
projectId,
filter,
searchQuery,
@@ -104,13 +205,50 @@ export const getTracesTable = async (
page,
});
return rows.map(convertToDomain);
return rows.map(convertToUiTableRows);
};
const getTracesTableGeneric = async <T>(props: FetchTracesTableProps) => {
const { select, projectId, filter, orderBy, limit, page, searchQuery } =
props;
let sqlSelect: string;
switch (select) {
case "count":
sqlSelect = "count(*) as count";
break;
case "metrics":
sqlSelect = `
t.id as id,
t.project_id as project_id,
t.timestamp as timestamp,
os.latency_milliseconds / 1000 as latency,
os.cost_details as cost_details,
os.usage_details as usage_details,
os.level as level,
os.observation_count as observation_count,
s.scores_avg as scores_avg,
t.public as public`;
break;
case "rows":
sqlSelect = `
t.id as id,
t.project_id as project_id,
t.timestamp as timestamp,
t.tags as tags,
t.bookmarked as bookmarked,
t.name as name,
t.release as release,
t.version as version,
t.user_id as user_id,
t.session_id as session_id,
t.public as public`;
break;
default:
const exhaustiveCheckDefault: never = select;
throw new Error(`Unknown select type: ${select}`);
}
const { tracesFilter, scoresFilter, observationsFilter } =
getProjectIdDefaultFilter(projectId, { tracesPrefix: "t" });
@@ -156,27 +294,20 @@ const getTracesTableGeneric = async <T>(props: FetchTracesTableProps) => {
f.field === "timestamp" && (f.operator === ">=" || f.operator === ">"),
) as DateTimeFilter | undefined;
timeStampFilter
? scoresFilter.push(
new DateTimeFilter({
clickhouseTable: "scores",
field: "timestamp",
operator: ">=",
value: timeStampFilter.value,
}),
)
: null;
const requiresScoresJoin =
tracesFilter.find((f) => f.clickhouseTable === "scores") !== undefined ||
tracesTableUiColumnDefinitions.find(
(c) =>
c.uiTableName === orderBy?.column || c.uiTableId === orderBy?.column,
)?.clickhouseTableName === "scores";
timeStampFilter
? observationsFilter.push(
new DateTimeFilter({
clickhouseTable: "observations",
field: "start_time",
operator: ">=",
value: timeStampFilter.value,
}),
)
: null;
const requiresObservationsJoin =
tracesFilter.find((f) => f.clickhouseTable === "observations") !==
undefined ||
tracesTableUiColumnDefinitions.find(
(c) =>
c.uiTableName === orderBy?.column || c.uiTableId === orderBy?.column,
)?.clickhouseTableName === "observations";
const tracesFilterRes = tracesFilter.apply();
const scoresFilterRes = scoresFilter.apply();
@@ -184,13 +315,52 @@ const getTracesTableGeneric = async <T>(props: FetchTracesTableProps) => {
const search = clickhouseSearchCondition(searchQuery);
const defaultOrder = orderBy?.order && orderBy?.column === "timestamp";
const orderByCols = [
...tracesTableUiColumnDefinitions,
{
clickhouseSelect: "toDate(t.timestamp)",
uiTableName: "timestamp_to_date",
uiTableId: "timestamp_to_date",
clickhouseTableName: "traces",
},
{
clickhouseSelect: "t.event_ts",
uiTableName: "event_ts",
uiTableId: "event_ts",
clickhouseTableName: "traces",
},
];
const chOrderBy = orderByToClickhouseSql(
[
defaultOrder
? [
{
column: "timestamp_to_date",
order: orderBy.order,
},
{ column: "event_ts", order: "DESC" as "DESC" },
]
: null,
orderBy ?? null,
].flat(),
orderByCols,
);
// complex query ahead:
// - we only join scores and observations if we really need them to speed up default views
// - we use FINAL on traces only in case we not need to order by something different than time. Otherwise we cannot guarantee correct reads.
// - we filter the observations and scores as much as possible before joining them to traces.
// - we order by todate(timestamp), event_ts desc per default and do not use FINAL.
// In this case, CH is able to read the data only from the latest date from disk and filtering them in memory. No need to read all data e.g. for 1 month from disk.
const query = `
WITH observations_stats AS (
SELECT
COUNT(*) AS observation_count,
sumMap(usage_details) as usage_details,
SUM(total_cost) AS total_cost,
date_diff('milliseconds', least(min(start_time), min(end_time)), greatest(max(start_time), max(end_time))) as latency_milliseconds,
date_diff('millisecond', least(min(start_time), min(end_time)), greatest(max(start_time), max(end_time))) as latency_milliseconds,
multiIf(
arrayExists(x -> x = 'ERROR', groupArray(level)), 'ERROR',
arrayExists(x -> x = 'WARNING', groupArray(level)), 'WARNING',
@@ -200,8 +370,10 @@ const getTracesTableGeneric = async <T>(props: FetchTracesTableProps) => {
sumMap(cost_details) as cost_details,
trace_id,
project_id
FROM observations FINAL
WHERE ${observationFilterRes.query}
FROM observations o FINAL
WHERE o.project_id = {projectId: String}
${timeStampFilter ? `AND o.start_time >= {traceTimestamp: DateTime64(3)} - ${OBSERVATIONS_TO_TRACE_INTERVAL}` : ""}
${observationsFilter ? `AND ${observationFilterRes.query}` : ""}
GROUP BY trace_id, project_id
),
scores_avg AS (
@@ -214,21 +386,28 @@ const getTracesTableGeneric = async <T>(props: FetchTracesTableProps) => {
trace_id,
name,
avg(value) avg_value
FROM scores final
WHERE ${scoresFilterRes.query}
FROM scores s FINAL
WHERE project_id = {projectId: String}
${timeStampFilter ? `AND s.timestamp >= {traceTimestamp: DateTime64(3)} - ${SCORE_TO_TRACE_OBSERVATIONS_INTERVAL}` : ""}
${scoresFilterRes ? `AND ${scoresFilterRes.query}` : ""}
GROUP BY project_id,
trace_id,
name
) tmp
GROUP BY project_id, trace_id
)
SELECT ${select}
FROM traces t final
LEFT JOIN observations_stats os on os.project_id = t.project_id and os.trace_id = t.id
LEFT JOIN scores_avg s on s.project_id = t.project_id and s.trace_id = t.id
WHERE ${tracesFilterRes.query}
SELECT ${sqlSelect}
-- FINAL is used for non default ordering and count.
FROM traces t ${["metrics", "rows"].includes(select) && defaultOrder ? "" : "FINAL"}
${select === "metrics" || requiresObservationsJoin ? `LEFT JOIN observations_stats os on os.project_id = t.project_id and os.trace_id = t.id` : ""}
${select === "metrics" || requiresScoresJoin ? `LEFT JOIN scores_avg s on s.project_id = t.project_id and s.trace_id = t.id` : ""}
WHERE t.project_id = {projectId: String}
${tracesFilterRes ? `AND ${tracesFilterRes.query}` : ""}
${search.query}
${orderByToClickhouseSql(orderBy ?? null, tracesTableUiColumnDefinitions)}
${chOrderBy}
-- This is used for metrics and row queries. Count has only one result.
-- This is only used for default ordering. Otherwise, we use final.
${["metrics", "rows"].includes(select) && defaultOrder ? "LIMIT 1 BY id, project_id" : ""}
${limit !== undefined && page !== undefined ? `LIMIT {limit: Int32} OFFSET {offset: Int32}` : ""}
`;
@@ -237,6 +416,8 @@ const getTracesTableGeneric = async <T>(props: FetchTracesTableProps) => {
params: {
limit: limit,
offset: limit && page ? limit * page : 0,
traceTimestamp: timeStampFilter?.value.getTime(),
projectId: projectId,
...tracesFilterRes.params,
...observationFilterRes.params,
...scoresFilterRes.params,
@@ -0,0 +1,32 @@
import { clickhouseClient } from "../clickhouse/client";
import {
TraceRecordInsertType,
ObservationRecordInsertType,
ScoreRecordInsertType,
} from "../repositories/definitions";
export const createTracesCh = async (trace: TraceRecordInsertType[]) => {
return await clickhouseClient().insert({
table: "traces",
format: "JSONEachRow",
values: trace,
});
};
export const createObservationsCh = async (
observations: ObservationRecordInsertType[],
) => {
return await clickhouseClient().insert({
table: "observations",
format: "JSONEachRow",
values: observations,
});
};
export const createScoresCh = async (scores: ScoreRecordInsertType[]) => {
return await clickhouseClient().insert({
table: "scores",
format: "JSONEachRow",
values: scores,
});
};
@@ -0,0 +1,3 @@
export * from "./tracing-factory";
export * from "./clickhouse-helpers";
export * from "./org-factory";
@@ -0,0 +1,50 @@
import { v4 } from "uuid";
import { prisma } from "../../db";
import { hashSecretKey, getDisplaySecretKey } from "../auth/apiKeys";
export function createBasicAuthHeader(
username: string,
password: string,
): string {
const base64Credentials = Buffer.from(`${username}:${password}`).toString(
"base64",
);
return `Basic ${base64Credentials}`;
}
export type CreateOrgProjectAndApiKeyOptions = {
projectId?: string;
};
export const createOrgProjectAndApiKey = async (
props?: CreateOrgProjectAndApiKeyOptions,
) => {
const projectId = props?.projectId ?? v4();
const org = await prisma.organization.create({
data: {
id: v4(),
name: v4(),
},
});
await prisma.project.create({
data: {
id: projectId,
name: v4(),
orgId: org.id,
},
});
const publicKey = v4();
const secretKey = v4();
const auth = createBasicAuthHeader(publicKey, secretKey);
await prisma.apiKey.create({
data: {
id: v4(),
projectId: projectId,
publicKey: publicKey,
hashedSecretKey: await hashSecretKey(secretKey),
displaySecretKey: getDisplaySecretKey(secretKey),
},
});
return { projectId, publicKey, secretKey, auth };
};
@@ -1,9 +1,9 @@
import {
type ScoreRecordInsertType,
type ObservationRecordInsertType,
type TraceRecordInsertType,
} from "@langfuse/shared/src/server";
import { v4 } from "uuid";
import {
TraceRecordInsertType,
ObservationRecordInsertType,
ScoreRecordInsertType,
} from "../repositories/definitions";
export const createTrace = (trace: Partial<TraceRecordInsertType>) => {
return {
@@ -12,7 +12,7 @@ export function transformStreamToCsv(): Transform {
transform(
row: Record<string, any>,
encoding: BufferEncoding,
callback: TransformCallback
callback: TransformCallback,
): void {
if (isFirstChunk) {
// Extract headers from the first object
@@ -23,7 +23,7 @@ export function transformStreamToCsv(): Transform {
// Convert the object to a CSV line and push it
const csvRow = headers.map((header) => {
const field = row[header];
const field = row[header] ?? "";
let str = stringify(field);
// escape and format fields that contain commas

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