Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
fa4ba016b5 | ||
|
|
9b7e0abfbc | ||
|
|
5556772388 | ||
|
|
68e0270051 | ||
|
|
5bd7d879ad | ||
|
|
87d34c05d9 | ||
|
|
ae27144346 | ||
|
|
f5101083a2 | ||
|
|
124c9e8107 | ||
|
|
fabbb75e40 | ||
|
|
857507195b | ||
|
|
cd7864cad4 | ||
|
|
cd9fd110e4 | ||
|
|
3c35e5e9c5 | ||
|
|
fe75e4c5f1 | ||
|
|
6435ce1a79 | ||
|
|
d270143f0e | ||
|
|
4041550d5e | ||
|
|
fc5ff52d1e | ||
|
|
6f6bef722b | ||
|
|
496ac68008 | ||
|
|
595f9f1e5d | ||
|
|
70bca50ad0 | ||
|
|
99791458b4 | ||
|
|
60dc78b498 | ||
|
|
43e5887361 | ||
|
|
c009d9602d | ||
|
|
830ebf6fff | ||
|
|
976f743841 | ||
|
|
89c1e46c55 | ||
|
|
832e5d85ff | ||
|
|
9b3a1f3af3 | ||
|
|
c2515f7e6a | ||
|
|
a7131dc422 | ||
|
|
bb73243a04 | ||
|
|
e543c7dc27 | ||
|
|
a2b6b7a14b | ||
|
|
e0930fbf46 | ||
|
|
125aa6b2c4 | ||
|
|
3caf70bd2b | ||
|
|
4139ccec98 | ||
|
|
07671c76b4 | ||
|
|
317608421f | ||
|
|
10d84a4094 | ||
|
|
f623bd9f79 | ||
|
|
08fb5f4000 | ||
|
|
03d00c405c | ||
|
|
783f7a205f | ||
|
|
83359e6fc6 | ||
|
|
edd1484c5a | ||
|
|
410c91b464 | ||
|
|
8962090404 | ||
|
|
4f88b78c71 | ||
|
|
8995b30959 | ||
|
|
321a25cf13 | ||
|
|
2990767c4a | ||
|
|
ad10e6f76f | ||
|
|
1f9adec948 | ||
|
|
a158d6da09 | ||
|
|
e6f9e5e06d | ||
|
|
c2463cb91e | ||
|
|
38e472ec66 | ||
|
|
616e79238b | ||
|
|
e2a59a2639 | ||
|
|
07609ade4f | ||
|
|
6627c59db4 | ||
|
|
70e1426790 | ||
|
|
8550d7313f | ||
|
|
1d76ef3a18 | ||
|
|
b68d15309b | ||
|
|
c7b3e95645 | ||
|
|
2c55cd842a | ||
|
|
dc4e217a6e | ||
|
|
90f89f8b33 | ||
|
|
2cbcf1fc02 | ||
|
|
04251e2451 | ||
|
|
b3ff1e0bd1 | ||
|
|
9fc1f79b94 | ||
|
|
06bf8302d6 | ||
|
|
7acc8e1f4d | ||
|
|
b88809f7e5 | ||
|
|
7ee5c2d573 | ||
|
|
702a591a0d | ||
|
|
6021ef94c8 | ||
|
|
d2d10893e1 | ||
|
|
dd78b9be7e | ||
|
|
ec1a9c53e7 | ||
|
|
74da65f9b2 | ||
|
|
654620c336 | ||
|
|
d1b935b280 | ||
|
|
7dfa914934 | ||
|
|
a126b05bb9 | ||
|
|
b81a81d76f | ||
|
|
15014eb8ea | ||
|
|
e1fb0e4102 | ||
|
|
c7d99476eb | ||
|
|
7e3980856c | ||
|
|
54991e2246 | ||
|
|
10bbfd75c3 | ||
|
|
4476d6c1e7 | ||
|
|
9dc758693b | ||
|
|
082031ccd2 | ||
|
|
a88764db7e | ||
|
|
b6e0acb6f3 | ||
|
|
366ccde902 | ||
|
|
21c60fab76 | ||
|
|
7c5a49b4ee | ||
|
|
bfbb83e74f | ||
|
|
8cde89689f | ||
|
|
823d94e6fd | ||
|
|
2bbb88ce93 | ||
|
|
52cfba89e8 | ||
|
|
2de3d6fe8f | ||
|
|
f873a7dc97 | ||
|
|
340e8927bd | ||
|
|
cf0ea0f9fb | ||
|
|
d46715ff79 | ||
|
|
73d06086b6 | ||
|
|
e728a73f29 | ||
|
|
ec3c946965 | ||
|
|
a4eedbaba3 | ||
|
|
32de95648c | ||
|
|
15c97956bf | ||
|
|
debde9c5d3 | ||
|
|
0f78cea5ce | ||
|
|
a3a25425c0 | ||
|
|
489e812fe9 | ||
|
|
42589e31ff | ||
|
|
0cf58be012 | ||
|
|
c338db5a4d | ||
|
|
6b22c19ce1 | ||
|
|
a9cac5d6d2 | ||
|
|
fb1c22896b | ||
|
|
8cb9d5fb60 | ||
|
|
c027575dc1 | ||
|
|
870610b575 | ||
|
|
5547e5cc88 | ||
|
|
dd4a92f9d8 | ||
|
|
9813cd27e0 | ||
|
|
b4c636d430 | ||
|
|
c54ca57f6f | ||
|
|
3a40df5daa | ||
|
|
528f1eb737 | ||
|
|
579f00c8fe | ||
|
|
f4e8a61e7b | ||
|
|
085e669c2c | ||
|
|
a5321fb965 | ||
|
|
0c7a11e698 | ||
|
|
0ae42e3ff1 | ||
|
|
0c27f41084 | ||
|
|
2722966892 | ||
|
|
31a2fc421d | ||
|
|
eb03ffaf85 | ||
|
|
08b04811f1 | ||
|
|
fab53a1c3d | ||
|
|
046ad8075d | ||
|
|
c8672b57cc | ||
|
|
852a43d844 | ||
|
|
4a323b0011 | ||
|
|
4f7e74a1e4 | ||
|
|
27d3bb8a85 | ||
|
|
8ee0b03df6 | ||
|
|
6ade4c6603 | ||
|
|
9ae0708cca | ||
|
|
1ba027fc2b | ||
|
|
e11d02a861 | ||
|
|
41cf5acbe3 | ||
|
|
95454abe05 | ||
|
|
ee92e9d7fa | ||
|
|
fa9e90ed75 | ||
|
|
f981da3457 | ||
|
|
384af7b817 | ||
|
|
6f23b34636 | ||
|
|
99b0f5b157 | ||
|
|
96cb57871d | ||
|
|
e49ba5448e | ||
|
|
ef8e312f48 | ||
|
|
e79553cd03 | ||
|
|
105db18bc9 | ||
|
|
dcba65d49e | ||
|
|
cf96ed67cc | ||
|
|
930d07e4da | ||
|
|
50199db733 | ||
|
|
3e65599966 | ||
|
|
f2c527a677 | ||
|
|
dcf91a112c | ||
|
|
5f5449bd78 | ||
|
|
e9e6802ba4 | ||
|
|
e5261ac73d | ||
|
|
0781c6cd4b | ||
|
|
e708318558 | ||
|
|
0f0af537ae | ||
|
|
2ae694625d | ||
|
|
b53ad98f50 | ||
|
|
10c0b18ca0 | ||
|
|
e0e346dccb | ||
|
|
dec0c5133a | ||
|
|
7c72aa16c0 | ||
|
|
373ee6dda0 | ||
|
|
2880bac4eb | ||
|
|
b28679112a | ||
|
|
7a22b0c3bf | ||
|
|
711f32d3f6 | ||
|
|
6161d1cf71 | ||
|
|
aeec1ebe79 | ||
|
|
e9a3e3c9ef | ||
|
|
2d5b3e2076 | ||
|
|
aeb15b80fe | ||
|
|
c254a36eb5 | ||
|
|
6b25d3c544 | ||
|
|
579c391b26 |
+6
-1
@@ -14,6 +14,9 @@ DATABASE_URL="postgresql://postgres:postgres@localhost:5432/postgres"
|
||||
NEXTAUTH_URL="http://localhost:3000"
|
||||
NEXTAUTH_SECRET="secret"
|
||||
|
||||
# Langfuse Cloud Environment
|
||||
NEXT_PUBLIC_LANGFUSE_CLOUD_REGION="DEV"
|
||||
|
||||
# Langfuse experimental features
|
||||
LANGFUSE_ENABLE_EXPERIMENTAL_FEATURES="true"
|
||||
|
||||
@@ -43,4 +46,6 @@ REDIS_HOST="127.0.0.1"
|
||||
REDIS_PORT=6379
|
||||
REDIS_AUTH="myredissecret"
|
||||
|
||||
LANGFUSE_WORKER_PASSWORD=mybasicauthsecret
|
||||
LANGFUSE_WORKER_PASSWORD=mybasicauthsecret
|
||||
# openssl rand -base64 32 used only here
|
||||
ENCRYPTION_KEY=6c16874e5c0f0cc74ddec00425fa99fbe9ffbe412b7d5d906a4b00005df91403
|
||||
+18
-1
@@ -76,10 +76,27 @@ LANGFUSE_CSP_ENFORCE_HTTPS="true"
|
||||
# The page size can be adjusted if needed to optimize performance
|
||||
# DB_EXPORT_PAGE_SIZE=1000
|
||||
|
||||
# Disable logging to `events` table in db
|
||||
# ENABLE_EVENT_LOG=false
|
||||
|
||||
|
||||
|
||||
### START Envs to be deprecated in Langfuse v3.0
|
||||
|
||||
# Disable the expensive analytics queries and related features
|
||||
# LANGFUSE_DISABLE_EXPENSIVE_POSTGRES_QUERIES="true"
|
||||
|
||||
# Customize the default datetime offset filter used in for the UI tables
|
||||
# LANGFUSE_DEFAULT_TABLE_DATETIME_OFFSET="7" # int (days)
|
||||
|
||||
### END Envs to be deprecated in Langfuse v3.0
|
||||
|
||||
|
||||
|
||||
|
||||
### START Langfuse Cloud Config
|
||||
# Used for Langfuse Cloud deployments
|
||||
# Not recommended for self-hosted deployments as these are NOT COVERED BY SEMVER
|
||||
# Not recommended for self-hosted deployments as these are NOT COVERED BY SEMANTIC VERSIONING
|
||||
|
||||
# NEXT_PUBLIC_LANGFUSE_CLOUD_REGION="US"
|
||||
# NEXTAUTH_COOKIE_DOMAIN=".langfuse.com"
|
||||
|
||||
@@ -12,10 +12,24 @@ body:
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: To reproduce
|
||||
description: Describe how to reproduce your bug. Steps, code snippets, reproduction repos etc.
|
||||
description: Describe how to reproduce your bug. Please provide detailed steps, code snippets, reproduction repos etc.
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: SDK and container versions
|
||||
description: If you're experiencing an issue with an integration or SDK, please ensure you're using the latest version. If you're self-hosting Langfuse, check that you're running the most recent version. If updating isn't an option, please provide the specific versions you're currently using.
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Additional information
|
||||
description: Add any other information related to the bug here, screenshots if applicable.
|
||||
- type: dropdown
|
||||
id: contribute
|
||||
attributes:
|
||||
label: Are you interested to contribute a fix for this bug?
|
||||
description: If this is a confirmed bug, the maintainers are happy to support with guidance and review.
|
||||
options:
|
||||
- "No"
|
||||
- "Yes"
|
||||
validations:
|
||||
required: true
|
||||
|
||||
@@ -109,8 +109,6 @@ jobs:
|
||||
strategy:
|
||||
matrix:
|
||||
node-version: [20]
|
||||
env:
|
||||
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
|
||||
steps:
|
||||
- name: Set Swap Space
|
||||
uses: pierotofy/set-swap-space@master
|
||||
|
||||
+138
-42
@@ -53,53 +53,63 @@ A good first step is to search for open [issues](https://github.com/langfuse/lan
|
||||
|
||||
### Architecture Overview
|
||||
|
||||
> [!NOTE]
|
||||
> Infrastructure will change in Langfuse version 3.0. More in the [GitHub Discussions](https://github.com/orgs/langfuse/discussions/1902).
|
||||
> `langfuse/langfuse/worker` is under active development and not recommended for production use in Langfuse 2.x
|
||||
|
||||
```mermaid
|
||||
flowchart TB
|
||||
subgraph s4["Clients"]
|
||||
subgraph s2["langfuse/langfuse-python"]
|
||||
Python["Python low-level SDK"]
|
||||
Decorator["observe() decorator"] -->|extends| Python
|
||||
OAI["OpenAI drop-in replacement"] -->|extends| Python
|
||||
Llamaindex["LlamaIndex Integration"] -->|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
|
||||
subgraph s4["Clients"]
|
||||
subgraph s2["langfuse/langfuse-python"]
|
||||
Python["Python low-level SDK"]
|
||||
Decorator["observe() decorator"] -->|extends| Python
|
||||
OAI["OpenAI drop-in replacement"] -->|extends| Python
|
||||
Llamaindex["LlamaIndex Integration"] -->|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
|
||||
DB[Postgres Database]
|
||||
Redis[Redis]
|
||||
|
||||
subgraph s1["Application (langfuse/langfuse/web)"]
|
||||
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
|
||||
|
||||
subgraph s5["Application (langfuse/langfuse/worker)"]
|
||||
Worker_API[Public HTTP API]
|
||||
end
|
||||
|
||||
API --> Worker_API
|
||||
Worker_API --- DB
|
||||
Worker_API --- Redis
|
||||
|
||||
Prisma --- DB
|
||||
JS --- API
|
||||
Python --- API
|
||||
```
|
||||
|
||||
### Database Overview
|
||||
### Network 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: [packages/shared/prisma/schema.prisma](packages/shared/prisma/schema.prisma)
|
||||
|
||||
<img src="./packages/shared/prisma/database.svg">
|
||||
|
||||
### Infrastructure & Network Overview
|
||||
> [!NOTE]
|
||||
> This will change in Langfuse version 3.0. More in the [GitHub Discussions](https://github.com/orgs/langfuse/discussions/1902).
|
||||
|
||||
```mermaid
|
||||
flowchart LR
|
||||
@@ -114,6 +124,14 @@ flowchart LR
|
||||
App --- DB
|
||||
```
|
||||
|
||||
### 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: [packages/shared/prisma/schema.prisma](packages/shared/prisma/schema.prisma)
|
||||
|
||||
<img src="./packages/shared/prisma/database.svg">
|
||||
|
||||
## Repository Structure
|
||||
|
||||
We built a monorepo using [pnpm](https://pnpm.io/motivation) and [turbo](https://turbo.build/repo/docs) to manage the dependencies and build process. The monorepo contains the following packages:
|
||||
@@ -135,7 +153,7 @@ Requirements
|
||||
|
||||
**Steps**
|
||||
|
||||
1. Fork the the repository and clone it locally
|
||||
1. Fork the repository and clone it locally
|
||||
2. Run the development database
|
||||
|
||||
```bash
|
||||
@@ -272,6 +290,84 @@ When a new release is tagged on the `main` branch (excluding prereleases), it tr
|
||||
1. The Docker image is published to GitHub Packages with the version number and `latest` tag.
|
||||
2. The deployment is carried out on Langfuse Cloud. This is done by force pushing the `main` branch to the `production` branch during every release, using the [`release.yml`](.github/workflows/release.yml) GitHub Action.
|
||||
|
||||
## Theming
|
||||
|
||||
At Langfuse, we utilize CSS variables to manage our theme settings across the platform.
|
||||
|
||||
Our approach leverages separate CSS variables for backgrounds (--background) and foregrounds (--foreground), fully adhering to the [shadcn/ui](https://ui.shadcn.com/docs/theming) color conventions. The background suffix can be omitted if the variable is used for the background color of the component. We recommend using HSL values for these colors to enhance consistency and customization. There is no need to manually handle dark mode styling with "dark:" prefixes, as next-themes automatically manages the theme switching.
|
||||
|
||||
Given the following CSS variables:
|
||||
|
||||
```
|
||||
--primary: 222.2 47.4% 11.2%; // e.g. background-color
|
||||
--primary-foreground: 210 40% 98%; // e.g. text-color
|
||||
```
|
||||
|
||||
The background color of the following component will be `hsl(var(--primary))` and the foreground color will be `hsl(var(--primary-foreground))`.
|
||||
|
||||
```
|
||||
<div class="bg-primary text-primary-foreground">Hello</div>
|
||||
```
|
||||
|
||||
### Color Variables
|
||||
|
||||
| Variable | Description | Examples |
|
||||
| ------------------------ | ------------------------------------------------------------------ | -------------------------------- |
|
||||
| --background | Background color | Default background color of body |
|
||||
| --foreground | Foreground color | Default text color of body |
|
||||
| --muted | Muted background color | TabsList, Skeleton and Switch |
|
||||
| --muted-foreground | Muted foreground color | |
|
||||
| --popover | Popover background color | DropdownMenu, HoverCard, Popover |
|
||||
| --popover-foreground | Popover foreground color | |
|
||||
| --card | Card background color | Card |
|
||||
| --card-foreground | Card foreground color | |
|
||||
| --border | Border color | Default border color |
|
||||
| --input | Input field border color | Input, Select, Textarea |
|
||||
| --primary | Primary button background colors | Button variant="primary" |
|
||||
| --primary-foreground | Primary button foreground color | |
|
||||
| --secondary | Secondary button background color | Button variant="secondary" |
|
||||
| --secondary-foreground | Secondary button foreground color | |
|
||||
| --accent | Used for accents such as hover effects | DropdownMenuItem, SelectItem |
|
||||
| --accent-foreground | Used for texts on hover effects | DropdownMenuItem, SelectItem |
|
||||
| --destructive | Destructive action color for background | Button variant="destructive" |
|
||||
| --destructive-foreground | Destructive action color for text | |
|
||||
| --ring | Focus ring color | MultiSelect |
|
||||
| --primary-accent | Primary accent color used for branding | Layout |
|
||||
| --hover-primary-accent | Primary accent color used for hover effects for links | SignIn and AuthCloudRegionSwitch |
|
||||
| --muted-green | Muted green for Event label | ObservationTree |
|
||||
| --muted-orange | Muted orange for Generation label | ObservationTree |
|
||||
| --muted-blue | Muted blue for Span label | ObservationTree |
|
||||
| --muted-gray | Muted gray for disabled status badges | StatusBadge |
|
||||
| --accent-light-green | Light green accent for background of output and assistant messages | IOPreview, Generations, Traces |
|
||||
| --accent-dark-green | Dark green accent for border of output and assistant messages | CodeJsonViewer and IOPReview |
|
||||
| --light-red | Light red for error background | level-color and StatusBadge |
|
||||
| --dark-red | Dark red for error text and error badge dot color | level-color and ErrorPage |
|
||||
| --light-yellow | Light yellow for warning background | LevelColor |
|
||||
| --dark-yellow | Dark yellow for warning text | LevelColor |
|
||||
| --light-green | Light green for success status badge background | StatusBadge |
|
||||
| --dark-green | Dark green for success status badge text and dot | StatusBadge |
|
||||
| --light-blue | Light blue for background of Staging label | LangfuseLogo |
|
||||
| --dark-blue | Dark blue for text and border of Staging label | LangfuseLogo |
|
||||
| --accent-light-blue | Light blue accent for table link hover effect | TableLink |
|
||||
| --accent-dark-blue | Dark blue accent for table link text | TableLink |
|
||||
|
||||
### Adding New Colors
|
||||
|
||||
1. Global Definitions: Add new CSS variable definitions in the global.css file.
|
||||
2. Tailwind Configuration: Reflect these new colors in the tailwind.config.js to maintain alignment with Tailwind's utility classes.
|
||||
|
||||
By following these guidelines, you can ensure that any contributions to our theme are consistent, maintainable, and aligned with our design system.
|
||||
|
||||
## Using secrets stored in 1Password
|
||||
|
||||
When applying changes to non-local environments, you may need to use secrets stored in 1Password. We use the 1Password CLI for this purpose.
|
||||
|
||||
Example:
|
||||
|
||||
```bash
|
||||
op run --env-file="./.env" -- pnpm --filter=shared run db:deploy
|
||||
```
|
||||
|
||||
## License
|
||||
|
||||
Langfuse is MIT licensed, except for `ee/` folder. See [LICENSE](LICENSE) and [docs](https://langfuse.com/docs/open-source) for more details.
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
Copyright (c) 2023 Finto Technologies GmbH
|
||||
Copyright (c) 2023--2024 Langfuse GmbH
|
||||
|
||||
Portions of this software are licensed as follows:
|
||||
|
||||
|
||||
@@ -26,7 +26,7 @@
|
||||
<br/>
|
||||
<br/>
|
||||
<div>
|
||||
<img src="https://img.shields.io/badge/License-MIT-red.svg?style=flat-square" alt="MIT License">
|
||||
<a href="https://github.com/langfuse/langfuse/blob/main/LICENSE"><img src="https://img.shields.io/badge/License-MIT-red.svg?style=flat-square" alt="MIT License"></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>
|
||||
@@ -37,17 +37,22 @@
|
||||
|
||||
## Overview
|
||||
|
||||
_Unmute video for voice-over_
|
||||
|
||||
https://github.com/langfuse/langfuse/assets/2834609/a94062e9-c782-4ee9-af59-dee6370149a8
|
||||
|
||||
### Develop
|
||||
|
||||
- **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))
|
||||
- **Prompt Management:** Manage, version and deploy prompts from within Langfuse ([Prompt Management](https://langfuse.com/docs/prompts/get-started))
|
||||
- **Prompt Engineering:** Test and iterate on your prompts with the [LLM Playground](https://langfuse.com/docs/playground)
|
||||
|
||||
### 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))
|
||||
- Run model-based evaluations ([Model-based evaluations](https://langfuse.com/docs/scores/model-based-evals)) within Langfuse
|
||||
- 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))
|
||||
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
_We are Hiring_
|
||||
Join us in scaling Langfuse in Berlin, Germany. We are an open source company, we hire in person, we are only hiring technical talent.
|
||||
|
||||
_Open Roles_
|
||||
|
||||
- Backend Engineer, 70-110k EUR, 0.25-0.75% Equity, https://www.ycombinator.com/companies/langfuse/jobs/mnrdwla-backend-engineer
|
||||
- Product Engineer, 70-110k EUR, 0.25-0.75% Equity, https://www.ycombinator.com/companies/langfuse/jobs/aAvmoFB-product-engineer
|
||||
- Developer Advocate, 60-100k EUR, 0.25-0.5% Equity, https://www.ycombinator.com/companies/langfuse/jobs/uHysbKH-developer-advocate-devrel
|
||||
|
||||
_More Info_
|
||||
|
||||
- https://langfuse.com/careers
|
||||
- https://langfuse.com/docs
|
||||
- https://langfuse.com/changelog
|
||||
+7
-1
@@ -4,7 +4,8 @@ services:
|
||||
langfuse-server:
|
||||
image: ghcr.io/langfuse/langfuse:latest
|
||||
depends_on:
|
||||
- db
|
||||
db:
|
||||
condition: service_healthy
|
||||
ports:
|
||||
- "3000:3000"
|
||||
environment:
|
||||
@@ -18,6 +19,11 @@ services:
|
||||
db:
|
||||
image: postgres
|
||||
restart: always
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "pg_isready -U postgres"]
|
||||
interval: 3s
|
||||
timeout: 3s
|
||||
retries: 10
|
||||
environment:
|
||||
- POSTGRES_USER=postgres
|
||||
- POSTGRES_PASSWORD=postgres
|
||||
|
||||
+37
@@ -0,0 +1,37 @@
|
||||
Langfuse Enterprise License (the “Enterprise License”)
|
||||
Copyright (c) 2024 Langfuse GmbH ('Langfuse')
|
||||
|
||||
With regard to the Langfuse Software:
|
||||
This software and associated documentation files (the "Software") may only be
|
||||
used in production, if you (and any entity that you represent) have agreed to,
|
||||
and are in compliance with, the Langfuse Terms of Service, available
|
||||
at https://langfuse.com/terms (the “Enterprise Terms”), or other
|
||||
agreement governing the use of the Software, as agreed by you and Langfuse,
|
||||
and otherwise have a valid Langfuse Enterprise license.
|
||||
|
||||
Subject to the foregoing sentence, you are free to
|
||||
modify this Software and publish patches to the Software. You agree that Langfuse
|
||||
and/or its licensors (as applicable) retain all right, title and interest in and
|
||||
to all such modifications and/or patches, and all such modifications and/or
|
||||
patches may only be used, copied, modified, displayed, distributed, or otherwise
|
||||
exploited with a valid Langfuse Enterprise license.
|
||||
|
||||
Notwithstanding the foregoing, you may copy and modify
|
||||
the Software for development and testing purposes, without requiring a
|
||||
subscription. You agree that Langfuse and/or its licensors (as applicable) retain
|
||||
all right, title and interest in and to all such modifications. You are not
|
||||
granted any other rights beyond what is expressly stated herein. Subject to the
|
||||
foregoing, it is forbidden to copy, merge, publish, distribute, sublicense,
|
||||
and/or sell the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
|
||||
For all third party components incorporated into the Langfuse Software, those
|
||||
components are licensed under the original license provided by the owner of the
|
||||
applicable component.
|
||||
+1
-1
@@ -25,7 +25,7 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@langfuse/shared": "workspace:*",
|
||||
"next": "^14.2.1",
|
||||
"next": "^14.2.3",
|
||||
"next-auth": "^4.24.7",
|
||||
"zod": "^3.22.4"
|
||||
},
|
||||
|
||||
@@ -15,7 +15,7 @@ groups:
|
||||
version: 0.0.26
|
||||
output:
|
||||
location: local-file-system
|
||||
path: ../../../web/generated/openapi-client
|
||||
path: ../../../web/public/generated/api-client
|
||||
config:
|
||||
namespaceExport: Langfuse
|
||||
allowCustomFetcher: true
|
||||
|
||||
@@ -1,11 +1,17 @@
|
||||
name: langfuse
|
||||
docs: |
|
||||
## Authentication
|
||||
Authenticate with the API using Basic Auth, get API keys in the project settings:
|
||||
|
||||
Authenticate with the API using [Basic Auth](https://en.wikipedia.org/wiki/Basic_access_authentication), get API keys in the project settings:
|
||||
|
||||
- username: Langfuse Public Key
|
||||
- password: Langfuse Secret Key
|
||||
|
||||
## Exports
|
||||
|
||||
- OpenAPI spec: https://cloud.langfuse.com/generated/api/openapi.yml
|
||||
- Postman collection: https://cloud.langfuse.com/generated/postman/collection.json
|
||||
|
||||
error-discrimination:
|
||||
strategy: status-code
|
||||
auth: basic
|
||||
|
||||
@@ -32,3 +32,6 @@ types:
|
||||
id:
|
||||
type: optional<string>
|
||||
docs: Dataset items are upserted on their id
|
||||
status:
|
||||
type: optional<commons.DatasetStatus>
|
||||
docs: Defaults to ACTIVE for newly created items
|
||||
|
||||
@@ -31,10 +31,10 @@ service:
|
||||
docs: Optional filter for metrics where traces include all of these tags
|
||||
fromTimestamp:
|
||||
type: optional<datetime>
|
||||
docs: Optional filter to only include traces on or after a certain timestamp
|
||||
docs: Optional filter to only include traces on or after a certain datetime (ISO 8601)
|
||||
toTimestamp:
|
||||
type: optional<datetime>
|
||||
docs: Optional filter to only include traces before a certain timestamp
|
||||
docs: Optional filter to only include traces before a certain datetime (ISO 8601)
|
||||
response: DailyMetrics
|
||||
types:
|
||||
DailyMetrics:
|
||||
|
||||
@@ -35,7 +35,7 @@ service:
|
||||
parentObservationId: optional<string>
|
||||
fromStartTime:
|
||||
type: optional<datetime>
|
||||
docs: Retrieve only observations with a start_time greater than this timestamp.
|
||||
docs: Retrieve only observations with a start_time greater than this datetime (ISO 8601).
|
||||
response: ObservationsViews
|
||||
|
||||
types:
|
||||
|
||||
@@ -4,18 +4,42 @@ imports:
|
||||
pagination: ./utils/pagination.yml
|
||||
service:
|
||||
auth: true
|
||||
base-path: /api/public
|
||||
base-path: /api/public/v2
|
||||
endpoints:
|
||||
get:
|
||||
docs: Get a prompt
|
||||
method: GET
|
||||
path: /prompts/{promptName}
|
||||
path-parameters:
|
||||
promptName:
|
||||
type: string
|
||||
docs: The name of the prompt
|
||||
request:
|
||||
name: GetPromptRequest
|
||||
query-parameters:
|
||||
version:
|
||||
type: optional<integer>
|
||||
docs: Version of the prompt to be retrieved.
|
||||
label:
|
||||
type: optional<string>
|
||||
docs: Label of the prompt to be retrieved. Defaults to "production" if no label or version is set.
|
||||
response: Prompt
|
||||
|
||||
list:
|
||||
docs: Get a list of prompt names with versions and labels
|
||||
method: GET
|
||||
path: /prompts
|
||||
request:
|
||||
name: GetParameterRequest
|
||||
name: ListPromptsMetaRequest
|
||||
query-parameters:
|
||||
name: string
|
||||
version: optional<integer>
|
||||
response: Prompt
|
||||
name: optional<string>
|
||||
label: optional<string>
|
||||
tag: optional<string>
|
||||
page: optional<integer>
|
||||
limit: optional<integer>
|
||||
|
||||
response: PromptMetaListResponse
|
||||
|
||||
create:
|
||||
docs: Create a prompt
|
||||
method: POST
|
||||
@@ -24,6 +48,18 @@ service:
|
||||
response: Prompt
|
||||
|
||||
types:
|
||||
PromptMetaListResponse:
|
||||
properties:
|
||||
data: list<PromptMeta>
|
||||
meta: pagination.MetaResponse
|
||||
|
||||
PromptMeta:
|
||||
properties:
|
||||
name: string
|
||||
versions: list<integer>
|
||||
labels: list<string>
|
||||
tags: list<string>
|
||||
|
||||
CreatePromptRequest:
|
||||
union:
|
||||
chat: CreateChatPromptRequest
|
||||
@@ -32,20 +68,16 @@ types:
|
||||
CreateChatPromptRequest:
|
||||
properties:
|
||||
name: string
|
||||
isActive:
|
||||
docs: Should the prompt be promoted to production immediately?
|
||||
type: boolean
|
||||
prompt: list<ChatMessage>
|
||||
config: optional<unknown>
|
||||
labels: optional<list<string>>
|
||||
|
||||
CreateTextPromptRequest:
|
||||
properties:
|
||||
name: string
|
||||
isActive:
|
||||
docs: Should the prompt be promoted to production immediately?
|
||||
type: boolean
|
||||
prompt: string
|
||||
config: optional<unknown>
|
||||
labels: optional<list<string>>
|
||||
|
||||
Prompt:
|
||||
union:
|
||||
@@ -57,6 +89,7 @@ types:
|
||||
name: string
|
||||
version: integer
|
||||
config: unknown
|
||||
labels: list<string>
|
||||
|
||||
ChatMessage:
|
||||
properties:
|
||||
|
||||
@@ -29,7 +29,7 @@ service:
|
||||
name: optional<string>
|
||||
fromTimestamp:
|
||||
type: optional<datetime>
|
||||
docs: Retrieve only scores newer than this timestamp.
|
||||
docs: Retrieve only scores newer than this datetime (ISO 8601).
|
||||
source:
|
||||
type: commons.ScoreSource
|
||||
docs: Retrieve only scores from a specific source.
|
||||
|
||||
@@ -32,7 +32,7 @@ service:
|
||||
name: optional<string>
|
||||
fromTimestamp:
|
||||
type: optional<datetime>
|
||||
docs: Retrieve only traces newer than this timestamp.
|
||||
docs: Retrieve only traces newer than this datetime (ISO 8601).
|
||||
orderBy:
|
||||
type: optional<string>
|
||||
docs: "Format of the string [field].[asc/desc]. Fields: id, timestamp, name, userId, release, version, public, bookmarked, sessionId. Example: timestamp.asc"
|
||||
|
||||
@@ -6,7 +6,7 @@ groups:
|
||||
version: 0.0.28
|
||||
output:
|
||||
location: local-file-system
|
||||
path: ../../../web/generated/openapi-server
|
||||
path: ../../../web/public/generated/api
|
||||
config:
|
||||
namespaceExport: Langfuse
|
||||
allowCustomFetcher: true
|
||||
@@ -33,7 +33,7 @@ groups:
|
||||
version: 0.0.45
|
||||
output:
|
||||
location: local-file-system
|
||||
path: ../../../generated/postman
|
||||
path: ../../../web/public/generated/postman
|
||||
# published:
|
||||
# generators:
|
||||
# - name: fernapi/fern-python-sdk
|
||||
|
||||
+4
-4
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "langfuse",
|
||||
"version": "2.30.2",
|
||||
"version": "2.45.0",
|
||||
"author": "engineering@langfuse.com",
|
||||
"license": "MIT",
|
||||
"private": true,
|
||||
@@ -28,10 +28,10 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@release-it/bumper": "^6.0.1",
|
||||
"dotenv-cli": "^7.4.1",
|
||||
"dotenv-cli": "^7.4.2",
|
||||
"prettier": "^3.2.5",
|
||||
"release-it": "^17.2.0",
|
||||
"turbo": "^1.13.2"
|
||||
"release-it": "^17.2.1",
|
||||
"turbo": "^1.13.3"
|
||||
},
|
||||
"release-it": {
|
||||
"git": {
|
||||
|
||||
@@ -11,9 +11,9 @@
|
||||
"@typescript-eslint/eslint-plugin": "^7.1.0",
|
||||
"@typescript-eslint/parser": "^7.7.0",
|
||||
"@vercel/style-guide": "^6.0.0",
|
||||
"eslint-config-next": "^14.2.1",
|
||||
"eslint-config-next": "^14.2.3",
|
||||
"eslint-config-prettier": "^9.1.0",
|
||||
"eslint-config-turbo": "^1.13.2",
|
||||
"eslint-config-turbo": "^1.13.3",
|
||||
"eslint-plugin-only-warn": "^1.1.0",
|
||||
"typescript": "^5.4.5"
|
||||
}
|
||||
|
||||
@@ -44,14 +44,14 @@
|
||||
"seed": "ts-node -r tsconfig-paths/register -r dotenv/config --compiler-options {\"module\":\"CommonJS\"} prisma/seed.ts"
|
||||
},
|
||||
"dependencies": {
|
||||
"@langchain/anthropic": "^0.1.14",
|
||||
"@langchain/core": "^0.1.58",
|
||||
"@langchain/openai": "^0.0.28",
|
||||
"@prisma/client": "^5.12.1",
|
||||
"@langchain/anthropic": "^0.1.21",
|
||||
"@langchain/core": "^0.1.61",
|
||||
"@langchain/openai": "^0.0.33",
|
||||
"@prisma/client": "^5.13.0",
|
||||
"@types/bcryptjs": "^2.4.6",
|
||||
"bcryptjs": "^2.4.3",
|
||||
"kysely": "^0.27.3",
|
||||
"langchain": "^0.1.33",
|
||||
"langchain": "^0.1.36",
|
||||
"prisma-extension-kysely": "^2.1.0",
|
||||
"zod": "^3.22.4",
|
||||
"zod-to-json-schema": "^3.22.5"
|
||||
@@ -59,9 +59,9 @@
|
||||
"devDependencies": {
|
||||
"@repo/eslint-config": "*",
|
||||
"@repo/typescript-config": "*",
|
||||
"@types/lodash": "^4.14.202",
|
||||
"@types/lodash": "^4.17.4",
|
||||
"@types/node": "^20.11.29",
|
||||
"@types/pg": "^8.11.5",
|
||||
"@types/pg": "^8.11.6",
|
||||
"@types/uuid": "^9.0.8",
|
||||
"@typescript-eslint/parser": "^7.7.0",
|
||||
"eslint": "^8.57.0",
|
||||
@@ -71,12 +71,12 @@
|
||||
"kysely-codegen": "^0.11.0",
|
||||
"nodemon": "^3.0.3",
|
||||
"prettier": "^3.2.5",
|
||||
"prisma": "^5.12.1",
|
||||
"prisma": "^5.13.0",
|
||||
"prisma-erd-generator": "^1.11.2",
|
||||
"prisma-kysely": "^1.8.0",
|
||||
"ts-node": "^10.9.2",
|
||||
"tsc-watch": "^6.2.0",
|
||||
"typescript": "^5.4.5",
|
||||
"vitest": "^1.5.0"
|
||||
"vitest": "^1.5.3"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,13 +4,13 @@ export type Generated<T> = T extends ColumnType<infer S, infer I, infer U>
|
||||
: ColumnType<T, T | undefined, T>;
|
||||
export type Timestamp = ColumnType<Date, Date | string, Date | string>;
|
||||
|
||||
export const MembershipRole = {
|
||||
export const ProjectRole = {
|
||||
OWNER: "OWNER",
|
||||
ADMIN: "ADMIN",
|
||||
MEMBER: "MEMBER",
|
||||
VIEWER: "VIEWER"
|
||||
} as const;
|
||||
export type MembershipRole = (typeof MembershipRole)[keyof typeof MembershipRole];
|
||||
export type ProjectRole = (typeof ProjectRole)[keyof typeof ProjectRole];
|
||||
export const ObservationType = {
|
||||
SPAN: "SPAN",
|
||||
EVENT: "EVENT",
|
||||
@@ -96,7 +96,7 @@ export type AuditLog = {
|
||||
updated_at: Generated<Timestamp>;
|
||||
user_id: string;
|
||||
project_id: string;
|
||||
user_project_role: MembershipRole;
|
||||
user_project_role: ProjectRole;
|
||||
resource_type: string;
|
||||
resource_id: string;
|
||||
action: string;
|
||||
@@ -199,17 +199,19 @@ export type JobExecution = {
|
||||
job_input_trace_id: string | null;
|
||||
job_output_score_id: string | null;
|
||||
};
|
||||
export type Membership = {
|
||||
project_id: string;
|
||||
user_id: string;
|
||||
role: MembershipRole;
|
||||
export type LlmApiKeys = {
|
||||
id: string;
|
||||
created_at: Generated<Timestamp>;
|
||||
updated_at: Generated<Timestamp>;
|
||||
provider: string;
|
||||
display_secret_key: string;
|
||||
secret_key: string;
|
||||
project_id: string;
|
||||
};
|
||||
export type MembershipInvitation = {
|
||||
id: string;
|
||||
email: string;
|
||||
role: MembershipRole;
|
||||
role: ProjectRole;
|
||||
project_id: string;
|
||||
sender_id: string | null;
|
||||
created_at: Generated<Timestamp>;
|
||||
@@ -291,6 +293,7 @@ export type ObservationView = {
|
||||
calculated_output_cost: string | null;
|
||||
calculated_total_cost: string | null;
|
||||
latency: number | null;
|
||||
time_to_first_token: number | null;
|
||||
};
|
||||
export type PosthogIntegration = {
|
||||
project_id: string;
|
||||
@@ -298,6 +301,7 @@ export type PosthogIntegration = {
|
||||
posthog_host_name: string;
|
||||
last_sync_at: Timestamp | null;
|
||||
enabled: boolean;
|
||||
created_at: Generated<Timestamp>;
|
||||
};
|
||||
export type Pricing = {
|
||||
id: string;
|
||||
@@ -314,6 +318,13 @@ export type Project = {
|
||||
name: string;
|
||||
cloud_config: unknown | null;
|
||||
};
|
||||
export type ProjectMembership = {
|
||||
project_id: string;
|
||||
user_id: string;
|
||||
role: ProjectRole;
|
||||
created_at: Generated<Timestamp>;
|
||||
updated_at: Generated<Timestamp>;
|
||||
};
|
||||
export type Prompt = {
|
||||
id: string;
|
||||
created_at: Generated<Timestamp>;
|
||||
@@ -324,16 +335,19 @@ export type Prompt = {
|
||||
name: string;
|
||||
version: number;
|
||||
type: Generated<string>;
|
||||
is_active: boolean;
|
||||
is_active: boolean | null;
|
||||
config: Generated<unknown>;
|
||||
tags: Generated<string[]>;
|
||||
labels: Generated<string[]>;
|
||||
};
|
||||
export type Score = {
|
||||
id: string;
|
||||
timestamp: Generated<Timestamp>;
|
||||
project_id: string;
|
||||
name: string;
|
||||
value: number;
|
||||
source: ScoreSource;
|
||||
author_user_id: string | null;
|
||||
comment: string | null;
|
||||
trace_id: string;
|
||||
observation_id: string | null;
|
||||
@@ -367,6 +381,8 @@ export type Trace = {
|
||||
input: unknown | null;
|
||||
output: unknown | null;
|
||||
session_id: string | null;
|
||||
created_at: Generated<Timestamp>;
|
||||
updated_at: Generated<Timestamp>;
|
||||
};
|
||||
export type TraceSession = {
|
||||
id: string;
|
||||
@@ -424,13 +440,14 @@ export type DB = {
|
||||
events: Events;
|
||||
job_configurations: JobConfiguration;
|
||||
job_executions: JobExecution;
|
||||
llm_api_keys: LlmApiKeys;
|
||||
membership_invitations: MembershipInvitation;
|
||||
memberships: Membership;
|
||||
models: Model;
|
||||
observations: Observation;
|
||||
observations_view: ObservationView;
|
||||
posthog_integrations: PosthogIntegration;
|
||||
pricings: Pricing;
|
||||
project_memberships: ProjectMembership;
|
||||
projects: Project;
|
||||
prompts: Prompt;
|
||||
scores: Score;
|
||||
|
||||
+2
@@ -0,0 +1,2 @@
|
||||
-- AlterTable
|
||||
ALTER TABLE "posthog_integrations" ADD COLUMN "created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP;
|
||||
@@ -0,0 +1,26 @@
|
||||
DELETE FROM models WHERE id = 'cluv2t5k3000508ih5kve9zag';
|
||||
DELETE FROM models WHERE id = 'clrkvq6iq000008ju6c16gynt';
|
||||
|
||||
INSERT INTO models (
|
||||
id,
|
||||
project_id,
|
||||
model_name,
|
||||
match_pattern,
|
||||
start_date,
|
||||
input_price,
|
||||
output_price,
|
||||
total_price,
|
||||
unit,
|
||||
tokenizer_id,
|
||||
tokenizer_config
|
||||
)
|
||||
VALUES
|
||||
-- updating tokenizer model to gpt-4-turbo-2024-04-09
|
||||
('cluv2t5k3000508ih5kve9zag', NULL, 'gpt-4-turbo-2024-04-09', '(?i)^(gpt-4-turbo-2024-04-09)$', NULL, 0.00001, 0.00003, NULL, 'TOKENS', 'openai', '{ "tokensPerMessage": 3, "tokensPerName": 1, "tokenizerModel": "gpt-4-turbo-2024-04-09" }'),
|
||||
|
||||
-- update gpt-4-1106-preview naming replacing gpt-4-turbo
|
||||
('clrkvq6iq000008ju6c16gynt', NULL, 'gpt-4-1106-preview', '(?i)^(gpt-4-1106-preview)$', NULL, 0.00001, 0.00003, NULL, 'TOKENS', 'openai', '{ "tokensPerMessage": 3, "tokensPerName": 1, "tokenizerModel": "gpt-4-1106-preview" }'),
|
||||
|
||||
-- apparently azure supports gpt-4-preview
|
||||
('clv2o2x0p000008jsf9afceau', NULL, ' gpt-4-preview', '(?i)^(gpt-4-preview)$', NULL, 0.00001, 0.00003, NULL, 'TOKENS', 'openai', '{ "tokensPerMessage": 3, "tokensPerName": 1, "tokenizerModel": "gpt-4-turbo-preview" }')
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
-- CreateTable
|
||||
CREATE TABLE "llm_api_keys" (
|
||||
"id" TEXT NOT NULL,
|
||||
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updated_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"provider" TEXT NOT NULL,
|
||||
"display_secret_key" TEXT NOT NULL,
|
||||
"secret_key" TEXT NOT NULL,
|
||||
"project_id" TEXT NOT NULL,
|
||||
|
||||
CONSTRAINT "llm_api_keys_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "llm_api_keys_id_key" ON "llm_api_keys"("id");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "llm_api_keys_project_id_provider_idx" ON "llm_api_keys"("project_id", "provider");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "llm_api_keys_project_id_provider_key" ON "llm_api_keys"("project_id", "provider");
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "llm_api_keys" ADD CONSTRAINT "llm_api_keys_project_id_fkey" FOREIGN KEY ("project_id") REFERENCES "projects"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
@@ -0,0 +1,2 @@
|
||||
-- CreateIndex
|
||||
CREATE INDEX "job_executions_project_id_status_idx" ON "job_executions"("project_id", "status");
|
||||
+16
@@ -0,0 +1,16 @@
|
||||
BEGIN;
|
||||
|
||||
-- Step 1: Alter the 'prompts' table to add the 'labels' column
|
||||
ALTER TABLE prompts
|
||||
ADD COLUMN labels TEXT[] DEFAULT ARRAY[]::TEXT[];
|
||||
|
||||
-- Step 2: Update the 'labels' column to include 'production' for active prompts
|
||||
UPDATE prompts
|
||||
SET labels = array_append(labels, 'production')
|
||||
WHERE is_active = TRUE;
|
||||
|
||||
-- Step 3: Drop the required constraint on 'is_active' column.
|
||||
ALTER TABLE prompts
|
||||
ALTER COLUMN is_active DROP NOT NULL;
|
||||
|
||||
COMMIT;
|
||||
@@ -0,0 +1,15 @@
|
||||
UPDATE
|
||||
prompts
|
||||
SET
|
||||
labels = array_append(labels, 'latest')
|
||||
WHERE
|
||||
id = (
|
||||
SELECT
|
||||
id
|
||||
FROM
|
||||
prompts AS p2
|
||||
WHERE
|
||||
p2.name = prompts.name
|
||||
ORDER BY
|
||||
created_at DESC
|
||||
LIMIT 1);
|
||||
+3
@@ -0,0 +1,3 @@
|
||||
-- AlterTable
|
||||
ALTER TABLE "traces" ADD COLUMN "created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
ADD COLUMN "updated_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP;
|
||||
@@ -0,0 +1,3 @@
|
||||
-- CreateIndex
|
||||
|
||||
CREATE INDEX CONCURRENTLY "traces_created_at_idx" ON "traces"("created_at");
|
||||
@@ -0,0 +1,3 @@
|
||||
-- CreateIndex
|
||||
|
||||
CREATE INDEX CONCURRENTLY "traces_updated_at_idx" ON "traces"("updated_at");
|
||||
@@ -0,0 +1,2 @@
|
||||
-- AlterTable
|
||||
ALTER TABLE "scores" ADD COLUMN "project_id" TEXT;
|
||||
+2
@@ -0,0 +1,2 @@
|
||||
-- CreateIndex
|
||||
CREATE INDEX CONCURRENTLY "scores_project_id_idx" ON "scores"("project_id");
|
||||
+5
@@ -0,0 +1,5 @@
|
||||
-- Backfill project_id on existing scores based on linked trace_id
|
||||
UPDATE scores
|
||||
SET project_id = traces.project_id
|
||||
FROM traces
|
||||
WHERE scores.trace_id = traces.id AND scores.project_id IS NULL;
|
||||
+15
@@ -0,0 +1,15 @@
|
||||
-- Rename table, will cause a downtime as it needs to be locked and synced with TRPC API
|
||||
ALTER TABLE "memberships"
|
||||
RENAME TO "project_memberships";
|
||||
|
||||
-- AlterTable
|
||||
ALTER TABLE "project_memberships" RENAME CONSTRAINT "memberships_pkey" TO "project_memberships_pkey";
|
||||
|
||||
-- RenameForeignKey
|
||||
ALTER TABLE "project_memberships" RENAME CONSTRAINT "memberships_project_id_fkey" TO "project_memberships_project_id_fkey";
|
||||
|
||||
-- RenameForeignKey
|
||||
ALTER TABLE "project_memberships" RENAME CONSTRAINT "memberships_user_id_fkey" TO "project_memberships_user_id_fkey";
|
||||
|
||||
-- RenameIndex
|
||||
ALTER INDEX "memberships_user_id_idx" RENAME TO "project_memberships_user_id_idx";
|
||||
+37
@@ -0,0 +1,37 @@
|
||||
-- Goal: Rename the enum `MembershipRole` to `ProjectRole` and update all tables that use it
|
||||
|
||||
-- Create a new Enum
|
||||
CREATE TYPE "ProjectRole" AS ENUM ('OWNER', 'ADMIN', 'MEMBER', 'VIEWER');
|
||||
|
||||
-- Add a temporary column to tables (for each table)
|
||||
ALTER TABLE "audit_logs" ADD COLUMN "user_project_role_temp" text;
|
||||
ALTER TABLE "membership_invitations" ADD COLUMN "role_temp" text;
|
||||
ALTER TABLE "project_memberships" ADD COLUMN "role_temp" text;
|
||||
|
||||
-- Copy data over to temporary columns (for each table)
|
||||
UPDATE "audit_logs" SET "user_project_role_temp" = "user_project_role"::text;
|
||||
UPDATE "membership_invitations" SET "role_temp" = "role"::text;
|
||||
UPDATE "project_memberships" SET "role_temp" = "role"::text;
|
||||
|
||||
-- Drop old columns
|
||||
ALTER TABLE "audit_logs" DROP COLUMN "user_project_role";
|
||||
ALTER TABLE "membership_invitations" DROP COLUMN "role";
|
||||
ALTER TABLE "project_memberships" DROP COLUMN "role";
|
||||
|
||||
-- Rename temporary columns to old column names
|
||||
ALTER TABLE "audit_logs" RENAME COLUMN "user_project_role_temp" TO "user_project_role";
|
||||
ALTER TABLE "membership_invitations" RENAME COLUMN "role_temp" TO "role";
|
||||
ALTER TABLE "project_memberships" RENAME COLUMN "role_temp" TO "role";
|
||||
|
||||
-- Convert the text columns to enum columns (for each table)
|
||||
ALTER TABLE "audit_logs" ALTER COLUMN "user_project_role" TYPE "ProjectRole" USING "user_project_role"::"ProjectRole";
|
||||
ALTER TABLE "membership_invitations" ALTER COLUMN "role" TYPE "ProjectRole" USING "role"::"ProjectRole";
|
||||
ALTER TABLE "project_memberships" ALTER COLUMN "role" TYPE "ProjectRole" USING "role"::"ProjectRole";
|
||||
|
||||
-- Make the columns NOT NULL (for each table)
|
||||
ALTER TABLE "audit_logs" ALTER COLUMN "user_project_role" SET NOT NULL;
|
||||
ALTER TABLE "membership_invitations" ALTER COLUMN "role" SET NOT NULL;
|
||||
ALTER TABLE "project_memberships" ALTER COLUMN "role" SET NOT NULL;
|
||||
|
||||
-- Now finally, you can drop your old enum
|
||||
DROP TYPE "MembershipRole";
|
||||
@@ -0,0 +1,20 @@
|
||||
INSERT INTO models (
|
||||
id,
|
||||
project_id,
|
||||
model_name,
|
||||
match_pattern,
|
||||
start_date,
|
||||
input_price,
|
||||
output_price,
|
||||
total_price,
|
||||
unit,
|
||||
tokenizer_id,
|
||||
tokenizer_config
|
||||
)
|
||||
VALUES
|
||||
-- gpt-4o
|
||||
('b9854a5c92dc496b997d99d20', NULL, 'gpt-4o', '(?i)^(gpt-4o)$', NULL, 0.000005, 0.000015, NULL, 'TOKENS', 'openai', '{ "tokensPerMessage": 3, "tokensPerName": 1, "tokenizerModel": "gpt-4o" }'),
|
||||
|
||||
-- gpt-4o-2024-05-13
|
||||
('b9854a5c92dc496b997d99d21', NULL, 'gpt-4o-2024-05-13', '(?i)^(gpt-4o-2024-05-13)$', NULL, 0.000005, 0.000015, NULL, 'TOKENS', 'openai', '{ "tokensPerMessage": 3, "tokensPerName": 1, "tokenizerModel": "gpt-4o-2024-05-13" }')
|
||||
|
||||
+5
@@ -0,0 +1,5 @@
|
||||
-- DropForeignKey
|
||||
ALTER TABLE "scores" DROP CONSTRAINT "scores_observation_id_fkey";
|
||||
|
||||
-- DropForeignKey
|
||||
ALTER TABLE "scores" DROP CONSTRAINT "scores_trace_id_fkey";
|
||||
+11
@@ -0,0 +1,11 @@
|
||||
/*
|
||||
Warnings:
|
||||
|
||||
- Made the column `project_id` on table `scores` required. This step will fail if there are existing NULL values in that column.
|
||||
|
||||
*/
|
||||
-- AlterTable
|
||||
ALTER TABLE "scores" ALTER COLUMN "project_id" SET NOT NULL;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "scores" ADD CONSTRAINT "scores_project_id_fkey" FOREIGN KEY ("project_id") REFERENCES "projects"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
+8
@@ -0,0 +1,8 @@
|
||||
/*
|
||||
Warnings:
|
||||
|
||||
- A unique constraint covering the columns `[id,project_id]` on the table `scores` will be added. If there are existing duplicate values, this will fail.
|
||||
|
||||
*/
|
||||
-- DropIndex
|
||||
DROP INDEX "scores_id_trace_id_key";
|
||||
+8
@@ -0,0 +1,8 @@
|
||||
/*
|
||||
Warnings:
|
||||
|
||||
- A unique constraint covering the columns `[id,project_id]` on the table `scores` will be added. If there are existing duplicate values, this will fail.
|
||||
|
||||
*/
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX CONCURRENTLY "scores_id_project_id_key" ON "scores"("id", "project_id");
|
||||
+61
@@ -0,0 +1,61 @@
|
||||
CREATE OR REPLACE VIEW "observations_view" AS
|
||||
SELECT
|
||||
o.*,
|
||||
m.id AS "model_id",
|
||||
m.start_date AS "model_start_date",
|
||||
m.input_price,
|
||||
m.output_price,
|
||||
m.total_price,
|
||||
m.tokenizer_config AS "tokenizer_config",
|
||||
CASE
|
||||
WHEN o.input_cost IS NULL AND o.output_cost IS NULL AND o.total_cost IS NULL THEN
|
||||
o.prompt_tokens::decimal * m.input_price
|
||||
ELSE
|
||||
o.input_cost
|
||||
END AS "calculated_input_cost",
|
||||
CASE
|
||||
WHEN o.input_cost IS NULL AND o.output_cost IS NULL AND o.total_cost IS NULL THEN
|
||||
o.completion_tokens::decimal * m.output_price
|
||||
ELSE
|
||||
o.output_cost
|
||||
END AS "calculated_output_cost",
|
||||
CASE
|
||||
WHEN o.input_cost IS NULL AND o.output_cost IS NULL AND o.total_cost IS NULL THEN
|
||||
CASE
|
||||
WHEN m.total_price IS NOT NULL AND o.total_tokens IS NOT NULL THEN
|
||||
m.total_price * o.total_tokens
|
||||
ELSE
|
||||
o.prompt_tokens::decimal * m.input_price +
|
||||
o.completion_tokens::decimal * m.output_price
|
||||
END
|
||||
ELSE
|
||||
o.total_cost
|
||||
END AS "calculated_total_cost",
|
||||
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",
|
||||
CASE WHEN o.completion_start_time IS NOT NULL AND o.start_time IS NOT NULL THEN EXTRACT(EPOCH FROM (completion_start_time - start_time))::double precision ELSE NULL END as "time_to_first_token"
|
||||
|
||||
FROM
|
||||
observations o
|
||||
LEFT JOIN LATERAL (
|
||||
SELECT
|
||||
models.*
|
||||
FROM
|
||||
models
|
||||
WHERE (models.project_id = o.project_id OR models.project_id IS NULL)
|
||||
AND models.model_name = o.internal_model
|
||||
AND (models.start_date < o.start_time OR models.start_date IS NULL)
|
||||
AND o.unit::TEXT = models.unit
|
||||
ORDER BY
|
||||
models.project_id ASC, -- in postgres, NULLs are sorted last when ordering ASC
|
||||
models.start_date DESC NULLS LAST -- now, NULLs are sorted last when ordering DESC as well
|
||||
LIMIT 1
|
||||
) m ON TRUE
|
||||
|
||||
|
||||
-- requirements:
|
||||
-- 1. The view should return all columns from the observations table
|
||||
-- 2. The view should match with only one model for each observation if:
|
||||
-- a. The model has the same project_id as the observation, otherwise the model without project_id.
|
||||
-- b. The model has the same model_name as the observation
|
||||
-- c. The model has a start_date that is less than the observation start_time, otherwise the model without start_date
|
||||
-- d. The model has the same unit as the observation
|
||||
+2
@@ -0,0 +1,2 @@
|
||||
-- AlterTable
|
||||
ALTER TABLE "scores" ADD COLUMN "author_user_id" TEXT;
|
||||
+2
@@ -0,0 +1,2 @@
|
||||
-- CreateIndex
|
||||
CREATE INDEX CONCURRENTLY "scores_author_user_id_idx" ON "scores"("author_user_id");
|
||||
@@ -3,7 +3,7 @@
|
||||
|
||||
generator client {
|
||||
provider = "prisma-client-js"
|
||||
previewFeatures = ["tracing", "views"]
|
||||
previewFeatures = ["tracing", "views", "relationJoins"]
|
||||
}
|
||||
|
||||
datasource db {
|
||||
@@ -63,21 +63,21 @@ model Session {
|
||||
}
|
||||
|
||||
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[]
|
||||
invitations MembershipInvitation[]
|
||||
createdAt DateTime @default(now()) @map("created_at")
|
||||
updatedAt DateTime @default(now()) @updatedAt @map("updated_at")
|
||||
featureFlags String[] @default([]) @map("feature_flags")
|
||||
AuditLog AuditLog[]
|
||||
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[]
|
||||
projectMemberships ProjectMembership[]
|
||||
invitations MembershipInvitation[]
|
||||
createdAt DateTime @default(now()) @map("created_at")
|
||||
updatedAt DateTime @default(now()) @updatedAt @map("updated_at")
|
||||
featureFlags String[] @default([]) @map("feature_flags")
|
||||
AuditLog AuditLog[]
|
||||
|
||||
@@map("users")
|
||||
}
|
||||
@@ -97,7 +97,7 @@ model Project {
|
||||
updatedAt DateTime @default(now()) @updatedAt @map("updated_at")
|
||||
name String
|
||||
cloudConfig Json? @map("cloud_config") // Langfuse Cloud, for zod schema see projectsRouter.ts
|
||||
members Membership[]
|
||||
projectMembers ProjectMembership[]
|
||||
traces Trace[]
|
||||
observations Observation[]
|
||||
apiKeys ApiKey[]
|
||||
@@ -111,7 +111,9 @@ model Project {
|
||||
EvalTemplate EvalTemplate[]
|
||||
JobConfiguration JobConfiguration[]
|
||||
JobExecution JobExecution[]
|
||||
LlmApiKeys LlmApiKeys[]
|
||||
PosthogIntegration PosthogIntegration[]
|
||||
Score Score[]
|
||||
|
||||
@@map("projects")
|
||||
}
|
||||
@@ -136,37 +138,54 @@ model ApiKey {
|
||||
@@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")
|
||||
model LlmApiKeys {
|
||||
id String @id @unique @default(cuid())
|
||||
createdAt DateTime @default(now()) @map("created_at")
|
||||
updatedAt DateTime @default(now()) @updatedAt @map("updated_at")
|
||||
|
||||
provider String
|
||||
displaySecretKey String @map("display_secret_key")
|
||||
secretKey String @map("secret_key")
|
||||
|
||||
projectId String @map("project_id")
|
||||
project Project @relation(fields: [projectId], references: [id], onDelete: Cascade)
|
||||
|
||||
@@unique([projectId, provider])
|
||||
@@index([projectId, provider])
|
||||
@@map("llm_api_keys")
|
||||
}
|
||||
|
||||
model ProjectMembership {
|
||||
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 ProjectRole
|
||||
createdAt DateTime @default(now()) @map("created_at")
|
||||
updatedAt DateTime @default(now()) @updatedAt @map("updated_at")
|
||||
|
||||
@@id([projectId, userId])
|
||||
@@index([userId])
|
||||
@@map("memberships")
|
||||
@@map("project_memberships")
|
||||
}
|
||||
|
||||
model MembershipInvitation {
|
||||
id String @id @unique @default(cuid())
|
||||
id String @id @unique @default(cuid())
|
||||
email String
|
||||
role MembershipRole
|
||||
projectId String @map("project_id")
|
||||
project Project @relation(fields: [projectId], references: [id], onDelete: Cascade)
|
||||
senderId String? @map("sender_id")
|
||||
sender User? @relation(fields: [senderId], references: [id], onDelete: SetNull)
|
||||
createdAt DateTime @default(now()) @map("created_at")
|
||||
updatedAt DateTime @default(now()) @updatedAt @map("updated_at")
|
||||
role ProjectRole
|
||||
projectId String @map("project_id")
|
||||
project Project @relation(fields: [projectId], references: [id], onDelete: Cascade)
|
||||
senderId String? @map("sender_id")
|
||||
sender User? @relation(fields: [senderId], references: [id], onDelete: SetNull)
|
||||
createdAt DateTime @default(now()) @map("created_at")
|
||||
updatedAt DateTime @default(now()) @updatedAt @map("updated_at")
|
||||
|
||||
@@index([projectId])
|
||||
@@index([email])
|
||||
@@map("membership_invitations")
|
||||
}
|
||||
|
||||
enum MembershipRole {
|
||||
enum ProjectRole {
|
||||
OWNER
|
||||
ADMIN
|
||||
MEMBER
|
||||
@@ -209,8 +228,9 @@ model Trace {
|
||||
output Json?
|
||||
sessionId String? @map("session_id")
|
||||
session TraceSession? @relation(fields: [sessionId, projectId], references: [id, projectId])
|
||||
createdAt DateTime @default(now()) @map("created_at")
|
||||
updatedAt DateTime @default(now()) @updatedAt @map("updated_at")
|
||||
|
||||
scores Score[]
|
||||
DatasetRunItems DatasetRunItems[]
|
||||
DatasetItem DatasetItem[]
|
||||
JobExecution JobExecution[]
|
||||
@@ -223,6 +243,8 @@ model Trace {
|
||||
@@index([externalId])
|
||||
@@index(timestamp)
|
||||
@@index(release)
|
||||
@@index(updatedAt)
|
||||
@@index(createdAt)
|
||||
@@index([tags(ops: ArrayOps)], type: Gin)
|
||||
@@map("traces")
|
||||
}
|
||||
@@ -254,6 +276,8 @@ view TraceView {
|
||||
}
|
||||
|
||||
// Update ObservationView below when making changes to this model!
|
||||
// traceId is optional only due to timing during data injestion
|
||||
// (traceId is not necessarily known at the time of observation creation)
|
||||
model Observation {
|
||||
id String @id @default(cuid())
|
||||
traceId String? @map("trace_id")
|
||||
@@ -283,7 +307,6 @@ model Observation {
|
||||
outputCost Decimal? @map("output_cost")
|
||||
totalCost Decimal? @map("total_cost")
|
||||
completionStartTime DateTime? @map("completion_start_time")
|
||||
scores Score[]
|
||||
project Project @relation(fields: [projectId], references: [id], onDelete: Cascade)
|
||||
derivedDatasetItems DatasetItem[]
|
||||
datasetRunItems DatasetRunItems[]
|
||||
@@ -350,6 +373,7 @@ view ObservationView {
|
||||
calculatedOutputCost Decimal? @map("calculated_output_cost")
|
||||
calculatedTotalCost Decimal? @map("calculated_total_cost")
|
||||
latency Float? @map("latency")
|
||||
timeToFirstToken Float? @map("time_to_first_token")
|
||||
|
||||
@@map("observations_view")
|
||||
}
|
||||
@@ -370,19 +394,22 @@ enum ObservationLevel {
|
||||
model Score {
|
||||
id String @id @default(cuid())
|
||||
timestamp DateTime @default(now())
|
||||
projectId String @map("project_id")
|
||||
project Project @relation(fields: [projectId], references: [id], onDelete: Cascade)
|
||||
name String
|
||||
value Float
|
||||
source ScoreSource
|
||||
authorUserId String? @map("author_user_id")
|
||||
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)
|
||||
JobExecution JobExecution[]
|
||||
|
||||
@@unique([id, traceId]) // used for upsert
|
||||
@@unique([id, projectId]) // used for upserts via prisma
|
||||
@@index(timestamp)
|
||||
@@index([value])
|
||||
@@index([projectId])
|
||||
@@index([authorUserId])
|
||||
@@index([traceId], type: Hash)
|
||||
@@index([observationId], type: Hash)
|
||||
@@index([source])
|
||||
@@ -535,9 +562,10 @@ model Prompt {
|
||||
name String
|
||||
version Int
|
||||
type String @default("text")
|
||||
isActive Boolean @map("is_active")
|
||||
isActive Boolean? @map("is_active") // Deprecated. To be removed once 'production' labels work as expected.
|
||||
config Json @default("{}")
|
||||
tags String[] @default([])
|
||||
labels String[] @default([])
|
||||
Observation Observation[]
|
||||
|
||||
@@unique([projectId, name, version])
|
||||
@@ -575,16 +603,16 @@ model Model {
|
||||
}
|
||||
|
||||
model AuditLog {
|
||||
id String @id @default(cuid())
|
||||
createdAt DateTime @default(now()) @map("created_at")
|
||||
updatedAt DateTime @default(now()) @updatedAt @map("updated_at")
|
||||
userId String @map("user_id")
|
||||
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
||||
projectId String @map("project_id")
|
||||
project Project @relation(fields: [projectId], references: [id], onDelete: Cascade)
|
||||
userProjectRole MembershipRole @map("user_project_role")
|
||||
resourceType String @map("resource_type")
|
||||
resourceId String @map("resource_id")
|
||||
id String @id @default(cuid())
|
||||
createdAt DateTime @default(now()) @map("created_at")
|
||||
updatedAt DateTime @default(now()) @updatedAt @map("updated_at")
|
||||
userId String @map("user_id")
|
||||
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
||||
projectId String @map("project_id")
|
||||
project Project @relation(fields: [projectId], references: [id], onDelete: Cascade)
|
||||
userProjectRole ProjectRole @map("user_project_role")
|
||||
resourceType String @map("resource_type")
|
||||
resourceId String @map("resource_id")
|
||||
action String
|
||||
before String? //stringified JSON
|
||||
after String? // stringified JSON
|
||||
@@ -681,6 +709,7 @@ model JobExecution {
|
||||
jobOutputScoreId String? @map("job_output_score_id")
|
||||
score Score? @relation(fields: [jobOutputScoreId], references: [id], onDelete: SetNull) // job remains when scores are deleted
|
||||
|
||||
@@index([projectId, status])
|
||||
@@index([projectId, id])
|
||||
@@index([projectId])
|
||||
@@map("job_executions")
|
||||
@@ -708,6 +737,7 @@ model PosthogIntegration {
|
||||
posthogHostName String @map("posthog_host_name")
|
||||
lastSyncAt DateTime? @map("last_sync_at")
|
||||
enabled Boolean
|
||||
createdAt DateTime @default(now()) @map("created_at")
|
||||
|
||||
@@index([projectId])
|
||||
@@map("posthog_integrations")
|
||||
|
||||
@@ -51,7 +51,7 @@ async function main() {
|
||||
create: {
|
||||
id: seedProjectId,
|
||||
name: "llm-app",
|
||||
members: {
|
||||
projectMembers: {
|
||||
create: {
|
||||
role: "OWNER",
|
||||
userId: user.id,
|
||||
@@ -72,7 +72,7 @@ async function main() {
|
||||
name: "summary-prompt",
|
||||
project: { connect: { id: seedProjectId } },
|
||||
prompt: "prompt {{variable}} {{anotherVariable}}",
|
||||
isActive: true,
|
||||
labels: ["production", "latest"],
|
||||
version: 1,
|
||||
createdBy: "user-1",
|
||||
},
|
||||
@@ -110,7 +110,7 @@ async function main() {
|
||||
create: {
|
||||
id: "239ad00f-562f-411d-af14-831c75ddd875",
|
||||
name: "demo-app",
|
||||
members: {
|
||||
projectMembers: {
|
||||
create: {
|
||||
role: "OWNER",
|
||||
userId: user.id,
|
||||
@@ -459,7 +459,7 @@ function createObjects(
|
||||
const session =
|
||||
Math.random() > 0.3
|
||||
? {
|
||||
id: `session-${i % 10}`,
|
||||
id: `session-${i % 3}`,
|
||||
projectId: projectId,
|
||||
}
|
||||
: undefined;
|
||||
@@ -501,6 +501,8 @@ function createObjects(
|
||||
value: Math.floor(Math.random() * 3) - 1,
|
||||
timestamp: traceTs,
|
||||
source: ScoreSource.REVIEW,
|
||||
projectId,
|
||||
authorUserId: `user-${i}`,
|
||||
},
|
||||
]
|
||||
: []),
|
||||
@@ -512,6 +514,7 @@ function createObjects(
|
||||
value: Math.floor(Math.random() * 10) - 5,
|
||||
timestamp: traceTs,
|
||||
source: ScoreSource.API,
|
||||
projectId,
|
||||
},
|
||||
]
|
||||
: []),
|
||||
@@ -526,9 +529,9 @@ function createObjects(
|
||||
const spanTsStart = new Date(
|
||||
traceTs.getTime() + Math.floor(Math.random() * 30)
|
||||
);
|
||||
// random duration of upto 30ms
|
||||
// random duration of upto 5000ms
|
||||
const spanTsEnd = new Date(
|
||||
spanTsStart.getTime() + Math.floor(Math.random() * 30)
|
||||
spanTsStart.getTime() + Math.floor(Math.random() * 5000)
|
||||
);
|
||||
|
||||
const span = {
|
||||
@@ -572,6 +575,13 @@ function createObjects(
|
||||
(spanTsEnd.getTime() - generationTsStart.getTime())
|
||||
)
|
||||
);
|
||||
// somewhere in the middle
|
||||
const generationTsCompletionStart = new Date(
|
||||
generationTsStart.getTime() +
|
||||
Math.floor(
|
||||
(generationTsEnd.getTime() - generationTsStart.getTime()) / 3
|
||||
)
|
||||
);
|
||||
|
||||
const promptTokens = Math.floor(Math.random() * 1000) + 300;
|
||||
const completionTokens = Math.floor(Math.random() * 500) + 100;
|
||||
@@ -590,7 +600,9 @@ function createObjects(
|
||||
const model = models[Math.floor(Math.random() * models.length)];
|
||||
const promptId =
|
||||
promptIds.get(projectId)![
|
||||
Math.floor(Math.random() * promptIds.get(projectId)!.length)
|
||||
Math.floor(
|
||||
Math.random() * Math.floor(promptIds.get(projectId)!.length / 2)
|
||||
)
|
||||
];
|
||||
|
||||
const generation = {
|
||||
@@ -598,6 +610,8 @@ function createObjects(
|
||||
id: `generation-${v4()}`,
|
||||
startTime: generationTsStart,
|
||||
endTime: generationTsEnd,
|
||||
completionStartTime:
|
||||
Math.random() > 0.5 ? generationTsCompletionStart : undefined,
|
||||
name: `generation-${i}-${j}-${k}`,
|
||||
projectId: trace.projectId,
|
||||
promptId: promptId,
|
||||
@@ -693,6 +707,7 @@ function createObjects(
|
||||
observationId: generation.id,
|
||||
traceId: trace.id,
|
||||
source: ScoreSource.API,
|
||||
projectId: trace.projectId,
|
||||
});
|
||||
if (Math.random() > 0.6)
|
||||
scores.push({
|
||||
@@ -701,6 +716,7 @@ function createObjects(
|
||||
observationId: generation.id,
|
||||
traceId: trace.id,
|
||||
source: ScoreSource.API,
|
||||
projectId: trace.projectId,
|
||||
});
|
||||
|
||||
for (let l = 0; l < Math.floor(Math.random() * 2); l++) {
|
||||
@@ -764,7 +780,7 @@ async function generatePrompts(project: Project) {
|
||||
prompt: "Prompt 1 content",
|
||||
name: "Prompt 1",
|
||||
version: 1,
|
||||
isActive: true,
|
||||
labels: ["production", "latest"],
|
||||
},
|
||||
{
|
||||
id: `prompt-${v4()}`,
|
||||
@@ -773,7 +789,7 @@ async function generatePrompts(project: Project) {
|
||||
prompt: "Prompt 2 content",
|
||||
name: "Prompt 2",
|
||||
version: 1,
|
||||
isActive: true,
|
||||
labels: ["production", "latest"],
|
||||
},
|
||||
{
|
||||
id: `prompt-${v4()}`,
|
||||
@@ -782,7 +798,7 @@ async function generatePrompts(project: Project) {
|
||||
prompt: "Prompt 3 content",
|
||||
name: "Prompt 3 by API",
|
||||
version: 1,
|
||||
isActive: true,
|
||||
labels: ["production", "latest"],
|
||||
},
|
||||
{
|
||||
id: `prompt-${v4()}`,
|
||||
@@ -791,7 +807,7 @@ async function generatePrompts(project: Project) {
|
||||
prompt: "Prompt 4 content",
|
||||
name: "Prompt 4",
|
||||
version: 1,
|
||||
isActive: true,
|
||||
labels: ["production", "latest"],
|
||||
tags: ["tag1", "tag2"],
|
||||
},
|
||||
];
|
||||
@@ -812,7 +828,7 @@ async function generatePrompts(project: Project) {
|
||||
prompt: prompt.prompt,
|
||||
name: prompt.name,
|
||||
version: prompt.version,
|
||||
isActive: prompt.isActive,
|
||||
labels: prompt.labels,
|
||||
tags: prompt.tags,
|
||||
},
|
||||
update: {
|
||||
@@ -833,7 +849,6 @@ async function generatePrompts(project: Project) {
|
||||
temperature: 0.7,
|
||||
},
|
||||
version: 1,
|
||||
isActive: false,
|
||||
},
|
||||
{
|
||||
id: `prompt-${v4()}`,
|
||||
@@ -846,7 +861,7 @@ async function generatePrompts(project: Project) {
|
||||
topP: 0.9,
|
||||
},
|
||||
version: 2,
|
||||
isActive: true,
|
||||
labels: ["production"],
|
||||
},
|
||||
{
|
||||
id: `prompt-${v4()}`,
|
||||
@@ -860,7 +875,7 @@ async function generatePrompts(project: Project) {
|
||||
frequencyPenalty: 0.5,
|
||||
},
|
||||
version: 3,
|
||||
isActive: false,
|
||||
labels: ["production", "latest"],
|
||||
},
|
||||
];
|
||||
|
||||
@@ -881,7 +896,7 @@ async function generatePrompts(project: Project) {
|
||||
name: version.name,
|
||||
config: version.config,
|
||||
version: version.version,
|
||||
isActive: version.isActive,
|
||||
labels: version.labels,
|
||||
},
|
||||
update: {
|
||||
id: version.id,
|
||||
@@ -910,7 +925,7 @@ async function generatePrompts(project: Project) {
|
||||
prompt: `${promptName} version ${i} content`,
|
||||
name: promptName,
|
||||
version: i,
|
||||
isActive: i === 20,
|
||||
labels: i === 20 ? ["production", "latest"] : [],
|
||||
},
|
||||
update: {
|
||||
id: promptId,
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
import { BaseError } from "./BaseError";
|
||||
|
||||
export class ApiError extends BaseError {
|
||||
constructor(description = "Api call failed") {
|
||||
super("ApiError", 500, description, true);
|
||||
}
|
||||
}
|
||||
@@ -7,7 +7,7 @@ export class BaseError extends Error {
|
||||
name: string,
|
||||
httpCode: number,
|
||||
description: string,
|
||||
isOperational: boolean,
|
||||
isOperational: boolean
|
||||
) {
|
||||
super(description);
|
||||
Object.setPrototypeOf(this, new.target.prototype); // restore prototype chain
|
||||
@@ -0,0 +1,7 @@
|
||||
import { BaseError } from "./BaseError";
|
||||
|
||||
export class LangfuseNotFoundError extends BaseError {
|
||||
constructor(description = "Not Found") {
|
||||
super("LangfuseNotFoundError", 404, description, true);
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
export { BaseError } from "./BaseError";
|
||||
export { NotFoundError } from "./NotFoundError";
|
||||
export { LangfuseNotFoundError } from "./NotFoundError";
|
||||
export { ValidationError } from "./ValidationError";
|
||||
export { UnauthorizedError } from "./UnauthorizedError";
|
||||
export { ForbiddenError } from "./ForbiddenError";
|
||||
export { MethodNotAllowedError } from "./MethodNotAllowedError";
|
||||
export { ApiError } from "./ApiError";
|
||||
@@ -1,5 +1,5 @@
|
||||
import z from "zod";
|
||||
import { ModelProvider, observationsTableCols } from "../..";
|
||||
import { ModelProvider, UIModelParams } from "../..";
|
||||
|
||||
export const langfuseObjects = [
|
||||
"trace",
|
||||
@@ -34,6 +34,17 @@ export const wipVariableMapping = z.object({
|
||||
selectedColumnId: z.string().nullish(),
|
||||
});
|
||||
|
||||
const observationCols = [
|
||||
{
|
||||
name: "Metadata",
|
||||
id: "metadata",
|
||||
type: "stringObject",
|
||||
internal: 'o."metadata"',
|
||||
},
|
||||
{ name: "Input", id: "input", internal: 'o."input"' },
|
||||
{ name: "Output", id: "output", internal: 'o."output"' },
|
||||
];
|
||||
|
||||
export const availableEvalVariables = [
|
||||
{
|
||||
id: "trace",
|
||||
@@ -52,54 +63,52 @@ export const availableEvalVariables = [
|
||||
{
|
||||
id: "span",
|
||||
display: "Span",
|
||||
availableColumns: [
|
||||
{
|
||||
name: "Metadata",
|
||||
id: "metadata",
|
||||
type: "stringObject",
|
||||
internal: 'o."metadata"',
|
||||
},
|
||||
{ name: "Input", id: "input", internal: 'o."input"' },
|
||||
{ name: "Output", id: "output", internal: 'o."output"' },
|
||||
],
|
||||
availableColumns: observationCols,
|
||||
},
|
||||
{
|
||||
id: "generation",
|
||||
display: "Generation",
|
||||
availableColumns: [
|
||||
{
|
||||
name: "Metadata",
|
||||
id: "metadata",
|
||||
type: "stringObject",
|
||||
internal: 'o."metadata"',
|
||||
},
|
||||
{ name: "Input", id: "input", internal: 'o."input"' },
|
||||
{ name: "Output", id: "output", internal: 'o."output"' },
|
||||
],
|
||||
availableColumns: observationCols,
|
||||
},
|
||||
{
|
||||
id: "event",
|
||||
display: "Event",
|
||||
availableColumns: observationCols,
|
||||
},
|
||||
{ id: "event", display: "Event", availableColumns: observationsTableCols },
|
||||
];
|
||||
|
||||
export const evalLLMModels = [
|
||||
export const evalLLMModels: UIModelParams[] = [
|
||||
{
|
||||
provider: ModelProvider.OpenAI,
|
||||
model: "gpt-3.5-turbo",
|
||||
temperature: 1,
|
||||
maxTemperature: 2,
|
||||
max_tokens: 256,
|
||||
top_p: 1,
|
||||
provider: { value: ModelProvider.OpenAI, enabled: true },
|
||||
model: { value: "gpt-3.5-turbo", enabled: true },
|
||||
temperature: { value: 1, enabled: true },
|
||||
maxTemperature: { value: 2, enabled: true },
|
||||
max_tokens: { value: 256, enabled: true },
|
||||
top_p: { value: 1, enabled: true },
|
||||
},
|
||||
{
|
||||
provider: ModelProvider.OpenAI,
|
||||
model: "gpt-4-turbo-preview",
|
||||
temperature: 1,
|
||||
maxTemperature: 2,
|
||||
max_tokens: 256,
|
||||
top_p: 1,
|
||||
provider: { value: ModelProvider.OpenAI, enabled: true },
|
||||
model: { value: "gpt-4-turbo-preview", enabled: true },
|
||||
temperature: { value: 1, enabled: true },
|
||||
maxTemperature: { value: 2, enabled: true },
|
||||
max_tokens: { value: 256, enabled: true },
|
||||
top_p: { value: 1, enabled: true },
|
||||
},
|
||||
{
|
||||
provider: { value: ModelProvider.OpenAI, enabled: true },
|
||||
model: { value: "gpt-4o", enabled: true },
|
||||
temperature: { value: 1, enabled: true },
|
||||
maxTemperature: { value: 2, enabled: true },
|
||||
max_tokens: { value: 256, enabled: true },
|
||||
top_p: { value: 1, enabled: true },
|
||||
},
|
||||
] as const;
|
||||
|
||||
export const EvalModelNames = z.enum(["gpt-3.5-turbo", "gpt-4-turbo-preview"]);
|
||||
export const EvalModelNames = z.enum([
|
||||
"gpt-3.5-turbo",
|
||||
"gpt-4-turbo-preview",
|
||||
"gpt-4o",
|
||||
]);
|
||||
|
||||
export const OutputSchema = z.object({
|
||||
reasoning: z.string(),
|
||||
|
||||
@@ -21,3 +21,6 @@ export * from "./features/evals/types";
|
||||
// export db types only
|
||||
export * from "@prisma/client";
|
||||
export { type DB } from "../prisma/generated/types";
|
||||
|
||||
// errors
|
||||
export * from "./errors/index";
|
||||
|
||||
@@ -27,6 +27,7 @@ type LLMCompletionParams = {
|
||||
modelParams: ModelParams;
|
||||
functionCall?: LLMFunctionCall;
|
||||
callbacks?: BaseCallbackHandler[];
|
||||
apiKey?: string;
|
||||
};
|
||||
|
||||
type FetchLLMCompletionParams = LLMCompletionParams & {
|
||||
@@ -55,7 +56,8 @@ export async function fetchLLMCompletion(
|
||||
export async function fetchLLMCompletion(
|
||||
params: FetchLLMCompletionParams
|
||||
): Promise<string | IterableReadableStream<Uint8Array> | unknown> {
|
||||
const { messages, modelParams, streaming, callbacks } = params;
|
||||
// the apiKey must never be printed to the console
|
||||
const { messages, modelParams, streaming, callbacks, apiKey } = params;
|
||||
const finalMessages = messages.map((message) => {
|
||||
if (message.role === ChatMessageRole.User)
|
||||
return new HumanMessage(message.content);
|
||||
@@ -68,7 +70,16 @@ export async function fetchLLMCompletion(
|
||||
let chatModel: ChatOpenAI | ChatAnthropic;
|
||||
if (modelParams.provider === ModelProvider.Anthropic) {
|
||||
chatModel = new ChatAnthropic({
|
||||
anthropicApiKey: process.env.ANTHROPIC_API_KEY,
|
||||
anthropicApiKey: apiKey,
|
||||
modelName: modelParams.model,
|
||||
temperature: modelParams.temperature,
|
||||
maxTokens: modelParams.max_tokens,
|
||||
topP: modelParams.top_p,
|
||||
callbacks,
|
||||
});
|
||||
} else if (modelParams.provider === ModelProvider.OpenAI) {
|
||||
chatModel = new ChatOpenAI({
|
||||
openAIApiKey: apiKey,
|
||||
modelName: modelParams.model,
|
||||
temperature: modelParams.temperature,
|
||||
maxTokens: modelParams.max_tokens,
|
||||
@@ -76,14 +87,9 @@ export async function fetchLLMCompletion(
|
||||
callbacks,
|
||||
});
|
||||
} else {
|
||||
chatModel = new ChatOpenAI({
|
||||
openAIApiKey: process.env.OPENAI_API_KEY,
|
||||
modelName: modelParams.model,
|
||||
temperature: modelParams.temperature,
|
||||
maxTokens: modelParams.max_tokens,
|
||||
topP: modelParams.top_p,
|
||||
callbacks,
|
||||
});
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
const _exhaustiveCheck: never = modelParams;
|
||||
throw new Error("This model provider is not supported.");
|
||||
}
|
||||
|
||||
console.log("Making LLM call with params: ", modelParams);
|
||||
|
||||
@@ -13,25 +13,30 @@ export enum ModelProvider {
|
||||
Anthropic = "anthropic",
|
||||
OpenAI = "openai",
|
||||
}
|
||||
|
||||
export enum ChatMessageRole {
|
||||
System = "system",
|
||||
User = "user",
|
||||
Assistant = "assistant",
|
||||
}
|
||||
export type ModelParams = AnthropicModelParams | OpenAIModelParams;
|
||||
export type UIModelParams = Required<
|
||||
AnthropicModelParams | OpenAIModelParams
|
||||
> & {
|
||||
maxTemperature: number;
|
||||
|
||||
type RecordWithEnabledFlag<T> = {
|
||||
[K in keyof T]: { value: T[K]; enabled: boolean };
|
||||
};
|
||||
export type UIModelParams = RecordWithEnabledFlag<
|
||||
Required<AnthropicModelParams | OpenAIModelParams> & {
|
||||
maxTemperature: number;
|
||||
}
|
||||
>;
|
||||
|
||||
// Generic config
|
||||
export type ModelConfig = z.infer<typeof ZodModelConfig>;
|
||||
|
||||
export const ZodModelConfig = z.object({
|
||||
max_tokens: z.number().optional(),
|
||||
temperature: z.number().optional(),
|
||||
top_p: z.number().optional(),
|
||||
max_tokens: z.coerce.number().optional(),
|
||||
temperature: z.coerce.number().optional(),
|
||||
top_p: z.coerce.number().optional(),
|
||||
});
|
||||
|
||||
// OpenAI
|
||||
@@ -41,6 +46,8 @@ export type OpenAIModelParams = {
|
||||
} & ModelConfig;
|
||||
|
||||
export const openAIModels = [
|
||||
"gpt-4o",
|
||||
"gpt-4o-2024-05-13",
|
||||
"gpt-4-turbo-preview",
|
||||
"gpt-4-1106-preview",
|
||||
"gpt-4-0613",
|
||||
|
||||
@@ -1,8 +1,13 @@
|
||||
import { ObservationLevel } from "@prisma/client";
|
||||
import { ColumnDefinition, OptionsDefinition } from ".";
|
||||
|
||||
export const tracesTableCols: ColumnDefinition[] = [
|
||||
{ name: "⭐️", id: "bookmarked", type: "boolean", internal: "t.bookmarked" },
|
||||
const tracesOnlyCols: ColumnDefinition[] = [
|
||||
{
|
||||
name: "⭐️",
|
||||
id: "bookmarked",
|
||||
type: "boolean",
|
||||
internal: "t.bookmarked",
|
||||
},
|
||||
{ name: "ID", id: "id", type: "string", internal: "t.id" },
|
||||
{
|
||||
name: "Name",
|
||||
@@ -24,6 +29,41 @@ export const tracesTableCols: ColumnDefinition[] = [
|
||||
type: "string",
|
||||
internal: 't."session_id"',
|
||||
},
|
||||
{
|
||||
name: "Metadata",
|
||||
id: "metadata",
|
||||
type: "stringObject",
|
||||
internal: 't."metadata"',
|
||||
},
|
||||
{
|
||||
name: "Version",
|
||||
id: "version",
|
||||
type: "string",
|
||||
internal: 't."version"',
|
||||
},
|
||||
{
|
||||
name: "Release",
|
||||
id: "release",
|
||||
type: "string",
|
||||
internal: 't."release"',
|
||||
},
|
||||
{
|
||||
name: "Level",
|
||||
id: "level",
|
||||
type: "stringOptions",
|
||||
internal: '"level"',
|
||||
options: Object.values(ObservationLevel).map((value) => ({ value })),
|
||||
},
|
||||
{
|
||||
name: "Tags",
|
||||
id: "tags",
|
||||
type: "arrayOptions",
|
||||
internal: 't."tags"',
|
||||
options: [], // to be filled in at runtime
|
||||
},
|
||||
];
|
||||
export const tracesTableCols: ColumnDefinition[] = [
|
||||
...tracesOnlyCols,
|
||||
{
|
||||
name: "Input Tokens",
|
||||
id: "inputTokens",
|
||||
@@ -48,12 +88,7 @@ export const tracesTableCols: ColumnDefinition[] = [
|
||||
type: "number",
|
||||
internal: 'tm."totalTokens"',
|
||||
},
|
||||
{
|
||||
name: "Metadata",
|
||||
id: "metadata",
|
||||
type: "stringObject",
|
||||
internal: 't."metadata"',
|
||||
},
|
||||
|
||||
{
|
||||
name: "Scores",
|
||||
id: "scores_avg",
|
||||
@@ -84,34 +119,10 @@ export const tracesTableCols: ColumnDefinition[] = [
|
||||
type: "number",
|
||||
internal: '"calculatedTotalCost"',
|
||||
},
|
||||
{
|
||||
name: "Version",
|
||||
id: "version",
|
||||
type: "string",
|
||||
internal: 't."version"',
|
||||
},
|
||||
{
|
||||
name: "Release",
|
||||
id: "release",
|
||||
type: "string",
|
||||
internal: 't."release"',
|
||||
},
|
||||
{
|
||||
name: "Level",
|
||||
id: "level",
|
||||
type: "stringOptions",
|
||||
internal: '"level"',
|
||||
options: Object.values(ObservationLevel).map((value) => ({ value })),
|
||||
},
|
||||
{
|
||||
name: "Tags",
|
||||
id: "tags",
|
||||
type: "arrayOptions",
|
||||
internal: 't."tags"',
|
||||
options: [], // to be filled in at runtime
|
||||
},
|
||||
];
|
||||
|
||||
export const evalTableCols: ColumnDefinition[] = tracesOnlyCols;
|
||||
|
||||
export type TraceOptions = {
|
||||
scores_avg: Array<string>;
|
||||
name: Array<OptionsDefinition>;
|
||||
@@ -119,9 +130,10 @@ export type TraceOptions = {
|
||||
};
|
||||
|
||||
export function tracesTableColsWithOptions(
|
||||
options?: TraceOptions
|
||||
options?: TraceOptions,
|
||||
cols: ColumnDefinition[] = tracesTableCols
|
||||
): ColumnDefinition[] {
|
||||
return tracesTableCols.map((col) => {
|
||||
return cols.map((col) => {
|
||||
if (col.id === "scores_avg") {
|
||||
return { ...col, keyOptions: options?.scores_avg ?? [] };
|
||||
}
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
"outDir": "./dist",
|
||||
"types": ["node"],
|
||||
"target": "ES2020",
|
||||
"rootDir": ".",
|
||||
"rootDir": "."
|
||||
},
|
||||
"include": ["."],
|
||||
"exclude": ["node_modules", "dist"]
|
||||
|
||||
Generated
+1254
-817
File diff suppressed because it is too large
Load Diff
+37
-30
@@ -1,14 +1,13 @@
|
||||
{
|
||||
"name": "web",
|
||||
"version": "2.30.2",
|
||||
"version": "2.45.0",
|
||||
"private": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=20"
|
||||
},
|
||||
"scripts": {
|
||||
"copy-openapi-spec": "cp generated/openapi-client/openapi.yml public/openapi-client.yml && cp generated/openapi-server/openapi.yml public/openapi-server.yml",
|
||||
"build": "pnpm run copy-openapi-spec && INLINE_RUNTIME_CHUNK=false dotenv -e ../.env -- next build",
|
||||
"build": "INLINE_RUNTIME_CHUNK=false dotenv -e ../.env -- next build",
|
||||
"dev": "dotenv -e ../.env -- next dev",
|
||||
"lint": "dotenv -e ../.env -- next lint",
|
||||
"lint:fix": "dotenv -e ../.env -- next lint --fix",
|
||||
@@ -23,17 +22,18 @@
|
||||
"dependencies": {
|
||||
"@anthropic-ai/tokenizer": "^0.0.4",
|
||||
"@aws-sdk/client-s3": "^3.550.0",
|
||||
"@aws-sdk/lib-storage": "^3.550.0",
|
||||
"@aws-sdk/lib-storage": "^3.568.0",
|
||||
"@aws-sdk/s3-request-presigner": "^3.554.0",
|
||||
"@codemirror/lang-json": "^6.0.1",
|
||||
"@headlessui/react": "^1.7.19",
|
||||
"@headlessui/react": "1.7.18",
|
||||
"@headlessui/tailwindcss": "0.2.0",
|
||||
"@heroicons/react": "^2.1.3",
|
||||
"@hookform/resolvers": "^3.3.4",
|
||||
"@langchain/anthropic": "^0.1.14",
|
||||
"@langchain/core": "^0.1.58",
|
||||
"@langchain/openai": "^0.0.28",
|
||||
"@langfuse/shared": "workspace:*",
|
||||
"@langchain/anthropic": "^0.1.21",
|
||||
"@langchain/core": "^0.1.61",
|
||||
"@langchain/openai": "^0.0.33",
|
||||
"@langfuse/ee": "workspace:*",
|
||||
"@langfuse/shared": "workspace:*",
|
||||
"@marsidev/react-turnstile": "^0.5.4",
|
||||
"@next-auth/prisma-adapter": "^1.0.7",
|
||||
"@radix-ui/react-accordion": "^1.1.2",
|
||||
@@ -56,51 +56,56 @@
|
||||
"@radix-ui/react-tabs": "^1.0.4",
|
||||
"@radix-ui/react-toggle": "^1.0.3",
|
||||
"@radix-ui/react-tooltip": "^1.0.7",
|
||||
"@react-email/components": "^0.0.16",
|
||||
"@react-email/render": "^0.0.12",
|
||||
"@react-email/components": "^0.0.18",
|
||||
"@react-email/render": "^0.0.14",
|
||||
"@remixicon/react": "^4.2.0",
|
||||
"@repo/eslint-config": "workspace:*",
|
||||
"@repo/typescript-config": "workspace:*",
|
||||
"@sentry/nextjs": "^7.110.1",
|
||||
"@sentry/node": "^7.110.1",
|
||||
"@sentry/profiling-node": "^7.110.1",
|
||||
"@sentry/types": "^7.110.1",
|
||||
"@t3-oss/env-nextjs": "^0.8.0",
|
||||
"@tailwindcss/forms": "^0.5.7",
|
||||
"@sentry/nextjs": "^7.113.0",
|
||||
"@sentry/node": "^7.113.0",
|
||||
"@sentry/profiling-node": "^7.113.0",
|
||||
"@sentry/types": "^7.113.0",
|
||||
"@t3-oss/env-nextjs": "^0.10.1",
|
||||
"@tailwindcss/container-queries": "^0.1.1",
|
||||
"@tanstack/react-query": "^4.36.1",
|
||||
"@tanstack/react-table": "^8.11.8",
|
||||
"@tremor/react": "3.11.1",
|
||||
"@tanstack/react-virtual": "^3.5.0",
|
||||
"@tremor/react": "3.16.2",
|
||||
"@trpc/client": "^10.45.0",
|
||||
"@trpc/next": "^10.45.0",
|
||||
"@trpc/react-query": "^10.45.0",
|
||||
"@trpc/server": "^10.45.0",
|
||||
"@uiw/codemirror-theme-github": "^4.21.25",
|
||||
"@uiw/codemirror-theme-tokyo-night": "^4.22.1",
|
||||
"@uiw/react-codemirror": "^4.21.25",
|
||||
"ai": "^3.0.23",
|
||||
"bcryptjs": "^2.4.3",
|
||||
"class-variance-authority": "^0.7.0",
|
||||
"clsx": "^2.1.0",
|
||||
"clsx": "^2.1.1",
|
||||
"cmdk": "^1.0.0",
|
||||
"core-js": "^3.36.1",
|
||||
"cors": "^2.8.5",
|
||||
"date-fns": "^3.3.1",
|
||||
"decimal.js": "^10.4.3",
|
||||
"dompurify": "^3.1.4",
|
||||
"exponential-backoff": "^3.1.1",
|
||||
"js-tiktoken": "^1.0.11",
|
||||
"js-tiktoken": "^1.0.12",
|
||||
"kysely": "^0.27.3",
|
||||
"langchain": "^0.1.33",
|
||||
"langchain": "^0.1.36",
|
||||
"lodash": "^4.17.21",
|
||||
"lucide-react": "^0.364.0",
|
||||
"next": "^14.2.1",
|
||||
"next": "^14.2.3",
|
||||
"next-auth": "^4.24.7",
|
||||
"next-query-params": "^5.0.0",
|
||||
"next-themes": "^0.3.0",
|
||||
"nodemailer": "^6.9.13",
|
||||
"posthog-js": "^1.122.0",
|
||||
"posthog-node": "^3.6.3",
|
||||
"prisma": "^5.12.1",
|
||||
"prisma": "^5.13.0",
|
||||
"react": "18.2.0",
|
||||
"react-day-picker": "^8.10.1",
|
||||
"react-dom": "18.2.0",
|
||||
"react-hook-form": "^7.51.3",
|
||||
"react-hook-form": "^7.51.5",
|
||||
"react-icons": "^5.0.1",
|
||||
"react-responsive": "^10.0.0",
|
||||
"react18-json-view": "^0.2.8-canary.6",
|
||||
@@ -116,24 +121,26 @@
|
||||
"@jedmao/location": "^3.0.0",
|
||||
"@mermaid-js/mermaid-cli": "^10.7.0",
|
||||
"@playwright/test": "^1.43.1",
|
||||
"@testing-library/jest-dom": "^6.4.2",
|
||||
"@testing-library/react": "^15.0.2",
|
||||
"@tailwindcss/forms": "^0.5.7",
|
||||
"@testing-library/jest-dom": "^6.4.5",
|
||||
"@testing-library/react": "^15.0.7",
|
||||
"@types/bcryptjs": "^2.4.6",
|
||||
"@types/cors": "^2.8.17",
|
||||
"@types/dompurify": "^3.0.5",
|
||||
"@types/eslint": "^8.56.7",
|
||||
"@types/jest": "^29.5.12",
|
||||
"@types/lodash": "^4.14.202",
|
||||
"@types/lodash": "^4.17.4",
|
||||
"@types/node": "20.10.5",
|
||||
"@types/nodemailer": "^6.4.14",
|
||||
"@types/nodemailer": "^6.4.15",
|
||||
"@types/react": "^18.2.79",
|
||||
"@types/react-dom": "^18.2.25",
|
||||
"@types/uuid": "^9.0.8",
|
||||
"@typescript-eslint/eslint-plugin": "^6.21.0",
|
||||
"@typescript-eslint/parser": "^7.7.0",
|
||||
"autoprefixer": "^10.4.19",
|
||||
"dotenv-cli": "^7.3.0",
|
||||
"dotenv-cli": "^7.4.2",
|
||||
"eslint": "^8.56.0",
|
||||
"eslint-config-next": "^14.2.1",
|
||||
"eslint-config-next": "^14.2.3",
|
||||
"jest": "^29.7.0",
|
||||
"jest-environment-jsdom": "^29.7.0",
|
||||
"node-mocks-http": "^1.14.1",
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
openapi*.yml
|
||||
@@ -6,13 +6,23 @@ info:
|
||||
## Authentication
|
||||
|
||||
|
||||
Authenticate with the API using Basic Auth, get API keys in the project
|
||||
settings:
|
||||
Authenticate with the API using [Basic
|
||||
Auth](https://en.wikipedia.org/wiki/Basic_access_authentication), get API
|
||||
keys in the project settings:
|
||||
|
||||
|
||||
- username: Langfuse Public Key
|
||||
|
||||
- password: Langfuse Secret Key
|
||||
|
||||
|
||||
## Exports
|
||||
|
||||
|
||||
- OpenAPI spec: https://cloud.langfuse.com/generated/api/openapi.yml
|
||||
|
||||
- Postman collection:
|
||||
https://cloud.langfuse.com/generated/postman/collection.json
|
||||
paths:
|
||||
/api/public/dataset-items:
|
||||
post:
|
||||
@@ -486,7 +496,7 @@ paths:
|
||||
in: query
|
||||
description: >-
|
||||
Optional filter to only include traces on or after a certain
|
||||
timestamp
|
||||
datetime (ISO 8601)
|
||||
required: false
|
||||
schema:
|
||||
type: string
|
||||
@@ -494,7 +504,9 @@ paths:
|
||||
nullable: true
|
||||
- name: toTimestamp
|
||||
in: query
|
||||
description: Optional filter to only include traces before a certain timestamp
|
||||
description: >-
|
||||
Optional filter to only include traces before a certain datetime
|
||||
(ISO 8601)
|
||||
required: false
|
||||
schema:
|
||||
type: string
|
||||
@@ -638,7 +650,7 @@ paths:
|
||||
in: query
|
||||
description: >-
|
||||
Retrieve only observations with a start_time greater than this
|
||||
timestamp.
|
||||
datetime (ISO 8601).
|
||||
required: false
|
||||
schema:
|
||||
type: string
|
||||
@@ -717,19 +729,100 @@ paths:
|
||||
application/json:
|
||||
schema: {}
|
||||
security: *ref_0
|
||||
/api/public/prompts:
|
||||
/api/public/v2/prompts/{promptName}:
|
||||
get:
|
||||
description: Get a prompt
|
||||
operationId: prompts_get
|
||||
tags:
|
||||
- Prompts
|
||||
parameters:
|
||||
- name: name
|
||||
in: query
|
||||
- name: promptName
|
||||
in: path
|
||||
description: The name of the prompt
|
||||
required: true
|
||||
schema:
|
||||
type: string
|
||||
- name: version
|
||||
in: query
|
||||
description: Version of the prompt to be retrieved.
|
||||
required: false
|
||||
schema:
|
||||
type: integer
|
||||
nullable: true
|
||||
- name: label
|
||||
in: query
|
||||
description: >-
|
||||
Label of the prompt to be retrieved. Defaults to "production" if no
|
||||
label or version is set.
|
||||
required: false
|
||||
schema:
|
||||
type: string
|
||||
nullable: true
|
||||
responses:
|
||||
'200':
|
||||
description: ''
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/Prompt'
|
||||
'400':
|
||||
description: ''
|
||||
content:
|
||||
application/json:
|
||||
schema: {}
|
||||
'401':
|
||||
description: ''
|
||||
content:
|
||||
application/json:
|
||||
schema: {}
|
||||
'403':
|
||||
description: ''
|
||||
content:
|
||||
application/json:
|
||||
schema: {}
|
||||
'404':
|
||||
description: ''
|
||||
content:
|
||||
application/json:
|
||||
schema: {}
|
||||
'405':
|
||||
description: ''
|
||||
content:
|
||||
application/json:
|
||||
schema: {}
|
||||
security: *ref_0
|
||||
/api/public/v2/prompts:
|
||||
get:
|
||||
description: Get a list of prompt names with versions and labels
|
||||
operationId: prompts_list
|
||||
tags:
|
||||
- Prompts
|
||||
parameters:
|
||||
- name: name
|
||||
in: query
|
||||
required: false
|
||||
schema:
|
||||
type: string
|
||||
nullable: true
|
||||
- name: label
|
||||
in: query
|
||||
required: false
|
||||
schema:
|
||||
type: string
|
||||
nullable: true
|
||||
- name: tag
|
||||
in: query
|
||||
required: false
|
||||
schema:
|
||||
type: string
|
||||
nullable: true
|
||||
- name: page
|
||||
in: query
|
||||
required: false
|
||||
schema:
|
||||
type: integer
|
||||
nullable: true
|
||||
- name: limit
|
||||
in: query
|
||||
required: false
|
||||
schema:
|
||||
@@ -741,7 +834,7 @@ paths:
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/Prompt'
|
||||
$ref: '#/components/schemas/PromptMetaListResponse'
|
||||
'400':
|
||||
description: ''
|
||||
content:
|
||||
@@ -895,7 +988,7 @@ paths:
|
||||
nullable: true
|
||||
- name: fromTimestamp
|
||||
in: query
|
||||
description: Retrieve only scores newer than this timestamp.
|
||||
description: Retrieve only scores newer than this datetime (ISO 8601).
|
||||
required: false
|
||||
schema:
|
||||
type: string
|
||||
@@ -1171,7 +1264,7 @@ paths:
|
||||
nullable: true
|
||||
- name: fromTimestamp
|
||||
in: query
|
||||
description: Retrieve only traces newer than this timestamp.
|
||||
description: Retrieve only traces newer than this datetime (ISO 8601).
|
||||
required: false
|
||||
schema:
|
||||
type: string
|
||||
@@ -1757,6 +1850,10 @@ components:
|
||||
type: string
|
||||
nullable: true
|
||||
description: Dataset items are upserted on their id
|
||||
status:
|
||||
$ref: '#/components/schemas/DatasetStatus'
|
||||
nullable: true
|
||||
description: Defaults to ACTIVE for newly created items
|
||||
required:
|
||||
- datasetName
|
||||
CreateDatasetRunItemRequest:
|
||||
@@ -2479,6 +2576,42 @@ components:
|
||||
required:
|
||||
- id
|
||||
- name
|
||||
PromptMetaListResponse:
|
||||
title: PromptMetaListResponse
|
||||
type: object
|
||||
properties:
|
||||
data:
|
||||
type: array
|
||||
items:
|
||||
$ref: '#/components/schemas/PromptMeta'
|
||||
meta:
|
||||
$ref: '#/components/schemas/utilsMetaResponse'
|
||||
required:
|
||||
- data
|
||||
- meta
|
||||
PromptMeta:
|
||||
title: PromptMeta
|
||||
type: object
|
||||
properties:
|
||||
name:
|
||||
type: string
|
||||
versions:
|
||||
type: array
|
||||
items:
|
||||
type: integer
|
||||
labels:
|
||||
type: array
|
||||
items:
|
||||
type: string
|
||||
tags:
|
||||
type: array
|
||||
items:
|
||||
type: string
|
||||
required:
|
||||
- name
|
||||
- versions
|
||||
- labels
|
||||
- tags
|
||||
CreatePromptRequest:
|
||||
title: CreatePromptRequest
|
||||
oneOf:
|
||||
@@ -2510,18 +2643,19 @@ components:
|
||||
properties:
|
||||
name:
|
||||
type: string
|
||||
isActive:
|
||||
type: boolean
|
||||
description: Should the prompt be promoted to production immediately?
|
||||
prompt:
|
||||
type: array
|
||||
items:
|
||||
$ref: '#/components/schemas/ChatMessage'
|
||||
config:
|
||||
nullable: true
|
||||
labels:
|
||||
type: array
|
||||
items:
|
||||
type: string
|
||||
nullable: true
|
||||
required:
|
||||
- name
|
||||
- isActive
|
||||
- prompt
|
||||
CreateTextPromptRequest:
|
||||
title: CreateTextPromptRequest
|
||||
@@ -2529,16 +2663,17 @@ components:
|
||||
properties:
|
||||
name:
|
||||
type: string
|
||||
isActive:
|
||||
type: boolean
|
||||
description: Should the prompt be promoted to production immediately?
|
||||
prompt:
|
||||
type: string
|
||||
config:
|
||||
nullable: true
|
||||
labels:
|
||||
type: array
|
||||
items:
|
||||
type: string
|
||||
nullable: true
|
||||
required:
|
||||
- name
|
||||
- isActive
|
||||
- prompt
|
||||
Prompt:
|
||||
title: Prompt
|
||||
@@ -2574,10 +2709,15 @@ components:
|
||||
version:
|
||||
type: integer
|
||||
config: {}
|
||||
labels:
|
||||
type: array
|
||||
items:
|
||||
type: string
|
||||
required:
|
||||
- name
|
||||
- version
|
||||
- config
|
||||
- labels
|
||||
ChatMessage:
|
||||
title: ChatMessage
|
||||
type: object
|
||||
@@ -2,7 +2,7 @@
|
||||
"info": {
|
||||
"name": "Langfuse",
|
||||
"schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json",
|
||||
"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"
|
||||
"description": "## Authentication\n\nAuthenticate with the API using [Basic Auth](https://en.wikipedia.org/wiki/Basic_access_authentication), get API keys in the project settings:\n\n- username: Langfuse Public Key\n- password: Langfuse Secret Key\n\n## Exports\n\n- OpenAPI spec: https://cloud.langfuse.com/generated/api/openapi.yml\n- Postman collection: https://cloud.langfuse.com/generated/postman/collection.json"
|
||||
},
|
||||
"variable": [
|
||||
{
|
||||
@@ -65,7 +65,7 @@
|
||||
"auth": null,
|
||||
"body": {
|
||||
"mode": "raw",
|
||||
"raw": "{\n \"datasetName\": \"example\",\n \"input\": \"UNKNOWN\",\n \"expectedOutput\": \"UNKNOWN\",\n \"metadata\": \"UNKNOWN\",\n \"sourceTraceId\": \"example\",\n \"sourceObservationId\": \"example\",\n \"id\": \"example\"\n}",
|
||||
"raw": "{\n \"datasetName\": \"example\",\n \"input\": \"UNKNOWN\",\n \"expectedOutput\": \"UNKNOWN\",\n \"metadata\": \"UNKNOWN\",\n \"sourceTraceId\": \"example\",\n \"sourceObservationId\": \"example\",\n \"id\": \"example\",\n \"status\": \"ACTIVE\"\n}",
|
||||
"options": {
|
||||
"raw": {
|
||||
"language": "json"
|
||||
@@ -418,12 +418,12 @@
|
||||
{
|
||||
"key": "fromTimestamp",
|
||||
"value": "",
|
||||
"description": "Optional filter to only include traces on or after a certain timestamp"
|
||||
"description": "Optional filter to only include traces on or after a certain datetime (ISO 8601)"
|
||||
},
|
||||
{
|
||||
"key": "toTimestamp",
|
||||
"value": "",
|
||||
"description": "Optional filter to only include traces before a certain timestamp"
|
||||
"description": "Optional filter to only include traces before a certain datetime (ISO 8601)"
|
||||
}
|
||||
],
|
||||
"variable": []
|
||||
@@ -528,7 +528,7 @@
|
||||
{
|
||||
"key": "fromStartTime",
|
||||
"value": "",
|
||||
"description": "Retrieve only observations with a start_time greater than this timestamp."
|
||||
"description": "Retrieve only observations with a start_time greater than this datetime (ISO 8601)."
|
||||
}
|
||||
],
|
||||
"variable": []
|
||||
@@ -585,13 +585,58 @@
|
||||
"request": {
|
||||
"description": "Get a prompt",
|
||||
"url": {
|
||||
"raw": "{{baseUrl}}/api/public/prompts?name=&version=",
|
||||
"raw": "{{baseUrl}}/api/public/v2/prompts/:promptName?version=&label=",
|
||||
"host": [
|
||||
"{{baseUrl}}"
|
||||
],
|
||||
"path": [
|
||||
"api",
|
||||
"public",
|
||||
"v2",
|
||||
"prompts",
|
||||
":promptName"
|
||||
],
|
||||
"query": [
|
||||
{
|
||||
"key": "version",
|
||||
"value": "",
|
||||
"description": "Version of the prompt to be retrieved."
|
||||
},
|
||||
{
|
||||
"key": "label",
|
||||
"value": "",
|
||||
"description": "Label of the prompt to be retrieved. Defaults to \"production\" if no label or version is set."
|
||||
}
|
||||
],
|
||||
"variable": [
|
||||
{
|
||||
"key": "promptName",
|
||||
"value": "",
|
||||
"description": "The name of the prompt"
|
||||
}
|
||||
]
|
||||
},
|
||||
"header": [],
|
||||
"method": "GET",
|
||||
"auth": null,
|
||||
"body": null
|
||||
},
|
||||
"response": []
|
||||
},
|
||||
{
|
||||
"_type": "endpoint",
|
||||
"name": "List",
|
||||
"request": {
|
||||
"description": "Get a list of prompt names with versions and labels",
|
||||
"url": {
|
||||
"raw": "{{baseUrl}}/api/public/v2/prompts?name=&label=&tag=&page=&limit=",
|
||||
"host": [
|
||||
"{{baseUrl}}"
|
||||
],
|
||||
"path": [
|
||||
"api",
|
||||
"public",
|
||||
"v2",
|
||||
"prompts"
|
||||
],
|
||||
"query": [
|
||||
@@ -601,7 +646,22 @@
|
||||
"description": null
|
||||
},
|
||||
{
|
||||
"key": "version",
|
||||
"key": "label",
|
||||
"value": "",
|
||||
"description": null
|
||||
},
|
||||
{
|
||||
"key": "tag",
|
||||
"value": "",
|
||||
"description": null
|
||||
},
|
||||
{
|
||||
"key": "page",
|
||||
"value": "",
|
||||
"description": null
|
||||
},
|
||||
{
|
||||
"key": "limit",
|
||||
"value": "",
|
||||
"description": null
|
||||
}
|
||||
@@ -621,13 +681,14 @@
|
||||
"request": {
|
||||
"description": "Create a prompt",
|
||||
"url": {
|
||||
"raw": "{{baseUrl}}/api/public/prompts",
|
||||
"raw": "{{baseUrl}}/api/public/v2/prompts",
|
||||
"host": [
|
||||
"{{baseUrl}}"
|
||||
],
|
||||
"path": [
|
||||
"api",
|
||||
"public",
|
||||
"v2",
|
||||
"prompts"
|
||||
],
|
||||
"query": [],
|
||||
@@ -638,7 +699,7 @@
|
||||
"auth": null,
|
||||
"body": {
|
||||
"mode": "raw",
|
||||
"raw": "{\n \"type\": \"chat\",\n \"name\": \"example\",\n \"isActive\": true,\n \"prompt\": [\n {\n \"role\": \"example\",\n \"content\": \"example\"\n }\n ],\n \"config\": \"UNKNOWN\"\n}",
|
||||
"raw": "{\n \"type\": \"chat\",\n \"name\": \"example\",\n \"prompt\": [\n {\n \"role\": \"example\",\n \"content\": \"example\"\n }\n ],\n \"config\": \"UNKNOWN\",\n \"labels\": [\n \"example\"\n ]\n}",
|
||||
"options": {
|
||||
"raw": {
|
||||
"language": "json"
|
||||
@@ -727,7 +788,7 @@
|
||||
{
|
||||
"key": "fromTimestamp",
|
||||
"value": "",
|
||||
"description": "Retrieve only scores newer than this timestamp."
|
||||
"description": "Retrieve only scores newer than this datetime (ISO 8601)."
|
||||
},
|
||||
{
|
||||
"key": "source",
|
||||
@@ -935,7 +996,7 @@
|
||||
{
|
||||
"key": "fromTimestamp",
|
||||
"value": "",
|
||||
"description": "Retrieve only traces newer than this timestamp."
|
||||
"description": "Retrieve only traces newer than this datetime (ISO 8601)."
|
||||
},
|
||||
{
|
||||
"key": "orderBy",
|
||||
@@ -62,6 +62,7 @@ export async function modelMatch() {
|
||||
where: {
|
||||
internalModel: null,
|
||||
type: "GENERATION",
|
||||
model: { not: null },
|
||||
},
|
||||
take: BATCH_SIZE,
|
||||
skip: index * BATCH_SIZE,
|
||||
@@ -235,14 +236,23 @@ export async function modelMatch() {
|
||||
}
|
||||
}
|
||||
|
||||
await prisma.observation.updateMany({
|
||||
where: {
|
||||
internalModel: "LANGFUSETMPNOMODEL",
|
||||
},
|
||||
data: {
|
||||
internalModel: null,
|
||||
},
|
||||
});
|
||||
let updatedCount;
|
||||
do {
|
||||
console.log(`Updating LANGFUSETMPNOMODEL ${updatedCount}`);
|
||||
const result = await prisma.$queryRaw<[{ id: string }]>`
|
||||
WITH to_update AS (
|
||||
SELECT id
|
||||
FROM observations
|
||||
WHERE internal_model = 'LANGFUSETMPNOMODEL'
|
||||
AND "type" = 'GENERATION'
|
||||
LIMIT 50000
|
||||
)
|
||||
UPDATE observations
|
||||
set internal_model = NULL
|
||||
WHERE id IN (SELECT id FROM to_update)
|
||||
RETURNING id;`;
|
||||
updatedCount = result.length;
|
||||
} while (updatedCount > 0);
|
||||
|
||||
const end = Date.now();
|
||||
|
||||
|
||||
@@ -15,13 +15,21 @@ if (process.env.NEXT_PUBLIC_SENTRY_DSN)
|
||||
tracesSampler: (samplingContext: SamplingContext) => {
|
||||
if (
|
||||
samplingContext.request &&
|
||||
samplingContext.request.method === "POST" &&
|
||||
samplingContext.request.url &&
|
||||
samplingContext.request.url.includes("api/trpc")
|
||||
) {
|
||||
return 0.9;
|
||||
return 0.3;
|
||||
}
|
||||
return 0.15;
|
||||
if (
|
||||
samplingContext.request &&
|
||||
samplingContext.request.url &&
|
||||
samplingContext.request.url.includes("api/auth") &&
|
||||
samplingContext.transactionContext.status !== "ok" &&
|
||||
samplingContext.transactionContext.status !== "unauthenticated"
|
||||
) {
|
||||
return 1;
|
||||
}
|
||||
return 0.1;
|
||||
},
|
||||
|
||||
profilesSampleRate: 0.2, // Profiling sample rate is relative to tracesSampleRate
|
||||
@@ -29,6 +37,7 @@ if (process.env.NEXT_PUBLIC_SENTRY_DSN)
|
||||
// Add profiling integration to list of integrations
|
||||
new ProfilingIntegration(),
|
||||
new Sentry.Integrations.Prisma({ client: prisma }),
|
||||
Sentry.metrics.metricsAggregatorIntegration(),
|
||||
],
|
||||
|
||||
// filter out passwords from the signup request body
|
||||
|
||||
@@ -70,3 +70,38 @@ test("Signup input validation", async ({ page }) => {
|
||||
// random email address to be used in tests
|
||||
const randomEmailAddress = () =>
|
||||
Math.random().toString(36).substring(2, 11) + "@example.com";
|
||||
|
||||
test("Unauthenticated user should be redirected to target URL after login", async ({
|
||||
page,
|
||||
}) => {
|
||||
await page.goto("/auth/sign-in");
|
||||
await page.fill('input[name="email"]', "demo@langfuse.com");
|
||||
await page.fill('input[type="password"]', "password");
|
||||
await page.click('button[data-testid="submit-email-password-sign-in-form"]');
|
||||
|
||||
// wait 2 seconds
|
||||
await page.waitForTimeout(2000);
|
||||
|
||||
// project id and prompt from seed.ts
|
||||
const promptUrl = "/project/7a88fb47-b4e2-43b8-a06c-a5ce950dc53a/prompts/summary-prompt";
|
||||
|
||||
await page.getByRole("button", { name: /Demo User/ }).click();
|
||||
|
||||
await page.getByRole("menuitem", { name: "Sign Out" }).click();
|
||||
|
||||
await expect(page).toHaveURL("/auth/sign-in");
|
||||
|
||||
await page.goto(promptUrl);
|
||||
|
||||
await page.waitForTimeout(2000);
|
||||
|
||||
await expect(page).toHaveURL(/targetPath/);
|
||||
|
||||
await page.fill('input[name="email"]', "demo@langfuse.com");
|
||||
await page.fill('input[type="password"]', "password");
|
||||
await page.click('button[data-testid="submit-email-password-sign-in-form"]');
|
||||
|
||||
await page.waitForTimeout(2000);
|
||||
|
||||
await expect(page).toHaveURL(promptUrl);
|
||||
});
|
||||
|
||||
@@ -210,11 +210,14 @@ describe("/api/public/datasets and /api/public/dataset-items API Endpoints", ()
|
||||
datasetName: "dataset-name",
|
||||
input: { key: "value2" },
|
||||
metadata: ["hello-world"],
|
||||
status: "ARCHIVED",
|
||||
});
|
||||
expect(item2.status).toBe(200);
|
||||
expect(item2.body).toMatchObject({
|
||||
id: "dataset-item-id",
|
||||
input: { key: "value2" },
|
||||
metadata: ["hello-world"],
|
||||
status: "ARCHIVED",
|
||||
});
|
||||
|
||||
const dbDatasetItem = await prisma.datasetItem.findFirst({
|
||||
@@ -223,6 +226,7 @@ describe("/api/public/datasets and /api/public/dataset-items API Endpoints", ()
|
||||
expect(dbDatasetItem).not.toBeNull();
|
||||
expect(dbDatasetItem?.input).toMatchObject({ key: "value2" });
|
||||
expect(dbDatasetItem?.metadata).toMatchObject(["hello-world"]);
|
||||
expect(dbDatasetItem?.status).toBe("ARCHIVED");
|
||||
});
|
||||
|
||||
it("should create and get a dataset run, include special characters", async () => {
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
import * as DOMPurify from "dompurify";
|
||||
|
||||
// Create test cases
|
||||
describe("should sanitize dom", () => {
|
||||
it("should sanitize urls", () => {
|
||||
const dirty =
|
||||
"http://www.some.site/page.html?default=<script>alert(document.cookie)</script>";
|
||||
const cleam = DOMPurify.sanitize(dirty);
|
||||
|
||||
expect(cleam).toBe("http://www.some.site/page.html?default=");
|
||||
});
|
||||
it("should should not sanitize valid langfuse urls", () => {
|
||||
const validLangfuseUrl =
|
||||
"https://cloud.langfuse.com/project/abjdnksadn/traces?filter=timestamp%3Bdatetime%3B%3B%3E%3B2024-04-19T00%253A00%253A00.000Z%2Cid%3Bstring%3B%3Bcontains%3Ba%2Ctimestamp%3Bdatetime%3B%3B%3C%3B2024-04-30T22%253A00%253A00.000Z%2CinputTokens%3Bnumber%3B%3B%3E%3B1";
|
||||
const cleam = DOMPurify.sanitize(validLangfuseUrl);
|
||||
|
||||
expect(cleam).toBe(validLangfuseUrl);
|
||||
});
|
||||
});
|
||||
@@ -281,6 +281,7 @@ describe("/api/public/ingestion API Endpoint", () => {
|
||||
expect(dbScore?.name).toBe("score-name");
|
||||
expect(dbScore?.value).toBe(100.5);
|
||||
expect(dbScore?.observationId).toBeNull();
|
||||
expect(dbScore?.projectId).toBe("7a88fb47-b4e2-43b8-a06c-a5ce950dc53a");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -726,6 +727,7 @@ describe("/api/public/ingestion API Endpoint", () => {
|
||||
metadata: { key: "value" },
|
||||
release: "1.0.0",
|
||||
version: "2.0.0",
|
||||
tags: ["tag-1", "tag-2", "tag-2"],
|
||||
},
|
||||
},
|
||||
],
|
||||
@@ -742,6 +744,7 @@ describe("/api/public/ingestion API Endpoint", () => {
|
||||
id: traceId,
|
||||
name: "trace-name",
|
||||
userId: "user-2",
|
||||
tags: ["tag-1", "tag-4", "tag-3"],
|
||||
},
|
||||
},
|
||||
],
|
||||
@@ -762,6 +765,8 @@ describe("/api/public/ingestion API Endpoint", () => {
|
||||
expect(dbTrace[0]?.externalId).toBeNull();
|
||||
expect(dbTrace[0]?.version).toBe("2.0.0");
|
||||
expect(dbTrace[0]?.projectId).toBe("7a88fb47-b4e2-43b8-a06c-a5ce950dc53a");
|
||||
expect(dbTrace[0]?.tags).toEqual(["tag-1", "tag-2", "tag-3", "tag-4"]);
|
||||
expect(dbTrace[0]?.tags.length).toBe(4);
|
||||
});
|
||||
|
||||
it("should fail for wrong event formats", async () => {
|
||||
@@ -816,57 +821,6 @@ describe("/api/public/ingestion API Endpoint", () => {
|
||||
expect(dbTrace.length).toBe(1);
|
||||
});
|
||||
|
||||
it("should fail for auth errors", async () => {
|
||||
const traceId = v4();
|
||||
const scoreId = v4();
|
||||
|
||||
const responseOne = await makeAPICall("POST", "/api/public/ingestion", {
|
||||
batch: [
|
||||
{
|
||||
id: v4(),
|
||||
type: "trace-create",
|
||||
timestamp: new Date().toISOString(),
|
||||
body: {
|
||||
id: traceId,
|
||||
name: "trace-name",
|
||||
userId: "user-1",
|
||||
metadata: { key: "value" },
|
||||
release: "1.0.0",
|
||||
version: "2.0.0",
|
||||
},
|
||||
},
|
||||
{
|
||||
id: v4(),
|
||||
type: "score-create",
|
||||
timestamp: new Date().toISOString(),
|
||||
body: {
|
||||
id: scoreId,
|
||||
name: "score-name",
|
||||
value: 100.5,
|
||||
traceId: "some-random-id",
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
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();
|
||||
|
||||
@@ -1653,121 +1607,6 @@ IB Home / . . . / News / News about the IB / Why ChatGPT is an o
|
||||
expect(dbScore?.value).toBe(scoreValue);
|
||||
});
|
||||
|
||||
it("should error on Bearer Auth for a trace from different project", async () => {
|
||||
const otherProjectId = "other_project_id";
|
||||
const traceId = "trace_id";
|
||||
const bearerAuth = "Bearer pk-lf-1234567890";
|
||||
|
||||
const scoreId = "score_id";
|
||||
const scoreEventId = "score_event_id";
|
||||
const scoreName = "score-name";
|
||||
const scoreValue = 100.5;
|
||||
|
||||
// Seed db with a trace to be scored
|
||||
try {
|
||||
await prisma.project.create({
|
||||
data: {
|
||||
id: otherProjectId,
|
||||
name: "another-project",
|
||||
},
|
||||
});
|
||||
|
||||
await prisma.trace.create({
|
||||
data: {
|
||||
id: traceId,
|
||||
name: "trace-name",
|
||||
project: { connect: { id: otherProjectId } },
|
||||
},
|
||||
});
|
||||
|
||||
const response = await makeAPICall(
|
||||
"POST",
|
||||
"/api/public/ingestion",
|
||||
{
|
||||
batch: [
|
||||
{
|
||||
id: scoreEventId,
|
||||
type: "score-create",
|
||||
timestamp: new Date().toISOString(),
|
||||
body: {
|
||||
id: scoreId,
|
||||
name: scoreName,
|
||||
value: scoreValue,
|
||||
traceId: traceId,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
bearerAuth,
|
||||
);
|
||||
|
||||
expect(response.status).toBe(207);
|
||||
expect(response.body.successes.length).toBe(0);
|
||||
expect(response.body).toHaveProperty("errors");
|
||||
expect(response.body.errors.length).toEqual(1);
|
||||
expect(response.body.errors[0]?.id).toEqual(scoreEventId);
|
||||
|
||||
expect(await prisma.trace.count()).toBe(1);
|
||||
expect(await prisma.trace.count({ where: { id: traceId } })).toBe(1);
|
||||
|
||||
const dbScore = await prisma.score.findUnique({
|
||||
where: {
|
||||
id: scoreId,
|
||||
},
|
||||
});
|
||||
|
||||
expect(dbScore).toBeNull();
|
||||
} finally {
|
||||
await prisma.project.delete({ where: { id: otherProjectId } });
|
||||
}
|
||||
});
|
||||
|
||||
it("should error on Bearer Auth with a trace that does not exist", async () => {
|
||||
const traceId = "trace_id";
|
||||
const bearerAuth = "Bearer pk-lf-1234567890";
|
||||
|
||||
const scoreId = "score_id";
|
||||
const scoreEventId = "score_event_id";
|
||||
const scoreName = "score-name";
|
||||
const scoreValue = 100.5;
|
||||
|
||||
const response = await makeAPICall(
|
||||
"POST",
|
||||
"/api/public/ingestion",
|
||||
{
|
||||
batch: [
|
||||
{
|
||||
id: scoreEventId,
|
||||
type: "score-create",
|
||||
timestamp: new Date().toISOString(),
|
||||
body: {
|
||||
id: scoreId,
|
||||
name: scoreName,
|
||||
value: scoreValue,
|
||||
traceId: traceId,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
bearerAuth,
|
||||
);
|
||||
|
||||
expect(response.status).toBe(207);
|
||||
expect(response.body.successes.length).toBe(0);
|
||||
expect(response.body.errors.length).toBe(1);
|
||||
expect(response.body.errors[0]?.id).toBe(scoreEventId);
|
||||
|
||||
expect(await prisma.trace.count()).toBe(0);
|
||||
|
||||
const dbScore = await prisma.score.findUnique({
|
||||
where: {
|
||||
id: scoreId,
|
||||
},
|
||||
});
|
||||
|
||||
expect(dbScore).toBeNull();
|
||||
});
|
||||
|
||||
it("should error on wrong input", async () => {
|
||||
const traceId = "trace_id";
|
||||
const bearerAuth = "Bearer pk-lf-1234567890";
|
||||
|
||||
@@ -0,0 +1,92 @@
|
||||
/** @jest-environment node */
|
||||
/* eslint-disable @typescript-eslint/no-unsafe-member-access */
|
||||
|
||||
import type { Session } from "next-auth";
|
||||
import { pruneDatabase } from "@/src/__tests__/test-utils";
|
||||
import { ModelProvider } from "@langfuse/shared";
|
||||
import { prisma } from "@langfuse/shared/src/db";
|
||||
import { appRouter } from "@/src/server/api/root";
|
||||
import { createInnerTRPCContext } from "@/src/server/api/trpc";
|
||||
|
||||
describe("llmApiKey.all RPC", () => {
|
||||
const projectId = "7a88fb47-b4e2-43b8-a06c-a5ce950dc53a";
|
||||
|
||||
beforeEach(async () => await pruneDatabase());
|
||||
|
||||
const session: Session = {
|
||||
expires: "1",
|
||||
user: {
|
||||
id: "user-1",
|
||||
name: "Demo User",
|
||||
projects: [
|
||||
{
|
||||
id: projectId,
|
||||
role: "ADMIN",
|
||||
name: "test",
|
||||
},
|
||||
],
|
||||
featureFlags: {
|
||||
templateFlag: true,
|
||||
evals: true,
|
||||
},
|
||||
admin: true,
|
||||
},
|
||||
};
|
||||
|
||||
const ctx = createInnerTRPCContext({ session });
|
||||
const caller = appRouter.createCaller({ ...ctx, prisma });
|
||||
|
||||
it("should create an llm api key", async () => {
|
||||
const secret = "test-secret";
|
||||
|
||||
await caller.llmApiKey.create({
|
||||
projectId,
|
||||
secretKey: secret,
|
||||
provider: ModelProvider.OpenAI,
|
||||
});
|
||||
|
||||
const llmApiKeys = await prisma.llmApiKeys.findMany({
|
||||
where: {
|
||||
projectId,
|
||||
},
|
||||
});
|
||||
|
||||
expect(llmApiKeys.length).toBe(1);
|
||||
expect(llmApiKeys[0].projectId).toBe(projectId);
|
||||
expect(llmApiKeys[0].secretKey).not.toBeNull();
|
||||
expect(llmApiKeys[0].secretKey).not.toEqual(secret);
|
||||
expect(llmApiKeys[0].provider).toBe(ModelProvider.OpenAI);
|
||||
// this has to be 3 dots and the last 4 characters of the secret
|
||||
expect(llmApiKeys[0].displaySecretKey).toMatch(/^...[a-zA-Z0-9]{4}$/);
|
||||
});
|
||||
|
||||
it("should create and get an llm api key", async () => {
|
||||
const secret = "test-secret";
|
||||
|
||||
await caller.llmApiKey.create({
|
||||
projectId,
|
||||
secretKey: secret,
|
||||
provider: ModelProvider.OpenAI,
|
||||
});
|
||||
|
||||
const dbLlmApiKeys = await prisma.llmApiKeys.findMany({
|
||||
where: {
|
||||
projectId,
|
||||
},
|
||||
});
|
||||
|
||||
expect(dbLlmApiKeys.length).toBe(1);
|
||||
|
||||
const llmApiKeys = await caller.llmApiKey.all({
|
||||
projectId,
|
||||
});
|
||||
|
||||
expect(llmApiKeys.data.length).toBe(1);
|
||||
expect(llmApiKeys.data[0].provider).toBe(ModelProvider.OpenAI);
|
||||
// this has to be 3 dots and the last 4 characters of the secret
|
||||
expect(llmApiKeys.data[0].displaySecretKey).toMatch(/^...[a-zA-Z0-9]{4}$/);
|
||||
|
||||
// response must not contain the secret key itself
|
||||
expect(llmApiKeys.data[0]).not.toHaveProperty("secretKey");
|
||||
});
|
||||
});
|
||||
@@ -73,6 +73,15 @@ describe("model match", () => {
|
||||
input: "I am a prompt",
|
||||
output: "I am a completion",
|
||||
},
|
||||
{
|
||||
type: "GENERATION",
|
||||
projectId: "7a88fb47-b4e2-43b8-a06c-a5ce950dc53a",
|
||||
model: "gpt-4o",
|
||||
startTime: new Date("2024-01-01T00:00:00.000Z"),
|
||||
unit: ModelUsageUnit.Tokens,
|
||||
input: "I am a prompt",
|
||||
output: "I am a completion",
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
@@ -86,7 +95,7 @@ describe("model match", () => {
|
||||
expect(observation1?.completionTokens).toEqual(3000);
|
||||
expect(observation1?.totalTokens).toEqual(0);
|
||||
|
||||
expect(observations.length).toEqual(3);
|
||||
expect(observations.length).toEqual(4);
|
||||
observations.forEach((observation) => {
|
||||
expect(observation.internalModel).toBeDefined();
|
||||
expect(observation.promptTokens).toBeGreaterThan(0);
|
||||
|
||||
+194
-60
@@ -5,10 +5,10 @@ import { makeAPICall, pruneDatabase } from "@/src/__tests__/test-utils";
|
||||
import { v4 as uuidv4, v4 } from "uuid";
|
||||
import { type Prompt } from "@langfuse/shared";
|
||||
import {
|
||||
PromptSchema,
|
||||
LegacyPromptSchema,
|
||||
PromptType,
|
||||
type ValidatedPrompt,
|
||||
} from "@/src/features/prompts/server/validation";
|
||||
type LegacyValidatedPrompt,
|
||||
} from "@/src/features/prompts/server/utils/validation";
|
||||
|
||||
describe("/api/public/prompts API Endpoint", () => {
|
||||
beforeEach(async () => await pruneDatabase());
|
||||
@@ -21,7 +21,7 @@ describe("/api/public/prompts API Endpoint", () => {
|
||||
id: promptId,
|
||||
name: "prompt-name",
|
||||
prompt: "prompt",
|
||||
isActive: true,
|
||||
labels: ["production"],
|
||||
version: 1,
|
||||
config: {
|
||||
temperature: 0.1,
|
||||
@@ -51,6 +51,7 @@ describe("/api/public/prompts API Endpoint", () => {
|
||||
expect(fetchedObservations.body.type).toBe("text");
|
||||
expect(fetchedObservations.body.version).toBe(1);
|
||||
expect(fetchedObservations.body.isActive).toBe(true);
|
||||
expect(fetchedObservations.body.labels).toEqual(["production"]);
|
||||
expect(fetchedObservations.body.createdBy).toBe("user-1");
|
||||
expect(fetchedObservations.body.config).toEqual({ temperature: 0.1 });
|
||||
expect(fetchedObservations.body.tags).toEqual([]);
|
||||
@@ -64,7 +65,7 @@ describe("/api/public/prompts API Endpoint", () => {
|
||||
id: promptId,
|
||||
name: "prompt + name",
|
||||
prompt: "prompt",
|
||||
isActive: true,
|
||||
labels: ["production"],
|
||||
version: 1,
|
||||
config: {
|
||||
temperature: 0.1,
|
||||
@@ -94,6 +95,7 @@ describe("/api/public/prompts API Endpoint", () => {
|
||||
expect(fetchedObservations.body.type).toBe("text");
|
||||
expect(fetchedObservations.body.version).toBe(1);
|
||||
expect(fetchedObservations.body.isActive).toBe(true);
|
||||
expect(fetchedObservations.body.labels).toEqual(["production"]);
|
||||
expect(fetchedObservations.body.createdBy).toBe("user-1");
|
||||
expect(fetchedObservations.body.config).toEqual({ temperature: 0.1 });
|
||||
expect(fetchedObservations.body.tags).toEqual([]);
|
||||
@@ -107,7 +109,7 @@ describe("/api/public/prompts API Endpoint", () => {
|
||||
id: promptId,
|
||||
name: "prompt-name",
|
||||
prompt: "prompt",
|
||||
isActive: false,
|
||||
labels: [],
|
||||
version: 1,
|
||||
config: {
|
||||
temperature: 0.1,
|
||||
@@ -137,7 +139,7 @@ describe("/api/public/prompts API Endpoint", () => {
|
||||
id: promptId,
|
||||
name: "prompt-name",
|
||||
prompt: "prompt-one",
|
||||
isActive: false,
|
||||
labels: [],
|
||||
version: 1,
|
||||
config: {
|
||||
temperature: 0.1,
|
||||
@@ -154,7 +156,7 @@ describe("/api/public/prompts API Endpoint", () => {
|
||||
id: promptTwoId,
|
||||
name: "prompt-name",
|
||||
prompt: "prompt",
|
||||
isActive: true,
|
||||
labels: ["production"],
|
||||
version: 2,
|
||||
config: {
|
||||
temperature: 0.2,
|
||||
@@ -183,68 +185,192 @@ describe("/api/public/prompts API Endpoint", () => {
|
||||
expect(fetchedObservations.body.prompt).toBe("prompt-one");
|
||||
expect(fetchedObservations.body.version).toBe(1);
|
||||
expect(fetchedObservations.body.isActive).toBe(false);
|
||||
expect(fetchedObservations.body.labels).toEqual([]);
|
||||
expect(fetchedObservations.body.createdBy).toBe("user-1");
|
||||
expect(fetchedObservations.body.config).toEqual({ temperature: 0.1 });
|
||||
});
|
||||
|
||||
it("should fetch active prompt when multiple exist", async () => {
|
||||
const promptIdOne = uuidv4();
|
||||
const promptIdTwo = uuidv4();
|
||||
|
||||
await prisma.prompt.create({
|
||||
data: {
|
||||
id: promptIdOne,
|
||||
name: "prompt-name",
|
||||
prompt: "prompt",
|
||||
isActive: false,
|
||||
version: 1,
|
||||
config: {
|
||||
temperature: 0.1,
|
||||
},
|
||||
project: {
|
||||
connect: { id: "7a88fb47-b4e2-43b8-a06c-a5ce950dc53a" },
|
||||
},
|
||||
createdBy: "user-1",
|
||||
// First prompt is activated
|
||||
const prompt1 = await makeAPICall("POST", "/api/public/prompts", {
|
||||
name: "prompt-name",
|
||||
projectId: "7a88fb47-b4e2-43b8-a06c-a5ce950dc53a",
|
||||
prompt: "prompt1",
|
||||
isActive: true,
|
||||
version: 1,
|
||||
config: {
|
||||
temperature: 0.1,
|
||||
},
|
||||
createdBy: "user-1",
|
||||
});
|
||||
|
||||
await prisma.prompt.create({
|
||||
data: {
|
||||
id: promptIdTwo,
|
||||
name: "prompt-name",
|
||||
prompt: "prompt",
|
||||
isActive: true,
|
||||
version: 2,
|
||||
config: {
|
||||
temperature: 0.2,
|
||||
},
|
||||
project: {
|
||||
connect: { id: "7a88fb47-b4e2-43b8-a06c-a5ce950dc53a" },
|
||||
},
|
||||
createdBy: "user-1",
|
||||
// Second prompt also activated
|
||||
const prompt2 = await makeAPICall("POST", "/api/public/prompts", {
|
||||
name: "prompt-name",
|
||||
projectId: "7a88fb47-b4e2-43b8-a06c-a5ce950dc53a",
|
||||
prompt: "prompt2",
|
||||
labels: ["production"],
|
||||
isActive: true,
|
||||
version: 2,
|
||||
config: {
|
||||
temperature: 0.2,
|
||||
},
|
||||
createdBy: "user-1",
|
||||
});
|
||||
|
||||
const fetchedObservations = await makeAPICall(
|
||||
// Third prompt is deactivated
|
||||
const prompt3 = await makeAPICall("POST", "/api/public/prompts", {
|
||||
name: "prompt-name",
|
||||
projectId: "7a88fb47-b4e2-43b8-a06c-a5ce950dc53a",
|
||||
prompt: "prompt3",
|
||||
labels: [], // This should be ignored
|
||||
isActive: false,
|
||||
version: 3,
|
||||
config: {
|
||||
temperature: 0.3,
|
||||
},
|
||||
createdBy: "user-1",
|
||||
});
|
||||
|
||||
// Expect the second prompt to be fetched
|
||||
const fetchedProductionPrompt = await makeAPICall(
|
||||
"GET",
|
||||
"/api/public/prompts?name=prompt-name",
|
||||
undefined,
|
||||
);
|
||||
|
||||
expect(fetchedObservations.status).toBe(200);
|
||||
expect(fetchedProductionPrompt.status).toBe(200);
|
||||
|
||||
if (!isPrompt(fetchedObservations.body)) {
|
||||
if (!isPrompt(fetchedProductionPrompt.body)) {
|
||||
throw new Error("Expected body to be a prompt");
|
||||
}
|
||||
|
||||
expect(fetchedObservations.body.id).toBe(promptIdTwo);
|
||||
expect(fetchedObservations.body.name).toBe("prompt-name");
|
||||
expect(fetchedObservations.body.prompt).toBe("prompt");
|
||||
expect(fetchedObservations.body.type).toBe("text");
|
||||
expect(fetchedObservations.body.version).toBe(2);
|
||||
expect(fetchedObservations.body.isActive).toBe(true);
|
||||
expect(fetchedObservations.body.createdBy).toBe("user-1");
|
||||
expect(fetchedObservations.body.config).toEqual({ temperature: 0.2 });
|
||||
// @ts-expect-error
|
||||
expect(fetchedProductionPrompt.body.id).toBe(prompt2.body.id);
|
||||
expect(fetchedProductionPrompt.body.name).toBe("prompt-name");
|
||||
expect(fetchedProductionPrompt.body.prompt).toBe("prompt2");
|
||||
expect(fetchedProductionPrompt.body.type).toBe("text");
|
||||
expect(fetchedProductionPrompt.body.version).toBe(2);
|
||||
expect(fetchedProductionPrompt.body.isActive).toBe(true);
|
||||
expect(fetchedProductionPrompt.body.labels).toEqual(["production"]);
|
||||
expect(fetchedProductionPrompt.body.createdBy).toBe("API");
|
||||
expect(fetchedProductionPrompt.body.config).toEqual({ temperature: 0.2 });
|
||||
|
||||
// Expect the first prompt to be deactivated
|
||||
const fetchedOldProductionPrompt = await makeAPICall(
|
||||
"GET",
|
||||
"/api/public/prompts?name=prompt-name&version=1",
|
||||
undefined,
|
||||
);
|
||||
|
||||
expect(fetchedOldProductionPrompt.status).toBe(200);
|
||||
|
||||
if (!isPrompt(fetchedOldProductionPrompt.body)) {
|
||||
throw new Error("Expected body to be a prompt");
|
||||
}
|
||||
|
||||
// @ts-expect-error
|
||||
expect(fetchedOldProductionPrompt.body.id).toBe(prompt1.body.id);
|
||||
expect(fetchedOldProductionPrompt.body.name).toBe("prompt-name");
|
||||
expect(fetchedOldProductionPrompt.body.prompt).toBe("prompt1");
|
||||
expect(fetchedOldProductionPrompt.body.type).toBe("text");
|
||||
expect(fetchedOldProductionPrompt.body.version).toBe(1);
|
||||
expect(fetchedOldProductionPrompt.body.isActive).toBe(false);
|
||||
expect(fetchedOldProductionPrompt.body.labels).toEqual([]);
|
||||
expect(fetchedOldProductionPrompt.body.createdBy).toBe("API");
|
||||
expect(fetchedOldProductionPrompt.body.config).toEqual({
|
||||
temperature: 0.1,
|
||||
});
|
||||
});
|
||||
|
||||
it("should correctly handle overwriting labels", async () => {
|
||||
// First prompt has multiple labels
|
||||
const prompt1 = await makeAPICall("POST", "/api/public/prompts", {
|
||||
name: "prompt-name",
|
||||
projectId: "7a88fb47-b4e2-43b8-a06c-a5ce950dc53a",
|
||||
prompt: "prompt1",
|
||||
labels: ["production", "staging", "development"],
|
||||
isActive: true,
|
||||
version: 1,
|
||||
config: {
|
||||
temperature: 0.1,
|
||||
},
|
||||
createdBy: "user-1",
|
||||
});
|
||||
|
||||
// Second prompt overwrites production and staging label
|
||||
const prompt2 = await makeAPICall("POST", "/api/public/prompts", {
|
||||
name: "prompt-name",
|
||||
projectId: "7a88fb47-b4e2-43b8-a06c-a5ce950dc53a",
|
||||
prompt: "prompt2",
|
||||
labels: ["production", "production", "staging"], // Should be deduped
|
||||
isActive: true,
|
||||
version: 2,
|
||||
config: {
|
||||
temperature: 0.2,
|
||||
},
|
||||
createdBy: "user-1",
|
||||
});
|
||||
|
||||
// Third prompt overwrites staging label
|
||||
const prompt3 = await makeAPICall("POST", "/api/public/prompts", {
|
||||
name: "prompt-name",
|
||||
projectId: "7a88fb47-b4e2-43b8-a06c-a5ce950dc53a",
|
||||
prompt: "prompt3",
|
||||
labels: ["staging"],
|
||||
isActive: false,
|
||||
version: 3,
|
||||
config: {
|
||||
temperature: 0.3,
|
||||
},
|
||||
createdBy: "user-1",
|
||||
});
|
||||
|
||||
// Expect the second prompt to be fetched as default production prompt
|
||||
const fetchedProductionPrompt = await makeAPICall(
|
||||
"GET",
|
||||
"/api/public/prompts?name=prompt-name",
|
||||
undefined,
|
||||
);
|
||||
expect(fetchedProductionPrompt.status).toBe(200);
|
||||
if (!isPrompt(fetchedProductionPrompt.body)) {
|
||||
throw new Error("Expected body to be a prompt");
|
||||
}
|
||||
// @ts-expect-error
|
||||
expect(fetchedProductionPrompt.body.id).toBe(prompt2.body.id);
|
||||
expect(fetchedProductionPrompt.body.labels).toEqual(["production"]); // Only production label should be present
|
||||
|
||||
// Expect the first prompt to have only development label
|
||||
const fetchedFirstPrompt = await makeAPICall(
|
||||
"GET",
|
||||
"/api/public/prompts?name=prompt-name&version=1",
|
||||
undefined,
|
||||
);
|
||||
|
||||
expect(fetchedFirstPrompt.status).toBe(200);
|
||||
if (!isPrompt(fetchedFirstPrompt.body)) {
|
||||
throw new Error("Expected body to be a prompt");
|
||||
}
|
||||
|
||||
// @ts-expect-error
|
||||
expect(fetchedFirstPrompt.body.id).toBe(prompt1.body.id);
|
||||
expect(fetchedFirstPrompt.body.labels).toEqual(["development"]);
|
||||
|
||||
// Expect the third prompt to have only staging label
|
||||
const fetchedThirdPrompt = await makeAPICall(
|
||||
"GET",
|
||||
"/api/public/prompts?name=prompt-name&version=3",
|
||||
undefined,
|
||||
);
|
||||
|
||||
expect(fetchedThirdPrompt.status).toBe(200);
|
||||
if (!isPrompt(fetchedThirdPrompt.body)) {
|
||||
throw new Error("Expected body to be a prompt");
|
||||
}
|
||||
|
||||
// @ts-expect-error
|
||||
expect(fetchedThirdPrompt.body.id).toBe(prompt3.body.id);
|
||||
expect(fetchedThirdPrompt.body.labels).toEqual(["staging", "latest"]);
|
||||
});
|
||||
|
||||
it("should create and fetch a prompt", async () => {
|
||||
@@ -275,6 +401,7 @@ describe("/api/public/prompts API Endpoint", () => {
|
||||
expect(fetchedObservations.body.type).toBe("text");
|
||||
expect(fetchedObservations.body.version).toBe(1);
|
||||
expect(fetchedObservations.body.isActive).toBe(true);
|
||||
expect(fetchedObservations.body.labels).toEqual(["production", "latest"]);
|
||||
expect(fetchedObservations.body.createdBy).toBe("API");
|
||||
expect(fetchedObservations.body.config).toEqual({ temperature: 0.1 });
|
||||
});
|
||||
@@ -290,7 +417,7 @@ describe("/api/public/prompts API Endpoint", () => {
|
||||
id: promptId,
|
||||
name: "prompt-name",
|
||||
prompt: "prompt",
|
||||
isActive: true,
|
||||
labels: ["production"],
|
||||
version: 1,
|
||||
project: {
|
||||
connect: { id: "7a88fb47-b4e2-43b8-a06c-a5ce950dc53a" },
|
||||
@@ -353,7 +480,7 @@ describe("/api/public/prompts API Endpoint", () => {
|
||||
id: promptId,
|
||||
name: "prompt-name",
|
||||
prompt: "prompt",
|
||||
isActive: true,
|
||||
labels: ["production"],
|
||||
version: 1,
|
||||
project: {
|
||||
connect: { id: "7a88fb47-b4e2-43b8-a06c-a5ce950dc53a" },
|
||||
@@ -428,6 +555,7 @@ describe("/api/public/prompts API Endpoint", () => {
|
||||
expect(fetchedObservations.body.type).toBe("text");
|
||||
expect(fetchedObservations.body.version).toBe(1);
|
||||
expect(fetchedObservations.body.isActive).toBe(true);
|
||||
expect(fetchedObservations.body.labels).toEqual(["production", "latest"]);
|
||||
expect(fetchedObservations.body.createdBy).toBe("API");
|
||||
expect(fetchedObservations.body.config).toEqual({});
|
||||
});
|
||||
@@ -461,6 +589,7 @@ describe("/api/public/prompts API Endpoint", () => {
|
||||
expect(validatedPrompt.type).toBe("chat");
|
||||
expect(validatedPrompt.version).toBe(1);
|
||||
expect(validatedPrompt.isActive).toBe(true);
|
||||
expect(validatedPrompt.labels).toEqual(["production", "latest"]);
|
||||
expect(validatedPrompt.createdBy).toBe("API");
|
||||
expect(validatedPrompt.config).toEqual({});
|
||||
});
|
||||
@@ -484,7 +613,7 @@ describe("/api/public/prompts API Endpoint", () => {
|
||||
);
|
||||
expect(status).toBe(404);
|
||||
expect(body).toEqual({
|
||||
error: "NotFoundError",
|
||||
error: "LangfuseNotFoundError",
|
||||
message: "Prompt not found",
|
||||
});
|
||||
});
|
||||
@@ -512,7 +641,7 @@ describe("/api/public/prompts API Endpoint", () => {
|
||||
);
|
||||
expect(status).toBe(404);
|
||||
expect(body).toEqual({
|
||||
error: "NotFoundError",
|
||||
error: "LangfuseNotFoundError",
|
||||
message: "Prompt not found",
|
||||
});
|
||||
});
|
||||
@@ -535,7 +664,7 @@ describe("/api/public/prompts API Endpoint", () => {
|
||||
);
|
||||
expect(status).toBe(404);
|
||||
expect(body).toEqual({
|
||||
error: "NotFoundError",
|
||||
error: "LangfuseNotFoundError",
|
||||
message: "Prompt not found",
|
||||
});
|
||||
});
|
||||
@@ -589,6 +718,7 @@ describe("/api/public/prompts API Endpoint", () => {
|
||||
expect(validatedPrompt.type).toBe("chat");
|
||||
expect(validatedPrompt.version).toBe(1);
|
||||
expect(validatedPrompt.isActive).toBe(true);
|
||||
expect(validatedPrompt.labels).toEqual(["production", "latest"]);
|
||||
expect(validatedPrompt.createdBy).toBe("API");
|
||||
expect(validatedPrompt.config).toEqual({});
|
||||
|
||||
@@ -600,15 +730,17 @@ describe("/api/public/prompts API Endpoint", () => {
|
||||
);
|
||||
expect(getResponse2.status).toBe(404);
|
||||
expect(getResponse2.body).toEqual({
|
||||
error: "NotFoundError",
|
||||
error: "LangfuseNotFoundError",
|
||||
message: "Prompt not found",
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
const isPrompt = (x: unknown): x is Prompt => {
|
||||
type PromptWithIsActive = Prompt & { isActive: boolean };
|
||||
|
||||
const isPrompt = (x: unknown): x is PromptWithIsActive => {
|
||||
if (typeof x !== "object" || x === null) return false;
|
||||
const prompt = x as Prompt;
|
||||
const prompt = x as PromptWithIsActive;
|
||||
return (
|
||||
typeof prompt.id === "string" &&
|
||||
typeof prompt.name === "string" &&
|
||||
@@ -622,7 +754,9 @@ const isPrompt = (x: unknown): x is Prompt => {
|
||||
);
|
||||
};
|
||||
|
||||
const validatePrompt = (obj: Record<string, unknown>): ValidatedPrompt => {
|
||||
const validatePrompt = (
|
||||
obj: Record<string, unknown>,
|
||||
): LegacyValidatedPrompt => {
|
||||
Object.keys(obj).forEach((key) => {
|
||||
obj[key] =
|
||||
key === "createdAt" || key === "updatedAt"
|
||||
@@ -630,5 +764,5 @@ const validatePrompt = (obj: Record<string, unknown>): ValidatedPrompt => {
|
||||
: obj[key];
|
||||
});
|
||||
|
||||
return PromptSchema.parse(obj);
|
||||
return LegacyPromptSchema.parse(obj);
|
||||
};
|
||||
File diff suppressed because it is too large
Load Diff
@@ -50,7 +50,7 @@ describe("Build valid SQL queries", () => {
|
||||
table: "traces_scores",
|
||||
values: ["project-id"],
|
||||
strings: [
|
||||
' FROM traces t JOIN scores s ON t.id = s.trace_id WHERE t."project_id" = ',
|
||||
' FROM traces t JOIN scores s ON t.id = s.trace_id AND t.project_id = s.project_id WHERE t."project_id" = ',
|
||||
";",
|
||||
],
|
||||
} as const,
|
||||
|
||||
@@ -60,6 +60,7 @@ describe("/api/public/scores API Endpoint", () => {
|
||||
expect(dbScore?.observationId).toBeNull();
|
||||
expect(dbScore?.comment).toBe("comment");
|
||||
expect(dbScore?.source).toBe("API");
|
||||
expect(dbScore?.projectId).toBe("7a88fb47-b4e2-43b8-a06c-a5ce950dc53a");
|
||||
});
|
||||
|
||||
it("should create score for a trace with int", async () => {
|
||||
|
||||
@@ -23,6 +23,7 @@ describe("Traces TRPC Router", () => {
|
||||
],
|
||||
featureFlags: {
|
||||
templateFlag: true,
|
||||
evals: true,
|
||||
},
|
||||
admin: true,
|
||||
},
|
||||
|
||||
@@ -12,6 +12,7 @@ export const pruneDatabase = async () => {
|
||||
await prisma.prompt.deleteMany();
|
||||
await prisma.events.deleteMany();
|
||||
await prisma.model.deleteMany();
|
||||
await prisma.llmApiKeys.deleteMany();
|
||||
};
|
||||
|
||||
export function createBasicAuthHeader(
|
||||
@@ -89,7 +90,7 @@ export const setupUserAndProject = async () => {
|
||||
},
|
||||
],
|
||||
},
|
||||
members: {
|
||||
projectMembers: {
|
||||
create: {
|
||||
role: "OWNER",
|
||||
userId: user.id,
|
||||
|
||||
@@ -182,6 +182,32 @@ describe("/api/public/traces API Endpoint", () => {
|
||||
endTime: "2021-01-01T00:20:00.000Z",
|
||||
});
|
||||
|
||||
// Simulate scores on the trace
|
||||
const scoreId1 = uuidv4();
|
||||
await makeAPICall("POST", "/api/public/scores", {
|
||||
id: scoreId1,
|
||||
name: "score-1",
|
||||
value: 75.0,
|
||||
traceId: traceId,
|
||||
comment: "First score",
|
||||
});
|
||||
const scoreId2 = uuidv4();
|
||||
await makeAPICall("POST", "/api/public/scores", {
|
||||
id: scoreId2,
|
||||
name: "score-2",
|
||||
value: 85.5,
|
||||
traceId: traceId,
|
||||
comment: "Second score",
|
||||
});
|
||||
const scoreId3 = uuidv4();
|
||||
await makeAPICall("POST", "/api/public/scores", {
|
||||
id: scoreId3,
|
||||
name: "score-3",
|
||||
value: 95.0,
|
||||
traceId: traceId,
|
||||
comment: "Third score",
|
||||
});
|
||||
|
||||
// GET traces
|
||||
// Retrieve the trace with totalCost and latency
|
||||
const traces = await makeAPICall<GetTracesAPIResponse>(
|
||||
|
||||
@@ -33,6 +33,7 @@ describe("Token Count Functions", () => {
|
||||
{ model: "claude", tokenizer: "claude", tokens: 118 },
|
||||
{ model: "claude-instant-1.2", tokenizer: "claude", tokens: 118 },
|
||||
{ model: "gpt-3.5-turbo-1106", tokenizer: "openai", tokens: 114 },
|
||||
{ model: "gpt-4o-2024-05-13", tokenizer: "openai", tokens: 112 },
|
||||
].forEach(({ model, tokens, tokenizer }) => {
|
||||
it(`should return token count ${tokens} for ${model}`, () => {
|
||||
const result = tokenCount({
|
||||
|
||||
+2
-1
@@ -5,7 +5,7 @@ import { ChatMessageRole, type ChatMessageWithId } from "@langfuse/shared";
|
||||
import { Button } from "@/src/components/ui/button";
|
||||
import { Card, CardContent } from "@/src/components/ui/card";
|
||||
import { Textarea } from "@/src/components/ui/textarea";
|
||||
import type { MessagesContext } from "@/src/features/playground/client/components/Messages";
|
||||
import type { MessagesContext } from "./types";
|
||||
|
||||
type ChatMessageProps = Pick<
|
||||
MessagesContext,
|
||||
@@ -63,6 +63,7 @@ export const ChatMessageComponent: React.FC<ChatMessageProps> = ({
|
||||
|
||||
<Textarea
|
||||
ref={textAreaRef}
|
||||
id={message.id}
|
||||
className="height-[auto] min-h-8 w-full pt-3 font-mono text-xs focus:outline-none"
|
||||
placeholder={placeholder}
|
||||
value={message.content}
|
||||
+5
-3
@@ -1,10 +1,12 @@
|
||||
import { PlusCircleIcon } from "lucide-react";
|
||||
import { useEffect, useRef } from "react";
|
||||
|
||||
import { Button } from "@/src/components/ui/button";
|
||||
import { ChatMessageComponent } from "@/src/features/playground/client/components/ChatMessageComponent";
|
||||
import type { MessagesContext } from "@/src/features/playground/client/components/Messages";
|
||||
import { ChatMessageRole } from "@langfuse/shared";
|
||||
import { useRef, useEffect } from "react";
|
||||
|
||||
import { ChatMessageComponent } from "./ChatMessageComponent";
|
||||
|
||||
import type { MessagesContext } from "./types";
|
||||
|
||||
type ChatMessagesProps = MessagesContext;
|
||||
export const ChatMessages: React.FC<ChatMessagesProps> = (props) => {
|
||||
@@ -0,0 +1,13 @@
|
||||
import type { ChatMessageRole, ChatMessageWithId } from "@langfuse/shared";
|
||||
|
||||
|
||||
export type MessagesContext = {
|
||||
messages: ChatMessageWithId[];
|
||||
addMessage: (role: ChatMessageRole, content?: string) => ChatMessageWithId;
|
||||
deleteMessage: (id: string) => void;
|
||||
updateMessage: <Key extends keyof ChatMessageWithId>(
|
||||
id: string,
|
||||
key: Key,
|
||||
value: ChatMessageWithId[Key]
|
||||
) => void;
|
||||
};
|
||||
@@ -39,18 +39,18 @@ export const LangfuseLogo = ({
|
||||
{showEnvLabel && env.NEXT_PUBLIC_LANGFUSE_CLOUD_REGION && (
|
||||
<div
|
||||
className={cn(
|
||||
"flex items-center gap-2 self-stretch rounded-md px-3 py-2 text-xs ring-1 lg:-mx-2",
|
||||
"flex items-center gap-2 self-stretch rounded-md px-1 py-1 text-xs ring-1 sm:px-3 sm:py-2 lg:-mx-2",
|
||||
env.NEXT_PUBLIC_LANGFUSE_CLOUD_REGION === "STAGING"
|
||||
? "bg-blue-100 text-blue-500 ring-blue-500"
|
||||
? "bg-light-blue text-dark-blue ring-dark-blue"
|
||||
: env.NEXT_PUBLIC_LANGFUSE_CLOUD_REGION === "DEV"
|
||||
? "bg-green-100 text-green-500 ring-green-500"
|
||||
: "bg-red-100 text-red-500 ring-red-500",
|
||||
? "bg-light-green text-dark-green ring-dark-green"
|
||||
: "bg-light-red text-dark-red ring-dark-red",
|
||||
)}
|
||||
>
|
||||
{env.NEXT_PUBLIC_LANGFUSE_CLOUD_REGION === "DEV" ? (
|
||||
<Check size={16} />
|
||||
<Check size={16} className="hidden sm:block" />
|
||||
) : (
|
||||
<AlertTriangle size={16} />
|
||||
<AlertTriangle size={16} className="hidden sm:block" />
|
||||
)}
|
||||
<span className="whitespace-nowrap">
|
||||
{["EU", "US"].includes(env.NEXT_PUBLIC_LANGFUSE_CLOUD_REGION)
|
||||
@@ -76,7 +76,7 @@ export const LangfuseLogo = ({
|
||||
target="_blank"
|
||||
rel="noopener"
|
||||
title="View releases on GitHub"
|
||||
className="ml-2 text-xs text-gray-400"
|
||||
className="ml-2 text-xs text-muted-foreground"
|
||||
>
|
||||
{VERSION}
|
||||
</a>
|
||||
|
||||
+95
-54
@@ -1,8 +1,3 @@
|
||||
import {
|
||||
supportedModels,
|
||||
ModelProvider,
|
||||
type UIModelParams,
|
||||
} from "@langfuse/shared";
|
||||
import { Input } from "@/src/components/ui/input";
|
||||
import {
|
||||
Select,
|
||||
@@ -12,23 +7,31 @@ import {
|
||||
SelectValue,
|
||||
} from "@/src/components/ui/select";
|
||||
import { Slider } from "@/src/components/ui/slider";
|
||||
import { Switch } from "@/src/components/ui/switch";
|
||||
import { cn } from "@/src/utils/tailwind";
|
||||
import {
|
||||
ModelProvider,
|
||||
supportedModels,
|
||||
type UIModelParams,
|
||||
} from "@langfuse/shared";
|
||||
|
||||
export type ModelParamsContext = {
|
||||
modelParams: UIModelParams;
|
||||
availableModels?: UIModelParams[];
|
||||
updateModelParam: <Key extends keyof UIModelParams>(
|
||||
updateModelParamValue: <Key extends keyof UIModelParams>(
|
||||
key: Key,
|
||||
value: UIModelParams[Key],
|
||||
value: UIModelParams[Key]["value"],
|
||||
) => void;
|
||||
updateModelParams: <UIModelParams>(params: UIModelParams) => void;
|
||||
disabled?: boolean;
|
||||
setModelParamEnabled?: (key: keyof UIModelParams, enabled: boolean) => void;
|
||||
formDisabled?: boolean;
|
||||
};
|
||||
|
||||
export const ModelParameters: React.FC<ModelParamsContext> = ({
|
||||
modelParams,
|
||||
availableModels,
|
||||
updateModelParam,
|
||||
disabled,
|
||||
updateModelParamValue,
|
||||
setModelParamEnabled,
|
||||
formDisabled = false,
|
||||
}) => {
|
||||
return (
|
||||
<div className="flex flex-col space-y-4">
|
||||
@@ -37,61 +40,69 @@ export const ModelParameters: React.FC<ModelParamsContext> = ({
|
||||
<ModelParamsSelect
|
||||
title="Provider"
|
||||
modelParamsKey="provider"
|
||||
disabled={disabled}
|
||||
value={modelParams.provider}
|
||||
disabled={formDisabled}
|
||||
value={modelParams.provider.value}
|
||||
options={
|
||||
availableModels
|
||||
? [...new Set(availableModels.map((m) => m.provider))]
|
||||
? [...new Set(availableModels.map((m) => m.provider.value))]
|
||||
: Object.values(ModelProvider)
|
||||
}
|
||||
updateModelParam={updateModelParam}
|
||||
updateModelParam={updateModelParamValue}
|
||||
/>
|
||||
<ModelParamsSelect
|
||||
title="Model name"
|
||||
modelParamsKey="model"
|
||||
disabled={disabled}
|
||||
value={modelParams.model}
|
||||
disabled={formDisabled}
|
||||
value={modelParams.model.value}
|
||||
options={Object.values(
|
||||
availableModels
|
||||
? availableModels
|
||||
.filter((m) => m.provider === modelParams.provider)
|
||||
.map((m) => m.model)
|
||||
: supportedModels[modelParams.provider],
|
||||
.filter(
|
||||
(m) => m.provider.value === modelParams.provider.value,
|
||||
)
|
||||
.map((m) => m.model.value)
|
||||
: supportedModels[modelParams.provider.value],
|
||||
)}
|
||||
updateModelParam={updateModelParam}
|
||||
updateModelParam={updateModelParamValue}
|
||||
/>
|
||||
<ModelParamsSlider
|
||||
title="Temperature"
|
||||
modelParamsKey="temperature"
|
||||
disabled={disabled}
|
||||
value={modelParams.temperature}
|
||||
formDisabled={formDisabled}
|
||||
enabled={modelParams.temperature.enabled}
|
||||
setModelParamEnabled={setModelParamEnabled}
|
||||
value={modelParams.temperature.value}
|
||||
min={0}
|
||||
max={modelParams.maxTemperature}
|
||||
max={modelParams.maxTemperature.value}
|
||||
step={0.01}
|
||||
tooltip="The sampling temperature. Higher values will make the output more random, while lower values like will make it more focused and deterministic."
|
||||
updateModelParam={updateModelParam}
|
||||
tooltip="The sampling temperature. Higher values will make the output more random, while lower values will make it more focused and deterministic."
|
||||
updateModelParam={updateModelParamValue}
|
||||
/>
|
||||
<ModelParamsSlider
|
||||
title="Output token limit"
|
||||
modelParamsKey="max_tokens"
|
||||
disabled={disabled}
|
||||
value={modelParams.max_tokens}
|
||||
formDisabled={formDisabled}
|
||||
enabled={modelParams.max_tokens.enabled}
|
||||
setModelParamEnabled={setModelParamEnabled}
|
||||
value={modelParams.max_tokens.value}
|
||||
min={1}
|
||||
max={4096}
|
||||
step={1}
|
||||
tooltip="The maximum number of tokens that can be generated in the chat completion."
|
||||
updateModelParam={updateModelParam}
|
||||
updateModelParam={updateModelParamValue}
|
||||
/>
|
||||
<ModelParamsSlider
|
||||
title="Top P"
|
||||
modelParamsKey="top_p"
|
||||
disabled={disabled}
|
||||
value={modelParams.top_p}
|
||||
formDisabled={formDisabled}
|
||||
enabled={modelParams.top_p.enabled}
|
||||
setModelParamEnabled={setModelParamEnabled}
|
||||
value={modelParams.top_p.value}
|
||||
min={0}
|
||||
max={1}
|
||||
step={0.01}
|
||||
tooltip="An alternative to sampling with temperature, called nucleus sampling, where the model considers the results of the tokens with top_p probability mass. So 0.1 means only the tokens comprising the top 10% probability mass are considered. We generally recommend altering this or temperature but not both."
|
||||
updateModelParam={updateModelParam}
|
||||
updateModelParam={updateModelParamValue}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
@@ -103,7 +114,7 @@ type ModelParamsSelectProps = {
|
||||
modelParamsKey: keyof UIModelParams;
|
||||
value: string;
|
||||
options: string[];
|
||||
updateModelParam: ModelParamsContext["updateModelParam"];
|
||||
updateModelParam: ModelParamsContext["updateModelParamValue"];
|
||||
disabled?: boolean;
|
||||
};
|
||||
const ModelParamsSelect = ({
|
||||
@@ -116,7 +127,14 @@ const ModelParamsSelect = ({
|
||||
}: ModelParamsSelectProps) => {
|
||||
return (
|
||||
<div className="space-y-2">
|
||||
<p className="text-xs font-semibold">{title}</p>
|
||||
<p
|
||||
className={cn(
|
||||
"text-xs font-semibold",
|
||||
disabled && "text-muted-foreground",
|
||||
)}
|
||||
>
|
||||
{title}
|
||||
</p>
|
||||
<Select
|
||||
disabled={disabled}
|
||||
onValueChange={(value) =>
|
||||
@@ -150,8 +168,10 @@ type ModelParamsSliderProps = {
|
||||
min: number;
|
||||
max: number;
|
||||
step: number;
|
||||
updateModelParam: ModelParamsContext["updateModelParam"];
|
||||
disabled?: boolean;
|
||||
updateModelParam: ModelParamsContext["updateModelParamValue"];
|
||||
enabled?: boolean;
|
||||
formDisabled?: boolean;
|
||||
setModelParamEnabled?: ModelParamsContext["setModelParamEnabled"];
|
||||
};
|
||||
const ModelParamsSlider = ({
|
||||
title,
|
||||
@@ -162,30 +182,51 @@ const ModelParamsSlider = ({
|
||||
max,
|
||||
step,
|
||||
updateModelParam,
|
||||
disabled,
|
||||
setModelParamEnabled,
|
||||
enabled,
|
||||
formDisabled,
|
||||
}: ModelParamsSliderProps) => {
|
||||
return (
|
||||
<div className="space-y-3" title={tooltip}>
|
||||
<div className="flex flex-row">
|
||||
<p className="flex-1 text-xs font-semibold">{title}</p>
|
||||
<Input
|
||||
className="h-6 w-14 appearance-none px-2 text-right"
|
||||
type="number"
|
||||
disabled={disabled}
|
||||
min={min}
|
||||
max={max}
|
||||
step={step}
|
||||
value={value}
|
||||
onChange={(event) => {
|
||||
updateModelParam(
|
||||
modelParamsKey,
|
||||
Math.max(Math.min(parseFloat(event.target.value), max), min),
|
||||
);
|
||||
}}
|
||||
/>
|
||||
<p
|
||||
className={cn(
|
||||
"flex-1 text-xs font-semibold",
|
||||
(!enabled || formDisabled) && "text-muted-foreground",
|
||||
)}
|
||||
>
|
||||
{title}
|
||||
</p>
|
||||
<div className="flex flex-row space-x-3">
|
||||
<Input
|
||||
className="h-6 w-14 appearance-none px-2 text-right"
|
||||
type="number"
|
||||
disabled={!enabled || formDisabled}
|
||||
min={min}
|
||||
max={max}
|
||||
step={step}
|
||||
value={value}
|
||||
onChange={(event) => {
|
||||
updateModelParam(
|
||||
modelParamsKey,
|
||||
Math.max(Math.min(parseFloat(event.target.value), max), min),
|
||||
);
|
||||
}}
|
||||
/>
|
||||
{setModelParamEnabled ? (
|
||||
<Switch
|
||||
title={`Control sending the ${title} parameter`}
|
||||
disabled={formDisabled}
|
||||
checked={enabled}
|
||||
onCheckedChange={(checked) => {
|
||||
setModelParamEnabled(modelParamsKey, checked);
|
||||
}}
|
||||
/>
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
<Slider
|
||||
disabled={disabled}
|
||||
disabled={!enabled || formDisabled}
|
||||
min={min}
|
||||
max={max}
|
||||
step={step}
|
||||
@@ -7,27 +7,23 @@ export const Slider = (props: {
|
||||
loading?: boolean;
|
||||
onChecked?: (checked: boolean) => void;
|
||||
isChecked?: boolean; // whether the slider is active
|
||||
}) => {
|
||||
console.log("props", props);
|
||||
|
||||
return (
|
||||
<Switch
|
||||
checked={props.isChecked}
|
||||
disabled={props.loading || props.disabled}
|
||||
onChange={props.onChecked}
|
||||
}) => (
|
||||
<Switch
|
||||
checked={props.isChecked}
|
||||
disabled={props.loading || props.disabled}
|
||||
onChange={props.onChecked}
|
||||
className={cn(
|
||||
props.isChecked ? "bg-background" : "bg-input",
|
||||
"relative inline-flex h-6 w-11 flex-shrink-0 cursor-pointer rounded-full border-2 border-transparent transition-colors duration-200 ease-in-out focus:outline-none focus:ring-2 focus:ring-foreground focus:ring-offset-2",
|
||||
)}
|
||||
>
|
||||
<span className="sr-only">Use setting</span>
|
||||
<span
|
||||
aria-hidden="true"
|
||||
className={cn(
|
||||
props.isChecked ? "bg-black" : "bg-gray-200",
|
||||
"relative inline-flex h-6 w-11 flex-shrink-0 cursor-pointer rounded-full border-2 border-transparent transition-colors duration-200 ease-in-out focus:outline-none focus:ring-2 focus:ring-black focus:ring-offset-2",
|
||||
props.isChecked ? "translate-x-5" : "translate-x-0",
|
||||
"pointer-events-none inline-block h-5 w-5 transform rounded-full bg-background shadow ring-0 transition duration-200 ease-in-out",
|
||||
)}
|
||||
>
|
||||
<span className="sr-only">Use setting</span>
|
||||
<span
|
||||
aria-hidden="true"
|
||||
className={cn(
|
||||
props.isChecked ? "translate-x-5" : "translate-x-0",
|
||||
"pointer-events-none inline-block h-5 w-5 transform rounded-full bg-white shadow ring-0 transition duration-200 ease-in-out",
|
||||
)}
|
||||
/>
|
||||
</Switch>
|
||||
);
|
||||
};
|
||||
/>
|
||||
</Switch>
|
||||
);
|
||||
|
||||
@@ -7,10 +7,10 @@ import {
|
||||
} from "@/src/components/ui/popover";
|
||||
import { Button } from "@/src/components/ui/button";
|
||||
import { TrashIcon } from "lucide-react";
|
||||
import { usePostHog } from "posthog-js/react";
|
||||
import { useHasAccess } from "@/src/features/rbac/utils/checkAccess";
|
||||
import { type Scope } from "@/src/features/rbac/constants/roleAccessRights";
|
||||
import { api } from "@/src/utils/api";
|
||||
import { usePostHogClientCapture } from "@/src/features/posthog-analytics/usePostHogClientCapture";
|
||||
|
||||
interface DeleteButtonProps {
|
||||
itemId: string;
|
||||
@@ -33,7 +33,7 @@ export function DeleteButton({
|
||||
}: DeleteButtonProps) {
|
||||
const [isDeleted, setIsDeleted] = useState(false);
|
||||
const router = useRouter();
|
||||
const posthog = usePostHog();
|
||||
const capture = usePostHogClientCapture();
|
||||
|
||||
const hasAccess = useHasAccess({ projectId, scope: scope });
|
||||
const traceMutation = api.traces.deleteMany.useMutation({
|
||||
@@ -63,6 +63,15 @@ export function DeleteButton({
|
||||
<Button
|
||||
variant={isTableAction ? "ghost" : "outline"}
|
||||
size={isTableAction ? "xs" : "icon"}
|
||||
onClick={() =>
|
||||
type === "trace"
|
||||
? capture("trace:delete_form_open", {
|
||||
source: isTableAction ? "table-single-row" : "trace detail",
|
||||
})
|
||||
: capture("datasets:delete_form_open", {
|
||||
source: "dataset",
|
||||
})
|
||||
}
|
||||
>
|
||||
<TrashIcon className="h-4 w-4" />
|
||||
</Button>
|
||||
@@ -84,7 +93,7 @@ export function DeleteButton({
|
||||
traceIds: [itemId],
|
||||
projectId,
|
||||
});
|
||||
posthog.capture("trace:delete", {
|
||||
capture("trace:delete", {
|
||||
source: isTableAction ? "table-single-row" : "trace",
|
||||
});
|
||||
}}
|
||||
@@ -101,7 +110,7 @@ export function DeleteButton({
|
||||
projectId,
|
||||
datasetId: itemId,
|
||||
});
|
||||
posthog.capture("dataset:delete", {
|
||||
capture("datasets:delete_dataset_button_click", {
|
||||
source: isTableAction ? "table-single-row" : "dataset",
|
||||
});
|
||||
}}
|
||||
|
||||
@@ -14,14 +14,23 @@ export const ErrorPage = ({
|
||||
}) => {
|
||||
const session = useSession();
|
||||
const router = useRouter();
|
||||
const newTargetPath = router.asPath;
|
||||
|
||||
return (
|
||||
<div className="flex h-screen flex-col items-center justify-center">
|
||||
<AlertCircle className="mb-4 h-12 w-12 text-red-500" />
|
||||
<AlertCircle className="text-dark-red mb-4 h-12 w-12" />
|
||||
<h1 className="mb-4 text-xl font-bold">{title}</h1>
|
||||
<p className="mb-8 text-center">{message}</p>
|
||||
{session.status === "unauthenticated" ? (
|
||||
<Button onClick={() => void router.push("/")}>Sign In</Button>
|
||||
<Button
|
||||
onClick={() =>
|
||||
void router.push(
|
||||
`/auth/sign-in?targetPath=${encodeURIComponent(newTargetPath)}`,
|
||||
)
|
||||
}
|
||||
>
|
||||
Sign In
|
||||
</Button>
|
||||
) : null}
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -58,7 +58,7 @@ export const GroupedScoreBadges = ({
|
||||
.sort(([a], [b]) => (a < b ? -1 : 1))
|
||||
.map(([name, scores]) => (
|
||||
<div key={name}>
|
||||
<div className="text-xs text-gray-500">{name}</div>
|
||||
<div className="text-xs text-muted-foreground">{name}</div>
|
||||
<ScoresOfGroup scores={scores} />
|
||||
</div>
|
||||
))}
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
import CodeMirror, { EditorView } from "@uiw/react-codemirror";
|
||||
import { githubLight } from "@uiw/codemirror-theme-github";
|
||||
import { tokyoNight } from "@uiw/codemirror-theme-tokyo-night";
|
||||
import { json } from "@codemirror/lang-json";
|
||||
import { useTheme } from "next-themes";
|
||||
import { cn } from "@/src/utils/tailwind";
|
||||
|
||||
// todo: add json linting
|
||||
|
||||
@@ -9,16 +12,20 @@ export function JsonEditor({
|
||||
onChange,
|
||||
editable = true,
|
||||
lineWrapping = true,
|
||||
className,
|
||||
}: {
|
||||
defaultValue: string;
|
||||
onChange?: (value: string) => void;
|
||||
editable?: boolean;
|
||||
lineWrapping?: boolean;
|
||||
className?: string;
|
||||
}) {
|
||||
const { theme } = useTheme();
|
||||
const codeMirrorTheme = theme === "dark" ? tokyoNight : githubLight;
|
||||
return (
|
||||
<CodeMirror
|
||||
value={defaultValue}
|
||||
theme={githubLight}
|
||||
theme={codeMirrorTheme}
|
||||
basicSetup={{
|
||||
foldGutter: true,
|
||||
}}
|
||||
@@ -26,7 +33,7 @@ export function JsonEditor({
|
||||
extensions={[json(), ...(lineWrapping ? [EditorView.lineWrapping] : [])]}
|
||||
defaultValue={defaultValue}
|
||||
onChange={onChange}
|
||||
className="overflow-hidden rounded-md border"
|
||||
className={cn("overflow-hidden rounded-md border", className)}
|
||||
editable={editable}
|
||||
/>
|
||||
);
|
||||
|
||||
@@ -3,9 +3,9 @@ import {
|
||||
HoverCardContent,
|
||||
HoverCardTrigger,
|
||||
} from "@/src/components/ui/hover-card";
|
||||
import { usePostHogClientCapture } from "@/src/features/posthog-analytics/usePostHogClientCapture";
|
||||
import { HelpCircle, Info } from "lucide-react";
|
||||
import Link from "next/link";
|
||||
import { usePostHog } from "posthog-js/react";
|
||||
|
||||
export type DocPopupProps = {
|
||||
description: React.ReactNode;
|
||||
@@ -26,14 +26,17 @@ export default function DocPopup({
|
||||
md: "w-6 h-6",
|
||||
lg: "w-8 h-8",
|
||||
};
|
||||
const posthog = usePostHog();
|
||||
const capture = usePostHogClientCapture();
|
||||
|
||||
return (
|
||||
<HoverCard
|
||||
openDelay={200}
|
||||
onOpenChange={(open) => {
|
||||
if (open) {
|
||||
posthog.capture("help-popup:opened", { href, description });
|
||||
capture("help_popup:opened", {
|
||||
hfref: href,
|
||||
description: description,
|
||||
});
|
||||
}
|
||||
}}
|
||||
>
|
||||
@@ -43,7 +46,13 @@ export default function DocPopup({
|
||||
href={href}
|
||||
rel="noopener"
|
||||
target="_blank"
|
||||
className="inline-block whitespace-nowrap text-gray-500 sm:pl-0"
|
||||
className="inline-block whitespace-nowrap text-muted-foreground sm:pl-0"
|
||||
onClick={() => {
|
||||
capture("help_popup:href_clicked", {
|
||||
href: href,
|
||||
description: description,
|
||||
});
|
||||
}}
|
||||
>
|
||||
{
|
||||
{
|
||||
@@ -53,7 +62,7 @@ export default function DocPopup({
|
||||
}
|
||||
</Link>
|
||||
) : (
|
||||
<div className="inline-block whitespace-nowrap text-gray-500 sm:pl-0">
|
||||
<div className="inline-block whitespace-nowrap text-muted-foreground sm:pl-0">
|
||||
{
|
||||
{
|
||||
question: <HelpCircle className={sizes[size]} />,
|
||||
@@ -65,7 +74,7 @@ export default function DocPopup({
|
||||
</HoverCardTrigger>
|
||||
<HoverCardContent>
|
||||
{typeof description === "string" ? (
|
||||
<div className="whitespace-break-spaces text-xs font-normal text-gray-800 sm:pl-0">
|
||||
<div className="whitespace-break-spaces text-xs font-normal text-primary sm:pl-0">
|
||||
{description}
|
||||
</div>
|
||||
) : (
|
||||
@@ -89,7 +98,7 @@ export function Popup({ triggerContent, description }: PopupProps) {
|
||||
</HoverCardTrigger>
|
||||
<HoverCardContent>
|
||||
{typeof description === "string" ? (
|
||||
<div className="whitespace-break-spaces text-xs font-normal text-gray-800 sm:pl-0">
|
||||
<div className="whitespace-break-spaces text-xs font-normal text-primary sm:pl-0">
|
||||
{description}
|
||||
</div>
|
||||
) : (
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
export const FullScreenPage: React.FC<React.PropsWithChildren<{}>> = ({
|
||||
children,
|
||||
}) => {
|
||||
return (
|
||||
<div className="flex h-[calc(100dvh-6rem)] flex-col overflow-hidden lg:h-[calc(100dvh-2rem)]">
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -49,10 +49,10 @@ export default function Header({
|
||||
<nav className="sm:hidden" aria-label="Back">
|
||||
<Link
|
||||
href={backHref}
|
||||
className="flex items-center text-sm font-medium text-gray-500 hover:text-gray-700"
|
||||
className="flex items-center text-sm font-medium text-muted-foreground hover:text-primary"
|
||||
>
|
||||
<ChevronLeftIcon
|
||||
className="-ml-1 mr-1 h-5 w-5 flex-shrink-0 text-gray-400"
|
||||
className="-ml-1 mr-1 h-5 w-5 flex-shrink-0 text-muted-foreground"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
Back
|
||||
@@ -67,19 +67,19 @@ export default function Header({
|
||||
<div className="flex items-center">
|
||||
{index !== 0 && (
|
||||
<ChevronRightIcon
|
||||
className="mr-4 h-5 w-5 flex-shrink-0 text-gray-400"
|
||||
className="mr-4 h-5 w-5 flex-shrink-0 text-muted-foreground"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
)}
|
||||
{href ? (
|
||||
<Link
|
||||
href={href}
|
||||
className="text-sm font-medium text-gray-500 hover:text-gray-700"
|
||||
className="text-sm font-medium text-muted-foreground hover:text-primary"
|
||||
>
|
||||
{name}
|
||||
</Link>
|
||||
) : (
|
||||
<div className="text-sm font-medium text-gray-500">
|
||||
<div className="text-sm font-medium text-muted-foreground">
|
||||
{name}
|
||||
</div>
|
||||
)}
|
||||
@@ -94,11 +94,11 @@ export default function Header({
|
||||
<div className="flex items-center gap-3 md:gap-5">
|
||||
<div className="flex min-w-0 flex-row justify-center align-middle">
|
||||
{level === "h2" ? (
|
||||
<h2 className="text-2xl font-bold leading-7 text-gray-900 sm:truncate sm:text-3xl sm:tracking-tight">
|
||||
<h2 className="text-2xl font-bold leading-7 sm:truncate sm:text-3xl sm:tracking-tight">
|
||||
{props.title}
|
||||
</h2>
|
||||
) : (
|
||||
<h3 className="text-lg font-bold leading-7 text-gray-900 sm:truncate sm:text-xl sm:tracking-tight">
|
||||
<h3 className="text-lg font-bold leading-7 sm:truncate sm:text-xl sm:tracking-tight">
|
||||
{props.title}
|
||||
</h3>
|
||||
)}
|
||||
|
||||
@@ -14,7 +14,6 @@ import {
|
||||
AvatarFallback,
|
||||
AvatarImage,
|
||||
} from "@/src/components/ui/avatar";
|
||||
import { api } from "@/src/utils/api";
|
||||
import { NewProjectButton } from "@/src/features/projects/components/NewProjectButton";
|
||||
import { FeedbackButtonWrapper } from "@/src/features/feedback/component/FeedbackButton";
|
||||
import { Button } from "@/src/components/ui/button";
|
||||
@@ -31,14 +30,27 @@ import {
|
||||
import { ChevronDownIcon } from "@heroicons/react/20/solid";
|
||||
import useLocalStorage from "@/src/components/useLocalStorage";
|
||||
import { ProjectNavigation } from "@/src/components/projectNavigation";
|
||||
import DOMPurify from "dompurify";
|
||||
import { ThemeToggle } from "@/src/features/theming/ThemeToggle";
|
||||
|
||||
const signOutUser = async () => {
|
||||
localStorage.clear();
|
||||
sessionStorage.clear();
|
||||
|
||||
await signOut({
|
||||
callbackUrl: "/auth/sign-in",
|
||||
});
|
||||
};
|
||||
|
||||
const userNavigation = [
|
||||
{
|
||||
name: "Theme",
|
||||
onClick: () => {},
|
||||
content: <ThemeToggle />,
|
||||
},
|
||||
{
|
||||
name: "Sign out",
|
||||
onClick: () =>
|
||||
signOut({
|
||||
callbackUrl: "/auth/sign-in",
|
||||
}),
|
||||
onClick: signOutUser,
|
||||
},
|
||||
];
|
||||
|
||||
@@ -61,7 +73,7 @@ export default function Layout(props: PropsWithChildren) {
|
||||
useCheckNotification(NOTIFICATIONS, session.status === "authenticated");
|
||||
|
||||
const enableExperimentalFeatures =
|
||||
api.environment.enableExperimentalFeatures.useQuery().data ?? false;
|
||||
session.data?.environment.enableExperimentalFeatures ?? false;
|
||||
|
||||
const projectId = router.query.projectId as string | undefined;
|
||||
|
||||
@@ -135,9 +147,8 @@ export default function Layout(props: PropsWithChildren) {
|
||||
!publishablePaths.includes(router.pathname) &&
|
||||
!router.pathname.startsWith("/public/")
|
||||
) {
|
||||
void signOut({
|
||||
callbackUrl: "/auth/sign-in",
|
||||
});
|
||||
signOutUser();
|
||||
|
||||
return <Spinner message="Redirecting" />;
|
||||
}
|
||||
|
||||
@@ -147,7 +158,14 @@ export default function Layout(props: PropsWithChildren) {
|
||||
!publishablePaths.includes(router.pathname) &&
|
||||
!router.pathname.startsWith("/public/")
|
||||
) {
|
||||
void router.replace("/auth/sign-in");
|
||||
const newTargetPath = router.asPath;
|
||||
if (newTargetPath && newTargetPath !== "/") {
|
||||
void router.replace(
|
||||
`/auth/sign-in?targetPath=${encodeURIComponent(newTargetPath)}`,
|
||||
);
|
||||
} else {
|
||||
void router.replace(`/auth/sign-in`);
|
||||
}
|
||||
return <Spinner message="Redirecting" />;
|
||||
}
|
||||
|
||||
@@ -155,18 +173,23 @@ export default function Layout(props: PropsWithChildren) {
|
||||
session.status === "authenticated" &&
|
||||
unauthenticatedPaths.includes(router.pathname)
|
||||
) {
|
||||
void router.replace("/");
|
||||
const targetPath = router.query.targetPath as string | undefined;
|
||||
|
||||
const sanitizedTargetPath = targetPath
|
||||
? DOMPurify.sanitize(targetPath)
|
||||
: undefined;
|
||||
|
||||
void router.replace(sanitizedTargetPath ?? "/");
|
||||
return <Spinner message="Redirecting" />;
|
||||
}
|
||||
|
||||
const hideNavigation =
|
||||
session.status === "unauthenticated" ||
|
||||
projects.length === 0 ||
|
||||
pathsWithoutNavigation.includes(router.pathname) ||
|
||||
router.pathname.startsWith("/public/");
|
||||
if (hideNavigation)
|
||||
return (
|
||||
<main className="min-h-screen bg-gray-50 px-4 py-4 sm:px-6 lg:px-8">
|
||||
<main className="min-h-screen bg-primary-foreground px-4 py-4 sm:px-6 lg:px-8">
|
||||
{props.children}
|
||||
</main>
|
||||
);
|
||||
@@ -210,7 +233,7 @@ export default function Layout(props: PropsWithChildren) {
|
||||
leaveFrom="opacity-100"
|
||||
leaveTo="opacity-0"
|
||||
>
|
||||
<div className="fixed inset-0 bg-gray-900/80" />
|
||||
<div className="fixed inset-0 bg-primary/80" />
|
||||
</Transition.Child>
|
||||
|
||||
<div className="fixed inset-0 flex">
|
||||
@@ -241,14 +264,14 @@ export default function Layout(props: PropsWithChildren) {
|
||||
>
|
||||
<span className="sr-only">Close sidebar</span>
|
||||
<XMarkIcon
|
||||
className="h-6 w-6 text-white"
|
||||
className="h-5 w-5 text-background"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
</Transition.Child>
|
||||
{/* Sidebar component, swap this element with another sidebar if you like */}
|
||||
<div className="flex grow flex-col gap-y-5 overflow-y-auto bg-white px-6 py-4">
|
||||
<div className="flex grow flex-col gap-y-5 overflow-y-auto bg-background px-6 py-4">
|
||||
<LangfuseLogo
|
||||
version
|
||||
size="xl"
|
||||
@@ -261,7 +284,7 @@ export default function Layout(props: PropsWithChildren) {
|
||||
<MainNavigation nav={navigation} />
|
||||
</ul>
|
||||
<div className="mb-2 flex flex-row place-content-between items-center">
|
||||
<div className="text-xs font-semibold leading-6 text-gray-400">
|
||||
<div className="text-xs font-semibold text-muted-foreground">
|
||||
Project
|
||||
</div>
|
||||
<NewProjectButton size="xs" />
|
||||
@@ -279,9 +302,9 @@ export default function Layout(props: PropsWithChildren) {
|
||||
</Transition.Root>
|
||||
|
||||
{/* Static sidebar for desktop */}
|
||||
<div className="hidden lg:fixed lg:inset-y-0 lg:z-50 lg:flex lg:w-60 lg:flex-col">
|
||||
<div className="hidden lg:fixed lg:inset-y-0 lg:z-50 lg:flex lg:w-56 lg:flex-col">
|
||||
{/* Sidebar component, swap this element with another sidebar if you like */}
|
||||
<div className="flex h-screen grow flex-col border-r border-gray-200 bg-white pt-7">
|
||||
<div className="flex h-screen grow flex-col border-r border-border bg-background pt-7">
|
||||
<LangfuseLogo
|
||||
version
|
||||
size="xl"
|
||||
@@ -300,16 +323,16 @@ export default function Layout(props: PropsWithChildren) {
|
||||
description="What do you think about this project? What can be improved?"
|
||||
type="feedback"
|
||||
>
|
||||
<li className="group -mx-2 my-1 flex cursor-pointer gap-x-3 rounded-md p-2 text-sm font-semibold leading-6 text-gray-700 hover:bg-gray-50 hover:text-indigo-600">
|
||||
<li className="group -mx-2 my-1 flex cursor-pointer gap-x-3 rounded-md p-1.5 text-sm font-semibold text-primary hover:bg-primary-foreground hover:text-primary-accent">
|
||||
<MessageSquarePlus
|
||||
className="h-6 w-6 shrink-0 text-gray-400 group-hover:text-indigo-600"
|
||||
className="h-5 w-5 shrink-0 text-muted-foreground group-hover:text-primary-accent"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
Feedback
|
||||
</li>
|
||||
</FeedbackButtonWrapper>
|
||||
<div className="mb-2 flex flex-row place-content-between items-center">
|
||||
<div className="text-xs font-semibold leading-6 text-gray-400">
|
||||
<div className="text-xs font-semibold text-muted-foreground">
|
||||
Project
|
||||
</div>
|
||||
<NewProjectButton size="xs" />
|
||||
@@ -322,9 +345,9 @@ export default function Layout(props: PropsWithChildren) {
|
||||
</nav>
|
||||
|
||||
<Menu as="div" className="relative">
|
||||
<Menu.Button className="flex w-full items-center gap-x-4 overflow-hidden p-1.5 py-3 pl-6 pr-10 text-sm font-semibold leading-6 text-gray-900 hover:bg-gray-50">
|
||||
<Menu.Button className="flex w-full items-center gap-x-2 overflow-hidden p-1.5 py-3 pl-6 pr-8 text-sm font-semibold text-primary hover:bg-primary-foreground">
|
||||
<span className="sr-only">Open user menu</span>
|
||||
<Avatar className="h-8 w-8">
|
||||
<Avatar className="h-7 w-7">
|
||||
<AvatarImage src={session.data?.user?.image ?? undefined} />
|
||||
<AvatarFallback>
|
||||
{session.data?.user?.name
|
||||
@@ -336,12 +359,12 @@ export default function Layout(props: PropsWithChildren) {
|
||||
: null}
|
||||
</AvatarFallback>
|
||||
</Avatar>
|
||||
<span className="flex-shrink truncate text-sm font-semibold leading-6 text-gray-900">
|
||||
<span className="flex-shrink truncate text-sm font-semibold text-primary">
|
||||
{session.data?.user?.name}
|
||||
</span>
|
||||
<div className="flex-1" />
|
||||
<ChevronDownIcon
|
||||
className="h-5 w-5 text-gray-400"
|
||||
className="h-5 w-5 text-muted-foreground"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
</Menu.Button>
|
||||
@@ -354,8 +377,8 @@ export default function Layout(props: PropsWithChildren) {
|
||||
leaveFrom="transform opacity-100 scale-100"
|
||||
leaveTo="transform opacity-0 scale-95"
|
||||
>
|
||||
<Menu.Items className="absolute -top-full right-0 z-10 mt-2.5 rounded-md bg-white py-2 shadow-lg ring-1 ring-gray-900/5 focus:outline-none">
|
||||
<span className="mb-1 block border-b px-3 pb-2 text-sm leading-6 text-gray-500">
|
||||
<Menu.Items className="absolute -top-full bottom-1 right-0 z-10 overflow-hidden rounded-md bg-background py-2 shadow-lg ring-1 ring-border focus:outline-none">
|
||||
<span className="block border-b px-3 pb-2 text-sm leading-6 text-muted-foreground">
|
||||
{session.data?.user?.email}
|
||||
</span>
|
||||
{userNavigation.map((item) => (
|
||||
@@ -364,11 +387,12 @@ export default function Layout(props: PropsWithChildren) {
|
||||
<a
|
||||
onClick={() => void item.onClick()}
|
||||
className={cn(
|
||||
active ? "bg-gray-50" : "",
|
||||
"block cursor-pointer px-3 py-1 text-sm leading-6 text-gray-900",
|
||||
active ? "bg-primary-foreground" : "",
|
||||
"flex cursor-pointer items-center justify-between px-2 py-0.5 text-sm leading-6 text-primary",
|
||||
)}
|
||||
>
|
||||
{item.name}
|
||||
{item.content}
|
||||
</a>
|
||||
)}
|
||||
</Menu.Item>
|
||||
@@ -379,14 +403,14 @@ export default function Layout(props: PropsWithChildren) {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="sticky top-0 z-40 flex items-center gap-x-6 bg-white px-4 py-4 shadow-sm sm:px-6 lg:hidden">
|
||||
<div className="sticky top-0 z-40 flex items-center gap-x-6 bg-background px-4 py-4 shadow-sm sm:px-6 lg:hidden">
|
||||
<button
|
||||
type="button"
|
||||
className="-m-2.5 p-2.5 text-gray-700 lg:hidden"
|
||||
className="-m-2.5 p-2.5 text-primary lg:hidden"
|
||||
onClick={() => setSidebarOpen(true)}
|
||||
>
|
||||
<span className="sr-only">Open sidebar</span>
|
||||
<Bars3Icon className="h-6 w-6" aria-hidden="true" />
|
||||
<Bars3Icon className="h-5 w-5" aria-hidden="true" />
|
||||
</button>
|
||||
<LangfuseLogo
|
||||
version
|
||||
@@ -394,7 +418,7 @@ export default function Layout(props: PropsWithChildren) {
|
||||
showEnvLabel={session.data?.user?.email?.endsWith("@langfuse.com")}
|
||||
/>
|
||||
<Menu as="div" className="relative">
|
||||
<Menu.Button className="flex items-center gap-x-4 text-sm font-semibold leading-6 text-gray-900">
|
||||
<Menu.Button className="flex items-center gap-x-4 text-sm font-semibold text-primary">
|
||||
<span className="sr-only">Open user menu</span>
|
||||
<Avatar className="h-8 w-8">
|
||||
<AvatarImage src={session.data?.user?.image ?? undefined} />
|
||||
@@ -418,8 +442,8 @@ export default function Layout(props: PropsWithChildren) {
|
||||
leaveFrom="transform opacity-100 scale-100"
|
||||
leaveTo="transform opacity-0 scale-95"
|
||||
>
|
||||
<Menu.Items className="absolute right-0 z-10 mt-2.5 rounded-md bg-white py-2 shadow-lg ring-1 ring-gray-900/5 focus:outline-none">
|
||||
<span className="mb-1 block border-b px-3 pb-2 text-sm leading-6 text-gray-500">
|
||||
<Menu.Items className="absolute right-0 z-10 mt-2.5 rounded-md bg-background py-2 pb-1 shadow-lg ring-1 ring-border focus:outline-none">
|
||||
<span className="mb-1 block border-b px-3 pb-2 text-sm leading-6 text-muted-foreground">
|
||||
{session.data?.user?.email}
|
||||
</span>
|
||||
{userNavigation.map((item) => (
|
||||
@@ -428,11 +452,12 @@ export default function Layout(props: PropsWithChildren) {
|
||||
<a
|
||||
onClick={() => void item.onClick()}
|
||||
className={cn(
|
||||
active ? "bg-gray-50" : "",
|
||||
"block cursor-pointer px-3 py-1 text-sm leading-6 text-gray-900",
|
||||
active ? "bg-primary-foreground" : "",
|
||||
"flex cursor-pointer items-center justify-between px-2 py-1 text-sm leading-6 text-primary",
|
||||
)}
|
||||
>
|
||||
{item.name}
|
||||
{item.content}
|
||||
</a>
|
||||
)}
|
||||
</Menu.Item>
|
||||
@@ -441,13 +466,13 @@ export default function Layout(props: PropsWithChildren) {
|
||||
</Transition>
|
||||
</Menu>
|
||||
</div>
|
||||
<div className="lg:pl-60">
|
||||
<div className="lg:pl-56">
|
||||
{env.NEXT_PUBLIC_DEMO_PROJECT_ID &&
|
||||
projectId === env.NEXT_PUBLIC_DEMO_PROJECT_ID &&
|
||||
(env.NEXT_PUBLIC_LANGFUSE_CLOUD_REGION === "STAGING" ||
|
||||
env.NEXT_PUBLIC_LANGFUSE_CLOUD_REGION === "EU") &&
|
||||
!session.data?.user?.email?.endsWith("@langfuse.com") ? (
|
||||
<div className="flex w-full items-center border-b border-yellow-500 bg-yellow-100 px-4 py-2 lg:sticky lg:top-0 lg:z-40">
|
||||
<div className="flex w-full items-center border-b border-dark-yellow bg-light-yellow px-4 py-2 lg:sticky lg:top-0 lg:z-40">
|
||||
<div className="flex flex-1 flex-wrap gap-1">
|
||||
<div className="flex items-center gap-1">
|
||||
<Info className="h-4 w-4" />
|
||||
@@ -511,9 +536,9 @@ const MainNavigation: React.FC<{
|
||||
href={item.href}
|
||||
className={clsx(
|
||||
item.current
|
||||
? "bg-gray-50 text-indigo-600"
|
||||
: "text-gray-700 hover:bg-gray-50 hover:text-indigo-600",
|
||||
"group flex gap-x-3 rounded-md p-2 text-sm font-semibold leading-6",
|
||||
? "bg-primary-foreground text-primary-accent"
|
||||
: "text-primary hover:bg-primary-foreground hover:text-primary-accent",
|
||||
"group flex gap-x-3 rounded-md p-2 text-sm font-semibold",
|
||||
)}
|
||||
onClick={onNavitemClick}
|
||||
target={item.newTab ? "_blank" : undefined}
|
||||
@@ -522,9 +547,9 @@ const MainNavigation: React.FC<{
|
||||
<item.icon
|
||||
className={clsx(
|
||||
item.current
|
||||
? "text-indigo-600"
|
||||
: "text-gray-400 group-hover:text-indigo-600",
|
||||
"h-6 w-6 shrink-0",
|
||||
? "text-primary-accent"
|
||||
: "text-muted-foreground group-hover:text-primary-accent",
|
||||
"h-5 w-5 shrink-0",
|
||||
)}
|
||||
aria-hidden="true"
|
||||
/>
|
||||
@@ -533,10 +558,10 @@ const MainNavigation: React.FC<{
|
||||
{item.label && (
|
||||
<span
|
||||
className={cn(
|
||||
"self-center whitespace-nowrap break-keep rounded-sm border px-1 py-0.5 text-xs",
|
||||
"-my-0.5 self-center whitespace-nowrap break-keep rounded-sm border px-1 py-0.5 text-xs",
|
||||
item.current
|
||||
? "border-indigo-600 text-indigo-600"
|
||||
: "border-gray-200 text-gray-400 group-hover:border-indigo-600 group-hover:text-indigo-600",
|
||||
? "border-primary-accent text-primary-accent"
|
||||
: "border-border text-muted-foreground group-hover:border-primary-accent group-hover:text-primary-accent",
|
||||
)}
|
||||
>
|
||||
{item.label}
|
||||
@@ -553,12 +578,12 @@ const MainNavigation: React.FC<{
|
||||
{({ open }) => (
|
||||
<>
|
||||
<Disclosure.Button
|
||||
className="group flex w-full items-center gap-x-3 rounded-md p-2 text-left text-sm font-semibold leading-6 hover:bg-gray-50 hover:text-indigo-600"
|
||||
className="group flex w-full items-center gap-x-3 rounded-md p-2 text-left text-sm font-semibold hover:bg-primary-foreground hover:text-primary-accent"
|
||||
onClick={() => setIsOpen(!isOpen)}
|
||||
>
|
||||
{item.icon && (
|
||||
<item.icon
|
||||
className="h-6 w-6 shrink-0 text-gray-400 group-hover:text-indigo-600"
|
||||
className="h-5 w-5 shrink-0 text-muted-foreground group-hover:text-primary-accent"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
)}
|
||||
@@ -566,10 +591,10 @@ const MainNavigation: React.FC<{
|
||||
{item.label && (
|
||||
<span
|
||||
className={cn(
|
||||
"self-center whitespace-nowrap break-keep rounded-sm border px-1 py-0.5 text-xs",
|
||||
"-my-0.5 self-center whitespace-nowrap break-keep rounded-sm border px-1 py-0.5 text-xs",
|
||||
item.current
|
||||
? "border-indigo-600 text-indigo-600"
|
||||
: "border-gray-200 text-gray-400 group-hover:border-indigo-600 group-hover:text-indigo-600",
|
||||
? "border-primary-accent text-primary-accent"
|
||||
: "border-border text-muted-foreground group-hover:border-primary-accent group-hover:text-primary-accent",
|
||||
)}
|
||||
>
|
||||
{item.label}
|
||||
@@ -577,13 +602,15 @@ const MainNavigation: React.FC<{
|
||||
)}
|
||||
<ChevronRightIcon
|
||||
className={clsx(
|
||||
open ? "rotate-90 text-gray-500" : "text-gray-400",
|
||||
open
|
||||
? "rotate-90 text-muted-foreground"
|
||||
: "text-muted-foreground",
|
||||
"ml-auto h-5 w-5 shrink-0",
|
||||
)}
|
||||
aria-hidden="true"
|
||||
/>
|
||||
</Disclosure.Button>
|
||||
<Disclosure.Panel as="ul" className="mt-1 px-2">
|
||||
<Disclosure.Panel as="ul" className="mt-1 space-y-1 px-2">
|
||||
{item.children?.map((subItem) => (
|
||||
<li key={subItem.name}>
|
||||
{/* 44px */}
|
||||
@@ -591,15 +618,15 @@ const MainNavigation: React.FC<{
|
||||
href={subItem.href ?? "#"}
|
||||
className={clsx(
|
||||
subItem.current
|
||||
? "bg-gray-50 text-indigo-600"
|
||||
: "text-gray-700 hover:bg-gray-50 hover:text-indigo-600",
|
||||
"flex w-full items-center gap-x-3 rounded-md py-2 pl-9 pr-2 text-sm leading-6",
|
||||
? "bg-primary-foreground text-primary-accent"
|
||||
: "text-primary hover:bg-primary-foreground hover:text-primary-accent",
|
||||
"ml-0.5 flex w-full items-center gap-x-3 rounded-md p-1.5 pl-7 pr-2 text-sm",
|
||||
)}
|
||||
target={subItem.newTab ? "_blank" : undefined}
|
||||
>
|
||||
{subItem.name}
|
||||
{subItem.label && (
|
||||
<span className="self-center whitespace-nowrap break-keep rounded-sm border border-gray-200 px-1 py-0.5 text-xs text-gray-400 group-hover:border-indigo-600 group-hover:text-indigo-600">
|
||||
<span className="self-center whitespace-nowrap break-keep rounded-sm border border-border px-1 py-0.5 text-xs text-muted-foreground group-hover:border-primary-accent group-hover:text-primary-accent">
|
||||
{subItem.label}
|
||||
</span>
|
||||
)}
|
||||
|
||||
@@ -63,19 +63,25 @@ export const ROUTES: Route[] = [
|
||||
name: "Evaluation",
|
||||
icon: Lightbulb,
|
||||
cloudOnly: true,
|
||||
featureFlag: "evals",
|
||||
label: "Beta",
|
||||
children: [
|
||||
{
|
||||
name: "Templates",
|
||||
pathname: `/project/[projectId]/evals/templates`,
|
||||
cloudOnly: true,
|
||||
featureFlag: "evals",
|
||||
rbacScope: "evalTemplate:read",
|
||||
},
|
||||
{
|
||||
name: "Configs",
|
||||
pathname: `/project/[projectId]/evals/configs`,
|
||||
cloudOnly: true,
|
||||
featureFlag: "evals",
|
||||
rbacScope: "evalJob:read",
|
||||
},
|
||||
{
|
||||
name: "Log",
|
||||
pathname: `/project/[projectId]/evals/log`,
|
||||
cloudOnly: true,
|
||||
rbacScope: "evalJobExecution:read",
|
||||
},
|
||||
],
|
||||
},
|
||||
@@ -94,7 +100,6 @@ export const ROUTES: Route[] = [
|
||||
name: "Playground",
|
||||
pathname: "/project/[projectId]/playground",
|
||||
icon: TerminalIcon,
|
||||
featureFlag: "playground",
|
||||
cloudOnly: true,
|
||||
label: "Beta",
|
||||
},
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user