Compare commits

...
176 Commits
Author SHA1 Message Date
Nimar 753a716724 chore: release v3.106.0
Codespell / Check for spelling errors (push) Waiting to run
CI/CD / pre-job (push) Waiting to run
CI/CD / lint (push) Blocked by required conditions
CI/CD / prettier-check (push) Blocked by required conditions
CI/CD / test-docker-build (push) Blocked by required conditions
CI/CD / tests-web-sync (node24, pg12) (push) Blocked by required conditions
CI/CD / tests-web-sync (node24, pg15) (push) Blocked by required conditions
CI/CD / tests-web-async (node24, pg12, mode) (push) Blocked by required conditions
CI/CD / tests-web-async (node24, pg15, mode) (push) Blocked by required conditions
CI/CD / tests-web-async (node24, pg12, mode-azure) (push) Blocked by required conditions
CI/CD / tests-web-async (node24, pg15, mode-azure) (push) Blocked by required conditions
CI/CD / tests-web-async (node24, pg12, mode-redis-cluster) (push) Blocked by required conditions
CI/CD / tests-web-async (node24, pg15, mode-redis-cluster) (push) Blocked by required conditions
CI/CD / tests-worker (node24, pg12, mode) (push) Blocked by required conditions
CI/CD / tests-worker (node24, pg15, mode) (push) Blocked by required conditions
CI/CD / tests-worker (node24, pg12, mode-azure) (push) Blocked by required conditions
CI/CD / tests-worker (node24, pg15, mode-azure) (push) Blocked by required conditions
CI/CD / tests-worker (node24, pg12, mode-redis-cluster) (push) Blocked by required conditions
CI/CD / tests-worker (node24, pg15, mode-redis-cluster) (push) Blocked by required conditions
CI/CD / e2e-tests (push) Blocked by required conditions
CI/CD / e2e-server-tests (push) Blocked by required conditions
CI/CD / all-ci-passed (push) Blocked by required conditions
CI/CD / push-docker-image (push) Blocked by required conditions
release.yml / release (push) Waiting to run
2025-08-28 15:57:53 +02:00
Marc KlingenandGitHub ce7ac9888e fix: cursor background agent dev setup (#8728) 2025-08-28 15:34:12 +02:00
NimarandGitHub 8f1a202226 feat(tracing): graph views for new observation types (#8690)
* feat(trace-graph): more generalized availability for graphs

* update

* simplify

* some cleanup

* simplify

* optimize

* fix typescript

* simplify

* physics again

* simplify

* fix color

* fix parallel node display in langgraph

* slightly optimize

* farben

* restructure

* some optimizations

* optimize

* cleanup

* fix do graph detection

* cleanup

* cleanup

* final

* fiux test
2025-08-28 13:13:50 +00:00
Hassieb Pakzad 3ba29710b2 chore: release v3.105.0
Codespell / Check for spelling errors (push) Waiting to run
CI/CD / pre-job (push) Waiting to run
CI/CD / lint (push) Blocked by required conditions
CI/CD / prettier-check (push) Blocked by required conditions
CI/CD / test-docker-build (push) Blocked by required conditions
CI/CD / tests-web-sync (node24, pg12) (push) Blocked by required conditions
CI/CD / tests-web-sync (node24, pg15) (push) Blocked by required conditions
CI/CD / tests-web-async (node24, pg12, mode) (push) Blocked by required conditions
CI/CD / tests-web-async (node24, pg15, mode) (push) Blocked by required conditions
CI/CD / tests-web-async (node24, pg12, mode-azure) (push) Blocked by required conditions
CI/CD / tests-web-async (node24, pg15, mode-azure) (push) Blocked by required conditions
CI/CD / tests-web-async (node24, pg12, mode-redis-cluster) (push) Blocked by required conditions
CI/CD / tests-web-async (node24, pg15, mode-redis-cluster) (push) Blocked by required conditions
CI/CD / tests-worker (node24, pg12, mode) (push) Blocked by required conditions
CI/CD / tests-worker (node24, pg15, mode) (push) Blocked by required conditions
CI/CD / tests-worker (node24, pg12, mode-azure) (push) Blocked by required conditions
CI/CD / tests-worker (node24, pg15, mode-azure) (push) Blocked by required conditions
CI/CD / tests-worker (node24, pg12, mode-redis-cluster) (push) Blocked by required conditions
CI/CD / tests-worker (node24, pg15, mode-redis-cluster) (push) Blocked by required conditions
CI/CD / e2e-tests (push) Blocked by required conditions
CI/CD / e2e-server-tests (push) Blocked by required conditions
CI/CD / all-ci-passed (push) Blocked by required conditions
CI/CD / push-docker-image (push) Blocked by required conditions
release.yml / release (push) Waiting to run
2025-08-28 09:49:02 +02:00
marliessophieandGitHub dafda00e9d chore(annotation-queues-ui): show Source ID as column on Annotation Queue Items Table (#8779) 2025-08-28 06:01:40 +00:00
marliessophieandGitHub f2bcf2f5cc fix(evalQueue): update timeout error handling to match case sensitivity (#8752) (#8777) 2025-08-27 22:47:15 +00:00
marliessophieandGitHub 1acef7c0e8 chore(peek): navigation event to create browser history entry (#8772) 2025-08-27 17:49:43 +00:00
marliessophieandGitHub e6b4af4004 feat(dataset-runs): support score filters in ui tables (#8755)
* fixup(dataset-runs): support score filters

* chore: remove dataset run level score filters

* fixup: adjust DR query

* chore: add working score filters for dataset metrics

* chore: working version of score filters

* chore: fix tests

* test: add filter tests

* fix: add timestamps to remove test flakyness

* chore: rename score filters

* chore: drop aggregated prefix for score columns

* chore: make trpc changes safe

* chore: add ordering ASC to dataset run metrics

* chore: convert ordering to DESC
2025-08-27 17:18:05 +00:00
marliessophieandGitHub 414d950fa0 chore(datasets): enhance description display in table UI (#8771) 2025-08-27 17:09:34 +00:00
Max DeichmannandGitHub f49a20b489 perf: remove traces env filter form observations table (#8768)
push
2025-08-27 15:17:38 +00:00
Hassieb PakzadandGitHub 3c2e963bfb fix(traces-api): include excluded fields in response with empty values (#8753)
* fix(traces-api): include excluded fields in response with empty values

* fix tests
2025-08-27 12:39:27 +00:00
Marc KlingenandGitHub 8769b65157 chore(cloud): show label that demo is not available only on HIPAA data region (#8764) 2025-08-27 12:23:17 +00:00
Max Deichmann 231a8f82bc chore: release v3.104.0
Codespell / Check for spelling errors (push) Waiting to run
CI/CD / pre-job (push) Waiting to run
CI/CD / lint (push) Blocked by required conditions
CI/CD / prettier-check (push) Blocked by required conditions
CI/CD / test-docker-build (push) Blocked by required conditions
CI/CD / tests-web-sync (node24, pg12) (push) Blocked by required conditions
CI/CD / tests-web-sync (node24, pg15) (push) Blocked by required conditions
CI/CD / tests-web-async (node24, pg12, mode) (push) Blocked by required conditions
CI/CD / tests-web-async (node24, pg15, mode) (push) Blocked by required conditions
CI/CD / tests-web-async (node24, pg12, mode-azure) (push) Blocked by required conditions
CI/CD / tests-web-async (node24, pg15, mode-azure) (push) Blocked by required conditions
CI/CD / tests-web-async (node24, pg12, mode-redis-cluster) (push) Blocked by required conditions
CI/CD / tests-web-async (node24, pg15, mode-redis-cluster) (push) Blocked by required conditions
CI/CD / tests-worker (node24, pg12, mode) (push) Blocked by required conditions
CI/CD / tests-worker (node24, pg15, mode) (push) Blocked by required conditions
CI/CD / tests-worker (node24, pg12, mode-azure) (push) Blocked by required conditions
CI/CD / tests-worker (node24, pg15, mode-azure) (push) Blocked by required conditions
CI/CD / tests-worker (node24, pg12, mode-redis-cluster) (push) Blocked by required conditions
CI/CD / tests-worker (node24, pg15, mode-redis-cluster) (push) Blocked by required conditions
CI/CD / e2e-tests (push) Blocked by required conditions
CI/CD / e2e-server-tests (push) Blocked by required conditions
CI/CD / all-ci-passed (push) Blocked by required conditions
CI/CD / push-docker-image (push) Blocked by required conditions
release.yml / release (push) Waiting to run
2025-08-27 11:58:38 +02:00
Max DeichmannandGitHub a933bf68fb chore: fix log comments (#8760) 2025-08-27 08:45:52 +02:00
Max DeichmannandGitHub 21a16963e2 chore: add log comments ch write (#8758)
push
2025-08-27 06:28:52 +00:00
marliessophieandGitHub fe6c647c2b chore(evals): graceful failure in case of timeouts of LLM providers (#8752) 2025-08-26 21:02:43 +00:00
marliessophieandGitHub f1e5f35659 chore(dataset-run-items): remove PG execution code (#8732)
* chore(dataset-run-items): remove PG execution code

* chore: remove all dri postgres code

* chore: fix return

* chore: remove env variables

* chore: drop unused exports

* remove: env's from test env configs
2025-08-26 15:42:15 +00:00
Max DeichmannandGitHub d39f7ba6e5 chore: upgrade to node 24 (#8734) 2025-08-26 17:06:04 +02:00
NimarandGitHub f701562396 fix: make chain coloring unique (#8749) 2025-08-26 13:57:56 +02:00
marliessophieandGitHub 83cbd336e2 fix(runs-compare): infinite re-renders on data table (#8744) 2025-08-26 13:02:29 +02:00
marliessophieandGitHub ec33992412 test(datasets): skip flaky dataset runs test in CI (#8738) 2025-08-25 19:06:04 +00:00
Marc KlingenandGitHub 4e67edaaff fix: cursor bg agent clock scew (skip check) (#8737)
push
2025-08-25 20:01:02 +02:00
marliessophieandGitHub ac3ee18373 chore(dataset-run-items-seeder): remove PG dataset run item creation path (#8733)
* chore(dataset-run-items-seeder): remove PG dataset run item creation path

* chore: drop import

* chore: increase timeout
2025-08-25 16:42:49 +00:00
Hassieb PakzadandGitHub 7f646757c5 feat(otel): parse vercel ai SDK ai.usage.* (#8730) 2025-08-25 14:53:15 +00:00
marliessophieandGitHub 2d82ae6e86 chore(dataset-runs): improve loading animations on runs table (#8719) 2025-08-25 13:04:46 +00:00
Marlies Mayerhofer 9aa4c11c56 chore: release v3.103.0
Codespell / Check for spelling errors (push) Waiting to run
CI/CD / pre-job (push) Waiting to run
CI/CD / lint (push) Blocked by required conditions
CI/CD / prettier-check (push) Blocked by required conditions
CI/CD / test-docker-build (push) Blocked by required conditions
CI/CD / tests-web-sync (node20, pg12) (push) Blocked by required conditions
CI/CD / tests-web-sync (node20, pg15) (push) Blocked by required conditions
CI/CD / tests-web-async (node20, pg12, mode) (push) Blocked by required conditions
CI/CD / tests-web-async (node20, pg15, mode) (push) Blocked by required conditions
CI/CD / tests-web-async (node20, pg12, mode-azure) (push) Blocked by required conditions
CI/CD / tests-web-async (node20, pg15, mode-azure) (push) Blocked by required conditions
CI/CD / tests-web-async (node20, pg12, mode-redis-cluster) (push) Blocked by required conditions
CI/CD / tests-web-async (node20, pg15, mode-redis-cluster) (push) Blocked by required conditions
CI/CD / tests-worker (node20, pg12, mode) (push) Blocked by required conditions
CI/CD / tests-worker (node20, pg15, mode) (push) Blocked by required conditions
CI/CD / tests-worker (node20, pg12, mode-azure) (push) Blocked by required conditions
CI/CD / tests-worker (node20, pg15, mode-azure) (push) Blocked by required conditions
CI/CD / tests-worker (node20, pg12, mode-redis-cluster) (push) Blocked by required conditions
CI/CD / tests-worker (node20, pg15, mode-redis-cluster) (push) Blocked by required conditions
CI/CD / e2e-tests (push) Blocked by required conditions
CI/CD / e2e-server-tests (push) Blocked by required conditions
CI/CD / all-ci-passed (push) Blocked by required conditions
CI/CD / push-docker-image (push) Blocked by required conditions
release.yml / release (push) Waiting to run
2025-08-25 14:10:11 +02:00
marliessophieandGitHub 63a99b39fa chore(datasets): default to reading/writing CH (#8724) 2025-08-25 11:49:02 +00:00
marliessophieandGitHub 2066658773 chore(datasets): fix flaky test (#8722)
* chore(datasets): fix flaky test

* chore: specify timeout
2025-08-25 11:10:55 +00:00
marliessophieandGitHub 43ffd1b72d perf(evals): propagate trace timestamp to eval execution for better trace and observations filtering (#8687)
* chore(job_executions): add `job_input_trace_timestamp` to table

* chore: rely on trace timestamp in eval execution trace and observation queries

* fixup: return trace timestamp in existance check

* chore: return trace existence bool and timestamp

* chore: fix tests

* chore: rename

* chore: bump migration
2025-08-25 09:06:42 +00:00
Max DeichmannandGitHub e5f96790c8 chore: upgrade gcs storage client library (#8698) 2025-08-23 12:31:58 +00:00
0b062f7c5b feat: add dashboard filter persistence (#8377)
* docs: add spec

* feat: add filter and date range saving to dashboard

* feat: take out url params hook for date ranges

* docs: remove specs

* move migration

* move migration

* fixes

* fixes

---------

Co-authored-by: Max Deichmann <m.deichmann@tum.de>
2025-08-23 11:51:50 +00:00
Leo WeigandandGitHub 8dc119365a chore: re-enable focus-refetching for data tables (#8688) 2025-08-22 15:05:31 +00:00
marliessophieandGitHub 4bdc46ac31 chore(dataset-run-items): remove PG execution fallbacks (#8685) 2025-08-22 14:47:58 +00:00
Leo WeigandandGitHub 2308495088 fix(playground): empty message error when submitting all windows (#8686)
* fix(prompts): variables should not break

* fix(playground): empty message error when submitting all
2025-08-22 14:43:15 +00:00
marliessophieandGitHub 3f847591f4 feat(datasets): full text search for dataset items (#8680)
* feat(dataset-items-ui): allow filtering by metadata keys

* chore: push

* chore: push

* chore: fix after rebase

* feat(datasets): full text search for dataset items

* chore: remove checking for trace and observation existence before returning dataset items

* chore: eslint

* chore: pass search parameters
2025-08-22 13:51:38 +00:00
Hassieb PakzadandGitHub dff54e8ba9 fix(otel): override span observation types if generation-like attributes present (#8683) 2025-08-22 12:59:54 +00:00
marliessophieandGitHub 27d43a7a29 chore(dataset-run-items): remove PG execution path from tests (#8638)
* chore(dataset-run-items): remove PG execution path from web-async tests

* chore: return CH result on CH write

* chore: remove PG code

* chore: add timeouts

* chore: push

* chore: ch inserts

* chore: use CH inserts in worker batch action test
2025-08-22 07:26:09 +00:00
marliessophieandGitHub fced289c1c feat(dataset-items-ui): allow filtering by metadata keys (#8627)
* feat(dataset-items-ui): allow filtering by metadata keys

* chore: push

* chore: push

* chore: fix after rebase
2025-08-22 07:10:34 +00:00
Marc KlingenandGitHub f37b0cbe3b fix: clock sync issue with cursor bg agent (#8674) 2025-08-21 21:27:03 +02:00
Marc KlingenandGitHub ca9d42c7a3 chore: add TERM env to cursor dockerfile (#8673) 2025-08-21 21:21:32 +02:00
marliessophieandGitHub 4da2fe32b2 docs(dataset-runs-ui): rename experiments to dataset runs across ui (#8662)
* docs(dataset-runs-ui): rename `experiments` to `dataset runs` across ui

* chore: push
2025-08-21 17:53:12 +00:00
marliessophieandGitHub 3770613a03 fix(annotation): verify existing CH score data type prior to upsert (#8666)
* fix(annotation): verify existing CH score data type prior to upsert

* chore: push
2025-08-21 16:21:38 +00:00
Nimar f543dc2e38 chore: release v3.102.0
Codespell / Check for spelling errors (push) Waiting to run
CI/CD / pre-job (push) Waiting to run
CI/CD / lint (push) Blocked by required conditions
CI/CD / prettier-check (push) Blocked by required conditions
CI/CD / test-docker-build (push) Blocked by required conditions
CI/CD / tests-web-sync (node20, pg12) (push) Blocked by required conditions
CI/CD / tests-web-sync (node20, pg15) (push) Blocked by required conditions
CI/CD / tests-web-async (node20, pg12, mode) (push) Blocked by required conditions
CI/CD / tests-web-async (node20, pg15, mode) (push) Blocked by required conditions
CI/CD / tests-web-async (node20, pg12, mode-azure) (push) Blocked by required conditions
CI/CD / tests-web-async (node20, pg15, mode-azure) (push) Blocked by required conditions
CI/CD / tests-web-async (node20, pg12, mode-redis-cluster) (push) Blocked by required conditions
CI/CD / tests-web-async (node20, pg15, mode-redis-cluster) (push) Blocked by required conditions
CI/CD / tests-worker (node20, pg12, mode) (push) Blocked by required conditions
CI/CD / tests-worker (node20, pg15, mode) (push) Blocked by required conditions
CI/CD / tests-worker (node20, pg12, mode-azure) (push) Blocked by required conditions
CI/CD / tests-worker (node20, pg15, mode-azure) (push) Blocked by required conditions
CI/CD / tests-worker (node20, pg12, mode-redis-cluster) (push) Blocked by required conditions
CI/CD / tests-worker (node20, pg15, mode-redis-cluster) (push) Blocked by required conditions
CI/CD / e2e-tests (push) Blocked by required conditions
CI/CD / e2e-server-tests (push) Blocked by required conditions
CI/CD / all-ci-passed (push) Blocked by required conditions
CI/CD / push-docker-image (push) Blocked by required conditions
release.yml / release (push) Waiting to run
2025-08-21 18:00:43 +02:00
NimarandGitHub db328c5d82 feat(tracing): map vercel AI sdk observation types (#8670) 2025-08-21 14:59:53 +00:00
NimarandGitHub 9a38bdcce2 feat(tracing): map openinference, openai to new observation types (#8664)
* feat(tracing): map openinference, openai to new observation types

* make it simpler!

* simplify

* add otel gen-ai

* move instatiation to otelingest
2025-08-21 14:15:25 +00:00
steffen911 6d893cdc75 chore: release v3.101.0
Codespell / Check for spelling errors (push) Waiting to run
CI/CD / pre-job (push) Waiting to run
CI/CD / lint (push) Blocked by required conditions
CI/CD / prettier-check (push) Blocked by required conditions
CI/CD / test-docker-build (push) Blocked by required conditions
CI/CD / tests-web-sync (node20, pg12) (push) Blocked by required conditions
CI/CD / tests-web-sync (node20, pg15) (push) Blocked by required conditions
CI/CD / tests-web-async (node20, pg12, mode) (push) Blocked by required conditions
CI/CD / tests-web-async (node20, pg15, mode) (push) Blocked by required conditions
CI/CD / tests-web-async (node20, pg12, mode-azure) (push) Blocked by required conditions
CI/CD / tests-web-async (node20, pg15, mode-azure) (push) Blocked by required conditions
CI/CD / tests-web-async (node20, pg12, mode-redis-cluster) (push) Blocked by required conditions
CI/CD / tests-web-async (node20, pg15, mode-redis-cluster) (push) Blocked by required conditions
CI/CD / tests-worker (node20, pg12, mode) (push) Blocked by required conditions
CI/CD / tests-worker (node20, pg15, mode) (push) Blocked by required conditions
CI/CD / tests-worker (node20, pg12, mode-azure) (push) Blocked by required conditions
CI/CD / tests-worker (node20, pg15, mode-azure) (push) Blocked by required conditions
CI/CD / tests-worker (node20, pg12, mode-redis-cluster) (push) Blocked by required conditions
CI/CD / tests-worker (node20, pg15, mode-redis-cluster) (push) Blocked by required conditions
CI/CD / e2e-tests (push) Blocked by required conditions
CI/CD / e2e-server-tests (push) Blocked by required conditions
CI/CD / all-ci-passed (push) Blocked by required conditions
CI/CD / push-docker-image (push) Blocked by required conditions
release.yml / release (push) Waiting to run
2025-08-21 14:42:45 +02:00
Steffen SchmitzGitHubellipsis-dev[bot] <65095814+ellipsis-dev[bot]@users.noreply.github.com>
bd196aa31c feat: allow opt-out of blob-storage-file-log for lifecycle policed buckets (#8654)
* feat: allow opt-out of blob-storage-file-log for lifecycle policed buckets

* Update worker/src/features/scores/processClickhouseScoreDelete.ts

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

---------

Co-authored-by: ellipsis-dev[bot] <65095814+ellipsis-dev[bot]@users.noreply.github.com>
2025-08-21 12:11:15 +00:00
Steffen SchmitzandGitHub 1c8d1304d6 perf: avoid final modified for traces.countAll via uniq (#8661) 2025-08-21 11:09:31 +02:00
Nimar 9276d2dbbb chore: release v3.100.0
Codespell / Check for spelling errors (push) Waiting to run
CI/CD / pre-job (push) Waiting to run
CI/CD / lint (push) Blocked by required conditions
CI/CD / prettier-check (push) Blocked by required conditions
CI/CD / test-docker-build (push) Blocked by required conditions
CI/CD / tests-web-sync (node20, pg12) (push) Blocked by required conditions
CI/CD / tests-web-sync (node20, pg15) (push) Blocked by required conditions
CI/CD / tests-web-async (node20, pg12, mode) (push) Blocked by required conditions
CI/CD / tests-web-async (node20, pg15, mode) (push) Blocked by required conditions
CI/CD / tests-web-async (node20, pg12, mode-azure) (push) Blocked by required conditions
CI/CD / tests-web-async (node20, pg15, mode-azure) (push) Blocked by required conditions
CI/CD / tests-web-async (node20, pg12, mode-redis-cluster) (push) Blocked by required conditions
CI/CD / tests-web-async (node20, pg15, mode-redis-cluster) (push) Blocked by required conditions
CI/CD / tests-worker (node20, pg12, mode) (push) Blocked by required conditions
CI/CD / tests-worker (node20, pg15, mode) (push) Blocked by required conditions
CI/CD / tests-worker (node20, pg12, mode-azure) (push) Blocked by required conditions
CI/CD / tests-worker (node20, pg15, mode-azure) (push) Blocked by required conditions
CI/CD / tests-worker (node20, pg12, mode-redis-cluster) (push) Blocked by required conditions
CI/CD / tests-worker (node20, pg15, mode-redis-cluster) (push) Blocked by required conditions
CI/CD / e2e-tests (push) Blocked by required conditions
CI/CD / e2e-server-tests (push) Blocked by required conditions
CI/CD / all-ci-passed (push) Blocked by required conditions
CI/CD / push-docker-image (push) Blocked by required conditions
release.yml / release (push) Waiting to run
2025-08-21 10:01:50 +02:00
Leo WeigandandGitHub 0f76e0769c fix(trace-detail): remove unnecessary z-indexes (#8658) 2025-08-21 07:53:23 +00:00
Steffen SchmitzandGitHub b309b3ae22 perf: use uniq instead of count for prompt metrics call (#8647) 2025-08-21 06:21:48 +00:00
Marc KlingenandGitHub b22038baaa fix(posthog-integration): skip processing of person profiles for anon… (#8617)
* fix(posthog-integration): skip processing of person profiles for anon events

* improve perf via random distinctId for anon events

* fix
2025-08-20 20:50:17 +00:00
Steffen SchmitzandGitHub d769ed848a perf: use aggregations to calculate traces all API result (#8616)
* perf: use aggregations to calculate traces all API result

* chore: adjust AMT settings for redis-cluster tests

* chore: patch test cases

* chore: handle prefix and filter differentl

* chore: move trace column def to shared server to use env imports
2025-08-20 17:36:10 +00:00
NimarandGitHub 064137c93f fix: explicit observation types should be respected (#8644)
* fix: explicit observation types should be respected

* fix test
2025-08-20 16:34:31 +00:00
Leo WeigandandGitHub 22b897a799 fix(playground): tools popover breaking with many tools (#8643) 2025-08-20 16:11:41 +00:00
Tyler AshworthandGitHub afc0668e40 fix: use dynamic asset path for Ragas logo with custom basePath (#8612)
* fix: use dynamic asset path for Ragas logo with custom basePath

- Fixes hardcoded /assets/ragas-logo.png path that breaks with NEXT_PUBLIC_BASE_PATH
- Uses router.basePath to construct correct asset URL dynamically  
- Resolves broken Ragas evaluator icons for custom base path deployments
- Backward compatible with root path deployments

Fixes #8611

* fix: prettier formatting for ragas-logo.tsx

- Add missing newline at end of file
- Ensure consistent formatting

* fix: apply prettier formatting to ragas-logo.tsx

* fix: use env.NEXT_PUBLIC_BASE_PATH directly for Ragas logo

Addresses reviewer feedback to use environment variable directly
instead of useRouter().basePath, matching pattern used throughout
the codebase (LangfuseLogo, auth redirects, API endpoints, etc.)
2025-08-20 15:49:34 +00:00
Steffen SchmitzandGitHub 63a066c9bd perf: split batch to be processed on string length errors in CH writer (#8615)
* perf: split batch to be processed on string length errors in CH writer

* chore: add ress comments

* chore: format
2025-08-20 15:41:04 +00:00
Leo WeigandandGitHub 81d2884364 chore(tracing): consistently use thousands separator (#8642) 2025-08-20 15:28:54 +00:00
Max DeichmannandGitHub 14b030eedf perf: update eval execution indices (#8641)
* perf: update eval execution indices

* push
2025-08-20 15:14:11 +00:00
NimarandGitHub 0de920c32f chore: upgrade trpc to v11, react-query to v5, typescript to v5.7.2 (#8529)
* chore: upgrade to trpc v11, react-query v5

* equalize ts version

* onSuccess -> useEffect

* isPending

* restore some

* fix

* fixx

* fix again

* fix

* ugly working state

* make the TS concise types a bit more beautifyul

* fix one more

* more loading

* mock

* moar pending

* moar

* use queryclient instead

* upgrade superjson

* pot fix

* more specific types

* fix build

* clean up

* undo

* cleanup

* cleanup
2025-08-20 16:21:26 +02:00
marliessophieandGitHub df3a4c761e fix(posthog-telemetry): read from respective strategy for DRI count (#8639) 2025-08-20 14:07:33 +00:00
Steffen SchmitzGitHubellipsis-dev[bot] <65095814+ellipsis-dev[bot]@users.noreply.github.com>
dfce40d029 chore: add debugging logs for otel timestamp parsing errors (#8631)
* chore: add debugging logs for otel timestamp parsing errors

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

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

* chore: prettier

---------

Co-authored-by: ellipsis-dev[bot] <65095814+ellipsis-dev[bot]@users.noreply.github.com>
2025-08-20 13:40:51 +00:00
marliessophieandGitHub 01d2095628 chore(dataset-run-items): run experiment service test (#8595)
* chore(dataset-run-items): run experiment service test

* chore: push
2025-08-20 13:21:07 +00:00
NimarandGitHub af80548ea1 chore: add test for encryption (#8635) 2025-08-20 15:12:52 +02:00
Leo WeigandandGitHub e45c24838c feat: highlight variables in prompt content (#8632)
chore: highlight variables in prompt content
2025-08-20 12:37:11 +00:00
Steffen SchmitzandGitHub f9e7d706b7 fix: calculate correct traces table count (#8629) 2025-08-20 13:41:46 +02:00
Łukasz JernaśandGitHub 6ea1b986a5 fix: Coerce SLACK_FETCH_LIMIT to number (#8628)
You can't pass numbers as Kubernetes env variables, so we need
to coerce from strings.

Signed-off-by: Łukasz Jernaś <lukasz.jernas@allegro.com>
2025-08-20 11:38:54 +00:00
Steffen SchmitzandGitHub babc02a067 perf: use aggregations to calculate traces trpc all route (#8599) 2025-08-20 12:01:33 +02:00
Steffen SchmitzandGitHub e33b5ff4e1 chore: bump clickhouse deletion timeout default (#8620) 2025-08-20 09:03:50 +00:00
Leo WeigandandGitHub 8cd2b8867e feat(trace-detail): improved search that works well with new trace tree (#8604)
* wip small scores

* fix: root node missing selected state

* refactor: only use command for search results

* chore: more consistent styling between search and tree

* fix: rerendering issue

* fix: more styling issues

* fix: lint warnings and eslint vscode plugin config

* more cleanup

* fix: selecting root node from search
2025-08-20 08:26:52 +00:00
Marc KlingenandGitHub 542e043f2c feat(ui): improved demo app menu buttons and link from home to projectid/traces (#8608)
* jump to traces for demo project instead of / (dashboard)

* add new demo main menu
2025-08-19 18:18:36 +00:00
Marc KlingenandGitHub 1046a9f65e feat(ui): improve comments in annotation drawer within own panel (#8601)
* feat(ui): improve comments in annotation drawer within own panel

* autofocus and scroll on submission

* debounce
2025-08-19 16:00:50 +00:00
Marc KlingenandGitHub 28f023b06a fix(ui): comment count on trace-level trace-tree-node (#8603) 2025-08-19 17:17:48 +02:00
NimarandGitHub 698331a99d fix: make test data not error UI (#8564) 2025-08-19 12:05:35 +00:00
marliessophieandGitHub 0f818048ae chore(evals-ui): move target data type selection above running evaluator time scope (#8596) 2025-08-19 11:38:25 +00:00
marliessophieandGitHub 514d5edb0d chore(dataset-api-servertest): introduce awaits for async operations and increase timeouts (#8582) 2025-08-19 11:29:37 +02:00
Leo ba91ed14f1 chore: release v3.99.0 2025-08-19 10:23:41 +02:00
Leo WeigandandGitHub 49e8c8c5e0 fix(trace-detail): better contrast for selected span (#8591)
improve selected item contrast
2025-08-19 08:01:11 +00:00
Steffen SchmitzandGitHub 5748cfedd4 perf: reduce expensive joins for default case on metrics/daily route (#8587)
* perf: reduce expensive joins for default case on metrics/daily route

* chorE: clarify comment

* chore: ensure tests pass
2025-08-18 16:29:28 +00:00
Leo WeigandandGitHub 3f6916b13f feat(datasets): allow copying items within source dataset (#8580)
* feat(datasets): allow duplicating items in same datset

* revert some changes
2025-08-18 16:05:11 +00:00
NimarandGitHub d7a4a34df1 fix(eval-service): selected observation type not handled in eval service (#8586)
* fix: selected observation type not handled in eval service

* sort alphabetically

* add test

* update
2025-08-18 14:12:21 +00:00
Steffen SchmitzandGitHub 3d3e8967a6 fix: drop unused redis reference in evalService (#8585) 2025-08-18 13:10:25 +00:00
marliessophieandGitHub f1bf562811 chore(migrations): add trace_id index for dataset_run_items_rmt (#8583)
* chore(migrations): add trace_id index for dataset_run_items_rmt

* chore: fix typo in file name
2025-08-18 12:34:21 +00:00
Steffen SchmitzandGitHub 02d6c78626 perf: skip trace upsert queue if no job configs for project configured (#8558) 2025-08-18 11:57:32 +00:00
Leo WeigandandGitHub 251741aacb fix(trace-detail): collapse all button ignoring root node (#8581) 2025-08-18 13:16:06 +02:00
Steffen SchmitzandGitHub ce49294607 perf: add caching for clickhouse dataset run items in createEvalJob (#8551)
* perf: add caching for clickhouse dataset run items in createEvalJob

* chore: add return statement

* chore: simplify return statement

* chore: patch test case to be PG and CH compatible

* chore: patch worker tests

* chore: update get traceById aggregation behaviour

* chore: comment out redis cluster DRI settings

* chore: add note about parallelization

* chore: add clarification on in-memory filter
2025-08-18 09:55:47 +00:00
Leo WeigandandGitHub a0c876271a feat(trace-detail): tree view improvements (#8557)
* turn timeline toggle into dropdown

* improve collapse / expand all

* fix missing title on view options

* fix observation name overflow

* allow resizing tree view

* better tree indicators and smaller font size

* integrate new feedback

* add some spacing to scores

* fix timeline toggle label

* fix timeline view dynamic sizing

* resizable improvements

* last tree connector fixes

* switching views shouldn't push to history

* even more density

* polish

* fix type errors

* update dataset compare detail view

* fix missing vertical padding in tree node without metrics

* rebase cleanup
2025-08-18 08:26:30 +00:00
NimarandGitHub 221d8e9043 chore: upgrade turborepo 2.5.6 (#8575)
upgrade turbo
2025-08-18 08:19:37 +00:00
Steffen SchmitzandGitHub e5e30eb4cb fix: check that value is defined before accessing length in ClickhouseWriter (#8565) 2025-08-15 20:59:26 +00:00
Steffen SchmitzandGitHub 7834a37d32 chore: upgrade clickhouse-js to 1.12.1 (#8560) 2025-08-15 15:28:24 +00:00
Steffen SchmitzandGitHub 261d857e90 perf: add metadata indexes on observations table (#8555)
* perf: add metadata indexes on observations table

* chore: add materialization settings

* chore: update statements to make materialization async
2025-08-15 13:06:13 +00:00
Marc Klingen 25889323e6 chore: use input in issue form for self-hosted version 2025-08-15 14:29:00 +02:00
Marc Klingen 14f766908b chore: improve issue template for bug reports 2025-08-15 14:24:42 +02:00
Steffen SchmitzandGitHub 6f06a3ab5f feat: add membership deletion API endpoints (#8553)
* feat: add membership deletion API endpoints

* chore: patch deleteMany behaviour
2025-08-15 09:49:23 +00:00
Jannik MaierhöferandGitHub f706721c12 chore(UI): replace links to reflect new docs structure (#8552)
* data model

* push

* prompts

* scores

* datasets

* tracing features

* openai

* langchain

* misc

* remove
2025-08-15 09:12:59 +00:00
35c8adaccc fix: implement a recursive version of getChannels (#8470)
* fix: implement a recursive version of getChannels

SlackService.getChannels only fetched 200 channels, which after
filtering might get even less. This doesn't work for large
organizations, as there are usually more than 200 active channels,
with possibly more archived (which counted into the limit)

Signed-off-by: Łukasz Jernaś <lukasz.jernas@allegro.com>

* chore: Add limit on how many records we can download from Slack API

Signed-off-by: Łukasz Jernaś <lukasz.jernas@allegro.com>

* fix: Actually respect the fetch limit

Signed-off-by: Łukasz Jernaś <lukasz.jernas@allegro.com>

* fix: Implement review suggestions

Signed-off-by: Łukasz Jernaś <lukasz.jernas@allegro.com>

---------

Signed-off-by: Łukasz Jernaś <lukasz.jernas@allegro.com>
Co-authored-by: Steffen Schmitz <steffen@langfuse.com>
2025-08-15 07:31:53 +00:00
marliessophieandGitHub 465e1661fa fix(trace-detail-playground): ensure input validation in parseGeneration function (#8548) 2025-08-14 20:34:37 +00:00
Marc KlingenandGitHub 1cf917edde chore: update in-product docs links (#8547) 2025-08-14 22:12:11 +02:00
Marc KlingenandGitHub 80efe21420 fix(ui): hide separator of demo badge when sidebar is collapsed & make envlabel dismissable onClick (#8546) 2025-08-14 22:11:52 +02:00
marliessophieandGitHub 6be2e203ba chore(datasets-ui): prefix I/O with "Trace" on runs table in items detail view (#8545)
* chore(datasets-ui): prefix I/O with "Trace" on runs table in items detail view

* chore: fix space
2025-08-14 20:02:08 +00:00
marliessophieandGitHub 488f5494e0 fix(dataset-run-items): CH execution for dataset-level evals (#8540)
* fix(dataset-run-items): CH execution for dataset run items

* chore: fix batch action historical evals for CH execution

* chore: comment

* chore: fix tooltip
2025-08-14 18:12:00 +00:00
Nimar b5eed6653a chore: release v3.98.2 2025-08-14 17:04:55 +02:00
NimarandGitHub 78aa8fdd26 feat(traces): add more observation types (#8507)
* feat(traces): add more observation types

* observations table default filter for all types

* fix test running instructions

* add seeder

* add basic tests

* add to evaluator object selector

* add test to verify we default to span-create for all types

* allow generation like attributes on all types

* check if something is generation LIKE not strictly a GENERATION

* fix test

* ensure that GENERATION-like type's fields can also be null

* simplify create- logic
2025-08-14 13:28:06 +00:00
Marlies Mayerhofer 9f0fbf8081 chore: release v3.98.1 2025-08-14 15:38:30 +02:00
marliessophieandGitHub d63ce00194 fix(dataset-run-items): pg table read (#8537) 2025-08-14 15:37:23 +02:00
Marlies Mayerhofer 9ca761bdc6 chore: release v3.98.0 2025-08-14 14:39:26 +02:00
marliessophieandGitHub e2ec566fe4 chore(dataset-run-items): read and write to dataset_run_items_rmt clickhouse (#8532)
* chore(dataset-run-items): add ReplicatedReplacingMergeTree dataset_run_items_rmt

* chore: drop dataset_run_items background migration row and add dataset_run_items_rmt background migration row and script

* chore: read and write to dataset_run_items_rmt

* chore: add

* chore: push

* chore: push

* chore: fix

* chore: push
2025-08-14 11:58:37 +00:00
marliessophieandGitHub bebc76a502 chore: fix typo in migration name (#8536) 2025-08-14 10:13:04 +00:00
Steffen SchmitzandGitHub 7bfbe19a8c chore: add clickhouse and postgres review guide (#8535) 2025-08-14 10:12:02 +00:00
Marc KlingenandGitHub e625053849 chore: add REVIEW.md for PR reviews 2025-08-14 11:52:09 +02:00
marliessophieandGitHub dc3530f195 chore(dataset-run-items-rmt): add table with suffix, add background migration from postgres to clickhouse (#8531)
* chore(dataset-run-items): add ReplicatedReplacingMergeTree dataset_run_items_rmt

* chore: drop dataset_run_items background migration row and add dataset_run_items_rmt background migration row and script

* chore: rename to include rmt suffix

* chore: push
2025-08-14 09:49:16 +00:00
Hassieb PakzadGitHubellipsis-dev[bot] <65095814+ellipsis-dev[bot]@users.noreply.github.com>
1cf7c81386 feat(llm-invocations): allow additional provider options (#8510)
* feat(llm-invocations): allow additional provider options

* push

* push

* push

* push

* Apply suggestion from @ellipsis-dev[bot]

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

* push

---------

Co-authored-by: ellipsis-dev[bot] <65095814+ellipsis-dev[bot]@users.noreply.github.com>
2025-08-14 08:39:25 +00:00
Marlies Mayerhofer 7e31dda960 chore: release v3.97.5 2025-08-13 23:19:49 +02:00
marliessophieandGitHub 1f64991e7e fix(dataset-run-items): set trace creation default to PG for self-hosters (#8519) 2025-08-13 21:07:33 +00:00
Steffen SchmitzandGitHub 577ad574d1 chore: add projectId to eval processing spans (#8513) 2025-08-13 16:48:41 +00:00
steffen911 879ef9c8f9 chore: release v3.97.4 2025-08-13 17:40:13 +02:00
Steffen SchmitzandGitHub a343d5b187 fix: reinstate ability to remove tags from prompts (#8505) 2025-08-13 15:28:51 +00:00
marliessophieandGitHub ac2b9eaa56 chore(dataset-run-items): add env variable for trace creation (#8508)
* chore(dataset-run-items): add env variable for trace creation

* chore: push

* chore: push
2025-08-13 16:38:59 +02:00
marliessophieandGitHub 43e9cba99a chore: create traces in CH execution path (#8504)
* chore: create traces in CH execution path

* chore: push

* chore: fix import

* chore: push
2025-08-13 15:47:11 +02:00
Steffen SchmitzandGitHub 601f431f69 perf: parallelize S3 and clickhouse deletions for data retention (#8501) 2025-08-13 13:44:49 +00:00
marliessophieandGitHub 4fd1f86f9d chore(dataset-run-items): sunset dual-write phase; do not fall back on PG in CH execution path (#8497)
* chore(dataset-run-items): return CH result

* chore: sunset dual-write phase for dri in CH execution path; do not fall back on PG for reads

* chore: eslint
2025-08-13 12:27:35 +00:00
marliessophieandGitHub 8245888e3c fix(dataset-runs): implement optimistic concurrency handling for dataset run creation in POST /dataset-run-items (#8494) 2025-08-13 10:18:00 +00:00
steffen911 daf4e2fe02 chore: release v3.97.3 2025-08-13 11:38:24 +02:00
Steffen SchmitzandGitHub 129693fb69 chore: skip body validation for bullmq GET requests (#8490) 2025-08-13 09:15:33 +00:00
Steffen SchmitzandGitHub 204948db44 perf: skip FINAL for traces all route without metrics (#8489) 2025-08-13 09:15:29 +00:00
marliessophieandGitHub d42ba5fc99 fix(evals): redirect and pull data for latest template version post template edit (#8491) 2025-08-13 09:15:25 +00:00
Steffen SchmitzandGitHub 97a539ced3 chore: allow whitelisting which AMTs can be used (#8488) 2025-08-13 09:12:07 +00:00
Leo WeigandandGitHub 9d8dace197 feat(llm-connections): populate gcp region in update form (#8487)
feat(llm-connections): correctly show gcp region in update form
2025-08-13 08:58:31 +00:00
marliessophieandGitHub bc2dc4d89c feat(annotation): add POST queue API (#8478)
* feat(annotation): add POST queue API

* chore: fix types

* docs: add API ref

* chore: improve score configs check

* chore: add postman collection
2025-08-12 17:25:19 +00:00
Marc Klingen 2dd5c8a8aa chore: release v3.97.2 2025-08-12 17:31:22 +02:00
Marc Klingen d17b21f04d chore: specify development node version 2025-08-12 17:30:31 +02:00
Marc KlingenandGitHub c5a0f44bdd fix: fail silently when /api/latest-releases returns invalid schema (#8476)
* fix: fail silently when /api/latest-releases returns invalid schema

* push
2025-08-12 15:18:27 +00:00
Steffen SchmitzandGitHub 011b4912f2 chore: limit traces to trace AMT migration to only write into traces_all_amt (#8458)
* chore: limit traces to trace AMT migration to only write into traces_all_amt

* chore: revert validation changes

* chore: simplify query

* chore: tune both queries

* chore: avoid full aggregation during migration

* chore: handle IO coalescing to skip aggregations fully

* chore: remove obsolete query parts
2025-08-12 14:44:55 +00:00
Steffen SchmitzandGitHub a4d773066f chore: move health check to new traces AMTs (#8473) 2025-08-12 14:44:09 +00:00
Marlies Mayerhofer 25bdb1690b chore: release v3.97.1 2025-08-12 11:33:34 +02:00
marliessophieandGitHub f8565d7772 fix(evals): update trace deletion logic to remove job executions directly (#8466) 2025-08-12 09:15:34 +00:00
marliessophieandGitHub a912057082 fix(evals): ensure default model supports langfuse evals at set up (#8411)
* fix(evals): ensure default model supports langfuse evals at set up

* fix: await upsertDefaultModel

* chore: provide actionable error messages in case of misconfiguration
2025-08-12 09:11:00 +00:00
marliessophieandGitHub e0d3270f50 fix(evals): json parse preview to properly handle doubleEncoded IO (#8464)
* fixup(evals): json parse preview

* chore: add comment
2025-08-12 07:47:09 +00:00
Hassieb Pakzad 99e5a0b224 chore: release v3.97.0 2025-08-11 19:22:53 +02:00
bccdee5410 feat: add HTTPS proxy support for LLM API calls (#8461)
* feat: Add HTTPS proxy support for LLM API calls (#7932)

* remove tests

---------

Co-authored-by: suhwan <52690419+suhwan-cheon@users.noreply.github.com>
2025-08-11 17:18:54 +00:00
marliessophieandGitHub 738cbbc8cd feat(annotation-assignment): allow removing user assignments in UI (#8370)
* feat(annotation-assignment): allow removing user assignments in UI

* chore: push
2025-08-11 15:57:02 +00:00
marliessophieandGitHub 920c52bb06 chore(evals): cancel job executions in case of underlying trace delete (#8443)
* chore(evals): cancel job executions in case of underlying trace delete

* chore: lint
2025-08-11 15:56:27 +00:00
Leo WeigandandGitHub 4f134790af chore: add edit button to dataset items dot menu (#8444)
* chore: add edit button to dataset items dot menu

* fix: disable button based on access

* fix: DatasetActionButton not forwarding refs

* fix: eslint
2025-08-11 15:33:03 +00:00
Steffen SchmitzandGitHub 21b3ce3c82 Revert "perf: stream new records to clickhouse in writer (#8421)" (#8459)
This reverts commit b35583056b.
2025-08-11 15:30:42 +00:00
Leo WeigandandGitHub 0531b57e1a refactor: update create org form validation (#8455) 2025-08-11 14:26:45 +00:00
Leo WeigandandGitHub 7b857a0dc4 chore: surface required models for Azure/Bedrock (#8453)
- LLM connections: surface required model selection
for Azure/Bedrock; simplify advanced settings and validation
- Move custom model names to main form for Azure and Bedrock; require at least one model
- Remove default models toggle for Azure/Bedrock (they don’t support defaults)
- Keep Azure base URL and extra headers in main form; remove Azure advanced panel entirely
- Show advanced settings only for OpenAI, Anthropic, Vertex AI, and Google AI Studio
- Improve validation order to avoid confusing errors when defaults aren’t supported
- Add adapter-based placeholder for provider name; clarify copy
- Consolidate adapter logic to a single helper used in UI and schema
2025-08-11 14:26:27 +00:00
Steffen SchmitzandGitHub 6c97fe3c04 chore: drop "remove tag" capability from UI (#8454) 2025-08-11 14:24:08 +00:00
Steffen SchmitzandGitHub 1d69cbd41b chore: migrate remaining analytics and export queries to traces AMTs (#8451)
* chore: migrate remaining analytics and export queries to traces AMTs

* chore: patch

* chore: patches

* dummy

* chore: replace start_time with timestamp

* chore: adjust timeshift

* chore: switch to startTime

* chore: updat ereadme
2025-08-11 13:48:48 +00:00
Steffen SchmitzandGitHub ab26692913 fix(dashboards): patch invalid tags filter (#8445)
* fix(dashboards): patch invalid tags filter

* chore: move tests

* chore: remove unnecessary auth overwrite

* chore: test update
2025-08-11 12:14:56 +00:00
Hassieb PakzadandGitHub b791544864 fix(model-prices): gpt-5 prices with model date (#8442)
* fix(model-prices): gpt-5 prices with model date

* add to playground and evals

* add cache clearance
2025-08-11 11:49:32 +00:00
Steffen SchmitzandGitHub b35583056b perf: stream new records to clickhouse in writer (#8421)
* perf: stream new records to clickhouse in writer

* chore: update unit tests
2025-08-11 08:23:16 +00:00
Steffen SchmitzandGitHub 4504530e3d chore: skip unavailable shards in traces AMT background migration (#8438) 2025-08-11 08:21:50 +00:00
marliessophieandGitHub 45eed4b5c0 feat(scores-exports): add author to score exports (#8419) 2025-08-08 16:29:45 +00:00
marliessophieandGitHub 9fa31ba68e chore(datasets-csv-upload): no longer nest column values if single valid json object (#8416) 2025-08-08 16:04:55 +00:00
Hassieb PakzadandGitHub ea35c25269 fix: gemini-2.5-flash name (#8412) 2025-08-08 14:47:05 +00:00
steffen911 c427791070 chore: release v3.96.2 2025-08-08 16:35:11 +02:00
Steffen SchmitzandGitHub 25220aef55 perf: allow sharding for trace upsert queue (#8396)
* perf: allow sharding for trace upsert queue

* chore: handle metrics for traceupsertqueue

* chore: remove outdated comment

* chore: test util upgrade

* chore: patch typing
2025-08-08 14:16:27 +00:00
Max Deichmann 8c02d5dc22 chore: release v3.96.1 2025-08-08 15:57:05 +02:00
Max DeichmannandGitHub 0b60076871 chore: upgrade form data (#8408) 2025-08-08 13:17:12 +00:00
marliessophieandGitHub b28a83531b fix(evals): display final status in peek view (#8406) 2025-08-08 13:12:38 +00:00
Steffen SchmitzandGitHub aa4d34ae66 chore: skip S3 list and legacy trace insert based on env flag (#8298)
* chore: skip S3 list and legacy trace insert based on env flag

* chore: adjust skip s3 list conditions

* chore; refactor traceUpserQueue entity assoication

* chore: reverse
2025-08-08 12:47:04 +00:00
Steffen SchmitzandGitHub cca352ad13 chore: remove trace null deletions (#8405) 2025-08-08 15:02:37 +02:00
Steffen SchmitzandGitHub 58c96cada7 chore: include boolean scores in the reference of scores-numeric (#8086) 2025-08-08 12:39:56 +00:00
Steffen SchmitzandGitHub 6f93389936 chore: adjust devcontainer setup to install deps via dockerfile (#8399)
* chore: adjust devcontainer setup to install deps via dockerfile

* Update Dockerfile
2025-08-08 12:10:05 +00:00
Max DeichmannandGitHub b8d9586490 chore: fix dev command (#8404) 2025-08-08 14:38:21 +02:00
Leo 534f5696ad chore: release v3.96.0 2025-08-08 14:20:05 +02:00
Leo WeigandandGitHub 4cebe2831c fix(onboarding): only show org onboarding for Cloud and more UX touches (#8392) 2025-08-08 14:17:49 +02:00
45a5f3b8a2 chore: upgrade slack dependency (#8402)
* chore: upgrade slack dependencies

* chore: upgrade slack deps and remove unnecessary deps from web/worker

---------

Co-authored-by: Max Deichmann <m.deichmann@tum.de>
2025-08-08 14:05:41 +02:00
Max DeichmannandGitHub 8dea9b6a3a chore: remove explicit axios dependency (#8397)
remove axios
2025-08-08 11:38:42 +00:00
marliessophieandGitHub c5b781d3f6 fix(llm-connections): ensure dialog remains open when switching tabs (#8391)
* fix(llm-connections): ensure dialog remains open when switching tabs

* chore: lint
2025-08-08 09:52:48 +00:00
Leo WeigandandGitHub 07469c928d feat(cloud): new onboarding survey to better understand our users (#8374) 2025-08-08 10:37:13 +02:00
Marc KlingenandGitHub 5b10110dfe fix(auth): honor NEXT_PUBLIC_BASE_PATH in password reset verification (#8385) 2025-08-07 23:14:32 +00:00
Thorsten SpiekerandGitHub 0eb5d1c3c0 feat: pivot table sorting (#8214) 2025-08-08 00:12:09 +02:00
Max Deichmann 1585bf5e14 chore: release v3.95.2 2025-08-07 20:11:10 +02:00
Max DeichmannandGitHub dec6d6f975 chore: add openai 5 models (#8378) 2025-08-07 20:09:36 +02:00
Max DeichmannandGitHub 36fa7ea15e chore: improve typing of users (#8376)
push
2025-08-07 17:28:10 +00:00
427 changed files with 17812 additions and 8920 deletions
-54
View File
@@ -1,54 +0,0 @@
FROM node:20
# ---------- System packages --------------------------------------------------
# The buildpack-deps base already ships git, build-essential, python, etc.
# Add a few extra tools handy during Langfuse development.
RUN apt-get update && \
DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends \
openssl \
wget \
curl \
ca-certificates \
postgresql-client \
redis-tools \
less nano \
sudo \
&& rm -rf /var/lib/apt/lists/*
# ---------- Docker -----------------------------------------------------------
# Install Docker for background agents that need container capabilities
RUN curl -fsSL https://get.docker.com -o get-docker.sh && \
sh get-docker.sh && \
rm get-docker.sh
# ---------- pnpm -------------------------------------------------------------
# Langfuse monorepo relies on pnpm 9.5.0 (see CONTRIBUTING.md)
ENV PNPM_HOME="/pnpm"
ENV PATH="$PNPM_HOME:$PATH"
RUN corepack enable && \
corepack prepare pnpm@9.5.0 --activate
# ---------- golang-migrate ----------------------------------------------------
# CLI used for database migrations during development.
ENV MIGRATE_VERSION=4.18.3
RUN wget -qO- "https://github.com/golang-migrate/migrate/releases/download/v${MIGRATE_VERSION}/migrate.linux-amd64.tar.gz" \
| tar -xz -C /usr/local/bin && \
chmod +x /usr/local/bin/migrate
# ---------- Non-root user -----------------------------------------------------
# Create non-root user with sudo privileges and docker group access
RUN useradd -ms /bin/bash ubuntu && \
usermod -aG sudo ubuntu && \
usermod -aG docker ubuntu && \
echo "ubuntu ALL=(ALL) NOPASSWD:ALL" >> /etc/sudoers
# Pre-create pnpm store and set correct ownership to avoid first-run cost & permission issues
RUN pnpm store path > /dev/null && \
chown -R ubuntu:ubuntu /pnpm
USER ubuntu
WORKDIR /home/ubuntu
ENV HOME=/home/ubuntu
# Container starts with a bash shell ready for hacking.
CMD ["bash"]
+4 -7
View File
@@ -1,14 +1,11 @@
{
"agentCanUpdateSnapshot": false,
"install": "cp .env.dev.example .env && pnpm i",
"build": {
"context": ".",
"dockerfile": "Dockerfile"
},
"start": "sudo service docker start",
"terminals": [
{
"name": "dev server",
"command": "pnpm run dx-f"
"name": "Development Terminal",
"command": "pnpm run dev",
"description": "Main development terminal running the development server"
}
]
}
-6
View File
@@ -1,6 +0,0 @@
{
"name": "langfuse-development",
"forwardPorts": [3000, 5432, 6379, 8123, 9000],
"onCreateCommand": "npm install -g pnpm@9.5.0",
"postCreateCommand": "curl -L https://github.com/golang-migrate/migrate/releases/download/v4.18.3/migrate.linux-amd64.tar.gz | tar xvz && git restore LICENSE README.md && chmod +x migrate && sudo mv migrate /usr/bin && cp .env.dev.example .env && npm install -g @anthropic-ai/claude-code && pnpm i"
}
+13
View File
@@ -0,0 +1,13 @@
# Dev container Dockerfile
FROM mcr.microsoft.com/vscode/devcontainers/typescript-node:20-bookworm
# Install golang-migrate for database migrations
RUN curl -L https://github.com/golang-migrate/migrate/releases/download/v4.18.3/migrate.linux-amd64.tar.gz | tar xvz && \
chmod +x migrate && \
mv migrate /usr/local/bin/migrate
# Install pnpm globally
RUN npm install -g pnpm@9.5.0
# Install Claude Code CLI
RUN npm install -g @anthropic-ai/claude-code
+8
View File
@@ -0,0 +1,8 @@
{
"name": "langfuse-development",
"build": {
"dockerfile": "Dockerfile"
},
"forwardPorts": [3000, 5432, 6379, 8123, 9000],
"postCreateCommand": "cp .env.dev.example .env && pnpm i"
}
+2 -4
View File
@@ -76,6 +76,7 @@ REDIS_AUTH="bitnami"
REDIS_CLUSTER_ENABLED="true"
REDIS_CLUSTER_NODES="127.0.0.1:6370,127.0.0.1:6371,127.0.0.1:6372,127.0.0.1:6373,127.0.0.1:6374,127.0.0.1:6375"
LANGFUSE_INGESTION_QUEUE_SHARD_COUNT=8
LANGFUSE_TRACE_UPSERT_QUEUE_SHARD_COUNT=4
# openssl rand -hex 32 used only here
ENCRYPTION_KEY=0000000000000000000000000000000000000000000000000000000000000000
@@ -85,8 +86,5 @@ NEXT_PUBLIC_LANGFUSE_RUN_NEXT_INIT="false"
# Use the following settings to enforce running the new AMTs during the tests
LANGFUSE_EXPERIMENT_INSERT_INTO_AGGREGATING_MERGE_TREES="true"
LANGFUSE_EXPERIMENT_COMPARE_READ_FROM_AGGREGATING_MERGE_TREES="true"
LANGFUSE_EXPERIMENT_WHITELISTED_PROJECT_IDS="7a88fb47-b4e2-43b8-a06c-a5ce950dc53a"
LANGFUSE_EXPERIMENT_ADD_QUERY_RESULT_TO_SPAN_PROJECT_IDS="7a88fb47-b4e2-43b8-a06c-a5ce950dc53a"
LANGFUSE_EXPERIMENT_INSERT_INTO_TRACES_TABLE="false"
LANGFUSE_EXPERIMENT_RETURN_NEW_RESULT="true"
LANGFUSE_EXPERIMENT_SAMPLING_RATE=1
+1 -1
View File
@@ -87,4 +87,4 @@ NEXT_PUBLIC_LANGFUSE_RUN_NEXT_INIT="false"
# Slack credentials for development
SLACK_CLIENT_ID=your_slack_client_id
SLACK_CLIENT_SECRET=your_slack_client_secret
SLACK_STATE_SECRET=your_slack_state_secret
SLACK_STATE_SECRET=your_slack_state_secret
+3
View File
@@ -156,6 +156,9 @@ OTEL_SERVICE_NAME="langfuse"
# LANGFUSE_S3_EVENT_UPLOAD_REGION=
# LANGFUSE_S3_EVENT_UPLOAD_ACCESS_KEY_ID=
# LANGFUSE_S3_EVENT_UPLOAD_SECRET_ACCESS_KEY=
# Whether to use blob_storage_file_log table to manage blob storage events
# Can be set to `false` if `event` entities are managed using lifecycle policies in the blob storage bucket.
LANGFUSE_ENABLE_BLOB_STORAGE_FILE_LOG=true
# Automated provisioning of default resources
# LANGFUSE_INIT_ORG_ID=org-id
+22 -10
View File
@@ -1,33 +1,45 @@
name: 🐞 Bug Report
description: Create a bug report to help us improve
title: "bug: "
description: Report a bug to help us improve
title: "bug: <short description>"
labels: ["🐞❔ unconfirmed bug"]
body:
- type: textarea
attributes:
label: Describe the bug
description: A clear and concise description of the bug, as well as what you expected to happen when encountering it.
description: A clear and concise description of the bug, and what you expected to happen when you encountered it.
validations:
required: true
- type: textarea
attributes:
label: To reproduce
description: Describe how to reproduce your bug. Please provide detailed steps, code snippets, reproduction repos etc.
label: Steps to reproduce
description: Describe how to reproduce the bug. Please provide detailed steps, code snippets, a minimal reproduction repository, etc.
validations:
required: true
- type: dropdown
attributes:
label: Langfuse Cloud or self-hosted?
options:
- "Langfuse Cloud"
- "Self-hosted"
validations:
required: true
- type: input
attributes:
label: If self-hosted, what version are you running?
description: We may ask you to upgrade to the latest version, as many issues are continuously being fixed.
- type: textarea
attributes:
label: SDK and container versions
description: If you're experiencing an issue with an integration or SDK, please ensure you're using the latest version. If you're self-hosting Langfuse, check that you're running the most recent version. If updating isn't an option, please provide the specific versions you're currently using.
label: SDK and integration versions
description: If you're experiencing an issue with an integration or SDK, please share all package versions you're using. If you are not on the latest version, try upgrading, as this will often resolve the issue.
- type: textarea
attributes:
label: Additional information
description: Add any other information related to the bug here, screenshots if applicable.
description: Add any other information related to the bug here, including screenshots if applicable.
- type: dropdown
id: contribute
attributes:
label: Are you interested to contribute a fix for this bug?
description: If this is a confirmed bug, the maintainers are happy to support with guidance and review.
label: Are you interested in contributing a fix for this bug?
description: If this is a confirmed bug, the maintainers are happy to provide guidance and review.
options:
- "No"
- "Yes"
+12 -11
View File
@@ -40,7 +40,7 @@ jobs:
version: 9.5.0
- uses: actions/setup-node@v4
with:
node-version: 20
node-version: 24
cache: "pnpm"
cache-dependency-path: "pnpm-lock.yaml"
- name: install dependencies
@@ -66,7 +66,7 @@ jobs:
version: 9.5.0
- uses: actions/setup-node@v4
with:
node-version: 20
node-version: 24
cache: "pnpm"
cache-dependency-path: "pnpm-lock.yaml"
- name: install dependencies
@@ -82,12 +82,12 @@ jobs:
else
BASE_SHA=$(git merge-base origin/main HEAD)
fi
echo "Checking files changed from $BASE_SHA to HEAD"
# Get changed files
CHANGED_FILES=$(git diff --name-only $BASE_SHA HEAD -- '*.js' '*.jsx' '*.ts' '*.tsx' '*.css' | tr '\n' ' ')
if [ -n "$CHANGED_FILES" ] && [ "$CHANGED_FILES" != " " ]; then
echo "Files to check: $CHANGED_FILES"
pnpm prettier --check --experimental-cli $CHANGED_FILES
@@ -142,7 +142,7 @@ jobs:
name: tests-web-sync (node${{ matrix.node-version }}, pg${{ matrix.postgres-version }})
strategy:
matrix:
node-version: [20]
node-version: [24]
postgres-version: [12, 15]
steps:
- uses: actions/checkout@v4
@@ -216,7 +216,7 @@ jobs:
name: tests-web-async (node${{ matrix.node-version }}, pg${{ matrix.postgres-version }}, mode${{ matrix.deploy-mode }})
strategy:
matrix:
node-version: [20]
node-version: [24]
postgres-version: [12, 15]
deploy-mode: ["", "-azure", "-redis-cluster"]
steps:
@@ -291,7 +291,7 @@ jobs:
name: tests-worker (node${{ matrix.node-version }}, pg${{ matrix.postgres-version }}, mode${{ matrix.deploy-mode }})
strategy:
matrix:
node-version: [20]
node-version: [24]
postgres-version: [12, 15]
deploy-mode: ["", "-azure", "-redis-cluster"]
steps:
@@ -359,7 +359,7 @@ jobs:
version: 9.5.0
- uses: actions/setup-node@v4
with:
node-version: 20
node-version: 24
cache: "pnpm"
cache-dependency-path: "pnpm-lock.yaml"
- name: Login to Docker Hub
@@ -423,9 +423,10 @@ jobs:
version: 9.5.0
- uses: actions/setup-node@v4
with:
node-version: 20
node-version: 24
cache: "pnpm"
cache-dependency-path: "pnpm-lock.yaml"
- name: install dependencies
run: |
pnpm install
@@ -517,7 +518,7 @@ jobs:
- name: Setup node
uses: actions/setup-node@v4
with:
node-version: 20
node-version: 24
cache-dependency-path: "pnpm-lock.yaml"
- name: Checkout
uses: actions/checkout@v4
-2
View File
@@ -57,8 +57,6 @@ yarn-error.log*
# vscode
.devcontainer
node_modules
**/node_modules
**/dist
+1 -1
View File
@@ -1 +1 @@
v20
v24.6.0
+2 -1
View File
@@ -28,5 +28,6 @@
"editor.defaultFormatter": "Prisma.prisma"
},
"eslint.lintTask.enable": true,
"eslint.workingDirectories": ["./web", "./worker"]
"eslint.workingDirectories": ["./web", "./worker"],
"eslint.useFlatConfig": false
}
+5 -2
View File
@@ -83,7 +83,10 @@ Depending on the file location (sync, async)
`web` related tests must go into the `web/src/__tests__/` folder.
```sh
pnpm test-sync --testPathPattern="$FILE_LOCATION_PATTERN" --testNamePattern="$TEST_NAME_PATTERN"
pnpm test-async --testPathPattern="$FILE_LOCATION_PATTERN" --testNamePattern="$TEST_NAME_PATTERN"
# For tests in the async folder:
pnpm test -- --testPathPattern="$FILE_LOCATION_PATTERN" --testNamePattern="$TEST_NAME_PATTERN"
# For client tests:
pnpm test-client --testPathPattern="buildStepData" --testNamePattern="buildStepData"
```
### Testing in the Worker Package
@@ -194,4 +197,4 @@ To get a project, use the `get_project` capability with the full project name as
- For easier code reviews, prefer not to move functions etc around within a file unless necessary or instructed to do so
## Development Tips
- Before trying to build the package, try running the linter once first
- Before trying to build the package, try running the linter once first
+3 -1
View File
@@ -241,7 +241,7 @@ We're using Jest with in the `web` package. Therefore, if you want to provide an
There are three types of unit tests:
- `test-sync`
- `test-async`
- `test` (for async folder tests)
- `test-client`
To run a specific test, for example the test: `"should handle special characters in prompt names"` in `prompts.v2.servertest.ts`, run:
@@ -249,6 +249,8 @@ To run a specific test, for example the test: `"should handle special characters
```sh
cd web # or with --filter=web
pnpm test-sync --testPathPattern="prompts\.v2\.servertest" --testNamePattern="should handle special characters in prompt names"
# for async folder tests:
pnpm test -- --testPathPattern="observations-api" --testNamePattern="should fetch all observations"
```
To run all tests:
+88 -86
View File
@@ -91,12 +91,11 @@ Langfuse 是一个 **开源 LLM 工程** 平台。它帮助团队协作 **开发
![Langfuse 概览](https://langfuse.com/images/docs/github-readme/github-feature-overview.png)
- [LLM 应用可观察性](https://langfuse.com/docs/tracing):为你的应用插入仪表代码,并开始将追踪数据传送到 Langfuse,从而追踪 LLM 调用及应用中其他相关逻辑(如检索、嵌入或代理操作)。检查并调试复杂日志及用户会话。试试互动的 [演示](https://langfuse.com/docs/demo) 看看效果。
- [提示管理](https://langfuse.com/docs/prompts/get-started) 帮助你集中管理、版本控制并协作迭代提示。得益于服务器和客户端的高效缓存,你可以在不增加延迟的情况下反复迭代提示。
- [提示管理](https://langfuse.com/docs/prompt-management/get-started) 帮助你集中管理、版本控制并协作迭代提示。得益于服务器和客户端的高效缓存,你可以在不增加延迟的情况下反复迭代提示。
- [评估](https://langfuse.com/docs/scores/overview) 是 LLM 应用开发流程的关键组成部分,Langfuse 能够满足你的多样需求。它支持 LLM 作为"裁判"、用户反馈收集、手动标注以及通过 API/SDK 实现自定义评估流程。
- [评估](https://langfuse.com/docs/evaluation/overview) 是 LLM 应用开发流程的关键组成部分,Langfuse 能够满足你的多样需求。它支持 LLM 作为"裁判"、用户反馈收集、手动标注以及通过 API/SDK 实现自定义评估流程。
- [数据集](https://langfuse.com/docs/datasets/overview) 为评估你的 LLM 应用提供测试集和基准。它们支持持续改进、部署前测试、结构化实验、灵活评估,并能与 LangChain、LlamaIndex 等框架无缝整合。
- [数据集](https://langfuse.com/docs/evaluation/dataset-runs/datasets) 为评估你的 LLM 应用提供测试集和基准。它们支持持续改进、部署前测试、结构化实验、灵活评估,并能与 LangChain、LlamaIndex 等框架无缝整合。
- [LLM 试玩平台](https://langfuse.com/docs/playground) 是用于测试和迭代提示及模型配置的工具,缩短反馈周期,加速开发。当你在追踪中发现异常结果时,可以直接跳转至试玩平台进行调整。
@@ -122,14 +121,14 @@ Langfuse 是一个 **开源 LLM 工程** 平台。它帮助团队协作 **开发
- [本地(docker compose](https://langfuse.com/self-hosting/local):使用 Docker Compose 在你的机器上于 5 分钟内运行 Langfuse。
````bash:README.md/docker-compose
```bash:README.md/docker-compose
# 获取最新的 Langfuse 仓库副本
git clone https://github.com/langfuse/langfuse.git
cd langfuse
# 运行 Langfuse 的 docker compose
docker compose up
`````
```
- [KubernetesHelm](https://langfuse.com/self-hosting/kubernetes-helm):使用 Helm 在 Kubernetes 集群上部署 Langfuse。这是推荐的生产环境部署方式。
@@ -145,40 +144,40 @@ Langfuse 是一个 **开源 LLM 工程** 平台。它帮助团队协作 **开发
### 主要集成:
| 集成 | 支持语言/平台 | 描述 |
| ---------------------------------------------------------------------- | ------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------- |
| [SDK](https://langfuse.com/docs/sdk) | Python, JS/TS | 使用 SDK 进行手动仪表化,实现全面灵活性。 |
| [OpenAI](https://langfuse.com/docs/integrations/openai) | Python, JS/TS | 通过直接替换 OpenAI SDK 实现自动仪表化。 |
| [Langchain](https://langfuse.com/docs/integrations/langchain) | Python, JS/TS | 通过传入回调处理器至 Langchain 应用实现自动仪表化。 |
| [LlamaIndex](https://langfuse.com/docs/integrations/llama-index/get-started) | Python | 通过 LlamaIndex 回调系统实现自动仪表化。 |
| [Haystack](https://langfuse.com/docs/integrations/haystack) | Python | 通过 Haystack 内容追踪系统实现自动仪表化。 |
| [LiteLLM](https://langfuse.com/docs/integrations/litellm) | Python, JS/TS (仅代理) | 允许使用任何 LLM 替代 GPT。支持 Azure、OpenAI、Cohere、Anthropic、Ollama、VLLM、Sagemaker、HuggingFace、Replicate100+ LLMs)。 |
| [Vercel AI SDK](https://langfuse.com/docs/integrations/vercel-ai-sdk) | JS/TS | 基于 TypeScript 的工具包,帮助开发者使用 React、Next.js、Vue、Svelte 和 Node.js 构建 AI 驱动的应用。 |
| [API](https://langfuse.com/docs/api) | | 直接调用公共 API。提供 OpenAPI 规格。 |
| [Google VertexAI 和 Gemini](https://langfuse.com/docs/integrations/google-vertex-ai) | 模型 | 在 Google 上运行基础模型和微调模型。 |
| 集成 | 支持语言/平台 | 描述 |
| ------------------------------------------------------------------------------------ | ---------------------- | -------------------------------------------------------------------------------------------------------------------------------- |
| [SDK](https://langfuse.com/docs/sdk) | Python, JS/TS | 使用 SDK 进行手动仪表化,实现全面灵活性。 |
| [OpenAI](https://langfuse.com/integrations/model-providers/openai-py) | Python, JS/TS | 通过直接替换 OpenAI SDK 实现自动仪表化。 |
| [Langchain](https://langfuse.com/docs/integrations/langchain) | Python, JS/TS | 通过传入回调处理器至 Langchain 应用实现自动仪表化。 |
| [LlamaIndex](https://langfuse.com/docs/integrations/llama-index/get-started) | Python | 通过 LlamaIndex 回调系统实现自动仪表化。 |
| [Haystack](https://langfuse.com/docs/integrations/haystack) | Python | 通过 Haystack 内容追踪系统实现自动仪表化。 |
| [LiteLLM](https://langfuse.com/docs/integrations/litellm) | Python, JS/TS (仅代理) | 允许使用任何 LLM 替代 GPT。支持 Azure、OpenAI、Cohere、Anthropic、Ollama、VLLM、Sagemaker、HuggingFace、Replicate100+ LLMs)。 |
| [Vercel AI SDK](https://langfuse.com/docs/integrations/vercel-ai-sdk) | JS/TS | 基于 TypeScript 的工具包,帮助开发者使用 React、Next.js、Vue、Svelte 和 Node.js 构建 AI 驱动的应用。 |
| [API](https://langfuse.com/docs/api) | | 直接调用公共 API。提供 OpenAPI 规格。 |
| [Google VertexAI 和 Gemini](https://langfuse.com/docs/integrations/google-vertex-ai) | 模型 | 在 Google 上运行基础模型和微调模型。 |
### 与 Langfuse 集成的软件包:
| 名称 | 类型 | 描述 |
| ---------------------------------------------------------------------- | ------------------- | -------------------------------------------------------------------------------------------------------- |
| [Instructor](https://langfuse.com/docs/integrations/instructor) | 库 | 用于获取结构化 LLM 输出(JSON、Pydantic)的库。 |
| [DSPy](https://langfuse.com/docs/integrations/dspy) | 库 | 一个系统性优化语言模型提示和权重的框架。 |
| [Amazon Bedrock](https://langfuse.com/docs/integrations/amazon-bedrock) | 模型 | 在 AWS 上运行基础模型和微调模型。 |
| [Mirascope](https://langfuse.com/docs/integrations/mirascope) | 库 | 构建 LLM 应用的 Python 工具包。 |
| [Ollama](https://langfuse.com/docs/integrations/ollama) | 模型(本地) | 在你的机器上轻松运行开源 LLM。 |
| [AutoGen](https://langfuse.com/docs/integrations/autogen) | 代理框架 | 用于构建分布式代理的开源 LLM 平台。 |
| [Flowise](https://langfuse.com/docs/integrations/flowise) | 聊天/代理界面 | 基于 JS/TS 的无代码构建器,用于定制化 LLM 流程。 |
| [Langflow](https://langfuse.com/docs/integrations/langflow) | 聊天/代理界面 | 基于 Python 的 LangChain 用户界面,采用 react-flow 设计,提供便捷的实验与原型构建体验。 |
| [Dify](https://langfuse.com/docs/integrations/dify) | 聊天/代理界面 | 带有无代码构建器的开源 LLM 应用开发平台。 |
| [OpenWebUI](https://langfuse.com/docs/integrations/openwebui) | 聊天/代理界面 | 自托管的 LLM 聊天网页界面,支持包括自托管和本地模型在内的多种 LLM 运行器。 |
| [Promptfoo](https://langfuse.com/docs/integrations/promptfoo) | 工具 | 开源 LLM 测试平台。 |
| [LobeChat](https://langfuse.com/docs/integrations/lobechat) | 聊天/代理界面 | 开源聊天机器人平台。 |
| [Vapi](https://langfuse.com/docs/integrations/vapi) | 平台 | 开源语音 AI 平台。 |
| [Inferable](https://langfuse.com/docs/integrations/other/inferable) | 代理 | 构建分布式代理的开源 LLM 平台。 |
| [Gradio](https://langfuse.com/docs/integrations/other/gradio) | 聊天/代理界面 | 开源 Python 库,可用于构建类似聊天 UI 的网页界面。 |
| [Goose](https://langfuse.com/docs/integrations/goose) | 代理 | 构建分布式代理的开源 LLM 平台。 |
| [smolagents](https://langfuse.com/docs/integrations/smolagents) | 代理 | 开源 AI 代理框架。 |
| [CrewAI](https://langfuse.com/docs/integrations/crewai) | 代理 | 多代理框架,用于实现代理之间的协作与工具调用。 |
| 名称 | 类型 | 描述 |
| ----------------------------------------------------------------------- | ------------- | --------------------------------------------------------------------------------------- |
| [Instructor](https://langfuse.com/docs/integrations/instructor) | 库 | 用于获取结构化 LLM 输出(JSON、Pydantic)的库。 |
| [DSPy](https://langfuse.com/docs/integrations/dspy) | 库 | 一个系统性优化语言模型提示和权重的框架。 |
| [Amazon Bedrock](https://langfuse.com/docs/integrations/amazon-bedrock) | 模型 | 在 AWS 上运行基础模型和微调模型。 |
| [Mirascope](https://langfuse.com/docs/integrations/mirascope) | 库 | 构建 LLM 应用的 Python 工具包。 |
| [Ollama](https://langfuse.com/docs/integrations/ollama) | 模型(本地) | 在你的机器上轻松运行开源 LLM。 |
| [AutoGen](https://langfuse.com/docs/integrations/autogen) | 代理框架 | 用于构建分布式代理的开源 LLM 平台。 |
| [Flowise](https://langfuse.com/docs/integrations/flowise) | 聊天/代理界面 | 基于 JS/TS 的无代码构建器,用于定制化 LLM 流程。 |
| [Langflow](https://langfuse.com/docs/integrations/langflow) | 聊天/代理界面 | 基于 Python 的 LangChain 用户界面,采用 react-flow 设计,提供便捷的实验与原型构建体验。 |
| [Dify](https://langfuse.com/docs/integrations/dify) | 聊天/代理界面 | 带有无代码构建器的开源 LLM 应用开发平台。 |
| [OpenWebUI](https://langfuse.com/docs/integrations/openwebui) | 聊天/代理界面 | 自托管的 LLM 聊天网页界面,支持包括自托管和本地模型在内的多种 LLM 运行器。 |
| [Promptfoo](https://langfuse.com/docs/integrations/promptfoo) | 工具 | 开源 LLM 测试平台。 |
| [LobeChat](https://langfuse.com/docs/integrations/lobechat) | 聊天/代理界面 | 开源聊天机器人平台。 |
| [Vapi](https://langfuse.com/docs/integrations/vapi) | 平台 | 开源语音 AI 平台。 |
| [Inferable](https://langfuse.com/docs/integrations/other/inferable) | 代理 | 构建分布式代理的开源 LLM 平台。 |
| [Gradio](https://langfuse.com/docs/integrations/other/gradio) | 聊天/代理界面 | 开源 Python 库,可用于构建类似聊天 UI 的网页界面。 |
| [Goose](https://langfuse.com/docs/integrations/goose) | 代理 | 构建分布式代理的开源 LLM 平台。 |
| [smolagents](https://langfuse.com/docs/integrations/smolagents) | 代理 | 开源 AI 代理框架。 |
| [CrewAI](https://langfuse.com/docs/integrations/crewai) | 代理 | 多代理框架,用于实现代理之间的协作与工具调用。 |
## 🚀 快速入门
@@ -192,25 +191,25 @@ Langfuse 是一个 **开源 LLM 工程** 平台。它帮助团队协作 **开发
### 2️⃣ 记录你的第一个 LLM 调用
使用 [<code>@observe()</code> 装饰器](https://langfuse.com/docs/sdk/python/decorators) 可轻松跟踪任何 Python LLM 应用。在本快速入门中,我们还使用了 Langfuse 的 [OpenAI 集成](https://langfuse.com/docs/integrations/openai) 来自动捕获所有模型参数。
使用 [<code>@observe()</code> 装饰器](https://langfuse.com/docs/sdk/python/decorators) 可轻松跟踪任何 Python LLM 应用。在本快速入门中,我们还使用了 Langfuse 的 [OpenAI 集成](https://langfuse.com/integrations/model-providers/openai-py) 来自动捕获所有模型参数。
> [!提示]
> 不使用 OpenAI?请访问 [我们的文档](https://langfuse.com/docs/get-started#log-your-first-llm-call-to-langfuse) 了解如何记录其他模型和框架。
安装依赖:
````bash
```bash
pip install langfuse openai
````
```
配置环境变量(创建名为 **.env** 的文件):
````bash:.env
```bash:.env
LANGFUSE_SECRET_KEY="sk-lf-..."
LANGFUSE_PUBLIC_KEY="pk-lf-..."
LANGFUSE_HOST="https://cloud.langfuse.com" # 🇪🇺 欧盟区域
# LANGFUSE_HOST="https://us.cloud.langfuse.com" # 🇺🇸 美洲区域
````
```
创建示例代码(文件名:**main.py**):
@@ -230,7 +229,7 @@ def main():
return story()
main()
````
```
### 3️⃣ 在 Langfuse 中查看追踪记录
@@ -289,51 +288,51 @@ _[Langfuse 中的公共示例追踪](https://cloud.langfuse.com/project/cloramnk
以下是使用 Langfuse 的顶级开源 Python 项目,按星标数排名([来源](https://github.com/langfuse/langfuse-docs/blob/main/components-mdx/dependents)):
| 仓库 | 星数 |
| :-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----: |
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/127165244?s=40&v=4" width="20" height="20" alt=""> &nbsp; [langgenius](https://github.com/langgenius) / [dify](https://github.com/langgenius/dify) | 54865 |
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/158137808?s=40&v=4" width="20" height="20" alt=""> &nbsp; [open-webui](https://github.com/open-webui) / [open-webui](https://github.com/open-webui/open-webui) | 51531 |
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/131470832?s=40&v=4" width="20" height="20" alt=""> &nbsp; [lobehub](https://github.com/lobehub) / [lobe-chat](https://github.com/lobehub/lobe-chat) | 49003 |
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/85702467?s=40&v=4" width="20" height="20" alt=""> &nbsp; [langflow-ai](https://github.com/langflow-ai) / [langflow](https://github.com/langflow-ai/langflow) | 39093 |
| 仓库 | 星数 |
| :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ----: |
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/127165244?s=40&v=4" width="20" height="20" alt=""> &nbsp; [langgenius](https://github.com/langgenius) / [dify](https://github.com/langgenius/dify) | 54865 |
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/158137808?s=40&v=4" width="20" height="20" alt=""> &nbsp; [open-webui](https://github.com/open-webui) / [open-webui](https://github.com/open-webui/open-webui) | 51531 |
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/131470832?s=40&v=4" width="20" height="20" alt=""> &nbsp; [lobehub](https://github.com/lobehub) / [lobe-chat](https://github.com/lobehub/lobe-chat) | 49003 |
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/85702467?s=40&v=4" width="20" height="20" alt=""> &nbsp; [langflow-ai](https://github.com/langflow-ai) / [langflow](https://github.com/langflow-ai/langflow) | 39093 |
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/130722866?s=40&v=4" width="20" height="20" alt=""> &nbsp; [run-llama](https://github.com/run-llama) / [llama_index](https://github.com/run-llama/llama_index) | 37368 |
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/139558948?s=40&v=4" width="20" height="20" alt=""> &nbsp; [chatchat-space](https://github.com/chatchat-space) / [Langchain-Chatchat](https://github.com/chatchat-space/Langchain-Chatchat) | 32486 |
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/128289781?s=40&v=4" width="20" height="20" alt=""> &nbsp; [FlowiseAI](https://github.com/FlowiseAI) / [Flowise](https://github.com/FlowiseAI/Flowise) | 32448 |
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/31035808?s=40&v=4" width="20" height="20" alt=""> &nbsp; [mindsdb](https://github.com/mindsdb) / [mindsdb](https://github.com/mindsdb/mindsdb) | 26931 |
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/119600397?s=40&v=4" width="20" height="20" alt=""> &nbsp; [twentyhq](https://github.com/twentyhq) / [twenty](https://github.com/twentyhq/twenty) | 24195 |
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/60330232?s=40&v=4" width="20" height="20" alt=""> &nbsp; [PostHog](https://github.com/PostHog) / [posthog](https://github.com/PostHog/posthog) | 22618 |
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/121462774?s=40&v=4" width="20" height="20" alt=""> &nbsp; [BerriAI](https://github.com/BerriAI) / [litellm](https://github.com/BerriAI/litellm) | 15151 |
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/179202840?s=40&v=4" width="20" height="20" alt=""> &nbsp; [mediar-ai](https://github.com/mediar-ai) / [screenpipe](https://github.com/mediar-ai/screenpipe) | 11037 |
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/105877416?s=40&v=4" width="20" height="20" alt=""> &nbsp; [formbricks](https://github.com/formbricks) / [formbricks](https://github.com/formbricks/formbricks) | 9386 |
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/76263028?s=40&v=4" width="20" height="20" alt=""> &nbsp; [anthropics](https://github.com/anthropics) / [courses](https://github.com/anthropics/courses) | 8385 |
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/78410652?s=40&v=4" width="20" height="20" alt=""> &nbsp; [GreyDGL](https://github.com/GreyDGL) / [PentestGPT](https://github.com/GreyDGL/PentestGPT) | 7374 |
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/152537519?s=40&v=4" width="20" height="20" alt=""> &nbsp; [superagent-ai](https://github.com/superagent-ai) / [superagent](https://github.com/superagent-ai/superagent) | 5391 |
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/137907881?s=40&v=4" width="20" height="20" alt=""> &nbsp; [promptfoo](https://github.com/promptfoo) / [promptfoo](https://github.com/promptfoo/promptfoo) | 4976 |
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/157326433?s=40&v=4" width="20" height="20" alt=""> &nbsp; [onlook-dev](https://github.com/onlook-dev) / [onlook](https://github.com/onlook-dev/onlook) | 4141 |
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/7250217?s=40&v=4" width="20" height="20" alt=""> &nbsp; [Canner](https://github.com/Canner) / [WrenAI](https://github.com/Canner/WrenAI) | 2526 |
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/11855343?s=40&v=4" width="20" height="20" alt=""> &nbsp; [pingcap](https://github.com/pingcap) / [autoflow](https://github.com/pingcap/autoflow) | 2061 |
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/85268109?s=40&v=4" width="20" height="20" alt=""> &nbsp; [MLSysOps](https://github.com/MLSysOps) / [MLE-agent](https://github.com/MLSysOps/MLE-agent) | 1161 |
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/158137808?s=40&v=4" width="20" height="20" alt=""> &nbsp; [open-webui](https://github.com/open-webui) / [pipelines](https://github.com/open-webui/pipelines) | 1100 |
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/18422723?s=40&v=4" width="20" height="20" alt=""> &nbsp; [alishobeiri](https://github.com/alishobeiri) / [thread](https://github.com/alishobeiri/thread) | 1074 |
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/125468716?s=40&v=4" width="20" height="20" alt=""> &nbsp; [topoteretes](https://github.com/topoteretes) / [cognee](https://github.com/topoteretes/cognee) | 971 |
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/188657705?s=40&v=4" width="20" height="20" alt=""> &nbsp; [bRAGAI](https://github.com/bRAGAI) / [bRAG-langchain](https://github.com/bRAGAI/bRAG-langchain) | 823 |
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/169500408?s=40&v=4" width="20" height="20" alt=""> &nbsp; [opslane](https://github.com/opslane) / [opslane](https://github.com/opslane/opslane) | 677 |
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/151867818?s=40&v=4" width="20" height="20" alt=""> &nbsp; [dynamiq-ai](https://github.com/dynamiq-ai) / [dynamiq](https://github.com/dynamiq-ai/dynamiq) | 639 |
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/48585267?s=40&v=4" width="20" height="20" alt=""> &nbsp; [theopenconversationkit](https://github.com/theopenconversationkit) / [tock](https://github.com/theopenconversationkit/tock) | 514 |
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/20493493?s=40&v=4" width="20" height="20" alt=""> &nbsp; [andysingal](https://github.com/andysingal) / [llm-course](https://github.com/andysingal/llm-course) | 394 |
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/132396805?s=40&v=4" width="20" height="20" alt=""> &nbsp; [phospho-app](https://github.com/phospho-app) / [phospho](https://github.com/phospho-app/phospho) | 384 |
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/178644984?s=40&v=4" width="20" height="20" alt=""> &nbsp; [sentient-engineering](https://github.com/sentient-engineering) / [agent-q](https://github.com/sentient-engineering/agent-q) | 370 |
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/168552753?s=40&v=4" width="20" height="20" alt=""> &nbsp; [sql-agi](https://github.com/sql-agi) / [DB-GPT](https://github.com/sql-agi/DB-GPT) | 324 |
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/60330232?s=40&v=4" width="20" height="20" alt=""> &nbsp; [PostHog](https://github.com/PostHog) / [posthog-foss](https://github.com/PostHog/posthog-foss) | 305 |
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/154247157?s=40&v=4" width="20" height="20" alt=""> &nbsp; [vespperhq](https://github.com/vespperhq) / [vespper](https://github.com/vespperhq/vespper) | 304 |
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/185116535?s=40&v=4" width="20" height="20" alt=""> &nbsp; [block](https://github.com/block) / [goose](https://github.com/block/goose) | 295 |
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/609489?s=40&v=4" width="20" height="20" alt=""> &nbsp; [aorwall](https://github.com/aorwall) / [moatless-tools](https://github.com/aorwall/moatless-tools) | 291 |
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/2357342?s=40&v=4" width="20" height="20" alt=""> &nbsp; [dmayboroda](https://github.com/dmayboroda) / [minima](https://github.com/dmayboroda/minima) | 221 |
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/66303003?s=40&v=4" width="20" height="20" alt=""> &nbsp; [RobotecAI](https://github.com/RobotecAI) / [rai](https://github.com/RobotecAI/rai) | 172 |
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/148684274?s=40&v=4" width="20" height="20" alt=""> &nbsp; [i-am-alice](https://github.com/i-am-alice) / [3rd-devs](https://github.com/i-am-alice/3rd-devs) | 148 |
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/171735272?s=40&v=4" width="20" height="20" alt=""> &nbsp; [8090-inc](https://github.com/8090-inc) / [xrx-sample-apps](https://github.com/8090-inc/xrx-sample-apps) | 138 |
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/104478511?s=40&v=4" width="20" height="20" alt=""> &nbsp; [babelcloud](https://github.com/babelcloud) / [LLM-RGB](https://github.com/babelcloud/LLM-RGB) | 135 |
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/15125613?s=40&v=4" width="20" height="20" alt=""> &nbsp; [souzatharsis](https://github.com/souzatharsis) / [tamingLLMs](https://github.com/souzatharsis/tamingLLMs) | 129 |
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/169401942?s=40&v=4" width="20" height="20" alt=""> &nbsp; [LibreChat-AI](https://github.com/LibreChat-AI) / [librechat.ai](https://github.com/LibreChat-AI/librechat.ai) | 128 |
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/128289781?s=40&v=4" width="20" height="20" alt=""> &nbsp; [FlowiseAI](https://github.com/FlowiseAI) / [Flowise](https://github.com/FlowiseAI/Flowise) | 32448 |
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/31035808?s=40&v=4" width="20" height="20" alt=""> &nbsp; [mindsdb](https://github.com/mindsdb) / [mindsdb](https://github.com/mindsdb/mindsdb) | 26931 |
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/119600397?s=40&v=4" width="20" height="20" alt=""> &nbsp; [twentyhq](https://github.com/twentyhq) / [twenty](https://github.com/twentyhq/twenty) | 24195 |
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/60330232?s=40&v=4" width="20" height="20" alt=""> &nbsp; [PostHog](https://github.com/PostHog) / [posthog](https://github.com/PostHog/posthog) | 22618 |
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/121462774?s=40&v=4" width="20" height="20" alt=""> &nbsp; [BerriAI](https://github.com/BerriAI) / [litellm](https://github.com/BerriAI/litellm) | 15151 |
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/179202840?s=40&v=4" width="20" height="20" alt=""> &nbsp; [mediar-ai](https://github.com/mediar-ai) / [screenpipe](https://github.com/mediar-ai/screenpipe) | 11037 |
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/105877416?s=40&v=4" width="20" height="20" alt=""> &nbsp; [formbricks](https://github.com/formbricks) / [formbricks](https://github.com/formbricks/formbricks) | 9386 |
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/76263028?s=40&v=4" width="20" height="20" alt=""> &nbsp; [anthropics](https://github.com/anthropics) / [courses](https://github.com/anthropics/courses) | 8385 |
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/78410652?s=40&v=4" width="20" height="20" alt=""> &nbsp; [GreyDGL](https://github.com/GreyDGL) / [PentestGPT](https://github.com/GreyDGL/PentestGPT) | 7374 |
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/152537519?s=40&v=4" width="20" height="20" alt=""> &nbsp; [superagent-ai](https://github.com/superagent-ai) / [superagent](https://github.com/superagent-ai/superagent) | 5391 |
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/137907881?s=40&v=4" width="20" height="20" alt=""> &nbsp; [promptfoo](https://github.com/promptfoo) / [promptfoo](https://github.com/promptfoo/promptfoo) | 4976 |
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/157326433?s=40&v=4" width="20" height="20" alt=""> &nbsp; [onlook-dev](https://github.com/onlook-dev) / [onlook](https://github.com/onlook-dev/onlook) | 4141 |
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/7250217?s=40&v=4" width="20" height="20" alt=""> &nbsp; [Canner](https://github.com/Canner) / [WrenAI](https://github.com/Canner/WrenAI) | 2526 |
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/11855343?s=40&v=4" width="20" height="20" alt=""> &nbsp; [pingcap](https://github.com/pingcap) / [autoflow](https://github.com/pingcap/autoflow) | 2061 |
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/85268109?s=40&v=4" width="20" height="20" alt=""> &nbsp; [MLSysOps](https://github.com/MLSysOps) / [MLE-agent](https://github.com/MLSysOps/MLE-agent) | 1161 |
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/158137808?s=40&v=4" width="20" height="20" alt=""> &nbsp; [open-webui](https://github.com/open-webui) / [pipelines](https://github.com/open-webui/pipelines) | 1100 |
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/18422723?s=40&v=4" width="20" height="20" alt=""> &nbsp; [alishobeiri](https://github.com/alishobeiri) / [thread](https://github.com/alishobeiri/thread) | 1074 |
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/125468716?s=40&v=4" width="20" height="20" alt=""> &nbsp; [topoteretes](https://github.com/topoteretes) / [cognee](https://github.com/topoteretes/cognee) | 971 |
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/188657705?s=40&v=4" width="20" height="20" alt=""> &nbsp; [bRAGAI](https://github.com/bRAGAI) / [bRAG-langchain](https://github.com/bRAGAI/bRAG-langchain) | 823 |
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/169500408?s=40&v=4" width="20" height="20" alt=""> &nbsp; [opslane](https://github.com/opslane) / [opslane](https://github.com/opslane/opslane) | 677 |
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/151867818?s=40&v=4" width="20" height="20" alt=""> &nbsp; [dynamiq-ai](https://github.com/dynamiq-ai) / [dynamiq](https://github.com/dynamiq-ai/dynamiq) | 639 |
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/48585267?s=40&v=4" width="20" height="20" alt=""> &nbsp; [theopenconversationkit](https://github.com/theopenconversationkit) / [tock](https://github.com/theopenconversationkit/tock) | 514 |
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/20493493?s=40&v=4" width="20" height="20" alt=""> &nbsp; [andysingal](https://github.com/andysingal) / [llm-course](https://github.com/andysingal/llm-course) | 394 |
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/132396805?s=40&v=4" width="20" height="20" alt=""> &nbsp; [phospho-app](https://github.com/phospho-app) / [phospho](https://github.com/phospho-app/phospho) | 384 |
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/178644984?s=40&v=4" width="20" height="20" alt=""> &nbsp; [sentient-engineering](https://github.com/sentient-engineering) / [agent-q](https://github.com/sentient-engineering/agent-q) | 370 |
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/168552753?s=40&v=4" width="20" height="20" alt=""> &nbsp; [sql-agi](https://github.com/sql-agi) / [DB-GPT](https://github.com/sql-agi/DB-GPT) | 324 |
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/60330232?s=40&v=4" width="20" height="20" alt=""> &nbsp; [PostHog](https://github.com/PostHog) / [posthog-foss](https://github.com/PostHog/posthog-foss) | 305 |
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/154247157?s=40&v=4" width="20" height="20" alt=""> &nbsp; [vespperhq](https://github.com/vespperhq) / [vespper](https://github.com/vespperhq/vespper) | 304 |
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/185116535?s=40&v=4" width="20" height="20" alt=""> &nbsp; [block](https://github.com/block) / [goose](https://github.com/block/goose) | 295 |
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/609489?s=40&v=4" width="20" height="20" alt=""> &nbsp; [aorwall](https://github.com/aorwall) / [moatless-tools](https://github.com/aorwall/moatless-tools) | 291 |
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/2357342?s=40&v=4" width="20" height="20" alt=""> &nbsp; [dmayboroda](https://github.com/dmayboroda) / [minima](https://github.com/dmayboroda/minima) | 221 |
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/66303003?s=40&v=4" width="20" height="20" alt=""> &nbsp; [RobotecAI](https://github.com/RobotecAI) / [rai](https://github.com/RobotecAI/rai) | 172 |
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/148684274?s=40&v=4" width="20" height="20" alt=""> &nbsp; [i-am-alice](https://github.com/i-am-alice) / [3rd-devs](https://github.com/i-am-alice/3rd-devs) | 148 |
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/171735272?s=40&v=4" width="20" height="20" alt=""> &nbsp; [8090-inc](https://github.com/8090-inc) / [xrx-sample-apps](https://github.com/8090-inc/xrx-sample-apps) | 138 |
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/104478511?s=40&v=4" width="20" height="20" alt=""> &nbsp; [babelcloud](https://github.com/babelcloud) / [LLM-RGB](https://github.com/babelcloud/LLM-RGB) | 135 |
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/15125613?s=40&v=4" width="20" height="20" alt=""> &nbsp; [souzatharsis](https://github.com/souzatharsis) / [tamingLLMs](https://github.com/souzatharsis/tamingLLMs) | 129 |
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/169401942?s=40&v=4" width="20" height="20" alt=""> &nbsp; [LibreChat-AI](https://github.com/LibreChat-AI) / [librechat.ai](https://github.com/LibreChat-AI/librechat.ai) | 128 |
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/51827949?s=40&v=4" width="20" height="20" alt=""> &nbsp; [deepset-ai](https://github.com/deepset-ai) / [haystack-core-integrations](https://github.com/deepset-ai/haystack-core-integrations) | 126 |
## 🔒 安全与隐私
@@ -352,4 +351,7 @@ _[Langfuse 中的公共示例追踪](https://cloud.langfuse.com/project/cloramnk
所有数据均不会与第三方共享,也不包含任何敏感信息。我们对这一过程保持高度透明,你可以在 [此处](/web/src/features/telemetry/index.ts) 查看我们收集的具体数据。
你可以通过设置 `TELEMETRY_ENABLED=false` 来选择退出。
```
```
+80 -80
View File
@@ -84,15 +84,15 @@ Langfuseは**数分でセルフホスト可能**で、**多くの実績を持つ
複雑なログやユーザーセッションを解析・デバッグできます。
インタラクティブな[デモ](https://langfuse.com/docs/demo)で動作を確認してください。
- **[プロンプト管理](https://langfuse.com/docs/prompts/get-started):**
- **[プロンプト管理](https://langfuse.com/docs/prompt-management/get-started):**
プロンプトを一元管理し、バージョン管理しながら共同で改善を行えます。
サーバーおよびクライアント側で強力なキャッシングを行うため、アプリケーションのレイテンシを増やすことなくプロンプトの改良が可能です。
- **[評価](https://langfuse.com/docs/scores/overview):**
- **[評価](https://langfuse.com/docs/evaluation/overview):**
評価はLLMアプリケーション開発ワークフローの要であり、Langfuseは多様なニーズに対応します。
LLMを判定者として用いる方法、ユーザーフィードバックの収集、手動によるラベリング、API/SDKを通じたカスタム評価パイプラインをサポートします。
- **[データセット](https://langfuse.com/docs/datasets/overview):**
- **[データセット](https://langfuse.com/docs/evaluation/dataset-runs/datasets):**
LLMアプリケーション評価用のテストセットやベンチマークを構築できます。
継続的な改善、事前デプロイテスト、構造化された実験、柔軟な評価、さらにLangChainやLlamaIndexなどとのシームレスな統合をサポートします。
@@ -151,40 +151,40 @@ Langfuseチームによるマネージドデプロイメント。充実した無
### 主なインテグレーション:
| インテグレーション | 対応言語・環境 | 説明 |
| ----------------------------------------------------------------------------------- | ------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------- |
| [SDK](https://langfuse.com/docs/sdk) | Python, JS/TS | SDKを利用して手動でインストゥルメンテーションを実装し、完全な柔軟性を提供します。 |
| [OpenAI](https://langfuse.com/docs/integrations/openai) | Python, JS/TS | OpenAI SDKのドロップイン置換による自動インストゥルメンテーションを実現します。 |
| [Langchain](https://langfuse.com/docs/integrations/langchain) | Python, JS/TS | Langchainアプリケーションにコールバックハンドラーを渡すことで自動的に計測します。 |
| [LlamaIndex](https://langfuse.com/docs/integrations/llama-index/get-started) | Python | LlamaIndexのコールバックシステムを介して自動的にインストゥルメントします。 |
| [Haystack](https://langfuse.com/docs/integrations/haystack) | Python | Haystackのコンテンツトレースシステムを利用した自動インストゥルメンテーションを実現します。 |
| [LiteLLM](https://langfuse.com/docs/integrations/litellm) | Python, JS/TS (proxy only)| GPTのドロップイン置換として任意のLLMを使用できます。Azure、OpenAI、Cohere、Anthropic、Ollama、VLLM、Sagemaker、HuggingFace、Replicate100+ LLM)に対応。 |
| [Vercel AI SDK](https://langfuse.com/docs/integrations/vercel-ai-sdk) | JS/TS | React、Next.js、Vue、Svelte、Node.jsを使用してAI搭載アプリケーションの構築を支援するTypeScriptツールキットです。 |
| [API](https://langfuse.com/docs/api) | | 公開APIを直接呼び出すことが可能です。OpenAPI仕様も利用できます。 |
| インテグレーション | 対応言語・環境 | 説明 |
| ---------------------------------------------------------------------------- | -------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------- |
| [SDK](https://langfuse.com/docs/sdk) | Python, JS/TS | SDKを利用して手動でインストゥルメンテーションを実装し、完全な柔軟性を提供します。 |
| [OpenAI](https://langfuse.com/integrations/model-providers/openai-py) | Python, JS/TS | OpenAI SDKのドロップイン置換による自動インストゥルメンテーションを実現します。 |
| [Langchain](https://langfuse.com/docs/integrations/langchain) | Python, JS/TS | Langchainアプリケーションにコールバックハンドラーを渡すことで自動的に計測します。 |
| [LlamaIndex](https://langfuse.com/docs/integrations/llama-index/get-started) | Python | LlamaIndexのコールバックシステムを介して自動的にインストゥルメントします。 |
| [Haystack](https://langfuse.com/docs/integrations/haystack) | Python | Haystackのコンテンツトレースシステムを利用した自動インストゥルメンテーションを実現します。 |
| [LiteLLM](https://langfuse.com/docs/integrations/litellm) | Python, JS/TS (proxy only) | GPTのドロップイン置換として任意のLLMを使用できます。Azure、OpenAI、Cohere、Anthropic、Ollama、VLLM、Sagemaker、HuggingFace、Replicate100+ LLM)に対応。 |
| [Vercel AI SDK](https://langfuse.com/docs/integrations/vercel-ai-sdk) | JS/TS | React、Next.js、Vue、Svelte、Node.jsを使用してAI搭載アプリケーションの構築を支援するTypeScriptツールキットです。 |
| [API](https://langfuse.com/docs/api) | | 公開APIを直接呼び出すことが可能です。OpenAPI仕様も利用できます。 |
### Langfuseと統合されているパッケージ:
| 名前 | タイプ | 説明 |
| ------------------------------------------------------------------------- | ------------------ | --------------------------------------------------------------------------------------------------------- |
| [Instructor](https://langfuse.com/docs/integrations/instructor) | ライブラリ | 構造化されたLLM出力(JSON、Pydantic)を取得するためのライブラリ |
| [DSPy](https://langfuse.com/docs/integrations/dspy) | ライブラリ | LLMプロンプトや重み付けを体系的に最適化するためのフレームワーク |
| [Mirascope](https://langfuse.com/docs/integrations/mirascope) | ライブラリ | LLMアプリケーション構築用のPythonツールキット |
| [Ollama](https://langfuse.com/docs/integrations/ollama) | モデル(ローカル) | オープンソースLLMを手軽にローカルで実行するためのツール |
| [Amazon Bedrock](https://langfuse.com/docs/integrations/amazon-bedrock) | モデル | AWS上でファウンデーションモデルやファインチューニング済みモデルを実行 |
| [Google VertexAI and Gemini](https://langfuse.com/docs/integrations/google-vertex-ai) | モデル | Google上でファウンデーションモデルやファインチューニング済みモデルを実行 |
| [AutoGen](https://langfuse.com/docs/integrations/autogen) | エージェントフレームワーク | 分散型エージェント構築のためのオープンソースLLMプラットフォーム |
| [Flowise](https://langfuse.com/docs/integrations/flowise) | チャット/エージェント UI | JS/TSのノーコードビルダーで、カスタマイズ可能なLLMフローを構築 |
| [Langflow](https://langfuse.com/docs/integrations/langflow) | チャット/エージェント UI | PythonベースのUIで、react-flowを用いてLangChainの実験やプロトタイピングを容易に実現 |
| [Dify](https://langfuse.com/docs/integrations/dify) | チャット/エージェント UI | ノーコードでLLMアプリ開発が可能なオープンソースプラットフォーム |
| [OpenWebUI](https://langfuse.com/docs/integrations/openwebui) | チャット/エージェント UI | 自前ホストおよびローカルモデルに対応するLLMチャットWeb UI |
| [Promptfoo](https://langfuse.com/docs/integrations/promptfoo) | ツール | オープンソースのLLMテストプラットフォーム |
| [LobeChat](https://langfuse.com/docs/integrations/lobechat) | チャット/エージェント UI | オープンソースのチャットボットプラットフォーム |
| [Vapi](https://langfuse.com/docs/integrations/vapi) | プラットフォーム | オープンソースの音声AIプラットフォーム |
| [Inferable](https://langfuse.com/docs/integrations/other/inferable) | エージェント | 分散型エージェント構築のためのオープンソースLLMプラットフォーム |
| [Gradio](https://langfuse.com/docs/integrations/other/gradio) | チャット/エージェント UI | チャットUIなどのWebインターフェース構築のためのオープンソースPythonライブラリ |
| [Goose](https://langfuse.com/docs/integrations/goose) | エージェント | 分散型エージェント構築のためのオープンソースLLMプラットフォーム |
| [smolagents](https://langfuse.com/docs/integrations/smolagents) | エージェント | オープンソースのAIエージェントフレームワーク |
| [CrewAI](https://langfuse.com/docs/integrations/crewai) | エージェント | エージェントの協調とツール利用を実現するマルチエージェントフレームワーク |
| 名前 | タイプ | 説明 |
| ------------------------------------------------------------------------------------- | -------------------------- | ----------------------------------------------------------------------------------- |
| [Instructor](https://langfuse.com/docs/integrations/instructor) | ライブラリ | 構造化されたLLM出力(JSON、Pydantic)を取得するためのライブラリ |
| [DSPy](https://langfuse.com/docs/integrations/dspy) | ライブラリ | LLMプロンプトや重み付けを体系的に最適化するためのフレームワーク |
| [Mirascope](https://langfuse.com/docs/integrations/mirascope) | ライブラリ | LLMアプリケーション構築用のPythonツールキット |
| [Ollama](https://langfuse.com/docs/integrations/ollama) | モデル(ローカル) | オープンソースLLMを手軽にローカルで実行するためのツール |
| [Amazon Bedrock](https://langfuse.com/docs/integrations/amazon-bedrock) | モデル | AWS上でファウンデーションモデルやファインチューニング済みモデルを実行 |
| [Google VertexAI and Gemini](https://langfuse.com/docs/integrations/google-vertex-ai) | モデル | Google上でファウンデーションモデルやファインチューニング済みモデルを実行 |
| [AutoGen](https://langfuse.com/docs/integrations/autogen) | エージェントフレームワーク | 分散型エージェント構築のためのオープンソースLLMプラットフォーム |
| [Flowise](https://langfuse.com/docs/integrations/flowise) | チャット/エージェント UI | JS/TSのノーコードビルダーで、カスタマイズ可能なLLMフローを構築 |
| [Langflow](https://langfuse.com/docs/integrations/langflow) | チャット/エージェント UI | PythonベースのUIで、react-flowを用いてLangChainの実験やプロトタイピングを容易に実現 |
| [Dify](https://langfuse.com/docs/integrations/dify) | チャット/エージェント UI | ノーコードでLLMアプリ開発が可能なオープンソースプラットフォーム |
| [OpenWebUI](https://langfuse.com/docs/integrations/openwebui) | チャット/エージェント UI | 自前ホストおよびローカルモデルに対応するLLMチャットWeb UI |
| [Promptfoo](https://langfuse.com/docs/integrations/promptfoo) | ツール | オープンソースのLLMテストプラットフォーム |
| [LobeChat](https://langfuse.com/docs/integrations/lobechat) | チャット/エージェント UI | オープンソースのチャットボットプラットフォーム |
| [Vapi](https://langfuse.com/docs/integrations/vapi) | プラットフォーム | オープンソースの音声AIプラットフォーム |
| [Inferable](https://langfuse.com/docs/integrations/other/inferable) | エージェント | 分散型エージェント構築のためのオープンソースLLMプラットフォーム |
| [Gradio](https://langfuse.com/docs/integrations/other/gradio) | チャット/エージェント UI | チャットUIなどのWebインターフェース構築のためのオープンソースPythonライブラリ |
| [Goose](https://langfuse.com/docs/integrations/goose) | エージェント | 分散型エージェント構築のためのオープンソースLLMプラットフォーム |
| [smolagents](https://langfuse.com/docs/integrations/smolagents) | エージェント | オープンソースのAIエージェントフレームワーク |
| [CrewAI](https://langfuse.com/docs/integrations/crewai) | エージェント | エージェントの協調とツール利用を実現するマルチエージェントフレームワーク |
## 🚀 クイックスタート
@@ -200,7 +200,7 @@ Langfuseチームによるマネージドデプロイメント。充実した無
### 2️⃣ 初めてのLLM呼び出しのログ記録
[`@observe()` デコレーター](https://langfuse.com/docs/sdk/python/decorators)を利用することで、任意のPython製LLMアプリケーションのトレースが簡単に行えます。
このクイックスタートでは、Langfuseの[OpenAI統合](https://langfuse.com/docs/integrations/openai)を使用して、全てのモデルパラメータを自動で取得します。
このクイックスタートでは、Langfuseの[OpenAI統合](https://langfuse.com/integrations/model-providers/openai-py)を使用して、全てのモデルパラメータを自動で取得します。
> [!TIP]
> OpenAIを利用していない場合は、[こちらのドキュメント](https://langfuse.com/docs/get-started#log-your-first-llm-call-to-langfuse)で、他のモデルやフレームワークのログ記録方法をご確認ください。
@@ -295,51 +295,51 @@ _[Langfuseの公開トレース例](https://cloud.langfuse.com/project/cloramnkj
Langfuseを利用している主要なオープンソースPythonプロジェクト(スター数順): ([出典](https://github.com/langfuse/langfuse-docs/blob/main/components-mdx/dependents))
| リポジトリ | スター |
| :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ----: |
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/127165244?s=40&v=4" width="20" height="20" alt=""> &nbsp; [langgenius](https://github.com/langgenius) / [dify](https://github.com/langgenius/dify) | 54865 |
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/158137808?s=40&v=4" width="20" height="20" alt=""> &nbsp; [open-webui](https://github.com/open-webui) / [open-webui](https://github.com/open-webui/open-webui) | 51531 |
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/131470832?s=40&v=4" width="20" height="20" alt=""> &nbsp; [lobehub](https://github.com/lobehub) / [lobe-chat](https://github.com/lobehub/lobe-chat) | 49003 |
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/85702467?s=40&v=4" width="20" height="20" alt=""> &nbsp; [langflow-ai](https://github.com/langflow-ai) / [langflow](https://github.com/langflow-ai/langflow) | 39093 |
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/130722866?s=40&v=4" width="20" height="20" alt=""> &nbsp; [run-llama](https://github.com/run-llama) / [llama_index](https://github.com/run-llama/llama_index) | 37368 |
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/139558948?s=40&v=4" width="20" height="20" alt=""> &nbsp; [chatchat-space](https://github.com/chatchat-space) / [Langchain-Chatchat](https://github.com/chatchat-space/Langchain-Chatchat) | 32486 |
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/128289781?s=40&v=4" width="20" height="20" alt=""> &nbsp; [FlowiseAI](https://github.com/FlowiseAI) / [Flowise](https://github.com/FlowiseAI/Flowise) | 32448 |
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/31035808?s=40&v=4" width="20" height="20" alt=""> &nbsp; [mindsdb](https://github.com/mindsdb) / [mindsdb](https://github.com/mindsdb/mindsdb) | 26931 |
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/119600397?s=40&v=4" width="20" height="20" alt=""> &nbsp; [twentyhq](https://github.com/twentyhq) / [twenty](https://github.com/twentyhq/twenty) | 24195 |
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/60330232?s=40&v=4" width="20" height="20" alt=""> &nbsp; [PostHog](https://github.com/PostHog) / [posthog](https://github.com/PostHog/posthog) | 22618 |
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/121462774?s=40&v=4" width="20" height="20" alt=""> &nbsp; [BerriAI](https://github.com/BerriAI) / [litellm](https://github.com/BerriAI/litellm) | 15151 |
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/179202840?s=40&v=4" width="20" height="20" alt=""> &nbsp; [mediar-ai](https://github.com/mediar-ai) / [screenpipe](https://github.com/mediar-ai/screenpipe) | 11037 |
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/105877416?s=40&v=4" width="20" height="20" alt=""> &nbsp; [formbricks](https://github.com/formbricks) / [formbricks](https://github.com/formbricks/formbricks) | 9386 |
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/76263028?s=40&v=4" width="20" height="20" alt=""> &nbsp; [anthropics](https://github.com/anthropics) / [courses](https://github.com/anthropics/courses) | 8385 |
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/78410652?s=40&v=4" width="20" height="20" alt=""> &nbsp; [GreyDGL](https://github.com/GreyDGL) / [PentestGPT](https://github.com/GreyDGL/PentestGPT) | 7374 |
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/152537519?s=40&v=4" width="20" height="20" alt=""> &nbsp; [superagent-ai](https://github.com/superagent-ai) / [superagent](https://github.com/superagent-ai/superagent) | 5391 |
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/137907881?s=40&v=4" width="20" height="20" alt=""> &nbsp; [promptfoo](https://github.com/promptfoo) / [promptfoo](https://github.com/promptfoo/promptfoo) | 4976 |
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/157326433?s=40&v=4" width="20" height="20" alt=""> &nbsp; [onlook-dev](https://github.com/onlook-dev) / [onlook](https://github.com/onlook-dev/onlook) | 4141 |
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/7250217?s=40&v=4" width="20" height="20" alt=""> &nbsp; [Canner](https://github.com/Canner) / [WrenAI](https://github.com/Canner/WrenAI) | 2526 |
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/11855343?s=40&v=4" width="20" height="20" alt=""> &nbsp; [pingcap](https://github.com/pingcap) / [autoflow](https://github.com/pingcap/autoflow) | 2061 |
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/85268109?s=40&v=4" width="20" height="20" alt=""> &nbsp; [MLSysOps](https://github.com/MLSysOps) / [MLE-agent](https://github.com/MLSysOps/MLE-agent) | 1161 |
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/158137808?s=40&v=4" width="20" height="20" alt=""> &nbsp; [open-webui](https://github.com/open-webui) / [pipelines](https://github.com/open-webui/pipelines) | 1100 |
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/18422723?s=40&v=4" width="20" height="20" alt=""> &nbsp; [alishobeiri](https://github.com/alishobeiri) / [thread](https://github.com/alishobeiri/thread) | 1074 |
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/125468716?s=40&v=4" width="20" height="20" alt=""> &nbsp; [topoteretes](https://github.com/topoteretes) / [cognee](https://github.com/topoteretes/cognee) | 971 |
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/188657705?s=40&v=4" width="20" height="20" alt=""> &nbsp; [bRAGAI](https://github.com/bRAGAI) / [bRAG-langchain](https://github.com/bRAGAI/bRAG-langchain) | 823 |
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/169500408?s=40&v=4" width="20" height="20" alt=""> &nbsp; [opslane](https://github.com/opslane) / [opslane](https://github.com/opslane/opslane) | 677 |
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/151867818?s=40&v=4" width="20" height="20" alt=""> &nbsp; [dynamiq-ai](https://github.com/dynamiq-ai) / [dynamiq](https://github.com/dynamiq-ai/dynamiq) | 639 |
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/48585267?s=40&v=4" width="20" height="20" alt=""> &nbsp; [theopenconversationkit](https://github.com/theopenconversationkit) / [tock](https://github.com/theopenconversationkit/tock) | 514 |
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/20493493?s=40&v=4" width="20" height="20" alt=""> &nbsp; [andysingal](https://github.com/andysingal) / [llm-course](https://github.com/andysingal/llm-course) | 394 |
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/132396805?s=40&v=4" width="20" height="20" alt=""> &nbsp; [phospho-app](https://github.com/phospho-app) / [phospho](https://github.com/phospho-app/phospho) | 384 |
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/178644984?s=40&v=4" width="20" height="20" alt=""> &nbsp; [sentient-engineering](https://github.com/sentient-engineering) / [agent-q](https://github.com/sentient-engineering/agent-q) | 370 |
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/168552753?s=40&v=4" width="20" height="20" alt=""> &nbsp; [sql-agi](https://github.com/sql-agi) / [DB-GPT](https://github.com/sql-agi/DB-GPT) | 324 |
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/60330232?s=40&v=4" width="20" height="20" alt=""> &nbsp; [PostHog](https://github.com/PostHog) / [posthog-foss](https://github.com/PostHog/posthog-foss) | 305 |
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/154247157?s=40&v=4" width="20" height="20" alt=""> &nbsp; [vespperhq](https://github.com/vespperhq) / [vespper](https://github.com/vespperhq/vespper) | 304 |
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/185116535?s=40&v=4" width="20" height="20" alt=""> &nbsp; [block](https://github.com/block) / [goose](https://github.com/block/goose) | 295 |
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/609489?s=40&v=4" width="20" height="20" alt=""> &nbsp; [aorwall](https://github.com/aorwall) / [moatless-tools](https://github.com/aorwall/moatless-tools) | 291 |
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/2357342?s=40&v=4" width="20" height="20" alt=""> &nbsp; [dmayboroda](https://github.com/dmayboroda) / [minima](https://github.com/dmayboroda/minima) | 221 |
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/66303003?s=40&v=4" width="20" height="20" alt=""> &nbsp; [RobotecAI](https://github.com/RobotecAI) / [rai](https://github.com/RobotecAI/rai) | 172 |
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/148684274?s=40&v=4" width="20" height="20" alt=""> &nbsp; [i-am-alice](https://github.com/i-am-alice) / [3rd-devs](https://github.com/i-am-alice/3rd-devs) | 148 |
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/171735272?s=40&v=4" width="20" height="20" alt=""> &nbsp; [8090-inc](https://github.com/8090-inc) / [xrx-sample-apps](https://github.com/8090-inc/xrx-sample-apps) | 138 |
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/104478511?s=40&v=4" width="20" height="20" alt=""> &nbsp; [babelcloud](https://github.com/babelcloud) / [LLM-RGB](https://github.com/babelcloud/LLM-RGB) | 135 |
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/15125613?s=40&v=4" width="20" height="20" alt=""> &nbsp; [souzatharsis](https://github.com/souzatharsis) / [tamingLLMs](https://github.com/souzatharsis/tamingLLMs) | 129 |
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/169401942?s=40&v=4" width="20" height="20" alt=""> &nbsp; [LibreChat-AI](https://github.com/LibreChat-AI) / [librechat.ai](https://github.com/LibreChat-AI/librechat.ai) | 128 |
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/51827949?s=40&v=4" width="20" height="20" alt=""> &nbsp; [deepset-ai](https://github.com/deepset-ai) / [haystack-core-integrations](https://github.com/deepset-ai/haystack-core-integrations) | 126 |
| :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | -----: |
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/127165244?s=40&v=4" width="20" height="20" alt=""> &nbsp; [langgenius](https://github.com/langgenius) / [dify](https://github.com/langgenius/dify) | 54865 |
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/158137808?s=40&v=4" width="20" height="20" alt=""> &nbsp; [open-webui](https://github.com/open-webui) / [open-webui](https://github.com/open-webui/open-webui) | 51531 |
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/131470832?s=40&v=4" width="20" height="20" alt=""> &nbsp; [lobehub](https://github.com/lobehub) / [lobe-chat](https://github.com/lobehub/lobe-chat) | 49003 |
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/85702467?s=40&v=4" width="20" height="20" alt=""> &nbsp; [langflow-ai](https://github.com/langflow-ai) / [langflow](https://github.com/langflow-ai/langflow) | 39093 |
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/130722866?s=40&v=4" width="20" height="20" alt=""> &nbsp; [run-llama](https://github.com/run-llama) / [llama_index](https://github.com/run-llama/llama_index) | 37368 |
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/139558948?s=40&v=4" width="20" height="20" alt=""> &nbsp; [chatchat-space](https://github.com/chatchat-space) / [Langchain-Chatchat](https://github.com/chatchat-space/Langchain-Chatchat) | 32486 |
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/128289781?s=40&v=4" width="20" height="20" alt=""> &nbsp; [FlowiseAI](https://github.com/FlowiseAI) / [Flowise](https://github.com/FlowiseAI/Flowise) | 32448 |
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/31035808?s=40&v=4" width="20" height="20" alt=""> &nbsp; [mindsdb](https://github.com/mindsdb) / [mindsdb](https://github.com/mindsdb/mindsdb) | 26931 |
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/119600397?s=40&v=4" width="20" height="20" alt=""> &nbsp; [twentyhq](https://github.com/twentyhq) / [twenty](https://github.com/twentyhq/twenty) | 24195 |
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/60330232?s=40&v=4" width="20" height="20" alt=""> &nbsp; [PostHog](https://github.com/PostHog) / [posthog](https://github.com/PostHog/posthog) | 22618 |
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/121462774?s=40&v=4" width="20" height="20" alt=""> &nbsp; [BerriAI](https://github.com/BerriAI) / [litellm](https://github.com/BerriAI/litellm) | 15151 |
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/179202840?s=40&v=4" width="20" height="20" alt=""> &nbsp; [mediar-ai](https://github.com/mediar-ai) / [screenpipe](https://github.com/mediar-ai/screenpipe) | 11037 |
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/105877416?s=40&v=4" width="20" height="20" alt=""> &nbsp; [formbricks](https://github.com/formbricks) / [formbricks](https://github.com/formbricks/formbricks) | 9386 |
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/76263028?s=40&v=4" width="20" height="20" alt=""> &nbsp; [anthropics](https://github.com/anthropics) / [courses](https://github.com/anthropics/courses) | 8385 |
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/78410652?s=40&v=4" width="20" height="20" alt=""> &nbsp; [GreyDGL](https://github.com/GreyDGL) / [PentestGPT](https://github.com/GreyDGL/PentestGPT) | 7374 |
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/152537519?s=40&v=4" width="20" height="20" alt=""> &nbsp; [superagent-ai](https://github.com/superagent-ai) / [superagent](https://github.com/superagent-ai/superagent) | 5391 |
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/137907881?s=40&v=4" width="20" height="20" alt=""> &nbsp; [promptfoo](https://github.com/promptfoo) / [promptfoo](https://github.com/promptfoo/promptfoo) | 4976 |
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/157326433?s=40&v=4" width="20" height="20" alt=""> &nbsp; [onlook-dev](https://github.com/onlook-dev) / [onlook](https://github.com/onlook-dev/onlook) | 4141 |
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/7250217?s=40&v=4" width="20" height="20" alt=""> &nbsp; [Canner](https://github.com/Canner) / [WrenAI](https://github.com/Canner/WrenAI) | 2526 |
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/11855343?s=40&v=4" width="20" height="20" alt=""> &nbsp; [pingcap](https://github.com/pingcap) / [autoflow](https://github.com/pingcap/autoflow) | 2061 |
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/85268109?s=40&v=4" width="20" height="20" alt=""> &nbsp; [MLSysOps](https://github.com/MLSysOps) / [MLE-agent](https://github.com/MLSysOps/MLE-agent) | 1161 |
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/158137808?s=40&v=4" width="20" height="20" alt=""> &nbsp; [open-webui](https://github.com/open-webui) / [pipelines](https://github.com/open-webui/pipelines) | 1100 |
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/18422723?s=40&v=4" width="20" height="20" alt=""> &nbsp; [alishobeiri](https://github.com/alishobeiri) / [thread](https://github.com/alishobeiri/thread) | 1074 |
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/125468716?s=40&v=4" width="20" height="20" alt=""> &nbsp; [topoteretes](https://github.com/topoteretes) / [cognee](https://github.com/topoteretes/cognee) | 971 |
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/188657705?s=40&v=4" width="20" height="20" alt=""> &nbsp; [bRAGAI](https://github.com/bRAGAI) / [bRAG-langchain](https://github.com/bRAGAI/bRAG-langchain) | 823 |
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/169500408?s=40&v=4" width="20" height="20" alt=""> &nbsp; [opslane](https://github.com/opslane) / [opslane](https://github.com/opslane/opslane) | 677 |
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/151867818?s=40&v=4" width="20" height="20" alt=""> &nbsp; [dynamiq-ai](https://github.com/dynamiq-ai) / [dynamiq](https://github.com/dynamiq-ai/dynamiq) | 639 |
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/48585267?s=40&v=4" width="20" height="20" alt=""> &nbsp; [theopenconversationkit](https://github.com/theopenconversationkit) / [tock](https://github.com/theopenconversationkit/tock) | 514 |
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/20493493?s=40&v=4" width="20" height="20" alt=""> &nbsp; [andysingal](https://github.com/andysingal) / [llm-course](https://github.com/andysingal/llm-course) | 394 |
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/132396805?s=40&v=4" width="20" height="20" alt=""> &nbsp; [phospho-app](https://github.com/phospho-app) / [phospho](https://github.com/phospho-app/phospho) | 384 |
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/178644984?s=40&v=4" width="20" height="20" alt=""> &nbsp; [sentient-engineering](https://github.com/sentient-engineering) / [agent-q](https://github.com/sentient-engineering/agent-q) | 370 |
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/168552753?s=40&v=4" width="20" height="20" alt=""> &nbsp; [sql-agi](https://github.com/sql-agi) / [DB-GPT](https://github.com/sql-agi/DB-GPT) | 324 |
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/60330232?s=40&v=4" width="20" height="20" alt=""> &nbsp; [PostHog](https://github.com/PostHog) / [posthog-foss](https://github.com/PostHog/posthog-foss) | 305 |
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/154247157?s=40&v=4" width="20" height="20" alt=""> &nbsp; [vespperhq](https://github.com/vespperhq) / [vespper](https://github.com/vespperhq/vespper) | 304 |
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/185116535?s=40&v=4" width="20" height="20" alt=""> &nbsp; [block](https://github.com/block) / [goose](https://github.com/block/goose) | 295 |
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/609489?s=40&v=4" width="20" height="20" alt=""> &nbsp; [aorwall](https://github.com/aorwall) / [moatless-tools](https://github.com/aorwall/moatless-tools) | 291 |
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/2357342?s=40&v=4" width="20" height="20" alt=""> &nbsp; [dmayboroda](https://github.com/dmayboroda) / [minima](https://github.com/dmayboroda/minima) | 221 |
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/66303003?s=40&v=4" width="20" height="20" alt=""> &nbsp; [RobotecAI](https://github.com/RobotecAI) / [rai](https://github.com/RobotecAI/rai) | 172 |
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/148684274?s=40&v=4" width="20" height="20" alt=""> &nbsp; [i-am-alice](https://github.com/i-am-alice) / [3rd-devs](https://github.com/i-am-alice/3rd-devs) | 148 |
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/171735272?s=40&v=4" width="20" height="20" alt=""> &nbsp; [8090-inc](https://github.com/8090-inc) / [xrx-sample-apps](https://github.com/8090-inc/xrx-sample-apps) | 138 |
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/104478511?s=40&v=4" width="20" height="20" alt=""> &nbsp; [babelcloud](https://github.com/babelcloud) / [LLM-RGB](https://github.com/babelcloud/LLM-RGB) | 135 |
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/15125613?s=40&v=4" width="20" height="20" alt=""> &nbsp; [souzatharsis](https://github.com/souzatharsis) / [tamingLLMs](https://github.com/souzatharsis/tamingLLMs) | 129 |
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/169401942?s=40&v=4" width="20" height="20" alt=""> &nbsp; [LibreChat-AI](https://github.com/LibreChat-AI) / [librechat.ai](https://github.com/LibreChat-AI/librechat.ai) | 128 |
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/51827949?s=40&v=4" width="20" height="20" alt=""> &nbsp; [deepset-ai](https://github.com/deepset-ai) / [haystack-core-integrations](https://github.com/deepset-ai/haystack-core-integrations) | 126 |
## 🔒 セキュリティとプライバシー
+35 -34
View File
@@ -138,40 +138,40 @@ Langfuse 팀이 관리하는 배포 방식으로, 후한 무료 플랜(취미
### 주요 통합:
| 통합 | 지원 | 설명 |
| --------------------------------------------------------------------- | ---------------------- | ----------------------------------------------------------------------------------------------------------------------------------------- |
| [SDK](https://langfuse.com/docs/sdk) | Python, JS/TS | SDK를 사용하여 완전한 유연성을 갖춘 수동 계측(manual instrumentation)을 수행합니다. |
| [OpenAI](https://langfuse.com/docs/integrations/openai) | Python, JS/TS | OpenAI SDK의 드롭인 대체(drop-in replacement)를 통해 자동 계측(automated instrumentation)을 수행합니다. |
| [Langchain](https://langfuse.com/docs/integrations/langchain) | Python, JS/TS | Langchain 애플리케이션에 callback 핸들러를 전달하여 자동 계측합니다. |
| [LlamaIndex](https://langfuse.com/docs/integrations/llama-index/get-started) | Python | LlamaIndex 콜백 시스템을 통한 자동 계측을 지원합니다. |
| [Haystack](https://langfuse.com/docs/integrations/haystack) | Python | Haystack 콘텐츠 추적 시스템을 통한 자동 계측을 지원합니다. |
| [LiteLLM](https://langfuse.com/docs/integrations/litellm) | Python, JS/TS (proxy only) | GPT의 드롭인 대체품으로 어떤 LLM도 사용할 수 있습니다. Azure, OpenAI, Cohere, Anthropic, Ollama, VLLM, Sagemaker, HuggingFace, Replicate 등 100개 이상의 LLM 지원. |
| [Vercel AI SDK](https://langfuse.com/docs/integrations/vercel-ai-sdk) | JS/TS | React, Next.js, Vue, Svelte, Node.js와 함께 AI 기반 애플리케이션 구축을 돕는 TypeScript 툴킷입니다. |
| [API](https://langfuse.com/docs/api) | | 공개 API를 직접 호출합니다. OpenAPI 명세가 제공됩니다. |
| 통합 | 지원 | 설명 |
| ---------------------------------------------------------------------------- | -------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| [SDK](https://langfuse.com/docs/sdk) | Python, JS/TS | SDK를 사용하여 완전한 유연성을 갖춘 수동 계측(manual instrumentation)을 수행합니다. |
| [OpenAI](https://langfuse.com/integrations/model-providers/openai-py) | Python, JS/TS | OpenAI SDK의 드롭인 대체(drop-in replacement)를 통해 자동 계측(automated instrumentation)을 수행합니다. |
| [Langchain](https://langfuse.com/docs/integrations/langchain) | Python, JS/TS | Langchain 애플리케이션에 callback 핸들러를 전달하여 자동 계측합니다. |
| [LlamaIndex](https://langfuse.com/docs/integrations/llama-index/get-started) | Python | LlamaIndex 콜백 시스템을 통한 자동 계측을 지원합니다. |
| [Haystack](https://langfuse.com/docs/integrations/haystack) | Python | Haystack 콘텐츠 추적 시스템을 통한 자동 계측을 지원합니다. |
| [LiteLLM](https://langfuse.com/docs/integrations/litellm) | Python, JS/TS (proxy only) | GPT의 드롭인 대체품으로 어떤 LLM도 사용할 수 있습니다. Azure, OpenAI, Cohere, Anthropic, Ollama, VLLM, Sagemaker, HuggingFace, Replicate 등 100개 이상의 LLM 지원. |
| [Vercel AI SDK](https://langfuse.com/docs/integrations/vercel-ai-sdk) | JS/TS | React, Next.js, Vue, Svelte, Node.js와 함께 AI 기반 애플리케이션 구축을 돕는 TypeScript 툴킷입니다. |
| [API](https://langfuse.com/docs/api) | | 공개 API를 직접 호출합니다. OpenAPI 명세가 제공됩니다. |
### Langfuse와 통합된 패키지:
| 이름 | 유형 | 설명 |
| ----------------------------------------------------------------------- | ------------------ | ---------------------------------------------------------------------------------------------------------------- |
| [Instructor](https://langfuse.com/docs/integrations/instructor) | 라이브러리 | 구조화된 LLM 출력을(JSON, Pydantic) 얻기 위한 라이브러리입니다. |
| [DSPy](https://langfuse.com/docs/integrations/dspy) | 라이브러리 | 언어 모델 프롬프트와 가중치를 체계적으로 최적화하는 프레임워크입니다. |
| [Mirascope](https://langfuse.com/docs/integrations/mirascope) | 라이브러리 | LLM 애플리케이션 구축을 위한 Python 툴킷입니다. |
| [Ollama](https://langfuse.com/docs/integrations/ollama) | 모델 (로컬) | 자신의 컴퓨터에서 오픈 소스 LLM을 손쉽게 실행할 수 있습니다. |
| [Amazon Bedrock](https://langfuse.com/docs/integrations/amazon-bedrock) | 모델 | AWS에서 기본 및 파인튜닝된 모델을 실행합니다. |
| [Google VertexAI and Gemini](https://langfuse.com/docs/integrations/google-vertex-ai) | 모델 | Google에서 기본 및 파인튜닝된 모델을 실행합니다. |
| [AutoGen](https://langfuse.com/docs/integrations/autogen) | 에이전트 프레임워크 | 분산 에이전트 구축을 위한 오픈 소스 LLM 플랫폼입니다. |
| [Flowise](https://langfuse.com/docs/integrations/flowise) | 채팅/에이전트 UI | 맞춤형 LLM 플로우를 위한 JS/TS 코드 없는(no-code) 빌더입니다. |
| [Langflow](https://langfuse.com/docs/integrations/langflow) | 채팅/에이전트 UI | react-flow를 활용하여 실험 및 프로토타이핑을 손쉽게 할 수 있도록 디자인된 LangChain용 Python 기반 UI입니다. |
| [Dify](https://langfuse.com/docs/integrations/dify) | 채팅/에이전트 UI | 코드 없는 빌더와 함께 제공되는 오픈 소스 LLM 애플리케이션 개발 플랫폼입니다. |
| [OpenWebUI](https://langfuse.com/docs/integrations/openwebui) | 채팅/에이전트 UI | 셀프 호스팅 및 로컬 모델 등 다양한 LLM 실행기를 지원하는 셀프 호스팅 LLM 채팅 웹 UI입니다. |
| [Promptfoo](https://langfuse.com/docs/integrations/promptfoo) | 도구 | 오픈 소스 LLM 테스트 플랫폼입니다. |
| [LobeChat](https://langfuse.com/docs/integrations/lobechat) | 채팅/에이전트 UI | 오픈 소스 챗봇 플랫폼입니다. |
| [Vapi](https://langfuse.com/docs/integrations/vapi) | 플랫폼 | 오픈 소스 음성 AI 플랫폼입니다. |
| [Inferable](https://langfuse.com/docs/integrations/other/inferable) | 에이전트 | 분산 에이전트 구축을 위한 오픈 소스 LLM 플랫폼입니다. |
| [Gradio](https://langfuse.com/docs/integrations/other/gradio) | 채팅/에이전트 UI | 채팅 UI와 같은 웹 인터페이스 구축을 위한 오픈 소스 Python 라이브러리입니다. |
| [Goose](https://langfuse.com/docs/integrations/goose) | 에이전트 | 분산 에이전트 구축을 위한 오픈 소스 LLM 플랫폼입니다. |
| [smolagents](https://langfuse.com/docs/integrations/smolagents) | 에이전트 | 오픈 소스 AI 에이전트 프레임워크입니다. |
| [CrewAI](https://langfuse.com/docs/integrations/crewai) | 에이전트 | 에이전트 간 협업 및 도구 사용을 위한 다중 에이전트 프레임워크입니다. |
| 이름 | 유형 | 설명 |
| ------------------------------------------------------------------------------------- | ------------------- | ----------------------------------------------------------------------------------------------------------- |
| [Instructor](https://langfuse.com/docs/integrations/instructor) | 라이브러리 | 구조화된 LLM 출력을(JSON, Pydantic) 얻기 위한 라이브러리입니다. |
| [DSPy](https://langfuse.com/docs/integrations/dspy) | 라이브러리 | 언어 모델 프롬프트와 가중치를 체계적으로 최적화하는 프레임워크입니다. |
| [Mirascope](https://langfuse.com/docs/integrations/mirascope) | 라이브러리 | LLM 애플리케이션 구축을 위한 Python 툴킷입니다. |
| [Ollama](https://langfuse.com/docs/integrations/ollama) | 모델 (로컬) | 자신의 컴퓨터에서 오픈 소스 LLM을 손쉽게 실행할 수 있습니다. |
| [Amazon Bedrock](https://langfuse.com/docs/integrations/amazon-bedrock) | 모델 | AWS에서 기본 및 파인튜닝된 모델을 실행합니다. |
| [Google VertexAI and Gemini](https://langfuse.com/docs/integrations/google-vertex-ai) | 모델 | Google에서 기본 및 파인튜닝된 모델을 실행합니다. |
| [AutoGen](https://langfuse.com/docs/integrations/autogen) | 에이전트 프레임워크 | 분산 에이전트 구축을 위한 오픈 소스 LLM 플랫폼입니다. |
| [Flowise](https://langfuse.com/docs/integrations/flowise) | 채팅/에이전트 UI | 맞춤형 LLM 플로우를 위한 JS/TS 코드 없는(no-code) 빌더입니다. |
| [Langflow](https://langfuse.com/docs/integrations/langflow) | 채팅/에이전트 UI | react-flow를 활용하여 실험 및 프로토타이핑을 손쉽게 할 수 있도록 디자인된 LangChain용 Python 기반 UI입니다. |
| [Dify](https://langfuse.com/docs/integrations/dify) | 채팅/에이전트 UI | 코드 없는 빌더와 함께 제공되는 오픈 소스 LLM 애플리케이션 개발 플랫폼입니다. |
| [OpenWebUI](https://langfuse.com/docs/integrations/openwebui) | 채팅/에이전트 UI | 셀프 호스팅 및 로컬 모델 등 다양한 LLM 실행기를 지원하는 셀프 호스팅 LLM 채팅 웹 UI입니다. |
| [Promptfoo](https://langfuse.com/docs/integrations/promptfoo) | 도구 | 오픈 소스 LLM 테스트 플랫폼입니다. |
| [LobeChat](https://langfuse.com/docs/integrations/lobechat) | 채팅/에이전트 UI | 오픈 소스 챗봇 플랫폼입니다. |
| [Vapi](https://langfuse.com/docs/integrations/vapi) | 플랫폼 | 오픈 소스 음성 AI 플랫폼입니다. |
| [Inferable](https://langfuse.com/docs/integrations/other/inferable) | 에이전트 | 분산 에이전트 구축을 위한 오픈 소스 LLM 플랫폼입니다. |
| [Gradio](https://langfuse.com/docs/integrations/other/gradio) | 채팅/에이전트 UI | 채팅 UI와 같은 웹 인터페이스 구축을 위한 오픈 소스 Python 라이브러리입니다. |
| [Goose](https://langfuse.com/docs/integrations/goose) | 에이전트 | 분산 에이전트 구축을 위한 오픈 소스 LLM 플랫폼입니다. |
| [smolagents](https://langfuse.com/docs/integrations/smolagents) | 에이전트 | 오픈 소스 AI 에이전트 프레임워크입니다. |
| [CrewAI](https://langfuse.com/docs/integrations/crewai) | 에이전트 | 에이전트 간 협업 및 도구 사용을 위한 다중 에이전트 프레임워크입니다. |
## 🚀 빠른 시작
@@ -185,7 +185,7 @@ Langfuse 팀이 관리하는 배포 방식으로, 후한 무료 플랜(취미
### 2️⃣ 첫 번째 LLM 호출 기록하기
[`@observe()` 데코레이터](https://langfuse.com/docs/sdk/python/decorators)를 사용하면 Python LLM 애플리케이션의 추적이 매우 간편해집니다. 이 빠른 시작 예제에서는 Langfuse [OpenAI 통합](https://langfuse.com/docs/integrations/openai)을 사용하여 모든 모델 파라미터를 자동으로 캡처합니다.
[`@observe()` 데코레이터](https://langfuse.com/docs/sdk/python/decorators)를 사용하면 Python LLM 애플리케이션의 추적이 매우 간편해집니다. 이 빠른 시작 예제에서는 Langfuse [OpenAI 통합](https://langfuse.com/integrations/model-providers/openai-py)을 사용하여 모든 모델 파라미터를 자동으로 캡처합니다.
> [!TIP]
> OpenAI를 사용하지 않으시다면, 다른 모델 및 프레임워크의 로그 기록 방법은 [문서](https://langfuse.com/docs/get-started#log-your-first-llm-call-to-langfuse)를 참조하세요.
@@ -276,8 +276,8 @@ _[Langfuse의 공개 예제 trace](https://cloud.langfuse.com/project/cloramnkj0
별(star) 수를 기준으로 순위가 매겨진 Langfuse를 사용하는 상위 오픈 소스 Python 프로젝트들 ([출처](https://github.com/langfuse/langfuse-docs/blob/main/components-mdx/dependents)):
| 저장소 | |
| :---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---: |
| 저장소 | |
| :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ----: |
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/127165244?s=40&v=4" width="20" height="20" alt=""> &nbsp; [langgenius](https://github.com/langgenius) / [dify](https://github.com/langgenius/dify) | 54865 |
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/158137808?s=40&v=4" width="20" height="20" alt=""> &nbsp; [open-webui](https://github.com/open-webui) / [open-webui](https://github.com/open-webui/open-webui) | 51531 |
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/131470832?s=40&v=4" width="20" height="20" alt=""> &nbsp; [lobehub](https://github.com/lobehub) / [lobe-chat](https://github.com/lobehub/lobe-chat) | 49003 |
@@ -332,6 +332,7 @@ _[Langfuse의 공개 예제 trace](https://cloud.langfuse.com/project/cloramnkj0
기본적으로 Langfuse는 자체 호스팅 인스턴스의 기본 사용 통계를 중앙 서버(PostHog)로 자동 보고합니다.
이를 통해:
1. Langfuse가 어떻게 사용되는지 이해하고, 가장 관련성 높은 기능을 개선할 수 있습니다.
2. 내부 및 외부(예: 자금 조달) 보고를 위한 전체 사용량을 추적할 수 있습니다.
+6 -5
View File
@@ -80,11 +80,11 @@ Langfuse is an **open source LLM engineering** platform. It helps teams collabor
- [LLM Application Observability](https://langfuse.com/docs/tracing): Instrument your app and start ingesting traces to Langfuse, thereby tracking LLM calls and other relevant logic in your app such as retrieval, embedding, or agent actions. Inspect and debug complex logs and user sessions. Try the interactive [demo](https://langfuse.com/docs/demo) to see this in action.
- [Prompt Management](https://langfuse.com/docs/prompts/get-started) helps you centrally manage, version control, and collaboratively iterate on your prompts. Thanks to strong caching on server and client side, you can iterate on prompts without adding latency to your application.
- [Prompt Management](https://langfuse.com/docs/prompt-management/get-started) helps you centrally manage, version control, and collaboratively iterate on your prompts. Thanks to strong caching on server and client side, you can iterate on prompts without adding latency to your application.
- [Evaluations](https://langfuse.com/docs/scores/overview) are key to the LLM application development workflow, and Langfuse adapts to your needs. It supports LLM-as-a-judge, user feedback collection, manual labeling, and custom evaluation pipelines via APIs/SDKs.
- [Evaluations](https://langfuse.com/docs/evaluation/overview) are key to the LLM application development workflow, and Langfuse adapts to your needs. It supports LLM-as-a-judge, user feedback collection, manual labeling, and custom evaluation pipelines via APIs/SDKs.
- [Datasets](https://langfuse.com/docs/datasets/overview) enable test sets and benchmarks for evaluating your LLM application. They support continuous improvement, pre-deployment testing, structured experiments, flexible evaluation, and seamless integration with frameworks like LangChain and LlamaIndex.
- [Datasets](https://langfuse.com/docs/evaluation/dataset-runs/datasets) enable test sets and benchmarks for evaluating your LLM application. They support continuous improvement, pre-deployment testing, structured experiments, flexible evaluation, and seamless integration with frameworks like LangChain and LlamaIndex.
- [LLM Playground](https://langfuse.com/docs/playground) is a tool for testing and iterating on your prompts and model configurations, shortening the feedback loop and accelerating development. When you see a bad result in tracing, you can directly jump to the playground to iterate on it.
@@ -118,6 +118,7 @@ Run Langfuse on your own infrastructure:
# Run the langfuse docker compose
docker compose up
```
- [VM](https://langfuse.com/self-hosting/docker-compose): Run Langfuse on a single Virtual Machine using Docker Compose.
- [Kubernetes (Helm)](https://langfuse.com/self-hosting/kubernetes-helm): Run Langfuse on a Kubernetes cluster using Helm. This is the preferred production deployment.
- Terraform Templates: [AWS](https://langfuse.com/self-hosting/aws), [Azure](https://langfuse.com/self-hosting/azure), [GCP](https://langfuse.com/self-hosting/gcp)
@@ -133,7 +134,7 @@ See [self-hosting documentation](https://langfuse.com/self-hosting) to learn mor
| Integration | Supports | Description |
| ---------------------------------------------------------------------------- | -------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------ |
| [SDK](https://langfuse.com/docs/sdk) | Python, JS/TS | Manual instrumentation using the SDKs for full flexibility. |
| [OpenAI](https://langfuse.com/docs/integrations/openai) | Python, JS/TS | Automated instrumentation using drop-in replacement of OpenAI SDK. |
| [OpenAI](https://langfuse.com/integrations/model-providers/openai-py) | Python, JS/TS | Automated instrumentation using drop-in replacement of OpenAI SDK. |
| [Langchain](https://langfuse.com/docs/integrations/langchain) | Python, JS/TS | Automated instrumentation by passing callback handler to Langchain application. |
| [LlamaIndex](https://langfuse.com/docs/integrations/llama-index/get-started) | Python | Automated instrumentation via LlamaIndex callback system. |
| [Haystack](https://langfuse.com/docs/integrations/haystack) | Python | Automated instrumentation via Haystack content tracing system. |
@@ -176,7 +177,7 @@ Instrument your app and start ingesting traces to Langfuse, thereby tracking LLM
### 2️⃣ Log your first LLM call
The [`@observe()` decorator](https://langfuse.com/docs/sdk/python/decorators) makes it easy to trace any Python LLM application. In this quickstart we also use the Langfuse [OpenAI integration](https://langfuse.com/docs/integrations/openai) to automatically capture all model parameters.
The [`@observe()` decorator](https://langfuse.com/docs/sdk/python/decorators) makes it easy to trace any Python LLM application. In this quickstart we also use the Langfuse [OpenAI integration](https://langfuse.com/integrations/model-providers/openai-py) to automatically capture all model parameters.
> [!TIP]
> Not using OpenAI? Visit [our documentation](https://langfuse.com/docs/get-started#log-your-first-llm-call-to-langfuse) to learn how to log other models and frameworks.
+15
View File
@@ -0,0 +1,15 @@
# Code Review Instructions
## Database Migrations
### ClickHouse
- ClickHouse migrations in the `packages/shared/clickhouse/migrations/clustered` directory should include `ON CLUSTER default` and should use `Replicated` merge tree table types.
- E.g. `ReplacingMergeTree` is likely an error while `ReplicatedReplacingMergeTree` would be correct in most cases.
- ClickHouse migrations in the `packages/shared/clickhouse/migrations/unclustered` directory must not include `ON CLUSTER` statements and must not use `Replicated` merge tree table types.
- Migrations in `packages/shared/clickhouse/migrations/clustered` should match their counterparts in `packages/shared/clickhouse/migrations/unclustered` aside from the restrictions listed above.
- When adding new indexes on ClickHouse, ensure that there is a corresponding `MATERIALIZE INDEX` statement in the same migration. The materialization can use `SETTINGS mutations_sync = 2` if they operate on smaller tables, but may timeout otherwise.
### Postgres
- Most `schema.prisma` changes should produce a change in `packages/shared/prisma/migrations`.
+4 -5
View File
@@ -15,7 +15,7 @@
}
},
"engines": {
"node": "20"
"node": "24"
},
"scripts": {
"build": "tsc",
@@ -26,7 +26,6 @@
"dependencies": {
"@langfuse/shared": "workspace:*",
"@opentelemetry/api": ">=1.0.0 <1.10.0",
"axios": "^1.8.2",
"https-proxy-agent": "^7.0.6",
"next": "^14.2.30",
"next-auth": "^4.24.11",
@@ -35,7 +34,7 @@
"devDependencies": {
"@repo/eslint-config": "workspace:*",
"@repo/typescript-config": "workspace:*",
"@types/node": "^20.11.29",
"@types/node": "^24.3.0",
"@typescript-eslint/parser": "^7.12.0",
"eslint": "^8.57.0",
"eslint-config-prettier": "^9.1.0",
@@ -44,6 +43,6 @@
"prettier": "^3.6.2",
"ts-node": "^10.9.2",
"tsc-watch": "^6.2.0",
"typescript": "^5.4.5"
"typescript": "^5.7.2"
}
}
}
+2 -2
View File
@@ -3,10 +3,10 @@
"compilerOptions": {
"moduleResolution": "NodeNext",
"module": "NodeNext",
"lib": ["ES2020"],
"lib": ["es2023"],
"outDir": "./dist",
"types": ["node"],
"target": "ES2020",
"target": "es2024",
"rootDir": "."
},
"include": ["."],
@@ -22,6 +22,13 @@ service:
docs: limit of items per page
response: PaginatedAnnotationQueues
createQueue:
docs: Create an annotation queue
method: POST
path: /annotation-queues
request: CreateAnnotationQueueRequest
response: AnnotationQueue
getQueue:
docs: Get an annotation queue by ID
method: GET
@@ -168,6 +175,12 @@ types:
data: list<AnnotationQueueItem>
meta: pagination.MetaResponse
CreateAnnotationQueueRequest:
properties:
name: string
description: optional<string>
scoreConfigIds: list<string>
CreateAnnotationQueueItemRequest:
properties:
objectId: string
+8 -1
View File
@@ -19,7 +19,7 @@ service:
I.e. if you want to update a trace, you'd use the same body id, but separate event IDs.
Notes:
- Introduction to data model: https://langfuse.com/docs/tracing-data-model
- Introduction to data model: https://langfuse.com/docs/observability/data-model
- Batch sizes are limited to 3.5 MB in total. You need to adjust the number of events per batch accordingly.
- The API does not return a 4xx status code for input errors. Instead, it responds with a 207 status code, which includes a list of the encountered errors.
method: POST
@@ -145,6 +145,13 @@ types:
- SPAN
- GENERATION
- EVENT
- AGENT
- TOOL
- CHAIN
- RETRIEVER
- EVALUATOR
- EMBEDDING
- GUARDRAIL
IngestionUsage:
discriminated: false
@@ -20,6 +20,13 @@ service:
request: MembershipRequest
response: MembershipResponse
deleteOrganizationMembership:
docs: Delete a membership from the organization associated with the API key (requires organization-scoped API key)
method: DELETE
path: /organizations/memberships
request: DeleteMembershipRequest
response: MembershipDeletionResponse
getProjectMemberships:
docs: Get all memberships for a specific project (requires organization-scoped API key)
method: GET
@@ -37,6 +44,15 @@ service:
request: MembershipRequest
response: MembershipResponse
deleteProjectMembership:
docs: Delete a membership from a specific project (requires organization-scoped API key). The user must be a member of the organization.
method: DELETE
path: /projects/{projectId}/memberships
path-parameters:
projectId: string
request: DeleteMembershipRequest
response: MembershipDeletionResponse
getOrganizationProjects:
docs: Get all projects for the organization associated with the API key (requires organization-scoped API key)
method: GET
@@ -56,6 +72,10 @@ types:
userId: string
role: MembershipRole
DeleteMembershipRequest:
properties:
userId: string
MembershipResponse:
properties:
userId: string
@@ -63,6 +83,11 @@ types:
email: string
name: string
MembershipDeletionResponse:
properties:
message: string
userId: string
MembershipsResponse:
properties:
memberships: list<MembershipResponse>
+1 -1
View File
@@ -65,7 +65,7 @@ service:
docs: Optional filter for traces where the environment is one of the provided values.
fields:
type: optional<string>
docs: "Comma-separated list of fields to include in the response. Available field groups are 'core' (always included), 'io' (input, output, metadata), 'scores', 'observations', 'metrics'. If not provided, all fields are included. Example: 'core,scores,metrics'"
docs: "Comma-separated list of fields to include in the response. Available field groups: 'core' (always included), 'io' (input, output, metadata), 'scores', 'observations', 'metrics'. If not specified, all fields are returned. Example: 'core,scores,metrics'. Note: Excluded 'observations' or 'scores' fields return empty arrays; excluded 'metrics' returns -1 for 'totalCost' and 'latency'."
response: Traces
deleteMultiple:
docs: Delete multiple traces
+5 -4
View File
@@ -1,11 +1,11 @@
{
"name": "langfuse",
"version": "3.95.1",
"version": "3.106.0",
"author": "engineering@langfuse.com",
"license": "MIT",
"private": true,
"engines": {
"node": "20"
"node": "24"
},
"scripts": {
"preinstall": "npx only-allow pnpm",
@@ -40,7 +40,7 @@
"husky": "^9.0.11",
"prettier": "^3.6.2",
"release-it": "^19.0.3",
"turbo": "^2.5.5"
"turbo": "^2.5.6"
},
"release-it": {
"git": {
@@ -91,7 +91,8 @@
"nanoid": "^3.3.8",
"katex": "^0.16.21",
"tar-fs": "^2.1.2",
"rollup@^4.0.0": "^4.22.4"
"rollup@^4.0.0": "^4.22.4",
"@types/node-fetch": "^2.6.13"
},
"patchedDependencies": {
"next-auth@4.24.11": "patches/next-auth@4.24.11.patch"
+8
View File
@@ -41,6 +41,14 @@ module.exports = {
// https://typescript-eslint.io/troubleshooting/faqs/eslint/#i-get-errors-from-the-no-undef-rule-about-global-variables-not-being-defined-even-though-there-are-no-typescript-errors
rules: {
"no-undef": "off",
"no-restricted-globals": [
"error",
{
name: "redis",
message:
"Import redis explicitly from '@langfuse/shared/src/server' instead of using global.",
},
],
},
},
],
+2 -2
View File
@@ -13,8 +13,8 @@
"@vercel/style-guide": "^6.0.0",
"eslint-config-next": "^14.2.15",
"eslint-config-prettier": "^9.1.0",
"eslint-config-turbo": "^2.5.5",
"eslint-config-turbo": "^2.5.6",
"eslint-plugin-only-warn": "^1.1.0",
"typescript": "^5.4.5"
"typescript": "^5.7.2"
}
}
+23 -7
View File
@@ -3,9 +3,17 @@
"display": "Next.js",
"extends": "./base.json",
"compilerOptions": {
"plugins": [{ "name": "next" }],
"target": "es2017",
"lib": ["dom", "dom.iterable", "esnext"],
"plugins": [
{
"name": "next"
}
],
"target": "es2024",
"lib": [
"dom",
"dom.iterable",
"esnext"
],
"allowJs": true,
"checkJs": true,
"skipLibCheck": true,
@@ -18,8 +26,16 @@
"resolveJsonModule": true,
"isolatedModules": true,
"jsx": "preserve",
"types": ["jest", "node"],
"types": [
"jest",
"node"
],
},
"include": ["src", "next-env.d.ts"],
"exclude": ["node_modules"]
}
"include": [
"src",
"next-env.d.ts"
],
"exclude": [
"node_modules"
]
}
@@ -0,0 +1 @@
DROP TABLE dataset_run_items_rmt ON CLUSTER default;
@@ -0,0 +1,36 @@
CREATE TABLE dataset_run_items_rmt ON CLUSTER default (
-- primary identifiers
`id` String,
`project_id` String,
`dataset_run_id` String,
`dataset_item_id` String,
`dataset_id` String,
`trace_id` String,
`observation_id` Nullable(String),
-- error field
`error` Nullable(String),
-- timestamps
`created_at` DateTime64(3) DEFAULT now(),
`updated_at` DateTime64(3) DEFAULT now(),
-- denormalized immutable dataset run fields
`dataset_run_name` String,
`dataset_run_description` Nullable(String),
`dataset_run_metadata` Map(LowCardinality(String), String),
`dataset_run_created_at` DateTime64(3),
-- denormalized dataset item fields (mutable, but snapshots are relevant)
`dataset_item_input` Nullable(String) CODEC(ZSTD(3)), -- json
`dataset_item_expected_output` Nullable(String) CODEC(ZSTD(3)), -- json
`dataset_item_metadata` Map(LowCardinality(String), String),
-- clickhouse engine fields
`event_ts` DateTime64(3),
`is_deleted` UInt8,
-- For dataset item lookups
INDEX idx_dataset_item dataset_item_id TYPE bloom_filter(0.001) GRANULARITY 1,
) ENGINE = ReplicatedReplacingMergeTree(event_ts, is_deleted)
ORDER BY (project_id, dataset_id, dataset_run_id, id);
@@ -0,0 +1,2 @@
ALTER TABLE observations ON CLUSTER default DROP INDEX IF EXISTS idx_res_metadata_value;
ALTER TABLE observations ON CLUSTER default DROP INDEX IF EXISTS idx_res_metadata_key;
@@ -0,0 +1,4 @@
ALTER TABLE observations ON CLUSTER default ADD INDEX IF NOT EXISTS idx_res_metadata_key mapKeys(metadata) TYPE bloom_filter(0.01) GRANULARITY 1;
ALTER TABLE observations ON CLUSTER default ADD INDEX IF NOT EXISTS idx_res_metadata_value mapValues(metadata) TYPE bloom_filter(0.01) GRANULARITY 1;
ALTER TABLE observations ON CLUSTER default MATERIALIZE INDEX IF EXISTS idx_res_metadata_key;
ALTER TABLE observations ON CLUSTER default MATERIALIZE INDEX IF EXISTS idx_res_metadata_value;
@@ -0,0 +1 @@
ALTER TABLE dataset_run_items_rmt ON CLUSTER default DROP INDEX IF EXISTS idx_trace_id;
@@ -0,0 +1,2 @@
ALTER TABLE dataset_run_items_rmt ON CLUSTER default ADD INDEX IF NOT EXISTS idx_trace_id trace_id TYPE bloom_filter(0.001) GRANULARITY 1;
ALTER TABLE dataset_run_items_rmt ON CLUSTER default MATERIALIZE INDEX IF EXISTS idx_trace_id;
@@ -0,0 +1 @@
DROP TABLE dataset_run_items_rmt;
@@ -0,0 +1,36 @@
CREATE TABLE dataset_run_items_rmt (
-- primary identifiers
`id` String,
`project_id` String,
`dataset_run_id` String,
`dataset_item_id` String,
`dataset_id` String,
`trace_id` String,
`observation_id` Nullable(String),
-- error field
`error` Nullable(String),
-- timestamps
`created_at` DateTime64(3) DEFAULT now(),
`updated_at` DateTime64(3) DEFAULT now(),
-- denormalized immutable dataset run fields
`dataset_run_name` String,
`dataset_run_description` Nullable(String),
`dataset_run_metadata` Map(LowCardinality(String), String),
`dataset_run_created_at` DateTime64(3),
-- denormalized dataset item fields (mutable, but snapshots are relevant)
`dataset_item_input` Nullable(String) CODEC(ZSTD(3)), -- json
`dataset_item_expected_output` Nullable(String) CODEC(ZSTD(3)), -- json
`dataset_item_metadata` Map(LowCardinality(String), String),
-- clickhouse engine fields
`event_ts` DateTime64(3),
`is_deleted` UInt8,
-- For dataset item lookups
INDEX idx_dataset_item dataset_item_id TYPE bloom_filter(0.001) GRANULARITY 1,
) ENGINE = ReplacingMergeTree(event_ts, is_deleted)
ORDER BY (project_id, dataset_id, dataset_run_id, id);
@@ -0,0 +1,2 @@
ALTER TABLE observations DROP INDEX IF EXISTS idx_res_metadata_value;
ALTER TABLE observations DROP INDEX IF EXISTS idx_res_metadata_key;
@@ -0,0 +1,4 @@
ALTER TABLE observations ADD INDEX IF NOT EXISTS idx_res_metadata_key mapKeys(metadata) TYPE bloom_filter(0.01) GRANULARITY 1;
ALTER TABLE observations ADD INDEX IF NOT EXISTS idx_res_metadata_value mapValues(metadata) TYPE bloom_filter(0.01) GRANULARITY 1;
ALTER TABLE observations MATERIALIZE INDEX IF EXISTS idx_res_metadata_key;
ALTER TABLE observations MATERIALIZE INDEX IF EXISTS idx_res_metadata_value;
@@ -0,0 +1 @@
ALTER TABLE dataset_run_items_rmt DROP INDEX IF EXISTS idx_trace_id;
@@ -0,0 +1,2 @@
ALTER TABLE dataset_run_items_rmt ADD INDEX IF NOT EXISTS idx_trace_id trace_id TYPE bloom_filter(0.001) GRANULARITY 1;
ALTER TABLE dataset_run_items_rmt MATERIALIZE INDEX IF EXISTS idx_trace_id;
+8 -9
View File
@@ -6,7 +6,7 @@
"main": "./dist/src/index.js",
"types": "./dist/src/index.d.ts",
"engines": {
"node": "20"
"node": "24"
},
"exports": {
".": {
@@ -61,8 +61,8 @@
"@aws-sdk/lib-storage": "^3.675.0",
"@aws-sdk/s3-request-presigner": "^3.679.0",
"@azure/storage-blob": "^12.26.0",
"@clickhouse/client": "^1.12.0",
"@google-cloud/storage": "^7.15.2",
"@clickhouse/client": "^1.12.1",
"@google-cloud/storage": "^7.17.0",
"@langchain/anthropic": "^0.3.22",
"@langchain/aws": "^0.1.11",
"@langchain/core": "^0.3.58",
@@ -73,10 +73,9 @@
"@prisma/client": "^6.10.1",
"@react-email/components": "^0.1.0",
"@react-email/render": "^1.1.2",
"@slack/oauth": "^2.6.0",
"@slack/web-api": "^7.0.0",
"@slack/oauth": "^3.0.3",
"@slack/web-api": "^7.9.3",
"@types/bcryptjs": "^2.4.6",
"axios": "^1.8.2",
"bcryptjs": "^2.4.3",
"bullmq": "^5.34.10",
"dd-trace": "^5.36.0",
@@ -102,7 +101,7 @@
"@repo/eslint-config": "workspace:*",
"@repo/typescript-config": "workspace:*",
"@types/lodash": "^4.17.10",
"@types/node": "^20.11.29",
"@types/node": "^24.3.0",
"@types/nodemailer": "^6.4.16",
"@types/pg": "^8.11.10",
"@types/react": "18.2.79",
@@ -120,10 +119,10 @@
"prisma-kysely": "^1.8.0",
"ts-node": "^10.9.2",
"tsc-watch": "^6.2.0",
"typescript": "^5.4.5"
"typescript": "^5.7.2"
},
"peerDependencies": {
"@types/react": "~18.2.79",
"react": "~18.2.0"
}
}
}
+24
View File
@@ -22,6 +22,13 @@ export const LegacyPrismaObservationType = {
SPAN: "SPAN",
EVENT: "EVENT",
GENERATION: "GENERATION",
AGENT: "AGENT",
TOOL: "TOOL",
CHAIN: "CHAIN",
RETRIEVER: "RETRIEVER",
EVALUATOR: "EVALUATOR",
EMBEDDING: "EMBEDDING",
GUARDRAIL: "GUARDRAIL",
} as const;
export type LegacyPrismaObservationType =
(typeof LegacyPrismaObservationType)[keyof typeof LegacyPrismaObservationType];
@@ -150,6 +157,11 @@ export const ActionExecutionStatus = {
} as const;
export type ActionExecutionStatus =
(typeof ActionExecutionStatus)[keyof typeof ActionExecutionStatus];
export const SurveyName = {
ORG_ONBOARDING: "org_onboarding",
USER_ONBOARDING: "user_onboarding",
} as const;
export type SurveyName = (typeof SurveyName)[keyof typeof SurveyName];
export type Account = {
id: string;
user_id: string;
@@ -346,6 +358,7 @@ export type Dashboard = {
name: string;
description: string;
definition: unknown;
filters: Generated<unknown>;
};
export type DashboardWidget = {
id: string;
@@ -461,6 +474,7 @@ export type JobExecution = {
end_time: Timestamp | null;
error: string | null;
job_input_trace_id: string | null;
job_input_trace_timestamp: Timestamp | null;
job_input_observation_id: string | null;
job_input_dataset_item_id: string | null;
job_output_score_id: string | null;
@@ -749,6 +763,15 @@ export type SsoConfig = {
auth_provider: string;
auth_config: unknown | null;
};
export type Survey = {
id: string;
created_at: Generated<Timestamp>;
survey_name: SurveyName;
response: unknown;
user_id: string | null;
user_email: string | null;
org_id: string | null;
};
export type TableViewPreset = {
id: string;
created_at: Generated<Timestamp>;
@@ -858,6 +881,7 @@ export type DB = {
Session: Session;
slack_integrations: SlackIntegration;
sso_configs: SsoConfig;
surveys: Survey;
table_view_presets: TableViewPreset;
trace_media: TraceMedia;
trace_sessions: TraceSession;
@@ -0,0 +1,24 @@
-- CreateEnum
CREATE TYPE "SurveyName" AS ENUM ('org_onboarding', 'user_onboarding');
-- CreateTable
CREATE TABLE "surveys" (
"id" TEXT NOT NULL,
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"survey_name" "SurveyName" NOT NULL,
"response" JSONB NOT NULL,
"user_id" TEXT,
"user_email" TEXT,
"org_id" TEXT,
CONSTRAINT "surveys_pkey" PRIMARY KEY ("id")
);
-- AddForeignKey
ALTER TABLE "surveys" ADD CONSTRAINT "surveys_org_id_fkey" FOREIGN KEY ("org_id") REFERENCES "organizations"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "surveys" ADD CONSTRAINT "surveys_user_id_fkey" FOREIGN KEY ("user_id") REFERENCES "users"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- RenameIndex
ALTER INDEX "annotation_queue_assignments_project_id_queue_id_key" RENAME TO "annotation_queue_assignments_project_id_queue_id_user_id_key";
@@ -0,0 +1 @@
DELETE FROM background_migrations WHERE id = '8d47f91b-3e5c-4a26-9f85-c12d6e4b9a3d';
@@ -0,0 +1,2 @@
INSERT INTO background_migrations (id, name, script, args)
VALUES ('9f32e84c-7b1d-4f59-a803-d67ae5c9b2e8', '20250814_1001_migrate_dataset_run_items_rmt_pg_to_ch', 'migrateDatasetRunItemsFromPostgresToClickhouseRmt', '{}');
@@ -0,0 +1,15 @@
-- AlterEnum
-- This migration adds more than one value to an enum.
-- With PostgreSQL versions 11 and earlier, this is not possible
-- in a single migration. This can be worked around by creating
-- multiple migrations, each migration adding only one value to
-- the enum.
ALTER TYPE "ObservationType" ADD VALUE 'AGENT';
ALTER TYPE "ObservationType" ADD VALUE 'TOOL';
ALTER TYPE "ObservationType" ADD VALUE 'CHAIN';
ALTER TYPE "ObservationType" ADD VALUE 'RETRIEVER';
ALTER TYPE "ObservationType" ADD VALUE 'EVALUATOR';
ALTER TYPE "ObservationType" ADD VALUE 'EMBEDDING';
ALTER TYPE "ObservationType" ADD VALUE 'GUARDRAIL';
@@ -0,0 +1,2 @@
-- DropIndex
DROP INDEX CONCURRENTLY "job_executions_created_at_idx";
@@ -0,0 +1,2 @@
-- DropIndex
DROP INDEX CONCURRENTLY "job_executions_job_configuration_id_idx";
@@ -0,0 +1,3 @@
-- DropIndex
DROP INDEX CONCURRENTLY "job_executions_job_input_trace_id_idx";
@@ -0,0 +1,3 @@
-- DropIndex
DROP INDEX CONCURRENTLY "job_executions_job_output_score_id_idx";
@@ -0,0 +1,2 @@
-- DropIndex
DROP INDEX CONCURRENTLY "job_executions_updated_at_idx";
@@ -0,0 +1,2 @@
-- CreateIndex
CREATE INDEX CONCURRENTLY "job_executions_project_id_job_configuration_id_job_input_tr_idx" ON "job_executions"("project_id", "job_configuration_id", "job_input_trace_id");
@@ -0,0 +1,2 @@
-- AlterTable
ALTER TABLE "dashboards" ADD COLUMN "filters" JSONB NOT NULL DEFAULT '[]';
@@ -0,0 +1,2 @@
-- AlterTable
ALTER TABLE "job_executions" ADD COLUMN "job_input_trace_timestamp" TIMESTAMP(3);
+34 -6
View File
@@ -89,6 +89,7 @@ model User {
tableViewPresetCreated TableViewPreset[] @relation("CreatedByUser")
tableViewPresetUpdated TableViewPreset[] @relation("UpdatedByUser")
annotationQueueAssignment AnnotationQueueAssignment[]
surveys Survey[]
@@map("users")
}
@@ -113,6 +114,7 @@ model Organization {
projects Project[]
MembershipInvitation MembershipInvitation[]
ApiKey ApiKey[]
surveys Survey[]
@@map("organizations")
}
@@ -366,7 +368,6 @@ model LegacyPrismaObservation {
version String?
createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime @default(now()) @updatedAt @map("updated_at")
// GENERATION ONLY
model String? // user-provided model attribute
internalModel String? @map("internal_model") // matched model.name that is matched at ingestion time, to be deprecated
internalModelId String? @map("internal_model_id") // matched model.id that is matched at ingestion time
@@ -407,6 +408,13 @@ enum LegacyPrismaObservationType {
SPAN
EVENT
GENERATION
AGENT
TOOL
CHAIN
RETRIEVER
EVALUATOR
EMBEDDING
GUARDRAIL
@@map("ObservationType")
}
@@ -908,19 +916,17 @@ model JobExecution {
jobInputTraceId String? @map("job_input_trace_id") // no fk constraint - traces in ClickHouse, deletion handled via project cascade
jobInputTraceTimestamp DateTime? @map("job_input_trace_timestamp")
jobInputObservationId String? @map("job_input_observation_id") // no fk constraint - observations in ClickHouse, deletion handled via project cascade
jobInputDatasetItemId String? @map("job_input_dataset_item_id") // no fk constraint - job execution sensible standalone
jobOutputScoreId String? @map("job_output_score_id")
@@index([projectId, jobConfigurationId, jobInputTraceId])
@@index([projectId, status])
@@index([projectId, id])
@@index([jobConfigurationId])
@@index([jobOutputScoreId])
@@index([jobInputTraceId])
@@index([createdAt])
@@index([updatedAt])
@@map("job_executions")
}
@@ -1176,6 +1182,7 @@ model Dashboard {
description String @map("description")
definition Json @map("definition")
filters Json @default("[]") @map("filters")
@@map("dashboards")
}
@@ -1397,3 +1404,24 @@ model PendingDeletion {
@@index([objectId, object])
@@map("pending_deletions")
}
model Survey {
id String @id @default(cuid())
createdAt DateTime @default(now()) @map("created_at")
surveyName SurveyName @map("survey_name")
response Json
userId String? @map("user_id")
userEmail String? @map("user_email")
orgId String? @map("org_id")
org Organization? @relation(fields: [orgId], references: [id], onDelete: Cascade)
user User? @relation(fields: [userId], references: [id], onDelete: Cascade)
@@map("surveys")
}
enum SurveyName {
ORG_ONBOARDING @map("org_onboarding")
USER_ONBOARDING @map("user_onboarding")
@@map("SurveyName")
}
@@ -24,7 +24,6 @@ import {
} from "./utils/postgres-seed-constants";
import {
generateDatasetItemId,
generateDatasetRunTraceId,
generateEvalObservationId,
generateEvalScoreId,
generateEvalTraceId,
@@ -564,17 +563,17 @@ export async function createDatasets(
}
for (let datasetRunNumber = 0; datasetRunNumber < 3; datasetRunNumber++) {
const datasetRun = await prisma.datasetRuns.upsert({
await prisma.datasetRuns.upsert({
where: {
id_projectId: {
id: `demo-dataset-run-${datasetRunNumber}-${projectId.slice(-8)}`,
id: `demo-dataset-run-${datasetRunNumber}-${datasetName}-${projectId.slice(-8)}`,
projectId,
},
},
create: {
projectId,
id: `demo-dataset-run-${datasetRunNumber}-${projectId.slice(-8)}`,
name: `demo-dataset-run-${datasetRunNumber}`,
id: `demo-dataset-run-${datasetRunNumber}-${datasetName}-${projectId.slice(-8)}`,
name: `demo-dataset-run-${datasetRunNumber}-${datasetName}`,
description: Math.random() > 0.5 ? "Dataset run description" : "",
datasetId: dataset.id,
metadata: [
@@ -587,25 +586,6 @@ export async function createDatasets(
},
update: {},
});
for (let index = 0; index < datasetItemIds.length; index++) {
await prisma.datasetRunItems.upsert({
where: {
id_projectId: {
id: `${dataset.id}-${index}-${datasetRunNumber}`,
projectId,
},
},
create: {
id: `${dataset.id}-${index}-${datasetRunNumber}`,
projectId,
datasetItemId: datasetItemIds[index],
traceId: `${generateDatasetRunTraceId(datasetName, index, projectId, datasetRunNumber)}`,
datasetRunId: datasetRun.id,
},
update: {},
});
}
}
}
}
@@ -95,3 +95,57 @@ export const REALISTIC_METADATA_EXAMPLES = [
},
{ file_type: "JSON", validation: "passed", schema_version: "v1.2" },
];
export const REALISTIC_AGENT_NAMES = [
"AI-Coordinator",
"TaskManager",
"WorkflowOrchestrator",
"PlanningAgent",
"MultiStepAgent",
"SmartCoordinator",
];
export const REALISTIC_TOOL_NAMES = [
"WebSearchTool",
"CalculatorTool",
"WeatherForecastTool",
"EmailSenderTool",
];
export const REALISTIC_CHAIN_NAMES = [
"DataTransformationChain",
"ProcessingPipeline",
"TransformationChain",
"MultiStepProcess",
];
export const REALISTIC_RETRIEVER_NAMES = [
"DocumentRetriever",
"SemanticSearch",
"InformationRetriever",
"VectorSearch",
"MemoryRetriever",
];
export const REALISTIC_EVALUATOR_NAMES = [
"QualityEvaluator",
"RelevanceScorer",
"AccuracyChecker",
"ResponseEvaluator",
"ContentScorer",
];
export const REALISTIC_EMBEDDING_NAMES = [
"TextEmbedding",
"DocumentEncoder",
"SemanticEncoder",
"VectorEmbedding",
];
export const REALISTIC_GUARDRAIL_NAMES = [
"SafetyChecker",
"ContentModerator",
"ToxicityFilter",
"JailbreakDetector",
"PolicyEnforcer",
];
@@ -4,6 +4,13 @@ import {
REALISTIC_SPAN_NAMES,
REALISTIC_GENERATION_NAMES,
REALISTIC_MODELS,
REALISTIC_AGENT_NAMES,
REALISTIC_TOOL_NAMES,
REALISTIC_CHAIN_NAMES,
REALISTIC_RETRIEVER_NAMES,
REALISTIC_EVALUATOR_NAMES,
REALISTIC_EMBEDDING_NAMES,
REALISTIC_GUARDRAIL_NAMES,
} from "./clickhouse-seed-constants";
import {
generateDatasetItemId,
@@ -79,7 +86,6 @@ export class DataGenerator {
input.runNumber || 0,
);
// TODO: there are too many dataset run items in the postgres database?
return createDatasetRunItem({
id: datasetRunItemId,
project_id: projectId,
@@ -90,8 +96,8 @@ export class DataGenerator {
input.runNumber || 0,
),
dataset_id: `${input.datasetName}-${projectId.slice(-8)}`,
dataset_run_id: `demo-dataset-run-${input.runNumber}-${projectId.slice(-8)}`,
dataset_run_name: `demo-dataset-run-${input.runNumber}-${projectId.slice(-8)}`,
dataset_run_id: `demo-dataset-run-${input.runNumber}-${input.datasetName}-${projectId.slice(-8)}`,
dataset_run_name: `demo-dataset-run-${input.runNumber}-${input.datasetName}`,
dataset_run_created_at: input.runCreatedAt,
dataset_run_description:
(input.runNumber || 0) % 2 === 0 ? "Dataset run description" : "",
@@ -103,7 +109,7 @@ export class DataGenerator {
input.runNumber || 0,
),
dataset_item_input: input.item.input,
dataset_item_expected_output: input.item.output,
dataset_item_expected_output: input.item.expectedOutput,
});
}
@@ -365,8 +371,56 @@ export class DataGenerator {
const observations: ObservationRecordInsertType[] = [];
traces.forEach((trace, traceIndex) => {
if (this.randomBoolean(0.1)) {
const { observations: workflowObservations } =
this.generateComprehensiveAIWorkflowTrace(trace.id, trace.project_id);
observations.push(...workflowObservations);
return;
}
for (let i = 0; i < observationsPerTrace; i++) {
const obsType = this.randomElement(["GENERATION", "SPAN", "EVENT"]);
const obsType = this.randomBoolean(0.8) // More "traditional" types, are more common in app
? this.randomElement(["GENERATION", "SPAN", "EVENT"])
: this.randomElement([
"AGENT",
"TOOL",
"CHAIN",
"RETRIEVER",
"EVALUATOR",
"EMBEDDING",
"GUARDRAIL",
]);
let observationName: string;
switch (obsType) {
case "AGENT":
observationName = this.randomElement(REALISTIC_AGENT_NAMES);
break;
case "TOOL":
observationName = this.randomElement(REALISTIC_TOOL_NAMES);
break;
case "CHAIN":
observationName = this.randomElement(REALISTIC_CHAIN_NAMES);
break;
case "RETRIEVER":
observationName = this.randomElement(REALISTIC_RETRIEVER_NAMES);
break;
case "EVALUATOR":
observationName = this.randomElement(REALISTIC_EVALUATOR_NAMES);
break;
case "EMBEDDING":
observationName = this.randomElement(REALISTIC_EMBEDDING_NAMES);
break;
case "GUARDRAIL":
observationName = this.randomElement(REALISTIC_GUARDRAIL_NAMES);
break;
case "GENERATION":
observationName = this.randomElement(REALISTIC_GENERATION_NAMES);
break;
default:
observationName = this.randomElement(REALISTIC_SPAN_NAMES);
break;
}
const observation: ObservationRecordInsertType = createObservation({
id: `obs-synthetic-${traceIndex}-${i}`,
@@ -375,28 +429,28 @@ export class DataGenerator {
parent_observation_id:
i > 0 ? `obs-synthetic-${traceIndex}-${i - 1}` : undefined,
type: obsType as any,
name:
obsType === "GENERATION"
? this.randomElement(REALISTIC_GENERATION_NAMES)
: this.randomElement(REALISTIC_SPAN_NAMES),
name: observationName,
input:
obsType === "GENERATION"
obsType === "GENERATION" || obsType === "EMBEDDING"
? this.generateObservationInput()
: undefined,
output:
obsType === "GENERATION"
obsType === "GENERATION" ||
obsType === "RETRIEVER" ||
obsType === "EVALUATOR" ||
obsType === "GUARDRAIL"
? this.generateObservationOutput()
: undefined,
provided_model_name:
obsType === "GENERATION"
obsType === "GENERATION" || obsType === "EMBEDDING"
? this.randomElement(REALISTIC_MODELS)
: undefined,
model_parameters:
obsType === "GENERATION"
obsType === "GENERATION" || obsType === "EMBEDDING"
? JSON.stringify({ temperature: 0.7 })
: undefined,
usage_details:
obsType === "GENERATION"
obsType === "GENERATION" || obsType === "EMBEDDING"
? {
input: this.randomInt(20, 200),
output: this.randomInt(10, 100),
@@ -404,7 +458,7 @@ export class DataGenerator {
}
: undefined,
provided_usage_details:
obsType === "GENERATION"
obsType === "GENERATION" || obsType === "EMBEDDING"
? {
input: this.randomInt(20, 200),
output: this.randomInt(10, 100),
@@ -412,7 +466,7 @@ export class DataGenerator {
}
: undefined,
cost_details:
obsType === "GENERATION"
obsType === "GENERATION" || obsType === "EMBEDDING"
? {
input: this.randomInt(1, 10) / 100000,
output: this.randomInt(1, 20) / 100000,
@@ -420,7 +474,7 @@ export class DataGenerator {
}
: undefined,
provided_cost_details:
obsType === "GENERATION"
obsType === "GENERATION" || obsType === "EMBEDDING"
? {
input: this.randomInt(1, 10) / 100000,
output: this.randomInt(1, 20) / 100000,
@@ -500,6 +554,252 @@ export class DataGenerator {
return scores;
}
/**
* Creates a workflow trace with all possible observation types.
*/
generateComprehensiveAIWorkflowTrace(
traceId: string,
projectId: string,
): {
trace: TraceRecordInsertType;
observations: ObservationRecordInsertType[];
} {
// Create the main trace
const trace = createTrace({
id: traceId,
project_id: projectId,
name: "AI-Agent-Workflow",
input:
"Analyze and summarize the latest research papers on quantum computing",
output:
"Here is a comprehensive summary of quantum computing research trends with key insights and recommendations.",
user_id: this.randomBoolean(0.3)
? `user_${this.randomInt(1, 1000)}`
: null,
session_id: this.randomBoolean(0.3)
? `session_${this.randomInt(1, 100)}`
: undefined,
environment: "default",
metadata: { workflowType: "comprehensive-ai", purpose: "demonstration" },
tags: ["ai-agent", "multi-step", "comprehensive"],
public: true,
bookmarked: this.randomBoolean(0.2),
});
const observations: ObservationRecordInsertType[] = [];
const baseTime = Date.now();
// 1. AGENT - Main coordinator
observations.push(
createObservation({
id: `${traceId}-agent`,
trace_id: trace.id,
project_id: projectId,
type: "AGENT",
name: this.randomElement(REALISTIC_AGENT_NAMES),
input: "Plan and coordinate the research analysis workflow",
output:
"Workflow planned: retrieve documents → create embeddings → analyze → evaluate → check safety",
start_time: baseTime,
end_time: baseTime + 500,
level: "DEFAULT",
environment: trace.environment,
metadata: { role: "coordinator", step: "1" },
}),
);
// 2. RETRIEVER - Document retrieval
observations.push(
createObservation({
id: `${traceId}-retriever`,
trace_id: trace.id,
project_id: projectId,
parent_observation_id: `${traceId}-agent`,
type: "RETRIEVER",
name: this.randomElement(REALISTIC_RETRIEVER_NAMES),
input: "query: quantum computing research papers 2024",
output: "Retrieved 15 relevant research papers from arXiv and IEEE",
start_time: baseTime + 500,
end_time: baseTime + 2000,
level: "DEFAULT",
environment: trace.environment,
metadata: { documentsFound: "15", sources: "arXiv,IEEE" },
}),
);
// 3. EMBEDDING - Create document embeddings
observations.push(
createObservation({
id: `${traceId}-embedding`,
trace_id: trace.id,
project_id: projectId,
parent_observation_id: `${traceId}-retriever`,
type: "EMBEDDING",
name: this.randomElement(REALISTIC_EMBEDDING_NAMES),
input: "15 research paper abstracts and titles",
output: "Generated 1536-dimensional embeddings for semantic similarity",
start_time: baseTime + 2000,
end_time: baseTime + 3500,
level: "DEFAULT",
environment: trace.environment,
metadata: {
embeddingModel: "text-embedding-ada-002",
dimensions: "1536",
},
usage_details: {
input: this.randomInt(2000, 4000),
total: this.randomInt(2000, 4000),
},
provided_usage_details: {
input: this.randomInt(2000, 4000),
total: this.randomInt(2000, 4000),
},
cost_details: {
input: this.randomInt(5, 15) / 100000,
total: this.randomInt(5, 15) / 100000,
},
provided_cost_details: {
input: this.randomInt(5, 15) / 100000,
total: this.randomInt(5, 15) / 100000,
},
}),
);
// 4. CHAIN - Processing pipeline
observations.push(
createObservation({
id: `${traceId}-chain`,
trace_id: trace.id,
project_id: projectId,
parent_observation_id: `${traceId}-embedding`,
type: "CHAIN",
name: this.randomElement(REALISTIC_CHAIN_NAMES),
input: "Research papers with embeddings",
output:
"Processed and analyzed 15 papers through multi-step analysis chain",
start_time: baseTime + 3500,
end_time: baseTime + 8000,
level: "DEFAULT",
environment: trace.environment,
metadata: { steps: "4", processed: "15" },
}),
);
// 5. TOOL - External API call for additional context
observations.push(
createObservation({
id: `${traceId}-tool`,
trace_id: trace.id,
project_id: projectId,
parent_observation_id: `${traceId}-chain`,
type: "TOOL",
name: this.randomElement(REALISTIC_TOOL_NAMES),
input: "Search for quantum computing market trends",
output:
"Market data: $1.2B industry, 25% YoY growth, key players identified",
start_time: baseTime + 8000,
end_time: baseTime + 10000,
level: "DEFAULT",
environment: trace.environment,
metadata: { toolType: "api-call", endpoint: "market-research" },
}),
);
// 6. GENERATION - Final summary generation
observations.push(
createObservation({
id: `${traceId}-generation`,
trace_id: trace.id,
project_id: projectId,
parent_observation_id: `${traceId}-tool`,
type: "GENERATION",
name: this.randomElement(REALISTIC_GENERATION_NAMES),
input:
"Synthesize research analysis and market data into comprehensive summary",
output:
"Generated comprehensive 2000-word analysis of quantum computing research trends",
provided_model_name: this.randomElement(REALISTIC_MODELS),
model_parameters: JSON.stringify({
temperature: 0.3,
max_tokens: 2000,
}),
start_time: baseTime + 10000,
end_time: baseTime + 15000,
level: "DEFAULT",
environment: trace.environment,
usage_details: {
input: this.randomInt(1500, 2500),
output: this.randomInt(1800, 2200),
total: this.randomInt(3300, 4700),
},
provided_usage_details: {
input: this.randomInt(1500, 2500),
output: this.randomInt(1800, 2200),
total: this.randomInt(3300, 4700),
},
cost_details: {
input: this.randomInt(15, 25) / 100000,
output: this.randomInt(35, 45) / 100000,
total: this.randomInt(50, 70) / 100000,
},
provided_cost_details: {
input: this.randomInt(15, 25) / 100000,
output: this.randomInt(35, 45) / 100000,
total: this.randomInt(50, 70) / 100000,
},
}),
);
// 7. EVALUATOR - Quality evaluation
observations.push(
createObservation({
id: `${traceId}-evaluator`,
trace_id: trace.id,
project_id: projectId,
parent_observation_id: `${traceId}-generation`,
type: "EVALUATOR",
name: this.randomElement(REALISTIC_EVALUATOR_NAMES),
input: "Evaluate summary quality, accuracy, and completeness",
output: "Quality score: 8.7/10, High accuracy, Comprehensive coverage",
start_time: baseTime + 15000,
end_time: baseTime + 16500,
level: "DEFAULT",
environment: trace.environment,
metadata: {
qualityScore: "8.7",
accuracy: "high",
completeness: "comprehensive",
},
}),
);
// 8. GUARDRAIL - Safety and compliance check
observations.push(
createObservation({
id: `${traceId}-guardrail`,
trace_id: trace.id,
project_id: projectId,
parent_observation_id: `${traceId}-evaluator`,
type: "GUARDRAIL",
name: this.randomElement(REALISTIC_GUARDRAIL_NAMES),
input: "Check content for safety, bias, and compliance issues",
output:
"✓ Content approved: No safety issues, Low bias detected, Compliant",
start_time: baseTime + 16500,
end_time: baseTime + 17000,
level: "DEFAULT",
environment: trace.environment,
metadata: {
safetyCheck: "passed",
biasLevel: "low",
compliance: "approved",
},
}),
);
return { trace, observations };
}
/**
* Creates evaluation traces for testing evaluator configurations.
* Use for: Evaluation testing, score validation, evaluator development.
@@ -4,6 +4,7 @@ import { ClickHouseQueryBuilder } from "./clickhouse-builder";
import { EVAL_TRACE_COUNT, SEED_DATASETS } from "./postgres-seed-constants";
import {
clickhouseClient,
DatasetRunItemRecordInsertType,
logger,
ObservationRecordInsertType,
TraceRecordInsertType,
@@ -92,25 +93,25 @@ export class SeederOrchestrator {
logger.info(
`Processing run ${runNumber + 1}/${numberOfRuns} for project ${projectId}`,
);
// const now = Date.now();
const now = Date.now();
const traces: TraceRecordInsertType[] = [];
const observations: ObservationRecordInsertType[] = [];
// const datasetRunItems: DatasetRunItemRecordInsertType[] = [];
const datasetRunItems: DatasetRunItemRecordInsertType[] = [];
for (const seedDataset of SEED_DATASETS) {
for (const [itemIndex, datasetItem] of seedDataset.items.entries()) {
// // Generate dataset run item data
// const datasetRunItem = this.dataGenerator.generateDatasetRunItem(
// {
// datasetName: seedDataset.name,
// itemIndex,
// item: datasetItem,
// runNumber,
// runCreatedAt: now,
// },
// projectId,
// );
// Generate dataset run item data
const datasetRunItem = this.dataGenerator.generateDatasetRunItem(
{
datasetName: seedDataset.name,
itemIndex,
item: datasetItem,
runNumber,
runCreatedAt: now,
},
projectId,
);
// Generate trace data
const trace = this.dataGenerator.generateDatasetTrace(
@@ -137,14 +138,14 @@ export class SeederOrchestrator {
traces.push(trace);
observations.push(observation);
// datasetRunItems.push(datasetRunItem);
datasetRunItems.push(datasetRunItem);
}
}
try {
await this.queryBuilder.executeTracesInsert(traces);
await this.queryBuilder.executeObservationsInsert(observations);
// await this.queryBuilder.executeDatasetRunItemsInsert(datasetRunItems);
await this.queryBuilder.executeDatasetRunItemsInsert(datasetRunItems);
} catch (error) {
logger.error(`✗ Insert failed:`, error);
throw error;
+46 -1
View File
@@ -6,8 +6,27 @@ export const ObservationType = {
SPAN: "SPAN",
EVENT: "EVENT",
GENERATION: "GENERATION",
AGENT: "AGENT",
TOOL: "TOOL",
CHAIN: "CHAIN",
RETRIEVER: "RETRIEVER",
EVALUATOR: "EVALUATOR",
EMBEDDING: "EMBEDDING",
GUARDRAIL: "GUARDRAIL",
} as const;
export const ObservationTypeDomain = z.enum(["SPAN", "EVENT", "GENERATION"]);
export const ObservationTypeDomain = z.enum([
"SPAN",
"EVENT",
"GENERATION",
"AGENT",
"TOOL",
"CHAIN",
"RETRIEVER",
"EVALUATOR",
"EMBEDDING",
"GUARDRAIL",
]);
export type ObservationType = z.infer<typeof ObservationTypeDomain>;
export const ObservationLevel = {
@@ -74,3 +93,29 @@ export const ObservationSchema = z.object({
});
export type Observation = z.infer<typeof ObservationSchema>;
/**
* Returns true if an observation type is generation-like, meaning it could include LLM calls
* and potentially has similar input/output fields.
*/
export const GenerationLikeObservationTypes = [
ObservationType.GENERATION,
ObservationType.AGENT,
ObservationType.TOOL,
ObservationType.CHAIN,
ObservationType.RETRIEVER,
ObservationType.EVALUATOR,
ObservationType.EMBEDDING,
ObservationType.GUARDRAIL,
] as const;
export const isGenerationLike = (observationType: ObservationType): boolean => {
return GenerationLikeObservationTypes.includes(observationType as any);
};
/**
* Returns all generation-like observation types for use in filters and queries.
*/
export const getGenerationLikeTypes = (): ObservationType[] => {
return [...GenerationLikeObservationTypes];
};
+10 -6
View File
@@ -22,8 +22,8 @@ export function encrypt(plainText: string): string {
const iv = crypto.randomBytes(IV_LENGTH); // Directly use Buffer returned by randomBytes
const cipher = crypto.createCipheriv(
"aes-256-gcm",
Buffer.from(ENCRYPTION_KEY, "hex"),
iv,
new Uint8Array(Buffer.from(ENCRYPTION_KEY, "hex")),
new Uint8Array(iv),
);
let encrypted = cipher.update(plainText, "utf8", "hex");
encrypted += cipher.final("hex");
@@ -48,12 +48,16 @@ export function decrypt(text: string): string {
const decipher = crypto.createDecipheriv(
"aes-256-gcm",
Buffer.from(ENCRYPTION_KEY, "hex"),
iv,
new Uint8Array(Buffer.from(ENCRYPTION_KEY, "hex")),
new Uint8Array(iv),
);
decipher.setAuthTag(authTag);
decipher.setAuthTag(new Uint8Array(authTag));
let decrypted = decipher.update(encryptedText, undefined, "utf8");
let decrypted = decipher.update(
new Uint8Array(encryptedText),
undefined,
"utf8",
);
decrypted += decipher.final("utf8");
return decrypted.toString();
+30 -9
View File
@@ -50,6 +50,10 @@ const EnvSchema = z.object({
.nonnegative()
.default(15_000),
LANGFUSE_INGESTION_QUEUE_SHARD_COUNT: z.coerce.number().positive().default(1),
LANGFUSE_TRACE_UPSERT_QUEUE_SHARD_COUNT: z.coerce
.number()
.positive()
.default(1),
LANGFUSE_TRACE_DELETE_DELAY_MS: z.coerce
.number()
.nonnegative()
@@ -99,6 +103,10 @@ const EnvSchema = z.object({
LANGFUSE_GOOGLE_CLOUD_STORAGE_CREDENTIALS: z.string().optional(),
STRIPE_SECRET_KEY: z.string().optional(),
LANGFUSE_ENABLE_BLOB_STORAGE_FILE_LOG: z
.enum(["true", "false"])
.default("true"),
LANGFUSE_S3_LIST_MAX_KEYS: z.coerce.number().positive().default(200),
LANGFUSE_S3_CORE_DATA_EXPORT_IS_ENABLED: z
.enum(["true", "false"])
@@ -115,16 +123,9 @@ const EnvSchema = z.object({
LANGFUSE_API_TRACE_OBSERVATIONS_SIZE_LIMIT_BYTES: z.coerce
.number()
.default(80e6), // 80MB
LANGFUSE_CLICKHOUSE_DELETION_TIMEOUT_MS: z.coerce.number().default(240_000), // 4 minutes
LANGFUSE_CLICKHOUSE_DELETION_TIMEOUT_MS: z.coerce.number().default(600_000), // 10 minutes
LANGFUSE_CLICKHOUSE_QUERY_MAX_ATTEMPTS: z.coerce.number().default(3), // Maximum attempts for socket hang up errors
LANGFUSE_SKIP_S3_LIST_FOR_OBSERVATIONS_PROJECT_IDS: z.string().optional(),
// Dataset Run Items Migration Environment Variables
LANGFUSE_EXPERIMENT_DATASET_RUN_ITEMS_WRITE_CH: z
.enum(["true", "false"])
.default("false"),
LANGFUSE_EXPERIMENT_DATASET_RUN_ITEMS_READ_CH: z
.enum(["true", "false"])
.default("false"),
LANGFUSE_EXPERIMENT_COMPARE_READ_FROM_AGGREGATING_MERGE_TREES: z
.enum(["true", "false"])
.default("false"),
@@ -154,6 +155,12 @@ const EnvSchema = z.object({
LANGFUSE_EXPERIMENT_INSERT_INTO_AGGREGATING_MERGE_TREES: z
.enum(["true", "false"])
.default("false"),
LANGFUSE_EXPERIMENT_WHITELISTED_AMT_TABLES: z
.string()
.optional()
.transform((s) =>
s ? s.split(",").map((s) => s.toLowerCase().trim()) : [],
),
LANGFUSE_INGESTION_PROCESSING_SAMPLED_PROJECTS: z
.string()
.optional()
@@ -186,10 +193,18 @@ const EnvSchema = z.object({
return new Map<string, number>();
}
}),
SLACK_CLIENT_ID: z.string().optional(),
SLACK_CLIENT_SECRET: z.string().optional(),
SLACK_STATE_SECRET: z.string().optional(),
SLACK_FETCH_LIMIT: z.coerce
.number()
.positive()
.optional()
.default(1_000)
.describe(
"How many records should be fetched from Slack, before we give up",
),
HTTPS_PROXY: z.string().optional(),
LANGFUSE_SERVER_SIDE_IO_CHAR_LIMIT: z.coerce
.number()
@@ -202,6 +217,12 @@ const EnvSchema = z.object({
.int()
.positive()
.default(600_000), // 10 minutes
LANGFUSE_FETCH_LLM_COMPLETION_TIMEOUT_MS: z.coerce
.number()
.int()
.positive()
.default(120_000), // 2 minutes
});
export const env: z.infer<typeof EnvSchema> =
@@ -7,4 +7,5 @@ export const QUEUE_ERROR_MESSAGES = {
TOO_LOW_MAX_TOKENS_ERROR: "Error: Unterminated string in JSON at position",
OUTPUT_TOKENS_TOO_LONG_ERROR:
"Could not parse response content as the length limit was reached",
TIMEOUT_ERROR: "Request timed out",
};
+57 -15
View File
@@ -5,6 +5,13 @@ export const langfuseObjects = [
"span",
"generation",
"event",
"agent",
"tool",
"chain",
"retriever",
"evaluator",
"embedding",
"guardrail",
"dataset_item",
] as const;
@@ -51,6 +58,56 @@ const observationCols = [
];
export const availableTraceEvalVariables = [
{
id: "agent",
display: "Agent",
availableColumns: observationCols,
},
{
id: "chain",
display: "Chain",
availableColumns: observationCols,
},
{
id: "embedding",
display: "Embedding",
availableColumns: observationCols,
},
{
id: "evaluator",
display: "Evaluator",
availableColumns: observationCols,
},
{
id: "event",
display: "Event",
availableColumns: observationCols,
},
{
id: "generation",
display: "Generation",
availableColumns: observationCols,
},
{
id: "guardrail",
display: "Guardrail",
availableColumns: observationCols,
},
{
id: "retriever",
display: "Retriever",
availableColumns: observationCols,
},
{
id: "span",
display: "Span",
availableColumns: observationCols,
},
{
id: "tool",
display: "Tool",
availableColumns: observationCols,
},
{
id: "trace",
display: "Trace",
@@ -65,21 +122,6 @@ export const availableTraceEvalVariables = [
{ name: "Output", id: "output", internal: 't."output"' },
],
},
{
id: "span",
display: "Span",
availableColumns: observationCols,
},
{
id: "generation",
display: "Generation",
availableColumns: observationCols,
},
{
id: "event",
display: "Event",
availableColumns: observationCols,
},
];
export const availableDatasetEvalVariables = [
@@ -27,16 +27,42 @@ export const parseUnknownToString = (value: unknown): string => {
return String(value);
};
/**
* Recursively parses JSON strings that may have been encoded multiple times.
* This handles cases where data has been JSON.stringify'd multiple times.
*
* @param value - The potentially multi-encoded JSON string
* @returns The final parsed object or the original value if parsing fails
*/
function parseMultiEncodedJson(value: unknown): unknown {
if (typeof value !== "string") {
return value;
}
try {
const parsed = JSON.parse(value);
// If result is still a string, it might be double-encoded - recurse
if (typeof parsed === "string") {
return parseMultiEncodedJson(parsed);
}
return parsed;
} catch {
// If parsing fails, return original value
return value;
}
}
function parseJsonDefault(selectedColumn: unknown, jsonSelector: string) {
// selectedColumn should already be preprocessed by preprocessObjectWithJsonFields
// so we can directly use it with JSONPath
const result = JSONPath({
path: jsonSelector,
json:
typeof selectedColumn === "string"
? JSON.parse(selectedColumn)
: selectedColumn,
json: selectedColumn as any, // JSONPath accepts unknown but types are strict
});
return result.length > 0 ? result[0] : undefined;
return Array.isArray(result) && result.length > 0 ? result[0] : undefined;
}
export function extractValueFromObject(
@@ -44,7 +70,13 @@ export function extractValueFromObject(
mapping: z.infer<typeof variableMapping>,
parseJson?: (selectedColumn: unknown, jsonSelector: string) => unknown, // eslint-disable-line no-unused-vars
): { value: string; error: Error | null } {
const selectedColumn = obj[mapping.selectedColumnId];
let selectedColumn = obj[mapping.selectedColumnId];
// Simple preprocessing: attempt to parse to valid JSON object
if (typeof selectedColumn === "string") {
selectedColumn = parseMultiEncodedJson(selectedColumn);
}
const jsonParser = parseJson || parseJsonDefault;
let jsonSelectedColumn;
@@ -2,7 +2,7 @@ export const ClickhouseTableNames = {
traces: "traces",
observations: "observations",
scores: "scores",
dataset_run_items: "dataset_run_items",
dataset_run_items_rmt: "dataset_run_items_rmt",
// Virtual tables for dashboards
// TODO: Check if we can do this more elegantly
@@ -27,6 +27,13 @@ export const getClickhouseEntityType = (
case eventTypes.SPAN_UPDATE:
case eventTypes.GENERATION_CREATE:
case eventTypes.GENERATION_UPDATE:
case eventTypes.AGENT_CREATE:
case eventTypes.TOOL_CREATE:
case eventTypes.CHAIN_CREATE:
case eventTypes.RETRIEVER_CREATE:
case eventTypes.EVALUATOR_CREATE:
case eventTypes.EMBEDDING_CREATE:
case eventTypes.GUARDRAIL_CREATE:
return "observation";
case eventTypes.SCORE_CREATE:
return "score";
@@ -88,7 +88,7 @@ async function removeIngestionEventsFromS3AndDeleteClickhouseRefs(p: {
);
await softDeleteInClickhouse(blobStorageRefs);
logger.info(
`Deleted batch ${batch} of size ${blobStorageRefs.length} for ${projectId} of deleting s3 refs`,
`Deleted last batch ${batch} of size ${blobStorageRefs.length} for ${projectId} of deleting s3 refs`,
);
}
@@ -1,94 +0,0 @@
import { env } from "../../env";
import { logger } from "../../server/logger";
import {
DatasetRunItemsExecutionStrategy,
DatasetRunItemsOperationType,
} from "./types";
/**
* Returns the execution strategy for dataset run items based on environment variables.
*
* Two-phase migration approach:
* 1. Dual-write phase: DATASET_RUN_ITEMS_WRITE_TO_CLICKHOUSE=true (write to both databases)
* 2. Read migration phase: DATASET_RUN_ITEMS_READ_FROM_CLICKHOUSE=true (read from ClickHouse)
*/
function getDatasetRunItemsExecutionStrategy(): DatasetRunItemsExecutionStrategy {
return {
shouldWriteToClickHouse:
env.LANGFUSE_EXPERIMENT_DATASET_RUN_ITEMS_WRITE_CH === "true",
shouldReadFromClickHouse:
env.LANGFUSE_EXPERIMENT_DATASET_RUN_ITEMS_READ_CH === "true",
};
}
// Re-export the enum for backward compatibility
/**
* Executes the appropriate database operation based on the execution strategy.
*
* @param postgresExecution - Function to execute PostgreSQL operation
* @param clickhouseExecution - Function to execute ClickHouse operation
* @param operationType - Type of operation ("read" or "write")
* @returns Result from the selected execution strategy
*/
export async function executeWithDatasetRunItemsStrategy<TInput, TOutput>({
input,
operationType,
postgresExecution,
clickhouseExecution,
}: {
input: TInput;
operationType: DatasetRunItemsOperationType;
// eslint-disable-next-line no-unused-vars
postgresExecution: (input: TInput) => Promise<TOutput>;
// eslint-disable-next-line no-unused-vars
clickhouseExecution: (input: TInput) => Promise<TOutput>;
}): Promise<TOutput> {
const strategy = getDatasetRunItemsExecutionStrategy();
if (operationType === DatasetRunItemsOperationType.WRITE) {
// For write operations, implement dual-write strategy
if (strategy.shouldWriteToClickHouse) {
// Dual-write phase: write to both databases
const postgresResult = await postgresExecution(input);
try {
await clickhouseExecution(input);
logger.debug("Successfully wrote to both PostgreSQL and ClickHouse", {
operation: `dataset_run_items_${operationType}`,
});
} catch (error) {
logger.error("ClickHouse write failed during dual-write phase", {
error: error instanceof Error ? error.message : String(error),
operation: `dataset_run_items_${operationType}`,
});
// Continue with PostgreSQL result since it succeeded
}
return postgresResult;
} else {
// Write only to PostgreSQL
return await postgresExecution(input);
}
} else {
// For read operations, rely on the strategy
const shouldExecuteClickhouse = strategy.shouldReadFromClickHouse;
if (shouldExecuteClickhouse) {
try {
return await clickhouseExecution(input);
} catch (error) {
logger.error(
"ClickHouse execution failed, falling back to PostgreSQL",
{
error: error instanceof Error ? error.message : String(error),
operation: `dataset_run_items_${operationType}`,
},
);
// Fallback to PostgreSQL for reliability
return await postgresExecution(input);
}
} else {
return await postgresExecution(input);
}
}
}
@@ -1,16 +0,0 @@
/**
* Types and enums for dataset run items execution.
* This file is frontend-safe and doesn't import server-side dependencies.
*/
export enum DatasetRunItemsOperationType {
// eslint-disable-next-line no-unused-vars
READ = "read",
// eslint-disable-next-line no-unused-vars
WRITE = "write",
}
export type DatasetRunItemsExecutionStrategy = {
shouldWriteToClickHouse: boolean;
shouldReadFromClickHouse: boolean;
};
@@ -0,0 +1,82 @@
import { logger } from "./logger";
import { redis } from "./";
/**
* Redis cache utilities for eval job configuration optimization.
*
* This module provides caching functionality to reduce database calls when
* checking for job configurations. When a project has no active evaluation
* job configurations, we cache this information in Redis to avoid unnecessary
* database queries and queue processing.
*/
const NO_JOB_CONFIG_PREFIX = "langfuse:eval:no-job-configs";
/**
* Check if a project has no job configurations cached in Redis.
* Returns true if the cache indicates no eval job configs exist.
*
* @param projectId - The project ID to check cache for
* @returns Promise<boolean> - true if no eval job configs present
*/
export const hasNoJobConfigsCache = async (
projectId: string,
): Promise<boolean> => {
if (!redis) {
return false;
}
try {
const cacheKey = `${NO_JOB_CONFIG_PREFIX}:${projectId}`;
const cached = await redis.get(cacheKey);
return Boolean(cached);
} catch (error) {
logger.error("Failed to check no eval job configs cache", error);
return false;
}
};
/**
* Cache that a project has no active job configurations.
* This is set when a database query returns no active EVAL job configurations.
* The cache expires after 10 minutes to ensure eventual consistency.
*
* @param projectId - The project ID to cache
*/
export const setNoJobConfigsCache = async (
projectId: string,
): Promise<void> => {
if (!redis) {
return;
}
try {
const cacheKey = `${NO_JOB_CONFIG_PREFIX}:${projectId}`;
await redis.setex(cacheKey, 600, "1"); // Cache for 10 minutes
logger.debug(`Cached no eval job configs for project ${projectId}`);
} catch (error) {
logger.error("Failed to cache no eval job configs status", error);
}
};
/**
* Clear the "no eval job configs" cache for a project.
* Should be called when job configurations are created or activated.
*
* @param projectId - The project ID to clear cache for
*/
export const clearNoJobConfigsCache = async (
projectId: string,
): Promise<void> => {
if (!redis) {
return;
}
try {
const cacheKey = `${NO_JOB_CONFIG_PREFIX}:${projectId}`;
await redis.del(cacheKey);
logger.debug(`Cleared no eval job configs cache for project ${projectId}`);
} catch (error) {
logger.error("Failed to clear no eval job configs cache", error);
}
};
+3 -3
View File
@@ -7,6 +7,7 @@ export * from "./services/PromptService";
export * from "./services/PromptService/types";
export * from "./services/traces-ui-table-service";
export * from "./services/InMemoryFilterService";
export * from "./evalJobConfigCache";
export * from "./auth/apiKeys";
export * from "./auth/customSsoProvider";
export * from "./auth/gitHubEnterpriseProvider";
@@ -14,6 +15,7 @@ export * from "./llm/fetchLLMCompletion";
export * from "./llm/utils";
export * from "./llm/types";
export * from "./llm/compileChatMessages";
export * from "./llm/testModelCall";
export * from "./utils/DatabaseReadStream";
export * from "./utils/transforms";
export * from "./clickhouse/client";
@@ -61,19 +63,17 @@ export * from "./repositories";
export * from "./utils/rendering";
export * from "./redis/evalExecutionQueue";
export * from "./services/sessions-ui-table-service";
export * from "./services/datasets-ui-table-service";
export * from "./services/DashboardService";
export * from "./services/TableViewService";
export * from "./services/DefaultEvaluationModelService";
export * from "./clickhouse/measureAndReturn";
export * from "./services/SlackService";
export * from "./tableMappings";
export * from "./data-deletion/ingestionFileDeletion";
export * from "./s3";
// dataset run items
export * from "./dataset-run-items/datasetExecution";
export * from "./dataset-run-items/types";
export * from "./dataset-run-items/addToDeleteQueue";
// test utils
@@ -16,6 +16,8 @@ export type ModelMatchProps = {
model: string;
};
const MODEL_MATCH_CACHE_LOCKED_KEY = "LOCK:model-match-clear";
export async function findModel(p: ModelMatchProps): Promise<Model | null> {
return instrumentAsync(
{
@@ -78,6 +80,14 @@ const getModelFromRedis = async (
}
try {
if (await isModelMatchCacheLocked()) {
logger.info(
"Model match cache is locked. Skipping model lookup from Redis.",
);
return null;
}
const key = getRedisModelKey(p);
const redisModel = await redis?.get(key);
if (redisModel) {
@@ -241,6 +251,70 @@ export async function clearModelCacheForProject(
);
}
} catch (error) {
logger.error(`Error clearing model cache for project ${projectId}`, error);
logger.error(
`Error clearing model cache for project ${projectId}: ${error}`,
);
}
}
export async function isModelMatchCacheLocked() {
try {
return Boolean(await redis?.exists(MODEL_MATCH_CACHE_LOCKED_KEY));
} catch (err) {
logger.error("Failed to check whether model match is locked", err);
return false;
}
}
export async function clearFullModelCache() {
if (env.LANGFUSE_CACHE_MODEL_MATCH_ENABLED === "false" || !redis) {
return;
}
try {
// Use lock to protect for concurrent executions
// This function is called on worker startup, so we want to avoid all workers triggering this delete
if (await isModelMatchCacheLocked()) {
logger.info("Model cache clearing already in progress; skipping.");
return;
}
const startTime = Date.now();
logger.info("Clearing full model cache...");
const tenMinutesInSeconds = 60 * 10;
await redis.setex(
MODEL_MATCH_CACHE_LOCKED_KEY,
tenMinutesInSeconds,
"locked",
);
const pattern = getModelMatchKeyPrefix() + "*";
const keys =
env.REDIS_CLUSTER_ENABLED === "true"
? (
await Promise.all(
(redis as Cluster)
.nodes("master")
.map((node) => node.keys(pattern) || []),
)
).flat()
: await redis.keys(pattern);
if (keys.length > 0) {
await safeMultiDel(redis, keys);
logger.info(
`Cleared full model cache with ${keys.length} keys in ${Date.now() - startTime}ms.`,
);
} else {
logger.info(`No keys found for match pattern '${pattern}'`);
}
} catch (error) {
logger.error(`Error clearing full model cache: ${error}`);
} finally {
await redis?.del(MODEL_MATCH_CACHE_LOCKED_KEY);
}
}
+71 -1
View File
@@ -223,6 +223,13 @@ export const eventTypes = {
SPAN_UPDATE: "span-update",
GENERATION_CREATE: "generation-create",
GENERATION_UPDATE: "generation-update",
AGENT_CREATE: "agent-create",
TOOL_CREATE: "tool-create",
CHAIN_CREATE: "chain-create",
RETRIEVER_CREATE: "retriever-create",
EVALUATOR_CREATE: "evaluator-create",
EMBEDDING_CREATE: "embedding-create",
GUARDRAIL_CREATE: "guardrail-create",
SDK_LOG: "sdk-log",
DATASET_RUN_ITEM_CREATE: "dataset-run-item-create",
// LEGACY, only required for backwards compatibility
@@ -564,6 +571,41 @@ const createAllIngestionSchemas = ({
body: UpdateGenerationBody,
});
const agentCreateEvent = base.extend({
type: z.literal(eventTypes.AGENT_CREATE),
body: CreateGenerationBody,
});
const toolCreateEvent = base.extend({
type: z.literal(eventTypes.TOOL_CREATE),
body: CreateGenerationBody,
});
const chainCreateEvent = base.extend({
type: z.literal(eventTypes.CHAIN_CREATE),
body: CreateGenerationBody,
});
const retrieverCreateEvent = base.extend({
type: z.literal(eventTypes.RETRIEVER_CREATE),
body: CreateGenerationBody,
});
const evaluatorCreateEvent = base.extend({
type: z.literal(eventTypes.EVALUATOR_CREATE),
body: CreateGenerationBody,
});
const embeddingCreateEvent = base.extend({
type: z.literal(eventTypes.EMBEDDING_CREATE),
body: CreateGenerationBody,
});
const guardrailCreateEvent = base.extend({
type: z.literal(eventTypes.GUARDRAIL_CREATE),
body: CreateGenerationBody,
});
const scoreEvent = base.extend({
type: z.literal(eventTypes.SCORE_CREATE),
body: ScoreBody,
@@ -603,6 +645,13 @@ const createAllIngestionSchemas = ({
spanUpdateEvent,
generationCreateEvent,
generationUpdateEvent,
agentCreateEvent,
toolCreateEvent,
chainCreateEvent,
retrieverCreateEvent,
evaluatorCreateEvent,
embeddingCreateEvent,
guardrailCreateEvent,
sdkLogEvent,
datasetRunItemCreateEvent,
// LEGACY, only required for backwards compatibility
@@ -629,6 +678,13 @@ const createAllIngestionSchemas = ({
spanUpdateEvent,
generationCreateEvent,
generationUpdateEvent,
agentCreateEvent,
toolCreateEvent,
chainCreateEvent,
retrieverCreateEvent,
evaluatorCreateEvent,
embeddingCreateEvent,
guardrailCreateEvent,
scoreEvent,
datasetRunItemCreateEvent,
sdkLogEvent,
@@ -666,6 +722,13 @@ export const spanCreateEvent = publicSchemas.spanCreateEvent;
export const spanUpdateEvent = publicSchemas.spanUpdateEvent;
export const generationCreateEvent = publicSchemas.generationCreateEvent;
export const generationUpdateEvent = publicSchemas.generationUpdateEvent;
export const agentCreateEvent = publicSchemas.agentCreateEvent;
export const toolCreateEvent = publicSchemas.toolCreateEvent;
export const chainCreateEvent = publicSchemas.chainCreateEvent;
export const retrieverCreateEvent = publicSchemas.retrieverCreateEvent;
export const evaluatorCreateEvent = publicSchemas.evaluatorCreateEvent;
export const embeddingCreateEvent = publicSchemas.embeddingCreateEvent;
export const guardrailCreateEvent = publicSchemas.guardrailCreateEvent;
export const scoreEvent = publicSchemas.scoreEvent;
export const sdkLogEvent = publicSchemas.sdkLogEvent;
export const datasetRunItemCreateEvent =
@@ -707,4 +770,11 @@ export type ObservationEvent =
| z.infer<typeof spanCreateEvent>
| z.infer<typeof spanUpdateEvent>
| z.infer<typeof generationCreateEvent>
| z.infer<typeof generationUpdateEvent>;
| z.infer<typeof generationUpdateEvent>
| z.infer<typeof agentCreateEvent>
| z.infer<typeof toolCreateEvent>
| z.infer<typeof chainCreateEvent>
| z.infer<typeof retrieverCreateEvent>
| z.infer<typeof evaluatorCreateEvent>
| z.infer<typeof embeddingCreateEvent>
| z.infer<typeof guardrailCreateEvent>;
@@ -42,6 +42,7 @@ import {
} from "./types";
import { CallbackHandler } from "langfuse-langchain";
import type { BaseCallbackHandler } from "@langchain/core/callbacks/base";
import { HttpsProxyAgent } from "https-proxy-agent";
const isLangfuseCloud = Boolean(env.NEXT_PUBLIC_LANGFUSE_CLOUD_REGION);
@@ -217,6 +218,11 @@ export async function fetchLLMCompletion(
(m) => m.content.length > 0 || "tool_calls" in m,
);
// Common proxy configuration for all adapters
const proxyUrl = env.HTTPS_PROXY;
const proxyAgent = proxyUrl ? new HttpsProxyAgent(proxyUrl) : undefined;
const timeoutMs = env.LANGFUSE_FETCH_LLM_COMPLETION_TIMEOUT_MS;
let chatModel:
| ChatOpenAI
| ChatAnthropic
@@ -232,7 +238,12 @@ export async function fetchLLMCompletion(
maxTokens: modelParams.max_tokens,
topP: modelParams.top_p,
callbacks: finalCallbacks,
clientOptions: { maxRetries, timeout: 1000 * 60 * 2 }, // 2 minutes timeout
clientOptions: {
maxRetries,
timeout: timeoutMs,
...(proxyAgent && { httpAgent: proxyAgent }),
},
invocationKwargs: modelParams.providerOptions,
});
} else if (modelParams.adapter === LLMAdapter.OpenAI) {
chatModel = new ChatOpenAI({
@@ -247,8 +258,10 @@ export async function fetchLLMCompletion(
configuration: {
baseURL,
defaultHeaders: extraHeaders,
...(proxyAgent && { httpAgent: proxyAgent }),
},
timeout: 1000 * 60 * 2, // 2 minutes timeout
modelKwargs: modelParams.providerOptions,
timeout: timeoutMs,
});
} else if (modelParams.adapter === LLMAdapter.Azure) {
chatModel = new AzureChatOpenAI({
@@ -261,10 +274,12 @@ export async function fetchLLMCompletion(
topP: modelParams.top_p,
callbacks: finalCallbacks,
maxRetries,
timeout: 1000 * 60 * 2, // 2 minutes timeout
timeout: timeoutMs,
configuration: {
defaultHeaders: extraHeaders,
...(proxyAgent && { httpAgent: proxyAgent }),
},
modelKwargs: modelParams.providerOptions,
});
} else if (modelParams.adapter === LLMAdapter.Bedrock) {
const { region } = BedrockConfigSchema.parse(config);
@@ -283,7 +298,8 @@ export async function fetchLLMCompletion(
topP: modelParams.top_p,
callbacks: finalCallbacks,
maxRetries,
timeout: 1000 * 60 * 2, // 2 minutes timeout
timeout: timeoutMs,
additionalModelRequestFields: modelParams.providerOptions as any,
});
} else if (modelParams.adapter === LLMAdapter.VertexAI) {
const credentials = GCPServiceAccountKeySchema.parse(JSON.parse(apiKey));
@@ -340,51 +356,6 @@ export async function fetchLLMCompletion(
};
}
/*
Workaround OpenAI reasoning models:
This is a temporary workaround to avoid sending unsupported parameters to OpenAI's O1 models.
O1 models do not support:
- system messages
- top_p
- max_tokens at all, one has to use max_completion_tokens instead
- temperature different than 1
Reference: https://platform.openai.com/docs/guides/reasoning/beta-limitations
*/
if (
modelParams.model.startsWith("o1-") ||
modelParams.model.startsWith("o3-")
) {
const filteredMessages = finalMessages.filter((message) => {
return (
modelParams.model.startsWith("o3-") || message._getType() !== "system"
);
});
return {
completion: await new ChatOpenAI({
openAIApiKey: apiKey,
modelName: modelParams.model,
temperature: 1,
maxTokens: undefined,
topP: undefined,
callbacks,
maxRetries,
modelKwargs: {
max_completion_tokens: modelParams.max_tokens,
},
configuration: {
baseURL,
},
timeout: 1000 * 60 * 2, // 2 minutes timeout
})
.pipe(new StringOutputParser())
.invoke(filteredMessages, runConfig),
processTracedEvents,
};
}
if (tools && tools.length > 0) {
const langchainTools = tools.map((tool) => ({
type: "function",
@@ -0,0 +1,52 @@
import { z as zodV3 } from "zod/v3";
import {
ChatMessageRole,
ChatMessageType,
LLMApiKeySchema,
type ModelConfig,
} from "./types";
import { decrypt } from "../../encryption";
import { fetchLLMCompletion } from "./fetchLLMCompletion";
import { decryptAndParseExtraHeaders } from "./utils";
import z from "zod/v4";
export const testModelCall = async ({
provider,
model,
apiKey,
prompt,
modelConfig,
}: {
provider: string;
model: string;
apiKey: z.infer<typeof LLMApiKeySchema>;
prompt?: string;
modelConfig?: ModelConfig | null;
}) => {
(
await fetchLLMCompletion({
streaming: false,
apiKey: decrypt(apiKey.secretKey), // decrypt the secret key
extraHeaders: decryptAndParseExtraHeaders(apiKey.extraHeaders),
baseURL: apiKey.baseURL ?? undefined,
messages: [
{
role: ChatMessageRole.User,
content: prompt ?? "mock content",
type: ChatMessageType.User,
},
],
modelParams: {
provider: provider,
model: model,
adapter: apiKey.adapter,
...modelConfig,
},
structuredOutputSchema: zodV3.object({
score: zodV3.string(),
reasoning: zodV3.string(),
}),
config: apiKey.config,
})
).completion;
};
+10 -2
View File
@@ -6,6 +6,7 @@ import {
} from "../../interfaces/customLLMProviderConfigSchemas";
import { TokenCountDelegate } from "../ingestion/processEventBatch";
import { AuthHeaderValidVerificationResult } from "../auth/types";
import { JSONObjectSchema } from "../../utils/zod";
/* eslint-disable no-unused-vars */
// disable lint as this is exported and used in web/worker
@@ -271,6 +272,7 @@ export const ZodModelConfig = z.object({
max_tokens: z.coerce.number().optional(),
temperature: z.coerce.number().optional(),
top_p: z.coerce.number().optional(),
providerOptions: JSONObjectSchema.optional(),
});
// Experiment config
@@ -285,7 +287,7 @@ export const ExperimentMetadataSchema = z
.strict();
export type ExperimentMetadata = z.infer<typeof ExperimentMetadataSchema>;
// NOTE: Update docs page when changing this! https://langfuse.com/docs/playground#openai-playground--anthropic-playground
// NOTE: Update docs page when changing this! https://langfuse.com/docs/prompt-management/features/playground#openai-playground--anthropic-playground
// WARNING: The first entry in the array is chosen as the default model to add LLM API keys
export const openAIModels = [
"gpt-4.1",
@@ -294,6 +296,12 @@ export const openAIModels = [
"gpt-4.1-mini-2025-04-14",
"gpt-4.1-nano",
"gpt-4.1-nano-2025-04-14",
"gpt-5",
"gpt-5-2025-08-07",
"gpt-5-mini",
"gpt-5-mini-2025-08-07",
"gpt-5-nano",
"gpt-5-nano-2025-08-07",
"o3",
"o3-2025-04-16",
"o4-mini",
@@ -327,7 +335,7 @@ export const openAIModels = [
export type OpenAIModel = (typeof openAIModels)[number];
// NOTE: Update docs page when changing this! https://langfuse.com/docs/playground#openai-playground--anthropic-playground
// NOTE: Update docs page when changing this! https://langfuse.com/docs/prompt-management/features/playground#openai-playground--anthropic-playground
// WARNING: The first entry in the array is chosen as the default model to add LLM API keys
export const anthropicModels = [
"claude-sonnet-4-20250514",
@@ -7,6 +7,7 @@ export type ClickhouseOperator =
export interface Filter {
apply(): ClickhouseFilter;
clickhouseTable: string;
tablePrefix?: string;
operator: ClickhouseOperator;
field: string;
}
@@ -20,7 +21,7 @@ export class StringFilter implements Filter {
public field: string;
public value: string;
public operator: (typeof filterOperators)["string"][number];
protected tablePrefix?: string;
public tablePrefix?: string;
constructor(opts: {
clickhouseTable: string;
@@ -74,7 +75,7 @@ export class NumberFilter implements Filter {
public value: number;
public operator: (typeof filterOperators)["number"][number] | "!=";
public clickhouseTypeOverwrite?: string;
protected tablePrefix?: string;
public tablePrefix?: string;
constructor(opts: {
clickhouseTable: string;
@@ -108,7 +109,7 @@ export class DateTimeFilter implements Filter {
public field: string;
public value: Date;
public operator: (typeof filterOperators)["datetime"][number];
protected tablePrefix?: string;
public tablePrefix?: string;
constructor(opts: {
clickhouseTable: string;
@@ -139,7 +140,7 @@ export class StringOptionsFilter implements Filter {
public field: string;
public values: string[];
public operator: (typeof filterOperators.stringOptions)[number];
protected tablePrefix?: string;
public tablePrefix?: string;
constructor(opts: {
clickhouseTable: string;
@@ -174,7 +175,7 @@ export class CategoryOptionsFilter implements Filter {
public key: string;
public values: string[];
public operator: (typeof filterOperators.categoryOptions)[number];
protected tablePrefix?: string;
public tablePrefix?: string;
constructor(opts: {
clickhouseTable: string;
@@ -229,7 +230,7 @@ export class StringObjectFilter implements Filter {
public key: string;
public value: string;
public operator: (typeof filterOperators)["stringObject"][number];
protected tablePrefix?: string;
public tablePrefix?: string;
constructor(opts: {
clickhouseTable: string;
@@ -287,7 +288,7 @@ export class ArrayOptionsFilter implements Filter {
public field: string;
public values: string[];
public operator: (typeof filterOperators.arrayOptions)[number];
protected tablePrefix?: string;
public tablePrefix?: string;
constructor(opts: {
clickhouseTable: string;
@@ -333,7 +334,7 @@ export class NullFilter implements Filter {
public clickhouseTable: string;
public field: string;
public operator: (typeof filterOperators)["null"][number];
protected tablePrefix?: string;
public tablePrefix?: string;
constructor(opts: {
clickhouseTable: string;
@@ -361,7 +362,7 @@ export class NumberObjectFilter implements Filter {
public key: string;
public value: number;
public operator: (typeof filterOperators)["numberObject"][number] | "!=";
protected tablePrefix?: string;
public tablePrefix?: string;
constructor(opts: {
clickhouseTable: string;
@@ -395,7 +396,7 @@ export class BooleanFilter implements Filter {
public field: string;
public operator: (typeof filterOperators)["boolean"][number];
public value: boolean;
protected tablePrefix?: string;
public tablePrefix?: string;
constructor(opts: {
clickhouseTable: string;
@@ -442,6 +443,11 @@ export class FilterList {
return new FilterList(this.filters.filter(predicate));
}
// eslint-disable-next-line no-unused-vars
map(predicate: (filter: Filter) => Filter) {
return new FilterList(this.filters.map(predicate));
}
// eslint-disable-next-line no-unused-vars
some(predicate: (filter: Filter) => boolean) {
return this.filters.some(predicate);
@@ -8,6 +8,7 @@ type OrderByStateNotNull = Exclude<OrderByState, null>;
export function orderByToClickhouseSql(
orderBy: OrderByState | OrderByState[] = [],
tableColumns: UiColumnMappings,
usedInAggregation = false,
): string {
if (
!orderBy ||
@@ -44,9 +45,11 @@ export function orderByToClickhouseSql(
throw new Error("Invalid order: " + ob.order);
}
const column = `${col.queryPrefix ? col.queryPrefix + "." : ""}${col.clickhouseSelect}`;
// Append the order by clause to the array
orderByClauses.push(
`${col.queryPrefix ? col.queryPrefix + "." : ""}${col.clickhouseSelect} ${order.data}`,
`${usedInAggregation ? `anyLast(${column})` : column} ${order.data}`,
);
}
@@ -10,9 +10,10 @@ export const clickhouseSearchCondition = (
) => {
const prefix = tablePrefix ? `${tablePrefix}.` : "";
// We use a hard-coded prefix for user_id as it only occurs in the trace context.
const conditions = [
!searchType || searchType.includes("id")
? `${prefix}id ILIKE {searchString: String} OR user_id ILIKE {searchString: String} OR ${prefix}name ILIKE {searchString: String}`
? `${prefix}id ILIKE {searchString: String} OR t.user_id ILIKE {searchString: String} OR ${prefix}name ILIKE {searchString: String}`
: null,
searchType && searchType.includes("content") && !useTracesAmtCompatMode
? `${prefix}input ILIKE {searchString: String} OR ${prefix}output ILIKE {searchString: String}`
+6 -6
View File
@@ -6,7 +6,6 @@ import { DatasetRunItemUpsertQueue } from "./datasetRunItemUpsert";
import { EvalExecutionQueue } from "./evalExecutionQueue";
import { ExperimentCreateQueue } from "./experimentCreateQueue";
import { SecondaryIngestionQueue } from "./ingestionQueue";
import { TraceUpsertQueue } from "./traceUpsert";
import { TraceDeleteQueue } from "./traceDelete";
import { ProjectDeleteQueue } from "./projectDelete";
import { PostHogIntegrationQueue } from "./postHogIntegrationQueue";
@@ -25,10 +24,13 @@ import { WebhookQueue } from "./webhookQueue";
import { EntityChangeQueue } from "./entityChangeQueue";
import { DatasetDeleteQueue } from "./datasetDelete";
// IngestionQueue is sharded and requires a sharding key
// Use IngestionQueue.getInstance({ shardName: queueName }) directly instead
// IngestionQueue and TraceUpsert are sharded and require a sharding key
// Use IngestionQueue.getInstance({ shardName: queueName }) or TraceUpsertQueue.getInstance({ shardName: queueName }) directly instead
export function getQueue(
queueName: Exclude<QueueName, QueueName.IngestionQueue>,
queueName: Exclude<
QueueName,
QueueName.IngestionQueue | QueueName.TraceUpsert
>,
): Queue | null {
switch (queueName) {
case QueueName.BatchExport:
@@ -43,8 +45,6 @@ export function getQueue(
return EvalExecutionQueue.getInstance();
case QueueName.ExperimentCreate:
return ExperimentCreateQueue.getInstance();
case QueueName.TraceUpsert:
return TraceUpsertQueue.getInstance();
case QueueName.TraceDelete:
return TraceDeleteQueue.getInstance();
case QueueName.ProjectDelete:
+72 -25
View File
@@ -6,45 +6,92 @@ import {
getQueuePrefix,
} from "./redis";
import { logger } from "../logger";
import { getShardIndex } from "./sharding";
import { env } from "../../env";
export class TraceUpsertQueue {
private static instance: Queue<TQueueJobTypes[QueueName.TraceUpsert]> | null =
null;
private static instances: Map<
number,
Queue<TQueueJobTypes[QueueName.TraceUpsert]> | null
> = new Map();
public static getInstance(): Queue<
TQueueJobTypes[QueueName.TraceUpsert]
> | null {
if (TraceUpsertQueue.instance) return TraceUpsertQueue.instance;
public static getShardNames() {
return Array.from(
{ length: env.LANGFUSE_TRACE_UPSERT_QUEUE_SHARD_COUNT },
(_, i) => `${QueueName.TraceUpsert}${i > 0 ? `-${i}` : ""}`,
);
}
static getShardIndexFromShardName(
shardName: string | undefined,
): number | null {
if (!shardName) return null;
// Extract shard index from shard name
const shardIndex =
shardName === QueueName.TraceUpsert
? 0
: parseInt(shardName.replace(`${QueueName.TraceUpsert}-`, ""), 10);
if (isNaN(shardIndex)) return null;
return shardIndex;
}
/**
* Get the trace upsert queue instance for the given sharding key or shard name.
* @param shardingKey - ShardingKey is being hashed and randomly allocated to a shard. Should be `projectId-traceId`.
* @param shardName - Name of the shard. Should be `trace-upsert-queue-${shardIndex}` or plainly `trace-upsert-queue` for the first shard.
*/
public static getInstance({
shardingKey,
shardName,
}: {
shardingKey?: string;
shardName?: string;
} = {}): Queue<TQueueJobTypes[QueueName.TraceUpsert]> | null {
const shardIndex =
TraceUpsertQueue.getShardIndexFromShardName(shardName) ??
(env.REDIS_CLUSTER_ENABLED === "true" && shardingKey
? getShardIndex(
shardingKey,
env.LANGFUSE_TRACE_UPSERT_QUEUE_SHARD_COUNT,
)
: 0);
// Check if we already have an instance for this shard
if (TraceUpsertQueue.instances.has(shardIndex)) {
return TraceUpsertQueue.instances.get(shardIndex) || null;
}
const newRedis = createNewRedisInstance({
enableOfflineQueue: false,
...redisQueueRetryOptions,
});
TraceUpsertQueue.instance = newRedis
? new Queue<TQueueJobTypes[QueueName.TraceUpsert]>(
QueueName.TraceUpsert,
{
connection: newRedis,
prefix: getQueuePrefix(QueueName.TraceUpsert),
defaultJobOptions: {
removeOnComplete: 100, // Important: If not true, new jobs for that ID would be ignored as jobs in the complete set are still considered as part of the queue
removeOnFail: 100_000,
attempts: 5,
delay: 15_000, // 15 seconds
backoff: {
type: "exponential",
delay: 5000,
},
const name = `${QueueName.TraceUpsert}${shardIndex > 0 ? `-${shardIndex}` : ""}`;
const queueInstance = newRedis
? new Queue<TQueueJobTypes[QueueName.TraceUpsert]>(name, {
connection: newRedis,
prefix: getQueuePrefix(name),
defaultJobOptions: {
removeOnComplete: 100,
removeOnFail: 100_000,
attempts: 5,
delay: 15_000, // 15 seconds
backoff: {
type: "exponential",
delay: 5000,
},
},
)
})
: null;
TraceUpsertQueue.instance?.on("error", (err) => {
logger.error("TraceUpsertQueue error", err);
queueInstance?.on("error", (err) => {
logger.error(`TraceUpsertQueue shard ${shardIndex} error`, err);
});
return TraceUpsertQueue.instance;
TraceUpsertQueue.instances.set(shardIndex, queueInstance);
return queueInstance;
}
}
@@ -185,7 +185,7 @@ export const insertIntoS3RefsTableFromEventLog = async (
) => {
const query = `
INSERT INTO blob_storage_file_log
SELECT
SELECT
id,
project_id,
entity_type,
@@ -239,10 +239,10 @@ export const findS3RefsByPrimaryKey = async (primaryKey: {
bucket_path: string;
}) => {
const query = `
SELECT *
FROM blob_storage_file_log
WHERE project_id = {project_id: String}
AND entity_type = {entity_type: String}
SELECT *
FROM blob_storage_file_log
WHERE project_id = {project_id: String}
AND entity_type = {entity_type: String}
AND entity_id = {entity_id: String}
AND bucket_path = {bucket_path: String}
`;
@@ -62,28 +62,30 @@ export async function upsertClickhouse<
const eventId = randomUUID();
const bucketPath = `${env.LANGFUSE_S3_EVENT_UPLOAD_PREFIX}${record.project_id}/${getClickhouseEntityType(eventType)}/${record.id}/${eventId}.json`;
// Write new file directly to ClickHouse. We don't use the ClickHouse writer here as we expect more limited traffic
// and are not worried that much about latency.
await clickhouseClient().insert({
table: "blob_storage_file_log",
values: [
{
id: randomUUID(),
project_id: record.project_id,
entity_type: getClickhouseEntityType(eventType),
entity_id: record.id,
event_id: eventId,
bucket_name: env.LANGFUSE_S3_EVENT_UPLOAD_BUCKET,
bucket_path: bucketPath,
event_ts: convertDateToClickhouseDateTime(new Date()),
is_deleted: 0,
if (env.LANGFUSE_ENABLE_BLOB_STORAGE_FILE_LOG === "true") {
// Write new file directly to ClickHouse. We don't use the ClickHouse writer here as we expect more limited traffic
// and are not worried that much about latency.
await clickhouseClient().insert({
table: "blob_storage_file_log",
values: [
{
id: randomUUID(),
project_id: record.project_id,
entity_type: getClickhouseEntityType(eventType),
entity_id: record.id,
event_id: eventId,
bucket_name: env.LANGFUSE_S3_EVENT_UPLOAD_BUCKET,
bucket_path: bucketPath,
event_ts: convertDateToClickhouseDateTime(new Date()),
is_deleted: 0,
},
],
format: "JSONEachRow",
clickhouse_settings: {
log_comment: JSON.stringify(opts.tags ?? {}),
},
],
format: "JSONEachRow",
clickhouse_settings: {
log_comment: JSON.stringify(opts.tags ?? {}),
},
});
});
}
return getS3StorageServiceClient(
env.LANGFUSE_S3_EVENT_UPLOAD_BUCKET,
@@ -5,7 +5,7 @@ import {
import { createFilterFromFilterState } from "../queries/clickhouse-sql/factory";
import { FilterState } from "../../types";
import { DateTimeFilter, FilterList } from "../queries";
import { dashboardColumnDefinitions } from "../../tableDefinitions";
import { dashboardColumnDefinitions } from "../tableMappings";
import { convertDateToClickhouseDateTime } from "../clickhouse/client";
import {
OBSERVATIONS_TO_TRACE_INTERVAL,
@@ -1,12 +1,14 @@
import { DatasetRunItemDomain } from "../../domain/dataset-run-items";
import { type OrderByState } from "../../interfaces/orderBy";
import { datasetRunItemsTableUiColumnDefinitions } from "../../tableDefinitions";
import { datasetRunItemsTableUiColumnDefinitions } from "../tableMappings";
import { datasetRunsTableUiColumnDefinitions } from "../../tableDefinitions/mapDatasetRunsTable";
import { FilterState } from "../../types";
import {
createFilterFromFilterState,
FilterList,
orderByToClickhouseSql,
StringFilter,
StringOptionsFilter,
} from "../queries";
import {
parseClickhouseUTCDateTimeFormat,
@@ -17,19 +19,39 @@ import { DatasetRunItemRecordReadType } from "./definitions";
import { env } from "../../env";
import { commandClickhouse } from "./clickhouse";
import Decimal from "decimal.js";
import { ClickHouseClientConfigOptions } from "@clickhouse/client";
import { convertDateToClickhouseDateTime } from "../clickhouse/client";
type DatasetItemIdsByTraceIdQuery = {
projectId: string;
traceId: string;
// this filter should include a dataset_id filter to search along primary key
filter: FilterState;
};
type DatasetRunItemsTableQuery = {
projectId: string;
datasetId: string;
filter: FilterState;
datasetId?: string;
orderBy?: OrderByState | OrderByState[];
limit?: number;
offset?: number;
clickhouseConfigs?: ClickHouseClientConfigOptions;
};
type DatasetRunItemsByDatasetIdQuery = Omit<
DatasetRunItemsTableQuery,
"datasetId"
> & {
datasetId: string;
};
type DatasetRunsMetricsTableQuery = {
select: "rows" | "metrics" | "count";
projectId: string;
datasetId: string;
filter: FilterState;
runIds?: string[];
orderBy?: OrderByState;
limit?: number;
offset?: number;
@@ -37,22 +59,46 @@ type DatasetRunsMetricsTableQuery = {
export type DatasetRunsMetrics = {
id: string;
name: string;
projectId: string;
createdAt: Date;
datasetId: string;
countRunItems: number;
avgTotalCost: Decimal;
avgLatency: number;
aggScoresAvg: Array<[string, number]>;
aggScoreCategories: string[];
};
type DatasetRunsRows = {
id: string;
name: string;
projectId: string;
createdAt: Date;
datasetId: string;
description: string;
metadata: string;
};
type DatasetRunsMetricsRecordType = {
dataset_run_id: string;
dataset_run_name: string;
project_id: string;
dataset_run_created_at: string;
dataset_id: string;
count_run_items: number;
avg_latency_seconds: number;
avg_total_cost: number;
agg_scores_avg: Array<[string, number]>;
agg_score_categories: string[];
};
type DatasetRunsRowsRecordType = {
dataset_run_id: string;
dataset_run_name: string;
project_id: string;
dataset_id: string;
dataset_run_created_at: string;
dataset_run_description: string;
dataset_run_metadata: string;
};
const convertDatasetRunsMetricsRecord = (
@@ -60,35 +106,66 @@ const convertDatasetRunsMetricsRecord = (
): DatasetRunsMetrics => {
return {
id: record.dataset_run_id,
name: record.dataset_run_name,
projectId: record.project_id,
createdAt: parseClickhouseUTCDateTimeFormat(record.dataset_run_created_at),
datasetId: record.dataset_id,
countRunItems: record.count_run_items,
avgTotalCost: record.avg_total_cost
? new Decimal(record.avg_total_cost)
: new Decimal(0),
avgLatency: record.avg_latency_seconds ?? 0,
aggScoresAvg: record.agg_scores_avg ?? [],
aggScoreCategories: record.agg_score_categories ?? [],
};
};
const convertDatasetRunsRowsRecord = (
record: DatasetRunsRowsRecordType,
): DatasetRunsRows => {
return {
id: record.dataset_run_id,
name: record.dataset_run_name,
projectId: record.project_id,
createdAt: parseClickhouseUTCDateTimeFormat(record.dataset_run_created_at),
datasetId: record.dataset_id,
description: record.dataset_run_description,
metadata: record.dataset_run_metadata,
};
};
const getProjectDatasetIdDefaultFilter = (
projectId: string,
datasetId: string,
datasetId?: string,
runIds?: string[],
) => {
return {
datasetRunItemsFilter: new FilterList([
new StringFilter({
clickhouseTable: "dataset_run_items",
clickhouseTable: "dataset_run_items_rmt",
field: "project_id",
operator: "=",
value: projectId,
}),
new StringFilter({
clickhouseTable: "dataset_run_items",
field: "dataset_id",
operator: "=",
value: datasetId,
}),
...(datasetId
? [
new StringFilter({
clickhouseTable: "dataset_run_items_rmt",
field: "dataset_id",
operator: "=",
value: datasetId,
}),
]
: []),
...(runIds && runIds.length > 0
? [
new StringOptionsFilter({
clickhouseTable: "dataset_run_items_rmt",
field: "dataset_run_id",
operator: "any of",
values: runIds,
}),
]
: []),
]),
};
};
@@ -98,17 +175,85 @@ const getDatasetRunsTableInternal = async <T>(
tags: Record<string, string>;
},
): Promise<Array<T>> => {
const { projectId, datasetId, orderBy, limit, offset } = opts;
const { projectId, datasetId, runIds, filter, orderBy, limit, offset } = opts;
let select = "";
switch (opts.select) {
case "rows":
select = `
drm.project_id as project_id,
drm.dataset_id as dataset_id,
drm.dataset_run_id as dataset_run_id,
drm.dataset_run_name as dataset_run_name,
drm.dataset_run_created_at as dataset_run_created_at,
drm.dataset_run_description as dataset_run_description,
drm.dataset_run_metadata as dataset_run_metadata
`;
break;
case "metrics":
select = `
drm.project_id as project_id,
drm.dataset_id as dataset_id,
drm.dataset_run_id as dataset_run_id,
drm.dataset_run_name as dataset_run_name,
drm.count_run_items as count_run_items,
-- Latency metrics (priority: trace > observation - matching old PostgreSQL behavior)
CASE
WHEN drm.trace_avg_latency IS NOT NULL THEN drm.trace_avg_latency
ELSE drm.obs_avg_latency
END as avg_latency_seconds,
-- Cost metrics (priority: trace > observation - matching old PostgreSQL behavior)
CASE
WHEN drm.trace_avg_cost IS NOT NULL THEN drm.trace_avg_cost
ELSE COALESCE(drm.obs_avg_cost, 0)
END as avg_total_cost,
-- Score aggregations
sa.scores_avg as agg_scores_avg,
sa.score_categories as agg_score_categories`;
break;
case "count":
select = "count(DISTINCT drm.dataset_run_id) as count";
break;
}
const { datasetRunItemsFilter } = getProjectDatasetIdDefaultFilter(
projectId,
datasetId,
runIds,
);
const baseFilter = datasetRunItemsFilter.apply();
const scoresFilter = new FilterList([
new StringFilter({
clickhouseTable: "scores",
field: "project_id",
operator: "=",
value: projectId,
}),
]);
const appliedScoresFilter = scoresFilter.apply();
const userFilters = createFilterFromFilterState(
filter,
datasetRunsTableUiColumnDefinitions,
);
datasetRunItemsFilter.push(...userFilters);
const appliedFilter = datasetRunItemsFilter.apply();
// Build ORDER BY array - conditionally add event_ts DESC for rows
const orderByArray: OrderByState[] = [];
// Build ORDER BY array - conditionally add dataset_run_created_at ASC for rows
if (opts.select === "metrics" && orderBy?.column !== "createdAt") {
orderByArray.push({
column: "createdAt",
order: "DESC",
});
}
// Add user ordering if provided
if (orderBy) {
orderByArray.push(orderBy);
@@ -116,11 +261,50 @@ const getDatasetRunsTableInternal = async <T>(
const orderByClause = orderByToClickhouseSql(
orderByArray,
datasetRunItemsTableUiColumnDefinitions,
datasetRunsTableUiColumnDefinitions,
);
const query = `
WITH observations_filtered AS (
const scoresCte = `
WITH scores_aggregated AS (
SELECT
dri.dataset_run_id,
dri.project_id,
-- For numeric scores, use tuples of (name, avg_value)
groupArrayIf(
tuple(s.name, s.avg_value),
s.data_type IN ('NUMERIC', 'BOOLEAN')
) AS scores_avg,
-- For categorical scores, use name:value format for improved query performance
groupArrayIf(
concat(s.name, ':', s.string_value),
s.data_type = 'CATEGORICAL' AND notEmpty(s.string_value)
) AS score_categories
FROM dataset_run_items_rmt dri
LEFT JOIN (
SELECT
project_id,
trace_id,
name,
data_type,
string_value,
avg(value) as avg_value
FROM scores s FINAL
WHERE ${appliedScoresFilter.query}
GROUP BY
project_id,
trace_id,
name,
data_type,
string_value
) s ON s.project_id = dri.project_id AND s.trace_id = dri.trace_id
WHERE dri.project_id = {projectId: String}
AND dri.dataset_id = {datasetId: String}
GROUP BY dri.dataset_run_id, dri.project_id
),
`;
const filteredObservationsCte = `
observations_filtered AS (
SELECT
o.id,
o.trace_id,
@@ -132,75 +316,80 @@ const getDatasetRunsTableInternal = async <T>(
WHERE o.project_id = {projectId: String}
AND o.start_time >= (
SELECT min(dri.dataset_run_created_at) - INTERVAL 1 DAY
FROM dataset_run_items dri
FROM dataset_run_items_rmt dri
WHERE dri.project_id = {projectId: String}
AND dri.dataset_id = {datasetId: String}
)
AND o.start_time <= (
SELECT max(dri.dataset_run_created_at) + INTERVAL 1 DAY
FROM dataset_run_items dri
FROM dataset_run_items_rmt dri
WHERE dri.project_id = {projectId: String}
AND dri.dataset_id = {datasetId: String}
)
),
traces_aggregated AS (
`;
const traceMetricsCte = `
trace_metrics AS (
SELECT
of.trace_id,
of.project_id,
dateDiff('millisecond', min(of.start_time), max(of.end_time)) as latency_ms,
sum(of.total_cost) as total_cost
FROM observations_filtered of
JOIN dataset_run_items dri ON dri.trace_id = of.trace_id
JOIN dataset_run_items_rmt dri ON dri.trace_id = of.trace_id
AND dri.project_id = of.project_id
AND dri.observation_id IS NULL -- Only for trace-level dataset run items
WHERE dri.dataset_id = {datasetId: String}
WHERE ${baseFilter.query}
GROUP BY of.trace_id, of.project_id
),
observations_direct AS (
`;
const datasetRunMetricsCte = `
dataset_run_metrics AS (
SELECT
dri.observation_id,
dri.project_id,
dri.trace_id,
of.total_cost,
dateDiff('millisecond', of.start_time, of.end_time) as latency_ms
FROM dataset_run_items dri
JOIN observations_filtered of ON dri.observation_id = of.id
dri.dataset_run_id as dataset_run_id,
dri.project_id as project_id,
dri.dataset_id as dataset_id,
dri.dataset_run_created_at as dataset_run_created_at,
dri.dataset_run_name as dataset_run_name,
dri.dataset_run_description as dataset_run_description,
dri.dataset_run_metadata as dataset_run_metadata,
count(DISTINCT dri.project_id, dri.dataset_id, dri.dataset_run_id, dri.dataset_item_id) as count_run_items,
-- Trace-level metrics (average across traces in this dataset run)
AVG(CASE WHEN dri.observation_id IS NULL THEN tm.latency_ms ELSE NULL END) / 1000.0 as trace_avg_latency,
AVG(CASE WHEN dri.observation_id IS NULL THEN tm.total_cost ELSE NULL END) as trace_avg_cost,
-- Observation-level metrics
AVG(CASE WHEN dri.observation_id IS NOT NULL THEN
dateDiff('millisecond', of.start_time, of.end_time) / 1000.0
ELSE NULL END) as obs_avg_latency,
AVG(CASE WHEN dri.observation_id IS NOT NULL THEN of.total_cost ELSE NULL END) as obs_avg_cost
FROM dataset_run_items_rmt dri
LEFT JOIN observations_filtered of ON dri.observation_id = of.id
AND dri.project_id = of.project_id
AND dri.trace_id = of.trace_id
WHERE dri.dataset_id = {datasetId: String}
AND dri.observation_id IS NOT NULL -- Only for observation-level dataset run items
LEFT JOIN trace_metrics tm ON dri.trace_id = tm.trace_id
AND dri.project_id = tm.project_id
AND dri.observation_id IS NULL
WHERE ${baseFilter.query}
GROUP BY dri.project_id, dri.dataset_id, dri.dataset_run_id, dri.dataset_run_name, dri.dataset_run_description, dri.dataset_run_metadata, dri.dataset_run_created_at
)
SELECT DISTINCT
dri.dataset_run_id as dataset_run_id,
dri.project_id as project_id,
dri.dataset_id as dataset_id,
dri.dataset_run_created_at as dataset_run_created_at,
count(DISTINCT dri.project_id, dri.dataset_id, dri.dataset_run_id, dri.dataset_item_id) as count_run_items,
-- Latency metrics (priority: observation > trace)
AVG(CASE
WHEN dri.observation_id IS NOT NULL AND od.latency_ms IS NOT NULL
THEN od.latency_ms / 1000.0
ELSE COALESCE(ta.latency_ms / 1000.0, 0)
END) as avg_latency_seconds,
-- Cost metrics (priority: observation > trace)
AVG(CASE
WHEN dri.observation_id IS NOT NULL AND od.total_cost IS NOT NULL
THEN od.total_cost
ELSE COALESCE(ta.total_cost, 0)
END) as avg_total_cost
FROM dataset_run_items dri
LEFT JOIN traces_aggregated ta
ON dri.trace_id = ta.trace_id
AND dri.project_id = ta.project_id
LEFT JOIN observations_direct od
ON dri.observation_id = od.observation_id
AND dri.project_id = od.project_id
AND dri.trace_id = od.trace_id
WHERE ${appliedFilter.query}
GROUP BY dri.project_id, dri.dataset_id, dri.dataset_run_id, dri.dataset_run_created_at
ORDER BY dri.dataset_run_created_at DESC
`;
const query = `
${scoresCte}
${filteredObservationsCte}
${traceMetricsCte}
${datasetRunMetricsCte}
SELECT ${opts.select === "count" ? "" : "DISTINCT"}
${select}
FROM dataset_run_metrics drm
LEFT JOIN scores_aggregated sa ON drm.dataset_run_id = sa.dataset_run_id AND drm.project_id = sa.project_id
WHERE drm.project_id = {projectId: String} AND drm.dataset_id = {datasetId: String}
${appliedFilter.query ? `AND ${appliedFilter.query}` : ""}
${orderByClause}
${limit !== undefined && offset !== undefined ? `LIMIT ${limit} OFFSET ${offset}` : ""};`;
@@ -209,6 +398,9 @@ const getDatasetRunsTableInternal = async <T>(
params: {
projectId,
datasetId,
...(runIds && runIds.length > 0 ? { runIds } : {}),
...appliedScoresFilter.params,
...baseFilter.params,
...appliedFilter.params,
},
tags: {
@@ -224,17 +416,42 @@ const getDatasetRunsTableInternal = async <T>(
};
export const getDatasetRunsTableMetricsCh = async (
opts: DatasetRunsMetricsTableQuery,
opts: Omit<DatasetRunsMetricsTableQuery, "select">,
): Promise<DatasetRunsMetrics[]> => {
// First get the metrics (latency, cost, counts)
const rows = await getDatasetRunsTableInternal<DatasetRunsMetricsRecordType>({
...opts,
select: "metrics",
tags: { kind: "list" },
});
return rows.map(convertDatasetRunsMetricsRecord);
};
export const getDatasetRunsTableRowsCh = async (
opts: Omit<DatasetRunsMetricsTableQuery, "select">,
): Promise<DatasetRunsRows[]> => {
const rows = await getDatasetRunsTableInternal<DatasetRunsRowsRecordType>({
...opts,
select: "rows",
tags: { kind: "list" },
});
return rows.map(convertDatasetRunsRowsRecord);
};
export const getDatasetRunsTableCountCh = async (
opts: Omit<DatasetRunsMetricsTableQuery, "select">,
): Promise<number> => {
const rows = await getDatasetRunsTableInternal<{ count: string }>({
...opts,
select: "count",
tags: { kind: "list" },
});
return Number(rows[0]?.count);
};
const getDatasetRunItemsTableInternal = async <T>(
opts: DatasetRunItemsTableQuery & {
select: "count" | "rows";
@@ -317,7 +534,7 @@ const getDatasetRunItemsTableInternal = async <T>(
const query = `
SELECT
${selectString}
FROM dataset_run_items dri
FROM dataset_run_items_rmt dri
WHERE ${appliedFilter.query}
${orderByClause}
${opts.select === "rows" ? "LIMIT 1 BY dri.project_id, dri.dataset_id, dri.dataset_run_id, dri.dataset_item_id" : ""}
@@ -333,14 +550,15 @@ const getDatasetRunItemsTableInternal = async <T>(
feature: "datasets",
type: "dataset-run-items",
projectId,
datasetId,
...(datasetId ? { datasetId } : {}),
},
clickhouseConfigs: opts.clickhouseConfigs,
});
return res;
};
export const getDatasetRunItemsByDatasetIdCh = async (
export const getDatasetRunItemsCh = async (
opts: DatasetRunItemsTableQuery,
): Promise<DatasetRunItemDomain[]> => {
const rows =
@@ -353,7 +571,80 @@ export const getDatasetRunItemsByDatasetIdCh = async (
return rows.map(convertDatasetRunItemClickhouseToDomain);
};
export const getDatasetRunItemsCountByDatasetIdCh = async (
export const getDatasetRunItemsByDatasetIdCh = async (
opts: DatasetRunItemsByDatasetIdQuery,
): Promise<DatasetRunItemDomain[]> => {
const rows =
await getDatasetRunItemsTableInternal<DatasetRunItemRecordReadType>({
...opts,
select: "rows",
tags: { kind: "list" },
});
return rows.map(convertDatasetRunItemClickhouseToDomain);
};
export const getDatasetItemIdsByTraceIdCh = async (
opts: DatasetItemIdsByTraceIdQuery,
): Promise<{ id: string; datasetId: string }[]> => {
const { projectId, traceId, filter } = opts;
const datasetRunItemsFilter = new FilterList([
new StringFilter({
clickhouseTable: "dataset_run_items_rmt",
field: "project_id",
operator: "=",
value: projectId,
}),
new StringFilter({
clickhouseTable: "dataset_run_items_rmt",
field: "trace_id",
operator: "=",
value: traceId,
}),
]);
datasetRunItemsFilter.push(
...createFilterFromFilterState(
filter,
datasetRunItemsTableUiColumnDefinitions,
),
);
const appliedFilter = datasetRunItemsFilter.apply();
const query = `
SELECT
dri.dataset_item_id as dataset_item_id,
dri.dataset_id as dataset_id
FROM dataset_run_items_rmt dri
WHERE ${appliedFilter.query}
LIMIT 1 BY dri.project_id, dri.dataset_id, dri.dataset_run_id, dri.dataset_item_id;`;
const res = await queryClickhouse<{
dataset_item_id: string;
dataset_id: string;
}>({
query,
params: {
...appliedFilter.params,
},
tags: {
feature: "datasets",
type: "dataset-run-items",
projectId,
traceId,
},
});
return res.map((runItem) => {
return {
id: runItem.dataset_item_id,
datasetId: runItem.dataset_id,
};
});
};
export const getDatasetRunItemsCountCh = async (
opts: DatasetRunItemsTableQuery,
): Promise<number> => {
const rows = await getDatasetRunItemsTableInternal<{ count: string }>({
@@ -365,13 +656,25 @@ export const getDatasetRunItemsCountByDatasetIdCh = async (
return Number(rows[0]?.count);
};
export const getDatasetRunItemsCountByDatasetIdCh = async (
opts: DatasetRunItemsByDatasetIdQuery,
): Promise<number> => {
const rows = await getDatasetRunItemsTableInternal<{ count: string }>({
...opts,
select: "count",
tags: { kind: "list" },
});
return Number(rows[0]?.count);
};
export const deleteDatasetRunItemsByProjectId = async ({
projectId,
}: {
projectId: string;
}) => {
const query = `
DELETE FROM dataset_run_items
DELETE FROM dataset_run_items_rmt
WHERE project_id = {projectId: String};
`;
await commandClickhouse({
@@ -399,7 +702,7 @@ export const deleteDatasetRunItemsByDatasetId = async ({
datasetId: string;
}) => {
const query = `
DELETE FROM dataset_run_items
DELETE FROM dataset_run_items_rmt
WHERE project_id = {projectId: String}
AND dataset_id = {datasetId: String}
`;
@@ -432,7 +735,7 @@ export const deleteDatasetRunItemsByDatasetRunIds = async ({
datasetId: string;
}) => {
const query = `
DELETE FROM dataset_run_items
DELETE FROM dataset_run_items_rmt
WHERE project_id = {projectId: String}
AND dataset_id = {datasetId: String}
AND dataset_run_id IN ({datasetRunIds: Array(String)})
@@ -456,3 +759,40 @@ export const deleteDatasetRunItemsByDatasetRunIds = async ({
},
});
};
export const getDatasetRunItemCountsByProjectInCreationInterval = async ({
start,
end,
}: {
start: Date;
end: Date;
}) => {
const query = `
SELECT
project_id,
count(*) as count
FROM dataset_run_items_rmt
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),
},
tags: {
feature: "datasets",
type: "dataset-run-items",
kind: "analytic",
operation_name: "getDatasetRunItemCountsByProjectInCreationInterval",
},
});
return rows.map((row) => ({
projectId: row.project_id,
count: Number(row.count),
}));
};
@@ -21,7 +21,7 @@ import { createFilterFromFilterState } from "../queries/clickhouse-sql/factory";
import {
observationsTableTraceUiColumnDefinitions,
observationsTableUiColumnDefinitions,
} from "../../tableDefinitions";
} from "../tableMappings";
import { OrderByState } from "../../interfaces/orderBy";
import { getTimeframesTracesAMT, getTracesByIds } from "./traces";
import { measureAndReturn } from "../clickhouse/measureAndReturn";
@@ -227,13 +227,19 @@ export const getObservationsForTrace = async <IncludeIO extends boolean>(
});
};
export const getObservationForTraceIdByName = async (
traceId: string,
projectId: string,
name: string,
timestamp?: Date,
fetchWithInputOutput: boolean = false,
) => {
export const getObservationForTraceIdByName = async ({
traceId,
projectId,
name,
timestamp,
fetchWithInputOutput = false,
}: {
traceId: string;
projectId: string;
name: string;
timestamp?: Date;
fetchWithInputOutput?: boolean;
}) => {
const query = `
SELECT
id,
@@ -1158,8 +1164,8 @@ export const getObservationsWithPromptName = async (
promptNames: string[],
) => {
const query = `
SELECT count(*) as count, prompt_name
FROM observations FINAL
SELECT uniq(id) as count, prompt_name
FROM observations
WHERE project_id = {projectId: String}
AND prompt_name IN ({promptNames: Array(String)})
AND prompt_name IS NOT NULL
@@ -1508,6 +1514,15 @@ export const getGenerationsForPostHog = async function* (
minTimestamp: Date,
maxTimestamp: Date,
) {
// Determine which trace table to use based on experiment flag
const useAMT = env.LANGFUSE_EXPERIMENT_RETURN_NEW_RESULT === "true";
// Subtract 7d from minTimestamp to account for shift in query
const traceTable = useAMT
? getTimeframesTracesAMT(
new Date(minTimestamp.getTime() - 7 * 24 * 60 * 60 * 1000),
)
: "traces";
const query = `
SELECT
o.name as name,
@@ -1532,7 +1547,7 @@ export const getGenerationsForPostHog = async function* (
t.tags as trace_tags,
t.metadata['$posthog_session_id'] as posthog_session_id
FROM observations o FINAL
LEFT JOIN traces t FINAL ON o.trace_id = t.id AND o.project_id = t.project_id
LEFT JOIN ${traceTable} t FINAL ON o.trace_id = t.id AND o.project_id = t.project_id
WHERE o.project_id = {projectId: String}
AND t.project_id = {projectId: String}
AND o.start_time >= {minTimestamp: DateTime64(3)}
@@ -1554,6 +1569,7 @@ export const getGenerationsForPostHog = async function* (
type: "observation",
kind: "analytic",
projectId,
experiment_amt: useAMT ? "new" : "original",
},
clickhouseConfigs: {
request_timeout: env.LANGFUSE_CLICKHOUSE_DATA_EXPORT_REQUEST_TIMEOUT_MS,
@@ -1579,7 +1595,7 @@ export const getGenerationsForPostHog = async function* (
langfuse_total_units: record.total_tokens,
langfuse_session_id: record.trace_session_id,
langfuse_project_id: projectId,
langfuse_user_id: record.trace_user_id || "langfuse_unknown_user",
langfuse_user_id: record.trace_user_id || null,
langfuse_latency: record.latency,
langfuse_time_to_first_token: record.time_to_first_token,
langfuse_release: record.trace_release,
@@ -1590,11 +1606,15 @@ export const getGenerationsForPostHog = async function* (
langfuse_environment: record.environment,
langfuse_event_version: "1.0.0",
$session_id: record.posthog_session_id ?? null,
$set: {
langfuse_user_url: record.user_id
? `${baseUrl}/project/${projectId}/users/${encodeURIComponent(record.user_id as string)}`
: null,
},
...(record.trace_user_id
? {
$set: {
langfuse_user_url: `${baseUrl}/project/${projectId}/users/${encodeURIComponent(record.trace_user_id as string)}`,
},
}
: // Capture as anonymous PostHog event (cheaper/faster)
// https://posthog.com/docs/data/anonymous-vs-identified-events?tab=Backend
{ $process_person_profile: false }),
};
}
};
+108 -28
View File
@@ -17,7 +17,7 @@ import { OrderByState } from "../../interfaces/orderBy";
import {
dashboardColumnDefinitions,
scoresTableUiColumnDefinitions,
} from "../../tableDefinitions";
} from "../tableMappings";
import {
convertScoreAggregation,
convertToScore,
@@ -33,6 +33,7 @@ import { ClickHouseClientConfigOptions } from "@clickhouse/client";
import { recordDistribution } from "../instrumentation";
import { prisma } from "../../db";
import { measureAndReturn } from "../clickhouse/measureAndReturn";
import { getTimeframesTracesAMT } from "./traces";
export const searchExistingAnnotationScore = async (
projectId: string,
@@ -41,6 +42,7 @@ export const searchExistingAnnotationScore = async (
sessionId: string | null,
name: string | undefined,
configId: string | undefined,
dataType: ScoreDataType,
) => {
if (!name && !configId) {
throw new Error("Either name or configId (or both) must be provided.");
@@ -51,8 +53,10 @@ export const searchExistingAnnotationScore = async (
FROM scores s
WHERE s.project_id = {projectId: String}
AND s.source = 'ANNOTATION'
AND s.trace_id = {traceId: String}
AND s.data_type = {dataType: String}
${traceId ? `AND s.trace_id = {traceId: String}` : "AND isNull(s.trace_id)"}
${observationId ? `AND s.observation_id = {observationId: String}` : "AND isNull(s.observation_id)"}
${sessionId ? `AND s.session_id = {sessionId: String}` : "AND isNull(s.session_id)"}
AND (
FALSE
${name ? `OR s.name = {name: String}` : ""}
@@ -71,6 +75,8 @@ export const searchExistingAnnotationScore = async (
configId,
traceId,
observationId,
sessionId,
dataType,
},
tags: {
feature: "tracing",
@@ -293,10 +299,30 @@ export const getTraceScoresForDatasetRuns = async (
const query = `
SELECT
s.* EXCEPT (metadata),
s.id as id,
s.timestamp as timestamp,
s.project_id as project_id,
s.environment as environment,
s.trace_id as trace_id,
s.session_id as session_id,
s.observation_id as observation_id,
s.dataset_run_id as dataset_run_id,
s.name as name,
s.value as value,
s.source as source,
s.comment as comment,
s.author_user_id as author_user_id,
s.config_id as config_id,
s.data_type as data_type,
s.string_value as string_value,
s.queue_id as queue_id,
s.created_at as created_at,
s.updated_at as updated_at,
s.event_ts as event_ts,
s.is_deleted as is_deleted,
length(mapKeys(s.metadata)) > 0 AS has_metadata,
dri.dataset_run_id as run_id
FROM dataset_run_items dri
FROM dataset_run_items_rmt dri
JOIN scores s FINAL ON dri.trace_id = s.trace_id
AND dri.project_id = s.project_id
WHERE dri.project_id = {projectId: String}
@@ -503,12 +529,27 @@ export const getScoresForObservations = async <
export const getRunScoresGroupedByNameSourceType = async (
projectId: string,
datasetRunIds: string[],
timestamp: Date | undefined,
timestampFilter?: FilterState,
) => {
if (datasetRunIds.length === 0) {
return [];
}
const chFilter = timestampFilter
? createFilterFromFilterState(timestampFilter, [
{
uiTableName: "Timestamp",
uiTableId: "timestamp",
clickhouseTableName: "scores",
clickhouseSelect: "timestamp",
},
])
: undefined;
const timestampFilterRes = chFilter
? new FilterList(chFilter).apply()
: undefined;
// We mainly use queries like this to retrieve filter options.
// Therefore, we can skip final as some inaccuracy in count is acceptable.
const query = `
@@ -518,7 +559,7 @@ export const getRunScoresGroupedByNameSourceType = async (
data_type
from scores s
WHERE s.project_id = {projectId: String}
${timestamp ? `AND s.timestamp >= {timestamp: DateTime64(3)}` : ""}
${timestampFilterRes?.query ? `AND ${timestampFilterRes.query}` : ""}
AND s.dataset_run_id IN ({datasetRunIds: Array(String)})
GROUP BY name, source, data_type
ORDER BY count() desc
@@ -533,9 +574,7 @@ export const getRunScoresGroupedByNameSourceType = async (
query: query,
params: {
projectId: projectId,
...(timestamp
? { timestamp: convertDateToClickhouseDateTime(timestamp) }
: {}),
...(timestampFilterRes ? timestampFilterRes.params : {}),
datasetRunIds: datasetRunIds,
},
tags: {
@@ -1152,7 +1191,7 @@ export const getNumericScoreHistogram = async (
const query = `
select s.value
from scores s
${traceFilter ? `LEFT JOIN traces t ON s.trace_id = t.id AND t.project_id = s.project_id` : ""}
${traceFilter ? `LEFT JOIN __TRACE_TABLE__ t ON s.trace_id = t.id AND t.project_id = s.project_id` : ""}
WHERE s.project_id = {projectId: String}
${traceFilter ? `AND t.project_id = {projectId: String}` : ""}
${chFilterRes?.query ? `AND ${chFilterRes.query}` : ""}
@@ -1161,18 +1200,45 @@ export const getNumericScoreHistogram = async (
${limit !== undefined ? `limit {limit: Int32}` : ""}
`;
return queryClickhouse<{ value: number }>({
query,
params: {
projectId,
limit,
...(chFilterRes ? chFilterRes.params : {}),
// Extract timestamp from filter for AMT table selection
const timestampFilter = chFilter.find(
(f) => f.clickhouseTable === "traces" && f.field === "timestamp",
) as TimeFilter | undefined;
const timestamp = timestampFilter?.value;
return measureAndReturn({
operationName: "getNumericScoreHistogram",
projectId,
minStartTime: timestamp,
input: {
params: {
projectId,
limit,
...(chFilterRes ? chFilterRes.params : {}),
},
tags: {
feature: "tracing",
type: "score",
kind: "analytic",
projectId,
operation_name: "getNumericScoreHistogram",
},
timestamp,
},
tags: {
feature: "tracing",
type: "score",
kind: "analytic",
projectId,
existingExecution: async (input) => {
return queryClickhouse<{ value: number }>({
query: query.replace("__TRACE_TABLE__", "traces"),
params: input.params,
tags: { ...input.tags, experiment_amt: "original" },
});
},
newExecution: async (input) => {
const traceAmt = getTimeframesTracesAMT(input.timestamp);
return queryClickhouse<{ value: number }>({
query: query.replace("__TRACE_TABLE__", traceAmt),
params: input.params,
tags: { ...input.tags, experiment_amt: "new" },
});
},
});
};
@@ -1399,6 +1465,15 @@ export const getScoresForPostHog = async function* (
minTimestamp: Date,
maxTimestamp: Date,
) {
// Determine which trace table to use based on experiment flag
const useAMT = env.LANGFUSE_EXPERIMENT_RETURN_NEW_RESULT === "true";
// Subtract 7d from minTimestamp to account for shift in query
const traceTable = useAMT
? getTimeframesTracesAMT(
new Date(minTimestamp.getTime() - 7 * 24 * 60 * 60 * 1000),
)
: "traces";
const query = ` SELECT
s.id as id,
s.timestamp as timestamp,
@@ -1417,7 +1492,7 @@ export const getScoresForPostHog = async function* (
s.metadata as metadata,
t.metadata['$posthog_session_id'] as posthog_session_id
FROM scores s FINAL
LEFT JOIN traces t FINAL ON s.trace_id = t.id AND s.project_id = t.project_id
LEFT JOIN ${traceTable} t FINAL ON s.trace_id = t.id AND s.project_id = t.project_id
WHERE s.project_id = {projectId: String}
AND t.project_id = {projectId: String}
AND s.timestamp >= {minTimestamp: DateTime64(3)}
@@ -1438,6 +1513,7 @@ export const getScoresForPostHog = async function* (
type: "score",
kind: "analytic",
projectId,
experiment_amt: useAMT ? "new" : "original",
},
clickhouseConfigs: {
request_timeout: env.LANGFUSE_CLICKHOUSE_DATA_EXPORT_REQUEST_TIMEOUT_MS,
@@ -1463,17 +1539,21 @@ export const getScoresForPostHog = async function* (
langfuse_id: record.id,
langfuse_session_id: record.trace_session_id,
langfuse_project_id: projectId,
langfuse_user_id: record.trace_user_id || "langfuse_unknown_user",
langfuse_user_id: record.trace_user_id || null,
langfuse_release: record.trace_release,
langfuse_tags: record.trace_tags,
langfuse_environment: record.environment,
langfuse_event_version: "1.0.0",
$session_id: record.posthog_session_id ?? null,
$set: {
langfuse_user_url: record.user_id
? `${baseUrl}/project/${projectId}/users/${encodeURIComponent(record.user_id as string)}`
: null,
},
...(record.trace_user_id
? {
$set: {
langfuse_user_url: `${baseUrl}/project/${projectId}/users/${encodeURIComponent(record.trace_user_id as string)}`,
},
}
: // Capture as anonymous PostHog event (cheaper/faster)
// https://posthog.com/docs/data/anonymous-vs-identified-events?tab=Backend
{ $process_person_profile: false }),
};
}
};
@@ -16,7 +16,7 @@ import {
StringFilter,
} from "../queries/clickhouse-sql/clickhouse-filter";
import { TraceRecordReadType, convertTraceToTraceNull } from "./definitions";
import { tracesTableUiColumnDefinitions } from "../../tableDefinitions/mapTracesTable";
import { tracesTableUiColumnDefinitions } from "../tableMappings/mapTracesTable";
import { UiColumnMappings } from "../../tableDefinitions";
import {
clickhouseClient,
@@ -47,12 +47,16 @@ enum TracesAMTs {
* for <= 29 days, we use traces_30d_amt,
* for all other cases we use traces_all_amt.
*
* If LANGFUSE_EXPERIMENT_WHITELISTED_AMT_TABLES is set, we only return timeframes
* that are whitelisted or fallback to the traces_all_amt.
*
* @param fromTimestamp
*/
export const getTimeframesTracesAMT = (
fromTimestamp: Date | undefined,
): TracesAMTs => {
if (!fromTimestamp) {
// The TracesAllAMT must always be returned if there is no timestamp.
return TracesAMTs.TracesAllAMT;
}
@@ -60,16 +64,26 @@ export const getTimeframesTracesAMT = (
const diffInDays = Math.floor(
(now.getTime() - fromTimestamp.getTime()) / (1000 * 60 * 60 * 24),
);
let selectedTable: TracesAMTs;
if (diffInDays <= 6) {
return TracesAMTs.Traces7dAMT;
selectedTable = TracesAMTs.Traces7dAMT;
} else if (diffInDays <= 29) {
return TracesAMTs.Traces30dAMT;
selectedTable = TracesAMTs.Traces30dAMT;
} else {
selectedTable = TracesAMTs.TracesAllAMT;
}
return TracesAMTs.TracesAllAMT;
// Check if the selected table is whitelisted, fallback to TracesAllAMT if not
return env.LANGFUSE_EXPERIMENT_WHITELISTED_AMT_TABLES.length === 0 ||
env.LANGFUSE_EXPERIMENT_WHITELISTED_AMT_TABLES.includes(selectedTable)
? selectedTable
: TracesAMTs.TracesAllAMT;
};
/**
* Checks if trace exists in clickhouse.
* Additionally, give back the timestamp of the trace as metadata.
*
* @param {string} projectId - Project ID for the trace
* @param {string} traceId - ID of the trace to check
@@ -81,7 +95,7 @@ export const getTimeframesTracesAMT = (
* Filters within ±2 day window
* Used for validating trace references before eval job creation
*/
export const checkTraceExists = async ({
export const checkTraceExistsAndGetTimestamp = async ({
projectId,
traceId,
timestamp,
@@ -95,7 +109,7 @@ export const checkTraceExists = async ({
filter: FilterState;
maxTimeStamp: Date | undefined;
exactTimestamp?: Date;
}): Promise<boolean> => {
}): Promise<{ exists: boolean; timestamp?: Date }> => {
const { tracesFilter } = getProjectIdDefaultFilter(projectId, {
tracesPrefix: "t",
});
@@ -145,7 +159,7 @@ export const checkTraceExists = async ({
`;
return measureAndReturn({
operationName: "checkTraceExists",
operationName: "checkTraceExistsAndGetTimestamp",
projectId,
minStartTime: timestamp ?? exactTimestamp,
input: {
@@ -168,7 +182,7 @@ export const checkTraceExists = async ({
type: "trace",
kind: "exists",
projectId,
operation_name: "checkTraceExists",
operation_name: "checkTraceExistsAndGetTimestamp",
},
timestamp: timestamp ?? exactTimestamp,
},
@@ -177,7 +191,8 @@ export const checkTraceExists = async ({
${observations_cte}
SELECT
t.id as id,
t.project_id as project_id
t.project_id as project_id,
t.timestamp as timestamp
FROM traces t FINAL
${observationFilterRes ? `INNER JOIN observations_agg o ON t.id = o.trace_id AND t.project_id = o.project_id` : ""}
WHERE ${tracesFilterRes.query}
@@ -186,16 +201,26 @@ export const checkTraceExists = async ({
${maxTimeStamp ? `AND timestamp <= {maxTimeStamp: DateTime64(3)}` : ""}
${!maxTimeStamp ? `AND timestamp <= {timestamp: DateTime64(3)} + INTERVAL 2 DAY` : ""}
${exactTimestamp ? `AND timestamp = {exactTimestamp: DateTime64(3)}` : ""}
GROUP BY t.id, t.project_id
GROUP BY t.id, t.project_id, t.timestamp
`;
const rows = await queryClickhouse<{ id: string; project_id: string }>({
const rows = await queryClickhouse<{
id: string;
project_id: string;
timestamp: string;
}>({
query,
params: input.params,
tags: { ...input.tags, experiment_amt: "original" },
});
return rows.length > 0;
return {
exists: rows.length > 0,
timestamp:
rows.length > 0
? parseClickhouseUTCDateTimeFormat(rows[0].timestamp)
: undefined,
};
},
newExecution: async (input) => {
const traceAmt = getTimeframesTracesAMT(input.timestamp);
@@ -212,13 +237,23 @@ export const checkTraceExists = async ({
AND t.project_id = {projectId: String}
`;
const rows = await queryClickhouse<{ id: string; project_id: string }>({
const rows = await queryClickhouse<{
id: string;
project_id: string;
timestamp: string;
}>({
query,
params: input.params,
tags: { ...input.tags, experiment_amt: "new" },
});
return rows.length > 0;
return {
exists: rows.length > 0,
timestamp:
rows.length > 0
? parseClickhouseUTCDateTimeFormat(rows[0].timestamp)
: undefined,
};
},
});
};
@@ -725,27 +760,28 @@ export const getTraceById = async ({
const query = `
SELECT
id,
name as name,
user_id as user_id,
metadata as metadata,
release as release,
version as version,
anyLast(name) as name,
anyLast(user_id) as user_id,
maxMap(metadata) as metadata,
anyLast(release) as release,
anyLast(version) as version,
project_id,
environment,
finalizeAggregation(public) as public,
finalizeAggregation(bookmarked) as bookmarked,
tags,
${renderingProps.truncated ? `left(finalizeAggregation(input), ${env.LANGFUSE_SERVER_SIDE_IO_CHAR_LIMIT})` : "finalizeAggregation(input)"} as input,
${renderingProps.truncated ? `left(finalizeAggregation(output), ${env.LANGFUSE_SERVER_SIDE_IO_CHAR_LIMIT})` : "finalizeAggregation(output)"} as output,
session_id as session_id,
anyLast(environment) as environment,
argMaxMerge(public) as public,
argMaxMerge(bookmarked) as bookmarked,
groupUniqArrayArray(tags) as tags,
${renderingProps.truncated ? `left(argMaxMerge(input), ${env.LANGFUSE_SERVER_SIDE_IO_CHAR_LIMIT})` : "argMaxMerge(input)"} as input,
${renderingProps.truncated ? `left(argMaxMerge(output), ${env.LANGFUSE_SERVER_SIDE_IO_CHAR_LIMIT})` : "argMaxMerge(output)"} as output,
anyLast(session_id) as session_id,
0 as is_deleted,
start_time as timestamp,
created_at,
updated_at,
updated_at as event_ts
FROM traces_all_amt
min(start_time) as timestamp,
min(start_time) as created_at,
max(t.updated_at) as updated_at,
max(t.updated_at) as event_ts
FROM traces_all_amt t
WHERE id = {traceId: String}
AND project_id = {projectId: String}
GROUP BY project_id, id
LIMIT 1
`;
@@ -1184,19 +1220,6 @@ export const deleteTraces = async (projectId: string, traceIds: string[]) => {
},
tags: input.tags,
}),
// Delete from traces_null
commandClickhouse({
query: `
DELETE FROM traces_null
WHERE project_id = {projectId: String}
AND id IN ({traceIds: Array(String)});
`,
params: input.params,
clickhouseConfigs: {
request_timeout: env.LANGFUSE_CLICKHOUSE_DELETION_TIMEOUT_MS,
},
tags: input.tags,
}),
// Delete from traces_all_amt
commandClickhouse({
query: `
@@ -1377,18 +1400,6 @@ export const deleteTracesByProjectId = async (projectId: string) => {
},
tags: input.tags,
}),
// Delete from traces_null
commandClickhouse({
query: `
DELETE FROM traces_null
WHERE project_id = {projectId: String};
`,
params: input.params,
clickhouseConfigs: {
request_timeout: env.LANGFUSE_CLICKHOUSE_DELETION_TIMEOUT_MS,
},
tags: input.tags,
}),
// Delete from traces_all_amt
commandClickhouse({
query: `
@@ -1617,7 +1628,7 @@ export const getUserMetrics = async (
${timestampFilter ? `AND o.start_time >= {traceTimestamp: DateTime64(3)} - ${OBSERVATIONS_TO_TRACE_INTERVAL}` : ""}
AND o.trace_id in (
SELECT distinct id
from __TRACE_TABLE__
from __TRACE_TABLE__ t
where
user_id IN ({userIds: Array(String) })
AND project_id = {projectId: String }
@@ -1761,6 +1772,10 @@ export const getTracesForBlobStorageExport = function (
minTimestamp: Date,
maxTimestamp: Date,
) {
// Determine which trace table to use based on experiment flag
const useAMT = env.LANGFUSE_EXPERIMENT_RETURN_NEW_RESULT === "true";
const traceTable = useAMT ? getTimeframesTracesAMT(minTimestamp) : "traces";
const query = `
SELECT
id,
@@ -1773,12 +1788,12 @@ export const getTracesForBlobStorageExport = function (
session_id,
release,
version,
public,
bookmarked,
${useAMT ? "finalizeAggregation(public)" : "public"} as public,
${useAMT ? "finalizeAggregation(bookmarked)" : "bookmarked"} as bookmarked,
tags,
input,
output
FROM traces FINAL
${useAMT ? "finalizeAggregation(input)" : "input"} as input,
${useAMT ? "finalizeAggregation(output)" : "output"} as output
FROM ${traceTable} FINAL
WHERE project_id = {projectId: String}
AND timestamp >= {minTimestamp: DateTime64(3)}
AND timestamp <= {maxTimestamp: DateTime64(3)}
@@ -1796,6 +1811,7 @@ export const getTracesForBlobStorageExport = function (
type: "trace",
kind: "analytic",
projectId,
experiment_amt: useAMT ? "new" : "original",
},
clickhouseConfigs: {
request_timeout: env.LANGFUSE_CLICKHOUSE_DATA_EXPORT_REQUEST_TIMEOUT_MS,
@@ -1808,6 +1824,10 @@ export const getTracesForPostHog = async function* (
minTimestamp: Date,
maxTimestamp: Date,
) {
// Determine which trace table to use based on experiment flag
const useAMT = env.LANGFUSE_EXPERIMENT_RETURN_NEW_RESULT === "true";
const traceTable = useAMT ? getTimeframesTracesAMT(minTimestamp) : "traces";
const query = `
WITH observations_agg AS (
SELECT o.project_id,
@@ -1835,7 +1855,7 @@ export const getTracesForPostHog = async function* (
o.total_cost as total_cost,
o.latency_milliseconds / 1000 as latency,
o.observation_count as observation_count
FROM traces t FINAL
FROM ${traceTable} t FINAL
LEFT JOIN observations_agg o ON t.id = o.trace_id AND t.project_id = o.project_id
WHERE t.project_id = {projectId: String}
AND t.timestamp >= {minTimestamp: DateTime64(3)}
@@ -1854,6 +1874,7 @@ export const getTracesForPostHog = async function* (
type: "trace",
kind: "analytic",
projectId,
experiment_amt: useAMT ? "new" : "original",
},
clickhouseConfigs: {
request_timeout: env.LANGFUSE_CLICKHOUSE_DATA_EXPORT_REQUEST_TIMEOUT_MS,
@@ -1876,7 +1897,7 @@ export const getTracesForPostHog = async function* (
langfuse_count_observations: record.observation_count,
langfuse_session_id: record.session_id,
langfuse_project_id: projectId,
langfuse_user_id: record.user_id || "langfuse_unknown_user",
langfuse_user_id: record.user_id || null,
langfuse_latency: record.latency,
langfuse_release: record.release,
langfuse_version: record.version,
@@ -1884,11 +1905,15 @@ export const getTracesForPostHog = async function* (
langfuse_environment: record.environment,
langfuse_event_version: "1.0.0",
$session_id: record.posthog_session_id ?? null,
$set: {
langfuse_user_url: record.user_id
? `${baseUrl}/project/${projectId}/users/${encodeURIComponent(record.user_id as string)}`
: null,
},
...(record.user_id
? {
$set: {
langfuse_user_url: `${baseUrl}/project/${projectId}/users/${encodeURIComponent(record.user_id as string)}`,
},
}
: // Capture as anonymous PostHog event (cheaper/faster)
// https://posthog.com/docs/data/anonymous-vs-identified-events?tab=Backend
{ $process_person_profile: false }),
};
}
};
@@ -1970,6 +1995,10 @@ export async function getAgentGraphData(params: {
SELECT
id,
parent_observation_id,
type,
name,
start_time,
end_time,
metadata['langgraph_node'] AS node,
metadata['langgraph_step'] AS step
FROM
@@ -15,6 +15,7 @@ import {
DashboardDefinitionSchema,
} from "./types";
import { z } from "zod/v4";
import { singleFilter } from "../../../";
export class DashboardService {
/**
@@ -155,6 +156,32 @@ export class DashboardService {
});
}
/**
* Updates a dashboard's filters.
*/
public static async updateDashboardFilters(
dashboardId: string,
projectId: string,
filters: z.infer<typeof singleFilter>[],
userId?: string,
): Promise<DashboardDomain> {
const updatedDashboard = await prisma.dashboard.update({
where: {
id: dashboardId,
projectId,
},
data: {
updatedBy: userId,
filters,
},
});
return DashboardDomainSchema.parse({
...updatedDashboard,
owner: updatedDashboard.projectId ? "PROJECT" : "LANGFUSE",
});
}
/**
* Gets a dashboard by ID.
*/
@@ -35,6 +35,12 @@ export const HistogramChartConfig = BaseTotalValueChartConfig.extend({
export const PivotTableChartConfig = BaseTotalValueChartConfig.extend({
type: z.literal("PIVOT_TABLE"),
defaultSort: z
.object({
column: z.string(),
order: z.enum(["ASC", "DESC"]),
})
.optional(),
});
// Define dimension schema
@@ -91,6 +97,7 @@ export const DashboardDomainSchema = z.object({
name: z.string(),
description: z.string(),
definition: DashboardDefinitionSchema,
filters: z.array(singleFilter).default([]),
owner: OwnerEnum,
});
@@ -1,7 +1,12 @@
import z from "zod/v4";
import { prisma } from "../../../db";
import { LangfuseNotFoundError, QUEUE_ERROR_MESSAGES } from "../../../errors";
import {
ForbiddenError,
LangfuseNotFoundError,
QUEUE_ERROR_MESSAGES,
} from "../../../errors";
import { LLMApiKeySchema, ZodModelConfig } from "../../llm/types";
import { testModelCall } from "../../llm/testModelCall";
type ValidConfig = {
provider: string;
@@ -47,6 +52,23 @@ export class DefaultEvalModelService {
);
}
try {
if (LLMApiKeySchema.safeParse(llmApiKey).success) {
// Make a test structured output call to validate the LLM key
await testModelCall({
provider,
model,
apiKey: llmApiKey as z.infer<typeof LLMApiKeySchema>,
modelConfig: modelParams,
});
}
} catch (err) {
const message = err instanceof Error ? err.message : "Unknown error";
throw new ForbiddenError(
`Model configuration not valid for evaluation. ${message}`,
);
}
// Create or update the default model
return prisma.defaultLlmModel.upsert({
where: {
@@ -88,7 +88,7 @@ export function parseSlackInstallationMetadata(
*/
export class SlackService {
private static instance: SlackService | null = null;
private installer: InstallProvider;
private readonly installer: InstallProvider;
private constructor() {
this.installer = new InstallProvider({
@@ -296,14 +296,20 @@ export class SlackService {
}
/**
* Get channels accessible to the bot
* Recursively fetch all channels accessible to the bot
* Uses cursor-based pagination defined by Slack API https://api.slack.com/apis/pagination
*/
async getChannels(client: WebClient): Promise<SlackChannel[]> {
private async getChannelsRecursive(
client: WebClient,
cursor?: string,
fetchedRecords: number = 0,
): Promise<SlackChannel[]> {
try {
const result = await client.conversations.list({
exclude_archived: true,
types: "public_channel",
limit: 200,
cursor: cursor,
});
if (!result.ok) {
@@ -319,6 +325,41 @@ export class SlackService {
}),
);
const nextCursor = result.response_metadata?.next_cursor;
if (
nextCursor &&
fetchedRecords + channels.length < env.SLACK_FETCH_LIMIT
) {
try {
const nextPageChannels = await this.getChannelsRecursive(
client,
nextCursor,
fetchedRecords + channels.length,
);
return [...channels, ...nextPageChannels];
} catch (error) {
logger.error(
`Failed to retrieve next page of channels, returning only already fetched`,
error,
);
}
}
return channels;
} catch (error) {
logger.error("Failed to fetch channels recursively", { error, cursor });
throw new Error(
`Failed to fetch channels: ${error instanceof Error ? error.message : "Unknown error"}`,
);
}
}
/**
* Get channels accessible to the bot
*/
async getChannels(client: WebClient): Promise<SlackChannel[]> {
try {
const channels = await this.getChannelsRecursive(client);
logger.debug("Retrieved channels from Slack", {
channelCount: channels.length,
});
@@ -1,61 +0,0 @@
import { evalDatasetFormFilterCols } from "../../tableDefinitions/tracesTable";
import { FilterState } from "../../types";
import { tableColumnsToSqlFilterAndPrefix } from "../filterToPrisma";
import { Prisma, prisma } from "../../db";
type FetchDatasetItemsTableProps = {
select: "count";
projectId: string;
filter: FilterState;
};
const getDatasetRunItemsTableGenericPg = async <T>(
props: FetchDatasetItemsTableProps,
) => {
const { select, projectId, filter } = props;
let sqlSelect: Prisma.Sql;
switch (select) {
case "count":
sqlSelect = Prisma.sql`count(*) as count`;
break;
default:
// eslint-disable-next-line no-case-declarations, no-unused-vars
const exhaustiveCheckDefault: never = select;
throw new Error(`Unknown select type: ${select}`);
}
const datasetItemsFilter = tableColumnsToSqlFilterAndPrefix(
filter,
evalDatasetFormFilterCols,
"dataset_items",
);
const query = Prisma.sql`
SELECT
${sqlSelect}
FROM dataset_run_items as dri
JOIN dataset_items as di ON di.id = dri.dataset_item_id AND di.project_id = ${projectId}
WHERE dri.project_id = ${projectId}
${datasetItemsFilter}
`;
const res = await prisma.$queryRaw<T>(query);
return res;
};
export const getDatasetRunItemsTableCountPg = async (props: {
projectId: string;
filter: FilterState;
}) => {
const res = await getDatasetRunItemsTableGenericPg<Array<{ count: bigint }>>({
select: "count",
projectId: props.projectId,
filter: props.filter,
});
const totalCount = res.length > 0 ? Number(res[0].count) : 0;
return { totalCount };
};
@@ -1,6 +1,6 @@
import { ClickHouseClientConfigOptions } from "@clickhouse/client";
import { OrderByState } from "../../interfaces/orderBy";
import { sessionCols } from "../../tableDefinitions/mapSessionTable";
import { sessionCols } from "../tableMappings/mapSessionTable";
import { FilterState } from "../../types";
import { convertDateToClickhouseDateTime } from "../clickhouse/client";
import { measureAndReturn } from "../clickhouse/measureAndReturn";
@@ -1,5 +1,5 @@
import { OrderByState } from "../../interfaces/orderBy";
import { tracesTableUiColumnDefinitions } from "../../tableDefinitions";
import { tracesTableUiColumnDefinitions } from "../tableMappings";
import { FilterState } from "../../types";
import {
StringFilter,
@@ -356,22 +356,23 @@ async function getTracesTableGeneric(props: FetchTracesTableProps) {
let sqlSelect: string;
switch (select) {
case "count":
sqlSelect = "count(*) as count";
// Using uniqExact here as we need the correct count to handle pagination right
sqlSelect = "uniqExact(t.id) 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.aggregated_level as level,
os.error_count as error_count,
os.warning_count as warning_count,
os.default_count as default_count,
os.debug_count as debug_count,
os.observation_count as observation_count,
o.latency_milliseconds / 1000 as latency,
o.cost_details as cost_details,
o.usage_details as usage_details,
o.aggregated_level as level,
o.error_count as error_count,
o.warning_count as warning_count,
o.default_count as default_count,
o.debug_count as debug_count,
o.observation_count as observation_count,
s.scores_avg as scores_avg,
s.score_categories as score_categories,
t.public as public`;
@@ -447,9 +448,9 @@ async function getTracesTableGeneric(props: FetchTracesTableProps) {
${observationsAndScoresCTE}
SELECT ${sqlSelect}
-- FINAL is used for non default ordering and count.
FROM traces t ${["metrics", "rows", "identifiers"].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` : ""}
-- FINAL is used for non default ordering.
FROM traces t ${defaultOrder || select === "count" ? "" : "FINAL"}
${select === "metrics" || requiresObservationsJoin ? `LEFT JOIN observations_stats o on o.project_id = t.project_id and o.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}` : ""}
@@ -492,46 +493,46 @@ async function getTracesTableGeneric(props: FetchTracesTableProps) {
let sqlSelect: string;
switch (select) {
case "count":
sqlSelect = "count(*) as count";
sqlSelect = "uniq(t.id) 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.aggregated_level as level,
os.error_count as error_count,
os.warning_count as warning_count,
os.default_count as default_count,
os.debug_count as debug_count,
os.observation_count as observation_count,
s.scores_avg as scores_avg,
s.score_categories as score_categories,
t.public as public`;
min(t.timestamp) as timestamp,
max(o.latency_milliseconds) / 1000 as latency,
anyLast(o.cost_details) as cost_details,
anyLast(o.usage_details) as usage_details,
anyLast(o.aggregated_level) as level,
anyLast(o.error_count) as error_count,
anyLast(o.warning_count) as warning_count,
anyLast(o.default_count) as default_count,
anyLast(o.debug_count) as debug_count,
anyLast(o.observation_count) as observation_count,
anyLast(s.scores_avg) as scores_avg,
anyLast(s.score_categories) as score_categories,
argMaxMerge(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,
finalizeAggregation(t.bookmarked) as bookmarked,
t.name as name,
t.release as release,
t.version as version,
t.user_id as user_id,
t.environment as environment,
t.session_id as session_id,
finalizeAggregation(t.public) as public`;
min(t.timestamp) as timestamp,
groupUniqArrayArray(t.tags) as tags,
argMaxMerge(t.bookmarked) as bookmarked,
anyLast(t.name) as name,
anyLast(t.release) as release,
anyLast(t.version) as version,
anyLast(t.user_id) as user_id,
anyLast(t.environment) as environment,
anyLast(t.session_id) as session_id,
argMaxMerge(t.public) as public`;
break;
case "identifiers":
sqlSelect = `
t.id as id,
t.project_id as projectId,
t.timestamp as timestamp`;
min(t.timestamp) as timestamp`;
break;
default:
throw new Error(`Unknown select type: ${select}`);
@@ -547,6 +548,7 @@ async function getTracesTableGeneric(props: FetchTracesTableProps) {
const chOrderBy = orderByToClickhouseSql(
[orderBy ?? null].flat(),
tracesTableUiColumnDefinitions,
true,
);
const tracesAmt =
@@ -558,12 +560,13 @@ async function getTracesTableGeneric(props: FetchTracesTableProps) {
${observationsAndScoresCTE}
SELECT ${sqlSelect}
FROM ${tracesAmt} t FINAL
${select === "metrics" || requiresObservationsJoin ? `LEFT JOIN observations_stats os on os.project_id = t.project_id and os.trace_id = t.id` : ""}
FROM ${tracesAmt} t
${select === "metrics" || requiresObservationsJoin ? `LEFT JOIN observations_stats o on o.project_id = t.project_id and o.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}
${select !== "count" ? "GROUP BY project_id, id" : ""}
${chOrderBy}
${limit !== undefined && page !== undefined ? `LIMIT {limit: Int32} OFFSET {offset: Int32}` : ""}
`;
@@ -0,0 +1,5 @@
export * from "./mapObservationsTable";
export * from "./mapTracesTable";
export * from "../../tableDefinitions/mapDashboards";
export * from "./mapScoresTable";
export * from "./mapDatasetRunItemsTable";
@@ -0,0 +1,60 @@
import { UiColumnMappings } from "../../tableDefinitions";
import { DatasetRunItemDomain } from "../../domain/dataset-run-items";
export const datasetRunItemsTableUiColumnDefinitions: UiColumnMappings = [
{
uiTableName: "Dataset Run ID",
uiTableId: "datasetRunId",
clickhouseTableName: "dataset_run_items_rmt",
clickhouseSelect: 'dri."dataset_run_id"',
},
{
uiTableName: "Created At",
uiTableId: "createdAt",
clickhouseTableName: "dataset_run_items_rmt",
clickhouseSelect: 'dri."created_at"',
},
{
uiTableName: "Event Timestamp",
uiTableId: "eventTs",
clickhouseTableName: "dataset_run_items_rmt",
clickhouseSelect: 'dri."event_ts"',
},
{
uiTableName: "Dataset Item ID",
uiTableId: "datasetItemId",
clickhouseTableName: "dataset_run_items_rmt",
clickhouseSelect: 'dri."dataset_item_id"',
},
{
uiTableName: "Dataset",
uiTableId: "datasetId",
clickhouseTableName: "dataset_run_items_rmt",
clickhouseSelect: 'dri."dataset_id"',
},
];
export const mapDatasetRunItemFilterColumn = (
dataset: Pick<DatasetRunItemDomain, "id" | "datasetId">,
column: string,
): unknown => {
const columnDef = datasetRunItemsTableUiColumnDefinitions.find(
(col) =>
col.uiTableId === column ||
col.uiTableName === column ||
col.clickhouseSelect === column,
);
if (!columnDef) {
throw new Error(`Unhandled column for dataset run items filter: ${column}`);
}
switch (columnDef.uiTableId) {
case "id":
return dataset.id;
case "datasetId":
return dataset.datasetId;
default:
throw new Error(
`Unhandled column in dataset run items filter mapping: ${column}`,
);
}
};

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