Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5fd042c7b1 | ||
|
|
dc37f1a58a | ||
|
|
bc4597e183 | ||
|
|
9bb1caf738 | ||
|
|
3ab4eb6341 | ||
|
|
5944611b51 | ||
|
|
e439c81512 | ||
|
|
799d32fa21 | ||
|
|
7927d0013f | ||
|
|
147262a437 | ||
|
|
c02bb556ca | ||
|
|
1b792d844f | ||
|
|
52a42812b4 | ||
|
|
3b84af8cbf | ||
|
|
1521445939 | ||
|
|
a1e961de09 | ||
|
|
c360cf10d4 | ||
|
|
3e2f3f3d28 | ||
|
|
11e5da2cd8 | ||
|
|
8205595f2c | ||
|
|
101277cab8 | ||
|
|
954e31ca4a | ||
|
|
43c3258135 | ||
|
|
eb8ae94a5d | ||
|
|
33abe53acc | ||
|
|
5e8ec0bd13 | ||
|
|
177f370914 | ||
|
|
5538242649 | ||
|
|
8c8e58e2b8 | ||
|
|
a2c367c1b6 | ||
|
|
da0ac73603 | ||
|
|
e60e74478c | ||
|
|
329f83e844 | ||
|
|
28c035da76 | ||
|
|
4dfeceb9d6 | ||
|
|
df802184a6 | ||
|
|
407d169095 | ||
|
|
e1958e73d4 | ||
|
|
2a08c3395c | ||
|
|
5c405f4ed8 | ||
|
|
25cc29f353 | ||
|
|
b5e1b9916f | ||
|
|
8075996a1c | ||
|
|
80cc85d756 | ||
|
|
45c611bc6d | ||
|
|
1478aa9c2b | ||
|
|
4944408b62 | ||
|
|
f6e81f818b | ||
|
|
b2d5839dcd | ||
|
|
2608878d20 | ||
|
|
62a2857619 | ||
|
|
0c1942c2b3 | ||
|
|
b95c93cdb1 | ||
|
|
3691099c74 | ||
|
|
3f1ae71eec | ||
|
|
668cf3f6c2 | ||
|
|
6221be155d | ||
|
|
3e13d191db | ||
|
|
61297826f4 | ||
|
|
3268ef6488 | ||
|
|
4aba13ec35 | ||
|
|
0f5d5cd9a0 | ||
|
|
78df145e20 | ||
|
|
b35618f1c9 | ||
|
|
a139ada85e | ||
|
|
cdfb0c6b7c | ||
|
|
c0bbf39b29 | ||
|
|
a6900ad3c4 | ||
|
|
21940a0464 | ||
|
|
fe642f8e9e | ||
|
|
c803b0d9c2 | ||
|
|
e7086cf044 | ||
|
|
9d49bbe987 | ||
|
|
f2c792d3dc | ||
|
|
0527fcf097 | ||
|
|
85737221d9 | ||
|
|
ffe95cf427 | ||
|
|
adec1476ea | ||
|
|
c5d7772fe6 | ||
|
|
a88a6b8026 | ||
|
|
5d77e09eed | ||
|
|
85e5ea4661 | ||
|
|
366df5a5c9 | ||
|
|
4441383dfd | ||
|
|
7f7696e9ff | ||
|
|
a829447410 | ||
|
|
53e7ff4e16 | ||
|
|
4a85b4a4f2 | ||
|
|
d2e6dca3d3 | ||
|
|
cdbdc0af67 | ||
|
|
2af9b52213 | ||
|
|
894a3d1b32 | ||
|
|
82f0134368 | ||
|
|
e52ba46b0e | ||
|
|
6aca4954ef | ||
|
|
9d5fa4e2f1 | ||
|
|
c1db242edd | ||
|
|
91a2c63c88 | ||
|
|
f7d9bd7297 | ||
|
|
d94e880520 | ||
|
|
74e9053f85 | ||
|
|
ae5260d808 | ||
|
|
f72f8b7e2c | ||
|
|
43108eccde | ||
|
|
c93c9560e1 | ||
|
|
ddad1afd7b | ||
|
|
2b70ae3742 | ||
|
|
b110fd7932 | ||
|
|
cd537fb571 | ||
|
|
3cbd252980 | ||
|
|
0a99988e6e | ||
|
|
a19d20d973 | ||
|
|
b86eab9e04 | ||
|
|
b36530ff96 | ||
|
|
1019298060 | ||
|
|
99a2ad30ce | ||
|
|
9783a22abf | ||
|
|
68f63b3b90 | ||
|
|
1189d13c14 | ||
|
|
243a784480 | ||
|
|
c89b8c3bab | ||
|
|
b5172219af | ||
|
|
db90f85041 | ||
|
|
db9f90aa91 | ||
|
|
41b19582cc | ||
|
|
766102797b | ||
|
|
773cb1312b | ||
|
|
bb593020d0 | ||
|
|
33ffe62add | ||
|
|
6e1aabb2de | ||
|
|
ab3bdebc57 | ||
|
|
20d00e5ef2 | ||
|
|
e689c1cacb | ||
|
|
37c0737624 | ||
|
|
23bc448088 | ||
|
|
f1bdea98be | ||
|
|
9c00d1905f | ||
|
|
a31d737538 | ||
|
|
7535872222 | ||
|
|
7e7bf99fac | ||
|
|
ac5a7a3171 | ||
|
|
eda5da635d | ||
|
|
dc0cef5dbb | ||
|
|
24607c10ee | ||
|
|
8ddcfa8136 | ||
|
|
1f54043778 | ||
|
|
97bec94093 | ||
|
|
93df11e043 | ||
|
|
327b66c241 | ||
|
|
f3fd52fdbb | ||
|
|
f3727c6209 | ||
|
|
8a3281db03 | ||
|
|
19a21dca5a | ||
|
|
09dcad7427 | ||
|
|
172cbed9b5 | ||
|
|
f7e20a9ff6 | ||
|
|
4b213151d1 | ||
|
|
dd99efe692 | ||
|
|
ff303750cc | ||
|
|
ef6e1e0bf4 | ||
|
|
c0ed93132f | ||
|
|
3503537539 | ||
|
|
b9a3f33e97 | ||
|
|
c2f612d0b8 | ||
|
|
9f492ecec9 | ||
|
|
d87432cce3 | ||
|
|
cf34c91734 | ||
|
|
851b54fa47 | ||
|
|
c858a1800b | ||
|
|
f2405f1561 | ||
|
|
deee890647 | ||
|
|
ebabf0e100 | ||
|
|
e05d4693d2 | ||
|
|
77d49adc00 | ||
|
|
7dc45f0074 | ||
|
|
44d1da3c9c | ||
|
|
564ba4bd68 | ||
|
|
63df788908 | ||
|
|
fb463cbf98 | ||
|
|
cfd99efd48 | ||
|
|
3b2c5f8bf3 | ||
|
|
d08a276b67 | ||
|
|
23bc2cd39c | ||
|
|
a31eda8c68 | ||
|
|
d044ae51d1 | ||
|
|
552578afc3 | ||
|
|
00997db325 | ||
|
|
550464cf0b | ||
|
|
e43568a3d2 | ||
|
|
4869e8a093 | ||
|
|
d9b307ec87 | ||
|
|
3abfbef4c3 | ||
|
|
86aa1eb34f | ||
|
|
59edc9851a | ||
|
|
0e31dcef99 | ||
|
|
a67235ab8a | ||
|
|
eb88c9bdf8 | ||
|
|
77e4791cbe | ||
|
|
5d922dfae7 | ||
|
|
c704c84451 | ||
|
|
6f23488438 | ||
|
|
ca50abb290 | ||
|
|
15067114a2 | ||
|
|
645f14535b | ||
|
|
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 |
+2
-1
@@ -1,3 +1,4 @@
|
||||
[codespell]
|
||||
skip = .git,*.pdf,*.svg,package-lock.json,*.prisma
|
||||
ignore-words-list = afterall
|
||||
ignore-words-list = afterall,vertx
|
||||
|
||||
|
||||
+18
-1
@@ -16,4 +16,21 @@ NEXTAUTH_SECRET="secret"
|
||||
|
||||
# Langfuse experimental features
|
||||
LANGFUSE_ENABLE_EXPERIMENTAL_FEATURES="true"
|
||||
SALT="salt"
|
||||
|
||||
# Salt for API key hashing
|
||||
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.
|
||||
|
||||
# S3 storage
|
||||
S3_ENDPOINT=
|
||||
S3_ACCESS_KEY_ID=
|
||||
S3_SECRET_ACCESS_KEY=
|
||||
S3_BUCKET_NAME=
|
||||
S3_REGION=
|
||||
|
||||
# Set during docker build of application
|
||||
# Used to disable environment verification at build time
|
||||
# DOCKER_BUILD=1
|
||||
+2
-4
@@ -1,16 +1,14 @@
|
||||
# When adding additional environment variables, the schema in "/src/env.mjs"
|
||||
# should be updated accordingly.
|
||||
|
||||
# Prisma
|
||||
# https://www.prisma.io/docs/reference/database-reference/connection-urls#env
|
||||
DIRECT_URL="postgresql://postgres:postgres@db:5432/postgres"
|
||||
DATABASE_URL="postgresql://postgres:postgres@db:5432/postgres"
|
||||
# Next Auth
|
||||
# You can generate a new secret on the command line with:
|
||||
# openssl rand -base64 32
|
||||
# https://next-auth.js.org/configuration/options#secret
|
||||
NEXTAUTH_SECRET="secret"
|
||||
NEXTAUTH_URL="http://localhost:3000"
|
||||
|
||||
# feature flag to enable experimental features locally
|
||||
LANGFUSE_ENABLE_EXPERIMENTAL_FEATURES="false"
|
||||
SALT="salt"
|
||||
SALT="salt"
|
||||
|
||||
+74
-24
@@ -1,31 +1,75 @@
|
||||
# Assuming deployment on Vercel with Postgres database on Supabase
|
||||
NEXT_PUBLIC_LANGFUSE_CLOUD_REGION="US"
|
||||
NEXTAUTH_COOKIE_DOMAIN=".langfuse.com"
|
||||
# More information: https://langfuse.com/docs/deployment/self-host
|
||||
|
||||
# When adding additional environment variables, the schema in "/src/env.mjs"
|
||||
# should be updated accordingly.
|
||||
|
||||
# Prisma
|
||||
|
||||
# https://www.prisma.io/docs/reference/database-reference/connection-urls#env
|
||||
DIRECT_URL="postgresql://postgres:[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"
|
||||
# DATABASE_URL supports pooled connections, but then you need to set DIRECT_URL
|
||||
DATABASE_URL="postgresql://postgres:postgres@db:5432/postgres"
|
||||
# DIRECT_URL="postgresql://postgres:postgres@db:5432/postgres"
|
||||
# SHADOW_DATABASE_URL=
|
||||
|
||||
# Next Auth
|
||||
|
||||
# NEXTAUTH_URL does not need to be set when deploying on Vercel
|
||||
# NEXTAUTH_URL="http://localhost:3000"
|
||||
|
||||
# AUTH_REDIRECT_PROXY_URL used to proxy oauth callbacks on e.g. preview deployments. optional.
|
||||
# AUTH_REDIRECT_PROXY_URL="https://example.com/api/auth"
|
||||
NEXTAUTH_URL="http://localhost:3000"
|
||||
|
||||
# You can generate a new secret on the command line with:
|
||||
# openssl rand -base64 32
|
||||
# https://next-auth.js.org/configuration/options#secret
|
||||
NEXTAUTH_SECRET="secret"
|
||||
SALT="salt"
|
||||
# Sentry; set via Vercel integration
|
||||
# NEXT_PUBLIC_SENTRY_DSN=
|
||||
# NEXT_SENTRY_ORG=
|
||||
# NEXT_SENTRY_PROJECT=
|
||||
# SENTRY_AUTH_TOKEN=
|
||||
|
||||
# Docker only, optional
|
||||
# PORT=3000
|
||||
# HOSTNAME=localhost
|
||||
|
||||
# Default project, optional
|
||||
# LANGFUSE_DEFAULT_PROJECT_ID=
|
||||
# LANGFUSE_DEFAULT_PROJECT_ROLE=
|
||||
|
||||
# Enable experimental features, optional
|
||||
# NEXT_PUBLIC_ENABLE_EXPERIMENTAL_FEATURES
|
||||
|
||||
# Auth, optional configuration
|
||||
# AUTH_DOMAINS_WITH_SSO_ENFORCEMENT=domain1.com,domain2.com
|
||||
# AUTH_DISABLE_USERNAME_PASSWORD=true
|
||||
|
||||
# SSO, each group is optional
|
||||
# AUTH_GOOGLE_CLIENT_ID=
|
||||
# AUTH_GOOGLE_CLIENT_SECRET=
|
||||
# AUTH_GITHUB_CLIENT_ID=
|
||||
# AUTH_GITHUB_CLIENT_SECRET=
|
||||
# AUTH_AZURE_AD_CLIENT_ID=
|
||||
# AUTH_AZURE_AD_CLIENT_SECRET=
|
||||
# AUTH_AZURE_AD_TENANT_ID=
|
||||
|
||||
# Transactional email, optional
|
||||
# Defines the email address to use as the from address.
|
||||
# EMAIL_FROM_ADDRESS=
|
||||
# Defines the connection url for smtp server.
|
||||
# SMTP_CONNECTION_URL=
|
||||
|
||||
# S3 storage, optional, used for exports from the UI
|
||||
# S3_ENDPOINT=
|
||||
# S3_ACCESS_KEY_ID=
|
||||
# S3_SECRET_ACCESS_KEY=
|
||||
# S3_BUCKET_NAME=
|
||||
# S3_REGION=
|
||||
|
||||
# Exports are streamed to S3 in pages to avoid memory issues
|
||||
# The page size can be adjusted if needed to optimize performance
|
||||
# DB_EXPORT_PAGE_SIZE=1000
|
||||
|
||||
|
||||
|
||||
|
||||
### START Langfuse Cloud Config
|
||||
# Used for Langfuse Cloud deployments
|
||||
# Not recommended for self-hosted deployments as these are NOT COVERED BY SEMVER
|
||||
|
||||
# NEXT_PUBLIC_LANGFUSE_CLOUD_REGION="US"
|
||||
# NEXTAUTH_COOKIE_DOMAIN=".langfuse.com"
|
||||
|
||||
# LANGFUSE_TEAM_SLACK_WEBHOOK=
|
||||
# LANGFUSE_NEW_USER_SIGNUP_WEBHOOK=
|
||||
@@ -34,13 +78,19 @@ SALT="salt"
|
||||
# NEXT_PUBLIC_POSTHOG_HOST=
|
||||
# NEXT_PUBLIC_POSTHOG_KEY=
|
||||
|
||||
# Id of demo project to automatically assign new users to
|
||||
# Sentry
|
||||
# NEXT_PUBLIC_SENTRY_DSN=
|
||||
# NEXT_SENTRY_ORG=
|
||||
# NEXT_SENTRY_PROJECT=
|
||||
# SENTRY_AUTH_TOKEN=
|
||||
|
||||
# Betterstack
|
||||
# LANGFUSE_TEAM_BETTERSTACK_TOKEN=
|
||||
|
||||
# Demo project that users can use to try the platform
|
||||
# NEXT_PUBLIC_DEMO_PROJECT_ID=
|
||||
|
||||
# Auth, each group is optional
|
||||
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_DISABLE_USERNAME_PASSWORD=true
|
||||
# Crisp chat
|
||||
# NEXT_PUBLIC_CRISP_WEBSITE_ID=
|
||||
|
||||
### END Langfuse Cloud Config
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
node_modules
|
||||
build
|
||||
coverage
|
||||
@@ -0,0 +1,11 @@
|
||||
body:
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Describe the feature or potential improvement
|
||||
description: Please describe the change as clear and concise as possible. Remember to add context as to why you believe this is needed.
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Additional information
|
||||
description: Add any other information related to the change here. If your idea is related to any issues or discussions, link them here.
|
||||
@@ -0,0 +1,7 @@
|
||||
contact_links:
|
||||
- name: 💡 Feature Request
|
||||
url: https://github.com/orgs/langfuse/discussions/new?category=ideas
|
||||
about: Suggest any ideas you have using our discussion forums.
|
||||
- name: 🤗 Get Help
|
||||
url: https://github.com/orgs/langfuse/discussions/new?category=support
|
||||
about: If you can’t get something to work the way you expect, open a question in 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
|
||||
|
||||
@@ -18,6 +18,9 @@ updates:
|
||||
- dependency-name: "@types/node"
|
||||
- dependency-name: "@trpc/*"
|
||||
groups:
|
||||
sentry:
|
||||
patterns:
|
||||
- "@sentry/*"
|
||||
prisma:
|
||||
patterns:
|
||||
- "prisma"
|
||||
|
||||
@@ -4,6 +4,7 @@ on:
|
||||
push:
|
||||
branches:
|
||||
- "main"
|
||||
merge_group:
|
||||
pull_request:
|
||||
branches:
|
||||
- "main"
|
||||
|
||||
@@ -7,10 +7,15 @@ on:
|
||||
- "main"
|
||||
tags:
|
||||
- "v*"
|
||||
merge_group:
|
||||
pull_request:
|
||||
branches:
|
||||
- "main"
|
||||
|
||||
defaults:
|
||||
run:
|
||||
working-directory: web
|
||||
|
||||
jobs:
|
||||
lint:
|
||||
runs-on: ubuntu-latest
|
||||
@@ -20,40 +25,78 @@ jobs:
|
||||
with:
|
||||
node-version: 20
|
||||
cache: "npm"
|
||||
cache-dependency-path: "**/package-lock.json"
|
||||
- name: install dependencies
|
||||
run: |
|
||||
npm ci
|
||||
- name: Load default env
|
||||
working-directory: .
|
||||
run: |
|
||||
cp .env.dev.example .env
|
||||
- name: lint
|
||||
run: npm run lint
|
||||
|
||||
test-docker-build:
|
||||
defaults:
|
||||
run:
|
||||
working-directory: web
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: Build Web Docker image
|
||||
uses: docker/build-push-action@v4
|
||||
with:
|
||||
context: web
|
||||
push: false
|
||||
|
||||
- name: Build Worker Docker image
|
||||
uses: docker/build-push-action@v4
|
||||
with:
|
||||
context: worker
|
||||
push: false
|
||||
|
||||
tests:
|
||||
defaults:
|
||||
run:
|
||||
working-directory: web
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
matrix:
|
||||
node-version: [18, 20]
|
||||
steps:
|
||||
- name: Set Swap Space
|
||||
uses: pierotofy/set-swap-space@master
|
||||
with:
|
||||
swap-size-gb: 10
|
||||
|
||||
- uses: actions/checkout@v3
|
||||
- name: Use Node.js ${{ matrix.node-version }}
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: ${{ matrix.node-version }}
|
||||
cache: "npm"
|
||||
cache-dependency-path: "**/package-lock.json"
|
||||
|
||||
- name: install dependencies
|
||||
run: |
|
||||
npm ci
|
||||
|
||||
- name: Load default env
|
||||
working-directory: .
|
||||
run: |
|
||||
cp .env.dev.example .env
|
||||
cp .env.dev.example web/.env
|
||||
|
||||
- name: Run, migrate, seed DB
|
||||
- name: Run + migrate
|
||||
working-directory: .
|
||||
run: |
|
||||
docker-compose -f docker-compose.dev.yml up -d
|
||||
sleep 5 # Wait for PostgreSQL to accept connections
|
||||
|
||||
- name: Seed DB
|
||||
run: |
|
||||
npx --yes prisma migrate reset --force --skip-generate
|
||||
|
||||
- name: Build
|
||||
@@ -66,6 +109,9 @@ jobs:
|
||||
run: npm run test
|
||||
|
||||
e2e-tests:
|
||||
defaults:
|
||||
run:
|
||||
working-directory: web
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
@@ -73,19 +119,26 @@ jobs:
|
||||
with:
|
||||
node-version: 20
|
||||
cache: "npm"
|
||||
cache-dependency-path: "**/package-lock.json"
|
||||
|
||||
- name: install dependencies
|
||||
run: |
|
||||
npm ci
|
||||
|
||||
- name: Load default env
|
||||
working-directory: .
|
||||
run: |
|
||||
cp .env.dev.example .env
|
||||
cp .env.dev.example web/.env
|
||||
|
||||
- name: Run, migrate, seed DB
|
||||
- name: Run + migrate
|
||||
working-directory: .
|
||||
run: |
|
||||
docker-compose -f docker-compose.dev.yml up -d
|
||||
sleep 5 # Wait for PostgreSQL to accept connections
|
||||
|
||||
- name: Seed DB
|
||||
run: |
|
||||
npx --yes prisma migrate reset --force --skip-generate
|
||||
|
||||
- name: Build
|
||||
@@ -100,15 +153,17 @@ 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
|
||||
if: ${{ !(contains(needs.*.result, 'failure')) }}
|
||||
run: exit 0
|
||||
working-directory: .
|
||||
- name: Failing deploy
|
||||
if: ${{ contains(needs.*.result, 'failure') }}
|
||||
run: exit 1
|
||||
working-directory: .
|
||||
|
||||
push-docker-image:
|
||||
needs: all-ci-passed
|
||||
@@ -116,10 +171,6 @@ jobs:
|
||||
environment: "protected branches"
|
||||
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 }}
|
||||
permissions:
|
||||
@@ -131,6 +182,7 @@ jobs:
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: 20
|
||||
cache-dependency-path: "**/package-lock.json"
|
||||
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v3
|
||||
@@ -158,12 +210,7 @@ jobs:
|
||||
- name: Build and push Docker image
|
||||
uses: docker/build-push-action@v4
|
||||
with:
|
||||
context: .
|
||||
context: web
|
||||
push: true
|
||||
tags: ${{ steps.meta.outputs.tags }}
|
||||
labels: ${{ steps.meta.outputs.labels }}
|
||||
build-args: |
|
||||
DATABASE_URL=${{ env.DATABASE_URL }}
|
||||
NEXTAUTH_SECRET=${{ env.NEXTAUTH_SECRET }}
|
||||
NEXTAUTH_URL=${{ env.NEXTAUTH_URL }}
|
||||
SALT=${{ env.SALT }}
|
||||
|
||||
+5
-1
@@ -48,4 +48,8 @@ yarn-error.log*
|
||||
/generated/typescript-server
|
||||
|
||||
# openapi spec that is copied during build
|
||||
/public/openapi*.yml
|
||||
/public/openapi*.yml
|
||||
|
||||
|
||||
# vscode
|
||||
.devcontainer
|
||||
Vendored
+4
-5
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"editor.codeActionsOnSave": {
|
||||
"source.fixAll.eslint": true
|
||||
"source.fixAll.eslint": "explicit"
|
||||
},
|
||||
"editor.defaultFormatter": "esbenp.prettier-vscode",
|
||||
"editor.formatOnSave": true,
|
||||
@@ -21,12 +21,11 @@
|
||||
"**/*.{cjs,mjs,ts,tsx,astro,md,mdx,json,yaml,yml}"
|
||||
],
|
||||
"mdx.experimentalLanguageServer": true,
|
||||
"[astro]": {
|
||||
"editor.defaultFormatter": "astro-build.astro-vscode"
|
||||
},
|
||||
"typescript.preferences.importModuleSpecifier": "non-relative",
|
||||
"docwriter.style": "JSDoc",
|
||||
"[prisma]": {
|
||||
"editor.defaultFormatter": "Prisma.prisma"
|
||||
}
|
||||
},
|
||||
"eslint.lintTask.enable": true,
|
||||
"eslint.workingDirectories": ["./web", "./worker"]
|
||||
}
|
||||
|
||||
+131
-17
@@ -2,29 +2,114 @@
|
||||
|
||||
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.
|
||||
The best ways to contribute to Langfuse:
|
||||
|
||||
We gratefully welcome improvements to documentation as well as to code.
|
||||
- Submit and vote on [Ideas](https://github.com/orgs/langfuse/discussions/categories/ideas)
|
||||
- Create and comment on [Issues](https://github.com/langfuse/langfuse/issues)
|
||||
- Open a PR.
|
||||
|
||||
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 ([docs repo](https://github.com/langfuse/langfuse-docs)), the core application (this repo) and the SDKs ([Python](https://github.com/langfuse/langfuse-python), [JS](https://github.com/langfuse/langfuse-js)).
|
||||
|
||||
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:
|
||||
> And if you like the project, but just don't have time to contribute code, 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;
|
||||
> - Submit and vote on [Ideas](https://github.com/orgs/langfuse/discussions/categories/ideas);
|
||||
> - Create and comment on [Issues](https://github.com/langfuse/langfuse/issues);
|
||||
> - Mention the project at local meetups and tell your friends/colleagues.
|
||||
|
||||
## 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.
|
||||
_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. Large changes that were not discussed in an issue may be rejected.
|
||||
|
||||
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).
|
||||
|
||||
## Project Overview
|
||||
|
||||
### Technologies we use
|
||||
|
||||
- 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
|
||||
|
||||
### Architecture Overview
|
||||
|
||||
```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
|
||||
|
||||
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="./web/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
|
||||
@@ -35,25 +120,31 @@ Requirements
|
||||
**Steps**
|
||||
|
||||
1. Fork the the repository and clone it locally
|
||||
2. Install dependencies
|
||||
2. Run the development database
|
||||
|
||||
```bash
|
||||
npm install
|
||||
npm run infra:dev:up
|
||||
```
|
||||
|
||||
3. Run the development database
|
||||
|
||||
```bash
|
||||
docker-compose -f docker-compose.dev.yml up -d
|
||||
```
|
||||
|
||||
4. Create an env file
|
||||
3. Create an env file
|
||||
|
||||
```bash
|
||||
cp .env.dev.example .env
|
||||
```
|
||||
|
||||
5. Run the migrations
|
||||
4. CD into the web directory
|
||||
|
||||
```bash
|
||||
cd web
|
||||
```
|
||||
|
||||
5. Install dependencies
|
||||
|
||||
```bash
|
||||
npm install
|
||||
```
|
||||
|
||||
6. Run the migrations
|
||||
|
||||
```bash
|
||||
npm run db:migrate
|
||||
@@ -63,16 +154,37 @@ Requirements
|
||||
# npm run db:seed:examples
|
||||
```
|
||||
|
||||
6. Start the development server
|
||||
7. 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.
|
||||
|
||||
## 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)
|
||||
@@ -108,3 +220,5 @@ When a new release is tagged on the `main` branch (excluding prereleases), it tr
|
||||
## 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.
|
||||
|
||||
@@ -2,42 +2,43 @@
|
||||
<a href="https://langfuse.com">
|
||||
<h1>🪢 Langfuse</h1>
|
||||
</a>
|
||||
<div>
|
||||
<h3> <a href="https://cloud.langfuse.com">
|
||||
<strong>Sign up</strong>
|
||||
</a> ·
|
||||
<a href="https://langfuse.com/docs/deployment/self-host">
|
||||
<strong>Self Host Langfuse</strong>
|
||||
</a> ·
|
||||
<a href="https://langfuse.com/demo">
|
||||
<strong>Demo Project (live data)</strong>
|
||||
</a>
|
||||
</h3>
|
||||
<h3>
|
||||
Open source observability & analytics for LLM-based applications
|
||||
Langfuse is the open source LLM engineering platform.
|
||||
</h3>
|
||||
<div>
|
||||
<strong>Observability:</strong> Explore and debug complex logs & traces in a visual UI
|
||||
</div>
|
||||
<div>
|
||||
<strong>Analytics:</strong> Measure & improve costs, latency and response quality
|
||||
<div>
|
||||
Debug, analyze and iterate - together
|
||||
</div>
|
||||
</br>
|
||||
<div>
|
||||
<a href="https://discord.gg/7NXusRtqYU">
|
||||
<strong>Join the Langfuse Discord »</strong>
|
||||
</a>
|
||||
</br>
|
||||
<a href="https://langfuse.com">
|
||||
<strong>langfuse.com</strong>
|
||||
</a> ·
|
||||
<a href="https://langfuse.com/docs">
|
||||
<strong>Docs</strong>
|
||||
</a> ·
|
||||
<a href="https://github.com/langfuse/langfuse/issues/new?labels=%F0%9F%90%9E%E2%9D%94+unconfirmed+bug&projects=&template=bug_report.yml&title=bug%3A+">
|
||||
<a href="https://langfuse.com/issue">
|
||||
<strong>Report Bug</strong>
|
||||
</a> ·
|
||||
<a href="https://github.com/langfuse/langfuse/issues/new?assignees=&labels=%E2%9C%A8+enhancement&projects=&template=feature_request.yml&title=feat%3A+">
|
||||
<a href="https://langfuse.com/idea">
|
||||
<strong>Feature Request</strong>
|
||||
</a>
|
||||
</a> ·
|
||||
<a href="https://langfuse.com/changelog">
|
||||
<strong>Changelog</strong>
|
||||
</a> ·
|
||||
<a href="https://langfuse.com/discord">
|
||||
<strong>Discord</strong>
|
||||
</a>
|
||||
</div>
|
||||
</br>
|
||||
<div>
|
||||
<img src="https://img.shields.io/badge/License-MIT-red.svg?style=flat-square" alt="MIT License">
|
||||
<a href="https://discord.gg/7NXusRtqYU"><img src="https://img.shields.io/discord/1111061815649124414?style=flat-square&logo=Discord&logoColor=white&label=Discord&color=%23434EE4" alt="Discord"></a>
|
||||
<a href="https://github.com/langfuse/langfuse"><img src="https://img.shields.io/github/stars/langfuse/langfuse?style=flat-square&logo=GitHub&label=langfuse%2Flangfuse" alt="Github Repo Stars"></a>
|
||||
<a href="https://github.com/langfuse/langfuse/releases"><img src="https://img.shields.io/github/v/release/langfuse/langfuse?include_prereleases&style=flat-square" alt="langfuse releases"></a>
|
||||
<a href="https://github.com/langfuse/langfuse/actions/workflows/pipeline.yml?query=branch:main"><img src="https://img.shields.io/github/actions/workflow/status/langfuse/langfuse/pipeline.yml?style=flat-square&label=All%20tests" alt="CI test status"></a>
|
||||
<a href="https://status.langfuse.com"><img src="https://uptime.betterstack.com/status-badges/v1/monitor/udlc.svg" alt="Uptime Status"/></a>
|
||||
<a href="https://www.ycombinator.com/companies/langfuse"><img src="https://img.shields.io/badge/Y%20Combinator-W23-orange?style=flat-square" alt="Y Combinator W23"></a>
|
||||
<a href="https://github.com/langfuse/langfuse/pkgs/container/langfuse"><img alt="Docker Image" src="https://img.shields.io/badge/docker-langfuse-blue?logo=Docker&logoColor=white&style=flat-square"></a>
|
||||
<a href="https://www.npmjs.com/package/langfuse"><img src="https://img.shields.io/npm/v/langfuse?style=flat-square&label=npm+langfuse" alt="langfuse npm package"></a>
|
||||
@@ -45,61 +46,44 @@
|
||||
</div>
|
||||
</div>
|
||||
</br>
|
||||
</div>
|
||||
</br>
|
||||
|
||||
## What is Langfuse?
|
||||
## Overview
|
||||
|
||||
Langfuse is an open source observability & analytics solution for LLM-based applications. It is mostly geared towards production usage but some users also use it for local development of their LLM applications.
|
||||
### Develop
|
||||
|
||||
Langfuse is focused on applications built on top of LLMs. Many new abstractions and common best practices evolved recently, e.g. agents, chained prompts, embedding-based retrieval, LLM access to REPLs & APIs. These make applications more powerful but also unpredictable for developers as they cannot fully anticipate how changes impact the quality, cost and overall latency of their application. Thus Langfuse helps to monitor and debug these applications.
|
||||
- **Observability:** Instrument your app and start ingesting traces to Langfuse ([Quickstart](https://langfuse.com/docs/get-started), [Integrations](https://langfuse.com/docs/integrations) [Tracing](https://langfuse.com/docs/tracing))
|
||||
- **Langfuse UI:** Inspect and debug complex logs ([Demo](https://langfuse.com/docs/demo), [Tracing](https://langfuse.com/docs/tracing))
|
||||
- **Prompts:** Manage, version and deploy prompts from within Langfuse ([Prompt Management](https://langfuse.com/docs/prompts))
|
||||
|
||||
**Demo (2 min)**
|
||||
### Monitor
|
||||
|
||||
- **Analytics:** Track metrics (cost, latency, quality) and gain insights from dashboards & data exports ([Analytics](https://langfuse.com/docs/analytics))
|
||||
- **Evals:** Collect and calculate scores for your LLM completions ([Scores & Evaluations](https://langfuse.com/docs/scores))
|
||||
- Run model-based evaluations ([Model-based evaluations](https://langfuse.com/docs/scores/model-based-evals))
|
||||
- Collect user feedback ([User Feedback](https://langfuse.com/docs/scores/user-feedback))
|
||||
- Manually score observations in Langfuse ([Manual Scores](https://langfuse.com/docs/scores/manually))
|
||||
|
||||
### Test
|
||||
|
||||
- **Experiments:** Track and test app behaviour before deploying a new version
|
||||
- Datasets let you test expected in and output pairs and benchmark performance before deployiong ([Datasets](https://langfuse.com/docs/datasets))
|
||||
- Track versions and releases in your application ([Experimentation](https://langfuse.com/docs/experimentation), [Prompt Management](https://langfuse.com/docs/prompts))
|
||||
|
||||
### Video: Langfuse in two minutes
|
||||
|
||||
https://github.com/langfuse/langfuse/assets/2834609/6041347a-b517-4a11-8737-93ef8f8af49f
|
||||
|
||||
_Muted by default, enable sound for voice-over_
|
||||
|
||||
Explore demo project in Langfuse here (free account required): https://langfuse.com/demo
|
||||
|
||||
### Observability
|
||||
|
||||
Langfuse offers an admin UI to explore the ingested data.
|
||||
|
||||
- Nested view of LLM app executions; detailed information along the traces on: latency, cost, scores
|
||||
- Segment execution traces by user feedback, to e.g. identify production issues
|
||||
|
||||
### Analytics
|
||||
|
||||
Reporting on
|
||||
|
||||
- Token usage by model
|
||||
- Volume of traces
|
||||
- Scores/evals
|
||||
|
||||
Broken down by
|
||||
|
||||
- Users
|
||||
- Releases
|
||||
- Prompt/chain versions
|
||||
- Prompt/chain types
|
||||
- Time
|
||||
|
||||
→ Expect releases with more ways to analyze the data over the next weeks.
|
||||
|
||||
## Get started
|
||||
|
||||
### Step 1: Run Server
|
||||
### Langfuse Cloud
|
||||
|
||||
#### Langfuse Cloud
|
||||
Managed deployment by the Langfuse team, generous free-tier (hobby plan), no credit card required.
|
||||
|
||||
Managed deployment by the Langfuse team, generous free-tier (hobby plan) available, no credit card required.
|
||||
**[» Langfuse Cloud](https://cloud.langfuse.com)**
|
||||
|
||||
Links: [Create account](https://cloud.langfuse.com), [learn more](https://cloud.langfuse.com)
|
||||
|
||||
#### Localhost
|
||||
|
||||
Requirements: docker, docker compose (e.g. using Docker Desktop)
|
||||
### Localhost (docker)
|
||||
|
||||
```bash
|
||||
# Clone repository
|
||||
@@ -110,143 +94,84 @@ cd langfuse
|
||||
docker compose up -d
|
||||
```
|
||||
|
||||
#### Self-host (Docker)
|
||||
[→ Learn more about deploying locally](https://langfuse.com/docs/deployment/local)
|
||||
|
||||
[→ Instructions](https://langfuse.com/docs/deployment/self-host)
|
||||
### Self-host (docker)
|
||||
|
||||
[](https://railway.app/template/gmbqa_)
|
||||
Langfuse is simple to self-host and keep updated. It currently requires only a single docker container.
|
||||
[→ Self Hosting Instructions](https://langfuse.com/docs/deployment/self-host)
|
||||
|
||||
### Step 2: Data ingestion
|
||||
Templated deployments: [Railway, GCP Cloud Run, AWS Fargate, Kubernetes and others](https://langfuse.com/docs/deployment/self-host#platform-specific-information)
|
||||
|
||||
#### SDKs to instrument application
|
||||
## Get Started
|
||||
|
||||
Fully async, typed SDKs to instrument any LLM application. Currently available for Python & JS/TS.
|
||||
### API Keys
|
||||
|
||||
→ [Guide](https://langfuse.com/docs/guides/sdk-integration) with an example of how the SDK can be used
|
||||
You require a Langfuse public and secret key to get started. Sign up [here](https://cloud.langfuse.com) and find them in your project settings.
|
||||
|
||||
| Package | Description | Links |
|
||||
| --------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------- | -------------------------------------------------------------------------------------------------------------- |
|
||||
| [](https://pypi.python.org/pypi/langfuse) | Python | [docs](https://langfuse.com/docs/integrations/sdk/python), [repo](https://github.com/langfuse/langfuse-python) |
|
||||
| [](https://www.npmjs.com/package/langfuse) | JS/TS: Node >= 18, Edge runtimes | [docs](https://langfuse.com/docs/integrations/sdk/typescript), [repo](https://github.com/langfuse/langfuse-js) |
|
||||
| [](https://www.npmjs.com/package/langfuse-node) | JS/TS: Node <18 | [docs](https://langfuse.com/docs/integrations/sdk/typescript), [repo](https://github.com/langfuse/langfuse-js) |
|
||||
### Ingesting Data · Instrumenting Your Application
|
||||
|
||||
#### Langchain applications
|
||||
Note: We recommend using our fully async, typed [SDKs](https://langfuse.com/docs/sdk) that allow you to instrument any LLM application with any underlying model. They are available in [Python](https://langfuse.com/docs/sdk/python) & [JS/TS](https://langfuse.com/docs/sdk/typescript). The SDKs will always be the most fully featured and stable way to ingest data into Langfuse.
|
||||
|
||||
The Langfuse callback handler automatically instruments Langchain applications. Currently available for Python and JS/TS.
|
||||
You may want to use another integration to get started quickly or implement a use case that we do not yet support. However, we recommend to migrate to the Langfuse SDKs over time to ensure performance and stability.
|
||||
|
||||
**Python**
|
||||
See our the [→ Quickstart](https://langfuse.com/docs/get-started) to get started in integrating Langfuse.
|
||||
|
||||
```shell
|
||||
pip install langfuse
|
||||
```
|
||||
### Integrations
|
||||
|
||||
```python
|
||||
# Initialize Langfuse handler
|
||||
from langfuse.callback import CallbackHandler
|
||||
handler = CallbackHandler(PUBLIC_KEY, SECRET_KEY)
|
||||
| Integration | Supports | Description |
|
||||
| -------------------------------------------------------- | ------------- | ------------------------------------------------------------------------------- |
|
||||
| [**SDK** - _recommended_](https://langfuse.com/docs/sdk) | Python, JS/TS | Manual instrumentation using the SDKs for full flexibility. |
|
||||
| [OpenAI](https://langfuse.com/docs/openai) | Python | Automated instrumentation using drop-in replacement of OpenAI SDK. |
|
||||
| [Langchain](https://langfuse.com/docs/langchain) | Python, JS/TS | Automated instrumentation by passing callback handler to Langchain application. |
|
||||
| [API](https://langfuse.com/docs/api) | | Directly call the public API. OpenAPI spec available. |
|
||||
|
||||
# Setup Langchain
|
||||
from langchain.chains import LLMChain
|
||||
...
|
||||
chain = LLMChain(llm=llm, prompt=prompt)
|
||||
External projects/packages that integrate with Langfuse:
|
||||
|
||||
# Add Langfuse handler as callback
|
||||
chain.run(input="<user_input", callbacks=[handler])
|
||||
```
|
||||
| Name | Description |
|
||||
| ---------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------ |
|
||||
| [LiteLLM](/https://langfuse.comdocs/litellm) | Use any LLM as a drop in replacement for GPT. Use Azure, OpenAI, Cohere, Anthropic, Ollama, VLLM, Sagemaker, HuggingFace, Replicate (100+ LLMs). |
|
||||
| [Flowise](https://langfuse.com/docs/flowise) | JS/TS no-code builder for customized LLM flows. |
|
||||
| [Langflow](https://langfuse.com/docs/langflow) | Python-based UI for LangChain, designed with react-flow to provide an effortless way to experiment and prototype flows. |
|
||||
|
||||
→ [Langchain integration docs for Python](https://langfuse.com/docs/integrations/langchain/python)
|
||||
## Questions and feedback
|
||||
|
||||
**JS/TS**
|
||||
### Ideas and roadmap
|
||||
|
||||
→ [Langchain integration docs for JS/TS](https://langfuse.com/docs/integrations/langchain/typescript)
|
||||
- [GitHub Discussions](https://github.com/orgs/langfuse/discussions)
|
||||
- [Feature Requests](https://langfuse.com/idea)
|
||||
|
||||
#### Add scores/evaluations to traces (optional)
|
||||
### Support and feedback
|
||||
|
||||
Quality/evaluation of traces is tracked via scores ([docs](https://langfuse.com/docs/scores)). Scores are related to traces and optionally to observations. Scores can be added via:
|
||||
In order of preference the best way to communicate with us:
|
||||
|
||||
- **Backend SDKs** (see docs above): `{trace, event, span, generation}.score()`
|
||||
- **API** (see docs below): `POST /api/public/scores`
|
||||
- **Client-side using Web SDK**, e.g. to capture user feedback or other user-based quality metrics:
|
||||
|
||||
```sh
|
||||
npm install langfuse
|
||||
```
|
||||
|
||||
```ts
|
||||
// Client-side (browser)
|
||||
|
||||
import { LangfuseWeb } from "langfuse";
|
||||
|
||||
const langfuseWeb = new LangfuseWeb({
|
||||
publicKey: process.env.LANGFUSE_PUBLIC_KEY,
|
||||
});
|
||||
|
||||
// frontend handler (example: React)
|
||||
export function UserFeedbackComponent(props: { traceId: string }) {
|
||||
const handleUserFeedback = async (value: number) => {
|
||||
await langfuseWeb.score({
|
||||
traceId: props.traceId,
|
||||
name: "user_feedback",
|
||||
value,
|
||||
});
|
||||
};
|
||||
return (
|
||||
<div>
|
||||
<button onClick={() => handleUserFeedback(1)}>👍</button>
|
||||
<button onClick={() => handleUserFeedback(-1)}>👎</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
#### API
|
||||
|
||||
[**Api reference**](https://langfuse.com/docs/integrations/api)
|
||||
|
||||
- POST/PATCH routes to ingest data
|
||||
- GET routes to use data in downstream applications (e.g. embedded analytics)
|
||||
|
||||
## Questions / Feedback
|
||||
|
||||
The maintainers are very active in the Langfuse [Discord](https://langfuse.com/discord) and are happy to answer questions or discuss feedback/ideas regarding the future of the project.
|
||||
- [GitHub Discussions](https://github.com/orgs/langfuse/discussions): Contribute [ideas](https://langfuse.com/idea) [support requests](https://github.com/orgs/langfuse/discussions/categories/support) and [report bugs](https://github.com/langfuse/langfuse/issues/new?labels=%F0%9F%90%9E%E2%9D%94+unconfirmed+bug&projects=&template=bug_report.yml&title=bug%3A+) (preferred as we create a permanent, indexed artifact for other community members)
|
||||
- [Discord](https://langfuse.com/discord): For community support and to chat directly with maintainers
|
||||
- Privately: Email contact at langfuse dot com
|
||||
|
||||
## Contributing to Langfuse
|
||||
|
||||
Join the community [on Discord](https://discord.gg/7NXusRtqYU).
|
||||
|
||||
To contribute, send us a PR, raise a GitHub issue, or email at contributing@langfuse.com
|
||||
|
||||
### Development setup
|
||||
|
||||
See [CONTRIBUTING.md](CONTRIBUTING.md) for details on how to setup a development environment.
|
||||
- Vote on [Ideas](https://github.com/orgs/langfuse/discussions/categories/ideas)
|
||||
- Raise and comment on [Issues](https://github.com/langfuse/langfuse/issues)
|
||||
- Open a PR - see [CONTRIBUTING.md](CONTRIBUTING.md) for details on how to setup a development environment.
|
||||
|
||||
## License
|
||||
|
||||
Langfuse is MIT licensed, except for `ee/` folder. See [LICENSE](LICENSE) and [docs](https://langfuse.com/docs/open-source) for more details.
|
||||
This repository is MIT licensed, except for the `ee/` folder. See [LICENSE](LICENSE) and [docs](https://langfuse.com/docs/open-source) for more details.
|
||||
|
||||
## Misc
|
||||
|
||||
### Upgrade Langfuse (localhost)
|
||||
### GET API to export your data
|
||||
|
||||
```bash
|
||||
# Stop server and db
|
||||
docker compose down
|
||||
[**GET routes**](https://langfuse.com/docs/integrations/api) to use data in downstream applications (e.g. embedded analytics).
|
||||
|
||||
# Pull latest changes
|
||||
git pull
|
||||
docker-compose pull
|
||||
### Security & Privacy
|
||||
|
||||
# Run server and db
|
||||
docker compose up -d
|
||||
```
|
||||
|
||||
### Run Langfuse in CI for integration tests
|
||||
|
||||
Checkout GitHub Actions workflows of [Python SDK](https://github.com/langfuse/langfuse-python/blob/main/.github/workflows/ci.yml) and [JS/TS SDK](https://github.com/langfuse/langfuse-js/blob/main/.github/workflows/ci.yml).
|
||||
We take data security and privacy seriously. Please refer to our [Security and Privacy](https://langfuse.com/security) page for more information.
|
||||
|
||||
### Telemetry
|
||||
|
||||
By default, Langfuse automatically reports basic usage statistics to a centralized server (PostHog).
|
||||
By default, Langfuse automatically reports basic usage statistics of self-hosted instances to a centralized server (PostHog).
|
||||
|
||||
This helps us to:
|
||||
|
||||
|
||||
@@ -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.
|
||||
@@ -1,29 +1,57 @@
|
||||
version: "3.5"
|
||||
|
||||
services:
|
||||
langfuse-server:
|
||||
server:
|
||||
build:
|
||||
dockerfile: Dockerfile
|
||||
args:
|
||||
- DATABASE_URL=postgresql://postgres:postgres@db:5432/postgres
|
||||
- NEXTAUTH_SECRET=mysecret
|
||||
- SALT=mysalt
|
||||
- NEXTAUTH_URL=http:localhost:3000
|
||||
dockerfile: ./Dockerfile
|
||||
context: ./web
|
||||
depends_on:
|
||||
- db
|
||||
- redis
|
||||
ports:
|
||||
- "3000:3000"
|
||||
environment:
|
||||
- NODE_ENV=production
|
||||
- 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}
|
||||
- REDIS_URL=${REDIS_URL:-redis}
|
||||
- REDIS_PORT=${REDIS_PORT:-6379}
|
||||
- REDIS_AUTH=${REDIS_AUTH:-myredissecret}
|
||||
restart: always
|
||||
|
||||
worker:
|
||||
build:
|
||||
dockerfile: ./Dockerfile
|
||||
context: ./worker
|
||||
depends_on:
|
||||
- db
|
||||
- redis
|
||||
- server
|
||||
ports:
|
||||
- "3030:3030"
|
||||
environment:
|
||||
- DATABASE_URL=postgresql://postgres:postgres@db:5432/postgres
|
||||
- NEXTAUTH_SECRET=mysecret
|
||||
- TELEMETRY_ENABLED=${TELEMETRY_ENABLED:-true}
|
||||
- LANGFUSE_ENABLE_EXPERIMENTAL_FEATURES=${LANGFUSE_ENABLE_EXPERIMENTAL_FEATURES:-false}
|
||||
- PORT=${PORT:-3030}
|
||||
- REDIS_URL=${REDIS_URL:-redis}
|
||||
- REDIS_PORT=${REDIS_PORT:-6379}
|
||||
- REDIS_AUTH=${REDIS_AUTH:-myredissecret}
|
||||
restart: always
|
||||
|
||||
redis:
|
||||
image: redis
|
||||
command: >
|
||||
--requirepass ${REDIS_AUTH:-myredissecret}
|
||||
restart: always
|
||||
ports:
|
||||
- 6379:6379
|
||||
|
||||
db:
|
||||
image: postgres
|
||||
restart: always
|
||||
|
||||
@@ -1,6 +1,12 @@
|
||||
version: "3.5"
|
||||
|
||||
services:
|
||||
redis:
|
||||
image: redis
|
||||
restart: always
|
||||
ports:
|
||||
- 6379:6379
|
||||
|
||||
db:
|
||||
image: postgres
|
||||
restart: always
|
||||
|
||||
+1
-2
@@ -8,11 +8,10 @@ services:
|
||||
ports:
|
||||
- "3000:3000"
|
||||
environment:
|
||||
- NODE_ENV=production
|
||||
- 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,18 +0,0 @@
|
||||
#!/bin/sh
|
||||
|
||||
# Set DIRECT_URL to the value of DATABASE_URL, required for migrations
|
||||
export DIRECT_URL=$DATABASE_URL
|
||||
|
||||
# Apply migrations
|
||||
prisma migrate deploy
|
||||
status=$?
|
||||
|
||||
# If migration fails (returns non-zero exit status), exit script with that status
|
||||
if [ $status -ne 0 ]; then
|
||||
echo "Applying database migrations failed. This is mostly caused by the database being unavailable."
|
||||
echo "Exiting..."
|
||||
exit $status
|
||||
fi
|
||||
|
||||
# Start server
|
||||
node server.js
|
||||
@@ -15,7 +15,7 @@ groups:
|
||||
version: 0.0.26
|
||||
output:
|
||||
location: local-file-system
|
||||
path: ../../../generated/openapi-client
|
||||
path: ../../../web/generated/openapi-client
|
||||
config:
|
||||
namespaceExport: Langfuse
|
||||
allowCustomFetcher: true
|
||||
|
||||
@@ -1,4 +1,11 @@
|
||||
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,24 +7,55 @@ 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>
|
||||
TraceWithDetails:
|
||||
tags: optional<list<string>>
|
||||
public:
|
||||
type: optional<boolean>
|
||||
docs: Public traces are accessible via url without login
|
||||
TraceWithDetails: # GET /traces
|
||||
extends: Trace
|
||||
properties:
|
||||
htmlPath:
|
||||
type: string
|
||||
docs: Path of trace in Langfuse UI
|
||||
latency:
|
||||
type: double
|
||||
docs: Latency of trace in seconds
|
||||
totalCost:
|
||||
type: double
|
||||
docs: Cost of trace in USD
|
||||
observations:
|
||||
type: list<string>
|
||||
docs: List of observation ids
|
||||
scores:
|
||||
type: list<string>
|
||||
docs: List of score ids
|
||||
TraceWithFullDetails:
|
||||
TraceWithFullDetails: # GET traces/[traceID]
|
||||
extends: Trace
|
||||
properties:
|
||||
observations: list<Observation>
|
||||
htmlPath:
|
||||
type: string
|
||||
docs: Path of trace in Langfuse UI
|
||||
totalCost:
|
||||
type: double
|
||||
docs: Cost of trace in USD
|
||||
observations: list<ObservationsView>
|
||||
scores: list<Score>
|
||||
Session:
|
||||
properties:
|
||||
id: string
|
||||
createdAt: datetime
|
||||
projectId: string
|
||||
SessionWithTraces:
|
||||
extends: Session
|
||||
properties:
|
||||
traces: list<Trace>
|
||||
Observation:
|
||||
properties:
|
||||
id: string
|
||||
@@ -68,12 +71,33 @@ 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>
|
||||
|
||||
ObservationsView:
|
||||
extends: Observation
|
||||
properties:
|
||||
modelId: optional<string>
|
||||
inputPrice: optional<double>
|
||||
outputPrice: optional<double>
|
||||
totalPrice: optional<double>
|
||||
calculatedInputCost: optional<double>
|
||||
calculatedOutputCost: optional<double>
|
||||
calculatedTotalCost: optional<double>
|
||||
latency: optional<double>
|
||||
|
||||
Usage:
|
||||
properties:
|
||||
input: optional<integer>
|
||||
output: optional<integer>
|
||||
total: optional<integer>
|
||||
unit: optional<ModelUsageUnit>
|
||||
inputCost: optional<double>
|
||||
outputCost: optional<double>
|
||||
totalCost: optional<double>
|
||||
Score:
|
||||
properties:
|
||||
id: string
|
||||
@@ -87,7 +111,6 @@ types:
|
||||
properties:
|
||||
id: string
|
||||
name: string
|
||||
status: DatasetStatus
|
||||
projectId: string
|
||||
createdAt: datetime
|
||||
updatedAt: datetime
|
||||
@@ -121,6 +144,13 @@ types:
|
||||
datasetRunItems: list<DatasetRunItem>
|
||||
|
||||
# Utilities
|
||||
ModelUsageUnit:
|
||||
enum:
|
||||
- CHARACTERS
|
||||
- TOKENS
|
||||
- MILLISECONDS
|
||||
- SECONDS
|
||||
- IMAGES
|
||||
ObservationLevel:
|
||||
enum:
|
||||
- DEBUG
|
||||
@@ -133,11 +163,7 @@ types:
|
||||
- optional<string>
|
||||
- optional<integer>
|
||||
- optional<boolean>
|
||||
LLMUsage:
|
||||
properties:
|
||||
promptTokens: optional<integer>
|
||||
completionTokens: optional<integer>
|
||||
totalTokens: optional<integer>
|
||||
- optional<list<string>>
|
||||
DatasetStatus:
|
||||
enum:
|
||||
- ACTIVE
|
||||
|
||||
@@ -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,207 @@ 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>
|
||||
timestamp: optional<datetime>
|
||||
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,48 @@
|
||||
# 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:
|
||||
daily:
|
||||
docs: Get daily metrics of the Langfuse project
|
||||
method: GET
|
||||
path: /metrics/daily
|
||||
request:
|
||||
name: GetDailyMetricsRequest
|
||||
query-parameters:
|
||||
page: optional<integer>
|
||||
limit: optional<integer>
|
||||
traceName:
|
||||
type: optional<string>
|
||||
docs: Optional filter by the name of the trace
|
||||
userId:
|
||||
type: optional<string>
|
||||
docs: Optional filter by the userId associated with the trace
|
||||
tags:
|
||||
type: optional<string>
|
||||
allow-multiple: true
|
||||
docs: Optional filter for metrics where traces include all of these tags
|
||||
response: DailyMetrics
|
||||
types:
|
||||
DailyMetrics:
|
||||
properties:
|
||||
data:
|
||||
type: list<DailyMetricsDetails>
|
||||
docs: A list of daily metrics, only days with ingested data are included.
|
||||
meta: pagination.MetaResponse
|
||||
DailyMetricsDetails:
|
||||
properties:
|
||||
date: date
|
||||
countTraces: integer
|
||||
totalCost: double
|
||||
usage: list<UsageByModel>
|
||||
UsageByModel:
|
||||
docs: Daily usage of a given model. Usage corresponds to the unit set for the specific model (e.g. tokens).
|
||||
properties:
|
||||
model: string
|
||||
inputUsage: integer
|
||||
outputUsage: integer
|
||||
totalUsage: integer
|
||||
@@ -14,7 +14,7 @@ service:
|
||||
observationId:
|
||||
type: string
|
||||
docs: The unique langfuse identifier of an observation, can be an event, span or generation
|
||||
response: commons.Observation
|
||||
response: commons.ObservationsView
|
||||
getMany:
|
||||
docs: Get a list of observations
|
||||
method: GET
|
||||
@@ -29,10 +29,15 @@ service:
|
||||
type: optional<string>
|
||||
traceId: optional<string>
|
||||
parentObservationId: optional<string>
|
||||
response: Observations
|
||||
response: ObservationsViews
|
||||
|
||||
types:
|
||||
Observations:
|
||||
properties:
|
||||
data: list<commons.Observation>
|
||||
meta: pagination.MetaResponse
|
||||
|
||||
ObservationsViews:
|
||||
properties:
|
||||
data: list<commons.ObservationsView>
|
||||
meta: pagination.MetaResponse
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
# 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
|
||||
config: optional<unknown>
|
||||
|
||||
Prompt:
|
||||
properties:
|
||||
name: string
|
||||
version: integer
|
||||
prompt: string
|
||||
config: unknown
|
||||
@@ -24,6 +24,14 @@ service:
|
||||
userId: optional<string>
|
||||
name: optional<string>
|
||||
response: Scores
|
||||
delete:
|
||||
docs: Delete a score
|
||||
method: DELETE
|
||||
path: /scores/{scoreId}
|
||||
path-parameters:
|
||||
scoreId:
|
||||
type: string
|
||||
docs: The unique langfuse identifier of a score
|
||||
types:
|
||||
CreateScoreRequest:
|
||||
properties:
|
||||
|
||||
@@ -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,19 +26,20 @@ service:
|
||||
limit: optional<integer>
|
||||
userId: optional<string>
|
||||
name: optional<string>
|
||||
orderBy:
|
||||
type: string
|
||||
docs: Format of the string sort_by=timestamp.asc (id, timestamp, name, userId, release, version, public, bookmarked, sessionId)
|
||||
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>
|
||||
meta: pagination.MetaResponse
|
||||
Sort:
|
||||
properties:
|
||||
id: string
|
||||
|
||||
@@ -6,18 +6,19 @@ groups:
|
||||
version: 0.0.28
|
||||
output:
|
||||
location: local-file-system
|
||||
path: ../../../generated/openapi-server
|
||||
path: ../../../web/generated/openapi-server
|
||||
config:
|
||||
namespaceExport: Langfuse
|
||||
allowCustomFetcher: true
|
||||
- name: fernapi/fern-python-sdk
|
||||
version: 0.6.6
|
||||
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.3"
|
||||
"version": "0.16.36"
|
||||
}
|
||||
+1113
-529
File diff suppressed because it is too large
Load Diff
+229
-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 \"timestamp\": \"1994-11-05T13:15:30Z\",\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"
|
||||
@@ -413,6 +332,65 @@
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"_type": "container",
|
||||
"description": null,
|
||||
"name": "Metrics",
|
||||
"item": [
|
||||
{
|
||||
"_type": "endpoint",
|
||||
"name": "Daily",
|
||||
"request": {
|
||||
"description": "Get daily metrics of the Langfuse project",
|
||||
"url": {
|
||||
"raw": "{{baseUrl}}/api/public/metrics/daily?page=&limit=&traceName=&userId=&tags=",
|
||||
"host": [
|
||||
"{{baseUrl}}"
|
||||
],
|
||||
"path": [
|
||||
"api",
|
||||
"public",
|
||||
"metrics",
|
||||
"daily"
|
||||
],
|
||||
"query": [
|
||||
{
|
||||
"key": "page",
|
||||
"value": "",
|
||||
"description": null
|
||||
},
|
||||
{
|
||||
"key": "limit",
|
||||
"value": "",
|
||||
"description": null
|
||||
},
|
||||
{
|
||||
"key": "traceName",
|
||||
"value": "",
|
||||
"description": "Optional filter by the name of the trace"
|
||||
},
|
||||
{
|
||||
"key": "userId",
|
||||
"value": "",
|
||||
"description": "Optional filter by the userId associated with the trace"
|
||||
},
|
||||
{
|
||||
"key": "tags",
|
||||
"value": "",
|
||||
"description": "Optional filter for metrics where traces include all of these tags"
|
||||
}
|
||||
],
|
||||
"variable": []
|
||||
},
|
||||
"header": [],
|
||||
"method": "GET",
|
||||
"auth": null,
|
||||
"body": null
|
||||
},
|
||||
"response": []
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"_type": "container",
|
||||
"description": null,
|
||||
@@ -545,6 +523,82 @@
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"_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 \"config\": \"UNKNOWN\"\n}",
|
||||
"options": {
|
||||
"raw": {
|
||||
"language": "json"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"response": []
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"_type": "container",
|
||||
"description": null,
|
||||
@@ -628,77 +682,75 @@
|
||||
"body": null
|
||||
},
|
||||
"response": []
|
||||
},
|
||||
{
|
||||
"_type": "endpoint",
|
||||
"name": "Delete",
|
||||
"request": {
|
||||
"description": "Delete a score",
|
||||
"url": {
|
||||
"raw": "{{baseUrl}}/api/public/scores/:scoreId",
|
||||
"host": [
|
||||
"{{baseUrl}}"
|
||||
],
|
||||
"path": [
|
||||
"api",
|
||||
"public",
|
||||
"scores",
|
||||
":scoreId"
|
||||
],
|
||||
"query": [],
|
||||
"variable": [
|
||||
{
|
||||
"key": "scoreId",
|
||||
"value": "",
|
||||
"description": "The unique langfuse identifier of a score"
|
||||
}
|
||||
]
|
||||
},
|
||||
"header": [],
|
||||
"method": "DELETE",
|
||||
"auth": null,
|
||||
"body": null
|
||||
},
|
||||
"response": []
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"_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": []
|
||||
}
|
||||
@@ -709,39 +761,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",
|
||||
@@ -780,7 +799,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=&orderBy=&tags=",
|
||||
"host": [
|
||||
"{{baseUrl}}"
|
||||
],
|
||||
@@ -809,6 +828,16 @@
|
||||
"key": "name",
|
||||
"value": "",
|
||||
"description": null
|
||||
},
|
||||
{
|
||||
"key": "orderBy",
|
||||
"value": "",
|
||||
"description": "Format of the string sort_by=timestamp.asc (id, timestamp, name, userId, release, version, public, bookmarked, sessionId)"
|
||||
},
|
||||
{
|
||||
"key": "tags",
|
||||
"value": "",
|
||||
"description": "Only traces that include all of these tags will be returned."
|
||||
}
|
||||
],
|
||||
"variable": []
|
||||
|
||||
Generated
+274
-14563
File diff suppressed because it is too large
Load Diff
+14
-153
@@ -1,159 +1,20 @@
|
||||
{
|
||||
"name": "langfuse-core",
|
||||
"version": "1.8.2",
|
||||
"private": true,
|
||||
"name": "langfuse",
|
||||
"version": "2.7.0",
|
||||
"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",
|
||||
"postinstall": "prisma generate",
|
||||
"lint": "next lint",
|
||||
"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:seed": "npx prisma db seed",
|
||||
"db:seed:examples": "npx prisma db seed -- --environment examples",
|
||||
"release": "release-it"
|
||||
},
|
||||
"prisma": {
|
||||
"seed": "ts-node -r tsconfig-paths/register --compiler-options {\"module\":\"CommonJS\"} prisma/seed.ts"
|
||||
},
|
||||
"dependencies": {
|
||||
"@anthropic-ai/tokenizer": "^0.0.4",
|
||||
"@emotion/react": "^11.11.1",
|
||||
"@emotion/styled": "^11.11.0",
|
||||
"@formkit/auto-animate": "^0.8.1",
|
||||
"@headlessui/react": "^1.7.17",
|
||||
"@heroicons/react": "^2.0.18",
|
||||
"@hookform/resolvers": "^3.3.2",
|
||||
"@next-auth/prisma-adapter": "^1.0.7",
|
||||
"@prisma/client": "^5.6.0",
|
||||
"@radix-ui/react-avatar": "^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",
|
||||
"@radix-ui/react-hover-card": "^1.0.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-select": "^2.0.0",
|
||||
"@radix-ui/react-separator": "^1.0.3",
|
||||
"@radix-ui/react-slider": "^1.1.2",
|
||||
"@radix-ui/react-slot": "^1.0.2",
|
||||
"@radix-ui/react-switch": "^1.0.3",
|
||||
"@radix-ui/react-tabs": "^1.0.4",
|
||||
"@radix-ui/react-toggle": "^1.0.3",
|
||||
"@radix-ui/react-tooltip": "^1.0.7",
|
||||
"@sentry/nextjs": "^7.83.0",
|
||||
"@sentry/profiling-node": "^1.2.6",
|
||||
"@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",
|
||||
"@tremor/react": "^3.11.1",
|
||||
"@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",
|
||||
"cors": "^2.8.5",
|
||||
"date-fns": "^2.30.0",
|
||||
"decimal.js": "^10.4.3",
|
||||
"exponential-backoff": "^3.1.1",
|
||||
"js-yaml": "^4.1.0",
|
||||
"lodash": "^4.17.21",
|
||||
"lucide-react": "^0.294.0",
|
||||
"next": "^14.0.3",
|
||||
"next-auth": "^4.24.5",
|
||||
"next-query-params": "^5.0.0",
|
||||
"next-swagger-doc": "^0.4.0",
|
||||
"posthog-js": "^1.93.3",
|
||||
"posthog-node": "^3.1.3",
|
||||
"react": "18.2.0",
|
||||
"react-day-picker": "^8.9.1",
|
||||
"react-dom": "18.2.0",
|
||||
"react-hook-form": "^7.48.2",
|
||||
"react-icons": "^4.12.0",
|
||||
"react-responsive": "^9.0.2",
|
||||
"react18-json-view": "^0.2.7-canary.1",
|
||||
"superagent": "^8.1.2",
|
||||
"superjson": "2.2.1",
|
||||
"tailwind-merge": "^2.0.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"
|
||||
"infra:dev:up": "docker-compose -f ../docker-compose.dev.yml up -d",
|
||||
"infra:dev:down": "docker-compose -f ../docker-compose.dev.yml down",
|
||||
"dev": "npm run dev --prefix worker/ & npm run dev --prefix web/",
|
||||
"postinstall": "cd worker && npm install && cd ../web && npm install"
|
||||
},
|
||||
"author": "",
|
||||
"license": "MIT",
|
||||
"devDependencies": {
|
||||
"@playwright/test": "^1.40.1",
|
||||
"@release-it/bumper": "^6.0.1",
|
||||
"@testing-library/jest-dom": "^6.1.4",
|
||||
"@types/bcryptjs": "^2.4.6",
|
||||
"@types/cors": "^2.8.17",
|
||||
"@types/eslint": "^8.44.7",
|
||||
"@types/jest": "^29.5.10",
|
||||
"@types/js-yaml": "^4.0.9",
|
||||
"@types/lodash": "^4.14.202",
|
||||
"@types/node": "20.10.0",
|
||||
"@types/react": "^18.2.39",
|
||||
"@types/react-dom": "^18.2.17",
|
||||
"@types/swagger-ui-react": "^4.18.3",
|
||||
"@types/uuid": "^9.0.7",
|
||||
"@typescript-eslint/eslint-plugin": "^6.13.1",
|
||||
"@typescript-eslint/parser": "^6.13.1",
|
||||
"autoprefixer": "^10.4.16",
|
||||
"dotenv-cli": "^7.3.0",
|
||||
"eslint": "^8.54.0",
|
||||
"eslint-config-next": "^14.0.3",
|
||||
"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",
|
||||
"tsconfig-paths": "^4.2.0",
|
||||
"typescript": "^5.3.2"
|
||||
},
|
||||
"ct3aMetadata": {
|
||||
"initVersion": "7.13.0"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"crisp-sdk-web": "^1.0.21"
|
||||
},
|
||||
"release-it": {
|
||||
"git": {
|
||||
"commitMessage": "chore: release v${version}",
|
||||
"tagName": "v${version}"
|
||||
},
|
||||
"github": {
|
||||
"release": true,
|
||||
"web": true,
|
||||
"autoGenerate": true,
|
||||
"releaseName": "v${version}"
|
||||
},
|
||||
"plugins": {
|
||||
"@release-it/bumper": {
|
||||
"out": {
|
||||
"file": "./src/constants/VERSION.ts",
|
||||
"type": "application/typescript"
|
||||
}
|
||||
}
|
||||
}
|
||||
"@types/eslint": "^8.56.2",
|
||||
"@types/jest": "^29.5.12",
|
||||
"@typescript-eslint/eslint-plugin": "^7.0.2",
|
||||
"@typescript-eslint/parser": "^7.0.2",
|
||||
"eslint": "^8.56.0",
|
||||
"prettier": "^3.2.5"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,342 +0,0 @@
|
||||
// This is your Prisma schema file,
|
||||
// learn more about it in the docs: https://pris.ly/d/prisma-schema
|
||||
|
||||
generator client {
|
||||
provider = "prisma-client-js"
|
||||
previewFeatures = ["tracing"]
|
||||
}
|
||||
|
||||
datasource db {
|
||||
provider = "postgresql"
|
||||
url = env("DATABASE_URL")
|
||||
directUrl = env("DIRECT_URL")
|
||||
}
|
||||
|
||||
model Example {
|
||||
id String @id @default(cuid())
|
||||
createdAt DateTime @default(now()) @map("created_at")
|
||||
updatedAt DateTime @updatedAt @map("updated_at")
|
||||
}
|
||||
|
||||
// Necessary for Next auth
|
||||
model Account {
|
||||
id String @id @default(cuid())
|
||||
userId String @map("user_id")
|
||||
type String
|
||||
provider String
|
||||
providerAccountId String
|
||||
refresh_token String? // @db.Text
|
||||
access_token String? // @db.Text
|
||||
expires_at Int?
|
||||
token_type String?
|
||||
scope String?
|
||||
id_token String? // @db.Text
|
||||
session_state String?
|
||||
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
||||
|
||||
@@unique([provider, providerAccountId])
|
||||
@@index([userId])
|
||||
}
|
||||
|
||||
model Session {
|
||||
id String @id @default(cuid())
|
||||
sessionToken String @unique @map("session_token")
|
||||
userId String @map("user_id")
|
||||
expires DateTime
|
||||
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
||||
}
|
||||
|
||||
model User {
|
||||
id String @id @default(cuid())
|
||||
name String?
|
||||
email String? @unique
|
||||
emailVerified DateTime? @map("email_verified")
|
||||
password String?
|
||||
image String?
|
||||
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")
|
||||
|
||||
@@map("users")
|
||||
}
|
||||
|
||||
model VerificationToken {
|
||||
identifier String
|
||||
token String @unique
|
||||
expires DateTime
|
||||
|
||||
@@unique([identifier, token])
|
||||
@@map("verification_tokens")
|
||||
}
|
||||
|
||||
model Project {
|
||||
id String @id @default(cuid())
|
||||
createdAt DateTime @default(now()) @map("created_at")
|
||||
updatedAt DateTime @default(now()) @updatedAt @map("updated_at")
|
||||
name String
|
||||
members Membership[]
|
||||
traces Trace[]
|
||||
observations Observation[]
|
||||
apiKeys ApiKey[]
|
||||
dataset Dataset[]
|
||||
RawEvents Events[]
|
||||
|
||||
@@map("projects")
|
||||
}
|
||||
|
||||
model ApiKey {
|
||||
id String @id @unique @default(cuid())
|
||||
createdAt DateTime @default(now()) @map("created_at")
|
||||
note String?
|
||||
publicKey String @unique @map("public_key")
|
||||
hashedSecretKey String @unique @map("hashed_secret_key")
|
||||
fastHashedSecretKey String? @unique @map("fast_hashed_secret_key")
|
||||
displaySecretKey String @map("display_secret_key")
|
||||
lastUsedAt DateTime? @map("last_used_at")
|
||||
expiresAt DateTime? @map("expires_at")
|
||||
projectId String @map("project_id")
|
||||
project Project @relation(fields: [projectId], references: [id], onDelete: Cascade)
|
||||
|
||||
@@index(projectId)
|
||||
@@index(publicKey)
|
||||
@@index(hashedSecretKey)
|
||||
@@index(fastHashedSecretKey)
|
||||
@@map("api_keys")
|
||||
}
|
||||
|
||||
model Membership {
|
||||
projectId String @map("project_id")
|
||||
project Project @relation(fields: [projectId], references: [id], onDelete: Cascade)
|
||||
userId String @map("user_id")
|
||||
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
||||
role MembershipRole
|
||||
createdAt DateTime @default(now()) @map("created_at")
|
||||
updatedAt DateTime @default(now()) @updatedAt @map("updated_at")
|
||||
|
||||
@@id([projectId, userId])
|
||||
@@index([userId])
|
||||
@@map("memberships")
|
||||
}
|
||||
|
||||
enum MembershipRole {
|
||||
OWNER
|
||||
ADMIN
|
||||
MEMBER
|
||||
VIEWER
|
||||
}
|
||||
|
||||
model Trace {
|
||||
id String @id @default(cuid())
|
||||
externalId String? @map("external_id")
|
||||
timestamp DateTime @default(now())
|
||||
name String?
|
||||
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)
|
||||
bookmarked Boolean @default(false)
|
||||
|
||||
scores Score[]
|
||||
|
||||
@@unique([projectId, externalId])
|
||||
@@index([projectId])
|
||||
@@index([name])
|
||||
@@index([userId])
|
||||
@@index([externalId])
|
||||
@@index(timestamp)
|
||||
@@index(release)
|
||||
@@map("traces")
|
||||
}
|
||||
|
||||
model Observation {
|
||||
id String @id @default(cuid())
|
||||
traceId String? @map("trace_id")
|
||||
projectId String @map("project_id")
|
||||
type ObservationType
|
||||
startTime DateTime @default(now()) @map("start_time")
|
||||
endTime DateTime? @map("end_time")
|
||||
name String?
|
||||
metadata Json?
|
||||
parentObservationId String? @map("parent_observation_id")
|
||||
level ObservationLevel @default(DEFAULT)
|
||||
statusMessage String? @map("status_message")
|
||||
version String?
|
||||
createdAt DateTime @default(now()) @map("created_at")
|
||||
|
||||
// GENERATION ONLY
|
||||
model String?
|
||||
modelParameters Json?
|
||||
input Json?
|
||||
output Json?
|
||||
promptTokens Int @default(0) @map("prompt_tokens")
|
||||
completionTokens Int @default(0) @map("completion_tokens")
|
||||
totalTokens Int @default(0) @map("total_tokens")
|
||||
completionStartTime DateTime? @map("completion_start_time")
|
||||
scores Score[]
|
||||
project Project @relation(fields: [projectId], references: [id], onDelete: Cascade)
|
||||
derivedDatasetItems DatasetItem[]
|
||||
datasetRunItems DatasetRunItems[]
|
||||
|
||||
@@unique([id, projectId])
|
||||
@@index([traceId])
|
||||
@@index([type])
|
||||
@@index(startTime)
|
||||
@@index(createdAt)
|
||||
@@index(projectId)
|
||||
@@index(parentObservationId)
|
||||
@@map("observations")
|
||||
}
|
||||
|
||||
enum ObservationType {
|
||||
SPAN
|
||||
EVENT
|
||||
GENERATION
|
||||
}
|
||||
|
||||
enum ObservationLevel {
|
||||
DEBUG
|
||||
DEFAULT
|
||||
WARNING
|
||||
ERROR
|
||||
}
|
||||
|
||||
model Score {
|
||||
id String @id @default(cuid())
|
||||
timestamp DateTime @default(now())
|
||||
name String
|
||||
value Float
|
||||
comment String?
|
||||
traceId String @map("trace_id")
|
||||
trace Trace @relation(fields: [traceId], references: [id], onDelete: Cascade)
|
||||
observationId String? @map("observation_id")
|
||||
observation Observation? @relation(fields: [observationId], references: [id], onDelete: SetNull)
|
||||
|
||||
@@unique([id, traceId]) // used for upsert
|
||||
@@index([value])
|
||||
@@index([traceId], type: Hash)
|
||||
@@index([observationId], type: Hash)
|
||||
@@map("scores")
|
||||
}
|
||||
|
||||
enum PricingUnit {
|
||||
PER_1000_TOKENS
|
||||
PER_1000_CHARS
|
||||
}
|
||||
|
||||
enum TokenType {
|
||||
PROMPT
|
||||
COMPLETION
|
||||
TOTAL
|
||||
}
|
||||
|
||||
model Pricing {
|
||||
id String @id @default(cuid())
|
||||
modelName String @map("model_name")
|
||||
pricingUnit PricingUnit @default(PER_1000_TOKENS) @map("pricing_unit")
|
||||
price Decimal
|
||||
currency String @default("USD")
|
||||
tokenType TokenType @map("token_type")
|
||||
|
||||
@@map("pricings")
|
||||
}
|
||||
|
||||
model CronJobs {
|
||||
name String @id
|
||||
lastRun DateTime? @map("last_run")
|
||||
jobStartedAt DateTime? @map("job_started_at")
|
||||
state String?
|
||||
|
||||
@@map("cron_jobs")
|
||||
}
|
||||
|
||||
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")
|
||||
updatedAt DateTime @default(now()) @updatedAt @map("updated_at")
|
||||
datasetItems DatasetItem[]
|
||||
datasetRuns DatasetRuns[]
|
||||
|
||||
@@unique([projectId, name])
|
||||
@@index([projectId], type: Hash)
|
||||
@@map("datasets")
|
||||
}
|
||||
|
||||
model DatasetItem {
|
||||
id String @id @default(cuid())
|
||||
status DatasetStatus @default(ACTIVE)
|
||||
input Json
|
||||
expectedOutput Json? @map("expected_output")
|
||||
sourceObservationId String? @map("source_observation_id")
|
||||
sourceObservation Observation? @relation(fields: [sourceObservationId], references: [id], onDelete: SetNull)
|
||||
datasetId String @map("dataset_id")
|
||||
dataset Dataset @relation(fields: [datasetId], references: [id], onDelete: Cascade)
|
||||
createdAt DateTime @default(now()) @map("created_at")
|
||||
updatedAt DateTime @default(now()) @updatedAt @map("updated_at")
|
||||
datasetRunItems DatasetRunItems[]
|
||||
|
||||
@@index([sourceObservationId], type: Hash)
|
||||
@@index([datasetId], type: Hash)
|
||||
@@map("dataset_items")
|
||||
}
|
||||
|
||||
enum DatasetStatus {
|
||||
ACTIVE
|
||||
ARCHIVED
|
||||
}
|
||||
|
||||
model DatasetRuns {
|
||||
id String @id @default(cuid())
|
||||
name String
|
||||
datasetId String @map("dataset_id")
|
||||
dataset Dataset @relation(fields: [datasetId], references: [id], onDelete: Cascade)
|
||||
createdAt DateTime @default(now()) @map("created_at")
|
||||
updatedAt DateTime @default(now()) @updatedAt @map("updated_at")
|
||||
datasetRunItems DatasetRunItems[]
|
||||
|
||||
@@unique([datasetId, name])
|
||||
@@index([datasetId], type: Hash)
|
||||
@@map("dataset_runs")
|
||||
}
|
||||
|
||||
model DatasetRunItems {
|
||||
id String @id @default(cuid())
|
||||
datasetRunId String @map("dataset_run_id")
|
||||
datasetRun DatasetRuns @relation(fields: [datasetRunId], references: [id], onDelete: Cascade)
|
||||
datasetItemId String @map("dataset_item_id")
|
||||
datasetItem DatasetItem @relation(fields: [datasetItemId], references: [id], onDelete: Cascade)
|
||||
observationId String @map("observation_id")
|
||||
observation Observation @relation(fields: [observationId], references: [id], onDelete: Cascade)
|
||||
createdAt DateTime @default(now()) @map("created_at")
|
||||
updatedAt DateTime @default(now()) @updatedAt @map("updated_at")
|
||||
|
||||
@@index([datasetRunId], type: Hash)
|
||||
@@index([datasetItemId], type: Hash)
|
||||
@@index([observationId], type: Hash)
|
||||
@@map("dataset_run_items")
|
||||
}
|
||||
|
||||
model Events {
|
||||
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)
|
||||
data Json
|
||||
headers Json @default("{}")
|
||||
url String?
|
||||
method String?
|
||||
|
||||
@@index(projectId)
|
||||
@@map("events")
|
||||
}
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 312 KiB |
-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.
|
||||
@@ -1,689 +0,0 @@
|
||||
/** @jest-environment node */
|
||||
|
||||
import { makeAPICall, pruneDatabase } from "@/src/__tests__/test-utils";
|
||||
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 () => {
|
||||
const traceId = v4();
|
||||
const generationId = v4();
|
||||
const spanId = v4();
|
||||
const scoreId = 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: 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: { promptTokens: 400, completionTokens: 1000 },
|
||||
},
|
||||
},
|
||||
{
|
||||
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);
|
||||
|
||||
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");
|
||||
|
||||
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(400);
|
||||
expect(dbGeneration?.completionTokens).toEqual(1000);
|
||||
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 upsert threats", 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",
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
expect(responseOne.status).toBe(207);
|
||||
|
||||
const responseTwo = await makeAPICall("POST", "/api/public/ingestion", {
|
||||
batch: [
|
||||
{
|
||||
id: v4(),
|
||||
type: "trace-create",
|
||||
timestamp: new Date().toISOString(),
|
||||
body: {
|
||||
id: traceId,
|
||||
name: "trace-name",
|
||||
userId: "user-2",
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
expect(responseTwo.status).toBe(207);
|
||||
|
||||
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]?.userId).toBe("user-2");
|
||||
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");
|
||||
});
|
||||
|
||||
it("should fail for wrong event formats", async () => {
|
||||
const traceId = v4();
|
||||
|
||||
const responseOne = await makeAPICall("POST", "/api/public/ingestion", {
|
||||
batch: [
|
||||
{
|
||||
id: "invalid-event",
|
||||
},
|
||||
{
|
||||
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);
|
||||
|
||||
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 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();
|
||||
|
||||
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",
|
||||
},
|
||||
},
|
||||
{
|
||||
id: v4(),
|
||||
type: "observation-create",
|
||||
timestamp: new Date().toISOString(),
|
||||
body: {
|
||||
id: generationId,
|
||||
traceId: traceId,
|
||||
type: "GENERATION",
|
||||
name: "generation-name",
|
||||
input: { key: "value" },
|
||||
model: "gpt-3.5",
|
||||
},
|
||||
},
|
||||
{
|
||||
id: v4(),
|
||||
type: "observation-update",
|
||||
timestamp: new Date().toISOString(),
|
||||
body: {
|
||||
id: generationId,
|
||||
type: "GENERATION",
|
||||
output: { key: "this is a great gpt output" },
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
expect(responseOne.status).toBe(207);
|
||||
|
||||
const dbTrace = await prisma.trace.findMany({
|
||||
where: {
|
||||
name: "trace-name",
|
||||
},
|
||||
});
|
||||
|
||||
expect(dbTrace.length).toEqual(1);
|
||||
|
||||
const observation = await prisma.observation.findUnique({
|
||||
where: {
|
||||
id: generationId,
|
||||
},
|
||||
});
|
||||
|
||||
expect(observation?.output).toEqual({
|
||||
key: "this is a great gpt output",
|
||||
});
|
||||
expect(observation?.input).toEqual({ key: "value" });
|
||||
expect(observation?.model).toEqual("gpt-3.5");
|
||||
expect(observation?.output).toEqual({ key: "this is a great gpt output" });
|
||||
expect(observation?.promptTokens).toEqual(5);
|
||||
expect(observation?.completionTokens).toEqual(11);
|
||||
});
|
||||
|
||||
it("should update all token counts if update does not contain model name and events come in wrong order", 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",
|
||||
userId: "user-1",
|
||||
},
|
||||
},
|
||||
{
|
||||
id: v4(),
|
||||
type: "observation-update",
|
||||
timestamp: new Date().toISOString(),
|
||||
body: {
|
||||
id: generationId,
|
||||
type: "GENERATION",
|
||||
output: { key: "this is a great gpt output" },
|
||||
},
|
||||
},
|
||||
{
|
||||
id: v4(),
|
||||
type: "observation-create",
|
||||
timestamp: new Date().toISOString(),
|
||||
body: {
|
||||
id: generationId,
|
||||
traceId: traceId,
|
||||
type: "GENERATION",
|
||||
name: "generation-name",
|
||||
input: { key: "value" },
|
||||
model: "gpt-3.5",
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
expect(responseOne.status).toBe(207);
|
||||
|
||||
const dbTrace = await prisma.trace.findMany({
|
||||
where: {
|
||||
name: "trace-name",
|
||||
},
|
||||
});
|
||||
|
||||
expect(dbTrace.length).toEqual(1);
|
||||
|
||||
const observation = await prisma.observation.findUnique({
|
||||
where: {
|
||||
id: generationId,
|
||||
},
|
||||
});
|
||||
|
||||
expect(observation?.output).toEqual({
|
||||
key: "this is a great gpt output",
|
||||
});
|
||||
expect(observation?.input).toEqual({ key: "value" });
|
||||
expect(observation?.model).toEqual("gpt-3.5");
|
||||
expect(observation?.output).toEqual({ key: "this is a great gpt output" });
|
||||
expect(observation?.promptTokens).toEqual(5);
|
||||
expect(observation?.completionTokens).toEqual(11);
|
||||
});
|
||||
|
||||
it("null does not override set values", 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: "trace-create",
|
||||
timestamp: new Date().toISOString(),
|
||||
body: {
|
||||
id: traceId,
|
||||
name: "trace-name",
|
||||
userId: "user-1",
|
||||
metadata: { key: "value" },
|
||||
release: null,
|
||||
version: null,
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
expect(responseOne.status).toBe(207);
|
||||
|
||||
const dbTrace = await prisma.trace.findMany({
|
||||
where: {
|
||||
name: "trace-name",
|
||||
},
|
||||
});
|
||||
|
||||
expect(dbTrace.length).toEqual(1);
|
||||
expect(dbTrace[0]?.release).toBe("1.0.0");
|
||||
expect(dbTrace[0]?.version).toBe("2.0.0");
|
||||
});
|
||||
|
||||
[
|
||||
{
|
||||
inputs: [{ a: "a" }, { b: "b" }],
|
||||
output: { a: "a", b: "b" },
|
||||
},
|
||||
{
|
||||
inputs: [[{ a: "a" }], [{ b: "b" }]],
|
||||
output: [{ a: "a", b: "b" }],
|
||||
},
|
||||
{
|
||||
inputs: [
|
||||
{
|
||||
a: {
|
||||
"1": 1,
|
||||
},
|
||||
},
|
||||
{
|
||||
b: "b",
|
||||
a: {
|
||||
"2": 2,
|
||||
},
|
||||
},
|
||||
],
|
||||
output: { a: { "1": 1, "2": 2 }, b: "b" },
|
||||
},
|
||||
{
|
||||
inputs: [{ a: "a" }, undefined],
|
||||
output: { a: "a" },
|
||||
},
|
||||
{
|
||||
inputs: [undefined, { b: "b" }],
|
||||
output: { b: "b" },
|
||||
},
|
||||
].forEach(({ inputs, output }) => {
|
||||
it(`merges metadata ${JSON.stringify(inputs)}, ${JSON.stringify(
|
||||
output,
|
||||
)}`, 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",
|
||||
userId: "user-1",
|
||||
metadata: inputs[0],
|
||||
},
|
||||
},
|
||||
{
|
||||
id: v4(),
|
||||
type: "trace-create",
|
||||
timestamp: new Date().toISOString(),
|
||||
body: {
|
||||
id: traceId,
|
||||
name: "trace-name",
|
||||
metadata: inputs[1],
|
||||
},
|
||||
},
|
||||
{
|
||||
id: v4(),
|
||||
type: "observation-create",
|
||||
timestamp: new Date().toISOString(),
|
||||
body: {
|
||||
id: generationId,
|
||||
traceId: traceId,
|
||||
type: "GENERATION",
|
||||
name: "generation-name",
|
||||
metadata: inputs[0],
|
||||
},
|
||||
},
|
||||
{
|
||||
id: v4(),
|
||||
type: "observation-update",
|
||||
timestamp: new Date().toISOString(),
|
||||
body: {
|
||||
id: generationId,
|
||||
traceId: traceId,
|
||||
type: "GENERATION",
|
||||
metadata: inputs[1],
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
expect(responseOne.status).toBe(207);
|
||||
|
||||
const dbTrace = await prisma.trace.findMany({
|
||||
where: {
|
||||
name: "trace-name",
|
||||
},
|
||||
});
|
||||
|
||||
expect(dbTrace.length).toEqual(1);
|
||||
expect(dbTrace[0]?.metadata).toEqual(output);
|
||||
|
||||
const dbGeneration = await prisma.observation.findMany({
|
||||
where: {
|
||||
name: "generation-name",
|
||||
},
|
||||
});
|
||||
|
||||
expect(dbGeneration.length).toEqual(1);
|
||||
expect(dbGeneration[0]?.metadata).toEqual(output);
|
||||
});
|
||||
});
|
||||
|
||||
it("additional fields do not fail the API to support users sending traceidtype Langfuse", 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",
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
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).toBeTruthy();
|
||||
});
|
||||
});
|
||||
@@ -1,72 +0,0 @@
|
||||
/** @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", async () => {
|
||||
await pruneDatabase();
|
||||
|
||||
await makeAPICall("POST", "/api/public/traces", {
|
||||
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: {
|
||||
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");
|
||||
});
|
||||
|
||||
it("should upsert second trace", async () => {
|
||||
await pruneDatabase();
|
||||
|
||||
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: "2.0.0",
|
||||
});
|
||||
|
||||
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",
|
||||
});
|
||||
|
||||
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("5.0.0");
|
||||
expect(dbTrace[0]?.projectId).toBe("7a88fb47-b4e2-43b8-a06c-a5ce950dc53a");
|
||||
});
|
||||
});
|
||||
@@ -1,116 +0,0 @@
|
||||
import { tokenCount } from "@/src/features/ingest/lib/usage";
|
||||
|
||||
describe("Token Count Functions", () => {
|
||||
describe("token count for strings", () => {
|
||||
[
|
||||
{ model: "gpt-3.5", 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 },
|
||||
].forEach(({ model, tokens }) => {
|
||||
it(`should return token count ${tokens} for ${model}`, () => {
|
||||
const result = tokenCount({
|
||||
model: model,
|
||||
text: "Lorem Ipsum is simply dummy text of the printing and typesetting industry. Lorem Ipsum has been the industry's standard dummy text ever since the 1500s, when an unknown printer took a galley of type and scrambled it to make a type specimen book. It has survived not only five centuries, but also the leap into electronic typesetting, remaining essentially unchanged. It was popularised in the 1960s with the release of Letraset sheets containing Lorem Ipsum passages, and more recently with desktop publishing software like Aldus PageMaker including versions of Lorem Ipsum.",
|
||||
});
|
||||
expect(result).toBeDefined();
|
||||
expect(result).toBe(tokens);
|
||||
});
|
||||
});
|
||||
|
||||
it("should return undefined for unknown model", () => {
|
||||
const result = tokenCount({
|
||||
model: "unknown-model",
|
||||
text: "Hello, World!",
|
||||
});
|
||||
expect(result).toBeUndefined();
|
||||
});
|
||||
|
||||
it("should return for invalid text type", () => {
|
||||
const result = tokenCount({ model: "gpt-4", text: 1234 });
|
||||
expect(result).toBe(2);
|
||||
});
|
||||
|
||||
it("should return correct token count for empty string", () => {
|
||||
const result = tokenCount({
|
||||
model: "gpt-4",
|
||||
text: "",
|
||||
});
|
||||
expect(result).toBe(0);
|
||||
});
|
||||
|
||||
it("should return correct token count for very long string", () => {
|
||||
const longString = "A".repeat(10000);
|
||||
const result = tokenCount({
|
||||
model: "gpt-4",
|
||||
text: longString,
|
||||
});
|
||||
expect(result).toBeDefined();
|
||||
expect(result).toBe(1250);
|
||||
});
|
||||
|
||||
it("should return undefined for null text input", () => {
|
||||
const result = tokenCount({
|
||||
model: "gpt-4",
|
||||
text: null,
|
||||
});
|
||||
expect(result).toBeUndefined();
|
||||
});
|
||||
it("should return undefined for undefined text input", () => {
|
||||
const result = tokenCount({
|
||||
model: "gpt-4",
|
||||
text: undefined,
|
||||
});
|
||||
expect(result).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe("token count for chat messages", () => {
|
||||
[
|
||||
{ model: "gpt-4", tokens: 44 },
|
||||
{ model: "gpt-3.5-turbo-16k-0613", tokens: 44 },
|
||||
{ model: "claude-instant-1.2", tokens: 48 },
|
||||
].forEach(({ model, tokens }) => {
|
||||
it(`should return token count ${tokens} for ${model}`, () => {
|
||||
const result = tokenCount({
|
||||
model: model,
|
||||
text: [
|
||||
{ role: "system", content: "You are a helpful assistant." },
|
||||
{ role: "user", content: "Who won the world series in 2020?" },
|
||||
{
|
||||
role: "assistant",
|
||||
content: "The Los Angeles Dodgers won the World Series in 2020.",
|
||||
},
|
||||
],
|
||||
});
|
||||
expect(result).toBeDefined();
|
||||
expect(result).toBe(tokens);
|
||||
});
|
||||
});
|
||||
|
||||
it("should return for non array", () => {
|
||||
const result = tokenCount({
|
||||
model: "gpt-4",
|
||||
text: { role: "Helo world" },
|
||||
});
|
||||
expect(result).toBe(7);
|
||||
});
|
||||
|
||||
it("should return for empty array", () => {
|
||||
const result = tokenCount({
|
||||
model: "gpt-4",
|
||||
text: [],
|
||||
});
|
||||
expect(result).toBeUndefined();
|
||||
});
|
||||
|
||||
it("should return for array of invalid object", () => {
|
||||
const result = tokenCount({
|
||||
model: "gpt-4",
|
||||
text: [{ role: "Helo world" }],
|
||||
});
|
||||
expect(result).toBe(9);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,38 +0,0 @@
|
||||
import { cn } from "@/src/utils/tailwind";
|
||||
|
||||
export const LangfuseIcon = ({
|
||||
size = 32,
|
||||
className,
|
||||
}: {
|
||||
size?: number;
|
||||
className?: string;
|
||||
}) => (
|
||||
// eslint-disable-next-line @next/next/no-img-element
|
||||
<img
|
||||
src="/icon256.png"
|
||||
width={size}
|
||||
height={size}
|
||||
alt="Langfuse Icon"
|
||||
className={className}
|
||||
/>
|
||||
);
|
||||
|
||||
export const LangfuseLogo = ({
|
||||
className,
|
||||
size = "sm",
|
||||
}: {
|
||||
size?: "sm" | "xl";
|
||||
className?: string;
|
||||
}) => (
|
||||
<div className={cn("flex items-center", className)}>
|
||||
<LangfuseIcon size={size === "sm" ? 16 : 20} />
|
||||
<span
|
||||
className={cn(
|
||||
"font-mono font-semibold",
|
||||
size === "sm" ? "ml-2 text-sm" : "ml-3 text-xl",
|
||||
)}
|
||||
>
|
||||
Langfuse
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
@@ -1,60 +0,0 @@
|
||||
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 { useEffect, useState } from "react";
|
||||
|
||||
export function BookmarkTrace({
|
||||
traceId,
|
||||
projectId,
|
||||
isBookmarked,
|
||||
size = "sm",
|
||||
}: {
|
||||
traceId: string;
|
||||
projectId: string;
|
||||
isBookmarked: boolean;
|
||||
size?: "sm" | "xs";
|
||||
}) {
|
||||
const utils = api.useUtils();
|
||||
const [cacheIsBookmarked, setCacheIsBookmarked] = useState<boolean | null>(
|
||||
null,
|
||||
);
|
||||
const localIsBookmarked = cacheIsBookmarked ?? isBookmarked;
|
||||
const hasAccess = useHasAccess({ projectId, scope: "traces:bookmark" });
|
||||
const mutBookmarkTrace = api.traces.bookmark.useMutation({
|
||||
onSuccess: () => {
|
||||
setCacheIsBookmarked(!localIsBookmarked);
|
||||
void utils.traces.invalidate();
|
||||
},
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
setCacheIsBookmarked(null);
|
||||
}, [isBookmarked]);
|
||||
|
||||
const handleBookmarkClick = () => {
|
||||
if (!hasAccess) return;
|
||||
void mutBookmarkTrace.mutateAsync({
|
||||
traceId,
|
||||
projectId,
|
||||
bookmarked: !localIsBookmarked,
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<Button
|
||||
variant="ghost"
|
||||
size={size}
|
||||
onClick={handleBookmarkClick}
|
||||
disabled={!hasAccess}
|
||||
loading={mutBookmarkTrace.isLoading}
|
||||
>
|
||||
<StarIcon
|
||||
className={`h-4 w-4 ${
|
||||
localIsBookmarked ? "fill-current text-yellow-500" : "text-gray-500"
|
||||
}`}
|
||||
/>
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
@@ -1,46 +0,0 @@
|
||||
import {
|
||||
HoverCard,
|
||||
HoverCardContent,
|
||||
HoverCardTrigger,
|
||||
} from "@/src/components/ui/hover-card";
|
||||
import { HelpCircle } from "lucide-react";
|
||||
import Link from "next/link";
|
||||
|
||||
export type DocPopupProps = {
|
||||
description: React.ReactNode;
|
||||
href: string;
|
||||
size?: "sm" | "md" | "lg";
|
||||
};
|
||||
|
||||
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",
|
||||
};
|
||||
|
||||
return (
|
||||
<HoverCard openDelay={200}>
|
||||
<HoverCardTrigger className="mx-2 cursor-pointer" asChild>
|
||||
<Link href={href} rel="noopener" target="_blank">
|
||||
<div className="whitespace-nowrap text-gray-500 sm:pl-0">
|
||||
<HelpCircle className={sizes[size]} />
|
||||
</div>
|
||||
</Link>
|
||||
</HoverCardTrigger>
|
||||
<HoverCardContent>
|
||||
{typeof description === "string" ? (
|
||||
<div className="whitespace-break-spaces text-xs font-normal text-gray-800 sm:pl-0">
|
||||
{description}
|
||||
</div>
|
||||
) : (
|
||||
description
|
||||
)}
|
||||
</HoverCardContent>
|
||||
</HoverCard>
|
||||
);
|
||||
}
|
||||
@@ -1,60 +0,0 @@
|
||||
import { type Flag } from "@/src/features/feature-flags/types";
|
||||
import {
|
||||
Database,
|
||||
LayoutDashboard,
|
||||
LifeBuoy,
|
||||
LineChart,
|
||||
ListTree,
|
||||
type LucideIcon,
|
||||
Settings,
|
||||
TextSelect,
|
||||
UsersIcon,
|
||||
} from "lucide-react";
|
||||
|
||||
export const ROUTES: Array<{
|
||||
name: string;
|
||||
pathname: string;
|
||||
icon: LucideIcon;
|
||||
featureFlag?: Flag;
|
||||
}> = [
|
||||
{
|
||||
name: "Dashboard",
|
||||
pathname: `/project/[projectId]`,
|
||||
icon: LayoutDashboard,
|
||||
},
|
||||
{
|
||||
name: "Traces",
|
||||
pathname: `/project/[projectId]/traces`,
|
||||
icon: ListTree,
|
||||
},
|
||||
{
|
||||
name: "Generations",
|
||||
pathname: `/project/[projectId]/generations`,
|
||||
icon: TextSelect,
|
||||
},
|
||||
{
|
||||
name: "Scores",
|
||||
pathname: `/project/[projectId]/scores`,
|
||||
icon: LineChart,
|
||||
},
|
||||
{
|
||||
name: "Users",
|
||||
pathname: `/project/[projectId]/users`,
|
||||
icon: UsersIcon,
|
||||
},
|
||||
{
|
||||
name: "Datasets",
|
||||
pathname: `/project/[projectId]/datasets`,
|
||||
icon: Database,
|
||||
},
|
||||
{
|
||||
name: "Settings",
|
||||
pathname: "/project/[projectId]/settings",
|
||||
icon: Settings,
|
||||
},
|
||||
{
|
||||
name: "Support",
|
||||
pathname: "/project/[projectId]/support",
|
||||
icon: LifeBuoy,
|
||||
},
|
||||
];
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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,136 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import {
|
||||
type ColumnDef,
|
||||
flexRender,
|
||||
getCoreRowModel,
|
||||
useReactTable,
|
||||
type ColumnFiltersState,
|
||||
getFilteredRowModel,
|
||||
type OnChangeFn,
|
||||
type PaginationState,
|
||||
} from "@tanstack/react-table";
|
||||
import {
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableHead,
|
||||
TableHeader,
|
||||
TableRow,
|
||||
} from "@/src/components/ui/table";
|
||||
import { useState } from "react";
|
||||
import { DataTablePagination } from "@/src/components/table/data-table-pagination";
|
||||
|
||||
interface DataTableProps<TData, TValue> {
|
||||
columns: ColumnDef<TData, TValue>[];
|
||||
data: AsyncTableData<TData[]>;
|
||||
pagination?: {
|
||||
pageCount: number;
|
||||
onChange: OnChangeFn<PaginationState>;
|
||||
state: PaginationState;
|
||||
};
|
||||
}
|
||||
|
||||
export interface AsyncTableData<T> {
|
||||
isLoading: boolean;
|
||||
isError: boolean;
|
||||
data?: T;
|
||||
error?: string;
|
||||
}
|
||||
|
||||
export function DataTable<TData, TValue>({
|
||||
columns,
|
||||
data,
|
||||
pagination,
|
||||
}: DataTableProps<TData, TValue>) {
|
||||
const [columnFilters, setColumnFilters] = useState<ColumnFiltersState>([]);
|
||||
|
||||
const table = useReactTable({
|
||||
data: data.data ?? [],
|
||||
columns,
|
||||
onColumnFiltersChange: setColumnFilters,
|
||||
getFilteredRowModel: getFilteredRowModel(),
|
||||
getCoreRowModel: getCoreRowModel(),
|
||||
manualPagination: pagination !== undefined,
|
||||
pageCount: pagination?.pageCount ?? 0,
|
||||
onPaginationChange: pagination?.onChange,
|
||||
state: {
|
||||
columnFilters,
|
||||
pagination: pagination?.state,
|
||||
},
|
||||
manualFiltering: true,
|
||||
});
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="space-y-4">
|
||||
<div className="rounded-md border">
|
||||
<Table>
|
||||
<TableHeader>
|
||||
{table.getHeaderGroups().map((headerGroup) => (
|
||||
<TableRow key={headerGroup.id}>
|
||||
{headerGroup.headers.map((header) => {
|
||||
return (
|
||||
<TableHead
|
||||
key={header.id}
|
||||
className="whitespace-nowrap p-2"
|
||||
>
|
||||
{header.isPlaceholder
|
||||
? null
|
||||
: flexRender(
|
||||
header.column.columnDef.header,
|
||||
header.getContext(),
|
||||
)}
|
||||
</TableHead>
|
||||
);
|
||||
})}
|
||||
</TableRow>
|
||||
))}
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{data.isLoading || !data.data ? (
|
||||
<TableRow>
|
||||
<TableCell
|
||||
colSpan={columns.length}
|
||||
className="h-24 text-center"
|
||||
>
|
||||
Loading...
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
) : table.getRowModel().rows?.length ? (
|
||||
table.getRowModel().rows.map((row) => (
|
||||
<TableRow
|
||||
key={row.id}
|
||||
data-state={row.getIsSelected() && "selected"}
|
||||
>
|
||||
{row.getVisibleCells().map((cell) => (
|
||||
<TableCell
|
||||
key={cell.id}
|
||||
className="overflow-hidden whitespace-nowrap p-2 text-xs"
|
||||
>
|
||||
{flexRender(
|
||||
cell.column.columnDef.cell,
|
||||
cell.getContext(),
|
||||
)}
|
||||
</TableCell>
|
||||
))}
|
||||
</TableRow>
|
||||
))
|
||||
) : (
|
||||
<TableRow>
|
||||
<TableCell
|
||||
colSpan={columns.length}
|
||||
className="h-24 text-center"
|
||||
>
|
||||
No results.
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
)}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</div>
|
||||
</div>
|
||||
{pagination !== undefined ? <DataTablePagination table={table} /> : null}
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -1,31 +0,0 @@
|
||||
import { useRouter } from "next/router";
|
||||
|
||||
export type TableLinkProps = {
|
||||
path: string;
|
||||
value: string;
|
||||
truncateAt?: number;
|
||||
};
|
||||
|
||||
export default function TableLink({
|
||||
path,
|
||||
value,
|
||||
truncateAt = 7,
|
||||
}: TableLinkProps) {
|
||||
const router = useRouter();
|
||||
|
||||
return (
|
||||
<div>
|
||||
<button
|
||||
key="openTrace"
|
||||
className="rounded bg-indigo-50 px-2 py-1 text-xs font-semibold text-blue-600 shadow-sm hover:bg-indigo-100"
|
||||
onClick={() => {
|
||||
void router.push(path);
|
||||
}}
|
||||
>
|
||||
{value.length > truncateAt
|
||||
? `...${value.substring(value.length - truncateAt)}`
|
||||
: value}
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,6 +0,0 @@
|
||||
import { type LucideIcon } from "lucide-react";
|
||||
|
||||
export type TableRowOptions = {
|
||||
columnId: string;
|
||||
options: { label: string; value: number; icon?: LucideIcon }[];
|
||||
};
|
||||
@@ -1,297 +0,0 @@
|
||||
import { DeleteTrace } from "@/src/components/delete-trace";
|
||||
import { GroupedScoreBadges } from "@/src/components/grouped-score-badge";
|
||||
import { DataTable } from "@/src/components/table/data-table";
|
||||
import { DataTableToolbar } from "@/src/components/table/data-table-toolbar";
|
||||
import TableLink from "@/src/components/table/table-link";
|
||||
import { TokenUsageBadge } from "@/src/components/token-usage-badge";
|
||||
import { useQueryFilterState } from "@/src/features/filters/hooks/useFilterState";
|
||||
import { type FilterState } from "@/src/features/filters/types";
|
||||
import { useDetailPageLists } from "@/src/features/navigate-detail-pages/context";
|
||||
import { tracesTableColsWithOptions } from "@/src/server/api/definitions/tracesTable";
|
||||
import { api } from "@/src/utils/api";
|
||||
import { utcDateOffsetByDays } from "@/src/utils/dates";
|
||||
import { type RouterInput, type RouterOutput } from "@/src/utils/types";
|
||||
import { type Score } from "@prisma/client";
|
||||
import { type ColumnDef } from "@tanstack/react-table";
|
||||
import { useEffect } from "react";
|
||||
import {
|
||||
NumberParam,
|
||||
StringParam,
|
||||
useQueryParam,
|
||||
useQueryParams,
|
||||
withDefault,
|
||||
} from "use-query-params";
|
||||
import { BookmarkTrace } from "@/src/components/bookmark-trace";
|
||||
|
||||
export type TraceTableRow = {
|
||||
bookmarked: boolean;
|
||||
id: string;
|
||||
timestamp: string;
|
||||
name: string;
|
||||
userId: string;
|
||||
metadata?: string;
|
||||
latency?: number;
|
||||
release?: string;
|
||||
version?: string;
|
||||
scores: Score[];
|
||||
usage: {
|
||||
promptTokens: number;
|
||||
completionTokens: number;
|
||||
totalTokens: number;
|
||||
};
|
||||
};
|
||||
|
||||
export type TraceTableProps = {
|
||||
projectId: string;
|
||||
userId?: string;
|
||||
omittedFilter?: string[];
|
||||
};
|
||||
|
||||
export type TraceFilterInput = Omit<RouterInput["traces"]["all"], "projectId">;
|
||||
|
||||
export default function TracesTable({
|
||||
projectId,
|
||||
userId,
|
||||
omittedFilter = [],
|
||||
}: TraceTableProps) {
|
||||
const { setDetailPageList } = useDetailPageLists();
|
||||
const [searchQuery, setSearchQuery] = useQueryParam(
|
||||
"search",
|
||||
withDefault(StringParam, null),
|
||||
);
|
||||
|
||||
const [userFilterState, setUserFilterState] = useQueryFilterState([
|
||||
{
|
||||
column: "timestamp",
|
||||
type: "datetime",
|
||||
operator: ">",
|
||||
value: utcDateOffsetByDays(-14),
|
||||
},
|
||||
]);
|
||||
|
||||
const userIdFilter: FilterState = userId
|
||||
? [
|
||||
{
|
||||
column: "userId",
|
||||
type: "string",
|
||||
operator: "=",
|
||||
value: userId,
|
||||
},
|
||||
]
|
||||
: [];
|
||||
|
||||
const filterState = userFilterState.concat(userIdFilter);
|
||||
|
||||
const [paginationState, setPaginationState] = useQueryParams({
|
||||
pageIndex: withDefault(NumberParam, 0),
|
||||
pageSize: withDefault(NumberParam, 50),
|
||||
});
|
||||
|
||||
const traces = api.traces.all.useQuery({
|
||||
page: paginationState.pageIndex,
|
||||
limit: paginationState.pageSize,
|
||||
projectId,
|
||||
filter: filterState,
|
||||
searchQuery,
|
||||
});
|
||||
const totalCount = traces.data?.slice(1)[0]?.totalCount ?? 0;
|
||||
useEffect(() => {
|
||||
if (traces.isSuccess && traces.data) {
|
||||
setDetailPageList(
|
||||
"traces",
|
||||
traces.data.map((t) => t.id),
|
||||
);
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [traces.isSuccess, traces.data]);
|
||||
|
||||
const traceFilterOptions = api.traces.filterOptions.useQuery({
|
||||
projectId,
|
||||
});
|
||||
|
||||
const convertToTableRow = (
|
||||
trace: RouterOutput["traces"]["all"][0],
|
||||
): TraceTableRow => {
|
||||
return {
|
||||
bookmarked: trace.bookmarked,
|
||||
id: trace.id,
|
||||
timestamp: trace.timestamp.toLocaleString(),
|
||||
name: trace.name ?? "",
|
||||
metadata: JSON.stringify(trace.metadata),
|
||||
release: trace.release ?? undefined,
|
||||
version: trace.version ?? undefined,
|
||||
userId: trace.userId ?? "",
|
||||
scores: trace.scores,
|
||||
latency: trace.latency === null ? undefined : trace.latency,
|
||||
usage: {
|
||||
promptTokens: trace.promptTokens,
|
||||
completionTokens: trace.completionTokens,
|
||||
totalTokens: trace.totalTokens,
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
const columns: ColumnDef<TraceTableRow>[] = [
|
||||
{
|
||||
accessorKey: "bookmarked",
|
||||
header: undefined,
|
||||
cell: ({ row }) => {
|
||||
const isBookmarked = row.getValue("bookmarked");
|
||||
const traceId = row.getValue("id");
|
||||
|
||||
return typeof traceId === "string" &&
|
||||
typeof isBookmarked === "boolean" ? (
|
||||
<BookmarkTrace
|
||||
traceId={traceId}
|
||||
projectId={projectId}
|
||||
isBookmarked={isBookmarked}
|
||||
size="xs"
|
||||
/>
|
||||
) : undefined;
|
||||
},
|
||||
},
|
||||
{
|
||||
accessorKey: "id",
|
||||
header: "ID",
|
||||
cell: ({ row }) => {
|
||||
const value = row.getValue("id");
|
||||
return value && typeof value === "string" ? (
|
||||
<TableLink
|
||||
path={`/project/${projectId}/traces/${value}`}
|
||||
value={value}
|
||||
/>
|
||||
) : undefined;
|
||||
},
|
||||
},
|
||||
{
|
||||
accessorKey: "timestamp",
|
||||
header: "Timestamp",
|
||||
},
|
||||
{
|
||||
accessorKey: "name",
|
||||
header: "Name",
|
||||
},
|
||||
{
|
||||
accessorKey: "userId",
|
||||
enableColumnFilter: !omittedFilter.find((f) => f === "userId"),
|
||||
header: "User ID",
|
||||
cell: ({ row }) => {
|
||||
const value = row.getValue("userId");
|
||||
return value && typeof value === "string" ? (
|
||||
<TableLink
|
||||
path={`/project/${projectId}/users/${value}`}
|
||||
value={value}
|
||||
truncateAt={40}
|
||||
/>
|
||||
) : undefined;
|
||||
},
|
||||
},
|
||||
{
|
||||
accessorKey: "latency",
|
||||
header: "Latency",
|
||||
// add seconds to the end of the latency
|
||||
cell: ({ row }) => {
|
||||
const value: number | undefined = row.getValue("latency");
|
||||
return value !== undefined ? `${value.toFixed(2)} sec` : undefined;
|
||||
},
|
||||
},
|
||||
{
|
||||
accessorKey: "usage",
|
||||
header: "Usage",
|
||||
cell: ({ row }) => {
|
||||
const value: {
|
||||
promptTokens: number;
|
||||
completionTokens: number;
|
||||
totalTokens: number;
|
||||
} = row.getValue("usage");
|
||||
return (
|
||||
<TokenUsageBadge
|
||||
promptTokens={value.promptTokens}
|
||||
completionTokens={value.completionTokens}
|
||||
totalTokens={value.totalTokens}
|
||||
inline
|
||||
/>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
accessorKey: "scores",
|
||||
header: "Scores",
|
||||
enableColumnFilter: !omittedFilter.find((f) => f === "scores"),
|
||||
cell: ({ row }) => {
|
||||
const values: Score[] = row.getValue("scores");
|
||||
return <GroupedScoreBadges scores={values} variant="headings" />;
|
||||
},
|
||||
},
|
||||
{
|
||||
accessorKey: "metadata",
|
||||
header: "Metadata",
|
||||
cell: ({ row }) => {
|
||||
const values: string = row.getValue("metadata");
|
||||
return <div className="flex flex-wrap gap-x-3 gap-y-1">{values}</div>;
|
||||
},
|
||||
},
|
||||
{
|
||||
accessorKey: "version",
|
||||
header: "Version",
|
||||
},
|
||||
{
|
||||
accessorKey: "release",
|
||||
header: "Release",
|
||||
},
|
||||
{
|
||||
accessorKey: "action",
|
||||
header: "Action",
|
||||
cell: ({ row }) => {
|
||||
const traceId = row.getValue("id");
|
||||
return traceId && typeof traceId === "string" ? (
|
||||
<DeleteTrace
|
||||
traceId={traceId}
|
||||
isTableAction={true}
|
||||
projectId={projectId}
|
||||
/>
|
||||
) : undefined;
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<div>
|
||||
<DataTableToolbar
|
||||
filterColumnDefinition={tracesTableColsWithOptions(
|
||||
traceFilterOptions.data,
|
||||
)}
|
||||
searchConfig={{
|
||||
placeholder: "Search by id, name, user id",
|
||||
updateQuery: setSearchQuery,
|
||||
currentQuery: searchQuery ?? undefined,
|
||||
}}
|
||||
filterState={userFilterState}
|
||||
setFilterState={setUserFilterState}
|
||||
/>
|
||||
<DataTable
|
||||
columns={columns}
|
||||
data={
|
||||
traces.isLoading
|
||||
? { isLoading: true, isError: false }
|
||||
: traces.isError
|
||||
? {
|
||||
isLoading: false,
|
||||
isError: true,
|
||||
error: traces.error.message,
|
||||
}
|
||||
: {
|
||||
isLoading: false,
|
||||
isError: false,
|
||||
data: traces.data?.map((t) => convertToTableRow(t)),
|
||||
}
|
||||
}
|
||||
pagination={{
|
||||
pageCount: Math.ceil(totalCount / paginationState.pageSize),
|
||||
onChange: setPaginationState,
|
||||
state: paginationState,
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,200 +0,0 @@
|
||||
import { type NestedObservation } from "@/src/utils/types";
|
||||
import { cn } from "@/src/utils/tailwind";
|
||||
import { type Trace, type Score } from "@prisma/client";
|
||||
import { GroupedScoreBadges } from "@/src/components/grouped-score-badge";
|
||||
import { Fragment } from "react";
|
||||
import { type ObservationReturnType } from "@/src/server/api/routers/traces";
|
||||
|
||||
export const ObservationTree = (props: {
|
||||
observations: ObservationReturnType[];
|
||||
trace: Trace;
|
||||
scores: Score[];
|
||||
currentObservationId: string | undefined;
|
||||
setCurrentObservationId: (id: string | undefined) => void;
|
||||
}) => {
|
||||
const nestedObservations = nestObservations(props.observations);
|
||||
return (
|
||||
<div className="flex flex-col">
|
||||
<ObservationTreeTraceNode
|
||||
trace={props.trace}
|
||||
scores={props.scores}
|
||||
currentObservationId={props.currentObservationId}
|
||||
setCurrentObservationId={props.setCurrentObservationId}
|
||||
/>
|
||||
<ObservationTreeNode
|
||||
observations={nestedObservations}
|
||||
scores={props.scores}
|
||||
indentationLevel={1}
|
||||
currentObservationId={props.currentObservationId}
|
||||
setCurrentObservationId={props.setCurrentObservationId}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
const ObservationTreeTraceNode = (props: {
|
||||
trace: Trace & { latency?: number };
|
||||
scores: Score[];
|
||||
currentObservationId: string | undefined;
|
||||
setCurrentObservationId: (id: string | undefined) => void;
|
||||
}) => (
|
||||
<div
|
||||
className={cn(
|
||||
"group my-1 flex cursor-pointer flex-col gap-1 rounded-sm p-2",
|
||||
props.currentObservationId === undefined ||
|
||||
props.currentObservationId === ""
|
||||
? "bg-gray-100"
|
||||
: "hover:bg-gray-50",
|
||||
)}
|
||||
onClick={() => props.setCurrentObservationId(undefined)}
|
||||
>
|
||||
<div className="flex gap-2">
|
||||
<span className={cn("rounded-sm bg-gray-200 p-1 text-xs")}>TRACE</span>
|
||||
<span>{props.trace.name}</span>
|
||||
</div>
|
||||
|
||||
{props.trace.latency ? (
|
||||
<div className="flex gap-2">
|
||||
<span className="text-xs text-gray-500">
|
||||
{props.trace.latency.toFixed(2)} sec
|
||||
</span>
|
||||
</div>
|
||||
) : null}
|
||||
<div className="flex flex-wrap gap-1">
|
||||
{props.scores.find((s) => s.observationId === null) ? (
|
||||
<GroupedScoreBadges
|
||||
scores={props.scores.filter((s) => s.observationId === null)}
|
||||
/>
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
const ObservationTreeNode = (props: {
|
||||
observations: NestedObservation[];
|
||||
scores: Score[];
|
||||
indentationLevel: number;
|
||||
currentObservationId: string | undefined;
|
||||
setCurrentObservationId: (id: string | undefined) => void;
|
||||
}) => (
|
||||
<>
|
||||
{props.observations
|
||||
.sort((a, b) => a.startTime.getTime() - b.startTime.getTime())
|
||||
.map((observation) => (
|
||||
<Fragment key={observation.id}>
|
||||
<div className="flex">
|
||||
{Array.from({ length: props.indentationLevel }, (_, i) => (
|
||||
<div className="mx-2 border-r lg:mr-4" key={i} />
|
||||
))}
|
||||
<div
|
||||
className={cn(
|
||||
"group my-1 flex flex-1 cursor-pointer flex-col gap-1 rounded-sm p-2 ",
|
||||
props.currentObservationId === observation.id
|
||||
? "bg-gray-100"
|
||||
: "hover:bg-gray-50",
|
||||
)}
|
||||
onClick={() => props.setCurrentObservationId(observation.id)}
|
||||
>
|
||||
<div className="flex gap-2">
|
||||
<span
|
||||
className={cn(
|
||||
"self-start rounded-sm bg-gray-200 p-1 text-xs",
|
||||
)}
|
||||
>
|
||||
{observation.type}
|
||||
</span>
|
||||
<span className="line-clamp-1">{observation.name}</span>
|
||||
</div>
|
||||
<div className="flex gap-2">
|
||||
{observation.endTime ? (
|
||||
<span className="text-xs text-gray-500">
|
||||
{(
|
||||
(observation.endTime.getTime() -
|
||||
observation.startTime.getTime()) /
|
||||
1000
|
||||
).toFixed(2)}{" "}
|
||||
sec
|
||||
</span>
|
||||
) : null}
|
||||
{observation.promptTokens ||
|
||||
observation.completionTokens ||
|
||||
observation.totalTokens ? (
|
||||
<span className="text-xs text-gray-500">
|
||||
{observation.promptTokens} → {observation.completionTokens}{" "}
|
||||
(∑ {observation.totalTokens})
|
||||
</span>
|
||||
) : null}
|
||||
</div>
|
||||
{observation.level !== "DEFAULT" ? (
|
||||
<div className="flex">
|
||||
<span
|
||||
className={cn(
|
||||
"rounded-sm text-xs",
|
||||
LevelColor[observation.level].bg,
|
||||
LevelColor[observation.level].text,
|
||||
)}
|
||||
>
|
||||
{observation.level}
|
||||
</span>
|
||||
</div>
|
||||
) : null}
|
||||
{props.scores.find((s) => s.observationId === observation.id) ? (
|
||||
<div className="flex flex-wrap gap-1">
|
||||
<GroupedScoreBadges
|
||||
scores={props.scores.filter(
|
||||
(s) => s.observationId === observation.id,
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
<ObservationTreeNode
|
||||
observations={observation.children}
|
||||
scores={props.scores}
|
||||
indentationLevel={props.indentationLevel + 1}
|
||||
currentObservationId={props.currentObservationId}
|
||||
setCurrentObservationId={props.setCurrentObservationId}
|
||||
/>
|
||||
</Fragment>
|
||||
))}
|
||||
</>
|
||||
);
|
||||
|
||||
const LevelColor = {
|
||||
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" },
|
||||
};
|
||||
|
||||
export function nestObservations(
|
||||
list: ObservationReturnType[],
|
||||
): NestedObservation[] {
|
||||
if (list.length === 0) return [];
|
||||
|
||||
// Step 1: Create a map where the keys are object IDs, and the values are
|
||||
// the corresponding objects with an added 'children' property.
|
||||
const map = new Map<string, NestedObservation>();
|
||||
for (const obj of list) {
|
||||
map.set(obj.id, { ...obj, children: [] });
|
||||
}
|
||||
|
||||
// Step 2: Create another map for the roots of all trees.
|
||||
const roots = new Map<string, NestedObservation>();
|
||||
|
||||
// Step 3: Populate the 'children' arrays and root map.
|
||||
for (const obj of map.values()) {
|
||||
if (obj.parentObservationId) {
|
||||
const parent = map.get(obj.parentObservationId);
|
||||
if (parent) {
|
||||
parent.children.push(obj);
|
||||
}
|
||||
} else {
|
||||
roots.set(obj.id, obj);
|
||||
}
|
||||
}
|
||||
|
||||
// TODO sum token amounts per level
|
||||
|
||||
// Step 4: Return the roots.
|
||||
return Array.from(roots.values());
|
||||
}
|
||||
@@ -1,148 +0,0 @@
|
||||
import { type Trace, type Score } from "@prisma/client";
|
||||
import { ObservationTree } from "./ObservationTree";
|
||||
import { ObservationPreview } from "./ObservationPreview";
|
||||
import { TracePreview } from "./TracePreview";
|
||||
|
||||
import Header from "@/src/components/layouts/header";
|
||||
import { Badge } from "@/src/components/ui/badge";
|
||||
import { TraceAggUsageBadge } from "@/src/components/token-usage-badge";
|
||||
import Decimal from "decimal.js";
|
||||
import { StringParam, useQueryParam } from "use-query-params";
|
||||
import { PublishTraceSwitch } from "@/src/features/public-traces/components/PublishTraceSwitch";
|
||||
import { DetailPageNav } from "@/src/features/navigate-detail-pages/DetailPageNav";
|
||||
import { useRouter } from "next/router";
|
||||
import { type ObservationReturnType } from "@/src/server/api/routers/traces";
|
||||
import { api } from "@/src/utils/api";
|
||||
import { DeleteTrace } from "@/src/components/delete-trace";
|
||||
import { BookmarkTrace } from "@/src/components/bookmark-trace";
|
||||
|
||||
export function Trace(props: {
|
||||
observations: Array<ObservationReturnType>;
|
||||
trace: Trace;
|
||||
scores: Score[];
|
||||
projectId: string;
|
||||
}) {
|
||||
const [currentObservationId, setCurrentObservationId] = useQueryParam(
|
||||
"observation",
|
||||
StringParam,
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="grid h-full gap-4 md:grid-cols-3">
|
||||
<div className="col-span-1 lg:hidden">
|
||||
<ObservationTree
|
||||
observations={props.observations}
|
||||
trace={props.trace}
|
||||
scores={props.scores}
|
||||
currentObservationId={currentObservationId ?? undefined}
|
||||
setCurrentObservationId={setCurrentObservationId}
|
||||
/>
|
||||
</div>
|
||||
<div className="col-span-2 h-full overflow-y-auto">
|
||||
{currentObservationId === undefined ||
|
||||
currentObservationId === "" ||
|
||||
currentObservationId === null ? (
|
||||
<TracePreview
|
||||
trace={props.trace}
|
||||
observations={props.observations}
|
||||
scores={props.scores}
|
||||
/>
|
||||
) : (
|
||||
<ObservationPreview
|
||||
observations={props.observations}
|
||||
scores={props.scores}
|
||||
projectId={props.projectId}
|
||||
currentObservationId={currentObservationId}
|
||||
traceId={props.trace.id}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
<div className="col-span-1 hidden h-full overflow-y-auto lg:block">
|
||||
<ObservationTree
|
||||
observations={props.observations}
|
||||
trace={props.trace}
|
||||
scores={props.scores}
|
||||
currentObservationId={currentObservationId ?? undefined}
|
||||
setCurrentObservationId={setCurrentObservationId}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function TracePage({ traceId }: { traceId: string }) {
|
||||
const router = useRouter();
|
||||
const trace = api.traces.byId.useQuery({ traceId });
|
||||
const totalCost = trace.data?.observations.reduce(
|
||||
(acc, o) => {
|
||||
if (!o.price) return acc;
|
||||
|
||||
return acc
|
||||
? acc.plus(o.price ? o.price : new Decimal(0))
|
||||
: new Decimal(0).plus(o.price ? o.price : new Decimal(0));
|
||||
},
|
||||
undefined as Decimal | undefined,
|
||||
);
|
||||
|
||||
if (!trace.data) return <div>loading...</div>;
|
||||
|
||||
return (
|
||||
<div className="flex flex-col overflow-hidden xl:container lg:h-[calc(100vh-100px)] xl:h-[calc(100vh-50px)]">
|
||||
<Header
|
||||
title="Trace Detail"
|
||||
breadcrumb={[
|
||||
{
|
||||
name: "Traces",
|
||||
href: `/project/${router.query.projectId as string}/traces`,
|
||||
},
|
||||
{ name: traceId },
|
||||
]}
|
||||
actionButtons={
|
||||
<>
|
||||
<BookmarkTrace
|
||||
traceId={trace.data.id}
|
||||
projectId={trace.data.projectId}
|
||||
isBookmarked={trace.data.bookmarked}
|
||||
/>
|
||||
<PublishTraceSwitch
|
||||
traceId={trace.data.id}
|
||||
projectId={trace.data.projectId}
|
||||
isPublic={trace.data.public}
|
||||
/>
|
||||
<DetailPageNav
|
||||
currentId={traceId}
|
||||
path={(id) =>
|
||||
`/project/${router.query.projectId as string}/traces/${id}`
|
||||
}
|
||||
listKey="traces"
|
||||
/>
|
||||
<DeleteTrace
|
||||
traceId={trace.data.id}
|
||||
projectId={trace.data.projectId}
|
||||
/>
|
||||
</>
|
||||
}
|
||||
/>
|
||||
<div className="flex gap-2">
|
||||
{trace.data.userId ? (
|
||||
<Badge variant="outline">User ID: {trace.data.userId}</Badge>
|
||||
) : null}
|
||||
<TraceAggUsageBadge observations={trace.data.observations ?? []} />
|
||||
{totalCost ? (
|
||||
<Badge variant="outline">
|
||||
Total cost: {totalCost.toString()} USD
|
||||
</Badge>
|
||||
) : undefined}
|
||||
</div>
|
||||
<div className="mt-5 flex-1 overflow-hidden border-t pt-5">
|
||||
<Trace
|
||||
key={trace.data.id}
|
||||
trace={trace.data}
|
||||
scores={trace.data.scores}
|
||||
projectId={trace.data.projectId}
|
||||
observations={trace.data.observations ?? []}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,23 +0,0 @@
|
||||
import { cn } from "@/src/utils/tailwind";
|
||||
import * as React from "react";
|
||||
|
||||
export type InputProps = React.InputHTMLAttributes<HTMLInputElement>;
|
||||
|
||||
const Input = React.forwardRef<HTMLInputElement, InputProps>(
|
||||
({ className, type, ...props }, ref) => {
|
||||
return (
|
||||
<input
|
||||
type={type}
|
||||
className={cn(
|
||||
"flex h-10 w-full rounded-md border border-input bg-transparent px-3 py-2 text-sm ring-offset-background file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50",
|
||||
className,
|
||||
)}
|
||||
ref={ref}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
},
|
||||
);
|
||||
Input.displayName = "Input";
|
||||
|
||||
export { Input };
|
||||
@@ -1 +0,0 @@
|
||||
export const VERSION = "v1.8.2";
|
||||
@@ -1,18 +0,0 @@
|
||||
import { type Key, type ReactNode } from "react";
|
||||
import { cn } from "@/src/utils/tailwind";
|
||||
|
||||
export const RightAlignedCell = ({
|
||||
children,
|
||||
className,
|
||||
key,
|
||||
}: {
|
||||
children: ReactNode;
|
||||
className?: string;
|
||||
key?: Key;
|
||||
}) => {
|
||||
return (
|
||||
<div className={cn("text-right", className)} key={key}>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -1,88 +0,0 @@
|
||||
import {
|
||||
dateTimeAggregationOptions,
|
||||
dateTimeAggregationSettings,
|
||||
} from "@/src/features/dashboard/lib/timeseries-aggregation";
|
||||
import { z } from "zod";
|
||||
|
||||
import {
|
||||
createTRPCRouter,
|
||||
protectedProjectProcedure,
|
||||
} from "@/src/server/api/trpc";
|
||||
import { executeQuery } from "@/src/server/api/services/query-builder";
|
||||
import { sqlInterface } from "@/src/server/api/services/sqlInterface";
|
||||
|
||||
export const dashboardRouter = createTRPCRouter({
|
||||
chart: protectedProjectProcedure
|
||||
.input(sqlInterface.extend({ projectId: z.string() }))
|
||||
.query(async ({ input, ctx }) => {
|
||||
return await executeQuery(ctx.prisma, input.projectId, input);
|
||||
}),
|
||||
|
||||
scores: protectedProjectProcedure
|
||||
.input(
|
||||
z.object({
|
||||
projectId: z.string(),
|
||||
agg: z.enum(dateTimeAggregationOptions),
|
||||
}),
|
||||
)
|
||||
.query(async ({ input, ctx }) => {
|
||||
// queryRawUnsafe to add input.agg to the WHERE clause
|
||||
const output = await ctx.prisma.$queryRawUnsafe<
|
||||
{
|
||||
date_trunc: Date;
|
||||
values: {
|
||||
[key: string]: number;
|
||||
} | null;
|
||||
}[]
|
||||
>(`
|
||||
WITH timeseries AS (
|
||||
SELECT
|
||||
date_trunc('${
|
||||
dateTimeAggregationSettings[input.agg].date_trunc
|
||||
}', dt) as date_trunc
|
||||
FROM generate_series(
|
||||
NOW() - INTERVAL '${input.agg}', NOW(), INTERVAL '1 minute'
|
||||
) as dt
|
||||
WHERE dt > NOW() - INTERVAL '${input.agg}'
|
||||
GROUP BY 1
|
||||
),
|
||||
metrics AS (
|
||||
SELECT
|
||||
date_trunc('${
|
||||
dateTimeAggregationSettings[input.agg].date_trunc
|
||||
}', scores.timestamp) as date_trunc,
|
||||
scores.name as metric_name,
|
||||
AVG(value) as avg_value
|
||||
FROM scores
|
||||
LEFT JOIN traces ON scores.trace_id = traces.id
|
||||
WHERE scores.timestamp > NOW() - INTERVAL '${input.agg}'
|
||||
AND traces.project_id = '${input.projectId}'
|
||||
GROUP BY 1,2
|
||||
),
|
||||
json_metrics AS (
|
||||
SELECT
|
||||
date_trunc,
|
||||
jsonb_object_agg(metric_name, avg_value) as values
|
||||
FROM metrics
|
||||
GROUP BY 1
|
||||
)
|
||||
SELECT
|
||||
timeseries.date_trunc,
|
||||
json_metrics.values as values
|
||||
FROM timeseries
|
||||
LEFT JOIN json_metrics ON timeseries.date_trunc = json_metrics.date_trunc
|
||||
ORDER BY 1
|
||||
`);
|
||||
|
||||
return output.map((row) => ({
|
||||
...row,
|
||||
values: row.values
|
||||
? Object.entries(row.values).map(([label, value]) => ({
|
||||
label: "avg_" + label,
|
||||
value,
|
||||
}))
|
||||
: [],
|
||||
ts: row.date_trunc.getTime(),
|
||||
}));
|
||||
}),
|
||||
});
|
||||
@@ -1,121 +0,0 @@
|
||||
import { type FilterState } from "@/src/features/filters/types";
|
||||
import { filterOperators } from "@/src/server/api/interfaces/filters";
|
||||
import { type ColumnDefinition } from "@/src/server/api/interfaces/tableDefinition";
|
||||
import { Prisma } from "@prisma/client";
|
||||
|
||||
const operatorReplacements = {
|
||||
"any of": "IN",
|
||||
"none of": "NOT IN",
|
||||
contains: "ILIKE",
|
||||
"does not contain": "NOT ILIKE",
|
||||
"starts with": "ILIKE",
|
||||
"ends with": "ILIKE",
|
||||
};
|
||||
|
||||
export function filterToPrismaSql(
|
||||
filters: FilterState,
|
||||
tableColumns: ColumnDefinition[],
|
||||
): Prisma.Sql {
|
||||
const statements = filters.map((filter) => {
|
||||
// Get column definition to map column to internal name, e.g. "t.id"
|
||||
const col = tableColumns.find((c) => c.name === filter.column);
|
||||
if (!col) {
|
||||
console.error("Invalid filter column", filter.column);
|
||||
throw new Error("Invalid filter column: " + filter.column);
|
||||
}
|
||||
|
||||
const colPrisma = Prisma.raw(col.internal);
|
||||
const operatorPrisma =
|
||||
filter.operator in operatorReplacements
|
||||
? Prisma.raw(
|
||||
operatorReplacements[
|
||||
filter.operator as keyof typeof operatorReplacements
|
||||
],
|
||||
)
|
||||
: Prisma.raw(filter.operator); //checked by zod
|
||||
|
||||
// Get prisma value
|
||||
let valuePrisma: Prisma.Sql;
|
||||
switch (filter.type) {
|
||||
case "datetime":
|
||||
valuePrisma = Prisma.sql`${
|
||||
filter.value
|
||||
.toISOString()
|
||||
.split(".")[0]! // remove milliseconds
|
||||
.replace("T", " ") // to Postgres datetime
|
||||
}::TIMESTAMP`;
|
||||
break;
|
||||
case "number":
|
||||
case "numberObject":
|
||||
valuePrisma = Prisma.sql`${filter.value}::DOUBLE PRECISION`;
|
||||
break;
|
||||
case "string":
|
||||
case "stringObject":
|
||||
valuePrisma = Prisma.sql`${filter.value}`;
|
||||
break;
|
||||
case "stringOptions":
|
||||
valuePrisma = Prisma.sql`(${Prisma.join(
|
||||
filter.value.map((v) => Prisma.sql`${v}`),
|
||||
)})`;
|
||||
break;
|
||||
case "boolean":
|
||||
valuePrisma = Prisma.sql`${filter.value}`;
|
||||
break;
|
||||
}
|
||||
const jsonKeyPrisma =
|
||||
filter.type === "stringObject" || filter.type === "numberObject"
|
||||
? Prisma.sql`->>${filter.key}`
|
||||
: Prisma.empty;
|
||||
const [cast1, cast2] =
|
||||
filter.type === "numberObject"
|
||||
? [Prisma.raw("cast("), Prisma.raw(" as double precision)")]
|
||||
: [Prisma.empty, Prisma.empty];
|
||||
const [valuePrefix, valueSuffix] =
|
||||
filter.type === "string" || filter.type === "stringObject"
|
||||
? [
|
||||
["contains", "does not contain", "ends with"].includes(
|
||||
filter.operator,
|
||||
)
|
||||
? Prisma.raw("'%' || ")
|
||||
: Prisma.empty,
|
||||
["contains", "does not contain", "starts with"].includes(
|
||||
filter.operator,
|
||||
)
|
||||
? Prisma.raw(" || '%'")
|
||||
: Prisma.empty,
|
||||
]
|
||||
: [Prisma.empty, Prisma.empty];
|
||||
|
||||
return Prisma.sql`${cast1}${colPrisma}${jsonKeyPrisma}${cast2} ${operatorPrisma} ${valuePrefix}${valuePrisma}${valueSuffix}`;
|
||||
});
|
||||
if (statements.length === 0) {
|
||||
return Prisma.empty;
|
||||
}
|
||||
|
||||
return Prisma.join(
|
||||
[Prisma.raw("AND "), Prisma.join(statements, " AND ")],
|
||||
"",
|
||||
);
|
||||
}
|
||||
|
||||
const dateOperators = filterOperators["datetime"];
|
||||
|
||||
export const datetimeFilterToPrismaSql = (
|
||||
safeColumn: string,
|
||||
operator: (typeof dateOperators)[number],
|
||||
value: Date,
|
||||
) => {
|
||||
if (!dateOperators.includes(operator)) {
|
||||
throw new Error("Invalid operator: " + operator);
|
||||
}
|
||||
if (isNaN(value.getTime())) {
|
||||
throw new Error("Invalid date: " + value.toString());
|
||||
}
|
||||
|
||||
return Prisma.sql`AND ${Prisma.raw(safeColumn)} ${Prisma.raw(operator)} ${
|
||||
value
|
||||
.toISOString()
|
||||
.split(".")[0]! // remove milliseconds
|
||||
.replace("T", " ") // to Postgres datetime
|
||||
}::TIMESTAMP`;
|
||||
};
|
||||
@@ -1,291 +0,0 @@
|
||||
import {
|
||||
type TiktokenEncoding,
|
||||
get_encoding,
|
||||
encoding_for_model,
|
||||
type TiktokenModel,
|
||||
type Tiktoken,
|
||||
} from "tiktoken";
|
||||
import { countTokens } from "@anthropic-ai/tokenizer";
|
||||
import { type PricingUnit, type Pricing } from "@prisma/client";
|
||||
import { Decimal } from "decimal.js";
|
||||
|
||||
type ChatMessage = {
|
||||
role: string;
|
||||
name?: string;
|
||||
content: string;
|
||||
};
|
||||
|
||||
type TokenCalculationParams = {
|
||||
messages: ChatMessage[];
|
||||
model: TiktokenModel;
|
||||
};
|
||||
|
||||
export function tokenCount(p: {
|
||||
model: string;
|
||||
text: unknown;
|
||||
}): number | undefined {
|
||||
if (
|
||||
p.text === null ||
|
||||
p.text === undefined ||
|
||||
(Array.isArray(p.text) && p.text.length === 0)
|
||||
) {
|
||||
return undefined;
|
||||
} else if (isOpenAiModel(p.model)) {
|
||||
return isChatMessageArray(p.text)
|
||||
? openAiChatTokenCount({
|
||||
model: p.model,
|
||||
messages: p.text,
|
||||
})
|
||||
: isString(p.text)
|
||||
? openAiStringTokenCount({ model: p.model, text: p.text })
|
||||
: openAiStringTokenCount({
|
||||
model: p.model,
|
||||
text: JSON.stringify(p.text),
|
||||
});
|
||||
} else if (isClaudeModel(p.model)) {
|
||||
return isString(p.text)
|
||||
? claudeStringTokenCount({ model: p.model, text: p.text })
|
||||
: claudeStringTokenCount({
|
||||
model: p.model,
|
||||
text: JSON.stringify(p.text),
|
||||
});
|
||||
} else {
|
||||
console.log("Unknown model provider", p.model);
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
function openAiChatTokenCount(params: TokenCalculationParams) {
|
||||
let encoding: Tiktoken;
|
||||
try {
|
||||
encoding = encoding_for_model(params.model);
|
||||
} catch (KeyError) {
|
||||
console.log("Warning: model not found. Using cl100k_base encoding.");
|
||||
encoding = get_encoding("cl100k_base");
|
||||
}
|
||||
let tokens_per_message = 0;
|
||||
let tokens_per_name = 0;
|
||||
|
||||
if (
|
||||
[
|
||||
"gpt-3.5-turbo-0613",
|
||||
"gpt-3.5-turbo-16k-0613",
|
||||
"gpt-4-0314",
|
||||
"gpt-4-32k-0314",
|
||||
"gpt-4-0613",
|
||||
"gpt-4-32k-0613",
|
||||
].includes(params.model)
|
||||
) {
|
||||
tokens_per_message = 3;
|
||||
tokens_per_name = 1;
|
||||
} else if (params.model === "gpt-3.5-turbo-0301") {
|
||||
tokens_per_message = 4; // every message follows <|start|>{role/name}\n{content}<|end|>\n
|
||||
tokens_per_name = -1; // if there's a name, the role is omitted
|
||||
} else if (
|
||||
params.model.includes("gpt-3.5-turbo") ||
|
||||
params.model.startsWith("gpt-3.5")
|
||||
) {
|
||||
return openAiChatTokenCount({ ...params, model: "gpt-3.5-turbo-0613" });
|
||||
} else if (params.model.includes("gpt-4")) {
|
||||
return openAiChatTokenCount({ ...params, model: "gpt-4-0613" });
|
||||
} else {
|
||||
console.error(`Not implemented for model ${params.model}`);
|
||||
throw new Error(`Not implemented for model ${params.model}`);
|
||||
}
|
||||
let num_tokens = 0;
|
||||
params.messages.forEach((message) => {
|
||||
num_tokens += tokens_per_message;
|
||||
|
||||
Object.keys(message).forEach((key) => {
|
||||
const value = message[key as keyof typeof message];
|
||||
if (value) {
|
||||
num_tokens += encoding.encode(value).length;
|
||||
}
|
||||
if (key === "name") {
|
||||
num_tokens += tokens_per_name;
|
||||
}
|
||||
});
|
||||
});
|
||||
num_tokens += 3; // every reply is primed with <| start |> assistant <| message |>
|
||||
return num_tokens;
|
||||
}
|
||||
|
||||
const openAiStringTokenCount = (p: { model: string; text: string }) => {
|
||||
if (
|
||||
p.model.toLowerCase().startsWith("gpt") ||
|
||||
p.model.toLowerCase().includes("ada")
|
||||
) {
|
||||
return getTokens("cl100k_base", p.text);
|
||||
}
|
||||
if (p.model.toLowerCase().startsWith("text-davinci")) {
|
||||
return getTokens("p50k_base", p.text);
|
||||
}
|
||||
console.log("Unknown model", p.model);
|
||||
return undefined;
|
||||
};
|
||||
|
||||
const claudeStringTokenCount = (p: { model: string; text: string }) => {
|
||||
return countTokens(p.text);
|
||||
};
|
||||
|
||||
const getTokens = (name: TiktokenEncoding, text: string) => {
|
||||
const encoding = get_encoding(name);
|
||||
const tokens = encoding.encode(text);
|
||||
encoding.free();
|
||||
return tokens.length;
|
||||
};
|
||||
|
||||
function isString(value: unknown): value is string {
|
||||
return typeof value === "string";
|
||||
}
|
||||
|
||||
function isClaudeModel(model: string) {
|
||||
return model.toLowerCase().startsWith("claude");
|
||||
}
|
||||
|
||||
function isOpenAiModel(model: string): model is TiktokenModel {
|
||||
return (
|
||||
[
|
||||
"text-davinci-003",
|
||||
"text-davinci-002",
|
||||
"text-davinci-001",
|
||||
"text-curie-001",
|
||||
"text-babbage-001",
|
||||
"text-ada-001",
|
||||
"davinci",
|
||||
"curie",
|
||||
"babbage",
|
||||
"ada",
|
||||
"code-davinci-002",
|
||||
"code-davinci-001",
|
||||
"code-cushman-002",
|
||||
"code-cushman-001",
|
||||
"davinci-codex",
|
||||
"cushman-codex",
|
||||
"text-davinci-edit-001",
|
||||
"code-davinci-edit-001",
|
||||
"text-embedding-ada-002",
|
||||
"text-similarity-davinci-001",
|
||||
"text-similarity-curie-001",
|
||||
"text-similarity-babbage-001",
|
||||
"text-similarity-ada-001",
|
||||
"text-search-davinci-doc-001",
|
||||
"text-search-curie-doc-001",
|
||||
"text-search-babbage-doc-001",
|
||||
"text-search-ada-doc-001",
|
||||
"code-search-babbage-code-001",
|
||||
"code-search-ada-code-001",
|
||||
"gpt2",
|
||||
"gpt-4",
|
||||
"gpt-4-0314",
|
||||
"gpt-4-0613",
|
||||
"gpt-4-32k",
|
||||
"gpt-4-32k-0314",
|
||||
"gpt-4-32k-0613",
|
||||
"gpt-4-1106-preview",
|
||||
"gpt-4-vision-preview",
|
||||
"gpt-3.5",
|
||||
"gpt-3.5-turbo",
|
||||
"gpt-3.5-turbo-0301",
|
||||
"gpt-3.5-turbo-0613",
|
||||
"gpt-3.5-turbo-16k",
|
||||
"gpt-3.5-turbo-16k-0613",
|
||||
].find((m) => m === model) !== undefined
|
||||
);
|
||||
}
|
||||
|
||||
function isChatMessageArray(value: unknown): value is ChatMessage[] {
|
||||
if (!Array.isArray(value)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return value.every(
|
||||
(item: unknown) =>
|
||||
typeof item === "object" &&
|
||||
item !== null &&
|
||||
"role" in item &&
|
||||
typeof item.role === "string" &&
|
||||
"content" in item &&
|
||||
typeof item.content === "string" &&
|
||||
(!("name" in item) || typeof item.name === "string"),
|
||||
);
|
||||
}
|
||||
|
||||
export function calculateTokenCost(
|
||||
pricingList: Pricing[],
|
||||
input: {
|
||||
model: string;
|
||||
input: unknown;
|
||||
output: unknown;
|
||||
totalTokens: Decimal;
|
||||
promptTokens: Decimal;
|
||||
completionTokens: Decimal;
|
||||
},
|
||||
): Decimal | undefined {
|
||||
const pricing = pricingList.filter((p) => p.modelName === input.model);
|
||||
|
||||
if (!pricing || pricing.length === 0) {
|
||||
console.log("no pricing found for model", input.model);
|
||||
return undefined;
|
||||
} else {
|
||||
if (pricing.length === 1 && pricing[0]?.tokenType === "TOTAL") {
|
||||
return calculateValue(
|
||||
pricing[0].price,
|
||||
pricing[0].pricingUnit,
|
||||
input.totalTokens,
|
||||
JSON.stringify(input.input) + JSON.stringify(input.output),
|
||||
);
|
||||
}
|
||||
|
||||
if (pricing.length === 2) {
|
||||
let promptPrice: Decimal = new Decimal(0);
|
||||
let completionPrice: Decimal = new Decimal(0);
|
||||
|
||||
const promptPricing = pricing.find((p) => p.tokenType === "PROMPT");
|
||||
const completionPricing = pricing.find(
|
||||
(p) => p.tokenType === "COMPLETION",
|
||||
);
|
||||
|
||||
if (promptPricing) {
|
||||
promptPrice = calculateValue(
|
||||
promptPricing.price,
|
||||
promptPricing.pricingUnit,
|
||||
input.promptTokens,
|
||||
JSON.stringify(input.input),
|
||||
);
|
||||
}
|
||||
|
||||
if (completionPricing) {
|
||||
completionPrice = calculateValue(
|
||||
completionPricing.price,
|
||||
completionPricing.pricingUnit,
|
||||
input.completionTokens,
|
||||
JSON.stringify(input.output),
|
||||
);
|
||||
}
|
||||
|
||||
return promptPrice.plus(completionPrice);
|
||||
}
|
||||
console.log("unknown model", input.model);
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
const calculateValue = (
|
||||
price: Decimal,
|
||||
unit: PricingUnit,
|
||||
tokens: Decimal,
|
||||
characters: string,
|
||||
) => {
|
||||
if (unit === "PER_1000_TOKENS")
|
||||
return price.times(tokens.dividedBy(new Decimal(1000))).toDecimalPlaces(5);
|
||||
else if (unit === "PER_1000_CHARS")
|
||||
return (
|
||||
price
|
||||
// strip whitespace, default for google bison, only character based model for now
|
||||
.times(new Decimal(characters.replace(/\s/g, "").length))
|
||||
.dividedBy(new Decimal(1000))
|
||||
.toDecimalPlaces(5)
|
||||
);
|
||||
else throw new Error("Unknown pricing unit", unit);
|
||||
};
|
||||
@@ -1,108 +0,0 @@
|
||||
import { Button } from "@/src/components/ui/button";
|
||||
import { CommandShortcut } from "@/src/components/ui/command";
|
||||
import {
|
||||
Tooltip,
|
||||
TooltipContent,
|
||||
TooltipProvider,
|
||||
TooltipTrigger,
|
||||
} from "@/src/components/ui/tooltip";
|
||||
import { useDetailPageLists } from "@/src/features/navigate-detail-pages/context";
|
||||
import { ChevronDown, ChevronUp } from "lucide-react";
|
||||
import { useRouter } from "next/router";
|
||||
import { usePostHog } from "posthog-js/react";
|
||||
import { useEffect } from "react";
|
||||
|
||||
export const DetailPageNav = (props: {
|
||||
currentId: string;
|
||||
path: (id: string) => string;
|
||||
listKey: string;
|
||||
}) => {
|
||||
const { detailPagelists } = useDetailPageLists();
|
||||
const ids = detailPagelists[props.listKey] ?? [];
|
||||
|
||||
const posthog = usePostHog();
|
||||
|
||||
const router = useRouter();
|
||||
const currentIndex = ids.findIndex((id) => id === props.currentId);
|
||||
const previousPageId = currentIndex > 0 ? ids[currentIndex - 1] : undefined;
|
||||
const nextPageId =
|
||||
currentIndex < ids.length - 1 ? ids[currentIndex + 1] : undefined;
|
||||
|
||||
// keyboard shortcuts for buttons k and j
|
||||
useEffect(() => {
|
||||
const handleKeyDown = (event: KeyboardEvent) => {
|
||||
// don't trigger keyboard shortcuts if the user is typing in an input
|
||||
if (document.activeElement instanceof HTMLInputElement) return;
|
||||
|
||||
if (event.key === "k" && previousPageId) {
|
||||
void router.push(props.path(previousPageId));
|
||||
} else if (event.key === "j" && nextPageId) {
|
||||
void router.push(props.path(nextPageId));
|
||||
}
|
||||
};
|
||||
window.addEventListener("keydown", handleKeyDown);
|
||||
return () => window.removeEventListener("keydown", handleKeyDown);
|
||||
}, [previousPageId, nextPageId, router, props]);
|
||||
|
||||
if (ids.length > 0)
|
||||
return (
|
||||
<div>
|
||||
<TooltipProvider>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="icon"
|
||||
disabled={!previousPageId}
|
||||
onClick={() => {
|
||||
if (previousPageId) {
|
||||
posthog.capture(
|
||||
"navigate_detail_pages:button_click_prev_or_next",
|
||||
);
|
||||
void router.push(props.path(previousPageId));
|
||||
}
|
||||
}}
|
||||
className="mr-2"
|
||||
>
|
||||
<ChevronUp className="h-4 w-4" />
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
<span>Navigate up</span>
|
||||
<CommandShortcut className="ml-2 rounded-sm bg-gray-100 p-1 px-2">
|
||||
k
|
||||
</CommandShortcut>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
<TooltipProvider>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="icon"
|
||||
disabled={!nextPageId}
|
||||
onClick={() => {
|
||||
if (nextPageId) {
|
||||
posthog.capture(
|
||||
"navigate_detail_pages:button_click_prev_or_next",
|
||||
);
|
||||
void router.push(props.path(nextPageId));
|
||||
}
|
||||
}}
|
||||
>
|
||||
<ChevronDown className="h-4 w-4" />
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
<span>Navigate down</span>
|
||||
<CommandShortcut className="ml-2 rounded-sm bg-gray-100 p-1 px-2">
|
||||
j
|
||||
</CommandShortcut>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
</div>
|
||||
);
|
||||
else return null;
|
||||
};
|
||||
@@ -1,133 +0,0 @@
|
||||
import { env } from "@/src/env.mjs";
|
||||
import {
|
||||
createShaHash,
|
||||
verifySecretKey,
|
||||
} from "@/src/features/public-api/lib/apiKeys";
|
||||
import { type ApiAccessScope } from "@/src/features/public-api/server/types";
|
||||
import { prisma } from "@/src/server/db";
|
||||
import { instrumentAsync } from "@/src/utils/instrumentation";
|
||||
|
||||
export type AuthHeaderVerificationResult =
|
||||
| {
|
||||
validKey: true;
|
||||
scope: ApiAccessScope;
|
||||
}
|
||||
| {
|
||||
validKey: false;
|
||||
error: string;
|
||||
};
|
||||
|
||||
export async function verifyAuthHeaderAndReturnScope(
|
||||
authHeader: string | undefined,
|
||||
): Promise<AuthHeaderVerificationResult> {
|
||||
return instrumentAsync(
|
||||
{ name: "verifyAuthHeaderAndReturnScope" },
|
||||
async () => {
|
||||
if (!authHeader) {
|
||||
console.error("No authorization header");
|
||||
return {
|
||||
validKey: false,
|
||||
error: "No authorization header",
|
||||
};
|
||||
}
|
||||
|
||||
try {
|
||||
// Basic auth, full scope, needs secret key and public key
|
||||
if (authHeader.startsWith("Basic ")) {
|
||||
const { username: publicKey, password: secretKey } =
|
||||
extractBasicAuthCredentials(authHeader);
|
||||
|
||||
const salt = env.SALT;
|
||||
const hashFromProvidedKey = createShaHash(secretKey, salt);
|
||||
const apiKey = await prisma.apiKey.findUnique({
|
||||
where: { fastHashedSecretKey: hashFromProvidedKey },
|
||||
});
|
||||
let projectId = apiKey?.projectId;
|
||||
|
||||
if (!apiKey || !apiKey.fastHashedSecretKey) {
|
||||
const dbKey = await findDbKeyOrThrow(publicKey);
|
||||
const isValid = await verifySecretKey(
|
||||
secretKey,
|
||||
dbKey.hashedSecretKey,
|
||||
);
|
||||
|
||||
if (!isValid) {
|
||||
console.log("Old key is invalid", publicKey);
|
||||
throw new Error("Invalid credentials");
|
||||
}
|
||||
|
||||
const shaKey = createShaHash(secretKey, salt);
|
||||
|
||||
await prisma.apiKey.update({
|
||||
where: { publicKey },
|
||||
data: {
|
||||
fastHashedSecretKey: shaKey,
|
||||
},
|
||||
});
|
||||
projectId = dbKey.projectId;
|
||||
}
|
||||
|
||||
if (!projectId) {
|
||||
console.log("No project id found for key", publicKey);
|
||||
throw new Error("Invalid credentials");
|
||||
}
|
||||
|
||||
return {
|
||||
validKey: true,
|
||||
scope: {
|
||||
projectId: projectId,
|
||||
accessLevel: "all",
|
||||
},
|
||||
};
|
||||
}
|
||||
// Bearer auth, limited scope, only needs public key
|
||||
if (authHeader.startsWith("Bearer ")) {
|
||||
const publicKey = authHeader.replace("Bearer ", "");
|
||||
|
||||
const dbKey = await findDbKeyOrThrow(publicKey);
|
||||
|
||||
return {
|
||||
validKey: true,
|
||||
scope: {
|
||||
projectId: dbKey.projectId,
|
||||
accessLevel: "scores",
|
||||
},
|
||||
};
|
||||
}
|
||||
} catch (error: unknown) {
|
||||
console.error("Error verifying auth header: ", error);
|
||||
return {
|
||||
validKey: false,
|
||||
error: error instanceof Error ? error.message : "Authorization error",
|
||||
};
|
||||
}
|
||||
return {
|
||||
validKey: false,
|
||||
error: "Invalid authorization header",
|
||||
};
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
function extractBasicAuthCredentials(basicAuthHeader: string): {
|
||||
username: string;
|
||||
password: string;
|
||||
} {
|
||||
const authValue = basicAuthHeader.split(" ")[1];
|
||||
if (!authValue) throw new Error("Invalid authorization header");
|
||||
|
||||
const [username, password] = atob(authValue).split(":");
|
||||
if (!username || !password) throw new Error("Invalid authorization header");
|
||||
return { username, password };
|
||||
}
|
||||
|
||||
async function findDbKeyOrThrow(publicKey: string) {
|
||||
const dbKey = await prisma.apiKey.findUnique({
|
||||
where: { publicKey },
|
||||
});
|
||||
if (!dbKey) {
|
||||
console.log("No api key found for public key:", publicKey);
|
||||
throw new Error("Invalid public key");
|
||||
}
|
||||
return dbKey;
|
||||
}
|
||||
@@ -1,189 +0,0 @@
|
||||
import { ObservationLevel } from "@prisma/client";
|
||||
import { jsonSchema } from "@/src/utils/zod";
|
||||
import { z } from "zod";
|
||||
|
||||
export const TraceSchema = z.object({
|
||||
id: z.string().nullish(),
|
||||
name: z.string().nullish(),
|
||||
externalId: z.string().nullish(),
|
||||
userId: z.string().nullish(),
|
||||
metadata: jsonSchema.nullish(),
|
||||
release: z.string().nullish(),
|
||||
version: z.string().nullish(),
|
||||
});
|
||||
export const SpanPostSchema = z.object({
|
||||
id: z.string().nullish(),
|
||||
traceId: z.string().nullish(),
|
||||
name: z.string().nullish(),
|
||||
startTime: z.string().datetime({ offset: true }).nullish(),
|
||||
endTime: z.string().datetime({ offset: true }).nullish(),
|
||||
metadata: jsonSchema.nullish(),
|
||||
input: jsonSchema.nullish(),
|
||||
output: jsonSchema.nullish(),
|
||||
parentObservationId: z.string().nullish(),
|
||||
level: z.nativeEnum(ObservationLevel).nullish(),
|
||||
statusMessage: z.string().nullish(),
|
||||
version: z.string().nullish(),
|
||||
});
|
||||
|
||||
export const SpanPatchSchema = z.object({
|
||||
spanId: z.string(),
|
||||
traceId: z.string().nullish(),
|
||||
name: z.string().nullish(),
|
||||
endTime: z.string().datetime({ offset: true }).nullish(),
|
||||
metadata: jsonSchema.nullish(),
|
||||
input: jsonSchema.nullish(),
|
||||
output: jsonSchema.nullish(),
|
||||
level: z.nativeEnum(ObservationLevel).nullish(),
|
||||
statusMessage: z.string().nullish(),
|
||||
version: z.string().nullish(),
|
||||
});
|
||||
export const GenerationsCreateSchema = z.object({
|
||||
id: z.string().nullish(),
|
||||
traceId: z.string().nullish(),
|
||||
name: z.string().nullish(),
|
||||
startTime: z.string().datetime({ offset: true }).nullish(),
|
||||
endTime: z.string().datetime({ offset: true }).nullish(),
|
||||
completionStartTime: z.string().datetime({ offset: true }).nullish(),
|
||||
model: z.string().nullish(),
|
||||
modelParameters: z
|
||||
.record(
|
||||
z.string(),
|
||||
z.union([z.string(), z.number(), z.boolean()]).nullish(),
|
||||
)
|
||||
.nullish(),
|
||||
prompt: jsonSchema.nullish(),
|
||||
completion: jsonSchema.nullish(),
|
||||
usage: z
|
||||
.object({
|
||||
promptTokens: z.number().nullish(),
|
||||
completionTokens: z.number().nullish(),
|
||||
totalTokens: z.number().nullish(),
|
||||
})
|
||||
.nullish(),
|
||||
metadata: jsonSchema.nullish(),
|
||||
parentObservationId: z.string().nullish(),
|
||||
level: z.nativeEnum(ObservationLevel).nullish(),
|
||||
statusMessage: z.string().nullish(),
|
||||
version: z.string().nullish(),
|
||||
});
|
||||
|
||||
export const GenerationPatchSchema = z.object({
|
||||
generationId: z.string(),
|
||||
traceId: z.string().nullish(),
|
||||
name: z.string().nullish(),
|
||||
endTime: z.string().datetime({ offset: true }).nullish(),
|
||||
completionStartTime: z.string().datetime({ offset: true }).nullish(),
|
||||
model: z.string().nullish(),
|
||||
modelParameters: z
|
||||
.record(
|
||||
z.string(),
|
||||
z.union([z.string(), z.number(), z.boolean()]).nullish(),
|
||||
)
|
||||
.nullish(),
|
||||
prompt: jsonSchema.nullish(),
|
||||
completion: jsonSchema.nullish(),
|
||||
usage: z
|
||||
.object({
|
||||
promptTokens: z.number().nullish(),
|
||||
completionTokens: z.number().nullish(),
|
||||
totalTokens: z.number().nullish(),
|
||||
})
|
||||
.nullish(),
|
||||
metadata: jsonSchema.nullish(),
|
||||
level: z.nativeEnum(ObservationLevel).nullish(),
|
||||
statusMessage: z.string().nullish(),
|
||||
version: z.string().nullish(),
|
||||
});
|
||||
export const EventSchema = z.object({
|
||||
id: z.string().nullish(),
|
||||
traceId: z.string().nullish(),
|
||||
name: z.string().nullish(),
|
||||
startTime: z.string().datetime({ offset: true }).nullish(),
|
||||
metadata: jsonSchema.nullish(),
|
||||
input: jsonSchema.nullish(),
|
||||
output: jsonSchema.nullish(),
|
||||
level: z.nativeEnum(ObservationLevel).nullish(),
|
||||
statusMessage: z.string().nullish(),
|
||||
parentObservationId: z.string().nullish(),
|
||||
version: z.string().nullish(),
|
||||
});
|
||||
|
||||
export const ObservationSchema = z.object({
|
||||
id: z.string().nullish(),
|
||||
traceId: z.string().nullish(),
|
||||
type: z.enum(["GENERATION", "SPAN", "EVENT"]),
|
||||
name: z.string().nullish(),
|
||||
startTime: z.string().datetime({ offset: true }).nullish(),
|
||||
endTime: z.string().datetime({ offset: true }).nullish(),
|
||||
completionStartTime: z.string().datetime({ offset: true }).nullish(),
|
||||
model: z.string().nullish(),
|
||||
modelParameters: z
|
||||
.record(
|
||||
z.string(),
|
||||
z.union([z.string(), z.number(), z.boolean()]).nullish(),
|
||||
)
|
||||
.nullish(),
|
||||
input: jsonSchema.nullish(),
|
||||
output: jsonSchema.nullish(),
|
||||
usage: z
|
||||
.object({
|
||||
promptTokens: z.number().nullish(),
|
||||
completionTokens: z.number().nullish(),
|
||||
totalTokens: z.number().nullish(),
|
||||
})
|
||||
.nullish(),
|
||||
metadata: jsonSchema.nullish(),
|
||||
parentObservationId: z.string().nullish(),
|
||||
level: z.nativeEnum(ObservationLevel).nullish(),
|
||||
statusMessage: z.string().nullish(),
|
||||
version: z.string().nullish(),
|
||||
});
|
||||
|
||||
export const ScoreSchema = z.object({
|
||||
id: z.string().nullish(),
|
||||
name: z.string(),
|
||||
value: z.number(),
|
||||
traceId: z.string(),
|
||||
observationId: z.string().nullish(),
|
||||
comment: z.string().nullish(),
|
||||
});
|
||||
|
||||
export const eventTypes = {
|
||||
TRACE_CREATE: "trace-create",
|
||||
OBSERVATION_CREATE: "observation-create",
|
||||
OBSERVAION_UPDATE: "observation-update",
|
||||
SCORE_CREATE: "score-create",
|
||||
} as const;
|
||||
const base = z.object({
|
||||
id: z.string(),
|
||||
timestamp: z.string().datetime({ offset: true }),
|
||||
});
|
||||
export const traceEvent = base.extend({
|
||||
type: z.literal(eventTypes.TRACE_CREATE),
|
||||
body: TraceSchema,
|
||||
});
|
||||
export const observationEvent = base.extend({
|
||||
type: z.literal(eventTypes.OBSERVATION_CREATE),
|
||||
body: ObservationSchema,
|
||||
});
|
||||
export const observationUpdateEvent = base.extend({
|
||||
type: z.literal(eventTypes.OBSERVAION_UPDATE),
|
||||
body: ObservationSchema,
|
||||
});
|
||||
export const scoreEvent = base.extend({
|
||||
type: z.literal(eventTypes.SCORE_CREATE),
|
||||
body: ScoreSchema,
|
||||
});
|
||||
export const singleEventSchema = z.discriminatedUnion("type", [
|
||||
traceEvent,
|
||||
observationEvent,
|
||||
observationUpdateEvent,
|
||||
scoreEvent,
|
||||
]);
|
||||
|
||||
export const ingestionBatch = z.array(singleEventSchema);
|
||||
|
||||
export const ingestionApiSchema = z.object({
|
||||
batch: ingestionBatch,
|
||||
});
|
||||
@@ -1,71 +0,0 @@
|
||||
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: "traces:publish",
|
||||
});
|
||||
const utils = api.useUtils();
|
||||
const mut = api.publishTraces.update.useMutation({
|
||||
onSuccess: () => utils.traces.invalidate(),
|
||||
});
|
||||
|
||||
const [isCopied, setIsCopied] = useState(false);
|
||||
|
||||
const handleCopy = (text: string) => {
|
||||
setIsCopied(true);
|
||||
void navigator.clipboard.writeText(text);
|
||||
setTimeout(() => setIsCopied(false), 1500);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="text-sm font-semibold">
|
||||
{mut.isLoading ? (
|
||||
"Loading.."
|
||||
) : props.isPublic ? (
|
||||
<div
|
||||
className="flex cursor-pointer items-center gap-1 text-green-800"
|
||||
onClick={() =>
|
||||
handleCopy(
|
||||
window.location.origin + "/public/traces/" + props.traceId,
|
||||
)
|
||||
}
|
||||
>
|
||||
{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 (mut.isLoading) return;
|
||||
mut.mutate({
|
||||
traceId: props.traceId,
|
||||
public: !props.isPublic,
|
||||
projectId: props.projectId,
|
||||
});
|
||||
posthog.capture("trace_detail:publish_trace_button_click");
|
||||
}}
|
||||
disabled={!hasAccess}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -1,33 +0,0 @@
|
||||
import { throwIfNoAccess } from "@/src/features/rbac/utils/checkAccess";
|
||||
import {
|
||||
createTRPCRouter,
|
||||
protectedProjectProcedure,
|
||||
} from "@/src/server/api/trpc";
|
||||
import * as z from "zod";
|
||||
|
||||
export const publishTracesRouter = createTRPCRouter({
|
||||
update: protectedProjectProcedure
|
||||
.input(
|
||||
z.object({
|
||||
projectId: z.string(),
|
||||
traceId: z.string(),
|
||||
public: z.boolean(),
|
||||
}),
|
||||
)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
throwIfNoAccess({
|
||||
session: ctx.session,
|
||||
projectId: input.projectId,
|
||||
scope: "traces:publish",
|
||||
});
|
||||
return ctx.prisma.trace.update({
|
||||
where: {
|
||||
id: input.traceId,
|
||||
projectId: input.projectId,
|
||||
},
|
||||
data: {
|
||||
public: input.public,
|
||||
},
|
||||
});
|
||||
}),
|
||||
});
|
||||
@@ -1,170 +0,0 @@
|
||||
import { Button } from "@/src/components/ui/button";
|
||||
import { api } from "@/src/utils/api";
|
||||
import { useState } from "react";
|
||||
import { PlusIcon } from "lucide-react";
|
||||
import * as z from "zod";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { useForm } from "react-hook-form";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
DialogTrigger,
|
||||
} from "@/src/components/ui/dialog";
|
||||
import {
|
||||
Form,
|
||||
FormControl,
|
||||
FormDescription,
|
||||
FormField,
|
||||
FormItem,
|
||||
FormLabel,
|
||||
FormMessage,
|
||||
} from "@/src/components/ui/form";
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@/src/components/ui/select";
|
||||
import { Input } from "@/src/components/ui/input";
|
||||
import { MembershipRole } from "@prisma/client";
|
||||
import { roleAccessRights } from "@/src/features/rbac/constants/roleAccessRights";
|
||||
import { useHasAccess } from "@/src/features/rbac/utils/checkAccess";
|
||||
|
||||
const availableRoles = [
|
||||
MembershipRole.ADMIN,
|
||||
MembershipRole.MEMBER,
|
||||
MembershipRole.VIEWER,
|
||||
] as const;
|
||||
|
||||
const formSchema = z.object({
|
||||
email: z.string().trim().email(),
|
||||
role: z.enum(availableRoles),
|
||||
});
|
||||
|
||||
export function CreateProjectMemberButton(props: { projectId: string }) {
|
||||
const [open, setOpen] = useState(false);
|
||||
const hasAccess = useHasAccess({
|
||||
projectId: props.projectId,
|
||||
scope: "members:create",
|
||||
});
|
||||
|
||||
const utils = api.useUtils();
|
||||
const mutCreateProjectMember = api.projectMembers.create.useMutation({
|
||||
onSuccess: () => utils.projectMembers.invalidate(),
|
||||
});
|
||||
|
||||
const form = useForm<z.infer<typeof formSchema>>({
|
||||
resolver: zodResolver(formSchema),
|
||||
defaultValues: {
|
||||
email: "",
|
||||
role: MembershipRole.MEMBER,
|
||||
},
|
||||
});
|
||||
|
||||
if (!hasAccess) return null;
|
||||
|
||||
function onSubmit(values: z.infer<typeof formSchema>) {
|
||||
return mutCreateProjectMember
|
||||
.mutateAsync({
|
||||
projectId: props.projectId,
|
||||
email: values.email,
|
||||
role: values.role,
|
||||
})
|
||||
.then(() => {
|
||||
form.reset();
|
||||
setOpen(false);
|
||||
})
|
||||
.catch(() => {
|
||||
form.setError("email", {
|
||||
type: "manual",
|
||||
message: "User does not exist or already has access to this project",
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={setOpen}>
|
||||
<DialogTrigger asChild>
|
||||
<Button variant="secondary" loading={mutCreateProjectMember.isLoading}>
|
||||
<PlusIcon className="-ml-0.5 mr-1.5 h-5 w-5" aria-hidden="true" />
|
||||
Add new member
|
||||
</Button>
|
||||
</DialogTrigger>
|
||||
<DialogContent>
|
||||
<DialogHeader>
|
||||
<DialogTitle>Add new member to project</DialogTitle>
|
||||
</DialogHeader>
|
||||
<Form {...form}>
|
||||
<form
|
||||
className="space-y-6"
|
||||
// eslint-disable-next-line @typescript-eslint/no-misused-promises
|
||||
onSubmit={form.handleSubmit(onSubmit)}
|
||||
>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="email"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Email</FormLabel>
|
||||
<FormControl>
|
||||
<Input placeholder="jsdoe@example.com" {...field} />
|
||||
</FormControl>
|
||||
<FormDescription>
|
||||
User must already have an account
|
||||
</FormDescription>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="role"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Role</FormLabel>
|
||||
<Select
|
||||
defaultValue={field.value}
|
||||
onValueChange={(value) =>
|
||||
field.onChange(value as (typeof availableRoles)[number])
|
||||
}
|
||||
>
|
||||
<FormControl>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="Select a verified email to display" />
|
||||
</SelectTrigger>
|
||||
</FormControl>
|
||||
<SelectContent>
|
||||
{availableRoles.map((role) => (
|
||||
<SelectItem value={role} key={role}>
|
||||
{role}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<FormDescription>
|
||||
Rights of role:{" "}
|
||||
{roleAccessRights[field.value].length
|
||||
? roleAccessRights[field.value].join(", ")
|
||||
: "none"}
|
||||
</FormDescription>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<Button
|
||||
type="submit"
|
||||
className="w-full"
|
||||
loading={form.formState.isSubmitting}
|
||||
>
|
||||
Grant access
|
||||
</Button>
|
||||
<FormMessage />
|
||||
</form>
|
||||
</Form>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
@@ -1,107 +0,0 @@
|
||||
import { throwIfNoAccess } from "@/src/features/rbac/utils/checkAccess";
|
||||
import {
|
||||
createTRPCRouter,
|
||||
protectedProjectProcedure,
|
||||
} from "@/src/server/api/trpc";
|
||||
import { MembershipRole } from "@prisma/client";
|
||||
import * as z from "zod";
|
||||
|
||||
export const projectMembersRouter = createTRPCRouter({
|
||||
get: protectedProjectProcedure
|
||||
.input(
|
||||
z.object({
|
||||
projectId: z.string(),
|
||||
}),
|
||||
)
|
||||
.query(async ({ input, ctx }) => {
|
||||
throwIfNoAccess({
|
||||
session: ctx.session,
|
||||
projectId: input.projectId,
|
||||
scope: "members:read",
|
||||
});
|
||||
|
||||
return await ctx.prisma.membership.findMany({
|
||||
where: {
|
||||
projectId: input.projectId,
|
||||
project: {
|
||||
members: {
|
||||
some: {
|
||||
userId: ctx.session.user.id,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
include: {
|
||||
user: {
|
||||
select: {
|
||||
id: true,
|
||||
name: true,
|
||||
email: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
}),
|
||||
delete: protectedProjectProcedure
|
||||
.input(
|
||||
z.object({
|
||||
projectId: z.string(),
|
||||
userId: z.string(),
|
||||
}),
|
||||
)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
throwIfNoAccess({
|
||||
session: ctx.session,
|
||||
projectId: input.projectId,
|
||||
scope: "members:delete",
|
||||
});
|
||||
|
||||
if (input.userId === ctx.session.user.id)
|
||||
throw new Error("You cannot remove yourself from a project");
|
||||
|
||||
// use deleteMany to protect against deleting owner with where clause
|
||||
return ctx.prisma.membership.deleteMany({
|
||||
where: {
|
||||
projectId: input.projectId,
|
||||
userId: input.userId,
|
||||
role: {
|
||||
not: MembershipRole.OWNER,
|
||||
},
|
||||
},
|
||||
});
|
||||
}),
|
||||
create: protectedProjectProcedure
|
||||
.input(
|
||||
z.object({
|
||||
projectId: z.string(),
|
||||
email: z.string().email(),
|
||||
role: z.enum([
|
||||
MembershipRole.ADMIN,
|
||||
MembershipRole.MEMBER,
|
||||
MembershipRole.VIEWER,
|
||||
]),
|
||||
}),
|
||||
)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
throwIfNoAccess({
|
||||
session: ctx.session,
|
||||
projectId: input.projectId,
|
||||
scope: "members:create",
|
||||
});
|
||||
|
||||
const user = await ctx.prisma.user.findUnique({
|
||||
where: {
|
||||
email: input.email.toLowerCase(),
|
||||
},
|
||||
});
|
||||
if (!user) throw new Error("User not found");
|
||||
|
||||
return await ctx.prisma.membership.create({
|
||||
data: {
|
||||
userId: user.id,
|
||||
projectId: input.projectId,
|
||||
role: input.role,
|
||||
},
|
||||
});
|
||||
}),
|
||||
});
|
||||
@@ -1,47 +0,0 @@
|
||||
import {
|
||||
roleAccessRights,
|
||||
type Scope,
|
||||
} from "@/src/features/rbac/constants/roleAccessRights";
|
||||
import { type MembershipRole } from "@prisma/client";
|
||||
import { type Session } from "next-auth";
|
||||
import { useSession, type SessionContextValue } from "next-auth/react";
|
||||
|
||||
type HasAccessParams =
|
||||
| {
|
||||
role: MembershipRole;
|
||||
scope: Scope;
|
||||
}
|
||||
| {
|
||||
session: SessionContextValue | Session;
|
||||
projectId: string;
|
||||
scope: Scope;
|
||||
};
|
||||
|
||||
// For use in TRPC routes
|
||||
export const throwIfNoAccess = (p: HasAccessParams) => {
|
||||
if (!hasAccess(p)) throw new Error("No access");
|
||||
};
|
||||
|
||||
// For use in UI components as react hook
|
||||
export const useHasAccess = (p: { projectId: string; scope: Scope }) => {
|
||||
const session = useSession();
|
||||
return hasAccess({ session, ...p });
|
||||
};
|
||||
|
||||
function hasAccess(p: HasAccessParams): boolean {
|
||||
const role: MembershipRole | undefined =
|
||||
"role" in p
|
||||
? // MembershipRole
|
||||
p.role
|
||||
: "data" in p.session
|
||||
? // SessionContextValue
|
||||
p.session.data?.user?.projects.find(
|
||||
(project) => project.id === p.projectId,
|
||||
)?.role
|
||||
: // Session
|
||||
p.session.user?.projects.find((project) => project.id === p.projectId)
|
||||
?.role;
|
||||
if (role === undefined) return false;
|
||||
|
||||
return roleAccessRights[role].includes(p.scope);
|
||||
}
|
||||
@@ -1,57 +0,0 @@
|
||||
// Langfuse Cloud only
|
||||
|
||||
import { env } from "@/src/env.mjs";
|
||||
import { api } from "@/src/utils/api";
|
||||
import { Card, Flex, MarkerBar, Metric, Text } from "@tremor/react";
|
||||
|
||||
export const ProjectUsageChart: React.FC<{ projectId: string }> = ({
|
||||
projectId,
|
||||
}) => {
|
||||
const usage = api.usageMetering.currentMonth.useQuery({
|
||||
projectId,
|
||||
});
|
||||
// TODO: use planLimit from the API
|
||||
const planLimit = 100000;
|
||||
const currentMonth = new Date().toLocaleDateString("en-US", {
|
||||
month: "short",
|
||||
});
|
||||
|
||||
if (!env.NEXT_PUBLIC_LANGFUSE_CLOUD_REGION) return null;
|
||||
|
||||
return (
|
||||
<div>
|
||||
<h2 className="mb-5 text-base font-semibold leading-6 text-gray-900">
|
||||
Usage
|
||||
</h2>
|
||||
<Card className="p-4 lg:w-1/2">
|
||||
{usage.data !== undefined ? (
|
||||
<>
|
||||
<Text>Observations / month</Text>
|
||||
<Metric>{usage.data}</Metric>
|
||||
<Flex className="mt-4">
|
||||
<Text>
|
||||
{`${currentMonth}: ${usage.data} (${(
|
||||
(usage.data / planLimit) *
|
||||
100
|
||||
).toLocaleString(undefined, {
|
||||
maximumFractionDigits: 2,
|
||||
})}%)`}
|
||||
</Text>
|
||||
<Text>Plan limit: {simplifyNumber(planLimit)}</Text>
|
||||
</Flex>
|
||||
<MarkerBar
|
||||
value={Math.min((usage.data / planLimit) * 100, 100)}
|
||||
className="mt-3"
|
||||
/>
|
||||
</>
|
||||
) : null}
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
function simplifyNumber(num: number) {
|
||||
if (num >= 1000000) return num / 1000000 + "m";
|
||||
if (num >= 1000) return num / 1000 + "k";
|
||||
return num.toString();
|
||||
}
|
||||
@@ -1,13 +0,0 @@
|
||||
import { withSwagger } from "next-swagger-doc";
|
||||
|
||||
const swaggerHandler = withSwagger({
|
||||
definition: {
|
||||
openapi: "3.0.0",
|
||||
info: {
|
||||
title: "NextJS Swagger",
|
||||
version: "0.1.0",
|
||||
},
|
||||
},
|
||||
apiFolder: "src/pages/api/public",
|
||||
});
|
||||
export default swaggerHandler();
|
||||
@@ -1,210 +0,0 @@
|
||||
import { type NextApiRequest, type NextApiResponse } from "next";
|
||||
import { z } from "zod";
|
||||
import { cors, runMiddleware } from "@/src/features/public-api/server/cors";
|
||||
import { prisma } from "@/src/server/db";
|
||||
import { verifyAuthHeaderAndReturnScope } from "@/src/features/public-api/server/apiAuth";
|
||||
import { Prisma } from "@prisma/client";
|
||||
import { paginationZod } from "@/src/utils/zod";
|
||||
|
||||
const GetUsageSchema = z.object({
|
||||
...paginationZod,
|
||||
group_by: z.enum(["trace_name"]).nullish(),
|
||||
trace_name: z.string().nullish(),
|
||||
});
|
||||
|
||||
export default async function handler(
|
||||
req: NextApiRequest,
|
||||
res: NextApiResponse,
|
||||
) {
|
||||
await runMiddleware(req, res, cors);
|
||||
|
||||
// CHECK AUTH
|
||||
const authCheck = await verifyAuthHeaderAndReturnScope(
|
||||
req.headers.authorization,
|
||||
);
|
||||
if (!authCheck.validKey)
|
||||
return res.status(401).json({
|
||||
message: authCheck.error,
|
||||
});
|
||||
// END CHECK AUTH
|
||||
|
||||
try {
|
||||
if (req.method === "GET") {
|
||||
if (authCheck.scope.accessLevel !== "all") {
|
||||
return res.status(401).json({
|
||||
message:
|
||||
"Access denied - need to use basic auth with secret key to GET scores",
|
||||
});
|
||||
}
|
||||
const obj = GetUsageSchema.parse(req.query); // uses query and not body
|
||||
|
||||
const traceNameCondition = obj.trace_name
|
||||
? Prisma.sql`AND t.name = ${obj.trace_name}`
|
||||
: Prisma.empty;
|
||||
|
||||
if (obj.group_by === undefined) {
|
||||
const [usage, totalItemsRes] = await Promise.all([
|
||||
prisma.$queryRaw`
|
||||
WITH model_usage AS (
|
||||
SELECT
|
||||
DATE_TRUNC('DAY',
|
||||
o.start_time) observation_day,
|
||||
o.model,
|
||||
SUM(o.prompt_tokens) prompt_tokens,
|
||||
SUM(o.completion_tokens) completion_tokens,
|
||||
SUM(o.total_tokens) total_tokens
|
||||
FROM
|
||||
traces t
|
||||
LEFT JOIN observations o ON o.trace_id = t.id
|
||||
WHERE o.start_time IS NOT NULL
|
||||
AND o.project_id = ${authCheck.scope.projectId}
|
||||
AND t.project_id = ${authCheck.scope.projectId}
|
||||
${traceNameCondition}
|
||||
GROUP BY 1,2
|
||||
order by 1,2
|
||||
),
|
||||
daily_usage AS (
|
||||
SELECT
|
||||
observation_day,
|
||||
json_agg(json_build_object('model',
|
||||
model,
|
||||
'prompt_tokens',
|
||||
prompt_tokens,
|
||||
'completion_tokens',
|
||||
completion_tokens,
|
||||
'total_tokens',
|
||||
total_tokens)) daily_usage_json
|
||||
FROM model_usage
|
||||
group by 1
|
||||
)
|
||||
SELECT
|
||||
observation_day "date",
|
||||
daily_usage_json usage
|
||||
FROM daily_usage
|
||||
ORDER BY 1 desc
|
||||
LIMIT ${obj.limit} OFFSET ${(obj.page - 1) * obj.limit}
|
||||
`,
|
||||
prisma.$queryRaw<{ count: bigint }[]>`
|
||||
SELECT
|
||||
count(DISTINCT DATE_TRUNC('DAY', observations.start_time))
|
||||
FROM
|
||||
observations
|
||||
JOIN traces ON observations.trace_id = traces.id
|
||||
WHERE traces.project_id = ${authCheck.scope.projectId}
|
||||
`,
|
||||
]);
|
||||
|
||||
const totalItems =
|
||||
totalItemsRes[0] !== undefined ? Number(totalItemsRes[0].count) : 0;
|
||||
|
||||
return res.status(200).json({
|
||||
data: usage,
|
||||
meta: {
|
||||
page: obj.page,
|
||||
limit: obj.limit,
|
||||
totalItems,
|
||||
totalPages: Math.ceil(totalItems / obj.limit),
|
||||
},
|
||||
});
|
||||
} else if (obj.group_by === "trace_name") {
|
||||
const [usage, totalItemsRes] = await Promise.all([
|
||||
prisma.$queryRaw`
|
||||
WITH model_usage AS (
|
||||
SELECT
|
||||
t."name" trace_name,
|
||||
DATE_TRUNC('DAY',
|
||||
o.start_time) observation_day,
|
||||
o.model,
|
||||
SUM(o.prompt_tokens) prompt_tokens,
|
||||
SUM(o.completion_tokens) completion_tokens,
|
||||
SUM(o.total_tokens) total_tokens
|
||||
FROM
|
||||
traces t
|
||||
LEFT JOIN observations o ON o.trace_id = t.id
|
||||
WHERE o.start_time IS NOT NULL
|
||||
AND t.project_id = ${authCheck.scope.projectId}
|
||||
AND o.project_id = ${authCheck.scope.projectId}
|
||||
${traceNameCondition}
|
||||
GROUP BY 1,2,3
|
||||
order by 1,2,3
|
||||
),
|
||||
daily_usage AS (
|
||||
SELECT
|
||||
trace_name,
|
||||
observation_day,
|
||||
json_agg(json_build_object('model',
|
||||
model,
|
||||
'prompt_tokens',
|
||||
prompt_tokens,
|
||||
'completion_tokens',
|
||||
completion_tokens,
|
||||
'total_tokens',
|
||||
total_tokens)) daily_usage_json
|
||||
FROM model_usage
|
||||
WHERE prompt_tokens > 0
|
||||
OR completion_tokens > 0
|
||||
OR total_tokens > 0
|
||||
group by 1,2
|
||||
order by 1,2 desc
|
||||
),
|
||||
all_trace_names AS (
|
||||
SELECT t."name" trace_name
|
||||
FROM traces t
|
||||
WHERE t.project_id = ${authCheck.scope.projectId}
|
||||
${traceNameCondition}
|
||||
GROUP BY 1
|
||||
)
|
||||
SELECT
|
||||
all_trace_names.trace_name,
|
||||
json_agg(json_build_object(
|
||||
'date',
|
||||
observation_day,
|
||||
'usage',
|
||||
daily_usage_json
|
||||
)) metrics
|
||||
FROM all_trace_names
|
||||
LEFT JOIN daily_usage ON all_trace_names.trace_name = daily_usage.trace_name
|
||||
group by 1
|
||||
ORDER BY 1
|
||||
LIMIT ${obj.limit} OFFSET ${(obj.page - 1) * obj.limit}
|
||||
`,
|
||||
prisma.$queryRaw<{ count: bigint }[]>`
|
||||
SELECT
|
||||
count(DISTINCT CASE WHEN "name" IS NULL THEN 'COUNT_NULL' ELSE "name" END)
|
||||
FROM
|
||||
traces
|
||||
WHERE project_id = ${authCheck.scope.projectId}
|
||||
`,
|
||||
]);
|
||||
|
||||
const totalItems =
|
||||
totalItemsRes[0] !== undefined ? Number(totalItemsRes[0].count) : 0;
|
||||
|
||||
return res.status(200).json({
|
||||
data: usage,
|
||||
meta: {
|
||||
page: obj.page,
|
||||
limit: obj.limit,
|
||||
totalItems,
|
||||
totalPages: Math.ceil(totalItems / obj.limit),
|
||||
},
|
||||
});
|
||||
} else {
|
||||
return res.status(400).json({
|
||||
message: "Invalid group_by value",
|
||||
});
|
||||
}
|
||||
} else {
|
||||
console.error(req.method, req.body);
|
||||
return res.status(405).json({ message: "Method not allowed" });
|
||||
}
|
||||
} catch (error: unknown) {
|
||||
console.error(error);
|
||||
const errorMessage =
|
||||
error instanceof Error ? error.message : "An unknown error occurred";
|
||||
res.status(400).json({
|
||||
message: "Invalid request data",
|
||||
error: errorMessage,
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -1,47 +0,0 @@
|
||||
import { FilterBuilder } from "@/src/features/filters/components/filter-builder";
|
||||
import { useQueryFilterState } from "@/src/features/filters/hooks/useFilterState";
|
||||
import { type ColumnDefinition } from "@/src/server/api/interfaces/tableDefinition";
|
||||
|
||||
const cols: ColumnDefinition[] = [
|
||||
{
|
||||
name: "col1",
|
||||
type: "string",
|
||||
internal: "col1",
|
||||
},
|
||||
{
|
||||
name: "col2",
|
||||
type: "number",
|
||||
internal: "col2",
|
||||
},
|
||||
{
|
||||
name: "col3",
|
||||
type: "datetime",
|
||||
internal: "col3",
|
||||
},
|
||||
{
|
||||
name: "col4",
|
||||
type: "stringOptions",
|
||||
internal: "col4",
|
||||
options: ["option1", "option2", "option3"].map((o, i) => ({
|
||||
value: o,
|
||||
count: i,
|
||||
})),
|
||||
},
|
||||
];
|
||||
|
||||
const FilterPage = () => {
|
||||
const [filterState, setFilterState] = useQueryFilterState([]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<FilterBuilder
|
||||
columns={cols}
|
||||
filterState={filterState}
|
||||
onChange={setFilterState}
|
||||
/>
|
||||
<pre>{JSON.stringify(filterState, null, 2)}</pre>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default FilterPage;
|
||||
@@ -1,310 +0,0 @@
|
||||
import Header from "@/src/components/layouts/header";
|
||||
import { api, directApi } from "@/src/utils/api";
|
||||
import { type ColumnDef } from "@tanstack/react-table";
|
||||
import { DataTable } from "@/src/components/table/data-table";
|
||||
import TableLink from "@/src/components/table/table-link";
|
||||
import { DataTableToolbar } from "@/src/components/table/data-table-toolbar";
|
||||
import { useState } from "react";
|
||||
import { useRouter } from "next/router";
|
||||
import { TokenUsageBadge } from "@/src/components/token-usage-badge";
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuTrigger,
|
||||
} from "@/src/components/ui/dropdown-menu";
|
||||
import { Button } from "@/src/components/ui/button";
|
||||
import { ChevronDownIcon, Loader } from "lucide-react";
|
||||
import { type ExportFileFormats } from "@/src/server/api/routers/generations";
|
||||
import { usePostHog } from "posthog-js/react";
|
||||
import {
|
||||
NumberParam,
|
||||
StringParam,
|
||||
useQueryParam,
|
||||
useQueryParams,
|
||||
withDefault,
|
||||
} from "use-query-params";
|
||||
import { useQueryFilterState } from "@/src/features/filters/hooks/useFilterState";
|
||||
import { observationsTableColsWithOptions } from "@/src/server/api/definitions/observationsTable";
|
||||
import { utcDateOffsetByDays } from "@/src/utils/dates";
|
||||
|
||||
type GenerationTableRow = {
|
||||
id: string;
|
||||
traceId: string;
|
||||
startTime: string;
|
||||
endTime?: string;
|
||||
latency?: number;
|
||||
name?: string;
|
||||
model?: string;
|
||||
traceName?: string;
|
||||
usage: {
|
||||
promptTokens: number;
|
||||
completionTokens: number;
|
||||
totalTokens: number;
|
||||
};
|
||||
};
|
||||
|
||||
const exportOptions: Record<
|
||||
ExportFileFormats,
|
||||
{
|
||||
label: string;
|
||||
extension: string;
|
||||
fileType: string;
|
||||
}
|
||||
> = {
|
||||
CSV: { label: "CSV", extension: "csv", fileType: "text/csv" },
|
||||
JSON: { label: "JSON", extension: "json", fileType: "application/json" },
|
||||
"OPENAI-JSONL": {
|
||||
label: "OpenAI JSONL (fine-tuning)",
|
||||
extension: "jsonl",
|
||||
fileType: "application/json",
|
||||
},
|
||||
} as const;
|
||||
|
||||
export default function Generations() {
|
||||
const posthog = usePostHog();
|
||||
const router = useRouter();
|
||||
const projectId = router.query.projectId as string;
|
||||
const [isExporting, setIsExporting] = useState(false);
|
||||
|
||||
const [searchQuery, setSearchQuery] = useQueryParam(
|
||||
"search",
|
||||
withDefault(StringParam, null),
|
||||
);
|
||||
|
||||
const [paginationState, setPaginationState] = useQueryParams({
|
||||
pageIndex: withDefault(NumberParam, 0),
|
||||
pageSize: withDefault(NumberParam, 50),
|
||||
});
|
||||
|
||||
const [filterState, setFilterState] = useQueryFilterState([
|
||||
{
|
||||
column: "start_time",
|
||||
type: "datetime",
|
||||
operator: ">",
|
||||
value: utcDateOffsetByDays(-14),
|
||||
},
|
||||
]);
|
||||
|
||||
const generations = api.generations.all.useQuery({
|
||||
page: paginationState.pageIndex,
|
||||
limit: paginationState.pageSize,
|
||||
projectId,
|
||||
filter: filterState,
|
||||
searchQuery,
|
||||
});
|
||||
const totalCount = generations.data?.slice(1)[0]?.totalCount ?? 0;
|
||||
|
||||
const filterOptions = api.generations.filterOptions.useQuery({
|
||||
projectId,
|
||||
});
|
||||
|
||||
const handleExport = async (fileFormat: ExportFileFormats) => {
|
||||
if (isExporting) return;
|
||||
|
||||
setIsExporting(true);
|
||||
posthog.capture("generations:export", { file_format: fileFormat });
|
||||
const fileData = await directApi.generations.export.query({
|
||||
projectId,
|
||||
fileFormat,
|
||||
filter: filterState,
|
||||
searchQuery,
|
||||
});
|
||||
|
||||
if (fileData) {
|
||||
const file = new File(
|
||||
[fileData],
|
||||
`generations.${exportOptions[fileFormat].extension}`,
|
||||
{
|
||||
type: exportOptions[fileFormat].fileType,
|
||||
},
|
||||
);
|
||||
|
||||
// create url from file
|
||||
const url = URL.createObjectURL(file);
|
||||
|
||||
// Use a dynamically created anchor element to trigger the download
|
||||
const a = document.createElement("a");
|
||||
document.body.appendChild(a);
|
||||
a.href = url;
|
||||
a.download = `generations.${exportOptions[fileFormat].extension}`; // name of the downloaded file
|
||||
a.click();
|
||||
a.remove();
|
||||
|
||||
// Revoke the blob URL after using it
|
||||
setTimeout(() => URL.revokeObjectURL(url), 100);
|
||||
}
|
||||
setIsExporting(false);
|
||||
};
|
||||
|
||||
const columns: ColumnDef<GenerationTableRow>[] = [
|
||||
{
|
||||
accessorKey: "id",
|
||||
header: "ID",
|
||||
cell: ({ row }) => {
|
||||
const observationId = row.getValue("id");
|
||||
const traceId = row.getValue("traceId");
|
||||
return typeof observationId === "string" &&
|
||||
typeof traceId === "string" ? (
|
||||
<TableLink
|
||||
path={`/project/${projectId}/traces/${traceId}?observation=${observationId}`}
|
||||
value={observationId}
|
||||
/>
|
||||
) : null;
|
||||
},
|
||||
},
|
||||
{
|
||||
accessorKey: "name",
|
||||
header: "name",
|
||||
},
|
||||
{
|
||||
accessorKey: "traceId",
|
||||
header: "Trace ID",
|
||||
cell: ({ row }) => {
|
||||
const value = row.getValue("traceId");
|
||||
return typeof value === "string" ? (
|
||||
<TableLink
|
||||
path={`/project/${projectId}/traces/${value}`}
|
||||
value={value}
|
||||
/>
|
||||
) : undefined;
|
||||
},
|
||||
},
|
||||
{
|
||||
accessorKey: "traceName",
|
||||
header: "Trace Name",
|
||||
},
|
||||
{
|
||||
accessorKey: "startTime",
|
||||
header: "Start Time",
|
||||
},
|
||||
{
|
||||
accessorKey: "latency",
|
||||
header: "Latency",
|
||||
cell: ({ row }) => {
|
||||
const value: number | undefined = row.getValue("latency");
|
||||
return value !== undefined ? (
|
||||
<span>{value.toFixed(2)} sec</span>
|
||||
) : undefined;
|
||||
},
|
||||
},
|
||||
{
|
||||
accessorKey: "model",
|
||||
header: "Model",
|
||||
},
|
||||
{
|
||||
accessorKey: "usage",
|
||||
header: "Usage",
|
||||
cell: ({ row }) => {
|
||||
const value: {
|
||||
promptTokens: number;
|
||||
completionTokens: number;
|
||||
totalTokens: number;
|
||||
} = row.getValue("usage");
|
||||
return (
|
||||
<TokenUsageBadge
|
||||
promptTokens={value.promptTokens}
|
||||
completionTokens={value.completionTokens}
|
||||
totalTokens={value.totalTokens}
|
||||
inline
|
||||
/>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
accessorKey: "version",
|
||||
header: "Version",
|
||||
},
|
||||
];
|
||||
|
||||
const rows: GenerationTableRow[] = generations.isSuccess
|
||||
? generations.data.map((generation) => ({
|
||||
id: generation.id,
|
||||
traceId: generation.traceId,
|
||||
traceName: generation.traceName,
|
||||
startTime: generation.startTime.toLocaleString(),
|
||||
endTime: generation.endTime?.toLocaleString() ?? undefined,
|
||||
latency: generation.latency === null ? undefined : generation.latency,
|
||||
name: generation.name ?? undefined,
|
||||
version: generation.version ?? "",
|
||||
model: generation.model ?? "",
|
||||
usage: {
|
||||
promptTokens: generation.promptTokens,
|
||||
completionTokens: generation.completionTokens,
|
||||
totalTokens: generation.totalTokens,
|
||||
},
|
||||
}))
|
||||
: [];
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Header title="Generations" />
|
||||
<DataTableToolbar
|
||||
filterColumnDefinition={observationsTableColsWithOptions(
|
||||
filterOptions.data,
|
||||
)}
|
||||
filterState={filterState}
|
||||
setFilterState={setFilterState}
|
||||
searchConfig={{
|
||||
placeholder: "Search by id, name, traceName, model",
|
||||
updateQuery: setSearchQuery,
|
||||
currentQuery: searchQuery ?? undefined,
|
||||
}}
|
||||
actionButtons={
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button
|
||||
variant="outline"
|
||||
className="ml-auto whitespace-nowrap"
|
||||
size="sm"
|
||||
>
|
||||
{filterState.length > 0 || searchQuery
|
||||
? "Export selection"
|
||||
: "Export all"}{" "}
|
||||
{isExporting ? (
|
||||
<Loader className="ml-2 h-4 w-4 animate-spin" />
|
||||
) : (
|
||||
<ChevronDownIcon className="ml-2 h-4 w-4" />
|
||||
)}
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end">
|
||||
{Object.entries(exportOptions).map(([key, options]) => (
|
||||
<DropdownMenuItem
|
||||
key={key}
|
||||
className="capitalize"
|
||||
onClick={() => void handleExport(key as ExportFileFormats)}
|
||||
>
|
||||
as {options.label}
|
||||
</DropdownMenuItem>
|
||||
))}
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
}
|
||||
/>
|
||||
<DataTable
|
||||
columns={columns}
|
||||
data={
|
||||
generations.isLoading
|
||||
? { isLoading: true, isError: false }
|
||||
: generations.isError
|
||||
? {
|
||||
isLoading: false,
|
||||
isError: true,
|
||||
error: generations.error.message,
|
||||
}
|
||||
: {
|
||||
isLoading: false,
|
||||
isError: false,
|
||||
data: rows,
|
||||
}
|
||||
}
|
||||
pagination={{
|
||||
pageCount: Math.ceil(totalCount / paginationState.pageSize),
|
||||
onChange: setPaginationState,
|
||||
state: paginationState,
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,12 +0,0 @@
|
||||
import { useRouter } from "next/router";
|
||||
import { TracePage } from "@/src/components/trace";
|
||||
|
||||
export default function PublicTracePage() {
|
||||
const router = useRouter();
|
||||
const traceId = router.query.traceId as string;
|
||||
return (
|
||||
<div className="py-5">
|
||||
<TracePage traceId={traceId} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,309 +0,0 @@
|
||||
import { z } from "zod";
|
||||
|
||||
import {
|
||||
createTRPCRouter,
|
||||
protectedProjectProcedure,
|
||||
} from "@/src/server/api/trpc";
|
||||
|
||||
import { type Observation, Prisma } from "@prisma/client";
|
||||
import { paginationZod } from "@/src/utils/zod";
|
||||
import { singleFilter } from "@/src/server/api/interfaces/filters";
|
||||
import {
|
||||
datetimeFilterToPrismaSql,
|
||||
filterToPrismaSql,
|
||||
} from "@/src/features/filters/server/filterToPrisma";
|
||||
import {
|
||||
type ObservationOptions,
|
||||
observationsTableCols,
|
||||
} from "@/src/server/api/definitions/observationsTable";
|
||||
|
||||
const exportFileFormats = ["CSV", "JSON", "OPENAI-JSONL"] as const;
|
||||
export type ExportFileFormats = (typeof exportFileFormats)[number];
|
||||
|
||||
const GenerationFilterOptions = z.object({
|
||||
projectId: z.string(), // Required for protectedProjectProcedure
|
||||
filter: z.array(singleFilter),
|
||||
searchQuery: z.string().nullable(),
|
||||
});
|
||||
|
||||
const ListInputs = GenerationFilterOptions.extend({
|
||||
...paginationZod,
|
||||
});
|
||||
|
||||
// extend generationfilteroptions with export options
|
||||
const ExportInputs = GenerationFilterOptions.extend({
|
||||
fileFormat: z.enum(exportFileFormats),
|
||||
});
|
||||
|
||||
export const generationsRouter = createTRPCRouter({
|
||||
all: protectedProjectProcedure
|
||||
.input(ListInputs)
|
||||
.query(async ({ input, ctx }) => {
|
||||
const searchCondition = input.searchQuery
|
||||
? Prisma.sql`AND (
|
||||
o."id" ILIKE ${`%${input.searchQuery}%`} OR
|
||||
o."name" ILIKE ${`%${input.searchQuery}%`} OR
|
||||
o."model" ILIKE ${`%${input.searchQuery}%`} OR
|
||||
t."name" ILIKE ${`%${input.searchQuery}%`}
|
||||
)`
|
||||
: Prisma.empty;
|
||||
|
||||
const filterCondition = filterToPrismaSql(
|
||||
input.filter ?? [],
|
||||
observationsTableCols,
|
||||
);
|
||||
|
||||
// to improve query performance, add timeseries filter to observation queries as well
|
||||
const startTimeFilter = input.filter?.find(
|
||||
(f) => f.column === "start_time" && f.type === "datetime",
|
||||
);
|
||||
const datetimeFilter =
|
||||
startTimeFilter && startTimeFilter.type === "datetime"
|
||||
? datetimeFilterToPrismaSql(
|
||||
"start_time",
|
||||
startTimeFilter.operator,
|
||||
startTimeFilter.value,
|
||||
)
|
||||
: Prisma.empty;
|
||||
|
||||
const generations = await ctx.prisma.$queryRaw<
|
||||
Array<
|
||||
Observation & {
|
||||
traceId: string;
|
||||
traceName: string;
|
||||
totalCount: number;
|
||||
latency: number | null;
|
||||
}
|
||||
>
|
||||
>(
|
||||
Prisma.sql`
|
||||
WITH observations_with_latency AS (
|
||||
SELECT
|
||||
o.*,
|
||||
CASE WHEN o.end_time IS NULL THEN NULL ELSE (EXTRACT(EPOCH FROM o."end_time") - EXTRACT(EPOCH FROM o."start_time"))::double precision END AS "latency"
|
||||
FROM observations o
|
||||
WHERE o.type = 'GENERATION'
|
||||
AND o.project_id = ${input.projectId}
|
||||
${datetimeFilter}
|
||||
)
|
||||
SELECT
|
||||
o.id,
|
||||
o.name,
|
||||
o.model,
|
||||
o.start_time as "startTime",
|
||||
o.end_time as "endTime",
|
||||
o.latency,
|
||||
o.input,
|
||||
o.output,
|
||||
o.metadata,
|
||||
o.trace_id as "traceId",
|
||||
t.name as "traceName",
|
||||
o.completion_start_time as "completionStartTime",
|
||||
o.prompt_tokens as "promptTokens",
|
||||
o.completion_tokens as "completionTokens",
|
||||
o.total_tokens as "totalTokens",
|
||||
o.version,
|
||||
(count(*) OVER())::int AS "totalCount"
|
||||
FROM observations_with_latency o
|
||||
JOIN traces t ON t.id = o.trace_id
|
||||
WHERE
|
||||
t.project_id = ${input.projectId}
|
||||
${searchCondition}
|
||||
${filterCondition}
|
||||
ORDER BY o.start_time DESC
|
||||
LIMIT ${input.limit}
|
||||
OFFSET ${input.page * input.limit}
|
||||
`,
|
||||
);
|
||||
|
||||
return generations;
|
||||
}),
|
||||
|
||||
export: protectedProjectProcedure
|
||||
.input(ExportInputs)
|
||||
.query(async ({ input, ctx }) => {
|
||||
const searchCondition = input.searchQuery
|
||||
? Prisma.sql`AND (
|
||||
o."id" ILIKE ${`%${input.searchQuery}%`} OR
|
||||
o."name" ILIKE ${`%${input.searchQuery}%`} OR
|
||||
o."model" ILIKE ${`%${input.searchQuery}%`} OR
|
||||
t."name" ILIKE ${`%${input.searchQuery}%`}
|
||||
)`
|
||||
: Prisma.empty;
|
||||
|
||||
const filterCondition = filterToPrismaSql(
|
||||
input.filter ?? [],
|
||||
observationsTableCols,
|
||||
);
|
||||
console.log("filters: ", filterCondition);
|
||||
|
||||
const generations = await ctx.prisma.$queryRaw<
|
||||
Array<
|
||||
Observation & {
|
||||
traceId: string;
|
||||
traceName: string;
|
||||
}
|
||||
>
|
||||
>(
|
||||
Prisma.sql`
|
||||
SELECT
|
||||
o.id,
|
||||
o.name,
|
||||
o.model,
|
||||
o.start_time as "startTime",
|
||||
o.end_time as "endTime",
|
||||
o.input,
|
||||
o.output,
|
||||
o.metadata,
|
||||
o.trace_id as "traceId",
|
||||
t.name as "traceName",
|
||||
o.completion_start_time as "completionStartTime",
|
||||
o.prompt_tokens as "promptTokens",
|
||||
o.completion_tokens as "completionTokens",
|
||||
o.total_tokens as "totalTokens",
|
||||
o.version
|
||||
FROM observations o
|
||||
JOIN traces t ON t.id = o.trace_id
|
||||
WHERE o.type = 'GENERATION'
|
||||
AND o.project_id = ${input.projectId}
|
||||
AND t.project_id = ${input.projectId}
|
||||
${searchCondition}
|
||||
${filterCondition}
|
||||
ORDER BY o.start_time DESC
|
||||
`,
|
||||
);
|
||||
|
||||
// create csv
|
||||
switch (input.fileFormat) {
|
||||
case "CSV":
|
||||
return [
|
||||
[
|
||||
"traceId",
|
||||
"name",
|
||||
"model",
|
||||
"startTime",
|
||||
"endTime",
|
||||
"prompt",
|
||||
"completion",
|
||||
"metadata",
|
||||
],
|
||||
]
|
||||
.concat(
|
||||
generations.map((generation) =>
|
||||
[
|
||||
generation.traceId,
|
||||
generation.name ?? "",
|
||||
generation.model ?? "",
|
||||
generation.startTime.toISOString(),
|
||||
generation.endTime?.toISOString() ?? "",
|
||||
JSON.stringify(generation.input),
|
||||
JSON.stringify(generation.output),
|
||||
JSON.stringify(generation.metadata),
|
||||
].map((field) => {
|
||||
const str = typeof field === "string" ? field : String(field);
|
||||
return `"${str.replace(/"/g, '""')}"`;
|
||||
}),
|
||||
),
|
||||
)
|
||||
.map((row) => row.join(","))
|
||||
.join("\n");
|
||||
case "JSON":
|
||||
return JSON.stringify(generations);
|
||||
case "OPENAI-JSONL":
|
||||
const inputSchemaOpenAI = z.array(
|
||||
z.object({
|
||||
role: z.enum(["system", "user", "assistant"]),
|
||||
content: z.string(),
|
||||
}),
|
||||
);
|
||||
const outputSchema = z.object({
|
||||
completion: z.string(),
|
||||
});
|
||||
|
||||
return (
|
||||
generations
|
||||
.map((generation) => ({
|
||||
parsedInput: inputSchemaOpenAI.safeParse(generation.input),
|
||||
parsedOutput: outputSchema.safeParse(generation.output),
|
||||
}))
|
||||
.filter((generation) => generation.parsedInput.success)
|
||||
.map((generation) =>
|
||||
generation.parsedInput.success // check for typescript validation, is always true due to previous filter
|
||||
? generation.parsedInput.data.concat(
|
||||
generation.parsedOutput.success
|
||||
? [
|
||||
{
|
||||
role: "assistant",
|
||||
content: generation.parsedOutput.data.completion,
|
||||
},
|
||||
]
|
||||
: [],
|
||||
)
|
||||
: [],
|
||||
)
|
||||
// to jsonl
|
||||
.map((row) => JSON.stringify(row))
|
||||
.join("\n")
|
||||
);
|
||||
}
|
||||
}),
|
||||
filterOptions: protectedProjectProcedure
|
||||
.input(z.object({ projectId: z.string() }))
|
||||
.query(async ({ input, ctx }) => {
|
||||
const queryFilter = {
|
||||
projectId: input.projectId,
|
||||
type: "GENERATION",
|
||||
} as const;
|
||||
|
||||
const [model, name, traceName] = await Promise.all([
|
||||
ctx.prisma.observation.groupBy({
|
||||
by: ["model"],
|
||||
where: queryFilter,
|
||||
_count: { _all: true },
|
||||
}),
|
||||
ctx.prisma.observation.groupBy({
|
||||
by: ["name"],
|
||||
where: queryFilter,
|
||||
_count: { _all: true },
|
||||
}),
|
||||
ctx.prisma.$queryRaw<
|
||||
Array<{
|
||||
traceName: string | null;
|
||||
count: number;
|
||||
}>
|
||||
>(Prisma.sql`
|
||||
SELECT
|
||||
t.name "traceName",
|
||||
count(*)::int AS count
|
||||
FROM traces t
|
||||
JOIN observations o ON o.trace_id = t.id
|
||||
WHERE o.type = 'GENERATION'
|
||||
AND o.project_id = ${input.projectId}
|
||||
AND t.project_id = ${input.projectId}
|
||||
GROUP BY 1
|
||||
`),
|
||||
]);
|
||||
// typecheck filter options, needs to include all columns with options
|
||||
const res: ObservationOptions = {
|
||||
model: model
|
||||
.filter((i) => i.model !== null)
|
||||
.map((i) => ({
|
||||
value: i.model as string,
|
||||
count: i._count._all,
|
||||
})),
|
||||
name: name
|
||||
.filter((i) => i.name !== null)
|
||||
.map((i) => ({
|
||||
value: i.name as string,
|
||||
count: i._count._all,
|
||||
})),
|
||||
traceName: traceName
|
||||
.filter((i) => i.traceName !== null)
|
||||
.map((i) => ({
|
||||
value: i.traceName as string,
|
||||
count: i.count,
|
||||
})),
|
||||
};
|
||||
return res;
|
||||
}),
|
||||
});
|
||||
@@ -1,346 +0,0 @@
|
||||
import { z } from "zod";
|
||||
|
||||
import {
|
||||
createTRPCRouter,
|
||||
protectedGetTraceProcedure,
|
||||
protectedProjectProcedure,
|
||||
} from "@/src/server/api/trpc";
|
||||
import {
|
||||
type Observation,
|
||||
Prisma,
|
||||
type Score,
|
||||
type Trace,
|
||||
} from "@prisma/client";
|
||||
import { calculateTokenCost } from "@/src/features/ingest/lib/usage";
|
||||
import Decimal from "decimal.js";
|
||||
import { paginationZod } from "@/src/utils/zod";
|
||||
import { singleFilter } from "@/src/server/api/interfaces/filters";
|
||||
import {
|
||||
type TraceOptions,
|
||||
tracesTableCols,
|
||||
} from "@/src/server/api/definitions/tracesTable";
|
||||
import {
|
||||
datetimeFilterToPrismaSql,
|
||||
filterToPrismaSql,
|
||||
} from "@/src/features/filters/server/filterToPrisma";
|
||||
import { throwIfNoAccess } from "@/src/features/rbac/utils/checkAccess";
|
||||
|
||||
const TraceFilterOptions = z.object({
|
||||
projectId: z.string(), // Required for protectedProjectProcedure
|
||||
searchQuery: z.string().nullable(),
|
||||
filter: z.array(singleFilter).nullable(),
|
||||
...paginationZod,
|
||||
});
|
||||
|
||||
export type ObservationReturnType = Omit<Observation, "input" | "output"> & {
|
||||
traceId: string;
|
||||
} & { price?: Decimal };
|
||||
|
||||
export const traceRouter = createTRPCRouter({
|
||||
all: protectedProjectProcedure
|
||||
.input(TraceFilterOptions)
|
||||
.query(async ({ input, ctx }) => {
|
||||
const filterCondition = filterToPrismaSql(
|
||||
input.filter ?? [],
|
||||
tracesTableCols,
|
||||
);
|
||||
|
||||
// to improve query performance, add timeseries filter to observation queries as well
|
||||
const timeseriesFilter = input.filter?.find(
|
||||
(f) => f.column === "timestamp" && f.type === "datetime",
|
||||
);
|
||||
const observationTimeseriesFilter =
|
||||
timeseriesFilter && timeseriesFilter.type === "datetime"
|
||||
? datetimeFilterToPrismaSql(
|
||||
"start_time",
|
||||
timeseriesFilter.operator,
|
||||
timeseriesFilter.value,
|
||||
)
|
||||
: Prisma.empty;
|
||||
|
||||
const searchCondition = input.searchQuery
|
||||
? Prisma.sql`AND (
|
||||
t."id" ILIKE ${`%${input.searchQuery}%`} OR
|
||||
t."external_id" ILIKE ${`%${input.searchQuery}%`} OR
|
||||
t."user_id" ILIKE ${`%${input.searchQuery}%`} OR
|
||||
t."name" ILIKE ${`%${input.searchQuery}%`}
|
||||
)`
|
||||
: Prisma.empty;
|
||||
|
||||
const traces = await ctx.prisma.$queryRaw<
|
||||
Array<
|
||||
Trace & {
|
||||
promptTokens: number;
|
||||
completionTokens: number;
|
||||
totalTokens: number;
|
||||
totalCount: number;
|
||||
latency: number | null;
|
||||
scores: Score[];
|
||||
}
|
||||
>
|
||||
>(Prisma.sql`
|
||||
WITH usage AS (
|
||||
SELECT
|
||||
trace_id,
|
||||
sum(prompt_tokens) AS "promptTokens",
|
||||
sum(completion_tokens) AS "completionTokens",
|
||||
sum(total_tokens) AS "totalTokens"
|
||||
FROM
|
||||
"observations"
|
||||
WHERE
|
||||
"trace_id" IS NOT NULL
|
||||
AND "type" = 'GENERATION'
|
||||
AND "project_id" = ${input.projectId}
|
||||
${observationTimeseriesFilter}
|
||||
GROUP BY
|
||||
trace_id
|
||||
),
|
||||
trace_latency AS (
|
||||
SELECT
|
||||
trace_id,
|
||||
EXTRACT(EPOCH FROM COALESCE(MAX("end_time"), MAX("start_time"))) * 1000 - EXTRACT(EPOCH FROM MIN("start_time")) * 1000 AS "latency"
|
||||
FROM
|
||||
"observations"
|
||||
WHERE
|
||||
"trace_id" IS NOT NULL
|
||||
AND "project_id" = ${input.projectId}
|
||||
${observationTimeseriesFilter}
|
||||
GROUP BY
|
||||
trace_id
|
||||
),
|
||||
-- used for filtering
|
||||
scores_avg AS (
|
||||
SELECT
|
||||
trace_id,
|
||||
jsonb_object_agg(name::text, avg_value::double precision) AS scores_avg
|
||||
FROM (
|
||||
SELECT
|
||||
trace_id,
|
||||
name,
|
||||
avg(value) avg_value
|
||||
FROM
|
||||
scores
|
||||
GROUP BY
|
||||
1,
|
||||
2
|
||||
ORDER BY
|
||||
1) tmp
|
||||
GROUP BY
|
||||
1
|
||||
),
|
||||
scores_json AS (
|
||||
SELECT
|
||||
trace_id,
|
||||
json_agg(json_build_object('id',
|
||||
"id",
|
||||
'timestamp',
|
||||
"timestamp",
|
||||
'name',
|
||||
"name",
|
||||
'value',
|
||||
"value",
|
||||
'traceId',
|
||||
"trace_id",
|
||||
'observationId',
|
||||
"observation_id",
|
||||
'comment',
|
||||
"comment")) AS scores
|
||||
FROM
|
||||
scores
|
||||
GROUP BY
|
||||
1
|
||||
)
|
||||
SELECT
|
||||
t.*,
|
||||
t."user_id" AS "userId",
|
||||
t."metadata" AS "metadata",
|
||||
t."bookmarked" AS "bookmarked",
|
||||
COALESCE(u."promptTokens", 0)::int AS "promptTokens",
|
||||
COALESCE(u."completionTokens", 0)::int AS "completionTokens",
|
||||
COALESCE(u."totalTokens", 0)::int AS "totalTokens",
|
||||
COALESCE(s_json.scores, '[]'::json) AS "scores",
|
||||
tl.latency/1000::double precision AS "latency",
|
||||
(count(*) OVER ())::int AS "totalCount"
|
||||
FROM
|
||||
"traces" AS t
|
||||
LEFT JOIN usage AS u ON u.trace_id = t.id
|
||||
-- used for filtering
|
||||
LEFT JOIN scores_avg AS s_avg ON s_avg.trace_id = t.id
|
||||
LEFT JOIN scores_json AS s_json ON s_json.trace_id = t.id
|
||||
LEFT JOIN trace_latency AS tl ON tl.trace_id = t.id
|
||||
WHERE
|
||||
t."project_id" = ${input.projectId}
|
||||
${searchCondition}
|
||||
${filterCondition}
|
||||
ORDER BY
|
||||
t."timestamp" DESC
|
||||
LIMIT ${input.limit}
|
||||
OFFSET ${input.page * input.limit}
|
||||
`);
|
||||
return traces;
|
||||
}),
|
||||
filterOptions: protectedProjectProcedure
|
||||
.input(z.object({ projectId: z.string() }))
|
||||
.query(async ({ input, ctx }) => {
|
||||
const [scores, names] = await Promise.all([
|
||||
ctx.prisma.score.groupBy({
|
||||
where: {
|
||||
trace: {
|
||||
projectId: input.projectId,
|
||||
},
|
||||
},
|
||||
by: ["name"],
|
||||
}),
|
||||
ctx.prisma.trace.groupBy({
|
||||
where: {
|
||||
projectId: input.projectId,
|
||||
},
|
||||
by: ["name"],
|
||||
_count: {
|
||||
_all: true,
|
||||
},
|
||||
}),
|
||||
]);
|
||||
const res: TraceOptions = {
|
||||
scores_avg: scores.map((score) => score.name),
|
||||
name: names
|
||||
.filter((n) => n.name !== null)
|
||||
.map((name) => ({
|
||||
value: name.name ?? "undefined",
|
||||
count: name._count._all,
|
||||
})),
|
||||
};
|
||||
return res;
|
||||
}),
|
||||
byId: protectedGetTraceProcedure
|
||||
.input(z.object({ traceId: z.string() }))
|
||||
.query(async ({ input, ctx }) => {
|
||||
const [trace, observations, pricings] = await Promise.all([
|
||||
ctx.prisma.trace.findFirstOrThrow({
|
||||
where: {
|
||||
id: input.traceId,
|
||||
},
|
||||
include: {
|
||||
scores: true,
|
||||
},
|
||||
}),
|
||||
ctx.prisma.observation.findMany({
|
||||
where: {
|
||||
traceId: {
|
||||
equals: input.traceId,
|
||||
not: null,
|
||||
},
|
||||
},
|
||||
}),
|
||||
ctx.prisma.pricing.findMany(),
|
||||
]);
|
||||
|
||||
const obsStartTimes = observations
|
||||
.map((o) => o.startTime)
|
||||
.sort((a, b) => a.getTime() - b.getTime());
|
||||
const obsEndTimes = observations
|
||||
.map((o) => o.endTime)
|
||||
.filter((t) => t)
|
||||
.sort((a, b) => (a as Date).getTime() - (b as Date).getTime());
|
||||
const latencyMs =
|
||||
obsStartTimes.length > 0
|
||||
? obsEndTimes.length > 0
|
||||
? (obsEndTimes[obsEndTimes.length - 1] as Date).getTime() -
|
||||
obsStartTimes[0]!.getTime()
|
||||
: obsStartTimes.length > 1
|
||||
? obsStartTimes[obsStartTimes.length - 1]!.getTime() -
|
||||
obsStartTimes[0]!.getTime()
|
||||
: undefined
|
||||
: undefined;
|
||||
|
||||
const enrichedObservations = observations.map(
|
||||
({ input, output, ...rest }) => {
|
||||
return {
|
||||
...rest,
|
||||
price: rest.model
|
||||
? calculateTokenCost(pricings, {
|
||||
model: rest.model,
|
||||
totalTokens: new Decimal(rest.totalTokens),
|
||||
promptTokens: new Decimal(rest.promptTokens),
|
||||
completionTokens: new Decimal(rest.completionTokens),
|
||||
input: input,
|
||||
output: output,
|
||||
})
|
||||
: undefined,
|
||||
};
|
||||
},
|
||||
);
|
||||
|
||||
return {
|
||||
...trace,
|
||||
latency: latencyMs !== undefined ? latencyMs / 1000 : undefined,
|
||||
observations: enrichedObservations as ObservationReturnType[],
|
||||
};
|
||||
}),
|
||||
delete: protectedProjectProcedure
|
||||
.input(z.object({ traceId: z.string(), projectId: z.string() }))
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
throwIfNoAccess({
|
||||
session: ctx.session,
|
||||
projectId: input.projectId,
|
||||
scope: "traces:delete",
|
||||
});
|
||||
|
||||
const trace = await ctx.prisma.trace.findFirst({
|
||||
where: {
|
||||
id: input.traceId,
|
||||
projectId: input.projectId,
|
||||
},
|
||||
});
|
||||
|
||||
if (!trace) {
|
||||
throw new Error("Trace not found in project");
|
||||
}
|
||||
|
||||
return ctx.prisma.$transaction([
|
||||
ctx.prisma.trace.delete({
|
||||
where: {
|
||||
id: input.traceId,
|
||||
},
|
||||
}),
|
||||
ctx.prisma.observation.deleteMany({
|
||||
where: {
|
||||
traceId: input.traceId,
|
||||
},
|
||||
}),
|
||||
]);
|
||||
}),
|
||||
bookmark: protectedProjectProcedure
|
||||
.input(
|
||||
z.object({
|
||||
traceId: z.string(),
|
||||
projectId: z.string(),
|
||||
bookmarked: z.boolean(),
|
||||
}),
|
||||
)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
throwIfNoAccess({
|
||||
session: ctx.session,
|
||||
projectId: input.projectId,
|
||||
scope: "traces:bookmark",
|
||||
});
|
||||
const trace = await ctx.prisma.trace.findFirst({
|
||||
where: {
|
||||
id: input.traceId,
|
||||
projectId: input.projectId,
|
||||
},
|
||||
});
|
||||
if (!trace) {
|
||||
throw new Error("Trace not found in project");
|
||||
}
|
||||
|
||||
return ctx.prisma.trace.update({
|
||||
where: {
|
||||
id: input.traceId,
|
||||
},
|
||||
data: {
|
||||
bookmarked: input.bookmarked,
|
||||
},
|
||||
});
|
||||
}),
|
||||
});
|
||||
@@ -1,327 +0,0 @@
|
||||
import { tokenCount } from "@/src/features/ingest/lib/usage";
|
||||
import { checkApiAccessScope } from "@/src/features/public-api/server/apiScope";
|
||||
import { type ApiAccessScope } from "@/src/features/public-api/server/types";
|
||||
import { AuthenticationError } from "@/src/pages/api/public/ingestion";
|
||||
import {
|
||||
type observationEvent,
|
||||
eventTypes,
|
||||
type traceEvent,
|
||||
type scoreEvent,
|
||||
type observationUpdateEvent,
|
||||
} from "@/src/features/public-api/server/ingestion-api-schema";
|
||||
import { prisma } from "@/src/server/db";
|
||||
import { ResourceNotFoundError } from "@/src/utils/exceptions";
|
||||
import { mergeJson } from "@/src/utils/json";
|
||||
import {
|
||||
type Trace,
|
||||
type Observation,
|
||||
type Score,
|
||||
type Prisma,
|
||||
} from "@prisma/client";
|
||||
import { v4 } from "uuid";
|
||||
import { type z } from "zod";
|
||||
import { jsonSchema } from "@/src/utils/zod";
|
||||
|
||||
export interface EventProcessor {
|
||||
process(apiScope: ApiAccessScope): Promise<Trace | Observation | Score>;
|
||||
}
|
||||
export class ObservationProcessor implements EventProcessor {
|
||||
event:
|
||||
| z.infer<typeof observationEvent>
|
||||
| z.infer<typeof observationUpdateEvent>;
|
||||
|
||||
constructor(
|
||||
event:
|
||||
| z.infer<typeof observationEvent>
|
||||
| z.infer<typeof observationUpdateEvent>,
|
||||
) {
|
||||
this.event = event;
|
||||
}
|
||||
|
||||
async convertToObservation(apiScope: ApiAccessScope): Promise<{
|
||||
id: string;
|
||||
create: Prisma.ObservationCreateInput;
|
||||
update: Prisma.ObservationUpdateInput;
|
||||
}> {
|
||||
const { body } = this.event;
|
||||
|
||||
const {
|
||||
id,
|
||||
traceId,
|
||||
type,
|
||||
name,
|
||||
startTime,
|
||||
endTime,
|
||||
completionStartTime,
|
||||
model,
|
||||
modelParameters,
|
||||
input,
|
||||
output,
|
||||
usage,
|
||||
metadata,
|
||||
parentObservationId,
|
||||
level,
|
||||
statusMessage,
|
||||
version,
|
||||
} = body;
|
||||
|
||||
const existingObservation = id
|
||||
? await prisma.observation.findUnique({
|
||||
where: { id, projectId: apiScope.projectId },
|
||||
})
|
||||
: null;
|
||||
|
||||
if (
|
||||
this.event.type === eventTypes.OBSERVAION_UPDATE &&
|
||||
!existingObservation
|
||||
) {
|
||||
throw new ResourceNotFoundError(this.event.id, "Observation not found");
|
||||
}
|
||||
|
||||
const finalTraceId =
|
||||
!traceId && !existingObservation
|
||||
? // Create trace if no traceid
|
||||
(
|
||||
await prisma.trace.create({
|
||||
data: {
|
||||
projectId: apiScope.projectId,
|
||||
name: name,
|
||||
},
|
||||
})
|
||||
).id
|
||||
: traceId;
|
||||
|
||||
const [newPromptTokens, newCompletionTokens] = this.calculateTokenCounts(
|
||||
body,
|
||||
existingObservation ?? undefined,
|
||||
);
|
||||
|
||||
// merge metadata from existingObservation.metadata and metadata
|
||||
const mergedMetadata = mergeJson(
|
||||
existingObservation?.metadata
|
||||
? jsonSchema.parse(existingObservation.metadata)
|
||||
: undefined,
|
||||
metadata ?? undefined,
|
||||
);
|
||||
|
||||
const observationId = id ?? v4();
|
||||
return {
|
||||
id: observationId,
|
||||
create: {
|
||||
id: observationId,
|
||||
traceId: finalTraceId,
|
||||
type: type,
|
||||
name: name,
|
||||
startTime: startTime ? new Date(startTime) : undefined,
|
||||
endTime: endTime ? new Date(endTime) : undefined,
|
||||
completionStartTime: completionStartTime
|
||||
? new Date(completionStartTime)
|
||||
: undefined,
|
||||
metadata: mergedMetadata ?? metadata ?? undefined,
|
||||
model: model ?? undefined,
|
||||
modelParameters: modelParameters ?? undefined,
|
||||
input: input ?? undefined,
|
||||
output: output ?? undefined,
|
||||
promptTokens: newPromptTokens,
|
||||
completionTokens: newCompletionTokens,
|
||||
totalTokens:
|
||||
usage?.totalTokens ??
|
||||
(newPromptTokens ?? 0) + (newCompletionTokens ?? 0),
|
||||
level: level ?? undefined,
|
||||
statusMessage: statusMessage ?? undefined,
|
||||
parentObservationId: parentObservationId ?? undefined,
|
||||
version: version ?? undefined,
|
||||
project: { connect: { id: apiScope.projectId } },
|
||||
},
|
||||
update: {
|
||||
type: type,
|
||||
name,
|
||||
startTime: startTime ? new Date(startTime) : undefined,
|
||||
endTime: endTime ? new Date(endTime) : undefined,
|
||||
completionStartTime: completionStartTime
|
||||
? new Date(completionStartTime)
|
||||
: undefined,
|
||||
metadata: mergedMetadata ?? metadata ?? undefined,
|
||||
model: model ?? undefined,
|
||||
modelParameters: modelParameters ?? undefined,
|
||||
input: input ?? undefined,
|
||||
output: output ?? undefined,
|
||||
promptTokens: newPromptTokens,
|
||||
completionTokens: newCompletionTokens,
|
||||
totalTokens:
|
||||
usage?.totalTokens ??
|
||||
(newPromptTokens ?? 0) + (newCompletionTokens ?? 0),
|
||||
level: level ?? undefined,
|
||||
statusMessage: statusMessage ?? undefined,
|
||||
parentObservationId: parentObservationId ?? undefined,
|
||||
version: version ?? undefined,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
calculateTokenCounts(
|
||||
body: z.infer<typeof observationEvent>["body"],
|
||||
existingObservation?: Observation,
|
||||
) {
|
||||
const mergedModel = body.model ?? existingObservation?.model;
|
||||
|
||||
const newPromptTokens =
|
||||
body.usage?.promptTokens ??
|
||||
((body.input || existingObservation?.input) && mergedModel
|
||||
? tokenCount({
|
||||
model: mergedModel,
|
||||
text: body.input ?? existingObservation?.input,
|
||||
})
|
||||
: undefined);
|
||||
|
||||
const newCompletionTokens =
|
||||
body.usage?.completionTokens ??
|
||||
((body.output || existingObservation?.output) && mergedModel
|
||||
? tokenCount({
|
||||
model: mergedModel,
|
||||
text: body.output ?? existingObservation?.output,
|
||||
})
|
||||
: undefined);
|
||||
return [newPromptTokens, newCompletionTokens];
|
||||
}
|
||||
|
||||
async process(
|
||||
apiScope: ApiAccessScope,
|
||||
): Promise<Trace | Observation | Score> {
|
||||
if (apiScope.accessLevel !== "all")
|
||||
throw new AuthenticationError("Access denied for observation creation");
|
||||
|
||||
const obs = await this.convertToObservation(apiScope);
|
||||
|
||||
return await prisma.observation.upsert({
|
||||
where: {
|
||||
id_projectId: {
|
||||
id: obs.id,
|
||||
projectId: apiScope.projectId,
|
||||
},
|
||||
},
|
||||
create: obs.create,
|
||||
update: obs.update,
|
||||
});
|
||||
}
|
||||
}
|
||||
export class TraceProcessor implements EventProcessor {
|
||||
event: z.infer<typeof traceEvent>;
|
||||
|
||||
constructor(event: z.infer<typeof traceEvent>) {
|
||||
this.event = event;
|
||||
}
|
||||
|
||||
async process(
|
||||
apiScope: ApiAccessScope,
|
||||
): Promise<Trace | Observation | Score> {
|
||||
const { body } = this.event;
|
||||
|
||||
if (apiScope.accessLevel !== "all")
|
||||
throw new AuthenticationError("Access denied for trace creation");
|
||||
|
||||
const internalId = body.id ?? v4();
|
||||
|
||||
console.log(
|
||||
"Trying to create trace, project ",
|
||||
apiScope.projectId,
|
||||
", body:",
|
||||
body,
|
||||
);
|
||||
|
||||
const existingTrace = await prisma.trace.findUnique({
|
||||
where: {
|
||||
id: internalId,
|
||||
},
|
||||
});
|
||||
|
||||
const mergedMetadata = mergeJson(
|
||||
existingTrace?.metadata
|
||||
? jsonSchema.parse(existingTrace.metadata)
|
||||
: undefined,
|
||||
body.metadata ?? undefined,
|
||||
);
|
||||
|
||||
const upsertedTrace = await prisma.trace.upsert({
|
||||
where: {
|
||||
id: internalId,
|
||||
projectId: apiScope.projectId,
|
||||
},
|
||||
create: {
|
||||
id: internalId,
|
||||
name: body.name ?? undefined,
|
||||
userId: body.userId ?? undefined,
|
||||
metadata: mergedMetadata ?? body.metadata ?? undefined,
|
||||
release: body.release ?? undefined,
|
||||
version: body.version ?? undefined,
|
||||
project: { connect: { id: apiScope.projectId } },
|
||||
},
|
||||
update: {
|
||||
name: body.name ?? undefined,
|
||||
userId: body.userId ?? undefined,
|
||||
metadata: mergedMetadata ?? body.metadata ?? undefined,
|
||||
release: body.release ?? undefined,
|
||||
version: body.version ?? undefined,
|
||||
},
|
||||
});
|
||||
return upsertedTrace;
|
||||
}
|
||||
}
|
||||
export class ScoreProcessor implements EventProcessor {
|
||||
event: z.infer<typeof scoreEvent>;
|
||||
|
||||
constructor(event: z.infer<typeof scoreEvent>) {
|
||||
this.event = event;
|
||||
}
|
||||
|
||||
async process(
|
||||
apiScope: ApiAccessScope,
|
||||
): Promise<Trace | Observation | Score> {
|
||||
const { body } = this.event;
|
||||
|
||||
const accessCheck = await checkApiAccessScope(
|
||||
apiScope,
|
||||
[
|
||||
{ type: "trace", id: body.traceId },
|
||||
...(body.observationId
|
||||
? [{ type: "observation" as const, id: body.observationId }]
|
||||
: []),
|
||||
],
|
||||
"score",
|
||||
);
|
||||
if (!accessCheck)
|
||||
throw new AuthenticationError("Access denied for score creation");
|
||||
|
||||
const id = body.id ?? v4();
|
||||
|
||||
// access control via traceId
|
||||
return await prisma.score.upsert({
|
||||
where: {
|
||||
id_traceId: {
|
||||
id,
|
||||
traceId: body.traceId,
|
||||
},
|
||||
},
|
||||
create: {
|
||||
id,
|
||||
trace: { connect: { id: body.traceId } },
|
||||
timestamp: new Date(),
|
||||
value: body.value,
|
||||
name: body.name,
|
||||
comment: body.comment,
|
||||
...(body.observationId && {
|
||||
observation: { connect: { id: body.observationId } },
|
||||
}),
|
||||
},
|
||||
update: {
|
||||
timestamp: new Date(),
|
||||
value: body.value,
|
||||
name: body.name,
|
||||
comment: body.comment,
|
||||
...(body.observationId && {
|
||||
observation: { connect: { id: body.observationId } },
|
||||
}),
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -1,16 +0,0 @@
|
||||
import { PrismaClient } from "@prisma/client";
|
||||
|
||||
import { env } from "@/src/env.mjs";
|
||||
|
||||
const globalForPrisma = globalThis as unknown as {
|
||||
prisma: PrismaClient | undefined;
|
||||
};
|
||||
|
||||
export const prisma =
|
||||
globalForPrisma.prisma ??
|
||||
new PrismaClient({
|
||||
log:
|
||||
env.NODE_ENV === "development" ? ["query", "error", "warn"] : ["error"],
|
||||
});
|
||||
|
||||
if (env.NODE_ENV !== "production") globalForPrisma.prisma = prisma;
|
||||
@@ -1,8 +0,0 @@
|
||||
export class ResourceNotFoundError extends Error {
|
||||
id: string;
|
||||
constructor(type: string, id: string) {
|
||||
super(`${type} with ${id} not found}`);
|
||||
this.name = "ResourceNotFoundError";
|
||||
this.id = id;
|
||||
}
|
||||
}
|
||||
@@ -1,25 +0,0 @@
|
||||
export const compactNumberFormatter = (number: number) => {
|
||||
return Intl.NumberFormat("us", {
|
||||
notation: "compact",
|
||||
compactDisplay: "short",
|
||||
maximumFractionDigits: 2,
|
||||
}).format(number);
|
||||
};
|
||||
|
||||
export const numberFormatter = (number: number) => {
|
||||
return Intl.NumberFormat("us", {
|
||||
notation: "standard",
|
||||
minimumFractionDigits: 2,
|
||||
maximumFractionDigits: 2,
|
||||
}).format(number);
|
||||
};
|
||||
|
||||
export const usdFormatter = (number: number) =>
|
||||
new Intl.NumberFormat("en-US", {
|
||||
style: "currency",
|
||||
currency: "USD",
|
||||
|
||||
// These options are needed to round to whole numbers if that's what you want.
|
||||
//minimumFractionDigits: 0, // (this suffices for whole numbers, but will print 2500.10 as $2,500.1)
|
||||
//maximumFractionDigits: 0, // (causes 2500.99 to be printed as $2,501)
|
||||
}).format(number);
|
||||
@@ -1,3 +0,0 @@
|
||||
export function lastCharacters(str: string, n: number) {
|
||||
return str.substring(str.length - n);
|
||||
}
|
||||
@@ -1,54 +0,0 @@
|
||||
import { type Observation } from "@prisma/client";
|
||||
import { type inferRouterInputs, type inferRouterOutputs } from "@trpc/server";
|
||||
import { type AppRouter } from "@/src/server/api/root";
|
||||
import {
|
||||
type DateTimeAggregationOption,
|
||||
dateTimeAggregationOptions,
|
||||
} from "@/src/features/dashboard/lib/timeseries-aggregation";
|
||||
import { type ObservationReturnType } from "@/src/server/api/routers/traces";
|
||||
|
||||
export type NestedObservation = ObservationReturnType & {
|
||||
children: NestedObservation[];
|
||||
};
|
||||
|
||||
export type TypedObservation = Event | Span | Generation;
|
||||
|
||||
export type Event = Observation & {
|
||||
type: "EVENT";
|
||||
};
|
||||
|
||||
export type Span = Observation & {
|
||||
type: "SPAN";
|
||||
endTime: Date; // not null
|
||||
};
|
||||
|
||||
export type LLMChatMessages = { role: string; content: string };
|
||||
|
||||
export type Generation = Observation & {
|
||||
type: "GENERATION";
|
||||
promptTokens: number;
|
||||
completionTokens: number;
|
||||
totalTokens: number;
|
||||
modelParameters: {
|
||||
[key: string]: string | number | boolean;
|
||||
};
|
||||
};
|
||||
|
||||
export type RouterInput = inferRouterInputs<AppRouter>;
|
||||
export type RouterOutput = inferRouterOutputs<AppRouter>;
|
||||
|
||||
export const isUndefinedOrNull = <T>(val?: T | null): val is undefined | null =>
|
||||
val === undefined || val === null;
|
||||
|
||||
export const isNotNullOrUndefined = <T>(
|
||||
val?: T | null,
|
||||
): val is Exclude<T, null | undefined> => !isUndefinedOrNull(val);
|
||||
|
||||
export function isValidOption(
|
||||
value: unknown,
|
||||
): value is DateTimeAggregationOption {
|
||||
return (
|
||||
typeof value === "string" &&
|
||||
dateTimeAggregationOptions.includes(value as DateTimeAggregationOption)
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
Dockerfile
|
||||
.dockerignore
|
||||
node_modules
|
||||
npm-debug.log
|
||||
README.md
|
||||
.next
|
||||
.git
|
||||
@@ -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,7 +35,13 @@ const config = {
|
||||
},
|
||||
],
|
||||
"@typescript-eslint/no-unused-vars": ["warn", { argsIgnorePattern: "^_" }],
|
||||
"react/jsx-key": [
|
||||
"error",
|
||||
{
|
||||
warnOnDuplicates: true,
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
module.exports = config;
|
||||
module.exports = config;
|
||||
@@ -0,0 +1,55 @@
|
||||
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
|
||||
|
||||
# dependencies
|
||||
/node_modules
|
||||
/.pnp
|
||||
.pnp.js
|
||||
|
||||
# testing
|
||||
/coverage
|
||||
|
||||
# database
|
||||
/prisma/db.sqlite
|
||||
/prisma/db.sqlite-journal
|
||||
|
||||
# next.js
|
||||
/.next/
|
||||
/out/
|
||||
next-env.d.ts
|
||||
|
||||
# production
|
||||
/build
|
||||
|
||||
# idea
|
||||
.idea
|
||||
|
||||
# misc
|
||||
.DS_Store
|
||||
*.pem
|
||||
|
||||
# debug
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
.pnpm-debug.log*
|
||||
|
||||
# local env files
|
||||
# do not commit any .env files to git, except for the .env.example file. https://create.t3.gg/en/usage/env-variables#using-environment-variables
|
||||
.env
|
||||
.env.local
|
||||
.env*.local
|
||||
|
||||
# vercel
|
||||
.vercel
|
||||
|
||||
# typescript
|
||||
*.tsbuildinfo
|
||||
|
||||
/generated/typescript-server
|
||||
|
||||
# openapi spec that is copied during build
|
||||
/public/openapi*.yml
|
||||
|
||||
|
||||
# vscode
|
||||
.devcontainer
|
||||
@@ -1,19 +1,11 @@
|
||||
# Base image
|
||||
FROM node:20-alpine AS base
|
||||
ARG DATABASE_URL
|
||||
ARG NEXTAUTH_SECRET
|
||||
ARG NEXTAUTH_URL
|
||||
ARG SALT
|
||||
|
||||
# It's important to update the index before installing packages to ensure you're getting the latest versions.
|
||||
# Check https://github.com/nodejs/docker-node/tree/b4117f9333da4138b03a546ec926ef50a31506c3#nodealpine to understand why libc6-compat might be needed.
|
||||
RUN apk update && apk upgrade --no-cache libcrypto3 libssl3 libc6-compat
|
||||
|
||||
FROM base AS deps
|
||||
ARG DATABASE_URL
|
||||
ARG NEXTAUTH_SECRET
|
||||
ARG NEXTAUTH_URL
|
||||
ARG SALT
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
@@ -29,10 +21,6 @@ RUN \
|
||||
|
||||
# Rebuild the source code only when needed
|
||||
FROM base AS builder
|
||||
ARG DATABASE_URL
|
||||
ARG NEXTAUTH_SECRET
|
||||
ARG NEXTAUTH_URL
|
||||
ARG SALT
|
||||
|
||||
WORKDIR /app
|
||||
COPY --from=deps /app/node_modules ./node_modules
|
||||
@@ -46,6 +34,9 @@ RUN rm -f ./src/middleware.ts
|
||||
# Uncomment the following line in case you want to disable telemetry during the build.
|
||||
ENV NEXT_TELEMETRY_DISABLED 1
|
||||
|
||||
# Disable validation of environment variables during build
|
||||
ENV DOCKER_BUILD 1
|
||||
|
||||
# Generate prisma client
|
||||
RUN npx prisma generate
|
||||
|
||||
@@ -54,10 +45,6 @@ RUN npm run build
|
||||
|
||||
# Production image, copy all the files and run next
|
||||
FROM base AS runner
|
||||
ARG DATABASE_URL
|
||||
ARG NEXTAUTH_SECRET
|
||||
ARG NEXTAUTH_URL
|
||||
ARG SALT
|
||||
|
||||
RUN apk add --no-cache dumb-init
|
||||
|
||||
@@ -66,6 +53,8 @@ WORKDIR /app
|
||||
ENV NODE_ENV production
|
||||
# Uncomment the following line in case you want to disable telemetry during runtime.
|
||||
ENV NEXT_TELEMETRY_DISABLED 1
|
||||
# Needed to re-enable validation of environment variables during runtime
|
||||
ENV DOCKER_BUILD 0
|
||||
|
||||
RUN addgroup --system --gid 1001 nodejs
|
||||
RUN adduser --system --uid 1001 nextjs
|
||||
@@ -0,0 +1,33 @@
|
||||
#!/bin/sh
|
||||
|
||||
# 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
|
||||
status=$?
|
||||
|
||||
# If migration fails (returns non-zero exit status), exit script with that status
|
||||
if [ $status -ne 0 ]; then
|
||||
echo "Applying database migrations failed. This is mostly caused by the database being unavailable."
|
||||
echo "Exiting..."
|
||||
exit $status
|
||||
fi
|
||||
|
||||
# Start server
|
||||
node server.js
|
||||
@@ -0,0 +1,104 @@
|
||||
openapi: 3.0.1
|
||||
info:
|
||||
title: langfuse
|
||||
version: ''
|
||||
paths:
|
||||
/api/public/scores:
|
||||
post:
|
||||
description: Add a score to the database, upserts on id
|
||||
operationId: score_create
|
||||
tags:
|
||||
- Score
|
||||
parameters: []
|
||||
responses:
|
||||
'200':
|
||||
description: ''
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/Score'
|
||||
'400':
|
||||
description: ''
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: string
|
||||
'401':
|
||||
description: ''
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: string
|
||||
'403':
|
||||
description: ''
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: string
|
||||
'405':
|
||||
description: ''
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: string
|
||||
security:
|
||||
- BearerAuth: []
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/CreateScoreRequest'
|
||||
components:
|
||||
schemas:
|
||||
CreateScoreRequest:
|
||||
title: CreateScoreRequest
|
||||
type: object
|
||||
properties:
|
||||
id:
|
||||
type: string
|
||||
traceId:
|
||||
type: string
|
||||
name:
|
||||
type: string
|
||||
value:
|
||||
type: number
|
||||
format: double
|
||||
observationId:
|
||||
type: string
|
||||
comment:
|
||||
type: string
|
||||
required:
|
||||
- traceId
|
||||
- name
|
||||
- value
|
||||
Score:
|
||||
title: Score
|
||||
type: object
|
||||
properties:
|
||||
id:
|
||||
type: string
|
||||
traceId:
|
||||
type: string
|
||||
name:
|
||||
type: string
|
||||
value:
|
||||
type: number
|
||||
format: double
|
||||
observationId:
|
||||
type: string
|
||||
timestamp:
|
||||
type: string
|
||||
format: date-time
|
||||
comment:
|
||||
type: string
|
||||
required:
|
||||
- id
|
||||
- traceId
|
||||
- name
|
||||
- value
|
||||
- timestamp
|
||||
securitySchemes:
|
||||
BearerAuth:
|
||||
type: http
|
||||
scheme: bearer
|
||||
File diff suppressed because it is too large
Load Diff
@@ -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("|")})/)`],
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
@@ -23,27 +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
+19977
File diff suppressed because it is too large
Load Diff
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user