Compare commits

...
81 Commits
Author SHA1 Message Date
Hassieb Pakzad dd823da1da chore: release v2.49.0
CI/CD / lint (push) Waiting to run
CI/CD / test-docker-build (push) Waiting to run
CI/CD / tests-web (20) (push) Waiting to run
CI/CD / tests-worker (20) (push) Waiting to run
CI/CD / e2e-tests (push) Waiting to run
CI/CD / all-ci-passed (push) Blocked by required conditions
CI/CD / push-docker-image (push) Blocked by required conditions
release.yml / release (push) Waiting to run
2024-06-06 17:52:18 +02:00
992f0affbc chore: remove trace fkey constraint from datasetrunitems (#2262)
Co-authored-by: Marc Klingen <git@marcklingen.com>
2024-06-06 17:49:17 +02:00
Mish UshakovandGitHub b8c192324c docs: Updated contributing guide (#2259) 2024-06-06 14:28:57 +02:00
Marc KlingenandGitHub 1416e760ff chore: remove dangling worker/generated/types.ts (#2257) 2024-06-06 14:11:01 +02:00
Marc KlingenandGitHub 4266b46dd9 chore(db): drop legacy pricings table, replaced by models (#2256) 2024-06-06 11:36:08 +02:00
Marc KlingenandGitHub 1933f8f201 feat: add token/character count support for gemini-1.5-flash and gemini-1.5-pro (#2255) 2024-06-06 11:21:42 +02:00
Marc KlingenandGitHub 1e35eb3a66 fix(ui): encode prompt name on link from traces (#2253) 2024-06-06 08:30:52 +00:00
Marc KlingenandGitHub 5478c5e317 feat(ui): use dropdownmenu for version modal (#2252) 2024-06-06 08:20:01 +00:00
Marc KlingenandGitHub 140452c89e feat(ui): add upgrade popover to version number (#2251) 2024-06-06 00:48:23 +00:00
Marc KlingenandGitHub 7ac92e83da feat(ui): use pagination instead of virtualization on sessions view (#2248) 2024-06-05 22:21:01 +00:00
Marc Klingen 8a65bad7d4 chore: release v2.48.1
CI/CD / lint (push) Waiting to run
CI/CD / test-docker-build (push) Waiting to run
CI/CD / tests-web (20) (push) Waiting to run
CI/CD / tests-worker (20) (push) Waiting to run
CI/CD / e2e-tests (push) Waiting to run
CI/CD / all-ci-passed (push) Blocked by required conditions
CI/CD / push-docker-image (push) Blocked by required conditions
release.yml / release (push) Waiting to run
2024-06-05 23:04:25 +02:00
Marc Klingen 6c6348bfbe chore(rbac): grant new scoreConfigs:CUD scope to MEMBER role 2024-06-05 23:02:42 +02:00
Marlies Mayerhofer 9ff7fc942d fix: add maintainers heading in contributing file 2024-06-05 22:53:30 +02:00
Marlies Mayerhofer a3f59e7e95 feat: enhance robustness of multi select item search 2024-06-05 22:46:48 +02:00
Max DeichmannandGitHub 445bdcb65c chore: log level worker (#2246)
* push

* push
2024-06-05 20:34:12 +02:00
Max DeichmannandGitHub 8230aa79b3 chore: remove comments (#2245) 2024-06-05 20:11:10 +02:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
57ca7fb478 chore(deps-dev): bump @typescript-eslint/parser from 7.7.0 to 7.12.0 (#2222)
Bumps [@typescript-eslint/parser](https://github.com/typescript-eslint/typescript-eslint/tree/HEAD/packages/parser) from 7.7.0 to 7.12.0.
- [Release notes](https://github.com/typescript-eslint/typescript-eslint/releases)
- [Changelog](https://github.com/typescript-eslint/typescript-eslint/blob/main/packages/parser/CHANGELOG.md)
- [Commits](https://github.com/typescript-eslint/typescript-eslint/commits/v7.12.0/packages/parser)

---
updated-dependencies:
- dependency-name: "@typescript-eslint/parser"
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-06-05 20:05:17 +02:00
marliessophieandGitHub 2db88101e1 feat: add score and score config user tracking events (#2240)
* feat: add score and score config user tracking events

* add: icon for annotation button
2024-06-05 13:12:22 +00:00
Hassieb PakzadandGitHub 669167002a fix: parse object type message content (#2239) 2024-06-05 12:48:30 +00:00
marliessophieandGitHub 2f6ade9354 fix(annotation): improve keyboard navigation experience in drawer (#2236) 2024-06-05 13:41:04 +02:00
Marc Klingen 86f44df5dc chore: release v2.48.0
CI/CD / lint (push) Waiting to run
CI/CD / test-docker-build (push) Waiting to run
CI/CD / tests-web (20) (push) Waiting to run
CI/CD / tests-worker (20) (push) Waiting to run
CI/CD / e2e-tests (push) Waiting to run
CI/CD / all-ci-passed (push) Blocked by required conditions
CI/CD / push-docker-image (push) Blocked by required conditions
release.yml / release (push) Waiting to run
2024-06-05 00:32:21 +02:00
marliessophieandGitHub 34da790d5e chore(scores): backfill manual scores with config_id (#2233) 2024-06-05 00:25:10 +02:00
c16d86f7e5 feat(scores): add annotation slider and score config settings for categorical annotation (#2157)
---------

Co-authored-by: Marc Klingen <git@marcklingen.com>
2024-06-05 00:06:05 +02:00
Marc KlingenandGitHub 1b2b8cc43a ci: fix flaky docker test build step by pinning turbo v1 (#2234) 2024-06-04 23:48:00 +02:00
Marc Klingen 3903790086 docs(api): refer to GET /traces endpoint to fetch large sessions with pagination 2024-06-04 20:22:23 +02:00
6da1280c43 perf(user-table): add timestamp filter to user table (#2231)
* feat: add timestamp filter to user table

* users table definition instead of reusing traces

* add filter to total as well to make table state consistent

---------

Co-authored-by: Marc Klingen <git@marcklingen.com>
2024-06-04 19:08:00 +02:00
Marc Klingen 85343bf26c chore: release v2.47.7
CI/CD / lint (push) Waiting to run
CI/CD / test-docker-build (push) Waiting to run
CI/CD / tests-web (20) (push) Waiting to run
CI/CD / tests-worker (20) (push) Waiting to run
CI/CD / e2e-tests (push) Waiting to run
CI/CD / all-ci-passed (push) Blocked by required conditions
CI/CD / push-docker-image (push) Blocked by required conditions
release.yml / release (push) Waiting to run
2024-06-04 16:56:48 +02:00
Richard KrümmelandGitHub e3b4dc1928 feat(api): add sessionId filter for traces endpoint (#2227) 2024-06-04 14:38:51 +00:00
Marc KlingenandGitHub a4939bacd8 perf: add index on scores.name (#2228) 2024-06-04 15:35:21 +02:00
Hassieb Pakzad 4441942cf5 chore: release v2.47.6
CI/CD / lint (push) Waiting to run
CI/CD / test-docker-build (push) Waiting to run
CI/CD / tests-web (20) (push) Waiting to run
CI/CD / tests-worker (20) (push) Waiting to run
CI/CD / e2e-tests (push) Waiting to run
CI/CD / all-ci-passed (push) Blocked by required conditions
CI/CD / push-docker-image (push) Blocked by required conditions
release.yml / release (push) Waiting to run
2024-06-04 14:02:23 +02:00
Hassieb PakzadandGitHub 6597b02c09 perf: boost sessions table performance (#2225) 2024-06-04 13:42:09 +02:00
Marc KlingenandGitHub 0df3ad27dc perf: add index on dataset_items.source_trace_id (#2220) 2024-06-04 01:57:19 +02:00
Marc KlingenandGitHub 696d458a27 fix(cloud): handle posthog settings rbac correctly in frontend (#2223) 2024-06-03 23:35:49 +00:00
Marc KlingenandGitHub fd18ab605d feat(api): add count and cost to daily metrics api (#2219) 2024-06-03 22:41:01 +02:00
Marc Klingen d47d14eecd chore: release v2.47.5
CI/CD / lint (push) Waiting to run
CI/CD / test-docker-build (push) Waiting to run
CI/CD / tests-web (20) (push) Waiting to run
CI/CD / tests-worker (20) (push) Waiting to run
CI/CD / e2e-tests (push) Waiting to run
CI/CD / all-ci-passed (push) Blocked by required conditions
CI/CD / push-docker-image (push) Blocked by required conditions
release.yml / release (push) Waiting to run
2024-06-03 19:56:18 +02:00
Richard KrümmelandGitHub 24a81e1589 fix: update validation schema for prompt tags (#2217) 2024-06-03 18:51:39 +02:00
Max Deichmann ac0c13f3e5 chore: release v2.47.4
CI/CD / lint (push) Waiting to run
CI/CD / test-docker-build (push) Waiting to run
CI/CD / tests-web (20) (push) Waiting to run
CI/CD / tests-worker (20) (push) Waiting to run
CI/CD / e2e-tests (push) Waiting to run
CI/CD / all-ci-passed (push) Blocked by required conditions
CI/CD / push-docker-image (push) Blocked by required conditions
release.yml / release (push) Waiting to run
2024-06-03 18:07:58 +02:00
Marc KlingenandGitHub a7b90fa32c feat(ui): preview model cost per 1k, 100k, and 1M units (#2216) 2024-06-03 13:45:10 +00:00
Marc Klingen 9eaf2a2e0f docs(api): add tags to prompt api response type 2024-06-03 14:24:48 +02:00
Max DeichmannandGitHub 61e3cc33bf fix: do not log expected eval errors as sentry errors (#2213) 2024-06-03 12:22:41 +00:00
2be198400d feat(api): add prompt tags via API (#2180)
---------

Co-authored-by: Marc Klingen <git@marcklingen.com>
2024-06-03 12:10:15 +00:00
Max Deichmann 830df682f9 chore: release v2.47.3
CI/CD / lint (push) Waiting to run
CI/CD / test-docker-build (push) Waiting to run
CI/CD / tests-web (20) (push) Waiting to run
CI/CD / tests-worker (20) (push) Waiting to run
CI/CD / e2e-tests (push) Waiting to run
CI/CD / all-ci-passed (push) Blocked by required conditions
CI/CD / push-docker-image (push) Blocked by required conditions
release.yml / release (push) Waiting to run
2024-06-03 12:19:27 +02:00
Max DeichmannandGitHub 2a420b2e90 chore: reduce sentry sampling (#2212) 2024-06-03 11:06:56 +02:00
Marc Klingen 53127c34ab chore: add posthog ui_host 2024-06-02 21:18:35 +02:00
Marc Klingen 9c57ff4853 chore: reduce cloud telemetry to 4 times a day 2024-06-02 20:18:55 +02:00
Marc Klingen a71176eda4 chore: disable posthog autocapture 2024-06-02 20:12:54 +02:00
Marc KlingenandGitHub f59a85f412 fix(ui): full prompt name on hover when saving as new version from playground (#2208) 2024-06-02 14:31:01 +00:00
Marc Klingen ec59436522 chore: release v2.47.2
CI/CD / lint (push) Waiting to run
CI/CD / test-docker-build (push) Waiting to run
CI/CD / tests-web (20) (push) Waiting to run
CI/CD / tests-worker (20) (push) Waiting to run
CI/CD / e2e-tests (push) Waiting to run
CI/CD / all-ci-passed (push) Blocked by required conditions
CI/CD / push-docker-image (push) Blocked by required conditions
release.yml / release (push) Waiting to run
2024-06-02 01:37:09 +02:00
Marc KlingenandGitHub 53a3d58a2f fix(ui): do not run llm api key query when it is not available (#2205) 2024-06-02 01:35:41 +02:00
Marc KlingenandGitHub c1162a3a8b chore: update support page to include GH discussions (#2204) 2024-06-01 15:59:34 +00:00
Marc Klingen 43d8c4ec99 chore: release v2.47.1
CI/CD / lint (push) Waiting to run
CI/CD / test-docker-build (push) Waiting to run
CI/CD / tests-web (20) (push) Waiting to run
CI/CD / tests-worker (20) (push) Waiting to run
CI/CD / e2e-tests (push) Waiting to run
CI/CD / all-ci-passed (push) Blocked by required conditions
CI/CD / push-docker-image (push) Blocked by required conditions
release.yml / release (push) Waiting to run
2024-05-31 16:15:00 +02:00
Marc KlingenandGitHub 3a88315ac5 perf: add date range filter to scores, default to 7 days across core tables (#2198) 2024-05-31 14:12:16 +00:00
Marc KlingenandGitHub e06b96a5ee fix(auth): add custom timeout to sso config query (#2192) 2024-05-31 10:21:03 +00:00
Marc KlingenandGitHub 307f54ef8a chore(cloud): update usage & billing copy (#2193) 2024-05-30 23:38:43 +00:00
Marc KlingenandGitHub 196dc1bf00 feat: support claude-3 pricing on google vertex (#2191) 2024-05-30 23:36:11 +02:00
Marc Klingen ee2c0fb27b chore: release v2.47.0
CI/CD / lint (push) Waiting to run
CI/CD / test-docker-build (push) Waiting to run
CI/CD / tests-web (20) (push) Waiting to run
CI/CD / tests-worker (20) (push) Waiting to run
CI/CD / e2e-tests (push) Waiting to run
CI/CD / all-ci-passed (push) Blocked by required conditions
CI/CD / push-docker-image (push) Blocked by required conditions
release.yml / release (push) Waiting to run
2024-05-30 19:26:01 +02:00
Marc KlingenandGitHub 27623de650 fix(ui): do not wrap tags within tables (#2189) 2024-05-30 14:05:39 +00:00
Marc Klingen 61c8f6c269 docs update integrations in readme 2024-05-30 15:40:02 +02:00
Marc KlingenandGitHub cc9e21ccb3 perf(datasets): lateral join on runsByDatasetId, remove unused include on runitemsByRunIdOrItemId (#2183) 2024-05-29 23:59:48 +00:00
078ceb7ff1 feat(ui): add client-side error notifications (#2166)
---------

Co-authored-by: Marc Klingen <git@marcklingen.com>
2024-05-29 14:41:41 +00:00
Marc KlingenandGitHub 9436c33025 fix(ui): json views should use resolve theme instead of the literal theme which can be system (#2168) 2024-05-29 14:14:38 +00:00
Marc KlingenandGitHub 88b3e1bea6 chore(cloud): remove posthog ingestion metrics as we moved to airbyte (#2179) 2024-05-29 13:22:02 +00:00
Richard KrümmelandGitHub da042d8284 fix(ui): allow for long model names in select filter (#2173) 2024-05-29 13:42:51 +02:00
Max DeichmannandGitHub 50264701f6 chore: reduce logging (#2178)
push
2024-05-29 09:14:33 +00:00
Marc KlingenandGitHub 3ee8a2e026 perf(db): add cursors and indicies on cursors (#2174) 2024-05-29 00:06:40 +02:00
Marc KlingenandGitHub ed04357815 feat(ui): add linter to json inputs (#2171) 2024-05-28 14:40:13 +00:00
Max Deichmann fa26275634 chore: release v2.46.0
CI/CD / lint (push) Waiting to run
CI/CD / test-docker-build (push) Waiting to run
CI/CD / tests-web (20) (push) Waiting to run
CI/CD / tests-worker (20) (push) Waiting to run
CI/CD / e2e-tests (push) Waiting to run
CI/CD / all-ci-passed (push) Blocked by required conditions
CI/CD / push-docker-image (push) Blocked by required conditions
release.yml / release (push) Waiting to run
2024-05-28 16:04:42 +02:00
Max DeichmannandGitHub 7587c75459 chore: do not log io (#2164) 2024-05-28 11:27:12 +00:00
ClemoandGitHub a956d0115c docs: update 2024-05-27 19:14:33 -07:00
Marc KlingenandGitHub cb82835489 fix(ui): overflow behavior of tag manager (#2156) 2024-05-27 17:13:32 +02:00
Hassieb PakzadandGitHub d1c1e60eca feat: use custom keys for playground (#2139) 2024-05-27 14:29:05 +02:00
Marc Klingen 81b612273a chore: release v2.45.1
CI/CD / lint (push) Waiting to run
CI/CD / test-docker-build (push) Waiting to run
CI/CD / tests-web (20) (push) Waiting to run
CI/CD / tests-worker (20) (push) Waiting to run
CI/CD / e2e-tests (push) Waiting to run
CI/CD / all-ci-passed (push) Blocked by required conditions
CI/CD / push-docker-image (push) Blocked by required conditions
release.yml / release (push) Waiting to run
2024-05-27 13:10:28 +02:00
Marc KlingenandGitHub 2c1abf2f6e feat(auth): add AWS Cognito provider (#2151)
Fixes #2107
2024-05-27 10:21:04 +00:00
Marc KlingenandGitHub db1a662f7f fix(ui): default hide version and release on trace table (#2150) 2024-05-27 10:06:22 +00:00
Richard KrümmelandGitHub 1b0e3de709 fix(ui): table header tooltips (#2148) 2024-05-27 11:20:48 +02:00
Richard KrümmelandGitHub be8aec2f8f feat(theme): add posthog (#2147) 2024-05-27 11:20:11 +02:00
adc01014c0 fix(scores): table source filter (#2144)
Co-authored-by: Marc Klingen <git@marcklingen.com>
2024-05-24 19:29:39 +00:00
Marc KlingenandGitHub 215af995fb perf: add indicies to foreign keys on job executions table (#2143) 2024-05-24 21:12:52 +02:00
marliessophieandGitHub 74ff237145 feat(scores): rename source REVIEW to ANNOTATION (#2141)
* feat(scores): add 'ANNOTATION' to `ScoreSource`

* feat: adjust seeder

* feat: adjust trpc router

* add: backfill scores source `REVIEW` to `ANNOTATION`

* add: drop `REVIEW` from scores source enum

* update yml

* refactor: rename trpc procedures
2024-05-24 19:58:30 +02:00
porter-deployment-app[bot]GitHubporter-deployment-app[bot] <87230664+porter-deployment-app[bot]@users.noreply.github.com>
5c2ae0e678 Enable Porter Application worker (#2138)
Create porter_stack_worker.yml file

Co-authored-by: porter-deployment-app[bot] <87230664+porter-deployment-app[bot]@users.noreply.github.com>
2024-05-24 14:42:46 +00:00
marliessophieandGitHub a6d8ce8c96 feat(score_config): add table via migration (#2126)
* feat: add author user id column to scores table

* feat: add author user id to scores created via trpc router

* feat: add author user id to scores created in seeder

* fixup

* add: index to author user id on scores

* feat: annotation config schema

* add: migration

* add: migrations in order

* fixup after merge

* fix: replicate to worker/types

* fix: address feedback

* rn: to scoreConfig

* push

* refactor: rn `ScoreDataType` to `ScoreConfigDataType`

* fix

* fix: scores server test

* context: order of returned data can vary based on how data is retrieved. Likely re-ordered due to new index on configId in scores table

* fix: re-generate types for correct alphabetical order

* fix: remodel migrations to match new schema

* feat: rm backfill migration for data_type on scores

* make data_type column optional on scores

* push: regenerate types

* push

* feat: make dataType on scores required again

* rename: scores add data type migration

* re-run migrations

* push
2024-05-23 19:26:16 +02:00
179 changed files with 4726 additions and 2029 deletions
+1 -1
View File
@@ -47,5 +47,5 @@ REDIS_PORT=6379
REDIS_AUTH="myredissecret"
LANGFUSE_WORKER_PASSWORD=mybasicauthsecret
# openssl rand -base64 32 used only here
# openssl rand -hex 32 used only here
ENCRYPTION_KEY=6c16874e5c0f0cc74ddec00425fa99fbe9ffbe412b7d5d906a4b00005df91403
+5
View File
@@ -58,6 +58,11 @@ LANGFUSE_CSP_ENFORCE_HTTPS="true"
# AUTH_AUTH0_CLIENT_SECRET=
# AUTH_AUTH0_ISSUER=
# AUTH_AUTH0_ALLOW_ACCOUNT_LINKING=false
# AUTH_COGNITO_CLIENT_ID=
# AUTH_COGNITO_CLIENT_SECRET=
# AUTH_COGNITO_ISSUER=
# AUTH_COGNITO_ALLOW_ACCOUNT_LINKING=false
# Transactional email, optional
# Defines the email address to use as the from address.
+29
View File
@@ -0,0 +1,29 @@
"on":
push:
branches:
- main
name: Deploy to worker
jobs:
porter-deploy:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v3
- name: Set Github tag
id: vars
run: echo "sha_short=$(git rev-parse --short HEAD)" >> $GITHUB_OUTPUT
- name: Setup porter
uses: porter-dev/setup-porter@v0.1.0
- name: Deploy stack
timeout-minutes: 30
run: exec porter apply
env:
PORTER_CLUSTER: "3959"
PORTER_DEPLOYMENT_TARGET_ID: d2bb23e4-1d77-48f2-a113-383d076959b3
PORTER_HOST: https://dashboard.getporter.dev
PORTER_PR_NUMBER: ${{ github.event.number }}
PORTER_PROJECT: "12565"
PORTER_REPO_NAME: ${{ github.event.repository.name }}
PORTER_STACK_NAME: worker
PORTER_TAG: ${{ steps.vars.outputs.sha_short }}
PORTER_TOKEN: ${{ secrets.PORTER_STACK_12565_3959 }}
+13 -1
View File
@@ -191,6 +191,16 @@ Requirements
pnpm run dev
```
7. Open the web app in the browser:
http://localhost:3000
8. Log in as a test user (after you ran `db:seed` command):
Username: demo@langfuse.com
Password: password
## Monorepo quickstart
- Available packages and their dependencies
@@ -358,7 +368,9 @@ The background color of the following component will be `hsl(var(--primary))` an
By following these guidelines, you can ensure that any contributions to our theme are consistent, maintainable, and aligned with our design system.
## Using secrets stored in 1Password
## Maintainers
### Using secrets stored in 1Password
When applying changes to non-local environments, you may need to use secrets stored in 1Password. We use the 1Password CLI for this purpose.
+18 -14
View File
@@ -106,22 +106,26 @@ See the [→ Quickstart](https://langfuse.com/docs/get-started) to integrate Lan
### Integrations
| Integration | Supports | Description |
| ---------------------------------------------------------------- | ------------- | ----------------------------------------------------------- |
| [**SDK** - _recommended_](https://langfuse.com/docs/sdk) | Python, JS/TS | Manual instrumentation using the SDKs for full flexibility. |
| [OpenAI SDK](https://langfuse.com/docs/integrations/openai) | Python, JS/TS | Automated instrumentation of OpenAI SDK. |
| [Langchain](https://langfuse.com/docs/integrations/langchain) | Python, JS/TS | Instrumentation via Langchain callbacks. |
| [LlamaIndex](https://langfuse.com/docs/integrations/llama-index) | Python | Automated instrumentation via LlamaIndex callback system. |
| [API](https://langfuse.com/docs/api) | | Directly call the public API. OpenAPI spec available. |
| Integration | Supports | Description |
| -------------------------------------------------------- | -------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------ |
| [SDK](/docs/sdk) | Python, JS/TS | Manual instrumentation using the SDKs for full flexibility. |
| [OpenAI](/docs/integrations/openai) | Python, JS/TS | Automated instrumentation using drop-in replacement of OpenAI SDK. |
| [Langchain](/docs/integrations/langchain) | Python, JS/TS | Automated instrumentation by passing callback handler to Langchain application. |
| [LlamaIndex](/docs/integrations/llama-index/get-started) | Python | Automated instrumentation via LlamaIndex callback system. |
| [Haystack](/docs/integrations/haystack) | Python | Automated instrumentation via Haystack content tracing system. |
| [LiteLLM](/docs/integrations/litellm) | Python, JS/TS (proxy only) | Use any LLM as a drop in replacement for GPT. Use Azure, OpenAI, Cohere, Anthropic, Ollama, VLLM, Sagemaker, HuggingFace, Replicate (100+ LLMs). |
| [API](/docs/api) | | Directly call the public API. OpenAPI spec available. |
External projects/packages that integrate with Langfuse:
Packages that integrate with Langfuse:
| Name | Description |
| --------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------ |
| [LiteLLM](https://langfuse.com/docs/integrations/litellm) | Use any LLM as a drop in replacement for GPT. Use Azure, OpenAI, Cohere, Anthropic, Ollama, VLLM, Sagemaker, HuggingFace, Replicate (100+ LLMs). |
| [Flowise](https://langfuse.com/docs/integrations/flowise) | JS/TS no-code builder for customized LLM flows. |
| [Langflow](https://langfuse.com/docs/integrations/langflow) | Python-based UI for LangChain, designed with react-flow to provide an effortless way to experiment and prototype flows. |
| [Superagent](https://langfuse.com/docs/integrations/superagent) | Open Source AI Assistant Framework & API for prototyping and deployment of agents. |
| Name | Description |
| ---------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------- |
| [Instructor](/docs/integrations/instructor) | Library to get structured LLM outputs (JSON, Pydantic) |
| [Mirascope](/docs/integrations/mirascope) | Python toolkit for building LLM applications. |
| [AI SDK by Vercel](/docs/sdk/typescript/example-vercel-ai) | Typescript SDK that makes streaming LLM outputs super easy. |
| [Flowise](/docs/integrations/flowise) | JS/TS no-code builder for customized LLM flows. |
| [Langflow](/docs/integrations/langflow) | Python-based UI for LangChain, designed with react-flow to provide an effortless way to experiment and prototype flows. |
| [Superagent](/docs/integrations/superagent) | Open Source AI Assistant Framework & API for prototyping and deployment of agents. |
## Questions and feedback
+3 -3
View File
@@ -3,9 +3,9 @@ Join us in scaling Langfuse in Berlin, Germany. We are an open source company, w
_Open Roles_
- Backend Engineer, 70-110k EUR, 0.25-0.75% Equity, https://www.ycombinator.com/companies/langfuse/jobs/mnrdwla-backend-engineer
- Product Engineer, 70-110k EUR, 0.25-0.75% Equity, https://www.ycombinator.com/companies/langfuse/jobs/aAvmoFB-product-engineer
- Developer Advocate, 60-100k EUR, 0.25-0.5% Equity, https://www.ycombinator.com/companies/langfuse/jobs/uHysbKH-developer-advocate-devrel
- Backend Engineer, 70-130k EUR, 0.25-0.75% Equity, https://www.ycombinator.com/companies/langfuse/jobs/mnrdwla-backend-engineer
- Product Engineer, 70-130k EUR, 0.25-0.75% Equity, https://www.ycombinator.com/companies/langfuse/jobs/aAvmoFB-product-engineer
- Developer Advocate, 60-110k EUR, 0.25-0.5% Equity, https://www.ycombinator.com/companies/langfuse/jobs/uHysbKH-developer-advocate-devrel
_More Info_
+1 -1
View File
@@ -33,7 +33,7 @@
"@repo/eslint-config": "*",
"@repo/typescript-config": "*",
"@types/node": "^20.11.29",
"@typescript-eslint/parser": "^7.7.0",
"@typescript-eslint/parser": "^7.12.0",
"eslint": "^8.57.0",
"eslint-config-prettier": "^9.1.0",
"eslint-config-standard": "^17.1.0",
+31 -2
View File
@@ -2,10 +2,11 @@ import { type Provider } from "next-auth/providers/index";
import GoogleProvider from "next-auth/providers/google";
import GitHubProvider from "next-auth/providers/github";
import OktaProvider from "next-auth/providers/okta";
import CognitoProvider from "next-auth/providers/cognito";
import Auth0Provider from "next-auth/providers/auth0";
import AzureADProvider from "next-auth/providers/azure-ad";
import { isEeAvailable } from "..";
import { prisma } from "@langfuse/shared/src/db";
import { type SsoConfig, prisma } from "@langfuse/shared/src/db";
import { encrypt, decrypt } from "@langfuse/shared/encryption";
import { SsoProviderSchema } from "./types";
import { type NextApiRequest, type NextApiResponse } from "next";
@@ -25,14 +26,36 @@ let cachedSsoConfigs: {
async function getSsoConfigs(): Promise<SsoProviderSchema[]> {
if (!isEeAvailable) return [];
const CACHE_TTL = 60 * 1000; // 1 minute
const DB_MAX_WAIT = 2000; // 2 seconds
const DB_TIMEOUT = 3000; // 3 seconds
// Set/refresh the cache if it's empty or expired
if (
cachedSsoConfigs === null ||
Date.now() - cachedSsoConfigs.timestamp > CACHE_TTL
) {
// findMany with custom timeout via $transaction
let dbConfigs: SsoConfig[] = [];
try {
dbConfigs = await prisma.$transaction(
async (prisma) => prisma.ssoConfig.findMany(),
{
maxWait: DB_MAX_WAIT,
timeout: DB_TIMEOUT,
}
);
} catch (e) {
// cache empty array to prevent repeated DB calls on error
cachedSsoConfigs = {
data: [],
timestamp: Date.now(),
};
// caught and logged in the caller
throw e;
}
// transform into zod object
const dbConfigs = await prisma.ssoConfig.findMany();
const parsedSsoConfigs = dbConfigs
.map((v) => {
try {
@@ -144,6 +167,12 @@ const dbToNextAuthProvider = (provider: SsoProviderSchema): Provider | null => {
...provider.authConfig,
clientSecret: decrypt(provider.authConfig.clientSecret),
});
else if (provider.authProvider === "cognito")
return CognitoProvider({
id: getAuthProviderIdForSsoConfig(provider), // use the domain as the provider id as we use domain-specific credentials
...provider.authConfig,
clientSecret: decrypt(provider.authConfig.clientSecret),
});
else {
// Type check to ensure we handle all providers
// eslint-disable-next-line no-unused-vars
+14
View File
@@ -64,11 +64,24 @@ export const AzureAdProviderSchema = base.extend({
.nullish(),
});
export const CognitoProviderSchema = base.extend({
authProvider: z.literal("cognito"),
authConfig: z
.object({
clientId: z.string(),
clientSecret: z.string(),
issuer: z.string(),
allowDangerousEmailAccountLinking: z.boolean().optional().default(false),
})
.nullish(),
});
export type GoogleProviderSchema = z.infer<typeof GoogleProviderSchema>;
export type GithubProviderSchema = z.infer<typeof GithubProviderSchema>;
export type Auth0ProviderSchema = z.infer<typeof Auth0ProviderSchema>;
export type OktaProviderSchema = z.infer<typeof OktaProviderSchema>;
export type AzureAdProviderSchema = z.infer<typeof AzureAdProviderSchema>;
export type CognitoProviderSchema = z.infer<typeof CognitoProviderSchema>;
export const SsoProviderSchema = z.discriminatedUnion("authProvider", [
GoogleProviderSchema,
@@ -76,6 +89,7 @@ export const SsoProviderSchema = z.discriminatedUnion("authProvider", [
Auth0ProviderSchema,
OktaProviderSchema,
AzureAdProviderSchema,
CognitoProviderSchema,
]);
export type SsoProviderSchema = z.infer<typeof SsoProviderSchema>;
+1 -1
View File
@@ -209,8 +209,8 @@ types:
- ARCHIVED
ScoreSource:
enum:
- ANNOTATION
- API
- REVIEW
- EVAL
errors:
+17 -4
View File
@@ -48,12 +48,25 @@ types:
date: date
countTraces: integer
countObservations: integer
totalCost: double
totalCost:
type: double
docs: Total model cost in USD
usage: list<UsageByModel>
UsageByModel:
docs: Daily usage of a given model. Usage corresponds to the unit set for the specific model (e.g. tokens).
properties:
model: optional<string>
inputUsage: integer
outputUsage: integer
totalUsage: integer
inputUsage:
type: integer
docs: Total number of generation input units (e.g. tokens)
outputUsage:
type: integer
docs: Total number of generation output units (e.g. tokens)
totalUsage:
type: integer
docs: Total number of generation total units (e.g. tokens)
countTraces: integer
countObservations: integer
totalCost:
type: double
docs: Total model cost in USD
+18 -3
View File
@@ -70,14 +70,24 @@ types:
name: string
prompt: list<ChatMessage>
config: optional<unknown>
labels: optional<list<string>>
labels:
type: optional<list<string>>
docs: List of deployment labels of this prompt version.
tags:
type: optional<list<string>>
docs: List of tags to apply to all versions of this prompt.
CreateTextPromptRequest:
properties:
name: string
prompt: string
config: optional<unknown>
labels: optional<list<string>>
labels:
type: optional<list<string>>
docs: List of deployment labels of this prompt version.
tags:
type: optional<list<string>>
docs: List of tags to apply to all versions of this prompt.
Prompt:
union:
@@ -89,7 +99,12 @@ types:
name: string
version: integer
config: unknown
labels: list<string>
labels:
type: list<string>
docs: List of deployment labels of this prompt version.
tags:
type: list<string>
docs: List of tags. Used to filter via UI and API. The same across versions of a prompt.
ChatMessage:
properties:
+1 -1
View File
@@ -7,7 +7,7 @@ service:
base-path: /api/public
endpoints:
get:
docs: Get a session
docs: Get a session. Please note that `traces` on this endpoint are not paginated, if you plan to fetch large sessions, consider `GET /api/public/traces?sessionId=<sessionId>`
method: GET
path: /sessions/{sessionId}
path-parameters:
+1
View File
@@ -30,6 +30,7 @@ service:
docs: Limit of items per page. If you encounter api issues due to too large page sizes, try to reduce the limit.
userId: optional<string>
name: optional<string>
sessionId: optional<string>
fromTimestamp:
type: optional<datetime>
docs: Retrieve only traces newer than this datetime (ISO 8601).
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "langfuse",
"version": "2.45.0",
"version": "2.49.0",
"author": "engineering@langfuse.com",
"license": "MIT",
"private": true,
+1 -1
View File
@@ -9,7 +9,7 @@
],
"devDependencies": {
"@typescript-eslint/eslint-plugin": "^7.1.0",
"@typescript-eslint/parser": "^7.7.0",
"@typescript-eslint/parser": "^7.12.0",
"@vercel/style-guide": "^6.0.0",
"eslint-config-next": "^14.2.3",
"eslint-config-prettier": "^9.1.0",
+1 -1
View File
@@ -63,7 +63,7 @@
"@types/node": "^20.11.29",
"@types/pg": "^8.11.6",
"@types/uuid": "^9.0.8",
"@typescript-eslint/parser": "^7.7.0",
"@typescript-eslint/parser": "^7.12.0",
"eslint": "^8.57.0",
"eslint-config-prettier": "^9.1.0",
"eslint-config-standard": "^17.1.0",
+26 -20
View File
@@ -25,22 +25,17 @@ export const ObservationLevel = {
} as const;
export type ObservationLevel = (typeof ObservationLevel)[keyof typeof ObservationLevel];
export const ScoreSource = {
ANNOTATION: "ANNOTATION",
API: "API",
REVIEW: "REVIEW",
EVAL: "EVAL"
} as const;
export type ScoreSource = (typeof ScoreSource)[keyof typeof ScoreSource];
export const PricingUnit = {
PER_1000_TOKENS: "PER_1000_TOKENS",
PER_1000_CHARS: "PER_1000_CHARS"
export const ScoreDataType = {
CATEGORICAL: "CATEGORICAL",
NUMERIC: "NUMERIC",
BOOLEAN: "BOOLEAN"
} as const;
export type PricingUnit = (typeof PricingUnit)[keyof typeof PricingUnit];
export const TokenType = {
PROMPT: "PROMPT",
COMPLETION: "COMPLETION",
TOTAL: "TOTAL"
} as const;
export type TokenType = (typeof TokenType)[keyof typeof TokenType];
export type ScoreDataType = (typeof ScoreDataType)[keyof typeof ScoreDataType];
export const DatasetStatus = {
ACTIVE: "ACTIVE",
ARCHIVED: "ARCHIVED"
@@ -246,6 +241,7 @@ export type Observation = {
status_message: string | null;
version: string | null;
created_at: Generated<Timestamp>;
updated_at: Generated<Timestamp>;
model: string | null;
internal_model: string | null;
modelParameters: unknown | null;
@@ -303,14 +299,6 @@ export type PosthogIntegration = {
enabled: boolean;
created_at: Generated<Timestamp>;
};
export type Pricing = {
id: string;
model_name: string;
pricing_unit: Generated<PricingUnit>;
price: string;
currency: Generated<string>;
token_type: TokenType;
};
export type Project = {
id: string;
created_at: Generated<Timestamp>;
@@ -351,6 +339,24 @@ export type Score = {
comment: string | null;
trace_id: string;
observation_id: string | null;
config_id: string | null;
string_value: string | null;
created_at: Generated<Timestamp>;
updated_at: Generated<Timestamp>;
data_type: Generated<ScoreDataType>;
};
export type ScoreConfig = {
id: string;
created_at: Generated<Timestamp>;
updated_at: Generated<Timestamp>;
project_id: string;
name: string;
data_type: ScoreDataType;
is_archived: Generated<boolean>;
min_value: number | null;
max_value: number | null;
categories: unknown | null;
description: string | null;
};
export type Session = {
id: string;
@@ -446,10 +452,10 @@ export type DB = {
observations: Observation;
observations_view: ObservationView;
posthog_integrations: PosthogIntegration;
pricings: Pricing;
project_memberships: ProjectMembership;
projects: Project;
prompts: Prompt;
score_configs: ScoreConfig;
scores: Score;
Session: Session;
sso_configs: SsoConfig;
@@ -0,0 +1,42 @@
-- CreateEnum
CREATE TYPE "ScoreDataType" AS ENUM ('CATEGORICAL', 'NUMERIC');
-- AlterTable
ALTER TABLE "scores" ADD COLUMN "config_id" TEXT,
ADD COLUMN "data_type" "ScoreDataType" NOT NULL DEFAULT 'NUMERIC',
ADD COLUMN "string_value" TEXT;
-- CreateTable
CREATE TABLE "score_configs" (
"id" TEXT NOT NULL,
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updated_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"project_id" TEXT NOT NULL,
"name" TEXT NOT NULL,
"data_type" "ScoreDataType" NOT NULL,
"is_archived" BOOLEAN NOT NULL DEFAULT false,
"min_value" DOUBLE PRECISION,
"max_value" DOUBLE PRECISION,
"categories" JSONB,
"description" TEXT,
CONSTRAINT "score_configs_pkey" PRIMARY KEY ("id")
);
-- CreateIndex
CREATE INDEX "score_configs_data_type_idx" ON "score_configs"("data_type");
-- CreateIndex
CREATE INDEX "score_configs_is_archived_idx" ON "score_configs"("is_archived");
-- CreateIndex
CREATE INDEX "score_configs_project_id_idx" ON "score_configs"("project_id");
-- CreateIndex
CREATE INDEX "score_configs_categories_idx" ON "score_configs"("categories");
-- CreateIndex
CREATE UNIQUE INDEX "score_configs_id_project_id_key" ON "score_configs"("id", "project_id");
-- AddForeignKey
ALTER TABLE "score_configs" ADD CONSTRAINT "score_configs_project_id_fkey" FOREIGN KEY ("project_id") REFERENCES "projects"("id") ON DELETE CASCADE ON UPDATE CASCADE;
@@ -0,0 +1,2 @@
-- CreateIndex
CREATE INDEX CONCURRENTLY "scores_config_id_idx" ON "scores"("config_id");
@@ -0,0 +1,2 @@
-- AddForeignKey
ALTER TABLE "scores" ADD CONSTRAINT "scores_config_id_fkey" FOREIGN KEY ("config_id") REFERENCES "score_configs"("id") ON DELETE SET NULL ON UPDATE CASCADE;
@@ -0,0 +1,2 @@
-- AlterEnum
ALTER TYPE "ScoreSource" ADD VALUE 'ANNOTATION';
@@ -0,0 +1,4 @@
-- Backfill the scores source for 'REVIEW' to be 'ANNOTATION'
UPDATE "scores"
SET "source" = 'ANNOTATION'::"ScoreSource"
WHERE "source" = 'REVIEW'::"ScoreSource";
@@ -0,0 +1,14 @@
/*
Warnings:
- The values [REVIEW] on the enum `ScoreSource` will be removed. If these variants are still used in the database, this will fail.
*/
-- AlterEnum
BEGIN;
CREATE TYPE "ScoreSource_new" AS ENUM ('ANNOTATION', 'API', 'EVAL');
ALTER TABLE "scores" ALTER COLUMN "source" TYPE "ScoreSource_new" USING ("source"::text::"ScoreSource_new");
ALTER TYPE "ScoreSource" RENAME TO "ScoreSource_old";
ALTER TYPE "ScoreSource_new" RENAME TO "ScoreSource";
DROP TYPE "ScoreSource_old";
COMMIT;
@@ -0,0 +1,2 @@
-- CreateIndex
CREATE INDEX CONCURRENTLY "job_executions_job_configuration_id_idx" ON "job_executions"("job_configuration_id");
@@ -0,0 +1,2 @@
-- CreateIndex
CREATE INDEX CONCURRENTLY "job_executions_job_output_score_id_idx" ON "job_executions"("job_output_score_id");
@@ -0,0 +1,2 @@
-- CreateIndex
CREATE INDEX CONCURRENTLY "job_executions_job_input_trace_id_idx" ON "job_executions"("job_input_trace_id");
@@ -0,0 +1,2 @@
-- CreateIndex
CREATE INDEX CONCURRENTLY "job_executions_created_at_idx" ON "job_executions"("created_at");
@@ -0,0 +1,2 @@
-- AlterTable
ALTER TABLE "observations" ADD COLUMN "updated_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP;
@@ -0,0 +1,4 @@
-- AlterTable
ALTER TABLE "scores" ADD COLUMN "created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
ADD COLUMN "updated_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP;
@@ -0,0 +1,2 @@
-- CreateIndex
CREATE INDEX CONCURRENTLY "audit_logs_updated_at_idx" ON "audit_logs"("updated_at");
@@ -0,0 +1,3 @@
-- CreateIndex
CREATE INDEX CONCURRENTLY "dataset_items_created_at_idx" ON "dataset_items"("created_at");
@@ -0,0 +1,3 @@
-- CreateIndex
CREATE INDEX CONCURRENTLY "dataset_items_updated_at_idx" ON "dataset_items"("updated_at");
@@ -0,0 +1,3 @@
-- CreateIndex
CREATE INDEX CONCURRENTLY "dataset_run_items_created_at_idx" ON "dataset_run_items"("created_at");
@@ -0,0 +1,3 @@
-- CreateIndex
CREATE INDEX CONCURRENTLY "dataset_run_items_updated_at_idx" ON "dataset_run_items"("updated_at");
@@ -0,0 +1,3 @@
-- CreateIndex
CREATE INDEX CONCURRENTLY "dataset_runs_created_at_idx" ON "dataset_runs"("created_at");
@@ -0,0 +1,3 @@
-- CreateIndex
CREATE INDEX CONCURRENTLY "dataset_runs_updated_at_idx" ON "dataset_runs"("updated_at");
@@ -0,0 +1,3 @@
-- CreateIndex
CREATE INDEX CONCURRENTLY "datasets_created_at_idx" ON "datasets"("created_at");
@@ -0,0 +1,3 @@
-- CreateIndex
CREATE INDEX CONCURRENTLY "datasets_updated_at_idx" ON "datasets"("updated_at");
@@ -0,0 +1,3 @@
-- CreateIndex
CREATE INDEX CONCURRENTLY "job_executions_updated_at_idx" ON "job_executions"("updated_at");
@@ -0,0 +1,3 @@
-- CreateIndex
CREATE INDEX CONCURRENTLY "observations_updated_at_idx" ON "observations"("updated_at");
@@ -0,0 +1,3 @@
-- CreateIndex
CREATE INDEX CONCURRENTLY "prompts_created_at_idx" ON "prompts"("created_at");
@@ -0,0 +1,3 @@
-- CreateIndex
CREATE INDEX CONCURRENTLY "prompts_updated_at_idx" ON "prompts"("updated_at");
@@ -0,0 +1,3 @@
-- CreateIndex
CREATE INDEX CONCURRENTLY "score_configs_created_at_idx" ON "score_configs"("created_at");
@@ -0,0 +1,3 @@
-- CreateIndex
CREATE INDEX CONCURRENTLY "score_configs_updated_at_idx" ON "score_configs"("updated_at");
@@ -0,0 +1,3 @@
-- CreateIndex
CREATE INDEX CONCURRENTLY "scores_created_at_idx" ON "scores"("created_at");
@@ -0,0 +1,3 @@
-- CreateIndex
CREATE INDEX CONCURRENTLY "scores_updated_at_idx" ON "scores"("updated_at");
@@ -0,0 +1,3 @@
-- CreateIndex
CREATE INDEX CONCURRENTLY "trace_sessions_updated_at_idx" ON "trace_sessions"("updated_at");
@@ -0,0 +1,7 @@
-- Google Vertex uses @ to separate model name and version
UPDATE "public"."models" SET "match_pattern" = '(?i)^(claude-3-haiku(-|@)?20240307)$' WHERE "id" = 'cltr0w45b000008k1407o9qv1';
UPDATE "public"."models" SET "match_pattern" = '(?i)^(claude-3-opus(-|@)?20240229)$' WHERE "id" = 'cltgy0iuw000008le3vod1hhy';
UPDATE "public"."models" SET "match_pattern" = '(?i)^(claude-3-sonnet(-|@)?20240229)$' WHERE "id" = 'cltgy0pp6000108le56se7bl3';
@@ -0,0 +1,2 @@
-- CreateIndex
CREATE INDEX CONCURRENTLY "dataset_items_source_trace_id_idx" ON "dataset_items" USING HASH ("source_trace_id");
@@ -0,0 +1,2 @@
-- CreateIndex
CREATE INDEX "scores_project_id_name_idx" ON "scores"("project_id", "name");
@@ -0,0 +1,2 @@
-- AlterEnum
ALTER TYPE "ScoreDataType" ADD VALUE 'BOOLEAN';
@@ -0,0 +1,50 @@
-- Create manual-score config for any project with manual scores and link config_id to scores
BEGIN;
WITH project_configs AS (
INSERT INTO score_configs (id,
project_id,
name,
data_type,
is_archived,
min_value,
max_value,
description)
SELECT
gen_random_uuid () AS id,
s.project_id,
'manual-score',
'NUMERIC',
FALSE,
- 1,
1,
'Langfuse legacy annotation score.'
FROM ( SELECT DISTINCT
project_id
FROM
scores
WHERE
name = 'manual-score'
AND source = 'ANNOTATION') s
WHERE
NOT EXISTS (
SELECT
1
FROM
score_configs sc
WHERE
sc.name = 'manual-score'
AND sc.project_id = s.project_id)
RETURNING
id,
project_id
)
UPDATE
scores
SET
config_id = pc.id
FROM
project_configs pc
WHERE
scores.project_id = pc.project_id
AND scores.name = 'manual-score';
COMMIT;
@@ -0,0 +1,5 @@
-- does not include pricing yet, will be added as soon as it is calculated at ingestion time
INSERT INTO "public"."models" ("id", "model_name", "match_pattern", "unit") VALUES ('clx30djsn0000w9mzebiv41we', 'gemini-1.5-flash', '(?i)^(gemini-1.5-flash)(@[a-zA-Z0-9]+)?$', 'CHARACTERS');
INSERT INTO "public"."models" ("id", "model_name", "match_pattern", "unit") VALUES ('clx30hkrx0000w9mz7lqi0ial', 'gemini-1.5-pro', '(?i)^(gemini-1.5-pro)(@[a-zA-Z0-9]+)?$', 'CHARACTERS');
@@ -0,0 +1,14 @@
/*
Warnings:
- You are about to drop the `pricings` table. If the table is not empty, all the data it contains will be lost.
*/
-- DropTable
DROP TABLE "pricings";
-- DropEnum
DROP TYPE "PricingUnit";
-- DropEnum
DROP TYPE "TokenType";
@@ -0,0 +1,3 @@
-- DropForeignKey
ALTER TABLE "dataset_run_items" DROP CONSTRAINT "dataset_run_items_trace_id_fkey";
ALTER TABLE "dataset_run_items" DROP CONSTRAINT "dataset_run_items_observation_id_fkey";
+79 -44
View File
@@ -114,6 +114,7 @@ model Project {
LlmApiKeys LlmApiKeys[]
PosthogIntegration PosthogIntegration[]
Score Score[]
scoreConfig ScoreConfig[]
@@map("projects")
}
@@ -205,6 +206,7 @@ model TraceSession {
@@id([id, projectId])
@@index([projectId])
@@index([createdAt])
@@index([updatedAt])
@@map("trace_sessions")
}
@@ -231,9 +233,8 @@ model Trace {
createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime @default(now()) @updatedAt @map("updated_at")
DatasetRunItems DatasetRunItems[]
DatasetItem DatasetItem[]
JobExecution JobExecution[]
DatasetItem DatasetItem[]
JobExecution JobExecution[]
@@index([projectId])
@@index([sessionId])
@@ -292,24 +293,24 @@ model Observation {
statusMessage String? @map("status_message")
version String?
createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime @default(now()) @updatedAt @map("updated_at")
// GENERATION ONLY
model String?
internalModel String? @map("internal_model")
internalModel String? @map("internal_model")
modelParameters Json?
input Json?
output Json?
promptTokens Int @default(0) @map("prompt_tokens")
completionTokens Int @default(0) @map("completion_tokens")
totalTokens Int @default(0) @map("total_tokens")
promptTokens Int @default(0) @map("prompt_tokens")
completionTokens Int @default(0) @map("completion_tokens")
totalTokens Int @default(0) @map("total_tokens")
unit String?
inputCost Decimal? @map("input_cost")
outputCost Decimal? @map("output_cost")
totalCost Decimal? @map("total_cost")
completionStartTime DateTime? @map("completion_start_time")
project Project @relation(fields: [projectId], references: [id], onDelete: Cascade)
inputCost Decimal? @map("input_cost")
outputCost Decimal? @map("output_cost")
totalCost Decimal? @map("total_cost")
completionStartTime DateTime? @map("completion_start_time")
project Project @relation(fields: [projectId], references: [id], onDelete: Cascade)
derivedDatasetItems DatasetItem[]
datasetRunItems DatasetRunItems[]
promptId String? @map("prompt_id")
prompt Prompt? @relation(fields: [promptId], onDelete: SetNull, references: [id])
@@ -323,6 +324,7 @@ model Observation {
@@index([type])
@@index(startTime)
@@index(createdAt)
@@index(updatedAt)
@@index(projectId)
@@index(parentObservationId)
@@index(model)
@@ -403,46 +405,64 @@ model Score {
comment String?
traceId String @map("trace_id")
observationId String? @map("observation_id")
configId String? @map("config_id")
stringValue String? @map("string_value")
createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime @default(now()) @updatedAt @map("updated_at")
dataType ScoreDataType @default(NUMERIC) @map("data_type")
JobExecution JobExecution[]
scoreConfig ScoreConfig? @relation(fields: [configId], references: [id], onDelete: SetNull)
@@unique([id, projectId]) // used for upserts via prisma
@@index(timestamp)
@@index([value])
@@index([projectId])
@@index([projectId, name])
@@index([authorUserId])
@@index([configId])
@@index([traceId], type: Hash)
@@index([observationId], type: Hash)
@@index([source])
@@index([createdAt])
@@index([updatedAt])
@@map("scores")
}
enum ScoreSource {
ANNOTATION
API
REVIEW
EVAL
}
enum PricingUnit {
PER_1000_TOKENS
PER_1000_CHARS
model ScoreConfig {
id String @id @default(cuid())
createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime @default(now()) @updatedAt @map("updated_at")
projectId String @map("project_id")
project Project @relation(fields: [projectId], references: [id], onDelete: Cascade)
name String
dataType ScoreDataType @map("data_type")
isArchived Boolean @default(false) @map("is_archived")
minValue Float? @map("min_value")
maxValue Float? @map("max_value")
categories Json? @map("categories")
description String?
score Score[]
@@unique([id, projectId]) // used for upserts via prisma
@@index([dataType])
@@index([isArchived])
@@index([projectId])
@@index([categories])
@@index([createdAt])
@@index([updatedAt])
@@map("score_configs")
}
enum TokenType {
PROMPT
COMPLETION
TOTAL
}
model Pricing {
id String @id @default(cuid())
modelName String @map("model_name")
pricingUnit PricingUnit @default(PER_1000_TOKENS) @map("pricing_unit")
price Decimal
currency String @default("USD")
tokenType TokenType @map("token_type")
@@index(modelName)
@@map("pricings")
enum ScoreDataType {
CATEGORICAL
NUMERIC
BOOLEAN
}
model CronJobs {
@@ -468,6 +488,8 @@ model Dataset {
@@unique([projectId, name])
@@index([projectId], type: Hash)
@@index([createdAt])
@@index([updatedAt])
@@map("datasets")
}
@@ -487,8 +509,11 @@ model DatasetItem {
updatedAt DateTime @default(now()) @updatedAt @map("updated_at")
datasetRunItems DatasetRunItems[]
@@index([sourceTraceId], type: Hash)
@@index([sourceObservationId], type: Hash)
@@index([datasetId], type: Hash)
@@index([createdAt])
@@index([updatedAt])
@@map("dataset_items")
}
@@ -510,26 +535,28 @@ model DatasetRuns {
@@unique([datasetId, name])
@@index([datasetId], type: Hash)
@@index([createdAt])
@@index([updatedAt])
@@map("dataset_runs")
}
model DatasetRunItems {
id String @id @default(cuid())
datasetRunId String @map("dataset_run_id")
datasetRun DatasetRuns @relation(fields: [datasetRunId], references: [id], onDelete: Cascade)
datasetItemId String @map("dataset_item_id")
datasetItem DatasetItem @relation(fields: [datasetItemId], references: [id], onDelete: Cascade)
traceId String @map("trace_id")
trace Trace @relation(fields: [traceId], references: [id], onDelete: Cascade)
observationId String? @map("observation_id")
observation Observation? @relation(fields: [observationId], references: [id], onDelete: Cascade)
createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime @default(now()) @updatedAt @map("updated_at")
id String @id @default(cuid())
datasetRunId String @map("dataset_run_id")
datasetRun DatasetRuns @relation(fields: [datasetRunId], references: [id], onDelete: Cascade)
datasetItemId String @map("dataset_item_id")
datasetItem DatasetItem @relation(fields: [datasetItemId], references: [id], onDelete: Cascade)
traceId String @map("trace_id")
observationId String? @map("observation_id")
createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime @default(now()) @updatedAt @map("updated_at")
@@index([datasetRunId], type: Hash)
@@index([datasetItemId], type: Hash)
@@index([observationId], type: Hash)
@@index([traceId])
@@index([createdAt])
@@index([updatedAt])
@@map("dataset_run_items")
}
@@ -572,6 +599,8 @@ model Prompt {
@@index([projectId, name, version])
@@index([projectId, id])
@@index([projectId])
@@index([createdAt])
@@index([updatedAt])
@@index([tags(ops: ArrayOps)], type: Gin)
@@map("prompts")
}
@@ -619,6 +648,7 @@ model AuditLog {
@@index([projectId])
@@index([createdAt])
@@index([updatedAt])
@@map("audit_logs")
}
@@ -712,6 +742,11 @@ model JobExecution {
@@index([projectId, status])
@@index([projectId, id])
@@index([projectId])
@@index([jobConfigurationId])
@@index([jobOutputScoreId])
@@index([jobInputTraceId])
@@index([createdAt])
@@index([updatedAt])
@@map("job_executions")
}
+123 -4
View File
@@ -4,6 +4,7 @@ import {
type Prisma,
ObservationType,
ScoreSource,
ScoreDataType,
} from "../src/index";
import { hash } from "bcryptjs";
import { parseArgs } from "node:util";
@@ -12,6 +13,7 @@ import { chunk } from "lodash";
import { v4 } from "uuid";
import { ModelUsageUnit } from "../src";
import { getDisplaySecretKey, hashSecretKey } from "../src/server/auth";
import { encrypt } from "../src/encryption";
const LOAD_TRACE_VOLUME = 10_000;
@@ -38,6 +40,7 @@ async function main() {
name: "Demo User",
email: "demo@langfuse.com",
password: await hash("password", 12),
image: "https://static.langfuse.com/langfuse-dev%2Fexample-avatar.png",
},
});
@@ -143,6 +146,11 @@ async function main() {
});
}
const configIdsAndNames = await generateConfigsForProject([
project1,
project2,
]);
const promptIds = await generatePromptsForProject([project1, project2]);
const envTags = [null, "development", "staging", "production"];
@@ -156,7 +164,8 @@ async function main() {
colorTags,
project1,
project2,
promptIds
promptIds,
configIdsAndNames
);
console.log(
@@ -165,6 +174,24 @@ async function main() {
await uploadObjects(traces, observations, scores, sessions, events);
// If openai key is in environment, add it to the projects LLM API keys
const OPENAI_API_KEY = process.env.OPENAI_API_KEY;
if (OPENAI_API_KEY) {
await prisma.llmApiKeys.create({
data: {
projectId: project1.id,
secretKey: encrypt(OPENAI_API_KEY),
displaySecretKey: getDisplaySecretKey(OPENAI_API_KEY),
provider: "openai",
},
});
} else {
console.warn(
"No OPENAI_API_KEY found in environment. Skipping seeding LLM API key."
);
}
// add eval objects
const evalTemplate = await prisma.evalTemplate.upsert({
where: {
@@ -434,13 +461,15 @@ function createObjects(
colorTags: (string | null)[],
project1: Project,
project2: Project,
promptIds: Map<string, string[]>
promptIds: Map<string, string[]>,
configIdsAndNames: Map<string, { name: string; id: string }[]>
) {
const traces: Prisma.TraceCreateManyInput[] = [];
const observations: Prisma.ObservationCreateManyInput[] = [];
const scores: Prisma.ScoreCreateManyInput[] = [];
const sessions: Prisma.TraceSessionCreateManyInput[] = [];
const events: Prisma.ObservationCreateManyInput[] = [];
const configs: Prisma.ScoreConfigCreateManyInput[] = [];
for (let i = 0; i < traceVolume; i++) {
// print progress to console with a progress bar that refreshes every 10 iterations
@@ -492,17 +521,28 @@ function createObjects(
traces.push(trace);
const configArray = configIdsAndNames.get(projectId) ?? [];
const randomIndex = Math.floor(Math.random() * 3);
const config =
configArray.length >= randomIndex - 1 && configArray[randomIndex];
const { name: annotationScoreName, id: configId } = config || {
name: "manual-score",
id: undefined,
};
const traceScores = [
...(Math.random() > 0.5
? [
{
traceId: trace.id,
name: "manual-score",
name: annotationScoreName,
value: Math.floor(Math.random() * 3) - 1,
timestamp: traceTs,
source: ScoreSource.REVIEW,
source: ScoreSource.ANNOTATION,
projectId,
authorUserId: `user-${i}`,
dataType: ScoreDataType.NUMERIC,
...(configId ? { configId } : {}),
},
]
: []),
@@ -515,6 +555,7 @@ function createObjects(
timestamp: traceTs,
source: ScoreSource.API,
projectId,
dataType: ScoreDataType.NUMERIC,
},
]
: []),
@@ -753,6 +794,7 @@ function createObjects(
traces,
observations,
scores,
configs,
sessions: uniqueSessions,
events,
};
@@ -935,3 +977,80 @@ async function generatePrompts(project: Project) {
}
return promptIds;
}
async function generateConfigsForProject(projects: Project[]) {
const projectIdsToConfigs: Map<string, { name: string; id: string }[]> =
new Map();
await Promise.all(
projects.map(async (project) => {
const configNameAndId = await generateConfigs(project);
projectIdsToConfigs.set(project.id, configNameAndId);
})
);
return projectIdsToConfigs;
}
async function generateConfigs(project: Project) {
const configNameAndId: { name: string; id: string }[] = [];
const configs = [
{
id: `config-${v4()}`,
name: "manual-score",
dataType: ScoreDataType.NUMERIC,
projectId: project.id,
isArchived: false,
},
{
id: `config-${v4()}`,
projectId: project.id,
name: "Accuracy",
dataType: ScoreDataType.CATEGORICAL,
categories: [
{ label: "Incorrect", value: 0 },
{ label: "Partially Correct", value: 1 },
{ label: "Correct", value: 2 },
],
isArchived: false,
},
{
id: `config-${v4()}`,
projectId: project.id,
name: "Toxicity",
dataType: ScoreDataType.BOOLEAN,
categories: [
{ label: "True", value: 1 },
{ label: "False", value: 0 },
],
description:
"Used to indicate if text was harmful or offensive in nature.",
isArchived: false,
},
];
for (const config of configs) {
await prisma.scoreConfig.upsert({
where: {
id_projectId: {
projectId: config.projectId,
id: config.id,
},
},
create: {
id: config.id,
projectId: config.projectId,
name: config.name,
dataType: config.dataType,
categories: config.categories,
isArchived: config.isArchived,
},
update: {
id: config.id,
},
});
configNameAndId.push({ name: config.name, id: config.id });
}
return configNameAndId;
}
+4 -4
View File
@@ -1,7 +1,7 @@
import { Prisma } from "@prisma/client";
import {
ColumnDefinition,
type TableNames as TableName,
type TableNames,
} from "./interfaces/tableDefinition";
import { FilterState } from "./types";
import { filterOperators } from "./interfaces/filters";
@@ -27,7 +27,7 @@ const arrayOperatorReplacements = {
export function tableColumnsToSqlFilterAndPrefix(
filters: FilterState,
tableColumns: ColumnDefinition[],
table: TableName
table: TableNames
): Prisma.Sql {
const sql = tableColumnsToSqlFilter(filters, tableColumns, table);
if (sql === Prisma.empty) {
@@ -43,7 +43,7 @@ export function tableColumnsToSqlFilterAndPrefix(
export function tableColumnsToSqlFilter(
filters: FilterState,
tableColumns: ColumnDefinition[],
table: TableName
table: TableNames
): Prisma.Sql {
const internalFilters = filters.map((filter) => {
// Get column definition to map column to internal name, e.g. "t.id"
@@ -153,7 +153,7 @@ export function tableColumnsToSqlFilter(
const castValueToPostgresTypes = (
column: ColumnDefinition,
table: TableName
table: TableNames
) => {
return column.name === "type" &&
(table === "observations" ||
@@ -42,6 +42,7 @@ export const tableNames = [
"traces_parent_observation_scores",
"sessions",
"prompts",
"users",
] as const;
export type TableNames = (typeof tableNames)[number];
+2 -1
View File
@@ -30,4 +30,5 @@ export type TableName =
| "sessions"
| "scores"
| "prompts"
| "dashboard";
| "dashboard"
| "users";
+362 -187
View File
File diff suppressed because it is too large Load Diff
+1 -1
View File
@@ -5,7 +5,7 @@ FROM node:20-alpine AS alpine
RUN apk update && apk upgrade --no-cache libcrypto3 libssl3 libc6-compat
FROM alpine AS base
RUN npm install turbo --global
RUN npm install turbo@^1.13.3 --global
ENV PNPM_HOME="/pnpm"
ENV PATH="$PNPM_HOME:$PATH"
RUN corepack enable
+6 -4
View File
@@ -1,6 +1,6 @@
{
"name": "web",
"version": "2.45.0",
"version": "2.49.0",
"private": true,
"license": "MIT",
"engines": {
@@ -25,6 +25,7 @@
"@aws-sdk/lib-storage": "^3.568.0",
"@aws-sdk/s3-request-presigner": "^3.554.0",
"@codemirror/lang-json": "^6.0.1",
"@codemirror/lint": "^6.8.0",
"@headlessui/react": "1.7.18",
"@headlessui/tailwindcss": "0.2.0",
"@heroicons/react": "^2.1.3",
@@ -55,6 +56,7 @@
"@radix-ui/react-switch": "^1.0.3",
"@radix-ui/react-tabs": "^1.0.4",
"@radix-ui/react-toggle": "^1.0.3",
"@radix-ui/react-toggle-group": "^1.0.4",
"@radix-ui/react-tooltip": "^1.0.7",
"@react-email/components": "^0.0.18",
"@react-email/render": "^0.0.14",
@@ -69,7 +71,6 @@
"@tailwindcss/container-queries": "^0.1.1",
"@tanstack/react-query": "^4.36.1",
"@tanstack/react-table": "^8.11.8",
"@tanstack/react-virtual": "^3.5.0",
"@tremor/react": "3.16.2",
"@trpc/client": "^10.45.0",
"@trpc/next": "^10.45.0",
@@ -106,7 +107,7 @@
"react-day-picker": "^8.10.1",
"react-dom": "18.2.0",
"react-hook-form": "^7.51.5",
"react-icons": "^5.0.1",
"react-icons": "^5.2.1",
"react-responsive": "^10.0.0",
"react18-json-view": "^0.2.8-canary.6",
"sonner": "^1.4.41",
@@ -115,6 +116,7 @@
"tailwindcss-animate": "^1.0.7",
"use-query-params": "^2.2.1",
"uuid": "^9.0.1",
"vaul": "^0.9.1",
"zod": "^3.22.4"
},
"devDependencies": {
@@ -136,7 +138,7 @@
"@types/react-dom": "^18.2.25",
"@types/uuid": "^9.0.8",
"@typescript-eslint/eslint-plugin": "^6.21.0",
"@typescript-eslint/parser": "^7.7.0",
"@typescript-eslint/parser": "^7.12.0",
"autoprefixer": "^10.4.19",
"dotenv-cli": "^7.4.2",
"eslint": "^8.56.0",
+49 -2
View File
@@ -1137,7 +1137,10 @@ paths:
security: *ref_0
/api/public/sessions/{sessionId}:
get:
description: Get a session
description: >-
Get a session. Please note that `traces` on this endpoint are not
paginated, if you plan to fetch large sessions, consider `GET
/api/public/traces?sessionId=<sessionId>`
operationId: sessions_get
tags:
- Sessions
@@ -1262,6 +1265,12 @@ paths:
schema:
type: string
nullable: true
- name: sessionId
in: query
required: false
schema:
type: string
nullable: true
- name: fromTimestamp
in: query
description: Retrieve only traces newer than this datetime (ISO 8601).
@@ -1825,8 +1834,8 @@ components:
title: ScoreSource
type: string
enum:
- ANNOTATION
- API
- REVIEW
- EVAL
CreateDatasetItemRequest:
title: CreateDatasetItemRequest
@@ -2499,6 +2508,7 @@ components:
totalCost:
type: number
format: double
description: Total model cost in USD
usage:
type: array
items:
@@ -2521,14 +2531,28 @@ components:
nullable: true
inputUsage:
type: integer
description: Total number of generation input units (e.g. tokens)
outputUsage:
type: integer
description: Total number of generation output units (e.g. tokens)
totalUsage:
type: integer
description: Total number of generation total units (e.g. tokens)
countTraces:
type: integer
countObservations:
type: integer
totalCost:
type: number
format: double
description: Total model cost in USD
required:
- inputUsage
- outputUsage
- totalUsage
- countTraces
- countObservations
- totalCost
Observations:
title: Observations
type: object
@@ -2654,6 +2678,13 @@ components:
items:
type: string
nullable: true
description: List of deployment labels of this prompt version.
tags:
type: array
items:
type: string
nullable: true
description: List of tags to apply to all versions of this prompt.
required:
- name
- prompt
@@ -2672,6 +2703,13 @@ components:
items:
type: string
nullable: true
description: List of deployment labels of this prompt version.
tags:
type: array
items:
type: string
nullable: true
description: List of tags to apply to all versions of this prompt.
required:
- name
- prompt
@@ -2713,11 +2751,20 @@ components:
type: array
items:
type: string
description: List of deployment labels of this prompt version.
tags:
type: array
items:
type: string
description: >-
List of tags. Used to filter via UI and API. The same across
versions of a prompt.
required:
- name
- version
- config
- labels
- tags
ChatMessage:
title: ChatMessage
type: object
+8 -3
View File
@@ -699,7 +699,7 @@
"auth": null,
"body": {
"mode": "raw",
"raw": "{\n \"type\": \"chat\",\n \"name\": \"example\",\n \"prompt\": [\n {\n \"role\": \"example\",\n \"content\": \"example\"\n }\n ],\n \"config\": \"UNKNOWN\",\n \"labels\": [\n \"example\"\n ]\n}",
"raw": "{\n \"type\": \"chat\",\n \"name\": \"example\",\n \"prompt\": [\n {\n \"role\": \"example\",\n \"content\": \"example\"\n }\n ],\n \"config\": \"UNKNOWN\",\n \"labels\": [\n \"example\"\n ],\n \"tags\": [\n \"example\"\n ]\n}",
"options": {
"raw": {
"language": "json"
@@ -890,7 +890,7 @@
"_type": "endpoint",
"name": "Get",
"request": {
"description": "Get a session",
"description": "Get a session. Please note that `traces` on this endpoint are not paginated, if you plan to fetch large sessions, consider `GET /api/public/traces?sessionId=<sessionId>`",
"url": {
"raw": "{{baseUrl}}/api/public/sessions/:sessionId",
"host": [
@@ -963,7 +963,7 @@
"request": {
"description": "Get list of traces.",
"url": {
"raw": "{{baseUrl}}/api/public/traces?page=&limit=&userId=&name=&fromTimestamp=&orderBy=&tags=",
"raw": "{{baseUrl}}/api/public/traces?page=&limit=&userId=&name=&sessionId=&fromTimestamp=&orderBy=&tags=",
"host": [
"{{baseUrl}}"
],
@@ -993,6 +993,11 @@
"value": "",
"description": null
},
{
"key": "sessionId",
"value": "",
"description": null
},
{
"key": "fromTimestamp",
"value": "",
+4 -4
View File
@@ -18,7 +18,7 @@ if (process.env.NEXT_PUBLIC_SENTRY_DSN)
samplingContext.request.url &&
samplingContext.request.url.includes("api/trpc")
) {
return 0.3;
return 0.1;
}
if (
samplingContext.request &&
@@ -27,12 +27,12 @@ if (process.env.NEXT_PUBLIC_SENTRY_DSN)
samplingContext.transactionContext.status !== "ok" &&
samplingContext.transactionContext.status !== "unauthenticated"
) {
return 1;
return 0.1;
}
return 0.1;
return 0.01;
},
profilesSampleRate: 0.2, // Profiling sample rate is relative to tracesSampleRate
profilesSampleRate: 0.1,
integrations: [
// Add profiling integration to list of integrations
new ProfilingIntegration(),
@@ -68,12 +68,18 @@ describe("/api/public/metrics/daily API Endpoint", () => {
inputUsage: 333,
outputUsage: 0,
totalUsage: 333,
countObservations: 1,
countTraces: 1,
totalCost: 0,
},
{
model: "modelC",
inputUsage: 666,
outputUsage: 777,
totalUsage: 1443,
countObservations: 1,
countTraces: 1,
totalCost: 1024.22,
},
]);
@@ -88,6 +94,9 @@ describe("/api/public/metrics/daily API Endpoint", () => {
inputUsage: 100,
outputUsage: 200,
totalUsage: 300,
countObservations: 1,
countTraces: 1,
totalCost: 0,
},
]);
});
@@ -680,6 +680,71 @@ describe("/api/public/v2/prompts API Endpoint", () => {
expect(fetchedPrompt.body.createdBy).toBe("API");
expect(fetchedPrompt.body.config).toEqual({});
});
it("should update tags across versions", async () => {
const promptName = "prompt-name" + nanoid();
const createPromptVersion = async (tags?: string[]) => {
await makeAPICall("POST", baseURI, {
name: promptName,
prompt: "This is a test prompt",
type: PromptType.Text,
...(tags !== undefined && { tags: tags }),
});
};
const fetchPromptVersion = async (version: number) => {
const fetchedPrompt = await makeAPICall(
"GET",
`${baseURI}/${promptName}?version=${version}`,
undefined,
);
expect(fetchedPrompt.status).toBe(200);
if (!isPrompt(fetchedPrompt.body)) {
throw new Error("Expected body to be a prompt");
}
return fetchedPrompt.body;
};
// Create version 1 with ["tag"]
await createPromptVersion(["tag"]);
let fetchedPrompt1 = await fetchPromptVersion(1);
expect(fetchedPrompt1.tags).toEqual(["tag"]);
expect(fetchedPrompt1.version).toBe(1);
// Create version 2 with no tags provided (should use tags from version 1)
await createPromptVersion();
let fetchedPrompt2 = await fetchPromptVersion(2);
expect(fetchedPrompt2.tags).toEqual(["tag"]);
expect(fetchedPrompt2.version).toBe(2);
// Create version 3 with ["tag1", "tag2", "tag3"] (should update tags across versions)
await createPromptVersion(["tag1", "tag2", "tag3"]);
fetchedPrompt1 = await fetchPromptVersion(1);
fetchedPrompt2 = await fetchPromptVersion(2);
let fetchedPrompt3 = await fetchPromptVersion(3);
expect(fetchedPrompt1.tags).toEqual(["tag1", "tag2", "tag3"]);
expect(fetchedPrompt1.version).toBe(1);
expect(fetchedPrompt2.tags).toEqual(["tag1", "tag2", "tag3"]);
expect(fetchedPrompt2.version).toBe(2);
expect(fetchedPrompt3.tags).toEqual(["tag1", "tag2", "tag3"]);
expect(fetchedPrompt3.version).toBe(3);
// remove tags
await createPromptVersion([]);
fetchedPrompt1 = await fetchPromptVersion(1);
fetchedPrompt2 = await fetchPromptVersion(2);
fetchedPrompt3 = await fetchPromptVersion(3);
let fetchedPrompt4 = await fetchPromptVersion(4);
expect(fetchedPrompt1.tags).toEqual([]);
expect(fetchedPrompt1.version).toBe(1);
expect(fetchedPrompt2.tags).toEqual([]);
expect(fetchedPrompt2.version).toBe(2);
expect(fetchedPrompt3.tags).toEqual([]);
expect(fetchedPrompt3.version).toBe(3);
expect(fetchedPrompt4.tags).toEqual([]);
expect(fetchedPrompt4.version).toBe(4);
});
});
describe("when fetching a prompt list", () => {
@@ -50,7 +50,7 @@ describe("Build valid SQL queries", () => {
table: "traces_scores",
values: ["project-id"],
strings: [
' FROM traces t JOIN scores s ON t.id = s.trace_id AND t.project_id = s.project_id WHERE t."project_id" = ',
` FROM traces t JOIN scores s ON t.id = s.trace_id AND s.data_type != 'CATEGORICAL' AND t.project_id = s.project_id WHERE t."project_id" = `,
";",
],
} as const,
+15 -15
View File
@@ -489,16 +489,16 @@ describe("/api/public/scores API Endpoint", () => {
totalPages: 1,
});
expect(getScore.body.data).toMatchObject([
{
id: scoreId_1,
name: scoreName,
value: 10.5,
},
{
id: scoreId_2,
name: scoreName,
value: 50.5,
},
{
id: scoreId_1,
name: scoreName,
value: 10.5,
},
]);
});
it("test operator >=", async () => {
@@ -514,16 +514,16 @@ describe("/api/public/scores API Endpoint", () => {
totalPages: 1,
});
expect(getScore.body.data).toMatchObject([
{
id: scoreId_2,
name: scoreName,
value: 50.5,
},
{
id: scoreId_3,
name: scoreName,
value: 100.8,
},
{
id: scoreId_2,
name: scoreName,
value: 50.5,
},
]);
});
it("test operator !=", async () => {
@@ -539,16 +539,16 @@ describe("/api/public/scores API Endpoint", () => {
totalPages: 1,
});
expect(getScore.body.data).toMatchObject([
{
id: scoreId_1,
name: scoreName,
value: 10.5,
},
{
id: scoreId_3,
name: scoreName,
value: 100.8,
},
{
id: scoreId_1,
name: scoreName,
value: 10.5,
},
]);
});
it("test operator =", async () => {
+58
View File
@@ -238,4 +238,62 @@ describe("/api/public/traces API Endpoint", () => {
expect(trace.body.htmlPath).toContain(`/traces/${traceId}`);
expect(trace.body.htmlPath).toContain(`/project/`); // do not know the projectId
});
it("should filter traces by session ID", async () => {
const sessionId = "test-session-id";
const anotherSessionId = "another-session-id";
// Create traces with different session IDs
await makeAPICall("POST", "/api/public/traces", {
id: "trace-1",
name: "test-trace-1",
sessionId,
userId: "user-1",
projectId: "project-1",
metadata: { key: "value" },
release: "1.0.0",
version: "1.0.0",
});
await makeAPICall("POST", "/api/public/traces", {
id: "trace-2",
name: "test-trace-2",
sessionId: anotherSessionId,
userId: "user-2",
projectId: "project-1",
metadata: { key: "value" },
release: "1.0.0",
version: "1.0.0",
});
// Filter by session ID
const tracesBySessionId = await makeAPICall<GetTracesAPIResponse>(
"GET",
`/api/public/traces?sessionId=${sessionId}`,
);
expect(tracesBySessionId.status).toBe(200);
expect(tracesBySessionId.body.data).toHaveLength(1);
expect(tracesBySessionId.body.data[0].id).toBe("trace-1");
// Filter by another session ID
const tracesByAnotherSessionId = await makeAPICall<GetTracesAPIResponse>(
"GET",
`/api/public/traces?sessionId=${anotherSessionId}`,
);
expect(tracesByAnotherSessionId.status).toBe(200);
expect(tracesByAnotherSessionId.body.data).toHaveLength(1);
expect(tracesByAnotherSessionId.body.data[0].id).toBe("trace-2");
// Filter by non-existent session ID
const tracesByNonExistentSessionId =
await makeAPICall<GetTracesAPIResponse>(
"GET",
`/api/public/traces?sessionId=non-existent-session-id`,
);
expect(tracesByNonExistentSessionId.status).toBe(200);
expect(tracesByNonExistentSessionId.body.data).toHaveLength(0);
});
});
+6
View File
@@ -0,0 +1,6 @@
import chatCompletionHandler from "@/src/ee/features/playground/server/chatCompletionHandler";
export const dynamic = "force-dynamic";
export const maxDuration = 60;
export const POST = chatCompletionHandler;
+100 -13
View File
@@ -1,8 +1,105 @@
import { AlertTriangle, Check } from "lucide-react";
import {
AlertTriangle,
Check,
Github,
HardDriveDownload,
Newspaper,
} from "lucide-react";
import { VERSION } from "@/src/constants";
import { env } from "@/src/env.mjs";
import { cn } from "@/src/utils/tailwind";
import {
DropdownMenu,
DropdownMenuTrigger,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuLabel,
DropdownMenuSeparator,
} from "@/src/components/ui/dropdown-menu";
import { ArrowUp } from "lucide-react";
import { api } from "@/src/utils/api";
import { Button } from "@/src/components/ui/button";
import Link from "next/link";
const VersionLabel = ({ className }: { className?: string }) => {
const checkUpdate = api.public.checkUpdate.useQuery(undefined, {
refetchOnMount: false,
refetchOnWindowFocus: false,
refetchOnReconnect: false,
enabled: !env.NEXT_PUBLIC_LANGFUSE_CLOUD_REGION, // do not check for updates on Langfuse Cloud
onError: (error) => console.error("checkUpdate error", error), // do not render default error message
});
const hasUpdate =
!env.NEXT_PUBLIC_LANGFUSE_CLOUD_REGION &&
checkUpdate.data &&
checkUpdate.data.updateType;
const color =
checkUpdate.data?.updateType === "major"
? "text-dark-red"
: checkUpdate.data?.updateType === "minor"
? "text-dark-yellow"
: undefined;
return (
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="ghost" size="xs" className={className}>
{VERSION}
{hasUpdate && <ArrowUp className={`ml-1 h-3 w-3 ${color}`} />}
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent>
{hasUpdate ? (
<>
<DropdownMenuLabel>
New {checkUpdate.data?.updateType} version:{" "}
{checkUpdate.data?.latestRelease}
</DropdownMenuLabel>
<DropdownMenuSeparator />
</>
) : !env.NEXT_PUBLIC_LANGFUSE_CLOUD_REGION ? (
<>
<DropdownMenuLabel>This is the latest release</DropdownMenuLabel>
<DropdownMenuSeparator />
</>
) : null}
<DropdownMenuItem asChild>
<Link
href="https://github.com/langfuse/langfuse/releases"
target="_blank"
>
<Github size={16} className="mr-2" />
Releases
</Link>
</DropdownMenuItem>
<DropdownMenuItem asChild>
<Link href="https://langfuse.com/changelog" target="_blank">
<Newspaper size={16} className="mr-2" />
Changelog
</Link>
</DropdownMenuItem>
{hasUpdate && (
<>
<DropdownMenuSeparator />
<DropdownMenuItem asChild>
<Link
href="https://langfuse.com/docs/deployment/self-host#update"
target="_blank"
>
<HardDriveDownload size={16} className="mr-2" />
Update
</Link>
</DropdownMenuItem>
</>
)}
</DropdownMenuContent>
</DropdownMenu>
);
};
export const LangfuseIcon = ({
size = 32,
@@ -65,22 +162,12 @@ export const LangfuseLogo = ({
<span
className={cn(
"ml-2 font-mono font-semibold",
size === "sm" ? "text-sm" : "text-xl",
size === "sm" ? "text-sm" : "text-lg",
)}
>
Langfuse
</span>
{version && (
<a
href="https://github.com/langfuse/langfuse/releases"
target="_blank"
rel="noopener"
title="View releases on GitHub"
className="ml-2 text-xs text-muted-foreground"
>
{VERSION}
</a>
)}
{version && <VersionLabel className="ml-2" />}
</div>
</div>
);
@@ -0,0 +1,77 @@
import Link from "next/link";
import { Label } from "@/src/components/ui/label";
import { useHasAccess } from "@/src/features/rbac/utils/checkAccess";
import { api } from "@/src/utils/api";
import { supportedModels, type UIModelParams } from "@langfuse/shared";
import { ArrowTopRightIcon } from "@radix-ui/react-icons";
export const LLMApiKeyComponent = (p: {
projectId: string;
modelParams: UIModelParams;
}) => {
const hasAccess = useHasAccess({
projectId: p.projectId,
scope: "llmApiKeys:read",
});
if (!hasAccess) {
return (
<div>
<Label className="text-xs font-semibold">API key</Label>
<p className="text-sm text-muted-foreground">
LLM API Key only visible to Owner and Admin roles.
</p>
</div>
);
}
const apiKeys = api.llmApiKey.all.useQuery({
projectId: p.projectId,
});
if (apiKeys.isLoading) {
return (
<div>
<Label className="text-xs font-semibold">API key</Label>
<p className="text-sm text-muted-foreground">Loading...</p>
</div>
);
}
const model = p.modelParams.model.value;
const modelProvider = Object.entries(supportedModels).find((providerData) =>
(providerData[1] as any as string[]).includes(model),
)?.[0];
const apiKey = apiKeys.data?.data.find((k) => k.provider === modelProvider);
return (
<div className="space-y-2 text-xs">
<Label className="text-xs font-semibold">API key</Label>
<div>
{apiKey ? (
<span className="mr-2 rounded-sm bg-input p-1 text-xs">
{apiKey.displaySecretKey}
</span>
) : undefined}
</div>
{/* Custom form message to include a link to the already existing prompt */}
{!apiKey ? (
<div className="flex flex-col font-medium text-destructive">
{`No LLM API key found for provider ${modelProvider}.`}
<Link
href={`/project/${p.projectId}/settings`}
className="flex flex-row"
>
Create a new LLM API key here. <ArrowTopRightIcon />
</Link>
</div>
) : undefined}
<p className="text-muted-foreground">
The LLM API key is used for each execution and will incur costs.
</p>
</div>
);
};
@@ -8,6 +8,7 @@ import {
} from "@/src/components/ui/select";
import { Slider } from "@/src/components/ui/slider";
import { Switch } from "@/src/components/ui/switch";
import useProjectIdFromURL from "@/src/hooks/useProjectIdFromURL";
import { cn } from "@/src/utils/tailwind";
import {
ModelProvider,
@@ -15,6 +16,8 @@ import {
type UIModelParams,
} from "@langfuse/shared";
import { LLMApiKeyComponent } from "./LLMApiKeyComponent";
export type ModelParamsContext = {
modelParams: UIModelParams;
availableModels?: UIModelParams[];
@@ -33,10 +36,14 @@ export const ModelParameters: React.FC<ModelParamsContext> = ({
setModelParamEnabled,
formDisabled = false,
}) => {
const projectId = useProjectIdFromURL();
if (!projectId) return null;
return (
<div className="flex flex-col space-y-4">
<p className="font-semibold">Model</p>
<div className="space-y-6">
<div className="space-y-4">
<ModelParamsSelect
title="Provider"
modelParamsKey="provider"
@@ -104,6 +111,7 @@ export const ModelParameters: React.FC<ModelParamsContext> = ({
tooltip="An alternative to sampling with temperature, called nucleus sampling, where the model considers the results of the tokens with top_p probability mass. So 0.1 means only the tokens comprising the top 10% probability mass are considered. We generally recommend altering this or temperature but not both."
updateModelParam={updateModelParamValue}
/>
<LLMApiKeyComponent {...{ projectId, modelParams }} />
</div>
</div>
);
+15 -22
View File
@@ -8,14 +8,7 @@ import {
CardTitle,
} from "@/src/components/ui/card";
import { chatAvailable, openChat } from "@/src/features/support-chat/chat";
import {
Book,
Calendar,
Github,
Mail,
MessageSquare,
Slack,
} from "lucide-react";
import { Book, Github, Mail, MessageSquare, Slack } from "lucide-react";
import Link from "next/link";
import { SiDiscord } from "react-icons/si";
@@ -30,7 +23,7 @@ const supportChannels = [
},
{
icon: Github,
title: "GitHub",
title: "GitHub Issues",
description:
"Create an issue on Github to report bugs or request new features.",
href: "https://github.com/langfuse/langfuse/issues/new/choose",
@@ -38,12 +31,11 @@ const supportChannels = [
primary: true,
},
{
icon: SiDiscord,
title: "Discord",
description:
"Get support from community and maintainers. Follow announcements to stay up to date with new features.",
href: "https://langfuse.com/discord",
buttonText: "Join Discord",
icon: Github,
title: "GitHub Support",
description: "Create a support ticket via GitHub discussions.",
href: "https://github.com/orgs/langfuse/discussions/categories/support",
buttonText: "Submit question",
primary: true,
},
{
@@ -57,24 +49,25 @@ const supportChannels = [
{
icon: MessageSquare,
title: "Chat",
description: "Get quick support directly from the team.",
description: "Get support directly from the team.",
onClick: () => openChat(),
available: chatAvailable,
buttonText: "Launch Chat",
},
{
icon: Calendar,
title: "Schedule call",
description: "Schedule a call with one of the founders.",
href: "https://cal.com/marc-kl/office-hours",
buttonText: "Schedule Call",
icon: SiDiscord,
title: "Discord",
description:
"Get support from community. Follow announcements to stay up to date with new features.",
href: "https://langfuse.com/discord",
buttonText: "Join Discord",
},
{
icon: Slack,
title: "Slack Connect",
description: "Get a dedicated support channel for you and your team.",
href: "mailto:help@langfuse.com?subject=Slack%20Connect%20Request&body=I'd%20like%20to%20request%20a%20dedicated%20Slack%20Connect%20channel%20for%20me%20and%20my%20team.%0D%0A%0D%0AUsers%20(emails)%20to%20include%20besides%20mine%3A%0D%0A%0D%0A",
buttonText: "Request Slack Connect (via Email)",
buttonText: "Request via Email",
},
];
+14 -5
View File
@@ -4,9 +4,13 @@ import {
HoverCardContent,
HoverCardTrigger,
} from "@/src/components/ui/hover-card";
import {
isBooleanDataType,
isCategoricalDataType,
} from "@/src/features/manual-scoring/lib/helpers";
import { type ScoreSimplified } from "@/src/server/api/routers/generations/getAllQuery";
import { cn } from "@/src/utils/tailwind";
import { MessageCircle } from "lucide-react";
import { MessageCircleMore } from "lucide-react";
export const GroupedScoreBadges = ({
scores,
@@ -34,11 +38,13 @@ export const GroupedScoreBadges = ({
<div className={cn("text-xs", props.className)}>
{props.scores.map((s, i) => (
<span key={i} className="group/score ml-1 first:ml-0">
{s.value.toFixed(2)}
{isCategoricalDataType(s.dataType) || isBooleanDataType(s.dataType)
? s.stringValue
: s.value.toFixed(2)}
{s.comment && (
<HoverCard>
<HoverCardTrigger className="ml-1 inline-block cursor-pointer">
<MessageCircle size={12} />
<MessageCircleMore size={12} />
</HoverCardTrigger>
<HoverCardContent className="overflow-hidden whitespace-normal break-normal">
<p>{s.comment}</p>
@@ -73,9 +79,12 @@ export const GroupedScoreBadges = ({
<Badge
variant="outline"
key={name}
className="break-all font-normal"
className="grid grid-cols-[1fr,auto] gap-1 font-normal"
>
{name}: <ScoresOfGroup scores={scores} className="ml-2" />
<p className="truncate" title={name}>
{name}:
</p>
<ScoresOfGroup scores={scores} />
</Badge>
))}
</>
+19 -5
View File
@@ -1,9 +1,11 @@
import CodeMirror, { EditorView } from "@uiw/react-codemirror";
import { githubLight } from "@uiw/codemirror-theme-github";
import { tokyoNight } from "@uiw/codemirror-theme-tokyo-night";
import { json } from "@codemirror/lang-json";
import { json, jsonParseLinter } from "@codemirror/lang-json";
import { linter } from "@codemirror/lint";
import { useTheme } from "next-themes";
import { cn } from "@/src/utils/tailwind";
import { useState } from "react";
// todo: add json linting
@@ -20,8 +22,13 @@ export function JsonEditor({
lineWrapping?: boolean;
className?: string;
}) {
const { theme } = useTheme();
const codeMirrorTheme = theme === "dark" ? tokyoNight : githubLight;
const { resolvedTheme } = useTheme();
const codeMirrorTheme = resolvedTheme === "dark" ? tokyoNight : githubLight;
// used to disable linter when field is empty
const [linterEnabled, setLinterEnabled] = useState<boolean>(
!!defaultValue && defaultValue !== "",
);
return (
<CodeMirror
value={defaultValue}
@@ -30,9 +37,16 @@ export function JsonEditor({
foldGutter: true,
}}
lang={"json"}
extensions={[json(), ...(lineWrapping ? [EditorView.lineWrapping] : [])]}
extensions={[
json(),
...(linterEnabled ? [linter(jsonParseLinter())] : []),
...(lineWrapping ? [EditorView.lineWrapping] : []),
]}
defaultValue={defaultValue}
onChange={onChange}
onChange={(c) => {
if (onChange) onChange(c);
setLinterEnabled(c !== "");
}}
className={cn("overflow-hidden rounded-md border", className)}
editable={editable}
/>
+5 -1
View File
@@ -4,6 +4,7 @@ import {
HoverCardTrigger,
} from "@/src/components/ui/hover-card";
import { usePostHogClientCapture } from "@/src/features/posthog-analytics/usePostHogClientCapture";
import { cn } from "@/src/utils/tailwind";
import { HelpCircle, Info } from "lucide-react";
import Link from "next/link";
@@ -40,7 +41,10 @@ export default function DocPopup({
}
}}
>
<HoverCardTrigger className="mx-1 cursor-pointer" asChild>
<HoverCardTrigger
className={cn("mx-1", href ? "cursor-pointer" : "cursor-default")}
asChild
>
{href ? (
<Link
href={href}
+50 -118
View File
@@ -7,38 +7,24 @@ import { IOPreview } from "@/src/components/trace/IOPreview";
import { JsonSkeleton } from "@/src/components/ui/CodeJsonViewer";
import { Badge } from "@/src/components/ui/badge";
import { Card } from "@/src/components/ui/card";
import { ManualScoreButton } from "@/src/features/manual-scoring/components/ManualScoreButton";
import { DetailPageNav } from "@/src/features/navigate-detail-pages/DetailPageNav";
import { useDetailPageLists } from "@/src/features/navigate-detail-pages/context";
import { api } from "@/src/utils/api";
import { usdFormatter } from "@/src/utils/numbers";
import Link from "next/link";
import { useEffect, useRef } from "react";
import { useWindowVirtualizer } from "@tanstack/react-virtual";
import { type RouterOutput } from "@/src/utils/types";
import {
DataTableRowHeightSwitch,
type RowHeight,
useRowHeightLocalStorage,
} from "@/src/components/table/data-table-row-height-switch";
import { ScrollArea } from "@/src/components/ui/scroll-area";
import { useEffect, useState } from "react";
import { AnnotateButton } from "@/src/features/manual-scoring/components/AnnotateButton";
import { Button } from "@/src/components/ui/button";
// do not use the usual table row heights here
const rowHeightMapping: Record<RowHeight, number> = {
s: 200,
m: 350,
l: 700,
};
// some projects have thousands of traces in a sessions, paginate to avoid rendering all at once
const PAGE_SIZE = 50;
export const SessionPage: React.FC<{
sessionId: string;
projectId: string;
}> = ({ sessionId, projectId }) => {
const { setDetailPageList } = useDetailPageLists();
const [rowHeight, setRowHeight] = useRowHeightLocalStorage(
"single-session",
"m",
);
const [visibleTraces, setVisibleTraces] = useState(PAGE_SIZE);
const session = api.sessions.byId.useQuery(
{
sessionId,
@@ -96,11 +82,6 @@ export const SessionPage: React.FC<{
}
listKey="sessions"
/>,
<DataTableRowHeightSwitch
rowHeight={rowHeight}
setRowHeight={setRowHeight}
key="height"
/>,
]}
/>
<div className="flex flex-wrap gap-2">
@@ -121,97 +102,49 @@ export const SessionPage: React.FC<{
</Badge>
)}
</div>
{session.data && (
<TraceCardList
session={session.data}
projectId={projectId}
rowHeight={rowHeightMapping[rowHeight]}
/>
)}
</div>
);
};
const TraceCardList = ({
session,
projectId,
rowHeight,
}: {
session: RouterOutput["sessions"]["byId"];
projectId: string;
rowHeight: number;
}) => {
const listVirtualizationRef = useRef<HTMLDivElement | null>(null);
const virtualizer = useWindowVirtualizer({
count: session.traces.length,
estimateSize: () => rowHeight,
overscan: 5,
scrollMargin: listVirtualizationRef.current?.offsetTop ?? 0,
gap: 10,
});
useEffect(() => {
// re-measure when rowHeight changes to update the virtualizer
virtualizer.measure();
}, [rowHeight, virtualizer]);
return (
<div className="mt-5 border-t pt-5">
<div
ref={listVirtualizationRef}
style={{
height: `${virtualizer.getTotalSize()}px`,
position: "relative",
}}
>
{virtualizer
.getVirtualItems()
.map((virtualItem) => ({
virtualItem,
trace: session.traces[virtualItem.index],
}))
.map(({ virtualItem, trace }) => (
<Card
className="group grid w-full gap-3 overflow-hidden border-border p-2 shadow-none hover:border-ring md:grid-cols-3"
key={virtualItem.key}
data-index={virtualItem.index}
style={{
position: "absolute",
top: 0,
left: 0,
height: `${rowHeight}px`,
transform: `translateY(${virtualItem.start - virtualizer.options.scrollMargin}px)`,
}}
>
<ScrollArea className="col-span-2 pr-3">
<SessionIO traceId={trace.id} />
</ScrollArea>
<ScrollArea>
<div className="-mt-1 overflow-y-auto p-1 opacity-50 transition-opacity group-hover:opacity-100">
<Link
href={`/project/${projectId}/traces/${trace.id}`}
className="text-xs hover:underline"
>
Trace: {trace.name} ({trace.id})&nbsp;
</Link>
<div className="text-xs text-muted-foreground">
{trace.timestamp.toLocaleString()}
</div>
<div className="mb-1 mt-2 text-xs text-muted-foreground">
Scores
</div>
<div className="flex flex-wrap content-start items-start gap-1">
<GroupedScoreBadges scores={trace.scores} />
</div>
<ManualScoreButton
projectId={projectId}
traceId={trace.id}
scores={trace.scores}
variant="badge"
/>
</div>
</ScrollArea>
</Card>
))}
<div className="mt-5 flex flex-col gap-2 border-t pt-5">
{session.data?.traces.slice(0, visibleTraces).map((trace) => (
<Card
className="group grid gap-3 border-border p-2 shadow-none hover:border-ring md:grid-cols-3"
key={trace.id}
>
<SessionIO traceId={trace.id} />
<div className="-mt-1 p-1 opacity-50 transition-opacity group-hover:opacity-100">
<Link
href={`/project/${projectId}/traces/${trace.id}`}
className="text-xs hover:underline"
>
Trace: {trace.name} ({trace.id})&nbsp;
</Link>
<div className="text-xs text-muted-foreground">
{trace.timestamp.toLocaleString()}
</div>
<div className="mb-1 mt-2 text-xs text-muted-foreground">
Scores
</div>
<div className="mb-1 flex flex-wrap content-start items-start gap-1">
<GroupedScoreBadges scores={trace.scores} />
</div>
<AnnotateButton
projectId={projectId}
traceId={trace.id}
scores={trace.scores}
variant="badge"
type="session"
source="SessionDetail"
/>
</div>
</Card>
))}
{session.data?.traces && session.data.traces.length > visibleTraces && (
<Button
onClick={() => setVisibleTraces((prev) => prev + PAGE_SIZE)}
variant="ghost"
className="self-center"
>
{`Load ${Math.min(session.data.traces.length - visibleTraces, PAGE_SIZE)} More`}
</Button>
)}
</div>
</div>
);
@@ -230,9 +163,8 @@ const SessionIO = ({ traceId }: { traceId: string }) => {
refetchOnMount: false, // prevents refetching loops
},
);
return (
<div className="flex flex-col gap-2 overflow-x-hidden overflow-y-scroll p-0">
<div className="col-span-2 flex flex-col gap-2 p-0">
{!trace.data ? (
<JsonSkeleton
className="h-full w-full overflow-hidden px-2 py-1"
+3 -2
View File
@@ -7,6 +7,7 @@ import { cn } from "@/src/utils/tailwind";
import { type RouterOutput, type RouterInput } from "@/src/utils/types";
import { useState } from "react";
import { usePostHogClientCapture } from "@/src/features/posthog-analytics/usePostHogClientCapture";
import { trpcErrorToast } from "@/src/utils/trpcErrorToast";
export function StarToggle({
value,
@@ -76,7 +77,7 @@ export function StarTraceToggle({
onError: (err, _newTodo, context) => {
setIsLoading(false);
// Rollback to the previous value if mutation fails
console.log("error", err);
trpcErrorToast(err);
utils.traces.all.setData(tracesFilter, context?.prev);
},
onSettled: () => {
@@ -157,7 +158,7 @@ export function StarTraceDetailsToggle({
},
onError: (err, _newTodo, context) => {
setIsLoading(false);
console.log("error", err);
trpcErrorToast(err);
// Rollback to the previous value if mutation fails
utils.traces.byId.setData({ traceId }, context?.prevData);
},
+23 -3
View File
@@ -50,6 +50,8 @@ interface DataTableProps<TData, TValue> {
help?: { description: string; href: string };
rowHeight?: RowHeight;
className?: string;
paginationClassName?: string;
isBorderless?: boolean;
}
export interface AsyncTableData<T> {
@@ -71,6 +73,9 @@ export function DataTable<TData extends object, TValue>({
orderBy,
setOrderBy,
rowHeight,
className,
paginationClassName,
isBorderless = false,
}: DataTableProps<TData, TValue>) {
const [columnFilters, setColumnFilters] = useState<ColumnFiltersState>([]);
const rowheighttw = getRowHeightTailwindClass(rowHeight);
@@ -105,8 +110,18 @@ export function DataTable<TData extends object, TValue>({
return (
<>
<div className="flex w-full max-w-full flex-1 flex-col gap-1 overflow-auto">
<div className="w-full overflow-auto rounded-md border">
<div
className={cn(
"flex w-full max-w-full flex-1 flex-col gap-1 overflow-auto",
className,
)}
>
<div
className={cn(
"w-full overflow-auto",
isBorderless ? "" : "rounded-md border",
)}
>
<Table>
<TableHeader>
{table.getHeaderGroups().map((headerGroup) => (
@@ -242,7 +257,12 @@ export function DataTable<TData extends object, TValue>({
<div className="grow"></div>
</div>
{pagination !== undefined ? (
<div className="bg:background sticky bottom-0 z-10 flex w-full justify-end font-medium">
<div
className={cn(
"sticky bottom-0 z-10 flex w-full justify-end bg-background font-medium",
paginationClassName,
)}
>
<DataTablePagination
table={table}
paginationOptions={pagination.options}
@@ -21,11 +21,7 @@ import {
withDefault,
} from "use-query-params";
import { useQueryFilterState } from "@/src/features/filters/hooks/useFilterState";
import {
formatIntervalSeconds,
intervalInSeconds,
utcDateOffsetByDays,
} from "@/src/utils/dates";
import { formatIntervalSeconds, utcDateOffsetByDays } from "@/src/utils/dates";
import useColumnVisibility from "@/src/features/column-visibility/hooks/useColumnVisibility";
import { type LangfuseColumnDef } from "@/src/components/table/types";
import {
@@ -119,7 +115,7 @@ export default function GenerationsTable({
type: "datetime",
operator: ">",
value: utcDateOffsetByDays(
session.data?.environment.defaultTableDateTimeOffset ?? -14,
session.data?.environment.defaultTableDateTimeOffset ?? -7,
),
},
],
@@ -0,0 +1,174 @@
import React from "react";
import { Card } from "@/src/components/ui/card";
import { useRowHeightLocalStorage } from "@/src/components/table/data-table-row-height-switch";
import useColumnVisibility from "@/src/features/column-visibility/hooks/useColumnVisibility";
import { api } from "@/src/utils/api";
import { type LangfuseColumnDef } from "@/src/components/table/types";
import { DataTableToolbar } from "@/src/components/table/data-table-toolbar";
import { DataTable } from "@/src/components/table/data-table";
import { type ScoreDataType, type Prisma } from "@langfuse/shared";
import { IOTableCell } from "@/src/components/ui/CodeJsonViewer";
import { NumberParam, useQueryParams, withDefault } from "use-query-params";
import { isNumericDataType } from "@/src/features/manual-scoring/lib/helpers";
type ScoreConfigTableRow = {
id: string;
name: string;
dataType: ScoreDataType;
createdAt: string;
updatedAt: string;
range: {
maxValue?: number | null;
minValue?: number | null;
categories?: Prisma.JsonValue | null;
};
description?: string | null;
};
function getConfigRange(
originalRow: ScoreConfigTableRow,
): Prisma.JsonValue | undefined {
const { range, dataType } = originalRow;
if (isNumericDataType(dataType)) {
return [
{ minValue: range.minValue ?? "-∞", maxValue: range.maxValue ?? "∞" },
];
}
return range.categories;
}
export function ScoreConfigsTable({ projectId }: { projectId: string }) {
const [paginationState, setPaginationState] = useQueryParams({
pageIndex: withDefault(NumberParam, 0),
pageSize: withDefault(NumberParam, 50),
});
const [rowHeight, setRowHeight] = useRowHeightLocalStorage(
"scoreConfigs",
"s",
);
const configs = api.scoreConfigs.all.useQuery({
projectId,
page: paginationState.pageIndex,
limit: paginationState.pageSize,
});
const totalCount = configs.data?.totalCount ?? 0;
const columns: LangfuseColumnDef<ScoreConfigTableRow>[] = [
{
accessorKey: "name",
id: "name",
header: "Name",
enableHiding: true,
},
{
accessorKey: "dataType",
id: "dataType",
header: "Data Type",
enableHiding: true,
},
{
accessorKey: "range",
id: "range",
header: "Range",
enableHiding: true,
cell: ({ row }) => {
const range = getConfigRange(row.original);
return !!range ? (
<IOTableCell data={range} singleLine={rowHeight === "s"} />
) : null;
},
},
{
accessorKey: "description",
id: "description",
header: "Description",
enableHiding: true,
cell: ({ row }) => {
const value = row.original.description;
return !!value ? (
<IOTableCell data={value} singleLine={rowHeight === "s"} />
) : null;
},
},
{
accessorKey: "id",
id: "id",
header: "Config ID",
enableHiding: true,
defaultHidden: true,
},
{
accessorKey: "createdAt",
id: "createdAt",
header: "Created At",
enableHiding: true,
defaultHidden: true,
},
];
const [columnVisibility, setColumnVisibility] =
useColumnVisibility<ScoreConfigTableRow>(
"scoreConfigsColumnVisibility",
columns,
);
return (
<>
<DataTableToolbar
columns={columns}
columnVisibility={columnVisibility}
setColumnVisibility={setColumnVisibility}
rowHeight={rowHeight}
setRowHeight={setRowHeight}
/>
<Card className="mb-4 flex max-h-[calc(100dvh-40rem)] flex-col overflow-hidden">
<DataTable
columns={columns}
data={
configs.isLoading
? { isLoading: true, isError: false }
: configs.isError
? {
isLoading: false,
isError: true,
error: configs.error.message,
}
: {
isLoading: false,
isError: false,
data: configs.data?.configs.map((config) => ({
id: config.id,
name: config.name,
dataType: config.dataType,
description: config.description,
createdAt: config.createdAt.toLocaleString(),
updatedAt: config.updatedAt.toLocaleString(),
range: {
maxValue: config.maxValue,
minValue: config.minValue,
categories: config.categories,
},
})),
}
}
pagination={{
pageCount: Math.ceil(totalCount / paginationState.pageSize),
onChange: setPaginationState,
state: paginationState,
}}
columnVisibility={columnVisibility}
onColumnVisibilityChange={setColumnVisibility}
rowHeight={rowHeight}
className="gap-2"
paginationClassName="-mx-2 mb-2"
isBorderless
/>
</Card>
</>
);
}
+80 -29
View File
@@ -4,16 +4,20 @@ import { DataTableToolbar } from "@/src/components/table/data-table-toolbar";
import TableLink from "@/src/components/table/table-link";
import { type LangfuseColumnDef } from "@/src/components/table/types";
import { IOTableCell } from "@/src/components/ui/CodeJsonViewer";
import { Avatar, AvatarImage } from "@/src/components/ui/avatar";
import useColumnVisibility from "@/src/features/column-visibility/hooks/useColumnVisibility";
import { useQueryFilterState } from "@/src/features/filters/hooks/useFilterState";
import { isNumericDataType } from "@/src/features/manual-scoring/lib/helpers";
import { useOrderByState } from "@/src/features/orderBy/hooks/useOrderByState";
import {
type ScoreOptions,
scoresTableColsWithOptions,
} from "@/src/server/api/definitions/scoresTable";
import { api } from "@/src/utils/api";
import { utcDateOffsetByDays } from "@/src/utils/dates";
import type { RouterOutput, RouterInput } from "@/src/utils/types";
import type { FilterState } from "@langfuse/shared";
import type { FilterState, ScoreDataType } from "@langfuse/shared";
import { useSession } from "next-auth/react";
import { useQueryParams, withDefault, NumberParam } from "use-query-params";
export type ScoresTableRow = {
@@ -22,7 +26,12 @@ export type ScoresTableRow = {
timestamp: string;
source: string;
name: string;
value: number;
dataType: ScoreDataType;
value: string;
author: {
image?: string;
name?: string;
};
comment?: string;
observationId?: string;
traceName?: string;
@@ -72,11 +81,20 @@ export default function ScoresTable({
pageIndex: withDefault(NumberParam, 0),
pageSize: withDefault(NumberParam, 50),
});
const session = useSession();
const [rowHeight, setRowHeight] = useRowHeightLocalStorage("scores", "s");
const [userFilterState, setUserFilterState] = useQueryFilterState(
[],
[
{
column: "Timestamp",
type: "datetime",
operator: ">",
value: utcDateOffsetByDays(
session.data?.environment.defaultTableDateTimeOffset ?? -7,
),
},
],
"scores",
);
@@ -161,6 +179,29 @@ export default function ScoresTable({
) : undefined;
},
},
{
accessorKey: "userId",
header: "Trace User ID",
id: "userId",
headerTooltip: {
description: "The user ID associated with the trace.",
href: "https://langfuse.com/docs/tracing-features/users",
},
enableHiding: true,
enableSorting: true,
cell: ({ row }) => {
const value = row.getValue("userId");
return typeof value === "string" ? (
<>
<TableLink
path={`/project/${projectId}/users/${value}`}
value={value}
truncateAt={40}
/>
</>
) : undefined;
},
},
{
accessorKey: "timestamp",
header: "Timestamp",
@@ -182,38 +223,39 @@ export default function ScoresTable({
enableHiding: true,
enableSorting: true,
},
{
accessorKey: "dataType",
header: "Data Type",
id: "dataType",
enableHiding: true,
enableSorting: true,
},
{
accessorKey: "value",
header: "Value",
id: "value",
enableHiding: true,
enableSorting: true,
cell: ({ row }) => {
const value: number = row.getValue("value");
return value % 1 === 0 ? value : value.toFixed(4);
},
},
{
accessorKey: "userId",
header: "User ID",
id: "userId",
headerTooltip: {
description: "The user ID associated with the trace.",
href: "https://langfuse.com/docs/tracing-features/users",
},
enableHiding: true,
enableSorting: true,
accessorKey: "author",
id: "author",
header: "Author",
cell: ({ row }) => {
const value = row.getValue("userId");
return typeof value === "string" ? (
<>
<TableLink
path={`/project/${projectId}/users/${value}`}
value={value}
truncateAt={40}
/>
</>
) : undefined;
const { name, image } = row.getValue(
"author",
) as ScoresTableRow["author"];
return (
<div className="flex items-center space-x-2">
<Avatar className="h-7 w-7">
<AvatarImage
src={image ?? undefined}
alt={name ?? "User Avatar"}
/>
</Avatar>
<span>{name}</span>
</div>
);
},
},
{
@@ -268,12 +310,21 @@ export default function ScoresTable({
timestamp: score.timestamp.toLocaleString(),
source: score.source,
name: score.name,
value: score.value,
dataType: score.dataType,
value: isNumericDataType(score.dataType)
? score.value % 1 === 0
? String(score.value)
: score.value.toFixed(4)
: score.stringValue ?? "",
author: {
image: score.authorUserImage ?? undefined,
name: score.authorUserName ?? undefined,
},
comment: score.comment ?? undefined,
observationId: score.observationId ?? undefined,
traceId: score.traceId,
traceName: score.traceName ?? undefined,
userId: score.userId ?? undefined,
userId: score.traceUserId ?? undefined,
jobConfigurationId: score.jobConfigurationId ?? undefined,
};
};
@@ -55,7 +55,7 @@ export default function SessionsTable({
type: "datetime",
operator: ">",
value: utcDateOffsetByDays(
session.data?.environment.defaultTableDateTimeOffset ?? -14,
session.data?.environment.defaultTableDateTimeOffset ?? -7,
),
},
],
@@ -96,7 +96,7 @@ export default function TracesTable({
type: "datetime",
operator: ">",
value: utcDateOffsetByDays(
session.data?.environment.defaultTableDateTimeOffset ?? -14,
session.data?.environment.defaultTableDateTimeOffset ?? -7,
),
},
],
@@ -547,6 +547,7 @@ export default function TracesTable({
header: "Version",
enableHiding: true,
enableSorting: true,
defaultHidden: true,
},
{
accessorKey: "release",
@@ -554,6 +555,7 @@ export default function TracesTable({
header: "Release",
enableHiding: true,
enableSorting: true,
defaultHidden: true,
},
{
accessorKey: "tags",
@@ -8,7 +8,6 @@ import {
CardTitle,
} from "@/src/components/ui/card";
import { Badge } from "@/src/components/ui/badge";
import { ManualScoreButton } from "@/src/features/manual-scoring/components/ManualScoreButton";
import { NewDatasetItemFromTrace } from "@/src/features/datasets/components/NewDatasetItemFromObservationButton";
import { type ObservationReturnType } from "@/src/server/api/routers/traces";
import { api } from "@/src/utils/api";
@@ -22,6 +21,7 @@ import { withDefault, StringParam, useQueryParam } from "use-query-params";
import ScoresTable from "@/src/components/table/use-cases/scores";
import { ScoresPreview } from "@/src/components/trace/ScoresPreview";
import { JumpToPlaygroundButton } from "@/src/ee/features/playground/page/components/JumpToPlaygroundButton";
import { AnnotateButton } from "@/src/features/manual-scoring/components/AnnotateButton";
export const ObservationPreview = (props: {
observations: Array<ObservationReturnType>;
@@ -154,11 +154,12 @@ export const ObservationPreview = (props: {
</div>
</div>
<div className="flex flex-wrap gap-2">
<ManualScoreButton
<AnnotateButton
projectId={props.projectId}
traceId={preloadedObservation.traceId}
observationId={preloadedObservation.id}
scores={props.scores}
type="observation"
/>
{observationWithInputAndOutput.data?.type === "GENERATION" && (
<JumpToPlaygroundButton
@@ -237,7 +238,7 @@ const PromptBadge = (props: { promptId: string; projectId: string }) => {
if (prompt.isLoading || !prompt.data) return null;
return (
<Link
href={`/project/${props.projectId}/prompts/${prompt.data.name}?version=${prompt.data.version}`}
href={`/project/${props.projectId}/prompts/${encodeURIComponent(prompt.data.name)}?version=${prompt.data.version}`}
>
<Badge>
Prompt: {prompt.data.name}
+4 -4
View File
@@ -9,7 +9,6 @@ import {
CardTitle,
} from "@/src/components/ui/card";
import { TraceAggUsageBadge } from "@/src/components/token-usage-badge";
import { ManualScoreButton } from "@/src/features/manual-scoring/components/ManualScoreButton";
import { Badge } from "@/src/components/ui/badge";
import { type ObservationReturnType } from "@/src/server/api/routers/traces";
import { IOPreview } from "@/src/components/trace/IOPreview";
@@ -19,6 +18,7 @@ import { Tabs, TabsList, TabsTrigger } from "@/src/components/ui/tabs";
import { withDefault, StringParam, useQueryParam } from "use-query-params";
import ScoresTable from "@/src/components/table/use-cases/scores";
import { ScoresPreview } from "@/src/components/trace/ScoresPreview";
import { AnnotateButton } from "@/src/features/manual-scoring/components/AnnotateButton";
export const TracePreview = ({
trace,
@@ -54,13 +54,13 @@ export const TracePreview = ({
<TabsList className="bg-background py-0">
<TabsTrigger
value="preview"
className="data-[state=active]:border-primary-accent h-full rounded-none border-b-4 border-transparent data-[state=active]:shadow-none"
className="h-full rounded-none border-b-4 border-transparent data-[state=active]:border-primary-accent data-[state=active]:shadow-none"
>
Preview
</TabsTrigger>
<TabsTrigger
value="scores"
className="data-[state=active]:border-primary-accent h-full rounded-none border-b-4 border-transparent data-[state=active]:shadow-none"
className="h-full rounded-none border-b-4 border-transparent data-[state=active]:border-primary-accent data-[state=active]:shadow-none"
>
Scores
</TabsTrigger>
@@ -95,7 +95,7 @@ export const TracePreview = ({
</div>
</div>
<div className="flex flex-wrap gap-2">
<ManualScoreButton
<AnnotateButton
projectId={trace.projectId}
traceId={trace.id}
scores={scores}
+1
View File
@@ -277,6 +277,7 @@ export function TracePage({ traceId }: { traceId: string }) {
availableTags={allTags}
traceId={trace.data.id}
projectId={trace.data.projectId}
className="flex-wrap"
/>
</div>
</div>
+2 -2
View File
@@ -17,7 +17,7 @@ export function JSONView(props: {
}) {
// some users ingest stringified json nested in json, parse it
const parsedJson = deepParseJson(props.json);
const { theme } = useTheme();
const { resolvedTheme } = useTheme();
return (
<div className={cn("rounded-md border", props.className)}>
{props.title ? (
@@ -44,7 +44,7 @@ export function JSONView(props: {
<React18JsonView
src={parsedJson}
theme="github"
dark={theme === "dark"}
dark={resolvedTheme === "dark"}
collapseObjectsAfterLength={20}
collapseStringsAfterLength={500}
displaySize={"collapsed"}
+2
View File
@@ -19,6 +19,8 @@ const buttonVariants = cva(
"bg-secondary text-secondary-foreground hover:bg-secondary/80",
ghost: "hover:bg-accent hover:text-accent-foreground",
link: "text-primary underline-offset-4 hover:underline",
errorNotification:
"bg-destructive-foreground/90 text-destructive hover:bg-destructive-foreground/80",
},
size: {
default: "h-10 px-4 py-2",
+1 -1
View File
@@ -42,7 +42,7 @@ const CommandInput = React.forwardRef<
React.ComponentPropsWithoutRef<typeof CommandPrimitive.Input>
>(({ className, ...props }, ref) => (
<div
className="flex items-center rounded-lg border px-3"
className="flex items-center rounded-md border px-3"
cmdk-input-wrapper=""
>
<Search className="mr-2 h-4 w-4 shrink-0 opacity-50" />
+129
View File
@@ -0,0 +1,129 @@
"use client";
import * as React from "react";
import { Drawer as DrawerPrimitive } from "vaul";
import { cn } from "@/src/utils/tailwind";
import { useMediaQuery } from "react-responsive";
// https://tailwindcss.com/docs/responsive-design
const TAILWIND_MD_MEDIA_QUERY = 768;
const Drawer = ({
shouldScaleBackground = true,
...props
}: React.ComponentProps<typeof DrawerPrimitive.Root>) => {
const isMediumScreen = useMediaQuery({
query: `(min-width: ${TAILWIND_MD_MEDIA_QUERY}px)`,
});
const direction = isMediumScreen ? "right" : "bottom";
return (
<DrawerPrimitive.Root
shouldScaleBackground={shouldScaleBackground}
direction={direction}
{...props}
/>
);
};
Drawer.displayName = "Drawer";
const DrawerTrigger = DrawerPrimitive.Trigger;
const DrawerPortal = DrawerPrimitive.Portal;
const DrawerClose = DrawerPrimitive.Close;
const DrawerOverlay = React.forwardRef<
React.ElementRef<typeof DrawerPrimitive.Overlay>,
React.ComponentPropsWithoutRef<typeof DrawerPrimitive.Overlay>
>(({ className, ...props }, ref) => (
<DrawerPrimitive.Overlay
ref={ref}
className={cn("fixed inset-0 z-50 bg-primary/20", className)}
{...props}
/>
));
DrawerOverlay.displayName = DrawerPrimitive.Overlay.displayName;
const DrawerContent = React.forwardRef<
React.ElementRef<typeof DrawerPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof DrawerPrimitive.Content>
>(({ className, children, ...props }, ref) => (
<DrawerPortal>
<DrawerOverlay />
<DrawerPrimitive.Content
ref={ref}
className={cn(
"fixed inset-x-0 bottom-0 z-50 mt-24 flex h-auto flex-col rounded-t-[10px] border bg-background md:inset-x-auto md:inset-y-0 md:right-0 md:mt-0 md:h-full md:w-2/5 md:rounded-l-[10px] md:rounded-t-[0px] lg:w-1/3 xl:w-1/4 2xl:w-1/4",
className,
)}
{...props}
>
{children}
</DrawerPrimitive.Content>
</DrawerPortal>
));
DrawerContent.displayName = "DrawerContent";
const DrawerHeader = ({
className,
...props
}: React.HTMLAttributes<HTMLDivElement>) => (
<div
className={cn("grid gap-1.5 p-4 text-center sm:text-left", className)}
{...props}
/>
);
DrawerHeader.displayName = "DrawerHeader";
const DrawerFooter = ({
className,
...props
}: React.HTMLAttributes<HTMLDivElement>) => (
<div
className={cn("mt-auto flex flex-col gap-2 p-4", className)}
{...props}
/>
);
DrawerFooter.displayName = "DrawerFooter";
const DrawerTitle = React.forwardRef<
React.ElementRef<typeof DrawerPrimitive.Title>,
React.ComponentPropsWithoutRef<typeof DrawerPrimitive.Title>
>(({ className, ...props }, ref) => (
<DrawerPrimitive.Title
ref={ref}
className={cn(
"text-lg font-semibold leading-none tracking-tight",
className,
)}
{...props}
/>
));
DrawerTitle.displayName = DrawerPrimitive.Title.displayName;
const DrawerDescription = React.forwardRef<
React.ElementRef<typeof DrawerPrimitive.Description>,
React.ComponentPropsWithoutRef<typeof DrawerPrimitive.Description>
>(({ className, ...props }, ref) => (
<DrawerPrimitive.Description
ref={ref}
className={cn("text-sm text-muted-foreground", className)}
{...props}
/>
));
DrawerDescription.displayName = DrawerPrimitive.Description.displayName;
export {
Drawer,
DrawerPortal,
DrawerOverlay,
DrawerTrigger,
DrawerClose,
DrawerContent,
DrawerHeader,
DrawerFooter,
DrawerTitle,
DrawerDescription,
};
+3 -1
View File
@@ -28,4 +28,6 @@ const PopoverContent = React.forwardRef<
));
PopoverContent.displayName = PopoverPrimitive.Content.displayName;
export { Popover, PopoverTrigger, PopoverContent };
const PopoverClose = PopoverPrimitive.Close;
export { Popover, PopoverTrigger, PopoverContent, PopoverClose };
+1 -1
View File
@@ -74,7 +74,7 @@ const TableHead = React.forwardRef<
<th
ref={ref}
className={cn(
"sticky top-0 z-10 border-b bg-background px-4 text-left align-middle font-medium text-muted-foreground [&:has([role=checkbox])]:pr-0",
"sticky top-0 z-10 rounded-t-md border-b bg-background px-4 text-left align-middle font-medium text-muted-foreground [&:has([role=checkbox])]:pr-0",
className,
)}
{...props}
+60
View File
@@ -0,0 +1,60 @@
"use client";
import * as React from "react";
import * as ToggleGroupPrimitive from "@radix-ui/react-toggle-group";
import { type VariantProps } from "class-variance-authority";
import { cn } from "@/src/utils/tailwind";
import { toggleVariants } from "@/src/components/ui/toggle";
const ToggleGroupContext = React.createContext<
VariantProps<typeof toggleVariants>
>({
size: "default",
variant: "default",
});
const ToggleGroup = React.forwardRef<
React.ElementRef<typeof ToggleGroupPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof ToggleGroupPrimitive.Root> &
VariantProps<typeof toggleVariants>
>(({ className, variant, size, children, ...props }, ref) => (
<ToggleGroupPrimitive.Root
ref={ref}
className={cn("flex items-center justify-center gap-1", className)}
{...props}
>
<ToggleGroupContext.Provider value={{ variant, size }}>
{children}
</ToggleGroupContext.Provider>
</ToggleGroupPrimitive.Root>
));
ToggleGroup.displayName = ToggleGroupPrimitive.Root.displayName;
const ToggleGroupItem = React.forwardRef<
React.ElementRef<typeof ToggleGroupPrimitive.Item>,
React.ComponentPropsWithoutRef<typeof ToggleGroupPrimitive.Item> &
VariantProps<typeof toggleVariants>
>(({ className, children, variant, size, ...props }, ref) => {
const context = React.useContext(ToggleGroupContext);
return (
<ToggleGroupPrimitive.Item
ref={ref}
className={cn(
toggleVariants({
variant: context.variant || variant,
size: context.size || size,
}),
className,
)}
{...props}
>
{children}
</ToggleGroupPrimitive.Item>
);
});
ToggleGroupItem.displayName = ToggleGroupPrimitive.Item.displayName;
export { ToggleGroup, ToggleGroupItem };
+1 -1
View File
@@ -1 +1 @@
export const VERSION = "v2.45.0";
export const VERSION = "v2.49.0";
@@ -33,8 +33,6 @@ import {
type ModelParams,
} from "@langfuse/shared";
import { PromptDescription } from "@/src/features/prompts/components/prompt-description";
import Link from "next/dist/client/link";
import { ArrowTopRightIcon } from "@radix-ui/react-icons";
import {
Select,
SelectContent,
@@ -43,8 +41,6 @@ import {
SelectValue,
} from "@/src/components/ui/select";
import { TEMPLATES } from "@/src/ee/features/evals/components/templates";
import { Label } from "@/src/components/ui/label";
import { useHasAccess } from "@/src/features/rbac/utils/checkAccess";
import { usePostHogClientCapture } from "@/src/features/posthog-analytics/usePostHogClientCapture";
import { getFinalModelParams } from "@/src/ee/utils/getFinalModelParams";
@@ -439,10 +435,6 @@ export const InnerEvalTemplateForm = (props: {
availableModels={[...evalLLMModels]}
formDisabled={!props.isEditing}
/>
<LLMApiKeyComponent
projectId={props.projectId}
modelParams={modelParams}
/>
</div>
</div>
@@ -465,78 +457,6 @@ export const InnerEvalTemplateForm = (props: {
);
};
export const LLMApiKeyComponent = (p: {
projectId: string;
modelParams: UIModelParams;
}) => {
const hasAccess = useHasAccess({
projectId: p.projectId,
scope: "llmApiKeys:read",
});
if (!hasAccess) {
return (
<div>
<Label>API key</Label>
<p className="text-sm text-muted-foreground">
LLM API Key only visible to Owner and Admin roles.
</p>
</div>
);
}
const apiKeys = api.llmApiKey.all.useQuery({
projectId: p.projectId,
});
if (apiKeys.isLoading) {
return (
<div>
<Label>API key</Label>
<p className="text-sm text-muted-foreground">Loading...</p>
</div>
);
}
const getModelProvider = (model: string) => {
return evalLLMModels.find((m) => m.model.value === model)?.provider.value;
};
const getApiKeyForModel = (model: string) => {
const modelProvider = getModelProvider(model);
return apiKeys.data?.data.find((k) => k.provider === modelProvider);
};
return (
<div>
<Label>API key</Label>
<div>
{getApiKeyForModel(p.modelParams.model.value) ? (
<span className="mr-2 rounded-sm bg-input p-1 text-xs">
{getApiKeyForModel(p.modelParams.model.value)?.displaySecretKey}
</span>
) : undefined}
</div>
{/* Custom form message to include a link to the already existing prompt */}
{!getApiKeyForModel(p.modelParams.model.value) ? (
<div className="flex flex-col text-sm font-medium text-destructive">
{"No LLM API key found."}
<Link
href={`/project/${p.projectId}/settings`}
className="flex flex-row"
>
Create a new API key here. <ArrowTopRightIcon />
</Link>
</div>
) : undefined}
<p className="text-sm text-muted-foreground">
The API key is used for each evaluation and will incur costs.
</p>
</div>
);
};
function getModelParamsWithEnabledFlag(
evalPreFill?: EvalTemplateFormPreFill,
): UIModelParams {

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