Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
af789fb3ac | ||
|
|
7fcf9e98ae | ||
|
|
45c7a95a9c | ||
|
|
75009cf83d | ||
|
|
70642da8ab | ||
|
|
6754733aac | ||
|
|
e45bd75afa | ||
|
|
9b271f0b78 | ||
|
|
e98565ee9c | ||
|
|
62d1f31527 | ||
|
|
c1190d70da | ||
|
|
bf9badb711 | ||
|
|
8155749b2b | ||
|
|
84c0ba6487 | ||
|
|
6320f48737 | ||
|
|
fe053405ea | ||
|
|
63d1abffb2 | ||
|
|
3050ed756f | ||
|
|
6eebf0b49d | ||
|
|
bbc57779c3 | ||
|
|
ccc9161967 | ||
|
|
6df7a2c716 | ||
|
|
88e4f210d0 | ||
|
|
011c1452a0 | ||
|
|
4a3025dbae | ||
|
|
367e8c8a61 | ||
|
|
84edae6468 | ||
|
|
87e04a3482 | ||
|
|
90abf7fb34 | ||
|
|
027c3791a0 | ||
|
|
4023e7fd4a | ||
|
|
3d178caa17 | ||
|
|
128b03bec0 | ||
|
|
73889fcc96 | ||
|
|
d88a295f8d | ||
|
|
15c5fdc2a9 | ||
|
|
ca9c0e07d1 | ||
|
|
bf0eab424f | ||
|
|
89d38ff6ad | ||
|
|
8d7b334521 | ||
|
|
a374a378a0 | ||
|
|
f95a6943a6 | ||
|
|
fec89b8c62 | ||
|
|
a10a6a7ca5 | ||
|
|
8f32ef935b | ||
|
|
4f0b309e0d | ||
|
|
c424a5578e | ||
|
|
2113c684a4 | ||
|
|
d900056f69 | ||
|
|
a2930f3f2a | ||
|
|
9a3fa2b461 | ||
|
|
4ac3b13ca6 | ||
|
|
4c32113ec3 | ||
|
|
7f81135e6b | ||
|
|
4a362ee10f | ||
|
|
04374dd0b3 | ||
|
|
a69571cedc | ||
|
|
53e78ee249 | ||
|
|
64c0e26da6 | ||
|
|
fa52811224 | ||
|
|
213d9da88a | ||
|
|
8611afd533 | ||
|
|
e96a3bbf6e | ||
|
|
60e636e37d | ||
|
|
e9e8319099 | ||
|
|
8ee0e611d9 | ||
|
|
b7b9cc8dd1 | ||
|
|
21240cb594 | ||
|
|
fdda5db1a3 | ||
|
|
0538f27caf | ||
|
|
c73bc5a77f | ||
|
|
9204c9dc69 | ||
|
|
feb935e737 | ||
|
|
e4a029988f | ||
|
|
d8bb7703ae | ||
|
|
bd7574411e | ||
|
|
2f55acec7c | ||
|
|
78207ba274 | ||
|
|
2250be58a5 | ||
|
|
338517828c | ||
|
|
2e80d4b475 | ||
|
|
45c9af76d8 | ||
|
|
bc27618c0e | ||
|
|
a4c8f0b702 | ||
|
|
787eb90c54 | ||
|
|
9b4601debf | ||
|
|
fcc0ee922f | ||
|
|
3271c4abd0 | ||
|
|
2dbb9fec87 | ||
|
|
e7438c05b8 | ||
|
|
45fcf8902b | ||
|
|
a4bfdb25a5 | ||
|
|
462e8a7b8c | ||
|
|
86edd502b3 | ||
|
|
01a7e679fa | ||
|
|
c6406e1ddb | ||
|
|
82d4881dcf | ||
|
|
42d3167591 | ||
|
|
977ac39ec5 | ||
|
|
e4d8db6a34 | ||
|
|
a39bbf541d | ||
|
|
eef5218dbd | ||
|
|
b5e802171b | ||
|
|
96c4f28b5b | ||
|
|
8ad4a46850 | ||
|
|
d8f2d6cf4e | ||
|
|
b8639ebb3b | ||
|
|
8be73970c2 | ||
|
|
e18ab26bb5 | ||
|
|
095478869d | ||
|
|
4db7dab8ae | ||
|
|
086d26b041 | ||
|
|
5fd042c7b1 | ||
|
|
dc37f1a58a | ||
|
|
bc4597e183 | ||
|
|
9bb1caf738 | ||
|
|
3ab4eb6341 | ||
|
|
5944611b51 | ||
|
|
e439c81512 | ||
|
|
799d32fa21 | ||
|
|
7927d0013f | ||
|
|
147262a437 | ||
|
|
c02bb556ca | ||
|
|
1b792d844f | ||
|
|
52a42812b4 | ||
|
|
3b84af8cbf | ||
|
|
1521445939 | ||
|
|
a1e961de09 | ||
|
|
c360cf10d4 | ||
|
|
3e2f3f3d28 | ||
|
|
11e5da2cd8 | ||
|
|
8205595f2c | ||
|
|
101277cab8 | ||
|
|
954e31ca4a | ||
|
|
43c3258135 | ||
|
|
eb8ae94a5d | ||
|
|
33abe53acc | ||
|
|
5e8ec0bd13 | ||
|
|
177f370914 | ||
|
|
5538242649 | ||
|
|
8c8e58e2b8 | ||
|
|
a2c367c1b6 | ||
|
|
da0ac73603 | ||
|
|
e60e74478c | ||
|
|
329f83e844 | ||
|
|
28c035da76 | ||
|
|
4dfeceb9d6 | ||
|
|
df802184a6 | ||
|
|
407d169095 | ||
|
|
e1958e73d4 | ||
|
|
2a08c3395c | ||
|
|
5c405f4ed8 | ||
|
|
25cc29f353 | ||
|
|
b5e1b9916f | ||
|
|
8075996a1c | ||
|
|
80cc85d756 | ||
|
|
45c611bc6d | ||
|
|
1478aa9c2b | ||
|
|
4944408b62 | ||
|
|
f6e81f818b | ||
|
|
b2d5839dcd | ||
|
|
2608878d20 | ||
|
|
62a2857619 | ||
|
|
0c1942c2b3 | ||
|
|
b95c93cdb1 | ||
|
|
3691099c74 | ||
|
|
3f1ae71eec | ||
|
|
668cf3f6c2 | ||
|
|
6221be155d | ||
|
|
3e13d191db | ||
|
|
61297826f4 | ||
|
|
3268ef6488 |
@@ -29,4 +29,8 @@ S3_ENDPOINT=
|
||||
S3_ACCESS_KEY_ID=
|
||||
S3_SECRET_ACCESS_KEY=
|
||||
S3_BUCKET_NAME=
|
||||
S3_REGION=
|
||||
S3_REGION=
|
||||
|
||||
# Set during docker build of application
|
||||
# Used to disable environment verification at build time
|
||||
# DOCKER_BUILD=1
|
||||
@@ -29,7 +29,7 @@ SALT="salt"
|
||||
# LANGFUSE_DEFAULT_PROJECT_ROLE=
|
||||
|
||||
# Enable experimental features, optional
|
||||
# NEXT_PUBLIC_ENABLE_EXPERIMENTAL_FEATURES
|
||||
# LANGFUSE_ENABLE_EXPERIMENTAL_FEATURES=true
|
||||
|
||||
# Auth, optional configuration
|
||||
# AUTH_DOMAINS_WITH_SSO_ENFORCEMENT=domain1.com,domain2.com
|
||||
@@ -38,11 +38,14 @@ SALT="salt"
|
||||
# SSO, each group is optional
|
||||
# AUTH_GOOGLE_CLIENT_ID=
|
||||
# AUTH_GOOGLE_CLIENT_SECRET=
|
||||
# AUTH_GOOGLE_ALLOW_ACCOUNT_LINKING=false
|
||||
# AUTH_GITHUB_CLIENT_ID=
|
||||
# AUTH_GITHUB_CLIENT_SECRET=
|
||||
# AUTH_GITHUB_ALLOW_ACCOUNT_LINKING=false
|
||||
# AUTH_AZURE_AD_CLIENT_ID=
|
||||
# AUTH_AZURE_AD_CLIENT_SECRET=
|
||||
# AUTH_AZURE_AD_TENANT_ID=
|
||||
# AUTH_AZURE_ALLOW_ACCOUNT_LINKING=false
|
||||
|
||||
# Transactional email, optional
|
||||
# Defines the email address to use as the from address.
|
||||
@@ -57,6 +60,9 @@ SALT="salt"
|
||||
# S3_BUCKET_NAME=
|
||||
# S3_REGION=
|
||||
|
||||
# Exports are streamed to S3 in pages to avoid memory issues
|
||||
# The page size can be adjusted if needed to optimize performance
|
||||
# DB_EXPORT_PAGE_SIZE=1000
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
node_modules
|
||||
build
|
||||
coverage
|
||||
@@ -9,10 +9,3 @@ body:
|
||||
attributes:
|
||||
label: Additional information
|
||||
description: Add any other information related to the change here. If your idea is related to any issues or discussions, link them here.
|
||||
- type: checkboxes
|
||||
id: contribute
|
||||
attributes:
|
||||
label: Contribute
|
||||
description: Are you willing to contribute to the implementation of this idea?
|
||||
options:
|
||||
- label: Yes, I can implement this and raise a PR
|
||||
|
||||
@@ -6,7 +6,34 @@
|
||||
version: 2
|
||||
updates:
|
||||
- package-ecosystem: npm
|
||||
directory: "/" # Location of package manifests
|
||||
directory: "/worker" # Location of package manifests
|
||||
schedule:
|
||||
interval: "daily"
|
||||
rebase-strategy: "disabled" # use dependabot-rebase-stale
|
||||
commit-message:
|
||||
prefix: chore
|
||||
prefix-development: chore
|
||||
include: scope
|
||||
ignore:
|
||||
- dependency-name: "@types/node"
|
||||
- dependency-name: "@trpc/*"
|
||||
groups:
|
||||
sentry:
|
||||
patterns:
|
||||
- "@sentry/*"
|
||||
prisma:
|
||||
patterns:
|
||||
- "prisma"
|
||||
- "@prisma/*"
|
||||
next:
|
||||
patterns:
|
||||
- "eslint-config-next"
|
||||
- "next"
|
||||
patches:
|
||||
update-types:
|
||||
- "patch"
|
||||
- package-ecosystem: npm
|
||||
directory: "/web" # Location of package manifests
|
||||
schedule:
|
||||
interval: "daily"
|
||||
rebase-strategy: "disabled" # use dependabot-rebase-stale
|
||||
|
||||
@@ -12,8 +12,15 @@ on:
|
||||
branches:
|
||||
- "main"
|
||||
|
||||
defaults:
|
||||
run:
|
||||
working-directory: web
|
||||
|
||||
jobs:
|
||||
lint:
|
||||
defaults:
|
||||
run:
|
||||
working-directory: .
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
@@ -21,46 +28,45 @@ jobs:
|
||||
with:
|
||||
node-version: 20
|
||||
cache: "npm"
|
||||
cache-dependency-path: "**/package-lock.json"
|
||||
- name: install dependencies
|
||||
run: |
|
||||
npm ci
|
||||
- name: Load default env
|
||||
run: |
|
||||
cp .env.dev.example .env
|
||||
- name: lint
|
||||
- name: lint web
|
||||
working-directory: ./web
|
||||
run: npm run lint
|
||||
- name: lint worker
|
||||
working-directory: ./worker
|
||||
run: npm run lint
|
||||
|
||||
test-docker-build:
|
||||
defaults:
|
||||
run:
|
||||
working-directory: web
|
||||
runs-on: ubuntu-latest
|
||||
env:
|
||||
DATABASE_URL: postgresql://postgres:postgres@localhost:5432/postgres
|
||||
NEXTAUTH_SECRET: "secret"
|
||||
SALT: "salt"
|
||||
NEXTAUTH_URL: "http://localhost:3030"
|
||||
REGISTRY: ghcr.io
|
||||
IMAGE_NAME: ${{ github.repository }}
|
||||
|
||||
steps:
|
||||
- name: Setup node
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: 20
|
||||
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: Build Docker image
|
||||
- name: Build Web Docker image
|
||||
uses: docker/build-push-action@v4
|
||||
with:
|
||||
context: .
|
||||
context: web
|
||||
push: false
|
||||
|
||||
- name: Build Worker Docker image
|
||||
uses: docker/build-push-action@v4
|
||||
with:
|
||||
context: worker
|
||||
push: false
|
||||
build-args: |
|
||||
DATABASE_URL=${{ env.DATABASE_URL }}
|
||||
NEXTAUTH_SECRET=${{ env.NEXTAUTH_SECRET }}
|
||||
NEXTAUTH_URL=${{ env.NEXTAUTH_URL }}
|
||||
SALT=${{ env.SALT }}
|
||||
|
||||
tests:
|
||||
defaults:
|
||||
run:
|
||||
working-directory: web
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
matrix:
|
||||
@@ -77,19 +83,26 @@ jobs:
|
||||
with:
|
||||
node-version: ${{ matrix.node-version }}
|
||||
cache: "npm"
|
||||
cache-dependency-path: "**/package-lock.json"
|
||||
|
||||
- name: install dependencies
|
||||
run: |
|
||||
npm ci
|
||||
|
||||
- name: Load default env
|
||||
working-directory: .
|
||||
run: |
|
||||
cp .env.dev.example .env
|
||||
cp .env.dev.example web/.env
|
||||
|
||||
- name: Run, migrate, seed DB
|
||||
- name: Run + migrate
|
||||
working-directory: .
|
||||
run: |
|
||||
docker-compose -f docker-compose.dev.yml up -d
|
||||
sleep 5 # Wait for PostgreSQL to accept connections
|
||||
|
||||
- name: Seed DB
|
||||
run: |
|
||||
npx --yes prisma migrate reset --force --skip-generate
|
||||
|
||||
- name: Build
|
||||
@@ -102,6 +115,9 @@ jobs:
|
||||
run: npm run test
|
||||
|
||||
e2e-tests:
|
||||
defaults:
|
||||
run:
|
||||
working-directory: web
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
@@ -109,19 +125,26 @@ jobs:
|
||||
with:
|
||||
node-version: 20
|
||||
cache: "npm"
|
||||
cache-dependency-path: "**/package-lock.json"
|
||||
|
||||
- name: install dependencies
|
||||
run: |
|
||||
npm ci
|
||||
|
||||
- name: Load default env
|
||||
working-directory: .
|
||||
run: |
|
||||
cp .env.dev.example .env
|
||||
cp .env.dev.example web/.env
|
||||
|
||||
- name: Run, migrate, seed DB
|
||||
- name: Run + migrate
|
||||
working-directory: .
|
||||
run: |
|
||||
docker-compose -f docker-compose.dev.yml up -d
|
||||
sleep 5 # Wait for PostgreSQL to accept connections
|
||||
|
||||
- name: Seed DB
|
||||
run: |
|
||||
npx --yes prisma migrate reset --force --skip-generate
|
||||
|
||||
- name: Build
|
||||
@@ -142,9 +165,11 @@ jobs:
|
||||
- name: Successful deploy
|
||||
if: ${{ !(contains(needs.*.result, 'failure')) }}
|
||||
run: exit 0
|
||||
working-directory: .
|
||||
- name: Failing deploy
|
||||
if: ${{ contains(needs.*.result, 'failure') }}
|
||||
run: exit 1
|
||||
working-directory: .
|
||||
|
||||
push-docker-image:
|
||||
needs: all-ci-passed
|
||||
@@ -152,10 +177,6 @@ jobs:
|
||||
environment: "protected branches"
|
||||
runs-on: ubuntu-latest
|
||||
env:
|
||||
DATABASE_URL: postgresql://postgres:postgres@localhost:5432/postgres
|
||||
NEXTAUTH_SECRET: "secret"
|
||||
SALT: "salt"
|
||||
NEXTAUTH_URL: "http://localhost:3030"
|
||||
REGISTRY: ghcr.io
|
||||
IMAGE_NAME: ${{ github.repository }}
|
||||
permissions:
|
||||
@@ -167,6 +188,7 @@ jobs:
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: 20
|
||||
cache-dependency-path: "**/package-lock.json"
|
||||
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v3
|
||||
@@ -190,16 +212,12 @@ jobs:
|
||||
type=sha
|
||||
type=semver,pattern={{version}}
|
||||
type=semver,pattern={{major}}.{{minor}}
|
||||
type=semver,pattern={{major}}
|
||||
|
||||
- name: Build and push Docker image
|
||||
uses: docker/build-push-action@v4
|
||||
with:
|
||||
context: .
|
||||
context: web
|
||||
push: true
|
||||
tags: ${{ steps.meta.outputs.tags }}
|
||||
labels: ${{ steps.meta.outputs.labels }}
|
||||
build-args: |
|
||||
DATABASE_URL=${{ env.DATABASE_URL }}
|
||||
NEXTAUTH_SECRET=${{ env.NEXTAUTH_SECRET }}
|
||||
NEXTAUTH_URL=${{ env.NEXTAUTH_URL }}
|
||||
SALT=${{ env.SALT }}
|
||||
|
||||
@@ -21,13 +21,11 @@
|
||||
"**/*.{cjs,mjs,ts,tsx,astro,md,mdx,json,yaml,yml}"
|
||||
],
|
||||
"mdx.experimentalLanguageServer": true,
|
||||
"[astro]": {
|
||||
"editor.defaultFormatter": "astro-build.astro-vscode"
|
||||
},
|
||||
"typescript.preferences.importModuleSpecifier": "non-relative",
|
||||
"docwriter.style": "JSDoc",
|
||||
"[prisma]": {
|
||||
"editor.defaultFormatter": "Prisma.prisma"
|
||||
},
|
||||
"eslint.lintTask.enable": true
|
||||
"eslint.lintTask.enable": true,
|
||||
"eslint.workingDirectories": ["./web", "./worker"]
|
||||
}
|
||||
|
||||
@@ -91,9 +91,9 @@ flowchart TB
|
||||
|
||||
The diagram below may not show all relationships if the foreign key is not defined in the database schema. For instance, `trace_id` in the `observation` table is not defined as a foreign key to the `trace` table to allow unordered ingestion of these objects, but it is still a foreign key in the application code.
|
||||
|
||||
Full database schema: [prisma/schema.prisma](prisma/schema.prisma)
|
||||
Full database schema: [web/prisma/schema.prisma](web/prisma/schema.prisma)
|
||||
|
||||
<img src="./prisma/database.svg">
|
||||
<img src="./web/prisma/database.svg">
|
||||
|
||||
### Infrastructure & Network Overview
|
||||
|
||||
@@ -110,7 +110,14 @@ flowchart LR
|
||||
App --- DB
|
||||
```
|
||||
|
||||
## Development Setup
|
||||
## Repository Structure
|
||||
|
||||
This repository contains the following packages:
|
||||
|
||||
- `web`: is the main application package providing Frontend and Backend APIs for Langfuse
|
||||
- `worker` (no production yet): contains an application for asynchronous processing of tasks. This package is not yet used in production.
|
||||
|
||||
## Development Setup (web package only)
|
||||
|
||||
Requirements
|
||||
|
||||
@@ -120,25 +127,33 @@ Requirements
|
||||
**Steps**
|
||||
|
||||
1. Fork the the repository and clone it locally
|
||||
2. Install dependencies
|
||||
2. Run the development database
|
||||
|
||||
```bash
|
||||
npm install
|
||||
npm run infra:dev:up
|
||||
```
|
||||
|
||||
3. Run the development database
|
||||
|
||||
```bash
|
||||
docker-compose -f docker-compose.dev.yml up -d
|
||||
```
|
||||
|
||||
4. Create an env file
|
||||
3. Create an env file
|
||||
|
||||
```bash
|
||||
cp .env.dev.example .env
|
||||
```
|
||||
|
||||
5. Run the migrations
|
||||
4. CD into the web directory
|
||||
|
||||
```bash
|
||||
cd web
|
||||
```
|
||||
|
||||
5. Install dependencies
|
||||
|
||||
```bash
|
||||
npm install
|
||||
```
|
||||
|
||||
6. Run the migrations
|
||||
|
||||
The `schema.prisma` file is in the `web` directory and hence, migrations are applied from there.
|
||||
|
||||
```bash
|
||||
npm run db:migrate
|
||||
@@ -148,7 +163,7 @@ Requirements
|
||||
# npm run db:seed:examples
|
||||
```
|
||||
|
||||
6. Start the development server
|
||||
7. Start the development server
|
||||
|
||||
```bash
|
||||
npm run dev
|
||||
|
||||
@@ -21,21 +21,12 @@
|
||||
</div>
|
||||
</br>
|
||||
<div>
|
||||
<a href="https://langfuse.com/docs">
|
||||
<strong>Docs</strong>
|
||||
</a> ·
|
||||
<a href="https://langfuse.com/issue">
|
||||
<strong>Report Bug</strong>
|
||||
</a> ·
|
||||
<a href="https://langfuse.com/idea">
|
||||
<strong>Feature Request</strong>
|
||||
</a> ·
|
||||
<a href="https://langfuse.com/changelog">
|
||||
<strong>Changelog</strong>
|
||||
</a> ·
|
||||
<a href="https://langfuse.com/discord">
|
||||
<strong>Discord</strong>
|
||||
</a>
|
||||
<a href="https://langfuse.com/docs"><strong>Docs</strong></a> ·
|
||||
<a href="https://langfuse.com/issues"><strong>Report Bug</strong></a> ·
|
||||
<a href="https://langfuse.com/ideas"><strong>Feature Request</strong></a> ·
|
||||
<a href="https://langfuse.com/changelog"><strong>Changelog</strong></a> ·
|
||||
<a href="https://langfuse.com/roadmap"><strong>Roadmap</strong></a> ·
|
||||
<a href="https://langfuse.com/discord"><strong>Discord</strong></a>
|
||||
</div>
|
||||
</br>
|
||||
<img src="https://img.shields.io/badge/License-MIT-red.svg?style=flat-square" alt="MIT License">
|
||||
@@ -119,20 +110,22 @@ See our the [→ Quickstart](https://langfuse.com/docs/get-started) to get start
|
||||
|
||||
### Integrations
|
||||
|
||||
| Integration | Supports | Description |
|
||||
| -------------------------------------------------------- | ------------- | ------------------------------------------------------------------------------- |
|
||||
| [**SDK** - _recommended_](https://langfuse.com/docs/sdk) | Python, JS/TS | Manual instrumentation using the SDKs for full flexibility. |
|
||||
| [OpenAI](https://langfuse.com/docs/openai) | Python | Automated instrumentation using drop-in replacement of OpenAI SDK. |
|
||||
| [Langchain](https://langfuse.com/docs/langchain) | Python, JS/TS | Automated instrumentation by passing callback handler to Langchain application. |
|
||||
| [API](https://langfuse.com/docs/api) | | Directly call the public API. OpenAPI spec available. |
|
||||
| Integration | Supports | Description |
|
||||
| ---------------------------------------------------------------- | ------------- | ---------------------------------------------------------------- |
|
||||
| [**SDK** - _recommended_](https://langfuse.com/docs/sdk) | Python, JS/TS | Manual instrumentation using the SDKs for full flexibility. |
|
||||
| [OpenAI SDK](https://langfuse.com/docs/integrations/openai) | Python | Automated instrumentation via drop-in replacement of OpenAI SDK. |
|
||||
| [Langchain](https://langfuse.com/docs/integrations/langchain) | Python, JS/TS | Automated instrumentation via LlamaIndex callback system. |
|
||||
| [LlamaIndex](https://langfuse.com/docs/integrations/llama-index) | Python, JS/TS | Automated instrumentation via LlamaIndex callback system. |
|
||||
| [API](https://langfuse.com/docs/api) | | Directly call the public API. OpenAPI spec available. |
|
||||
|
||||
External projects/packages that integrate with Langfuse:
|
||||
|
||||
| Name | Description |
|
||||
| ---------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------ |
|
||||
| [LiteLLM](/https://langfuse.comdocs/litellm) | Use any LLM as a drop in replacement for GPT. Use Azure, OpenAI, Cohere, Anthropic, Ollama, VLLM, Sagemaker, HuggingFace, Replicate (100+ LLMs). |
|
||||
| [Flowise](https://langfuse.com/docs/flowise) | JS/TS no-code builder for customized LLM flows. |
|
||||
| [Langflow](https://langfuse.com/docs/langflow) | Python-based UI for LangChain, designed with react-flow to provide an effortless way to experiment and prototype flows. |
|
||||
| Name | Description |
|
||||
| --------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------ |
|
||||
| [LiteLLM](https://langfuse.com/docs/integrations/litellm) | Use any LLM as a drop in replacement for GPT. Use Azure, OpenAI, Cohere, Anthropic, Ollama, VLLM, Sagemaker, HuggingFace, Replicate (100+ LLMs). |
|
||||
| [Flowise](https://langfuse.com/docs/integrations/flowise) | JS/TS no-code builder for customized LLM flows. |
|
||||
| [Langflow](https://langfuse.com/docs/integrations/langflow) | Python-based UI for LangChain, designed with react-flow to provide an effortless way to experiment and prototype flows. |
|
||||
| [Superagent](https://langfuse.com/docs/integrations/superagent) | Open Source AI Assistant Framework & API for prototyping and deployment of agents. |
|
||||
|
||||
## Questions and feedback
|
||||
|
||||
@@ -140,6 +133,7 @@ External projects/packages that integrate with Langfuse:
|
||||
|
||||
- [GitHub Discussions](https://github.com/orgs/langfuse/discussions)
|
||||
- [Feature Requests](https://langfuse.com/idea)
|
||||
- [Roadmap](https://langfuse.com/roadmap)
|
||||
|
||||
### Support and feedback
|
||||
|
||||
@@ -178,6 +172,6 @@ This helps us to:
|
||||
1. Understand how Langfuse is used and improve the most relevant features.
|
||||
2. Track overall usage for internal and external (e.g. fundraising) reporting.
|
||||
|
||||
None of the data is shared with third parties and does not include any sensitive information. We want to be super transparent about this and you can find the exact data we collect [here](/src/features/telemetry/index.ts).
|
||||
None of the data is shared with third parties and does not include any sensitive information. We want to be super transparent about this and you can find the exact data we collect [here](/web/src/features/telemetry/index.ts).
|
||||
|
||||
You can opt-out by setting `TELEMETRY_ENABLED=false`.
|
||||
|
||||
@@ -1,20 +1,16 @@
|
||||
version: "3.5"
|
||||
|
||||
services:
|
||||
langfuse-server:
|
||||
server:
|
||||
build:
|
||||
dockerfile: Dockerfile
|
||||
args:
|
||||
- DATABASE_URL=postgresql://postgres:postgres@db:5432/postgres
|
||||
- NEXTAUTH_SECRET=mysecret
|
||||
- SALT=mysalt
|
||||
- NEXTAUTH_URL=http://localhost:3000
|
||||
dockerfile: ./Dockerfile
|
||||
context: ./web
|
||||
depends_on:
|
||||
- db
|
||||
- redis
|
||||
ports:
|
||||
- "3000:3000"
|
||||
environment:
|
||||
- NODE_ENV=production
|
||||
- DATABASE_URL=postgresql://postgres:postgres@db:5432/postgres
|
||||
- NEXTAUTH_SECRET=mysecret
|
||||
- SALT=mysalt
|
||||
@@ -22,8 +18,40 @@ services:
|
||||
- TELEMETRY_ENABLED=${TELEMETRY_ENABLED:-true}
|
||||
- NEXT_PUBLIC_SIGN_UP_DISABLED=${NEXT_PUBLIC_SIGN_UP_DISABLED:-false}
|
||||
- LANGFUSE_ENABLE_EXPERIMENTAL_FEATURES=${LANGFUSE_ENABLE_EXPERIMENTAL_FEATURES:-false}
|
||||
- REDIS_URL=${REDIS_URL:-redis}
|
||||
- REDIS_PORT=${REDIS_PORT:-6379}
|
||||
- REDIS_AUTH=${REDIS_AUTH:-myredissecret}
|
||||
restart: always
|
||||
|
||||
worker:
|
||||
build:
|
||||
dockerfile: ./Dockerfile
|
||||
context: ./worker
|
||||
depends_on:
|
||||
- db
|
||||
- redis
|
||||
- server
|
||||
ports:
|
||||
- "3030:3030"
|
||||
environment:
|
||||
- DATABASE_URL=postgresql://postgres:postgres@db:5432/postgres
|
||||
- NEXTAUTH_SECRET=mysecret
|
||||
- TELEMETRY_ENABLED=${TELEMETRY_ENABLED:-true}
|
||||
- LANGFUSE_ENABLE_EXPERIMENTAL_FEATURES=${LANGFUSE_ENABLE_EXPERIMENTAL_FEATURES:-false}
|
||||
- PORT=${PORT:-3030}
|
||||
- REDIS_URL=${REDIS_URL:-redis}
|
||||
- REDIS_PORT=${REDIS_PORT:-6379}
|
||||
- REDIS_AUTH=${REDIS_AUTH:-myredissecret}
|
||||
restart: always
|
||||
|
||||
redis:
|
||||
image: redis
|
||||
command: >
|
||||
--requirepass ${REDIS_AUTH:-myredissecret}
|
||||
restart: always
|
||||
ports:
|
||||
- 6379:6379
|
||||
|
||||
db:
|
||||
image: postgres
|
||||
restart: always
|
||||
|
||||
@@ -1,6 +1,12 @@
|
||||
version: "3.5"
|
||||
|
||||
services:
|
||||
redis:
|
||||
image: redis
|
||||
restart: always
|
||||
ports:
|
||||
- 6379:6379
|
||||
|
||||
db:
|
||||
image: postgres
|
||||
restart: always
|
||||
|
||||
@@ -8,7 +8,6 @@ services:
|
||||
ports:
|
||||
- "3000:3000"
|
||||
environment:
|
||||
- NODE_ENV=production
|
||||
- DATABASE_URL=postgresql://postgres:postgres@db:5432/postgres
|
||||
- NEXTAUTH_SECRET=mysecret
|
||||
- SALT=mysalt
|
||||
|
||||
@@ -15,7 +15,7 @@ groups:
|
||||
version: 0.0.26
|
||||
output:
|
||||
location: local-file-system
|
||||
path: ../../../generated/openapi-client
|
||||
path: ../../../web/generated/openapi-client
|
||||
config:
|
||||
namespaceExport: Langfuse
|
||||
allowCustomFetcher: true
|
||||
|
||||
@@ -18,18 +18,33 @@ types:
|
||||
public:
|
||||
type: optional<boolean>
|
||||
docs: Public traces are accessible via url without login
|
||||
TraceWithDetails:
|
||||
TraceWithDetails: # GET /traces
|
||||
extends: Trace
|
||||
properties:
|
||||
htmlPath:
|
||||
type: string
|
||||
docs: Path of trace in Langfuse UI
|
||||
latency:
|
||||
type: double
|
||||
docs: Latency of trace in seconds
|
||||
totalCost:
|
||||
type: double
|
||||
docs: Cost of trace in USD
|
||||
observations:
|
||||
type: list<string>
|
||||
docs: List of observation ids
|
||||
scores:
|
||||
type: list<string>
|
||||
docs: List of score ids
|
||||
TraceWithFullDetails:
|
||||
TraceWithFullDetails: # GET traces/[traceID]
|
||||
extends: Trace
|
||||
properties:
|
||||
htmlPath:
|
||||
type: string
|
||||
docs: Path of trace in Langfuse UI
|
||||
totalCost:
|
||||
type: double
|
||||
docs: Cost of trace in USD
|
||||
observations: list<ObservationsView>
|
||||
scores: list<Score>
|
||||
Session:
|
||||
@@ -72,22 +87,38 @@ types:
|
||||
calculatedInputCost: optional<double>
|
||||
calculatedOutputCost: optional<double>
|
||||
calculatedTotalCost: optional<double>
|
||||
latency: optional<double>
|
||||
|
||||
Usage:
|
||||
docs: Standard interface for usage and cost
|
||||
properties:
|
||||
input: optional<integer>
|
||||
output: optional<integer>
|
||||
total: optional<integer>
|
||||
input:
|
||||
docs: Number of input units (e.g. tokens)
|
||||
type: optional<integer>
|
||||
output:
|
||||
docs: Number of output units (e.g. tokens)
|
||||
type: optional<integer>
|
||||
total:
|
||||
docs: Defaults to input+output if not set
|
||||
type: optional<integer>
|
||||
unit: optional<ModelUsageUnit>
|
||||
inputCost: optional<double>
|
||||
outputCost: optional<double>
|
||||
totalCost: optional<double>
|
||||
inputCost:
|
||||
docs: USD input cost
|
||||
type: optional<double>
|
||||
outputCost:
|
||||
docs: USD output cost
|
||||
type: optional<double>
|
||||
totalCost:
|
||||
docs: USD total cost, defaults to input+output
|
||||
type: optional<double>
|
||||
|
||||
Score:
|
||||
properties:
|
||||
id: string
|
||||
traceId: string
|
||||
name: string
|
||||
value: double
|
||||
source: ScoreSource
|
||||
observationId: optional<string>
|
||||
timestamp: datetime
|
||||
comment: optional<string>
|
||||
@@ -129,6 +160,7 @@ types:
|
||||
|
||||
# Utilities
|
||||
ModelUsageUnit:
|
||||
docs: Unit of usage in Langfuse
|
||||
enum:
|
||||
- CHARACTERS
|
||||
- TOKENS
|
||||
@@ -152,6 +184,10 @@ types:
|
||||
enum:
|
||||
- ACTIVE
|
||||
- ARCHIVED
|
||||
ScoreSource:
|
||||
enum:
|
||||
- API
|
||||
- REVIEW
|
||||
|
||||
errors:
|
||||
Error:
|
||||
|
||||
@@ -7,12 +7,12 @@ service:
|
||||
endpoints:
|
||||
create:
|
||||
method: POST
|
||||
docs: Create a dataset item, upserts on id
|
||||
docs: Create a dataset item
|
||||
path: /dataset-items
|
||||
request: CreateDatasetItemRequest
|
||||
response: commons.DatasetItem
|
||||
get:
|
||||
docs: Get a specific dataset item
|
||||
docs: Get a dataset item
|
||||
method: GET
|
||||
path: /dataset-items/{id}
|
||||
path-parameters:
|
||||
@@ -26,4 +26,6 @@ types:
|
||||
datasetName: string
|
||||
input: unknown
|
||||
expectedOutput: optional<unknown>
|
||||
id: optional<string>
|
||||
id:
|
||||
type: optional<string>
|
||||
docs: Dataset items are upserted on their id
|
||||
|
||||
@@ -7,7 +7,7 @@ service:
|
||||
base-path: /api/public
|
||||
endpoints:
|
||||
batch:
|
||||
docs: Ingest multiple events to Langfuse
|
||||
docs: Batched ingestion for Langfuse Tracing
|
||||
method: POST
|
||||
path: /ingestion
|
||||
request:
|
||||
@@ -31,8 +31,12 @@ types:
|
||||
sdk-log: SDKLogEvent
|
||||
|
||||
# both are legacy
|
||||
observation-create: CreateObservationEvent
|
||||
observation-update: UpdateObservationEvent
|
||||
observation-create:
|
||||
type: CreateObservationEvent
|
||||
docs: Deprecated event type
|
||||
observation-update:
|
||||
type: UpdateObservationEvent
|
||||
docs: Deprecated event type
|
||||
|
||||
ObservationType:
|
||||
enum:
|
||||
@@ -47,6 +51,7 @@ types:
|
||||
- OpenAIUsage
|
||||
|
||||
OpenAIUsage:
|
||||
docs: Usage interface of OpenAI for improved compatibility.
|
||||
properties:
|
||||
promptTokens: optional<integer>
|
||||
completionTokens: optional<integer>
|
||||
@@ -128,6 +133,7 @@ types:
|
||||
TraceBody:
|
||||
properties:
|
||||
id: optional<string>
|
||||
timestamp: optional<datetime>
|
||||
name: optional<string>
|
||||
userId: optional<string>
|
||||
input: optional<unknown>
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
# yaml-language-server: $schema=https://raw.githubusercontent.com/fern-api/fern/main/fern.schema.json
|
||||
imports:
|
||||
pagination: ./utils/pagination.yml
|
||||
commons: ./commons.yml
|
||||
service:
|
||||
auth: true
|
||||
base-path: /api/public
|
||||
endpoints:
|
||||
daily:
|
||||
docs: Get daily metrics of the Langfuse project
|
||||
method: GET
|
||||
path: /metrics/daily
|
||||
request:
|
||||
name: GetDailyMetricsRequest
|
||||
query-parameters:
|
||||
page: optional<integer>
|
||||
limit: optional<integer>
|
||||
traceName:
|
||||
type: optional<string>
|
||||
docs: Optional filter by the name of the trace
|
||||
userId:
|
||||
type: optional<string>
|
||||
docs: Optional filter by the userId associated with the trace
|
||||
tags:
|
||||
type: optional<string>
|
||||
allow-multiple: true
|
||||
docs: Optional filter for metrics where traces include all of these tags
|
||||
response: DailyMetrics
|
||||
types:
|
||||
DailyMetrics:
|
||||
properties:
|
||||
data:
|
||||
type: list<DailyMetricsDetails>
|
||||
docs: A list of daily metrics, only days with ingested data are included.
|
||||
meta: pagination.MetaResponse
|
||||
DailyMetricsDetails:
|
||||
properties:
|
||||
date: date
|
||||
countTraces: integer
|
||||
totalCost: double
|
||||
usage: list<UsageByModel>
|
||||
UsageByModel:
|
||||
docs: Daily usage of a given model. Usage corresponds to the unit set for the specific model (e.g. tokens).
|
||||
properties:
|
||||
model: string
|
||||
inputUsage: integer
|
||||
outputUsage: integer
|
||||
totalUsage: integer
|
||||
@@ -7,7 +7,7 @@ service:
|
||||
base-path: /api/public
|
||||
endpoints:
|
||||
get:
|
||||
docs: Get a specific observation
|
||||
docs: Get a observation
|
||||
method: GET
|
||||
path: /observations/{observationId}
|
||||
path-parameters:
|
||||
|
||||
@@ -7,6 +7,7 @@ service:
|
||||
base-path: /api/public
|
||||
endpoints:
|
||||
get:
|
||||
docs: Get Project associated with API key
|
||||
method: GET
|
||||
path: /projects
|
||||
response: Projects
|
||||
|
||||
@@ -7,7 +7,7 @@ service:
|
||||
base-path: /api/public
|
||||
endpoints:
|
||||
get:
|
||||
docs: Get a specific prompt
|
||||
docs: Get a prompt
|
||||
method: GET
|
||||
path: /prompts
|
||||
request:
|
||||
@@ -17,7 +17,7 @@ service:
|
||||
version: optional<integer>
|
||||
response: Prompt
|
||||
create:
|
||||
docs: Create a specific prompt
|
||||
docs: Create a prompt
|
||||
method: POST
|
||||
path: /prompts
|
||||
request: CreatePromptRequest
|
||||
@@ -27,10 +27,15 @@ types:
|
||||
CreatePromptRequest:
|
||||
properties:
|
||||
name: string
|
||||
isActive: boolean
|
||||
isActive:
|
||||
docs: Should the prompt be promoted to production immediately?
|
||||
type: boolean
|
||||
prompt: string
|
||||
config: optional<unknown>
|
||||
|
||||
Prompt:
|
||||
properties:
|
||||
name: string
|
||||
version: integer
|
||||
prompt: string
|
||||
config: unknown
|
||||
|
||||
@@ -7,13 +7,13 @@ service:
|
||||
base-path: /api/public
|
||||
endpoints:
|
||||
create:
|
||||
docs: Add a score to the database, upserts on id
|
||||
docs: Create a score
|
||||
method: POST
|
||||
path: /scores
|
||||
request: CreateScoreRequest
|
||||
response: commons.Score
|
||||
get:
|
||||
docs: Get scores
|
||||
docs: Get a list of scores
|
||||
method: GET
|
||||
path: /scores
|
||||
request:
|
||||
@@ -24,6 +24,23 @@ service:
|
||||
userId: optional<string>
|
||||
name: optional<string>
|
||||
response: Scores
|
||||
get-by-id:
|
||||
docs: Get a score
|
||||
method: GET
|
||||
path: /scores/{scoreId}
|
||||
path-parameters:
|
||||
scoreId:
|
||||
type: string
|
||||
docs: The unique langfuse identifier of a score
|
||||
response: commons.Score
|
||||
delete:
|
||||
docs: Delete a score
|
||||
method: DELETE
|
||||
path: /scores/{scoreId}
|
||||
path-parameters:
|
||||
scoreId:
|
||||
type: string
|
||||
docs: The unique langfuse identifier of a score
|
||||
types:
|
||||
CreateScoreRequest:
|
||||
properties:
|
||||
|
||||
@@ -27,8 +27,8 @@ service:
|
||||
userId: optional<string>
|
||||
name: optional<string>
|
||||
orderBy:
|
||||
type: string
|
||||
docs: Format of the string sort_by=timestamp.asc (id, timestamp, name, userId, release, version, public, bookmarked, sessionId)
|
||||
type: optional<string>
|
||||
docs: "Format of the string [field].[asc/desc]. Fields: id, timestamp, name, userId, release, version, public, bookmarked, sessionId. Example: timestamp.asc"
|
||||
tags:
|
||||
type: optional<string>
|
||||
allow-multiple: true
|
||||
|
||||
@@ -6,7 +6,7 @@ groups:
|
||||
version: 0.0.28
|
||||
output:
|
||||
location: local-file-system
|
||||
path: ../../../generated/openapi-server
|
||||
path: ../../../web/generated/openapi-server
|
||||
config:
|
||||
namespaceExport: Langfuse
|
||||
allowCustomFetcher: true
|
||||
|
||||
@@ -387,6 +387,81 @@ paths:
|
||||
$ref: '#/components/schemas/IngestionEvent'
|
||||
required:
|
||||
- batch
|
||||
/api/public/metrics/daily:
|
||||
get:
|
||||
description: Get daily metrics of the Langfuse project
|
||||
operationId: metrics_daily
|
||||
tags:
|
||||
- Metrics
|
||||
parameters:
|
||||
- name: page
|
||||
in: query
|
||||
required: false
|
||||
schema:
|
||||
type: integer
|
||||
nullable: true
|
||||
- name: limit
|
||||
in: query
|
||||
required: false
|
||||
schema:
|
||||
type: integer
|
||||
nullable: true
|
||||
- name: traceName
|
||||
in: query
|
||||
description: Optional filter by the name of the trace
|
||||
required: false
|
||||
schema:
|
||||
type: string
|
||||
nullable: true
|
||||
- name: userId
|
||||
in: query
|
||||
description: Optional filter by the userId associated with the trace
|
||||
required: false
|
||||
schema:
|
||||
type: string
|
||||
nullable: true
|
||||
- name: tags
|
||||
in: query
|
||||
description: Optional filter for metrics where traces include all of these tags
|
||||
required: false
|
||||
schema:
|
||||
type: array
|
||||
items:
|
||||
type: string
|
||||
nullable: true
|
||||
responses:
|
||||
'200':
|
||||
description: ''
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/DailyMetrics'
|
||||
'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/observations/{observationId}:
|
||||
get:
|
||||
description: Get a specific observation
|
||||
@@ -761,6 +836,48 @@ paths:
|
||||
application/json:
|
||||
schema: {}
|
||||
security: *ref_0
|
||||
/api/public/scores/{scoreId}:
|
||||
delete:
|
||||
description: Delete a score
|
||||
operationId: score_delete
|
||||
tags:
|
||||
- Score
|
||||
parameters:
|
||||
- name: scoreId
|
||||
in: path
|
||||
description: The unique langfuse identifier of a score
|
||||
required: true
|
||||
schema:
|
||||
type: string
|
||||
responses:
|
||||
'204':
|
||||
description: ''
|
||||
'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/sessions/{sessionId}:
|
||||
get:
|
||||
description: Get a session
|
||||
@@ -983,6 +1100,17 @@ components:
|
||||
title: TraceWithDetails
|
||||
type: object
|
||||
properties:
|
||||
htmlPath:
|
||||
type: string
|
||||
description: Path of trace in Langfuse UI
|
||||
latency:
|
||||
type: number
|
||||
format: double
|
||||
description: Latency of trace in seconds
|
||||
totalCost:
|
||||
type: number
|
||||
format: double
|
||||
description: Cost of trace in USD
|
||||
observations:
|
||||
type: array
|
||||
items:
|
||||
@@ -994,6 +1122,9 @@ components:
|
||||
type: string
|
||||
description: List of score ids
|
||||
required:
|
||||
- htmlPath
|
||||
- latency
|
||||
- totalCost
|
||||
- observations
|
||||
- scores
|
||||
allOf:
|
||||
@@ -1002,6 +1133,13 @@ components:
|
||||
title: TraceWithFullDetails
|
||||
type: object
|
||||
properties:
|
||||
htmlPath:
|
||||
type: string
|
||||
description: Path of trace in Langfuse UI
|
||||
totalCost:
|
||||
type: number
|
||||
format: double
|
||||
description: Cost of trace in USD
|
||||
observations:
|
||||
type: array
|
||||
items:
|
||||
@@ -1011,6 +1149,8 @@ components:
|
||||
items:
|
||||
$ref: '#/components/schemas/Score'
|
||||
required:
|
||||
- htmlPath
|
||||
- totalCost
|
||||
- observations
|
||||
- scores
|
||||
allOf:
|
||||
@@ -1134,6 +1274,10 @@ components:
|
||||
type: number
|
||||
format: double
|
||||
nullable: true
|
||||
latency:
|
||||
type: number
|
||||
format: double
|
||||
nullable: true
|
||||
allOf:
|
||||
- $ref: '#/components/schemas/Observation'
|
||||
Usage:
|
||||
@@ -1720,6 +1864,10 @@ components:
|
||||
id:
|
||||
type: string
|
||||
nullable: true
|
||||
timestamp:
|
||||
type: string
|
||||
format: date-time
|
||||
nullable: true
|
||||
name:
|
||||
type: string
|
||||
nullable: true
|
||||
@@ -1936,6 +2084,60 @@ components:
|
||||
required:
|
||||
- successes
|
||||
- errors
|
||||
DailyMetrics:
|
||||
title: DailyMetrics
|
||||
type: object
|
||||
properties:
|
||||
data:
|
||||
type: array
|
||||
items:
|
||||
$ref: '#/components/schemas/DailyMetricsDetails'
|
||||
description: A list of daily metrics, only days with ingested data are included.
|
||||
meta:
|
||||
$ref: '#/components/schemas/utilsMetaResponse'
|
||||
required:
|
||||
- data
|
||||
- meta
|
||||
DailyMetricsDetails:
|
||||
title: DailyMetricsDetails
|
||||
type: object
|
||||
properties:
|
||||
date:
|
||||
type: string
|
||||
countTraces:
|
||||
type: integer
|
||||
totalCost:
|
||||
type: number
|
||||
format: double
|
||||
usage:
|
||||
type: array
|
||||
items:
|
||||
$ref: '#/components/schemas/UsageByModel'
|
||||
required:
|
||||
- date
|
||||
- countTraces
|
||||
- totalCost
|
||||
- usage
|
||||
UsageByModel:
|
||||
title: UsageByModel
|
||||
type: object
|
||||
description: >-
|
||||
Daily usage of a given model. Usage corresponds to the unit set for the
|
||||
specific model (e.g. tokens).
|
||||
properties:
|
||||
model:
|
||||
type: string
|
||||
inputUsage:
|
||||
type: integer
|
||||
outputUsage:
|
||||
type: integer
|
||||
totalUsage:
|
||||
type: integer
|
||||
required:
|
||||
- model
|
||||
- inputUsage
|
||||
- outputUsage
|
||||
- totalUsage
|
||||
Observations:
|
||||
title: Observations
|
||||
type: object
|
||||
@@ -1993,6 +2195,8 @@ components:
|
||||
type: boolean
|
||||
prompt:
|
||||
type: string
|
||||
config:
|
||||
nullable: true
|
||||
required:
|
||||
- name
|
||||
- isActive
|
||||
@@ -2007,10 +2211,12 @@ components:
|
||||
type: integer
|
||||
prompt:
|
||||
type: string
|
||||
config: {}
|
||||
required:
|
||||
- name
|
||||
- version
|
||||
- prompt
|
||||
- config
|
||||
CreateScoreRequest:
|
||||
title: CreateScoreRequest
|
||||
type: object
|
||||
|
||||
@@ -46,7 +46,7 @@
|
||||
"_type": "endpoint",
|
||||
"name": "Create",
|
||||
"request": {
|
||||
"description": "Create a dataset item, upserts on id",
|
||||
"description": "Create a dataset item",
|
||||
"url": {
|
||||
"raw": "{{baseUrl}}/api/public/dataset-items",
|
||||
"host": [
|
||||
@@ -79,7 +79,7 @@
|
||||
"_type": "endpoint",
|
||||
"name": "Get",
|
||||
"request": {
|
||||
"description": "Get a specific dataset item",
|
||||
"description": "Get a dataset item",
|
||||
"url": {
|
||||
"raw": "{{baseUrl}}/api/public/dataset-items/:id",
|
||||
"host": [
|
||||
@@ -301,7 +301,7 @@
|
||||
"_type": "endpoint",
|
||||
"name": "Batch",
|
||||
"request": {
|
||||
"description": "Ingest multiple events to Langfuse",
|
||||
"description": "Batched ingestion for Langfuse Tracing",
|
||||
"url": {
|
||||
"raw": "{{baseUrl}}/api/public/ingestion",
|
||||
"host": [
|
||||
@@ -320,7 +320,7 @@
|
||||
"auth": null,
|
||||
"body": {
|
||||
"mode": "raw",
|
||||
"raw": "{\n \"batch\": [\n {\n \"type\": \"trace-create\",\n \"body\": {\n \"id\": \"example\",\n \"name\": \"example\",\n \"userId\": \"example\",\n \"input\": \"UNKNOWN\",\n \"output\": \"UNKNOWN\",\n \"sessionId\": \"example\",\n \"release\": \"example\",\n \"version\": \"example\",\n \"metadata\": \"UNKNOWN\",\n \"tags\": [\n \"example\"\n ],\n \"public\": true\n },\n \"id\": \"example\",\n \"timestamp\": \"example\",\n \"metadata\": \"UNKNOWN\"\n }\n ]\n}",
|
||||
"raw": "{\n \"batch\": [\n {\n \"type\": \"trace-create\",\n \"body\": {\n \"id\": \"example\",\n \"timestamp\": \"1994-11-05T13:15:30Z\",\n \"name\": \"example\",\n \"userId\": \"example\",\n \"input\": \"UNKNOWN\",\n \"output\": \"UNKNOWN\",\n \"sessionId\": \"example\",\n \"release\": \"example\",\n \"version\": \"example\",\n \"metadata\": \"UNKNOWN\",\n \"tags\": [\n \"example\"\n ],\n \"public\": true\n },\n \"id\": \"example\",\n \"timestamp\": \"example\",\n \"metadata\": \"UNKNOWN\"\n }\n ]\n}",
|
||||
"options": {
|
||||
"raw": {
|
||||
"language": "json"
|
||||
@@ -332,6 +332,65 @@
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"_type": "container",
|
||||
"description": null,
|
||||
"name": "Metrics",
|
||||
"item": [
|
||||
{
|
||||
"_type": "endpoint",
|
||||
"name": "Daily",
|
||||
"request": {
|
||||
"description": "Get daily metrics of the Langfuse project",
|
||||
"url": {
|
||||
"raw": "{{baseUrl}}/api/public/metrics/daily?page=&limit=&traceName=&userId=&tags=",
|
||||
"host": [
|
||||
"{{baseUrl}}"
|
||||
],
|
||||
"path": [
|
||||
"api",
|
||||
"public",
|
||||
"metrics",
|
||||
"daily"
|
||||
],
|
||||
"query": [
|
||||
{
|
||||
"key": "page",
|
||||
"value": "",
|
||||
"description": null
|
||||
},
|
||||
{
|
||||
"key": "limit",
|
||||
"value": "",
|
||||
"description": null
|
||||
},
|
||||
{
|
||||
"key": "traceName",
|
||||
"value": "",
|
||||
"description": "Optional filter by the name of the trace"
|
||||
},
|
||||
{
|
||||
"key": "userId",
|
||||
"value": "",
|
||||
"description": "Optional filter by the userId associated with the trace"
|
||||
},
|
||||
{
|
||||
"key": "tags",
|
||||
"value": "",
|
||||
"description": "Optional filter for metrics where traces include all of these tags"
|
||||
}
|
||||
],
|
||||
"variable": []
|
||||
},
|
||||
"header": [],
|
||||
"method": "GET",
|
||||
"auth": null,
|
||||
"body": null
|
||||
},
|
||||
"response": []
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"_type": "container",
|
||||
"description": null,
|
||||
@@ -341,7 +400,7 @@
|
||||
"_type": "endpoint",
|
||||
"name": "Get",
|
||||
"request": {
|
||||
"description": "Get a specific observation",
|
||||
"description": "Get a observation",
|
||||
"url": {
|
||||
"raw": "{{baseUrl}}/api/public/observations/:observationId",
|
||||
"host": [
|
||||
@@ -441,7 +500,7 @@
|
||||
"_type": "endpoint",
|
||||
"name": "Get",
|
||||
"request": {
|
||||
"description": null,
|
||||
"description": "Get Project associated with API key",
|
||||
"url": {
|
||||
"raw": "{{baseUrl}}/api/public/projects",
|
||||
"host": [
|
||||
@@ -473,7 +532,7 @@
|
||||
"_type": "endpoint",
|
||||
"name": "Get",
|
||||
"request": {
|
||||
"description": "Get a specific prompt",
|
||||
"description": "Get a prompt",
|
||||
"url": {
|
||||
"raw": "{{baseUrl}}/api/public/prompts?name=&version=",
|
||||
"host": [
|
||||
@@ -509,7 +568,7 @@
|
||||
"_type": "endpoint",
|
||||
"name": "Create",
|
||||
"request": {
|
||||
"description": "Create a specific prompt",
|
||||
"description": "Create a prompt",
|
||||
"url": {
|
||||
"raw": "{{baseUrl}}/api/public/prompts",
|
||||
"host": [
|
||||
@@ -528,7 +587,7 @@
|
||||
"auth": null,
|
||||
"body": {
|
||||
"mode": "raw",
|
||||
"raw": "{\n \"name\": \"example\",\n \"isActive\": true,\n \"prompt\": \"example\"\n}",
|
||||
"raw": "{\n \"name\": \"example\",\n \"isActive\": true,\n \"prompt\": \"example\",\n \"config\": \"UNKNOWN\"\n}",
|
||||
"options": {
|
||||
"raw": {
|
||||
"language": "json"
|
||||
@@ -549,7 +608,7 @@
|
||||
"_type": "endpoint",
|
||||
"name": "Create",
|
||||
"request": {
|
||||
"description": "Add a score to the database, upserts on id",
|
||||
"description": "Create a score",
|
||||
"url": {
|
||||
"raw": "{{baseUrl}}/api/public/scores",
|
||||
"host": [
|
||||
@@ -582,7 +641,7 @@
|
||||
"_type": "endpoint",
|
||||
"name": "Get",
|
||||
"request": {
|
||||
"description": "Get scores",
|
||||
"description": "Get a list of scores",
|
||||
"url": {
|
||||
"raw": "{{baseUrl}}/api/public/scores?page=&limit=&userId=&name=",
|
||||
"host": [
|
||||
@@ -623,6 +682,70 @@
|
||||
"body": null
|
||||
},
|
||||
"response": []
|
||||
},
|
||||
{
|
||||
"_type": "endpoint",
|
||||
"name": "Get By Id",
|
||||
"request": {
|
||||
"description": "Get a score",
|
||||
"url": {
|
||||
"raw": "{{baseUrl}}/api/public/scores/:scoreId",
|
||||
"host": [
|
||||
"{{baseUrl}}"
|
||||
],
|
||||
"path": [
|
||||
"api",
|
||||
"public",
|
||||
"scores",
|
||||
":scoreId"
|
||||
],
|
||||
"query": [],
|
||||
"variable": [
|
||||
{
|
||||
"key": "scoreId",
|
||||
"value": "",
|
||||
"description": "The unique langfuse identifier of a score"
|
||||
}
|
||||
]
|
||||
},
|
||||
"header": [],
|
||||
"method": "GET",
|
||||
"auth": null,
|
||||
"body": null
|
||||
},
|
||||
"response": []
|
||||
},
|
||||
{
|
||||
"_type": "endpoint",
|
||||
"name": "Delete",
|
||||
"request": {
|
||||
"description": "Delete a score",
|
||||
"url": {
|
||||
"raw": "{{baseUrl}}/api/public/scores/:scoreId",
|
||||
"host": [
|
||||
"{{baseUrl}}"
|
||||
],
|
||||
"path": [
|
||||
"api",
|
||||
"public",
|
||||
"scores",
|
||||
":scoreId"
|
||||
],
|
||||
"query": [],
|
||||
"variable": [
|
||||
{
|
||||
"key": "scoreId",
|
||||
"value": "",
|
||||
"description": "The unique langfuse identifier of a score"
|
||||
}
|
||||
]
|
||||
},
|
||||
"header": [],
|
||||
"method": "DELETE",
|
||||
"auth": null,
|
||||
"body": null
|
||||
},
|
||||
"response": []
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -741,7 +864,7 @@
|
||||
{
|
||||
"key": "orderBy",
|
||||
"value": "",
|
||||
"description": "Format of the string sort_by=timestamp.asc (id, timestamp, name, userId, release, version, public, bookmarked, sessionId)"
|
||||
"description": "Format of the string [field].[asc/desc]. Fields: id, timestamp, name, userId, release, version, public, bookmarked, sessionId. Example: timestamp.asc"
|
||||
},
|
||||
{
|
||||
"key": "tags",
|
||||
|
||||
@@ -1,176 +1,23 @@
|
||||
{
|
||||
"name": "langfuse-core",
|
||||
"version": "2.4.0",
|
||||
"private": true,
|
||||
"name": "langfuse",
|
||||
"version": "2.7.0",
|
||||
"scripts": {
|
||||
"prebuild": "cp generated/openapi-client/openapi.yml public/openapi-client.yml && cp generated/openapi-server/openapi.yml public/openapi-server.yml",
|
||||
"build": "next build",
|
||||
"dev": "next dev",
|
||||
"dx": "npm i && npm run db:reset && npm run db:seed:examples && npm run dev",
|
||||
"postinstall": "prisma generate",
|
||||
"lint": "next lint",
|
||||
"lint:fix": "next lint --fix",
|
||||
"prettier": "prettier --write ./src *.{ts,js}",
|
||||
"clean": "rm -rf node_modules",
|
||||
"start": "next start",
|
||||
"test": "jest --runInBand",
|
||||
"test:watch": "jest --watch --runInBand",
|
||||
"test:e2e": "playwright test",
|
||||
"infra:dev:up": "docker-compose -f docker-compose.dev.yml up -d",
|
||||
"infra:dev:down": "docker-compose -f docker-compose.dev.yml down",
|
||||
"db:migrate": "DISABLE_ERD=false npx prisma migrate dev",
|
||||
"db:reset": "npx prisma migrate reset",
|
||||
"db:seed": "npx prisma db seed",
|
||||
"db:seed:examples": "npx prisma db seed -- --environment examples",
|
||||
"release": "release-it",
|
||||
"models:migrate": "tsx scripts/model-match.ts"
|
||||
},
|
||||
"prisma": {
|
||||
"seed": "ts-node -r tsconfig-paths/register --compiler-options {\"module\":\"CommonJS\"} prisma/seed.ts"
|
||||
},
|
||||
"dependencies": {
|
||||
"@anthropic-ai/tokenizer": "^0.0.4",
|
||||
"@aws-sdk/client-s3": "^3.507.0",
|
||||
"@aws-sdk/s3-request-presigner": "^3.507.0",
|
||||
"@headlessui/react": "^1.7.18",
|
||||
"@heroicons/react": "^2.1.1",
|
||||
"@hookform/resolvers": "^3.3.4",
|
||||
"@next-auth/prisma-adapter": "^1.0.7",
|
||||
"@prisma/client": "^5.9.1",
|
||||
"@radix-ui/react-accordion": "^1.1.2",
|
||||
"@radix-ui/react-alert-dialog": "^1.0.5",
|
||||
"@radix-ui/react-avatar": "^1.0.4",
|
||||
"@radix-ui/react-checkbox": "^1.0.4",
|
||||
"@radix-ui/react-collapsible": "^1.0.3",
|
||||
"@radix-ui/react-dialog": "^1.0.5",
|
||||
"@radix-ui/react-dropdown-menu": "^2.0.6",
|
||||
"@radix-ui/react-hover-card": "^1.0.7",
|
||||
"@radix-ui/react-icons": "^1.3.0",
|
||||
"@radix-ui/react-label": "^2.0.2",
|
||||
"@radix-ui/react-popover": "^1.0.7",
|
||||
"@radix-ui/react-scroll-area": "^1.0.5",
|
||||
"@radix-ui/react-select": "^2.0.0",
|
||||
"@radix-ui/react-separator": "^1.0.3",
|
||||
"@radix-ui/react-slider": "^1.1.2",
|
||||
"@radix-ui/react-slot": "^1.0.2",
|
||||
"@radix-ui/react-switch": "^1.0.3",
|
||||
"@radix-ui/react-tabs": "^1.0.4",
|
||||
"@radix-ui/react-toggle": "^1.0.3",
|
||||
"@radix-ui/react-tooltip": "^1.0.7",
|
||||
"@react-email/components": "^0.0.14",
|
||||
"@react-email/render": "^0.0.12",
|
||||
"@sentry/nextjs": "^7.100.1",
|
||||
"@sentry/profiling-node": "^7.100.1",
|
||||
"@sentry/types": "^7.88.0",
|
||||
"@t3-oss/env-nextjs": "^0.8.0",
|
||||
"@tailwindcss/forms": "^0.5.7",
|
||||
"@tanstack/react-query": "^4.36.1",
|
||||
"@tanstack/react-table": "^8.11.8",
|
||||
"@tremor/react": "^3.11.1",
|
||||
"@trpc/client": "^10.45.0",
|
||||
"@trpc/next": "^10.45.0",
|
||||
"@trpc/react-query": "^10.45.0",
|
||||
"@trpc/server": "^10.45.0",
|
||||
"@vercel/edge-config": "^0.4.1",
|
||||
"bcryptjs": "^2.4.3",
|
||||
"class-variance-authority": "^0.7.0",
|
||||
"clsx": "^2.1.0",
|
||||
"cmdk": "^0.2.1",
|
||||
"core-js": "^3.35.1",
|
||||
"cors": "^2.8.5",
|
||||
"date-fns": "^3.3.1",
|
||||
"decimal.js": "^10.4.3",
|
||||
"exponential-backoff": "^3.1.1",
|
||||
"js-tiktoken": "^1.0.10",
|
||||
"lodash": "^4.17.21",
|
||||
"lucide-react": "^0.323.0",
|
||||
"next": "^14.1.0",
|
||||
"next-auth": "^4.24.5",
|
||||
"next-query-params": "^5.0.0",
|
||||
"nodemailer": "^6.9.9",
|
||||
"posthog-js": "^1.105.7",
|
||||
"posthog-node": "^3.6.2",
|
||||
"react": "18.2.0",
|
||||
"react-day-picker": "^8.10.0",
|
||||
"react-dom": "18.2.0",
|
||||
"react-hook-form": "^7.50.1",
|
||||
"react-icons": "^5.0.1",
|
||||
"react-responsive": "^9.0.2",
|
||||
"react18-json-view": "^0.2.7",
|
||||
"sonner": "^1.4.0",
|
||||
"superjson": "2.2.1",
|
||||
"tailwind-merge": "^2.2.1",
|
||||
"tailwindcss-animate": "^1.0.7",
|
||||
"use-query-params": "^2.2.1",
|
||||
"uuid": "^9.0.1",
|
||||
"zod": "^3.22.4"
|
||||
"infra:dev:up": "docker-compose -f ./docker-compose.dev.yml up -d",
|
||||
"infra:dev:down": "docker-compose -f ./docker-compose.dev.yml down",
|
||||
"dev": "npm run dev --prefix worker/ & npm run dev --prefix web/",
|
||||
"postinstall": "cd worker && npm install && cd ../web && npm install",
|
||||
"nuke": "bash ./scripts/nuke.sh",
|
||||
"dx": "dotenv -e .env -- npm i && npm run infra:dev:up && cd web && npm run db:reset && npm run db:seed:examples && cd .. && npm run dev"
|
||||
},
|
||||
"author": "",
|
||||
"license": "MIT",
|
||||
"devDependencies": {
|
||||
"@jedmao/location": "^3.0.0",
|
||||
"@mermaid-js/mermaid-cli": "^10.7.0",
|
||||
"@playwright/test": "^1.41.2",
|
||||
"@release-it/bumper": "^6.0.1",
|
||||
"@testing-library/jest-dom": "^6.4.2",
|
||||
"@testing-library/react": "^14.2.1",
|
||||
"@types/bcryptjs": "^2.4.6",
|
||||
"@types/cors": "^2.8.17",
|
||||
"@types/eslint": "^8.56.2",
|
||||
"@types/jest": "^29.5.12",
|
||||
"@types/lodash": "^4.14.202",
|
||||
"@types/node": "20.10.5",
|
||||
"@types/nodemailer": "^6.4.14",
|
||||
"@types/react": "^18.2.55",
|
||||
"@types/react-dom": "^18.2.19",
|
||||
"@types/uuid": "^9.0.8",
|
||||
"@typescript-eslint/eslint-plugin": "^6.21.0",
|
||||
"@typescript-eslint/parser": "^6.21.0",
|
||||
"autoprefixer": "^10.4.17",
|
||||
"@typescript-eslint/eslint-plugin": "^7.0.2",
|
||||
"@typescript-eslint/parser": "^7.0.2",
|
||||
"dotenv-cli": "^7.3.0",
|
||||
"eslint": "^8.56.0",
|
||||
"eslint-config-next": "^14.1.0",
|
||||
"jest": "^29.7.0",
|
||||
"jest-environment-jsdom": "^29.7.0",
|
||||
"postcss": "^8.4.35",
|
||||
"prettier": "^3.2.5",
|
||||
"prettier-plugin-tailwindcss": "^0.5.11",
|
||||
"prisma": "^5.9.1",
|
||||
"prisma-erd-generator": "^1.11.2",
|
||||
"release-it": "^17.0.3",
|
||||
"tailwindcss": "^3.4.1",
|
||||
"ts-node": "^10.9.2",
|
||||
"tsconfig-paths": "^4.2.0",
|
||||
"tsx": "^4.7.1",
|
||||
"typescript": "^5.3.3"
|
||||
},
|
||||
"ct3aMetadata": {
|
||||
"initVersion": "7.13.0"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"crisp-sdk-web": "^1.0.21"
|
||||
},
|
||||
"release-it": {
|
||||
"git": {
|
||||
"commitMessage": "chore: release v${version}",
|
||||
"tagName": "v${version}"
|
||||
},
|
||||
"github": {
|
||||
"release": true,
|
||||
"web": true,
|
||||
"autoGenerate": true,
|
||||
"releaseName": "v${version}",
|
||||
"comments": {
|
||||
"submit": true,
|
||||
"issue": ":rocket: _This issue has been resolved in v${version}. See [${releaseName}](${releaseUrl}) for release notes._",
|
||||
"pr": ":rocket: _This pull request is included in v${version}. See [${releaseName}](${releaseUrl}) for release notes._"
|
||||
}
|
||||
},
|
||||
"plugins": {
|
||||
"@release-it/bumper": {
|
||||
"out": {
|
||||
"file": "./src/constants/VERSION.ts",
|
||||
"type": "application/typescript"
|
||||
}
|
||||
}
|
||||
}
|
||||
"prettier": "^3.2.5"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,603 +0,0 @@
|
||||
import { PrismaClient } from "@prisma/client";
|
||||
import {
|
||||
hashSecretKey,
|
||||
getDisplaySecretKey,
|
||||
} from "@/src/features/public-api/lib/apiKeys";
|
||||
import { hash } from "bcryptjs";
|
||||
import { parseArgs } from "node:util";
|
||||
import { ModelUsageUnit } from "@/src/constants";
|
||||
|
||||
const options = {
|
||||
environment: { type: "string" },
|
||||
} as const;
|
||||
|
||||
const prisma = new PrismaClient();
|
||||
|
||||
const TRACE_VOLUME = 100;
|
||||
|
||||
async function main() {
|
||||
const environment = parseArgs({
|
||||
options,
|
||||
}).values.environment;
|
||||
|
||||
const user = await prisma.user.upsert({
|
||||
where: { id: "user-1" },
|
||||
update: {
|
||||
name: "Demo User",
|
||||
email: "demo@langfuse.com",
|
||||
password: await hash("password", 12),
|
||||
},
|
||||
create: {
|
||||
id: "user-1",
|
||||
name: "Demo User",
|
||||
email: "demo@langfuse.com",
|
||||
password: await hash("password", 12),
|
||||
},
|
||||
});
|
||||
|
||||
const seedProjectId = "7a88fb47-b4e2-43b8-a06c-a5ce950dc53a";
|
||||
|
||||
const project1 = await prisma.project.upsert({
|
||||
where: { id: seedProjectId },
|
||||
update: {
|
||||
name: "llm-app",
|
||||
},
|
||||
create: {
|
||||
id: seedProjectId,
|
||||
name: "llm-app",
|
||||
members: {
|
||||
create: {
|
||||
role: "OWNER",
|
||||
userId: user.id,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const prompt = await prisma.prompt.upsert({
|
||||
where: {
|
||||
projectId_name_version: {
|
||||
projectId: seedProjectId,
|
||||
name: "summary-prompt",
|
||||
version: 1,
|
||||
},
|
||||
},
|
||||
create: {
|
||||
name: "summary-prompt",
|
||||
project: { connect: { id: seedProjectId } },
|
||||
prompt: "prompt {{variable}} {{anotherVariable}}",
|
||||
isActive: true,
|
||||
version: 1,
|
||||
createdBy: "user-1",
|
||||
},
|
||||
update: {},
|
||||
});
|
||||
|
||||
const seedApiKey = {
|
||||
id: "seed-api-key",
|
||||
secret: process.env.SEED_SECRET_KEY ?? "sk-lf-1234567890",
|
||||
public: "pk-lf-1234567890",
|
||||
note: "seeded key",
|
||||
};
|
||||
|
||||
if (!(await prisma.apiKey.findUnique({ where: { id: seedApiKey.id } }))) {
|
||||
await prisma.apiKey.create({
|
||||
data: {
|
||||
note: seedApiKey.note,
|
||||
id: seedApiKey.id,
|
||||
publicKey: seedApiKey.public,
|
||||
hashedSecretKey: await hashSecretKey(seedApiKey.secret),
|
||||
displaySecretKey: getDisplaySecretKey(seedApiKey.secret),
|
||||
project: {
|
||||
connect: {
|
||||
id: project1.id,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
// Do not run the following for local docker compose setup
|
||||
if (environment === "examples") {
|
||||
const project2 = await prisma.project.upsert({
|
||||
where: { id: "239ad00f-562f-411d-af14-831c75ddd875" },
|
||||
create: {
|
||||
name: "demo-app",
|
||||
apiKeys: {
|
||||
create: [
|
||||
{
|
||||
note: "seeded key",
|
||||
hashedSecretKey: await hashSecretKey("sk-lf-asdfghjkl"),
|
||||
displaySecretKey: getDisplaySecretKey("sk-lf-asdfghjkl"),
|
||||
publicKey: "pk-lf-asdfghjkl",
|
||||
},
|
||||
],
|
||||
},
|
||||
members: {
|
||||
create: {
|
||||
role: "OWNER",
|
||||
userId: user.id,
|
||||
},
|
||||
},
|
||||
},
|
||||
update: {},
|
||||
});
|
||||
|
||||
const promptIds: string[] = [];
|
||||
|
||||
const prompts = [
|
||||
{
|
||||
id: `prompt-${Math.floor(Math.random() * 1000000000)}`,
|
||||
projectId: project2.id,
|
||||
createdBy: "user-1",
|
||||
prompt: "Prompt 1 content",
|
||||
name: "Prompt 1",
|
||||
version: 1,
|
||||
isActive: true,
|
||||
},
|
||||
{
|
||||
id: `prompt-${Math.floor(Math.random() * 1000000000)}`,
|
||||
projectId: project2.id,
|
||||
createdBy: "user-1",
|
||||
prompt: "Prompt 2 content",
|
||||
name: "Prompt 2",
|
||||
version: 1,
|
||||
isActive: true,
|
||||
},
|
||||
{
|
||||
id: `prompt-${Math.floor(Math.random() * 1000000000)}`,
|
||||
projectId: project2.id,
|
||||
createdBy: "API",
|
||||
prompt: "Prompt 3 content",
|
||||
name: "Prompt 3 by API",
|
||||
version: 1,
|
||||
isActive: true,
|
||||
},
|
||||
];
|
||||
|
||||
for (const prompt of prompts) {
|
||||
await prisma.prompt.create({
|
||||
data: {
|
||||
id: prompt.id,
|
||||
projectId: prompt.projectId,
|
||||
createdBy: prompt.createdBy,
|
||||
prompt: prompt.prompt,
|
||||
name: prompt.name,
|
||||
version: prompt.version,
|
||||
isActive: prompt.isActive,
|
||||
},
|
||||
});
|
||||
promptIds.push(prompt.id);
|
||||
}
|
||||
|
||||
const promptVersionsWithVariables = [
|
||||
{
|
||||
id: `prompt-${Math.floor(Math.random() * 1000000000)}`,
|
||||
projectId: project2.id,
|
||||
createdBy: "user-1",
|
||||
prompt: "Prompt 4 version 1 content with {{variable}}",
|
||||
name: "Prompt 4 with variable",
|
||||
version: 1,
|
||||
isActive: false,
|
||||
},
|
||||
{
|
||||
id: `prompt-${Math.floor(Math.random() * 1000000000)}`,
|
||||
projectId: project2.id,
|
||||
createdBy: "user-1",
|
||||
prompt: "Prompt 4 version 2 content with {{variable}}",
|
||||
name: "Prompt 4 with variable",
|
||||
version: 2,
|
||||
isActive: true,
|
||||
},
|
||||
{
|
||||
id: `prompt-${Math.floor(Math.random() * 1000000000)}`,
|
||||
projectId: project2.id,
|
||||
createdBy: "user-1",
|
||||
prompt: "Prompt 4 version 3 content with {{variable}}",
|
||||
name: "Prompt 4 with variable",
|
||||
version: 3,
|
||||
isActive: false,
|
||||
},
|
||||
];
|
||||
|
||||
for (const version of promptVersionsWithVariables) {
|
||||
await prisma.prompt.create({
|
||||
data: {
|
||||
id: version.id,
|
||||
projectId: version.projectId,
|
||||
createdBy: version.createdBy,
|
||||
prompt: version.prompt,
|
||||
name: version.name,
|
||||
version: version.version,
|
||||
isActive: version.isActive,
|
||||
},
|
||||
});
|
||||
promptIds.push(version.id);
|
||||
}
|
||||
const promptName = "Prompt with Longer Name";
|
||||
const projectId = project2.id;
|
||||
const createdBy = "user-1";
|
||||
|
||||
for (let i = 1; i <= 20; i++) {
|
||||
const promptId = `prompt-${Math.floor(Math.random() * 1000000000)}`;
|
||||
await prisma.prompt.create({
|
||||
data: {
|
||||
id: promptId,
|
||||
projectId: projectId,
|
||||
createdBy: createdBy,
|
||||
prompt: `${promptName} version ${i} content`,
|
||||
name: promptName,
|
||||
version: i,
|
||||
isActive: i === 20,
|
||||
},
|
||||
});
|
||||
promptIds.push(promptId);
|
||||
}
|
||||
|
||||
const generationIds: string[] = [];
|
||||
const envTags = [null, "development", "staging", "production"];
|
||||
const colorTags = [null, "red", "blue", "yellow"];
|
||||
|
||||
for (let i = 0; i < TRACE_VOLUME; i++) {
|
||||
// print progress to console with a progress bar that refreshes every 10 iterations
|
||||
if ((i + 1) % 10 === 0 || i === TRACE_VOLUME - 1) {
|
||||
process.stdout.clearLine(0);
|
||||
process.stdout.cursorTo(0);
|
||||
process.stdout.write(`Seeding ${i + 1} of ${TRACE_VOLUME}`);
|
||||
}
|
||||
// random date within last 90 days, with a linear bias towards more recent dates
|
||||
const traceTs = new Date(
|
||||
Date.now() -
|
||||
Math.floor(Math.random() ** 1.5 * 90 * 24 * 60 * 60 * 1000),
|
||||
);
|
||||
|
||||
const envTag = envTags[Math.floor(Math.random() * envTags.length)];
|
||||
const colorTag = colorTags[Math.floor(Math.random() * colorTags.length)];
|
||||
|
||||
const tags = [envTag, colorTag].filter((tag) => tag !== null);
|
||||
|
||||
const projectId = [project1.id, project2.id][i % 2] as string;
|
||||
|
||||
const trace = await prisma.trace.create({
|
||||
data: {
|
||||
id: `trace-${Math.floor(Math.random() * 1000000000)}`,
|
||||
timestamp: traceTs,
|
||||
name: ["generate-outreach", "label-inbound", "draft-response"][
|
||||
i % 3
|
||||
] as string,
|
||||
metadata: {
|
||||
user: `user-${i}@langfuse.com`,
|
||||
},
|
||||
tags: tags as string[],
|
||||
project: {
|
||||
connect: { id: projectId },
|
||||
},
|
||||
userId: `user-${i % 10}`,
|
||||
session:
|
||||
Math.random() > 0.3
|
||||
? {
|
||||
connectOrCreate: {
|
||||
where: {
|
||||
id_projectId: {
|
||||
id: `session-${i % 10}`,
|
||||
projectId: projectId,
|
||||
},
|
||||
},
|
||||
create: {
|
||||
id: `session-${i % 10}`,
|
||||
projectId: projectId,
|
||||
},
|
||||
},
|
||||
}
|
||||
: undefined,
|
||||
input:
|
||||
Math.random() > 0.3
|
||||
? "I'm looking for a React component"
|
||||
: undefined,
|
||||
output:
|
||||
Math.random() > 0.3
|
||||
? "What kind of component are you looking for?"
|
||||
: undefined,
|
||||
scores: {
|
||||
createMany: {
|
||||
data: [
|
||||
...(Math.random() > 0.5
|
||||
? [
|
||||
{
|
||||
name: "feedback",
|
||||
value: Math.floor(Math.random() * 3) - 1,
|
||||
timestamp: traceTs,
|
||||
},
|
||||
]
|
||||
: []),
|
||||
...(Math.random() > 0.7
|
||||
? [
|
||||
{
|
||||
name: "sentiment",
|
||||
value: Math.floor(Math.random() * 10) - 5,
|
||||
timestamp: traceTs,
|
||||
},
|
||||
]
|
||||
: []),
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const existingSpanIds: string[] = [];
|
||||
|
||||
for (let j = 0; j < Math.floor(Math.random() * 10) + 1; j++) {
|
||||
// add between 1 and 30 ms to trace timestamp
|
||||
const spanTsStart = new Date(
|
||||
traceTs.getTime() + Math.floor(Math.random() * 30),
|
||||
);
|
||||
// random duration of upto 30ms
|
||||
const spanTsEnd = new Date(
|
||||
spanTsStart.getTime() + Math.floor(Math.random() * 30),
|
||||
);
|
||||
|
||||
const span = await prisma.observation.create({
|
||||
data: {
|
||||
type: "SPAN",
|
||||
id: `span-${Math.floor(Math.random() * 1000000000)}`,
|
||||
startTime: spanTsStart,
|
||||
endTime: spanTsEnd,
|
||||
name: `span-${i}-${j}`,
|
||||
metadata: {
|
||||
user: `user-${i}@langfuse.com`,
|
||||
},
|
||||
project: { connect: { id: trace.projectId } },
|
||||
traceId: trace.id,
|
||||
// if this is the first span or in 50% of cases, add no parent; otherwise randomly select parent from existing spans
|
||||
...(existingSpanIds.length === 0 || Math.random() > 0.5
|
||||
? {}
|
||||
: {
|
||||
parentObservationId:
|
||||
existingSpanIds[
|
||||
Math.floor(Math.random() * existingSpanIds.length)
|
||||
],
|
||||
}),
|
||||
},
|
||||
});
|
||||
|
||||
existingSpanIds.push(span.id);
|
||||
|
||||
for (let k = 0; k < Math.floor(Math.random() * 2) + 1; k++) {
|
||||
// random start and end times within span
|
||||
const generationTsStart = new Date(
|
||||
spanTsStart.getTime() +
|
||||
Math.floor(
|
||||
Math.random() * (spanTsEnd.getTime() - spanTsStart.getTime()),
|
||||
),
|
||||
);
|
||||
const generationTsEnd = new Date(
|
||||
generationTsStart.getTime() +
|
||||
Math.floor(
|
||||
Math.random() *
|
||||
(spanTsEnd.getTime() - generationTsStart.getTime()),
|
||||
),
|
||||
);
|
||||
|
||||
const promptTokens = Math.floor(Math.random() * 1000) + 300;
|
||||
const completionTokens = Math.floor(Math.random() * 500) + 100;
|
||||
|
||||
const models = [
|
||||
"gpt-3.5-turbo",
|
||||
"gpt-4",
|
||||
"gpt-4-32k-0613",
|
||||
"gpt-3.5-turbo-16k-0613",
|
||||
"claude-instant-1",
|
||||
"claude-2.1",
|
||||
"gpt-4-vision-preview",
|
||||
"MIXTRAL-8X7B",
|
||||
];
|
||||
|
||||
const model = models[Math.floor(Math.random() * models.length)];
|
||||
const promptId =
|
||||
promptIds[Math.floor(Math.random() * promptIds.length)];
|
||||
const generation = await prisma.observation.create({
|
||||
data: {
|
||||
type: "GENERATION",
|
||||
id: `generation-${Math.floor(Math.random() * 1000000000)}`,
|
||||
startTime: generationTsStart,
|
||||
endTime: generationTsEnd,
|
||||
name: `generation-${i}-${j}-${k}`,
|
||||
project: { connect: { id: trace.projectId } },
|
||||
prompt: { connect: { id: promptId } },
|
||||
input:
|
||||
Math.random() > 0.5
|
||||
? [
|
||||
{
|
||||
role: "system",
|
||||
content: "Be a helpful assistant",
|
||||
},
|
||||
{
|
||||
role: "user",
|
||||
content: "How can i create a React component?",
|
||||
},
|
||||
]
|
||||
: {
|
||||
input: "How can i create a React component?",
|
||||
retrievedDocuments: [
|
||||
{
|
||||
title: "How to create a React component",
|
||||
url: "https://www.google.com",
|
||||
description: "A guide to creating React components",
|
||||
},
|
||||
{
|
||||
title: "React component creation",
|
||||
url: "https://www.google.com",
|
||||
description: "A guide to creating React components",
|
||||
},
|
||||
],
|
||||
},
|
||||
output: {
|
||||
completion: `Creating a React component can be done in two ways: as a functional component or as a class component. Let's start with a basic example of both.
|
||||
|
||||
1. **Functional Component**:
|
||||
|
||||
A functional component is just a plain JavaScript function that accepts props as an argument, and returns a React element. Here's how you can create one:
|
||||
|
||||
|
||||
'import React from 'react'; function Greeting(props) { return <h1>Hello, {props.name}</h1>; } export default Greeting;'
|
||||
|
||||
To use this component in another file, you can do:
|
||||
|
||||
|
||||
'import Greeting from './Greeting'; function App() { return ( <div> <Greeting name="John" /> </div> ); } export default App;'
|
||||
|
||||
2. **Class Component**:
|
||||
|
||||
You can also define components as classes in React. These have some additional features compared to functional components:
|
||||
|
||||
|
||||
'import React, { Component } from 'react'; class Greeting extends Component { render() { return <h1>Hello, {this.props.name}</h1>; } } export default Greeting;'
|
||||
|
||||
And here's how to use this component:
|
||||
|
||||
|
||||
'import Greeting from './Greeting'; class App extends Component { render() { return ( <div> <Greeting name="John" /> </div> ); } } export default App;'
|
||||
|
||||
With the advent of hooks in React, functional components can do everything that class components can do and hence, the community has been favoring functional components over class components.
|
||||
|
||||
Remember to import React at the top of your file whenever you're creating a component, because JSX transpiles to 'React.createElement' calls under the hood.`,
|
||||
},
|
||||
model: model,
|
||||
internalModel: model,
|
||||
modelParameters: {
|
||||
temperature:
|
||||
Math.random() > 0.9 ? undefined : Math.random().toFixed(2),
|
||||
topP:
|
||||
Math.random() > 0.9 ? undefined : Math.random().toFixed(2),
|
||||
maxTokens:
|
||||
Math.random() > 0.9
|
||||
? undefined
|
||||
: Math.floor(Math.random() * 1000),
|
||||
},
|
||||
metadata: {
|
||||
user: `user-${i}@langfuse.com`,
|
||||
},
|
||||
promptTokens,
|
||||
completionTokens,
|
||||
totalTokens: promptTokens + completionTokens,
|
||||
parentObservationId: span.id,
|
||||
traceId: trace.id,
|
||||
...{
|
||||
...(Math.random() > 0.5 && trace.projectId === prompt.projectId
|
||||
? { prompt: { connect: { id: prompt.id } } }
|
||||
: {}),
|
||||
},
|
||||
unit: ModelUsageUnit.Tokens,
|
||||
},
|
||||
});
|
||||
if (Math.random() > 0.6)
|
||||
await prisma.score.create({
|
||||
data: {
|
||||
name: "quality",
|
||||
value: Math.random() * 2 - 1,
|
||||
observationId: generation.id,
|
||||
traceId: trace.id,
|
||||
},
|
||||
});
|
||||
if (Math.random() > 0.6)
|
||||
await prisma.score.create({
|
||||
data: {
|
||||
name: "conciseness",
|
||||
value: Math.random() * 2 - 1,
|
||||
observationId: generation.id,
|
||||
traceId: trace.id,
|
||||
},
|
||||
});
|
||||
|
||||
generationIds.push(generation.id);
|
||||
|
||||
for (let l = 0; l < Math.floor(Math.random() * 2); l++) {
|
||||
// random start time within span
|
||||
const eventTs = new Date(
|
||||
spanTsStart.getTime() +
|
||||
Math.floor(
|
||||
Math.random() * (spanTsEnd.getTime() - spanTsStart.getTime()),
|
||||
),
|
||||
);
|
||||
|
||||
await prisma.observation.create({
|
||||
data: {
|
||||
type: "EVENT",
|
||||
id: `event-${Math.floor(Math.random() * 1000000000)}`,
|
||||
startTime: eventTs,
|
||||
name: `event-${i}-${j}-${k}-${l}`,
|
||||
metadata: {
|
||||
user: `user-${i}@langfuse.com`,
|
||||
},
|
||||
parentObservationId: span.id,
|
||||
traceId: trace.id,
|
||||
project: { connect: { id: trace.projectId } },
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (let datasetNumber = 0; datasetNumber < 2; datasetNumber++) {
|
||||
const dataset = await prisma.dataset.create({
|
||||
data: {
|
||||
name: `demo-dataset-${datasetNumber}`,
|
||||
projectId: project2.id,
|
||||
},
|
||||
});
|
||||
|
||||
for (let datasetRunNumber = 0; datasetRunNumber < 2; datasetRunNumber++) {
|
||||
const datasetRun = await prisma.datasetRuns.create({
|
||||
data: {
|
||||
name: `demo-dataset-run-${datasetRunNumber}`,
|
||||
datasetId: dataset.id,
|
||||
},
|
||||
});
|
||||
|
||||
for (let runNumber = 0; runNumber < 10; runNumber++) {
|
||||
//pick randomly from existingSpanIds
|
||||
const sourceObservationId =
|
||||
generationIds[Math.floor(Math.random() * generationIds.length)];
|
||||
const runObservationId =
|
||||
generationIds[Math.floor(Math.random() * generationIds.length)];
|
||||
|
||||
const datasetItem = await prisma.datasetItem.create({
|
||||
data: {
|
||||
datasetId: dataset.id,
|
||||
sourceObservationId:
|
||||
Math.random() > 0.5 ? sourceObservationId : undefined,
|
||||
input: [
|
||||
{
|
||||
role: "user",
|
||||
content: "How can i create a React component?",
|
||||
},
|
||||
],
|
||||
expectedOutput:
|
||||
"Creating a React component can be done in two ways: as a functional component or as a class component. Let's start with a basic example of both.",
|
||||
},
|
||||
});
|
||||
|
||||
await prisma.datasetRunItems.create({
|
||||
data: {
|
||||
datasetItemId: datasetItem.id,
|
||||
observationId: runObservationId!,
|
||||
datasetRunId: datasetRun.id,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
main()
|
||||
.then(async () => {
|
||||
await prisma.$disconnect();
|
||||
})
|
||||
.catch(async (e) => {
|
||||
console.error(e);
|
||||
await prisma.$disconnect();
|
||||
process.exit(1);
|
||||
});
|
||||
|
Before Width: | Height: | Size: 42 KiB |
|
Before Width: | Height: | Size: 37 KiB |
|
Before Width: | Height: | Size: 772 B |
|
Before Width: | Height: | Size: 2.1 KiB |
|
Before Width: | Height: | Size: 15 KiB |
|
Before Width: | Height: | Size: 60 KiB |
|
Before Width: | Height: | Size: 312 KiB |
@@ -0,0 +1,9 @@
|
||||
|
||||
# used to nuke the dev environment for engineers
|
||||
|
||||
find . -name 'node_modules' -type d -prune -print -exec rm -rf '{}' \;
|
||||
find . -name '.next' -type d -prune -print -exec rm -rf '{}' \;
|
||||
find . -iname "bin" -type d -prune -print -exec rm -rf '{}' \;
|
||||
find . -iname "dist" -type d -prune -print -exec rm -rf '{}' \;
|
||||
find . -iname "out" -type d -prune -print -exec rm -rf '{}' \;
|
||||
find . -iname ".turbo" -type d -prune -print -exec rm -rf '{}' \;
|
||||
@@ -1,84 +0,0 @@
|
||||
import { TrashIcon } from "lucide-react";
|
||||
import { useRouter } from "next/router";
|
||||
import { useState } from "react";
|
||||
import { Button } from "@/src/components/ui/button";
|
||||
import { api } from "@/src/utils/api";
|
||||
import {
|
||||
Popover,
|
||||
PopoverContent,
|
||||
PopoverTrigger,
|
||||
} from "@/src/components/ui/popover";
|
||||
import { useHasAccess } from "@/src/features/rbac/utils/checkAccess";
|
||||
import { usePostHog } from "posthog-js/react";
|
||||
|
||||
export function DeleteTrace({
|
||||
traceId,
|
||||
projectId,
|
||||
isTableAction,
|
||||
}: {
|
||||
traceId: string;
|
||||
projectId: string;
|
||||
isTableAction?: boolean;
|
||||
}) {
|
||||
const [isDeleted, setIsDeleted] = useState(false);
|
||||
const router = useRouter();
|
||||
const utils = api.useUtils();
|
||||
const posthog = usePostHog();
|
||||
|
||||
const hasAccess = useHasAccess({ projectId, scope: "traces:delete" });
|
||||
|
||||
const mutDeleteTraces = api.traces.deleteMany.useMutation({
|
||||
onSuccess: () => {
|
||||
setIsDeleted(true);
|
||||
void utils.traces.invalidate();
|
||||
if (!isTableAction) {
|
||||
void router.push(`/project/${projectId}/traces`);
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
if (!hasAccess) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<Popover key={traceId}>
|
||||
<PopoverTrigger asChild>
|
||||
{isTableAction ? (
|
||||
<Button variant="ghost" size="xs">
|
||||
<TrashIcon className="h-4 w-4" />
|
||||
</Button>
|
||||
) : (
|
||||
<Button variant="outline" type="button" size="icon">
|
||||
<TrashIcon className="h-4 w-4" />
|
||||
</Button>
|
||||
)}
|
||||
</PopoverTrigger>
|
||||
<PopoverContent>
|
||||
<h2 className="text-md mb-3 font-semibold">Please confirm</h2>
|
||||
<p className="mb-3 text-sm">
|
||||
This action cannot be undone and removes all the data associated with
|
||||
this trace.
|
||||
</p>
|
||||
<div className="flex justify-end space-x-4">
|
||||
<Button
|
||||
type="button"
|
||||
variant="destructive"
|
||||
loading={mutDeleteTraces.isLoading || isDeleted}
|
||||
onClick={() => {
|
||||
void mutDeleteTraces.mutateAsync({
|
||||
traceIds: [traceId],
|
||||
projectId,
|
||||
});
|
||||
posthog.capture("trace:delete", {
|
||||
source: isTableAction ? "table-single-row" : "trace",
|
||||
});
|
||||
}}
|
||||
>
|
||||
Delete trace
|
||||
</Button>
|
||||
</div>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
);
|
||||
}
|
||||
@@ -1 +0,0 @@
|
||||
export const VERSION = "v2.4.0";
|
||||
@@ -1,88 +0,0 @@
|
||||
import {
|
||||
dateTimeAggregationOptions,
|
||||
dateTimeAggregationSettings,
|
||||
} from "@/src/features/dashboard/lib/timeseries-aggregation";
|
||||
import { z } from "zod";
|
||||
|
||||
import {
|
||||
createTRPCRouter,
|
||||
protectedProjectProcedure,
|
||||
} from "@/src/server/api/trpc";
|
||||
import { executeQuery } from "@/src/server/api/services/query-builder";
|
||||
import { sqlInterface } from "@/src/server/api/services/sqlInterface";
|
||||
|
||||
export const dashboardRouter = createTRPCRouter({
|
||||
chart: protectedProjectProcedure
|
||||
.input(sqlInterface.extend({ projectId: z.string() }))
|
||||
.query(async ({ input, ctx }) => {
|
||||
return await executeQuery(ctx.prisma, input.projectId, input);
|
||||
}),
|
||||
|
||||
scores: protectedProjectProcedure
|
||||
.input(
|
||||
z.object({
|
||||
projectId: z.string(),
|
||||
agg: z.enum(dateTimeAggregationOptions),
|
||||
}),
|
||||
)
|
||||
.query(async ({ input, ctx }) => {
|
||||
// queryRawUnsafe to add input.agg to the WHERE clause
|
||||
const output = await ctx.prisma.$queryRawUnsafe<
|
||||
{
|
||||
date_trunc: Date;
|
||||
values: {
|
||||
[key: string]: number;
|
||||
} | null;
|
||||
}[]
|
||||
>(`
|
||||
WITH timeseries AS (
|
||||
SELECT
|
||||
date_trunc('${
|
||||
dateTimeAggregationSettings[input.agg].date_trunc
|
||||
}', dt) as date_trunc
|
||||
FROM generate_series(
|
||||
NOW() - INTERVAL '${input.agg}', NOW(), INTERVAL '1 minute'
|
||||
) as dt
|
||||
WHERE dt > NOW() - INTERVAL '${input.agg}'
|
||||
GROUP BY 1
|
||||
),
|
||||
metrics AS (
|
||||
SELECT
|
||||
date_trunc('${
|
||||
dateTimeAggregationSettings[input.agg].date_trunc
|
||||
}', scores.timestamp) as date_trunc,
|
||||
scores.name as metric_name,
|
||||
AVG(value) as avg_value
|
||||
FROM scores
|
||||
LEFT JOIN traces ON scores.trace_id = traces.id
|
||||
WHERE scores.timestamp > NOW() - INTERVAL '${input.agg}'
|
||||
AND traces.project_id = '${input.projectId}'
|
||||
GROUP BY 1,2
|
||||
),
|
||||
json_metrics AS (
|
||||
SELECT
|
||||
date_trunc,
|
||||
jsonb_object_agg(metric_name, avg_value) as values
|
||||
FROM metrics
|
||||
GROUP BY 1
|
||||
)
|
||||
SELECT
|
||||
timeseries.date_trunc,
|
||||
json_metrics.values as values
|
||||
FROM timeseries
|
||||
LEFT JOIN json_metrics ON timeseries.date_trunc = json_metrics.date_trunc
|
||||
ORDER BY 1
|
||||
`);
|
||||
|
||||
return output.map((row) => ({
|
||||
...row,
|
||||
values: row.values
|
||||
? Object.entries(row.values).map(([label, value]) => ({
|
||||
label: "avg_" + label,
|
||||
value,
|
||||
}))
|
||||
: [],
|
||||
ts: row.date_trunc.getTime(),
|
||||
}));
|
||||
}),
|
||||
});
|
||||
@@ -1,52 +0,0 @@
|
||||
import { Button } from "@/src/components/ui/button";
|
||||
import { LockIcon, PlusIcon } from "lucide-react";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from "@/src/components/ui/dialog";
|
||||
import { useState } from "react";
|
||||
import { DialogTrigger } from "@radix-ui/react-dialog";
|
||||
import { NewDatasetForm } from "@/src/features/datasets/components/NewDatasetForm";
|
||||
import { useHasAccess } from "@/src/features/rbac/utils/checkAccess";
|
||||
|
||||
export const NewDatasetButton = (props: {
|
||||
projectId: string;
|
||||
datasetId?: string;
|
||||
className?: string;
|
||||
}) => {
|
||||
const [open, setOpen] = useState(false);
|
||||
const hasAccess = useHasAccess({
|
||||
projectId: props.projectId,
|
||||
scope: "datasets:CUD",
|
||||
});
|
||||
|
||||
return (
|
||||
<Dialog open={hasAccess && open} onOpenChange={setOpen}>
|
||||
<DialogTrigger asChild>
|
||||
<Button
|
||||
variant="secondary"
|
||||
className={props.className}
|
||||
disabled={!hasAccess}
|
||||
>
|
||||
{hasAccess ? (
|
||||
<PlusIcon className="-ml-0.5 mr-1.5" aria-hidden="true" />
|
||||
) : (
|
||||
<LockIcon className="-ml-0.5 mr-1.5 h-3 w-3" aria-hidden="true" />
|
||||
)}
|
||||
New dataset
|
||||
</Button>
|
||||
</DialogTrigger>
|
||||
<DialogContent className="sm:max-w-3xl">
|
||||
<DialogHeader>
|
||||
<DialogTitle className="mb-5">Create new dataset</DialogTitle>
|
||||
</DialogHeader>
|
||||
<NewDatasetForm
|
||||
projectId={props.projectId}
|
||||
onFormSuccess={() => setOpen(false)}
|
||||
/>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
};
|
||||
@@ -1,94 +0,0 @@
|
||||
import { Button } from "@/src/components/ui/button";
|
||||
import * as z from "zod";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { useForm } from "react-hook-form";
|
||||
import {
|
||||
Form,
|
||||
FormControl,
|
||||
FormField,
|
||||
FormItem,
|
||||
FormLabel,
|
||||
FormMessage,
|
||||
} from "@/src/components/ui/form";
|
||||
import { api } from "@/src/utils/api";
|
||||
import { useState } from "react";
|
||||
import { usePostHog } from "posthog-js/react";
|
||||
import { Input } from "@/src/components/ui/input";
|
||||
|
||||
const formSchema = z.object({
|
||||
name: z.string(),
|
||||
});
|
||||
|
||||
export const NewDatasetForm = (props: {
|
||||
projectId: string;
|
||||
onFormSuccess?: () => void;
|
||||
}) => {
|
||||
const [formError, setFormError] = useState<string | null>(null);
|
||||
const posthog = usePostHog();
|
||||
const form = useForm<z.infer<typeof formSchema>>({
|
||||
resolver: zodResolver(formSchema),
|
||||
defaultValues: {
|
||||
name: "",
|
||||
},
|
||||
});
|
||||
|
||||
const utils = api.useUtils();
|
||||
const createDatasetMutation = api.datasets.createDataset.useMutation({
|
||||
onSuccess: () => utils.datasets.invalidate(),
|
||||
onError: (error) => setFormError(error.message),
|
||||
});
|
||||
|
||||
function onSubmit(values: z.infer<typeof formSchema>) {
|
||||
posthog.capture("datasets:new_dataset_form_submit");
|
||||
createDatasetMutation
|
||||
.mutateAsync({
|
||||
...values,
|
||||
projectId: props.projectId,
|
||||
})
|
||||
.then(() => {
|
||||
props.onFormSuccess?.();
|
||||
form.reset();
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error(error);
|
||||
});
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Form {...form}>
|
||||
<form
|
||||
// eslint-disable-next-line @typescript-eslint/no-misused-promises
|
||||
onSubmit={form.handleSubmit(onSubmit)}
|
||||
className="space-y-8"
|
||||
>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="name"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Name</FormLabel>
|
||||
<FormControl>
|
||||
<Input {...field} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<Button
|
||||
type="submit"
|
||||
loading={createDatasetMutation.isLoading}
|
||||
className="w-full"
|
||||
>
|
||||
New dataset
|
||||
</Button>
|
||||
</form>
|
||||
</Form>
|
||||
{formError ? (
|
||||
<p className="text-red text-center">
|
||||
<span className="font-bold">Error:</span> {formError}
|
||||
</p>
|
||||
) : null}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -1,108 +0,0 @@
|
||||
// Langfuse Cloud only
|
||||
|
||||
import { Button } from "@/src/components/ui/button";
|
||||
import { env } from "@/src/env.mjs";
|
||||
import { api } from "@/src/utils/api";
|
||||
import { Card, Flex, MarkerBar, Metric, Text } from "@tremor/react";
|
||||
import Link from "next/link";
|
||||
import { PricingPage } from "@/src/features/pricing-page/PricingPage";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogHeader,
|
||||
DialogTrigger,
|
||||
} from "@/src/components/ui/dialog";
|
||||
import Header from "@/src/components/layouts/header";
|
||||
import { usePostHog } from "posthog-js/react";
|
||||
|
||||
export const ProjectUsageChart: React.FC<{ projectId: string }> = ({
|
||||
projectId,
|
||||
}) => {
|
||||
const usage = api.usageMetering.currentMonth.useQuery({
|
||||
projectId,
|
||||
});
|
||||
const posthog = usePostHog();
|
||||
const project = api.projects.byId.useQuery({ projectId });
|
||||
const planLimit =
|
||||
project.data?.cloudConfig?.monthlyObservationLimit ?? 100_000;
|
||||
const plan = project.data?.cloudConfig?.plan ?? "Hobby";
|
||||
const currentMonth = new Date().toLocaleDateString("en-US", {
|
||||
month: "short",
|
||||
});
|
||||
|
||||
if (!env.NEXT_PUBLIC_LANGFUSE_CLOUD_REGION) return null;
|
||||
|
||||
return (
|
||||
<div>
|
||||
<h2 className="mb-5 text-base font-semibold leading-6 text-gray-900">
|
||||
Usage
|
||||
</h2>
|
||||
<Card className="p-4 lg:w-1/2">
|
||||
{usage.data !== undefined ? (
|
||||
<>
|
||||
<Text>Observations / month</Text>
|
||||
<Metric>{usage.data}</Metric>
|
||||
<Flex className="mt-4">
|
||||
<Text>
|
||||
{`${currentMonth}: ${usage.data} (${(
|
||||
(usage.data / planLimit) *
|
||||
100
|
||||
).toLocaleString(undefined, {
|
||||
maximumFractionDigits: 2,
|
||||
})}%)`}
|
||||
</Text>
|
||||
<Text>Plan limit: {simplifyNumber(planLimit)}</Text>
|
||||
</Flex>
|
||||
<MarkerBar
|
||||
value={Math.min((usage.data / planLimit) * 100, 100)}
|
||||
className="mt-3"
|
||||
/>
|
||||
</>
|
||||
) : null}
|
||||
</Card>
|
||||
<div className="mt-4 flex flex-row items-center gap-2">
|
||||
<Dialog
|
||||
onOpenChange={(open) => {
|
||||
if (open) {
|
||||
posthog.capture("project_settings:pricing_dialog_opened");
|
||||
}
|
||||
}}
|
||||
>
|
||||
<DialogTrigger asChild>
|
||||
<Button variant="secondary">Change plans</Button>
|
||||
</DialogTrigger>
|
||||
<DialogContent className="max-w-3xl">
|
||||
<DialogHeader>
|
||||
<Header
|
||||
title="Select plan"
|
||||
level="h3"
|
||||
actionButtons={
|
||||
<Button variant="secondary" asChild>
|
||||
<Link href="https://langfuse.com/pricing">
|
||||
Pricing page ↗
|
||||
</Link>
|
||||
</Button>
|
||||
}
|
||||
/>
|
||||
</DialogHeader>
|
||||
<p>
|
||||
All plans offer a 7-day free trial. For more information about the
|
||||
plans, please visit our pricing page or reach out to us via the
|
||||
chat.
|
||||
</p>
|
||||
<PricingPage className="mb-5 mt-10 " />
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
<div className="inline-block text-sm text-gray-500">
|
||||
Currently: {plan}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
function simplifyNumber(num: number) {
|
||||
if (num >= 1000000) return num / 1000000 + "m";
|
||||
if (num >= 1000) return num / 1000 + "k";
|
||||
return num.toString();
|
||||
}
|
||||
@@ -1,35 +0,0 @@
|
||||
// Middleware to block requests from certain IPs on Langfuse Cloud
|
||||
// Not included in the self-host build, removed in Dockerfile
|
||||
|
||||
// import { type NextApiRequest } from "next";
|
||||
// import { type NextRequest } from "next/server";
|
||||
// import { get } from "@vercel/edge-config";
|
||||
|
||||
// export async function middleware(req: NextRequest) {
|
||||
// try {
|
||||
// if (process.env.NEXT_PUBLIC_LANGFUSE_CLOUD_REGION !== undefined) {
|
||||
// const config = await get("blockedIps");
|
||||
// const blockedIps = Array.isArray(config) ? config : [config];
|
||||
|
||||
// const ip = getIP(req);
|
||||
// if (ip && blockedIps.includes(ip)) {
|
||||
// console.log("Blocked request by ip: ", ip);
|
||||
// return new Response("Access denied", { status: 403 });
|
||||
// }
|
||||
// }
|
||||
|
||||
// return;
|
||||
// } catch (e) {
|
||||
// console.error("Server side error in middleware: ", e);
|
||||
// return new Response("Internal server error", { status: 500 });
|
||||
// }
|
||||
// }
|
||||
|
||||
// export default function getIP(request: Request | NextApiRequest) {
|
||||
// const xff =
|
||||
// request instanceof Request
|
||||
// ? request.headers.get("x-forwarded-for")
|
||||
// : request.headers["x-forwarded-for"];
|
||||
|
||||
// return xff ? (Array.isArray(xff) ? xff[0] : xff.split(",")[0]) : "127.0.0.1";
|
||||
// }
|
||||
@@ -1,206 +0,0 @@
|
||||
import { type NextApiRequest, type NextApiResponse } from "next";
|
||||
import { z } from "zod";
|
||||
import { cors, runMiddleware } from "@/src/features/public-api/server/cors";
|
||||
import { prisma } from "@/src/server/db";
|
||||
import { verifyAuthHeaderAndReturnScope } from "@/src/features/public-api/server/apiAuth";
|
||||
import { Prisma } from "@prisma/client";
|
||||
import { paginationZod } from "@/src/utils/zod";
|
||||
|
||||
const GetUsageSchema = z.object({
|
||||
...paginationZod,
|
||||
group_by: z.enum(["trace_name"]).nullish(),
|
||||
trace_name: z.string().nullish(),
|
||||
});
|
||||
|
||||
export default async function handler(
|
||||
req: NextApiRequest,
|
||||
res: NextApiResponse,
|
||||
) {
|
||||
await runMiddleware(req, res, cors);
|
||||
|
||||
// CHECK AUTH
|
||||
const authCheck = await verifyAuthHeaderAndReturnScope(
|
||||
req.headers.authorization,
|
||||
);
|
||||
if (!authCheck.validKey)
|
||||
return res.status(401).json({
|
||||
message: authCheck.error,
|
||||
});
|
||||
// END CHECK AUTH
|
||||
|
||||
try {
|
||||
if (req.method === "GET") {
|
||||
if (authCheck.scope.accessLevel !== "all") {
|
||||
return res.status(401).json({
|
||||
message:
|
||||
"Access denied - need to use basic auth with secret key to GET scores",
|
||||
});
|
||||
}
|
||||
const obj = GetUsageSchema.parse(req.query); // uses query and not body
|
||||
|
||||
const traceNameCondition = obj.trace_name
|
||||
? Prisma.sql`AND t.name = ${obj.trace_name}`
|
||||
: Prisma.empty;
|
||||
|
||||
if (obj.group_by === undefined) {
|
||||
const usage = await prisma.$queryRaw`
|
||||
WITH model_usage AS (
|
||||
SELECT
|
||||
DATE_TRUNC('DAY',
|
||||
o.start_time) observation_day,
|
||||
o.model,
|
||||
SUM(o.prompt_tokens) prompt_tokens,
|
||||
SUM(o.completion_tokens) completion_tokens,
|
||||
SUM(o.total_tokens) total_tokens
|
||||
FROM
|
||||
traces t
|
||||
LEFT JOIN observations o ON o.trace_id = t.id
|
||||
WHERE o.start_time IS NOT NULL
|
||||
AND o.project_id = ${authCheck.scope.projectId}
|
||||
AND t.project_id = ${authCheck.scope.projectId}
|
||||
${traceNameCondition}
|
||||
GROUP BY 1,2
|
||||
order by 1,2
|
||||
),
|
||||
daily_usage AS (
|
||||
SELECT
|
||||
observation_day,
|
||||
json_agg(json_build_object('model',
|
||||
model,
|
||||
'prompt_tokens',
|
||||
prompt_tokens,
|
||||
'completion_tokens',
|
||||
completion_tokens,
|
||||
'total_tokens',
|
||||
total_tokens)) daily_usage_json
|
||||
FROM model_usage
|
||||
group by 1
|
||||
)
|
||||
SELECT
|
||||
observation_day "date",
|
||||
daily_usage_json usage
|
||||
FROM daily_usage
|
||||
ORDER BY 1 desc
|
||||
LIMIT ${obj.limit} OFFSET ${(obj.page - 1) * obj.limit}
|
||||
`;
|
||||
const totalItemsRes = await prisma.$queryRaw<{ count: bigint }[]>`
|
||||
SELECT
|
||||
count(DISTINCT DATE_TRUNC('DAY', observations.start_time))
|
||||
FROM
|
||||
observations
|
||||
JOIN traces ON observations.trace_id = traces.id
|
||||
WHERE traces.project_id = ${authCheck.scope.projectId}
|
||||
`;
|
||||
|
||||
const totalItems =
|
||||
totalItemsRes[0] !== undefined ? Number(totalItemsRes[0].count) : 0;
|
||||
|
||||
return res.status(200).json({
|
||||
data: usage,
|
||||
meta: {
|
||||
page: obj.page,
|
||||
limit: obj.limit,
|
||||
totalItems,
|
||||
totalPages: Math.ceil(totalItems / obj.limit),
|
||||
},
|
||||
});
|
||||
} else if (obj.group_by === "trace_name") {
|
||||
const usage = await prisma.$queryRaw`
|
||||
WITH model_usage AS (
|
||||
SELECT
|
||||
t."name" trace_name,
|
||||
DATE_TRUNC('DAY',
|
||||
o.start_time) observation_day,
|
||||
o.model,
|
||||
SUM(o.prompt_tokens) prompt_tokens,
|
||||
SUM(o.completion_tokens) completion_tokens,
|
||||
SUM(o.total_tokens) total_tokens
|
||||
FROM
|
||||
traces t
|
||||
LEFT JOIN observations o ON o.trace_id = t.id
|
||||
WHERE o.start_time IS NOT NULL
|
||||
AND t.project_id = ${authCheck.scope.projectId}
|
||||
AND o.project_id = ${authCheck.scope.projectId}
|
||||
${traceNameCondition}
|
||||
GROUP BY 1,2,3
|
||||
order by 1,2,3
|
||||
),
|
||||
daily_usage AS (
|
||||
SELECT
|
||||
trace_name,
|
||||
observation_day,
|
||||
json_agg(json_build_object('model',
|
||||
model,
|
||||
'prompt_tokens',
|
||||
prompt_tokens,
|
||||
'completion_tokens',
|
||||
completion_tokens,
|
||||
'total_tokens',
|
||||
total_tokens)) daily_usage_json
|
||||
FROM model_usage
|
||||
WHERE prompt_tokens > 0
|
||||
OR completion_tokens > 0
|
||||
OR total_tokens > 0
|
||||
group by 1,2
|
||||
order by 1,2 desc
|
||||
),
|
||||
all_trace_names AS (
|
||||
SELECT t."name" trace_name
|
||||
FROM traces t
|
||||
WHERE t.project_id = ${authCheck.scope.projectId}
|
||||
${traceNameCondition}
|
||||
GROUP BY 1
|
||||
)
|
||||
SELECT
|
||||
all_trace_names.trace_name,
|
||||
json_agg(json_build_object(
|
||||
'date',
|
||||
observation_day,
|
||||
'usage',
|
||||
daily_usage_json
|
||||
)) metrics
|
||||
FROM all_trace_names
|
||||
LEFT JOIN daily_usage ON all_trace_names.trace_name = daily_usage.trace_name
|
||||
group by 1
|
||||
ORDER BY 1
|
||||
LIMIT ${obj.limit} OFFSET ${(obj.page - 1) * obj.limit}
|
||||
`;
|
||||
const totalItemsRes = await prisma.$queryRaw<{ count: bigint }[]>`
|
||||
SELECT
|
||||
count(DISTINCT CASE WHEN "name" IS NULL THEN 'COUNT_NULL' ELSE "name" END)
|
||||
FROM
|
||||
traces
|
||||
WHERE project_id = ${authCheck.scope.projectId}
|
||||
`;
|
||||
|
||||
const totalItems =
|
||||
totalItemsRes[0] !== undefined ? Number(totalItemsRes[0].count) : 0;
|
||||
|
||||
return res.status(200).json({
|
||||
data: usage,
|
||||
meta: {
|
||||
page: obj.page,
|
||||
limit: obj.limit,
|
||||
totalItems,
|
||||
totalPages: Math.ceil(totalItems / obj.limit),
|
||||
},
|
||||
});
|
||||
} else {
|
||||
return res.status(400).json({
|
||||
message: "Invalid group_by value",
|
||||
});
|
||||
}
|
||||
} else {
|
||||
console.error(req.method, req.body);
|
||||
return res.status(405).json({ message: "Method not allowed" });
|
||||
}
|
||||
} catch (error: unknown) {
|
||||
console.error(error);
|
||||
const errorMessage =
|
||||
error instanceof Error ? error.message : "An unknown error occurred";
|
||||
res.status(400).json({
|
||||
message: "Invalid request data",
|
||||
error: errorMessage,
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -1,573 +0,0 @@
|
||||
import { z } from "zod";
|
||||
|
||||
import {
|
||||
createTRPCRouter,
|
||||
protectedProjectProcedure,
|
||||
} from "@/src/server/api/trpc";
|
||||
|
||||
import { Prisma, type ObservationView } from "@prisma/client";
|
||||
import { jsonSchema, paginationZod } from "@/src/utils/zod";
|
||||
import { singleFilter } from "@/src/server/api/interfaces/filters";
|
||||
import {
|
||||
datetimeFilterToPrismaSql,
|
||||
filterToPrismaSql,
|
||||
} from "@/src/features/filters/server/filterToPrisma";
|
||||
import {
|
||||
type ObservationOptions,
|
||||
observationsTableCols,
|
||||
} from "@/src/server/api/definitions/observationsTable";
|
||||
import { usdFormatter } from "@/src/utils/numbers";
|
||||
import { env } from "@/src/env.mjs";
|
||||
import {
|
||||
S3Client,
|
||||
PutObjectCommand,
|
||||
GetObjectCommand,
|
||||
} from "@aws-sdk/client-s3";
|
||||
import { getSignedUrl } from "@aws-sdk/s3-request-presigner";
|
||||
import {
|
||||
exportFileFormats,
|
||||
exportOptions,
|
||||
} from "@/src/server/api/interfaces/exportTypes";
|
||||
import { orderBy } from "@/src/server/api/interfaces/orderBy";
|
||||
import { orderByToPrismaSql } from "@/src/features/orderBy/server/orderByToPrisma";
|
||||
|
||||
const GenerationTableOptions = z.object({
|
||||
projectId: z.string(), // Required for protectedProjectProcedure
|
||||
filter: z.array(singleFilter),
|
||||
searchQuery: z.string().nullable(),
|
||||
orderBy: orderBy,
|
||||
});
|
||||
|
||||
const ListInputs = GenerationTableOptions.extend({
|
||||
...paginationZod,
|
||||
});
|
||||
|
||||
// extend generationfilteroptions with export options
|
||||
const ExportInputs = GenerationTableOptions.extend({
|
||||
fileFormat: z.enum(exportFileFormats),
|
||||
});
|
||||
|
||||
export const generationsRouter = createTRPCRouter({
|
||||
all: protectedProjectProcedure
|
||||
.input(ListInputs)
|
||||
.query(async ({ input, ctx }) => {
|
||||
// ATTENTION: When making changes to this query, make sure to also update the export query
|
||||
const searchCondition = input.searchQuery
|
||||
? Prisma.sql`AND (
|
||||
o."id" ILIKE ${`%${input.searchQuery}%`} OR
|
||||
o."name" ILIKE ${`%${input.searchQuery}%`} OR
|
||||
o."model" ILIKE ${`%${input.searchQuery}%`} OR
|
||||
t."name" ILIKE ${`%${input.searchQuery}%`}
|
||||
)`
|
||||
: Prisma.empty;
|
||||
|
||||
const filterCondition = filterToPrismaSql(
|
||||
input.filter,
|
||||
observationsTableCols,
|
||||
);
|
||||
|
||||
const orderByCondition = orderByToPrismaSql(
|
||||
input.orderBy,
|
||||
observationsTableCols,
|
||||
);
|
||||
|
||||
// to improve query performance, add timeseries filter to observation queries as well
|
||||
const startTimeFilter = input.filter.find(
|
||||
(f) => f.column === "start_time" && f.type === "datetime",
|
||||
);
|
||||
const datetimeFilter =
|
||||
startTimeFilter && startTimeFilter.type === "datetime"
|
||||
? datetimeFilterToPrismaSql(
|
||||
"start_time",
|
||||
startTimeFilter.operator,
|
||||
startTimeFilter.value,
|
||||
)
|
||||
: Prisma.empty;
|
||||
|
||||
const generations = await ctx.prisma.$queryRaw<
|
||||
Array<
|
||||
ObservationView & {
|
||||
traceId: string;
|
||||
traceName: string;
|
||||
latency: number | null;
|
||||
}
|
||||
>
|
||||
>(
|
||||
Prisma.sql`
|
||||
WITH observations_with_latency AS (
|
||||
SELECT
|
||||
o.*,
|
||||
CASE WHEN o.end_time IS NULL THEN NULL ELSE (EXTRACT(EPOCH FROM o."end_time") - EXTRACT(EPOCH FROM o."start_time"))::double precision END AS "latency"
|
||||
FROM observations_view o
|
||||
WHERE o.type = 'GENERATION'
|
||||
AND o.project_id = ${input.projectId}
|
||||
${datetimeFilter}
|
||||
),
|
||||
-- used for filtering
|
||||
scores_avg AS (
|
||||
SELECT
|
||||
trace_id,
|
||||
observation_id,
|
||||
jsonb_object_agg(name::text, avg_value::double precision) AS scores_avg
|
||||
FROM (
|
||||
SELECT
|
||||
trace_id,
|
||||
observation_id,
|
||||
name,
|
||||
avg(value) avg_value
|
||||
FROM
|
||||
scores
|
||||
GROUP BY
|
||||
1,
|
||||
2,
|
||||
3
|
||||
ORDER BY
|
||||
1) tmp
|
||||
GROUP BY
|
||||
1, 2
|
||||
)
|
||||
SELECT
|
||||
o.id,
|
||||
o.name,
|
||||
o.model,
|
||||
o.start_time as "startTime",
|
||||
o.end_time as "endTime",
|
||||
o.latency,
|
||||
o.input,
|
||||
o.output,
|
||||
o.metadata,
|
||||
o.trace_id as "traceId",
|
||||
t.name as "traceName",
|
||||
o.completion_start_time as "completionStartTime",
|
||||
o.prompt_tokens as "promptTokens",
|
||||
o.completion_tokens as "completionTokens",
|
||||
o.total_tokens as "totalTokens",
|
||||
o.level,
|
||||
o.status_message as "statusMessage",
|
||||
o.version,
|
||||
o.model_id as "modelId",
|
||||
o.input_price as "inputPrice",
|
||||
o.output_price as "outputPrice",
|
||||
o.total_price as "totalPrice",
|
||||
o.calculated_input_cost as "calculatedInputCost",
|
||||
o.calculated_output_cost as "calculatedOutputCost",
|
||||
o.calculated_total_cost as "calculatedTotalCost"
|
||||
FROM observations_with_latency o
|
||||
JOIN traces t ON t.id = o.trace_id
|
||||
LEFT JOIN scores_avg AS s_avg ON s_avg.trace_id = t.id and s_avg.observation_id = o.id
|
||||
WHERE
|
||||
t.project_id = ${input.projectId}
|
||||
${searchCondition}
|
||||
${filterCondition}
|
||||
${orderByCondition}
|
||||
LIMIT ${input.limit}
|
||||
OFFSET ${input.page * input.limit}
|
||||
`,
|
||||
);
|
||||
|
||||
const totalGenerations = await ctx.prisma.$queryRaw<
|
||||
Array<{ count: bigint }>
|
||||
>(
|
||||
Prisma.sql`
|
||||
WITH observations_with_latency AS (
|
||||
SELECT
|
||||
o.*,
|
||||
CASE WHEN o.end_time IS NULL THEN NULL ELSE (EXTRACT(EPOCH FROM o."end_time") - EXTRACT(EPOCH FROM o."start_time"))::double precision END AS "latency"
|
||||
FROM observations_view o
|
||||
WHERE o.type = 'GENERATION'
|
||||
AND o.project_id = ${input.projectId}
|
||||
${datetimeFilter}
|
||||
),
|
||||
-- used for filtering
|
||||
scores_avg AS (
|
||||
SELECT
|
||||
trace_id,
|
||||
observation_id,
|
||||
jsonb_object_agg(name::text, avg_value::double precision) AS scores_avg
|
||||
FROM (
|
||||
SELECT
|
||||
trace_id,
|
||||
observation_id,
|
||||
name,
|
||||
avg(value) avg_value
|
||||
FROM
|
||||
scores
|
||||
GROUP BY
|
||||
1,
|
||||
2,
|
||||
3
|
||||
ORDER BY
|
||||
1) tmp
|
||||
GROUP BY
|
||||
1, 2
|
||||
)
|
||||
SELECT
|
||||
count(*)
|
||||
FROM observations_with_latency o
|
||||
JOIN traces t ON t.id = o.trace_id
|
||||
LEFT JOIN scores_avg AS s_avg ON s_avg.trace_id = t.id and s_avg.observation_id = o.id
|
||||
WHERE
|
||||
t.project_id = ${input.projectId}
|
||||
${searchCondition}
|
||||
${filterCondition}
|
||||
`,
|
||||
);
|
||||
|
||||
const scores = await ctx.prisma.score.findMany({
|
||||
where: {
|
||||
trace: {
|
||||
projectId: input.projectId,
|
||||
},
|
||||
observationId: {
|
||||
in: generations.map((gen) => gen.id),
|
||||
},
|
||||
},
|
||||
});
|
||||
const count = totalGenerations[0]?.count;
|
||||
return {
|
||||
totalCount: count ? Number(count) : undefined,
|
||||
generations: generations.map((generation) => {
|
||||
const filteredScores = scores.filter(
|
||||
(s) => s.observationId === generation.id,
|
||||
);
|
||||
return {
|
||||
...generation,
|
||||
scores: filteredScores,
|
||||
};
|
||||
}),
|
||||
};
|
||||
}),
|
||||
|
||||
export: protectedProjectProcedure
|
||||
.input(ExportInputs)
|
||||
.query(async ({ input, ctx }) => {
|
||||
// ATTENTION: When making changes to this query, make sure to also update the all query
|
||||
const searchCondition = input.searchQuery
|
||||
? Prisma.sql`AND (
|
||||
o."id" ILIKE ${`%${input.searchQuery}%`} OR
|
||||
o."name" ILIKE ${`%${input.searchQuery}%`} OR
|
||||
o."model" ILIKE ${`%${input.searchQuery}%`} OR
|
||||
t."name" ILIKE ${`%${input.searchQuery}%`}
|
||||
)`
|
||||
: Prisma.empty;
|
||||
|
||||
const filterCondition = filterToPrismaSql(
|
||||
input.filter,
|
||||
observationsTableCols,
|
||||
);
|
||||
|
||||
const orderByCondition = orderByToPrismaSql(
|
||||
input.orderBy,
|
||||
observationsTableCols,
|
||||
);
|
||||
|
||||
// to improve query performance, add timeseries filter to observation queries as well
|
||||
const startTimeFilter = input.filter.find(
|
||||
(f) => f.column === "start_time" && f.type === "datetime",
|
||||
);
|
||||
const datetimeFilter =
|
||||
startTimeFilter && startTimeFilter.type === "datetime"
|
||||
? datetimeFilterToPrismaSql(
|
||||
"start_time",
|
||||
startTimeFilter.operator,
|
||||
startTimeFilter.value,
|
||||
)
|
||||
: Prisma.empty;
|
||||
|
||||
const generations = await ctx.prisma.$queryRaw<
|
||||
Array<
|
||||
ObservationView & {
|
||||
traceId: string;
|
||||
traceName: string;
|
||||
latency: number | null;
|
||||
}
|
||||
>
|
||||
>(
|
||||
Prisma.sql`
|
||||
WITH observations_with_latency AS (
|
||||
SELECT
|
||||
o.*,
|
||||
CASE WHEN o.end_time IS NULL THEN NULL ELSE (EXTRACT(EPOCH FROM o."end_time") - EXTRACT(EPOCH FROM o."start_time"))::double precision END AS "latency"
|
||||
FROM observations_view o
|
||||
WHERE o.type = 'GENERATION'
|
||||
AND o.project_id = ${input.projectId}
|
||||
${datetimeFilter}
|
||||
),
|
||||
-- used for filtering
|
||||
scores_avg AS (
|
||||
SELECT
|
||||
trace_id,
|
||||
observation_id,
|
||||
jsonb_object_agg(name::text, avg_value::double precision) AS scores_avg
|
||||
FROM (
|
||||
SELECT
|
||||
trace_id,
|
||||
observation_id,
|
||||
name,
|
||||
avg(value) avg_value
|
||||
FROM
|
||||
scores
|
||||
GROUP BY
|
||||
1,
|
||||
2,
|
||||
3
|
||||
ORDER BY
|
||||
1) tmp
|
||||
GROUP BY
|
||||
1, 2
|
||||
)
|
||||
SELECT
|
||||
o.id,
|
||||
o.name,
|
||||
o.model,
|
||||
o.start_time as "startTime",
|
||||
o.end_time as "endTime",
|
||||
o.latency,
|
||||
o.input,
|
||||
o.output,
|
||||
o.metadata,
|
||||
o.trace_id as "traceId",
|
||||
t.name as "traceName",
|
||||
o.completion_start_time as "completionStartTime",
|
||||
o.prompt_tokens as "promptTokens",
|
||||
o.completion_tokens as "completionTokens",
|
||||
o.total_tokens as "totalTokens",
|
||||
o.level,
|
||||
o.status_message as "statusMessage",
|
||||
o.version,
|
||||
o.model_id as "modelId",
|
||||
o.input_price as "inputPrice",
|
||||
o.output_price as "outputPrice",
|
||||
o.total_price as "totalPrice",
|
||||
o.calculated_input_cost as "calculatedInputCost",
|
||||
o.calculated_output_cost as "calculatedOutputCost",
|
||||
o.calculated_total_cost as "calculatedTotalCost"
|
||||
FROM observations_with_latency o
|
||||
JOIN traces t ON t.id = o.trace_id
|
||||
LEFT JOIN scores_avg AS s_avg ON s_avg.trace_id = t.id and s_avg.observation_id = o.id
|
||||
WHERE
|
||||
t.project_id = ${input.projectId}
|
||||
${searchCondition}
|
||||
${filterCondition}
|
||||
${orderByCondition}
|
||||
`,
|
||||
);
|
||||
|
||||
let output: string = "";
|
||||
|
||||
// create file
|
||||
switch (input.fileFormat) {
|
||||
case "CSV":
|
||||
output = [
|
||||
[
|
||||
"traceId",
|
||||
"name",
|
||||
"model",
|
||||
"startTime",
|
||||
"endTime",
|
||||
"cost",
|
||||
"prompt",
|
||||
"completion",
|
||||
"metadata",
|
||||
],
|
||||
]
|
||||
.concat(
|
||||
generations.map((generation) =>
|
||||
[
|
||||
generation.traceId,
|
||||
generation.name ?? "",
|
||||
generation.model ?? "",
|
||||
generation.startTime.toISOString(),
|
||||
generation.endTime?.toISOString() ?? "",
|
||||
generation.calculatedTotalCost
|
||||
? usdFormatter(
|
||||
generation.calculatedTotalCost.toNumber(),
|
||||
2,
|
||||
8,
|
||||
)
|
||||
: "",
|
||||
JSON.stringify(generation.input),
|
||||
JSON.stringify(generation.output),
|
||||
JSON.stringify(generation.metadata),
|
||||
].map((field) => {
|
||||
const str = typeof field === "string" ? field : String(field);
|
||||
return `"${str.replace(/"/g, '""')}"`;
|
||||
}),
|
||||
),
|
||||
)
|
||||
.map((row) => row.join(","))
|
||||
.join("\n");
|
||||
break;
|
||||
case "JSON":
|
||||
output = JSON.stringify(generations);
|
||||
break;
|
||||
case "OPENAI-JSONL":
|
||||
const inputSchemaOpenAI = z.array(
|
||||
z.object({
|
||||
role: z.enum(["system", "user", "assistant"]),
|
||||
content: z.string(),
|
||||
}),
|
||||
);
|
||||
const outputSchema = z
|
||||
.object({
|
||||
completion: jsonSchema,
|
||||
})
|
||||
.or(jsonSchema);
|
||||
output = generations
|
||||
.map((generation) => ({
|
||||
parsedInput: inputSchemaOpenAI.safeParse(generation.input),
|
||||
parsedOutput: outputSchema.safeParse(generation.output),
|
||||
}))
|
||||
.filter((generation) => generation.parsedInput.success)
|
||||
.map((generation) =>
|
||||
generation.parsedInput.success // check for typescript validation, is always true due to previous filter
|
||||
? generation.parsedInput.data.concat(
|
||||
generation.parsedOutput.success
|
||||
? [
|
||||
{
|
||||
role: "assistant",
|
||||
content:
|
||||
typeof generation.parsedOutput.data ===
|
||||
"object" &&
|
||||
"completion" in generation.parsedOutput.data
|
||||
? JSON.stringify(
|
||||
generation.parsedOutput.data.completion,
|
||||
)
|
||||
: JSON.stringify(generation.parsedOutput.data),
|
||||
},
|
||||
]
|
||||
: [],
|
||||
)
|
||||
: [],
|
||||
)
|
||||
// to jsonl
|
||||
.map((row) => JSON.stringify(row))
|
||||
.join("\n");
|
||||
|
||||
break;
|
||||
default:
|
||||
throw new Error("Invalid export file format");
|
||||
}
|
||||
|
||||
const fileName = `lf-export-${
|
||||
input.projectId
|
||||
}-${new Date().toISOString()}.${
|
||||
exportOptions[input.fileFormat].extension
|
||||
}`;
|
||||
|
||||
if (
|
||||
env.S3_BUCKET_NAME &&
|
||||
env.S3_ACCESS_KEY_ID &&
|
||||
env.S3_SECRET_ACCESS_KEY &&
|
||||
env.S3_ENDPOINT &&
|
||||
env.S3_REGION
|
||||
) {
|
||||
const client = new S3Client({
|
||||
credentials: {
|
||||
accessKeyId: env.S3_ACCESS_KEY_ID,
|
||||
secretAccessKey: env.S3_SECRET_ACCESS_KEY,
|
||||
},
|
||||
endpoint: env.S3_ENDPOINT,
|
||||
region: env.S3_REGION,
|
||||
});
|
||||
await client.send(
|
||||
new PutObjectCommand({
|
||||
Bucket: env.S3_BUCKET_NAME,
|
||||
Key: fileName,
|
||||
Body: output,
|
||||
ContentType: exportOptions[input.fileFormat].fileType,
|
||||
}),
|
||||
);
|
||||
const signedUrl = await getSignedUrl(
|
||||
client,
|
||||
new GetObjectCommand({
|
||||
Bucket: env.S3_BUCKET_NAME,
|
||||
Key: fileName,
|
||||
ResponseContentDisposition: `attachment; filename="${fileName}"`,
|
||||
}),
|
||||
{
|
||||
expiresIn: 60 * 60, // in 1 hour, signed url will expire
|
||||
},
|
||||
);
|
||||
return {
|
||||
type: "s3",
|
||||
url: signedUrl,
|
||||
fileName,
|
||||
} as const;
|
||||
} else {
|
||||
return {
|
||||
type: "data",
|
||||
data: output,
|
||||
fileName,
|
||||
} as const;
|
||||
}
|
||||
}),
|
||||
filterOptions: protectedProjectProcedure
|
||||
.input(z.object({ projectId: z.string() }))
|
||||
.query(async ({ input, ctx }) => {
|
||||
const queryFilter = {
|
||||
projectId: input.projectId,
|
||||
type: "GENERATION",
|
||||
} as const;
|
||||
|
||||
const scores = await ctx.prisma.score.groupBy({
|
||||
where: {
|
||||
observation: {
|
||||
projectId: input.projectId,
|
||||
},
|
||||
},
|
||||
by: ["name"],
|
||||
});
|
||||
|
||||
const model = await ctx.prisma.observation.groupBy({
|
||||
by: ["model"],
|
||||
where: queryFilter,
|
||||
_count: { _all: true },
|
||||
});
|
||||
const name = await ctx.prisma.observation.groupBy({
|
||||
by: ["name"],
|
||||
where: queryFilter,
|
||||
_count: { _all: true },
|
||||
});
|
||||
const traceName = await ctx.prisma.$queryRaw<
|
||||
Array<{
|
||||
traceName: string | null;
|
||||
count: number;
|
||||
}>
|
||||
>(Prisma.sql`
|
||||
SELECT
|
||||
t.name "traceName",
|
||||
count(*)::int AS count
|
||||
FROM traces t
|
||||
JOIN observations o ON o.trace_id = t.id
|
||||
WHERE o.type = 'GENERATION'
|
||||
AND o.project_id = ${input.projectId}
|
||||
AND t.project_id = ${input.projectId}
|
||||
GROUP BY 1
|
||||
`);
|
||||
|
||||
// typecheck filter options, needs to include all columns with options
|
||||
const res: ObservationOptions = {
|
||||
model: model
|
||||
.filter((i) => i.model !== null)
|
||||
.map((i) => ({
|
||||
value: i.model as string,
|
||||
count: i._count._all,
|
||||
})),
|
||||
name: name
|
||||
.filter((i) => i.name !== null)
|
||||
.map((i) => ({
|
||||
value: i.name as string,
|
||||
count: i._count._all,
|
||||
})),
|
||||
traceName: traceName
|
||||
.filter((i) => i.traceName !== null)
|
||||
.map((i) => ({
|
||||
value: i.traceName as string,
|
||||
count: i.count,
|
||||
})),
|
||||
scores_avg: scores.map((score) => score.name),
|
||||
};
|
||||
return res;
|
||||
}),
|
||||
});
|
||||
@@ -1,16 +0,0 @@
|
||||
import { PrismaClient } from "@prisma/client";
|
||||
|
||||
import { env } from "@/src/env.mjs";
|
||||
|
||||
const globalForPrisma = globalThis as unknown as {
|
||||
prisma: PrismaClient | undefined;
|
||||
};
|
||||
|
||||
export const prisma =
|
||||
globalForPrisma.prisma ??
|
||||
new PrismaClient({
|
||||
log:
|
||||
env.NODE_ENV === "development" ? ["query", "error", "warn"] : ["error"],
|
||||
});
|
||||
|
||||
if (env.NODE_ENV !== "production") globalForPrisma.prisma = prisma;
|
||||
@@ -1,8 +0,0 @@
|
||||
export class ResourceNotFoundError extends Error {
|
||||
id: string;
|
||||
constructor(type: string, id: string) {
|
||||
super(`${type} with ${id} not found}`);
|
||||
this.name = "ResourceNotFoundError";
|
||||
this.id = id;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
Dockerfile
|
||||
.dockerignore
|
||||
node_modules
|
||||
npm-debug.log
|
||||
README.md
|
||||
.next
|
||||
.git
|
||||
@@ -44,4 +44,4 @@ const config = {
|
||||
},
|
||||
};
|
||||
|
||||
module.exports = config;
|
||||
module.exports = config;
|
||||
@@ -0,0 +1,55 @@
|
||||
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
|
||||
|
||||
# dependencies
|
||||
/node_modules
|
||||
/.pnp
|
||||
.pnp.js
|
||||
|
||||
# testing
|
||||
/coverage
|
||||
|
||||
# database
|
||||
/prisma/db.sqlite
|
||||
/prisma/db.sqlite-journal
|
||||
|
||||
# next.js
|
||||
/.next/
|
||||
/out/
|
||||
next-env.d.ts
|
||||
|
||||
# production
|
||||
/build
|
||||
|
||||
# idea
|
||||
.idea
|
||||
|
||||
# misc
|
||||
.DS_Store
|
||||
*.pem
|
||||
|
||||
# debug
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
.pnpm-debug.log*
|
||||
|
||||
# local env files
|
||||
# do not commit any .env files to git, except for the .env.example file. https://create.t3.gg/en/usage/env-variables#using-environment-variables
|
||||
.env
|
||||
.env.local
|
||||
.env*.local
|
||||
|
||||
# vercel
|
||||
.vercel
|
||||
|
||||
# typescript
|
||||
*.tsbuildinfo
|
||||
|
||||
/generated/typescript-server
|
||||
|
||||
# openapi spec that is copied during build
|
||||
/public/openapi*.yml
|
||||
|
||||
|
||||
# vscode
|
||||
.devcontainer
|
||||
@@ -1,19 +1,11 @@
|
||||
# Base image
|
||||
FROM node:20-alpine AS base
|
||||
ARG DATABASE_URL
|
||||
ARG NEXTAUTH_SECRET
|
||||
ARG NEXTAUTH_URL
|
||||
ARG SALT
|
||||
|
||||
# It's important to update the index before installing packages to ensure you're getting the latest versions.
|
||||
# Check https://github.com/nodejs/docker-node/tree/b4117f9333da4138b03a546ec926ef50a31506c3#nodealpine to understand why libc6-compat might be needed.
|
||||
RUN apk update && apk upgrade --no-cache libcrypto3 libssl3 libc6-compat
|
||||
|
||||
FROM base AS deps
|
||||
ARG DATABASE_URL
|
||||
ARG NEXTAUTH_SECRET
|
||||
ARG NEXTAUTH_URL
|
||||
ARG SALT
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
@@ -29,10 +21,6 @@ RUN \
|
||||
|
||||
# Rebuild the source code only when needed
|
||||
FROM base AS builder
|
||||
ARG DATABASE_URL
|
||||
ARG NEXTAUTH_SECRET
|
||||
ARG NEXTAUTH_URL
|
||||
ARG SALT
|
||||
|
||||
WORKDIR /app
|
||||
COPY --from=deps /app/node_modules ./node_modules
|
||||
@@ -46,6 +34,9 @@ RUN rm -f ./src/middleware.ts
|
||||
# Uncomment the following line in case you want to disable telemetry during the build.
|
||||
ENV NEXT_TELEMETRY_DISABLED 1
|
||||
|
||||
# Disable validation of environment variables during build
|
||||
ENV DOCKER_BUILD 1
|
||||
|
||||
# Generate prisma client
|
||||
RUN npx prisma generate
|
||||
|
||||
@@ -54,10 +45,6 @@ RUN npm run build
|
||||
|
||||
# Production image, copy all the files and run next
|
||||
FROM base AS runner
|
||||
ARG DATABASE_URL
|
||||
ARG NEXTAUTH_SECRET
|
||||
ARG NEXTAUTH_URL
|
||||
ARG SALT
|
||||
|
||||
RUN apk add --no-cache dumb-init
|
||||
|
||||
@@ -66,6 +53,8 @@ WORKDIR /app
|
||||
ENV NODE_ENV production
|
||||
# Uncomment the following line in case you want to disable telemetry during runtime.
|
||||
ENV NEXT_TELEMETRY_DISABLED 1
|
||||
# Needed to re-enable validation of environment variables during runtime
|
||||
ENV DOCKER_BUILD 0
|
||||
|
||||
RUN addgroup --system --gid 1001 nodejs
|
||||
RUN adduser --system --uid 1001 nextjs
|
||||
@@ -0,0 +1,104 @@
|
||||
openapi: 3.0.1
|
||||
info:
|
||||
title: langfuse
|
||||
version: ''
|
||||
paths:
|
||||
/api/public/scores:
|
||||
post:
|
||||
description: Add a score to the database, upserts on id
|
||||
operationId: score_create
|
||||
tags:
|
||||
- Score
|
||||
parameters: []
|
||||
responses:
|
||||
'200':
|
||||
description: ''
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/Score'
|
||||
'400':
|
||||
description: ''
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: string
|
||||
'401':
|
||||
description: ''
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: string
|
||||
'403':
|
||||
description: ''
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: string
|
||||
'405':
|
||||
description: ''
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: string
|
||||
security:
|
||||
- BearerAuth: []
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/CreateScoreRequest'
|
||||
components:
|
||||
schemas:
|
||||
CreateScoreRequest:
|
||||
title: CreateScoreRequest
|
||||
type: object
|
||||
properties:
|
||||
id:
|
||||
type: string
|
||||
traceId:
|
||||
type: string
|
||||
name:
|
||||
type: string
|
||||
value:
|
||||
type: number
|
||||
format: double
|
||||
observationId:
|
||||
type: string
|
||||
comment:
|
||||
type: string
|
||||
required:
|
||||
- traceId
|
||||
- name
|
||||
- value
|
||||
Score:
|
||||
title: Score
|
||||
type: object
|
||||
properties:
|
||||
id:
|
||||
type: string
|
||||
traceId:
|
||||
type: string
|
||||
name:
|
||||
type: string
|
||||
value:
|
||||
type: number
|
||||
format: double
|
||||
observationId:
|
||||
type: string
|
||||
timestamp:
|
||||
type: string
|
||||
format: date-time
|
||||
comment:
|
||||
type: string
|
||||
required:
|
||||
- id
|
||||
- traceId
|
||||
- name
|
||||
- value
|
||||
- timestamp
|
||||
securitySchemes:
|
||||
BearerAuth:
|
||||
type: http
|
||||
scheme: bearer
|
||||
@@ -34,26 +34,26 @@ const nextConfig = {
|
||||
],
|
||||
},
|
||||
// Required to check authentication status from langfuse.com
|
||||
...(env.NEXT_PUBLIC_LANGFUSE_CLOUD_REGION !== undefined ?
|
||||
[
|
||||
{
|
||||
source: "/api/auth/session",
|
||||
headers: [
|
||||
{
|
||||
key: "Access-Control-Allow-Origin",
|
||||
value: "https://langfuse.com",
|
||||
},
|
||||
{ key: "Access-Control-Allow-Credentials", value: "true" },
|
||||
{ key: "Access-Control-Allow-Methods", value: "GET,POST" },
|
||||
{
|
||||
key: "Access-Control-Allow-Headers",
|
||||
value: "Content-Type, Authorization",
|
||||
},
|
||||
]
|
||||
},
|
||||
] : []
|
||||
)
|
||||
]
|
||||
...(env.NEXT_PUBLIC_LANGFUSE_CLOUD_REGION !== undefined
|
||||
? [
|
||||
{
|
||||
source: "/api/auth/session",
|
||||
headers: [
|
||||
{
|
||||
key: "Access-Control-Allow-Origin",
|
||||
value: "https://langfuse.com",
|
||||
},
|
||||
{ key: "Access-Control-Allow-Credentials", value: "true" },
|
||||
{ key: "Access-Control-Allow-Methods", value: "GET,POST" },
|
||||
{
|
||||
key: "Access-Control-Allow-Headers",
|
||||
value: "Content-Type, Authorization",
|
||||
},
|
||||
],
|
||||
},
|
||||
]
|
||||
: []),
|
||||
];
|
||||
},
|
||||
|
||||
// webassembly support for @dqbd/tiktoken
|
||||
@@ -0,0 +1,180 @@
|
||||
{
|
||||
"name": "langfuse-core",
|
||||
"version": "2.11.2",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"prebuild": "cp generated/openapi-client/openapi.yml public/openapi-client.yml && cp generated/openapi-server/openapi.yml public/openapi-server.yml",
|
||||
"build": "next build",
|
||||
"dev": "dotenv -e ../.env -- next dev",
|
||||
"dx": "dotenv -e ../.env -- npm i && npm run db:reset && npm run db:seed:examples && npm run dev",
|
||||
"postinstall": "dotenv -e ../.env -- prisma generate",
|
||||
"lint": "dotenv -e ../.env -- next lint",
|
||||
"lint:fix": "dotenv -e ../.env -- next lint --fix",
|
||||
"prettier": "prettier --write ./src *.{ts,js}",
|
||||
"clean": "rm -rf node_modules",
|
||||
"start": "next start",
|
||||
"test": "dotenv -e ../.env -- jest --runInBand",
|
||||
"test:watch": "dotenv -e ../.env -- jest --watch --runInBand",
|
||||
"test:e2e": "dotenv -e ../.env -- playwright test",
|
||||
"db:migrate": "DISABLE_ERD=false dotenv -e ../.env -- npx prisma migrate dev && npm run db:worker",
|
||||
"db:reset": "dotenv -e ../.env npx -- prisma migrate reset",
|
||||
"db:deploy": "dotenv -e ../.env npx -- prisma migrate deploy",
|
||||
"db:seed": "dotenv -e ../.env -- npx prisma db seed",
|
||||
"db:seed:examples": "dotenv -e ../.env -- npx prisma db seed -- --environment examples",
|
||||
"db:seed:load": "dotenv -e ../.env -- npx prisma db seed -- --environment load",
|
||||
"release": "dotenv -e ../.env -- release-it",
|
||||
"models:migrate": "dotenv -e ../.env -- tsx scripts/model-match.ts",
|
||||
"db:worker": "cp -r prisma/generated ../worker"
|
||||
},
|
||||
"prisma": {
|
||||
"seed": "ts-node -r tsconfig-paths/register -r dotenv/config --compiler-options {\"module\":\"CommonJS\"} prisma/seed.ts"
|
||||
},
|
||||
"dependencies": {
|
||||
"@anthropic-ai/tokenizer": "^0.0.4",
|
||||
"@aws-sdk/client-s3": "^3.507.0",
|
||||
"@aws-sdk/lib-storage": "^3.515.0",
|
||||
"@aws-sdk/s3-request-presigner": "^3.507.0",
|
||||
"@headlessui/react": "^1.7.18",
|
||||
"@heroicons/react": "^2.1.1",
|
||||
"@hookform/resolvers": "^3.3.4",
|
||||
"@next-auth/prisma-adapter": "^1.0.7",
|
||||
"@prisma/client": "^5.10.2",
|
||||
"@radix-ui/react-accordion": "^1.1.2",
|
||||
"@radix-ui/react-alert-dialog": "^1.0.5",
|
||||
"@radix-ui/react-avatar": "^1.0.4",
|
||||
"@radix-ui/react-checkbox": "^1.0.4",
|
||||
"@radix-ui/react-collapsible": "^1.0.3",
|
||||
"@radix-ui/react-dialog": "^1.0.5",
|
||||
"@radix-ui/react-dropdown-menu": "^2.0.6",
|
||||
"@radix-ui/react-hover-card": "^1.0.7",
|
||||
"@radix-ui/react-icons": "^1.3.0",
|
||||
"@radix-ui/react-label": "^2.0.2",
|
||||
"@radix-ui/react-popover": "^1.0.7",
|
||||
"@radix-ui/react-scroll-area": "^1.0.5",
|
||||
"@radix-ui/react-select": "^2.0.0",
|
||||
"@radix-ui/react-separator": "^1.0.3",
|
||||
"@radix-ui/react-slider": "^1.1.2",
|
||||
"@radix-ui/react-slot": "^1.0.2",
|
||||
"@radix-ui/react-switch": "^1.0.3",
|
||||
"@radix-ui/react-tabs": "^1.0.4",
|
||||
"@radix-ui/react-toggle": "^1.0.3",
|
||||
"@radix-ui/react-tooltip": "^1.0.7",
|
||||
"@react-email/components": "^0.0.14",
|
||||
"@react-email/render": "^0.0.12",
|
||||
"@sentry/nextjs": "^7.102.1",
|
||||
"@sentry/profiling-node": "^7.102.1",
|
||||
"@sentry/types": "^7.88.0",
|
||||
"@t3-oss/env-nextjs": "^0.8.0",
|
||||
"@tailwindcss/forms": "^0.5.7",
|
||||
"@tanstack/react-query": "^4.36.1",
|
||||
"@tanstack/react-table": "^8.11.8",
|
||||
"@tremor/react": "^3.11.1",
|
||||
"@trpc/client": "^10.45.0",
|
||||
"@trpc/next": "^10.45.0",
|
||||
"@trpc/react-query": "^10.45.0",
|
||||
"@trpc/server": "^10.45.0",
|
||||
"bcryptjs": "^2.4.3",
|
||||
"class-variance-authority": "^0.7.0",
|
||||
"clsx": "^2.1.0",
|
||||
"cmdk": "^0.2.1",
|
||||
"core-js": "^3.36.0",
|
||||
"cors": "^2.8.5",
|
||||
"date-fns": "^3.3.1",
|
||||
"decimal.js": "^10.4.3",
|
||||
"exponential-backoff": "^3.1.1",
|
||||
"js-tiktoken": "^1.0.10",
|
||||
"kysely": "^0.27.2",
|
||||
"lodash": "^4.17.21",
|
||||
"lucide-react": "^0.330.0",
|
||||
"next": "^14.1.0",
|
||||
"next-auth": "^4.24.6",
|
||||
"next-query-params": "^5.0.0",
|
||||
"nodemailer": "^6.9.9",
|
||||
"posthog-js": "^1.105.9",
|
||||
"posthog-node": "^3.6.3",
|
||||
"prisma-kysely": "^1.8.0",
|
||||
"react": "18.2.0",
|
||||
"react-day-picker": "^8.10.0",
|
||||
"react-dom": "18.2.0",
|
||||
"react-hook-form": "^7.50.1",
|
||||
"react-icons": "^5.0.1",
|
||||
"react-responsive": "^9.0.2",
|
||||
"react18-json-view": "^0.2.8-canary.1",
|
||||
"sonner": "^1.4.0",
|
||||
"superjson": "2.2.1",
|
||||
"tailwind-merge": "^2.2.1",
|
||||
"tailwindcss-animate": "^1.0.7",
|
||||
"use-query-params": "^2.2.1",
|
||||
"uuid": "^9.0.1",
|
||||
"zod": "^3.22.4"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@jedmao/location": "^3.0.0",
|
||||
"@mermaid-js/mermaid-cli": "^10.7.0",
|
||||
"@playwright/test": "^1.41.2",
|
||||
"@release-it/bumper": "^6.0.1",
|
||||
"@testing-library/jest-dom": "^6.4.2",
|
||||
"@testing-library/react": "^14.2.1",
|
||||
"@types/bcryptjs": "^2.4.6",
|
||||
"@types/cors": "^2.8.17",
|
||||
"@types/eslint": "^8.56.2",
|
||||
"@types/jest": "^29.5.12",
|
||||
"@types/lodash": "^4.14.202",
|
||||
"@types/node": "20.10.5",
|
||||
"@types/nodemailer": "^6.4.14",
|
||||
"@types/react": "^18.2.55",
|
||||
"@types/react-dom": "^18.2.19",
|
||||
"@types/uuid": "^9.0.8",
|
||||
"@typescript-eslint/eslint-plugin": "^6.21.0",
|
||||
"@typescript-eslint/parser": "^6.21.0",
|
||||
"autoprefixer": "^10.4.17",
|
||||
"dotenv-cli": "^7.3.0",
|
||||
"eslint": "^8.56.0",
|
||||
"eslint-config-next": "^14.1.0",
|
||||
"jest": "^29.7.0",
|
||||
"jest-environment-jsdom": "^29.7.0",
|
||||
"node-mocks-http": "^1.14.1",
|
||||
"postcss": "^8.4.35",
|
||||
"prettier": "^3.2.5",
|
||||
"prettier-plugin-tailwindcss": "^0.5.11",
|
||||
"prisma": "^5.10.2",
|
||||
"prisma-erd-generator": "^1.11.2",
|
||||
"release-it": "^17.1.1",
|
||||
"tailwindcss": "^3.4.1",
|
||||
"ts-node": "^10.9.2",
|
||||
"tsconfig-paths": "^4.2.0",
|
||||
"tsx": "^4.7.1",
|
||||
"typescript": "^5.3.3"
|
||||
},
|
||||
"ct3aMetadata": {
|
||||
"initVersion": "7.13.0"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"crisp-sdk-web": "^1.0.21"
|
||||
},
|
||||
"release-it": {
|
||||
"git": {
|
||||
"commitMessage": "chore: release v${version}",
|
||||
"tagName": "v${version}"
|
||||
},
|
||||
"github": {
|
||||
"release": true,
|
||||
"web": true,
|
||||
"autoGenerate": true,
|
||||
"releaseName": "v${version}",
|
||||
"comments": {
|
||||
"submit": true,
|
||||
"issue": ":rocket: _This issue has been resolved in v${version}. See [${releaseName}](${releaseUrl}) for release notes._",
|
||||
"pr": ":rocket: _This pull request is included in v${version}. See [${releaseName}](${releaseUrl}) for release notes._"
|
||||
}
|
||||
},
|
||||
"plugins": {
|
||||
"@release-it/bumper": {
|
||||
"out": {
|
||||
"file": "./src/constants/VERSION.ts",
|
||||
"type": "application/typescript"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
Before Width: | Height: | Size: 194 KiB After Width: | Height: | Size: 194 KiB |
@@ -0,0 +1,345 @@
|
||||
import type { ColumnType } from "kysely";
|
||||
export type Generated<T> = T extends ColumnType<infer S, infer I, infer U>
|
||||
? ColumnType<S, I | undefined, U>
|
||||
: ColumnType<T, T | undefined, T>;
|
||||
export type Timestamp = ColumnType<Date, Date | string, Date | string>;
|
||||
|
||||
export const MembershipRole = {
|
||||
OWNER: "OWNER",
|
||||
ADMIN: "ADMIN",
|
||||
MEMBER: "MEMBER",
|
||||
VIEWER: "VIEWER"
|
||||
} as const;
|
||||
export type MembershipRole = (typeof MembershipRole)[keyof typeof MembershipRole];
|
||||
export const ObservationType = {
|
||||
SPAN: "SPAN",
|
||||
EVENT: "EVENT",
|
||||
GENERATION: "GENERATION"
|
||||
} as const;
|
||||
export type ObservationType = (typeof ObservationType)[keyof typeof ObservationType];
|
||||
export const ObservationLevel = {
|
||||
DEBUG: "DEBUG",
|
||||
DEFAULT: "DEFAULT",
|
||||
WARNING: "WARNING",
|
||||
ERROR: "ERROR"
|
||||
} as const;
|
||||
export type ObservationLevel = (typeof ObservationLevel)[keyof typeof ObservationLevel];
|
||||
export const ScoreSource = {
|
||||
API: "API",
|
||||
REVIEW: "REVIEW"
|
||||
} as const;
|
||||
export type ScoreSource = (typeof ScoreSource)[keyof typeof ScoreSource];
|
||||
export const PricingUnit = {
|
||||
PER_1000_TOKENS: "PER_1000_TOKENS",
|
||||
PER_1000_CHARS: "PER_1000_CHARS"
|
||||
} as const;
|
||||
export type PricingUnit = (typeof PricingUnit)[keyof typeof PricingUnit];
|
||||
export const TokenType = {
|
||||
PROMPT: "PROMPT",
|
||||
COMPLETION: "COMPLETION",
|
||||
TOTAL: "TOTAL"
|
||||
} as const;
|
||||
export type TokenType = (typeof TokenType)[keyof typeof TokenType];
|
||||
export const DatasetStatus = {
|
||||
ACTIVE: "ACTIVE",
|
||||
ARCHIVED: "ARCHIVED"
|
||||
} as const;
|
||||
export type DatasetStatus = (typeof DatasetStatus)[keyof typeof DatasetStatus];
|
||||
export type Account = {
|
||||
id: string;
|
||||
user_id: string;
|
||||
type: string;
|
||||
provider: string;
|
||||
providerAccountId: string;
|
||||
refresh_token: string | null;
|
||||
access_token: string | null;
|
||||
expires_at: number | null;
|
||||
expires_in: number | null;
|
||||
ext_expires_in: number | null;
|
||||
token_type: string | null;
|
||||
scope: string | null;
|
||||
id_token: string | null;
|
||||
session_state: string | null;
|
||||
};
|
||||
export type ApiKey = {
|
||||
id: string;
|
||||
created_at: Generated<Timestamp>;
|
||||
note: string | null;
|
||||
public_key: string;
|
||||
hashed_secret_key: string;
|
||||
fast_hashed_secret_key: string | null;
|
||||
display_secret_key: string;
|
||||
last_used_at: Timestamp | null;
|
||||
expires_at: Timestamp | null;
|
||||
project_id: string;
|
||||
};
|
||||
export type AuditLog = {
|
||||
id: string;
|
||||
created_at: Generated<Timestamp>;
|
||||
updated_at: Generated<Timestamp>;
|
||||
user_id: string;
|
||||
project_id: string;
|
||||
user_project_role: MembershipRole;
|
||||
resource_type: string;
|
||||
resource_id: string;
|
||||
action: string;
|
||||
before: string | null;
|
||||
after: string | null;
|
||||
};
|
||||
export type CronJobs = {
|
||||
name: string;
|
||||
last_run: Timestamp | null;
|
||||
job_started_at: Timestamp | null;
|
||||
state: string | null;
|
||||
};
|
||||
export type Dataset = {
|
||||
id: string;
|
||||
name: string;
|
||||
project_id: string;
|
||||
created_at: Generated<Timestamp>;
|
||||
updated_at: Generated<Timestamp>;
|
||||
};
|
||||
export type DatasetItem = {
|
||||
id: string;
|
||||
status: Generated<DatasetStatus>;
|
||||
input: unknown;
|
||||
expected_output: unknown | null;
|
||||
source_observation_id: string | null;
|
||||
dataset_id: string;
|
||||
created_at: Generated<Timestamp>;
|
||||
updated_at: Generated<Timestamp>;
|
||||
};
|
||||
export type DatasetRunItems = {
|
||||
id: string;
|
||||
dataset_run_id: string;
|
||||
dataset_item_id: string;
|
||||
observation_id: string;
|
||||
created_at: Generated<Timestamp>;
|
||||
updated_at: Generated<Timestamp>;
|
||||
};
|
||||
export type DatasetRuns = {
|
||||
id: string;
|
||||
name: string;
|
||||
dataset_id: string;
|
||||
created_at: Generated<Timestamp>;
|
||||
updated_at: Generated<Timestamp>;
|
||||
};
|
||||
export type Events = {
|
||||
id: string;
|
||||
created_at: Generated<Timestamp>;
|
||||
updated_at: Generated<Timestamp>;
|
||||
project_id: string;
|
||||
data: unknown;
|
||||
headers: Generated<unknown>;
|
||||
url: string | null;
|
||||
method: string | null;
|
||||
};
|
||||
export type Example = {
|
||||
id: string;
|
||||
created_at: Generated<Timestamp>;
|
||||
updated_at: Timestamp;
|
||||
};
|
||||
export type Membership = {
|
||||
project_id: string;
|
||||
user_id: string;
|
||||
role: MembershipRole;
|
||||
created_at: Generated<Timestamp>;
|
||||
updated_at: Generated<Timestamp>;
|
||||
};
|
||||
export type MembershipInvitation = {
|
||||
id: string;
|
||||
email: string;
|
||||
role: MembershipRole;
|
||||
project_id: string;
|
||||
sender_id: string | null;
|
||||
created_at: Generated<Timestamp>;
|
||||
updated_at: Generated<Timestamp>;
|
||||
};
|
||||
export type Model = {
|
||||
id: string;
|
||||
created_at: Generated<Timestamp>;
|
||||
updated_at: Generated<Timestamp>;
|
||||
project_id: string | null;
|
||||
model_name: string;
|
||||
match_pattern: string;
|
||||
start_date: Timestamp | null;
|
||||
input_price: string | null;
|
||||
output_price: string | null;
|
||||
total_price: string | null;
|
||||
unit: string;
|
||||
tokenizer_id: string | null;
|
||||
tokenizer_config: unknown | null;
|
||||
};
|
||||
export type Observation = {
|
||||
id: string;
|
||||
trace_id: string | null;
|
||||
project_id: string;
|
||||
type: ObservationType;
|
||||
start_time: Generated<Timestamp>;
|
||||
end_time: Timestamp | null;
|
||||
name: string | null;
|
||||
metadata: unknown | null;
|
||||
parent_observation_id: string | null;
|
||||
level: Generated<ObservationLevel>;
|
||||
status_message: string | null;
|
||||
version: string | null;
|
||||
created_at: Generated<Timestamp>;
|
||||
model: string | null;
|
||||
internal_model: string | null;
|
||||
modelParameters: unknown | null;
|
||||
input: unknown | null;
|
||||
output: unknown | null;
|
||||
prompt_tokens: Generated<number>;
|
||||
completion_tokens: Generated<number>;
|
||||
total_tokens: Generated<number>;
|
||||
unit: string | null;
|
||||
input_cost: string | null;
|
||||
output_cost: string | null;
|
||||
total_cost: string | null;
|
||||
completion_start_time: Timestamp | null;
|
||||
prompt_id: string | null;
|
||||
};
|
||||
export type ObservationView = {
|
||||
id: string;
|
||||
trace_id: string | null;
|
||||
project_id: string;
|
||||
type: ObservationType;
|
||||
start_time: Generated<Timestamp>;
|
||||
end_time: Timestamp | null;
|
||||
name: string | null;
|
||||
metadata: unknown | null;
|
||||
parent_observation_id: string | null;
|
||||
level: Generated<ObservationLevel>;
|
||||
status_message: string | null;
|
||||
version: string | null;
|
||||
created_at: Generated<Timestamp>;
|
||||
model: string | null;
|
||||
modelParameters: unknown | null;
|
||||
input: unknown | null;
|
||||
output: unknown | null;
|
||||
prompt_tokens: Generated<number>;
|
||||
completion_tokens: Generated<number>;
|
||||
total_tokens: Generated<number>;
|
||||
unit: string | null;
|
||||
completion_start_time: Timestamp | null;
|
||||
prompt_id: string | null;
|
||||
model_id: string | null;
|
||||
input_price: string | null;
|
||||
output_price: string | null;
|
||||
total_price: string | null;
|
||||
calculated_input_cost: string | null;
|
||||
calculated_output_cost: string | null;
|
||||
calculated_total_cost: string | null;
|
||||
latency: number | null;
|
||||
};
|
||||
export type Pricing = {
|
||||
id: string;
|
||||
model_name: string;
|
||||
pricing_unit: Generated<PricingUnit>;
|
||||
price: string;
|
||||
currency: Generated<string>;
|
||||
token_type: TokenType;
|
||||
};
|
||||
export type Project = {
|
||||
id: string;
|
||||
created_at: Generated<Timestamp>;
|
||||
updated_at: Generated<Timestamp>;
|
||||
name: string;
|
||||
cloud_config: unknown | null;
|
||||
};
|
||||
export type Prompt = {
|
||||
id: string;
|
||||
created_at: Generated<Timestamp>;
|
||||
updated_at: Generated<Timestamp>;
|
||||
project_id: string;
|
||||
created_by: string;
|
||||
prompt: string;
|
||||
name: string;
|
||||
version: number;
|
||||
is_active: boolean;
|
||||
config: Generated<unknown>;
|
||||
};
|
||||
export type Score = {
|
||||
id: string;
|
||||
timestamp: Generated<Timestamp>;
|
||||
name: string;
|
||||
value: number;
|
||||
source: ScoreSource;
|
||||
comment: string | null;
|
||||
trace_id: string;
|
||||
observation_id: string | null;
|
||||
};
|
||||
export type Session = {
|
||||
id: string;
|
||||
session_token: string;
|
||||
user_id: string;
|
||||
expires: Timestamp;
|
||||
};
|
||||
export type Trace = {
|
||||
id: string;
|
||||
external_id: string | null;
|
||||
timestamp: Generated<Timestamp>;
|
||||
name: string | null;
|
||||
user_id: string | null;
|
||||
metadata: unknown | null;
|
||||
release: string | null;
|
||||
version: string | null;
|
||||
project_id: string;
|
||||
public: Generated<boolean>;
|
||||
bookmarked: Generated<boolean>;
|
||||
tags: Generated<string[]>;
|
||||
input: unknown | null;
|
||||
output: unknown | null;
|
||||
session_id: string | null;
|
||||
};
|
||||
export type TraceSession = {
|
||||
id: string;
|
||||
created_at: Generated<Timestamp>;
|
||||
updated_at: Generated<Timestamp>;
|
||||
project_id: string;
|
||||
bookmarked: Generated<boolean>;
|
||||
public: Generated<boolean>;
|
||||
};
|
||||
export type User = {
|
||||
id: string;
|
||||
name: string | null;
|
||||
email: string | null;
|
||||
email_verified: Timestamp | null;
|
||||
password: string | null;
|
||||
image: string | null;
|
||||
admin: Generated<boolean>;
|
||||
created_at: Generated<Timestamp>;
|
||||
updated_at: Generated<Timestamp>;
|
||||
feature_flags: Generated<string[]>;
|
||||
};
|
||||
export type VerificationToken = {
|
||||
identifier: string;
|
||||
token: string;
|
||||
expires: Timestamp;
|
||||
};
|
||||
export type DB = {
|
||||
Account: Account;
|
||||
api_keys: ApiKey;
|
||||
audit_logs: AuditLog;
|
||||
cron_jobs: CronJobs;
|
||||
dataset_items: DatasetItem;
|
||||
dataset_run_items: DatasetRunItems;
|
||||
dataset_runs: DatasetRuns;
|
||||
datasets: Dataset;
|
||||
events: Events;
|
||||
Example: Example;
|
||||
membership_invitations: MembershipInvitation;
|
||||
memberships: Membership;
|
||||
models: Model;
|
||||
observations: Observation;
|
||||
observations_view: ObservationView;
|
||||
pricings: Pricing;
|
||||
projects: Project;
|
||||
prompts: Prompt;
|
||||
scores: Score;
|
||||
Session: Session;
|
||||
trace_sessions: TraceSession;
|
||||
traces: Trace;
|
||||
users: User;
|
||||
verification_tokens: VerificationToken;
|
||||
};
|
||||