Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
250a5b03d2 | ||
|
|
5ee060dfc6 | ||
|
|
f08502cde4 | ||
|
|
90ecb88249 | ||
|
|
16e60f74da | ||
|
|
0ab252cc7c | ||
|
|
ebadbaf30d | ||
|
|
7ce6b6817e | ||
|
|
6d4040d5ed | ||
|
|
4a7a4c0add | ||
|
|
883e53d8b5 | ||
|
|
a9e6b9ae5c | ||
|
|
a61ee1f29f | ||
|
|
eefdf60e31 | ||
|
|
19a633a756 | ||
|
|
e84c1a3ab8 | ||
|
|
4d41aa7e53 | ||
|
|
a4c28cc324 | ||
|
|
187f2e7b1d | ||
|
|
b355825295 | ||
|
|
33dc18356a | ||
|
|
c804d90f6a | ||
|
|
09aa547de1 | ||
|
|
57839eae5c | ||
|
|
a43f5f38a1 | ||
|
|
8220d07057 | ||
|
|
89ffe20363 | ||
|
|
9a3adf54b1 | ||
|
|
459129118e | ||
|
|
0862370ad6 | ||
|
|
1bddde99f0 | ||
|
|
fe2e7681da | ||
|
|
d209766f01 | ||
|
|
82bfe03dba | ||
|
|
15fc8e4c3c | ||
|
|
374dbf9716 | ||
|
|
6819988f90 | ||
|
|
e3079afb2f | ||
|
|
3b6689f44b | ||
|
|
fdf417e961 | ||
|
|
a751f1f0f1 | ||
|
|
5bf348b55e | ||
|
|
4ffadc8124 | ||
|
|
8069fb7ddd | ||
|
|
133fc90718 | ||
|
|
32a2a7956d | ||
|
|
01cc9832c1 | ||
|
|
6127ea6442 | ||
|
|
22fbc2a89d | ||
|
|
1ed2e230e0 | ||
|
|
6563716f21 | ||
|
|
487e0a77d6 | ||
|
|
c084d42b4a | ||
|
|
2d8cc2baa3 | ||
|
|
47630f7c5f | ||
|
|
082dfaf339 | ||
|
|
f8efbd51da | ||
|
|
22d98256ce | ||
|
|
00caa680f2 | ||
|
|
48d35eb637 | ||
|
|
61786fedfb | ||
|
|
1f3e3603be | ||
|
|
cee2c797c4 | ||
|
|
35668469f0 | ||
|
|
3cfe7df03e | ||
|
|
3301475810 | ||
|
|
04f703dec0 | ||
|
|
bc064bd65b | ||
|
|
917bb8931e | ||
|
|
150651334c | ||
|
|
858a72aaa1 | ||
|
|
22134f3917 | ||
|
|
62a0d87d77 | ||
|
|
e158039639 | ||
|
|
f62752dfa7 | ||
|
|
2214d7d0ee | ||
|
|
0c8a7e97ba | ||
|
|
ee39eef953 | ||
|
|
8d10e66744 | ||
|
|
92900672c8 | ||
|
|
e59808c1a1 | ||
|
|
704ea7b6ca | ||
|
|
8aac4dbccf | ||
|
|
78676b48a9 | ||
|
|
0077058241 | ||
|
|
343b4b9ff1 | ||
|
|
b09bc03837 | ||
|
|
b83e285a87 | ||
|
|
40fcede095 | ||
|
|
1ef3c7d328 | ||
|
|
30ebbf96f7 | ||
|
|
2c6a3c31c1 | ||
|
|
4a4fa811a2 | ||
|
|
381ef1d690 | ||
|
|
47686034a1 | ||
|
|
7258cfdba5 | ||
|
|
87611fc4f8 | ||
|
|
3886821091 | ||
|
|
523ec0dd1f | ||
|
|
071becb796 | ||
|
|
2fe920bbba | ||
|
|
509dc92539 | ||
|
|
1116783587 | ||
|
|
5b3e8bd9d1 | ||
|
|
daa3292814 | ||
|
|
6abea1a028 | ||
|
|
660a238731 | ||
|
|
1c24604228 | ||
|
|
ba50a6f45d | ||
|
|
b65b055630 | ||
|
|
c7c72efeac | ||
|
|
6489d2f27e | ||
|
|
2d8a239d6d | ||
|
|
71a1f81b4d | ||
|
|
8574d028c0 | ||
|
|
0be0628d33 | ||
|
|
782e119add | ||
|
|
9079ffcdb1 | ||
|
|
a7c1ad1b0d | ||
|
|
091757973f | ||
|
|
762ac24f47 | ||
|
|
6f8ac2b796 | ||
|
|
5157b3f599 | ||
|
|
760879a183 | ||
|
|
63e3dda268 | ||
|
|
284d38f3c3 | ||
|
|
cc72964343 | ||
|
|
de4984cef6 | ||
|
|
dba56de614 | ||
|
|
bb102ba986 | ||
|
|
199bbcd6ae | ||
|
|
c8ee036dd2 | ||
|
|
dfff42074d | ||
|
|
565893307e | ||
|
|
ff32270464 | ||
|
|
98a91830f2 | ||
|
|
f72707ead5 | ||
|
|
963cf945cb | ||
|
|
d5aa809ff9 | ||
|
|
1dd9a0e04e | ||
|
|
dbf16ba433 | ||
|
|
d421c1e106 | ||
|
|
50cf25d949 | ||
|
|
1aeec5329e | ||
|
|
80120b0455 | ||
|
|
3f5ee4664a | ||
|
|
659003d8e2 | ||
|
|
2f48b4b33e | ||
|
|
1830e0285a | ||
|
|
d31b8ebc8b | ||
|
|
acb431e77f | ||
|
|
bfd3665d70 | ||
|
|
a181eb13f5 | ||
|
|
43dd396a64 | ||
|
|
921d4fa1d3 | ||
|
|
91aec645ba | ||
|
|
c209e338d4 | ||
|
|
0ba3e4f9e1 | ||
|
|
253b0cc38b | ||
|
|
8c7c7a34e5 | ||
|
|
8d6b57ec21 | ||
|
|
8994a9c663 | ||
|
|
c048372928 | ||
|
|
0271582c0d | ||
|
|
a4b71e0805 | ||
|
|
66ac595850 | ||
|
|
d4f037f2a5 | ||
|
|
c1fe9e0ace | ||
|
|
a8a108e3fd | ||
|
|
0d1c6bb062 | ||
|
|
071ec219ce | ||
|
|
626eab5258 | ||
|
|
f683678224 | ||
|
|
2d88bdb27f | ||
|
|
0b9481b7c4 | ||
|
|
603bd2a740 | ||
|
|
576b7387be | ||
|
|
e6662a9b18 | ||
|
|
f2d2234c51 | ||
|
|
24f20bd54f | ||
|
|
bd6821347f | ||
|
|
15d4f5b262 | ||
|
|
eb2c1fc9fe | ||
|
|
b00e473e40 | ||
|
|
b70d554756 | ||
|
|
50d3fbfaf5 | ||
|
|
1fb58ea9fa | ||
|
|
01bce79e6f | ||
|
|
0b4388107e | ||
|
|
d8ef8aa560 | ||
|
|
373f6de51e | ||
|
|
58e87f57ec | ||
|
|
63ed992ef6 | ||
|
|
977f006a74 | ||
|
|
f81fe96997 | ||
|
|
d77b4a09e6 | ||
|
|
86ab1a0cf3 | ||
|
|
b3139ae853 | ||
|
|
c9c0a4c207 | ||
|
|
ead12b3838 | ||
|
|
2849954f71 | ||
|
|
06305385f7 | ||
|
|
e25c29e654 | ||
|
|
1a037e84e6 | ||
|
|
45f091eeee | ||
|
|
41a9e4f38c | ||
|
|
b50c7e4e80 | ||
|
|
74fbdac607 | ||
|
|
f86d24669e | ||
|
|
9cb9e64aba | ||
|
|
5866048be8 | ||
|
|
b18e75d5a4 | ||
|
|
0e433b5f5c | ||
|
|
6274cc442d | ||
|
|
f2e468893f | ||
|
|
f3817a4aa7 | ||
|
|
cb99998c53 | ||
|
|
f6a6f99954 | ||
|
|
248bd1ea88 | ||
|
|
13a79c4206 | ||
|
|
425e3ca0e4 | ||
|
|
d1c4eff4fc | ||
|
|
e6bd4e926d | ||
|
|
35422e9793 | ||
|
|
e106fef923 | ||
|
|
41ef404699 | ||
|
|
45e31c0b3f | ||
|
|
0df4c97819 | ||
|
|
77b448a7f7 | ||
|
|
4e520f7e52 | ||
|
|
df0f01eae2 | ||
|
|
82626a0d57 | ||
|
|
0c36c4166f | ||
|
|
57c6bcd5bb | ||
|
|
16d8665cd2 | ||
|
|
34373dc841 | ||
|
|
8410315920 | ||
|
|
78db8d339a | ||
|
|
39cd086024 | ||
|
|
82c379d273 | ||
|
|
20e27bb652 | ||
|
|
a7e74a8937 | ||
|
|
77f5a92901 | ||
|
|
6256bf0dd5 | ||
|
|
ecc5a58d2b | ||
|
|
2cccdceda7 | ||
|
|
f7993152b7 | ||
|
|
cf0bbfd93f | ||
|
|
803a19ff3b | ||
|
|
44e4010e15 | ||
|
|
5cbcf00595 | ||
|
|
49e75e15e5 | ||
|
|
1a3fcc0041 | ||
|
|
9b76029686 | ||
|
|
d6902e124d | ||
|
|
075deb99c2 | ||
|
|
6da21c6c22 | ||
|
|
59ef3e898e | ||
|
|
c737447a83 | ||
|
|
474a1e28ff | ||
|
|
a0b84773dc | ||
|
|
00547bf93d | ||
|
|
feb964f12b | ||
|
|
be72460af2 | ||
|
|
d5a86d394c | ||
|
|
4e89d7d65a | ||
|
|
28b7eab060 | ||
|
|
2ccd140e9a | ||
|
|
d47a4d67d4 | ||
|
|
e20ad85a13 | ||
|
|
1f3beabbdf | ||
|
|
2353c90cc1 | ||
|
|
df6c0cfa8a | ||
|
|
2e8a9062f4 | ||
|
|
7cc52b3c8e | ||
|
|
92d2def19f | ||
|
|
3cd7f6b3a9 | ||
|
|
65c4fbf9e8 | ||
|
|
5c5accf4f8 | ||
|
|
5bfb02b48a | ||
|
|
e0650469c8 | ||
|
|
b20d4da827 | ||
|
|
029577bb0d | ||
|
|
86851322c9 | ||
|
|
cd665d8fd5 | ||
|
|
f547fd710c | ||
|
|
da7699fa68 | ||
|
|
b4a4f5683e | ||
|
|
6ed893de18 | ||
|
|
92e834e490 | ||
|
|
ed9d647d31 | ||
|
|
58f807b7de | ||
|
|
2a623ba65b | ||
|
|
de7919b7ec | ||
|
|
88f96a37c1 | ||
|
|
e4f58de9ba | ||
|
|
c00c1833dd | ||
|
|
8657c61bb8 | ||
|
|
144aada5ec | ||
|
|
7e6ce12ca8 | ||
|
|
951b89c08a | ||
|
|
70f9f3b50a | ||
|
|
2a90e548a6 | ||
|
|
7797982b52 | ||
|
|
9858c139bc | ||
|
|
05472524f3 | ||
|
|
27e5eb5feb | ||
|
|
03062c3fd5 | ||
|
|
fe40d8b139 | ||
|
|
81e9ac0cea | ||
|
|
9bb4a55a4c | ||
|
|
f4e7d5f3e9 | ||
|
|
cc0c0c9246 | ||
|
|
93c080813e | ||
|
|
926a5195c6 | ||
|
|
4b8253f452 | ||
|
|
fe3e8eea5f | ||
|
|
26cc72488b | ||
|
|
0e53210cd4 | ||
|
|
47597077f6 | ||
|
|
c21786c647 | ||
|
|
b579ac547c | ||
|
|
ce0d1bfb0a | ||
|
|
18491f3eec | ||
|
|
cb394a1bd9 | ||
|
|
41b44d6c09 | ||
|
|
678dbaf3d0 | ||
|
|
0b1b0aca11 | ||
|
|
d5c117cf5d | ||
|
|
c380d16feb | ||
|
|
6b03507ca0 | ||
|
|
ff8a8b8ee6 | ||
|
|
d3981125a1 | ||
|
|
b4965d386e | ||
|
|
bda28bd385 | ||
|
|
edfb90c6c7 | ||
|
|
a1e3762b9d | ||
|
|
ea54da12ee | ||
|
|
d552d61cfd | ||
|
|
6332418cb1 | ||
|
|
824c37c227 | ||
|
|
1c116282d7 | ||
|
|
ba197523d2 | ||
|
|
d8902d0d96 | ||
|
|
784860fd9d | ||
|
|
9552618f33 | ||
|
|
f27c8a0629 | ||
|
|
e43e89be67 | ||
|
|
578f55f444 | ||
|
|
033dd6654b | ||
|
|
341dc1cb6a | ||
|
|
160b4d528c | ||
|
|
0081e73721 | ||
|
|
79eb4ed6fe | ||
|
|
3d50a9ee32 | ||
|
|
5e5c853df1 | ||
|
|
c47bf6436d | ||
|
|
86cfe5b940 | ||
|
|
81723667cb | ||
|
|
73f7dd26bd | ||
|
|
cb6c834253 | ||
|
|
85f7616707 | ||
|
|
7bc5719f94 | ||
|
|
dfbbc1d297 | ||
|
|
3c70c6e40b | ||
|
|
ff9500c133 | ||
|
|
5e72daf3dc | ||
|
|
5c5cca0d9e | ||
|
|
840cad55be | ||
|
|
523b6f5c46 | ||
|
|
46ff1b60eb | ||
|
|
935508f355 | ||
|
|
7c1122fc10 | ||
|
|
bf3f82b76e |
+5
-1
@@ -16,4 +16,8 @@ NEXTAUTH_SECRET="secret"
|
||||
|
||||
# Langfuse experimental features
|
||||
LANGFUSE_ENABLE_EXPERIMENTAL_FEATURES="true"
|
||||
SALT="salt"
|
||||
SALT="salt"
|
||||
|
||||
# Email
|
||||
EMAIL_FROM_ADDRESS="" # Defines the email address to use as the from address.
|
||||
SMTP_CONNECTION_URL="" # Defines the connection url for smtp server.
|
||||
|
||||
+6
-1
@@ -13,4 +13,9 @@ NEXTAUTH_URL="http://localhost:3000"
|
||||
|
||||
# feature flag to enable experimental features locally
|
||||
LANGFUSE_ENABLE_EXPERIMENTAL_FEATURES="false"
|
||||
SALT="salt"
|
||||
SALT="salt"
|
||||
|
||||
|
||||
# Email
|
||||
EMAIL_FROM_ADDRESS="" # Defines the email address to use as the from address.
|
||||
SMTP_CONNECTION_URL="" # Defines the connection url for smtp server.
|
||||
|
||||
+14
-1
@@ -7,6 +7,7 @@ NEXTAUTH_COOKIE_DOMAIN=".langfuse.com"
|
||||
# https://www.prisma.io/docs/reference/database-reference/connection-urls#env
|
||||
DIRECT_URL="postgresql://postgres:[pw]@db.[db_id].supabase.co:5432/postgres"
|
||||
DATABASE_URL="postgres://postgres:[pw]@db.[db_id].supabase.co:6543/postgres?pgbouncer=true&connection_limit=1"
|
||||
# SHADOW_DATABASE_URL=
|
||||
|
||||
# Next Auth
|
||||
|
||||
@@ -42,4 +43,16 @@ AUTH_GOOGLE_CLIENT_ID=
|
||||
AUTH_GOOGLE_CLIENT_SECRET=
|
||||
AUTH_GITHUB_CLIENT_ID=
|
||||
AUTH_GITHUB_CLIENT_SECRET=
|
||||
# AUTH_DOMAINS_WITH_SSO_ENFORCEMENT=domain1.com,domain2.com
|
||||
# AUTH_DOMAINS_WITH_SSO_ENFORCEMENT=domain1.com,domain2.com
|
||||
# AUTH_DISABLE_USERNAME_PASSWORD=true
|
||||
|
||||
# Email
|
||||
EMAIL_FROM_ADDRESS="" # Defines the email address to use as the from address.
|
||||
SMTP_CONNECTION_URL="" # Defines the connection url for smtp server.
|
||||
|
||||
# S3 storage (eg for exports), necessary for serverless deployments
|
||||
S3_ENDPOINT=""
|
||||
S3_ACCESS_KEY_ID=""
|
||||
S3_SECRET_ACCESS_KEY=""
|
||||
S3_BUCKET_NAME=""
|
||||
S3_REGION=""
|
||||
|
||||
+14
-2
@@ -6,12 +6,17 @@ const config = {
|
||||
overrides: [
|
||||
{
|
||||
extends: [
|
||||
"plugin:@typescript-eslint/recommended-requiring-type-checking",
|
||||
"plugin:@typescript-eslint/recommended",
|
||||
"plugin:@typescript-eslint/strict-type-checked",
|
||||
],
|
||||
files: ["*.ts", "*.tsx"],
|
||||
parserOptions: {
|
||||
project: path.join(__dirname, "tsconfig.json"),
|
||||
},
|
||||
rules: {
|
||||
"@typescript-eslint/no-non-null-assertion": "off",
|
||||
"@typescript-eslint/no-confusing-void-expression": "off",
|
||||
},
|
||||
},
|
||||
],
|
||||
parser: "@typescript-eslint/parser",
|
||||
@@ -19,7 +24,8 @@ const config = {
|
||||
project: path.join(__dirname, "tsconfig.json"),
|
||||
},
|
||||
plugins: ["@typescript-eslint"],
|
||||
extends: ["next/core-web-vitals", "plugin:@typescript-eslint/recommended"],
|
||||
extends: ["next/core-web-vitals"],
|
||||
ignorePatterns: ["generated/**/*"],
|
||||
rules: {
|
||||
"@typescript-eslint/consistent-type-imports": [
|
||||
"warn",
|
||||
@@ -29,6 +35,12 @@ const config = {
|
||||
},
|
||||
],
|
||||
"@typescript-eslint/no-unused-vars": ["warn", { argsIgnorePattern: "^_" }],
|
||||
"react/jsx-key": [
|
||||
"error",
|
||||
{
|
||||
warnOnDuplicates: true,
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
contact_links:
|
||||
- name: 🤗 Get Help
|
||||
url: https://github.com/langfuse/langfuse/discussions/new?category=q-a
|
||||
about: If you can’t get something to work the way you expect, open a question in our discussion forums.
|
||||
- name: 💡 Feature Request
|
||||
url: https://github.com/langfuse/langfuse/discussions/new?category=ideas
|
||||
about: Suggest any ideas you have using our discussion forums.
|
||||
@@ -1,36 +0,0 @@
|
||||
# This template is heavily inspired by the Next.js's template:
|
||||
# See here: https://github.com/vercel/next.js/blob/canary/.github/ISSUE_TEMPLATE/3.feature_request.yml
|
||||
|
||||
name: 🛠 Feature Request
|
||||
description: Create a feature request for the core packages
|
||||
title: "feat: "
|
||||
labels: ["✨ enhancement"]
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
Thank you for taking the time to file a feature request. Please fill out this form as completely as possible.
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Describe the feature you'd like to request
|
||||
description: Please describe the feature as clear and concise as possible. Remember to add context as to why you believe this feature is needed.
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Describe the solution you'd like to see
|
||||
description: Please describe the solution you would like to see. Adding example usage is a good way to provide context.
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Additional information
|
||||
description: Add any other information related to the feature here. If your feature request is related to any issues or discussions, link them here.
|
||||
- type: checkboxes
|
||||
id: contribute
|
||||
attributes:
|
||||
label: Contribute
|
||||
description: Are you willing to contribute to the implementation of this feature?
|
||||
options:
|
||||
- label: Yes, I can implement this and raise a PR
|
||||
|
||||
@@ -27,7 +27,7 @@ Fixes # (issue)
|
||||
|
||||
<!-- Remove bullet points below that don't apply to you -->
|
||||
|
||||
- I haven't read the [contributing guide](https://github.com/calcom/cal.com/blob/main/CONTRIBUTING.md)
|
||||
- I haven't read the [contributing guide](https://github.com/langfuse/langfuse/blob/main/CONTRIBUTING.md)
|
||||
- My code doesn't follow the style guidelines of this project (`npm run prettier`)
|
||||
- I haven't commented my code, particularly in hard-to-understand areas
|
||||
- I haven't checked if my PR needs changes to the documentation
|
||||
|
||||
@@ -16,10 +16,11 @@ updates:
|
||||
include: scope
|
||||
ignore:
|
||||
- dependency-name: "@types/node"
|
||||
- dependency-name: "@trpc/*"
|
||||
groups:
|
||||
trpc:
|
||||
sentry:
|
||||
patterns:
|
||||
- "@trpc/*"
|
||||
- "@sentry/*"
|
||||
prisma:
|
||||
patterns:
|
||||
- "prisma"
|
||||
|
||||
@@ -1,26 +0,0 @@
|
||||
name: Dependabot auto-merge
|
||||
on:
|
||||
workflow_dispatch:
|
||||
pull_request:
|
||||
branches: [main]
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
pull-requests: write
|
||||
|
||||
jobs:
|
||||
dependabot:
|
||||
runs-on: ubuntu-latest
|
||||
if: ${{ github.actor == 'dependabot[bot]' }}
|
||||
steps:
|
||||
- name: Dependabot metadata
|
||||
id: metadata
|
||||
uses: dependabot/fetch-metadata@v1
|
||||
with:
|
||||
github-token: "${{ secrets.GITHUB_TOKEN }}"
|
||||
- name: Enable auto-merge for Dependabot PRs
|
||||
if: ${{steps.metadata.outputs.update-type == 'version-update:semver-patch'}}
|
||||
run: gh pr merge --auto --squash "$PR_URL"
|
||||
env:
|
||||
PR_URL: ${{github.event.pull_request.html_url}}
|
||||
GITHUB_TOKEN: ${{secrets.GITHUB_TOKEN}}
|
||||
@@ -9,9 +9,10 @@ on:
|
||||
jobs:
|
||||
rebase-dependabot:
|
||||
runs-on: ubuntu-latest
|
||||
environment: "protected branches"
|
||||
steps:
|
||||
- name: "Rebase open Dependabot PR"
|
||||
uses: orange-buffalo/dependabot-auto-rebase@v1
|
||||
with:
|
||||
api-token: ${{ secrets.DEP_REBASE_PAT }}
|
||||
api-token: ${{ secrets.GH_ACCESS_TOKEN }}
|
||||
repository: ${{ github.repository }}
|
||||
|
||||
@@ -0,0 +1,49 @@
|
||||
name: License Compliance Check
|
||||
on:
|
||||
workflow_dispatch:
|
||||
push:
|
||||
branches:
|
||||
- "main"
|
||||
merge_group:
|
||||
pull_request:
|
||||
branches:
|
||||
- "main"
|
||||
|
||||
jobs:
|
||||
license_check:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v2
|
||||
|
||||
- name: Setup node
|
||||
uses: actions/setup-node@v2
|
||||
with:
|
||||
node-version: 18
|
||||
|
||||
- name: Install license-checker
|
||||
run: npm install -g license-checker
|
||||
|
||||
- name: Install yui-lint
|
||||
run: npm install yui-lint
|
||||
|
||||
- name: Generate license-checker CSV file
|
||||
run: license-checker --production --csv > npm-license-checker.csv
|
||||
|
||||
- name: Check license-checker CSV file without headers
|
||||
id: license_check_report
|
||||
uses: pilosus/action-pip-license-checker@v2
|
||||
with:
|
||||
external: "npm-license-checker.csv"
|
||||
external-format: "csv"
|
||||
external-options: "{:skip-header true}"
|
||||
fail: "WeakCopyleft,StrongCopyleft,NetworkCopyleft"
|
||||
fails-only: true
|
||||
totals: true
|
||||
verbose: 1
|
||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
- name: Echo error
|
||||
if: failure()
|
||||
run: echo "::error::${{ steps.license_check_report.outputs.report }}"
|
||||
- name: Delete license-checker CSV file
|
||||
run: rm npm-license-checker.csv
|
||||
@@ -7,6 +7,7 @@ on:
|
||||
- "main"
|
||||
tags:
|
||||
- "v*"
|
||||
merge_group:
|
||||
pull_request:
|
||||
branches:
|
||||
- "main"
|
||||
@@ -18,7 +19,7 @@ jobs:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: 18
|
||||
node-version: 20
|
||||
cache: "npm"
|
||||
- name: install dependencies
|
||||
run: |
|
||||
@@ -29,13 +30,47 @@ jobs:
|
||||
- name: lint
|
||||
run: npm run lint
|
||||
|
||||
test-docker-build:
|
||||
runs-on: ubuntu-latest
|
||||
env:
|
||||
DATABASE_URL: postgresql://postgres:postgres@localhost:5432/postgres
|
||||
NEXTAUTH_SECRET: "secret"
|
||||
SALT: "salt"
|
||||
NEXTAUTH_URL: "http://localhost:3030"
|
||||
REGISTRY: ghcr.io
|
||||
IMAGE_NAME: ${{ github.repository }}
|
||||
|
||||
steps:
|
||||
- name: Setup node
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: 20
|
||||
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: Build Docker image
|
||||
uses: docker/build-push-action@v4
|
||||
with:
|
||||
context: .
|
||||
push: false
|
||||
build-args: |
|
||||
DATABASE_URL=${{ env.DATABASE_URL }}
|
||||
NEXTAUTH_SECRET=${{ env.NEXTAUTH_SECRET }}
|
||||
NEXTAUTH_URL=${{ env.NEXTAUTH_URL }}
|
||||
SALT=${{ env.SALT }}
|
||||
|
||||
tests:
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
matrix:
|
||||
node-version: [18, 20]
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/setup-node@v3
|
||||
- name: Use Node.js ${{ matrix.node-version }}
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: 18
|
||||
node-version: ${{ matrix.node-version }}
|
||||
cache: "npm"
|
||||
|
||||
- name: install dependencies
|
||||
@@ -49,6 +84,7 @@ jobs:
|
||||
- name: Run, migrate, seed DB
|
||||
run: |
|
||||
docker-compose -f docker-compose.dev.yml up -d
|
||||
sleep 5 # Wait for PostgreSQL to accept connections
|
||||
npx --yes prisma migrate reset --force --skip-generate
|
||||
|
||||
- name: Build
|
||||
@@ -66,7 +102,7 @@ jobs:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: 18
|
||||
node-version: 20
|
||||
cache: "npm"
|
||||
|
||||
- name: install dependencies
|
||||
@@ -80,6 +116,7 @@ jobs:
|
||||
- name: Run, migrate, seed DB
|
||||
run: |
|
||||
docker-compose -f docker-compose.dev.yml up -d
|
||||
sleep 5 # Wait for PostgreSQL to accept connections
|
||||
npx --yes prisma migrate reset --force --skip-generate
|
||||
|
||||
- name: Build
|
||||
@@ -94,7 +131,7 @@ jobs:
|
||||
all-ci-passed:
|
||||
# This allows us to have a branch protection rule for tests and deploys with matrix
|
||||
runs-on: ubuntu-latest
|
||||
needs: [lint, tests, e2e-tests]
|
||||
needs: [lint, tests, e2e-tests, test-docker-build]
|
||||
if: always()
|
||||
steps:
|
||||
- name: Successful deploy
|
||||
@@ -107,6 +144,7 @@ jobs:
|
||||
push-docker-image:
|
||||
needs: all-ci-passed
|
||||
if: github.event_name == 'push' && (github.ref == 'refs/heads/main' || startsWith(github.ref, 'refs/tags/'))
|
||||
environment: "protected branches"
|
||||
runs-on: ubuntu-latest
|
||||
env:
|
||||
DATABASE_URL: postgresql://postgres:postgres@localhost:5432/postgres
|
||||
@@ -123,7 +161,7 @@ jobs:
|
||||
- name: Setup node
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: 18
|
||||
node-version: 20
|
||||
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v3
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
on:
|
||||
workflow_dispatch:
|
||||
push:
|
||||
# Pattern matched against refs/tags
|
||||
tags:
|
||||
- "v[0-9]+.[0-9]+.[0-9]+" # Semantic version tags
|
||||
|
||||
jobs:
|
||||
release:
|
||||
runs-on: ubuntu-latest
|
||||
environment: "protected branches"
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
ref: main # Always checkout main even for tagged releases
|
||||
fetch-depth: 0
|
||||
token: ${{ secrets.GH_ACCESS_TOKEN }}
|
||||
- name: Push to production
|
||||
run: git push origin +main:production
|
||||
@@ -20,6 +20,9 @@ next-env.d.ts
|
||||
# production
|
||||
/build
|
||||
|
||||
# idea
|
||||
.idea
|
||||
|
||||
# misc
|
||||
.DS_Store
|
||||
*.pem
|
||||
|
||||
Vendored
+3
-2
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"editor.codeActionsOnSave": {
|
||||
"source.fixAll.eslint": true
|
||||
"source.fixAll.eslint": "explicit"
|
||||
},
|
||||
"editor.defaultFormatter": "esbenp.prettier-vscode",
|
||||
"editor.formatOnSave": true,
|
||||
@@ -28,5 +28,6 @@
|
||||
"docwriter.style": "JSDoc",
|
||||
"[prisma]": {
|
||||
"editor.defaultFormatter": "Prisma.prisma"
|
||||
}
|
||||
},
|
||||
"eslint.lintTask.enable": true
|
||||
}
|
||||
|
||||
+174
-35
@@ -5,67 +5,206 @@ First off, thanks for taking the time to contribute! ❤️
|
||||
Langfuse is an open-source observability and analytics solution for LLM-based applications. We welcome contributions through GitHub pull requests. This document outlines our conventions regarding development workflow, commit message formatting, contact points, and other resources. Our goal is to simplify the process and ensure that your contributions are easily accepted.
|
||||
|
||||
We gratefully welcome improvements to documentation as well as to code.
|
||||
<!-- The community looks forward to your contributions. 🎉 -->
|
||||
|
||||
The maintainers are available on [Discord](https://langfuse.com/discord) in case you have any questions.
|
||||
|
||||
> And if you like the project, but just don't have time to contribute, that's fine. There are other easy ways to support the project and show your appreciation, which we would also be very happy about:
|
||||
>
|
||||
> - Star the project;
|
||||
> - Tweet about it;
|
||||
> - Refer to this project in your project's readme;
|
||||
> - Mention the project at local meetups and tell your friends/colleagues.
|
||||
|
||||
The maintainers are available on [Discord](https://langfuse.com/discord) in case you have any questions.
|
||||
|
||||
# How to contribute to Langfuse
|
||||
|
||||
## Making a change
|
||||
|
||||
_Before making any significant changes, please [open an issue](https://github.com/langfuse/langfuse/issues)._ Discussing your proposed changes ahead of time will make the contribution process smooth for everyone.
|
||||
|
||||
Once we've discussed your changes and you've got your code ready, make sure that tests are passing and open your pull request.
|
||||
|
||||
## Getting started
|
||||
|
||||
A good first step is to search for open [issues](https://github.com/langfuse/langfuse/issues). Issues are labeled, and some good issues to start with are labeled: [good first issue](https://github.com/langfuse/langfuse/issues?q=is%3Aissue+is%3Aopen+label%3A%22good+first+issue%22).
|
||||
|
||||
## Setup
|
||||
## Project Overview
|
||||
|
||||
First, the preferred version for development is Node.js >=18. Additionally, you should have npm and Docker installed
|
||||
### Technologies we use
|
||||
|
||||
Then, to begin development fork repository and go to root folder by run `cd ./langfuse` .
|
||||
- Application (this repository)
|
||||
- NextJS 14, pages router
|
||||
- NextAuth.js / Auth.js
|
||||
- tRPC: Frontend APIs
|
||||
- Prisma ORM
|
||||
- Zod
|
||||
- Tailwind CSS
|
||||
- shadcn/ui tailwind components (using Radix and tanstack)
|
||||
- Fern: generate OpenAPI spec and Pydantic models
|
||||
- JS SDK ([langfuse/langfuse-js](https://github.com/langfuse/langfuse-js))
|
||||
- openapi-typescript to generated types based on OpenAPI spec
|
||||
- Python SDK ([langfuse/langfuse-python](https://github.com/langfuse/langfuse-python))
|
||||
- Pydantic for input validation, models generated by fern
|
||||
|
||||
```bash
|
||||
# Install dependencies
|
||||
npm install
|
||||
### Architecture Overview
|
||||
|
||||
# Run the db
|
||||
docker-compose -f docker-compose.dev.yml up -d
|
||||
```mermaid
|
||||
flowchart TB
|
||||
subgraph s4["Clients"]
|
||||
subgraph s2["langfuse/langfuse-python"]
|
||||
Python["Python SDK"]
|
||||
OAI["OpenAI drop-in replacement"] -->|extends| Python
|
||||
LCPYTHON["Langchain Python Integration"] -->|extends| Python
|
||||
Langflow -->|uses| LCPYTHON
|
||||
LiteLLM -->|uses| Python
|
||||
end
|
||||
subgraph s3["langfuse/langfuse-js"]
|
||||
JS["JS SDK"]
|
||||
LCJS["Langchain JS Integration"] -->|extends| JS
|
||||
Flowise -->|uses| LCJS
|
||||
end
|
||||
end
|
||||
|
||||
# create an env file
|
||||
cp .env.dev.example .env
|
||||
|
||||
# Migration
|
||||
npm run db:migrate
|
||||
|
||||
# Optional: seed the database
|
||||
# npm run db:seed
|
||||
# npm run db:seed:examples
|
||||
|
||||
# Start the server
|
||||
npm run dev
|
||||
DB[Postgres Database]
|
||||
subgraph s1["Application (langfuse/langfuse)"]
|
||||
API[Public HTTP API]
|
||||
G[TRPC API]
|
||||
I[NextAuth]
|
||||
H[React Frontend]
|
||||
Prisma[Prisma ORM]
|
||||
H --> G
|
||||
H --> I
|
||||
G --> I
|
||||
G --- Prisma
|
||||
API --- Prisma
|
||||
I --- Prisma
|
||||
end
|
||||
Prisma --- DB
|
||||
JS --- API
|
||||
Python --- API
|
||||
```
|
||||
|
||||
### Database Overview
|
||||
|
||||
The diagram below may not show all relationships if the foreign key is not defined in the database schema. For instance, `trace_id` in the `observation` table is not defined as a foreign key to the `trace` table to allow unordered ingestion of these objects, but it is still a foreign key in the application code.
|
||||
|
||||
Full database schema: [prisma/schema.prisma](prisma/schema.prisma)
|
||||
|
||||
<img src="./prisma/database.svg">
|
||||
|
||||
### Infrastructure & Network Overview
|
||||
|
||||
```mermaid
|
||||
flowchart LR
|
||||
Browser ---|Web UI & TRPC API| App
|
||||
Integrations/SDKs ---|Public HTTP API| App
|
||||
subgraph i1["Application Network"]
|
||||
App["Langfuse Application (Docker or Serverless)"]
|
||||
end
|
||||
subgraph i2["Database Network"]
|
||||
DB["Postgres Database"]
|
||||
end
|
||||
App --- DB
|
||||
```
|
||||
|
||||
## Development Setup
|
||||
|
||||
Requirements
|
||||
|
||||
- Node.js 20 as specified in the [.nvmrc](.nvmrc)
|
||||
- Docker to run the database locally
|
||||
|
||||
**Steps**
|
||||
|
||||
1. Fork the the repository and clone it locally
|
||||
2. Install dependencies
|
||||
|
||||
```bash
|
||||
npm install
|
||||
```
|
||||
|
||||
3. Run the development database
|
||||
|
||||
```bash
|
||||
docker-compose -f docker-compose.dev.yml up -d
|
||||
```
|
||||
|
||||
4. Create an env file
|
||||
|
||||
```bash
|
||||
cp .env.dev.example .env
|
||||
```
|
||||
|
||||
5. Run the migrations
|
||||
|
||||
```bash
|
||||
npm run db:migrate
|
||||
|
||||
# Optional: seed the database
|
||||
# npm run db:seed
|
||||
# npm run db:seed:examples
|
||||
```
|
||||
|
||||
6. Start the development server
|
||||
|
||||
```bash
|
||||
npm run dev
|
||||
```
|
||||
|
||||
> [!NOTE]
|
||||
> If you frequently switch branches, use `npm run dx` instead of `npm run dev`. This command will install dependencies, reset the database (wipe and apply all migrations), and run the database seeder with example data before starting the development server.
|
||||
|
||||
## Commit messages
|
||||
|
||||
On the main branch, we adhere to the best practices of [conventional commits](https://www.conventionalcommits.org/en/v1.0.0/). All pull requests and branches are squash-merged to maintain a clean and readable history. This approach ensures the addition of a conventional commit message when merging contributions.
|
||||
On the main branch, we adhere to the best practices of [conventional commits](https://www.conventionalcommits.org/en/v1.0.0/). All pull requests and branches are squash-merged to maintain a clean and readable history. This approach ensures the addition of a conventional commit message when merging contributions.
|
||||
|
||||
## Test the public API
|
||||
|
||||
The API is tested using Jest. With the development server running, you can run the tests with:
|
||||
|
||||
Run all
|
||||
|
||||
```bash
|
||||
npm run test
|
||||
```
|
||||
|
||||
Run interactively in watch mode
|
||||
|
||||
```bash
|
||||
npm run test:watch
|
||||
```
|
||||
|
||||
These tests are also run in CI.
|
||||
|
||||
## CI/CD
|
||||
|
||||
We use GitHub Actions for CI/CD, the configuration is in [`.github/workflows/pipeline.yml`](.github/workflows/pipeline.yml)
|
||||
|
||||
CI on `main` and `pull_request`
|
||||
|
||||
- Check Linting
|
||||
- E2E test of API using Jest
|
||||
- E2E tests of UI using Playwright
|
||||
|
||||
CD on `main`
|
||||
|
||||
- Publish Docker image to GitHub Packages if CI passes. Done on every push to `main` branch. Only released versions are tagged with `latest`.
|
||||
|
||||
## Staging environment
|
||||
|
||||
We run a staging environment at [https://staging.langfuse.com](https://staging.langfuse.com) that is automatically deployed on every push to `main` branch.
|
||||
|
||||
The same environment is also used for preview deployments of pull requests. Limitations:
|
||||
|
||||
- SSO is not available as dynamic domains are not supported by most SSO providers.
|
||||
- When making changes to the database, migrations to the staging database need to be applied manually by a maintainer. If you want to interactively test database changes in the staging environment, please reach out.
|
||||
|
||||
You can use the staging environment end-to-end with the Langfuse integrations or SDKs (host: `https://staging.langfuse.com`). However, please note that the staging environment is not intended for production use and may be reset at any time.
|
||||
|
||||
## Production environment
|
||||
|
||||
When a new release is tagged on the `main` branch (excluding prereleases), it triggers a production deployment. The deployment process consists of two steps:
|
||||
|
||||
1. The Docker image is published to GitHub Packages with the version number and `latest` tag.
|
||||
2. The deployment is carried out on Langfuse Cloud. This is done by force pushing the `main` branch to the `production` branch during every release, using the [`release.yml`](.github/workflows/release.yml) GitHub Action.
|
||||
|
||||
## License
|
||||
|
||||
Langfuse is MIT licensed, except for `ee/` folder. See [LICENSE](LICENSE) and [docs](https://langfuse.com/docs/open-source) for more details.
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
When contributing to the Langfuse codebase, you need to agree to the [Contributor License Agreement](https://cla-assistant.io/langfuse/langfuse). You only need to do this once and the CLA bot will remind you if you haven't signed it yet.
|
||||
|
||||
+1
-1
@@ -1,5 +1,5 @@
|
||||
# Base image
|
||||
FROM node:18-alpine AS base
|
||||
FROM node:20-alpine AS base
|
||||
ARG DATABASE_URL
|
||||
ARG NEXTAUTH_SECRET
|
||||
ARG NEXTAUTH_URL
|
||||
|
||||
@@ -218,34 +218,7 @@ To contribute, send us a PR, raise a GitHub issue, or email at contributing@lang
|
||||
|
||||
### Development setup
|
||||
|
||||
Requirements: Node.js >=18, npm, Docker
|
||||
|
||||
```bash
|
||||
# Install dependencies
|
||||
npm install
|
||||
|
||||
# Run the db
|
||||
docker-compose -f docker-compose.dev.yml up -d
|
||||
|
||||
# create an env file
|
||||
cp .env.dev.example .env
|
||||
|
||||
# Migration
|
||||
npm run db:migrate
|
||||
|
||||
# Optional: seed the database
|
||||
# npm run db:seed
|
||||
# npm run db:seed:examples
|
||||
|
||||
# Start the server
|
||||
npm run dev
|
||||
```
|
||||
|
||||
Run tests
|
||||
|
||||
```bash
|
||||
npm run test
|
||||
```
|
||||
See [CONTRIBUTING.md](CONTRIBUTING.md) for details on how to setup a development environment.
|
||||
|
||||
## License
|
||||
|
||||
@@ -280,6 +253,6 @@ This helps us to:
|
||||
1. Understand how Langfuse is used and improve the most relevant features.
|
||||
2. Track overall usage for internal and external (e.g. fundraising) reporting.
|
||||
|
||||
None of the data is shared with third parties and does not include any sensitive information. We want to be super transparent about this and you can find the exact data we collect [here](/src/pages/api/cron/telemetry.ts).
|
||||
None of the data is shared with third parties and does not include any sensitive information. We want to be super transparent about this and you can find the exact data we collect [here](/src/features/telemetry/index.ts).
|
||||
|
||||
You can opt-out by setting `TELEMETRY_ENABLED=false`.
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
## Security Policy
|
||||
We strongly recommend using the latest version of Langfuse to receive all security updates.
|
||||
|
||||
For more information, please refer to the [Data Security & Privacy](https://langfuse.com/docs/data-security-privacy) page in the documentation or contact security@langfuse.com.
|
||||
@@ -8,7 +8,7 @@ services:
|
||||
- DATABASE_URL=postgresql://postgres:postgres@db:5432/postgres
|
||||
- NEXTAUTH_SECRET=mysecret
|
||||
- SALT=mysalt
|
||||
- NEXTAUTH_URL=http:localhost:3000
|
||||
- NEXTAUTH_URL=http://localhost:3000
|
||||
depends_on:
|
||||
- db
|
||||
ports:
|
||||
@@ -18,7 +18,7 @@ services:
|
||||
- DATABASE_URL=postgresql://postgres:postgres@db:5432/postgres
|
||||
- NEXTAUTH_SECRET=mysecret
|
||||
- SALT=mysalt
|
||||
- NEXTAUTH_URL=http:localhost:3000
|
||||
- NEXTAUTH_URL=http://localhost:3000
|
||||
- TELEMETRY_ENABLED=${TELEMETRY_ENABLED:-true}
|
||||
- NEXT_PUBLIC_SIGN_UP_DISABLED=${NEXT_PUBLIC_SIGN_UP_DISABLED:-false}
|
||||
- LANGFUSE_ENABLE_EXPERIMENTAL_FEATURES=${LANGFUSE_ENABLE_EXPERIMENTAL_FEATURES:-false}
|
||||
|
||||
+1
-1
@@ -12,7 +12,7 @@ services:
|
||||
- DATABASE_URL=postgresql://postgres:postgres@db:5432/postgres
|
||||
- NEXTAUTH_SECRET=mysecret
|
||||
- SALT=mysalt
|
||||
- NEXTAUTH_URL=http:localhost:3000
|
||||
- NEXTAUTH_URL=http://localhost:3000
|
||||
- TELEMETRY_ENABLED=${TELEMETRY_ENABLED:-true}
|
||||
- NEXT_PUBLIC_SIGN_UP_DISABLED=${NEXT_PUBLIC_SIGN_UP_DISABLED:-false}
|
||||
- LANGFUSE_ENABLE_EXPERIMENTAL_FEATURES=${LANGFUSE_ENABLE_EXPERIMENTAL_FEATURES:-false}
|
||||
|
||||
+17
-2
@@ -1,7 +1,22 @@
|
||||
#!/bin/sh
|
||||
|
||||
# Set DIRECT_URL to the value of DATABASE_URL, required for migrations
|
||||
export DIRECT_URL=$DATABASE_URL
|
||||
# Check if DATABASE_URL is not set
|
||||
if [ -z "$DATABASE_URL" ]; then
|
||||
# Check if all required variables are provided
|
||||
if [ -n "$DATABASE_HOST" ] && [ -n "$DATABASE_USERNAME" ] && [ -n "$DATABASE_PASSWORD" ] && [ -n "$DATABASE_NAME" ]; then
|
||||
# Construct DATABASE_URL from the provided variables
|
||||
DATABASE_URL="postgresql://${DATABASE_USERNAME}:${DATABASE_PASSWORD}@${DATABASE_HOST}/${DATABASE_NAME}"
|
||||
export DATABASE_URL
|
||||
else
|
||||
echo "Error: Required database environment variables are not set. Provide a postgres url for DATABASE_URL."
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
|
||||
# Set DIRECT_URL to the value of DATABASE_URL if it is not set, required for migrations
|
||||
if [ -z "$DIRECT_URL" ]; then
|
||||
export DIRECT_URL=$DATABASE_URL
|
||||
fi
|
||||
|
||||
# Apply migrations
|
||||
prisma migrate deploy
|
||||
|
||||
@@ -1,4 +1,10 @@
|
||||
name: langfuse
|
||||
docs: |
|
||||
## Authentication
|
||||
Authenticate with the API using Basic Auth, get API keys in the project settings:
|
||||
|
||||
- username: Langfuse Public Key
|
||||
- password: Langfuse Secret Key
|
||||
error-discrimination:
|
||||
strategy: status-code
|
||||
auth: basic
|
||||
|
||||
@@ -1,32 +1,4 @@
|
||||
types:
|
||||
# Create Bodies
|
||||
CreateEventRequest:
|
||||
properties:
|
||||
id: optional<string>
|
||||
traceId: optional<string>
|
||||
name: optional<string>
|
||||
startTime: optional<datetime>
|
||||
metadata: optional<unknown>
|
||||
input: optional<unknown>
|
||||
output: optional<unknown>
|
||||
level: optional<ObservationLevel>
|
||||
statusMessage: optional<string>
|
||||
parentObservationId: optional<string>
|
||||
version: optional<string>
|
||||
CreateSpanRequest:
|
||||
extends: CreateEventRequest
|
||||
properties:
|
||||
endTime: optional<datetime>
|
||||
CreateGenerationRequest:
|
||||
extends: CreateSpanRequest
|
||||
properties:
|
||||
completionStartTime: optional<datetime>
|
||||
model: optional<string>
|
||||
modelParameters: optional<map<string, MapValue>>
|
||||
prompt: optional<unknown>
|
||||
completion: optional<unknown>
|
||||
usage: optional<LLMUsage>
|
||||
|
||||
# Objects
|
||||
Trace:
|
||||
properties:
|
||||
@@ -35,10 +7,17 @@ types:
|
||||
docs: The unique identifier of a trace
|
||||
timestamp: datetime
|
||||
name: optional<string>
|
||||
input: optional<unknown>
|
||||
output: optional<unknown>
|
||||
sessionId: optional<string>
|
||||
release: optional<string>
|
||||
version: optional<string>
|
||||
userId: optional<string>
|
||||
metadata: optional<unknown>
|
||||
tags: optional<list<string>>
|
||||
public:
|
||||
type: optional<boolean>
|
||||
docs: Public traces are accessible via url without login
|
||||
TraceWithDetails:
|
||||
extends: Trace
|
||||
properties:
|
||||
@@ -53,6 +32,15 @@ types:
|
||||
properties:
|
||||
observations: list<Observation>
|
||||
scores: list<Score>
|
||||
Session:
|
||||
properties:
|
||||
id: string
|
||||
createdAt: datetime
|
||||
projectId: string
|
||||
SessionWithTraces:
|
||||
extends: Session
|
||||
properties:
|
||||
traces: list<Trace>
|
||||
Observation:
|
||||
properties:
|
||||
id: string
|
||||
@@ -68,12 +56,17 @@ types:
|
||||
version: optional<string>
|
||||
metadata: optional<unknown>
|
||||
output: optional<unknown>
|
||||
promptTokens: integer
|
||||
completionTokens: integer
|
||||
totalTokens: integer
|
||||
usage: optional<Usage>
|
||||
level: ObservationLevel
|
||||
statusMessage: optional<string>
|
||||
parentObservationId: optional<string>
|
||||
promptId: optional<string>
|
||||
Usage:
|
||||
properties:
|
||||
input: optional<integer>
|
||||
output: optional<integer>
|
||||
total: optional<integer>
|
||||
unit: optional<ModelUsageUnit>
|
||||
Score:
|
||||
properties:
|
||||
id: string
|
||||
@@ -87,7 +80,6 @@ types:
|
||||
properties:
|
||||
id: string
|
||||
name: string
|
||||
status: DatasetStatus
|
||||
projectId: string
|
||||
createdAt: datetime
|
||||
updatedAt: datetime
|
||||
@@ -121,6 +113,10 @@ types:
|
||||
datasetRunItems: list<DatasetRunItem>
|
||||
|
||||
# Utilities
|
||||
ModelUsageUnit:
|
||||
enum:
|
||||
- CHARACTERS
|
||||
- TOKENS
|
||||
ObservationLevel:
|
||||
enum:
|
||||
- DEBUG
|
||||
@@ -133,11 +129,6 @@ types:
|
||||
- optional<string>
|
||||
- optional<integer>
|
||||
- optional<boolean>
|
||||
LLMUsage:
|
||||
properties:
|
||||
promptTokens: optional<integer>
|
||||
completionTokens: optional<integer>
|
||||
totalTokens: optional<integer>
|
||||
DatasetStatus:
|
||||
enum:
|
||||
- ACTIVE
|
||||
@@ -146,16 +137,16 @@ types:
|
||||
errors:
|
||||
Error:
|
||||
status-code: 400
|
||||
type: string
|
||||
type: unknown
|
||||
UnauthorizedError:
|
||||
status-code: 401
|
||||
type: string
|
||||
type: unknown
|
||||
AccessDeniedError:
|
||||
status-code: 403
|
||||
type: string
|
||||
type: unknown
|
||||
NotFoundError:
|
||||
status-code: 404
|
||||
type: string
|
||||
type: unknown
|
||||
MethodNotAllowedError:
|
||||
status-code: 405
|
||||
type: string
|
||||
type: unknown
|
||||
|
||||
@@ -7,13 +7,23 @@ service:
|
||||
endpoints:
|
||||
create:
|
||||
method: POST
|
||||
docs: Create a dataset item
|
||||
docs: Create a dataset item, upserts on id
|
||||
path: /dataset-items
|
||||
request: CreateDatasetItemRequest
|
||||
response: commons.DatasetItem
|
||||
get:
|
||||
docs: Get a specific dataset item
|
||||
method: GET
|
||||
path: /dataset-items/{id}
|
||||
path-parameters:
|
||||
id:
|
||||
type: string
|
||||
response: commons.DatasetItem
|
||||
|
||||
types:
|
||||
CreateDatasetItemRequest:
|
||||
properties:
|
||||
datasetName: string
|
||||
input: unknown
|
||||
expectedOutput: optional<unknown>
|
||||
id: optional<string>
|
||||
|
||||
@@ -1,13 +0,0 @@
|
||||
# yaml-language-server: $schema=https://raw.githubusercontent.com/fern-api/fern/main/fern.schema.json
|
||||
imports:
|
||||
commons: ./commons.yml
|
||||
service:
|
||||
auth: true
|
||||
base-path: /api/public
|
||||
endpoints:
|
||||
create:
|
||||
docs: Add an event to the database
|
||||
method: POST
|
||||
path: /events
|
||||
request: commons.CreateEventRequest
|
||||
response: commons.Observation
|
||||
@@ -1,36 +0,0 @@
|
||||
# yaml-language-server: $schema=https://raw.githubusercontent.com/fern-api/fern/main/fern.schema.json
|
||||
imports:
|
||||
commons: ./commons.yml
|
||||
pagination: ./utils/pagination.yml
|
||||
service:
|
||||
auth: true
|
||||
base-path: /api/public
|
||||
endpoints:
|
||||
log:
|
||||
method: POST
|
||||
path: /generations
|
||||
request: commons.CreateGenerationRequest
|
||||
response: commons.Observation
|
||||
update:
|
||||
method: PATCH
|
||||
path: /generations
|
||||
request: UpdateGenerationRequest
|
||||
response: commons.Observation
|
||||
|
||||
types:
|
||||
UpdateGenerationRequest:
|
||||
properties:
|
||||
generationId: string
|
||||
traceId: optional<string>
|
||||
name: optional<string>
|
||||
endTime: optional<datetime>
|
||||
completionStartTime: optional<datetime>
|
||||
model: optional<string>
|
||||
modelParameters: optional<map<string, commons.MapValue>>
|
||||
prompt: optional<unknown>
|
||||
version: optional<string>
|
||||
metadata: optional<unknown>
|
||||
completion: optional<unknown>
|
||||
usage: optional<commons.LLMUsage>
|
||||
level: optional<commons.ObservationLevel>
|
||||
statusMessage: optional<string>
|
||||
@@ -7,9 +7,22 @@ service:
|
||||
docs: Check health of API and database
|
||||
method: GET
|
||||
path: /health
|
||||
response: HealthResponse
|
||||
errors:
|
||||
- ServiceUnavailableError
|
||||
|
||||
types:
|
||||
HealthResponse:
|
||||
properties:
|
||||
version:
|
||||
type: string
|
||||
docs: Langfuse server version
|
||||
status: string
|
||||
examples:
|
||||
- value:
|
||||
version: 1.25.0
|
||||
status: OK
|
||||
|
||||
errors:
|
||||
ServiceUnavailableError:
|
||||
status-code: 503
|
||||
|
||||
@@ -15,7 +15,7 @@ service:
|
||||
body:
|
||||
properties:
|
||||
batch: list<IngestionEvent>
|
||||
response: commons.Score
|
||||
response: IngestionResponse # will be reportet as 200 response, but endpoint returns 207
|
||||
|
||||
types:
|
||||
IngestionEvent:
|
||||
@@ -23,26 +23,206 @@ types:
|
||||
union:
|
||||
trace-create: TraceEvent
|
||||
score-create: ScoreEvent
|
||||
observation-create: ObservationCreateEvent
|
||||
observation-update: ObservationUpdateEvent
|
||||
event-create: CreateEventEvent
|
||||
generation-create: CreateGenerationEvent
|
||||
generation-update: UpdateGenerationEvent
|
||||
span-create: CreateSpanEvent
|
||||
span-update: UpdateSpanEvent
|
||||
sdk-log: SDKLogEvent
|
||||
|
||||
# both are legacy
|
||||
observation-create: CreateObservationEvent
|
||||
observation-update: UpdateObservationEvent
|
||||
|
||||
ObservationType:
|
||||
enum:
|
||||
- SPAN
|
||||
- GENERATION
|
||||
- EVENT
|
||||
|
||||
IngestionUsage:
|
||||
discriminated: false
|
||||
union:
|
||||
- commons.Usage
|
||||
- OpenAIUsage
|
||||
|
||||
OpenAIUsage:
|
||||
properties:
|
||||
promptTokens: optional<integer>
|
||||
completionTokens: optional<integer>
|
||||
totalTokens: optional<integer>
|
||||
|
||||
OptionalObservationBody:
|
||||
properties:
|
||||
traceId: optional<string>
|
||||
name: optional<string>
|
||||
startTime: optional<datetime>
|
||||
metadata: optional<unknown>
|
||||
input: optional<unknown>
|
||||
output: optional<unknown>
|
||||
level: optional<commons.ObservationLevel>
|
||||
statusMessage: optional<string>
|
||||
parentObservationId: optional<string>
|
||||
version: optional<string>
|
||||
|
||||
CreateEventBody:
|
||||
extends: OptionalObservationBody
|
||||
properties:
|
||||
id: optional<string>
|
||||
|
||||
UpdateEventBody:
|
||||
extends: OptionalObservationBody
|
||||
properties:
|
||||
id: string
|
||||
|
||||
CreateSpanBody:
|
||||
extends: CreateEventBody
|
||||
properties:
|
||||
endTime: optional<datetime>
|
||||
|
||||
UpdateSpanBody:
|
||||
extends: UpdateEventBody
|
||||
properties:
|
||||
endTime: optional<datetime>
|
||||
|
||||
CreateGenerationBody:
|
||||
extends: CreateSpanBody
|
||||
properties:
|
||||
completionStartTime: optional<datetime>
|
||||
model: optional<string>
|
||||
modelParameters: optional<map<string, commons.MapValue>>
|
||||
usage: optional<IngestionUsage>
|
||||
promptName: optional<string>
|
||||
promptVersion: optional<integer>
|
||||
|
||||
UpdateGenerationBody:
|
||||
extends: UpdateSpanBody
|
||||
properties:
|
||||
completionStartTime: optional<datetime>
|
||||
model: optional<string>
|
||||
modelParameters: optional<map<string, commons.MapValue>>
|
||||
usage: optional<IngestionUsage>
|
||||
promptName: optional<string>
|
||||
promptVersion: optional<integer>
|
||||
|
||||
ObservationBody:
|
||||
properties:
|
||||
id: optional<string>
|
||||
traceId: optional<string>
|
||||
type: ObservationType
|
||||
name: optional<string>
|
||||
startTime: optional<datetime>
|
||||
endTime: optional<datetime>
|
||||
completionStartTime: optional<datetime>
|
||||
model: optional<string>
|
||||
modelParameters: optional<map<string, commons.MapValue>>
|
||||
input: optional<unknown>
|
||||
version: optional<string>
|
||||
metadata: optional<unknown>
|
||||
output: optional<unknown>
|
||||
usage: optional<commons.Usage>
|
||||
level: optional<commons.ObservationLevel>
|
||||
statusMessage: optional<string>
|
||||
parentObservationId: optional<string>
|
||||
|
||||
TraceBody:
|
||||
properties:
|
||||
id: optional<string>
|
||||
name: optional<string>
|
||||
userId: optional<string>
|
||||
input: optional<unknown>
|
||||
output: optional<unknown>
|
||||
sessionId: optional<string>
|
||||
release: optional<string>
|
||||
version: optional<string>
|
||||
metadata: optional<unknown>
|
||||
tags: optional<list<string>>
|
||||
public:
|
||||
type: optional<boolean>
|
||||
docs: Make trace publicly accessible via url
|
||||
|
||||
SDKLogBody:
|
||||
properties:
|
||||
log: unknown
|
||||
|
||||
ScoreBody:
|
||||
properties:
|
||||
id: optional<string>
|
||||
traceId: string
|
||||
name: string
|
||||
value: double
|
||||
observationId: optional<string>
|
||||
comment: optional<string>
|
||||
|
||||
BaseEvent:
|
||||
properties:
|
||||
id: string
|
||||
timestamp: string
|
||||
metadata: unknown
|
||||
|
||||
TraceEvent:
|
||||
extends: BaseEvent
|
||||
properties:
|
||||
id: string
|
||||
timestamp: string
|
||||
body: commons.Trace
|
||||
ObservationCreateEvent:
|
||||
body: TraceBody
|
||||
|
||||
CreateObservationEvent:
|
||||
extends: BaseEvent
|
||||
properties:
|
||||
id: string
|
||||
timestamp: string
|
||||
body: commons.Observation
|
||||
ObservationUpdateEvent:
|
||||
body: ObservationBody
|
||||
|
||||
UpdateObservationEvent:
|
||||
extends: BaseEvent
|
||||
properties:
|
||||
id: string
|
||||
timestamp: string
|
||||
body: commons.Observation
|
||||
body: ObservationBody
|
||||
|
||||
ScoreEvent:
|
||||
extends: BaseEvent
|
||||
properties:
|
||||
body: ScoreBody
|
||||
|
||||
SDKLogEvent:
|
||||
extends: BaseEvent
|
||||
properties:
|
||||
body: SDKLogBody
|
||||
|
||||
CreateGenerationEvent:
|
||||
extends: BaseEvent
|
||||
properties:
|
||||
body: CreateGenerationBody
|
||||
|
||||
UpdateGenerationEvent:
|
||||
extends: BaseEvent
|
||||
properties:
|
||||
body: UpdateGenerationBody
|
||||
|
||||
CreateSpanEvent:
|
||||
extends: BaseEvent
|
||||
properties:
|
||||
body: CreateSpanBody
|
||||
|
||||
UpdateSpanEvent:
|
||||
extends: BaseEvent
|
||||
properties:
|
||||
body: UpdateSpanBody
|
||||
|
||||
CreateEventEvent:
|
||||
extends: BaseEvent
|
||||
properties:
|
||||
body: CreateEventBody
|
||||
|
||||
IngestionSuccess:
|
||||
properties:
|
||||
id: string
|
||||
timestamp: string
|
||||
body: commons.Score
|
||||
status: integer
|
||||
|
||||
IngestionError:
|
||||
properties:
|
||||
id: string
|
||||
status: integer
|
||||
message: optional<string>
|
||||
error: optional<unknown>
|
||||
|
||||
IngestionResponse:
|
||||
properties:
|
||||
successes: list<IngestionSuccess>
|
||||
errors: list<IngestionError>
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
# yaml-language-server: $schema=https://raw.githubusercontent.com/fern-api/fern/main/fern.schema.json
|
||||
imports:
|
||||
commons: ./commons.yml
|
||||
pagination: ./utils/pagination.yml
|
||||
service:
|
||||
auth: true
|
||||
base-path: /api/public
|
||||
endpoints:
|
||||
get:
|
||||
method: GET
|
||||
path: /projects
|
||||
response: Projects
|
||||
|
||||
types:
|
||||
Projects:
|
||||
properties:
|
||||
data: list<Project>
|
||||
|
||||
Project:
|
||||
properties:
|
||||
id: string
|
||||
name: string
|
||||
@@ -0,0 +1,36 @@
|
||||
# yaml-language-server: $schema=https://raw.githubusercontent.com/fern-api/fern/main/fern.schema.json
|
||||
imports:
|
||||
commons: ./commons.yml
|
||||
pagination: ./utils/pagination.yml
|
||||
service:
|
||||
auth: true
|
||||
base-path: /api/public
|
||||
endpoints:
|
||||
get:
|
||||
docs: Get a specific prompt
|
||||
method: GET
|
||||
path: /prompts
|
||||
request:
|
||||
name: GetParameterRequest
|
||||
query-parameters:
|
||||
name: string
|
||||
version: optional<integer>
|
||||
response: Prompt
|
||||
create:
|
||||
docs: Create a specific prompt
|
||||
method: POST
|
||||
path: /prompts
|
||||
request: CreatePromptRequest
|
||||
response: Prompt
|
||||
|
||||
types:
|
||||
CreatePromptRequest:
|
||||
properties:
|
||||
name: string
|
||||
isActive: boolean
|
||||
prompt: string
|
||||
Prompt:
|
||||
properties:
|
||||
name: string
|
||||
version: integer
|
||||
prompt: string
|
||||
@@ -0,0 +1,17 @@
|
||||
# yaml-language-server: $schema=https://raw.githubusercontent.com/fern-api/fern/main/fern.schema.json
|
||||
imports:
|
||||
pagination: ./utils/pagination.yml
|
||||
commons: ./commons.yml
|
||||
service:
|
||||
auth: true
|
||||
base-path: /api/public
|
||||
endpoints:
|
||||
get:
|
||||
docs: Get a session
|
||||
method: GET
|
||||
path: /sessions/{sessionId}
|
||||
path-parameters:
|
||||
sessionId:
|
||||
type: string
|
||||
docs: The unique id of a session
|
||||
response: commons.SessionWithTraces
|
||||
@@ -1,33 +0,0 @@
|
||||
# yaml-language-server: $schema=https://raw.githubusercontent.com/fern-api/fern/main/fern.schema.json
|
||||
imports:
|
||||
commons: ./commons.yml
|
||||
service:
|
||||
auth: true
|
||||
base-path: /api/public
|
||||
endpoints:
|
||||
create:
|
||||
docs: Add a span to the database
|
||||
method: POST
|
||||
path: /spans
|
||||
request: commons.CreateSpanRequest
|
||||
response: commons.Observation
|
||||
update:
|
||||
docs: Update a span to the database
|
||||
method: PATCH
|
||||
path: /spans
|
||||
request: UpdateSpanRequest
|
||||
response: commons.Observation
|
||||
|
||||
types:
|
||||
UpdateSpanRequest:
|
||||
properties:
|
||||
spanId: string
|
||||
traceId: optional<string>
|
||||
endTime: optional<datetime>
|
||||
name: optional<string>
|
||||
metadata: optional<unknown>
|
||||
input: optional<unknown>
|
||||
output: optional<unknown>
|
||||
level: optional<commons.ObservationLevel>
|
||||
version: optional<string>
|
||||
statusMessage: optional<string>
|
||||
@@ -6,12 +6,6 @@ service:
|
||||
auth: true
|
||||
base-path: /api/public
|
||||
endpoints:
|
||||
create:
|
||||
docs: Add a trace to the database
|
||||
method: POST
|
||||
path: /traces
|
||||
request: CreateTraceRequest
|
||||
response: commons.Trace
|
||||
get:
|
||||
docs: Get a specific trace
|
||||
method: GET
|
||||
@@ -32,18 +26,13 @@ service:
|
||||
limit: optional<integer>
|
||||
userId: optional<string>
|
||||
name: optional<string>
|
||||
tags:
|
||||
type: optional<string>
|
||||
allow-multiple: true
|
||||
docs: Only traces that include all of these tags will be returned.
|
||||
response: Traces
|
||||
|
||||
types:
|
||||
CreateTraceRequest:
|
||||
properties:
|
||||
id: optional<string>
|
||||
name: optional<string>
|
||||
userId: optional<string>
|
||||
externalId: optional<string>
|
||||
release: optional<string>
|
||||
version: optional<string>
|
||||
metadata: optional<unknown>
|
||||
Traces:
|
||||
properties:
|
||||
data: list<commons.TraceWithDetails>
|
||||
|
||||
@@ -11,13 +11,14 @@ groups:
|
||||
namespaceExport: Langfuse
|
||||
allowCustomFetcher: true
|
||||
- name: fernapi/fern-python-sdk
|
||||
version: 0.3.7
|
||||
version: 0.7.3
|
||||
output:
|
||||
location: local-file-system
|
||||
path: ../../../generated/python
|
||||
config:
|
||||
namespaceExport: Langfuse
|
||||
allowCustomFetcher: true
|
||||
client_class_name: FernLangfuse
|
||||
pydantic_config:
|
||||
require_optional_fields: false
|
||||
- name: fernapi/fern-typescript-node-sdk
|
||||
version: 0.7.1
|
||||
output:
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
{
|
||||
"organization": "finto",
|
||||
"version": "0.15.0-rc68"
|
||||
"version": "0.16.22"
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
+165
-200
@@ -2,7 +2,7 @@
|
||||
"info": {
|
||||
"name": "Langfuse",
|
||||
"schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json",
|
||||
"description": null
|
||||
"description": "## Authentication\n\nAuthenticate with the API using Basic Auth, get API keys in the project settings:\n\n- username: Langfuse Public Key\n- password: Langfuse Secret Key"
|
||||
},
|
||||
"variable": [
|
||||
{
|
||||
@@ -46,7 +46,7 @@
|
||||
"_type": "endpoint",
|
||||
"name": "Create",
|
||||
"request": {
|
||||
"description": "Create a dataset item",
|
||||
"description": "Create a dataset item, upserts on id",
|
||||
"url": {
|
||||
"raw": "{{baseUrl}}/api/public/dataset-items",
|
||||
"host": [
|
||||
@@ -65,7 +65,7 @@
|
||||
"auth": null,
|
||||
"body": {
|
||||
"mode": "raw",
|
||||
"raw": "{\n \"datasetName\": \"example\",\n \"input\": \"UNKNOWN\",\n \"expectedOutput\": \"UNKNOWN\"\n}",
|
||||
"raw": "{\n \"datasetName\": \"example\",\n \"input\": \"UNKNOWN\",\n \"expectedOutput\": \"UNKNOWN\",\n \"id\": \"example\"\n}",
|
||||
"options": {
|
||||
"raw": {
|
||||
"language": "json"
|
||||
@@ -74,6 +74,38 @@
|
||||
}
|
||||
},
|
||||
"response": []
|
||||
},
|
||||
{
|
||||
"_type": "endpoint",
|
||||
"name": "Get",
|
||||
"request": {
|
||||
"description": "Get a specific dataset item",
|
||||
"url": {
|
||||
"raw": "{{baseUrl}}/api/public/dataset-items/:id",
|
||||
"host": [
|
||||
"{{baseUrl}}"
|
||||
],
|
||||
"path": [
|
||||
"api",
|
||||
"public",
|
||||
"dataset-items",
|
||||
":id"
|
||||
],
|
||||
"query": [],
|
||||
"variable": [
|
||||
{
|
||||
"key": "id",
|
||||
"value": "",
|
||||
"description": null
|
||||
}
|
||||
]
|
||||
},
|
||||
"header": [],
|
||||
"method": "GET",
|
||||
"auth": null,
|
||||
"body": null
|
||||
},
|
||||
"response": []
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -228,119 +260,6 @@
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"_type": "container",
|
||||
"description": null,
|
||||
"name": "Event",
|
||||
"item": [
|
||||
{
|
||||
"_type": "endpoint",
|
||||
"name": "Create",
|
||||
"request": {
|
||||
"description": "Add an event to the database",
|
||||
"url": {
|
||||
"raw": "{{baseUrl}}/api/public/events",
|
||||
"host": [
|
||||
"{{baseUrl}}"
|
||||
],
|
||||
"path": [
|
||||
"api",
|
||||
"public",
|
||||
"events"
|
||||
],
|
||||
"query": [],
|
||||
"variable": []
|
||||
},
|
||||
"header": [],
|
||||
"method": "POST",
|
||||
"auth": null,
|
||||
"body": {
|
||||
"mode": "raw",
|
||||
"raw": "{\n \"id\": \"example\",\n \"traceId\": \"example\",\n \"name\": \"example\",\n \"startTime\": \"1994-11-05T13:15:30Z\",\n \"metadata\": \"UNKNOWN\",\n \"input\": \"UNKNOWN\",\n \"output\": \"UNKNOWN\",\n \"level\": \"DEBUG\",\n \"statusMessage\": \"example\",\n \"parentObservationId\": \"example\",\n \"version\": \"example\"\n}",
|
||||
"options": {
|
||||
"raw": {
|
||||
"language": "json"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"response": []
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"_type": "container",
|
||||
"description": null,
|
||||
"name": "Generations",
|
||||
"item": [
|
||||
{
|
||||
"_type": "endpoint",
|
||||
"name": "Log",
|
||||
"request": {
|
||||
"description": null,
|
||||
"url": {
|
||||
"raw": "{{baseUrl}}/api/public/generations",
|
||||
"host": [
|
||||
"{{baseUrl}}"
|
||||
],
|
||||
"path": [
|
||||
"api",
|
||||
"public",
|
||||
"generations"
|
||||
],
|
||||
"query": [],
|
||||
"variable": []
|
||||
},
|
||||
"header": [],
|
||||
"method": "POST",
|
||||
"auth": null,
|
||||
"body": {
|
||||
"mode": "raw",
|
||||
"raw": "{\n \"completionStartTime\": \"1994-11-05T13:15:30Z\",\n \"model\": \"example\",\n \"modelParameters\": {\n \"example\": \"example\"\n },\n \"prompt\": \"UNKNOWN\",\n \"completion\": \"UNKNOWN\",\n \"usage\": {\n \"promptTokens\": 0,\n \"completionTokens\": 0,\n \"totalTokens\": 0\n },\n \"endTime\": \"1994-11-05T13:15:30Z\",\n \"id\": \"example\",\n \"traceId\": \"example\",\n \"name\": \"example\",\n \"startTime\": \"1994-11-05T13:15:30Z\",\n \"metadata\": \"UNKNOWN\",\n \"input\": \"UNKNOWN\",\n \"output\": \"UNKNOWN\",\n \"level\": \"DEBUG\",\n \"statusMessage\": \"example\",\n \"parentObservationId\": \"example\",\n \"version\": \"example\"\n}",
|
||||
"options": {
|
||||
"raw": {
|
||||
"language": "json"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"response": []
|
||||
},
|
||||
{
|
||||
"_type": "endpoint",
|
||||
"name": "Update",
|
||||
"request": {
|
||||
"description": null,
|
||||
"url": {
|
||||
"raw": "{{baseUrl}}/api/public/generations",
|
||||
"host": [
|
||||
"{{baseUrl}}"
|
||||
],
|
||||
"path": [
|
||||
"api",
|
||||
"public",
|
||||
"generations"
|
||||
],
|
||||
"query": [],
|
||||
"variable": []
|
||||
},
|
||||
"header": [],
|
||||
"method": "PATCH",
|
||||
"auth": null,
|
||||
"body": {
|
||||
"mode": "raw",
|
||||
"raw": "{\n \"generationId\": \"example\",\n \"traceId\": \"example\",\n \"name\": \"example\",\n \"endTime\": \"1994-11-05T13:15:30Z\",\n \"completionStartTime\": \"1994-11-05T13:15:30Z\",\n \"model\": \"example\",\n \"modelParameters\": {\n \"example\": \"example\"\n },\n \"prompt\": \"UNKNOWN\",\n \"version\": \"example\",\n \"metadata\": \"UNKNOWN\",\n \"completion\": \"UNKNOWN\",\n \"usage\": {\n \"promptTokens\": 0,\n \"completionTokens\": 0,\n \"totalTokens\": 0\n },\n \"level\": \"DEBUG\",\n \"statusMessage\": \"example\"\n}",
|
||||
"options": {
|
||||
"raw": {
|
||||
"language": "json"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"response": []
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"_type": "container",
|
||||
"description": null,
|
||||
@@ -401,7 +320,7 @@
|
||||
"auth": null,
|
||||
"body": {
|
||||
"mode": "raw",
|
||||
"raw": "{\n \"batch\": [\n {\n \"type\": \"trace-create\",\n \"id\": \"example\",\n \"timestamp\": \"example\",\n \"body\": {\n \"id\": \"example\",\n \"timestamp\": \"1994-11-05T13:15:30Z\",\n \"name\": \"example\",\n \"release\": \"example\",\n \"version\": \"example\",\n \"userId\": \"example\",\n \"metadata\": \"UNKNOWN\"\n }\n }\n ]\n}",
|
||||
"raw": "{\n \"batch\": [\n {\n \"type\": \"trace-create\",\n \"body\": {\n \"id\": \"example\",\n \"name\": \"example\",\n \"userId\": \"example\",\n \"input\": \"UNKNOWN\",\n \"output\": \"UNKNOWN\",\n \"sessionId\": \"example\",\n \"release\": \"example\",\n \"version\": \"example\",\n \"metadata\": \"UNKNOWN\",\n \"tags\": [\n \"example\"\n ],\n \"public\": true\n },\n \"id\": \"example\",\n \"timestamp\": \"example\",\n \"metadata\": \"UNKNOWN\"\n }\n ]\n}",
|
||||
"options": {
|
||||
"raw": {
|
||||
"language": "json"
|
||||
@@ -513,6 +432,114 @@
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"_type": "container",
|
||||
"description": null,
|
||||
"name": "Projects",
|
||||
"item": [
|
||||
{
|
||||
"_type": "endpoint",
|
||||
"name": "Get",
|
||||
"request": {
|
||||
"description": null,
|
||||
"url": {
|
||||
"raw": "{{baseUrl}}/api/public/projects",
|
||||
"host": [
|
||||
"{{baseUrl}}"
|
||||
],
|
||||
"path": [
|
||||
"api",
|
||||
"public",
|
||||
"projects"
|
||||
],
|
||||
"query": [],
|
||||
"variable": []
|
||||
},
|
||||
"header": [],
|
||||
"method": "GET",
|
||||
"auth": null,
|
||||
"body": null
|
||||
},
|
||||
"response": []
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"_type": "container",
|
||||
"description": null,
|
||||
"name": "Prompts",
|
||||
"item": [
|
||||
{
|
||||
"_type": "endpoint",
|
||||
"name": "Get",
|
||||
"request": {
|
||||
"description": "Get a specific prompt",
|
||||
"url": {
|
||||
"raw": "{{baseUrl}}/api/public/prompts?name=&version=",
|
||||
"host": [
|
||||
"{{baseUrl}}"
|
||||
],
|
||||
"path": [
|
||||
"api",
|
||||
"public",
|
||||
"prompts"
|
||||
],
|
||||
"query": [
|
||||
{
|
||||
"key": "name",
|
||||
"value": "",
|
||||
"description": null
|
||||
},
|
||||
{
|
||||
"key": "version",
|
||||
"value": "",
|
||||
"description": null
|
||||
}
|
||||
],
|
||||
"variable": []
|
||||
},
|
||||
"header": [],
|
||||
"method": "GET",
|
||||
"auth": null,
|
||||
"body": null
|
||||
},
|
||||
"response": []
|
||||
},
|
||||
{
|
||||
"_type": "endpoint",
|
||||
"name": "Create",
|
||||
"request": {
|
||||
"description": "Create a specific prompt",
|
||||
"url": {
|
||||
"raw": "{{baseUrl}}/api/public/prompts",
|
||||
"host": [
|
||||
"{{baseUrl}}"
|
||||
],
|
||||
"path": [
|
||||
"api",
|
||||
"public",
|
||||
"prompts"
|
||||
],
|
||||
"query": [],
|
||||
"variable": []
|
||||
},
|
||||
"header": [],
|
||||
"method": "POST",
|
||||
"auth": null,
|
||||
"body": {
|
||||
"mode": "raw",
|
||||
"raw": "{\n \"name\": \"example\",\n \"isActive\": true,\n \"prompt\": \"example\"\n}",
|
||||
"options": {
|
||||
"raw": {
|
||||
"language": "json"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"response": []
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"_type": "container",
|
||||
"description": null,
|
||||
@@ -602,71 +629,37 @@
|
||||
{
|
||||
"_type": "container",
|
||||
"description": null,
|
||||
"name": "Span",
|
||||
"name": "Sessions",
|
||||
"item": [
|
||||
{
|
||||
"_type": "endpoint",
|
||||
"name": "Create",
|
||||
"name": "Get",
|
||||
"request": {
|
||||
"description": "Add a span to the database",
|
||||
"description": "Get a session",
|
||||
"url": {
|
||||
"raw": "{{baseUrl}}/api/public/spans",
|
||||
"raw": "{{baseUrl}}/api/public/sessions/:sessionId",
|
||||
"host": [
|
||||
"{{baseUrl}}"
|
||||
],
|
||||
"path": [
|
||||
"api",
|
||||
"public",
|
||||
"spans"
|
||||
"sessions",
|
||||
":sessionId"
|
||||
],
|
||||
"query": [],
|
||||
"variable": []
|
||||
"variable": [
|
||||
{
|
||||
"key": "sessionId",
|
||||
"value": "",
|
||||
"description": "The unique id of a session"
|
||||
}
|
||||
]
|
||||
},
|
||||
"header": [],
|
||||
"method": "POST",
|
||||
"method": "GET",
|
||||
"auth": null,
|
||||
"body": {
|
||||
"mode": "raw",
|
||||
"raw": "{\n \"endTime\": \"1994-11-05T13:15:30Z\",\n \"id\": \"example\",\n \"traceId\": \"example\",\n \"name\": \"example\",\n \"startTime\": \"1994-11-05T13:15:30Z\",\n \"metadata\": \"UNKNOWN\",\n \"input\": \"UNKNOWN\",\n \"output\": \"UNKNOWN\",\n \"level\": \"DEBUG\",\n \"statusMessage\": \"example\",\n \"parentObservationId\": \"example\",\n \"version\": \"example\"\n}",
|
||||
"options": {
|
||||
"raw": {
|
||||
"language": "json"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"response": []
|
||||
},
|
||||
{
|
||||
"_type": "endpoint",
|
||||
"name": "Update",
|
||||
"request": {
|
||||
"description": "Update a span to the database",
|
||||
"url": {
|
||||
"raw": "{{baseUrl}}/api/public/spans",
|
||||
"host": [
|
||||
"{{baseUrl}}"
|
||||
],
|
||||
"path": [
|
||||
"api",
|
||||
"public",
|
||||
"spans"
|
||||
],
|
||||
"query": [],
|
||||
"variable": []
|
||||
},
|
||||
"header": [],
|
||||
"method": "PATCH",
|
||||
"auth": null,
|
||||
"body": {
|
||||
"mode": "raw",
|
||||
"raw": "{\n \"spanId\": \"example\",\n \"traceId\": \"example\",\n \"endTime\": \"1994-11-05T13:15:30Z\",\n \"name\": \"example\",\n \"metadata\": \"UNKNOWN\",\n \"input\": \"UNKNOWN\",\n \"output\": \"UNKNOWN\",\n \"level\": \"DEBUG\",\n \"version\": \"example\",\n \"statusMessage\": \"example\"\n}",
|
||||
"options": {
|
||||
"raw": {
|
||||
"language": "json"
|
||||
}
|
||||
}
|
||||
}
|
||||
"body": null
|
||||
},
|
||||
"response": []
|
||||
}
|
||||
@@ -677,39 +670,6 @@
|
||||
"description": null,
|
||||
"name": "Trace",
|
||||
"item": [
|
||||
{
|
||||
"_type": "endpoint",
|
||||
"name": "Create",
|
||||
"request": {
|
||||
"description": "Add a trace to the database",
|
||||
"url": {
|
||||
"raw": "{{baseUrl}}/api/public/traces",
|
||||
"host": [
|
||||
"{{baseUrl}}"
|
||||
],
|
||||
"path": [
|
||||
"api",
|
||||
"public",
|
||||
"traces"
|
||||
],
|
||||
"query": [],
|
||||
"variable": []
|
||||
},
|
||||
"header": [],
|
||||
"method": "POST",
|
||||
"auth": null,
|
||||
"body": {
|
||||
"mode": "raw",
|
||||
"raw": "{\n \"id\": \"example\",\n \"name\": \"example\",\n \"userId\": \"example\",\n \"externalId\": \"example\",\n \"release\": \"example\",\n \"version\": \"example\",\n \"metadata\": \"UNKNOWN\"\n}",
|
||||
"options": {
|
||||
"raw": {
|
||||
"language": "json"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"response": []
|
||||
},
|
||||
{
|
||||
"_type": "endpoint",
|
||||
"name": "Get",
|
||||
@@ -748,7 +708,7 @@
|
||||
"request": {
|
||||
"description": "Get list of traces",
|
||||
"url": {
|
||||
"raw": "{{baseUrl}}/api/public/traces?page=&limit=&userId=&name=",
|
||||
"raw": "{{baseUrl}}/api/public/traces?page=&limit=&userId=&name=&tags=",
|
||||
"host": [
|
||||
"{{baseUrl}}"
|
||||
],
|
||||
@@ -777,6 +737,11 @@
|
||||
"key": "name",
|
||||
"value": "",
|
||||
"description": null
|
||||
},
|
||||
{
|
||||
"key": "tags",
|
||||
"value": "",
|
||||
"description": "Only traces that include all of these tags will be returned."
|
||||
}
|
||||
],
|
||||
"variable": []
|
||||
|
||||
+6
-1
@@ -18,6 +18,8 @@ const serverTestConfig = {
|
||||
testEnvironment: "jest-environment-node",
|
||||
};
|
||||
|
||||
// To avoid the "Cannot use import statement outside a module" errors while transforming ESM.
|
||||
const esModules = ["superjson"];
|
||||
// Add any custom config to be passed to Jest
|
||||
/** @type {import('jest').Config} */
|
||||
const config = {
|
||||
@@ -26,7 +28,10 @@ const config = {
|
||||
verbose: true,
|
||||
projects: [
|
||||
await createJestConfig(clientTestConfig)(),
|
||||
await createJestConfig(serverTestConfig)(),
|
||||
{
|
||||
...(await createJestConfig(serverTestConfig)()),
|
||||
transformIgnorePatterns: [`/node_modules/(?!(${esModules.join("|")})/)`],
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
|
||||
+31
-15
@@ -23,21 +23,37 @@ const nextConfig = {
|
||||
output: "standalone",
|
||||
|
||||
async headers() {
|
||||
// Required to check authentication status from langfuse.com
|
||||
if (env.NEXT_PUBLIC_LANGFUSE_CLOUD_REGION !== undefined)
|
||||
return [
|
||||
{
|
||||
source: '/api/auth/session',
|
||||
headers: [
|
||||
{ key: "Access-Control-Allow-Origin", value: "https://langfuse.com" },
|
||||
{ key: "Access-Control-Allow-Credentials", value: "true" },
|
||||
{ key: "Access-Control-Allow-Methods", value: "GET,POST" },
|
||||
{ key: "Access-Control-Allow-Headers", value: "Content-Type, Authorization" },
|
||||
]
|
||||
}
|
||||
]
|
||||
|
||||
return []
|
||||
return [
|
||||
{
|
||||
source: "/:path*",
|
||||
headers: [
|
||||
{
|
||||
key: "x-frame-options",
|
||||
value: "SAMEORIGIN",
|
||||
},
|
||||
],
|
||||
},
|
||||
// Required to check authentication status from langfuse.com
|
||||
...(env.NEXT_PUBLIC_LANGFUSE_CLOUD_REGION !== undefined ?
|
||||
[
|
||||
{
|
||||
source: "/api/auth/session",
|
||||
headers: [
|
||||
{
|
||||
key: "Access-Control-Allow-Origin",
|
||||
value: "https://langfuse.com",
|
||||
},
|
||||
{ key: "Access-Control-Allow-Credentials", value: "true" },
|
||||
{ key: "Access-Control-Allow-Methods", value: "GET,POST" },
|
||||
{
|
||||
key: "Access-Control-Allow-Headers",
|
||||
value: "Content-Type, Authorization",
|
||||
},
|
||||
]
|
||||
},
|
||||
] : []
|
||||
)
|
||||
]
|
||||
},
|
||||
|
||||
// webassembly support for @dqbd/tiktoken
|
||||
|
||||
Generated
+3858
-1541
File diff suppressed because it is too large
Load Diff
+77
-56
@@ -1,20 +1,23 @@
|
||||
{
|
||||
"name": "langfuse-core",
|
||||
"version": "1.4.0",
|
||||
"version": "1.33.6",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"prebuild": "cp generated/openapi-client/openapi.yml public/openapi-client.yml && cp generated/openapi-server/openapi.yml public/openapi-server.yml",
|
||||
"build": "next build",
|
||||
"dev": "next dev",
|
||||
"dx": "npm i && npm run db:reset && npm run db:seed:examples && npm run dev",
|
||||
"postinstall": "prisma generate",
|
||||
"lint": "next lint",
|
||||
"lint:fix": "next lint --fix",
|
||||
"prettier": "prettier --write ./src *.{ts,js}",
|
||||
"clean": "rm -rf node_modules",
|
||||
"start": "next start",
|
||||
"test": "jest --runInBand",
|
||||
"test:watch": "jest --watch --runInBand",
|
||||
"test:e2e": "playwright test",
|
||||
"db:migrate": "npx prisma migrate dev",
|
||||
"db:migrate": "DISABLE_ERD=false npx prisma migrate dev",
|
||||
"db:reset": "npx prisma migrate reset",
|
||||
"db:seed": "npx prisma db seed",
|
||||
"db:seed:examples": "npx prisma db seed -- --environment examples",
|
||||
"release": "release-it"
|
||||
@@ -24,15 +27,17 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@anthropic-ai/tokenizer": "^0.0.4",
|
||||
"@emotion/react": "^11.11.1",
|
||||
"@emotion/styled": "^11.11.0",
|
||||
"@formkit/auto-animate": "^0.8.1",
|
||||
"@aws-sdk/client-s3": "^3.485.0",
|
||||
"@aws-sdk/s3-request-presigner": "^3.485.0",
|
||||
"@headlessui/react": "^1.7.17",
|
||||
"@heroicons/react": "^2.0.18",
|
||||
"@hookform/resolvers": "^3.3.2",
|
||||
"@heroicons/react": "^2.1.1",
|
||||
"@hookform/resolvers": "^3.3.4",
|
||||
"@next-auth/prisma-adapter": "^1.0.7",
|
||||
"@prisma/client": "^5.6.0",
|
||||
"@prisma/client": "^5.7.1",
|
||||
"@radix-ui/react-accordion": "^1.1.2",
|
||||
"@radix-ui/react-alert-dialog": "^1.0.5",
|
||||
"@radix-ui/react-avatar": "^1.0.4",
|
||||
"@radix-ui/react-checkbox": "^1.0.4",
|
||||
"@radix-ui/react-collapsible": "^1.0.3",
|
||||
"@radix-ui/react-dialog": "^1.0.5",
|
||||
"@radix-ui/react-dropdown-menu": "^2.0.6",
|
||||
@@ -40,6 +45,7 @@
|
||||
"@radix-ui/react-icons": "^1.3.0",
|
||||
"@radix-ui/react-label": "^2.0.2",
|
||||
"@radix-ui/react-popover": "^1.0.7",
|
||||
"@radix-ui/react-scroll-area": "^1.0.5",
|
||||
"@radix-ui/react-select": "^2.0.0",
|
||||
"@radix-ui/react-separator": "^1.0.3",
|
||||
"@radix-ui/react-slider": "^1.1.2",
|
||||
@@ -48,86 +54,88 @@
|
||||
"@radix-ui/react-tabs": "^1.0.4",
|
||||
"@radix-ui/react-toggle": "^1.0.3",
|
||||
"@radix-ui/react-tooltip": "^1.0.7",
|
||||
"@sentry/nextjs": "^7.80.1",
|
||||
"@sentry/profiling-node": "^1.2.6",
|
||||
"@react-email/components": "^0.0.12",
|
||||
"@react-email/render": "^0.0.10",
|
||||
"@sentry/nextjs": "^7.92.0",
|
||||
"@sentry/profiling-node": "^1.3.2",
|
||||
"@sentry/types": "^7.88.0",
|
||||
"@t3-oss/env-nextjs": "^0.7.1",
|
||||
"@tailwindcss/forms": "^0.5.7",
|
||||
"@tailwindcss/line-clamp": "^0.4.4",
|
||||
"@tanstack/react-query": "^4.36.1",
|
||||
"@tanstack/react-table": "^8.10.7",
|
||||
"@tanstack/react-table": "^8.11.3",
|
||||
"@tremor/react": "^3.11.1",
|
||||
"@trpc/client": "^10.43.6",
|
||||
"@trpc/next": "^10.43.6",
|
||||
"@trpc/react-query": "^10.43.6",
|
||||
"@trpc/server": "^10.43.6",
|
||||
"@trpc/client": "^10.44.1",
|
||||
"@trpc/next": "^10.44.1",
|
||||
"@trpc/react-query": "^10.44.1",
|
||||
"@trpc/server": "^10.44.1",
|
||||
"@vercel/edge-config": "^0.4.1",
|
||||
"axios": "^1.6.2",
|
||||
"bcryptjs": "^2.4.3",
|
||||
"class-variance-authority": "^0.7.0",
|
||||
"clsx": "^2.0.0",
|
||||
"cmdk": "^0.2.0",
|
||||
"core-js": "^3.35.0",
|
||||
"cors": "^2.8.5",
|
||||
"date-fns": "^2.30.0",
|
||||
"date-fns": "^3.1.0",
|
||||
"decimal.js": "^10.4.3",
|
||||
"exponential-backoff": "^3.1.1",
|
||||
"js-yaml": "^4.1.0",
|
||||
"lodash": "^4.17.21",
|
||||
"lucide-react": "^0.292.0",
|
||||
"next": "^14.0.3",
|
||||
"lucide-react": "^0.306.0",
|
||||
"next": "^14.0.4",
|
||||
"next-auth": "^4.24.5",
|
||||
"next-query-params": "^4.3.0",
|
||||
"next-swagger-doc": "^0.4.0",
|
||||
"posthog-js": "^1.91.1",
|
||||
"posthog-node": "^3.1.3",
|
||||
"next-query-params": "^5.0.0",
|
||||
"nodemailer": "^6.9.8",
|
||||
"posthog-js": "^1.96.1",
|
||||
"posthog-node": "^3.2.1",
|
||||
"react": "18.2.0",
|
||||
"react-day-picker": "^8.9.1",
|
||||
"react-day-picker": "^8.10.0",
|
||||
"react-dom": "18.2.0",
|
||||
"react-hook-form": "^7.48.2",
|
||||
"react-hook-form": "^7.49.2",
|
||||
"react-icons": "^4.12.0",
|
||||
"react-responsive": "^9.0.2",
|
||||
"react18-json-view": "^0.2.7-canary.1",
|
||||
"superagent": "^8.1.2",
|
||||
"react18-json-view": "^0.2.7-canary.2",
|
||||
"superjson": "2.2.1",
|
||||
"tailwind-merge": "^2.0.0",
|
||||
"tailwind-merge": "^2.2.0",
|
||||
"tailwindcss-animate": "^1.0.7",
|
||||
"tiktoken": "^1.0.11",
|
||||
"ts-pattern": "^5.0.5",
|
||||
"use-query-params": "^2.2.1",
|
||||
"uuid": "^9.0.1",
|
||||
"zod": "^3.22.4"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@playwright/test": "^1.40.0",
|
||||
"@testing-library/jest-dom": "^6.1.4",
|
||||
"@jedmao/location": "^3.0.0",
|
||||
"@mermaid-js/mermaid-cli": "^10.6.1",
|
||||
"@playwright/test": "^1.40.1",
|
||||
"@release-it/bumper": "^6.0.1",
|
||||
"@testing-library/jest-dom": "^6.1.5",
|
||||
"@testing-library/react": "^14.1.2",
|
||||
"@types/bcryptjs": "^2.4.6",
|
||||
"@types/cors": "^2.8.16",
|
||||
"@types/eslint": "^8.44.7",
|
||||
"@types/jest": "^29.5.8",
|
||||
"@types/js-yaml": "^4.0.9",
|
||||
"@types/lodash": "^4.14.201",
|
||||
"@types/node": "18.16.0",
|
||||
"@types/react": "^18.2.37",
|
||||
"@types/react-dom": "^18.2.15",
|
||||
"@types/swagger-ui-react": "^4.18.3",
|
||||
"@types/cors": "^2.8.17",
|
||||
"@types/eslint": "^8.56.1",
|
||||
"@types/jest": "^29.5.11",
|
||||
"@types/lodash": "^4.14.202",
|
||||
"@types/node": "20.10.5",
|
||||
"@types/nodemailer": "^6.4.14",
|
||||
"@types/react": "^18.2.46",
|
||||
"@types/react-dom": "^18.2.18",
|
||||
"@types/uuid": "^9.0.7",
|
||||
"@typescript-eslint/eslint-plugin": "^6.11.0",
|
||||
"@typescript-eslint/parser": "^6.11.0",
|
||||
"@typescript-eslint/eslint-plugin": "^6.17.0",
|
||||
"@typescript-eslint/parser": "^6.16.0",
|
||||
"autoprefixer": "^10.4.16",
|
||||
"dotenv-cli": "^7.3.0",
|
||||
"eslint": "^8.54.0",
|
||||
"eslint-config-next": "^14.0.3",
|
||||
"eslint": "^8.56.0",
|
||||
"eslint-config-next": "^14.0.4",
|
||||
"jest": "^29.7.0",
|
||||
"jest-environment-jsdom": "^29.7.0",
|
||||
"postcss": "^8.4.31",
|
||||
"prettier": "^3.1.0",
|
||||
"prettier-plugin-tailwindcss": "^0.5.7",
|
||||
"prisma": "^5.6.0",
|
||||
"release-it": "^17.0.0",
|
||||
"swagger-jsdoc": "^6.2.8",
|
||||
"tailwindcss": "^3.3.5",
|
||||
"ts-node": "^10.9.1",
|
||||
"postcss": "^8.4.33",
|
||||
"prettier": "^3.1.1",
|
||||
"prettier-plugin-tailwindcss": "^0.5.11",
|
||||
"prisma": "^5.7.1",
|
||||
"prisma-erd-generator": "^1.11.2",
|
||||
"release-it": "^17.0.1",
|
||||
"tailwindcss": "^3.4.1",
|
||||
"ts-node": "^10.9.2",
|
||||
"tsconfig-paths": "^4.2.0",
|
||||
"typescript": "^5.2.2"
|
||||
"typescript": "^5.3.3"
|
||||
},
|
||||
"ct3aMetadata": {
|
||||
"initVersion": "7.13.0"
|
||||
@@ -144,7 +152,20 @@
|
||||
"release": true,
|
||||
"web": true,
|
||||
"autoGenerate": true,
|
||||
"releaseName": "v${version}"
|
||||
"releaseName": "v${version}",
|
||||
"comments": {
|
||||
"submit": true,
|
||||
"issue": ":rocket: _This issue has been resolved in v${version}. See [${releaseName}](${releaseUrl}) for release notes._",
|
||||
"pr": ":rocket: _This pull request is included in v${version}. See [${releaseName}](${releaseUrl}) for release notes._"
|
||||
}
|
||||
},
|
||||
"plugins": {
|
||||
"@release-it/bumper": {
|
||||
"out": {
|
||||
"file": "./src/constants/VERSION.ts",
|
||||
"type": "application/typescript"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
File diff suppressed because one or more lines are too long
|
After Width: | Height: | Size: 194 KiB |
@@ -0,0 +1,3 @@
|
||||
|
||||
-- AlterTable
|
||||
ALTER TABLE "traces" ADD COLUMN "bookmarked" BOOLEAN NOT NULL DEFAULT false;
|
||||
@@ -0,0 +1,27 @@
|
||||
-- CreateTable
|
||||
CREATE TABLE "membership_invitations" (
|
||||
"id" TEXT NOT NULL,
|
||||
"email" TEXT NOT NULL,
|
||||
"role" "MembershipRole" NOT NULL,
|
||||
"project_id" TEXT NOT NULL,
|
||||
"sender_id" TEXT,
|
||||
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updated_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
|
||||
CONSTRAINT "membership_invitations_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "membership_invitations_id_key" ON "membership_invitations"("id");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "membership_invitations_project_id_idx" ON "membership_invitations"("project_id");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "membership_invitations_email_idx" ON "membership_invitations"("email");
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "membership_invitations" ADD CONSTRAINT "membership_invitations_project_id_fkey" FOREIGN KEY ("project_id") REFERENCES "projects"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "membership_invitations" ADD CONSTRAINT "membership_invitations_sender_id_fkey" FOREIGN KEY ("sender_id") REFERENCES "users"("id") ON DELETE SET NULL ON UPDATE CASCADE;
|
||||
@@ -0,0 +1,34 @@
|
||||
-- AlterTable
|
||||
ALTER TABLE "traces" ADD COLUMN "input" JSONB,
|
||||
ADD COLUMN "output" JSONB,
|
||||
ADD COLUMN "session_id" TEXT;
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "trace_sessions" (
|
||||
"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,
|
||||
"bookmarked" BOOLEAN NOT NULL DEFAULT false,
|
||||
"public" BOOLEAN NOT NULL DEFAULT false,
|
||||
|
||||
CONSTRAINT "trace_sessions_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "trace_sessions_project_id_idx" ON "trace_sessions"("project_id");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "trace_sessions_created_at_idx" ON "trace_sessions"("created_at");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "trace_sessions_id_project_id_key" ON "trace_sessions"("id", "project_id");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "traces_session_id_idx" ON "traces"("session_id");
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "trace_sessions" ADD CONSTRAINT "trace_sessions_project_id_fkey" FOREIGN KEY ("project_id") REFERENCES "projects"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "traces" ADD CONSTRAINT "traces_session_id_fkey" FOREIGN KEY ("session_id") REFERENCES "trace_sessions"("id") ON DELETE SET NULL ON UPDATE CASCADE;
|
||||
@@ -0,0 +1,4 @@
|
||||
-- AlterTable
|
||||
-- Adding this migration does not lock the postgres table. For non volaitile data, the default value is stored in the
|
||||
-- table metadata and accessed on read query time. No table re-write is required (https://www.postgresql.org/docs/current/sql-altertable.html#:~:text=When%20a%20column%20is%20added,is%20specified%2C%20NULL%20is%20used.)
|
||||
ALTER TABLE "observations" ADD COLUMN "unit" TEXT NOT NULL DEFAULT 'TOKENS';
|
||||
@@ -0,0 +1,2 @@
|
||||
-- AlterTable
|
||||
ALTER TABLE "projects" ADD COLUMN "cloud_config" JSONB;
|
||||
@@ -0,0 +1,3 @@
|
||||
-- AlterTable
|
||||
ALTER TABLE "Account" ADD COLUMN "expires_in" INTEGER,
|
||||
ADD COLUMN "ext_expires_in" INTEGER;
|
||||
@@ -0,0 +1,29 @@
|
||||
-- AlterTable
|
||||
ALTER TABLE "observations" ADD COLUMN "prompt_id" TEXT;
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "prompts" (
|
||||
"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,
|
||||
"created_by" TEXT NOT NULL,
|
||||
"prompt" TEXT NOT NULL,
|
||||
"name" TEXT NOT NULL,
|
||||
"version" INTEGER NOT NULL,
|
||||
"is_active" BOOLEAN NOT NULL,
|
||||
|
||||
CONSTRAINT "prompts_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "prompts_project_id_name_version_idx" ON "prompts"("project_id", "name", "version");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "prompts_project_id_name_version_key" ON "prompts"("project_id", "name", "version");
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "observations" ADD CONSTRAINT "observations_prompt_id_fkey" FOREIGN KEY ("prompt_id") REFERENCES "prompts"("id") ON DELETE SET NULL ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "prompts" ADD CONSTRAINT "prompts_project_id_fkey" FOREIGN KEY ("project_id") REFERENCES "projects"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
@@ -0,0 +1,30 @@
|
||||
-- Migration to add gpt-3.5-turbo-1106 and gpt-3.5-turbo-instruct models
|
||||
|
||||
INSERT INTO pricings (
|
||||
id,
|
||||
model_name,
|
||||
pricing_unit,
|
||||
price,
|
||||
currency,
|
||||
token_type
|
||||
)
|
||||
VALUES
|
||||
('clqqpc2pr000008l3hvy63gxy','gpt-3.5-turbo-1106', 'PER_1000_TOKENS', 0.001, 'USD', 'PROMPT'),
|
||||
('clqqpcb6d000208l3atrfbmou','gpt-3.5-turbo-1106', 'PER_1000_TOKENS', 0.002, 'USD', 'COMPLETION'),
|
||||
('clqqpdh45000008lfgrnx76cv','gpt-3.5-turbo-instruct', 'PER_1000_TOKENS', 0.0015, 'USD', 'PROMPT'),
|
||||
('clqqpdjya000108lf3s4b4c4m','gpt-3.5-turbo-instruct', 'PER_1000_TOKENS', 0.002, 'USD', 'COMPLETION'),
|
||||
('clnnuiuq6000008l4dxp43wc6', 'claude-instant-1.2', 'PER_1000_TOKENS', 0.00163, 'USD', 'PROMPT'),
|
||||
('clnnujlmj000108l48wne4ii9', 'claude-instant-1.2', 'PER_1000_TOKENS', 0.00551, 'USD', 'COMPLETION'),
|
||||
('clnnukutg000208l49qqt9lyr', 'claude-instant-1.1', 'PER_1000_TOKENS', 0.00163, 'USD', 'PROMPT'),
|
||||
('clnnun3x3000408l490qz9uv0', 'claude-instant-1.1', 'PER_1000_TOKENS', 0.00551, 'USD', 'COMPLETION'),
|
||||
('clnnuosvy000508l4gsjy2pp4', 'claude-2.0', 'PER_1000_TOKENS', 0.01102, 'USD', 'PROMPT'),
|
||||
('clnnuqcns000608l40qaz8trt', 'claude-2.0', 'PER_1000_TOKENS', 0.03268, 'USD', 'COMPLETION'),
|
||||
('clnnuuif8000808l4gal4fjq4', 'claude-1.0', 'PER_1000_TOKENS', 0.01102, 'USD', 'PROMPT'),
|
||||
('clnnutptp000708l47r091vvd', 'claude-1.0', 'PER_1000_TOKENS', 0.03268, 'USD', 'COMPLETION'),
|
||||
('clnon8riv000308mlgfr1agiv', 'text-embedding-ada-002', 'PER_1000_TOKENS', 0.0001, 'USD', 'PROMPT'),
|
||||
('clnon9kfz000408ml1bg81o6z', 'text-embedding-ada-002', 'PER_1000_TOKENS', 0.0001, 'USD', 'COMPLETION'),
|
||||
('clqwniv8a000d08l2frjl7mmw', 'codechat-bison-32k', 'PER_1000_CHARS', 0.0005, 'USD', 'PROMPT'),
|
||||
('clqwnj044000e08l2dfjm5g90', 'chat-bison-32k', 'PER_1000_CHARS', 0.0005, 'USD', 'PROMPT'),
|
||||
('clqwnj47r000f08l2a16fekls', 'chat-bison-32k', 'PER_1000_CHARS', 0.0005, 'USD', 'COMPLETION'),
|
||||
('clqwnj863000g08l2bwxgdapm', 'codechat-bison-32k', 'PER_1000_CHARS', 0.0005, 'USD', 'COMPLETION')
|
||||
ON CONFLICT (id) DO NOTHING;
|
||||
@@ -0,0 +1,3 @@
|
||||
-- CreateIndex
|
||||
CREATE INDEX CONCURRENTLY "observations_model_idx" ON "observations"("model");
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
-- CreateIndex
|
||||
CREATE INDEX CONCURRENTLY "pricings_model_name_idx" ON "pricings"("model_name");
|
||||
@@ -0,0 +1,2 @@
|
||||
-- AlterTable
|
||||
ALTER TABLE "traces" ADD COLUMN "tags" TEXT[] DEFAULT ARRAY[]::TEXT[];
|
||||
@@ -0,0 +1,2 @@
|
||||
-- CreateIndex
|
||||
CREATE INDEX "traces_tags_idx" ON "traces" USING GIN ("tags" array_ops);
|
||||
@@ -0,0 +1,8 @@
|
||||
/*
|
||||
Warnings:
|
||||
|
||||
- You are about to drop the column `status` on the `datasets` table. All the data in the column will be lost.
|
||||
|
||||
*/
|
||||
-- AlterTable
|
||||
ALTER TABLE "datasets" DROP COLUMN "status";
|
||||
@@ -0,0 +1,16 @@
|
||||
-- Migration to add gpt-35 spelling
|
||||
|
||||
INSERT INTO pricings (
|
||||
id,
|
||||
model_name,
|
||||
pricing_unit,
|
||||
price,
|
||||
currency,
|
||||
token_type
|
||||
)
|
||||
VALUES
|
||||
('clqqpc2pr000008l3hvy63gxy1','gpt-35-turbo-1106', 'PER_1000_TOKENS', 0.001, 'USD', 'PROMPT'),
|
||||
('clqqpcb6d000208l3atrfbmou1','gpt-35-turbo-1106', 'PER_1000_TOKENS', 0.002, 'USD', 'COMPLETION'),
|
||||
('clqqpdh45000008lfgrnx76cv1','gpt-35-turbo-instruct', 'PER_1000_TOKENS', 0.0015, 'USD', 'PROMPT'),
|
||||
('clqqpdjya000108lf3s4b4c4m1','gpt-35-turbo-instruct', 'PER_1000_TOKENS', 0.002, 'USD', 'COMPLETION')
|
||||
ON CONFLICT (id) DO NOTHING;
|
||||
@@ -0,0 +1,2 @@
|
||||
-- DropIndex
|
||||
DROP INDEX "traces_project_id_external_id_key";
|
||||
+103
-22
@@ -7,9 +7,18 @@ generator client {
|
||||
}
|
||||
|
||||
datasource db {
|
||||
provider = "postgresql"
|
||||
url = env("DATABASE_URL")
|
||||
directUrl = env("DIRECT_URL")
|
||||
provider = "postgresql"
|
||||
url = env("DATABASE_URL")
|
||||
directUrl = env("DIRECT_URL")
|
||||
shadowDatabaseUrl = env("SHADOW_DATABASE_URL")
|
||||
}
|
||||
|
||||
generator erd {
|
||||
provider = "prisma-erd-generator"
|
||||
ignoreTables = ["_prisma_migrations", "Session", "Account", "Example"]
|
||||
disabled = true
|
||||
ignoreEnums = true
|
||||
output = "database.svg"
|
||||
}
|
||||
|
||||
model Example {
|
||||
@@ -28,6 +37,8 @@ model Account {
|
||||
refresh_token String? // @db.Text
|
||||
access_token String? // @db.Text
|
||||
expires_at Int?
|
||||
expires_in Int?
|
||||
ext_expires_in Int?
|
||||
token_type String?
|
||||
scope String?
|
||||
id_token String? // @db.Text
|
||||
@@ -47,19 +58,20 @@ model Session {
|
||||
}
|
||||
|
||||
model User {
|
||||
id String @id @default(cuid())
|
||||
id String @id @default(cuid())
|
||||
name String?
|
||||
email String? @unique
|
||||
emailVerified DateTime? @map("email_verified")
|
||||
email String? @unique
|
||||
emailVerified DateTime? @map("email_verified")
|
||||
password String?
|
||||
image String?
|
||||
admin Boolean @default(false)
|
||||
admin Boolean @default(false)
|
||||
accounts Account[]
|
||||
sessions Session[]
|
||||
memberships Membership[]
|
||||
createdAt DateTime @default(now()) @map("created_at")
|
||||
updatedAt DateTime @default(now()) @updatedAt @map("updated_at")
|
||||
featureFlags String[] @default([]) @map("feature_flags")
|
||||
invitations MembershipInvitation[]
|
||||
createdAt DateTime @default(now()) @map("created_at")
|
||||
updatedAt DateTime @default(now()) @updatedAt @map("updated_at")
|
||||
featureFlags String[] @default([]) @map("feature_flags")
|
||||
|
||||
@@map("users")
|
||||
}
|
||||
@@ -74,16 +86,20 @@ model VerificationToken {
|
||||
}
|
||||
|
||||
model Project {
|
||||
id String @id @default(cuid())
|
||||
createdAt DateTime @default(now()) @map("created_at")
|
||||
updatedAt DateTime @default(now()) @updatedAt @map("updated_at")
|
||||
id String @id @default(cuid())
|
||||
createdAt DateTime @default(now()) @map("created_at")
|
||||
updatedAt DateTime @default(now()) @updatedAt @map("updated_at")
|
||||
name String
|
||||
cloudConfig Json? @map("cloud_config") // Langfuse Cloud, for zod schema see projectsRouter.ts
|
||||
members Membership[]
|
||||
traces Trace[]
|
||||
observations Observation[]
|
||||
apiKeys ApiKey[]
|
||||
dataset Dataset[]
|
||||
RawEvents Events[]
|
||||
invitations MembershipInvitation[]
|
||||
sessions TraceSession[]
|
||||
Prompt Prompt[]
|
||||
|
||||
@@map("projects")
|
||||
}
|
||||
@@ -122,6 +138,22 @@ model Membership {
|
||||
@@map("memberships")
|
||||
}
|
||||
|
||||
model MembershipInvitation {
|
||||
id String @id @unique @default(cuid())
|
||||
email String
|
||||
role MembershipRole
|
||||
projectId String @map("project_id")
|
||||
project Project @relation(fields: [projectId], references: [id], onDelete: Cascade)
|
||||
senderId String? @map("sender_id")
|
||||
sender User? @relation(fields: [senderId], references: [id], onDelete: SetNull)
|
||||
createdAt DateTime @default(now()) @map("created_at")
|
||||
updatedAt DateTime @default(now()) @updatedAt @map("updated_at")
|
||||
|
||||
@@index([projectId])
|
||||
@@index([email])
|
||||
@@map("membership_invitations")
|
||||
}
|
||||
|
||||
enum MembershipRole {
|
||||
OWNER
|
||||
ADMIN
|
||||
@@ -129,28 +161,51 @@ enum MembershipRole {
|
||||
VIEWER
|
||||
}
|
||||
|
||||
model Trace {
|
||||
model TraceSession {
|
||||
id String @id @default(cuid())
|
||||
externalId String? @map("external_id")
|
||||
timestamp DateTime @default(now())
|
||||
createdAt DateTime @default(now()) @map("created_at")
|
||||
updatedAt DateTime @default(now()) @updatedAt @map("updated_at")
|
||||
projectId String @map("project_id")
|
||||
project Project @relation(fields: [projectId], references: [id], onDelete: Cascade)
|
||||
bookmarked Boolean @default(false)
|
||||
public Boolean @default(false)
|
||||
traces Trace[]
|
||||
|
||||
@@unique([id, projectId])
|
||||
@@index([projectId])
|
||||
@@index([createdAt])
|
||||
@@map("trace_sessions")
|
||||
}
|
||||
|
||||
model Trace {
|
||||
id String @id @default(cuid())
|
||||
externalId String? @map("external_id")
|
||||
timestamp DateTime @default(now())
|
||||
name String?
|
||||
userId String? @map("user_id")
|
||||
userId String? @map("user_id")
|
||||
metadata Json?
|
||||
release String?
|
||||
version String?
|
||||
projectId String @map("project_id")
|
||||
project Project @relation(fields: [projectId], references: [id], onDelete: Cascade)
|
||||
public Boolean @default(false)
|
||||
projectId String @map("project_id")
|
||||
project Project @relation(fields: [projectId], references: [id], onDelete: Cascade)
|
||||
public Boolean @default(false)
|
||||
bookmarked Boolean @default(false)
|
||||
tags String[] @default([])
|
||||
input Json?
|
||||
output Json?
|
||||
sessionId String? @map("session_id")
|
||||
session TraceSession? @relation(fields: [sessionId], references: [id], onDelete: SetNull)
|
||||
|
||||
scores Score[]
|
||||
|
||||
@@unique([projectId, externalId])
|
||||
@@index([projectId])
|
||||
@@index([sessionId])
|
||||
@@index([name])
|
||||
@@index([userId])
|
||||
@@index([externalId])
|
||||
@@index(timestamp)
|
||||
@@index(release)
|
||||
@@index([tags(ops: ArrayOps)], type: Gin)
|
||||
@@map("traces")
|
||||
}
|
||||
|
||||
@@ -177,12 +232,16 @@ model Observation {
|
||||
promptTokens Int @default(0) @map("prompt_tokens")
|
||||
completionTokens Int @default(0) @map("completion_tokens")
|
||||
totalTokens Int @default(0) @map("total_tokens")
|
||||
unit String @default("TOKENS")
|
||||
completionStartTime DateTime? @map("completion_start_time")
|
||||
scores Score[]
|
||||
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])
|
||||
|
||||
@@unique([id, projectId])
|
||||
@@index([traceId])
|
||||
@@index([type])
|
||||
@@ -190,6 +249,7 @@ model Observation {
|
||||
@@index(createdAt)
|
||||
@@index(projectId)
|
||||
@@index(parentObservationId)
|
||||
@@index(model)
|
||||
@@map("observations")
|
||||
}
|
||||
|
||||
@@ -243,6 +303,7 @@ model Pricing {
|
||||
currency String @default("USD")
|
||||
tokenType TokenType @map("token_type")
|
||||
|
||||
@@index(modelName)
|
||||
@@map("pricings")
|
||||
}
|
||||
|
||||
@@ -258,7 +319,6 @@ model CronJobs {
|
||||
model Dataset {
|
||||
id String @id @default(cuid())
|
||||
name String
|
||||
status DatasetStatus @default(ACTIVE)
|
||||
projectId String @map("project_id")
|
||||
project Project @relation(fields: [projectId], references: [id], onDelete: Cascade)
|
||||
createdAt DateTime @default(now()) @map("created_at")
|
||||
@@ -339,3 +399,24 @@ model Events {
|
||||
@@index(projectId)
|
||||
@@map("events")
|
||||
}
|
||||
|
||||
model Prompt {
|
||||
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)
|
||||
|
||||
createdBy String @map("created_by")
|
||||
|
||||
prompt String
|
||||
name String
|
||||
version Int
|
||||
isActive Boolean @map("is_active")
|
||||
Observation Observation[]
|
||||
|
||||
@@unique([projectId, name, version])
|
||||
@@index([projectId, name, version])
|
||||
@@map("prompts")
|
||||
}
|
||||
|
||||
+129
-66
@@ -53,6 +53,25 @@ async function main() {
|
||||
},
|
||||
});
|
||||
|
||||
const prompt = await prisma.prompt.upsert({
|
||||
where: {
|
||||
projectId_name_version: {
|
||||
projectId: seedProjectId,
|
||||
name: "summary-prompt",
|
||||
version: 1,
|
||||
},
|
||||
},
|
||||
create: {
|
||||
name: "summary-prompt",
|
||||
project: { connect: { id: seedProjectId } },
|
||||
prompt: "prompt {{variable}} {{anotherVariable}}",
|
||||
isActive: true,
|
||||
version: 1,
|
||||
createdBy: "user-1",
|
||||
},
|
||||
update: {},
|
||||
});
|
||||
|
||||
const seedApiKey = {
|
||||
id: "seed-api-key",
|
||||
secret: process.env.SEED_SECRET_KEY ?? "sk-lf-1234567890",
|
||||
@@ -103,13 +122,15 @@ async function main() {
|
||||
});
|
||||
|
||||
const generationIds: string[] = [];
|
||||
const envTags = [null, "development", "staging", "production"];
|
||||
const colorTags = [null, "red", "blue", "yellow"];
|
||||
|
||||
for (let i = 0; i < TRACE_VOLUME; i++) {
|
||||
// print progress to console with a progress bar that refreshes every 10 iterations
|
||||
if (i % 10 === 0) {
|
||||
if ((i + 1) % 10 === 0 || i === TRACE_VOLUME - 1) {
|
||||
process.stdout.clearLine(0);
|
||||
process.stdout.cursorTo(0);
|
||||
process.stdout.write(`Seeding ${i} of ${TRACE_VOLUME}`);
|
||||
process.stdout.write(`Seeding ${i + 1} of ${TRACE_VOLUME}`);
|
||||
}
|
||||
// random date within last 90 days, with a linear bias towards more recent dates
|
||||
const traceTs = new Date(
|
||||
@@ -117,6 +138,11 @@ async function main() {
|
||||
Math.floor(Math.random() ** 1.5 * 90 * 24 * 60 * 60 * 1000),
|
||||
);
|
||||
|
||||
const envTag = envTags[Math.floor(Math.random() * envTags.length)];
|
||||
const colorTag = colorTags[Math.floor(Math.random() * colorTags.length)];
|
||||
|
||||
const tags = [envTag, colorTag].filter((tag) => tag !== null);
|
||||
|
||||
const trace = await prisma.trace.create({
|
||||
data: {
|
||||
id: `trace-${Math.floor(Math.random() * 1000000000)}`,
|
||||
@@ -127,25 +153,49 @@ async function main() {
|
||||
metadata: {
|
||||
user: `user-${i}@langfuse.com`,
|
||||
},
|
||||
tags: tags as string[],
|
||||
project: {
|
||||
connect: {
|
||||
id: [project1.id, project2.id][i % 2],
|
||||
},
|
||||
},
|
||||
userId: `user-${i % 10}`,
|
||||
session:
|
||||
Math.random() > 0.3
|
||||
? {
|
||||
connectOrCreate: {
|
||||
where: {
|
||||
id: `session-${i % 10}`,
|
||||
},
|
||||
create: {
|
||||
id: `session-${i % 10}`,
|
||||
project: {
|
||||
connect: { id: [project1.id, project2.id][i % 2] },
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
: undefined,
|
||||
input:
|
||||
Math.random() > 0.3
|
||||
? "I'm looking for a React component"
|
||||
: undefined,
|
||||
output:
|
||||
Math.random() > 0.3
|
||||
? "What kind of component are you looking for?"
|
||||
: undefined,
|
||||
scores: {
|
||||
createMany: {
|
||||
data: [
|
||||
{
|
||||
name: "latency",
|
||||
value: Math.floor(Math.random() * 20),
|
||||
timestamp: traceTs,
|
||||
},
|
||||
{
|
||||
name: "feedback",
|
||||
value: Math.floor(Math.random() * 3) - 1,
|
||||
timestamp: traceTs,
|
||||
},
|
||||
...(Math.random() > 0.5
|
||||
? [
|
||||
{
|
||||
name: "feedback",
|
||||
value: Math.floor(Math.random() * 3) - 1,
|
||||
timestamp: traceTs,
|
||||
},
|
||||
]
|
||||
: []),
|
||||
...(Math.random() > 0.7
|
||||
? [
|
||||
{
|
||||
@@ -303,25 +353,31 @@ async function main() {
|
||||
totalTokens: promptTokens + completionTokens,
|
||||
parentObservationId: span.id,
|
||||
traceId: trace.id,
|
||||
...{
|
||||
...(Math.random() > 0.5 && trace.projectId === prompt.projectId
|
||||
? { prompt: { connect: { id: prompt.id } } }
|
||||
: {}),
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
await prisma.score.create({
|
||||
data: {
|
||||
name: "quality",
|
||||
value: Math.random() * 2 - 1,
|
||||
observationId: generation.id,
|
||||
traceId: trace.id,
|
||||
},
|
||||
});
|
||||
await prisma.score.create({
|
||||
data: {
|
||||
name: "conciseness",
|
||||
value: Math.random() * 2 - 1,
|
||||
observationId: generation.id,
|
||||
traceId: trace.id,
|
||||
},
|
||||
});
|
||||
if (Math.random() > 0.6)
|
||||
await prisma.score.create({
|
||||
data: {
|
||||
name: "quality",
|
||||
value: Math.random() * 2 - 1,
|
||||
observationId: generation.id,
|
||||
traceId: trace.id,
|
||||
},
|
||||
});
|
||||
if (Math.random() > 0.6)
|
||||
await prisma.score.create({
|
||||
data: {
|
||||
name: "conciseness",
|
||||
value: Math.random() * 2 - 1,
|
||||
observationId: generation.id,
|
||||
traceId: trace.id,
|
||||
},
|
||||
});
|
||||
|
||||
generationIds.push(generation.id);
|
||||
|
||||
@@ -353,47 +409,54 @@ async function main() {
|
||||
}
|
||||
}
|
||||
|
||||
const dataset = await prisma.dataset.create({
|
||||
data: {
|
||||
name: "demo-dataset",
|
||||
projectId: project2.id,
|
||||
},
|
||||
});
|
||||
|
||||
const datasetRun = await prisma.datasetRuns.create({
|
||||
data: {
|
||||
name: "demo-dataset-run",
|
||||
datasetId: dataset.id,
|
||||
},
|
||||
});
|
||||
|
||||
for (let runNumber = 0; runNumber < 10; runNumber++) {
|
||||
//pick randomly from existingSpanIds
|
||||
const sourceObservationId =
|
||||
generationIds[Math.floor(Math.random() * generationIds.length)];
|
||||
const runObservationId =
|
||||
generationIds[Math.floor(Math.random() * generationIds.length)];
|
||||
|
||||
const datasetItem = await prisma.datasetItem.create({
|
||||
for (let datasetNumber = 0; datasetNumber < 2; datasetNumber++) {
|
||||
const dataset = await prisma.dataset.create({
|
||||
data: {
|
||||
datasetId: dataset.id,
|
||||
sourceObservationId:
|
||||
Math.random() > 0.5 ? sourceObservationId : undefined,
|
||||
input: [
|
||||
{ role: "user", content: "How can i create a React component?" },
|
||||
],
|
||||
expectedOutput:
|
||||
"Creating a React component can be done in two ways: as a functional component or as a class component. Let's start with a basic example of both.",
|
||||
name: `demo-dataset-${datasetNumber}`,
|
||||
projectId: project2.id,
|
||||
},
|
||||
});
|
||||
|
||||
await prisma.datasetRunItems.create({
|
||||
data: {
|
||||
datasetItemId: datasetItem.id,
|
||||
observationId: runObservationId!,
|
||||
datasetRunId: datasetRun.id,
|
||||
},
|
||||
});
|
||||
for (let datasetRunNumber = 0; datasetRunNumber < 2; datasetRunNumber++) {
|
||||
const datasetRun = await prisma.datasetRuns.create({
|
||||
data: {
|
||||
name: `demo-dataset-run-${datasetRunNumber}`,
|
||||
datasetId: dataset.id,
|
||||
},
|
||||
});
|
||||
|
||||
for (let runNumber = 0; runNumber < 10; runNumber++) {
|
||||
//pick randomly from existingSpanIds
|
||||
const sourceObservationId =
|
||||
generationIds[Math.floor(Math.random() * generationIds.length)];
|
||||
const runObservationId =
|
||||
generationIds[Math.floor(Math.random() * generationIds.length)];
|
||||
|
||||
const datasetItem = await prisma.datasetItem.create({
|
||||
data: {
|
||||
datasetId: dataset.id,
|
||||
sourceObservationId:
|
||||
Math.random() > 0.5 ? sourceObservationId : undefined,
|
||||
input: [
|
||||
{
|
||||
role: "user",
|
||||
content: "How can i create a React component?",
|
||||
},
|
||||
],
|
||||
expectedOutput:
|
||||
"Creating a React component can be done in two ways: as a functional component or as a class component. Let's start with a basic example of both.",
|
||||
},
|
||||
});
|
||||
|
||||
await prisma.datasetRunItems.create({
|
||||
data: {
|
||||
datasetItemId: datasetItem.id,
|
||||
observationId: runObservationId!,
|
||||
datasetRunId: datasetRun.id,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
-11
@@ -1,11 +0,0 @@
|
||||
# Security Policy
|
||||
|
||||
## Supported Versions
|
||||
|
||||
We always recommend using the latest version of Langfuse to ensure you get all security updates.
|
||||
|
||||
## Reporting a Vulnerability
|
||||
|
||||
Please report security vulnerabilities to security@langfuse.com.
|
||||
|
||||
We currently do not operate a bug bounty program.
|
||||
@@ -9,7 +9,7 @@ if (process.env.NEXT_PUBLIC_SENTRY_DSN)
|
||||
// Set tracesSampleRate to 1.0 to capture 100%
|
||||
// of transactions for performance monitoring.
|
||||
// We recommend adjusting this value in production
|
||||
tracesSampleRate: 0.8,
|
||||
tracesSampleRate: 0.3,
|
||||
|
||||
// Capture Replay for 100% of all sessions,
|
||||
// plus for 100% of sessions with an error
|
||||
|
||||
@@ -7,7 +7,7 @@ if (process.env.NEXT_PUBLIC_SENTRY_DSN)
|
||||
// Set tracesSampleRate to 1.0 to capture 100%
|
||||
// of transactions for performance monitoring.
|
||||
// We recommend adjusting this value in production
|
||||
tracesSampleRate: 0.75,
|
||||
tracesSampleRate: 0.3,
|
||||
|
||||
// ...
|
||||
|
||||
|
||||
+34
-2
@@ -1,23 +1,55 @@
|
||||
import { prisma } from "@/src/server/db";
|
||||
import { jsonSchema } from "@/src/utils/zod";
|
||||
import * as Sentry from "@sentry/nextjs";
|
||||
import { ProfilingIntegration } from "@sentry/profiling-node";
|
||||
import type { TransactionEvent } from "@sentry/types";
|
||||
|
||||
if (process.env.NEXT_PUBLIC_SENTRY_DSN)
|
||||
Sentry.init({
|
||||
// debug: true,
|
||||
dsn: process.env.NEXT_PUBLIC_SENTRY_DSN,
|
||||
|
||||
// Set tracesSampleRate to 1.0 to capture 100%
|
||||
// of transactions for performance monitoring.
|
||||
// We recommend adjusting this value in production
|
||||
tracesSampleRate: 0.8,
|
||||
tracesSampleRate: 0.3,
|
||||
|
||||
profilesSampleRate: 0.8, // Profiling sample rate is relative to tracesSampleRate
|
||||
profilesSampleRate: 0.3, // Profiling sample rate is relative to tracesSampleRate
|
||||
integrations: [
|
||||
// Add profiling integration to list of integrations
|
||||
new ProfilingIntegration(),
|
||||
new Sentry.Integrations.Prisma({ client: prisma }),
|
||||
],
|
||||
|
||||
// filter out passwords from the signup request body
|
||||
// transaction events are sentry transactions which include logs and spans.
|
||||
beforeSendTransaction(transaction: TransactionEvent) {
|
||||
if (
|
||||
transaction.request &&
|
||||
typeof transaction.request.data === "string" &&
|
||||
transaction.request.url &&
|
||||
transaction.request.url.includes("api/auth/signup")
|
||||
) {
|
||||
const jsonBody = jsonSchema.safeParse(transaction.request.data);
|
||||
|
||||
if (
|
||||
jsonBody.success &&
|
||||
typeof jsonBody.data === "object" &&
|
||||
"data" in jsonBody.data
|
||||
) {
|
||||
delete jsonBody.data.password;
|
||||
transaction.request.data = JSON.stringify(jsonBody.data);
|
||||
transaction.request.data = JSON.stringify(jsonBody);
|
||||
} else {
|
||||
console.log("Signup: Non Json Request body");
|
||||
}
|
||||
|
||||
return transaction;
|
||||
}
|
||||
|
||||
return transaction;
|
||||
},
|
||||
|
||||
// ...
|
||||
|
||||
// Note: if you want to override the automatic release value, do not set a
|
||||
|
||||
@@ -0,0 +1,187 @@
|
||||
/** @jest-environment node */
|
||||
|
||||
import { prisma } from "@/src/server/db";
|
||||
import { makeAPICall, pruneDatabase } from "@/src/__tests__/test-utils";
|
||||
import { v4 } from "uuid";
|
||||
|
||||
describe("/api/public/datasets and /api/public/dataset-items API Endpoints", () => {
|
||||
beforeEach(async () => await pruneDatabase());
|
||||
afterEach(async () => await pruneDatabase());
|
||||
|
||||
it("should create and get a dataset", async () => {
|
||||
await makeAPICall("POST", "/api/public/datasets", {
|
||||
name: "dataset-name",
|
||||
});
|
||||
|
||||
const dbDataset = await prisma.dataset.findMany({
|
||||
where: {
|
||||
name: "dataset-name",
|
||||
},
|
||||
});
|
||||
|
||||
expect(dbDataset.length).toBeGreaterThan(0);
|
||||
|
||||
const getDataset = await makeAPICall(
|
||||
"GET",
|
||||
`/api/public/datasets/dataset-name`,
|
||||
);
|
||||
|
||||
expect(getDataset.status).toBe(200);
|
||||
expect(getDataset.body).toMatchObject({
|
||||
name: "dataset-name",
|
||||
});
|
||||
});
|
||||
|
||||
it("should create and get a dataset item (via datasets and individually)", async () => {
|
||||
await makeAPICall("POST", "/api/public/datasets", {
|
||||
name: "dataset-name",
|
||||
});
|
||||
await makeAPICall("POST", "/api/public/dataset-items", {
|
||||
datasetName: "dataset-name",
|
||||
input: { key: "value" },
|
||||
expectedOutput: { key: "value" },
|
||||
});
|
||||
const dbDatasetItem = await prisma.datasetItem.findFirst({
|
||||
where: {
|
||||
dataset: {
|
||||
name: "dataset-name",
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
expect(dbDatasetItem).not.toBeNull();
|
||||
|
||||
const getDataset = await makeAPICall(
|
||||
"GET",
|
||||
`/api/public/datasets/dataset-name`,
|
||||
);
|
||||
expect(getDataset.status).toBe(200);
|
||||
expect(getDataset.body).toMatchObject({
|
||||
name: "dataset-name",
|
||||
items: [
|
||||
{
|
||||
id: dbDatasetItem!.id,
|
||||
input: { key: "value" },
|
||||
expectedOutput: { key: "value" },
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
const getDatasetItem = await makeAPICall(
|
||||
"GET",
|
||||
`/api/public/dataset-items/${dbDatasetItem!.id}`,
|
||||
);
|
||||
expect(getDatasetItem.status).toBe(200);
|
||||
expect(getDatasetItem.body).toMatchObject({
|
||||
id: dbDatasetItem!.id,
|
||||
input: { key: "value" },
|
||||
expectedOutput: { key: "value" },
|
||||
});
|
||||
});
|
||||
|
||||
it("should upsert a dataset item", async () => {
|
||||
await makeAPICall("POST", "/api/public/datasets", {
|
||||
name: "dataset-name",
|
||||
});
|
||||
|
||||
const item1 = await makeAPICall("POST", "/api/public/dataset-items", {
|
||||
id: "dataset-item-id",
|
||||
datasetName: "dataset-name",
|
||||
input: { key: "value" },
|
||||
});
|
||||
expect(item1.status).toBe(200);
|
||||
expect(item1.body).toMatchObject({
|
||||
id: "dataset-item-id",
|
||||
});
|
||||
|
||||
const item2 = await makeAPICall("POST", "/api/public/dataset-items", {
|
||||
id: "dataset-item-id",
|
||||
datasetName: "dataset-name",
|
||||
input: { key: "value2" },
|
||||
});
|
||||
expect(item2.status).toBe(200);
|
||||
expect(item2.body).toMatchObject({
|
||||
id: "dataset-item-id",
|
||||
input: { key: "value2" },
|
||||
});
|
||||
|
||||
const dbDatasetItem = await prisma.datasetItem.findFirst({
|
||||
where: { id: "dataset-item-id" },
|
||||
});
|
||||
expect(dbDatasetItem).not.toBeNull();
|
||||
expect(dbDatasetItem?.input).toMatchObject({ key: "value2" });
|
||||
});
|
||||
|
||||
it("should create and get a dataset run", async () => {
|
||||
const dataset = await makeAPICall("POST", "/api/public/datasets", {
|
||||
name: "dataset-name",
|
||||
});
|
||||
expect(dataset.status).toBe(200);
|
||||
expect(dataset.body).toMatchObject({
|
||||
name: "dataset-name",
|
||||
});
|
||||
await makeAPICall("POST", "/api/public/dataset-items", {
|
||||
datasetName: "dataset-name",
|
||||
id: "dataset-item-id",
|
||||
input: { key: "value" },
|
||||
expectedOutput: { key: "value" },
|
||||
});
|
||||
const traceId = v4();
|
||||
const observationId = v4();
|
||||
const response = await makeAPICall("POST", "/api/public/ingestion", {
|
||||
batch: [
|
||||
{
|
||||
id: v4(),
|
||||
type: "trace-create",
|
||||
timestamp: new Date().toISOString(),
|
||||
body: {
|
||||
id: traceId,
|
||||
name: "trace-name",
|
||||
userId: "user-1",
|
||||
metadata: { key: "value" },
|
||||
release: "1.0.0",
|
||||
version: "2.0.0",
|
||||
},
|
||||
},
|
||||
{
|
||||
id: v4(),
|
||||
type: "observation-create",
|
||||
timestamp: new Date().toISOString(),
|
||||
body: {
|
||||
id: observationId,
|
||||
traceId: traceId,
|
||||
type: "GENERATION",
|
||||
name: "generation-name",
|
||||
startTime: "2021-01-01T00:00:00.000Z",
|
||||
endTime: "2021-01-01T00:00:00.000Z",
|
||||
modelParameters: { key: "value" },
|
||||
input: { key: "value" },
|
||||
metadata: { key: "value" },
|
||||
version: "2.0.0",
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
expect(response.status).toBe(207);
|
||||
|
||||
const runItem = await makeAPICall("POST", "/api/public/dataset-run-items", {
|
||||
datasetItemId: "dataset-item-id",
|
||||
observationId: observationId,
|
||||
runName: "run-name",
|
||||
});
|
||||
const dbRun = await prisma.datasetRuns.findFirst({
|
||||
where: {
|
||||
name: "run-name",
|
||||
},
|
||||
});
|
||||
expect(dbRun).not.toBeNull();
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
|
||||
expect(dbRun?.datasetId).toBe(dataset.body.id);
|
||||
expect(runItem.status).toBe(200);
|
||||
expect(runItem.body).toMatchObject({
|
||||
datasetItemId: "dataset-item-id",
|
||||
observationId: observationId,
|
||||
datasetRunId: dbRun?.id,
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -241,12 +241,12 @@ describe("/api/public/events API Endpoint", () => {
|
||||
|
||||
expect(createSpan.status).toBe(200);
|
||||
|
||||
expect(dbEvent?.id).toBe(spanId);
|
||||
expect(dbEvent?.traceId).toBe(dbTrace[0]?.id);
|
||||
expect(dbEvent?.name).toBe(generationName);
|
||||
expect(dbEvent?.startTime).toEqual(new Date("2021-01-01T00:00:00.000Z"));
|
||||
expect(dbEvent?.input).toEqual({ key: "value" });
|
||||
expect(dbEvent?.metadata).toEqual({ key: "value" });
|
||||
expect(dbEvent?.version).toBe("2.0.0");
|
||||
expect(dbEvent.id).toBe(spanId);
|
||||
expect(dbEvent.traceId).toBe(dbTrace[0]?.id);
|
||||
expect(dbEvent.name).toBe(generationName);
|
||||
expect(dbEvent.startTime).toEqual(new Date("2021-01-01T00:00:00.000Z"));
|
||||
expect(dbEvent.input).toEqual({ key: "value" });
|
||||
expect(dbEvent.metadata).toEqual({ key: "value" });
|
||||
expect(dbEvent.version).toBe("2.0.0");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -0,0 +1,110 @@
|
||||
/* eslint-disable @typescript-eslint/no-unsafe-assignment */
|
||||
/* eslint-disable @typescript-eslint/no-unsafe-call */
|
||||
import { cleanup, render } from "@testing-library/react";
|
||||
|
||||
import { TestRouter } from "@/src/__tests__/fixtures/TestRouter";
|
||||
import { useOrderByState } from "@/src/features/orderBy/hooks/useOrderByState";
|
||||
import { LocationMock } from "@jedmao/location";
|
||||
import NextAdapterPages from "next-query-params/pages";
|
||||
import { useRouter } from "next/router";
|
||||
import { QueryParamProvider } from "use-query-params";
|
||||
|
||||
afterEach(cleanup);
|
||||
|
||||
jest.mock("next/router", () => ({
|
||||
useRouter: jest.fn(),
|
||||
}));
|
||||
|
||||
const { location: savedLocation } = window;
|
||||
|
||||
// The test for the useOrderByState hook
|
||||
describe("useOrderByState hook", () => {
|
||||
let testRouter: TestRouter;
|
||||
let locationMock: LocationMock;
|
||||
|
||||
beforeAll(() => {
|
||||
// @ts-expect-error - TS only allows to delete optional params; but here we want to delete it
|
||||
// as it will be set in beforeEach
|
||||
delete window.location;
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
jest.resetAllMocks();
|
||||
locationMock = new LocationMock("https://langfuse.com");
|
||||
testRouter = new TestRouter(locationMock);
|
||||
window.location = locationMock;
|
||||
|
||||
(useRouter as jest.Mock).mockReturnValue(testRouter);
|
||||
});
|
||||
|
||||
afterAll(() => {
|
||||
window.location = savedLocation;
|
||||
});
|
||||
|
||||
test("orderBy takes the default value if no url param is given", () => {
|
||||
const result = render(
|
||||
<QueryParamProvider adapter={NextAdapterPages}>
|
||||
<UseOrderByStateExample />
|
||||
</QueryParamProvider>,
|
||||
);
|
||||
|
||||
expect(result.queryByText(/column Column 1, order DESC/)).toBeTruthy();
|
||||
});
|
||||
|
||||
test("setOrderBy updates orderBy query param", () => {
|
||||
const result = render(
|
||||
<QueryParamProvider adapter={NextAdapterPages}>
|
||||
<UseOrderByStateExample />
|
||||
</QueryParamProvider>,
|
||||
);
|
||||
|
||||
result.queryByText(/Set Order/)!.click();
|
||||
result.rerender(
|
||||
<QueryParamProvider adapter={NextAdapterPages}>
|
||||
<UseOrderByStateExample />
|
||||
</QueryParamProvider>,
|
||||
);
|
||||
|
||||
expect(result.queryByText(/column Column 2, order ASC/)).toBeTruthy();
|
||||
expect(locationMock.searchParams.get("orderBy")).toBe(
|
||||
"column-Column 2_order-ASC",
|
||||
);
|
||||
});
|
||||
|
||||
test("orderBy reads the given param from the url", () => {
|
||||
locationMock.replace(
|
||||
"https://langfuse.com?orderBy=column-Column 3_order-DESC",
|
||||
);
|
||||
|
||||
const result = render(
|
||||
<QueryParamProvider adapter={NextAdapterPages}>
|
||||
<UseOrderByStateExample />
|
||||
</QueryParamProvider>,
|
||||
);
|
||||
|
||||
expect(result.queryByText(/column Column 3, order DESC/)).toBeTruthy();
|
||||
});
|
||||
});
|
||||
|
||||
const UseOrderByStateExample = () => {
|
||||
const [orderBy, setOrderBy] = useOrderByState({
|
||||
column: "Column 1",
|
||||
order: "DESC",
|
||||
});
|
||||
|
||||
return (
|
||||
<>
|
||||
<h1>{`column ${orderBy?.column}, order ${orderBy?.order}`}</h1>
|
||||
<button
|
||||
onClick={() => {
|
||||
setOrderBy({
|
||||
column: "Column 2",
|
||||
order: "ASC",
|
||||
});
|
||||
}}
|
||||
>
|
||||
Set Order
|
||||
</button>
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,36 @@
|
||||
import { orderByToPrismaSql } from "@/src/features/orderBy/server/orderByToPrisma";
|
||||
import { tracesTableCols } from "@/src/server/api/definitions/tracesTable";
|
||||
import { Prisma } from "@prisma/client";
|
||||
|
||||
// The test for the orderByToPrisma function
|
||||
describe("orderByToPrisma (Convert orderBy to Prisma.sql)", () => {
|
||||
test("orderByToPrisma returns default sql when orderBy=null", () => {
|
||||
expect(orderByToPrismaSql(null, tracesTableCols)).toStrictEqual(
|
||||
Prisma.sql`ORDER BY t.timestamp DESC`,
|
||||
);
|
||||
});
|
||||
|
||||
test("orderByToPrisma returns correct clause for orderBy column included in column defs", () => {
|
||||
expect(
|
||||
orderByToPrismaSql(
|
||||
{
|
||||
column: "latency",
|
||||
order: "ASC",
|
||||
},
|
||||
tracesTableCols,
|
||||
),
|
||||
).toStrictEqual(Prisma.sql`ORDER BY tl.latency ASC`);
|
||||
});
|
||||
|
||||
test("orderByToPrisma throws error for orderBy column not included in column defs", () => {
|
||||
expect(() =>
|
||||
orderByToPrismaSql(
|
||||
{
|
||||
column: "InvalidCol",
|
||||
order: "ASC",
|
||||
},
|
||||
tracesTableCols,
|
||||
),
|
||||
).toThrow(/Invalid filter column: InvalidCol/);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,56 @@
|
||||
/* eslint-disable @typescript-eslint/no-unsafe-member-access */
|
||||
/* eslint-disable @typescript-eslint/no-unsafe-call */
|
||||
import { type Router } from "next/router";
|
||||
import type { UrlObject } from "url";
|
||||
import { type LocationMock } from "@jedmao/location";
|
||||
|
||||
type PartialRouter = Partial<Router>;
|
||||
|
||||
export const BASE_URL = "https://langfuse.com";
|
||||
|
||||
/**
|
||||
* A Router to be used for testing which provides the bare minimum needed
|
||||
* for the useQueryParam(s) hook and NextAdapter to work.
|
||||
*/
|
||||
export class TestRouter implements PartialRouter {
|
||||
isReady = true;
|
||||
pathname = "/";
|
||||
private currentUrl = "";
|
||||
private history: string[] = [];
|
||||
|
||||
constructor(private locationMock: LocationMock) {}
|
||||
|
||||
replace = (url: string | UrlObject) => {
|
||||
// eslint-disable-next-line @typescript-eslint/no-base-to-string, @typescript-eslint/restrict-template-expressions
|
||||
this.locationMock.assign(`${BASE_URL}${url}`);
|
||||
this.currentUrl = TestRouter.getURLString(url);
|
||||
this.locationMock.assign(`${BASE_URL}${this.currentUrl}`);
|
||||
return Promise.resolve(true);
|
||||
};
|
||||
|
||||
push = (url: string | UrlObject) => {
|
||||
this.history.push(this.currentUrl);
|
||||
this.currentUrl = TestRouter.getURLString(url);
|
||||
this.locationMock.assign(`${BASE_URL}${this.currentUrl}`);
|
||||
return Promise.resolve(true);
|
||||
};
|
||||
|
||||
setIsReady = (isReady: boolean) => {
|
||||
this.isReady = isReady;
|
||||
};
|
||||
|
||||
get asPath() {
|
||||
return this.pathname;
|
||||
}
|
||||
|
||||
static getURLString(url: string | UrlObject): string {
|
||||
if (typeof url === "string") {
|
||||
return url;
|
||||
}
|
||||
return `${url.pathname}${url.search}`;
|
||||
}
|
||||
|
||||
getParams(): URLSearchParams {
|
||||
return new URL(`${BASE_URL}${this.currentUrl}`).searchParams;
|
||||
}
|
||||
}
|
||||
@@ -6,69 +6,154 @@ import { v4 as uuidv4 } from "uuid";
|
||||
|
||||
describe("/api/public/generations API Endpoint", () => {
|
||||
beforeEach(async () => await pruneDatabase());
|
||||
afterEach(async () => await pruneDatabase());
|
||||
|
||||
it("should create generation after trace", async () => {
|
||||
await pruneDatabase();
|
||||
[
|
||||
{
|
||||
usage: {
|
||||
input: 100,
|
||||
output: 200,
|
||||
total: 100,
|
||||
unit: "CHARACTERS",
|
||||
},
|
||||
expectedUnit: "CHARACTERS",
|
||||
expectedPromptTokens: 100,
|
||||
expectedCompletionTokens: 200,
|
||||
expectedTotalTokens: 100,
|
||||
},
|
||||
{
|
||||
usage: {
|
||||
total: 100,
|
||||
unit: "CHARACTERS",
|
||||
},
|
||||
expectedUnit: "CHARACTERS",
|
||||
expectedPromptTokens: 0,
|
||||
expectedCompletionTokens: 0,
|
||||
expectedTotalTokens: 100,
|
||||
},
|
||||
{
|
||||
usage: {
|
||||
total: 100,
|
||||
},
|
||||
expectedUnit: "TOKENS",
|
||||
expectedPromptTokens: 0,
|
||||
expectedCompletionTokens: 0,
|
||||
expectedTotalTokens: 100,
|
||||
},
|
||||
{
|
||||
usage: {
|
||||
promptTokens: 100,
|
||||
completionTokens: 200,
|
||||
totalTokens: 100,
|
||||
},
|
||||
expectedPromptTokens: 100,
|
||||
expectedCompletionTokens: 200,
|
||||
expectedTotalTokens: 100,
|
||||
expectedUnit: "TOKENS",
|
||||
},
|
||||
{
|
||||
usage: {
|
||||
totalTokens: 100,
|
||||
},
|
||||
expectedPromptTokens: 0,
|
||||
expectedCompletionTokens: 0,
|
||||
expectedTotalTokens: 100,
|
||||
expectedUnit: "TOKENS",
|
||||
},
|
||||
{
|
||||
usage: undefined,
|
||||
expectedPromptTokens: 0,
|
||||
expectedCompletionTokens: 0,
|
||||
expectedTotalTokens: 0,
|
||||
expectedUnit: "TOKENS",
|
||||
},
|
||||
{
|
||||
usage: null,
|
||||
expectedPromptTokens: 0,
|
||||
expectedCompletionTokens: 0,
|
||||
expectedTotalTokens: 0,
|
||||
expectedUnit: "TOKENS",
|
||||
},
|
||||
{
|
||||
usage: {},
|
||||
expectedPromptTokens: 0,
|
||||
expectedCompletionTokens: 0,
|
||||
expectedTotalTokens: 0,
|
||||
expectedUnit: "TOKENS",
|
||||
},
|
||||
].forEach((testConfig) => {
|
||||
it(`should create generation after trace 1 ${JSON.stringify(
|
||||
testConfig,
|
||||
)}`, async () => {
|
||||
await pruneDatabase();
|
||||
|
||||
const traceId = uuidv4();
|
||||
const traceId = uuidv4();
|
||||
|
||||
await makeAPICall("POST", "/api/public/traces", {
|
||||
id: traceId,
|
||||
name: "trace-name",
|
||||
userId: "user-1",
|
||||
projectId: "7a88fb47-b4e2-43b8-a06c-a5ce950dc53a",
|
||||
metadata: { key: "value" },
|
||||
release: "1.0.0",
|
||||
version: "2.0.0",
|
||||
});
|
||||
|
||||
const dbTrace = await prisma.trace.findMany({
|
||||
where: {
|
||||
await makeAPICall("POST", "/api/public/traces", {
|
||||
id: traceId,
|
||||
},
|
||||
});
|
||||
|
||||
expect(dbTrace.length).toBeGreaterThan(0);
|
||||
expect(dbTrace[0]?.id).toBe(traceId);
|
||||
|
||||
const generationId = uuidv4();
|
||||
const createGeneration = await makeAPICall(
|
||||
"POST",
|
||||
"/api/public/generations",
|
||||
{
|
||||
id: generationId,
|
||||
traceId: traceId,
|
||||
name: "generation-name",
|
||||
startTime: "2021-01-01T00:00:00.000Z",
|
||||
endTime: "2021-01-01T00:00:00.000Z",
|
||||
model: "model-name",
|
||||
modelParameters: { key: "value" },
|
||||
prompt: { key: "value" },
|
||||
name: "trace-name",
|
||||
userId: "user-1",
|
||||
projectId: "7a88fb47-b4e2-43b8-a06c-a5ce950dc53a",
|
||||
metadata: { key: "value" },
|
||||
release: "1.0.0",
|
||||
version: "2.0.0",
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
expect(createGeneration.status).toBe(200);
|
||||
const dbGeneration = await prisma.observation.findUnique({
|
||||
where: {
|
||||
id: generationId,
|
||||
},
|
||||
const dbTrace = await prisma.trace.findMany({
|
||||
where: {
|
||||
id: traceId,
|
||||
},
|
||||
});
|
||||
|
||||
expect(dbTrace.length).toBeGreaterThan(0);
|
||||
expect(dbTrace[0]?.id).toBe(traceId);
|
||||
|
||||
const generationId = uuidv4();
|
||||
const createGeneration = await makeAPICall(
|
||||
"POST",
|
||||
"/api/public/generations",
|
||||
{
|
||||
id: generationId,
|
||||
traceId: traceId,
|
||||
name: "generation-name",
|
||||
startTime: "2021-01-01T00:00:00.000Z",
|
||||
endTime: "2021-01-01T00:00:00.000Z",
|
||||
model: "model-name",
|
||||
modelParameters: { key: "value" },
|
||||
prompt: { key: "value" },
|
||||
metadata: { key: "value" },
|
||||
version: "2.0.0",
|
||||
usage: testConfig.usage,
|
||||
},
|
||||
);
|
||||
|
||||
expect(createGeneration.status).toBe(200);
|
||||
const dbGeneration = await prisma.observation.findUnique({
|
||||
where: {
|
||||
id: generationId,
|
||||
},
|
||||
});
|
||||
|
||||
expect(dbGeneration?.id).toBe(generationId);
|
||||
expect(dbGeneration?.traceId).toBe(traceId);
|
||||
expect(dbGeneration?.name).toBe("generation-name");
|
||||
expect(dbGeneration?.startTime).toEqual(
|
||||
new Date("2021-01-01T00:00:00.000Z"),
|
||||
);
|
||||
expect(dbGeneration?.endTime).toEqual(
|
||||
new Date("2021-01-01T00:00:00.000Z"),
|
||||
);
|
||||
expect(dbGeneration?.model).toBe("model-name");
|
||||
expect(dbGeneration?.modelParameters).toEqual({ key: "value" });
|
||||
expect(dbGeneration?.input).toEqual({ key: "value" });
|
||||
expect(dbGeneration?.metadata).toEqual({ key: "value" });
|
||||
expect(dbGeneration?.version).toBe("2.0.0");
|
||||
expect(dbGeneration?.unit).toBe(testConfig.expectedUnit);
|
||||
expect(dbGeneration?.promptTokens).toBe(testConfig.expectedPromptTokens);
|
||||
expect(dbGeneration?.completionTokens).toBe(
|
||||
testConfig.expectedCompletionTokens,
|
||||
);
|
||||
expect(dbGeneration?.totalTokens).toBe(testConfig.expectedTotalTokens);
|
||||
});
|
||||
|
||||
expect(dbGeneration?.id).toBe(generationId);
|
||||
expect(dbGeneration?.traceId).toBe(traceId);
|
||||
expect(dbGeneration?.name).toBe("generation-name");
|
||||
expect(dbGeneration?.startTime).toEqual(
|
||||
new Date("2021-01-01T00:00:00.000Z"),
|
||||
);
|
||||
expect(dbGeneration?.endTime).toEqual(new Date("2021-01-01T00:00:00.000Z"));
|
||||
expect(dbGeneration?.model).toBe("model-name");
|
||||
expect(dbGeneration?.modelParameters).toEqual({ key: "value" });
|
||||
expect(dbGeneration?.input).toEqual({ key: "value" });
|
||||
expect(dbGeneration?.metadata).toEqual({ key: "value" });
|
||||
expect(dbGeneration?.version).toBe("2.0.0");
|
||||
});
|
||||
|
||||
it("should create generation before trace", async () => {
|
||||
@@ -339,8 +424,8 @@ describe("/api/public/generations API Endpoint", () => {
|
||||
name: generationName,
|
||||
},
|
||||
});
|
||||
expect(dbGeneration?.id).toBe(generationId);
|
||||
expect(dbGeneration?.traceId).toBe(externalTraceId);
|
||||
expect(dbGeneration.id).toBe(generationId);
|
||||
expect(dbGeneration.traceId).toBe(externalTraceId);
|
||||
|
||||
const dbTraces = await prisma.trace.findMany();
|
||||
expect(dbTraces.length).toBe(0);
|
||||
@@ -383,18 +468,18 @@ describe("/api/public/generations API Endpoint", () => {
|
||||
|
||||
expect(createGeneration.status).toBe(200);
|
||||
|
||||
expect(dbGeneration?.id).toBe(generationId);
|
||||
expect(dbGeneration?.traceId).toBe(dbTrace[0]?.id);
|
||||
expect(dbGeneration?.name).toBe(generationName);
|
||||
expect(dbGeneration?.startTime).toEqual(
|
||||
expect(dbGeneration.id).toBe(generationId);
|
||||
expect(dbGeneration.traceId).toBe(dbTrace[0]?.id);
|
||||
expect(dbGeneration.name).toBe(generationName);
|
||||
expect(dbGeneration.startTime).toEqual(
|
||||
new Date("2021-01-01T00:00:00.000Z"),
|
||||
);
|
||||
expect(dbGeneration?.endTime).toEqual(new Date("2021-01-01T00:00:00.000Z"));
|
||||
expect(dbGeneration?.model).toBe("model-name");
|
||||
expect(dbGeneration?.modelParameters).toEqual({ key: "value" });
|
||||
expect(dbGeneration?.input).toEqual({ key: "value" });
|
||||
expect(dbGeneration?.metadata).toEqual({ key: "value" });
|
||||
expect(dbGeneration?.version).toBe("2.0.0");
|
||||
expect(dbGeneration.endTime).toEqual(new Date("2021-01-01T00:00:00.000Z"));
|
||||
expect(dbGeneration.model).toBe("model-name");
|
||||
expect(dbGeneration.modelParameters).toEqual({ key: "value" });
|
||||
expect(dbGeneration.input).toEqual({ key: "value" });
|
||||
expect(dbGeneration.metadata).toEqual({ key: "value" });
|
||||
expect(dbGeneration.version).toBe("2.0.0");
|
||||
});
|
||||
|
||||
it("should update generation", async () => {
|
||||
|
||||
@@ -1,18 +1,268 @@
|
||||
/** @jest-environment node */
|
||||
|
||||
import { makeAPICall, pruneDatabase } from "@/src/__tests__/test-utils";
|
||||
import { cleanEvent } from "@/src/pages/api/public/ingestion";
|
||||
import { prisma } from "@/src/server/db";
|
||||
import { v4 } from "uuid";
|
||||
|
||||
describe("/api/public/ingestion API Endpoint", () => {
|
||||
beforeEach(async () => await pruneDatabase());
|
||||
|
||||
it("should create trace and generation", async () => {
|
||||
[
|
||||
{
|
||||
usage: {
|
||||
input: 100,
|
||||
output: 200,
|
||||
total: 100,
|
||||
unit: "CHARACTERS",
|
||||
},
|
||||
expectedUnit: "CHARACTERS",
|
||||
expectedPromptTokens: 100,
|
||||
expectedCompletionTokens: 200,
|
||||
expectedTotalTokens: 100,
|
||||
},
|
||||
{
|
||||
usage: {
|
||||
total: 100,
|
||||
unit: "CHARACTERS",
|
||||
},
|
||||
expectedUnit: "CHARACTERS",
|
||||
expectedPromptTokens: 0,
|
||||
expectedCompletionTokens: 0,
|
||||
expectedTotalTokens: 100,
|
||||
},
|
||||
{
|
||||
usage: {
|
||||
total: 100,
|
||||
},
|
||||
expectedUnit: "TOKENS",
|
||||
expectedPromptTokens: 0,
|
||||
expectedCompletionTokens: 0,
|
||||
expectedTotalTokens: 100,
|
||||
},
|
||||
{
|
||||
usage: {
|
||||
promptTokens: 100,
|
||||
completionTokens: 200,
|
||||
totalTokens: 100,
|
||||
},
|
||||
expectedPromptTokens: 100,
|
||||
expectedCompletionTokens: 200,
|
||||
expectedTotalTokens: 100,
|
||||
expectedUnit: "TOKENS",
|
||||
},
|
||||
{
|
||||
usage: {
|
||||
totalTokens: 100,
|
||||
},
|
||||
expectedPromptTokens: 0,
|
||||
expectedCompletionTokens: 0,
|
||||
expectedTotalTokens: 100,
|
||||
expectedUnit: "TOKENS",
|
||||
},
|
||||
{
|
||||
usage: undefined,
|
||||
expectedPromptTokens: 0,
|
||||
expectedCompletionTokens: 0,
|
||||
expectedTotalTokens: 0,
|
||||
expectedUnit: "TOKENS",
|
||||
},
|
||||
{
|
||||
usage: null,
|
||||
expectedPromptTokens: 0,
|
||||
expectedCompletionTokens: 0,
|
||||
expectedTotalTokens: 0,
|
||||
expectedUnit: "TOKENS",
|
||||
},
|
||||
{
|
||||
usage: {},
|
||||
expectedPromptTokens: 0,
|
||||
expectedCompletionTokens: 0,
|
||||
expectedTotalTokens: 0,
|
||||
expectedUnit: "TOKENS",
|
||||
},
|
||||
].forEach((testConfig) => {
|
||||
it(`should create trace and generation ${JSON.stringify(
|
||||
testConfig,
|
||||
)}`, async () => {
|
||||
const traceId = v4();
|
||||
const generationId = v4();
|
||||
const spanId = v4();
|
||||
const scoreId = v4();
|
||||
|
||||
const response = await makeAPICall("POST", "/api/public/ingestion", {
|
||||
metadata: {
|
||||
sdk_verion: "1.0.0",
|
||||
sdk_name: "python",
|
||||
},
|
||||
batch: [
|
||||
{
|
||||
id: v4(),
|
||||
type: "trace-create",
|
||||
timestamp: new Date().toISOString(),
|
||||
body: {
|
||||
id: traceId,
|
||||
name: "trace-name",
|
||||
userId: "user-1",
|
||||
metadata: { key: "value" },
|
||||
release: "1.0.0",
|
||||
version: "2.0.0",
|
||||
tags: ["tag-1", "tag-2"],
|
||||
},
|
||||
},
|
||||
{
|
||||
id: v4(),
|
||||
type: "observation-create",
|
||||
timestamp: new Date().toISOString(),
|
||||
body: {
|
||||
id: generationId,
|
||||
traceId: traceId,
|
||||
type: "GENERATION",
|
||||
name: "generation-name",
|
||||
startTime: "2021-01-01T00:00:00.000Z",
|
||||
endTime: "2021-01-01T00:00:00.000Z",
|
||||
modelParameters: { key: "value" },
|
||||
input: { key: "value" },
|
||||
metadata: { key: "value" },
|
||||
version: "2.0.0",
|
||||
},
|
||||
},
|
||||
{
|
||||
id: v4(),
|
||||
type: "observation-update",
|
||||
timestamp: new Date().toISOString(),
|
||||
body: {
|
||||
id: generationId,
|
||||
type: "GENERATION",
|
||||
output: { key: "this is a great gpt output" },
|
||||
usage: testConfig.usage,
|
||||
},
|
||||
},
|
||||
{
|
||||
id: v4(),
|
||||
type: "observation-create",
|
||||
timestamp: new Date().toISOString(),
|
||||
body: {
|
||||
id: spanId,
|
||||
traceId: traceId,
|
||||
type: "SPAN",
|
||||
name: "span-name",
|
||||
startTime: "2021-01-01T00:00:00.000Z",
|
||||
endTime: "2021-01-01T00:00:00.000Z",
|
||||
input: { input: "value" },
|
||||
metadata: { meta: "value" },
|
||||
version: "2.0.0",
|
||||
},
|
||||
},
|
||||
{
|
||||
id: v4(),
|
||||
type: "score-create",
|
||||
timestamp: new Date().toISOString(),
|
||||
body: {
|
||||
id: scoreId,
|
||||
name: "score-name",
|
||||
value: 100.5,
|
||||
traceId: traceId,
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
expect(response.status).toBe(207);
|
||||
|
||||
console.log("response body", response.body);
|
||||
|
||||
const dbTrace = await prisma.trace.findMany({
|
||||
where: {
|
||||
name: "trace-name",
|
||||
},
|
||||
});
|
||||
|
||||
expect(dbTrace.length).toBeGreaterThan(0);
|
||||
expect(dbTrace[0]?.name).toBe("trace-name");
|
||||
expect(dbTrace[0]?.release).toBe("1.0.0");
|
||||
expect(dbTrace[0]?.externalId).toBeNull();
|
||||
expect(dbTrace[0]?.version).toBe("2.0.0");
|
||||
expect(dbTrace[0]?.projectId).toBe(
|
||||
"7a88fb47-b4e2-43b8-a06c-a5ce950dc53a",
|
||||
);
|
||||
expect(dbTrace[0]?.tags).toEqual(["tag-1", "tag-2"]);
|
||||
|
||||
const dbGeneration = await prisma.observation.findUnique({
|
||||
where: {
|
||||
id: generationId,
|
||||
},
|
||||
});
|
||||
|
||||
expect(dbGeneration?.id).toBe(generationId);
|
||||
expect(dbGeneration?.traceId).toBe(traceId);
|
||||
expect(dbGeneration?.name).toBe("generation-name");
|
||||
expect(dbGeneration?.startTime).toEqual(
|
||||
new Date("2021-01-01T00:00:00.000Z"),
|
||||
);
|
||||
expect(dbGeneration?.endTime).toEqual(
|
||||
new Date("2021-01-01T00:00:00.000Z"),
|
||||
);
|
||||
expect(dbGeneration?.model).toBeNull();
|
||||
expect(dbGeneration?.modelParameters).toEqual({ key: "value" });
|
||||
expect(dbGeneration?.input).toEqual({ key: "value" });
|
||||
expect(dbGeneration?.metadata).toEqual({ key: "value" });
|
||||
expect(dbGeneration?.version).toBe("2.0.0");
|
||||
expect(dbGeneration?.promptTokens).toEqual(
|
||||
testConfig.expectedPromptTokens,
|
||||
);
|
||||
expect(dbGeneration?.completionTokens).toEqual(
|
||||
testConfig.expectedCompletionTokens,
|
||||
);
|
||||
expect(dbGeneration?.totalTokens).toEqual(testConfig.expectedTotalTokens);
|
||||
expect(dbGeneration?.unit).toEqual(testConfig.expectedUnit);
|
||||
expect(dbGeneration?.output).toEqual({
|
||||
key: "this is a great gpt output",
|
||||
});
|
||||
|
||||
const dbSpan = await prisma.observation.findUnique({
|
||||
where: {
|
||||
id: spanId,
|
||||
},
|
||||
});
|
||||
|
||||
expect(dbSpan?.id).toBe(spanId);
|
||||
expect(dbSpan?.name).toBe("span-name");
|
||||
expect(dbSpan?.startTime).toEqual(new Date("2021-01-01T00:00:00.000Z"));
|
||||
expect(dbSpan?.endTime).toEqual(new Date("2021-01-:00:00.000Z"));
|
||||
expect(dbSpan?.input).toEqual({ input: "value" });
|
||||
expect(dbSpan?.metadata).toEqual({ meta: "value" });
|
||||
expect(dbSpan?.version).toBe("2.0.0");
|
||||
|
||||
const dbScore = await prisma.score.findUnique({
|
||||
where: {
|
||||
id: scoreId,
|
||||
},
|
||||
});
|
||||
|
||||
expect(dbScore?.id).toBe(scoreId);
|
||||
expect(dbScore?.traceId).toBe(traceId);
|
||||
expect(dbScore?.name).toBe("score-name");
|
||||
expect(dbScore?.value).toBe(100.5);
|
||||
expect(dbScore?.observationId).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
it("should create and update all events", async () => {
|
||||
const traceId = v4();
|
||||
const generationId = v4();
|
||||
const spanId = v4();
|
||||
const eventId = v4();
|
||||
const scoreId = v4();
|
||||
|
||||
const exception = `
|
||||
ERROR langfuse:callback.py:677 'model_name'
|
||||
Traceback (most recent call last):
|
||||
File "/Users/maximiliandeichmann/development/github.com/langfuse/langfuse-python/langfuse/callback.py", line 674, in __on_llm_action
|
||||
model_name = kwargs["invocation_params"]["model_name"]
|
||||
KeyError: 'model_name'
|
||||
`;
|
||||
|
||||
const response = await makeAPICall("POST", "/api/public/ingestion", {
|
||||
batch: [
|
||||
{
|
||||
@@ -21,55 +271,55 @@ describe("/api/public/ingestion API Endpoint", () => {
|
||||
timestamp: new Date().toISOString(),
|
||||
body: {
|
||||
id: traceId,
|
||||
name: "trace-name",
|
||||
userId: "user-1",
|
||||
metadata: { key: "value" },
|
||||
release: "1.0.0",
|
||||
version: "2.0.0",
|
||||
},
|
||||
},
|
||||
{
|
||||
id: v4(),
|
||||
type: "observation-create",
|
||||
type: "span-create",
|
||||
timestamp: new Date().toISOString(),
|
||||
body: {
|
||||
id: generationId,
|
||||
id: spanId,
|
||||
traceId: traceId,
|
||||
type: "GENERATION",
|
||||
name: "generation-name",
|
||||
startTime: "2021-01-01T00:00:00.000Z",
|
||||
endTime: "2021-01-01T00:00:00.000Z",
|
||||
modelParameters: { key: "value" },
|
||||
input: { key: "value" },
|
||||
metadata: { key: "value" },
|
||||
version: "2.0.0",
|
||||
},
|
||||
},
|
||||
{
|
||||
id: v4(),
|
||||
type: "observation-update",
|
||||
timestamp: new Date().toISOString(),
|
||||
body: {
|
||||
id: generationId,
|
||||
type: "GENERATION",
|
||||
output: { key: "this is a great gpt output" },
|
||||
usage: { promptTokens: 400, completionTokens: 1000 },
|
||||
},
|
||||
},
|
||||
{
|
||||
id: v4(),
|
||||
type: "observation-create",
|
||||
type: "span-update",
|
||||
timestamp: new Date().toISOString(),
|
||||
body: {
|
||||
id: spanId,
|
||||
traceId: traceId,
|
||||
type: "SPAN",
|
||||
name: "span-name",
|
||||
startTime: "2021-01-01T00:00:00.000Z",
|
||||
endTime: "2021-01-01T00:00:00.000Z",
|
||||
input: { input: "value" },
|
||||
metadata: { meta: "value" },
|
||||
version: "2.0.0",
|
||||
},
|
||||
},
|
||||
{
|
||||
id: v4(),
|
||||
type: "generation-create",
|
||||
timestamp: new Date().toISOString(),
|
||||
body: {
|
||||
id: generationId,
|
||||
traceId: traceId,
|
||||
parentObservationId: spanId,
|
||||
},
|
||||
},
|
||||
{
|
||||
id: v4(),
|
||||
type: "generation-update",
|
||||
timestamp: new Date().toISOString(),
|
||||
body: {
|
||||
id: generationId,
|
||||
name: "generation-name",
|
||||
},
|
||||
},
|
||||
{
|
||||
id: v4(),
|
||||
type: "event-create",
|
||||
timestamp: new Date().toISOString(),
|
||||
body: {
|
||||
id: eventId,
|
||||
traceId: traceId,
|
||||
name: "event-name",
|
||||
parentObservationId: generationId,
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -79,8 +329,17 @@ describe("/api/public/ingestion API Endpoint", () => {
|
||||
body: {
|
||||
id: scoreId,
|
||||
name: "score-name",
|
||||
value: 100.5,
|
||||
traceId: traceId,
|
||||
value: 100.5,
|
||||
observationId: generationId,
|
||||
},
|
||||
},
|
||||
{
|
||||
id: v4(),
|
||||
type: "sdk-log",
|
||||
timestamp: new Date().toISOString(),
|
||||
body: {
|
||||
log: exception,
|
||||
},
|
||||
},
|
||||
],
|
||||
@@ -90,17 +349,23 @@ describe("/api/public/ingestion API Endpoint", () => {
|
||||
|
||||
const dbTrace = await prisma.trace.findMany({
|
||||
where: {
|
||||
name: "trace-name",
|
||||
id: traceId,
|
||||
},
|
||||
});
|
||||
|
||||
expect(dbTrace.length).toBeGreaterThan(0);
|
||||
expect(dbTrace[0]?.name).toBe("trace-name");
|
||||
expect(dbTrace[0]?.release).toBe("1.0.0");
|
||||
expect(dbTrace[0]?.externalId).toBeNull();
|
||||
expect(dbTrace[0]?.version).toBe("2.0.0");
|
||||
expect(dbTrace[0]?.projectId).toBe("7a88fb47-b4e2-43b8-a06c-a5ce950dc53a");
|
||||
|
||||
const dbSpan = await prisma.observation.findUnique({
|
||||
where: {
|
||||
id: spanId,
|
||||
},
|
||||
});
|
||||
|
||||
expect(dbSpan?.id).toBe(spanId);
|
||||
expect(dbSpan?.name).toBe("span-name");
|
||||
expect(dbSpan?.traceId).toBe(traceId);
|
||||
|
||||
const dbGeneration = await prisma.observation.findUnique({
|
||||
where: {
|
||||
id: generationId,
|
||||
@@ -110,34 +375,18 @@ describe("/api/public/ingestion API Endpoint", () => {
|
||||
expect(dbGeneration?.id).toBe(generationId);
|
||||
expect(dbGeneration?.traceId).toBe(traceId);
|
||||
expect(dbGeneration?.name).toBe("generation-name");
|
||||
expect(dbGeneration?.startTime).toEqual(
|
||||
new Date("2021-01-01T00:00:00.000Z"),
|
||||
);
|
||||
expect(dbGeneration?.endTime).toEqual(new Date("2021-01-01T00:00:00.000Z"));
|
||||
expect(dbGeneration?.model).toBeNull();
|
||||
expect(dbGeneration?.modelParameters).toEqual({ key: "value" });
|
||||
expect(dbGeneration?.input).toEqual({ key: "value" });
|
||||
expect(dbGeneration?.metadata).toEqual({ key: "value" });
|
||||
expect(dbGeneration?.version).toBe("2.0.0");
|
||||
expect(dbGeneration?.promptTokens).toEqual(400);
|
||||
expect(dbGeneration?.completionTokens).toEqual(1000);
|
||||
expect(dbGeneration?.output).toEqual({
|
||||
key: "this is a great gpt output",
|
||||
});
|
||||
expect(dbGeneration?.parentObservationId).toBe(spanId);
|
||||
|
||||
const dbSpan = await prisma.observation.findUnique({
|
||||
const dbEvent = await prisma.observation.findUnique({
|
||||
where: {
|
||||
id: spanId,
|
||||
id: eventId,
|
||||
},
|
||||
});
|
||||
|
||||
expect(dbSpan?.id).toBe(spanId);
|
||||
expect(dbSpan?.name).toBe("span-name");
|
||||
expect(dbSpan?.startTime).toEqual(new Date("2021-01-01T00:00:00.000Z"));
|
||||
expect(dbSpan?.endTime).toEqual(new Date("2021-01-:00:00.000Z"));
|
||||
expect(dbSpan?.input).toEqual({ input: "value" });
|
||||
expect(dbSpan?.metadata).toEqual({ meta: "value" });
|
||||
expect(dbSpan?.version).toBe("2.0.0");
|
||||
expect(dbEvent?.id).toBe(eventId);
|
||||
expect(dbEvent?.traceId).toBe(traceId);
|
||||
expect(dbEvent?.name).toBe("event-name");
|
||||
expect(dbEvent?.parentObservationId).toBe(generationId);
|
||||
|
||||
const dbScore = await prisma.score.findUnique({
|
||||
where: {
|
||||
@@ -147,9 +396,21 @@ describe("/api/public/ingestion API Endpoint", () => {
|
||||
|
||||
expect(dbScore?.id).toBe(scoreId);
|
||||
expect(dbScore?.traceId).toBe(traceId);
|
||||
expect(dbScore?.name).toBe("score-name");
|
||||
expect(dbScore?.observationId).toBe(generationId);
|
||||
expect(dbScore?.value).toBe(100.5);
|
||||
expect(dbScore?.observationId).toBeNull();
|
||||
|
||||
const logEvent = await prisma.events.findFirst({
|
||||
where: {
|
||||
data: {
|
||||
path: ["body", "log"],
|
||||
string_contains: "ERROR",
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
expect(logEvent).toBeDefined();
|
||||
expect(logEvent).not.toBeFalsy();
|
||||
expect(JSON.stringify(logEvent?.data)).toContain("KeyError: 'model_name'");
|
||||
});
|
||||
|
||||
it("should upsert threats", async () => {
|
||||
@@ -247,6 +508,108 @@ describe("/api/public/ingestion API Endpoint", () => {
|
||||
expect(dbTrace.length).toBe(1);
|
||||
});
|
||||
|
||||
it("should fail for auth errors", async () => {
|
||||
const traceId = v4();
|
||||
const scoreId = v4();
|
||||
|
||||
const responseOne = await makeAPICall("POST", "/api/public/ingestion", {
|
||||
batch: [
|
||||
{
|
||||
id: v4(),
|
||||
type: "trace-create",
|
||||
timestamp: new Date().toISOString(),
|
||||
body: {
|
||||
id: traceId,
|
||||
name: "trace-name",
|
||||
userId: "user-1",
|
||||
metadata: { key: "value" },
|
||||
release: "1.0.0",
|
||||
version: "2.0.0",
|
||||
},
|
||||
},
|
||||
{
|
||||
id: v4(),
|
||||
type: "score-create",
|
||||
timestamp: new Date().toISOString(),
|
||||
body: {
|
||||
id: scoreId,
|
||||
name: "score-name",
|
||||
value: 100.5,
|
||||
traceId: "some-random-id",
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
console.log(responseOne.body);
|
||||
expect(responseOne.status).toBe(207);
|
||||
|
||||
expect("errors" in responseOne.body).toBe(true);
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
|
||||
expect(responseOne.body?.errors.length).toBe(1);
|
||||
expect("successes" in responseOne.body).toBe(true);
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
|
||||
expect(responseOne.body?.successes.length).toBe(1);
|
||||
|
||||
const dbTrace = await prisma.trace.findMany({
|
||||
where: {
|
||||
name: "trace-name",
|
||||
},
|
||||
});
|
||||
|
||||
expect(dbTrace.length).toBe(1);
|
||||
});
|
||||
|
||||
it("should fail for resource not found", async () => {
|
||||
const traceId = v4();
|
||||
|
||||
const responseOne = await makeAPICall("POST", "/api/public/ingestion", {
|
||||
batch: [
|
||||
{
|
||||
id: v4(),
|
||||
type: "trace-create",
|
||||
timestamp: new Date().toISOString(),
|
||||
body: {
|
||||
id: traceId,
|
||||
name: "trace-name",
|
||||
userId: "user-1",
|
||||
metadata: { key: "value" },
|
||||
release: "1.0.0",
|
||||
version: "2.0.0",
|
||||
},
|
||||
},
|
||||
{
|
||||
id: v4(),
|
||||
type: "observation-update",
|
||||
timestamp: new Date().toISOString(),
|
||||
body: {
|
||||
id: "some-random-id",
|
||||
type: "GENERATION",
|
||||
output: { key: "this is a great gpt output" },
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
console.log(responseOne.body);
|
||||
expect(responseOne.status).toBe(207);
|
||||
|
||||
expect("errors" in responseOne.body).toBe(true);
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
|
||||
expect(responseOne.body?.errors.length).toBe(1);
|
||||
expect("successes" in responseOne.body).toBe(true);
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
|
||||
expect(responseOne.body?.successes.length).toBe(1);
|
||||
|
||||
const dbTrace = await prisma.trace.findMany({
|
||||
where: {
|
||||
name: "trace-name",
|
||||
},
|
||||
});
|
||||
|
||||
expect(dbTrace.length).toBe(1);
|
||||
});
|
||||
|
||||
it("should update all token counts if update does not contain model name", async () => {
|
||||
const traceId = v4();
|
||||
const generationId = v4();
|
||||
@@ -427,6 +790,66 @@ describe("/api/public/ingestion API Endpoint", () => {
|
||||
expect(dbTrace[0]?.version).toBe("2.0.0");
|
||||
});
|
||||
|
||||
it("should not override a trace from a different project", async () => {
|
||||
const traceId = v4();
|
||||
const newProjectId = v4();
|
||||
|
||||
await prisma.project.create({
|
||||
data: {
|
||||
id: newProjectId,
|
||||
name: "another-project",
|
||||
},
|
||||
});
|
||||
|
||||
await prisma.trace.create({
|
||||
data: {
|
||||
id: traceId,
|
||||
project: { connect: { id: newProjectId } },
|
||||
},
|
||||
});
|
||||
|
||||
const responseOne = await makeAPICall("POST", "/api/public/ingestion", {
|
||||
batch: [
|
||||
{
|
||||
id: v4(),
|
||||
type: "trace-create",
|
||||
timestamp: new Date().toISOString(),
|
||||
body: {
|
||||
id: traceId,
|
||||
name: "trace-name",
|
||||
userId: "user-1",
|
||||
metadata: { key: "value" },
|
||||
release: "1.0.0",
|
||||
version: "2.0.0",
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
expect(responseOne.status).toBe(207);
|
||||
|
||||
console.log(responseOne.body);
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-member-access
|
||||
const errors = responseOne.body.errors;
|
||||
|
||||
expect(errors).toBeDefined();
|
||||
console.log(errors);
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
|
||||
expect(errors.length).toBe(1);
|
||||
|
||||
const dbTrace = await prisma.trace.findMany({
|
||||
where: {
|
||||
id: traceId,
|
||||
},
|
||||
});
|
||||
|
||||
expect(dbTrace.length).toEqual(1);
|
||||
expect(dbTrace[0]?.name).toBeNull();
|
||||
expect(dbTrace[0]?.release).toBeNull();
|
||||
expect(dbTrace[0]?.metadata).toBeNull();
|
||||
expect(dbTrace[0]?.version).toBeNull();
|
||||
});
|
||||
|
||||
[
|
||||
{
|
||||
inputs: [{ a: "a" }, { b: "b" }],
|
||||
@@ -584,4 +1007,80 @@ describe("/api/public/ingestion API Endpoint", () => {
|
||||
|
||||
expect(dbGeneration).toBeTruthy();
|
||||
});
|
||||
|
||||
it("filters out NULL characters", async () => {
|
||||
const traceId = v4();
|
||||
const generationId = v4();
|
||||
|
||||
const responseOne = await makeAPICall("POST", "/api/public/ingestion", {
|
||||
batch: [
|
||||
{
|
||||
id: v4(),
|
||||
type: "trace-create",
|
||||
timestamp: new Date().toISOString(),
|
||||
body: {
|
||||
id: traceId,
|
||||
name: "trace-name",
|
||||
},
|
||||
},
|
||||
{
|
||||
id: v4(),
|
||||
type: "observation-create",
|
||||
timestamp: new Date().toISOString(),
|
||||
body: {
|
||||
id: generationId,
|
||||
traceId: traceId,
|
||||
type: "GENERATION",
|
||||
name: "generation-name",
|
||||
traceIdType: "LANGFUSE",
|
||||
input: {
|
||||
key: "IB\nibo.org Change site\nIB Home / . . . / News / News about the IB / Why ChatGPT is an opportunity for schools IB Home / News / News about the IB / Why ChatGPT is an opportunity for schools Why ChatGPT is an opportunity for schools Published: 06 March 2023 Last updated: 06 June 2023 Date published: 28 February 2023 Dr Matthew Glanville, Head of Assessment Principles and Practice Source: Why ChatGPT is an opportunity for schools | The Times Those of us who work in the schools or exam sector should not be terri \u0000 ed by ChatGPT and the rise of AI software – we should be excited. We should embrace it as an extraordinary opportunity. Contrary to some stark warnings, it is not the end of exams, nor even a huge threat to coursework, but it does bring into very sharp focus the impact that arti \u0000 ",
|
||||
},
|
||||
output: {
|
||||
key: "제점이 있었죠. 그중 하나는 일제가 한국의 신용체계를 망가뜨린 채 한국을 떠났다는 겁니다. 해방전 일제는 조선의 신용체계를 거의 독점적으로 소유한 상황이었습니다. 1945년 6월 기준 일제는 조선의 본점을 둔 전체은행 5개의 불입자본 총액의 89.7%를",
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
expect(responseOne.status).toBe(207);
|
||||
|
||||
const dbTrace = await prisma.trace.findMany({
|
||||
where: {
|
||||
id: traceId,
|
||||
},
|
||||
});
|
||||
|
||||
expect(dbTrace.length).toEqual(1);
|
||||
|
||||
const dbGeneration = await prisma.observation.findUnique({
|
||||
where: {
|
||||
id: generationId,
|
||||
},
|
||||
});
|
||||
expect(dbGeneration?.input).toStrictEqual({
|
||||
key: `IB
|
||||
ibo.org Change site
|
||||
IB Home / . . . / News / News about the IB / Why ChatGPT is an opportunity for schools IB Home / News / News about the IB / Why ChatGPT is an opportunity for schools Why ChatGPT is an opportunity for schools Published: 06 March 2023 Last updated: 06 June 2023 Date published: 28 February 2023 Dr Matthew Glanville, Head of Assessment Principles and Practice Source: Why ChatGPT is an opportunity for schools | The Times Those of us who work in the schools or exam sector should not be terri ed by ChatGPT and the rise of AI software – we should be excited. We should embrace it as an extraordinary opportunity. Contrary to some stark warnings, it is not the end of exams, nor even a huge threat to coursework, but it does bring into very sharp focus the impact that arti `,
|
||||
});
|
||||
expect(dbGeneration?.output).toStrictEqual({
|
||||
key: "제점이 있었죠. 그중 하나는 일제가 한국의 신용체계를 망가뜨린 채 한국을 떠났다는 겁니다. 해방전 일제는 조선의 신용체계를 거의 독점적으로 소유한 상황이었습니다. 1945년 6월 기준 일제는 조선의 본점을 둔 전체은행 5개의 불입자본 총액의 89.7%를",
|
||||
});
|
||||
|
||||
expect(dbGeneration).toBeTruthy();
|
||||
});
|
||||
|
||||
[
|
||||
{ input: "A\u0000hallo", expected: "Ahallo" },
|
||||
{ input: ["A\u0000hallo"], expected: ["Ahallo"] },
|
||||
{ input: { obj: ["A\u0000hallo"] }, expected: { obj: ["Ahallo"] } },
|
||||
].forEach(({ input, expected }) => {
|
||||
it(`cleans events with null values ${JSON.stringify(
|
||||
input,
|
||||
)} ${JSON.stringify(expected)}`, () => {
|
||||
const cleanedEvent = cleanEvent(input);
|
||||
console.log(cleanedEvent);
|
||||
expect(cleanedEvent).toStrictEqual(expected);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -0,0 +1,346 @@
|
||||
/** @jest-environment node */
|
||||
|
||||
import { prisma } from "@/src/server/db";
|
||||
import { makeAPICall, pruneDatabase } from "@/src/__tests__/test-utils";
|
||||
import { v4 as uuidv4, v4 } from "uuid";
|
||||
import { type Prompt } from "@prisma/client";
|
||||
|
||||
describe("/api/public/prompts API Endpoint", () => {
|
||||
beforeEach(async () => await pruneDatabase());
|
||||
|
||||
it("should fetch a prompt", async () => {
|
||||
const promptId = uuidv4();
|
||||
|
||||
await prisma.prompt.create({
|
||||
data: {
|
||||
id: promptId,
|
||||
name: "prompt-name",
|
||||
prompt: "prompt",
|
||||
isActive: true,
|
||||
version: 1,
|
||||
project: {
|
||||
connect: { id: "7a88fb47-b4e2-43b8-a06c-a5ce950dc53a" },
|
||||
},
|
||||
createdBy: "user-1",
|
||||
},
|
||||
});
|
||||
|
||||
const fetchedObservations = await makeAPICall(
|
||||
"GET",
|
||||
"/api/public/prompts?name=prompt-name&version=1",
|
||||
undefined,
|
||||
);
|
||||
|
||||
expect(fetchedObservations.status).toBe(200);
|
||||
|
||||
if (!isPrompt(fetchedObservations.body)) {
|
||||
throw new Error("Expected body to be a prompt");
|
||||
}
|
||||
|
||||
expect(fetchedObservations.body.id).toBe(promptId);
|
||||
expect(fetchedObservations.body.name).toBe("prompt-name");
|
||||
expect(fetchedObservations.body.prompt).toBe("prompt");
|
||||
expect(fetchedObservations.body.version).toBe(1);
|
||||
expect(fetchedObservations.body.isActive).toBe(true);
|
||||
expect(fetchedObservations.body.createdBy).toBe("user-1");
|
||||
});
|
||||
|
||||
it("should fetch active prompt only if no prompt version is given", async () => {
|
||||
const promptId = uuidv4();
|
||||
|
||||
await prisma.prompt.create({
|
||||
data: {
|
||||
id: promptId,
|
||||
name: "prompt-name",
|
||||
prompt: "prompt",
|
||||
isActive: false,
|
||||
version: 1,
|
||||
project: {
|
||||
connect: { id: "7a88fb47-b4e2-43b8-a06c-a5ce950dc53a" },
|
||||
},
|
||||
createdBy: "user-1",
|
||||
},
|
||||
});
|
||||
|
||||
const fetchedObservations = await makeAPICall(
|
||||
"GET",
|
||||
"/api/public/prompts?name=prompt-name",
|
||||
undefined,
|
||||
);
|
||||
|
||||
expect(fetchedObservations.status).toBe(404);
|
||||
});
|
||||
|
||||
it("should fetch inactive prompt if prompt version is given", async () => {
|
||||
const promptId = uuidv4();
|
||||
const promptTwoId = uuidv4();
|
||||
|
||||
await prisma.prompt.create({
|
||||
data: {
|
||||
id: promptId,
|
||||
name: "prompt-name",
|
||||
prompt: "prompt-one",
|
||||
isActive: false,
|
||||
version: 1,
|
||||
project: {
|
||||
connect: { id: "7a88fb47-b4e2-43b8-a06c-a5ce950dc53a" },
|
||||
},
|
||||
createdBy: "user-1",
|
||||
},
|
||||
});
|
||||
|
||||
await prisma.prompt.create({
|
||||
data: {
|
||||
id: promptTwoId,
|
||||
name: "prompt-name",
|
||||
prompt: "prompt",
|
||||
isActive: true,
|
||||
version: 2,
|
||||
project: {
|
||||
connect: { id: "7a88fb47-b4e2-43b8-a06c-a5ce950dc53a" },
|
||||
},
|
||||
createdBy: "user-1",
|
||||
},
|
||||
});
|
||||
|
||||
const fetchedObservations = await makeAPICall(
|
||||
"GET",
|
||||
"/api/public/prompts?name=prompt-name&version=1",
|
||||
undefined,
|
||||
);
|
||||
|
||||
expect(fetchedObservations.status).toBe(200);
|
||||
|
||||
if (!isPrompt(fetchedObservations.body)) {
|
||||
throw new Error("Expected body to be a prompt");
|
||||
}
|
||||
|
||||
expect(fetchedObservations.body.id).toBe(promptId);
|
||||
expect(fetchedObservations.body.name).toBe("prompt-name");
|
||||
expect(fetchedObservations.body.prompt).toBe("prompt-one");
|
||||
expect(fetchedObservations.body.version).toBe(1);
|
||||
expect(fetchedObservations.body.isActive).toBe(false);
|
||||
expect(fetchedObservations.body.createdBy).toBe("user-1");
|
||||
});
|
||||
|
||||
it("should fetch active prompt when multiple exist", async () => {
|
||||
const promptIdOne = uuidv4();
|
||||
const promptIdTwo = uuidv4();
|
||||
|
||||
await prisma.prompt.create({
|
||||
data: {
|
||||
id: promptIdOne,
|
||||
name: "prompt-name",
|
||||
prompt: "prompt",
|
||||
isActive: false,
|
||||
version: 1,
|
||||
project: {
|
||||
connect: { id: "7a88fb47-b4e2-43b8-a06c-a5ce950dc53a" },
|
||||
},
|
||||
createdBy: "user-1",
|
||||
},
|
||||
});
|
||||
|
||||
await prisma.prompt.create({
|
||||
data: {
|
||||
id: promptIdTwo,
|
||||
name: "prompt-name",
|
||||
prompt: "prompt",
|
||||
isActive: true,
|
||||
version: 2,
|
||||
project: {
|
||||
connect: { id: "7a88fb47-b4e2-43b8-a06c-a5ce950dc53a" },
|
||||
},
|
||||
createdBy: "user-1",
|
||||
},
|
||||
});
|
||||
|
||||
const fetchedObservations = await makeAPICall(
|
||||
"GET",
|
||||
"/api/public/prompts?name=prompt-name",
|
||||
undefined,
|
||||
);
|
||||
|
||||
expect(fetchedObservations.status).toBe(200);
|
||||
|
||||
if (!isPrompt(fetchedObservations.body)) {
|
||||
throw new Error("Expected body to be a prompt");
|
||||
}
|
||||
|
||||
expect(fetchedObservations.body.id).toBe(promptIdTwo);
|
||||
expect(fetchedObservations.body.name).toBe("prompt-name");
|
||||
expect(fetchedObservations.body.prompt).toBe("prompt");
|
||||
expect(fetchedObservations.body.version).toBe(2);
|
||||
expect(fetchedObservations.body.isActive).toBe(true);
|
||||
expect(fetchedObservations.body.createdBy).toBe("user-1");
|
||||
});
|
||||
|
||||
it("should create and fetch a prompt", async () => {
|
||||
await makeAPICall("POST", "/api/public/prompts", {
|
||||
name: "prompt-name",
|
||||
prompt: "prompt",
|
||||
isActive: true,
|
||||
projectId: "7a88fb47-b4e2-43b8-a06c-a5ce950dc53a",
|
||||
});
|
||||
|
||||
const fetchedObservations = await makeAPICall(
|
||||
"GET",
|
||||
"/api/public/prompts?name=prompt-name&version=1",
|
||||
undefined,
|
||||
);
|
||||
|
||||
expect(fetchedObservations.status).toBe(200);
|
||||
|
||||
if (!isPrompt(fetchedObservations.body)) {
|
||||
throw new Error("Expected body to be an array of observations");
|
||||
}
|
||||
|
||||
expect(fetchedObservations.body.name).toBe("prompt-name");
|
||||
expect(fetchedObservations.body.prompt).toBe("prompt");
|
||||
expect(fetchedObservations.body.version).toBe(1);
|
||||
expect(fetchedObservations.body.isActive).toBe(true);
|
||||
expect(fetchedObservations.body.createdBy).toBe("API");
|
||||
});
|
||||
|
||||
it("should relate generation to prompt", async () => {
|
||||
const traceId = v4();
|
||||
const generationId = v4();
|
||||
|
||||
const promptId = uuidv4();
|
||||
|
||||
await prisma.prompt.create({
|
||||
data: {
|
||||
id: promptId,
|
||||
name: "prompt-name",
|
||||
prompt: "prompt",
|
||||
isActive: true,
|
||||
version: 1,
|
||||
project: {
|
||||
connect: { id: "7a88fb47-b4e2-43b8-a06c-a5ce950dc53a" },
|
||||
},
|
||||
createdBy: "user-1",
|
||||
},
|
||||
});
|
||||
|
||||
const response = await makeAPICall("POST", "/api/public/ingestion", {
|
||||
metadata: {
|
||||
sdk_verion: "1.0.0",
|
||||
sdk_name: "python",
|
||||
},
|
||||
batch: [
|
||||
{
|
||||
id: v4(),
|
||||
type: "trace-create",
|
||||
timestamp: new Date().toISOString(),
|
||||
body: {
|
||||
id: traceId,
|
||||
name: "trace-name",
|
||||
},
|
||||
},
|
||||
{
|
||||
id: v4(),
|
||||
type: "generation-create",
|
||||
timestamp: new Date().toISOString(),
|
||||
body: {
|
||||
id: generationId,
|
||||
traceId: traceId,
|
||||
type: "GENERATION",
|
||||
name: "generation-name",
|
||||
promptName: "prompt-name",
|
||||
promptVersion: 1,
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
expect(response.status).toBe(207);
|
||||
|
||||
console.log("response body", response.body);
|
||||
|
||||
const dbGeneration = await prisma.observation.findUnique({
|
||||
where: {
|
||||
id: generationId,
|
||||
},
|
||||
});
|
||||
|
||||
expect(dbGeneration?.id).toBe(generationId);
|
||||
expect(dbGeneration?.promptId).toBe(promptId);
|
||||
});
|
||||
|
||||
it("should fail if prompt version is missing", async () => {
|
||||
const traceId = v4();
|
||||
const generationId = v4();
|
||||
|
||||
const promptId = uuidv4();
|
||||
|
||||
await prisma.prompt.create({
|
||||
data: {
|
||||
id: promptId,
|
||||
name: "prompt-name",
|
||||
prompt: "prompt",
|
||||
isActive: true,
|
||||
version: 1,
|
||||
project: {
|
||||
connect: { id: "7a88fb47-b4e2-43b8-a06c-a5ce950dc53a" },
|
||||
},
|
||||
createdBy: "user-1",
|
||||
},
|
||||
});
|
||||
|
||||
const response = await makeAPICall("POST", "/api/public/ingestion", {
|
||||
metadata: {
|
||||
sdk_verion: "1.0.0",
|
||||
sdk_name: "python",
|
||||
},
|
||||
batch: [
|
||||
{
|
||||
id: v4(),
|
||||
type: "trace-create",
|
||||
timestamp: new Date().toISOString(),
|
||||
body: {
|
||||
id: traceId,
|
||||
name: "trace-name",
|
||||
},
|
||||
},
|
||||
{
|
||||
id: v4(),
|
||||
type: "generation-create",
|
||||
timestamp: new Date().toISOString(),
|
||||
body: {
|
||||
id: generationId,
|
||||
traceId: traceId,
|
||||
type: "GENERATION",
|
||||
name: "generation-name",
|
||||
promptName: "prompt-name",
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
expect(response.status).toBe(207);
|
||||
|
||||
console.log("response body", response.body);
|
||||
|
||||
const dbGeneration = await prisma.observation.findUnique({
|
||||
where: {
|
||||
id: generationId,
|
||||
},
|
||||
});
|
||||
|
||||
expect(dbGeneration).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
const isPrompt = (x: unknown): x is Prompt => {
|
||||
if (typeof x !== "object" || x === null) return false;
|
||||
const prompt = x as Prompt;
|
||||
return (
|
||||
typeof prompt.id === "string" &&
|
||||
typeof prompt.name === "string" &&
|
||||
typeof prompt.version === "number" &&
|
||||
typeof prompt.prompt === "string" &&
|
||||
typeof prompt.isActive === "boolean" &&
|
||||
typeof prompt.projectId === "string" &&
|
||||
typeof prompt.createdBy === "string"
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,107 @@
|
||||
/** @jest-environment node */
|
||||
import { pruneDatabase } from "@/src/__tests__/test-utils";
|
||||
import { appRouter } from "@/src/server/api/root";
|
||||
import { createInnerTRPCContext } from "@/src/server/api/trpc";
|
||||
import { prisma } from "@/src/server/db";
|
||||
import type { Session } from "next-auth";
|
||||
|
||||
describe("Traces TRPC Router", () => {
|
||||
beforeEach(async () => await pruneDatabase());
|
||||
afterEach(async () => await pruneDatabase());
|
||||
|
||||
const session: Session = {
|
||||
expires: "1",
|
||||
user: {
|
||||
id: "clgb17vnp000008jjere5g15i",
|
||||
name: "John Doe",
|
||||
projects: [
|
||||
{
|
||||
id: "7a88fb47-b4e2-43b8-a06c-a5ce950dc53a",
|
||||
role: "ADMIN",
|
||||
name: "test",
|
||||
},
|
||||
],
|
||||
featureFlags: {
|
||||
templateFlag: true,
|
||||
},
|
||||
admin: true,
|
||||
},
|
||||
};
|
||||
|
||||
const ctx = createInnerTRPCContext({ session });
|
||||
const caller = appRouter.createCaller({ ...ctx, prisma });
|
||||
|
||||
test("traces.all RPC returns an array of traces", async () => {
|
||||
const trace = {
|
||||
name: "trace-name",
|
||||
userId: "user-1",
|
||||
metadata: { key: "value" },
|
||||
release: "1.0.0",
|
||||
version: "2.0.0",
|
||||
};
|
||||
await prisma.trace.create({
|
||||
data: { ...trace, projectId: "7a88fb47-b4e2-43b8-a06c-a5ce950dc53a" },
|
||||
});
|
||||
|
||||
const traces = await caller.traces.all({
|
||||
page: 0,
|
||||
limit: 10,
|
||||
// projectId from `seed.ts`
|
||||
projectId: "7a88fb47-b4e2-43b8-a06c-a5ce950dc53a",
|
||||
filter: null,
|
||||
searchQuery: "",
|
||||
orderBy: null,
|
||||
});
|
||||
expect(traces).toBeDefined();
|
||||
expect(traces).toMatchObject({ traces: [trace] });
|
||||
});
|
||||
|
||||
test("traces.all RPC orders traces by userId", async () => {
|
||||
const traceTmpl = {
|
||||
name: "trace-name",
|
||||
userId: "user-1",
|
||||
metadata: { key: "value" },
|
||||
release: "1.0.0",
|
||||
version: "2.0.0",
|
||||
};
|
||||
const trace1 = traceTmpl;
|
||||
const trace2 = { ...traceTmpl, userId: "user-2" };
|
||||
await prisma.trace.create({
|
||||
data: { ...traceTmpl, projectId: "7a88fb47-b4e2-43b8-a06c-a5ce950dc53a" },
|
||||
});
|
||||
await prisma.trace.create({
|
||||
data: {
|
||||
...trace2,
|
||||
projectId: "7a88fb47-b4e2-43b8-a06c-a5ce950dc53a",
|
||||
},
|
||||
});
|
||||
|
||||
const tracesASC = await caller.traces.all({
|
||||
page: 0,
|
||||
limit: 10,
|
||||
// projectId from `seed.ts`
|
||||
projectId: "7a88fb47-b4e2-43b8-a06c-a5ce950dc53a",
|
||||
filter: null,
|
||||
searchQuery: "",
|
||||
orderBy: {
|
||||
column: "userId",
|
||||
order: "ASC",
|
||||
},
|
||||
});
|
||||
expect(tracesASC).toMatchObject({ traces: [trace1, trace2] });
|
||||
|
||||
const tracesDESC = await caller.traces.all({
|
||||
page: 0,
|
||||
limit: 10,
|
||||
// projectId from `seed.ts`
|
||||
projectId: "7a88fb47-b4e2-43b8-a06c-a5ce950dc53a",
|
||||
filter: null,
|
||||
searchQuery: "",
|
||||
orderBy: {
|
||||
column: "userId",
|
||||
order: "DESC",
|
||||
},
|
||||
});
|
||||
expect(tracesDESC).toMatchObject({ traces: [trace2, trace1] });
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,62 @@
|
||||
/** @jest-environment node */
|
||||
|
||||
import { makeAPICall, pruneDatabase } from "@/src/__tests__/test-utils";
|
||||
import { prisma } from "@/src/server/db";
|
||||
|
||||
describe("/api/public/traces API Endpoint", () => {
|
||||
beforeEach(async () => await pruneDatabase());
|
||||
afterEach(async () => await pruneDatabase());
|
||||
|
||||
it("should create a session via a trace", async () => {
|
||||
await pruneDatabase();
|
||||
|
||||
await makeAPICall("POST", "/api/public/traces", {
|
||||
name: "trace-name",
|
||||
projectId: "7a88fb47-b4e2-43b8-a06c-a5ce950dc53a",
|
||||
sessionId: "session-id",
|
||||
});
|
||||
|
||||
const dbSession = await prisma.traceSession.findFirst({
|
||||
where: {
|
||||
id: "session-id",
|
||||
},
|
||||
});
|
||||
|
||||
expect(dbSession).not.toBeNull();
|
||||
expect(dbSession).toMatchObject({
|
||||
id: "session-id",
|
||||
projectId: "7a88fb47-b4e2-43b8-a06c-a5ce950dc53a",
|
||||
});
|
||||
});
|
||||
|
||||
it("should get session including traces", async () => {
|
||||
await pruneDatabase();
|
||||
|
||||
await makeAPICall("POST", "/api/public/traces", {
|
||||
name: "trace-name",
|
||||
id: "trace-id",
|
||||
input: { hello: "world" },
|
||||
output: "hi",
|
||||
projectId: "7a88fb47-b4e2-43b8-a06c-a5ce950dc53a",
|
||||
sessionId: "session-id",
|
||||
});
|
||||
|
||||
const response = await makeAPICall(
|
||||
"GET",
|
||||
"/api/public/sessions/session-id",
|
||||
);
|
||||
|
||||
expect(response.status).toBe(200);
|
||||
expect(response.body).toMatchObject({
|
||||
id: "session-id",
|
||||
projectId: "7a88fb47-b4e2-43b8-a06c-a5ce950dc53a",
|
||||
traces: [
|
||||
{
|
||||
id: "trace-id",
|
||||
input: { hello: "world" },
|
||||
output: "hi",
|
||||
},
|
||||
],
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -140,8 +140,8 @@ describe("/api/public/spans API Endpoint", () => {
|
||||
name: "spanName",
|
||||
},
|
||||
});
|
||||
expect(dbSpan?.id).toBe(spanId);
|
||||
expect(dbSpan?.traceId).toBe(traceId);
|
||||
expect(dbSpan.id).toBe(spanId);
|
||||
expect(dbSpan.traceId).toBe(traceId);
|
||||
});
|
||||
|
||||
it("should create trace when creating span without existing trace", async () => {
|
||||
@@ -248,14 +248,14 @@ describe("/api/public/spans API Endpoint", () => {
|
||||
|
||||
expect(createSpan.status).toBe(200);
|
||||
|
||||
expect(dbSpan?.id).toBe(spanId);
|
||||
expect(dbSpan?.traceId).toBe(dbTrace[0]?.id);
|
||||
expect(dbSpan?.name).toBe(generationName);
|
||||
expect(dbSpan?.startTime).toEqual(new Date("2021-01-01T00:00:00.000Z"));
|
||||
expect(dbSpan?.endTime).toEqual(new Date("2021-01-01T00:00:00.000Z"));
|
||||
expect(dbSpan?.input).toEqual({ key: "value" });
|
||||
expect(dbSpan?.metadata).toEqual({ key: "value" });
|
||||
expect(dbSpan?.version).toBe("2.0.0");
|
||||
expect(dbSpan.id).toBe(spanId);
|
||||
expect(dbSpan.traceId).toBe(dbTrace[0]?.id);
|
||||
expect(dbSpan.name).toBe(generationName);
|
||||
expect(dbSpan.startTime).toEqual(new Date("2021-01-01T00:00:00.000Z"));
|
||||
expect(dbSpan.endTime).toEqual(new Date("2021-01-01T00:00:00.000Z"));
|
||||
expect(dbSpan.input).toEqual({ key: "value" });
|
||||
expect(dbSpan.metadata).toEqual({ key: "value" });
|
||||
expect(dbSpan.version).toBe("2.0.0");
|
||||
});
|
||||
|
||||
it("should update span", async () => {
|
||||
|
||||
@@ -9,6 +9,11 @@ export const pruneDatabase = async () => {
|
||||
await prisma.score.deleteMany();
|
||||
await prisma.observation.deleteMany();
|
||||
await prisma.trace.deleteMany();
|
||||
await prisma.datasetItem.deleteMany();
|
||||
await prisma.dataset.deleteMany();
|
||||
await prisma.datasetRuns.deleteMany();
|
||||
await prisma.prompt.deleteMany();
|
||||
await prisma.events.deleteMany();
|
||||
};
|
||||
|
||||
export function createBasicAuthHeader(
|
||||
@@ -24,7 +29,7 @@ export function createBasicAuthHeader(
|
||||
export async function makeAPICall(
|
||||
method: "POST" | "GET" | "PUT" | "DELETE" | "PATCH",
|
||||
url: string,
|
||||
body: unknown,
|
||||
body?: unknown,
|
||||
) {
|
||||
const finalUrl = `http://localhost:3000/${url}`;
|
||||
const options = {
|
||||
@@ -37,7 +42,9 @@ export async function makeAPICall(
|
||||
"sk-lf-1234567890",
|
||||
),
|
||||
},
|
||||
body: JSON.stringify(body),
|
||||
// Conditionally include the body property if the method is not "GET"
|
||||
...(method !== "GET" &&
|
||||
body !== undefined && { body: JSON.stringify(body) }),
|
||||
};
|
||||
const a = await fetch(finalUrl, options);
|
||||
|
||||
|
||||
@@ -40,33 +40,100 @@ describe("/api/public/traces API Endpoint", () => {
|
||||
id: "trace-id",
|
||||
name: "trace-name",
|
||||
userId: "user-1",
|
||||
projectId: "7a88fb47-b4e2-43b8-a06c-a5ce950dc53a",
|
||||
metadata: { key: "value" },
|
||||
release: "1.0.0",
|
||||
version: "2.0.0",
|
||||
public: true,
|
||||
});
|
||||
|
||||
const dbTrace1 = await prisma.trace.findFirst({
|
||||
where: {
|
||||
id: "trace-id",
|
||||
},
|
||||
});
|
||||
|
||||
expect(dbTrace1).not.toBeNull();
|
||||
expect(dbTrace1).toMatchObject({
|
||||
name: "trace-name",
|
||||
release: "1.0.0",
|
||||
externalId: null,
|
||||
version: "2.0.0",
|
||||
public: true,
|
||||
userId: "user-1",
|
||||
});
|
||||
|
||||
await makeAPICall("POST", "/api/public/traces", {
|
||||
id: "trace-id",
|
||||
name: "trace-name",
|
||||
userId: "user-1",
|
||||
projectId: "7a88fb47-b4e2-43b8-a06c-a5ce950dc53a",
|
||||
metadata: { key: "value" },
|
||||
release: "1.0.0",
|
||||
version: "5.0.0",
|
||||
public: false,
|
||||
});
|
||||
|
||||
const dbTrace = await prisma.trace.findMany({
|
||||
const dbTrace2 = await prisma.trace.findFirst({
|
||||
where: {
|
||||
name: "trace-name",
|
||||
},
|
||||
});
|
||||
|
||||
expect(dbTrace.length).toBeGreaterThan(0);
|
||||
expect(dbTrace[0]?.name).toBe("trace-name");
|
||||
expect(dbTrace[0]?.release).toBe("1.0.0");
|
||||
expect(dbTrace[0]?.externalId).toBeNull();
|
||||
expect(dbTrace[0]?.version).toBe("5.0.0");
|
||||
expect(dbTrace[0]?.projectId).toBe("7a88fb47-b4e2-43b8-a06c-a5ce950dc53a");
|
||||
expect(dbTrace2).not.toBeNull();
|
||||
expect(dbTrace2).toMatchObject({
|
||||
name: "trace-name",
|
||||
release: "1.0.0",
|
||||
externalId: null,
|
||||
version: "5.0.0",
|
||||
public: false,
|
||||
userId: "user-1",
|
||||
});
|
||||
});
|
||||
|
||||
it("should use tags correctly on POST and GET", async () => {
|
||||
await pruneDatabase();
|
||||
|
||||
await makeAPICall("POST", "/api/public/traces", {
|
||||
id: "trace-1",
|
||||
tags: ["tag-1", "tag-2", "tag-3"],
|
||||
});
|
||||
|
||||
await makeAPICall("POST", "/api/public/traces", {
|
||||
id: "trace-2",
|
||||
tags: ["tag-1"],
|
||||
});
|
||||
|
||||
await makeAPICall("POST", "/api/public/traces", {
|
||||
id: "trace-3",
|
||||
tags: ["tag-2", "tag-3"],
|
||||
});
|
||||
|
||||
// multiple tags
|
||||
const traces = await makeAPICall(
|
||||
"GET",
|
||||
"/api/public/traces?tags=tag-2&tags=tag-3",
|
||||
);
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-call, @typescript-eslint/no-unsafe-member-access
|
||||
const traceIds = traces.body.data.map((t: { id: string }) => t.id);
|
||||
// check for equality ok as ordered by timestamp
|
||||
expect(traceIds).toEqual(["trace-3", "trace-1"]);
|
||||
|
||||
// single tag
|
||||
const traces2 = await makeAPICall("GET", "/api/public/traces?tags=tag-1");
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-call, @typescript-eslint/no-unsafe-member-access
|
||||
const traceIds2 = traces2.body.data.map((t: { id: string }) => t.id);
|
||||
// check for equality ok as ordered by timestamp
|
||||
expect(traceIds2).toEqual(["trace-2", "trace-1"]);
|
||||
|
||||
// wrong tag
|
||||
const traces3 = await makeAPICall("GET", "/api/public/traces?tags=tag-10");
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-call, @typescript-eslint/no-unsafe-member-access
|
||||
const traceIds3 = traces3.body.data.map((t: { id: string }) => t.id);
|
||||
// check for equality ok as ordered by timestamp
|
||||
expect(traceIds3).toEqual([]);
|
||||
|
||||
// no tag
|
||||
const traces4 = await makeAPICall("GET", "/api/public/traces?tags=");
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-call, @typescript-eslint/no-unsafe-member-access
|
||||
const traceIds4 = traces4.body.data.map((t: { id: string }) => t.id);
|
||||
// check for equality ok as ordered by timestamp
|
||||
expect(traceIds4).toEqual(["trace-3", "trace-2", "trace-1"]);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -4,10 +4,13 @@ describe("Token Count Functions", () => {
|
||||
describe("token count for strings", () => {
|
||||
[
|
||||
{ model: "gpt-3.5", tokens: 114 },
|
||||
{ model: "gpt-35", tokens: 114 },
|
||||
{ model: "gpt-4-1106-preview", tokens: 114 },
|
||||
{ model: "gpt-4-vision-preview", tokens: 114 },
|
||||
{ model: "claude", tokens: 118 },
|
||||
{ model: "claude-instant-1.2", tokens: 118 },
|
||||
{ model: "gpt-3.5-turbo-1106", tokens: 114 },
|
||||
{ model: "gpt-35-turbo-1106", tokens: 114 },
|
||||
].forEach(({ model, tokens }) => {
|
||||
it(`should return token count ${tokens} for ${model}`, () => {
|
||||
const result = tokenCount({
|
||||
@@ -70,6 +73,7 @@ describe("Token Count Functions", () => {
|
||||
[
|
||||
{ model: "gpt-4", tokens: 44 },
|
||||
{ model: "gpt-3.5-turbo-16k-0613", tokens: 44 },
|
||||
{ model: "gpt-35-turbo-16k-0613", tokens: 44 },
|
||||
{ model: "claude-instant-1.2", tokens: 48 },
|
||||
].forEach(({ model, tokens }) => {
|
||||
it(`should return token count ${tokens} for ${model}`, () => {
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { VERSION } from "@/src/constants/VERSION";
|
||||
import { cn } from "@/src/utils/tailwind";
|
||||
|
||||
export const LangfuseIcon = ({
|
||||
@@ -20,9 +21,11 @@ export const LangfuseIcon = ({
|
||||
export const LangfuseLogo = ({
|
||||
className,
|
||||
size = "sm",
|
||||
version = false,
|
||||
}: {
|
||||
size?: "sm" | "xl";
|
||||
className?: string;
|
||||
version?: boolean;
|
||||
}) => (
|
||||
<div className={cn("flex items-center", className)}>
|
||||
<LangfuseIcon size={size === "sm" ? 16 : 20} />
|
||||
@@ -34,5 +37,16 @@ export const LangfuseLogo = ({
|
||||
>
|
||||
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-gray-400"
|
||||
>
|
||||
{VERSION}
|
||||
</a>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -191,7 +191,7 @@ export function DatePickerWithRange({
|
||||
{DEFAULT_DATE_RANGE_SELECTION}
|
||||
</SelectItem>
|
||||
{dateTimeAggregationOptions.toReversed().map((item) => (
|
||||
<SelectItem key={item} value={`${item}`}>
|
||||
<SelectItem key={item} value={item}>
|
||||
{item}
|
||||
</SelectItem>
|
||||
))}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { TrashIcon } from "lucide-react";
|
||||
import { useRouter } from "next/router";
|
||||
|
||||
import { useState } from "react";
|
||||
import { Button } from "@/src/components/ui/button";
|
||||
import { api } from "@/src/utils/api";
|
||||
import {
|
||||
@@ -9,6 +9,7 @@ import {
|
||||
PopoverTrigger,
|
||||
} from "@/src/components/ui/popover";
|
||||
import { useHasAccess } from "@/src/features/rbac/utils/checkAccess";
|
||||
import { usePostHog } from "posthog-js/react";
|
||||
|
||||
export function DeleteTrace({
|
||||
traceId,
|
||||
@@ -19,13 +20,16 @@ export function DeleteTrace({
|
||||
projectId: string;
|
||||
isTableAction?: boolean;
|
||||
}) {
|
||||
const [isDeleted, setIsDeleted] = useState(false);
|
||||
const router = useRouter();
|
||||
const utils = api.useContext();
|
||||
const utils = api.useUtils();
|
||||
const posthog = usePostHog();
|
||||
|
||||
const hasAccess = useHasAccess({ projectId, scope: "traces:delete" });
|
||||
|
||||
const mutDeleteTrace = api.traces.delete.useMutation({
|
||||
const mutDeleteTraces = api.traces.deleteMany.useMutation({
|
||||
onSuccess: () => {
|
||||
setIsDeleted(true);
|
||||
if (!isTableAction) {
|
||||
void router
|
||||
.push(`/project/${projectId}/traces`)
|
||||
@@ -41,14 +45,14 @@ export function DeleteTrace({
|
||||
}
|
||||
|
||||
return (
|
||||
<Popover>
|
||||
<Popover key={traceId}>
|
||||
<PopoverTrigger asChild>
|
||||
{isTableAction ? (
|
||||
<Button variant="ghost" size="xs">
|
||||
<TrashIcon className="h-4 w-4" />
|
||||
</Button>
|
||||
) : (
|
||||
<Button variant="outline" type="button">
|
||||
<Button variant="outline" type="button" size="sm">
|
||||
<TrashIcon className="h-4 w-4" />
|
||||
</Button>
|
||||
)}
|
||||
@@ -63,10 +67,16 @@ export function DeleteTrace({
|
||||
<Button
|
||||
type="button"
|
||||
variant="destructive"
|
||||
loading={mutDeleteTrace.isLoading}
|
||||
onClick={() =>
|
||||
void mutDeleteTrace.mutateAsync({ traceId, projectId })
|
||||
}
|
||||
loading={mutDeleteTraces.isLoading || isDeleted}
|
||||
onClick={() => {
|
||||
void mutDeleteTraces.mutateAsync({
|
||||
traceIds: [traceId],
|
||||
projectId,
|
||||
});
|
||||
posthog.capture("trace:delete", {
|
||||
source: isTableAction ? "table-single-row" : "trace",
|
||||
});
|
||||
}}
|
||||
>
|
||||
Delete trace
|
||||
</Button>
|
||||
|
||||
@@ -1,8 +1,16 @@
|
||||
import { Badge } from "@/src/components/ui/badge";
|
||||
import {
|
||||
HoverCard,
|
||||
HoverCardContent,
|
||||
HoverCardTrigger,
|
||||
} from "@/src/components/ui/hover-card";
|
||||
import { cn } from "@/src/utils/tailwind";
|
||||
import { MessageCircle } from "lucide-react";
|
||||
|
||||
type ScoreSimplified = {
|
||||
name: string;
|
||||
value: number;
|
||||
comment?: string | null;
|
||||
};
|
||||
|
||||
export const GroupedScoreBadges = ({
|
||||
@@ -12,7 +20,7 @@ export const GroupedScoreBadges = ({
|
||||
scores: ScoreSimplified[];
|
||||
variant?: "badge" | "headings";
|
||||
}) => {
|
||||
const groupedScores = scores.reduce(
|
||||
const groupedScores = scores.reduce<Record<string, ScoreSimplified[]>>(
|
||||
(acc, score) => {
|
||||
if (!acc[score.name] || !Array.isArray(acc[score.name])) {
|
||||
acc[score.name] = [score];
|
||||
@@ -21,7 +29,31 @@ export const GroupedScoreBadges = ({
|
||||
}
|
||||
return acc;
|
||||
},
|
||||
{} as Record<string, ScoreSimplified[]>,
|
||||
{},
|
||||
);
|
||||
|
||||
const ScoresOfGroup = (props: {
|
||||
scores: ScoreSimplified[];
|
||||
className?: string;
|
||||
}) => (
|
||||
<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)}
|
||||
{s.comment && (
|
||||
<HoverCard>
|
||||
<HoverCardTrigger className="ml-1 inline-block cursor-pointer">
|
||||
<MessageCircle size={12} />
|
||||
</HoverCardTrigger>
|
||||
<HoverCardContent>
|
||||
<p>{s.comment}</p>
|
||||
</HoverCardContent>
|
||||
</HoverCard>
|
||||
)}
|
||||
<span className="group-last/score:hidden">,</span>
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
|
||||
if (variant === "headings")
|
||||
@@ -32,9 +64,7 @@ export const GroupedScoreBadges = ({
|
||||
.map(([name, scores]) => (
|
||||
<div key={name}>
|
||||
<div className="text-xs text-gray-500">{name}</div>
|
||||
<div className="text-xs">
|
||||
{scores.map((s) => s.value.toFixed(2)).join(", ")}
|
||||
</div>
|
||||
<ScoresOfGroup scores={scores} />
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
@@ -50,7 +80,7 @@ export const GroupedScoreBadges = ({
|
||||
key={name}
|
||||
className="break-all font-normal"
|
||||
>
|
||||
{name}: {scores.map((s) => s.value.toFixed(2)).join(", ")}
|
||||
{name}: <ScoresOfGroup scores={scores} className="ml-2" />
|
||||
</Badge>
|
||||
))}
|
||||
</>
|
||||
|
||||
@@ -5,34 +5,43 @@ import {
|
||||
} from "@/src/components/ui/hover-card";
|
||||
import { HelpCircle } from "lucide-react";
|
||||
import Link from "next/link";
|
||||
import { usePostHog } from "posthog-js/react";
|
||||
|
||||
export type DocPopupProps = {
|
||||
description: React.ReactNode;
|
||||
link: string;
|
||||
href: string;
|
||||
size?: "sm" | "md" | "lg";
|
||||
};
|
||||
|
||||
export default function DocPopup({ description, link, size }: DocPopupProps) {
|
||||
let sizeClass = "w-4 h-4";
|
||||
switch (size) {
|
||||
case "sm": {
|
||||
sizeClass = "w-4 h-4";
|
||||
}
|
||||
case "md": {
|
||||
sizeClass = "w-6 h-6";
|
||||
}
|
||||
case "lg": {
|
||||
sizeClass = "w-8 h-8";
|
||||
}
|
||||
}
|
||||
export default function DocPopup({
|
||||
description,
|
||||
href,
|
||||
size = "sm",
|
||||
}: DocPopupProps) {
|
||||
const sizes = {
|
||||
sm: "w-4 h-4",
|
||||
md: "w-6 h-6",
|
||||
lg: "w-8 h-8",
|
||||
};
|
||||
const posthog = usePostHog();
|
||||
|
||||
return (
|
||||
<HoverCard openDelay={200}>
|
||||
<HoverCardTrigger className="mx-2 cursor-pointer">
|
||||
<Link href={link} rel="noopener" target="_blank">
|
||||
<div className="whitespace-nowrap text-gray-500 sm:pl-0">
|
||||
<HelpCircle className={sizeClass} />
|
||||
</div>
|
||||
<HoverCard
|
||||
openDelay={200}
|
||||
onOpenChange={(open) => {
|
||||
if (open) {
|
||||
posthog.capture("help-popup:opened", { href, description });
|
||||
}
|
||||
}}
|
||||
>
|
||||
<HoverCardTrigger className="mx-1 cursor-pointer" asChild>
|
||||
<Link
|
||||
href={href}
|
||||
rel="noopener"
|
||||
target="_blank"
|
||||
className="inline-block whitespace-nowrap text-gray-500 sm:pl-0"
|
||||
>
|
||||
<HelpCircle className={sizes[size]} />
|
||||
</Link>
|
||||
</HoverCardTrigger>
|
||||
<HoverCardContent>
|
||||
|
||||
@@ -2,12 +2,20 @@ import { ChevronLeftIcon, ChevronRightIcon } from "@heroicons/react/20/solid";
|
||||
import Link from "next/link";
|
||||
import { useRouter } from "next/router";
|
||||
import { useSession } from "next-auth/react";
|
||||
import DocPopup from "@/src/components/layouts/doc-popup";
|
||||
import { type Status, StatusBadge } from "./status-badge";
|
||||
import { cn } from "@/src/utils/tailwind";
|
||||
|
||||
export default function Header(props: {
|
||||
export default function Header({
|
||||
level = "h2",
|
||||
...props
|
||||
}: {
|
||||
title: string;
|
||||
breadcrumb?: { name: string; href?: string }[];
|
||||
live?: boolean;
|
||||
status?: Status;
|
||||
help?: { description: string; href: string };
|
||||
actionButtons?: React.ReactNode;
|
||||
level?: "h2" | "h3";
|
||||
}) {
|
||||
const router = useRouter();
|
||||
const session = useSession();
|
||||
@@ -16,12 +24,23 @@ export default function Header(props: {
|
||||
const projectId = router.query.projectId;
|
||||
|
||||
const project = session.data?.user?.projects.find((p) => p.id === projectId);
|
||||
const breadcrumb = [
|
||||
...(project && projectId && currentPath !== "/project/[projectId]"
|
||||
? [
|
||||
{
|
||||
name: project.name,
|
||||
href: `/project/${projectId as string}`,
|
||||
},
|
||||
]
|
||||
: []),
|
||||
...(props.breadcrumb ?? []),
|
||||
];
|
||||
const backHref =
|
||||
props.breadcrumb &&
|
||||
[...props.breadcrumb.map((i) => i.href).filter(Boolean)].pop();
|
||||
|
||||
return (
|
||||
<div className="mb-8">
|
||||
<div className={cn(level === "h2" ? "mb-8" : "mb-1")}>
|
||||
<div>
|
||||
{backHref ? (
|
||||
<nav className="sm:hidden" aria-label="Back">
|
||||
@@ -37,10 +56,10 @@ export default function Header(props: {
|
||||
</Link>
|
||||
</nav>
|
||||
) : null}
|
||||
{props.breadcrumb ? (
|
||||
{(level === "h2" || props.breadcrumb) && breadcrumb.length ? (
|
||||
<nav className="hidden sm:flex" aria-label="Breadcrumb">
|
||||
<ol role="list" className="flex items-center space-x-4">
|
||||
{props.breadcrumb.map(({ name, href }, index) => (
|
||||
{breadcrumb.map(({ name, href }, index) => (
|
||||
<li key={index}>
|
||||
<div className="flex items-center">
|
||||
{index !== 0 && (
|
||||
@@ -68,30 +87,31 @@ export default function Header(props: {
|
||||
</nav>
|
||||
) : null}
|
||||
</div>
|
||||
<div className="mt-2 flex flex-wrap items-center justify-between gap-5">
|
||||
<div className="mt-2 flex flex-wrap items-center justify-between gap-2">
|
||||
<div className="flex items-center gap-3 md:gap-5">
|
||||
<div className="min-w-0">
|
||||
{project && projectId && currentPath !== "/project/[projectId]" ? (
|
||||
<div className="text-sm font-medium text-gray-500">
|
||||
{project.name}
|
||||
</div>
|
||||
<div className="flex min-w-0 flex-row">
|
||||
{level === "h2" ? (
|
||||
<h2 className="text-2xl font-bold leading-7 text-gray-900 sm:truncate sm:text-3xl sm:tracking-tight">
|
||||
{props.title}
|
||||
</h2>
|
||||
) : (
|
||||
<h3 className="text-lg font-bold leading-7 text-gray-900 sm:truncate sm:text-xl sm:tracking-tight">
|
||||
{props.title}
|
||||
</h3>
|
||||
)}
|
||||
{props.help ? (
|
||||
<DocPopup
|
||||
description={props.help.description}
|
||||
href={props.help.href}
|
||||
size="sm"
|
||||
/>
|
||||
) : null}
|
||||
<h2 className="text-2xl font-bold leading-7 text-gray-900 sm:truncate sm:text-3xl sm:tracking-tight">
|
||||
{props.title}
|
||||
</h2>
|
||||
</div>
|
||||
{props.live ? (
|
||||
<div className="flex items-center gap-2 rounded-sm bg-green-100 px-3 text-green-600">
|
||||
<span className="relative flex h-2 w-2 ">
|
||||
<span className="absolute inline-flex h-full w-full animate-ping rounded-full bg-green-500 opacity-75"></span>
|
||||
<span className="relative inline-flex h-2 w-2 rounded-full bg-green-600"></span>
|
||||
</span>
|
||||
Live
|
||||
</div>
|
||||
) : null}
|
||||
{props.status && <StatusBadge type={props.status} />}
|
||||
</div>
|
||||
<div className="flex items-center gap-3">
|
||||
{props.actionButtons ?? null}
|
||||
</div>
|
||||
<div className="md:flex-1" />
|
||||
{props.actionButtons ?? null}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
+268
-190
@@ -1,12 +1,12 @@
|
||||
import { ROUTES } from "@/src/components/layouts/routes";
|
||||
import { ROUTES, type Route } from "@/src/components/layouts/routes";
|
||||
import { Fragment, type PropsWithChildren, useState } from "react";
|
||||
import { Dialog, Menu, Transition } from "@headlessui/react";
|
||||
import { Dialog, Disclosure, Menu, Transition } from "@headlessui/react";
|
||||
import { Bars3Icon, XMarkIcon } from "@heroicons/react/24/outline";
|
||||
|
||||
import Link from "next/link";
|
||||
import { useRouter } from "next/router";
|
||||
import clsx from "clsx";
|
||||
import { Code, MessageSquarePlus, Info } from "lucide-react";
|
||||
import { Code, MessageSquarePlus, Info, ChevronRightIcon } from "lucide-react";
|
||||
import { signOut, useSession } from "next-auth/react";
|
||||
import { ChevronDownIcon } from "@heroicons/react/20/solid";
|
||||
import { cn } from "@/src/utils/tailwind";
|
||||
@@ -21,7 +21,9 @@ import { FeedbackButtonWrapper } from "@/src/features/feedback/component/Feedbac
|
||||
import { Button } from "@/src/components/ui/button";
|
||||
import Head from "next/head";
|
||||
import { env } from "@/src/env.mjs";
|
||||
import { LangfuseIcon, LangfuseLogo } from "@/src/components/LangfuseLogo";
|
||||
import { LangfuseLogo } from "@/src/components/LangfuseLogo";
|
||||
import { Spinner } from "@/src/components/layouts/spinner";
|
||||
import { hasAccess } from "@/src/features/rbac/utils/checkAccess";
|
||||
|
||||
const userNavigation = [
|
||||
{
|
||||
@@ -33,42 +35,66 @@ const userNavigation = [
|
||||
},
|
||||
];
|
||||
|
||||
const pathsWithoutNavigation: string[] = [];
|
||||
const unauthenticatedPaths = ["/auth/sign-in", "/auth/sign-up"];
|
||||
const pathsWithoutNavigation: string[] = ["/onboarding"];
|
||||
const unauthenticatedPaths: string[] = ["/auth/sign-in", "/auth/sign-up"];
|
||||
const publishablePaths: string[] = [
|
||||
"/project/[projectId]/sessions/[sessionId]",
|
||||
"/project/[projectId]/traces/[traceId]",
|
||||
];
|
||||
|
||||
export default function Layout(props: PropsWithChildren) {
|
||||
const [sidebarOpen, setSidebarOpen] = useState(false);
|
||||
const router = useRouter();
|
||||
const session = useSession();
|
||||
const enableExperimentalFeatures =
|
||||
api.environment.enableExperimentalFeatures.useQuery().data;
|
||||
api.environment.enableExperimentalFeatures.useQuery().data ?? false;
|
||||
|
||||
const projectId = router.query.projectId as string | undefined;
|
||||
const navigation = ROUTES.filter(
|
||||
({ pathname }) => projectId || !pathname.includes("[projectId]"),
|
||||
)
|
||||
.filter(
|
||||
({ featureFlag }) =>
|
||||
featureFlag === undefined ||
|
||||
|
||||
const mapNavigation = (route: Route): NavigationItem | null => {
|
||||
// Project-level routes
|
||||
if (!projectId && route.pathname?.includes("[projectId]")) return null;
|
||||
|
||||
// Feature Flags
|
||||
if (
|
||||
!(
|
||||
route.featureFlag === undefined ||
|
||||
enableExperimentalFeatures ||
|
||||
session.data?.user?.featureFlags[featureFlag],
|
||||
session.data?.user?.featureFlags[route.featureFlag]
|
||||
)
|
||||
)
|
||||
.map(({ pathname, ...rest }) => ({
|
||||
pathname,
|
||||
href: pathname.replace("[projectId]", projectId ?? ""),
|
||||
...rest,
|
||||
}))
|
||||
.map(({ pathname, ...rest }) => ({
|
||||
pathname,
|
||||
current: router.pathname === pathname,
|
||||
...rest,
|
||||
}));
|
||||
return null;
|
||||
|
||||
// RBAC
|
||||
if (
|
||||
route.rbacScope !== undefined &&
|
||||
(!projectId || !hasAccess({ projectId, scope: route.rbacScope, session }))
|
||||
)
|
||||
return null;
|
||||
|
||||
// apply to children as well
|
||||
const children: (NavigationItem | null)[] =
|
||||
route.children?.map((child) => mapNavigation(child)).filter(Boolean) ??
|
||||
[];
|
||||
return {
|
||||
...route,
|
||||
href: route.pathname?.replace("[projectId]", projectId ?? ""),
|
||||
current: router.pathname === route.pathname,
|
||||
children:
|
||||
children.length > 0
|
||||
? (children as NavigationItem[]) // does not include null due to filter
|
||||
: undefined,
|
||||
};
|
||||
};
|
||||
|
||||
const navigationMapped: (NavigationItem | null)[] = ROUTES.map((route) =>
|
||||
mapNavigation(route),
|
||||
).filter(Boolean);
|
||||
const navigation = navigationMapped.filter(Boolean) as NavigationItem[]; // does not include null due to filter
|
||||
|
||||
const currentPathName = navigation.find(({ current }) => current)?.name;
|
||||
|
||||
const projects = api.projects.all.useQuery(undefined, {
|
||||
enabled: session.status === "authenticated",
|
||||
});
|
||||
const projects = session.data?.user?.projects ?? [];
|
||||
|
||||
if (session.status === "loading") return <Spinner message="Loading" />;
|
||||
|
||||
@@ -77,6 +103,7 @@ export default function Layout(props: PropsWithChildren) {
|
||||
session.data &&
|
||||
session.data.user === null &&
|
||||
!unauthenticatedPaths.includes(router.pathname) &&
|
||||
!publishablePaths.includes(router.pathname) &&
|
||||
!router.pathname.startsWith("/public/")
|
||||
) {
|
||||
void signOut({
|
||||
@@ -88,9 +115,10 @@ export default function Layout(props: PropsWithChildren) {
|
||||
if (
|
||||
session.status === "unauthenticated" &&
|
||||
!unauthenticatedPaths.includes(router.pathname) &&
|
||||
!publishablePaths.includes(router.pathname) &&
|
||||
!router.pathname.startsWith("/public/")
|
||||
) {
|
||||
void router.push("/auth/sign-in");
|
||||
void router.replace("/auth/sign-in");
|
||||
return <Spinner message="Redirecting" />;
|
||||
}
|
||||
|
||||
@@ -98,18 +126,18 @@ export default function Layout(props: PropsWithChildren) {
|
||||
session.status === "authenticated" &&
|
||||
unauthenticatedPaths.includes(router.pathname)
|
||||
) {
|
||||
void router.push("/");
|
||||
void router.replace("/");
|
||||
return <Spinner message="Redirecting" />;
|
||||
}
|
||||
|
||||
const hideNavigation =
|
||||
session.status === "unauthenticated" ||
|
||||
projects.data?.length === 0 ||
|
||||
projects.length === 0 ||
|
||||
pathsWithoutNavigation.includes(router.pathname) ||
|
||||
router.pathname.startsWith("/public/");
|
||||
if (hideNavigation)
|
||||
return (
|
||||
<main className="h-full bg-gray-50 px-4 py-4 sm:px-6 lg:px-8">
|
||||
<main className="min-h-screen bg-gray-50 px-4 py-4 sm:px-6 lg:px-8">
|
||||
{props.children}
|
||||
</main>
|
||||
);
|
||||
@@ -193,46 +221,10 @@ export default function Layout(props: PropsWithChildren) {
|
||||
</Transition.Child>
|
||||
{/* Sidebar component, swap this element with another sidebar if you like */}
|
||||
<div className="flex grow flex-col gap-y-5 overflow-y-auto bg-white px-6 py-4">
|
||||
<LangfuseLogo size="xl" />
|
||||
<LangfuseLogo version size="xl" />
|
||||
<nav className="flex flex-1 flex-col">
|
||||
<ul role="list" className="flex flex-1 flex-col gap-y-7">
|
||||
<li>
|
||||
<ul role="list" className="-mx-2 space-y-1">
|
||||
{navigation.map((item) => (
|
||||
<li key={item.name}>
|
||||
<Link
|
||||
href={item.href}
|
||||
className={clsx(
|
||||
item.current
|
||||
? "bg-gray-50 text-indigo-600"
|
||||
: "text-gray-700 hover:bg-gray-50 hover:text-indigo-600",
|
||||
"group flex gap-x-3 rounded-md p-2 text-sm font-semibold leading-6",
|
||||
)}
|
||||
>
|
||||
<item.icon
|
||||
className={clsx(
|
||||
item.current
|
||||
? "text-indigo-600"
|
||||
: "text-gray-400 group-hover:text-indigo-600",
|
||||
"h-6 w-6 shrink-0",
|
||||
)}
|
||||
aria-hidden="true"
|
||||
/>
|
||||
{item.name}
|
||||
</Link>
|
||||
</li>
|
||||
))}
|
||||
<FeedbackButtonWrapper className="w-full">
|
||||
<li className="group flex cursor-pointer gap-x-3 rounded-md p-2 text-sm font-semibold leading-6 text-gray-700 hover:bg-gray-50 hover:text-indigo-600">
|
||||
<MessageSquarePlus
|
||||
className="h-6 w-6 shrink-0 text-gray-400 group-hover:text-indigo-600"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
Feedback
|
||||
</li>
|
||||
</FeedbackButtonWrapper>
|
||||
</ul>
|
||||
</li>
|
||||
<MainNavigation nav={navigation} />
|
||||
<li>
|
||||
<div className="flex flex-row place-content-between items-center">
|
||||
<div className="text-xs font-semibold leading-6 text-gray-400">
|
||||
@@ -241,7 +233,7 @@ export default function Layout(props: PropsWithChildren) {
|
||||
<NewProjectButton size="xs" />
|
||||
</div>
|
||||
<ul role="list" className="-mx-2 mt-2 space-y-1">
|
||||
{projects.data?.map((project) => (
|
||||
{projects.map((project) => (
|
||||
<li key={project.name}>
|
||||
<Link
|
||||
href={`/project/${project.id}`}
|
||||
@@ -268,7 +260,7 @@ export default function Layout(props: PropsWithChildren) {
|
||||
{project.role === "VIEWER" ? (
|
||||
<span
|
||||
className={cn(
|
||||
"whitespace-nowrap break-keep rounded-sm border p-1 text-xs",
|
||||
"self-center whitespace-nowrap break-keep rounded-sm border px-1 py-0.5 text-xs",
|
||||
projectId === project.id
|
||||
? "border-indigo-600 text-indigo-600"
|
||||
: "border-gray-200 text-gray-400 group-hover:border-indigo-600 group-hover:text-indigo-600",
|
||||
@@ -294,48 +286,11 @@ export default function Layout(props: PropsWithChildren) {
|
||||
{/* Static sidebar for desktop */}
|
||||
<div className="hidden xl:fixed xl:inset-y-0 xl:z-50 xl:flex xl:w-72 xl:flex-col">
|
||||
{/* Sidebar component, swap this element with another sidebar if you like */}
|
||||
<div className="flex grow flex-col gap-y-5 overflow-y-auto border-r border-gray-200 bg-white px-6 pb-3 pt-7">
|
||||
<LangfuseLogo size="xl" className="mb-4" />
|
||||
<nav className="flex flex-1 flex-col">
|
||||
<ul role="list" className="flex flex-1 flex-col gap-y-4">
|
||||
<li>
|
||||
<ul role="list" className="-mx-2 space-y-1">
|
||||
{navigation.map((item) => (
|
||||
<li key={item.name}>
|
||||
<Link
|
||||
href={item.href}
|
||||
className={clsx(
|
||||
item.current
|
||||
? "bg-gray-50 text-indigo-600"
|
||||
: "text-gray-700 hover:bg-gray-50 hover:text-indigo-600",
|
||||
"group flex gap-x-3 rounded-md p-2 text-sm font-semibold leading-6",
|
||||
)}
|
||||
>
|
||||
<item.icon
|
||||
className={clsx(
|
||||
item.current
|
||||
? "text-indigo-600"
|
||||
: "text-gray-400 group-hover:text-indigo-600",
|
||||
"h-6 w-6 shrink-0",
|
||||
)}
|
||||
aria-hidden="true"
|
||||
/>
|
||||
{item.name}
|
||||
</Link>
|
||||
</li>
|
||||
))}
|
||||
<FeedbackButtonWrapper className="w-full">
|
||||
<li className="group flex cursor-pointer gap-x-3 rounded-md p-2 text-sm font-semibold leading-6 text-gray-700 hover:bg-gray-50 hover:text-indigo-600">
|
||||
<MessageSquarePlus
|
||||
className="h-6 w-6 shrink-0 text-gray-400 group-hover:text-indigo-600"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
Feedback
|
||||
</li>
|
||||
</FeedbackButtonWrapper>
|
||||
</ul>
|
||||
</li>
|
||||
|
||||
<div className="flex h-screen grow flex-col gap-y-5 border-r border-gray-200 bg-white pt-7">
|
||||
<LangfuseLogo version size="xl" className="mb-2 px-6" />
|
||||
<nav className="flex h-full flex-1 flex-col overflow-y-auto px-6 pb-3">
|
||||
<ul role="list" className="flex h-full flex-col gap-y-4">
|
||||
<MainNavigation nav={navigation} />
|
||||
<li className="mt-auto">
|
||||
<div className="flex flex-row place-content-between items-center">
|
||||
<div className="text-xs font-semibold leading-6 text-gray-400">
|
||||
@@ -344,7 +299,7 @@ export default function Layout(props: PropsWithChildren) {
|
||||
<NewProjectButton size="xs" />
|
||||
</div>
|
||||
<ul role="list" className="-mx-2 mt-2 space-y-1">
|
||||
{projects.data?.map((project, index) => (
|
||||
{projects.map((project, index) => (
|
||||
<li key={project.name}>
|
||||
<Link
|
||||
href={`/project/${project.id}`}
|
||||
@@ -374,7 +329,7 @@ export default function Layout(props: PropsWithChildren) {
|
||||
{project.role === "VIEWER" ? (
|
||||
<span
|
||||
className={cn(
|
||||
"whitespace-nowrap break-keep rounded-sm border p-1 text-xs",
|
||||
"self-center whitespace-nowrap break-keep rounded-sm border px-1 py-0.5 text-xs",
|
||||
projectId === project.id
|
||||
? "border-indigo-600 text-indigo-600"
|
||||
: "border-gray-200 text-gray-400 group-hover:border-indigo-600 group-hover:text-indigo-600",
|
||||
@@ -388,64 +343,60 @@ export default function Layout(props: PropsWithChildren) {
|
||||
))}
|
||||
</ul>
|
||||
</li>
|
||||
|
||||
<li className="-mx-6 ">
|
||||
<Menu as="div" className="relative">
|
||||
<Menu.Button className="flex w-full items-center gap-x-4 p-1.5 px-6 py-3 text-sm font-semibold leading-6 text-gray-900 hover:bg-gray-50">
|
||||
<span className="sr-only">Open user menu</span>
|
||||
<Avatar className="h-8 w-8">
|
||||
<AvatarImage
|
||||
src={session.data?.user?.image ?? undefined}
|
||||
/>
|
||||
<AvatarFallback>
|
||||
{session.data?.user?.name
|
||||
? session.data.user.name
|
||||
.split(" ")
|
||||
.map((word) => word[0])
|
||||
.slice(0, 2)
|
||||
.concat("")
|
||||
: null}
|
||||
</AvatarFallback>
|
||||
</Avatar>
|
||||
<span className="flex-shrink truncate text-sm font-semibold leading-6 text-gray-900">
|
||||
{session.data?.user?.name}
|
||||
</span>
|
||||
<ChevronDownIcon
|
||||
className="h-5 w-5 text-gray-400"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
</Menu.Button>
|
||||
<Transition
|
||||
as={Fragment}
|
||||
enter="transition ease-out duration-100"
|
||||
enterFrom="transform opacity-0 scale-95"
|
||||
enterTo="transform opacity-100 scale-100"
|
||||
leave="transition ease-in duration-75"
|
||||
leaveFrom="transform opacity-100 scale-100"
|
||||
leaveTo="transform opacity-0 scale-95"
|
||||
>
|
||||
<Menu.Items className="absolute -top-full right-0 z-10 mt-2.5 w-32 rounded-md bg-white py-2 shadow-lg ring-1 ring-gray-900/5 focus:outline-none">
|
||||
{userNavigation.map((item) => (
|
||||
<Menu.Item key={item.name}>
|
||||
{({ active }) => (
|
||||
<a
|
||||
onClick={() => void item.onClick()}
|
||||
className={cn(
|
||||
active ? "bg-gray-50" : "",
|
||||
"block cursor-pointer px-3 py-1 text-sm leading-6 text-gray-900",
|
||||
)}
|
||||
>
|
||||
{item.name}
|
||||
</a>
|
||||
)}
|
||||
</Menu.Item>
|
||||
))}
|
||||
</Menu.Items>
|
||||
</Transition>
|
||||
</Menu>
|
||||
</li>
|
||||
</ul>
|
||||
</nav>
|
||||
|
||||
<Menu as="div" className="relative left-1">
|
||||
<Menu.Button className="flex w-full items-center gap-x-4 p-1.5 px-6 py-3 text-sm font-semibold leading-6 text-gray-900 hover:bg-gray-50">
|
||||
<span className="sr-only">Open user menu</span>
|
||||
<Avatar className="h-8 w-8">
|
||||
<AvatarImage src={session.data?.user?.image ?? undefined} />
|
||||
<AvatarFallback>
|
||||
{session.data?.user?.name
|
||||
? session.data.user.name
|
||||
.split(" ")
|
||||
.map((word) => word[0])
|
||||
.slice(0, 2)
|
||||
.concat("")
|
||||
: null}
|
||||
</AvatarFallback>
|
||||
</Avatar>
|
||||
<span className="flex-shrink truncate text-sm font-semibold leading-6 text-gray-900">
|
||||
{session.data?.user?.name}
|
||||
</span>
|
||||
<ChevronDownIcon
|
||||
className="h-5 w-5 text-gray-400"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
</Menu.Button>
|
||||
<Transition
|
||||
as={Fragment}
|
||||
enter="transition ease-out duration-100"
|
||||
enterFrom="transform opacity-0 scale-95"
|
||||
enterTo="transform opacity-100 scale-100"
|
||||
leave="transition ease-in duration-75"
|
||||
leaveFrom="transform opacity-100 scale-100"
|
||||
leaveTo="transform opacity-0 scale-95"
|
||||
>
|
||||
<Menu.Items className="absolute -top-full right-0 z-10 mt-2.5 w-32 rounded-md bg-white py-2 shadow-lg ring-1 ring-gray-900/5 focus:outline-none">
|
||||
{userNavigation.map((item) => (
|
||||
<Menu.Item key={item.name}>
|
||||
{({ active }) => (
|
||||
<a
|
||||
onClick={() => void item.onClick()}
|
||||
className={cn(
|
||||
active ? "bg-gray-50" : "",
|
||||
"block cursor-pointer px-3 py-1 text-sm leading-6 text-gray-900",
|
||||
)}
|
||||
>
|
||||
{item.name}
|
||||
</a>
|
||||
)}
|
||||
</Menu.Item>
|
||||
))}
|
||||
</Menu.Items>
|
||||
</Transition>
|
||||
</Menu>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -458,7 +409,7 @@ export default function Layout(props: PropsWithChildren) {
|
||||
<span className="sr-only">Open sidebar</span>
|
||||
<Bars3Icon className="h-6 w-6" aria-hidden="true" />
|
||||
</button>
|
||||
<LangfuseLogo className="flex-1" />
|
||||
<LangfuseLogo version className="flex-1" />
|
||||
<Menu as="div" className="relative">
|
||||
<Menu.Button className="flex items-center gap-x-4 text-sm font-semibold leading-6 text-gray-900">
|
||||
<span className="sr-only">Open user menu</span>
|
||||
@@ -507,6 +458,8 @@ export default function Layout(props: PropsWithChildren) {
|
||||
<div className="xl:pl-72">
|
||||
{env.NEXT_PUBLIC_DEMO_PROJECT_ID &&
|
||||
projectId === env.NEXT_PUBLIC_DEMO_PROJECT_ID &&
|
||||
(env.NEXT_PUBLIC_LANGFUSE_CLOUD_REGION === "STAGING" ||
|
||||
env.NEXT_PUBLIC_LANGFUSE_CLOUD_REGION === "EU") &&
|
||||
!session.data?.user?.email?.endsWith("@langfuse.com") ? (
|
||||
<div className="flex w-full items-center border-b border-yellow-500 bg-yellow-100 px-4 py-2 xl:sticky xl:top-0 xl:z-40">
|
||||
<div className="flex flex-1 flex-wrap gap-1">
|
||||
@@ -516,31 +469,156 @@ export default function Layout(props: PropsWithChildren) {
|
||||
</div>
|
||||
<div>Live data from the Langfuse Q&A Chatbot.</div>
|
||||
</div>
|
||||
<Button size="sm" variant="ghost" asChild className="ml-2">
|
||||
<Link href="https://langfuse.com/docs/demo" target="_blank">
|
||||
Learn more ↗
|
||||
|
||||
<Button size="sm" asChild className="ml-2">
|
||||
<Link
|
||||
href={
|
||||
env.NEXT_PUBLIC_LANGFUSE_CLOUD_REGION === "EU"
|
||||
? "https://langfuse.com/docs/qa-chatbot"
|
||||
: "https://docs-staging.langfuse.com/docs/qa-chatbot"
|
||||
}
|
||||
target="_blank"
|
||||
>
|
||||
{env.NEXT_PUBLIC_LANGFUSE_CLOUD_REGION === "EU"
|
||||
? "Q&A Chatbot ↗"
|
||||
: "Q&A Chatbot (staging) ↗"}
|
||||
</Link>
|
||||
</Button>
|
||||
</div>
|
||||
) : null}
|
||||
<main className="py-4">
|
||||
<div className="px-4">{props.children}</div>
|
||||
</main>
|
||||
<main className="p-4">{props.children}</main>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
function Spinner(props: { message: string }) {
|
||||
return (
|
||||
<div className="flex min-h-full flex-1 flex-col justify-center py-12 sm:px-6 lg:px-8">
|
||||
<div className="sm:mx-auto sm:w-full sm:max-w-md">
|
||||
<LangfuseIcon className="mx-auto motion-safe:animate-spin" size={42} />
|
||||
<h2 className="mt-5 text-center text-2xl font-bold leading-9 tracking-tight text-gray-900">
|
||||
{props.message} ...
|
||||
</h2>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
type NavigationItem = NestedNavigationItem & {
|
||||
children?: NestedNavigationItem[];
|
||||
};
|
||||
|
||||
type NestedNavigationItem = Omit<Route, "children"> & {
|
||||
href?: string;
|
||||
current: boolean;
|
||||
};
|
||||
|
||||
const MainNavigation: React.FC<{
|
||||
nav: NavigationItem[];
|
||||
onNavitemClick?: () => void;
|
||||
}> = ({ nav, onNavitemClick }) => (
|
||||
<li>
|
||||
<ul role="list" className="-mx-2 space-y-1">
|
||||
{nav.map((item) => (
|
||||
<li key={item.name}>
|
||||
{(!item.children || item.children.length === 0) && item.href ? (
|
||||
<Link
|
||||
href={item.href}
|
||||
className={clsx(
|
||||
item.current
|
||||
? "bg-gray-50 text-indigo-600"
|
||||
: "text-gray-700 hover:bg-gray-50 hover:text-indigo-600",
|
||||
"group flex gap-x-3 rounded-md p-2 text-sm font-semibold leading-6",
|
||||
)}
|
||||
onClick={onNavitemClick}
|
||||
>
|
||||
{item.icon && (
|
||||
<item.icon
|
||||
className={clsx(
|
||||
item.current
|
||||
? "text-indigo-600"
|
||||
: "text-gray-400 group-hover:text-indigo-600",
|
||||
"h-6 w-6 shrink-0",
|
||||
)}
|
||||
aria-hidden="true"
|
||||
/>
|
||||
)}
|
||||
{item.name}
|
||||
{item.label && (
|
||||
<span
|
||||
className={cn(
|
||||
"self-center whitespace-nowrap break-keep rounded-sm border px-1 py-0.5 text-xs",
|
||||
item.current
|
||||
? "border-indigo-600 text-indigo-600"
|
||||
: "border-gray-200 text-gray-400 group-hover:border-indigo-600 group-hover:text-indigo-600",
|
||||
)}
|
||||
>
|
||||
{item.label}
|
||||
</span>
|
||||
)}
|
||||
</Link>
|
||||
) : item.children && item.children.length > 0 ? (
|
||||
<Disclosure
|
||||
as="div"
|
||||
defaultOpen={item.children.some((child) => child.current)}
|
||||
>
|
||||
{({ open }) => (
|
||||
<>
|
||||
<Disclosure.Button className="group flex w-full items-center gap-x-3 rounded-md p-2 text-left text-sm font-semibold leading-6 hover:bg-gray-50 hover:text-indigo-600">
|
||||
{item.icon && (
|
||||
<item.icon
|
||||
className="h-6 w-6 shrink-0 text-gray-400 group-hover:text-indigo-600"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
)}
|
||||
{item.name}
|
||||
{item.label && (
|
||||
<span
|
||||
className={cn(
|
||||
"self-center whitespace-nowrap break-keep rounded-sm border px-1 py-0.5 text-xs",
|
||||
item.current
|
||||
? "border-indigo-600 text-indigo-600"
|
||||
: "border-gray-200 text-gray-400 group-hover:border-indigo-600 group-hover:text-indigo-600",
|
||||
)}
|
||||
>
|
||||
{item.label}
|
||||
</span>
|
||||
)}
|
||||
<ChevronRightIcon
|
||||
className={clsx(
|
||||
open ? "rotate-90 text-gray-500" : "text-gray-400",
|
||||
"ml-auto h-5 w-5 shrink-0",
|
||||
)}
|
||||
aria-hidden="true"
|
||||
/>
|
||||
</Disclosure.Button>
|
||||
<Disclosure.Panel as="ul" className="mt-1 px-2">
|
||||
{item.children?.map((subItem) => (
|
||||
<li key={subItem.name}>
|
||||
{/* 44px */}
|
||||
<Link
|
||||
href={subItem.href ?? "#"}
|
||||
className={clsx(
|
||||
subItem.current
|
||||
? "bg-gray-50 text-indigo-600"
|
||||
: "text-gray-700 hover:bg-gray-50 hover:text-indigo-600",
|
||||
"flex w-full items-center gap-x-3 rounded-md py-2 pl-9 pr-2 text-sm leading-6",
|
||||
)}
|
||||
>
|
||||
{subItem.name}
|
||||
{subItem.label && (
|
||||
<span className="self-center whitespace-nowrap break-keep rounded-sm border border-gray-200 px-1 py-0.5 text-xs text-gray-400 group-hover:border-indigo-600 group-hover:text-indigo-600">
|
||||
{subItem.label}
|
||||
</span>
|
||||
)}
|
||||
</Link>
|
||||
</li>
|
||||
))}
|
||||
</Disclosure.Panel>
|
||||
</>
|
||||
)}
|
||||
</Disclosure>
|
||||
) : null}
|
||||
</li>
|
||||
))}
|
||||
<FeedbackButtonWrapper className="w-full">
|
||||
<li className="group flex cursor-pointer gap-x-3 rounded-md p-2 text-sm font-semibold leading-6 text-gray-700 hover:bg-gray-50 hover:text-indigo-600">
|
||||
<MessageSquarePlus
|
||||
className="h-6 w-6 shrink-0 text-gray-400 group-hover:text-indigo-600"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
Feedback
|
||||
</li>
|
||||
</FeedbackButtonWrapper>
|
||||
</ul>
|
||||
</li>
|
||||
);
|
||||
|
||||
@@ -1,47 +1,66 @@
|
||||
import { type Flag } from "@/src/features/feature-flags/types";
|
||||
import { type Scope } from "@/src/features/rbac/constants/roleAccessRights";
|
||||
import {
|
||||
Database,
|
||||
LayoutDashboard,
|
||||
LifeBuoy,
|
||||
LineChart,
|
||||
ListTree,
|
||||
type LucideIcon,
|
||||
Settings,
|
||||
TextSelect,
|
||||
UsersIcon,
|
||||
PenSquareIcon,
|
||||
} from "lucide-react";
|
||||
|
||||
export const ROUTES: Array<{
|
||||
export type Route = {
|
||||
name: string;
|
||||
pathname: string;
|
||||
icon: LucideIcon;
|
||||
featureFlag?: Flag;
|
||||
}> = [
|
||||
label?: string;
|
||||
rbacScope?: Scope;
|
||||
icon?: LucideIcon; // ignored for nested routes
|
||||
pathname?: string; // link, ignored if children
|
||||
children?: Array<Route>; // folder
|
||||
};
|
||||
|
||||
export const ROUTES: Route[] = [
|
||||
{
|
||||
name: "Dashboard",
|
||||
pathname: `/project/[projectId]`,
|
||||
icon: LayoutDashboard,
|
||||
},
|
||||
{
|
||||
name: "Traces",
|
||||
pathname: `/project/[projectId]/traces`,
|
||||
name: "Tracing",
|
||||
icon: ListTree,
|
||||
},
|
||||
{
|
||||
name: "Generations",
|
||||
pathname: `/project/[projectId]/generations`,
|
||||
icon: TextSelect,
|
||||
},
|
||||
{
|
||||
name: "Scores",
|
||||
pathname: `/project/[projectId]/scores`,
|
||||
icon: LineChart,
|
||||
children: [
|
||||
{
|
||||
name: "Traces",
|
||||
pathname: `/project/[projectId]/traces`,
|
||||
},
|
||||
{
|
||||
name: "Sessions",
|
||||
pathname: `/project/[projectId]/sessions`,
|
||||
},
|
||||
{
|
||||
name: "Generations",
|
||||
pathname: `/project/[projectId]/generations`,
|
||||
},
|
||||
{
|
||||
name: "Scores",
|
||||
pathname: `/project/[projectId]/scores`,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: "Users",
|
||||
pathname: `/project/[projectId]/users`,
|
||||
icon: UsersIcon,
|
||||
},
|
||||
{
|
||||
name: "Prompts",
|
||||
pathname: "/project/[projectId]/prompts",
|
||||
icon: PenSquareIcon,
|
||||
label: "Beta",
|
||||
rbacScope: "prompts:read",
|
||||
},
|
||||
{
|
||||
name: "Datasets",
|
||||
pathname: `/project/[projectId]/datasets`,
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
import { LangfuseIcon } from "@/src/components/LangfuseLogo";
|
||||
|
||||
export function Spinner(props: { message: string }) {
|
||||
return (
|
||||
<div className="flex min-h-full flex-1 flex-col justify-center py-12 sm:px-6 lg:px-8">
|
||||
<div className="sm:mx-auto sm:w-full sm:max-w-md">
|
||||
<LangfuseIcon className="mx-auto motion-safe:animate-spin" size={42} />
|
||||
<h2 className="mt-5 text-center text-2xl font-bold leading-9 tracking-tight text-gray-900">
|
||||
{props.message} ...
|
||||
</h2>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
import { cn } from "@/src/utils/tailwind";
|
||||
|
||||
const statusCategories = {
|
||||
active: ["production", "live"],
|
||||
inactive: ["disabled", "inactive"],
|
||||
};
|
||||
|
||||
export type Status =
|
||||
(typeof statusCategories)[keyof typeof statusCategories][number];
|
||||
|
||||
export const StatusBadge = (props: { className?: string; type: Status }) => {
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
"inline-flex items-center gap-2 rounded-sm px-2 py-1 text-xs",
|
||||
statusCategories.active.includes(props.type)
|
||||
? " bg-green-100 text-green-600"
|
||||
: "bg-gray-100 text-gray-800",
|
||||
props.className,
|
||||
)}
|
||||
>
|
||||
<span className="relative inline-flex h-2 w-2">
|
||||
<span
|
||||
className={cn(
|
||||
"absolute inline-flex h-full w-full rounded-full opacity-75",
|
||||
statusCategories.active.includes(props.type)
|
||||
? "animate-ping bg-green-500"
|
||||
: "bg-gray-500",
|
||||
)}
|
||||
></span>
|
||||
<span
|
||||
className={cn(
|
||||
"relative inline-flex h-2 w-2 rounded-full ",
|
||||
statusCategories.active.includes(props.type)
|
||||
? "bg-green-600"
|
||||
: "bg-gray-600",
|
||||
)}
|
||||
></span>
|
||||
</span>
|
||||
<span>{props.type}</span>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,6 @@
|
||||
export const LevelColors = {
|
||||
DEFAULT: { text: "", bg: "" },
|
||||
DEBUG: { text: "text-gray-500", bg: "bg-gray-50" },
|
||||
WARNING: { text: "text-yellow-800", bg: "bg-yellow-50" },
|
||||
ERROR: { text: "text-red-800", bg: "bg-red-50" },
|
||||
};
|
||||
@@ -0,0 +1,22 @@
|
||||
import { Button } from "@/src/components/ui/button";
|
||||
import { AlertCircle } from "lucide-react";
|
||||
import { useSession } from "next-auth/react";
|
||||
import { useRouter } from "next/router";
|
||||
|
||||
export const NoAccessError = () => {
|
||||
const session = useSession();
|
||||
const router = useRouter();
|
||||
|
||||
return (
|
||||
<div className="flex h-screen flex-col items-center justify-center">
|
||||
<AlertCircle className="mb-4 h-12 w-12 text-red-500" />
|
||||
<h1 className="mb-4 text-xl font-bold">Error</h1>
|
||||
<p className="mb-8 text-center">
|
||||
You do not have access to this resource.
|
||||
</p>
|
||||
{session.status === "unauthenticated" ? (
|
||||
<Button onClick={() => void router.push("/")}>Sign In</Button>
|
||||
) : null}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,120 @@
|
||||
import { Switch } from "@/src/components/ui/switch";
|
||||
import { useHasAccess } from "@/src/features/rbac/utils/checkAccess";
|
||||
import { api } from "@/src/utils/api";
|
||||
import { Link, LockIcon } from "lucide-react";
|
||||
import { usePostHog } from "posthog-js/react";
|
||||
import { useState } from "react";
|
||||
|
||||
export const PublishTraceSwitch = (props: {
|
||||
traceId: string;
|
||||
projectId: string;
|
||||
isPublic: boolean;
|
||||
}) => {
|
||||
const posthog = usePostHog();
|
||||
const hasAccess = useHasAccess({
|
||||
projectId: props.projectId,
|
||||
scope: "objects:publish",
|
||||
});
|
||||
const utils = api.useUtils();
|
||||
const mut = api.traces.publish.useMutation({
|
||||
onSuccess: () => utils.traces.invalidate(),
|
||||
});
|
||||
|
||||
return (
|
||||
<Base
|
||||
id={props.traceId}
|
||||
isPublic={props.isPublic}
|
||||
onChange={(val) => {
|
||||
mut.mutate({
|
||||
projectId: props.projectId,
|
||||
traceId: props.traceId,
|
||||
public: val,
|
||||
});
|
||||
posthog.capture("trace_detail:publish_trace_button_click");
|
||||
}}
|
||||
isLoading={mut.isLoading}
|
||||
disabled={!hasAccess}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export const PublishSessionSwitch = (props: {
|
||||
sessionId: string;
|
||||
projectId: string;
|
||||
isPublic: boolean;
|
||||
}) => {
|
||||
const posthog = usePostHog();
|
||||
const hasAccess = useHasAccess({
|
||||
projectId: props.projectId,
|
||||
scope: "objects:publish",
|
||||
});
|
||||
const utils = api.useUtils();
|
||||
const mut = api.sessions.publish.useMutation({
|
||||
onSuccess: () => utils.sessions.invalidate(),
|
||||
});
|
||||
|
||||
return (
|
||||
<Base
|
||||
id={props.sessionId}
|
||||
isPublic={props.isPublic}
|
||||
onChange={(val) => {
|
||||
mut.mutate({
|
||||
projectId: props.projectId,
|
||||
sessionId: props.sessionId,
|
||||
public: val,
|
||||
});
|
||||
posthog.capture("session_detail:publish_session_button_click");
|
||||
}}
|
||||
isLoading={mut.isLoading}
|
||||
disabled={!hasAccess}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
const Base = (props: {
|
||||
id: string;
|
||||
onChange: (value: boolean) => void;
|
||||
isLoading: boolean;
|
||||
isPublic: boolean;
|
||||
disabled?: boolean;
|
||||
}) => {
|
||||
const [isCopied, setIsCopied] = useState(false);
|
||||
|
||||
const copyUrl = () => {
|
||||
setIsCopied(true);
|
||||
void navigator.clipboard.writeText(window.location.href);
|
||||
setTimeout(() => setIsCopied(false), 1500);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="text-sm font-semibold">
|
||||
{props.isLoading ? (
|
||||
"Loading.."
|
||||
) : props.isPublic ? (
|
||||
<div
|
||||
className="flex cursor-pointer items-center gap-1 text-green-800"
|
||||
onClick={() => copyUrl()}
|
||||
>
|
||||
{isCopied ? "Link copied ..." : "Public"}
|
||||
<Link size={16} />
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex items-center gap-1">
|
||||
Private
|
||||
<LockIcon size={16} />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<Switch
|
||||
id="publish-trace"
|
||||
checked={props.isPublic}
|
||||
onCheckedChange={() => {
|
||||
if (props.isLoading) return;
|
||||
props.onChange(!props.isPublic);
|
||||
}}
|
||||
disabled={props.disabled}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,131 @@
|
||||
import { GroupedScoreBadges } from "@/src/components/grouped-score-badge";
|
||||
import Header from "@/src/components/layouts/header";
|
||||
import { NoAccessError } from "@/src/components/no-access";
|
||||
import { PublishSessionSwitch } from "@/src/components/publish-object-switch";
|
||||
import { StarSessionToggle } from "@/src/components/star-toggle";
|
||||
import { IOPreview } from "@/src/components/trace/IOPreview";
|
||||
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 Link from "next/link";
|
||||
import { useEffect } from "react";
|
||||
|
||||
export const SessionPage: React.FC<{
|
||||
sessionId: string;
|
||||
projectId: string;
|
||||
}> = ({ sessionId, projectId }) => {
|
||||
const { setDetailPageList } = useDetailPageLists();
|
||||
const session = api.sessions.byId.useQuery(
|
||||
{
|
||||
sessionId,
|
||||
projectId: projectId,
|
||||
},
|
||||
{
|
||||
retry(failureCount, error) {
|
||||
if (error.data?.code === "UNAUTHORIZED") return false;
|
||||
return failureCount < 3;
|
||||
},
|
||||
},
|
||||
);
|
||||
useEffect(() => {
|
||||
if (session.isSuccess) {
|
||||
setDetailPageList(
|
||||
"traces",
|
||||
session.data.traces.map((t) => t.id),
|
||||
);
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [session.isSuccess, session.data]);
|
||||
|
||||
if (session.error?.data?.code === "UNAUTHORIZED") return <NoAccessError />;
|
||||
|
||||
return (
|
||||
<div className="flex flex-col overflow-hidden xl:container">
|
||||
<Header
|
||||
title="Session"
|
||||
breadcrumb={[
|
||||
{
|
||||
name: "Sessions",
|
||||
href: `/project/${projectId}/sessions`,
|
||||
},
|
||||
{ name: sessionId },
|
||||
]}
|
||||
actionButtons={[
|
||||
<StarSessionToggle
|
||||
key="star"
|
||||
projectId={projectId}
|
||||
sessionId={sessionId}
|
||||
value={session.data?.bookmarked ?? false}
|
||||
/>,
|
||||
<PublishSessionSwitch
|
||||
projectId={projectId}
|
||||
sessionId={sessionId}
|
||||
isPublic={session.data?.public ?? false}
|
||||
key="publish"
|
||||
/>,
|
||||
<DetailPageNav
|
||||
key="nav"
|
||||
currentId={sessionId}
|
||||
path={(id) => `/project/${projectId}/sessions/${id}`}
|
||||
listKey="sessions"
|
||||
/>,
|
||||
]}
|
||||
/>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{session.data?.users.map((userId) => (
|
||||
<Link key={userId} href={`/project/${projectId}/users/${userId}`}>
|
||||
<Badge>User ID: {userId}</Badge>
|
||||
</Link>
|
||||
))}
|
||||
<Badge variant="outline">Traces: {session.data?.traces.length}</Badge>
|
||||
</div>
|
||||
<div className="mt-5 flex flex-col gap-2 border-t pt-5">
|
||||
{session.data?.traces.map((trace) => (
|
||||
<Card
|
||||
className="border-border-gray-150 group grid gap-3 p-2 shadow-none hover:border-gray-300 md:grid-cols-3"
|
||||
key={trace.id}
|
||||
>
|
||||
<div className="col-span-2 flex flex-col gap-2 p-0">
|
||||
{trace.input || trace.output ? (
|
||||
<IOPreview
|
||||
key={trace.id}
|
||||
input={trace.input}
|
||||
output={trace.output}
|
||||
hideIfNull
|
||||
/>
|
||||
) : (
|
||||
<div className="p-2 text-xs text-gray-500">
|
||||
This trace has no input or output.
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<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}) ↗
|
||||
</Link>
|
||||
<div className="text-xs text-gray-500">
|
||||
{trace.timestamp.toLocaleString()}
|
||||
</div>
|
||||
<div className="mb-1 mt-2 text-xs text-gray-500">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>
|
||||
</Card>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,228 @@
|
||||
import { StarIcon } from "lucide-react";
|
||||
|
||||
import { Button } from "@/src/components/ui/button";
|
||||
import { api } from "@/src/utils/api";
|
||||
import { useHasAccess } from "@/src/features/rbac/utils/checkAccess";
|
||||
import { cn } from "@/src/utils/tailwind";
|
||||
import { type RouterOutput, type RouterInput } from "@/src/utils/types";
|
||||
import { useState } from "react";
|
||||
|
||||
export function StarToggle({
|
||||
value,
|
||||
disabled = false,
|
||||
onClick,
|
||||
size = "sm",
|
||||
isLoading,
|
||||
}: {
|
||||
value: boolean;
|
||||
disabled?: boolean;
|
||||
onClick: (value: boolean) => Promise<unknown>;
|
||||
size?: "sm" | "xs";
|
||||
isLoading: boolean;
|
||||
}) {
|
||||
return (
|
||||
<Button
|
||||
variant="ghost"
|
||||
size={size}
|
||||
onClick={() => void onClick(!value)}
|
||||
disabled={disabled}
|
||||
loading={isLoading}
|
||||
>
|
||||
<StarIcon
|
||||
className={cn(
|
||||
"h-4 w-4",
|
||||
value ? "fill-current text-yellow-500" : "text-gray-500",
|
||||
)}
|
||||
/>
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
|
||||
export function StarTraceToggle({
|
||||
tracesFilter,
|
||||
projectId,
|
||||
traceId,
|
||||
value,
|
||||
size = "sm",
|
||||
}: {
|
||||
tracesFilter: RouterInput["traces"]["all"];
|
||||
projectId: string;
|
||||
traceId: string;
|
||||
value: boolean;
|
||||
size?: "sm" | "xs";
|
||||
}) {
|
||||
const utils = api.useUtils();
|
||||
const hasAccess = useHasAccess({ projectId, scope: "objects:bookmark" });
|
||||
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
|
||||
const mutBookmarkTrace = api.traces.bookmark.useMutation({
|
||||
// Optimistic update
|
||||
// Tanstack docs: https://tanstack.com/query/v4/docs/react/guides/optimistic-updates
|
||||
|
||||
onMutate: async () => {
|
||||
// Cancel any outgoing refetches
|
||||
// (so they don't overwrite our optimistic update)
|
||||
await utils.traces.all.cancel();
|
||||
|
||||
setIsLoading(true);
|
||||
|
||||
// Snapshot the previous value
|
||||
const prev = utils.traces.all.getData(tracesFilter);
|
||||
|
||||
return { prev };
|
||||
},
|
||||
onError: (err, _newTodo, context) => {
|
||||
setIsLoading(false);
|
||||
// Rollback to the previous value if mutation fails
|
||||
console.log("error", err);
|
||||
utils.traces.all.setData(tracesFilter, context?.prev);
|
||||
},
|
||||
onSettled: () => {
|
||||
setIsLoading(false);
|
||||
utils.traces.all.setData(
|
||||
tracesFilter,
|
||||
(oldQueryData: RouterOutput["traces"]["all"] | undefined) => {
|
||||
return {
|
||||
totalCount: oldQueryData?.totalCount,
|
||||
traces: oldQueryData?.traces
|
||||
? oldQueryData.traces.map((trace) => {
|
||||
return {
|
||||
...trace,
|
||||
bookmarked:
|
||||
trace.id === traceId
|
||||
? !trace.bookmarked
|
||||
: trace.bookmarked,
|
||||
};
|
||||
})
|
||||
: [],
|
||||
};
|
||||
},
|
||||
);
|
||||
void utils.traces.all.invalidate();
|
||||
},
|
||||
});
|
||||
|
||||
return (
|
||||
<StarToggle
|
||||
value={value}
|
||||
size={size}
|
||||
disabled={!hasAccess}
|
||||
isLoading={isLoading}
|
||||
onClick={(value) =>
|
||||
mutBookmarkTrace.mutateAsync({
|
||||
projectId,
|
||||
traceId,
|
||||
bookmarked: value,
|
||||
})
|
||||
}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export function StarTraceDetailsToggle({
|
||||
projectId,
|
||||
traceId,
|
||||
value,
|
||||
size = "sm",
|
||||
}: {
|
||||
projectId: string;
|
||||
traceId: string;
|
||||
value: boolean;
|
||||
size?: "sm" | "xs";
|
||||
}) {
|
||||
const utils = api.useUtils();
|
||||
const hasAccess = useHasAccess({ projectId, scope: "objects:bookmark" });
|
||||
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
|
||||
const mutBookmarkTrace = api.traces.bookmark.useMutation({
|
||||
onMutate: async () => {
|
||||
// Cancel any outgoing refetches
|
||||
// (so they don't overwrite our optimistic update)
|
||||
await utils.traces.byId.cancel();
|
||||
|
||||
setIsLoading(true);
|
||||
|
||||
// Snapshot the previous value
|
||||
const prevData = utils.traces.byId.getData({ traceId });
|
||||
|
||||
return { prevData };
|
||||
},
|
||||
onError: (err, _newTodo, context) => {
|
||||
setIsLoading(false);
|
||||
console.log("error", err);
|
||||
// Rollback to the previous value if mutation fails
|
||||
utils.traces.byId.setData({ traceId }, context?.prevData);
|
||||
},
|
||||
onSettled: () => {
|
||||
setIsLoading(false);
|
||||
|
||||
utils.traces.byId.setData(
|
||||
{ traceId },
|
||||
(oldQueryData: RouterOutput["traces"]["byId"] | undefined) => {
|
||||
return oldQueryData
|
||||
? {
|
||||
...oldQueryData,
|
||||
bookmarked: !oldQueryData.bookmarked,
|
||||
}
|
||||
: undefined;
|
||||
},
|
||||
);
|
||||
void utils.traces.byId.invalidate();
|
||||
void utils.traces.all.invalidate();
|
||||
},
|
||||
});
|
||||
|
||||
return (
|
||||
<StarToggle
|
||||
value={value}
|
||||
size={size}
|
||||
disabled={!hasAccess}
|
||||
isLoading={isLoading}
|
||||
onClick={(value) =>
|
||||
mutBookmarkTrace.mutateAsync({
|
||||
projectId,
|
||||
traceId,
|
||||
bookmarked: value,
|
||||
})
|
||||
}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export function StarSessionToggle({
|
||||
projectId,
|
||||
sessionId,
|
||||
value,
|
||||
size = "sm",
|
||||
}: {
|
||||
projectId: string;
|
||||
sessionId: string;
|
||||
value: boolean;
|
||||
size?: "sm" | "xs";
|
||||
}) {
|
||||
const utils = api.useUtils();
|
||||
const hasAccess = useHasAccess({ projectId, scope: "objects:bookmark" });
|
||||
const mutBookmarkSession = api.sessions.bookmark.useMutation({
|
||||
onSuccess: () => {
|
||||
void utils.sessions.invalidate();
|
||||
},
|
||||
});
|
||||
|
||||
return (
|
||||
<StarToggle
|
||||
value={value}
|
||||
size={size}
|
||||
isLoading={mutBookmarkSession.isLoading}
|
||||
disabled={!hasAccess}
|
||||
onClick={(value) =>
|
||||
mutBookmarkSession.mutateAsync({
|
||||
projectId,
|
||||
sessionId,
|
||||
bookmarked: value,
|
||||
})
|
||||
}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
import React, {
|
||||
useCallback,
|
||||
type Dispatch,
|
||||
type SetStateAction,
|
||||
useState,
|
||||
} from "react";
|
||||
import { Button } from "@/src/components/ui/button";
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuTrigger,
|
||||
DropdownMenuCheckboxItem,
|
||||
} from "@/src/components/ui/dropdown-menu";
|
||||
import { type VisibilityState } from "@tanstack/react-table";
|
||||
import { ChevronDownIcon } from "lucide-react";
|
||||
import { type LangfuseColumnDef } from "@/src/components/table/types";
|
||||
|
||||
interface DataTableColumnVisibilityFilterProps<TData, TValue> {
|
||||
columns: LangfuseColumnDef<TData, TValue>[];
|
||||
columnVisibility: VisibilityState;
|
||||
setColumnVisibility: Dispatch<SetStateAction<VisibilityState>>;
|
||||
}
|
||||
|
||||
export function DataTableColumnVisibilityFilter<TData, TValue>({
|
||||
columns,
|
||||
columnVisibility,
|
||||
setColumnVisibility,
|
||||
}: DataTableColumnVisibilityFilterProps<TData, TValue>) {
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
|
||||
const toggleColumn = useCallback(
|
||||
(columnId: string) => {
|
||||
setColumnVisibility((old) => ({
|
||||
...old,
|
||||
[columnId]: !old[columnId],
|
||||
}));
|
||||
},
|
||||
[setColumnVisibility],
|
||||
);
|
||||
|
||||
return (
|
||||
<DropdownMenu open={isOpen}>
|
||||
<DropdownMenuTrigger
|
||||
onClick={() => {
|
||||
setIsOpen(!isOpen);
|
||||
}}
|
||||
className="select-none"
|
||||
asChild
|
||||
>
|
||||
<Button variant="outline" className="ml-auto">
|
||||
Columns
|
||||
<ChevronDownIcon className="ml-2 h-4 w-4" />
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent
|
||||
align="end"
|
||||
onPointerDownOutside={() => setIsOpen(false)}
|
||||
>
|
||||
{columns.map(
|
||||
(column, index) =>
|
||||
"accessorKey" in column &&
|
||||
column.enableHiding && (
|
||||
<DropdownMenuCheckboxItem
|
||||
key={index}
|
||||
className="capitalize"
|
||||
checked={columnVisibility[column.accessorKey]}
|
||||
onCheckedChange={() =>
|
||||
toggleColumn(column.accessorKey.toString())
|
||||
}
|
||||
>
|
||||
{column.header?.toString() ?? column.accessorKey.toString()}
|
||||
</DropdownMenuCheckboxItem>
|
||||
),
|
||||
)}
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
);
|
||||
}
|
||||
@@ -1,151 +0,0 @@
|
||||
import * as React from "react";
|
||||
import { Check, PlusCircle } from "lucide-react";
|
||||
|
||||
import { Badge } from "@/src/components/ui/badge";
|
||||
import { Button } from "@/src/components/ui/button";
|
||||
|
||||
import {
|
||||
Popover,
|
||||
PopoverContent,
|
||||
PopoverTrigger,
|
||||
} from "@/src/components/ui/popover";
|
||||
import { Separator } from "@/src/components/ui/separator";
|
||||
import { type KeyValueFilter } from "@/src/utils/tanstack";
|
||||
|
||||
import { Input } from "@/src/components/ui/input";
|
||||
import {
|
||||
Command,
|
||||
CommandEmpty,
|
||||
CommandGroup,
|
||||
CommandItem,
|
||||
CommandList,
|
||||
CommandSeparator,
|
||||
} from "@/src/components/ui/command";
|
||||
import { cn } from "@/src/utils/tailwind";
|
||||
|
||||
interface DataTableKeyValueFilter {
|
||||
title?: string;
|
||||
meta: KeyValueFilter;
|
||||
}
|
||||
|
||||
export function DataTableKeyValueFilter({
|
||||
title,
|
||||
meta,
|
||||
}: DataTableKeyValueFilter) {
|
||||
const selectedValues = new Set(meta.values);
|
||||
|
||||
const [key, setKey] = React.useState("");
|
||||
const [value, setValue] = React.useState("");
|
||||
|
||||
const clearFilter = () => {
|
||||
meta.updateFunction(null);
|
||||
};
|
||||
|
||||
const filter = () => {
|
||||
meta.updateFunction({ key, value });
|
||||
setKey("");
|
||||
setValue("");
|
||||
};
|
||||
|
||||
return (
|
||||
<Popover>
|
||||
<PopoverTrigger asChild>
|
||||
<Button variant="outline" size="sm" className="h-8 border-dashed">
|
||||
<PlusCircle className="mr-2 h-4 w-4" />
|
||||
{title}
|
||||
{selectedValues
|
||||
? Array.from(selectedValues.values()).map((value) => (
|
||||
<>
|
||||
<Separator orientation="vertical" className="mx-2 h-4" />
|
||||
<Badge
|
||||
variant="secondary"
|
||||
key={`${value.key}-${value.value}`}
|
||||
className="rounded-sm px-1 font-normal"
|
||||
>
|
||||
{value.key}-{value.value}
|
||||
</Badge>
|
||||
</>
|
||||
))
|
||||
: undefined}
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent className="w-[200px] p-0" align="start">
|
||||
<Command>
|
||||
<CommandList>
|
||||
<CommandEmpty>No results found.</CommandEmpty>
|
||||
<CommandGroup>
|
||||
{Array.from(selectedValues.values()).map((selectedValue) => {
|
||||
return (
|
||||
<CommandItem
|
||||
key={selectedValue.key}
|
||||
onSelect={() => {
|
||||
meta.removeSelectedValue({
|
||||
key: selectedValue.key,
|
||||
value: selectedValue.value,
|
||||
});
|
||||
}}
|
||||
>
|
||||
<div
|
||||
className={
|
||||
"mr-2 flex h-4 w-4 items-center justify-center rounded-sm border border-primary bg-primary text-primary-foreground"
|
||||
}
|
||||
>
|
||||
<Check className={cn("h-4 w-4")} />
|
||||
</div>
|
||||
<span>
|
||||
{selectedValue.key} - {selectedValue.value}
|
||||
</span>
|
||||
</CommandItem>
|
||||
);
|
||||
})}
|
||||
</CommandGroup>
|
||||
{selectedValues.size > 0 ? <Separator /> : undefined}
|
||||
|
||||
<div className="flex flex-row justify-end p-2">
|
||||
<div className="flex flex-col space-y-1.5">
|
||||
<Input
|
||||
id="name-key"
|
||||
placeholder="Key"
|
||||
value={key}
|
||||
onChange={(e) => setKey(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex flex-col space-y-1.5">
|
||||
<Input
|
||||
id="name-value"
|
||||
placeholder="Value"
|
||||
value={value}
|
||||
onChange={(e) => setValue(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<CommandSeparator />
|
||||
<CommandGroup>
|
||||
<CommandItem
|
||||
onSelect={() => filter()}
|
||||
className="justify-center text-center"
|
||||
>
|
||||
Filter
|
||||
</CommandItem>
|
||||
</CommandGroup>
|
||||
|
||||
{meta.values && (
|
||||
<>
|
||||
<CommandSeparator />
|
||||
<CommandGroup>
|
||||
<CommandItem
|
||||
onSelect={() => clearFilter()}
|
||||
className="justify-center text-center"
|
||||
>
|
||||
Clear filters
|
||||
</CommandItem>
|
||||
</CommandGroup>
|
||||
</>
|
||||
)}
|
||||
</CommandList>
|
||||
</Command>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
);
|
||||
}
|
||||
+114
@@ -0,0 +1,114 @@
|
||||
import { ChevronDown, Trash } from "lucide-react";
|
||||
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuTrigger,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
} from "@/src/components/ui/dropdown-menu";
|
||||
import { Button } from "@/src/components/ui/button";
|
||||
import { useHasAccess } from "@/src/features/rbac/utils/checkAccess";
|
||||
import { api } from "@/src/utils/api";
|
||||
import { usePostHog } from "posthog-js/react";
|
||||
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from "@/src/components/ui/dialog";
|
||||
import { useState } from "react";
|
||||
|
||||
export function TraceTableMultiSelectAction({
|
||||
selectedTraceIds,
|
||||
projectId,
|
||||
onDeleteSuccess,
|
||||
}: {
|
||||
selectedTraceIds: string[];
|
||||
projectId: string;
|
||||
onDeleteSuccess: () => void;
|
||||
}) {
|
||||
const utils = api.useUtils();
|
||||
const [open, setOpen] = useState(false);
|
||||
const posthog = usePostHog();
|
||||
|
||||
const hasDeleteAccess = useHasAccess({ projectId, scope: "traces:delete" });
|
||||
const mutDeleteTraces = api.traces.deleteMany.useMutation({
|
||||
onSuccess: () => {
|
||||
onDeleteSuccess();
|
||||
void utils.traces.invalidate();
|
||||
},
|
||||
});
|
||||
|
||||
return (
|
||||
<>
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button
|
||||
variant="outline"
|
||||
className="bg-white p-2 font-medium text-black"
|
||||
disabled={selectedTraceIds.length < 1}
|
||||
>
|
||||
Actions ({selectedTraceIds.length} selected)
|
||||
<ChevronDown className="h-5 w-5" />
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent>
|
||||
<DropdownMenuItem
|
||||
disabled={!hasDeleteAccess}
|
||||
onClick={() => {
|
||||
setOpen(true);
|
||||
}}
|
||||
>
|
||||
<Trash className="mr-2 h-4 w-4" />
|
||||
<span>Delete</span>
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
<Dialog
|
||||
open={open}
|
||||
onOpenChange={(isOpen) => {
|
||||
if (!isOpen) {
|
||||
setOpen(false);
|
||||
}
|
||||
}}
|
||||
>
|
||||
<DialogContent className="sm:max-w-md">
|
||||
<DialogHeader>
|
||||
<DialogTitle>Delete traces</DialogTitle>
|
||||
<DialogDescription>
|
||||
This action cannot be undone and removes all the data associated
|
||||
with these traces.
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
<DialogFooter className="sm:justify-start">
|
||||
<Button
|
||||
type="button"
|
||||
variant="destructive"
|
||||
loading={mutDeleteTraces.isLoading}
|
||||
disabled={mutDeleteTraces.isLoading}
|
||||
onClick={() => {
|
||||
void mutDeleteTraces
|
||||
.mutateAsync({
|
||||
traceIds: selectedTraceIds,
|
||||
projectId,
|
||||
})
|
||||
.then(() => {
|
||||
setOpen(false);
|
||||
});
|
||||
posthog.capture("trace:delete", {
|
||||
count: selectedTraceIds.length,
|
||||
source: "table-multi-select",
|
||||
});
|
||||
}}
|
||||
>
|
||||
Delete {selectedTraceIds.length} trace(s)
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -1,204 +0,0 @@
|
||||
import * as React from "react";
|
||||
import { Check, PlusCircle } from "lucide-react";
|
||||
|
||||
import { cn } from "@/src/utils/tailwind";
|
||||
import { Badge } from "@/src/components/ui/badge";
|
||||
import { Button } from "@/src/components/ui/button";
|
||||
import {
|
||||
Command,
|
||||
CommandEmpty,
|
||||
CommandGroup,
|
||||
CommandInput,
|
||||
CommandItem,
|
||||
CommandList,
|
||||
CommandSeparator,
|
||||
} from "@/src/components/ui/command";
|
||||
import {
|
||||
Popover,
|
||||
PopoverContent,
|
||||
PopoverTrigger,
|
||||
} from "@/src/components/ui/popover";
|
||||
import { Separator } from "@/src/components/ui/separator";
|
||||
import { type TableRowOptions } from "@/src/components/table/types";
|
||||
import { type NumberComparisonFilter } from "@/src/utils/tanstack";
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectGroup,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@/src/components/ui/select";
|
||||
import { Input } from "@/src/components/ui/input";
|
||||
|
||||
interface DataTableNumberFilter {
|
||||
title?: string;
|
||||
meta: NumberComparisonFilter;
|
||||
options: TableRowOptions;
|
||||
}
|
||||
|
||||
export function DataTableNumberFilter({
|
||||
title,
|
||||
meta,
|
||||
options,
|
||||
}: DataTableNumberFilter) {
|
||||
const clearFilter = () => {
|
||||
meta.selectedValues = { name: null, operator: null, value: null };
|
||||
meta.updateFunction(null);
|
||||
};
|
||||
|
||||
const filter = () => {
|
||||
if (
|
||||
meta.selectedValues?.operator &&
|
||||
meta.selectedValues?.value !== null &&
|
||||
meta.selectedValues?.name
|
||||
) {
|
||||
meta.updateFunction({
|
||||
name: meta.selectedValues?.name,
|
||||
operator: meta.selectedValues.operator,
|
||||
value: meta.selectedValues.value,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Popover>
|
||||
<PopoverTrigger asChild>
|
||||
<Button variant="outline" size="sm" className="h-8 border-dashed">
|
||||
<PlusCircle className="mr-2 h-4 w-4" />
|
||||
{title}
|
||||
{meta.values ? (
|
||||
<>
|
||||
<Separator orientation="vertical" className="mx-2 h-4" />
|
||||
<Badge
|
||||
variant="secondary"
|
||||
key={meta.values.name}
|
||||
className="rounded-sm px-1 font-normal"
|
||||
>
|
||||
{meta.values.name}-{meta.values.operator}-{meta.values.value}
|
||||
</Badge>
|
||||
</>
|
||||
) : undefined}
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent className="w-[200px] p-0" align="start">
|
||||
<Command>
|
||||
<CommandInput placeholder={title} />
|
||||
<CommandList>
|
||||
<CommandEmpty>No results found.</CommandEmpty>
|
||||
<CommandGroup>
|
||||
{options.options.map((option) => {
|
||||
const isSelected =
|
||||
meta.selectedValues?.name &&
|
||||
meta.selectedValues?.name === option.label;
|
||||
return (
|
||||
<CommandItem
|
||||
key={option.label}
|
||||
onSelect={() => {
|
||||
if (isSelected) {
|
||||
meta.selectedValues.name = null;
|
||||
} else {
|
||||
meta.updateSelectedScores({
|
||||
...meta.selectedValues,
|
||||
name: option.label,
|
||||
});
|
||||
}
|
||||
}}
|
||||
>
|
||||
<div
|
||||
className={cn(
|
||||
"mr-2 flex h-4 w-4 items-center justify-center rounded-sm border border-primary",
|
||||
isSelected
|
||||
? "bg-primary text-primary-foreground"
|
||||
: "opacity-50 [&_svg]:invisible",
|
||||
)}
|
||||
>
|
||||
<Check className={cn("h-4 w-4")} />
|
||||
</div>
|
||||
{option.icon && (
|
||||
<option.icon className="mr-2 h-4 w-4 text-muted-foreground" />
|
||||
)}
|
||||
<span>{option.label}</span>
|
||||
{option.value && (
|
||||
<span className="ml-auto flex h-4 w-4 items-center justify-center font-mono text-xs">
|
||||
{option.value}
|
||||
</span>
|
||||
)}
|
||||
</CommandItem>
|
||||
);
|
||||
})}
|
||||
</CommandGroup>
|
||||
<Separator />
|
||||
<div className="flex flex-row justify-end p-2">
|
||||
<Select
|
||||
onValueChange={(operator) => {
|
||||
if (
|
||||
operator === "gt" ||
|
||||
operator === "gte" ||
|
||||
operator === "lt" ||
|
||||
operator === "lte" ||
|
||||
operator === "equals"
|
||||
) {
|
||||
meta.updateSelectedScores({
|
||||
...meta.selectedValues,
|
||||
operator: operator,
|
||||
});
|
||||
}
|
||||
}}
|
||||
value={meta.selectedValues.operator ?? undefined}
|
||||
>
|
||||
<SelectTrigger className="w-[180px]">
|
||||
<SelectValue placeholder="Operator" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectGroup>
|
||||
<SelectItem value="equals">equals</SelectItem>
|
||||
<SelectItem value="gt">gt</SelectItem>
|
||||
<SelectItem value="lt">lt</SelectItem>
|
||||
<SelectItem value="gte">gte</SelectItem>
|
||||
<SelectItem value="lte">lte</SelectItem>
|
||||
</SelectGroup>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<Input
|
||||
type="number"
|
||||
placeholder="Value"
|
||||
value={meta.selectedValues.value ?? undefined}
|
||||
onChange={(event) =>
|
||||
meta.updateSelectedScores({
|
||||
...meta.selectedValues,
|
||||
value: Number(event.currentTarget.value),
|
||||
})
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
<>
|
||||
<CommandSeparator />
|
||||
<CommandGroup>
|
||||
<CommandItem
|
||||
onSelect={() => filter()}
|
||||
className="justify-center text-center"
|
||||
>
|
||||
Filter
|
||||
</CommandItem>
|
||||
</CommandGroup>
|
||||
</>
|
||||
{meta.values && (
|
||||
<>
|
||||
<CommandSeparator />
|
||||
<CommandGroup>
|
||||
<CommandItem
|
||||
onSelect={() => clearFilter()}
|
||||
className="justify-center text-center"
|
||||
>
|
||||
Clear filters
|
||||
</CommandItem>
|
||||
</CommandGroup>
|
||||
</>
|
||||
)}
|
||||
</CommandList>
|
||||
</Command>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
);
|
||||
}
|
||||
@@ -1,144 +0,0 @@
|
||||
import * as React from "react";
|
||||
import { Check, PlusCircle } from "lucide-react";
|
||||
|
||||
import { cn } from "@/src/utils/tailwind";
|
||||
import { Badge } from "@/src/components/ui/badge";
|
||||
import { Button } from "@/src/components/ui/button";
|
||||
import {
|
||||
Command,
|
||||
CommandEmpty,
|
||||
CommandGroup,
|
||||
CommandInput,
|
||||
CommandItem,
|
||||
CommandList,
|
||||
CommandSeparator,
|
||||
} from "@/src/components/ui/command";
|
||||
import {
|
||||
Popover,
|
||||
PopoverContent,
|
||||
PopoverTrigger,
|
||||
} from "@/src/components/ui/popover";
|
||||
import { Separator } from "@/src/components/ui/separator";
|
||||
import { type TableRowOptions } from "@/src/components/table/types";
|
||||
import { type SelectFilter } from "@/src/utils/tanstack";
|
||||
|
||||
interface DataTableSelectFilter {
|
||||
title?: string;
|
||||
meta: SelectFilter;
|
||||
options: TableRowOptions;
|
||||
}
|
||||
|
||||
export function DataTableSelectFilter({
|
||||
title,
|
||||
meta,
|
||||
options,
|
||||
}: DataTableSelectFilter) {
|
||||
const selectedValues = new Set(meta.values ?? []);
|
||||
|
||||
return (
|
||||
<Popover>
|
||||
<PopoverTrigger asChild>
|
||||
<Button variant="outline" size="sm" className="h-8 border-dashed">
|
||||
<PlusCircle className="mr-2 h-4 w-4" />
|
||||
{title}
|
||||
{selectedValues?.size > 0 && (
|
||||
<>
|
||||
<Separator orientation="vertical" className="mx-2 h-4" />
|
||||
<Badge
|
||||
variant="secondary"
|
||||
className="rounded-sm px-1 font-normal lg:hidden"
|
||||
>
|
||||
{selectedValues.size}
|
||||
</Badge>
|
||||
<div className="hidden space-x-1 lg:flex">
|
||||
{selectedValues.size > 2 ? (
|
||||
<Badge
|
||||
variant="secondary"
|
||||
className="rounded-sm px-1 font-normal"
|
||||
>
|
||||
{selectedValues.size} selected
|
||||
</Badge>
|
||||
) : (
|
||||
options.options
|
||||
.filter((option) => selectedValues.has(option.label))
|
||||
.map((option) => (
|
||||
<Badge
|
||||
variant="secondary"
|
||||
key={option.value}
|
||||
className="rounded-sm px-1 font-normal"
|
||||
>
|
||||
{option.label}
|
||||
</Badge>
|
||||
))
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent className="w-[200px] p-0" align="start">
|
||||
<Command>
|
||||
<CommandInput placeholder={title} />
|
||||
<CommandList>
|
||||
<CommandEmpty>No results found.</CommandEmpty>
|
||||
<CommandGroup>
|
||||
{options.options.map((option) => {
|
||||
const isSelected = selectedValues.has(option.label);
|
||||
return (
|
||||
<CommandItem
|
||||
key={option.label}
|
||||
onSelect={() => {
|
||||
if (isSelected) {
|
||||
selectedValues.delete(option.label);
|
||||
} else {
|
||||
selectedValues.add(option.label);
|
||||
}
|
||||
const filterValues = Array.from(selectedValues);
|
||||
|
||||
meta.updateFunction(
|
||||
filterValues.length ? filterValues : null,
|
||||
);
|
||||
}}
|
||||
>
|
||||
<div
|
||||
className={cn(
|
||||
"mr-2 flex h-4 w-4 items-center justify-center rounded-sm border border-primary",
|
||||
isSelected
|
||||
? "bg-primary text-primary-foreground"
|
||||
: "opacity-50 [&_svg]:invisible",
|
||||
)}
|
||||
>
|
||||
<Check className={cn("h-4 w-4")} />
|
||||
</div>
|
||||
{option.icon && (
|
||||
<option.icon className="mr-2 h-4 w-4 text-muted-foreground" />
|
||||
)}
|
||||
<span>{option.label}</span>
|
||||
{option.value && (
|
||||
<span className="ml-auto flex h-4 w-4 items-center justify-center font-mono text-xs">
|
||||
{option.value}
|
||||
</span>
|
||||
)}
|
||||
</CommandItem>
|
||||
);
|
||||
})}
|
||||
</CommandGroup>
|
||||
{selectedValues.size > 0 && (
|
||||
<>
|
||||
<CommandSeparator />
|
||||
<CommandGroup>
|
||||
<CommandItem
|
||||
onSelect={() => meta.updateFunction(null)}
|
||||
className="justify-center text-center"
|
||||
>
|
||||
Clear filters
|
||||
</CommandItem>
|
||||
</CommandGroup>
|
||||
</>
|
||||
)}
|
||||
</CommandList>
|
||||
</Command>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
);
|
||||
}
|
||||
@@ -1,9 +1,12 @@
|
||||
import { Button } from "@/src/components/ui/button";
|
||||
import React, { type Dispatch, type SetStateAction, useState } from "react";
|
||||
import { Input } from "@/src/components/ui/input";
|
||||
import { DataTableColumnVisibilityFilter } from "@/src/components/table/data-table-column-visibility-filter";
|
||||
import { type FilterState } from "@/src/features/filters/types";
|
||||
import { FilterBuilder } from "@/src/features/filters/components/filter-builder";
|
||||
import { type ColumnDefinition } from "@/src/server/api/interfaces/tableDefinition";
|
||||
import { type VisibilityState } from "@tanstack/react-table";
|
||||
import { type LangfuseColumnDef } from "@/src/components/table/types";
|
||||
|
||||
interface SearchConfig {
|
||||
placeholder: string;
|
||||
@@ -11,21 +14,27 @@ interface SearchConfig {
|
||||
currentQuery?: string;
|
||||
}
|
||||
|
||||
interface DataTableToolbarProps {
|
||||
interface DataTableToolbarProps<TData, TValue> {
|
||||
columns: LangfuseColumnDef<TData, TValue>[];
|
||||
filterColumnDefinition: ColumnDefinition[];
|
||||
searchConfig?: SearchConfig;
|
||||
actionButtons?: React.ReactNode;
|
||||
filterState: FilterState;
|
||||
setFilterState: Dispatch<SetStateAction<FilterState>>;
|
||||
columnVisibility?: VisibilityState;
|
||||
setColumnVisibility?: Dispatch<SetStateAction<VisibilityState>>;
|
||||
}
|
||||
|
||||
export function DataTableToolbar({
|
||||
export function DataTableToolbar<TData, TValue>({
|
||||
columns,
|
||||
filterColumnDefinition,
|
||||
searchConfig,
|
||||
actionButtons,
|
||||
filterState,
|
||||
setFilterState,
|
||||
}: DataTableToolbarProps) {
|
||||
columnVisibility,
|
||||
setColumnVisibility,
|
||||
}: DataTableToolbarProps<TData, TValue>) {
|
||||
const [searchString, setSearchString] = useState(
|
||||
searchConfig?.currentQuery ?? "",
|
||||
);
|
||||
@@ -33,33 +42,42 @@ export function DataTableToolbar({
|
||||
return (
|
||||
<div className="my-2 flex max-w-full items-center justify-between overflow-x-auto">
|
||||
<div className="flex flex-1 items-center space-x-2">
|
||||
{searchConfig ? (
|
||||
{searchConfig && (
|
||||
<div className="flex max-w-md items-center space-x-2">
|
||||
<Input
|
||||
autoFocus
|
||||
placeholder={searchConfig.placeholder}
|
||||
value={searchString}
|
||||
className="h-10 w-[200px] lg:w-[350px]"
|
||||
onChange={(event) => {
|
||||
setSearchString(event.currentTarget.value);
|
||||
onChange={(event) => setSearchString(event.currentTarget.value)}
|
||||
onKeyDown={(event) => {
|
||||
if (event.key === "Enter") {
|
||||
searchConfig.updateQuery(searchString);
|
||||
}
|
||||
}}
|
||||
className="h-10 w-[200px] lg:w-[350px]"
|
||||
/>
|
||||
<Button
|
||||
variant="outline"
|
||||
type="submit"
|
||||
onClick={() => searchConfig.updateQuery(searchString)}
|
||||
>
|
||||
Search
|
||||
</Button>
|
||||
</div>
|
||||
) : undefined}
|
||||
)}
|
||||
<FilterBuilder
|
||||
columns={filterColumnDefinition}
|
||||
filterState={filterState}
|
||||
onChange={setFilterState}
|
||||
/>
|
||||
<div className="flex-1" />
|
||||
{actionButtons ? actionButtons : null}
|
||||
{!!columnVisibility && !!setColumnVisibility && (
|
||||
<DataTableColumnVisibilityFilter
|
||||
columns={columns}
|
||||
columnVisibility={columnVisibility}
|
||||
setColumnVisibility={setColumnVisibility}
|
||||
/>
|
||||
)}
|
||||
{actionButtons}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user