Compare commits

...
193 Commits
Author SHA1 Message Date
Nimar 66accb563c chore: release v3.86.0 2025-07-24 16:30:43 +02:00
NimarandGitHub 9f2e8906d0 fix(trace-table): expanded state and make it fast! (#8099)
* preserve expanded state

* lazy load table for better performance

* fix broken commit

* pending: verbose state tracking working again

* fix states

* fix lint

* cleanup

* cleaner even

* fix build
2025-07-24 14:24:47 +00:00
c5d61b7ed2 chore: Remove add window button and its imports (#8097)
* Remove Add Window button from playground page

Co-authored-by: max <max@langfuse.com>

* feat(playground): remove Add Window button from header

- Remove Add Window button from playground page header
- Clean up unused imports (Plus icon, MULTI_WINDOW_CONFIG)
- Remove unused isAddWindowDisabled variable
- Preserve addWindow function for copy functionality
- Fix linting warnings for unused imports

Users can still add windows via the copy button on individual windows.

---------

Co-authored-by: Cursor Agent <cursoragent@cursor.com>
2025-07-24 13:05:27 +00:00
Max DeichmannandGitHub f95dd872e6 chore: fix output box for playground (#8096) 2025-07-24 12:38:11 +00:00
marliessophieandGitHub 04339741a9 chore(dataset-run-items): adjust converter for background migration (#8075)
* chore: reorder converter

* chore: treat numbers and booleans as strings

* chore: adjust metadata conversion

* Revert "chore: adjust metadata conversion"

This reverts commit d5ca3a5cb1b6e5972e5a321ab4884c589b938087.
2025-07-24 11:38:48 +00:00
marliessophieandGitHub 9c85e46996 feat(table): persist column sizes in local storage (#7816)
* feat(table): persist column sizing in localStorage

* chore: apply to all tables

* chore: push

* chore: add string
2025-07-24 11:37:16 +00:00
Steffen SchmitzandGitHub 4a7236451f chore: bump clickhouse client to 1.12.0 (#8092) 2025-07-24 11:20:21 +00:00
Steffen SchmitzandGitHub 5cb5204181 chore: migrate remaining trace reads to AMT (#8056)
* chore: migrate remaining trace reads to AMT

* chore: revert box ticks

* chore: revert streamed exports

* chore: confirm trace count by project in creation interval

* chore: confirm trace count since created

* chore: confirm getTracesByIdsForAnyProject

* chore: session table simplification

* chore: refactor user metrics

* chore: update
2025-07-24 10:49:44 +00:00
NimarandGitHub c19066afa0 fix(playground): api key link should show llm connection (#8080) 2025-07-24 09:43:03 +00:00
Max DeichmannandGitHub bbb9dc285d chore: remove delay for otel (#8085) 2025-07-24 11:16:59 +02:00
Steffen SchmitzandGitHub b750acba06 feat(admin-api): add okta role assignment capabilities (#8084)
* feat(admin-api): add okta role assignment capabilities

* chore: lint
2025-07-24 08:21:36 +00:00
Max DeichmannandGitHub bb06e079eb chore: upgrade prism (#8083)
* chore: upgrade prismjs

* fix
2025-07-24 07:49:50 +00:00
Max DeichmannandGitHub 5f711780e0 chore: upgrade prismjs (#8077) 2025-07-23 17:20:53 +00:00
Max DeichmannandGitHub 0f58ea1ebf chore: remove trivy (#8076) 2025-07-23 17:11:59 +00:00
Nimar ec70ca3951 chore: release v3.85.2 2025-07-23 18:20:34 +02:00
NimarandGitHub 142d3a1612 fix(trace-view): render new lines in json table view (#8072) 2025-07-23 16:09:32 +00:00
Max DeichmannandGitHub 1dee7092b3 chore: configure posthog SDK (#8067)
* chore: configure posthog SDK

* chore: add score values to scores

* update posthog integration

* update posthog integration
2025-07-23 15:27:06 +00:00
Steffen SchmitzandGitHub 83b64f5e77 perf: extend model cache duration and invalidate on model changes (#8053) 2025-07-23 16:39:04 +02:00
Steffen SchmitzandGitHub 48b05247c8 fix(admin-api): handle Okta provisioning for POST and PATCH (#8064) 2025-07-23 14:15:10 +00:00
Max DeichmannandGitHub 9732262466 chore: fix node types discrepancies across packages (#8060)
* chore: fix node types discrepancies across packages

* chore: fix node types discrepancies across packages
2025-07-23 12:23:43 +00:00
Max DeichmannandGitHub e51e2df2dc chore: fix snyk again (#8050)
* fixes

* fixes

* fixes

* fixes

* fixes

* fixes

* fixes

* fixes

* fixes
2025-07-23 12:04:01 +00:00
Nimar 74d1dfe5ac chore: release v3.85.1 2025-07-23 13:55:34 +02:00
NimarandGitHub 5633a8867e fix(datasets-table): don't table render json here (#8058) 2025-07-23 11:52:50 +00:00
6a8bdae22e feat: multi window playground (#7661)
* docs: add technical specs for multi window/prompt playground

* docs: add requirements and tech summary for sharing

* feat: add multi-window playground architecture and state isolation

* feat: make playground UI multi-window capable

* fix: multi window playground caching

* fix: jump to playground button adds windows to playground using stable id

* fix: avoid race condition in writing to cache before navigating to playground

* feat: add collapsible, more compact sections to non-model config

* feat: add compact version of model picker/params component

* feat: add ability to copy window states to multi playground

* fix: solve header crowding for thin windows in playground

* feat: make entire playground config section collapsible

* feat: change SaveToPrompt button to align with other window buttons

* feat: switch buttons in page header to icon-only when too small

* fix: adjust button labels to fit button size

* fix: update the execution window status correctly

* docs: complete remaining task items

* refactor: simplify playground state management hooks

* fix: make the model configuration section prettier

* docs: remove agent work files

* refactor: call register functions on the registry directly

* chore: refactored some components and compacted the design even more

* chore: move add message buttons

* chore: start align model param settings popover when compact

* chore: small UI spacing improvements

* fix: scrolling bug

* fix: handle config headers when window gets small

* chore: rename button hover

* feat: move CMD+Enter to execute all button

* feat: give user the choice to jump to fresh/existing playground

* push

* chore: fix

* chore: fix

* push

* push

* fixes

---------

Co-authored-by: Max Deichmann <m.deichmann@tum.de>
2025-07-23 10:01:43 +00:00
Steffen SchmitzandGitHub dbf994f9bb perf: replace prisma upsert with raw query (#8036)
* perf: replace prisma upsert with raw query

* chore: drop prsima import

* chore: lint
2025-07-23 10:00:06 +00:00
Steffen SchmitzandGitHub 4d5c288d1e perf: initialize clickhouse skip read map on startup (#8039)
* perf: initialize clickhouse skip read map on startup

* chore: linting

* chore: patch skip cache init
2025-07-23 09:29:44 +00:00
Hassieb PakzadandGitHub cf119fb9cc fix(otel): parse trace name if as_root is true (#8008)
* fix(otel): parse trace name if as_root is true

* push

* push

* push
2025-07-23 09:13:54 +00:00
Hassieb PakzadandGitHub 5af8c4e400 fix(otel): parse public attribute for traces (#8049) 2025-07-23 10:18:31 +02:00
Max DeichmannandGitHub 835a19bbd2 fix: fix snyk uploads (#8044) 2025-07-22 20:48:38 +00:00
Max DeichmannandGitHub ae91a98ad9 fix: use drop down for prompt full text search (#8043)
* fix: use drop down for prompt full text search

* fix: use drop down for prompt full text search

* fix: use drop down for prompt full text search

* fix: use drop down for prompt full text search

* fix: use drop down for prompt full text search
2025-07-22 20:33:25 +00:00
NimarandGitHub e38cc24205 chore: add default claude settings (#8042) 2025-07-22 19:12:19 +00:00
marliessophieandGitHub 7ab45ec685 feat(dataset-run-items): migrate from postgres to clickhouse; write to CH API-only (#7912)
* feat(migrations): add dataset_run_items table in clickhouse

* feat(dataset): add environment variables and utility for pg/ch execution

* chore: re-write `runitemsByRunIdOrItemId` to support both clickhouse and postgres execution

* chore: use `runItemsByRunId` or `runItemsByItemId` rather than  `runitemsByRunIdOrItemId`

* chore: rename env variables

* chore: reflect dri as ch data is seeder

* fixup: dataset run item types and converters

* chore: background migration

* chore: rewrite dataset-router routes

* chore(dataset): dual-write strategy for dataset run items to support ClickHouse and PostgreSQL

* chore: add utilities and converters

* chore: rewrite GET api/public/dataset-run-items

* feat(dataset): implement dataset run items deletion queue and processing

* chore: rewrite DELETE api/public/datasets/{datasetName}/runs/{runName}

* chore: rewrite GET api/public/datasets/{datasetName}/runs/{runName}

* fixup: rewrite GET api/public/datasets/{datasetName}/runs/{runName}

* chore(schema): add error to DRI schema

* fixup(ingestion): support dataset run items

* fixup: rewrite POST /api/public/dataset-run-items​

* fixup: rewrite POST /api/public/dataset-run-items​ --amend

* fixup(ingestion): support dataset run items --amend

* fixup(experiments): preliminary implementation, for illustrative purposes only

* fixup(schema): indexes

* fixup(schema): remove indexes, add `output`

* chore: support DRI ingestion

* chore: eslint, writes related

* chore: rewrite experiment service

* chore: rm file

* chore: cascade delete dataset run items

* chore: add clustered migrations

* chore: update background migration

* chore: handle experiment job errors

* chore: remove CH read implementation

* chore: remove md file

* chore: remove CH read implementation II

* chore: fix typing

* chore: fix migration types

* fix: lint/imports

* fix: import errors

* chore: imports and comments

* chore: remove only from test

* fix: validateDatasetRunAndFetch

* fix: validateDatasetRunAndFetch id

* fixup: attempt fix of test

* fix: test

* chore: test validate logging

* chore: adjust logs

* chore: revert cascade delete

* Revert "feat(dataset): implement dataset run items deletion queue and processing"

This reverts commit 9f725f053386ded41e3617efaacee128d5ac023f.

* chore: delete imports

* revert: experiment service changes

* chore: adjust comment

* rename: env variables to incl experiment

* chore: remove unused variable eslint disables from datasetExecution and types files

* refactor: remove enrichedDatasetRunItem function and integrate its logic directly into IngestionService

* chore: remove TODO comment regarding dataset run item authorization

* refactor: replace Date constructor with parseClickhouseUTCDateTimeFormat for date parsing in dataset run item conversion

* chore: remove deprecated flag for runitemsByRunIdOrItemId method

* refactor: remove getDatasetRunItemsByRunId function to streamline dataset run item retrieval

* refactor: add back run properties in dataset runs mapping

* chore: revert changes to delete-dataset-run API

* refactor: remove validateDatasetItemAndFetch function and replace its usage with direct Prisma query in IngestionService

* refactor: delete validateCreateDatasetRunItemBodyAndFetch function and replace its logic with direct Prisma queries in dataset-run-items API

* refactor: move createOrFetchDatasetRun function to a new dataset-runs API file and maintain unique constraint handling

* refactor: remove validateDatasetRunAndFetch function and replace its usage with direct Prisma queries in dataset-run-items and IngestionService

* chore: nits

* refactor: streamline dataset run item creation by consolidating validation and execution logic

* chore: fix imports

* fix: types

* fix: simplify ingestion schema

* refactor: enhance ingestion schema creation to support public and internal environments

* chore: extend immutable keys list DRI

* chore: use same id for clickhouse and postgres

* chore: remove background migration trigger

* chore: move dataset run items migration into readme

* chore: remove creation of DRI in seeder

* chore: lint
2025-07-22 16:20:47 +00:00
Nimar b101bf48d3 chore: release v3.85.0 2025-07-22 16:47:56 +02:00
NimarandGitHub 4c4f0ff3c0 feat(tracing): pretty display json as collapsible table (#7938)
* feat(tracing): pretty display json as collapsible table

* show empty list and unwrap single item containing objects

* enable toggle for code or pretty view

* clean up

* fix type casting

* some more fixes

* expand row with click anywhere

* fix alignment

* remove dead code

* cleanup

* make it more clean

* don't unwrap initial items

* collapse expand all

* fixing collapse button

* don't render for chatml

* better markdown check

* reduce col width

* make table borderless

* display null / empty string for empty value

* smaller text

* make table more compact

* make preview items grey & italic

* linebreak path column

* table headers inherit color

* cleanup

* external collapsed state; also move button into code view

* remove code view switch

* show empty dicts also as empty

* always show objects as tables

* extract markdown into helper function

* review fixes

* update
2025-07-22 14:41:11 +00:00
Steffen SchmitzandGitHub a42ca96225 chore: expose email_from_address and smtp_connection_url in docker-compose (#8033)
chore: expose email_from_address and smtp_connection_url in docker-compose.yml
2025-07-22 13:26:03 +00:00
marliessophieandGitHub f63420827b feat(prompts/playground): allow arbitrary message sorting in messages (#8031) 2025-07-22 12:41:39 +00:00
Steffen SchmitzandGitHub e6fd056ae5 chore: migrate analytic routes to new AMTs (#8029)
* chore: migrate analytic routes to new AMTs

* chore: reduce new shared code

* chore: cleanup

* chore: cleanup
2025-07-22 12:39:48 +00:00
Steffen SchmitzandGitHub 4708fe4f47 chore: migrate additional session routes to new AMTs (#8027)
* chore: migrate additional session routes to new AMTs

* chore: remove unused function

* chore: remove unused code

* chore: remove outdated row in readme

* chore: update assertions
2025-07-22 12:24:57 +00:00
marliessophieandGitHub 362246b616 chore(datasets): update button label from "Rename" to "Edit" in DatasetActionButton component (#8030) 2025-07-22 12:21:45 +00:00
Steffen SchmitzandGitHub b3de5fd71a chore: migrate traces table to AMTs (#7853)
* chore: migrate traces table to AMTs

* chore: fix linting errors

* chore: ensure inplace update

* chore: add updated query

* chore: use final on query
2025-07-21 14:12:25 +00:00
Hassieb PakzadandGitHub ad236ecc99 fix(model-params): increase max tokens limit (#8005) 2025-07-21 15:12:37 +02:00
NimarandGitHub beb2063dcb fix: add select all to multi select dropdown (#8004) 2025-07-21 12:53:09 +00:00
Hassieb PakzadandGitHub 4de10298dc fix(otel): parse only trace io on trace update (#8003) 2025-07-21 12:11:04 +00:00
Danushka FernandoandGitHub f9924975fa feat: expose a key prefix option for Redis (#7958) 2025-07-21 14:01:14 +02:00
Steffen SchmitzandGitHub 98774cbd61 fix(sessions): infer session env from trace (#7996) 2025-07-21 10:44:17 +00:00
Steffen SchmitzandGitHub 80361ef53f fix(sessions): align duration unit with UI (#7998)
* fix(sessions): align duration unit with UI

* chore: update tests
2025-07-21 09:40:17 +00:00
Steffen SchmitzandGitHub 8350507434 chore: add missing blob-storage export entitlement (#7990) 2025-07-21 08:09:22 +00:00
marliessophieandGitHub 0a976edb70 fix(prompts): commitMessage error handling in NewPromptForm (#7984) 2025-07-20 16:13:39 +00:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>Max Deichmann
6a67c93010 chore(deps): bump exponential-backoff from 3.1.1 to 3.1.2 (#7412)
Bumps [exponential-backoff](https://github.com/coveooss/exponential-backoff) from 3.1.1 to 3.1.2.
- [Commits](https://github.com/coveooss/exponential-backoff/compare/v3.1.1...v3.1.2)

---
updated-dependencies:
- dependency-name: exponential-backoff
  dependency-version: 3.1.2
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Max Deichmann <m.deichmann@tum.de>
2025-07-20 09:33:39 +00:00
Max DeichmannandGitHub e51ee05d9c chore: upgrade express 5 (#7978)
* chore: upgrade express 5

* chore: upgrade express 5
2025-07-20 09:13:22 +00:00
Max Deichmann 565c561715 chore: release v3.84.0 2025-07-20 09:45:00 +02:00
Max DeichmannandGitHub d1338ad5e1 chore: move dev dependencies to dev dependencies web container (#7977)
* fix

* fix

* fix

* fix

* chore: move dev dependencies to dev dependencies web container
2025-07-19 19:15:48 +00:00
Max DeichmannandGitHub 52d4fe37de chore: remove unused tsx (#7976)
* fix

* fix

* fix

* fix
2025-07-19 19:15:40 +00:00
Max DeichmannandGitHub 67ef7abb63 chore: full text search for prompts (#7973)
* fix

* fix

* fix
2025-07-19 18:55:58 +00:00
Max DeichmannandGitHub 727ae71a93 fix: fix codemirror upgrade (#7975)
fix
2025-07-19 17:57:49 +00:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
a8eabbc96d chore(deps): bump the radix-ui group with 23 updates (#5562)
Bumps the radix-ui group with 23 updates:

| Package | From | To |
| --- | --- | --- |
| [@radix-ui/react-accordion](https://github.com/radix-ui/primitives) | `1.2.1` | `1.2.3` |
| [@radix-ui/react-alert-dialog](https://github.com/radix-ui/primitives) | `1.1.2` | `1.1.6` |
| [@radix-ui/react-avatar](https://github.com/radix-ui/primitives) | `1.1.1` | `1.1.3` |
| [@radix-ui/react-checkbox](https://github.com/radix-ui/primitives) | `1.1.2` | `1.1.4` |
| [@radix-ui/react-collapsible](https://github.com/radix-ui/primitives) | `1.1.1` | `1.1.3` |
| [@radix-ui/react-dialog](https://github.com/radix-ui/primitives) | `1.1.2` | `1.1.6` |
| [@radix-ui/react-dropdown-menu](https://github.com/radix-ui/primitives) | `2.1.2` | `2.1.6` |
| [@radix-ui/react-hover-card](https://github.com/radix-ui/primitives) | `1.1.2` | `1.1.6` |
| @radix-ui/react-icons | `1.3.0` | `1.3.2` |
| [@radix-ui/react-label](https://github.com/radix-ui/primitives) | `2.1.0` | `2.1.2` |
| [@radix-ui/react-popover](https://github.com/radix-ui/primitives) | `1.1.2` | `1.1.6` |
| [@radix-ui/react-progress](https://github.com/radix-ui/primitives) | `1.1.1` | `1.1.2` |
| [@radix-ui/react-radio-group](https://github.com/radix-ui/primitives) | `1.2.1` | `1.2.3` |
| [@radix-ui/react-scroll-area](https://github.com/radix-ui/primitives) | `1.2.0` | `1.2.3` |
| [@radix-ui/react-select](https://github.com/radix-ui/primitives) | `2.1.2` | `2.1.6` |
| [@radix-ui/react-separator](https://github.com/radix-ui/primitives) | `1.1.0` | `1.1.2` |
| [@radix-ui/react-slider](https://github.com/radix-ui/primitives) | `1.2.1` | `1.2.3` |
| [@radix-ui/react-slot](https://github.com/radix-ui/primitives) | `1.1.0` | `1.1.2` |
| [@radix-ui/react-switch](https://github.com/radix-ui/primitives) | `1.1.1` | `1.1.3` |
| [@radix-ui/react-tabs](https://github.com/radix-ui/primitives) | `1.1.1` | `1.1.3` |
| [@radix-ui/react-toggle](https://github.com/radix-ui/primitives) | `1.1.0` | `1.1.2` |
| [@radix-ui/react-toggle-group](https://github.com/radix-ui/primitives) | `1.1.0` | `1.1.2` |
| [@radix-ui/react-tooltip](https://github.com/radix-ui/primitives) | `1.1.3` | `1.1.8` |


Updates `@radix-ui/react-accordion` from 1.2.1 to 1.2.3
- [Changelog](https://github.com/radix-ui/primitives/blob/main/release-process.md)
- [Commits](https://github.com/radix-ui/primitives/commits)

Updates `@radix-ui/react-alert-dialog` from 1.1.2 to 1.1.6
- [Changelog](https://github.com/radix-ui/primitives/blob/main/release-process.md)
- [Commits](https://github.com/radix-ui/primitives/commits)

Updates `@radix-ui/react-avatar` from 1.1.1 to 1.1.3
- [Changelog](https://github.com/radix-ui/primitives/blob/main/release-process.md)
- [Commits](https://github.com/radix-ui/primitives/commits)

Updates `@radix-ui/react-checkbox` from 1.1.2 to 1.1.4
- [Changelog](https://github.com/radix-ui/primitives/blob/main/release-process.md)
- [Commits](https://github.com/radix-ui/primitives/commits)

Updates `@radix-ui/react-collapsible` from 1.1.1 to 1.1.3
- [Changelog](https://github.com/radix-ui/primitives/blob/main/release-process.md)
- [Commits](https://github.com/radix-ui/primitives/commits)

Updates `@radix-ui/react-dialog` from 1.1.2 to 1.1.6
- [Changelog](https://github.com/radix-ui/primitives/blob/main/release-process.md)
- [Commits](https://github.com/radix-ui/primitives/commits)

Updates `@radix-ui/react-dropdown-menu` from 2.1.2 to 2.1.6
- [Changelog](https://github.com/radix-ui/primitives/blob/main/release-process.md)
- [Commits](https://github.com/radix-ui/primitives/commits)

Updates `@radix-ui/react-hover-card` from 1.1.2 to 1.1.6
- [Changelog](https://github.com/radix-ui/primitives/blob/main/release-process.md)
- [Commits](https://github.com/radix-ui/primitives/commits)

Updates `@radix-ui/react-icons` from 1.3.0 to 1.3.2

Updates `@radix-ui/react-label` from 2.1.0 to 2.1.2
- [Changelog](https://github.com/radix-ui/primitives/blob/main/release-process.md)
- [Commits](https://github.com/radix-ui/primitives/commits)

Updates `@radix-ui/react-popover` from 1.1.2 to 1.1.6
- [Changelog](https://github.com/radix-ui/primitives/blob/main/release-process.md)
- [Commits](https://github.com/radix-ui/primitives/commits)

Updates `@radix-ui/react-progress` from 1.1.1 to 1.1.2
- [Changelog](https://github.com/radix-ui/primitives/blob/main/release-process.md)
- [Commits](https://github.com/radix-ui/primitives/commits)

Updates `@radix-ui/react-radio-group` from 1.2.1 to 1.2.3
- [Changelog](https://github.com/radix-ui/primitives/blob/main/release-process.md)
- [Commits](https://github.com/radix-ui/primitives/commits)

Updates `@radix-ui/react-scroll-area` from 1.2.0 to 1.2.3
- [Changelog](https://github.com/radix-ui/primitives/blob/main/release-process.md)
- [Commits](https://github.com/radix-ui/primitives/commits)

Updates `@radix-ui/react-select` from 2.1.2 to 2.1.6
- [Changelog](https://github.com/radix-ui/primitives/blob/main/release-process.md)
- [Commits](https://github.com/radix-ui/primitives/commits)

Updates `@radix-ui/react-separator` from 1.1.0 to 1.1.2
- [Changelog](https://github.com/radix-ui/primitives/blob/main/release-process.md)
- [Commits](https://github.com/radix-ui/primitives/commits)

Updates `@radix-ui/react-slider` from 1.2.1 to 1.2.3
- [Changelog](https://github.com/radix-ui/primitives/blob/main/release-process.md)
- [Commits](https://github.com/radix-ui/primitives/commits)

Updates `@radix-ui/react-slot` from 1.1.0 to 1.1.2
- [Changelog](https://github.com/radix-ui/primitives/blob/main/release-process.md)
- [Commits](https://github.com/radix-ui/primitives/commits)

Updates `@radix-ui/react-switch` from 1.1.1 to 1.1.3
- [Changelog](https://github.com/radix-ui/primitives/blob/main/release-process.md)
- [Commits](https://github.com/radix-ui/primitives/commits)

Updates `@radix-ui/react-tabs` from 1.1.1 to 1.1.3
- [Changelog](https://github.com/radix-ui/primitives/blob/main/release-process.md)
- [Commits](https://github.com/radix-ui/primitives/commits)

Updates `@radix-ui/react-toggle` from 1.1.0 to 1.1.2
- [Changelog](https://github.com/radix-ui/primitives/blob/main/release-process.md)
- [Commits](https://github.com/radix-ui/primitives/commits)

Updates `@radix-ui/react-toggle-group` from 1.1.0 to 1.1.2
- [Changelog](https://github.com/radix-ui/primitives/blob/main/release-process.md)
- [Commits](https://github.com/radix-ui/primitives/commits)

Updates `@radix-ui/react-tooltip` from 1.1.3 to 1.1.8
- [Changelog](https://github.com/radix-ui/primitives/blob/main/release-process.md)
- [Commits](https://github.com/radix-ui/primitives/commits)

---
updated-dependencies:
- dependency-name: "@radix-ui/react-accordion"
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: radix-ui
- dependency-name: "@radix-ui/react-alert-dialog"
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: radix-ui
- dependency-name: "@radix-ui/react-avatar"
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: radix-ui
- dependency-name: "@radix-ui/react-checkbox"
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: radix-ui
- dependency-name: "@radix-ui/react-collapsible"
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: radix-ui
- dependency-name: "@radix-ui/react-dialog"
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: radix-ui
- dependency-name: "@radix-ui/react-dropdown-menu"
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: radix-ui
- dependency-name: "@radix-ui/react-hover-card"
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: radix-ui
- dependency-name: "@radix-ui/react-icons"
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: radix-ui
- dependency-name: "@radix-ui/react-label"
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: radix-ui
- dependency-name: "@radix-ui/react-popover"
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: radix-ui
- dependency-name: "@radix-ui/react-progress"
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: radix-ui
- dependency-name: "@radix-ui/react-radio-group"
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: radix-ui
- dependency-name: "@radix-ui/react-scroll-area"
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: radix-ui
- dependency-name: "@radix-ui/react-select"
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: radix-ui
- dependency-name: "@radix-ui/react-separator"
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: radix-ui
- dependency-name: "@radix-ui/react-slider"
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: radix-ui
- dependency-name: "@radix-ui/react-slot"
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: radix-ui
- dependency-name: "@radix-ui/react-switch"
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: radix-ui
- dependency-name: "@radix-ui/react-tabs"
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: radix-ui
- dependency-name: "@radix-ui/react-toggle"
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: radix-ui
- dependency-name: "@radix-ui/react-toggle-group"
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: radix-ui
- dependency-name: "@radix-ui/react-tooltip"
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: radix-ui
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-07-19 17:21:00 +00:00
Max DeichmannandGitHub 51befcad0f chore: upgrade codemirror (#7972)
fix
2025-07-19 17:13:38 +00:00
Max DeichmannandGitHub ee8b5e4998 chore: package upgrade vitest (#7971)
fix
2025-07-19 17:13:14 +00:00
Max DeichmannandGitHub 9bb9f7fa39 chore: remove ineffective pnpm configs (#7970) 2025-07-19 17:04:43 +00:00
Thorsten SpiekerandGitHub 4cc10840ab feat: automation secrets (#7807) 2025-07-18 17:20:48 +02:00
Hassieb Pakzad 311cd900fb chore: release v3.83.0 2025-07-18 14:23:53 +02:00
Hassieb PakzadandGitHub 92f14fd297 feat(llm-connections): default credential provider chain for Bedrock when self-hosted (#7947)
* feat(llm-connections): default credential provider chain for Bedrock when self-hosted

* push

* push
2025-07-18 12:14:59 +00:00
Max DeichmannandGitHub 42306e5247 chore: fix snyk configuration (#7944) 2025-07-18 11:09:07 +02:00
5bb1b6de54 fix(playground): scroll variables if many are present (#7946)
* Refactor Variables rendering with improved sorting and key management

Co-authored-by: nimar <nimar@langfuse.com>

* fix(playground): make placeholders and variables scroll individually

---------

Co-authored-by: Cursor Agent <cursoragent@cursor.com>
2025-07-17 15:06:38 +00:00
Marc KlingenandGitHub dae0ee6165 fix(ui): main menu icons clickable when collapsed (#7942) 2025-07-17 12:10:02 +00:00
Nimar 31fb304a5e chore: release v3.82.0 2025-07-17 10:03:10 +02:00
NimarandGitHub d6dac50734 fix(playground): use playground with tool calls form langgraph traces (#7923)
* fix(playground): test with toolcalls from langgraph traces

* delete comment

* fix ts error

* remove superfluous comment
2025-07-16 18:36:03 +00:00
Max DeichmannandGitHub 0cda8e2e21 fix: add timeouts to webhooks (#7924)
* fix

* fix

* fix
2025-07-16 20:01:07 +02:00
Hassieb PakzadandGitHub 3f4fd2ff5e feat(model-prices): match cross-region inference profile for claude via bedrock (#7918) 2025-07-16 16:19:32 +00:00
Steffen SchmitzandGitHub 17f0a10b23 feat: add level filter param to observations api (#7905)
* feat: add level filter param to observations api

* chore: update fern reference

* chore: generate api spec

* chore: patch

* chore: extend timeout
2025-07-16 12:24:40 +00:00
Max DeichmannandGitHub 6c80b5b4a0 chore: add linter rule for react icons (#7910)
* push

* perf: import react-icons from one sub path
2025-07-16 13:51:48 +02:00
Max DeichmannandGitHub 91b56518c6 perf: import react-icons from one sub path (#7907) 2025-07-16 10:00:00 +00:00
Steffen SchmitzGitHubellipsis-dev[bot] <65095814+ellipsis-dev[bot]@users.noreply.github.com>
257c4136fe chore: expand logging for large entity truncation (#7900)
* chore: expand logging for large entity truncation

* Update worker/src/services/ClickhouseWriter/index.ts

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

* chore: fix tests

* chore: patch table name typing

---------

Co-authored-by: ellipsis-dev[bot] <65095814+ellipsis-dev[bot]@users.noreply.github.com>
2025-07-16 07:50:54 +00:00
94e5f4b2e7 perf: optimize shared package rebuilds for dev speed b069 (#7895)
* Update turbo.json to use @langfuse/shared#dev instead of #build

Co-authored-by: max <max@langfuse.com>

* Add incremental build script and update turbo.json dev dependencies

Co-authored-by: max <max@langfuse.com>

* Remove build:dev script and update turbo.json dependencies

Co-authored-by: max <max@langfuse.com>

* push

---------

Co-authored-by: Cursor Agent <cursoragent@cursor.com>
2025-07-15 20:43:13 +00:00
Steffen SchmitzandGitHub f09c363aaf perf: truncate very large records on insertion (#7894)
* perf: truncate very large records on insertion

* chore: skip stringify
2025-07-15 19:09:31 +00:00
d0c317b423 chore(ingestions): add s3 download size hist and span attribute (#7887)
* chore(ingestions): add s3 download size hist and span attribute

* push

* Apply suggestion from @maxdeichmann

Co-authored-by: Max Deichmann <m.deichmann@tum.de>

* Apply suggestion from @maxdeichmann

Co-authored-by: Max Deichmann <m.deichmann@tum.de>

* Apply suggestion from @hassiebp

* push

* push

---------

Co-authored-by: Max Deichmann <m.deichmann@tum.de>
2025-07-15 18:18:13 +00:00
Max DeichmannandGitHub 5bcb6b5b38 chore: remove markdown react-icon (#7893)
push
2025-07-15 17:59:39 +00:00
Max DeichmannandGitHub 3806cb72a1 chore: remove react icons as much as possible (#7889)
* fixes

* fixes

* fixes

* fixes

* push

* fixes

* fixes
2025-07-15 16:02:20 +00:00
Max DeichmannandGitHub cbfc78d0e3 chore: fix automations scrolling (#7884)
fixes
2025-07-15 13:26:26 +00:00
marliessophieandGitHub 5d87047577 chore(layout): restructure main navigation (#7875)
* chore(layout): restructure main navigation

* chore: refactor page-header tabs component

* chore: push

* chore: push

* chore: push

* chore: push

* chore: move sessions to main nav

* chore: update test

* chore: test
2025-07-15 11:52:57 +00:00
Steffen SchmitzandGitHub 7a3e5ba0c8 fix: fallback to auto for falsy region value in blob storage integration (#7879) 2025-07-15 09:33:14 +00:00
Steffen SchmitzandGitHub df1ec4d81e fix(otel): handle missing scope name gracefully (#7872) 2025-07-15 07:21:57 +00:00
steffen911 68a884c5ed chore: release v3.81.1 2025-07-15 08:41:30 +02:00
Steffen SchmitzandGitHub 7bcfb9a57e fix: adjust trace session indexes for faster API accesses (#7864) 2025-07-15 06:30:40 +00:00
Hassieb PakzadandGitHub 0e360759ec fix(playground): overflow on large description (#7863) 2025-07-14 17:43:51 +02:00
steffen911 0510a352a3 chore: release v3.81.0 2025-07-14 15:22:22 +02:00
Steffen SchmitzandGitHub 7854a7e41f perf: enable API Key and Prompt caching in redis by default (#7854)
* perf: enable API Key and Prompt caching in redis by default

* chore: update sync prompt test

* chore: disable prompt caching for sync web tests

* chore: enable prompt caching in sync tests

* chore: disable cachking for v1 pipeline

* chore Use cacheEnabled as pure test overwrite

* chore; undefined check

* chore: log body

* chore: patch promptCache test

* chore: fmt

* remove console log
2025-07-14 13:20:37 +00:00
Hassieb PakzadandGitHub f0fee7b0ce chore: remove turbo flag from dev command (#7859) 2025-07-14 13:49:44 +02:00
marliessophieandGitHub 52fd2b9ed8 chore(nav): remove all close/open transition delays for improved user experience (#7856) 2025-07-14 09:57:22 +00:00
Hassieb PakzadandGitHub 3ba75e57a0 fix(otel): keep hex ids in place in id parsing (#7827) 2025-07-14 09:54:43 +00:00
marliessophieandGitHub a42cf72d2a chore(experiments): enhance error handling in LLM call and implement retry logic for experiment creation jobs (#7632)
* chore(experiments): enhance error handling in LLM call and implement retry logic for experiment creation jobs

- Added try-catch block in callLLM function to handle specific API errors and return appropriate ApiError responses.
- Implemented retry logic for experiment creation jobs in experimentQueue, allowing retries for rate-limited and server errors, with a delay mechanism based on job age.

* chore: push

* refactor: introduce delay utility for job processing

* chore: abstract standardized error handling for LLM operations

* refactor: reorganize utilities and enhance error handling for job processing

* chore: push

* chore: typing

* chore: unify error message

* fix(retry-handler): change executeTakeFirst to executeTakeFirstOrThrow for better error handling
2025-07-14 09:35:59 +00:00
Steffen SchmitzandGitHub 35171cc77d chore: add projectId to prices table (#7817) 2025-07-14 09:34:11 +02:00
Steffen SchmitzandGitHub 6a5f221567 fix(auditlog): patch llmTool and llmSchema records in auditLog (#7826)
* fix(auditlog): patch llmTool and llmSchema records in auditLog

* chore: linting

* chore: remove redundant L
2025-07-14 06:55:29 +00:00
c9c61c4b8e ci: all-ci-passed also checks for cancelled status (#7845)
Update pipeline workflow to handle cancelled jobs in deployment status

Co-authored-by: Cursor Agent <cursoragent@cursor.com>
2025-07-13 15:25:23 +00:00
Max DeichmannandGitHub 8b6186f191 chore: use --turbo always in web dev (#7842) 2025-07-13 11:07:56 +00:00
Steffen SchmitzandGitHub 73fc0e71a3 chore: remove local claude settings file (#7828)
chore: remove local cloud settings file
2025-07-11 15:12:34 +00:00
Steffen SchmitzandGitHub 40152d1f3c chore: forbid html elements on names and descriptions via trpc routes (#7815)
* chore: forbid html elements on names and descriptions via trpc routes

* chore: patch test cases

* chore: patches
2025-07-11 14:39:59 +00:00
Max Deichmann 6cfb78e06e chore: release v3.80.1 2025-07-11 14:26:20 +02:00
Max DeichmannandGitHub 4f01e773b3 fix: navigation history automations (#7822)
* fix: navigation history automations

* fix: navigation history automations
2025-07-11 12:21:43 +00:00
Max DeichmannandGitHub 2a83fdbc9a fix: remove automations cud for members (#7820)
* fix: hipaa scaling

* fix: move webhook cud operations to admin
2025-07-11 11:40:14 +00:00
Max DeichmannandGitHub 84dd3fa63b chore: set default webhook selector without setting the browser history (#7819)
fix: hipaa scaling
2025-07-11 11:24:39 +00:00
Max DeichmannandGitHub fdf70cdde9 fix: remove S3 delete marker in S3 backfill (#7812)
push
2025-07-10 20:44:22 +00:00
Marc KlingenandGitHub 07f0125e7c fix: stripe webhook should also match subscriptions which were not created via checkout session (#7810)
* wip

* attempted fix

* typo
2025-07-10 19:29:17 +00:00
NimarandGitHub bacd44abb1 fix(prompts): prompt folder pagination fix (#7776)
* remove folder detection from frontend

* filter in be

* update

* update

* caps

* cleanup

* cleanup

* clarify

* fix

* robust

* dedup

* smiplif

* simplift

* update
2025-07-10 17:16:33 +00:00
Marc KlingenandGitHub cb032047d3 fix: when projects are queued for deletion, hide them and block access from UI (#7809)
fix: when projects are queued for deletion, hide them and block access
2025-07-10 15:35:50 +00:00
Max DeichmannandGitHub 3ecbcc1670 fix: webhook UI (#7808) 2025-07-10 14:48:08 +00:00
Steffen SchmitzandGitHub 85774535ae chore: create script to refill arbitrary queue events (#7806) 2025-07-10 13:29:55 +00:00
Steffen SchmitzandGitHub db15742d29 chore: add migration checks to confirm that required env variables are present (#7802) 2025-07-10 12:42:19 +00:00
cc1c847728 README: update language-specific README files (#7798)
* Update README.cn.md with Terraform templates and minor text changes

Co-authored-by: jannik <jannik@langfuse.com>

* Update cloud deployment and integration sections in README files

Co-authored-by: jannik <jannik@langfuse.com>

* Changes from background composer bc-f4eefbe6-9e55-4dc0-be21-0fc7d81c9079

---------

Co-authored-by: Cursor Agent <cursoragent@cursor.com>
2025-07-10 09:37:24 +00:00
Max DeichmannandGitHub a6bf28495c security: upgrade alpine image (#7796)
push
2025-07-10 09:13:58 +00:00
marliessophieandGitHub 0391b9a263 chore(prompt-experiments): set langfuse-native environment (#7573)
* chore(prompt-experiments): set langfuse-native environment

* refactor: no longer export ingestionEvent schema directly

* chore: rm line number

* chore(ingestion): refactor processEventBatch to accept options object

* chore(ingestion): refactor ingestion schemas for public and internal environments

- Introduced separate schemas for public and internal environments.
- Updated environment name validation logic and error messages.
- Refactored event schemas to utilize the new schema structure.
- Deprecated direct export of `ingestionEvent` schema in favor of factory method for better environment handling.

* chore: push

* chore: type environment schema as string

* chore: push

* chore: fix import statements

* chore: update langfuse-langchain to version 3.38.3

* chore: prettier

* chore: revert prettier
2025-07-10 08:23:28 +00:00
Max DeichmannandGitHub 4fc2e33058 fix: gracefully handle missing config for webhooks (#7787)
* fix: gracefully handle missing config for webhooks

* fix: gracefully handle missing config for webhooks
2025-07-09 18:34:08 +00:00
Max DeichmannandGitHub 9a6fa8747b fix: remove swap settings from release gha (#7786)
fix: remove swap settings from releasegha
2025-07-09 18:18:52 +00:00
Max Deichmann bd0b204d18 chore: release v3.80.0 2025-07-09 19:30:09 +02:00
Max DeichmannandGitHub 94586e3f62 fix: fix webhook storage (#7783) 2025-07-09 17:20:49 +00:00
steffen911 63349976fa chore: rename migration folder 2025-07-09 18:02:21 +02:00
Max DeichmannandGitHub 8d092d0e18 chore: remove prompt logs (#7779)
* security: upgrade golang-migrate

* push

* push

* push
2025-07-09 15:45:43 +00:00
Steffen SchmitzGitHubellipsis-dev[bot] <65095814+ellipsis-dev[bot]@users.noreply.github.com>
c8c031e0bd feat(blob-integration): allow custom start date for blob storage exports (#7745)
* chore: formatting

* chore: add migration file and patches

* format

* chore: add test case

* chore: patch test case

* chore; frontend cleanup

* Apply suggestion from @ellipsis-dev[bot]

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

* Update .devcontainer.json

* chore: test

* patch azure

* chore: revert

---------

Co-authored-by: ellipsis-dev[bot] <65095814+ellipsis-dev[bot]@users.noreply.github.com>
2025-07-09 15:44:22 +00:00
Steffen SchmitzandGitHub b616b0d260 fix(dashboard): allow score numeric value filter and metric (#7771)
* fix(dashboard): allow score numeric value filter and metric

* revert

* chore: drop debug log
2025-07-09 13:33:55 +00:00
Hassieb PakzadandGitHub 91d929646e chore(prompt-experiments): upgrade langfuse-langchain (#7770) 2025-07-09 13:18:14 +00:00
NimarandGitHub bfdbd93997 feat(playground): allow arbitrary values for placeholders in playground (#7768)
* allow arbitrary values for placeholders in playground

* update playground

* fix lint

* re-add removed functions

* stringify if arbitrary

* safety first!
2025-07-09 12:14:57 +00:00
Steffen SchmitzGitHubellipsis-dev[bot] <65095814+ellipsis-dev[bot]@users.noreply.github.com>
2916b4228d chore: improve retry script for ingestion events (#7769)
* chore: improve retry script for ingestion events

* chore: move file

* Update worker/src/scripts/replayIngestionEvents/README.md

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-07-09 11:30:31 +00:00
e5a04751e1 fix: Ensure json output for single value csv import (#7763)
* Ensure CSV parsing always returns JSON object for single columns

Co-authored-by: marc <marc@langfuse.com>

* Update web/src/features/datasets/lib/csvHelpers.ts

---------

Co-authored-by: Cursor Agent <cursoragent@cursor.com>
2025-07-09 10:57:11 +00:00
bb554b38af chore: remove webhook button from single prompt page (#7762)
* Changes from background composer bc-61bc6129-a444-40fd-9e03-d397fc1c9ee1

* fix lint

---------

Co-authored-by: Cursor Agent <cursoragent@cursor.com>
2025-07-09 10:57:05 +00:00
marliessophieandGitHub cfc8161473 chore(experimentService): extend logging (#7759)
* chore(experimentService): extend logging

* chore: eslint

* chore: revert
2025-07-09 08:49:41 +00:00
Steffen SchmitzandGitHub aecd8eff2d chore: add backoff retries to clickhouse writer (#7761) 2025-07-09 08:39:54 +00:00
Nimar 74c59f58c2 chore: release v3.79.1 2025-07-09 09:55:32 +02:00
NimarandGitHub e073ec9a21 fix(traces): jumptoplayground possible for langgraph traces with tools (#7744)
* fix jumptoplayground for langgraph

* update type
2025-07-09 07:38:31 +00:00
NimarandGitHub 5920f3a6c7 chore(dx): add prettier to CI (#7735)
* add prettier to CI

* escape

* use an array

* make CI simple again

* debug

* debug

* debug2

* debug again
2025-07-08 08:54:55 +00:00
c2a0b381d4 feat: Add gen_ai.conversation.id support (#7738)
Changes from background composer bc-11040caa-e818-42ef-8bb2-4b004ae862a7

Co-authored-by: Cursor Agent <cursoragent@cursor.com>
2025-07-08 06:45:45 +00:00
7598c95fc3 fix(prompts): prompts with many labels look good (#7731)
* Add TruncatedLabels component for flexible label display with hover

Co-authored-by: nimar <nimar@langfuse.com>

* Refactor: Improve code formatting and consistency across components

Co-authored-by: nimar <nimar@langfuse.com>

* add many labelled prompt

* add many labelled prompt

* remove superfluous code

* do 8 labels here

---------

Co-authored-by: Cursor Agent <cursoragent@cursor.com>
2025-07-07 15:16:08 +00:00
Steffen SchmitzandGitHub 35f6f5f0fc feat(dashboard): allow breakdown by month (#7725)
* feat(dashboard): allow breakdown by month

* chore: add aliases to the queries

* chore: add test case and formatting

* chore: adjust test behaviour
2025-07-07 15:07:39 +00:00
da23a5ad0b chore(ui-markdown-viewer): stop interpreting $currency symbol as latex math block marker (#7723)
Remove remark-math plugin from MarkdownViewer

Co-authored-by: Cursor Agent <cursoragent@cursor.com>
2025-07-07 14:38:08 +00:00
Max DeichmannandGitHub 5adac14049 chore: print response body webhook errors (#7728)
* security: upgrade golang-migrate

* push

* push
2025-07-07 13:36:45 +00:00
Nimar 234f131493 chore: release v3.79.0 2025-07-07 15:24:20 +02:00
Nimar c87e3f88fc chore: release v3.78.3 2025-07-07 15:17:43 +02:00
b0aeb8c454 fix(prompts): handle placeholder messages in diff view (#7726)
* Improve prompt diff display with formatted content for placeholders

Co-authored-by: nimar <nimar@langfuse.com>

* tmp

* Update PromptVersionDiffDialog.tsx

* fix diff

---------

Co-authored-by: Cursor Agent <cursoragent@cursor.com>
2025-07-07 13:15:14 +00:00
NimarandGitHub a71ac105b9 chore(dx): fix prettier version in ee (#7722)
* chore(dx): fix prettier in ee

* fix tailwind

* update formatting

* cache formattgin
2025-07-07 12:09:29 +00:00
Steffen SchmitzandGitHub 225d64d0f8 build: add registry prefix to docker-compose files (#7719) 2025-07-07 11:48:33 +00:00
Steffen SchmitzandGitHub 327018eb09 build: remove swap settings from pipeline config (#7717) 2025-07-07 11:29:05 +00:00
fe8ba75ff3 chore(dx): prettier for all (#7692)
* add prettier command

* add pre-commit hook

* sync prettier config with .vscode config

* add line

* fix husky

* update for less changes

* upgrade to prettier 3.4 for bug fixes

* ignore no-undef in TS (also worker!)

* changes to prettier

* adhere to style

* make prettier 3.4 default for bug fix

* add prettier ignore

* reset to standard

* update to 3.6.2

---------

Co-authored-by: Max Deichmann <m.deichmann@tum.de>
2025-07-07 11:00:45 +00:00
Jannik MaierhöferandGitHub 74e8c0fe58 chore(ui): change link on traces and observations table to data model docs page (#7713) 2025-07-07 09:15:09 +00:00
marliessophieandGitHub 4f57af8ea1 refactor(datasets-compare): implement context for managing selected resource metrics (#7660)
* refactor(datasets-compare): implement context for managing selected resource metrics

* chore: push

* chore: add dialog header wrapper
2025-07-07 09:12:17 +00:00
Max DeichmannandGitHub 4132335c96 security: upgrade golang-migrate (#7712)
* security: upgrade golang-migrate

* push
2025-07-07 08:52:53 +00:00
Steffen SchmitzandGitHub 67dae75e3a chore: add timestamp column to AMTs for backwards compat (#7710)
* chore: add timestamp column to AMTs for backwards compat

* chore; expand allowed claude functions
2025-07-07 08:12:30 +00:00
ed0a07a958 fix(cloud): update CSP to allow for file upload in support chat (#7703)
Update CSP to allow connections to AWS S3 domains

Co-authored-by: Cursor Agent <cursoragent@cursor.com>
2025-07-06 12:45:09 +02:00
Steffen SchmitzandGitHub 0a63cc61fc chore: fix timestamp ordering for amt table (#7700)
* chore: fix timestamp ordering for amt table

* chore: update comment
2025-07-05 13:50:53 +00:00
Max DeichmannandGitHub c385121406 chore: improve auto selection of automations page (#7698)
* chore: add webhook throughput alert

* chore: add webhook throughput alert

* chore: add webhook throughput alert
2025-07-05 08:28:45 +00:00
Max DeichmannandGitHub 7d7932e024 fix: do not store output of webhook automations (#7697)
* push

* fixes
2025-07-05 08:07:46 +00:00
Max DeichmannGitHubellipsis-dev[bot] <65095814+ellipsis-dev[bot]@users.noreply.github.com>
b26362cbca feat: webhook for prompt version changes (#7012)
* push

* fix

* fix

* fix

* fix

* fix

* push

* fix

* push

* push

* push

* push

* fix

* fix

* push

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fixes

* Update .cursor/rules/global.mdc

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

* Update web/src/features/rbac/constants/projectAccessRights.ts

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

* fixes

* fixes

* fixes

* fixes

* fixes

* fixes

* fixes

* fixes

* fixes

* fixes

* fixes

* fixes

* fixes

* fixes

* fixes

* fixes

* fixes

* fixes

* push

* fixes

* fixes

* fixes

* fixes

* fixes

* fixes

* fixes

* fixes

* fixes

* fixes

* fixes

* fixes

* fixes

* fixes

* fixes

* fixes

* push

* change event prcessing

* fixes

* fixes

* fixes

* fixes

* fixes

* fixes

* fixes

* fixes

* fixes

* fixes

* fixes

* fixes

* fixes

* fixes

* fixes

* fixes

* fixes

* fixes

* fixes

* fixes

* fixes

* fixes

* fixes

* fixes

* fixes

* fixes

* fixes

* fixes

* fixes

* fixes

* fixes

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

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

* fixes

* fixes

* fixes

* fixes

* fixes

* fixes

* fixes

* fixes

* fixes

* fixes

* fixes

* fixes

* fixes

* fixes

* fixes

* fixes

* push

* fixes

* fixes

* fixes

* fixes

* fixes

* fixes

* fixes

* fixes

* fixes

* fixes

* fixes

* fixes

* fixes

* fixes

* fixes

* fixes

* fixes

* fixes

* fixes

* fixes

* fix tests

* Update web/src/__tests__/async/prompts.v2.servertest.ts

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

* data model

* fixes

* fixes

* fixes

* fixes

* fixes

* fixes

* fixes

* fixes

* fixes

* fixes

* fixes

* fixes

* fixes

* fixes

* fixes

* fixes

* fixes

* fixes

* fixes

* fixes

* fixes

---------

Co-authored-by: ellipsis-dev[bot] <65095814+ellipsis-dev[bot]@users.noreply.github.com>
2025-07-05 07:02:30 +00:00
NimarandGitHub 9b8095f8f5 chore(dx): unit tests run against their own db (#7671)
* chore(dx): unit tests run against their own db

* fix clickhouse

* clean up a bit

* close

* update only use different postgres

* update with command

* remove CH
2025-07-04 14:50:27 +00:00
Hassieb PakzadandGitHub 8b76617019 fix(dataset-run-items-api-spec): correct response type (#7690) 2025-07-04 15:20:54 +02:00
93e50f4720 feat(prompts): Add prompt filtering by config (#7688)
* Add config column to prompts table for JSON filtering

Co-authored-by: marc <marc@langfuse.com>

* remove plan

---------

Co-authored-by: Cursor Agent <cursoragent@cursor.com>
2025-07-04 13:09:07 +00:00
Hassieb PakzadandGitHub c1388c8699 chore: upgrade langfuse-langchain (#7687) 2025-07-04 12:37:00 +00:00
Steffen SchmitzandGitHub 43fc305d5b fix: create virtual timestamp column in AMT query to support all filters (#7676)
* fix: create virtual timestamp column in AMT query to support all filters

* chore: add test logging

* chore: skip database prune for async tests

* chore: cleanup

* chore: drop console logs
2025-07-04 12:06:43 +00:00
marliessophieandGitHub ebd4cf3e8a chore(sentry): restrict loggerLink to development environment only for less noise in sentry (#7684) 2025-07-04 12:00:23 +00:00
NimarandGitHub 5dfc821575 fix: run prettier on my changes (#7685)
format all
2025-07-04 11:52:37 +00:00
marliessophieandGitHub 3965a5174f chore(sentry): only report server side errors (5xx) to sentry; skip reporting client side errors (#7682) 2025-07-04 11:09:46 +00:00
Max DeichmannandGitHub df237a11d5 chore: refactor prompts test (#7680)
* push

* fixes

* fixes

* fixes
2025-07-04 09:55:27 +00:00
marliessophieandGitHub 373cefdcb5 chore(sentry): do not report expected API key errors to sentry (#7678) 2025-07-04 08:57:37 +00:00
Steffen SchmitzandGitHub b92a8c6d81 fix(dashboard): adjust filters on view change and expose bad request errors (#7675)
* fix(dashboard): adjust filters on view change and expose bad request errors

* chore: skip error typing
2025-07-04 08:52:27 +00:00
Steffen SchmitzandGitHub 68422d404c chore(dashboard): remove breakdowns by 1:N dimensional relations (#7674) 2025-07-04 07:28:42 +00:00
Steffen SchmitzandGitHub d42c4e5e2f perf: start to experimentally shift reads to new aggregatingmergetrees (#7631)
* perf: start to experimentally shift reads to new aggregatingmergetrees

* chore: linting

* chore: more linting

* chore: create checklist for trace table view migrations

* feat: shift by id queries onto new AMTs

* chore: cover traces for public api

* chore: add notes for maxMap

* chore: incorporate feedback into query declarations

* chore: cleanup queries

* chore: adjust reads to new layout

* chore: update ingestion pipeline to make use of nullable fields

* chore: add sampling

* chore: update sampling decision attribute

* chore: fix limit for getTracesByIds

* chore: spacing

* chore: linting
2025-07-03 17:30:10 +00:00
0d2586e4f2 feat(ui): Add trace download button, exports trace and observation metadata as json (#7668)
* Add download trace as JSON button to TracePage

Co-authored-by: marc <marc@langfuse.com>

* Add JSON download functionality for trace details

Co-authored-by: marc <marc@langfuse.com>

* Checkpoint before follow-up message

* Refactor trace download button placement and add conditional rendering

Co-authored-by: marc <marc@langfuse.com>

* Refactor trace download to export trace and observations data

Co-authored-by: marc <marc@langfuse.com>

* fix

* push

---------

Co-authored-by: Cursor Agent <cursoragent@cursor.com>
2025-07-03 17:28:00 +00:00
Hassieb PakzadandGitHub ff40e79143 feat(llm-connections): add gemini 2.5 flash lite and gemini 2.5 prices (#7666)
* feat(llm-connections): add gemini 2.5 flash lite and gemini 2.5 prices

* push

* add reasoning
2025-07-03 16:25:11 +00:00
marliessophieandGitHub bd04f6b186 fix(nav): implement responsive navigation with DropdownMenu for mobile (#7665)
* feat(nav): implement responsive navigation with DropdownMenu for mobile

* chore: push
2025-07-03 16:06:44 +00:00
Max DeichmannandGitHub d107f98654 perf: increase ingestion queue retry attempts (#7649)
* push

* push
2025-07-03 12:55:37 +00:00
Hassieb PakzadandGitHub de9fa8199a feat(llm-connections): add gemini 2.5 pro and flash GA (#7659) 2025-07-03 12:33:32 +00:00
Marlies Mayerhofer 0e1498a094 chore: release v3.78.2 2025-07-03 13:37:47 +02:00
marliessophieandGitHub 38f020e482 fix(scores): update score validation logic to accept inclusive ranges for score config validation (#7650)
* fix(scores): update score validation logic to accept inclusive ranges for score config validation

* chore: typo
2025-07-03 09:22:33 +00:00
Hassieb PakzadandGitHub 07e1d4e960 fix(otel): trace update events on present metadata (#7648) 2025-07-03 08:36:40 +00:00
Hassieb PakzadandGitHub 602d00a5bb chore: upgrade langfuse-langchain (#7647) 2025-07-03 08:16:41 +00:00
7395bd8d83 fix: loading state of csv import button (#7640)
* Disable import button during processing and show loading state

Co-authored-by: marc <marc@langfuse.com>

* prettier

---------

Co-authored-by: Cursor Agent <cursoragent@cursor.com>
2025-07-03 00:12:27 +00:00
Marc KlingenandGitHub 63d164ad52 chore: improve cursor background agent configuration (#7641)
* fix

* push

* test
2025-07-03 01:41:14 +02:00
Thorsten SpiekerandGitHub f1b915c8b7 fix: remove invalid metrics and dimensions from pivot table widget on view change (#7636)
* fix: remove invalid metrics and dimensions from pivot table widget on view change

* linting
2025-07-02 19:01:26 +02:00
Steffen SchmitzandGitHub e3886b94c1 chore: set static backoff for query clickhouse retries (#7635) 2025-07-02 17:35:43 +02:00
Steffen SchmitzandGitHub 44376c2a9f chore: ignore all pending linting errors and enforce zero warnings (#7634) 2025-07-02 14:32:35 +00:00
marliessophieandGitHub 2e85c9ac15 chore(evals): add QUEUE_ERROR_MESSAGES constants for improved error handling (#7630)
* chore(evals): add QUEUE_ERROR_MESSAGES constants for improved error handling

* chore: push
2025-07-02 14:01:15 +00:00
Steffen SchmitzandGitHub 7cc81deb65 chore: retry socket hang ups for CH queries (#7628) 2025-07-02 13:52:26 +00:00
Steffen SchmitzandGitHub a13ddd41ae exp: write observation and score info to traces_mt (#7590)
* exp: write observation and score info to traces_mt

* chore: update declarations for experimental traces table

* chore: change anyLast pattern to working condition

* chore: revert anyLast setup

* chore: revert

* chore: cleanup queries
2025-07-02 11:53:38 +00:00
Nimar ebe85f2a67 chore: release v3.78.1 2025-07-02 12:15:50 +02:00
NimarandGitHub c7b62adbab feat(experiments) support for placeholders as message histories (#7615)
* run experiments with placeholders as well

* add missing file

* fix experiements

* run outside of loop

* add tests

* fix tests

* make tests clean again

* also test that variables resovle first
2025-07-02 10:04:51 +00:00
66d4926fd0 feat: add tags to search on prompts table (#7619)
* Enhance prompt search to include tags with case-insensitive matching

Co-authored-by: marc <marc@langfuse.com>

* drop md

* prettier

---------

Co-authored-by: Cursor Agent <cursoragent@cursor.com>
2025-07-01 20:37:57 +00:00
b26f5c512f refactor: pivot table dropdowns (#7611)
---------

Co-authored-by: Max Deichmann <m.deichmann@tum.de>
2025-07-01 21:21:46 +02:00
marliessophieandGitHub 9227eaab9c feat(deleteButton): add custom delete message for running evaluator (#7617) 2025-07-01 18:33:41 +00:00
Nimar 3debac082a chore: release v3.78.0 2025-07-01 18:34:30 +02:00
fb43e03171 feat(prompts): Add search bar to prompts table (#7603)
* Add search functionality to prompts table with query param support

Co-authored-by: marc <marc@langfuse.com>

* prettier

* fix

---------

Co-authored-by: Cursor Agent <cursoragent@cursor.com>
2025-07-01 14:16:45 +00:00
marliessophieandGitHub dd04da64ba fix(prisma): update JobExecution and DefaultLlmModel relations to include onUpdate and onDelete behaviors to match DB constraints (#7602)
* fix(prisma): update JobExecution and DefaultLlmModel relations to include onUpdate and onDelete behaviors to match DB constraints

* chore(prisma): update foreign key onUpdate behavior for job_template_id in JobExecution model

* Revert "chore(prisma): update foreign key onUpdate behavior for job_template_id in JobExecution model"

This reverts commit e65c12c9c36a1f8732560e665b1d8d7422c530f0.
2025-07-01 14:07:42 +00:00
b94cd9880a chore: fix typos (#7604)
* Fix typos: GitHub brand name and "check out" verb phrase

Co-authored-by: marc <marc@langfuse.com>

* Checkpoint before follow-up message

* Fix GitHub brand name and typos across documentation and UI

Co-authored-by: marc <marc@langfuse.com>

* Changes from background composer bc-bdbd909e-6932-4489-b125-e66b1d0f5ebe

---------

Co-authored-by: Cursor Agent <cursoragent@cursor.com>
2025-07-01 13:54:09 +00:00
NimarandGitHub 0f2d2b8850 fix(prompts): don't allow placeholders and variable with same name on same prompt version (#7598)
* no naming collisions between prompt and placeholder

* tests for variable placeholder name collision

* fix test spelling

* also show conflict in playhground
2025-07-01 13:39:35 +00:00
steffen911 69db85cfe4 chore: release v3.77.0 2025-07-01 13:37:53 +02:00
Thorsten SpiekerandGitHub b445471f98 feat(dashboard): add pivot table widget type (#7546) 2025-07-01 13:36:54 +02:00
2ce014980b feat(posthog-export): Add environment attribute to PostHog exports (#7581)
* Add environment to PostHog event tracking for traces and observations

Co-authored-by: marc <marc@langfuse.com>

* use object envs instead of trace env

* fix

---------

Co-authored-by: Cursor Agent <cursoragent@cursor.com>
2025-06-30 22:47:14 +00:00
451 changed files with 30986 additions and 7152 deletions
+14
View File
@@ -0,0 +1,14 @@
{
"permissions": {
"allow": [
"Bash(find:*)",
"Bash(rg:*)",
"Bash(grep:*)",
"Bash(ls:*)",
"Bash(cat:*)",
"Bash(head:*)",
"Bash(tail:*)"
],
"deny": []
}
}
+1 -1
View File
@@ -1,4 +1,4 @@
[codespell]
skip = .git,*.pdf,*.svg,package-lock.json,*.prisma,pnpm-lock.yaml
ignore-words-list = afterall,vertx,notIn
ignore-words-list = afterall,vertx,notIn,alue
+13 -6
View File
@@ -12,8 +12,15 @@ RUN apt-get update && \
postgresql-client \
redis-tools \
less nano \
sudo \
&& rm -rf /var/lib/apt/lists/*
# ---------- Docker -----------------------------------------------------------
# Install Docker for background agents that need container capabilities
RUN curl -fsSL https://get.docker.com -o get-docker.sh && \
sh get-docker.sh && \
rm get-docker.sh
# ---------- pnpm -------------------------------------------------------------
# Langfuse monorepo relies on pnpm 9.5.0 (see CONTRIBUTING.md)
ENV PNPM_HOME="/pnpm"
@@ -23,17 +30,17 @@ RUN corepack enable && \
# ---------- golang-migrate ----------------------------------------------------
# CLI used for database migrations during development.
ENV MIGRATE_VERSION=4.18.2
ENV MIGRATE_VERSION=4.18.3
RUN wget -qO- "https://github.com/golang-migrate/migrate/releases/download/v${MIGRATE_VERSION}/migrate.linux-amd64.tar.gz" \
| tar -xz -C /usr/local/bin && \
chmod +x /usr/local/bin/migrate
# ---------- Non-root user -----------------------------------------------------
# Use root for convenience in development containers
WORKDIR /workspace
# Create non-root user
RUN useradd -ms /bin/bash ubuntu
# Create non-root user with sudo privileges and docker group access
RUN useradd -ms /bin/bash ubuntu && \
usermod -aG sudo ubuntu && \
usermod -aG docker ubuntu && \
echo "ubuntu ALL=(ALL) NOPASSWD:ALL" >> /etc/sudoers
# Pre-create pnpm store and set correct ownership to avoid first-run cost & permission issues
RUN pnpm store path > /dev/null && \
+7 -1
View File
@@ -4,5 +4,11 @@
"context": ".",
"dockerfile": "Dockerfile"
},
"start": "pnpm dx-f"
"start": "sudo service docker start",
"terminals": [
{
"name": "dev server",
"command": "pnpm run dx-f"
}
]
}
+1 -1
View File
@@ -11,7 +11,7 @@ alwaysApply: true
## 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.
- Check out other features to learn about the common structure.
## API for frontend features
+2 -1
View File
@@ -5,4 +5,5 @@ alwaysApply: true
---
# General rules
- Linting in this repo only works if the development server is running
- Linting in this repo only works if the development server is running
- Always run the full mono-repo via `pnpm run dx` (use `pnpm dx-f` when you run this in a background agent). Thereby the database will also be seeded.
+20
View File
@@ -0,0 +1,20 @@
---
description:
globs:
alwaysApply: true
---
## Project setup
- This project is a Turborepo monorepo
- We build two containers, web (./web) and worker (./worker)
- We have shared code between these two in the shared package (./packages/shared). For that package, we have different entry points [package.json](mdc:packages/shared/package.json).
## Domain layer
The most important domain objects are in [observations.ts](mdc:packages/shared/src/domain/observations.ts), [traces.ts](mdc:packages/shared/src/domain/traces.ts), [scores.ts](mdc:packages/shared/src/domain/scores.ts).
## Database schema
We use Postgres and Clickhouse.
- The postgres schema is in [schema.prisma](mdc:packages/shared/prisma/schema.prisma)
- The clickhouse schema is in [0001_traces.up.sql](mdc:packages/shared/clickhouse/migrations/clustered/0001_traces.up.sql), [0002_observations.up.sql](mdc:packages/shared/clickhouse/migrations/clustered/0002_observations.up.sql), [0003_scores.up.sql](mdc:packages/shared/clickhouse/migrations/clustered/0003_scores.up.sql)
+3 -2
View File
@@ -1,6 +1,6 @@
---
description: How to create new public api routes for Langfuse
globs:
globs:
alwaysApply: false
---
@@ -13,4 +13,5 @@ alwaysApply: false
- 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
- Pagination starts at 1, query typing defined in publicApiPaginationZod, return meta in paginationMetaResponseZod
- For tests, please look at [this directory](mdc:web/src/__tests__/async/) for examples
+9
View File
@@ -0,0 +1,9 @@
---
description:
globs:
alwaysApply: true
---
# Writing tests
- When writing tests, focus on decoupling each `it` or `test` block to ensure that they can run independently and concurrently. Tests must never depend on the action or outcome of previous or subsequent tests.
- When writing tests, especially in the __tests__/async directory, ensure that you avoid `pruneDatabase` calls.
+12
View File
@@ -0,0 +1,12 @@
# Test Environment Configuration
# Copy this file to .env.test for test database isolation
# Only overrides specific test variables - other values inherited from .env
# PostgreSQL - Test Database
DIRECT_URL="postgresql://postgres:postgres@localhost:5432/langfuse_test"
DATABASE_URL="postgresql://postgres:postgres@localhost:5432/langfuse_test"
# ClickHouse - Use Default Database for now, nothing set
# Redis - Test Database (database 1 for isolation)
REDIS_CONNECTION_STRING="redis://:myredissecret@127.0.0.1:6379/1"
+1 -1
View File
@@ -28,7 +28,7 @@ Fixes # (issue)
<!-- Remove bullet points below that don't apply to you -->
- I haven't read the [contributing guide](https://github.com/langfuse/langfuse/blob/main/CONTRIBUTING.md)
- My code doesn't follow the style guidelines of this project (`npm run prettier`)
- My code doesn't follow the style guidelines of this project (`pnpm run format`)
- I haven't commented my code, particularly in hard-to-understand areas
- I haven't checked if my PR needs changes to the documentation
- I haven't checked if my changes generate no new warnings (`npm run lint`)
+1
View File
@@ -36,6 +36,7 @@ updates:
patterns:
- "express"
- "@types/express"
- "@types/express-serve-static-core"
observability:
patterns:
- "dd-trace"
@@ -15,10 +15,6 @@ jobs:
runs-on: ubuntu-latest
environment: ${{ inputs.environment }}
steps:
- name: Set Swap Space
uses: pierotofy/set-swap-space@master
with:
swap-size-gb: 10
- name: Get app name
uses: winterjung/split@v2
id: split
+52 -21
View File
@@ -52,6 +52,49 @@ jobs:
- name: lint web
run: pnpm run lint
prettier-check:
runs-on: ubuntu-latest
needs:
- pre-job
if: needs.pre-job.outputs.should_skip != 'true'
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- uses: pnpm/action-setup@v3
with:
version: 9.5.0
- uses: actions/setup-node@v4
with:
node-version: 20
cache: "pnpm"
cache-dependency-path: "pnpm-lock.yaml"
- name: install dependencies
run: |
pnpm i
- name: Load default env
run: |
cp .env.dev.example .env
- name: Check formatting on changed files
run: |
if [ "${{ github.event_name }}" = "pull_request" ]; then
BASE_SHA=${{ github.event.pull_request.base.sha }}
else
BASE_SHA=$(git merge-base origin/main HEAD)
fi
echo "Checking files changed from $BASE_SHA to HEAD"
# Get changed files
CHANGED_FILES=$(git diff --name-only $BASE_SHA HEAD -- '*.js' '*.jsx' '*.ts' '*.tsx' '*.css' | tr '\n' ' ')
if [ -n "$CHANGED_FILES" ] && [ "$CHANGED_FILES" != " " ]; then
echo "Files to check: $CHANGED_FILES"
pnpm prettier --check --experimental-cli $CHANGED_FILES
else
echo "No JS/TS/CSS files changed - skipping prettier check"
fi
test-docker-build:
timeout-minutes: 20
runs-on: ubuntu-latest
@@ -100,14 +143,10 @@ jobs:
node-version: [20]
postgres-version: [12, 15]
steps:
- name: Set Swap Space
uses: pierotofy/set-swap-space@master
with:
swap-size-gb: 10
- uses: actions/checkout@v4
- name: Install golang-migrate for Clickhouse migrations
run: |
curl -L https://github.com/golang-migrate/migrate/releases/download/v4.18.2/migrate.linux-amd64.tar.gz | tar xvz
curl -L https://github.com/golang-migrate/migrate/releases/download/v4.18.3/migrate.linux-amd64.tar.gz | tar xvz
sudo mv migrate /usr/bin/migrate
which migrate
- uses: pnpm/action-setup@v3
@@ -133,6 +172,7 @@ jobs:
cp .env.dev.example .env
grep -v -e '^LANGFUSE_S3_BATCH_EXPORT_ENABLED=' -e '^NEXT_PUBLIC_LANGFUSE_RUN_NEXT_INIT=' .env.dev.example > .env
echo "LANGFUSE_INGESTION_QUEUE_DELAY_MS=1" >> .env
echo "LANGFUSE_CACHE_PROMPT_ENABLED=false" >> .env
echo "LANGFUSE_INGESTION_CLICKHOUSE_WRITE_INTERVAL_MS=1" >> .env
- name: Run dev containers
run: |
@@ -178,14 +218,10 @@ jobs:
postgres-version: [12, 15]
deploy-mode: ["", "-azure", "-redis-cluster"]
steps:
- name: Set Swap Space
uses: pierotofy/set-swap-space@master
with:
swap-size-gb: 10
- uses: actions/checkout@v4
- name: Install golang-migrate for Clickhouse migrations
run: |
curl -L https://github.com/golang-migrate/migrate/releases/download/v4.18.2/migrate.linux-amd64.tar.gz | tar xvz
curl -L https://github.com/golang-migrate/migrate/releases/download/v4.18.3/migrate.linux-amd64.tar.gz | tar xvz
sudo mv migrate /usr/bin/migrate
which migrate
- uses: pnpm/action-setup@v3
@@ -257,10 +293,6 @@ jobs:
postgres-version: [12, 15]
deploy-mode: ["", "-azure", "-redis-cluster"]
steps:
- name: Set Swap Space
uses: pierotofy/set-swap-space@master
with:
swap-size-gb: 10
- uses: actions/checkout@v4
- uses: pnpm/action-setup@v3
with:
@@ -282,7 +314,7 @@ jobs:
pnpm install
- name: Install golang-migrate for Clickhouse migrations
run: |
curl -L https://github.com/golang-migrate/migrate/releases/download/v4.18.2/migrate.linux-amd64.tar.gz | tar xvz
curl -L https://github.com/golang-migrate/migrate/releases/download/v4.18.3/migrate.linux-amd64.tar.gz | tar xvz
sudo mv migrate /usr/bin/migrate
which migrate
- name: Load default env
@@ -343,7 +375,7 @@ jobs:
cp .env.dev.example web/.env
- name: Install golang-migrate for Clickhouse migrations
run: |
curl -L https://github.com/golang-migrate/migrate/releases/download/v4.18.2/migrate.linux-amd64.tar.gz | tar xvz
curl -L https://github.com/golang-migrate/migrate/releases/download/v4.18.3/migrate.linux-amd64.tar.gz | tar xvz
sudo mv migrate /usr/bin/migrate
which migrate
- name: Run + migrate
@@ -397,14 +429,12 @@ jobs:
pnpm install
- name: Install golang-migrate for Clickhouse migrations
run: |
curl -L https://github.com/golang-migrate/migrate/releases/download/v4.18.2/migrate.linux-amd64.tar.gz | tar xvz
curl -L https://github.com/golang-migrate/migrate/releases/download/v4.18.3/migrate.linux-amd64.tar.gz | tar xvz
sudo mv migrate /usr/bin/migrate
which migrate
- name: Load default env
run: |
cp .env.dev.example .env
echo "LANGFUSE_CACHE_API_KEY_ENABLED=true" >> .env
echo "LANGFUSE_CACHE_PROMPT_ENABLED=true" >> .env
- name: Run + migrate
run: |
docker compose -f docker-compose.dev.yml up -d
@@ -442,6 +472,7 @@ jobs:
needs:
[
lint,
prettier-check,
tests-web-sync,
tests-worker,
e2e-tests,
@@ -452,11 +483,11 @@ jobs:
if: always()
steps:
- name: Successful deploy
if: ${{ !(contains(needs.*.result, 'failure')) }}
if: ${{ !(contains(needs.*.result, 'failure') || contains(needs.*.result, 'cancelled')) }}
run: exit 0
working-directory: .
- name: Failing deploy
if: ${{ contains(needs.*.result, 'failure') }}
if: ${{ contains(needs.*.result, 'failure') || contains(needs.*.result, 'cancelled') }}
run: exit 1
working-directory: .
+10 -47
View File
@@ -1,65 +1,28 @@
name: Snyk Container
on:
pull_request:
branches:
- "**"
push:
branches:
- main
# Snyk cannot upload results in merge group. Hence, we only run on PRs and when pushingon the main branch https://github.com/github/codeql-action/issues/1572
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: ${{ github.event_name == 'pull_request' }}
branches: ["main"]
jobs:
snyk:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Build a Docker image
run: docker compose -f docker-compose.build.yml up -d
- uses: actions/checkout@v3
- name: Run Snyk to check Docker image for vulnerabilities (langfuse-web)
continue-on-error: true
- name: Scan web image with Snyk
uses: snyk/actions/docker@master
continue-on-error: true # let upload step always run
env:
SNYK_TOKEN: ${{ secrets.SNYK_TOKEN }}
with:
image: langfuse-langfuse-web
args: --file=web/Dockerfile
image: langfuse/langfuse # pulled from Docker Hub
args: --severity-threshold=high # (any extra CLI flags, but NO --file)
# Workaround for https://github.com/github/codeql-action/issues/2187
- name: Replace security-severity undefined for license-related findings
run: "sed -i 's/\"security-severity\": \"undefined\"/\"security-severity\": \"0\"/g' snyk.sarif"
- name: Replace security-severity null for license-related findings
run: "sed -i 's/\"security-severity\": \"null\"/\"security-severity\": \"0\"/g' snyk.sarif"
- name: Upload result to GitHub Code Scanning
uses: github/codeql-action/upload-sarif@v3
with:
sarif_file: snyk.sarif
category: web
- name: Run Snyk to check Docker image for vulnerabilities (langfuse-worker)
continue-on-error: true
- name: Scan worker image with Snyk
uses: snyk/actions/docker@master
continue-on-error: true # let upload step always run
env:
SNYK_TOKEN: ${{ secrets.SNYK_TOKEN }}
with:
image: langfuse-langfuse-worker
args: --file=worker/Dockerfile
# Workaround for https://github.com/github/codeql-action/issues/2187
- name: Replace security-severity undefined for license-related findings
run: "sed -i 's/\"security-severity\": \"undefined\"/\"security-severity\": \"0\"/g' snyk.sarif"
- name: Replace security-severity null for license-related findings
run: "sed -i 's/\"security-severity\": \"null\"/\"security-severity\": \"0\"/g' snyk.sarif"
- name: Upload result to GitHub Code Scanning
uses: github/codeql-action/upload-sarif@v3
with:
sarif_file: snyk.sarif
category: worker
image: langfuse/langfuse-worker # pulled from Docker Hub
args: --severity-threshold=high # (any extra CLI flags, but NO --file)
+5 -1
View File
@@ -42,6 +42,7 @@ yarn-error.log*
!.env.dev-azure.example
!.env.dev-redis-cluster.example
!.env.prod.example
!.env.test.example
# vercel
.vercel
@@ -66,4 +67,7 @@ node_modules
.yarn
.turbo
web/test-results/*
web/test-results/*
# local config files
*.local.*
+4 -3
View File
@@ -11,12 +11,13 @@ if [ "$current_branch" = "$protected_branch" ]; then
echo "🚨 You are about to commit to the $protected_branch branch. Are you sure? (y/n)"
read -r answer < /dev/tty
if [ "$answer" != "${answer#[Yy]}" ]; then
exit 0 # Commit will proceed
# Commit approved, check formatting. On files changed, block commit
pnpm run format:check
else
echo "Commit to $protected_branch branch has been canceled."
exit 1 # Commit will be blocked
fi
fi
# If not the protected branch, proceed with the commit
exit 0
# If not the protected branch, check formatting (on changed files, block)
pnpm run format:check
+2
View File
@@ -0,0 +1,2 @@
# Generated code
packages/shared/prisma/generated
+2
View File
@@ -8,6 +8,8 @@ Langfuse is an **open source LLM engineering** platform for developing, monitori
## Tests
- Codex cannot run the test suite because it depends on Docker-based infrastructure that is unavailable in this environment.
- When writing tests, focus on decoupling each `it` or `test` block to ensure that they can run independently and concurrently. Tests must never depend on the action or outcome of previous or subsequent tests.
- When writing tests, especially in the __tests__/async directory, ensure that you avoid `pruneDatabase` calls.
## Cursor Rules
- Additional folder-specific rules live in `.cursor/rules/`.
+6
View File
@@ -94,6 +94,7 @@ pnpm run test --filter=worker -- $TEST_FILE_NAME -t "$TEST_NAME"
### Utilities
```bash
pnpm run format # Format code across entire project
pnpm run nuke # Remove all node_modules, build files, wipe database, docker containers. **USE WITH CAUTION**
```
@@ -152,6 +153,8 @@ pnpm run nuke # Remove all node_modules, build files, wipe database
- Jest for API tests, Playwright for E2E tests
- For backend/API changes, tests must pass before pushes
- Add tests for new API endpoints and features
- When writing tests, focus on decoupling each `it` or `test` block to ensure that they can run independently and concurrently. Tests must never depend on the action or outcome of previous or subsequent tests.
- When writing tests, especially in the __tests__/async directory, ensure that you avoid `pruneDatabase` calls.
### Code Conventions
- **Pages Router** (not App Router)
@@ -186,3 +189,6 @@ To get a project, use the `get_project` capability with the full project name as
## TypeScript Best Practices
- In TypeScript, if possible, don't use the `any` type
## General Coding Guidelines
- For easier code reviews, prefer not to move functions etc around within a file unless necessary or instructed to do so
+36 -5
View File
@@ -124,16 +124,23 @@ Requirements
cd langfuse
```
3. Create an env file
3. Install dependencies and set up pre-commit hooks
```bash
pnpm install
pnpm run prepare # Sets up Husky pre-commit hooks for code formatting
```
4. Create an env file
```bash
cp .env.dev.example .env
```
4. Run the entire infrastructure in dev mode. **Note**: if you have an existing database, this command wipes it.
4. Run the entire infrastructure in dev mode. **Note**: if you have an existing database, this command wipes it. Also, this will fail on the very first run. Please run it again.
```bash
pnpm run dx # first run only (resets db, node_modules, ...)
pnpm run dx # first run only (resets db, docker containers, etc...)
pnpm run dev # any subsequent runs
```
@@ -149,8 +156,8 @@ Requirements
- Username: `demo@langfuse.com`
- Password: `password`
To get comprehensive example data, you can use the `seed` command:
```sh
pnpm run db:seed:examples
```
@@ -209,31 +216,55 @@ On the main branch, we adhere to the best practices of [conventional commits](ht
All tests run in the CI and must pass before merging.
All tests run against a running langfuse instance and **write/delete real data from the database**.
### Test Database Setup
Per default, the tests use the local development database. Therefore, wiping your data in the process.
For proper test isolation, create a `.env.test` file in the root directory:
```bash
cp .env.test.example .env.test
```
Then, a different PostgreSQL and Redis are used for the tests.
The `.env.test` file only overrides the set values and falls back on `.env` for all undefined values.
- **PostgreSQL**: Uses separate `langfuse_test` database for isolation
- **ClickHouse**: Uses shared `default` database for now
- **Redis**: Uses database 1 instead of 0 for isolation (Redis data is not cleaned between tests)
Tests automatically create the PostgreSQL test database if it doesn't exist and clean up data between runs.
### Tests in the `web` package (public API)
We're using Jest with in the `web` package. Therefore, if you want to provide an argument to the test runner, do it directly without an intermittent ` -- `.
We're using Jest with in the `web` package. Therefore, if you want to provide an argument to the test runner, do it directly without an intermittent `--`.
There are three types of unit tests:
- `test-sync`
- `test-async`
- `test-client`
To run a specific test, for example the test: `"should handle special characters in prompt names"` in `prompts.v2.servertest.ts`, run:
```sh
cd web # or with --filter=web
pnpm test-sync --testPathPattern="prompts\.v2\.servertest" --testNamePattern="should handle special characters in prompt names"
```
To run all tests:
```sh
pnpm run test
```
Run interactively in watch mode (not recommended!)
```sh
pnpm run test:watch
```
### Tests in the `worker` package
For the `worker` package, we're using `vitest` to run unit tests.
```sh
+7 -6
View File
@@ -26,7 +26,7 @@
<a href="https://langfuse.com/roadmap"><strong>路线图</strong></a> ·
</div>
<br/>
<span>Langfuse 使用 <a href="https://github.com/orgs/langfuse/discussions"><strong>Github Discussions</strong></a> 作为支持和功能请求的平台。</span>
<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>
<br/>
@@ -94,7 +94,7 @@ Langfuse 是一个 **开源 LLM 工程** 平台。它帮助团队协作 **开发
- [提示管理](https://langfuse.com/docs/prompts/get-started) 帮助你集中管理、版本控制并协作迭代提示。得益于服务器和客户端的高效缓存,你可以在不增加延迟的情况下反复迭代提示。
- [评估](https://langfuse.com/docs/scores/overview) 是 LLM 应用开发流程的关键组成部分,Langfuse 能够满足你的多样需求。它支持 LLM 作为裁判、用户反馈收集、手动标注以及通过 API/SDK 实现自定义评估流程。
- [评估](https://langfuse.com/docs/scores/overview) 是 LLM 应用开发流程的关键组成部分,Langfuse 能够满足你的多样需求。它支持 LLM 作为"裁判"、用户反馈收集、手动标注以及通过 API/SDK 实现自定义评估流程。
- [数据集](https://langfuse.com/docs/datasets/overview) 为评估你的 LLM 应用提供测试集和基准。它们支持持续改进、部署前测试、结构化实验、灵活评估,并能与 LangChain、LlamaIndex 等框架无缝整合。
@@ -135,7 +135,7 @@ Langfuse 是一个 **开源 LLM 工程** 平台。它帮助团队协作 **开发
- [虚拟机](https://langfuse.com/self-hosting/docker-compose):使用 Docker Compose 在单台虚拟机上部署 Langfuse。
- 【计划中】:针对各云平台的部署指南,欢迎在以下讨论中投票和评论:[AWS](https://github.com/orgs/langfuse/discussions/4645)、[Google Cloud](https://github.com/langfuse/discussions/4646)、[Azure](https://github.com/orgs/langfuse/discussions/4647)。
- Terraform 模板: [AWS](https://langfuse.com/self-hosting/aws)、[Azure](https://langfuse.com/self-hosting/azure)、[GCP](https://langfuse.com/self-hosting/gcp)
请参阅 [自托管文档](https://langfuse.com/self-hosting) 了解更多关于架构和配置选项的信息。
@@ -155,6 +155,7 @@ Langfuse 是一个 **开源 LLM 工程** 平台。它帮助团队协作 **开发
| [LiteLLM](https://langfuse.com/docs/integrations/litellm) | Python, JS/TS (仅代理) | 允许使用任何 LLM 替代 GPT。支持 Azure、OpenAI、Cohere、Anthropic、Ollama、VLLM、Sagemaker、HuggingFace、Replicate100+ LLMs)。 |
| [Vercel AI SDK](https://langfuse.com/docs/integrations/vercel-ai-sdk) | JS/TS | 基于 TypeScript 的工具包,帮助开发者使用 React、Next.js、Vue、Svelte 和 Node.js 构建 AI 驱动的应用。 |
| [API](https://langfuse.com/docs/api) | | 直接调用公共 API。提供 OpenAPI 规格。 |
| [Google VertexAI 和 Gemini](https://langfuse.com/docs/integrations/google-vertex-ai) | 模型 | 在 Google 上运行基础模型和微调模型。 |
### 与 Langfuse 集成的软件包:
@@ -162,9 +163,9 @@ Langfuse 是一个 **开源 LLM 工程** 平台。它帮助团队协作 **开发
| ---------------------------------------------------------------------- | ------------------- | -------------------------------------------------------------------------------------------------------- |
| [Instructor](https://langfuse.com/docs/integrations/instructor) | 库 | 用于获取结构化 LLM 输出(JSON、Pydantic)的库。 |
| [DSPy](https://langfuse.com/docs/integrations/dspy) | 库 | 一个系统性优化语言模型提示和权重的框架。 |
| [Amazon Bedrock](https://langfuse.com/docs/integrations/amazon-bedrock) | 模型 | 在 AWS 上运行基础模型和微调模型。 |
| [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 上运行基础模型和微调模型。 |
| [AutoGen](https://langfuse.com/docs/integrations/autogen) | 代理框架 | 用于构建分布式代理的开源 LLM 平台。 |
| [Flowise](https://langfuse.com/docs/integrations/flowise) | 聊天/代理界面 | 基于 JS/TS 的无代码构建器,用于定制化 LLM 流程。 |
| [Langflow](https://langfuse.com/docs/integrations/langflow) | 聊天/代理界面 | 基于 Python 的 LangChain 用户界面,采用 react-flow 设计,提供便捷的实验与原型构建体验。 |
@@ -253,7 +254,7 @@ _[Langfuse 中的公共示例追踪](https://cloud.langfuse.com/project/cloramnk
- 我们的 [文档](https://langfuse.com/docs) 是查找答案的最佳起点。内容全面,我们投入大量时间进行维护。你也可以通过 GitHub 提出文档修改建议。
- [Langfuse 常见问题](https://langfuse.com/faq) 解答了最常见的问题。
- 使用 “[Ask AI](https://langfuse.com/docs/ask-ai)” 立即获取问题答案。
- 使用 "Ask AI" 立即获取问题答案。
支持渠道:
@@ -351,4 +352,4 @@ _[Langfuse 中的公共示例追踪](https://cloud.langfuse.com/project/cloramnk
所有数据均不会与第三方共享,也不包含任何敏感信息。我们对这一过程保持高度透明,你可以在 [此处](/web/src/features/telemetry/index.ts) 查看我们收集的具体数据。
你可以通过设置 `TELEMETRY_ENABLED=false` 来选择退出。
````
```
+2 -4
View File
@@ -26,7 +26,7 @@
<a href="https://langfuse.com/roadmap"><strong>ロードマップ</strong></a> ·
</div>
<br/>
<span>Langfuseは、サポートと機能リクエストのために <a href="https://github.com/orgs/langfuse/discussions"><strong>Github Discussions</strong></a> を利用しています。</span>
<span>Langfuseは、サポートと機能リクエストのために <a href="https://github.com/orgs/langfuse/discussions"><strong>GitHub Discussions</strong></a> を利用しています。</span>
<br/>
<span><b>We're hiring.</b> <a href="https://langfuse.com/careers"><strong>チームに加わる</strong></a> (製品エンジニアリングおよびテクニカルGTMのポジション)への応募をお待ちしています。</span>
<br/>
@@ -141,9 +141,7 @@ Langfuseチームによるマネージドデプロイメント。充実した無
- **[VM](https://langfuse.com/self-hosting/docker-compose):**
Docker Composeを使用して、単一の仮想マシン上でLangfuseを実行します。
- **Planned:**
クラウド固有のデプロイガイドは計画中です。以下のスレッドに対して投票やコメントをお願いします:
[AWS](https://github.com/orgs/langfuse/discussions/4645), [Google Cloud](https://github.com/orgs/langfuse/discussions/4646), [Azure](https://github.com/orgs/langfuse/discussions/4647).
- Terraform テンプレート: [AWS](https://langfuse.com/self-hosting/aws), [Azure](https://langfuse.com/self-hosting/azure), [GCP](https://langfuse.com/self-hosting/gcp)
[セルフホスティングのドキュメント](https://langfuse.com/self-hosting)を参照し、アーキテクチャや設定オプションの詳細をご確認ください。
+2 -1
View File
@@ -128,7 +128,7 @@ Langfuse 팀이 관리하는 배포 방식으로, 후한 무료 플랜(취미
- [Kubernetes (Helm)](https://langfuse.com/self-hosting/kubernetes-helm): Helm을 사용해 Kubernetes 클러스터에서 Langfuse를 실행합니다. 이는 권장되는 프로덕션 배포 방식입니다.
- [VM](https://langfuse.com/self-hosting/docker-compose): Docker Compose를 사용해 단일 가상 머신에서 Langfuse를 실행합니다.
- 예정: 클라우드별 배포 가이드 – 아래 스레드에서 투표 및 댓글을 남겨주세요: [AWS](https://github.com/orgs/langfuse/discussions/4645), [Google Cloud](https://github.com/orgs/langfuse/discussions/4646), [Azure](https://github.com/orgs/langfuse/discussions/4647).
- Terraform 템플릿: [AWS](https://langfuse.com/self-hosting/aws), [Azure](https://langfuse.com/self-hosting/azure), [GCP](https://langfuse.com/self-hosting/gcp)
자세한 내용은 [자체 호스팅 문서](https://langfuse.com/self-hosting)를 참조하세요.
@@ -158,6 +158,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 | 맞춤형 LLM 플로우를 위한 JS/TS 코드 없는(no-code) 빌더입니다. |
| [Langflow](https://langfuse.com/docs/integrations/langflow) | 채팅/에이전트 UI | react-flow를 활용하여 실험 및 프로토타이핑을 손쉽게 할 수 있도록 디자인된 LangChain용 Python 기반 UI입니다. |
+1 -1
View File
@@ -26,7 +26,7 @@
<a href="https://langfuse.com/roadmap"><strong>Roadmap</strong></a> ·
</div>
<br/>
<span>Langfuse uses <a href="https://github.com/orgs/langfuse/discussions"><strong>Github Discussions</strong></a> for Support and Feature Requests.</span>
<span>Langfuse uses <a href="https://github.com/orgs/langfuse/discussions"><strong>GitHub Discussions</strong></a> for Support and Feature Requests.</span>
<br/>
<span><b>We're hiring.</b> <a href="https://langfuse.com/careers"><strong>Join us</strong></a> in product engineering and technical go-to-market roles.</span>
<br/>
+4 -4
View File
@@ -78,7 +78,7 @@ services:
retries: 3
clickhouse:
image: clickhouse/clickhouse-server
image: docker.io/clickhouse/clickhouse-server
user: "101:101"
environment:
CLICKHOUSE_DB: default
@@ -98,7 +98,7 @@ services:
start_period: 1s
minio:
image: minio/minio
image: docker.io/minio/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'
@@ -118,7 +118,7 @@ services:
start_period: 1s
redis:
image: redis:7
image: docker.io/redis:7
restart: always
command: >
--requirepass ${REDIS_AUTH:-myredissecret}
@@ -131,7 +131,7 @@ services:
retries: 10
postgres:
image: postgres:${POSTGRES_VERSION:-latest}
image: docker.io/postgres:${POSTGRES_VERSION:-latest}
restart: always
healthcheck:
test: ["CMD-SHELL", "pg_isready -U postgres"]
+3 -3
View File
@@ -1,6 +1,6 @@
services:
clickhouse:
image: clickhouse/clickhouse-server:24.3
image: docker.io/clickhouse/clickhouse-server:24.3
user: "101:101"
environment:
CLICKHOUSE_DB: default
@@ -24,7 +24,7 @@ services:
- langfuse_azurite_data:/data
redis:
image: redis:7.2.4
image: docker.io/redis:7.2.4
restart: always
command: >
--requirepass ${REDIS_AUTH:-myredissecret}
@@ -32,7 +32,7 @@ services:
- 6379:6379
postgres:
image: postgres:${POSTGRES_VERSION:-latest}
image: docker.io/postgres:${POSTGRES_VERSION:-latest}
restart: always
healthcheck:
test: ["CMD-SHELL", "pg_isready -U postgres"]
+3 -3
View File
@@ -1,6 +1,6 @@
services:
clickhouse:
image: clickhouse/clickhouse-server:24.3
image: docker.io/clickhouse/clickhouse-server:24.3
user: "101:101"
environment:
CLICKHOUSE_DB: default
@@ -16,7 +16,7 @@ services:
- postgres
minio:
image: minio/minio
image: docker.io/minio/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'
@@ -36,7 +36,7 @@ services:
start_period: 1s
postgres:
image: postgres:${POSTGRES_VERSION:-latest}
image: docker.io/postgres:${POSTGRES_VERSION:-latest}
restart: always
healthcheck:
test: ["CMD-SHELL", "pg_isready -U postgres"]
+4 -4
View File
@@ -1,6 +1,6 @@
services:
clickhouse:
image: clickhouse/clickhouse-server:24.3
image: docker.io/clickhouse/clickhouse-server:24.3
user: "101:101"
environment:
CLICKHOUSE_DB: default
@@ -16,7 +16,7 @@ services:
- postgres
minio:
image: minio/minio
image: docker.io/minio/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'
@@ -36,7 +36,7 @@ services:
start_period: 1s
redis:
image: redis:7.2.4
image: docker.io/redis:7.2.4
restart: always
command: >
--requirepass ${REDIS_AUTH:-myredissecret}
@@ -44,7 +44,7 @@ services:
- 127.0.0.1:6379:6379
postgres:
image: postgres:${POSTGRES_VERSION:-latest}
image: docker.io/postgres:${POSTGRES_VERSION:-latest}
restart: always
healthcheck:
test: ["CMD-SHELL", "pg_isready -U postgres"]
+8 -6
View File
@@ -5,7 +5,7 @@
# External connections from other machines will not be able to reach these services directly.
services:
langfuse-worker:
image: langfuse/langfuse-worker:3
image: docker.io/langfuse/langfuse-worker:3
restart: always
depends_on: &langfuse-depends-on
postgres:
@@ -62,9 +62,11 @@ services:
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}
EMAIL_FROM_ADDRESS: ${EMAIL_FROM_ADDRESS:-}
SMTP_CONNECTION_URL: ${SMTP_CONNECTION_URL:-}
langfuse-web:
image: langfuse/langfuse:3
image: docker.io/langfuse/langfuse:3
restart: always
depends_on: *langfuse-depends-on
ports:
@@ -84,7 +86,7 @@ services:
LANGFUSE_INIT_USER_PASSWORD: ${LANGFUSE_INIT_USER_PASSWORD:-}
clickhouse:
image: clickhouse/clickhouse-server
image: docker.io/clickhouse/clickhouse-server
restart: always
user: "101:101"
environment:
@@ -105,7 +107,7 @@ services:
start_period: 1s
minio:
image: minio/minio
image: docker.io/minio/minio
restart: always
entrypoint: sh
# create the 'langfuse' bucket before starting the service
@@ -126,7 +128,7 @@ services:
start_period: 1s
redis:
image: redis:7
image: docker.io/redis:7
restart: always
# CHANGEME: row below to secure redis password
command: >
@@ -140,7 +142,7 @@ services:
retries: 10
postgres:
image: postgres:${POSTGRES_VERSION:-latest}
image: docker.io/postgres:${POSTGRES_VERSION:-latest}
restart: always
healthcheck:
test: ["CMD-SHELL", "pg_isready -U postgres"]
+1 -6
View File
@@ -41,14 +41,9 @@
"eslint-config-prettier": "^9.1.0",
"eslint-config-standard": "^17.1.0",
"eslint-plugin-prettier": "^5.1.3",
"prettier": "^3.3.3",
"prettier": "^3.6.2",
"ts-node": "^10.9.2",
"tsc-watch": "^6.2.0",
"typescript": "^5.4.5"
},
"pnpm": {
"overrides": {
"nanoid": "^3.3.8"
}
}
}
@@ -27,7 +27,7 @@ service:
limit:
type: optional<integer>
docs: limit of items per page
response: PaginatedDatasetRunItems
response: PaginatedDatasetRunItems
types:
CreateDatasetRunItemRequest:
@@ -32,6 +32,9 @@ service:
userId: optional<string>
type: optional<string>
traceId: optional<string>
level:
type: optional<commons.ObservationLevel>
docs: Optional filter for observations with a specific level (e.g. "DEBUG", "DEFAULT", "WARNING", "ERROR").
parentObservationId: optional<string>
environment:
type: optional<string>
+7 -5
View File
@@ -1,6 +1,6 @@
{
"name": "langfuse",
"version": "3.76.0",
"version": "3.86.0",
"author": "engineering@langfuse.com",
"license": "MIT",
"private": true,
@@ -17,9 +17,9 @@
"db:seed": "turbo run db:seed",
"db:seed:examples": "turbo run db:seed:examples",
"nuke": "bash ./scripts/nuke.sh",
"dx": "pnpm i && pnpm run infra:dev:prune && pnpm run infra:dev:up --pull always && pnpm --filter=shared run db:reset && pnpm --filter=shared run ch:reset && pnpm --filter=shared run db:seed:examples && pnpm run dev",
"dx-f": "pnpm i && pnpm run infra:dev:prune && pnpm run infra:dev:up --pull always && pnpm --filter=shared run db:reset -f && SKIP_CONFIRM=1 pnpm --filter=shared run ch:reset && pnpm --filter=shared run db:seed:examples && pnpm run dev",
"dx:skip-infra": "pnpm i && pnpm --filter=shared run db:reset && pnpm --filter=shared run ch:reset && pnpm --filter=shared run db:seed:examples && pnpm run dev",
"dx": "pnpm i && pnpm run infra:dev:prune && pnpm run infra:dev:up --pull always && pnpm --filter=shared run db:reset:test && pnpm --filter=shared run db:reset && pnpm --filter=shared run ch:reset && pnpm --filter=shared run db:seed:examples && pnpm run dev",
"dx-f": "pnpm i && pnpm run infra:dev:prune && pnpm run infra:dev:up --pull always && pnpm --filter=shared run db:reset:test && pnpm --filter=shared run db:reset -f && SKIP_CONFIRM=1 pnpm --filter=shared run ch:reset && pnpm --filter=shared run db:seed:examples && pnpm run dev",
"dx:skip-infra": "pnpm i && pnpm --filter=shared run db:reset:test && pnpm --filter=shared run db:reset && pnpm --filter=shared run ch:reset && pnpm --filter=shared run db:seed:examples && pnpm run dev",
"build": "turbo run build",
"start": "turbo run start",
"dev": "turbo run dev",
@@ -27,6 +27,8 @@
"dev:web": "turbo run dev --filter=web",
"dev:web-turbo": "turbo run dev --filter=web -- --turbo",
"lint": "turbo run lint",
"format": "prettier --write \"**/*.{js,jsx,ts,tsx,css}\" --experimental-cli",
"format:check": "prettier --check \"**/*.{js,jsx,ts,tsx,css}\" --experimental-cli",
"test": "turbo run test",
"release": "dotenv -e ../.env -- release-it",
"prepare": "husky"
@@ -36,7 +38,7 @@
"braces": "3.0.3",
"dotenv-cli": "^7.4.2",
"husky": "^9.0.11",
"prettier": "^3.3.3",
"prettier": "^3.6.2",
"release-it": "^19.0.3",
"turbo": "^2.5.4"
},
+8
View File
@@ -35,5 +35,13 @@ module.exports = {
{
files: ["*.js?(x)", "*.ts?(x)"],
},
{
files: ["*.ts", "*.mts", "*.cts", "*.tsx"],
// no-undef doesn't make sense in TS, see:
// https://typescript-eslint.io/troubleshooting/faqs/eslint/#i-get-errors-from-the-no-undef-rule-about-global-variables-not-being-defined-even-though-there-are-no-typescript-errors
rules: {
"no-undef": "off",
},
},
],
};
+24 -2
View File
@@ -5,8 +5,30 @@
# Check if CLICKHOUSE_URL is configured
if [ -z "${CLICKHOUSE_URL}" ]; then
echo "Info: CLICKHOUSE_URL not configured, skipping migration."
exit 0
echo "Error: CLICKHOUSE_URL is not configured."
echo "Please set CLICKHOUSE_URL in your environment variables."
exit 1
fi
# Check if CLICKHOUSE_MIGRATION_URL is configured
if [ -z "${CLICKHOUSE_MIGRATION_URL}" ]; then
echo "Error: CLICKHOUSE_MIGRATION_URL is not configured."
echo "Please set CLICKHOUSE_MIGRATION_URL in your environment variables."
exit 1
fi
# Check if CLICKHOUSE_USER is set
if [ -z "${CLICKHOUSE_USER}" ]; then
echo "Error: CLICKHOUSE_USER is not set."
echo "Please set CLICKHOUSE_USER in your environment variables."
exit 1
fi
# Check if CLICKHOUSE_PASSWORD is set
if [ -z "${CLICKHOUSE_PASSWORD}" ]; then
echo "Error: CLICKHOUSE_PASSWORD is not set."
echo "Please set CLICKHOUSE_PASSWORD in your environment variables."
exit 1
fi
# Check if golang-migrate is installed
+7 -13
View File
@@ -33,11 +33,12 @@
"scripts": {
"build": "tsc",
"dev": "tsc --watch",
"lint": "eslint . --ext .js,.jsx,.ts,.tsx --max-warnings 72",
"lint": "eslint . --ext .js,.jsx,.ts,.tsx --max-warnings 0",
"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",
"db:reset": "dotenv -e ../../.env npx -- prisma migrate reset",
"db:reset:test": "if [ -f ../../.env.test ]; then dotenv -e ../../.env.test -e ../../.env -- npx prisma migrate reset --force; fi",
"db:deploy": "dotenv -e ../../.env npx -- prisma migrate deploy",
"db:seed": "dotenv -e ../../.env -- npx prisma db seed",
"db:generate": "dotenv -e ../../.env -- npx prisma generate",
@@ -60,7 +61,7 @@
"@aws-sdk/lib-storage": "^3.675.0",
"@aws-sdk/s3-request-presigner": "^3.679.0",
"@azure/storage-blob": "^12.26.0",
"@clickhouse/client": "^1.11.2",
"@clickhouse/client": "^1.12.0",
"@google-cloud/storage": "^7.15.2",
"@langchain/anthropic": "^0.3.22",
"@langchain/aws": "^0.1.11",
@@ -78,13 +79,13 @@
"bullmq": "^5.34.10",
"dd-trace": "^5.36.0",
"decimal.js": "^10.4.3",
"exponential-backoff": "^3.1.1",
"exponential-backoff": "^3.1.2",
"https-proxy-agent": "^7.0.6",
"ioredis": "^5.4.1",
"jsonpath-plus": "10.3.0",
"kysely": "^0.27.4",
"langchain": "^0.3.28",
"langfuse-langchain": "3.37.4",
"langfuse-langchain": "3.38.4",
"lodash": "^4.17.21",
"lossless-json": "^4.1.1",
"next-auth": "^4.24.11",
@@ -111,23 +112,16 @@
"eslint-plugin-prettier": "^5.1.3",
"kysely-codegen": "^0.16.8",
"nodemon": "^3.1.7",
"prettier": "^3.3.3",
"prettier": "^3.6.2",
"prisma": "^6.10.1",
"prisma-erd-generator": "^1.11.2",
"prisma-kysely": "^1.8.0",
"ts-node": "^10.9.2",
"tsc-watch": "^6.2.0",
"tsx": "^4.19.1",
"typescript": "^5.4.5",
"vitest": "^2.1.2"
"typescript": "^5.4.5"
},
"peerDependencies": {
"@types/react": "~18.2.79",
"react": "~18.2.0"
},
"pnpm": {
"overrides": {
"nanoid": "^3.3.8"
}
}
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,9 @@
-- Database migration to add PIVOT_TABLE chart type to DashboardWidgetChartType enum
-- This enables the creation of pivot table widgets in the dashboard system
--
-- This migration adds support for tabular data visualization with configurable
-- row dimensions and metrics, extending the existing widget types (line charts,
-- bar charts, pie charts, etc.) to include pivot table functionality.
-- AlterEnum
ALTER TYPE "DashboardWidgetChartType" ADD VALUE 'PIVOT_TABLE';
@@ -0,0 +1,111 @@
-- CreateEnum
CREATE TYPE "ActionType" AS ENUM ('WEBHOOK');
-- CreateEnum
CREATE TYPE "ActionExecutionStatus" AS ENUM ('COMPLETED', 'ERROR', 'PENDING', 'CANCELLED');
-- CreateTable
CREATE TABLE "actions" (
"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,
"type" "ActionType" NOT NULL,
"config" JSONB NOT NULL,
CONSTRAINT "actions_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "triggers" (
"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,
"eventSource" TEXT NOT NULL,
"eventActions" TEXT[],
"filter" JSONB,
"status" "JobConfigState" NOT NULL DEFAULT 'ACTIVE',
CONSTRAINT "triggers_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "automations" (
"id" TEXT NOT NULL,
"name" TEXT NOT NULL,
"trigger_id" TEXT NOT NULL,
"action_id" TEXT NOT NULL,
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"project_id" TEXT NOT NULL,
CONSTRAINT "automations_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "automation_executions" (
"id" TEXT NOT NULL,
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updated_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"source_id" TEXT NOT NULL,
"automation_id" TEXT NOT NULL,
"trigger_id" TEXT NOT NULL,
"action_id" TEXT NOT NULL,
"project_id" TEXT NOT NULL,
"status" "ActionExecutionStatus" NOT NULL DEFAULT 'PENDING',
"input" JSONB NOT NULL,
"output" JSONB,
"started_at" TIMESTAMP(3),
"finished_at" TIMESTAMP(3),
"error" TEXT,
CONSTRAINT "automation_executions_pkey" PRIMARY KEY ("id")
);
-- CreateIndex
CREATE INDEX "actions_project_id_idx" ON "actions"("project_id");
-- CreateIndex
CREATE INDEX "triggers_project_id_idx" ON "triggers"("project_id");
-- CreateIndex
CREATE INDEX "automations_project_id_action_id_trigger_id_idx" ON "automations"("project_id", "action_id", "trigger_id");
-- CreateIndex
CREATE INDEX "automations_project_id_name_idx" ON "automations"("project_id", "name");
-- CreateIndex
CREATE INDEX "automation_executions_trigger_id_idx" ON "automation_executions"("trigger_id");
-- CreateIndex
CREATE INDEX "automation_executions_action_id_idx" ON "automation_executions"("action_id");
-- CreateIndex
CREATE INDEX "automation_executions_project_id_idx" ON "automation_executions"("project_id");
-- AddForeignKey
ALTER TABLE "actions" ADD CONSTRAINT "actions_project_id_fkey" FOREIGN KEY ("project_id") REFERENCES "projects"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "triggers" ADD CONSTRAINT "triggers_project_id_fkey" FOREIGN KEY ("project_id") REFERENCES "projects"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "automations" ADD CONSTRAINT "automations_trigger_id_fkey" FOREIGN KEY ("trigger_id") REFERENCES "triggers"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "automations" ADD CONSTRAINT "automations_action_id_fkey" FOREIGN KEY ("action_id") REFERENCES "actions"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "automations" ADD CONSTRAINT "automations_project_id_fkey" FOREIGN KEY ("project_id") REFERENCES "projects"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "automation_executions" ADD CONSTRAINT "automation_executions_automation_id_fkey" FOREIGN KEY ("automation_id") REFERENCES "automations"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "automation_executions" ADD CONSTRAINT "automation_executions_trigger_id_fkey" FOREIGN KEY ("trigger_id") REFERENCES "triggers"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "automation_executions" ADD CONSTRAINT "automation_executions_action_id_fkey" FOREIGN KEY ("action_id") REFERENCES "actions"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "automation_executions" ADD CONSTRAINT "automation_executions_project_id_fkey" FOREIGN KEY ("project_id") REFERENCES "projects"("id") ON DELETE CASCADE ON UPDATE CASCADE;
@@ -0,0 +1,6 @@
-- CreateEnum
CREATE TYPE "BlobStorageExportMode" AS ENUM ('FULL_HISTORY', 'FROM_TODAY', 'FROM_CUSTOM_DATE');
-- AlterTable
ALTER TABLE "blob_storage_integrations" ADD COLUMN "export_mode" "BlobStorageExportMode" NOT NULL DEFAULT 'FULL_HISTORY',
ADD COLUMN "export_start_date" TIMESTAMP(3);
@@ -0,0 +1,13 @@
-- AlterTable
ALTER TABLE "prices"
ADD COLUMN "project_id" TEXT;
-- AddForeignKey
ALTER TABLE "prices"
ADD CONSTRAINT "prices_project_id_fkey" FOREIGN KEY ("project_id") REFERENCES "projects" ("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- BackfillData
UPDATE "prices"
SET "project_id" = (SELECT "models"."project_id"
FROM "models"
WHERE "models"."id" = "prices"."model_id");
@@ -0,0 +1,2 @@
INSERT INTO background_migrations (id, name, script, args)
VALUES ('3445cac4-d9d5-4750-8b65-351135c1b85e', '20250711_1347_patch_llm_tool_schema_audit_logs', 'patchLLMToolAndLLLMSchemaAuditLogs', '{}');
@@ -0,0 +1,2 @@
-- CreateIndex
CREATE INDEX CONCURRENTLY IF NOT EXISTS "trace_sessions_project_id_created_at_idx" ON "trace_sessions"("project_id", "created_at" DESC);
@@ -0,0 +1,2 @@
-- DropIndex
DROP INDEX CONCURRENTLY IF EXISTS "trace_sessions_created_at_idx";
@@ -0,0 +1,2 @@
-- DropIndex
DROP INDEX CONCURRENTLY IF EXISTS "trace_sessions_project_id_idx";
@@ -0,0 +1,2 @@
-- DropIndex
DROP INDEX CONCURRENTLY IF EXISTS "trace_sessions_updated_at_idx";
@@ -1,3 +1,3 @@
# Please do not edit this file manually
# It should be added in your version-control system (e.g., Git)
provider = "postgresql"
provider = "postgresql"
+127 -5
View File
@@ -157,7 +157,12 @@ model Project {
Dashboard Dashboard[]
DashboardWidget DashboardWidget[]
TableViewPreset TableViewPreset[]
actions Action[]
triggers Trigger[]
automationExecutions AutomationExecution[]
Automation Automation[]
DefaultLlmModel DefaultLlmModel[]
Price Price[]
@@index([orgId])
@@map("projects")
@@ -306,9 +311,7 @@ model TraceSession {
environment String @default("default")
@@id([id, projectId])
@@index([projectId])
@@index([createdAt])
@@index([updatedAt])
@@index([projectId, createdAt(sort: Desc)])
@@map("trace_sessions")
}
@@ -751,6 +754,8 @@ model Price {
updatedAt DateTime @default(now()) @updatedAt @map("updated_at")
modelId String @map("model_id") // Model is already linked to project (or default), so we don't need projectId here
Model Model @relation(fields: [modelId], references: [id], onDelete: Cascade)
projectId String? @map("project_id")
project Project? @relation(fields: [projectId], references: [id], onDelete: Cascade)
usageType String @map("usage_type")
price Decimal
@@ -871,7 +876,7 @@ model JobExecution {
jobConfiguration JobConfiguration @relation(fields: [jobConfigurationId], references: [id], onDelete: Cascade)
jobTemplateId String? @map("job_template_id")
jobTemplate EvalTemplate? @relation(fields: [jobTemplateId], references: [id], onDelete: SetNull)
jobTemplate EvalTemplate? @relation(fields: [jobTemplateId], references: [id], onDelete: SetNull, onUpdate: NoAction)
status JobExecutionStatus
startTime DateTime? @map("start_time")
@@ -902,7 +907,7 @@ model DefaultLlmModel {
updatedAt DateTime @default(now()) @updatedAt @map("updated_at")
projectId String @map("project_id")
Project Project @relation(fields: [projectId], references: [id])
Project Project @relation(fields: [projectId], references: [id], onDelete: Cascade)
llmApiKeyId String @map("llm_api_key_id")
LlmApiKey LlmApiKeys @relation("LlmApiKeyId", fields: [llmApiKeyId], references: [id], onDelete: Cascade)
@@ -963,6 +968,8 @@ model BlobStorageIntegration {
enabled Boolean
exportFrequency String @map("export_frequency")
fileType BlobStorageIntegrationFileType @default(CSV) @map("file_type")
exportMode BlobStorageExportMode @default(FULL_HISTORY) @map("export_mode")
exportStartDate DateTime? @map("export_start_date")
createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime @default(now()) @updatedAt @map("updated_at")
@@ -986,6 +993,14 @@ enum BlobStorageIntegrationType {
@@map("BlobStorageIntegrationType")
}
enum BlobStorageExportMode {
FULL_HISTORY
FROM_TODAY
FROM_CUSTOM_DATE
@@map("BlobStorageExportMode")
}
model BatchExport {
id String @id @default(cuid())
createdAt DateTime @default(now()) @map("created_at")
@@ -1157,6 +1172,7 @@ enum DashboardWidgetChartType {
PIE
NUMBER
HISTOGRAM
PIVOT_TABLE
}
model DashboardWidget {
@@ -1213,3 +1229,109 @@ model TableViewPreset {
@@unique([projectId, tableName, name])
@@map("table_view_presets")
}
model Action {
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)
type ActionType
// Configuration specific to each action type
config Json // Structured JSON for different action types
// For WEBHOOK: { version: "1.0", url: "...", method: "POST", headers: {...}, secretId: "..." }
// For ANNOTATION_QUEUE: { version: "1.0", queueId: "..." }
automations Automation[]
automationExecutions AutomationExecution[]
@@index([projectId])
@@map("actions")
}
model Trigger {
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)
// When should this trigger fire
eventSource String // trace, prompt, etc.
eventActions String[] // created, updated, deleted
filter Json? // Filter conditions (format: { field: "name", operator: "equals", value: "my_trace" })
// Additional attributes
status JobConfigState @default(ACTIVE) @map("status")
// Link to executions
automationExecutions AutomationExecution[]
automations Automation[]
@@index([projectId])
@@map("triggers")
}
model Automation {
id String @id @default(cuid())
name String @map("name")
trigger Trigger @relation(fields: [triggerId], references: [id], onDelete: Cascade)
triggerId String @map("trigger_id")
action Action @relation(fields: [actionId], references: [id], onDelete: Cascade)
actionId String @map("action_id")
createdAt DateTime @default(now()) @map("created_at")
projectId String @map("project_id")
project Project @relation(fields: [projectId], references: [id], onDelete: Cascade)
AutomationExecution AutomationExecution[]
@@index([projectId, actionId, triggerId])
@@index([projectId, name])
@@map("automations")
}
enum ActionType {
WEBHOOK
// More action types can be added as needed
}
enum ActionExecutionStatus {
COMPLETED
ERROR
PENDING
CANCELLED
}
model AutomationExecution {
id String @id @default(cuid())
createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime @default(now()) @updatedAt @map("updated_at")
sourceId String @map("source_id")
automationId String @map("automation_id")
automation Automation @relation(fields: [automationId], references: [id], onDelete: Cascade)
triggerId String @map("trigger_id")
trigger Trigger @relation(fields: [triggerId], references: [id], onDelete: Cascade)
actionId String @map("action_id")
action Action @relation(fields: [actionId], references: [id], onDelete: Cascade)
projectId String @map("project_id")
project Project @relation(fields: [projectId], references: [id], onDelete: Cascade)
status ActionExecutionStatus @default(PENDING) @map("status")
input Json @map("input")
output Json? @map("output")
startedAt DateTime? @map("started_at")
finishedAt DateTime? @map("finished_at")
error String? @map("error")
@@index([triggerId])
@@index([actionId])
@@index([projectId])
@@map("automation_executions")
}
+27 -14
View File
@@ -4,15 +4,16 @@ import { hash } from "bcryptjs";
import { v4 } from "uuid";
import { encrypt } from "../../src/encryption";
import {
type JobConfiguration,
JobExecutionStatus,
PrismaClient,
type Project,
ScoreDataType,
type JobConfiguration,
JobExecutionStatus,
PrismaClient,
type Project,
ScoreDataType,
} from "../../src/index";
import { getDisplaySecretKey, hashSecretKey, logger } from "../../src/server";
import { redis } from "../../src/server/redis/redis";
import {EVAL_TRACE_COUNT,
import {
EVAL_TRACE_COUNT,
FAILED_EVAL_TRACE_INTERVAL,
SEED_CHAT_ML_PROMPTS,
SEED_DATASETS,
@@ -22,6 +23,7 @@ import {EVAL_TRACE_COUNT,
SEED_TEXT_PROMPTS,
} from "./utils/postgres-seed-constants";
import {
generateDatasetItemId,
generateDatasetRunTraceId,
generateEvalObservationId,
generateEvalScoreId,
@@ -178,7 +180,7 @@ async function main() {
const seedApiKey = {
id: "seed-api-key",
secret: process.env.SEED_SECRET_KEY ?? "sk-lf-1234567890",
secret: process.env.SEED_SECRET_KEY ?? "sk-lf-1234567890", // eslint-disable-line turbo/no-undeclared-env-vars
public: "pk-lf-1234567890",
note: "seeded key",
};
@@ -241,7 +243,7 @@ async function main() {
const secondKey = {
id: "seed-api-key-2",
secret: process.env.SEED_SECRET_KEY ?? "sk-lf-asdfghjkl",
secret: process.env.SEED_SECRET_KEY ?? "sk-lf-asdfghjkl", // eslint-disable-line turbo/no-undeclared-env-vars
public: "pk-lf-asdfghjkl",
note: "seeded key 2",
};
@@ -274,7 +276,7 @@ async function main() {
await createTraceSessions(project1, project2);
// If openai key is in environment, add it to the projects LLM API keys
const OPENAI_API_KEY = process.env.OPENAI_API_KEY;
const OPENAI_API_KEY = process.env.OPENAI_API_KEY; // eslint-disable-line turbo/no-undeclared-env-vars
if (OPENAI_API_KEY) {
await prisma.llmApiKeys.create({
@@ -516,6 +518,7 @@ export async function createDatasets(
description: data.description,
projectId,
metadata: data.metadata,
id: `${datasetName}-${projectId.slice(-8)}`,
},
}));
@@ -531,13 +534,23 @@ export async function createDatasets(
const datasetItem = await prisma.datasetItem.upsert({
where: {
id_projectId: {
id: `${dataset.id}-${index}`,
id: generateDatasetItemId(
datasetName,
index,
projectId,
SEED_DATASETS.indexOf(data) || 0,
),
projectId,
},
},
create: {
projectId,
id: `${dataset.id}-${index}`,
id: generateDatasetItemId(
datasetName,
index,
projectId,
SEED_DATASETS.indexOf(data) || 0,
),
datasetId: dataset.id,
sourceTraceId: sourceTraceId ?? null,
sourceObservationId: null,
@@ -553,14 +566,14 @@ export async function createDatasets(
for (let datasetRunNumber = 0; datasetRunNumber < 3; datasetRunNumber++) {
const datasetRun = await prisma.datasetRuns.upsert({
where: {
datasetId_projectId_name: {
datasetId: dataset.id,
id_projectId: {
id: `demo-dataset-run-${datasetRunNumber}-${projectId.slice(-8)}`,
projectId,
name: `demo-dataset-run-${datasetRunNumber}`,
},
},
create: {
projectId,
id: `demo-dataset-run-${datasetRunNumber}-${projectId.slice(-8)}`,
name: `demo-dataset-run-${datasetRunNumber}`,
description: Math.random() > 0.5 ? "Dataset run description" : "",
datasetId: dataset.id,
@@ -2,6 +2,8 @@ import {
TraceRecordInsertType,
ObservationRecordInsertType,
ScoreRecordInsertType,
DatasetRunItemRecordInsertType,
createDatasetRunItemsCh,
} from "../../../src/server";
import { SEED_TEXT_PROMPTS } from "./postgres-seed-constants";
import {
@@ -42,6 +44,16 @@ export class ClickHouseQueryBuilder {
return await createObservationsCh(observations);
}
/**
* Creates INSERT query for dataset run items data using VALUES syntax.
* Use for: Small datasets, dataset run items that link to postgres data (e.g. dataset runs)
*/
async executeDatasetRunItemsInsert(
datasetRunItems: DatasetRunItemRecordInsertType[],
): Promise<InsertResult> {
return await createDatasetRunItemsCh(datasetRunItems);
}
/**
* Creates INSERT query for score data using VALUES syntax.
* Use for: Small datasets, scores with custom values and metadata.
@@ -6,6 +6,8 @@ import {
REALISTIC_MODELS,
} from "./clickhouse-seed-constants";
import {
generateDatasetItemId,
generateDatasetRunItemId,
generateDatasetRunTraceId,
generateEvalObservationId,
generateEvalScoreId,
@@ -23,6 +25,8 @@ import {
ObservationRecordInsertType,
ScoreRecordInsertType,
TraceRecordInsertType,
DatasetRunItemRecordInsertType,
createDatasetRunItem,
} from "../../../src/server";
/**
@@ -60,6 +64,49 @@ export class DataGenerator {
return Math.floor(Math.random() * (max - min + 1)) + min;
}
/**
* Creates dataset run items for dataset runs.
* Use for: Dataset experiment scenarios.
*/
generateDatasetRunItem(
input: DatasetItemInput & { runCreatedAt: number },
projectId: string,
): DatasetRunItemRecordInsertType {
const datasetRunItemId = generateDatasetRunItemId(
input.datasetName,
input.itemIndex,
projectId,
input.runNumber || 0,
);
// TODO: there are too many dataset run items in the postgres database?
return createDatasetRunItem({
id: datasetRunItemId,
project_id: projectId,
trace_id: generateDatasetRunTraceId(
input.datasetName,
input.itemIndex,
projectId,
input.runNumber || 0,
),
dataset_id: `${input.datasetName}-${projectId.slice(-8)}`,
dataset_run_id: `demo-dataset-run-${input.runNumber}-${projectId.slice(-8)}`,
dataset_run_name: `demo-dataset-run-${input.runNumber}-${projectId.slice(-8)}`,
dataset_run_created_at: input.runCreatedAt,
dataset_run_description:
(input.runNumber || 0) % 2 === 0 ? "Dataset run description" : "",
dataset_run_metadata: { key: "value" },
dataset_item_id: generateDatasetItemId(
input.datasetName,
input.itemIndex,
projectId,
input.runNumber || 0,
),
dataset_item_input: input.item.input,
dataset_item_expected_output: input.item.output,
});
}
/**
* Creates traces from dataset items for experiment runs.
* Use for: Dataset experiments scenarios.
@@ -271,6 +271,68 @@ export const SEED_TEXT_PROMPTS = [
labels: ["production", "latest"],
tags: ["tag1", "tag2"],
},
{
id: `prompt-with-many-labels`,
createdBy: "user-1",
prompt:
"This is a comprehensive prompt for testing multiple label scenarios. It demonstrates how prompts can be tagged with numerous labels for organization, categorization, and filtering purposes. Use this prompt to understand how label management works at scale. Variables: {{input}}",
name: "prompt-with-many-labels",
version: 1,
labels: [
"production",
"latest",
"v1",
"v2",
"stable",
"beta",
"alpha",
"test",
"development",
"staging",
"experimental",
"feature",
"bugfix",
"hotfix",
"critical",
"high-priority",
"medium-priority",
"low-priority",
"urgent",
"customer-facing",
"internal",
"public",
"private",
"confidential",
"ai",
"nlp",
"chatbot",
"assistant",
"automation",
"ml",
"data",
"analytics",
"monitoring",
"logging",
"debug",
"performance",
"security",
"compliance",
"audit",
"review",
"approved",
"rejected",
"pending",
"archived",
"deprecated",
"legacy",
"migration",
"upgrade",
"downgrade",
"template",
"example",
],
tags: [],
},
];
export const SEED_CHAT_ML_PROMPTS = [
@@ -1,3 +1,21 @@
export const generateDatasetRunItemId = (
datasetName: string,
itemIndex: number,
projectId: string,
runNumber: number,
) => {
return `dataset-run-item-${datasetName}-${itemIndex}-${projectId.slice(-8)}-${runNumber}`;
};
export const generateDatasetItemId = (
datasetName: string,
itemIndex: number,
projectId: string,
runNumber: number,
) => {
return `dataset-item-${datasetName}-${itemIndex}-${projectId.slice(-8)}-${runNumber}`;
};
export const generateDatasetRunTraceId = (
datasetName: string,
itemIndex: number,
@@ -92,12 +92,26 @@ export class SeederOrchestrator {
logger.info(
`Processing run ${runNumber + 1}/${numberOfRuns} for project ${projectId}`,
);
// const now = Date.now();
const traces: TraceRecordInsertType[] = [];
const observations: ObservationRecordInsertType[] = [];
// const datasetRunItems: DatasetRunItemRecordInsertType[] = [];
for (const seedDataset of SEED_DATASETS) {
for (const [itemIndex, datasetItem] of seedDataset.items.entries()) {
// // Generate dataset run item data
// const datasetRunItem = this.dataGenerator.generateDatasetRunItem(
// {
// datasetName: seedDataset.name,
// itemIndex,
// item: datasetItem,
// runNumber,
// runCreatedAt: now,
// },
// projectId,
// );
// Generate trace data
const trace = this.dataGenerator.generateDatasetTrace(
{
@@ -123,12 +137,14 @@ export class SeederOrchestrator {
traces.push(trace);
observations.push(observation);
// datasetRunItems.push(datasetRunItem);
}
}
try {
await this.queryBuilder.executeTracesInsert(traces);
await this.queryBuilder.executeObservationsInsert(observations);
// await this.queryBuilder.executeDatasetRunItemsInsert(datasetRunItems);
} catch (error) {
logger.error(`✗ Insert failed:`, error);
throw error;
+1
View File
@@ -88,6 +88,7 @@ declare const globalThis: {
kyselyPrismaGlobal: { $kysely: Kysely<DB> } | undefined;
} & typeof global;
// eslint-disable-next-line turbo/no-undeclared-env-vars
if (process.env.NODE_ENV === "development") {
globalThis.prismaGlobal ??= createPrismaInstance(); // regular instantiation
globalThis.kyselyPrismaGlobal ??= globalThis.prismaGlobal.$extends(
+94
View File
@@ -0,0 +1,94 @@
import { Action, Trigger } from "@prisma/client";
import { FilterState } from "../types";
import { z } from "zod/v4";
export enum TriggerEventSource {
// eslint-disable-next-line no-unused-vars
Prompt = "prompt",
}
export const EventActionSchema = z.enum(["created", "updated", "deleted"]);
export type TriggerEventAction = z.infer<typeof EventActionSchema>;
export const TriggerEventSourceSchema = z.enum([TriggerEventSource.Prompt]);
export type TriggerDomain = Omit<
Trigger,
"filter" | "eventSource" | "eventActions"
> & {
filter: FilterState;
eventSource: TriggerEventSource;
eventActions: TriggerEventAction[];
};
export type AutomationDomain = {
id: string;
name: string;
trigger: TriggerDomain;
action: ActionDomain;
};
export type ActionDomain = Omit<Action, "config"> & {
config: SafeWebhookActionConfig;
};
export type ActionDomainWithSecrets = Omit<Action, "config"> & {
config: WebhookActionConfigWithSecrets;
};
export const ActionTypeSchema = z.enum(["WEBHOOK"]);
export const AvailableWebhookApiSchema = z.record(
z.enum(["prompt"]),
z.enum(["v1"]),
);
export const RequestHeaderSchema = z.object({
secret: z.boolean(),
value: z.string(),
});
export const WebhookActionConfigSchema = z.object({
type: z.literal("WEBHOOK"),
url: z.url(),
headers: z.record(z.string(), z.string()),
requestHeaders: z.record(z.string(), RequestHeaderSchema),
displayHeaders: z.record(z.string(), RequestHeaderSchema),
apiVersion: AvailableWebhookApiSchema,
secretKey: z.string(),
displaySecretKey: z.string(),
});
export const SafeWebhookActionConfigSchema = WebhookActionConfigSchema.omit({
secretKey: true,
headers: true,
requestHeaders: true,
});
export type SafeWebhookActionConfig = z.infer<
typeof SafeWebhookActionConfigSchema
>;
export const WebhookActionCreateSchema = WebhookActionConfigSchema.omit({
secretKey: true,
displaySecretKey: true,
headers: true, // don't use legacy field anymore
displayHeaders: true,
});
export const ActionConfigSchema = z.discriminatedUnion("type", [
WebhookActionConfigSchema,
]);
export const ActionCreateSchema = z.discriminatedUnion("type", [
WebhookActionCreateSchema,
]);
export type ActionTypes = z.infer<typeof ActionTypeSchema>;
export type ActionConfig = z.infer<typeof ActionConfigSchema>;
export type ActionCreate = z.infer<typeof ActionCreateSchema>;
export type WebhookActionConfigWithSecrets = z.infer<
typeof WebhookActionConfigSchema
>;
@@ -0,0 +1,28 @@
import z from "zod/v4";
import { jsonSchema } from "../utils/zod";
import { MetadataDomain } from "./traces";
export const DatasetRunItemSchema = z.object({
id: z.string(),
projectId: z.string(),
datasetRunId: z.string(),
datasetItemId: z.string(),
datasetId: z.string(),
traceId: z.string(),
observationId: z.string().nullable(),
error: z.string().nullable(),
// timestamps
createdAt: z.date(),
updatedAt: z.date(),
// dataset run fields
datasetRunName: z.string(),
datasetRunDescription: z.string().nullable(),
datasetRunMetadata: MetadataDomain,
datasetRunCreatedAt: z.date(),
// dataset item fields
datasetItemInput: jsonSchema,
datasetItemExpectedOutput: jsonSchema,
datasetItemMetadata: MetadataDomain,
});
export type DatasetRunItemDomain = z.infer<typeof DatasetRunItemSchema>;
+3
View File
@@ -2,3 +2,6 @@ export * from "./observations";
export * from "./traces";
export * from "./scores";
export * from "./table-view-presets";
export * from "./automations";
export * from "./webhooks";
export * from "./prompts";
+21
View File
@@ -0,0 +1,21 @@
import { z } from "zod/v4";
import { jsonSchemaNullable } from "../utils/zod";
export const PromptDomainSchema = z.object({
id: z.string(),
name: z.string(),
version: z.number(),
createdAt: z.date(),
updatedAt: z.date(),
createdBy: z.string(),
isActive: z.boolean().nullable(),
type: z.string().default("text"),
tags: z.array(z.string()).default([]),
labels: z.array(z.string()).default([]),
prompt: jsonSchemaNullable,
config: jsonSchemaNullable,
projectId: z.string(),
commitMessage: z.string().nullable(),
});
export type PromptDomain = z.infer<typeof PromptDomainSchema>;
@@ -3,11 +3,11 @@ import { orderBy } from "../interfaces/orderBy";
import z from "zod/v4";
export enum TableViewPresetTableName {
Traces = "traces",
Observations = "observations",
Scores = "scores",
Sessions = "sessions",
Datasets = "datasets",
Traces = "traces", // eslint-disable-line no-unused-vars
Observations = "observations", // eslint-disable-line no-unused-vars
Scores = "scores", // eslint-disable-line no-unused-vars
Sessions = "sessions", // eslint-disable-line no-unused-vars
Datasets = "datasets", // eslint-disable-line no-unused-vars
}
const TableViewPresetDomainSchema = z.object({
+37
View File
@@ -0,0 +1,37 @@
import { z } from "zod/v4";
import { jsonSchema } from "../utils/zod";
import { EventActionSchema } from "./automations";
export const WebhookDefaultHeaders = {
"content-type": "application/json",
"user-agent": "Langfuse/1.0",
};
export const WebhookOutboundBaseSchema = z.object({
id: z.string(),
timestamp: z.coerce.date(),
type: z.literal("prompt-version"),
apiVersion: z.literal("v1"),
action: EventActionSchema,
});
export const PromptWebhookOutboundSchema = z
.object({
prompt: z.object({
id: z.string(),
name: z.string(),
version: z.number(),
projectId: z.string(),
labels: z.array(z.string()),
prompt: jsonSchema.nullable(),
type: z.string(),
config: z.record(z.string(), z.any()),
commitMessage: z.string().nullable(),
tags: z.array(z.string()),
createdAt: z.coerce.date(),
updatedAt: z.coerce.date(),
}),
})
.and(WebhookOutboundBaseSchema);
export type PromptWebhookOutput = z.infer<typeof PromptWebhookOutboundSchema>;
@@ -0,0 +1,60 @@
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 = 12; // For AES-GCM, this is always 12
// Alternatively: openssl rand -hex 32
export function keyGen() {
return crypto.randomBytes(32).toString("hex");
}
/**
* Encrypts the given plain text using AES-256-GCM algorithm.
*
* @param {string} plainText - The text to encrypt.
* @returns {string} The encrypted data in hex format, including IV and authentication tag.
*/
export function encrypt(plainText: string): string {
if (!ENCRYPTION_KEY) {
throw new Error("Missing environment variable: `ENCRYPTION_KEY`");
}
const iv = crypto.randomBytes(IV_LENGTH); // Directly use Buffer returned by randomBytes
const cipher = crypto.createCipheriv(
"aes-256-gcm",
Buffer.from(ENCRYPTION_KEY, "hex"),
iv,
);
let encrypted = cipher.update(plainText, "utf8", "hex");
encrypted += cipher.final("hex");
const authTag = cipher.getAuthTag();
// Return iv, encrypted data, and authTag as hex, combined in one line
return iv.toString("hex") + ":" + encrypted + ":" + authTag.toString("hex");
}
export function decrypt(text: string): string {
if (!ENCRYPTION_KEY) {
throw new Error("Missing environment variable: `ENCRYPTION_KEY`");
}
const [ivHex, encryptedHex, authTagHex] = text.split(":");
if (!ivHex || !encryptedHex || !authTagHex) {
throw new Error("Invalid or corrupted cipher format");
}
const iv = Buffer.from(ivHex, "hex");
const encryptedText = Buffer.from(encryptedHex, "hex");
const authTag = Buffer.from(authTagHex, "hex");
const decipher = crypto.createDecipheriv(
"aes-256-gcm",
Buffer.from(ENCRYPTION_KEY, "hex"),
iv,
);
decipher.setAuthTag(authTag);
let decrypted = decipher.update(encryptedText, undefined, "utf8");
decrypted += decipher.final("utf8");
return decrypted.toString();
}
+2 -60
View File
@@ -1,60 +1,2 @@
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 = 12; // For AES-GCM, this is always 12
// Alternatively: openssl rand -hex 32
export function keyGen() {
return crypto.randomBytes(32).toString("hex");
}
/**
* Encrypts the given plain text using AES-256-GCM algorithm.
*
* @param {string} plainText - The text to encrypt.
* @returns {string} The encrypted data in hex format, including IV and authentication tag.
*/
export function encrypt(plainText: string): string {
if (!ENCRYPTION_KEY) {
throw new Error("Missing environment variable: `ENCRYPTION_KEY`");
}
const iv = crypto.randomBytes(IV_LENGTH); // Directly use Buffer returned by randomBytes
const cipher = crypto.createCipheriv(
"aes-256-gcm",
Buffer.from(ENCRYPTION_KEY, "hex"),
iv,
);
let encrypted = cipher.update(plainText, "utf8", "hex");
encrypted += cipher.final("hex");
const authTag = cipher.getAuthTag();
// Return iv, encrypted data, and authTag as hex, combined in one line
return iv.toString("hex") + ":" + encrypted + ":" + authTag.toString("hex");
}
export function decrypt(text: string): string {
if (!ENCRYPTION_KEY) {
throw new Error("Missing environment variable: `ENCRYPTION_KEY`");
}
const [ivHex, encryptedHex, authTagHex] = text.split(":");
if (!ivHex || !encryptedHex || !authTagHex) {
throw new Error("Invalid or corrupted cipher format");
}
const iv = Buffer.from(ivHex, "hex");
const encryptedText = Buffer.from(encryptedHex, "hex");
const authTag = Buffer.from(authTagHex, "hex");
const decipher = crypto.createDecipheriv(
"aes-256-gcm",
Buffer.from(ENCRYPTION_KEY, "hex"),
iv,
);
decipher.setAuthTag(authTag);
let decrypted = decipher.update(encryptedText, undefined, "utf8");
decrypted += decipher.final("utf8");
return decrypted.toString();
}
export * from "./encryption";
export * from "./signature";
@@ -0,0 +1,41 @@
import crypto from "crypto";
// Generate a webhook secret key
export function generateWebhookSecret(): {
secretKey: string;
displaySecretKey: string;
} {
// Generate 32 random bytes and encode as hex (64 characters)
const rawSecret = crypto.randomBytes(32).toString("hex");
const secretKey = `lf-whsec_${rawSecret}`;
return { secretKey, displaySecretKey: getDisplaySecretKey(secretKey) };
}
// Create display version of webhook secret
export function getDisplaySecretKey(secretKey: string): string {
if (!secretKey || secretKey.length < 12) {
// whsec_ + at least 4 chars
return "****";
}
return `lf-whsec_...${secretKey.slice(-4)}`;
}
// Generate HMAC-SHA256 signature for webhook payload
export function generateWebhookSignature(
payload: string,
timestamp: number,
secret: string,
) {
const signedPayload = `${timestamp}.${payload}`;
return crypto
.createHmac("sha256", secret)
.update(signedPayload, "utf8")
.digest("hex");
}
export function createSignatureHeader(payload: string, secret: string): string {
const timestamp = Math.floor(Date.now() / 1000);
const signature = generateWebhookSignature(payload, timestamp, secret);
return `t=${timestamp},v1=${signature}`;
}
+37 -3
View File
@@ -16,6 +16,7 @@ const EnvSchema = z.object({
.nullable(),
REDIS_AUTH: z.string().nullish(),
REDIS_CONNECTION_STRING: z.string().nullish(),
REDIS_KEY_PREFIX: 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(),
@@ -31,8 +32,10 @@ const EnvSchema = z.object({
"ENCRYPTION_KEY must be 256 bits, 64 string characters in hex format, generate via: openssl rand -hex 32",
)
.optional(),
LANGFUSE_CACHE_PROMPT_ENABLED: z.enum(["true", "false"]).default("false"),
LANGFUSE_CACHE_PROMPT_TTL_SECONDS: z.coerce.number().default(60 * 60),
LANGFUSE_CACHE_MODEL_MATCH_ENABLED: z.enum(["true", "false"]).default("true"),
LANGFUSE_CACHE_MODEL_MATCH_TTL_SECONDS: z.coerce.number().default(86400), // 24 hours
LANGFUSE_CACHE_PROMPT_ENABLED: z.enum(["true", "false"]).default("true"),
LANGFUSE_CACHE_PROMPT_TTL_SECONDS: z.coerce.number().default(300), // 5 minutes
CLICKHOUSE_URL: z.string().url(),
CLICKHOUSE_CLUSTER_NAME: z.string().default("default"),
CLICKHOUSE_DB: z.string().default("default"),
@@ -105,10 +108,41 @@ const EnvSchema = z.object({
.number()
.default(80e6), // 80MB
LANGFUSE_CLICKHOUSE_DELETION_TIMEOUT_MS: z.coerce.number().default(240_000), // 4 minutes
LANGFUSE_CLICKHOUSE_QUERY_MAX_ATTEMPTS: z.coerce.number().default(3), // Maximum attempts for socket hang up errors
LANGFUSE_SKIP_S3_LIST_FOR_OBSERVATIONS_PROJECT_IDS: z.string().optional(),
// Dataset Run Items Migration Environment Variables
LANGFUSE_EXPERIMENT_DATASET_RUN_ITEMS_WRITE_CH: z
.enum(["true", "false"])
.default("false"),
LANGFUSE_EXPERIMENT_DATASET_RUN_ITEMS_READ_CH: z
.enum(["true", "false"])
.default("false"),
LANGFUSE_EXPERIMENT_COMPARE_READ_FROM_AGGREGATING_MERGE_TREES: z
.enum(["true", "false"])
.default("false"),
LANGFUSE_EXPERIMENT_ADD_QUERY_RESULT_TO_SPAN_PROJECT_IDS: z
.string()
.optional()
.transform((s) =>
s ? s.split(",").map((s) => s.toLowerCase().trim()) : [],
),
LANGFUSE_EXPERIMENT_SAMPLING_RATE: z.coerce
.number()
.min(0)
.max(1)
.default(0.1),
LANGFUSE_EXPERIMENT_WHITELISTED_PROJECT_IDS: z
.string()
.optional()
.transform((s) =>
s ? s.split(",").map((s) => s.toLowerCase().trim()) : [],
),
LANGFUSE_EXPERIMENT_RETURN_NEW_RESULT: z
.enum(["true", "false"])
.default("false"),
});
export const env: z.infer<typeof EnvSchema> =
process.env.DOCKER_BUILD === "1"
process.env.DOCKER_BUILD === "1" // eslint-disable-line turbo/no-undeclared-env-vars
? (process.env as any)
: EnvSchema.parse(removeEmptyEnvVariables(process.env));
+1 -1
View File
@@ -7,7 +7,7 @@ export class BaseError extends Error {
name: string,
httpCode: number,
description: string,
isOperational: boolean
isOperational: boolean,
) {
super(description);
Object.setPrototypeOf(this, new.target.prototype); // restore prototype chain
+1
View File
@@ -7,3 +7,4 @@ export { MethodNotAllowedError } from "./MethodNotAllowedError";
export { ApiError } from "./ApiError";
export { InternalServerError } from "./InternalServerError";
export { LangfuseConflictError } from "./ConflictError";
export { QUEUE_ERROR_MESSAGES } from "./utils/constants";
@@ -0,0 +1,10 @@
export const QUEUE_ERROR_MESSAGES = {
API_KEY_ERROR: "API key for provider",
NO_DEFAULT_MODEL_ERROR: "No default model or custom model found for project",
MAPPED_DATA_ERROR:
"Please ensure the mapped data exists and consider extending the job delay.",
INVALID_JSON_ERROR: "is not valid JSON",
TOO_LOW_MAX_TOKENS_ERROR: "Error: Unterminated string in JSON at position",
OUTPUT_TOKENS_TOO_LONG_ERROR:
"Could not parse response content as the length limit was reached",
};
@@ -1,5 +1,6 @@
import z from "zod/v4";
import { type ScoreDataType } from "../../db";
import { StringNoHTML, StringNoHTMLNonEmpty } from "../../utils/zod";
const NUMERIC: ScoreDataType = "NUMERIC";
const CATEGORICAL: ScoreDataType = "CATEGORICAL";
@@ -47,12 +48,12 @@ export type ScoreTargetSession = z.infer<typeof ScoreTargetSession>;
export type ScoreTarget = z.infer<typeof ScoreTarget>;
const CreateAnnotationScoreBase = z.object({
name: z.string(),
name: StringNoHTMLNonEmpty,
projectId: z.string(),
environment: z.string().default("default"),
scoreTarget: ScoreTarget,
configId: z.string().optional(),
comment: z.string().nullish(),
comment: StringNoHTML.nullish(),
queueId: z.string().nullish(),
});
@@ -83,8 +84,8 @@ export const UpdateAnnotationScoreData = z.discriminatedUnion("dataType", [
// annotation queues
export const CreateQueueData = z.object({
name: z.string().min(1).max(35),
description: z.string().max(1000).optional(),
name: StringNoHTMLNonEmpty.max(35),
description: StringNoHTML.max(1000).optional(),
scoreConfigIds: z.array(z.string()).min(1, {
message: "At least 1 score config must be selected",
}),
@@ -8,16 +8,16 @@ import { BatchTableNames } from "../../interfaces/tableNames";
import { TracingSearchType } from "../../interfaces/search";
export enum BatchExportStatus {
QUEUED = "QUEUED",
PROCESSING = "PROCESSING",
COMPLETED = "COMPLETED",
FAILED = "FAILED",
QUEUED = "QUEUED", // eslint-disable-line no-unused-vars
PROCESSING = "PROCESSING", // eslint-disable-line no-unused-vars
COMPLETED = "COMPLETED", // eslint-disable-line no-unused-vars
FAILED = "FAILED", // eslint-disable-line no-unused-vars
}
export enum BatchExportFileFormat {
JSON = "JSON",
CSV = "CSV",
JSONL = "JSONL",
JSON = "JSON", // eslint-disable-line no-unused-vars
CSV = "CSV", // eslint-disable-line no-unused-vars
JSONL = "JSONL", // eslint-disable-line no-unused-vars
}
// Use shared BatchTableNames enum for consistency across batch operations
@@ -42,7 +42,7 @@ function parseJsonDefault(selectedColumn: unknown, jsonSelector: string) {
export function extractValueFromObject(
obj: Record<string, unknown>,
mapping: z.infer<typeof variableMapping>,
parseJson?: (selectedColumn: unknown, jsonSelector: string) => unknown,
parseJson?: (selectedColumn: unknown, jsonSelector: string) => unknown, // eslint-disable-line no-unused-vars
): { value: string; error: Error | null } {
const selectedColumn = obj[mapping.selectedColumnId];
const jsonParser = parseJson || parseJsonDefault;
@@ -1,12 +1,12 @@
import { z } from "zod/v4";
import { StringNoHTMLNonEmpty } from "../../utils/zod";
/**
* Prompt name validation schema for API, tRPC and client
*/
export const PromptNameSchema = z
.string()
.min(1, "Enter a name")
.regex(/^[^|]*$/, "Prompt name cannot contain '|' character")
export const PromptNameSchema = StringNoHTMLNonEmpty.regex(
/^[^|]*$/,
"Prompt name cannot contain '|' character",
)
.regex(/^[^/]/, "Name cannot start with a slash")
.regex(/^(?!.*\/\/)/, "Name cannot contain consecutive slashes")
.regex(/^.*[^/]$/, "Name cannot end with a slash")
@@ -37,13 +37,13 @@ const ScorePropsAgainstConfigNumeric = z
dataType: z.literal("NUMERIC"),
})
.superRefine((data, ctx) => {
if (isPresent(data.maxValue) && data.value >= data.maxValue) {
if (isPresent(data.maxValue) && data.value > data.maxValue) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
message: `Value exceeds maximum value of ${data.maxValue} defined in config`,
});
}
if (isPresent(data.minValue) && data.value <= data.minValue) {
if (isPresent(data.minValue) && data.value < data.minValue) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
message: `Value is below minimum value of ${data.minValue} defined in config`,
@@ -150,7 +150,7 @@ const ValidatedScoreConfigSchema = z
*/
export const filterAndValidateDbScoreConfigList = (
scoreConfigs: ScoreConfigDbType[],
onParseError?: (error: z.ZodError) => void,
onParseError?: (error: z.ZodError) => void, // eslint-disable-line no-unused-vars
): ValidatedScoreConfig[] =>
scoreConfigs.reduce((acc, ts) => {
const result = ValidatedScoreConfigSchema.safeParse(ts);
+9 -1
View File
@@ -13,9 +13,11 @@ export * from "./utils/json";
export * from "./utils/stringChecks";
export * from "./utils/objects";
export * from "./utils/typeChecks";
export * from "./utils/prompts";
export * from "./features/entitlements/plans";
export * from "./interfaces/rate-limits";
export * from "./tableDefinitions/typeHelpers";
export * from "./domain/webhooks";
// llm api
export * from "./server/llm/types";
@@ -45,7 +47,13 @@ export * from "./features/prompts/parsePromptDependencyTags";
export * from "./features/prompts/validation";
export * from "./features/prompts/types";
export * from "./features/prompts/constants";
export * from "./server/llm/compileChatMessages";
export {
compileChatMessages,
compileChatMessagesWithIds,
isPlaceholder,
type MessagePlaceholderValues,
type PromptMessage as ServerPromptMessage,
} from "./server/llm/compileChatMessages";
// export db types only
export * from "@prisma/client";
@@ -1,5 +1,9 @@
import { z } from "zod/v4";
// Sentinel value for Bedrock default credential provider chain
export const BEDROCK_USE_DEFAULT_CREDENTIALS =
"__BEDROCK_DEFAULT_CREDENTIALS__";
export const BedrockConfigSchema = z.object({ region: z.string() });
export type BedrockConfig = z.infer<typeof BedrockConfigSchema>;
+7 -7
View File
@@ -4,11 +4,11 @@
* to avoid coupling between different batch operation types.
*/
export enum BatchTableNames {
Scores = "scores",
Sessions = "sessions",
Traces = "traces",
Observations = "observations",
DatasetRunItems = "dataset_run_items",
DatasetItems = "dataset_items",
AuditLogs = "audit_logs",
Scores = "scores", // eslint-disable-line no-unused-vars
Sessions = "sessions", // eslint-disable-line no-unused-vars
Traces = "traces", // eslint-disable-line no-unused-vars
Observations = "observations", // eslint-disable-line no-unused-vars
DatasetRunItems = "dataset_run_items", // eslint-disable-line no-unused-vars
DatasetItems = "dataset_items", // eslint-disable-line no-unused-vars
AuditLogs = "audit_logs", // eslint-disable-line no-unused-vars
}
@@ -12,9 +12,8 @@ interface CustomSSOUser extends Record<string, any> {
verified: boolean;
}
export function CustomSSOProvider<P extends CustomSSOUser>(
options: OAuthUserConfig<P>
options: OAuthUserConfig<P>,
): OAuthConfig<P> {
return {
id: "custom",
@@ -0,0 +1,98 @@
import { instrumentAsync } from "../instrumentation";
import * as opentelemetry from "@opentelemetry/api";
import { env } from "../../env";
import { logger } from "../logger";
const executionWrapper = async <T, Y>(
input: T,
fn: (input: T) => Promise<Y>, // eslint-disable-line no-unused-vars
span?: opentelemetry.Span,
attributePrefix?: string,
): Promise<[Y, number]> => {
const startTime = Date.now();
const res = await fn(input);
const duration = Date.now() - startTime;
span?.setAttribute(
`langfuse.experiment.amts.${attributePrefix}-duration`,
duration,
);
return [res, duration];
};
export const measureAndReturn = async <T, Y>(args: {
operationName: string;
projectId: string;
input: T;
existingExecution: (input: T) => Promise<Y>; // eslint-disable-line no-unused-vars
newExecution: (input: T) => Promise<Y>; // eslint-disable-line no-unused-vars
}): Promise<Y> => {
return instrumentAsync(
{
name: `experiment-${args.operationName}`,
spanKind: opentelemetry.SpanKind.CLIENT,
},
async (currentSpan) => {
const { input, existingExecution, newExecution } = args;
if (
env.LANGFUSE_EXPERIMENT_COMPARE_READ_FROM_AGGREGATING_MERGE_TREES !==
"true"
) {
currentSpan.setAttribute(`langfuse.experiment.amts.run`, "disabled");
return existingExecution(input);
}
// If not whitelisted, apply sampling logic
if (
!env.LANGFUSE_EXPERIMENT_WHITELISTED_PROJECT_IDS.includes(
args.projectId,
) &&
Math.random() > env.LANGFUSE_EXPERIMENT_SAMPLING_RATE
) {
currentSpan.setAttribute(`langfuse.experiment.amts.run`, "sampled-out");
return existingExecution(input);
}
currentSpan.setAttribute(`langfuse.experiment.amts.run`, "true");
try {
const [[existingResult, existingDuration], [newResult, newDuration]] =
await Promise.all([
executionWrapper(input, existingExecution, currentSpan, "existing"),
executionWrapper(input, newExecution, currentSpan, "new"),
]);
// Positive duration difference means new is faster
const durationDifference = existingDuration - newDuration;
currentSpan?.setAttribute(
"langfuse.experiment.amts.execution-time-difference",
durationDifference,
);
if (
env.LANGFUSE_EXPERIMENT_ADD_QUERY_RESULT_TO_SPAN_PROJECT_IDS.some(
(p) => p === args.projectId,
)
) {
currentSpan?.setAttribute(
"langfuse.experiment.amts.existing-result",
JSON.stringify(existingResult),
);
currentSpan?.setAttribute(
"langfuse.experiment.amts.new-result",
JSON.stringify(newResult),
);
}
return env.LANGFUSE_EXPERIMENT_RETURN_NEW_RESULT === "true"
? newResult
: existingResult;
} catch (e) {
logger.error(
"Failed to run experiment wrapper. Retrying existing query",
e,
);
return existingExecution(input);
}
},
);
};
@@ -11,7 +11,8 @@ export type IngestionEntityTypes =
| "trace"
| "observation"
| "score"
| "sdk_log";
| "sdk_log"
| "dataset_run_item";
export const getClickhouseEntityType = (
eventType: string,
@@ -29,6 +30,8 @@ export const getClickhouseEntityType = (
return "observation";
case eventTypes.SCORE_CREATE:
return "score";
case eventTypes.DATASET_RUN_ITEM_CREATE:
return "dataset_run_item";
case eventTypes.SDK_LOG:
return "sdk_log";
default:
@@ -0,0 +1,94 @@
import { env } from "../../env";
import { logger } from "../../server/logger";
import {
DatasetRunItemsExecutionStrategy,
DatasetRunItemsOperationType,
} from "./types";
/**
* Returns the execution strategy for dataset run items based on environment variables.
*
* Two-phase migration approach:
* 1. Dual-write phase: DATASET_RUN_ITEMS_WRITE_TO_CLICKHOUSE=true (write to both databases)
* 2. Read migration phase: DATASET_RUN_ITEMS_READ_FROM_CLICKHOUSE=true (read from ClickHouse)
*/
function getDatasetRunItemsExecutionStrategy(): DatasetRunItemsExecutionStrategy {
return {
shouldWriteToClickHouse:
env.LANGFUSE_EXPERIMENT_DATASET_RUN_ITEMS_WRITE_CH === "true",
shouldReadFromClickHouse:
env.LANGFUSE_EXPERIMENT_DATASET_RUN_ITEMS_READ_CH === "true",
};
}
// Re-export the enum for backward compatibility
/**
* Executes the appropriate database operation based on the execution strategy.
*
* @param postgresExecution - Function to execute PostgreSQL operation
* @param clickhouseExecution - Function to execute ClickHouse operation
* @param operationType - Type of operation ("read" or "write")
* @returns Result from the selected execution strategy
*/
export async function executeWithDatasetRunItemsStrategy<TInput, TOutput>({
input,
operationType,
postgresExecution,
clickhouseExecution,
}: {
input: TInput;
operationType: DatasetRunItemsOperationType;
// eslint-disable-next-line no-unused-vars
postgresExecution: (input: TInput) => Promise<TOutput>;
// eslint-disable-next-line no-unused-vars
clickhouseExecution: (input: TInput) => Promise<TOutput>;
}): Promise<TOutput> {
const strategy = getDatasetRunItemsExecutionStrategy();
if (operationType === DatasetRunItemsOperationType.WRITE) {
// For write operations, implement dual-write strategy
if (strategy.shouldWriteToClickHouse) {
// Dual-write phase: write to both databases
const postgresResult = await postgresExecution(input);
try {
await clickhouseExecution(input);
logger.debug("Successfully wrote to both PostgreSQL and ClickHouse", {
operation: `dataset_run_items_${operationType}`,
});
} catch (error) {
logger.error("ClickHouse write failed during dual-write phase", {
error: error instanceof Error ? error.message : String(error),
operation: `dataset_run_items_${operationType}`,
});
// Continue with PostgreSQL result since it succeeded
}
return postgresResult;
} else {
// Write only to PostgreSQL
return await postgresExecution(input);
}
} else {
// For read operations, rely on the strategy
const shouldExecuteClickhouse = strategy.shouldReadFromClickHouse;
if (shouldExecuteClickhouse) {
try {
return await clickhouseExecution(input);
} catch (error) {
logger.error(
"ClickHouse execution failed, falling back to PostgreSQL",
{
error: error instanceof Error ? error.message : String(error),
operation: `dataset_run_items_${operationType}`,
},
);
// Fallback to PostgreSQL for reliability
return await postgresExecution(input);
}
} else {
return await postgresExecution(input);
}
}
}
@@ -0,0 +1,16 @@
/**
* Types and enums for dataset run items execution.
* This file is frontend-safe and doesn't import server-side dependencies.
*/
export enum DatasetRunItemsOperationType {
// eslint-disable-next-line no-unused-vars
READ = "read",
// eslint-disable-next-line no-unused-vars
WRITE = "write",
}
export type DatasetRunItemsExecutionStrategy = {
shouldWriteToClickHouse: boolean;
shouldReadFromClickHouse: boolean;
};
+10 -2
View File
@@ -3,6 +3,7 @@ export * from "./services/email/organizationInvitation/sendMembershipInvitationE
export * from "./services/email/batchExportSuccess/sendBatchExportSuccessEmail";
export * from "./services/email/passwordReset/sendResetPasswordVerificationRequest";
export * from "./services/PromptService";
export * from "./services/PromptService/types";
export * from "./services/traces-ui-table-service";
export * from "./services/InMemoryFilterService";
export * from "./auth/apiKeys";
@@ -19,14 +20,15 @@ export * from "./clickhouse/schemaUtils";
export * from "./clickhouse/schema";
export * from "./repositories/definitions";
export * from "../server/ingestion/types";
export * from "../server/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/webhookQueue";
export * from "./redis/traceDelete";
export * from "./redis/projectDelete";
export * from "./redis/scoreDelete";
@@ -44,6 +46,7 @@ export * from "./redis/coreDataS3ExportQueue";
export * from "./redis/meteringDataPostgresExportQueue";
export * from "./redis/experimentCreateQueue";
export * from "./redis/dlqRetryQueue";
export * from "./redis/entityChangeQueue";
export * from "./auth/types";
export * from "./queues";
export * from "./orderByToPrisma";
@@ -59,9 +62,14 @@ export * from "./services/datasets-ui-table-service";
export * from "./services/DashboardService";
export * from "./services/TableViewService";
export * from "./services/DefaultEvaluationModelService";
export * from "./clickhouse/measureAndReturn";
export * from "./data-deletion/ingestionFileDeletion";
export * from "./s3";
// dataset run items
export * from "./dataset-run-items/datasetExecution";
export * from "./dataset-run-items/types";
// test utils
export * from "./test-utils";
export * from "./utils/headerUtils";
@@ -1,13 +1,15 @@
import { Model, Prisma } from "@langfuse/shared";
import { Model, Prisma } from "../../";
import {
instrumentAsync,
logger,
recordIncrement,
} from "@langfuse/shared/src/server";
import { env } from "../env";
import { redis } from "@langfuse/shared/src/server";
redis,
safeMultiDel,
} from "../";
import { type Cluster } from "ioredis";
import { env } from "../../env";
import { Decimal } from "decimal.js";
import { prisma } from "@langfuse/shared/src/db";
import { prisma } from "../../db";
export type ModelMatchProps = {
projectId: string;
@@ -178,6 +180,11 @@ export const getRedisModelKey = (p: ModelMatchProps) => {
};
const getModelMatchKeyPrefix = () => {
if (env.REDIS_CLUSTER_ENABLED === "true") {
// Use hash tags for Redis cluster compatibility
// This ensures all model cache keys are placed on the same hash slot
return "{model-match}";
}
return "model-match";
};
@@ -205,3 +212,35 @@ export const redisModelToPrismaModel = (redisModel: string): Model => {
: null,
};
};
export async function clearModelCacheForProject(
projectId: string,
): Promise<void> {
if (env.LANGFUSE_CACHE_MODEL_MATCH_ENABLED === "false" || !redis) {
return;
}
try {
const pattern = `${getModelMatchKeyPrefix()}:${projectId}:*`;
const keys =
env.REDIS_CLUSTER_ENABLED === "true"
? (
await Promise.all(
(redis as Cluster)
.nodes("master")
.map((node) => node.keys(pattern) || []),
)
).flat()
: await redis.keys(pattern);
if (keys.length > 0) {
await safeMultiDel(redis, keys);
logger.info(
`Cleared ${keys.length} model cache entries for project ${projectId}`,
);
}
} catch (error) {
logger.error(`Error clearing model cache for project ${projectId}`, error);
}
}
@@ -21,7 +21,11 @@ import { logger } from "../logger";
import { QueueJobs } from "../queues";
import { IngestionQueue } from "../redis/ingestionQueue";
import { redis } from "../redis/redis";
import { eventTypes, ingestionEvent, IngestionEventType } from "./types";
import {
eventTypes,
createIngestionEventSchema,
IngestionEventType,
} from "./types";
import {
StorageService,
StorageServiceFactory,
@@ -45,6 +49,7 @@ const getS3StorageServiceClient = (bucketName: string): StorageService => {
return s3StorageServiceClient;
};
// eslint-disable-next-line no-unused-vars
export type TokenCountDelegate = (p: {
model: Model;
text: unknown;
@@ -56,7 +61,7 @@ export type TokenCountDelegate = (p: {
* We need the delay around date boundaries to avoid duplicates for out-of-order processing of events.
* @param delay - Delay overwrite. Used if non-null.
*/
const getDelay = (delay: number | null) => {
const getDelay = (delay: number | null, source: "api" | "otel") => {
if (delay !== null) {
return delay;
}
@@ -68,24 +73,38 @@ const getDelay = (delay: number | null) => {
return env.LANGFUSE_INGESTION_QUEUE_DELAY_MS;
}
if (source === "otel") {
return 0;
}
// Use 5s here to avoid duplicate processing on the worker. If the ingestion delay is set to a lower value,
// we use this instead.
// Values should be revisited based on a cost/performance trade-off.
return Math.min(5000, env.LANGFUSE_INGESTION_QUEUE_DELAY_MS);
};
/**
* Options for event batch processing.
* @property delay - Delay in ms to wait before processing events in the batch.
* @property source - Source of the events for metrics tracking (e.g., "otel", "api").
* @property isLangfuseInternal - Whether the events are being ingested by Langfuse internally (e.g. traces created for prompt experiments).
*/
type ProcessEventBatchOptions = {
delay?: number | null;
source?: "api" | "otel";
isLangfuseInternal?: boolean;
};
/**
* Processes a batch of events.
* @param input - Batch of IngestionEventType. Will validate the types first thing and return errors if they are invalid.
* @param authCheck - AuthHeaderValidVerificationResult
* @param delay - (Optional) Delay in ms to wait before processing events in the batch.
* @param source - (Optional) Source of the events for metrics tracking (e.g., "otel", "api").
* @param options - (Optional) Options for the event batch processing.
*/
export const processEventBatch = async (
input: unknown[],
authCheck: AuthHeaderValidVerificationResult,
delay: number | null = null,
source: "api" | "otel" = "api",
options: ProcessEventBatchOptions = {},
): Promise<{
successes: { id: string; status: number }[];
errors: {
@@ -95,6 +114,8 @@ export const processEventBatch = async (
error?: string;
}[];
}> => {
const { delay = null, source = "api", isLangfuseInternal = false } = options;
// add context of api call to the span
const currentSpan = getCurrentSpan();
recordIncrement("langfuse.ingestion.event", input.length, { source });
@@ -120,9 +141,10 @@ export const processEventBatch = async (
const validationErrors: { id: string; error: unknown }[] = [];
const authenticationErrors: { id: string; error: unknown }[] = [];
const batch: z.infer<typeof ingestionEvent>[] = input
const ingestionSchema = createIngestionEventSchema(isLangfuseInternal);
const batch: z.infer<typeof ingestionSchema>[] = input
.flatMap((event) => {
const parsed = ingestionEvent.safeParse(event);
const parsed = ingestionSchema.safeParse(event);
if (!parsed.success) {
validationErrors.push({
id:
@@ -238,11 +260,18 @@ export const processEventBatch = async (
const shardingKey = `${authCheck.scope.projectId}-${eventData.eventBodyId}`;
const queue = IngestionQueue.getInstance({ shardingKey });
const shouldSkipS3List =
getClickhouseEntityType(eventData.type) === "observation" &&
const isDatasetRunItemEvent =
getClickhouseEntityType(eventData.type) === "dataset_run_item";
const isObservationEvent =
getClickhouseEntityType(eventData.type) === "observation";
const isOtelOrSkipS3Project =
authCheck.scope.projectId !== null &&
(projectIdsToSkipS3List.includes(authCheck.scope.projectId) ||
source === "otel");
(source === "otel" ||
projectIdsToSkipS3List.includes(authCheck.scope.projectId));
const shouldSkipS3List =
isDatasetRunItemEvent || (isObservationEvent && isOtelOrSkipS3Project);
return queue
? queue.add(
@@ -267,7 +296,7 @@ export const processEventBatch = async (
},
},
},
{ delay: getDelay(delay) },
{ delay: getDelay(delay, source) },
)
: Promise.reject("Failed to instantiate queue");
}),
@@ -301,7 +330,7 @@ const isAuthorized = (
/**
* Sorts a batch of ingestion events. Orders by: updating events last, sorted by timestamp asc.
*/
const sortBatch = (batch: Array<z.infer<typeof ingestionEvent>>) => {
const sortBatch = (batch: IngestionEventType[]) => {
const updateEvents: (typeof eventTypes)[keyof typeof eventTypes][] = [
eventTypes.GENERATION_UPDATE,
eventTypes.SPAN_UPDATE,
+386 -239
View File
@@ -193,178 +193,42 @@ export const UsageDetails = z
])
.nullish();
export const EnvironmentName = z
const INTERNAL_ENVIRONMENT_NAME_REGEX_ERROR_MESSAGE =
"Only alphanumeric lower case characters, hyphens, and underscores are allowed";
const ENVIRONMENT_NAME_REGEX_ERROR_MESSAGE =
INTERNAL_ENVIRONMENT_NAME_REGEX_ERROR_MESSAGE +
" and it must not start with 'langfuse'";
const PublicEnvironmentName = 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'",
)
.regex(/^(?!langfuse)[a-z0-9-_]+$/, ENVIRONMENT_NAME_REGEX_ERROR_MESSAGE)
.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: idSchema.nullish(),
timestamp: stringDateTime,
name: z.string().max(1000).nullish(),
externalId: z.string().nullish(),
input: z.any().nullish(),
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(),
public: z.boolean().nullish(),
tags: z.array(z.string()).nullish(),
});
const InternalEnvironmentName = z
.string()
.max(40, "Maximum length is 40 characters")
.regex(/^[a-z0-9-_]+$/, INTERNAL_ENVIRONMENT_NAME_REGEX_ERROR_MESSAGE)
.default("default");
export const OptionalObservationBody = z.object({
traceId: idSchema.nullish(),
environment: EnvironmentName,
name: z.string().nullish(),
startTime: stringDateTime,
metadata: jsonSchema.nullish(),
input: z.any().nullish(),
output: z.any().nullish(),
level: ObservationLevel.nullish(),
statusMessage: z.string().nullish(),
parentObservationId: z.string().nullish(),
version: z.string().nullish(),
});
/** @deprecated Use PublicEnvironmentName or InternalEnvironmentName instead */
export const EnvironmentName = PublicEnvironmentName;
export const CreateEventEvent = OptionalObservationBody.extend({
id: idSchema,
});
export const UpdateEventEvent = OptionalObservationBody.extend({
id: idSchema,
});
export const CreateSpanBody = CreateEventEvent.extend({
endTime: stringDateTime,
});
export const UpdateSpanBody = UpdateEventEvent.extend({
endTime: stringDateTime,
});
export const CreateGenerationBody = CreateSpanBody.extend({
completionStartTime: stringDateTime,
model: z.string().nullish(),
modelParameters: z
.record(
z.string(),
z
.union([
z.string(),
z.number(),
z.boolean(),
z.array(z.string()),
z.record(z.string(), z.string()),
])
.nullish(),
)
.nullish(),
usage: usage,
usageDetails: UsageDetails,
costDetails: CostDetails,
promptName: z.string().nullish(),
promptVersion: z.number().int().nullish(),
}).refine((value) => {
// ensure that either promptName and promptVersion are set, or none
if (!value.promptName && !value.promptVersion) return true;
if (value.promptName && value.promptVersion) return true;
return false;
});
export const UpdateGenerationBody = UpdateSpanBody.extend({
completionStartTime: stringDateTime,
model: z.string().nullish(),
modelParameters: z
.record(
z.string(),
z
.union([
z.string(),
z.number(),
z.boolean(),
z.array(z.string()),
z.record(z.string(), z.string()),
])
.nullish(),
)
.nullish(),
usage: usage,
usageDetails: UsageDetails,
costDetails: CostDetails,
promptName: z.string().nullish(),
promptVersion: z.number().int().nullish(),
}).refine((value) => {
// ensure that either promptName and promptVersion are set, or none
if (!value.promptName && !value.promptVersion) return true;
if (value.promptName && value.promptVersion) return true;
return false;
});
const BaseScoreBody = z.object({
id: idSchema.nullish(),
name: NonEmptyString,
traceId: z.string().nullish(),
sessionId: z.string().nullish(),
datasetRunId: z.string().nullish(),
environment: EnvironmentName,
observationId: z.string().nullish(),
comment: z.string().nullish(),
metadata: jsonSchema.nullish(),
source: z
.enum(["API", "EVAL", "ANNOTATION"])
.default("API" as ScoreSourceType),
});
/**
* ScoreBody exactly mirrors `PostScoresBody` in the public API. Please refer there for source of truth.
*/
export const ScoreBody = applyScoreValidation(
z.discriminatedUnion("dataType", [
BaseScoreBody.merge(
z.object({
value: z.number(),
dataType: z.literal("NUMERIC"),
configId: z.string().nullish(),
}),
),
BaseScoreBody.merge(
z.object({
value: z.string(),
dataType: z.literal("CATEGORICAL"),
configId: z.string().nullish(),
}),
),
BaseScoreBody.merge(
z.object({
value: z.number().refine((value) => value === 0 || value === 1, {
message:
"Value must be a number equal to either 0 or 1 for data type BOOLEAN",
}),
dataType: z.literal("BOOLEAN"),
configId: z.string().nullish(),
}),
),
BaseScoreBody.merge(
z.object({
value: z.union([z.string(), z.number()]),
dataType: z.undefined(),
configId: z.string().nullish(),
}),
),
]),
);
export const eventTypes = {
TRACE_CREATE: "trace-create",
SCORE_CREATE: "score-create",
EVENT_CREATE: "event-create",
SPAN_CREATE: "span-create",
SPAN_UPDATE: "span-update",
GENERATION_CREATE: "generation-create",
GENERATION_UPDATE: "generation-update",
SDK_LOG: "sdk-log",
DATASET_RUN_ITEM_CREATE: "dataset-run-item-create",
// LEGACY, only required for backwards compatibility
OBSERVATION_CREATE: "observation-create",
OBSERVATION_UPDATE: "observation-update",
} as const;
// LEGACY, only required for backwards compatibility
export const LegacySpanPostSchema = z.object({
@@ -475,83 +339,366 @@ export const SdkLogEvent = z.object({
id: z.string().nullish(), // Not used, but makes downstream processing easier.
});
export const eventTypes = {
TRACE_CREATE: "trace-create",
SCORE_CREATE: "score-create",
EVENT_CREATE: "event-create",
SPAN_CREATE: "span-create",
SPAN_UPDATE: "span-update",
GENERATION_CREATE: "generation-create",
GENERATION_UPDATE: "generation-update",
SDK_LOG: "sdk-log",
// LEGACY, only required for backwards compatibility
OBSERVATION_CREATE: "observation-create",
OBSERVATION_UPDATE: "observation-update",
} as const;
// 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.
const base = z.object({
id: idSchema,
timestamp: z.string().datetime({ offset: true }),
metadata: jsonSchema.nullish(),
});
export const traceEvent = base.extend({
type: z.literal(eventTypes.TRACE_CREATE),
body: TraceBody,
});
export type TraceEventType = z.infer<typeof traceEvent>;
// Complete schema factory - single source of truth for ALL schemas
const createAllIngestionSchemas = ({
isPublic = true,
}: {
isPublic: boolean;
}) => {
const environmentSchema = isPublic
? PublicEnvironmentName
: InternalEnvironmentName;
export const eventCreateEvent = base.extend({
type: z.literal(eventTypes.EVENT_CREATE),
body: CreateEventEvent,
});
export const spanCreateEvent = base.extend({
type: z.literal(eventTypes.SPAN_CREATE),
body: CreateSpanBody,
});
export const spanUpdateEvent = base.extend({
type: z.literal(eventTypes.SPAN_UPDATE),
body: UpdateSpanBody,
});
export const generationCreateEvent = base.extend({
type: z.literal(eventTypes.GENERATION_CREATE),
body: CreateGenerationBody,
});
export const generationUpdateEvent = base.extend({
type: z.literal(eventTypes.GENERATION_UPDATE),
body: UpdateGenerationBody,
});
export const scoreEvent = base.extend({
type: z.literal(eventTypes.SCORE_CREATE),
body: ScoreBody,
});
export type ScoreEventType = z.infer<typeof scoreEvent>;
export const sdkLogEvent = base.extend({
type: z.literal(eventTypes.SDK_LOG),
body: SdkLogEvent,
});
export const legacyObservationCreateEvent = base.extend({
type: z.literal(eventTypes.OBSERVATION_CREATE),
body: LegacyObservationBody,
});
export const legacyObservationUpdateEvent = base.extend({
type: z.literal(eventTypes.OBSERVATION_UPDATE),
body: LegacyObservationBody,
});
// Base schemas with environment
const TraceBody = z.object({
id: idSchema.nullish(),
timestamp: stringDateTime,
name: z.string().max(1000).nullish(),
externalId: z.string().nullish(),
input: z.any().nullish(),
output: z.any().nullish(),
sessionId: z.string().nullish(),
userId: z.string().nullish(),
environment: environmentSchema,
metadata: jsonSchema.nullish(),
release: z.string().nullish(),
version: z.string().nullish(),
public: z.boolean().nullish(),
tags: z.array(z.string()).nullish(),
});
export const ingestionEvent = z.discriminatedUnion("type", [
traceEvent,
scoreEvent,
eventCreateEvent,
spanCreateEvent,
spanUpdateEvent,
generationCreateEvent,
generationUpdateEvent,
sdkLogEvent,
// LEGACY, only required for backwards compatibility
legacyObservationCreateEvent,
legacyObservationUpdateEvent,
]);
const OptionalObservationBody = z.object({
traceId: idSchema.nullish(),
environment: environmentSchema,
name: z.string().nullish(),
startTime: stringDateTime,
metadata: jsonSchema.nullish(),
input: z.any().nullish(),
output: z.any().nullish(),
level: ObservationLevel.nullish(),
statusMessage: z.string().nullish(),
parentObservationId: z.string().nullish(),
version: z.string().nullish(),
});
// Derivative schemas
const CreateEventEvent = OptionalObservationBody.extend({
id: idSchema,
});
const UpdateEventEvent = OptionalObservationBody.extend({
id: idSchema,
});
const CreateSpanBody = CreateEventEvent.extend({
endTime: stringDateTime,
});
const UpdateSpanBody = UpdateEventEvent.extend({
endTime: stringDateTime,
});
const CreateGenerationBody = CreateSpanBody.extend({
completionStartTime: stringDateTime,
model: z.string().nullish(),
modelParameters: z
.record(
z.string(),
z
.union([
z.string(),
z.number(),
z.boolean(),
z.array(z.string()),
z.record(z.string(), z.string()),
])
.nullish(),
)
.nullish(),
usage: usage,
usageDetails: UsageDetails,
costDetails: CostDetails,
promptName: z.string().nullish(),
promptVersion: z.number().int().nullish(),
}).refine((value) => {
// ensure that either promptName and promptVersion are set, or none
if (!value.promptName && !value.promptVersion) return true;
if (value.promptName && value.promptVersion) return true;
return false;
});
const UpdateGenerationBody = UpdateSpanBody.extend({
completionStartTime: stringDateTime,
model: z.string().nullish(),
modelParameters: z
.record(
z.string(),
z
.union([
z.string(),
z.number(),
z.boolean(),
z.array(z.string()),
z.record(z.string(), z.string()),
])
.nullish(),
)
.nullish(),
usage: usage,
usageDetails: UsageDetails,
costDetails: CostDetails,
promptName: z.string().nullish(),
promptVersion: z.number().int().nullish(),
}).refine((value) => {
// ensure that either promptName and promptVersion are set, or none
if (!value.promptName && !value.promptVersion) return true;
if (value.promptName && value.promptVersion) return true;
return false;
});
const BaseScoreBody = z.object({
id: idSchema.nullish(),
name: NonEmptyString,
traceId: z.string().nullish(),
sessionId: z.string().nullish(),
datasetRunId: z.string().nullish(),
environment: environmentSchema,
observationId: z.string().nullish(),
comment: z.string().nullish(),
metadata: jsonSchema.nullish(),
source: z
.enum(["API", "EVAL", "ANNOTATION"])
.default("API" as ScoreSourceType),
});
const ScoreBody = applyScoreValidation(
z.discriminatedUnion("dataType", [
BaseScoreBody.merge(
z.object({
value: z.number(),
dataType: z.literal("NUMERIC"),
configId: z.string().nullish(),
}),
),
BaseScoreBody.merge(
z.object({
value: z.string(),
dataType: z.literal("CATEGORICAL"),
configId: z.string().nullish(),
}),
),
BaseScoreBody.merge(
z.object({
value: z.number().refine((value) => value === 0 || value === 1, {
message:
"Value must be a number equal to either 0 or 1 for data type BOOLEAN",
}),
dataType: z.literal("BOOLEAN"),
configId: z.string().nullish(),
}),
),
BaseScoreBody.merge(
z.object({
value: z.union([z.string(), z.number()]),
dataType: z.undefined(),
configId: z.string().nullish(),
}),
),
]),
);
const DatasetRunItemBody = z.object({
// Core identifiers
id: idSchema.nullish(),
traceId: z.string(),
observationId: z.string().nullish(),
error: z.string().nullish(),
// Metadata (optional)
createdAt: stringDateTime.nullish(),
// Dataset identification
datasetId: z.string(),
// Run identification
runId: z.string(),
// Dataset item identification
datasetItemId: z.string(),
});
// Event schemas
const base = z.object({
id: idSchema,
timestamp: z.string().datetime({ offset: true }),
metadata: jsonSchema.nullish(),
});
const traceEvent = base.extend({
type: z.literal(eventTypes.TRACE_CREATE),
body: TraceBody,
});
const eventCreateEvent = base.extend({
type: z.literal(eventTypes.EVENT_CREATE),
body: CreateEventEvent,
});
const spanCreateEvent = base.extend({
type: z.literal(eventTypes.SPAN_CREATE),
body: CreateSpanBody,
});
const spanUpdateEvent = base.extend({
type: z.literal(eventTypes.SPAN_UPDATE),
body: UpdateSpanBody,
});
const generationCreateEvent = base.extend({
type: z.literal(eventTypes.GENERATION_CREATE),
body: CreateGenerationBody,
});
const generationUpdateEvent = base.extend({
type: z.literal(eventTypes.GENERATION_UPDATE),
body: UpdateGenerationBody,
});
const scoreEvent = base.extend({
type: z.literal(eventTypes.SCORE_CREATE),
body: ScoreBody,
});
const baseDatasetRunItemCreateEvent = base.extend({
type: z.literal(eventTypes.DATASET_RUN_ITEM_CREATE),
body: DatasetRunItemBody,
});
const datasetRunItemCreateEvent = isPublic
? baseDatasetRunItemCreateEvent.refine(() => false, {
message: "Dataset run item creation is only allowed for internal usage",
})
: baseDatasetRunItemCreateEvent;
const sdkLogEvent = base.extend({
type: z.literal(eventTypes.SDK_LOG),
body: SdkLogEvent,
});
const legacyObservationCreateEvent = base.extend({
type: z.literal(eventTypes.OBSERVATION_CREATE),
body: LegacyObservationBody,
});
const legacyObservationUpdateEvent = base.extend({
type: z.literal(eventTypes.OBSERVATION_UPDATE),
body: LegacyObservationBody,
});
const ingestionEvent = z.discriminatedUnion("type", [
traceEvent,
scoreEvent,
eventCreateEvent,
spanCreateEvent,
spanUpdateEvent,
generationCreateEvent,
generationUpdateEvent,
sdkLogEvent,
datasetRunItemCreateEvent,
// LEGACY, only required for backwards compatibility
legacyObservationCreateEvent,
legacyObservationUpdateEvent,
]);
return {
// Body schemas
TraceBody,
OptionalObservationBody,
CreateEventEvent,
UpdateEventEvent,
CreateSpanBody,
UpdateSpanBody,
CreateGenerationBody,
UpdateGenerationBody,
BaseScoreBody,
ScoreBody,
// Event schemas
traceEvent,
eventCreateEvent,
spanCreateEvent,
spanUpdateEvent,
generationCreateEvent,
generationUpdateEvent,
scoreEvent,
datasetRunItemCreateEvent,
sdkLogEvent,
legacyObservationCreateEvent,
legacyObservationUpdateEvent,
// Complete schema
ingestionEvent,
};
};
// Create both public and internal schema instances
const publicSchemas = createAllIngestionSchemas({ isPublic: true });
const internalSchemas = createAllIngestionSchemas({ isPublic: false });
// Export individual schemas for backwards compatibility
export const TraceBody = publicSchemas.TraceBody;
export const OptionalObservationBody = publicSchemas.OptionalObservationBody;
export const CreateEventEvent = publicSchemas.CreateEventEvent;
export const UpdateEventEvent = publicSchemas.UpdateEventEvent;
export const CreateSpanBody = publicSchemas.CreateSpanBody;
export const UpdateSpanBody = publicSchemas.UpdateSpanBody;
export const CreateGenerationBody = publicSchemas.CreateGenerationBody;
export const UpdateGenerationBody = publicSchemas.UpdateGenerationBody;
export const BaseScoreBody = publicSchemas.BaseScoreBody;
/**
* ScoreBody exactly mirrors `PostScoresBody` in the public API. Please refer there for source of truth.
*/
export const ScoreBody = publicSchemas.ScoreBody;
// Export individual event schemas for backwards compatibility
export const traceEvent = publicSchemas.traceEvent;
export const eventCreateEvent = publicSchemas.eventCreateEvent;
export const spanCreateEvent = publicSchemas.spanCreateEvent;
export const spanUpdateEvent = publicSchemas.spanUpdateEvent;
export const generationCreateEvent = publicSchemas.generationCreateEvent;
export const generationUpdateEvent = publicSchemas.generationUpdateEvent;
export const scoreEvent = publicSchemas.scoreEvent;
export const sdkLogEvent = publicSchemas.sdkLogEvent;
export const datasetRunItemCreateEvent =
publicSchemas.datasetRunItemCreateEvent;
export const legacyObservationCreateEvent =
publicSchemas.legacyObservationCreateEvent;
export const legacyObservationUpdateEvent =
publicSchemas.legacyObservationUpdateEvent;
/** @deprecated Use createIngestionEventSchema() instead */
export const ingestionEvent = publicSchemas.ingestionEvent;
/**
* Type definitions for both schema variants (public and internal).
* These types are equivalent to the return types of createIngestionEventSchema() and all exported schemas,
* since the factory patterns only differ in environment validation rules, not in the actual TypeScript types.
* The environment field remains `string` in all cases - only the validation logic differs.
*/
export type IngestionEventType = z.infer<typeof ingestionEvent>;
export type TraceEventType = z.infer<typeof traceEvent>;
export type ScoreEventType = z.infer<typeof scoreEvent>;
export type DatasetRunItemEventType = z.infer<typeof datasetRunItemCreateEvent>;
/**
* Creates an ingestion event schema with appropriate environment validation.
* @param isLangfuseInternal - Whether the events are being ingested by Langfuse internally (e.g. traces created for prompt experiments).
* @returns The ingestion event schema.
*/
export const createIngestionEventSchema = (isLangfuseInternal = false) => {
return isLangfuseInternal
? internalSchemas.ingestionEvent
: publicSchemas.ingestionEvent;
};
export type ObservationEvent =
| z.infer<typeof legacyObservationCreateEvent>
@@ -22,7 +22,7 @@ export type SpanCtx = {
traceContext?: TCarrier;
};
type AsyncCallbackFn<T> = (span: opentelemetry.Span) => Promise<T>;
type AsyncCallbackFn<T> = (span: opentelemetry.Span) => Promise<T>; // eslint-disable-line no-unused-vars
export async function instrumentAsync<T>(
ctx: SpanCtx,
@@ -64,7 +64,7 @@ export async function instrumentAsync<T>(
);
}
type SyncCallbackFn<T> = (span: opentelemetry.Span) => T;
type SyncCallbackFn<T> = (span: opentelemetry.Span) => T; // eslint-disable-line no-unused-vars
export function instrumentSync<T>(
ctx: SpanCtx,
@@ -1,21 +1,26 @@
import { z } from "zod/v4";
import { v4 as uuidv4 } from "uuid";
import { type ChatMessage, type PlaceholderMessage, ChatMessageType, type PromptChatMessageSchema, type ChatMessageWithId, type ChatMessageWithIdNoPlaceholders, ChatMessageSchema } from "./types";
import {
type ChatMessage,
type PlaceholderMessage,
ChatMessageType,
type PromptChatMessageSchema,
type ChatMessageWithId,
type ChatMessageWithIdNoPlaceholders,
} from "./types";
export type MessagePlaceholderValues = Record<string, ChatMessage[]>;
export type MessagePlaceholderValues = Record<string, unknown[]>;
export type PromptMessage = z.infer<typeof PromptChatMessageSchema>;
export function isPlaceholder(message: PromptMessage): message is PlaceholderMessage {
export function isPlaceholder(
message: PromptMessage,
): message is PlaceholderMessage {
return "type" in message && message.type === ChatMessageType.Placeholder;
}
function validateMessage(message: unknown): message is ChatMessage {
return ChatMessageSchema.safeParse(message).success;
}
function replaceTextVariables(
content: string,
textVariables: Record<string, string>
textVariables: Record<string, string>,
): string {
let result = content;
for (const [varName, varValue] of Object.entries(textVariables)) {
@@ -28,35 +33,44 @@ function replaceTextVariables(
function expandPlaceholder(
placeholder: PlaceholderMessage,
placeholderValues: MessagePlaceholderValues
placeholderValues: MessagePlaceholderValues,
): ChatMessage[] {
const replacementMessages = placeholderValues[placeholder.name];
if (!replacementMessages) {
throw new Error(`Missing value for message placeholder: ${placeholder.name}`);
throw new Error(
`Missing value for message placeholder: ${placeholder.name}`,
);
}
if (!Array.isArray(replacementMessages)) {
throw new Error(`Placeholder value for '${placeholder.name}' must be an array of messages`);
throw new Error(
`Placeholder value for '${placeholder.name}' must be an array of messages`,
);
}
for (const replacementMsg of replacementMessages) {
if (!validateMessage(replacementMsg)) {
throw new Error(`Invalid message format in placeholder '${placeholder.name}': messages must have 'role' and 'content' properties`);
// Allow arbitrary objects - just pass them through as ChatMessage
// Users might want to use ChatML with placeholders for any message structure
return replacementMessages.map((replacementMsg) => {
if (typeof replacementMsg === "object" && replacementMsg !== null) {
return replacementMsg as ChatMessage;
}
}
return replacementMessages;
throw new Error(
`Invalid message in placeholder '${placeholder.name}': expected object but got ${typeof replacementMsg}`,
);
});
}
export function compileChatMessages(
messages: PromptMessage[],
placeholderValues: MessagePlaceholderValues,
textVariables?: Record<string, string>
textVariables?: Record<string, string>,
): ChatMessage[] {
const expandedMessages = messages.flatMap((message) =>
isPlaceholder(message)
? expandPlaceholder(message, placeholderValues)
: [message as ChatMessage]
: [message as ChatMessage],
);
// substitute text variables
@@ -65,27 +79,27 @@ export function compileChatMessages(
}
return expandedMessages.map((message) => {
if (!message.content) {
if (!message.content || typeof message.content !== "string") {
return message;
}
return {
...message,
content: replaceTextVariables(message.content, textVariables)
content: replaceTextVariables(message.content, textVariables),
};
});
}
export function compileChatMessagesWithIds(
messages: ChatMessageWithId[],
placeholderValues: Record<string, ChatMessage[]>,
textVariables?: Record<string, string>
placeholderValues: MessagePlaceholderValues,
textVariables?: Record<string, string>,
): ChatMessageWithIdNoPlaceholders[] {
// TODO: check, is it even important to retain the IDs?
const expandedMessages = messages.flatMap((message) => {
if (isPlaceholder(message)) {
const expandedMsgs = expandPlaceholder(message, placeholderValues);
return expandedMsgs.map(msg => ({ ...msg, id: uuidv4() }));
return expandedMsgs.map((msg) => ({ ...msg, id: uuidv4() }));
} else {
// Preserve message IDs for already non-placeholder messages
return [message as ChatMessageWithIdNoPlaceholders];
@@ -98,19 +112,22 @@ export function compileChatMessagesWithIds(
}
return expandedMessages.map((message) => {
if (!message.content) {
if (!message.content || typeof message.content !== "string") {
return message;
}
return {
...message,
content: replaceTextVariables(message.content, textVariables)
content: replaceTextVariables(message.content, textVariables),
};
});
}
export function extractPlaceholderNames(messages: PromptMessage[]): string[] {
return messages
.filter((msg): msg is PlaceholderMessage => "type" in msg && msg.type === ChatMessageType.Placeholder)
.map(msg => msg.name);
.filter(
(msg): msg is PlaceholderMessage =>
"type" in msg && msg.type === ChatMessageType.Placeholder,
)
.map((msg) => msg.name);
}
@@ -19,10 +19,12 @@ import {
} from "@langchain/core/output_parsers";
import { IterableReadableStream } from "@langchain/core/utils/stream";
import { ChatOpenAI, AzureChatOpenAI } from "@langchain/openai";
import { env } from "../../env";
import GCPServiceAccountKeySchema, {
BedrockConfigSchema,
BedrockCredentialSchema,
VertexAIConfigSchema,
BEDROCK_USE_DEFAULT_CREDENTIALS,
} from "../../interfaces/customLLMProviderConfigSchemas";
import { processEventBatch } from "../ingestion/processEventBatch";
import { logger } from "../logger";
@@ -41,6 +43,8 @@ import {
import { CallbackHandler } from "langfuse-langchain";
import type { BaseCallbackHandler } from "@langchain/core/callbacks/base";
const isLangfuseCloud = Boolean(env.NEXT_PUBLIC_LANGFUSE_CLOUD_REGION);
type ProcessTracedEvents = () => Promise<void>;
type LLMCompletionParams = {
@@ -63,6 +67,7 @@ type FetchLLMCompletionParams = LLMCompletionParams & {
};
export async function fetchLLMCompletion(
// eslint-disable-next-line no-unused-vars
params: LLMCompletionParams & {
streaming: true;
},
@@ -72,6 +77,7 @@ export async function fetchLLMCompletion(
}>;
export async function fetchLLMCompletion(
// eslint-disable-next-line no-unused-vars
params: LLMCompletionParams & {
streaming: false;
},
@@ -81,6 +87,7 @@ export async function fetchLLMCompletion(
}>;
export async function fetchLLMCompletion(
// eslint-disable-next-line no-unused-vars
params: LLMCompletionParams & {
streaming: false;
structuredOutputSchema: ZodSchema;
@@ -91,6 +98,7 @@ export async function fetchLLMCompletion(
}>;
export async function fetchLLMCompletion(
// eslint-disable-next-line no-unused-vars
params: LLMCompletionParams & {
tools: LLMToolDefinition[];
streaming: false;
@@ -133,7 +141,7 @@ export async function fetchLLMCompletion(
const handler = new CallbackHandler({
_projectId: traceParams.projectId,
_isLocalEventExportEnabled: true,
tags: traceParams.tags,
environment: traceParams.environment,
});
finalCallbacks.push(handler);
@@ -145,6 +153,7 @@ export async function fetchLLMCompletion(
await processEventBatch(
JSON.parse(JSON.stringify(events)), // stringify to emulate network event batch from network call
traceParams.authCheck,
{ isLangfuseInternal: true },
);
} catch (e) {
logger.error("Failed to process traced events", { error: e });
@@ -154,28 +163,48 @@ export async function fetchLLMCompletion(
finalCallbacks = finalCallbacks.length > 0 ? finalCallbacks : undefined;
// Helper function to safely stringify content
const safeStringify = (content: any): string => {
try {
return JSON.stringify(content);
} catch {
return "[Unserializable content]";
}
};
let finalMessages: BaseMessage[];
// VertexAI requires at least 1 user message
if (modelParams.adapter === LLMAdapter.VertexAI && messages.length === 1) {
finalMessages = [new HumanMessage(messages[0].content)];
const safeContent =
typeof messages[0].content === "string"
? messages[0].content
: JSON.stringify(messages[0].content);
finalMessages = [new HumanMessage(safeContent)];
} else {
finalMessages = messages.map((message) => {
// For arbitrary content types, convert to string safely
const safeContent =
typeof message.content === "string"
? message.content
: safeStringify(message.content);
if (message.role === ChatMessageRole.User)
return new HumanMessage(message.content);
return new HumanMessage(safeContent);
if (
message.role === ChatMessageRole.System ||
message.role === ChatMessageRole.Developer
)
return new SystemMessage(message.content);
return new SystemMessage(safeContent);
if (message.type === ChatMessageType.ToolResult)
if (message.type === ChatMessageType.ToolResult) {
return new ToolMessage({
content: message.content,
content: safeContent,
tool_call_id: message.toolCallId,
});
}
return new AIMessage({
content: message.content,
content: safeContent,
tool_calls:
message.type === ChatMessageType.AssistantToolCall
? (message.toolCalls as any)
@@ -239,7 +268,11 @@ export async function fetchLLMCompletion(
});
} else if (modelParams.adapter === LLMAdapter.Bedrock) {
const { region } = BedrockConfigSchema.parse(config);
const credentials = BedrockCredentialSchema.parse(JSON.parse(apiKey));
// Handle both explicit credentials and default provider chain
const credentials =
apiKey === BEDROCK_USE_DEFAULT_CREDENTIALS && !isLangfuseCloud
? undefined // undefined = use AWS SDK default credential provider chain
: BedrockCredentialSchema.parse(JSON.parse(apiKey));
chatModel = new ChatBedrockConverse({
model: modelParams.model,
@@ -325,7 +358,7 @@ export async function fetchLLMCompletion(
/*
Workaround OpenAI reasoning models:
This is a temporary workaround to avoid sending unsupported parameters to OpenAI's O1 models.
O1 models do not support:
- system messages
+25 -11
View File
@@ -176,7 +176,12 @@ export type ToolResultMessage = z.infer<typeof ToolResultMessageSchema>;
export const PlaceholderMessageSchema = z.object({
type: z.literal(ChatMessageType.Placeholder),
name: z.string().regex(/^[a-zA-Z][a-zA-Z0-9_]*$/, "Placeholder name must start with a letter and contain only alphanumeric characters and underscores"),
name: z
.string()
.regex(
/^[a-zA-Z][a-zA-Z0-9_]*$/,
"Placeholder name must start with a letter and contain only alphanumeric characters and underscores",
),
});
export type PlaceholderMessage = z.infer<typeof PlaceholderMessageSchema>;
@@ -191,7 +196,7 @@ export const ChatMessageSchema = z.union([
z
.object({
role: z.union([ChatMessageDefaultRoleSchema, z.string()]), // Users may ingest any string as role via API/SDK
content: z.string(),
content: z.union([z.string(), z.array(z.any()), z.any()]), // Support arbitrary content types for message placeholders
})
.transform((msg) => {
return {
@@ -202,8 +207,10 @@ export const ChatMessageSchema = z.union([
]);
export type ChatMessage = z.infer<typeof ChatMessageSchema>;
export type ChatMessageWithId = (ChatMessage & { id: string }) | (PlaceholderMessage & { id: string });
export type ChatMessageWithIdNoPlaceholders = (ChatMessage & { id: string });
export type ChatMessageWithId =
| (ChatMessage & { id: string })
| (PlaceholderMessage & { id: string });
export type ChatMessageWithIdNoPlaceholders = ChatMessage & { id: string };
export const PromptChatMessageSchema = z.union([
z.object({
@@ -226,11 +233,6 @@ export enum LLMAdapter {
GoogleAIStudio = "google-ai-studio",
}
export const SYSTEM_ROLES: string[] = [
ChatMessageRole.System,
ChatMessageRole.Developer,
];
export const TextPromptContentSchema = z.string().min(1, "Enter a prompt");
export const PromptContentSchema = z.union([
@@ -335,9 +337,12 @@ export const anthropicModels = [
// WARNING: The first entry in the array is chosen as the default model to add LLM API keys
export const vertexAIModels = [
"gemini-2.0-flash",
"gemini-2.5-pro",
"gemini-2.5-flash",
"gemini-2.5-flash-lite-preview-06-17",
"gemini-2.5-pro-preview-05-06",
"gemini-2.5-flash-preview-05-20",
"gemini-2.0-flash",
"gemini-2.0-pro-exp-02-05",
"gemini-2.0-flash-001",
"gemini-2.0-flash-lite-preview-02-05",
@@ -349,6 +354,9 @@ export const vertexAIModels = [
// WARNING: The first entry in the array is chosen as the default model to add LLM API keys. Make sure it supports top_p, max_tokens and temperature.
export const googleAIStudioModels = [
"gemini-2.5-flash",
"gemini-2.5-pro",
"gemini-2.5-flash-lite-preview-06-17",
"gemini-2.5-pro-preview-05-06",
"gemini-2.5-flash-preview-05-20",
"gemini-2.0-flash",
@@ -406,11 +414,17 @@ export type LLMApiKey =
? z.infer<typeof LLMApiKeySchema>
: never;
// NOTE: This string is whitelisted in the TS SDK to allow ingestion of traces by Langfuse. Please mirror edits to this string in https://github.com/langfuse/langfuse-js/blob/main/langfuse-core/src/index.ts.
export const PROMPT_EXPERIMENT_ENVIRONMENT =
"langfuse-prompt-experiment" as const;
type PromptExperimentEnvironment = typeof PROMPT_EXPERIMENT_ENVIRONMENT;
export type TraceParams = {
traceName: string;
traceId: string;
projectId: string;
tags: string[];
environment: PromptExperimentEnvironment;
tokenCountDelegate: TokenCountDelegate;
authCheck: AuthHeaderValidVerificationResult;
};
@@ -432,18 +432,22 @@ export class FilterList {
this.filters.push(...filter);
}
// eslint-disable-next-line no-unused-vars
find(predicate: (filter: Filter) => boolean) {
return this.filters.find(predicate);
}
// eslint-disable-next-line no-unused-vars
filter(predicate: (filter: Filter) => boolean) {
return new FilterList(this.filters.filter(predicate));
}
// eslint-disable-next-line no-unused-vars
some(predicate: (filter: Filter) => boolean) {
return this.filters.some(predicate);
}
// eslint-disable-next-line no-unused-vars
forEach(callback: (filter: Filter) => void) {
this.filters.forEach(callback);
}
@@ -6,6 +6,7 @@ export const clickhouseSearchCondition = (
query?: string,
searchType?: TracingSearchType[],
tablePrefix?: string,
useTracesAmtCompatMode: boolean = false,
) => {
const prefix = tablePrefix ? `${tablePrefix}.` : "";
@@ -13,9 +14,11 @@ export const clickhouseSearchCondition = (
!searchType || searchType.includes("id")
? `${prefix}id ILIKE {searchString: String} OR user_id ILIKE {searchString: String} OR ${prefix}name ILIKE {searchString: String}`
: null,
searchType && searchType.includes("content")
searchType && searchType.includes("content") && !useTracesAmtCompatMode
? `${prefix}input ILIKE {searchString: String} OR ${prefix}output ILIKE {searchString: String}`
: null,
: searchType && searchType.includes("content") && useTracesAmtCompatMode
? `finalizeAggregation(${prefix}input) ILIKE {searchString: String} OR finalizeAggregation(${prefix}output) ILIKE {searchString: String}`
: null,
].filter(Boolean);
return {
+47
View File
@@ -6,6 +6,8 @@ import {
BatchActionType,
} from "../features/batchAction/types";
import { BatchTableNames } from "../interfaces/tableNames";
import { EventActionSchema } from "../domain";
import { PromptDomainSchema } from "../domain/prompts";
export const IngestionEvent = z.object({
data: z.object({
@@ -129,6 +131,33 @@ export const DeadLetterRetryQueueEventSchema = z.object({
timestamp: z.date(),
});
export const WebhookOutboundEnvelopeSchema = z.object({
prompt: PromptDomainSchema,
action: EventActionSchema,
type: z.literal("prompt-version"),
});
export const WebhookInputSchema = z.object({
projectId: z.string(),
automationId: z.string(),
executionId: z.string(),
payload: WebhookOutboundEnvelopeSchema,
});
export const EntityChangeEventSchema = z.discriminatedUnion("entityType", [
z.object({
entityType: z.literal("prompt-version"),
projectId: z.string(),
promptId: z.string(),
action: EventActionSchema,
prompt: PromptDomainSchema,
}),
// Add other entity types here in the future
]);
export type WebhookInput = z.infer<typeof WebhookInputSchema>;
export type EntityChangeEventType = z.infer<typeof EntityChangeEventSchema>;
export type CreateEvalQueueEventType = z.infer<
typeof CreateEvalQueueEventSchema
>;
@@ -161,6 +190,8 @@ export type DeadLetterRetryQueueEventType = z.infer<
typeof DeadLetterRetryQueueEventSchema
>;
export type WebhookQueueEventType = z.infer<typeof WebhookInputSchema>;
export enum QueueName {
TraceUpsert = "trace-upsert", // Ingestion pipeline adds events on each Trace upsert
TraceDelete = "trace-delete",
@@ -184,6 +215,8 @@ export enum QueueName {
CreateEvalQueue = "create-eval-queue",
ScoreDelete = "score-delete",
DeadLetterRetryQueue = "dead-letter-retry-queue",
WebhookQueue = "webhook-queue",
EntityChangeQueue = "entity-change-queue",
}
export enum QueueJobs {
@@ -209,6 +242,8 @@ export enum QueueJobs {
CreateEvalJob = "create-eval-job",
ScoreDelete = "score-delete",
DeadLetterRetryJob = "dead-letter-retry-job",
WebhookJob = "webhook-job",
EntityChangeJob = "entity-change-job",
}
export type TQueueJobTypes = {
@@ -308,4 +343,16 @@ export type TQueueJobTypes = {
payload: DeadLetterRetryQueueEventType;
name: QueueJobs.DeadLetterRetryJob;
};
[QueueName.WebhookQueue]: {
timestamp: Date;
id: string;
payload: WebhookInput;
name: QueueJobs.WebhookJob;
};
[QueueName.EntityChangeQueue]: {
timestamp: Date;
id: string;
payload: EntityChangeEventType;
name: QueueJobs.EntityChangeJob;
};
};
@@ -1,6 +1,10 @@
import { Queue } from "bullmq";
import { QueueName, TQueueJobTypes } from "../queues";
import { createNewRedisInstance, redisQueueRetryOptions, getQueuePrefix } from "./redis";
import {
createNewRedisInstance,
redisQueueRetryOptions,
getQueuePrefix,
} from "./redis";
import { logger } from "../logger";
export class BatchExportQueue {
@@ -1,6 +1,10 @@
import { Queue } from "bullmq";
import { QueueName } from "../queues";
import { createNewRedisInstance, redisQueueRetryOptions, getQueuePrefix } from "./redis";
import {
createNewRedisInstance,
redisQueueRetryOptions,
getQueuePrefix,
} from "./redis";
import { logger } from "../logger";
export class BlobStorageIntegrationProcessingQueue {
@@ -18,8 +22,10 @@ export class BlobStorageIntegrationProcessingQueue {
BlobStorageIntegrationProcessingQueue.instance = newRedis
? new Queue(QueueName.BlobStorageIntegrationProcessingQueue, {
connection: newRedis,
prefix: getQueuePrefix(QueueName.BlobStorageIntegrationProcessingQueue),
connection: newRedis,
prefix: getQueuePrefix(
QueueName.BlobStorageIntegrationProcessingQueue,
),
defaultJobOptions: {
removeOnComplete: true,
removeOnFail: 100_000,
@@ -38,4 +44,4 @@ export class BlobStorageIntegrationProcessingQueue {
return BlobStorageIntegrationProcessingQueue.instance;
}
}
}
@@ -1,7 +1,11 @@
import { Queue } from "bullmq";
import { env } from "../../env";
import { QueueName, QueueJobs } from "../queues";
import { createNewRedisInstance, redisQueueRetryOptions, getQueuePrefix } from "./redis";
import {
createNewRedisInstance,
redisQueueRetryOptions,
getQueuePrefix,
} from "./redis";
import { logger } from "../logger";
export class CloudUsageMeteringQueue {
@@ -1,6 +1,10 @@
import { Queue } from "bullmq";
import { QueueName, QueueJobs } from "../queues";
import { createNewRedisInstance, redisQueueRetryOptions, getQueuePrefix } from "./redis";
import {
createNewRedisInstance,
redisQueueRetryOptions,
getQueuePrefix,
} from "./redis";
import { logger } from "../logger";
import { env } from "../../env";
@@ -23,8 +27,8 @@ export class CoreDataS3ExportQueue {
CoreDataS3ExportQueue.instance = newRedis
? new Queue(QueueName.CoreDataS3ExportQueue, {
connection: newRedis,
prefix: getQueuePrefix(QueueName.CoreDataS3ExportQueue),
connection: newRedis,
prefix: getQueuePrefix(QueueName.CoreDataS3ExportQueue),
defaultJobOptions: {
removeOnComplete: true,
removeOnFail: 100,
@@ -1,6 +1,10 @@
import { QueueName, TQueueJobTypes } from "../queues";
import { Queue } from "bullmq";
import { createNewRedisInstance, redisQueueRetryOptions, getQueuePrefix } from "./redis";
import {
createNewRedisInstance,
redisQueueRetryOptions,
getQueuePrefix,
} from "./redis";
import { logger } from "../logger";
export class CreateEvalQueue {
@@ -1,6 +1,10 @@
import { Queue } from "bullmq";
import { QueueName } from "../queues";
import { createNewRedisInstance, redisQueueRetryOptions, getQueuePrefix } from "./redis";
import {
createNewRedisInstance,
redisQueueRetryOptions,
getQueuePrefix,
} from "./redis";
import { logger } from "../logger";
export class DataRetentionProcessingQueue {
@@ -18,8 +22,8 @@ export class DataRetentionProcessingQueue {
DataRetentionProcessingQueue.instance = newRedis
? new Queue(QueueName.DataRetentionProcessingQueue, {
connection: newRedis,
prefix: getQueuePrefix(QueueName.DataRetentionProcessingQueue),
connection: newRedis,
prefix: getQueuePrefix(QueueName.DataRetentionProcessingQueue),
defaultJobOptions: {
removeOnComplete: true,
removeOnFail: 10000,

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