Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
dd632fea9e | ||
|
|
7527bb0d84 | ||
|
|
8cc4a5537f | ||
|
|
0a6d3f108a | ||
|
|
1bf83313e3 | ||
|
|
9c3a715d77 | ||
|
|
0cf2a33473 | ||
|
|
51554eb066 | ||
|
|
cbc21bb9cc | ||
|
|
8e30694214 | ||
|
|
0d20d9de2b | ||
|
|
eeeba25439 | ||
|
|
a59630d656 | ||
|
|
10e7dea9c9 | ||
|
|
9bf326db4f | ||
|
|
13190c3ec4 | ||
|
|
7a4ce9ee5d | ||
|
|
bc02989ccf | ||
|
|
f0dac0299c | ||
|
|
19997064c1 | ||
|
|
c393c64a40 | ||
|
|
d04e027107 | ||
|
|
53869c7200 | ||
|
|
6d964894fb | ||
|
|
81653b2bd2 | ||
|
|
02d6486f10 | ||
|
|
07ee4ed961 | ||
|
|
00f770b053 | ||
|
|
e12386f9d4 | ||
|
|
b1ac930462 | ||
|
|
8e9521522f | ||
|
|
87ed9fe7e1 | ||
|
|
1c40823f0c | ||
|
|
730946f91e | ||
|
|
fbec38c79e | ||
|
|
6603be6711 | ||
|
|
859c58e96c | ||
|
|
316f02f9dc | ||
|
|
221d4e4489 | ||
|
|
4a16f6cf33 |
@@ -235,6 +235,9 @@ LANGFUSE_ENABLE_BLOB_STORAGE_FILE_LOG=true
|
||||
# Comma-separated default field groups for GET /api/public/traces when no fields param is provided
|
||||
# Valid values: core, io, scores, observations, metrics
|
||||
# LANGFUSE_API_TRACES_DEFAULT_FIELDS=
|
||||
# Comma-separated default field groups for GET /api/public/traces/{traceId} when no fields param is provided
|
||||
# Valid values: core, io, scores, observations, metrics
|
||||
# LANGFUSE_API_TRACEBYID_DEFAULT_FIELDS=
|
||||
|
||||
### START Enterprise Edition Configuration
|
||||
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
# Workaround for a known cla-assistant bug where the CLA check gets stuck and
|
||||
# never re-runs after a contributor signs the CLA.
|
||||
# See: https://github.com/cla-assistant/cla-assistant/issues/528
|
||||
#
|
||||
# Usage: comment `/check-cla` on any PR to manually retrigger the CLA check.
|
||||
name: CLA Assistant
|
||||
|
||||
on:
|
||||
issue_comment:
|
||||
types: [created]
|
||||
|
||||
jobs:
|
||||
retrigger_cla:
|
||||
# Only run on PR comments (not issue comments) with the /check-cla command
|
||||
if: github.event.issue.pull_request && github.event.comment.body == '/check-cla'
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Retrigger CLA check
|
||||
run: |
|
||||
curl -s "https://cla-assistant.io/check/langfuse/langfuse?pullRequest=${{ github.event.issue.number }}"
|
||||
@@ -5,7 +5,7 @@ on:
|
||||
branches:
|
||||
- main
|
||||
paths:
|
||||
- 'fern/**'
|
||||
- "fern/**"
|
||||
workflow_dispatch:
|
||||
|
||||
concurrency:
|
||||
@@ -38,6 +38,11 @@ jobs:
|
||||
FERN_TOKEN: ${{ secrets.FERN_TOKEN }}
|
||||
run: npx fern-api generate --api server --force
|
||||
|
||||
- name: Install uv
|
||||
uses: astral-sh/setup-uv@cec208311dfd045dd5311c1add060b2062131d57 # v8
|
||||
with:
|
||||
version: "0.11.2"
|
||||
|
||||
- name: Update Python SDK
|
||||
env:
|
||||
GH_ACCESS_TOKEN: ${{ secrets.GH_ACCESS_TOKEN }}
|
||||
@@ -61,10 +66,9 @@ jobs:
|
||||
# (could not find the right option to prevent this in generation step)
|
||||
rm -rf langfuse/api/tests
|
||||
|
||||
# Install poetry and format
|
||||
pip install poetry
|
||||
poetry install --all-extras
|
||||
poetry run ruff format langfuse/api
|
||||
# Install dependencies and format
|
||||
uv sync --all-extras --frozen
|
||||
uv run ruff format langfuse/api
|
||||
|
||||
# Check for changes and show diff for debugging
|
||||
if git diff --quiet; then
|
||||
|
||||
+4
-2
@@ -11,13 +11,15 @@ if [ "$current_branch" = "$protected_branch" ]; then
|
||||
echo "🚨 You are about to commit to the $protected_branch branch. Are you sure? (y/n)"
|
||||
read -r answer < /dev/tty
|
||||
if [ "$answer" != "${answer#[Yy]}" ]; then
|
||||
# Commit approved, check formatting. On files changed, block commit
|
||||
# Commit approved, run checks
|
||||
pnpm run format:check
|
||||
pnpm run lint
|
||||
else
|
||||
echo "Commit to $protected_branch branch has been canceled."
|
||||
exit 1 # Commit will be blocked
|
||||
fi
|
||||
fi
|
||||
|
||||
# If not the protected branch, check formatting (on changed files, block)
|
||||
# If not the protected branch, run checks
|
||||
pnpm run format:check
|
||||
pnpm run lint
|
||||
|
||||
+44
-39
@@ -418,45 +418,48 @@ The background color of the following component will be `hsl(var(--primary))` an
|
||||
|
||||
### Color Variables
|
||||
|
||||
| Variable | Description | Examples |
|
||||
| ------------------------ | ------------------------------------------------------------------ | -------------------------------- |
|
||||
| --background | Background color | Default background color of body |
|
||||
| --foreground | Foreground color | Default text color of body |
|
||||
| --muted | Muted background color | TabsList, Skeleton and Switch |
|
||||
| --muted-foreground | Muted foreground color | |
|
||||
| --popover | Popover background color | DropdownMenu, HoverCard, Popover |
|
||||
| --popover-foreground | Popover foreground color | |
|
||||
| --card | Card background color | Card |
|
||||
| --card-foreground | Card foreground color | |
|
||||
| --border | Border color | Default border color |
|
||||
| --input | Input field border color | Input, Select, Textarea |
|
||||
| --primary | Primary button background colors | Button variant="primary" |
|
||||
| --primary-foreground | Primary button foreground color | |
|
||||
| --secondary | Secondary button background color | Button variant="secondary" |
|
||||
| --secondary-foreground | Secondary button foreground color | |
|
||||
| --accent | Used for accents such as hover effects | DropdownMenuItem, SelectItem |
|
||||
| --accent-foreground | Used for texts on hover effects | DropdownMenuItem, SelectItem |
|
||||
| --destructive | Destructive action color for background | Button variant="destructive" |
|
||||
| --destructive-foreground | Destructive action color for text | |
|
||||
| --ring | Focus ring color | MultiSelect |
|
||||
| --primary-accent | Primary accent color used for branding | Layout |
|
||||
| --hover-primary-accent | Primary accent color used for hover effects for links | SignIn and AuthCloudRegionSwitch |
|
||||
| --muted-green | Muted green for Event label | ObservationTree |
|
||||
| --muted-magenta | Muted magenta for Generation label | ObservationTree |
|
||||
| --muted-blue | Muted blue for Span label | ObservationTree |
|
||||
| --muted-gray | Muted gray for disabled status badges | StatusBadge |
|
||||
| --accent-light-green | Light green accent for background of output and assistant messages | IOPreview, Generations, Traces |
|
||||
| --accent-dark-green | Dark green accent for border of output and assistant messages | CodeJsonViewer and IOPReview |
|
||||
| --light-red | Light red for error background | level-color and StatusBadge |
|
||||
| --dark-red | Dark red for error text and error badge dot color | level-color and ErrorPage |
|
||||
| --light-yellow | Light yellow for warning background | LevelColor |
|
||||
| --dark-yellow | Dark yellow for warning text | LevelColor |
|
||||
| --light-green | Light green for success status badge background | StatusBadge |
|
||||
| --dark-green | Dark green for success status badge text and dot | StatusBadge |
|
||||
| --light-blue | Light blue for background of Staging label | LangfuseLogo |
|
||||
| --dark-blue | Dark blue for text and border of Staging label | LangfuseLogo |
|
||||
| --accent-light-blue | Light blue accent for table link hover effect | TableLink |
|
||||
| --accent-dark-blue | Dark blue accent for table link text | TableLink |
|
||||
| Variable | Description | Examples |
|
||||
| -------------------------------- | ------------------------------------------------------------------ | -------------------------------- |
|
||||
| --background | Background color | Default background color of body |
|
||||
| --foreground | Foreground color | Default text color of body |
|
||||
| --muted | Muted background color | TabsList, Skeleton and Switch |
|
||||
| --muted-foreground | Muted foreground color | |
|
||||
| --popover | Popover background color | DropdownMenu, HoverCard, Popover |
|
||||
| --popover-foreground | Popover foreground color | |
|
||||
| --card | Card background color | Card |
|
||||
| --card-foreground | Card foreground color | |
|
||||
| --border | Border color | Default border color |
|
||||
| --input | Input field border color | Input, Select, Textarea |
|
||||
| --primary | Primary button background colors | Button variant="primary" |
|
||||
| --primary-foreground | Primary button foreground color | |
|
||||
| --secondary | Secondary button background color | Button variant="secondary" |
|
||||
| --secondary-foreground | Secondary button foreground color | |
|
||||
| --accent | Used for accents such as hover effects | DropdownMenuItem, SelectItem |
|
||||
| --accent-foreground | Used for texts on hover effects | DropdownMenuItem, SelectItem |
|
||||
| --destructive | Destructive action color for background | Button variant="destructive" |
|
||||
| --destructive-foreground | Destructive action color for text | |
|
||||
| --ring | Focus ring color | MultiSelect |
|
||||
| --primary-accent | Primary accent color used for branding | Layout |
|
||||
| --hover-primary-accent | Primary accent color used for hover effects for links | SignIn and AuthCloudRegionSwitch |
|
||||
| --muted-green | Muted green for Event label | ObservationTree |
|
||||
| --muted-magenta | Muted magenta for Generation label | ObservationTree |
|
||||
| --muted-blue | Muted blue for Span label | ObservationTree |
|
||||
| --muted-gray | Muted gray for disabled status badges | StatusBadge |
|
||||
| --accent-light-green | Light green accent for background of output and assistant messages | IOPreview, Generations, Traces |
|
||||
| --accent-dark-green | Dark green accent for border of output and assistant messages | CodeJsonViewer and IOPReview |
|
||||
| --light-red | Light red for error background | level-color and StatusBadge |
|
||||
| --dark-red | Dark red for error text and error badge dot color | level-color and ErrorPage |
|
||||
| --light-yellow | Light yellow for warning background | LevelColor |
|
||||
| --dark-yellow | Dark yellow for warning text | LevelColor |
|
||||
| --light-green | Light green for success status badge background | StatusBadge |
|
||||
| --dark-green | Dark green for success status badge text and dot | StatusBadge |
|
||||
| --light-blue | Light blue for background of Staging label | LangfuseLogo |
|
||||
| --dark-blue | Dark blue for text and border of Staging label | LangfuseLogo |
|
||||
| --accent-light-blue | Light blue accent for table link hover effect | TableLink |
|
||||
| --accent-dark-blue | Dark blue accent for table link text | TableLink |
|
||||
| --find-match-selected-background | Background color for selected search matches | CodeMirrorEditor |
|
||||
| --find-match-selected-foreground | Foreground color for selected search matches | CodeMirrorEditor |
|
||||
| --find-match-background | Background color for search matches | CodeMirrorEditor |
|
||||
|
||||
### Adding New Colors
|
||||
|
||||
@@ -510,3 +513,5 @@ npx fern-api generate --api organizations # for the organizations API
|
||||
Langfuse is MIT licensed, except for `ee/` folder. See [LICENSE](LICENSE) and [docs](https://langfuse.com/docs/open-source) for more details.
|
||||
|
||||
When contributing to the Langfuse codebase, you need to agree to the [Contributor License Agreement](https://cla-assistant.io/langfuse/langfuse). You only need to do this once and the CLA bot will remind you if you haven't signed it yet.
|
||||
|
||||
If the CLA check gets stuck after signing (a [known cla-assistant bug](https://github.com/cla-assistant/cla-assistant/issues/520)), comment `/check-cla` on your PR to retrigger it.
|
||||
|
||||
@@ -431,6 +431,12 @@ types:
|
||||
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
|
||||
TextScoreV1:
|
||||
extends: BaseScoreV1
|
||||
properties:
|
||||
stringValue:
|
||||
type: string
|
||||
docs: The text content of the score (1-500 characters)
|
||||
ScoreV1:
|
||||
discriminant: "dataType"
|
||||
union:
|
||||
@@ -443,6 +449,9 @@ types:
|
||||
BOOLEAN:
|
||||
type: BooleanScoreV1
|
||||
docs: "Score with BOOLEAN data type"
|
||||
TEXT:
|
||||
type: TextScoreV1
|
||||
docs: "Score with TEXT data type"
|
||||
|
||||
# Source: packages/shared/src/features/scores/interfaces/api/v2/schemas.ts - APIScoreSchemaV2
|
||||
BaseScore:
|
||||
@@ -516,6 +525,12 @@ types:
|
||||
stringValue:
|
||||
type: string
|
||||
docs: The string representation of the correction content
|
||||
TextScore:
|
||||
extends: BaseScore
|
||||
properties:
|
||||
stringValue:
|
||||
type: string
|
||||
docs: The text content of the score (1-500 characters)
|
||||
Score:
|
||||
discriminant: "dataType"
|
||||
union:
|
||||
@@ -531,13 +546,16 @@ types:
|
||||
CORRECTION:
|
||||
type: CorrectionScore
|
||||
docs: "Score with CORRECTION data type"
|
||||
TEXT:
|
||||
type: TextScore
|
||||
docs: "Score with TEXT data type"
|
||||
|
||||
CreateScoreValue:
|
||||
discriminated: false
|
||||
union:
|
||||
- double
|
||||
- string
|
||||
docs: The value of the score. Must be passed as string for categorical scores, and numeric for boolean and numeric scores
|
||||
docs: The value of the score. Must be passed as string for categorical and text scores, and numeric for boolean and numeric scores
|
||||
|
||||
# Source: web/src/features/public-api/types/comments.ts - APIComment
|
||||
Comment:
|
||||
@@ -932,6 +950,7 @@ types:
|
||||
- NUMERIC
|
||||
- BOOLEAN
|
||||
- CATEGORICAL
|
||||
- TEXT
|
||||
|
||||
ScoreDataType:
|
||||
enum:
|
||||
@@ -939,6 +958,7 @@ types:
|
||||
- BOOLEAN
|
||||
- CATEGORICAL
|
||||
- CORRECTION
|
||||
- TEXT
|
||||
|
||||
errors:
|
||||
Error:
|
||||
|
||||
@@ -287,7 +287,7 @@ types:
|
||||
docs: The annotation queue referenced by the score. Indicates if score was initially created while processing annotation queue.
|
||||
value:
|
||||
type: commons.CreateScoreValue
|
||||
docs: The value of the score. Must be passed as string for categorical scores, and numeric for boolean and numeric scores. Boolean score values must equal either 1 or 0 (true or false)
|
||||
docs: The value of the score. Must be passed as string for categorical and text scores, and numeric for boolean and numeric scores. Boolean score values must equal either 1 or 0 (true or false). Text score values must be between 1 and 500 characters.
|
||||
comment: optional<string>
|
||||
metadata: optional<unknown>
|
||||
dataType:
|
||||
|
||||
@@ -31,7 +31,7 @@ types:
|
||||
name: string
|
||||
value:
|
||||
type: commons.CreateScoreValue
|
||||
docs: The value of the score. Must be passed as string for categorical scores, and numeric for boolean and numeric scores. Boolean score values must equal either 1 or 0 (true or false)
|
||||
docs: The value of the score. Must be passed as string for categorical and text scores, and numeric for boolean and numeric scores. Boolean score values must equal either 1 or 0 (true or false). Text score values must be between 1 and 500 characters.
|
||||
comment: optional<string>
|
||||
metadata: optional<map<string, unknown>>
|
||||
environment:
|
||||
@@ -85,6 +85,11 @@ types:
|
||||
dataType: "BOOLEAN"
|
||||
configId: "1234-5678-90ab-cdef"
|
||||
traceId: "cdef-1234-5678-90ab"
|
||||
- value:
|
||||
name: "feedback"
|
||||
value: "Great explanation of the concept"
|
||||
dataType: "TEXT"
|
||||
traceId: "cdef-1234-5678-90ab"
|
||||
CreateScoreResponse:
|
||||
properties:
|
||||
id:
|
||||
|
||||
@@ -130,6 +130,11 @@ types:
|
||||
properties:
|
||||
trace: optional<GetScoresResponseTraceData>
|
||||
|
||||
GetScoresResponseDataText:
|
||||
extends: commons.TextScore
|
||||
properties:
|
||||
trace: optional<GetScoresResponseTraceData>
|
||||
|
||||
GetScoresResponseData:
|
||||
discriminant: dataType
|
||||
union:
|
||||
@@ -137,6 +142,7 @@ types:
|
||||
CATEGORICAL: GetScoresResponseDataCategorical
|
||||
BOOLEAN: GetScoresResponseDataBoolean
|
||||
CORRECTION: GetScoresResponseDataCorrection
|
||||
TEXT: GetScoresResponseDataText
|
||||
|
||||
GetScoresResponse:
|
||||
properties:
|
||||
|
||||
@@ -10,6 +10,12 @@ service:
|
||||
docs: Get a specific trace
|
||||
method: GET
|
||||
path: /traces/{traceId}
|
||||
request:
|
||||
name: GetTraceRequest
|
||||
query-parameters:
|
||||
fields:
|
||||
type: optional<string>
|
||||
docs: "Comma-separated list of fields to include in the response. Available field groups: 'core' (always included), 'io' (input, output, metadata), 'scores', 'observations', 'metrics'. If not specified, all fields are returned. Example: 'core,scores,metrics'. Note: Excluded 'observations' or 'scores' fields return empty arrays; excluded 'metrics' returns -1 for 'totalCost' and 'latency'."
|
||||
path-parameters:
|
||||
traceId:
|
||||
type: string
|
||||
|
||||
+2
-3
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "langfuse",
|
||||
"version": "3.164.0",
|
||||
"version": "3.167.0",
|
||||
"author": "engineering@langfuse.com",
|
||||
"license": "MIT",
|
||||
"private": true,
|
||||
@@ -48,7 +48,7 @@
|
||||
"husky": "^9.1.7",
|
||||
"prettier": "^3.8.1",
|
||||
"release-it": "^19.2.4",
|
||||
"turbo": "2.8.20"
|
||||
"turbo": "2.9.5"
|
||||
},
|
||||
"release-it": {
|
||||
"git": {
|
||||
@@ -104,7 +104,6 @@
|
||||
"nanoid": "^3.3.8",
|
||||
"katex": "^0.16.21",
|
||||
"tar-fs": "^2.1.2",
|
||||
"effect": "^3.20.0",
|
||||
"rollup@^4.0.0": "^4.22.4",
|
||||
"@types/node-fetch": "^2.6.13",
|
||||
"@types/react-dom": "19.2.3",
|
||||
|
||||
@@ -18,7 +18,7 @@
|
||||
"@eslint/js": "^9.39.2",
|
||||
"eslint-config-next": "16.2.1",
|
||||
"eslint-config-prettier": "^10.1.8",
|
||||
"eslint-config-turbo": "2.8.20",
|
||||
"eslint-config-turbo": "2.9.5",
|
||||
"eslint-plugin-only-warn": "^1.1.0",
|
||||
"eslint-plugin-prettier": "^5.5.4",
|
||||
"globals": "^16.0.0",
|
||||
|
||||
@@ -97,6 +97,15 @@ the same PR.
|
||||
2. Update ClickHouse query/mapping logic in `src/server/clickhouse/*` and
|
||||
related repositories.
|
||||
3. Validate ingestion/read path impact in both `web` and `worker`.
|
||||
4. If the change affects columns, types, or nullability of tables read by blob
|
||||
storage export queries (`getTracesForBlobStorageExport`,
|
||||
`getObservationsForBlobStorageExport`, `getScoresForBlobStorageExport`,
|
||||
`getEventsForBlobStorageExport`, or the EventsQueryBuilder `export` field
|
||||
set), fetch the latest published docs and check for discrepancies:
|
||||
- https://langfuse.com/docs/api-and-data-platform/features/export-to-blob-storage
|
||||
- https://langfuse.com/docs/api-and-data-platform/features/blob-storage-export-fields
|
||||
Surface any mismatches in field names, types, nullability, or filter
|
||||
descriptions so they can be addressed in the docs repo.
|
||||
|
||||
### Queue payload contract change
|
||||
|
||||
@@ -130,3 +139,8 @@ the same PR.
|
||||
- Do not hand-edit generated artifacts under `prisma/generated/*` or `dist/*`.
|
||||
- Avoid exposing server-only modules through `src/index.ts` if they must remain
|
||||
frontend-safe.
|
||||
- Changes to domain constants consumed by blob storage exports (e.g.
|
||||
`LISTABLE_SCORE_TYPES` in `src/domain/scores.ts`, score data type enums)
|
||||
should be reviewed against the blob storage export field reference docs for
|
||||
consistency — fetch the latest page and surface any discrepancies:
|
||||
https://langfuse.com/docs/api-and-data-platform/features/blob-storage-export-fields
|
||||
|
||||
@@ -91,7 +91,7 @@
|
||||
"@langchain/google-vertexai": "^2.1.13",
|
||||
"@langchain/openai": "^1.2.3",
|
||||
"@opentelemetry/api": ">=1.0.0 <1.10.0",
|
||||
"@prisma/client": "^6.17.1",
|
||||
"@prisma/client": "^6.19.3",
|
||||
"@react-email/components": "^0.5.1",
|
||||
"@react-email/render": "^1.2.1",
|
||||
"@slack/oauth": "^3.0.4",
|
||||
@@ -131,7 +131,7 @@
|
||||
"@types/uuid": "^9.0.8",
|
||||
"eslint": "^9.39.2",
|
||||
"prettier": "^3.8.1",
|
||||
"prisma": "^6.17.1",
|
||||
"prisma": "^6.19.3",
|
||||
"ts-node": "^10.9.2",
|
||||
"tsc-watch": "^6.2.0",
|
||||
"typescript": "^5.7.2"
|
||||
|
||||
+2
@@ -0,0 +1,2 @@
|
||||
-- AlterEnum
|
||||
ALTER TYPE "ScoreConfigDataType" ADD VALUE 'TEXT';
|
||||
@@ -495,6 +495,7 @@ enum ScoreConfigDataType {
|
||||
CATEGORICAL
|
||||
NUMERIC
|
||||
BOOLEAN
|
||||
TEXT
|
||||
}
|
||||
|
||||
model AnnotationQueue {
|
||||
@@ -924,7 +925,7 @@ model EvalTemplate {
|
||||
provider String?
|
||||
modelParams Json? @map("model_params")
|
||||
vars String[] @default([])
|
||||
outputDefinition Json @map("output_schema")
|
||||
outputDefinition Json @map("output_schema")
|
||||
JobConfiguration JobConfiguration[]
|
||||
JobExecution JobExecution[]
|
||||
|
||||
|
||||
@@ -965,6 +965,14 @@ async function generateConfigs(project: Project) {
|
||||
"Used to indicate if text was harmful or offensive in nature.",
|
||||
isArchived: false,
|
||||
},
|
||||
{
|
||||
id: `config-${v4()}`,
|
||||
projectId: project.id,
|
||||
name: "Feedback",
|
||||
dataType: ScoreDataTypeEnum.TEXT,
|
||||
description: "Free-form text feedback on the output quality.",
|
||||
isArchived: false,
|
||||
},
|
||||
];
|
||||
|
||||
for (const config of configs) {
|
||||
|
||||
@@ -1 +1 @@
|
||||
export const VERSION = "v3.164.0";
|
||||
export const VERSION = "v3.167.0";
|
||||
|
||||
@@ -72,6 +72,13 @@ export const CategoricalConfigFields = z.object({
|
||||
categories: z.array(ScoreConfigCategory).superRefine(validateCategories),
|
||||
});
|
||||
|
||||
export const TextConfigFields = z.object({
|
||||
maxValue: z.undefined().nullish(),
|
||||
minValue: z.undefined().nullish(),
|
||||
dataType: z.literal("TEXT"),
|
||||
categories: z.undefined().nullish(),
|
||||
});
|
||||
|
||||
const ScoreConfigBase = z.object({
|
||||
id: z.string(),
|
||||
name: z.string().min(1).max(35),
|
||||
@@ -114,6 +121,10 @@ export const ScoreConfigSchema = z
|
||||
...ScoreConfigBase.shape,
|
||||
...BooleanConfigFields.shape,
|
||||
}),
|
||||
z.object({
|
||||
...ScoreConfigBase.shape,
|
||||
...TextConfigFields.shape,
|
||||
}),
|
||||
])
|
||||
.superRefine(validateNumericRangeFields);
|
||||
|
||||
|
||||
@@ -12,17 +12,21 @@ export type ScoreSourceType = z.infer<typeof ScoreSourceDomain>;
|
||||
|
||||
export const CORRECTION_NAME = "output" as const;
|
||||
|
||||
export const TEXT_SCORE_MAX_LENGTH = 500 as const;
|
||||
|
||||
export const ScoreDataTypeArray = [
|
||||
"NUMERIC",
|
||||
"CATEGORICAL",
|
||||
"BOOLEAN",
|
||||
"CORRECTION",
|
||||
"TEXT",
|
||||
] as const;
|
||||
export const ScoreDataTypeEnum = {
|
||||
NUMERIC: "NUMERIC",
|
||||
CATEGORICAL: "CATEGORICAL",
|
||||
BOOLEAN: "BOOLEAN",
|
||||
CORRECTION: "CORRECTION",
|
||||
TEXT: "TEXT",
|
||||
} as const;
|
||||
export const ScoreDataTypeDomain = z.enum(ScoreDataTypeArray);
|
||||
export type ScoreDataTypeType = z.infer<typeof ScoreDataTypeDomain>;
|
||||
@@ -47,6 +51,11 @@ const CorrectionData = z.object({
|
||||
dataType: z.literal("CORRECTION"),
|
||||
});
|
||||
|
||||
export const TextData = z.object({
|
||||
stringValue: z.string().min(1).max(TEXT_SCORE_MAX_LENGTH),
|
||||
dataType: z.literal("TEXT"),
|
||||
});
|
||||
|
||||
// Only used for backwards compatibility with old score API schemas
|
||||
export const ScoreSchemaExclReferencesAndDates = z.object({
|
||||
// Core identifiers
|
||||
@@ -89,6 +98,7 @@ export const ScoreSchema = ScoreFoundationSchema.and(
|
||||
CategoricalData,
|
||||
BooleanData,
|
||||
CorrectionData,
|
||||
TextData,
|
||||
]),
|
||||
);
|
||||
|
||||
@@ -119,3 +129,13 @@ export type AggregatableScoreDataType =
|
||||
export type AggregatableScore = ScoresByDataTypes<
|
||||
typeof AGGREGATABLE_SCORE_TYPES
|
||||
>;
|
||||
|
||||
export const LISTABLE_SCORE_TYPES = [
|
||||
"NUMERIC",
|
||||
"BOOLEAN",
|
||||
"CATEGORICAL",
|
||||
"TEXT",
|
||||
] as const satisfies readonly ScoreDataTypeType[];
|
||||
|
||||
export type ListableScoreDataType = (typeof LISTABLE_SCORE_TYPES)[number];
|
||||
export type ListableScore = ScoresByDataTypes<typeof LISTABLE_SCORE_TYPES>;
|
||||
|
||||
@@ -1,6 +1,11 @@
|
||||
import z from "zod";
|
||||
import { StringNoHTML, StringNoHTMLNonEmpty } from "../../utils/zod";
|
||||
import { BooleanData, CategoricalData, NumericData } from "../../domain";
|
||||
import {
|
||||
BooleanData,
|
||||
CategoricalData,
|
||||
TextData,
|
||||
NumericData,
|
||||
} from "../../domain";
|
||||
|
||||
const ScoreTargetTrace = z.object({
|
||||
type: z.literal("trace"),
|
||||
@@ -45,7 +50,12 @@ const UpdateAnnotationScoreBase = CreateAnnotationScoreBase.extend({
|
||||
* For langfuse score types please refer to `web/src/features/public-api/types/scores.ts`
|
||||
*/
|
||||
export const CreateAnnotationScoreData = CreateAnnotationScoreBase.and(
|
||||
z.discriminatedUnion("dataType", [NumericData, CategoricalData, BooleanData]),
|
||||
z.discriminatedUnion("dataType", [
|
||||
NumericData,
|
||||
CategoricalData,
|
||||
BooleanData,
|
||||
TextData,
|
||||
]),
|
||||
);
|
||||
|
||||
export type CreateAnnotationScoreData = z.infer<
|
||||
@@ -57,7 +67,12 @@ export type CreateAnnotationScoreData = z.infer<
|
||||
* For langfuse score types please refer to `web/src/features/public-api/types/scores.ts`
|
||||
*/
|
||||
export const UpdateAnnotationScoreData = UpdateAnnotationScoreBase.and(
|
||||
z.discriminatedUnion("dataType", [NumericData, CategoricalData, BooleanData]),
|
||||
z.discriminatedUnion("dataType", [
|
||||
NumericData,
|
||||
CategoricalData,
|
||||
BooleanData,
|
||||
TextData,
|
||||
]),
|
||||
);
|
||||
|
||||
export type UpdateAnnotationScoreData = z.infer<
|
||||
|
||||
@@ -6,6 +6,7 @@ import { PostScoreBodyFoundationSchema } from "../shared";
|
||||
import {
|
||||
ScoreDataTypeDomain,
|
||||
ScoreSourceDomain,
|
||||
TEXT_SCORE_MAX_LENGTH,
|
||||
} from "../../../../domain/scores";
|
||||
import { singleFilter } from "../../../../interfaces/filters";
|
||||
import { InvalidRequestError } from "../../../../errors";
|
||||
@@ -74,7 +75,6 @@ export const GetScoresQuery = z.object({
|
||||
});
|
||||
|
||||
// POST /scores
|
||||
// Please note that the POST /scores endpoint supports all score types (trace, session, dataset run) across v1 and v2.
|
||||
/**
|
||||
* PostScoresBody is copied for the ingestion API as `ScoreBody`. Please copy any changes here in `packages/shared/src/features/ingestion/types.ts`
|
||||
*/
|
||||
@@ -104,6 +104,11 @@ export const PostScoresBody = applyScoreValidation(
|
||||
dataType: z.literal("CORRECTION"),
|
||||
configId: z.undefined().nullish(), // Cannot have config
|
||||
}),
|
||||
z.object({
|
||||
value: z.string().min(1).max(TEXT_SCORE_MAX_LENGTH),
|
||||
dataType: z.literal("TEXT"),
|
||||
configId: z.string().nullish(),
|
||||
}),
|
||||
z.object({
|
||||
value: z.union([z.string(), z.number()]),
|
||||
dataType: z.undefined(),
|
||||
|
||||
@@ -9,7 +9,7 @@ import {
|
||||
/**
|
||||
* Foundation schema for scores API v1 i.e. trace and observation scores ONLY
|
||||
*
|
||||
* Must be extended with score data specific schema (numeric, categorical, boolean)
|
||||
* Must be extended with score data specific schema (numeric, categorical, boolean, text)
|
||||
* @see {@link NumericData}, {@link CategoricalData}, {@link BooleanData}
|
||||
*/
|
||||
const ScoreFoundationSchemaV1 = ScoreSchemaExclReferencesAndDates.extend({
|
||||
@@ -22,8 +22,17 @@ const ScoreFoundationSchemaV1 = ScoreSchemaExclReferencesAndDates.extend({
|
||||
observationId: z.string().nullish(),
|
||||
});
|
||||
|
||||
export const APIScoreSchemaV1 = ScoreFoundationSchemaV1.and(
|
||||
z.discriminatedUnion("dataType", [NumericData, CategoricalData, BooleanData]),
|
||||
);
|
||||
// Response-only schema without input validation constraints (e.g. length limits)
|
||||
const TextData = z.object({
|
||||
stringValue: z.string(),
|
||||
dataType: z.literal("TEXT"),
|
||||
});
|
||||
|
||||
export const APIScoreSchemaV1 = z.discriminatedUnion("dataType", [
|
||||
ScoreFoundationSchemaV1.extend(NumericData.shape),
|
||||
ScoreFoundationSchemaV1.extend(CategoricalData.shape),
|
||||
ScoreFoundationSchemaV1.extend(BooleanData.shape),
|
||||
ScoreFoundationSchemaV1.omit({ value: true }).extend(TextData.shape),
|
||||
]);
|
||||
|
||||
export type APIScoreV1 = z.infer<typeof APIScoreSchemaV1>;
|
||||
|
||||
@@ -6,11 +6,18 @@ import {
|
||||
} from "../../../../../domain";
|
||||
import z from "zod";
|
||||
|
||||
// Response-only schemas without input validation constraints (e.g. length limits).
|
||||
// Input constraints are enforced at write time; response schemas must accept any stored value.
|
||||
const CorrectionData = z.object({
|
||||
stringValue: z.string(),
|
||||
dataType: z.literal("CORRECTION"),
|
||||
});
|
||||
|
||||
const TextData = z.object({
|
||||
stringValue: z.string(),
|
||||
dataType: z.literal("TEXT"),
|
||||
});
|
||||
|
||||
/**
|
||||
* Foundation schema for scores API v2 i.e. trace, observation AND session scores
|
||||
*
|
||||
@@ -29,13 +36,13 @@ const ScoreFoundationSchemaV2 = ScoreSchemaExclReferencesAndDates.extend({
|
||||
datasetRunId: z.string().nullish(),
|
||||
});
|
||||
|
||||
export const APIScoreSchemaV2 = ScoreFoundationSchemaV2.and(
|
||||
z.discriminatedUnion("dataType", [
|
||||
NumericData,
|
||||
CategoricalData,
|
||||
BooleanData,
|
||||
CorrectionData,
|
||||
]),
|
||||
);
|
||||
export const APIScoreSchemaV2 = z.discriminatedUnion("dataType", [
|
||||
ScoreFoundationSchemaV2.extend(NumericData.shape),
|
||||
ScoreFoundationSchemaV2.extend(CategoricalData.shape),
|
||||
ScoreFoundationSchemaV2.extend(BooleanData.shape),
|
||||
ScoreFoundationSchemaV2.extend(CorrectionData.shape),
|
||||
// a numeric value does not make sense for TEXT scores, so we omit the property
|
||||
ScoreFoundationSchemaV2.omit({ value: true }).extend(TextData.shape),
|
||||
]);
|
||||
|
||||
export type APIScoreV2 = z.infer<typeof APIScoreSchemaV2>;
|
||||
|
||||
@@ -3,34 +3,41 @@ import { applyScoreValidation } from "../../../../utils/scores";
|
||||
import { PostScoreBodyFoundationSchema } from "../shared";
|
||||
import { isPresent } from "../../../../utils/typeChecks";
|
||||
import { ScoreConfigCategory } from "../../../../domain/score-configs";
|
||||
import { TEXT_SCORE_MAX_LENGTH } from "../../../../domain/scores";
|
||||
|
||||
export const ScoreBodyWithoutConfig = applyScoreValidation(
|
||||
z.discriminatedUnion("dataType", [
|
||||
PostScoreBodyFoundationSchema.merge(
|
||||
PostScoreBodyFoundationSchema.extend(
|
||||
z.object({
|
||||
value: z.number(),
|
||||
dataType: z.literal("NUMERIC"),
|
||||
}),
|
||||
}).shape,
|
||||
),
|
||||
PostScoreBodyFoundationSchema.merge(
|
||||
PostScoreBodyFoundationSchema.extend(
|
||||
z.object({
|
||||
value: z.string(),
|
||||
dataType: z.literal("CATEGORICAL"),
|
||||
}),
|
||||
}).shape,
|
||||
),
|
||||
PostScoreBodyFoundationSchema.merge(
|
||||
PostScoreBodyFoundationSchema.extend(
|
||||
z.object({
|
||||
value: z.string(),
|
||||
dataType: z.literal("CORRECTION"),
|
||||
}),
|
||||
}).shape,
|
||||
),
|
||||
PostScoreBodyFoundationSchema.merge(
|
||||
PostScoreBodyFoundationSchema.extend(
|
||||
z.object({
|
||||
value: z.number().refine((val) => val === 0 || val === 1, {
|
||||
message: "Value must be either 0 or 1",
|
||||
}),
|
||||
dataType: z.literal("BOOLEAN"),
|
||||
}),
|
||||
}).shape,
|
||||
),
|
||||
PostScoreBodyFoundationSchema.extend(
|
||||
z.object({
|
||||
value: z.string().min(1).max(TEXT_SCORE_MAX_LENGTH),
|
||||
dataType: z.literal("TEXT"),
|
||||
}).shape,
|
||||
),
|
||||
]),
|
||||
);
|
||||
@@ -81,4 +88,8 @@ export const ScorePropsAgainstConfig = z.union([
|
||||
}),
|
||||
dataType: z.literal("BOOLEAN"),
|
||||
}),
|
||||
z.object({
|
||||
value: z.string().min(1).max(TEXT_SCORE_MAX_LENGTH),
|
||||
dataType: z.literal("TEXT"),
|
||||
}),
|
||||
]);
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import {
|
||||
AggregatableScoreDataType,
|
||||
ListableScoreDataType,
|
||||
MetadataDomain,
|
||||
ScoreSourceType,
|
||||
} from "../../../../domain";
|
||||
@@ -30,7 +30,7 @@ export type ScoreAggregate = Record<string, AggregatedScoreData>;
|
||||
export type ScoreSimplified = {
|
||||
id: string;
|
||||
name: string;
|
||||
dataType: AggregatableScoreDataType;
|
||||
dataType: ListableScoreDataType;
|
||||
source: ScoreSourceType;
|
||||
value?: number | null;
|
||||
comment?: string | null;
|
||||
|
||||
@@ -4,6 +4,7 @@ import { z } from "zod";
|
||||
import { NonEmptyString, jsonSchema } from "../../utils/zod";
|
||||
import { ModelUsageUnit } from "../../constants";
|
||||
import { ScoreSourceType } from "../../domain";
|
||||
import { TEXT_SCORE_MAX_LENGTH } from "../../domain/scores";
|
||||
import { applyScoreValidation } from "../../utils/scores";
|
||||
|
||||
export const idSchema = z
|
||||
@@ -526,21 +527,21 @@ const createAllIngestionSchemas = ({
|
||||
|
||||
const ScoreBody = applyScoreValidation(
|
||||
z.discriminatedUnion("dataType", [
|
||||
BaseScoreBody.merge(
|
||||
BaseScoreBody.extend(
|
||||
z.object({
|
||||
value: z.number(),
|
||||
dataType: z.literal("NUMERIC"),
|
||||
configId: z.string().nullish(),
|
||||
}),
|
||||
}).shape,
|
||||
),
|
||||
BaseScoreBody.merge(
|
||||
BaseScoreBody.extend(
|
||||
z.object({
|
||||
value: z.string(),
|
||||
dataType: z.literal("CATEGORICAL"),
|
||||
configId: z.string().nullish(),
|
||||
}),
|
||||
}).shape,
|
||||
),
|
||||
BaseScoreBody.merge(
|
||||
BaseScoreBody.extend(
|
||||
z.object({
|
||||
value: z.number().refine((value) => value === 0 || value === 1, {
|
||||
message:
|
||||
@@ -548,21 +549,28 @@ const createAllIngestionSchemas = ({
|
||||
}),
|
||||
dataType: z.literal("BOOLEAN"),
|
||||
configId: z.string().nullish(),
|
||||
}),
|
||||
}).shape,
|
||||
),
|
||||
BaseScoreBody.merge(
|
||||
BaseScoreBody.extend(
|
||||
z.object({
|
||||
value: z.string(),
|
||||
dataType: z.literal("CORRECTION"),
|
||||
configId: z.undefined().nullish(), // Cannot have config
|
||||
}),
|
||||
}).shape,
|
||||
),
|
||||
BaseScoreBody.merge(
|
||||
BaseScoreBody.extend(
|
||||
z.object({
|
||||
value: z.string().min(1).max(TEXT_SCORE_MAX_LENGTH),
|
||||
dataType: z.literal("TEXT"),
|
||||
configId: z.string().nullish(),
|
||||
}).shape,
|
||||
),
|
||||
BaseScoreBody.extend(
|
||||
z.object({
|
||||
value: z.union([z.string(), z.number()]),
|
||||
dataType: z.undefined(),
|
||||
configId: z.string().nullish(),
|
||||
}),
|
||||
}).shape,
|
||||
),
|
||||
]),
|
||||
);
|
||||
|
||||
@@ -64,7 +64,7 @@ export async function validateAndInflateScore(
|
||||
|
||||
if (!validation.success) {
|
||||
throw new InvalidRequestError(
|
||||
`Ingested score value type not valid against provided data type. Provide numeric values for numeric and boolean scores, and string values for categorical scores.`,
|
||||
`Ingested score value type not valid against provided data type. Provide numeric values for numeric and boolean scores, string values for categorical scores, and string values of 1-500 characters for text scores.`,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -141,6 +141,15 @@ function inflateScoreBody(
|
||||
};
|
||||
}
|
||||
|
||||
if (relevantDataType === ScoreDataTypeEnum.TEXT) {
|
||||
return {
|
||||
...scoreProps,
|
||||
value: 0,
|
||||
stringValue: body.value,
|
||||
dataType: ScoreDataTypeEnum.TEXT,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
...scoreProps,
|
||||
value: config ? mapStringValueToNumericValue(config, body.value) : 0,
|
||||
@@ -173,6 +182,7 @@ function resolveScoreValueAnnotation(
|
||||
case ScoreDataTypeEnum.BOOLEAN:
|
||||
return body.value;
|
||||
case ScoreDataTypeEnum.CATEGORICAL:
|
||||
case ScoreDataTypeEnum.TEXT:
|
||||
return body.stringValue;
|
||||
case ScoreDataTypeEnum.CORRECTION:
|
||||
throw new Error("CORRECTION type not supported in annotation drawer");
|
||||
|
||||
@@ -185,6 +185,39 @@ const FIELD_SETS = {
|
||||
"toolCalls",
|
||||
"toolCallNames",
|
||||
],
|
||||
baseWithoutTools: [
|
||||
"id",
|
||||
"type",
|
||||
"projectId",
|
||||
"name",
|
||||
"modelParameters",
|
||||
"startTime",
|
||||
"endTime",
|
||||
"traceId",
|
||||
"completionStartTime",
|
||||
"providedUsageDetails",
|
||||
"usageDetails",
|
||||
"providedCostDetails",
|
||||
"costDetails",
|
||||
"level",
|
||||
"environment",
|
||||
"bookmarked",
|
||||
"public",
|
||||
"statusMessage",
|
||||
"version",
|
||||
"parentObservationId",
|
||||
"createdAt",
|
||||
"updatedAt",
|
||||
"providedModelName",
|
||||
"totalCost",
|
||||
"promptId",
|
||||
"promptName",
|
||||
"promptVersion",
|
||||
"internalModelId",
|
||||
"userId",
|
||||
"sessionId",
|
||||
"traceName",
|
||||
],
|
||||
calculated: ["latency", "timeToFirstToken"],
|
||||
io: ["input", "output"],
|
||||
metadata: ["metadata"],
|
||||
|
||||
@@ -74,7 +74,7 @@ interface BaseScoresAggregationParams extends BaseScoresParams {
|
||||
hasScoreAggregationFilters?: boolean;
|
||||
/**
|
||||
* When true, adds an extra `score_categories_tuples` column with
|
||||
* `tuple(name, string_value)` encoding alongside the default concat-encoded
|
||||
* `tuple(name, string_value, data_type)` encoding alongside the default concat-encoded
|
||||
* `score_categories`. The tuple column is safe for programmatic parsing
|
||||
* (e.g. batch exports) when score names may contain colons.
|
||||
* The concat column is always present for hasAny filter compatibility.
|
||||
@@ -120,7 +120,7 @@ export const buildScoresAggregationCTE = (
|
||||
${primaryKey},
|
||||
${additionalOuterCols.length > 0 ? additionalOuterCols.join(",\n ") + "," : ""}
|
||||
groupArrayIf(tuple(name, avg_value, data_type, string_value), data_type IN ('NUMERIC', 'BOOLEAN')) AS scores_avg,
|
||||
groupArrayIf(concat(name, ':', string_value), data_type = 'CATEGORICAL' AND notEmpty(string_value)) AS score_categories${params.includeTupleEncoding ? `,\n groupArrayIf(tuple(name, string_value), data_type = 'CATEGORICAL' AND notEmpty(string_value)) AS score_categories_tuples` : ""}
|
||||
groupArrayIf(concat(name, ':', string_value), data_type IN ('CATEGORICAL', 'TEXT') AND notEmpty(string_value)) AS score_categories${params.includeTupleEncoding ? `,\n groupArrayIf(tuple(name, string_value, data_type), data_type IN ('CATEGORICAL', 'TEXT') AND notEmpty(string_value)) AS score_categories_tuples` : ""}
|
||||
FROM (
|
||||
SELECT
|
||||
${primaryKey},
|
||||
@@ -319,7 +319,7 @@ export const eventsSessionScoresAggregation = (params: {
|
||||
) AS scores_avg,
|
||||
groupArrayIf(
|
||||
concat(name, ':', string_value),
|
||||
data_type = 'CATEGORICAL' AND notEmpty(string_value)
|
||||
data_type IN ('CATEGORICAL', 'TEXT') AND notEmpty(string_value)
|
||||
) AS score_categories
|
||||
FROM (
|
||||
SELECT
|
||||
|
||||
@@ -375,16 +375,35 @@ export async function upsertDatasetItem(
|
||||
[Implementation.VERSIONED]: async () => {
|
||||
// VERSIONED: Invalidate old row by setting valid_to, then create new row
|
||||
await prisma.$transaction(async (tx) => {
|
||||
const newValidFrom = new Date();
|
||||
// 0. Re-read if there is an existing item to get the validFrom timestamp
|
||||
const current = await tx.datasetItem.findFirst({
|
||||
where: {
|
||||
id: itemId,
|
||||
projectId: props.projectId,
|
||||
validTo: null,
|
||||
},
|
||||
orderBy: {
|
||||
validFrom: "desc",
|
||||
},
|
||||
});
|
||||
|
||||
if (current && current.datasetId !== dataset.id) {
|
||||
throw new LangfuseNotFoundError(
|
||||
`Dataset item with id ${itemId} not found for project ${props.projectId}`,
|
||||
);
|
||||
}
|
||||
|
||||
const baseTs = current?.validFrom.getTime() ?? 0;
|
||||
const newValidFrom = new Date(Math.max(Date.now(), baseTs + 1));
|
||||
|
||||
// 1. If updating existing item, invalidate the current version
|
||||
if (existingItem) {
|
||||
if (current) {
|
||||
await tx.datasetItem.update({
|
||||
where: {
|
||||
id_projectId_validFrom: {
|
||||
id: existingItem.id,
|
||||
id: current.id,
|
||||
projectId: props.projectId,
|
||||
validFrom: existingItem.validFrom,
|
||||
validFrom: current.validFrom,
|
||||
},
|
||||
},
|
||||
data: {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { AGGREGATABLE_SCORE_TYPES } from "../../domain/scores";
|
||||
import { LISTABLE_SCORE_TYPES } from "../../domain/scores";
|
||||
import { queryClickhouse } from "./clickhouse";
|
||||
|
||||
export type EnvironmentFilterProps = {
|
||||
@@ -38,7 +38,7 @@ export const getEnvironmentsForProject = async (
|
||||
params: {
|
||||
projectId,
|
||||
fromTimestamp,
|
||||
dataTypes: AGGREGATABLE_SCORE_TYPES,
|
||||
dataTypes: LISTABLE_SCORE_TYPES,
|
||||
},
|
||||
tags: {
|
||||
feature: "tracing",
|
||||
|
||||
@@ -337,6 +337,7 @@ export const getObservationsForTraceFromEventsTable = async (params: {
|
||||
limit: MAX_OBSERVATIONS_PER_TRACE + 1,
|
||||
offset: 0,
|
||||
select: "rows",
|
||||
selectToolData: false,
|
||||
tags: { kind: "byTraceId" },
|
||||
},
|
||||
);
|
||||
@@ -394,6 +395,7 @@ export const getObservationsWithModelDataFromEventsTable = async (
|
||||
async function getObservationsFromEventsTableInternal<T>(
|
||||
opts: ObservationTableQuery & {
|
||||
select: "count" | "rows";
|
||||
selectToolData?: boolean;
|
||||
tags: Record<string, string>;
|
||||
},
|
||||
): Promise<Array<T>> {
|
||||
@@ -401,6 +403,7 @@ async function getObservationsFromEventsTableInternal<T>(
|
||||
projectId,
|
||||
filter,
|
||||
selectIOAndMetadata,
|
||||
selectToolData = true,
|
||||
renderingProps = DEFAULT_RENDERING_PROPS,
|
||||
limit,
|
||||
offset,
|
||||
@@ -461,7 +464,10 @@ async function getObservationsFromEventsTableInternal<T>(
|
||||
if (opts.select === "count") {
|
||||
queryBuilder.selectFieldSet("count");
|
||||
} else {
|
||||
queryBuilder.selectFieldSet("base", "calculated");
|
||||
queryBuilder.selectFieldSet(
|
||||
selectToolData ? "base" : "baseWithoutTools",
|
||||
"calculated",
|
||||
);
|
||||
if (selectIOAndMetadata) {
|
||||
queryBuilder
|
||||
.selectIO(
|
||||
|
||||
@@ -175,9 +175,7 @@ export const getObservationsForTrace = async <IncludeIO extends boolean>(
|
||||
prompt_id,
|
||||
prompt_name,
|
||||
prompt_version,
|
||||
tool_definitions,
|
||||
tool_calls,
|
||||
tool_call_names,
|
||||
${includeIO === true ? "tool_definitions, tool_calls, tool_call_names," : ""}
|
||||
created_at,
|
||||
updated_at,
|
||||
event_ts
|
||||
|
||||
@@ -5,6 +5,8 @@ import {
|
||||
AGGREGATABLE_SCORE_TYPES,
|
||||
AggregatableScoreDataType,
|
||||
ScoreByDataType,
|
||||
LISTABLE_SCORE_TYPES,
|
||||
ListableScoreDataType,
|
||||
} from "../../domain/scores";
|
||||
import {
|
||||
commandClickhouse,
|
||||
@@ -131,7 +133,7 @@ export const getScoresByIds = async (
|
||||
scoreId,
|
||||
source,
|
||||
scoreScope: "all",
|
||||
dataTypes: AGGREGATABLE_SCORE_TYPES,
|
||||
dataTypes: LISTABLE_SCORE_TYPES,
|
||||
});
|
||||
};
|
||||
|
||||
@@ -249,7 +251,7 @@ export const getScoresForSessions = async <
|
||||
sessionIds,
|
||||
limit,
|
||||
offset,
|
||||
dataTypes: AGGREGATABLE_SCORE_TYPES,
|
||||
dataTypes: LISTABLE_SCORE_TYPES,
|
||||
},
|
||||
tags: {
|
||||
feature: "sessions",
|
||||
@@ -594,7 +596,7 @@ export const getScoresForTraces = async <
|
||||
) => {
|
||||
return getScoresForTracesInternal({
|
||||
...props,
|
||||
dataTypes: AGGREGATABLE_SCORE_TYPES,
|
||||
dataTypes: LISTABLE_SCORE_TYPES,
|
||||
});
|
||||
};
|
||||
|
||||
@@ -673,7 +675,7 @@ export const getScoresForObservations = async <
|
||||
params: {
|
||||
projectId: projectId,
|
||||
observationIds: observationIds,
|
||||
dataTypes: AGGREGATABLE_SCORE_TYPES,
|
||||
dataTypes: LISTABLE_SCORE_TYPES,
|
||||
limit: limit,
|
||||
offset: offset,
|
||||
},
|
||||
@@ -809,7 +811,7 @@ export const getScoresGroupedByNameSourceType = async ({
|
||||
query: query,
|
||||
params: {
|
||||
projectId: projectId,
|
||||
dataTypes: AGGREGATABLE_SCORE_TYPES,
|
||||
dataTypes: LISTABLE_SCORE_TYPES,
|
||||
...(fromTimestamp
|
||||
? { fromTimestamp: convertDateToClickhouseDateTime(fromTimestamp) }
|
||||
: {}),
|
||||
@@ -832,7 +834,7 @@ export const getScoresGroupedByNameSourceType = async ({
|
||||
return rows.map((row) => ({
|
||||
name: row.name,
|
||||
source: row.source as ScoreSourceType,
|
||||
dataType: row.data_type as AggregatableScoreDataType,
|
||||
dataType: row.data_type as ListableScoreDataType,
|
||||
}));
|
||||
};
|
||||
|
||||
@@ -1189,7 +1191,7 @@ const getScoresUiGeneric = async <T>(props: {
|
||||
input: {
|
||||
params: {
|
||||
projectId: projectId,
|
||||
dataTypes: AGGREGATABLE_SCORE_TYPES,
|
||||
dataTypes: LISTABLE_SCORE_TYPES,
|
||||
...(scoresFilterRes ? scoresFilterRes.params : {}),
|
||||
limit: limit,
|
||||
offset: offset,
|
||||
@@ -1393,7 +1395,7 @@ const getScoresUiGenericFromEvents = async <T>(props: {
|
||||
input: {
|
||||
params: {
|
||||
projectId,
|
||||
dataTypes: AGGREGATABLE_SCORE_TYPES,
|
||||
dataTypes: LISTABLE_SCORE_TYPES,
|
||||
...(scoreOnlyFilterRes ? scoreOnlyFilterRes.params : {}),
|
||||
...tracesCTEParams,
|
||||
limit,
|
||||
@@ -1535,7 +1537,7 @@ export const getScoreNames = async (
|
||||
params: {
|
||||
projectId: projectId,
|
||||
...(timestampFilterRes ? timestampFilterRes.params : {}),
|
||||
dataTypes: AGGREGATABLE_SCORE_TYPES,
|
||||
dataTypes: LISTABLE_SCORE_TYPES,
|
||||
},
|
||||
tags: {
|
||||
feature: "tracing",
|
||||
@@ -1564,6 +1566,8 @@ export const getScoreStringValues = async (
|
||||
);
|
||||
const timestampFilterRes = chFilter.apply();
|
||||
|
||||
// exclude TEXT scores as they are arbitrary by nature and hence have high cardinality
|
||||
// which in turn can lead to performance issues
|
||||
const query = `
|
||||
select
|
||||
string_value,
|
||||
@@ -1572,6 +1576,7 @@ export const getScoreStringValues = async (
|
||||
WHERE s.project_id = {projectId: String}
|
||||
AND string_value IS NOT NULL
|
||||
AND string_value != ''
|
||||
AND s.data_type != 'TEXT'
|
||||
${timestampFilterRes?.query ? `AND ${timestampFilterRes.query}` : ""}
|
||||
GROUP BY string_value
|
||||
ORDER BY count() desc
|
||||
@@ -1838,7 +1843,7 @@ export const getAggregatedScoresForPrompts = async (
|
||||
params: {
|
||||
projectId,
|
||||
promptIds,
|
||||
dataTypes: AGGREGATABLE_SCORE_TYPES,
|
||||
dataTypes: LISTABLE_SCORE_TYPES,
|
||||
},
|
||||
tags: {
|
||||
feature: "tracing",
|
||||
@@ -1849,7 +1854,7 @@ export const getAggregatedScoresForPrompts = async (
|
||||
});
|
||||
|
||||
return rows.map((row) => ({
|
||||
...convertScoreAggregation<AggregatableScoreDataType>(row),
|
||||
...convertScoreAggregation<ListableScoreDataType>(row),
|
||||
promptId: row.prompt_id,
|
||||
hasMetadata: !!row.has_metadata,
|
||||
}));
|
||||
@@ -1878,7 +1883,7 @@ export const getScoreCountsByProjectInCreationInterval = async ({
|
||||
params: {
|
||||
start: convertDateToClickhouseDateTime(start),
|
||||
end: convertDateToClickhouseDateTime(end),
|
||||
dataTypes: AGGREGATABLE_SCORE_TYPES,
|
||||
dataTypes: LISTABLE_SCORE_TYPES,
|
||||
},
|
||||
clickhouseConfigs: {
|
||||
request_timeout: 120000, // 2 minutes timeout
|
||||
@@ -1957,7 +1962,7 @@ export const getDistinctScoreNames = async (p: {
|
||||
params: {
|
||||
projectId,
|
||||
cutoffCreatedAt: convertDateToClickhouseDateTime(cutoffCreatedAt),
|
||||
dataTypes: AGGREGATABLE_SCORE_TYPES,
|
||||
dataTypes: LISTABLE_SCORE_TYPES,
|
||||
...(scoreTimestampFilter
|
||||
? {
|
||||
filterTimestamp: convertDateToClickhouseDateTime(
|
||||
@@ -2014,7 +2019,7 @@ export const getScoresForBlobStorageExport = function (
|
||||
projectId,
|
||||
minTimestamp: convertDateToClickhouseDateTime(minTimestamp),
|
||||
maxTimestamp: convertDateToClickhouseDateTime(maxTimestamp),
|
||||
dataTypes: AGGREGATABLE_SCORE_TYPES,
|
||||
dataTypes: LISTABLE_SCORE_TYPES,
|
||||
},
|
||||
tags: {
|
||||
feature: "blobstorage",
|
||||
@@ -2087,7 +2092,7 @@ export const getScoresForAnalyticsIntegrations = async function* (
|
||||
projectId,
|
||||
minTimestamp: convertDateToClickhouseDateTime(minTimestamp),
|
||||
maxTimestamp: convertDateToClickhouseDateTime(maxTimestamp),
|
||||
dataTypes: AGGREGATABLE_SCORE_TYPES,
|
||||
dataTypes: LISTABLE_SCORE_TYPES,
|
||||
},
|
||||
tags: {
|
||||
feature: "posthog",
|
||||
@@ -2260,7 +2265,7 @@ export const getScoreCountsByProjectAndDay = async ({
|
||||
params: {
|
||||
startDate: convertDateToClickhouseDateTime(startDate),
|
||||
endDate: convertDateToClickhouseDateTime(endDate),
|
||||
dataTypes: AGGREGATABLE_SCORE_TYPES,
|
||||
dataTypes: LISTABLE_SCORE_TYPES,
|
||||
},
|
||||
tags: {
|
||||
feature: "tracing",
|
||||
|
||||
@@ -506,6 +506,7 @@ export const getTraceById = async ({
|
||||
clickhouseFeatureTag = "tracing",
|
||||
preferredClickhouseService,
|
||||
excludeInputOutput = false,
|
||||
excludeMetadata = false,
|
||||
}: {
|
||||
traceId: string;
|
||||
projectId: string;
|
||||
@@ -516,6 +517,8 @@ export const getTraceById = async ({
|
||||
preferredClickhouseService?: PreferredClickhouseService;
|
||||
/** When true, sets input/output columns to empty in the query to reduce database load */
|
||||
excludeInputOutput?: boolean;
|
||||
/** When true, sets metadata column to empty in the query to reduce database load */
|
||||
excludeMetadata?: boolean;
|
||||
}) => {
|
||||
const records = await measureAndReturn({
|
||||
operationName: "getTraceById",
|
||||
@@ -550,13 +553,14 @@ export const getTraceById = async ({
|
||||
: renderingProps.truncated
|
||||
? `leftUTF8(output, ${env.LANGFUSE_SERVER_SIDE_IO_CHAR_LIMIT})`
|
||||
: "output";
|
||||
const metadataColumn = excludeMetadata ? "'{}'" : "metadata";
|
||||
|
||||
const query = `
|
||||
SELECT
|
||||
id,
|
||||
name as name,
|
||||
user_id as user_id,
|
||||
metadata as metadata,
|
||||
${metadataColumn} as metadata,
|
||||
release as release,
|
||||
version as version,
|
||||
project_id,
|
||||
|
||||
Generated
+351
-108
@@ -9,7 +9,6 @@ overrides:
|
||||
nanoid: ^3.3.8
|
||||
katex: ^0.16.21
|
||||
tar-fs: ^2.1.2
|
||||
effect: ^3.20.0
|
||||
rollup@^4.0.0: ^4.22.4
|
||||
'@types/node-fetch': ^2.6.13
|
||||
'@types/react-dom': 19.2.3
|
||||
@@ -44,8 +43,8 @@ importers:
|
||||
specifier: ^19.2.4
|
||||
version: 19.2.4(@types/node@24.10.4)(magicast@0.5.2)
|
||||
turbo:
|
||||
specifier: 2.8.20
|
||||
version: 2.8.20
|
||||
specifier: 2.9.5
|
||||
version: 2.9.5
|
||||
|
||||
ee:
|
||||
dependencies:
|
||||
@@ -111,8 +110,8 @@ importers:
|
||||
specifier: ^10.1.8
|
||||
version: 10.1.8(eslint@9.39.4(jiti@2.6.1))
|
||||
eslint-config-turbo:
|
||||
specifier: 2.8.20
|
||||
version: 2.8.20(eslint@9.39.4(jiti@2.6.1))(turbo@2.8.20)
|
||||
specifier: 2.9.5
|
||||
version: 2.9.5(eslint@9.39.4(jiti@2.6.1))(turbo@2.9.5)
|
||||
eslint-plugin-only-warn:
|
||||
specifier: ^1.1.0
|
||||
version: 1.2.1
|
||||
@@ -179,8 +178,8 @@ importers:
|
||||
specifier: '>=1.0.0 <1.10.0'
|
||||
version: 1.9.0
|
||||
'@prisma/client':
|
||||
specifier: ^6.17.1
|
||||
version: 6.17.1(prisma@6.17.1(magicast@0.5.2)(typescript@5.9.2))(typescript@5.9.2)
|
||||
specifier: ^6.19.3
|
||||
version: 6.19.3(prisma@6.19.3(magicast@0.5.2)(typescript@5.9.2))(typescript@5.9.2)
|
||||
'@react-email/components':
|
||||
specifier: ^0.5.1
|
||||
version: 0.5.1(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
|
||||
@@ -297,8 +296,8 @@ importers:
|
||||
specifier: ^3.8.1
|
||||
version: 3.8.1
|
||||
prisma:
|
||||
specifier: ^6.17.1
|
||||
version: 6.17.1(magicast@0.5.2)(typescript@5.9.2)
|
||||
specifier: ^6.19.3
|
||||
version: 6.19.3(magicast@0.5.2)(typescript@5.9.2)
|
||||
ts-node:
|
||||
specifier: ^10.9.2
|
||||
version: 10.9.2(@types/node@24.3.0)(typescript@5.9.2)
|
||||
@@ -375,14 +374,14 @@ importers:
|
||||
specifier: ^1.2.3
|
||||
version: 1.2.3
|
||||
'@modelcontextprotocol/sdk':
|
||||
specifier: 1.27.1
|
||||
version: 1.27.1(@cfworker/json-schema@4.1.1)(zod@4.3.6)
|
||||
specifier: 1.29.0
|
||||
version: 1.29.0(@cfworker/json-schema@4.1.1)(zod@4.3.6)
|
||||
'@mui/material':
|
||||
specifier: ^7.3.9
|
||||
version: 7.3.9(@emotion/react@11.11.4(@types/react@19.2.14)(react@19.2.4))(@emotion/styled@11.11.5(@emotion/react@11.11.4(@types/react@19.2.14)(react@19.2.4))(@types/react@19.2.14)(react@19.2.4))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
|
||||
'@next-auth/prisma-adapter':
|
||||
specifier: ^1.0.7
|
||||
version: 1.0.7(@prisma/client@6.17.1(prisma@6.17.1(magicast@0.5.2)(typescript@5.9.2))(typescript@5.9.2))(next-auth@4.24.13(patch_hash=7b301d2607ee3cd8bba4f6978ba8cdd7a616b59d6e414191da3b38950ce08e66)(next@16.2.1(@babel/core@7.29.0)(@opentelemetry/api@1.9.0)(@playwright/test@1.57.0)(babel-plugin-macros@3.1.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(nodemailer@7.0.11)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))
|
||||
version: 1.0.7(@prisma/client@6.19.3(prisma@6.19.3(magicast@0.5.2)(typescript@5.9.2))(typescript@5.9.2))(next-auth@4.24.13(patch_hash=7b301d2607ee3cd8bba4f6978ba8cdd7a616b59d6e414191da3b38950ce08e66)(next@16.2.1(@babel/core@7.29.0)(@opentelemetry/api@1.9.0)(@playwright/test@1.57.0)(babel-plugin-macros@3.1.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(nodemailer@7.0.11)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))
|
||||
'@opentelemetry/api':
|
||||
specifier: ^1.9.0
|
||||
version: 1.9.0
|
||||
@@ -429,8 +428,8 @@ importers:
|
||||
specifier: ^2.2.2
|
||||
version: 2.2.2
|
||||
'@prisma/instrumentation':
|
||||
specifier: ^6.17.1
|
||||
version: 6.17.1(@opentelemetry/api@1.9.0)
|
||||
specifier: ^6.19.3
|
||||
version: 6.19.3(@opentelemetry/api@1.9.0)
|
||||
'@radix-ui/react-accordion':
|
||||
specifier: ^1.2.12
|
||||
version: 1.2.12(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
|
||||
@@ -639,8 +638,8 @@ importers:
|
||||
specifier: ^2.4.1
|
||||
version: 2.4.1(react@19.2.4)
|
||||
prisma:
|
||||
specifier: ^6.17.1
|
||||
version: 6.17.1(magicast@0.5.2)(typescript@5.9.2)
|
||||
specifier: ^6.19.3
|
||||
version: 6.19.3(magicast@0.5.2)(typescript@5.9.2)
|
||||
protobufjs:
|
||||
specifier: ^7.4.0
|
||||
version: 7.4.0
|
||||
@@ -889,8 +888,8 @@ importers:
|
||||
specifier: ^0.53.0
|
||||
version: 0.53.0(@opentelemetry/api@1.9.0)
|
||||
'@prisma/instrumentation':
|
||||
specifier: ^6.17.1
|
||||
version: 6.17.1(@opentelemetry/api@1.9.0)
|
||||
specifier: ^6.19.3
|
||||
version: 6.19.3(@opentelemetry/api@1.9.0)
|
||||
backoff:
|
||||
specifier: ^2.5.0
|
||||
version: 2.5.0
|
||||
@@ -2809,8 +2808,8 @@ packages:
|
||||
'@marijn/find-cluster-break@1.0.2':
|
||||
resolution: {integrity: sha512-l0h88YhZFyKdXIFNfSWpyjStDjGHwZ/U7iobcK1cQQD8sejsONdQtTVU+1wVN1PBw40PiiHB1vA5S7VTfQiP9g==}
|
||||
|
||||
'@modelcontextprotocol/sdk@1.27.1':
|
||||
resolution: {integrity: sha512-sr6GbP+4edBwFndLbM60gf07z0FQ79gaExpnsjMGePXqFcSSb7t6iscpjk9DhFhwd+mTEQrzNafGP8/iGGFYaA==}
|
||||
'@modelcontextprotocol/sdk@1.29.0':
|
||||
resolution: {integrity: sha512-zo37mZA9hJWpULgkRpowewez1y6ML5GsXJPY8FI0tBBCd77HEvza4jDqRKOXgHNn867PVGCyTdzqpz0izu5ZjQ==}
|
||||
engines: {node: '>=18'}
|
||||
peerDependencies:
|
||||
'@cfworker/json-schema': ^4.1.1
|
||||
@@ -3536,8 +3535,8 @@ packages:
|
||||
'@posthog/core@1.2.2':
|
||||
resolution: {integrity: sha512-f16Ozx6LIigRG+HsJdt+7kgSxZTHeX5f1JlCGKI1lXcvlZgfsCR338FuMI2QRYXGl+jg/vYFzGOTQBxl90lnBg==}
|
||||
|
||||
'@prisma/client@6.17.1':
|
||||
resolution: {integrity: sha512-zL58jbLzYamjnNnmNA51IOZdbk5ci03KviXCuB0Tydc9btH2kDWsi1pQm2VecviRTM7jGia0OPPkgpGnT3nKvw==}
|
||||
'@prisma/client@6.19.3':
|
||||
resolution: {integrity: sha512-mKq3jQFhjvko5LTJFHGilsuQs+W+T3Gm451NzuTDGQxwCzwXHYnIu2zGkRoW+Exq3Rob7yp2MfzSrdIiZVhrBg==}
|
||||
engines: {node: '>=18.18'}
|
||||
peerDependencies:
|
||||
prisma: '*'
|
||||
@@ -3548,26 +3547,26 @@ packages:
|
||||
typescript:
|
||||
optional: true
|
||||
|
||||
'@prisma/config@6.17.1':
|
||||
resolution: {integrity: sha512-fs8wY6DsvOCzuiyWVckrVs1LOcbY4LZNz8ki4uUIQ28jCCzojTGqdLhN2Jl5lDnC1yI8/gNIKpsWDM8pLhOdwA==}
|
||||
'@prisma/config@6.19.3':
|
||||
resolution: {integrity: sha512-CBPT44BjlQxEt8kiMEauji2WHTDoVBOKl7UlewXmUgBPnr/oPRZC3psci5chJnYmH0ivEIog2OU9PGWoki3DLQ==}
|
||||
|
||||
'@prisma/debug@6.17.1':
|
||||
resolution: {integrity: sha512-Vf7Tt5Wh9XcndpbmeotuqOMLWPTjEKCsgojxXP2oxE1/xYe7PtnP76hsouG9vis6fctX+TxgmwxTuYi/+xc7dQ==}
|
||||
'@prisma/debug@6.19.3':
|
||||
resolution: {integrity: sha512-ljkJ+SgpXNktLG0Q/n4JGYCkKf0f8oYLyjImS2I8e2q2WCfdRRtWER062ZV/ixaNP2M2VKlWXVJiGzZaUgbKZw==}
|
||||
|
||||
'@prisma/engines-version@6.17.1-1.272a37d34178c2894197e17273bf937f25acdeac':
|
||||
resolution: {integrity: sha512-17140E3huOuD9lMdJ9+SF/juOf3WR3sTJMVyyenzqUPbuH+89nPhSWcrY+Mf7tmSs6HvaO+7S+HkELinn6bhdg==}
|
||||
'@prisma/engines-version@7.1.1-3.c2990dca591cba766e3b7ef5d9e8a84796e47ab7':
|
||||
resolution: {integrity: sha512-03bgb1VD5gvuumNf+7fVGBzfpJPjmqV423l/WxsWk2cNQ42JD0/SsFBPhN6z8iAvdHs07/7ei77SKu7aZfq8bA==}
|
||||
|
||||
'@prisma/engines@6.17.1':
|
||||
resolution: {integrity: sha512-D95Ik3GYZkqZ8lSR4EyFOJ/tR33FcYRP8kK61o+WMsyD10UfJwd7+YielflHfKwiGodcqKqoraWw8ElAgMDbPw==}
|
||||
'@prisma/engines@6.19.3':
|
||||
resolution: {integrity: sha512-RSYxtlYFl5pJ8ZePgMv0lZ9IzVCOdTPOegrs2qcbAEFrBI1G33h6wyC9kjQvo0DnYEhEVY0X4LsuFHXLKQk88g==}
|
||||
|
||||
'@prisma/fetch-engine@6.17.1':
|
||||
resolution: {integrity: sha512-AYZiHOs184qkDMiTeshyJCtyL4yERkjfTkJiSJdYuSfc24m94lTNL5+GFinZ6vVz+ktX4NJzHKn1zIFzGTWrWg==}
|
||||
'@prisma/fetch-engine@6.19.3':
|
||||
resolution: {integrity: sha512-tKtl/qco9Nt7LU5iKhpultD8O4vMCZcU2CHjNTnRrL1QvSUr5W/GcyFPjNL87GtRrwBc7ubXXD9xy4EvLvt8JA==}
|
||||
|
||||
'@prisma/get-platform@6.17.1':
|
||||
resolution: {integrity: sha512-AKEn6fsfz0r482S5KRDFlIGEaq9wLNcgalD1adL+fPcFFblIKs1sD81kY/utrHdqKuVC6E1XSRpegDK3ZLL4Qg==}
|
||||
'@prisma/get-platform@6.19.3':
|
||||
resolution: {integrity: sha512-xFj1VcJ1N3MKooOQAGO0W5tsd0W2QzIvW7DD7c/8H14Zmp4jseeWAITm+w2LLoLrlhoHdPPh0NMZ8mfL6puoHA==}
|
||||
|
||||
'@prisma/instrumentation@6.17.1':
|
||||
resolution: {integrity: sha512-orza4eufBmjAcUAIs7OghKNc3p1vukSvEHr+sv9dT3BeiwvUdLAw/Fns+4B7iYmvPZfdnLHc7XqOF5FldZEeVw==}
|
||||
'@prisma/instrumentation@6.19.3':
|
||||
resolution: {integrity: sha512-gwU1D4pxrtBZXINZwSseEal+h8OghvIVZIH3VRv6+vPFAkZk1ykFUVcOhhboBc5WmhoD42NTJjla2i7GFNcZWA==}
|
||||
peerDependencies:
|
||||
'@opentelemetry/api': ^1.8
|
||||
|
||||
@@ -4453,139 +4452,277 @@ packages:
|
||||
cpu: [arm]
|
||||
os: [android]
|
||||
|
||||
'@rollup/rollup-android-arm-eabi@4.60.1':
|
||||
resolution: {integrity: sha512-d6FinEBLdIiK+1uACUttJKfgZREXrF0Qc2SmLII7W2AD8FfiZ9Wjd+rD/iRuf5s5dWrr1GgwXCvPqOuDquOowA==}
|
||||
cpu: [arm]
|
||||
os: [android]
|
||||
|
||||
'@rollup/rollup-android-arm64@4.60.0':
|
||||
resolution: {integrity: sha512-u6JHLll5QKRvjciE78bQXDmqRqNs5M/3GVqZeMwvmjaNODJih/WIrJlFVEihvV0MiYFmd+ZyPr9wxOVbPAG2Iw==}
|
||||
cpu: [arm64]
|
||||
os: [android]
|
||||
|
||||
'@rollup/rollup-android-arm64@4.60.1':
|
||||
resolution: {integrity: sha512-YjG/EwIDvvYI1YvYbHvDz/BYHtkY4ygUIXHnTdLhG+hKIQFBiosfWiACWortsKPKU/+dUwQQCKQM3qrDe8c9BA==}
|
||||
cpu: [arm64]
|
||||
os: [android]
|
||||
|
||||
'@rollup/rollup-darwin-arm64@4.60.0':
|
||||
resolution: {integrity: sha512-qEF7CsKKzSRc20Ciu2Zw1wRrBz4g56F7r/vRwY430UPp/nt1x21Q/fpJ9N5l47WWvJlkNCPJz3QRVw008fi7yA==}
|
||||
cpu: [arm64]
|
||||
os: [darwin]
|
||||
|
||||
'@rollup/rollup-darwin-arm64@4.60.1':
|
||||
resolution: {integrity: sha512-mjCpF7GmkRtSJwon+Rq1N8+pI+8l7w5g9Z3vWj4T7abguC4Czwi3Yu/pFaLvA3TTeMVjnu3ctigusqWUfjZzvw==}
|
||||
cpu: [arm64]
|
||||
os: [darwin]
|
||||
|
||||
'@rollup/rollup-darwin-x64@4.60.0':
|
||||
resolution: {integrity: sha512-WADYozJ4QCnXCH4wPB+3FuGmDPoFseVCUrANmA5LWwGmC6FL14BWC7pcq+FstOZv3baGX65tZ378uT6WG8ynTw==}
|
||||
cpu: [x64]
|
||||
os: [darwin]
|
||||
|
||||
'@rollup/rollup-darwin-x64@4.60.1':
|
||||
resolution: {integrity: sha512-haZ7hJ1JT4e9hqkoT9R/19XW2QKqjfJVv+i5AGg57S+nLk9lQnJ1F/eZloRO3o9Scy9CM3wQ9l+dkXtcBgN5Ew==}
|
||||
cpu: [x64]
|
||||
os: [darwin]
|
||||
|
||||
'@rollup/rollup-freebsd-arm64@4.60.0':
|
||||
resolution: {integrity: sha512-6b8wGHJlDrGeSE3aH5mGNHBjA0TTkxdoNHik5EkvPHCt351XnigA4pS7Wsj/Eo9Y8RBU6f35cjN9SYmCFBtzxw==}
|
||||
cpu: [arm64]
|
||||
os: [freebsd]
|
||||
|
||||
'@rollup/rollup-freebsd-arm64@4.60.1':
|
||||
resolution: {integrity: sha512-czw90wpQq3ZsAVBlinZjAYTKduOjTywlG7fEeWKUA7oCmpA8xdTkxZZlwNJKWqILlq0wehoZcJYfBvOyhPTQ6w==}
|
||||
cpu: [arm64]
|
||||
os: [freebsd]
|
||||
|
||||
'@rollup/rollup-freebsd-x64@4.60.0':
|
||||
resolution: {integrity: sha512-h25Ga0t4jaylMB8M/JKAyrvvfxGRjnPQIR8lnCayyzEjEOx2EJIlIiMbhpWxDRKGKF8jbNH01NnN663dH638mA==}
|
||||
cpu: [x64]
|
||||
os: [freebsd]
|
||||
|
||||
'@rollup/rollup-freebsd-x64@4.60.1':
|
||||
resolution: {integrity: sha512-KVB2rqsxTHuBtfOeySEyzEOB7ltlB/ux38iu2rBQzkjbwRVlkhAGIEDiiYnO2kFOkJp+Z7pUXKyrRRFuFUKt+g==}
|
||||
cpu: [x64]
|
||||
os: [freebsd]
|
||||
|
||||
'@rollup/rollup-linux-arm-gnueabihf@4.60.0':
|
||||
resolution: {integrity: sha512-RzeBwv0B3qtVBWtcuABtSuCzToo2IEAIQrcyB/b2zMvBWVbjo8bZDjACUpnaafaxhTw2W+imQbP2BD1usasK4g==}
|
||||
cpu: [arm]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@rollup/rollup-linux-arm-gnueabihf@4.60.1':
|
||||
resolution: {integrity: sha512-L+34Qqil+v5uC0zEubW7uByo78WOCIrBvci69E7sFASRl0X7b/MB6Cqd1lky/CtcSVTydWa2WZwFuWexjS5o6g==}
|
||||
cpu: [arm]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@rollup/rollup-linux-arm-musleabihf@4.60.0':
|
||||
resolution: {integrity: sha512-Sf7zusNI2CIU1HLzuu9Tc5YGAHEZs5Lu7N1ssJG4Tkw6e0MEsN7NdjUDDfGNHy2IU+ENyWT+L2obgWiguWibWQ==}
|
||||
cpu: [arm]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@rollup/rollup-linux-arm-musleabihf@4.60.1':
|
||||
resolution: {integrity: sha512-n83O8rt4v34hgFzlkb1ycniJh7IR5RCIqt6mz1VRJD6pmhRi0CXdmfnLu9dIUS6buzh60IvACM842Ffb3xd6Gg==}
|
||||
cpu: [arm]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@rollup/rollup-linux-arm64-gnu@4.60.0':
|
||||
resolution: {integrity: sha512-DX2x7CMcrJzsE91q7/O02IJQ5/aLkVtYFryqCjduJhUfGKG6yJV8hxaw8pZa93lLEpPTP/ohdN4wFz7yp/ry9A==}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@rollup/rollup-linux-arm64-gnu@4.60.1':
|
||||
resolution: {integrity: sha512-Nql7sTeAzhTAja3QXeAI48+/+GjBJ+QmAH13snn0AJSNL50JsDqotyudHyMbO2RbJkskbMbFJfIJKWA6R1LCJQ==}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@rollup/rollup-linux-arm64-musl@4.60.0':
|
||||
resolution: {integrity: sha512-09EL+yFVbJZlhcQfShpswwRZ0Rg+z/CsSELFCnPt3iK+iqwGsI4zht3secj5vLEs957QvFFXnzAT0FFPIxSrkQ==}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@rollup/rollup-linux-arm64-musl@4.60.1':
|
||||
resolution: {integrity: sha512-+pUymDhd0ys9GcKZPPWlFiZ67sTWV5UU6zOJat02M1+PiuSGDziyRuI/pPue3hoUwm2uGfxdL+trT6Z9rxnlMA==}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@rollup/rollup-linux-loong64-gnu@4.60.0':
|
||||
resolution: {integrity: sha512-i9IcCMPr3EXm8EQg5jnja0Zyc1iFxJjZWlb4wr7U2Wx/GrddOuEafxRdMPRYVaXjgbhvqalp6np07hN1w9kAKw==}
|
||||
cpu: [loong64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@rollup/rollup-linux-loong64-gnu@4.60.1':
|
||||
resolution: {integrity: sha512-VSvgvQeIcsEvY4bKDHEDWcpW4Yw7BtlKG1GUT4FzBUlEKQK0rWHYBqQt6Fm2taXS+1bXvJT6kICu5ZwqKCnvlQ==}
|
||||
cpu: [loong64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@rollup/rollup-linux-loong64-musl@4.60.0':
|
||||
resolution: {integrity: sha512-DGzdJK9kyJ+B78MCkWeGnpXJ91tK/iKA6HwHxF4TAlPIY7GXEvMe8hBFRgdrR9Ly4qebR/7gfUs9y2IoaVEyog==}
|
||||
cpu: [loong64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@rollup/rollup-linux-loong64-musl@4.60.1':
|
||||
resolution: {integrity: sha512-4LqhUomJqwe641gsPp6xLfhqWMbQV04KtPp7/dIp0nzPxAkNY1AbwL5W0MQpcalLYk07vaW9Kp1PBhdpZYYcEw==}
|
||||
cpu: [loong64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@rollup/rollup-linux-ppc64-gnu@4.60.0':
|
||||
resolution: {integrity: sha512-RwpnLsqC8qbS8z1H1AxBA1H6qknR4YpPR9w2XX0vo2Sz10miu57PkNcnHVaZkbqyw/kUWfKMI73jhmfi9BRMUQ==}
|
||||
cpu: [ppc64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@rollup/rollup-linux-ppc64-gnu@4.60.1':
|
||||
resolution: {integrity: sha512-tLQQ9aPvkBxOc/EUT6j3pyeMD6Hb8QF2BTBnCQWP/uu1lhc9AIrIjKnLYMEroIz/JvtGYgI9dF3AxHZNaEH0rw==}
|
||||
cpu: [ppc64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@rollup/rollup-linux-ppc64-musl@4.60.0':
|
||||
resolution: {integrity: sha512-Z8pPf54Ly3aqtdWC3G4rFigZgNvd+qJlOE52fmko3KST9SoGfAdSRCwyoyG05q1HrrAblLbk1/PSIV+80/pxLg==}
|
||||
cpu: [ppc64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@rollup/rollup-linux-ppc64-musl@4.60.1':
|
||||
resolution: {integrity: sha512-RMxFhJwc9fSXP6PqmAz4cbv3kAyvD1etJFjTx4ONqFP9DkTkXsAMU4v3Vyc5BgzC+anz7nS/9tp4obsKfqkDHg==}
|
||||
cpu: [ppc64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@rollup/rollup-linux-riscv64-gnu@4.60.0':
|
||||
resolution: {integrity: sha512-3a3qQustp3COCGvnP4SvrMHnPQ9d1vzCakQVRTliaz8cIp/wULGjiGpbcqrkv0WrHTEp8bQD/B3HBjzujVWLOA==}
|
||||
cpu: [riscv64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@rollup/rollup-linux-riscv64-gnu@4.60.1':
|
||||
resolution: {integrity: sha512-QKgFl+Yc1eEk6MmOBfRHYF6lTxiiiV3/z/BRrbSiW2I7AFTXoBFvdMEyglohPj//2mZS4hDOqeB0H1ACh3sBbg==}
|
||||
cpu: [riscv64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@rollup/rollup-linux-riscv64-musl@4.60.0':
|
||||
resolution: {integrity: sha512-pjZDsVH/1VsghMJ2/kAaxt6dL0psT6ZexQVrijczOf+PeP2BUqTHYejk3l6TlPRydggINOeNRhvpLa0AYpCWSQ==}
|
||||
cpu: [riscv64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@rollup/rollup-linux-riscv64-musl@4.60.1':
|
||||
resolution: {integrity: sha512-RAjXjP/8c6ZtzatZcA1RaQr6O1TRhzC+adn8YZDnChliZHviqIjmvFwHcxi4JKPSDAt6Uhf/7vqcBzQJy0PDJg==}
|
||||
cpu: [riscv64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@rollup/rollup-linux-s390x-gnu@4.60.0':
|
||||
resolution: {integrity: sha512-3ObQs0BhvPgiUVZrN7gqCSvmFuMWvWvsjG5ayJ3Lraqv+2KhOsp+pUbigqbeWqueGIsnn+09HBw27rJ+gYK4VQ==}
|
||||
cpu: [s390x]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@rollup/rollup-linux-s390x-gnu@4.60.1':
|
||||
resolution: {integrity: sha512-wcuocpaOlaL1COBYiA89O6yfjlp3RwKDeTIA0hM7OpmhR1Bjo9j31G1uQVpDlTvwxGn2nQs65fBFL5UFd76FcQ==}
|
||||
cpu: [s390x]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@rollup/rollup-linux-x64-gnu@4.60.0':
|
||||
resolution: {integrity: sha512-EtylprDtQPdS5rXvAayrNDYoJhIz1/vzN2fEubo3yLE7tfAw+948dO0g4M0vkTVFhKojnF+n6C8bDNe+gDRdTg==}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@rollup/rollup-linux-x64-gnu@4.60.1':
|
||||
resolution: {integrity: sha512-77PpsFQUCOiZR9+LQEFg9GClyfkNXj1MP6wRnzYs0EeWbPcHs02AXu4xuUbM1zhwn3wqaizle3AEYg5aeoohhg==}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@rollup/rollup-linux-x64-musl@4.60.0':
|
||||
resolution: {integrity: sha512-k09oiRCi/bHU9UVFqD17r3eJR9bn03TyKraCrlz5ULFJGdJGi7VOmm9jl44vOJvRJ6P7WuBi/s2A97LxxHGIdw==}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@rollup/rollup-linux-x64-musl@4.60.1':
|
||||
resolution: {integrity: sha512-5cIATbk5vynAjqqmyBjlciMJl1+R/CwX9oLk/EyiFXDWd95KpHdrOJT//rnUl4cUcskrd0jCCw3wpZnhIHdD9w==}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@rollup/rollup-openbsd-x64@4.60.0':
|
||||
resolution: {integrity: sha512-1o/0/pIhozoSaDJoDcec+IVLbnRtQmHwPV730+AOD29lHEEo4F5BEUB24H0OBdhbBBDwIOSuf7vgg0Ywxdfiiw==}
|
||||
cpu: [x64]
|
||||
os: [openbsd]
|
||||
|
||||
'@rollup/rollup-openbsd-x64@4.60.1':
|
||||
resolution: {integrity: sha512-cl0w09WsCi17mcmWqqglez9Gk8isgeWvoUZ3WiJFYSR3zjBQc2J5/ihSjpl+VLjPqjQ/1hJRcqBfLjssREQILw==}
|
||||
cpu: [x64]
|
||||
os: [openbsd]
|
||||
|
||||
'@rollup/rollup-openharmony-arm64@4.60.0':
|
||||
resolution: {integrity: sha512-pESDkos/PDzYwtyzB5p/UoNU/8fJo68vcXM9ZW2V0kjYayj1KaaUfi1NmTUTUpMn4UhU4gTuK8gIaFO4UGuMbA==}
|
||||
cpu: [arm64]
|
||||
os: [openharmony]
|
||||
|
||||
'@rollup/rollup-openharmony-arm64@4.60.1':
|
||||
resolution: {integrity: sha512-4Cv23ZrONRbNtbZa37mLSueXUCtN7MXccChtKpUnQNgF010rjrjfHx3QxkS2PI7LqGT5xXyYs1a7LbzAwT0iCA==}
|
||||
cpu: [arm64]
|
||||
os: [openharmony]
|
||||
|
||||
'@rollup/rollup-win32-arm64-msvc@4.60.0':
|
||||
resolution: {integrity: sha512-hj1wFStD7B1YBeYmvY+lWXZ7ey73YGPcViMShYikqKT1GtstIKQAtfUI6yrzPjAy/O7pO0VLXGmUVWXQMaYgTQ==}
|
||||
cpu: [arm64]
|
||||
os: [win32]
|
||||
|
||||
'@rollup/rollup-win32-arm64-msvc@4.60.1':
|
||||
resolution: {integrity: sha512-i1okWYkA4FJICtr7KpYzFpRTHgy5jdDbZiWfvny21iIKky5YExiDXP+zbXzm3dUcFpkEeYNHgQ5fuG236JPq0g==}
|
||||
cpu: [arm64]
|
||||
os: [win32]
|
||||
|
||||
'@rollup/rollup-win32-ia32-msvc@4.60.0':
|
||||
resolution: {integrity: sha512-SyaIPFoxmUPlNDq5EHkTbiKzmSEmq/gOYFI/3HHJ8iS/v1mbugVa7dXUzcJGQfoytp9DJFLhHH4U3/eTy2Bq4w==}
|
||||
cpu: [ia32]
|
||||
os: [win32]
|
||||
|
||||
'@rollup/rollup-win32-ia32-msvc@4.60.1':
|
||||
resolution: {integrity: sha512-u09m3CuwLzShA0EYKMNiFgcjjzwqtUMLmuCJLeZWjjOYA3IT2Di09KaxGBTP9xVztWyIWjVdsB2E9goMjZvTQg==}
|
||||
cpu: [ia32]
|
||||
os: [win32]
|
||||
|
||||
'@rollup/rollup-win32-x64-gnu@4.60.0':
|
||||
resolution: {integrity: sha512-RdcryEfzZr+lAr5kRm2ucN9aVlCCa2QNq4hXelZxb8GG0NJSazq44Z3PCCc8wISRuCVnGs0lQJVX5Vp6fKA+IA==}
|
||||
cpu: [x64]
|
||||
os: [win32]
|
||||
|
||||
'@rollup/rollup-win32-x64-gnu@4.60.1':
|
||||
resolution: {integrity: sha512-k+600V9Zl1CM7eZxJgMyTUzmrmhB/0XZnF4pRypKAlAgxmedUA+1v9R+XOFv56W4SlHEzfeMtzujLJD22Uz5zg==}
|
||||
cpu: [x64]
|
||||
os: [win32]
|
||||
|
||||
'@rollup/rollup-win32-x64-msvc@4.60.0':
|
||||
resolution: {integrity: sha512-PrsWNQ8BuE00O3Xsx3ALh2Df8fAj9+cvvX9AIA6o4KpATR98c9mud4XtDWVvsEuyia5U4tVSTKygawyJkjm60w==}
|
||||
cpu: [x64]
|
||||
os: [win32]
|
||||
|
||||
'@rollup/rollup-win32-x64-msvc@4.60.1':
|
||||
resolution: {integrity: sha512-lWMnixq/QzxyhTV6NjQJ4SFo1J6PvOX8vUx5Wb4bBPsEb+8xZ89Bz6kOXpfXj9ak9AHTQVQzlgzBEc1SyM27xQ==}
|
||||
cpu: [x64]
|
||||
os: [win32]
|
||||
|
||||
'@rtsao/scc@1.1.0':
|
||||
resolution: {integrity: sha512-zt6OdqaDoOnJ1ZYsCYGt9YmWzDXl4vQdKTyJev62gFhRGKdx7mcT54V9KIjg+d2wi9EXsPvAPKe7i7WjfVWB8g==}
|
||||
|
||||
@@ -5254,33 +5391,33 @@ packages:
|
||||
'@tsconfig/node16@1.0.4':
|
||||
resolution: {integrity: sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==}
|
||||
|
||||
'@turbo/darwin-64@2.8.20':
|
||||
resolution: {integrity: sha512-FQ9EX1xMU5nbwjxXxM3yU88AQQ6Sqc6S44exPRroMcx9XZHqqppl5ymJF0Ig/z3nvQNwDmz1Gsnvxubo+nXWjQ==}
|
||||
'@turbo/darwin-64@2.9.5':
|
||||
resolution: {integrity: sha512-qPxhKsLMQP+9+dsmPgAGidi5uNifD4AoAOnEnljab3Qgn0QZRR31Hp+/CgW3Ia5AanWj6JuLLTBYvuQj4mqTWg==}
|
||||
cpu: [x64]
|
||||
os: [darwin]
|
||||
|
||||
'@turbo/darwin-arm64@2.8.20':
|
||||
resolution: {integrity: sha512-Gpyh9ATFGThD6/s9L95YWY54cizg/VRWl2B67h0yofG8BpHf67DFAh9nuJVKG7bY0+SBJDAo5cMur+wOl9YOYw==}
|
||||
'@turbo/darwin-arm64@2.9.5':
|
||||
resolution: {integrity: sha512-vkF/9F/l3aWd4bHxTui5Hh0F5xrTZ4e3rbBsc57zA6O8gNbmHN3B6eZ5psAIP2CnJRZ8ZxRjV3WZHeNXMXkPBw==}
|
||||
cpu: [arm64]
|
||||
os: [darwin]
|
||||
|
||||
'@turbo/linux-64@2.8.20':
|
||||
resolution: {integrity: sha512-p2QxWUYyYUgUFG0b0kR+pPi8t7c9uaVlRtjTTI1AbCvVqkpjUfCcReBn6DgG/Hu8xrWdKLuyQFaLYFzQskZbcA==}
|
||||
'@turbo/linux-64@2.9.5':
|
||||
resolution: {integrity: sha512-z/Get5NUaUxm5HSGFqVMICDRjFNsCUhSc4wnFa/PP1QD0NXCjr7bu9a2EM6md/KMCBW0Qe393Ac+UM7/ryDDTw==}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
|
||||
'@turbo/linux-arm64@2.8.20':
|
||||
resolution: {integrity: sha512-Gn5yjlZGLRZWarLWqdQzv0wMqyBNIdq1QLi48F1oY5Lo9kiohuf7BPQWtWxeNVS2NgJ1+nb/DzK1JduYC4AWOA==}
|
||||
'@turbo/linux-arm64@2.9.5':
|
||||
resolution: {integrity: sha512-jyBifaNoI5/NheyswomiZXJvjdAdvT7hDRYzQ4meP0DKGvpXUjnqsD+4/J2YSDQ34OHxFkL30FnSCUIVOh2PHw==}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
|
||||
'@turbo/windows-64@2.8.20':
|
||||
resolution: {integrity: sha512-vyaDpYk/8T6Qz5V/X+ihKvKFEZFUoC0oxYpC1sZanK6gaESJlmV3cMRT3Qhcg4D2VxvtC2Jjs9IRkrZGL+exLw==}
|
||||
'@turbo/windows-64@2.9.5':
|
||||
resolution: {integrity: sha512-ph24K5uPtvo7UfuyDXnBiB/8XvrO+RQWbbw5zkA/bVNoy9HDiNoIJJj3s62MxT9tjEb6DnPje5PXSz1UR7QAyg==}
|
||||
cpu: [x64]
|
||||
os: [win32]
|
||||
|
||||
'@turbo/windows-arm64@2.8.20':
|
||||
resolution: {integrity: sha512-voicVULvUV5yaGXo0Iue13BcHGYW3u0VgqSbfQwBaHbpj1zLjYV4KIe+7fYIo6DO8FVUJzxFps3ODCQG/Wy2Qw==}
|
||||
'@turbo/windows-arm64@2.9.5':
|
||||
resolution: {integrity: sha512-6c5RccT/+iR39SdT1G5HyZaD2n57W77o+l0TTfxG/cVlhV94Acyg2gTQW7zUOhW1BeQpBjHzu9x8yVBZwrHh7g==}
|
||||
cpu: [arm64]
|
||||
os: [win32]
|
||||
|
||||
@@ -7014,8 +7151,8 @@ packages:
|
||||
peerDependencies:
|
||||
eslint: '>=7.0.0'
|
||||
|
||||
eslint-config-turbo@2.8.20:
|
||||
resolution: {integrity: sha512-zT17KSqM4gTrChIocN+WfSdjg21QZTCDrcssylFab/wxu6S1EHDbt0JOna9b8rH5OKafZxDuFxKMV8BkulWShg==}
|
||||
eslint-config-turbo@2.9.5:
|
||||
resolution: {integrity: sha512-MwAmBEge6WOONTll4BAI1ZbbtD4PrhEaumcu6MNIdrwygMXw2Ne2eV8wxVpj6cnPgyUNE6iS6WdOoi/vPI4YFg==}
|
||||
peerDependencies:
|
||||
eslint: '>6.6.0'
|
||||
turbo: '>2.0.0'
|
||||
@@ -7102,8 +7239,8 @@ packages:
|
||||
peerDependencies:
|
||||
eslint: ^3 || ^4 || ^5 || ^6 || ^7 || ^8 || ^9.7
|
||||
|
||||
eslint-plugin-turbo@2.8.20:
|
||||
resolution: {integrity: sha512-sMDremg73XbX+Imh7iDRS3TAro5jIf7CdzSqrT50wCGhClbiUvmWcLomnX+ldUbfTy+OvtqOeePvTivJMR87eQ==}
|
||||
eslint-plugin-turbo@2.9.5:
|
||||
resolution: {integrity: sha512-mwsR2dVy35crMS39TKSKG1xMxfn9M9t/sHXg70GAVqJxoFhsmM1XOcVGQ5w2z7GFUuMP70xUcpSendFzyuuSlQ==}
|
||||
peerDependencies:
|
||||
eslint: '>6.6.0'
|
||||
turbo: '>2.0.0'
|
||||
@@ -7239,8 +7376,8 @@ packages:
|
||||
exponential-backoff@3.1.2:
|
||||
resolution: {integrity: sha512-8QxYTVXUkuy7fIIoitQkPwGonB8F3Zj8eEO8Sqg9Zv/bkI7RJAzowee4gr81Hak/dUTpA2Z7VfQgoijjPNlUZA==}
|
||||
|
||||
express-rate-limit@8.3.1:
|
||||
resolution: {integrity: sha512-D1dKN+cmyPWuvB+G2SREQDzPY1agpBIcTa9sJxOPMCNeH3gwzhqJRDWCXW3gg0y//+LQ/8j52JbMROWyrKdMdw==}
|
||||
express-rate-limit@8.3.2:
|
||||
resolution: {integrity: sha512-77VmFeJkO0/rvimEDuUC5H30oqUC4EyOhyGccfqoLebB0oiEYfM7nwPrsDsBL1gsTpwfzX8SFy2MT3TDyRq+bg==}
|
||||
engines: {node: '>= 16'}
|
||||
peerDependencies:
|
||||
express: '>= 4.11'
|
||||
@@ -7249,9 +7386,6 @@ packages:
|
||||
resolution: {integrity: sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw==}
|
||||
engines: {node: '>= 18'}
|
||||
|
||||
exsolve@1.0.7:
|
||||
resolution: {integrity: sha512-VO5fQUzZtI6C+vx4w/4BWJpg3s/5l+6pRQEHzFRM8WFi4XffSP1Z+4qi7GbjWbvRQEbdIco5mIMq+zX4rPuLrw==}
|
||||
|
||||
exsolve@1.0.8:
|
||||
resolution: {integrity: sha512-LmDxfWXwcTArk8fUEnOfSZpHOJ6zOMUJKOtFLFqJLoKJetuQG874Uc7/Kki7zFLzYybmZhp1M7+98pfMqeX8yA==}
|
||||
|
||||
@@ -9665,8 +9799,8 @@ packages:
|
||||
peerDependencies:
|
||||
react: '>=16.0.0'
|
||||
|
||||
prisma@6.17.1:
|
||||
resolution: {integrity: sha512-ac6h0sM1Tg3zu8NInY+qhP/S9KhENVaw9n1BrGKQVFu05JT5yT5Qqqmb8tMRIE3ZXvVj4xcRA5yfrsy4X7Yy5g==}
|
||||
prisma@6.19.3:
|
||||
resolution: {integrity: sha512-++ZJ0ijLrDJF6hNB4t4uxg2br3fC4H9Yc9tcbjr2fcNFP3rh/SBNrAgjhsqBU4Ght8JPrVofG/ZkXfnSfnYsFg==}
|
||||
engines: {node: '>=18.18'}
|
||||
hasBin: true
|
||||
peerDependencies:
|
||||
@@ -10057,6 +10191,11 @@ packages:
|
||||
engines: {node: '>=18.0.0', npm: '>=8.0.0'}
|
||||
hasBin: true
|
||||
|
||||
rollup@4.60.1:
|
||||
resolution: {integrity: sha512-VmtB2rFU/GroZ4oL8+ZqXgSA38O6GR8KSIvWmEFv63pQ0G6KaBH9s07PO8XTXP4vI+3UJUEypOfjkGfmSBBR0w==}
|
||||
engines: {node: '>=18.0.0', npm: '>=8.0.0'}
|
||||
hasBin: true
|
||||
|
||||
router@2.2.0:
|
||||
resolution: {integrity: sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==}
|
||||
engines: {node: '>= 18'}
|
||||
@@ -10666,8 +10805,8 @@ packages:
|
||||
ttl-set@1.0.0:
|
||||
resolution: {integrity: sha512-2fuHn/UR+8Z9HK49r97+p2Ru1b5Eewg2QqPrU14BVCQ9QoyU3+vLLZk2WEiyZ9sgJh6W8G1cZr9I2NBLywAHrA==}
|
||||
|
||||
turbo@2.8.20:
|
||||
resolution: {integrity: sha512-Rb4qk5YT8RUwwdXtkLpkVhNEe/lor6+WV7S5tTlLpxSz6MjV5Qi8jGNn4gS6NAvrYGA/rNrE6YUQM85sCZUDbQ==}
|
||||
turbo@2.9.5:
|
||||
resolution: {integrity: sha512-JXNkRe6H6MjSlk5UQRTjyoKX5YN2zlc2632xcSlSFBao5yvbMWTpv9SNolOZlZmUlcDOHuszPLItbKrvcXnnZA==}
|
||||
hasBin: true
|
||||
|
||||
type-check@0.4.0:
|
||||
@@ -13695,7 +13834,7 @@ snapshots:
|
||||
|
||||
'@marijn/find-cluster-break@1.0.2': {}
|
||||
|
||||
'@modelcontextprotocol/sdk@1.27.1(@cfworker/json-schema@4.1.1)(zod@4.3.6)':
|
||||
'@modelcontextprotocol/sdk@1.29.0(@cfworker/json-schema@4.1.1)(zod@4.3.6)':
|
||||
dependencies:
|
||||
'@hono/node-server': 1.19.12(hono@4.12.9)
|
||||
ajv: 8.18.0
|
||||
@@ -13706,7 +13845,7 @@ snapshots:
|
||||
eventsource: 3.0.7
|
||||
eventsource-parser: 3.0.6
|
||||
express: 5.2.1
|
||||
express-rate-limit: 8.3.1(express@5.2.1)
|
||||
express-rate-limit: 8.3.2(express@5.2.1)
|
||||
hono: 4.12.9
|
||||
jose: 6.2.2
|
||||
json-schema-typed: 8.0.2
|
||||
@@ -13832,9 +13971,9 @@ snapshots:
|
||||
'@tybys/wasm-util': 0.10.1
|
||||
optional: true
|
||||
|
||||
'@next-auth/prisma-adapter@1.0.7(@prisma/client@6.17.1(prisma@6.17.1(magicast@0.5.2)(typescript@5.9.2))(typescript@5.9.2))(next-auth@4.24.13(patch_hash=7b301d2607ee3cd8bba4f6978ba8cdd7a616b59d6e414191da3b38950ce08e66)(next@16.2.1(@babel/core@7.29.0)(@opentelemetry/api@1.9.0)(@playwright/test@1.57.0)(babel-plugin-macros@3.1.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(nodemailer@7.0.11)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))':
|
||||
'@next-auth/prisma-adapter@1.0.7(@prisma/client@6.19.3(prisma@6.19.3(magicast@0.5.2)(typescript@5.9.2))(typescript@5.9.2))(next-auth@4.24.13(patch_hash=7b301d2607ee3cd8bba4f6978ba8cdd7a616b59d6e414191da3b38950ce08e66)(next@16.2.1(@babel/core@7.29.0)(@opentelemetry/api@1.9.0)(@playwright/test@1.57.0)(babel-plugin-macros@3.1.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(nodemailer@7.0.11)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))':
|
||||
dependencies:
|
||||
'@prisma/client': 6.17.1(prisma@6.17.1(magicast@0.5.2)(typescript@5.9.2))(typescript@5.9.2)
|
||||
'@prisma/client': 6.19.3(prisma@6.19.3(magicast@0.5.2)(typescript@5.9.2))(typescript@5.9.2)
|
||||
next-auth: 4.24.13(patch_hash=7b301d2607ee3cd8bba4f6978ba8cdd7a616b59d6e414191da3b38950ce08e66)(next@16.2.1(@babel/core@7.29.0)(@opentelemetry/api@1.9.0)(@playwright/test@1.57.0)(babel-plugin-macros@3.1.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(nodemailer@7.0.11)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
|
||||
|
||||
'@next/env@16.2.1': {}
|
||||
@@ -14525,12 +14664,12 @@ snapshots:
|
||||
|
||||
'@posthog/core@1.2.2': {}
|
||||
|
||||
'@prisma/client@6.17.1(prisma@6.17.1(magicast@0.5.2)(typescript@5.9.2))(typescript@5.9.2)':
|
||||
'@prisma/client@6.19.3(prisma@6.19.3(magicast@0.5.2)(typescript@5.9.2))(typescript@5.9.2)':
|
||||
optionalDependencies:
|
||||
prisma: 6.17.1(magicast@0.5.2)(typescript@5.9.2)
|
||||
prisma: 6.19.3(magicast@0.5.2)(typescript@5.9.2)
|
||||
typescript: 5.9.2
|
||||
|
||||
'@prisma/config@6.17.1(magicast@0.5.2)':
|
||||
'@prisma/config@6.19.3(magicast@0.5.2)':
|
||||
dependencies:
|
||||
c12: 3.1.0(magicast@0.5.2)
|
||||
deepmerge-ts: 7.1.5
|
||||
@@ -14539,28 +14678,28 @@ snapshots:
|
||||
transitivePeerDependencies:
|
||||
- magicast
|
||||
|
||||
'@prisma/debug@6.17.1': {}
|
||||
'@prisma/debug@6.19.3': {}
|
||||
|
||||
'@prisma/engines-version@6.17.1-1.272a37d34178c2894197e17273bf937f25acdeac': {}
|
||||
'@prisma/engines-version@7.1.1-3.c2990dca591cba766e3b7ef5d9e8a84796e47ab7': {}
|
||||
|
||||
'@prisma/engines@6.17.1':
|
||||
'@prisma/engines@6.19.3':
|
||||
dependencies:
|
||||
'@prisma/debug': 6.17.1
|
||||
'@prisma/engines-version': 6.17.1-1.272a37d34178c2894197e17273bf937f25acdeac
|
||||
'@prisma/fetch-engine': 6.17.1
|
||||
'@prisma/get-platform': 6.17.1
|
||||
'@prisma/debug': 6.19.3
|
||||
'@prisma/engines-version': 7.1.1-3.c2990dca591cba766e3b7ef5d9e8a84796e47ab7
|
||||
'@prisma/fetch-engine': 6.19.3
|
||||
'@prisma/get-platform': 6.19.3
|
||||
|
||||
'@prisma/fetch-engine@6.17.1':
|
||||
'@prisma/fetch-engine@6.19.3':
|
||||
dependencies:
|
||||
'@prisma/debug': 6.17.1
|
||||
'@prisma/engines-version': 6.17.1-1.272a37d34178c2894197e17273bf937f25acdeac
|
||||
'@prisma/get-platform': 6.17.1
|
||||
'@prisma/debug': 6.19.3
|
||||
'@prisma/engines-version': 7.1.1-3.c2990dca591cba766e3b7ef5d9e8a84796e47ab7
|
||||
'@prisma/get-platform': 6.19.3
|
||||
|
||||
'@prisma/get-platform@6.17.1':
|
||||
'@prisma/get-platform@6.19.3':
|
||||
dependencies:
|
||||
'@prisma/debug': 6.17.1
|
||||
'@prisma/debug': 6.19.3
|
||||
|
||||
'@prisma/instrumentation@6.17.1(@opentelemetry/api@1.9.0)':
|
||||
'@prisma/instrumentation@6.19.3(@opentelemetry/api@1.9.0)':
|
||||
dependencies:
|
||||
'@opentelemetry/api': 1.9.0
|
||||
'@opentelemetry/instrumentation': 0.53.0(@opentelemetry/api@1.9.0)
|
||||
@@ -15456,78 +15595,153 @@ snapshots:
|
||||
'@rollup/rollup-android-arm-eabi@4.60.0':
|
||||
optional: true
|
||||
|
||||
'@rollup/rollup-android-arm-eabi@4.60.1':
|
||||
optional: true
|
||||
|
||||
'@rollup/rollup-android-arm64@4.60.0':
|
||||
optional: true
|
||||
|
||||
'@rollup/rollup-android-arm64@4.60.1':
|
||||
optional: true
|
||||
|
||||
'@rollup/rollup-darwin-arm64@4.60.0':
|
||||
optional: true
|
||||
|
||||
'@rollup/rollup-darwin-arm64@4.60.1':
|
||||
optional: true
|
||||
|
||||
'@rollup/rollup-darwin-x64@4.60.0':
|
||||
optional: true
|
||||
|
||||
'@rollup/rollup-darwin-x64@4.60.1':
|
||||
optional: true
|
||||
|
||||
'@rollup/rollup-freebsd-arm64@4.60.0':
|
||||
optional: true
|
||||
|
||||
'@rollup/rollup-freebsd-arm64@4.60.1':
|
||||
optional: true
|
||||
|
||||
'@rollup/rollup-freebsd-x64@4.60.0':
|
||||
optional: true
|
||||
|
||||
'@rollup/rollup-freebsd-x64@4.60.1':
|
||||
optional: true
|
||||
|
||||
'@rollup/rollup-linux-arm-gnueabihf@4.60.0':
|
||||
optional: true
|
||||
|
||||
'@rollup/rollup-linux-arm-gnueabihf@4.60.1':
|
||||
optional: true
|
||||
|
||||
'@rollup/rollup-linux-arm-musleabihf@4.60.0':
|
||||
optional: true
|
||||
|
||||
'@rollup/rollup-linux-arm-musleabihf@4.60.1':
|
||||
optional: true
|
||||
|
||||
'@rollup/rollup-linux-arm64-gnu@4.60.0':
|
||||
optional: true
|
||||
|
||||
'@rollup/rollup-linux-arm64-gnu@4.60.1':
|
||||
optional: true
|
||||
|
||||
'@rollup/rollup-linux-arm64-musl@4.60.0':
|
||||
optional: true
|
||||
|
||||
'@rollup/rollup-linux-arm64-musl@4.60.1':
|
||||
optional: true
|
||||
|
||||
'@rollup/rollup-linux-loong64-gnu@4.60.0':
|
||||
optional: true
|
||||
|
||||
'@rollup/rollup-linux-loong64-gnu@4.60.1':
|
||||
optional: true
|
||||
|
||||
'@rollup/rollup-linux-loong64-musl@4.60.0':
|
||||
optional: true
|
||||
|
||||
'@rollup/rollup-linux-loong64-musl@4.60.1':
|
||||
optional: true
|
||||
|
||||
'@rollup/rollup-linux-ppc64-gnu@4.60.0':
|
||||
optional: true
|
||||
|
||||
'@rollup/rollup-linux-ppc64-gnu@4.60.1':
|
||||
optional: true
|
||||
|
||||
'@rollup/rollup-linux-ppc64-musl@4.60.0':
|
||||
optional: true
|
||||
|
||||
'@rollup/rollup-linux-ppc64-musl@4.60.1':
|
||||
optional: true
|
||||
|
||||
'@rollup/rollup-linux-riscv64-gnu@4.60.0':
|
||||
optional: true
|
||||
|
||||
'@rollup/rollup-linux-riscv64-gnu@4.60.1':
|
||||
optional: true
|
||||
|
||||
'@rollup/rollup-linux-riscv64-musl@4.60.0':
|
||||
optional: true
|
||||
|
||||
'@rollup/rollup-linux-riscv64-musl@4.60.1':
|
||||
optional: true
|
||||
|
||||
'@rollup/rollup-linux-s390x-gnu@4.60.0':
|
||||
optional: true
|
||||
|
||||
'@rollup/rollup-linux-s390x-gnu@4.60.1':
|
||||
optional: true
|
||||
|
||||
'@rollup/rollup-linux-x64-gnu@4.60.0':
|
||||
optional: true
|
||||
|
||||
'@rollup/rollup-linux-x64-gnu@4.60.1':
|
||||
optional: true
|
||||
|
||||
'@rollup/rollup-linux-x64-musl@4.60.0':
|
||||
optional: true
|
||||
|
||||
'@rollup/rollup-linux-x64-musl@4.60.1':
|
||||
optional: true
|
||||
|
||||
'@rollup/rollup-openbsd-x64@4.60.0':
|
||||
optional: true
|
||||
|
||||
'@rollup/rollup-openbsd-x64@4.60.1':
|
||||
optional: true
|
||||
|
||||
'@rollup/rollup-openharmony-arm64@4.60.0':
|
||||
optional: true
|
||||
|
||||
'@rollup/rollup-openharmony-arm64@4.60.1':
|
||||
optional: true
|
||||
|
||||
'@rollup/rollup-win32-arm64-msvc@4.60.0':
|
||||
optional: true
|
||||
|
||||
'@rollup/rollup-win32-arm64-msvc@4.60.1':
|
||||
optional: true
|
||||
|
||||
'@rollup/rollup-win32-ia32-msvc@4.60.0':
|
||||
optional: true
|
||||
|
||||
'@rollup/rollup-win32-ia32-msvc@4.60.1':
|
||||
optional: true
|
||||
|
||||
'@rollup/rollup-win32-x64-gnu@4.60.0':
|
||||
optional: true
|
||||
|
||||
'@rollup/rollup-win32-x64-gnu@4.60.1':
|
||||
optional: true
|
||||
|
||||
'@rollup/rollup-win32-x64-msvc@4.60.0':
|
||||
optional: true
|
||||
|
||||
'@rollup/rollup-win32-x64-msvc@4.60.1':
|
||||
optional: true
|
||||
|
||||
'@rtsao/scc@1.1.0': {}
|
||||
|
||||
'@selderee/plugin-htmlparser2@0.11.0':
|
||||
@@ -16358,22 +16572,22 @@ snapshots:
|
||||
|
||||
'@tsconfig/node16@1.0.4': {}
|
||||
|
||||
'@turbo/darwin-64@2.8.20':
|
||||
'@turbo/darwin-64@2.9.5':
|
||||
optional: true
|
||||
|
||||
'@turbo/darwin-arm64@2.8.20':
|
||||
'@turbo/darwin-arm64@2.9.5':
|
||||
optional: true
|
||||
|
||||
'@turbo/linux-64@2.8.20':
|
||||
'@turbo/linux-64@2.9.5':
|
||||
optional: true
|
||||
|
||||
'@turbo/linux-arm64@2.8.20':
|
||||
'@turbo/linux-arm64@2.9.5':
|
||||
optional: true
|
||||
|
||||
'@turbo/windows-64@2.8.20':
|
||||
'@turbo/windows-64@2.9.5':
|
||||
optional: true
|
||||
|
||||
'@turbo/windows-arm64@2.8.20':
|
||||
'@turbo/windows-arm64@2.9.5':
|
||||
optional: true
|
||||
|
||||
'@tybys/wasm-util@0.10.1':
|
||||
@@ -17456,7 +17670,7 @@ snapshots:
|
||||
confbox: 0.2.4
|
||||
defu: 6.1.4
|
||||
dotenv: 16.6.1
|
||||
exsolve: 1.0.7
|
||||
exsolve: 1.0.8
|
||||
giget: 2.0.0
|
||||
jiti: 2.6.1
|
||||
ohash: 2.0.11
|
||||
@@ -18349,11 +18563,11 @@ snapshots:
|
||||
dependencies:
|
||||
eslint: 9.39.4(jiti@2.6.1)
|
||||
|
||||
eslint-config-turbo@2.8.20(eslint@9.39.4(jiti@2.6.1))(turbo@2.8.20):
|
||||
eslint-config-turbo@2.9.5(eslint@9.39.4(jiti@2.6.1))(turbo@2.9.5):
|
||||
dependencies:
|
||||
eslint: 9.39.4(jiti@2.6.1)
|
||||
eslint-plugin-turbo: 2.8.20(eslint@9.39.4(jiti@2.6.1))(turbo@2.8.20)
|
||||
turbo: 2.8.20
|
||||
eslint-plugin-turbo: 2.9.5(eslint@9.39.4(jiti@2.6.1))(turbo@2.9.5)
|
||||
turbo: 2.9.5
|
||||
|
||||
eslint-import-resolver-node@0.3.9:
|
||||
dependencies:
|
||||
@@ -18509,11 +18723,11 @@ snapshots:
|
||||
string.prototype.matchall: 4.0.12
|
||||
string.prototype.repeat: 1.0.0
|
||||
|
||||
eslint-plugin-turbo@2.8.20(eslint@9.39.4(jiti@2.6.1))(turbo@2.8.20):
|
||||
eslint-plugin-turbo@2.9.5(eslint@9.39.4(jiti@2.6.1))(turbo@2.9.5):
|
||||
dependencies:
|
||||
dotenv: 16.0.3
|
||||
eslint: 9.39.4(jiti@2.6.1)
|
||||
turbo: 2.8.20
|
||||
turbo: 2.9.5
|
||||
|
||||
eslint-scope@5.1.1:
|
||||
dependencies:
|
||||
@@ -18679,7 +18893,7 @@ snapshots:
|
||||
|
||||
exponential-backoff@3.1.2: {}
|
||||
|
||||
express-rate-limit@8.3.1(express@5.2.1):
|
||||
express-rate-limit@8.3.2(express@5.2.1):
|
||||
dependencies:
|
||||
express: 5.2.1
|
||||
ip-address: 10.1.0
|
||||
@@ -18717,8 +18931,6 @@ snapshots:
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
exsolve@1.0.7: {}
|
||||
|
||||
exsolve@1.0.8: {}
|
||||
|
||||
extend@3.0.2: {}
|
||||
@@ -21617,10 +21829,10 @@ snapshots:
|
||||
clsx: 2.1.1
|
||||
react: 19.2.4
|
||||
|
||||
prisma@6.17.1(magicast@0.5.2)(typescript@5.9.2):
|
||||
prisma@6.19.3(magicast@0.5.2)(typescript@5.9.2):
|
||||
dependencies:
|
||||
'@prisma/config': 6.17.1(magicast@0.5.2)
|
||||
'@prisma/engines': 6.17.1
|
||||
'@prisma/config': 6.19.3(magicast@0.5.2)
|
||||
'@prisma/engines': 6.19.3
|
||||
optionalDependencies:
|
||||
typescript: 5.9.2
|
||||
transitivePeerDependencies:
|
||||
@@ -22134,6 +22346,37 @@ snapshots:
|
||||
'@rollup/rollup-win32-x64-msvc': 4.60.0
|
||||
fsevents: 2.3.3
|
||||
|
||||
rollup@4.60.1:
|
||||
dependencies:
|
||||
'@types/estree': 1.0.8
|
||||
optionalDependencies:
|
||||
'@rollup/rollup-android-arm-eabi': 4.60.1
|
||||
'@rollup/rollup-android-arm64': 4.60.1
|
||||
'@rollup/rollup-darwin-arm64': 4.60.1
|
||||
'@rollup/rollup-darwin-x64': 4.60.1
|
||||
'@rollup/rollup-freebsd-arm64': 4.60.1
|
||||
'@rollup/rollup-freebsd-x64': 4.60.1
|
||||
'@rollup/rollup-linux-arm-gnueabihf': 4.60.1
|
||||
'@rollup/rollup-linux-arm-musleabihf': 4.60.1
|
||||
'@rollup/rollup-linux-arm64-gnu': 4.60.1
|
||||
'@rollup/rollup-linux-arm64-musl': 4.60.1
|
||||
'@rollup/rollup-linux-loong64-gnu': 4.60.1
|
||||
'@rollup/rollup-linux-loong64-musl': 4.60.1
|
||||
'@rollup/rollup-linux-ppc64-gnu': 4.60.1
|
||||
'@rollup/rollup-linux-ppc64-musl': 4.60.1
|
||||
'@rollup/rollup-linux-riscv64-gnu': 4.60.1
|
||||
'@rollup/rollup-linux-riscv64-musl': 4.60.1
|
||||
'@rollup/rollup-linux-s390x-gnu': 4.60.1
|
||||
'@rollup/rollup-linux-x64-gnu': 4.60.1
|
||||
'@rollup/rollup-linux-x64-musl': 4.60.1
|
||||
'@rollup/rollup-openbsd-x64': 4.60.1
|
||||
'@rollup/rollup-openharmony-arm64': 4.60.1
|
||||
'@rollup/rollup-win32-arm64-msvc': 4.60.1
|
||||
'@rollup/rollup-win32-ia32-msvc': 4.60.1
|
||||
'@rollup/rollup-win32-x64-gnu': 4.60.1
|
||||
'@rollup/rollup-win32-x64-msvc': 4.60.1
|
||||
fsevents: 2.3.3
|
||||
|
||||
router@2.2.0:
|
||||
dependencies:
|
||||
debug: 4.4.3
|
||||
@@ -22786,14 +23029,14 @@ snapshots:
|
||||
dependencies:
|
||||
fast-fifo: 1.3.2
|
||||
|
||||
turbo@2.8.20:
|
||||
turbo@2.9.5:
|
||||
optionalDependencies:
|
||||
'@turbo/darwin-64': 2.8.20
|
||||
'@turbo/darwin-arm64': 2.8.20
|
||||
'@turbo/linux-64': 2.8.20
|
||||
'@turbo/linux-arm64': 2.8.20
|
||||
'@turbo/windows-64': 2.8.20
|
||||
'@turbo/windows-arm64': 2.8.20
|
||||
'@turbo/darwin-64': 2.9.5
|
||||
'@turbo/darwin-arm64': 2.9.5
|
||||
'@turbo/linux-64': 2.9.5
|
||||
'@turbo/linux-arm64': 2.9.5
|
||||
'@turbo/windows-64': 2.9.5
|
||||
'@turbo/windows-arm64': 2.9.5
|
||||
|
||||
type-check@0.4.0:
|
||||
dependencies:
|
||||
@@ -23100,7 +23343,7 @@ snapshots:
|
||||
fdir: 6.5.0(picomatch@4.0.4)
|
||||
picomatch: 4.0.4
|
||||
postcss: 8.5.8
|
||||
rollup: 4.60.0
|
||||
rollup: 4.60.1
|
||||
tinyglobby: 0.2.15
|
||||
optionalDependencies:
|
||||
'@types/node': 24.3.0
|
||||
|
||||
@@ -61,6 +61,24 @@ minimumReleaseAgeExclude:
|
||||
- "langfuse@3.38.20"
|
||||
- "langfuse-langchain@3.38.20"
|
||||
- "langfuse-core@3.38.20"
|
||||
- "@modelcontextprotocol/sdk@1.29.0"
|
||||
- "@prisma/instrumentation@6.19.3"
|
||||
- "prisma@6.19.3"
|
||||
- "@prisma/client@6.19.3"
|
||||
- "@prisma/config@6.19.3"
|
||||
- "@prisma/engines@6.19.3"
|
||||
- "@prisma/debug@6.19.3"
|
||||
- "@prisma/fetch-engine@6.19.3"
|
||||
- "@prisma/get-platform@6.19.3"
|
||||
- "eslint-config-turbo@2.9.5"
|
||||
- "eslint-plugin-turbo@2.9.5"
|
||||
- "turbo@2.9.5"
|
||||
- "@turbo/darwin-64@2.9.5"
|
||||
- "@turbo/darwin-arm64@2.9.5"
|
||||
- "@turbo/windows-64@2.9.5"
|
||||
- "@turbo/windows-arm64@2.9.5"
|
||||
- "@turbo/linux-64@2.9.5"
|
||||
- "@turbo/linux-arm64@2.9.5"
|
||||
allowBuilds:
|
||||
"@prisma/client": true
|
||||
"@prisma/engines": true
|
||||
|
||||
+4
-4
@@ -7,7 +7,7 @@ RUN apk update && apk upgrade --no-cache libcrypto3 libssl3 libc6-compat busybox
|
||||
|
||||
FROM --platform=${TARGETPLATFORM:-linux/amd64} alpine AS build-base
|
||||
# Pin turbo to avoid nondeterministic prune output from future patch releases.
|
||||
RUN npm install turbo@2.8.20 --global
|
||||
RUN npm install turbo@2.9.5 --global
|
||||
ENV PNPM_HOME="/pnpm"
|
||||
ENV PATH="$PNPM_HOME:$PATH"
|
||||
RUN corepack enable
|
||||
@@ -17,7 +17,7 @@ FROM --platform=${TARGETPLATFORM:-linux/amd64} alpine AS runtime-base
|
||||
# Remove build-only package managers. npm stays here because the runner stage
|
||||
# uses it to install prisma and optional dd-trace before removing it.
|
||||
RUN rm -rf /usr/local/lib/node_modules/corepack && \
|
||||
rm -f /usr/local/bin/corepack /usr/local/bin/pnpm /usr/local/bin/pnpx /usr/local/bin/yarn /usr/local/bin/yarnpkg
|
||||
rm -f /usr/local/bin/corepack /usr/local/bin/yarn /usr/local/bin/yarnpkg
|
||||
|
||||
FROM --platform=${TARGETPLATFORM:-linux/amd64} build-base AS pruner
|
||||
|
||||
@@ -122,7 +122,7 @@ ARG GID=1001
|
||||
RUN addgroup --system --gid ${GID} nodejs
|
||||
RUN adduser --system --uid ${UID} nextjs
|
||||
|
||||
RUN npm install -g --no-package-lock --no-save prisma@6.17.1
|
||||
RUN npm install -g --no-package-lock --no-save prisma@6.19.3
|
||||
|
||||
# Install dd-trace only if NEXT_PUBLIC_LANGFUSE_CLOUD_REGION is configured
|
||||
ARG NEXT_PUBLIC_LANGFUSE_CLOUD_REGION
|
||||
@@ -130,7 +130,7 @@ RUN if [ -n "$NEXT_PUBLIC_LANGFUSE_CLOUD_REGION" ]; then \
|
||||
npm install --no-package-lock --no-save dd-trace@5.65.0; \
|
||||
fi
|
||||
|
||||
# Runtime images do not need npm once explicit runtime tools are installed.
|
||||
# npm is only used for the installs above; remove it from the final runtime image.
|
||||
RUN rm -rf /usr/local/lib/node_modules/npm && \
|
||||
rm -f /usr/local/bin/npm /usr/local/bin/npx
|
||||
|
||||
|
||||
+4
-4
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "web",
|
||||
"version": "3.164.0",
|
||||
"version": "3.167.0",
|
||||
"private": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
@@ -47,7 +47,7 @@
|
||||
"@langfuse/ee": "workspace:*",
|
||||
"@langfuse/shared": "workspace:*",
|
||||
"@lezer/highlight": "^1.2.3",
|
||||
"@modelcontextprotocol/sdk": "1.27.1",
|
||||
"@modelcontextprotocol/sdk": "1.29.0",
|
||||
"@mui/material": "^7.3.9",
|
||||
"@next-auth/prisma-adapter": "^1.0.7",
|
||||
"@opentelemetry/api": "^1.9.0",
|
||||
@@ -65,7 +65,7 @@
|
||||
"@opentelemetry/sdk-trace-base": "^1.26.0",
|
||||
"@opentelemetry/sdk-trace-node": "^1.26.0",
|
||||
"@paralleldrive/cuid2": "^2.2.2",
|
||||
"@prisma/instrumentation": "^6.17.1",
|
||||
"@prisma/instrumentation": "^6.19.3",
|
||||
"@radix-ui/react-accordion": "^1.2.12",
|
||||
"@radix-ui/react-alert-dialog": "^1.1.15",
|
||||
"@radix-ui/react-avatar": "^1.1.11",
|
||||
@@ -135,7 +135,7 @@
|
||||
"posthog-node": "^5.8.4",
|
||||
"prexit": "^2.2.0",
|
||||
"prism-react-renderer": "^2.4.1",
|
||||
"prisma": "^6.17.1",
|
||||
"prisma": "^6.19.3",
|
||||
"protobufjs": "^7.4.0",
|
||||
"rate-limiter-flexible": "^5.0.4",
|
||||
"react": "19.2.4",
|
||||
|
||||
@@ -5639,6 +5639,19 @@ paths:
|
||||
required: true
|
||||
schema:
|
||||
type: string
|
||||
- name: fields
|
||||
in: query
|
||||
description: >-
|
||||
Comma-separated list of fields to include in the response. Available
|
||||
field groups: 'core' (always included), 'io' (input, output,
|
||||
metadata), 'scores', 'observations', 'metrics'. If not specified,
|
||||
all fields are returned. Example: 'core,scores,metrics'. Note:
|
||||
Excluded 'observations' or 'scores' fields return empty arrays;
|
||||
excluded 'metrics' returns -1 for 'totalCost' and 'latency'.
|
||||
required: false
|
||||
schema:
|
||||
type: string
|
||||
nullable: true
|
||||
responses:
|
||||
'200':
|
||||
description: ''
|
||||
@@ -6353,6 +6366,12 @@ components:
|
||||
description: >-
|
||||
Custom start date for exports (required when exportMode is
|
||||
FROM_CUSTOM_DATE)
|
||||
compressed:
|
||||
type: boolean
|
||||
nullable: true
|
||||
description: >-
|
||||
Enable gzip compression for exported files (.csv.gz, .json.gz,
|
||||
.jsonl.gz). Defaults to true.
|
||||
required:
|
||||
- projectId
|
||||
- type
|
||||
@@ -6399,6 +6418,8 @@ components:
|
||||
type: string
|
||||
format: date-time
|
||||
nullable: true
|
||||
compressed:
|
||||
type: boolean
|
||||
nextSyncAt:
|
||||
type: string
|
||||
format: date-time
|
||||
@@ -6432,6 +6453,7 @@ components:
|
||||
- forcePathStyle
|
||||
- fileType
|
||||
- exportMode
|
||||
- compressed
|
||||
- createdAt
|
||||
- updatedAt
|
||||
BlobStorageIntegrationsResponse:
|
||||
@@ -7283,6 +7305,17 @@ components:
|
||||
- stringValue
|
||||
allOf:
|
||||
- $ref: '#/components/schemas/BaseScoreV1'
|
||||
TextScoreV1:
|
||||
title: TextScoreV1
|
||||
type: object
|
||||
properties:
|
||||
stringValue:
|
||||
type: string
|
||||
description: The text content of the score (1-500 characters)
|
||||
required:
|
||||
- stringValue
|
||||
allOf:
|
||||
- $ref: '#/components/schemas/BaseScoreV1'
|
||||
ScoreV1:
|
||||
title: ScoreV1
|
||||
oneOf:
|
||||
@@ -7319,6 +7352,17 @@ components:
|
||||
- $ref: '#/components/schemas/BooleanScoreV1'
|
||||
required:
|
||||
- dataType
|
||||
- type: object
|
||||
allOf:
|
||||
- type: object
|
||||
properties:
|
||||
dataType:
|
||||
type: string
|
||||
enum:
|
||||
- TEXT
|
||||
- $ref: '#/components/schemas/TextScoreV1'
|
||||
required:
|
||||
- dataType
|
||||
BaseScore:
|
||||
title: BaseScore
|
||||
type: object
|
||||
@@ -7460,6 +7504,17 @@ components:
|
||||
- stringValue
|
||||
allOf:
|
||||
- $ref: '#/components/schemas/BaseScore'
|
||||
TextScore:
|
||||
title: TextScore
|
||||
type: object
|
||||
properties:
|
||||
stringValue:
|
||||
type: string
|
||||
description: The text content of the score (1-500 characters)
|
||||
required:
|
||||
- stringValue
|
||||
allOf:
|
||||
- $ref: '#/components/schemas/BaseScore'
|
||||
Score:
|
||||
title: Score
|
||||
oneOf:
|
||||
@@ -7507,6 +7562,17 @@ components:
|
||||
- $ref: '#/components/schemas/CorrectionScore'
|
||||
required:
|
||||
- dataType
|
||||
- type: object
|
||||
allOf:
|
||||
- type: object
|
||||
properties:
|
||||
dataType:
|
||||
type: string
|
||||
enum:
|
||||
- TEXT
|
||||
- $ref: '#/components/schemas/TextScore'
|
||||
required:
|
||||
- dataType
|
||||
CreateScoreValue:
|
||||
title: CreateScoreValue
|
||||
oneOf:
|
||||
@@ -7514,8 +7580,8 @@ components:
|
||||
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
|
||||
The value of the score. Must be passed as string for categorical and
|
||||
text scores, and numeric for boolean and numeric scores
|
||||
Comment:
|
||||
title: Comment
|
||||
type: object
|
||||
@@ -8235,6 +8301,7 @@ components:
|
||||
- NUMERIC
|
||||
- BOOLEAN
|
||||
- CATEGORICAL
|
||||
- TEXT
|
||||
ScoreDataType:
|
||||
title: ScoreDataType
|
||||
type: string
|
||||
@@ -8243,6 +8310,7 @@ components:
|
||||
- BOOLEAN
|
||||
- CATEGORICAL
|
||||
- CORRECTION
|
||||
- TEXT
|
||||
DeleteDatasetItemResponse:
|
||||
title: DeleteDatasetItemResponse
|
||||
type: object
|
||||
@@ -8869,9 +8937,10 @@ components:
|
||||
value:
|
||||
$ref: '#/components/schemas/CreateScoreValue'
|
||||
description: >-
|
||||
The value of the score. Must be passed as string for categorical
|
||||
scores, and numeric for boolean and numeric scores. Boolean score
|
||||
values must equal either 1 or 0 (true or false)
|
||||
The value of the score. Must be passed as string for categorical and
|
||||
text scores, and numeric for boolean and numeric scores. Boolean
|
||||
score values must equal either 1 or 0 (true or false). Text score
|
||||
values must be between 1 and 500 characters.
|
||||
comment:
|
||||
type: string
|
||||
nullable: true
|
||||
@@ -9188,9 +9257,10 @@ components:
|
||||
value:
|
||||
$ref: '#/components/schemas/CreateScoreValue'
|
||||
description: >-
|
||||
The value of the score. Must be passed as string for categorical
|
||||
scores, and numeric for boolean and numeric scores. Boolean score
|
||||
values must equal either 1 or 0 (true or false)
|
||||
The value of the score. Must be passed as string for categorical and
|
||||
text scores, and numeric for boolean and numeric scores. Boolean
|
||||
score values must equal either 1 or 0 (true or false). Text score
|
||||
values must be between 1 and 500 characters.
|
||||
comment:
|
||||
type: string
|
||||
nullable: true
|
||||
@@ -10821,6 +10891,15 @@ components:
|
||||
nullable: true
|
||||
allOf:
|
||||
- $ref: '#/components/schemas/CorrectionScore'
|
||||
GetScoresResponseDataText:
|
||||
title: GetScoresResponseDataText
|
||||
type: object
|
||||
properties:
|
||||
trace:
|
||||
$ref: '#/components/schemas/GetScoresResponseTraceData'
|
||||
nullable: true
|
||||
allOf:
|
||||
- $ref: '#/components/schemas/TextScore'
|
||||
GetScoresResponseData:
|
||||
title: GetScoresResponseData
|
||||
oneOf:
|
||||
@@ -10868,6 +10947,17 @@ components:
|
||||
- $ref: '#/components/schemas/GetScoresResponseDataCorrection'
|
||||
required:
|
||||
- dataType
|
||||
- type: object
|
||||
allOf:
|
||||
- type: object
|
||||
properties:
|
||||
dataType:
|
||||
type: string
|
||||
enum:
|
||||
- TEXT
|
||||
- $ref: '#/components/schemas/GetScoresResponseDataText'
|
||||
required:
|
||||
- dataType
|
||||
GetScoresResponse:
|
||||
title: GetScoresResponse
|
||||
type: object
|
||||
|
||||
@@ -320,21 +320,17 @@ describe("/api/public/datasets and /api/public/dataset-items API Endpoints", ()
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
it("should return 404 when getting an ARCHIVED dataset item by id", async () => {
|
||||
it("should return archived dataset item when getting by id", async () => {
|
||||
const datasetName = `dataset-archived-by-id-${v4()}`;
|
||||
|
||||
// Create dataset
|
||||
await makeZodVerifiedAPICall(
|
||||
PostDatasetsV1Response,
|
||||
"POST",
|
||||
"/api/public/datasets",
|
||||
{
|
||||
name: datasetName,
|
||||
},
|
||||
{ name: datasetName },
|
||||
auth,
|
||||
);
|
||||
|
||||
// Create an archived dataset item
|
||||
const archivedItem = await makeZodVerifiedAPICall(
|
||||
PostDatasetItemsV1Response,
|
||||
"POST",
|
||||
@@ -350,16 +346,29 @@ describe("/api/public/datasets and /api/public/dataset-items API Endpoints", ()
|
||||
expect(archivedItem.status).toBe(200);
|
||||
expect(archivedItem.body.status).toBe("ARCHIVED");
|
||||
|
||||
// Try to get the archived item by id - should return 404
|
||||
const getArchivedItem = await makeAPICall(
|
||||
const getArchivedItem = await makeZodVerifiedAPICall(
|
||||
GetDatasetItemV1Response,
|
||||
"GET",
|
||||
`/api/public/dataset-items/archived-item-by-id`,
|
||||
undefined,
|
||||
auth,
|
||||
);
|
||||
expect(getArchivedItem.status).toBe(404);
|
||||
expect(getArchivedItem.status).toBe(200);
|
||||
expect(getArchivedItem.body.id).toBe("archived-item-by-id");
|
||||
expect(getArchivedItem.body.status).toBe("ARCHIVED");
|
||||
});
|
||||
|
||||
it("should return active dataset item when getting by id", async () => {
|
||||
const datasetName = `dataset-active-by-id-${v4()}`;
|
||||
|
||||
await makeZodVerifiedAPICall(
|
||||
PostDatasetsV1Response,
|
||||
"POST",
|
||||
"/api/public/datasets",
|
||||
{ name: datasetName },
|
||||
auth,
|
||||
);
|
||||
|
||||
// Create an active item to verify GET still works for active items
|
||||
const activeItem = await makeZodVerifiedAPICall(
|
||||
PostDatasetItemsV1Response,
|
||||
"POST",
|
||||
@@ -374,7 +383,6 @@ describe("/api/public/datasets and /api/public/dataset-items API Endpoints", ()
|
||||
);
|
||||
expect(activeItem.status).toBe(200);
|
||||
|
||||
// Get the active item by id - should succeed
|
||||
const getActiveItem = await makeZodVerifiedAPICall(
|
||||
GetDatasetItemV1Response,
|
||||
"GET",
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import {
|
||||
CTEQueryBuilder,
|
||||
EventsAggregationQueryBuilder,
|
||||
EventsQueryBuilder,
|
||||
} from "@langfuse/shared/src/server";
|
||||
|
||||
describe("CTEQueryBuilder", () => {
|
||||
@@ -121,3 +122,27 @@ describe("CTEQueryBuilder", () => {
|
||||
expect(params.param3).toBe("value3");
|
||||
});
|
||||
});
|
||||
|
||||
describe("EventsQueryBuilder", () => {
|
||||
it("should allow list queries to omit tool payload columns", () => {
|
||||
const slimQuery = new EventsQueryBuilder({
|
||||
projectId: "test-project",
|
||||
})
|
||||
.selectFieldSet("baseWithoutTools", "calculated")
|
||||
.buildWithParams().query;
|
||||
|
||||
const defaultQuery = new EventsQueryBuilder({
|
||||
projectId: "test-project",
|
||||
})
|
||||
.selectFieldSet("base", "calculated")
|
||||
.buildWithParams().query;
|
||||
|
||||
expect(slimQuery).not.toContain('e.tool_definitions as "tool_definitions"');
|
||||
expect(slimQuery).not.toContain('e.tool_calls as "tool_calls"');
|
||||
expect(slimQuery).not.toContain('e.tool_call_names as "tool_call_names"');
|
||||
|
||||
expect(defaultQuery).toContain('e.tool_definitions as "tool_definitions"');
|
||||
expect(defaultQuery).toContain('e.tool_calls as "tool_calls"');
|
||||
expect(defaultQuery).toContain('e.tool_call_names as "tool_call_names"');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,24 +1,70 @@
|
||||
/** @jest-environment node */
|
||||
|
||||
jest.mock("@langfuse/shared/src/server", () => {
|
||||
const actual = jest.requireActual("@langfuse/shared/src/server");
|
||||
return {
|
||||
...actual,
|
||||
fetchLLMCompletion: jest.fn(),
|
||||
};
|
||||
});
|
||||
|
||||
import type { Session } from "next-auth";
|
||||
import { LLMAdapter } from "@langfuse/shared";
|
||||
import { prisma } from "@langfuse/shared/src/db";
|
||||
import { appRouter } from "@/src/server/api/root";
|
||||
import { createInnerTRPCContext } from "@/src/server/api/trpc";
|
||||
import { decrypt } from "@langfuse/shared/encryption";
|
||||
import { createOrgProjectAndApiKey } from "@langfuse/shared/src/server";
|
||||
import {
|
||||
createOrgProjectAndApiKey,
|
||||
fetchLLMCompletion,
|
||||
} from "@langfuse/shared/src/server";
|
||||
|
||||
const mockFetchLLMCompletion = jest.mocked(fetchLLMCompletion);
|
||||
|
||||
describe("llmApiKey.all RPC", () => {
|
||||
let projectId: string;
|
||||
let orgId: string;
|
||||
let session: Session;
|
||||
let caller: ReturnType<typeof appRouter.createCaller>;
|
||||
|
||||
const createCallerForProjectRole = (
|
||||
projectRole: "ADMIN" | "MEMBER" | "VIEWER",
|
||||
) => {
|
||||
const limitedSession: Session = {
|
||||
...session,
|
||||
user: {
|
||||
...session.user!,
|
||||
admin: false,
|
||||
organizations: [
|
||||
{
|
||||
...session.user!.organizations[0],
|
||||
role: "MEMBER",
|
||||
projects: [
|
||||
{
|
||||
...session.user!.organizations[0].projects[0],
|
||||
role: projectRole,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
const limitedCtx = createInnerTRPCContext({
|
||||
session: limitedSession,
|
||||
headers: {},
|
||||
});
|
||||
|
||||
return appRouter.createCaller({ ...limitedCtx, prisma });
|
||||
};
|
||||
|
||||
beforeEach(async () => {
|
||||
const setup = await createOrgProjectAndApiKey();
|
||||
projectId = setup.projectId;
|
||||
orgId = setup.orgId;
|
||||
mockFetchLLMCompletion.mockReset().mockResolvedValue({});
|
||||
|
||||
const session: Session = {
|
||||
session = {
|
||||
expires: "1",
|
||||
user: {
|
||||
id: "user-1",
|
||||
@@ -141,6 +187,159 @@ describe("llmApiKey.all RPC", () => {
|
||||
expect(secretKey).toBeUndefined();
|
||||
});
|
||||
|
||||
it("should require llmApiKeys:create access for testing a new llm api key", async () => {
|
||||
const memberCaller = createCallerForProjectRole("MEMBER");
|
||||
|
||||
await expect(
|
||||
memberCaller.llmApiKey.test({
|
||||
projectId,
|
||||
provider: "openai",
|
||||
adapter: LLMAdapter.OpenAI,
|
||||
secretKey: "sk-test",
|
||||
baseURL: "https://attacker.example.com/v1",
|
||||
}),
|
||||
).rejects.toThrow("User does not have access to this resource or action");
|
||||
});
|
||||
|
||||
it("should require llmApiKeys:create access for testing an existing llm api key", async () => {
|
||||
await caller.llmApiKey.create({
|
||||
projectId,
|
||||
provider: "openai",
|
||||
adapter: LLMAdapter.OpenAI,
|
||||
secretKey: "sk-test",
|
||||
baseURL: "https://api.openai.com/v1",
|
||||
});
|
||||
|
||||
const existingKey = await prisma.llmApiKeys.findFirstOrThrow({
|
||||
where: {
|
||||
projectId,
|
||||
provider: "openai",
|
||||
},
|
||||
});
|
||||
|
||||
const memberCaller = createCallerForProjectRole("MEMBER");
|
||||
|
||||
await expect(
|
||||
memberCaller.llmApiKey.testUpdate({
|
||||
id: existingKey.id,
|
||||
projectId,
|
||||
provider: "openai",
|
||||
adapter: LLMAdapter.OpenAI,
|
||||
baseURL: "https://attacker.example.com/v1",
|
||||
}),
|
||||
).rejects.toThrow("User does not have access to this resource or action");
|
||||
});
|
||||
|
||||
it("should block testUpdate when the base URL changes without a new secret key", async () => {
|
||||
await caller.llmApiKey.create({
|
||||
projectId,
|
||||
provider: "openai",
|
||||
adapter: LLMAdapter.OpenAI,
|
||||
secretKey: "sk-original",
|
||||
baseURL: "https://api.openai.com/v1",
|
||||
});
|
||||
|
||||
const existingKey = await prisma.llmApiKeys.findFirstOrThrow({
|
||||
where: {
|
||||
projectId,
|
||||
provider: "openai",
|
||||
},
|
||||
});
|
||||
|
||||
const result = await caller.llmApiKey.testUpdate({
|
||||
id: existingKey.id,
|
||||
projectId,
|
||||
provider: "openai",
|
||||
adapter: LLMAdapter.OpenAI,
|
||||
baseURL: "https://attacker.example.com/v1",
|
||||
});
|
||||
|
||||
expect(result).toEqual({
|
||||
success: false,
|
||||
error: "Secret key is required when changing the base URL",
|
||||
});
|
||||
expect(mockFetchLLMCompletion).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("should allow testUpdate without a new secret key when the base URL is unchanged", async () => {
|
||||
const existingExtraHeaders = {
|
||||
Authorization: "Bearer stored-token",
|
||||
"X-Custom-Header": "stored-value",
|
||||
};
|
||||
|
||||
await caller.llmApiKey.create({
|
||||
projectId,
|
||||
provider: "openai",
|
||||
adapter: LLMAdapter.OpenAI,
|
||||
secretKey: "sk-original",
|
||||
baseURL: "https://api.openai.com/v1",
|
||||
extraHeaders: existingExtraHeaders,
|
||||
});
|
||||
|
||||
const existingKey = await prisma.llmApiKeys.findFirstOrThrow({
|
||||
where: {
|
||||
projectId,
|
||||
provider: "openai",
|
||||
},
|
||||
});
|
||||
|
||||
const result = await caller.llmApiKey.testUpdate({
|
||||
id: existingKey.id,
|
||||
projectId,
|
||||
provider: "openai",
|
||||
adapter: LLMAdapter.OpenAI,
|
||||
baseURL: "https://api.openai.com/v1",
|
||||
});
|
||||
|
||||
expect(result).toEqual({ success: true });
|
||||
expect(mockFetchLLMCompletion).toHaveBeenCalledTimes(1);
|
||||
const llmConnection = mockFetchLLMCompletion.mock.calls[0][0].llmConnection;
|
||||
expect(llmConnection.baseURL).toBe("https://api.openai.com/v1");
|
||||
expect(decrypt(llmConnection.secretKey)).toBe("sk-original");
|
||||
expect(JSON.parse(decrypt(llmConnection.extraHeaders))).toEqual(
|
||||
existingExtraHeaders,
|
||||
);
|
||||
});
|
||||
|
||||
it("should allow testUpdate when the base URL changes and a new secret key is provided", async () => {
|
||||
const existingExtraHeaders = {
|
||||
Authorization: "Bearer stored-token",
|
||||
"X-Custom-Header": "stored-value",
|
||||
};
|
||||
|
||||
await caller.llmApiKey.create({
|
||||
projectId,
|
||||
provider: "openai",
|
||||
adapter: LLMAdapter.OpenAI,
|
||||
secretKey: "sk-original",
|
||||
baseURL: "https://api.openai.com/v1",
|
||||
extraHeaders: existingExtraHeaders,
|
||||
});
|
||||
|
||||
const existingKey = await prisma.llmApiKeys.findFirstOrThrow({
|
||||
where: {
|
||||
projectId,
|
||||
provider: "openai",
|
||||
},
|
||||
});
|
||||
|
||||
const result = await caller.llmApiKey.testUpdate({
|
||||
id: existingKey.id,
|
||||
projectId,
|
||||
provider: "openai",
|
||||
adapter: LLMAdapter.OpenAI,
|
||||
secretKey: "sk-rotated",
|
||||
baseURL: "https://new-endpoint.example.com/v1",
|
||||
});
|
||||
|
||||
expect(result).toEqual({ success: true });
|
||||
expect(mockFetchLLMCompletion).toHaveBeenCalledTimes(1);
|
||||
const llmConnection = mockFetchLLMCompletion.mock.calls[0][0].llmConnection;
|
||||
expect(llmConnection.baseURL).toBe("https://new-endpoint.example.com/v1");
|
||||
expect(decrypt(llmConnection.secretKey)).toBe("sk-rotated");
|
||||
expect(llmConnection.extraHeaders).toBeUndefined();
|
||||
});
|
||||
|
||||
it("should create and update an llm api key", async () => {
|
||||
const secret = "test-secret";
|
||||
const provider = "openai";
|
||||
|
||||
@@ -69,7 +69,7 @@ const setupTriggerAndAction = async (projectId: string) => {
|
||||
id: v4(),
|
||||
projectId: projectId,
|
||||
eventSource: "prompt",
|
||||
eventActions: ["updated"],
|
||||
eventActions: [],
|
||||
filter: [],
|
||||
status: "ACTIVE",
|
||||
},
|
||||
|
||||
@@ -2,11 +2,15 @@ import { prisma } from "@langfuse/shared/src/db";
|
||||
import {
|
||||
createScoresCh,
|
||||
getScoreById,
|
||||
getScoresByIds,
|
||||
getScoresGroupedByNameSourceType,
|
||||
getScoresUiTable,
|
||||
getScoresForTraces,
|
||||
getScoresForObservations,
|
||||
getScoresForSessions,
|
||||
getScoresForExperiments,
|
||||
getTraceScoresForDatasetRuns,
|
||||
getScoreNames,
|
||||
createTracesCh,
|
||||
createObservationsCh,
|
||||
createTrace,
|
||||
@@ -17,6 +21,7 @@ import {
|
||||
createDatasetRunScore,
|
||||
createSessionScore,
|
||||
createOrgProjectAndApiKey,
|
||||
getScoreStringValues,
|
||||
} from "@langfuse/shared/src/server";
|
||||
import { v4 } from "uuid";
|
||||
|
||||
@@ -798,4 +803,350 @@ describe("Clickhouse Scores Repository Test", () => {
|
||||
expect(result[0].metadata).toEqual({});
|
||||
});
|
||||
});
|
||||
|
||||
describe("TEXT scores", () => {
|
||||
it("should round-trip a TEXT score through ClickHouse", async () => {
|
||||
const scoreId = v4();
|
||||
const traceId = v4();
|
||||
|
||||
const score = createTraceScore({
|
||||
id: scoreId,
|
||||
project_id: projectId,
|
||||
trace_id: traceId,
|
||||
name: "free-form-score",
|
||||
value: 0,
|
||||
string_value: "This is free text feedback",
|
||||
data_type: "TEXT",
|
||||
source: "ANNOTATION",
|
||||
});
|
||||
|
||||
await createScoresCh([score]);
|
||||
|
||||
const result = await getScoreById({ projectId, scoreId });
|
||||
expect(result).toBeDefined();
|
||||
expect(result!.dataType).toBe("TEXT");
|
||||
expect(result!.stringValue).toBe("This is free text feedback");
|
||||
expect(result!.value).toBe(0);
|
||||
expect(result!.source).toBe("ANNOTATION");
|
||||
});
|
||||
|
||||
it("should include TEXT scores in getScoresGroupedByNameSourceType", async () => {
|
||||
const isolatedProjectId = v4();
|
||||
const traceId = v4();
|
||||
|
||||
const numericScore = createTraceScore({
|
||||
project_id: isolatedProjectId,
|
||||
trace_id: traceId,
|
||||
name: "numeric-score",
|
||||
data_type: "NUMERIC",
|
||||
value: 42,
|
||||
source: "API",
|
||||
});
|
||||
|
||||
const textScore = createTraceScore({
|
||||
project_id: isolatedProjectId,
|
||||
trace_id: traceId,
|
||||
name: "free-form-score",
|
||||
data_type: "TEXT",
|
||||
value: 0,
|
||||
string_value: "Some feedback text",
|
||||
source: "ANNOTATION",
|
||||
});
|
||||
|
||||
await createScoresCh([numericScore, textScore]);
|
||||
|
||||
const result = await getScoresGroupedByNameSourceType({
|
||||
projectId: isolatedProjectId,
|
||||
filter: [],
|
||||
});
|
||||
|
||||
expect(result).toHaveLength(2);
|
||||
expect(result).toEqual([
|
||||
{
|
||||
name: "free-form-score",
|
||||
source: "ANNOTATION",
|
||||
dataType: "TEXT",
|
||||
},
|
||||
{
|
||||
name: "numeric-score",
|
||||
source: "API",
|
||||
dataType: "NUMERIC",
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it("should exclude TEXT string values from getScoreStringValues", async () => {
|
||||
const isolatedProjectId = v4();
|
||||
const traceId = v4();
|
||||
|
||||
const categoricalScore = createTraceScore({
|
||||
project_id: isolatedProjectId,
|
||||
trace_id: traceId,
|
||||
name: "cat-score",
|
||||
data_type: "CATEGORICAL",
|
||||
value: 1,
|
||||
string_value: "Good",
|
||||
source: "API",
|
||||
});
|
||||
|
||||
const textScore = createTraceScore({
|
||||
project_id: isolatedProjectId,
|
||||
trace_id: traceId,
|
||||
name: "free-text-score",
|
||||
data_type: "TEXT",
|
||||
value: 0,
|
||||
string_value: "This should not appear in filter options",
|
||||
source: "ANNOTATION",
|
||||
});
|
||||
|
||||
await createScoresCh([categoricalScore, textScore]);
|
||||
|
||||
const result = await getScoreStringValues(isolatedProjectId, []);
|
||||
|
||||
const stringValues = result.map((r) => r.value);
|
||||
expect(stringValues).toContain("Good");
|
||||
expect(stringValues).not.toContain(
|
||||
"This should not appear in filter options",
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("score data type coverage", () => {
|
||||
let isolatedProjectId: string;
|
||||
const traceId = v4();
|
||||
const observationId = v4();
|
||||
const sessionId = v4();
|
||||
const scoreIds: string[] = [];
|
||||
|
||||
beforeAll(async () => {
|
||||
const { projectId: pid } = await createOrgProjectAndApiKey();
|
||||
isolatedProjectId = pid;
|
||||
|
||||
const trace = createTrace({
|
||||
id: traceId,
|
||||
project_id: isolatedProjectId,
|
||||
session_id: sessionId,
|
||||
});
|
||||
await createTracesCh([trace]);
|
||||
|
||||
const obs = createObservation({
|
||||
id: observationId,
|
||||
trace_id: traceId,
|
||||
project_id: isolatedProjectId,
|
||||
});
|
||||
await createObservationsCh([obs]);
|
||||
|
||||
const scores = [
|
||||
createTraceScore({
|
||||
project_id: isolatedProjectId,
|
||||
trace_id: traceId,
|
||||
observation_id: observationId,
|
||||
name: "numeric",
|
||||
data_type: "NUMERIC",
|
||||
value: 1,
|
||||
source: "API",
|
||||
}),
|
||||
createTraceScore({
|
||||
project_id: isolatedProjectId,
|
||||
trace_id: traceId,
|
||||
observation_id: observationId,
|
||||
name: "categorical",
|
||||
data_type: "CATEGORICAL",
|
||||
value: 0,
|
||||
string_value: "Good",
|
||||
source: "API",
|
||||
}),
|
||||
createTraceScore({
|
||||
project_id: isolatedProjectId,
|
||||
trace_id: traceId,
|
||||
observation_id: observationId,
|
||||
name: "boolean",
|
||||
data_type: "BOOLEAN",
|
||||
value: 1,
|
||||
string_value: "True",
|
||||
source: "API",
|
||||
}),
|
||||
createTraceScore({
|
||||
project_id: isolatedProjectId,
|
||||
trace_id: traceId,
|
||||
observation_id: observationId,
|
||||
name: "free-form",
|
||||
data_type: "TEXT",
|
||||
value: 0,
|
||||
string_value: "Some feedback",
|
||||
source: "ANNOTATION",
|
||||
}),
|
||||
];
|
||||
|
||||
scoreIds.push(...scores.map((s) => s.id));
|
||||
|
||||
const sessionScores = [
|
||||
createSessionScore({
|
||||
project_id: isolatedProjectId,
|
||||
session_id: sessionId,
|
||||
name: "session-numeric",
|
||||
data_type: "NUMERIC",
|
||||
value: 5,
|
||||
source: "API",
|
||||
}),
|
||||
createSessionScore({
|
||||
project_id: isolatedProjectId,
|
||||
session_id: sessionId,
|
||||
name: "session-free-form",
|
||||
data_type: "TEXT",
|
||||
value: 0,
|
||||
string_value: "Session feedback",
|
||||
source: "ANNOTATION",
|
||||
}),
|
||||
];
|
||||
|
||||
await createScoresCh([...scores, ...sessionScores]);
|
||||
});
|
||||
|
||||
it("getScoresByIds should return all non-CORRECTION types", async () => {
|
||||
const result = await getScoresByIds(isolatedProjectId, scoreIds);
|
||||
expect(result).toHaveLength(4);
|
||||
});
|
||||
|
||||
it("getScoresForTraces should return all non-CORRECTION types", async () => {
|
||||
const result = await getScoresForTraces({
|
||||
projectId: isolatedProjectId,
|
||||
traceIds: [traceId],
|
||||
});
|
||||
expect(result).toHaveLength(4);
|
||||
});
|
||||
|
||||
it("getScoresForObservations should return all non-CORRECTION types", async () => {
|
||||
const result = await getScoresForObservations({
|
||||
projectId: isolatedProjectId,
|
||||
observationIds: [observationId],
|
||||
});
|
||||
expect(result).toHaveLength(4);
|
||||
});
|
||||
|
||||
it("getScoresForSessions should return all non-CORRECTION types", async () => {
|
||||
const result = await getScoresForSessions({
|
||||
projectId: isolatedProjectId,
|
||||
sessionIds: [sessionId],
|
||||
});
|
||||
expect(result).toHaveLength(2);
|
||||
});
|
||||
|
||||
it("getScoreNames should return names of all non-CORRECTION types", async () => {
|
||||
const result = await getScoreNames(isolatedProjectId, []);
|
||||
const names = result.map((r) => r.name);
|
||||
expect(names).toContain("numeric");
|
||||
expect(names).toContain("categorical");
|
||||
expect(names).toContain("boolean");
|
||||
expect(names).toContain("free-form");
|
||||
});
|
||||
|
||||
it("getScoresGroupedByNameSourceType should return all listable score types", async () => {
|
||||
const result = await getScoresGroupedByNameSourceType({
|
||||
projectId: isolatedProjectId,
|
||||
filter: [],
|
||||
});
|
||||
const dataTypes = result.map((r) => r.dataType);
|
||||
expect(dataTypes).toContain("NUMERIC");
|
||||
expect(dataTypes).toContain("CATEGORICAL");
|
||||
expect(dataTypes).toContain("BOOLEAN");
|
||||
expect(dataTypes).toContain("TEXT");
|
||||
});
|
||||
|
||||
it("getScoresForExperiments should exclude TEXT scores", async () => {
|
||||
const dataset = await prisma.dataset.create({
|
||||
data: { projectId: isolatedProjectId, name: v4() },
|
||||
});
|
||||
const datasetRun = await prisma.datasetRuns.create({
|
||||
data: {
|
||||
projectId: isolatedProjectId,
|
||||
name: v4(),
|
||||
datasetId: dataset.id,
|
||||
},
|
||||
});
|
||||
|
||||
const scores = [
|
||||
createDatasetRunScore({
|
||||
project_id: isolatedProjectId,
|
||||
dataset_run_id: datasetRun.id,
|
||||
name: "run-numeric",
|
||||
data_type: "NUMERIC",
|
||||
value: 1,
|
||||
}),
|
||||
createDatasetRunScore({
|
||||
project_id: isolatedProjectId,
|
||||
dataset_run_id: datasetRun.id,
|
||||
name: "run-free-form",
|
||||
data_type: "TEXT",
|
||||
value: 0,
|
||||
string_value: "Should be excluded",
|
||||
}),
|
||||
];
|
||||
await createScoresCh(scores);
|
||||
|
||||
const result = await getScoresForExperiments({
|
||||
projectId: isolatedProjectId,
|
||||
runIds: [datasetRun.id],
|
||||
});
|
||||
|
||||
expect(result).toHaveLength(1);
|
||||
expect(result[0].dataType).toBe("NUMERIC");
|
||||
});
|
||||
|
||||
it("getTraceScoresForDatasetRuns should exclude TEXT scores", async () => {
|
||||
const dataset = await prisma.dataset.create({
|
||||
data: { projectId: isolatedProjectId, name: v4() },
|
||||
});
|
||||
const datasetRun = await prisma.datasetRuns.create({
|
||||
data: {
|
||||
projectId: isolatedProjectId,
|
||||
name: v4(),
|
||||
datasetId: dataset.id,
|
||||
},
|
||||
});
|
||||
const runTraceId = v4();
|
||||
const runTrace = createTrace({
|
||||
id: runTraceId,
|
||||
project_id: isolatedProjectId,
|
||||
});
|
||||
await createTracesCh([runTrace]);
|
||||
|
||||
const datasetRunItem = createDatasetRunItem({
|
||||
project_id: isolatedProjectId,
|
||||
dataset_run_id: datasetRun.id,
|
||||
dataset_id: dataset.id,
|
||||
dataset_run_name: datasetRun.name,
|
||||
dataset_item_id: v4(),
|
||||
trace_id: runTraceId,
|
||||
});
|
||||
await createDatasetRunItemsCh([datasetRunItem]);
|
||||
|
||||
const scores = [
|
||||
createTraceScore({
|
||||
project_id: isolatedProjectId,
|
||||
trace_id: runTraceId,
|
||||
name: "trace-boolean",
|
||||
data_type: "BOOLEAN",
|
||||
value: 1,
|
||||
string_value: "True",
|
||||
}),
|
||||
createTraceScore({
|
||||
project_id: isolatedProjectId,
|
||||
trace_id: runTraceId,
|
||||
name: "trace-free-form",
|
||||
data_type: "TEXT",
|
||||
value: 0,
|
||||
string_value: "Should be excluded",
|
||||
}),
|
||||
];
|
||||
await createScoresCh(scores);
|
||||
|
||||
const result = await getTraceScoresForDatasetRuns(isolatedProjectId, [
|
||||
datasetRun.id,
|
||||
]);
|
||||
|
||||
expect(result).toHaveLength(1);
|
||||
expect(result[0].dataType).toBe("BOOLEAN");
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -257,6 +257,34 @@ describe("/api/public/score-configs API Endpoint", () => {
|
||||
]);
|
||||
});
|
||||
|
||||
it("should POST a text score config", async () => {
|
||||
const postScoreConfig = await makeZodVerifiedAPICall(
|
||||
PostScoreConfigResponse,
|
||||
"POST",
|
||||
"/api/public/score-configs",
|
||||
{
|
||||
name: "text-config-name",
|
||||
dataType: "TEXT",
|
||||
},
|
||||
auth,
|
||||
);
|
||||
|
||||
const scoreConfig = await makeZodVerifiedAPICall(
|
||||
GetScoreConfigResponse,
|
||||
"GET",
|
||||
`/api/public/score-configs/${postScoreConfig.body.id}`,
|
||||
undefined,
|
||||
auth,
|
||||
);
|
||||
|
||||
expect(postScoreConfig.status).toBe(200);
|
||||
expect(scoreConfig.body.name).toBe("text-config-name");
|
||||
expect(scoreConfig.body.dataType).toBe("TEXT");
|
||||
expect(scoreConfig.body.categories).toBeFalsy();
|
||||
expect(scoreConfig.body.maxValue).toBeFalsy();
|
||||
expect(scoreConfig.body.minValue).toBeFalsy();
|
||||
});
|
||||
|
||||
it("should fail POST of numeric score config with invalid range", async () => {
|
||||
try {
|
||||
await makeZodVerifiedAPICall(
|
||||
|
||||
@@ -75,6 +75,51 @@ describe("/api/public/scores API Endpoint", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("should GET a text score", async () => {
|
||||
const { projectId, auth } = await createOrgProjectAndApiKey();
|
||||
|
||||
const scoreId = v4();
|
||||
const traceId = v4();
|
||||
const score = createTraceScore({
|
||||
id: scoreId,
|
||||
project_id: projectId,
|
||||
trace_id: traceId,
|
||||
name: "Text Score",
|
||||
timestamp: Date.now(),
|
||||
value: 0,
|
||||
string_value: "Great explanation",
|
||||
source: "API",
|
||||
comment: "comment",
|
||||
data_type: "TEXT" as const,
|
||||
created_at: Date.now(),
|
||||
updated_at: Date.now(),
|
||||
event_ts: Date.now(),
|
||||
is_deleted: 0,
|
||||
});
|
||||
|
||||
await createScoresCh([score]);
|
||||
|
||||
const getScore = await makeZodVerifiedAPICall(
|
||||
GetScoreResponseV1,
|
||||
"GET",
|
||||
`/api/public/scores/${scoreId}`,
|
||||
undefined,
|
||||
auth,
|
||||
);
|
||||
|
||||
expect(getScore.status).toBe(200);
|
||||
expect(getScore.body).toMatchObject({
|
||||
id: scoreId,
|
||||
name: "Text Score",
|
||||
stringValue: "Great explanation",
|
||||
comment: "comment",
|
||||
source: "API",
|
||||
traceId,
|
||||
dataType: "TEXT",
|
||||
});
|
||||
expect(getScore.body).not.toHaveProperty("value");
|
||||
});
|
||||
|
||||
it("should GET score with minimal score data and minimal trace data", async () => {
|
||||
const { projectId, auth } = await createOrgProjectAndApiKey();
|
||||
|
||||
@@ -422,6 +467,8 @@ describe("/api/public/scores API Endpoint", () => {
|
||||
const scoreId_5 = v4();
|
||||
const scoreId_6 = v4();
|
||||
const scoreId_7 = v4();
|
||||
const textScoreId_1 = v4();
|
||||
const textScoreId_2 = v4();
|
||||
let authentication: string;
|
||||
let newProjectId: string;
|
||||
|
||||
@@ -530,6 +577,26 @@ describe("/api/public/scores API Endpoint", () => {
|
||||
environment: "production",
|
||||
});
|
||||
|
||||
const textScore1 = createTraceScore({
|
||||
id: textScoreId_1,
|
||||
project_id: newProjectId,
|
||||
trace_id: traceId_2,
|
||||
name: "text-score-name",
|
||||
data_type: "TEXT",
|
||||
string_value: "text-value-1",
|
||||
value: 0,
|
||||
});
|
||||
|
||||
const textScore2 = createTraceScore({
|
||||
id: textScoreId_2,
|
||||
project_id: newProjectId,
|
||||
trace_id: traceId_3,
|
||||
name: "text-score-name",
|
||||
data_type: "TEXT",
|
||||
string_value: "text-value-2",
|
||||
value: 0,
|
||||
});
|
||||
|
||||
const sessionScore1 = createSessionScore({
|
||||
id: scoreId_6,
|
||||
project_id: newProjectId,
|
||||
@@ -554,6 +621,8 @@ describe("/api/public/scores API Endpoint", () => {
|
||||
score3,
|
||||
score4,
|
||||
score5,
|
||||
textScore1,
|
||||
textScore2,
|
||||
sessionScore1,
|
||||
sessionScore2,
|
||||
]);
|
||||
@@ -571,7 +640,7 @@ describe("/api/public/scores API Endpoint", () => {
|
||||
expect(getAllScore.body.meta).toMatchObject({
|
||||
page: 1,
|
||||
limit: 50,
|
||||
totalItems: 5, // 7 scores in total, but only 5 are trace scores
|
||||
totalItems: 7, // 9 scores in total, but only 7 are trace scores
|
||||
totalPages: 1,
|
||||
});
|
||||
for (const val of getAllScore.body.data) {
|
||||
@@ -631,6 +700,31 @@ describe("/api/public/scores API Endpoint", () => {
|
||||
}
|
||||
});
|
||||
|
||||
it("get all scores for text data type", async () => {
|
||||
const getAllScore = await makeZodVerifiedAPICall(
|
||||
GetScoresResponseV1,
|
||||
"GET",
|
||||
`/api/public/scores?dataType=TEXT`,
|
||||
undefined,
|
||||
authentication,
|
||||
);
|
||||
|
||||
expect(getAllScore.status).toBe(200);
|
||||
expect(getAllScore.body.meta).toMatchObject({
|
||||
page: 1,
|
||||
limit: 50,
|
||||
totalItems: 2,
|
||||
totalPages: 1,
|
||||
});
|
||||
for (const val of getAllScore.body.data) {
|
||||
expect(val).toMatchObject({
|
||||
dataType: "TEXT",
|
||||
name: "text-score-name",
|
||||
});
|
||||
expect(val.stringValue).toContain("text-value");
|
||||
}
|
||||
});
|
||||
|
||||
it("get all scores for trace tag 'prod'", async () => {
|
||||
const getAllScore = await makeZodVerifiedAPICall(
|
||||
GetScoresResponseV1,
|
||||
@@ -696,7 +790,7 @@ describe("/api/public/scores API Endpoint", () => {
|
||||
expect(getAllScore.body.meta).toMatchObject({
|
||||
page: 1,
|
||||
limit: 50,
|
||||
totalItems: 1,
|
||||
totalItems: 2,
|
||||
totalPages: 1,
|
||||
});
|
||||
for (const val of getAllScore.body.data) {
|
||||
|
||||
@@ -190,6 +190,51 @@ describe("/api/public/v2/scores API Endpoint", () => {
|
||||
dataType: "NUMERIC",
|
||||
});
|
||||
});
|
||||
|
||||
it("should GET a text score", async () => {
|
||||
const { projectId, auth } = await createOrgProjectAndApiKey();
|
||||
|
||||
const scoreId = v4();
|
||||
const traceId = v4();
|
||||
const score = createTraceScore({
|
||||
id: scoreId,
|
||||
project_id: projectId,
|
||||
trace_id: traceId,
|
||||
name: "Text Score",
|
||||
timestamp: Date.now(),
|
||||
value: 0,
|
||||
string_value: "Great explanation",
|
||||
source: "API",
|
||||
comment: "comment",
|
||||
data_type: "TEXT" as const,
|
||||
created_at: Date.now(),
|
||||
updated_at: Date.now(),
|
||||
event_ts: Date.now(),
|
||||
is_deleted: 0,
|
||||
});
|
||||
|
||||
await createScoresCh([score]);
|
||||
|
||||
const getScore = await makeZodVerifiedAPICall(
|
||||
GetScoreResponseV2,
|
||||
"GET",
|
||||
`/api/public/v2/scores/${scoreId}`,
|
||||
undefined,
|
||||
auth,
|
||||
);
|
||||
|
||||
expect(getScore.status).toBe(200);
|
||||
expect(getScore.body).toMatchObject({
|
||||
id: scoreId,
|
||||
name: "Text Score",
|
||||
stringValue: "Great explanation",
|
||||
comment: "comment",
|
||||
source: "API",
|
||||
traceId,
|
||||
dataType: "TEXT",
|
||||
});
|
||||
expect(getScore.body).not.toHaveProperty("value");
|
||||
});
|
||||
});
|
||||
|
||||
describe("GET /api/public/scores", () => {
|
||||
@@ -217,6 +262,8 @@ describe("/api/public/v2/scores API Endpoint", () => {
|
||||
const scoreId_9 = v4();
|
||||
const correctionScoreId_1 = v4();
|
||||
const correctionScoreId_2 = v4();
|
||||
const textScoreId_1 = v4();
|
||||
const textScoreId_2 = v4();
|
||||
let authentication: string;
|
||||
let newProjectId: string;
|
||||
let executionTraceId: string;
|
||||
@@ -348,6 +395,26 @@ describe("/api/public/v2/scores API Endpoint", () => {
|
||||
environment: "annotation",
|
||||
});
|
||||
|
||||
const textScore1 = createTraceScore({
|
||||
id: textScoreId_1,
|
||||
project_id: newProjectId,
|
||||
trace_id: traceId_2,
|
||||
name: "text-score-name",
|
||||
data_type: "TEXT",
|
||||
string_value: "text-value-1",
|
||||
value: 0,
|
||||
});
|
||||
|
||||
const textScore2 = createTraceScore({
|
||||
id: textScoreId_2,
|
||||
project_id: newProjectId,
|
||||
trace_id: traceId_3,
|
||||
name: "text-score-name",
|
||||
data_type: "TEXT",
|
||||
string_value: "text-value-2",
|
||||
value: 0,
|
||||
});
|
||||
|
||||
const sessionScore1 = createSessionScore({
|
||||
id: scoreId_6,
|
||||
project_id: newProjectId,
|
||||
@@ -396,6 +463,8 @@ describe("/api/public/v2/scores API Endpoint", () => {
|
||||
runScore2,
|
||||
correction1,
|
||||
correction2,
|
||||
textScore1,
|
||||
textScore2,
|
||||
]);
|
||||
});
|
||||
|
||||
@@ -411,7 +480,7 @@ describe("/api/public/v2/scores API Endpoint", () => {
|
||||
expect(getAllScore.body.meta).toMatchObject({
|
||||
page: 1,
|
||||
limit: 50,
|
||||
totalItems: 11,
|
||||
totalItems: 13,
|
||||
totalPages: 1,
|
||||
});
|
||||
for (const val of getAllScore.body.data) {
|
||||
@@ -517,6 +586,31 @@ describe("/api/public/v2/scores API Endpoint", () => {
|
||||
}
|
||||
});
|
||||
|
||||
it("get all scores for text data type", async () => {
|
||||
const getAllScore = await makeZodVerifiedAPICall(
|
||||
GetScoresResponseV2,
|
||||
"GET",
|
||||
`/api/public/v2/scores?dataType=TEXT`,
|
||||
undefined,
|
||||
authentication,
|
||||
);
|
||||
|
||||
expect(getAllScore.status).toBe(200);
|
||||
expect(getAllScore.body.meta).toMatchObject({
|
||||
page: 1,
|
||||
limit: 50,
|
||||
totalItems: 2,
|
||||
totalPages: 1,
|
||||
});
|
||||
for (const val of getAllScore.body.data) {
|
||||
expect(val).toMatchObject({
|
||||
dataType: "TEXT",
|
||||
name: "text-score-name",
|
||||
});
|
||||
expect(val.stringValue).toContain("text-value");
|
||||
}
|
||||
});
|
||||
|
||||
it("get all scores for trace tag 'prod'", async () => {
|
||||
const getAllScore = await makeZodVerifiedAPICall(
|
||||
GetScoresResponseV2,
|
||||
@@ -737,7 +831,7 @@ describe("/api/public/v2/scores API Endpoint", () => {
|
||||
expect(getAllScore.body.meta).toMatchObject({
|
||||
page: 1,
|
||||
limit: 50,
|
||||
totalItems: 2,
|
||||
totalItems: 3,
|
||||
totalPages: 1,
|
||||
});
|
||||
for (const val of getAllScore.body.data) {
|
||||
|
||||
@@ -220,6 +220,139 @@ describe("/api/public/traces API Endpoint", () => {
|
||||
);
|
||||
});
|
||||
|
||||
it("should fetch a trace with core-only fields when fields=core", async () => {
|
||||
const traceId = randomUUID();
|
||||
const createdTrace = createTrace({
|
||||
id: traceId,
|
||||
name: "trace-core-only",
|
||||
user_id: "user-1",
|
||||
project_id: projectId,
|
||||
metadata: { key: "value" },
|
||||
input: JSON.stringify({ prompt: "test" }),
|
||||
output: JSON.stringify({ response: "test response" }),
|
||||
});
|
||||
|
||||
const observation = createObservation({
|
||||
trace_id: traceId,
|
||||
project_id: projectId,
|
||||
name: "test-observation",
|
||||
end_time: new Date().getTime(),
|
||||
start_time: new Date().getTime() - 1000,
|
||||
cost_details: { input: 0.02, output: 0.03, total: 0.05 },
|
||||
});
|
||||
|
||||
const score = createTraceScore({
|
||||
trace_id: traceId,
|
||||
project_id: projectId,
|
||||
name: "test-score",
|
||||
value: 0.8,
|
||||
});
|
||||
|
||||
await createTracesCh([createdTrace]);
|
||||
await createObservationsCh([observation]);
|
||||
await createScoresCh([score]);
|
||||
|
||||
const trace = await makeZodVerifiedAPICall(
|
||||
GetTraceV1Response,
|
||||
"GET",
|
||||
`/api/public/traces/${traceId}?fields=core`,
|
||||
);
|
||||
|
||||
expect(trace.body.id).toBe(traceId);
|
||||
expect(trace.body.input).toBeNull();
|
||||
expect(trace.body.output).toBeNull();
|
||||
expect(trace.body.metadata).toEqual({});
|
||||
expect(trace.body.observations).toEqual([]);
|
||||
expect(trace.body.scores).toEqual([]);
|
||||
expect(trace.body.totalCost).toBe(-1);
|
||||
expect(trace.body.latency).toBe(-1);
|
||||
});
|
||||
|
||||
it("should fetch a trace with core,scores,metrics fields", async () => {
|
||||
const traceId = randomUUID();
|
||||
const createdTrace = createTrace({
|
||||
id: traceId,
|
||||
name: "trace-with-scores-metrics",
|
||||
project_id: projectId,
|
||||
input: JSON.stringify({ prompt: "test" }),
|
||||
output: JSON.stringify({ response: "test response" }),
|
||||
});
|
||||
|
||||
const observation = createObservation({
|
||||
trace_id: traceId,
|
||||
project_id: projectId,
|
||||
name: "test-observation",
|
||||
end_time: new Date().getTime(),
|
||||
start_time: new Date().getTime() - 1000,
|
||||
cost_details: { input: 0.02, output: 0.03, total: 0.05 },
|
||||
input: "observation input",
|
||||
output: "observation output",
|
||||
});
|
||||
|
||||
const score = createTraceScore({
|
||||
trace_id: traceId,
|
||||
project_id: projectId,
|
||||
name: "test-score",
|
||||
value: 0.8,
|
||||
});
|
||||
|
||||
await createTracesCh([createdTrace]);
|
||||
await createObservationsCh([observation]);
|
||||
await createScoresCh([score]);
|
||||
|
||||
const trace = await makeZodVerifiedAPICall(
|
||||
GetTraceV1Response,
|
||||
"GET",
|
||||
`/api/public/traces/${traceId}?fields=core,scores,metrics`,
|
||||
);
|
||||
|
||||
expect(trace.body.id).toBe(traceId);
|
||||
expect(trace.body.input).toBeNull();
|
||||
expect(trace.body.output).toBeNull();
|
||||
expect(trace.body.observations).toEqual([]);
|
||||
expect(trace.body.scores).toHaveLength(1);
|
||||
expect(trace.body.totalCost).toBe(0.05);
|
||||
expect(trace.body.latency).toBeCloseTo(1);
|
||||
});
|
||||
|
||||
it("should return all fields when fields param contains only invalid groups", async () => {
|
||||
const traceId = randomUUID();
|
||||
const createdTrace = createTrace({
|
||||
id: traceId,
|
||||
name: "trace-invalid-fields",
|
||||
project_id: projectId,
|
||||
input: JSON.stringify({ prompt: "test" }),
|
||||
output: JSON.stringify({ response: "test response" }),
|
||||
metadata: { key: "value" },
|
||||
});
|
||||
|
||||
const observation = createObservation({
|
||||
trace_id: traceId,
|
||||
project_id: projectId,
|
||||
name: "test-observation",
|
||||
end_time: new Date().getTime(),
|
||||
start_time: new Date().getTime() - 1000,
|
||||
cost_details: { input: 0.01, output: 0.02, total: 0.03 },
|
||||
});
|
||||
|
||||
await createTracesCh([createdTrace]);
|
||||
await createObservationsCh([observation]);
|
||||
|
||||
const trace = await makeZodVerifiedAPICall(
|
||||
GetTraceV1Response,
|
||||
"GET",
|
||||
`/api/public/traces/${traceId}?fields=invalid_group,also_invalid`,
|
||||
);
|
||||
|
||||
// All invalid fields should fall back to returning all field groups
|
||||
expect(trace.body.id).toBe(traceId);
|
||||
expect(trace.body.input).not.toBeNull();
|
||||
expect(trace.body.output).not.toBeNull();
|
||||
expect(trace.body.observations).toHaveLength(1);
|
||||
expect(trace.body.totalCost).toBeGreaterThanOrEqual(0);
|
||||
expect(trace.body.latency).toBeGreaterThanOrEqual(0);
|
||||
});
|
||||
|
||||
it("should fetch all traces", async () => {
|
||||
const timestamp = new Date();
|
||||
const createdTrace = createTrace({
|
||||
|
||||
@@ -0,0 +1,133 @@
|
||||
/** @jest-environment node */
|
||||
|
||||
import {
|
||||
validateConfigAgainstBody,
|
||||
type ScoreConfigDomain,
|
||||
type ScoreDomain,
|
||||
} from "@langfuse/shared/src/server";
|
||||
|
||||
const baseTextConfig: ScoreConfigDomain = {
|
||||
id: "config-1",
|
||||
name: "free-text-config",
|
||||
dataType: "TEXT",
|
||||
isArchived: false,
|
||||
description: null,
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
projectId: "project-1",
|
||||
maxValue: undefined,
|
||||
minValue: undefined,
|
||||
categories: undefined,
|
||||
};
|
||||
|
||||
const baseTextScore: ScoreDomain = {
|
||||
id: "score-1",
|
||||
projectId: "project-1",
|
||||
name: "free-text-config",
|
||||
value: 0,
|
||||
stringValue: "Some valid free text",
|
||||
dataType: "TEXT",
|
||||
source: "ANNOTATION",
|
||||
comment: null,
|
||||
metadata: {},
|
||||
configId: "config-1",
|
||||
queueId: null,
|
||||
executionTraceId: null,
|
||||
authorUserId: null,
|
||||
environment: "default",
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
timestamp: new Date(),
|
||||
traceId: "trace-1",
|
||||
sessionId: null,
|
||||
observationId: null,
|
||||
datasetRunId: null,
|
||||
longStringValue: "",
|
||||
};
|
||||
|
||||
describe("validateConfigAgainstBody for TEXT scores", () => {
|
||||
it("should succeed with valid TEXT annotation score", () => {
|
||||
expect(() =>
|
||||
validateConfigAgainstBody({
|
||||
body: baseTextScore,
|
||||
config: baseTextConfig,
|
||||
context: "ANNOTATION",
|
||||
}),
|
||||
).not.toThrow();
|
||||
});
|
||||
|
||||
it("should reject empty TEXT text", () => {
|
||||
const emptyScore: ScoreDomain = {
|
||||
...baseTextScore,
|
||||
stringValue: "",
|
||||
};
|
||||
|
||||
expect(() =>
|
||||
validateConfigAgainstBody({
|
||||
body: emptyScore,
|
||||
config: baseTextConfig,
|
||||
context: "ANNOTATION",
|
||||
}),
|
||||
).toThrow();
|
||||
});
|
||||
|
||||
it("should reject TEXT text exceeding 500 characters", () => {
|
||||
const longScore: ScoreDomain = {
|
||||
...baseTextScore,
|
||||
stringValue: "a".repeat(501),
|
||||
};
|
||||
|
||||
expect(() =>
|
||||
validateConfigAgainstBody({
|
||||
body: longScore,
|
||||
config: baseTextConfig,
|
||||
context: "ANNOTATION",
|
||||
}),
|
||||
).toThrow();
|
||||
});
|
||||
|
||||
it("should accept TEXT text at exactly 500 characters", () => {
|
||||
const maxLengthScore: ScoreDomain = {
|
||||
...baseTextScore,
|
||||
stringValue: "a".repeat(500),
|
||||
};
|
||||
|
||||
expect(() =>
|
||||
validateConfigAgainstBody({
|
||||
body: maxLengthScore,
|
||||
config: baseTextConfig,
|
||||
context: "ANNOTATION",
|
||||
}),
|
||||
).not.toThrow();
|
||||
});
|
||||
|
||||
it("should reject archived config", () => {
|
||||
const archivedConfig: ScoreConfigDomain = {
|
||||
...baseTextConfig,
|
||||
isArchived: true,
|
||||
};
|
||||
|
||||
expect(() =>
|
||||
validateConfigAgainstBody({
|
||||
body: baseTextScore,
|
||||
config: archivedConfig,
|
||||
context: "ANNOTATION",
|
||||
}),
|
||||
).toThrow("Config is archived");
|
||||
});
|
||||
|
||||
it("should reject name mismatch", () => {
|
||||
const mismatchedScore: ScoreDomain = {
|
||||
...baseTextScore,
|
||||
name: "different-name",
|
||||
};
|
||||
|
||||
expect(() =>
|
||||
validateConfigAgainstBody({
|
||||
body: mismatchedScore,
|
||||
config: baseTextConfig,
|
||||
context: "ANNOTATION",
|
||||
}),
|
||||
).toThrow("Name mismatch");
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,126 @@
|
||||
import { ChatMessageRole, ChatMessageType } from "@langfuse/shared";
|
||||
|
||||
import { createMessageSearchController } from "./controller";
|
||||
|
||||
describe("message search controller", () => {
|
||||
beforeEach(() => {
|
||||
jest.useFakeTimers();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
jest.runOnlyPendingTimers();
|
||||
jest.useRealTimers();
|
||||
});
|
||||
|
||||
const commitQuery = (
|
||||
controller: ReturnType<typeof createMessageSearchController>,
|
||||
query: string,
|
||||
) => {
|
||||
controller.setQueryInput(query);
|
||||
jest.runAllTimers();
|
||||
return controller.getSnapshot().matches;
|
||||
};
|
||||
|
||||
it("finds all occurrences of a query", () => {
|
||||
const controller = createMessageSearchController(["page-1"]);
|
||||
|
||||
controller.registerPageMessages("page-1", [
|
||||
{
|
||||
id: "message-1",
|
||||
type: ChatMessageType.System,
|
||||
role: ChatMessageRole.System,
|
||||
content:
|
||||
"Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. ",
|
||||
},
|
||||
{
|
||||
id: "message-2",
|
||||
type: ChatMessageType.User,
|
||||
role: ChatMessageRole.User,
|
||||
content:
|
||||
"Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. ",
|
||||
},
|
||||
]);
|
||||
|
||||
expect(commitQuery(controller, "Lorem")).toEqual([
|
||||
expect.objectContaining({
|
||||
messageId: "message-1",
|
||||
from: 0,
|
||||
to: 5,
|
||||
}),
|
||||
expect.objectContaining({
|
||||
messageId: "message-2",
|
||||
from: 0,
|
||||
to: 5,
|
||||
}),
|
||||
]);
|
||||
|
||||
expect(commitQuery(controller, "dolor")).toEqual([
|
||||
expect.objectContaining({
|
||||
messageId: "message-1",
|
||||
from: 12,
|
||||
to: 17,
|
||||
}),
|
||||
expect.objectContaining({
|
||||
messageId: "message-1",
|
||||
from: 103,
|
||||
to: 108,
|
||||
}),
|
||||
expect.objectContaining({
|
||||
messageId: "message-2",
|
||||
from: 12,
|
||||
to: 17,
|
||||
}),
|
||||
expect.objectContaining({
|
||||
messageId: "message-2",
|
||||
from: 103,
|
||||
to: 108,
|
||||
}),
|
||||
]);
|
||||
});
|
||||
|
||||
// Regression test for https://github.com/langfuse/langfuse/issues/13002
|
||||
it("matches fullwidth and halfwidth variants consistently", () => {
|
||||
const controller = createMessageSearchController(["page-1"]);
|
||||
|
||||
controller.registerPageMessages("page-1", [
|
||||
{
|
||||
id: "message-1",
|
||||
type: ChatMessageType.System,
|
||||
role: ChatMessageRole.System,
|
||||
content:
|
||||
"Langfuse is an LLM observability platform. Langfuse is also great.",
|
||||
},
|
||||
]);
|
||||
|
||||
expect(commitQuery(controller, "Langfuse")).toEqual([
|
||||
expect.objectContaining({ from: 0, to: 8 }),
|
||||
expect.objectContaining({ from: 43, to: 51 }),
|
||||
]);
|
||||
expect(commitQuery(controller, "Langfuse")).toEqual([
|
||||
expect.objectContaining({ from: 0, to: 8 }),
|
||||
expect.objectContaining({ from: 43, to: 51 }),
|
||||
]);
|
||||
expect(commitQuery(controller, "langfuse")).toEqual([
|
||||
expect.objectContaining({ from: 0, to: 8 }),
|
||||
expect.objectContaining({ from: 43, to: 51 }),
|
||||
]);
|
||||
});
|
||||
|
||||
// Regression test for https://github.com/langfuse/langfuse/issues/13002
|
||||
it("returns original document offsets for compatibility character matches", () => {
|
||||
const controller = createMessageSearchController(["page-1"]);
|
||||
|
||||
controller.registerPageMessages("page-1", [
|
||||
{
|
||||
id: "message-1",
|
||||
type: ChatMessageType.System,
|
||||
role: ChatMessageRole.System,
|
||||
content: "高さ180㌢の棚",
|
||||
},
|
||||
]);
|
||||
|
||||
expect(commitQuery(controller, "センチ")).toEqual([
|
||||
expect.objectContaining({ from: 5, to: 6 }),
|
||||
]);
|
||||
});
|
||||
});
|
||||
@@ -3,11 +3,14 @@
|
||||
import capitalize from "lodash/capitalize";
|
||||
import { type ReactCodeMirrorRef } from "@uiw/react-codemirror";
|
||||
import { ChatMessageType, type ChatMessageWithId } from "@langfuse/shared";
|
||||
import { EditorState } from "@codemirror/state";
|
||||
import { SearchQuery } from "@codemirror/search";
|
||||
import { type RefObject } from "react";
|
||||
|
||||
import {
|
||||
applyCodeMirrorSearchQuery,
|
||||
selectCodeMirrorRange,
|
||||
unsetActiveSearchMarkCodeMirrorRange,
|
||||
setActiveSearchMarkCodeMirrorRange,
|
||||
} from "@/src/components/editor";
|
||||
|
||||
export type MessageSearchMatch = {
|
||||
@@ -136,7 +139,11 @@ function buildMatches(state: MessageSearchState) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const lowerQuery = searchQuery.toLocaleLowerCase();
|
||||
const codeMirrorSearchQuery = new SearchQuery({
|
||||
search: searchQuery,
|
||||
caseSensitive: false,
|
||||
literal: true,
|
||||
});
|
||||
const allMatches: MessageSearchMatch[] = [];
|
||||
|
||||
for (const [pageIndex, pageId] of state.pageIds.entries()) {
|
||||
@@ -151,10 +158,13 @@ function buildMatches(state: MessageSearchState) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const lowerText = text.toLocaleLowerCase();
|
||||
let from = lowerText.indexOf(lowerQuery);
|
||||
const cursor = codeMirrorSearchQuery.getCursor(
|
||||
EditorState.create({ doc: text }),
|
||||
);
|
||||
let match = cursor.next();
|
||||
|
||||
while (from !== -1) {
|
||||
while (!match.done) {
|
||||
const { from, to } = match.value;
|
||||
const label = getMessageSearchLabel(message, messageIndex);
|
||||
const pageLabel = state.getPageLabel?.(pageId, pageIndex);
|
||||
const matchWithoutKey = {
|
||||
@@ -163,7 +173,7 @@ function buildMatches(state: MessageSearchState) {
|
||||
label,
|
||||
locationLabel: pageLabel ? `${pageLabel} · ${label}` : label,
|
||||
from,
|
||||
to: from + searchQuery.length,
|
||||
to,
|
||||
text,
|
||||
};
|
||||
|
||||
@@ -172,10 +182,7 @@ function buildMatches(state: MessageSearchState) {
|
||||
...matchWithoutKey,
|
||||
});
|
||||
|
||||
from = lowerText.indexOf(
|
||||
lowerQuery,
|
||||
from + Math.max(1, lowerQuery.length),
|
||||
);
|
||||
match = cursor.next();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -269,17 +276,33 @@ export function createMessageSearchController(
|
||||
inline: "center",
|
||||
});
|
||||
|
||||
const messageTarget = messageTargets.get(
|
||||
getMessageTargetKey(activeMatch.pageId, activeMatch.messageId),
|
||||
let activeMessageTarget: MessageSearchMessageTarget | null = null;
|
||||
const inactiveMessageTargets: MessageSearchMessageTarget[] = [];
|
||||
|
||||
const activeMessageTargetKey = getMessageTargetKey(
|
||||
activeMatch.pageId,
|
||||
activeMatch.messageId,
|
||||
);
|
||||
|
||||
messageTarget?.rowRef.current?.scrollIntoView({
|
||||
for (const [key, target] of messageTargets.entries()) {
|
||||
if (key === activeMessageTargetKey) {
|
||||
activeMessageTarget = target;
|
||||
} else {
|
||||
inactiveMessageTargets.push(target);
|
||||
}
|
||||
}
|
||||
|
||||
for (const target of inactiveMessageTargets) {
|
||||
unsetActiveSearchMarkCodeMirrorRange(target?.editorRef);
|
||||
}
|
||||
|
||||
activeMessageTarget?.rowRef.current?.scrollIntoView({
|
||||
behavior: "smooth",
|
||||
block: "center",
|
||||
inline: "nearest",
|
||||
});
|
||||
|
||||
selectCodeMirrorRange(messageTarget?.editorRef, {
|
||||
setActiveSearchMarkCodeMirrorRange(activeMessageTarget?.editorRef, {
|
||||
from: activeMatch.from,
|
||||
to: activeMatch.to,
|
||||
});
|
||||
@@ -318,7 +341,7 @@ export function createMessageSearchController(
|
||||
const refreshSearchResults = (shouldSyncEditors: boolean) => {
|
||||
const activeMatchChanged = recomputeMatches();
|
||||
|
||||
if (shouldSyncEditors) {
|
||||
if (shouldSyncEditors || activeMatchChanged) {
|
||||
syncEditorsToQuery();
|
||||
}
|
||||
|
||||
|
||||
@@ -21,7 +21,6 @@ export function MessageSearchToolbar({ className }: { className?: string }) {
|
||||
openRequestCount,
|
||||
queryInput,
|
||||
matches,
|
||||
activeMatch,
|
||||
activeMatchIndex,
|
||||
openSearch,
|
||||
closeSearch,
|
||||
@@ -109,9 +108,6 @@ export function MessageSearchToolbar({ className }: { className?: string }) {
|
||||
onClick={nextMatch}
|
||||
disabled={matches.length === 0}
|
||||
/>
|
||||
<div className="text-muted-foreground hidden max-w-48 truncate px-1 text-xs lg:block">
|
||||
{activeMatch?.locationLabel ?? "No matches"}
|
||||
</div>
|
||||
<IconButton icon={X} label="Close search" onClick={closeSearch} />
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -6,7 +6,7 @@ import CodeMirror, {
|
||||
ViewPlugin,
|
||||
type ViewUpdate,
|
||||
} from "@uiw/react-codemirror";
|
||||
import { RangeSetBuilder } from "@codemirror/state";
|
||||
import { RangeSetBuilder, StateEffect, StateField } from "@codemirror/state";
|
||||
import { SearchQuery, search, setSearchQuery } from "@codemirror/search";
|
||||
import { json, jsonParseLinter } from "@codemirror/lang-json";
|
||||
import { linter, type Diagnostic } from "@codemirror/lint";
|
||||
@@ -165,6 +165,142 @@ const bidiSupport = [
|
||||
),
|
||||
];
|
||||
|
||||
// Add custom search highlight decoration using the same class names as the default search match decorations
|
||||
// See: https://github.com/codemirror/search/blob/36e8f21e070d471fcbe2e2f338ef4e647b492ba8/src/search.ts#L396
|
||||
const searchMatchMark = Decoration.mark({
|
||||
class: "cm-searchMatch",
|
||||
});
|
||||
const selectedSearchMatchMark = Decoration.mark({
|
||||
class: "cm-searchMatch cm-searchMatch-selected",
|
||||
});
|
||||
|
||||
const setSearchHighlightMarks = StateEffect.define<
|
||||
{
|
||||
from: number;
|
||||
to: number;
|
||||
}[]
|
||||
>({
|
||||
map: (ranges, change) =>
|
||||
ranges.map(({ from, to }) => ({
|
||||
from: change.mapPos(from),
|
||||
to: change.mapPos(to),
|
||||
})),
|
||||
});
|
||||
|
||||
const setSelectedSearchHighlightMark = StateEffect.define<{
|
||||
from: number;
|
||||
to: number;
|
||||
}>({
|
||||
map: ({ from, to }, change) => ({
|
||||
from: change.mapPos(from),
|
||||
to: change.mapPos(to),
|
||||
}),
|
||||
});
|
||||
|
||||
const unsetSelectedSearchHighlightMark = StateEffect.define({});
|
||||
|
||||
const searchHighlightingSupport = StateField.define<DecorationSet>({
|
||||
create() {
|
||||
return Decoration.none;
|
||||
},
|
||||
update(decos, tr) {
|
||||
decos = decos.map(tr.changes);
|
||||
for (const effect of tr.effects) {
|
||||
if (effect.is(setSearchHighlightMarks)) {
|
||||
// Remove all existing search highlights
|
||||
decos = decos.update({
|
||||
filter: (from, to, decoration) => {
|
||||
return !decoration.spec.class?.includes("cm-searchMatch");
|
||||
},
|
||||
});
|
||||
|
||||
decos = decos.update({
|
||||
add: effect.value.map(({ from, to }) =>
|
||||
searchMatchMark.range(from, to),
|
||||
),
|
||||
});
|
||||
}
|
||||
|
||||
if (effect.is(unsetSelectedSearchHighlightMark)) {
|
||||
let selectedRange: { from: number; to: number } | null = null;
|
||||
decos = decos.update({
|
||||
filter: (from, to, decoration) => {
|
||||
if (decoration.spec.class?.includes("cm-searchMatch-selected")) {
|
||||
selectedRange = { from, to };
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
},
|
||||
});
|
||||
|
||||
// Reassign the value and cast it because typescript infers it to be always null,
|
||||
// not recognizing it to be assigned in the filter above.
|
||||
selectedRange = selectedRange as {
|
||||
from: number;
|
||||
to: number;
|
||||
} | null;
|
||||
|
||||
if (selectedRange) {
|
||||
decos = decos.update({
|
||||
add: [searchMatchMark.range(selectedRange.from, selectedRange.to)],
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (effect.is(setSelectedSearchHighlightMark)) {
|
||||
// Remove normal search match mark from the selected range,
|
||||
// otherwise there will _both_ a normal and selected highlight on the active match.
|
||||
decos = decos.update({
|
||||
filter: (from, to, decoration) => {
|
||||
if (from === effect.value.from && to === effect.value.to) {
|
||||
return !decoration.spec.class?.includes("cm-searchMatch");
|
||||
}
|
||||
|
||||
return true;
|
||||
},
|
||||
});
|
||||
|
||||
let previousSelectedRange: { from: number; to: number } | null = null;
|
||||
|
||||
// Make the existing selected search highlight a normal search highlight
|
||||
decos = decos.update({
|
||||
filter: (from, to, decoration) => {
|
||||
if (decoration.spec.class?.includes("cm-searchMatch-selected")) {
|
||||
previousSelectedRange = { from, to };
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
},
|
||||
});
|
||||
|
||||
// Reassign the value and cast it because typescript infers it to be always null,
|
||||
// not recognizing it to be assigned in the filter above.
|
||||
previousSelectedRange = previousSelectedRange as {
|
||||
from: number;
|
||||
to: number;
|
||||
} | null;
|
||||
|
||||
decos = decos.update({
|
||||
add: [
|
||||
...(previousSelectedRange
|
||||
? [
|
||||
searchMatchMark.range(
|
||||
previousSelectedRange.from,
|
||||
previousSelectedRange.to,
|
||||
),
|
||||
]
|
||||
: []),
|
||||
selectedSearchMatchMark.range(effect.value.from, effect.value.to),
|
||||
].toSorted((a, b) => a.from - b.from),
|
||||
});
|
||||
}
|
||||
}
|
||||
return decos;
|
||||
},
|
||||
provide: (f) => EditorView.decorations.from(f),
|
||||
});
|
||||
|
||||
export function applyCodeMirrorSearchQuery(
|
||||
editorRef: RefObject<ReactCodeMirrorRef | null> | undefined,
|
||||
searchValue: string,
|
||||
@@ -174,18 +310,30 @@ export function applyCodeMirrorSearchQuery(
|
||||
return;
|
||||
}
|
||||
|
||||
const searchQuery = new SearchQuery({
|
||||
search: searchValue,
|
||||
caseSensitive: false,
|
||||
literal: true,
|
||||
});
|
||||
|
||||
view.dispatch({
|
||||
effects: setSearchQuery.of(
|
||||
new SearchQuery({
|
||||
search: searchValue,
|
||||
caseSensitive: false,
|
||||
literal: true,
|
||||
}),
|
||||
),
|
||||
effects: setSearchQuery.of(searchQuery),
|
||||
});
|
||||
|
||||
const cursor = searchQuery.getCursor(view.state);
|
||||
const matchRanges: { from: number; to: number }[] = [];
|
||||
let current = cursor.next();
|
||||
while (!current.done) {
|
||||
matchRanges.push(current.value);
|
||||
current = cursor.next();
|
||||
}
|
||||
|
||||
view.dispatch({
|
||||
effects: setSearchHighlightMarks.of(matchRanges),
|
||||
});
|
||||
}
|
||||
|
||||
export function selectCodeMirrorRange(
|
||||
export function setActiveSearchMarkCodeMirrorRange(
|
||||
editorRef: RefObject<ReactCodeMirrorRef | null> | undefined,
|
||||
range: { from: number; to: number } | null,
|
||||
) {
|
||||
@@ -195,11 +343,23 @@ export function selectCodeMirrorRange(
|
||||
}
|
||||
|
||||
view.dispatch({
|
||||
selection: {
|
||||
anchor: range.from,
|
||||
head: range.to,
|
||||
},
|
||||
scrollIntoView: true,
|
||||
effects: [
|
||||
setSelectedSearchHighlightMark.of(range),
|
||||
EditorView.scrollIntoView(range.from),
|
||||
],
|
||||
});
|
||||
}
|
||||
|
||||
export function unsetActiveSearchMarkCodeMirrorRange(
|
||||
editorRef: RefObject<ReactCodeMirrorRef | null> | undefined,
|
||||
) {
|
||||
const view = editorRef?.current?.view;
|
||||
if (!view) {
|
||||
return;
|
||||
}
|
||||
|
||||
view.dispatch({
|
||||
effects: unsetSelectedSearchHighlightMark.of(null),
|
||||
});
|
||||
}
|
||||
|
||||
@@ -268,6 +428,7 @@ export function CodeMirrorEditor({
|
||||
}}
|
||||
lang={mode === "json" ? "json" : undefined}
|
||||
extensions={[
|
||||
searchHighlightingSupport,
|
||||
search(),
|
||||
// RTL/bidi support - must be early for proper line decoration
|
||||
...bidiSupport,
|
||||
@@ -277,6 +438,16 @@ export function CodeMirrorEditor({
|
||||
outline: "none",
|
||||
},
|
||||
}),
|
||||
// Update search match highlight styles
|
||||
EditorView.theme({
|
||||
".cm-searchMatch.cm-searchMatch": {
|
||||
backgroundColor: "hsl(var(--find-match-background))",
|
||||
},
|
||||
".cm-searchMatch.cm-searchMatch-selected": {
|
||||
backgroundColor: "hsl(var(--find-match-selected-background))",
|
||||
color: "hsl(var(--find-match-selected-foreground))",
|
||||
},
|
||||
}),
|
||||
// Hide gutter when lineNumbers is false
|
||||
// Fix missing gutter border
|
||||
...(!lineNumbers
|
||||
|
||||
@@ -65,24 +65,26 @@ const ScoreGroupBadge = <
|
||||
<Badge
|
||||
variant="tertiary"
|
||||
key={name}
|
||||
className={`flex items-center gap-1 ${compact ? "px-1.5 leading-tight" : "px-2.5"} text-xs font-normal${badgeClassName ? " " + badgeClassName : ""}`}
|
||||
className={`flex max-w-full min-w-0 items-center gap-1 ${compact ? "px-1.5 leading-tight" : "px-2.5"} text-xs font-normal${badgeClassName ? " " + badgeClassName : ""}`}
|
||||
>
|
||||
<div
|
||||
className={`w-fit max-w-20 truncate ${compact ? "leading-tight" : ""}`}
|
||||
className={`w-fit max-w-20 shrink-0 truncate ${compact ? "leading-tight" : ""}`}
|
||||
title={name}
|
||||
>
|
||||
{name}:
|
||||
</div>
|
||||
<div className="flex items-center gap-1 text-nowrap">
|
||||
<div className="flex min-w-0 items-center gap-1 text-nowrap">
|
||||
{scores.map((s, i) => (
|
||||
<span
|
||||
key={i}
|
||||
className="group/score ml-1 flex items-center gap-1 rounded-sm first:ml-0"
|
||||
className="group/score ml-1 flex min-w-0 items-center gap-1 rounded-sm first:ml-0"
|
||||
>
|
||||
{s.stringValue ?? s.value?.toFixed(2) ?? ""}
|
||||
<span className="truncate">
|
||||
{s.stringValue ?? s.value?.toFixed(2) ?? ""}
|
||||
</span>
|
||||
{s.comment && (
|
||||
<HoverCard>
|
||||
<HoverCardTrigger className="inline-block">
|
||||
<HoverCardTrigger className="inline-block shrink-0">
|
||||
<MessageCircleMoreIcon className="mb-0.25 size-3!" />
|
||||
</HoverCardTrigger>
|
||||
<HoverCardContent className="max-h-[50dvh] overflow-y-auto text-xs break-normal whitespace-normal">
|
||||
@@ -104,7 +106,7 @@ const ScoreGroupBadge = <
|
||||
)}
|
||||
{hasMetadata(s) && (
|
||||
<HoverCard>
|
||||
<HoverCardTrigger className="inline-block">
|
||||
<HoverCardTrigger className="inline-block shrink-0">
|
||||
<BracesIcon className="mb-0.25 size-3!" />
|
||||
</HoverCardTrigger>
|
||||
<HoverCardContent className="max-h-[50dvh] overflow-y-auto rounded-md border-none p-0 text-xs break-normal whitespace-normal">
|
||||
|
||||
@@ -11,6 +11,7 @@ interface ResizableDesktopLayoutProps {
|
||||
mainContent: ReactNode;
|
||||
sidebarContent: ReactNode;
|
||||
open: boolean;
|
||||
showHandle?: boolean;
|
||||
defaultMainSize?: number;
|
||||
defaultSidebarSize?: number;
|
||||
minMainSize?: number;
|
||||
@@ -36,6 +37,7 @@ export function ResizableDesktopLayout({
|
||||
mainContent,
|
||||
sidebarContent,
|
||||
open,
|
||||
showHandle = true,
|
||||
defaultMainSize = 70,
|
||||
defaultSidebarSize = 30,
|
||||
minMainSize = 30,
|
||||
@@ -108,7 +110,9 @@ export function ResizableDesktopLayout({
|
||||
{sidebarContent}
|
||||
</ResizablePanel>
|
||||
)}
|
||||
{sidebarPosition === "left" && open && <ResizableHandle withHandle />}
|
||||
{sidebarPosition === "left" && open && showHandle && (
|
||||
<ResizableHandle withHandle />
|
||||
)}
|
||||
<ResizablePanel
|
||||
id={MAIN_PANEL_ID}
|
||||
defaultSize={`${defaultMainSize}%`}
|
||||
@@ -121,7 +125,9 @@ export function ResizableDesktopLayout({
|
||||
{mainContent}
|
||||
</div>
|
||||
</ResizablePanel>
|
||||
{sidebarPosition === "right" && open && <ResizableHandle withHandle />}
|
||||
{sidebarPosition === "right" && open && showHandle && (
|
||||
<ResizableHandle withHandle />
|
||||
)}
|
||||
{sidebarPosition === "right" && (
|
||||
<ResizablePanel
|
||||
id={SIDEBAR_PANEL_ID}
|
||||
|
||||
@@ -59,6 +59,7 @@ export function ResizableContent({ children }: PropsWithChildren) {
|
||||
mainContent={children}
|
||||
sidebarContent={<SupportDrawer />}
|
||||
open={open}
|
||||
showHandle={false}
|
||||
defaultMainSize={70}
|
||||
defaultSidebarSize={30}
|
||||
minMainSize={30}
|
||||
|
||||
@@ -74,12 +74,15 @@ export const ScoresTableCell = ({
|
||||
|
||||
return (
|
||||
<span
|
||||
className={cn("flex flex-row gap-0.5 rounded-sm", COLOR_MAP.get(value))}
|
||||
className={cn(
|
||||
"flex min-w-0 flex-row gap-0.5 rounded-sm",
|
||||
COLOR_MAP.get(value),
|
||||
)}
|
||||
>
|
||||
{value}
|
||||
<span className="truncate">{value}</span>
|
||||
{aggregate.comment && (
|
||||
<HoverCard>
|
||||
<HoverCardTrigger className="inline-block cursor-pointer">
|
||||
<HoverCardTrigger className="inline-block shrink-0 cursor-pointer">
|
||||
<MessageCircleMore size={12} />
|
||||
</HoverCardTrigger>
|
||||
<HoverCardContent className="flex flex-col p-0 text-xs break-normal whitespace-normal">
|
||||
|
||||
@@ -29,6 +29,7 @@ import {
|
||||
isPresent,
|
||||
type FilterState,
|
||||
type ScoreDataTypeType,
|
||||
LISTABLE_SCORE_TYPES,
|
||||
BatchExportTableName,
|
||||
BatchActionType,
|
||||
TableViewPresetTableName,
|
||||
@@ -263,7 +264,7 @@ export default function ScoresTable({
|
||||
count: n.count !== undefined ? Number(n.count) : undefined,
|
||||
})) ?? undefined,
|
||||
source: ["ANNOTATION", "API", "EVAL"],
|
||||
dataType: ["NUMERIC", "CATEGORICAL", "BOOLEAN"],
|
||||
dataType: [...LISTABLE_SCORE_TYPES],
|
||||
value: [],
|
||||
stringValue:
|
||||
filterOptions.data?.stringValue?.map((sv) => ({
|
||||
|
||||
+2
-1
@@ -129,6 +129,7 @@ export function ObservationDetailView({
|
||||
setJsonViewPreference,
|
||||
jsonBetaEnabled,
|
||||
setJsonBetaEnabled,
|
||||
isPeekMode,
|
||||
} = useViewPreferences();
|
||||
|
||||
// Map jsonViewPreference to currentView format expected by child components
|
||||
@@ -480,7 +481,7 @@ export function ObservationDetailView({
|
||||
"userId",
|
||||
]}
|
||||
localStorageSuffix="ObservationPreview"
|
||||
disableUrlPersistence
|
||||
disableUrlPersistence={isPeekMode}
|
||||
/>
|
||||
</div>
|
||||
</TabsBarContent>
|
||||
|
||||
@@ -93,6 +93,7 @@ export function TraceDetailView({
|
||||
setJsonViewPreference,
|
||||
jsonBetaEnabled,
|
||||
setJsonBetaEnabled,
|
||||
isPeekMode,
|
||||
} = useViewPreferences();
|
||||
|
||||
// Map jsonViewPreference to currentView format expected by child components
|
||||
@@ -421,7 +422,7 @@ export function TraceDetailView({
|
||||
traceId={trace.id}
|
||||
hiddenColumns={["traceName", "jobConfigurationId", "userId"]}
|
||||
localStorageSuffix="TracePreview"
|
||||
disableUrlPersistence
|
||||
disableUrlPersistence={isPeekMode}
|
||||
/>
|
||||
</div>
|
||||
</TabsBarContent>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useMemo, useState } from "react";
|
||||
import { useMemo, useRef, useState } from "react";
|
||||
import { Button } from "@/src/components/ui/button";
|
||||
import {
|
||||
Check,
|
||||
@@ -217,10 +217,18 @@ export function CodeView(props: {
|
||||
defaultCollapsed?: boolean;
|
||||
title?: string;
|
||||
scrollable?: boolean;
|
||||
copiedToClipboardMessage?: string;
|
||||
}) {
|
||||
const { copiedToClipboardMessage } = props;
|
||||
|
||||
const [isCopied, setIsCopied] = useState(false);
|
||||
const [isCollapsed, setCollapsed] = useState(props.defaultCollapsed);
|
||||
|
||||
const copySuccessStateDuration = copiedToClipboardMessage ? 3_000 : 1_000;
|
||||
const copySuccessTimeoutRef = useRef<ReturnType<typeof setTimeout> | null>(
|
||||
null,
|
||||
);
|
||||
|
||||
const handleCopy = (event: React.MouseEvent<HTMLButtonElement>) => {
|
||||
event.preventDefault();
|
||||
setIsCopied(true);
|
||||
@@ -230,7 +238,12 @@ export function CodeView(props: {
|
||||
? props.content
|
||||
: (props.content?.join("\n") ?? ""));
|
||||
void copyTextToClipboard(content);
|
||||
setTimeout(() => setIsCopied(false), 1000);
|
||||
if (copySuccessTimeoutRef.current)
|
||||
clearTimeout(copySuccessTimeoutRef.current);
|
||||
copySuccessTimeoutRef.current = setTimeout(
|
||||
() => setIsCopied(false),
|
||||
copySuccessStateDuration,
|
||||
);
|
||||
|
||||
// Keep focus on the copy button to prevent focus shifting
|
||||
event.currentTarget.focus();
|
||||
@@ -238,6 +251,19 @@ export function CodeView(props: {
|
||||
|
||||
const handleShowAll = () => setCollapsed(!isCollapsed);
|
||||
|
||||
const CopySuccessIcon = useMemo(() => {
|
||||
return (
|
||||
<div className="animate-appear relative h-3">
|
||||
<Check className="h-3 w-3" />
|
||||
{copiedToClipboardMessage && (
|
||||
<div className="text-secondary-foreground absolute top-0 right-0 mr-6 h-full max-w-[60vw] transform truncate overflow-hidden text-right text-sm leading-none whitespace-nowrap">
|
||||
{copiedToClipboardMessage}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}, [copiedToClipboardMessage]);
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
@@ -256,11 +282,7 @@ export function CodeView(props: {
|
||||
onClick={handleCopy}
|
||||
className=""
|
||||
>
|
||||
{isCopied ? (
|
||||
<Check className="h-3 w-3" />
|
||||
) : (
|
||||
<Copy className="h-3 w-3" />
|
||||
)}
|
||||
{isCopied ? CopySuccessIcon : <Copy className="h-3 w-3" />}
|
||||
</Button>
|
||||
</div>
|
||||
) : undefined}
|
||||
@@ -278,11 +300,7 @@ export function CodeView(props: {
|
||||
onClick={handleCopy}
|
||||
className="absolute top-2 right-2 z-10"
|
||||
>
|
||||
{isCopied ? (
|
||||
<Check className="h-3 w-3" />
|
||||
) : (
|
||||
<Copy className="h-3 w-3" />
|
||||
)}
|
||||
{isCopied ? CopySuccessIcon : <Copy className="h-3 w-3" />}
|
||||
</Button>
|
||||
)}
|
||||
<code
|
||||
|
||||
@@ -1 +1 @@
|
||||
export const VERSION = "v3.164.0";
|
||||
export const VERSION = "v3.167.0";
|
||||
|
||||
@@ -399,6 +399,7 @@ export const env = createEnv({
|
||||
.enum(["true", "false"])
|
||||
.default("false"),
|
||||
LANGFUSE_API_TRACES_DEFAULT_FIELDS: z.string().optional(),
|
||||
LANGFUSE_API_TRACEBYID_DEFAULT_FIELDS: z.string().optional(),
|
||||
|
||||
// Events table migration
|
||||
LANGFUSE_ENABLE_EVENTS_TABLE_OBSERVATIONS: z
|
||||
@@ -781,6 +782,8 @@ export const env = createEnv({
|
||||
process.env.LANGFUSE_API_TRACES_REJECT_NO_DATE_RANGE,
|
||||
LANGFUSE_API_TRACES_DEFAULT_FIELDS:
|
||||
process.env.LANGFUSE_API_TRACES_DEFAULT_FIELDS,
|
||||
LANGFUSE_API_TRACEBYID_DEFAULT_FIELDS:
|
||||
process.env.LANGFUSE_API_TRACEBYID_DEFAULT_FIELDS,
|
||||
// Events table migration
|
||||
LANGFUSE_ENABLE_EVENTS_TABLE_OBSERVATIONS:
|
||||
process.env.LANGFUSE_ENABLE_EVENTS_TABLE_OBSERVATIONS,
|
||||
|
||||
@@ -16,6 +16,8 @@ import { useAnnotationObjectData } from "./shared/hooks/useAnnotationObjectData"
|
||||
import { TraceAnnotationProcessor } from "./processors/TraceAnnotationProcessor";
|
||||
import { SessionAnnotationProcessor } from "./processors/SessionAnnotationProcessor";
|
||||
import { ObjectNotFoundCard } from "@/src/components/ui/object-not-found-card";
|
||||
import { useV4Beta } from "@/src/features/events/hooks/useV4Beta";
|
||||
import { useSession } from "next-auth/react";
|
||||
|
||||
export const AnnotationQueueItemPage: React.FC<{
|
||||
annotationQueueId: string;
|
||||
@@ -24,6 +26,9 @@ export const AnnotationQueueItemPage: React.FC<{
|
||||
queryItemId?: string;
|
||||
}> = ({ annotationQueueId, projectId, view, queryItemId }) => {
|
||||
const router = useRouter();
|
||||
const { status: sessionStatus } = useSession();
|
||||
const sessionLoaded = sessionStatus !== "loading";
|
||||
const { isBetaEnabled } = useV4Beta();
|
||||
const isSingleItem = router.query.singleItem === "true";
|
||||
const [nextItemData, setNextItemData] = useState<
|
||||
RouterOutput["annotationQueues"]["fetchAndLockNext"] | null
|
||||
@@ -39,8 +44,8 @@ export const AnnotationQueueItemPage: React.FC<{
|
||||
const itemId = isSingleItem ? queryItemId : seenItemIds[progressIndex];
|
||||
|
||||
const seenItemData = api.annotationQueueItems.byId.useQuery(
|
||||
{ projectId, itemId: itemId as string },
|
||||
{ enabled: !!itemId, refetchOnMount: false },
|
||||
{ projectId, itemId: itemId as string, isBetaEnabled },
|
||||
{ enabled: !!itemId && sessionLoaded, refetchOnMount: false },
|
||||
);
|
||||
|
||||
const fetchAndLockNextMutation =
|
||||
@@ -49,18 +54,19 @@ export const AnnotationQueueItemPage: React.FC<{
|
||||
// Effects
|
||||
useEffect(() => {
|
||||
async function fetchNextItem() {
|
||||
if (!itemId && !isSingleItem) {
|
||||
if (!itemId && !isSingleItem && sessionLoaded) {
|
||||
const nextItem = await fetchAndLockNextMutation.mutateAsync({
|
||||
queueId: annotationQueueId,
|
||||
projectId,
|
||||
seenItemIds,
|
||||
isBetaEnabled,
|
||||
});
|
||||
setNextItemData(nextItem);
|
||||
}
|
||||
}
|
||||
fetchNextItem();
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
}, [sessionLoaded]);
|
||||
const { configs } = useAnnotationQueueData({ annotationQueueId, projectId });
|
||||
|
||||
const unseenPendingItemCount =
|
||||
@@ -86,6 +92,7 @@ export const AnnotationQueueItemPage: React.FC<{
|
||||
queueId: annotationQueueId,
|
||||
projectId,
|
||||
seenItemIds,
|
||||
isBetaEnabled,
|
||||
});
|
||||
setNextItemData(nextItem);
|
||||
}
|
||||
@@ -142,7 +149,8 @@ export const AnnotationQueueItemPage: React.FC<{
|
||||
(seenItemData.isPending && itemId) ||
|
||||
(fetchAndLockNextMutation.isPending && !itemId) ||
|
||||
unseenPendingItemCount.isPending ||
|
||||
objectData.isLoading
|
||||
objectData.isLoading ||
|
||||
(!sessionLoaded && !isSingleItem)
|
||||
) {
|
||||
return <Skeleton className="h-full w-full" />;
|
||||
}
|
||||
@@ -163,6 +171,7 @@ export const AnnotationQueueItemPage: React.FC<{
|
||||
queueId: annotationQueueId,
|
||||
projectId,
|
||||
seenItemIds,
|
||||
isBetaEnabled,
|
||||
});
|
||||
setNextItemData(nextItem);
|
||||
}
|
||||
|
||||
+56
-5
@@ -3,6 +3,8 @@ import {
|
||||
type AnnotationQueueItem,
|
||||
AnnotationQueueObjectType,
|
||||
} from "@langfuse/shared";
|
||||
import { useV4Beta } from "@/src/features/events/hooks/useV4Beta";
|
||||
import { useEventsTraceData } from "@/src/features/events/hooks/useEventsTraceData";
|
||||
|
||||
export interface ObjectDataHook<TData> {
|
||||
data: TData | undefined;
|
||||
@@ -15,15 +17,19 @@ export const useAnnotationObjectData = (
|
||||
item: (AnnotationQueueItem & { parentTraceId?: string | null }) | null,
|
||||
projectId: string,
|
||||
): ObjectDataHook<any> => {
|
||||
const { isBetaEnabled } = useV4Beta();
|
||||
const traceId = item?.parentTraceId ?? item?.objectId;
|
||||
|
||||
const isTraceOrObservation =
|
||||
!!item &&
|
||||
(item.objectType === AnnotationQueueObjectType.TRACE ||
|
||||
item.objectType === AnnotationQueueObjectType.OBSERVATION);
|
||||
|
||||
// Old path: fetch from traces table (beta OFF)
|
||||
const traceQuery = api.traces.byIdWithObservationsAndScores.useQuery(
|
||||
{ traceId: traceId as string, projectId },
|
||||
{
|
||||
enabled:
|
||||
!!item &&
|
||||
(item.objectType === AnnotationQueueObjectType.TRACE ||
|
||||
item.objectType === AnnotationQueueObjectType.OBSERVATION),
|
||||
enabled: isTraceOrObservation && !isBetaEnabled,
|
||||
retry(failureCount, error) {
|
||||
if (
|
||||
error.data?.code === "UNAUTHORIZED" ||
|
||||
@@ -35,13 +41,42 @@ export const useAnnotationObjectData = (
|
||||
},
|
||||
);
|
||||
|
||||
const eventsData = useEventsTraceData({
|
||||
projectId,
|
||||
traceId: traceId ?? "",
|
||||
enabled: isTraceOrObservation && isBetaEnabled,
|
||||
});
|
||||
|
||||
const isSession =
|
||||
!!item && item.objectType === AnnotationQueueObjectType.SESSION;
|
||||
|
||||
// Old path: fetch from traces table (beta OFF)
|
||||
const sessionQuery = api.sessions.byIdWithScores.useQuery(
|
||||
{
|
||||
sessionId: item?.objectId as string,
|
||||
projectId,
|
||||
},
|
||||
{
|
||||
enabled: !!item && item.objectType === AnnotationQueueObjectType.SESSION,
|
||||
enabled: isSession && !isBetaEnabled,
|
||||
retry(failureCount, error) {
|
||||
if (
|
||||
error.data?.code === "UNAUTHORIZED" ||
|
||||
error.data?.code === "NOT_FOUND"
|
||||
)
|
||||
return false;
|
||||
return failureCount < 3;
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
// New path: fetch from events table (beta ON)
|
||||
const sessionEventsQuery = api.sessions.byIdWithScoresFromEvents.useQuery(
|
||||
{
|
||||
sessionId: item?.objectId as string,
|
||||
projectId,
|
||||
},
|
||||
{
|
||||
enabled: isSession && isBetaEnabled,
|
||||
retry(failureCount, error) {
|
||||
if (
|
||||
error.data?.code === "UNAUTHORIZED" ||
|
||||
@@ -64,6 +99,14 @@ export const useAnnotationObjectData = (
|
||||
switch (item.objectType) {
|
||||
case AnnotationQueueObjectType.TRACE:
|
||||
case AnnotationQueueObjectType.OBSERVATION:
|
||||
if (isBetaEnabled) {
|
||||
return {
|
||||
data: eventsData.data,
|
||||
isLoading: eventsData.isLoading,
|
||||
isError: !!eventsData.error,
|
||||
errorCode: (eventsData.error as any)?.data?.code,
|
||||
};
|
||||
}
|
||||
return {
|
||||
data: traceQuery.data,
|
||||
isLoading: traceQuery.isLoading,
|
||||
@@ -71,6 +114,14 @@ export const useAnnotationObjectData = (
|
||||
errorCode: traceQuery.error?.data?.code,
|
||||
};
|
||||
case AnnotationQueueObjectType.SESSION:
|
||||
if (isBetaEnabled) {
|
||||
return {
|
||||
data: sessionEventsQuery.data,
|
||||
isLoading: sessionEventsQuery.isLoading,
|
||||
isError: sessionEventsQuery.isError,
|
||||
errorCode: sessionEventsQuery.error?.data?.code,
|
||||
};
|
||||
}
|
||||
return {
|
||||
data: sessionQuery.data,
|
||||
isLoading: sessionQuery.isLoading,
|
||||
|
||||
@@ -20,6 +20,7 @@ import {
|
||||
} from "@langfuse/shared";
|
||||
import {
|
||||
getObservationById,
|
||||
getObservationByIdFromEventsTable,
|
||||
getTraceIdsForObservations,
|
||||
logger,
|
||||
} from "@langfuse/shared/src/server";
|
||||
@@ -85,6 +86,7 @@ export const queueItemRouter = createTRPCRouter({
|
||||
z.object({
|
||||
projectId: z.string(),
|
||||
itemId: z.string(),
|
||||
isBetaEnabled: z.boolean().optional().default(false),
|
||||
}),
|
||||
)
|
||||
.query(async ({ input, ctx }) => {
|
||||
@@ -122,10 +124,15 @@ export const queueItemRouter = createTRPCRouter({
|
||||
};
|
||||
|
||||
if (item.objectType === AnnotationQueueObjectType.OBSERVATION) {
|
||||
const clickhouseObservation = await getObservationById({
|
||||
id: item.objectId,
|
||||
projectId: input.projectId,
|
||||
});
|
||||
const clickhouseObservation = input.isBetaEnabled
|
||||
? await getObservationByIdFromEventsTable({
|
||||
id: item.objectId,
|
||||
projectId: input.projectId,
|
||||
})
|
||||
: await getObservationById({
|
||||
id: item.objectId,
|
||||
projectId: input.projectId,
|
||||
});
|
||||
|
||||
if (!clickhouseObservation) {
|
||||
throw new LangfuseNotFoundError("Observation not found");
|
||||
|
||||
@@ -13,7 +13,11 @@ import {
|
||||
optionalPaginationZod,
|
||||
Prisma,
|
||||
} from "@langfuse/shared";
|
||||
import { getObservationById, logger } from "@langfuse/shared/src/server";
|
||||
import {
|
||||
getObservationById,
|
||||
getObservationByIdFromEventsTable,
|
||||
logger,
|
||||
} from "@langfuse/shared/src/server";
|
||||
import { TRPCError } from "@trpc/server";
|
||||
import { z } from "zod";
|
||||
|
||||
@@ -464,78 +468,73 @@ export const queueRouter = createTRPCRouter({
|
||||
queueId: z.string(),
|
||||
projectId: z.string(),
|
||||
seenItemIds: z.array(z.string()),
|
||||
isBetaEnabled: z.boolean().optional().default(false),
|
||||
}),
|
||||
)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
try {
|
||||
throwIfNoProjectAccess({
|
||||
session: ctx.session,
|
||||
throwIfNoProjectAccess({
|
||||
session: ctx.session,
|
||||
projectId: input.projectId,
|
||||
scope: "annotationQueues:CUD",
|
||||
});
|
||||
|
||||
const now = new Date();
|
||||
const fiveMinutesAgo = new Date(now.getTime() - 5 * 60 * 1000);
|
||||
|
||||
const item = await ctx.prisma.annotationQueueItem.findFirst({
|
||||
where: {
|
||||
queueId: input.queueId,
|
||||
projectId: input.projectId,
|
||||
scope: "annotationQueues:CUD",
|
||||
});
|
||||
|
||||
const now = new Date();
|
||||
const fiveMinutesAgo = new Date(now.getTime() - 5 * 60 * 1000);
|
||||
|
||||
const item = await ctx.prisma.annotationQueueItem.findFirst({
|
||||
where: {
|
||||
queueId: input.queueId,
|
||||
projectId: input.projectId,
|
||||
status: AnnotationQueueStatus.PENDING,
|
||||
OR: [
|
||||
{ lockedAt: null },
|
||||
{ lockedAt: { lt: fiveMinutesAgo } },
|
||||
{ lockedByUserId: ctx.session.user.id },
|
||||
],
|
||||
NOT: {
|
||||
id: { in: input.seenItemIds },
|
||||
},
|
||||
status: AnnotationQueueStatus.PENDING,
|
||||
OR: [
|
||||
{ lockedAt: null },
|
||||
{ lockedAt: { lt: fiveMinutesAgo } },
|
||||
{ lockedByUserId: ctx.session.user.id },
|
||||
],
|
||||
NOT: {
|
||||
id: { in: input.seenItemIds },
|
||||
},
|
||||
orderBy: {
|
||||
createdAt: "asc",
|
||||
},
|
||||
});
|
||||
},
|
||||
orderBy: {
|
||||
createdAt: "asc",
|
||||
},
|
||||
});
|
||||
|
||||
// Expected behavior, non-error case: all items have been seen AND/OR completed, no more unseen pending items
|
||||
if (!item) return null;
|
||||
// Expected behavior, non-error case: all items have been seen AND/OR completed, no more unseen pending items
|
||||
if (!item) return null;
|
||||
|
||||
const updatedItem = await ctx.prisma.annotationQueueItem.update({
|
||||
where: {
|
||||
id: item.id,
|
||||
projectId: input.projectId,
|
||||
},
|
||||
data: {
|
||||
lockedAt: now,
|
||||
lockedByUserId: ctx.session.user.id,
|
||||
},
|
||||
});
|
||||
const updatedItem = await ctx.prisma.annotationQueueItem.update({
|
||||
where: {
|
||||
id: item.id,
|
||||
projectId: input.projectId,
|
||||
},
|
||||
data: {
|
||||
lockedAt: now,
|
||||
lockedByUserId: ctx.session.user.id,
|
||||
},
|
||||
});
|
||||
|
||||
const inflatedUpdatedItem = {
|
||||
...updatedItem,
|
||||
lockedByUser: { name: ctx.session.user.name },
|
||||
const inflatedUpdatedItem = {
|
||||
...updatedItem,
|
||||
lockedByUser: { name: ctx.session.user.name },
|
||||
};
|
||||
|
||||
if (item.objectType === AnnotationQueueObjectType.OBSERVATION) {
|
||||
const clickhouseObservation = input.isBetaEnabled
|
||||
? await getObservationByIdFromEventsTable({
|
||||
id: item.objectId,
|
||||
projectId: input.projectId,
|
||||
})
|
||||
: await getObservationById({
|
||||
id: item.objectId,
|
||||
projectId: input.projectId,
|
||||
});
|
||||
return {
|
||||
...inflatedUpdatedItem,
|
||||
parentTraceId: clickhouseObservation?.traceId,
|
||||
};
|
||||
|
||||
if (item.objectType === AnnotationQueueObjectType.OBSERVATION) {
|
||||
const clickhouseObservation = await getObservationById({
|
||||
id: item.objectId,
|
||||
projectId: input.projectId,
|
||||
});
|
||||
return {
|
||||
...inflatedUpdatedItem,
|
||||
parentTraceId: clickhouseObservation?.traceId,
|
||||
};
|
||||
}
|
||||
|
||||
return inflatedUpdatedItem;
|
||||
} catch (error) {
|
||||
logger.error(error);
|
||||
if (error instanceof TRPCError) {
|
||||
throw error;
|
||||
}
|
||||
throw new TRPCError({
|
||||
code: "INTERNAL_SERVER_ERROR",
|
||||
message: "Fetching and locking next annotation queue item failed.",
|
||||
});
|
||||
}
|
||||
|
||||
return inflatedUpdatedItem;
|
||||
}),
|
||||
});
|
||||
|
||||
@@ -24,6 +24,7 @@ import { useIsAuthenticatedAndProjectMember } from "@/src/features/auth/hooks";
|
||||
import { parseJsonPrioritised } from "@langfuse/shared";
|
||||
import { ActionButton } from "@/src/components/ActionButton";
|
||||
import { type MetadataDomainClient } from "@/src/utils/clientSideDomainTypes";
|
||||
import { type Prisma } from "@langfuse/shared";
|
||||
|
||||
/**
|
||||
* Component for creating a new dataset item from an existing object.
|
||||
@@ -39,22 +40,30 @@ export const NewDatasetItemFromExistingObject = (props: {
|
||||
traceId?: string;
|
||||
observationId?: string;
|
||||
fromDatasetId?: string;
|
||||
input: string | null;
|
||||
output: string | null;
|
||||
input: Prisma.JsonValue | null;
|
||||
output: Prisma.JsonValue | null;
|
||||
metadata: MetadataDomainClient;
|
||||
isCopyItem?: boolean;
|
||||
buttonVariant?: ButtonProps["variant"];
|
||||
size?: ButtonProps["size"];
|
||||
}) => {
|
||||
const parsedInput =
|
||||
props.input && typeof props.input === "string"
|
||||
? (parseJsonPrioritised(props.input) ?? null)
|
||||
: null;
|
||||
const normalizePrefillValue = (
|
||||
value: Prisma.JsonValue | null,
|
||||
): Prisma.JsonValue | null => {
|
||||
if (value === null || value === undefined) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const parsedOutput =
|
||||
props.output && typeof props.output === "string"
|
||||
? (parseJsonPrioritised(props.output) ?? null)
|
||||
: null;
|
||||
if (typeof value === "string") {
|
||||
const parsed = parseJsonPrioritised(value);
|
||||
return parsed !== undefined ? parsed : value;
|
||||
}
|
||||
|
||||
return value;
|
||||
};
|
||||
|
||||
const parsedInput = normalizePrefillValue(props.input);
|
||||
const parsedOutput = normalizePrefillValue(props.output);
|
||||
|
||||
const [isFormOpen, setIsFormOpen] = useState(false);
|
||||
const isAuthenticatedAndProjectMember = useIsAuthenticatedAndProjectMember(
|
||||
|
||||
@@ -51,6 +51,7 @@ import { AlertCircle, ExternalLink } from "lucide-react";
|
||||
import { useVariableMappingSync } from "@/src/features/evals/hooks/useVariableMappingSync";
|
||||
import { Button } from "@/src/components/ui/button";
|
||||
import Link from "next/link";
|
||||
import { useRouter } from "next/router";
|
||||
|
||||
export const VariableMappingCard = ({
|
||||
projectId,
|
||||
@@ -74,6 +75,14 @@ export const VariableMappingCard = ({
|
||||
hideAdvancedSettings?: boolean;
|
||||
}) => {
|
||||
const [showPreview, setShowPreview] = useState(false);
|
||||
const [selectedPreviewIds, setSelectedPreviewIds] = useState<{
|
||||
traceId?: string;
|
||||
observationId?: string;
|
||||
}>();
|
||||
const router = useRouter();
|
||||
const peekId =
|
||||
typeof router.query.peek === "string" ? router.query.peek : undefined;
|
||||
const isPeekView = Boolean(peekId);
|
||||
|
||||
const { fields } = useFieldArray({
|
||||
control: form.control,
|
||||
@@ -89,6 +98,7 @@ export const VariableMappingCard = ({
|
||||
projectId,
|
||||
form,
|
||||
disabled,
|
||||
isPeekView ? (selectedPreviewIds ?? {}) : undefined,
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
@@ -98,8 +108,18 @@ export const VariableMappingCard = ({
|
||||
// For dataset and experiment targets, disable preview
|
||||
setShowPreview(false);
|
||||
}
|
||||
|
||||
if (isPeekView) {
|
||||
setSelectedPreviewIds(undefined);
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [form.watch("target"), disabled]);
|
||||
}, [form.watch("target"), disabled, isPeekView]);
|
||||
|
||||
useEffect(() => {
|
||||
if (isPeekView) {
|
||||
setSelectedPreviewIds(undefined);
|
||||
}
|
||||
}, [isPeekView, peekId]);
|
||||
|
||||
const mappingControlButtons = (
|
||||
<div className="flex items-center gap-2">
|
||||
@@ -124,6 +144,23 @@ export const VariableMappingCard = ({
|
||||
? "observations"
|
||||
: "traces"
|
||||
}
|
||||
onNavigate={
|
||||
isPeekView
|
||||
? (entry) => {
|
||||
if (isEventTarget(form.watch("target"))) {
|
||||
setSelectedPreviewIds({
|
||||
traceId: entry.params?.traceId,
|
||||
observationId: entry.id,
|
||||
});
|
||||
} else {
|
||||
setSelectedPreviewIds({
|
||||
traceId: entry.id,
|
||||
observationId: undefined,
|
||||
});
|
||||
}
|
||||
}
|
||||
: undefined
|
||||
}
|
||||
path={(entry) => {
|
||||
const isEvent = isEventTarget(form.watch("target"));
|
||||
const basePath = hideAdvancedSettings
|
||||
@@ -173,7 +210,7 @@ export const VariableMappingCard = ({
|
||||
title="Only user-managed templates can be edited"
|
||||
>
|
||||
Edit prompt
|
||||
<ExternalLink className="h-4 w-4" />
|
||||
<ExternalLink className="ml-1 h-4 w-4" />
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -18,16 +18,24 @@ export function useEvalConfigMappingData(
|
||||
projectId: string,
|
||||
form: UseFormReturn<EvalFormType>,
|
||||
disabled = false,
|
||||
selectedPreviewIds?: {
|
||||
traceId?: string;
|
||||
observationId?: string;
|
||||
},
|
||||
): EvalConfigMappingData {
|
||||
const router = useRouter();
|
||||
|
||||
// Get traceId and observationId from URL query parameters
|
||||
// In full-page mode, keep using URL params. In peek mode, callers can manage preview ids locally.
|
||||
const traceId =
|
||||
typeof router.query.traceId === "string" ? router.query.traceId : undefined;
|
||||
selectedPreviewIds?.traceId ??
|
||||
(typeof router.query.traceId === "string"
|
||||
? router.query.traceId
|
||||
: undefined);
|
||||
const observationId =
|
||||
typeof router.query.observationId === "string"
|
||||
selectedPreviewIds?.observationId ??
|
||||
(typeof router.query.observationId === "string"
|
||||
? router.query.observationId
|
||||
: undefined;
|
||||
: undefined);
|
||||
|
||||
const { previewData, isLoading } = usePreviewData(
|
||||
projectId,
|
||||
@@ -39,9 +47,15 @@ export function useEvalConfigMappingData(
|
||||
|
||||
const targetValue = form.watch("target");
|
||||
const prevTargetRef = useRef(targetValue);
|
||||
const isLocallyManagedPreview = Boolean(selectedPreviewIds);
|
||||
|
||||
// drop the traceId and observation-related params from the URL query parameters when target changes
|
||||
useEffect(() => {
|
||||
if (isLocallyManagedPreview) {
|
||||
prevTargetRef.current = targetValue;
|
||||
return;
|
||||
}
|
||||
|
||||
if (
|
||||
prevTargetRef.current !== targetValue &&
|
||||
prevTargetRef.current !== undefined
|
||||
@@ -60,7 +74,7 @@ export function useEvalConfigMappingData(
|
||||
);
|
||||
}
|
||||
prevTargetRef.current = targetValue;
|
||||
}, [targetValue, router]);
|
||||
}, [targetValue, router, isLocallyManagedPreview]);
|
||||
|
||||
const observationTypeToNames = new Map<ObservationType, Set<string>>([
|
||||
["SPAN", new Set()],
|
||||
|
||||
@@ -277,7 +277,7 @@ export default function ObservationsEventsTable({
|
||||
const [rawRefreshInterval, setRawRefreshInterval] =
|
||||
useSessionStorage<RefreshInterval>(
|
||||
`tableRefreshInterval-events-${projectId}`,
|
||||
60_000,
|
||||
null,
|
||||
);
|
||||
|
||||
// Validate session storage value against allowed intervals
|
||||
|
||||
@@ -6,7 +6,7 @@ import {
|
||||
} from "@/src/features/events/lib/eventsToTraceAdapter";
|
||||
import {
|
||||
filterAndValidateDbScoreList,
|
||||
AGGREGATABLE_SCORE_TYPES,
|
||||
ScoreDataTypeArray,
|
||||
ScoreDataTypeEnum,
|
||||
type ScoreDomain,
|
||||
} from "@langfuse/shared";
|
||||
@@ -123,7 +123,7 @@ export function useEventsTraceData(
|
||||
// Validate and partition scores
|
||||
const validatedScores = filterAndValidateDbScoreList({
|
||||
scores: scoresQuery.data ?? [],
|
||||
dataTypes: [...AGGREGATABLE_SCORE_TYPES, ScoreDataTypeEnum.CORRECTION],
|
||||
dataTypes: [...ScoreDataTypeArray],
|
||||
onParseError: (e) => {
|
||||
console.error("[useEventsTraceData] Score validation error:", e);
|
||||
},
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { type z } from "zod";
|
||||
import {
|
||||
AGGREGATABLE_SCORE_TYPES,
|
||||
type FilterCondition,
|
||||
LISTABLE_SCORE_TYPES,
|
||||
filterAndValidateDbScoreList,
|
||||
} from "@langfuse/shared";
|
||||
import {
|
||||
@@ -128,13 +128,13 @@ export async function getEventList(params: GetObservationsListParams) {
|
||||
|
||||
const validatedScores = filterAndValidateDbScoreList({
|
||||
scores,
|
||||
dataTypes: AGGREGATABLE_SCORE_TYPES,
|
||||
dataTypes: LISTABLE_SCORE_TYPES,
|
||||
includeHasMetadata: true,
|
||||
onParseError: traceException,
|
||||
});
|
||||
const validatedTraceScores = filterAndValidateDbScoreList({
|
||||
scores: traceScores,
|
||||
dataTypes: AGGREGATABLE_SCORE_TYPES,
|
||||
dataTypes: LISTABLE_SCORE_TYPES,
|
||||
includeHasMetadata: true,
|
||||
onParseError: traceException,
|
||||
});
|
||||
|
||||
@@ -301,11 +301,29 @@ export default function ExperimentsTable({
|
||||
header: getExperimentsColumnName("experimentDatasetId"),
|
||||
size: 150,
|
||||
cell: ({ row }) => {
|
||||
const key: string | undefined = row.getValue("datasetId");
|
||||
const value = filterOptions.experimentDatasetId?.find(
|
||||
(d) => d.value === key,
|
||||
const datasetId: string | undefined = row.getValue("datasetId");
|
||||
const datasetName = filterOptions.experimentDatasetId?.find(
|
||||
(d) => d.value === datasetId,
|
||||
)?.displayValue;
|
||||
return value ? <TableIdOrName value={value} /> : undefined;
|
||||
|
||||
if (!datasetId || !datasetName) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return (
|
||||
<Link
|
||||
href={`/project/${projectId}/datasets/${encodeURIComponent(datasetId)}`}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
<Badge
|
||||
variant="secondary"
|
||||
className="hover:bg-secondary/80 max-w-full cursor-pointer"
|
||||
>
|
||||
{datasetName}
|
||||
</Badge>
|
||||
</Link>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
|
||||
@@ -399,7 +399,13 @@ export const llmApiKeyRouter = createTRPCRouter({
|
||||
|
||||
test: protectedProjectProcedureWithoutTracing
|
||||
.input(CreateLlmApiKey)
|
||||
.mutation(async ({ input }) => {
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
throwIfNoProjectAccess({
|
||||
session: ctx.session,
|
||||
projectId: input.projectId,
|
||||
scope: "llmApiKeys:create",
|
||||
});
|
||||
|
||||
return testLLMConnection({
|
||||
adapter: input.adapter,
|
||||
provider: input.provider,
|
||||
@@ -414,13 +420,13 @@ export const llmApiKeyRouter = createTRPCRouter({
|
||||
testUpdate: protectedProjectProcedureWithoutTracing
|
||||
.input(UpdateLlmApiKey)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
try {
|
||||
throwIfNoProjectAccess({
|
||||
session: ctx.session,
|
||||
projectId: input.projectId,
|
||||
scope: "llmApiKeys:read",
|
||||
});
|
||||
throwIfNoProjectAccess({
|
||||
session: ctx.session,
|
||||
projectId: input.projectId,
|
||||
scope: "llmApiKeys:update",
|
||||
});
|
||||
|
||||
try {
|
||||
// Get the existing key from the database
|
||||
const existingKey = await ctx.prisma.llmApiKeys.findUnique({
|
||||
where: {
|
||||
@@ -436,25 +442,37 @@ export const llmApiKeyRouter = createTRPCRouter({
|
||||
});
|
||||
}
|
||||
|
||||
const decryptedSecretKey =
|
||||
input.secretKey !== undefined &&
|
||||
input.secretKey !== "" &&
|
||||
input.secretKey !== null
|
||||
? input.secretKey
|
||||
: decrypt(existingKey.secretKey);
|
||||
const hasNewSecretKey =
|
||||
typeof input.secretKey === "string" && input.secretKey.length > 0;
|
||||
const baseURL = input.baseURL ?? existingKey.baseURL;
|
||||
const isBaseURLChanged = baseURL !== existingKey.baseURL;
|
||||
|
||||
if (isBaseURLChanged && !hasNewSecretKey) {
|
||||
throw new TRPCError({
|
||||
code: "BAD_REQUEST",
|
||||
message: "Secret key is required when changing the base URL",
|
||||
});
|
||||
}
|
||||
|
||||
const secretKey = hasNewSecretKey
|
||||
? (input.secretKey as string)
|
||||
: decrypt(existingKey.secretKey);
|
||||
|
||||
// Merge existing key with provided input, giving priority to input
|
||||
const secretKey = decryptedSecretKey;
|
||||
const adapter = input.adapter ?? (existingKey.adapter as LLMAdapter);
|
||||
const provider = input.provider ?? existingKey.provider;
|
||||
const baseURL = input.baseURL ?? existingKey.baseURL;
|
||||
const customModels = input.customModels ?? existingKey.customModels;
|
||||
const config = input.config ?? existingKey.config;
|
||||
|
||||
// Never reuse stored headers across a destination change.
|
||||
const extraHeaders =
|
||||
input.extraHeaders ??
|
||||
(existingKey.extraHeaders
|
||||
? decryptAndParseExtraHeaders(existingKey.extraHeaders)
|
||||
: undefined);
|
||||
input.extraHeaders !== undefined
|
||||
? input.extraHeaders
|
||||
: isBaseURLChanged
|
||||
? undefined
|
||||
: existingKey.extraHeaders
|
||||
? decryptAndParseExtraHeaders(existingKey.extraHeaders)
|
||||
: undefined;
|
||||
|
||||
return testLLMConnection({
|
||||
adapter,
|
||||
|
||||
@@ -12,26 +12,43 @@ import {
|
||||
import { usePostHogClientCapture } from "@/src/features/posthog-analytics/usePostHogClientCapture";
|
||||
import { ChevronDown, ChevronUp } from "lucide-react";
|
||||
import { useRouter } from "next/router";
|
||||
import { useEffect } from "react";
|
||||
import { useCallback, useEffect } from "react";
|
||||
|
||||
export const DetailPageNav = (props: {
|
||||
currentId: string;
|
||||
path: (entry: ListEntry) => string;
|
||||
listKey: string;
|
||||
onNavigate?: (entry: ListEntry) => void;
|
||||
}) => {
|
||||
const { currentId, path, listKey, onNavigate } = props;
|
||||
const { detailPagelists } = useDetailPageLists();
|
||||
const entries = detailPagelists[props.listKey] ?? [];
|
||||
const entries = detailPagelists[listKey] ?? [];
|
||||
|
||||
const capture = usePostHogClientCapture();
|
||||
const router = useRouter();
|
||||
const currentIndex = entries.findIndex(
|
||||
(entry) => entry.id === props.currentId,
|
||||
);
|
||||
const currentIndex = entries.findIndex((entry) => entry.id === currentId);
|
||||
const previousPageEntry =
|
||||
currentIndex > 0 ? entries[currentIndex - 1] : undefined;
|
||||
const nextPageEntry =
|
||||
currentIndex < entries.length - 1 ? entries[currentIndex + 1] : undefined;
|
||||
|
||||
const navigateToEntry = useCallback(
|
||||
(entry: ListEntry) => {
|
||||
if (onNavigate) {
|
||||
onNavigate(entry);
|
||||
return;
|
||||
}
|
||||
|
||||
void router.push(
|
||||
path({
|
||||
id: encodeURIComponent(entry.id),
|
||||
params: entry.params,
|
||||
}),
|
||||
);
|
||||
},
|
||||
[onNavigate, path, router],
|
||||
);
|
||||
|
||||
// keyboard shortcuts for buttons k and j
|
||||
useEffect(() => {
|
||||
const handleKeyDown = (event: KeyboardEvent) => {
|
||||
@@ -50,22 +67,14 @@ export const DetailPageNav = (props: {
|
||||
}
|
||||
|
||||
if (event.key === "k" && previousPageEntry) {
|
||||
const newPath = props.path({
|
||||
id: encodeURIComponent(previousPageEntry.id),
|
||||
params: previousPageEntry.params,
|
||||
});
|
||||
void router.push(newPath);
|
||||
navigateToEntry(previousPageEntry);
|
||||
} else if (event.key === "j" && nextPageEntry) {
|
||||
const newPath = props.path({
|
||||
id: encodeURIComponent(nextPageEntry.id),
|
||||
params: nextPageEntry.params,
|
||||
});
|
||||
void router.push(newPath);
|
||||
navigateToEntry(nextPageEntry);
|
||||
}
|
||||
};
|
||||
window.addEventListener("keydown", handleKeyDown);
|
||||
return () => window.removeEventListener("keydown", handleKeyDown);
|
||||
}, [previousPageEntry, nextPageEntry, router, props]);
|
||||
}, [previousPageEntry, nextPageEntry, navigateToEntry]);
|
||||
|
||||
if (entries.length > 1)
|
||||
return (
|
||||
@@ -80,12 +89,7 @@ export const DetailPageNav = (props: {
|
||||
onClick={() => {
|
||||
if (previousPageEntry) {
|
||||
capture("navigate_detail_pages:button_click_prev_or_next");
|
||||
void router.push(
|
||||
props.path({
|
||||
id: encodeURIComponent(previousPageEntry.id),
|
||||
params: previousPageEntry.params,
|
||||
}),
|
||||
);
|
||||
navigateToEntry(previousPageEntry);
|
||||
}
|
||||
}}
|
||||
>
|
||||
@@ -113,12 +117,7 @@ export const DetailPageNav = (props: {
|
||||
onClick={() => {
|
||||
if (nextPageEntry) {
|
||||
capture("navigate_detail_pages:button_click_prev_or_next");
|
||||
void router.push(
|
||||
props.path({
|
||||
id: encodeURIComponent(nextPageEntry.id),
|
||||
params: nextPageEntry.params,
|
||||
}),
|
||||
);
|
||||
navigateToEntry(nextPageEntry);
|
||||
}
|
||||
}}
|
||||
>
|
||||
|
||||
@@ -95,7 +95,11 @@ export function ApiKeyList(props: { entityId: string; scope: ApiKeyScope }) {
|
||||
}}
|
||||
actionButtons={<CreateApiKeyButton entityId={entityId} scope={scope} />}
|
||||
/>
|
||||
<CodeView content={envCode} title=".env" />
|
||||
<CodeView
|
||||
content={envCode}
|
||||
title=".env"
|
||||
copiedToClipboardMessage="Secrets are not included, create a new key to copy them."
|
||||
/>
|
||||
<Card className="mb-4 overflow-hidden">
|
||||
<Table>
|
||||
<TableHeader>
|
||||
|
||||
@@ -4,10 +4,7 @@ import {
|
||||
convertScoreToPublicApi,
|
||||
type ScoreQueryType,
|
||||
} from "@/src/features/public-api/server/scores";
|
||||
import {
|
||||
AGGREGATABLE_SCORE_TYPES,
|
||||
type ScoreSourceType,
|
||||
} from "@langfuse/shared";
|
||||
import { LISTABLE_SCORE_TYPES, type ScoreSourceType } from "@langfuse/shared";
|
||||
import { _handleGetScoreById } from "@langfuse/shared/src/server";
|
||||
|
||||
export class ScoresApiService {
|
||||
@@ -15,8 +12,8 @@ export class ScoresApiService {
|
||||
|
||||
/**
|
||||
* Get a specific score by ID
|
||||
* v1: Only returns aggregatable scores (NUMERIC, BOOLEAN, CATEGORICAL) - excludes CORRECTION
|
||||
* v2: Returns all score types including CORRECTION
|
||||
* v1: Returns listable scores (NUMERIC, BOOLEAN, CATEGORICAL, TEXT) - excludes CORRECTION
|
||||
* v2: Returns all score types including CORRECTION and TEXT
|
||||
*/
|
||||
async getScoreById({
|
||||
projectId,
|
||||
@@ -33,7 +30,7 @@ export class ScoresApiService {
|
||||
source,
|
||||
scoreScope: this.apiVersion === "v1" ? "traces_only" : "all",
|
||||
scoreDataTypes:
|
||||
this.apiVersion === "v1" ? AGGREGATABLE_SCORE_TYPES : undefined,
|
||||
this.apiVersion === "v1" ? LISTABLE_SCORE_TYPES : undefined,
|
||||
preferredClickhouseService: "ReadOnly",
|
||||
});
|
||||
|
||||
@@ -46,29 +43,29 @@ export class ScoresApiService {
|
||||
|
||||
/**
|
||||
* Get list of scores with version-aware filtering
|
||||
* v1: Only returns aggregatable scores (NUMERIC, BOOLEAN, CATEGORICAL) - excludes CORRECTION
|
||||
* v2: Returns all score types including CORRECTION
|
||||
* v1: Returns listable scores (NUMERIC, BOOLEAN, CATEGORICAL, TEXT) - excludes CORRECTION
|
||||
* v2: Returns all score types including CORRECTION and TEXT
|
||||
*/
|
||||
async generateScoresForPublicApi(props: ScoreQueryType) {
|
||||
return _handleGenerateScoresForPublicApi({
|
||||
props,
|
||||
scoreScope: this.apiVersion === "v1" ? "traces_only" : "all",
|
||||
scoreDataTypes:
|
||||
this.apiVersion === "v1" ? AGGREGATABLE_SCORE_TYPES : undefined,
|
||||
this.apiVersion === "v1" ? LISTABLE_SCORE_TYPES : undefined,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Get count of scores with version-aware filtering
|
||||
* v1: Only counts aggregatable scores (NUMERIC, BOOLEAN, CATEGORICAL) - excludes CORRECTION
|
||||
* v2: Counts all score types including CORRECTION
|
||||
* v1: Only counts listable scores (NUMERIC, BOOLEAN, CATEGORICAL, TEXT) - excludes CORRECTION
|
||||
* v2: Counts all score types including CORRECTION and TEXT
|
||||
*/
|
||||
async getScoresCountForPublicApi(props: ScoreQueryType) {
|
||||
return _handleGetScoresCountForPublicApi({
|
||||
props,
|
||||
scoreScope: this.apiVersion === "v1" ? "traces_only" : "all",
|
||||
scoreDataTypes:
|
||||
this.apiVersion === "v1" ? AGGREGATABLE_SCORE_TYPES : undefined,
|
||||
this.apiVersion === "v1" ? LISTABLE_SCORE_TYPES : undefined,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,14 +18,26 @@ import {
|
||||
type FilterState,
|
||||
} from "@langfuse/shared";
|
||||
|
||||
type ScoreApiResult = Omit<ScoreDomain, "longStringValue"> & {
|
||||
stringValue?: string | null;
|
||||
};
|
||||
type TextScoreApiResult = Omit<ScoreDomain, "longStringValue" | "value"> & {
|
||||
stringValue?: string | null;
|
||||
};
|
||||
|
||||
/**
|
||||
* Converts a ScoreDomain object to API format.
|
||||
* For CORRECTION scores, moves longStringValue to stringValue for API compatibility.
|
||||
* For TEXT scores, removes longStringValue and value (always 0, not meaningful).
|
||||
* For other score types, removes longStringValue.
|
||||
*/
|
||||
export const convertScoreToPublicApi = <T extends ScoreDomain>(
|
||||
score: T,
|
||||
): Omit<T, "longStringValue"> & { stringValue?: string | null } => {
|
||||
export function convertScoreToPublicApi(
|
||||
score: ScoreDomain & { dataType: "TEXT" },
|
||||
): TextScoreApiResult;
|
||||
export function convertScoreToPublicApi(score: ScoreDomain): ScoreApiResult;
|
||||
export function convertScoreToPublicApi(
|
||||
score: ScoreDomain,
|
||||
): ScoreApiResult | TextScoreApiResult {
|
||||
if (score.dataType === ScoreDataTypeEnum.CORRECTION) {
|
||||
const { longStringValue, ...rest } = score;
|
||||
return {
|
||||
@@ -34,8 +46,12 @@ export const convertScoreToPublicApi = <T extends ScoreDomain>(
|
||||
};
|
||||
}
|
||||
|
||||
if (score.dataType === ScoreDataTypeEnum.TEXT) {
|
||||
return removeObjectKeys(score, ["longStringValue", "value"]);
|
||||
}
|
||||
|
||||
return removeObjectKeys(score, ["longStringValue"]);
|
||||
};
|
||||
}
|
||||
|
||||
export type ScoreQueryType = {
|
||||
page: number;
|
||||
|
||||
@@ -13,7 +13,7 @@ import {
|
||||
shouldSkipObservationsFinal,
|
||||
} from "@langfuse/shared/src/server";
|
||||
import {
|
||||
AGGREGATABLE_SCORE_TYPES,
|
||||
LISTABLE_SCORE_TYPES,
|
||||
type OrderByState,
|
||||
tracesTableCols,
|
||||
} from "@langfuse/shared";
|
||||
@@ -376,7 +376,7 @@ async function buildTracesBaseQuery(
|
||||
...appliedEnvironmentFilter.params,
|
||||
...appliedFilter.params,
|
||||
projectId: props.projectId,
|
||||
dataTypes: AGGREGATABLE_SCORE_TYPES,
|
||||
dataTypes: LISTABLE_SCORE_TYPES,
|
||||
...(props.limit !== undefined ? { limit: props.limit } : {}),
|
||||
...(props.page !== undefined
|
||||
? { offset: (props.page - 1) * props.limit }
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import {
|
||||
BooleanConfigFields,
|
||||
CategoricalConfigFields,
|
||||
TextConfigFields,
|
||||
jsonSchema,
|
||||
NumericConfigFields,
|
||||
paginationMetaResponseZod,
|
||||
@@ -53,6 +54,10 @@ const APIScoreConfig = z
|
||||
...ScoreConfigBase.shape,
|
||||
...BooleanConfigFields.shape,
|
||||
}),
|
||||
z.object({
|
||||
...ScoreConfigBase.shape,
|
||||
...TextConfigFields.shape,
|
||||
}),
|
||||
])
|
||||
.superRefine(validateNumericRangeFields);
|
||||
|
||||
@@ -92,6 +97,15 @@ export const PostScoreConfigBody = z
|
||||
categories: z.undefined(),
|
||||
}).shape,
|
||||
}),
|
||||
z.object({
|
||||
...PostScoreConfigBase.shape,
|
||||
...z.object({
|
||||
dataType: z.literal("TEXT"),
|
||||
categories: z.undefined(),
|
||||
maxValue: z.undefined().nullish(),
|
||||
minValue: z.undefined().nullish(),
|
||||
}).shape,
|
||||
}),
|
||||
])
|
||||
.superRefine(validateNumericRangeFields);
|
||||
|
||||
|
||||
@@ -89,10 +89,11 @@ export const GetTracesV1Query = z.object({
|
||||
.nullish()
|
||||
.transform((v) => {
|
||||
if (!v) return null;
|
||||
return v
|
||||
const parsed = v
|
||||
.split(",")
|
||||
.map((f) => f.trim())
|
||||
.filter((f) => TRACE_FIELD_GROUPS.includes(f as TraceFieldGroup));
|
||||
return parsed.length > 0 ? parsed : null;
|
||||
})
|
||||
.pipe(z.array(z.enum(TRACE_FIELD_GROUPS)).nullable()),
|
||||
useEventsTable: useEventsTableSchema,
|
||||
@@ -125,6 +126,18 @@ export const PostTracesV1Response = z.object({ id: z.string() });
|
||||
// GET /api/public/traces/{traceId}
|
||||
export const GetTraceV1Query = z.object({
|
||||
traceId: z.string(),
|
||||
fields: z
|
||||
.string()
|
||||
.nullish()
|
||||
.transform((v) => {
|
||||
if (!v) return null;
|
||||
const parsed = v
|
||||
.split(",")
|
||||
.map((f) => f.trim())
|
||||
.filter((f) => TRACE_FIELD_GROUPS.includes(f as TraceFieldGroup));
|
||||
return parsed.length > 0 ? parsed : null;
|
||||
})
|
||||
.pipe(z.array(z.enum(TRACE_FIELD_GROUPS)).nullable()),
|
||||
});
|
||||
export const GetTraceV1Response = APIExtendedTrace.extend({
|
||||
scores: z.array(APIScoreSchemaV1),
|
||||
|
||||
@@ -34,6 +34,7 @@ import { Textarea } from "@/src/components/ui/textarea";
|
||||
import {
|
||||
isBooleanDataType,
|
||||
isCategoricalDataType,
|
||||
isTextDataType,
|
||||
isNumericDataType,
|
||||
} from "@/src/features/scores/lib/helpers";
|
||||
import DocPopup from "@/src/components/layouts/doc-popup";
|
||||
@@ -192,16 +193,19 @@ export function UpsertScoreConfigDialog({
|
||||
disabled={!!id}
|
||||
defaultValue={field.value}
|
||||
onValueChange={(value) => {
|
||||
field.onChange(value as ScoreConfigDataType);
|
||||
const dt = value as ScoreConfigDataType;
|
||||
field.onChange(dt);
|
||||
form.clearErrors();
|
||||
if (isNumericDataType(value as ScoreConfigDataType)) {
|
||||
if (isNumericDataType(dt)) {
|
||||
form.setValue("categories", undefined);
|
||||
} else if (isTextDataType(dt)) {
|
||||
form.setValue("categories", undefined);
|
||||
form.setValue("minValue", undefined);
|
||||
form.setValue("maxValue", undefined);
|
||||
} else {
|
||||
form.setValue("minValue", undefined);
|
||||
form.setValue("maxValue", undefined);
|
||||
if (
|
||||
isBooleanDataType(value as ScoreConfigDataType)
|
||||
) {
|
||||
if (isBooleanDataType(dt)) {
|
||||
replace([
|
||||
{ label: "True", value: 1 },
|
||||
{ label: "False", value: 0 },
|
||||
@@ -282,7 +286,7 @@ export function UpsertScoreConfigDialog({
|
||||
)}
|
||||
/>
|
||||
</>
|
||||
) : (
|
||||
) : isTextDataType(form.getValues("dataType")) ? null : (
|
||||
<div className="grid grid-flow-row gap-2">
|
||||
<FormField
|
||||
control={form.control}
|
||||
|
||||
@@ -4,7 +4,7 @@ import { z } from "zod";
|
||||
export const createConfigSchema = z.object({
|
||||
name: z.string().min(1).max(35),
|
||||
description: z.string().optional(),
|
||||
dataType: z.enum(["CATEGORICAL", "NUMERIC", "BOOLEAN"]),
|
||||
dataType: z.enum(["CATEGORICAL", "NUMERIC", "BOOLEAN", "TEXT"]),
|
||||
minValue: z.coerce.number().optional(),
|
||||
maxValue: z.coerce.number().optional(),
|
||||
categories: z.array(ScoreConfigCategory).optional(),
|
||||
|
||||
@@ -2,6 +2,7 @@ import {
|
||||
NumericConfigFields,
|
||||
CategoricalConfigFields,
|
||||
BooleanConfigFields,
|
||||
TextConfigFields,
|
||||
validateNumericRangeFields,
|
||||
} from "@langfuse/shared";
|
||||
import { z } from "zod";
|
||||
@@ -19,6 +20,7 @@ const ScoreConfigValidationSchema = ScoreConfigBaseSchema.and(
|
||||
NumericConfigFields,
|
||||
CategoricalConfigFields,
|
||||
BooleanConfigFields,
|
||||
TextConfigFields,
|
||||
])
|
||||
.superRefine(validateNumericRangeFields),
|
||||
);
|
||||
|
||||
@@ -25,6 +25,7 @@ import {
|
||||
type ScoreConfigCategoryDomain,
|
||||
type UpdateAnnotationScoreData,
|
||||
type CreateAnnotationScoreData,
|
||||
TEXT_SCORE_MAX_LENGTH,
|
||||
} from "@langfuse/shared";
|
||||
import { Input } from "@/src/components/ui/input";
|
||||
import {
|
||||
@@ -39,6 +40,7 @@ import { HoverCardContent } from "@radix-ui/react-hover-card";
|
||||
import { HoverCard, HoverCardTrigger } from "@/src/components/ui/hover-card";
|
||||
import {
|
||||
formatAnnotateDescription,
|
||||
isTextDataType,
|
||||
isNumericDataType,
|
||||
isScoreUnsaved,
|
||||
} from "@/src/features/scores/lib/helpers";
|
||||
@@ -295,10 +297,17 @@ function InnerAnnotationForm<Target extends ScoreTarget>({
|
||||
form.setValue(`scoreData.${index}.stringValue`, previousScore.stringValue);
|
||||
form.setValue(`scoreData.${index}.comment`, previousScore.comment);
|
||||
form.setValue(`scoreData.${index}.timestamp`, previousScore.timestamp);
|
||||
form.setError(`scoreData.${index}.value`, {
|
||||
type: "server",
|
||||
message: "Failed to delete score",
|
||||
});
|
||||
if (isTextDataType(field.dataType)) {
|
||||
form.setError(`scoreData.${index}.stringValue`, {
|
||||
type: "server",
|
||||
message: "Failed to delete score",
|
||||
});
|
||||
} else {
|
||||
form.setError(`scoreData.${index}.value`, {
|
||||
type: "server",
|
||||
message: "Failed to delete score",
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const handleDeleteScore = (index: number) => {
|
||||
@@ -314,7 +323,11 @@ function InnerAnnotationForm<Target extends ScoreTarget>({
|
||||
};
|
||||
|
||||
// Optimistically clear form
|
||||
form.clearErrors(`scoreData.${index}.value`);
|
||||
if (isTextDataType(field.dataType)) {
|
||||
form.clearErrors(`scoreData.${index}.stringValue`);
|
||||
} else {
|
||||
form.clearErrors(`scoreData.${index}.value`);
|
||||
}
|
||||
update(index, {
|
||||
name: field.name,
|
||||
dataType: field.dataType,
|
||||
@@ -349,10 +362,17 @@ function InnerAnnotationForm<Target extends ScoreTarget>({
|
||||
) => {
|
||||
form.setValue(`scoreData.${index}.value`, previousValue);
|
||||
form.setValue(`scoreData.${index}.stringValue`, previousStringValue);
|
||||
form.setError(`scoreData.${index}.value`, {
|
||||
type: "server",
|
||||
message: "Failed to update score",
|
||||
});
|
||||
if (isTextDataType(controlledFields[index]?.dataType)) {
|
||||
form.setError(`scoreData.${index}.stringValue`, {
|
||||
type: "server",
|
||||
message: "Failed to update score",
|
||||
});
|
||||
} else {
|
||||
form.setError(`scoreData.${index}.value`, {
|
||||
type: "server",
|
||||
message: "Failed to update score",
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const rollbackCreateError = (
|
||||
@@ -366,10 +386,17 @@ function InnerAnnotationForm<Target extends ScoreTarget>({
|
||||
form.setValue(`scoreData.${index}.timestamp`, previousTimestamp);
|
||||
form.setValue(`scoreData.${index}.value`, previousValue);
|
||||
form.setValue(`scoreData.${index}.stringValue`, previousStringValue);
|
||||
form.setError(`scoreData.${index}.value`, {
|
||||
type: "server",
|
||||
message: "Failed to create score",
|
||||
});
|
||||
if (isTextDataType(controlledFields[index]?.dataType)) {
|
||||
form.setError(`scoreData.${index}.stringValue`, {
|
||||
type: "server",
|
||||
message: "Failed to create score",
|
||||
});
|
||||
} else {
|
||||
form.setError(`scoreData.${index}.value`, {
|
||||
type: "server",
|
||||
message: "Failed to create score",
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const handleUpsert = (
|
||||
@@ -487,6 +514,21 @@ function InnerAnnotationForm<Target extends ScoreTarget>({
|
||||
handleUpsert(index, numericCategoryValue, stringValue);
|
||||
};
|
||||
|
||||
const handleTextUpsert = (index: number) => {
|
||||
const field = controlledFields[index];
|
||||
const config = configs.find((c) => c.id === field.configId);
|
||||
|
||||
if (!config || !field) return;
|
||||
if (!field.stringValue) {
|
||||
if (field.id) {
|
||||
handleDeleteScore(index);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
handleUpsert(index, 0, field.stringValue);
|
||||
};
|
||||
|
||||
const rollbackCommentError = (
|
||||
index: number,
|
||||
field: (typeof controlledFields)[number],
|
||||
@@ -676,7 +718,28 @@ function InnerAnnotationForm<Target extends ScoreTarget>({
|
||||
</Popover>
|
||||
</div>
|
||||
<div className="grid grid-cols-[11fr_1fr] items-center py-1">
|
||||
{isNumericDataType(score.dataType) ? (
|
||||
{isTextDataType(score.dataType) ? (
|
||||
<FormField
|
||||
control={form.control}
|
||||
name={`scoreData.${index}.stringValue`}
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormControl>
|
||||
<Textarea
|
||||
{...field}
|
||||
value={field.value ?? ""}
|
||||
maxLength={TEXT_SCORE_MAX_LENGTH}
|
||||
className="text-xs"
|
||||
disabled={isInputDisabled(config)}
|
||||
placeholder="Enter free form text..."
|
||||
onBlur={() => handleTextUpsert(index)}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormMessage className="text-xs" />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
) : isNumericDataType(score.dataType) ? (
|
||||
<FormField
|
||||
control={form.control}
|
||||
name={`scoreData.${index}.value`}
|
||||
|
||||
@@ -4,7 +4,9 @@ import {
|
||||
type ScoreSimplified,
|
||||
type ScoreSourceType,
|
||||
type ScoreDomain,
|
||||
type AggregatableScoreDataType,
|
||||
type ScoreDataTypeType,
|
||||
type ListableScore,
|
||||
type ListableScoreDataType,
|
||||
} from "@langfuse/shared";
|
||||
|
||||
/**
|
||||
@@ -22,7 +24,7 @@ export const composeAggregateScoreKey = ({
|
||||
}: {
|
||||
name: string;
|
||||
source: ScoreSourceType;
|
||||
dataType: AggregatableScoreDataType;
|
||||
dataType: ScoreDataTypeType;
|
||||
keyPrefix?: string;
|
||||
}): string => {
|
||||
const formattedName = normalizeScoreName(name);
|
||||
@@ -34,13 +36,13 @@ export const decomposeAggregateScoreKey = (
|
||||
): {
|
||||
name: string;
|
||||
source: ScoreSourceType;
|
||||
dataType: AggregatableScoreDataType;
|
||||
dataType: ScoreDataTypeType;
|
||||
} => {
|
||||
const [name, source, dataType] = key.split("-");
|
||||
return {
|
||||
name,
|
||||
source: source as ScoreSourceType,
|
||||
dataType: dataType as AggregatableScoreDataType,
|
||||
dataType: dataType as ScoreDataTypeType,
|
||||
};
|
||||
};
|
||||
|
||||
@@ -51,7 +53,7 @@ export const getScoreLabelFromKey = (key: string): string => {
|
||||
|
||||
export type ScoreToAggregate =
|
||||
| (Omit<ScoreDomain, "dataType"> & {
|
||||
dataType: AggregatableScoreDataType;
|
||||
dataType: ListableScore["dataType"];
|
||||
hasMetadata?: boolean;
|
||||
})
|
||||
| (ScoreSimplified & {
|
||||
@@ -64,9 +66,9 @@ export type ScoreToAggregate =
|
||||
* aggregation logic (value counting vs numeric averaging).
|
||||
*/
|
||||
export const resolveAggregateType = (
|
||||
dataType: AggregatableScoreDataType,
|
||||
dataType: ListableScoreDataType,
|
||||
): "NUMERIC" | "CATEGORICAL" => {
|
||||
return dataType === "BOOLEAN" ? "CATEGORICAL" : dataType;
|
||||
return dataType === "NUMERIC" ? "NUMERIC" : "CATEGORICAL";
|
||||
};
|
||||
|
||||
export const aggregateScores = <T extends ScoreToAggregate>(
|
||||
|
||||
@@ -16,6 +16,9 @@ export const isCategoricalDataType = (dataType: ScoreDataTypeType) =>
|
||||
export const isBooleanDataType = (dataType: ScoreDataTypeType) =>
|
||||
dataType === ScoreDataTypeEnum.BOOLEAN;
|
||||
|
||||
export const isTextDataType = (dataType: ScoreDataTypeType) =>
|
||||
dataType === ScoreDataTypeEnum.TEXT;
|
||||
|
||||
export const isScoreUnsaved = (scoreId?: string | null): boolean => !scoreId;
|
||||
|
||||
export const toOrderedScoresList = (list: ScoreData[]): ScoreData[] =>
|
||||
|
||||
@@ -169,6 +169,8 @@ export const getScoreDataTypeIcon = (dataType: ScoreDataTypeType): string => {
|
||||
return "Ⓑ";
|
||||
case "CORRECTION":
|
||||
return "";
|
||||
case "TEXT":
|
||||
return "Aa";
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -109,12 +109,18 @@ function transformAggregates(
|
||||
const scores: AnnotationScore[] = [];
|
||||
|
||||
Object.entries(mergedAggregates).forEach(([key, aggregate]) => {
|
||||
const { name, source, dataType } = decomposeAggregateScoreKey(key);
|
||||
const {
|
||||
name,
|
||||
source,
|
||||
dataType: rawDataType,
|
||||
} = decomposeAggregateScoreKey(key);
|
||||
|
||||
// Only ANNOTATION source can be edited, and must have single ID
|
||||
// Multi-value aggregates (no id) are child observation scores - skip them
|
||||
if (source !== "ANNOTATION" || !aggregate.id) return;
|
||||
|
||||
const dataType = rawDataType as AnnotationScoreDataType;
|
||||
|
||||
const config = configs.find(
|
||||
(c) => normalizeScoreName(c.name) === name && c.dataType === dataType,
|
||||
);
|
||||
|
||||
@@ -8,7 +8,6 @@ import {
|
||||
type ScoreAggregate,
|
||||
type ScoreConfigDomain,
|
||||
type ScoreDomain,
|
||||
type AggregatableScoreDataType,
|
||||
ScoreConfigDataType,
|
||||
} from "@langfuse/shared";
|
||||
import { type z } from "zod";
|
||||
@@ -106,7 +105,7 @@ export type ScoreColumn = {
|
||||
key: string;
|
||||
name: string;
|
||||
source: ScoreSourceType;
|
||||
dataType: AggregatableScoreDataType;
|
||||
dataType: AnnotationScoreDataType;
|
||||
};
|
||||
|
||||
export type ScoreConfigSelection =
|
||||
|
||||
@@ -27,10 +27,9 @@ export default withMiddlewares({
|
||||
const datasetItem = await getDatasetItemById({
|
||||
projectId: auth.scope.projectId,
|
||||
datasetItemId: datasetItemId,
|
||||
status: "ACTIVE",
|
||||
});
|
||||
if (!datasetItem) {
|
||||
throw new LangfuseNotFoundError("Dataset item not found or archived");
|
||||
throw new LangfuseNotFoundError("Dataset item not found");
|
||||
}
|
||||
|
||||
const dataset = await prisma.dataset.findUnique({
|
||||
|
||||
@@ -8,7 +8,11 @@ import {
|
||||
PostDatasetItemsV1Response,
|
||||
transformDbDatasetItemDomainToAPIDatasetItem,
|
||||
} from "@/src/features/public-api/types/datasets";
|
||||
import { LangfuseNotFoundError, Prisma } from "@langfuse/shared";
|
||||
import {
|
||||
LangfuseConflictError,
|
||||
LangfuseNotFoundError,
|
||||
Prisma,
|
||||
} from "@langfuse/shared";
|
||||
import {
|
||||
createDatasetItemFilterState,
|
||||
getDatasetItems,
|
||||
@@ -82,12 +86,23 @@ export default withMiddlewares({
|
||||
// When this constraint is violated, the database will upsert based on (id, projectId, datasetId).
|
||||
// If this record does not exist, the database will throw an error.
|
||||
logger.warn(
|
||||
`Failed to upsert dataset item. Dataset item ${id} in project ${auth.scope.projectId} already exists for a different dataset than ${datasetName}`,
|
||||
`Failed to upsert dataset item. Dataset item ${id} already exists for a different dataset than ${datasetName}`,
|
||||
);
|
||||
throw new LangfuseNotFoundError(
|
||||
`The dataset item with id ${id} already exists in a dataset other than ${datasetName}`,
|
||||
);
|
||||
}
|
||||
if (e.code === "P2002") {
|
||||
// Unique constraint violation on (id, projectId, validFrom).
|
||||
// This can happen when concurrent requests try to update the same dataset item
|
||||
// and create versions with the same timestamp.
|
||||
logger.warn(
|
||||
`Failed to upsert dataset item due to version conflict. Dataset item ${id} was modified concurrently.`,
|
||||
);
|
||||
throw new LangfuseConflictError(
|
||||
`Dataset item ${id ?? "new"} was modified concurrently. Please retry the request.`,
|
||||
);
|
||||
}
|
||||
}
|
||||
throw e;
|
||||
}
|
||||
|
||||
@@ -116,6 +116,22 @@ export default withMiddlewares({
|
||||
return {};
|
||||
}
|
||||
|
||||
// Warn on oversized OTEL request bodies (16MB threshold)
|
||||
const bodyBytes = body.byteLength;
|
||||
if (bodyBytes > 16 * 1024 * 1024) {
|
||||
let spanCount = 0;
|
||||
for (const rs of resourceSpans) {
|
||||
for (const ss of rs?.scopeSpans ?? []) {
|
||||
spanCount += ss?.spans?.length ?? 0;
|
||||
}
|
||||
}
|
||||
logger.warn("OTEL request body exceeds 16MB", {
|
||||
projectId: auth.scope.projectId,
|
||||
bodyBytes,
|
||||
spanCount,
|
||||
});
|
||||
}
|
||||
|
||||
// Extract SDK headers for write path decision (supports both hyphen and underscore formats)
|
||||
const sdkName = getLangfuseHeader(req.headers, "x-langfuse-sdk-name");
|
||||
const sdkVersion = getLangfuseHeader(
|
||||
|
||||
@@ -6,7 +6,10 @@ import {
|
||||
GetTraceV1Response,
|
||||
DeleteTraceV1Query,
|
||||
DeleteTraceV1Response,
|
||||
TRACE_FIELD_GROUPS,
|
||||
type TraceFieldGroup,
|
||||
} from "@/src/features/public-api/types/traces";
|
||||
import { env } from "@/src/env.mjs";
|
||||
import {
|
||||
filterAndValidateDbTraceScoreList,
|
||||
LangfuseNotFoundError,
|
||||
@@ -29,11 +32,32 @@ export default withMiddlewares({
|
||||
responseSchema: GetTraceV1Response,
|
||||
fn: async ({ query, auth }) => {
|
||||
const { traceId } = query;
|
||||
|
||||
let effectiveFields: readonly TraceFieldGroup[] =
|
||||
query.fields ?? TRACE_FIELD_GROUPS;
|
||||
if (!query.fields && env.LANGFUSE_API_TRACEBYID_DEFAULT_FIELDS) {
|
||||
const parsed = env.LANGFUSE_API_TRACEBYID_DEFAULT_FIELDS.split(",")
|
||||
.map((f) => f.trim())
|
||||
.filter((f): f is TraceFieldGroup =>
|
||||
TRACE_FIELD_GROUPS.includes(f as TraceFieldGroup),
|
||||
);
|
||||
if (parsed.length > 0) {
|
||||
effectiveFields = parsed;
|
||||
}
|
||||
}
|
||||
const requestedFields = effectiveFields;
|
||||
const includeIO = requestedFields.includes("io");
|
||||
const includeObservations = requestedFields.includes("observations");
|
||||
const includeScores = requestedFields.includes("scores");
|
||||
const includeMetrics = requestedFields.includes("metrics");
|
||||
|
||||
const trace = await getTraceById({
|
||||
traceId,
|
||||
projectId: auth.scope.projectId,
|
||||
clickhouseFeatureTag: "tracing-public-api",
|
||||
preferredClickhouseService: "ReadOnly",
|
||||
excludeInputOutput: !includeIO,
|
||||
excludeMetadata: !includeIO,
|
||||
});
|
||||
|
||||
if (!trace) {
|
||||
@@ -43,19 +67,23 @@ export default withMiddlewares({
|
||||
}
|
||||
|
||||
const [observations, scores] = await Promise.all([
|
||||
getObservationsForTrace({
|
||||
traceId,
|
||||
projectId: auth.scope.projectId,
|
||||
timestamp: trace?.timestamp,
|
||||
includeIO: true,
|
||||
preferredClickhouseService: "ReadOnly",
|
||||
}),
|
||||
getScoresForTraces({
|
||||
projectId: auth.scope.projectId,
|
||||
traceIds: [traceId],
|
||||
timestamp: trace?.timestamp,
|
||||
preferredClickhouseService: "ReadOnly",
|
||||
}),
|
||||
includeObservations || includeMetrics
|
||||
? getObservationsForTrace({
|
||||
traceId,
|
||||
projectId: auth.scope.projectId,
|
||||
timestamp: trace?.timestamp,
|
||||
includeIO: includeObservations,
|
||||
preferredClickhouseService: "ReadOnly",
|
||||
})
|
||||
: Promise.resolve([]),
|
||||
includeScores
|
||||
? getScoresForTraces({
|
||||
projectId: auth.scope.projectId,
|
||||
traceIds: [traceId],
|
||||
timestamp: trace?.timestamp,
|
||||
preferredClickhouseService: "ReadOnly",
|
||||
})
|
||||
: Promise.resolve([]),
|
||||
]);
|
||||
|
||||
const uniqueModels: string[] = Array.from(
|
||||
@@ -129,16 +157,24 @@ export default withMiddlewares({
|
||||
return {
|
||||
...trace,
|
||||
externalId: null,
|
||||
scores: validatedScores,
|
||||
latency: latencyMs !== undefined ? latencyMs / 1000 : 0,
|
||||
observations: outObservations,
|
||||
metadata: includeIO ? trace.metadata : {},
|
||||
scores: includeScores ? validatedScores : [],
|
||||
latency: includeMetrics
|
||||
? latencyMs !== undefined
|
||||
? latencyMs / 1000
|
||||
: 0
|
||||
: -1,
|
||||
observations: includeObservations ? outObservations : [],
|
||||
htmlPath: `/project/${auth.scope.projectId}/traces/${traceId}`,
|
||||
totalCost: outObservations
|
||||
.reduce(
|
||||
(acc, obs) => acc.add(obs.calculatedTotalCost ?? new Decimal(0)),
|
||||
new Decimal(0),
|
||||
)
|
||||
.toNumber(),
|
||||
totalCost: includeMetrics
|
||||
? outObservations
|
||||
.reduce(
|
||||
(acc, obs) =>
|
||||
acc.add(obs.calculatedTotalCost ?? new Decimal(0)),
|
||||
new Decimal(0),
|
||||
)
|
||||
.toNumber()
|
||||
: -1,
|
||||
};
|
||||
},
|
||||
}),
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { env } from "@/src/env.mjs";
|
||||
import { aggregateScores } from "@/src/features/scores/lib/aggregateScores";
|
||||
import {
|
||||
AGGREGATABLE_SCORE_TYPES,
|
||||
filterAndValidateDbScoreList,
|
||||
LISTABLE_SCORE_TYPES,
|
||||
} from "@langfuse/shared";
|
||||
import {
|
||||
getObservationsTableWithModelData,
|
||||
@@ -43,7 +43,7 @@ export async function getAllGenerations({
|
||||
|
||||
const validatedScores = filterAndValidateDbScoreList({
|
||||
scores,
|
||||
dataTypes: AGGREGATABLE_SCORE_TYPES,
|
||||
dataTypes: LISTABLE_SCORE_TYPES,
|
||||
includeHasMetadata: true,
|
||||
onParseError: traceException,
|
||||
});
|
||||
|
||||
@@ -19,7 +19,7 @@ import {
|
||||
timeFilter,
|
||||
type SessionOptions,
|
||||
type ScoreDomain,
|
||||
AGGREGATABLE_SCORE_TYPES,
|
||||
LISTABLE_SCORE_TYPES,
|
||||
} from "@langfuse/shared";
|
||||
import { Prisma } from "@langfuse/shared/src/db";
|
||||
import { TRPCError } from "@trpc/server";
|
||||
@@ -127,7 +127,7 @@ const handleGetSessionById = async (input: {
|
||||
|
||||
const validatedScores = filterAndValidateDbScoreList({
|
||||
scores,
|
||||
dataTypes: AGGREGATABLE_SCORE_TYPES,
|
||||
dataTypes: LISTABLE_SCORE_TYPES,
|
||||
onParseError: traceException,
|
||||
});
|
||||
|
||||
@@ -405,7 +405,7 @@ export const sessionRouter = createTRPCRouter({
|
||||
|
||||
const validatedScores = filterAndValidateDbScoreList({
|
||||
scores,
|
||||
dataTypes: AGGREGATABLE_SCORE_TYPES,
|
||||
dataTypes: LISTABLE_SCORE_TYPES,
|
||||
onParseError: traceException,
|
||||
});
|
||||
|
||||
@@ -474,7 +474,7 @@ export const sessionRouter = createTRPCRouter({
|
||||
|
||||
const validatedScores = filterAndValidateDbScoreList({
|
||||
scores,
|
||||
dataTypes: AGGREGATABLE_SCORE_TYPES,
|
||||
dataTypes: LISTABLE_SCORE_TYPES,
|
||||
onParseError: traceException,
|
||||
});
|
||||
|
||||
@@ -657,7 +657,7 @@ export const sessionRouter = createTRPCRouter({
|
||||
|
||||
const validatedScores: ScoreDomain[] = filterAndValidateDbScoreList({
|
||||
scores,
|
||||
dataTypes: AGGREGATABLE_SCORE_TYPES,
|
||||
dataTypes: LISTABLE_SCORE_TYPES,
|
||||
onParseError: traceException,
|
||||
});
|
||||
|
||||
@@ -701,7 +701,7 @@ export const sessionRouter = createTRPCRouter({
|
||||
|
||||
const validatedScores: ScoreDomain[] = filterAndValidateDbScoreList({
|
||||
scores,
|
||||
dataTypes: AGGREGATABLE_SCORE_TYPES,
|
||||
dataTypes: LISTABLE_SCORE_TYPES,
|
||||
onParseError: traceException,
|
||||
});
|
||||
|
||||
@@ -747,7 +747,7 @@ export const sessionRouter = createTRPCRouter({
|
||||
|
||||
const validatedScores = filterAndValidateDbScoreList({
|
||||
scores,
|
||||
dataTypes: AGGREGATABLE_SCORE_TYPES,
|
||||
dataTypes: LISTABLE_SCORE_TYPES,
|
||||
onParseError: traceException,
|
||||
});
|
||||
|
||||
|
||||
@@ -22,8 +22,9 @@ import {
|
||||
type Observation,
|
||||
TracingSearchType,
|
||||
type ScoreDomain,
|
||||
AGGREGATABLE_SCORE_TYPES,
|
||||
ScoreDataTypeArray,
|
||||
ScoreDataTypeEnum,
|
||||
LISTABLE_SCORE_TYPES,
|
||||
} from "@langfuse/shared";
|
||||
import {
|
||||
traceException,
|
||||
@@ -244,7 +245,7 @@ export const traceRouter = createTRPCRouter({
|
||||
|
||||
const validatedScores = filterAndValidateDbScoreList({
|
||||
scores: traceScores,
|
||||
dataTypes: AGGREGATABLE_SCORE_TYPES,
|
||||
dataTypes: LISTABLE_SCORE_TYPES,
|
||||
includeHasMetadata: true,
|
||||
onParseError: traceException,
|
||||
});
|
||||
@@ -379,7 +380,7 @@ export const traceRouter = createTRPCRouter({
|
||||
|
||||
const validatedScores = filterAndValidateDbScoreList({
|
||||
scores: traceScores,
|
||||
dataTypes: [...AGGREGATABLE_SCORE_TYPES, ScoreDataTypeEnum.CORRECTION],
|
||||
dataTypes: [...ScoreDataTypeArray],
|
||||
onParseError: traceException,
|
||||
});
|
||||
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user