Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3e61592cf7 | ||
|
|
83952769b7 | ||
|
|
2291ac6625 | ||
|
|
273e206e2e | ||
|
|
a24c0d776d | ||
|
|
865ef2fbef | ||
|
|
6f62cf5f47 | ||
|
|
31a496880d | ||
|
|
f32255514d | ||
|
|
585ee0ae37 | ||
|
|
6f0c661114 | ||
|
|
febeec4534 | ||
|
|
40a46f1f84 | ||
|
|
a24d6ec742 | ||
|
|
4a55e709ae | ||
|
|
e591f1212d | ||
|
|
78f86264c2 | ||
|
|
4f64c37dad | ||
|
|
8746bcf54f | ||
|
|
8c8e8c11e1 | ||
|
|
2a68c0610b | ||
|
|
5c57b2b8e1 | ||
|
|
3a1f34a54c | ||
|
|
53a6769afb | ||
|
|
0df3b996cc | ||
|
|
04b8137299 | ||
|
|
52b2daf40e | ||
|
|
69442a5e1f | ||
|
|
8a9214c61b | ||
|
|
7b7c428fc3 | ||
|
|
4c6817731f | ||
|
|
1ed426a5e7 | ||
|
|
0106bf6e98 | ||
|
|
c2a68ab339 | ||
|
|
2b27698b8d | ||
|
|
796462c20c | ||
|
|
405b017814 | ||
|
|
b0e056a5b9 | ||
|
|
61641aca3b | ||
|
|
d045869dc4 | ||
|
|
6b9291fef4 | ||
|
|
87f16f58c5 | ||
|
|
700a3cd445 | ||
|
|
0041078830 | ||
|
|
33e4f8743c | ||
|
|
6228956384 | ||
|
|
454773f1c9 | ||
|
|
682983d252 | ||
|
|
6462d45790 | ||
|
|
7413b329d1 | ||
|
|
5532de5871 | ||
|
|
c32e0dbeca | ||
|
|
ab5a281e57 | ||
|
|
dc19bee8e5 | ||
|
|
dd9bf14204 | ||
|
|
82b77bce04 | ||
|
|
d9659ee339 | ||
|
|
324d96fc2e | ||
|
|
ccbe233ff6 | ||
|
|
0223c7eff1 | ||
|
|
4e44987e12 | ||
|
|
e0f605a0f2 | ||
|
|
05a4935a91 | ||
|
|
27e1dca4df | ||
|
|
53e4433032 | ||
|
|
c777d91f13 | ||
|
|
ce1e4318e4 |
+1
-1
@@ -3,6 +3,6 @@ Dockerfile
|
||||
node_modules
|
||||
npm-debug.log
|
||||
README.md
|
||||
.next
|
||||
**/.next
|
||||
.git
|
||||
**/node_modules
|
||||
@@ -134,17 +134,11 @@ LANGFUSE_CSP_ENFORCE_HTTPS="true"
|
||||
# Used to determine the Sentry sample rate
|
||||
# LANGFUSE_TRACING_SAMPLE_RATE=
|
||||
|
||||
# NewRelic
|
||||
# NEW_RELIC_API_KEY=
|
||||
# OTLP_ENDPOINT=
|
||||
|
||||
# Cloudflare Turnstile
|
||||
# NEXT_PUBLIC_TURNSTILE_SITE_KEY=
|
||||
# TURNSTILE_SECRET_KEY=
|
||||
|
||||
# Betterstack
|
||||
# LANGFUSE_TEAM_BETTERSTACK_TOKEN=
|
||||
|
||||
# Demo project that users can use to try the platform
|
||||
# NEXT_PUBLIC_DEMO_PROJECT_ID=
|
||||
|
||||
|
||||
@@ -292,7 +292,7 @@ jobs:
|
||||
labels: ${{ steps.meta-web.outputs.labels }}
|
||||
platforms: |
|
||||
linux/amd64
|
||||
linux/arm64
|
||||
${{ startsWith(github.ref, 'refs/tags/') && 'linux/arm64' || '' }}
|
||||
|
||||
- name: Extract metadata (tags, labels) for Docker
|
||||
id: meta-worker
|
||||
@@ -319,4 +319,4 @@ jobs:
|
||||
labels: ${{ steps.meta-worker.outputs.labels }}
|
||||
platforms: |
|
||||
linux/amd64
|
||||
linux/arm64
|
||||
${{ startsWith(github.ref, 'refs/tags/') && 'linux/arm64' || '' }}
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
|
||||
**/newrelic_agent.log
|
||||
|
||||
# dependencies
|
||||
/node_modules
|
||||
|
||||
+59
-10
@@ -53,9 +53,7 @@ A good first step is to search for open [issues](https://github.com/langfuse/lan
|
||||
|
||||
### Architecture Overview
|
||||
|
||||
> [!NOTE]
|
||||
> Infrastructure will change in Langfuse version 3.0. More in the [GitHub Discussions](https://github.com/orgs/langfuse/discussions/1902).
|
||||
> `langfuse/langfuse/worker` is under active development and not recommended for production use in Langfuse 2.x.
|
||||
**Langfuse v2**
|
||||
|
||||
```mermaid
|
||||
flowchart TB
|
||||
@@ -93,15 +91,66 @@ flowchart TB
|
||||
I --- Prisma
|
||||
end
|
||||
|
||||
subgraph s5["Application (langfuse/langfuse/worker)"]
|
||||
Worker_API[Public HTTP API]
|
||||
Prisma --- DB
|
||||
JS --- API
|
||||
Python --- API
|
||||
```
|
||||
|
||||
**Langfuse v3 (work in progress, not released yet)**
|
||||
|
||||
> [!NOTE]
|
||||
> Infrastructure will change in Langfuse version 3.0. More in the [GitHub Discussions](https://github.com/orgs/langfuse/discussions/1902).
|
||||
> `langfuse/langfuse/worker` is under active development and not recommended for production use in Langfuse 2.x.
|
||||
|
||||
```mermaid
|
||||
flowchart TB
|
||||
subgraph s4["Clients"]
|
||||
subgraph s2["langfuse/langfuse-python"]
|
||||
Python["Python low-level SDK"]
|
||||
Decorator["observe() decorator"] -->|extends| Python
|
||||
OAI["OpenAI drop-in replacement"] -->|extends| Python
|
||||
Llamaindex["LlamaIndex Integration"] -->|extends| Python
|
||||
LCPYTHON["Langchain Python Integration"] -->|extends| Python
|
||||
Langflow -->|uses| LCPYTHON
|
||||
LiteLLM -->|uses| Python
|
||||
end
|
||||
subgraph s3["langfuse/langfuse-js"]
|
||||
JS["JS SDK"]
|
||||
LCJS["Langchain JS Integration"] -->|extends| JS
|
||||
Flowise -->|uses| LCJS
|
||||
end
|
||||
end
|
||||
|
||||
API --> Worker_API
|
||||
Worker_API --- DB
|
||||
Worker_API --- Redis
|
||||
DB[Postgres Database]
|
||||
Redis[Redis Cache/Queue]
|
||||
Clickhouse[Clickhouse Database]
|
||||
|
||||
subgraph s1["Application (langfuse/langfuse/web)"]
|
||||
API[Public HTTP API]
|
||||
G[TRPC API]
|
||||
I[NextAuth]
|
||||
H[React Frontend]
|
||||
ORM
|
||||
H --> G
|
||||
H --> I
|
||||
G --> I
|
||||
G --- ORM
|
||||
API --- ORM
|
||||
I --- ORM
|
||||
end
|
||||
|
||||
subgraph s5["Application (langfuse/langfuse/worker)"]
|
||||
Worker
|
||||
end
|
||||
|
||||
Worker --- DB
|
||||
Worker --- Redis
|
||||
Worker --- Clickhouse
|
||||
|
||||
ORM --- DB
|
||||
ORM --- Redis
|
||||
ORM --- Clickhouse
|
||||
|
||||
Prisma --- DB
|
||||
JS --- API
|
||||
Python --- API
|
||||
```
|
||||
@@ -116,7 +165,7 @@ flowchart LR
|
||||
Browser ---|Web UI & TRPC API| App
|
||||
Integrations/SDKs ---|Public HTTP API| App
|
||||
subgraph i1["Application Network"]
|
||||
App["Langfuse Application (Docker or Serverless)"]
|
||||
App["Langfuse Application"]
|
||||
end
|
||||
subgraph i2["Database Network"]
|
||||
DB["Postgres Database"]
|
||||
|
||||
+2
-2
@@ -20,14 +20,14 @@
|
||||
"scripts": {
|
||||
"build": "tsc",
|
||||
"dev": "tsc --watch",
|
||||
"lint": "eslint . --ext .js,.jsx,.ts,.tsx",
|
||||
"lint": "eslint . --ext .js,.jsx,.ts,.tsx --max-warnings 0",
|
||||
"lint:fix": "eslint . --ext .js,.jsx,.ts,.tsx --fix"
|
||||
},
|
||||
"dependencies": {
|
||||
"@langfuse/shared": "workspace:*",
|
||||
"next": "^14.2.3",
|
||||
"next-auth": "^4.24.7",
|
||||
"zod": "^3.22.4"
|
||||
"zod": "^3.23.8"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@repo/eslint-config": "*",
|
||||
|
||||
@@ -9,7 +9,6 @@ service:
|
||||
method: POST
|
||||
path: /scores
|
||||
request: CreateScoreRequest
|
||||
response: Score
|
||||
|
||||
types:
|
||||
CreateScoreRequest:
|
||||
@@ -65,57 +64,45 @@ types:
|
||||
dataType: "BOOLEAN"
|
||||
configId: "1234-5678-90ab-cdef"
|
||||
traceId: "cdef-1234-5678-90ab"
|
||||
NumericScore:
|
||||
BaseScore:
|
||||
properties:
|
||||
id: string
|
||||
traceId: string
|
||||
name: string
|
||||
value:
|
||||
type: double
|
||||
docs: The numeric value of the score
|
||||
source: ScoreSource
|
||||
observationId: optional<string>
|
||||
timestamp: datetime
|
||||
createdAt: datetime
|
||||
updatedAt: datetime
|
||||
authorUserId: optional<string>
|
||||
comment: optional<string>
|
||||
configId:
|
||||
type: optional<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
|
||||
NumericScore:
|
||||
extends: BaseScore
|
||||
properties:
|
||||
value:
|
||||
type: double
|
||||
docs: The numeric value of the score
|
||||
BooleanScore:
|
||||
extends: BaseScore
|
||||
properties:
|
||||
id: string
|
||||
traceId: string
|
||||
name: string
|
||||
value:
|
||||
type: double
|
||||
docs: The numeric value of the score. Equals 1 for "True" and 0 for "False"
|
||||
stringValue:
|
||||
type: string
|
||||
docs: The string representation of the score value. Is inferred from the numeric value and equals "True" or "False"
|
||||
source: ScoreSource
|
||||
observationId: optional<string>
|
||||
timestamp: datetime
|
||||
comment: optional<string>
|
||||
configId:
|
||||
type: optional<string>
|
||||
docs: Reference a score config on a score. When set, config and score name must be equal
|
||||
CategoricalScore:
|
||||
extends: BaseScore
|
||||
properties:
|
||||
id: string
|
||||
traceId: string
|
||||
name: string
|
||||
value:
|
||||
type: optional<double>
|
||||
docs: Only defined if a config is linked. Represents the numeric category mapping of the stringValue
|
||||
stringValue:
|
||||
type: string
|
||||
docs: The string representation of the score value. If no config is linked, can be any string. Otherwise, must map to a config category
|
||||
source: ScoreSource
|
||||
observationId: optional<string>
|
||||
timestamp: datetime
|
||||
comment: optional<string>
|
||||
configId:
|
||||
type: optional<string>
|
||||
docs: Reference a score config on a score. When set, config and score name must be equal and stringValue must map to a config category
|
||||
Score:
|
||||
discriminant: "dataType"
|
||||
union:
|
||||
@@ -142,6 +129,6 @@ types:
|
||||
CreateScoreValue:
|
||||
discriminated: false
|
||||
union:
|
||||
- string
|
||||
- double
|
||||
- string
|
||||
docs: The value of the score. Must be passed as string for categorical scores, and numeric for boolean and numeric scores
|
||||
|
||||
@@ -42,11 +42,18 @@ types:
|
||||
htmlPath:
|
||||
type: string
|
||||
docs: Path of trace in Langfuse UI
|
||||
latency:
|
||||
type: double
|
||||
docs: Latency of trace in seconds
|
||||
totalCost:
|
||||
type: double
|
||||
docs: Cost of trace in USD
|
||||
observations: list<ObservationsView>
|
||||
scores: list<Score>
|
||||
observations:
|
||||
type: list<ObservationsView>
|
||||
docs: List of observations
|
||||
scores:
|
||||
type: list<Score>
|
||||
docs: List of scores
|
||||
Session:
|
||||
properties:
|
||||
id: string
|
||||
@@ -88,6 +95,7 @@ types:
|
||||
calculatedOutputCost: optional<double>
|
||||
calculatedTotalCost: optional<double>
|
||||
latency: optional<double>
|
||||
timeToFirstToken: optional<double>
|
||||
|
||||
Usage:
|
||||
docs: Standard interface for usage and cost
|
||||
@@ -137,57 +145,45 @@ types:
|
||||
properties:
|
||||
value: double
|
||||
label: string
|
||||
NumericScore:
|
||||
BaseScore:
|
||||
properties:
|
||||
id: string
|
||||
traceId: string
|
||||
name: string
|
||||
value:
|
||||
type: double
|
||||
docs: The numeric value of the score
|
||||
source: ScoreSource
|
||||
observationId: optional<string>
|
||||
timestamp: datetime
|
||||
createdAt: datetime
|
||||
updatedAt: datetime
|
||||
authorUserId: optional<string>
|
||||
comment: optional<string>
|
||||
configId:
|
||||
type: optional<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
|
||||
NumericScore:
|
||||
extends: BaseScore
|
||||
properties:
|
||||
value:
|
||||
type: double
|
||||
docs: The numeric value of the score
|
||||
BooleanScore:
|
||||
extends: BaseScore
|
||||
properties:
|
||||
id: string
|
||||
traceId: string
|
||||
name: string
|
||||
value:
|
||||
type: double
|
||||
docs: The numeric value of the score. Equals 1 for "True" and 0 for "False"
|
||||
stringValue:
|
||||
type: string
|
||||
docs: The string representation of the score value. Is inferred from the numeric value and equals "True" or "False"
|
||||
source: ScoreSource
|
||||
observationId: optional<string>
|
||||
timestamp: datetime
|
||||
comment: optional<string>
|
||||
configId:
|
||||
type: optional<string>
|
||||
docs: Reference a score config on a score. When set, config and score name must be equal
|
||||
CategoricalScore:
|
||||
extends: BaseScore
|
||||
properties:
|
||||
id: string
|
||||
traceId: string
|
||||
name: string
|
||||
value:
|
||||
type: optional<double>
|
||||
docs: Only defined if a config is linked. Represents the numeric category mapping of the stringValue
|
||||
stringValue:
|
||||
type: string
|
||||
docs: The string representation of the score value. If no config is linked, can be any string. Otherwise, must map to a config category
|
||||
source: ScoreSource
|
||||
observationId: optional<string>
|
||||
timestamp: datetime
|
||||
comment: optional<string>
|
||||
configId:
|
||||
type: optional<string>
|
||||
docs: Reference a score config on a score. When set, config and score name must be equal and stringValue must map to a config category
|
||||
Score:
|
||||
discriminant: "dataType"
|
||||
union:
|
||||
@@ -204,8 +200,8 @@ types:
|
||||
CreateScoreValue:
|
||||
discriminated: false
|
||||
union:
|
||||
- string
|
||||
- double
|
||||
- string
|
||||
docs: The value of the score. Must be passed as string for categorical scores, and numeric for boolean and numeric scores
|
||||
Dataset:
|
||||
properties:
|
||||
@@ -296,6 +292,7 @@ types:
|
||||
- MILLISECONDS
|
||||
- SECONDS
|
||||
- IMAGES
|
||||
- REQUESTS
|
||||
ObservationLevel:
|
||||
enum:
|
||||
- DEBUG
|
||||
|
||||
@@ -35,7 +35,10 @@ service:
|
||||
parentObservationId: optional<string>
|
||||
fromStartTime:
|
||||
type: optional<datetime>
|
||||
docs: Retrieve only observations with a start_time greater than this datetime (ISO 8601).
|
||||
docs: Retrieve only observations with a start_time or or after this datetime (ISO 8601).
|
||||
toStartTime:
|
||||
type: optional<datetime>
|
||||
docs: Retrieve only observations with a start_time before this datetime (ISO 8601).
|
||||
response: ObservationsViews
|
||||
|
||||
types:
|
||||
|
||||
@@ -11,7 +11,7 @@ service:
|
||||
method: POST
|
||||
path: /scores
|
||||
request: CreateScoreRequest
|
||||
response: commons.Score
|
||||
response: CreateScoreResponse
|
||||
get:
|
||||
docs: Get a list of scores
|
||||
method: GET
|
||||
@@ -33,7 +33,10 @@ service:
|
||||
docs: Retrieve only scores with this name.
|
||||
fromTimestamp:
|
||||
type: optional<datetime>
|
||||
docs: Retrieve only scores newer than this datetime (ISO 8601).
|
||||
docs: Optional filter to only include scores created on or after a certain datetime (ISO 8601)
|
||||
toTimestamp:
|
||||
type: optional<datetime>
|
||||
docs: Optional filter to only include scores created before a certain datetime (ISO 8601)
|
||||
source:
|
||||
type: optional<commons.ScoreSource>
|
||||
docs: Retrieve only scores from a specific source.
|
||||
@@ -124,7 +127,11 @@ types:
|
||||
dataType: "BOOLEAN"
|
||||
configId: "1234-5678-90ab-cdef"
|
||||
traceId: "cdef-1234-5678-90ab"
|
||||
|
||||
CreateScoreResponse:
|
||||
properties:
|
||||
id:
|
||||
type: string
|
||||
docs: The id of the created object in Langfuse
|
||||
Scores:
|
||||
properties:
|
||||
data: list<commons.Score>
|
||||
|
||||
@@ -6,6 +6,26 @@ service:
|
||||
auth: true
|
||||
base-path: /api/public
|
||||
endpoints:
|
||||
list:
|
||||
docs: Get sessions.
|
||||
method: GET
|
||||
path: /sessions
|
||||
request:
|
||||
name: GetSessionsRequest
|
||||
query-parameters:
|
||||
page:
|
||||
type: optional<integer>
|
||||
docs: Page number, starts at 1
|
||||
limit:
|
||||
type: optional<integer>
|
||||
docs: Limit of items per page. If you encounter api issues due to too large page sizes, try to reduce the limit.
|
||||
fromTimestamp:
|
||||
type: optional<datetime>
|
||||
docs: Optional filter to only include sessions created on or after a certain datetime (ISO 8601)
|
||||
toTimestamp:
|
||||
type: optional<datetime>
|
||||
docs: Optional filter to only include sessions created before a certain datetime (ISO 8601)
|
||||
response: PaginatedSessions
|
||||
get:
|
||||
docs: Get a session. Please note that `traces` on this endpoint are not paginated, if you plan to fetch large sessions, consider `GET /api/public/traces?sessionId=<sessionId>`
|
||||
method: GET
|
||||
@@ -15,3 +35,8 @@ service:
|
||||
type: string
|
||||
docs: The unique id of a session
|
||||
response: commons.SessionWithTraces
|
||||
types:
|
||||
PaginatedSessions:
|
||||
properties:
|
||||
data: list<commons.Session>
|
||||
meta: pagination.MetaResponse
|
||||
|
||||
@@ -33,7 +33,10 @@ service:
|
||||
sessionId: optional<string>
|
||||
fromTimestamp:
|
||||
type: optional<datetime>
|
||||
docs: Retrieve only traces newer than this datetime (ISO 8601).
|
||||
docs: Optional filter to only include traces with a trace.timestamp on or after a certain datetime (ISO 8601)
|
||||
toTimestamp:
|
||||
type: optional<datetime>
|
||||
docs: Optional filter to only include traces with a trace.timestamp before a certain datetime (ISO 8601)
|
||||
orderBy:
|
||||
type: optional<string>
|
||||
docs: "Format of the string [field].[asc/desc]. Fields: id, timestamp, name, userId, release, version, public, bookmarked, sessionId. Example: timestamp.asc"
|
||||
|
||||
+1
-4
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "langfuse",
|
||||
"version": "2.60.2",
|
||||
"version": "2.64.0",
|
||||
"author": "engineering@langfuse.com",
|
||||
"license": "MIT",
|
||||
"private": true,
|
||||
@@ -79,8 +79,5 @@
|
||||
"pr": ":rocket: _This pull request is included in v${version}. See [${releaseName}](${releaseUrl}) for release notes._"
|
||||
}
|
||||
}
|
||||
},
|
||||
"dependencies": {
|
||||
"newrelic": "^11.22.0"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -61,7 +61,7 @@
|
||||
"next-auth": "^4.24.7",
|
||||
"nodemailer": "^6.9.13",
|
||||
"prisma-extension-kysely": "^2.1.0",
|
||||
"zod": "^3.22.4",
|
||||
"zod": "^3.23.8",
|
||||
"zod-to-json-schema": "^3.22.5"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
-- Migration script to update score_configs entries
|
||||
-- Set categories to NULL where data_type is 'NUMERIC' and categories is an empty array
|
||||
|
||||
UPDATE score_configs
|
||||
SET categories = NULL
|
||||
WHERE data_type = 'NUMERIC' AND categories IS NOT NULL;
|
||||
@@ -0,0 +1,20 @@
|
||||
INSERT INTO models (
|
||||
id,
|
||||
project_id,
|
||||
model_name,
|
||||
match_pattern,
|
||||
start_date,
|
||||
input_price,
|
||||
output_price,
|
||||
total_price,
|
||||
unit,
|
||||
tokenizer_id,
|
||||
tokenizer_config
|
||||
)
|
||||
VALUES
|
||||
-- gpt-4o-mini
|
||||
('clyrjp56f0000t0mzapoocd7u', NULL, 'gpt-4o-mini', '(?i)^(gpt-4o-mini)$', NULL, 0.00000015, 0.0000006, NULL, 'TOKENS', 'openai', '{ "tokensPerMessage": 3, "tokensPerName": 1, "tokenizerModel": "gpt-4o" }'),
|
||||
|
||||
-- gpt-4o-mini-2024-07-18
|
||||
('clyrjpbe20000t0mzcbwc42rg', NULL, 'gpt-4o-mini-2024-07-18', '(?i)^(gpt-4o-mini-2024-07-18)$', NULL, 0.00000015, 0.0000006, NULL, 'TOKENS', 'openai', '{ "tokensPerMessage": 3, "tokensPerName": 1, "tokenizerModel": "gpt-4o" }')
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
|
||||
generator client {
|
||||
provider = "prisma-client-js"
|
||||
previewFeatures = ["tracing", "views", "relationJoins"]
|
||||
previewFeatures = ["tracing", "views", "relationJoins", "metrics"]
|
||||
}
|
||||
|
||||
datasource db {
|
||||
@@ -301,9 +301,9 @@ model Observation {
|
||||
updatedAt DateTime @default(now()) @updatedAt @map("updated_at")
|
||||
|
||||
// GENERATION ONLY
|
||||
model String?
|
||||
internalModel String? @map("internal_model")
|
||||
internalModelId String? @map("internal_model_id")
|
||||
model String? // user-provided model attribute
|
||||
internalModel String? @map("internal_model") // matched model.name that is matched at ingestion time, to be deprecated
|
||||
internalModelId String? @map("internal_model_id") // matched model.id that is matched at ingestion time
|
||||
|
||||
modelParameters Json?
|
||||
input Json?
|
||||
@@ -363,7 +363,7 @@ view ObservationView {
|
||||
statusMessage String? @map("status_message")
|
||||
version String?
|
||||
createdAt DateTime @map("created_at")
|
||||
updateAt DateTime @map("updated_at")
|
||||
updatedAt DateTime @map("updated_at")
|
||||
|
||||
// GENERATION ONLY
|
||||
model String?
|
||||
@@ -632,7 +632,7 @@ model Model {
|
||||
inputPrice Decimal? @map("input_price")
|
||||
outputPrice Decimal? @map("output_price")
|
||||
totalPrice Decimal? @map("total_price")
|
||||
unit String // TOKENS, CHARACTERS, MILLISECONDS, SECONDS, or IMAGES
|
||||
unit String // TOKENS, CHARACTERS, MILLISECONDS, SECONDS, REQUESTS, or IMAGES
|
||||
tokenizerId String? @map("tokenizer_id")
|
||||
tokenizerConfig Json? @map("tokenizer_config")
|
||||
|
||||
|
||||
@@ -710,7 +710,7 @@ function createObjects(
|
||||
},
|
||||
{
|
||||
role: "user",
|
||||
content: "How can i create a React component?",
|
||||
content: "How can i create a *React* component?",
|
||||
},
|
||||
]
|
||||
: {
|
||||
@@ -728,37 +728,8 @@ function createObjects(
|
||||
},
|
||||
],
|
||||
},
|
||||
output: {
|
||||
completion: `Creating a React component can be done in two ways: as a functional component or as a class component. Let's start with a basic example of both.
|
||||
|
||||
1. **Functional Component**:
|
||||
|
||||
A functional component is just a plain JavaScript function that accepts props as an argument, and returns a React element. Here's how you can create one:
|
||||
|
||||
|
||||
'import React from 'react'; function Greeting(props) { return <h1>Hello, {props.name}</h1>; } export default Greeting;'
|
||||
|
||||
To use this component in another file, you can do:
|
||||
|
||||
|
||||
'import Greeting from './Greeting'; function App() { return ( <div> <Greeting name="John" /> </div> ); } export default App;'
|
||||
|
||||
2. **Class Component**:
|
||||
|
||||
You can also define components as classes in React. These have some additional features compared to functional components:
|
||||
|
||||
|
||||
'import React, { Component } from 'react'; class Greeting extends Component { render() { return <h1>Hello, {this.props.name}</h1>; } } export default Greeting;'
|
||||
|
||||
And here's how to use this component:
|
||||
|
||||
|
||||
'import Greeting from './Greeting'; class App extends Component { render() { return ( <div> <Greeting name="John" /> </div> ); } } export default App;'
|
||||
|
||||
With the advent of hooks in React, functional components can do everything that class components can do and hence, the community has been favoring functional components over class components.
|
||||
|
||||
Remember to import React at the top of your file whenever you're creating a component, because JSX transpiles to 'React.createElement' calls under the hood.`,
|
||||
},
|
||||
output:
|
||||
"Creating a React component can be done in two ways: as a functional component or as a class component. Let's start with a basic example of both.\n\n1. **Functional Component**:\n\nA functional component is just a plain JavaScript function that accepts props as an argument, and returns a React element. Here's how you can create one:\n\n```javascript\nimport React from 'react';\nfunction Greeting(props) {\n return <h1>Hello, {props.name}</h1>;\n}\nexport default Greeting;\n```\n\nTo use this component in another file, you can do:\n\n```javascript\nimport Greeting from './Greeting';\nfunction App() {\n return (\n <div>\n <Greeting name=\"John\" />\n </div>\n );\n}\nexport default App;\n```\n\n2. **Class Component**:\n\nYou can also define components as classes in React. These have some additional features compared to functional components:\n\n```javascript\nimport React, { Component } from 'react';\nclass Greeting extends Component {\n render() {\n return <h1>Hello, {this.props.name}</h1>;\n }\n}\nexport default Greeting;\n```\n\nAnd here's how to use this component:\n\n```javascript\nimport Greeting from './Greeting';\nclass App extends Component {\n render() {\n return (\n <div>\n <Greeting name=\"John\" />\n </div>\n );\n }\n}\nexport default App;\n```\n\nWith the advent of hooks in React, functional components can do everything that class components can do and hence, the community has been favoring functional components over class components.\n\nRemember to import React at the top of your file whenever you're creating a component, because JSX transpiles to `React.createElement` calls under the hood.",
|
||||
model: model,
|
||||
internalModel: model,
|
||||
modelParameters: {
|
||||
|
||||
@@ -6,4 +6,5 @@ export enum ModelUsageUnit {
|
||||
Seconds = "SECONDS",
|
||||
Milliseconds = "MILLISECONDS",
|
||||
Images = "IMAGES",
|
||||
Requests = "REQUESTS",
|
||||
}
|
||||
|
||||
@@ -28,7 +28,7 @@ const MixedUsage = z.object({
|
||||
totalCost: z.number().nullish(),
|
||||
});
|
||||
|
||||
export const stringDate = z.string().datetime({ offset: true }).nullish();
|
||||
export const stringDateTime = z.string().datetime({ offset: true }).nullish();
|
||||
|
||||
export const usage = MixedUsage.nullish()
|
||||
// transform mixed usage model input to new one
|
||||
@@ -58,7 +58,7 @@ export const usage = MixedUsage.nullish()
|
||||
|
||||
export const TraceBody = z.object({
|
||||
id: z.string().nullish(),
|
||||
timestamp: stringDate,
|
||||
timestamp: stringDateTime,
|
||||
name: z.string().nullish(),
|
||||
externalId: z.string().nullish(),
|
||||
input: jsonSchema.nullish(),
|
||||
@@ -75,7 +75,7 @@ export const TraceBody = z.object({
|
||||
export const OptionalObservationBody = z.object({
|
||||
traceId: z.string().nullish(),
|
||||
name: z.string().nullish(),
|
||||
startTime: stringDate,
|
||||
startTime: stringDateTime,
|
||||
metadata: jsonSchema.nullish(),
|
||||
input: jsonSchema.nullish(),
|
||||
output: jsonSchema.nullish(),
|
||||
@@ -94,21 +94,27 @@ export const UpdateEventEvent = OptionalObservationBody.extend({
|
||||
});
|
||||
|
||||
export const CreateSpanBody = CreateEventEvent.extend({
|
||||
endTime: stringDate,
|
||||
endTime: stringDateTime,
|
||||
});
|
||||
|
||||
export const UpdateSpanBody = UpdateEventEvent.extend({
|
||||
endTime: stringDate,
|
||||
endTime: stringDateTime,
|
||||
});
|
||||
|
||||
export const CreateGenerationBody = CreateSpanBody.extend({
|
||||
completionStartTime: stringDate,
|
||||
completionStartTime: stringDateTime,
|
||||
model: z.string().nullish(),
|
||||
modelParameters: z
|
||||
.record(
|
||||
z.string(),
|
||||
z
|
||||
.union([z.string(), z.number(), z.boolean(), z.array(z.string())])
|
||||
.union([
|
||||
z.string(),
|
||||
z.number(),
|
||||
z.boolean(),
|
||||
z.array(z.string()),
|
||||
z.record(z.string()),
|
||||
])
|
||||
.nullish()
|
||||
)
|
||||
.nullish(),
|
||||
@@ -124,13 +130,19 @@ export const CreateGenerationBody = CreateSpanBody.extend({
|
||||
});
|
||||
|
||||
export const UpdateGenerationBody = UpdateSpanBody.extend({
|
||||
completionStartTime: stringDate,
|
||||
completionStartTime: stringDateTime,
|
||||
model: z.string().nullish(),
|
||||
modelParameters: z
|
||||
.record(
|
||||
z.string(),
|
||||
z
|
||||
.union([z.string(), z.number(), z.boolean(), z.array(z.string())])
|
||||
.union([
|
||||
z.string(),
|
||||
z.number(),
|
||||
z.boolean(),
|
||||
z.array(z.string()),
|
||||
z.record(z.string()),
|
||||
])
|
||||
.nullish()
|
||||
)
|
||||
.nullish(),
|
||||
@@ -219,8 +231,8 @@ export const LegacySpanPostSchema = z.object({
|
||||
id: z.string().nullish(),
|
||||
traceId: z.string().nullish(),
|
||||
name: z.string().nullish(),
|
||||
startTime: stringDate,
|
||||
endTime: stringDate,
|
||||
startTime: stringDateTime,
|
||||
endTime: stringDateTime,
|
||||
metadata: jsonSchema.nullish(),
|
||||
input: jsonSchema.nullish(),
|
||||
output: jsonSchema.nullish(),
|
||||
@@ -234,8 +246,8 @@ export const LegacySpanPatchSchema = z.object({
|
||||
spanId: z.string(),
|
||||
traceId: z.string().nullish(),
|
||||
name: z.string().nullish(),
|
||||
startTime: stringDate,
|
||||
endTime: stringDate,
|
||||
startTime: stringDateTime,
|
||||
endTime: stringDateTime,
|
||||
metadata: jsonSchema.nullish(),
|
||||
input: jsonSchema.nullish(),
|
||||
output: jsonSchema.nullish(),
|
||||
@@ -248,9 +260,9 @@ export const LegacyGenerationsCreateSchema = z.object({
|
||||
id: z.string().nullish(),
|
||||
traceId: z.string().nullish(),
|
||||
name: z.string().nullish(),
|
||||
startTime: stringDate,
|
||||
endTime: stringDate,
|
||||
completionStartTime: stringDate,
|
||||
startTime: stringDateTime,
|
||||
endTime: stringDateTime,
|
||||
completionStartTime: stringDateTime,
|
||||
model: z.string().nullish(),
|
||||
modelParameters: z
|
||||
.record(
|
||||
@@ -272,9 +284,9 @@ export const LegacyGenerationPatchSchema = z.object({
|
||||
generationId: z.string(),
|
||||
traceId: z.string().nullish(),
|
||||
name: z.string().nullish(),
|
||||
startTime: stringDate,
|
||||
endTime: stringDate,
|
||||
completionStartTime: stringDate,
|
||||
startTime: stringDateTime,
|
||||
endTime: stringDateTime,
|
||||
completionStartTime: stringDateTime,
|
||||
model: z.string().nullish(),
|
||||
modelParameters: z
|
||||
.record(
|
||||
@@ -296,9 +308,9 @@ export const LegacyObservationBody = z.object({
|
||||
traceId: z.string().nullish(),
|
||||
type: z.enum(["GENERATION", "SPAN", "EVENT"]),
|
||||
name: z.string().nullish(),
|
||||
startTime: stringDate,
|
||||
endTime: stringDate,
|
||||
completionStartTime: stringDate,
|
||||
startTime: stringDateTime,
|
||||
endTime: stringDateTime,
|
||||
completionStartTime: stringDateTime,
|
||||
model: z.string().nullish(),
|
||||
modelParameters: z
|
||||
.record(
|
||||
|
||||
Generated
+1367
-1951
File diff suppressed because it is too large
Load Diff
+11
-7
@@ -11,7 +11,6 @@ ENV PATH="$PNPM_HOME:$PATH"
|
||||
RUN corepack enable
|
||||
RUN corepack prepare pnpm@8.15.5 --activate
|
||||
|
||||
|
||||
FROM --platform=${TARGETPLATFORM:-linux/amd64} base AS pruner
|
||||
|
||||
WORKDIR /app
|
||||
@@ -19,7 +18,6 @@ WORKDIR /app
|
||||
COPY . .
|
||||
RUN turbo prune --scope=web --docker
|
||||
|
||||
|
||||
FROM --platform=${TARGETPLATFORM:-linux/amd64} base AS builder
|
||||
|
||||
WORKDIR /app
|
||||
@@ -32,6 +30,7 @@ COPY --from=pruner /app/out/json/ .
|
||||
RUN pnpm install --frozen-lockfile
|
||||
|
||||
ENV DOCKER_BUILD 1
|
||||
ENV NEXT_MANUAL_SIG_HANDLE true
|
||||
|
||||
# pass public variables in build step
|
||||
ARG NEXT_PUBLIC_LANGFUSE_CLOUD_REGION
|
||||
@@ -53,14 +52,13 @@ RUN rm -f ./web/src/middleware.ts
|
||||
# Learn more here: https://nextjs.org/telemetry
|
||||
# Uncomment the following line in case you want to disable telemetry during the build.
|
||||
ENV NEXT_TELEMETRY_DISABLED 1
|
||||
ENV NEXT_MANUAL_SIG_HANDLE true
|
||||
|
||||
RUN turbo run build --filter=web
|
||||
|
||||
# Production image, copy all the files and run next
|
||||
FROM --platform=${TARGETPLATFORM:-linux/amd64} base AS runner
|
||||
|
||||
RUN apk add --no-cache dumb-init
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
ENV NODE_ENV production
|
||||
@@ -68,13 +66,16 @@ ENV NODE_ENV production
|
||||
ENV NEXT_TELEMETRY_DISABLED 1
|
||||
# Needed to re-enable validation of environment variables during runtime
|
||||
ENV DOCKER_BUILD 0
|
||||
# Set NEXT_MANUAL_SIG_HANDLE for runtime
|
||||
ENV NEXT_MANUAL_SIG_HANDLE true
|
||||
|
||||
RUN apk add --no-cache dumb-init
|
||||
|
||||
RUN addgroup --system --gid 1001 nodejs
|
||||
RUN adduser --system --uid 1001 nextjs
|
||||
|
||||
RUN npm install -g --no-package-lock --no-save prisma
|
||||
|
||||
|
||||
COPY --from=builder --chown=nextjs:nodejs /app/web/next.config.mjs .
|
||||
COPY --from=builder --chown=nextjs:nodejs /app/web/package.json .
|
||||
|
||||
@@ -95,5 +96,8 @@ USER nextjs
|
||||
# Default port to 3000
|
||||
ENV PORT 3000
|
||||
|
||||
# CMD ["node", "server.js"]
|
||||
CMD ["dumb-init", "--", "./web/entrypoint.sh"]
|
||||
|
||||
ENTRYPOINT ["dumb-init", "--", "./web/entrypoint.sh"]
|
||||
|
||||
# startup command
|
||||
CMD ["node", "./web/server.js"]
|
||||
|
||||
+2
-2
@@ -35,5 +35,5 @@ if [ $status -ne 0 ]; then
|
||||
exit $status
|
||||
fi
|
||||
|
||||
# Start server
|
||||
node web/server.js
|
||||
# Run the command passed to the docker image on start
|
||||
exec "$@"
|
||||
+11
-16
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "web",
|
||||
"version": "2.60.2",
|
||||
"version": "2.64.0",
|
||||
"private": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
@@ -9,11 +9,11 @@
|
||||
"scripts": {
|
||||
"build": "INLINE_RUNTIME_CHUNK=false dotenv -e ../.env -- next build",
|
||||
"dev": "dotenv -e ../.env -- next dev",
|
||||
"lint": "dotenv -e ../.env -- next lint",
|
||||
"lint": "dotenv -e ../.env -- next lint --max-warnings 0",
|
||||
"lint:fix": "dotenv -e ../.env -- next lint --fix",
|
||||
"prettier": "prettier --write ./src *.{ts,js}",
|
||||
"clean": "rm -rf node_modules",
|
||||
"start": "dotenv -e ../.env -- next start",
|
||||
"start": "dotenv -e ../.env -- sh -c 'NEXT_MANUAL_SIG_HANDLE=true next start'",
|
||||
"test": "dotenv -e ../.env -- jest --runInBand",
|
||||
"test:watch": "dotenv -e ../.env -- jest --watch --runInBand",
|
||||
"test:e2e": "dotenv -e ../.env -- playwright test",
|
||||
@@ -36,18 +36,6 @@
|
||||
"@marsidev/react-turnstile": "^0.5.4",
|
||||
"@mui/x-tree-view": "^7.6.2",
|
||||
"@next-auth/prisma-adapter": "^1.0.7",
|
||||
"@opentelemetry/api": "^1.9.0",
|
||||
"@opentelemetry/auto-instrumentations-node": "^0.48.0",
|
||||
"@opentelemetry/exporter-jaeger": "^1.25.1",
|
||||
"@opentelemetry/exporter-trace-otlp-http": "^0.52.1",
|
||||
"@opentelemetry/resource-detector-aws": "^1.5.2",
|
||||
"@opentelemetry/resource-detector-container": "^0.3.11",
|
||||
"@opentelemetry/resources": "^1.25.1",
|
||||
"@opentelemetry/sdk-node": "^0.52.1",
|
||||
"@opentelemetry/sdk-trace-node": "^1.25.1",
|
||||
"@opentelemetry/semantic-conventions": "^1.25.1",
|
||||
"@opentelemetry/winston-transport": "^0.5.0",
|
||||
"@prisma/instrumentation": "^5.16.1",
|
||||
"@radix-ui/react-accordion": "^1.1.2",
|
||||
"@radix-ui/react-alert-dialog": "^1.0.5",
|
||||
"@radix-ui/react-avatar": "^1.0.4",
|
||||
@@ -99,6 +87,7 @@
|
||||
"decimal.js": "^10.4.3",
|
||||
"dompurify": "^3.1.5",
|
||||
"exponential-backoff": "^3.1.1",
|
||||
"graphql": "^16.9.0",
|
||||
"js-tiktoken": "^1.0.12",
|
||||
"kysely": "^0.27.3",
|
||||
"langchain": "^0.2.6",
|
||||
@@ -110,14 +99,19 @@
|
||||
"next-themes": "^0.3.0",
|
||||
"posthog-js": "^1.122.0",
|
||||
"posthog-node": "^3.6.3",
|
||||
"prexit": "^2.2.0",
|
||||
"prisma": "^5.13.0",
|
||||
"react": "18.2.0",
|
||||
"react-day-picker": "^8.10.1",
|
||||
"react-dom": "18.2.0",
|
||||
"react-hook-form": "^7.51.5",
|
||||
"react-icons": "^5.2.1",
|
||||
"react-markdown": "^9.0.1",
|
||||
"react-responsive": "^10.0.0",
|
||||
"react-syntax-highlighter": "^15.5.0",
|
||||
"react18-json-view": "^0.2.8-canary.6",
|
||||
"remark-gfm": "^4.0.0",
|
||||
"remark-math": "^6.0.0",
|
||||
"sonner": "^1.4.41",
|
||||
"superjson": "2.2.1",
|
||||
"tailwind-merge": "^2.2.1",
|
||||
@@ -125,7 +119,7 @@
|
||||
"use-query-params": "^2.2.1",
|
||||
"uuid": "^9.0.1",
|
||||
"vaul": "^0.9.1",
|
||||
"zod": "^3.22.4"
|
||||
"zod": "^3.23.8"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@jedmao/location": "^3.0.0",
|
||||
@@ -143,6 +137,7 @@
|
||||
"@types/node": "20.10.5",
|
||||
"@types/react": "^18.2.79",
|
||||
"@types/react-dom": "^18.2.25",
|
||||
"@types/react-syntax-highlighter": "^15.5.13",
|
||||
"@types/uuid": "^9.0.8",
|
||||
"@typescript-eslint/eslint-plugin": "^6.21.0",
|
||||
"@typescript-eslint/parser": "^7.12.0",
|
||||
|
||||
@@ -11,12 +11,8 @@ paths:
|
||||
- Score
|
||||
parameters: []
|
||||
responses:
|
||||
'200':
|
||||
'204':
|
||||
description: ''
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/Score'
|
||||
'400':
|
||||
description: ''
|
||||
content:
|
||||
@@ -90,8 +86,8 @@ components:
|
||||
- traceId
|
||||
- name
|
||||
- value
|
||||
NumericScore:
|
||||
title: NumericScore
|
||||
BaseScore:
|
||||
title: BaseScore
|
||||
type: object
|
||||
properties:
|
||||
id:
|
||||
@@ -100,10 +96,6 @@ components:
|
||||
type: string
|
||||
name:
|
||||
type: string
|
||||
value:
|
||||
type: number
|
||||
format: double
|
||||
description: The numeric value of the score
|
||||
source:
|
||||
$ref: '#/components/schemas/ScoreSource'
|
||||
observationId:
|
||||
@@ -111,6 +103,14 @@ components:
|
||||
timestamp:
|
||||
type: string
|
||||
format: date-time
|
||||
createdAt:
|
||||
type: string
|
||||
format: date-time
|
||||
updatedAt:
|
||||
type: string
|
||||
format: date-time
|
||||
authorUserId:
|
||||
type: string
|
||||
comment:
|
||||
type: string
|
||||
configId:
|
||||
@@ -123,19 +123,26 @@ components:
|
||||
- id
|
||||
- traceId
|
||||
- name
|
||||
- value
|
||||
- source
|
||||
- timestamp
|
||||
- createdAt
|
||||
- updatedAt
|
||||
NumericScore:
|
||||
title: NumericScore
|
||||
type: object
|
||||
properties:
|
||||
value:
|
||||
type: number
|
||||
format: double
|
||||
description: The numeric value of the score
|
||||
required:
|
||||
- value
|
||||
allOf:
|
||||
- $ref: '#/components/schemas/BaseScore'
|
||||
BooleanScore:
|
||||
title: BooleanScore
|
||||
type: object
|
||||
properties:
|
||||
id:
|
||||
type: string
|
||||
traceId:
|
||||
type: string
|
||||
name:
|
||||
type: string
|
||||
value:
|
||||
type: number
|
||||
format: double
|
||||
@@ -147,38 +154,15 @@ components:
|
||||
description: >-
|
||||
The string representation of the score value. Is inferred from the
|
||||
numeric value and equals "True" or "False"
|
||||
source:
|
||||
$ref: '#/components/schemas/ScoreSource'
|
||||
observationId:
|
||||
type: string
|
||||
timestamp:
|
||||
type: string
|
||||
format: date-time
|
||||
comment:
|
||||
type: string
|
||||
configId:
|
||||
type: string
|
||||
description: >-
|
||||
Reference a score config on a score. When set, config and score name
|
||||
must be equal
|
||||
required:
|
||||
- id
|
||||
- traceId
|
||||
- name
|
||||
- value
|
||||
- stringValue
|
||||
- source
|
||||
- timestamp
|
||||
allOf:
|
||||
- $ref: '#/components/schemas/BaseScore'
|
||||
CategoricalScore:
|
||||
title: CategoricalScore
|
||||
type: object
|
||||
properties:
|
||||
id:
|
||||
type: string
|
||||
traceId:
|
||||
type: string
|
||||
name:
|
||||
type: string
|
||||
value:
|
||||
type: number
|
||||
format: double
|
||||
@@ -190,27 +174,10 @@ components:
|
||||
description: >-
|
||||
The string representation of the score value. If no config is
|
||||
linked, can be any string. Otherwise, must map to a config category
|
||||
source:
|
||||
$ref: '#/components/schemas/ScoreSource'
|
||||
observationId:
|
||||
type: string
|
||||
timestamp:
|
||||
type: string
|
||||
format: date-time
|
||||
comment:
|
||||
type: string
|
||||
configId:
|
||||
type: string
|
||||
description: >-
|
||||
Reference a score config on a score. When set, config and score name
|
||||
must be equal and stringValue must map to a config category
|
||||
required:
|
||||
- id
|
||||
- traceId
|
||||
- name
|
||||
- stringValue
|
||||
- source
|
||||
- timestamp
|
||||
allOf:
|
||||
- $ref: '#/components/schemas/BaseScore'
|
||||
Score:
|
||||
title: Score
|
||||
oneOf:
|
||||
@@ -264,9 +231,9 @@ components:
|
||||
CreateScoreValue:
|
||||
title: CreateScoreValue
|
||||
oneOf:
|
||||
- type: string
|
||||
- type: number
|
||||
format: double
|
||||
- type: string
|
||||
description: >-
|
||||
The value of the score. Must be passed as string for categorical scores,
|
||||
and numeric for boolean and numeric scores
|
||||
|
||||
@@ -990,13 +990,23 @@ paths:
|
||||
- name: fromStartTime
|
||||
in: query
|
||||
description: >-
|
||||
Retrieve only observations with a start_time greater than this
|
||||
Retrieve only observations with a start_time or or after this
|
||||
datetime (ISO 8601).
|
||||
required: false
|
||||
schema:
|
||||
type: string
|
||||
format: date-time
|
||||
nullable: true
|
||||
- name: toStartTime
|
||||
in: query
|
||||
description: >-
|
||||
Retrieve only observations with a start_time before this datetime
|
||||
(ISO 8601).
|
||||
required: false
|
||||
schema:
|
||||
type: string
|
||||
format: date-time
|
||||
nullable: true
|
||||
responses:
|
||||
'200':
|
||||
description: ''
|
||||
@@ -1411,7 +1421,7 @@ paths:
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/Score'
|
||||
$ref: '#/components/schemas/CreateScoreResponse'
|
||||
'400':
|
||||
description: ''
|
||||
content:
|
||||
@@ -1482,7 +1492,19 @@ paths:
|
||||
nullable: true
|
||||
- name: fromTimestamp
|
||||
in: query
|
||||
description: Retrieve only scores newer than this datetime (ISO 8601).
|
||||
description: >-
|
||||
Optional filter to only include scores created on or after a certain
|
||||
datetime (ISO 8601)
|
||||
required: false
|
||||
schema:
|
||||
type: string
|
||||
format: date-time
|
||||
nullable: true
|
||||
- name: toTimestamp
|
||||
in: query
|
||||
description: >-
|
||||
Optional filter to only include scores created before a certain
|
||||
datetime (ISO 8601)
|
||||
required: false
|
||||
schema:
|
||||
type: string
|
||||
@@ -1651,6 +1673,82 @@ paths:
|
||||
application/json:
|
||||
schema: {}
|
||||
security: *ref_0
|
||||
/api/public/sessions:
|
||||
get:
|
||||
description: Get sessions.
|
||||
operationId: sessions_list
|
||||
tags:
|
||||
- Sessions
|
||||
parameters:
|
||||
- name: page
|
||||
in: query
|
||||
description: Page number, starts at 1
|
||||
required: false
|
||||
schema:
|
||||
type: integer
|
||||
nullable: true
|
||||
- name: limit
|
||||
in: query
|
||||
description: >-
|
||||
Limit of items per page. If you encounter api issues due to too
|
||||
large page sizes, try to reduce the limit.
|
||||
required: false
|
||||
schema:
|
||||
type: integer
|
||||
nullable: true
|
||||
- name: fromTimestamp
|
||||
in: query
|
||||
description: >-
|
||||
Optional filter to only include sessions created on or after a
|
||||
certain datetime (ISO 8601)
|
||||
required: false
|
||||
schema:
|
||||
type: string
|
||||
format: date-time
|
||||
nullable: true
|
||||
- name: toTimestamp
|
||||
in: query
|
||||
description: >-
|
||||
Optional filter to only include sessions created before a certain
|
||||
datetime (ISO 8601)
|
||||
required: false
|
||||
schema:
|
||||
type: string
|
||||
format: date-time
|
||||
nullable: true
|
||||
responses:
|
||||
'200':
|
||||
description: ''
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/PaginatedSessions'
|
||||
'400':
|
||||
description: ''
|
||||
content:
|
||||
application/json:
|
||||
schema: {}
|
||||
'401':
|
||||
description: ''
|
||||
content:
|
||||
application/json:
|
||||
schema: {}
|
||||
'403':
|
||||
description: ''
|
||||
content:
|
||||
application/json:
|
||||
schema: {}
|
||||
'404':
|
||||
description: ''
|
||||
content:
|
||||
application/json:
|
||||
schema: {}
|
||||
'405':
|
||||
description: ''
|
||||
content:
|
||||
application/json:
|
||||
schema: {}
|
||||
security: *ref_0
|
||||
/api/public/sessions/{sessionId}:
|
||||
get:
|
||||
description: >-
|
||||
@@ -1789,7 +1887,19 @@ paths:
|
||||
nullable: true
|
||||
- name: fromTimestamp
|
||||
in: query
|
||||
description: Retrieve only traces newer than this datetime (ISO 8601).
|
||||
description: >-
|
||||
Optional filter to only include traces with a trace.timestamp on or
|
||||
after a certain datetime (ISO 8601)
|
||||
required: false
|
||||
schema:
|
||||
type: string
|
||||
format: date-time
|
||||
nullable: true
|
||||
- name: toTimestamp
|
||||
in: query
|
||||
description: >-
|
||||
Optional filter to only include traces with a trace.timestamp before
|
||||
a certain datetime (ISO 8601)
|
||||
required: false
|
||||
schema:
|
||||
type: string
|
||||
@@ -1932,6 +2042,10 @@ components:
|
||||
htmlPath:
|
||||
type: string
|
||||
description: Path of trace in Langfuse UI
|
||||
latency:
|
||||
type: number
|
||||
format: double
|
||||
description: Latency of trace in seconds
|
||||
totalCost:
|
||||
type: number
|
||||
format: double
|
||||
@@ -1940,12 +2054,15 @@ components:
|
||||
type: array
|
||||
items:
|
||||
$ref: '#/components/schemas/ObservationsView'
|
||||
description: List of observations
|
||||
scores:
|
||||
type: array
|
||||
items:
|
||||
$ref: '#/components/schemas/Score'
|
||||
description: List of scores
|
||||
required:
|
||||
- htmlPath
|
||||
- latency
|
||||
- totalCost
|
||||
- observations
|
||||
- scores
|
||||
@@ -2074,6 +2191,10 @@ components:
|
||||
type: number
|
||||
format: double
|
||||
nullable: true
|
||||
timeToFirstToken:
|
||||
type: number
|
||||
format: double
|
||||
nullable: true
|
||||
allOf:
|
||||
- $ref: '#/components/schemas/Observation'
|
||||
Usage:
|
||||
@@ -2176,8 +2297,8 @@ components:
|
||||
required:
|
||||
- value
|
||||
- label
|
||||
NumericScore:
|
||||
title: NumericScore
|
||||
BaseScore:
|
||||
title: BaseScore
|
||||
type: object
|
||||
properties:
|
||||
id:
|
||||
@@ -2186,10 +2307,6 @@ components:
|
||||
type: string
|
||||
name:
|
||||
type: string
|
||||
value:
|
||||
type: number
|
||||
format: double
|
||||
description: The numeric value of the score
|
||||
source:
|
||||
$ref: '#/components/schemas/ScoreSource'
|
||||
observationId:
|
||||
@@ -2198,6 +2315,15 @@ components:
|
||||
timestamp:
|
||||
type: string
|
||||
format: date-time
|
||||
createdAt:
|
||||
type: string
|
||||
format: date-time
|
||||
updatedAt:
|
||||
type: string
|
||||
format: date-time
|
||||
authorUserId:
|
||||
type: string
|
||||
nullable: true
|
||||
comment:
|
||||
type: string
|
||||
nullable: true
|
||||
@@ -2212,19 +2338,26 @@ components:
|
||||
- id
|
||||
- traceId
|
||||
- name
|
||||
- value
|
||||
- source
|
||||
- timestamp
|
||||
- createdAt
|
||||
- updatedAt
|
||||
NumericScore:
|
||||
title: NumericScore
|
||||
type: object
|
||||
properties:
|
||||
value:
|
||||
type: number
|
||||
format: double
|
||||
description: The numeric value of the score
|
||||
required:
|
||||
- value
|
||||
allOf:
|
||||
- $ref: '#/components/schemas/BaseScore'
|
||||
BooleanScore:
|
||||
title: BooleanScore
|
||||
type: object
|
||||
properties:
|
||||
id:
|
||||
type: string
|
||||
traceId:
|
||||
type: string
|
||||
name:
|
||||
type: string
|
||||
value:
|
||||
type: number
|
||||
format: double
|
||||
@@ -2236,41 +2369,15 @@ components:
|
||||
description: >-
|
||||
The string representation of the score value. Is inferred from the
|
||||
numeric value and equals "True" or "False"
|
||||
source:
|
||||
$ref: '#/components/schemas/ScoreSource'
|
||||
observationId:
|
||||
type: string
|
||||
nullable: true
|
||||
timestamp:
|
||||
type: string
|
||||
format: date-time
|
||||
comment:
|
||||
type: string
|
||||
nullable: true
|
||||
configId:
|
||||
type: string
|
||||
nullable: true
|
||||
description: >-
|
||||
Reference a score config on a score. When set, config and score name
|
||||
must be equal
|
||||
required:
|
||||
- id
|
||||
- traceId
|
||||
- name
|
||||
- value
|
||||
- stringValue
|
||||
- source
|
||||
- timestamp
|
||||
allOf:
|
||||
- $ref: '#/components/schemas/BaseScore'
|
||||
CategoricalScore:
|
||||
title: CategoricalScore
|
||||
type: object
|
||||
properties:
|
||||
id:
|
||||
type: string
|
||||
traceId:
|
||||
type: string
|
||||
name:
|
||||
type: string
|
||||
value:
|
||||
type: number
|
||||
format: double
|
||||
@@ -2283,30 +2390,10 @@ components:
|
||||
description: >-
|
||||
The string representation of the score value. If no config is
|
||||
linked, can be any string. Otherwise, must map to a config category
|
||||
source:
|
||||
$ref: '#/components/schemas/ScoreSource'
|
||||
observationId:
|
||||
type: string
|
||||
nullable: true
|
||||
timestamp:
|
||||
type: string
|
||||
format: date-time
|
||||
comment:
|
||||
type: string
|
||||
nullable: true
|
||||
configId:
|
||||
type: string
|
||||
nullable: true
|
||||
description: >-
|
||||
Reference a score config on a score. When set, config and score name
|
||||
must be equal and stringValue must map to a config category
|
||||
required:
|
||||
- id
|
||||
- traceId
|
||||
- name
|
||||
- stringValue
|
||||
- source
|
||||
- timestamp
|
||||
allOf:
|
||||
- $ref: '#/components/schemas/BaseScore'
|
||||
Score:
|
||||
title: Score
|
||||
oneOf:
|
||||
@@ -2346,9 +2433,9 @@ components:
|
||||
CreateScoreValue:
|
||||
title: CreateScoreValue
|
||||
oneOf:
|
||||
- type: string
|
||||
- type: number
|
||||
format: double
|
||||
- type: string
|
||||
description: >-
|
||||
The value of the score. Must be passed as string for categorical scores,
|
||||
and numeric for boolean and numeric scores
|
||||
@@ -2563,6 +2650,7 @@ components:
|
||||
- MILLISECONDS
|
||||
- SECONDS
|
||||
- IMAGES
|
||||
- REQUESTS
|
||||
description: Unit of usage in Langfuse
|
||||
ObservationLevel:
|
||||
title: ObservationLevel
|
||||
@@ -3787,6 +3875,15 @@ components:
|
||||
- traceId
|
||||
- name
|
||||
- value
|
||||
CreateScoreResponse:
|
||||
title: CreateScoreResponse
|
||||
type: object
|
||||
properties:
|
||||
id:
|
||||
type: string
|
||||
description: The id of the created object in Langfuse
|
||||
required:
|
||||
- id
|
||||
Scores:
|
||||
title: Scores
|
||||
type: object
|
||||
@@ -3800,6 +3897,19 @@ components:
|
||||
required:
|
||||
- data
|
||||
- meta
|
||||
PaginatedSessions:
|
||||
title: PaginatedSessions
|
||||
type: object
|
||||
properties:
|
||||
data:
|
||||
type: array
|
||||
items:
|
||||
$ref: '#/components/schemas/Session'
|
||||
meta:
|
||||
$ref: '#/components/schemas/utilsMetaResponse'
|
||||
required:
|
||||
- data
|
||||
- meta
|
||||
Traces:
|
||||
title: Traces
|
||||
type: object
|
||||
|
||||
@@ -718,7 +718,7 @@
|
||||
"request": {
|
||||
"description": "Get a list of observations",
|
||||
"url": {
|
||||
"raw": "{{baseUrl}}/api/public/observations?page=&limit=&name=&userId=&type=&traceId=&parentObservationId=&fromStartTime=",
|
||||
"raw": "{{baseUrl}}/api/public/observations?page=&limit=&name=&userId=&type=&traceId=&parentObservationId=&fromStartTime=&toStartTime=",
|
||||
"host": [
|
||||
"{{baseUrl}}"
|
||||
],
|
||||
@@ -766,7 +766,12 @@
|
||||
{
|
||||
"key": "fromStartTime",
|
||||
"value": "",
|
||||
"description": "Retrieve only observations with a start_time greater than this datetime (ISO 8601)."
|
||||
"description": "Retrieve only observations with a start_time or or after this datetime (ISO 8601)."
|
||||
},
|
||||
{
|
||||
"key": "toStartTime",
|
||||
"value": "",
|
||||
"description": "Retrieve only observations with a start_time before this datetime (ISO 8601)."
|
||||
}
|
||||
],
|
||||
"variable": []
|
||||
@@ -1101,7 +1106,7 @@
|
||||
"request": {
|
||||
"description": "Get a list of scores",
|
||||
"url": {
|
||||
"raw": "{{baseUrl}}/api/public/scores?page=&limit=&userId=&name=&fromTimestamp=&source=&operator=&value=&scoreIds=&configId=&dataType=",
|
||||
"raw": "{{baseUrl}}/api/public/scores?page=&limit=&userId=&name=&fromTimestamp=&toTimestamp=&source=&operator=&value=&scoreIds=&configId=&dataType=",
|
||||
"host": [
|
||||
"{{baseUrl}}"
|
||||
],
|
||||
@@ -1134,7 +1139,12 @@
|
||||
{
|
||||
"key": "fromTimestamp",
|
||||
"value": "",
|
||||
"description": "Retrieve only scores newer than this datetime (ISO 8601)."
|
||||
"description": "Optional filter to only include scores created on or after a certain datetime (ISO 8601)"
|
||||
},
|
||||
{
|
||||
"key": "toTimestamp",
|
||||
"value": "",
|
||||
"description": "Optional filter to only include scores created before a certain datetime (ISO 8601)"
|
||||
},
|
||||
{
|
||||
"key": "source",
|
||||
@@ -1247,6 +1257,52 @@
|
||||
"description": null,
|
||||
"name": "Sessions",
|
||||
"item": [
|
||||
{
|
||||
"_type": "endpoint",
|
||||
"name": "List",
|
||||
"request": {
|
||||
"description": "Get sessions.",
|
||||
"url": {
|
||||
"raw": "{{baseUrl}}/api/public/sessions?page=&limit=&fromTimestamp=&toTimestamp=",
|
||||
"host": [
|
||||
"{{baseUrl}}"
|
||||
],
|
||||
"path": [
|
||||
"api",
|
||||
"public",
|
||||
"sessions"
|
||||
],
|
||||
"query": [
|
||||
{
|
||||
"key": "page",
|
||||
"value": "",
|
||||
"description": "Page number, starts at 1"
|
||||
},
|
||||
{
|
||||
"key": "limit",
|
||||
"value": "",
|
||||
"description": "Limit of items per page. If you encounter api issues due to too large page sizes, try to reduce the limit."
|
||||
},
|
||||
{
|
||||
"key": "fromTimestamp",
|
||||
"value": "",
|
||||
"description": "Optional filter to only include sessions created on or after a certain datetime (ISO 8601)"
|
||||
},
|
||||
{
|
||||
"key": "toTimestamp",
|
||||
"value": "",
|
||||
"description": "Optional filter to only include sessions created before a certain datetime (ISO 8601)"
|
||||
}
|
||||
],
|
||||
"variable": []
|
||||
},
|
||||
"header": [],
|
||||
"method": "GET",
|
||||
"auth": null,
|
||||
"body": null
|
||||
},
|
||||
"response": []
|
||||
},
|
||||
{
|
||||
"_type": "endpoint",
|
||||
"name": "Get",
|
||||
@@ -1324,7 +1380,7 @@
|
||||
"request": {
|
||||
"description": "Get list of traces.",
|
||||
"url": {
|
||||
"raw": "{{baseUrl}}/api/public/traces?page=&limit=&userId=&name=&sessionId=&fromTimestamp=&orderBy=&tags=",
|
||||
"raw": "{{baseUrl}}/api/public/traces?page=&limit=&userId=&name=&sessionId=&fromTimestamp=&toTimestamp=&orderBy=&tags=",
|
||||
"host": [
|
||||
"{{baseUrl}}"
|
||||
],
|
||||
@@ -1362,7 +1418,12 @@
|
||||
{
|
||||
"key": "fromTimestamp",
|
||||
"value": "",
|
||||
"description": "Retrieve only traces newer than this datetime (ISO 8601)."
|
||||
"description": "Optional filter to only include traces with a trace.timestamp on or after a certain datetime (ISO 8601)"
|
||||
},
|
||||
{
|
||||
"key": "toTimestamp",
|
||||
"value": "",
|
||||
"description": "Optional filter to only include traces with a trace.timestamp before a certain datetime (ISO 8601)"
|
||||
},
|
||||
{
|
||||
"key": "orderBy",
|
||||
|
||||
@@ -95,7 +95,8 @@ describe("/api/public/datasets and /api/public/dataset-items API Endpoints", ()
|
||||
expect(dbDataset.length).toBeGreaterThan(0);
|
||||
|
||||
// get dataset (v1) excluding items and runs
|
||||
const getDatasetV1 = await makeAPICall(
|
||||
const getDatasetV1 = await makeZodVerifiedAPICall(
|
||||
GetDatasetV1Response,
|
||||
"GET",
|
||||
`/api/public/datasets/${encodeURIComponent("dataset + name")}`,
|
||||
);
|
||||
|
||||
@@ -1,8 +1,13 @@
|
||||
/** @jest-environment node */
|
||||
|
||||
import { prisma } from "@langfuse/shared/src/db";
|
||||
import { makeAPICall, pruneDatabase } from "@/src/__tests__/test-utils";
|
||||
import {
|
||||
makeZodVerifiedAPICall,
|
||||
pruneDatabase,
|
||||
} from "@/src/__tests__/test-utils";
|
||||
import { v4 as uuidv4 } from "uuid";
|
||||
import { PostTracesV1Response } from "@/src/features/public-api/types/traces";
|
||||
import { PostEventsV1Response } from "@/src/features/public-api/types/events";
|
||||
|
||||
describe("/api/public/events API Endpoint", () => {
|
||||
beforeEach(async () => await pruneDatabase());
|
||||
@@ -12,15 +17,20 @@ describe("/api/public/events API Endpoint", () => {
|
||||
|
||||
const traceId = uuidv4();
|
||||
|
||||
await makeAPICall("POST", "/api/public/traces", {
|
||||
id: traceId,
|
||||
name: "trace-name",
|
||||
userId: "user-1",
|
||||
projectId: "7a88fb47-b4e2-43b8-a06c-a5ce950dc53a",
|
||||
metadata: { key: "value" },
|
||||
release: "1.0.0",
|
||||
version: "2.0.0",
|
||||
});
|
||||
await makeZodVerifiedAPICall(
|
||||
PostTracesV1Response,
|
||||
"POST",
|
||||
"/api/public/traces",
|
||||
{
|
||||
id: traceId,
|
||||
name: "trace-name",
|
||||
userId: "user-1",
|
||||
projectId: "7a88fb47-b4e2-43b8-a06c-a5ce950dc53a",
|
||||
metadata: { key: "value" },
|
||||
release: "1.0.0",
|
||||
version: "2.0.0",
|
||||
},
|
||||
);
|
||||
|
||||
const dbTrace = await prisma.trace.findMany({
|
||||
where: {
|
||||
@@ -32,16 +42,21 @@ describe("/api/public/events API Endpoint", () => {
|
||||
expect(dbTrace[0]?.id).toBe(traceId);
|
||||
|
||||
const eventId = uuidv4();
|
||||
const createEvent = await makeAPICall("POST", "/api/public/events", {
|
||||
id: eventId,
|
||||
traceId: traceId,
|
||||
name: "event-name",
|
||||
startTime: "2021-01-01T00:00:00.000Z",
|
||||
input: { input: "value" },
|
||||
output: { output: "value" },
|
||||
metadata: { meta: "value" },
|
||||
version: "2.0.0",
|
||||
});
|
||||
const createEvent = await makeZodVerifiedAPICall(
|
||||
PostEventsV1Response,
|
||||
"POST",
|
||||
"/api/public/events",
|
||||
{
|
||||
id: eventId,
|
||||
traceId: traceId,
|
||||
name: "event-name",
|
||||
startTime: "2021-01-01T00:00:00.000Z",
|
||||
input: { input: "value" },
|
||||
output: { output: "value" },
|
||||
metadata: { meta: "value" },
|
||||
version: "2.0.0",
|
||||
},
|
||||
);
|
||||
|
||||
expect(createEvent.status).toBe(200);
|
||||
const dbEvent = await prisma.observation.findUnique({
|
||||
@@ -66,16 +81,21 @@ describe("/api/public/events API Endpoint", () => {
|
||||
const traceId = uuidv4();
|
||||
const eventId = uuidv4();
|
||||
|
||||
const createEvent = await makeAPICall("POST", "/api/public/events", {
|
||||
id: eventId,
|
||||
traceId: traceId,
|
||||
name: "event-name",
|
||||
startTime: "2021-01-01T00:00:00.000Z",
|
||||
input: { input: "value" },
|
||||
output: { output: "value" },
|
||||
metadata: { meta: "value" },
|
||||
version: "2.0.0",
|
||||
});
|
||||
const createEvent = await makeZodVerifiedAPICall(
|
||||
PostEventsV1Response,
|
||||
"POST",
|
||||
"/api/public/events",
|
||||
{
|
||||
id: eventId,
|
||||
traceId: traceId,
|
||||
name: "event-name",
|
||||
startTime: "2021-01-01T00:00:00.000Z",
|
||||
input: { input: "value" },
|
||||
output: { output: "value" },
|
||||
metadata: { meta: "value" },
|
||||
version: "2.0.0",
|
||||
},
|
||||
);
|
||||
|
||||
expect(createEvent.status).toBe(200);
|
||||
const dbEvent = await prisma.observation.findUnique({
|
||||
@@ -93,15 +113,20 @@ describe("/api/public/events API Endpoint", () => {
|
||||
expect(dbEvent?.metadata).toEqual({ meta: "value" });
|
||||
expect(dbEvent?.version).toBe("2.0.0");
|
||||
|
||||
await makeAPICall("POST", "/api/public/traces", {
|
||||
id: traceId,
|
||||
name: "trace-name",
|
||||
userId: "user-1",
|
||||
projectId: "7a88fb47-b4e2-43b8-a06c-a5ce950dc53a",
|
||||
metadata: { key: "value" },
|
||||
release: "1.0.0",
|
||||
version: "2.0.0",
|
||||
});
|
||||
await makeZodVerifiedAPICall(
|
||||
PostTracesV1Response,
|
||||
"POST",
|
||||
"/api/public/traces",
|
||||
{
|
||||
id: traceId,
|
||||
name: "trace-name",
|
||||
userId: "user-1",
|
||||
projectId: "7a88fb47-b4e2-43b8-a06c-a5ce950dc53a",
|
||||
metadata: { key: "value" },
|
||||
release: "1.0.0",
|
||||
version: "2.0.0",
|
||||
},
|
||||
);
|
||||
|
||||
const dbTrace = await prisma.trace.findMany({
|
||||
where: {
|
||||
@@ -118,15 +143,20 @@ describe("/api/public/events API Endpoint", () => {
|
||||
|
||||
const traceId = uuidv4();
|
||||
|
||||
const response = await makeAPICall("POST", "/api/public/traces", {
|
||||
externalId: traceId,
|
||||
name: "trace-name",
|
||||
userId: "user-1",
|
||||
projectId: "7a88fb47-b4e2-43b8-a06c-a5ce950dc53a",
|
||||
metadata: { key: "value" },
|
||||
release: "1.0.0",
|
||||
version: "2.0.0",
|
||||
});
|
||||
const response = await makeZodVerifiedAPICall(
|
||||
PostTracesV1Response,
|
||||
"POST",
|
||||
"/api/public/traces",
|
||||
{
|
||||
externalId: traceId,
|
||||
name: "trace-name",
|
||||
userId: "user-1",
|
||||
projectId: "7a88fb47-b4e2-43b8-a06c-a5ce950dc53a",
|
||||
metadata: { key: "value" },
|
||||
release: "1.0.0",
|
||||
version: "2.0.0",
|
||||
},
|
||||
);
|
||||
|
||||
expect(response.status).toBe(200);
|
||||
|
||||
@@ -141,17 +171,22 @@ describe("/api/public/events API Endpoint", () => {
|
||||
expect(dbTrace[0]?.id).not.toBe(traceId);
|
||||
|
||||
const eventId = uuidv4();
|
||||
const createEvent = await makeAPICall("POST", "/api/public/events", {
|
||||
id: eventId,
|
||||
traceIdType: "EXTERNAL",
|
||||
traceId: dbTrace[0]?.id,
|
||||
name: "event-name",
|
||||
startTime: "2021-01-01T00:00:00.000Z",
|
||||
input: { input: "value" },
|
||||
output: { output: "value" },
|
||||
metadata: { meta: "value" },
|
||||
version: "2.0.0",
|
||||
});
|
||||
const createEvent = await makeZodVerifiedAPICall(
|
||||
PostEventsV1Response,
|
||||
"POST",
|
||||
"/api/public/events",
|
||||
{
|
||||
id: eventId,
|
||||
traceIdType: "EXTERNAL",
|
||||
traceId: dbTrace[0]?.id,
|
||||
name: "event-name",
|
||||
startTime: "2021-01-01T00:00:00.000Z",
|
||||
input: { input: "value" },
|
||||
output: { output: "value" },
|
||||
metadata: { meta: "value" },
|
||||
version: "2.0.0",
|
||||
},
|
||||
);
|
||||
|
||||
expect(createEvent.status).toBe(200);
|
||||
const dbEvent = await prisma.observation.findUnique({
|
||||
@@ -174,16 +209,21 @@ describe("/api/public/events API Endpoint", () => {
|
||||
const eventName = uuidv4();
|
||||
|
||||
const spanId = uuidv4();
|
||||
const createEvent = await makeAPICall("POST", "/api/public/events", {
|
||||
id: spanId,
|
||||
name: eventName,
|
||||
startTime: "2021-01-01T00:00:00.000Z",
|
||||
endTime: "2021-01-01T00:00:00.000Z",
|
||||
input: { input: "value" },
|
||||
output: { output: "value" },
|
||||
metadata: { meta: "value" },
|
||||
version: "2.0.0",
|
||||
});
|
||||
const createEvent = await makeZodVerifiedAPICall(
|
||||
PostEventsV1Response,
|
||||
"POST",
|
||||
"/api/public/events",
|
||||
{
|
||||
id: spanId,
|
||||
name: eventName,
|
||||
startTime: "2021-01-01T00:00:00.000Z",
|
||||
endTime: "2021-01-01T00:00:00.000Z",
|
||||
input: { input: "value" },
|
||||
output: { output: "value" },
|
||||
metadata: { meta: "value" },
|
||||
version: "2.0.0",
|
||||
},
|
||||
);
|
||||
|
||||
const dbTrace = await prisma.trace.findMany({
|
||||
where: {
|
||||
@@ -215,14 +255,19 @@ describe("/api/public/events API Endpoint", () => {
|
||||
const generationName = uuidv4();
|
||||
|
||||
const spanId = uuidv4();
|
||||
const createSpan = await makeAPICall("POST", "/api/public/events", {
|
||||
id: spanId,
|
||||
name: generationName,
|
||||
startTime: "2021-01-01T00:00:00.000Z",
|
||||
input: { key: "value" },
|
||||
metadata: { key: "value" },
|
||||
version: "2.0.0",
|
||||
});
|
||||
const createSpan = await makeZodVerifiedAPICall(
|
||||
PostEventsV1Response,
|
||||
"POST",
|
||||
"/api/public/events",
|
||||
{
|
||||
id: spanId,
|
||||
name: generationName,
|
||||
startTime: "2021-01-01T00:00:00.000Z",
|
||||
input: { key: "value" },
|
||||
metadata: { key: "value" },
|
||||
version: "2.0.0",
|
||||
},
|
||||
);
|
||||
|
||||
const dbEvent = await prisma.observation.findFirstOrThrow({
|
||||
where: {
|
||||
|
||||
@@ -2,9 +2,19 @@
|
||||
|
||||
import { v4 as uuidv4 } from "uuid";
|
||||
|
||||
import { makeAPICall, pruneDatabase } from "@/src/__tests__/test-utils";
|
||||
import {
|
||||
makeAPICall,
|
||||
makeZodVerifiedAPICall,
|
||||
pruneDatabase,
|
||||
} from "@/src/__tests__/test-utils";
|
||||
import { ModelUsageUnit } from "@langfuse/shared";
|
||||
import { prisma } from "@langfuse/shared/src/db";
|
||||
import { PostTracesV1Response } from "@/src/features/public-api/types/traces";
|
||||
import {
|
||||
PatchGenerationsV1Response,
|
||||
PostGenerationsV1Body,
|
||||
PostGenerationsV1Response,
|
||||
} from "@/src/features/public-api/types/generations";
|
||||
|
||||
describe("/api/public/generations API Endpoint", () => {
|
||||
beforeEach(async () => await pruneDatabase());
|
||||
@@ -90,15 +100,20 @@ describe("/api/public/generations API Endpoint", () => {
|
||||
|
||||
const traceId = uuidv4();
|
||||
|
||||
await makeAPICall("POST", "/api/public/traces", {
|
||||
id: traceId,
|
||||
name: "trace-name",
|
||||
userId: "user-1",
|
||||
projectId: "7a88fb47-b4e2-43b8-a06c-a5ce950dc53a",
|
||||
metadata: { key: "value" },
|
||||
release: "1.0.0",
|
||||
version: "2.0.0",
|
||||
});
|
||||
await makeZodVerifiedAPICall(
|
||||
PostTracesV1Response,
|
||||
"POST",
|
||||
"/api/public/traces",
|
||||
{
|
||||
id: traceId,
|
||||
name: "trace-name",
|
||||
userId: "user-1",
|
||||
projectId: "7a88fb47-b4e2-43b8-a06c-a5ce950dc53a",
|
||||
metadata: { key: "value" },
|
||||
release: "1.0.0",
|
||||
version: "2.0.0",
|
||||
},
|
||||
);
|
||||
|
||||
const dbTrace = await prisma.trace.findMany({
|
||||
where: {
|
||||
@@ -110,7 +125,8 @@ describe("/api/public/generations API Endpoint", () => {
|
||||
expect(dbTrace[0]?.id).toBe(traceId);
|
||||
|
||||
const generationId = uuidv4();
|
||||
const createGeneration = await makeAPICall(
|
||||
const createGeneration = await makeZodVerifiedAPICall(
|
||||
PostGenerationsV1Response,
|
||||
"POST",
|
||||
"/api/public/generations",
|
||||
{
|
||||
@@ -164,7 +180,8 @@ describe("/api/public/generations API Endpoint", () => {
|
||||
const traceId = uuidv4();
|
||||
|
||||
const generationId = uuidv4();
|
||||
const createGeneration = await makeAPICall(
|
||||
const createGeneration = await makeZodVerifiedAPICall(
|
||||
PostGenerationsV1Body,
|
||||
"POST",
|
||||
"/api/public/generations",
|
||||
{
|
||||
@@ -201,15 +218,20 @@ describe("/api/public/generations API Endpoint", () => {
|
||||
expect(dbGeneration?.metadata).toEqual({ key: "value" });
|
||||
expect(dbGeneration?.version).toBe("2.0.0");
|
||||
|
||||
await makeAPICall("POST", "/api/public/traces", {
|
||||
id: traceId,
|
||||
name: "trace-name",
|
||||
userId: "user-1",
|
||||
projectId: "7a88fb47-b4e2-43b8-a06c-a5ce950dc53a",
|
||||
metadata: { key: "value" },
|
||||
release: "1.0.0",
|
||||
version: "2.0.0",
|
||||
});
|
||||
await makeZodVerifiedAPICall(
|
||||
PostTracesV1Response,
|
||||
"POST",
|
||||
"/api/public/traces",
|
||||
{
|
||||
id: traceId,
|
||||
name: "trace-name",
|
||||
userId: "user-1",
|
||||
projectId: "7a88fb47-b4e2-43b8-a06c-a5ce950dc53a",
|
||||
metadata: { key: "value" },
|
||||
release: "1.0.0",
|
||||
version: "2.0.0",
|
||||
},
|
||||
);
|
||||
|
||||
const dbTrace = await prisma.trace.findMany({
|
||||
where: {
|
||||
@@ -224,15 +246,20 @@ describe("/api/public/generations API Endpoint", () => {
|
||||
it("should create generation after trace ignoring externalId", async () => {
|
||||
const traceId = uuidv4();
|
||||
|
||||
const response = await makeAPICall("POST", "/api/public/traces", {
|
||||
externalId: traceId,
|
||||
name: "trace-name",
|
||||
userId: "user-1",
|
||||
projectId: "7a88fb47-b4e2-43b8-a06c-a5ce950dc53a",
|
||||
metadata: { key: "value" },
|
||||
release: "1.0.0",
|
||||
version: "2.0.0",
|
||||
});
|
||||
const response = await makeZodVerifiedAPICall(
|
||||
PostTracesV1Response,
|
||||
"POST",
|
||||
"/api/public/traces",
|
||||
{
|
||||
externalId: traceId,
|
||||
name: "trace-name",
|
||||
userId: "user-1",
|
||||
projectId: "7a88fb47-b4e2-43b8-a06c-a5ce950dc53a",
|
||||
metadata: { key: "value" },
|
||||
release: "1.0.0",
|
||||
version: "2.0.0",
|
||||
},
|
||||
);
|
||||
|
||||
expect(response.status).toBe(200);
|
||||
|
||||
@@ -247,7 +274,8 @@ describe("/api/public/generations API Endpoint", () => {
|
||||
expect(dbTrace[0]?.id).not.toBe(traceId);
|
||||
|
||||
const generationId = uuidv4();
|
||||
const createGeneration = await makeAPICall(
|
||||
const createGeneration = await makeZodVerifiedAPICall(
|
||||
PostGenerationsV1Body,
|
||||
"POST",
|
||||
"/api/public/generations",
|
||||
{
|
||||
@@ -290,7 +318,8 @@ describe("/api/public/generations API Endpoint", () => {
|
||||
const generationName = uuidv4();
|
||||
|
||||
const generationId = uuidv4();
|
||||
const createGeneration = await makeAPICall(
|
||||
const createGeneration = await makeZodVerifiedAPICall(
|
||||
PostGenerationsV1Body,
|
||||
"POST",
|
||||
"/api/public/generations",
|
||||
{
|
||||
@@ -340,7 +369,8 @@ describe("/api/public/generations API Endpoint", () => {
|
||||
const generationName = uuidv4();
|
||||
|
||||
const generationId = uuidv4();
|
||||
const createGeneration = await makeAPICall(
|
||||
const createGeneration = await makeZodVerifiedAPICall(
|
||||
PostGenerationsV1Body,
|
||||
"POST",
|
||||
"/api/public/generations",
|
||||
{
|
||||
@@ -368,7 +398,8 @@ describe("/api/public/generations API Endpoint", () => {
|
||||
const generationId2 = uuidv4();
|
||||
const generationName2 = uuidv4();
|
||||
|
||||
const createGeneration2 = await makeAPICall(
|
||||
const createGeneration2 = await makeZodVerifiedAPICall(
|
||||
PostGenerationsV1Body,
|
||||
"POST",
|
||||
"/api/public/generations",
|
||||
{
|
||||
@@ -401,7 +432,8 @@ describe("/api/public/generations API Endpoint", () => {
|
||||
|
||||
const generationId = uuidv4();
|
||||
const externalTraceId = uuidv4();
|
||||
const createGeneration = await makeAPICall(
|
||||
const createGeneration = await makeZodVerifiedAPICall(
|
||||
PostGenerationsV1Body,
|
||||
"POST",
|
||||
"/api/public/generations",
|
||||
{
|
||||
@@ -437,7 +469,8 @@ describe("/api/public/generations API Endpoint", () => {
|
||||
const generationName = uuidv4();
|
||||
|
||||
const generationId = uuidv4();
|
||||
const createGeneration = await makeAPICall(
|
||||
const createGeneration = await makeZodVerifiedAPICall(
|
||||
PostGenerationsV1Body,
|
||||
"POST",
|
||||
"/api/public/generations",
|
||||
{
|
||||
@@ -488,7 +521,8 @@ describe("/api/public/generations API Endpoint", () => {
|
||||
const generationName = uuidv4();
|
||||
|
||||
const generationId = uuidv4();
|
||||
const createGeneration = await makeAPICall(
|
||||
const createGeneration = await makeZodVerifiedAPICall(
|
||||
PostGenerationsV1Body,
|
||||
"POST",
|
||||
"/api/public/generations",
|
||||
{
|
||||
@@ -506,7 +540,8 @@ describe("/api/public/generations API Endpoint", () => {
|
||||
|
||||
expect(createGeneration.status).toBe(200);
|
||||
|
||||
const updateGeneration = await makeAPICall(
|
||||
const updateGeneration = await makeZodVerifiedAPICall(
|
||||
PatchGenerationsV1Response,
|
||||
"PATCH",
|
||||
"/api/public/generations",
|
||||
{
|
||||
@@ -540,7 +575,8 @@ describe("/api/public/generations API Endpoint", () => {
|
||||
const generationName = uuidv4();
|
||||
|
||||
const generationId = uuidv4();
|
||||
const createGeneration = await makeAPICall(
|
||||
const createGeneration = await makeZodVerifiedAPICall(
|
||||
PostGenerationsV1Body,
|
||||
"POST",
|
||||
"/api/public/generations",
|
||||
{
|
||||
|
||||
@@ -57,6 +57,17 @@ describe("/api/public/ingestion API Endpoint", () => {
|
||||
expectedCompletionTokens: 2,
|
||||
expectedTotalTokens: 3,
|
||||
},
|
||||
{
|
||||
usage: {
|
||||
input: 1,
|
||||
output: 2,
|
||||
unit: ModelUsageUnit.Requests,
|
||||
},
|
||||
expectedUnit: ModelUsageUnit.Requests,
|
||||
expectedPromptTokens: 1,
|
||||
expectedCompletionTokens: 2,
|
||||
expectedTotalTokens: 3,
|
||||
},
|
||||
{
|
||||
usage: {
|
||||
input: 30,
|
||||
|
||||
@@ -0,0 +1,68 @@
|
||||
import { containsAnyMarkdown } from "@/src/components/schemas/MarkdownSchema";
|
||||
|
||||
describe("containsAnyMarkdown Function", () => {
|
||||
it("Detects simple bold using asterisks", () => {
|
||||
expect(containsAnyMarkdown("This is **bold** text")).toBe(true);
|
||||
});
|
||||
|
||||
it("Detects simple italics using asterisks", () => {
|
||||
expect(containsAnyMarkdown("This is *italic* text")).toBe(true);
|
||||
});
|
||||
|
||||
it("Detects bold using underscores", () => {
|
||||
expect(containsAnyMarkdown("This is __bold__ text")).toBe(true);
|
||||
});
|
||||
|
||||
it("Detects italics using underscores", () => {
|
||||
expect(containsAnyMarkdown("This is _italic_ text")).toBe(true);
|
||||
});
|
||||
|
||||
it("Detects inline code with backticks", () => {
|
||||
expect(containsAnyMarkdown("This is `code` inline")).toBe(true);
|
||||
});
|
||||
|
||||
it("Detects fenced code blocks", () => {
|
||||
const codeBlock = "```\nlet x = 10;\n```";
|
||||
expect(containsAnyMarkdown(codeBlock)).toBe(true);
|
||||
});
|
||||
|
||||
it("Detects headers", () => {
|
||||
expect(containsAnyMarkdown("# Header 1")).toBe(true);
|
||||
expect(containsAnyMarkdown("## Header 2")).toBe(true);
|
||||
});
|
||||
|
||||
it("Detects unordered lists", () => {
|
||||
expect(containsAnyMarkdown("- List item 1\n- List item 2")).toBe(true);
|
||||
});
|
||||
|
||||
it("Detects ordered lists", () => {
|
||||
expect(containsAnyMarkdown("1. First item\n2. Second item")).toBe(true);
|
||||
});
|
||||
|
||||
it("Detects blockquotes", () => {
|
||||
expect(containsAnyMarkdown("> This is a blockquote")).toBe(true);
|
||||
});
|
||||
|
||||
it("Detects links", () => {
|
||||
expect(containsAnyMarkdown("[Google](http://www.google.com)")).toBe(true);
|
||||
});
|
||||
|
||||
it("Detects images", () => {
|
||||
expect(containsAnyMarkdown("")).toBe(
|
||||
true,
|
||||
);
|
||||
});
|
||||
|
||||
it("Returns false for non-markdown text", () => {
|
||||
expect(
|
||||
containsAnyMarkdown("This is plain text without any markdown syntax"),
|
||||
).toBe(false);
|
||||
expect(containsAnyMarkdown("12345")).toBe(false);
|
||||
expect(containsAnyMarkdown("Simple text.")).toBe(false);
|
||||
});
|
||||
|
||||
it("Detects combination of markdown elements", () => {
|
||||
const complexMarkdown = "**Bold** and _italic_ and `code`";
|
||||
expect(containsAnyMarkdown(complexMarkdown)).toBe(true);
|
||||
});
|
||||
});
|
||||
@@ -1,4 +1,10 @@
|
||||
import { makeAPICall, pruneDatabase } from "@/src/__tests__/test-utils";
|
||||
import {
|
||||
makeZodVerifiedAPICall,
|
||||
pruneDatabase,
|
||||
} from "@/src/__tests__/test-utils";
|
||||
import { PostGenerationsV1Response } from "@/src/features/public-api/types/generations";
|
||||
import { GetMetricsDailyV1Response } from "@/src/features/public-api/types/metrics";
|
||||
import { PostTracesV1Response } from "@/src/features/public-api/types/traces";
|
||||
import { v4 as uuidv4 } from "uuid";
|
||||
|
||||
describe("/api/public/metrics/daily API Endpoint", () => {
|
||||
@@ -6,53 +12,89 @@ describe("/api/public/metrics/daily API Endpoint", () => {
|
||||
afterEach(async () => await pruneDatabase());
|
||||
|
||||
it("should handle daily metrics correctly", async () => {
|
||||
await pruneDatabase();
|
||||
|
||||
// Create traces with observations on different days
|
||||
const traceId1 = uuidv4();
|
||||
const traceId2 = uuidv4();
|
||||
await makeAPICall("POST", "/api/public/traces", {
|
||||
id: traceId1,
|
||||
timestamp: "2021-01-01T00:00:00.000Z",
|
||||
name: "trace-day-1",
|
||||
userId: "user-daily-metrics",
|
||||
projectId: "project-daily-metrics",
|
||||
});
|
||||
await makeAPICall("POST", "/api/public/traces", {
|
||||
id: traceId2,
|
||||
timestamp: "2021-01-02T00:00:00.000Z",
|
||||
name: "trace-day-2",
|
||||
userId: "user-daily-metrics",
|
||||
projectId: "project-daily-metrics",
|
||||
});
|
||||
await makeZodVerifiedAPICall(
|
||||
PostTracesV1Response,
|
||||
"POST",
|
||||
"/api/public/traces",
|
||||
{
|
||||
id: traceId1,
|
||||
timestamp: "2021-01-01T00:00:00.000Z",
|
||||
name: "trace-day-1",
|
||||
userId: "user-daily-metrics",
|
||||
projectId: "project-daily-metrics",
|
||||
},
|
||||
);
|
||||
await makeZodVerifiedAPICall(
|
||||
PostTracesV1Response,
|
||||
"POST",
|
||||
"/api/public/traces",
|
||||
{
|
||||
id: traceId2,
|
||||
timestamp: "2021-01-02T00:00:00.000Z",
|
||||
name: "trace-day-2",
|
||||
userId: "user-daily-metrics",
|
||||
projectId: "project-daily-metrics",
|
||||
},
|
||||
);
|
||||
|
||||
// Simulate observations with usage metrics on different days
|
||||
await makeAPICall("POST", "/api/public/generations", {
|
||||
traceId: traceId1,
|
||||
model: "modelA",
|
||||
usage: { input: 100, output: 200, total: 300 },
|
||||
startTime: "2021-01-01T00:00:00.000Z",
|
||||
endTime: "2021-01-01T00:01:00.000Z",
|
||||
});
|
||||
await makeAPICall("POST", "/api/public/generations", {
|
||||
traceId: traceId2,
|
||||
model: "modelB",
|
||||
usage: { input: 333 },
|
||||
startTime: "2021-01-02T00:00:00.000Z",
|
||||
endTime: "2021-01-02T00:02:00.000Z",
|
||||
});
|
||||
await makeAPICall("POST", "/api/public/generations", {
|
||||
traceId: traceId2,
|
||||
model: "modelC",
|
||||
usage: { input: 666, output: 777, totalCost: 1024.22 },
|
||||
startTime: "2021-01-02T00:00:00.000Z",
|
||||
endTime: "2021-01-02T00:04:00.000Z",
|
||||
});
|
||||
await makeZodVerifiedAPICall(
|
||||
PostGenerationsV1Response,
|
||||
"POST",
|
||||
"/api/public/generations",
|
||||
{
|
||||
traceId: traceId1,
|
||||
model: "modelA",
|
||||
usage: { input: 100, output: 200, total: 300 },
|
||||
startTime: "2021-01-01T00:00:00.000Z",
|
||||
endTime: "2021-01-01T00:01:00.000Z",
|
||||
},
|
||||
);
|
||||
await makeZodVerifiedAPICall(
|
||||
PostGenerationsV1Response,
|
||||
"POST",
|
||||
"/api/public/generations",
|
||||
{
|
||||
traceId: traceId2,
|
||||
model: "modelB",
|
||||
usage: { input: 333 },
|
||||
startTime: "2021-01-02T00:00:00.000Z",
|
||||
endTime: "2021-01-02T00:02:00.000Z",
|
||||
},
|
||||
);
|
||||
await makeZodVerifiedAPICall(
|
||||
PostGenerationsV1Response,
|
||||
"POST",
|
||||
"/api/public/generations",
|
||||
{
|
||||
traceId: traceId2,
|
||||
model: "modelC",
|
||||
usage: { input: 666, output: 777, totalCost: 1024.22 },
|
||||
startTime: "2021-01-02T00:00:00.000Z",
|
||||
endTime: "2021-01-02T00:04:00.000Z",
|
||||
},
|
||||
);
|
||||
await makeZodVerifiedAPICall(
|
||||
PostGenerationsV1Response,
|
||||
"POST",
|
||||
"/api/public/generations",
|
||||
{
|
||||
traceId: traceId2,
|
||||
usage: { output: 300 },
|
||||
startTime: "2021-01-02T00:00:00.000Z",
|
||||
endTime: "2021-01-02T00:04:00.000Z",
|
||||
},
|
||||
);
|
||||
|
||||
// Retrieve the daily metrics
|
||||
const dailyMetricsResponse = await makeAPICall<{
|
||||
data: Array<Record<string, unknown>>;
|
||||
}>("GET", `/api/public/metrics/daily`);
|
||||
const dailyMetricsResponse = await makeZodVerifiedAPICall(
|
||||
GetMetricsDailyV1Response,
|
||||
"GET",
|
||||
`/api/public/metrics/daily`,
|
||||
);
|
||||
const dailyMetricsData = dailyMetricsResponse.body.data;
|
||||
|
||||
// Check if the daily metrics are calculated correctly
|
||||
@@ -81,6 +123,15 @@ describe("/api/public/metrics/daily API Endpoint", () => {
|
||||
countTraces: 1,
|
||||
totalCost: 1024.22,
|
||||
},
|
||||
{
|
||||
model: null,
|
||||
countObservations: 1,
|
||||
countTraces: 1,
|
||||
inputUsage: 0,
|
||||
outputUsage: 300,
|
||||
totalCost: 0,
|
||||
totalUsage: 300,
|
||||
},
|
||||
]);
|
||||
|
||||
if (!dailyMetricsData[1])
|
||||
@@ -100,4 +151,48 @@ describe("/api/public/metrics/daily API Endpoint", () => {
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it("should handle daily metrics correctly when there is no data", async () => {
|
||||
// Retrieve the daily metrics
|
||||
const dailyMetricsResponse = await makeZodVerifiedAPICall(
|
||||
GetMetricsDailyV1Response,
|
||||
"GET",
|
||||
`/api/public/metrics/daily`,
|
||||
);
|
||||
const dailyMetricsData = dailyMetricsResponse.body.data;
|
||||
|
||||
// Check if the daily metrics are calculated correctly
|
||||
expect(dailyMetricsData).toHaveLength(0); // No data
|
||||
});
|
||||
|
||||
it("should handle daily metrics correctly when there is just a trace", async () => {
|
||||
const traceId1 = uuidv4();
|
||||
await makeZodVerifiedAPICall(
|
||||
PostTracesV1Response,
|
||||
"POST",
|
||||
"/api/public/traces",
|
||||
{
|
||||
id: traceId1,
|
||||
timestamp: "2021-01-01T00:00:00.000Z",
|
||||
name: "trace-day-1",
|
||||
userId: "user-daily-metrics",
|
||||
projectId: "project-daily-metrics",
|
||||
},
|
||||
);
|
||||
|
||||
// Retrieve the daily metrics
|
||||
const dailyMetricsResponse = await makeZodVerifiedAPICall(
|
||||
GetMetricsDailyV1Response,
|
||||
"GET",
|
||||
`/api/public/metrics/daily`,
|
||||
);
|
||||
const dailyMetricsData = dailyMetricsResponse.body.data;
|
||||
|
||||
// Check if the daily metrics are calculated correctly
|
||||
expect(dailyMetricsData).toHaveLength(1);
|
||||
expect(dailyMetricsData[0].date).toBe("2021-01-01");
|
||||
expect(dailyMetricsData[0].countTraces).toBe(1);
|
||||
expect(dailyMetricsData[0].totalCost).toEqual(0);
|
||||
expect(dailyMetricsData[0].usage).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -2,10 +2,16 @@
|
||||
|
||||
import { v4 as uuidv4 } from "uuid";
|
||||
|
||||
import { makeAPICall, pruneDatabase } from "@/src/__tests__/test-utils";
|
||||
import {
|
||||
makeZodVerifiedAPICall,
|
||||
pruneDatabase,
|
||||
} from "@/src/__tests__/test-utils";
|
||||
import { ModelUsageUnit } from "@langfuse/shared";
|
||||
import { prisma } from "@langfuse/shared/src/db";
|
||||
import { type ObservationView } from "@langfuse/shared";
|
||||
import {
|
||||
GetObservationV1Response,
|
||||
GetObservationsV1Response,
|
||||
} from "@/src/features/public-api/types/observations";
|
||||
|
||||
describe("/api/public/observations API Endpoint", () => {
|
||||
beforeEach(async () => await pruneDatabase());
|
||||
@@ -79,7 +85,8 @@ describe("/api/public/observations API Endpoint", () => {
|
||||
},
|
||||
});
|
||||
|
||||
const fetchedObservations = await makeAPICall(
|
||||
const fetchedObservations = await makeZodVerifiedAPICall(
|
||||
GetObservationsV1Response,
|
||||
"GET",
|
||||
"/api/public/observations",
|
||||
undefined,
|
||||
@@ -87,13 +94,6 @@ describe("/api/public/observations API Endpoint", () => {
|
||||
|
||||
expect(fetchedObservations.status).toBe(200);
|
||||
|
||||
if (!isObservationList(fetchedObservations.body)) {
|
||||
throw new Error(
|
||||
"Expected body to be an array of observations" +
|
||||
JSON.stringify(fetchedObservations.body),
|
||||
);
|
||||
}
|
||||
|
||||
expect(fetchedObservations.body.data.length).toBe(1);
|
||||
expect(fetchedObservations.body.data[0]?.traceId).toBe(traceId);
|
||||
expect(fetchedObservations.body.data[0]?.input).toEqual({ key: "input" });
|
||||
@@ -183,7 +183,8 @@ describe("/api/public/observations API Endpoint", () => {
|
||||
},
|
||||
});
|
||||
|
||||
const fetchedObservations = await makeAPICall(
|
||||
const fetchedObservations = await makeZodVerifiedAPICall(
|
||||
GetObservationsV1Response,
|
||||
"GET",
|
||||
"/api/public/observations?type=GENERATION",
|
||||
undefined,
|
||||
@@ -193,10 +194,6 @@ describe("/api/public/observations API Endpoint", () => {
|
||||
|
||||
expect(fetchedObservations.status).toBe(200);
|
||||
|
||||
if (!isObservationList(fetchedObservations.body)) {
|
||||
throw new Error("Expected body to be an array of observations");
|
||||
}
|
||||
|
||||
expect(fetchedObservations.body.data.length).toBe(1);
|
||||
expect(fetchedObservations.body.data[0]?.traceId).toBe(traceId);
|
||||
expect(fetchedObservations.body.data[0]?.input).toEqual({ key: "input" });
|
||||
@@ -205,37 +202,236 @@ describe("/api/public/observations API Endpoint", () => {
|
||||
});
|
||||
});
|
||||
|
||||
const isObservationList = (val: unknown): val is ObservationResponse => {
|
||||
return (
|
||||
typeof val === "object" &&
|
||||
val !== null &&
|
||||
"data" in val &&
|
||||
Array.isArray(val.data) &&
|
||||
val.data.every(
|
||||
(element) =>
|
||||
typeof element === "object" &&
|
||||
element !== null &&
|
||||
"id" in element &&
|
||||
"traceId" in element &&
|
||||
"name" in element &&
|
||||
"startTime" in element &&
|
||||
"endTime" in element &&
|
||||
"model" in element &&
|
||||
"input" in element &&
|
||||
"output" in element &&
|
||||
"metadata" in element &&
|
||||
"version" in element &&
|
||||
"modelId" in element &&
|
||||
"inputPrice" in element &&
|
||||
"outputPrice" in element &&
|
||||
"totalPrice" in element &&
|
||||
"calculatedInputCost" in element &&
|
||||
"calculatedOutputCost" in element &&
|
||||
"calculatedTotalCost" in element,
|
||||
)
|
||||
);
|
||||
};
|
||||
it("GET /observations with timestamp filters and pagination", async () => {
|
||||
await prisma.trace.create({
|
||||
data: {
|
||||
id: "trace-id",
|
||||
name: "trace-name",
|
||||
projectId: "7a88fb47-b4e2-43b8-a06c-a5ce950dc53a",
|
||||
},
|
||||
});
|
||||
await prisma.observation.createMany({
|
||||
data: [
|
||||
{
|
||||
id: "observation-2021-01-01",
|
||||
traceId: "trace-id",
|
||||
name: "generation-name",
|
||||
startTime: new Date("2021-01-01T00:00:00.000Z"),
|
||||
endTime: new Date("2021-01-01T00:00:00.000Z"),
|
||||
projectId: "7a88fb47-b4e2-43b8-a06c-a5ce950dc53a",
|
||||
type: "GENERATION",
|
||||
},
|
||||
{
|
||||
id: "observation-2021-02-01",
|
||||
traceId: "trace-id",
|
||||
name: "generation-name",
|
||||
startTime: new Date("2021-02-01T00:00:00.000Z"),
|
||||
endTime: new Date("2021-02-01T00:00:00.000Z"),
|
||||
projectId: "7a88fb47-b4e2-43b8-a06c-a5ce950dc53a",
|
||||
type: "SPAN",
|
||||
},
|
||||
{
|
||||
id: "observation-2021-03-01",
|
||||
traceId: "trace-id",
|
||||
name: "generation-name",
|
||||
startTime: new Date("2021-03-01T00:00:00.000Z"),
|
||||
projectId: "7a88fb47-b4e2-43b8-a06c-a5ce950dc53a",
|
||||
type: "EVENT",
|
||||
},
|
||||
{
|
||||
id: "observation-2021-04-01",
|
||||
traceId: "trace-id",
|
||||
name: "generation-name",
|
||||
startTime: new Date("2021-04-01T00:00:00.000Z"),
|
||||
endTime: new Date("2021-04-01T00:00:00.000Z"),
|
||||
projectId: "7a88fb47-b4e2-43b8-a06c-a5ce950dc53a",
|
||||
type: "GENERATION",
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
type ObservationResponse = {
|
||||
data: ObservationView[];
|
||||
};
|
||||
const fromTimestamp = "2021-02-01T00:00:00.000Z";
|
||||
const toTimestamp = "2021-04-01T00:00:00.000Z";
|
||||
|
||||
// Test with both fromTimestamp and toTimestamp
|
||||
let fetchedObservations = await makeZodVerifiedAPICall(
|
||||
GetObservationsV1Response,
|
||||
"GET",
|
||||
`/api/public/observations?fromStartTime=${fromTimestamp}&toStartTime=${toTimestamp}`,
|
||||
undefined,
|
||||
);
|
||||
|
||||
expect(fetchedObservations.body.data.length).toBe(2);
|
||||
expect(fetchedObservations.body.data[0]?.id).toBe("observation-2021-03-01");
|
||||
expect(fetchedObservations.body.data[1]?.id).toBe("observation-2021-02-01");
|
||||
expect(fetchedObservations.body.meta.totalItems).toBe(2);
|
||||
|
||||
// Test with only fromTimestamp
|
||||
fetchedObservations = await makeZodVerifiedAPICall(
|
||||
GetObservationsV1Response,
|
||||
"GET",
|
||||
`/api/public/observations?fromStartTime=${fromTimestamp}`,
|
||||
undefined,
|
||||
);
|
||||
|
||||
expect(fetchedObservations.body.data.length).toBe(3);
|
||||
expect(fetchedObservations.body.data[0]?.id).toBe("observation-2021-04-01");
|
||||
expect(fetchedObservations.body.data[1]?.id).toBe("observation-2021-03-01");
|
||||
expect(fetchedObservations.body.data[2]?.id).toBe("observation-2021-02-01");
|
||||
expect(fetchedObservations.body.meta.totalItems).toBe(3);
|
||||
|
||||
// Test with only toTimestamp
|
||||
fetchedObservations = await makeZodVerifiedAPICall(
|
||||
GetObservationsV1Response,
|
||||
"GET",
|
||||
`/api/public/observations?toStartTime=${toTimestamp}`,
|
||||
undefined,
|
||||
);
|
||||
|
||||
expect(fetchedObservations.body.data.length).toBe(3);
|
||||
expect(fetchedObservations.body.data[0]?.id).toBe("observation-2021-03-01");
|
||||
expect(fetchedObservations.body.data[1]?.id).toBe("observation-2021-02-01");
|
||||
expect(fetchedObservations.body.data[2]?.id).toBe("observation-2021-01-01");
|
||||
expect(fetchedObservations.body.meta.totalItems).toBe(3);
|
||||
|
||||
// test pagination only
|
||||
fetchedObservations = await makeZodVerifiedAPICall(
|
||||
GetObservationsV1Response,
|
||||
"GET",
|
||||
`/api/public/observations?limit=1&page=2`,
|
||||
undefined,
|
||||
);
|
||||
|
||||
expect(fetchedObservations.body.data.length).toBe(1);
|
||||
expect(fetchedObservations.body.data[0]?.id).toBe("observation-2021-03-01");
|
||||
expect(fetchedObservations.body.meta).toMatchObject({
|
||||
totalItems: 4,
|
||||
totalPages: 4,
|
||||
page: 2,
|
||||
limit: 1,
|
||||
});
|
||||
});
|
||||
|
||||
it("Get a single EVENT from /observations/:id", async () => {
|
||||
const traceId = uuidv4();
|
||||
await prisma.trace.create({
|
||||
data: {
|
||||
id: traceId,
|
||||
name: "trace-name",
|
||||
projectId: "7a88fb47-b4e2-43b8-a06c-a5ce950dc53a",
|
||||
},
|
||||
});
|
||||
|
||||
// EVENT
|
||||
const eventId = uuidv4();
|
||||
await prisma.observation.create({
|
||||
data: {
|
||||
id: eventId,
|
||||
traceId: traceId,
|
||||
name: "generation-name",
|
||||
startTime: new Date("2021-01-01T00:00:00.000Z"),
|
||||
type: "EVENT",
|
||||
project: {
|
||||
connect: { id: "7a88fb47-b4e2-43b8-a06c-a5ce950dc53a" },
|
||||
},
|
||||
},
|
||||
});
|
||||
const getEventRes = await makeZodVerifiedAPICall(
|
||||
GetObservationV1Response,
|
||||
"GET",
|
||||
"/api/public/observations/" + eventId,
|
||||
);
|
||||
expect(getEventRes.body).toMatchObject({
|
||||
id: eventId,
|
||||
traceId: traceId,
|
||||
type: "EVENT",
|
||||
});
|
||||
});
|
||||
it("Get a single GENERATION from /observations/:id", async () => {
|
||||
const traceId = uuidv4();
|
||||
await prisma.trace.create({
|
||||
data: {
|
||||
id: traceId,
|
||||
name: "trace-name",
|
||||
projectId: "7a88fb47-b4e2-43b8-a06c-a5ce950dc53a",
|
||||
},
|
||||
});
|
||||
|
||||
const generationId = uuidv4();
|
||||
await prisma.observation.create({
|
||||
data: {
|
||||
id: generationId,
|
||||
traceId: traceId,
|
||||
name: "generation-name",
|
||||
startTime: new Date("2021-01-01T00:00:00.000Z"),
|
||||
endTime: new Date("2021-01-01T00:00:00.000Z"),
|
||||
model: "gpt-3.5-turbo",
|
||||
internalModel: "gpt-3.5-turbo",
|
||||
modelParameters: { key: "value" },
|
||||
input: { key: "input" },
|
||||
output: { key: "output" },
|
||||
promptTokens: 10,
|
||||
completionTokens: 20,
|
||||
totalTokens: 30,
|
||||
version: "2.0.0",
|
||||
unit: ModelUsageUnit.Tokens,
|
||||
type: "GENERATION",
|
||||
project: {
|
||||
connect: { id: "7a88fb47-b4e2-43b8-a06c-a5ce950dc53a" },
|
||||
},
|
||||
},
|
||||
});
|
||||
const getGenerationRes = await makeZodVerifiedAPICall(
|
||||
GetObservationV1Response,
|
||||
"GET",
|
||||
"/api/public/observations/" + generationId,
|
||||
);
|
||||
expect(getGenerationRes.body).toMatchObject({
|
||||
id: generationId,
|
||||
traceId: traceId,
|
||||
input: { key: "input" },
|
||||
output: { key: "output" },
|
||||
type: "GENERATION",
|
||||
});
|
||||
});
|
||||
it("Get a single SPAN from /observations/:id", async () => {
|
||||
const traceId = uuidv4();
|
||||
await prisma.trace.create({
|
||||
data: {
|
||||
id: traceId,
|
||||
name: "trace-name",
|
||||
projectId: "7a88fb47-b4e2-43b8-a06c-a5ce950dc53a",
|
||||
},
|
||||
});
|
||||
|
||||
const spanId = uuidv4();
|
||||
await prisma.observation.create({
|
||||
data: {
|
||||
id: spanId,
|
||||
traceId: traceId,
|
||||
name: "generation-name",
|
||||
startTime: new Date("2021-01-01T00:00:00.000Z"),
|
||||
endTime: new Date("2021-01-01T00:00:00.000Z"),
|
||||
modelParameters: { key: "value" },
|
||||
input: { key: "input" },
|
||||
output: { key: "output" },
|
||||
version: "2.0.0",
|
||||
type: "SPAN",
|
||||
project: {
|
||||
connect: { id: "7a88fb47-b4e2-43b8-a06c-a5ce950dc53a" },
|
||||
},
|
||||
},
|
||||
});
|
||||
const getSpanRes = await makeZodVerifiedAPICall(
|
||||
GetObservationV1Response,
|
||||
"GET",
|
||||
"/api/public/observations/" + spanId,
|
||||
);
|
||||
expect(getSpanRes.body).toMatchObject({
|
||||
id: spanId,
|
||||
traceId: traceId,
|
||||
input: { key: "input" },
|
||||
output: { key: "output" },
|
||||
type: "SPAN",
|
||||
});
|
||||
});
|
||||
|
||||
@@ -219,7 +219,7 @@ describe("/api/public/prompts API Endpoint", () => {
|
||||
});
|
||||
|
||||
// Third prompt is deactivated
|
||||
const prompt3 = await makeAPICall("POST", "/api/public/prompts", {
|
||||
await makeAPICall("POST", "/api/public/prompts", {
|
||||
name: "prompt-name",
|
||||
projectId: "7a88fb47-b4e2-43b8-a06c-a5ce950dc53a",
|
||||
prompt: "prompt3",
|
||||
|
||||
@@ -13,6 +13,7 @@ import {
|
||||
GetScoresResponse,
|
||||
} from "@/src/features/public-api/types/scores";
|
||||
import { z } from "zod";
|
||||
import { PostTracesV1Response } from "@/src/features/public-api/types/traces";
|
||||
|
||||
const traceId = "de98afa2-89dc-47e9-9924-33f1490fdaf4";
|
||||
|
||||
@@ -21,15 +22,20 @@ describe("/api/public/scores API Endpoint", () => {
|
||||
beforeEach(async () => {
|
||||
if (should_prune_db) await pruneDatabase();
|
||||
|
||||
await makeAPICall("POST", "/api/public/traces", {
|
||||
id: traceId,
|
||||
name: "trace-name",
|
||||
userId: "user-1",
|
||||
projectId: "7a88fb47-b4e2-43b8-a06c-a5ce950dc53a",
|
||||
metadata: { key: "value" },
|
||||
release: "1.0.0",
|
||||
version: "2.0.0",
|
||||
});
|
||||
await makeZodVerifiedAPICall(
|
||||
PostTracesV1Response,
|
||||
"POST",
|
||||
"/api/public/traces",
|
||||
{
|
||||
id: traceId,
|
||||
name: "trace-name",
|
||||
userId: "user-1",
|
||||
projectId: "7a88fb47-b4e2-43b8-a06c-a5ce950dc53a",
|
||||
metadata: { key: "value" },
|
||||
release: "1.0.0",
|
||||
version: "2.0.0",
|
||||
},
|
||||
);
|
||||
});
|
||||
afterEach(async () => {
|
||||
if (should_prune_db) await pruneDatabase();
|
||||
@@ -105,22 +111,60 @@ describe("/api/public/scores API Endpoint", () => {
|
||||
expect(fetchedScore.body?.observationId).toBeNull();
|
||||
});
|
||||
|
||||
it("should GET score with minimal score data and minimal trace data", async () => {
|
||||
const minimalTraceId = uuidv4();
|
||||
await makeAPICall("POST", "/api/public/traces", {
|
||||
id: minimalTraceId,
|
||||
projectId: "7a88fb47-b4e2-43b8-a06c-a5ce950dc53a",
|
||||
});
|
||||
const dbTrace = await prisma.trace.findMany({
|
||||
where: {
|
||||
id: minimalTraceId,
|
||||
},
|
||||
});
|
||||
|
||||
expect(dbTrace.length).toBeGreaterThan(0);
|
||||
expect(dbTrace[0]?.id).toBe(minimalTraceId);
|
||||
|
||||
const minimalScoreId = uuidv4();
|
||||
|
||||
await makeAPICall("POST", "/api/public/scores", {
|
||||
id: minimalScoreId,
|
||||
name: "score-name",
|
||||
value: 100,
|
||||
traceId: minimalTraceId,
|
||||
});
|
||||
|
||||
const fetchedScore = await makeZodVerifiedAPICall(
|
||||
GetScoreResponse,
|
||||
"GET",
|
||||
`/api/public/scores/${minimalScoreId}`,
|
||||
);
|
||||
|
||||
expect(fetchedScore.status).toBe(200);
|
||||
});
|
||||
|
||||
it("should create score for a generation", async () => {
|
||||
await pruneDatabase();
|
||||
|
||||
const generationId = uuidv4();
|
||||
|
||||
await makeAPICall("POST", "/api/public/generations", {
|
||||
id: generationId,
|
||||
name: "generation-name",
|
||||
startTime: "2021-01-01T00:00:00.000Z",
|
||||
endTime: "2021-01-01T00:00:00.000Z",
|
||||
model: "model-name",
|
||||
modelParameters: { key: "value" },
|
||||
prompt: { key: "value" },
|
||||
metadata: { key: "value" },
|
||||
version: "2.0.0",
|
||||
});
|
||||
await makeZodVerifiedAPICall(
|
||||
PostTracesV1Response,
|
||||
"POST",
|
||||
"/api/public/generations",
|
||||
{
|
||||
id: generationId,
|
||||
name: "generation-name",
|
||||
startTime: "2021-01-01T00:00:00.000Z",
|
||||
endTime: "2021-01-01T00:00:00.000Z",
|
||||
model: "model-name",
|
||||
modelParameters: { key: "value" },
|
||||
prompt: { key: "value" },
|
||||
metadata: { key: "value" },
|
||||
version: "2.0.0",
|
||||
},
|
||||
);
|
||||
|
||||
const dbGeneration = await prisma.observation.findMany({
|
||||
where: {
|
||||
@@ -399,10 +443,8 @@ describe("/api/public/scores API Endpoint", () => {
|
||||
|
||||
expect(createScore.status).toBe(400);
|
||||
expect(createScore.body).toMatchObject({
|
||||
message: "Invalid request data",
|
||||
errors: [
|
||||
message:
|
||||
"Data type mismatch based on config: expected NUMERIC, got CATEGORICAL",
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
@@ -451,10 +493,8 @@ describe("/api/public/scores API Endpoint", () => {
|
||||
|
||||
expect(createScore.status).toBe(400);
|
||||
expect(createScore.body).toMatchObject({
|
||||
message: "Invalid request data",
|
||||
errors: [
|
||||
message:
|
||||
"Data type mismatch based on config: expected CATEGORICAL, got NUMERIC",
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
@@ -664,10 +704,8 @@ describe("/api/public/scores API Endpoint", () => {
|
||||
|
||||
expect(createScore.status).toBe(400);
|
||||
expect(createScore.body).toMatchObject({
|
||||
message: "Invalid request data",
|
||||
errors: [
|
||||
message:
|
||||
"Ingested score body not valid against provided config: - Value exceeds maximum value of 0 defined in config",
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
@@ -682,18 +720,23 @@ describe("/api/public/scores API Endpoint", () => {
|
||||
expect(dbTrace[0]?.id).toBe(traceId);
|
||||
|
||||
const generationId = uuidv4();
|
||||
await makeAPICall("POST", "/api/public/generations", {
|
||||
id: generationId,
|
||||
name: "generation-name",
|
||||
traceId,
|
||||
startTime: "2021-01-01T00:00:00.000Z",
|
||||
endTime: "2021-01-01T00:00:00.000Z",
|
||||
model: "model-name",
|
||||
modelParameters: { key: "value" },
|
||||
prompt: { key: "value" },
|
||||
metadata: { key: "value" },
|
||||
version: "2.0.0",
|
||||
});
|
||||
await makeZodVerifiedAPICall(
|
||||
PostTracesV1Response,
|
||||
"POST",
|
||||
"/api/public/generations",
|
||||
{
|
||||
id: generationId,
|
||||
name: "generation-name",
|
||||
traceId,
|
||||
startTime: "2021-01-01T00:00:00.000Z",
|
||||
endTime: "2021-01-01T00:00:00.000Z",
|
||||
model: "model-name",
|
||||
modelParameters: { key: "value" },
|
||||
prompt: { key: "value" },
|
||||
metadata: { key: "value" },
|
||||
version: "2.0.0",
|
||||
},
|
||||
);
|
||||
|
||||
const dbGeneration = await prisma.observation.findMany({
|
||||
where: {
|
||||
@@ -743,9 +786,14 @@ describe("/api/public/scores API Endpoint", () => {
|
||||
|
||||
const traceId = uuidv4();
|
||||
|
||||
await makeAPICall("POST", "/api/public/traces", {
|
||||
id: traceId,
|
||||
});
|
||||
await makeZodVerifiedAPICall(
|
||||
PostTracesV1Response,
|
||||
"POST",
|
||||
"/api/public/traces",
|
||||
{
|
||||
id: traceId,
|
||||
},
|
||||
);
|
||||
|
||||
const scoreId = uuidv4();
|
||||
const createScore = await makeAPICall("POST", "/api/public/scores", {
|
||||
@@ -783,13 +831,23 @@ describe("/api/public/scores API Endpoint", () => {
|
||||
|
||||
const traceId = uuidv4();
|
||||
|
||||
await makeAPICall("POST", "/api/public/traces", {
|
||||
id: traceId,
|
||||
});
|
||||
await makeZodVerifiedAPICall(
|
||||
PostTracesV1Response,
|
||||
"POST",
|
||||
"/api/public/traces",
|
||||
{
|
||||
id: traceId,
|
||||
},
|
||||
);
|
||||
const generationId = uuidv4();
|
||||
await makeAPICall("POST", "/api/public/generations", {
|
||||
id: generationId,
|
||||
});
|
||||
await makeZodVerifiedAPICall(
|
||||
PostTracesV1Response,
|
||||
"POST",
|
||||
"/api/public/generations",
|
||||
{
|
||||
id: generationId,
|
||||
},
|
||||
);
|
||||
|
||||
const scoreId = uuidv4();
|
||||
await makeAPICall("POST", "/api/public/scores", {
|
||||
@@ -835,13 +893,23 @@ describe("/api/public/scores API Endpoint", () => {
|
||||
should_prune_db = false;
|
||||
await pruneDatabase();
|
||||
|
||||
await makeAPICall("POST", "/api/public/traces", {
|
||||
id: traceId,
|
||||
userId: userId,
|
||||
});
|
||||
await makeAPICall("POST", "/api/public/generations", {
|
||||
id: generationId,
|
||||
});
|
||||
await makeZodVerifiedAPICall(
|
||||
PostTracesV1Response,
|
||||
"POST",
|
||||
"/api/public/traces",
|
||||
{
|
||||
id: traceId,
|
||||
userId: userId,
|
||||
},
|
||||
);
|
||||
await makeZodVerifiedAPICall(
|
||||
PostTracesV1Response,
|
||||
"POST",
|
||||
"/api/public/generations",
|
||||
{
|
||||
id: generationId,
|
||||
},
|
||||
);
|
||||
|
||||
await makeAPICall("POST", "/api/public/score-configs", {
|
||||
name: scoreName,
|
||||
@@ -1124,6 +1192,45 @@ describe("/api/public/scores API Endpoint", () => {
|
||||
},
|
||||
]);
|
||||
});
|
||||
it("should GET ALL scores with minimal score data and minimal trace data", async () => {
|
||||
const minimalTraceId = uuidv4();
|
||||
await makeAPICall("POST", "/api/public/traces", {
|
||||
id: minimalTraceId,
|
||||
projectId: "7a88fb47-b4e2-43b8-a06c-a5ce950dc53a",
|
||||
});
|
||||
const dbTrace = await prisma.trace.findMany({
|
||||
where: {
|
||||
id: minimalTraceId,
|
||||
},
|
||||
});
|
||||
|
||||
expect(dbTrace.length).toBeGreaterThan(0);
|
||||
expect(dbTrace[0]?.id).toBe(minimalTraceId);
|
||||
|
||||
const createScore = await makeAPICall("POST", "/api/public/scores", {
|
||||
name: "score-name",
|
||||
value: 100,
|
||||
traceId: minimalTraceId,
|
||||
});
|
||||
|
||||
expect(createScore.status).toBe(200);
|
||||
|
||||
const fetchedScores = await makeZodVerifiedAPICall(
|
||||
GetScoresResponse,
|
||||
"GET",
|
||||
`/api/public/scores`,
|
||||
);
|
||||
|
||||
expect(fetchedScores.status).toBe(200);
|
||||
expect(fetchedScores.body.meta).toMatchObject({
|
||||
page: 1,
|
||||
limit: 50,
|
||||
totalItems: 4,
|
||||
totalPages: 1,
|
||||
});
|
||||
expect(fetchedScores.body.data.length).toBe(4);
|
||||
});
|
||||
|
||||
it("test invalid operator", async () => {
|
||||
try {
|
||||
await makeZodVerifiedAPICall(
|
||||
|
||||
@@ -1,20 +1,33 @@
|
||||
/** @jest-environment node */
|
||||
|
||||
import { makeAPICall, pruneDatabase } from "@/src/__tests__/test-utils";
|
||||
import {
|
||||
makeZodVerifiedAPICall,
|
||||
pruneDatabase,
|
||||
} from "@/src/__tests__/test-utils";
|
||||
import {
|
||||
GetSessionV1Response,
|
||||
GetSessionsV1Response,
|
||||
} from "@/src/features/public-api/types/sessions";
|
||||
import { PostTracesV1Response } from "@/src/features/public-api/types/traces";
|
||||
import { prisma } from "@langfuse/shared/src/db";
|
||||
|
||||
describe("/api/public/traces API Endpoint", () => {
|
||||
describe("Create and get sessions", () => {
|
||||
beforeEach(async () => await pruneDatabase());
|
||||
afterEach(async () => await pruneDatabase());
|
||||
|
||||
it("should create a session via a trace", async () => {
|
||||
await pruneDatabase();
|
||||
|
||||
await makeAPICall("POST", "/api/public/traces", {
|
||||
name: "trace-name",
|
||||
projectId: "7a88fb47-b4e2-43b8-a06c-a5ce950dc53a",
|
||||
sessionId: "session-id",
|
||||
});
|
||||
await makeZodVerifiedAPICall(
|
||||
PostTracesV1Response,
|
||||
"POST",
|
||||
"/api/public/traces",
|
||||
{
|
||||
name: "trace-name",
|
||||
projectId: "7a88fb47-b4e2-43b8-a06c-a5ce950dc53a",
|
||||
sessionId: "session-id",
|
||||
},
|
||||
);
|
||||
|
||||
const dbSession = await prisma.traceSession.findFirst({
|
||||
where: {
|
||||
@@ -32,16 +45,22 @@ describe("/api/public/traces API Endpoint", () => {
|
||||
it("should get session including traces", async () => {
|
||||
await pruneDatabase();
|
||||
|
||||
await makeAPICall("POST", "/api/public/traces", {
|
||||
name: "trace-name",
|
||||
id: "trace-id",
|
||||
input: { hello: "world" },
|
||||
output: "hi",
|
||||
projectId: "7a88fb47-b4e2-43b8-a06c-a5ce950dc53a",
|
||||
sessionId: "session-id",
|
||||
});
|
||||
await makeZodVerifiedAPICall(
|
||||
PostTracesV1Response,
|
||||
"POST",
|
||||
"/api/public/traces",
|
||||
{
|
||||
name: "trace-name",
|
||||
id: "trace-id",
|
||||
input: { hello: "world" },
|
||||
output: "hi",
|
||||
projectId: "7a88fb47-b4e2-43b8-a06c-a5ce950dc53a",
|
||||
sessionId: "session-id",
|
||||
},
|
||||
);
|
||||
|
||||
const response = await makeAPICall(
|
||||
const response = await makeZodVerifiedAPICall(
|
||||
GetSessionV1Response,
|
||||
"GET",
|
||||
"/api/public/sessions/session-id",
|
||||
);
|
||||
@@ -60,3 +79,122 @@ describe("/api/public/traces API Endpoint", () => {
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("GET /api/public/sessions API Endpoint", () => {
|
||||
beforeEach(async () => {
|
||||
await pruneDatabase();
|
||||
await prisma.traceSession.createMany({
|
||||
data: [
|
||||
{
|
||||
id: "session-2021-01-01",
|
||||
createdAt: new Date("2021-01-01T00:00:00Z"),
|
||||
projectId: "7a88fb47-b4e2-43b8-a06c-a5ce950dc53a",
|
||||
},
|
||||
{
|
||||
id: "session-2021-02-01",
|
||||
createdAt: new Date("2021-02-01T00:00:00Z"),
|
||||
projectId: "7a88fb47-b4e2-43b8-a06c-a5ce950dc53a",
|
||||
},
|
||||
{
|
||||
id: "session-2021-03-01",
|
||||
createdAt: new Date("2021-03-01T00:00:00Z"),
|
||||
projectId: "7a88fb47-b4e2-43b8-a06c-a5ce950dc53a",
|
||||
},
|
||||
{
|
||||
id: "session-2021-04-01",
|
||||
createdAt: new Date("2021-04-01T00:00:00Z"),
|
||||
projectId: "7a88fb47-b4e2-43b8-a06c-a5ce950dc53a",
|
||||
},
|
||||
{
|
||||
id: "session-2021-05-01",
|
||||
createdAt: new Date("2021-05-01T00:00:00Z"),
|
||||
projectId: "7a88fb47-b4e2-43b8-a06c-a5ce950dc53a",
|
||||
},
|
||||
],
|
||||
});
|
||||
});
|
||||
afterEach(async () => await pruneDatabase());
|
||||
|
||||
it("should return all sessions", async () => {
|
||||
const sessions = await makeZodVerifiedAPICall(
|
||||
GetSessionsV1Response,
|
||||
"GET",
|
||||
"/api/public/sessions",
|
||||
);
|
||||
expect(sessions.body.data).toHaveLength(5);
|
||||
});
|
||||
|
||||
it("should return paginated sessions with limit and page number", async () => {
|
||||
const limit = 2;
|
||||
const page = 2;
|
||||
|
||||
const sessions = await makeZodVerifiedAPICall(
|
||||
GetSessionsV1Response,
|
||||
"GET",
|
||||
`/api/public/sessions?limit=${limit}&page=${page}`,
|
||||
);
|
||||
|
||||
expect(sessions.body.data).toHaveLength(2);
|
||||
expect(sessions.body.data.map((session) => session.id)).toEqual([
|
||||
"session-2021-03-01",
|
||||
"session-2021-02-01",
|
||||
]);
|
||||
expect(sessions.body.meta.totalItems).toBe(5);
|
||||
expect(sessions.body.meta.totalPages).toBe(3);
|
||||
expect(sessions.body.meta.page).toBe(page);
|
||||
expect(sessions.body.meta.limit).toBe(limit);
|
||||
});
|
||||
|
||||
it("should return sessions within a specific date range", async () => {
|
||||
const fromTimestamp = "2021-02-01T00:00:00Z";
|
||||
const toTimestamp = "2021-04-01T00:00:00Z";
|
||||
|
||||
const sessions = await makeZodVerifiedAPICall(
|
||||
GetSessionsV1Response,
|
||||
"GET",
|
||||
`/api/public/sessions?fromTimestamp=${fromTimestamp}&toTimestamp=${toTimestamp}`,
|
||||
);
|
||||
|
||||
expect(sessions.body.data).toHaveLength(2);
|
||||
expect(sessions.body.data.map((session) => session.id)).toEqual([
|
||||
"session-2021-03-01",
|
||||
"session-2021-02-01",
|
||||
]);
|
||||
expect(sessions.body.meta.totalItems).toBe(2);
|
||||
});
|
||||
|
||||
it("should return sessions from a specific date onwards (including the date)", async () => {
|
||||
const fromTimestamp = "2021-03-01T00:00:00Z";
|
||||
|
||||
const sessions = await makeZodVerifiedAPICall(
|
||||
GetSessionsV1Response,
|
||||
"GET",
|
||||
`/api/public/sessions?fromTimestamp=${fromTimestamp}`,
|
||||
);
|
||||
|
||||
expect(sessions.body.data).toHaveLength(3);
|
||||
expect(sessions.body.data.map((session) => session.id)).toEqual([
|
||||
"session-2021-05-01",
|
||||
"session-2021-04-01",
|
||||
"session-2021-03-01",
|
||||
]);
|
||||
expect(sessions.body.meta.totalItems).toBe(3);
|
||||
});
|
||||
|
||||
it("should return sessions up to a specific date (excluding it)", async () => {
|
||||
const toTimestamp = "2021-03-01T00:00:00Z";
|
||||
|
||||
const sessions = await makeZodVerifiedAPICall(
|
||||
GetSessionsV1Response,
|
||||
"GET",
|
||||
`/api/public/sessions?toTimestamp=${toTimestamp}`,
|
||||
);
|
||||
|
||||
expect(sessions.body.data).toHaveLength(2);
|
||||
expect(sessions.body.data.map((session) => session.id)).toEqual([
|
||||
"session-2021-02-01",
|
||||
"session-2021-01-01",
|
||||
]);
|
||||
expect(sessions.body.meta.totalItems).toBe(2);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,27 +1,39 @@
|
||||
/** @jest-environment node */
|
||||
|
||||
import { prisma } from "@langfuse/shared/src/db";
|
||||
import { makeAPICall, pruneDatabase } from "@/src/__tests__/test-utils";
|
||||
import {
|
||||
makeAPICall,
|
||||
makeZodVerifiedAPICall,
|
||||
pruneDatabase,
|
||||
} from "@/src/__tests__/test-utils";
|
||||
import { v4 as uuidv4 } from "uuid";
|
||||
import { PostTracesV1Response } from "@/src/features/public-api/types/traces";
|
||||
import {
|
||||
PatchSpansV1Response,
|
||||
PostSpansV1Response,
|
||||
} from "@/src/features/public-api/types/spans";
|
||||
|
||||
describe("/api/public/spans API Endpoint", () => {
|
||||
beforeEach(async () => await pruneDatabase());
|
||||
afterEach(async () => await pruneDatabase());
|
||||
|
||||
it("should create span after trace", async () => {
|
||||
await pruneDatabase();
|
||||
|
||||
const traceId = uuidv4();
|
||||
|
||||
await makeAPICall("POST", "/api/public/traces", {
|
||||
id: traceId,
|
||||
name: "trace-name",
|
||||
userId: "user-1",
|
||||
projectId: "7a88fb47-b4e2-43b8-a06c-a5ce950dc53a",
|
||||
metadata: { key: "value" },
|
||||
release: "1.0.0",
|
||||
version: "2.0.0",
|
||||
});
|
||||
await makeZodVerifiedAPICall(
|
||||
PostTracesV1Response,
|
||||
"POST",
|
||||
"/api/public/traces",
|
||||
{
|
||||
id: traceId,
|
||||
name: "trace-name",
|
||||
userId: "user-1",
|
||||
projectId: "7a88fb47-b4e2-43b8-a06c-a5ce950dc53a",
|
||||
metadata: { key: "value" },
|
||||
release: "1.0.0",
|
||||
version: "2.0.0",
|
||||
},
|
||||
);
|
||||
|
||||
const dbTrace = await prisma.trace.findMany({
|
||||
where: {
|
||||
@@ -33,16 +45,21 @@ describe("/api/public/spans API Endpoint", () => {
|
||||
expect(dbTrace[0]?.id).toBe(traceId);
|
||||
|
||||
const spanId = uuidv4();
|
||||
const createSpan = await makeAPICall("POST", "/api/public/spans", {
|
||||
id: spanId,
|
||||
traceId: traceId,
|
||||
name: "span-name",
|
||||
startTime: "2021-01-01T00:00:00.000Z",
|
||||
endTime: "2021-01-01T00:00:00.000Z",
|
||||
input: { input: "value" },
|
||||
metadata: { meta: "value" },
|
||||
version: "2.0.0",
|
||||
});
|
||||
const createSpan = await makeZodVerifiedAPICall(
|
||||
PostSpansV1Response,
|
||||
"POST",
|
||||
"/api/public/spans",
|
||||
{
|
||||
id: spanId,
|
||||
traceId: traceId,
|
||||
name: "span-name",
|
||||
startTime: "2021-01-01T00:00:00.000Z",
|
||||
endTime: "2021-01-01T00:00:00.000Z",
|
||||
input: { input: "value" },
|
||||
metadata: { meta: "value" },
|
||||
version: "2.0.0",
|
||||
},
|
||||
);
|
||||
|
||||
expect(createSpan.status).toBe(200);
|
||||
const dbSpan = await prisma.observation.findUnique({
|
||||
@@ -62,21 +79,24 @@ describe("/api/public/spans API Endpoint", () => {
|
||||
});
|
||||
|
||||
it("should create span before trace", async () => {
|
||||
await pruneDatabase();
|
||||
|
||||
const traceId = uuidv4();
|
||||
const spanId = uuidv4();
|
||||
|
||||
const createSpan = await makeAPICall("POST", "/api/public/spans", {
|
||||
id: spanId,
|
||||
traceId: traceId,
|
||||
name: "span-name",
|
||||
startTime: "2021-01-01T00:00:00.000Z",
|
||||
endTime: "2021-01-01T00:00:00.000Z",
|
||||
input: { input: "value" },
|
||||
metadata: { meta: "value" },
|
||||
version: "2.0.0",
|
||||
});
|
||||
const createSpan = await makeZodVerifiedAPICall(
|
||||
PostSpansV1Response,
|
||||
"POST",
|
||||
"/api/public/spans",
|
||||
{
|
||||
id: spanId,
|
||||
traceId: traceId,
|
||||
name: "span-name",
|
||||
startTime: "2021-01-01T00:00:00.000Z",
|
||||
endTime: "2021-01-01T00:00:00.000Z",
|
||||
input: { input: "value" },
|
||||
metadata: { meta: "value" },
|
||||
version: "2.0.0",
|
||||
},
|
||||
);
|
||||
|
||||
expect(createSpan.status).toBe(200);
|
||||
const dbSpan = await prisma.observation.findUnique({
|
||||
@@ -94,15 +114,20 @@ describe("/api/public/spans API Endpoint", () => {
|
||||
expect(dbSpan?.metadata).toEqual({ meta: "value" });
|
||||
expect(dbSpan?.version).toBe("2.0.0");
|
||||
|
||||
await makeAPICall("POST", "/api/public/traces", {
|
||||
id: traceId,
|
||||
name: "trace-name",
|
||||
userId: "user-1",
|
||||
projectId: "7a88fb47-b4e2-43b8-a06c-a5ce950dc53a",
|
||||
metadata: { key: "value" },
|
||||
release: "1.0.0",
|
||||
version: "2.0.0",
|
||||
});
|
||||
await makeZodVerifiedAPICall(
|
||||
PostTracesV1Response,
|
||||
"POST",
|
||||
"/api/public/traces",
|
||||
{
|
||||
id: traceId,
|
||||
name: "trace-name",
|
||||
userId: "user-1",
|
||||
projectId: "7a88fb47-b4e2-43b8-a06c-a5ce950dc53a",
|
||||
metadata: { key: "value" },
|
||||
release: "1.0.0",
|
||||
version: "2.0.0",
|
||||
},
|
||||
);
|
||||
|
||||
const dbTrace = await prisma.trace.findMany({
|
||||
where: {
|
||||
@@ -118,21 +143,31 @@ describe("/api/public/spans API Endpoint", () => {
|
||||
const traceId = uuidv4();
|
||||
const spanId = uuidv4();
|
||||
|
||||
const response = await makeAPICall("POST", "/api/public/traces", {
|
||||
externalId: uuidv4(),
|
||||
id: traceId,
|
||||
name: "trace-name",
|
||||
userId: "user-1",
|
||||
projectId: "7a88fb47-b4e2-43b8-a06c-a5ce950dc53a",
|
||||
});
|
||||
const response = await makeZodVerifiedAPICall(
|
||||
PostTracesV1Response,
|
||||
"POST",
|
||||
"/api/public/traces",
|
||||
{
|
||||
externalId: uuidv4(),
|
||||
id: traceId,
|
||||
name: "trace-name",
|
||||
userId: "user-1",
|
||||
projectId: "7a88fb47-b4e2-43b8-a06c-a5ce950dc53a",
|
||||
},
|
||||
);
|
||||
expect(response.status).toBe(200);
|
||||
const createSpan = await makeAPICall("POST", "/api/public/spans", {
|
||||
id: spanId,
|
||||
name: "spanName",
|
||||
projectId: "7a88fb47-b4e2-43b8-a06c-a5ce950dc53a",
|
||||
traceId: traceId,
|
||||
externalTraceIdType: "EXTERNAL",
|
||||
});
|
||||
const createSpan = await makeZodVerifiedAPICall(
|
||||
PostSpansV1Response,
|
||||
"POST",
|
||||
"/api/public/spans",
|
||||
{
|
||||
id: spanId,
|
||||
name: "spanName",
|
||||
projectId: "7a88fb47-b4e2-43b8-a06c-a5ce950dc53a",
|
||||
traceId: traceId,
|
||||
externalTraceIdType: "EXTERNAL",
|
||||
},
|
||||
);
|
||||
expect(createSpan.status).toBe(200);
|
||||
|
||||
const dbSpan = await prisma.observation.findFirstOrThrow({
|
||||
@@ -148,15 +183,20 @@ describe("/api/public/spans API Endpoint", () => {
|
||||
const spanName = uuidv4();
|
||||
|
||||
const spanId = uuidv4();
|
||||
const createSpan = await makeAPICall("POST", "/api/public/spans", {
|
||||
id: spanId,
|
||||
name: spanName,
|
||||
startTime: "2021-01-01T00:00:00.000Z",
|
||||
endTime: "2021-01-01T00:00:00.000Z",
|
||||
input: { input: "value" },
|
||||
metadata: { meta: "value" },
|
||||
version: "2.0.0",
|
||||
});
|
||||
const createSpan = await makeZodVerifiedAPICall(
|
||||
PostSpansV1Response,
|
||||
"POST",
|
||||
"/api/public/spans",
|
||||
{
|
||||
id: spanId,
|
||||
name: spanName,
|
||||
startTime: "2021-01-01T00:00:00.000Z",
|
||||
endTime: "2021-01-01T00:00:00.000Z",
|
||||
input: { input: "value" },
|
||||
metadata: { meta: "value" },
|
||||
version: "2.0.0",
|
||||
},
|
||||
);
|
||||
|
||||
const dbTrace = await prisma.trace.findMany({
|
||||
where: {
|
||||
@@ -189,17 +229,22 @@ describe("/api/public/spans API Endpoint", () => {
|
||||
|
||||
const spanId = uuidv4();
|
||||
const traceId = uuidv4();
|
||||
const createSpan = await makeAPICall("POST", "/api/public/spans", {
|
||||
id: spanId,
|
||||
traceIdType: "EXTERNAL",
|
||||
traceId: traceId,
|
||||
name: spanName,
|
||||
startTime: "2021-01-01T00:00:00.000Z",
|
||||
endTime: "2021-01-01T00:00:00.000Z",
|
||||
input: { input: "value" },
|
||||
metadata: { meta: "value" },
|
||||
version: "2.0.0",
|
||||
});
|
||||
const createSpan = await makeZodVerifiedAPICall(
|
||||
PostSpansV1Response,
|
||||
"POST",
|
||||
"/api/public/spans",
|
||||
{
|
||||
id: spanId,
|
||||
traceIdType: "EXTERNAL",
|
||||
traceId: traceId,
|
||||
name: spanName,
|
||||
startTime: "2021-01-01T00:00:00.000Z",
|
||||
endTime: "2021-01-01T00:00:00.000Z",
|
||||
input: { input: "value" },
|
||||
metadata: { meta: "value" },
|
||||
version: "2.0.0",
|
||||
},
|
||||
);
|
||||
|
||||
expect(createSpan.status).toBe(200);
|
||||
|
||||
@@ -221,15 +266,20 @@ describe("/api/public/spans API Endpoint", () => {
|
||||
const generationName = uuidv4();
|
||||
|
||||
const spanId = uuidv4();
|
||||
const createSpan = await makeAPICall("POST", "/api/public/spans", {
|
||||
id: spanId,
|
||||
name: generationName,
|
||||
startTime: "2021-01-01T00:00:00.000Z",
|
||||
endTime: "2021-01-01T00:00:00.000Z",
|
||||
input: { key: "value" },
|
||||
metadata: { key: "value" },
|
||||
version: "2.0.0",
|
||||
});
|
||||
const createSpan = await makeZodVerifiedAPICall(
|
||||
PostSpansV1Response,
|
||||
"POST",
|
||||
"/api/public/spans",
|
||||
{
|
||||
id: spanId,
|
||||
name: generationName,
|
||||
startTime: "2021-01-01T00:00:00.000Z",
|
||||
endTime: "2021-01-01T00:00:00.000Z",
|
||||
input: { key: "value" },
|
||||
metadata: { key: "value" },
|
||||
version: "2.0.0",
|
||||
},
|
||||
);
|
||||
|
||||
const dbSpan = await prisma.observation.findFirstOrThrow({
|
||||
where: {
|
||||
@@ -262,22 +312,32 @@ describe("/api/public/spans API Endpoint", () => {
|
||||
const spanName = uuidv4();
|
||||
|
||||
const spanId = uuidv4();
|
||||
const createSpan = await makeAPICall("POST", "/api/public/spans", {
|
||||
id: spanId,
|
||||
name: spanName,
|
||||
startTime: "2021-01-01T00:00:00.000Z",
|
||||
endTime: "2021-01-01T00:00:00.000Z",
|
||||
input: { input: "value" },
|
||||
metadata: { meta: "value" },
|
||||
version: "2.0.0",
|
||||
});
|
||||
const createSpan = await makeZodVerifiedAPICall(
|
||||
PostSpansV1Response,
|
||||
"POST",
|
||||
"/api/public/spans",
|
||||
{
|
||||
id: spanId,
|
||||
name: spanName,
|
||||
startTime: "2021-01-01T00:00:00.000Z",
|
||||
endTime: "2021-01-01T00:00:00.000Z",
|
||||
input: { input: "value" },
|
||||
metadata: { meta: "value" },
|
||||
version: "2.0.0",
|
||||
},
|
||||
);
|
||||
|
||||
expect(createSpan.status).toBe(200);
|
||||
|
||||
const updatedSpan = await makeAPICall("PATCH", "/api/public/spans", {
|
||||
spanId: spanId,
|
||||
output: { key: "this is a great gpt output" },
|
||||
});
|
||||
const updatedSpan = await makeZodVerifiedAPICall(
|
||||
PatchSpansV1Response,
|
||||
"PATCH",
|
||||
"/api/public/spans",
|
||||
{
|
||||
spanId: spanId,
|
||||
output: { key: "this is a great gpt output" },
|
||||
},
|
||||
);
|
||||
expect(updatedSpan.status).toBe(200);
|
||||
|
||||
const dbSpan = await prisma.observation.findUnique({
|
||||
|
||||
@@ -3,7 +3,7 @@ import { hash } from "bcryptjs";
|
||||
import { env } from "@/src/env.mjs";
|
||||
import { getDisplaySecretKey, hashSecretKey } from "@langfuse/shared";
|
||||
import { prisma } from "@langfuse/shared/src/db";
|
||||
import { type z, ZodObject } from "zod";
|
||||
import { type z } from "zod";
|
||||
|
||||
export const pruneDatabase = async () => {
|
||||
if (!env.DATABASE_URL.includes("localhost:5432")) {
|
||||
@@ -14,6 +14,7 @@ export const pruneDatabase = async () => {
|
||||
await prisma.scoreConfig.deleteMany();
|
||||
await prisma.observation.deleteMany();
|
||||
await prisma.trace.deleteMany();
|
||||
await prisma.traceSession.deleteMany();
|
||||
await prisma.datasetItem.deleteMany();
|
||||
await prisma.dataset.deleteMany();
|
||||
await prisma.datasetRuns.deleteMany();
|
||||
@@ -87,16 +88,11 @@ export async function makeZodVerifiedAPICall<T extends z.ZodTypeAny>(
|
||||
`API call did not return 200, returned status ${status}, body ${JSON.stringify(resBody)}`,
|
||||
);
|
||||
}
|
||||
try {
|
||||
if (responseZodSchema instanceof ZodObject) {
|
||||
responseZodSchema.strict().parse(resBody);
|
||||
} else {
|
||||
responseZodSchema.parse(resBody);
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
const typeCheckResult = responseZodSchema.safeParse(resBody);
|
||||
if (!typeCheckResult.success) {
|
||||
console.error(typeCheckResult.error);
|
||||
throw new Error(
|
||||
`API call (${method} ${url}) did not return valid response, returned status ${status}, body ${JSON.stringify(resBody)}, error ${e}`,
|
||||
`API call (${method} ${url}) did not return valid response, returned status ${status}, body ${JSON.stringify(resBody)}, error ${typeCheckResult.error}`,
|
||||
);
|
||||
}
|
||||
return { body: resBody, status };
|
||||
|
||||
@@ -126,30 +126,119 @@ describe("Token Cost Calculation", () => {
|
||||
|
||||
it("should correctly calculate token costs when only some user provided costs are given", async () => {
|
||||
const model = {
|
||||
inputPrice: new Decimal(0.01),
|
||||
outputPrice: new Decimal(0.02),
|
||||
totalPrice: new Decimal(0.03),
|
||||
inputPrice: new Decimal(1),
|
||||
outputPrice: new Decimal(1),
|
||||
};
|
||||
|
||||
const tokenCounts = {
|
||||
input: 100,
|
||||
output: 200,
|
||||
total: undefined,
|
||||
};
|
||||
const data = [
|
||||
// missing total
|
||||
{
|
||||
userProvidedCosts: {
|
||||
input: 1,
|
||||
output: 2,
|
||||
total: undefined,
|
||||
},
|
||||
expectedCost: {
|
||||
input: 1,
|
||||
output: 2,
|
||||
total: 3,
|
||||
},
|
||||
},
|
||||
// only total
|
||||
{
|
||||
userProvidedCosts: {
|
||||
input: undefined,
|
||||
output: undefined,
|
||||
total: 2,
|
||||
},
|
||||
expectedCost: {
|
||||
input: undefined,
|
||||
output: undefined,
|
||||
total: 2,
|
||||
},
|
||||
},
|
||||
// missing input
|
||||
{
|
||||
userProvidedCosts: {
|
||||
input: undefined,
|
||||
output: 2,
|
||||
total: 2,
|
||||
},
|
||||
expectedCost: {
|
||||
input: undefined,
|
||||
output: 2,
|
||||
total: 2,
|
||||
},
|
||||
},
|
||||
// only input
|
||||
{
|
||||
userProvidedCosts: {
|
||||
input: 1,
|
||||
output: undefined,
|
||||
total: undefined,
|
||||
},
|
||||
expectedCost: {
|
||||
input: 1,
|
||||
output: undefined,
|
||||
total: 1,
|
||||
},
|
||||
},
|
||||
|
||||
const userProvidedCosts = {
|
||||
outputCost: new Decimal(3.0),
|
||||
};
|
||||
// missing output
|
||||
{
|
||||
userProvidedCosts: {
|
||||
input: 1,
|
||||
output: undefined,
|
||||
total: 1,
|
||||
},
|
||||
expectedCost: {
|
||||
input: 1,
|
||||
output: undefined,
|
||||
total: 1,
|
||||
},
|
||||
},
|
||||
|
||||
const costs = ObservationProcessor.calculateTokenCosts(
|
||||
model as any,
|
||||
userProvidedCosts,
|
||||
tokenCounts,
|
||||
);
|
||||
// only output
|
||||
{
|
||||
userProvidedCosts: {
|
||||
input: undefined,
|
||||
output: 2,
|
||||
total: undefined,
|
||||
},
|
||||
expectedCost: {
|
||||
input: undefined,
|
||||
output: 2,
|
||||
total: 2,
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
expect(costs.inputCost).toBe(undefined); // No user provided cost
|
||||
expect(costs.outputCost?.toNumber()).toBe(3.0); // Overridden by user provided cost
|
||||
expect(costs.totalCost).toBe(undefined); // No user provided cost
|
||||
for (const { userProvidedCosts, expectedCost } of data) {
|
||||
const tokenCounts = {
|
||||
input: 0,
|
||||
output: 0,
|
||||
total: 0,
|
||||
};
|
||||
|
||||
const userProvidedCostsDecimal = {
|
||||
inputCost:
|
||||
userProvidedCosts.input && new Decimal(userProvidedCosts.input),
|
||||
outputCost:
|
||||
userProvidedCosts.output && new Decimal(userProvidedCosts.output),
|
||||
totalCost:
|
||||
userProvidedCosts.total && new Decimal(userProvidedCosts.total),
|
||||
};
|
||||
|
||||
const costs = ObservationProcessor.calculateTokenCosts(
|
||||
model as any,
|
||||
userProvidedCostsDecimal as any,
|
||||
tokenCounts,
|
||||
);
|
||||
|
||||
expect(costs.inputCost?.toNumber()).toBe(expectedCost.input);
|
||||
expect(costs.outputCost?.toNumber()).toBe(expectedCost.output);
|
||||
expect(costs.totalCost?.toNumber()).toBe(expectedCost.total);
|
||||
}
|
||||
});
|
||||
|
||||
it("should return empty costs if no model is provided", async () => {
|
||||
@@ -911,7 +1000,7 @@ describe("Token Cost Calculation", () => {
|
||||
expect(generation?.calculatedOutputCost?.toNumber()).toBe(
|
||||
generationUsage2.usage.outputCost,
|
||||
);
|
||||
expect(generation?.calculatedTotalCost?.toNumber()).toBe(undefined);
|
||||
expect(generation?.calculatedTotalCost?.toNumber()).toBe(1);
|
||||
expect(generation?.promptTokens).toBe(generationUsage1.usage.input);
|
||||
expect(generation?.completionTokens).toBe(generationUsage1.usage.output);
|
||||
expect(generation?.totalTokens).toBe(generationUsage1.usage.total);
|
||||
|
||||
@@ -1,58 +1,70 @@
|
||||
/** @jest-environment node */
|
||||
|
||||
import { makeAPICall, pruneDatabase } from "@/src/__tests__/test-utils";
|
||||
import {
|
||||
makeZodVerifiedAPICall,
|
||||
pruneDatabase,
|
||||
} from "@/src/__tests__/test-utils";
|
||||
import { PostGenerationsV1Response } from "@/src/features/public-api/types/generations";
|
||||
import { PostScoresResponse } from "@/src/features/public-api/types/scores";
|
||||
import {
|
||||
GetTraceV1Response,
|
||||
GetTracesV1Response,
|
||||
PostTracesV1Response,
|
||||
} from "@/src/features/public-api/types/traces";
|
||||
import { prisma } from "@langfuse/shared/src/db";
|
||||
import { v4 as uuidv4 } from "uuid";
|
||||
|
||||
interface GetTracesAPIResponse {
|
||||
data: Array<{
|
||||
id: string;
|
||||
[key: string]: unknown;
|
||||
}>;
|
||||
}
|
||||
|
||||
describe("/api/public/traces API Endpoint", () => {
|
||||
beforeEach(async () => await pruneDatabase());
|
||||
afterEach(async () => await pruneDatabase());
|
||||
|
||||
it("should create", async () => {
|
||||
it("should create and get a trace via /traces", async () => {
|
||||
await pruneDatabase();
|
||||
|
||||
await makeAPICall("POST", "/api/public/traces", {
|
||||
name: "trace-name",
|
||||
userId: "user-1",
|
||||
projectId: "7a88fb47-b4e2-43b8-a06c-a5ce950dc53a",
|
||||
metadata: { key: "value" },
|
||||
release: "1.0.0",
|
||||
version: "2.0.0",
|
||||
});
|
||||
|
||||
const dbTrace = await prisma.trace.findMany({
|
||||
where: {
|
||||
const traceCreate = await makeZodVerifiedAPICall(
|
||||
PostTracesV1Response,
|
||||
"POST",
|
||||
"/api/public/traces",
|
||||
{
|
||||
name: "trace-name",
|
||||
userId: "user-1",
|
||||
projectId: "7a88fb47-b4e2-43b8-a06c-a5ce950dc53a",
|
||||
metadata: { key: "value" },
|
||||
release: "1.0.0",
|
||||
version: "2.0.0",
|
||||
},
|
||||
});
|
||||
);
|
||||
|
||||
expect(dbTrace.length).toBeGreaterThan(0);
|
||||
expect(dbTrace[0]?.name).toBe("trace-name");
|
||||
expect(dbTrace[0]?.release).toBe("1.0.0");
|
||||
expect(dbTrace[0]?.externalId).toBeNull();
|
||||
expect(dbTrace[0]?.version).toBe("2.0.0");
|
||||
expect(dbTrace[0]?.projectId).toBe("7a88fb47-b4e2-43b8-a06c-a5ce950dc53a");
|
||||
const trace = await makeZodVerifiedAPICall(
|
||||
GetTraceV1Response,
|
||||
"GET",
|
||||
"/api/public/traces/" + traceCreate.body.id,
|
||||
);
|
||||
|
||||
expect(trace.body.name).toBe("trace-name");
|
||||
expect(trace.body.release).toBe("1.0.0");
|
||||
expect(trace.body.externalId).toBeNull();
|
||||
expect(trace.body.version).toBe("2.0.0");
|
||||
expect(trace.body.projectId).toBe("7a88fb47-b4e2-43b8-a06c-a5ce950dc53a");
|
||||
});
|
||||
|
||||
it("should upsert second trace", async () => {
|
||||
await pruneDatabase();
|
||||
|
||||
await makeAPICall("POST", "/api/public/traces", {
|
||||
id: "trace-id",
|
||||
name: "trace-name",
|
||||
userId: "user-1",
|
||||
metadata: { key: "value" },
|
||||
release: "1.0.0",
|
||||
version: "2.0.0",
|
||||
public: true,
|
||||
});
|
||||
await makeZodVerifiedAPICall(
|
||||
PostTracesV1Response,
|
||||
"POST",
|
||||
"/api/public/traces",
|
||||
{
|
||||
id: "trace-id",
|
||||
name: "trace-name",
|
||||
userId: "user-1",
|
||||
metadata: { key: "value" },
|
||||
release: "1.0.0",
|
||||
version: "2.0.0",
|
||||
public: true,
|
||||
},
|
||||
);
|
||||
|
||||
const dbTrace1 = await prisma.trace.findFirst({
|
||||
where: {
|
||||
@@ -70,14 +82,19 @@ describe("/api/public/traces API Endpoint", () => {
|
||||
userId: "user-1",
|
||||
});
|
||||
|
||||
await makeAPICall("POST", "/api/public/traces", {
|
||||
id: "trace-id",
|
||||
metadata: { key: "value" },
|
||||
timestamp: "2021-01-01T00:00:00.000Z",
|
||||
release: "1.0.0",
|
||||
version: "5.0.0",
|
||||
public: false,
|
||||
});
|
||||
await makeZodVerifiedAPICall(
|
||||
PostTracesV1Response,
|
||||
"POST",
|
||||
"/api/public/traces",
|
||||
{
|
||||
id: "trace-id",
|
||||
metadata: { key: "value" },
|
||||
timestamp: "2021-01-01T00:00:00.000Z",
|
||||
release: "1.0.0",
|
||||
version: "5.0.0",
|
||||
public: false,
|
||||
},
|
||||
);
|
||||
|
||||
const dbTrace2 = await prisma.trace.findFirst({
|
||||
where: {
|
||||
@@ -100,23 +117,39 @@ describe("/api/public/traces API Endpoint", () => {
|
||||
it("should use tags correctly on POST and GET", async () => {
|
||||
await pruneDatabase();
|
||||
|
||||
await makeAPICall("POST", "/api/public/traces", {
|
||||
id: "trace-1",
|
||||
tags: ["tag-1", "tag-2", "tag-3"],
|
||||
});
|
||||
await makeZodVerifiedAPICall(
|
||||
PostTracesV1Response,
|
||||
"POST",
|
||||
"/api/public/traces",
|
||||
{
|
||||
id: "trace-1",
|
||||
tags: ["tag-1", "tag-2", "tag-3"],
|
||||
},
|
||||
);
|
||||
|
||||
await makeAPICall("POST", "/api/public/traces", {
|
||||
id: "trace-2",
|
||||
tags: ["tag-1"],
|
||||
});
|
||||
await makeZodVerifiedAPICall(
|
||||
PostTracesV1Response,
|
||||
"POST",
|
||||
"/api/public/traces",
|
||||
{
|
||||
id: "trace-2",
|
||||
tags: ["tag-1"],
|
||||
},
|
||||
);
|
||||
|
||||
await makeAPICall("POST", "/api/public/traces", {
|
||||
id: "trace-3",
|
||||
tags: ["tag-2", "tag-3"],
|
||||
});
|
||||
await makeZodVerifiedAPICall(
|
||||
PostTracesV1Response,
|
||||
"POST",
|
||||
"/api/public/traces",
|
||||
{
|
||||
id: "trace-3",
|
||||
tags: ["tag-2", "tag-3"],
|
||||
},
|
||||
);
|
||||
|
||||
// multiple tags
|
||||
const traces = await makeAPICall<GetTracesAPIResponse>(
|
||||
const traces = await makeZodVerifiedAPICall(
|
||||
GetTracesV1Response,
|
||||
"GET",
|
||||
"/api/public/traces?tags=tag-2&tags=tag-3",
|
||||
);
|
||||
@@ -125,7 +158,8 @@ describe("/api/public/traces API Endpoint", () => {
|
||||
expect(traceIds).toEqual(["trace-3", "trace-1"]);
|
||||
|
||||
// single tag
|
||||
const traces2 = await makeAPICall<GetTracesAPIResponse>(
|
||||
const traces2 = await makeZodVerifiedAPICall(
|
||||
GetTracesV1Response,
|
||||
"GET",
|
||||
"/api/public/traces?tags=tag-1",
|
||||
);
|
||||
@@ -134,7 +168,8 @@ describe("/api/public/traces API Endpoint", () => {
|
||||
expect(traceIds2).toEqual(["trace-2", "trace-1"]);
|
||||
|
||||
// wrong tag
|
||||
const traces3 = await makeAPICall<GetTracesAPIResponse>(
|
||||
const traces3 = await makeZodVerifiedAPICall(
|
||||
GetTracesV1Response,
|
||||
"GET",
|
||||
"/api/public/traces?tags=tag-10",
|
||||
);
|
||||
@@ -143,7 +178,8 @@ describe("/api/public/traces API Endpoint", () => {
|
||||
expect(traceIds3).toEqual([]);
|
||||
|
||||
// no tag
|
||||
const traces4 = await makeAPICall<GetTracesAPIResponse>(
|
||||
const traces4 = await makeZodVerifiedAPICall(
|
||||
GetTracesV1Response,
|
||||
"GET",
|
||||
"/api/public/traces?tags=",
|
||||
);
|
||||
@@ -157,60 +193,94 @@ describe("/api/public/traces API Endpoint", () => {
|
||||
|
||||
// Create a trace with some observations that have costs and latencies
|
||||
const traceId = uuidv4();
|
||||
await makeAPICall("POST", "/api/public/traces", {
|
||||
id: traceId,
|
||||
name: "trace-with-costs",
|
||||
userId: "user-costs",
|
||||
projectId: "project-costs",
|
||||
metadata: { key: "value" },
|
||||
release: "1.0.0",
|
||||
version: "2.0.0",
|
||||
});
|
||||
await makeZodVerifiedAPICall(
|
||||
PostTracesV1Response,
|
||||
"POST",
|
||||
"/api/public/traces",
|
||||
{
|
||||
id: traceId,
|
||||
name: "trace-with-costs",
|
||||
userId: "user-costs",
|
||||
projectId: "project-costs",
|
||||
metadata: { key: "value" },
|
||||
release: "1.0.0",
|
||||
version: "2.0.0",
|
||||
},
|
||||
);
|
||||
console.log(traceId);
|
||||
|
||||
// Simulate observations with costs and latencies
|
||||
await makeAPICall("POST", "/api/public/generations", {
|
||||
traceId: traceId,
|
||||
usage: { totalCost: 10.5 },
|
||||
startTime: "2021-01-01T00:00:00.000Z",
|
||||
endTime: "2021-01-01T00:10:00.000Z",
|
||||
});
|
||||
await makeAPICall("POST", "/api/public/generations", {
|
||||
traceId: traceId,
|
||||
usage: { totalCost: 5.25 },
|
||||
startTime: "2021-01-01T00:10:00.000Z",
|
||||
endTime: "2021-01-01T00:20:00.000Z",
|
||||
});
|
||||
const generationId = uuidv4();
|
||||
await makeZodVerifiedAPICall(
|
||||
PostGenerationsV1Response,
|
||||
"POST",
|
||||
"/api/public/generations",
|
||||
{
|
||||
traceId: traceId,
|
||||
id: generationId,
|
||||
name: "Generation1",
|
||||
usage: { totalCost: 10.5 },
|
||||
startTime: "2021-01-01T00:00:00.000Z",
|
||||
endTime: "2021-01-01T00:10:00.000Z",
|
||||
},
|
||||
);
|
||||
await makeZodVerifiedAPICall(
|
||||
PostGenerationsV1Response,
|
||||
"POST",
|
||||
"/api/public/generations",
|
||||
{
|
||||
traceId: traceId,
|
||||
usage: { totalCost: 5.25 },
|
||||
startTime: "2021-01-01T00:10:00.000Z",
|
||||
endTime: "2021-01-01T00:20:00.000Z",
|
||||
},
|
||||
);
|
||||
|
||||
// Simulate scores on the trace
|
||||
const scoreId1 = uuidv4();
|
||||
await makeAPICall("POST", "/api/public/scores", {
|
||||
id: scoreId1,
|
||||
name: "score-1",
|
||||
value: 75.0,
|
||||
traceId: traceId,
|
||||
comment: "First score",
|
||||
});
|
||||
await makeZodVerifiedAPICall(
|
||||
PostScoresResponse,
|
||||
"POST",
|
||||
"/api/public/scores",
|
||||
{
|
||||
id: scoreId1,
|
||||
name: "score-1",
|
||||
value: 75.0,
|
||||
traceId: traceId,
|
||||
comment: "First score",
|
||||
},
|
||||
);
|
||||
const scoreId2 = uuidv4();
|
||||
await makeAPICall("POST", "/api/public/scores", {
|
||||
id: scoreId2,
|
||||
name: "score-2",
|
||||
value: 85.5,
|
||||
traceId: traceId,
|
||||
comment: "Second score",
|
||||
});
|
||||
await makeZodVerifiedAPICall(
|
||||
PostScoresResponse,
|
||||
"POST",
|
||||
"/api/public/scores",
|
||||
{
|
||||
id: scoreId2,
|
||||
name: "score-2",
|
||||
value: 85.5,
|
||||
traceId: traceId,
|
||||
comment: "Second score",
|
||||
},
|
||||
);
|
||||
const scoreId3 = uuidv4();
|
||||
await makeAPICall("POST", "/api/public/scores", {
|
||||
id: scoreId3,
|
||||
name: "score-3",
|
||||
value: 95.0,
|
||||
traceId: traceId,
|
||||
comment: "Third score",
|
||||
});
|
||||
await makeZodVerifiedAPICall(
|
||||
PostScoresResponse,
|
||||
"POST",
|
||||
"/api/public/scores",
|
||||
{
|
||||
id: scoreId3,
|
||||
name: "score-3",
|
||||
value: 95.0,
|
||||
traceId: traceId,
|
||||
comment: "Third score",
|
||||
},
|
||||
);
|
||||
|
||||
// GET traces
|
||||
// Retrieve the trace with totalCost and latency
|
||||
const traces = await makeAPICall<GetTracesAPIResponse>(
|
||||
const traces = await makeZodVerifiedAPICall(
|
||||
GetTracesV1Response,
|
||||
"GET",
|
||||
`/api/public/traces`,
|
||||
);
|
||||
@@ -226,17 +296,23 @@ describe("/api/public/traces API Endpoint", () => {
|
||||
|
||||
// GET trace
|
||||
// Retrieve the trace with total
|
||||
const trace = await makeAPICall<{
|
||||
id: string;
|
||||
totalCost: number;
|
||||
htmlPath: string;
|
||||
}>("GET", `/api/public/traces/${traceId}`);
|
||||
const trace = await makeZodVerifiedAPICall(
|
||||
GetTraceV1Response,
|
||||
"GET",
|
||||
`/api/public/traces/${traceId}`,
|
||||
);
|
||||
console.log(trace.body);
|
||||
expect(trace.body.totalCost).toBeCloseTo(15.75);
|
||||
expect(trace.body.id).toBe(traceId);
|
||||
expect(trace.body.id).toBe(traceId);
|
||||
expect(trace.body.htmlPath).toContain(`/traces/${traceId}`);
|
||||
expect(trace.body.htmlPath).toContain(`/project/`); // do not know the projectId
|
||||
expect(trace.body.scores).toHaveLength(3);
|
||||
expect(trace.body.scores[0].id).toBe(scoreId3);
|
||||
expect(trace.body.scores[0].name).toBe("score-3");
|
||||
expect(trace.body.observations).toHaveLength(2);
|
||||
expect(trace.body.observations[0].id).toBe(generationId);
|
||||
expect(trace.body.observations[0].name).toBe("Generation1");
|
||||
});
|
||||
|
||||
it("should filter traces by session ID", async () => {
|
||||
@@ -244,30 +320,41 @@ describe("/api/public/traces API Endpoint", () => {
|
||||
const anotherSessionId = "another-session-id";
|
||||
|
||||
// Create traces with different session IDs
|
||||
await makeAPICall("POST", "/api/public/traces", {
|
||||
id: "trace-1",
|
||||
name: "test-trace-1",
|
||||
sessionId,
|
||||
userId: "user-1",
|
||||
projectId: "project-1",
|
||||
metadata: { key: "value" },
|
||||
release: "1.0.0",
|
||||
version: "1.0.0",
|
||||
});
|
||||
await makeZodVerifiedAPICall(
|
||||
PostTracesV1Response,
|
||||
"POST",
|
||||
"/api/public/traces",
|
||||
{
|
||||
id: "trace-1",
|
||||
name: "test-trace-1",
|
||||
sessionId,
|
||||
userId: "user-1",
|
||||
projectId: "project-1",
|
||||
metadata: { key: "value" },
|
||||
release: "1.0.0",
|
||||
version: "1.0.0",
|
||||
},
|
||||
);
|
||||
|
||||
await makeAPICall("POST", "/api/public/traces", {
|
||||
id: "trace-2",
|
||||
name: "test-trace-2",
|
||||
sessionId: anotherSessionId,
|
||||
userId: "user-2",
|
||||
projectId: "project-1",
|
||||
metadata: { key: "value" },
|
||||
release: "1.0.0",
|
||||
version: "1.0.0",
|
||||
});
|
||||
await makeZodVerifiedAPICall(
|
||||
PostTracesV1Response,
|
||||
"POST",
|
||||
"/api/public/traces",
|
||||
{
|
||||
id: "trace-2",
|
||||
name: "test-trace-2",
|
||||
sessionId: anotherSessionId,
|
||||
userId: "user-2",
|
||||
projectId: "project-1",
|
||||
metadata: { key: "value" },
|
||||
release: "1.0.0",
|
||||
version: "1.0.0",
|
||||
},
|
||||
);
|
||||
|
||||
// Filter by session ID
|
||||
const tracesBySessionId = await makeAPICall<GetTracesAPIResponse>(
|
||||
const tracesBySessionId = await makeZodVerifiedAPICall(
|
||||
GetTracesV1Response,
|
||||
"GET",
|
||||
`/api/public/traces?sessionId=${sessionId}`,
|
||||
);
|
||||
@@ -277,7 +364,8 @@ describe("/api/public/traces API Endpoint", () => {
|
||||
expect(tracesBySessionId.body.data[0].id).toBe("trace-1");
|
||||
|
||||
// Filter by another session ID
|
||||
const tracesByAnotherSessionId = await makeAPICall<GetTracesAPIResponse>(
|
||||
const tracesByAnotherSessionId = await makeZodVerifiedAPICall(
|
||||
GetTracesV1Response,
|
||||
"GET",
|
||||
`/api/public/traces?sessionId=${anotherSessionId}`,
|
||||
);
|
||||
@@ -287,11 +375,11 @@ describe("/api/public/traces API Endpoint", () => {
|
||||
expect(tracesByAnotherSessionId.body.data[0].id).toBe("trace-2");
|
||||
|
||||
// Filter by non-existent session ID
|
||||
const tracesByNonExistentSessionId =
|
||||
await makeAPICall<GetTracesAPIResponse>(
|
||||
"GET",
|
||||
`/api/public/traces?sessionId=non-existent-session-id`,
|
||||
);
|
||||
const tracesByNonExistentSessionId = await makeZodVerifiedAPICall(
|
||||
GetTracesV1Response,
|
||||
"GET",
|
||||
`/api/public/traces?sessionId=non-existent-session-id`,
|
||||
);
|
||||
|
||||
expect(tracesByNonExistentSessionId.status).toBe(200);
|
||||
expect(tracesByNonExistentSessionId.body.data).toHaveLength(0);
|
||||
|
||||
@@ -15,6 +15,7 @@ import {
|
||||
import React from "react";
|
||||
import { api } from "@/src/utils/api";
|
||||
import { showSuccessToast } from "@/src/features/notifications/showSuccessToast";
|
||||
import { useHasAccess } from "@/src/features/rbac/utils/checkAccess";
|
||||
import { useIsEeEnabled } from "@/src/ee/utils/useIsEeEnabled";
|
||||
|
||||
export type BatchExportTableButtonProps = {
|
||||
@@ -31,6 +32,10 @@ export const BatchExportTableButton: React.FC<BatchExportTableButtonProps> = (
|
||||
const [isExporting, setIsExporting] = React.useState(false);
|
||||
const createExport = api.batchExport.create.useMutation();
|
||||
const isEeEnabled = useIsEeEnabled();
|
||||
const hasAccess = useHasAccess({
|
||||
projectId: props.projectId,
|
||||
scope: "batchExport:create",
|
||||
});
|
||||
|
||||
const handleExport = async (format: BatchExportFileFormat) => {
|
||||
setIsExporting(true);
|
||||
@@ -51,7 +56,7 @@ export const BatchExportTableButton: React.FC<BatchExportTableButtonProps> = (
|
||||
});
|
||||
};
|
||||
|
||||
if (!isEeEnabled) return null;
|
||||
if (!isEeEnabled || !hasAccess) return null;
|
||||
|
||||
return (
|
||||
<DropdownMenu>
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
import { z } from "zod";
|
||||
|
||||
export const ChatMlMessageSchema = z
|
||||
.object({
|
||||
role: z.string().optional(),
|
||||
name: z.string().optional(),
|
||||
content: z
|
||||
.union([z.record(z.any()), z.string(), z.array(z.any())])
|
||||
.nullish(),
|
||||
additional_kwargs: z.record(z.any()).optional(),
|
||||
})
|
||||
.passthrough()
|
||||
.refine((value) => value.content !== null || value.role !== undefined)
|
||||
.transform(({ additional_kwargs, ...other }) => ({
|
||||
...other,
|
||||
...additional_kwargs,
|
||||
}))
|
||||
.transform(({ role, name, content, ...other }) => ({
|
||||
role,
|
||||
name,
|
||||
content,
|
||||
json: Object.keys(other).length === 0 ? undefined : other,
|
||||
}));
|
||||
|
||||
export const ChatMlArraySchema = z.array(ChatMlMessageSchema).min(1);
|
||||
@@ -0,0 +1,22 @@
|
||||
import { z } from "zod";
|
||||
|
||||
const MARKDOWN_PATTERNS = [
|
||||
"(\\*\\*?|__?)(.*?)\\1", // Matches bold (** or __) and italic (* or _) with proper escaping
|
||||
"`{3}[\\s\\S]*?`{3}", // Matches fenced code blocks with triple backticks
|
||||
"`[\\s\\S]*?`", // Matches inline code with single backticks
|
||||
"(^|\\s)[-+*]\\s", // Matches unordered lists that start with -, +, or *
|
||||
"^\\s*#{1,6}\\s", // Matches headers that start with # to ######
|
||||
"^>\\s+", // Matches blockquotes starting with >
|
||||
"^\\d+\\.\\s", // Matches ordered lists starting with 1. or 2. etc
|
||||
"!\\[.*?\\]\\(.*?\\)", // Matches images 
|
||||
"\\[.*?\\]\\(.*?\\)", // Matches links [Link text](URL)
|
||||
].join("|");
|
||||
|
||||
const MARKDOWN_REGEX = new RegExp(MARKDOWN_PATTERNS, "gm");
|
||||
|
||||
export function containsAnyMarkdown(...texts: string[]): boolean {
|
||||
MARKDOWN_REGEX.lastIndex = 0;
|
||||
return texts.some((text) => MARKDOWN_REGEX.test(text));
|
||||
}
|
||||
|
||||
export const MarkdownSchema = z.string().refine(containsAnyMarkdown);
|
||||
@@ -132,6 +132,7 @@ export const SessionPage: React.FC<{
|
||||
variant="badge"
|
||||
type="session"
|
||||
source="SessionDetail"
|
||||
key={"annotation-drawer" + trace.id}
|
||||
/>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
+1
-1
@@ -38,7 +38,7 @@ export function TraceTableMultiSelectAction({
|
||||
const mutDeleteTraces = api.traces.deleteMany.useMutation({
|
||||
onSuccess: () => {
|
||||
onDeleteSuccess();
|
||||
void utils.traces.invalidate();
|
||||
void utils.traces.all.invalidate();
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@@ -43,7 +43,7 @@ import { type ScoreSimplified } from "@/src/server/api/routers/generations/getAl
|
||||
import { useRowHeightLocalStorage } from "@/src/components/table/data-table-row-height-switch";
|
||||
import { IOTableCell } from "@/src/components/ui/CodeJsonViewer";
|
||||
import { usePostHogClientCapture } from "@/src/features/posthog-analytics/usePostHogClientCapture";
|
||||
import { useLookBackDays } from "@/src/hooks/useLookBackDays";
|
||||
import { useTableLookBackDays } from "@/src/hooks/useTableLookBackDays";
|
||||
|
||||
export type GenerationsTableRow = {
|
||||
id: string;
|
||||
@@ -112,7 +112,7 @@ export default function GenerationsTable({
|
||||
column: "Start Time",
|
||||
type: "datetime",
|
||||
operator: ">",
|
||||
value: utcDateOffsetByDays(-useLookBackDays(projectId)),
|
||||
value: utcDateOffsetByDays(-useTableLookBackDays(projectId)),
|
||||
},
|
||||
],
|
||||
"generations",
|
||||
|
||||
@@ -43,7 +43,7 @@ const modelConfigDescriptions = {
|
||||
outputPrice: "Price per 1000 units of output",
|
||||
totalPrice:
|
||||
"Price per 1000 units, for models that don't have input/output specific prices",
|
||||
unit: "Unit of measurement for generative model, can be TOKENS, CHARACTERS, SECONDS, MILLISECONDS, or IMAGES.",
|
||||
unit: "Unit of measurement for generative model, can be TOKENS, CHARACTERS, SECONDS, MILLISECONDS, REQUESTS or IMAGES.",
|
||||
tokenizerId:
|
||||
"Tokenizer used for this model to calculate token counts if none are ingested. Pick from list of supported tokenizers.",
|
||||
config:
|
||||
|
||||
@@ -9,7 +9,7 @@ import useColumnVisibility from "@/src/features/column-visibility/hooks/useColum
|
||||
import { useQueryFilterState } from "@/src/features/filters/hooks/useFilterState";
|
||||
import { isNumericDataType } from "@/src/features/manual-scoring/lib/helpers";
|
||||
import { useOrderByState } from "@/src/features/orderBy/hooks/useOrderByState";
|
||||
import { useLookBackDays } from "@/src/hooks/useLookBackDays";
|
||||
import { useTableLookBackDays } from "@/src/hooks/useTableLookBackDays";
|
||||
import {
|
||||
type ScoreOptions,
|
||||
scoresTableColsWithOptions,
|
||||
@@ -91,7 +91,7 @@ export default function ScoresTable({
|
||||
column: "Timestamp",
|
||||
type: "datetime",
|
||||
operator: ">",
|
||||
value: utcDateOffsetByDays(-useLookBackDays(projectId)),
|
||||
value: utcDateOffsetByDays(-useTableLookBackDays(projectId)),
|
||||
},
|
||||
],
|
||||
"scores",
|
||||
|
||||
@@ -20,7 +20,7 @@ import { type RouterOutput } from "@/src/utils/types";
|
||||
import type Decimal from "decimal.js";
|
||||
import { useEffect } from "react";
|
||||
import { NumberParam, useQueryParams, withDefault } from "use-query-params";
|
||||
import { useLookBackDays } from "@/src/hooks/useLookBackDays";
|
||||
import { useTableLookBackDays } from "@/src/hooks/useTableLookBackDays";
|
||||
import { BatchExportTableButton } from "@/src/components/BatchExportTableButton";
|
||||
|
||||
export type SessionTableRow = {
|
||||
@@ -57,7 +57,7 @@ export default function SessionsTable({
|
||||
column: "Created At",
|
||||
type: "datetime",
|
||||
operator: ">",
|
||||
value: utcDateOffsetByDays(-useLookBackDays(projectId)),
|
||||
value: utcDateOffsetByDays(-useTableLookBackDays(projectId)),
|
||||
},
|
||||
],
|
||||
"sessions",
|
||||
|
||||
@@ -37,8 +37,8 @@ import {
|
||||
} from "@langfuse/shared";
|
||||
import { useRowHeightLocalStorage } from "@/src/components/table/data-table-row-height-switch";
|
||||
import { IOTableCell } from "@/src/components/ui/CodeJsonViewer";
|
||||
import { useLookBackDays } from "@/src/hooks/useLookBackDays";
|
||||
import { type ValidatedScore } from "@/src/features/public-api/types/scores";
|
||||
import { useTableLookBackDays } from "@/src/hooks/useTableLookBackDays";
|
||||
import { type APIScore } from "@/src/features/public-api/types/scores";
|
||||
|
||||
export type TracesTableRow = {
|
||||
bookmarked: boolean;
|
||||
@@ -56,12 +56,12 @@ export type TracesTableRow = {
|
||||
input?: unknown;
|
||||
output?: unknown;
|
||||
metadata?: unknown;
|
||||
scores: ValidatedScore[];
|
||||
scores: APIScore[];
|
||||
tags: string[];
|
||||
usage: {
|
||||
promptTokens: number;
|
||||
completionTokens: number;
|
||||
totalTokens: number;
|
||||
promptTokens: bigint;
|
||||
completionTokens: bigint;
|
||||
totalTokens: bigint;
|
||||
};
|
||||
inputCost?: Decimal;
|
||||
outputCost?: Decimal;
|
||||
@@ -95,7 +95,7 @@ export default function TracesTable({
|
||||
column: "Timestamp",
|
||||
type: "datetime",
|
||||
operator: ">",
|
||||
value: utcDateOffsetByDays(-useLookBackDays(projectId)),
|
||||
value: utcDateOffsetByDays(-useTableLookBackDays(projectId)),
|
||||
},
|
||||
],
|
||||
"traces",
|
||||
@@ -236,7 +236,8 @@ export default function TracesTable({
|
||||
header: undefined,
|
||||
id: "bookmarked",
|
||||
cell: ({ row }) => {
|
||||
const bookmarked = row.getValue("bookmarked");
|
||||
const bookmarked: TracesTableRow["bookmarked"] =
|
||||
row.getValue("bookmarked");
|
||||
const traceId = row.getValue("id");
|
||||
return typeof traceId === "string" &&
|
||||
typeof bookmarked === "boolean" ? (
|
||||
@@ -256,7 +257,7 @@ export default function TracesTable({
|
||||
header: "ID",
|
||||
id: "id",
|
||||
cell: ({ row }) => {
|
||||
const value = row.getValue("id");
|
||||
const value: TracesTableRow["id"] = row.getValue("id");
|
||||
return value && typeof value === "string" ? (
|
||||
<TableLink
|
||||
path={`/project/${projectId}/traces/${value}`}
|
||||
@@ -285,7 +286,7 @@ export default function TracesTable({
|
||||
header: "User ID",
|
||||
id: "userId",
|
||||
cell: ({ row }) => {
|
||||
const value = row.getValue("userId");
|
||||
const value: TracesTableRow["userId"] = row.getValue("userId");
|
||||
return value && typeof value === "string" ? (
|
||||
<TableLink
|
||||
path={`/project/${projectId}/users/${encodeURIComponent(value)}`}
|
||||
@@ -303,7 +304,7 @@ export default function TracesTable({
|
||||
id: "sessionId",
|
||||
header: "Session ID",
|
||||
cell: ({ row }) => {
|
||||
const value = row.getValue("sessionId");
|
||||
const value: TracesTableRow["sessionId"] = row.getValue("sessionId");
|
||||
return value && typeof value === "string" ? (
|
||||
<TableLink
|
||||
path={`/project/${projectId}/sessions/${encodeURIComponent(value)}`}
|
||||
@@ -321,7 +322,7 @@ export default function TracesTable({
|
||||
header: "Latency",
|
||||
// add seconds to the end of the latency
|
||||
cell: ({ row }) => {
|
||||
const value: number | undefined = row.getValue("latency");
|
||||
const value: TracesTableRow["latency"] = row.getValue("latency");
|
||||
return value !== undefined ? formatIntervalSeconds(value) : undefined;
|
||||
},
|
||||
enableHiding: true,
|
||||
@@ -332,11 +333,7 @@ export default function TracesTable({
|
||||
id: "inputTokens",
|
||||
header: "Input Tokens",
|
||||
cell: ({ row }) => {
|
||||
const value: {
|
||||
promptTokens: number;
|
||||
completionTokens: number;
|
||||
totalTokens: number;
|
||||
} = row.getValue("usage");
|
||||
const value: TracesTableRow["usage"] = row.getValue("usage");
|
||||
return <span>{numberFormatter(value.promptTokens, 0)}</span>;
|
||||
},
|
||||
enableHiding: true,
|
||||
@@ -348,11 +345,7 @@ export default function TracesTable({
|
||||
id: "outputTokens",
|
||||
header: "Output Tokens",
|
||||
cell: ({ row }) => {
|
||||
const value: {
|
||||
promptTokens: number;
|
||||
completionTokens: number;
|
||||
totalTokens: number;
|
||||
} = row.getValue("usage");
|
||||
const value: TracesTableRow["usage"] = row.getValue("usage");
|
||||
return <span>{numberFormatter(value.completionTokens, 0)}</span>;
|
||||
},
|
||||
enableHiding: true,
|
||||
@@ -364,11 +357,7 @@ export default function TracesTable({
|
||||
id: "totalTokens",
|
||||
header: "Total Tokens",
|
||||
cell: ({ row }) => {
|
||||
const value: {
|
||||
promptTokens: number;
|
||||
completionTokens: number;
|
||||
totalTokens: number;
|
||||
} = row.getValue("usage");
|
||||
const value: TracesTableRow["usage"] = row.getValue("usage");
|
||||
return <span>{numberFormatter(value.totalTokens, 0)}</span>;
|
||||
},
|
||||
enableHiding: true,
|
||||
@@ -381,11 +370,7 @@ export default function TracesTable({
|
||||
header: "Usage",
|
||||
id: "usage",
|
||||
cell: ({ row }) => {
|
||||
const value: {
|
||||
promptTokens: number;
|
||||
completionTokens: number;
|
||||
totalTokens: number;
|
||||
} = row.getValue("usage");
|
||||
const value: TracesTableRow["usage"] = row.getValue("usage");
|
||||
return (
|
||||
<TokenUsageBadge
|
||||
promptTokens={value.promptTokens}
|
||||
@@ -403,7 +388,7 @@ export default function TracesTable({
|
||||
id: "inputCost",
|
||||
header: "Input Cost",
|
||||
cell: ({ row }) => {
|
||||
const cost: Decimal | undefined = row.getValue("inputCost");
|
||||
const cost: TracesTableRow["inputCost"] = row.getValue("inputCost");
|
||||
return (
|
||||
<div>
|
||||
{cost ? (
|
||||
@@ -423,7 +408,7 @@ export default function TracesTable({
|
||||
id: "outputCost",
|
||||
header: "Output Cost",
|
||||
cell: ({ row }) => {
|
||||
const cost: Decimal | undefined = row.getValue("outputCost");
|
||||
const cost: TracesTableRow["outputCost"] = row.getValue("outputCost");
|
||||
return (
|
||||
<div>
|
||||
{cost ? (
|
||||
@@ -443,7 +428,7 @@ export default function TracesTable({
|
||||
id: "totalCost",
|
||||
header: "Total Cost",
|
||||
cell: ({ row }) => {
|
||||
const cost: Decimal | undefined = row.getValue("totalCost");
|
||||
const cost: TracesTableRow["totalCost"] = row.getValue("totalCost");
|
||||
return (
|
||||
<div>
|
||||
{cost ? (
|
||||
@@ -463,7 +448,7 @@ export default function TracesTable({
|
||||
header: "Scores",
|
||||
enableColumnFilter: !omittedFilter.find((f) => f === "scores"),
|
||||
cell: ({ row }) => {
|
||||
const values: ValidatedScore[] = row.getValue("scores");
|
||||
const values: TracesTableRow["scores"] = row.getValue("scores");
|
||||
return <GroupedScoreBadges scores={values} variant="headings" />;
|
||||
},
|
||||
enableHiding: true,
|
||||
@@ -473,7 +458,7 @@ export default function TracesTable({
|
||||
header: "Input",
|
||||
id: "input",
|
||||
cell: ({ row }) => {
|
||||
const traceId: string = row.getValue("id");
|
||||
const traceId: TracesTableRow["id"] = row.getValue("id");
|
||||
return (
|
||||
<TracesDynamicCell
|
||||
traceId={traceId}
|
||||
@@ -490,7 +475,7 @@ export default function TracesTable({
|
||||
header: "Output",
|
||||
id: "output",
|
||||
cell: ({ row }) => {
|
||||
const traceId: string = row.getValue("id");
|
||||
const traceId: TracesTableRow["id"] = row.getValue("id");
|
||||
return (
|
||||
<TracesDynamicCell
|
||||
traceId={traceId}
|
||||
@@ -506,7 +491,7 @@ export default function TracesTable({
|
||||
accessorKey: "metadata",
|
||||
header: "Metadata",
|
||||
cell: ({ row }) => {
|
||||
const traceId: string = row.getValue("id");
|
||||
const traceId: TracesTableRow["id"] = row.getValue("id");
|
||||
return (
|
||||
<TracesDynamicCell
|
||||
traceId={traceId}
|
||||
@@ -523,7 +508,7 @@ export default function TracesTable({
|
||||
id: "level",
|
||||
header: "Level",
|
||||
cell: ({ row }) => {
|
||||
const value: ObservationLevel = row.getValue("level");
|
||||
const value: TracesTableRow["level"] = row.getValue("level");
|
||||
return (
|
||||
<span
|
||||
className={cn(
|
||||
@@ -568,8 +553,8 @@ export default function TracesTable({
|
||||
id: "tags",
|
||||
header: "Tags",
|
||||
cell: ({ row }) => {
|
||||
const tags: string[] = row.getValue("tags");
|
||||
const traceId: string = row.getValue("id");
|
||||
const tags: TracesTableRow["tags"] = row.getValue("tags");
|
||||
const traceId: TracesTableRow["id"] = row.getValue("id");
|
||||
const filterOptionTags = traceFilterOptions.data?.tags ?? [];
|
||||
const allTags = filterOptionTags.map((t) => t.value);
|
||||
return (
|
||||
@@ -588,13 +573,13 @@ export default function TracesTable({
|
||||
accessorKey: "action",
|
||||
header: "Action",
|
||||
cell: ({ row }) => {
|
||||
const traceId = row.getValue("id");
|
||||
const traceId: TracesTableRow["id"] = row.getValue("id");
|
||||
return traceId && typeof traceId === "string" ? (
|
||||
<DeleteButton
|
||||
itemId={traceId}
|
||||
projectId={projectId}
|
||||
scope="traces:delete"
|
||||
invalidateFunc={() => void utils.traces.invalidate()}
|
||||
invalidateFunc={() => void utils.traces.all.invalidate()}
|
||||
type="trace"
|
||||
isTableAction={true}
|
||||
/>
|
||||
|
||||
@@ -26,9 +26,9 @@ export const TokenUsageBadge = (
|
||||
observation: Observation;
|
||||
}
|
||||
| {
|
||||
promptTokens: number;
|
||||
completionTokens: number;
|
||||
totalTokens: number;
|
||||
promptTokens: number | bigint;
|
||||
completionTokens: number | bigint;
|
||||
totalTokens: number | bigint;
|
||||
}
|
||||
) & {
|
||||
inline?: boolean;
|
||||
|
||||
@@ -1,20 +1,67 @@
|
||||
import { JSONView } from "@/src/components/ui/CodeJsonViewer";
|
||||
import { z } from "zod";
|
||||
import { deepParseJson } from "@langfuse/shared";
|
||||
import { type Prisma, deepParseJson } from "@langfuse/shared";
|
||||
import { cn } from "@/src/utils/tailwind";
|
||||
import { useState } from "react";
|
||||
import { useMemo, useState } from "react";
|
||||
import { Button } from "@/src/components/ui/button";
|
||||
import { Tabs, TabsList, TabsTrigger } from "@/src/components/ui/tabs";
|
||||
import { Fragment } from "react";
|
||||
import { usePostHogClientCapture } from "@/src/features/posthog-analytics/usePostHogClientCapture";
|
||||
import { MarkdownView } from "@/src/components/ui/MarkdownViewer";
|
||||
import { MarkdownSchema } from "@/src/components/schemas/MarkdownSchema";
|
||||
import {
|
||||
ChatMlArraySchema,
|
||||
ChatMlMessageSchema,
|
||||
} from "@/src/components/schemas/ChatMlSchema";
|
||||
import useLocalStorage from "@/src/components/useLocalStorage";
|
||||
|
||||
function MarkdownOrJsonView(props: {
|
||||
isMarkdown: boolean;
|
||||
setIsMarkdown?: (value: boolean) => void;
|
||||
content?: unknown;
|
||||
title?: string;
|
||||
className?: string;
|
||||
customCodeHeaderClassName?: string;
|
||||
}) {
|
||||
const validatedMarkdown = useMemo(
|
||||
() => MarkdownSchema.safeParse(props.content),
|
||||
[props.content],
|
||||
);
|
||||
|
||||
const handleMarkdownSelection = props.setIsMarkdown ?? (() => {});
|
||||
|
||||
return validatedMarkdown.success && props.isMarkdown ? (
|
||||
<MarkdownView
|
||||
markdown={validatedMarkdown.data}
|
||||
isMarkdown={props.isMarkdown}
|
||||
setIsMarkdown={handleMarkdownSelection}
|
||||
title={props.title}
|
||||
className={props.className}
|
||||
customCodeHeaderClassName={props.customCodeHeaderClassName}
|
||||
/>
|
||||
) : (
|
||||
<JSONView
|
||||
json={props.content}
|
||||
isMarkdown={props.isMarkdown}
|
||||
setIsMarkdown={handleMarkdownSelection}
|
||||
containsMarkdown={validatedMarkdown.success}
|
||||
title={props.title}
|
||||
className={props.className}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export const IOPreview: React.FC<{
|
||||
input?: unknown;
|
||||
output?: unknown;
|
||||
input?: Prisma.JsonValue;
|
||||
output?: Prisma.JsonValue;
|
||||
isLoading?: boolean;
|
||||
hideIfNull?: boolean;
|
||||
}> = ({ isLoading = false, hideIfNull = false, ...props }) => {
|
||||
const [currentView, setCurrentView] = useState<"pretty" | "json">("pretty");
|
||||
const [isMarkdown, setIsMarkdown] = useLocalStorage(
|
||||
"shouldRenderMarkdown",
|
||||
true,
|
||||
);
|
||||
const capture = usePostHogClientCapture();
|
||||
const input = deepParseJson(props.input);
|
||||
const output = deepParseJson(props.output);
|
||||
@@ -59,43 +106,75 @@ export const IOPreview: React.FC<{
|
||||
Array.isArray(output) ? output : [output],
|
||||
);
|
||||
|
||||
// Pretty view available
|
||||
const isPrettyViewAvailable = inChatMlArray.success;
|
||||
const inMarkdown = MarkdownSchema.safeParse(input);
|
||||
const outMarkdown = MarkdownSchema.safeParse(output);
|
||||
|
||||
const isPrettyViewAvailable =
|
||||
inChatMlArray.success || inMarkdown.success || outMarkdown.success;
|
||||
|
||||
// default I/O
|
||||
return (
|
||||
<>
|
||||
{isPrettyViewAvailable ? (
|
||||
<Tabs
|
||||
value={currentView}
|
||||
onValueChange={(v) => {
|
||||
setCurrentView(v as "pretty" | "json"),
|
||||
capture("trace_detail:io_mode_switch", { view: v });
|
||||
}}
|
||||
>
|
||||
<TabsList>
|
||||
<TabsTrigger value="pretty">Pretty ✨</TabsTrigger>
|
||||
<TabsTrigger value="json">JSON</TabsTrigger>
|
||||
</TabsList>
|
||||
</Tabs>
|
||||
<div className="flex flex-row justify-between">
|
||||
<Tabs
|
||||
value={currentView}
|
||||
onValueChange={(v) => {
|
||||
setCurrentView(v as "pretty" | "json"),
|
||||
capture("trace_detail:io_mode_switch", { view: v });
|
||||
}}
|
||||
>
|
||||
<TabsList>
|
||||
<TabsTrigger value="pretty">Pretty ✨</TabsTrigger>
|
||||
<TabsTrigger value="json">JSON</TabsTrigger>
|
||||
</TabsList>
|
||||
</Tabs>
|
||||
</div>
|
||||
) : null}
|
||||
{isPrettyViewAvailable && currentView === "pretty" ? (
|
||||
<OpenAiMessageView
|
||||
messages={[
|
||||
...inChatMlArray.data,
|
||||
...(outChatMlArray.success
|
||||
? outChatMlArray.data.map((m) => ({
|
||||
...m,
|
||||
role: m.role ?? "assistant",
|
||||
}))
|
||||
: [
|
||||
ChatMlMessageSchema.parse({
|
||||
role: "assistant",
|
||||
content: outputClean ? JSON.stringify(outputClean) : null,
|
||||
}),
|
||||
]),
|
||||
]}
|
||||
/>
|
||||
<>
|
||||
{inChatMlArray.success ? (
|
||||
<OpenAiMessageView
|
||||
messages={[
|
||||
...inChatMlArray.data,
|
||||
...(outChatMlArray.success
|
||||
? outChatMlArray.data.map((m) => ({
|
||||
...m,
|
||||
role: m.role ?? "assistant",
|
||||
}))
|
||||
: [
|
||||
ChatMlMessageSchema.parse({
|
||||
role: "assistant",
|
||||
content: outputClean,
|
||||
}),
|
||||
]),
|
||||
]}
|
||||
isMarkdown={isMarkdown}
|
||||
setIsMarkdown={setIsMarkdown}
|
||||
/>
|
||||
) : (
|
||||
<>
|
||||
{!(hideIfNull && !input) ? (
|
||||
<MarkdownOrJsonView
|
||||
title="Input"
|
||||
content={input}
|
||||
isMarkdown={isMarkdown}
|
||||
setIsMarkdown={setIsMarkdown}
|
||||
/>
|
||||
) : null}
|
||||
{!(hideIfNull && !output) ? (
|
||||
<MarkdownOrJsonView
|
||||
title="Output"
|
||||
content={output}
|
||||
isMarkdown={isMarkdown}
|
||||
setIsMarkdown={setIsMarkdown}
|
||||
className="bg-accent-light-green dark:border-accent-dark-green"
|
||||
customCodeHeaderClassName="bg-muted-green dark:bg-secondary"
|
||||
/>
|
||||
) : null}
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
) : null}
|
||||
{currentView === "json" || !isPrettyViewAvailable ? (
|
||||
<>
|
||||
@@ -121,57 +200,24 @@ export const IOPreview: React.FC<{
|
||||
);
|
||||
};
|
||||
|
||||
const ChatMlMessageSchema = z
|
||||
.object({
|
||||
role: z
|
||||
.enum(["system", "user", "assistant", "function", "tool"])
|
||||
.optional(),
|
||||
name: z.string().optional(),
|
||||
content: z
|
||||
.union([z.record(z.any()), z.string(), z.array(z.any())])
|
||||
.nullish(),
|
||||
additional_kwargs: z.record(z.any()).optional(),
|
||||
})
|
||||
.passthrough()
|
||||
.refine((value) => value.content !== null || value.role !== undefined)
|
||||
.transform(({ additional_kwargs, ...other }) => ({
|
||||
...other,
|
||||
...additional_kwargs,
|
||||
}))
|
||||
.transform(({ role, name, content, ...other }) => ({
|
||||
role,
|
||||
name,
|
||||
content,
|
||||
json: Object.keys(other).length === 0 ? undefined : other,
|
||||
}));
|
||||
export const ChatMlArraySchema = z.array(ChatMlMessageSchema).min(1);
|
||||
|
||||
export const OpenAiMessageView: React.FC<{
|
||||
title?: string;
|
||||
messages: z.infer<typeof ChatMlArraySchema>;
|
||||
}> = ({ title, messages }) => {
|
||||
isMarkdown?: boolean;
|
||||
setIsMarkdown?: (value: boolean) => void;
|
||||
}> = ({ title, messages, isMarkdown, setIsMarkdown }) => {
|
||||
const COLLAPSE_THRESHOLD = 3;
|
||||
const [isCollapsed, setCollapsed] = useState(
|
||||
messages.length > COLLAPSE_THRESHOLD ? true : null,
|
||||
);
|
||||
|
||||
const transformedMessages = messages;
|
||||
// const transformedMessages = messages.map(
|
||||
// ({ role, name, content, ...rest }) => ({
|
||||
// role,
|
||||
// name,
|
||||
// content,
|
||||
// json: rest,
|
||||
// }),
|
||||
// );
|
||||
|
||||
return (
|
||||
<div className="rounded-md border">
|
||||
{title && (
|
||||
<div className="border-b px-3 py-1 text-xs font-medium">{title}</div>
|
||||
)}
|
||||
<div className="flex flex-col gap-2 p-3">
|
||||
{transformedMessages
|
||||
{messages
|
||||
.filter(
|
||||
(_, i) =>
|
||||
// show all if not collapsed or null; show first and last n if collapsed
|
||||
@@ -183,9 +229,11 @@ export const OpenAiMessageView: React.FC<{
|
||||
<Fragment key={index}>
|
||||
<div>
|
||||
{!!message.content && (
|
||||
<JSONView
|
||||
<MarkdownOrJsonView
|
||||
title={message.name ?? message.role}
|
||||
json={message.content}
|
||||
content={message.content}
|
||||
isMarkdown={isMarkdown ?? false}
|
||||
setIsMarkdown={setIsMarkdown}
|
||||
className={cn(
|
||||
"bg-muted",
|
||||
message.role === "system" && "bg-primary-foreground",
|
||||
@@ -194,6 +242,10 @@ export const OpenAiMessageView: React.FC<{
|
||||
message.role === "user" && "bg-background",
|
||||
!!message.json && "rounded-b-none",
|
||||
)}
|
||||
customCodeHeaderClassName={cn(
|
||||
message.role === "assistant" &&
|
||||
"bg-muted-green dark:bg-secondary",
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
{!!message.json && (
|
||||
|
||||
@@ -22,12 +22,12 @@ import ScoresTable from "@/src/components/table/use-cases/scores";
|
||||
import { ScoresPreview } from "@/src/components/trace/ScoresPreview";
|
||||
import { JumpToPlaygroundButton } from "@/src/ee/features/playground/page/components/JumpToPlaygroundButton";
|
||||
import { AnnotateDrawer } from "@/src/features/manual-scoring/components/AnnotateDrawer";
|
||||
import { type ValidatedScore } from "@/src/features/public-api/types/scores";
|
||||
import { type APIScore } from "@/src/features/public-api/types/scores";
|
||||
|
||||
export const ObservationPreview = (props: {
|
||||
observations: Array<ObservationReturnType>;
|
||||
projectId: string;
|
||||
scores: ValidatedScore[];
|
||||
scores: APIScore[];
|
||||
currentObservationId: string;
|
||||
traceId: string;
|
||||
}) => {
|
||||
@@ -60,7 +60,7 @@ export const ObservationPreview = (props: {
|
||||
}
|
||||
acc.get(score.source)?.push(score);
|
||||
return acc;
|
||||
}, new Map<ScoreSource, ValidatedScore[]>());
|
||||
}, new Map<ScoreSource, APIScore[]>());
|
||||
|
||||
return (
|
||||
<Card className="col-span-2 flex max-h-full flex-col overflow-hidden">
|
||||
@@ -148,7 +148,11 @@ export const ObservationPreview = (props: {
|
||||
.filter(Boolean)
|
||||
.map(([key, value]) => (
|
||||
<Badge variant="outline" key={key}>
|
||||
{key}: {value?.toString()}
|
||||
{key}:{" "}
|
||||
{Object.prototype.toString.call(value) ===
|
||||
"[object Object]"
|
||||
? JSON.stringify(value)
|
||||
: value?.toString()}
|
||||
</Badge>
|
||||
))
|
||||
: null}
|
||||
@@ -161,6 +165,7 @@ export const ObservationPreview = (props: {
|
||||
observationId={preloadedObservation.id}
|
||||
scores={props.scores}
|
||||
type="observation"
|
||||
key={"annotation-drawer" + preloadedObservation.id}
|
||||
/>
|
||||
{observationWithInputAndOutput.data?.type === "GENERATION" && (
|
||||
<JumpToPlaygroundButton
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { type NestedObservation } from "@/src/utils/types";
|
||||
import { cn } from "@/src/utils/tailwind";
|
||||
import { type Trace, type $Enums } from "@langfuse/shared";
|
||||
import { type ValidatedScore } from "@/src/features/public-api/types/scores";
|
||||
import { type APIScore } from "@/src/features/public-api/types/scores";
|
||||
import { GroupedScoreBadges } from "@/src/components/grouped-score-badge";
|
||||
import { Fragment } from "react";
|
||||
import { type ObservationReturnType } from "@/src/server/api/routers/traces";
|
||||
@@ -23,7 +23,7 @@ export const ObservationTree = (props: {
|
||||
collapseAll: () => void;
|
||||
expandAll: () => void;
|
||||
trace: Trace;
|
||||
scores: ValidatedScore[];
|
||||
scores: APIScore[];
|
||||
currentObservationId: string | undefined;
|
||||
setCurrentObservationId: (id: string | undefined) => void;
|
||||
showMetrics: boolean;
|
||||
@@ -62,7 +62,7 @@ const ObservationTreeTraceNode = (props: {
|
||||
trace: Trace & { latency?: number };
|
||||
expandAll: () => void;
|
||||
collapseAll: () => void;
|
||||
scores: ValidatedScore[];
|
||||
scores: APIScore[];
|
||||
currentObservationId: string | undefined;
|
||||
setCurrentObservationId: (id: string | undefined) => void;
|
||||
showMetrics?: boolean;
|
||||
@@ -120,7 +120,7 @@ const ObservationTreeNode = (props: {
|
||||
observations: NestedObservation[];
|
||||
collapsedObservations: string[];
|
||||
toggleCollapsedObservation: (id: string) => void;
|
||||
scores: ValidatedScore[];
|
||||
scores: APIScore[];
|
||||
indentationLevel: number;
|
||||
currentObservationId: string | undefined;
|
||||
setCurrentObservationId: (id: string | undefined) => void;
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import { GroupedScoreBadges } from "@/src/components/grouped-score-badge";
|
||||
import { type ValidatedScore } from "@/src/features/public-api/types/scores";
|
||||
import { type APIScore } from "@/src/features/public-api/types/scores";
|
||||
|
||||
export const ScoresPreview = ({
|
||||
itemScoresBySource,
|
||||
}: {
|
||||
itemScoresBySource: Map<string, ValidatedScore[]>;
|
||||
itemScoresBySource: Map<string, APIScore[]>;
|
||||
}) => {
|
||||
if (!Boolean(itemScoresBySource.size)) return null;
|
||||
|
||||
|
||||
@@ -19,7 +19,7 @@ import { withDefault, StringParam, useQueryParam } from "use-query-params";
|
||||
import ScoresTable from "@/src/components/table/use-cases/scores";
|
||||
import { ScoresPreview } from "@/src/components/trace/ScoresPreview";
|
||||
import { AnnotateDrawer } from "@/src/features/manual-scoring/components/AnnotateDrawer";
|
||||
import { type ValidatedScore } from "@/src/features/public-api/types/scores";
|
||||
import { type APIScore } from "@/src/features/public-api/types/scores";
|
||||
|
||||
export const TracePreview = ({
|
||||
trace,
|
||||
@@ -28,7 +28,7 @@ export const TracePreview = ({
|
||||
}: {
|
||||
trace: Trace & { latency?: number };
|
||||
observations: ObservationReturnType[];
|
||||
scores: ValidatedScore[];
|
||||
scores: APIScore[];
|
||||
}) => {
|
||||
const [selectedTab, setSelectedTab] = useQueryParam(
|
||||
"view",
|
||||
@@ -42,7 +42,7 @@ export const TracePreview = ({
|
||||
}
|
||||
acc.get(score.source)?.push(score);
|
||||
return acc;
|
||||
}, new Map<ScoreSource, ValidatedScore[]>());
|
||||
}, new Map<ScoreSource, APIScore[]>());
|
||||
|
||||
return (
|
||||
<Card className="col-span-2 flex max-h-full flex-col overflow-hidden">
|
||||
@@ -100,6 +100,7 @@ export const TracePreview = ({
|
||||
projectId={trace.projectId}
|
||||
traceId={trace.id}
|
||||
scores={scores}
|
||||
key={"annotation-drawer" + trace.id}
|
||||
/>
|
||||
<NewDatasetItemFromTrace
|
||||
traceId={trace.id}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { Card } from "@/src/components/ui/card";
|
||||
import { type ObservationReturnType } from "@/src/server/api/routers/traces";
|
||||
import { type Trace } from "@langfuse/shared";
|
||||
import { type ValidatedScore } from "@/src/features/public-api/types/scores";
|
||||
import { type APIScore } from "@/src/features/public-api/types/scores";
|
||||
|
||||
import React, { useEffect, useMemo, useRef, useState } from "react";
|
||||
import { SimpleTreeView } from "@mui/x-tree-view/SimpleTreeView";
|
||||
@@ -188,7 +188,7 @@ function TraceTreeItem({
|
||||
traceStartTime: Date;
|
||||
totalScaleSpan: number;
|
||||
projectId: string;
|
||||
scores: ValidatedScore[];
|
||||
scores: APIScore[];
|
||||
observations: Array<ObservationReturnType>;
|
||||
cardWidth: number;
|
||||
}) {
|
||||
@@ -267,7 +267,7 @@ export function TraceTimelineView({
|
||||
trace: Trace & { latency?: number };
|
||||
observations: Array<ObservationReturnType>;
|
||||
projectId: string;
|
||||
scores: ValidatedScore[];
|
||||
scores: APIScore[];
|
||||
}) {
|
||||
const { latency, name, id } = trace;
|
||||
const [backgroundColor, setBackgroundColor] = useState("");
|
||||
|
||||
@@ -34,12 +34,12 @@ import { usePostHogClientCapture } from "@/src/features/posthog-analytics/usePos
|
||||
import { Tabs, TabsList, TabsTrigger } from "@/src/components/ui/tabs";
|
||||
import { TraceTimelineView } from "@/src/components/trace/TraceTimelineView";
|
||||
import { Alert, AlertDescription, AlertTitle } from "@/src/components/ui/alert";
|
||||
import { type ValidatedScore } from "@/src/features/public-api/types/scores";
|
||||
import { type APIScore } from "@/src/features/public-api/types/scores";
|
||||
|
||||
export function Trace(props: {
|
||||
observations: Array<ObservationReturnType>;
|
||||
trace: Trace;
|
||||
scores: ValidatedScore[];
|
||||
scores: APIScore[];
|
||||
projectId: string;
|
||||
}) {
|
||||
const capture = usePostHogClientCapture();
|
||||
@@ -102,6 +102,7 @@ export function Trace(props: {
|
||||
const expandAll = useCallback(() => {
|
||||
capture("trace_detail:observation_tree_expand", { type: "all" });
|
||||
setCollapsedObservations([]);
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
|
||||
return (
|
||||
@@ -261,7 +262,7 @@ export function TracePage({ traceId }: { traceId: string }) {
|
||||
itemId={traceId}
|
||||
projectId={trace.data.projectId}
|
||||
scope="traces:delete"
|
||||
invalidateFunc={() => void utils.traces.invalidate()}
|
||||
invalidateFunc={() => void utils.traces.all.invalidate()}
|
||||
type="trace"
|
||||
redirectUrl={`/project/${router.query.projectId as string}/traces`}
|
||||
/>
|
||||
|
||||
@@ -16,8 +16,19 @@ export function nestObservations(
|
||||
): NestedObservation[] {
|
||||
if (list.length === 0) return [];
|
||||
|
||||
// Data prep: Remove parentObservationId attribute from observations if the id does not exist in the list of observations
|
||||
const mutableList = list.map((o) => ({ ...o }));
|
||||
mutableList.forEach((observation) => {
|
||||
if (
|
||||
observation.parentObservationId &&
|
||||
!list.find((o) => o.id === observation.parentObservationId)
|
||||
) {
|
||||
observation.parentObservationId = null;
|
||||
}
|
||||
});
|
||||
|
||||
// Step 0: Sort the list by start time to ensure observations are in right order
|
||||
const sortedObservations = list.sort(
|
||||
const sortedObservations = mutableList.sort(
|
||||
(a, b) => a.startTime.getTime() - b.startTime.getTime(),
|
||||
);
|
||||
|
||||
|
||||
@@ -7,29 +7,89 @@ import "react18-json-view/src/dark.css";
|
||||
import { deepParseJson } from "@langfuse/shared";
|
||||
import { Skeleton } from "@/src/components/ui/skeleton";
|
||||
import { useTheme } from "next-themes";
|
||||
import { BsMarkdown } from "react-icons/bs";
|
||||
import { usePostHogClientCapture } from "@/src/features/posthog-analytics/usePostHogClientCapture";
|
||||
|
||||
export function JSONView(props: {
|
||||
isMarkdown?: boolean;
|
||||
setIsMarkdown?: (isMarkdown: boolean) => void;
|
||||
containsMarkdown?: boolean;
|
||||
json?: unknown;
|
||||
title?: string;
|
||||
className?: string;
|
||||
isLoading?: boolean;
|
||||
codeClassName?: string;
|
||||
collapseStringsAfterLength?: number | null;
|
||||
}) {
|
||||
// some users ingest stringified json nested in json, parse it
|
||||
const [isCopied, setIsCopied] = useState(false);
|
||||
const parsedJson = deepParseJson(props.json);
|
||||
const { resolvedTheme } = useTheme();
|
||||
const capture = usePostHogClientCapture();
|
||||
|
||||
const collapseStringsAfterLength =
|
||||
props.collapseStringsAfterLength === null
|
||||
? 100_000_000 // if null, show all (100M chars)
|
||||
: props.collapseStringsAfterLength ?? 500;
|
||||
|
||||
const handleMarkdownSelection = props.setIsMarkdown ?? (() => {});
|
||||
|
||||
const handleCopy = () => {
|
||||
setIsCopied(true);
|
||||
void navigator.clipboard.writeText(stringifyJsonNode(parsedJson));
|
||||
setTimeout(() => setIsCopied(false), 1000);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={cn("rounded-md border", props.className)}>
|
||||
<div className={cn("overflow-hidden rounded-md border", props.className)}>
|
||||
{props.title ? (
|
||||
<div
|
||||
className={cn(
|
||||
props.title === "assistant" || props.title === "Output"
|
||||
? "dark:border-accent-dark-green"
|
||||
: "",
|
||||
"border-b px-3 py-1 text-xs font-medium",
|
||||
"flex flex-row items-center justify-between border-b px-3 py-1 text-xs font-medium",
|
||||
)}
|
||||
>
|
||||
{props.title}
|
||||
<div className="flex items-center gap-1">
|
||||
{!!props.setIsMarkdown && props.containsMarkdown && (
|
||||
<Button
|
||||
title={
|
||||
props.isMarkdown ? "Disable Markdown" : "Enable Markdown"
|
||||
}
|
||||
variant="ghost"
|
||||
type="button"
|
||||
size="xs"
|
||||
onClick={() => {
|
||||
handleMarkdownSelection(!props.isMarkdown);
|
||||
capture("trace_detail:io_pretty_format_toggle_group", {
|
||||
renderMarkdown: props.isMarkdown,
|
||||
});
|
||||
}}
|
||||
className={cn(
|
||||
"hover:bg-border",
|
||||
!props.isMarkdown && "opacity-50",
|
||||
)}
|
||||
>
|
||||
<BsMarkdown className="h-4 w-4 text-foreground" />
|
||||
</Button>
|
||||
)}
|
||||
<Button
|
||||
title="Copy to clipboard"
|
||||
variant="ghost"
|
||||
size="xs"
|
||||
type="button"
|
||||
onClick={handleCopy}
|
||||
className="hover:bg-border"
|
||||
>
|
||||
{isCopied ? (
|
||||
<Check className="h-3 w-3" />
|
||||
) : (
|
||||
<Copy className="h-3 w-3" />
|
||||
)}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
) : undefined}
|
||||
<div
|
||||
@@ -46,7 +106,15 @@ export function JSONView(props: {
|
||||
theme="github"
|
||||
dark={resolvedTheme === "dark"}
|
||||
collapseObjectsAfterLength={20}
|
||||
collapseStringsAfterLength={500}
|
||||
collapseStringsAfterLength={collapseStringsAfterLength}
|
||||
collapseStringMode="word"
|
||||
customizeCollapseStringUI={(fullSTring, truncated) =>
|
||||
truncated ? (
|
||||
<div className="opacity-50">{`\n...expand (${Math.max(fullSTring.length - collapseStringsAfterLength, 0)} more characters)`}</div>
|
||||
) : (
|
||||
""
|
||||
)
|
||||
}
|
||||
displaySize={"collapsed"}
|
||||
matchesURL={true}
|
||||
customizeCopy={(node) => stringifyJsonNode(node)}
|
||||
@@ -148,6 +216,7 @@ export const IOTableCell = ({
|
||||
className,
|
||||
)}
|
||||
codeClassName="py-1 px-2"
|
||||
collapseStringsAfterLength={null} // in table, show full strings as row height is fixed
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
|
||||
@@ -0,0 +1,107 @@
|
||||
import { Button } from "@/src/components/ui/button";
|
||||
import { cn } from "@/src/utils/tailwind";
|
||||
import { Check, Copy } from "lucide-react";
|
||||
import { type FC, memo, useState } from "react";
|
||||
import { Prism as SyntaxHighlighter } from "react-syntax-highlighter";
|
||||
import {
|
||||
coldarkDark,
|
||||
solarizedlight,
|
||||
} from "react-syntax-highlighter/dist/cjs/styles/prism";
|
||||
|
||||
interface Props {
|
||||
language: string;
|
||||
value: string;
|
||||
theme?: string;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
interface languageMap {
|
||||
[key: string]: string;
|
||||
}
|
||||
|
||||
export const programmingLanguages: languageMap = {
|
||||
javascript: ".js",
|
||||
python: ".py",
|
||||
java: ".java",
|
||||
c: ".c",
|
||||
cpp: ".cpp",
|
||||
"c++": ".cpp",
|
||||
"c#": ".cs",
|
||||
ruby: ".rb",
|
||||
php: ".php",
|
||||
swift: ".swift",
|
||||
"objective-c": ".m",
|
||||
kotlin: ".kt",
|
||||
typescript: ".ts",
|
||||
go: ".go",
|
||||
perl: ".pl",
|
||||
rust: ".rs",
|
||||
scala: ".scala",
|
||||
haskell: ".hs",
|
||||
lua: ".lua",
|
||||
shell: ".sh",
|
||||
sql: ".sql",
|
||||
html: ".html",
|
||||
css: ".css",
|
||||
// add more file extensions here, make sure the key is same as language prop in CodeBlock.tsx component
|
||||
};
|
||||
|
||||
const CodeBlock: FC<Props> = memo(({ language, value, theme, className }) => {
|
||||
const [isCopied, setIsCopied] = useState(false);
|
||||
const handleCopy = () => {
|
||||
setIsCopied(true);
|
||||
void navigator.clipboard.writeText(value ?? "");
|
||||
setTimeout(() => setIsCopied(false), 1000);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="codeblock relative w-full overflow-hidden rounded border font-sans dark:bg-zinc-950">
|
||||
<div
|
||||
className={cn(
|
||||
"flex w-full items-center justify-between bg-secondary px-2",
|
||||
className,
|
||||
)}
|
||||
>
|
||||
<span className="text-xs lowercase">{language}</span>
|
||||
<div className="flex items-center py-1">
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="xs"
|
||||
className="text-xs hover:bg-border focus-visible:ring-1 focus-visible:ring-offset-0"
|
||||
onClick={handleCopy}
|
||||
>
|
||||
{isCopied ? (
|
||||
<Check className="h-3 w-3" />
|
||||
) : (
|
||||
<Copy className="h-3 w-3" />
|
||||
)}
|
||||
<span className="sr-only">Copy code</span>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<SyntaxHighlighter
|
||||
language={language}
|
||||
style={theme === "dark" ? coldarkDark : solarizedlight}
|
||||
PreTag="div"
|
||||
customStyle={{
|
||||
margin: 0,
|
||||
width: "100%",
|
||||
background: "transparent",
|
||||
padding: "0.5rem",
|
||||
}}
|
||||
codeTagProps={{
|
||||
style: {
|
||||
fontSize: "0.75rem",
|
||||
fontFamily: "var(--font-mono)",
|
||||
display: "block",
|
||||
},
|
||||
}}
|
||||
>
|
||||
{value}
|
||||
</SyntaxHighlighter>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
CodeBlock.displayName = "CodeBlock";
|
||||
|
||||
export { CodeBlock };
|
||||
@@ -0,0 +1,255 @@
|
||||
import { cn } from "@/src/utils/tailwind";
|
||||
import {
|
||||
type FC,
|
||||
type ReactNode,
|
||||
type ReactElement,
|
||||
memo,
|
||||
useState,
|
||||
isValidElement,
|
||||
Children,
|
||||
createElement,
|
||||
} from "react";
|
||||
import ReactMarkdown, { type Options } from "react-markdown";
|
||||
import Link from "next/link";
|
||||
import remarkGfm from "remark-gfm";
|
||||
import remarkMath from "remark-math";
|
||||
import { CodeBlock } from "@/src/components/ui/Codeblock";
|
||||
import { useTheme } from "next-themes";
|
||||
import { Button } from "@/src/components/ui/button";
|
||||
import { Check, Copy } from "lucide-react";
|
||||
import { BsMarkdown } from "react-icons/bs";
|
||||
import { usePostHogClientCapture } from "@/src/features/posthog-analytics/usePostHogClientCapture";
|
||||
|
||||
// ReactMarkdown does not render raw HTML by default for security reasons, to prevent XSS (Cross-Site Scripting) attacks.
|
||||
// html is rendered as plain text by default.
|
||||
const MemoizedReactMarkdown: FC<Options> = memo(
|
||||
ReactMarkdown,
|
||||
(prevProps, nextProps) =>
|
||||
prevProps.children === nextProps.children &&
|
||||
prevProps.className === nextProps.className,
|
||||
);
|
||||
|
||||
const isChecklist = (children: ReactNode) =>
|
||||
Array.isArray(children) &&
|
||||
children.some((child: any) => child?.props?.className === "task-list-item");
|
||||
|
||||
const isTextElement = (child: ReactNode): child is ReactElement =>
|
||||
isValidElement(child) &&
|
||||
typeof child.type !== "string" &&
|
||||
["p", "h1", "h2", "h3", "h4", "h5", "h6"].includes(child.type.name);
|
||||
|
||||
const transformListItemChildren = (children: ReactNode) =>
|
||||
Children.map(children, (child) =>
|
||||
isTextElement(child) ? (
|
||||
<div className="mb-1 inline-flex">
|
||||
{createElement(child.type, { ...child.props })}
|
||||
</div>
|
||||
) : (
|
||||
child
|
||||
),
|
||||
);
|
||||
|
||||
export function MarkdownView({
|
||||
markdown,
|
||||
isMarkdown,
|
||||
setIsMarkdown,
|
||||
title,
|
||||
className,
|
||||
customCodeHeaderClassName,
|
||||
}: {
|
||||
markdown: string;
|
||||
isMarkdown: boolean;
|
||||
setIsMarkdown: (value: boolean) => void;
|
||||
title?: string;
|
||||
className?: string;
|
||||
customCodeHeaderClassName?: string;
|
||||
}) {
|
||||
const [isCopied, setIsCopied] = useState(false);
|
||||
const { resolvedTheme: theme } = useTheme();
|
||||
const capture = usePostHogClientCapture();
|
||||
|
||||
const handleCopy = () => {
|
||||
setIsCopied(true);
|
||||
void navigator.clipboard.writeText(markdown);
|
||||
setTimeout(() => setIsCopied(false), 1000);
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cn("overflow-hidden rounded-md border", className)}
|
||||
key={theme}
|
||||
>
|
||||
{title ? (
|
||||
<div
|
||||
className={cn(
|
||||
title === "assistant" || title === "Output"
|
||||
? "dark:border-accent-dark-green"
|
||||
: "",
|
||||
"flex flex-row items-center justify-between border-b px-3 py-1 text-xs font-medium",
|
||||
)}
|
||||
>
|
||||
{title}
|
||||
<div className="flex items-center gap-1">
|
||||
<Button
|
||||
title={isMarkdown ? "Disable Markdown" : "Enable Markdown"}
|
||||
variant="ghost"
|
||||
size="xs"
|
||||
type="button"
|
||||
onClick={() => {
|
||||
setIsMarkdown(!isMarkdown);
|
||||
capture("trace_detail:io_pretty_format_toggle_group", {
|
||||
renderMarkdown: isMarkdown,
|
||||
});
|
||||
}}
|
||||
className={cn("hover:bg-border", !isMarkdown && "opacity-50")}
|
||||
>
|
||||
<BsMarkdown className="h-4 w-4" />
|
||||
</Button>
|
||||
<Button
|
||||
title="Copy to clipboard"
|
||||
variant="ghost"
|
||||
size="xs"
|
||||
type="button"
|
||||
onClick={handleCopy}
|
||||
className="hover:bg-border"
|
||||
>
|
||||
{isCopied ? (
|
||||
<Check className="h-3 w-3" />
|
||||
) : (
|
||||
<Copy className="h-3 w-3" />
|
||||
)}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
) : undefined}
|
||||
<MemoizedReactMarkdown
|
||||
className={cn(
|
||||
"space-y-4 overflow-x-auto break-words p-3 text-sm",
|
||||
className,
|
||||
)}
|
||||
remarkPlugins={[remarkGfm, remarkMath]}
|
||||
components={{
|
||||
p({ children }) {
|
||||
return (
|
||||
<p className="mb-2 whitespace-pre-wrap last:mb-0">{children}</p>
|
||||
);
|
||||
},
|
||||
a({ children, href }) {
|
||||
if (href)
|
||||
return (
|
||||
<Link href={href} className="underline" target="_blank">
|
||||
{children}
|
||||
</Link>
|
||||
);
|
||||
},
|
||||
ul({ children }) {
|
||||
if (isChecklist(children))
|
||||
return <ul className="list-none">{children}</ul>;
|
||||
|
||||
return <ul className="list-inside list-disc pl-2">{children}</ul>;
|
||||
},
|
||||
ol({ children }) {
|
||||
return (
|
||||
<ol className="list-inside list-decimal pl-2">{children}</ol>
|
||||
);
|
||||
},
|
||||
li({ children }) {
|
||||
return (
|
||||
<li className="mb-1 list-item">
|
||||
{transformListItemChildren(children)}
|
||||
</li>
|
||||
);
|
||||
},
|
||||
pre({ children }) {
|
||||
return <pre className="rounded p-2">{children}</pre>;
|
||||
},
|
||||
h1({ children }) {
|
||||
return <h1 className="text-2xl font-bold">{children}</h1>;
|
||||
},
|
||||
h2({ children }) {
|
||||
return <h2 className="text-xl font-bold">{children}</h2>;
|
||||
},
|
||||
h3({ children }) {
|
||||
return <h3 className="text-lg font-bold">{children}</h3>;
|
||||
},
|
||||
h4({ children }) {
|
||||
return <h4 className="text-base font-bold">{children}</h4>;
|
||||
},
|
||||
h5({ children }) {
|
||||
return <h5 className="text-sm font-bold">{children}</h5>;
|
||||
},
|
||||
h6({ children }) {
|
||||
return <h6 className="text-xs font-bold">{children}</h6>;
|
||||
},
|
||||
code({ children, className }) {
|
||||
const languageMatch = /language-(\w+)/.exec(className || "");
|
||||
const language = languageMatch ? languageMatch[1] : "";
|
||||
const codeContent = String(children).replace(/\n$/, "");
|
||||
const isMultiLine = codeContent.includes("\n");
|
||||
|
||||
return language || isMultiLine ? (
|
||||
// code block
|
||||
<CodeBlock
|
||||
key={Math.random()}
|
||||
language={language}
|
||||
value={codeContent}
|
||||
theme={theme}
|
||||
className={customCodeHeaderClassName}
|
||||
/>
|
||||
) : (
|
||||
// inline code
|
||||
<code className="rounded border bg-secondary px-0.5">
|
||||
{codeContent}
|
||||
</code>
|
||||
);
|
||||
},
|
||||
blockquote({ children }) {
|
||||
return (
|
||||
<blockquote className="border-l-4 pl-4 italic">
|
||||
{children}
|
||||
</blockquote>
|
||||
);
|
||||
},
|
||||
img({ src }) {
|
||||
return (
|
||||
<Link href={src ?? ""} className="underline" target="_blank">
|
||||
{src ?? ""}
|
||||
</Link>
|
||||
);
|
||||
},
|
||||
hr() {
|
||||
return <hr className="my-4" />;
|
||||
},
|
||||
table({ children }) {
|
||||
return (
|
||||
<div className="overflow-hidden rounded border">
|
||||
<table className="min-w-full divide-y">{children}</table>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
thead({ children }) {
|
||||
return <thead>{children}</thead>;
|
||||
},
|
||||
tbody({ children }) {
|
||||
return <tbody className="divide-y divide-border">{children}</tbody>;
|
||||
},
|
||||
tr({ children }) {
|
||||
return <tr>{children}</tr>;
|
||||
},
|
||||
th({ children }) {
|
||||
return (
|
||||
<th className="px-4 py-2 text-left text-xs font-medium uppercase tracking-wider">
|
||||
{children}
|
||||
</th>
|
||||
);
|
||||
},
|
||||
td({ children }) {
|
||||
return <td className="whitespace-nowrap px-4 py-2">{children}</td>;
|
||||
},
|
||||
}}
|
||||
>
|
||||
{markdown}
|
||||
</MemoizedReactMarkdown>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1 +1 @@
|
||||
export const VERSION = "v2.60.2";
|
||||
export const VERSION = "v2.64.0";
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
// import { registerOTel } from "@vercel/otel";
|
||||
|
||||
// const { TracerProvider } = (await import("dd-trace")).default.init({
|
||||
// runtimeMetrics: true,
|
||||
// profiling: true,
|
||||
// });
|
||||
|
||||
// const provider = new TracerProvider();
|
||||
|
||||
// registerOTel({
|
||||
// // instrumentations: [new PrismaInstrumentation()],
|
||||
// });
|
||||
// provider.register();
|
||||
@@ -153,7 +153,7 @@ export const InnerEvalConfigForm = (props: {
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (props.evalTemplate) {
|
||||
if (props.evalTemplate && form.getValues("mapping").length === 0) {
|
||||
form.setValue(
|
||||
"mapping",
|
||||
props.evalTemplate.vars.map((v) => ({
|
||||
|
||||
@@ -28,7 +28,6 @@ import { useIsEeEnabled } from "@/src/ee/utils/useIsEeEnabled";
|
||||
|
||||
export const SaveToPromptButton: React.FC = () => {
|
||||
const isEeEnabled = useIsEeEnabled();
|
||||
const [open, setOpen] = useState(false);
|
||||
const [selectedPromptId, setSelectedPromptId] = useState("");
|
||||
const { modelParams, messages, output, promptVariables } =
|
||||
usePlaygroundContext();
|
||||
@@ -115,7 +114,6 @@ export const SaveToPromptButton: React.FC = () => {
|
||||
setSelectedPromptId(
|
||||
currentValue === selectedPromptId ? "" : currentValue,
|
||||
);
|
||||
setOpen(false);
|
||||
}}
|
||||
>
|
||||
<Check
|
||||
|
||||
@@ -26,7 +26,6 @@ export const env = createEnv({
|
||||
),
|
||||
NEXTAUTH_COOKIE_DOMAIN: z.string().optional(),
|
||||
LANGFUSE_TEAM_SLACK_WEBHOOK: z.string().url().optional(),
|
||||
LANGFUSE_TEAM_BETTERSTACK_TOKEN: z.string().optional(),
|
||||
LANGFUSE_NEW_USER_SIGNUP_WEBHOOK: z.string().url().optional(),
|
||||
// Add `.min(1) on ID and SECRET if you want to make sure they're not empty
|
||||
LANGFUSE_ENABLE_EXPERIMENTAL_FEATURES: z.enum(["true", "false"]).optional(),
|
||||
@@ -157,8 +156,6 @@ export const env = createEnv({
|
||||
LANGFUSE_DISABLE_EXPENSIVE_POSTGRES_QUERIES:
|
||||
process.env.LANGFUSE_DISABLE_EXPENSIVE_POSTGRES_QUERIES,
|
||||
LANGFUSE_TEAM_SLACK_WEBHOOK: process.env.LANGFUSE_TEAM_SLACK_WEBHOOK,
|
||||
LANGFUSE_TEAM_BETTERSTACK_TOKEN:
|
||||
process.env.LANGFUSE_TEAM_BETTERSTACK_TOKEN,
|
||||
LANGFUSE_NEW_USER_SIGNUP_WEBHOOK:
|
||||
process.env.LANGFUSE_NEW_USER_SIGNUP_WEBHOOK,
|
||||
SALT: process.env.SALT,
|
||||
|
||||
@@ -1,27 +0,0 @@
|
||||
import { env } from "@/src/env.mjs";
|
||||
|
||||
export const sendToBetterstack = async (message: unknown) => {
|
||||
if (!env.LANGFUSE_TEAM_BETTERSTACK_TOKEN) return;
|
||||
|
||||
const url = "https://in.logs.betterstack.com";
|
||||
|
||||
const headers = new Headers({
|
||||
"Content-Type": "application/json",
|
||||
Authorization: `Bearer ${env.LANGFUSE_TEAM_BETTERSTACK_TOKEN}`,
|
||||
});
|
||||
|
||||
const payload = {
|
||||
dt: new Date().toISOString(), // Gets the current date in ISO format (UTC)
|
||||
message: JSON.stringify(message, null, 2),
|
||||
};
|
||||
|
||||
const response = await fetch(url, {
|
||||
method: "POST",
|
||||
headers: headers,
|
||||
body: JSON.stringify(payload),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP error! status: ${response.status}`);
|
||||
}
|
||||
};
|
||||
@@ -36,6 +36,7 @@ function useColumnVisibility<TData>(
|
||||
if (!isEqual(initialColumnVisibility, columnVisibility)) {
|
||||
setColumnVisibility(initialColumnVisibility);
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [columnVisibility, setColumnVisibility]);
|
||||
|
||||
return [columnVisibility, setColumnVisibility] as const;
|
||||
|
||||
@@ -7,7 +7,7 @@ import { formatIntervalSeconds } from "@/src/utils/dates";
|
||||
import { type RouterOutput } from "@/src/utils/types";
|
||||
import { useQueryParams, withDefault, NumberParam } from "use-query-params";
|
||||
|
||||
import { type ValidatedScore } from "@/src/features/public-api/types/scores";
|
||||
import { type APIScore } from "@/src/features/public-api/types/scores";
|
||||
import { usdFormatter } from "../../../utils/numbers";
|
||||
import useColumnVisibility from "@/src/features/column-visibility/hooks/useColumnVisibility";
|
||||
import { DataTableToolbar } from "@/src/components/table/data-table-toolbar";
|
||||
@@ -30,7 +30,7 @@ type RowData = {
|
||||
output?: unknown;
|
||||
expectedOutput?: unknown;
|
||||
|
||||
scores: ValidatedScore[];
|
||||
scores: APIScore[];
|
||||
latency?: number;
|
||||
totalCost?: string;
|
||||
};
|
||||
|
||||
@@ -35,7 +35,7 @@ import {
|
||||
CreateAnnotationScoreData,
|
||||
UpdateAnnotationScoreData,
|
||||
} from "@langfuse/shared";
|
||||
import { type ValidatedScore } from "@/src/features/public-api/types/scores";
|
||||
import { type APIScore } from "@/src/features/public-api/types/scores";
|
||||
import {
|
||||
type ValidatedScoreConfig,
|
||||
type ConfigCategory,
|
||||
@@ -137,7 +137,7 @@ export function AnnotateDrawer({
|
||||
source = "TraceDetail",
|
||||
}: {
|
||||
traceId: string;
|
||||
scores: ValidatedScore[];
|
||||
scores: APIScore[];
|
||||
observationId?: string;
|
||||
projectId: string;
|
||||
variant?: "button" | "badge";
|
||||
@@ -214,7 +214,7 @@ export function AnnotateDrawer({
|
||||
|
||||
const utils = api.useUtils();
|
||||
|
||||
const onSettledUpsert = async (data?: ValidatedScore, error?: unknown) => {
|
||||
const onSettledUpsert = async (data?: APIScore, error?: unknown) => {
|
||||
if (!data || error) return;
|
||||
|
||||
const { id, value, stringValue, name, dataType, configId, comment } = data;
|
||||
|
||||
@@ -207,7 +207,7 @@ export function CreateScoreConfigButton({ projectId }: { projectId: string }) {
|
||||
);
|
||||
form.clearErrors();
|
||||
if (isNumericDataType(value as ScoreDataType)) {
|
||||
remove();
|
||||
form.setValue("categories", undefined);
|
||||
} else {
|
||||
form.setValue("minValue", undefined);
|
||||
form.setValue("maxValue", undefined);
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { ScoreSource } from "@langfuse/shared";
|
||||
import { type ValidatedScore } from "@/src/features/public-api/types/scores";
|
||||
import { type APIScore } from "@/src/features/public-api/types/scores";
|
||||
import { type ValidatedScoreConfig } from "@/src/features/public-api/types/score-configs";
|
||||
|
||||
export const getDefaultScoreData = ({
|
||||
@@ -9,7 +9,7 @@ export const getDefaultScoreData = ({
|
||||
traceId,
|
||||
observationId,
|
||||
}: {
|
||||
scores: ValidatedScore[];
|
||||
scores: APIScore[];
|
||||
emptySelectedConfigIds: string[];
|
||||
configs: ValidatedScoreConfig[];
|
||||
traceId: string;
|
||||
|
||||
@@ -24,6 +24,7 @@ const events = {
|
||||
"observation_tree_toggle_scores",
|
||||
"observation_tree_toggle_metrics",
|
||||
"io_mode_switch",
|
||||
"io_pretty_format_toggle_group",
|
||||
"test_in_playground_button_click",
|
||||
"display_mode_switch",
|
||||
],
|
||||
|
||||
@@ -4,10 +4,7 @@ import { useRouter } from "next/router";
|
||||
import { NumberParam, useQueryParam } from "use-query-params";
|
||||
import type { z } from "zod";
|
||||
import Header from "@/src/components/layouts/header";
|
||||
import {
|
||||
ChatMlArraySchema,
|
||||
OpenAiMessageView,
|
||||
} from "@/src/components/trace/IOPreview";
|
||||
import { OpenAiMessageView } from "@/src/components/trace/IOPreview";
|
||||
import { Tabs, TabsList, TabsTrigger } from "@/src/components/ui/tabs";
|
||||
import { Badge } from "@/src/components/ui/badge";
|
||||
import { Button } from "@/src/components/ui/button";
|
||||
@@ -31,6 +28,7 @@ import {
|
||||
} from "@/src/components/ui/accordion";
|
||||
import { usePostHogClientCapture } from "@/src/features/posthog-analytics/usePostHogClientCapture";
|
||||
import { JumpToPlaygroundButton } from "@/src/ee/features/playground/page/components/JumpToPlaygroundButton";
|
||||
import { ChatMlArraySchema } from "@/src/components/schemas/ChatMlSchema";
|
||||
|
||||
export const PromptDetail = () => {
|
||||
const projectId = useProjectIdFromURL();
|
||||
|
||||
@@ -6,7 +6,11 @@ Implementation
|
||||
|
||||
- Wrap with `withMiddleware`
|
||||
- Type-safe and authed API Route with `createAuthedAPIRoute`
|
||||
- Add zod types to `/features/public-api/types` folder. Use [`coerce`](https://zod.dev/?id=coercion-for-primitives) to handle primitives, such as dates, for use in your application and tests.
|
||||
- Add zod types to `/features/public-api/types` folder.
|
||||
|
||||
- Use [`coerce`](https://zod.dev/?id=coercion-for-primitives) to handle primitives, such as dates, for use in your application and tests.
|
||||
- Use `strict()` on all objects that should not return additional properties. Recommended as default. In these cases, the test utility `makeZodVerifiedAPICall` will throw an error if the response contains additional properties. Also, we will log an error in production if the response contains additional properties.
|
||||
|
||||
- Throw errors defined in `shared/src/errors` which translate to HTTP status codes
|
||||
|
||||
Testing
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { type NextApiRequest, type NextApiResponse } from "next";
|
||||
import { type ZodType, ZodObject, type z } from "zod";
|
||||
import { type ZodType, type z } from "zod";
|
||||
import * as Sentry from "@sentry/node";
|
||||
import {
|
||||
verifyAuthHeaderAndReturnScope,
|
||||
@@ -74,17 +74,10 @@ export const createAuthedAPIRoute = <
|
||||
});
|
||||
|
||||
if (routeConfig.responseSchema) {
|
||||
try {
|
||||
// If the response schema is an object, we need to call strict() to ensure that the response object doesn't have any extra keys
|
||||
const responseSchema = routeConfig.responseSchema;
|
||||
if (responseSchema instanceof ZodObject) {
|
||||
responseSchema.strict().parse(response);
|
||||
} else {
|
||||
responseSchema.parse(response);
|
||||
}
|
||||
} catch (error: unknown) {
|
||||
console.error("Response validation failed:", error);
|
||||
Sentry.captureException(error);
|
||||
const parsingResult = routeConfig.responseSchema.safeParse(response);
|
||||
if (!parsingResult.success) {
|
||||
console.error("Response validation failed:", parsingResult.error);
|
||||
Sentry.captureException(parsingResult.error);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,25 +0,0 @@
|
||||
import { type ObservationView } from "@langfuse/shared";
|
||||
|
||||
// This function exists to endure backwards compatibility with GET API consumers
|
||||
// Old APIs expect to return promptTokens, completionTokens, totalTokens
|
||||
// New APIs expect to return usage: { input, output, total, unit }
|
||||
// We decided to return both formats to not break anything. In the future, this function
|
||||
// will take a variable passed from a query param to determine which API version to return.
|
||||
export const mapUsageOutput = (observation: ObservationView) => {
|
||||
const { promptTokens, completionTokens, totalTokens, unit } = observation;
|
||||
return {
|
||||
...observation,
|
||||
inputPrice: observation.inputPrice?.toNumber(),
|
||||
outputPrice: observation.outputPrice?.toNumber(),
|
||||
totalPrice: observation.totalPrice?.toNumber(),
|
||||
calculatedInputCost: observation.calculatedInputCost?.toNumber(),
|
||||
calculatedOutputCost: observation.calculatedOutputCost?.toNumber(),
|
||||
calculatedTotalCost: observation.calculatedTotalCost?.toNumber(),
|
||||
usage: {
|
||||
unit,
|
||||
input: promptTokens,
|
||||
output: completionTokens,
|
||||
total: totalTokens,
|
||||
},
|
||||
};
|
||||
};
|
||||
@@ -5,7 +5,7 @@ import { z } from "zod";
|
||||
import { BaseError, MethodNotAllowedError } from "@langfuse/shared";
|
||||
import * as Sentry from "@sentry/node";
|
||||
|
||||
const httpMethods = ["GET", "POST", "PUT", "DELETE"] as const;
|
||||
const httpMethods = ["GET", "POST", "PUT", "DELETE", "PATCH"] as const;
|
||||
export type HttpMethod = (typeof httpMethods)[number];
|
||||
type Handlers = {
|
||||
[Method in HttpMethod]?: (
|
||||
@@ -32,6 +32,7 @@ export function withMiddlewares(handlers: Handlers) {
|
||||
POST: defaultHandler,
|
||||
PUT: defaultHandler,
|
||||
DELETE: defaultHandler,
|
||||
PATCH: defaultHandler,
|
||||
},
|
||||
...handlers,
|
||||
};
|
||||
@@ -40,9 +41,10 @@ export function withMiddlewares(handlers: Handlers) {
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
|
||||
Sentry.captureException(error);
|
||||
|
||||
if (error instanceof BaseError) {
|
||||
if (error.httpCode >= 500 && error.httpCode < 600) {
|
||||
Sentry.captureException(error);
|
||||
}
|
||||
return res.status(error.httpCode).json({
|
||||
message: error.message,
|
||||
error: error.name,
|
||||
@@ -50,6 +52,7 @@ export function withMiddlewares(handlers: Handlers) {
|
||||
}
|
||||
|
||||
if (isPrismaException(error)) {
|
||||
Sentry.captureException(error);
|
||||
return res.status(500).json({
|
||||
message: "Internal Server Error",
|
||||
error: "An unknown error occurred",
|
||||
@@ -63,6 +66,7 @@ export function withMiddlewares(handlers: Handlers) {
|
||||
});
|
||||
}
|
||||
|
||||
Sentry.captureException(error);
|
||||
return res.status(500).json({
|
||||
message: "Internal Server Error",
|
||||
error:
|
||||
|
||||
@@ -10,51 +10,59 @@ import { z } from "zod";
|
||||
* Objects
|
||||
*/
|
||||
|
||||
const Dataset = z.object({
|
||||
id: z.string(),
|
||||
projectId: z.string(),
|
||||
name: z.string(),
|
||||
description: z.string().nullable(),
|
||||
metadata: z.any(),
|
||||
createdAt: z.coerce.date(),
|
||||
updatedAt: z.coerce.date(),
|
||||
});
|
||||
const Dataset = z
|
||||
.object({
|
||||
id: z.string(),
|
||||
projectId: z.string(),
|
||||
name: z.string(),
|
||||
description: z.string().nullable(),
|
||||
metadata: z.any(),
|
||||
createdAt: z.coerce.date(),
|
||||
updatedAt: z.coerce.date(),
|
||||
})
|
||||
.strict();
|
||||
|
||||
const DatasetRun = z.object({
|
||||
datasetName: z.string(),
|
||||
id: z.string(),
|
||||
name: z.string(),
|
||||
description: z.string().nullable(),
|
||||
metadata: z.any(), // Assuming Prisma.JsonValue is any type
|
||||
datasetId: z.string(),
|
||||
createdAt: z.coerce.date(),
|
||||
updatedAt: z.coerce.date(),
|
||||
});
|
||||
const DatasetRun = z
|
||||
.object({
|
||||
datasetName: z.string(),
|
||||
id: z.string(),
|
||||
name: z.string(),
|
||||
description: z.string().nullable(),
|
||||
metadata: z.any(), // Assuming Prisma.JsonValue is any type
|
||||
datasetId: z.string(),
|
||||
createdAt: z.coerce.date(),
|
||||
updatedAt: z.coerce.date(),
|
||||
})
|
||||
.strict();
|
||||
|
||||
const DatasetRunItem = z.object({
|
||||
datasetRunName: z.string(),
|
||||
id: z.string(),
|
||||
datasetRunId: z.string(),
|
||||
datasetItemId: z.string(),
|
||||
traceId: z.string(),
|
||||
observationId: z.string().nullable(),
|
||||
createdAt: z.coerce.date(),
|
||||
updatedAt: z.coerce.date(),
|
||||
});
|
||||
const DatasetRunItem = z
|
||||
.object({
|
||||
datasetRunName: z.string(),
|
||||
id: z.string(),
|
||||
datasetRunId: z.string(),
|
||||
datasetItemId: z.string(),
|
||||
traceId: z.string(),
|
||||
observationId: z.string().nullable(),
|
||||
createdAt: z.coerce.date(),
|
||||
updatedAt: z.coerce.date(),
|
||||
})
|
||||
.strict();
|
||||
|
||||
const DatasetItem = z.object({
|
||||
datasetName: z.string(),
|
||||
id: z.string(),
|
||||
status: z.enum(["ACTIVE", "ARCHIVED"]),
|
||||
input: z.any(), // Assuming Prisma.JsonValue is any type
|
||||
expectedOutput: z.any(), // Assuming Prisma.JsonValue is any type
|
||||
metadata: z.any(), // Assuming Prisma.JsonValue is any type
|
||||
sourceTraceId: z.string().nullable(),
|
||||
sourceObservationId: z.string().nullable(),
|
||||
datasetId: z.string(),
|
||||
createdAt: z.coerce.date(),
|
||||
updatedAt: z.coerce.date(),
|
||||
});
|
||||
const DatasetItem = z
|
||||
.object({
|
||||
datasetName: z.string(),
|
||||
id: z.string(),
|
||||
status: z.enum(["ACTIVE", "ARCHIVED"]),
|
||||
input: z.any(), // Assuming Prisma.JsonValue is any type
|
||||
expectedOutput: z.any(), // Assuming Prisma.JsonValue is any type
|
||||
metadata: z.any(), // Assuming Prisma.JsonValue is any type
|
||||
sourceTraceId: z.string().nullable(),
|
||||
sourceObservationId: z.string().nullable(),
|
||||
datasetId: z.string(),
|
||||
createdAt: z.coerce.date(),
|
||||
updatedAt: z.coerce.date(),
|
||||
})
|
||||
.strict();
|
||||
|
||||
/**
|
||||
* Endpoints
|
||||
@@ -66,32 +74,36 @@ export const PostDatasetsV2Body = z.object({
|
||||
description: z.string().nullish(),
|
||||
metadata: jsonSchema.nullish(),
|
||||
});
|
||||
export const PostDatasetsV2Response = Dataset;
|
||||
export const PostDatasetsV2Response = Dataset.strict();
|
||||
|
||||
// GET /v2/datasets
|
||||
export const GetDatasetsV2Query = z.object({
|
||||
...paginationZod,
|
||||
});
|
||||
export const GetDatasetsV2Response = z.object({
|
||||
data: z.array(Dataset),
|
||||
meta: paginationMetaResponseZod,
|
||||
});
|
||||
export const GetDatasetsV2Response = z
|
||||
.object({
|
||||
data: z.array(Dataset),
|
||||
meta: paginationMetaResponseZod,
|
||||
})
|
||||
.strict();
|
||||
|
||||
// GET /v2/datasets/{datasetName}
|
||||
export const GetDatasetV2Query = z.object({
|
||||
datasetName: queryStringZod,
|
||||
});
|
||||
export const GetDatasetV2Response = Dataset;
|
||||
export const GetDatasetV2Response = Dataset.strict();
|
||||
|
||||
// GET /datasets/{name}/runs
|
||||
export const GetDatasetRunsV1Query = z.object({
|
||||
name: queryStringZod, // dataset name from URL, name as it is v1
|
||||
...paginationZod,
|
||||
});
|
||||
export const GetDatasetRunsV1Response = z.object({
|
||||
data: z.array(DatasetRun),
|
||||
meta: paginationMetaResponseZod,
|
||||
});
|
||||
export const GetDatasetRunsV1Response = z
|
||||
.object({
|
||||
data: z.array(DatasetRun),
|
||||
meta: paginationMetaResponseZod,
|
||||
})
|
||||
.strict();
|
||||
|
||||
// GET /datasets/{name}/runs/{runName}
|
||||
export const GetDatasetRunV1Query = z.object({
|
||||
@@ -100,7 +112,7 @@ export const GetDatasetRunV1Query = z.object({
|
||||
});
|
||||
export const GetDatasetRunV1Response = DatasetRun.extend({
|
||||
datasetRunItems: z.array(DatasetRunItem),
|
||||
});
|
||||
}).strict();
|
||||
|
||||
// POST /dataset-items
|
||||
export const PostDatasetItemsV1Body = z.object({
|
||||
@@ -113,7 +125,7 @@ export const PostDatasetItemsV1Body = z.object({
|
||||
sourceObservationId: z.string().nullish(),
|
||||
status: z.enum(["ACTIVE", "ARCHIVED"]).nullish(),
|
||||
});
|
||||
export const PostDatasetItemsV1Response = DatasetItem;
|
||||
export const PostDatasetItemsV1Response = DatasetItem.strict();
|
||||
|
||||
// GET /dataset-items
|
||||
export const GetDatasetItemsV1Query = z.object({
|
||||
@@ -122,16 +134,18 @@ export const GetDatasetItemsV1Query = z.object({
|
||||
sourceObservationId: z.string().nullish(),
|
||||
...paginationZod,
|
||||
});
|
||||
export const GetDatasetItemsV1Response = z.object({
|
||||
data: z.array(DatasetItem),
|
||||
meta: paginationMetaResponseZod,
|
||||
});
|
||||
export const GetDatasetItemsV1Response = z
|
||||
.object({
|
||||
data: z.array(DatasetItem),
|
||||
meta: paginationMetaResponseZod,
|
||||
})
|
||||
.strict();
|
||||
|
||||
// GET /dataset-items/{datasetItemId}
|
||||
export const GetDatasetItemV1Query = z.object({
|
||||
datasetItemId: z.string(),
|
||||
});
|
||||
export const GetDatasetItemV1Response = DatasetItem;
|
||||
export const GetDatasetItemV1Response = DatasetItem.strict();
|
||||
|
||||
// POST /dataset-run-items
|
||||
export const PostDatasetRunItemsV1Body = z
|
||||
@@ -148,7 +162,7 @@ export const PostDatasetRunItemsV1Body = z
|
||||
message: "observationId or traceId must be provided",
|
||||
path: ["observationId", "traceId"], // Specify the path of the error
|
||||
});
|
||||
export const PostDatasetRunItemsV1Response = DatasetRunItem;
|
||||
export const PostDatasetRunItemsV1Response = DatasetRunItem.strict();
|
||||
|
||||
/**
|
||||
* Deprecated endpoints replaced with v2, available for backward compatibility
|
||||
@@ -163,21 +177,23 @@ export const PostDatasetsV1Body = z.object({
|
||||
export const PostDatasetsV1Response = Dataset.extend({
|
||||
items: z.array(DatasetItem),
|
||||
runs: z.array(DatasetRun),
|
||||
});
|
||||
}).strict();
|
||||
|
||||
// GET /datasets
|
||||
export const GetDatasetsV1Query = z.object({
|
||||
...paginationZod,
|
||||
});
|
||||
export const GetDatasetsV1Response = z.object({
|
||||
data: z.array(
|
||||
Dataset.extend({
|
||||
items: z.array(z.string()), // dataset item ids
|
||||
runs: z.array(z.string()), // dataset run names
|
||||
}),
|
||||
),
|
||||
meta: paginationMetaResponseZod,
|
||||
});
|
||||
export const GetDatasetsV1Response = z
|
||||
.object({
|
||||
data: z.array(
|
||||
Dataset.extend({
|
||||
items: z.array(z.string()), // dataset item ids
|
||||
runs: z.array(z.string()), // dataset run names
|
||||
}),
|
||||
),
|
||||
meta: paginationMetaResponseZod,
|
||||
})
|
||||
.strict();
|
||||
|
||||
// GET /datasets/{name}
|
||||
export const GetDatasetV1Query = z.object({
|
||||
@@ -186,4 +202,4 @@ export const GetDatasetV1Query = z.object({
|
||||
export const GetDatasetV1Response = Dataset.extend({
|
||||
items: z.array(DatasetItem),
|
||||
runs: z.array(z.string()), // dataset run names
|
||||
});
|
||||
}).strict();
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
import { z } from "zod";
|
||||
import {
|
||||
CreateEventEvent,
|
||||
eventTypes,
|
||||
type ingestionApiSchema,
|
||||
} from "@langfuse/shared";
|
||||
import { v4 as uuidv4 } from "uuid";
|
||||
|
||||
/**
|
||||
* Transforms
|
||||
*/
|
||||
|
||||
export const transformEventToIngestionBatch = (
|
||||
event: z.infer<typeof CreateEventEvent>,
|
||||
): z.infer<typeof ingestionApiSchema>["batch"] => {
|
||||
return [
|
||||
{
|
||||
id: uuidv4(),
|
||||
type: eventTypes.OBSERVATION_CREATE,
|
||||
timestamp: new Date().toISOString(),
|
||||
body: {
|
||||
...event,
|
||||
type: "EVENT",
|
||||
},
|
||||
},
|
||||
];
|
||||
};
|
||||
|
||||
/**
|
||||
* Endpoints
|
||||
*/
|
||||
|
||||
// POST /events
|
||||
export const PostEventsV1Body = CreateEventEvent;
|
||||
export const PostEventsV1Response = z.object({ id: z.string() });
|
||||
@@ -0,0 +1,72 @@
|
||||
// src/features/public-api/types/generations.ts
|
||||
|
||||
import { z } from "zod";
|
||||
import {
|
||||
LegacyGenerationsCreateSchema,
|
||||
LegacyGenerationPatchSchema,
|
||||
eventTypes,
|
||||
type ingestionApiSchema,
|
||||
} from "@langfuse/shared";
|
||||
import { v4 as uuidv4 } from "uuid";
|
||||
|
||||
/**
|
||||
* Transforms
|
||||
*/
|
||||
|
||||
export const transformGenerationPostToIngestionBatch = ({
|
||||
prompt,
|
||||
completion,
|
||||
...generation
|
||||
}: z.infer<typeof LegacyGenerationsCreateSchema>): z.infer<
|
||||
typeof ingestionApiSchema
|
||||
>["batch"] => {
|
||||
return [
|
||||
{
|
||||
id: uuidv4(),
|
||||
type: eventTypes.OBSERVATION_CREATE,
|
||||
timestamp: new Date().toISOString(),
|
||||
body: {
|
||||
...generation,
|
||||
type: "GENERATION",
|
||||
input: prompt,
|
||||
output: completion,
|
||||
},
|
||||
},
|
||||
];
|
||||
};
|
||||
|
||||
export const transformGenerationPatchToIngestionBatch = ({
|
||||
generationId,
|
||||
prompt,
|
||||
completion,
|
||||
...generation
|
||||
}: z.infer<typeof LegacyGenerationPatchSchema>): z.infer<
|
||||
typeof ingestionApiSchema
|
||||
>["batch"] => {
|
||||
return [
|
||||
{
|
||||
id: uuidv4(),
|
||||
type: eventTypes.OBSERVATION_UPDATE,
|
||||
timestamp: new Date().toISOString(),
|
||||
body: {
|
||||
...generation,
|
||||
id: generationId,
|
||||
type: "GENERATION",
|
||||
input: prompt,
|
||||
output: completion,
|
||||
},
|
||||
},
|
||||
];
|
||||
};
|
||||
|
||||
/**
|
||||
* Endpoints
|
||||
*/
|
||||
|
||||
// POST /generations
|
||||
export const PostGenerationsV1Body = LegacyGenerationsCreateSchema;
|
||||
export const PostGenerationsV1Response = z.object({ id: z.string() });
|
||||
|
||||
// PATCH /generations
|
||||
export const PatchGenerationsV1Body = LegacyGenerationPatchSchema;
|
||||
export const PatchGenerationsV1Response = z.object({ id: z.string() });
|
||||
@@ -0,0 +1,48 @@
|
||||
import {
|
||||
paginationMetaResponseZod,
|
||||
paginationZod,
|
||||
stringDateTime,
|
||||
} from "@langfuse/shared";
|
||||
import { z } from "zod";
|
||||
|
||||
/**
|
||||
* Endpoints
|
||||
*/
|
||||
|
||||
// Get /metrics/daily
|
||||
export const GetMetricsDailyV1Query = z.object({
|
||||
...paginationZod,
|
||||
traceName: z.string().nullish(),
|
||||
userId: z.string().nullish(),
|
||||
tags: z.union([z.array(z.string()), z.string()]).nullish(),
|
||||
fromTimestamp: stringDateTime,
|
||||
toTimestamp: stringDateTime,
|
||||
});
|
||||
export const GetMetricsDailyV1Response = z
|
||||
.object({
|
||||
data: z.array(
|
||||
z
|
||||
.object({
|
||||
date: z.string().date(),
|
||||
countTraces: z.number(),
|
||||
countObservations: z.number(),
|
||||
totalCost: z.number(),
|
||||
usage: z.array(
|
||||
z
|
||||
.object({
|
||||
model: z.string().nullable(),
|
||||
inputUsage: z.number(),
|
||||
outputUsage: z.number(),
|
||||
totalUsage: z.number(),
|
||||
countObservations: z.number(),
|
||||
countTraces: z.number(),
|
||||
totalCost: z.number(),
|
||||
})
|
||||
.strict(),
|
||||
),
|
||||
})
|
||||
.strict(),
|
||||
),
|
||||
meta: paginationMetaResponseZod,
|
||||
})
|
||||
.strict();
|
||||
@@ -1,5 +1,5 @@
|
||||
import {
|
||||
type ModelUsageUnit,
|
||||
type ModelUsageUnit as PrismaModelUsageUnit,
|
||||
paginationMetaResponseZod,
|
||||
paginationZod,
|
||||
type Model as PrismaModel,
|
||||
@@ -11,20 +11,31 @@ import { z } from "zod";
|
||||
* Objects
|
||||
*/
|
||||
|
||||
const ModelDefinition = z.object({
|
||||
id: z.string(),
|
||||
modelName: z.string(),
|
||||
matchPattern: z.string(),
|
||||
startDate: z.coerce.date().nullable(),
|
||||
inputPrice: z.number().nonnegative().nullable(),
|
||||
outputPrice: z.number().nonnegative().nullable(),
|
||||
totalPrice: z.number().nonnegative().nullable(),
|
||||
unit: z.enum(["TOKENS", "CHARACTERS", "MILLISECONDS", "SECONDS", "IMAGES"]),
|
||||
tokenizerId: z.string().nullable(),
|
||||
tokenizerConfig: z.any(), // Assuming Prisma.JsonValue is any type
|
||||
isLangfuseManaged: z.boolean(),
|
||||
createdAt: z.coerce.date(),
|
||||
});
|
||||
const APIModelUsageUnit = z.enum([
|
||||
"TOKENS",
|
||||
"CHARACTERS",
|
||||
"MILLISECONDS",
|
||||
"SECONDS",
|
||||
"REQUESTS",
|
||||
"IMAGES",
|
||||
]);
|
||||
|
||||
const APIModelDefinition = z
|
||||
.object({
|
||||
id: z.string(),
|
||||
modelName: z.string(),
|
||||
matchPattern: z.string(),
|
||||
startDate: z.coerce.date().nullable(),
|
||||
inputPrice: z.number().nonnegative().nullable(),
|
||||
outputPrice: z.number().nonnegative().nullable(),
|
||||
totalPrice: z.number().nonnegative().nullable(),
|
||||
unit: APIModelUsageUnit,
|
||||
tokenizerId: z.string().nullable(),
|
||||
tokenizerConfig: z.any(), // Assuming Prisma.JsonValue is any type
|
||||
isLangfuseManaged: z.boolean(),
|
||||
createdAt: z.coerce.date(),
|
||||
})
|
||||
.strict();
|
||||
|
||||
/**
|
||||
* Transforms
|
||||
@@ -39,10 +50,10 @@ export function prismaToApiModelDefinition({
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
updatedAt,
|
||||
...model
|
||||
}: PrismaModel): z.infer<typeof ModelDefinition> {
|
||||
}: PrismaModel): z.infer<typeof APIModelDefinition> {
|
||||
return {
|
||||
...model,
|
||||
unit: unit as ModelUsageUnit,
|
||||
unit: unit as PrismaModelUsageUnit,
|
||||
inputPrice: inputPrice?.toNumber() ?? null,
|
||||
outputPrice: outputPrice?.toNumber() ?? null,
|
||||
totalPrice: totalPrice?.toNumber() ?? null,
|
||||
@@ -58,10 +69,12 @@ export function prismaToApiModelDefinition({
|
||||
export const GetModelsV1Query = z.object({
|
||||
...paginationZod,
|
||||
});
|
||||
export const GetModelsV1Response = z.object({
|
||||
data: z.array(ModelDefinition),
|
||||
meta: paginationMetaResponseZod,
|
||||
});
|
||||
export const GetModelsV1Response = z
|
||||
.object({
|
||||
data: z.array(APIModelDefinition),
|
||||
meta: paginationMetaResponseZod,
|
||||
})
|
||||
.strict();
|
||||
|
||||
// POST /models
|
||||
export const PostModelsV1Body = z
|
||||
@@ -72,7 +85,7 @@ export const PostModelsV1Body = z
|
||||
inputPrice: z.number().nonnegative().nullish(),
|
||||
outputPrice: z.number().nonnegative().nullish(),
|
||||
totalPrice: z.number().nonnegative().nullish(),
|
||||
unit: z.enum(["TOKENS", "CHARACTERS", "MILLISECONDS", "SECONDS", "IMAGES"]),
|
||||
unit: APIModelUsageUnit,
|
||||
tokenizerId: z.enum(["openai", "claude"]).nullish(),
|
||||
tokenizerConfig: jsonSchema.nullish(), // Assuming Prisma.JsonValue is any type
|
||||
})
|
||||
@@ -88,18 +101,20 @@ export const PostModelsV1Body = z
|
||||
message: "If input and/or output price is set, total price must be null",
|
||||
},
|
||||
);
|
||||
export const PostModelsV1Response = ModelDefinition;
|
||||
export const PostModelsV1Response = APIModelDefinition.strict();
|
||||
|
||||
// GET /models/{modelId}
|
||||
export const GetModelV1Query = z.object({
|
||||
modelId: z.string(),
|
||||
});
|
||||
export const GetModelV1Response = ModelDefinition;
|
||||
export const GetModelV1Response = APIModelDefinition.strict();
|
||||
|
||||
// DELETE /models/{modelId}
|
||||
export const DeleteModelV1Query = z.object({
|
||||
modelId: z.string(),
|
||||
});
|
||||
export const DeleteModelV1Response = z.object({
|
||||
message: z.literal("Model successfully deleted"),
|
||||
});
|
||||
export const DeleteModelV1Response = z
|
||||
.object({
|
||||
message: z.literal("Model successfully deleted"),
|
||||
})
|
||||
.strict();
|
||||
|
||||
@@ -0,0 +1,133 @@
|
||||
import {
|
||||
type ObservationView,
|
||||
paginationMetaResponseZod,
|
||||
paginationZod,
|
||||
stringDateTime,
|
||||
} from "@langfuse/shared";
|
||||
import { z } from "zod";
|
||||
|
||||
/**
|
||||
* Objects
|
||||
*/
|
||||
|
||||
const ObservationType = z.enum(["GENERATION", "SPAN", "EVENT"]);
|
||||
|
||||
export const APIObservation = z
|
||||
.object({
|
||||
id: z.string(),
|
||||
projectId: z.string(),
|
||||
traceId: z.string().nullable(),
|
||||
parentObservationId: z.string().nullable(),
|
||||
name: z.string().nullable(),
|
||||
type: ObservationType,
|
||||
startTime: z.coerce.date(),
|
||||
endTime: z.coerce.date().nullable(),
|
||||
version: z.string().nullable(),
|
||||
createdAt: z.coerce.date(),
|
||||
updatedAt: z.coerce.date(),
|
||||
input: z.any(),
|
||||
output: z.any(),
|
||||
metadata: z.any(),
|
||||
level: z.enum(["DEBUG", "DEFAULT", "WARNING", "ERROR"]),
|
||||
statusMessage: z.string().nullable(),
|
||||
|
||||
// GENERATION only
|
||||
model: z.string().nullable(),
|
||||
modelParameters: z.any(),
|
||||
completionStartTime: z.coerce.date().nullable(),
|
||||
promptId: z.string().nullable(),
|
||||
|
||||
// usage
|
||||
usage: z.object({
|
||||
unit: z.string().nullable(),
|
||||
input: z.number(),
|
||||
output: z.number(),
|
||||
total: z.number(),
|
||||
}),
|
||||
unit: z.string().nullable(), // backwards compatibility
|
||||
promptTokens: z.number(), // backwards compatibility
|
||||
completionTokens: z.number(), // backwards compatibility
|
||||
totalTokens: z.number(), // backwards compatibility
|
||||
|
||||
// matched model
|
||||
modelId: z.string().nullable(),
|
||||
inputPrice: z.number().nullable(),
|
||||
outputPrice: z.number().nullable(),
|
||||
totalPrice: z.number().nullable(),
|
||||
|
||||
// costs
|
||||
calculatedInputCost: z.number().nullable(),
|
||||
calculatedOutputCost: z.number().nullable(),
|
||||
calculatedTotalCost: z.number().nullable(),
|
||||
|
||||
// metrics
|
||||
latency: z.number().nullable(),
|
||||
|
||||
// generation metrics
|
||||
timeToFirstToken: z.number().nullable(),
|
||||
})
|
||||
.strict();
|
||||
|
||||
/**
|
||||
* Transforms
|
||||
*/
|
||||
|
||||
/**
|
||||
*
|
||||
* @param observation - DB Observation
|
||||
* @returns API Observation as defined in the public API
|
||||
*/
|
||||
export const transformDbToApiObservation = (
|
||||
observation: ObservationView,
|
||||
): z.infer<typeof APIObservation> => {
|
||||
const { promptTokens, completionTokens, totalTokens, unit, ...rest } =
|
||||
observation;
|
||||
|
||||
return {
|
||||
...rest,
|
||||
unit,
|
||||
promptTokens,
|
||||
completionTokens,
|
||||
totalTokens,
|
||||
inputPrice: observation.inputPrice?.toNumber() ?? null,
|
||||
outputPrice: observation.outputPrice?.toNumber() ?? null,
|
||||
totalPrice: observation.totalPrice?.toNumber() ?? null,
|
||||
calculatedInputCost: observation.calculatedInputCost?.toNumber() ?? null,
|
||||
calculatedOutputCost: observation.calculatedOutputCost?.toNumber() ?? null,
|
||||
calculatedTotalCost: observation.calculatedTotalCost?.toNumber() ?? null,
|
||||
usage: {
|
||||
unit,
|
||||
input: promptTokens,
|
||||
output: completionTokens,
|
||||
total: totalTokens,
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Endpoints
|
||||
*/
|
||||
|
||||
// GET /observations
|
||||
export const GetObservationsV1Query = z.object({
|
||||
...paginationZod,
|
||||
type: ObservationType.nullish(),
|
||||
name: z.string().nullish(),
|
||||
userId: z.string().nullish(),
|
||||
traceId: z.string().nullish(),
|
||||
parentObservationId: z.string().nullish(),
|
||||
fromStartTime: stringDateTime,
|
||||
toStartTime: stringDateTime,
|
||||
});
|
||||
export const GetObservationsV1Response = z
|
||||
.object({
|
||||
data: z.array(APIObservation),
|
||||
meta: paginationMetaResponseZod,
|
||||
})
|
||||
.strict();
|
||||
|
||||
// GET /observations/{observationId}
|
||||
export const GetObservationV1Query = z.object({
|
||||
observationId: z.string(),
|
||||
});
|
||||
export const GetObservationV1Response = APIObservation;
|
||||
@@ -8,6 +8,12 @@ import {
|
||||
import { z } from "zod";
|
||||
import * as Sentry from "@sentry/node";
|
||||
|
||||
/**
|
||||
* Types to use across codebase
|
||||
*/
|
||||
export type ConfigCategory = z.infer<typeof Category>;
|
||||
export type ValidatedScoreConfig = z.infer<typeof ValidatedScoreConfigSchema>;
|
||||
|
||||
const validateCategories = (
|
||||
categories: ConfigCategory[],
|
||||
ctx: z.RefinementCtx,
|
||||
@@ -44,8 +50,6 @@ export const Category = z.object({
|
||||
value: z.number(),
|
||||
});
|
||||
|
||||
export type ConfigCategory = z.infer<typeof Category>;
|
||||
|
||||
const Categories = z.array(Category);
|
||||
|
||||
const NumericScoreConfig = z.object({
|
||||
@@ -137,8 +141,12 @@ const ValidatedScoreConfigSchema = z
|
||||
}
|
||||
});
|
||||
|
||||
export type ValidatedScoreConfig = z.infer<typeof ValidatedScoreConfigSchema>;
|
||||
|
||||
/**
|
||||
* Use this function when pulling a list of score configs from the database before using in the application to ensure type safety.
|
||||
* All score configs are expected to pass the validation. If a score fails validation, it will be logged to Sentry.
|
||||
* @param scoreConfigs
|
||||
* @returns list of validated score configs
|
||||
*/
|
||||
export const filterAndValidateDbScoreConfigList = (
|
||||
scoreConfigs: ScoreConfigDbType[],
|
||||
): ValidatedScoreConfig[] =>
|
||||
@@ -152,10 +160,26 @@ export const filterAndValidateDbScoreConfigList = (
|
||||
return acc;
|
||||
}, [] as ValidatedScoreConfig[]);
|
||||
|
||||
/**
|
||||
* Use this function when pulling a single score config from the database before using in the application to ensure type safety.
|
||||
* The score is expected to pass the validation. If a score fails validation, an error will be thrown.
|
||||
* @param scoreConfig
|
||||
* @returns validated score config
|
||||
* @throws error if score fails validation
|
||||
*/
|
||||
export const validateDbScoreConfig = (
|
||||
scoreConfig: ScoreConfigDbType,
|
||||
): ValidatedScoreConfig => ValidatedScoreConfigSchema.parse(scoreConfig);
|
||||
|
||||
/**
|
||||
* Use this function when pulling a single score config from the database before using in the application to ensure type safety.
|
||||
* This function will NOT throw an error by default. The score is expected to pass the validation.
|
||||
* @param scoreConfig
|
||||
* @returns score config validation object:
|
||||
* - success: true if the score config passes validation
|
||||
* - data: the validated score config if success is true
|
||||
* - error: the error object if success is false
|
||||
*/
|
||||
export const validateDbScoreConfigSafe = (scoreConfig: ScoreConfigDbType) =>
|
||||
ValidatedScoreConfigSchema.safeParse(scoreConfig);
|
||||
|
||||
|
||||
@@ -4,13 +4,19 @@ import {
|
||||
paginationMetaResponseZod,
|
||||
NonEmptyString,
|
||||
type Score,
|
||||
stringDateTime,
|
||||
} from "@langfuse/shared";
|
||||
import { z } from "zod";
|
||||
import { isPresent } from "@/src/utils/typeChecks";
|
||||
import { Category as ConfigCategory } from "./score-configs";
|
||||
|
||||
/**
|
||||
* Objects
|
||||
* Types to use across codebase
|
||||
*/
|
||||
export type APIScore = z.infer<typeof APIScore>;
|
||||
|
||||
/**
|
||||
* Helpers
|
||||
*/
|
||||
|
||||
const ScoreSource = ["API", "EVAL", "ANNOTATION"] as const;
|
||||
@@ -59,36 +65,16 @@ const BaseScoreBody = z.object({
|
||||
comment: z.string().nullish(),
|
||||
});
|
||||
|
||||
const GetScoresDataBase = z.object({
|
||||
id: z.string(),
|
||||
timestamp: z.coerce.date(),
|
||||
name: z.string(),
|
||||
source: z.enum(ScoreSource),
|
||||
comment: z.string().nullish(),
|
||||
traceId: z.string(),
|
||||
observationId: z.string().nullish(),
|
||||
trace: z.object({
|
||||
userId: z.string(),
|
||||
}),
|
||||
configId: z.string().nullish(),
|
||||
});
|
||||
/**
|
||||
* Objects
|
||||
*/
|
||||
|
||||
export const GetScoresData = z.discriminatedUnion("dataType", [
|
||||
GetScoresDataBase.merge(NumericData),
|
||||
GetScoresDataBase.merge(CategoricalData),
|
||||
GetScoresDataBase.merge(BooleanData),
|
||||
]);
|
||||
|
||||
const ValidatedScoreSchema = z.discriminatedUnion("dataType", [
|
||||
export const APIScore = z.discriminatedUnion("dataType", [
|
||||
ScoreBase.merge(NumericData),
|
||||
ScoreBase.merge(CategoricalData),
|
||||
ScoreBase.merge(BooleanData),
|
||||
]);
|
||||
|
||||
export type ValidatedGetScoresData = z.infer<typeof GetScoresData>;
|
||||
|
||||
export type ValidatedScore = z.infer<typeof ValidatedScoreSchema>;
|
||||
|
||||
/**
|
||||
* Validation objects
|
||||
*/
|
||||
@@ -167,21 +153,33 @@ export const ScorePropsAgainstConfig = z.union([
|
||||
* Transformations
|
||||
*/
|
||||
|
||||
export const filterAndValidateDbScoreList = (
|
||||
scores: Score[],
|
||||
): ValidatedScore[] =>
|
||||
/**
|
||||
* 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 Sentry.
|
||||
* @param scores
|
||||
* @returns list of validated scores
|
||||
*/
|
||||
export const filterAndValidateDbScoreList = (scores: Score[]): APIScore[] =>
|
||||
scores.reduce((acc, ts) => {
|
||||
const result = ValidatedScoreSchema.safeParse(ts);
|
||||
const result = APIScore.safeParse(ts);
|
||||
if (result.success) {
|
||||
acc.push(result.data);
|
||||
} else {
|
||||
console.error("Score parsing error: ", result.error);
|
||||
Sentry.captureException(result.error);
|
||||
}
|
||||
return acc;
|
||||
}, [] as ValidatedScore[]);
|
||||
}, [] as APIScore[]);
|
||||
|
||||
export const validateDbScore = (score: Score): ValidatedScore =>
|
||||
ValidatedScoreSchema.parse(score);
|
||||
/**
|
||||
* Use this function when pulling a single score from the database before using in the application to ensure type safety.
|
||||
* The score is expected to pass the validation. If a score fails validation, an error will be thrown.
|
||||
* @param score
|
||||
* @returns validated score
|
||||
* @throws error if score fails validation
|
||||
*/
|
||||
export const validateDbScore = (score: Score): APIScore =>
|
||||
APIScore.parse(score);
|
||||
|
||||
/**
|
||||
* Endpoints
|
||||
@@ -249,7 +247,7 @@ export const PostScoresBody = z
|
||||
}
|
||||
});
|
||||
|
||||
export const PostScoresResponse = z.void();
|
||||
export const PostScoresResponse = z.object({ id: z.string() });
|
||||
|
||||
// GET /scores
|
||||
export const GetScoresQuery = z.object({
|
||||
@@ -258,7 +256,8 @@ export const GetScoresQuery = z.object({
|
||||
dataType: z.enum(ScoreDataType).nullish(),
|
||||
configId: z.string().nullish(),
|
||||
name: z.string().nullish(),
|
||||
fromTimestamp: z.coerce.date().nullish(),
|
||||
fromTimestamp: stringDateTime,
|
||||
toTimestamp: stringDateTime,
|
||||
source: z.enum(ScoreSource).nullish(),
|
||||
value: z.coerce.number().nullish(),
|
||||
operator: z.enum(operators).nullish(),
|
||||
@@ -271,17 +270,43 @@ export const GetScoresQuery = z.object({
|
||||
.nullish(),
|
||||
});
|
||||
|
||||
// LegacyGetScoreResponseDataV1 is only used for response of GET /scores list endpoint
|
||||
const LegacyGetScoreResponseDataV1 = z.intersection(
|
||||
APIScore,
|
||||
z.object({
|
||||
trace: z.object({
|
||||
userId: z.string().nullish(),
|
||||
}),
|
||||
}),
|
||||
);
|
||||
export const GetScoresResponse = z.object({
|
||||
data: z.array(GetScoresData),
|
||||
data: z.array(LegacyGetScoreResponseDataV1),
|
||||
meta: paginationMetaResponseZod,
|
||||
});
|
||||
|
||||
export const legacyFilterAndValidateV1GetScoreList = (
|
||||
scores: unknown[],
|
||||
): z.infer<typeof LegacyGetScoreResponseDataV1>[] =>
|
||||
scores.reduce(
|
||||
(acc: z.infer<typeof LegacyGetScoreResponseDataV1>[], ts) => {
|
||||
const result = LegacyGetScoreResponseDataV1.safeParse(ts);
|
||||
if (result.success) {
|
||||
acc.push(result.data);
|
||||
} else {
|
||||
console.error("Score parsing error: ", result.error);
|
||||
Sentry.captureException(result.error);
|
||||
}
|
||||
return acc;
|
||||
},
|
||||
[] as z.infer<typeof LegacyGetScoreResponseDataV1>[],
|
||||
);
|
||||
|
||||
// GET /scores/{scoreId}
|
||||
export const GetScoreQuery = z.object({
|
||||
scoreId: z.string(),
|
||||
});
|
||||
|
||||
export const GetScoreResponse = ValidatedScoreSchema;
|
||||
export const GetScoreResponse = APIScore;
|
||||
|
||||
// DELETE /scores/{scoreId}
|
||||
export const DeleteScoreQuery = z.object({
|
||||
|
||||
@@ -0,0 +1,44 @@
|
||||
import { APITrace } from "@/src/features/public-api/types/traces";
|
||||
import {
|
||||
paginationMetaResponseZod,
|
||||
paginationZod,
|
||||
stringDateTime,
|
||||
} from "@langfuse/shared";
|
||||
import { z } from "zod";
|
||||
|
||||
/**
|
||||
* Objects
|
||||
*/
|
||||
|
||||
const APISession = z
|
||||
.strictObject({
|
||||
id: z.string(),
|
||||
createdAt: z.coerce.date(),
|
||||
projectId: z.string(),
|
||||
})
|
||||
.strict();
|
||||
|
||||
/**
|
||||
* Endpoints
|
||||
*/
|
||||
|
||||
// GET /sessions
|
||||
export const GetSessionsV1Query = z.object({
|
||||
...paginationZod,
|
||||
fromTimestamp: stringDateTime,
|
||||
toTimestamp: stringDateTime,
|
||||
});
|
||||
export const GetSessionsV1Response = z
|
||||
.object({
|
||||
data: z.array(APISession),
|
||||
meta: paginationMetaResponseZod,
|
||||
})
|
||||
.strict();
|
||||
|
||||
// GET /sessions/:id
|
||||
export const GetSessionV1Query = z.object({
|
||||
sessionId: z.string(),
|
||||
});
|
||||
export const GetSessionV1Response = APISession.extend({
|
||||
traces: z.array(APITrace),
|
||||
}).strict();
|
||||
@@ -0,0 +1,64 @@
|
||||
import { z } from "zod";
|
||||
import {
|
||||
LegacySpanPatchSchema,
|
||||
LegacySpanPostSchema,
|
||||
eventTypes,
|
||||
type ingestionApiSchema,
|
||||
} from "@langfuse/shared";
|
||||
import { v4 } from "uuid";
|
||||
|
||||
/**
|
||||
* Objects
|
||||
*/
|
||||
|
||||
/**
|
||||
* Transforms
|
||||
*/
|
||||
|
||||
export const transformLegacySpanPostToIngestionBatch = (
|
||||
span: z.infer<typeof LegacySpanPostSchema>,
|
||||
): z.infer<typeof ingestionApiSchema>["batch"] => {
|
||||
return [
|
||||
{
|
||||
id: v4(),
|
||||
type: eventTypes.OBSERVATION_CREATE,
|
||||
timestamp: new Date().toISOString(),
|
||||
body: {
|
||||
...span,
|
||||
type: "SPAN",
|
||||
},
|
||||
},
|
||||
];
|
||||
};
|
||||
|
||||
export const transformLegacySpanPatchToIngestionBatch = ({
|
||||
spanId,
|
||||
...span
|
||||
}: z.infer<typeof LegacySpanPatchSchema>): z.infer<
|
||||
typeof ingestionApiSchema
|
||||
>["batch"] => {
|
||||
return [
|
||||
{
|
||||
id: v4(),
|
||||
type: eventTypes.OBSERVATION_UPDATE,
|
||||
timestamp: new Date().toISOString(),
|
||||
body: {
|
||||
...span,
|
||||
id: spanId,
|
||||
type: "SPAN",
|
||||
},
|
||||
},
|
||||
];
|
||||
};
|
||||
|
||||
/**
|
||||
* Endpoints
|
||||
*/
|
||||
|
||||
// POST /spans
|
||||
export const PostSpansV1Body = LegacySpanPostSchema;
|
||||
export const PostSpansV1Response = z.object({ id: z.string() });
|
||||
|
||||
// PATCH /spans
|
||||
export const PatchSpansV1Body = LegacySpanPatchSchema;
|
||||
export const PatchSpansV1Response = z.object({ id: z.string() });
|
||||
@@ -0,0 +1,87 @@
|
||||
import { APIObservation } from "@/src/features/public-api/types/observations";
|
||||
import { APIScore } from "@/src/features/public-api/types/scores";
|
||||
import {
|
||||
paginationZod,
|
||||
paginationMetaResponseZod,
|
||||
stringDateTime,
|
||||
orderBy,
|
||||
TraceBody,
|
||||
} from "@langfuse/shared";
|
||||
import { z } from "zod";
|
||||
|
||||
/**
|
||||
* Objects
|
||||
*/
|
||||
|
||||
export const APITrace = z
|
||||
.object({
|
||||
id: z.string(),
|
||||
externalId: z.string().nullable(),
|
||||
timestamp: z.coerce.date(),
|
||||
name: z.string().nullable(),
|
||||
userId: z.string().nullable(),
|
||||
metadata: z.any(), // Prisma JSON
|
||||
release: z.string().nullable(),
|
||||
version: z.string().nullable(),
|
||||
projectId: z.string(),
|
||||
public: z.boolean(),
|
||||
bookmarked: z.boolean(),
|
||||
tags: z.array(z.string()),
|
||||
input: z.any(), // Prisma JSON
|
||||
output: z.any(), // Prisma JSON
|
||||
sessionId: z.string().nullable(),
|
||||
createdAt: z.coerce.date(),
|
||||
updatedAt: z.coerce.date(),
|
||||
})
|
||||
.strict();
|
||||
|
||||
const APIExtendedTrace = APITrace.extend({
|
||||
observations: z.array(z.string()),
|
||||
scores: z.array(z.string()),
|
||||
totalCost: z.number(),
|
||||
latency: z.number(),
|
||||
htmlPath: z.string(),
|
||||
}).strict();
|
||||
|
||||
/**
|
||||
* Endpoints
|
||||
*/
|
||||
|
||||
// GET /api/public/traces
|
||||
export const GetTracesV1Query = z.object({
|
||||
...paginationZod,
|
||||
userId: z.string().nullish(),
|
||||
name: z.string().nullish(),
|
||||
tags: z.union([z.array(z.string()), z.string()]).nullish(),
|
||||
sessionId: z.string().nullish(),
|
||||
fromTimestamp: stringDateTime,
|
||||
toTimestamp: stringDateTime,
|
||||
orderBy: z
|
||||
.string() // orderBy=timestamp.asc
|
||||
.nullish()
|
||||
.transform((v) => {
|
||||
if (!v) return null;
|
||||
const [column, order] = v.split(".");
|
||||
return { column, order: order?.toUpperCase() };
|
||||
})
|
||||
.pipe(orderBy.nullish()),
|
||||
});
|
||||
export const GetTracesV1Response = z
|
||||
.object({
|
||||
data: z.array(APIExtendedTrace),
|
||||
meta: paginationMetaResponseZod,
|
||||
})
|
||||
.strict();
|
||||
|
||||
// POST /api/public/traces
|
||||
export const PostTracesV1Body = TraceBody;
|
||||
export const PostTracesV1Response = z.object({ id: z.string() });
|
||||
|
||||
// GET /api/public/traces/{traceId}
|
||||
export const GetTraceV1Query = z.object({
|
||||
traceId: z.string(),
|
||||
});
|
||||
export const GetTraceV1Response = APIExtendedTrace.extend({
|
||||
scores: z.array(APIScore),
|
||||
observations: z.array(APIObservation),
|
||||
}).strict();
|
||||
@@ -1,6 +1,6 @@
|
||||
import { useSession } from "next-auth/react";
|
||||
|
||||
export function useLookBackDays(projectId: string) {
|
||||
export function useTableLookBackDays(projectId: string) {
|
||||
const session = useSession();
|
||||
const lookBackDays =
|
||||
session.data?.environment.defaultTableDateTimeOffset ??
|
||||
@@ -1,6 +1,17 @@
|
||||
// See: https://vercel.com/docs/observability/otel-overview
|
||||
|
||||
import { shutdown } from "@/src/utils/shutdown";
|
||||
import prexit from "prexit";
|
||||
|
||||
export async function register() {
|
||||
if (process.env.NEXT_RUNTIME === "nodejs") {
|
||||
await import("./sentry.server.config");
|
||||
if (process.env.NEXT_MANUAL_SIG_HANDLE) {
|
||||
prexit(async (signal) => {
|
||||
console.log("Signal: ", signal);
|
||||
return await shutdown(signal);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (process.env.NEXT_RUNTIME === "edge") {
|
||||
|
||||
@@ -1,51 +0,0 @@
|
||||
import { NodeSDK } from "@opentelemetry/sdk-node";
|
||||
import { getNodeAutoInstrumentations } from "@opentelemetry/auto-instrumentations-node";
|
||||
import { OTLPTraceExporter } from "@opentelemetry/exporter-trace-otlp-http";
|
||||
import { Resource } from "@opentelemetry/resources";
|
||||
import { SEMRESATTRS_SERVICE_NAME } from "@opentelemetry/semantic-conventions";
|
||||
import { BatchSpanProcessor } from "@opentelemetry/sdk-trace-node";
|
||||
import { PrismaInstrumentation } from "@prisma/instrumentation";
|
||||
import {
|
||||
awsEksDetector,
|
||||
awsEc2Detector,
|
||||
} from "@opentelemetry/resource-detector-aws";
|
||||
import {
|
||||
hostDetector,
|
||||
osDetector,
|
||||
processDetector,
|
||||
} from "@opentelemetry/resources/build/src/detectors/platform";
|
||||
import { envDetector } from "@opentelemetry/resources";
|
||||
import { containerDetector } from "@opentelemetry/resource-detector-container";
|
||||
|
||||
const sdk = new NodeSDK({
|
||||
resource: new Resource({
|
||||
[SEMRESATTRS_SERVICE_NAME]: "web",
|
||||
}),
|
||||
spanProcessors: [
|
||||
new BatchSpanProcessor(
|
||||
new OTLPTraceExporter({
|
||||
url:
|
||||
process.env.OTLP_ENDPOINT ||
|
||||
"https://otlp.eu01.nr-data.net/v1/traces",
|
||||
headers: {
|
||||
"api-key": process.env.NEW_RELIC_API_KEY,
|
||||
},
|
||||
}),
|
||||
),
|
||||
],
|
||||
resourceDetectors: [
|
||||
containerDetector,
|
||||
envDetector,
|
||||
hostDetector,
|
||||
osDetector,
|
||||
processDetector,
|
||||
awsEksDetector,
|
||||
awsEc2Detector,
|
||||
],
|
||||
instrumentations: [
|
||||
getNodeAutoInstrumentations(),
|
||||
new PrismaInstrumentation(),
|
||||
],
|
||||
});
|
||||
|
||||
sdk.start();
|
||||
@@ -18,6 +18,7 @@ import { useRouter } from "next/router";
|
||||
import posthog from "posthog-js";
|
||||
import { PostHogProvider } from "posthog-js/react";
|
||||
import { CrispWidget, chatSetUser } from "@/src/features/support-chat";
|
||||
import prexit from "prexit";
|
||||
|
||||
// Custom polyfills not yet available in `next-core`:
|
||||
// https://github.com/vercel/next.js/issues/58242
|
||||
@@ -31,6 +32,7 @@ import "react18-json-view/src/style.css";
|
||||
import { DetailPageListsProvider } from "@/src/features/navigate-detail-pages/context";
|
||||
import { env } from "@/src/env.mjs";
|
||||
import { ThemeProvider } from "@/src/features/theming/ThemeProvider";
|
||||
import { shutdown } from "@/src/utils/shutdown";
|
||||
|
||||
const setProjectInPosthog = () => {
|
||||
// project
|
||||
@@ -163,3 +165,10 @@ function UserTracking() {
|
||||
}, [session]);
|
||||
return null;
|
||||
}
|
||||
|
||||
if (process.env.NEXT_MANUAL_SIG_HANDLE) {
|
||||
prexit(async (signal) => {
|
||||
console.log("Signal: ", signal);
|
||||
return await shutdown(signal);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,88 +1,29 @@
|
||||
import { type NextApiRequest, type NextApiResponse } from "next";
|
||||
import { cors, runMiddleware } from "@/src/features/public-api/server/cors";
|
||||
import { verifyAuthHeaderAndReturnScope } from "@/src/features/public-api/server/apiAuth";
|
||||
import { v4 as uuidv4 } from "uuid";
|
||||
import {
|
||||
CreateEventEvent,
|
||||
eventTypes,
|
||||
ingestionBatchEvent,
|
||||
} from "@langfuse/shared";
|
||||
PostEventsV1Body,
|
||||
PostEventsV1Response,
|
||||
transformEventToIngestionBatch,
|
||||
} from "@/src/features/public-api/types/events";
|
||||
import { withMiddlewares } from "@/src/features/public-api/server/withMiddlewares";
|
||||
import { createAuthedAPIRoute } from "@/src/features/public-api/server/createAuthedAPIRoute";
|
||||
import {
|
||||
handleBatch,
|
||||
handleBatchResultLegacy,
|
||||
parseSingleTypedIngestionApiResponse,
|
||||
} from "@/src/pages/api/public/ingestion";
|
||||
import { z } from "zod";
|
||||
import { isPrismaException } from "@/src/utils/exceptions";
|
||||
|
||||
export default async function handler(
|
||||
req: NextApiRequest,
|
||||
res: NextApiResponse,
|
||||
) {
|
||||
await runMiddleware(req, res, cors);
|
||||
|
||||
if (req.method !== "POST") {
|
||||
return res.status(405).json({ message: "Method not allowed" });
|
||||
}
|
||||
|
||||
// CHECK AUTH
|
||||
const authCheck = await verifyAuthHeaderAndReturnScope(
|
||||
req.headers.authorization,
|
||||
);
|
||||
if (!authCheck.validKey)
|
||||
return res.status(401).json({
|
||||
message: authCheck.error,
|
||||
});
|
||||
// END CHECK AUTH
|
||||
|
||||
console.log(
|
||||
"trying to create observation for event, project ",
|
||||
authCheck.scope.projectId,
|
||||
", body:",
|
||||
JSON.stringify(req.body, null, 2),
|
||||
);
|
||||
|
||||
try {
|
||||
const convertToObservation = (
|
||||
generation: z.infer<typeof CreateEventEvent>,
|
||||
) => {
|
||||
return {
|
||||
...generation,
|
||||
type: "EVENT",
|
||||
};
|
||||
};
|
||||
|
||||
const event = {
|
||||
id: uuidv4(),
|
||||
type: eventTypes.OBSERVATION_CREATE,
|
||||
timestamp: new Date().toISOString(),
|
||||
body: convertToObservation(CreateEventEvent.parse(req.body)),
|
||||
};
|
||||
|
||||
const result = await handleBatch(
|
||||
ingestionBatchEvent.parse([event]),
|
||||
{},
|
||||
req,
|
||||
authCheck,
|
||||
);
|
||||
handleBatchResultLegacy(result.errors, result.results, res);
|
||||
} catch (error: unknown) {
|
||||
console.error(error);
|
||||
if (error instanceof z.ZodError) {
|
||||
return res.status(400).json({
|
||||
message: "Invalid request data",
|
||||
error: error.errors,
|
||||
});
|
||||
}
|
||||
if (isPrismaException(error)) {
|
||||
return res.status(500).json({
|
||||
error: "Internal Server Error",
|
||||
});
|
||||
}
|
||||
const errorMessage =
|
||||
error instanceof Error ? error.message : "An unknown error occurred";
|
||||
res.status(500).json({
|
||||
message: "Invalid request data",
|
||||
error: errorMessage,
|
||||
});
|
||||
}
|
||||
}
|
||||
export default withMiddlewares({
|
||||
POST: createAuthedAPIRoute({
|
||||
name: "Create Event",
|
||||
bodySchema: PostEventsV1Body,
|
||||
responseSchema: PostEventsV1Response,
|
||||
fn: async ({ body, auth, req }) => {
|
||||
const ingestionBatch = transformEventToIngestionBatch(body);
|
||||
const result = await handleBatch(ingestionBatch, {}, req, auth);
|
||||
const response = parseSingleTypedIngestionApiResponse(
|
||||
result.errors,
|
||||
result.results,
|
||||
PostEventsV1Response,
|
||||
);
|
||||
return response;
|
||||
},
|
||||
}),
|
||||
});
|
||||
|
||||
@@ -1,166 +1,47 @@
|
||||
import { type NextApiRequest, type NextApiResponse } from "next";
|
||||
import { cors, runMiddleware } from "@/src/features/public-api/server/cors";
|
||||
import { verifyAuthHeaderAndReturnScope } from "@/src/features/public-api/server/apiAuth";
|
||||
import { v4 as uuidv4 } from "uuid";
|
||||
import { ResourceNotFoundError } from "../../../utils/exceptions";
|
||||
import {
|
||||
LegacyGenerationPatchSchema,
|
||||
LegacyGenerationsCreateSchema,
|
||||
eventTypes,
|
||||
ingestionBatchEvent,
|
||||
} from "@langfuse/shared";
|
||||
PostGenerationsV1Body,
|
||||
PostGenerationsV1Response,
|
||||
PatchGenerationsV1Body,
|
||||
PatchGenerationsV1Response,
|
||||
transformGenerationPostToIngestionBatch,
|
||||
transformGenerationPatchToIngestionBatch,
|
||||
} from "@/src/features/public-api/types/generations";
|
||||
import { withMiddlewares } from "@/src/features/public-api/server/withMiddlewares";
|
||||
import { createAuthedAPIRoute } from "@/src/features/public-api/server/createAuthedAPIRoute";
|
||||
import {
|
||||
handleBatch,
|
||||
handleBatchResultLegacy,
|
||||
parseSingleTypedIngestionApiResponse,
|
||||
} from "@/src/pages/api/public/ingestion";
|
||||
import { z } from "zod";
|
||||
import { isPrismaException } from "@/src/utils/exceptions";
|
||||
|
||||
export default async function handler(
|
||||
req: NextApiRequest,
|
||||
res: NextApiResponse,
|
||||
) {
|
||||
await runMiddleware(req, res, cors);
|
||||
|
||||
if (req.method === "POST") {
|
||||
try {
|
||||
// CHECK AUTH
|
||||
const authCheck = await verifyAuthHeaderAndReturnScope(
|
||||
req.headers.authorization,
|
||||
export default withMiddlewares({
|
||||
POST: createAuthedAPIRoute({
|
||||
name: "Create Generation (Legacy)",
|
||||
bodySchema: PostGenerationsV1Body,
|
||||
responseSchema: PostGenerationsV1Response,
|
||||
fn: async ({ body, auth, req }) => {
|
||||
const ingestionBatch = transformGenerationPostToIngestionBatch(body);
|
||||
const result = await handleBatch(ingestionBatch, {}, req, auth);
|
||||
const response = parseSingleTypedIngestionApiResponse(
|
||||
result.errors,
|
||||
result.results,
|
||||
PostGenerationsV1Response,
|
||||
);
|
||||
if (!authCheck.validKey)
|
||||
return res.status(401).json({
|
||||
message: authCheck.error,
|
||||
});
|
||||
// END CHECK AUTH
|
||||
console.log(
|
||||
"trying to create observation for generation, project ",
|
||||
authCheck.scope.projectId,
|
||||
", body:",
|
||||
JSON.stringify(req.body, null, 2),
|
||||
return response;
|
||||
},
|
||||
}),
|
||||
PATCH: createAuthedAPIRoute({
|
||||
name: "Patch Generation (Legacy)",
|
||||
bodySchema: PatchGenerationsV1Body,
|
||||
responseSchema: PatchGenerationsV1Response,
|
||||
fn: async ({ body, auth, req }) => {
|
||||
const ingestionBatch = transformGenerationPatchToIngestionBatch(body);
|
||||
const result = await handleBatch(ingestionBatch, {}, req, auth);
|
||||
const response = parseSingleTypedIngestionApiResponse(
|
||||
result.errors,
|
||||
result.results,
|
||||
PatchGenerationsV1Response,
|
||||
);
|
||||
|
||||
const convertToObservation = (
|
||||
generation: z.infer<typeof LegacyGenerationsCreateSchema>,
|
||||
) => {
|
||||
return {
|
||||
...generation,
|
||||
type: "GENERATION",
|
||||
input: generation.prompt,
|
||||
output: generation.completion,
|
||||
};
|
||||
};
|
||||
|
||||
const event = {
|
||||
id: uuidv4(),
|
||||
type: eventTypes.OBSERVATION_CREATE,
|
||||
timestamp: new Date().toISOString(),
|
||||
body: convertToObservation(
|
||||
LegacyGenerationsCreateSchema.parse(req.body),
|
||||
),
|
||||
};
|
||||
|
||||
const result = await handleBatch(
|
||||
ingestionBatchEvent.parse([event]),
|
||||
{},
|
||||
req,
|
||||
authCheck,
|
||||
);
|
||||
|
||||
handleBatchResultLegacy(result.errors, result.results, res);
|
||||
} catch (error: unknown) {
|
||||
console.error(error);
|
||||
if (error instanceof z.ZodError) {
|
||||
return res.status(400).json({
|
||||
message: "Invalid request data",
|
||||
error: error.errors,
|
||||
});
|
||||
}
|
||||
if (isPrismaException(error)) {
|
||||
return res.status(500).json({
|
||||
error: "Internal Server Error",
|
||||
});
|
||||
}
|
||||
const errorMessage =
|
||||
error instanceof Error ? error.message : "An unknown error occurred";
|
||||
res.status(500).json({
|
||||
message: "Invalid request data",
|
||||
error: errorMessage,
|
||||
});
|
||||
}
|
||||
} else if (req.method === "PATCH") {
|
||||
try {
|
||||
// CHECK AUTH
|
||||
const authCheck = await verifyAuthHeaderAndReturnScope(
|
||||
req.headers.authorization,
|
||||
);
|
||||
if (!authCheck.validKey)
|
||||
return res.status(401).json({
|
||||
message: authCheck.error,
|
||||
});
|
||||
// END CHECK AUTH
|
||||
console.log(
|
||||
"trying to update observation for generation, project ",
|
||||
authCheck.scope.projectId,
|
||||
", body:",
|
||||
JSON.stringify(req.body, null, 2),
|
||||
);
|
||||
|
||||
const convertToObservation = (
|
||||
generation: z.infer<typeof LegacyGenerationPatchSchema>,
|
||||
) => {
|
||||
return {
|
||||
...generation,
|
||||
id: generation.generationId,
|
||||
type: "GENERATION",
|
||||
input: generation.prompt,
|
||||
output: generation.completion,
|
||||
};
|
||||
};
|
||||
|
||||
const event = {
|
||||
id: uuidv4(),
|
||||
type: eventTypes.OBSERVATION_UPDATE,
|
||||
timestamp: new Date().toISOString(),
|
||||
body: convertToObservation(LegacyGenerationPatchSchema.parse(req.body)),
|
||||
};
|
||||
|
||||
const result = await handleBatch(
|
||||
ingestionBatchEvent.parse([event]),
|
||||
{},
|
||||
req,
|
||||
authCheck,
|
||||
);
|
||||
|
||||
handleBatchResultLegacy(result.errors, result.results, res);
|
||||
} catch (error: unknown) {
|
||||
console.error(error);
|
||||
if (error instanceof z.ZodError) {
|
||||
return res.status(400).json({
|
||||
message: "Invalid request data",
|
||||
error: error.errors,
|
||||
});
|
||||
}
|
||||
if (isPrismaException(error)) {
|
||||
return res.status(500).json({
|
||||
error: "Internal Server Error",
|
||||
});
|
||||
}
|
||||
|
||||
if (error instanceof ResourceNotFoundError) {
|
||||
return res.status(404).json({
|
||||
message: "Observation not found",
|
||||
});
|
||||
}
|
||||
|
||||
const errorMessage =
|
||||
error instanceof Error ? error.message : "An unknown error occurred";
|
||||
res.status(500).json({
|
||||
message: "Invalid request data",
|
||||
error: errorMessage,
|
||||
});
|
||||
}
|
||||
} else {
|
||||
return res.status(405).json({ message: "Method not allowed" });
|
||||
}
|
||||
}
|
||||
return response;
|
||||
},
|
||||
}),
|
||||
});
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { VERSION } from "@/src/constants";
|
||||
import { cors, runMiddleware } from "@/src/features/public-api/server/cors";
|
||||
import { telemetry } from "@/src/features/telemetry";
|
||||
import { isSigtermReceived } from "@/src/utils/shutdown";
|
||||
import { prisma } from "@langfuse/shared/src/db";
|
||||
import { type NextApiRequest, type NextApiResponse } from "next";
|
||||
|
||||
@@ -8,53 +9,71 @@ export default async function handler(
|
||||
req: NextApiRequest,
|
||||
res: NextApiResponse,
|
||||
) {
|
||||
await runMiddleware(req, res, cors);
|
||||
await telemetry();
|
||||
const failIfNoRecentEvents = req.query.failIfNoRecentEvents === "true";
|
||||
|
||||
try {
|
||||
await prisma.$queryRaw`SELECT 1;`;
|
||||
await runMiddleware(req, res, cors);
|
||||
await telemetry();
|
||||
const failIfNoRecentEvents = req.query.failIfNoRecentEvents === "true";
|
||||
|
||||
if (failIfNoRecentEvents) {
|
||||
const now = Date.now();
|
||||
const trace = await prisma.trace.findFirst({
|
||||
where: {
|
||||
timestamp: {
|
||||
gte: new Date(now - 180000), // 3 minutes ago
|
||||
lte: new Date(now),
|
||||
},
|
||||
},
|
||||
select: {
|
||||
id: true,
|
||||
},
|
||||
});
|
||||
const observation = await prisma.observation.findFirst({
|
||||
where: {
|
||||
startTime: {
|
||||
gte: new Date(now - 180000), // 3 minutes ago
|
||||
lte: new Date(now),
|
||||
},
|
||||
},
|
||||
select: {
|
||||
id: true,
|
||||
},
|
||||
});
|
||||
if (!!!trace || !!!observation) {
|
||||
return res.status(503).json({
|
||||
status: `No ${
|
||||
!!!trace
|
||||
? "traces"
|
||||
: !!!observation
|
||||
? "observations"
|
||||
: "<should not happen>"
|
||||
} within the last 3 minutes`,
|
||||
try {
|
||||
if (isSigtermReceived()) {
|
||||
console.log(
|
||||
"Health check failed: SIGTERM / SIGINT received, shutting down.",
|
||||
);
|
||||
return res.status(500).json({
|
||||
status: "SIGTERM / SIGINT received, shutting down",
|
||||
version: VERSION.replace("v", ""),
|
||||
});
|
||||
}
|
||||
await prisma.$queryRaw`SELECT 1;`;
|
||||
|
||||
if (failIfNoRecentEvents) {
|
||||
const now = Date.now();
|
||||
const trace = await prisma.trace.findFirst({
|
||||
where: {
|
||||
timestamp: {
|
||||
gte: new Date(now - 180000), // 3 minutes ago
|
||||
lte: new Date(now),
|
||||
},
|
||||
},
|
||||
select: {
|
||||
id: true,
|
||||
},
|
||||
});
|
||||
const observation = await prisma.observation.findFirst({
|
||||
where: {
|
||||
startTime: {
|
||||
gte: new Date(now - 180000), // 3 minutes ago
|
||||
lte: new Date(now),
|
||||
},
|
||||
},
|
||||
select: {
|
||||
id: true,
|
||||
},
|
||||
});
|
||||
if (!!!trace || !!!observation) {
|
||||
return res.status(503).json({
|
||||
status: `No ${
|
||||
!!!trace
|
||||
? "traces"
|
||||
: !!!observation
|
||||
? "observations"
|
||||
: "<should not happen>"
|
||||
} within the last 3 minutes`,
|
||||
version: VERSION.replace("v", ""),
|
||||
});
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
console.log("Health check failed: db not available", e);
|
||||
return res.status(503).json({
|
||||
status: "Database not available",
|
||||
version: VERSION.replace("v", ""),
|
||||
});
|
||||
}
|
||||
} catch (e) {
|
||||
console.log("Health check failed: ", e);
|
||||
return res.status(503).json({
|
||||
status: "Database not available",
|
||||
status: "Health check failed",
|
||||
version: VERSION.replace("v", ""),
|
||||
});
|
||||
}
|
||||
|
||||
@@ -13,6 +13,8 @@ import {
|
||||
type TraceUpsertEventType,
|
||||
type EventBodyType,
|
||||
EventName,
|
||||
LangfuseNotFoundError,
|
||||
InternalServerError,
|
||||
} from "@langfuse/shared";
|
||||
import { type ApiAccessScope } from "@/src/features/public-api/server/types";
|
||||
import { persistEventMiddleware } from "@/src/server/api/services/event-service";
|
||||
@@ -38,6 +40,7 @@ import {
|
||||
ForbiddenError,
|
||||
UnauthorizedError,
|
||||
} from "@langfuse/shared";
|
||||
import { isSigtermReceived } from "@/src/utils/shutdown";
|
||||
|
||||
export const config = {
|
||||
api: {
|
||||
@@ -81,6 +84,8 @@ export default async function handler(
|
||||
parsedSchema.success ? parsedSchema.data.batch.length : 0,
|
||||
);
|
||||
|
||||
await gaugePrismaStats();
|
||||
|
||||
if (!parsedSchema.success) {
|
||||
console.log("Invalid request data", parsedSchema.error);
|
||||
return res.status(400).json({
|
||||
@@ -200,7 +205,9 @@ export const handleBatch = async (
|
||||
req: NextApiRequest,
|
||||
authCheck: AuthHeaderVerificationResult,
|
||||
) => {
|
||||
console.log(`handling ingestion ${events.length} events`);
|
||||
console.log(
|
||||
`handling ingestion ${events.length} events ${isSigtermReceived() ? "after SIGTERM" : ""}`,
|
||||
);
|
||||
|
||||
if (!authCheck.validKey) throw new UnauthorizedError(authCheck.error);
|
||||
|
||||
@@ -289,6 +296,7 @@ const handleSingleEvent = async (
|
||||
const { output, ...rest } = restEvent;
|
||||
restEvent = rest;
|
||||
}
|
||||
|
||||
console.log(
|
||||
`handling single event ${event.id} of type ${event.type}: ${JSON.stringify({ body: restEvent })}`,
|
||||
);
|
||||
@@ -401,37 +409,79 @@ export const handleBatchResult = (
|
||||
return res.status(207).send({ errors: returnedErrors, successes });
|
||||
};
|
||||
|
||||
export const handleBatchResultLegacy = (
|
||||
/**
|
||||
* Handle single event which is usually send via /ingestion endpoint. Returns errors and results via `res` directly.
|
||||
*
|
||||
* Use `parseSingleTypedIngestionApiResponse` for a typed version of this function that throws `BaseError`.
|
||||
*/
|
||||
export const handleSingleIngestionObject = (
|
||||
errors: Array<{ id: string; error: unknown }>,
|
||||
results: Array<{ id: string; result: unknown }>,
|
||||
res: NextApiResponse,
|
||||
) => {
|
||||
const unknownErrors = errors.map((error) => error.error);
|
||||
try {
|
||||
// use method untyped for backwards compatibility
|
||||
const parsedResult = parseSingleTypedIngestionApiResponse(errors, results);
|
||||
|
||||
return res.status(200).json(parsedResult);
|
||||
} catch (error) {
|
||||
if (error instanceof BaseError) {
|
||||
return res.status(error.httpCode).json({
|
||||
message: error.message,
|
||||
error: error.name,
|
||||
});
|
||||
}
|
||||
return res.status(500).json({
|
||||
message: "Internal Server Error",
|
||||
error:
|
||||
error instanceof Error ? error.message : "An unknown error occurred",
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Parses the response from the ingestion batch API event processor and throws an error of `BaserError` if the response is not as expected.
|
||||
*
|
||||
* @param errors - Array of errors from `handleBatch()`
|
||||
* @param results - Array of results from `handleBatch()`
|
||||
* @param object - Zod object to parse the result, if not provided, the result is returned as is without parsing
|
||||
* @returns - Parsed result
|
||||
* @throws - Throws an error of type `BaseError` if there are errors in the arguments
|
||||
*/
|
||||
|
||||
export const parseSingleTypedIngestionApiResponse = <T extends z.ZodTypeAny>(
|
||||
errors: Array<{ id: string; error: unknown }>,
|
||||
results: Array<{ id: string; result: unknown }>,
|
||||
object?: T,
|
||||
): T extends z.ZodTypeAny ? z.infer<T> : unknown => {
|
||||
const unknownErrors = errors.map((error) => error.error);
|
||||
const badRequestErrors = getBadRequestError(unknownErrors);
|
||||
if (badRequestErrors.length > 0) {
|
||||
console.log("Bad request errors", badRequestErrors);
|
||||
return res.status(400).json({
|
||||
message: "Invalid request data",
|
||||
errors: badRequestErrors.map((error) => error.message),
|
||||
});
|
||||
throw new InvalidRequestError(badRequestErrors[0].message);
|
||||
}
|
||||
|
||||
const ResourceNotFoundError = getResourceNotFoundError(unknownErrors);
|
||||
if (ResourceNotFoundError.length > 0) {
|
||||
return res.status(404).json({
|
||||
message: "Resource not found",
|
||||
errors: ResourceNotFoundError.map((error) => error.message),
|
||||
});
|
||||
throw new LangfuseNotFoundError(ResourceNotFoundError[0].message);
|
||||
}
|
||||
if (errors.length > 0) {
|
||||
throw new InternalServerError("Internal Server Error");
|
||||
}
|
||||
|
||||
if (errors.length > 0) {
|
||||
console.log("Error processing events", unknownErrors);
|
||||
return res.status(500).json({
|
||||
errors: ["Internal Server Error"],
|
||||
});
|
||||
if (results.length === 0) {
|
||||
throw new InternalServerError("No results returned");
|
||||
}
|
||||
return res.status(200).send(results.length > 0 ? results[0]?.result : {});
|
||||
|
||||
if (object === undefined) {
|
||||
return results[0].result as T extends z.ZodTypeAny ? z.infer<T> : unknown;
|
||||
}
|
||||
|
||||
const parsedObj = object.safeParse(results[0].result);
|
||||
if (!parsedObj.success) {
|
||||
console.error("Error parsing response", parsedObj.error);
|
||||
Sentry.captureException(parsedObj.error);
|
||||
}
|
||||
// should not fail in prod but just log an exception, see above
|
||||
return results[0].result as z.infer<T>;
|
||||
};
|
||||
|
||||
// cleans NULL characters from the event
|
||||
@@ -502,3 +552,15 @@ export const sendToWorkerIfEnvironmentConfigured = async (
|
||||
console.error("Error sending events to worker", error);
|
||||
}
|
||||
};
|
||||
|
||||
const gaugePrismaStats = async () => {
|
||||
// execute with a 50% probability
|
||||
if (Math.random() > 0.5) {
|
||||
return;
|
||||
}
|
||||
const metrics = await prisma.$metrics.json();
|
||||
|
||||
metrics.gauges.forEach((gauge) => {
|
||||
Sentry.metrics.gauge(gauge.key, gauge.value, gauge.labels);
|
||||
});
|
||||
};
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user