Compare commits

...
78 Commits
Author SHA1 Message Date
Marc Klingen ca3b95312b chore: release v3.0.0-rc.3
CI/CD / lint (push) Waiting to run
CI/CD / test-docker-build (push) Waiting to run
CI/CD / tests-web-sync (node20, pg12) (push) Waiting to run
CI/CD / tests-web-sync (node20, pg15) (push) Waiting to run
CI/CD / tests-web-async (node20, pg12, mode) (push) Waiting to run
CI/CD / tests-web-async (node20, pg15, mode) (push) Waiting to run
CI/CD / tests-web-async (node20, pg12, mode-azure) (push) Waiting to run
CI/CD / tests-web-async (node20, pg15, mode-azure) (push) Waiting to run
CI/CD / tests-worker (node20, pg12, mode) (push) Waiting to run
CI/CD / tests-worker (node20, pg15, mode) (push) Waiting to run
CI/CD / tests-worker (node20, pg12, mode-azure) (push) Waiting to run
CI/CD / tests-worker (node20, pg15, mode-azure) (push) Waiting to run
CI/CD / e2e-tests (push) Waiting to run
CI/CD / e2e-server-tests (push) Waiting to run
CI/CD / all-ci-passed (push) Blocked by required conditions
CI/CD / push-docker-image (push) Blocked by required conditions
2024-12-03 18:53:59 +01:00
b9aaf05acb feat(auth): add KeyCloak authentication option (#2866)
---------

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

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

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

* chore: lint

* chore: remove console logs

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

* feat

* feat

* push

* feat

* feat

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

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

* chore: lint

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

* chore: adjust pattern

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

* chore: add prefixes correctly

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

* push

* add migration

* render correct usage event type in settings

* render number of seats in settings

* wip

* add entitlement limits

* add new actionbutton

* migrate annotation queue create

* add to table date range selectors

* add limit to dashboard date range

* move migration

* fix usage preview in billing settings

* get seats from subscription instead of next invoice

* remove old console.log

* ad pro plan self-hosted

* fix

* readme

* rename / fixes

* push

* only alert on over 90% usage

* push

* push

* push

* push

* add new self host entitlements for v3

* drop batch-export entitlement

* push

* move logic to clickhouse

* push

* fix padding

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

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

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

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

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

* fix flush

* shutdown instead of shutdownasync

---------

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

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

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

---
updated-dependencies:
- dependency-name: lucide-react
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-11-27 23:47:05 +00:00
Marc Klingen 66d925ee1f chore: do not run codeql on merge queue 2024-11-28 01:05:07 +01:00
Marc Klingen 1b06385111 chore: run ci on merge groups 2024-11-28 00:23:08 +01:00
Hassieb PakzadandGitHub 6e290b9ad4 fix(media): increase retries (#4474) 2024-11-27 22:57:53 +00:00
Max DeichmannandGitHub 8b8afff705 fix: fix generations search in clickhouse (#4472) 2024-11-27 23:20:21 +01:00
Max DeichmannandGitHub 89fcf27f84 chore: add logs for media API (#4471) 2024-11-27 22:46:44 +01:00
Max DeichmannandGitHub 6717b16679 perf: add improved default performance to traces table (#4469)
* feat

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

* fix file extensions

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

* password complexity
* no url in name

* e2e test
2024-11-26 13:28:43 +00:00
Hassieb Pakzad e52530351a chore: update v3preview docker compose to main tagged images 2024-11-26 13:49:28 +01:00
Hassieb Pakzad 8a52c0b888 chore: update v3preview docker compose to 3 tagged images 2024-11-26 13:32:16 +01:00
Hassieb Pakzad d02c9b4902 chore: release v3.0.0-rc.1
CI/CD / lint (push) Waiting to run
CI/CD / test-docker-build (push) Waiting to run
CI/CD / tests-web-sync (node20, pg12) (push) Waiting to run
CI/CD / tests-web-sync (node20, pg15) (push) Waiting to run
CI/CD / tests-web-async (node20, pg12, mode) (push) Waiting to run
CI/CD / tests-web-async (node20, pg15, mode) (push) Waiting to run
CI/CD / tests-web-async (node20, pg12, mode-azure) (push) Waiting to run
CI/CD / tests-web-async (node20, pg15, mode-azure) (push) Waiting to run
CI/CD / tests-worker (node20, pg12, mode) (push) Waiting to run
CI/CD / tests-worker (node20, pg15, mode) (push) Waiting to run
CI/CD / tests-worker (node20, pg12, mode-azure) (push) Waiting to run
CI/CD / tests-worker (node20, pg15, mode-azure) (push) Waiting to run
CI/CD / e2e-tests (push) Waiting to run
CI/CD / e2e-server-tests (push) Waiting to run
CI/CD / all-ci-passed (push) Blocked by required conditions
CI/CD / push-docker-image (push) Blocked by required conditions
2024-11-26 12:20:20 +01:00
Hassieb Pakzad 014c3d9dad chore: do not tag v3 tags as latest image 2024-11-26 12:17:51 +01:00
Max DeichmannandGitHub 1d3c74435b fix: change the default of the migration script (#4419) 2024-11-26 09:48:37 +00:00
194 changed files with 4893 additions and 2274 deletions
+10 -17
View File
@@ -11,7 +11,7 @@ CLICKHOUSE_MIGRATION_URL="clickhouse://localhost:9000"
CLICKHOUSE_URL="http://localhost:8123"
CLICKHOUSE_USER="clickhouse"
CLICKHOUSE_PASSWORD="clickhouse"
CLICKHOUSE_MIGRATION_CLUSTER_DISABLED="true"
CLICKHOUSE_CLUSTER_ENABLED="false"
# Next Auth
# You can generate a new secret on the command line with:
@@ -35,14 +35,16 @@ EMAIL_FROM_ADDRESS="" # Defines the email address to use as the from address.
SMTP_CONNECTION_URL="" # Defines the connection url for smtp server.
# DON'T PANIC: The Azurite Secrets are well-known and meant to be hard-coded
# S3 storage
S3_ENDPOINT=http://localhost:10000/devstoreaccount1
S3_ACCESS_KEY_ID=devstoreaccount1
S3_SECRET_ACCESS_KEY=Eby8vdM02xNOcqFlqUwJPLlmEtlCDXJ1OUzFT50uSRZ6IFsuFq2UVErCz4I6tq/K1SZFPTOtr/KBHBeksoGMGw==
S3_BUCKET_NAME=langfuse
S3_REGION=auto
# S3 Batch Exports
LANGFUSE_S3_BATCH_EXPORT_ENABLED=true
LANGFUSE_S3_BATCH_EXPORT_BUCKET=langfuse
LANGFUSE_S3_BATCH_EXPORT_ACCESS_KEY_ID=devstoreaccount1
LANGFUSE_S3_BATCH_EXPORT_SECRET_ACCESS_KEY=Eby8vdM02xNOcqFlqUwJPLlmEtlCDXJ1OUzFT50uSRZ6IFsuFq2UVErCz4I6tq/K1SZFPTOtr/KBHBeksoGMGw==
LANGFUSE_S3_BATCH_EXPORT_REGION=auto
LANGFUSE_S3_BATCH_EXPORT_ENDPOINT=http://localhost:10000/devstoreaccount1
## Necessary for minio compatibility
S3_FORCE_PATH_STYLE=true
LANGFUSE_S3_BATCH_EXPORT_FORCE_PATH_STYLE=true
LANGFUSE_S3_BATCH_EXPORT_PREFIX=exports/
# S3 Media Upload LOCAL
LANGFUSE_S3_MEDIA_UPLOAD_ENABLED=true
@@ -57,7 +59,6 @@ LANGFUSE_S3_MEDIA_UPLOAD_PREFIX=media/
# S3 Event Bucket Upload
## Set to true to test uploading all events to S3
LANGFUSE_S3_EVENT_UPLOAD_ENABLED=true
LANGFUSE_S3_EVENT_UPLOAD_BUCKET=langfuse
LANGFUSE_S3_EVENT_UPLOAD_ACCESS_KEY_ID=devstoreaccount1
LANGFUSE_S3_EVENT_UPLOAD_SECRET_ACCESS_KEY=Eby8vdM02xNOcqFlqUwJPLlmEtlCDXJ1OUzFT50uSRZ6IFsuFq2UVErCz4I6tq/K1SZFPTOtr/KBHBeksoGMGw==
@@ -82,11 +83,3 @@ ENCRYPTION_KEY=0000000000000000000000000000000000000000000000000000000000000000
# speeds up local development by not executing init scripts on server startup
NEXT_PUBLIC_LANGFUSE_RUN_NEXT_INIT="false"
LANGFUSE_READ_FROM_POSTGRES_ONLY=false
LANGFUSE_RETURN_FROM_CLICKHOUSE=true
LANGFUSE_READ_DASHBOARDS_FROM_CLICKHOUSE=true
LANGFUSE_READ_FROM_CLICKHOUSE_ONLY=true
LANGFUSE_ASYNC_INGESTION_PROCESSING="true"
LANGFUSE_ASYNC_CLICKHOUSE_INGESTION_PROCESSING="true"
+10 -17
View File
@@ -11,7 +11,7 @@ CLICKHOUSE_MIGRATION_URL="clickhouse://localhost:9000"
CLICKHOUSE_URL="http://localhost:8123"
CLICKHOUSE_USER="clickhouse"
CLICKHOUSE_PASSWORD="clickhouse"
CLICKHOUSE_CLUSTER_DISABLED="true"
CLICKHOUSE_CLUSTER_ENABLED="false"
# Next Auth
# You can generate a new secret on the command line with:
@@ -34,14 +34,16 @@ SALT="salt"
EMAIL_FROM_ADDRESS="" # Defines the email address to use as the from address.
SMTP_CONNECTION_URL="" # Defines the connection url for smtp server.
# S3 storage
S3_ENDPOINT=http://localhost:9090
S3_ACCESS_KEY_ID=minio
S3_SECRET_ACCESS_KEY=miniosecret
S3_BUCKET_NAME=langfuse
S3_REGION=us-east-1
# S3 Batch Exports
LANGFUSE_S3_BATCH_EXPORT_ENABLED=true
LANGFUSE_S3_BATCH_EXPORT_BUCKET=langfuse
LANGFUSE_S3_BATCH_EXPORT_ACCESS_KEY_ID=minio
LANGFUSE_S3_BATCH_EXPORT_SECRET_ACCESS_KEY=miniosecret
LANGFUSE_S3_BATCH_EXPORT_REGION=us-east-1
LANGFUSE_S3_BATCH_EXPORT_ENDPOINT=http://localhost:9090
## Necessary for minio compatibility
S3_FORCE_PATH_STYLE=true
LANGFUSE_S3_BATCH_EXPORT_FORCE_PATH_STYLE=true
LANGFUSE_S3_BATCH_EXPORT_PREFIX=exports/
# S3 Media Upload LOCAL
LANGFUSE_S3_MEDIA_UPLOAD_ENABLED=true
@@ -56,7 +58,6 @@ LANGFUSE_S3_MEDIA_UPLOAD_PREFIX=media/
# S3 Event Bucket Upload
## Set to true to test uploading all events to S3
LANGFUSE_S3_EVENT_UPLOAD_ENABLED=true
LANGFUSE_S3_EVENT_UPLOAD_BUCKET=langfuse
LANGFUSE_S3_EVENT_UPLOAD_ACCESS_KEY_ID=minio
LANGFUSE_S3_EVENT_UPLOAD_SECRET_ACCESS_KEY=miniosecret
@@ -79,11 +80,3 @@ ENCRYPTION_KEY=0000000000000000000000000000000000000000000000000000000000000000
# speeds up local development by not executing init scripts on server startup
NEXT_PUBLIC_LANGFUSE_RUN_NEXT_INIT="false"
LANGFUSE_READ_FROM_POSTGRES_ONLY=false
LANGFUSE_RETURN_FROM_CLICKHOUSE=true
LANGFUSE_READ_DASHBOARDS_FROM_CLICKHOUSE=true
LANGFUSE_READ_FROM_CLICKHOUSE_ONLY=true
LANGFUSE_ASYNC_INGESTION_PROCESSING="true"
LANGFUSE_ASYNC_CLICKHOUSE_INGESTION_PROCESSING="true"
+15 -9
View File
@@ -34,14 +34,16 @@ SALT="salt"
EMAIL_FROM_ADDRESS="" # Defines the email address to use as the from address.
SMTP_CONNECTION_URL="" # Defines the connection url for smtp server.
# S3 storage
S3_ENDPOINT=http://localhost:9090
S3_ACCESS_KEY_ID=minio
S3_SECRET_ACCESS_KEY=miniosecret
S3_BUCKET_NAME=langfuse
S3_REGION=us-east-1
# S3 Batch Exports
LANGFUSE_S3_BATCH_EXPORT_ENABLED=true
LANGFUSE_S3_BATCH_EXPORT_BUCKET=langfuse
LANGFUSE_S3_BATCH_EXPORT_ACCESS_KEY_ID=minio
LANGFUSE_S3_BATCH_EXPORT_SECRET_ACCESS_KEY=miniosecret
LANGFUSE_S3_BATCH_EXPORT_REGION=us-east-1
LANGFUSE_S3_BATCH_EXPORT_ENDPOINT=http://localhost:9090
## Necessary for minio compatibility
S3_FORCE_PATH_STYLE=true
LANGFUSE_S3_BATCH_EXPORT_FORCE_PATH_STYLE=true
LANGFUSE_S3_BATCH_EXPORT_PREFIX=exports/
# # S3 Media Upload LOCAL
LANGFUSE_S3_MEDIA_UPLOAD_ENABLED=true
@@ -56,7 +58,6 @@ LANGFUSE_S3_MEDIA_UPLOAD_PREFIX=media/
# S3 Event Bucket Upload
## Set to true to test uploading all events to S3
LANGFUSE_S3_EVENT_UPLOAD_ENABLED=true
LANGFUSE_S3_EVENT_UPLOAD_BUCKET=langfuse
LANGFUSE_S3_EVENT_UPLOAD_ACCESS_KEY_ID=minio
LANGFUSE_S3_EVENT_UPLOAD_SECRET_ACCESS_KEY=miniosecret
@@ -78,4 +79,9 @@ REDIS_AUTH="myredissecret"
ENCRYPTION_KEY=0000000000000000000000000000000000000000000000000000000000000000
# speeds up local development by not executing init scripts on server startup
NEXT_PUBLIC_LANGFUSE_RUN_NEXT_INIT="false"
NEXT_PUBLIC_LANGFUSE_RUN_NEXT_INIT="false"
LANGFUSE_READ_FROM_POSTGRES_ONLY=true
LANGFUSE_RETURN_FROM_CLICKHOUSE=false
LANGFUSE_READ_DASHBOARDS_FROM_CLICKHOUSE=false
LANGFUSE_READ_FROM_CLICKHOUSE_ONLY=false
-22
View File
@@ -1,22 +0,0 @@
# When adding additional environment variables, the schema in "/src/env.mjs"
# should be updated accordingly.
# Prisma
# https://www.prisma.io/docs/reference/database-reference/connection-urls#env
DIRECT_URL="postgresql://postgres:postgres@db:5432/postgres"
DATABASE_URL="postgresql://postgres:postgres@db:5432/postgres"
# Next Auth
NEXTAUTH_SECRET="secret"
NEXTAUTH_URL="http://localhost:3000"
# feature flag to enable experimental features locally
LANGFUSE_ENABLE_EXPERIMENTAL_FEATURES="false"
SALT="salt"
# Redis
REDIS_HOST="127.0.0.1"
REDIS_PORT=6379
REDIS_AUTH="myredissecret"
# openssl rand -hex 32 used only here
ENCRYPTION_KEY=0000000000000000000000000000000000000000000000000000000000000000
+16 -9
View File
@@ -67,6 +67,10 @@ OTEL_SERVICE_NAME="langfuse"
# AUTH_GITHUB_CLIENT_ID=
# AUTH_GITHUB_CLIENT_SECRET=
# AUTH_GITHUB_ALLOW_ACCOUNT_LINKING=false
# AUTH_GITHUB_ENTERPRISE_CLIENT_ID=
# AUTH_GITHUB_ENTERPRISE_CLIENT_SECRET=
# AUTH_GITHUB_ENTERPRISE_BASE_URL=
# AUTH_GITHUB_ENTERPRISE_ALLOW_ACCOUNT_LINKING=false
# AUTH_GITLAB_CLIENT_ID=
# AUTH_GITLAB_CLIENT_SECRET=
# AUTH_GITLAB_ALLOW_ACCOUNT_LINKING=false
@@ -87,6 +91,10 @@ OTEL_SERVICE_NAME="langfuse"
# AUTH_COGNITO_CLIENT_SECRET=
# AUTH_COGNITO_ISSUER=
# AUTH_COGNITO_ALLOW_ACCOUNT_LINKING=false
# AUTH_KEYCLOAK_CLIENT_ID=
# AUTH_KEYCLOAK_CLIENT_SECRET=
# AUTH_KEYCLOAK_ISSUER=
# AUTH_KEYCLOAK_ALLOW_ACCOUNT_LINKING=false
# AUTH_CUSTOM_CLIENT_ID=
# AUTH_CUSTOM_CLIENT_SECRET=
# AUTH_CUSTOM_ISSUER=
@@ -100,16 +108,16 @@ OTEL_SERVICE_NAME="langfuse"
# Defines the connection url for smtp server.
# SMTP_CONNECTION_URL=
# S3 storage, optional, used for exports from the UI
# S3_ENDPOINT=
# S3_ACCESS_KEY_ID=
# S3_SECRET_ACCESS_KEY=
# S3_BUCKET_NAME=
# S3_REGION=
# BATCH_EXPORT_DOWNLOAD_LINK_EXPIRATION_HOURS=
# S3 Batch Exports
# LANGFUSE_S3_BATCH_EXPORT_ENABLED=
# LANGFUSE_S3_BATCH_EXPORT_BUCKET=
# LANGFUSE_S3_BATCH_EXPORT_ACCESS_KEY_ID=
# LANGFUSE_S3_BATCH_EXPORT_SECRET_ACCESS_KEY=
# LANGFUSE_S3_BATCH_EXPORT_REGION=
# LANGFUSE_S3_BATCH_EXPORT_ENDPOINT=
# LANGFUSE_S3_BATCH_EXPORT_PREFIX=
# S3 storage for events, optional, used to persist all incoming events
# LANGFUSE_S3_EVENT_UPLOAD_ENABLED="true"
# LANGFUSE_S3_EVENT_UPLOAD_BUCKET=
# Optional prefix to be used within the bucket. Must end with `/` if set
# LANGFUSE_S3_EVENT_UPLOAD_PREFIX=events/
@@ -242,7 +250,6 @@ OTEL_SERVICE_NAME="langfuse"
# LANGFUSE_INGESTION_CLICKHOUSE_WRITE_INTERVAL_MS=
# LANGFUSE_INGESTION_CLICKHOUSE_MAX_ATTEMPTS=
# LANGFUSE_LEGACY_INGESTION_WORKER_CONCURRENCY=
# LANGFUSE_ASYNC_INGESTION_PROCESSING="true"
# QUEUE_CONSUMER_LEGACY_INGESTION_QUEUE_IS_ENABLED="true"
## END Langfuse V3 Ingestion
@@ -58,7 +58,6 @@ jobs:
--build-arg SENTRY_PROJECT=${{ vars.SENTRY_PROJECT }} \
.
docker push $REGISTRY/$REPOSITORY:$IMAGE_TAG
- name: Render AWS ECS Task Definition
id: render-task-definition
uses: aws-actions/amazon-ecs-render-task-definition@v1
+1
View File
@@ -6,6 +6,7 @@ on:
branches: [main]
pull_request:
branches: [main]
merge_group:
permissions:
contents: read
+17 -6
View File
@@ -10,7 +10,11 @@ on:
merge_group:
pull_request:
branches:
- "*"
- "**"
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: ${{ github.event_name == 'pull_request' }}
jobs:
lint:
@@ -41,6 +45,7 @@ jobs:
- name: Checkout
uses: actions/checkout@v4
- name: Login to Docker Hub
if: github.repository == 'langfuse/langfuse' && (github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository) && (github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository)
uses: docker/login-action@v3
with:
username: ${{ secrets.DOCKERHUB_USERNAME_READ }}
@@ -89,6 +94,7 @@ jobs:
with:
version: 9.5.0
- name: Login to Docker Hub
if: github.repository == 'langfuse/langfuse' && (github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository)
uses: docker/login-action@v3
with:
username: ${{ secrets.DOCKERHUB_USERNAME_READ }}
@@ -105,7 +111,7 @@ jobs:
- name: Load default env
run: |
cp .env.dev.example .env
grep -v -e '^S3_BUCKET_NAME=' -e '^REDIS_HOST=' -e '^NEXT_PUBLIC_LANGFUSE_RUN_NEXT_INIT=' .env.dev.legacy.example > .env
grep -v -e '^LANGFUSE_S3_BATCH_EXPORT_ENABLED=' -e '^REDIS_HOST=' -e '^NEXT_PUBLIC_LANGFUSE_RUN_NEXT_INIT=' .env.dev.legacy.example > .env
- name: Run + migrate
run: |
docker compose -f docker-compose.dev.yml up -d
@@ -158,6 +164,7 @@ jobs:
with:
version: 9.5.0
- name: Login to Docker Hub
if: github.repository == 'langfuse/langfuse' && (github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository)
uses: docker/login-action@v3
with:
username: ${{ secrets.DOCKERHUB_USERNAME_READ }}
@@ -174,7 +181,7 @@ jobs:
- name: Load default env
run: |
cp .env.dev${{ matrix.blob-provider }}.example .env
grep -v -e '^S3_BUCKET_NAME=' -e '^REDIS_HOST=' -e '^NEXT_PUBLIC_LANGFUSE_RUN_NEXT_INIT=' .env.dev${{ matrix.blob-provider }}.example > .env
grep -v -e '^LANGFUSE_S3_BATCH_EXPORT_ENABLED=' -e '^REDIS_HOST=' -e '^NEXT_PUBLIC_LANGFUSE_RUN_NEXT_INIT=' .env.dev${{ matrix.blob-provider }}.example > .env
- name: Run + migrate
run: |
docker compose -f docker-compose.dev${{ matrix.blob-provider }}.yml up -d
@@ -222,6 +229,7 @@ jobs:
with:
version: 9.5.0
- name: Login to Docker Hub
if: github.repository == 'langfuse/langfuse' && (github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository)
uses: docker/login-action@v3
with:
username: ${{ secrets.DOCKERHUB_USERNAME_READ }}
@@ -281,6 +289,7 @@ jobs:
cache: "pnpm"
cache-dependency-path: "pnpm-lock.yaml"
- name: Login to Docker Hub
if: github.repository == 'langfuse/langfuse' && (github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository)
uses: docker/login-action@v3
with:
username: ${{ secrets.DOCKERHUB_USERNAME_READ }}
@@ -313,6 +322,7 @@ jobs:
steps:
- uses: actions/checkout@v4
- name: Login to Docker Hub
if: github.repository == 'langfuse/langfuse' && (github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository)
uses: docker/login-action@v3
with:
username: ${{ secrets.DOCKERHUB_USERNAME_READ }}
@@ -336,8 +346,6 @@ jobs:
- name: Load default env
run: |
cp .env.dev.example .env
echo "LANGFUSE_ASYNC_CLICKHOUSE_INGESTION_PROCESSING=true" >> .env
echo "LANGFUSE_ASYNC_INGESTION_PROCESSING=true" >> .env
echo "LANGFUSE_CACHE_API_KEY_ENABLED=true" >> .env
echo "LANGFUSE_CACHE_PROMPT_ENABLED=true" >> .env
- name: Run + migrate
@@ -450,7 +458,7 @@ jobs:
type=semver,pattern={{version}}
type=semver,pattern={{major}}.{{minor}}
type=semver,pattern={{major}}
type=raw,value=latest,enable=${{ startsWith(github.ref, 'refs/tags/v3') }}
# type=raw,value=latest,enable=${{ startsWith(github.ref, 'refs/tags/v3') }}
- name: Build and push Docker image (web)
uses: docker/build-push-action@v4
with:
@@ -469,6 +477,8 @@ jobs:
images: |
ghcr.io/langfuse/langfuse-worker # GitHub
langfuse/langfuse-worker # Docker Hub
flavor: |
latest=false
tags: |
type=ref,event=branch
type=ref,event=pr
@@ -476,6 +486,7 @@ jobs:
type=semver,pattern={{version}}
type=semver,pattern={{major}}.{{minor}}
type=semver,pattern={{major}}
# type=raw,value=latest,enable=${{ startsWith(github.ref, 'refs/tags/v3') }}
- name: Build and push Docker image (worker)
uses: docker/build-push-action@v4
with:
+8 -3
View File
@@ -6,6 +6,11 @@ on:
push:
branches:
- main
merge_group:
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: ${{ github.event_name == 'pull_request' }}
jobs:
snyk:
@@ -15,13 +20,13 @@ jobs:
- name: Build a Docker image
run: docker compose -f docker-compose.build.yml up -d
- name: Run Snyk to check Docker image for vulnerabilities (langfuse-server)
- name: Run Snyk to check Docker image for vulnerabilities (langfuse-web)
continue-on-error: true
uses: snyk/actions/docker@master
env:
SNYK_TOKEN: ${{ secrets.SNYK_TOKEN }}
with:
image: langfuse-server
image: langfuse-langfuse-web
args: --file=web/Dockerfile
- name: Upload result to GitHub Code Scanning
@@ -36,7 +41,7 @@ jobs:
env:
SNYK_TOKEN: ${{ secrets.SNYK_TOKEN }}
with:
image: langfuse-worker
image: langfuse-langfuse-worker
args: --file=worker/Dockerfile
- name: Upload result to GitHub Code Scanning
-1
View File
@@ -38,7 +38,6 @@ yarn-error.log*
.env*
!.env.dev.example
!.env.dev-azure.example
!.env.local.example
!.env.prod.example
!.env.dev.legacy.example
+37
View File
@@ -191,3 +191,40 @@ You can opt-out by setting `TELEMETRY_ENABLED=false`.
<img alt="Star History Chart" src="https://api.star-history.com/svg?repos=langfuse/langfuse&type=Date" />
</picture>
</a>
### Open Source Projects Using Langfuse
Top open-source Python projects that use Langfuse, ranked by stars ([Source](https://github.com/langfuse/langfuse-docs/blob/main/components-mdx/dependents/python.md)):
| Repository | Stars |
| :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ----: |
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/127165244?s=40&v=4" width="20" height="20" alt=""> &nbsp; [langgenius](https://github.com/langgenius) / [dify](https://github.com/langgenius/dify) | 53464 |
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/158137808?s=40&v=4" width="20" height="20" alt=""> &nbsp; [open-webui](https://github.com/open-webui) / [open-webui](https://github.com/open-webui/open-webui) | 49387 |
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/130722866?s=40&v=4" width="20" height="20" alt=""> &nbsp; [run-llama](https://github.com/run-llama) / [llama_index](https://github.com/run-llama/llama_index) | 37087 |
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/85702467?s=40&v=4" width="20" height="20" alt=""> &nbsp; [langflow-ai](https://github.com/langflow-ai) / [langflow](https://github.com/langflow-ai/langflow) | 36690 |
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/31035808?s=40&v=4" width="20" height="20" alt=""> &nbsp; [mindsdb](https://github.com/mindsdb) / [mindsdb](https://github.com/mindsdb/mindsdb) | 26871 |
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/60330232?s=40&v=4" width="20" height="20" alt=""> &nbsp; [PostHog](https://github.com/PostHog) / [posthog](https://github.com/PostHog/posthog) | 22348 |
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/121462774?s=40&v=4" width="20" height="20" alt=""> &nbsp; [BerriAI](https://github.com/BerriAI) / [litellm](https://github.com/BerriAI/litellm) | 14508 |
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/78410652?s=40&v=4" width="20" height="20" alt=""> &nbsp; [GreyDGL](https://github.com/GreyDGL) / [PentestGPT](https://github.com/GreyDGL/PentestGPT) | 7319 |
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/152537519?s=40&v=4" width="20" height="20" alt=""> &nbsp; [superagent-ai](https://github.com/superagent-ai) / [superagent](https://github.com/superagent-ai/superagent) | 5352 |
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/7250217?s=40&v=4" width="20" height="20" alt=""> &nbsp; [Canner](https://github.com/Canner) / [WrenAI](https://github.com/Canner/WrenAI) | 2084 |
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/11855343?s=40&v=4" width="20" height="20" alt=""> &nbsp; [pingcap](https://github.com/pingcap) / [autoflow](https://github.com/pingcap/autoflow) | 1936 |
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/85268109?s=40&v=4" width="20" height="20" alt=""> &nbsp; [MLSysOps](https://github.com/MLSysOps) / [MLE-agent](https://github.com/MLSysOps/MLE-agent) | 1140 |
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/158137808?s=40&v=4" width="20" height="20" alt=""> &nbsp; [open-webui](https://github.com/open-webui) / [pipelines](https://github.com/open-webui/pipelines) | 1044 |
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/125468716?s=40&v=4" width="20" height="20" alt=""> &nbsp; [topoteretes](https://github.com/topoteretes) / [cognee](https://github.com/topoteretes/cognee) | 945 |
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/188657705?s=40&v=4" width="20" height="20" alt=""> &nbsp; [bRAGAI](https://github.com/bRAGAI) / [bRAG-langchain](https://github.com/bRAGAI/bRAG-langchain) | 770 |
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/169500408?s=40&v=4" width="20" height="20" alt=""> &nbsp; [opslane](https://github.com/opslane) / [opslane](https://github.com/opslane/opslane) | 675 |
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/151867818?s=40&v=4" width="20" height="20" alt=""> &nbsp; [dynamiq-ai](https://github.com/dynamiq-ai) / [dynamiq](https://github.com/dynamiq-ai/dynamiq) | 583 |
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/48585267?s=40&v=4" width="20" height="20" alt=""> &nbsp; [theopenconversationkit](https://github.com/theopenconversationkit) / [tock](https://github.com/theopenconversationkit/tock) | 505 |
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/20493493?s=40&v=4" width="20" height="20" alt=""> &nbsp; [andysingal](https://github.com/andysingal) / [llm-course](https://github.com/andysingal/llm-course) | 383 |
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/132396805?s=40&v=4" width="20" height="20" alt=""> &nbsp; [phospho-app](https://github.com/phospho-app) / [phospho](https://github.com/phospho-app/phospho) | 379 |
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/178644984?s=40&v=4" width="20" height="20" alt=""> &nbsp; [sentient-engineering](https://github.com/sentient-engineering) / [agent-q](https://github.com/sentient-engineering/agent-q) | 357 |
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/168552753?s=40&v=4" width="20" height="20" alt=""> &nbsp; [sql-agi](https://github.com/sql-agi) / [DB-GPT](https://github.com/sql-agi/DB-GPT) | 324 |
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/60330232?s=40&v=4" width="20" height="20" alt=""> &nbsp; [PostHog](https://github.com/PostHog) / [posthog-foss](https://github.com/PostHog/posthog-foss) | 300 |
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/609489?s=40&v=4" width="20" height="20" alt=""> &nbsp; [aorwall](https://github.com/aorwall) / [moatless-tools](https://github.com/aorwall/moatless-tools) | 278 |
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/185116535?s=40&v=4" width="20" height="20" alt=""> &nbsp; [block](https://github.com/block) / [goose](https://github.com/block/goose) | 237 |
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/66303003?s=40&v=4" width="20" height="20" alt=""> &nbsp; [RobotecAI](https://github.com/RobotecAI) / [rai](https://github.com/RobotecAI/rai) | 164 |
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/171735272?s=40&v=4" width="20" height="20" alt=""> &nbsp; [8090-inc](https://github.com/8090-inc) / [xrx-sample-apps](https://github.com/8090-inc/xrx-sample-apps) | 133 |
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/51827949?s=40&v=4" width="20" height="20" alt=""> &nbsp; [deepset-ai](https://github.com/deepset-ai) / [haystack-core-integrations](https://github.com/deepset-ai/haystack-core-integrations) | 122 |
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/134601687?s=40&v=4" width="20" height="20" alt=""> &nbsp; [langfuse](https://github.com/langfuse) / [langfuse-python](https://github.com/langfuse/langfuse-python) | 108 |
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/42655961?s=40&v=4" width="20" height="20" alt=""> &nbsp; [zozoheir](https://github.com/zozoheir) / [tinyllm](https://github.com/zozoheir/tinyllm) | 95 |
+114 -44
View File
@@ -1,34 +1,53 @@
version: "3.5"
services:
server:
langfuse-web:
build:
dockerfile: ./web/Dockerfile
context: .
args:
- NEXT_PUBLIC_LANGFUSE_CLOUD_REGION=${NEXT_PUBLIC_LANGFUSE_CLOUD_REGION}
depends_on:
- db
- redis
depends_on: &langfuse-depends-on
postgres:
condition: service_healthy
minio:
condition: service_healthy
redis:
condition: service_healthy
clickhouse:
condition: service_healthy
ports:
- "3000:3000"
environment:
- DATABASE_URL=postgresql://postgres:postgres@db:5432/postgres
- NEXTAUTH_SECRET=mysecret
- SALT=mysalt
- ENCRYPTION_KEY=0000000000000000000000000000000000000000000000000000000000000000 # generate via `openssl rand -hex 32`
- NEXTAUTH_URL=http://localhost:3000
- TELEMETRY_ENABLED=${TELEMETRY_ENABLED:-true}
- LANGFUSE_ENABLE_EXPERIMENTAL_FEATURES=${LANGFUSE_ENABLE_EXPERIMENTAL_FEATURES:-false}
- LANGFUSE_INIT_ORG_ID=${LANGFUSE_INIT_ORG_ID:-}
- LANGFUSE_INIT_ORG_NAME=${LANGFUSE_INIT_ORG_NAME:-}
- LANGFUSE_INIT_PROJECT_ID=${LANGFUSE_INIT_PROJECT_ID:-}
- LANGFUSE_INIT_PROJECT_NAME=${LANGFUSE_INIT_PROJECT_NAME:-}
- LANGFUSE_INIT_PROJECT_PUBLIC_KEY=${LANGFUSE_INIT_PROJECT_PUBLIC_KEY:-}
- LANGFUSE_INIT_PROJECT_SECRET_KEY=${LANGFUSE_INIT_PROJECT_SECRET_KEY:-}
- LANGFUSE_INIT_USER_EMAIL=${LANGFUSE_INIT_USER_EMAIL:-}
- LANGFUSE_INIT_USER_NAME=${LANGFUSE_INIT_USER_NAME:-}
- LANGFUSE_INIT_USER_PASSWORD=${LANGFUSE_INIT_USER_PASSWORD:-}
environment: &langfuse-web-env
DATABASE_URL: postgresql://postgres:postgres@postgres:5432/postgres
NEXTAUTH_SECRET: mysecret
SALT: mysalt
ENCRYPTION_KEY: "0000000000000000000000000000000000000000000000000000000000000000" # generate via `openssl rand -hex 32`
NEXTAUTH_URL: http://localhost:3000
TELEMETRY_ENABLED: ${TELEMETRY_ENABLED:-true}
LANGFUSE_ENABLE_EXPERIMENTAL_FEATURES: ${LANGFUSE_ENABLE_EXPERIMENTAL_FEATURES:-false}
LANGFUSE_INIT_ORG_ID: ${LANGFUSE_INIT_ORG_ID:-}
LANGFUSE_INIT_ORG_NAME: ${LANGFUSE_INIT_ORG_NAME:-}
LANGFUSE_INIT_PROJECT_ID: ${LANGFUSE_INIT_PROJECT_ID:-}
LANGFUSE_INIT_PROJECT_NAME: ${LANGFUSE_INIT_PROJECT_NAME:-}
LANGFUSE_INIT_PROJECT_PUBLIC_KEY: ${LANGFUSE_INIT_PROJECT_PUBLIC_KEY:-}
LANGFUSE_INIT_PROJECT_SECRET_KEY: ${LANGFUSE_INIT_PROJECT_SECRET_KEY:-}
LANGFUSE_INIT_USER_EMAIL: ${LANGFUSE_INIT_USER_EMAIL:-}
LANGFUSE_INIT_USER_NAME: ${LANGFUSE_INIT_USER_NAME:-}
LANGFUSE_INIT_USER_PASSWORD: ${LANGFUSE_INIT_USER_PASSWORD:-}
CLICKHOUSE_MIGRATION_URL: ${CLICKHOUSE_MIGRATION_URL:-clickhouse://clickhouse:9000}
CLICKHOUSE_URL: ${CLICKHOUSE_URL:-http://clickhouse:8123}
CLICKHOUSE_USER: ${CLICKHOUSE_USER:-clickhouse}
CLICKHOUSE_PASSWORD: ${CLICKHOUSE_PASSWORD:-clickhouse}
CLICKHOUSE_CLUSTER_ENABLED: ${CLICKHOUSE_CLUSTER_ENABLED:-false}
LANGFUSE_S3_EVENT_UPLOAD_BUCKET: ${LANGFUSE_S3_EVENT_UPLOAD_BUCKET:-langfuse}
LANGFUSE_S3_EVENT_UPLOAD_REGION: ${LANGFUSE_S3_EVENT_UPLOAD_REGION:-us-east-1}
LANGFUSE_S3_EVENT_UPLOAD_ACCESS_KEY_ID: ${LANGFUSE_S3_EVENT_UPLOAD_ACCESS_KEY_ID:-minio}
LANGFUSE_S3_EVENT_UPLOAD_SECRET_ACCESS_KEY: ${LANGFUSE_S3_EVENT_UPLOAD_SECRET_ACCESS_KEY:-miniosecret}
LANGFUSE_S3_EVENT_UPLOAD_ENDPOINT: ${LANGFUSE_S3_EVENT_UPLOAD_ENDPOINT:-http://minio:9000}
LANGFUSE_S3_EVENT_UPLOAD_FORCE_PATH_STYLE: ${LANGFUSE_S3_EVENT_UPLOAD_FORCE_PATH_STYLE:-true}
LANGFUSE_S3_EVENT_UPLOAD_PREFIX: ${LANGFUSE_S3_EVENT_UPLOAD_PREFIX:-events/}
REDIS_HOST: ${REDIS_HOST:-redis}
REDIS_PORT: ${REDIS_PORT:-6379}
REDIS_AUTH: ${REDIS_AUTH:-myredissecret}
restart: always
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:3000/api/public/health"]
@@ -36,26 +55,17 @@ services:
timeout: 10s
retries: 3
worker:
langfuse-worker:
build:
dockerfile: ./worker/Dockerfile
context: .
args:
- NEXT_PUBLIC_LANGFUSE_CLOUD_REGION=${NEXT_PUBLIC_LANGFUSE_CLOUD_REGION}
depends_on:
- db
- redis
depends_on: *langfuse-depends-on
ports:
- "3030:3030"
environment:
- DATABASE_URL=postgresql://postgres:postgres@db:5432/postgres
- NEXTAUTH_SECRET=mysecret
- TELEMETRY_ENABLED=${TELEMETRY_ENABLED:-true}
- LANGFUSE_ENABLE_EXPERIMENTAL_FEATURES=${LANGFUSE_ENABLE_EXPERIMENTAL_FEATURES:-false}
- PORT=${PORT:-3030}
- REDIS_HOST=${REDIS_HOST:-redis}
- REDIS_PORT=${REDIS_PORT:-6379}
- REDIS_AUTH=${REDIS_AUTH:-myredissecret}
<<: *langfuse-web-env
restart: always
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:3030/api/health"]
@@ -63,26 +73,86 @@ services:
timeout: 10s
retries: 3
clickhouse:
image: clickhouse/clickhouse-server
user: "101:101"
container_name: clickhouse
hostname: clickhouse
environment:
CLICKHOUSE_DB: default
CLICKHOUSE_USER: clickhouse
CLICKHOUSE_PASSWORD: clickhouse
volumes:
- langfuse_clickhouse_data:/var/lib/clickhouse
- langfuse_clickhouse_logs:/var/log/clickhouse-server
ports:
- "8123:8123"
- "9000:9000"
healthcheck:
test: wget --no-verbose --tries=1 --spider http://localhost:8123/ping || exit 1
interval: 5s
timeout: 5s
retries: 10
start_period: 1s
minio:
image: minio/minio
container_name: minio
entrypoint: sh
# create the 'langfuse' bucket before starting the service
command: -c 'mkdir -p /data/langfuse && minio server --address ":9000" --console-address ":9001" /data'
environment:
MINIO_ROOT_USER: minio
MINIO_ROOT_PASSWORD: miniosecret
ports:
- "9090:9000"
- "9091:9001"
volumes:
- langfuse_minio_data:/data
healthcheck:
test: ["CMD", "mc", "ready", "local"]
interval: 1s
timeout: 5s
retries: 5
start_period: 1s
redis:
image: redis:7.2.4
image: redis:7
restart: always
command: >
--requirepass ${REDIS_AUTH:-myredissecret}
restart: always
ports:
- 6379:6379
healthcheck:
test: ["CMD", "redis-cli", "ping"]
interval: 3s
timeout: 10s
retries: 10
db:
image: postgres
postgres:
image: postgres:${POSTGRES_VERSION:-latest}
restart: always
healthcheck:
test: ["CMD-SHELL", "pg_isready -U postgres"]
interval: 3s
timeout: 3s
retries: 10
environment:
- POSTGRES_USER=postgres
- POSTGRES_PASSWORD=postgres
- POSTGRES_DB=postgres
POSTGRES_USER: postgres
POSTGRES_PASSWORD: postgres
POSTGRES_DB: postgres
ports:
- 5432:5432
volumes:
- database_data:/var/lib/postgresql/data
- langfuse_postgres_data:/var/lib/postgresql/data
volumes:
database_data:
langfuse_postgres_data:
driver: local
langfuse_clickhouse_data:
driver: local
langfuse_clickhouse_logs:
driver: local
langfuse_minio_data:
driver: local
-145
View File
@@ -1,145 +0,0 @@
services:
langfuse-worker:
image: langfuse/langfuse-worker:latest
depends_on: &langfuse-depends-on
postgres:
condition: service_healthy
minio:
condition: service_healthy
redis:
condition: service_healthy
ports:
- "3030:3030"
environment: &langfuse-worker-env
DATABASE_URL: postgresql://postgres:postgres@postgres:5432/postgres
SALT: "mysalt"
ENCRYPTION_KEY: "0000000000000000000000000000000000000000000000000000000000000000" # generate via `openssl rand -hex 32`
TELEMETRY_ENABLED: ${TELEMETRY_ENABLED:-true}
LANGFUSE_ENABLE_EXPERIMENTAL_FEATURES: ${LANGFUSE_ENABLE_EXPERIMENTAL_FEATURES:-true}
LANGFUSE_ASYNC_INGESTION_PROCESSING: ${LANGFUSE_ASYNC_INGESTION_PROCESSING:-true}
LANGFUSE_ASYNC_CLICKHOUSE_INGESTION_PROCESSING: ${LANGFUSE_ASYNC_CLICKHOUSE_INGESTION_PROCESSING:-true}
LANGFUSE_READ_DASHBOARDS_FROM_CLICKHOUSE: ${LANGFUSE_READ_DASHBOARDS_FROM_CLICKHOUSE:-true}
LANGFUSE_READ_FROM_POSTGRES_ONLY: ${LANGFUSE_READ_FROM_POSTGRES_ONLY:-false}
LANGFUSE_RETURN_FROM_CLICKHOUSE: ${LANGFUSE_RETURN_FROM_CLICKHOUSE:-true}
CLICKHOUSE_MIGRATION_URL: ${CLICKHOUSE_MIGRATION_URL:-clickhouse://clickhouse:9000}
CLICKHOUSE_URL: ${CLICKHOUSE_URL:-http://clickhouse:8123}
CLICKHOUSE_USER: ${CLICKHOUSE_USER:-clickhouse}
CLICKHOUSE_PASSWORD: ${CLICKHOUSE_PASSWORD:-clickhouse}
CLICKHOUSE_CLUSTER_ENABLED: ${CLICKHOUSE_CLUSTER_ENABLED:-false}
LANGFUSE_S3_EVENT_UPLOAD_ENABLED: ${LANGFUSE_S3_EVENT_UPLOAD_ENABLED:-true}
LANGFUSE_S3_EVENT_UPLOAD_BUCKET: ${LANGFUSE_S3_EVENT_UPLOAD_BUCKET:-langfuse}
LANGFUSE_S3_EVENT_UPLOAD_REGION: ${LANGFUSE_S3_EVENT_UPLOAD_REGION:-us-east-1}
LANGFUSE_S3_EVENT_UPLOAD_ACCESS_KEY_ID: ${LANGFUSE_S3_EVENT_UPLOAD_ACCESS_KEY_ID:-minio}
LANGFUSE_S3_EVENT_UPLOAD_SECRET_ACCESS_KEY: ${LANGFUSE_S3_EVENT_UPLOAD_SECRET_ACCESS_KEY:-miniosecret}
LANGFUSE_S3_EVENT_UPLOAD_ENDPOINT: ${LANGFUSE_S3_EVENT_UPLOAD_ENDPOINT:-http://minio:9000}
LANGFUSE_S3_EVENT_UPLOAD_FORCE_PATH_STYLE: ${LANGFUSE_S3_EVENT_UPLOAD_FORCE_PATH_STYLE:-true}
LANGFUSE_S3_EVENT_UPLOAD_PREFIX: ${LANGFUSE_S3_EVENT_UPLOAD_PREFIX:-events/}
LANGFUSE_S3_MEDIA_UPLOAD_ENABLED: ${LANGFUSE_S3_MEDIA_UPLOAD_ENABLED:-true}
LANGFUSE_S3_MEDIA_UPLOAD_BUCKET: ${LANGFUSE_S3_MEDIA_UPLOAD_BUCKET:-langfuse}
LANGFUSE_S3_MEDIA_UPLOAD_REGION: ${LANGFUSE_S3_MEDIA_UPLOAD_REGION:-us-east-1}
LANGFUSE_S3_MEDIA_UPLOAD_ACCESS_KEY_ID: ${LANGFUSE_S3_MEDIA_UPLOAD_ACCESS_KEY_ID:-minio}
LANGFUSE_S3_MEDIA_UPLOAD_SECRET_ACCESS_KEY: ${LANGFUSE_S3_MEDIA_UPLOAD_SECRET_ACCESS_KEY:-miniosecret}
LANGFUSE_S3_MEDIA_UPLOAD_ENDPOINT: ${LANGFUSE_S3_MEDIA_UPLOAD_ENDPOINT:-http://minio:9000}
LANGFUSE_S3_MEDIA_UPLOAD_FORCE_PATH_STYLE: ${LANGFUSE_S3_MEDIA_UPLOAD_FORCE_PATH_STYLE:-true}
LANGFUSE_S3_MEDIA_UPLOAD_PREFIX: ${LANGFUSE_S3_MEDIA_UPLOAD_PREFIX:-media/}
REDIS_HOST: ${REDIS_HOST:-redis}
REDIS_PORT: ${REDIS_PORT:-6379}
REDIS_AUTH: ${REDIS_AUTH:-myredissecret}
langfuse-web:
image: langfuse/langfuse:latest
depends_on: *langfuse-depends-on
ports:
- "3000:3000"
environment:
<<: *langfuse-worker-env
NEXTAUTH_URL: http://localhost:3000
NEXTAUTH_SECRET: mysecret
LANGFUSE_INIT_ORG_ID: ${LANGFUSE_INIT_ORG_ID:-}
LANGFUSE_INIT_ORG_NAME: ${LANGFUSE_INIT_ORG_NAME:-}
LANGFUSE_INIT_PROJECT_ID: ${LANGFUSE_INIT_PROJECT_ID:-}
LANGFUSE_INIT_PROJECT_NAME: ${LANGFUSE_INIT_PROJECT_NAME:-}
LANGFUSE_INIT_PROJECT_PUBLIC_KEY: ${LANGFUSE_INIT_PROJECT_PUBLIC_KEY:-}
LANGFUSE_INIT_PROJECT_SECRET_KEY: ${LANGFUSE_INIT_PROJECT_SECRET_KEY:-}
LANGFUSE_INIT_USER_EMAIL: ${LANGFUSE_INIT_USER_EMAIL:-}
LANGFUSE_INIT_USER_NAME: ${LANGFUSE_INIT_USER_NAME:-}
LANGFUSE_INIT_USER_PASSWORD: ${LANGFUSE_INIT_USER_PASSWORD:-}
clickhouse:
image: clickhouse/clickhouse-server
user: "101:101"
container_name: clickhouse
hostname: clickhouse
environment:
CLICKHOUSE_DB: default
CLICKHOUSE_USER: clickhouse
CLICKHOUSE_PASSWORD: clickhouse
volumes:
- langfuse_clickhouse_data:/var/lib/clickhouse
- langfuse_clickhouse_logs:/var/log/clickhouse-server
ports:
- "8123:8123"
- "9000:9000"
depends_on:
- postgres
minio:
image: minio/minio
container_name: minio
entrypoint: sh
# create the 'langfuse' bucket before starting the service
command: -c 'mkdir -p /data/langfuse && minio server --address ":9000" --console-address ":9001" /data'
environment:
MINIO_ROOT_USER: minio
MINIO_ROOT_PASSWORD: miniosecret
ports:
- "9090:9000"
- "9091:9001"
volumes:
- langfuse_minio_data:/data
healthcheck:
test: ["CMD", "mc", "ready", "local"]
interval: 1s
timeout: 5s
retries: 5
start_period: 1s
redis:
image: redis:7
restart: always
command: >
--requirepass ${REDIS_AUTH:-myredissecret}
ports:
- 6379:6379
healthcheck:
test: ["CMD", "redis-cli", "ping"]
interval: 3s
timeout: 10s
retries: 10
postgres:
image: postgres:${POSTGRES_VERSION:-latest}
restart: always
healthcheck:
test: ["CMD-SHELL", "pg_isready -U postgres"]
interval: 3s
timeout: 3s
retries: 10
environment:
POSTGRES_USER: postgres
POSTGRES_PASSWORD: postgres
POSTGRES_DB: postgres
ports:
- 5432:5432
volumes:
- langfuse_postgres_data:/var/lib/postgresql/data
volumes:
langfuse_postgres_data:
driver: local
langfuse_clickhouse_data:
driver: local
langfuse_clickhouse_logs:
driver: local
langfuse_minio_data:
driver: local
+118 -27
View File
@@ -1,31 +1,116 @@
services:
langfuse-server:
image: langfuse/langfuse:2
depends_on:
db:
langfuse-worker:
image: langfuse/langfuse-worker:main # TODO: Switch to :3 after go-live
depends_on: &langfuse-depends-on
postgres:
condition: service_healthy
minio:
condition: service_healthy
redis:
condition: service_healthy
clickhouse:
condition: service_healthy
ports:
- "3030:3030"
environment: &langfuse-worker-env
DATABASE_URL: postgresql://postgres:postgres@postgres:5432/postgres
SALT: "mysalt"
ENCRYPTION_KEY: "0000000000000000000000000000000000000000000000000000000000000000" # generate via `openssl rand -hex 32`
TELEMETRY_ENABLED: ${TELEMETRY_ENABLED:-true}
LANGFUSE_ENABLE_EXPERIMENTAL_FEATURES: ${LANGFUSE_ENABLE_EXPERIMENTAL_FEATURES:-true}
CLICKHOUSE_MIGRATION_URL: ${CLICKHOUSE_MIGRATION_URL:-clickhouse://clickhouse:9000}
CLICKHOUSE_URL: ${CLICKHOUSE_URL:-http://clickhouse:8123}
CLICKHOUSE_USER: ${CLICKHOUSE_USER:-clickhouse}
CLICKHOUSE_PASSWORD: ${CLICKHOUSE_PASSWORD:-clickhouse}
CLICKHOUSE_CLUSTER_ENABLED: ${CLICKHOUSE_CLUSTER_ENABLED:-false}
LANGFUSE_S3_EVENT_UPLOAD_BUCKET: ${LANGFUSE_S3_EVENT_UPLOAD_BUCKET:-langfuse}
LANGFUSE_S3_EVENT_UPLOAD_REGION: ${LANGFUSE_S3_EVENT_UPLOAD_REGION:-us-east-1}
LANGFUSE_S3_EVENT_UPLOAD_ACCESS_KEY_ID: ${LANGFUSE_S3_EVENT_UPLOAD_ACCESS_KEY_ID:-minio}
LANGFUSE_S3_EVENT_UPLOAD_SECRET_ACCESS_KEY: ${LANGFUSE_S3_EVENT_UPLOAD_SECRET_ACCESS_KEY:-miniosecret}
LANGFUSE_S3_EVENT_UPLOAD_ENDPOINT: ${LANGFUSE_S3_EVENT_UPLOAD_ENDPOINT:-http://minio:9000}
LANGFUSE_S3_EVENT_UPLOAD_FORCE_PATH_STYLE: ${LANGFUSE_S3_EVENT_UPLOAD_FORCE_PATH_STYLE:-true}
LANGFUSE_S3_EVENT_UPLOAD_PREFIX: ${LANGFUSE_S3_EVENT_UPLOAD_PREFIX:-events/}
REDIS_HOST: ${REDIS_HOST:-redis}
REDIS_PORT: ${REDIS_PORT:-6379}
REDIS_AUTH: ${REDIS_AUTH:-myredissecret}
langfuse-web:
image: langfuse/langfuse:main # TODO: Switch to :3 after go-live
depends_on: *langfuse-depends-on
ports:
- "3000:3000"
environment:
- DATABASE_URL=postgresql://postgres:postgres@db:5432/postgres
- NEXTAUTH_SECRET=mysecret
- SALT=mysalt
- ENCRYPTION_KEY=0000000000000000000000000000000000000000000000000000000000000000 # generate via `openssl rand -hex 32`
- NEXTAUTH_URL=http://localhost:3000
- TELEMETRY_ENABLED=${TELEMETRY_ENABLED:-true}
- LANGFUSE_ENABLE_EXPERIMENTAL_FEATURES=${LANGFUSE_ENABLE_EXPERIMENTAL_FEATURES:-false}
- LANGFUSE_INIT_ORG_ID=${LANGFUSE_INIT_ORG_ID:-}
- LANGFUSE_INIT_ORG_NAME=${LANGFUSE_INIT_ORG_NAME:-}
- LANGFUSE_INIT_PROJECT_ID=${LANGFUSE_INIT_PROJECT_ID:-}
- LANGFUSE_INIT_PROJECT_NAME=${LANGFUSE_INIT_PROJECT_NAME:-}
- LANGFUSE_INIT_PROJECT_PUBLIC_KEY=${LANGFUSE_INIT_PROJECT_PUBLIC_KEY:-}
- LANGFUSE_INIT_PROJECT_SECRET_KEY=${LANGFUSE_INIT_PROJECT_SECRET_KEY:-}
- LANGFUSE_INIT_USER_EMAIL=${LANGFUSE_INIT_USER_EMAIL:-}
- LANGFUSE_INIT_USER_NAME=${LANGFUSE_INIT_USER_NAME:-}
- LANGFUSE_INIT_USER_PASSWORD=${LANGFUSE_INIT_USER_PASSWORD:-}
<<: *langfuse-worker-env
NEXTAUTH_URL: http://localhost:3000
NEXTAUTH_SECRET: mysecret
LANGFUSE_INIT_ORG_ID: ${LANGFUSE_INIT_ORG_ID:-}
LANGFUSE_INIT_ORG_NAME: ${LANGFUSE_INIT_ORG_NAME:-}
LANGFUSE_INIT_PROJECT_ID: ${LANGFUSE_INIT_PROJECT_ID:-}
LANGFUSE_INIT_PROJECT_NAME: ${LANGFUSE_INIT_PROJECT_NAME:-}
LANGFUSE_INIT_PROJECT_PUBLIC_KEY: ${LANGFUSE_INIT_PROJECT_PUBLIC_KEY:-}
LANGFUSE_INIT_PROJECT_SECRET_KEY: ${LANGFUSE_INIT_PROJECT_SECRET_KEY:-}
LANGFUSE_INIT_USER_EMAIL: ${LANGFUSE_INIT_USER_EMAIL:-}
LANGFUSE_INIT_USER_NAME: ${LANGFUSE_INIT_USER_NAME:-}
LANGFUSE_INIT_USER_PASSWORD: ${LANGFUSE_INIT_USER_PASSWORD:-}
db:
image: postgres
clickhouse:
image: clickhouse/clickhouse-server
user: "101:101"
container_name: clickhouse
hostname: clickhouse
environment:
CLICKHOUSE_DB: default
CLICKHOUSE_USER: clickhouse
CLICKHOUSE_PASSWORD: clickhouse
volumes:
- langfuse_clickhouse_data:/var/lib/clickhouse
- langfuse_clickhouse_logs:/var/log/clickhouse-server
ports:
- "8123:8123"
- "9000:9000"
healthcheck:
test: wget --no-verbose --tries=1 --spider http://localhost:8123/ping || exit 1
interval: 5s
timeout: 5s
retries: 10
start_period: 1s
minio:
image: minio/minio
container_name: minio
entrypoint: sh
# create the 'langfuse' bucket before starting the service
command: -c 'mkdir -p /data/langfuse && minio server --address ":9000" --console-address ":9001" /data'
environment:
MINIO_ROOT_USER: minio
MINIO_ROOT_PASSWORD: miniosecret
ports:
- "9090:9000"
- "9091:9001"
volumes:
- langfuse_minio_data:/data
healthcheck:
test: ["CMD", "mc", "ready", "local"]
interval: 1s
timeout: 5s
retries: 5
start_period: 1s
redis:
image: redis:7
restart: always
command: >
--requirepass ${REDIS_AUTH:-myredissecret}
ports:
- 6379:6379
healthcheck:
test: ["CMD", "redis-cli", "ping"]
interval: 3s
timeout: 10s
retries: 10
postgres:
image: postgres:${POSTGRES_VERSION:-latest}
restart: always
healthcheck:
test: ["CMD-SHELL", "pg_isready -U postgres"]
@@ -33,14 +118,20 @@ services:
timeout: 3s
retries: 10
environment:
- POSTGRES_USER=postgres
- POSTGRES_PASSWORD=postgres
- POSTGRES_DB=postgres
POSTGRES_USER: postgres
POSTGRES_PASSWORD: postgres
POSTGRES_DB: postgres
ports:
- 5432:5432
volumes:
- database_data:/var/lib/postgresql/data
- langfuse_postgres_data:/var/lib/postgresql/data
volumes:
database_data:
langfuse_postgres_data:
driver: local
langfuse_clickhouse_data:
driver: local
langfuse_clickhouse_logs:
driver: local
langfuse_minio_data:
driver: local
+2 -5
View File
@@ -1,12 +1,9 @@
import { z } from "zod";
import { env as sharedEnv, removeEmptyEnvVariables } from "@langfuse/shared";
import { removeEmptyEnvVariables } from "@langfuse/shared";
const EnvSchema = z.object({
NEXT_PUBLIC_LANGFUSE_CLOUD_REGION: z.string().optional(),
LANGFUSE_EE_LICENSE_KEY: z.string().optional(),
});
export const env = {
...sharedEnv,
...EnvSchema.parse(removeEmptyEnvVariables(process.env)),
};
export const env = EnvSchema.parse(removeEmptyEnvVariables(process.env));
+1 -1
View File
@@ -99,5 +99,5 @@ types:
docs: The unique langfuse identifier of a media record
MediaContentType:
type: literal<"image/png","image/jpeg","image/jpg","image/webp","audio/mpeg","audio/mp3","audio/wav","text/plain","application/pdf">
type: literal<"image/png","image/jpeg","image/jpg","image/webp","image/gif","image/svg+xml","image/tiff","image/bmp","audio/mpeg","audio/mp3","audio/wav","audio/ogg","audio/oga","audio/aac","audio/mp4","audio/flac","video/mp4","video/webm","text/plain","text/html","text/css","text/csv","application/pdf","application/msword","application/vnd.ms-excel","application/zip","application/json","application/xml","application/octet-stream">
docs: The MIME type of the media record
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "langfuse",
"version": "2.92.0",
"version": "3.0.0-rc.3",
"author": "engineering@langfuse.com",
"license": "MIT",
"private": true,
+10 -10
View File
@@ -19,16 +19,7 @@ then
fi
# Construct the database URL
if [ "$CLICKHOUSE_CLUSTER_ENABLED" == "true" ] ; then
if [ "$CLICKHOUSE_MIGRATION_SSL" = true ] ; then
DATABASE_URL="${CLICKHOUSE_MIGRATION_URL}?username=${CLICKHOUSE_USER}&password=${CLICKHOUSE_PASSWORD}&database=default&x-multi-statement=true&secure=true&skip_verify=true&x-cluster-name=default&x-migrations-table-engine=ReplicatedMergeTree"
else
DATABASE_URL="${CLICKHOUSE_MIGRATION_URL}?username=${CLICKHOUSE_USER}&password=${CLICKHOUSE_PASSWORD}&database=default&x-multi-statement=true&x-cluster-name=default&x-migrations-table-engine=ReplicatedMergeTree"
fi
# Execute the up command
migrate -source file://clickhouse/migrations/clustered -database "$DATABASE_URL" down
else
if [ "$CLICKHOUSE_CLUSTER_ENABLED" == "false" ] ; then
if [ "$CLICKHOUSE_MIGRATION_SSL" = true ] ; then
DATABASE_URL="${CLICKHOUSE_MIGRATION_URL}?username=${CLICKHOUSE_USER}&password=${CLICKHOUSE_PASSWORD}&database=default&x-multi-statement=true&secure=true&skip_verify=true&x-migrations-table-engine=MergeTree"
else
@@ -37,4 +28,13 @@ else
# Execute the up command
migrate -source file://clickhouse/migrations/unclustered -database "$DATABASE_URL" down
else
if [ "$CLICKHOUSE_MIGRATION_SSL" = true ] ; then
DATABASE_URL="${CLICKHOUSE_MIGRATION_URL}?username=${CLICKHOUSE_USER}&password=${CLICKHOUSE_PASSWORD}&database=default&x-multi-statement=true&secure=true&skip_verify=true&x-cluster-name=default&x-migrations-table-engine=ReplicatedMergeTree"
else
DATABASE_URL="${CLICKHOUSE_MIGRATION_URL}?username=${CLICKHOUSE_USER}&password=${CLICKHOUSE_PASSWORD}&database=default&x-multi-statement=true&x-cluster-name=default&x-migrations-table-engine=ReplicatedMergeTree"
fi
# Execute the up command
migrate -source file://clickhouse/migrations/clustered -database "$DATABASE_URL" down
fi
+1 -1
View File
@@ -2,7 +2,7 @@ import {
clickhouseClient,
ObservationRecordReadType,
} from "@langfuse/shared/src/server";
import { Prisma, prisma } from "../../src/db";
import { prisma } from "../../src/db";
import { redis } from "@langfuse/shared/src/server";
import { prepareClickhouse } from "../../scripts/prepareClickhouse";
import { createDatasets } from "../../prisma/seed";
+10 -10
View File
@@ -19,16 +19,7 @@ then
fi
# Construct the database URL
if [ "$CLICKHOUSE_CLUSTER_ENABLED" == "true" ] ; then
if [ "$CLICKHOUSE_MIGRATION_SSL" = true ] ; then
DATABASE_URL="${CLICKHOUSE_MIGRATION_URL}?username=${CLICKHOUSE_USER}&password=${CLICKHOUSE_PASSWORD}&database=default&x-multi-statement=true&secure=true&skip_verify=true&x-cluster-name=default&x-migrations-table-engine=ReplicatedMergeTree"
else
DATABASE_URL="${CLICKHOUSE_MIGRATION_URL}?username=${CLICKHOUSE_USER}&password=${CLICKHOUSE_PASSWORD}&database=default&x-multi-statement=true&x-cluster-name=default&x-migrations-table-engine=ReplicatedMergeTree"
fi
# Execute the up command
migrate -source file://clickhouse/migrations/clustered -database "$DATABASE_URL" up
else
if [ "$CLICKHOUSE_CLUSTER_ENABLED" == "false" ] ; then
if [ "$CLICKHOUSE_MIGRATION_SSL" = true ] ; then
DATABASE_URL="${CLICKHOUSE_MIGRATION_URL}?username=${CLICKHOUSE_USER}&password=${CLICKHOUSE_PASSWORD}&database=default&x-multi-statement=true&secure=true&skip_verify=true&x-migrations-table-engine=MergeTree"
else
@@ -37,4 +28,13 @@ else
# Execute the up command
migrate -source file://clickhouse/migrations/unclustered -database "$DATABASE_URL" up
else
if [ "$CLICKHOUSE_MIGRATION_SSL" = true ] ; then
DATABASE_URL="${CLICKHOUSE_MIGRATION_URL}?username=${CLICKHOUSE_USER}&password=${CLICKHOUSE_PASSWORD}&database=default&x-multi-statement=true&secure=true&skip_verify=true&x-cluster-name=default&x-migrations-table-engine=ReplicatedMergeTree"
else
DATABASE_URL="${CLICKHOUSE_MIGRATION_URL}?username=${CLICKHOUSE_USER}&password=${CLICKHOUSE_PASSWORD}&database=default&x-multi-statement=true&x-cluster-name=default&x-migrations-table-engine=ReplicatedMergeTree"
fi
# Execute the up command
migrate -source file://clickhouse/migrations/clustered -database "$DATABASE_URL" up
fi
+1
View File
@@ -64,6 +64,7 @@
"@langchain/anthropic": "^0.3.8",
"@langchain/aws": "^0.1.2",
"@langchain/core": "^0.3.18",
"@langchain/google-vertexai": "^0.1.3",
"@langchain/openai": "^0.3.14",
"@opentelemetry/api": ">=1.0.0 <1.10.0",
"@prisma/client": "^5.22.0",
+3
View File
@@ -450,6 +450,9 @@ export async function createDatasets(
Math.random() > 0.3
? observations[Math.floor(Math.random() * observations.length)]
: undefined;
if (!sourceObservation) {
continue;
}
const datasetItem = await prisma.datasetItem.create({
data: {
projectId,
+1 -1
View File
@@ -110,7 +110,7 @@ export const prepareClickhouse = async (
map('input', toDecimal64(randUniform(0, 1000), 12), 'output', toDecimal64(randUniform(0, 1000), 12), 'total', toDecimal64(randUniform(0, 2000), 12)) AS provided_cost_details,
map('input', toDecimal64(randUniform(0, 1000), 12), 'output', toDecimal64(randUniform(0, 1000), 12), 'total', toDecimal64(randUniform(0, 2000), 12)) AS cost_details,
toDecimal64(randUniform(0, 2000), 12) AS total_cost,
start_time AS completion_start_time,
addMilliseconds(start_time, if(rand() < 0.6, floor(randUniform(0, 500)), floor(randUniform(0, 600)))) AS completion_start_time,
array(${SEED_PROMPTS.map((p) => `concat('${p.id}',project_id)`).join(
",",
)})[(number % ${SEED_PROMPTS.length})+1] AS prompt_id,
+17 -10
View File
@@ -27,15 +27,18 @@ const EnvSchema = z.object({
.optional(),
LANGFUSE_CACHE_PROMPT_ENABLED: z.enum(["true", "false"]).default("false"),
LANGFUSE_CACHE_PROMPT_TTL_SECONDS: z.coerce.number().default(60 * 60),
CLICKHOUSE_URL: z.string().url().optional(),
CLICKHOUSE_USER: z.string().optional(),
CLICKHOUSE_PASSWORD: z.string().optional(),
LANGFUSE_ASYNC_CLICKHOUSE_INGESTION_PROCESSING: z
CLICKHOUSE_URL: z.string().url(),
CLICKHOUSE_USER: z.string(),
CLICKHOUSE_PASSWORD: z.string(),
LANGFUSE_CLICKHOUSE_INGESTION_ENABLED: z
.enum(["true", "false"])
.default("false"),
LANGFUSE_ASYNC_INGESTION_PROCESSING: z
.default("true"),
// TODO: Disable Postgres before go-live
LANGFUSE_POSTGRES_INGESTION_ENABLED: z
.enum(["true", "false"])
.default("false"),
.default("true"),
LANGFUSE_INGESTION_QUEUE_DELAY_MS: z.coerce
.number()
.nonnegative()
@@ -48,8 +51,9 @@ const EnvSchema = z.object({
ENABLE_AWS_CLOUDWATCH_METRIC_PUBLISHING: z
.enum(["true", "false"])
.default("false"),
LANGFUSE_S3_EVENT_UPLOAD_ENABLED: z.enum(["true", "false"]).default("false"),
LANGFUSE_S3_EVENT_UPLOAD_BUCKET: z.string().optional(),
LANGFUSE_S3_EVENT_UPLOAD_BUCKET: z.string({
required_error: "Langfuse requires a bucket name for S3 Event Uploads.",
}),
LANGFUSE_S3_EVENT_UPLOAD_PREFIX: z.string().default(""),
LANGFUSE_S3_EVENT_UPLOAD_REGION: z.string().optional(),
LANGFUSE_S3_EVENT_UPLOAD_ENDPOINT: z.string().optional(),
@@ -62,4 +66,7 @@ const EnvSchema = z.object({
STRIPE_SECRET_KEY: z.string().optional(),
});
export const env = EnvSchema.parse(removeEmptyEnvVariables(process.env));
export const env: z.infer<typeof EnvSchema> =
process.env.DOCKER_BUILD === "1"
? (process.env as any)
: EnvSchema.parse(removeEmptyEnvVariables(process.env));
@@ -3,15 +3,17 @@ export const planLabels = {
"cloud:hobby": "Hobby",
"cloud:pro": "Pro",
"cloud:team": "Team",
"self-hosted:enterprise": "Enterprise",
"self-hosted:pro": "Pro (self-hosted)",
"self-hosted:enterprise": "Enterprise (self-hosted)",
} as const;
export type Plan = keyof typeof planLabels;
export const plans = Object.keys(planLabels) as Plan[];
// This function is kept here to ensure consistency when updating plan names in the future.
// These functions are kept here to ensure consistency when updating plan names in the future.
export const isCloudPlan = (plan: Plan) => plan.startsWith("cloud");
export const isSelfHostedPlan = (plan: Plan) => plan.startsWith("self-hosted");
export const isPlan = (value: string): value is Plan =>
plans.includes(value as Plan);
-2
View File
@@ -7,7 +7,6 @@ export * from "./interfaces/customLLMProviderConfigSchemas";
export * from "./tableDefinitions";
export * from "./types";
export * from "./tableDefinitions/tracesTable";
export * from "./server/auth/apiKeys";
export * from "./observationsTable";
export * from "./utils/zod";
export * from "./utils/json";
@@ -16,7 +15,6 @@ export * from "./utils/objects";
export * from "./utils/typeChecks";
export * from "./features/entitlements/plans";
export * from "./interfaces/rate-limits";
export { env } from "./env";
// llm api
export * from "./server/llm/types";
@@ -10,3 +10,19 @@ export const BedrockCredentialSchema = z
})
.optional();
export type BedrockCredential = z.infer<typeof BedrockCredentialSchema>;
export const GCPServiceAccountKeySchema = z.object({
type: z.literal("service_account"),
project_id: z.string(),
private_key_id: z.string(),
private_key: z.string(),
client_email: z.string(),
client_id: z.string(),
auth_uri: z.string(),
token_uri: z.string(),
auth_provider_x509_cert_url: z.string(),
client_x509_cert_url: z.string(),
});
export type GCPServiceAccountKey = z.infer<typeof GCPServiceAccountKeySchema>;
export default GCPServiceAccountKeySchema;
@@ -0,0 +1,65 @@
import type { OAuthConfig, OAuthUserConfig } from "next-auth/providers/oauth";
import type { GithubProfile, GithubEmail } from "next-auth/providers/github";
export function GitHubEnterpriseProvider<P extends GithubProfile>(
options: OAuthUserConfig<P> & {
enterprise?: {
baseUrl?: string;
};
}
): OAuthConfig<P> {
const baseUrl = options?.enterprise?.baseUrl ?? "https://github.com"
const apiBaseUrl = options?.enterprise?.baseUrl
? `${options?.enterprise?.baseUrl}/api/v3`
: "https://api.github.com"
return {
id: "github-enterprise",
name: "GitHub Enterprise",
type: "oauth",
authorization: {
url: `${baseUrl}/login/oauth/authorize`,
params: { scope: "read:user user:email" },
},
token: `${baseUrl}/login/oauth/access_token`,
userinfo: {
url: `${apiBaseUrl}/user`,
async request({ client, tokens }) {
const profile = await client.userinfo(tokens.access_token!)
if (!profile.email) {
// If the user does not have a public email, get another via the GitHub API
// See https://docs.github.com/en/rest/users/emails#list-email-addresses-for-the-authenticated-user
const res = await fetch(`${apiBaseUrl}/user/emails`, {
headers: { Authorization: `token ${tokens.access_token}` },
})
if (res.ok) {
const emails: GithubEmail[] = await res.json()
profile.email = (emails.find((e) => e.primary) ?? emails[0]).email
}
}
return profile
},
},
profile(profile) {
return {
id: profile.id.toString(),
name: profile.name ?? profile.login,
email: profile.email,
image: profile.avatar_url,
}
},
style: {
logo: "https://raw.githubusercontent.com/nextauthjs/next-auth/main/packages/next-auth/provider-logos/github.svg",
logoDark:
"https://raw.githubusercontent.com/nextauthjs/next-auth/main/packages/next-auth/provider-logos/github-dark.svg",
bg: "#fff",
bgDark: "#000",
text: "#000",
textDark: "#fff",
},
options,
}
}
+3 -1
View File
@@ -6,6 +6,7 @@ export * from "./services/PromptService";
export * from "./services/traces-ui-table-service";
export * from "./auth/apiKeys";
export * from "./auth/customSsoProvider";
export * from "./auth/gitHubEnterpriseProvider";
export * from "./llm/fetchLLMCompletion";
export * from "./llm/types";
export * from "./utils/DatabaseReadStream";
@@ -21,8 +22,9 @@ export * from "../server/ingestion/types";
export * from "../server/ingestion/validateAndInflateScore";
export * from "./redis/redis";
export * from "./redis/traceUpsert";
export * from "./redis/CloudUsageMeteringQueue";
export * from "./redis/cloudUsageMeteringQueue";
export * from "./redis/getQueue";
export * from "./redis/traceDelete";
export * from "./redis/datasetRunItemUpsert";
export * from "./redis/batchExport";
export * from "./redis/legacyIngestion";
@@ -83,7 +83,7 @@ export const handleBatch = async (
async function retry<T>(request: () => Promise<T>): Promise<T> {
return await backOff(request, {
numOfAttempts: env.LANGFUSE_ASYNC_INGESTION_PROCESSING === "true" ? 5 : 3,
numOfAttempts: 5,
retry: (e: Error, attemptNumber: number) => {
if (e instanceof UnauthorizedError || e instanceof ForbiddenError) {
logger.info("not retrying auth error");
@@ -156,91 +156,89 @@ export const processEventBatch = async (
* ASYNC PROCESSING *
********************/
let s3UploadErrored = false;
if (env.LANGFUSE_S3_EVENT_UPLOAD_ENABLED === "true") {
await instrumentAsync({ name: "s3-upload-events" }, async () => {
if (env.LANGFUSE_S3_EVENT_UPLOAD_BUCKET === undefined) {
throw new Error("S3 event store is enabled but no bucket is set");
}
const s3Client = getS3StorageServiceClient(
env.LANGFUSE_S3_EVENT_UPLOAD_BUCKET,
);
// S3 Event Upload is currently blocking, but non-failing.
// If a promise rejects, we log it below, but do not throw an error.
// In this case, we upload the full batch into the Redis queue.
const results = await Promise.allSettled(
Object.keys(sortedBatchByEventBodyId).map(async (id) => {
// We upload the event in an array to the S3 bucket grouped by the eventBodyId.
// That way we batch updates from the same invocation into a single file and reduce
// write operations on S3.
const { data, key, type, eventBodyId } = sortedBatchByEventBodyId[id];
return s3Client.uploadJson(
`${env.LANGFUSE_S3_EVENT_UPLOAD_PREFIX}${authCheck.scope.projectId}/${getClickhouseEntityType(type)}/${eventBodyId}/${key}.json`,
data,
);
}),
);
results.forEach((result) => {
if (result.status === "rejected") {
s3UploadErrored = true;
logger.error("Failed to upload event to S3", {
error: result.reason,
});
}
});
});
}
// Send each event individually to IngestionQueue for new processing
if (
env.LANGFUSE_ASYNC_INGESTION_PROCESSING === "true" &&
env.LANGFUSE_S3_EVENT_UPLOAD_ENABLED === "true" &&
env.LANGFUSE_ASYNC_CLICKHOUSE_INGESTION_PROCESSING === "true" &&
redis &&
!s3UploadErrored
) {
const queue = IngestionQueue.getInstance();
await instrumentAsync({ name: "s3-upload-events" }, async () => {
const s3Client = getS3StorageServiceClient(
env.LANGFUSE_S3_EVENT_UPLOAD_BUCKET,
);
// S3 Event Upload is blocking, but non-failing.
// If a promise rejects, we log it below, but do not throw an error.
// In this case, we upload the full batch into the Redis queue.
const results = await Promise.allSettled(
Object.keys(sortedBatchByEventBodyId).map(async (id) =>
queue
? queue.add(
QueueJobs.IngestionJob,
{
id: randomUUID(),
timestamp: new Date(),
name: QueueJobs.IngestionJob as const,
payload: {
data: {
type: sortedBatchByEventBodyId[id].type,
eventBodyId: sortedBatchByEventBodyId[id].eventBodyId,
},
authCheck,
},
},
{
delay: env.LANGFUSE_INGESTION_QUEUE_DELAY_MS,
},
)
: Promise.reject("Failed to instantiate queue"),
),
Object.keys(sortedBatchByEventBodyId).map(async (id) => {
// We upload the event in an array to the S3 bucket grouped by the eventBodyId.
// That way we batch updates from the same invocation into a single file and reduce
// write operations on S3.
const { data, key, type, eventBodyId } = sortedBatchByEventBodyId[id];
return s3Client.uploadJson(
`${env.LANGFUSE_S3_EVENT_UPLOAD_PREFIX}${authCheck.scope.projectId}/${getClickhouseEntityType(type)}/${eventBodyId}/${key}.json`,
data,
);
}),
);
results.forEach((result) => {
if (result.status === "rejected") {
logger.error("Failed to add event to IngestionQueue", {
s3UploadErrored = true;
logger.error("Failed to upload event to S3", {
error: result.reason,
});
}
});
});
// Send each event individually to IngestionQueue for ClickHouse processing
if (env.LANGFUSE_CLICKHOUSE_INGESTION_ENABLED === "true") {
if (s3UploadErrored) {
throw new Error(
"Failed to upload events to blob storage, aborting event processing",
);
}
if (redis) {
const queue = IngestionQueue.getInstance();
await Promise.all(
Object.keys(sortedBatchByEventBodyId).map(async (id) =>
queue
? queue.add(
QueueJobs.IngestionJob,
{
id: randomUUID(),
timestamp: new Date(),
name: QueueJobs.IngestionJob as const,
payload: {
data: {
type: sortedBatchByEventBodyId[id].type,
eventBodyId: sortedBatchByEventBodyId[id].eventBodyId,
},
authCheck,
},
},
{
delay: env.LANGFUSE_INGESTION_QUEUE_DELAY_MS,
},
)
: Promise.reject("Failed to instantiate queue"),
),
);
if (env.LANGFUSE_POSTGRES_INGESTION_ENABLED !== "true") {
// If postgres ingestion is disabled, we return early
return aggregateBatchResult(
[...validationErrors, ...authenticationErrors],
sortedBatch.map((event) => ({ id: event.id, result: event })),
authCheck.scope.projectId,
);
}
}
}
// As part of the legacy processing we sent the entire batch to the worker.
if (env.LANGFUSE_ASYNC_INGESTION_PROCESSING === "true" && redis) {
const queue = LegacyIngestionQueue.getInstance();
if (env.LANGFUSE_POSTGRES_INGESTION_ENABLED === "true") {
// As part of the legacy processing we sent the entire batch to the worker.
if (redis) {
const queue = LegacyIngestionQueue.getInstance();
if (queue) {
let addToQueueFailed = false;
if (queue) {
let addToQueueFailed = false;
const queuePayload: LegacyIngestionEventType =
env.LANGFUSE_S3_EVENT_UPLOAD_ENABLED === "true" && !s3UploadErrored
const queuePayload: LegacyIngestionEventType = !s3UploadErrored
? {
data: Object.keys(sortedBatchByEventBodyId).map((id) => {
const { key, type, eventBodyId } = sortedBatchByEventBodyId[id];
@@ -255,44 +253,55 @@ export const processEventBatch = async (
}
: { data: sortedBatch, authCheck, useS3EventStore: false };
try {
await queue.add(QueueJobs.LegacyIngestionJob, {
payload: queuePayload,
id: randomUUID(),
timestamp: new Date(),
name: QueueJobs.LegacyIngestionJob as const,
});
} catch (e: unknown) {
logger.warn(
"Failed to add batch to queue, falling back to sync processing",
e,
);
addToQueueFailed = true;
}
try {
await queue.add(QueueJobs.LegacyIngestionJob, {
payload: queuePayload,
id: randomUUID(),
timestamp: new Date(),
name: QueueJobs.LegacyIngestionJob as const,
});
} catch (e: unknown) {
logger.warn(
"Failed to add batch to queue, falling back to sync processing",
e,
);
addToQueueFailed = true;
}
if (!addToQueueFailed) {
return aggregateBatchResult(
// we are not sending additional server errors to the client in case of early return
[...validationErrors, ...authenticationErrors],
sortedBatch.map((event) => ({ id: event.id, result: event })),
if (!addToQueueFailed) {
return aggregateBatchResult(
// we are not sending additional server errors to the client in case of early return
[...validationErrors, ...authenticationErrors],
sortedBatch.map((event) => ({ id: event.id, result: event })),
authCheck.scope.projectId,
);
}
} else {
logger.warn(
"Ingestion queue not initialized, falling back to sync processing",
);
}
} else {
logger.error(
"Ingestion queue not initialized, falling back to sync processing",
);
}
/*******************
* SYNC PROCESSING *
*******************/
const result = await handleBatch(
sortedBatch,
authCheck,
tokenCountDelegate,
);
// in case we did not return early, we return the result here
return aggregateBatchResult(
[...validationErrors, ...authenticationErrors, ...result.errors],
result.results,
authCheck.scope.projectId,
);
}
/*******************
* SYNC PROCESSING *
*******************/
const result = await handleBatch(sortedBatch, authCheck, tokenCountDelegate);
// in case we did not return early, we return the result here
return aggregateBatchResult(
[...validationErrors, ...authenticationErrors, ...result.errors],
result.results,
throw new Error(
"Either Clickhouse or Postgres ingestion (or both) must be enabled",
);
};
@@ -336,6 +345,7 @@ const sortBatch = (batch: Array<z.infer<typeof ingestionEvent>>) => {
export const aggregateBatchResult = (
errors: Array<{ id: string; error: unknown }>,
results: Array<{ id: string; result: unknown }>,
projectId?: string,
) => {
const returnedErrors: {
id: string;
@@ -382,7 +392,10 @@ export const aggregateBatchResult = (
if (returnedErrors.length > 0) {
traceException(errors);
logger.error("Error processing events", returnedErrors);
logger.error("Error processing events", {
errors: returnedErrors,
"langfuse.project.id": projectId,
});
}
results.forEach((result) => {
@@ -59,7 +59,7 @@ export const usage = MixedUsage.nullish()
export const TraceBody = z.object({
id: z.string().nullish(),
timestamp: stringDateTime,
name: z.string().nullish(),
name: z.string().max(1000).nullish(),
externalId: z.string().nullish(),
input: jsonSchema.nullish(),
output: jsonSchema.nullish(),
@@ -17,7 +17,7 @@ type ValidateAndInflateScoreParams = {
export async function validateAndInflateScore(
params: ValidateAndInflateScoreParams,
): Promise<Score> {
const { body, projectId } = params;
const { body, projectId, scoreId } = params;
if (body.configId) {
const config = await prisma.scoreConfig.findFirst({
@@ -32,10 +32,22 @@ export async function validateAndInflateScore(
"The configId you provided does not match a valid config in this project",
);
validateConfigAgainstBody(body, config as ValidatedScoreConfig);
// Override some fields in the score body with config fields
// We ignore the set fields in the body
const bodyWithConfigOverrides = {
...body,
name: config.name,
};
validateConfigAgainstBody(
bodyWithConfigOverrides,
config as ValidatedScoreConfig,
);
return inflateScoreBody({
...params,
projectId,
scoreId,
body: bodyWithConfigOverrides,
config: config as ValidatedScoreConfig,
});
}
@@ -3,6 +3,7 @@ import type { ZodSchema } from "zod";
import { CallbackHandler } from "langfuse-langchain";
import { ChatAnthropic } from "@langchain/anthropic";
import { ChatVertexAI } from "@langchain/google-vertexai";
import { ChatBedrockConverse } from "@langchain/aws";
import {
AIMessage,
@@ -15,7 +16,7 @@ import {
} from "@langchain/core/output_parsers";
import { IterableReadableStream } from "@langchain/core/utils/stream";
import { ChatOpenAI } from "@langchain/openai";
import {
import GCPServiceAccountKeySchema, {
BedrockConfigSchema,
BedrockCredentialSchema,
} from "../../interfaces/customLLMProviderConfigSchemas";
@@ -139,7 +140,11 @@ export async function fetchLLMCompletion(
return new AIMessage(message.content);
});
let chatModel: ChatOpenAI | ChatAnthropic | ChatBedrockConverse;
let chatModel:
| ChatOpenAI
| ChatAnthropic
| ChatBedrockConverse
| ChatVertexAI;
if (modelParams.adapter === LLMAdapter.Anthropic) {
chatModel = new ChatAnthropic({
anthropicApiKey: apiKey,
@@ -191,6 +196,21 @@ export async function fetchLLMCompletion(
callbacks: finalCallbacks,
maxRetries,
});
} else if (modelParams.adapter === LLMAdapter.VertexAI) {
const credentials = GCPServiceAccountKeySchema.parse(JSON.parse(apiKey));
chatModel = new ChatVertexAI({
modelName: modelParams.model,
temperature: modelParams.temperature,
maxOutputTokens: modelParams.max_tokens,
topP: modelParams.top_p,
callbacks: finalCallbacks,
maxRetries,
authOptions: {
projectId: credentials.project_id,
credentials,
},
});
} else {
// eslint-disable-next-line no-unused-vars
const _exhaustiveCheck: never = modelParams.adapter;
+9
View File
@@ -16,6 +16,7 @@ export enum LLMAdapter {
OpenAI = "openai",
Azure = "azure",
Bedrock = "bedrock",
VertexAI = "vertex-ai",
}
export enum ChatMessageRole {
@@ -114,10 +115,18 @@ export const anthropicModels = [
"claude-instant-1.2",
] as const;
export const vertexAIModels = [
"gemini-1.5-pro",
"gemini-1.5-flash",
"gemini-1.0-pro",
] as const;
export type AnthropicModel = (typeof anthropicModels)[number];
export type VertexAIModel = (typeof vertexAIModels)[number];
export const supportedModels = {
[LLMAdapter.Anthropic]: anthropicModels,
[LLMAdapter.OpenAI]: openAIModels,
[LLMAdapter.VertexAI]: vertexAIModels,
[LLMAdapter.Azure]: [],
[LLMAdapter.Bedrock]: [],
} as const;
@@ -161,8 +161,8 @@ export class StringOptionsFilter implements Filter {
return {
query:
this.operator === "any of"
? `has({${varName}: Array(String)}, ${this.tablePrefix ? this.tablePrefix + "." : ""}${this.field}) = True`
: `has({${varName}: Array(String)}, ${this.tablePrefix ? this.tablePrefix + "." : ""}${this.field}) = False`,
? `${this.tablePrefix ? this.tablePrefix + "." : ""}${this.field} IN ({${varName}: Array(String)})`
: `${this.tablePrefix ? this.tablePrefix + "." : ""}${this.field} NOT IN ({${varName}: Array(String)})`,
params: { [varName]: this.values },
};
}
@@ -3,31 +3,53 @@ import { OrderByState } from "../../../interfaces/orderBy";
import { UiColumnMapping } from "../../../tableDefinitions";
import { logger } from "../../logger";
type OrderByStateNotNull = Exclude<OrderByState, null>;
export function orderByToClickhouseSql(
orderBy: OrderByState,
orderBy: OrderByState | OrderByState[] = [],
tableColumns: UiColumnMapping[],
): string {
if (!orderBy) {
if (
!orderBy ||
(Array.isArray(orderBy) && orderBy.filter(Boolean).length === 0)
) {
return "";
}
// Get column definition to map column to internal name, e.g. "t.id"
const col = tableColumns.find(
(c) => c.uiTableName === orderBy.column || c.uiTableId === orderBy.column,
);
if (!col) {
logger.warn("Invalid order by column", orderBy.column);
throw new Error("Invalid order by column: " + orderBy.column);
if (!Array.isArray(orderBy)) {
orderBy = [orderBy];
}
// Initialize an array to hold order by clauses
const orderByClauses: string[] = [];
// Loop through each orderBy entry
for (const ob of orderBy.filter((o): o is OrderByStateNotNull =>
Boolean(o),
)) {
// Get column definition to map column to internal name, e.g. "t.id"
const col = tableColumns.find(
(c) => c.uiTableName === ob.column || c.uiTableId === ob.column,
);
if (!col) {
logger.warn("Invalid order by column", ob.column);
throw new Error("Invalid order by column: " + ob.column);
}
// Assert that ob.order is either "asc" or "desc"
const orderByOrder = z.enum(["ASC", "DESC"]);
const order = orderByOrder.safeParse(ob.order);
if (!order.success) {
logger.warn("Invalid order", ob.order);
throw new Error("Invalid order: " + ob.order);
}
// Append the order by clause to the array
orderByClauses.push(
`${col.queryPrefix ? col.queryPrefix + "." : ""}${col.clickhouseSelect} ${order.data}`,
);
}
// Assert that orderBy.order is either "asc" or "desc"
const orderByOrder = z.enum(["ASC", "DESC"]);
const order = orderByOrder.safeParse(orderBy.order);
if (!order.success) {
logger.warn("Invalid order", orderBy.order);
throw new Error("Invalid order: " + orderBy.order);
}
// Both column and order are safe, can use raw SQL
return `ORDER BY ${col.queryPrefix ? col.queryPrefix + "." : ""}${col.clickhouseSelect} ${order.data}`;
// Join all order by clauses with a comma and return
return `ORDER BY ${orderByClauses.join(", ")}`;
}
@@ -20,3 +20,4 @@ export {
NullFilter,
type ClickhouseOperator,
} from "./clickhouse-sql/clickhouse-filter";
export { orderByToClickhouseSql } from "./clickhouse-sql/orderby-factory";
+12 -4
View File
@@ -63,7 +63,7 @@ export const BatchExportJobSchema = z.object({
projectId: z.string(),
batchExportId: z.string(),
});
export const TraceUpsertEventSchema = z.object({
export const TraceQueueEventSchema = z.object({
projectId: z.string(),
traceId: z.string(),
});
@@ -87,7 +87,7 @@ export const ExperimentCreateEventSchema = z.object({
});
export type BatchExportJobType = z.infer<typeof BatchExportJobSchema>;
export type TraceUpsertEventType = z.infer<typeof TraceUpsertEventSchema>;
export type TraceQueueEventType = z.infer<typeof TraceQueueEventSchema>;
export type DatasetRunItemUpsertEventType = z.infer<
typeof DatasetRunItemUpsertEventSchema
>;
@@ -101,7 +101,7 @@ export type ExperimentCreateEventType = z.infer<
export const EventBodySchema = z.union([
z.object({
name: z.literal(EventName.TraceUpsert),
payload: z.array(TraceUpsertEventSchema),
payload: z.array(TraceQueueEventSchema),
}),
z.object({
name: z.literal(EventName.EvaluationExecution),
@@ -120,6 +120,7 @@ export type EventBodyType = z.infer<typeof EventBodySchema>;
export enum QueueName {
TraceUpsert = "trace-upsert", // Ingestion pipeline adds events on each Trace upsert
TraceDelete = "trace-delete",
EvaluationExecution = "evaluation-execution-queue", // Worker executes Evals
DatasetRunItemUpsert = "dataset-run-item-upsert-queue",
BatchExport = "batch-export-queue",
@@ -131,6 +132,7 @@ export enum QueueName {
export enum QueueJobs {
TraceUpsert = "trace-upsert",
TraceDelete = "trace-delete",
DatasetRunItemUpsert = "dataset-run-item-upsert",
EvaluationExecution = "evaluation-execution-job",
BatchExportJob = "batch-export-job",
@@ -145,9 +147,15 @@ export type TQueueJobTypes = {
[QueueName.TraceUpsert]: {
timestamp: Date;
id: string;
payload: TraceUpsertEventType;
payload: TraceQueueEventType;
name: QueueJobs.TraceUpsert;
};
[QueueName.TraceDelete]: {
timestamp: Date;
id: string;
payload: TraceQueueEventType;
name: QueueJobs.TraceDelete;
};
[QueueName.DatasetRunItemUpsert]: {
timestamp: Date;
id: string;
@@ -1,5 +1,5 @@
import { Queue } from "bullmq";
import { env } from "../..";
import { env } from "../../env";
import { logger } from "@azure/storage-blob";
import { QueueName, QueueJobs } from "../queues";
import { createNewRedisInstance, redisQueueRetryOptions } from "./redis";
+4 -1
View File
@@ -1,13 +1,14 @@
import { Queue } from "bullmq";
import { QueueName } from "../queues";
import { BatchExportQueue } from "./batchExport";
import { CloudUsageMeteringQueue } from "./CloudUsageMeteringQueue";
import { CloudUsageMeteringQueue } from "./cloudUsageMeteringQueue";
import { DatasetRunItemUpsertQueue } from "./datasetRunItemUpsert";
import { EvalExecutionQueue } from "./evalExecutionQueue";
import { ExperimentCreateQueue } from "./experimentCreateQueue";
import { IngestionQueue } from "./ingestionQueue";
import { LegacyIngestionQueue } from "./legacyIngestion";
import { TraceUpsertQueue } from "./traceUpsert";
import { TraceDeleteQueue } from "./traceDelete";
export function getQueue(queueName: QueueName): Queue | null {
switch (queueName) {
@@ -25,6 +26,8 @@ export function getQueue(queueName: QueueName): Queue | null {
return ExperimentCreateQueue.getInstance();
case QueueName.TraceUpsert:
return TraceUpsertQueue.getInstance();
case QueueName.TraceDelete:
return TraceDeleteQueue.getInstance();
case QueueName.IngestionQueue:
return IngestionQueue.getInstance();
default:
@@ -0,0 +1,44 @@
import { QueueName, TQueueJobTypes } from "../queues";
import { Queue } from "bullmq";
import { createNewRedisInstance, redisQueueRetryOptions } from "./redis";
import { logger } from "../logger";
export class TraceDeleteQueue {
private static instance: Queue<TQueueJobTypes[QueueName.TraceDelete]> | null =
null;
public static getInstance(): Queue<
TQueueJobTypes[QueueName.TraceDelete]
> | null {
if (TraceDeleteQueue.instance) return TraceDeleteQueue.instance;
const newRedis = createNewRedisInstance({
enableOfflineQueue: false,
...redisQueueRetryOptions,
});
TraceDeleteQueue.instance = newRedis
? new Queue<TQueueJobTypes[QueueName.TraceDelete]>(
QueueName.TraceDelete,
{
connection: newRedis,
defaultJobOptions: {
removeOnComplete: true,
removeOnFail: 100_000,
attempts: 5,
backoff: {
type: "exponential",
delay: 5000,
},
},
},
)
: null;
TraceDeleteQueue.instance?.on("error", (err) => {
logger.error("TraceDeleteQueue error", err);
});
return TraceDeleteQueue.instance;
}
}
@@ -1,9 +1,6 @@
import { randomUUID } from "crypto";
import {
QueueJobs,
QueueName,
TQueueJobTypes,
TraceUpsertEventType,
} from "../queues";
import { Queue } from "bullmq";
import { createNewRedisInstance, redisQueueRetryOptions } from "./redis";
@@ -40,37 +40,31 @@ export async function upsertClickhouse<
// https://opentelemetry.io/docs/specs/semconv/database/database-spans/
span.setAttribute("ch.query.table", opts.table);
// If event upload is enabled, we store all rows in S3 to have a backup
if (env.LANGFUSE_S3_EVENT_UPLOAD_ENABLED === "true") {
if (env.LANGFUSE_S3_EVENT_UPLOAD_BUCKET === undefined) {
throw new Error("S3 event store is enabled but no bucket is set");
}
const s3Client = getS3StorageServiceClient(
env.LANGFUSE_S3_EVENT_UPLOAD_BUCKET,
);
await Promise.all(
opts.records.map((record) => {
// drop trailing s and pretend it's always a create.
// Only applicable to scores and traces.
let eventType = `${opts.table.slice(0, -1)}-create`;
if (opts.table === "observations") {
// @ts-ignore - If it's an observation we now that `type` is a string
eventType = `${record["type"].toLowerCase()}-create`;
}
s3Client.uploadJson(
`${env.LANGFUSE_S3_EVENT_UPLOAD_PREFIX}${record.project_id}/${getClickhouseEntityType(eventType)}/${record.id}/${randomUUID()}.json`,
[
{
id: randomUUID(),
timestamp: new Date().toISOString(),
type: eventType,
body: opts.eventBodyMapper(record),
},
],
);
}),
);
}
const s3Client = getS3StorageServiceClient(
env.LANGFUSE_S3_EVENT_UPLOAD_BUCKET,
);
await Promise.all(
opts.records.map((record) => {
// drop trailing s and pretend it's always a create.
// Only applicable to scores and traces.
let eventType = `${opts.table.slice(0, -1)}-create`;
if (opts.table === "observations") {
// @ts-ignore - If it's an observation we now that `type` is a string
eventType = `${record["type"].toLowerCase()}-create`;
}
s3Client.uploadJson(
`${env.LANGFUSE_S3_EVENT_UPLOAD_PREFIX}${record.project_id}/${getClickhouseEntityType(eventType)}/${record.id}/${randomUUID()}.json`,
[
{
id: randomUUID(),
timestamp: new Date().toISOString(),
type: eventType,
body: opts.eventBodyMapper(record),
},
],
);
}),
);
const res = await clickhouseClient().insert({
table: opts.table,
@@ -7,3 +7,4 @@ export * from "./traces_converters";
export * from "./scores_converters";
export * from "./observations_converters";
export * from "./clickhouse";
export * from "./constants";
@@ -389,7 +389,7 @@ export type ObservationsTableQueryResult = ObservationRecordReadType & {
export const getObservationsTableCount = async (opts: ObservationTableQuery) =>
getObservationsTableInternal<TableCount>({
...opts,
select: "count(*) as count",
select: "count",
});
export type ObservationsTableRow = Omit<
@@ -407,29 +407,7 @@ export const getObservationsTable = async (
>
>({
...opts,
select: `
o.id as id,
o.name as name,
o."model_parameters" as model_parameters,
o.start_time as "start_time",
o.end_time as "end_time",
o.trace_id as "trace_id",
o.completion_start_time as "completion_start_time",
o.provided_usage_details as "provided_usage_details",
o.usage_details as "usage_details",
o.provided_cost_details as "provided_cost_details",
o.cost_details as "cost_details",
o.level as level,
o.status_message as "status_message",
o.version as version,
o.parent_observation_id as "parent_observation_id",
o.created_at as "created_at",
o.updated_at as "updated_at",
o.provided_model_name as "provided_model_name",
o.total_cost as "total_cost",
internal_model_id as "internal_model_id",
if(isNull(end_time), NULL, date_diff('milliseconds', start_time, end_time)) as latency,
if(isNull(completion_start_time), NULL, date_diff('milliseconds', start_time, completion_start_time)) as "time_to_first_token"`,
select: "rows",
});
const traces = await getTracesByIds(
@@ -464,29 +442,7 @@ export const getObservationsTableWithModelData = async (
>
>({
...opts,
select: `
o.id as id,
o.name as name,
o."model_parameters" as model_parameters,
o.start_time as "start_time",
o.end_time as "end_time",
o.trace_id as "trace_id",
o.completion_start_time as "completion_start_time",
o.provided_usage_details as "provided_usage_details",
o.usage_details as "usage_details",
o.provided_cost_details as "provided_cost_details",
o.cost_details as "cost_details",
o.level as level,
o.status_message as "status_message",
o.version as version,
o.parent_observation_id as "parent_observation_id",
o.created_at as "created_at",
o.updated_at as "updated_at",
o.provided_model_name as "provided_model_name",
o.total_cost as "total_cost",
internal_model_id as "internal_model_id",
if(isNull(end_time), NULL, date_diff('milliseconds', start_time, end_time)) as latency,
if(isNull(completion_start_time), NULL, date_diff('milliseconds', start_time, completion_start_time)) as "time_to_first_token"`,
select: "rows",
});
const uniqueModels: string[] = Array.from(
@@ -543,17 +499,44 @@ export const getObservationsTableWithModelData = async (
};
const getObservationsTableInternal = async <T>(
opts: ObservationTableQuery & { select: string },
opts: ObservationTableQuery & { select: "count" | "rows" },
): Promise<Array<T>> => {
const select =
opts.select === "count"
? "count(*) as count"
: `
o.id as id,
o.name as name,
o."model_parameters" as model_parameters,
o.start_time as "start_time",
o.end_time as "end_time",
o.trace_id as "trace_id",
o.completion_start_time as "completion_start_time",
o.provided_usage_details as "provided_usage_details",
o.usage_details as "usage_details",
o.provided_cost_details as "provided_cost_details",
o.cost_details as "cost_details",
o.level as level,
o.status_message as "status_message",
o.version as version,
o.parent_observation_id as "parent_observation_id",
o.created_at as "created_at",
o.updated_at as "updated_at",
o.provided_model_name as "provided_model_name",
o.total_cost as "total_cost",
internal_model_id as "internal_model_id",
if(isNull(end_time), NULL, date_diff('milliseconds', start_time, end_time)) as latency,
if(isNull(completion_start_time), NULL, date_diff('milliseconds', start_time, completion_start_time)) as "time_to_first_token"`;
const { projectId, filter, selectIOAndMetadata, limit, offset, orderBy } =
opts;
const selectString = selectIOAndMetadata
? `
${opts.select},
${select},
${selectIOAndMetadata ? `o.input, o.output, o.metadata` : ""}
`
: opts.select;
: select;
const scoresFilter = new FilterList([
new StringFilter({
@@ -580,6 +563,10 @@ const getObservationsTableInternal = async <T>(
.includes(f.column),
);
const hasScoresFilter = filter.some(
(f) => f.column === "Scores" || f.column === "scores",
);
const orderByTraces = opts.orderBy
? observationsTableTraceUiColumnDefinitions
.map((c) => c.uiTableId)
@@ -650,63 +637,58 @@ const getObservationsTableInternal = async <T>(
observation_id
)`;
if (traceTableFilter.length > 0 || orderByTraces) {
// joins with traces are very expensive. We need to filter by time as well.
// We assume that a trace has to have been within the last 2 days to be relevant.
// if we have default ordering by time, we order by toDate(o.start_time) first and then by
// o.start_time. This way, clickhouse is able to read more efficiently directly from disk without ordering
const newDefaultOrder =
orderBy?.column === "startTime"
? [{ column: "order_by_date", order: orderBy.order }, orderBy]
: [orderBy ?? null];
const query = `
const chOrderBy = orderByToClickhouseSql(newDefaultOrder, [
...observationsTableUiColumnDefinitions,
{
uiTableName: "order_by_date",
uiTableId: "order_by_date",
clickhouseTableName: "observation",
clickhouseSelect: "toDate(o.start_time)",
},
]);
// joins with traces are very expensive. We need to filter by time as well.
// We assume that a trace has to have been within the last 2 days to be relevant.
const query = `
${scoresCte}
SELECT
${selectString}
FROM observations o FINAL
LEFT JOIN traces t FINAL ON t.id = o.trace_id AND t.project_id = o.project_id
LEFT JOIN scores_avg AS s_avg ON s_avg.trace_id = o.trace_id and s_avg.observation_id = o.id
FROM observations o
${traceTableFilter.length > 0 || orderByTraces || search.query ? "LEFT JOIN traces t FINAL ON t.id = o.trace_id AND t.project_id = o.project_id" : ""}
${hasScoresFilter ? `LEFT JOIN scores_avg AS s_avg ON s_avg.trace_id = o.trace_id and s_avg.observation_id = o.id` : ""}
WHERE ${appliedObservationsFilter.query}
AND o.type = 'GENERATION'
${timeFilter ? `AND t.timestamp > {tracesTimestampFilter: DateTime64(3)} - ${OBSERVATIONS_TO_TRACE_INTERVAL}` : ""}
${timeFilter && (traceTableFilter.length > 0 || orderByTraces) ? `AND t.timestamp > {tracesTimestampFilter: DateTime64(3)} - ${OBSERVATIONS_TO_TRACE_INTERVAL}` : ""}
${search.query}
${orderByToClickhouseSql(orderBy ?? null, observationsTableUiColumnDefinitions)}
${chOrderBy}
${opts.select === "rows" ? "LIMIT 1 BY o.id, o.project_id" : ""}
${limit !== undefined && offset !== undefined ? `LIMIT ${limit} OFFSET ${offset}` : ""};`;
const res = await queryClickhouse<T>({
query,
params: {
...appliedScoresFilter.params,
...appliedObservationsFilter.params,
...(timeFilter
? {
tracesTimestampFilter: convertDateToClickhouseDateTime(
timeFilter.value as Date,
),
}
: {}),
...search.params,
},
});
const res = await queryClickhouse<T>({
query,
params: {
...appliedScoresFilter.params,
...appliedObservationsFilter.params,
...(timeFilter
? {
tracesTimestampFilter: convertDateToClickhouseDateTime(
timeFilter.value as Date,
),
}
: {}),
...search.params,
},
});
return res;
} else {
// we query by T, which could also be {count: string}.
const query = `
${scoresCte}
SELECT
${selectString}
FROM observations o FINAL
LEFT JOIN scores_avg AS s_avg ON s_avg.trace_id = o.trace_id and s_avg.observation_id = o.id
WHERE ${appliedObservationsFilter.query}
${orderByToClickhouseSql(orderBy ?? null, observationsTableUiColumnDefinitions)}
${limit !== undefined && offset !== undefined ? `LIMIT ${limit} OFFSET ${offset}` : ""};`;
const res = await queryClickhouse<T>({
query,
params: {
...appliedScoresFilter.params,
...appliedObservationsFilter.params,
},
});
return res;
}
return res;
};
export const getObservationsGroupedByModel = async (
@@ -1069,3 +1051,60 @@ export const getLatencyAndTotalCostForObservationsByTraces = async (
latency: Number(r.latency_ms) / 1000,
}));
};
export const getObservationCountsByProjectInCreationInterval = async ({
start,
end,
}: {
start: Date;
end: Date;
}) => {
const query = `
SELECT
project_id,
count(*) as count
FROM observations
WHERE created_at >= {start: DateTime64(3)}
AND created_at < {end: DateTime64(3)}
GROUP BY project_id
`;
const rows = await queryClickhouse<{ project_id: string; count: string }>({
query,
params: {
start: convertDateToClickhouseDateTime(start),
end: convertDateToClickhouseDateTime(end),
},
});
return rows.map((row) => ({
projectId: row.project_id,
count: Number(row.count),
}));
};
export const getObservationCountOfProjectsSinceCreationDate = async ({
projectIds,
start,
}: {
projectIds: string[];
start: Date;
}) => {
const query = `
SELECT
count(*) as count
FROM observations
WHERE project_id IN ({projectIds: Array(String)})
AND created_at >= {start: DateTime64(3)}
`;
const rows = await queryClickhouse<{ count: string }>({
query,
params: {
projectIds,
start: convertDateToClickhouseDateTime(start),
},
});
return Number(rows[0]?.count ?? 0);
};
@@ -549,3 +549,34 @@ export const getAggregatedScoresForPrompts = async (
promptId: row.prompt_id,
}));
};
export const getScoreCountsByProjectInCreationInterval = async ({
start,
end,
}: {
start: Date;
end: Date;
}) => {
const query = `
SELECT
project_id,
count(*) as count
FROM scores
WHERE created_at >= {start: DateTime64(3)}
AND created_at < {end: DateTime64(3)}
GROUP BY project_id
`;
const rows = await queryClickhouse<{ project_id: string; count: string }>({
query,
params: {
start: convertDateToClickhouseDateTime(start),
end: convertDateToClickhouseDateTime(end),
},
});
return rows.map((row) => ({
projectId: row.project_id,
count: Number(row.count),
}));
};
@@ -151,6 +151,37 @@ export const hasAnyTrace = async (projectId: string) => {
return rows.length > 0 && Number(rows[0].count) > 0;
};
export const getTraceCountsByProjectInCreationInterval = async ({
start,
end,
}: {
start: Date;
end: Date;
}) => {
const query = `
SELECT
project_id,
count(*) as count
FROM traces
WHERE created_at >= {start: DateTime64(3)}
AND created_at < {end: DateTime64(3)}
GROUP BY project_id
`;
const rows = await queryClickhouse<{ project_id: string; count: string }>({
query,
params: {
start: convertDateToClickhouseDateTime(start),
end: convertDateToClickhouseDateTime(end),
},
});
return rows.map((row) => ({
projectId: row.project_id,
count: Number(row.count),
}));
};
export const getTraceById = async (
traceId: string,
projectId: string,
@@ -395,7 +426,17 @@ export const getSessionsTable = async (props: {
return rows;
};
const getSessionsTableGeneric = async <T>(props: FetchTracesTableProps) => {
export type FetchSessionsTableProps = {
select: string;
projectId: string;
filter: FilterState;
searchQuery?: string;
orderBy?: OrderByState;
limit?: number;
page?: number;
};
const getSessionsTableGeneric = async <T>(props: FetchSessionsTableProps) => {
const { select, projectId, filter, orderBy, limit, page } = props;
const { tracesFilter, scoresFilter, observationsFilter } =
@@ -1,10 +1,7 @@
import { ObservationLevel, Trace } from "@prisma/client";
import { Trace } from "@prisma/client";
import { parseClickhouseUTCDateTimeFormat } from "./clickhouse";
import { TraceRecordReadType } from "./definitions";
import Decimal from "decimal.js";
import { ScoreAggregate } from "../../features/scores";
import { convertDateToClickhouseDateTime } from "../clickhouse/client";
import { TracesTableReturnType } from "../services/traces-ui-table-service";
export const convertTraceDomainToClickhouse = (
trace: Trace,
@@ -54,53 +51,3 @@ export const convertClickhouseToDomain = (
externalId: null,
};
};
export type TracesAllReturnType = {
id: string;
timestamp: Date;
name: string | null;
projectId: string;
userId: string | null;
release: string | null;
version: string | null;
public: boolean;
bookmarked: boolean;
sessionId: string | null;
tags: string[];
};
export const convertToDomain = (row: TracesTableReturnType) => {
return {
id: row.id,
projectId: row.project_id,
timestamp: parseClickhouseUTCDateTimeFormat(row.timestamp),
tags: row.tags,
bookmarked: row.bookmarked,
name: row.name ?? null,
release: row.release ?? null,
version: row.version ?? null,
userId: row.user_id ?? null,
sessionId: row.session_id ?? null,
latency: Number(row.latency),
usageDetails: row.usage_details,
costDetails: row.cost_details,
level: row.level,
observationCount: Number(row.observation_count),
scoresAvg: row.scores_avg,
public: row.public,
};
};
export type TracesMetricsReturnType = {
id: string;
promptTokens: bigint;
completionTokens: bigint;
totalTokens: bigint;
latency: number | null;
level: ObservationLevel;
observationCount: bigint;
calculatedTotalCost: Decimal | null;
calculatedInputCost: Decimal | null;
calculatedOutputCost: Decimal | null;
scores: ScoreAggregate;
};
@@ -13,9 +13,17 @@ import {
} from "../queries/clickhouse-sql/factory";
import { orderByToClickhouseSql } from "../queries/clickhouse-sql/orderby-factory";
import { clickhouseSearchCondition } from "../queries/clickhouse-sql/search";
import { convertToDomain } from "../repositories";
import { queryClickhouse } from "../repositories/clickhouse";
import {
parseClickhouseUTCDateTimeFormat,
queryClickhouse,
} from "../repositories/clickhouse";
import { TraceRecordReadType } from "../repositories/definitions";
import {
OBSERVATIONS_TO_TRACE_INTERVAL,
SCORE_TO_TRACE_OBSERVATIONS_INTERVAL,
} from "../repositories/constants";
import Decimal from "decimal.js";
import { ScoreAggregate } from "../../features/scores";
export type TracesTableReturnType = Pick<
TraceRecordReadType,
@@ -30,7 +38,83 @@ export type TracesTableReturnType = Pick<
| "session_id"
| "tags"
| "public"
> & {
>;
export type TracesAllUiReturnType = {
id: string;
timestamp: Date;
name: string | null;
projectId: string;
userId: string | null;
release: string | null;
version: string | null;
public: boolean;
bookmarked: boolean;
sessionId: string | null;
tags: string[];
};
export type TracesMetricsUiReturnType = {
id: string;
projectId: string;
promptTokens: bigint;
completionTokens: bigint;
totalTokens: bigint;
latency: number | null;
level: ObservationLevel;
observationCount: bigint;
calculatedTotalCost: Decimal | null;
calculatedInputCost: Decimal | null;
calculatedOutputCost: Decimal | null;
scores: ScoreAggregate;
};
export const convertToUiTableRows = (
row: TracesTableReturnType,
): TracesAllUiReturnType => {
return {
id: row.id,
projectId: row.project_id,
timestamp: parseClickhouseUTCDateTimeFormat(row.timestamp),
tags: row.tags,
bookmarked: row.bookmarked,
name: row.name ?? null,
release: row.release ?? null,
version: row.version ?? null,
userId: row.user_id ?? null,
sessionId: row.session_id ?? null,
public: row.public,
};
};
export const convertToUITableMetrics = (
row: TracesTableMetricsClickhouseReturnType,
): Omit<TracesMetricsUiReturnType, "scores"> => {
return {
id: row.id,
projectId: row.project_id,
latency: Number(row.latency),
promptTokens: BigInt(row.usage_details?.input ?? 0),
completionTokens: BigInt(row.usage_details?.output ?? 0),
totalTokens: BigInt(row.usage_details?.total ?? 0),
observationCount: BigInt(row.observation_count ?? 0),
calculatedTotalCost: row.cost_details?.total
? new Decimal(row.cost_details.total)
: null,
calculatedInputCost: row.cost_details?.input
? new Decimal(row.cost_details.input)
: null,
calculatedOutputCost: row.cost_details?.output
? new Decimal(row.cost_details.output)
: null,
level: row.level,
};
};
export type TracesTableMetricsClickhouseReturnType = {
id: string;
project_id: string;
timestamp: Date;
level: ObservationLevel;
observation_count: number | null;
latency: string | null;
@@ -40,7 +124,7 @@ export type TracesTableReturnType = Pick<
};
export type FetchTracesTableProps = {
select: string;
select: "count" | "rows" | "metrics";
projectId: string;
filter: FilterState;
searchQuery?: string;
@@ -58,7 +142,7 @@ export const getTracesTableCount = async (props: {
page?: number;
}) => {
const countRows = await getTracesTableGeneric<{ count: string }>({
select: "count(*) as count",
select: "count",
...props,
});
@@ -69,6 +153,23 @@ export const getTracesTableCount = async (props: {
return converted.length > 0 ? converted[0].count : 0;
};
export const getTracesTableMetrics = async (props: {
projectId: string;
filter: FilterState;
searchQuery?: string;
orderBy?: OrderByState;
limit?: number;
page?: number;
}): Promise<Array<Omit<TracesMetricsUiReturnType, "scores">>> => {
const countRows =
await getTracesTableGeneric<TracesTableMetricsClickhouseReturnType>({
select: "metrics",
...props,
});
return countRows.map(convertToUITableMetrics);
};
export const getTracesTable = async (
projectId: string,
filter: FilterState,
@@ -78,24 +179,7 @@ export const getTracesTable = async (
page?: number,
) => {
const rows = await getTracesTableGeneric<TracesTableReturnType>({
select: `
t.id,
t.project_id as project_id,
t.timestamp,
t.tags,
t.bookmarked,
t.name,
t.release,
t.version,
t.user_id,
t.session_id,
os.latency_milliseconds / 1000 as latency,
os.cost_details as cost_details,
os.usage_details as usage_details,
os.level as level,
os.observation_count as observation_count,
s.scores_avg as scores_avg,
t.public`,
select: "rows",
projectId,
filter,
searchQuery,
@@ -104,13 +188,50 @@ export const getTracesTable = async (
page,
});
return rows.map(convertToDomain);
return rows.map(convertToUiTableRows);
};
const getTracesTableGeneric = async <T>(props: FetchTracesTableProps) => {
const { select, projectId, filter, orderBy, limit, page, searchQuery } =
props;
let sqlSelect: string;
switch (select) {
case "count":
sqlSelect = "count(*) as count";
break;
case "metrics":
sqlSelect = `
t.id as id,
t.project_id as project_id,
t.timestamp as timestamp,
os.latency_milliseconds / 1000 as latency,
os.cost_details as cost_details,
os.usage_details as usage_details,
os.level as level,
os.observation_count as observation_count,
s.scores_avg as scores_avg,
t.public as public`;
break;
case "rows":
sqlSelect = `
t.id as id,
t.project_id as project_id,
t.timestamp as timestamp,
t.tags as tags,
t.bookmarked as bookmarked,
t.name as name,
t.release as release,
t.version as version,
t.user_id as user_id,
t.session_id as session_id,
t.public as public`;
break;
default:
const exhaustiveCheckDefault: never = select;
throw new Error(`Unknown select type: ${select}`);
}
const { tracesFilter, scoresFilter, observationsFilter } =
getProjectIdDefaultFilter(projectId, { tracesPrefix: "t" });
@@ -156,27 +277,20 @@ const getTracesTableGeneric = async <T>(props: FetchTracesTableProps) => {
f.field === "timestamp" && (f.operator === ">=" || f.operator === ">"),
) as DateTimeFilter | undefined;
timeStampFilter
? scoresFilter.push(
new DateTimeFilter({
clickhouseTable: "scores",
field: "timestamp",
operator: ">=",
value: timeStampFilter.value,
}),
)
: null;
const requiresScoresJoin =
tracesFilter.find((f) => f.clickhouseTable === "scores") !== undefined ||
tracesTableUiColumnDefinitions.find(
(c) =>
c.uiTableName === orderBy?.column || c.uiTableId === orderBy?.column,
)?.clickhouseTableName === "scores";
timeStampFilter
? observationsFilter.push(
new DateTimeFilter({
clickhouseTable: "observations",
field: "start_time",
operator: ">=",
value: timeStampFilter.value,
}),
)
: null;
const requiresObservationsJoin =
tracesFilter.find((f) => f.clickhouseTable === "observations") !==
undefined ||
tracesTableUiColumnDefinitions.find(
(c) =>
c.uiTableName === orderBy?.column || c.uiTableId === orderBy?.column,
)?.clickhouseTableName === "observations";
const tracesFilterRes = tracesFilter.apply();
const scoresFilterRes = scoresFilter.apply();
@@ -184,6 +298,36 @@ const getTracesTableGeneric = async <T>(props: FetchTracesTableProps) => {
const search = clickhouseSearchCondition(searchQuery);
const defaultOrder = orderBy?.order && orderBy?.column === "timestamp";
const orderByCols = [
...tracesTableUiColumnDefinitions,
{
clickhouseSelect: "toDate(t.timestamp)",
uiTableName: "timestamp_to_date",
uiTableId: "timestamp_to_date",
clickhouseTableName: "observations",
},
];
const chOrderBy = orderByToClickhouseSql(
[
defaultOrder
? {
column: "timestamp_to_date",
order: orderBy.order,
}
: null,
orderBy ?? null,
],
orderByCols,
);
// complex query ahead:
// - we only join scores and observations if we really need them to speed up default views
// - we use FINAL on traces only in case we not need to order by something different than time. Otherwise we cannot guarantee correct reads.
// - we filter the observations and scores as much as possible before joining them to traces.
// - we order by todate(timestamp), timestamp per default and do not use FINAL.
// In this case, CH is able to read the data only from the latest date from disk and filtering them in memory. No need to read all data e.g. for 1 month from disk.
const query = `
WITH observations_stats AS (
SELECT
@@ -200,8 +344,10 @@ const getTracesTableGeneric = async <T>(props: FetchTracesTableProps) => {
sumMap(cost_details) as cost_details,
trace_id,
project_id
FROM observations FINAL
WHERE ${observationFilterRes.query}
FROM observations o FINAL
WHERE o.project_id = {projectId: String}
${timeStampFilter ? `AND o.start_time >= {traceTimestamp: DateTime64(3)} - ${OBSERVATIONS_TO_TRACE_INTERVAL}` : ""}
${observationsFilter ? `AND ${observationFilterRes.query}` : ""}
GROUP BY trace_id, project_id
),
scores_avg AS (
@@ -214,21 +360,28 @@ const getTracesTableGeneric = async <T>(props: FetchTracesTableProps) => {
trace_id,
name,
avg(value) avg_value
FROM scores final
WHERE ${scoresFilterRes.query}
FROM scores s FINAL
WHERE project_id = {projectId: String}
${timeStampFilter ? `AND s.timestamp >= {traceTimestamp: DateTime64(3)} - ${SCORE_TO_TRACE_OBSERVATIONS_INTERVAL}` : ""}
${scoresFilterRes ? `AND ${scoresFilterRes.query}` : ""}
GROUP BY project_id,
trace_id,
name
) tmp
GROUP BY project_id, trace_id
)
SELECT ${select}
FROM traces t final
LEFT JOIN observations_stats os on os.project_id = t.project_id and os.trace_id = t.id
LEFT JOIN scores_avg s on s.project_id = t.project_id and s.trace_id = t.id
WHERE ${tracesFilterRes.query}
SELECT ${sqlSelect}
-- FINAL is used for non default ordering and count.
FROM traces t ${["metrics", "rows"].includes(select) && defaultOrder ? "" : "FINAL"}
${select === "metrics" || requiresObservationsJoin ? `LEFT JOIN observations_stats os on os.project_id = t.project_id and os.trace_id = t.id` : ""}
${select === "metrics" || requiresScoresJoin ? `LEFT JOIN scores_avg s on s.project_id = t.project_id and s.trace_id = t.id` : ""}
WHERE t.project_id = {projectId: String}
${tracesFilterRes ? `AND ${tracesFilterRes.query}` : ""}
${search.query}
${orderByToClickhouseSql(orderBy ?? null, tracesTableUiColumnDefinitions)}
${chOrderBy}
-- This is used for metrics and row queries. Count has only one result.
-- This is only used for default ordering. Otherwise, we use final.
${["metrics", "rows"].includes(select) && defaultOrder ? "LIMIT 1 BY id, project_id" : ""}
${limit !== undefined && page !== undefined ? `LIMIT {limit: Int32} OFFSET {offset: Int32}` : ""}
`;
@@ -237,6 +390,8 @@ const getTracesTableGeneric = async <T>(props: FetchTracesTableProps) => {
params: {
limit: limit,
offset: limit && page ? limit * page : 0,
traceTimestamp: timeStampFilter?.value.getTime(),
projectId: projectId,
...tracesFilterRes.params,
...observationFilterRes.params,
...scoresFilterRes.params,
@@ -68,7 +68,7 @@ export const observationsTableUiColumnDefinitions: UiColumnMapping[] = [
uiTableId: "timeToFirstToken",
clickhouseTableName: "observations",
clickhouseSelect:
"if(isNull(completion_start_time), NULL, date_diff('seconds', start_time, completion_start_time))",
"if(isNull(completion_start_time), NULL, date_diff('milliseconds', start_time, completion_start_time))",
// If we use the default of Decimal64(12), we cannot filter for more than ~40min due to an overflow
clickhouseTypeOverwrite: "Decimal64(3)",
},
@@ -77,7 +77,7 @@ export const observationsTableUiColumnDefinitions: UiColumnMapping[] = [
uiTableId: "latency",
clickhouseTableName: "observations",
clickhouseSelect:
"if(isNull(end_time), NULL, date_diff('seconds', start_time, end_time))",
"if(isNull(end_time), NULL, date_diff('milliseconds', start_time, end_time))",
// If we use the default of Decimal64(12), we cannot filter for more than ~40min due to an overflow
clickhouseTypeOverwrite: "Decimal64(3)",
},
@@ -86,7 +86,7 @@ export const observationsTableUiColumnDefinitions: UiColumnMapping[] = [
uiTableId: "tokensPerSecond",
clickhouseTableName: "observations",
clickhouseSelect:
"usage_details['input'] / date_diff('seconds', start_time, end_time)",
"(usage_details['input'] / date_diff('milliseconds', start_time, end_time))",
},
{
uiTableName: "Input Cost ($)",
@@ -71,28 +71,28 @@ export const tracesTableUiColumnDefinitions: UiColumnMapping[] = [
{
uiTableName: "Input Tokens",
uiTableId: "inputTokens",
clickhouseTableName: "traces",
clickhouseTableName: "observations",
clickhouseSelect:
"if(mapExists((k, v) -> (k = 'input'), usage_details), usage_details['input'], NULL)",
},
{
uiTableName: "Output Tokens",
uiTableId: "outputTokens",
clickhouseTableName: "traces",
clickhouseTableName: "observations",
clickhouseSelect:
"if(mapExists((k, v) -> (k = 'output'), usage_details), usage_details['output'], NULL)",
},
{
uiTableName: "Total Tokens",
uiTableId: "totalTokens",
clickhouseTableName: "traces",
clickhouseTableName: "observations",
clickhouseSelect:
"if(mapExists((k, v) -> (k = 'total'), usage_details), usage_details['total'], NULL)",
},
{
uiTableName: "Usage",
uiTableId: "usage",
clickhouseTableName: "traces",
clickhouseTableName: "observations",
clickhouseSelect:
"if(mapExists((k, v) -> (k = 'total'), usage_details), usage_details['total'], NULL)",
},
@@ -105,7 +105,7 @@ export const tracesTableUiColumnDefinitions: UiColumnMapping[] = [
{
uiTableName: "Latency (s)",
uiTableId: "latency",
clickhouseTableName: "traces",
clickhouseTableName: "observations",
clickhouseSelect: "latency_milliseconds / 1000",
// If we use the default of Decimal64(12), we cannot filter for more than ~40min due to an overflow
clickhouseTypeOverwrite: "Decimal64(3)",
@@ -113,19 +113,19 @@ export const tracesTableUiColumnDefinitions: UiColumnMapping[] = [
{
uiTableName: "Input Cost ($)",
uiTableId: "inputCost",
clickhouseTableName: "traces",
clickhouseTableName: "observations",
clickhouseSelect: "cost_details['input']",
},
{
uiTableName: "Output Cost ($)",
uiTableId: "outputCost",
clickhouseTableName: "traces",
clickhouseTableName: "observations",
clickhouseSelect: "cost_details['output']",
},
{
uiTableName: "Total Cost ($)",
uiTableId: "totalCost",
clickhouseTableName: "traces",
clickhouseTableName: "observations",
clickhouseSelect: "cost_details['total']",
},
];
+10 -7
View File
@@ -26,7 +26,7 @@ export const jsonSchemaNullable: z.ZodType<JsonNested> = z.lazy(() =>
nestedLiteralSchema,
z.array(jsonSchemaNullable),
z.record(jsonSchemaNullable),
])
]),
);
// Root schema that does not allow nulls at the root level
@@ -35,17 +35,17 @@ export const jsonSchema: z.ZodType<Json> = z.lazy(() =>
rootLiteralSchema,
z.array(jsonSchemaNullable),
z.record(jsonSchemaNullable),
])
]),
);
export const paginationZod = {
page: z.preprocess(
(x) => (x === "" ? undefined : x),
z.coerce.number().default(1)
z.coerce.number().default(1),
),
limit: z.preprocess(
(x) => (x === "" ? undefined : x),
z.coerce.number().lte(100).default(50)
z.coerce.number().lte(100).default(50),
),
};
@@ -69,8 +69,11 @@ export const paginationMetaResponseZod = z.object({
totalPages: z.number().int().nonnegative(),
});
export const noHtmlRegex = /<[^>]*>/;
export const noHtmlCheck = (value: string) => !noHtmlRegex.test(value);
export const htmlRegex = /<[^>]*>/;
export const noHtmlCheck = (value: string) => !htmlRegex.test(value);
const urlRegex = /https?:\/\/[^\s/$.?#].[^\s]*/i;
export const noUrlCheck = (value: string) => !urlRegex.test(value);
export const NonEmptyString = z.string().min(1);
@@ -83,7 +86,7 @@ export const NonEmptyString = z.string().min(1);
*/
export const validateZodSchema = <T extends z.ZodTypeAny>(
schema: T,
object: z.infer<T>
object: z.infer<T>,
): z.infer<T> => {
return schema.parse(object);
};
+227 -59
View File
@@ -152,6 +152,9 @@ importers:
'@langchain/core':
specifier: ^0.3.18
version: 0.3.18(openai@4.72.0(zod@3.23.8))
'@langchain/google-vertexai':
specifier: ^0.1.3
version: 0.1.3(@langchain/core@0.3.18(openai@4.72.0(zod@3.23.8)))(zod@3.23.8)
'@langchain/openai':
specifier: ^0.3.14
version: 0.3.14(@langchain/core@0.3.18(openai@4.72.0(zod@3.23.8)))
@@ -199,10 +202,10 @@ importers:
version: 0.27.4
langchain:
specifier: ^0.3.6
version: 0.3.6(@langchain/anthropic@0.3.8(@langchain/core@0.3.18(openai@4.72.0(zod@3.23.8))))(@langchain/aws@0.1.2(@aws-sdk/client-sso-oidc@3.668.0(@aws-sdk/client-sts@3.668.0))(@aws-sdk/client-sts@3.668.0)(@langchain/core@0.3.18(openai@4.72.0(zod@3.23.8))))(@langchain/core@0.3.18(openai@4.72.0(zod@3.23.8)))(axios@1.7.7)(handlebars@4.7.8)(openai@4.72.0(zod@3.23.8))
version: 0.3.6(@langchain/anthropic@0.3.8(@langchain/core@0.3.18(openai@4.72.0(zod@3.23.8))))(@langchain/aws@0.1.2(@aws-sdk/client-sso-oidc@3.668.0(@aws-sdk/client-sts@3.668.0))(@aws-sdk/client-sts@3.668.0)(@langchain/core@0.3.18(openai@4.72.0(zod@3.23.8))))(@langchain/core@0.3.18(openai@4.72.0(zod@3.23.8)))(@langchain/google-vertexai@0.1.3(@langchain/core@0.3.18(openai@4.72.0(zod@3.23.8)))(zod@3.23.8))(axios@1.7.7)(handlebars@4.7.8)(openai@4.72.0(zod@3.23.8))
langfuse-langchain:
specifier: 3.30.3
version: 3.30.3(langchain@0.3.6(@langchain/anthropic@0.3.8(@langchain/core@0.3.18(openai@4.72.0(zod@3.23.8))))(@langchain/aws@0.1.2(@aws-sdk/client-sso-oidc@3.668.0(@aws-sdk/client-sts@3.668.0))(@aws-sdk/client-sts@3.668.0)(@langchain/core@0.3.18(openai@4.72.0(zod@3.23.8))))(@langchain/core@0.3.18(openai@4.72.0(zod@3.23.8)))(axios@1.7.7)(handlebars@4.7.8)(openai@4.72.0(zod@3.23.8)))
version: 3.30.3(langchain@0.3.6(@langchain/anthropic@0.3.8(@langchain/core@0.3.18(openai@4.72.0(zod@3.23.8))))(@langchain/aws@0.1.2(@aws-sdk/client-sso-oidc@3.668.0(@aws-sdk/client-sts@3.668.0))(@aws-sdk/client-sts@3.668.0)(@langchain/core@0.3.18(openai@4.72.0(zod@3.23.8))))(@langchain/core@0.3.18(openai@4.72.0(zod@3.23.8)))(@langchain/google-vertexai@0.1.3(@langchain/core@0.3.18(openai@4.72.0(zod@3.23.8)))(zod@3.23.8))(axios@1.7.7)(handlebars@4.7.8)(openai@4.72.0(zod@3.23.8)))
lodash:
specifier: ^4.17.21
version: 4.17.21
@@ -578,13 +581,13 @@ importers:
version: 0.27.4
langchain:
specifier: ^0.3.6
version: 0.3.6(@langchain/anthropic@0.3.8(@langchain/core@0.3.18(openai@4.72.0(zod@3.23.8))))(@langchain/aws@0.1.2(@aws-sdk/client-sts@3.675.0)(@langchain/core@0.3.18(openai@4.72.0(zod@3.23.8))))(@langchain/core@0.3.18(openai@4.72.0(zod@3.23.8)))(axios@1.7.7)(handlebars@4.7.8)(openai@4.72.0(zod@3.23.8))
version: 0.3.6(@langchain/anthropic@0.3.8(@langchain/core@0.3.18(openai@4.72.0(zod@3.23.8))))(@langchain/aws@0.1.2(@aws-sdk/client-sts@3.675.0)(@langchain/core@0.3.18(openai@4.72.0(zod@3.23.8))))(@langchain/core@0.3.18(openai@4.72.0(zod@3.23.8)))(@langchain/google-vertexai@0.1.3(@langchain/core@0.3.18(openai@4.72.0(zod@3.23.8)))(zod@3.23.8))(axios@1.7.7)(handlebars@4.7.8)(openai@4.72.0(zod@3.23.8))
lodash:
specifier: ^4.17.21
version: 4.17.21
lucide-react:
specifier: ^0.447.0
version: 0.447.0(react@18.2.0)
specifier: ^0.462.0
version: 0.462.0(react@18.2.0)
next:
specifier: ^14.2.15
version: 14.2.15(@babel/core@7.24.3)(@opentelemetry/api@1.9.0)(@playwright/test@1.47.2)(babel-plugin-macros@3.1.0)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
@@ -601,8 +604,8 @@ importers:
specifier: ^1.176.0
version: 1.176.0
posthog-node:
specifier: ^3.6.3
version: 3.6.3
specifier: ^4.3.1
version: 4.3.1
prexit:
specifier: ^2.2.0
version: 2.2.0
@@ -652,8 +655,8 @@ importers:
specifier: ^1.4.41
version: 1.4.41(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
stripe:
specifier: ^16.8.0
version: 16.8.0
specifier: ^17.4.0
version: 17.4.0
superjson:
specifier: 2.2.1
version: 2.2.1
@@ -885,8 +888,8 @@ importers:
specifier: ^8.13.0
version: 8.13.0
stripe:
specifier: ^16.8.0
version: 16.8.0
specifier: ^17.4.0
version: 17.4.0
tiktoken:
specifier: ^1.0.15
version: 1.0.15
@@ -2391,6 +2394,24 @@ packages:
resolution: {integrity: sha512-IEZCrFs1Xd0J2FTH1D3Lnm3/Yk2r8LSpwDeLYwcCom3rNAK5k4mKQ2rwIpNq3YuqBdrTNMKRO+PopjkP1SB17A==}
engines: {node: '>=18'}
'@langchain/google-common@0.1.3':
resolution: {integrity: sha512-yry0taVC5AUwn55Gc6PTQX2XHLfbh6PAOheatkB77u22bYzLNaKG+DGQR8CRVRS9oZEXuNpXyAV7X2+2wv1w6Q==}
engines: {node: '>=18'}
peerDependencies:
'@langchain/core': '>=0.2.21 <0.4.0'
'@langchain/google-gauth@0.1.3':
resolution: {integrity: sha512-/FfF/5KNP78gUHLMhyqI9kMlYy7v+uyRddjx0lsiTOGWTZeFhhemmYw4Vbb8ltUSgfyx7/c9M8GryxY9vW5pqA==}
engines: {node: '>=18'}
peerDependencies:
'@langchain/core': '>=0.2.21 <0.4.0'
'@langchain/google-vertexai@0.1.3':
resolution: {integrity: sha512-AOvojzPiKKv1gZiH9tBYPBcZ3nOCuQAQsh4a/djYmlwjcA5bIb/teOa9eFZVTB83L2eM1U5steZF4nwQHnIIvw==}
engines: {node: '>=18'}
peerDependencies:
'@langchain/core': '>=0.2.21 <0.4.0'
'@langchain/openai@0.3.14':
resolution: {integrity: sha512-lNWjUo1tbvsss45IF7UQtMu1NJ6oUKvhgPYWXnX9f/d6OmuLu7D99HQ3Y88vLcUo9XjjOy417olYHignMduMjA==}
engines: {node: '>=18'}
@@ -5518,6 +5539,10 @@ packages:
resolution: {integrity: sha512-bMxMKAjg13EBSVscxTaYA4mRc5t1UAXa2kXiGTNfZ079HIWXEkKmkgFrh/nJqamaLSrXO5H4WFFkPEaLJWbs3A==}
engines: {node: '>= 0.4'}
arrify@2.0.1:
resolution: {integrity: sha512-3duEwti880xqi4eAMN8AyR4a0ByT90zoYdLlevfrvU43vb0YZwZVfxOgxWrLXXXpyugL0hNZc9G6BiB5B3nUug==}
engines: {node: '>=8'}
assertion-error@2.0.1:
resolution: {integrity: sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==}
engines: {node: '>=12'}
@@ -5619,6 +5644,9 @@ packages:
before-after-hook@2.2.3:
resolution: {integrity: sha512-NzUnlZexiaH/46WDhANlyR2bXRopNg4F/zuSA3OpZnllCUgRaOF2znDioDWrmbNVsuZk6l9pMquQB38cfBZwkQ==}
bignumber.js@9.1.2:
resolution: {integrity: sha512-2/mKyZH9K85bzOEfhXDBFZTGd1CTs+5IHpeFQo9luiBG7hghdC851Pj2WAhb6E3R6b9tZj/XKhbg4fum+Kepug==}
binary-extensions@2.3.0:
resolution: {integrity: sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==}
engines: {node: '>=8'}
@@ -5668,6 +5696,9 @@ packages:
buffer-crc32@0.2.13:
resolution: {integrity: sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==}
buffer-equal-constant-time@1.0.1:
resolution: {integrity: sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==}
buffer-from@1.1.2:
resolution: {integrity: sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==}
@@ -6616,6 +6647,9 @@ packages:
eastasianwidth@0.2.0:
resolution: {integrity: sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==}
ecdsa-sig-formatter@1.0.11:
resolution: {integrity: sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==}
editorconfig@1.0.4:
resolution: {integrity: sha512-L9Qe08KWTlqYMVvMcTIvMAdl1cDUubzRNYL+WfA4bLDMHe4nemKkpmYzkznE1FwLKu0EEmy6obgQKzMJrg4x9Q==}
engines: {node: '>=14'}
@@ -7098,6 +7132,9 @@ packages:
fast-levenshtein@2.0.6:
resolution: {integrity: sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==}
fast-text-encoding@1.0.6:
resolution: {integrity: sha512-VhXlQgj9ioXCqGstD37E/HBeqEGV/qOD/kmbVG8h5xKBYvM1L3lR1Zn4555cQ8GkYbJa8aJSipLPndE1k6zK2w==}
fast-xml-parser@4.4.1:
resolution: {integrity: sha512-xkjOecfnKGkSsOwtZ5Pz7Us/T6mrbPQrq0nh+aCO5V9nk5NLWmasAHumTKjiPJPWANe+kAZ84Jc8ooJkzZ88Sw==}
hasBin: true
@@ -7278,6 +7315,14 @@ packages:
functions-have-names@1.2.3:
resolution: {integrity: sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==}
gaxios@5.1.3:
resolution: {integrity: sha512-95hVgBRgEIRQQQHIbnxBXeHbW4TqFk4ZDJW7wmVtvYar72FdhRIo1UGOLS2eRAKCPEdPBWu+M7+A33D9CdX9rA==}
engines: {node: '>=12'}
gcp-metadata@5.3.0:
resolution: {integrity: sha512-FNTkdNEnBdlqF2oatizolQqNANMrcqJt6AAYt99B3y1aLLC8Hc5IOBb+ZnnzllodEEf6xMBp6wRcBbc16fa65w==}
engines: {node: '>=12'}
gensync@1.0.0-beta.2:
resolution: {integrity: sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==}
engines: {node: '>=6.9.0'}
@@ -7411,6 +7456,16 @@ packages:
resolution: {integrity: sha512-jOMLD2Z7MAhyG8aJpNOpmziMOP4rPLcc95oQPKXBazW82z+CEgPFBQvEpRUa1KeIMUJo4Wsm+q6uzO/Q/4BksQ==}
engines: {node: '>=18'}
google-auth-library@8.9.0:
resolution: {integrity: sha512-f7aQCJODJFmYWN6PeNKzgvy9LI2tYmXnzpNDHEjG5sDNPgGb2FXQyTBnXeSH+PAtpKESFD+LmHw3Ox3mN7e1Fg==}
engines: {node: '>=12'}
google-p12-pem@4.0.1:
resolution: {integrity: sha512-WPkN4yGtz05WZ5EhtlxNDWPhC4JIic6G8ePitwUWy4l+XPVYec+a0j0Ts47PDtW59y3RwAhUd9/h9ZZ63px6RQ==}
engines: {node: '>=12.0.0'}
deprecated: Package is no longer maintained
hasBin: true
gopd@1.0.1:
resolution: {integrity: sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==}
@@ -7435,6 +7490,10 @@ packages:
resolution: {integrity: sha512-GGTKBX4SD7Wdb8mqeDLni2oaRGYQWjWHGKPQ24ZMnUtKfcsVoiv4uX8+LJr1K6U5VW2Lu1BwJnj7uiori0YtRw==}
engines: {node: ^12.22.0 || ^14.16.0 || ^16.0.0 || >=17.0.0}
gtoken@6.1.2:
resolution: {integrity: sha512-4ccGpzz7YAr7lxrT2neugmXQ3hP9ho2gcaityLVkiUecAiwiy60Ii8gRbZeOsXV19fYaRjgBSshs8kXw+NKCPQ==}
engines: {node: '>=12.0.0'}
hachure-fill@0.5.2:
resolution: {integrity: sha512-3GKBOn+m2LX9iq+JC1064cSFprJY4jL1jCXTcpnfER5HYE2l/4EfWSGzkPa/ZDBmYI0ZOEj5VHV/eKnPGkHuOg==}
@@ -8206,6 +8265,9 @@ packages:
engines: {node: '>=6'}
hasBin: true
json-bigint@1.0.0:
resolution: {integrity: sha512-SiPv/8VpZuWbvLSMtTDU8hEfrZWg/mH/nV/b4o0CYbSxu1UIQPLdwKOCIyLQX+VIPO5vrLX3i8qtqFyhdPSUSQ==}
json-buffer@3.0.1:
resolution: {integrity: sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==}
@@ -8251,6 +8313,12 @@ packages:
resolution: {integrity: sha512-ZZow9HBI5O6EPgSJLUb8n2NKgmVWTwCvHGwFuJlMjvLFqlGG6pjirPhtdsseaLZjSibD8eegzmYpUZwoIlj2cQ==}
engines: {node: '>=4.0'}
jwa@2.0.0:
resolution: {integrity: sha512-jrZ2Qx916EA+fq9cEAeCROWPTfCwi1IVHqT2tapuqLEVVDKFDENFw1oL+MwrTvH6msKxsd1YTDVw6uKEcsrLEA==}
jws@4.0.0:
resolution: {integrity: sha512-KDncfTmOZoOMTFG4mBlG0qUIOlc03fmzH+ru6RgYVZhPkyiy/92Owlt/8UEN+a4TXR1FQetfIpJE8ApdvdVxTg==}
katex@0.16.11:
resolution: {integrity: sha512-RQrI8rlHY92OLf3rho/Ts8i/XvjgguEjOkO1BEXcU3N8BqPpSzBNwV/G0Ukr+P/l3ivvJUE/Fa/CwbS6HesGNQ==}
hasBin: true
@@ -8549,8 +8617,8 @@ packages:
resolution: {integrity: sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA==}
engines: {node: '>=12'}
lucide-react@0.447.0:
resolution: {integrity: sha512-SZ//hQmvi+kDKrNepArVkYK7/jfeZ5uFNEnYmd45RKZcbGD78KLnrcNXmgeg6m+xNHFvTG+CblszXCy4n6DN4w==}
lucide-react@0.462.0:
resolution: {integrity: sha512-NTL7EbAao9IFtuSivSZgrAh4fZd09Lr+6MTkqIxuHaH2nnYiYIzXPo06cOxHg9wKLdj6LL8TByG4qpePqwgx/g==}
peerDependencies:
react: ^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0-rc
@@ -9027,6 +9095,10 @@ packages:
resolution: {integrity: sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA==}
engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0}
node-forge@1.3.1:
resolution: {integrity: sha512-dPEtOeMvF9VMcYV/1Wb8CPoVAXtp6MKMlcbAt4ddqmGqUJ6fQZFXkNZNkNlfevtNkGtaSoXf/vNNNSvgrdXwtA==}
engines: {node: '>= 6.13.0'}
node-gyp-build-optional-packages@5.2.2:
resolution: {integrity: sha512-s+w+rBWnpTMwSFbaE0UXsRlg7hU4FjekKU4eyAih5T8nJuNZT1nNsskXpxmeqSK9UzkBl6UgRlnKc8hz8IEqOw==}
hasBin: true
@@ -9584,8 +9656,8 @@ packages:
posthog-js@1.176.0:
resolution: {integrity: sha512-T5XKNtRzp7q6CGb7Vc7wAI76rWap9fiuDUPxPsyPBPDkreKya91x9RIsSapAVFafwD1AEin1QMczCmt9Le9BWw==}
posthog-node@3.6.3:
resolution: {integrity: sha512-JB+ei0LkwE+rKHyW5z79Nd1jUaGxU6TvkfjFqY9vQaHxU5aU8dRl0UUaEmZdZbHwjp3WmXCBQQRNyimwbNQfCw==}
posthog-node@4.3.1:
resolution: {integrity: sha512-By9SEGZxBLC7GVyVb+HlJlpxM/xI4iLUgwtsBS8f4bZ0wqYKiNHoYcFEwMJnTM9xQcQZztr6dqLmsJ7jCv0vxA==}
engines: {node: '>=15.0.0'}
pprof-format@2.1.0:
@@ -9805,10 +9877,6 @@ packages:
pure-rand@6.0.4:
resolution: {integrity: sha512-LA0Y9kxMYv47GIPJy6MI84fqTd2HmYZI83W/kM/SkKfDlajnZYfmXFTxkbY+xSBPkLJxltMa9hIkmdc29eguMA==}
qs@6.11.0:
resolution: {integrity: sha512-MvjoMCJwEarSbUYk5O+nmoSzSutSsTwF85zcHPQ9OrlFoZOYIjaqBAJIqIXjptyD5vThxGq52Xu/MaJzRkIk4Q==}
engines: {node: '>=0.6'}
qs@6.13.0:
resolution: {integrity: sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg==}
engines: {node: '>=0.6'}
@@ -10564,8 +10632,8 @@ packages:
resolution: {integrity: sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==}
engines: {node: '>=8'}
stripe@16.8.0:
resolution: {integrity: sha512-6rOIcGOkxcc29jvhEyOYmpPFilekOBV+7vpemAoIAfbtCRW1yxzdDGM0/0vyekHglLL+wqGpP5ldrhO3dJ2JEQ==}
stripe@17.4.0:
resolution: {integrity: sha512-sQQGZguPxe7/QYXJKtDpfzT2OAH9F8nyE2SOsVdTU793iiU33/dpaKgWaJEGJm8396Yy/6NvTLblgdHlueGLhA==}
engines: {node: '>=12.*'}
strnum@1.0.5:
@@ -14034,6 +14102,33 @@ snapshots:
transitivePeerDependencies:
- openai
'@langchain/google-common@0.1.3(@langchain/core@0.3.18(openai@4.72.0(zod@3.23.8)))(zod@3.23.8)':
dependencies:
'@langchain/core': 0.3.18(openai@4.72.0(zod@3.23.8))
uuid: 10.0.0
zod-to-json-schema: 3.23.5(zod@3.23.8)
transitivePeerDependencies:
- zod
'@langchain/google-gauth@0.1.3(@langchain/core@0.3.18(openai@4.72.0(zod@3.23.8)))(zod@3.23.8)':
dependencies:
'@langchain/core': 0.3.18(openai@4.72.0(zod@3.23.8))
'@langchain/google-common': 0.1.3(@langchain/core@0.3.18(openai@4.72.0(zod@3.23.8)))(zod@3.23.8)
google-auth-library: 8.9.0
transitivePeerDependencies:
- encoding
- supports-color
- zod
'@langchain/google-vertexai@0.1.3(@langchain/core@0.3.18(openai@4.72.0(zod@3.23.8)))(zod@3.23.8)':
dependencies:
'@langchain/core': 0.3.18(openai@4.72.0(zod@3.23.8))
'@langchain/google-gauth': 0.1.3(@langchain/core@0.3.18(openai@4.72.0(zod@3.23.8)))(zod@3.23.8)
transitivePeerDependencies:
- encoding
- supports-color
- zod
'@langchain/openai@0.3.14(@langchain/core@0.3.18(openai@4.72.0(zod@3.23.8)))':
dependencies:
'@langchain/core': 0.3.18(openai@4.72.0(zod@3.23.8))
@@ -17378,10 +17473,10 @@ snapshots:
'@typescript-eslint/eslint-plugin': 7.3.1(@typescript-eslint/parser@7.12.0(eslint@8.57.0)(typescript@5.4.5))(eslint@8.57.0)(typescript@5.4.5)
'@typescript-eslint/parser': 7.12.0(eslint@8.57.0)(typescript@5.4.5)
eslint-config-prettier: 9.1.0(eslint@8.57.0)
eslint-import-resolver-alias: 1.1.2(eslint-plugin-import@2.29.1(@typescript-eslint/parser@7.12.0(eslint@8.57.0)(typescript@5.4.5))(eslint-import-resolver-typescript@3.6.1)(eslint@8.57.0))
eslint-import-resolver-typescript: 3.6.1(@typescript-eslint/parser@7.12.0(eslint@8.57.0)(typescript@5.4.5))(eslint-plugin-import@2.29.1(@typescript-eslint/parser@7.12.0(eslint@8.57.0)(typescript@5.4.5))(eslint-import-resolver-typescript@3.6.1)(eslint@8.57.0))(eslint@8.57.0)
eslint-import-resolver-alias: 1.1.2(eslint-plugin-import@2.29.1)
eslint-import-resolver-typescript: 3.6.1(@typescript-eslint/parser@7.12.0(eslint@8.57.0)(typescript@5.4.5))(eslint-plugin-import@2.29.1)(eslint@8.57.0)
eslint-plugin-eslint-comments: 3.2.0(eslint@8.57.0)
eslint-plugin-import: 2.29.1(@typescript-eslint/parser@7.12.0(eslint@8.57.0)(typescript@5.4.5))(eslint-import-resolver-typescript@3.6.1(@typescript-eslint/parser@7.12.0(eslint@8.57.0)(typescript@5.4.5))(eslint-plugin-import@2.29.1(@typescript-eslint/parser@7.12.0(eslint@8.57.0)(typescript@5.4.5))(eslint-import-resolver-typescript@3.6.1)(eslint@8.57.0))(eslint@8.57.0))(eslint@8.57.0)
eslint-plugin-import: 2.29.1(@typescript-eslint/parser@7.12.0(eslint@8.57.0)(typescript@5.4.5))(eslint-import-resolver-typescript@3.6.1(@typescript-eslint/parser@7.12.0(eslint@8.57.0)(typescript@5.4.5))(eslint-plugin-import@2.29.1)(eslint@8.57.0))(eslint@8.57.0)
eslint-plugin-jest: 27.9.0(@typescript-eslint/eslint-plugin@7.3.1(@typescript-eslint/parser@7.12.0(eslint@8.57.0)(typescript@5.4.5))(eslint@8.57.0)(typescript@5.4.5))(eslint@8.57.0)(jest@29.7.0(@types/node@20.14.8)(babel-plugin-macros@3.1.0))(typescript@5.4.5)
eslint-plugin-jsx-a11y: 6.8.0(eslint@8.57.0)
eslint-plugin-playwright: 1.5.4(eslint-plugin-jest@27.9.0(@typescript-eslint/eslint-plugin@7.3.1(@typescript-eslint/parser@7.12.0(eslint@8.57.0)(typescript@5.4.5))(eslint@8.57.0)(typescript@5.4.5))(eslint@8.57.0)(jest@29.7.0(@types/node@20.14.8)(babel-plugin-macros@3.1.0))(typescript@5.4.5))(eslint@8.57.0)
@@ -17865,6 +17960,8 @@ snapshots:
is-array-buffer: 3.0.4
is-shared-array-buffer: 1.0.3
arrify@2.0.1: {}
assertion-error@2.0.1: {}
ast-types-flow@0.0.8: {}
@@ -17988,6 +18085,8 @@ snapshots:
before-after-hook@2.2.3: {}
bignumber.js@9.1.2: {}
binary-extensions@2.3.0: {}
bl@4.1.0:
@@ -18066,6 +18165,8 @@ snapshots:
buffer-crc32@0.2.13: {}
buffer-equal-constant-time@1.0.1: {}
buffer-from@1.1.2: {}
buffer@5.6.0:
@@ -19043,6 +19144,10 @@ snapshots:
eastasianwidth@0.2.0: {}
ecdsa-sig-formatter@1.0.11:
dependencies:
safe-buffer: 5.2.1
editorconfig@1.0.4:
dependencies:
'@one-ini/wasm': 0.1.1
@@ -19286,7 +19391,7 @@ snapshots:
eslint: 8.57.0
eslint-import-resolver-node: 0.3.9
eslint-import-resolver-typescript: 3.6.1(@typescript-eslint/parser@7.12.0(eslint@8.57.0)(typescript@5.4.5))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.29.1)(eslint@8.57.0)
eslint-plugin-import: 2.29.1(@typescript-eslint/parser@7.12.0(eslint@8.57.0)(typescript@5.4.5))(eslint-import-resolver-typescript@3.6.1(@typescript-eslint/parser@7.12.0(eslint@8.57.0)(typescript@5.4.5))(eslint-plugin-import@2.29.1(@typescript-eslint/parser@7.12.0(eslint@8.57.0)(typescript@5.4.5))(eslint-import-resolver-typescript@3.6.1)(eslint@8.57.0))(eslint@8.57.0))(eslint@8.57.0)
eslint-plugin-import: 2.29.1(@typescript-eslint/parser@7.12.0(eslint@8.57.0)(typescript@5.4.5))(eslint-import-resolver-typescript@3.6.1(@typescript-eslint/parser@7.12.0(eslint@8.57.0)(typescript@5.4.5))(eslint-plugin-import@2.29.1)(eslint@8.57.0))(eslint@8.57.0)
eslint-plugin-jsx-a11y: 6.8.0(eslint@8.57.0)
eslint-plugin-react: 7.34.1(eslint@8.57.0)
eslint-plugin-react-hooks: 4.6.0(eslint@8.57.0)
@@ -19303,7 +19408,7 @@ snapshots:
eslint-config-standard@17.1.0(eslint-plugin-import@2.29.1(@typescript-eslint/parser@7.12.0(eslint@8.57.0)(typescript@5.4.5))(eslint@8.57.0))(eslint-plugin-n@16.6.2(eslint@8.57.0))(eslint-plugin-promise@6.4.0(eslint@8.57.0))(eslint@8.57.0):
dependencies:
eslint: 8.57.0
eslint-plugin-import: 2.29.1(@typescript-eslint/parser@7.12.0(eslint@8.57.0)(typescript@5.4.5))(eslint-import-resolver-typescript@3.6.1(@typescript-eslint/parser@7.12.0(eslint@8.57.0)(typescript@5.4.5))(eslint-plugin-import@2.29.1(@typescript-eslint/parser@7.12.0(eslint@8.57.0)(typescript@5.4.5))(eslint-import-resolver-typescript@3.6.1)(eslint@8.57.0))(eslint@8.57.0))(eslint@8.57.0)
eslint-plugin-import: 2.29.1(@typescript-eslint/parser@7.12.0(eslint@8.57.0)(typescript@5.4.5))(eslint-import-resolver-typescript@3.6.1(@typescript-eslint/parser@7.12.0(eslint@8.57.0)(typescript@5.4.5))(eslint-plugin-import@2.29.1)(eslint@8.57.0))(eslint@8.57.0)
eslint-plugin-n: 16.6.2(eslint@8.57.0)
eslint-plugin-promise: 6.4.0(eslint@8.57.0)
@@ -19312,9 +19417,9 @@ snapshots:
eslint: 8.57.0
eslint-plugin-turbo: 1.13.4(eslint@8.57.0)
eslint-import-resolver-alias@1.1.2(eslint-plugin-import@2.29.1(@typescript-eslint/parser@7.12.0(eslint@8.57.0)(typescript@5.4.5))(eslint-import-resolver-typescript@3.6.1)(eslint@8.57.0)):
eslint-import-resolver-alias@1.1.2(eslint-plugin-import@2.29.1):
dependencies:
eslint-plugin-import: 2.29.1(@typescript-eslint/parser@7.12.0(eslint@8.57.0)(typescript@5.4.5))(eslint-import-resolver-typescript@3.6.1(@typescript-eslint/parser@7.12.0(eslint@8.57.0)(typescript@5.4.5))(eslint-plugin-import@2.29.1(@typescript-eslint/parser@7.12.0(eslint@8.57.0)(typescript@5.4.5))(eslint-import-resolver-typescript@3.6.1)(eslint@8.57.0))(eslint@8.57.0))(eslint@8.57.0)
eslint-plugin-import: 2.29.1(@typescript-eslint/parser@7.12.0(eslint@8.57.0)(typescript@5.4.5))(eslint-import-resolver-typescript@3.6.1(@typescript-eslint/parser@7.12.0(eslint@8.57.0)(typescript@5.4.5))(eslint-plugin-import@2.29.1)(eslint@8.57.0))(eslint@8.57.0)
eslint-import-resolver-node@0.3.9:
dependencies:
@@ -19329,8 +19434,8 @@ snapshots:
debug: 4.3.7(supports-color@5.5.0)
enhanced-resolve: 5.17.1
eslint: 8.57.0
eslint-module-utils: 2.8.1(@typescript-eslint/parser@7.12.0(eslint@8.57.0)(typescript@5.4.5))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.1(@typescript-eslint/parser@7.12.0(eslint@8.57.0)(typescript@5.4.5))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.29.1)(eslint@8.57.0))(eslint@8.57.0)
eslint-plugin-import: 2.29.1(@typescript-eslint/parser@7.12.0(eslint@8.57.0)(typescript@5.4.5))(eslint-import-resolver-typescript@3.6.1(@typescript-eslint/parser@7.12.0(eslint@8.57.0)(typescript@5.4.5))(eslint-plugin-import@2.29.1(@typescript-eslint/parser@7.12.0(eslint@8.57.0)(typescript@5.4.5))(eslint-import-resolver-typescript@3.6.1)(eslint@8.57.0))(eslint@8.57.0))(eslint@8.57.0)
eslint-module-utils: 2.8.1(@typescript-eslint/parser@7.12.0(eslint@8.57.0)(typescript@5.4.5))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.1)(eslint@8.57.0)
eslint-plugin-import: 2.29.1(@typescript-eslint/parser@7.12.0(eslint@8.57.0)(typescript@5.4.5))(eslint-import-resolver-typescript@3.6.1(@typescript-eslint/parser@7.12.0(eslint@8.57.0)(typescript@5.4.5))(eslint-plugin-import@2.29.1)(eslint@8.57.0))(eslint@8.57.0)
fast-glob: 3.3.2
get-tsconfig: 4.8.1
is-core-module: 2.15.1
@@ -19341,13 +19446,13 @@ snapshots:
- eslint-import-resolver-webpack
- supports-color
eslint-import-resolver-typescript@3.6.1(@typescript-eslint/parser@7.12.0(eslint@8.57.0)(typescript@5.4.5))(eslint-plugin-import@2.29.1(@typescript-eslint/parser@7.12.0(eslint@8.57.0)(typescript@5.4.5))(eslint-import-resolver-typescript@3.6.1)(eslint@8.57.0))(eslint@8.57.0):
eslint-import-resolver-typescript@3.6.1(@typescript-eslint/parser@7.12.0(eslint@8.57.0)(typescript@5.4.5))(eslint-plugin-import@2.29.1)(eslint@8.57.0):
dependencies:
debug: 4.3.7(supports-color@5.5.0)
enhanced-resolve: 5.17.1
eslint: 8.57.0
eslint-module-utils: 2.8.1(@typescript-eslint/parser@7.12.0(eslint@8.57.0)(typescript@5.4.5))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.1(@typescript-eslint/parser@7.12.0(eslint@8.57.0)(typescript@5.4.5))(eslint-plugin-import@2.29.1(@typescript-eslint/parser@7.12.0(eslint@8.57.0)(typescript@5.4.5))(eslint-import-resolver-typescript@3.6.1)(eslint@8.57.0))(eslint@8.57.0))(eslint@8.57.0)
eslint-plugin-import: 2.29.1(@typescript-eslint/parser@7.12.0(eslint@8.57.0)(typescript@5.4.5))(eslint-import-resolver-typescript@3.6.1(@typescript-eslint/parser@7.12.0(eslint@8.57.0)(typescript@5.4.5))(eslint-plugin-import@2.29.1(@typescript-eslint/parser@7.12.0(eslint@8.57.0)(typescript@5.4.5))(eslint-import-resolver-typescript@3.6.1)(eslint@8.57.0))(eslint@8.57.0))(eslint@8.57.0)
eslint-module-utils: 2.8.1(@typescript-eslint/parser@7.12.0(eslint@8.57.0)(typescript@5.4.5))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.1(@typescript-eslint/parser@7.12.0(eslint@8.57.0)(typescript@5.4.5))(eslint-plugin-import@2.29.1)(eslint@8.57.0))(eslint@8.57.0)
eslint-plugin-import: 2.29.1(@typescript-eslint/parser@7.12.0(eslint@8.57.0)(typescript@5.4.5))(eslint-import-resolver-typescript@3.6.1(@typescript-eslint/parser@7.12.0(eslint@8.57.0)(typescript@5.4.5))(eslint-plugin-import@2.29.1)(eslint@8.57.0))(eslint@8.57.0)
fast-glob: 3.3.2
get-tsconfig: 4.8.1
is-core-module: 2.15.1
@@ -19358,7 +19463,18 @@ snapshots:
- eslint-import-resolver-webpack
- supports-color
eslint-module-utils@2.8.1(@typescript-eslint/parser@7.12.0(eslint@8.57.0)(typescript@5.4.5))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.1(@typescript-eslint/parser@7.12.0(eslint@8.57.0)(typescript@5.4.5))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.29.1)(eslint@8.57.0))(eslint@8.57.0):
eslint-module-utils@2.8.1(@typescript-eslint/parser@7.12.0(eslint@8.57.0)(typescript@5.4.5))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.1(@typescript-eslint/parser@7.12.0(eslint@8.57.0)(typescript@5.4.5))(eslint-plugin-import@2.29.1)(eslint@8.57.0))(eslint@8.57.0):
dependencies:
debug: 3.2.7
optionalDependencies:
'@typescript-eslint/parser': 7.12.0(eslint@8.57.0)(typescript@5.4.5)
eslint: 8.57.0
eslint-import-resolver-node: 0.3.9
eslint-import-resolver-typescript: 3.6.1(@typescript-eslint/parser@7.12.0(eslint@8.57.0)(typescript@5.4.5))(eslint-plugin-import@2.29.1)(eslint@8.57.0)
transitivePeerDependencies:
- supports-color
eslint-module-utils@2.8.1(@typescript-eslint/parser@7.12.0(eslint@8.57.0)(typescript@5.4.5))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.1)(eslint@8.57.0):
dependencies:
debug: 3.2.7
optionalDependencies:
@@ -19369,17 +19485,6 @@ snapshots:
transitivePeerDependencies:
- supports-color
eslint-module-utils@2.8.1(@typescript-eslint/parser@7.12.0(eslint@8.57.0)(typescript@5.4.5))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.1(@typescript-eslint/parser@7.12.0(eslint@8.57.0)(typescript@5.4.5))(eslint-plugin-import@2.29.1(@typescript-eslint/parser@7.12.0(eslint@8.57.0)(typescript@5.4.5))(eslint-import-resolver-typescript@3.6.1)(eslint@8.57.0))(eslint@8.57.0))(eslint@8.57.0):
dependencies:
debug: 3.2.7
optionalDependencies:
'@typescript-eslint/parser': 7.12.0(eslint@8.57.0)(typescript@5.4.5)
eslint: 8.57.0
eslint-import-resolver-node: 0.3.9
eslint-import-resolver-typescript: 3.6.1(@typescript-eslint/parser@7.12.0(eslint@8.57.0)(typescript@5.4.5))(eslint-plugin-import@2.29.1(@typescript-eslint/parser@7.12.0(eslint@8.57.0)(typescript@5.4.5))(eslint-import-resolver-typescript@3.6.1)(eslint@8.57.0))(eslint@8.57.0)
transitivePeerDependencies:
- supports-color
eslint-plugin-es-x@7.8.0(eslint@8.57.0):
dependencies:
'@eslint-community/eslint-utils': 4.4.1(eslint@8.57.0)
@@ -19393,7 +19498,7 @@ snapshots:
eslint: 8.57.0
ignore: 5.3.1
eslint-plugin-import@2.29.1(@typescript-eslint/parser@7.12.0(eslint@8.57.0)(typescript@5.4.5))(eslint-import-resolver-typescript@3.6.1(@typescript-eslint/parser@7.12.0(eslint@8.57.0)(typescript@5.4.5))(eslint-plugin-import@2.29.1(@typescript-eslint/parser@7.12.0(eslint@8.57.0)(typescript@5.4.5))(eslint-import-resolver-typescript@3.6.1)(eslint@8.57.0))(eslint@8.57.0))(eslint@8.57.0):
eslint-plugin-import@2.29.1(@typescript-eslint/parser@7.12.0(eslint@8.57.0)(typescript@5.4.5))(eslint-import-resolver-typescript@3.6.1(@typescript-eslint/parser@7.12.0(eslint@8.57.0)(typescript@5.4.5))(eslint-plugin-import@2.29.1)(eslint@8.57.0))(eslint@8.57.0):
dependencies:
array-includes: 3.1.7
array.prototype.findlastindex: 1.2.4
@@ -19403,7 +19508,7 @@ snapshots:
doctrine: 2.1.0
eslint: 8.57.0
eslint-import-resolver-node: 0.3.9
eslint-module-utils: 2.8.1(@typescript-eslint/parser@7.12.0(eslint@8.57.0)(typescript@5.4.5))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.1(@typescript-eslint/parser@7.12.0(eslint@8.57.0)(typescript@5.4.5))(eslint-plugin-import@2.29.1(@typescript-eslint/parser@7.12.0(eslint@8.57.0)(typescript@5.4.5))(eslint-import-resolver-typescript@3.6.1)(eslint@8.57.0))(eslint@8.57.0))(eslint@8.57.0)
eslint-module-utils: 2.8.1(@typescript-eslint/parser@7.12.0(eslint@8.57.0)(typescript@5.4.5))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.1(@typescript-eslint/parser@7.12.0(eslint@8.57.0)(typescript@5.4.5))(eslint-plugin-import@2.29.1)(eslint@8.57.0))(eslint@8.57.0)
hasown: 2.0.2
is-core-module: 2.15.1
is-glob: 4.0.3
@@ -19785,6 +19890,8 @@ snapshots:
fast-levenshtein@2.0.6: {}
fast-text-encoding@1.0.6: {}
fast-xml-parser@4.4.1:
dependencies:
strnum: 1.0.5
@@ -19956,6 +20063,24 @@ snapshots:
functions-have-names@1.2.3: {}
gaxios@5.1.3:
dependencies:
extend: 3.0.2
https-proxy-agent: 5.0.1
is-stream: 2.0.1
node-fetch: 2.7.0
transitivePeerDependencies:
- encoding
- supports-color
gcp-metadata@5.3.0:
dependencies:
gaxios: 5.1.3
json-bigint: 1.0.0
transitivePeerDependencies:
- encoding
- supports-color
gensync@1.0.0-beta.2: {}
get-caller-file@2.0.5: {}
@@ -20124,6 +20249,25 @@ snapshots:
slash: 5.1.0
unicorn-magic: 0.1.0
google-auth-library@8.9.0:
dependencies:
arrify: 2.0.1
base64-js: 1.5.1
ecdsa-sig-formatter: 1.0.11
fast-text-encoding: 1.0.6
gaxios: 5.1.3
gcp-metadata: 5.3.0
gtoken: 6.1.2
jws: 4.0.0
lru-cache: 6.0.0
transitivePeerDependencies:
- encoding
- supports-color
google-p12-pem@4.0.1:
dependencies:
node-forge: 1.3.1
gopd@1.0.1:
dependencies:
get-intrinsic: 1.2.4
@@ -20164,6 +20308,15 @@ snapshots:
graphql@16.9.0: {}
gtoken@6.1.2:
dependencies:
gaxios: 5.1.3
google-p12-pem: 4.0.1
jws: 4.0.0
transitivePeerDependencies:
- encoding
- supports-color
hachure-fill@0.5.2: {}
handlebars@4.7.8:
@@ -21221,6 +21374,10 @@ snapshots:
jsesc@3.0.2: {}
json-bigint@1.0.0:
dependencies:
bignumber.js: 9.1.2
json-buffer@3.0.1: {}
json-parse-even-better-errors@2.3.1: {}
@@ -21264,6 +21421,17 @@ snapshots:
object.assign: 4.1.5
object.values: 1.2.0
jwa@2.0.0:
dependencies:
buffer-equal-constant-time: 1.0.1
ecdsa-sig-formatter: 1.0.11
safe-buffer: 5.2.1
jws@4.0.0:
dependencies:
jwa: 2.0.0
safe-buffer: 5.2.1
katex@0.16.11:
dependencies:
commander: 8.3.0
@@ -21299,7 +21467,7 @@ snapshots:
kysely@0.27.4: {}
langchain@0.3.6(@langchain/anthropic@0.3.8(@langchain/core@0.3.18(openai@4.72.0(zod@3.23.8))))(@langchain/aws@0.1.2(@aws-sdk/client-sso-oidc@3.668.0(@aws-sdk/client-sts@3.668.0))(@aws-sdk/client-sts@3.668.0)(@langchain/core@0.3.18(openai@4.72.0(zod@3.23.8))))(@langchain/core@0.3.18(openai@4.72.0(zod@3.23.8)))(axios@1.7.7)(handlebars@4.7.8)(openai@4.72.0(zod@3.23.8)):
langchain@0.3.6(@langchain/anthropic@0.3.8(@langchain/core@0.3.18(openai@4.72.0(zod@3.23.8))))(@langchain/aws@0.1.2(@aws-sdk/client-sso-oidc@3.668.0(@aws-sdk/client-sts@3.668.0))(@aws-sdk/client-sts@3.668.0)(@langchain/core@0.3.18(openai@4.72.0(zod@3.23.8))))(@langchain/core@0.3.18(openai@4.72.0(zod@3.23.8)))(@langchain/google-vertexai@0.1.3(@langchain/core@0.3.18(openai@4.72.0(zod@3.23.8)))(zod@3.23.8))(axios@1.7.7)(handlebars@4.7.8)(openai@4.72.0(zod@3.23.8)):
dependencies:
'@langchain/core': 0.3.18(openai@4.72.0(zod@3.23.8))
'@langchain/openai': 0.3.14(@langchain/core@0.3.18(openai@4.72.0(zod@3.23.8)))
@@ -21317,13 +21485,14 @@ snapshots:
optionalDependencies:
'@langchain/anthropic': 0.3.8(@langchain/core@0.3.18(openai@4.72.0(zod@3.23.8)))
'@langchain/aws': 0.1.2(@aws-sdk/client-sso-oidc@3.668.0(@aws-sdk/client-sts@3.668.0))(@aws-sdk/client-sts@3.668.0)(@langchain/core@0.3.18(openai@4.72.0(zod@3.23.8)))
'@langchain/google-vertexai': 0.1.3(@langchain/core@0.3.18(openai@4.72.0(zod@3.23.8)))(zod@3.23.8)
axios: 1.7.7
handlebars: 4.7.8
transitivePeerDependencies:
- encoding
- openai
langchain@0.3.6(@langchain/anthropic@0.3.8(@langchain/core@0.3.18(openai@4.72.0(zod@3.23.8))))(@langchain/aws@0.1.2(@aws-sdk/client-sts@3.675.0)(@langchain/core@0.3.18(openai@4.72.0(zod@3.23.8))))(@langchain/core@0.3.18(openai@4.72.0(zod@3.23.8)))(axios@1.7.7)(handlebars@4.7.8)(openai@4.72.0(zod@3.23.8)):
langchain@0.3.6(@langchain/anthropic@0.3.8(@langchain/core@0.3.18(openai@4.72.0(zod@3.23.8))))(@langchain/aws@0.1.2(@aws-sdk/client-sts@3.675.0)(@langchain/core@0.3.18(openai@4.72.0(zod@3.23.8))))(@langchain/core@0.3.18(openai@4.72.0(zod@3.23.8)))(@langchain/google-vertexai@0.1.3(@langchain/core@0.3.18(openai@4.72.0(zod@3.23.8)))(zod@3.23.8))(axios@1.7.7)(handlebars@4.7.8)(openai@4.72.0(zod@3.23.8)):
dependencies:
'@langchain/core': 0.3.18(openai@4.72.0(zod@3.23.8))
'@langchain/openai': 0.3.14(@langchain/core@0.3.18(openai@4.72.0(zod@3.23.8)))
@@ -21341,6 +21510,7 @@ snapshots:
optionalDependencies:
'@langchain/anthropic': 0.3.8(@langchain/core@0.3.18(openai@4.72.0(zod@3.23.8)))
'@langchain/aws': 0.1.2(@aws-sdk/client-sts@3.675.0)(@langchain/core@0.3.18(openai@4.72.0(zod@3.23.8)))
'@langchain/google-vertexai': 0.1.3(@langchain/core@0.3.18(openai@4.72.0(zod@3.23.8)))(zod@3.23.8)
axios: 1.7.7
handlebars: 4.7.8
transitivePeerDependencies:
@@ -21351,9 +21521,9 @@ snapshots:
dependencies:
mustache: 4.2.0
langfuse-langchain@3.30.3(langchain@0.3.6(@langchain/anthropic@0.3.8(@langchain/core@0.3.18(openai@4.72.0(zod@3.23.8))))(@langchain/aws@0.1.2(@aws-sdk/client-sso-oidc@3.668.0(@aws-sdk/client-sts@3.668.0))(@aws-sdk/client-sts@3.668.0)(@langchain/core@0.3.18(openai@4.72.0(zod@3.23.8))))(@langchain/core@0.3.18(openai@4.72.0(zod@3.23.8)))(axios@1.7.7)(handlebars@4.7.8)(openai@4.72.0(zod@3.23.8))):
langfuse-langchain@3.30.3(langchain@0.3.6(@langchain/anthropic@0.3.8(@langchain/core@0.3.18(openai@4.72.0(zod@3.23.8))))(@langchain/aws@0.1.2(@aws-sdk/client-sso-oidc@3.668.0(@aws-sdk/client-sts@3.668.0))(@aws-sdk/client-sts@3.668.0)(@langchain/core@0.3.18(openai@4.72.0(zod@3.23.8))))(@langchain/core@0.3.18(openai@4.72.0(zod@3.23.8)))(@langchain/google-vertexai@0.1.3(@langchain/core@0.3.18(openai@4.72.0(zod@3.23.8)))(zod@3.23.8))(axios@1.7.7)(handlebars@4.7.8)(openai@4.72.0(zod@3.23.8))):
dependencies:
langchain: 0.3.6(@langchain/anthropic@0.3.8(@langchain/core@0.3.18(openai@4.72.0(zod@3.23.8))))(@langchain/aws@0.1.2(@aws-sdk/client-sso-oidc@3.668.0(@aws-sdk/client-sts@3.668.0))(@aws-sdk/client-sts@3.668.0)(@langchain/core@0.3.18(openai@4.72.0(zod@3.23.8))))(@langchain/core@0.3.18(openai@4.72.0(zod@3.23.8)))(axios@1.7.7)(handlebars@4.7.8)(openai@4.72.0(zod@3.23.8))
langchain: 0.3.6(@langchain/anthropic@0.3.8(@langchain/core@0.3.18(openai@4.72.0(zod@3.23.8))))(@langchain/aws@0.1.2(@aws-sdk/client-sso-oidc@3.668.0(@aws-sdk/client-sts@3.668.0))(@aws-sdk/client-sts@3.668.0)(@langchain/core@0.3.18(openai@4.72.0(zod@3.23.8))))(@langchain/core@0.3.18(openai@4.72.0(zod@3.23.8)))(@langchain/google-vertexai@0.1.3(@langchain/core@0.3.18(openai@4.72.0(zod@3.23.8)))(zod@3.23.8))(axios@1.7.7)(handlebars@4.7.8)(openai@4.72.0(zod@3.23.8))
langfuse: 3.30.3
langfuse-core: 3.30.3
@@ -21514,7 +21684,7 @@ snapshots:
lru-cache@7.18.3: {}
lucide-react@0.447.0(react@18.2.0):
lucide-react@0.462.0(react@18.2.0):
dependencies:
react: 18.2.0
@@ -22257,6 +22427,8 @@ snapshots:
fetch-blob: 3.2.0
formdata-polyfill: 4.0.10
node-forge@1.3.1: {}
node-gyp-build-optional-packages@5.2.2:
dependencies:
detect-libc: 2.0.3
@@ -22844,7 +23016,7 @@ snapshots:
preact: 10.19.7
web-vitals: 4.2.3
posthog-node@3.6.3:
posthog-node@4.3.1:
dependencies:
axios: 1.7.7
rusha: 0.8.14
@@ -23061,10 +23233,6 @@ snapshots:
pure-rand@6.0.4: {}
qs@6.11.0:
dependencies:
side-channel: 1.0.6
qs@6.13.0:
dependencies:
side-channel: 1.0.6
@@ -23946,10 +24114,10 @@ snapshots:
strip-json-comments@3.1.1: {}
stripe@16.8.0:
stripe@17.4.0:
dependencies:
'@types/node': 20.14.8
qs: 6.11.0
qs: 6.13.0
strnum@1.0.5: {}
+1 -2
View File
@@ -85,8 +85,7 @@ ENV NEXT_MANUAL_SIG_HANDLE true
# set the CI flag to true to get CI specific logs
ENV CI true
RUN NODE_OPTIONS='--max-old-space-size=4096' turbo run build --filter=web
RUN NODE_OPTIONS='--max-old-space-size=4096' turbo run build --filter=web...
# Production image, copy all the files and run next
FROM --platform=${TARGETPLATFORM:-linux/amd64} base AS runner
+28 -16
View File
@@ -97,6 +97,7 @@ const nextConfig = {
...(env.SENTRY_CSP_REPORT_URI ? [reportToHeader] : []),
],
},
// CSP header
{
source: "/:path((?!api).*)*",
headers: [
@@ -105,26 +106,37 @@ const nextConfig = {
value: cspHeader.replace(/\n/g, ""),
},
],
// Disable CSP on Hugging Face to allow for embedded use of Langfuse
missing: [
{
type: "host",
value: "huggingface.co",
},
{
type: "host",
value: ".*\\.hf\\.space$", // *.hf.space
},
],
},
// Required to check authentication status from langfuse.com
...(env.NEXT_PUBLIC_LANGFUSE_CLOUD_REGION !== undefined
? [
{
source: "/api/auth/session",
headers: [
{
key: "Access-Control-Allow-Origin",
value: "https://langfuse.com",
},
{ key: "Access-Control-Allow-Credentials", value: "true" },
{ key: "Access-Control-Allow-Methods", value: "GET,POST" },
{
key: "Access-Control-Allow-Headers",
value: "Content-Type, Authorization",
},
],
},
]
{
source: "/api/auth/session",
headers: [
{
key: "Access-Control-Allow-Origin",
value: "https://langfuse.com",
},
{ key: "Access-Control-Allow-Credentials", value: "true" },
{ key: "Access-Control-Allow-Methods", value: "GET,POST" },
{
key: "Access-Control-Allow-Headers",
value: "Content-Type, Authorization",
},
],
},
]
: []),
// all files in /public/generated are public and can be accessed from any origin, e.g. to render an API reference based on our openapi schema
{
+4 -4
View File
@@ -1,6 +1,6 @@
{
"name": "web",
"version": "2.92.0",
"version": "3.0.0-rc.3",
"private": true,
"license": "MIT",
"engines": {
@@ -115,13 +115,13 @@
"kysely": "^0.27.4",
"langchain": "^0.3.6",
"lodash": "^4.17.21",
"lucide-react": "^0.447.0",
"lucide-react": "^0.462.0",
"next": "^14.2.15",
"next-auth": "^4.24.7",
"next-query-params": "^5.0.1",
"next-themes": "^0.3.0",
"posthog-js": "^1.176.0",
"posthog-node": "^3.6.3",
"posthog-node": "^4.3.1",
"prexit": "^2.2.0",
"prisma": "^5.22.0",
"rate-limiter-flexible": "^5.0.3",
@@ -138,7 +138,7 @@
"remark-gfm": "^4.0.0",
"remark-math": "^6.0.0",
"sonner": "^1.4.41",
"stripe": "^16.8.0",
"stripe": "^17.4.0",
"superjson": "2.2.1",
"tailwind-merge": "^2.5.2",
"tailwindcss-animate": "^1.0.7",
+1 -1
View File
@@ -4059,7 +4059,7 @@ components:
type: string
enum:
- >-
image/png","image/jpeg","image/jpg","image/webp","audio/mpeg","audio/mp3","audio/wav","text/plain","application/pdf
image/png","image/jpeg","image/jpg","image/webp","image/gif","image/svg+xml","image/tiff","image/bmp","audio/mpeg","audio/mp3","audio/wav","audio/ogg","audio/oga","audio/aac","audio/mp4","audio/flac","video/mp4","video/webm","text/plain","text/html","text/css","text/csv","application/pdf","application/msword","application/vnd.ms-excel","application/zip","application/json","application/xml","application/octet-stream
description: The MIME type of the media record
DailyMetrics:
title: DailyMetrics
+3 -3
View File
@@ -22,7 +22,7 @@ test("Successfully sign up & able to go to homepage", async ({ page }) => {
await page.goto("auth/sign-up");
await page.fill('input[name="name"]', "demo lang");
await page.fill('input[name="email"]', randomEmailAddress());
await page.fill('input[type="password"]', "password2");
await page.fill('input[type="password"]', "Password2#!");
await page.click('button[data-testid="submit-email-password-sign-up-form"]');
await page.waitForTimeout(2000);
// see get started page
@@ -35,7 +35,7 @@ test("Successfully sign up & able to go to homepage with uppercase email", async
await page.goto("auth/sign-up");
await page.fill('input[name="name"]', "demo lang");
await page.fill('input[name="email"]', "A" + randomEmailAddress());
await page.fill('input[type="password"]', "password3");
await page.fill('input[type="password"]', "Password3#!");
await page.click('button[data-testid="submit-email-password-sign-up-form"]');
await page.waitForTimeout(2000);
// see get started page
@@ -50,7 +50,7 @@ test("Signup input validation", async ({ page }) => {
await page.waitForTimeout(2000);
await expect(page.getByText("Invalid email")).toBeVisible();
await expect(
page.getByText("Password must be at least 8 characters long"),
page.getByText("Password must be at least 8 characters long."),
).toBeVisible();
await page.click('button[data-testid="submit-email-password-sign-up-form"]');
await page.waitForTimeout(2000);
@@ -0,0 +1,75 @@
import {
createObservation,
createTrace,
} from "@/src/__tests__/fixtures/tracing-factory";
import {
createObservations,
createTraces,
} from "@/src/__tests__/server/repositories/clickhouse-helpers";
import { makeZodVerifiedAPICall } from "@/src/__tests__/test-utils";
import { randomUUID } from "crypto";
import { GetMetricsDailyV1Response } from "@/src/features/public-api/types/metrics";
describe("/api/public/metrics/daily API Endpoint", () => {
it("should get correct results from daily metrics", async () => {
const traceName = randomUUID();
const createdTrace = createTrace({
name: traceName,
user_id: "user-1",
project_id: "7a88fb47-b4e2-43b8-a06c-a5ce950dc53a",
metadata: { key: "value" },
release: "1.0.0",
version: "2.0.0",
});
const observations = [
createObservation({
trace_id: createdTrace.id,
project_id: createdTrace.project_id,
name: "observation-name",
end_time: new Date().getTime(),
start_time: new Date().getTime() - 1000,
input: "input",
output: "output",
provided_model_name: "model-1",
}),
createObservation({
trace_id: createdTrace.id,
project_id: createdTrace.project_id,
name: "observation-name-2",
end_time: new Date().getTime(),
start_time: new Date().getTime() - 100000,
input: "input-2",
output: "output-2",
provided_model_name: "model-2",
}),
];
await createTraces([createdTrace]);
await createObservations(observations);
const metrics = await makeZodVerifiedAPICall(
GetMetricsDailyV1Response,
"GET",
`/api/public/metrics/daily?traceName=${traceName}`,
);
expect(metrics.body.meta.totalItems).toBe(1);
expect(metrics.body.meta.totalPages).toBe(1);
const metric = metrics.body.data[0];
expect(metric.countTraces).toBe(1);
expect(metric.countObservations).toBe(2);
expect(metric.usage).toHaveLength(2);
expect(metric.totalCost).toBe(600);
for (const usage of metric.usage) {
expect(usage.model).toMatch(/model-\d/g);
expect(usage.inputUsage).toBe(1234);
expect(usage.outputUsage).toBe(5678);
expect(usage.totalUsage).toBe(6912);
expect(usage.countObservations).toBe(1);
expect(usage.countTraces).toBe(1);
expect(usage.totalCost).toBe(300);
}
});
});
@@ -7,7 +7,12 @@ import {
createTraces,
} from "@/src/__tests__/server/repositories/clickhouse-helpers";
import { makeZodVerifiedAPICall } from "@/src/__tests__/test-utils";
import { GetTraceV1Response } from "@/src/features/public-api/types/traces";
import {
GetTracesV1Response,
GetTraceV1Response,
} from "@/src/features/public-api/types/traces";
import { randomUUID } from "crypto";
import { snakeCase } from "lodash";
describe("/api/public/traces API Endpoint", () => {
it("should create and get a trace via /traces", async () => {
@@ -73,4 +78,182 @@ describe("/api/public/traces API Endpoint", () => {
]),
);
});
it("should fetch all traces", async () => {
const createdTrace = createTrace({
name: "trace-name",
user_id: "user-1",
project_id: "7a88fb47-b4e2-43b8-a06c-a5ce950dc53a",
metadata: { key: "value" },
release: "1.0.0",
version: "2.0.0",
});
const observations = [
createObservation({
trace_id: createdTrace.id,
project_id: createdTrace.project_id,
name: "observation-name",
end_time: new Date().getTime(),
start_time: new Date().getTime() - 1000,
input: "input",
output: "output",
}),
createObservation({
trace_id: createdTrace.id,
project_id: createdTrace.project_id,
name: "observation-name-2",
end_time: new Date().getTime(),
start_time: new Date().getTime() - 100000,
input: "input-2",
output: "output-2",
}),
];
await createTraces([createdTrace]);
await createObservations(observations);
const traces = await makeZodVerifiedAPICall(
GetTracesV1Response,
"GET",
"/api/public/traces",
);
expect(traces.body.meta.totalItems).toBeGreaterThanOrEqual(1);
expect(traces.body.data.length).toBeGreaterThanOrEqual(1);
const trace = traces.body.data.find((t) => t.id === createdTrace.id);
expect(trace).toBeTruthy();
if (!trace) {
return; // to satisfy TypeScript
}
expect(trace.name).toBe("trace-name");
expect(trace.release).toBe("1.0.0");
expect(trace.externalId).toBeNull();
expect(trace.version).toBe("2.0.0");
expect(trace.projectId).toBe("7a88fb47-b4e2-43b8-a06c-a5ce950dc53a");
expect(trace.latency).toBe(100);
expect(trace.observations.length).toBe(2);
expect(trace.scores.length).toBe(0);
});
it.each([
["userId", randomUUID()],
["sessionId", randomUUID()],
["release", randomUUID()],
["version", randomUUID()],
["name", randomUUID()],
])(
"should fetch all traces filtered by a value (%s, %s)",
async (prop: string, value: string) => {
const createdTrace = createTrace({
[snakeCase(prop)]: value,
project_id: "7a88fb47-b4e2-43b8-a06c-a5ce950dc53a",
metadata: { key: "value" },
});
await createTraces([createdTrace]);
const traces = await makeZodVerifiedAPICall(
GetTracesV1Response,
"GET",
`/api/public/traces?${prop}=${value}`,
);
expect(traces.body.meta.totalItems).toBe(1);
expect(traces.body.data.length).toBe(1);
const trace = traces.body.data[0];
expect(trace.projectId).toBe("7a88fb47-b4e2-43b8-a06c-a5ce950dc53a");
expect((trace as any)[prop]).toBe(value);
},
);
it("should fetch all traces filtered by a tag", async () => {
const tag = randomUUID();
const createdTrace = createTrace({
name: "trace-name",
project_id: "7a88fb47-b4e2-43b8-a06c-a5ce950dc53a",
metadata: { key: "value" },
tags: [tag],
});
await createTraces([createdTrace]);
const traces = await makeZodVerifiedAPICall(
GetTracesV1Response,
"GET",
`/api/public/traces?tags=${[tag]}`,
);
expect(traces.body.meta.totalItems).toBe(1);
expect(traces.body.data.length).toBe(1);
const trace = traces.body.data[0];
expect(trace.projectId).toBe("7a88fb47-b4e2-43b8-a06c-a5ce950dc53a");
});
it("should fetch all traces with pagination", async () => {
const tag = randomUUID();
const createdTrace1 = createTrace({
name: "trace-name",
project_id: "7a88fb47-b4e2-43b8-a06c-a5ce950dc53a",
metadata: { key: "value" },
tags: [tag],
});
const createdTrace2 = createTrace({
name: "trace-name",
project_id: "7a88fb47-b4e2-43b8-a06c-a5ce950dc53a",
metadata: { key: "value" },
tags: [tag],
});
const createdTrace3 = createTrace({
name: "trace-name",
project_id: "7a88fb47-b4e2-43b8-a06c-a5ce950dc53a",
metadata: { key: "value" },
tags: [tag],
});
await createTraces([createdTrace1, createdTrace2, createdTrace3]);
const traces = await makeZodVerifiedAPICall(
GetTracesV1Response,
"GET",
`/api/public/traces?tags=${[tag]}&limit=1&offset=1`,
);
expect(traces.body.meta.totalItems).toBe(3);
expect(traces.body.data.length).toBe(1);
expect(traces.body.meta.totalPages).toBe(3);
const trace = traces.body.data[0];
expect(trace.projectId).toBe("7a88fb47-b4e2-43b8-a06c-a5ce950dc53a");
});
it("should fetch all traces with custom order", async () => {
const tag = randomUUID();
const createdTrace1 = createTrace({
name: "trace-name1",
project_id: "7a88fb47-b4e2-43b8-a06c-a5ce950dc53a",
metadata: { key: "value" },
tags: [tag],
});
const createdTrace2 = createTrace({
name: "trace-name2",
project_id: "7a88fb47-b4e2-43b8-a06c-a5ce950dc53a",
metadata: { key: "value" },
tags: [tag],
});
await createTraces([createdTrace1, createdTrace2]);
const traces = await makeZodVerifiedAPICall(
GetTracesV1Response,
"GET",
`/api/public/traces?tags=${[tag]}&orderBy=name.desc`,
);
expect(traces.body.meta.totalItems).toBe(2);
expect(traces.body.data.length).toBe(2);
const trace1 = traces.body.data[0];
expect(trace1.name).toBe("trace-name2");
const trace2 = traces.body.data[1];
expect(trace2.name).toBe("trace-name1");
});
});
@@ -9,9 +9,9 @@ import {
} from "@/src/__tests__/fixtures/tracing-factory";
import {
getTracesTable,
type TracesAllUiReturnType,
type ObservationRecordInsertType,
type TraceRecordInsertType,
type TracesTableReturnType,
} from "@langfuse/shared/src/server";
import { type FilterState } from "@langfuse/shared";
@@ -43,12 +43,6 @@ describe("Traces table API test", () => {
expect(tableRows[0].userId).toEqual(trace.user_id);
expect(tableRows[0].sessionId).toEqual(trace.session_id);
expect(tableRows[0].public).toEqual(trace.public);
expect(tableRows[0].latency).toBeGreaterThanOrEqual(0);
expect(tableRows[0].usageDetails).toEqual({});
expect(tableRows[0].costDetails).toEqual({});
expect(tableRows[0].level).toBeDefined();
expect(tableRows[0].observationCount).toBeGreaterThanOrEqual(0);
expect(tableRows[0].scoresAvg).toEqual([]);
});
it("should get a correct trace with observations", async () => {
@@ -82,29 +76,13 @@ describe("Traces table API test", () => {
expect(tableRows[0].userId).toEqual(trace.user_id);
expect(tableRows[0].sessionId).toEqual(trace.session_id);
expect(tableRows[0].public).toEqual(trace.public);
expect(tableRows[0].latency).toBeGreaterThanOrEqual(0);
expect(tableRows[0].usageDetails).toEqual({
input: (obs1.usage_details.input + obs2.usage_details.input).toString(),
output: (
obs1.usage_details.output + obs2.usage_details.output
).toString(),
total: (obs1.usage_details.total + obs2.usage_details.total).toString(),
});
expect(tableRows[0].costDetails).toEqual({
input: obs1.cost_details.input + obs2.cost_details.input,
output: obs1.cost_details.output + obs2.cost_details.output,
total: obs1.cost_details.total + obs2.cost_details.total,
});
expect(tableRows[0].level).toBeDefined();
expect(tableRows[0].observationCount).toBeGreaterThanOrEqual(0);
expect(tableRows[0].scoresAvg).toEqual([]);
});
type TestCase = {
traceInput: Partial<TraceRecordInsertType>;
observationInput: Partial<ObservationRecordInsertType>[];
filterstate: FilterState;
expected: Partial<TracesTableReturnType>[];
expected: Partial<TracesAllUiReturnType>[];
};
[
@@ -247,30 +225,6 @@ describe("Traces table API test", () => {
if (expectedTrace.public !== undefined) {
expect(tableRows[index].public).toEqual(expectedTrace.public);
}
if (expectedTrace.latency !== undefined) {
expect(tableRows[index].latency).toEqual(expectedTrace.latency);
}
if (expectedTrace.usage_details !== undefined) {
expect(tableRows[index].usageDetails).toEqual(
expectedTrace.usage_details,
);
}
if (expectedTrace.cost_details !== undefined) {
expect(tableRows[index].costDetails).toEqual(
expectedTrace.cost_details,
);
}
if (expectedTrace.level !== undefined) {
expect(tableRows[index].level).toEqual(expectedTrace.level);
}
if (expectedTrace.observation_count !== undefined) {
expect(tableRows[index].observationCount).toEqual(
expectedTrace.observation_count,
);
}
if (expectedTrace.scores_avg !== undefined) {
expect(tableRows[index].scoresAvg).toEqual(expectedTrace.scores_avg);
}
});
});
});
@@ -14,7 +14,7 @@ describe("observations.export RPC", () => {
beforeAll(async () => {
// Disable S3 upload
process.env.S3_BUCKET_NAME = "";
process.env.LANGFUSE_S3_BATCH_EXPORT_ENABLED = "false";
await pruneDatabase();
const traceId = "trace-1";
+33
View File
@@ -978,6 +978,39 @@ describe("/api/public/ingestion API Endpoint", () => {
expect(dbTrace.length).toBe(1);
});
it("should fail for long trace name", async () => {
const traceId = v4();
const baseString =
"Lorem ipsum dolor sit amet, consectetur adipiscing elit. ";
const repeatCount = Math.ceil(1500 / baseString.length);
const name = baseString.repeat(repeatCount);
const response = await makeAPICall("POST", "/api/public/ingestion", {
batch: [
{
id: v4(),
type: "trace-create",
timestamp: new Date().toISOString(),
body: {
id: traceId,
name,
userId: "user-1",
metadata: { key: "value" },
release: "1.0.0",
version: "2.0.0",
},
},
],
});
expect(response.status).toBe(207);
expect("errors" in response.body).toBe(true);
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
expect(response.body.errors.length).toBe(1);
expect(response.body.errors[0].message).toBe("Invalid request data");
});
it("should update all token counts if update does not contain model name", async () => {
const traceId = v4();
const generationId = v4();
@@ -157,7 +157,7 @@ describe("UI Prompts Table", () => {
firstObservation: expect.any(Date),
lastObservation: expect.any(Date),
medianInputUsage: 100,
medianLatencyMs: 0,
medianLatencyMs: expect.any(Number),
medianOutputUsage: 5654,
medianTotalCost: 0,
promptId: "some-prompt-id",
@@ -168,7 +168,7 @@ describe("UI Prompts Table", () => {
firstObservation: expect.any(Date),
lastObservation: expect.any(Date),
medianInputUsage: 100,
medianLatencyMs: 0,
medianLatencyMs: expect.any(Number),
medianOutputUsage: 200,
medianTotalCost: 0,
promptId: "some-prompt-id",
+151
View File
@@ -0,0 +1,151 @@
import { Lock, AlertCircle, Sparkle } from "lucide-react";
import { Button, type ButtonProps } from "@/src/components/ui/button";
import {
HoverCard,
HoverCardContent,
HoverCardTrigger,
} from "@/src/components/ui/hover-card";
import { HoverCardPortal } from "@radix-ui/react-hover-card";
import Link from "next/link";
const BUTTON_STATE_MESSAGES = {
limitReached: (current: number, max: number) =>
`You have reached the limit (${current}/${max}) for this resource at your current plan. Upgrade your plan to increase the limit.`,
noAccess:
"You do not have access to this resource, please ask your admin to grant you access.",
entitlement: "This feature is not available in your current plan.",
} as const;
interface ActionButtonProps extends ButtonProps {
icon?: React.ReactNode;
loading?: boolean;
hasAccess?: boolean;
hasEntitlement?: boolean;
limitValue?: number;
limit?: number | false;
children: React.ReactNode;
className?: string;
href?: string;
}
export function ActionButton({
loading = false,
hasAccess = true,
hasEntitlement = true,
limitValue,
limit = false,
disabled = false,
children,
icon,
className,
href,
...buttonProps
}: ActionButtonProps) {
const hasReachedLimit =
typeof limit === "number" &&
limitValue !== undefined &&
limitValue >= limit;
const isDisabled =
disabled || !hasAccess || !hasEntitlement || hasReachedLimit;
const getMessage = () => {
if (!hasAccess) return BUTTON_STATE_MESSAGES.noAccess;
if (!hasEntitlement) return BUTTON_STATE_MESSAGES.entitlement;
if (
hasReachedLimit &&
typeof limit === "number" &&
limitValue !== undefined
) {
return BUTTON_STATE_MESSAGES.limitReached(limitValue, limit);
}
return null;
};
const message = getMessage();
const btnContent = (
<ButtonContent
icon={icon}
isDisabled={isDisabled}
loading={loading}
hasAccess={hasAccess}
hasEntitlement={hasEntitlement}
hasReachedLimit={hasReachedLimit}
className={className}
buttonProps={buttonProps}
href={href}
>
{children}
</ButtonContent>
);
if (isDisabled && message) {
return (
<HoverCard openDelay={200}>
<HoverCardTrigger asChild>
<span>{btnContent}</span>
</HoverCardTrigger>
<HoverCardPortal>
<HoverCardContent className="w-80 text-sm">
{message}
</HoverCardContent>
</HoverCardPortal>
</HoverCard>
);
}
return btnContent;
}
function ButtonContent({
icon,
isDisabled,
loading,
hasAccess,
hasEntitlement,
hasReachedLimit,
className,
buttonProps,
children,
href,
}: {
icon?: React.ReactNode;
isDisabled: boolean;
loading: boolean;
hasAccess: boolean;
hasEntitlement: boolean;
hasReachedLimit: boolean;
className?: string;
buttonProps: Omit<ButtonProps, "disabled" | "loading" | "className">;
children: React.ReactNode;
href?: string;
}) {
const content = (
<>
{!hasAccess ? (
<Lock className="mr-1 h-4 w-4" />
) : !hasEntitlement ? (
<AlertCircle className="mr-1 h-4 w-4" />
) : hasReachedLimit ? (
<Sparkle className="mr-1 h-4 w-4" />
) : icon ? (
<div className="mr-1">{icon}</div>
) : null}
{children}
</>
);
const renderLink = href && !isDisabled;
return (
<Button
disabled={isDisabled}
loading={loading}
className={className}
{...buttonProps}
asChild={renderLink ? true : undefined}
>
{renderLink ? <Link href={href}>{content}</Link> : content}
</Button>
);
}
@@ -16,7 +16,6 @@ import React from "react";
import { api } from "@/src/utils/api";
import { showSuccessToast } from "@/src/features/notifications/showSuccessToast";
import { useHasProjectAccess } from "@/src/features/rbac/utils/checkProjectAccess";
import { useHasOrgEntitlement } from "@/src/features/entitlements/hooks";
export type BatchExportTableButtonProps = {
projectId: string;
@@ -41,7 +40,6 @@ export const BatchExportTableButton: React.FC<BatchExportTableButtonProps> = (
});
},
});
const entitled = useHasOrgEntitlement("batch-export");
const hasAccess = useHasProjectAccess({
projectId: props.projectId,
scope: "batchExport:create",
@@ -61,7 +59,7 @@ export const BatchExportTableButton: React.FC<BatchExportTableButtonProps> = (
});
};
if (!entitled || !hasAccess) return null;
if (!hasAccess) return null;
return (
<DropdownMenu>
+24 -11
View File
@@ -19,9 +19,10 @@ import {
import { ArrowUp } from "lucide-react";
import { api } from "@/src/utils/api";
import { Button } from "@/src/components/ui/button";
import { useIsEeEnabled } from "@/src/ee/utils/useIsEeEnabled";
import { env } from "@/src/env.mjs";
import { cn } from "@/src/utils/tailwind";
import { usePlan } from "@/src/features/entitlements/hooks";
import { isSelfHostedPlan, planLabels } from "@langfuse/shared";
export const VersionLabel = ({ className }: { className?: string }) => {
const checkUpdate = api.public.checkUpdate.useQuery(undefined, {
@@ -31,9 +32,24 @@ export const VersionLabel = ({ className }: { className?: string }) => {
enabled: !env.NEXT_PUBLIC_LANGFUSE_CLOUD_REGION, // do not check for updates on Langfuse Cloud
onError: (error) => console.error("checkUpdate error", error), // do not render default error message
});
const isEeVersion = useIsEeEnabled();
const plan = usePlan();
const isLangfuseCloud = Boolean(env.NEXT_PUBLIC_LANGFUSE_CLOUD_REGION);
const selfHostedPlanLabel = !isLangfuseCloud
? plan && isSelfHostedPlan(plan)
? // self-host plan
{
short: plan === "self-hosted:pro" ? "Pro" : "EE",
long: planLabels[plan],
}
: // no plan, oss
{
short: "OSS",
long: "Open Source",
}
: // null on cloud
null;
const hasUpdate =
!env.NEXT_PUBLIC_LANGFUSE_CLOUD_REGION &&
checkUpdate.data &&
@@ -49,9 +65,9 @@ export const VersionLabel = ({ className }: { className?: string }) => {
return (
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="ghost" size="xs" className={cn("text-xs",className)}>
<Button variant="ghost" size="xs" className={cn("text-xs", className)}>
{VERSION}
{!isLangfuseCloud && (isEeVersion ? " EE" : " OSS")}
{selfHostedPlanLabel ? ` ${selfHostedPlanLabel.short}` : null}
{hasUpdate && <ArrowUp className={`ml-1 h-3 w-3 ${color}`} />}
</Button>
</DropdownMenuTrigger>
@@ -70,11 +86,11 @@ export const VersionLabel = ({ className }: { className?: string }) => {
<DropdownMenuSeparator />
</>
) : null}
{isEeVersion && (
{selfHostedPlanLabel && (
<>
<DropdownMenuLabel className="flex items-center font-normal">
<BadgeCheck size={16} className="mr-2" />
Enterprise Edition
{selfHostedPlanLabel.long}
</DropdownMenuLabel>
<DropdownMenuSeparator />
</>
@@ -100,12 +116,9 @@ export const VersionLabel = ({ className }: { className?: string }) => {
Roadmap
</Link>
</DropdownMenuItem>
{!isLangfuseCloud && !isEeVersion && (
{!isLangfuseCloud && (
<DropdownMenuItem asChild>
<Link
href="https://langfuse.com/docs/deployment/feature-overview"
target="_blank"
>
<Link href="https://langfuse.com/pricing-self-host" target="_blank">
<Info size={16} className="mr-2" />
Compare Versions
</Link>
+1 -1
View File
@@ -33,7 +33,7 @@ export function AppSidebar({
return (
<Sidebar collapsible="icon" variant="sidebar" {...props}>
<SidebarHeader>
<div className="flex items-center gap-2 p-2">
<div className="flex items-center gap-2 p-2 pr-0">
<LangfuseLogo version />
</div>
<DemoBadge />
+3
View File
@@ -90,6 +90,7 @@ export type DatePickerWithRangeProps = {
dateRange?: DashboardDateRange;
className?: string;
selectedOption: DashboardDateRangeOptions;
disabled?: React.ComponentProps<typeof Calendar>["disabled"];
setDateRangeAndOption: (
option: DashboardDateRangeOptions,
date?: DashboardDateRange,
@@ -101,6 +102,7 @@ export function DatePickerWithRange({
dateRange,
selectedOption,
setDateRangeAndOption,
disabled,
}: DatePickerWithRangeProps) {
const [internalDateRange, setInternalDateRange] = useState<
DateRange | undefined
@@ -215,6 +217,7 @@ export function DatePickerWithRange({
selected={internalDateRange}
onSelect={onCalendarSelection}
numberOfMonths={isSmallScreen ? 1 : 2}
disabled={disabled}
/>
{!isSmallScreen && (
<div className="flex flex-row border-t-2 py-1.5">
+60 -5
View File
@@ -17,18 +17,30 @@ import {
type DashboardDateRange,
TABLE_AGGREGATION_OPTIONS,
getDateFromOption,
isTableDataRangeOptionAvailable,
isDashboardDateRangeOptionAvailable,
} from "@/src/utils/date-range-utils";
import { Clock } from "lucide-react";
import { useEntitlementLimit } from "@/src/features/entitlements/hooks";
import { useMemo } from "react";
import {
HoverCard,
HoverCardContent,
HoverCardTrigger,
} from "@/src/components/ui/hover-card";
import { HoverCardPortal } from "@radix-ui/react-hover-card";
type BaseDateRangeDropdownProps<T> = {
selectedOption: T;
options: readonly T[];
limitedOptions?: readonly T[];
onSelectionChange: (value: T) => void;
};
const BaseDateRangeDropdown = <T extends string>({
selectedOption,
options,
limitedOptions,
onSelectionChange,
}: BaseDateRangeDropdownProps<T>) => {
return (
@@ -38,11 +50,33 @@ const BaseDateRangeDropdown = <T extends string>({
<SelectValue placeholder="Select" />
</SelectTrigger>
<SelectContent position="popper" defaultValue={60}>
{options.map((item) => (
<SelectItem key={item} value={item}>
{item}
</SelectItem>
))}
{options.map((item) => {
const itemObj = (
<SelectItem
key={item}
value={item}
disabled={limitedOptions?.includes(item)}
>
{item}
</SelectItem>
);
const isLimited = limitedOptions?.includes(item);
return isLimited ? (
<HoverCard openDelay={200}>
<HoverCardTrigger asChild>
<span>{itemObj}</span>
</HoverCardTrigger>
<HoverCardPortal>
<HoverCardContent className="w-60 text-sm" side="right">
This time range is not available in your current plan.
</HoverCardContent>
</HoverCardPortal>
</HoverCard>
) : (
itemObj
);
})}
</SelectContent>
</Select>
);
@@ -59,6 +93,17 @@ type DashboardDateRangeDropdownProps = {
export const DashboardDateRangeDropdown: React.FC<
DashboardDateRangeDropdownProps
> = ({ selectedOption, setDateRangeAndOption }) => {
const lookbackLimit = useEntitlementLimit("data-access-days");
const disabledOptions = useMemo(() => {
return DASHBOARD_AGGREGATION_OPTIONS.filter(
(option) =>
!isDashboardDateRangeOptionAvailable({
option,
limitDays: lookbackLimit,
}),
);
}, [lookbackLimit]);
const onDropDownSelection = (value: DashboardDateRangeOptions) => {
if (value === DASHBOARD_AGGREGATION_PLACEHOLDER) {
setDateRangeAndOption(DASHBOARD_AGGREGATION_PLACEHOLDER, undefined);
@@ -82,6 +127,7 @@ export const DashboardDateRangeDropdown: React.FC<
<BaseDateRangeDropdown
selectedOption={selectedOption}
options={options}
limitedOptions={disabledOptions}
onSelectionChange={onDropDownSelection}
/>
);
@@ -99,6 +145,14 @@ export const TableDateRangeDropdown: React.FC<TableDateRangeDropdownProps> = ({
selectedOption,
setDateRangeAndOption,
}) => {
const lookbackLimit = useEntitlementLimit("data-access-days");
const disabledOptions = useMemo(() => {
return TABLE_AGGREGATION_OPTIONS.filter(
(option) =>
!isTableDataRangeOptionAvailable({ option, limitDays: lookbackLimit }),
);
}, [lookbackLimit]);
const onDropDownSelection = (value: TableDateRangeOptions) => {
const dateFromOption = getDateFromOption({
filterSource: "TABLE",
@@ -116,6 +170,7 @@ export const TableDateRangeDropdown: React.FC<TableDateRangeDropdownProps> = ({
<BaseDateRangeDropdown
selectedOption={selectedOption}
options={TABLE_AGGREGATION_OPTIONS}
limitedOptions={disabledOptions}
onSelectionChange={onDropDownSelection}
/>
);
+25
View File
@@ -11,6 +11,8 @@ import { useHasProjectAccess } from "@/src/features/rbac/utils/checkProjectAcces
import { type ProjectScope } from "@/src/features/rbac/constants/projectAccessRights";
import { api } from "@/src/utils/api";
import { usePostHogClientCapture } from "@/src/features/posthog-analytics/usePostHogClientCapture";
import { Input } from "@/src/components/ui/input";
import { Label } from "@/src/components/ui/label";
interface DeleteButtonProps {
itemId: string;
@@ -20,6 +22,7 @@ interface DeleteButtonProps {
invalidateFunc: () => void;
type: "trace" | "dataset";
redirectUrl?: string;
deleteConfirmation?: string;
}
export function DeleteButton({
@@ -30,10 +33,12 @@ export function DeleteButton({
invalidateFunc,
type,
redirectUrl,
deleteConfirmation,
}: DeleteButtonProps) {
const [isDeleted, setIsDeleted] = useState(false);
const router = useRouter();
const capture = usePostHogClientCapture();
const [deleteConfirmationInput, setDeleteConfirmationInput] = useState("");
const hasAccess = useHasProjectAccess({ projectId, scope: scope });
const traceMutation = api.traces.deleteMany.useMutation({
@@ -79,6 +84,18 @@ export function DeleteButton({
This action cannot be undone and removes all the data associated with
this {type}.
</p>
{deleteConfirmation && (
<div className="mb-4 grid w-full gap-1.5">
<Label htmlFor="delete-confirmation">
Type &quot;{deleteConfirmation}&quot; to confirm
</Label>
<Input
id="delete-confirmation"
value={deleteConfirmationInput}
onChange={(e) => setDeleteConfirmationInput(e.target.value)}
/>
</div>
)}
<div className="flex justify-end space-x-4">
{type === "trace" ? (
<Button
@@ -86,6 +103,10 @@ export function DeleteButton({
variant="destructive"
loading={traceMutation.isLoading || isDeleted}
onClick={() => {
if (deleteConfirmationInput !== deleteConfirmation) {
alert("Please type the correct confirmation");
return;
}
void traceMutation.mutateAsync({
traceIds: [itemId],
projectId,
@@ -103,6 +124,10 @@ export function DeleteButton({
variant="destructive"
loading={datasetMutation.isLoading || isDeleted}
onClick={() => {
if (deleteConfirmationInput !== deleteConfirmation) {
alert("Please type the correct confirmation");
return;
}
void datasetMutation.mutateAsync({
projectId,
datasetId: itemId,
+18 -7
View File
@@ -13,10 +13,15 @@ export const ErrorPage = ({
}: {
title?: string;
message: string;
additionalButton?: {
label: string;
href: string;
};
additionalButton?:
| {
label: string;
href: string;
}
| {
label: string;
onClick: () => void;
};
}) => {
const session = useSession();
const router = useRouter();
@@ -40,9 +45,15 @@ export const ErrorPage = ({
</Button>
) : null}
{additionalButton ? (
<Button variant="secondary" asChild>
<Link href={additionalButton.href}>{additionalButton.label}</Link>
</Button>
"onClick" in additionalButton ? (
<Button variant="secondary" onClick={additionalButton.onClick}>
{additionalButton.label}
</Button>
) : (
<Button variant="secondary" asChild>
<Link href={additionalButton.href}>{additionalButton.label}</Link>
</Button>
)
) : null}
</div>
</div>
+2 -2
View File
@@ -10,7 +10,7 @@ import { Toaster } from "@/src/components/ui/sonner";
import DOMPurify from "dompurify";
import { ThemeToggle } from "@/src/features/theming/ThemeToggle";
import { useQueryProjectOrOrganization } from "@/src/features/projects/hooks";
import { useOrgEntitlements } from "@/src/features/entitlements/hooks";
import { useEntitlements } from "@/src/features/entitlements/hooks";
import { useUiCustomization } from "@/src/ee/features/ui-customization/useUiCustomization";
import { hasOrganizationAccess } from "@/src/features/rbac/utils/checkOrganizationAccess";
import { ClickhouseAdminToggle } from "@/src/components/layouts/ClickhouseAdminToggle";
@@ -105,7 +105,7 @@ export default function Layout(props: PropsWithChildren) {
const enableExperimentalFeatures =
session.data?.environment.enableExperimentalFeatures ?? false;
const entitlements = useOrgEntitlements();
const entitlements = useEntitlements();
const uiCustomization = useUiCustomization();
+23 -5
View File
@@ -37,7 +37,11 @@ export const SessionPage: React.FC<{
},
{
retry(failureCount, error) {
if (error.data?.code === "UNAUTHORIZED") return false;
if (
error.data?.code === "UNAUTHORIZED" ||
error.data?.code === "NOT_FOUND"
)
return false;
return failureCount < 3;
},
},
@@ -68,6 +72,18 @@ export const SessionPage: React.FC<{
if (session.error?.data?.code === "UNAUTHORIZED")
return <ErrorPage message="You do not have access to this session." />;
if (session.error?.data?.code === "NOT_FOUND")
return (
<ErrorPage
title="Session not found"
message="The session is either still being processed or has been deleted."
additionalButton={{
label: "Retry",
onClick: () => void window.location.reload(),
}}
/>
);
return (
<ScrollScreenPage>
<Header
@@ -118,7 +134,7 @@ export const SessionPage: React.FC<{
userId ?? "",
)}`}
>
<Badge>User ID: {userId}</Badge>
<Badge className="max-w-[300px] truncate">User ID: {userId}</Badge>
</Link>
))}
<Badge variant="outline">Traces: {session.data?.traces.length}</Badge>
@@ -134,7 +150,9 @@ export const SessionPage: React.FC<{
className="group grid gap-3 border-border p-2 shadow-none hover:border-ring md:grid-cols-3"
key={trace.id}
>
<SessionIO traceId={trace.id} projectId={projectId} />
<div className="col-span-2 overflow-hidden">
<SessionIO traceId={trace.id} projectId={projectId} />
</div>
<div className="-mt-1 p-1 opacity-50 transition-opacity group-hover:opacity-100">
<Link
href={`/project/${projectId}/traces/${trace.id}`}
@@ -195,11 +213,11 @@ const SessionIO = ({
skipBatch: true,
},
},
refetchOnMount: false, // prevents refetching loops
refetchOnMount: false,
},
);
return (
<div className="col-span-2 flex flex-col gap-2 p-0">
<div className="flex w-full flex-col gap-2 overflow-hidden p-0">
{!trace.data ? (
<JsonSkeleton
className="h-full w-full overflow-hidden px-2 py-1"
@@ -39,7 +39,7 @@ import { zodResolver } from "@hookform/resolvers/zod";
import { useForm } from "react-hook-form";
import { z } from "zod";
import { useSession } from "next-auth/react";
import { useHasOrgEntitlement } from "@/src/features/entitlements/hooks";
import { useHasEntitlement } from "@/src/features/entitlements/hooks";
const addToQueueFormSchema = z.object({
queueId: z.string(),
@@ -71,7 +71,7 @@ export function TraceTableMultiSelectAction({
},
});
const hasEntitlement = useHasOrgEntitlement("annotation-queues");
const hasEntitlement = useHasEntitlement("annotation-queues");
const hasAddToQueueAccess = useHasProjectAccess({
projectId,
scope: "annotationQueues:CUD",
@@ -46,7 +46,7 @@ import { useIndividualScoreColumns } from "@/src/features/scores/hooks/useIndivi
import TagList from "@/src/features/tag/components/TagList";
import useColumnOrder from "@/src/features/column-visibility/hooks/useColumnOrder";
import { BatchExportTableButton } from "@/src/components/BatchExportTableButton";
import { useOrganizationPlan } from "@/src/features/entitlements/hooks";
import { usePlan } from "@/src/features/entitlements/hooks";
import {
DropdownMenu,
DropdownMenuContent,
@@ -104,7 +104,7 @@ export default function GenerationsTable({
omittedFilter = [],
}: GenerationsTableProps) {
const capture = usePostHogClientCapture();
const plan = useOrganizationPlan();
const plan = usePlan();
const [isExporting, setIsExporting] = useState(false);
const [searchQuery, setSearchQuery] = useQueryParam(
"search",
+46 -1
View File
@@ -17,6 +17,7 @@ import {
} from "@/src/components/schemas/ChatMlSchema";
import { useMarkdownContext } from "@/src/features/theming/useMarkdownContext";
import { type MediaReturnType } from "@/src/features/media/validation";
import { LangfuseMediaView } from "@/src/components/ui/LangfuseMediaView";
const isSupportedMarkdownFormat = (
content: unknown,
@@ -67,6 +68,7 @@ export function MarkdownOrJsonView({
className={className}
customCodeHeaderClassName={customCodeHeaderClassName}
audio={audio}
media={media}
/>
) : (
<JSONView
@@ -148,6 +150,14 @@ export const IOPreview: React.FC<{
const isPrettyViewAvailable =
inChatMlArray.success || inMarkdown.success || outMarkdown.success;
// If there are additional input fields beyond the messages, render them
const additionalInput =
typeof input === "object"
? Object.fromEntries(
Object.entries(input as object).filter(([key]) => key !== "messages"),
)
: undefined;
// default I/O
return (
<>
@@ -188,6 +198,12 @@ export const IOPreview: React.FC<{
]),
]}
shouldRenderMarkdown
additionalInput={
Object.keys(additionalInput ?? {}).length > 0
? additionalInput
: undefined
}
media={media ?? []}
/>
) : (
<>
@@ -241,7 +257,15 @@ export const OpenAiMessageView: React.FC<{
messages: z.infer<typeof ChatMlArraySchema>;
title?: string;
shouldRenderMarkdown?: boolean;
}> = ({ title, messages, shouldRenderMarkdown = false }) => {
media?: MediaReturnType[];
additionalInput?: Record<string, unknown>;
}> = ({
title,
messages,
shouldRenderMarkdown = false,
media,
additionalInput,
}) => {
const COLLAPSE_THRESHOLD = 3;
const [isCollapsed, setCollapsed] = useState(
messages.length > COLLAPSE_THRESHOLD ? true : null,
@@ -330,6 +354,27 @@ export const OpenAiMessageView: React.FC<{
</Fragment>
))}
</div>
{additionalInput && (
<div className="p-3 pt-1">
<JSONView title="Additional Input" json={additionalInput} />
</div>
)}
{media && media.length > 0 && (
<>
<div className="mx-3 border-t px-2 py-1 text-xs text-muted-foreground">
Media
</div>
<div className="flex flex-wrap gap-2 p-4 pt-1">
{media.map((m) => (
<LangfuseMediaView
mediaAPIReturnValue={m}
asFileIcon={true}
key={m.mediaId}
/>
))}
</div>
</>
)}
</div>
);
};
@@ -28,7 +28,7 @@ import { CommentDrawerButton } from "@/src/features/comments/CommentDrawerButton
import { cn } from "@/src/utils/tailwind";
import { NewDatasetItemFromTrace } from "@/src/features/datasets/components/NewDatasetItemFromObservationButton";
import { CreateNewAnnotationQueueItem } from "@/src/ee/features/annotation-queues/components/CreateNewAnnotationQueueItem";
import { useHasOrgEntitlement } from "@/src/features/entitlements/hooks";
import { useHasEntitlement } from "@/src/features/entitlements/hooks";
import { calculateDisplayTotalCost } from "@/src/components/trace/lib/helpers";
import { useMemo } from "react";
import { useIsAuthenticatedAndProjectMember } from "@/src/features/auth/hooks";
@@ -65,7 +65,7 @@ export const ObservationPreview = ({
const [emptySelectedConfigIds, setEmptySelectedConfigIds] = useLocalStorage<
string[]
>("emptySelectedConfigIds", []);
const hasEntitlement = useHasOrgEntitlement("annotation-queues");
const hasEntitlement = useHasEntitlement("annotation-queues");
const isAuthenticatedAndProjectMember =
useIsAuthenticatedAndProjectMember(projectId);
+2 -2
View File
@@ -28,7 +28,7 @@ import { api } from "@/src/utils/api";
import { cn } from "@/src/utils/tailwind";
import { NewDatasetItemFromTrace } from "@/src/features/datasets/components/NewDatasetItemFromObservationButton";
import { CreateNewAnnotationQueueItem } from "@/src/ee/features/annotation-queues/components/CreateNewAnnotationQueueItem";
import { useHasOrgEntitlement } from "@/src/features/entitlements/hooks";
import { useHasEntitlement } from "@/src/features/entitlements/hooks";
import { useMemo } from "react";
import { usdFormatter } from "@/src/utils/numbers";
import { calculateDisplayTotalCost } from "@/src/components/trace/lib/helpers";
@@ -61,7 +61,7 @@ export const TracePreview = ({
const [emptySelectedConfigIds, setEmptySelectedConfigIds] = useLocalStorage<
string[]
>("emptySelectedConfigIds", []);
const hasEntitlement = useHasOrgEntitlement("annotation-queues");
const hasEntitlement = useHasEntitlement("annotation-queues");
const isAuthenticatedAndProjectMember = useIsAuthenticatedAndProjectMember(
trace.projectId,
);
@@ -138,7 +138,7 @@ function TreeItemInner({
<PanelRightOpen className="h-4 w-4"></PanelRightOpen>
</Button>
</DrawerTrigger>
<DrawerContent className="w-full overflow-hidden" size="md">
<DrawerContent className="overflow-hidden" size="md">
{children}
</DrawerContent>
</Drawer>
+19 -1
View File
@@ -268,7 +268,11 @@ export function TracePage({
},
{
retry(failureCount, error) {
if (error.data?.code === "UNAUTHORIZED") return false;
if (
error.data?.code === "UNAUTHORIZED" ||
error.data?.code === "NOT_FOUND"
)
return false;
return failureCount < 3;
},
},
@@ -309,7 +313,21 @@ export function TracePage({
if (trace.error?.data?.code === "UNAUTHORIZED")
return <ErrorPage message="You do not have access to this trace." />;
if (trace.error?.data?.code === "NOT_FOUND")
return (
<ErrorPage
title="Trace not found"
message="The trace is either still being processed or has been deleted."
additionalButton={{
label: "Retry",
onClick: () => void window.location.reload(),
}}
/>
);
if (!trace.data) return <div>loading...</div>;
return (
<FullScreenPage>
<Header
+28 -27
View File
@@ -8,7 +8,7 @@ import {
import { ResizableImage } from "@/src/components/ui/resizable-image";
import useProjectIdFromURL from "@/src/hooks/useProjectIdFromURL";
import {
MediaContentType,
type MediaContentType,
type MediaReturnType,
} from "@/src/features/media/validation";
import { FileIcon, ImageIcon, SpeakerLoudIcon } from "@radix-ui/react-icons";
@@ -78,32 +78,22 @@ export const LangfuseMediaView = ({
return <FileViewer src={mediaUrl} contentType={mediaData.type} />;
}
switch (mediaData.type) {
case MediaContentType.JPEG:
case MediaContentType.PNG:
case MediaContentType.WEBP:
return (
<div>
<ResizableImage
src={mediaUrl}
isDefaultVisible={true}
shouldValidateImageSource={false}
/>
</div>
);
case MediaContentType.MP3:
case MediaContentType.MP3_LEGACY:
case MediaContentType.WAV:
return <AudioPlayer src={mediaUrl} />;
case MediaContentType.PDF:
return <FileViewer src={mediaUrl} contentType={mediaData.type} />;
case MediaContentType.TXT:
return <FileViewer src={mediaUrl} contentType={mediaData.type} />;
default:
return null;
if (mediaData.type.startsWith("image")) {
return (
<div>
<ResizableImage
src={mediaUrl}
isDefaultVisible={true}
shouldValidateImageSource={false}
/>
</div>
);
} else if (mediaData.type.startsWith("audio")) {
return <AudioPlayer src={mediaUrl} />;
} else if (mediaData.type.startsWith("video")) {
return <VideoPlayer src={mediaUrl} />;
} else {
return <FileViewer src={mediaUrl} contentType={mediaData.type} />;
}
};
@@ -161,3 +151,14 @@ function AudioPlayer({ src }: { src?: string }) {
</audio>
);
}
function VideoPlayer({ src }: { src?: string }) {
if (!src) return null;
return (
<video controls className="w-full" preload="metadata">
<source src={src} />
Your browser does not support the video element.
</video>
);
}
+19
View File
@@ -31,6 +31,7 @@ import {
import { type z } from "zod";
import { ResizableImage } from "@/src/components/ui/resizable-image";
import { LangfuseMediaView } from "@/src/components/ui/LangfuseMediaView";
import { type MediaReturnType } from "@/src/features/media/validation";
type ReactMarkdownNode = ReactMarkdownExtraProps["node"];
type ReactMarkdownNodeChildren = Exclude<
@@ -233,12 +234,14 @@ export function MarkdownView({
className,
customCodeHeaderClassName,
audio,
media,
}: {
markdown: string | z.infer<typeof OpenAIContentSchema>;
title?: string;
className?: string;
customCodeHeaderClassName?: string;
audio?: OpenAIOutputAudioType;
media?: MediaReturnType[];
}) {
const [isCopied, setIsCopied] = useState(false);
const capture = usePostHogClientCapture();
@@ -364,6 +367,22 @@ export function MarkdownView({
</>
) : null}
</div>
{media && media.length > 0 && (
<>
<div className="mx-3 border-t px-2 py-1 text-xs text-muted-foreground">
Media
</div>
<div className="flex flex-wrap gap-2 p-4 pt-1">
{media.map((m) => (
<LangfuseMediaView
mediaAPIReturnValue={m}
asFileIcon={true}
key={m.mediaId}
/>
))}
</div>
</>
)}
</div>
);
}
+2
View File
@@ -13,12 +13,14 @@ function Calendar({
className,
classNames,
showOutsideDays = true,
disabled,
...props
}: CalendarProps) {
return (
<DayPicker
showOutsideDays={showOutsideDays}
className={cn("p-3", className)}
disabled={disabled}
classNames={{
months: "flex flex-col sm:flex-row space-y-4 sm:space-x-4 sm:space-y-0",
month: "space-y-4",
+1 -1
View File
@@ -1 +1 @@
export const VERSION = "v2.92.0";
export const VERSION = "v3.0.0-rc.3";
@@ -3,7 +3,7 @@ import Header from "@/src/components/layouts/header";
import { Tabs, TabsList, TabsTrigger } from "@/src/components/ui/tabs";
import useSessionStorage from "@/src/components/useSessionStorage";
import { SupportOrUpgradePage } from "@/src/ee/features/billing/components/SupportOrUpgradePage";
import { useHasOrgEntitlement } from "@/src/features/entitlements/hooks";
import { useHasEntitlement } from "@/src/features/entitlements/hooks";
import { useHasProjectAccess } from "@/src/features/rbac/utils/checkProjectAccess";
import { AnnotationQueueItemPage } from "@/src/ee/features/annotation-queues/components/AnnotationQueueItemPage";
import { api } from "@/src/utils/api";
@@ -22,7 +22,7 @@ export const AnnotationQueuesItem = ({
projectId,
scope: "annotationQueues:read",
});
const hasEntitlement = useHasOrgEntitlement("annotation-queues");
const hasEntitlement = useHasEntitlement("annotation-queues");
const queue = api.annotationQueues.byId.useQuery(
{
@@ -19,7 +19,7 @@ import {
import { Input } from "@/src/components/ui/input";
import { useHasProjectAccess } from "@/src/features/rbac/utils/checkProjectAccess";
import { zodResolver } from "@hookform/resolvers/zod";
import { CircleAlert, Edit, LockIcon, PlusIcon } from "lucide-react";
import { Edit, PlusIcon } from "lucide-react";
import { useForm } from "react-hook-form";
import { Form } from "@/src/components/ui/form";
import { Textarea } from "@/src/components/ui/textarea";
@@ -34,12 +34,11 @@ import { MultiSelectKeyValues } from "@/src/features/scores/components/multi-sel
import { CommandItem } from "@/src/components/ui/command";
import { useRouter } from "next/router";
import { usePostHogClientCapture } from "@/src/features/posthog-analytics/usePostHogClientCapture";
import { useOrganizationPlan } from "@/src/features/entitlements/hooks";
import {
HoverCard,
HoverCardContent,
HoverCardTrigger,
} from "@/src/components/ui/hover-card";
useHasEntitlement,
useEntitlementLimit,
} from "@/src/features/entitlements/hooks";
import { ActionButton } from "@/src/components/ActionButton";
export const CreateOrEditAnnotationQueueButton = ({
projectId,
@@ -55,8 +54,9 @@ export const CreateOrEditAnnotationQueueButton = ({
projectId: projectId,
scope: "annotationQueues:CUD",
});
const hasEntitlement = useHasEntitlement("annotation-queues");
const queueLimit = useEntitlementLimit("annotation-queue-count");
const router = useRouter();
const plan = useOrganizationPlan();
const capture = usePostHogClientCapture();
const queueQuery = api.annotationQueues.byId.useQuery(
@@ -119,55 +119,6 @@ export const CreateOrEditAnnotationQueueButton = ({
const configs = configsData.data?.configs ?? [];
if (!hasAccess) {
return (
<Button variant={variant} disabled={true} className="justify-start">
<LockIcon className="-ml-0.5 mr-1.5 h-4 w-4" aria-hidden="true" />
<span className="text-sm">{queueId ? "Edit" : "New queue"}</span>
</Button>
);
}
if (queueCountData.isLoading) return null;
// gate cloud hobby usage of annotation queue
if (plan === "cloud:hobby" && !!queueCountData.data && !queueId) {
return (
<HoverCard>
<HoverCardTrigger asChild>
<Button
variant={variant}
className="relative grid grid-flow-row items-start justify-start overflow-hidden py-0 disabled:cursor-default"
disabled
>
<div className="mt-2 flex h-6 flex-row items-center justify-center">
<PlusIcon className="mr-1.5 h-4 w-4" aria-hidden="true" />
<span className="text-sm">New queue</span>
</div>
<div className="absolute top-0 flex h-3 w-full items-center justify-center bg-primary-accent">
<CircleAlert
className="mr-1 h-2 w-2 text-white"
aria-hidden="true"
/>
<span className="text-xs text-white">At usage limit</span>
</div>
</Button>
</HoverCardTrigger>
<HoverCardContent className="w-80" align="start" side="right">
<div className="flex justify-between space-x-4">
<div className="space-y-1">
<h4 className="text-sm font-semibold">Usage Limit Reached</h4>
<p className="text-xs">
You have reached the maximum number of annotation queues allowed
on the Hobby plan. Upgrade your plan to create more queues.
</p>
</div>
</div>
</HoverCardContent>
</HoverCard>
);
}
const onSubmit = (data: CreateQueue) => {
if (queueId) {
editQueueMutation.mutateAsync({
@@ -202,20 +153,26 @@ export const CreateOrEditAnnotationQueueButton = ({
return (
<Dialog open={isOpen} onOpenChange={setIsOpen}>
<DialogTrigger asChild>
<Button
<ActionButton
variant={variant}
onClick={() => setIsOpen(true)}
className="justify-start"
icon={
queueId ? (
<Edit className="h-4 w-4" aria-hidden="true" />
) : (
<PlusIcon className="h-4 w-4" aria-hidden="true" />
)
}
hasAccess={hasAccess}
hasEntitlement={hasEntitlement}
limitValue={queueCountData.data}
limit={queueLimit}
>
{queueId ? (
<Edit className="-ml-0.5 mr-1.5 h-4 w-4" aria-hidden="true" />
) : (
<PlusIcon className="-ml-0.5 mr-1.5 h-4 w-4" aria-hidden="true" />
)}
<span className="ml-1 text-sm font-normal">
{queueId ? "Edit" : "New queue"}
</span>
</Button>
</ActionButton>
</DialogTrigger>
{configsData.data && (
<DialogContent className="max-h-[90vh] overflow-y-auto">
@@ -12,7 +12,7 @@ import { getScoreDataTypeIcon } from "@/src/features/scores/components/ScoreDeta
import Link from "next/link";
import { CreateOrEditAnnotationQueueButton } from "@/src/ee/features/annotation-queues/components/CreateOrEditAnnotationQueueButton";
import { useHasProjectAccess } from "@/src/features/rbac/utils/checkProjectAccess";
import { useHasOrgEntitlement } from "@/src/features/entitlements/hooks";
import { useHasEntitlement } from "@/src/features/entitlements/hooks";
import { SupportOrUpgradePage } from "@/src/ee/features/billing/components/SupportOrUpgradePage";
import { Skeleton } from "@/src/components/ui/skeleton";
import { TableWithMetadataWrapper } from "@/src/components/table/TableWithMetadataWrapper";
@@ -35,7 +35,7 @@ export default function QueueItems() {
projectId,
scope: "annotationQueues:CUD",
});
const hasEntitlement = useHasOrgEntitlement("annotation-queues");
const hasEntitlement = useHasEntitlement("annotation-queues");
if (!hasReadAccess || !hasEntitlement) return <SupportOrUpgradePage />;
return (
@@ -3,7 +3,7 @@ import { useRouter } from "next/router";
import { FullScreenPage } from "@/src/components/layouts/full-screen-page";
import { AnnotationQueuesTable } from "@/src/ee/features/annotation-queues/components/AnnotationQueuesTable";
import { useHasProjectAccess } from "@/src/features/rbac/utils/checkProjectAccess";
import { useHasOrgEntitlement } from "@/src/features/entitlements/hooks";
import { useHasEntitlement } from "@/src/features/entitlements/hooks";
import { SupportOrUpgradePage } from "@/src/ee/features/billing/components/SupportOrUpgradePage";
export default function AnnotationQueues() {
@@ -13,7 +13,7 @@ export default function AnnotationQueues() {
projectId: projectId,
scope: "annotationQueues:read",
});
const hasEntitlement = useHasOrgEntitlement("annotation-queues");
const hasEntitlement = useHasEntitlement("annotation-queues");
if (!hasAccess || !hasEntitlement) return <SupportOrUpgradePage />;
return (
@@ -4,20 +4,12 @@ import { Button } from "@/src/components/ui/button";
import { api } from "@/src/utils/api";
import { Flex, MarkerBar, Metric, Text } from "@tremor/react";
import Link from "next/link";
import {
Dialog,
DialogContent,
DialogHeader,
DialogTrigger,
} from "@/src/components/ui/dialog";
import Header from "@/src/components/layouts/header";
import { usePostHogClientCapture } from "@/src/features/posthog-analytics/usePostHogClientCapture";
import { useQueryOrganization } from "@/src/features/organizations/hooks";
import { Card } from "@/src/components/ui/card";
import { numberFormatter, compactNumberFormatter } from "@/src/utils/numbers";
import { useHasOrgEntitlement } from "@/src/features/entitlements/hooks";
import { useHasEntitlement } from "@/src/features/entitlements/hooks";
import { type Plan, planLabels } from "@langfuse/shared";
import { stripeProducts } from "@/src/ee/features/billing/utils/stripeProducts";
import { useRouter } from "next/router";
import {
chatAvailable,
@@ -26,7 +18,15 @@ import {
import { env } from "@/src/env.mjs";
import { useHasOrganizationAccess } from "@/src/features/rbac/utils/checkOrganizationAccess";
import { Alert, AlertDescription, AlertTitle } from "@/src/components/ui/alert";
import { MAX_OBSERVATIONS_FREE_PLAN } from "@/src/ee/features/billing/constants";
import { MAX_EVENTS_FREE_PLAN } from "@/src/ee/features/billing/constants";
import {
Dialog,
DialogContent,
DialogHeader,
DialogTrigger,
} from "@/src/components/ui/dialog";
import { usePostHogClientCapture } from "@/src/features/posthog-analytics/usePostHogClientCapture";
import { stripeProducts } from "@/src/ee/features/billing/utils/stripeProducts";
export const BillingSettings = () => {
const router = useRouter();
@@ -36,7 +36,7 @@ export const BillingSettings = () => {
scope: "langfuseCloudBilling:CRUD",
});
const entitled = useHasOrgEntitlement("cloud-billing");
const entitled = useHasEntitlement("cloud-billing");
if (!entitled) return null;
if (!hasAccess)
@@ -72,11 +72,14 @@ const OrganizationUsageChart = () => {
},
},
);
const planLimit =
organization?.cloudConfig?.monthlyObservationLimit ??
MAX_OBSERVATIONS_FREE_PLAN;
const hobbyPlanLimit =
organization?.cloudConfig?.monthlyObservationLimit ?? MAX_EVENTS_FREE_PLAN;
const plan: Plan = organization?.plan ?? "cloud:hobby";
const planLabel = planLabels[plan];
const usageType = usage.data?.usageType
? usage.data.usageType.charAt(0).toUpperCase() +
usage.data.usageType.slice(1)
: "Events";
return (
<div>
@@ -85,19 +88,21 @@ const OrganizationUsageChart = () => {
<>
<Text>
{usage.data.billingPeriod
? `Observations in current billing period`
: "Observations / last 30d"}
? `${usageType} in current billing period`
: `${usageType} / last 30d`}
</Text>
<Metric>{numberFormatter(usage.data.countObservations, 0)}</Metric>
<Metric>{numberFormatter(usage.data.usageCount, 0)}</Metric>
{plan === "cloud:hobby" && (
<>
<Flex className="mt-4">
<Text>{`${numberFormatter((usage.data.countObservations / planLimit) * 100)}%`}</Text>
<Text>Plan limit: {compactNumberFormatter(planLimit)}</Text>
<Text>{`${numberFormatter((usage.data.usageCount / hobbyPlanLimit) * 100)}%`}</Text>
<Text>
Plan limit: {compactNumberFormatter(hobbyPlanLimit)}
</Text>
</Flex>
<MarkerBar
value={Math.min(
(usage.data.countObservations / planLimit) * 100,
(usage.data.usageCount / hobbyPlanLimit) * 100,
100,
)}
className="mt-3"
@@ -138,35 +143,35 @@ const OrganizationUsageChart = () => {
const BillingPortalOrPricingPageButton = () => {
const organization = useQueryOrganization();
const router = useRouter();
const capture = usePostHogClientCapture();
const billingPortalUrl = api.cloudBilling.getStripeCustomerPortalUrl.useQuery(
{
orgId: organization?.id as string,
},
{
enabled: organization !== undefined,
refetchOnMount: false,
refetchOnReconnect: false,
refetchOnWindowFocus: false,
},
);
if (billingPortalUrl.isLoading) return null;
if (!billingPortalUrl.data) return <PricingPageButton />;
return (
<Button asChild>
<Link href={billingPortalUrl.data}>Billing portal</Link>
</Button>
);
};
const PricingPageButton = () => {
const capture = usePostHogClientCapture();
const organization = useQueryOrganization();
const router = useRouter();
const mutCreateCheckoutSession =
api.cloudBilling.createStripeCheckoutSession.useMutation({
onSuccess: (url) => {
router.push(url);
},
});
if (!organization) return null;
if (billingPortalUrl.isLoading) return null;
if (billingPortalUrl.data)
return (
<Button asChild>
<Link href={billingPortalUrl.data}>Billing portal</Link>
</Button>
);
// Do not show checkout or customer portal if manual plan is set in cloud config
if (organization.cloudConfig?.plan) {
@@ -186,6 +191,7 @@ const PricingPageButton = () => {
else return null;
}
// Show pricing page button
return (
<Dialog
onOpenChange={(open) => {
@@ -1,11 +1,8 @@
import { api } from "@/src/utils/api";
import { useQueryProjectOrOrganization } from "@/src/features/projects/hooks";
import { useHasOrganizationAccess } from "@/src/features/rbac/utils/checkOrganizationAccess";
import { MAX_OBSERVATIONS_FREE_PLAN } from "@/src/ee/features/billing/constants";
import {
useHasOrgEntitlement,
useOrganizationPlan,
} from "@/src/features/entitlements/hooks";
import { MAX_EVENTS_FREE_PLAN } from "@/src/ee/features/billing/constants";
import { useHasEntitlement, usePlan } from "@/src/features/entitlements/hooks";
import {
Card,
CardContent,
@@ -18,8 +15,8 @@ import Link from "next/link";
export const UsageTracker = () => {
const { organization } = useQueryProjectOrOrganization();
const hasEntitlement = useHasOrgEntitlement("cloud-billing");
const plan = useOrganizationPlan();
const hasEntitlement = useHasEntitlement("cloud-billing");
const plan = usePlan();
const hasAccess = useHasOrganizationAccess({
organizationId: organization?.id,
scope: "langfuseCloudBilling:CRUD",
@@ -48,10 +45,11 @@ export const UsageTracker = () => {
return null;
}
const usage = usageQuery.data.countObservations || 0;
const percentage = (usage / MAX_OBSERVATIONS_FREE_PLAN) * 100;
const usage = usageQuery.data.usageCount || 0;
const usageType = usageQuery.data.usageType;
const percentage = (usage / MAX_EVENTS_FREE_PLAN) * 100;
if (percentage < 80) {
if (percentage < 90) {
return null;
}
@@ -60,7 +58,7 @@ export const UsageTracker = () => {
<CardHeader className="p-4 pb-0">
<CardTitle className="text-sm">Hobby Plan Usage Limit</CardTitle>
<CardDescription>
{`${usage.toLocaleString()} / ${MAX_OBSERVATIONS_FREE_PLAN.toLocaleString()} (${percentage.toFixed(0)}%) observations in last 30 days. Please upgrade your plan to avoid interruptions.`}
{`${usage.toLocaleString()} / ${MAX_EVENTS_FREE_PLAN.toLocaleString()} (${percentage.toFixed(0)}%) ${usageType} in last 30 days. Please upgrade your plan to avoid interruptions.`}
</CardDescription>
</CardHeader>
<CardContent className="p-4 pt-2">
+1 -1
View File
@@ -1 +1 @@
export const MAX_OBSERVATIONS_FREE_PLAN = 50_000;
export const MAX_EVENTS_FREE_PLAN = 50_000;
@@ -12,6 +12,7 @@ import { TRPCError } from "@trpc/server";
import * as z from "zod";
import { throwIfNoOrganizationAccess } from "@/src/features/rbac/utils/checkOrganizationAccess";
import { auditLog } from "@/src/features/audit-logs/auditLog";
import { getObservationCountOfProjectsSinceCreationDate } from "@langfuse/shared/src/server";
export const cloudBillingRouter = createTRPCRouter({
createStripeCheckoutSession: protectedOrganizationProcedure
@@ -221,6 +222,13 @@ export const cloudBillingRouter = createTRPCRouter({
where: {
id: input.orgId,
},
include: {
projects: {
select: {
id: true,
},
},
},
});
if (!organization) {
throw new TRPCError({
@@ -244,6 +252,7 @@ export const cloudBillingRouter = createTRPCRouter({
start: new Date(subscription.current_period_start * 1000),
end: new Date(subscription.current_period_end * 1000),
};
const stripeInvoice = await stripeClient.invoices.retrieveUpcoming({
subscription: parsedOrg.cloudConfig.stripe.activeSubscriptionId,
});
@@ -251,38 +260,78 @@ export const cloudBillingRouter = createTRPCRouter({
usdAmount: stripeInvoice.amount_due / 100,
date: new Date(stripeInvoice.period_end * 1000),
};
const usage = stripeInvoice.lines.data.reduce((acc, line) => {
const usageInvoiceLines = stripeInvoice.lines.data.filter((line) =>
Boolean(line.plan?.meter),
);
const usage = usageInvoiceLines.reduce((acc, line) => {
if (line.quantity) {
return acc + line.quantity;
}
return acc;
}, 0);
// get meter for usage type (events or observations)
const meterId = usageInvoiceLines[0]?.plan?.meter;
const meter = meterId
? await stripeClient.billing.meters.retrieve(meterId)
: undefined;
return {
countObservations: usage,
usageCount: usage,
usageType: meter?.display_name.toLowerCase() ?? "events",
billingPeriod,
upcomingInvoice,
};
}
}
// For non-Stripe subscriptions, we can only get usage from the LangFuse API
// Free plan, usage not tracked on Stripe
const thirtyDaysAgo = new Date();
thirtyDaysAgo.setDate(thirtyDaysAgo.getDate() - 30);
thirtyDaysAgo.setHours(0, 0, 0, 0);
const projectIds = organization.projects.map((p) => p.id);
const usage = await ctx.prisma.observation.count({
where: {
project: {
orgId: input.orgId,
},
startTime: {
gte: thirtyDaysAgo,
},
},
});
const countObservations =
await getObservationCountOfProjectsSinceCreationDate({
projectIds,
start: thirtyDaysAgo,
});
// const usageArr = await Promise.all([
// ctx.prisma.observation.count({
// where: {
// project: {
// orgId: input.orgId,
// },
// createdAt: {
// gte: thirtyDaysAgo,
// },
// },
// }),
// ctx.prisma.trace.count({
// where: {
// project: {
// orgId: input.orgId,
// },
// createdAt: {
// gte: thirtyDaysAgo,
// },
// },
// }),
// ctx.prisma.score.count({
// where: {
// project: {
// orgId: input.orgId,
// },
// createdAt: {
// gte: thirtyDaysAgo,
// },
// },
// }),
// ]);
return {
countObservations: usage,
usageCount: countObservations,
usageType: "observations",
};
}),
});
@@ -104,6 +104,7 @@ async function handleSubscriptionChanged(
});
if (!checkoutSessionsResponse || checkoutSessionsResponse.data.length !== 1) {
logger.error("[Stripe Webhook] No checkout session found");
traceException("[Stripe Webhook] No checkout session found");
return;
}
const checkoutSession = checkoutSessionsResponse.data[0];
@@ -112,6 +113,7 @@ async function handleSubscriptionChanged(
const clientReference = checkoutSession.client_reference_id;
if (!clientReference) {
logger.error("[Stripe Webhook] No client reference");
traceException("[Stripe Webhook] No client reference");
return NextResponse.json(
{ message: "No client reference" },
{ status: 400 },
@@ -133,6 +135,7 @@ async function handleSubscriptionChanged(
});
if (!organization) {
logger.error("[Stripe Webhook] Organization not found");
traceException("[Stripe Webhook] Organization not found");
return;
}
const parsedOrg = parseDbOrg(organization);
@@ -140,14 +143,15 @@ async function handleSubscriptionChanged(
// assert that no other stripe customer id is already set on the org
const customerId = subscription.customer;
if (!customerId || typeof customerId !== "string") {
logger.error("[Stripe Webhook] Product ID not found");
traceException("[Stripe Webhook] Product ID not found");
logger.error("[Stripe Webhook] Customer ID not found");
traceException("[Stripe Webhook] Customer ID not found");
return;
}
if (
parsedOrg.cloudConfig?.stripe?.customerId &&
parsedOrg.cloudConfig?.stripe?.customerId !== customerId
) {
logger.error("[Stripe Webhook] Another customer id already set on org");
traceException("[Stripe Webhook] Another customer id already set on org");
return;
}
@@ -159,6 +163,9 @@ async function handleSubscriptionChanged(
logger.error(
"[Stripe Webhook] Subscription items not found or more than one",
);
traceException(
"[Stripe Webhook] Subscription items not found or more than one",
);
return;
}
@@ -180,6 +187,9 @@ async function handleSubscriptionChanged(
traceException(
"[Stripe Webhook] Another active product id already set on (one of the) org with this active subscription id",
);
logger.error(
"[Stripe Webhook] Another active product id already set on (one of the) org with this active subscription id",
);
return;
}
+4 -3
View File
@@ -1,6 +1,7 @@
import { env } from "@/src/env.mjs";
import Stripe from "stripe";
export const stripeClient = env.STRIPE_SECRET_KEY
? new Stripe(env.STRIPE_SECRET_KEY)
: undefined;
export const stripeClient =
env.STRIPE_SECRET_KEY && env.NEXT_PUBLIC_LANGFUSE_CLOUD_REGION
? new Stripe(env.STRIPE_SECRET_KEY)
: undefined;
@@ -24,7 +24,7 @@ export const stripeProducts: StripeProduct[] = [
checkout: {
title: "Pro",
description:
"For serious projects. Includes access to full history and higher usage.",
"For serious projects. Includes access to full history, higher usage and access to features.",
price: "$59 / month + $10/100k observations",
},
},
+19 -17
View File
@@ -1,18 +1,26 @@
import Header from "@/src/components/layouts/header";
import { useRouter } from "next/router";
import { Button } from "@/src/components/ui/button";
import Link from "next/link";
import { useHasProjectAccess } from "@/src/features/rbac/utils/checkProjectAccess";
import { Lock, Plus } from "lucide-react";
import { Plus } from "lucide-react";
import EvaluatorTable from "@/src/ee/features/evals/components/evaluator-table";
import { usePostHogClientCapture } from "@/src/features/posthog-analytics/usePostHogClientCapture";
import { FullScreenPage } from "@/src/components/layouts/full-screen-page";
import { Tabs, TabsList, TabsTrigger } from "@/src/components/ui/tabs";
import { ActionButton } from "@/src/components/ActionButton";
import { api } from "@/src/utils/api";
import { useEntitlementLimit } from "@/src/features/entitlements/hooks";
export default function EvaluatorsPage() {
const router = useRouter();
const projectId = router.query.projectId as string;
const capture = usePostHogClientCapture();
const evaluatorCount = api.evals.allConfigs.useQuery({
projectId,
}).data?.totalCount;
const evaluatorLimit = useEntitlementLimit(
"model-based-evaluations-count-evaluators",
);
const hasWriteAccess = useHasProjectAccess({
projectId,
scope: "evalJob:CUD",
@@ -37,23 +45,17 @@ export default function EvaluatorsPage() {
href: "https://langfuse.com/docs/scores/model-based-evals",
}}
actionButtons={
<Button
disabled={!hasWriteAccess}
onClick={() => capture("eval_config:new_form_open")}
asChild
<ActionButton
hasAccess={hasWriteAccess}
icon={<Plus className="h-4 w-4" />}
variant="secondary"
onClick={() => capture("eval_config:new_form_open")}
href={`/project/${projectId}/evals/new`}
limitValue={evaluatorCount ?? 0}
limit={evaluatorLimit}
>
<Link
href={hasWriteAccess ? `/project/${projectId}/evals/new` : "#"}
>
{hasWriteAccess ? (
<Plus className="mr-2 h-4 w-4" />
) : (
<Lock className="mr-2 h-4 w-4" />
)}
New evaluator
</Link>
</Button>
New evaluator
</ActionButton>
}
/>
<EvaluatorTable
@@ -1,4 +1,3 @@
import { env } from "@/src/env.mjs";
import { randomUUID } from "crypto";
import {
QueueJobs,
@@ -17,7 +16,7 @@ export const addDatasetRunItemsToEvalQueue = async ({
traceId: string;
observationId?: string;
}) => {
if (redis && env.NEXT_PUBLIC_LANGFUSE_CLOUD_REGION) {
if (redis) {
const queue = DatasetRunItemUpsertQueue.getInstance();
if (queue) {
+2 -3
View File
@@ -1,5 +1,4 @@
import { z } from "zod";
import {
createTRPCRouter,
protectedProjectProcedure,
@@ -12,6 +11,7 @@ import {
singleFilter,
variableMapping,
ChatMessageRole,
paginationZod,
type JobConfiguration,
JobConfigState,
JobType,
@@ -111,8 +111,7 @@ export const evalRouter = createTRPCRouter({
.input(
z.object({
projectId: z.string(),
limit: z.number(),
page: z.number(),
...paginationZod,
}),
)
.query(async ({ input, ctx }) => {

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