Compare commits

...
266 Commits
Author SHA1 Message Date
steffen911 fae508d820 chore: release v3.45.2 2025-03-26 09:59:47 +01:00
Steffen SchmitzandGitHub a6e581d349 fix: use correct column name for filtering traces bar chart (#6215) 2025-03-26 08:56:09 +00:00
Max DeichmannandGitHub 9e77738a2f fix: fix by model charts (#6208)
* fix

* push

* push

* push
2025-03-25 20:56:00 +00:00
Marc Klingen d15c0a27f5 chore: release v3.45.1 2025-03-25 19:49:44 +01:00
Marc KlingenandGitHub a8626841b5 chore(cloud): update blobstorage integration entitlement (#6209) 2025-03-25 18:40:57 +00:00
marliessophieandGitHub 5945a93b7d fix(evaluator-config): preview execution of dataset run items (#6206)
* chore(evaluator-config): preview execution of dataset run items

* push
2025-03-25 17:44:11 +00:00
Steffen SchmitzandGitHub 9c447de584 chore: auto-close stale issues (#6207) 2025-03-25 17:21:44 +00:00
Max DeichmannGitHubellipsis-dev[bot] <65095814+ellipsis-dev[bot]@users.noreply.github.com>
48df0e04de fix: fix model charts (#6202)
* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* Update packages/shared/src/server/repositories/dashboards.ts

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

* Update web/src/__tests__/async/repositories/dashboard-repository.servertest.ts

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

* fix

* fix

* fix

---------

Co-authored-by: ellipsis-dev[bot] <65095814+ellipsis-dev[bot]@users.noreply.github.com>
2025-03-25 17:12:50 +00:00
Steffen SchmitzandGitHub 0897ac753b feat: add custom-query backend (#6006)
* feat: add self-serve dashboard backend poc

* chore: data model thoughts

* chore: create initial query builder example and test case

* chore: handle empty dimensions and metrics

* chore: lint

* chore: extend filter conditions to account for timestamps

* chore: refactor query builder into sub-functions

* chore: use template queries for user-supplied values

* chore: add query builder tests

* chore: handle time dimension

* chore: add trpc endpoint to execute custom clickhouse query

* chore: add a query playground component to test a couple of queries

* chore: handle multiple joins

* chore: add observations table to data model

* chore: fix bug in data model

* chore: add test case to compare with old dashboard results

* chore: add scaffold for additional views

* expand the users view and add segments to filter scores subviews

* chore: add test cases for score views

* chore: drop users and sessions for now

* chore: add scores aggregate and observations cost tests

* chore: add sql injection tests

* chore: lint

* chore: fix dashboard test cases

* chore: drop users query test

* chore: add order by logic

* chore: fill timeseries values

* chore: use new query function on dashboard

* chore: convert TracesBarListChart.tsx to new query endpoint

* chore: make chart data compile

* chore: update tests

* chore: add tags to custom queries

* chore: typing

* chore: suffix join condition with sql

* chore: add typing in queryBuidler

* chore: limit playground to cloud admin users

* chore: add util to map legacy dashboard columns to new model

* chore: separate time filter state for TracesBarListChart.tsx

* chore: pass timestamps directly into chart components

* chore: add userId and sessionId on observations

* chore: pick auto time granularity based on hours
2025-03-25 14:54:37 +00:00
Marc KlingenandGitHub d00c63100e chore(cloud): remove rate limits from projects api as it is used by auth_check in sdks (#6193)
* chore(cloud): remove rate limits from projects api as it is used by auth_check in sdks

* address feedback

* improve comment
2025-03-25 14:52:23 +00:00
steffen911 e8970bc11c chore: release v3.45.0 2025-03-25 15:52:33 +01:00
Steffen SchmitzGitHubellipsis-dev[bot] <65095814+ellipsis-dev[bot]@users.noreply.github.com>
eecb253093 feat: add blob storage integration (#6143)
* feat: add page to configure blob storage integration

* chore: remove blob storage logo and include three config options

* chore: add blob storage integration table

* chore: update router for blobstorage

* chore: placeholder for password and type update

* chore: create scaffolding for blobstorage queue processors

* chore: add scheduling logic for blob storage integration

* chore: prepare setup in processing job

* chore: implement extraction fucntion

* chore: add test case and fix generator setup

* chore: lint

* chore: overwrite default region to empty

* chore: remove details from auditlog and add beta

* Update web/src/pages/project/[projectId]/settings/integrations/blobstorage.tsx

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

* Update worker/src/__tests__/blobStorageIntegrationProcessing.test.ts

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

* chore: correct env name in tests

* Update worker/src/ee/integrations/blobstorage/handleBlobStorageIntegrationProjectJob.ts

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

* chore: update doc urls

* chore: open docs in new tab

* chore: ensure consistent block id length azure

* chore: revert test change

* chore: remove dummy error log

* chore: type annotation

---------

Co-authored-by: ellipsis-dev[bot] <65095814+ellipsis-dev[bot]@users.noreply.github.com>
2025-03-25 13:39:33 +00:00
marliessophieGitHubellipsis-dev[bot] <65095814+ellipsis-dev[bot]@users.noreply.github.com>
4e4fbb5101 fix(prompt-experiments): show form error if name is duplicated (#6189)
* fix(prompt-experiments): show form error if name is duplicated

* chore: fix typo

* Update web/src/ee/features/experiments/hooks/useExperimentNameValidation.tsx

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

* eslint

---------

Co-authored-by: ellipsis-dev[bot] <65095814+ellipsis-dev[bot]@users.noreply.github.com>
2025-03-25 09:07:16 +00:00
marliessophieandGitHub bb4823aa27 fix(evaluator-templates): control editing state (#6187) 2025-03-24 13:29:19 +00:00
marliessophieandGitHub 174d96c100 feat(evals): preview number of historic items to be evaluated (#6116)
* feat(evals): preview number of historic items to be evaluated

* refactor: split up large evaluator form file

* chore: drop optional pagination

* chore: fix eslint

* chore: rename test
2025-03-24 13:05:50 +00:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
8e59b544c9 chore(deps): bump next from 14.2.21 to 14.2.25 (#6175)
Bumps [next](https://github.com/vercel/next.js) from 14.2.21 to 14.2.25.
- [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.21...v14.2.25)

---
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-03-24 12:57:26 +00:00
Hassieb PakzadandGitHub cffd84f0e7 chore(azure-llm-connection): update api-version to support reasoning models (#6186) 2025-03-24 14:06:55 +01:00
Hassieb PakzadandGitHub 845faee385 fix(cost-tracking): null values in OpenAI usage schema (#6184) 2025-03-24 13:42:20 +01:00
Hassieb PakzadGitHubellipsis-dev[bot] <65095814+ellipsis-dev[bot]@users.noreply.github.com>
e45c4af30f chore(prompts): add error logs for resolvePromptGraph (#6181)
* chore(prompts): add error logs for resolvePromptGraph

* Update packages/shared/src/server/services/PromptService/index.ts

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

* fix

---------

Co-authored-by: ellipsis-dev[bot] <65095814+ellipsis-dev[bot]@users.noreply.github.com>
2025-03-24 11:31:29 +00:00
Marc KlingenandGitHub 616544a48c chore: apply prettier (#6176) 2025-03-24 09:46:13 +00:00
steffen911 75d64b38b3 chore: release v3.44.0 2025-03-24 09:35:15 +01:00
Steffen SchmitzandGitHub 65cdd75282 chore: drop queue backup option in bullmq (#6141)
* chore: drop queue backup option in bullmq

* chore: lint
2025-03-24 08:26:32 +00:00
Steffen SchmitzandGitHub 2744028f4c feat: extract top-level metadata from langfuse.metadata (#6173)
* feat: extract top-level metadata from langfuse.metadata

* chore: extract metadata based on prefix

* chore: add support for resourceAttributes
2025-03-24 08:09:04 +00:00
Max DeichmannandGitHub 3c02c54de2 fix: fix model paring from redis (#6157)
* push

* fix

* push

* fix

* fix
2025-03-21 18:42:06 +00:00
marliessophieandGitHub 974d2c1007 fix(ui): reference errors in prompt detail screen (#6126)
* chore(ui): convert `action-button` to use forwardRef for compatibility w/ buttons

* fix(ui): ensure prompt item ref points to `command-item`
2025-03-21 17:07:21 +00:00
Max DeichmannandGitHub 008045c771 perf: cache model not found (#6148)
* push

* push

* fix
2025-03-21 14:57:17 +00:00
Marc KlingenandGitHub c6790c6c0e chore(ui): do not auto-generate new api keys in setup-tracing step (#6149)
* chore(ui): do not auto-generate new api keys in setup-tracing step

* fix
2025-03-21 14:17:01 +00:00
Marc KlingenandGitHub 909445ce5b fix(prompts): trim prompt names (#6145)
* fix(prompts): trim prompt names

* push
2025-03-21 13:38:04 +00:00
Max DeichmannandGitHub f4ed4a2635 chore: improve model match observability (#6147)
push
2025-03-21 13:18:39 +00:00
Max DeichmannandGitHub d6206b5269 chore: increase model match observability (#6144) 2025-03-21 13:45:36 +01:00
Max DeichmannandGitHub 62fa43aa78 feat: cache LLM models in Redis (#6129)
* push

* fix

* fix

* fix

* push

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* push

* push

* push

* fix

* fix

* fix

* fix

* push
2025-03-21 11:26:11 +00:00
Steffen SchmitzandGitHub 3c9239a4ab chore: update .env.prod.example (#6140) 2025-03-21 10:24:48 +00:00
marliessophieandGitHub 476cddb4e1 chore(ui): clamp page-header title to one line (#6139) 2025-03-21 10:01:45 +00:00
Steffen SchmitzandGitHub a6c1678da8 feat: add additional input/output parsing for pydantic via OTel (#6138) 2025-03-21 10:00:33 +00:00
Steffen SchmitzandGitHub 129805ebde fix: safely parse big numbers in api responses (#6136)
* fix: safely parse big numbers in api responses

* feat: add additional input/output parsing for pydantic via OTel

* Revert "feat: add additional input/output parsing for pydantic via OTel"

This reverts commit 71d0f3abd951702393aac7ab79a875b888f089cf.
2025-03-21 09:30:24 +00:00
marliessophieandGitHub 59eafb42c3 fix(ui): require annotation queue selection for bulk action (#6127) 2025-03-20 21:08:57 +00:00
marliessophieandGitHub e4695ac717 fix(ui): dom nesting error data-table-row-height-switch (#6125) 2025-03-20 20:12:52 +00:00
marliessophieandGitHub eebd6ea0e9 fix(ui): dom nesting error page-header (#6124) 2025-03-20 20:06:05 +00:00
Marc KlingenandGitHub b4a8b08d3d chore(cloud): add posthog for projects and orgs (#6117)
* chore(cloud): add posthog for projects and orgs

* push
2025-03-20 18:07:17 +00:00
Max DeichmannandGitHub ed44b663ff chore: add dd env variables to github build command (#6119) 2025-03-20 17:36:48 +01:00
Marc KlingenandGitHub 720ebe861e chore(cloud): cache response of cloudStatus api for 5 minutes (#6115)
chore(cloud): keep response of cloudStatus api cached across views for 5 minutes
2025-03-20 14:25:54 +00:00
Max DeichmannandGitHub 9a6f8fd75d refactor: refactor naming of database batch exports (#6111)
* refactor: refactor naming of database batch exports

* refactor: refactor naming of database batch exports
2025-03-20 12:56:38 +00:00
Steffen SchmitzandGitHub f0da1cfa30 chore: reduce cloudwatch flush frequency and batch metrics (#6108)
* chore: reduce cloudwatch flush frequency and batch metrics

* chore: simplify metrics reset

* chore: keep 30s flush

* chore: correct CW metrics
2025-03-20 12:05:07 +00:00
Marc KlingenandGitHub 0c9ebb3da7 feat(cloud): show status page menu item during incidents (#6100)
* feat(cloud): show status page menu item during incidents

* add to support menu
2025-03-20 09:59:49 +00:00
Hassieb PakzadandGitHub 3d16d1ac57 fix(model-params-ui): duplicate model names leading to rendering issues (#6107) 2025-03-20 10:40:59 +01:00
Marc KlingenandGitHub fa462a8bc4 chore(ui): improve padding in peek view header (#6099) 2025-03-19 18:59:29 +00:00
David Alexander PfeifferandGitHub 695b65fe0d chore(tests): fully unpin node version from temporary fix #6063 (#6096) 2025-03-19 15:38:36 +00:00
Hassieb PakzadGitHubjake goldenellipsis-dev[bot] <65095814+ellipsis-dev[bot]@users.noreply.github.com>
955388e846 feat(llm-connections): add atla adapter (#6088)
* feat: enable Atla integration (#6022)

* push

* push

* Update packages/shared/src/server/llm/fetchLLMCompletion.ts

* Update web/src/features/public-api/components/CreateLLMApiKeyForm.tsx

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

* Revert "Update web/src/features/public-api/components/CreateLLMApiKeyForm.tsx"

This reverts commit efb2d3b359a85680224876481421d31bfbd44fd3.

---------

Co-authored-by: jake golden <jackson.golden@gmail.com>
Co-authored-by: ellipsis-dev[bot] <65095814+ellipsis-dev[bot]@users.noreply.github.com>
2025-03-19 12:36:12 +00:00
David Alexander PfeifferandGitHub c6864ed8d9 fix(tests): disable --experimental-require-module (default since Node 20.19.0) since it breaks jest-based testing (#6075)
* pin node to problematic version for working on a fix

* added cross-env and disabled the feature flag causing the testing issue - long term fix should probably be switching to vitest

* unpin node version since problematic feature is now disabled fo affected tests
2025-03-19 11:04:59 +00:00
marliessophieandGitHub 917516fa91 chore(ui): control for more specific onClick actions in table with peek view (#6086) 2025-03-19 10:25:03 +00:00
David Alexander PfeifferandGitHub 19017a6709 fix(ui): OpenAiMessageView now renders messages where content is an empty string instead of ignoring them (#6068)
* fix(ui): IOPreview now renders messages where content is an empty string instead of ignoring them

* removed debug logs

* OpenAiMessageView first filters valid messages to render and then executes the "show x more ..." logic, this way we ensure that there can never be a mismatch between messages items and the amount of fields we support displaying

* Render empty string quotation marks if content is empty but not null or undefined

* Change length check to falsy check for conditional display of empty quotes if content is empty

* cleaner falsy check for rendering empty string quotation marks
2025-03-19 09:41:32 +00:00
marliessophieandGitHub ca5e6569f9 fix(peek-view): ensure onClickRow action smoothly keeps peek view open; onClick of select cells independent of peek view state (#6079)
* fix(ui): stop propagation on traces table checkbox

* fix(peek-ui): handle onRowClick action correctly for peek view

* feat(peek): do not close upon any screen button or checkbox clicks

* push
2025-03-19 00:41:36 +00:00
marliessophieandGitHub 101942b98b fix(ui): padding on single prompt page (#6070) 2025-03-18 18:35:32 +00:00
marliessophieandGitHub 1188a21b02 feat(traces): add peek view on traces table (#6048)
* feat: adjust table to new style

* style: filter and column options

* fixup: add peek view

* feat: add detail navigation

* chore: remove duplicated code

* chore: style settings tables

* chore: pin first column on traces table

* style: adjust animation speed of peek view

* fix: padding on score configs table

* style: onClick cursor in table

* fix: eslint

* style

* chore: fix toggle for timeline

* push

* style: add padding
2025-03-18 17:25:52 +00:00
Marc KlingenandGitHub ec72dd840d chore(ui): render min-level hint in a single line (#6051) 2025-03-18 14:55:54 +00:00
steffen911 c2d800ff0e chore: release v3.43.0 2025-03-18 15:15:41 +01:00
Steffen SchmitzandGitHub dcf159ef82 chore: lock dd-trace to 5.36.0 (#6064) 2025-03-18 14:13:30 +00:00
Marc KlingenandGitHub 998984b9ed chore(cloud): improved pricing/packaging (#5664)
* wip

* simplify

* actionbutton for upgrade prompt

* count events for free plan limit

* add sso settings

* rename lite to core

* add product id

* nit

* nits

* fix rate limit test

* ci run on teams plan

* fix

* fix names of pipeline steps

* test tmp

* add better error logs to makeAPICall

* push

* clone to throw better errors

* improve error

* improve error

* fix

* remove comment

* refactor

* push

* next attempt

* nit

* create new auth for failing test suites

* push

* fix rate limit test
2025-03-18 13:47:03 +00:00
Marc KlingenandGitHub 022b234e57 chore: pin node 20.18.3 to avoid issue introduced by 20.19.0 (#6063)
* test commit

* chore: pin node 20.18.3 to avoid issue introduced by 20.19.0

* docker

* revert test commit

* push
2025-03-18 13:30:58 +00:00
b8777aa5d1 chore: better error message when uploading existing dataset item (#6053)
chore: better docstring when uploading existing dataset item to different dataset

Co-authored-by: Marc Klingen <git@marcklingen.com>
2025-03-18 13:07:11 +00:00
Steffen SchmitzandGitHub 2e84feeab7 feat: support external S3 endpoint for batch exports (#6055)
* feat: add media and export external endpoint config

* chore: add docs string

* chore: add external endpoint support for azure

* chore: drop feature for media
2025-03-18 10:57:14 +00:00
Steffen SchmitzandGitHub a508485f88 chore: enable datadog profiling for web (#6020)
* chore: enable datadog profiling for web

* chore: update dockerfile
2025-03-18 07:26:53 +00:00
marliessophieandGitHub d8bb283a44 fix(ui): make models detail page scrollable (#6050) 2025-03-17 19:48:27 +00:00
Steffen SchmitzandGitHub 75ec05e206 Delete .devcontainer/devcontainer.json 2025-03-17 15:01:41 +01:00
Steffen SchmitzandGitHub 19f710abe2 chore: add devcontainer config 2025-03-17 14:56:35 +01:00
David Alexander PfeifferandGitHub ad2d6bee66 chore(ui): remove padding of "Additional Input" field on OpenAiMessageView (#6037) 2025-03-17 10:06:32 +00:00
David Alexander PfeifferandGitHub 40d0acae5f chore(ui): alignment and sizing consitency in ObservationTree / GroupedScoreBadge (#6031)
* Unified design of grouped score badge with trace detail score badges, except that text is no longer medium/semibold

* Made padding of root item of ObservationTree consistent with other items
2025-03-17 09:58:23 +00:00
Marc KlingenandGitHub 4d387cc35c chore(ui): standardize the prompt variable list preview across views (#6027)
* chore(ui): standardize the prompt variable list preview across views

* use ctx prisma
2025-03-17 09:34:44 +00:00
690885c900 fix(ui): fixed scrollbar appearing on IO preview mode switcher in trace and observation details (#5995)
* fixed scrollbar appearing on IO preview mode switcher in trace and observation details

* fix

---------

Co-authored-by: Marc Klingen <git@marcklingen.com>
2025-03-14 19:32:46 +00:00
David Alexander PfeifferandGitHub ef2baa94be fix(ui): fixed misalignment of badges in trace and observation details (#5994)
* fixed alignment issues of badges wrapped in links in trace and observation detail views

* Fixed tooltip of aggregated usage data badge (in trace details) rendering even if there is no underlying data
2025-03-14 19:19:14 +00:00
db6b442832 feat: Add environment tag on trace / observation detail views (#5985)
* feat: Add environment tag on trace / observation detail views

* shorten label

---------

Co-authored-by: Marc Klingen <git@marcklingen.com>
2025-03-14 19:17:19 +00:00
Hassieb Pakzad 8231ebae4a chore: release v3.42.1 2025-03-14 17:05:33 +01:00
Marc KlingenandGitHub 326e8d3189 chore(ui): always show tag buttons in table cells instead of only on hover (#6025) 2025-03-14 15:23:32 +00:00
Hassieb PakzadandGitHub d2001d110d chore(docker-compose): bind services to localhost (#6023) 2025-03-14 14:16:36 +01:00
Hassieb PakzadandGitHub 3f81c6b9d5 feat(cost-tracking): add OpenAIResponseUsageSchema support (#6003) 2025-03-14 12:08:28 +00:00
Marc KlingenandGitHub 991f837ee5 fix(ui): hide jump to playground button when not entitled (#6014) 2025-03-14 00:01:40 +00:00
Marc Klingen 062a7f27b8 chore: release v3.42.0 2025-03-13 19:23:52 +01:00
Steffen SchmitzandGitHub 924b326db6 chore: remove unused prisma query builder (#6005) 2025-03-13 17:21:23 +00:00
Andres CarrilloandGitHub 2780e66c13 feat(auth): add custom Gitlab SSO URL option (#5984) (#6004) 2025-03-13 17:50:16 +01:00
Marc KlingenandGitHub c2f51b8d2d feat(ui): add csv/json export for scores table (#5949)
* feat(ui): add csv/json export for scores table

* Merge branch 'main' into marc/lfe-3466-export-scores

* push

* add test

* fix cols

* clean up diff

* fix
2025-03-13 16:23:30 +00:00
Steffen SchmitzandGitHub 3a3021a176 chore: reduce host port mapping for internal docker compose components (#6002) 2025-03-13 15:36:34 +00:00
Marc KlingenandGitHub 5dd5a39f90 feat(api): add api for annotation queues (#5945)
* feat(api): add api for annotation queues

* fix

* push

* strict zod interfaces

* fix

* fix spec and types

* add additional tests
2025-03-13 15:24:19 +00:00
Steffen SchmitzandGitHub 9ee4544e1d docs: add security notes to docker compose deployment (#5999) 2025-03-13 14:27:10 +00:00
Hassieb PakzadandGitHub 136f8a3e83 fix(llm-connections): show bedrock error (#6000) 2025-03-13 15:06:42 +01:00
Hassieb Pakzad eecf0d680b chore: release v3.41.1 2025-03-13 11:07:07 +01:00
Hassieb PakzadandGitHub d50dbed797 feat(llm-connections): allow extraHeaders for azure (#5996) 2025-03-13 09:46:38 +00:00
Steffen SchmitzandGitHub bb1dbdf824 chore: reproduce clickhouse missing surrogate key error (#5980)
* chore: reproduce clickhouse missing surrogate key error

* chore: add fix candidate

* chore: adjust test cases

* chore: adjust test case
2025-03-13 07:47:31 +00:00
Hassieb Pakzad 0991d44525 chore: release v3.41.0 2025-03-12 18:28:25 +01:00
Max DeichmannandGitHub 449c9360ac fix: search past observations with correct timestamp in evals (#5986)
fix: use readonly for the column definitions
2025-03-12 16:41:14 +00:00
Hassieb PakzadandGitHub cc4a88d6da fix(prompts): block label deletion if dependent exists (#5983)
* fix(prompts): block label deletion if dependent exists

* fix

* Update web/src/features/prompts/server/actions/updatePrompts.ts
2025-03-12 16:05:46 +00:00
Steffen SchmitzandGitHub 11b2411a5b chore: prevent sorting by environment in UI tables (#5978) 2025-03-12 13:45:25 +00:00
d8a5ebc42e chore(cloud): Make UI recognize cloud:enterprise plan and add corresponding access rights and rate limits (#5975)
* chore(cloud): Make UI recognize cloud:enterprise plan and add corresponding access rights and rate limits

- label on project selector
- rate limits same as team
- access rights same as team
- no stripe product id for this (yet), cannot be purchased self-serve in product

* remove comments

---------

Co-authored-by: Marc Klingen <git@marcklingen.com>
2025-03-12 13:44:01 +00:00
Steffen SchmitzandGitHub 9042b005b8 feat: extract logfire.msg as span and trace name (#5977) 2025-03-12 13:23:33 +00:00
Hassieb PakzadandGitHub 7b70b5b678 feat(prompts): add composability (#5882) 2025-03-12 13:26:00 +01:00
Marc KlingenandGitHub c3ce043d47 chore: remove unnecessary comment (#5961) 2025-03-11 13:20:18 +00:00
Marc KlingenandGitHub 8c5737ebea chore: rename batch exports from generation to observation as table was generalized (#5950)
* chore: rename batch exports from generation to observation as table was generalized

* fix test
2025-03-11 13:15:36 +00:00
steffen911 cb928bf3fc chore: release v3.40.0 2025-03-11 13:49:18 +01:00
Steffen SchmitzandGitHub dc8a997324 feat: add score deletion to UI (#5956)
* feat: enable score deletion via Langfuse UI

* chore: add test cases

* chore: typo

* chore: implement deleteAnnotationScore

* chore: lint

* feat: add score deletion to UI

* chore: add trpc tests

* chore: allow multi-score deletion from table view

* chore: typoes and mock resets

* chore: update mock handling
2025-03-11 12:44:17 +00:00
Steffen SchmitzandGitHub 31da4bc5a5 chore: refactor score deletions into async queue (#5939)
* feat: enable score deletion via Langfuse UI

* chore: add test cases

* chore: typo

* chore: implement deleteAnnotationScore

* chore: lint
2025-03-11 08:02:13 +00:00
Max DeichmannandGitHub c0189eecad fix: use readonly for the column definitions (#5948)
* fix: use readonly for the column definitions

* fix: use readonly for the column definitions
2025-03-10 22:13:28 +00:00
Marc KlingenandGitHub a3f85e75dc docs: add zod type strict() to public api cursor rule (#5947) 2025-03-10 21:56:41 +00:00
Marc KlingenandGitHub 88ee2f3823 docs: improve public api cursor rule (#5946) 2025-03-10 21:35:38 +00:00
Max DeichmannandGitHub 0b485ad5d8 fix: fix eval status on details page (#5944)
* fix: fix eval status on details page

* push

* fix: fix eval status on details page

* fix: fix eval status on details page
2025-03-10 20:18:04 +00:00
Marc KlingenandGitHub 4e95550411 feat(datasets): show run name on runitems table (#5943)
* feat(datasets): show run name on runitems table

* perf

* push
2025-03-10 19:50:00 +00:00
Marc KlingenandGitHub ba9e26181b chore: remove models page notification (#5942) 2025-03-10 19:28:48 +00:00
Marc KlingenandGitHub 289a7210ee chore(cloud): add more data masking for posthog (#5941) 2025-03-10 19:27:36 +00:00
Max Deichmann fe0c4dc98d chore: release v3.39.0 2025-03-10 17:09:30 +01:00
Hassieb PakzadandGitHub 61c2ec268a perf(playground): memoizate ChatMessages (#5937)
* perf(playground): memoizate ChatMessages

* push

* add callbacks
2025-03-10 16:07:28 +00:00
Max DeichmannandGitHub 1df2781b59 fix: correctly extract jsonpath form observations for evals (#5938)
* fix: correctly extract jsonpath form observations for evals

* fix: correctly extract jsonpath form observations for evals
2025-03-10 15:42:44 +00:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
5478e870b3 chore(deps): bump axios from 1.7.7 to 1.8.2 (#5915)
Bumps [axios](https://github.com/axios/axios) from 1.7.7 to 1.8.2.
- [Release notes](https://github.com/axios/axios/releases)
- [Changelog](https://github.com/axios/axios/blob/v1.x/CHANGELOG.md)
- [Commits](https://github.com/axios/axios/compare/v1.7.7...v1.8.2)

---
updated-dependencies:
- dependency-name: axios
  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-03-10 14:49:50 +00:00
Max DeichmannandGitHub 80c298fa1e chore: limit event id length (#5934) 2025-03-10 14:48:51 +00:00
Steffen SchmitzandGitHub 330cca6978 feat: add trace delete API endpoints (#5931)
* feat: add trace delete API endpoints

* chore: add api docs

* chore: update docs

* chore: typo

* chore: update audit logs

* chore: lint

* chore: update fern docs

* chore: fix return type
2025-03-10 14:13:33 +00:00
Steffen SchmitzandGitHub 286b78a1fe feat: add api key actions to auditlog table (#5932)
* feat: add api key actions to auditlog table

* chore: propagate audit logs to all relevant API routes

* chore: lint
2025-03-10 12:35:10 +00:00
Marc KlingenGitHubellipsis-dev[bot] <65095814+ellipsis-dev[bot]@users.noreply.github.com>
f0c37c8309 chore: add initial cursor rules (#5926)
* chore: add some initial cursor rules

* Update .cursor/rules/frontend-features.mdc

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

* Update .cursor/rules/public-api.mdc

---------

Co-authored-by: ellipsis-dev[bot] <65095814+ellipsis-dev[bot]@users.noreply.github.com>
2025-03-09 22:54:01 +00:00
Marc KlingenandGitHub 326b27cdda fix: ui config in localstorage should persist signing out (#5916)
* fix: ui config in localstorage should persist signing out

* push

* push
2025-03-07 17:50:04 +00:00
Marc KlingenandGitHub 886e352c39 fix: select/highlight text in Compare Dataset view (#5914)
* fix: select/highlight text in Compare Dataset view

* clean diff
2025-03-07 15:59:53 +00:00
steffen911 1cd76a49f1 chore: release v3.38.0 2025-03-07 16:48:21 +01:00
Marc KlingenandGitHub 0639b78468 feat(datasets): add multi-delete for dataset runs table (#5912) 2025-03-07 15:29:50 +00:00
Steffen SchmitzandGitHub 2d6e51e219 chore: test processing of special characters in S3 (#5907)
* chore: test processing of special characters in S3

* chore: reject carriage-return character in IDs

* Update ingestion-api.servertest.ts
2025-03-07 15:20:11 +00:00
Yohan GonçalvesandGitHub 1ed6358738 feat: add customizable claim mapping for SSO (#5858) 2025-03-07 16:19:51 +01:00
4fc339dbac chore: remove redundant readme files (#5900)
chore: remoe redundant readme files

Co-authored-by: Marc Klingen <git@marcklingen.com>
2025-03-06 19:55:34 +00:00
Steffen SchmitzandGitHub 1e64d97b75 feat: create new environment materialized views for quick lookups (#5895)
* feat: create new environment materialized views for quick lookups

* chore: update repository

* chore: lint

* chore: add final to environments query

* chore: always include default in array
2025-03-06 19:09:28 +00:00
Steffen SchmitzandGitHub 4d06482d78 fix: enforce ints on usageDetails (#5889)
* fix: enforce ints on usageDetails

* chore: make test actually test a  generation

* chore: lint
2025-03-06 17:05:10 +00:00
Marc KlingenandGitHub 603e5ca7c9 chore(ui): add env label on env multi-select (#5890) 2025-03-06 16:37:22 +00:00
Steffen SchmitzandGitHub 61c6560f24 chore: return 200 instead of 207 as OTel success status (#5883) 2025-03-06 14:58:40 +00:00
Steffen SchmitzandGitHub 6bff022a88 chore: log dropped data items correctly in clickhouse writer (#5876)
* chore: log dropped data items correctly in clickhouse writer

* chore: update test case
2025-03-06 14:43:11 +00:00
Steffen SchmitzandGitHub 1f7cac2dd8 feat: add environment column to UI tables (#5877)
* feat: add environment column to UI tables

* feat: add environment on sessions table

* chore: add to observations

* chore: add to scores

* chore: add to users table

* chore: lint
2025-03-06 14:08:41 +00:00
Hassieb PakzadandGitHub 49bccb8fa0 fix(ui-graph-view): handle single top most node (#5881) 2025-03-06 15:15:03 +01:00
Marc KlingenandGitHub fea8a3d8d1 chore: add noindex header (#5880) 2025-03-06 13:46:00 +00:00
Max DeichmannandGitHub 08fcdb68be chore: fix contributingmd file (#5879) 2025-03-06 14:34:46 +01:00
f18a120bd8 feat: add environment filter (#5765)
* feat: add environment filter on dashboard

* chore: add a useEffect and dashboard columns for filtering

* chore: add dashboard filters

* fixup: add environment filter hook and barebone code

* chore: add traces table filter

* chore: add sessions environment filter

* chore: do observations, scores, users

* chore make filter bar dynamic and change UI placement

* chore: map environment column specially in dashboard

* chore: add column

* chore: lint

* chore: remove overflow on multi-select

* chore: only load downstream calls once environment is available

* chore: only use isLoading condition

---------

Co-authored-by: Marlies Mayerhofer <74332854+marliessophie@users.noreply.github.com>
2025-03-06 10:45:18 +00:00
Max DeichmannandGitHub 2b3ca85344 chore: fix contributingmd file (#5870)
* chore: fix contributingmd file

* chore: fix contributingmd file

* chore: fix contributingmd file

* chore: fix contributingmd file

* chore: fix contributingmd file
2025-03-06 08:03:39 +00:00
Hassieb PakzadandGitHub 8b408858c4 feat(batch-exports): increase row limit to 1.5M (#5872) 2025-03-06 08:47:56 +01:00
89ad92c0ff chore: consistent casing of README files (#5865)
* docs: fix broken link in CONTRIBUTING.md

* chore: rename files to fix casing

---------

Co-authored-by: Marc Klingen <git@marcklingen.com>
2025-03-05 23:23:54 +00:00
Marc Klingen 16e1d19917 chore: release v3.37.0 2025-03-05 23:51:35 +01:00
Marc KlingenandGitHub 18d5ba1bfa feat(auth): add AUTH_WORKOS_ORGANIZATION_ID and AUTH_WORKOS_CONNECTION_ID envs (#5866) 2025-03-05 21:31:45 +00:00
Steffen SchmitzandGitHub 95d6b9fe13 chore: add name mappings to sync prisma schema with reality (#5863) 2025-03-05 16:56:47 +00:00
Max DeichmannandGitHub 23183a71c1 chore: add missing clickhouse writer metrics (#5851)
* chore: add missing clickhouse writer metrics

* chore: add missing clickhouse writer metrics

* Update worker/src/services/ClickhouseWriter/index.ts
2025-03-05 16:37:00 +00:00
5eb0a275a5 feat: add S3_CONCURRENT_READS and S3_CONCURRENT_WRITES config options (#5824)
Co-authored-by: Steffen Schmitz <steffen@langfuse.com>
2025-03-05 16:58:26 +01:00
Steffen SchmitzandGitHub 4c5366fa20 perf: reduce trace scans for scores counts (#5860)
* perf: reduce trace scans for sessions counts

* chore: add countAll test

* chore: add session for testing
2025-03-05 15:18:10 +00:00
Marc Klingen 4ebaf0a24a chore: release v3.36.0 2025-03-05 13:32:42 +01:00
Marc KlingenandGitHub b98b90e635 feat(auth): add workos as idp (#5828)
* feat(auth): add workos as idp

* add button for org and connection based sign in

* fix

* remove client auth method and checks

* remove unnecessary diff
2025-03-05 12:29:26 +00:00
Marc Klingen 36f11a645f chore: release v3.35.1 2025-03-04 22:32:19 +01:00
Marc KlingenandGitHub 3c8b26aa79 chore(api): update api reference and postman collection (#5842) 2025-03-04 22:31:38 +01:00
Marc Klingen 86840e4119 chore: release v3.35.0 2025-03-04 22:23:24 +01:00
Steffen SchmitzandGitHub 4b412fb6ca feat: map pydantic logfire events to langfuse datamodel (#5841)
* feat: map pydantic logfire events to langfuse datamodel

* chore: map events to empty if parsing fails
2025-03-04 20:58:42 +00:00
Steffen SchmitzandGitHub 271f75aa17 chore: reduce default batch size of background migrations (#5838)
* chore: reduce default batch size of background migrations

* chore: update the cli defaults
2025-03-04 15:58:11 +00:00
Marc KlingenandGitHub 669ad4cb92 feat(datasets): deletion of dataset runs via api (#5834)
* public api

* improve public api
2025-03-04 13:41:27 +00:00
Steffen SchmitzandGitHub 9b237d4041 fix: detect openinference LLM calls as generations (#5833)
feat: detect openinference LLM calls as generations
2025-03-04 11:13:42 +00:00
marliessophieandGitHub 15b4ca01ec fix(ui): overlapping status message on trace detail page (#5830) 2025-03-04 09:07:29 +00:00
Marc KlingenandGitHub 196b5319bf feat(datasets): allow for deletion of dataset items in ui and public API (#5816)
* push

* move into drop down menu

* fix

* move copy to the right side

* fern generate

* use trpc errors
2025-03-03 14:03:18 +00:00
Steffen SchmitzandGitHub d88e0456b8 perf: reduce memory consumption on observations query (#5811)
* perf: reduce memory consumption on observations query

* chore: drop skip index list for lint
2025-03-03 13:35:40 +00:00
Marc KlingenandGitHub 78463a9ce3 feat(datasets): copy items between datasets (#5800)
* wip

* push

* push
2025-03-03 12:51:06 +00:00
marliessophieandGitHub 0bd7219eba style(ui): trace view alignment improvements (#5789)
* style(ui): trace view alignment improvements

* fix: test
2025-03-03 12:25:15 +00:00
Steffen SchmitzandGitHub a307270d8c chore: upgrade fern and add java sdk example (#5814) 2025-03-03 11:09:41 +00:00
Max DeichmannandGitHub f1555190c1 fix: use aes-gcm 12 bytes for IV (#5764)
* fix: use aes-gcm 12 bytes for IV

* fix: use aes-gcm 12 bytes for IV

* fix: use aes-gcm 12 bytes for IV
2025-03-03 10:32:22 +00:00
Max DeichmannandGitHub 56c77e0de8 chore: reduce log level for frequent logs (#5804) 2025-03-02 17:54:07 +00:00
Marc KlingenandGitHub 48cbbb694f feat(ui): add LLM connections settings page separate from api keys (#5798) 2025-03-01 19:04:33 +00:00
Marc KlingenandGitHub 168dfd2caa feat(datasets): add archive/unarchive functionality for dataset items on single item view (#5797)
* feat(datasets): add archive/unarchive functionality for dataset items on single item view

* imrpove copy
2025-03-01 19:04:23 +00:00
Marc KlingenandGitHub ae66dd9e6f fix(ui): add playsInline attribute to video players (#5795) 2025-03-01 18:33:46 +00:00
Marc KlingenandGitHub d081aab38f feat(datasets): support adding dataset items to multiple datasets (#5796)
* feat(datasets): support adding dataset items to multiple datasets

* feat(datasets): optimize bulk dataset item creation with createMany
2025-03-01 18:09:46 +00:00
Marc KlingenandGitHub a86f0ab263 fix(cloud): only active evals should count towards plan limit (#5793) 2025-03-01 12:42:39 +00:00
Marc KlingenandGitHub 738df01ab7 feat(ui): add settings deep links to cmd+k (#5680)
* proejct settings

* push

* add org and small fixes
2025-03-01 10:12:04 +00:00
marliessophieandGitHub 549eac6fd3 fix(ui): overflow behaviour on trace timeline (#5792) 2025-03-01 07:50:55 +00:00
Marc KlingenandGitHub 6d125ec274 chore(ui): remove redundant className props from splash screens (#5791) 2025-03-01 00:44:46 +00:00
Marc KlingenandGitHub 4f54577f83 chore(ui): improve perf of sessions hasAny (#5783)
chore: improve performance of sessions hasAny
2025-03-01 00:31:29 +00:00
Steffen SchmitzandGitHub 1ec1bdf849 feat: add environment properties on api routes (#5781)
* feat: add environment properties on api routes

* chore: add env filter to traces

* chore: add observation env and daily metrics env filters

* chore: add env to score endpoints

* chore: filter sessions by environment

* chore: filter traces by environment

* chore: expand trace test assertions

* chore: lint
2025-02-28 16:12:18 +00:00
Hassieb PakzadandGitHub ed5a53b10e fix(ui): llm api key form model name delete (#5786) 2025-02-28 15:57:18 +00:00
marliessophieandGitHub 5b1d9b1dd1 style(page-header): margins (#5784) 2025-02-28 14:42:19 +00:00
marliessophieandGitHub adf4508ffd feat(traces): redesign single trace UI (#5718)
* feat(traces-ui): refactor preview for traces and observations

* fix: typo in use prompt code

* feat(ui): add redesigned timeline

* feat: support all settings on timeline view

* feat: support langgraph trace

* style: langgraph view

* chore: fix other usages of single trace components

* style: comment icon color

* fix: eslint

* style: optimise for reponsive design

* chore: refactor common functions to helpers

* chore(globals): remove comments

* style: expand clickable area; adjust search string

* style: small improvements

* fix: styling of trace preview page

* fix: eslint

* fix: remove duplicated cost in observation preview

* fix: session IO formatting

* style

* style

* style: final margins
2025-02-28 13:37:07 +00:00
Steffen SchmitzandGitHub 45f8b2e97b chore: skip tracing for LLM Api key routes (#5780) 2025-02-28 10:49:40 +00:00
marliessophieandGitHub 39c49b65a4 fix(ui): overlap on dataset runs screen (#5778) 2025-02-28 10:15:26 +00:00
steffen911 d905623cb3 chore: release v3.34.1 2025-02-28 11:11:43 +01:00
Steffen SchmitzandGitHub a87a3505b8 chore: add env.HOSTNAME support for worker container (#5779) 2025-02-28 10:06:41 +00:00
Steffen SchmitzGitHubellipsis-dev[bot] <65095814+ellipsis-dev[bot]@users.noreply.github.com>
591de20a72 feat: simplify otel input/output and accept string token values (#5777)
* feat: simplify otel input/output and accept string token values

* chore: typo

* Update web/src/features/otel/server/index.ts

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

* chore: also beautify event attribute fallback

---------

Co-authored-by: ellipsis-dev[bot] <65095814+ellipsis-dev[bot]@users.noreply.github.com>
2025-02-28 09:49:49 +00:00
Marc Klingen 33d3ab80cb chore: release v3.34.0 2025-02-27 22:54:18 +01:00
Marc KlingenandGitHub 6b9773caec feat(models): add openai gpt-4.5-preview (#5772) 2025-02-27 21:47:24 +00:00
Steffen SchmitzandGitHub c90b477d40 fix: disable tracing for resetPassword route (#5763) 2025-02-27 15:36:36 +00:00
Max DeichmannGitHubellipsis-dev[bot] <65095814+ellipsis-dev[bot]@users.noreply.github.com>
0f448ccdde fix: always return 422 for signup error handler (#5761)
* fix: always return 422 for signup error handler

* Update web/src/features/auth-credentials/server/signupApiHandler.ts

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

* Update web/src/features/auth-credentials/server/signupApiHandler.ts

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-02-27 15:33:41 +00:00
marliessophieandGitHub 74f93aad79 chore: move dashboard button to the right, unify eval headers (#5762)
chore: move dashboard button to the right
2025-02-27 15:15:40 +00:00
Max DeichmannandGitHub 9650e87f64 fix: standardize way to get sso blocked domains (#5760)
fix: standardise way to get sso blocked domains
2025-02-27 15:15:10 +00:00
Steffen SchmitzandGitHub c5fc30a9d0 feat: map otel environment config to langfuse environment (#5757) 2025-02-27 14:46:43 +00:00
steffen911 4a26f47971 chore: release v3.33.1 2025-02-27 15:34:24 +01:00
Steffen SchmitzGitHubellipsis-dev[bot] <65095814+ellipsis-dev[bot]@users.noreply.github.com>
e3a9e61d07 perf: filter relevant observation for data set run item stats (#5752)
* perf: filter relevant observation for data set run item stats

* Update web/src/features/datasets/server/service.ts

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-02-27 14:08:07 +00:00
Hassieb Pakzad a12c2a3a97 fix(model-prices): bedrock claude 3-7 version 2025-02-27 13:14:05 +01:00
Hassieb PakzadandGitHub 80681e08af fix(model-prices): bedrock claude sonnet 3-7 version (#5753) 2025-02-27 13:10:08 +01:00
steffen911 4e97494390 chore: release v3.33.0 2025-02-27 11:58:10 +01:00
Steffen SchmitzandGitHub 346e5fe7b6 feat: map environment to score within eval service (#5749)
* feat: map environment to score within eval service

* chore: adjust tests

* chore: drop unused imports
2025-02-27 10:50:36 +00:00
Steffen SchmitzandGitHub 16482f588a fix: create traces via otel for empty parent span buffer (#5748)
* fix: create traces via otel for empty parent span buffer

* chore: test range for session duration
2025-02-27 10:10:51 +00:00
Marc KlingenandGitHub f047e21e13 chore: improved domain handling and early return in signupApiHandler (#5744) 2025-02-26 21:56:20 +00:00
marliessophieandGitHub 534e832c13 fix(ui): overlapping variables for chat prompt (#5740) 2025-02-26 17:47:03 +00:00
marliessophieandGitHub 5dbfec24f0 fix(ui): experiment from the prompt screen -> window does not fit screen (#5738) 2025-02-26 17:21:05 +00:00
Hassieb PakzadandGitHub 700705be84 chore(llm-keys): update bedrock form description (#5737) 2025-02-26 17:58:49 +01:00
Hassieb Pakzad 611c31ce54 chore: release v3.32.1 2025-02-26 17:06:23 +01:00
Hassieb PakzadandGitHub bdb5ddadd9 fix(ingestion): environment to immutable keys (#5736) 2025-02-26 16:01:26 +00:00
Hassieb PakzadandGitHub 0fd1f065bd fix(api): add environment to ingestion spec (#5735) 2025-02-26 16:02:49 +01:00
Marc KlingenandGitHub 8aa063303a chore(ui): move "configure tracing" to the right in dashboard header (#5731) 2025-02-26 13:59:21 +00:00
Marc KlingenandGitHub d0ff8a87fe chore(ui): minor improvements to onboarding pages (#5730)
chore: minor improvements to onboarding pages
2025-02-26 13:56:39 +00:00
marliessophieandGitHub 0e61ffd621 style(prompts): always show edit label button for prompt versions (#5729) 2025-02-26 13:41:01 +00:00
steffen911 9f20926bc4 chore: release v3.32.0 2025-02-26 14:36:38 +01:00
Steffen SchmitzandGitHub 81a232e4a4 fix: fallback to "default" for missing environment (#5728) 2025-02-26 13:34:33 +00:00
Marc KlingenandGitHub c7e7d62f47 feat(ui): add onboarding screens for core features (#5706)
* feat: add in-ui-onboarding for tracing

* feat(ui): add onboarding screens for core features

* add annotation queues

* move videos to static, add eval onboarding

* push

* push

* push

* push

* push
2025-02-26 13:13:43 +00:00
steffen911 2aa8971dff chore: release v3.31.0 2025-02-26 13:53:58 +01:00
Steffen SchmitzandGitHub 85f4cb0c13 fix: show retention settings for self-hosters (#5727) 2025-02-26 12:34:14 +00:00
Hassieb PakzadandGitHub 4bbba9d983 feat(api): add environment property to traces, scores, obs (#5724) 2025-02-26 13:32:03 +01:00
Steffen SchmitzandGitHub 0b7dd5a1a7 feat: process environment in ingestion pipeline (#5671)
* feat: process environment in ingestion pipeline

* chore: remove unnecessary status

* chore: typing

* chore: more typing

* chore: score env prop

* chore: adjust apis to pass tests

* chore: adjust ingestion service test

* chore: restrict allowed prefix
2025-02-26 12:18:38 +00:00
Steffen SchmitzandGitHub b5cad2693f fix: exclude clickhouse default date from session duration calc (#5726) 2025-02-26 09:51:52 +00:00
Max Deichmann 58d85d66b3 chore: release v3.30.0 2025-02-26 10:30:59 +01:00
Max DeichmannandGitHub 55a5a896f1 chore: track distribution of ingested event body size (#5723)
push
2025-02-26 09:05:08 +00:00
Steffen SchmitzandGitHub 5437beec48 Revert "fix: return nulls for left-joins for correct session durations (#5697)" (#5717)
This reverts commit ce967bac12.
2025-02-25 18:14:26 +01:00
Steffen SchmitzandGitHub 418e33a333 fix: only delete media items with no remaining references (#5713)
* fix: only delete media items with no remaining references

* chore: expand filter condition
2025-02-25 15:18:02 +00:00
Hassieb PakzadandGitHub e9bbd4488f feat(playground+evals): add Google AI Studio support (#5711) 2025-02-25 14:48:22 +01:00
Steffen SchmitzandGitHub 668ca24d5e fix: support gzip encoding on otel endpoint (#5710)
* fix: support gzip encoding on otel endpoint

* chore: check for includes
2025-02-25 10:41:43 +00:00
Hassieb PakzadandGitHub 7c746c7cfc feat(models): add claude-3-7-sonnet-20250219 (#5709) 2025-02-25 10:07:41 +00:00
Hassieb Pakzad de0eb76153 chore: release v3.29.1 2025-02-24 20:53:46 +01:00
Hassieb PakzadandGitHub 85088574b2 fix(public-api): list observations with name filter (#5700) 2025-02-24 20:52:58 +01:00
Steffen SchmitzandGitHub ce967bac12 fix: return nulls for left-joins for correct session durations (#5697)
* fix: return nulls for left-joins for correct session durations

* chore: comment updates

* chore: adjust trace_count
2025-02-24 15:57:47 +00:00
Steffen SchmitzandGitHub b8d0a4bbe8 chore: remove prisma references to traces, observations, scores (#5672)
* chore: remove prisma references to traces, observations, scores

* chore: separate enum and type name

* chore: type overwrites

* chore: chore

* chore: drop views

* chore: reduce diff

* chore: diff

* chore: types gen

* chore: lint

* chore: adjust tests

* chore: remove unused objects in test utils

* chore: adjust tests

* chore: score type parsing

* chore: fix otelmapping

* chore: make observation level internal
2025-02-24 15:43:25 +00:00
marliessophieandGitHub 1e7aeb8fc5 chore(llmCompletion): support system/developer messages for o3 (#5661) 2025-02-24 09:25:33 +00:00
marliessophieandGitHub 9d5cc39cd4 style(ui): improvements to prompt details screen (#5678)
* fix: header

* fix: search always show current version

* chore: adjust code block

* chore: re-add copy in code editor if no title

* chore: do not match current version always

* adjust margins

* chore: remove breadcrumb

* fix: typo

* fix: final margin

* style: eval template

* small design nits
2025-02-22 09:16:44 +00:00
Marc KlingenandGitHub 5c5dfcbac6 chore(ui): improve consistency of prompt and eval template forms (#5679)
* chore(ui): consistency in prompt form labels

* eval prompt form
2025-02-21 18:10:12 +00:00
marliessophieandGitHub 628d180051 feat(ui): new prompt detail screen; adjust design of IO and message view components (#5537)
* style(ui): adjust design of IO and message view components

* chore: adjust final style and fix imports

* style

* nit: show "null" if no output in dataset compare view

* style: add header and tertiary colors

* chore: add Header levels

* chore: add Timeline component

* feat: make JSONView optionally scrollable

* chore: refactor TagManager styling

* style: button variants for header

* chore: ensure chatml message overflow behaves within chat message

* feat: new prompt screen outline

* chore: refactor `extractVariables` to return count

* chore: ensure robust mobile behaviour

* style: small component adjustments

* chore: refactor style of `Comment` button

* feat: add search functionality to prompt nodes

* style: restyle tags

* revert: variable extraction count

* style: prompts page overflow

* chore: control label behaviour

* labels: show buttons on hover

* fix: formatting of titles

* eslint

* push

* push

* fix: overflows

* push

* diabled view
2025-02-21 13:46:59 +00:00
Max Deichmann 57708f0219 chore: release v3.29.0 2025-02-21 13:42:45 +01:00
Max DeichmannandGitHub 5d30aefcb3 fix: do not allow to deactivate eval configs which are only on existing objects (#5675)
* fix: do not allow to deactivate eval configs which rano on historic traces only

* fix: do not allow to deactivate eval configs which rano on historic traces only

* fix: do not allow to deactivate eval configs which rano on historic traces only

* fix: do not allow to deactivate eval configs which rano on historic traces only

* fix: do not allow to deactivate eval configs which rano on historic traces only
2025-02-21 12:40:18 +00:00
Max DeichmannandGitHub dd14e11535 perf: reduce eval create queue concurrency (#5665) 2025-02-20 19:24:38 +00:00
Max DeichmannandGitHub 5f87980013 fix: do not return all executions for eval config list trpc (#5663)
* fix: do not return all executions for eval config list trpc

* fix: do not return all executions for eval config list trpc

* fix: do not return all executions for eval config list trpc

* fix: do not return all executions for eval config list trpc
2025-02-20 17:56:38 +00:00
Max DeichmannandGitHub 2edc2487ae perf: limit concurrency for eval job creations (#5660)
* push

* push
2025-02-20 16:05:41 +00:00
Steffen SchmitzandGitHub 18508187f6 chore: add environment column to trace_sessions (#5656)
* chore: add environment column to trace_sessions

* chore: update test
2025-02-20 15:06:39 +00:00
Max DeichmannGitHubellipsis-dev[bot] <65095814+ellipsis-dev[bot]@users.noreply.github.com>
eb5ee957e6 feat: apply evals on history of datasets + traces (#5488)
* push

* push

* push

* push

* fix

* fix

* add tests

* add tests

* add tests

* add tests

* add tests

* add tests

* fic

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* push

* push

* Update web/src/ee/features/evals/components/evaluator-form.tsx

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

* push

* fix: fix dompurify

---------

Co-authored-by: ellipsis-dev[bot] <65095814+ellipsis-dev[bot]@users.noreply.github.com>
2025-02-20 13:59:14 +00:00
Steffen SchmitzandGitHub 582f150b72 chore: add environment columns in clickhouse (#5649)
* chore: add environment columns in clickhouse

* chore: replace logger.warning with logger.warn
2025-02-20 13:05:35 +00:00
Steffen SchmitzandGitHub 7ce4e24cab chore: replace logger.warning with logger.warn (#5650) 2025-02-20 12:12:31 +00:00
marliessophieandGitHub b2d3080790 chore(prompts): support developer message (#5639)
* chore(prompts): support developer message

* chore: fix eslint config

* push
2025-02-20 09:40:32 +00:00
steffen911 3590608d30 chore: increase event log deletion timeout to 5min 2025-02-19 15:56:11 +01:00
Steffen SchmitzandGitHub 78e6c882ba chore: reduce attempts and increase delay on trace delete errors (#5637) 2025-02-19 14:24:28 +00:00
Steffen SchmitzandGitHub d5ea44b33c perf: only validate response schema on dev servers (#5630) 2025-02-19 13:11:42 +00:00
Steffen SchmitzandGitHub 8b601eed20 fix: re-enable trace deletion and remove trace S3 files (#5594)
* fix: re-enable trace deletion and remove trace S3 files

* chore: lint

* chore: lint

* chore: increase query timeout

* chore: reduce query result to only return event log

* chore: keep condition pattern

* chore: revert entitlement changes

* chore: move permission check

* chore: explain semi join and shift entitlement comment
2025-02-19 12:45:37 +00:00
Steffen SchmitzandGitHub caebc92051 chore: bump static page generation timeout (#5621) 2025-02-18 14:44:33 +00:00
Steffen SchmitzandGitHub e41f1c5e5b build: notify slack on failed deployment (#5617) 2025-02-18 14:40:29 +01:00
Steffen SchmitzandGitHub c419fd69fa chore: remove superfluous container_name overwrites (#5615) 2025-02-18 13:03:45 +00:00
Steffen SchmitzandGitHub 485dc94444 feat: map langfuse data model to further otel attributes (#5614)
* feat: map langfuse data model to further otel attributes

* chore: expand tests
2025-02-18 11:29:17 +00:00
Steffen SchmitzandGitHub c233c7fa8d feat: parse mlflow attributes on otel spans (#5609)
* feat: parse mlflow attributes on otel spans

* chore: correct failing test
2025-02-18 09:07:12 +00:00
marliessophieandGitHub 0263b40987 perf(sessions): display only first 10 users; show rest in paginated popover (#5599)
* perf(sessions): show only up to 10 initial users and the rest in popover

* nit: fix header on traces page
2025-02-17 19:01:01 +00:00
Marc KlingenandGitHub 44621d0d1f chore: remove the unnecessary file (#5598)
chore: remove unnecessary file
2025-02-17 18:34:01 +00:00
marliessophieandGitHub 5bd891ddc7 style(traces): style traces delete button in more menu in table (#5596) 2025-02-17 14:59:12 +00:00
marliessophieandGitHub 8e02f86fa7 chore(page-header): add new tabsComponent (#5593)
* chore(page-header): add tabs component

* style: replace tabs component on datasets pages

* style: replace tabs component on evals pages

* nit: fix dataset item page formatting

* style: replace tabs component on prompts pages

* chore: adjust col behaviour on header

* eslint
2025-02-17 13:34:57 +00:00
Max DeichmannandGitHub d7582c66ee security: upgrade jsonpath plus (#5591) 2025-02-17 12:41:12 +00:00
marliessophieandGitHub 75bef08f0b style(page-header): optimise for mobile view (#5590)
* chore: fix demo banner

* fix(page-header): info icon relative positioning

* fix: header title wrap behaviour

* fix(page-header): have info icon follow title

* style: move dataset level buttons to more menu

* push

* push
2025-02-17 12:36:33 +00:00
Max Deichmann 5f7b50cea8 chore: release v3.28.3 2025-02-17 09:56:07 +01:00
Steffen SchmitzandGitHub a9757d144f chore: fallback to LLM http status for /api/chatCompletion (#5554) 2025-02-17 08:42:49 +00:00
Steffen SchmitzandGitHub 12fb26388c fix: accept further content type formats on otel endpoint (#5584) 2025-02-17 08:41:14 +00:00
Max Deichmann 95d9aef705 chore: release v3.28.2 2025-02-15 13:29:06 +01:00
Max Deichmann 15fea028c2 chore: release v3.28.1 2025-02-15 01:04:28 +01:00
Max DeichmannandGitHub ac6e94ee3c security: upgrade release-it (#5558)
* security: upgrade release-it

* security: upgrade release-it

* security: upgrade release-it
2025-02-14 18:59:53 +00:00
Max DeichmannandGitHub e7332bd1e5 security: upgrade dompurify (#5557) 2025-02-14 19:39:59 +01:00
Max Deichmann fe8b2ba7ae chore: release v3.28.0 2025-02-14 19:20:25 +01:00
marliessophieandGitHub 652e96ee9d fix(header): breakpoint header behaviour (#5555)
fix(header): fix breakpoint header behaviour
2025-02-14 17:49:13 +00:00
Steffen SchmitzandGitHub 40ce493b06 fix: change numeric type parsing for sessions table filters (#5553) 2025-02-14 16:21:43 +00:00
Max DeichmannGitHubellipsis-dev[bot] <65095814+ellipsis-dev[bot]@users.noreply.github.com>
aa7ba1a9c5 chore: add dashboard time filter test case (#5526)
* add tests

* Update web/src/__tests__/async/repositories/dashboard-repository.servertest.ts

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-02-14 15:22:15 +00:00
Jannik MaierhöferandGitHub 869b027110 docs: fix gif on Readme (#5502) 2025-02-14 15:18:05 +00:00
Hassieb PakzadandGitHub 1243d9022e feat(trace-ui): add graph view (#5511)
* feat(trace-ui): add graph view

* push

* rename

* fix build error
2025-02-14 15:08:15 +00:00
marliessophieandGitHub d988c07d46 style(header): adjust minor paddings and relative scroll behaviour (#5551)
* style(header): relative positioning on scroll page

* style: add padding to container layout

* chore: rm top margin
2025-02-14 14:27:46 +00:00
0a53682ed6 chore: Update README.ja.md (#5545)
Update README.ja.md

modification in Japanese and put Japanese support info.

Co-authored-by: Marc Klingen <git@marcklingen.com>
2025-02-14 14:24:15 +00:00
Steffen SchmitzandGitHub ac517c1849 chore: enhance ingestion api documentation (#5550) 2025-02-14 14:17:45 +00:00
Iaroslav VolkovandGitHub 2778be5abb feat: add TLS env variable support for Redis (#5393) 2025-02-14 13:28:15 +01:00
56e4b92fd7 feat(ui): add new langfuse header (#5451)
* chore: extract PageContainer component

* chore: add ItemBadge component

* style: redesign publish trace switch

* style: redesign detail page nav

* chore: extract copybutton

* chore: wrap all pages in `PageContainer`

* chore: add settings-container; style nits

* chore: simplify langfuse header

* chore: replace session icon in itembadge

* chore: mv lg:container to settings-container level

* chore: refactor component logic in ProjectOverview

* style: make project button primary

* style: adjust light mode sidebar colors

* fix: rm unneccessary class

* fix: eslint

* chore: update readme

* chore: adjust page header to <h2/>

* chore: adjust colors

* style: button color

* style: refactor public/private highlight

* push

* chore: rename components

* style: add breadcrumb to pages

* style: add sidebar border

* style: adjust sharing logic

* push

* chore: eslint

* fix: header alignment

* push

* push

* eslint

---------

Co-authored-by: Max Deichmann <m.deichmann@tum.de>
2025-02-14 10:39:42 +00:00
Steffen SchmitzandGitHub 613126bbfb chore: add clickhouse tags to queries (#5535) (#5538)
* chore: add clickhouse tags to queries (#5535)

* chore: add clickhouse tags to queries

* chore: linting

* chore: add dashboard tags

(cherry picked from commit 95d67d65d7)

* chore: move tags to json

* chore: move to json format

* chore: fix build

* chore: remove redundant fallback
2025-02-14 10:09:08 +00:00
537 changed files with 36834 additions and 12287 deletions
+6
View File
@@ -0,0 +1,6 @@
---
description: How to manage authorization and RBAC within full-stack langfuse features
globs: web/
alwaysApply: true
---
See [README.md](mdc:web/src/features/rbac/README.md) for details
+7
View File
@@ -0,0 +1,7 @@
---
description: How to enforce entitlements across full-stack features
globs: web/**
alwaysApply: true
---
Check [README.md](mdc:web/src/features/entitlements/README.md) to learn more about entitlements
+33
View File
@@ -0,0 +1,33 @@
---
description: How we build new full-stack features including frontend APIs, file structure, and components
globs: web/**
alwaysApply: true
---
## Framework
- Next.js
- Pages router
## File Structure
- We generally put all code related to a net-new feature into a folder within web/src/features.
- Checkout other features to learn about the common structure.
## API for frontend features
- We use TRPC.io to power full-stack features of the Langfuse Frontend
- Entry point for all trpc routes: [root.ts](mdc:web/src/server/api/root.ts)
- Authentication, see [authorization-and-rbac.mdc](mdc:.cursor/rules/authorization-and-rbac.mdc)
- Entitlements, see [entitlements.mdc](mdc:.cursor/rules/entitlements.mdc)
## Components
- We use Shadcn/ui
- Components in `@/src/components/ui`
- If a component is not installed yet, ask the user to install it for you
- When creating new custom components that generalize, we add them with a generalizable naming to `@/src/components`
## Styling
- We use Tailwind CSS
- We use a standard color palette which automatically handles light/dark mode, see [globals.css](mdc:web/src/styles/globals.css)
+16
View File
@@ -0,0 +1,16 @@
---
description: How to create new public api routes for Langfuse
globs:
alwaysApply: false
---
# Implementation
- All public api routes are in /web/src/pages/api/public
- New api routes should follow these guidelines:
- Use [withMiddlewares.ts](mdc:web/src/features/public-api/server/withMiddlewares.ts) as a wrapper
- Define types of api request and response in /web/src/features/public-api/types, use strict() for all zod objects
- Add end-to-end test, similar to [datasets-api.servertest.ts](mdc:web/src/__tests__/async/datasets-api.servertest.ts)
- Add fern configuration in /fern, learn more about structure here: https://buildwithfern.com/learn/api-definition/fern/overview
- Prompt user to regenerate the OpenAPI spec via the fern CLI
- Pagination starts at 1, query typing defined in publicApiPaginationZod, return meta in paginationMetaResponseZod
+40 -27
View File
@@ -84,6 +84,7 @@ OTEL_SERVICE_NAME="langfuse"
# AUTH_GITLAB_ISSUER=
# AUTH_GITLAB_CLIENT_AUTH_METHOD=
# AUTH_GITLAB_CHECKS=
# AUTH_GITLAB_URL=
# AUTH_AZURE_AD_CLIENT_ID=
# AUTH_AZURE_AD_CLIENT_SECRET=
# AUTH_AZURE_AD_TENANT_ID=
@@ -114,6 +115,11 @@ OTEL_SERVICE_NAME="langfuse"
# AUTH_KEYCLOAK_ALLOW_ACCOUNT_LINKING=false
# AUTH_KEYCLOAK_CLIENT_AUTH_METHOD=
# AUTH_KEYCLOAK_CHECKS=
# AUTH_WORKOS_CLIENT_ID=
# AUTH_WORKOS_CLIENT_SECRET=
# AUTH_WORKOS_ALLOW_ACCOUNT_LINKING=false
# AUTH_WORKOS_ORGANIZATION_ID=
# AUTH_WORKOS_CONNECTION_ID=
# AUTH_CUSTOM_CLIENT_ID=
# AUTH_CUSTOM_CLIENT_SECRET=
# AUTH_CUSTOM_ISSUER=
@@ -166,7 +172,32 @@ OTEL_SERVICE_NAME="langfuse"
# LANGFUSE_INIT_USER_NAME=User Name
# LANGFUSE_INIT_USER_PASSWORD=password
# Redis configuration
# REDIS_HOST=
# REDIS_PORT=
# REDIS_AUTH=
# REDIS_CONNECTION_STRING=
# REDIS_ENABLE_AUTO_PIPELINING=
# Cache configuration
# LANGFUSE_CACHE_API_KEY_ENABLED=
# LANGFUSE_CACHE_API_KEY_TTL_SECONDS=
# LANGFUSE_CACHE_PROMPT_ENABLED=
# LANGFUSE_CACHE_PROMPT_TTL_SECONDS=
# Clickhouse configuration
# CLICKHOUSE_URL=
# CLICKHOUSE_CLUSTER_NAME=default
# CLICKHOUSE_DB=default
# CLICKHOUSE_USER=
# CLICKHOUSE_PASSWORD=
# CLICKHOUSE_CLUSTER_ENABLED=true
# Ingestion configuration
# LANGFUSE_INGESTION_QUEUE_DELAY_MS=
# LANGFUSE_INGESTION_CLICKHOUSE_WRITE_BATCH_SIZE=
# LANGFUSE_INGESTION_CLICKHOUSE_WRITE_INTERVAL_MS=
# LANGFUSE_INGESTION_CLICKHOUSE_MAX_ATTEMPTS=
### START Enterprise Edition Configuration
@@ -235,19 +266,8 @@ OTEL_SERVICE_NAME="langfuse"
# Admin API
# ADMIN_API_KEY=
# Redis
# REDIS_HOST=
# REDIS_PORT=
# REDIS_AUTH=
# REDIS_CONNECTION_STRING=
# REDIS_ENABLE_AUTO_PIPELINING=
# Cache configuration
# LANGFUSE_CACHE_API_KEY_ENABLED=
# LANGFUSE_CACHE_API_KEY_TTL_SECONDS=
# LANGFUSE_CACHE_PROMPT_ENABLED=
# LANGFUSE_CACHE_PROMPT_TTL_SECONDS=
# LANGFUSE_CACHE_MODEL_MATCH_ENABLED=
# LANGFUSE_CACHE_MODEL_MATCH_TTL_SECONDS=
# Rate limiting
# LANGFUSE_RATE_LIMITS_ENABLED=
@@ -256,22 +276,15 @@ OTEL_SERVICE_NAME="langfuse"
# STRIPE_SECRET_KEY=
# STRIPE_WEBHOOK_SIGNING_SECRET=
# Betterstack Status Page
# BETTERSTACK_UPTIME_API_KEY=
# BETTERSTACK_UPTIME_STATUS_PAGE_ID=
### END Langfuse Cloud Config
## START Langfuse V3 Ingestion
### START Langfuse CI Config
# Clickhouse
# CLICKHOUSE_MIGRATION_URL=
# CLICKHOUSE_URL=
# CLICKHOUSE_USER=
# CLICKHOUSE_PASSWORD=
# CLICKHOUSE_DB=
# LANGFUSE_INIT_ORG_CLOUD_PLAN=
# Ingestion
# LANGFUSE_INGESTION_QUEUE_DELAY_MS=
# LANGFUSE_INGESTION_CLICKHOUSE_WRITE_BATCH_SIZE=
# LANGFUSE_INGESTION_CLICKHOUSE_WRITE_INTERVAL_MS=
# LANGFUSE_INGESTION_CLICKHOUSE_MAX_ATTEMPTS=
## END Langfuse V3 Ingestion
### END Langfuse CI Config
@@ -56,6 +56,12 @@ jobs:
--build-arg SENTRY_AUTH_TOKEN=${{ secrets.SENTRY_AUTH_TOKEN }} \
--build-arg SENTRY_ORG=${{ vars.SENTRY_ORG }} \
--build-arg SENTRY_PROJECT=${{ vars.SENTRY_PROJECT }} \
--build-arg NEXT_PUBLIC_DATADOG_APPLICATION_ID=${{ vars.NEXT_PUBLIC_DATADOG_APPLICATION_ID }} \
--build-arg NEXT_PUBLIC_DATADOG_CLIENT_TOKEN=${{ vars.NEXT_PUBLIC_DATADOG_CLIENT_TOKEN }} \
--build-arg NEXT_PUBLIC_DATADOG_SITE=${{ vars.NEXT_PUBLIC_DATADOG_SITE }} \
--build-arg NEXT_PUBLIC_DATADOG_ENVIRONMENT=${{ vars.NEXT_PUBLIC_DATADOG_ENVIRONMENT }} \
--build-arg NEXT_PUBLIC_DATADOG_SESSION_SAMPLE_RATE=${{ vars.NEXT_PUBLIC_DATADOG_SESSION_SAMPLE_RATE }} \
--build-arg NEXT_PUBLIC_DATADOG_SESSION_REPLAY_SAMPLE_RATE=${{ vars.NEXT_PUBLIC_DATADOG_SESSION_REPLAY_SAMPLE_RATE }} \
.
docker push $REGISTRY/$REPOSITORY:$IMAGE_TAG
- name: Render AWS ECS Task Definition
+16 -6
View File
@@ -89,7 +89,7 @@ jobs:
timeout 10 bash -c 'until curl -f http://localhost:3000/api/public/health; do sleep 2; done'
tests-web-sync:
timeout-minutes: 20
timeout-minutes: 30
runs-on: ubuntu-latest
needs:
- pre-job
@@ -134,14 +134,14 @@ jobs:
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
- name: Run dev containers
run: |
docker compose -f docker-compose.dev.yml up -d
sleep 5 # Wait for PostgreSQL to accept connections
docker compose ps
env:
POSTGRES_VERSION: ${{ matrix.postgres-version }}
- name: Seed DB
- name: Migrate DB
run: |
pnpm run db:migrate
pnpm --filter=shared ch:up
@@ -152,6 +152,7 @@ jobs:
env:
LANGFUSE_INIT_ORG_ID: "seed-org-id"
LANGFUSE_INIT_ORG_NAME: "Seed Org"
LANGFUSE_INIT_ORG_CLOUD_PLAN: "Team"
LANGFUSE_INIT_PROJECT_ID: "7a88fb47-b4e2-43b8-a06c-a5ce950dc53a"
LANGFUSE_INIT_PROJECT_NAME: "Seed Project"
LANGFUSE_INIT_PROJECT_PUBLIC_KEY: "pk-lf-1234567890"
@@ -165,7 +166,7 @@ jobs:
run: pnpm --filter=web run test-client
tests-web-async:
timeout-minutes: 20
timeout-minutes: 30
runs-on: ubuntu-latest
needs:
- pre-job
@@ -211,14 +212,14 @@ jobs:
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
- name: Run dev containers
run: |
docker compose -f docker-compose.dev${{ matrix.blob-provider }}.yml up -d
sleep 5 # Wait for PostgreSQL to accept connections
docker compose ps
env:
POSTGRES_VERSION: ${{ matrix.postgres-version }}
- name: Seed DB
- name: Migrate DB
run: |
pnpm run db:migrate
pnpm --filter=shared ch:up
@@ -229,6 +230,7 @@ jobs:
env:
LANGFUSE_INIT_ORG_ID: "seed-org-id"
LANGFUSE_INIT_ORG_NAME: "Seed Org"
LANGFUSE_INIT_ORG_CLOUD_PLAN: "Team"
LANGFUSE_INIT_PROJECT_ID: "7a88fb47-b4e2-43b8-a06c-a5ce950dc53a"
LANGFUSE_INIT_PROJECT_NAME: "Seed Project"
LANGFUSE_INIT_PROJECT_PUBLIC_KEY: "pk-lf-1234567890"
@@ -536,3 +538,11 @@ jobs:
platforms: |
linux/amd64
${{ startsWith(github.ref, 'refs/tags/') && 'linux/arm64' || '' }}
- name: Notify Slack
uses: ravsamhq/notify-slack-action@v2
if: always()
with:
status: ${{ job.status }}
notify_when: "failure"
env:
SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }}
+22
View File
@@ -0,0 +1,22 @@
name: Close inactive issues
on:
schedule:
- cron: "30 1 * * *"
jobs:
close-issues:
runs-on: ubuntu-latest
permissions:
issues: write
pull-requests: write
steps:
- uses: actions/stale@v9
with:
days-before-issue-stale: 30
days-before-issue-close: 14
stale-issue-label: "stale"
stale-issue-message: "This issue is stale because it has been open for 30 days with no activity."
close-issue-message: "This issue was closed because it has been inactive for 14 days since being marked as stale. Please reopen if the issue persists."
days-before-pr-stale: -1
days-before-pr-close: -1
repo-token: ${{ secrets.GITHUB_TOKEN }}
+1
View File
@@ -7,6 +7,7 @@
# testing
/coverage
/certs/
# database
/prisma/db.sqlite
+14 -137
View File
@@ -53,117 +53,10 @@ A good first step is to search for open [issues](https://github.com/langfuse/lan
### Architecture Overview
**Langfuse v2**
```mermaid
flowchart TB
subgraph s4["Clients"]
subgraph s2["langfuse/langfuse-python"]
Python["Python low-level SDK"]
Decorator["observe() decorator"] -->|extends| Python
OAI["OpenAI drop-in replacement"] -->|extends| Python
Llamaindex["LlamaIndex Integration"] -->|extends| Python
LCPYTHON["Langchain Python Integration"] -->|extends| Python
Langflow -->|uses| LCPYTHON
LiteLLM -->|uses| Python
end
subgraph s3["langfuse/langfuse-js"]
JS["JS SDK"]
LCJS["Langchain JS Integration"] -->|extends| JS
Flowise -->|uses| LCJS
end
end
DB[Postgres Database]
Redis[Redis]
subgraph s1["Application (langfuse/langfuse/web)"]
API[Public HTTP API]
G[TRPC API]
I[NextAuth]
H[React Frontend]
Prisma[Prisma ORM]
H --> G
H --> I
G --> I
G --- Prisma
API --- Prisma
I --- Prisma
end
Prisma --- DB
JS --- API
Python --- API
```
**Langfuse v3 (work in progress, not released yet)**
> [!NOTE]
> Infrastructure will change in Langfuse version 3.0. More in the [GitHub Discussions](https://github.com/orgs/langfuse/discussions/1902).
> `langfuse/langfuse/worker` is under active development and not recommended for production use in Langfuse 2.x.
```mermaid
flowchart TB
subgraph s4["Clients"]
subgraph s2["langfuse/langfuse-python"]
Python["Python low-level SDK"]
Decorator["observe() decorator"] -->|extends| Python
OAI["OpenAI drop-in replacement"] -->|extends| Python
Llamaindex["LlamaIndex Integration"] -->|extends| Python
LCPYTHON["Langchain Python Integration"] -->|extends| Python
Langflow -->|uses| LCPYTHON
LiteLLM -->|uses| Python
end
subgraph s3["langfuse/langfuse-js"]
JS["JS SDK"]
LCJS["Langchain JS Integration"] -->|extends| JS
Flowise -->|uses| LCJS
end
end
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
end
JS --- API
Python --- API
```
See this [diagram](https://langfuse.com/self-hosting#architecture) for an overview of the architecture.
### Network Overview
> [!NOTE]
> This will change in Langfuse version 3.0. More in the [GitHub Discussions](https://github.com/orgs/langfuse/discussions/1902).
```mermaid
flowchart LR
Browser ---|Web UI & TRPC API| App
@@ -209,11 +102,12 @@ Requirements
**Steps**
1. Fork the repository and clone it locally
2. Run the development database
1. Install [golang-migrate](https://github.com/golang-migrate/migrate/tree/master/cmd/migrate#migrate-cli) as CLI
2. Fork the repository and clone it locally
```bash
pnpm run infra:dev:up
git clone https://github.com/langfuse/langfuse.git
cd langfuse
```
3. Create an env file
@@ -222,40 +116,23 @@ Requirements
cp .env.dev.example .env
```
4. Install dependencies
4. Run the entire infrastructure in dev mode
```bash
pnpm install
pnpm run dx
```
5. Run the migrations
You will be asked whether you want to reset Postgres and ClickHouse. Confirm both with 'Y' and press enter.
All database migrations and configs are in the `shared` package.
5. Open the web app in your browser to start using Langfuse:
```bash
pnpm --filter=shared run db:migrate
- [Sign up page, http://localhost:3000](http://localhost:3000)
- [Demo project, http://localhost:3000/project/7a88fb47-b4e2-43b8-a06c-a5ce950dc53a](http://localhost:3000/project/7a88fb47-b4e2-43b8-a06c-a5ce950dc53a)
# Optional: seed the database
# pnpm run db:seed
# pnpm run db:seed:examples
# pnpm --filter=shared run db:seed:load
```
6. Log in as a test user:
6. Start the development server
```bash
pnpm run dev
```
7. Open the web app in the browser:
http://localhost:3000
8. Log in as a test user (after you ran `db:seed` command):
Username: demo@langfuse.com
Password: password
- Username: `demo@langfuse.com`
- Password: `password`
## Monorepo quickstart
+5 -5
View File
@@ -28,7 +28,7 @@
<br/>
<span>Langfuseは、サポートと機能リクエストのために <a href="https://github.com/orgs/langfuse/discussions"><strong>Github Discussions</strong></a> を利用しています。</span>
<br/>
<span><b>採用中です。</b> <a href="https://langfuse.com/careers"><strong>当社で働く</strong></a> (製品エンジニアリングおよびテクニカル・ゴートゥーマーケットのポジション)への応募をお待ちしています。</span>
<span><b>We're hiring.</b> <a href="https://langfuse.com/careers"><strong>チームに加わる</strong></a> (製品エンジニアリングおよびテクニカルGTMのポジション)への応募をお待ちしています。</span>
<br/>
<br/>
<div>
@@ -71,7 +71,7 @@
Langfuseは**オープンソースのLLMエンジニアリング**プラットフォームです。
チームが共同でAIアプリケーションを**開発、監視、評価**、および**デバッグ**するのを支援します。
Langfuseは**数分でセルフホスト可能**で、**実績に裏打ちされた**システムです。
Langfuseは**数分でセルフホスト可能**で、**多くの実績を持つ**システムです。
[![Langfuse Overview Video](https://github.com/user-attachments/assets/3926b288-ff61-4b95-8aa1-45d041c70866)](https://langfuse.com/watch-demo)
@@ -173,6 +173,7 @@ Langfuseチームによるマネージドデプロイメント。充実した無
| [Mirascope](https://langfuse.com/docs/integrations/mirascope) | ライブラリ | LLMアプリケーション構築用のPythonツールキット |
| [Ollama](https://langfuse.com/docs/integrations/ollama) | モデル(ローカル) | オープンソースLLMを手軽にローカルで実行するためのツール |
| [Amazon Bedrock](https://langfuse.com/docs/integrations/amazon-bedrock) | モデル | AWS上でファウンデーションモデルやファインチューニング済みモデルを実行 |
| [Google VertexAI and Gemini](https://langfuse.com/docs/integrations/google-vertex-ai) | モデル | Google上でファウンデーションモデルやファインチューニング済みモデルを実行 |
| [AutoGen](https://langfuse.com/docs/integrations/autogen) | エージェントフレームワーク | 分散型エージェント構築のためのオープンソースLLMプラットフォーム |
| [Flowise](https://langfuse.com/docs/integrations/flowise) | チャット/エージェント UI | JS/TSのノーコードビルダーで、カスタマイズ可能なLLMフローを構築 |
| [Langflow](https://langfuse.com/docs/integrations/langflow) | チャット/エージェント UI | PythonベースのUIで、react-flowを用いてLangChainの実験やプロトタイピングを容易に実現 |
@@ -258,6 +259,7 @@ _[Langfuseの公開トレース例](https://cloud.langfuse.com/project/cloramnkj
- 当社の[ドキュメント](https://langfuse.com/docs)は、回答を探すための最良の出発点です。内容が充実しており、継続的なメンテナンスに努めています。GitHubを通じてドキュメントへの修正提案も可能です。
- よくある質問は[Langfuse FAQ](https://langfuse.com/faq)にまとめられています。
- [Ask AI](https://langfuse.com/docs/ask-ai)を利用すれば、質問に対して即座に回答を得ることができます。
- 日本語のサポートや決済, 請求書払いなどをお求めの場合は、日本のリセラー (https://gao-ai.com) にご相談ください。
サポートチャネル:
@@ -348,9 +350,7 @@ Langfuseを利用している主要なオープンソースPythonプロジェク
### テレメトリー
デフォルトでは、Langfuseはセルフホストされたインスタンスの基本的な使用統計情報を中央サーバ(PostHog)へ自動的に報告します。
これにより、以下が可能となります:
デフォルトでは、Langfuseは以下の目的でセルフホストされたインスタンスの基本的な使用統計情報を中央サーバ(PostHog)へ自動的に報告します。
1. Langfuseの利用状況を把握し、最も重要な機能の改善に役立てる
2. 内部および外部(例:資金調達)のレポートのために全体の利用状況を追跡する
+1 -1
View File
@@ -222,7 +222,7 @@ _[Public example trace in Langfuse](https://cloud.langfuse.com/project/cloramnkj
## ⭐️ Star Us
![Star Langfuse](https://langfuse.com/images/docs/github-readme/github-star-howto.gif)
![star-langfuse-on-github](https://github.com/user-attachments/assets/79a1d816-d229-4526-aecc-097d4a19f1ad)
## 💭 Support
+4 -3
View File
@@ -48,6 +48,10 @@ services:
REDIS_HOST: ${REDIS_HOST:-redis}
REDIS_PORT: ${REDIS_PORT:-6379}
REDIS_AUTH: ${REDIS_AUTH:-myredissecret}
REDIS_TLS_ENABLED: ${REDIS_TLS_ENABLED:-false}
REDIS_TLS_CA: ${REDIS_TLS_CA:-/certs/ca.crt}
REDIS_TLS_CERT: ${REDIS_TLS_CERT:-/certs/redis.crt}
REDIS_TLS_KEY: ${REDIS_TLS_KEY:-/certs/redis.key}
restart: always
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:3000/api/public/health"]
@@ -76,8 +80,6 @@ services:
clickhouse:
image: clickhouse/clickhouse-server
user: "101:101"
container_name: clickhouse
hostname: clickhouse
environment:
CLICKHOUSE_DB: default
CLICKHOUSE_USER: clickhouse
@@ -97,7 +99,6 @@ services:
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'
-3
View File
@@ -2,8 +2,6 @@ services:
clickhouse:
image: clickhouse/clickhouse-server
user: "101:101"
container_name: clickhouse
hostname: clickhouse
environment:
CLICKHOUSE_DB: default
CLICKHOUSE_USER: clickhouse
@@ -19,7 +17,6 @@ services:
azurite:
image: mcr.microsoft.com/azure-storage/azurite
container_name: azurite
command: azurite-blob --blobHost 0.0.0.0
ports:
- "10000:10000"
+6 -9
View File
@@ -2,8 +2,6 @@ services:
clickhouse:
image: clickhouse/clickhouse-server
user: "101:101"
container_name: clickhouse
hostname: clickhouse
environment:
CLICKHOUSE_DB: default
CLICKHOUSE_USER: clickhouse
@@ -12,14 +10,13 @@ services:
- langfuse_clickhouse_data:/var/lib/clickhouse
- langfuse_clickhouse_logs:/var/log/clickhouse-server
ports:
- "8123:8123"
- "9000:9000"
- 127.0.0.1:8123:8123
- 127.0.0.1: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'
@@ -27,8 +24,8 @@ services:
MINIO_ACCESS_KEY: minio
MINIO_SECRET_KEY: miniosecret
ports:
- "9090:9000"
- "9091:9001"
- 127.0.0.1:9090:9000
- 127.0.0.1:9091:9001
volumes:
- langfuse_minio_data:/data
healthcheck:
@@ -44,7 +41,7 @@ services:
command: >
--requirepass ${REDIS_AUTH:-myredissecret}
ports:
- 6379:6379
- 127.0.0.1:6379:6379
postgres:
image: postgres:${POSTGRES_VERSION:-latest}
@@ -60,7 +57,7 @@ services:
- POSTGRES_PASSWORD=postgres
- POSTGRES_DB=postgres
ports:
- 5432:5432
- 127.0.0.1:5432:5432
volumes:
- langfuse_postgres_data:/var/lib/postgresql/data
+29 -22
View File
@@ -1,3 +1,8 @@
# Make sure to update the credential placeholders with your own secrets.
# We mark them with # CHANGEME in the file below.
# In addition, we recommend to restrict inbound traffic on the host to langfuse-web (port 3000) and minio (port 9090) only.
# All other components are bound to localhost (127.0.0.1) to only accept connections from the local machine.
# External connections from other machines will not be able to reach these services directly.
services:
langfuse-worker:
image: langfuse/langfuse-worker:3
@@ -12,29 +17,29 @@ services:
clickhouse:
condition: service_healthy
ports:
- "3030:3030"
- 127.0.0.1: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`
DATABASE_URL: postgresql://postgres:postgres@postgres:5432/postgres # CHANGEME
SALT: "mysalt" # CHANGEME
ENCRYPTION_KEY: "0000000000000000000000000000000000000000000000000000000000000000" # CHANGEME: 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_PASSWORD: ${CLICKHOUSE_PASSWORD:-clickhouse} # CHANGEME
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_SECRET_ACCESS_KEY: ${LANGFUSE_S3_EVENT_UPLOAD_SECRET_ACCESS_KEY:-miniosecret} # CHANGEME
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_SECRET_ACCESS_KEY: ${LANGFUSE_S3_MEDIA_UPLOAD_SECRET_ACCESS_KEY:-miniosecret} # CHANGEME
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/}
@@ -42,18 +47,22 @@ services:
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}
REDIS_AUTH: ${REDIS_AUTH:-myredissecret} # CHANGEME
REDIS_TLS_ENABLED: ${REDIS_TLS_ENABLED:-false}
REDIS_TLS_CA: ${REDIS_TLS_CA:-/certs/ca.crt}
REDIS_TLS_CERT: ${REDIS_TLS_CERT:-/certs/redis.crt}
REDIS_TLS_KEY: ${REDIS_TLS_KEY:-/certs/redis.key}
langfuse-web:
image: langfuse/langfuse:3
restart: always
depends_on: *langfuse-depends-on
ports:
- "3000:3000"
- 3000:3000
environment:
<<: *langfuse-worker-env
NEXTAUTH_URL: http://localhost:3000
NEXTAUTH_SECRET: mysecret
NEXTAUTH_SECRET: mysecret # CHANGEME
LANGFUSE_INIT_ORG_ID: ${LANGFUSE_INIT_ORG_ID:-}
LANGFUSE_INIT_ORG_NAME: ${LANGFUSE_INIT_ORG_NAME:-}
LANGFUSE_INIT_PROJECT_ID: ${LANGFUSE_INIT_PROJECT_ID:-}
@@ -68,18 +77,16 @@ services:
image: clickhouse/clickhouse-server
restart: always
user: "101:101"
container_name: clickhouse
hostname: clickhouse
environment:
CLICKHOUSE_DB: default
CLICKHOUSE_USER: clickhouse
CLICKHOUSE_PASSWORD: clickhouse
CLICKHOUSE_PASSWORD: clickhouse # CHANGEME
volumes:
- langfuse_clickhouse_data:/var/lib/clickhouse
- langfuse_clickhouse_logs:/var/log/clickhouse-server
ports:
- "8123:8123"
- "9000:9000"
- 127.0.0.1:8123:8123
- 127.0.0.1:9000:9000
healthcheck:
test: wget --no-verbose --tries=1 --spider http://localhost:8123/ping || exit 1
interval: 5s
@@ -90,16 +97,15 @@ services:
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
MINIO_ROOT_PASSWORD: miniosecret # CHANGEME
ports:
- "9090:9000"
- "9091:9001"
- 9090:9000
- 127.0.0.1:9091:9001
volumes:
- langfuse_minio_data:/data
healthcheck:
@@ -112,10 +118,11 @@ services:
redis:
image: redis:7
restart: always
# CHANGEME: row below to secure redis password
command: >
--requirepass ${REDIS_AUTH:-myredissecret}
ports:
- 6379:6379
- 127.0.0.1:6379:6379
healthcheck:
test: ["CMD", "redis-cli", "ping"]
interval: 3s
@@ -132,10 +139,10 @@ services:
retries: 10
environment:
POSTGRES_USER: postgres
POSTGRES_PASSWORD: postgres
POSTGRES_PASSWORD: postgres # CHANGEME
POSTGRES_DB: postgres
ports:
- 5432:5432
- 127.0.0.1:5432:5432
volumes:
- langfuse_postgres_data:/var/lib/postgresql/data
View File
+2 -2
View File
@@ -26,9 +26,9 @@
"dependencies": {
"@langfuse/shared": "workspace:*",
"@opentelemetry/api": ">=1.0.0 <1.10.0",
"axios": "^1.7.7",
"axios": "^1.8.2",
"https-proxy-agent": "^7.0.6",
"next": "^14.2.21",
"next": "^14.2.25",
"next-auth": "^4.24.11",
"zod": "^3.23.8"
},
@@ -0,0 +1,165 @@
# yaml-language-server: $schema=https://raw.githubusercontent.com/fern-api/fern/main/fern.schema.json
imports:
commons: ./commons.yml
pagination: ./utils/pagination.yml
service:
auth: true
base-path: /api/public
endpoints:
listQueues:
docs: Get all annotation queues
method: GET
path: /annotation-queues
request:
name: GetAnnotationQueuesRequest
query-parameters:
page:
type: optional<integer>
docs: page number, starts at 1
limit:
type: optional<integer>
docs: limit of items per page
response: PaginatedAnnotationQueues
getQueue:
docs: Get an annotation queue by ID
method: GET
path: /annotation-queues/{queueId}
path-parameters:
queueId:
type: string
docs: The unique identifier of the annotation queue
response: AnnotationQueue
listQueueItems:
docs: Get items for a specific annotation queue
method: GET
path: /annotation-queues/{queueId}/items
path-parameters:
queueId:
type: string
docs: The unique identifier of the annotation queue
request:
name: GetAnnotationQueueItemsRequest
query-parameters:
status:
type: optional<AnnotationQueueStatus>
docs: Filter by status
page:
type: optional<integer>
docs: page number, starts at 1
limit:
type: optional<integer>
docs: limit of items per page
response: PaginatedAnnotationQueueItems
getQueueItem:
docs: Get a specific item from an annotation queue
method: GET
path: /annotation-queues/{queueId}/items/{itemId}
path-parameters:
queueId:
type: string
docs: The unique identifier of the annotation queue
itemId:
type: string
docs: The unique identifier of the annotation queue item
response: AnnotationQueueItem
createQueueItem:
docs: Add an item to an annotation queue
method: POST
path: /annotation-queues/{queueId}/items
path-parameters:
queueId:
type: string
docs: The unique identifier of the annotation queue
request: CreateAnnotationQueueItemRequest
response: AnnotationQueueItem
updateQueueItem:
docs: Update an annotation queue item
method: PATCH
path: /annotation-queues/{queueId}/items/{itemId}
path-parameters:
queueId:
type: string
docs: The unique identifier of the annotation queue
itemId:
type: string
docs: The unique identifier of the annotation queue item
request: UpdateAnnotationQueueItemRequest
response: AnnotationQueueItem
deleteQueueItem:
docs: Remove an item from an annotation queue
method: DELETE
path: /annotation-queues/{queueId}/items/{itemId}
path-parameters:
queueId:
type: string
docs: The unique identifier of the annotation queue
itemId:
type: string
docs: The unique identifier of the annotation queue item
response: DeleteAnnotationQueueItemResponse
types:
AnnotationQueueStatus:
enum:
- PENDING
- COMPLETED
AnnotationQueueObjectType:
enum:
- TRACE
- OBSERVATION
AnnotationQueue:
properties:
id: string
name: string
description: optional<string>
scoreConfigIds: list<string>
createdAt: datetime
updatedAt: datetime
AnnotationQueueItem:
properties:
id: string
queueId: string
objectId: string
objectType: AnnotationQueueObjectType
status: AnnotationQueueStatus
completedAt: optional<datetime>
createdAt: datetime
updatedAt: datetime
PaginatedAnnotationQueues:
properties:
data: list<AnnotationQueue>
meta: pagination.MetaResponse
PaginatedAnnotationQueueItems:
properties:
data: list<AnnotationQueueItem>
meta: pagination.MetaResponse
CreateAnnotationQueueItemRequest:
properties:
objectId: string
objectType: AnnotationQueueObjectType
status:
type: optional<AnnotationQueueStatus>
docs: Defaults to PENDING for new queue items
UpdateAnnotationQueueItemRequest:
properties:
status:
type: optional<AnnotationQueueStatus>
DeleteAnnotationQueueItemResponse:
properties:
success: boolean
message: string
+12
View File
@@ -38,6 +38,9 @@ types:
public:
type: optional<boolean>
docs: Public traces are accessible via url without login
environment:
type: optional<string>
docs: The environment from which this trace originated. Can be any lowercase alphanumeric string with hyphens and underscores that does not start with 'langfuse'.
TraceWithDetails: # GET /traces
extends: Trace
properties:
@@ -79,6 +82,9 @@ types:
id: string
createdAt: datetime
projectId: string
environment:
type: optional<string>
docs: The environment from which this session originated.
SessionWithTraces:
extends: Session
properties:
@@ -145,6 +151,9 @@ types:
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.
environment:
type: optional<string>
docs: The environment from which this observation originated. Can be any lowercase alphanumeric string with hyphens and underscores that does not start with 'langfuse'.
ObservationsView:
extends: Observation
@@ -249,6 +258,9 @@ types:
queueId:
type: optional<string>
docs: Reference an annotation queue on a score. Populated if the score was initially created in an annotation queue.
environment:
type: optional<string>
docs: The environment from which this score originated. Can be any lowercase alphanumeric string with hyphens and underscores that does not start with 'langfuse'.
NumericScore:
extends: BaseScore
properties:
@@ -37,8 +37,21 @@ service:
type: optional<integer>
docs: limit of items per page
response: PaginatedDatasetItems
delete:
docs: Delete a dataset item and all its run items. This action is irreversible.
method: DELETE
path: /dataset-items/{id}
path-parameters:
id:
type: string
response: DeleteDatasetItemResponse
types:
DeleteDatasetItemResponse:
properties:
message:
type: string
docs: Success message after deletion
CreateDatasetItemRequest:
properties:
datasetName: string
+11
View File
@@ -41,6 +41,14 @@ service:
datasetName: string
runName: string
response: commons.DatasetRunWithItems
deleteRun:
method: DELETE
docs: Delete a dataset run and all its run items. This action is irreversible.
path: /datasets/{datasetName}/runs/{runName}
path-parameters:
datasetName: string
runName: string
response: DeleteDatasetRunResponse
getRuns:
method: GET
docs: Get dataset runs
@@ -72,3 +80,6 @@ types:
properties:
data: list<commons.DatasetRun>
meta: pagination.MetaResponse
DeleteDatasetRunResponse:
properties:
message: string
+92 -5
View File
@@ -8,7 +8,15 @@ service:
endpoints:
batch:
docs: |
Batched ingestion for Langfuse Tracing. If you want to use tracing via the API, such as to build your own Langfuse client implementation, this is the only API route you need to implement.
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.
Within each batch, there can be multiple events.
Each event has a type, an id, a timestamp, metadata and a body.
Internally, we refer to this as the "event envelope" as it tells us something about the event but not the trace.
We use the event id within this envelope to deduplicate messages to avoid processing the same event twice, i.e. the event id should be unique per request.
The event.body.id is the ID of the actual trace and will be used for updates and will be visible within the Langfuse App.
I.e. if you want to update a trace, you'd use the same body id, but separate event IDs.
Notes:
- Introduction to data model: https://langfuse.com/docs/tracing-data-model
@@ -29,6 +37,70 @@ service:
response:
type: IngestionResponse
status-code: 207
examples:
# Trace Create Request
- request:
batch:
- id: abcdef-1234-5678-90ab
timestamp: "2022-01-01T00:00:00.000Z"
type: "trace-create"
body:
id: abcdef-1234-5678-90ab
timestamp: "2022-01-01T00:00:00.000Z"
environment: "production"
name: "My Trace"
userId: "1234-5678-90ab-cdef"
input: "My input"
output: "My output"
sessionId: "1234-5678-90ab-cdef"
release: "1.0.0"
version: "1.0.0"
metadata: "My metadata"
tags: ["tag1", "tag2"]
public: true
response:
body:
successes:
- id: abcdef-1234-5678-90ab
status: 201
errors: []
# Observation Create Request
- request:
batch:
- id: abcdef-1234-5678-90ab
timestamp: "2022-01-01T00:00:00.000Z"
type: "span-create"
body:
id: abcdef-1234-5678-90ab
traceId: "1234-5678-90ab-cdef"
startTime: "2022-01-01T00:00:00.000Z"
environment: "test"
response:
body:
successes:
- id: abcdef-1234-5678-90ab
status: 201
errors: []
# Score Create Request
- request:
batch:
- id: abcdef-1234-5678-90ab
timestamp: "2022-01-01T00:00:00.000Z"
type: "score-create"
body:
id: abcdef-1234-5678-90ab
traceId: "1234-5678-90ab-cdef"
name: "My Score"
value: 0.9
environment: "default"
response:
body:
successes:
- id: abcdef-1234-5678-90ab
status: 201
errors: []
types:
IngestionEvent:
@@ -99,6 +171,7 @@ types:
statusMessage: optional<string>
parentObservationId: optional<string>
version: optional<string>
environment: optional<string>
CreateEventBody:
extends: OptionalObservationBody
@@ -163,6 +236,7 @@ types:
level: optional<commons.ObservationLevel>
statusMessage: optional<string>
parentObservationId: optional<string>
environment: optional<string>
TraceBody:
properties:
@@ -177,6 +251,7 @@ types:
version: optional<string>
metadata: optional<unknown>
tags: optional<list<string>>
environment: optional<string>
public:
type: optional<boolean>
docs: Make trace publicly accessible via url
@@ -190,6 +265,7 @@ types:
id: optional<string>
traceId: string
name: string
environment: optional<string>
value:
type: commons.CreateScoreValue
docs: The value of the score. Must be passed as string for categorical scores, and numeric for boolean and numeric scores. Boolean score values must equal either 1 or 0 (true or false)
@@ -318,16 +394,27 @@ types:
successes: list<IngestionSuccess>
errors: list<IngestionError>
OpenAIUsageSchema:
OpenAICompletionUsageSchema:
docs: OpenAI Usage schema from (Chat-)Completion APIs
properties:
prompt_tokens: integer
completion_tokens: integer
total_tokens: integer
prompt_tokens_details: optional<map<string, integer>>
completion_tokens_details: optional<map<string, integer>>
prompt_tokens_details: optional<map<string, optional<integer>>>
completion_tokens_details: optional<map<string, optional<integer>>>
OpenAIResponseUsageSchema:
docs: OpenAI Usage schema from Response API
properties:
input_tokens: integer
output_tokens: integer
total_tokens: integer
input_tokens_details: optional<map<string, optional<integer>>>
output_tokens_details: optional<map<string, optional<integer>>>
UsageDetails:
discriminated: false
union:
- map<string, integer>
- OpenAIUsageSchema
- OpenAICompletionUsageSchema
- OpenAIResponseUsageSchema
+4
View File
@@ -29,6 +29,10 @@ service:
type: optional<string>
allow-multiple: true
docs: Optional filter for metrics where traces include all of these tags
environment:
type: optional<string>
allow-multiple: true
docs: Optional filter for metrics where events include any of these environments
fromTimestamp:
type: optional<datetime>
docs: Optional filter to only include traces and observations on or after a certain datetime (ISO 8601)
@@ -33,6 +33,10 @@ service:
type: optional<string>
traceId: optional<string>
parentObservationId: optional<string>
environment:
type: optional<string>
allow-multiple: true
docs: Optional filter for observations where the environment is one of the provided values.
fromStartTime:
type: optional<datetime>
docs: Retrieve only observations with a start_time or or after this datetime (ISO 8601).
+3
View File
@@ -128,6 +128,9 @@ types:
commitMessage:
type: optional<string>
docs: Commit message for this prompt version.
resolutionGraph:
type: optional<map<string, unknown>>
docs: The dependency resolution graph for the current prompt. Null if prompt has no dependencies.
ChatMessage:
properties:
+12
View File
@@ -37,6 +37,10 @@ service:
toTimestamp:
type: optional<datetime>
docs: Optional filter to only include scores created before a certain datetime (ISO 8601)
environment:
type: optional<string>
allow-multiple: true
docs: Optional filter for scores where the environment is one of the provided values.
source:
type: optional<commons.ScoreSource>
docs: Retrieve only scores from a specific source.
@@ -91,6 +95,9 @@ types:
docs: The value of the score. Must be passed as string for categorical scores, and numeric for boolean and numeric scores. Boolean score values must equal either 1 or 0 (true or false)
observationId: optional<string>
comment: optional<string>
environment:
type: optional<string>
docs: The environment of the score. Can be any lowercase alphanumeric string with hyphens and underscores that does not start with 'langfuse'.
dataType:
type: optional<commons.ScoreDataType>
docs: The data type of the score. When passing a configId this field is inferred. Otherwise, this field must be passed or will default to numeric.
@@ -113,10 +120,12 @@ types:
dataType: "NUMERIC"
configId: "9203-4567-89ab-cdef"
traceId: "cdef-1234-5678-90ab"
environment: "test"
- value:
name: "toxicity"
value: "not toxic"
traceId: "cdef-1234-5678-90ab"
environment: "production"
- value:
name: "correctness"
value: "partially correct"
@@ -147,6 +156,9 @@ types:
tags:
type: optional<list<string>>
docs: A list of tags associated with the trace referenced by score
environment:
type: optional<string>
docs: The environment of the trace referenced by score
GetScoresResponseDataNumeric:
extends: commons.NumericScore
+4
View File
@@ -25,6 +25,10 @@ service:
toTimestamp:
type: optional<datetime>
docs: Optional filter to only include sessions created before a certain datetime (ISO 8601)
environment:
type: optional<string>
allow-multiple: true
docs: Optional filter for sessions where the environment is one of the provided values.
response: PaginatedSessions
get:
docs: Get a session. Please note that `traces` on this endpoint are not paginated, if you plan to fetch large sessions, consider `GET /api/public/traces?sessionId=<sessionId>`
+28
View File
@@ -15,6 +15,15 @@ service:
type: string
docs: The unique langfuse identifier of a trace
response: commons.TraceWithFullDetails
delete:
docs: Delete a specific trace
method: DELETE
path: /traces/{traceId}
path-parameters:
traceId:
type: string
docs: The unique langfuse identifier of the trace to delete
response: DeleteTraceResponse
list:
docs: Get list of traces
method: GET
@@ -50,13 +59,32 @@ service:
release:
type: optional<string>
docs: Optional filter to only include traces with a certain release.
environment:
type: optional<string>
allow-multiple: true
docs: Optional filter for traces where the environment is one of the provided values.
response: Traces
deleteMultiple:
docs: Delete multiple traces
method: DELETE
path: /traces
request:
name: DeleteTracesRequest
body:
properties:
traceIds:
type: list<string>
docs: List of trace IDs to delete
response: DeleteTraceResponse
types:
Traces:
properties:
data: list<commons.TraceWithDetails>
meta: pagination.MetaResponse
DeleteTraceResponse:
properties:
message: string
Sort:
properties:
id: string
+7
View File
@@ -19,6 +19,13 @@ groups:
pydantic_config:
require_optional_fields: false
use_str_enums: false
# - name: fernapi/fern-java-sdk
# version: 2.20.1
# output:
# location: local-file-system
# path: ../../../../langfuse-java/src/main/java/com/langfuse/client/
# config:
# client-class-name: LangfuseClient
- name: fernapi/fern-postman
version: 0.0.45
output:
+1 -2
View File
@@ -1,5 +1,4 @@
{
"organization": "langfuse",
"version": "0.43.7"
"version": "0.56.0"
}
+3 -3
View File
@@ -1,6 +1,6 @@
{
"name": "langfuse",
"version": "3.27.2",
"version": "3.45.2",
"author": "engineering@langfuse.com",
"license": "MIT",
"private": true,
@@ -29,12 +29,12 @@
"prepare": "husky"
},
"devDependencies": {
"@release-it/bumper": "^6.0.1",
"@release-it/bumper": "^7.0.1",
"braces": "3.0.3",
"dotenv-cli": "^7.4.2",
"husky": "^9.0.11",
"prettier": "^3.3.3",
"release-it": "^17.3.0",
"release-it": "^18.1.2",
"turbo": "^1.13.4"
},
"release-it": {
@@ -0,0 +1,3 @@
ALTER TABLE traces ON CLUSTER default DROP COLUMN IF EXISTS environment;
ALTER TABLE observations ON CLUSTER default DROP COLUMN IF EXISTS environment;
ALTER TABLE scores ON CLUSTER default DROP COLUMN IF EXISTS environment;
@@ -0,0 +1,3 @@
ALTER TABLE traces ON CLUSTER default ADD COLUMN environment LowCardinality(String) DEFAULT 'default' AFTER project_id;
ALTER TABLE observations ON CLUSTER default ADD COLUMN environment LowCardinality(String) DEFAULT 'default' AFTER project_id;
ALTER TABLE scores ON CLUSTER default ADD COLUMN environment LowCardinality(String) DEFAULT 'default' AFTER project_id;
@@ -0,0 +1,7 @@
-- Drop materialized views
DROP VIEW IF EXISTS project_environments_traces_mv ON CLUSTER default;
DROP VIEW IF EXISTS project_environments_observations_mv ON CLUSTER default;
DROP VIEW IF EXISTS project_environments_scores_mv ON CLUSTER default;
-- Drop the project_environments table
DROP TABLE IF EXISTS project_environments ON CLUSTER default;
@@ -0,0 +1,30 @@
-- Create the project_environments table with AggregatingMergeTree engine
CREATE TABLE project_environments ON CLUSTER default (
`project_id` String,
`environments` SimpleAggregateFunction(groupUniqArrayArray, Array(String))
) ENGINE = ReplicatedAggregatingMergeTree
ORDER BY (project_id);
-- Create materialized view for traces
CREATE MATERIALIZED VIEW project_environments_traces_mv ON CLUSTER default TO project_environments AS
SELECT
project_id,
groupUniqArray(environment) AS environments
FROM traces
GROUP BY project_id;
-- Create materialized view for observations
CREATE MATERIALIZED VIEW project_environments_observations_mv ON CLUSTER default TO project_environments AS
SELECT
project_id,
groupUniqArray(environment) AS environments
FROM observations
GROUP BY project_id;
-- Create materialized view for scores
CREATE MATERIALIZED VIEW project_environments_scores_mv ON CLUSTER default TO project_environments AS
SELECT
project_id,
groupUniqArray(environment) AS environments
FROM scores
GROUP BY project_id;
@@ -0,0 +1,3 @@
ALTER TABLE traces DROP COLUMN IF EXISTS environment;
ALTER TABLE observations DROP COLUMN IF EXISTS environment;
ALTER TABLE scores DROP COLUMN IF EXISTS environment;
@@ -0,0 +1,3 @@
ALTER TABLE traces ADD COLUMN environment LowCardinality(String) DEFAULT 'default' AFTER project_id;
ALTER TABLE observations ADD COLUMN environment LowCardinality(String) DEFAULT 'default' AFTER project_id;
ALTER TABLE scores ADD COLUMN environment LowCardinality(String) DEFAULT 'default' AFTER project_id;
@@ -0,0 +1,7 @@
-- Drop materialized views
DROP VIEW IF EXISTS project_environments_traces_mv;
DROP VIEW IF EXISTS project_environments_observations_mv;
DROP VIEW IF EXISTS project_environments_scores_mv;
-- Drop the project_environments table
DROP TABLE IF EXISTS project_environments;
@@ -0,0 +1,30 @@
-- Create the project_environments table with AggregatingMergeTree engine
CREATE TABLE project_environments (
`project_id` String,
`environments` SimpleAggregateFunction(groupUniqArrayArray, Array(String))
) ENGINE = AggregatingMergeTree
ORDER BY (project_id);
-- Create materialized view for traces
CREATE MATERIALIZED VIEW project_environments_traces_mv TO project_environments AS
SELECT
project_id,
groupUniqArray(environment) AS environments
FROM traces
GROUP BY project_id;
-- Create materialized view for observations
CREATE MATERIALIZED VIEW project_environments_observations_mv TO project_environments AS
SELECT
project_id,
groupUniqArray(environment) AS environments
FROM observations
GROUP BY project_id;
-- Create materialized view for scores
CREATE MATERIALIZED VIEW project_environments_scores_mv TO project_environments AS
SELECT
project_id,
groupUniqArray(environment) AS environments
FROM scores
GROUP BY project_id;
+1 -6
View File
@@ -1,9 +1,5 @@
import {
clickhouseClient,
ObservationRecordReadType,
} from "@langfuse/shared/src/server";
import { prisma } from "../../src/db";
import { redis } from "@langfuse/shared/src/server";
import { ObservationRecordReadType, redis } from "../../src/server";
import { prepareClickhouse } from "../../scripts/prepareClickhouse";
import { createDatasets } from "../../prisma/seed";
import { queryClickhouse } from "../../src/server/repositories/clickhouse";
@@ -65,7 +61,6 @@ async function main() {
} catch (error) {
console.error("Error during Clickhouse preparation:", error);
} finally {
await clickhouseClient().close();
await prisma.$disconnect();
redis?.disconnect();
console.log("Disconnected from Clickhouse.");
+5 -3
View File
@@ -33,7 +33,7 @@
"scripts": {
"build": "tsc",
"dev": "tsc --watch",
"lint": "eslint . --ext .js,.jsx,.ts,.tsx --max-warnings 110",
"lint": "eslint . --ext .js,.jsx,.ts,.tsx --max-warnings 64",
"lint:fix": "eslint . --ext .js,.jsx,.ts,.tsx --fix",
"db:migrate": "DISABLE_ERD=false dotenv -e ../../.env -- npx prisma migrate dev",
"db:push": "DISABLE_ERD=false dotenv -e ../../.env -- npx prisma db push",
@@ -47,7 +47,7 @@
"ch:down": "bash clickhouse/scripts/down.sh",
"ch:drop": "bash clickhouse/scripts/drop.sh",
"ch:reset": "pnpm run ch:down && pnpm run ch:up && pnpm run ch:seed",
"ch:seed": "dotenv -e ../../.env -- tsx clickhouse/scripts/seed.ts",
"ch:seed": "dotenv -e ../../.env -- ts-node -r tsconfig-paths/register -r dotenv/config --compiler-options '{\"module\":\"CommonJS\"}' clickhouse/scripts/seed.ts",
"load:setup": "dotenv -e ../../.env -- tsx scripts/load-seed.ts"
},
"prisma": {
@@ -64,6 +64,7 @@
"@langchain/anthropic": "^0.3.12",
"@langchain/aws": "^0.1.3",
"@langchain/core": "^0.3.37",
"@langchain/google-genai": "^0.1.9",
"@langchain/google-vertexai": "^0.1.8",
"@langchain/openai": "^0.3.17",
"@opentelemetry/api": ">=1.0.0 <1.10.0",
@@ -71,7 +72,7 @@
"@react-email/components": "^0.0.19",
"@react-email/render": "^0.0.15",
"@types/bcryptjs": "^2.4.6",
"axios": "^1.7.7",
"axios": "^1.8.2",
"bcryptjs": "^2.4.3",
"bullmq": "^5.34.10",
"dd-trace": "^5.36.0",
@@ -83,6 +84,7 @@
"langchain": "^0.3.15",
"langfuse-langchain": "3.30.3",
"lodash": "^4.17.21",
"lossless-json": "^4.0.2",
"next-auth": "^4.24.11",
"nodemailer": "^6.9.15",
"prisma-extension-kysely": "^2.1.0",
+122 -145
View File
@@ -12,25 +12,25 @@ export const Role = {
NONE: "NONE"
} as const;
export type Role = (typeof Role)[keyof typeof Role];
export const ObservationType = {
export const LegacyPrismaObservationType = {
SPAN: "SPAN",
EVENT: "EVENT",
GENERATION: "GENERATION"
} as const;
export type ObservationType = (typeof ObservationType)[keyof typeof ObservationType];
export const ObservationLevel = {
export type LegacyPrismaObservationType = (typeof LegacyPrismaObservationType)[keyof typeof LegacyPrismaObservationType];
export const LegacyPrismaObservationLevel = {
DEBUG: "DEBUG",
DEFAULT: "DEFAULT",
WARNING: "WARNING",
ERROR: "ERROR"
} as const;
export type ObservationLevel = (typeof ObservationLevel)[keyof typeof ObservationLevel];
export const ScoreSource = {
export type LegacyPrismaObservationLevel = (typeof LegacyPrismaObservationLevel)[keyof typeof LegacyPrismaObservationLevel];
export const LegacyPrismaScoreSource = {
ANNOTATION: "ANNOTATION",
API: "API",
EVAL: "EVAL"
} as const;
export type ScoreSource = (typeof ScoreSource)[keyof typeof ScoreSource];
export type LegacyPrismaScoreSource = (typeof LegacyPrismaScoreSource)[keyof typeof LegacyPrismaScoreSource];
export const ScoreDataType = {
CATEGORICAL: "CATEGORICAL",
NUMERIC: "NUMERIC",
@@ -59,6 +59,11 @@ export const CommentObjectType = {
PROMPT: "PROMPT"
} as const;
export type CommentObjectType = (typeof CommentObjectType)[keyof typeof CommentObjectType];
export const AuditLogRecordType = {
USER: "USER",
API_KEY: "API_KEY"
} as const;
export type AuditLogRecordType = (typeof AuditLogRecordType)[keyof typeof AuditLogRecordType];
export const JobType = {
EVAL: "EVAL"
} as const;
@@ -75,6 +80,12 @@ export const JobExecutionStatus = {
CANCELLED: "CANCELLED"
} as const;
export type JobExecutionStatus = (typeof JobExecutionStatus)[keyof typeof JobExecutionStatus];
export const BlobStorageIntegrationType = {
S3: "S3",
S3_COMPATIBLE: "S3_COMPATIBLE",
AZURE_BLOB_STORAGE: "AZURE_BLOB_STORAGE"
} as const;
export type BlobStorageIntegrationType = (typeof BlobStorageIntegrationType)[keyof typeof BlobStorageIntegrationType];
export type Account = {
id: string;
user_id: string;
@@ -132,9 +143,11 @@ export type AuditLog = {
id: string;
created_at: Generated<Timestamp>;
updated_at: Generated<Timestamp>;
user_id: string;
type: Generated<AuditLogRecordType>;
api_key_id: string | null;
user_id: string | null;
org_id: string;
user_org_role: string;
user_org_role: string | null;
project_id: string | null;
user_project_role: string | null;
resource_type: string;
@@ -181,6 +194,23 @@ export type BillingMeterBackup = {
created_at: Generated<Timestamp>;
updated_at: Generated<Timestamp>;
};
export type BlobStorageIntegration = {
project_id: string;
type: BlobStorageIntegrationType;
bucket_name: string;
prefix: string;
access_key_id: string;
secret_access_key: string;
region: string;
endpoint: string | null;
force_path_style: boolean;
next_sync_at: Timestamp | null;
last_sync_at: Timestamp | null;
enabled: boolean;
export_frequency: string;
created_at: Generated<Timestamp>;
updated_at: Generated<Timestamp>;
};
export type Comment = {
id: string;
project_id: string;
@@ -267,6 +297,7 @@ export type JobConfiguration = {
variable_mapping: unknown;
sampling: string;
delay: number;
time_scope: Generated<string[]>;
};
export type JobExecution = {
id: string;
@@ -283,6 +314,77 @@ export type JobExecution = {
job_input_dataset_item_id: string | null;
job_output_score_id: string | null;
};
export type LegacyPrismaObservation = {
id: string;
trace_id: string | null;
project_id: string;
type: LegacyPrismaObservationType;
start_time: Generated<Timestamp>;
end_time: Timestamp | null;
name: string | null;
metadata: unknown | null;
parent_observation_id: string | null;
level: Generated<LegacyPrismaObservationLevel>;
status_message: string | null;
version: string | null;
created_at: Generated<Timestamp>;
updated_at: Generated<Timestamp>;
model: string | null;
internal_model: string | null;
internal_model_id: string | null;
modelParameters: unknown | null;
input: unknown | null;
output: unknown | null;
prompt_tokens: Generated<number>;
completion_tokens: Generated<number>;
total_tokens: Generated<number>;
unit: string | null;
input_cost: string | null;
output_cost: string | null;
total_cost: string | null;
calculated_input_cost: string | null;
calculated_output_cost: string | null;
calculated_total_cost: string | null;
completion_start_time: Timestamp | null;
prompt_id: string | null;
};
export type LegacyPrismaScore = {
id: string;
timestamp: Generated<Timestamp>;
project_id: string;
name: string;
value: number | null;
source: LegacyPrismaScoreSource;
author_user_id: string | null;
comment: string | null;
trace_id: string;
observation_id: string | null;
config_id: string | null;
string_value: string | null;
queue_id: string | null;
created_at: Generated<Timestamp>;
updated_at: Generated<Timestamp>;
data_type: Generated<ScoreDataType>;
};
export type LegacyPrismaTrace = {
id: string;
external_id: string | null;
timestamp: Generated<Timestamp>;
name: string | null;
user_id: string | null;
metadata: unknown | null;
release: string | null;
version: string | null;
project_id: string;
public: Generated<boolean>;
bookmarked: Generated<boolean>;
tags: Generated<string[]>;
input: unknown | null;
output: unknown | null;
session_id: string | null;
created_at: Generated<Timestamp>;
updated_at: Generated<Timestamp>;
};
export type LlmApiKeys = {
id: string;
created_at: Generated<Timestamp>;
@@ -339,40 +441,6 @@ export type Model = {
tokenizer_id: string | null;
tokenizer_config: unknown | null;
};
export type Observation = {
id: string;
trace_id: string | null;
project_id: string;
type: ObservationType;
start_time: Generated<Timestamp>;
end_time: Timestamp | null;
name: string | null;
metadata: unknown | null;
parent_observation_id: string | null;
level: Generated<ObservationLevel>;
status_message: string | null;
version: string | null;
created_at: Generated<Timestamp>;
updated_at: Generated<Timestamp>;
model: string | null;
internal_model: string | null;
internal_model_id: string | null;
modelParameters: unknown | null;
input: unknown | null;
output: unknown | null;
prompt_tokens: Generated<number>;
completion_tokens: Generated<number>;
total_tokens: Generated<number>;
unit: string | null;
input_cost: string | null;
output_cost: string | null;
total_cost: string | null;
calculated_input_cost: string | null;
calculated_output_cost: string | null;
calculated_total_cost: string | null;
completion_start_time: Timestamp | null;
prompt_id: string | null;
};
export type ObservationMedia = {
id: string;
project_id: string;
@@ -383,43 +451,6 @@ export type ObservationMedia = {
observation_id: string;
field: string;
};
export type ObservationView = {
id: string;
trace_id: string | null;
project_id: string;
type: ObservationType;
start_time: Timestamp;
end_time: Timestamp | null;
name: string | null;
metadata: unknown | null;
parent_observation_id: string | null;
level: Generated<ObservationLevel>;
status_message: string | null;
version: string | null;
created_at: Timestamp;
updated_at: Timestamp;
model: string | null;
modelParameters: unknown | null;
input: unknown | null;
output: unknown | null;
prompt_tokens: Generated<number>;
completion_tokens: Generated<number>;
total_tokens: Generated<number>;
unit: string | null;
completion_start_time: Timestamp | null;
prompt_id: string | null;
prompt_name: string | null;
prompt_version: number | null;
model_id: string | null;
input_price: string | null;
output_price: string | null;
total_price: string | null;
calculated_input_cost: string | null;
calculated_output_cost: string | null;
calculated_total_cost: string | null;
latency: number | null;
time_to_first_token: number | null;
};
export type Organization = {
id: string;
name: string;
@@ -484,30 +515,15 @@ export type Prompt = {
labels: Generated<string[]>;
commit_message: string | null;
};
export type QueueBackUp = {
export type PromptDependency = {
id: string;
project_id: string | null;
queue_name: string;
content: unknown;
created_at: Generated<Timestamp>;
};
export type Score = {
id: string;
timestamp: Generated<Timestamp>;
project_id: string;
name: string;
value: number | null;
source: ScoreSource;
author_user_id: string | null;
comment: string | null;
trace_id: string;
observation_id: string | null;
config_id: string | null;
string_value: string | null;
queue_id: string | null;
created_at: Generated<Timestamp>;
updated_at: Generated<Timestamp>;
data_type: Generated<ScoreDataType>;
project_id: string;
parent_id: string;
child_name: string;
child_label: string | null;
child_version: number | null;
};
export type ScoreConfig = {
id: string;
@@ -535,25 +551,6 @@ export type SsoConfig = {
auth_provider: string;
auth_config: unknown | null;
};
export type Trace = {
id: string;
external_id: string | null;
timestamp: Generated<Timestamp>;
name: string | null;
user_id: string | null;
metadata: unknown | null;
release: string | null;
version: string | null;
project_id: string;
public: Generated<boolean>;
bookmarked: Generated<boolean>;
tags: Generated<string[]>;
input: unknown | null;
output: unknown | null;
session_id: string | null;
created_at: Generated<Timestamp>;
updated_at: Generated<Timestamp>;
};
export type TraceMedia = {
id: string;
project_id: string;
@@ -570,26 +567,7 @@ export type TraceSession = {
project_id: string;
bookmarked: Generated<boolean>;
public: Generated<boolean>;
};
export type TraceView = {
id: string;
external_id: string | null;
timestamp: Generated<Timestamp>;
name: string | null;
user_id: string | null;
metadata: unknown | null;
release: string | null;
version: string | null;
project_id: string;
public: Generated<boolean>;
bookmarked: Generated<boolean>;
tags: Generated<string[]>;
input: unknown | null;
output: unknown | null;
session_id: string | null;
created_at: Timestamp;
updated_at: Timestamp;
duration: number | null;
environment: Generated<string>;
};
export type User = {
id: string;
@@ -617,6 +595,7 @@ export type DB = {
background_migrations: BackgroundMigration;
batch_exports: BatchExport;
billing_meter_backups: BillingMeterBackup;
blob_storage_integrations: BlobStorageIntegration;
comments: Comment;
cron_jobs: CronJobs;
dataset_items: DatasetItem;
@@ -631,24 +610,22 @@ export type DB = {
membership_invitations: MembershipInvitation;
models: Model;
observation_media: ObservationMedia;
observations: Observation;
observations_view: ObservationView;
observations: LegacyPrismaObservation;
organization_memberships: OrganizationMembership;
organizations: Organization;
posthog_integrations: PosthogIntegration;
prices: Price;
project_memberships: ProjectMembership;
projects: Project;
prompt_dependencies: PromptDependency;
prompts: Prompt;
queue_backups: QueueBackUp;
score_configs: ScoreConfig;
scores: Score;
scores: LegacyPrismaScore;
Session: Session;
sso_configs: SsoConfig;
trace_media: TraceMedia;
trace_sessions: TraceSession;
traces: Trace;
traces_view: TraceView;
traces: LegacyPrismaTrace;
users: User;
verification_tokens: VerificationToken;
};
@@ -0,0 +1,2 @@
-- AlterTable
ALTER TABLE "job_configurations" ADD COLUMN "time_scope" TEXT[] DEFAULT ARRAY['NEW']::TEXT[];
@@ -0,0 +1,3 @@
-- AlterTable
ALTER TABLE "trace_sessions"
ADD COLUMN "environment" TEXT NOT NULL DEFAULT 'default';
@@ -0,0 +1,2 @@
DROP VIEW IF EXISTS "observations_view";
DROP VIEW IF EXISTS "traces_view";
@@ -0,0 +1,18 @@
CREATE TABLE "prompt_dependencies" (
"id" TEXT NOT NULL,
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updated_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"project_id" TEXT NOT NULL,
"parent_id" TEXT NOT NULL,
"child_name" TEXT NOT NULL,
"child_label" TEXT,
"child_version" INTEGER,
CONSTRAINT "prompt_dependencies_pkey" PRIMARY KEY ("id")
);
ALTER TABLE "prompt_dependencies" ADD CONSTRAINT "prompt_dependencies_project_id_fkey" FOREIGN KEY ("project_id") REFERENCES "projects"("id") ON DELETE CASCADE ON UPDATE CASCADE;
ALTER TABLE "prompt_dependencies" ADD CONSTRAINT "prompt_dependencies_parent_id_fkey" FOREIGN KEY ("parent_id") REFERENCES "prompts"("id") ON DELETE CASCADE ON UPDATE CASCADE;
CREATE INDEX "prompt_dependencies_project_id_parent_id" ON "prompt_dependencies"("project_id", "parent_id");
CREATE INDEX "prompt_dependencies_project_id_child_name" ON "prompt_dependencies"("project_id", "child_name");
@@ -0,0 +1,11 @@
-- CreateEnum
CREATE TYPE "AuditLogRecordType" AS ENUM ('USER', 'API_KEY');
-- AlterTable
ALTER TABLE "audit_logs" ADD COLUMN "api_key_id" TEXT,
ADD COLUMN "type" "AuditLogRecordType" NOT NULL DEFAULT 'USER',
ALTER COLUMN "user_id" DROP NOT NULL,
ALTER COLUMN "user_org_role" DROP NOT NULL;
-- CreateIndex
CREATE INDEX "audit_logs_api_key_id_idx" ON "audit_logs"("api_key_id");
@@ -0,0 +1,2 @@
-- DropTable
DROP TABLE "queue_backups";
@@ -0,0 +1,26 @@
-- CreateEnum
CREATE TYPE "BlobStorageIntegrationType" AS ENUM ('S3', 'S3_COMPATIBLE', 'AZURE_BLOB_STORAGE');
-- CreateTable
CREATE TABLE "blob_storage_integrations" (
"project_id" TEXT NOT NULL,
"type" "BlobStorageIntegrationType" NOT NULL,
"bucket_name" TEXT NOT NULL,
"prefix" TEXT NOT NULL,
"access_key_id" TEXT NOT NULL,
"secret_access_key" TEXT NOT NULL,
"region" TEXT NOT NULL,
"endpoint" TEXT,
"force_path_style" BOOLEAN NOT NULL,
"next_sync_at" TIMESTAMP(3),
"last_sync_at" TIMESTAMP(3),
"enabled" BOOLEAN NOT NULL,
"export_frequency" TEXT NOT NULL,
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updated_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "blob_storage_integrations_pkey" PRIMARY KEY ("project_id")
);
-- AddForeignKey
ALTER TABLE "blob_storage_integrations" ADD CONSTRAINT "blob_storage_integrations_project_id_fkey" FOREIGN KEY ("project_id") REFERENCES "projects"("id") ON DELETE CASCADE ON UPDATE CASCADE;
@@ -1,3 +1,3 @@
# Please do not edit this file manually
# It should be added in your version-control system (i.e. Git)
# It should be added in your version-control system (e.g., Git)
provider = "postgresql"
+197 -225
View File
@@ -3,7 +3,7 @@
generator client {
provider = "prisma-client-js"
previewFeatures = ["tracing", "views", "relationJoins", "metrics"]
previewFeatures = ["views", "relationJoins", "metrics"]
}
datasource db {
@@ -109,37 +109,39 @@ model Organization {
}
model Project {
id String @id @default(cuid())
orgId String @map("org_id")
createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime @default(now()) @updatedAt @map("updated_at")
deletedAt DateTime? @map("deleted_at")
name String
retentionDays Int? @map("retention_days")
projectMembers ProjectMembership[]
organization Organization @relation(fields: [orgId], references: [id], onUpdate: Cascade, onDelete: Cascade)
traces Trace[]
observations Observation[]
apiKeys ApiKey[]
dataset Dataset[]
invitations MembershipInvitation[]
sessions TraceSession[]
Prompt Prompt[]
Model Model[]
EvalTemplate EvalTemplate[]
JobConfiguration JobConfiguration[]
JobExecution JobExecution[]
LlmApiKeys LlmApiKeys[]
PosthogIntegration PosthogIntegration[]
Score Score[]
scoreConfig ScoreConfig[]
BatchExport BatchExport[]
comment Comment[]
annotationQueue AnnotationQueue[]
annotationQueueItem AnnotationQueueItem[]
TraceMedia TraceMedia[]
Media Media[]
ObservationMedia ObservationMedia[]
id String @id @default(cuid())
orgId String @map("org_id")
createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime @default(now()) @updatedAt @map("updated_at")
deletedAt DateTime? @map("deleted_at")
name String
retentionDays Int? @map("retention_days")
projectMembers ProjectMembership[]
organization Organization @relation(fields: [orgId], references: [id], onUpdate: Cascade, onDelete: Cascade)
apiKeys ApiKey[]
dataset Dataset[]
invitations MembershipInvitation[]
sessions TraceSession[]
Prompt Prompt[]
Model Model[]
EvalTemplate EvalTemplate[]
JobConfiguration JobConfiguration[]
JobExecution JobExecution[]
LlmApiKeys LlmApiKeys[]
PosthogIntegration PosthogIntegration[]
BlobStorageIntegration BlobStorageIntegration[]
scoreConfig ScoreConfig[]
BatchExport BatchExport[]
comment Comment[]
annotationQueue AnnotationQueue[]
annotationQueueItem AnnotationQueueItem[]
TraceMedia TraceMedia[]
Media Media[]
ObservationMedia ObservationMedia[]
LegacyTrace LegacyPrismaTrace[]
LegacyObservation LegacyPrismaObservation[]
LegacyScore LegacyPrismaScore[]
PromptDependency PromptDependency[]
@@index([orgId])
@@map("projects")
@@ -267,13 +269,14 @@ enum Role {
}
model TraceSession {
id String @default(cuid())
createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime @default(now()) @updatedAt @map("updated_at")
projectId String @map("project_id")
project Project @relation(fields: [projectId], references: [id], onDelete: Cascade)
bookmarked Boolean @default(false)
public Boolean @default(false)
id String @default(cuid())
createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime @default(now()) @updatedAt @map("updated_at")
projectId String @map("project_id")
project Project @relation(fields: [projectId], references: [id], onDelete: Cascade)
bookmarked Boolean @default(false)
public Boolean @default(false)
environment String @default("default")
@@id([id, projectId])
@@index([projectId])
@@ -282,9 +285,7 @@ model TraceSession {
@@map("trace_sessions")
}
// Update TraceView below when making changes to this model!
model Trace {
model LegacyPrismaTrace {
id String @id @default(cuid())
externalId String? @map("external_id")
timestamp DateTime @default(now())
@@ -315,80 +316,43 @@ model Trace {
@@map("traces")
}
// This view is based on the trace table. Once prisma supports
// inheritance, we should remove code duplication here.
view TraceView {
// trace fields
id String @id @default(cuid())
externalId String? @map("external_id")
timestamp DateTime @default(now())
name String?
userId String? @map("user_id")
metadata Json?
release String?
version String?
projectId String @map("project_id")
public Boolean @default(false)
bookmarked Boolean @default(false)
tags String[] @default([])
input Json?
output Json?
sessionId String? @map("session_id")
createdAt DateTime @map("created_at")
updatedAt DateTime @map("updated_at")
// calculated fields
duration Float? @map("duration") // can be null if no observations in trace
@@map("traces_view")
}
// Update ObservationView below when making changes to this model!
// traceId is optional only due to timing during data injestion
// (traceId is not necessarily known at the time of observation creation)
model Observation {
id String @id @default(cuid())
traceId String? @map("trace_id")
projectId String @map("project_id")
type ObservationType
startTime DateTime @default(now()) @map("start_time")
endTime DateTime? @map("end_time")
name String?
metadata Json?
parentObservationId String? @map("parent_observation_id")
level ObservationLevel @default(DEFAULT)
statusMessage String? @map("status_message")
version String?
createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime @default(now()) @updatedAt @map("updated_at")
model LegacyPrismaObservation {
id String @id @default(cuid())
traceId String? @map("trace_id")
projectId String @map("project_id")
project Project @relation(fields: [projectId], references: [id], onDelete: Cascade)
type LegacyPrismaObservationType
startTime DateTime @default(now()) @map("start_time")
endTime DateTime? @map("end_time")
name String?
metadata Json?
parentObservationId String? @map("parent_observation_id")
level LegacyPrismaObservationLevel @default(DEFAULT)
statusMessage String? @map("status_message")
version String?
createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime @default(now()) @updatedAt @map("updated_at")
// GENERATION ONLY
model String? // user-provided model attribute
internalModel String? @map("internal_model") // matched model.name that is matched at ingestion time, to be deprecated
internalModelId String? @map("internal_model_id") // matched model.id that is matched at ingestion time
modelParameters Json?
input Json?
output Json?
promptTokens Int @default(0) @map("prompt_tokens")
completionTokens Int @default(0) @map("completion_tokens")
totalTokens Int @default(0) @map("total_tokens")
unit String?
model String? // user-provided model attribute
internalModel String? @map("internal_model") // matched model.name that is matched at ingestion time, to be deprecated
internalModelId String? @map("internal_model_id") // matched model.id that is matched at ingestion time
modelParameters Json?
input Json?
output Json?
promptTokens Int @default(0) @map("prompt_tokens")
completionTokens Int @default(0) @map("completion_tokens")
totalTokens Int @default(0) @map("total_tokens")
unit String?
// User provided cost at ingestion
inputCost Decimal? @map("input_cost")
outputCost Decimal? @map("output_cost")
totalCost Decimal? @map("total_cost")
inputCost Decimal? @map("input_cost")
outputCost Decimal? @map("output_cost")
totalCost Decimal? @map("total_cost")
// Calculated cost
calculatedInputCost Decimal? @map("calculated_input_cost")
calculatedOutputCost Decimal? @map("calculated_output_cost")
calculatedTotalCost Decimal? @map("calculated_total_cost")
completionStartTime DateTime? @map("completion_start_time")
project Project @relation(fields: [projectId], references: [id], onDelete: Cascade)
promptId String? @map("prompt_id") // no fk constraint, prompt can be deleted
calculatedInputCost Decimal? @map("calculated_input_cost")
calculatedOutputCost Decimal? @map("calculated_output_cost")
calculatedTotalCost Decimal? @map("calculated_total_cost")
completionStartTime DateTime? @map("completion_start_time")
promptId String? @map("prompt_id") // no fk constraint, prompt can be deleted
@@unique([id, projectId])
@@index([projectId, internalModel, startTime, unit])
@@ -405,88 +369,42 @@ model Observation {
@@map("observations")
}
// This view is a mix of the observation and model. Once prisma supports
// inheritance, we should remove code duplication here.
view ObservationView {
id String @id @default(cuid())
traceId String? @map("trace_id")
projectId String @map("project_id")
type ObservationType
startTime DateTime @map("start_time")
endTime DateTime? @map("end_time")
name String?
metadata Json?
parentObservationId String? @map("parent_observation_id")
level ObservationLevel @default(DEFAULT)
statusMessage String? @map("status_message")
version String?
createdAt DateTime @map("created_at")
updatedAt DateTime @map("updated_at")
// GENERATION ONLY
model String?
modelParameters Json?
input Json?
output Json?
promptTokens Int @default(0) @map("prompt_tokens")
completionTokens Int @default(0) @map("completion_tokens")
totalTokens Int @default(0) @map("total_tokens")
unit String?
completionStartTime DateTime? @map("completion_start_time")
// prompts
promptId String? @map("prompt_id")
promptName String? @map("prompt_name")
promptVersion Int? @map("prompt_version")
// model fields
modelId String? @map("model_id")
inputPrice Decimal? @map("input_price")
outputPrice Decimal? @map("output_price")
totalPrice Decimal? @map("total_price")
// calculated fields
calculatedInputCost Decimal? @map("calculated_input_cost")
calculatedOutputCost Decimal? @map("calculated_output_cost")
calculatedTotalCost Decimal? @map("calculated_total_cost")
latency Float? @map("latency")
timeToFirstToken Float? @map("time_to_first_token")
@@map("observations_view")
}
enum ObservationType {
enum LegacyPrismaObservationType {
SPAN
EVENT
GENERATION
@@map("ObservationType")
}
enum ObservationLevel {
enum LegacyPrismaObservationLevel {
DEBUG
DEFAULT
WARNING
ERROR
@@map("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)
model LegacyPrismaScore {
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")
source LegacyPrismaScoreSource
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")
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)
@@ -501,26 +419,28 @@ model Score {
@@map("scores")
}
enum ScoreSource {
enum LegacyPrismaScoreSource {
ANNOTATION
API
EVAL
@@map("ScoreSource")
}
model ScoreConfig {
id String @id @default(cuid())
createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime @default(now()) @updatedAt @map("updated_at")
projectId String @map("project_id")
project Project @relation(fields: [projectId], references: [id], onDelete: Cascade)
id String @id @default(cuid())
createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime @default(now()) @updatedAt @map("updated_at")
projectId String @map("project_id")
project Project @relation(fields: [projectId], references: [id], onDelete: Cascade)
name String
dataType ScoreDataType @map("data_type")
isArchived Boolean @default(false) @map("is_archived")
minValue Float? @map("min_value")
maxValue Float? @map("max_value")
categories Json? @map("categories")
dataType ScoreDataType @map("data_type")
isArchived Boolean @default(false) @map("is_archived")
minValue Float? @map("min_value")
maxValue Float? @map("max_value")
categories Json? @map("categories")
description String?
score Score[]
legacyScore LegacyPrismaScore[]
@@unique([id, projectId]) // used for upserts via prisma
@@index([dataType])
@@ -722,15 +642,16 @@ model Prompt {
createdBy String @map("created_by")
prompt Json
name String
version Int
type String @default("text")
isActive Boolean? @map("is_active") // Deprecated. To be removed once 'production' labels work as expected.
config Json @default("{}") @db.Json
tags String[] @default([])
labels String[] @default([])
commitMessage String? @map("commit_message")
prompt Json
name String
version Int
type String @default("text")
isActive Boolean? @map("is_active") // Deprecated. To be removed once 'production' labels work as expected.
config Json @default("{}") @db.Json
tags String[] @default([])
labels String[] @default([])
commitMessage String? @map("commit_message")
PromptDependency PromptDependency[]
@@unique([projectId, name, version])
@@index([projectId, id])
@@ -740,6 +661,26 @@ model Prompt {
@@map("prompts")
}
model PromptDependency {
id String @id @default(cuid())
createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime @default(now()) @updatedAt @map("updated_at")
projectId String @map("project_id")
project Project @relation(fields: [projectId], references: [id], onDelete: Cascade)
parentId String @map("parent_id")
parent Prompt @relation(fields: [parentId], references: [id], onDelete: Cascade)
childName String @map("child_name")
childLabel String? @map("child_label")
childVersion Int? @map("child_version")
@@index([projectId, parentId], map: "prompt_dependencies_project_id_parent_id")
@@index([projectId, childName], map: "prompt_dependencies_project_id_child_name")
@@map("prompt_dependencies")
}
// Update ObservationView below when making changes to this model!
model Model {
id String @id @default(cuid())
@@ -778,23 +719,31 @@ model Price {
@@map("prices")
}
enum AuditLogRecordType {
USER
API_KEY
}
// No FK constraints to preserve audit logs
model AuditLog {
id String @id @default(cuid())
createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime @default(now()) @updatedAt @map("updated_at")
userId String @map("user_id")
orgId String @map("org_id")
userOrgRole String @map("user_org_role")
projectId String? @map("project_id")
userProjectRole String? @map("user_project_role")
resourceType String @map("resource_type")
resourceId String @map("resource_id")
id String @id @default(cuid())
createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime @default(now()) @updatedAt @map("updated_at")
type AuditLogRecordType @default(USER)
apiKeyId String? @map("api_key_id")
userId String? @map("user_id")
orgId String @map("org_id")
userOrgRole String? @map("user_org_role")
projectId String? @map("project_id")
userProjectRole String? @map("user_project_role")
resourceType String @map("resource_type")
resourceId String @map("resource_id")
action String
before String? //stringified JSON
before String? // stringified JSON
after String? // stringified JSON
@@index([projectId])
@@index([apiKeyId])
@@index([userId])
@@index([orgId])
@@index([createdAt])
@@ -853,6 +802,7 @@ model JobConfiguration {
variableMapping Json @map("variable_mapping")
sampling Decimal // ratio of jobs that are executed for sampling (0..1)
delay Int // delay in milliseconds
timeScope String[] @default(["NEW"]) @map("time_scope")
JobExecution JobExecution[]
@@index([projectId, id])
@@ -911,7 +861,6 @@ model SsoConfig {
authConfig Json? @map("auth_config")
// e.g. { "clientId": "1234", "clientSecret": "5678" }, null if credentials from env should be used
// secrets like clientSecret are encrypted on the application level
@@map("sso_configs")
}
@@ -927,6 +876,40 @@ model PosthogIntegration {
@@map("posthog_integrations")
}
model BlobStorageIntegration {
projectId String @id @map("project_id")
project Project @relation(fields: [projectId], references: [id], onDelete: Cascade)
// Blob Storage Config
type BlobStorageIntegrationType @map("type")
bucketName String @map("bucket_name")
prefix String @map("prefix")
accessKeyId String @map("access_key_id")
secretAccessKey String @map("secret_access_key")
region String @map("region")
endpoint String? @map("endpoint")
forcePathStyle Boolean @map("force_path_style")
// Integration Config
nextSyncAt DateTime? @map("next_sync_at")
lastSyncAt DateTime? @map("last_sync_at")
enabled Boolean
exportFrequency String @map("export_frequency")
createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime @default(now()) @updatedAt @map("updated_at")
@@map("blob_storage_integrations")
}
enum BlobStorageIntegrationType {
S3
S3_COMPATIBLE
AZURE_BLOB_STORAGE
@@map("BlobStorageIntegrationType")
}
model BatchExport {
id String @id @default(cuid())
createdAt DateTime @default(now()) @map("created_at")
@@ -1004,17 +987,6 @@ model ObservationMedia {
@@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")
+30 -115
View File
@@ -2,8 +2,6 @@ import {
PrismaClient,
type Project,
type Prisma,
ObservationType,
ScoreSource,
ScoreDataType,
AnnotationQueueObjectType,
} from "../src/index";
@@ -74,6 +72,9 @@ async function main() {
where: { id: seedOrgId },
update: {
name: "Seed Org",
cloudConfig: {
plan: "Team",
},
},
create: {
id: seedOrgId,
@@ -97,7 +98,7 @@ async function main() {
},
});
const orgMembership = await prisma.organizationMembership.upsert({
await prisma.organizationMembership.upsert({
where: {
orgId_userId: {
userId: user.id,
@@ -127,7 +128,7 @@ async function main() {
update: {},
});
const projectMembership = await prisma.projectMembership.upsert({
await prisma.projectMembership.upsert({
where: {
projectId_userId: {
projectId: project1.id,
@@ -266,38 +267,23 @@ async function main() {
const traceVolume = environment === "load" ? LOAD_TRACE_VOLUME : 100;
const {
traces,
observations,
scores,
sessions,
events,
comments,
queueItems,
} = createObjects(
traceVolume,
envTags,
colorTags,
project1,
project2,
promptIds,
queueIds,
configIdsAndNames,
);
const { traces, observations, scores, sessions, comments, queueItems } =
createObjects(
traceVolume,
envTags,
colorTags,
project1,
project2,
promptIds,
queueIds,
configIdsAndNames,
);
logger.info(
`Seeding ${traces.length} traces, ${observations.length} observations, and ${scores.length} scores`,
);
await uploadObjects(
traces,
observations,
scores,
sessions,
events,
comments,
queueItems,
);
await uploadObjects(sessions, comments, queueItems);
// If openai key is in environment, add it to the projects LLM API keys
const OPENAI_API_KEY = process.env.OPENAI_API_KEY;
@@ -420,7 +406,7 @@ export async function createDatasets(
updatedAt: Date;
name: string;
},
observations: Prisma.ObservationCreateManyInput[],
observations: { id?: string; projectId?: string; traceId?: string | null }[],
) {
for (let datasetNumber = 0; datasetNumber < 2; datasetNumber++) {
for (const projectId of [project1.id, project2.id]) {
@@ -532,11 +518,7 @@ export async function createDatasets(
}
async function uploadObjects(
traces: Prisma.TraceCreateManyInput[],
observations: Prisma.ObservationCreateManyInput[],
scores: Prisma.ScoreCreateManyInput[],
sessions: Prisma.TraceSessionCreateManyInput[],
events: Prisma.ObservationCreateManyInput[],
comments: Prisma.CommentCreateManyInput[],
queueItems: Prisma.AnnotationQueueItemCreateManyInput[],
) {
@@ -564,73 +546,6 @@ async function uploadObjects(
await promises[i];
}
promises = [];
chunk(traces, chunkSize).forEach((chunk) => {
promises.push(
prisma.trace.createMany({
data: chunk,
}),
);
});
for (let i = 0; i < promises.length; i++) {
if (i + 1 >= promises.length || i % Math.ceil(promises.length / 10) === 0)
logger.info(
`Seeding of Traces ${((i + 1) / promises.length) * 100}% complete`,
);
await promises[i];
}
promises = [];
chunk(observations, chunkSize).forEach((chunk) => {
promises.push(
prisma.observation.createMany({
data: chunk,
}),
);
});
for (let i = 0; i < promises.length; i++) {
if (i + 1 >= promises.length || i % Math.ceil(promises.length / 10) === 0)
logger.info(
`Seeding of Observations ${((i + 1) / promises.length) * 100}% complete`,
);
await promises[i];
}
promises = [];
chunk(events, chunkSize).forEach((chunk) => {
promises.push(
prisma.observation.createMany({
data: chunk,
}),
);
});
for (let i = 0; i < promises.length; i++) {
if (i + 1 >= promises.length || i % Math.ceil(promises.length / 10) === 0)
logger.info(
`Seeding of Events ${((i + 1) / promises.length) * 100}% complete`,
);
await promises[i];
}
promises = [];
chunk(scores, chunkSize).forEach((chunk) => {
promises.push(
prisma.score.createMany({
data: chunk,
}),
);
});
for (let i = 0; i < promises.length; i++) {
if (i + 1 >= promises.length || i % Math.ceil(promises.length / 10) === 0)
logger.info(
`Seeding of Scores ${((i + 1) / promises.length) * 100}% complete`,
);
await promises[i];
}
promises = [];
chunk(comments, chunkSize).forEach((chunk) => {
promises.push(
@@ -682,11 +597,11 @@ function createObjects(
}[]
>,
) {
const traces: Prisma.TraceCreateManyInput[] = [];
const observations: Prisma.ObservationCreateManyInput[] = [];
const scores: Prisma.ScoreCreateManyInput[] = [];
const traces: any[] = [];
const observations: any[] = [];
const scores: any[] = [];
const sessions: Prisma.TraceSessionCreateManyInput[] = [];
const events: Prisma.ObservationCreateManyInput[] = [];
const events: any[] = [];
const configs: Prisma.ScoreConfigCreateManyInput[] = [];
const comments: Prisma.CommentCreateManyInput[] = [];
const queueItems: Prisma.AnnotationQueueItemCreateManyInput[] = [];
@@ -795,7 +710,7 @@ function createObjects(
name: annotationScoreName,
timestamp: traceTs,
createdAt: traceTs,
source: ScoreSource.ANNOTATION,
source: "ANNOTATION",
projectId,
authorUserId: `user-${i}`,
dataType,
@@ -812,7 +727,7 @@ function createObjects(
value: Math.floor(Math.random() * 10) - 5,
timestamp: traceTs,
createdAt: traceTs,
source: ScoreSource.API,
source: "API",
projectId,
dataType: ScoreDataType.NUMERIC,
},
@@ -825,7 +740,7 @@ function createObjects(
name: "Completeness",
timestamp: traceTs,
createdAt: traceTs,
source: ScoreSource.API,
source: "API",
projectId,
dataType: ScoreDataType.CATEGORICAL,
stringValue:
@@ -859,7 +774,7 @@ function createObjects(
);
const span = {
type: ObservationType.SPAN,
type: "SPAN",
id: `span-${v4()}`,
startTime: spanTsStart,
createdAt: spanTsStart,
@@ -933,7 +848,7 @@ function createObjects(
const { input, output } = getGenerationInputOutput();
const generation = {
type: ObservationType.GENERATION,
type: "GENERATION",
id: `generation-${v4()}`,
startTime: generationTsStart,
createdAt: generationTsStart,
@@ -978,7 +893,7 @@ function createObjects(
value: Math.random() * 2 - 1,
observationId: generation.id,
traceId: trace.id,
source: ScoreSource.API,
source: "API",
projectId: trace.projectId,
timestamp: generationTsEnd,
createdAt: traceTs,
@@ -989,7 +904,7 @@ function createObjects(
value: Math.random() * 2 - 1,
observationId: generation.id,
traceId: trace.id,
source: ScoreSource.API,
source: "API",
projectId: trace.projectId,
timestamp: generationTsEnd,
createdAt: traceTs,
@@ -1013,7 +928,7 @@ function createObjects(
);
events.push({
type: ObservationType.EVENT,
type: "EVENT",
id: `event-${v4()}`,
startTime: eventTs,
createdAt: eventTs,
+4 -7
View File
@@ -1,11 +1,6 @@
import { randomUUID } from "crypto";
import { prisma } from "../src/db";
import {
clickhouseClient,
getDisplaySecretKey,
hashSecretKey,
logger,
} from "../src/server";
import { getDisplaySecretKey, hashSecretKey, logger } from "../src/server";
import { prepareClickhouse } from "./prepareClickhouse";
import { redis } from "../src/server";
@@ -32,6 +27,9 @@ const prepareProjectsAndApiKeys = async (
create: {
id: orgId,
name: `Organization for ${projectId}`,
cloudConfig: {
plan: "Team",
},
},
});
@@ -124,7 +122,6 @@ async function main() {
} catch (error) {
logger.error("Error during Clickhouse preparation:", error);
} finally {
await clickhouseClient().close();
await prisma.$disconnect();
redis?.disconnect();
logger.info("Disconnected from Clickhouse.");
+4 -1
View File
@@ -67,6 +67,7 @@ export const prepareClickhouse = async (
concat('release_', toString(randUniform(0, 100))) AS release,
concat('version_', toString(randUniform(0, 100))) AS version,
'${projectId}' AS project_id,
'default' AS environment,
if(rand() < 0.8, true, false) as public,
if(rand() < 0.8, true, false) as bookmarked,
array('tag1', 'tag2') as tags,
@@ -85,6 +86,7 @@ export const prepareClickhouse = async (
SELECT toString(number) AS id,
toString(floor(randUniform(0, ${tracesPerProject}))) AS trace_id,
'${projectId}' AS project_id,
'default' AS environment,
if(randUniform(0, 1) < 0.47, 'GENERATION', if(randUniform(0, 1) < 0.94, 'SPAN', 'EVENT')) AS type,
toString(rand()) AS parent_observation_id,
toDateTime(now() - randUniform(0, ${opts.numberOfDays} * 24 * 60 * 60)) AS start_time,
@@ -143,9 +145,10 @@ export const prepareClickhouse = async (
const scoresQuery = `
INSERT INTO scores
SELECT toString(floor(randUniform(0, 100))) AS id,
SELECT toString(number) AS id,
toDateTime(now() - randUniform(0, ${opts.numberOfDays} * 24 * 60 * 60)) AS timestamp,
'${projectId}' AS project_id,
'default' AS environment,
toString(floor(randUniform(0, ${tracesPerProject}))) AS trace_id,
if(
rand() > 0.9,
+3 -3
View File
@@ -2,7 +2,7 @@ import crypto from "crypto";
import { env } from "../env";
const ENCRYPTION_KEY: string | undefined = env.ENCRYPTION_KEY; // Must be 256 bits (32 bytes, 64 hex characters)
const IV_LENGTH: number = 16; // For AES, this is always 16
const IV_LENGTH: number = 12; // For AES-GCM, this is always 12
// Alternatively: openssl rand -hex 32
export function keyGen() {
@@ -23,7 +23,7 @@ export function encrypt(plainText: string): string {
const cipher = crypto.createCipheriv(
"aes-256-gcm",
Buffer.from(ENCRYPTION_KEY, "hex"),
iv
iv,
);
let encrypted = cipher.update(plainText, "utf8", "hex");
encrypted += cipher.final("hex");
@@ -49,7 +49,7 @@ export function decrypt(text: string): string {
const decipher = crypto.createDecipheriv(
"aes-256-gcm",
Buffer.from(ENCRYPTION_KEY, "hex"),
iv
iv,
);
decipher.setAuthTag(authTag);
+10 -1
View File
@@ -10,7 +10,7 @@ const EnvSchema = z.object({
REDIS_PORT: z.coerce
.number({
description:
".env files convert numbers to strings, therefoore we have to enforce them to be numbers",
".env files convert numbers to strings, therefore we have to enforce them to be numbers",
})
.positive()
.max(65536, `options.port should be >= 0 and < 65536`)
@@ -18,6 +18,10 @@ const EnvSchema = z.object({
.nullable(),
REDIS_AUTH: z.string().nullish(),
REDIS_CONNECTION_STRING: z.string().nullish(),
REDIS_TLS_ENABLED: z.enum(["true", "false"]).default("false"),
REDIS_TLS_CA_PATH: z.string().optional(),
REDIS_TLS_CERT_PATH: z.string().optional(),
REDIS_TLS_KEY_PATH: z.string().optional(),
REDIS_ENABLE_AUTO_PIPELINING: z.enum(["true", "false"]).default("true"),
ENCRYPTION_KEY: z
.string()
@@ -46,6 +50,7 @@ const EnvSchema = z.object({
ENABLE_AWS_CLOUDWATCH_METRIC_PUBLISHING: z
.enum(["true", "false"])
.default("false"),
LANGFUSE_S3_CONCURRENT_WRITES: z.coerce.number().positive().default(50),
LANGFUSE_S3_EVENT_UPLOAD_BUCKET: z.string({
required_error: "Langfuse requires a bucket name for S3 Event Uploads.",
}),
@@ -66,6 +71,10 @@ const EnvSchema = z.object({
LANGFUSE_POSTGRES_METERING_DATA_EXPORT_IS_ENABLED: z
.enum(["true", "false"])
.default("false"),
LANGFUSE_CUSTOM_SSO_EMAIL_CLAIM: z.string().default("email"),
LANGFUSE_CUSTOM_SSO_NAME_CLAIM: z.string().default("name"),
LANGFUSE_CUSTOM_SSO_SUB_CLAIM: z.string().default("sub"),
});
export const env: z.infer<typeof EnvSchema> =
@@ -28,6 +28,7 @@ const BooleanData = z.object({
const CreateAnnotationScoreBase = z.object({
name: z.string(),
projectId: z.string(),
environment: z.string().default("default"),
traceId: z.string(),
configId: z.string().optional(),
observationId: z.string().optional(),
@@ -10,6 +10,7 @@ export enum BatchActionType {
}
const ActionIdSchema = z.enum([
"score-delete",
"trace-delete",
"trace-add-to-annotation-queue",
]);
@@ -18,9 +18,11 @@ export enum BatchExportFileFormat {
}
export enum BatchExportTableName {
Scores = "scores",
Sessions = "sessions",
Traces = "traces",
Generations = "generations",
Observations = "observations",
DatasetRunItems = "dataset_run_items",
}
export const exportOptions: Record<
@@ -1,8 +1,19 @@
// used on organization.cloudConfig.plan
export const cloudConfigPlans = [
"Hobby",
"Core",
"Pro",
"Team",
"Enterprise",
] as const;
export const planLabels = {
oss: "OSS",
"cloud:hobby": "Hobby",
"cloud:core": "Core",
"cloud:pro": "Pro",
"cloud:team": "Team",
"cloud:enterprise": "Enterprise",
"self-hosted:pro": "Pro (self-hosted)",
"self-hosted:enterprise": "Enterprise (self-hosted)",
} as const;
@@ -12,8 +23,9 @@ export type Plan = keyof typeof planLabels;
export const plans = Object.keys(planLabels) as Plan[];
// 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 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);
+5 -5
View File
@@ -106,9 +106,9 @@ export const OutputSchema = z.object({
score: z.string(),
});
export enum EvalTargetObject {
Trace = "trace",
Dataset = "dataset",
}
export const DEFAULT_TRACE_JOB_DELAY = 10_000;
export const JobTimeScopeZod = z.enum(["NEW", "EXISTING"]);
export type JobTimeScope = z.infer<typeof JobTimeScopeZod>;
export const TimeScopeSchema = z.array(JobTimeScopeZod).default(["NEW"]);
@@ -0,0 +1,61 @@
import { z } from "zod";
export const PromptDependencyRegex = /@@@langfusePrompt:(.*?)@@@/g;
export const ParsedPromptDependencySchema = z.union([
z.object({
name: z.string(),
type: z.literal("version"),
version: z.coerce.number(),
}),
z.object({ name: z.string(), type: z.literal("label"), label: z.string() }),
]);
export type ParsedPromptDependencyTag = z.infer<
typeof ParsedPromptDependencySchema
>;
export function parsePromptDependencyTags(
content: string | object,
): ParsedPromptDependencyTag[] {
const matchedTags = JSON.stringify(content).match(PromptDependencyRegex);
const validTags: ParsedPromptDependencyTag[] = [];
for (const match of new Set(matchedTags ?? [])) {
const innerContent = match.replace(/^@@@langfusePrompt:|@@@$/g, "");
const parts = innerContent.split("|");
const params: Record<string, string> = {};
// Check if the first parameter is name
const firstPart = parts[0];
if (!firstPart || !firstPart.startsWith("name=")) {
continue; // Skip this tag if name is not the first parameter. This makes it easier to replace the tag with the resolved prompt.
}
// There can be only 2 parts
if (parts.length !== 2) {
continue;
}
parts.forEach((part) => {
const [key, value] = part.split("=");
params[key] = value;
});
if (params.name) {
const parsed = ParsedPromptDependencySchema.safeParse({
name: params.name,
...(params.version
? { version: params.version, type: "version" }
: { label: params.label, type: "label" }),
});
if (parsed.success) {
validTags.push(parsed.data);
}
}
}
return validTags;
}
@@ -1,8 +1,7 @@
import { z } from "zod";
import { Score } from "@prisma/client";
import { isPresent, stringDateTime } from "../../utils/typeChecks";
import { Score } from "../../server";
import {
NonEmptyString,
paginationMetaResponseZod,
@@ -46,6 +45,7 @@ const ScoreBase = z.object({
id: z.string(),
timestamp: z.coerce.date(),
projectId: z.string(),
environment: z.string().default("default"),
name: z.string(),
source: z.enum(ScoreSource),
authorUserId: z.string().nullish(),
@@ -64,6 +64,7 @@ const BaseScoreBody = z.object({
traceId: z.string(),
observationId: z.string().nullish(),
comment: z.string().nullish(),
environment: z.string().default("default"),
});
/**
@@ -237,6 +238,7 @@ export const GetScoresQuery = z.object({
configId: z.string().nullish(),
queueId: z.string().nullish(),
traceTags: z.union([z.array(z.string()), z.string()]).nullish(),
environment: z.union([z.array(z.string()), z.string()]).nullish(),
name: z.string().nullish(),
fromTimestamp: stringDateTime,
toTimestamp: stringDateTime,
@@ -259,6 +261,7 @@ const LegacyGetScoreResponseDataV1 = z.intersection(
trace: z.object({
userId: z.string().nullish(),
tags: z.array(z.string()).nullish(),
environment: z.string().nullish(),
}),
}),
);
+3 -2
View File
@@ -1,4 +1,5 @@
import { ScoreDataType, ScoreSource } from "@prisma/client";
import { ScoreDataType } from "@prisma/client";
import { ScoreSourceType } from "../../server";
export type CategoricalAggregate = {
type: "CATEGORICAL";
@@ -22,7 +23,7 @@ export type ScoreAggregate = Record<
export type ScoreSimplified = {
name: string;
dataType: ScoreDataType;
source: ScoreSource;
source: ScoreSourceType;
value?: number | null;
comment?: string | null;
stringValue?: string | null;
+4
View File
@@ -38,9 +38,13 @@ export * from "./features/comments/types";
// experiments
export * from "./features/experiments/utils";
// prompts
export * from "./features/prompts/parsePromptDependencyTags";
// export db types only
export * from "@prisma/client";
export { type DB } from "../prisma/generated/types";
export * from "./server/repositories/types";
// errors
export * from "./errors/index";
@@ -1,8 +1,9 @@
import { z } from "zod";
import { CloudConfigRateLimit } from "./rate-limits";
import { cloudConfigPlans } from "../features/entitlements/plans";
export const CloudConfigSchema = z.object({
plan: z.enum(["Hobby", "Pro", "Team", "Enterprise"]).optional(),
plan: z.enum(cloudConfigPlans).optional(),
monthlyObservationLimit: z.number().int().positive().optional(),
// used for table and dashboard queries
defaultLookBackDays: z.number().int().positive().optional(),
+8 -3
View File
@@ -1,8 +1,8 @@
import { ObservationLevel } from "@prisma/client";
import {
type OptionsDefinition,
type ColumnDefinition,
} from "./tableDefinitions/types";
} from "./tableDefinitions";
import { ObservationLevelType } from "./server";
// to be used server side
export const observationsTableCols: ColumnDefinition[] = [
@@ -102,7 +102,12 @@ export const observationsTableCols: ColumnDefinition[] = [
id: "level",
type: "stringOptions",
internal: 'o."level"::text',
options: Object.values(ObservationLevel).map((value) => ({ value })),
options: [
{ value: "DEBUG" },
{ value: "DEFAULT" },
{ value: "WARNING" },
{ value: "ERROR" },
] as { value: ObservationLevelType }[],
},
{
name: "Status Message",
@@ -1,4 +1,9 @@
import type { OAuthConfig, OAuthUserConfig } from "next-auth/providers/oauth";
import { env } from "../../env";
const CUSTOM_EMAIL_CLAIM = env.LANGFUSE_CUSTOM_SSO_EMAIL_CLAIM;
const CUSTOM_NAME_CLAIM = env.LANGFUSE_CUSTOM_SSO_NAME_CLAIM;
const CUSTOM_SUB_CLAIM = env.LANGFUSE_CUSTOM_SSO_SUB_CLAIM;
interface CustomSSOUser extends Record<string, any> {
email: string;
@@ -7,6 +12,7 @@ interface CustomSSOUser extends Record<string, any> {
verified: boolean;
}
export function CustomSSOProvider<P extends CustomSSOUser>(
options: OAuthUserConfig<P>
): OAuthConfig<P> {
@@ -19,9 +25,9 @@ export function CustomSSOProvider<P extends CustomSSOUser>(
checks: ["pkce", "state"],
profile(profile) {
return {
id: profile.sub,
name: profile.name,
email: profile.email,
id: profile[CUSTOM_SUB_CLAIM],
name: profile[CUSTOM_NAME_CLAIM],
email: profile[CUSTOM_EMAIL_CLAIM],
image: null,
};
},
+1
View File
@@ -43,4 +43,5 @@ export type ApiAccessScope = {
orgId: string;
plan: Plan;
rateLimitOverrides: z.infer<typeof CloudConfigRateLimit>;
apiKeyId: string;
};
@@ -6,22 +6,30 @@ import { propagation, context } from "@opentelemetry/api";
export type ClickhouseClientType = ReturnType<typeof createClient>;
export const clickhouseClient = (opts?: NodeClickHouseClientConfigOptions) => {
const headers = opts?.http_headers ?? {};
export const clickhouseClient = (
params: {
tags?: Record<string, string>;
opts?: NodeClickHouseClientConfigOptions;
} = {},
) => {
const headers = params.opts?.http_headers ?? {};
const activeSpan = getCurrentSpan();
if (activeSpan) {
propagation.inject(context.active(), headers);
}
return createClient({
...opts,
...params.opts,
url: env.CLICKHOUSE_URL,
username: env.CLICKHOUSE_USER,
password: env.CLICKHOUSE_PASSWORD,
database: env.CLICKHOUSE_DB,
http_headers: headers,
clickhouse_settings: {
log_comment: JSON.stringify(params.tags ?? {}),
async_insert: 1,
wait_for_async_insert: 1, // if disabled, we won't get errors from clickhouse
input_format_json_throw_on_bad_escape_sequence: 0,
},
});
};
@@ -2,6 +2,11 @@ export const ClickhouseTableNames = {
traces: "traces",
observations: "observations",
scores: "scores",
// Virtual tables for dashboards
// TODO: Check if we can do this more elegantly
scores_numeric: "scores_numeric",
scores_categorical: "scores_categorical",
} as const;
export type ClickhouseTableName = keyof typeof ClickhouseTableNames;
+5 -1
View File
@@ -17,22 +17,25 @@ export * from "./clickhouse/schemaUtils";
export * from "./clickhouse/schema";
export * from "./repositories/definitions";
export * from "../server/ingestion/types";
export * from "./ingestion/modelMatch";
export * from "./ingestion/processEventBatch";
export * from "../server/ingestion/types";
export * from "../server/ingestion/validateAndInflateScore";
export * from "./redis/redis";
export * from "./redis/traceUpsert";
export * from "./redis/createEvalQueue";
export * from "./redis/cloudUsageMeteringQueue";
export * from "./redis/getQueue";
export * from "./redis/traceDelete";
export * from "./redis/projectDelete";
export * from "./redis/scoreDelete";
export * from "./redis/datasetRunItemUpsert";
export * from "./redis/batchExport";
export * from "./redis/batchActionQueue";
export * from "./redis/ingestionQueue";
export * from "./redis/postHogIntegrationQueue";
export * from "./redis/postHogIntegrationProcessingQueue";
export * from "./redis/blobStorageIntegrationQueue";
export * from "./redis/blobStorageIntegrationProcessingQueue";
export * from "./redis/dataRetentionQueue";
export * from "./redis/dataRetentionProcessingQueue";
export * from "./redis/coreDataS3ExportQueue";
@@ -48,6 +51,7 @@ export * from "./queries";
export * from "./repositories";
export * from "./redis/evalExecutionQueue";
export * from "./services/sessions-ui-table-service";
export * from "./services/datasets-ui-table-service";
// test utils
export * from "./test-utils";
@@ -1,63 +0,0 @@
import { Model, Observation, Prisma } from "@prisma/client";
import { prisma } from "../../db";
export async function findModel(p: {
event: {
projectId: string;
model?: string;
unit?: string;
startTime?: Date;
};
existingDbObservation?: Omit<Observation, "input" | "output">;
}): Promise<Model | null> {
const { event, existingDbObservation } = p;
// either get the model from the existing observation
// or match pattern on the user provided model name
const modelCondition = event.model
? Prisma.sql`AND ${event.model} ~ match_pattern`
: existingDbObservation?.internalModel
? Prisma.sql`AND model_name = ${existingDbObservation.internalModel}`
: undefined;
if (!modelCondition) return null;
// unit based on the current event or the existing observation, both can be undefined
const mergedUnit = event.unit ?? existingDbObservation?.unit;
const unitCondition = mergedUnit
? Prisma.sql`AND unit = ${mergedUnit}`
: Prisma.empty;
const sql = Prisma.sql`
SELECT
id,
created_at AS "createdAt",
updated_at AS "updatedAt",
project_id AS "projectId",
model_name AS "modelName",
match_pattern AS "matchPattern",
start_date AS "startDate",
input_price AS "inputPrice",
output_price AS "outputPrice",
total_price AS "totalPrice",
unit,
tokenizer_id AS "tokenizerId",
tokenizer_config AS "tokenizerConfig"
FROM
models
WHERE (project_id = ${event.projectId}
OR project_id IS NULL)
${modelCondition}
${unitCondition}
AND (start_date IS NULL OR start_date <= ${
event.startTime ? new Date(event.startTime) : new Date()
}::timestamp with time zone at time zone 'UTC')
ORDER BY
project_id ASC,
start_date DESC NULLS LAST
LIMIT 1
`;
const foundModels = await prisma.$queryRaw<Array<Model>>(sql);
return foundModels[0] ?? null;
}
@@ -14,6 +14,7 @@ import {
getCurrentSpan,
instrumentAsync,
instrumentSync,
recordDistribution,
recordIncrement,
traceException,
} from "../instrumentation";
@@ -94,6 +95,8 @@ export const processEventBatch = async (
// add context of api call to the span
const currentSpan = getCurrentSpan();
recordIncrement("langfuse.ingestion.event", input.length);
recordDistribution("langfuse.ingestion.event_distribution", input.length);
currentSpan?.setAttribute("langfuse.ingestion.batch_size", input.length);
currentSpan?.setAttribute("langfuse.project.id", authCheck.scope.projectId);
currentSpan?.setAttribute("langfuse.org.id", authCheck.scope.orgId);
+138 -80
View File
@@ -3,7 +3,17 @@ import { z } from "zod";
import { NonEmptyString, jsonSchema } from "../../utils/zod";
import { ModelUsageUnit } from "../../constants";
import { ObservationLevel, ScoreSource } from "@prisma/client";
import { type ScoreSourceType } from "../repositories";
export const idSchema = z
.string()
.min(1)
.max(800) // AWS S3 allows for 1024 bytes for object keys and we need enough room to construct the entire key/path. https://docs.aws.amazon.com/AmazonS3/latest/userguide/object-keys.html
.refine((id) => !id.includes("\r"), {
message: "ID cannot contain carriage return characters",
});
const ObservationLevel = z.enum(["DEBUG", "DEFAULT", "WARNING", "ERROR"]);
export const Usage = z.object({
input: z.number().int().nullish(),
@@ -56,21 +66,25 @@ export const usage = MixedUsage.nullish()
// ensure output is always of new usage model
.pipe(Usage.nullish());
const RawUsageOrCostDetails = z.record(
const CostDetails = z
.record(z.string(), z.number().nonnegative().nullish())
.nullish();
const RawUsageDetails = z.record(
z.string(),
z.number().nonnegative().nullish(),
z.number().int().nonnegative().nullish(),
);
const OpenAIUsageSchema = z
const OpenAICompletionUsageSchema = z
.object({
prompt_tokens: z.number().nonnegative(),
completion_tokens: z.number().nonnegative(),
total_tokens: z.number().nonnegative(),
prompt_tokens: z.number().int().nonnegative(),
completion_tokens: z.number().int().nonnegative(),
total_tokens: z.number().int().nonnegative(),
prompt_tokens_details: z
.record(z.string(), z.number().nonnegative())
.record(z.string(), z.number().int().nonnegative().nullish())
.nullish(),
completion_tokens_details: z
.record(z.string(), z.number().nonnegative())
.record(z.string(), z.number().int().nonnegative().nullish())
.nullish(),
})
.strict()
@@ -84,7 +98,7 @@ const OpenAIUsageSchema = z
prompt_tokens_details,
completion_tokens_details,
} = v;
const result: z.infer<typeof RawUsageOrCostDetails> & {
const result: z.infer<typeof RawUsageDetails> & {
input: number;
output: number;
total: number;
@@ -96,31 +110,104 @@ const OpenAIUsageSchema = z
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 (value !== null && value !== undefined) {
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);
if (value !== null && value !== undefined) {
result[`output_${key}`] = value;
result.output = Math.max(result.output - (value ?? 0), 0);
}
}
}
return result;
})
.pipe(RawUsageOrCostDetails);
.pipe(RawUsageDetails);
export const UsageOrCostDetails = z
.union([OpenAIUsageSchema, RawUsageOrCostDetails])
// The new OpenAI Response API uses a new Usage schema that departs from the Completion API Usage schema
const OpenAIResponseUsageSchema = z
.object({
input_tokens: z.number().int().nonnegative(),
output_tokens: z.number().int().nonnegative(),
total_tokens: z.number().int().nonnegative(),
input_tokens_details: z
.record(z.string(), z.number().int().nonnegative().nullish())
.nullish(),
output_tokens_details: z
.record(z.string(), z.number().int().nonnegative().nullish())
.nullish(),
})
.strict()
.transform((v) => {
if (!v) return;
const {
input_tokens,
output_tokens,
total_tokens,
input_tokens_details,
output_tokens_details,
} = v;
const result: z.infer<typeof RawUsageDetails> & {
input: number;
output: number;
total: number;
} = {
input: input_tokens,
output: output_tokens,
total: total_tokens,
};
if (input_tokens_details) {
for (const [key, value] of Object.entries(input_tokens_details)) {
if (value !== null && value !== undefined) {
result[`input_${key}`] = value;
result.input = Math.max(result.input - (value ?? 0), 0);
}
}
}
if (output_tokens_details) {
for (const [key, value] of Object.entries(output_tokens_details)) {
if (value !== null && value !== undefined) {
result[`output_${key}`] = value;
result.output = Math.max(result.output - (value ?? 0), 0);
}
}
}
return result;
})
.pipe(RawUsageDetails);
export const UsageDetails = z
.union([
OpenAICompletionUsageSchema,
OpenAIResponseUsageSchema,
RawUsageDetails,
])
.nullish();
export const EnvironmentName = z
.string()
.max(40, "Maximum length is 40 characters")
.regex(
/^(?!langfuse)[a-z0-9-_]+$/,
"Only alphanumeric lower case characters, hyphens, and underscores are allowed, and it must not start with 'langfuse'",
)
.default("default");
// 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(),
id: idSchema.nullish(),
timestamp: stringDateTime,
name: z.string().max(1000).nullish(),
externalId: z.string().nullish(),
@@ -128,6 +215,7 @@ export const TraceBody = z.object({
output: z.any().nullish(),
sessionId: z.string().nullish(),
userId: z.string().nullish(),
environment: EnvironmentName,
metadata: jsonSchema.nullish(),
release: z.string().nullish(),
version: z.string().nullish(),
@@ -136,24 +224,25 @@ export const TraceBody = z.object({
});
export const OptionalObservationBody = z.object({
traceId: z.string().nullish(),
traceId: idSchema.nullish(),
environment: EnvironmentName,
name: z.string().nullish(),
startTime: stringDateTime,
metadata: jsonSchema.nullish(),
input: z.any().nullish(),
output: z.any().nullish(),
level: z.nativeEnum(ObservationLevel).nullish(),
level: ObservationLevel.nullish(),
statusMessage: z.string().nullish(),
parentObservationId: z.string().nullish(),
version: z.string().nullish(),
});
export const CreateEventEvent = OptionalObservationBody.extend({
id: NonEmptyString,
id: idSchema,
});
export const UpdateEventEvent = OptionalObservationBody.extend({
id: NonEmptyString,
id: idSchema,
});
export const CreateSpanBody = CreateEventEvent.extend({
@@ -182,8 +271,8 @@ export const CreateGenerationBody = CreateSpanBody.extend({
)
.nullish(),
usage: usage,
usageDetails: UsageOrCostDetails,
costDetails: UsageOrCostDetails,
usageDetails: UsageDetails,
costDetails: CostDetails,
promptName: z.string().nullish(),
promptVersion: z.number().int().nullish(),
}).refine((value) => {
@@ -212,8 +301,8 @@ export const UpdateGenerationBody = UpdateSpanBody.extend({
)
.nullish(),
usage: usage,
usageDetails: UsageOrCostDetails,
costDetails: UsageOrCostDetails,
usageDetails: UsageDetails,
costDetails: CostDetails,
promptName: z.string().nullish(),
promptVersion: z.number().int().nullish(),
}).refine((value) => {
@@ -225,12 +314,15 @@ export const UpdateGenerationBody = UpdateSpanBody.extend({
});
const BaseScoreBody = z.object({
id: z.string().nullish(),
id: idSchema.nullish(),
name: NonEmptyString,
traceId: z.string(),
environment: EnvironmentName,
observationId: z.string().nullish(),
comment: z.string().nullish(),
source: z.nativeEnum(ScoreSource).default(ScoreSource.API),
source: z
.enum(["API", "EVAL", "ANNOTATION"])
.default("API" as ScoreSourceType),
});
/**
@@ -272,8 +364,8 @@ export const ScoreBody = z.discriminatedUnion("dataType", [
// LEGACY, only required for backwards compatibility
export const LegacySpanPostSchema = z.object({
id: z.string().nullish(),
traceId: z.string().nullish(),
id: idSchema.nullish(),
traceId: idSchema.nullish(),
name: z.string().nullish(),
startTime: stringDateTime,
endTime: stringDateTime,
@@ -281,28 +373,28 @@ export const LegacySpanPostSchema = z.object({
input: jsonSchema.nullish(),
output: jsonSchema.nullish(),
parentObservationId: z.string().nullish(),
level: z.nativeEnum(ObservationLevel).nullish(),
level: ObservationLevel.nullish(),
statusMessage: z.string().nullish(),
version: z.string().nullish(),
});
export const LegacySpanPatchSchema = z.object({
spanId: z.string(),
traceId: z.string().nullish(),
spanId: idSchema,
traceId: idSchema.nullish(),
name: z.string().nullish(),
startTime: stringDateTime,
endTime: stringDateTime,
metadata: jsonSchema.nullish(),
input: jsonSchema.nullish(),
output: jsonSchema.nullish(),
level: z.nativeEnum(ObservationLevel).nullish(),
level: ObservationLevel.nullish(),
statusMessage: z.string().nullish(),
version: z.string().nullish(),
});
export const LegacyGenerationsCreateSchema = z.object({
id: z.string().nullish(),
traceId: z.string().nullish(),
id: idSchema.nullish(),
traceId: idSchema.nullish(),
name: z.string().nullish(),
startTime: stringDateTime,
endTime: stringDateTime,
@@ -319,14 +411,14 @@ export const LegacyGenerationsCreateSchema = z.object({
usage: usage,
metadata: jsonSchema.nullish(),
parentObservationId: z.string().nullish(),
level: z.nativeEnum(ObservationLevel).nullish(),
level: ObservationLevel.nullish(),
statusMessage: z.string().nullish(),
version: z.string().nullish(),
});
export const LegacyGenerationPatchSchema = z.object({
generationId: z.string(),
traceId: z.string().nullish(),
generationId: idSchema,
traceId: idSchema.nullish(),
name: z.string().nullish(),
startTime: stringDateTime,
endTime: stringDateTime,
@@ -342,14 +434,14 @@ export const LegacyGenerationPatchSchema = z.object({
completion: jsonSchema.nullish(),
usage: usage,
metadata: jsonSchema.nullish(),
level: z.nativeEnum(ObservationLevel).nullish(),
level: ObservationLevel.nullish(),
statusMessage: z.string().nullish(),
version: z.string().nullish(),
});
export const LegacyObservationBody = z.object({
id: z.string().nullish(),
traceId: z.string().nullish(),
id: idSchema.nullish(),
traceId: idSchema.nullish(),
type: z.enum(["GENERATION", "SPAN", "EVENT"]),
name: z.string().nullish(),
startTime: stringDateTime,
@@ -365,11 +457,11 @@ export const LegacyObservationBody = z.object({
input: jsonSchema.nullish(),
output: jsonSchema.nullish(),
usage: usage,
usageDetails: UsageOrCostDetails,
costDetails: UsageOrCostDetails,
usageDetails: UsageDetails,
costDetails: CostDetails,
metadata: jsonSchema.nullish(),
parentObservationId: z.string().nullish(),
level: z.nativeEnum(ObservationLevel).nullish(),
level: ObservationLevel.nullish(),
statusMessage: z.string().nullish(),
version: z.string().nullish(),
});
@@ -379,18 +471,6 @@ export const SdkLogEvent = z.object({
id: z.string().nullish(), // Not used, but makes downstream processing easier.
});
// definitions for the ingestion API
export const observationTypes = [
"observation-create",
"observation-update",
"generation-create",
"generation-update",
"span-create",
"span-update",
"event-create",
];
export const eventTypes = {
TRACE_CREATE: "trace-create",
SCORE_CREATE: "score-create",
@@ -406,7 +486,7 @@ export const eventTypes = {
} as const;
const base = z.object({
id: z.string(),
id: idSchema,
timestamp: z.string().datetime({ offset: true }),
metadata: jsonSchema.nullish(),
});
@@ -469,28 +549,6 @@ export const ingestionEvent = z.discriminatedUnion("type", [
]);
export type IngestionEventType = z.infer<typeof ingestionEvent>;
export const ingestionBatchEvent = z.array(ingestionEvent);
export type IngestionBatchEventType = z.infer<typeof ingestionBatchEvent>;
export const ingestionEventWithProjectId = ingestionEvent.and(
z.object({ projectId: z.string() }),
);
export type IngestionEventWithProjectIdType = z.infer<
typeof ingestionEventWithProjectId
>;
export const ingestionApiSchema = z.object({
batch: ingestionBatchEvent,
metadata: jsonSchema.nullish(),
});
export const ingestionApiSchemaWithProjectId = ingestionApiSchema.extend({
projectId: z.string(),
});
export type IngestionApiSchemaWithProjectId = z.infer<
typeof ingestionApiSchemaWithProjectId
>;
export type ObservationEvent =
| z.infer<typeof legacyObservationCreateEvent>
| z.infer<typeof legacyObservationUpdateEvent>
@@ -4,7 +4,8 @@ import {
validateDbScoreConfigSafe,
ValidatedScoreConfig,
} from "../../../src";
import { prisma, Score, ScoreDataType } from "../../db";
import { prisma, ScoreDataType } from "../../db";
import { Score } from "../repositories";
import { InvalidRequestError, LangfuseNotFoundError } from "../../errors";
@@ -160,35 +160,49 @@ export const addUserToSpan = (
export const getTracer = (name: string) => opentelemetry.trace.getTracer(name);
const cloudWatchClient = new CloudWatchClient();
const cloudWatchLastSubmitted: Record<string, number> = {};
const sendCloudWatchMetric = (key: string, value: number | undefined) => {
const currentTime = Date.now();
const interval = 30 * 1000;
let lastFlushTime = 0;
let metricCache: Record<string, number> = {};
// Check if the function has been executed in the last 30s for this key
if (
!cloudWatchLastSubmitted[key] ||
currentTime - cloudWatchLastSubmitted[key] >= interval
) {
cloudWatchLastSubmitted[key] = currentTime;
cloudWatchClient
.send(
new PutMetricDataCommand({
Namespace: "Langfuse",
MetricData: [
{
MetricName: key,
Value: value ?? 0,
},
],
}),
)
.catch((error) => {
logger.warn("Failed to send metric to CloudWatch", error);
});
// Caches metrics and flushes them on schedule
const sendCloudWatchMetric = (key: string, value: number, replace: boolean) => {
// Store the latest value for each metric key. If replace is false (e.g. for increments) we add the value to the existing value.
metricCache[key] = replace ? value : (metricCache[key] || 0) + value;
const currentTime = Date.now();
const flushInterval = 30 * 1000; // 30 seconds
// Check if it's time to flush the metrics
if (currentTime - lastFlushTime >= flushInterval) {
flushMetricsToCloudWatch();
}
};
// Flush all cached metrics in a single API call
const flushMetricsToCloudWatch = () => {
if (Object.keys(metricCache).length === 0) return;
lastFlushTime = Date.now();
const metricData = Object.entries(metricCache).map(([key, value]) => ({
MetricName: key,
Value: value,
}));
// Clear the cache after preparing the metrics
metricCache = {};
cloudWatchClient
.send(
new PutMetricDataCommand({
Namespace: "Langfuse",
MetricData: metricData,
}),
)
.catch((error) => {
logger.warn("Failed to send metrics to CloudWatch", error);
});
};
export const recordGauge = (
stat: string,
value?: number | undefined,
@@ -199,7 +213,7 @@ export const recordGauge = (
| undefined,
) => {
if (env.ENABLE_AWS_CLOUDWATCH_METRIC_PUBLISHING === "true") {
sendCloudWatchMetric(stat, value);
sendCloudWatchMetric(stat, value ?? 0, true);
}
dd.dogstatsd.gauge(stat, value, tags);
};
@@ -210,7 +224,7 @@ export const recordIncrement = (
tags?: { [tag: string]: string | number } | undefined,
) => {
if (env.ENABLE_AWS_CLOUDWATCH_METRIC_PUBLISHING === "true") {
sendCloudWatchMetric(stat, value);
sendCloudWatchMetric(stat, value ?? 1, false);
}
dd.dogstatsd.increment(stat, value, tags);
};
@@ -220,9 +234,6 @@ export const recordHistogram = (
value?: number | undefined,
tags?: { [tag: string]: string | number } | undefined,
) => {
if (env.ENABLE_AWS_CLOUDWATCH_METRIC_PUBLISHING === "true") {
sendCloudWatchMetric(stat, value);
}
dd.dogstatsd.histogram(stat, value, tags);
};
@@ -3,6 +3,7 @@ import type { ZodSchema } from "zod";
import { ChatAnthropic } from "@langchain/anthropic";
import { ChatVertexAI } from "@langchain/google-vertexai";
import { ChatBedrockConverse } from "@langchain/aws";
import { ChatGoogleGenerativeAI } from "@langchain/google-genai";
import {
AIMessage,
BaseMessage,
@@ -133,7 +134,10 @@ export async function fetchLLMCompletion(
finalMessages = messages.map((message) => {
if (message.role === ChatMessageRole.User)
return new HumanMessage(message.content);
if (message.role === ChatMessageRole.System)
if (
message.role === ChatMessageRole.System ||
message.role === ChatMessageRole.Developer
)
return new SystemMessage(message.content);
return new AIMessage(message.content);
@@ -146,7 +150,8 @@ export async function fetchLLMCompletion(
| ChatOpenAI
| ChatAnthropic
| ChatBedrockConverse
| ChatVertexAI;
| ChatVertexAI
| ChatGoogleGenerativeAI;
if (modelParams.adapter === LLMAdapter.Anthropic) {
chatModel = new ChatAnthropic({
anthropicApiKey: apiKey,
@@ -179,13 +184,16 @@ export async function fetchLLMCompletion(
azureOpenAIApiKey: apiKey,
azureOpenAIBasePath: baseURL,
azureOpenAIApiDeploymentName: modelParams.model,
azureOpenAIApiVersion: "2024-02-01",
azureOpenAIApiVersion: "2025-02-01-preview",
temperature: modelParams.temperature,
maxTokens: modelParams.max_tokens,
topP: modelParams.top_p,
callbacks: finalCallbacks,
maxRetries,
timeout: 1000 * 60 * 2, // 2 minutes timeout
configuration: {
defaultHeaders: extraHeaders,
},
});
} else if (modelParams.adapter === LLMAdapter.Bedrock) {
const { region } = BedrockConfigSchema.parse(config);
@@ -219,6 +227,32 @@ export async function fetchLLMCompletion(
credentials,
},
});
} else if (modelParams.adapter === LLMAdapter.GoogleAIStudio) {
chatModel = new ChatGoogleGenerativeAI({
model: modelParams.model,
temperature: modelParams.temperature,
maxOutputTokens: modelParams.max_tokens,
topP: modelParams.top_p,
callbacks: finalCallbacks,
maxRetries,
apiKey,
});
} else if (modelParams.adapter === LLMAdapter.Atla) {
// Atla models do not support:
// - temperature
// - max_tokens
// - top_p
chatModel = new ChatOpenAI({
openAIApiKey: apiKey,
modelName: modelParams.model,
callbacks: finalCallbacks,
maxRetries,
configuration: {
baseURL: baseURL,
defaultHeaders: extraHeaders,
},
timeout: 1000 * 60, // 1 minute timeout
});
} else {
// eslint-disable-next-line no-unused-vars
const _exhaustiveCheck: never = modelParams.adapter;
@@ -244,8 +278,8 @@ export async function fetchLLMCompletion(
/*
Workaround OpenAI reasoning models:
This is a temporary workaround to avoid sending system messages to OpenAI's O1 models.
O1 models do not support in beta:
This is a temporary workaround to avoid sending unsupported parameters to OpenAI's O1 models.
O1 models do not support:
- system messages
- top_p
- max_tokens at all, one has to use max_completion_tokens instead
@@ -257,6 +291,12 @@ export async function fetchLLMCompletion(
modelParams.model.startsWith("o1-") ||
modelParams.model.startsWith("o3-")
) {
const filteredMessages = finalMessages.filter((message) => {
return (
modelParams.model.startsWith("o3-") || message._getType() !== "system"
);
});
return {
completion: await new ChatOpenAI({
openAIApiKey: apiKey,
@@ -275,10 +315,7 @@ export async function fetchLLMCompletion(
timeout: 1000 * 60 * 2, // 2 minutes timeout
})
.pipe(new StringOutputParser())
.invoke(
finalMessages.filter((message) => message._getType() !== "system"),
runConfig,
),
.invoke(filteredMessages, runConfig),
processTracedEvents,
};
}
+27
View File
@@ -4,6 +4,9 @@ import { BedrockConfigSchema } from "../../interfaces/customLLMProviderConfigSch
import { TokenCountDelegate } from "../ingestion/processEventBatch";
import { AuthHeaderValidVerificationResult } from "../auth/types";
/* eslint-disable no-unused-vars */
// disable lint as this is exported and used in web/worker
export type PromptVariable = { name: string; value: string; isUsed: boolean };
export type ChatMessage = {
@@ -16,17 +19,25 @@ export type ChatMessageWithId = ChatMessage & { id: string };
export enum LLMAdapter {
Anthropic = "anthropic",
OpenAI = "openai",
Atla = "atla",
Azure = "azure",
Bedrock = "bedrock",
VertexAI = "google-vertex-ai",
GoogleAIStudio = "google-ai-studio",
}
export enum ChatMessageRole {
System = "system",
User = "user",
Assistant = "assistant",
Developer = "developer",
}
export const SYSTEM_ROLES: string[] = [
ChatMessageRole.System,
ChatMessageRole.Developer,
];
export const ChatMessageDefaultRoleSchema = z.nativeEnum(ChatMessageRole);
const ChatMessageSchema = z.object({
@@ -91,6 +102,8 @@ export const openAIModels = [
"o1-preview-2024-09-12",
"o1-mini",
"o1-mini-2024-09-12",
"gpt-4.5-preview",
"gpt-4.5-preview-2025-02-27",
"gpt-4-turbo-preview",
"gpt-4-1106-preview",
"gpt-4-0613",
@@ -109,6 +122,7 @@ export type OpenAIModel = (typeof openAIModels)[number];
// NOTE: Update docs page when changing this! https://langfuse.com/docs/playground#openai-playground--anthropic-playground
export const anthropicModels = [
"claude-3-7-sonnet-20250219",
"claude-3-5-sonnet-20241022",
"claude-3-5-sonnet-20240620",
"claude-3-opus-20240229",
@@ -130,14 +144,27 @@ export const vertexAIModels = [
"gemini-1.0-pro",
] as const;
export const googleAIStudioModels = [
"gemini-2.0-flash",
"gemini-2.0-flash-lite-preview-02-05",
"gemini-2.0-flash-thinking-exp-01-21",
"gemini-1.5-pro",
"gemini-1.5-flash",
"gemini-1.5-flash-8b",
] as const;
export const atlaModels = ["atla-selene", "atla-selene-20250214"] 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.GoogleAIStudio]: googleAIStudioModels,
[LLMAdapter.Azure]: [],
[LLMAdapter.Bedrock]: [],
[LLMAdapter.Atla]: atlaModels,
} as const;
export type LLMFunctionCall = {
@@ -383,6 +383,10 @@ export class FilterList {
return this.filters.find(predicate);
}
filter(predicate: (filter: Filter) => boolean) {
return new FilterList(this.filters.filter(predicate));
}
some(predicate: (filter: Filter) => boolean) {
return this.filters.some(predicate);
}
@@ -3,7 +3,7 @@ import { singleFilter } from "../../../interfaces/filters";
import { FilterCondition } from "../../../types";
import { isValidTableName } from "../../clickhouse/schemaUtils";
import { logger } from "../../logger";
import { UiColumnMapping } from "../../../tableDefinitions";
import { UiColumnMappings } from "../../../tableDefinitions";
import {
StringFilter,
DateTimeFilter,
@@ -29,7 +29,7 @@ export class QueryBuilderError extends Error {
// User input for values (e.g. project_id = <value>) are sent to Clickhouse as parameters to prevent SQL injection
export const createFilterFromFilterState = (
filter: FilterCondition[],
columnMapping: UiColumnMapping[],
columnMapping: UiColumnMappings,
) => {
return filter.map((frontEndFilter) => {
// checks if the column exists in the clickhouse schema
@@ -120,7 +120,7 @@ export const createFilterFromFilterState = (
const matchAndVerifyTracesUiColumn = (
filter: z.infer<typeof singleFilter>,
uiTableDefinitions: UiColumnMapping[],
uiTableDefinitions: UiColumnMappings,
) => {
// tries to match the column name to the clickhouse table name
logger.debug(`Filter to match: ${JSON.stringify(filter)}`);
@@ -1,13 +1,13 @@
import z from "zod";
import { OrderByState } from "../../../interfaces/orderBy";
import { UiColumnMapping } from "../../../tableDefinitions";
import { UiColumnMappings } from "../../../tableDefinitions";
import { logger } from "../../logger";
type OrderByStateNotNull = Exclude<OrderByState, null>;
export function orderByToClickhouseSql(
orderBy: OrderByState | OrderByState[] = [],
tableColumns: UiColumnMapping[],
tableColumns: UiColumnMappings,
): string {
if (
!orderBy ||
@@ -1,4 +1,4 @@
import { ObservationView } from "@prisma/client";
import { ObservationView } from "../repositories";
type AdditionalObservationFields = {
traceName: string | null;
@@ -18,3 +18,4 @@ export {
type ClickhouseOperator,
} from "./clickhouse-sql/clickhouse-filter";
export { orderByToClickhouseSql } from "./clickhouse-sql/orderby-factory";
export { createFilterFromFilterState } from "./clickhouse-sql/factory";
+95 -9
View File
@@ -1,3 +1,4 @@
/* eslint-disable no-unused-vars */
import { z } from "zod";
import { eventTypes } from ".";
import {
@@ -33,6 +34,10 @@ export const TracesQueueEventSchema = z.object({
projectId: z.string(),
traceIds: z.array(z.string()),
});
export const ScoresQueueEventSchema = z.object({
projectId: z.string(),
scoreIds: z.array(z.string()),
});
export const ProjectQueueEventSchema = z.object({
projectId: z.string(),
orgId: z.string(),
@@ -51,6 +56,9 @@ export const EvalExecutionEvent = z.object({
export const PostHogIntegrationProcessingEventSchema = z.object({
projectId: z.string(),
});
export const BlobStorageIntegrationProcessingEventSchema = z.object({
projectId: z.string(),
});
export const ExperimentCreateEventSchema = z.object({
projectId: z.string(),
datasetId: z.string(),
@@ -61,19 +69,68 @@ export const DataRetentionProcessingEventSchema = z.object({
projectId: z.string(),
retention: z.number(),
});
export const BatchActionProcessingEventSchema = z.object({
projectId: z.string(),
actionId: z.string(),
query: BatchActionQuerySchema,
tableName: z.nativeEnum(BatchExportTableName),
cutoffCreatedAt: z.date(),
targetId: z.string().optional(),
type: z.nativeEnum(BatchActionType),
});
export const BatchActionProcessingEventSchema = z.discriminatedUnion(
"actionId",
[
z.object({
actionId: z.literal("score-delete"),
projectId: z.string(),
query: BatchActionQuerySchema,
tableName: z.nativeEnum(BatchExportTableName),
cutoffCreatedAt: z.date(),
targetId: z.string().optional(),
type: z.nativeEnum(BatchActionType),
}),
z.object({
actionId: z.literal("trace-delete"),
projectId: z.string(),
query: BatchActionQuerySchema,
tableName: z.nativeEnum(BatchExportTableName),
cutoffCreatedAt: z.date(),
targetId: z.string().optional(),
type: z.nativeEnum(BatchActionType),
}),
z.object({
actionId: z.literal("trace-add-to-annotation-queue"),
projectId: z.string(),
query: BatchActionQuerySchema,
tableName: z.nativeEnum(BatchExportTableName),
cutoffCreatedAt: z.date(),
targetId: z.string().optional(),
type: z.nativeEnum(BatchActionType),
}),
z.object({
actionId: z.literal("eval-create"),
targetObject: z.enum(["trace", "dataset"]),
configId: z.string(),
projectId: z.string(),
cutoffCreatedAt: z.date(),
query: BatchActionQuerySchema,
}),
],
);
export const CreateEvalQueueEventSchema = DatasetRunItemUpsertEventSchema.and(
z.object({
configId: z.string(),
timestamp: z.date(),
}),
).or(
TraceQueueEventSchema.and(
z.object({
timestamp: z.date(),
configId: z.string(),
}),
),
);
export type CreateEvalQueueEventType = z.infer<
typeof CreateEvalQueueEventSchema
>;
export type BatchExportJobType = z.infer<typeof BatchExportJobSchema>;
export type TraceQueueEventType = z.infer<typeof TraceQueueEventSchema>;
export type TracesQueueEventType = z.infer<typeof TracesQueueEventSchema>;
export type ScoresQueueEventType = z.infer<typeof ScoresQueueEventSchema>;
export type ProjectQueueEventType = z.infer<typeof ProjectQueueEventSchema>;
export type DatasetRunItemUpsertEventType = z.infer<
typeof DatasetRunItemUpsertEventSchema
@@ -92,6 +149,9 @@ export type DataRetentionProcessingEventType = z.infer<
export type BatchActionProcessingEventType = z.infer<
typeof BatchActionProcessingEventSchema
>;
export type BlobStorageIntegrationProcessingEventType = z.infer<
typeof BlobStorageIntegrationProcessingEventSchema
>;
export enum QueueName {
TraceUpsert = "trace-upsert", // Ingestion pipeline adds events on each Trace upsert
@@ -106,11 +166,15 @@ export enum QueueName {
ExperimentCreate = "experiment-create-queue",
PostHogIntegrationQueue = "posthog-integration-queue",
PostHogIntegrationProcessingQueue = "posthog-integration-processing-queue",
BlobStorageIntegrationQueue = "blobstorage-integration-queue",
BlobStorageIntegrationProcessingQueue = "blobstorage-integration-processing-queue",
CoreDataS3ExportQueue = "core-data-s3-export-queue",
MeteringDataPostgresExportQueue = "metering-data-postgres-export-queue",
DataRetentionQueue = "data-retention-queue",
DataRetentionProcessingQueue = "data-retention-processing-queue",
BatchActionQueue = "batch-action-queue",
CreateEvalQueue = "create-eval-queue",
ScoreDelete = "score-delete",
}
export enum QueueJobs {
@@ -126,11 +190,15 @@ export enum QueueJobs {
ExperimentCreateJob = "experiment-create-job",
PostHogIntegrationJob = "posthog-integration-job",
PostHogIntegrationProcessingJob = "posthog-integration-processing-job",
BlobStorageIntegrationJob = "blobstorage-integration-job",
BlobStorageIntegrationProcessingJob = "blobstorage-integration-processing-job",
CoreDataS3ExportJob = "core-data-s3-export-job",
MeteringDataPostgresExportJob = "metering-data-postgres-export-job",
DataRetentionJob = "data-retention-job",
DataRetentionProcessingJob = "data-retention-processing-job",
BatchActionProcessingJob = "batch-action-processing-job",
CreateEvalJob = "create-eval-job",
ScoreDelete = "score-delete",
}
export type TQueueJobTypes = {
@@ -146,6 +214,12 @@ export type TQueueJobTypes = {
payload: TracesQueueEventType | TraceQueueEventType;
name: QueueJobs.TraceDelete;
};
[QueueName.ScoreDelete]: {
timestamp: Date;
id: string;
payload: ScoresQueueEventType;
name: QueueJobs.ScoreDelete;
};
[QueueName.ProjectDelete]: {
timestamp: Date;
id: string;
@@ -206,4 +280,16 @@ export type TQueueJobTypes = {
payload: BatchActionProcessingEventType;
name: QueueJobs.BatchActionProcessingJob;
};
[QueueName.CreateEvalQueue]: {
timestamp: Date;
id: string;
payload: CreateEvalQueueEventType;
name: QueueJobs.CreateEvalJob;
};
[QueueName.BlobStorageIntegrationProcessingQueue]: {
timestamp: Date;
id: string;
payload: BlobStorageIntegrationProcessingEventType;
name: QueueJobs.BlobStorageIntegrationProcessingJob;
};
};
@@ -0,0 +1,40 @@
import { Queue } from "bullmq";
import { QueueName } from "../queues";
import { createNewRedisInstance, redisQueueRetryOptions } from "./redis";
import { logger } from "../logger";
export class BlobStorageIntegrationProcessingQueue {
private static instance: Queue | null = null;
public static getInstance(): Queue | null {
if (BlobStorageIntegrationProcessingQueue.instance) {
return BlobStorageIntegrationProcessingQueue.instance;
}
const newRedis = createNewRedisInstance({
enableOfflineQueue: false,
...redisQueueRetryOptions,
});
BlobStorageIntegrationProcessingQueue.instance = newRedis
? new Queue(QueueName.BlobStorageIntegrationProcessingQueue, {
connection: newRedis,
defaultJobOptions: {
removeOnComplete: true,
removeOnFail: 100_000,
attempts: 5,
backoff: {
type: "exponential",
delay: 5000,
},
},
})
: null;
BlobStorageIntegrationProcessingQueue.instance?.on("error", (err) => {
logger.error("BlobStorageIntegrationProcessingQueue error", err);
});
return BlobStorageIntegrationProcessingQueue.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 BlobStorageIntegrationQueue {
private static instance: Queue | null = null;
public static getInstance(): Queue | null {
if (BlobStorageIntegrationQueue.instance) {
return BlobStorageIntegrationQueue.instance;
}
const newRedis = createNewRedisInstance({
enableOfflineQueue: false,
...redisQueueRetryOptions,
});
BlobStorageIntegrationQueue.instance = newRedis
? new Queue(QueueName.BlobStorageIntegrationQueue, {
connection: newRedis,
defaultJobOptions: {
removeOnComplete: true,
removeOnFail: 100,
attempts: 5,
backoff: {
type: "exponential",
delay: 5000,
},
},
})
: null;
BlobStorageIntegrationQueue.instance?.on("error", (err) => {
logger.error("BlobStorageIntegrationQueue error", err);
});
if (BlobStorageIntegrationQueue.instance) {
logger.debug("Scheduling jobs for BlobStorageIntegrationQueue");
BlobStorageIntegrationQueue.instance
.add(
QueueJobs.BlobStorageIntegrationJob,
{},
{
repeat: { pattern: "20 * * * *" }, // every hour at 20 minutes past
},
)
.catch((err) => {
logger.error("Error adding BlobStorageIntegrationJob schedule", err);
});
}
return BlobStorageIntegrationQueue.instance;
}
}
@@ -0,0 +1,45 @@
import { QueueName, TQueueJobTypes } from "../queues";
import { Queue } from "bullmq";
import { createNewRedisInstance, redisQueueRetryOptions } from "./redis";
import { logger } from "../logger";
export class CreateEvalQueue {
private static instance: Queue<
TQueueJobTypes[QueueName.CreateEvalQueue]
> | null = null;
public static getInstance(): Queue<
TQueueJobTypes[QueueName.CreateEvalQueue]
> | null {
if (CreateEvalQueue.instance) return CreateEvalQueue.instance;
const newRedis = createNewRedisInstance({
enableOfflineQueue: false,
...redisQueueRetryOptions,
});
CreateEvalQueue.instance = newRedis
? new Queue<TQueueJobTypes[QueueName.CreateEvalQueue]>(
QueueName.CreateEvalQueue,
{
connection: newRedis,
defaultJobOptions: {
removeOnComplete: 100, // Important: If not true, new jobs for that ID would be ignored as jobs in the complete set are still considered as part of the queue
removeOnFail: 100_000,
attempts: 5,
backoff: {
type: "exponential",
delay: 5000,
},
},
},
)
: null;
CreateEvalQueue.instance?.on("error", (err) => {
logger.error("CreateEvalQueue error", err);
});
return CreateEvalQueue.instance;
}
}
@@ -11,11 +11,15 @@ import { TraceDeleteQueue } from "./traceDelete";
import { ProjectDeleteQueue } from "./projectDelete";
import { PostHogIntegrationQueue } from "./postHogIntegrationQueue";
import { PostHogIntegrationProcessingQueue } from "./postHogIntegrationProcessingQueue";
import { BlobStorageIntegrationQueue } from "./blobStorageIntegrationQueue";
import { BlobStorageIntegrationProcessingQueue } from "./blobStorageIntegrationProcessingQueue";
import { CoreDataS3ExportQueue } from "./coreDataS3ExportQueue";
import { MeteringDataPostgresExportQueue } from "./meteringDataPostgresExportQueue";
import { DataRetentionQueue } from "./dataRetentionQueue";
import { DataRetentionProcessingQueue } from "./dataRetentionProcessingQueue";
import { BatchActionQueue } from "./batchActionQueue";
import { CreateEvalQueue } from "./createEvalQueue";
import { ScoreDeleteQueue } from "./scoreDelete";
export function getQueue(queueName: QueueName): Queue | null {
switch (queueName) {
@@ -41,6 +45,10 @@ export function getQueue(queueName: QueueName): Queue | null {
return PostHogIntegrationQueue.getInstance();
case QueueName.PostHogIntegrationProcessingQueue:
return PostHogIntegrationProcessingQueue.getInstance();
case QueueName.BlobStorageIntegrationQueue:
return BlobStorageIntegrationQueue.getInstance();
case QueueName.BlobStorageIntegrationProcessingQueue:
return BlobStorageIntegrationProcessingQueue.getInstance();
case QueueName.IngestionSecondaryQueue:
return SecondaryIngestionQueue.getInstance();
case QueueName.CoreDataS3ExportQueue:
@@ -53,6 +61,10 @@ export function getQueue(queueName: QueueName): Queue | null {
return DataRetentionProcessingQueue.getInstance();
case QueueName.BatchActionQueue:
return BatchActionQueue.getInstance();
case QueueName.CreateEvalQueue:
return CreateEvalQueue.getInstance();
case QueueName.ScoreDelete:
return ScoreDeleteQueue.getInstance();
default:
const exhaustiveCheckDefault: never = queueName;
throw new Error(`Queue ${queueName} not found`);
+15
View File
@@ -1,4 +1,5 @@
import Redis, { RedisOptions } from "ioredis";
import fs from 'fs';
import { env } from "../../env";
import { logger } from "../logger";
@@ -23,10 +24,23 @@ export const redisQueueRetryOptions: Partial<RedisOptions> = {
export const createNewRedisInstance = (
additionalOptions: Partial<RedisOptions> = {},
) => {
const tlsEnabled = env.REDIS_TLS_ENABLED === "true";
const tlsOptions = tlsEnabled
? {
tls: {
ca: env.REDIS_TLS_CA_PATH ? fs.readFileSync(env.REDIS_TLS_CA_PATH) : undefined,
cert: env.REDIS_TLS_CERT_PATH ? fs.readFileSync(env.REDIS_TLS_CERT_PATH) : undefined,
key: env.REDIS_TLS_KEY_PATH ? fs.readFileSync(env.REDIS_TLS_KEY_PATH) : undefined,
},
}
: {};
const instance = env.REDIS_CONNECTION_STRING
? new Redis(env.REDIS_CONNECTION_STRING, {
...defaultRedisOptions,
...additionalOptions,
...tlsOptions,
})
: env.REDIS_HOST
? new Redis({
@@ -35,6 +49,7 @@ export const createNewRedisInstance = (
password: String(env.REDIS_AUTH),
...defaultRedisOptions,
...additionalOptions,
...tlsOptions,
})
: null;
@@ -0,0 +1,38 @@
import { QueueName, TQueueJobTypes } from "../queues";
import { Queue } from "bullmq";
import { createNewRedisInstance, redisQueueRetryOptions } from "./redis";
import { logger } from "../logger";
export class ScoreDeleteQueue {
private static instance: Queue<TQueueJobTypes[QueueName.ScoreDelete]> | null = null;
public static getInstance(): Queue<TQueueJobTypes[QueueName.ScoreDelete]> | null {
if (ScoreDeleteQueue.instance) return ScoreDeleteQueue.instance;
const newRedis = createNewRedisInstance({
enableOfflineQueue: false,
...redisQueueRetryOptions,
});
ScoreDeleteQueue.instance = newRedis
? new Queue<TQueueJobTypes[QueueName.ScoreDelete]>(QueueName.ScoreDelete, {
connection: newRedis,
defaultJobOptions: {
removeOnComplete: true,
removeOnFail: 100_000,
attempts: 2,
backoff: {
type: "exponential",
delay: 30_000,
},
},
})
: null;
ScoreDeleteQueue.instance?.on("error", (err) => {
logger.error("ScoreDeleteQueue error", err);
});
return ScoreDeleteQueue.instance;
}
}
@@ -25,10 +25,10 @@ export class TraceDeleteQueue {
defaultJobOptions: {
removeOnComplete: true,
removeOnFail: 100_000,
attempts: 5,
attempts: 2,
backoff: {
type: "exponential",
delay: 5000,
delay: 30_000,
},
},
},
@@ -36,6 +36,7 @@ export async function upsertClickhouse<
table: "scores" | "traces" | "observations";
records: T[];
eventBodyMapper: (body: T) => Record<string, unknown>;
tags?: Record<string, string>;
}): Promise<void> {
return await instrumentAsync({ name: "clickhouse-upsert" }, async (span) => {
// https://opentelemetry.io/docs/specs/semconv/database/database-spans/
@@ -56,7 +57,9 @@ export async function upsertClickhouse<
// Write new file directly to ClickHouse. We don't use the ClickHouse writer here as we expect more limited traffic
// and are not worried that much about latency.
await clickhouseClient().insert({
await clickhouseClient({
tags: opts.tags,
}).insert({
table: "event_log",
values: [
{
@@ -85,7 +88,7 @@ export async function upsertClickhouse<
}),
);
const res = await clickhouseClient().insert({
const res = await clickhouseClient({ tags: opts.tags }).insert({
table: opts.table,
values: opts.records.map((record) => ({
...record,
@@ -124,6 +127,7 @@ export async function* queryClickhouseStream<T>(opts: {
query: string;
params?: Record<string, unknown> | undefined;
clickhouseConfigs?: NodeClickHouseClientConfigOptions;
tags?: Record<string, string>;
}): AsyncGenerator<T> {
const tracer = getTracer("clickhouse-query-stream");
const span = tracer.startSpan("clickhouse-query-stream");
@@ -135,7 +139,10 @@ export async function* queryClickhouseStream<T>(opts: {
// https://opentelemetry.io/docs/specs/semconv/database/database-spans/
span.setAttribute("ch.query.text", opts.query);
const res = await clickhouseClient(opts.clickhouseConfigs).query({
const res = await clickhouseClient({
tags: opts.tags,
opts: opts.clickhouseConfigs,
}).query({
query: opts.query,
format: "JSONEachRow",
query_params: opts.params,
@@ -182,12 +189,16 @@ export async function queryClickhouse<T>(opts: {
query: string;
params?: Record<string, unknown> | undefined;
clickhouseConfigs?: NodeClickHouseClientConfigOptions;
tags?: Record<string, string>;
}): Promise<T[]> {
return await instrumentAsync({ name: "clickhouse-query" }, async (span) => {
// https://opentelemetry.io/docs/specs/semconv/database/database-spans/
span.setAttribute("ch.query.text", opts.query);
const res = await clickhouseClient(opts.clickhouseConfigs).query({
const res = await clickhouseClient({
tags: opts.tags,
opts: opts.clickhouseConfigs,
}).query({
query: opts.query,
format: "JSONEachRow",
query_params: opts.params,
@@ -221,15 +232,19 @@ export async function queryClickhouse<T>(opts: {
});
}
export async function commandClickhouse<T>(opts: {
export async function commandClickhouse(opts: {
query: string;
params?: Record<string, unknown> | undefined;
clickhouseConfigs?: NodeClickHouseClientConfigOptions;
tags?: Record<string, string>;
}): Promise<void> {
return await instrumentAsync({ name: "clickhouse-command" }, async (span) => {
// https://opentelemetry.io/docs/specs/semconv/database/database-spans/
span.setAttribute("ch.query.text", opts.query);
const res = await clickhouseClient(opts.clickhouseConfigs).command({
const res = await clickhouseClient({
tags: opts.tags,
opts: opts.clickhouseConfigs,
}).command({
query: opts.query,
query_params: opts.params,
});
@@ -4,11 +4,8 @@ import {
} from "./clickhouse";
import { createFilterFromFilterState } from "../queries/clickhouse-sql/factory";
import { FilterState } from "../../types";
import {
DateTimeFilter,
FilterList,
} from "../queries/clickhouse-sql/clickhouse-filter";
import { dashboardColumnDefinitions } from "../../tableDefinitions/mapDashboards";
import { DateTimeFilter, FilterList } from "../queries";
import { dashboardColumnDefinitions } from "../../tableDefinitions";
import { convertDateToClickhouseDateTime } from "../clickhouse/client";
import {
OBSERVATIONS_TO_TRACE_INTERVAL,
@@ -18,12 +15,37 @@ import {
export type DateTrunc = "year" | "month" | "week" | "day" | "hour" | "minute";
const extractEnvironmentFilterFromFilters = (
filter: FilterState,
): { envFilter: FilterState; remainingFilters: FilterState } => {
return {
envFilter: filter.filter((f) => f.column === "environment"),
remainingFilters: filter.filter((f) => f.column !== "environment"),
};
};
const convertEnvFilterToClickhouseFilter = (filter: FilterState) => {
return createFilterFromFilterState(filter, [
{
clickhouseSelect: "environment",
clickhouseTableName: "traces",
uiTableId: "environment",
uiTableName: "Environment",
},
]);
};
export const getTotalTraces = async (
projectId: string,
filter: FilterState,
) => {
const { envFilter, remainingFilters } =
extractEnvironmentFilterFromFilters(filter);
const environmentFilter = new FilterList(
convertEnvFilterToClickhouseFilter(envFilter),
).apply();
const chFilter = new FilterList(
createFilterFromFilterState(filter, dashboardColumnDefinitions),
createFilterFromFilterState(remainingFilters, dashboardColumnDefinitions),
).apply();
const query = `
@@ -31,13 +53,22 @@ export const getTotalTraces = async (
count(id) as count
FROM traces t FINAL
WHERE project_id = {projectId: String}
AND ${chFilter.query}`;
AND ${chFilter.query}
${environmentFilter.query ? `AND ${environmentFilter.query}` : ""}
`;
const result = await queryClickhouse<{ count: number }>({
query,
params: {
projectId,
...chFilter.params,
...environmentFilter.params,
},
tags: {
feature: "dashboard",
type: "totalTraces",
kind: "analytic",
projectId,
},
});
@@ -52,8 +83,13 @@ export const getObservationsCostGroupedByName = async (
projectId: string,
filter: FilterState,
) => {
const { envFilter, remainingFilters } =
extractEnvironmentFilterFromFilters(filter);
const environmentFilter = new FilterList(
convertEnvFilterToClickhouseFilter(envFilter),
).apply();
const chFilter = new FilterList(
createFilterFromFilterState(filter, dashboardColumnDefinitions),
createFilterFromFilterState(remainingFilters, dashboardColumnDefinitions),
);
const appliedFilter = chFilter.apply();
@@ -68,6 +104,7 @@ export const getObservationsCostGroupedByName = async (
FROM observations o FINAL ${hasTraceFilter ? "LEFT JOIN traces t ON o.trace_id = t.id AND o.project_id = t.project_id" : ""}
WHERE project_id = {projectId: String}
${appliedFilter.query ? `AND ${appliedFilter.query}` : ""}
${environmentFilter.query ? `AND ${environmentFilter.query}` : ""}
GROUP BY provided_model_name
ORDER BY sumMap(cost_details)['total'] DESC
LIMIT 50
@@ -82,6 +119,13 @@ export const getObservationsCostGroupedByName = async (
params: {
projectId,
...appliedFilter.params,
...environmentFilter.params,
},
tags: {
feature: "dashboard",
type: "observationCostGroupedByName",
kind: "analytic",
projectId,
},
});
@@ -92,8 +136,13 @@ export const getScoreAggregate = async (
projectId: string,
filter: FilterState,
) => {
const { envFilter, remainingFilters } =
extractEnvironmentFilterFromFilters(filter);
const environmentFilter = new FilterList(
convertEnvFilterToClickhouseFilter(envFilter),
).apply();
const chFilter = new FilterList(
createFilterFromFilterState(filter, dashboardColumnDefinitions),
createFilterFromFilterState(remainingFilters, dashboardColumnDefinitions),
);
const timeFilter = chFilter.find(
@@ -117,6 +166,7 @@ export const getScoreAggregate = async (
${hasTraceFilter ? "JOIN traces t FINAL ON t.id = s.trace_id AND t.project_id = s.project_id" : ""}
WHERE s.project_id = {projectId: String}
AND ${chFilterApplied.query}
${environmentFilter.query ? `AND ${environmentFilter.query}` : ""}
${timeFilter && hasTraceFilter ? `AND t.timestamp >= {tracesTimestamp: DateTime64(3)} - ${SCORE_TO_TRACE_OBSERVATIONS_INTERVAL}` : ""}
GROUP BY s.name, s.source, s.data_type
ORDER BY count(*) DESC
@@ -133,10 +183,17 @@ export const getScoreAggregate = async (
params: {
projectId,
...chFilterApplied.params,
...environmentFilter.params,
...(timeFilter
? { tracesTimestamp: convertDateToClickhouseDateTime(timeFilter.value) }
: {}),
},
tags: {
feature: "dashboard",
type: "scoreAggregate",
kind: "analytic",
projectId,
},
});
return result;
@@ -146,8 +203,13 @@ export const groupTracesByTime = async (
projectId: string,
filter: FilterState,
) => {
const { envFilter, remainingFilters } =
extractEnvironmentFilterFromFilters(filter);
const environmentFilter = new FilterList(
convertEnvFilterToClickhouseFilter(envFilter),
).apply();
const chFilter = new FilterList(
createFilterFromFilterState(filter, dashboardColumnDefinitions),
createFilterFromFilterState(remainingFilters, dashboardColumnDefinitions),
).apply();
const [orderByQuery, orderByParams, bucketSizeInSeconds] = orderByTimeSeries(
@@ -162,6 +224,7 @@ export const groupTracesByTime = async (
FROM traces t FINAL
WHERE project_id = {projectId: String}
AND ${chFilter.query}
${environmentFilter.query ? `AND ${environmentFilter.query}` : ""}
GROUP BY timestamp
${orderByQuery}
`;
@@ -173,8 +236,15 @@ export const groupTracesByTime = async (
params: {
projectId,
...chFilter.params,
...environmentFilter.params,
...orderByParams,
},
tags: {
feature: "dashboard",
type: "tracesByTime",
kind: "analytic",
projectId,
},
});
return result.map((row) => ({
@@ -183,12 +253,17 @@ export const groupTracesByTime = async (
}));
};
export const getObservationUsageByTime = async (
export const getTotalObservationUsageByTimeByModel = async (
projectId: string,
filter: FilterState,
) => {
const { envFilter, remainingFilters } =
extractEnvironmentFilterFromFilters(filter);
const environmentFilter = new FilterList(
convertEnvFilterToClickhouseFilter(envFilter),
).apply();
const chFilter = new FilterList(
createFilterFromFilterState(filter, dashboardColumnDefinitions),
createFilterFromFilterState(remainingFilters, dashboardColumnDefinitions),
);
const appliedFilter = chFilter.apply();
@@ -211,13 +286,14 @@ export const getObservationUsageByTime = async (
const query = `
SELECT
${selectTimeseriesColumn(bucketSizeInSeconds, "start_time", "start_time")},
sumMap(usage_details) as units,
sumMap(cost_details) as cost,
sumMap(usage_details)['total'] as units,
sumMap(cost_details)['total'] 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" : ""}
WHERE project_id = {projectId: String}
AND ${appliedFilter.query}
${environmentFilter.query ? `AND ${environmentFilter.query}` : ""}
${timeFilter ? `AND t.timestamp >= {traceTimestamp: DateTime64(3)} - ${OBSERVATIONS_TO_TRACE_INTERVAL}` : ""}
GROUP BY start_time, provided_model_name
${orderByQuery}
@@ -225,45 +301,243 @@ export const getObservationUsageByTime = async (
const result = await queryClickhouse<{
start_time: string;
units: Record<string, number>;
cost: Record<string, number>;
units: string;
cost: string;
provided_model_name: string;
}>({
query,
params: {
projectId,
...appliedFilter.params,
...environmentFilter.params,
...orderByParams,
...(timeFilter
? { traceTimestamp: convertDateToClickhouseDateTime(timeFilter.value) }
: {}),
},
tags: {
feature: "dashboard",
type: "observationUsageByTime",
kind: "analytic",
projectId,
},
});
return result.map((row) => ({
start_time: parseClickhouseUTCDateTimeFormat(row.start_time),
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,
startTime: parseClickhouseUTCDateTimeFormat(row.start_time),
units: Number(row.units),
cost: Number(row.cost),
model: row.provided_model_name,
}));
};
export const getObservationCostByTypeByTime = async (
projectId: string,
filter: FilterState,
) => {
const { envFilter, remainingFilters } =
extractEnvironmentFilterFromFilters(filter);
const environmentFilter = new FilterList(
convertEnvFilterToClickhouseFilter(envFilter),
).apply();
const chFilter = new FilterList(
createFilterFromFilterState(remainingFilters, dashboardColumnDefinitions),
);
const appliedFilter = chFilter.apply();
const tracesFilter = chFilter.find((f) => f.clickhouseTable === "traces");
const timeFilter = tracesFilter
? (chFilter.find(
(f) =>
f.clickhouseTable === "observations" &&
f.field.includes("start_time") &&
(f.operator === ">=" || f.operator === ">"),
) as DateTimeFilter | undefined)
: undefined;
const [orderByQuery, orderByParams, bucketSizeInSeconds] = orderByTimeSeries(
filter,
"start_time",
);
const query = `
SELECT
start_time,
groupArray((cost_key, cost_sum)) AS costs
FROM (
SELECT
${selectTimeseriesColumn(bucketSizeInSeconds, "start_time", "start_time")},
cost_key,
SUM(cost) AS cost_sum
FROM
observations o FINAL
${tracesFilter ? "LEFT JOIN traces t ON o.trace_id = t.id AND o.project_id = t.project_id" : ""}
ARRAY JOIN
mapKeys(cost_details) AS cost_key,
mapValues(cost_details) AS cost
WHERE project_id = {projectId: String}
AND ${appliedFilter.query}
${environmentFilter.query ? `AND ${environmentFilter.query}` : ""}
${timeFilter ? `AND t.timestamp >= {traceTimestamp: DateTime64(3)} - ${OBSERVATIONS_TO_TRACE_INTERVAL}` : ""}
GROUP BY
start_time,
cost_key
)
GROUP BY
start_time
${orderByQuery}
`;
const result = await queryClickhouse<{
start_time: string;
costs: Array<[string, number | null]>;
}>({
query,
params: {
projectId,
...appliedFilter.params,
...environmentFilter.params,
...orderByParams,
...(timeFilter
? { traceTimestamp: convertDateToClickhouseDateTime(timeFilter.value) }
: {}),
},
tags: {
feature: "dashboard",
type: "observationCostByTypeByTime",
kind: "analytic",
projectId,
},
});
const types = result.flatMap((row) => {
return row.costs.map((cost) => cost[0]);
});
const uniqueTypes = [...new Set(types)];
return result.flatMap((row) => {
const intervalStart = parseClickhouseUTCDateTimeFormat(row.start_time);
return uniqueTypes.map((type) => ({
intervalStart: intervalStart,
key: type,
sum: row.costs.find((cost) => cost[0] === type)?.[1]
? Number(row.costs.find((cost) => cost[0] === type)?.[1])
: 0,
}));
});
};
export const getObservationUsageByTypeByTime = async (
projectId: string,
filter: FilterState,
) => {
const { envFilter, remainingFilters } =
extractEnvironmentFilterFromFilters(filter);
const environmentFilter = new FilterList(
convertEnvFilterToClickhouseFilter(envFilter),
).apply();
const chFilter = new FilterList(
createFilterFromFilterState(remainingFilters, dashboardColumnDefinitions),
);
const appliedFilter = chFilter.apply();
const tracesFilter = chFilter.find((f) => f.clickhouseTable === "traces");
const timeFilter = tracesFilter
? (chFilter.find(
(f) =>
f.clickhouseTable === "observations" &&
f.field.includes("start_time") &&
(f.operator === ">=" || f.operator === ">"),
) as DateTimeFilter | undefined)
: undefined;
const [orderByQuery, orderByParams, bucketSizeInSeconds] = orderByTimeSeries(
filter,
"start_time",
);
const query = `
SELECT
start_time,
groupArray((usage_key, usage_sum)) AS usages
FROM (
SELECT
${selectTimeseriesColumn(bucketSizeInSeconds, "start_time", "start_time")} ,
usage_key,
SUM(usage) AS usage_sum
FROM
observations o FINAL
${tracesFilter ? "LEFT JOIN traces t ON o.trace_id = t.id AND o.project_id = t.project_id" : ""}
ARRAY JOIN
mapKeys(usage_details) AS usage_key,
mapValues(usage_details) AS usage
WHERE project_id = {projectId: String}
AND ${appliedFilter.query}
${environmentFilter.query ? `AND ${environmentFilter.query}` : ""}
${timeFilter ? `AND t.timestamp >= {traceTimestamp: DateTime64(3)} - ${OBSERVATIONS_TO_TRACE_INTERVAL}` : ""}
GROUP BY
start_time,
usage_key
)
GROUP BY
start_time
${orderByQuery}
`;
const result = await queryClickhouse<{
start_time: string;
usages: Array<[string, number | null]>;
}>({
query,
params: {
projectId,
...appliedFilter.params,
...environmentFilter.params,
...orderByParams,
...(timeFilter
? { traceTimestamp: convertDateToClickhouseDateTime(timeFilter.value) }
: {}),
},
tags: {
feature: "dashboard",
type: "observationUsageByTime",
kind: "analytic",
projectId,
},
});
const types = result.flatMap((row) => {
return row.usages.map((usage) => usage[0]);
});
const uniqueTypes = [...new Set(types)];
return result.flatMap((row) => {
const intervalStart = parseClickhouseUTCDateTimeFormat(row.start_time);
return uniqueTypes.map((type) => ({
intervalStart: intervalStart,
key: type,
sum: row.usages.find((usage) => usage[0] === type)?.[1]
? Number(row.usages.find((usage) => usage[0] === type)?.[1])
: 0,
}));
});
};
export const getDistinctModels = async (
projectId: string,
filter: FilterState,
) => {
const { envFilter, remainingFilters } =
extractEnvironmentFilterFromFilters(filter);
const environmentFilter = new FilterList(
convertEnvFilterToClickhouseFilter(envFilter),
).apply();
const chFilter = new FilterList(
createFilterFromFilterState(filter, dashboardColumnDefinitions),
createFilterFromFilterState(remainingFilters, dashboardColumnDefinitions),
);
const appliedFilter = chFilter.apply();
@@ -285,6 +559,7 @@ export const getDistinctModels = async (
${tracesFilter ? "LEFT JOIN traces t ON o.trace_id = t.id AND o.project_id = t.project_id" : ""}
WHERE project_id = {projectId: String}
AND ${appliedFilter.query}
${environmentFilter.query ? `AND ${environmentFilter.query}` : ""}
${timeFilter ? `AND t.timestamp >= {traceTimestamp: DateTime64(3)} - ${OBSERVATIONS_TO_TRACE_INTERVAL}` : ""}
GROUP BY provided_model_name
ORDER BY count(*) DESC
@@ -296,10 +571,17 @@ export const getDistinctModels = async (
params: {
projectId,
...appliedFilter.params,
...environmentFilter.params,
...(timeFilter
? { traceTimestamp: convertDateToClickhouseDateTime(timeFilter.value) }
: {}),
},
tags: {
feature: "dashboard",
type: "distinctModels",
kind: "analytic",
projectId,
},
});
return result;
@@ -309,8 +591,13 @@ export const getScoresAggregateOverTime = async (
projectId: string,
filter: FilterState,
) => {
const { envFilter, remainingFilters } =
extractEnvironmentFilterFromFilters(filter);
const environmentFilter = new FilterList(
convertEnvFilterToClickhouseFilter(envFilter),
).apply();
const chFilter = new FilterList(
createFilterFromFilterState(filter, dashboardColumnDefinitions),
createFilterFromFilterState(remainingFilters, dashboardColumnDefinitions),
);
const appliedFilter = chFilter.apply();
@@ -333,6 +620,7 @@ export const getScoresAggregateOverTime = async (
FROM scores FINAL
${traceFilter ? "JOIN traces t ON scores.trace_id = t.id AND scores.project_id = t.project_id" : ""}
WHERE project_id = {projectId: String}
${environmentFilter.query ? `AND ${environmentFilter.query}` : ""}
AND ${appliedFilter.query}
AND data_type IN ('NUMERIC', 'BOOLEAN')
GROUP BY
@@ -354,8 +642,15 @@ export const getScoresAggregateOverTime = async (
params: {
projectId,
...appliedFilter.params,
...environmentFilter.params,
...orderByParams,
},
tags: {
feature: "dashboard",
type: "scoresAggregateOverTime",
kind: "analytic",
projectId,
},
});
return result.map((row) => ({
@@ -371,8 +666,13 @@ export const getModelUsageByUser = async (
projectId: string,
filter: FilterState,
) => {
const { envFilter, remainingFilters } =
extractEnvironmentFilterFromFilters(filter);
const environmentFilter = new FilterList(
convertEnvFilterToClickhouseFilter(envFilter),
).apply();
const chFilter = new FilterList(
createFilterFromFilterState(filter, dashboardColumnDefinitions),
createFilterFromFilterState(remainingFilters, dashboardColumnDefinitions),
);
const appliedFilter = chFilter.apply();
@@ -395,6 +695,7 @@ export const getModelUsageByUser = async (
WHERE project_id = {projectId: String}
AND t.user_id IS NOT NULL
AND ${appliedFilter.query}
${environmentFilter.query ? `AND ${environmentFilter.query}` : ""}
${timeFilter ? `AND t.timestamp >= {traceTimestamp: DateTime64(3)} - ${OBSERVATIONS_TO_TRACE_INTERVAL}` : ""}
GROUP BY user_id
ORDER BY sum_cost_details DESC
@@ -409,10 +710,17 @@ export const getModelUsageByUser = async (
params: {
projectId,
...appliedFilter.params,
...environmentFilter.params,
...(timeFilter
? { traceTimestamp: convertDateToClickhouseDateTime(timeFilter.value) }
: {}),
},
tags: {
feature: "dashboard",
type: "modelUsageByUser",
kind: "analytic",
projectId,
},
});
return result.map((row) => ({
@@ -426,8 +734,13 @@ export const getObservationLatencies = async (
projectId: string,
filter: FilterState,
) => {
const { envFilter, remainingFilters } =
extractEnvironmentFilterFromFilters(filter);
const environmentFilter = new FilterList(
convertEnvFilterToClickhouseFilter(envFilter),
).apply();
const chFilter = new FilterList(
createFilterFromFilterState(filter, dashboardColumnDefinitions),
createFilterFromFilterState(remainingFilters, dashboardColumnDefinitions),
);
const appliedFilter = chFilter.apply();
@@ -441,13 +754,24 @@ export const getObservationLatencies = async (
${chFilter.find((f) => f.clickhouseTable === "traces") ? "LEFT JOIN traces t ON o.trace_id = t.id AND o.project_id = t.project_id" : ""}
WHERE project_id = {projectId: String}
AND ${appliedFilter.query}
${environmentFilter.query ? `AND ${environmentFilter.query}` : ""}
GROUP BY name
ORDER BY quantiles[2] DESC
`;
const result = await queryClickhouse<{ quantiles: string[]; name: string }>({
query,
params: { projectId, ...appliedFilter.params },
params: {
projectId,
...appliedFilter.params,
...environmentFilter.params,
},
tags: {
feature: "dashboard",
type: "observationLatencies",
kind: "analytic",
projectId,
},
});
return result.map((row) => ({
@@ -463,8 +787,13 @@ export const getTracesLatencies = async (
projectId: string,
filter: FilterState,
) => {
const { envFilter, remainingFilters } =
extractEnvironmentFilterFromFilters(filter);
const environmentFilter = new FilterList(
convertEnvFilterToClickhouseFilter(envFilter),
).apply();
const chFilter = new FilterList(
createFilterFromFilterState(filter, dashboardColumnDefinitions),
createFilterFromFilterState(remainingFilters, dashboardColumnDefinitions),
);
const appliedFilter = chFilter.apply();
@@ -488,6 +817,7 @@ export const getTracesLatencies = async (
ON o.trace_id = t.id AND o.project_id = t.project_id
WHERE project_id = {projectId: String}
AND ${appliedFilter.query}
${environmentFilter.query ? `AND ${environmentFilter.query}` : ""}
${timestampFilter ? `AND o.start_time > {dateTimeFilterObservations: DateTime64(3)} - ${TRACE_TO_OBSERVATIONS_INTERVAL}` : ""}
GROUP BY o.project_id, o.trace_id, t.name
)
@@ -505,10 +835,17 @@ export const getTracesLatencies = async (
params: {
projectId,
...appliedFilter.params,
...environmentFilter.params,
...(timestampFilter
? { dateTimeFilterObservations: timestampFilter.value }
: {}),
},
tags: {
feature: "dashboard",
type: "tracesLatencies",
kind: "analytic",
projectId,
},
});
return result.map((row) => ({
@@ -524,8 +861,13 @@ export const getModelLatenciesOverTime = async (
projectId: string,
filter: FilterState,
) => {
const { envFilter, remainingFilters } =
extractEnvironmentFilterFromFilters(filter);
const environmentFilter = new FilterList(
convertEnvFilterToClickhouseFilter(envFilter),
).apply();
const chFilter = new FilterList(
createFilterFromFilterState(filter, dashboardColumnDefinitions),
createFilterFromFilterState(remainingFilters, dashboardColumnDefinitions),
);
const appliedFilter = chFilter.apply();
@@ -546,6 +888,7 @@ export const getModelLatenciesOverTime = async (
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}
${environmentFilter.query ? `AND ${environmentFilter.query}` : ""}
AND ${appliedFilter.query}
GROUP BY provided_model_name, start_time_bucket
${orderByQuery};
@@ -557,7 +900,18 @@ export const getModelLatenciesOverTime = async (
quantiles: string[];
}>({
query,
params: { projectId, ...appliedFilter.params, ...orderByParams },
params: {
projectId,
...appliedFilter.params,
...environmentFilter.params,
...orderByParams,
},
tags: {
feature: "dashboard",
type: "modelLatenciesOverTime",
kind: "analytic",
projectId,
},
});
return result.map((row) => ({
@@ -575,8 +929,13 @@ export const getNumericScoreTimeSeries = async (
projectId: string,
filter: FilterState,
) => {
const { envFilter, remainingFilters } =
extractEnvironmentFilterFromFilters(filter);
const environmentFilter = new FilterList(
convertEnvFilterToClickhouseFilter(envFilter),
).apply();
const chFilter = new FilterList(
createFilterFromFilterState(filter, dashboardColumnDefinitions),
createFilterFromFilterState(remainingFilters, dashboardColumnDefinitions),
);
const chFilterRes = chFilter.apply();
@@ -595,6 +954,7 @@ export const getNumericScoreTimeSeries = async (
FROM scores s final
${traceFilter ? "JOIN traces t ON s.trace_id = t.id AND s.project_id = t.project_id" : ""}
WHERE s.project_id = {projectId: String}
${environmentFilter.query ? `AND ${environmentFilter.query}` : ""}
${chFilterRes?.query ? `AND ${chFilterRes.query}` : ""}
GROUP BY score_name, score_timestamp
${orderByQuery}
@@ -609,8 +969,15 @@ export const getNumericScoreTimeSeries = async (
params: {
projectId,
...(chFilterRes ? chFilterRes.params : {}),
...environmentFilter.params,
...orderByParams,
},
tags: {
feature: "dashboard",
type: "numericScoreTimeSeries",
kind: "analytic",
projectId,
},
});
return result.map((row) => ({
@@ -624,8 +991,13 @@ export const getCategoricalScoreTimeSeries = async (
projectId: string,
filter: FilterState,
) => {
const { envFilter, remainingFilters } =
extractEnvironmentFilterFromFilters(filter);
const environmentFilter = new FilterList(
convertEnvFilterToClickhouseFilter(envFilter),
).apply();
const chFilter = new FilterList(
createFilterFromFilterState(filter, dashboardColumnDefinitions),
createFilterFromFilterState(remainingFilters, dashboardColumnDefinitions),
);
const chFilterRes = chFilter.apply();
@@ -647,6 +1019,7 @@ export const getCategoricalScoreTimeSeries = async (
FROM scores s final
${traceFilter ? "JOIN traces t ON s.trace_id = t.id AND s.project_id = t.project_id" : ""}
WHERE s.project_id = {projectId: String}
${environmentFilter.query ? `AND ${environmentFilter.query}` : ""}
${chFilterRes?.query ? `AND ${chFilterRes.query}` : ""}
GROUP BY score_name, score_data_type, score_source, score_value ${bucketSizeInSeconds ? ", score_timestamp" : ""}
${orderByQuery}
@@ -664,8 +1037,15 @@ export const getCategoricalScoreTimeSeries = async (
params: {
projectId,
...(chFilterRes ? chFilterRes.params : {}),
...environmentFilter.params,
...orderByParams,
},
tags: {
feature: "dashboard",
type: "categoricalScoreTimeSeries",
kind: "analytic",
projectId,
},
});
return result.map((row) => ({
@@ -684,8 +1064,13 @@ export const getObservationsStatusTimeSeries = async (
projectId: string,
filter: FilterState,
) => {
const { envFilter, remainingFilters } =
extractEnvironmentFilterFromFilters(filter);
const environmentFilter = new FilterList(
convertEnvFilterToClickhouseFilter(envFilter),
).apply();
const chFilter = new FilterList(
createFilterFromFilterState(filter, dashboardColumnDefinitions),
createFilterFromFilterState(remainingFilters, dashboardColumnDefinitions),
);
const chFilterRes = chFilter.apply();
@@ -704,6 +1089,7 @@ export const getObservationsStatusTimeSeries = async (
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}
${environmentFilter.query ? `AND ${environmentFilter.query}` : ""}
AND o.level IS NOT NULL
AND ${chFilterRes?.query}
GROUP BY level ${bucketSizeInSeconds ? ", start_time_bucket" : ""}
@@ -719,8 +1105,15 @@ export const getObservationsStatusTimeSeries = async (
params: {
projectId,
...(chFilterRes ? chFilterRes.params : {}),
...environmentFilter.params,
...orderByParams,
},
tags: {
feature: "dashboard",
type: "observationStatusTimeSeries",
kind: "analytic",
projectId,
},
});
return result.map((row) => ({
@@ -36,6 +36,7 @@ export const observationRecordBaseSchema = z.object({
project_id: z.string(),
type: z.string(),
parent_observation_id: z.string().nullish(),
environment: z.string().default("default"),
name: z.string().nullish(),
metadata: z.record(z.string()),
level: z.string().nullish(),
@@ -52,9 +53,6 @@ export const observationRecordBaseSchema = z.object({
prompt_version: z.number().nullish(),
is_deleted: z.number(),
});
export type ObservationRecordBaseType = z.infer<
typeof observationRecordBaseSchema
>;
export const observationRecordReadSchema = observationRecordBaseSchema.extend({
created_at: clickhouseStringDateSchema,
@@ -98,6 +96,7 @@ export const traceRecordBaseSchema = z.object({
release: z.string().nullish(),
version: z.string().nullish(),
project_id: z.string(),
environment: z.string().default("default"),
public: z.boolean(),
bookmarked: z.boolean(),
tags: z.array(z.string()),
@@ -106,7 +105,6 @@ export const traceRecordBaseSchema = z.object({
session_id: z.string().nullish(),
is_deleted: z.number(),
});
export type TraceRecordBaseType = z.infer<typeof traceRecordBaseSchema>;
export const traceRecordReadSchema = traceRecordBaseSchema.extend({
timestamp: clickhouseStringDateSchema,
@@ -129,6 +127,7 @@ export const scoreRecordBaseSchema = z.object({
project_id: z.string(),
trace_id: z.string(),
observation_id: z.string().nullish(),
environment: z.string().default("default"),
name: z.string(),
value: z.number().nullish(),
source: z.string(),
@@ -140,7 +139,6 @@ export const scoreRecordBaseSchema = z.object({
queue_id: z.string().nullish(),
is_deleted: z.number(),
});
export type ScoreRecordBaseType = z.infer<typeof scoreRecordBaseSchema>;
export const scoreRecordReadSchema = scoreRecordBaseSchema.extend({
created_at: clickhouseStringDateSchema,
@@ -246,6 +244,7 @@ export const convertPostgresTraceToInsert = (
: Array.isArray(trace.metadata)
? { metadata: trace.metadata }
: trace.metadata,
environment: trace.environment,
release: trace.release,
version: trace.version,
project_id: trace.project_id,
@@ -282,6 +281,7 @@ export const convertPostgresObservationToInsert = (
project_id: observation.project_id,
type: observation.type,
parent_observation_id: observation.parent_observation_id,
environment: observation.environment,
start_time: observation.start_time?.getTime(),
end_time: observation.end_time?.getTime(),
name: observation.name,
@@ -345,6 +345,7 @@ export const convertPostgresScoreToInsert = (
project_id: score.project_id,
trace_id: score.trace_id,
observation_id: score.observation_id,
environment: score.environment,
name: score.name,
value: score.value,
source: score.source,
@@ -0,0 +1,36 @@
import { queryClickhouse } from "./clickhouse";
export type EnvironmentFilterProps = {
projectId: string;
};
export const getEnvironmentsForProject = async (
props: EnvironmentFilterProps,
): Promise<{ environment: string }[]> => {
const { projectId } = props;
const query = `
SELECT environments
FROM project_environments FINAL
WHERE project_id = {projectId: String}
`;
const results = await queryClickhouse<{
environments: string[];
}>({
query,
params: { projectId },
tags: {
feature: "tracing",
type: "environment",
kind: "byId",
projectId,
},
});
const environments = results.length > 0 ? results[0].environments : [];
environments.push("default");
return Array.from(new Set(environments)).map((environment) => ({
environment,
}));
};
@@ -26,6 +26,11 @@ export const getEventLogByProjectAndEntityId = async (
entityType,
entityId,
},
tags: {
feature: "eventLog",
kind: "byID",
projectId,
},
});
};
@@ -43,6 +48,11 @@ export const getEventLogByProjectId = (
params: {
projectId,
},
tags: {
feature: "eventLog",
kind: "list",
projectId,
},
});
};
@@ -63,6 +73,140 @@ export const getEventLogByProjectIdBeforeDate = (
projectId,
beforeDate: convertDateToClickhouseDateTime(beforeDate),
},
tags: {
feature: "eventLog",
kind: "list",
projectId,
},
});
};
export const getEventLogByProjectIdAndEntityIds = (
projectId: string,
entityType: "observation" | "trace" | "score",
entityIds: string[],
): AsyncGenerator<EventLogRecordReadType> => {
const query = `
select *
from event_log
where project_id = {projectId: String}
and entity_type = {entityType: String}
and entity_id in ({entityIds: Array(String)})
`;
return queryClickhouseStream<EventLogRecordReadType>({
query,
params: {
projectId,
entityType,
entityIds,
},
clickhouseConfigs: {
request_timeout: 120_000, // 2 minutes
},
tags: {
feature: "eventLog",
kind: "list",
projectId,
},
});
};
export const getEventLogByProjectIdAndTraceIds = (
projectId: string,
traceIds: string[],
): AsyncGenerator<EventLogRecordReadType> => {
const query = `
with filtered_traces as (
select distinct
id as entity_id,
project_id as project_id,
'trace' as entity_type
from traces
where project_id = {projectId: String}
and id in ({traceIds: Array(String)})
), filtered_observations as (
select distinct
id as entity_id,
project_id as project_id,
'observation' as entity_type
from observations
where project_id = {projectId: String}
and trace_id in ({traceIds: Array(String)})
), filtered_scores as (
select distinct
id as entity_id,
project_id as project_id,
'score' as entity_type
from scores
where project_id = {projectId: String}
and trace_id in ({traceIds: Array(String)})
), filtered_events as (
select *
from filtered_traces
union all
select *
from filtered_observations
union all
select *
from filtered_scores
)
-- We use a semi join because we only use the 'filtered_events' as a filter.
-- There is no need to build the cartesian product (i.e. the combination) between the event log and the events.
select el.*
from event_log el
left semi join filtered_events fe
on el.project_id = fe.project_id and el.entity_id = fe.entity_id and el.entity_type = fe.entity_type
where el.project_id = {projectId: String}
`;
return queryClickhouseStream<EventLogRecordReadType>({
query,
params: {
projectId,
traceIds,
},
clickhouseConfigs: {
request_timeout: 120_000, // 2 minutes
},
tags: {
feature: "eventLog",
kind: "list",
projectId,
},
});
};
/**
* Deletes event log records by projectId and the _eventLog_.id
* @param projectId - Project ID
* @param ids - ID record of the event log table to be deleted
*/
export const deleteEventLogByProjectIdAndIds = async (
projectId: string,
ids: string[],
): Promise<void> => {
const query = `
delete from event_log
where project_id = {projectId: String}
and id in ({ids: Array(String)});
`;
await commandClickhouse({
query,
params: {
projectId,
ids,
},
clickhouseConfigs: {
request_timeout: 300_000, // 5 minutes
},
tags: {
feature: "eventLog",
kind: "delete",
projectId,
},
});
};
@@ -81,6 +225,11 @@ export const deleteEventLogByProjectId = async (
clickhouseConfigs: {
request_timeout: 120_000, // 2 minutes
},
tags: {
feature: "eventLog",
kind: "delete",
projectId,
},
});
};
@@ -102,5 +251,10 @@ export const deleteEventLogByProjectIdBeforeDate = async (
clickhouseConfigs: {
request_timeout: 120_000, // 2 minutes
},
tags: {
feature: "eventLog",
kind: "delete",
projectId,
},
});
};

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