Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
100ad2ecff | ||
|
|
617e396ba9 | ||
|
|
d1754b8d52 | ||
|
|
e4277eddcc | ||
|
|
1b4cd50c66 | ||
|
|
e30920406f | ||
|
|
94ed3a4b02 | ||
|
|
fc41347f39 | ||
|
|
80ed179fa6 | ||
|
|
7259baff17 | ||
|
|
68778ee4bc | ||
|
|
d1824a5e36 | ||
|
|
f30d6210dc | ||
|
|
bedc8b1e59 | ||
|
|
c3cc1d0a0d | ||
|
|
d570372e4a | ||
|
|
44825f5cb5 | ||
|
|
c9aa60f3b1 | ||
|
|
500d1dd0fa | ||
|
|
291b8f6420 | ||
|
|
8174e0153e | ||
|
|
88da95404f | ||
|
|
dcee2e9e39 | ||
|
|
cbd07b3d73 | ||
|
|
c5467ef614 | ||
|
|
e8d841ab43 | ||
|
|
4d0b5372e5 | ||
|
|
04dce2d7d9 | ||
|
|
9e47e12b6c | ||
|
|
f5328bc018 | ||
|
|
3cc6da2ff4 | ||
|
|
662e202041 | ||
|
|
6e6606f06e | ||
|
|
99562dbc6d | ||
|
|
60b5706c18 | ||
|
|
fb875bf26e | ||
|
|
695a2c01bc | ||
|
|
414c865fac | ||
|
|
3bf32dd981 | ||
|
|
2e58620f6e | ||
|
|
197125b4de | ||
|
|
cbd66df315 | ||
|
|
7681971dfa | ||
|
|
bb4cb1cb50 | ||
|
|
3695c70b92 | ||
|
|
9cbebbf4a0 | ||
|
|
8abc89b53f | ||
|
|
9667816ae0 | ||
|
|
246e844450 | ||
|
|
ca4fb68b53 | ||
|
|
02a83085e9 | ||
|
|
d9d9bc78a0 | ||
|
|
90cf22da84 | ||
|
|
0147836b41 | ||
|
|
ed11164d96 | ||
|
|
aa2369c4cb | ||
|
|
5ac1a18430 | ||
|
|
7c26edfb1e | ||
|
|
ced2fd3a40 | ||
|
|
7d328b404e | ||
|
|
3f77e7a091 | ||
|
|
4e02088135 | ||
|
|
24e165d595 | ||
|
|
212e88525e | ||
|
|
b520cad77b | ||
|
|
8fd5e5bfc1 | ||
|
|
3adc89e4d7 | ||
|
|
24195bb369 | ||
|
|
69c196c42d | ||
|
|
5d319e1fe3 | ||
|
|
c0120f7984 | ||
|
|
535bc2d2d7 | ||
|
|
a5db5aa940 | ||
|
|
f79a5cc52f | ||
|
|
44a8487410 | ||
|
|
a16f4a0526 | ||
|
|
60183f48fc | ||
|
|
2efaace4a8 | ||
|
|
ca24bebccd | ||
|
|
b7bb9eefe6 | ||
|
|
f2125ddc43 | ||
|
|
dc2789af31 | ||
|
|
39855914b5 | ||
|
|
a2b3ab9b93 | ||
|
|
2cdf13dc0d | ||
|
|
924d653993 | ||
|
|
e99b3a1e0e | ||
|
|
26ae2080d0 | ||
|
|
cd7f1b419f | ||
|
|
e835a9758e | ||
|
|
3d460b71f4 | ||
|
|
e33c6b42c9 | ||
|
|
80e441a211 | ||
|
|
6cbb93019e | ||
|
|
7f448db0ac | ||
|
|
5fae64e377 | ||
|
|
7aa0c4995c | ||
|
|
a8047b629b | ||
|
|
ae6acee9c9 | ||
|
|
1d0567426b | ||
|
|
d2deef409a | ||
|
|
240d4d7def | ||
|
|
99ffc45173 | ||
|
|
c6207c0f62 | ||
|
|
8499a38958 | ||
|
|
6f221302b7 | ||
|
|
f6ae7c5cd4 | ||
|
|
43b271ea4c | ||
|
|
b4fe4529d9 | ||
|
|
725a88f5ae | ||
|
|
1b2a2c01fa | ||
|
|
ae0857494b | ||
|
|
c278bc0ca1 | ||
|
|
5571ae376a | ||
|
|
c338b0409e | ||
|
|
95394b39aa | ||
|
|
1ebc413df4 | ||
|
|
12733ff7da | ||
|
|
41f064cb0f | ||
|
|
0e23a857c6 | ||
|
|
c77890c52a | ||
|
|
fa072d0074 | ||
|
|
0777afcd05 | ||
|
|
dba9fe80d1 | ||
|
|
a2e086d7e6 | ||
|
|
57537ad59e | ||
|
|
07e863f44a | ||
|
|
c50af94e91 | ||
|
|
22dade8808 | ||
|
|
41e6015580 | ||
|
|
c01bd573bb | ||
|
|
f45bc5ffe8 | ||
|
|
3224365277 | ||
|
|
6ec4eb4cf5 | ||
|
|
dacfb3b86e | ||
|
|
8f1b7e3db7 | ||
|
|
3570bbb22f | ||
|
|
5593d83f5b | ||
|
|
c64a9ba535 | ||
|
|
994b96e687 | ||
|
|
23f722bf7b | ||
|
|
d59b6a3b62 | ||
|
|
1857ae9d1e | ||
|
|
9d4d99010c | ||
|
|
1d3db1a6de | ||
|
|
1c629b9c0b | ||
|
|
9c799dfb33 | ||
|
|
009a932945 | ||
|
|
0b2cc19d92 | ||
|
|
64bd529c98 | ||
|
|
9fc7c990aa | ||
|
|
afbeb507db | ||
|
|
86af0efae7 | ||
|
|
be6ce949d6 | ||
|
|
09986a745a | ||
|
|
f1d6c26276 | ||
|
|
1515a49d53 | ||
|
|
0cec22ba56 | ||
|
|
dc23fa1d68 | ||
|
|
7d7ec1ed6c | ||
|
|
d18d2a0de9 | ||
|
|
51fd0edf80 | ||
|
|
a9621b2673 | ||
|
|
99ff3073b6 | ||
|
|
c04b990c49 | ||
|
|
4341215f70 | ||
|
|
e10102eb0b | ||
|
|
3bf5a351ef | ||
|
|
223f4571c9 | ||
|
|
ece6ee5144 | ||
|
|
66b850d365 | ||
|
|
dddc92e683 | ||
|
|
33022d7428 | ||
|
|
21de8052fa | ||
|
|
3d71892d16 | ||
|
|
6bcf0fad2c | ||
|
|
8df81dfdb5 | ||
|
|
080d73c7a5 | ||
|
|
2089a37ff5 | ||
|
|
8a90191156 | ||
|
|
9fe60a2971 | ||
|
|
0dd2a1ce0c |
@@ -50,6 +50,7 @@ Automatically activates when working on:
|
||||
- [ ] **Authentication**: Authorization via basic auth
|
||||
- [ ] **Validation**: Zod schemas for query/body/response
|
||||
- [ ] **Versioning**: Versioning in API path and Zod schemas for query/body/response
|
||||
- [ ] **Fern API Docs**: Update `fern/apis/server/definition/` to match TypeScript types
|
||||
- [ ] **Tests**: Add end-to-end test in `__tests__/async/`
|
||||
|
||||
### New Queue Processor Checklist (Worker)
|
||||
@@ -423,6 +424,32 @@ const traces = await queryClickhouse({
|
||||
});
|
||||
```
|
||||
|
||||
### 9. Keep Fern API Definitions in Sync with TypeScript Types
|
||||
|
||||
When modifying public API types in `web/src/features/public-api/types/`, the corresponding Fern API definitions in `fern/apis/server/definition/` must be updated to match.
|
||||
|
||||
**Zod to Fern Type Mapping:**
|
||||
|
||||
| Zod Type | Fern Type | Example |
|
||||
| -------- | --------- | ------- |
|
||||
| `.nullish()` | `optional<nullable<T>>` | `z.string().nullish()` → `optional<nullable<string>>` |
|
||||
| `.nullable()` | `nullable<T>` | `z.string().nullable()` → `nullable<string>` |
|
||||
| `.optional()` | `optional<T>` | `z.string().optional()` → `optional<string>` |
|
||||
| Always present | `T` | `z.string()` → `string` |
|
||||
|
||||
**Source References:**
|
||||
|
||||
Add a comment at the top of each Fern type referencing the TypeScript source file:
|
||||
|
||||
```yaml
|
||||
# Source: web/src/features/public-api/types/traces.ts - APITrace
|
||||
Trace:
|
||||
properties:
|
||||
id: string
|
||||
name:
|
||||
type: nullable<string>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Common Imports
|
||||
|
||||
@@ -645,7 +645,42 @@ async doIt()
|
||||
async execute()
|
||||
```
|
||||
|
||||
### 3. Return Types
|
||||
### 3. Use Params Objects for Multiple Arguments
|
||||
|
||||
When a function receives multiple arguments, use a single params object instead of positional arguments:
|
||||
|
||||
```typescript
|
||||
// ❌ BAD - Positional arguments are unclear and can be swapped
|
||||
async function createTrace(
|
||||
projectId: string,
|
||||
userId: string,
|
||||
sessionId: string,
|
||||
name: string,
|
||||
) {}
|
||||
|
||||
// Call site - which string is which?
|
||||
await createTrace(projectId, userId, sessionId, name);
|
||||
|
||||
// ✅ GOOD - Params object makes intent clear
|
||||
async function createTrace(params: {
|
||||
projectId: string;
|
||||
userId: string;
|
||||
sessionId: string;
|
||||
name: string;
|
||||
}) {}
|
||||
|
||||
// Call site - clear and prevents argument swapping bugs
|
||||
await createTrace({ projectId, userId, sessionId, name });
|
||||
```
|
||||
|
||||
**Benefits:**
|
||||
|
||||
- More readable at call sites
|
||||
- Prevents bugs when positional arguments of the same type are accidentally swapped
|
||||
- Easier to add optional parameters later
|
||||
- Self-documenting code
|
||||
|
||||
### 4. Return Types
|
||||
|
||||
Always use explicit return types:
|
||||
|
||||
@@ -659,7 +694,7 @@ async deleteUser(id: string): Promise<void> {}
|
||||
async createUser(data) {} // No types!
|
||||
```
|
||||
|
||||
### 4. Error Handling
|
||||
### 5. Error Handling
|
||||
|
||||
Services should throw meaningful errors:
|
||||
|
||||
@@ -679,7 +714,7 @@ if (!user) {
|
||||
}
|
||||
```
|
||||
|
||||
### 5. Avoid God Services
|
||||
### 6. Avoid God Services
|
||||
|
||||
Don't create services that do everything:
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
FROM mcr.microsoft.com/vscode/devcontainers/typescript-node:20-bookworm
|
||||
|
||||
# Install golang-migrate for database migrations
|
||||
RUN curl -L https://github.com/golang-migrate/migrate/releases/download/v4.19.0/migrate.linux-amd64.tar.gz | tar xvz && \
|
||||
RUN curl -L https://github.com/golang-migrate/migrate/releases/download/v4.19.1/migrate.linux-amd64.tar.gz | tar xvz && \
|
||||
chmod +x migrate && \
|
||||
mv migrate /usr/local/bin/migrate
|
||||
|
||||
|
||||
@@ -147,3 +147,5 @@ LANGFUSE_AWS_BEDROCK_MODEL="eu.anthropic.claude-3-haiku-20240307-v1:0"
|
||||
# Events table migration
|
||||
LANGFUSE_ENABLE_EVENTS_TABLE_OBSERVATIONS=true
|
||||
LANGFUSE_ENABLE_EVENTS_TABLE_FLAGS=true
|
||||
LANGFUSE_ENABLE_EVENTS_TABLE_V2_APIS=true
|
||||
LANGFUSE_EXPERIMENT_INSERT_INTO_EVENTS_TABLE=true
|
||||
|
||||
@@ -41,8 +41,10 @@ OTEL_EXPORTER_OTLP_ENDPOINT="http://localhost:4318"
|
||||
OTEL_SERVICE_NAME="langfuse"
|
||||
|
||||
# Default role for users who sign up, optional, can be org or org+project
|
||||
# Supports comma-separated IDs for multiple orgs (e.g., "org1,org2,org3")
|
||||
# LANGFUSE_DEFAULT_ORG_ID=
|
||||
# LANGFUSE_DEFAULT_ORG_ROLE=
|
||||
# Supports comma-separated IDs for multiple projects (e.g., "proj1,proj2,proj3")
|
||||
# LANGFUSE_DEFAULT_PROJECT_ID=
|
||||
# LANGFUSE_DEFAULT_PROJECT_ROLE=
|
||||
|
||||
|
||||
@@ -1,4 +0,0 @@
|
||||
node_modules
|
||||
**/node_modules
|
||||
build
|
||||
coverage
|
||||
@@ -172,7 +172,7 @@ jobs:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Install golang-migrate for Clickhouse migrations
|
||||
run: |
|
||||
curl -L https://github.com/golang-migrate/migrate/releases/download/v4.19.0/migrate.linux-amd64.tar.gz | tar xvz
|
||||
curl -L https://github.com/golang-migrate/migrate/releases/download/v4.19.1/migrate.linux-amd64.tar.gz | tar xvz
|
||||
sudo mv migrate /usr/bin/migrate
|
||||
which migrate
|
||||
- uses: pnpm/action-setup@v3
|
||||
@@ -269,7 +269,7 @@ jobs:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Install golang-migrate for Clickhouse migrations
|
||||
run: |
|
||||
curl -L https://github.com/golang-migrate/migrate/releases/download/v4.19.0/migrate.linux-amd64.tar.gz | tar xvz
|
||||
curl -L https://github.com/golang-migrate/migrate/releases/download/v4.19.1/migrate.linux-amd64.tar.gz | tar xvz
|
||||
sudo mv migrate /usr/bin/migrate
|
||||
which migrate
|
||||
- uses: pnpm/action-setup@v3
|
||||
@@ -396,7 +396,7 @@ jobs:
|
||||
pnpm install
|
||||
- name: Install golang-migrate for Clickhouse migrations
|
||||
run: |
|
||||
curl -L https://github.com/golang-migrate/migrate/releases/download/v4.19.0/migrate.linux-amd64.tar.gz | tar xvz
|
||||
curl -L https://github.com/golang-migrate/migrate/releases/download/v4.19.1/migrate.linux-amd64.tar.gz | tar xvz
|
||||
sudo mv migrate /usr/bin/migrate
|
||||
which migrate
|
||||
- name: Load default env
|
||||
@@ -422,6 +422,18 @@ jobs:
|
||||
pnpm run db:migrate
|
||||
pnpm run db:seed
|
||||
pnpm run --filter=shared ch:up
|
||||
- name: Download and extract ClickHouse client for dev-tables setup
|
||||
if: matrix.deploy-mode == ''
|
||||
run: |
|
||||
CH_VERSION="24.9.3.128"
|
||||
ARCH="amd64"
|
||||
wget "https://packages.clickhouse.com/deb/pool/main/c/clickhouse/clickhouse-common-static_${CH_VERSION}_${ARCH}.deb" -O clickhouse-client.deb
|
||||
dpkg-deb -x clickhouse-client.deb ch_client_dir
|
||||
sudo cp ch_client_dir/usr/bin/clickhouse /usr/local/bin/
|
||||
- name: Setup Dev Tables
|
||||
if: matrix.deploy-mode == ''
|
||||
run: |
|
||||
pnpm --filter=shared ch:dev-tables
|
||||
- name: Build
|
||||
run: pnpm --filter=worker... run build
|
||||
- name: run tests
|
||||
@@ -456,7 +468,7 @@ jobs:
|
||||
pnpm install
|
||||
- name: Install golang-migrate for Clickhouse migrations
|
||||
run: |
|
||||
curl -L https://github.com/golang-migrate/migrate/releases/download/v4.19.0/migrate.linux-amd64.tar.gz | tar xvz
|
||||
curl -L https://github.com/golang-migrate/migrate/releases/download/v4.19.1/migrate.linux-amd64.tar.gz | tar xvz
|
||||
sudo mv migrate /usr/bin/migrate
|
||||
which migrate
|
||||
- name: Load default env
|
||||
@@ -548,7 +560,7 @@ jobs:
|
||||
cp .env.dev.example web/.env
|
||||
- name: Install golang-migrate for Clickhouse migrations
|
||||
run: |
|
||||
curl -L https://github.com/golang-migrate/migrate/releases/download/v4.19.0/migrate.linux-amd64.tar.gz | tar xvz
|
||||
curl -L https://github.com/golang-migrate/migrate/releases/download/v4.19.1/migrate.linux-amd64.tar.gz | tar xvz
|
||||
sudo mv migrate /usr/bin/migrate
|
||||
which migrate
|
||||
- name: Run + migrate
|
||||
@@ -604,7 +616,7 @@ jobs:
|
||||
pnpm install
|
||||
- name: Install golang-migrate for Clickhouse migrations
|
||||
run: |
|
||||
curl -L https://github.com/golang-migrate/migrate/releases/download/v4.19.0/migrate.linux-amd64.tar.gz | tar xvz
|
||||
curl -L https://github.com/golang-migrate/migrate/releases/download/v4.19.1/migrate.linux-amd64.tar.gz | tar xvz
|
||||
sudo mv migrate /usr/bin/migrate
|
||||
which migrate
|
||||
- name: Load default env
|
||||
@@ -653,7 +665,7 @@ jobs:
|
||||
tests-web-sync,
|
||||
tests-worker,
|
||||
test-worker-llm-connections,
|
||||
e2e-tests,
|
||||
# e2e-tests, # Temporarily disabled - flaky timeouts
|
||||
test-docker-build,
|
||||
e2e-server-tests,
|
||||
tests-web-async,
|
||||
@@ -693,7 +705,7 @@ jobs:
|
||||
push-docker-image:
|
||||
needs: all-ci-passed
|
||||
# if something inside all-ci-passed was skipped, but everything that ran passed, we still want to deploy
|
||||
if: always() && needs.all-ci-passed.outputs.success == 'true' && github.event_name == 'push' && (github.ref == 'refs/heads/main' || startsWith(github.ref, 'refs/tags/'))
|
||||
if: always() && needs.all-ci-passed.outputs.success == 'true' && github.event_name == 'push' && startsWith(github.ref, 'refs/tags/')
|
||||
environment: "protected branches"
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
|
||||
@@ -17,6 +17,7 @@ migrate
|
||||
|
||||
# next.js
|
||||
/.next/
|
||||
/.next-check/
|
||||
/out/
|
||||
next-env.d.ts
|
||||
|
||||
@@ -64,6 +65,7 @@ node_modules
|
||||
**/node_modules
|
||||
**/dist
|
||||
**/.next/*
|
||||
**/.next-check/*
|
||||
**/.turbo/*
|
||||
.yarn
|
||||
.turbo
|
||||
|
||||
Vendored
+7
-2
@@ -28,6 +28,11 @@
|
||||
"editor.defaultFormatter": "Prisma.prisma"
|
||||
},
|
||||
"eslint.lintTask.enable": true,
|
||||
"eslint.workingDirectories": ["./web", "./worker"],
|
||||
"eslint.useFlatConfig": false
|
||||
"eslint.workingDirectories": [
|
||||
{ "pattern": "./web" },
|
||||
{ "pattern": "./worker" },
|
||||
{ "pattern": "./packages/shared" },
|
||||
{ "pattern": "./ee" }
|
||||
],
|
||||
"eslint.useFlatConfig": true
|
||||
}
|
||||
|
||||
@@ -72,9 +72,11 @@ pnpm run infra:dev:up # Start Docker services (PostgreSQL, ClickHouse, Redi
|
||||
pnpm run infra:dev:down # Stop Docker services
|
||||
```
|
||||
|
||||
### Building
|
||||
### Building & Type Checking
|
||||
```sh
|
||||
pnpm --filter=PACKAGE_NAME run build # Runs the build command, will show real typescript errors etc.
|
||||
pnpm tc # Fast typecheck across all packages (alias for pnpm typecheck)
|
||||
pnpm build:check # Full Next.js build to alternate dir (can run parallel with dev server)
|
||||
```
|
||||
|
||||
### Testing in Web Package
|
||||
@@ -82,11 +84,11 @@ The web package uses JEST for unit tests.
|
||||
Depending on the file location (sync, async)
|
||||
`web` related tests must go into the `web/src/__tests__/` folder.
|
||||
```sh
|
||||
pnpm test-sync --testPathPattern="$FILE_LOCATION_PATTERN" --testNamePattern="$TEST_NAME_PATTERN"
|
||||
pnpm test-sync --testPathPatterns="$FILE_LOCATION_PATTERN" --testNamePattern="$TEST_NAME_PATTERN"
|
||||
# For tests in the async folder:
|
||||
pnpm test -- --testPathPattern="$FILE_LOCATION_PATTERN" --testNamePattern="$TEST_NAME_PATTERN"
|
||||
pnpm test -- --testPathPatterns="$FILE_LOCATION_PATTERN" --testNamePattern="$TEST_NAME_PATTERN"
|
||||
# For client tests:
|
||||
pnpm test-client --testPathPattern="buildStepData" --testNamePattern="buildStepData"
|
||||
pnpm test-client --testPathPatterns="buildStepData" --testNamePattern="buildStepData"
|
||||
```
|
||||
|
||||
### Testing in the Worker Package
|
||||
@@ -192,6 +194,21 @@ To get a project, use the `get_project` capability with the full project name as
|
||||
|
||||
## TypeScript Best Practices
|
||||
- In TypeScript, if possible, don't use the `any` type
|
||||
- **Use a single params object for functions with multiple arguments** - This makes code more readable at call sites and prevents bugs when arguments of the same type are accidentally swapped:
|
||||
|
||||
```typescript
|
||||
// ❌ Bad - positional arguments are unclear and can be swapped without type errors
|
||||
function sendMessage(userId: string, sessionId: string, projectId: string) {
|
||||
// ...
|
||||
}
|
||||
sendMessage(someString, someOtherString, anotherString); // Which is which?
|
||||
|
||||
// ✅ Good - params object makes intent clear and prevents argument swapping
|
||||
function sendMessage(params: { userId: string; sessionId: string; projectId: string }) {
|
||||
// ...
|
||||
}
|
||||
sendMessage({ userId: someString, sessionId: someOtherString, projectId: anotherString });
|
||||
```
|
||||
|
||||
## General Coding Guidelines
|
||||
- For easier code reviews, prefer not to move functions etc around within a file unless necessary or instructed to do so
|
||||
|
||||
+6
-7
@@ -94,8 +94,6 @@ The diagram below may not show all relationships if the foreign key is not defin
|
||||
|
||||
Full database schema: [packages/shared/prisma/schema.prisma](packages/shared/prisma/schema.prisma)
|
||||
|
||||
<img src="./packages/shared/prisma/database.svg">
|
||||
|
||||
## Repository Structure
|
||||
|
||||
We built a monorepo using [pnpm](https://pnpm.io/motivation) and [turbo](https://turbo.build/repo/docs) to manage the dependencies and build process. The monorepo contains the following packages:
|
||||
@@ -122,8 +120,8 @@ Requirements
|
||||
**Steps**
|
||||
|
||||
1. Install development dependencies:
|
||||
- [golang-migrate](https://github.com/golang-migrate/migrate/tree/master/cmd/migrate#migrate-cli) as CLI
|
||||
- [clickhouse binary](https://clickhouse.com/docs/install) on macOS with brew: `brew install --cask clickhouse`
|
||||
- [golang-migrate](https://github.com/golang-migrate/migrate/tree/master/cmd/migrate#migrate-cli) as CLI
|
||||
- [clickhouse binary](https://clickhouse.com/docs/install) on macOS with brew: `brew install --cask clickhouse`
|
||||
|
||||
2. Fork the repository and clone it locally
|
||||
|
||||
@@ -182,6 +180,8 @@ pnpm run db:seed:examples
|
||||
pnpm install
|
||||
pnpm run dev
|
||||
pnpm --filter=web run dev # execute command only in one package
|
||||
pnpm tc # fast typecheck all packages
|
||||
pnpm build:check # Full Next.js build to alternate dir (can run parallel with dev server)
|
||||
```
|
||||
|
||||
In the root `package.json`, you can find scripts which are executed with turbo e.g. `turbo run dev`. These scripts are executed with the help of Turbo. Turbo executes the commands in all packages taking care of the correct order of execution. Task definitions can be found in the `turbo.config.js` file.
|
||||
@@ -254,9 +254,9 @@ To run a specific test, for example the test: `"should handle special characters
|
||||
|
||||
```sh
|
||||
cd web # or with --filter=web
|
||||
pnpm test-sync --testPathPattern="prompts\.v2\.servertest" --testNamePattern="should handle special characters in prompt names"
|
||||
pnpm test-sync --testPathPatterns="prompts\.v2\.servertest" --testNamePattern="should handle special characters in prompt names"
|
||||
# for async folder tests:
|
||||
pnpm test -- --testPathPattern="observations-api" --testNamePattern="should fetch all observations"
|
||||
pnpm test -- --testPathPatterns="observations-api" --testNamePattern="should fetch all observations"
|
||||
```
|
||||
|
||||
To run all tests:
|
||||
@@ -424,4 +424,3 @@ npx fern-api generate --api organizations # for the organizations API
|
||||
Langfuse is MIT licensed, except for `ee/` folder. See [LICENSE](LICENSE) and [docs](https://langfuse.com/docs/open-source) for more details.
|
||||
|
||||
When contributing to the Langfuse codebase, you need to agree to the [Contributor License Agreement](https://cla-assistant.io/langfuse/langfuse). You only need to do this once and the CLA bot will remind you if you haven't signed it yet.
|
||||
|
||||
|
||||
@@ -76,8 +76,6 @@ Langfuse is an **open source LLM engineering** platform. It helps teams collabor
|
||||
|
||||
[](https://langfuse.com/watch-demo)
|
||||
|
||||
|
||||
|
||||
## ✨ Core Features
|
||||
|
||||
<img width="4856" height="1944" alt="Langfuse Overview" src="https://github.com/user-attachments/assets/5dac68ef-d546-49fb-b06f-cfafc19282e3" />
|
||||
@@ -144,7 +142,7 @@ See [self-hosting documentation](https://langfuse.com/self-hosting) to learn mor
|
||||
| [Haystack](https://langfuse.com/docs/integrations/haystack) | Python | Automated instrumentation via Haystack content tracing system. |
|
||||
| [LiteLLM](https://langfuse.com/docs/integrations/litellm) | Python, JS/TS (proxy only) | Use any LLM as a drop in replacement for GPT. Use Azure, OpenAI, Cohere, Anthropic, Ollama, VLLM, Sagemaker, HuggingFace, Replicate (100+ LLMs). |
|
||||
| [Vercel AI SDK](https://langfuse.com/docs/integrations/vercel-ai-sdk) | JS/TS | TypeScript toolkit designed to help developers build AI-powered applications with React, Next.js, Vue, Svelte, Node.js. |
|
||||
| [Mastra](https://langfuse.com/docs/integrations/mastra) | JS/TS | Open source framework for building AI agents and multi-agent systems. |
|
||||
| [Mastra](https://langfuse.com/docs/integrations/mastra) | JS/TS | Open source framework for building AI agents and multi-agent systems. |
|
||||
| [API](https://langfuse.com/docs/api) | | Directly call the public API. OpenAPI spec available. |
|
||||
|
||||
### Packages integrated with Langfuse:
|
||||
@@ -259,6 +257,10 @@ Your contributions are welcome!
|
||||
|
||||
This repository is MIT licensed, except for the `ee` folders. See [LICENSE](LICENSE) and [docs](https://langfuse.com/docs/open-source) for more details.
|
||||
|
||||
## Dependencies
|
||||
|
||||
We deploy this code base in Docker containers based on the Linux Alpine Image ([source](https://github.com/nodejs/docker-node)). You may find the Dockerfiles in [web/Dockerfile](web/Dockerfile) and [worker/Dockerfile](worker/Dockerfile).
|
||||
|
||||
## ⭐️ Star History
|
||||
|
||||
<a href="https://star-history.com/#langfuse/langfuse&Date">
|
||||
|
||||
@@ -45,3 +45,10 @@
|
||||
## Seeder
|
||||
|
||||
- make sure that for new features with data model changes, the database seeder is adjusted.
|
||||
|
||||
## API Documentation
|
||||
|
||||
- Whenever a file in `web/src/features/public-api/types` changes, the `fern/apis` definition probably needs to be adjusted, too.
|
||||
- `nullish` types should map to `optional<nullable<T>>` in fern.
|
||||
- `nullable` types should map to `nullable<T>` in fern.
|
||||
- `optional` types should map to `optional<T>` in fern.
|
||||
|
||||
@@ -1,8 +0,0 @@
|
||||
/** @type {import("eslint").Linter.Config} */
|
||||
module.exports = {
|
||||
extends: ["@repo/eslint-config/library.js"],
|
||||
parser: "@typescript-eslint/parser",
|
||||
parserOptions: {
|
||||
project: true,
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,3 @@
|
||||
import baseConfig from "@repo/eslint-config";
|
||||
|
||||
export default [...baseConfig];
|
||||
+10
-7
@@ -19,27 +19,30 @@
|
||||
},
|
||||
"scripts": {
|
||||
"build": "tsc",
|
||||
"build:check": "tsc",
|
||||
"typecheck": "dotenv -e ../../.env -- tsgo --noEmit --skipLibCheck --incremental --tsBuildInfoFile .tsbuildinfo",
|
||||
"dev": "tsc --watch",
|
||||
"lint": "eslint . --ext .js,.jsx,.ts,.tsx --max-warnings 0",
|
||||
"lint:fix": "eslint . --ext .js,.jsx,.ts,.tsx --fix"
|
||||
"lint": "eslint . --cache --cache-location dist/.eslintcache --max-warnings 0",
|
||||
"lint:fix": "eslint . --cache --cache-location dist/.eslintcache --fix"
|
||||
},
|
||||
"dependencies": {
|
||||
"@langfuse/shared": "workspace:*",
|
||||
"@opentelemetry/api": ">=1.0.0 <1.10.0",
|
||||
"https-proxy-agent": "^7.0.6",
|
||||
"next": "15.5.9",
|
||||
"next-auth": "^4.24.12",
|
||||
"next-auth": "^4.24.13",
|
||||
"zod": "^3.25.62"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@repo/eslint-config": "workspace:*",
|
||||
"@repo/typescript-config": "workspace:*",
|
||||
"@types/node": "^24.3.0",
|
||||
"@typescript-eslint/parser": "^8.48.1",
|
||||
"eslint": "^8.57.0",
|
||||
"eslint-config-prettier": "^9.1.0",
|
||||
"@typescript-eslint/parser": "^8.50.1",
|
||||
"@typescript/native-preview": "7.0.0-dev.20260122.3",
|
||||
"eslint": "^9.39.2",
|
||||
"eslint-config-prettier": "^10.1.8",
|
||||
"eslint-config-standard": "^17.1.0",
|
||||
"eslint-plugin-prettier": "^5.1.3",
|
||||
"eslint-plugin-prettier": "^5.5.4",
|
||||
"prettier": "^3.6.2",
|
||||
"ts-node": "^10.9.2",
|
||||
"tsc-watch": "^6.2.0",
|
||||
|
||||
@@ -18,7 +18,9 @@ types:
|
||||
sessionId: optional<string>
|
||||
observationId: optional<string>
|
||||
datasetRunId: optional<string>
|
||||
name: string
|
||||
name:
|
||||
type: string
|
||||
docs: The name of the score. Always overrides "output" for correction scores.
|
||||
value:
|
||||
type: CreateScoreValue
|
||||
docs: The value of the score. Must be passed as string for categorical scores, and numeric for boolean and numeric scores. Boolean score values must equal either 1 or 0 (true or false)
|
||||
@@ -81,10 +83,11 @@ types:
|
||||
- NUMERIC
|
||||
- CATEGORICAL
|
||||
- BOOLEAN
|
||||
- CORRECTION
|
||||
|
||||
CreateScoreValue:
|
||||
discriminated: false
|
||||
union:
|
||||
- double
|
||||
- string
|
||||
docs: The value of the score. Must be passed as string for categorical scores, and numeric for boolean and numeric scores
|
||||
docs: The value of the score. Must be passed as string for categorical scores, and numeric for boolean and numeric scores. For corrections pass value as string and set data type to CORRECTION
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
types:
|
||||
# Objects
|
||||
# Source: web/src/features/public-api/types/traces.ts - APITrace, APIExtendedTrace, GetTraceV1Response
|
||||
Trace:
|
||||
properties:
|
||||
id:
|
||||
@@ -9,7 +10,7 @@ types:
|
||||
type: datetime
|
||||
docs: The timestamp when the trace was created
|
||||
name:
|
||||
type: optional<string>
|
||||
type: nullable<string>
|
||||
docs: The name of the trace
|
||||
input:
|
||||
type: optional<unknown>
|
||||
@@ -18,28 +19,28 @@ types:
|
||||
type: optional<unknown>
|
||||
docs: The output data of the trace. Can be any JSON.
|
||||
sessionId:
|
||||
type: optional<string>
|
||||
type: nullable<string>
|
||||
docs: The session identifier associated with the trace
|
||||
release:
|
||||
type: optional<string>
|
||||
type: nullable<string>
|
||||
docs: The release version of the application when the trace was created
|
||||
version:
|
||||
type: optional<string>
|
||||
type: nullable<string>
|
||||
docs: The version of the trace
|
||||
userId:
|
||||
type: optional<string>
|
||||
type: nullable<string>
|
||||
docs: The user identifier associated with the trace
|
||||
metadata:
|
||||
type: optional<unknown>
|
||||
docs: The metadata associated with the trace. Can be any JSON.
|
||||
tags:
|
||||
type: optional<list<string>>
|
||||
docs: The tags associated with the trace. Can be an array of strings or null.
|
||||
type: list<string>
|
||||
docs: The tags associated with the trace.
|
||||
public:
|
||||
type: optional<boolean>
|
||||
type: boolean
|
||||
docs: Public traces are accessible via url without login
|
||||
environment:
|
||||
type: optional<string>
|
||||
type: string
|
||||
docs: The environment from which this trace originated. Can be any lowercase alphanumeric string with hyphens and underscores that does not start with 'langfuse'.
|
||||
TraceWithDetails: # GET /traces
|
||||
extends: Trace
|
||||
@@ -48,16 +49,16 @@ types:
|
||||
type: string
|
||||
docs: Path of trace in Langfuse UI
|
||||
latency:
|
||||
type: double
|
||||
type: optional<nullable<double>>
|
||||
docs: Latency of trace in seconds
|
||||
totalCost:
|
||||
type: double
|
||||
type: optional<nullable<double>>
|
||||
docs: Cost of trace in USD
|
||||
observations:
|
||||
type: list<string>
|
||||
type: optional<nullable<list<string>>>
|
||||
docs: List of observation ids
|
||||
scores:
|
||||
type: list<string>
|
||||
type: optional<nullable<list<string>>>
|
||||
docs: List of score ids
|
||||
TraceWithFullDetails: # GET traces/[traceID]
|
||||
extends: Trace
|
||||
@@ -66,10 +67,10 @@ types:
|
||||
type: string
|
||||
docs: Path of trace in Langfuse UI
|
||||
latency:
|
||||
type: double
|
||||
type: optional<nullable<double>>
|
||||
docs: Latency of trace in seconds
|
||||
totalCost:
|
||||
type: double
|
||||
type: optional<nullable<double>>
|
||||
docs: Cost of trace in USD
|
||||
observations:
|
||||
type: list<ObservationsView>
|
||||
@@ -77,119 +78,121 @@ types:
|
||||
scores:
|
||||
type: list<ScoreV1>
|
||||
docs: List of scores
|
||||
# Source: web/src/features/public-api/types/sessions.ts - APISession
|
||||
Session:
|
||||
properties:
|
||||
id: string
|
||||
createdAt: datetime
|
||||
projectId: string
|
||||
environment:
|
||||
type: optional<string>
|
||||
type: string
|
||||
docs: The environment from which this session originated.
|
||||
SessionWithTraces:
|
||||
extends: Session
|
||||
properties:
|
||||
traces: list<Trace>
|
||||
# Source: web/src/features/public-api/types/observations.ts - APIObservation
|
||||
Observation:
|
||||
properties:
|
||||
id:
|
||||
type: string
|
||||
docs: The unique identifier of the observation
|
||||
traceId:
|
||||
type: optional<string>
|
||||
type: nullable<string>
|
||||
docs: The trace ID associated with the observation
|
||||
type:
|
||||
type: string
|
||||
docs: The type of the observation
|
||||
name:
|
||||
type: optional<string>
|
||||
type: nullable<string>
|
||||
docs: The name of the observation
|
||||
startTime:
|
||||
type: datetime
|
||||
docs: The start time of the observation
|
||||
endTime:
|
||||
type: optional<datetime>
|
||||
type: nullable<datetime>
|
||||
docs: The end time of the observation.
|
||||
completionStartTime:
|
||||
type: optional<datetime>
|
||||
type: nullable<datetime>
|
||||
docs: The completion start time of the observation
|
||||
model:
|
||||
type: optional<string>
|
||||
type: nullable<string>
|
||||
docs: The model used for the observation
|
||||
modelParameters:
|
||||
type: optional<map<string, MapValue>>
|
||||
type: unknown
|
||||
docs: The parameters of the model used for the observation
|
||||
input:
|
||||
type: optional<unknown>
|
||||
type: unknown
|
||||
docs: The input data of the observation
|
||||
version:
|
||||
type: optional<string>
|
||||
type: nullable<string>
|
||||
docs: The version of the observation
|
||||
metadata:
|
||||
type: optional<unknown>
|
||||
type: unknown
|
||||
docs: Additional metadata of the observation
|
||||
output:
|
||||
type: optional<unknown>
|
||||
type: unknown
|
||||
docs: The output data of the observation
|
||||
usage:
|
||||
type: optional<Usage>
|
||||
type: Usage
|
||||
docs: (Deprecated. Use usageDetails and costDetails instead.) The usage data of the observation
|
||||
level:
|
||||
type: ObservationLevel
|
||||
docs: The level of the observation
|
||||
statusMessage:
|
||||
type: optional<string>
|
||||
type: nullable<string>
|
||||
docs: The status message of the observation
|
||||
parentObservationId:
|
||||
type: optional<string>
|
||||
type: nullable<string>
|
||||
docs: The parent observation ID
|
||||
promptId:
|
||||
type: optional<string>
|
||||
type: nullable<string>
|
||||
docs: The prompt ID associated with the observation
|
||||
usageDetails:
|
||||
type: optional<map<string, integer>>
|
||||
type: map<string, integer>
|
||||
docs: The usage details of the observation. Key is the name of the usage metric, value is the number of units consumed. The total key is the sum of all (non-total) usage metrics or the total value ingested.
|
||||
costDetails:
|
||||
type: optional<map<string, double>>
|
||||
type: map<string, double>
|
||||
docs: The cost details of the observation. Key is the name of the cost metric, value is the cost in USD. The total key is the sum of all (non-total) cost metrics or the total value ingested.
|
||||
environment:
|
||||
type: optional<string>
|
||||
type: string
|
||||
docs: The environment from which this observation originated. Can be any lowercase alphanumeric string with hyphens and underscores that does not start with 'langfuse'.
|
||||
|
||||
ObservationsView:
|
||||
extends: Observation
|
||||
properties:
|
||||
promptName:
|
||||
type: optional<string>
|
||||
type: nullable<string>
|
||||
docs: The name of the prompt associated with the observation
|
||||
promptVersion:
|
||||
type: optional<integer>
|
||||
type: nullable<integer>
|
||||
docs: The version of the prompt associated with the observation
|
||||
modelId:
|
||||
type: optional<string>
|
||||
type: nullable<string>
|
||||
docs: The unique identifier of the model
|
||||
inputPrice:
|
||||
type: optional<double>
|
||||
type: nullable<double>
|
||||
docs: The price of the input in USD
|
||||
outputPrice:
|
||||
type: optional<double>
|
||||
type: nullable<double>
|
||||
docs: The price of the output in USD.
|
||||
totalPrice:
|
||||
type: optional<double>
|
||||
type: nullable<double>
|
||||
docs: The total price in USD.
|
||||
calculatedInputCost:
|
||||
type: optional<double>
|
||||
type: nullable<double>
|
||||
docs: (Deprecated. Use usageDetails and costDetails instead.) The calculated cost of the input in USD
|
||||
calculatedOutputCost:
|
||||
type: optional<double>
|
||||
type: nullable<double>
|
||||
docs: (Deprecated. Use usageDetails and costDetails instead.) The calculated cost of the output in USD
|
||||
calculatedTotalCost:
|
||||
type: optional<double>
|
||||
type: nullable<double>
|
||||
docs: (Deprecated. Use usageDetails and costDetails instead.) The calculated total cost in USD
|
||||
latency:
|
||||
type: optional<double>
|
||||
type: nullable<double>
|
||||
docs: The latency in seconds.
|
||||
timeToFirstToken:
|
||||
type: optional<double>
|
||||
type: nullable<double>
|
||||
docs: The time to the first token in seconds
|
||||
|
||||
Usage:
|
||||
@@ -197,14 +200,16 @@ types:
|
||||
properties:
|
||||
input:
|
||||
docs: Number of input units (e.g. tokens)
|
||||
type: optional<integer>
|
||||
type: integer
|
||||
output:
|
||||
docs: Number of output units (e.g. tokens)
|
||||
type: optional<integer>
|
||||
type: integer
|
||||
total:
|
||||
docs: Defaults to input+output if not set
|
||||
type: optional<integer>
|
||||
unit: optional<ModelUsageUnit>
|
||||
type: integer
|
||||
unit:
|
||||
docs: Unit of measurement
|
||||
type: nullable<string>
|
||||
inputCost:
|
||||
docs: USD input cost
|
||||
type: optional<double>
|
||||
@@ -214,6 +219,7 @@ types:
|
||||
totalCost:
|
||||
docs: USD total cost, defaults to input+output
|
||||
type: optional<double>
|
||||
# Source: web/src/features/public-api/types/score-configs.ts - APIScoreConfig
|
||||
ScoreConfig:
|
||||
docs: Configuration for a score
|
||||
properties:
|
||||
@@ -222,45 +228,56 @@ types:
|
||||
createdAt: datetime
|
||||
updatedAt: datetime
|
||||
projectId: string
|
||||
dataType: ScoreDataType
|
||||
dataType: ScoreConfigDataType
|
||||
isArchived:
|
||||
type: boolean
|
||||
docs: Whether the score config is archived. Defaults to false
|
||||
minValue:
|
||||
type: optional<double>
|
||||
type: optional<nullable<double>>
|
||||
docs: Sets minimum value for numerical scores. If not set, the minimum value defaults to -∞
|
||||
maxValue:
|
||||
type: optional<double>
|
||||
type: optional<nullable<double>>
|
||||
docs: Sets maximum value for numerical scores. If not set, the maximum value defaults to +∞
|
||||
categories:
|
||||
type: optional<list<ConfigCategory>>
|
||||
docs: Configures custom categories for categorical scores
|
||||
description: optional<string>
|
||||
description:
|
||||
type: optional<nullable<string>>
|
||||
docs: Description of the score config
|
||||
ConfigCategory:
|
||||
properties:
|
||||
value: double
|
||||
label: string
|
||||
# Source: packages/shared/src/features/scores/interfaces/api/v1/schemas.ts - APIScoreSchemaV1
|
||||
BaseScoreV1:
|
||||
properties:
|
||||
id: string
|
||||
traceId: string
|
||||
name: string
|
||||
source: ScoreSource
|
||||
observationId: optional<string>
|
||||
observationId:
|
||||
type: optional<nullable<string>>
|
||||
docs: The observation ID associated with the score
|
||||
timestamp: datetime
|
||||
createdAt: datetime
|
||||
updatedAt: datetime
|
||||
authorUserId: optional<string>
|
||||
comment: optional<string>
|
||||
metadata: optional<unknown>
|
||||
authorUserId:
|
||||
type: nullable<string>
|
||||
docs: The user ID of the author
|
||||
comment:
|
||||
type: nullable<string>
|
||||
docs: Comment on the score
|
||||
metadata:
|
||||
type: unknown
|
||||
docs: Metadata associated with the score
|
||||
configId:
|
||||
type: optional<string>
|
||||
type: nullable<string>
|
||||
docs: Reference a score config on a score. When set, config and score name must be equal and value must comply to optionally defined numerical range
|
||||
queueId:
|
||||
type: optional<string>
|
||||
type: nullable<string>
|
||||
docs: The annotation queue referenced by the score. Indicates if score was initially created while processing annotation queue.
|
||||
environment:
|
||||
type: optional<string>
|
||||
type: string
|
||||
docs: The environment from which this score originated. Can be any lowercase alphanumeric string with hyphens and underscores that does not start with 'langfuse'.
|
||||
NumericScoreV1:
|
||||
extends: BaseScoreV1
|
||||
@@ -299,29 +316,44 @@ types:
|
||||
type: BooleanScoreV1
|
||||
docs: "Score with BOOLEAN data type"
|
||||
|
||||
# Source: packages/shared/src/features/scores/interfaces/api/v2/schemas.ts - APIScoreSchemaV2
|
||||
BaseScore:
|
||||
properties:
|
||||
id: string
|
||||
traceId: optional<string>
|
||||
sessionId: optional<string>
|
||||
observationId: optional<string>
|
||||
datasetRunId: optional<string>
|
||||
traceId:
|
||||
type: optional<nullable<string>>
|
||||
docs: The trace ID associated with the score
|
||||
sessionId:
|
||||
type: optional<nullable<string>>
|
||||
docs: The session ID associated with the score
|
||||
observationId:
|
||||
type: optional<nullable<string>>
|
||||
docs: The observation ID associated with the score
|
||||
datasetRunId:
|
||||
type: optional<nullable<string>>
|
||||
docs: The dataset run ID associated with the score
|
||||
name: string
|
||||
source: ScoreSource
|
||||
timestamp: datetime
|
||||
createdAt: datetime
|
||||
updatedAt: datetime
|
||||
authorUserId: optional<string>
|
||||
comment: optional<string>
|
||||
metadata: optional<unknown>
|
||||
authorUserId:
|
||||
type: nullable<string>
|
||||
docs: The user ID of the author
|
||||
comment:
|
||||
type: nullable<string>
|
||||
docs: Comment on the score
|
||||
metadata:
|
||||
type: unknown
|
||||
docs: Metadata associated with the score
|
||||
configId:
|
||||
type: optional<string>
|
||||
type: nullable<string>
|
||||
docs: Reference a score config on a score. When set, config and score name must be equal and value must comply to optionally defined numerical range
|
||||
queueId:
|
||||
type: optional<string>
|
||||
type: nullable<string>
|
||||
docs: The annotation queue referenced by the score. Indicates if score was initially created while processing annotation queue.
|
||||
environment:
|
||||
type: optional<string>
|
||||
type: string
|
||||
docs: The environment from which this score originated. Can be any lowercase alphanumeric string with hyphens and underscores that does not start with 'langfuse'.
|
||||
NumericScore:
|
||||
extends: BaseScore
|
||||
@@ -367,6 +399,7 @@ types:
|
||||
- string
|
||||
docs: The value of the score. Must be passed as string for categorical scores, and numeric for boolean and numeric scores
|
||||
|
||||
# Source: web/src/features/public-api/types/comments.ts - APIComment
|
||||
Comment:
|
||||
properties:
|
||||
id: string
|
||||
@@ -376,36 +409,55 @@ types:
|
||||
objectType: CommentObjectType
|
||||
objectId: string
|
||||
content: string
|
||||
authorUserId: optional<string>
|
||||
authorUserId:
|
||||
type: optional<nullable<string>>
|
||||
docs: The user ID of the comment author
|
||||
|
||||
# Source: web/src/features/public-api/types/datasets.ts - APIDataset
|
||||
Dataset:
|
||||
properties:
|
||||
id: string
|
||||
name: string
|
||||
description: optional<string>
|
||||
metadata: optional<unknown>
|
||||
description:
|
||||
type: nullable<string>
|
||||
docs: Description of the dataset
|
||||
metadata:
|
||||
type: unknown
|
||||
docs: Metadata associated with the dataset
|
||||
inputSchema:
|
||||
type: optional<unknown>
|
||||
type: nullable<unknown>
|
||||
docs: JSON Schema for validating dataset item inputs
|
||||
expectedOutputSchema:
|
||||
type: optional<unknown>
|
||||
type: nullable<unknown>
|
||||
docs: JSON Schema for validating dataset item expected outputs
|
||||
projectId: string
|
||||
createdAt: datetime
|
||||
updatedAt: datetime
|
||||
# Source: web/src/features/public-api/types/datasets.ts - APIDatasetItem
|
||||
DatasetItem:
|
||||
properties:
|
||||
id: string
|
||||
status: DatasetStatus
|
||||
input: optional<unknown>
|
||||
expectedOutput: optional<unknown>
|
||||
metadata: optional<unknown>
|
||||
sourceTraceId: optional<string>
|
||||
sourceObservationId: optional<string>
|
||||
input:
|
||||
type: unknown
|
||||
docs: Input data for the dataset item
|
||||
expectedOutput:
|
||||
type: unknown
|
||||
docs: Expected output for the dataset item
|
||||
metadata:
|
||||
type: unknown
|
||||
docs: Metadata associated with the dataset item
|
||||
sourceTraceId:
|
||||
type: nullable<string>
|
||||
docs: The trace ID that sourced this dataset item
|
||||
sourceObservationId:
|
||||
type: nullable<string>
|
||||
docs: The observation ID that sourced this dataset item
|
||||
datasetId: string
|
||||
datasetName: string
|
||||
createdAt: datetime
|
||||
updatedAt: datetime
|
||||
# Source: web/src/features/public-api/types/datasets.ts - APIDatasetRunItem
|
||||
DatasetRunItem:
|
||||
properties:
|
||||
id: string
|
||||
@@ -413,9 +465,12 @@ types:
|
||||
datasetRunName: string
|
||||
datasetItemId: string
|
||||
traceId: string
|
||||
observationId: optional<string>
|
||||
observationId:
|
||||
type: nullable<string>
|
||||
docs: The observation ID associated with this run item
|
||||
createdAt: datetime
|
||||
updatedAt: datetime
|
||||
# Source: web/src/features/public-api/types/datasets.ts - APIDatasetRun
|
||||
DatasetRun:
|
||||
properties:
|
||||
id:
|
||||
@@ -425,10 +480,10 @@ types:
|
||||
type: string
|
||||
docs: Name of the dataset run
|
||||
description:
|
||||
type: optional<string>
|
||||
type: nullable<string>
|
||||
docs: Description of the run
|
||||
metadata:
|
||||
type: optional<unknown>
|
||||
type: unknown
|
||||
docs: Metadata of the dataset run
|
||||
datasetId:
|
||||
type: string
|
||||
@@ -446,6 +501,7 @@ types:
|
||||
extends: DatasetRun
|
||||
properties:
|
||||
datasetRunItems: list<DatasetRunItem>
|
||||
# Source: web/src/features/public-api/types/models.ts - APIModelDefinition
|
||||
Model:
|
||||
docs: |
|
||||
Model definition used for transforming usage into USD cost and/or tokenization.
|
||||
@@ -467,25 +523,25 @@ types:
|
||||
type: string
|
||||
startDate:
|
||||
docs: Apply only to generations which are newer than this ISO date.
|
||||
type: optional<datetime>
|
||||
type: nullable<datetime>
|
||||
unit:
|
||||
docs: Unit used by this model.
|
||||
type: optional<ModelUsageUnit>
|
||||
type: optional<nullable<ModelUsageUnit>>
|
||||
inputPrice:
|
||||
docs: Deprecated. See 'prices' instead. Price (USD) per input unit
|
||||
type: optional<double>
|
||||
type: nullable<double>
|
||||
outputPrice:
|
||||
docs: Deprecated. See 'prices' instead. Price (USD) per output unit
|
||||
type: optional<double>
|
||||
type: nullable<double>
|
||||
totalPrice:
|
||||
docs: Deprecated. See 'prices' instead. Price (USD) per total unit. Cannot be set if input or output price is set.
|
||||
type: optional<double>
|
||||
type: nullable<double>
|
||||
tokenizerId:
|
||||
docs: Optional. Tokenizer to be applied to observations which match to this model. See docs for more details.
|
||||
type: optional<string>
|
||||
type: nullable<string>
|
||||
tokenizerConfig:
|
||||
docs: Optional. Configuration for the selected tokenizer. Needs to be JSON. See docs for more details.
|
||||
type: optional<unknown>
|
||||
type: unknown
|
||||
isLangfuseManaged:
|
||||
type: boolean
|
||||
createdAt:
|
||||
@@ -730,11 +786,18 @@ types:
|
||||
- ANNOTATION
|
||||
- API
|
||||
- EVAL
|
||||
ScoreConfigDataType:
|
||||
enum:
|
||||
- NUMERIC
|
||||
- BOOLEAN
|
||||
- CATEGORICAL
|
||||
|
||||
ScoreDataType:
|
||||
enum:
|
||||
- NUMERIC
|
||||
- BOOLEAN
|
||||
- CATEGORICAL
|
||||
- CORRECTION
|
||||
|
||||
errors:
|
||||
Error:
|
||||
|
||||
@@ -278,7 +278,9 @@ types:
|
||||
sessionId: optional<string>
|
||||
observationId: optional<string>
|
||||
datasetRunId: optional<string>
|
||||
name: string
|
||||
name:
|
||||
type: string
|
||||
docs: The name of the score. Always overrides "output" for correction scores.
|
||||
environment: optional<string>
|
||||
queueId:
|
||||
type: optional<string>
|
||||
|
||||
@@ -9,7 +9,9 @@ service:
|
||||
metrics:
|
||||
docs: |
|
||||
Get metrics from the Langfuse project using a query object.
|
||||
|
||||
|
||||
Consider using the [v2 metrics endpoint](/api-reference#tag/metricsv2/GET/api/public/v2/metrics) for better performance.
|
||||
|
||||
For more details, see the [Metrics API documentation](https://langfuse.com/docs/metrics/features/metrics-api).
|
||||
method: GET
|
||||
path: /metrics
|
||||
|
||||
@@ -20,7 +20,7 @@ service:
|
||||
- `basic` - name, level, statusMessage, version, environment, bookmarked, public, userId, sessionId
|
||||
- `time` - completionStartTime, createdAt, updatedAt
|
||||
- `io` - input, output
|
||||
- `metadata` - metadata
|
||||
- `metadata` - metadata (truncated to 200 chars by default, use `expandMetadata` to get full values)
|
||||
- `model` - providedModelName, internalModelId, modelParameters
|
||||
- `usage` - usageDetails, costDetails, totalCost
|
||||
- `prompt` - promptId, promptName, promptVersion
|
||||
@@ -43,6 +43,13 @@ service:
|
||||
Available groups: core, basic, time, io, metadata, model, usage, prompt, metrics.
|
||||
If not specified, `core` and `basic` field groups are returned.
|
||||
Example: "basic,usage,model"
|
||||
expandMetadata:
|
||||
type: optional<string>
|
||||
docs: |
|
||||
Comma-separated list of metadata keys to return non-truncated.
|
||||
By default, metadata values over 200 characters are truncated.
|
||||
Use this parameter to retrieve full values for specific keys.
|
||||
Example: "key1,key2"
|
||||
limit:
|
||||
type: optional<integer>
|
||||
docs: Number of items to return per page. Maximum 1000, default 50.
|
||||
|
||||
@@ -16,7 +16,10 @@ service:
|
||||
docs: The unique langfuse identifier of an observation, can be an event, span or generation
|
||||
response: commons.ObservationsView
|
||||
getMany:
|
||||
docs: Get a list of observations
|
||||
docs: |
|
||||
Get a list of observations.
|
||||
|
||||
Consider using the [v2 observations endpoint](/api-reference#tag/observationsv2/GET/api/public/v2/observations) for cursor-based pagination and field selection.
|
||||
method: GET
|
||||
path: /observations
|
||||
request:
|
||||
|
||||
@@ -44,8 +44,12 @@ service:
|
||||
type: optional<map<string, unknown>>
|
||||
docs: Optional metadata for the project
|
||||
retention:
|
||||
type: integer
|
||||
docs: Number of days to retain data. Must be 0 or at least 3 days. Requires data-retention entitlement for non-zero values. Optional.
|
||||
type: optional<integer>
|
||||
docs: |
|
||||
Number of days to retain data.
|
||||
Must be 0 or at least 3 days.
|
||||
Requires data-retention entitlement for non-zero values.
|
||||
Optional. Will retain existing retention setting if omitted.
|
||||
response: Project
|
||||
|
||||
delete:
|
||||
@@ -102,10 +106,22 @@ types:
|
||||
properties:
|
||||
data: list<Project>
|
||||
|
||||
Organization:
|
||||
properties:
|
||||
id:
|
||||
type: string
|
||||
docs: The unique identifier of the organization
|
||||
name:
|
||||
type: string
|
||||
docs: The name of the organization
|
||||
|
||||
Project:
|
||||
properties:
|
||||
id: string
|
||||
name: string
|
||||
organization:
|
||||
type: Organization
|
||||
docs: The organization this project belongs to
|
||||
metadata:
|
||||
type: map<string, unknown>
|
||||
docs: Metadata for the project
|
||||
|
||||
@@ -55,7 +55,7 @@ types:
|
||||
CreateScoreConfigRequest:
|
||||
properties:
|
||||
name: string
|
||||
dataType: commons.ScoreDataType
|
||||
dataType: commons.ScoreConfigDataType
|
||||
categories:
|
||||
type: optional<list<commons.ConfigCategory>>
|
||||
docs: Configure custom categories for categorical scores. Pass a list of objects with `label` and `value` properties. Categories are autogenerated for boolean configs and cannot be passed
|
||||
|
||||
@@ -69,6 +69,9 @@ service:
|
||||
type: optional<string>
|
||||
allow-multiple: true
|
||||
docs: Only scores linked to traces that include all of these tags will be returned.
|
||||
fields:
|
||||
type: optional<string>
|
||||
docs: "Comma-separated list of field groups to include in the response. Available field groups: 'score' (core score fields), 'trace' (trace properties: userId, tags, environment). If not specified, both 'score' and 'trace' are returned by default. Example: 'score' to exclude trace data, 'score,trace' to include both. Note: When filtering by trace properties (using userId or traceTags parameters), the 'trace' field group must be included, otherwise a 400 error will be returned."
|
||||
response: GetScoresResponse
|
||||
get-by-id:
|
||||
docs: Get a score (supports both trace and session scores)
|
||||
|
||||
+8
-4
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "langfuse",
|
||||
"version": "3.141.0",
|
||||
"version": "3.149.0",
|
||||
"author": "engineering@langfuse.com",
|
||||
"license": "MIT",
|
||||
"private": true,
|
||||
@@ -21,6 +21,9 @@
|
||||
"dx-f": "pnpm i && pnpm run infra:dev:prune && pnpm run infra:dev:up --pull always && pnpm --filter=shared run db:reset:test && pnpm --filter=shared run db:reset -f && SKIP_CONFIRM=1 pnpm --filter=shared run ch:reset && pnpm --filter=shared run db:seed:examples && pnpm run dev",
|
||||
"dx:skip-infra": "pnpm i && pnpm --filter=shared run db:reset:test && pnpm --filter=shared run db:reset && pnpm --filter=shared run ch:reset && pnpm --filter=shared run db:seed:examples && pnpm run dev",
|
||||
"build": "turbo run build",
|
||||
"build:check": "turbo run build:check",
|
||||
"typecheck": "turbo run typecheck",
|
||||
"tc": "turbo run typecheck",
|
||||
"start": "turbo run start",
|
||||
"dev": "turbo run dev",
|
||||
"dev:worker": "turbo run dev --filter=worker",
|
||||
@@ -40,7 +43,7 @@
|
||||
"husky": "^9.1.7",
|
||||
"prettier": "^3.6.2",
|
||||
"release-it": "^19.0.4",
|
||||
"turbo": "^2.6.3"
|
||||
"turbo": "^2.7.5"
|
||||
},
|
||||
"release-it": {
|
||||
"git": {
|
||||
@@ -93,10 +96,11 @@
|
||||
"tar-fs": "^2.1.2",
|
||||
"rollup@^4.0.0": "^4.22.4",
|
||||
"@types/node-fetch": "^2.6.13",
|
||||
"glob": "^10.5.0"
|
||||
"glob": "^10.5.0",
|
||||
"qs": "6.14.1"
|
||||
},
|
||||
"patchedDependencies": {
|
||||
"next-auth@4.24.12": "patches/next-auth@4.24.12.patch"
|
||||
"next-auth@4.24.13": "patches/next-auth@4.24.13.patch"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,87 @@
|
||||
import js from "@eslint/js";
|
||||
import globals from "globals";
|
||||
import tseslint from "typescript-eslint";
|
||||
import turboConfig from "eslint-config-turbo/flat";
|
||||
import eslintPluginPrettierRecommended from "eslint-plugin-prettier/recommended";
|
||||
import "eslint-plugin-only-warn";
|
||||
|
||||
export default tseslint.config(
|
||||
// Global ignores
|
||||
{
|
||||
name: "langfuse/ignores",
|
||||
ignores: [
|
||||
"**/node_modules/",
|
||||
"**/dist/",
|
||||
"**/build/",
|
||||
"**/coverage/",
|
||||
"**/.next/",
|
||||
"**/.*",
|
||||
"eslint.config.mjs",
|
||||
],
|
||||
},
|
||||
|
||||
// Base JS rules (same as eslint v8 library.js)
|
||||
js.configs.recommended,
|
||||
|
||||
// Turbo monorepo rules
|
||||
...turboConfig,
|
||||
|
||||
// Prettier (last for rule precedence)
|
||||
eslintPluginPrettierRecommended,
|
||||
|
||||
// Global settings
|
||||
{
|
||||
name: "langfuse/base/globals",
|
||||
languageOptions: {
|
||||
ecmaVersion: 2022,
|
||||
sourceType: "module",
|
||||
globals: {
|
||||
...globals.node,
|
||||
...globals.es2021,
|
||||
React: "readonly",
|
||||
JSX: "readonly",
|
||||
},
|
||||
},
|
||||
rules: {
|
||||
"no-redeclare": "off",
|
||||
"import/order": "off",
|
||||
},
|
||||
},
|
||||
|
||||
// TypeScript-specific - parser only + custom rules
|
||||
// Note: Old library.js had no TS rules, only eslint:recommended
|
||||
// Adding parser + plugin to support custom rules, but not extending recommended
|
||||
{
|
||||
name: "langfuse/base/typescript",
|
||||
files: ["**/*.ts", "**/*.tsx", "**/*.mts", "**/*.cts"],
|
||||
plugins: {
|
||||
"@typescript-eslint": tseslint.plugin,
|
||||
},
|
||||
languageOptions: {
|
||||
parser: tseslint.parser,
|
||||
},
|
||||
rules: {
|
||||
"no-undef": "off", // TypeScript handles this
|
||||
"no-dupe-class-members": "off", // TypeScript handles this (and supports overloads)
|
||||
"no-unused-vars": "off", // Use @typescript-eslint/no-unused-vars instead
|
||||
"no-restricted-globals": [
|
||||
"error",
|
||||
{
|
||||
name: "redis",
|
||||
message: "Import redis explicitly from '@langfuse/shared/src/server'",
|
||||
},
|
||||
],
|
||||
// Custom rule from eslint v8 shared/.eslintrc.js
|
||||
"@typescript-eslint/no-unused-vars": [
|
||||
"warn",
|
||||
{
|
||||
argsIgnorePattern: "^_",
|
||||
varsIgnorePattern: "^_",
|
||||
caughtErrorsIgnorePattern: "^_",
|
||||
destructuredArrayIgnorePattern: "^_",
|
||||
ignoreRestSiblings: true,
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
);
|
||||
@@ -0,0 +1,2 @@
|
||||
// @ts-check
|
||||
export { default } from "./base.js";
|
||||
@@ -1,62 +0,0 @@
|
||||
const { resolve } = require("node:path");
|
||||
|
||||
const project = resolve(process.cwd(), "tsconfig.json");
|
||||
|
||||
// Handle eslint-config-turbo's default export
|
||||
const turboConfig = require("eslint-config-turbo");
|
||||
const turboConfigToUse = turboConfig.default || turboConfig;
|
||||
|
||||
/** @type {import("eslint").Linter.Config} */
|
||||
module.exports = {
|
||||
// extends: ["eslint:recommended", "prettier"require().default],
|
||||
extends: ["eslint:recommended", "prettier"],
|
||||
plugins: ["only-warn", "turbo"],
|
||||
globals: {
|
||||
React: true,
|
||||
JSX: true,
|
||||
},
|
||||
env: {
|
||||
node: true,
|
||||
es6: true,
|
||||
},
|
||||
settings: {
|
||||
"import/resolver": {
|
||||
typescript: {
|
||||
project,
|
||||
},
|
||||
},
|
||||
},
|
||||
ignorePatterns: [
|
||||
// Ignore dotfiles
|
||||
".*.js",
|
||||
"node_modules/",
|
||||
"dist/",
|
||||
],
|
||||
rules: {
|
||||
"no-redeclare": "off",
|
||||
"import/order": "off",
|
||||
...(turboConfigToUse.rules || {}),
|
||||
},
|
||||
overrides: [
|
||||
{
|
||||
files: ["*.js?(x)", "*.ts?(x)"],
|
||||
},
|
||||
{
|
||||
files: ["*.ts", "*.mts", "*.cts", "*.tsx"],
|
||||
// no-undef doesn't make sense in TS, see:
|
||||
// https://typescript-eslint.io/troubleshooting/faqs/eslint/#i-get-errors-from-the-no-undef-rule-about-global-variables-not-being-defined-even-though-there-are-no-typescript-errors
|
||||
rules: {
|
||||
"no-undef": "off",
|
||||
"no-restricted-globals": [
|
||||
"error",
|
||||
{
|
||||
name: "redis",
|
||||
message:
|
||||
"Import redis explicitly from '@langfuse/shared/src/server' instead of using global.",
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
...(turboConfigToUse.overrides || []),
|
||||
],
|
||||
};
|
||||
@@ -1,79 +1,92 @@
|
||||
const { resolve } = require("node:path");
|
||||
import tseslint from "typescript-eslint";
|
||||
import { FlatCompat } from "@eslint/eslintrc";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import path from "node:path";
|
||||
import eslintPluginPrettierRecommended from "eslint-plugin-prettier/recommended";
|
||||
import turboConfig from "eslint-config-turbo/flat";
|
||||
import "eslint-plugin-only-warn";
|
||||
|
||||
const project = resolve(process.cwd(), "tsconfig.json");
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
const __dirname = path.dirname(__filename);
|
||||
|
||||
const turboConfig = require("eslint-config-turbo");
|
||||
const turboConfigToUse = turboConfig.default || turboConfig;
|
||||
const compat = new FlatCompat({
|
||||
baseDirectory: __dirname,
|
||||
});
|
||||
|
||||
/*
|
||||
* This is a custom ESLint configuration for use with
|
||||
* Next.js apps.
|
||||
*
|
||||
* This config extends the Vercel Engineering Style Guide.
|
||||
* For more information, see https://github.com/vercel/style-guide
|
||||
*
|
||||
*/
|
||||
|
||||
module.exports = {
|
||||
parser: "@typescript-eslint/parser", // Set the parser to @typescript-eslint/parser
|
||||
extends: [
|
||||
"plugin:@typescript-eslint/recommended",
|
||||
"plugin:@typescript-eslint/strict-type-checked",
|
||||
],
|
||||
rules: {
|
||||
"@typescript-eslint/no-non-null-assertion": "off",
|
||||
"@typescript-eslint/no-confusing-void-expression": "off",
|
||||
export default tseslint.config(
|
||||
// Global ignores - include config files
|
||||
{
|
||||
name: "langfuse/ignores",
|
||||
ignores: [
|
||||
"**/node_modules/",
|
||||
"**/dist/",
|
||||
"**/.next/",
|
||||
"**/coverage/",
|
||||
"eslint.config.mjs",
|
||||
],
|
||||
},
|
||||
parser: "@typescript-eslint/parser",
|
||||
|
||||
parserOptions: {
|
||||
project,
|
||||
},
|
||||
globals: {
|
||||
React: true,
|
||||
JSX: true,
|
||||
},
|
||||
plugins: ["@typescript-eslint"],
|
||||
extends: ["next/core-web-vitals"],
|
||||
env: {
|
||||
es6: true,
|
||||
jest: true,
|
||||
},
|
||||
settings: {
|
||||
"import/resolver": {
|
||||
typescript: {
|
||||
project,
|
||||
},
|
||||
// Next.js rules via FlatCompat (applies to all files)
|
||||
...compat.extends("next/core-web-vitals"),
|
||||
|
||||
// Turbo rules
|
||||
...turboConfig,
|
||||
|
||||
// Disable noisy turbo env var rule - project has many env vars not in turbo.json
|
||||
{
|
||||
name: "langfuse/next/turbo-overrides",
|
||||
rules: {
|
||||
"turbo/no-undeclared-env-vars": "off",
|
||||
},
|
||||
},
|
||||
ignorePatterns: ["node_modules/", "dist/"],
|
||||
// add rules configurations here
|
||||
rules: {
|
||||
...(turboConfigToUse.rules || {}),
|
||||
"@typescript-eslint/consistent-type-imports": [
|
||||
"warn",
|
||||
{
|
||||
prefer: "type-imports",
|
||||
fixStyle: "inline-type-imports",
|
||||
|
||||
// Prettier (last)
|
||||
eslintPluginPrettierRecommended,
|
||||
|
||||
// TypeScript config for TS files
|
||||
// Note: The old config had a bug (duplicate extends) that prevented TS rules from applying
|
||||
// Only adding parser + plugin + custom rules to match old behavior
|
||||
{
|
||||
name: "langfuse/next/typescript",
|
||||
files: ["**/*.ts", "**/*.tsx"],
|
||||
plugins: {
|
||||
"@typescript-eslint": tseslint.plugin,
|
||||
},
|
||||
languageOptions: {
|
||||
parser: tseslint.parser,
|
||||
globals: {
|
||||
React: "readonly",
|
||||
JSX: "readonly",
|
||||
},
|
||||
],
|
||||
"@typescript-eslint/no-unused-vars": [
|
||||
// see: https://typescript-eslint.io/rules/no-unused-vars/#why-does-this-rule-report-variables-used-only-for-types
|
||||
// since v8, vars only used for types are unused at runtime and therefore throw a warning.
|
||||
// we fix those with workarounds (mostly just export the variables, as they are used as part of an API anyways)
|
||||
// see: https://github.com/typescript-eslint/typescript-eslint/issues/10266
|
||||
"warn",
|
||||
{
|
||||
argsIgnorePattern: "^_",
|
||||
varsIgnorePattern: "^_",
|
||||
caughtErrorsIgnorePattern: "^_",
|
||||
},
|
||||
settings: {
|
||||
"import/resolver": {
|
||||
typescript: {
|
||||
project: "./tsconfig.json",
|
||||
},
|
||||
},
|
||||
],
|
||||
"react/jsx-key": [
|
||||
"error",
|
||||
{
|
||||
warnOnDuplicates: true,
|
||||
},
|
||||
],
|
||||
},
|
||||
rules: {
|
||||
"no-unused-vars": "off", // Use @typescript-eslint/no-unused-vars instead
|
||||
// Custom rules from old config
|
||||
"@typescript-eslint/consistent-type-imports": [
|
||||
"warn",
|
||||
{
|
||||
prefer: "type-imports",
|
||||
fixStyle: "inline-type-imports",
|
||||
},
|
||||
],
|
||||
"@typescript-eslint/no-unused-vars": [
|
||||
"warn",
|
||||
{
|
||||
argsIgnorePattern: "^_",
|
||||
varsIgnorePattern: "^_",
|
||||
caughtErrorsIgnorePattern: "^_",
|
||||
destructuredArrayIgnorePattern: "^_",
|
||||
ignoreRestSiblings: true,
|
||||
},
|
||||
],
|
||||
"react/jsx-key": ["error", { warnOnDuplicates: true }],
|
||||
},
|
||||
},
|
||||
};
|
||||
);
|
||||
|
||||
@@ -3,18 +3,31 @@
|
||||
"version": "0.0.0",
|
||||
"license": "MIT",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"exports": {
|
||||
".": "./index.js",
|
||||
"./base": "./base.js",
|
||||
"./next": "./next.js"
|
||||
},
|
||||
"files": [
|
||||
"library.js",
|
||||
"index.js",
|
||||
"base.js",
|
||||
"next.js"
|
||||
],
|
||||
"devDependencies": {
|
||||
"@typescript-eslint/eslint-plugin": "^8.48.1",
|
||||
"@typescript-eslint/parser": "^8.48.1",
|
||||
"@vercel/style-guide": "^6.0.0",
|
||||
"dependencies": {
|
||||
"@eslint/compat": "^2.0.0",
|
||||
"@eslint/eslintrc": "^3.3.3",
|
||||
"@eslint/js": "^9.39.2",
|
||||
"eslint-config-next": "15.5.9",
|
||||
"eslint-config-prettier": "^9.1.0",
|
||||
"eslint-config-turbo": "^2.6.3",
|
||||
"eslint-config-prettier": "^10.1.8",
|
||||
"eslint-config-turbo": "^2.7.5",
|
||||
"eslint-plugin-only-warn": "^1.1.0",
|
||||
"typescript": "^5.7.2"
|
||||
"eslint-plugin-prettier": "^5.5.4",
|
||||
"globals": "^16.0.0",
|
||||
"typescript-eslint": "^8.50.1"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"eslint": "^9.39.0",
|
||||
"typescript": ">=5.0.0"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,8 +0,0 @@
|
||||
/** @type {import("eslint").Linter.Config} */
|
||||
module.exports = {
|
||||
extends: ["@repo/eslint-config/library.js"],
|
||||
parser: "@typescript-eslint/parser",
|
||||
parserOptions: {
|
||||
project: true,
|
||||
},
|
||||
};
|
||||
+1
@@ -0,0 +1 @@
|
||||
ALTER TABLE scores ON CLUSTER default DROP COLUMN IF EXISTS long_string_value;
|
||||
+1
@@ -0,0 +1 @@
|
||||
ALTER TABLE scores ON CLUSTER default ADD COLUMN IF NOT EXISTS long_string_value String CODEC(ZSTD(3));
|
||||
+1
@@ -0,0 +1 @@
|
||||
ALTER TABLE scores DROP COLUMN IF EXISTS long_string_value;
|
||||
+1
@@ -0,0 +1 @@
|
||||
ALTER TABLE scores ADD COLUMN IF NOT EXISTS long_string_value String CODEC(ZSTD(3));
|
||||
@@ -75,6 +75,8 @@ clickhouse client \
|
||||
-- Create observations_batch_staging table for batch processing
|
||||
-- This table uses 3-minute partitions to efficiently process observations in batches
|
||||
-- and merge them with traces data into the events table.
|
||||
-- Partitions are automatically expired after 12 hours via TTL (ttl_only_drop_parts=1
|
||||
-- ensures only complete partitions are dropped, not individual rows).
|
||||
-- See LFE-7122 for implementation details.
|
||||
CREATE TABLE IF NOT EXISTS observations_batch_staging
|
||||
(
|
||||
@@ -123,7 +125,9 @@ ORDER BY (
|
||||
toDate(s3_first_seen_timestamp),
|
||||
trace_id,
|
||||
id
|
||||
);
|
||||
)
|
||||
TTL s3_first_seen_timestamp + INTERVAL 12 HOUR
|
||||
SETTINGS ttl_only_drop_parts = 1;
|
||||
|
||||
-- Create new events table for development setups.
|
||||
-- We expect this to be fully immutable and eventually replace observations.
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
import baseConfig from "@repo/eslint-config";
|
||||
|
||||
export default [...baseConfig];
|
||||
@@ -25,6 +25,10 @@
|
||||
"import": "./dist/src/server/auth/apiKeys.js",
|
||||
"require": "./dist/src/server/auth/apiKeys.js"
|
||||
},
|
||||
"./src/utils/chatml": {
|
||||
"import": "./dist/src/utils/chatml/index.js",
|
||||
"require": "./dist/src/utils/chatml/index.js"
|
||||
},
|
||||
"./encryption": {
|
||||
"import": "./dist/src/encryption/index.js",
|
||||
"require": "./dist/src/encryption/index.js"
|
||||
@@ -32,9 +36,11 @@
|
||||
},
|
||||
"scripts": {
|
||||
"build": "tsc",
|
||||
"build:check": "tsc",
|
||||
"typecheck": "dotenv -e ../../.env -- tsc --noEmit --incremental --tsBuildInfoFile .tsbuildinfo",
|
||||
"dev": "tsc --watch",
|
||||
"lint": "eslint . --ext .js,.jsx,.ts,.tsx --max-warnings 0",
|
||||
"lint:fix": "eslint . --ext .js,.jsx,.ts,.tsx --fix",
|
||||
"lint": "eslint . --cache --cache-location dist/.eslintcache --max-warnings 0",
|
||||
"lint:fix": "eslint . --cache --cache-location dist/.eslintcache --fix",
|
||||
"db:migrate": "DISABLE_ERD=false dotenv -e ../../.env -- npx prisma migrate dev",
|
||||
"db:push": "DISABLE_ERD=false dotenv -e ../../.env -- npx prisma db push",
|
||||
"db:reset": "dotenv -e ../../.env npx -- prisma migrate reset",
|
||||
@@ -63,12 +69,12 @@
|
||||
"@aws-sdk/s3-request-presigner": "^3.679.0",
|
||||
"@azure/storage-blob": "^12.26.0",
|
||||
"@clickhouse/client": "^1.13.0",
|
||||
"@google-cloud/storage": "^7.17.0",
|
||||
"@google-cloud/storage": "^7.18.0",
|
||||
"@langchain/anthropic": "^0.3.32",
|
||||
"@langchain/aws": "^0.1.15",
|
||||
"@langchain/core": "^0.3.58",
|
||||
"@langchain/google-genai": "^0.2.12",
|
||||
"@langchain/google-vertexai": "^0.2.12",
|
||||
"@langchain/core": "^0.3.80",
|
||||
"@langchain/google-genai": "^0.2.18",
|
||||
"@langchain/google-vertexai": "^0.2.18",
|
||||
"@langchain/openai": "^0.5.13",
|
||||
"@opentelemetry/api": ">=1.0.0 <1.10.0",
|
||||
"@prisma/client": "^6.17.1",
|
||||
@@ -90,11 +96,11 @@
|
||||
"ipaddr.js": "^2.2.0",
|
||||
"jsonpath-plus": "10.3.0",
|
||||
"kysely": "^0.27.4",
|
||||
"langchain": "^0.3.30",
|
||||
"langchain": "^0.3.37",
|
||||
"langfuse-langchain": "3.38.6",
|
||||
"lodash": "^4.17.21",
|
||||
"lodash": "^4.17.23",
|
||||
"lossless-json": "^4.1.1",
|
||||
"next-auth": "^4.24.12",
|
||||
"next-auth": "^4.24.13",
|
||||
"nodemailer": "^7.0.11",
|
||||
"prisma-extension-kysely": "^3.0.0",
|
||||
"safe-regex2": "^5.0.0",
|
||||
@@ -106,21 +112,20 @@
|
||||
"devDependencies": {
|
||||
"@repo/eslint-config": "workspace:*",
|
||||
"@repo/typescript-config": "workspace:*",
|
||||
"@types/lodash": "^4.17.10",
|
||||
"@types/lodash": "^4.17.23",
|
||||
"@types/node": "^24.3.0",
|
||||
"@types/nodemailer": "^7.0.4",
|
||||
"@types/pg": "^8.11.10",
|
||||
"@types/react": "19.2.3",
|
||||
"@types/uuid": "^9.0.8",
|
||||
"@typescript-eslint/parser": "^8.48.1",
|
||||
"eslint": "^8.57.0",
|
||||
"eslint-config-prettier": "^9.1.0",
|
||||
"@typescript-eslint/parser": "^8.50.1",
|
||||
"eslint": "^9.39.2",
|
||||
"eslint-config-prettier": "^10.1.8",
|
||||
"eslint-config-standard": "^17.1.0",
|
||||
"eslint-plugin-prettier": "^5.1.3",
|
||||
"eslint-plugin-prettier": "^5.5.4",
|
||||
"kysely-codegen": "^0.16.8",
|
||||
"prettier": "^3.6.2",
|
||||
"prisma": "^6.17.1",
|
||||
"prisma-erd-generator": "^2.1.0",
|
||||
"prisma-kysely": "^1.8.0",
|
||||
"ts-node": "^10.9.2",
|
||||
"tsc-watch": "^6.2.0",
|
||||
|
||||
@@ -47,12 +47,13 @@ export const LegacyPrismaScoreSource = {
|
||||
} as const;
|
||||
export type LegacyPrismaScoreSource =
|
||||
(typeof LegacyPrismaScoreSource)[keyof typeof LegacyPrismaScoreSource];
|
||||
export const ScoreDataType = {
|
||||
export const ScoreConfigDataType = {
|
||||
CATEGORICAL: "CATEGORICAL",
|
||||
NUMERIC: "NUMERIC",
|
||||
BOOLEAN: "BOOLEAN",
|
||||
} as const;
|
||||
export type ScoreDataType = (typeof ScoreDataType)[keyof typeof ScoreDataType];
|
||||
export type ScoreConfigDataType =
|
||||
(typeof ScoreConfigDataType)[keyof typeof ScoreConfigDataType];
|
||||
export const AnnotationQueueStatus = {
|
||||
PENDING: "PENDING",
|
||||
COMPLETED: "COMPLETED",
|
||||
@@ -379,6 +380,10 @@ export type Comment = {
|
||||
updated_at: Generated<Timestamp>;
|
||||
content: string;
|
||||
author_user_id: string | null;
|
||||
data_field: string | null;
|
||||
path: Generated<string[]>;
|
||||
range_start: Generated<number[]>;
|
||||
range_end: Generated<number[]>;
|
||||
};
|
||||
export type CommentReaction = {
|
||||
id: string;
|
||||
@@ -447,25 +452,10 @@ export type DatasetItem = {
|
||||
dataset_id: string;
|
||||
created_at: Generated<Timestamp>;
|
||||
updated_at: Generated<Timestamp>;
|
||||
sys_id: Generated<string | null>;
|
||||
valid_from: Generated<Timestamp>;
|
||||
valid_to: Timestamp | null;
|
||||
is_deleted: Generated<boolean>;
|
||||
};
|
||||
export type DatasetItemEvent = {
|
||||
id: string;
|
||||
item_id: string;
|
||||
project_id: string;
|
||||
dataset_id: string;
|
||||
status: DatasetStatus | null;
|
||||
input: unknown | null;
|
||||
expected_output: unknown | null;
|
||||
metadata: unknown | null;
|
||||
source_trace_id: string | null;
|
||||
source_observation_id: string | null;
|
||||
created_at: Timestamp | null;
|
||||
deleted_at: Timestamp | null;
|
||||
};
|
||||
export type DatasetRunItems = {
|
||||
id: string;
|
||||
project_id: string;
|
||||
@@ -596,7 +586,7 @@ export type LegacyPrismaScore = {
|
||||
queue_id: string | null;
|
||||
created_at: Generated<Timestamp>;
|
||||
updated_at: Generated<Timestamp>;
|
||||
data_type: Generated<ScoreDataType>;
|
||||
data_type: Generated<ScoreConfigDataType>;
|
||||
};
|
||||
export type LegacyPrismaTrace = {
|
||||
id: string;
|
||||
@@ -834,7 +824,7 @@ export type ScoreConfig = {
|
||||
updated_at: Generated<Timestamp>;
|
||||
project_id: string;
|
||||
name: string;
|
||||
data_type: ScoreDataType;
|
||||
data_type: ScoreConfigDataType;
|
||||
is_archived: Generated<boolean>;
|
||||
min_value: number | null;
|
||||
max_value: number | null;
|
||||
@@ -954,7 +944,6 @@ export type DB = {
|
||||
cron_jobs: CronJobs;
|
||||
dashboard_widgets: DashboardWidget;
|
||||
dashboards: Dashboard;
|
||||
dataset_item_events: DatasetItemEvent;
|
||||
dataset_items: DatasetItem;
|
||||
dataset_run_items: DatasetRunItems;
|
||||
dataset_runs: DatasetRuns;
|
||||
|
||||
+8
@@ -0,0 +1,8 @@
|
||||
/*
|
||||
Warnings:
|
||||
|
||||
- Changed the type of `data_type` on the `score_configs` table. No cast exists, the column would be dropped and recreated, which cannot be done if there is data, since the column is required.
|
||||
|
||||
*/
|
||||
-- Rename enum
|
||||
ALTER TYPE "ScoreDataType" RENAME TO "ScoreConfigDataType";
|
||||
+2
@@ -0,0 +1,2 @@
|
||||
INSERT INTO background_migrations (id, name, script, args)
|
||||
VALUES ('01a0c890-2094-8e2f-c773-cf4d14638fa4', '20260106_encrypt_blob_storage_secrets', 'encryptBlobStorageSecrets', '{}');
|
||||
+14
@@ -0,0 +1,14 @@
|
||||
-- Add inline comment positioning columns to comments table
|
||||
-- These columns enable character-based inline comments on trace/observation IO
|
||||
|
||||
-- data_field: which IO field the comment is on ('input' | 'output' | 'metadata')
|
||||
ALTER TABLE "comments" ADD COLUMN "data_field" TEXT;
|
||||
|
||||
-- path: JSON Path expressions array, e.g., ["$.messages[1].text"]
|
||||
ALTER TABLE "comments" ADD COLUMN "path" TEXT[] DEFAULT '{}';
|
||||
|
||||
-- range_start: start offsets per path (inclusive), UTF-16 code units
|
||||
ALTER TABLE "comments" ADD COLUMN "range_start" INTEGER[] DEFAULT '{}';
|
||||
|
||||
-- range_end: end offsets per path (exclusive), UTF-16 code units
|
||||
ALTER TABLE "comments" ADD COLUMN "range_end" INTEGER[] DEFAULT '{}';
|
||||
+9
@@ -0,0 +1,9 @@
|
||||
/*
|
||||
Warnings:
|
||||
|
||||
- You are about to drop the column `sys_id` on the `dataset_items` table. All the data in the column will be lost.
|
||||
|
||||
*/
|
||||
-- AlterTable
|
||||
ALTER TABLE "dataset_items" DROP COLUMN "sys_id";
|
||||
|
||||
+3
@@ -0,0 +1,3 @@
|
||||
-- DropForeignKey
|
||||
ALTER TABLE "dataset_item_events" DROP CONSTRAINT "dataset_item_events_dataset_id_project_id_fkey";
|
||||
|
||||
+8
@@ -0,0 +1,8 @@
|
||||
/*
|
||||
Warnings:
|
||||
|
||||
- You are about to drop the `dataset_item_events` table. If the table is not empty, all the data it contains will be lost.
|
||||
|
||||
*/
|
||||
-- DropTable
|
||||
DROP TABLE "dataset_item_events";
|
||||
@@ -13,14 +13,6 @@ datasource db {
|
||||
shadowDatabaseUrl = env("SHADOW_DATABASE_URL")
|
||||
}
|
||||
|
||||
generator erd {
|
||||
provider = "prisma-erd-generator"
|
||||
ignoreTables = ["_prisma_migrations", "Session", "Account", "Example"]
|
||||
disabled = true
|
||||
ignoreEnums = true
|
||||
output = "database.svg"
|
||||
}
|
||||
|
||||
generator kysely {
|
||||
provider = "prisma-kysely"
|
||||
|
||||
@@ -457,7 +449,7 @@ model LegacyPrismaScore {
|
||||
queueId String? @map("queue_id")
|
||||
createdAt DateTime @default(now()) @map("created_at")
|
||||
updatedAt DateTime @default(now()) @updatedAt @map("updated_at")
|
||||
dataType ScoreDataType @default(NUMERIC) @map("data_type")
|
||||
dataType ScoreConfigDataType @default(NUMERIC) @map("data_type")
|
||||
scoreConfig ScoreConfig? @relation(fields: [configId], references: [id], onDelete: SetNull)
|
||||
|
||||
@@unique([id, projectId]) // used for upserts via prisma
|
||||
@@ -488,7 +480,7 @@ model ScoreConfig {
|
||||
projectId String @map("project_id")
|
||||
project Project @relation(fields: [projectId], references: [id], onDelete: Cascade)
|
||||
name String
|
||||
dataType ScoreDataType @map("data_type")
|
||||
dataType ScoreConfigDataType @map("data_type")
|
||||
isArchived Boolean @default(false) @map("is_archived")
|
||||
minValue Float? @map("min_value")
|
||||
maxValue Float? @map("max_value")
|
||||
@@ -506,7 +498,7 @@ model ScoreConfig {
|
||||
@@map("score_configs")
|
||||
}
|
||||
|
||||
enum ScoreDataType {
|
||||
enum ScoreConfigDataType {
|
||||
CATEGORICAL
|
||||
NUMERIC
|
||||
BOOLEAN
|
||||
@@ -606,7 +598,6 @@ model Dataset {
|
||||
updatedAt DateTime @default(now()) @updatedAt @map("updated_at")
|
||||
datasetItems DatasetItem[]
|
||||
datasetRuns DatasetRuns[]
|
||||
datasetItemEvents DatasetItemEvent[]
|
||||
|
||||
@@id([id, projectId])
|
||||
@@unique([projectId, name])
|
||||
@@ -629,7 +620,6 @@ model DatasetItem {
|
||||
createdAt DateTime @default(now()) @map("created_at")
|
||||
updatedAt DateTime @default(now()) @updatedAt @map("updated_at")
|
||||
// dataset version cols
|
||||
sysId String? @default(dbgenerated()) @map("sys_id")
|
||||
validFrom DateTime @default(now()) @map("valid_from")
|
||||
validTo DateTime? @map("valid_to")
|
||||
isDeleted Boolean @default(false) @map("is_deleted")
|
||||
@@ -645,25 +635,6 @@ model DatasetItem {
|
||||
@@map("dataset_items")
|
||||
}
|
||||
|
||||
model DatasetItemEvent {
|
||||
id String @id @default(cuid())
|
||||
itemId String @map("item_id")
|
||||
projectId String @map("project_id")
|
||||
datasetId String @map("dataset_id")
|
||||
dataset Dataset @relation(fields: [datasetId, projectId], references: [id, projectId], onDelete: Cascade)
|
||||
status DatasetStatus?
|
||||
input Json?
|
||||
expectedOutput Json? @map("expected_output")
|
||||
metadata Json?
|
||||
sourceTraceId String? @map("source_trace_id")
|
||||
sourceObservationId String? @map("source_observation_id")
|
||||
createdAt DateTime? @map("created_at")
|
||||
deletedAt DateTime? @map("deleted_at")
|
||||
|
||||
@@index([projectId, datasetId, itemId, createdAt])
|
||||
@@map("dataset_item_events")
|
||||
}
|
||||
|
||||
enum DatasetStatus {
|
||||
ACTIVE
|
||||
ARCHIVED
|
||||
@@ -722,6 +693,15 @@ model Comment {
|
||||
authorUserId String? @map("author_user_id") // no fk constraint, user can be deleted
|
||||
reactions CommentReaction[]
|
||||
|
||||
// Inline comment positioning (all must be set together or all null/empty)
|
||||
// dataField: which IO field the comment is on ('input' | 'output' | 'metadata')
|
||||
// path: Array of JSON Path expressions, e.g., ["$.messages[1].text"]
|
||||
// rangeStart/rangeEnd: parallel arrays for start/end offsets per path (exclusive end, UTF-16 code units)
|
||||
dataField String? @map("data_field")
|
||||
path String[] @default([]) @map("path")
|
||||
rangeStart Int[] @default([]) @map("range_start")
|
||||
rangeEnd Int[] @default([]) @map("range_end")
|
||||
|
||||
@@index([projectId, objectType, objectId])
|
||||
@@map("comments")
|
||||
}
|
||||
|
||||
@@ -0,0 +1,285 @@
|
||||
/**
|
||||
* Media Seeding Utilities
|
||||
*
|
||||
* Seeds test traces with media attachments (images, PDFs, audio) for testing
|
||||
* the JSON Beta view's media rendering capabilities.
|
||||
*
|
||||
* Integrated into the seeder flow - runs automatically during `dx` or `db:seed:examples`.
|
||||
*/
|
||||
|
||||
import crypto from "crypto";
|
||||
import fs from "fs";
|
||||
import path from "path";
|
||||
import { Readable } from "stream";
|
||||
|
||||
import { prisma } from "../../src/db";
|
||||
import { env } from "../../src/env";
|
||||
import { logger, StorageServiceFactory } from "../../src/server";
|
||||
|
||||
// Test file paths (relative to monorepo root)
|
||||
const TEST_FILES_DIR = path.join(
|
||||
__dirname,
|
||||
"../../../../web/src/__tests__/static",
|
||||
);
|
||||
|
||||
interface MediaFile {
|
||||
name: string;
|
||||
contentType: string;
|
||||
filePath: string;
|
||||
}
|
||||
|
||||
const MEDIA_FILES: Record<string, MediaFile> = {
|
||||
image: {
|
||||
name: "langfuse-logo.png",
|
||||
contentType: "image/png",
|
||||
filePath: path.join(TEST_FILES_DIR, "langfuse-logo.png"),
|
||||
},
|
||||
pdf: {
|
||||
name: "bitcoin.pdf",
|
||||
contentType: "application/pdf",
|
||||
filePath: path.join(TEST_FILES_DIR, "bitcoin.pdf"),
|
||||
},
|
||||
audio: {
|
||||
name: "sounds-of-mars.wav",
|
||||
contentType: "audio/wav",
|
||||
filePath: path.join(
|
||||
TEST_FILES_DIR,
|
||||
"sounds-of-mars-one-small-step-earth.wav",
|
||||
),
|
||||
},
|
||||
};
|
||||
|
||||
// Deterministic trace IDs for media test traces
|
||||
export const MEDIA_TEST_TRACE_IDS = {
|
||||
imageOnly: "seed-media-image-only",
|
||||
allTypes: "seed-media-all-types",
|
||||
allTypesChatML: "seed-media-all-types-chatml",
|
||||
} as const;
|
||||
|
||||
/**
|
||||
* Derive media ID from SHA256 hash (first 22 chars, URL-safe)
|
||||
*/
|
||||
function getMediaIdFromHash(sha256Hash: string): string {
|
||||
const urlSafeHash = sha256Hash.replaceAll("+", "-").replaceAll("/", "_");
|
||||
return urlSafeHash.slice(0, 22);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get bucket path for a media file
|
||||
*/
|
||||
function getBucketPath(
|
||||
projectId: string,
|
||||
mediaId: string,
|
||||
contentType: string,
|
||||
): string {
|
||||
const extensionMap: Record<string, string> = {
|
||||
"image/png": "png",
|
||||
"image/jpeg": "jpg",
|
||||
"application/pdf": "pdf",
|
||||
"audio/wav": "wav",
|
||||
"audio/mpeg": "mp3",
|
||||
};
|
||||
const extension = extensionMap[contentType] || "bin";
|
||||
const prefix = env.LANGFUSE_S3_MEDIA_UPLOAD_PREFIX || "";
|
||||
return `${prefix}${projectId}/${mediaId}.${extension}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Upload a media file to storage and create database records
|
||||
*/
|
||||
async function uploadAndCreateMediaRecord(
|
||||
projectId: string,
|
||||
traceId: string,
|
||||
field: "input" | "output" | "metadata",
|
||||
mediaFile: MediaFile,
|
||||
): Promise<void> {
|
||||
// Check if bucket is configured
|
||||
if (!env.LANGFUSE_S3_MEDIA_UPLOAD_BUCKET) {
|
||||
logger.warn(
|
||||
"[seed-media] LANGFUSE_S3_MEDIA_UPLOAD_BUCKET not configured, skipping media seeding",
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if file exists
|
||||
if (!fs.existsSync(mediaFile.filePath)) {
|
||||
logger.warn(
|
||||
`[seed-media] Test file not found: ${mediaFile.filePath}, skipping`,
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
const fileBytes = fs.readFileSync(mediaFile.filePath);
|
||||
const sha256Hash = crypto
|
||||
.createHash("sha256")
|
||||
.update(fileBytes)
|
||||
.digest("base64");
|
||||
const mediaId = getMediaIdFromHash(sha256Hash);
|
||||
const bucketPath = getBucketPath(projectId, mediaId, mediaFile.contentType);
|
||||
|
||||
// Check if media already exists
|
||||
const existingMedia = await prisma.media.findUnique({
|
||||
where: {
|
||||
projectId_sha256Hash: {
|
||||
projectId,
|
||||
sha256Hash,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
if (existingMedia && existingMedia.uploadHttpStatus === 200) {
|
||||
logger.debug(
|
||||
`[seed-media] Media already exists for ${mediaFile.name}, creating TraceMedia link only`,
|
||||
);
|
||||
|
||||
// Just create the TraceMedia link
|
||||
await prisma.$queryRaw`
|
||||
INSERT INTO "trace_media" ("id", "project_id", "trace_id", "media_id", "field")
|
||||
VALUES (${crypto.randomUUID()}, ${projectId}, ${traceId}, ${existingMedia.id}, ${field})
|
||||
ON CONFLICT DO NOTHING;
|
||||
`;
|
||||
return;
|
||||
}
|
||||
|
||||
// Upload to storage
|
||||
try {
|
||||
const storageClient = StorageServiceFactory.getInstance({
|
||||
bucketName: env.LANGFUSE_S3_MEDIA_UPLOAD_BUCKET,
|
||||
accessKeyId: env.LANGFUSE_S3_MEDIA_UPLOAD_ACCESS_KEY_ID,
|
||||
secretAccessKey: env.LANGFUSE_S3_MEDIA_UPLOAD_SECRET_ACCESS_KEY,
|
||||
endpoint: env.LANGFUSE_S3_MEDIA_UPLOAD_ENDPOINT,
|
||||
region: env.LANGFUSE_S3_MEDIA_UPLOAD_REGION,
|
||||
forcePathStyle: env.LANGFUSE_S3_MEDIA_UPLOAD_FORCE_PATH_STYLE === "true",
|
||||
awsSse: env.LANGFUSE_S3_MEDIA_UPLOAD_SSE,
|
||||
awsSseKmsKeyId: env.LANGFUSE_S3_MEDIA_UPLOAD_SSE_KMS_KEY_ID,
|
||||
});
|
||||
|
||||
await storageClient.uploadFile({
|
||||
fileName: bucketPath,
|
||||
fileType: mediaFile.contentType,
|
||||
data: Readable.from(fileBytes),
|
||||
});
|
||||
|
||||
logger.debug(`[seed-media] Uploaded ${mediaFile.name} to ${bucketPath}`);
|
||||
} catch (error) {
|
||||
logger.error(`[seed-media] Failed to upload ${mediaFile.name}:`, error);
|
||||
return;
|
||||
}
|
||||
|
||||
// Create Media record
|
||||
await prisma.$queryRaw`
|
||||
INSERT INTO "media" (
|
||||
"id",
|
||||
"project_id",
|
||||
"sha_256_hash",
|
||||
"bucket_path",
|
||||
"bucket_name",
|
||||
"content_type",
|
||||
"content_length",
|
||||
"uploaded_at",
|
||||
"upload_http_status"
|
||||
)
|
||||
VALUES (
|
||||
${mediaId},
|
||||
${projectId},
|
||||
${sha256Hash},
|
||||
${bucketPath},
|
||||
${env.LANGFUSE_S3_MEDIA_UPLOAD_BUCKET},
|
||||
${mediaFile.contentType},
|
||||
${BigInt(fileBytes.length)},
|
||||
${new Date()},
|
||||
${200}
|
||||
)
|
||||
ON CONFLICT ("project_id", "sha_256_hash")
|
||||
DO UPDATE SET
|
||||
"bucket_name" = ${env.LANGFUSE_S3_MEDIA_UPLOAD_BUCKET},
|
||||
"bucket_path" = ${bucketPath},
|
||||
"content_type" = ${mediaFile.contentType},
|
||||
"content_length" = ${BigInt(fileBytes.length)},
|
||||
"uploaded_at" = ${new Date()},
|
||||
"upload_http_status" = ${200}
|
||||
`;
|
||||
|
||||
// Create TraceMedia link
|
||||
await prisma.$queryRaw`
|
||||
INSERT INTO "trace_media" ("id", "project_id", "trace_id", "media_id", "field")
|
||||
VALUES (${crypto.randomUUID()}, ${projectId}, ${traceId}, ${mediaId}, ${field})
|
||||
ON CONFLICT DO NOTHING;
|
||||
`;
|
||||
|
||||
logger.info(
|
||||
`[seed-media] Created media record for ${mediaFile.name} -> ${field}`,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Seed media test traces for a project
|
||||
*
|
||||
* Creates two test traces with media attachments:
|
||||
* 1. Image only (in input)
|
||||
* 2. All types (image in input, PDF in output, audio in metadata)
|
||||
*/
|
||||
export async function seedMediaTraces(projectId: string): Promise<void> {
|
||||
logger.info(`[seed-media] Seeding media traces for project ${projectId}`);
|
||||
|
||||
// Check if bucket is configured
|
||||
if (!env.LANGFUSE_S3_MEDIA_UPLOAD_BUCKET) {
|
||||
logger.warn(
|
||||
"[seed-media] LANGFUSE_S3_MEDIA_UPLOAD_BUCKET not configured, skipping media seeding",
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
// Trace 1: Image only (input)
|
||||
const trace1Id = MEDIA_TEST_TRACE_IDS.imageOnly;
|
||||
await uploadAndCreateMediaRecord(
|
||||
projectId,
|
||||
trace1Id,
|
||||
"input",
|
||||
MEDIA_FILES.image,
|
||||
);
|
||||
|
||||
// Trace 2: All media types
|
||||
const trace2Id = MEDIA_TEST_TRACE_IDS.allTypes;
|
||||
await uploadAndCreateMediaRecord(
|
||||
projectId,
|
||||
trace2Id,
|
||||
"input",
|
||||
MEDIA_FILES.image,
|
||||
);
|
||||
await uploadAndCreateMediaRecord(
|
||||
projectId,
|
||||
trace2Id,
|
||||
"output",
|
||||
MEDIA_FILES.pdf,
|
||||
);
|
||||
await uploadAndCreateMediaRecord(
|
||||
projectId,
|
||||
trace2Id,
|
||||
"metadata",
|
||||
MEDIA_FILES.audio,
|
||||
);
|
||||
|
||||
// Trace 3: All media types with ChatML format (pretty-rendered)
|
||||
const trace3Id = MEDIA_TEST_TRACE_IDS.allTypesChatML;
|
||||
await uploadAndCreateMediaRecord(
|
||||
projectId,
|
||||
trace3Id,
|
||||
"input",
|
||||
MEDIA_FILES.image,
|
||||
);
|
||||
await uploadAndCreateMediaRecord(
|
||||
projectId,
|
||||
trace3Id,
|
||||
"output",
|
||||
MEDIA_FILES.pdf,
|
||||
);
|
||||
await uploadAndCreateMediaRecord(
|
||||
projectId,
|
||||
trace3Id,
|
||||
"metadata",
|
||||
MEDIA_FILES.audio,
|
||||
);
|
||||
|
||||
logger.info("[seed-media] Media seeding completed");
|
||||
}
|
||||
@@ -9,7 +9,8 @@ import {
|
||||
PrismaClient,
|
||||
type Project,
|
||||
ScoreConfigCategoryDomain,
|
||||
ScoreDataType,
|
||||
ScoreDataTypeEnum,
|
||||
type ScoreDataTypeType,
|
||||
} from "../../src/index";
|
||||
import { getDisplaySecretKey, hashSecretKey, logger } from "../../src/server";
|
||||
import { redis } from "../../src/server/redis/redis";
|
||||
@@ -30,6 +31,7 @@ import {
|
||||
generateEvalTraceId,
|
||||
} from "./utils/seed-helpers";
|
||||
import { seedDatasetVersions } from "./seed-dataset-versions";
|
||||
import { seedMediaTraces } from "./seed-media";
|
||||
|
||||
const options = {
|
||||
environment: { type: "string" },
|
||||
@@ -349,6 +351,9 @@ async function main() {
|
||||
await createDashboardsAndWidgets([project1, project2]);
|
||||
await seedDatasetVersions(prisma, [project1.id, project2.id]);
|
||||
|
||||
// Seed media test traces (uploads to MinIO + creates Media/TraceMedia records)
|
||||
await seedMediaTraces(project1.id);
|
||||
|
||||
await prisma.llmSchema.createMany({
|
||||
data: [
|
||||
{
|
||||
@@ -753,7 +758,7 @@ async function generateConfigsForProject(projects: Project[]) {
|
||||
{
|
||||
name: string;
|
||||
id: string;
|
||||
dataType: ScoreDataType;
|
||||
dataType: ScoreDataTypeType;
|
||||
categories: ScoreConfigCategoryDomain[] | null;
|
||||
}[]
|
||||
> = new Map();
|
||||
@@ -803,7 +808,7 @@ async function generateConfigs(project: Project) {
|
||||
const configNameAndId: {
|
||||
name: string;
|
||||
id: string;
|
||||
dataType: ScoreDataType;
|
||||
dataType: ScoreDataTypeType;
|
||||
categories: ScoreConfigCategoryDomain[] | null;
|
||||
}[] = [];
|
||||
|
||||
@@ -811,7 +816,7 @@ async function generateConfigs(project: Project) {
|
||||
{
|
||||
id: `config-${v4()}`,
|
||||
name: "manual-score",
|
||||
dataType: ScoreDataType.NUMERIC,
|
||||
dataType: ScoreDataTypeEnum.NUMERIC,
|
||||
projectId: project.id,
|
||||
isArchived: false,
|
||||
},
|
||||
@@ -819,7 +824,7 @@ async function generateConfigs(project: Project) {
|
||||
id: `config-${v4()}`,
|
||||
projectId: project.id,
|
||||
name: "Accuracy",
|
||||
dataType: ScoreDataType.CATEGORICAL,
|
||||
dataType: ScoreDataTypeEnum.CATEGORICAL,
|
||||
categories: [
|
||||
{ label: "Incorrect", value: 0 },
|
||||
{ label: "Partially Correct", value: 1 },
|
||||
@@ -831,7 +836,7 @@ async function generateConfigs(project: Project) {
|
||||
id: `config-${v4()}`,
|
||||
projectId: project.id,
|
||||
name: "Toxicity",
|
||||
dataType: ScoreDataType.BOOLEAN,
|
||||
dataType: ScoreDataTypeEnum.BOOLEAN,
|
||||
categories: [
|
||||
{ label: "True", value: 1 },
|
||||
{ label: "False", value: 0 },
|
||||
@@ -880,7 +885,7 @@ async function generateQueuesForProject(
|
||||
{
|
||||
name: string;
|
||||
id: string;
|
||||
dataType: ScoreDataType;
|
||||
dataType: ScoreDataTypeType;
|
||||
categories: ScoreConfigCategoryDomain[] | null;
|
||||
}[]
|
||||
>,
|
||||
@@ -904,7 +909,7 @@ async function generateQueues(
|
||||
configIdsAndNames: {
|
||||
name: string;
|
||||
id: string;
|
||||
dataType: ScoreDataType;
|
||||
dataType: ScoreDataTypeType;
|
||||
categories: ScoreConfigCategoryDomain[] | null;
|
||||
}[],
|
||||
) {
|
||||
|
||||
@@ -195,7 +195,11 @@ export class ClickHouseQueryBuilder {
|
||||
start_time AS event_ts,
|
||||
0 AS is_deleted,
|
||||
'' AS usage_pricing_tier_id,
|
||||
'' AS usage_pricing_tier_name
|
||||
'' AS usage_pricing_tier_name,
|
||||
map() AS tool_definitions,
|
||||
[] AS tool_calls,
|
||||
[] AS tool_call_names
|
||||
|
||||
FROM numbers(${totalObservations});
|
||||
`;
|
||||
}
|
||||
@@ -250,7 +254,8 @@ export class ClickHouseQueryBuilder {
|
||||
timestamp AS updated_at,
|
||||
timestamp AS event_ts,
|
||||
0 AS is_deleted,
|
||||
NULL AS execution_trace_id
|
||||
NULL AS execution_trace_id,
|
||||
'' AS long_string_value
|
||||
FROM numbers(${totalScores});
|
||||
`;
|
||||
}
|
||||
|
||||
@@ -759,6 +759,8 @@ export class DataGenerator {
|
||||
);
|
||||
|
||||
// 6. GENERATION - Final summary generation
|
||||
const toolObservation = observations[observations.length - 1];
|
||||
const toolName = toolObservation.name || "WebSearchTool";
|
||||
observations.push(
|
||||
createObservation({
|
||||
id: `${traceId}-generation`,
|
||||
@@ -800,6 +802,14 @@ export class DataGenerator {
|
||||
output: this.randomInt(35, 45) / 100000,
|
||||
total: this.randomInt(50, 70) / 100000,
|
||||
},
|
||||
tool_definitions: {
|
||||
WebSearchTool: "Search the web for information",
|
||||
CalculatorTool: "Perform mathematical calculations",
|
||||
WeatherForecastTool: "Get weather forecasts and data",
|
||||
EmailSenderTool: "Send emails to recipients",
|
||||
},
|
||||
tool_calls: ["call_workflow_1"],
|
||||
tool_call_names: [toolName],
|
||||
}),
|
||||
);
|
||||
|
||||
@@ -1121,6 +1131,16 @@ export class DataGenerator {
|
||||
updated_at: end,
|
||||
event_ts: start,
|
||||
is_deleted: 0,
|
||||
tool_definitions: d.tool
|
||||
? {
|
||||
"billing.lookup":
|
||||
"Look up billing records and transaction history",
|
||||
"billing.refund":
|
||||
"Issue refunds for duplicate or erroneous charges",
|
||||
}
|
||||
: undefined,
|
||||
tool_calls: d.tool ? [`call_${index}`] : undefined,
|
||||
tool_call_names: d.tool ? [d.tool.name] : undefined,
|
||||
};
|
||||
|
||||
if (!d.tool) return [baseGen];
|
||||
@@ -1157,6 +1177,8 @@ export class DataGenerator {
|
||||
updated_at: start - 5,
|
||||
event_ts: start - 40,
|
||||
is_deleted: 0,
|
||||
tool_definitions: undefined,
|
||||
tool_calls: undefined,
|
||||
};
|
||||
|
||||
return [toolObs, baseGen];
|
||||
@@ -1184,6 +1206,7 @@ export class DataGenerator {
|
||||
config_id: null,
|
||||
data_type: "NUMERIC",
|
||||
string_value: null,
|
||||
long_string_value: "",
|
||||
queue_id: null,
|
||||
created_at: baseTs,
|
||||
updated_at: baseTs,
|
||||
@@ -1203,6 +1226,7 @@ export class DataGenerator {
|
||||
environment: "default",
|
||||
name: "safe",
|
||||
value: safeVal,
|
||||
long_string_value: "",
|
||||
source: "API",
|
||||
comment: "Content safety",
|
||||
metadata: {},
|
||||
@@ -1230,6 +1254,7 @@ export class DataGenerator {
|
||||
observation_id: null,
|
||||
environment: "default",
|
||||
name: "resolved",
|
||||
long_string_value: "",
|
||||
value: 1,
|
||||
source: "API",
|
||||
comment: "Conversation resolved",
|
||||
|
||||
@@ -7,8 +7,10 @@ import { DataGenerator } from "./data-generators";
|
||||
import { ClickHouseQueryBuilder } from "./clickhouse-builder";
|
||||
import { FrameworkTraceLoader } from "./framework-traces/framework-trace-loader";
|
||||
import { EVAL_TRACE_COUNT, SEED_DATASETS } from "./postgres-seed-constants";
|
||||
import { MEDIA_TEST_TRACE_IDS } from "../seed-media";
|
||||
import {
|
||||
clickhouseClient,
|
||||
createTrace,
|
||||
DatasetRunItemRecordInsertType,
|
||||
logger,
|
||||
ObservationRecordInsertType,
|
||||
@@ -338,6 +340,9 @@ export class SeederOrchestrator {
|
||||
// create traces from real examples for each framework source
|
||||
await this.createFrameworkTraces(projectIds);
|
||||
|
||||
// Create traces for media attachment testing
|
||||
await this.createMediaTestTraces(projectIds);
|
||||
|
||||
// Log completion statistics (commented out to reduce terminal noise)
|
||||
await this.logStatistics();
|
||||
|
||||
@@ -448,4 +453,106 @@ export class SeederOrchestrator {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates test traces for media attachment testing (JSON Beta view).
|
||||
* Use for: Testing media rendering in the trace detail view.
|
||||
*/
|
||||
async createMediaTestTraces(projectIds: string[]): Promise<void> {
|
||||
logger.info(
|
||||
`Creating media test traces for ${projectIds.length} projects.`,
|
||||
);
|
||||
|
||||
const now = Date.now();
|
||||
|
||||
for (const projectId of projectIds) {
|
||||
logger.info(`Processing media test traces for project ${projectId}`);
|
||||
|
||||
const traces: TraceRecordInsertType[] = [
|
||||
// Trace 1: Image only (in input)
|
||||
createTrace({
|
||||
id: MEDIA_TEST_TRACE_IDS.imageOnly,
|
||||
project_id: projectId,
|
||||
name: "Media Test: Image Only",
|
||||
timestamp: now,
|
||||
input: JSON.stringify({
|
||||
message: "This trace has an image attachment in input",
|
||||
description: "Used for testing media rendering in JSON Beta view",
|
||||
}),
|
||||
output: JSON.stringify({ status: "success" }),
|
||||
metadata: { test_type: "media", media_types: "image" },
|
||||
tags: ["media-test", "image"],
|
||||
environment: "default",
|
||||
}),
|
||||
// Trace 2: All media types
|
||||
createTrace({
|
||||
id: MEDIA_TEST_TRACE_IDS.allTypes,
|
||||
project_id: projectId,
|
||||
name: "Media Test: All Types",
|
||||
timestamp: now + 1000,
|
||||
input: JSON.stringify({
|
||||
message: "This trace has an image in input",
|
||||
description: "Testing image attachment",
|
||||
}),
|
||||
output: JSON.stringify({
|
||||
message: "This trace has a PDF in output",
|
||||
description: "Testing PDF attachment",
|
||||
}),
|
||||
metadata: {
|
||||
message: "This trace has audio in metadata",
|
||||
description: "Testing audio attachment",
|
||||
test_type: "media",
|
||||
media_types: "image,pdf,audio",
|
||||
},
|
||||
tags: ["media-test", "all-types"],
|
||||
environment: "default",
|
||||
}),
|
||||
// Trace 3: All media types with ChatML format (pretty-rendered)
|
||||
createTrace({
|
||||
id: MEDIA_TEST_TRACE_IDS.allTypesChatML,
|
||||
project_id: projectId,
|
||||
name: "Media Test: All Types (ChatML)",
|
||||
timestamp: now + 2000,
|
||||
input: JSON.stringify([
|
||||
{
|
||||
role: "system",
|
||||
content:
|
||||
"You are a helpful assistant that can analyze images, documents, and audio files.",
|
||||
},
|
||||
{
|
||||
role: "user",
|
||||
content:
|
||||
"Please analyze the attached image and describe what you see.",
|
||||
},
|
||||
]),
|
||||
output: JSON.stringify([
|
||||
{
|
||||
role: "assistant",
|
||||
content:
|
||||
"I can see the Langfuse logo in the image. It appears to be a modern, clean design with distinctive branding elements. The attached PDF contains additional documentation about the Bitcoin whitepaper.",
|
||||
},
|
||||
]),
|
||||
metadata: {
|
||||
message: "This trace has audio in metadata",
|
||||
description: "Testing audio attachment with ChatML format",
|
||||
test_type: "media",
|
||||
media_types: "image,pdf,audio",
|
||||
format: "chatml",
|
||||
},
|
||||
tags: ["media-test", "all-types", "chatml"],
|
||||
environment: "default",
|
||||
}),
|
||||
];
|
||||
|
||||
try {
|
||||
await this.queryBuilder.executeTracesInsert(traces);
|
||||
logger.info(
|
||||
`✓ Created ${traces.length} media test traces for project ${projectId}`,
|
||||
);
|
||||
} catch (error) {
|
||||
logger.error(`✗ Media test traces insert failed:`, error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
/* eslint-disable no-unused-vars */
|
||||
// disable lint as this is exported and used in packages
|
||||
export enum ModelUsageUnit {
|
||||
Characters = "CHARACTERS",
|
||||
|
||||
@@ -3,7 +3,6 @@ import { FilterState } from "../types";
|
||||
import { z } from "zod/v4";
|
||||
|
||||
export enum TriggerEventSource {
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
Prompt = "prompt",
|
||||
}
|
||||
|
||||
|
||||
@@ -93,6 +93,10 @@ export const ObservationSchema = z.object({
|
||||
// pricing tier information
|
||||
usagePricingTierId: z.string().nullable(),
|
||||
usagePricingTierName: z.string().nullable(),
|
||||
// tool data
|
||||
toolDefinitions: z.record(z.string(), z.string()).nullable(),
|
||||
toolCalls: z.array(z.string()).nullable(),
|
||||
toolCallNames: z.array(z.string()).nullable(),
|
||||
});
|
||||
|
||||
export type Observation = z.infer<typeof ObservationSchema>;
|
||||
|
||||
@@ -10,15 +10,19 @@ export const ScoreSourceEnum = {
|
||||
export const ScoreSourceDomain = z.enum(ScoreSourceArray);
|
||||
export type ScoreSourceType = z.infer<typeof ScoreSourceDomain>;
|
||||
|
||||
export const CORRECTION_NAME = "output" as const;
|
||||
|
||||
export const ScoreDataTypeArray = [
|
||||
"NUMERIC",
|
||||
"CATEGORICAL",
|
||||
"BOOLEAN",
|
||||
"CORRECTION",
|
||||
] as const;
|
||||
export const ScoreDataTypeEnum = {
|
||||
NUMERIC: "NUMERIC",
|
||||
CATEGORICAL: "CATEGORICAL",
|
||||
BOOLEAN: "BOOLEAN",
|
||||
CORRECTION: "CORRECTION",
|
||||
} as const;
|
||||
export const ScoreDataTypeDomain = z.enum(ScoreDataTypeArray);
|
||||
export type ScoreDataTypeType = z.infer<typeof ScoreDataTypeDomain>;
|
||||
@@ -38,6 +42,11 @@ export const BooleanData = z.object({
|
||||
dataType: z.literal("BOOLEAN"),
|
||||
});
|
||||
|
||||
const CorrectionData = z.object({
|
||||
stringValue: z.null(),
|
||||
dataType: z.literal("CORRECTION"),
|
||||
});
|
||||
|
||||
// Only used for backwards compatibility with old score API schemas
|
||||
export const ScoreSchemaExclReferencesAndDates = z.object({
|
||||
// Core identifiers
|
||||
@@ -70,11 +79,43 @@ const ScoreFoundationSchema = ScoreSchemaExclReferencesAndDates.and(
|
||||
sessionId: z.string().nullable(),
|
||||
datasetRunId: z.string().nullable(),
|
||||
observationId: z.string().nullable(),
|
||||
longStringValue: z.string().default(""),
|
||||
}),
|
||||
);
|
||||
|
||||
export const ScoreSchema = ScoreFoundationSchema.and(
|
||||
z.discriminatedUnion("dataType", [NumericData, CategoricalData, BooleanData]),
|
||||
z.discriminatedUnion("dataType", [
|
||||
NumericData,
|
||||
CategoricalData,
|
||||
BooleanData,
|
||||
CorrectionData,
|
||||
]),
|
||||
);
|
||||
|
||||
export type ScoreDomain = z.infer<typeof ScoreSchema>;
|
||||
|
||||
export type ScoreByDataType<T extends ScoreDataTypeType> = ScoreDomain & {
|
||||
dataType: T;
|
||||
};
|
||||
|
||||
export type ScoresByDataTypes<T extends readonly ScoreDataTypeType[]> =
|
||||
T extends readonly (infer U)[]
|
||||
? U extends ScoreDataTypeType
|
||||
? ScoreByDataType<U>
|
||||
: never
|
||||
: never;
|
||||
|
||||
// Aggregatable score types - used in most read queries to exclude CORRECTION scores
|
||||
export const AGGREGATABLE_SCORE_TYPES = [
|
||||
"NUMERIC",
|
||||
"BOOLEAN",
|
||||
"CATEGORICAL",
|
||||
] as const satisfies readonly ScoreDataTypeType[];
|
||||
|
||||
export type AggregatableScoreDataType =
|
||||
(typeof AGGREGATABLE_SCORE_TYPES)[number];
|
||||
|
||||
// Type helper for functions that return only aggregatable scores
|
||||
export type AggregatableScore = ScoresByDataTypes<
|
||||
typeof AGGREGATABLE_SCORE_TYPES
|
||||
>;
|
||||
|
||||
@@ -3,13 +3,14 @@ import { orderBy } from "../interfaces/orderBy";
|
||||
import z from "zod/v4";
|
||||
|
||||
export enum TableViewPresetTableName {
|
||||
Traces = "traces", // eslint-disable-line no-unused-vars
|
||||
Observations = "observations", // eslint-disable-line no-unused-vars
|
||||
Scores = "scores", // eslint-disable-line no-unused-vars
|
||||
Sessions = "sessions", // eslint-disable-line no-unused-vars
|
||||
Datasets = "datasets", // eslint-disable-line no-unused-vars
|
||||
Traces = "traces",
|
||||
Observations = "observations",
|
||||
Scores = "scores",
|
||||
Sessions = "sessions",
|
||||
Datasets = "datasets",
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
const TableViewPresetDomainSchema = z.object({
|
||||
id: z.string(),
|
||||
projectId: z.string().nullable(),
|
||||
|
||||
@@ -60,6 +60,11 @@ const EnvSchema = z.object({
|
||||
// Optional to allow for server-setting fallbacks
|
||||
CLICKHOUSE_ASYNC_INSERT_MAX_DATA_SIZE: z.string().optional(),
|
||||
CLICKHOUSE_ASYNC_INSERT_BUSY_TIMEOUT_MS: z.coerce.number().int().optional(),
|
||||
CLICKHOUSE_ASYNC_INSERT_BUSY_TIMEOUT_MIN_MS: z.coerce
|
||||
.number()
|
||||
.int()
|
||||
.min(50)
|
||||
.optional(),
|
||||
CLICKHOUSE_LIGHTWEIGHT_DELETE_MODE: z
|
||||
.enum(["alter_update", "lightweight_update", "lightweight_update_force"])
|
||||
.default("alter_update"),
|
||||
@@ -192,7 +197,7 @@ const EnvSchema = z.object({
|
||||
}
|
||||
|
||||
return map;
|
||||
} catch (err) {
|
||||
} catch {
|
||||
return new Map<string, number>();
|
||||
}
|
||||
}),
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
import { BaseError } from "./BaseError";
|
||||
|
||||
export class NotImplementedError extends BaseError {
|
||||
constructor(description = "Not Implemented") {
|
||||
super("NotImplementedError", 501, description, true);
|
||||
}
|
||||
}
|
||||
@@ -8,3 +8,4 @@ export { ApiError } from "./ApiError";
|
||||
export { InternalServerError } from "./InternalServerError";
|
||||
export { LangfuseConflictError } from "./ConflictError";
|
||||
export { ServiceUnavailableError } from "./ServiceUnavailableError";
|
||||
export { NotImplementedError } from "./NotImplementedError";
|
||||
|
||||
@@ -73,6 +73,14 @@ export const eventsTableCols: ColumnDefinition[] = [
|
||||
internal: "e.session_id",
|
||||
nullable: true,
|
||||
},
|
||||
{
|
||||
name: "Trace Name",
|
||||
id: "traceName",
|
||||
type: "stringOptions",
|
||||
internal: "e.trace_name",
|
||||
options: [], // to be added at runtime
|
||||
nullable: true,
|
||||
},
|
||||
{
|
||||
name: "Level",
|
||||
id: "level",
|
||||
@@ -235,4 +243,34 @@ export const eventsTableCols: ColumnDefinition[] = [
|
||||
type: "string",
|
||||
internal: "", // handled by comment filter helpers
|
||||
},
|
||||
{
|
||||
name: "Has Parent Observation",
|
||||
id: "hasParentObservation",
|
||||
type: "boolean",
|
||||
internal: "e.parent_span_id != ''",
|
||||
},
|
||||
{
|
||||
name: "Experiment Dataset ID",
|
||||
id: "experimentDatasetId",
|
||||
type: "stringOptions",
|
||||
internal: "e.experiment_dataset_id",
|
||||
options: [], // to be added at runtime
|
||||
nullable: true,
|
||||
},
|
||||
{
|
||||
name: "Experiment ID",
|
||||
id: "experimentId",
|
||||
type: "stringOptions",
|
||||
internal: "e.experiment_id",
|
||||
options: [], // to be added at runtime
|
||||
nullable: true,
|
||||
},
|
||||
{
|
||||
name: "Experiment Name",
|
||||
id: "experimentName",
|
||||
type: "stringOptions",
|
||||
internal: "e.experiment_name",
|
||||
options: [], // to be added at runtime
|
||||
nullable: true,
|
||||
},
|
||||
];
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
import { JSONPath } from "jsonpath-plus";
|
||||
import { set } from "lodash";
|
||||
import set from "lodash/set";
|
||||
import type {
|
||||
FieldMappingConfig,
|
||||
SourceField,
|
||||
AddToDatasetMapping,
|
||||
} from "./addToDatasetTypes";
|
||||
import { parseJsonPrioritised } from "../../utils/json";
|
||||
|
||||
type ObservationData = {
|
||||
input: unknown;
|
||||
@@ -37,9 +38,14 @@ export function testJsonPath(props: { jsonPath: string; data: unknown }): {
|
||||
*/
|
||||
export function evaluateJsonPath(data: unknown, jsonPath: string): unknown {
|
||||
try {
|
||||
const parsed = typeof data === "string" ? JSON.parse(data) : data;
|
||||
const results = JSONPath({ path: jsonPath, json: parsed });
|
||||
return results?.[0];
|
||||
const parsed = typeof data === "string" ? parseJsonPrioritised(data) : data;
|
||||
const result = JSONPath({
|
||||
path: jsonPath,
|
||||
json: parsed as string | object,
|
||||
wrap: false,
|
||||
});
|
||||
|
||||
return result;
|
||||
} catch {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
@@ -4,7 +4,6 @@ import { orderBy } from "../../interfaces/orderBy";
|
||||
import { BatchTableNames } from "../../interfaces/tableNames";
|
||||
import { TracingSearchType } from "../../interfaces/search";
|
||||
|
||||
/* eslint-disable no-unused-vars */
|
||||
export enum BatchActionType {
|
||||
Create = "create",
|
||||
Delete = "delete",
|
||||
|
||||
@@ -8,17 +8,17 @@ import { BatchTableNames } from "../../interfaces/tableNames";
|
||||
import { TracingSearchType } from "../../interfaces/search";
|
||||
|
||||
export enum BatchExportStatus {
|
||||
QUEUED = "QUEUED", // eslint-disable-line no-unused-vars
|
||||
PROCESSING = "PROCESSING", // eslint-disable-line no-unused-vars
|
||||
COMPLETED = "COMPLETED", // eslint-disable-line no-unused-vars
|
||||
FAILED = "FAILED", // eslint-disable-line no-unused-vars
|
||||
CANCELLED = "CANCELLED", // eslint-disable-line no-unused-vars
|
||||
QUEUED = "QUEUED",
|
||||
PROCESSING = "PROCESSING",
|
||||
COMPLETED = "COMPLETED",
|
||||
FAILED = "FAILED",
|
||||
CANCELLED = "CANCELLED",
|
||||
}
|
||||
|
||||
export enum BatchExportFileFormat {
|
||||
JSON = "JSON", // eslint-disable-line no-unused-vars
|
||||
CSV = "CSV", // eslint-disable-line no-unused-vars
|
||||
JSONL = "JSONL", // eslint-disable-line no-unused-vars
|
||||
JSON = "JSON",
|
||||
CSV = "CSV",
|
||||
JSONL = "JSONL",
|
||||
}
|
||||
|
||||
// Use shared BatchTableNames enum for consistency across batch operations
|
||||
|
||||
@@ -2,19 +2,73 @@ import { z } from "zod/v4";
|
||||
|
||||
const MAX_COMMENT_LENGTH = 5000;
|
||||
|
||||
const COMMENT_OBJECT_TYPES = [
|
||||
export const COMMENT_OBJECT_TYPES = [
|
||||
"TRACE",
|
||||
"OBSERVATION",
|
||||
"SESSION",
|
||||
"PROMPT",
|
||||
] as const;
|
||||
|
||||
export const CreateCommentData = z.object({
|
||||
projectId: z.string(),
|
||||
content: z.string().trim().min(1).max(MAX_COMMENT_LENGTH),
|
||||
objectId: z.string(),
|
||||
objectType: z.enum(COMMENT_OBJECT_TYPES),
|
||||
});
|
||||
export const COMMENT_DATA_FIELDS = ["input", "output", "metadata"] as const;
|
||||
|
||||
// JSON Path validation
|
||||
// TODO: simplify
|
||||
// Allows: $ (root), $.foo, $[0], $.foo[0].bar, $['key-with-dash'], etc.
|
||||
export const jsonPathSchema = z
|
||||
.string()
|
||||
.regex(
|
||||
/^\$(\.[a-zA-Z_][a-zA-Z0-9_-]*|\[\d+\]|\['[^']+'\]|\["[^"]+"\])*$/,
|
||||
"Invalid JSON Path syntax",
|
||||
);
|
||||
|
||||
export const CreateCommentData = z
|
||||
.object({
|
||||
projectId: z.string(),
|
||||
content: z.string().trim().min(1).max(MAX_COMMENT_LENGTH),
|
||||
objectId: z.string(),
|
||||
objectType: z.enum(COMMENT_OBJECT_TYPES),
|
||||
// Optional inline positioning (parallel arrays)
|
||||
dataField: z.enum(COMMENT_DATA_FIELDS).nullish(),
|
||||
path: z.array(jsonPathSchema).nullish(),
|
||||
rangeStart: z.array(z.number().int().min(0)).nullish(),
|
||||
rangeEnd: z.array(z.number().int().min(1)).nullish(),
|
||||
})
|
||||
.refine(
|
||||
(data) => {
|
||||
const hasDataField = data.dataField != null;
|
||||
const hasPath = data.path != null && data.path.length > 0;
|
||||
const hasRangeStart =
|
||||
data.rangeStart != null && data.rangeStart.length > 0;
|
||||
const hasRangeEnd = data.rangeEnd != null && data.rangeEnd.length > 0;
|
||||
|
||||
// All must be set together, or all must be null/empty
|
||||
if (hasDataField || hasPath || hasRangeStart || hasRangeEnd) {
|
||||
return (
|
||||
hasDataField &&
|
||||
hasPath &&
|
||||
hasRangeStart &&
|
||||
hasRangeEnd &&
|
||||
data.path!.length === data.rangeStart!.length &&
|
||||
data.path!.length === data.rangeEnd!.length
|
||||
);
|
||||
}
|
||||
return true;
|
||||
},
|
||||
{
|
||||
message:
|
||||
"dataField, path, rangeStart, rangeEnd must all be set together with matching lengths, or all be null/empty",
|
||||
},
|
||||
)
|
||||
.refine(
|
||||
(data) => {
|
||||
// Validate each range: start must be < end
|
||||
if (data.rangeStart && data.rangeEnd) {
|
||||
return data.rangeStart.every((start, i) => start < data.rangeEnd![i]);
|
||||
}
|
||||
return true;
|
||||
},
|
||||
{ message: "Each rangeStart must be less than corresponding rangeEnd" },
|
||||
);
|
||||
|
||||
export const DeleteCommentData = z.object({
|
||||
projectId: z.string(),
|
||||
@@ -22,3 +76,5 @@ export const DeleteCommentData = z.object({
|
||||
objectId: z.string(),
|
||||
objectType: z.enum(COMMENT_OBJECT_TYPES),
|
||||
});
|
||||
|
||||
export type CreateCommentInput = z.infer<typeof CreateCommentData>;
|
||||
|
||||
@@ -68,7 +68,7 @@ function parseJsonDefault(selectedColumn: unknown, jsonSelector: string) {
|
||||
export function extractValueFromObject(
|
||||
obj: Record<string, unknown>,
|
||||
mapping: z.infer<typeof variableMapping>,
|
||||
parseJson?: (selectedColumn: unknown, jsonSelector: string) => unknown, // eslint-disable-line no-unused-vars
|
||||
parseJson?: (selectedColumn: unknown, jsonSelector: string) => unknown,
|
||||
): { value: string; error: Error | null } {
|
||||
let selectedColumn = obj[mapping.selectedColumnId];
|
||||
|
||||
|
||||
@@ -14,9 +14,8 @@ export const SingleChatMessageSchema = PromptChatMessageSchema;
|
||||
export type SingleChatMessage = z.infer<typeof SingleChatMessageSchema>;
|
||||
|
||||
export enum PromptType {
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
Chat = "chat",
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
|
||||
Text = "text",
|
||||
}
|
||||
|
||||
|
||||
@@ -13,7 +13,7 @@ import {
|
||||
*/
|
||||
export const filterAndValidateDbScoreConfigList = (
|
||||
scoreConfigs: ScoreConfigDbType[],
|
||||
onParseError?: (error: z.ZodError) => void, // eslint-disable-line no-unused-vars
|
||||
onParseError?: (error: z.ZodError) => void,
|
||||
): ScoreConfigDomain[] =>
|
||||
scoreConfigs.reduce((acc, ts) => {
|
||||
const result = ScoreConfigSchema.safeParse(ts);
|
||||
|
||||
@@ -10,6 +10,9 @@ import {
|
||||
|
||||
const operators = ["<", ">", "<=", ">=", "!=", "="] as const;
|
||||
|
||||
export const SCORE_FIELD_GROUPS = ["score", "trace"] as const;
|
||||
export type ScoreFieldGroup = (typeof SCORE_FIELD_GROUPS)[number];
|
||||
|
||||
/**
|
||||
* Endpoints
|
||||
*/
|
||||
@@ -41,6 +44,17 @@ export const GetScoresQuery = z.object({
|
||||
message: "Each score ID must be a string",
|
||||
})
|
||||
.nullish(),
|
||||
fields: z
|
||||
.string()
|
||||
.nullish()
|
||||
.transform((v) => {
|
||||
if (!v) return null;
|
||||
return v
|
||||
.split(",")
|
||||
.map((f) => f.trim())
|
||||
.filter((f) => SCORE_FIELD_GROUPS.includes(f as ScoreFieldGroup));
|
||||
})
|
||||
.pipe(z.array(z.enum(SCORE_FIELD_GROUPS)).nullable()),
|
||||
});
|
||||
|
||||
// POST /scores
|
||||
@@ -69,6 +83,11 @@ export const PostScoresBody = applyScoreValidation(
|
||||
dataType: z.literal("BOOLEAN"),
|
||||
configId: z.string().nullish(),
|
||||
}),
|
||||
z.object({
|
||||
value: z.string(), // Corrected output text
|
||||
dataType: z.literal("CORRECTION"),
|
||||
configId: z.undefined().nullish(), // Cannot have config
|
||||
}),
|
||||
z.object({
|
||||
value: z.union([z.string(), z.number()]),
|
||||
dataType: z.undefined(),
|
||||
|
||||
@@ -11,7 +11,7 @@ import { GetScoreResponseDataV1 } from "./endpoints";
|
||||
*/
|
||||
export const filterAndValidateV1GetScoreList = (
|
||||
scores: unknown[],
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
|
||||
onParseError?: (error: z.ZodError) => void,
|
||||
): z.infer<typeof GetScoreResponseDataV1>[] =>
|
||||
scores.reduce(
|
||||
|
||||
@@ -13,18 +13,20 @@ export const GetScoresQueryV2 = GetScoresQuery.extend({
|
||||
traceId: z.string().nullish(),
|
||||
datasetRunId: z.string().nullish(),
|
||||
});
|
||||
export const GetScoreResponseDataV2 = z.intersection(
|
||||
APIScoreSchemaV2,
|
||||
z.object({
|
||||
trace: z
|
||||
.object({
|
||||
userId: z.string().nullish(),
|
||||
tags: z.array(z.string()).nullish(),
|
||||
environment: z.string().nullish(),
|
||||
})
|
||||
.nullish(),
|
||||
}),
|
||||
);
|
||||
export const GetScoreResponseDataV2 = z
|
||||
.intersection(
|
||||
APIScoreSchemaV2,
|
||||
z.object({
|
||||
trace: z
|
||||
.object({
|
||||
userId: z.string().nullish(),
|
||||
tags: z.array(z.string()).nullish(),
|
||||
environment: z.string().nullish(),
|
||||
})
|
||||
.nullish(),
|
||||
}),
|
||||
)
|
||||
.or(APIScoreSchemaV2);
|
||||
|
||||
export const GetScoresResponseV2 = z.object({
|
||||
data: z.array(GetScoreResponseDataV2),
|
||||
|
||||
@@ -6,6 +6,11 @@ import {
|
||||
} from "../../../../../domain";
|
||||
import z from "zod/v4";
|
||||
|
||||
const CorrectionData = z.object({
|
||||
stringValue: z.string(),
|
||||
dataType: z.literal("CORRECTION"),
|
||||
});
|
||||
|
||||
/**
|
||||
* Foundation schema for scores API v2 i.e. trace, observation AND session scores
|
||||
*
|
||||
@@ -25,7 +30,12 @@ const ScoreFoundationSchemaV2 = ScoreSchemaExclReferencesAndDates.extend({
|
||||
});
|
||||
|
||||
export const APIScoreSchemaV2 = ScoreFoundationSchemaV2.and(
|
||||
z.discriminatedUnion("dataType", [NumericData, CategoricalData, BooleanData]),
|
||||
z.discriminatedUnion("dataType", [
|
||||
NumericData,
|
||||
CategoricalData,
|
||||
BooleanData,
|
||||
CorrectionData,
|
||||
]),
|
||||
);
|
||||
|
||||
export type APIScoreV2 = z.infer<typeof APIScoreSchemaV2>;
|
||||
|
||||
@@ -9,7 +9,7 @@ import { GetScoreResponseDataV2 } from "./endpoints";
|
||||
*/
|
||||
export const filterAndValidateV2GetScoreList = (
|
||||
scores: unknown[],
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
|
||||
onParseError?: (error: z.ZodError) => void,
|
||||
): z.infer<typeof GetScoreResponseDataV2>[] =>
|
||||
scores.reduce(
|
||||
|
||||
@@ -1,8 +1,16 @@
|
||||
import z from "zod/v4";
|
||||
import { APIScoreSchemaV1, APIScoreV1 } from "../api/v1/schemas";
|
||||
import { ScoreDomain, ScoreSchema } from "../../../../domain";
|
||||
import {
|
||||
ScoreDomain,
|
||||
ScoreSchema,
|
||||
ScoreDataTypeType,
|
||||
ScoresByDataTypes,
|
||||
} from "../../../../domain";
|
||||
|
||||
type ValidatedScore<IncludeHasMetadata extends boolean> = ScoreDomain & {
|
||||
type ValidatedScore<
|
||||
IncludeHasMetadata extends boolean,
|
||||
DataTypes extends readonly ScoreDataTypeType[],
|
||||
> = ScoresByDataTypes<DataTypes> & {
|
||||
hasMetadata: IncludeHasMetadata extends true ? boolean : never;
|
||||
};
|
||||
|
||||
@@ -21,35 +29,50 @@ export const validateDbScore = (score: unknown): ScoreDomain =>
|
||||
/**
|
||||
* Use this function when pulling a list of scores from the database before using in the application to ensure type safety.
|
||||
* All scores are expected to pass the validation. If a score fails validation, it will be logged to Otel.
|
||||
* @param scores
|
||||
* @returns list of validated scores
|
||||
* This function filters scores by the specified data types and validates them.
|
||||
* @param scores - List of scores to filter and validate
|
||||
* @param dataTypes - Array of data types to filter by (required)
|
||||
* @param includeHasMetadata - Whether to include hasMetadata field
|
||||
* @param onParseError - Optional callback for parse errors
|
||||
* @returns list of validated and filtered scores
|
||||
*/
|
||||
export const filterAndValidateDbScoreList = <
|
||||
IncludeHasMetadata extends boolean,
|
||||
DataTypes extends readonly ScoreDataTypeType[],
|
||||
>({
|
||||
scores,
|
||||
dataTypes,
|
||||
includeHasMetadata = false as IncludeHasMetadata,
|
||||
onParseError,
|
||||
}: {
|
||||
scores: InputScore[];
|
||||
dataTypes: DataTypes;
|
||||
includeHasMetadata?: IncludeHasMetadata;
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
|
||||
onParseError?: (error: z.ZodError) => void;
|
||||
}): ValidatedScore<IncludeHasMetadata>[] => {
|
||||
return scores.reduce((acc, ts) => {
|
||||
const result = ScoreSchema.safeParse(ts);
|
||||
if (result.success) {
|
||||
const score = { ...result.data };
|
||||
if (includeHasMetadata) {
|
||||
Object.assign(score, { hasMetadata: ts.hasMetadata ?? false });
|
||||
}): ValidatedScore<IncludeHasMetadata, DataTypes>[] => {
|
||||
return scores.reduce(
|
||||
(acc, ts) => {
|
||||
// Filter by dataType first
|
||||
if (!dataTypes.includes(ts.dataType)) {
|
||||
return acc;
|
||||
}
|
||||
acc.push(score as ValidatedScore<IncludeHasMetadata>);
|
||||
} else {
|
||||
console.error("Score parsing error: ", result.error);
|
||||
onParseError?.(result.error);
|
||||
}
|
||||
return acc;
|
||||
}, [] as ValidatedScore<IncludeHasMetadata>[]);
|
||||
|
||||
const result = ScoreSchema.safeParse(ts);
|
||||
if (result.success) {
|
||||
const score = { ...result.data };
|
||||
if (includeHasMetadata) {
|
||||
Object.assign(score, { hasMetadata: ts.hasMetadata ?? false });
|
||||
}
|
||||
acc.push(score as ValidatedScore<IncludeHasMetadata, DataTypes>);
|
||||
} else {
|
||||
console.error("Score parsing error: ", result.error);
|
||||
onParseError?.(result.error);
|
||||
}
|
||||
return acc;
|
||||
},
|
||||
[] as ValidatedScore<IncludeHasMetadata, DataTypes>[],
|
||||
);
|
||||
};
|
||||
|
||||
type ValidatedAPITraceScore<IncludeHasMetadata extends boolean> = APIScoreV1 & {
|
||||
@@ -73,7 +96,7 @@ export const filterAndValidateDbTraceScoreList = <
|
||||
}: {
|
||||
scores: InputScore[];
|
||||
includeHasMetadata?: IncludeHasMetadata;
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
|
||||
onParseError?: (error: z.ZodError) => void;
|
||||
}): ValidatedAPITraceScore<IncludeHasMetadata>[] => {
|
||||
return scores.reduce((acc, ts) => {
|
||||
|
||||
@@ -18,6 +18,12 @@ export const ScoreBodyWithoutConfig = applyScoreValidation(
|
||||
dataType: z.literal("CATEGORICAL"),
|
||||
}),
|
||||
),
|
||||
PostScoreBodyFoundationSchema.merge(
|
||||
z.object({
|
||||
value: z.string(),
|
||||
dataType: z.literal("CORRECTION"),
|
||||
}),
|
||||
),
|
||||
PostScoreBodyFoundationSchema.merge(
|
||||
z.object({
|
||||
value: z.number().refine((val) => val === 0 || val === 1, {
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
import { ScoreDataType } from "@prisma/client";
|
||||
import { MetadataDomain, ScoreSourceType } from "../../../../domain";
|
||||
import {
|
||||
AggregatableScoreDataType,
|
||||
MetadataDomain,
|
||||
ScoreSourceType,
|
||||
} from "../../../../domain";
|
||||
|
||||
export type BaseAggregate = {
|
||||
comment?: string | null;
|
||||
@@ -27,7 +30,7 @@ export type ScoreAggregate = Record<string, AggregatedScoreData>;
|
||||
export type ScoreSimplified = {
|
||||
id: string;
|
||||
name: string;
|
||||
dataType: ScoreDataType;
|
||||
dataType: AggregatableScoreDataType;
|
||||
source: ScoreSourceType;
|
||||
value?: number | null;
|
||||
comment?: string | null;
|
||||
|
||||
@@ -16,6 +16,8 @@ export * from "./utils/objects";
|
||||
export * from "./utils/typeChecks";
|
||||
export * from "./utils/prompts";
|
||||
export * from "./utils/jsonSchemaValidation";
|
||||
export * from "./utils/chatml";
|
||||
export * from "./utils/math";
|
||||
export * from "./features/entitlements/plans";
|
||||
export * from "./interfaces/rate-limits";
|
||||
export * from "./tableDefinitions/typeHelpers";
|
||||
|
||||
@@ -4,11 +4,11 @@
|
||||
* to avoid coupling between different batch operation types.
|
||||
*/
|
||||
export enum BatchTableNames {
|
||||
Scores = "scores", // eslint-disable-line no-unused-vars
|
||||
Sessions = "sessions", // eslint-disable-line no-unused-vars
|
||||
Traces = "traces", // eslint-disable-line no-unused-vars
|
||||
Observations = "observations", // eslint-disable-line no-unused-vars
|
||||
DatasetRunItems = "dataset_run_items", // eslint-disable-line no-unused-vars
|
||||
DatasetItems = "dataset_items", // eslint-disable-line no-unused-vars
|
||||
AuditLogs = "audit_logs", // eslint-disable-line no-unused-vars
|
||||
Scores = "scores",
|
||||
Sessions = "sessions",
|
||||
Traces = "traces",
|
||||
Observations = "observations",
|
||||
DatasetRunItems = "dataset_run_items",
|
||||
DatasetItems = "dataset_items",
|
||||
AuditLogs = "audit_logs",
|
||||
}
|
||||
|
||||
@@ -232,6 +232,36 @@ export const observationsTableCols: ColumnDefinition[] = [
|
||||
type: "string",
|
||||
internal: "", // handled by comment filter helpers
|
||||
},
|
||||
{
|
||||
name: "Available Tool Names",
|
||||
id: "toolNames",
|
||||
type: "arrayOptions",
|
||||
internal: "", // ClickHouse only - uses mapKeys(tool_definitions)
|
||||
options: [], // to be added at runtime
|
||||
nullable: true,
|
||||
},
|
||||
{
|
||||
name: "Called Tool Names",
|
||||
id: "calledToolNames",
|
||||
type: "arrayOptions",
|
||||
internal: "", // ClickHouse only - uses tool_call_names
|
||||
options: [], // to be added at runtime
|
||||
nullable: true,
|
||||
},
|
||||
{
|
||||
name: "Available Tools",
|
||||
id: "toolDefinitions",
|
||||
type: "number",
|
||||
internal: "", // ClickHouse only
|
||||
nullable: true,
|
||||
},
|
||||
{
|
||||
name: "Tool Calls",
|
||||
id: "toolCalls",
|
||||
type: "number",
|
||||
internal: "", // ClickHouse only
|
||||
nullable: true,
|
||||
},
|
||||
];
|
||||
|
||||
// to be used client side, insert options for use in filter-builder
|
||||
@@ -247,6 +277,8 @@ export type ObservationOptions = {
|
||||
promptName: Array<SingleValueOption>;
|
||||
tags: Array<SingleValueOption>;
|
||||
type: Array<SingleValueOption>;
|
||||
toolNames: Array<SingleValueOption>;
|
||||
calledToolNames: Array<SingleValueOption>;
|
||||
};
|
||||
|
||||
export function observationsTableColsWithOptions(
|
||||
@@ -283,6 +315,12 @@ export function observationsTableColsWithOptions(
|
||||
if (col.id === "type") {
|
||||
return formatColumnOptions(col, options?.type ?? []);
|
||||
}
|
||||
if (col.id === "toolNames") {
|
||||
return formatColumnOptions(col, options?.toolNames ?? []);
|
||||
}
|
||||
if (col.id === "calledToolNames") {
|
||||
return formatColumnOptions(col, options?.calledToolNames ?? []);
|
||||
}
|
||||
return col;
|
||||
});
|
||||
}
|
||||
|
||||
@@ -0,0 +1,66 @@
|
||||
import { ClickHouseLogLevel, Logger } from "@clickhouse/client";
|
||||
import { logger as winstonLogger } from "../logger";
|
||||
|
||||
/**
|
||||
* ClickHouseLogger bridges the ClickHouse client Logger interface to our Winston logger
|
||||
* This ensures ClickHouse client logs are formatted consistently with our application logs
|
||||
* and include OpenTelemetry trace context for correlation with spans in DataDog
|
||||
*/
|
||||
export class ClickHouseLogger implements Logger {
|
||||
trace({ module, message, args }: LogParams): void {
|
||||
winstonLogger.info(`[${module}] ${message}`, args);
|
||||
}
|
||||
|
||||
debug({ module, message, args }: LogParams): void {
|
||||
winstonLogger.info(`[${module}] ${message}`, args);
|
||||
}
|
||||
|
||||
info({ module, message, args }: LogParams): void {
|
||||
winstonLogger.info(`[${module}] ${message}`, args);
|
||||
}
|
||||
|
||||
warn({ module, message, args, err }: WarnLogParams): void {
|
||||
winstonLogger.warn(`[${module}] ${message}`, {
|
||||
...args,
|
||||
...(err ? { error: err.message, stack: err.stack } : {}),
|
||||
});
|
||||
}
|
||||
|
||||
error({ module, message, args, err }: ErrorLogParams): void {
|
||||
winstonLogger.error(`[${module}] ${message}`, {
|
||||
...args,
|
||||
error: err.message,
|
||||
stack: err.stack,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Types from @clickhouse/client documentation
|
||||
interface LogParams {
|
||||
module: string;
|
||||
message: string;
|
||||
args?: Record<string, unknown>;
|
||||
}
|
||||
|
||||
type ErrorLogParams = LogParams & { err: Error };
|
||||
type WarnLogParams = LogParams & { err?: Error };
|
||||
|
||||
/**
|
||||
* Map Winston log level to ClickHouse log level
|
||||
*/
|
||||
export const mapLogLevel = (level: string): ClickHouseLogLevel => {
|
||||
switch (level.toLowerCase()) {
|
||||
case "error":
|
||||
return ClickHouseLogLevel.ERROR;
|
||||
case "warn":
|
||||
return ClickHouseLogLevel.WARN;
|
||||
case "info":
|
||||
return ClickHouseLogLevel.INFO;
|
||||
case "debug":
|
||||
return ClickHouseLogLevel.DEBUG;
|
||||
case "trace":
|
||||
return ClickHouseLogLevel.TRACE;
|
||||
default:
|
||||
return ClickHouseLogLevel.OFF;
|
||||
}
|
||||
};
|
||||
@@ -3,6 +3,7 @@ import { env } from "../../env";
|
||||
import { NodeClickHouseClientConfigOptions } from "@clickhouse/client/dist/config";
|
||||
import { getCurrentSpan } from "../instrumentation";
|
||||
import { propagation, context } from "@opentelemetry/api";
|
||||
import { ClickHouseLogger, mapLogLevel } from "./clickhouse-logger";
|
||||
|
||||
export type ClickhouseClientType = ReturnType<typeof createClient>;
|
||||
|
||||
@@ -107,6 +108,10 @@ export class ClickHouseClientManager {
|
||||
idle_socket_ttl: env.CLICKHOUSE_KEEP_ALIVE_IDLE_SOCKET_TTL,
|
||||
},
|
||||
max_open_connections: env.CLICKHOUSE_MAX_OPEN_CONNECTIONS,
|
||||
log: {
|
||||
LoggerClass: ClickHouseLogger,
|
||||
level: mapLogLevel(env.LANGFUSE_LOG_LEVEL ?? "info"),
|
||||
},
|
||||
clickhouse_settings: {
|
||||
// Overwrite async insert settings to tune throughput
|
||||
...(env.CLICKHOUSE_ASYNC_INSERT_MAX_DATA_SIZE
|
||||
@@ -121,6 +126,12 @@ export class ClickHouseClientManager {
|
||||
env.CLICKHOUSE_ASYNC_INSERT_BUSY_TIMEOUT_MS,
|
||||
}
|
||||
: {}),
|
||||
...(env.CLICKHOUSE_ASYNC_INSERT_BUSY_TIMEOUT_MIN_MS
|
||||
? {
|
||||
async_insert_busy_timeout_min_ms:
|
||||
env.CLICKHOUSE_ASYNC_INSERT_BUSY_TIMEOUT_MIN_MS,
|
||||
}
|
||||
: {}),
|
||||
...(env.CLICKHOUSE_LIGHTWEIGHT_DELETE_MODE !== "alter_update"
|
||||
? {
|
||||
lightweight_delete_mode: env.CLICKHOUSE_LIGHTWEIGHT_DELETE_MODE,
|
||||
@@ -134,7 +145,7 @@ export class ClickHouseClientManager {
|
||||
...(opts.request_timeout && opts.request_timeout > 30000
|
||||
? {
|
||||
send_progress_in_http_headers: 1,
|
||||
http_headers_progress_interval_ms: "25000", // UInt64, should be passed as a string
|
||||
http_headers_progress_interval_ms: "10000", // UInt64, should be passed as a string
|
||||
}
|
||||
: {}),
|
||||
},
|
||||
|
||||
@@ -3,7 +3,7 @@ import { instrumentAsync } from "../instrumentation";
|
||||
|
||||
const executionWrapper = async <T, Y>(
|
||||
input: T,
|
||||
fn: (input: T) => Promise<Y>, // eslint-disable-line no-unused-vars
|
||||
fn: (input: T) => Promise<Y>,
|
||||
span?: opentelemetry.Span,
|
||||
attributePrefix?: string,
|
||||
): Promise<[Y, number]> => {
|
||||
@@ -26,7 +26,7 @@ export const measureAndReturn = async <T, Y>(args: {
|
||||
operationName: string;
|
||||
projectId: string;
|
||||
input: T;
|
||||
fn: (input: T) => Promise<Y>; // eslint-disable-line no-unused-vars
|
||||
fn: (input: T) => Promise<Y>;
|
||||
}): Promise<Y> => {
|
||||
return instrumentAsync(
|
||||
{
|
||||
@@ -39,10 +39,9 @@ export const measureAndReturn = async <T, Y>(args: {
|
||||
// When we want do to multiple executions for A/B testing or canary releases,
|
||||
// or some other form of a more complex wrapper we used to try-catch
|
||||
// using the wrapper function and fallback to a simple f(input) when it failed.
|
||||
const [[existingResult, _existingDuration]] = // eslint-disable-line no-unused-vars
|
||||
await Promise.all([
|
||||
executionWrapper(input, fn, currentSpan, "existing"),
|
||||
]);
|
||||
const [[existingResult, _existingDuration]] = await Promise.all([
|
||||
executionWrapper(input, fn, currentSpan, "existing"),
|
||||
]);
|
||||
|
||||
return existingResult;
|
||||
},
|
||||
|
||||
@@ -0,0 +1,128 @@
|
||||
import { queryClickhouse } from "../repositories";
|
||||
|
||||
// ============================================================================
|
||||
// Types
|
||||
// ============================================================================
|
||||
|
||||
export type QueryStatus = "running" | "completed" | "failed" | "not_found";
|
||||
|
||||
// ============================================================================
|
||||
// Utilities
|
||||
// ============================================================================
|
||||
|
||||
export function sleep(ms: number): Promise<void> {
|
||||
return new Promise((resolve) => setTimeout(resolve, ms));
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Query Status Polling
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* Polls ClickHouse to determine the status of a query by its query_id.
|
||||
* First checks system.processes for running queries, then system.query_log for completed/failed.
|
||||
*/
|
||||
export async function pollQueryStatus(
|
||||
queryId: string,
|
||||
tags?: Record<string, string>,
|
||||
): Promise<QueryStatus> {
|
||||
const tagsWithDefaults = {
|
||||
feature: "query-tracking",
|
||||
operation: "pollQueryStatus",
|
||||
...tags,
|
||||
};
|
||||
// First check if still running in system.processes
|
||||
const running = await queryClickhouse<{ query_id: string }>({
|
||||
query: `
|
||||
SELECT query_id
|
||||
FROM clusterAllReplicas('default', 'system.processes')
|
||||
WHERE query_id = {queryId: String}
|
||||
LIMIT 1
|
||||
`,
|
||||
params: { queryId },
|
||||
clickhouseConfigs: {
|
||||
request_timeout: 60_000,
|
||||
},
|
||||
clickhouseSettings: {
|
||||
skip_unavailable_shards: 1,
|
||||
},
|
||||
tags: tagsWithDefaults,
|
||||
});
|
||||
|
||||
if (running.length > 0) {
|
||||
return "running";
|
||||
}
|
||||
// Check query_log for completion status
|
||||
const result = await queryClickhouse<{
|
||||
type: string;
|
||||
exception_code: string;
|
||||
}>({
|
||||
query: `
|
||||
SELECT type, exception_code
|
||||
FROM clusterAllReplicas('default', 'system.query_log')
|
||||
WHERE query_id = {queryId: String}
|
||||
ORDER BY event_time_microseconds DESC
|
||||
LIMIT 1
|
||||
`,
|
||||
params: { queryId },
|
||||
clickhouseConfigs: {
|
||||
request_timeout: 60_000,
|
||||
},
|
||||
clickhouseSettings: {
|
||||
skip_unavailable_shards: 1,
|
||||
},
|
||||
tags: tagsWithDefaults,
|
||||
});
|
||||
|
||||
if (result.length === 0) {
|
||||
return "not_found";
|
||||
}
|
||||
|
||||
const { type, exception_code } = result[0];
|
||||
if (type === "QueryStart") {
|
||||
return "running";
|
||||
}
|
||||
|
||||
if (
|
||||
type === "ExceptionBeforeStart" ||
|
||||
type === "ExceptionWhileProcessing" ||
|
||||
parseInt(exception_code, 10) !== 0
|
||||
) {
|
||||
return "failed";
|
||||
}
|
||||
|
||||
if (type === "QueryFinish") {
|
||||
return "completed";
|
||||
}
|
||||
|
||||
throw new Error(`Unknown query log type: ${type}`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the error message for a failed query from system.query_log.
|
||||
*/
|
||||
export async function getQueryError(
|
||||
queryId: string,
|
||||
): Promise<string | undefined> {
|
||||
const result = await queryClickhouse<{ exception_message: string }>({
|
||||
query: `
|
||||
SELECT exception as exception_message
|
||||
FROM clusterAllReplicas('default', 'system.query_log')
|
||||
WHERE query_id = {queryId: String}
|
||||
AND type != 'QueryStart'
|
||||
AND exception != ''
|
||||
ORDER BY event_time_microseconds DESC
|
||||
LIMIT 1
|
||||
`,
|
||||
params: { queryId },
|
||||
clickhouseSettings: {
|
||||
skip_unavailable_shards: 1,
|
||||
},
|
||||
tags: {
|
||||
feature: "query-tracking",
|
||||
operation: "getQueryError",
|
||||
},
|
||||
});
|
||||
|
||||
return result[0]?.exception_message;
|
||||
}
|
||||
@@ -1,7 +1,5 @@
|
||||
import { env } from "../../env";
|
||||
|
||||
/* eslint-disable no-unused-vars */
|
||||
|
||||
export enum OperationType {
|
||||
READ = "READ",
|
||||
WRITE = "WRITE",
|
||||
|
||||
@@ -11,6 +11,7 @@ export * from "./services/PromptService/types";
|
||||
export * from "./services/traces-ui-table-service";
|
||||
export * from "./services/InMemoryFilterService";
|
||||
export * from "./services/DatasetService";
|
||||
export * from "./services/commentFilterService";
|
||||
export * from "./datasets/schemaValidation";
|
||||
export * from "./datasets/schemaTypes";
|
||||
export * from "./evalJobConfigCache";
|
||||
@@ -34,11 +35,14 @@ export * from "./otel/utils";
|
||||
export * from "./clickhouse/client";
|
||||
export * from "./clickhouse/schemaUtils";
|
||||
export * from "./clickhouse/schema";
|
||||
export * from "./clickhouse/queryTracking";
|
||||
export * from "./repositories/definitions";
|
||||
export * from "../utils/IORepresentation/chatML/types";
|
||||
export * from "../server/ingestion/types";
|
||||
export * from "../server/ingestion/modelMatch";
|
||||
export * from "./ingestion/processEventBatch";
|
||||
export * from "../server/ingestion/validateAndInflateScore";
|
||||
export * from "./ingestion/extractToolsBackend";
|
||||
export * from "../server/queries/public-api-filter-builder";
|
||||
export * from "../server/pricing-tiers";
|
||||
export * from "./redis/redis";
|
||||
@@ -71,10 +75,14 @@ export * from "./redis/meteringDataPostgresExportQueue";
|
||||
export * from "./redis/notificationQueue";
|
||||
export * from "./webhooks/validation";
|
||||
export * from "./webhooks/ipBlocking";
|
||||
export * from "./webhooks/redirectHandler";
|
||||
export * from "./redis/experimentCreateQueue";
|
||||
export * from "./redis/dlqRetryQueue";
|
||||
export * from "./redis/entityChangeQueue";
|
||||
export * from "./redis/eventPropagationQueue";
|
||||
export * from "./redis/batchProjectCleanerQueue";
|
||||
export * from "./redis/batchDataRetentionCleanerQueue";
|
||||
export * from "./redis/mediaRetentionCleanerQueue";
|
||||
export * from "./redis/otelProjectTracking";
|
||||
export * from "./redis/s3SlowdownTracking";
|
||||
export * from "./auth/types";
|
||||
|
||||
@@ -0,0 +1,387 @@
|
||||
import { z } from "zod/v4";
|
||||
|
||||
/**
|
||||
* ClickHouse storage schema for tool definitions.
|
||||
*
|
||||
* Based on ToolDefinitionSchema from packages/shared/src/utils/IORepresentation/chatML/types.ts
|
||||
* `parameters` stored as JSON string instead of z.record
|
||||
*/
|
||||
export const ClickhouseToolDefinitionSchema = z.object({
|
||||
name: z.string(),
|
||||
description: z.string().optional(),
|
||||
parameters: z.string().optional(), // JSON string of parameters schema
|
||||
});
|
||||
export type ClickhouseToolDefinition = z.infer<
|
||||
typeof ClickhouseToolDefinitionSchema
|
||||
>;
|
||||
|
||||
/**
|
||||
* ClickHouse storage schema for tool calls (invocations).
|
||||
*
|
||||
* Based on ToolCallSchema from packages/shared/src/utils/IORepresentation/chatML/types.ts
|
||||
* Adapted for ClickHouse Array(JSON) storage:
|
||||
* - `arguments` stored as JSON string (base may have parsed object)
|
||||
* - `index` optional field included for parallel tool call ordering
|
||||
*/
|
||||
export const ClickhouseToolArgumentSchema = z.object({
|
||||
id: z.string(),
|
||||
name: z.string(),
|
||||
arguments: z.string(), // JSON string of call arguments
|
||||
type: z.string().optional(),
|
||||
index: z.number().optional(),
|
||||
});
|
||||
export type ClickhouseToolArgument = z.infer<
|
||||
typeof ClickhouseToolArgumentSchema
|
||||
>;
|
||||
|
||||
/**
|
||||
* Flatten tool definition from nested or flat format.
|
||||
* Handles both OpenAI Chat Completions {type, function: {name, ...}} and flat {name, ...}.
|
||||
*/
|
||||
function flattenToolDefinition(tool: unknown): {
|
||||
name?: string;
|
||||
description?: string;
|
||||
parameters?: unknown;
|
||||
} {
|
||||
if (!tool || typeof tool !== "object") return {};
|
||||
const t = tool as Record<string, unknown>;
|
||||
|
||||
// Handle nested {type, function: {name, ...}} format (OpenAI Chat Completions)
|
||||
const toolData = (t.function as Record<string, unknown> | undefined) ?? t;
|
||||
|
||||
return {
|
||||
name: toolData.name as string | undefined,
|
||||
description: (toolData.description ?? toolData.desc) as string | undefined,
|
||||
parameters:
|
||||
toolData.parameters ??
|
||||
toolData.parameters_json_schema ??
|
||||
toolData.inputSchema,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Flatten tool call from nested or flat format.
|
||||
* Handles OpenAI {function: {name, arguments}} and flat {name, arguments}.
|
||||
*/
|
||||
function flattenToolCall(call: unknown): {
|
||||
id?: string;
|
||||
name?: string;
|
||||
arguments?: string;
|
||||
type?: string;
|
||||
index?: number;
|
||||
} {
|
||||
if (!call || typeof call !== "object") return {};
|
||||
const c = call as Record<string, unknown>;
|
||||
|
||||
// Handle nested {function: {name, arguments}} format
|
||||
const func = c.function as Record<string, unknown> | undefined;
|
||||
const name = (func?.name ?? c.name ?? c.toolName) as string | undefined;
|
||||
const rawArgs = func?.arguments ?? c.arguments ?? c.args;
|
||||
const args =
|
||||
typeof rawArgs === "string" ? rawArgs : JSON.stringify(rawArgs ?? {});
|
||||
|
||||
return {
|
||||
id: (c.id ?? c.toolCallId ?? c.call_id) as string | undefined,
|
||||
name,
|
||||
arguments: args,
|
||||
type: (c.type ?? (func ? "function" : undefined)) as string | undefined,
|
||||
index: c.index as number | undefined,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper to add a tool definition, deduplicating by name.
|
||||
*/
|
||||
function addToolDefinition(
|
||||
definitions: ClickhouseToolDefinition[],
|
||||
tool: unknown,
|
||||
): void {
|
||||
const flattened = flattenToolDefinition(tool);
|
||||
if (!flattened.name) return; // Skip invalid tools
|
||||
|
||||
const normalized: ClickhouseToolDefinition = {
|
||||
name: flattened.name,
|
||||
description: flattened.description,
|
||||
parameters: flattened.parameters
|
||||
? JSON.stringify(flattened.parameters)
|
||||
: undefined,
|
||||
};
|
||||
|
||||
// Deduplicate by name
|
||||
if (!definitions.some((t) => t.name === normalized.name)) {
|
||||
definitions.push(normalized);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper to add a tool call/argument.
|
||||
*/
|
||||
function addToolArgument(args: ClickhouseToolArgument[], call: unknown): void {
|
||||
const flattened = flattenToolCall(call);
|
||||
if (!flattened.name) return; // Skip invalid calls
|
||||
|
||||
args.push({
|
||||
id: flattened.id ?? "",
|
||||
name: flattened.name,
|
||||
arguments: flattened.arguments ?? "{}",
|
||||
type: flattened.type,
|
||||
index: flattened.index,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract tool definitions from raw input data (top-level tools array).
|
||||
*/
|
||||
function extractToolsFromRawInput(
|
||||
input: unknown,
|
||||
definitions: ClickhouseToolDefinition[],
|
||||
): void {
|
||||
if (!input || typeof input !== "object") return;
|
||||
|
||||
const obj = Array.isArray(input)
|
||||
? { messages: input }
|
||||
: (input as Record<string, unknown>);
|
||||
|
||||
// Top-level tools array (OpenAI request format)
|
||||
if (Array.isArray(obj.tools)) {
|
||||
for (const tool of obj.tools) {
|
||||
addToolDefinition(definitions, tool);
|
||||
}
|
||||
}
|
||||
|
||||
// Messages array with tools on individual messages
|
||||
if (Array.isArray(obj.messages)) {
|
||||
for (const msg of obj.messages) {
|
||||
if (msg && typeof msg === "object") {
|
||||
// Standard tools array on message
|
||||
if (Array.isArray((msg as any).tools)) {
|
||||
for (const tool of (msg as any).tools) {
|
||||
addToolDefinition(definitions, tool);
|
||||
}
|
||||
}
|
||||
|
||||
// LangGraph format: tool definition in role:"tool" message content
|
||||
// TODO: This should be handled by langgraph adapter preprocessing in the future
|
||||
if (
|
||||
(msg as any).role === "tool" &&
|
||||
(msg as any).content?.type === "function" &&
|
||||
(msg as any).content?.function
|
||||
) {
|
||||
addToolDefinition(definitions, (msg as any).content);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract tool calls from raw output data.
|
||||
*/
|
||||
function extractToolCallsFromRawOutput(
|
||||
output: unknown,
|
||||
args: ClickhouseToolArgument[],
|
||||
): void {
|
||||
if (!output) return;
|
||||
|
||||
// Array of messages
|
||||
if (Array.isArray(output)) {
|
||||
for (const msg of output) {
|
||||
if (msg && typeof msg === "object") {
|
||||
extractToolCallsFromMessage(msg as Record<string, unknown>, args);
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (typeof output !== "object") return;
|
||||
const obj = output as Record<string, unknown>;
|
||||
|
||||
// Direct tool_calls at top level
|
||||
if (Array.isArray(obj.tool_calls)) {
|
||||
for (const call of obj.tool_calls) {
|
||||
addToolArgument(args, call);
|
||||
}
|
||||
}
|
||||
|
||||
// OpenAI choices format: {choices: [{message: {tool_calls: [...]}}]}
|
||||
if (Array.isArray(obj.choices)) {
|
||||
for (const choice of obj.choices) {
|
||||
if (choice && typeof choice === "object") {
|
||||
const c = choice as Record<string, unknown>;
|
||||
const message = c.message as Record<string, unknown> | undefined;
|
||||
if (message && Array.isArray(message.tool_calls)) {
|
||||
for (const call of message.tool_calls) {
|
||||
addToolArgument(args, call);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Anthropic tool_use in content array: {content: [{type: "tool_use", ...}]}
|
||||
if (Array.isArray(obj.content)) {
|
||||
for (const part of obj.content) {
|
||||
if (
|
||||
part &&
|
||||
typeof part === "object" &&
|
||||
(part as Record<string, unknown>).type === "tool_use"
|
||||
) {
|
||||
const p = part as Record<string, unknown>;
|
||||
addToolArgument(args, {
|
||||
id: p.id,
|
||||
name: p.name,
|
||||
arguments: JSON.stringify(p.input ?? {}),
|
||||
type: "tool_use",
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// LangChain additional_kwargs: {additional_kwargs: {tool_calls: [...]}}
|
||||
const additionalKwargs = obj.additional_kwargs as
|
||||
| Record<string, unknown>
|
||||
| undefined;
|
||||
if (additionalKwargs && Array.isArray(additionalKwargs.tool_calls)) {
|
||||
for (const call of additionalKwargs.tool_calls) {
|
||||
addToolArgument(args, call);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract tool calls from a single message object.
|
||||
*/
|
||||
function extractToolCallsFromMessage(
|
||||
msg: Record<string, unknown>,
|
||||
args: ClickhouseToolArgument[],
|
||||
): void {
|
||||
if (Array.isArray(msg.tool_calls)) {
|
||||
for (const call of msg.tool_calls) {
|
||||
addToolArgument(args, call);
|
||||
}
|
||||
}
|
||||
|
||||
// Anthropic content array
|
||||
if (Array.isArray(msg.content)) {
|
||||
for (const part of msg.content) {
|
||||
if (
|
||||
part &&
|
||||
typeof part === "object" &&
|
||||
(part as Record<string, unknown>).type === "tool_use"
|
||||
) {
|
||||
const p = part as Record<string, unknown>;
|
||||
addToolArgument(args, {
|
||||
id: p.id,
|
||||
name: p.name,
|
||||
arguments: JSON.stringify(p.input ?? {}),
|
||||
type: "tool_use",
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse input that might be a JSON string or already an object.
|
||||
*/
|
||||
function parseIfString(data: unknown): unknown {
|
||||
if (typeof data === "string") {
|
||||
try {
|
||||
return JSON.parse(data);
|
||||
} catch {
|
||||
return data; // Return original if not valid JSON
|
||||
}
|
||||
}
|
||||
return data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract tool definitions and arguments from observation input/output.
|
||||
* Extracts directly from raw input/output formats.
|
||||
*
|
||||
* @param input - Raw observation input (object or JSON string)
|
||||
* @param output - Raw observation output (object or JSON string)
|
||||
* @returns Object with toolDefinitions and toolArguments arrays
|
||||
*/
|
||||
export function extractToolsFromObservation(
|
||||
input: unknown,
|
||||
output: unknown,
|
||||
): {
|
||||
toolDefinitions: ClickhouseToolDefinition[];
|
||||
toolArguments: ClickhouseToolArgument[];
|
||||
} {
|
||||
try {
|
||||
const toolDefinitions: ClickhouseToolDefinition[] = [];
|
||||
const toolArguments: ClickhouseToolArgument[] = [];
|
||||
|
||||
const parsedInput = parseIfString(input);
|
||||
const parsedOutput = parseIfString(output);
|
||||
|
||||
extractToolsFromRawInput(parsedInput, toolDefinitions);
|
||||
extractToolCallsFromRawOutput(parsedOutput, toolArguments);
|
||||
|
||||
// Deduplicate tool arguments by id
|
||||
const seenIds = new Set<string>();
|
||||
const uniqueArgs = toolArguments.filter((arg) => {
|
||||
const key = arg.id || `${arg.name}-${arg.arguments}`;
|
||||
if (seenIds.has(key)) return false;
|
||||
seenIds.add(key);
|
||||
return true;
|
||||
});
|
||||
|
||||
return { toolDefinitions, toolArguments: uniqueArgs };
|
||||
} catch (error) {
|
||||
console.error("Tool extraction error:", error);
|
||||
return { toolDefinitions: [], toolArguments: [] };
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert array of tool definitions to Map format for ClickHouse.
|
||||
* Key: tool name, Value: JSON string of {description, parameters}
|
||||
*/
|
||||
export function convertDefinitionsToMap(
|
||||
definitions: ClickhouseToolDefinition[],
|
||||
): Record<string, string> {
|
||||
const map: Record<string, string> = {};
|
||||
for (const def of definitions) {
|
||||
// Last definition wins if duplicate names (shouldn't happen after dedup)
|
||||
map[def.name] = JSON.stringify({
|
||||
description: def.description ?? "",
|
||||
parameters: def.parameters ?? "",
|
||||
});
|
||||
}
|
||||
return map;
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert array of tool calls to parallel arrays for ClickHouse.
|
||||
*
|
||||
* Returns:
|
||||
* - tool_calls: Array of JSON strings containing {id, arguments, type, index} (NO name)
|
||||
* - tool_call_names: Array of names in the same order as tool_calls
|
||||
*
|
||||
* This structure enables efficient filtering by name using has(tool_call_names, 'name')
|
||||
* without needing to parse JSON.
|
||||
*/
|
||||
export function convertCallsToArrays(args: ClickhouseToolArgument[]): {
|
||||
tool_calls: string[];
|
||||
tool_call_names: string[];
|
||||
} {
|
||||
const tool_calls: string[] = [];
|
||||
const tool_call_names: string[] = [];
|
||||
|
||||
for (const arg of args) {
|
||||
tool_call_names.push(arg.name);
|
||||
tool_calls.push(
|
||||
JSON.stringify({
|
||||
id: arg.id,
|
||||
arguments: arg.arguments ?? "{}",
|
||||
type: arg.type ?? "",
|
||||
index: arg.index ?? 0,
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
return { tool_calls, tool_call_names };
|
||||
}
|
||||
@@ -32,7 +32,9 @@ export async function findModel(p: ModelMatchProps): Promise<ModelWithPrices> {
|
||||
traceScope: "model-match",
|
||||
},
|
||||
async (span) => {
|
||||
logger.debug(`Finding model for ${JSON.stringify(p)}`);
|
||||
if (logger.isLevelEnabled("debug")) {
|
||||
logger.debug(`Finding model for ${JSON.stringify(p)}`);
|
||||
}
|
||||
const cachedResult = await getModelWithPricesFromRedis(p);
|
||||
if (cachedResult) {
|
||||
span.setAttribute("model_match_source", "redis");
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import lodash from "lodash";
|
||||
import isEmpty from "lodash/isEmpty";
|
||||
import { z } from "zod/v4";
|
||||
|
||||
import { NonEmptyString, jsonSchema } from "../../utils/zod";
|
||||
@@ -58,7 +58,7 @@ export const usage = MixedUsage.nullish()
|
||||
}
|
||||
|
||||
// if the object is empty, we return undefined
|
||||
if (lodash.isEmpty(v)) {
|
||||
if (isEmpty(v)) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
@@ -93,6 +93,11 @@ const RawUsageDetails = z.record(z.string(), z.unknown()).transform((val) => {
|
||||
for (const [key, value] of Object.entries(val)) {
|
||||
if (typeof value === "number" && Number.isInteger(value) && value >= 0) {
|
||||
result[key] = value;
|
||||
} else if (typeof value === "string") {
|
||||
const parsed = parseInt(value, 10);
|
||||
if (!isNaN(parsed) && parsed >= 0) {
|
||||
result[key] = parsed;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -534,6 +539,13 @@ const createAllIngestionSchemas = ({
|
||||
configId: z.string().nullish(),
|
||||
}),
|
||||
),
|
||||
BaseScoreBody.merge(
|
||||
z.object({
|
||||
value: z.string(),
|
||||
dataType: z.literal("CORRECTION"),
|
||||
configId: z.undefined().nullish(), // Cannot have config
|
||||
}),
|
||||
),
|
||||
BaseScoreBody.merge(
|
||||
z.object({
|
||||
value: z.union([z.string(), z.number()]),
|
||||
|
||||
@@ -1,10 +1,13 @@
|
||||
import {
|
||||
ScoreBodyWithoutConfig,
|
||||
ScoreConfigDomain,
|
||||
ScoreDomain,
|
||||
type ScoreDataTypeType,
|
||||
type ScoreDomain,
|
||||
ScorePropsAgainstConfig,
|
||||
ScoreDataTypeEnum,
|
||||
CORRECTION_NAME,
|
||||
} from "../../../src";
|
||||
import { prisma, ScoreDataType } from "../../db";
|
||||
import { prisma } from "../../db";
|
||||
import { InvalidRequestError, LangfuseNotFoundError } from "../../errors";
|
||||
import { validateDbScoreConfigSafe } from "../../features/scoreConfigs/validation";
|
||||
import { ScoreEventType } from "./types";
|
||||
@@ -68,10 +71,10 @@ export async function validateAndInflateScore(
|
||||
return inflateScoreBody(params);
|
||||
}
|
||||
|
||||
function inferDataType(value: string | number): ScoreDataType {
|
||||
function inferDataType(value: string | number): ScoreDataTypeType {
|
||||
return typeof value === "number"
|
||||
? ScoreDataType.NUMERIC
|
||||
: ScoreDataType.CATEGORICAL;
|
||||
? ScoreDataTypeEnum.NUMERIC
|
||||
: ScoreDataTypeEnum.CATEGORICAL;
|
||||
}
|
||||
|
||||
function mapStringValueToNumericValue(
|
||||
@@ -91,18 +94,19 @@ function inflateScoreBody(
|
||||
const relevantDataType = config?.dataType ?? body.dataType;
|
||||
const scoreProps = {
|
||||
...body,
|
||||
longStringValue: "",
|
||||
source: body.source ?? "API",
|
||||
id: scoreId,
|
||||
projectId,
|
||||
};
|
||||
|
||||
if (typeof body.value === "number") {
|
||||
if (relevantDataType && relevantDataType === ScoreDataType.BOOLEAN) {
|
||||
if (relevantDataType && relevantDataType === ScoreDataTypeEnum.BOOLEAN) {
|
||||
return {
|
||||
...scoreProps,
|
||||
value: body.value,
|
||||
stringValue: body.value === 1 ? "True" : "False",
|
||||
dataType: ScoreDataType.BOOLEAN,
|
||||
dataType: ScoreDataTypeEnum.BOOLEAN,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -110,7 +114,30 @@ function inflateScoreBody(
|
||||
...scoreProps,
|
||||
value: body.value,
|
||||
stringValue: null,
|
||||
dataType: ScoreDataType.NUMERIC,
|
||||
dataType: ScoreDataTypeEnum.NUMERIC,
|
||||
};
|
||||
}
|
||||
|
||||
if (relevantDataType && relevantDataType === ScoreDataTypeEnum.CORRECTION) {
|
||||
// CORRECTION scores can only be associated with traces or observations
|
||||
if (body.sessionId) {
|
||||
throw new InvalidRequestError(
|
||||
"CORRECTION scores cannot be associated with sessions. Please associate with a trace or observation instead.",
|
||||
);
|
||||
}
|
||||
if (body.datasetRunId) {
|
||||
throw new InvalidRequestError(
|
||||
"CORRECTION scores cannot be associated with dataset runs. Please associate with a trace or observation instead.",
|
||||
);
|
||||
}
|
||||
|
||||
return {
|
||||
...scoreProps,
|
||||
value: 0,
|
||||
name: CORRECTION_NAME,
|
||||
longStringValue: body.value,
|
||||
stringValue: null,
|
||||
dataType: ScoreDataTypeEnum.CORRECTION,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -118,7 +145,7 @@ function inflateScoreBody(
|
||||
...scoreProps,
|
||||
value: config ? mapStringValueToNumericValue(config, body.value) : 0,
|
||||
stringValue: body.value,
|
||||
dataType: ScoreDataType.CATEGORICAL,
|
||||
dataType: ScoreDataTypeEnum.CATEGORICAL,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -142,11 +169,13 @@ function resolveScoreValueAnnotation(
|
||||
body: ScoreDomain,
|
||||
): string | number | null {
|
||||
switch (body.dataType) {
|
||||
case ScoreDataType.NUMERIC:
|
||||
case ScoreDataType.BOOLEAN:
|
||||
case ScoreDataTypeEnum.NUMERIC:
|
||||
case ScoreDataTypeEnum.BOOLEAN:
|
||||
return body.value;
|
||||
case ScoreDataType.CATEGORICAL:
|
||||
case ScoreDataTypeEnum.CATEGORICAL:
|
||||
return body.stringValue;
|
||||
case ScoreDataTypeEnum.CORRECTION:
|
||||
throw new Error("CORRECTION type not supported in annotation drawer");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -23,7 +23,7 @@ export type SpanCtx = {
|
||||
startNewTrace?: boolean; // Start a new trace, severing any parent trace relationships
|
||||
};
|
||||
|
||||
type AsyncCallbackFn<T> = (span: opentelemetry.Span) => Promise<T>; // eslint-disable-line no-unused-vars
|
||||
type AsyncCallbackFn<T> = (span: opentelemetry.Span) => Promise<T>;
|
||||
|
||||
export async function instrumentAsync<T>(
|
||||
ctx: SpanCtx,
|
||||
@@ -67,7 +67,7 @@ export async function instrumentAsync<T>(
|
||||
);
|
||||
}
|
||||
|
||||
type SyncCallbackFn<T> = (span: opentelemetry.Span) => T; // eslint-disable-line no-unused-vars
|
||||
type SyncCallbackFn<T> = (span: opentelemetry.Span) => T;
|
||||
|
||||
export function instrumentSync<T>(
|
||||
ctx: SpanCtx,
|
||||
|
||||
@@ -92,21 +92,18 @@ type FetchLLMCompletionParams = LLMCompletionParams & {
|
||||
};
|
||||
|
||||
export async function fetchLLMCompletion(
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
params: LLMCompletionParams & {
|
||||
streaming: true;
|
||||
},
|
||||
): Promise<IterableReadableStream<Uint8Array>>;
|
||||
|
||||
export async function fetchLLMCompletion(
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
params: LLMCompletionParams & {
|
||||
streaming: false;
|
||||
},
|
||||
): Promise<string>;
|
||||
|
||||
export async function fetchLLMCompletion(
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
params: LLMCompletionParams & {
|
||||
streaming: false;
|
||||
structuredOutputSchema: ZodSchema;
|
||||
@@ -114,7 +111,6 @@ export async function fetchLLMCompletion(
|
||||
): Promise<Record<string, unknown>>;
|
||||
|
||||
export async function fetchLLMCompletion(
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
params: LLMCompletionParams & {
|
||||
streaming: false;
|
||||
tools: LLMToolDefinition[];
|
||||
@@ -385,6 +381,9 @@ export async function fetchLLMCompletion(
|
||||
maxRetries,
|
||||
location,
|
||||
authOptions,
|
||||
...(modelParams.maxReasoningTokens !== undefined && {
|
||||
maxReasoningTokens: modelParams.maxReasoningTokens,
|
||||
}),
|
||||
...(modelParams.providerOptions && {
|
||||
additionalModelRequestFields: modelParams.providerOptions,
|
||||
}),
|
||||
@@ -404,7 +403,6 @@ export async function fetchLLMCompletion(
|
||||
}),
|
||||
});
|
||||
} else {
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
const _exhaustiveCheck: never = modelParams.adapter;
|
||||
throw new Error(
|
||||
`This model provider is not supported: ${_exhaustiveCheck}`,
|
||||
@@ -457,7 +455,8 @@ export async function fetchLLMCompletion(
|
||||
} catch (e) {
|
||||
const responseStatusCode =
|
||||
(e as any)?.response?.status ?? (e as any)?.status ?? 500;
|
||||
const message = e instanceof Error ? e.message : String(e);
|
||||
const rawMessage = e instanceof Error ? e.message : String(e);
|
||||
const message = extractCleanErrorMessage(rawMessage);
|
||||
|
||||
// Check for non-retryable error patterns in message
|
||||
const nonRetryablePatterns = [
|
||||
@@ -525,3 +524,34 @@ function processOpenAIBaseURL(params: {
|
||||
|
||||
return url.replace("{model}", modelName);
|
||||
}
|
||||
|
||||
function extractCleanErrorMessage(rawMessage: string): string {
|
||||
// Try to parse JSON error format (common in Google/Vertex AI errors)
|
||||
// Example: '[{"error":{"code":404,"message":"Model not found..."}}]'
|
||||
try {
|
||||
// Check if the message starts with [ or { indicating JSON
|
||||
const trimmed = rawMessage.trim();
|
||||
if (trimmed.startsWith("[") || trimmed.startsWith("{")) {
|
||||
const parsed = JSON.parse(trimmed);
|
||||
|
||||
// Handle array format: [{"error": {"message": "..."}}]
|
||||
if (Array.isArray(parsed) && parsed[0]?.error?.message) {
|
||||
return parsed[0].error.message;
|
||||
}
|
||||
|
||||
// Handle object format: {"error": {"message": "..."}}
|
||||
if (parsed?.error?.message) {
|
||||
return parsed.error.message;
|
||||
}
|
||||
|
||||
// Handle direct message format: {"message": "..."}
|
||||
if (parsed?.message) {
|
||||
return parsed.message;
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
// Not valid JSON, return as-is
|
||||
}
|
||||
|
||||
return rawMessage;
|
||||
}
|
||||
|
||||
@@ -6,7 +6,6 @@ import {
|
||||
} from "../../interfaces/customLLMProviderConfigSchemas";
|
||||
import { JSONObjectSchema } from "../../utils/zod";
|
||||
|
||||
/* eslint-disable no-unused-vars */
|
||||
// disable lint as this is exported and used in web/worker
|
||||
|
||||
export const LLMJSONSchema = z.record(z.string(), z.any());
|
||||
@@ -284,6 +283,7 @@ export const ZodModelConfig = z.object({
|
||||
max_tokens: z.coerce.number().optional(),
|
||||
temperature: z.coerce.number().optional(),
|
||||
top_p: z.coerce.number().optional(),
|
||||
maxReasoningTokens: z.coerce.number().optional(),
|
||||
providerOptions: JSONObjectSchema.optional(),
|
||||
});
|
||||
|
||||
@@ -433,6 +433,7 @@ export const vertexAIModels = [
|
||||
"gemini-2.5-flash",
|
||||
"gemini-2.5-pro",
|
||||
"gemini-3-pro-preview",
|
||||
"gemini-3-flash-preview",
|
||||
"gemini-2.5-flash-preview-09-2025",
|
||||
"gemini-2.5-flash-lite",
|
||||
"gemini-2.5-flash-lite-preview-09-2025",
|
||||
@@ -450,6 +451,7 @@ export const googleAIStudioModels = [
|
||||
"gemini-2.5-flash",
|
||||
"gemini-2.5-pro",
|
||||
"gemini-3-pro-preview",
|
||||
"gemini-3-flash-preview",
|
||||
"gemini-2.5-flash-lite",
|
||||
"gemini-2.5-flash-lite-preview-09-2025",
|
||||
"gemini-2.0-flash",
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
/* eslint-disable no-unused-vars */
|
||||
import { LangfuseOtelSpanAttributes } from "./attributes";
|
||||
import { type ObservationType, ObservationTypeDomain } from "../../";
|
||||
|
||||
|
||||
@@ -194,7 +194,12 @@ export class OtelIngestionProcessor {
|
||||
const scopeAttributes = this.extractScopeAttributes(scopeSpan);
|
||||
for (const span of scopeSpan?.spans ?? []) {
|
||||
const spanAttributes = this.extractSpanAttributes(span);
|
||||
const traceId = this.parseId(span.traceId);
|
||||
// For LiteLLM spans, use langfuse.trace.id from attributes if provided
|
||||
const isLiteLLMSpan = scopeSpan?.scope?.name === "litellm";
|
||||
const traceId =
|
||||
isLiteLLMSpan && spanAttributes["langfuse.trace.id"]
|
||||
? (spanAttributes["langfuse.trace.id"] as string)
|
||||
: this.parseId(span.traceId);
|
||||
const spanId = this.parseId(span.spanId);
|
||||
const parentSpanId = span?.parentSpanId
|
||||
? this.parseId(span.parentSpanId)
|
||||
@@ -355,6 +360,9 @@ export class OtelIngestionProcessor {
|
||||
// Properties
|
||||
tags: this.extractTags(spanAttributes),
|
||||
public: this.extractPublic(spanAttributes),
|
||||
traceName:
|
||||
spanAttributes?.[LangfuseOtelSpanAttributes.TRACE_NAME] ??
|
||||
null,
|
||||
userId: this.extractUserId(spanAttributes),
|
||||
sessionId: this.extractSessionId(spanAttributes),
|
||||
|
||||
@@ -629,7 +637,12 @@ export class OtelIngestionProcessor {
|
||||
const events: IngestionEventType[] = [];
|
||||
const attributes = this.extractSpanAttributes(span);
|
||||
|
||||
const traceId = this.parseId(span.traceId?.data ?? span.traceId);
|
||||
// For LiteLLM spans, use langfuse.trace.id from attributes if provided
|
||||
const isLiteLLMSpan = scopeSpan?.scope?.name === "litellm";
|
||||
const traceId =
|
||||
isLiteLLMSpan && attributes["langfuse.trace.id"]
|
||||
? (attributes["langfuse.trace.id"] as string)
|
||||
: this.parseId(span.traceId?.data ?? span.traceId);
|
||||
const parentObservationId = span?.parentSpanId
|
||||
? this.parseId(span.parentSpanId?.data ?? span.parentSpanId)
|
||||
: null;
|
||||
@@ -1187,6 +1200,10 @@ export class OtelIngestionProcessor {
|
||||
]),
|
||||
);
|
||||
|
||||
// TODO: Map gen_ai.tool.definitions to input.tools for backend extraction
|
||||
// const toolDefs = attributes["gen_ai.tool.definitions"] || attributes["model_request_parameters"]?.function_tools;
|
||||
// if (toolDefs && input && typeof input === "object") { input = { ...input, tools: toolDefs }; }
|
||||
|
||||
// Langfuse
|
||||
input =
|
||||
domain === "trace" && attributes[LangfuseOtelSpanAttributes.TRACE_INPUT]
|
||||
@@ -1378,7 +1395,7 @@ export class OtelIngestionProcessor {
|
||||
if (typeof eventsArray === "string") {
|
||||
try {
|
||||
events = JSON.parse(eventsArray);
|
||||
} catch (e) {
|
||||
} catch {
|
||||
events = [];
|
||||
}
|
||||
}
|
||||
@@ -1589,7 +1606,7 @@ export class OtelIngestionProcessor {
|
||||
unknown
|
||||
>;
|
||||
}
|
||||
} catch (e) {
|
||||
} catch {
|
||||
// Continue with nested metadata extraction
|
||||
}
|
||||
}
|
||||
@@ -1740,7 +1757,7 @@ export class OtelIngestionProcessor {
|
||||
return this.sanitizeModelParams(
|
||||
JSON.parse(attributes["llm.invocation_parameters"] as string),
|
||||
);
|
||||
} catch (e) {
|
||||
} catch {
|
||||
// fallthrough
|
||||
}
|
||||
}
|
||||
@@ -1750,7 +1767,7 @@ export class OtelIngestionProcessor {
|
||||
return this.sanitizeModelParams(
|
||||
JSON.parse(attributes["model_config"] as string),
|
||||
);
|
||||
} catch (e) {
|
||||
} catch {
|
||||
// fallthrough
|
||||
}
|
||||
}
|
||||
@@ -1875,34 +1892,38 @@ export class OtelIngestionProcessor {
|
||||
usageDetails["output_reasoning_tokens"] =
|
||||
typeof parsed === "number" ? parsed : JSON.parse(value).intValue;
|
||||
}
|
||||
} else if (providerMetadata) {
|
||||
// Fall back to providerMetadata
|
||||
}
|
||||
|
||||
// Add additional usage details from provider metadata
|
||||
if (providerMetadata) {
|
||||
const parsed = JSON.parse(providerMetadata as string);
|
||||
|
||||
if ("openai" in parsed) {
|
||||
const openaiMetadata = parsed["openai"] as Record<string, number>;
|
||||
|
||||
usageDetails["input_cached_tokens"] =
|
||||
usageDetails["input_cached_tokens"] ??=
|
||||
openaiMetadata["cachedPromptTokens"];
|
||||
usageDetails["accepted_prediction_tokens"] =
|
||||
usageDetails["accepted_prediction_tokens"] ??=
|
||||
openaiMetadata["acceptedPredictionTokens"];
|
||||
usageDetails["rejected_prediction_tokens"] =
|
||||
usageDetails["rejected_prediction_tokens"] ??=
|
||||
openaiMetadata["rejectedPredictionTokens"];
|
||||
usageDetails["output_reasoning_tokens"] =
|
||||
usageDetails["output_reasoning_tokens"] ??=
|
||||
openaiMetadata["reasoningTokens"];
|
||||
}
|
||||
// "ai.response.providerMetadata": "{\"anthropic\":{\"cacheCreationInputTokens\":0,\"cacheReadInputTokens\":0}}"
|
||||
if ("anthropic" in parsed) {
|
||||
const openaiMetadata = parsed["anthropic"] as Record<
|
||||
|
||||
// "ai.response.providerMetadata": {"anthropic":{"usage":{"input_tokens":7,"cache_creation_input_tokens":2089,"cache_read_input_tokens":16399,"cache_creation":{"ephemeral_5m_input_tokens":2089,"ephemeral_1h_input_tokens":0},"output_tokens":445,"service_tier":"standard"},"cacheCreationInputTokens":2089,"stopSequence":null,"container":null,"contextManagement":null}}
|
||||
if ("anthropic" in parsed && "usage" in parsed["anthropic"]) {
|
||||
const anthropicMetadata = parsed["anthropic"]["usage"] as Record<
|
||||
string,
|
||||
number
|
||||
>;
|
||||
|
||||
usageDetails["input_cache_creation"] =
|
||||
openaiMetadata["cacheCreationInputTokens"];
|
||||
usageDetails["input_cache_read"] =
|
||||
openaiMetadata["cacheReadInputTokens"];
|
||||
usageDetails["input_cache_creation"] ??=
|
||||
anthropicMetadata["cache_creation_input_tokens"];
|
||||
usageDetails["input_cached_tokens"] ??=
|
||||
anthropicMetadata["cache_read_input_tokens"];
|
||||
}
|
||||
|
||||
// Bedrock provider metadata extraction
|
||||
// "ai.response.providerMetadata": "{\"bedrock\":{\"usage\":{\"cacheReadInputTokens\":4482,\"cacheWriteInputTokens\":0,\"cacheCreationInputTokens\":0}}}"
|
||||
if ("bedrock" in parsed) {
|
||||
@@ -1912,22 +1933,22 @@ export class OtelIngestionProcessor {
|
||||
const usage = bedrockMetadata["usage"] as Record<string, number>;
|
||||
|
||||
if (usage["cacheReadInputTokens"] !== undefined) {
|
||||
usageDetails["input_cache_read"] =
|
||||
usageDetails["input_cache_read"] ??=
|
||||
usage["cacheReadInputTokens"];
|
||||
}
|
||||
if (usage["cacheWriteInputTokens"] !== undefined) {
|
||||
usageDetails["input_cache_write"] =
|
||||
usageDetails["input_cache_write"] ??=
|
||||
usage["cacheWriteInputTokens"];
|
||||
}
|
||||
if (usage["cacheCreationInputTokens"] !== undefined) {
|
||||
usageDetails["input_cache_creation"] =
|
||||
usageDetails["input_cache_creation"] ??=
|
||||
usage["cacheCreationInputTokens"];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Subtract cached token count from total input
|
||||
// Subtract cached token count from total input and output
|
||||
usageDetails["input"] = Math.max(
|
||||
(usageDetails["input"] ?? 0) -
|
||||
(usageDetails["input_cached_tokens"] ?? 0) -
|
||||
@@ -1936,6 +1957,12 @@ export class OtelIngestionProcessor {
|
||||
0,
|
||||
);
|
||||
|
||||
usageDetails["output"] = Math.max(
|
||||
(usageDetails["output"] ?? 0) -
|
||||
(usageDetails["output_reasoning_tokens"] ?? 0),
|
||||
0,
|
||||
);
|
||||
|
||||
return usageDetails;
|
||||
} catch {
|
||||
// Fallthrough
|
||||
@@ -2076,7 +2103,7 @@ export class OtelIngestionProcessor {
|
||||
if (Array.isArray(parsedTags)) {
|
||||
return parsedTags.map((tag) => String(tag));
|
||||
}
|
||||
} catch (e) {
|
||||
} catch {
|
||||
// Continue with other methods
|
||||
}
|
||||
}
|
||||
@@ -2135,7 +2162,7 @@ export class OtelIngestionProcessor {
|
||||
if (experimentMetadataStr && typeof experimentMetadataStr === "string") {
|
||||
try {
|
||||
experimentMetadata = JSON.parse(experimentMetadataStr);
|
||||
} catch (e) {
|
||||
} catch {
|
||||
// If parsing fails, treat as empty
|
||||
}
|
||||
}
|
||||
@@ -2152,7 +2179,7 @@ export class OtelIngestionProcessor {
|
||||
) {
|
||||
try {
|
||||
experimentItemMetadata = JSON.parse(experimentItemMetadataStr);
|
||||
} catch (e) {
|
||||
} catch {
|
||||
// If parsing fails, treat as empty
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
/* eslint-disable no-unused-vars */
|
||||
export enum LangfuseOtelSpanAttributes {
|
||||
// Langfuse-Trace attributes
|
||||
TRACE_NAME = "langfuse.trace.name",
|
||||
|
||||
@@ -47,7 +47,7 @@ function evaluateCondition(
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
} catch (error) {
|
||||
} catch {
|
||||
return false; // Fail-safe: condition fails on error
|
||||
}
|
||||
}
|
||||
|
||||
@@ -468,27 +468,22 @@ export class FilterList {
|
||||
this.filters.push(...filter);
|
||||
}
|
||||
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
find(predicate: (filter: Filter) => boolean) {
|
||||
return this.filters.find(predicate);
|
||||
}
|
||||
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
filter(predicate: (filter: Filter) => boolean) {
|
||||
return new FilterList(this.filters.filter(predicate));
|
||||
}
|
||||
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
map(predicate: (filter: Filter) => Filter) {
|
||||
return new FilterList(this.filters.map(predicate));
|
||||
}
|
||||
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
some(predicate: (filter: Filter) => boolean) {
|
||||
return this.filters.some(predicate);
|
||||
}
|
||||
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
forEach(callback: (filter: Filter) => void) {
|
||||
this.filters.forEach(callback);
|
||||
}
|
||||
|
||||
@@ -49,6 +49,11 @@ const EVENTS_FIELDS = {
|
||||
promptName: 'e.prompt_name as "prompt_name"',
|
||||
promptVersion: 'e.prompt_version as "prompt_version"',
|
||||
|
||||
// Tool fields
|
||||
toolDefinitions: 'e.tool_definitions as "tool_definitions"',
|
||||
toolCalls: 'e.tool_calls as "tool_calls"',
|
||||
toolCallNames: 'e.tool_call_names as "tool_call_names"',
|
||||
|
||||
// I/O & metadata fields
|
||||
input: "e.input",
|
||||
output: "e.output",
|
||||
@@ -99,10 +104,14 @@ const FIELD_SETS = {
|
||||
"internalModelId",
|
||||
"userId",
|
||||
"sessionId",
|
||||
"toolDefinitions",
|
||||
"toolCalls",
|
||||
"toolCallNames",
|
||||
],
|
||||
calculated: ["latency", "timeToFirstToken"],
|
||||
io: ["input", "output"],
|
||||
metadata: ["metadata"],
|
||||
tools: ["toolDefinitions", "toolCalls", "toolCallNames"],
|
||||
eventTs: ["eventTs"],
|
||||
|
||||
// getById field sets (reuse the same fields - all queries use `FROM events e`)
|
||||
@@ -120,6 +129,9 @@ const FIELD_SETS = {
|
||||
"level",
|
||||
"statusMessage",
|
||||
"version",
|
||||
"toolDefinitions",
|
||||
"toolCalls",
|
||||
"toolCallNames",
|
||||
],
|
||||
byIdModel: [
|
||||
"providedModelName",
|
||||
@@ -307,7 +319,7 @@ abstract class AbstractQueryBuilder {
|
||||
when<T extends AbstractQueryBuilder>(
|
||||
this: T,
|
||||
condition: boolean,
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
|
||||
fn: (builder: T) => T,
|
||||
): T {
|
||||
return condition ? fn(this) : this;
|
||||
@@ -397,7 +409,7 @@ abstract class BaseEventsQueryBuilder<
|
||||
protected projectId: string | NoProjectIdType;
|
||||
|
||||
constructor(
|
||||
protected fields: TFields, // eslint-disable-line no-unused-vars
|
||||
protected fields: TFields,
|
||||
options: { projectId: string | NoProjectIdType },
|
||||
) {
|
||||
super();
|
||||
@@ -492,6 +504,8 @@ export class EventsQueryBuilder extends BaseEventsQueryBuilder<
|
||||
typeof EVENTS_FIELDS
|
||||
> {
|
||||
private ioFields: { truncated: boolean; charLimit?: number } | null = null;
|
||||
// Metadata expansion config: null = use truncated (default), string[] = expand specific keys, empty array = expand all
|
||||
private metadataExpansionKeys: string[] | null = null;
|
||||
|
||||
/**
|
||||
* Constructor
|
||||
@@ -508,6 +522,23 @@ export class EventsQueryBuilder extends BaseEventsQueryBuilder<
|
||||
super(EVENTS_FIELDS, options);
|
||||
}
|
||||
|
||||
/**
|
||||
* Select metadata with expanded values for specified keys.
|
||||
* Expands truncated metadata values by coalescing with metadata_long_values.
|
||||
*
|
||||
* @param keys - Keys to expand. Only the specified keys will have their full values returned.
|
||||
*
|
||||
* @example
|
||||
* // Expand specific keys only
|
||||
* builder.selectMetadataExpanded(['transcript', 'transitions'])
|
||||
*/
|
||||
selectMetadataExpanded(keys: string[]): this {
|
||||
this.metadataExpansionKeys = keys;
|
||||
// Also ensure metadata is in the select fields (will be replaced in buildSelectClause)
|
||||
this.selectFields.add("metadata");
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add SELECT fields from predefined field sets
|
||||
*/
|
||||
@@ -535,10 +566,22 @@ export class EventsQueryBuilder extends BaseEventsQueryBuilder<
|
||||
*/
|
||||
protected buildSelectClause(): string {
|
||||
// Build field expressions from field keys
|
||||
// If ioFields are configured, exclude regular input/output from selectFields
|
||||
const fieldsToProcess = this.ioFields
|
||||
? [...this.selectFields].filter((f) => f !== "input" && f !== "output")
|
||||
: [...this.selectFields];
|
||||
// Exclude fields that have custom handling (IO, metadata)
|
||||
let fieldsToExclude: string[] = [];
|
||||
if (this.ioFields) {
|
||||
fieldsToExclude.push("input", "output");
|
||||
}
|
||||
// Only exclude metadata if we have actual keys to expand
|
||||
if (
|
||||
this.metadataExpansionKeys !== null &&
|
||||
this.metadataExpansionKeys.length > 0
|
||||
) {
|
||||
fieldsToExclude.push("metadata");
|
||||
}
|
||||
|
||||
const fieldsToProcess = [...this.selectFields].filter(
|
||||
(f) => !fieldsToExclude.includes(f),
|
||||
);
|
||||
|
||||
const fieldExpressions: string[] = fieldsToProcess.flatMap((fieldKey) => {
|
||||
const fieldExpr = EVENTS_FIELDS[fieldKey as keyof typeof EVENTS_FIELDS];
|
||||
@@ -556,6 +599,30 @@ export class EventsQueryBuilder extends BaseEventsQueryBuilder<
|
||||
}
|
||||
}
|
||||
|
||||
// Add metadata field with expansion if configured
|
||||
if (
|
||||
this.metadataExpansionKeys !== null &&
|
||||
this.metadataExpansionKeys.length > 0 &&
|
||||
this.selectFields.has("metadata")
|
||||
) {
|
||||
// Add keys to params for safe parameterized query
|
||||
this.params.expandMetadataKeys = this.metadataExpansionKeys;
|
||||
|
||||
fieldExpressions.push(`mapFromArrays(
|
||||
e.metadata_names,
|
||||
arrayMap(
|
||||
(name, prefix, hash) -> if(
|
||||
has({expandMetadataKeys: Array(String)}, name) AND isNotNull(hash) AND mapContains(e.metadata_long_values, hash),
|
||||
e.metadata_long_values[hash],
|
||||
prefix
|
||||
),
|
||||
e.metadata_names,
|
||||
e.metadata_prefixes,
|
||||
e.metadata_hashes
|
||||
)
|
||||
) as metadata`);
|
||||
}
|
||||
|
||||
return `SELECT\n ${fieldExpressions.join(",\n ")}`;
|
||||
}
|
||||
|
||||
|
||||
@@ -134,16 +134,21 @@ const matchAndVerifyTracesUiColumn = (
|
||||
uiTableDefinitions: UiColumnMappings,
|
||||
) => {
|
||||
// tries to match the column name to the clickhouse table name
|
||||
logger.debug(`Filter to match: ${JSON.stringify(filter)}`);
|
||||
const uiTable = uiTableDefinitions.find(
|
||||
(col) =>
|
||||
col.uiTableName === filter.column || col.uiTableId === filter.column, // matches on the NAME of the column in the UI.
|
||||
);
|
||||
|
||||
if (!uiTable) {
|
||||
throw new QueryBuilderError(
|
||||
`Column ${filter.column} does not match a UI / CH table mapping.`,
|
||||
);
|
||||
const errorMessage = `Column ${filter.column} does not match a UI / CH table mapping.`;
|
||||
logger.error(errorMessage, {
|
||||
filterColumn: filter.column,
|
||||
filterType: filter.type,
|
||||
availableColumns: uiTableDefinitions.map(
|
||||
(col) => col.uiTableId ?? col.uiTableName,
|
||||
),
|
||||
});
|
||||
throw new QueryBuilderError(errorMessage);
|
||||
}
|
||||
|
||||
if (!isValidTableName(uiTable.clickhouseTableName)) {
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user