Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0e1498a094 | ||
|
|
38f020e482 | ||
|
|
07e1d4e960 | ||
|
|
602d00a5bb | ||
|
|
7395bd8d83 | ||
|
|
63d164ad52 | ||
|
|
f1b915c8b7 | ||
|
|
e3886b94c1 | ||
|
|
44376c2a9f | ||
|
|
2e85c9ac15 | ||
|
|
7cc81deb65 | ||
|
|
a13ddd41ae | ||
|
|
ebe85f2a67 | ||
|
|
c7b62adbab | ||
|
|
66d4926fd0 | ||
|
|
b26f5c512f | ||
|
|
9227eaab9c |
+12
-5
@@ -12,8 +12,15 @@ RUN apt-get update && \
|
||||
postgresql-client \
|
||||
redis-tools \
|
||||
less nano \
|
||||
sudo \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# ---------- Docker -----------------------------------------------------------
|
||||
# Install Docker for background agents that need container capabilities
|
||||
RUN curl -fsSL https://get.docker.com -o get-docker.sh && \
|
||||
sh get-docker.sh && \
|
||||
rm get-docker.sh
|
||||
|
||||
# ---------- pnpm -------------------------------------------------------------
|
||||
# Langfuse monorepo relies on pnpm 9.5.0 (see CONTRIBUTING.md)
|
||||
ENV PNPM_HOME="/pnpm"
|
||||
@@ -29,11 +36,11 @@ RUN wget -qO- "https://github.com/golang-migrate/migrate/releases/download/v${MI
|
||||
chmod +x /usr/local/bin/migrate
|
||||
|
||||
# ---------- Non-root user -----------------------------------------------------
|
||||
# Use root for convenience in development containers
|
||||
WORKDIR /workspace
|
||||
|
||||
# Create non-root user
|
||||
RUN useradd -ms /bin/bash ubuntu
|
||||
# Create non-root user with sudo privileges and docker group access
|
||||
RUN useradd -ms /bin/bash ubuntu && \
|
||||
usermod -aG sudo ubuntu && \
|
||||
usermod -aG docker ubuntu && \
|
||||
echo "ubuntu ALL=(ALL) NOPASSWD:ALL" >> /etc/sudoers
|
||||
|
||||
# Pre-create pnpm store and set correct ownership to avoid first-run cost & permission issues
|
||||
RUN pnpm store path > /dev/null && \
|
||||
|
||||
@@ -4,5 +4,11 @@
|
||||
"context": ".",
|
||||
"dockerfile": "Dockerfile"
|
||||
},
|
||||
"start": "pnpm dx-f"
|
||||
"start": "sudo service docker start",
|
||||
"terminals": [
|
||||
{
|
||||
"name": "dev server",
|
||||
"command": "pnpm run dx-f"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -5,4 +5,5 @@ alwaysApply: true
|
||||
---
|
||||
# General rules
|
||||
|
||||
- Linting in this repo only works if the development server is running
|
||||
- Linting in this repo only works if the development server is running
|
||||
- Always run the full mono-repo via `pnpm run dx` (use `pnpm dx-f` when you run this in a background agent). Thereby the database will also be seeded.
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "langfuse",
|
||||
"version": "3.78.0",
|
||||
"version": "3.78.2",
|
||||
"author": "engineering@langfuse.com",
|
||||
"license": "MIT",
|
||||
"private": true,
|
||||
|
||||
@@ -33,7 +33,7 @@
|
||||
"scripts": {
|
||||
"build": "tsc",
|
||||
"dev": "tsc --watch",
|
||||
"lint": "eslint . --ext .js,.jsx,.ts,.tsx --max-warnings 72",
|
||||
"lint": "eslint . --ext .js,.jsx,.ts,.tsx --max-warnings 0",
|
||||
"lint:fix": "eslint . --ext .js,.jsx,.ts,.tsx --fix",
|
||||
"db:migrate": "DISABLE_ERD=false dotenv -e ../../.env -- npx prisma migrate dev",
|
||||
"db:push": "DISABLE_ERD=false dotenv -e ../../.env -- npx prisma db push",
|
||||
@@ -84,7 +84,7 @@
|
||||
"jsonpath-plus": "10.3.0",
|
||||
"kysely": "^0.27.4",
|
||||
"langchain": "^0.3.28",
|
||||
"langfuse-langchain": "3.37.4",
|
||||
"langfuse-langchain": "3.38.1",
|
||||
"lodash": "^4.17.21",
|
||||
"lossless-json": "^4.1.1",
|
||||
"next-auth": "^4.24.11",
|
||||
|
||||
@@ -178,7 +178,7 @@ async function main() {
|
||||
|
||||
const seedApiKey = {
|
||||
id: "seed-api-key",
|
||||
secret: process.env.SEED_SECRET_KEY ?? "sk-lf-1234567890",
|
||||
secret: process.env.SEED_SECRET_KEY ?? "sk-lf-1234567890", // eslint-disable-line turbo/no-undeclared-env-vars
|
||||
public: "pk-lf-1234567890",
|
||||
note: "seeded key",
|
||||
};
|
||||
@@ -241,7 +241,7 @@ async function main() {
|
||||
|
||||
const secondKey = {
|
||||
id: "seed-api-key-2",
|
||||
secret: process.env.SEED_SECRET_KEY ?? "sk-lf-asdfghjkl",
|
||||
secret: process.env.SEED_SECRET_KEY ?? "sk-lf-asdfghjkl", // eslint-disable-line turbo/no-undeclared-env-vars
|
||||
public: "pk-lf-asdfghjkl",
|
||||
note: "seeded key 2",
|
||||
};
|
||||
@@ -274,7 +274,7 @@ async function main() {
|
||||
await createTraceSessions(project1, project2);
|
||||
|
||||
// If openai key is in environment, add it to the projects LLM API keys
|
||||
const OPENAI_API_KEY = process.env.OPENAI_API_KEY;
|
||||
const OPENAI_API_KEY = process.env.OPENAI_API_KEY; // eslint-disable-line turbo/no-undeclared-env-vars
|
||||
|
||||
if (OPENAI_API_KEY) {
|
||||
await prisma.llmApiKeys.create({
|
||||
|
||||
@@ -88,7 +88,7 @@ declare const globalThis: {
|
||||
kyselyPrismaGlobal: { $kysely: Kysely<DB> } | undefined;
|
||||
} & typeof global;
|
||||
|
||||
if (process.env.NODE_ENV === "development") {
|
||||
if (process.env.NODE_ENV === "development") { // eslint-disable-line turbo/no-undeclared-env-vars
|
||||
globalThis.prismaGlobal ??= createPrismaInstance(); // regular instantiation
|
||||
globalThis.kyselyPrismaGlobal ??= globalThis.prismaGlobal.$extends(
|
||||
kyselyExtension({
|
||||
|
||||
@@ -3,11 +3,11 @@ import { orderBy } from "../interfaces/orderBy";
|
||||
import z from "zod/v4";
|
||||
|
||||
export enum TableViewPresetTableName {
|
||||
Traces = "traces",
|
||||
Observations = "observations",
|
||||
Scores = "scores",
|
||||
Sessions = "sessions",
|
||||
Datasets = "datasets",
|
||||
Traces = "traces", // eslint-disable-line no-unused-vars
|
||||
Observations = "observations", // eslint-disable-line no-unused-vars
|
||||
Scores = "scores", // eslint-disable-line no-unused-vars
|
||||
Sessions = "sessions", // eslint-disable-line no-unused-vars
|
||||
Datasets = "datasets", // eslint-disable-line no-unused-vars
|
||||
}
|
||||
|
||||
const TableViewPresetDomainSchema = z.object({
|
||||
|
||||
@@ -105,10 +105,11 @@ const EnvSchema = z.object({
|
||||
.number()
|
||||
.default(80e6), // 80MB
|
||||
LANGFUSE_CLICKHOUSE_DELETION_TIMEOUT_MS: z.coerce.number().default(240_000), // 4 minutes
|
||||
LANGFUSE_CLICKHOUSE_QUERY_MAX_ATTEMPTS: z.coerce.number().default(3), // Maximum attempts for socket hang up errors
|
||||
LANGFUSE_SKIP_S3_LIST_FOR_OBSERVATIONS_PROJECT_IDS: z.string().optional(),
|
||||
});
|
||||
|
||||
export const env: z.infer<typeof EnvSchema> =
|
||||
process.env.DOCKER_BUILD === "1"
|
||||
process.env.DOCKER_BUILD === "1" // eslint-disable-line turbo/no-undeclared-env-vars
|
||||
? (process.env as any)
|
||||
: EnvSchema.parse(removeEmptyEnvVariables(process.env));
|
||||
|
||||
@@ -7,3 +7,4 @@ export { MethodNotAllowedError } from "./MethodNotAllowedError";
|
||||
export { ApiError } from "./ApiError";
|
||||
export { InternalServerError } from "./InternalServerError";
|
||||
export { LangfuseConflictError } from "./ConflictError";
|
||||
export { QUEUE_ERROR_MESSAGES } from "./utils/constants";
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
export const QUEUE_ERROR_MESSAGES = {
|
||||
API_KEY_ERROR: "API key for provider",
|
||||
NO_DEFAULT_MODEL_ERROR: "No default model or custom model found for project",
|
||||
MAPPED_DATA_ERROR:
|
||||
"Please ensure the mapped data exists and consider extending the job delay.",
|
||||
INVALID_JSON_ERROR: "is not valid JSON",
|
||||
TOO_LOW_MAX_TOKENS_ERROR: "Error: Unterminated string in JSON at position",
|
||||
OUTPUT_TOKENS_TOO_LONG_ERROR:
|
||||
"Could not parse response content as the length limit was reached",
|
||||
};
|
||||
@@ -8,16 +8,16 @@ import { BatchTableNames } from "../../interfaces/tableNames";
|
||||
import { TracingSearchType } from "../../interfaces/search";
|
||||
|
||||
export enum BatchExportStatus {
|
||||
QUEUED = "QUEUED",
|
||||
PROCESSING = "PROCESSING",
|
||||
COMPLETED = "COMPLETED",
|
||||
FAILED = "FAILED",
|
||||
QUEUED = "QUEUED", // eslint-disable-line no-unused-vars
|
||||
PROCESSING = "PROCESSING", // eslint-disable-line no-unused-vars
|
||||
COMPLETED = "COMPLETED", // eslint-disable-line no-unused-vars
|
||||
FAILED = "FAILED", // eslint-disable-line no-unused-vars
|
||||
}
|
||||
|
||||
export enum BatchExportFileFormat {
|
||||
JSON = "JSON",
|
||||
CSV = "CSV",
|
||||
JSONL = "JSONL",
|
||||
JSON = "JSON", // eslint-disable-line no-unused-vars
|
||||
CSV = "CSV", // eslint-disable-line no-unused-vars
|
||||
JSONL = "JSONL", // eslint-disable-line no-unused-vars
|
||||
}
|
||||
|
||||
// Use shared BatchTableNames enum for consistency across batch operations
|
||||
|
||||
@@ -42,7 +42,7 @@ function parseJsonDefault(selectedColumn: unknown, jsonSelector: string) {
|
||||
export function extractValueFromObject(
|
||||
obj: Record<string, unknown>,
|
||||
mapping: z.infer<typeof variableMapping>,
|
||||
parseJson?: (selectedColumn: unknown, jsonSelector: string) => unknown,
|
||||
parseJson?: (selectedColumn: unknown, jsonSelector: string) => unknown, // eslint-disable-line no-unused-vars
|
||||
): { value: string; error: Error | null } {
|
||||
const selectedColumn = obj[mapping.selectedColumnId];
|
||||
const jsonParser = parseJson || parseJsonDefault;
|
||||
|
||||
@@ -37,13 +37,13 @@ const ScorePropsAgainstConfigNumeric = z
|
||||
dataType: z.literal("NUMERIC"),
|
||||
})
|
||||
.superRefine((data, ctx) => {
|
||||
if (isPresent(data.maxValue) && data.value >= data.maxValue) {
|
||||
if (isPresent(data.maxValue) && data.value > data.maxValue) {
|
||||
ctx.addIssue({
|
||||
code: z.ZodIssueCode.custom,
|
||||
message: `Value exceeds maximum value of ${data.maxValue} defined in config`,
|
||||
});
|
||||
}
|
||||
if (isPresent(data.minValue) && data.value <= data.minValue) {
|
||||
if (isPresent(data.minValue) && data.value < data.minValue) {
|
||||
ctx.addIssue({
|
||||
code: z.ZodIssueCode.custom,
|
||||
message: `Value is below minimum value of ${data.minValue} defined in config`,
|
||||
|
||||
@@ -150,7 +150,7 @@ const ValidatedScoreConfigSchema = z
|
||||
*/
|
||||
export const filterAndValidateDbScoreConfigList = (
|
||||
scoreConfigs: ScoreConfigDbType[],
|
||||
onParseError?: (error: z.ZodError) => void,
|
||||
onParseError?: (error: z.ZodError) => void, // eslint-disable-line no-unused-vars
|
||||
): ValidatedScoreConfig[] =>
|
||||
scoreConfigs.reduce((acc, ts) => {
|
||||
const result = ValidatedScoreConfigSchema.safeParse(ts);
|
||||
|
||||
@@ -13,6 +13,7 @@ export * from "./utils/json";
|
||||
export * from "./utils/stringChecks";
|
||||
export * from "./utils/objects";
|
||||
export * from "./utils/typeChecks";
|
||||
export * from "./utils/prompts";
|
||||
export * from "./features/entitlements/plans";
|
||||
export * from "./interfaces/rate-limits";
|
||||
export * from "./tableDefinitions/typeHelpers";
|
||||
@@ -45,7 +46,7 @@ export * from "./features/prompts/parsePromptDependencyTags";
|
||||
export * from "./features/prompts/validation";
|
||||
export * from "./features/prompts/types";
|
||||
export * from "./features/prompts/constants";
|
||||
export * from "./server/llm/compileChatMessages";
|
||||
export { compileChatMessages, compileChatMessagesWithIds, isPlaceholder, type MessagePlaceholderValues, type PromptMessage as ServerPromptMessage } from "./server/llm/compileChatMessages";
|
||||
|
||||
// export db types only
|
||||
export * from "@prisma/client";
|
||||
|
||||
@@ -4,11 +4,11 @@
|
||||
* to avoid coupling between different batch operation types.
|
||||
*/
|
||||
export enum BatchTableNames {
|
||||
Scores = "scores",
|
||||
Sessions = "sessions",
|
||||
Traces = "traces",
|
||||
Observations = "observations",
|
||||
DatasetRunItems = "dataset_run_items",
|
||||
DatasetItems = "dataset_items",
|
||||
AuditLogs = "audit_logs",
|
||||
Scores = "scores", // eslint-disable-line no-unused-vars
|
||||
Sessions = "sessions", // eslint-disable-line no-unused-vars
|
||||
Traces = "traces", // eslint-disable-line no-unused-vars
|
||||
Observations = "observations", // eslint-disable-line no-unused-vars
|
||||
DatasetRunItems = "dataset_run_items", // eslint-disable-line no-unused-vars
|
||||
DatasetItems = "dataset_items", // eslint-disable-line no-unused-vars
|
||||
AuditLogs = "audit_logs", // eslint-disable-line no-unused-vars
|
||||
}
|
||||
|
||||
@@ -45,7 +45,7 @@ const getS3StorageServiceClient = (bucketName: string): StorageService => {
|
||||
return s3StorageServiceClient;
|
||||
};
|
||||
|
||||
export type TokenCountDelegate = (p: {
|
||||
export type TokenCountDelegate = (p: { // eslint-disable-line no-unused-vars
|
||||
model: Model;
|
||||
text: unknown;
|
||||
}) => number | undefined;
|
||||
|
||||
@@ -22,7 +22,7 @@ export type SpanCtx = {
|
||||
traceContext?: TCarrier;
|
||||
};
|
||||
|
||||
type AsyncCallbackFn<T> = (span: opentelemetry.Span) => Promise<T>;
|
||||
type AsyncCallbackFn<T> = (span: opentelemetry.Span) => Promise<T>; // eslint-disable-line no-unused-vars
|
||||
|
||||
export async function instrumentAsync<T>(
|
||||
ctx: SpanCtx,
|
||||
@@ -64,7 +64,7 @@ export async function instrumentAsync<T>(
|
||||
);
|
||||
}
|
||||
|
||||
type SyncCallbackFn<T> = (span: opentelemetry.Span) => T;
|
||||
type SyncCallbackFn<T> = (span: opentelemetry.Span) => T; // eslint-disable-line no-unused-vars
|
||||
|
||||
export function instrumentSync<T>(
|
||||
ctx: SpanCtx,
|
||||
|
||||
@@ -63,7 +63,7 @@ type FetchLLMCompletionParams = LLMCompletionParams & {
|
||||
};
|
||||
|
||||
export async function fetchLLMCompletion(
|
||||
params: LLMCompletionParams & {
|
||||
params: LLMCompletionParams & { // eslint-disable-line no-unused-vars
|
||||
streaming: true;
|
||||
},
|
||||
): Promise<{
|
||||
@@ -72,7 +72,7 @@ export async function fetchLLMCompletion(
|
||||
}>;
|
||||
|
||||
export async function fetchLLMCompletion(
|
||||
params: LLMCompletionParams & {
|
||||
params: LLMCompletionParams & { // eslint-disable-line no-unused-vars
|
||||
streaming: false;
|
||||
},
|
||||
): Promise<{
|
||||
@@ -81,7 +81,7 @@ export async function fetchLLMCompletion(
|
||||
}>;
|
||||
|
||||
export async function fetchLLMCompletion(
|
||||
params: LLMCompletionParams & {
|
||||
params: LLMCompletionParams & { // eslint-disable-line no-unused-vars
|
||||
streaming: false;
|
||||
structuredOutputSchema: ZodSchema;
|
||||
},
|
||||
@@ -91,7 +91,7 @@ export async function fetchLLMCompletion(
|
||||
}>;
|
||||
|
||||
export async function fetchLLMCompletion(
|
||||
params: LLMCompletionParams & {
|
||||
params: LLMCompletionParams & { // eslint-disable-line no-unused-vars
|
||||
tools: LLMToolDefinition[];
|
||||
streaming: false;
|
||||
},
|
||||
|
||||
@@ -432,19 +432,19 @@ export class FilterList {
|
||||
this.filters.push(...filter);
|
||||
}
|
||||
|
||||
find(predicate: (filter: Filter) => boolean) {
|
||||
find(predicate: (filter: Filter) => boolean) { // eslint-disable-line no-unused-vars
|
||||
return this.filters.find(predicate);
|
||||
}
|
||||
|
||||
filter(predicate: (filter: Filter) => boolean) {
|
||||
filter(predicate: (filter: Filter) => boolean) { // eslint-disable-line no-unused-vars
|
||||
return new FilterList(this.filters.filter(predicate));
|
||||
}
|
||||
|
||||
some(predicate: (filter: Filter) => boolean) {
|
||||
some(predicate: (filter: Filter) => boolean) { // eslint-disable-line no-unused-vars
|
||||
return this.filters.some(predicate);
|
||||
}
|
||||
|
||||
forEach(callback: (filter: Filter) => void) {
|
||||
forEach(callback: (filter: Filter) => void) { // eslint-disable-line no-unused-vars
|
||||
this.filters.forEach(callback);
|
||||
}
|
||||
|
||||
|
||||
@@ -70,8 +70,9 @@ export function getQueue(
|
||||
return ScoreDeleteQueue.getInstance();
|
||||
case QueueName.DeadLetterRetryQueue:
|
||||
return DeadLetterRetryQueue.getInstance();
|
||||
default:
|
||||
const exhaustiveCheckDefault: never = queueName;
|
||||
default: {
|
||||
const exhaustiveCheckDefault: never = queueName; // eslint-disable-line no-case-declarations, no-unused-vars
|
||||
throw new Error(`Queue ${queueName} not found`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -167,9 +167,9 @@ const createRedisClient = () => {
|
||||
|
||||
declare global {
|
||||
// eslint-disable-next-line no-var
|
||||
var redis: undefined | ReturnType<typeof createRedisClient>;
|
||||
var redis: undefined | ReturnType<typeof createRedisClient>; // eslint-disable-line no-unused-vars
|
||||
}
|
||||
|
||||
export const redis = globalThis.redis ?? createRedisClient();
|
||||
export const redis = globalThis.redis ?? createRedisClient(); // eslint-disable-line no-undef
|
||||
|
||||
if (env.NODE_ENV !== "production") globalThis.redis = redis;
|
||||
if (env.NODE_ENV !== "production") globalThis.redis = redis; // eslint-disable-line no-undef
|
||||
|
||||
@@ -9,6 +9,7 @@ import { randomUUID } from "crypto";
|
||||
import { getClickhouseEntityType } from "../clickhouse/schemaUtils";
|
||||
import { NodeClickHouseClientConfigOptions } from "@clickhouse/client/dist/config";
|
||||
import { context, SpanKind, trace } from "@opentelemetry/api";
|
||||
import { backOff } from "exponential-backoff";
|
||||
import {
|
||||
StorageService,
|
||||
StorageServiceFactory,
|
||||
@@ -203,6 +204,18 @@ export async function* queryClickhouseStream<T>(opts: {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines if an error is retryable (socket hang up, connection reset, etc.)
|
||||
*/
|
||||
function isRetryableError(error: unknown): boolean {
|
||||
if (!error || typeof error !== "object") return false;
|
||||
|
||||
const errorMessage = (error as Error).message?.toLowerCase() || "";
|
||||
|
||||
// Check for socket hang up and other network-related errors
|
||||
return errorMessage.includes("socket hang up");
|
||||
}
|
||||
|
||||
export async function queryClickhouse<T>(opts: {
|
||||
query: string;
|
||||
params?: Record<string, unknown> | undefined;
|
||||
@@ -218,40 +231,78 @@ export async function queryClickhouse<T>(opts: {
|
||||
span.setAttribute("db.query.text", opts.query);
|
||||
span.setAttribute("db.operation.name", "SELECT");
|
||||
|
||||
const res = await clickhouseClient(opts.clickhouseConfigs).query({
|
||||
query: opts.query,
|
||||
format: "JSONEachRow",
|
||||
query_params: opts.params,
|
||||
clickhouse_settings: {
|
||||
log_comment: JSON.stringify(opts.tags ?? {}),
|
||||
},
|
||||
});
|
||||
// same logic as for prisma. we want to see queries in development
|
||||
if (env.NODE_ENV === "development") {
|
||||
logger.info(`clickhouse:query ${res.query_id} ${opts.query}`);
|
||||
}
|
||||
// Retry logic for socket hang up and other network errors
|
||||
return await backOff(
|
||||
async () => {
|
||||
const res = await clickhouseClient(opts.clickhouseConfigs).query({
|
||||
query: opts.query,
|
||||
format: "JSONEachRow",
|
||||
query_params: opts.params,
|
||||
clickhouse_settings: {
|
||||
log_comment: JSON.stringify(opts.tags ?? {}),
|
||||
},
|
||||
});
|
||||
|
||||
span.setAttribute("ch.queryId", res.query_id);
|
||||
|
||||
// add summary headers to the span. Helps to tune performance
|
||||
const summaryHeader = res.response_headers["x-clickhouse-summary"];
|
||||
if (summaryHeader) {
|
||||
try {
|
||||
const summary = Array.isArray(summaryHeader)
|
||||
? JSON.parse(summaryHeader[0])
|
||||
: JSON.parse(summaryHeader);
|
||||
for (const key in summary) {
|
||||
span.setAttribute(`ch.${key}`, summary[key]);
|
||||
// same logic as for prisma. we want to see queries in development
|
||||
if (env.NODE_ENV === "development") {
|
||||
logger.info(`clickhouse:query ${res.query_id} ${opts.query}`);
|
||||
}
|
||||
} catch (error) {
|
||||
logger.debug(
|
||||
`Failed to parse clickhouse summary header ${summaryHeader}`,
|
||||
error,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return await res.json<T>();
|
||||
span.setAttribute("ch.queryId", res.query_id);
|
||||
|
||||
// add summary headers to the span. Helps to tune performance
|
||||
const summaryHeader = res.response_headers["x-clickhouse-summary"];
|
||||
if (summaryHeader) {
|
||||
try {
|
||||
const summary = Array.isArray(summaryHeader)
|
||||
? JSON.parse(summaryHeader[0])
|
||||
: JSON.parse(summaryHeader);
|
||||
for (const key in summary) {
|
||||
span.setAttribute(`ch.${key}`, summary[key]);
|
||||
}
|
||||
} catch (error) {
|
||||
logger.debug(
|
||||
`Failed to parse clickhouse summary header ${summaryHeader}`,
|
||||
error,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return await res.json<T>();
|
||||
},
|
||||
{
|
||||
numOfAttempts: env.LANGFUSE_CLICKHOUSE_QUERY_MAX_ATTEMPTS,
|
||||
retry: (error: Error, attemptNumber: number) => {
|
||||
const shouldRetry = isRetryableError(error);
|
||||
if (shouldRetry) {
|
||||
logger.warn(
|
||||
`ClickHouse query failed with retryable error (attempt ${attemptNumber}/${env.LANGFUSE_CLICKHOUSE_QUERY_MAX_ATTEMPTS}): ${error.message}`,
|
||||
{
|
||||
error: error.message,
|
||||
attemptNumber,
|
||||
tags: opts.tags,
|
||||
},
|
||||
);
|
||||
span.addEvent("clickhouse-query-retry", {
|
||||
"retry.attempt": attemptNumber,
|
||||
"retry.error": error.message,
|
||||
});
|
||||
} else {
|
||||
logger.error(
|
||||
`ClickHouse query failed with non-retryable error: ${error.message}`,
|
||||
{
|
||||
error: error.message,
|
||||
tags: opts.tags,
|
||||
},
|
||||
);
|
||||
}
|
||||
return shouldRetry;
|
||||
},
|
||||
startingDelay: 100,
|
||||
timeMultiple: 1,
|
||||
maxDelay: 100,
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
+3
-3
@@ -1,6 +1,6 @@
|
||||
import z from "zod/v4";
|
||||
import { prisma } from "../../../db";
|
||||
import { LangfuseNotFoundError } from "../../../errors";
|
||||
import { LangfuseNotFoundError, QUEUE_ERROR_MESSAGES } from "../../../errors";
|
||||
import { LLMApiKeySchema, ZodModelConfig } from "../../llm/types";
|
||||
|
||||
type ValidConfig = {
|
||||
@@ -156,7 +156,7 @@ export class DefaultEvalModelService {
|
||||
if (!selectedModel) {
|
||||
return {
|
||||
valid: false,
|
||||
error: `No default model or custom model found for project ${projectId}.`,
|
||||
error: `${QUEUE_ERROR_MESSAGES.NO_DEFAULT_MODEL_ERROR} ${projectId}.`,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -173,7 +173,7 @@ export class DefaultEvalModelService {
|
||||
if (!parsedKey.success) {
|
||||
return {
|
||||
valid: false,
|
||||
error: `API key for provider "${selectedModel.provider}" not found in project ${projectId}.`,
|
||||
error: `${QUEUE_ERROR_MESSAGES.API_KEY_ERROR} "${selectedModel.provider}" not found in project ${projectId}.`,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -13,7 +13,7 @@ export class InMemoryFilterService {
|
||||
static evaluateFilter<T>(
|
||||
data: T,
|
||||
filter: FilterState,
|
||||
fieldMapper: (data: T, column: string) => unknown,
|
||||
fieldMapper: (data: T, column: string) => unknown, // eslint-disable-line no-unused-vars
|
||||
): boolean {
|
||||
try {
|
||||
// If no filters, data matches
|
||||
@@ -45,7 +45,7 @@ export class InMemoryFilterService {
|
||||
private static evaluateFilterCondition<T>(
|
||||
data: T,
|
||||
condition: FilterCondition,
|
||||
fieldMapper: (data: T, column: string) => unknown,
|
||||
fieldMapper: (data: T, column: string) => unknown, // eslint-disable-line no-unused-vars
|
||||
): boolean {
|
||||
const { column, type, operator } = condition;
|
||||
|
||||
|
||||
@@ -13,8 +13,8 @@ export type PromptParams = {
|
||||
);
|
||||
|
||||
export enum PromptServiceMetrics {
|
||||
PromptCacheHit = "prompt_cache_hit",
|
||||
PromptCacheMiss = "prompt_cache_miss",
|
||||
PromptCacheHit = "prompt_cache_hit", // eslint-disable-line no-unused-vars
|
||||
PromptCacheMiss = "prompt_cache_miss", // eslint-disable-line no-unused-vars
|
||||
}
|
||||
|
||||
export type PartialPrompt = Pick<
|
||||
|
||||
@@ -28,21 +28,21 @@ type UploadFile = {
|
||||
};
|
||||
|
||||
export interface StorageService {
|
||||
uploadFile(params: UploadFile): Promise<{ signedUrl: string }>;
|
||||
uploadFile(params: UploadFile): Promise<{ signedUrl: string }>; // eslint-disable-line no-unused-vars
|
||||
|
||||
uploadJson(path: string, body: Record<string, unknown>[]): Promise<void>;
|
||||
uploadJson(path: string, body: Record<string, unknown>[]): Promise<void>; // eslint-disable-line no-unused-vars
|
||||
|
||||
download(path: string): Promise<string>;
|
||||
download(path: string): Promise<string>; // eslint-disable-line no-unused-vars
|
||||
|
||||
listFiles(prefix: string): Promise<{ file: string; createdAt: Date }[]>;
|
||||
listFiles(prefix: string): Promise<{ file: string; createdAt: Date }[]>; // eslint-disable-line no-unused-vars
|
||||
|
||||
getSignedUrl(
|
||||
fileName: string,
|
||||
ttlSeconds: number,
|
||||
asAttachment?: boolean,
|
||||
fileName: string, // eslint-disable-line no-unused-vars
|
||||
ttlSeconds: number, // eslint-disable-line no-unused-vars
|
||||
asAttachment?: boolean, // eslint-disable-line no-unused-vars
|
||||
): Promise<string>;
|
||||
|
||||
getSignedUploadUrl(params: {
|
||||
getSignedUploadUrl(params: { // eslint-disable-line no-unused-vars
|
||||
path: string;
|
||||
ttlSeconds: number;
|
||||
sha256Hash: string;
|
||||
@@ -50,7 +50,7 @@ export interface StorageService {
|
||||
contentLength: number;
|
||||
}): Promise<string>;
|
||||
|
||||
deleteFiles(paths: string[]): Promise<void>;
|
||||
deleteFiles(paths: string[]): Promise<void>; // eslint-disable-line no-unused-vars
|
||||
}
|
||||
|
||||
export class StorageServiceFactory {
|
||||
@@ -213,7 +213,7 @@ class AzureBlobStorageService implements StorageService {
|
||||
}
|
||||
|
||||
private async streamToString(
|
||||
readableStream: NodeJS.ReadableStream,
|
||||
readableStream: NodeJS.ReadableStream, // eslint-disable-line no-undef
|
||||
): Promise<string> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const chunks: string[] = [];
|
||||
|
||||
@@ -24,12 +24,12 @@ export class DatabaseReadStream<EntityType> extends Readable {
|
||||
|
||||
constructor(
|
||||
// the delegate function takes care of querying the database in a paginated manner
|
||||
private queryDelegate: (
|
||||
pageSize: number,
|
||||
offset: number
|
||||
private queryDelegate: ( // eslint-disable-line no-unused-vars
|
||||
pageSize: number, // eslint-disable-line no-unused-vars
|
||||
offset: number // eslint-disable-line no-unused-vars
|
||||
) => Promise<Array<EntityType>>,
|
||||
private pageSize: number,
|
||||
private maxRecords?: number
|
||||
private pageSize: number, // eslint-disable-line no-unused-vars
|
||||
private maxRecords?: number // eslint-disable-line no-unused-vars
|
||||
) {
|
||||
super({ objectMode: true }); // Set object mode to true to allow pushing objects to the stream rather than strings or buffers
|
||||
|
||||
|
||||
@@ -11,7 +11,7 @@ export function transformStreamToCsv(): Transform {
|
||||
objectMode: true,
|
||||
transform(
|
||||
row: Record<string, any>,
|
||||
encoding: BufferEncoding,
|
||||
encoding: BufferEncoding, // eslint-disable-line no-undef
|
||||
callback: TransformCallback,
|
||||
): void {
|
||||
if (isFirstChunk) {
|
||||
|
||||
@@ -9,8 +9,8 @@ export function transformStreamToJson(): Transform {
|
||||
|
||||
transform(
|
||||
row: any,
|
||||
encoding: BufferEncoding,
|
||||
callback: TransformCallback
|
||||
encoding: BufferEncoding, // eslint-disable-line no-undef, no-unused-vars
|
||||
callback: TransformCallback,
|
||||
): void {
|
||||
if (isFirstElement) {
|
||||
this.push("["); // Push the opening bracket for the first element
|
||||
|
||||
@@ -7,7 +7,7 @@ export function transformStreamToJsonl(): Transform {
|
||||
|
||||
transform(
|
||||
row: Record<string, any>,
|
||||
encoding: BufferEncoding,
|
||||
encoding: BufferEncoding, // eslint-disable-line no-undef, no-unused-vars
|
||||
callback: TransformCallback,
|
||||
): void {
|
||||
this.push(stringify(row) + "\n");
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
/**
|
||||
* Client-safe utility functions for prompt handling
|
||||
*/
|
||||
|
||||
export interface PromptMessage {
|
||||
type?: string;
|
||||
name?: string;
|
||||
role?: string;
|
||||
content?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Extracts placeholder names from prompt messages.
|
||||
* This is a client-safe version that doesn't depend on server-side types.
|
||||
* @param messages Array of prompt messages
|
||||
* @returns Array of placeholder names
|
||||
*/
|
||||
export function extractPlaceholderNames(messages: PromptMessage[]): string[] {
|
||||
return messages
|
||||
.filter((msg): msg is PromptMessage & { name: string } =>
|
||||
msg.type === "placeholder" && typeof msg.name === "string"
|
||||
)
|
||||
.map(msg => msg.name);
|
||||
}
|
||||
Generated
+214
-132
@@ -112,7 +112,7 @@ importers:
|
||||
version: 7.12.0(eslint@8.57.0)(typescript@5.4.5)
|
||||
'@vercel/style-guide':
|
||||
specifier: ^6.0.0
|
||||
version: 6.0.0(@next/eslint-plugin-next@14.2.15)(eslint@8.57.0)(jest@29.7.0(@types/node@20.14.8))(prettier@3.5.3)(typescript@5.4.5)(vitest@2.1.2(@types/node@20.14.8))
|
||||
version: 6.0.0(@next/eslint-plugin-next@14.2.15)(eslint@8.57.0)(jest@29.7.0(@types/node@20.14.8)(babel-plugin-macros@3.1.0))(prettier@3.5.3)(typescript@5.4.5)(vitest@2.1.2(@types/node@20.14.8)(jsdom@20.0.3)(msw@2.6.5(@types/node@20.14.8)(typescript@5.4.5))(terser@5.43.1))
|
||||
eslint-config-next:
|
||||
specifier: ^14.2.15
|
||||
version: 14.2.15(eslint@8.57.0)(typescript@5.4.5)
|
||||
@@ -224,8 +224,8 @@ importers:
|
||||
specifier: ^0.3.28
|
||||
version: 0.3.28(@langchain/anthropic@0.3.22(@langchain/core@0.3.58(openai@4.104.0(zod@3.25.62))))(@langchain/aws@0.1.11(@langchain/core@0.3.58(openai@4.104.0(zod@3.25.62))))(@langchain/core@0.3.58(openai@4.104.0(zod@3.25.62)))(@langchain/google-genai@0.2.12(@langchain/core@0.3.58(openai@4.104.0(zod@3.25.62))))(@langchain/google-vertexai@0.2.12(@langchain/core@0.3.58(openai@4.104.0(zod@3.25.62)))(zod@3.25.62))(axios@1.8.2)(cheerio@1.0.0)(handlebars@4.7.8)(openai@4.104.0(zod@3.25.62))
|
||||
langfuse-langchain:
|
||||
specifier: 3.37.4
|
||||
version: 3.37.4(langchain@0.3.28(@langchain/anthropic@0.3.22(@langchain/core@0.3.58(openai@4.104.0(zod@3.25.62))))(@langchain/aws@0.1.11(@langchain/core@0.3.58(openai@4.104.0(zod@3.25.62))))(@langchain/core@0.3.58(openai@4.104.0(zod@3.25.62)))(@langchain/google-genai@0.2.12(@langchain/core@0.3.58(openai@4.104.0(zod@3.25.62))))(@langchain/google-vertexai@0.2.12(@langchain/core@0.3.58(openai@4.104.0(zod@3.25.62)))(zod@3.25.62))(axios@1.8.2)(cheerio@1.0.0)(handlebars@4.7.8)(openai@4.104.0(zod@3.25.62)))
|
||||
specifier: 3.38.1
|
||||
version: 3.38.1(langchain@0.3.28(@langchain/anthropic@0.3.22(@langchain/core@0.3.58(openai@4.104.0(zod@3.25.62))))(@langchain/aws@0.1.11(@langchain/core@0.3.58(openai@4.104.0(zod@3.25.62))))(@langchain/core@0.3.58(openai@4.104.0(zod@3.25.62)))(@langchain/google-genai@0.2.12(@langchain/core@0.3.58(openai@4.104.0(zod@3.25.62))))(@langchain/google-vertexai@0.2.12(@langchain/core@0.3.58(openai@4.104.0(zod@3.25.62)))(zod@3.25.62))(axios@1.8.2)(cheerio@1.0.0)(handlebars@4.7.8)(openai@4.104.0(zod@3.25.62)))
|
||||
lodash:
|
||||
specifier: ^4.17.21
|
||||
version: 4.17.21
|
||||
@@ -328,7 +328,7 @@ importers:
|
||||
version: 5.4.5
|
||||
vitest:
|
||||
specifier: ^2.1.2
|
||||
version: 2.1.2(@types/node@20.11.29)(jsdom@20.0.3)(msw@2.6.5(@types/node@20.11.29)(typescript@5.4.5))(terser@5.43.0)
|
||||
version: 2.1.2(@types/node@20.11.29)(jsdom@20.0.3)(msw@2.6.5(@types/node@20.11.29)(typescript@5.4.5))(terser@5.43.1)
|
||||
|
||||
web:
|
||||
dependencies:
|
||||
@@ -758,7 +758,7 @@ importers:
|
||||
version: 0.5.7(tailwindcss@3.4.17(ts-node@10.9.2(@types/node@20.10.5)(typescript@5.4.5)))
|
||||
'@testing-library/jest-dom':
|
||||
specifier: ^6.4.6
|
||||
version: 6.4.6(@jest/globals@29.7.0)(@types/jest@29.5.12)(jest@29.7.0(@types/node@20.10.5)(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@types/node@20.10.5)(typescript@5.4.5)))(vitest@2.1.2(@types/node@20.10.5))
|
||||
version: 6.4.6(@jest/globals@29.7.0)(@types/jest@29.5.12)(jest@29.7.0(@types/node@20.10.5)(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@types/node@20.10.5)(typescript@5.4.5)))(vitest@2.1.2(@types/node@20.10.5)(jsdom@20.0.3)(msw@2.6.5(@types/node@20.10.5)(typescript@5.4.5))(terser@5.43.1))
|
||||
'@testing-library/react':
|
||||
specifier: ^15.0.7
|
||||
version: 15.0.7(@types/react@18.2.79)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
|
||||
@@ -1038,7 +1038,7 @@ importers:
|
||||
version: 5.4.5
|
||||
vitest:
|
||||
specifier: ^2.1.2
|
||||
version: 2.1.2(@types/node@20.11.29)(jsdom@20.0.3)(msw@2.6.5(@types/node@20.11.29)(typescript@5.4.5))(terser@5.43.0)
|
||||
version: 2.1.2(@types/node@20.11.29)(jsdom@20.0.3)(msw@2.6.5(@types/node@20.11.29)(typescript@5.4.5))(terser@5.43.1)
|
||||
wait-for-expect:
|
||||
specifier: ^3.0.2
|
||||
version: 3.0.2
|
||||
@@ -8936,18 +8936,18 @@ packages:
|
||||
typeorm:
|
||||
optional: true
|
||||
|
||||
langfuse-core@3.37.4:
|
||||
resolution: {integrity: sha512-yqXkQ1/8V9TxF8q6oULWPQ4cYZlUs1egN6JfGuhJSWb5x2Ymgt4Qh9of/Kz//CTM7IZgzZqvrlnx0VXhNKDITg==}
|
||||
langfuse-core@3.38.1:
|
||||
resolution: {integrity: sha512-qZsHZZ05eHv+EQoPmfrm6bWUflzvlCSAfT8EP1PP1WeHSwEcwT4p7qAs9H0EnWcUtm0dAdyd6U75HGWu3/ETUA==}
|
||||
engines: {node: '>=18'}
|
||||
|
||||
langfuse-langchain@3.37.4:
|
||||
resolution: {integrity: sha512-yh2+Rg+NqN6k+E29VW9lnLp9qpldBE3Ldhh/qsvInyk0hdhF64I/z0hWPFY4tYNvbzBXDHgApJ+66nSnXBf4Tg==}
|
||||
langfuse-langchain@3.38.1:
|
||||
resolution: {integrity: sha512-lkgEwUuGEtkmtxcMhfFn90FRAlGOSGVWczyBFMXck+1YcqnYLIuE/MNg3ufN54yPRVJFfEXj0kesZSLERGQ8eQ==}
|
||||
engines: {node: '>=18'}
|
||||
peerDependencies:
|
||||
langchain: '>=0.0.157 <0.4.0'
|
||||
|
||||
langfuse@3.37.4:
|
||||
resolution: {integrity: sha512-MxnJXkS3xqlydMKWouanghkWZb8OqoDns+NIAZoD+y/WQh1mtx40n8c0dOaT7OahzSfB9ZbWj+j+yqJww/PJYQ==}
|
||||
langfuse@3.38.1:
|
||||
resolution: {integrity: sha512-rv0x+NWi4JDN8KUyGcl8JQ7xmM/0UBXEcYmtBR+msLLImsM3I07CPE6imp3N2I4AbByPNKF4IU8Kq/LFS3D34A==}
|
||||
engines: {node: '>=18'}
|
||||
|
||||
langium@3.0.0:
|
||||
@@ -11310,11 +11310,6 @@ packages:
|
||||
uglify-js:
|
||||
optional: true
|
||||
|
||||
terser@5.43.0:
|
||||
resolution: {integrity: sha512-CqNNxKSGKSZCunSvwKLTs8u8sGGlp27sxNZ4quGh0QeNuyHM0JSEM/clM9Mf4zUp6J+tO2gUXhgXT2YMMkwfKQ==}
|
||||
engines: {node: '>=10'}
|
||||
hasBin: true
|
||||
|
||||
terser@5.43.1:
|
||||
resolution: {integrity: sha512-+6erLbBm0+LROX2sPXlUYx/ux5PyE9K/a92Wrt6oA+WDAoFTdpHE5tCYCI5PNzq2y8df4rA+QgHLJuR4jNymsg==}
|
||||
engines: {node: '>=10'}
|
||||
@@ -14359,12 +14354,26 @@ snapshots:
|
||||
optionalDependencies:
|
||||
'@types/node': 20.14.8
|
||||
|
||||
'@inquirer/confirm@5.0.2(@types/node@20.10.5)':
|
||||
dependencies:
|
||||
'@inquirer/core': 10.1.0(@types/node@20.10.5)
|
||||
'@inquirer/type': 3.0.1(@types/node@20.10.5)
|
||||
'@types/node': 20.10.5
|
||||
optional: true
|
||||
|
||||
'@inquirer/confirm@5.0.2(@types/node@20.11.29)':
|
||||
dependencies:
|
||||
'@inquirer/core': 10.1.0(@types/node@20.11.29)
|
||||
'@inquirer/type': 3.0.1(@types/node@20.11.29)
|
||||
'@types/node': 20.11.29
|
||||
|
||||
'@inquirer/confirm@5.0.2(@types/node@20.14.8)':
|
||||
dependencies:
|
||||
'@inquirer/core': 10.1.0(@types/node@20.14.8)
|
||||
'@inquirer/type': 3.0.1(@types/node@20.14.8)
|
||||
'@types/node': 20.14.8
|
||||
optional: true
|
||||
|
||||
'@inquirer/confirm@5.1.12(@types/node@20.14.8)':
|
||||
dependencies:
|
||||
'@inquirer/core': 10.1.13(@types/node@20.14.8)
|
||||
@@ -14372,6 +14381,21 @@ snapshots:
|
||||
optionalDependencies:
|
||||
'@types/node': 20.14.8
|
||||
|
||||
'@inquirer/core@10.1.0(@types/node@20.10.5)':
|
||||
dependencies:
|
||||
'@inquirer/figures': 1.0.8
|
||||
'@inquirer/type': 3.0.4(@types/node@20.10.5)
|
||||
ansi-escapes: 4.3.2
|
||||
cli-width: 4.1.0
|
||||
mute-stream: 2.0.0
|
||||
signal-exit: 4.1.0
|
||||
strip-ansi: 6.0.1
|
||||
wrap-ansi: 6.2.0
|
||||
yoctocolors-cjs: 2.1.2
|
||||
transitivePeerDependencies:
|
||||
- '@types/node'
|
||||
optional: true
|
||||
|
||||
'@inquirer/core@10.1.0(@types/node@20.11.29)':
|
||||
dependencies:
|
||||
'@inquirer/figures': 1.0.8
|
||||
@@ -14386,6 +14410,21 @@ snapshots:
|
||||
transitivePeerDependencies:
|
||||
- '@types/node'
|
||||
|
||||
'@inquirer/core@10.1.0(@types/node@20.14.8)':
|
||||
dependencies:
|
||||
'@inquirer/figures': 1.0.8
|
||||
'@inquirer/type': 3.0.4(@types/node@20.14.8)
|
||||
ansi-escapes: 4.3.2
|
||||
cli-width: 4.1.0
|
||||
mute-stream: 2.0.0
|
||||
signal-exit: 4.1.0
|
||||
strip-ansi: 6.0.1
|
||||
wrap-ansi: 6.2.0
|
||||
yoctocolors-cjs: 2.1.2
|
||||
transitivePeerDependencies:
|
||||
- '@types/node'
|
||||
optional: true
|
||||
|
||||
'@inquirer/core@10.1.13(@types/node@20.14.8)':
|
||||
dependencies:
|
||||
'@inquirer/figures': 1.0.12
|
||||
@@ -14483,14 +14522,34 @@ snapshots:
|
||||
optionalDependencies:
|
||||
'@types/node': 20.14.8
|
||||
|
||||
'@inquirer/type@3.0.1(@types/node@20.10.5)':
|
||||
dependencies:
|
||||
'@types/node': 20.10.5
|
||||
optional: true
|
||||
|
||||
'@inquirer/type@3.0.1(@types/node@20.11.29)':
|
||||
dependencies:
|
||||
'@types/node': 20.11.29
|
||||
|
||||
'@inquirer/type@3.0.1(@types/node@20.14.8)':
|
||||
dependencies:
|
||||
'@types/node': 20.14.8
|
||||
optional: true
|
||||
|
||||
'@inquirer/type@3.0.4(@types/node@20.10.5)':
|
||||
optionalDependencies:
|
||||
'@types/node': 20.10.5
|
||||
optional: true
|
||||
|
||||
'@inquirer/type@3.0.4(@types/node@20.11.29)':
|
||||
optionalDependencies:
|
||||
'@types/node': 20.11.29
|
||||
|
||||
'@inquirer/type@3.0.4(@types/node@20.14.8)':
|
||||
optionalDependencies:
|
||||
'@types/node': 20.14.8
|
||||
optional: true
|
||||
|
||||
'@inquirer/type@3.0.7(@types/node@20.14.8)':
|
||||
optionalDependencies:
|
||||
'@types/node': 20.14.8
|
||||
@@ -17685,7 +17744,7 @@ snapshots:
|
||||
lz-string: 1.5.0
|
||||
pretty-format: 27.5.1
|
||||
|
||||
'@testing-library/jest-dom@6.4.6(@jest/globals@29.7.0)(@types/jest@29.5.12)(jest@29.7.0(@types/node@20.10.5)(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@types/node@20.10.5)(typescript@5.4.5)))(vitest@2.1.2(@types/node@20.10.5))':
|
||||
'@testing-library/jest-dom@6.4.6(@jest/globals@29.7.0)(@types/jest@29.5.12)(jest@29.7.0(@types/node@20.10.5)(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@types/node@20.10.5)(typescript@5.4.5)))(vitest@2.1.2(@types/node@20.10.5)(jsdom@20.0.3)(msw@2.6.5(@types/node@20.10.5)(typescript@5.4.5))(terser@5.43.1))':
|
||||
dependencies:
|
||||
'@adobe/css-tools': 4.4.0
|
||||
'@babel/runtime': 7.24.7
|
||||
@@ -17699,7 +17758,7 @@ snapshots:
|
||||
'@jest/globals': 29.7.0
|
||||
'@types/jest': 29.5.12
|
||||
jest: 29.7.0(@types/node@20.10.5)(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@types/node@20.10.5)(typescript@5.4.5))
|
||||
vitest: 2.1.2(@types/node@20.10.5)
|
||||
vitest: 2.1.2(@types/node@20.10.5)(jsdom@20.0.3)(msw@2.6.5(@types/node@20.10.5)(typescript@5.4.5))(terser@5.43.1)
|
||||
|
||||
'@testing-library/react@15.0.7(@types/react@18.2.79)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)':
|
||||
dependencies:
|
||||
@@ -18358,7 +18417,7 @@ snapshots:
|
||||
|
||||
'@ungap/structured-clone@1.2.0': {}
|
||||
|
||||
'@vercel/style-guide@6.0.0(@next/eslint-plugin-next@14.2.15)(eslint@8.57.0)(jest@29.7.0(@types/node@20.14.8))(prettier@3.5.3)(typescript@5.4.5)(vitest@2.1.2(@types/node@20.14.8))':
|
||||
'@vercel/style-guide@6.0.0(@next/eslint-plugin-next@14.2.15)(eslint@8.57.0)(jest@29.7.0(@types/node@20.14.8)(babel-plugin-macros@3.1.0))(prettier@3.5.3)(typescript@5.4.5)(vitest@2.1.2(@types/node@20.14.8)(jsdom@20.0.3)(msw@2.6.5(@types/node@20.14.8)(typescript@5.4.5))(terser@5.43.1))':
|
||||
dependencies:
|
||||
'@babel/core': 7.24.3
|
||||
'@babel/eslint-parser': 7.24.1(@babel/core@7.24.3)(eslint@8.57.0)
|
||||
@@ -18366,19 +18425,19 @@ snapshots:
|
||||
'@typescript-eslint/eslint-plugin': 7.3.1(@typescript-eslint/parser@7.12.0(eslint@8.57.0)(typescript@5.4.5))(eslint@8.57.0)(typescript@5.4.5)
|
||||
'@typescript-eslint/parser': 7.12.0(eslint@8.57.0)(typescript@5.4.5)
|
||||
eslint-config-prettier: 9.1.0(eslint@8.57.0)
|
||||
eslint-import-resolver-alias: 1.1.2(eslint-plugin-import@2.29.1(@typescript-eslint/parser@7.12.0(eslint@8.57.0)(typescript@5.4.5))(eslint-import-resolver-typescript@3.6.1)(eslint@8.57.0))
|
||||
eslint-import-resolver-alias: 1.1.2(eslint-plugin-import@2.29.1)
|
||||
eslint-import-resolver-typescript: 3.6.1(@typescript-eslint/parser@7.12.0(eslint@8.57.0)(typescript@5.4.5))(eslint-plugin-import@2.29.1)(eslint@8.57.0)
|
||||
eslint-plugin-eslint-comments: 3.2.0(eslint@8.57.0)
|
||||
eslint-plugin-import: 2.29.1(@typescript-eslint/parser@7.12.0(eslint@8.57.0)(typescript@5.4.5))(eslint-import-resolver-typescript@3.6.1)(eslint@8.57.0)
|
||||
eslint-plugin-jest: 27.9.0(@typescript-eslint/eslint-plugin@7.3.1(@typescript-eslint/parser@7.12.0(eslint@8.57.0)(typescript@5.4.5))(eslint@8.57.0)(typescript@5.4.5))(eslint@8.57.0)(jest@29.7.0(@types/node@20.14.8))(typescript@5.4.5)
|
||||
eslint-plugin-import: 2.29.1(@typescript-eslint/parser@7.12.0(eslint@8.57.0)(typescript@5.4.5))(eslint-import-resolver-typescript@3.6.1(@typescript-eslint/parser@7.12.0(eslint@8.57.0)(typescript@5.4.5))(eslint-plugin-import@2.29.1)(eslint@8.57.0))(eslint@8.57.0)
|
||||
eslint-plugin-jest: 27.9.0(@typescript-eslint/eslint-plugin@7.3.1(@typescript-eslint/parser@7.12.0(eslint@8.57.0)(typescript@5.4.5))(eslint@8.57.0)(typescript@5.4.5))(eslint@8.57.0)(jest@29.7.0(@types/node@20.14.8)(babel-plugin-macros@3.1.0))(typescript@5.4.5)
|
||||
eslint-plugin-jsx-a11y: 6.8.0(eslint@8.57.0)
|
||||
eslint-plugin-playwright: 1.5.4(eslint-plugin-jest@27.9.0(@typescript-eslint/eslint-plugin@7.3.1(@typescript-eslint/parser@7.12.0(eslint@8.57.0)(typescript@5.4.5))(eslint@8.57.0)(typescript@5.4.5))(eslint@8.57.0)(jest@29.7.0(@types/node@20.14.8))(typescript@5.4.5))(eslint@8.57.0)
|
||||
eslint-plugin-playwright: 1.5.4(eslint-plugin-jest@27.9.0(@typescript-eslint/eslint-plugin@7.3.1(@typescript-eslint/parser@7.12.0(eslint@8.57.0)(typescript@5.4.5))(eslint@8.57.0)(typescript@5.4.5))(eslint@8.57.0)(jest@29.7.0(@types/node@20.14.8)(babel-plugin-macros@3.1.0))(typescript@5.4.5))(eslint@8.57.0)
|
||||
eslint-plugin-react: 7.34.1(eslint@8.57.0)
|
||||
eslint-plugin-react-hooks: 4.6.0(eslint@8.57.0)
|
||||
eslint-plugin-testing-library: 6.2.0(eslint@8.57.0)(typescript@5.4.5)
|
||||
eslint-plugin-tsdoc: 0.2.17
|
||||
eslint-plugin-unicorn: 51.0.1(eslint@8.57.0)
|
||||
eslint-plugin-vitest: 0.3.26(@typescript-eslint/eslint-plugin@7.3.1(@typescript-eslint/parser@7.12.0(eslint@8.57.0)(typescript@5.4.5))(eslint@8.57.0)(typescript@5.4.5))(eslint@8.57.0)(typescript@5.4.5)(vitest@2.1.2(@types/node@20.14.8))
|
||||
eslint-plugin-vitest: 0.3.26(@typescript-eslint/eslint-plugin@7.3.1(@typescript-eslint/parser@7.12.0(eslint@8.57.0)(typescript@5.4.5))(eslint@8.57.0)(typescript@5.4.5))(eslint@8.57.0)(typescript@5.4.5)(vitest@2.1.2(@types/node@20.14.8)(jsdom@20.0.3)(msw@2.6.5(@types/node@20.14.8)(typescript@5.4.5))(terser@5.43.1))
|
||||
prettier-plugin-packagejson: 2.4.12(prettier@3.5.3)
|
||||
optionalDependencies:
|
||||
'@next/eslint-plugin-next': 14.2.15
|
||||
@@ -18399,31 +18458,33 @@ snapshots:
|
||||
chai: 5.1.1
|
||||
tinyrainbow: 1.2.0
|
||||
|
||||
'@vitest/mocker@2.1.2(@vitest/spy@2.1.2)(msw@2.6.5(@types/node@20.11.29)(typescript@5.4.5))(vite@5.2.14(@types/node@20.11.29)(terser@5.43.0))':
|
||||
'@vitest/mocker@2.1.2(@vitest/spy@2.1.2)(msw@2.6.5(@types/node@20.10.5)(typescript@5.4.5))(vite@5.2.14(@types/node@20.10.5)(terser@5.43.1))':
|
||||
dependencies:
|
||||
'@vitest/spy': 2.1.2
|
||||
estree-walker: 3.0.3
|
||||
magic-string: 0.30.11
|
||||
optionalDependencies:
|
||||
msw: 2.6.5(@types/node@20.10.5)(typescript@5.4.5)
|
||||
vite: 5.2.14(@types/node@20.10.5)(terser@5.43.1)
|
||||
optional: true
|
||||
|
||||
'@vitest/mocker@2.1.2(@vitest/spy@2.1.2)(msw@2.6.5(@types/node@20.11.29)(typescript@5.4.5))(vite@5.2.14(@types/node@20.11.29)(terser@5.43.1))':
|
||||
dependencies:
|
||||
'@vitest/spy': 2.1.2
|
||||
estree-walker: 3.0.3
|
||||
magic-string: 0.30.11
|
||||
optionalDependencies:
|
||||
msw: 2.6.5(@types/node@20.11.29)(typescript@5.4.5)
|
||||
vite: 5.2.14(@types/node@20.11.29)(terser@5.43.0)
|
||||
vite: 5.2.14(@types/node@20.11.29)(terser@5.43.1)
|
||||
|
||||
'@vitest/mocker@2.1.2(@vitest/spy@2.1.2)(vite@5.2.14(@types/node@20.10.5))':
|
||||
'@vitest/mocker@2.1.2(@vitest/spy@2.1.2)(msw@2.6.5(@types/node@20.14.8)(typescript@5.4.5))(vite@5.2.14(@types/node@20.14.8)(terser@5.43.1))':
|
||||
dependencies:
|
||||
'@vitest/spy': 2.1.2
|
||||
estree-walker: 3.0.3
|
||||
magic-string: 0.30.11
|
||||
optionalDependencies:
|
||||
vite: 5.2.14(@types/node@20.10.5)
|
||||
optional: true
|
||||
|
||||
'@vitest/mocker@2.1.2(@vitest/spy@2.1.2)(vite@5.2.14(@types/node@20.14.8))':
|
||||
dependencies:
|
||||
'@vitest/spy': 2.1.2
|
||||
estree-walker: 3.0.3
|
||||
magic-string: 0.30.11
|
||||
optionalDependencies:
|
||||
vite: 5.2.14(@types/node@20.14.8)
|
||||
msw: 2.6.5(@types/node@20.14.8)(typescript@5.4.5)
|
||||
vite: 5.2.14(@types/node@20.14.8)(terser@5.43.1)
|
||||
optional: true
|
||||
|
||||
'@vitest/pretty-format@2.1.2':
|
||||
@@ -19505,7 +19566,7 @@ snapshots:
|
||||
- supports-color
|
||||
- ts-node
|
||||
|
||||
create-jest@29.7.0(@types/node@20.14.8):
|
||||
create-jest@29.7.0(@types/node@20.14.8)(babel-plugin-macros@3.1.0):
|
||||
dependencies:
|
||||
'@jest/types': 29.6.3
|
||||
chalk: 4.1.2
|
||||
@@ -20319,7 +20380,7 @@ snapshots:
|
||||
eslint: 8.57.0
|
||||
eslint-import-resolver-node: 0.3.9
|
||||
eslint-import-resolver-typescript: 3.6.1(@typescript-eslint/parser@7.12.0(eslint@8.57.0)(typescript@5.4.5))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.29.1)(eslint@8.57.0)
|
||||
eslint-plugin-import: 2.29.1(@typescript-eslint/parser@7.12.0(eslint@8.57.0)(typescript@5.4.5))(eslint-import-resolver-typescript@3.6.1)(eslint@8.57.0)
|
||||
eslint-plugin-import: 2.29.1(@typescript-eslint/parser@7.12.0(eslint@8.57.0)(typescript@5.4.5))(eslint-import-resolver-typescript@3.6.1(@typescript-eslint/parser@7.12.0(eslint@8.57.0)(typescript@5.4.5))(eslint-plugin-import@2.29.1)(eslint@8.57.0))(eslint@8.57.0)
|
||||
eslint-plugin-jsx-a11y: 6.8.0(eslint@8.57.0)
|
||||
eslint-plugin-react: 7.34.1(eslint@8.57.0)
|
||||
eslint-plugin-react-hooks: 4.6.0(eslint@8.57.0)
|
||||
@@ -20336,7 +20397,7 @@ snapshots:
|
||||
eslint-config-standard@17.1.0(eslint-plugin-import@2.29.1(@typescript-eslint/parser@7.12.0(eslint@8.57.0)(typescript@5.4.5))(eslint@8.57.0))(eslint-plugin-n@16.6.2(eslint@8.57.0))(eslint-plugin-promise@6.4.0(eslint@8.57.0))(eslint@8.57.0):
|
||||
dependencies:
|
||||
eslint: 8.57.0
|
||||
eslint-plugin-import: 2.29.1(@typescript-eslint/parser@7.12.0(eslint@8.57.0)(typescript@5.4.5))(eslint@8.57.0)
|
||||
eslint-plugin-import: 2.29.1(@typescript-eslint/parser@7.12.0(eslint@8.57.0)(typescript@5.4.5))(eslint-import-resolver-typescript@3.6.1(@typescript-eslint/parser@7.12.0(eslint@8.57.0)(typescript@5.4.5))(eslint-plugin-import@2.29.1)(eslint@8.57.0))(eslint@8.57.0)
|
||||
eslint-plugin-n: 16.6.2(eslint@8.57.0)
|
||||
eslint-plugin-promise: 6.4.0(eslint@8.57.0)
|
||||
|
||||
@@ -20346,9 +20407,9 @@ snapshots:
|
||||
eslint-plugin-turbo: 2.5.4(eslint@8.57.0)(turbo@2.5.4)
|
||||
turbo: 2.5.4
|
||||
|
||||
eslint-import-resolver-alias@1.1.2(eslint-plugin-import@2.29.1(@typescript-eslint/parser@7.12.0(eslint@8.57.0)(typescript@5.4.5))(eslint-import-resolver-typescript@3.6.1)(eslint@8.57.0)):
|
||||
eslint-import-resolver-alias@1.1.2(eslint-plugin-import@2.29.1):
|
||||
dependencies:
|
||||
eslint-plugin-import: 2.29.1(@typescript-eslint/parser@7.12.0(eslint@8.57.0)(typescript@5.4.5))(eslint-import-resolver-typescript@3.6.1)(eslint@8.57.0)
|
||||
eslint-plugin-import: 2.29.1(@typescript-eslint/parser@7.12.0(eslint@8.57.0)(typescript@5.4.5))(eslint-import-resolver-typescript@3.6.1(@typescript-eslint/parser@7.12.0(eslint@8.57.0)(typescript@5.4.5))(eslint-plugin-import@2.29.1)(eslint@8.57.0))(eslint@8.57.0)
|
||||
|
||||
eslint-import-resolver-node@0.3.9:
|
||||
dependencies:
|
||||
@@ -20363,8 +20424,8 @@ snapshots:
|
||||
debug: 4.3.7(supports-color@5.5.0)
|
||||
enhanced-resolve: 5.17.1
|
||||
eslint: 8.57.0
|
||||
eslint-module-utils: 2.8.1(@typescript-eslint/parser@7.12.0(eslint@8.57.0)(typescript@5.4.5))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.1(@typescript-eslint/parser@7.12.0(eslint@8.57.0)(typescript@5.4.5))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.29.1)(eslint@8.57.0))(eslint@8.57.0)
|
||||
eslint-plugin-import: 2.29.1(@typescript-eslint/parser@7.12.0(eslint@8.57.0)(typescript@5.4.5))(eslint-import-resolver-typescript@3.6.1)(eslint@8.57.0)
|
||||
eslint-module-utils: 2.8.1(@typescript-eslint/parser@7.12.0(eslint@8.57.0)(typescript@5.4.5))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.1)(eslint@8.57.0)
|
||||
eslint-plugin-import: 2.29.1(@typescript-eslint/parser@7.12.0(eslint@8.57.0)(typescript@5.4.5))(eslint-import-resolver-typescript@3.6.1(@typescript-eslint/parser@7.12.0(eslint@8.57.0)(typescript@5.4.5))(eslint-plugin-import@2.29.1)(eslint@8.57.0))(eslint@8.57.0)
|
||||
fast-glob: 3.3.3
|
||||
get-tsconfig: 4.8.1
|
||||
is-core-module: 2.15.1
|
||||
@@ -20381,7 +20442,7 @@ snapshots:
|
||||
enhanced-resolve: 5.17.1
|
||||
eslint: 8.57.0
|
||||
eslint-module-utils: 2.8.1(@typescript-eslint/parser@7.12.0(eslint@8.57.0)(typescript@5.4.5))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.1(@typescript-eslint/parser@7.12.0(eslint@8.57.0)(typescript@5.4.5))(eslint-plugin-import@2.29.1)(eslint@8.57.0))(eslint@8.57.0)
|
||||
eslint-plugin-import: 2.29.1(@typescript-eslint/parser@7.12.0(eslint@8.57.0)(typescript@5.4.5))(eslint-import-resolver-typescript@3.6.1)(eslint@8.57.0)
|
||||
eslint-plugin-import: 2.29.1(@typescript-eslint/parser@7.12.0(eslint@8.57.0)(typescript@5.4.5))(eslint-import-resolver-typescript@3.6.1(@typescript-eslint/parser@7.12.0(eslint@8.57.0)(typescript@5.4.5))(eslint-plugin-import@2.29.1)(eslint@8.57.0))(eslint@8.57.0)
|
||||
fast-glob: 3.3.3
|
||||
get-tsconfig: 4.8.1
|
||||
is-core-module: 2.15.1
|
||||
@@ -20392,17 +20453,6 @@ snapshots:
|
||||
- eslint-import-resolver-webpack
|
||||
- supports-color
|
||||
|
||||
eslint-module-utils@2.8.1(@typescript-eslint/parser@7.12.0(eslint@8.57.0)(typescript@5.4.5))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.1(@typescript-eslint/parser@7.12.0(eslint@8.57.0)(typescript@5.4.5))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.29.1)(eslint@8.57.0))(eslint@8.57.0):
|
||||
dependencies:
|
||||
debug: 3.2.7
|
||||
optionalDependencies:
|
||||
'@typescript-eslint/parser': 7.12.0(eslint@8.57.0)(typescript@5.4.5)
|
||||
eslint: 8.57.0
|
||||
eslint-import-resolver-node: 0.3.9
|
||||
eslint-import-resolver-typescript: 3.6.1(@typescript-eslint/parser@7.12.0(eslint@8.57.0)(typescript@5.4.5))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.29.1)(eslint@8.57.0)
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
eslint-module-utils@2.8.1(@typescript-eslint/parser@7.12.0(eslint@8.57.0)(typescript@5.4.5))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.1(@typescript-eslint/parser@7.12.0(eslint@8.57.0)(typescript@5.4.5))(eslint-plugin-import@2.29.1)(eslint@8.57.0))(eslint@8.57.0):
|
||||
dependencies:
|
||||
debug: 3.2.7
|
||||
@@ -20414,6 +20464,17 @@ snapshots:
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
eslint-module-utils@2.8.1(@typescript-eslint/parser@7.12.0(eslint@8.57.0)(typescript@5.4.5))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.1)(eslint@8.57.0):
|
||||
dependencies:
|
||||
debug: 3.2.7
|
||||
optionalDependencies:
|
||||
'@typescript-eslint/parser': 7.12.0(eslint@8.57.0)(typescript@5.4.5)
|
||||
eslint: 8.57.0
|
||||
eslint-import-resolver-node: 0.3.9
|
||||
eslint-import-resolver-typescript: 3.6.1(@typescript-eslint/parser@7.12.0(eslint@8.57.0)(typescript@5.4.5))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.29.1)(eslint@8.57.0)
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
eslint-plugin-es-x@7.8.0(eslint@8.57.0):
|
||||
dependencies:
|
||||
'@eslint-community/eslint-utils': 4.7.0(eslint@8.57.0)
|
||||
@@ -20427,7 +20488,7 @@ snapshots:
|
||||
eslint: 8.57.0
|
||||
ignore: 5.3.2
|
||||
|
||||
eslint-plugin-import@2.29.1(@typescript-eslint/parser@7.12.0(eslint@8.57.0)(typescript@5.4.5))(eslint-import-resolver-typescript@3.6.1)(eslint@8.57.0):
|
||||
eslint-plugin-import@2.29.1(@typescript-eslint/parser@7.12.0(eslint@8.57.0)(typescript@5.4.5))(eslint-import-resolver-typescript@3.6.1(@typescript-eslint/parser@7.12.0(eslint@8.57.0)(typescript@5.4.5))(eslint-plugin-import@2.29.1)(eslint@8.57.0))(eslint@8.57.0):
|
||||
dependencies:
|
||||
array-includes: 3.1.7
|
||||
array.prototype.findlastindex: 1.2.4
|
||||
@@ -20454,40 +20515,13 @@ snapshots:
|
||||
- eslint-import-resolver-webpack
|
||||
- supports-color
|
||||
|
||||
eslint-plugin-import@2.29.1(@typescript-eslint/parser@7.12.0(eslint@8.57.0)(typescript@5.4.5))(eslint@8.57.0):
|
||||
dependencies:
|
||||
array-includes: 3.1.7
|
||||
array.prototype.findlastindex: 1.2.4
|
||||
array.prototype.flat: 1.3.2
|
||||
array.prototype.flatmap: 1.3.2
|
||||
debug: 3.2.7
|
||||
doctrine: 2.1.0
|
||||
eslint: 8.57.0
|
||||
eslint-import-resolver-node: 0.3.9
|
||||
eslint-module-utils: 2.8.1(@typescript-eslint/parser@7.12.0(eslint@8.57.0)(typescript@5.4.5))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.1(@typescript-eslint/parser@7.12.0(eslint@8.57.0)(typescript@5.4.5))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.29.1)(eslint@8.57.0))(eslint@8.57.0)
|
||||
hasown: 2.0.2
|
||||
is-core-module: 2.15.1
|
||||
is-glob: 4.0.3
|
||||
minimatch: 3.1.2
|
||||
object.fromentries: 2.0.8
|
||||
object.groupby: 1.0.3
|
||||
object.values: 1.2.0
|
||||
semver: 6.3.1
|
||||
tsconfig-paths: 3.15.0
|
||||
optionalDependencies:
|
||||
'@typescript-eslint/parser': 7.12.0(eslint@8.57.0)(typescript@5.4.5)
|
||||
transitivePeerDependencies:
|
||||
- eslint-import-resolver-typescript
|
||||
- eslint-import-resolver-webpack
|
||||
- supports-color
|
||||
|
||||
eslint-plugin-jest@27.9.0(@typescript-eslint/eslint-plugin@7.3.1(@typescript-eslint/parser@7.12.0(eslint@8.57.0)(typescript@5.4.5))(eslint@8.57.0)(typescript@5.4.5))(eslint@8.57.0)(jest@29.7.0(@types/node@20.14.8))(typescript@5.4.5):
|
||||
eslint-plugin-jest@27.9.0(@typescript-eslint/eslint-plugin@7.3.1(@typescript-eslint/parser@7.12.0(eslint@8.57.0)(typescript@5.4.5))(eslint@8.57.0)(typescript@5.4.5))(eslint@8.57.0)(jest@29.7.0(@types/node@20.14.8)(babel-plugin-macros@3.1.0))(typescript@5.4.5):
|
||||
dependencies:
|
||||
'@typescript-eslint/utils': 5.62.0(eslint@8.57.0)(typescript@5.4.5)
|
||||
eslint: 8.57.0
|
||||
optionalDependencies:
|
||||
'@typescript-eslint/eslint-plugin': 7.3.1(@typescript-eslint/parser@7.12.0(eslint@8.57.0)(typescript@5.4.5))(eslint@8.57.0)(typescript@5.4.5)
|
||||
jest: 29.7.0(@types/node@20.14.8)
|
||||
jest: 29.7.0(@types/node@20.14.8)(babel-plugin-macros@3.1.0)
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
- typescript
|
||||
@@ -20529,12 +20563,12 @@ snapshots:
|
||||
|
||||
eslint-plugin-only-warn@1.1.0: {}
|
||||
|
||||
eslint-plugin-playwright@1.5.4(eslint-plugin-jest@27.9.0(@typescript-eslint/eslint-plugin@7.3.1(@typescript-eslint/parser@7.12.0(eslint@8.57.0)(typescript@5.4.5))(eslint@8.57.0)(typescript@5.4.5))(eslint@8.57.0)(jest@29.7.0(@types/node@20.14.8))(typescript@5.4.5))(eslint@8.57.0):
|
||||
eslint-plugin-playwright@1.5.4(eslint-plugin-jest@27.9.0(@typescript-eslint/eslint-plugin@7.3.1(@typescript-eslint/parser@7.12.0(eslint@8.57.0)(typescript@5.4.5))(eslint@8.57.0)(typescript@5.4.5))(eslint@8.57.0)(jest@29.7.0(@types/node@20.14.8)(babel-plugin-macros@3.1.0))(typescript@5.4.5))(eslint@8.57.0):
|
||||
dependencies:
|
||||
eslint: 8.57.0
|
||||
globals: 13.24.0
|
||||
optionalDependencies:
|
||||
eslint-plugin-jest: 27.9.0(@typescript-eslint/eslint-plugin@7.3.1(@typescript-eslint/parser@7.12.0(eslint@8.57.0)(typescript@5.4.5))(eslint@8.57.0)(typescript@5.4.5))(eslint@8.57.0)(jest@29.7.0(@types/node@20.14.8))(typescript@5.4.5)
|
||||
eslint-plugin-jest: 27.9.0(@typescript-eslint/eslint-plugin@7.3.1(@typescript-eslint/parser@7.12.0(eslint@8.57.0)(typescript@5.4.5))(eslint@8.57.0)(typescript@5.4.5))(eslint@8.57.0)(jest@29.7.0(@types/node@20.14.8)(babel-plugin-macros@3.1.0))(typescript@5.4.5)
|
||||
|
||||
eslint-plugin-prettier@5.1.3(@types/eslint@8.56.12)(eslint-config-prettier@9.1.0(eslint@8.57.0))(eslint@8.57.0)(prettier@3.3.3):
|
||||
dependencies:
|
||||
@@ -20617,13 +20651,13 @@ snapshots:
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
eslint-plugin-vitest@0.3.26(@typescript-eslint/eslint-plugin@7.3.1(@typescript-eslint/parser@7.12.0(eslint@8.57.0)(typescript@5.4.5))(eslint@8.57.0)(typescript@5.4.5))(eslint@8.57.0)(typescript@5.4.5)(vitest@2.1.2(@types/node@20.14.8)):
|
||||
eslint-plugin-vitest@0.3.26(@typescript-eslint/eslint-plugin@7.3.1(@typescript-eslint/parser@7.12.0(eslint@8.57.0)(typescript@5.4.5))(eslint@8.57.0)(typescript@5.4.5))(eslint@8.57.0)(typescript@5.4.5)(vitest@2.1.2(@types/node@20.14.8)(jsdom@20.0.3)(msw@2.6.5(@types/node@20.14.8)(typescript@5.4.5))(terser@5.43.1)):
|
||||
dependencies:
|
||||
'@typescript-eslint/utils': 7.3.1(eslint@8.57.0)(typescript@5.4.5)
|
||||
eslint: 8.57.0
|
||||
optionalDependencies:
|
||||
'@typescript-eslint/eslint-plugin': 7.3.1(@typescript-eslint/parser@7.12.0(eslint@8.57.0)(typescript@5.4.5))(eslint@8.57.0)(typescript@5.4.5)
|
||||
vitest: 2.1.2(@types/node@20.14.8)
|
||||
vitest: 2.1.2(@types/node@20.14.8)(jsdom@20.0.3)(msw@2.6.5(@types/node@20.14.8)(typescript@5.4.5))(terser@5.43.1)
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
- typescript
|
||||
@@ -21884,13 +21918,13 @@ snapshots:
|
||||
- supports-color
|
||||
- ts-node
|
||||
|
||||
jest-cli@29.7.0(@types/node@20.14.8):
|
||||
jest-cli@29.7.0(@types/node@20.14.8)(babel-plugin-macros@3.1.0):
|
||||
dependencies:
|
||||
'@jest/core': 29.7.0(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@types/node@20.10.5)(typescript@5.4.5))
|
||||
'@jest/test-result': 29.7.0
|
||||
'@jest/types': 29.6.3
|
||||
chalk: 4.1.2
|
||||
create-jest: 29.7.0(@types/node@20.14.8)
|
||||
create-jest: 29.7.0(@types/node@20.14.8)(babel-plugin-macros@3.1.0)
|
||||
exit: 0.1.2
|
||||
import-local: 3.1.0
|
||||
jest-config: 29.7.0(@types/node@20.14.8)(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@types/node@20.10.5)(typescript@5.4.5))
|
||||
@@ -22191,7 +22225,7 @@ snapshots:
|
||||
|
||||
jest-worker@27.5.1:
|
||||
dependencies:
|
||||
'@types/node': 20.10.5
|
||||
'@types/node': 20.14.8
|
||||
merge-stream: 2.0.0
|
||||
supports-color: 8.1.1
|
||||
|
||||
@@ -22214,12 +22248,12 @@ snapshots:
|
||||
- supports-color
|
||||
- ts-node
|
||||
|
||||
jest@29.7.0(@types/node@20.14.8):
|
||||
jest@29.7.0(@types/node@20.14.8)(babel-plugin-macros@3.1.0):
|
||||
dependencies:
|
||||
'@jest/core': 29.7.0(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@types/node@20.10.5)(typescript@5.4.5))
|
||||
'@jest/types': 29.6.3
|
||||
import-local: 3.1.0
|
||||
jest-cli: 29.7.0(@types/node@20.14.8)
|
||||
jest-cli: 29.7.0(@types/node@20.14.8)(babel-plugin-macros@3.1.0)
|
||||
transitivePeerDependencies:
|
||||
- '@types/node'
|
||||
- babel-plugin-macros
|
||||
@@ -22417,19 +22451,19 @@ snapshots:
|
||||
- openai
|
||||
- ws
|
||||
|
||||
langfuse-core@3.37.4:
|
||||
langfuse-core@3.38.1:
|
||||
dependencies:
|
||||
mustache: 4.2.0
|
||||
|
||||
langfuse-langchain@3.37.4(langchain@0.3.28(@langchain/anthropic@0.3.22(@langchain/core@0.3.58(openai@4.104.0(zod@3.25.62))))(@langchain/aws@0.1.11(@langchain/core@0.3.58(openai@4.104.0(zod@3.25.62))))(@langchain/core@0.3.58(openai@4.104.0(zod@3.25.62)))(@langchain/google-genai@0.2.12(@langchain/core@0.3.58(openai@4.104.0(zod@3.25.62))))(@langchain/google-vertexai@0.2.12(@langchain/core@0.3.58(openai@4.104.0(zod@3.25.62)))(zod@3.25.62))(axios@1.8.2)(cheerio@1.0.0)(handlebars@4.7.8)(openai@4.104.0(zod@3.25.62))):
|
||||
langfuse-langchain@3.38.1(langchain@0.3.28(@langchain/anthropic@0.3.22(@langchain/core@0.3.58(openai@4.104.0(zod@3.25.62))))(@langchain/aws@0.1.11(@langchain/core@0.3.58(openai@4.104.0(zod@3.25.62))))(@langchain/core@0.3.58(openai@4.104.0(zod@3.25.62)))(@langchain/google-genai@0.2.12(@langchain/core@0.3.58(openai@4.104.0(zod@3.25.62))))(@langchain/google-vertexai@0.2.12(@langchain/core@0.3.58(openai@4.104.0(zod@3.25.62)))(zod@3.25.62))(axios@1.8.2)(cheerio@1.0.0)(handlebars@4.7.8)(openai@4.104.0(zod@3.25.62))):
|
||||
dependencies:
|
||||
langchain: 0.3.28(@langchain/anthropic@0.3.22(@langchain/core@0.3.58(openai@4.104.0(zod@3.25.62))))(@langchain/aws@0.1.11(@langchain/core@0.3.58(openai@4.104.0(zod@3.25.62))))(@langchain/core@0.3.58(openai@4.104.0(zod@3.25.62)))(@langchain/google-genai@0.2.12(@langchain/core@0.3.58(openai@4.104.0(zod@3.25.62))))(@langchain/google-vertexai@0.2.12(@langchain/core@0.3.58(openai@4.104.0(zod@3.25.62)))(zod@3.25.62))(axios@1.8.2)(cheerio@1.0.0)(handlebars@4.7.8)(openai@4.104.0(zod@3.25.62))
|
||||
langfuse: 3.37.4
|
||||
langfuse-core: 3.37.4
|
||||
langfuse: 3.38.1
|
||||
langfuse-core: 3.38.1
|
||||
|
||||
langfuse@3.37.4:
|
||||
langfuse@3.38.1:
|
||||
dependencies:
|
||||
langfuse-core: 3.37.4
|
||||
langfuse-core: 3.38.1
|
||||
|
||||
langium@3.0.0:
|
||||
dependencies:
|
||||
@@ -23127,6 +23161,32 @@ snapshots:
|
||||
optionalDependencies:
|
||||
msgpackr-extract: 3.0.3
|
||||
|
||||
msw@2.6.5(@types/node@20.10.5)(typescript@5.4.5):
|
||||
dependencies:
|
||||
'@bundled-es-modules/cookie': 2.0.1
|
||||
'@bundled-es-modules/statuses': 1.0.1
|
||||
'@bundled-es-modules/tough-cookie': 0.1.6
|
||||
'@inquirer/confirm': 5.0.2(@types/node@20.10.5)
|
||||
'@mswjs/interceptors': 0.37.1
|
||||
'@open-draft/deferred-promise': 2.2.0
|
||||
'@open-draft/until': 2.1.0
|
||||
'@types/cookie': 0.6.0
|
||||
'@types/statuses': 2.0.5
|
||||
chalk: 4.1.2
|
||||
graphql: 16.9.0
|
||||
headers-polyfill: 4.0.3
|
||||
is-node-process: 1.2.0
|
||||
outvariant: 1.4.3
|
||||
path-to-regexp: 6.3.0
|
||||
strict-event-emitter: 0.5.1
|
||||
type-fest: 4.27.0
|
||||
yargs: 17.7.2
|
||||
optionalDependencies:
|
||||
typescript: 5.4.5
|
||||
transitivePeerDependencies:
|
||||
- '@types/node'
|
||||
optional: true
|
||||
|
||||
msw@2.6.5(@types/node@20.11.29)(typescript@5.4.5):
|
||||
dependencies:
|
||||
'@bundled-es-modules/cookie': 2.0.1
|
||||
@@ -23152,6 +23212,32 @@ snapshots:
|
||||
transitivePeerDependencies:
|
||||
- '@types/node'
|
||||
|
||||
msw@2.6.5(@types/node@20.14.8)(typescript@5.4.5):
|
||||
dependencies:
|
||||
'@bundled-es-modules/cookie': 2.0.1
|
||||
'@bundled-es-modules/statuses': 1.0.1
|
||||
'@bundled-es-modules/tough-cookie': 0.1.6
|
||||
'@inquirer/confirm': 5.0.2(@types/node@20.14.8)
|
||||
'@mswjs/interceptors': 0.37.1
|
||||
'@open-draft/deferred-promise': 2.2.0
|
||||
'@open-draft/until': 2.1.0
|
||||
'@types/cookie': 0.6.0
|
||||
'@types/statuses': 2.0.5
|
||||
chalk: 4.1.2
|
||||
graphql: 16.9.0
|
||||
headers-polyfill: 4.0.3
|
||||
is-node-process: 1.2.0
|
||||
outvariant: 1.4.3
|
||||
path-to-regexp: 6.3.0
|
||||
strict-event-emitter: 0.5.1
|
||||
type-fest: 4.27.0
|
||||
yargs: 17.7.2
|
||||
optionalDependencies:
|
||||
typescript: 5.4.5
|
||||
transitivePeerDependencies:
|
||||
- '@types/node'
|
||||
optional: true
|
||||
|
||||
mustache@4.2.0: {}
|
||||
|
||||
mute-stream@2.0.0: {}
|
||||
@@ -25180,14 +25266,6 @@ snapshots:
|
||||
terser: 5.43.1
|
||||
webpack: 5.97.1
|
||||
|
||||
terser@5.43.0:
|
||||
dependencies:
|
||||
'@jridgewell/source-map': 0.3.6
|
||||
acorn: 8.15.0
|
||||
commander: 2.20.3
|
||||
source-map-support: 0.5.21
|
||||
optional: true
|
||||
|
||||
terser@5.43.1:
|
||||
dependencies:
|
||||
'@jridgewell/source-map': 0.3.6
|
||||
@@ -25681,12 +25759,12 @@ snapshots:
|
||||
'@egjs/hammerjs': 2.0.17
|
||||
component-emitter: 1.3.1
|
||||
|
||||
vite-node@2.1.2(@types/node@20.10.5):
|
||||
vite-node@2.1.2(@types/node@20.10.5)(terser@5.43.1):
|
||||
dependencies:
|
||||
cac: 6.7.14
|
||||
debug: 4.3.7(supports-color@5.5.0)
|
||||
pathe: 1.1.2
|
||||
vite: 5.2.14(@types/node@20.10.5)
|
||||
vite: 5.2.14(@types/node@20.10.5)(terser@5.43.1)
|
||||
transitivePeerDependencies:
|
||||
- '@types/node'
|
||||
- less
|
||||
@@ -25698,12 +25776,12 @@ snapshots:
|
||||
- terser
|
||||
optional: true
|
||||
|
||||
vite-node@2.1.2(@types/node@20.11.29)(terser@5.43.0):
|
||||
vite-node@2.1.2(@types/node@20.11.29)(terser@5.43.1):
|
||||
dependencies:
|
||||
cac: 6.7.14
|
||||
debug: 4.3.7(supports-color@5.5.0)
|
||||
pathe: 1.1.2
|
||||
vite: 5.2.14(@types/node@20.11.29)(terser@5.43.0)
|
||||
vite: 5.2.14(@types/node@20.11.29)(terser@5.43.1)
|
||||
transitivePeerDependencies:
|
||||
- '@types/node'
|
||||
- less
|
||||
@@ -25714,12 +25792,12 @@ snapshots:
|
||||
- supports-color
|
||||
- terser
|
||||
|
||||
vite-node@2.1.2(@types/node@20.14.8):
|
||||
vite-node@2.1.2(@types/node@20.14.8)(terser@5.43.1):
|
||||
dependencies:
|
||||
cac: 6.7.14
|
||||
debug: 4.3.7(supports-color@5.5.0)
|
||||
pathe: 1.1.2
|
||||
vite: 5.2.14(@types/node@20.14.8)
|
||||
vite: 5.2.14(@types/node@20.14.8)(terser@5.43.1)
|
||||
transitivePeerDependencies:
|
||||
- '@types/node'
|
||||
- less
|
||||
@@ -25731,7 +25809,7 @@ snapshots:
|
||||
- terser
|
||||
optional: true
|
||||
|
||||
vite@5.2.14(@types/node@20.10.5):
|
||||
vite@5.2.14(@types/node@20.10.5)(terser@5.43.1):
|
||||
dependencies:
|
||||
esbuild: 0.20.2
|
||||
postcss: 8.5.3
|
||||
@@ -25739,9 +25817,10 @@ snapshots:
|
||||
optionalDependencies:
|
||||
'@types/node': 20.10.5
|
||||
fsevents: 2.3.3
|
||||
terser: 5.43.1
|
||||
optional: true
|
||||
|
||||
vite@5.2.14(@types/node@20.11.29)(terser@5.43.0):
|
||||
vite@5.2.14(@types/node@20.11.29)(terser@5.43.1):
|
||||
dependencies:
|
||||
esbuild: 0.20.2
|
||||
postcss: 8.5.3
|
||||
@@ -25749,9 +25828,9 @@ snapshots:
|
||||
optionalDependencies:
|
||||
'@types/node': 20.11.29
|
||||
fsevents: 2.3.3
|
||||
terser: 5.43.0
|
||||
terser: 5.43.1
|
||||
|
||||
vite@5.2.14(@types/node@20.14.8):
|
||||
vite@5.2.14(@types/node@20.14.8)(terser@5.43.1):
|
||||
dependencies:
|
||||
esbuild: 0.20.2
|
||||
postcss: 8.5.3
|
||||
@@ -25759,12 +25838,13 @@ snapshots:
|
||||
optionalDependencies:
|
||||
'@types/node': 20.14.8
|
||||
fsevents: 2.3.3
|
||||
terser: 5.43.1
|
||||
optional: true
|
||||
|
||||
vitest@2.1.2(@types/node@20.10.5):
|
||||
vitest@2.1.2(@types/node@20.10.5)(jsdom@20.0.3)(msw@2.6.5(@types/node@20.10.5)(typescript@5.4.5))(terser@5.43.1):
|
||||
dependencies:
|
||||
'@vitest/expect': 2.1.2
|
||||
'@vitest/mocker': 2.1.2(@vitest/spy@2.1.2)(vite@5.2.14(@types/node@20.10.5))
|
||||
'@vitest/mocker': 2.1.2(@vitest/spy@2.1.2)(msw@2.6.5(@types/node@20.10.5)(typescript@5.4.5))(vite@5.2.14(@types/node@20.10.5)(terser@5.43.1))
|
||||
'@vitest/pretty-format': 2.1.2
|
||||
'@vitest/runner': 2.1.2
|
||||
'@vitest/snapshot': 2.1.2
|
||||
@@ -25779,11 +25859,12 @@ snapshots:
|
||||
tinyexec: 0.3.0
|
||||
tinypool: 1.0.1
|
||||
tinyrainbow: 1.2.0
|
||||
vite: 5.2.14(@types/node@20.10.5)
|
||||
vite-node: 2.1.2(@types/node@20.10.5)
|
||||
vite: 5.2.14(@types/node@20.10.5)(terser@5.43.1)
|
||||
vite-node: 2.1.2(@types/node@20.10.5)(terser@5.43.1)
|
||||
why-is-node-running: 2.3.0
|
||||
optionalDependencies:
|
||||
'@types/node': 20.10.5
|
||||
jsdom: 20.0.3
|
||||
transitivePeerDependencies:
|
||||
- less
|
||||
- lightningcss
|
||||
@@ -25795,10 +25876,10 @@ snapshots:
|
||||
- terser
|
||||
optional: true
|
||||
|
||||
vitest@2.1.2(@types/node@20.11.29)(jsdom@20.0.3)(msw@2.6.5(@types/node@20.11.29)(typescript@5.4.5))(terser@5.43.0):
|
||||
vitest@2.1.2(@types/node@20.11.29)(jsdom@20.0.3)(msw@2.6.5(@types/node@20.11.29)(typescript@5.4.5))(terser@5.43.1):
|
||||
dependencies:
|
||||
'@vitest/expect': 2.1.2
|
||||
'@vitest/mocker': 2.1.2(@vitest/spy@2.1.2)(msw@2.6.5(@types/node@20.11.29)(typescript@5.4.5))(vite@5.2.14(@types/node@20.11.29)(terser@5.43.0))
|
||||
'@vitest/mocker': 2.1.2(@vitest/spy@2.1.2)(msw@2.6.5(@types/node@20.11.29)(typescript@5.4.5))(vite@5.2.14(@types/node@20.11.29)(terser@5.43.1))
|
||||
'@vitest/pretty-format': 2.1.2
|
||||
'@vitest/runner': 2.1.2
|
||||
'@vitest/snapshot': 2.1.2
|
||||
@@ -25813,8 +25894,8 @@ snapshots:
|
||||
tinyexec: 0.3.0
|
||||
tinypool: 1.0.1
|
||||
tinyrainbow: 1.2.0
|
||||
vite: 5.2.14(@types/node@20.11.29)(terser@5.43.0)
|
||||
vite-node: 2.1.2(@types/node@20.11.29)(terser@5.43.0)
|
||||
vite: 5.2.14(@types/node@20.11.29)(terser@5.43.1)
|
||||
vite-node: 2.1.2(@types/node@20.11.29)(terser@5.43.1)
|
||||
why-is-node-running: 2.3.0
|
||||
optionalDependencies:
|
||||
'@types/node': 20.11.29
|
||||
@@ -25829,10 +25910,10 @@ snapshots:
|
||||
- supports-color
|
||||
- terser
|
||||
|
||||
vitest@2.1.2(@types/node@20.14.8):
|
||||
vitest@2.1.2(@types/node@20.14.8)(jsdom@20.0.3)(msw@2.6.5(@types/node@20.14.8)(typescript@5.4.5))(terser@5.43.1):
|
||||
dependencies:
|
||||
'@vitest/expect': 2.1.2
|
||||
'@vitest/mocker': 2.1.2(@vitest/spy@2.1.2)(vite@5.2.14(@types/node@20.14.8))
|
||||
'@vitest/mocker': 2.1.2(@vitest/spy@2.1.2)(msw@2.6.5(@types/node@20.14.8)(typescript@5.4.5))(vite@5.2.14(@types/node@20.14.8)(terser@5.43.1))
|
||||
'@vitest/pretty-format': 2.1.2
|
||||
'@vitest/runner': 2.1.2
|
||||
'@vitest/snapshot': 2.1.2
|
||||
@@ -25847,11 +25928,12 @@ snapshots:
|
||||
tinyexec: 0.3.0
|
||||
tinypool: 1.0.1
|
||||
tinyrainbow: 1.2.0
|
||||
vite: 5.2.14(@types/node@20.14.8)
|
||||
vite-node: 2.1.2(@types/node@20.14.8)
|
||||
vite: 5.2.14(@types/node@20.14.8)(terser@5.43.1)
|
||||
vite-node: 2.1.2(@types/node@20.14.8)(terser@5.43.1)
|
||||
why-is-node-running: 2.3.0
|
||||
optionalDependencies:
|
||||
'@types/node': 20.14.8
|
||||
jsdom: 20.0.3
|
||||
transitivePeerDependencies:
|
||||
- less
|
||||
- lightningcss
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "web",
|
||||
"version": "3.78.0",
|
||||
"version": "3.78.2",
|
||||
"private": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
|
||||
@@ -244,6 +244,67 @@ describe("/api/public/scores API Endpoint", () => {
|
||||
"test-key": "test-value-updated",
|
||||
});
|
||||
});
|
||||
|
||||
it("should post score with score config if in valid range", async () => {
|
||||
const configId = v4();
|
||||
const traceId = v4();
|
||||
const scoreId = v4();
|
||||
|
||||
const { projectId: projectId, auth } = await createOrgProjectAndApiKey();
|
||||
|
||||
const config = await prisma.scoreConfig.create({
|
||||
data: {
|
||||
name: "score-name",
|
||||
id: configId,
|
||||
dataType: "NUMERIC",
|
||||
maxValue: 100,
|
||||
projectId: projectId,
|
||||
},
|
||||
});
|
||||
|
||||
const trace = createTrace({
|
||||
id: traceId,
|
||||
project_id: projectId,
|
||||
});
|
||||
await createTracesCh([trace]);
|
||||
|
||||
const score = createTraceScore({
|
||||
id: scoreId,
|
||||
project_id: projectId,
|
||||
trace_id: traceId,
|
||||
name: "score-name",
|
||||
value: 100,
|
||||
source: "API",
|
||||
comment: "comment",
|
||||
metadata: { "test-key": "test-value" },
|
||||
observation_id: null,
|
||||
environment: "production",
|
||||
config_id: config.id,
|
||||
});
|
||||
await createScoresCh([score]);
|
||||
|
||||
const fetchedScore = await makeZodVerifiedAPICall(
|
||||
GetScoreResponseV1,
|
||||
"GET",
|
||||
`/api/public/scores/${scoreId}`,
|
||||
undefined,
|
||||
auth,
|
||||
);
|
||||
|
||||
expect(fetchedScore.body?.id).toBe(scoreId);
|
||||
expect(fetchedScore.body?.traceId).toBe(traceId);
|
||||
expect(fetchedScore.body?.name).toBe("score-name");
|
||||
expect(fetchedScore.body?.value).toBe(100);
|
||||
expect(fetchedScore.body?.configId).toBe(configId);
|
||||
expect(fetchedScore.body?.observationId).toBeNull();
|
||||
expect(fetchedScore.body?.comment).toBe("comment");
|
||||
expect(fetchedScore.body?.source).toBe("API");
|
||||
expect(fetchedScore.body?.projectId).toBe(projectId);
|
||||
expect(fetchedScore.body?.environment).toBe("production");
|
||||
expect(fetchedScore.body?.metadata).toEqual({
|
||||
"test-key": "test-value",
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("GET /api/public/scores", () => {
|
||||
|
||||
@@ -338,6 +338,7 @@ export function DeleteEvalConfigButton(props: DeleteButtonProps) {
|
||||
source: isTableAction ? "table-single-row" : "eval config detail",
|
||||
})
|
||||
}
|
||||
customDeletePrompt="This action cannot be undone and removes all logs associated with this running evaluator. Scores produced by this evaluator will not be deleted."
|
||||
entityToDeleteName="running evaluator"
|
||||
executeDeleteMutation={executeDeleteMutation}
|
||||
isDeleteMutationLoading={evaluatorMutation.isLoading}
|
||||
|
||||
@@ -1 +1 @@
|
||||
export const VERSION = "v3.78.0";
|
||||
export const VERSION = "v3.78.2";
|
||||
|
||||
@@ -456,13 +456,15 @@ export function PreviewCsvImport({
|
||||
</Button>
|
||||
<Button
|
||||
disabled={
|
||||
selectedInputColumn.size === 0 &&
|
||||
selectedExpectedColumn.size === 0 &&
|
||||
selectedMetadataColumn.size === 0
|
||||
(selectedInputColumn.size === 0 &&
|
||||
selectedExpectedColumn.size === 0 &&
|
||||
selectedMetadataColumn.size === 0) ||
|
||||
progress.status === "processing"
|
||||
}
|
||||
loading={progress.status === "processing"}
|
||||
onClick={handleImport}
|
||||
>
|
||||
Import
|
||||
{progress.status === "processing" ? "Importing..." : "Import"}
|
||||
</Button>
|
||||
{progress.status === "processing" && (
|
||||
<div className="mt-2">
|
||||
|
||||
@@ -638,7 +638,7 @@ export const CreateExperimentsForm = ({
|
||||
<span>
|
||||
Given current prompt, dataset item input must
|
||||
contain at least one of these first-level JSON
|
||||
keys, mapped to a string value:
|
||||
keys:
|
||||
</span>
|
||||
<ul className="my-2 ml-2 list-inside list-disc">
|
||||
{expectedColumns.map((col) => (
|
||||
@@ -646,8 +646,9 @@ export const CreateExperimentsForm = ({
|
||||
))}
|
||||
</ul>
|
||||
<span>
|
||||
These will be used as the input to your
|
||||
prompt.
|
||||
Variables (like {"{{variable}}"}) should be mapped to string values.
|
||||
Placeholders should be mapped to arrays of message objects.
|
||||
These will be used as the input to your prompt.
|
||||
</span>
|
||||
</div>
|
||||
) : (
|
||||
@@ -721,7 +722,7 @@ export const CreateExperimentsForm = ({
|
||||
<Loader2 className="h-3 w-3 animate-spin" />
|
||||
</CardTitle>
|
||||
<CardDescription className="text-foreground">
|
||||
Checking dataset items against prompt variables
|
||||
Checking dataset items against prompt variables and placeholders
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
</Card>
|
||||
@@ -747,7 +748,7 @@ export const CreateExperimentsForm = ({
|
||||
<CircleCheck className="h-4 w-4" />
|
||||
</CardTitle>
|
||||
<div className="text-sm">
|
||||
Matches between dataset items and prompt variables
|
||||
Matches between dataset items and prompt variables/placeholders
|
||||
<ul className="my-2 ml-2 list-inside list-disc">
|
||||
{Object.entries(
|
||||
validationResult.data.variablesMap ?? {},
|
||||
@@ -760,7 +761,7 @@ export const CreateExperimentsForm = ({
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
Items missing all prompt variables will be excluded from
|
||||
Items missing all required variables and placeholders will be excluded from
|
||||
the experiment.
|
||||
</div>
|
||||
</CardHeader>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { useMemo } from "react";
|
||||
import { api } from "@/src/utils/api";
|
||||
import { type UseFormReturn } from "react-hook-form";
|
||||
import { extractVariables, PromptType } from "@langfuse/shared";
|
||||
import { extractVariables, PromptType, extractPlaceholderNames, type PromptMessage } from "@langfuse/shared";
|
||||
|
||||
type ExperimentPromptDataProps = {
|
||||
projectId: string;
|
||||
@@ -22,11 +22,18 @@ export function useExperimentPromptData({
|
||||
const prompt = promptMeta.data?.find((p) => p.id === promptId);
|
||||
if (!prompt) return [];
|
||||
|
||||
return extractVariables(
|
||||
const extractedVariables = extractVariables(
|
||||
prompt.type === PromptType.Text
|
||||
? (prompt?.prompt?.toString() ?? "")
|
||||
: JSON.stringify(prompt?.prompt),
|
||||
);
|
||||
|
||||
const promptMessages = prompt?.type === PromptType.Chat && Array.isArray(prompt.prompt)
|
||||
? prompt.prompt
|
||||
: [];
|
||||
const placeholderNames = extractPlaceholderNames(promptMessages as PromptMessage[]);
|
||||
|
||||
return [...extractedVariables, ...placeholderNames];
|
||||
}, [promptId, promptMeta.data]);
|
||||
|
||||
const promptsByName = useMemo(
|
||||
|
||||
@@ -2,12 +2,11 @@ import { z } from "zod/v4";
|
||||
import { randomUUID } from "crypto";
|
||||
import {
|
||||
type ExperimentMetadata,
|
||||
ExperimentCreateQueue,
|
||||
QueueJobs,
|
||||
QueueName,
|
||||
redis,
|
||||
ZodModelConfig,
|
||||
ExperimentCreateQueue,
|
||||
type PlaceholderMessage,
|
||||
} from "@langfuse/shared/src/server";
|
||||
import {
|
||||
createTRPCRouter,
|
||||
@@ -20,6 +19,8 @@ import {
|
||||
datasetItemMatchesVariable,
|
||||
UnauthorizedError,
|
||||
PromptType,
|
||||
extractPlaceholderNames,
|
||||
type PromptMessage,
|
||||
} from "@langfuse/shared";
|
||||
import { throwIfNoProjectAccess } from "@/src/features/rbac/utils/checkProjectAccess";
|
||||
|
||||
@@ -98,24 +99,17 @@ export const experimentsRouter = createTRPCRouter({
|
||||
: JSON.stringify(prompt.prompt),
|
||||
);
|
||||
|
||||
if (!Boolean(extractedVariables.length)) {
|
||||
return {
|
||||
isValid: false,
|
||||
message: "Selected prompt has no variables.",
|
||||
};
|
||||
}
|
||||
|
||||
const promptMessages = prompt?.type === PromptType.Chat && Array.isArray(prompt.prompt)
|
||||
? prompt.prompt
|
||||
: [];
|
||||
const hasPlaceholders = promptMessages.some((msg): msg is PlaceholderMessage =>
|
||||
(msg as PlaceholderMessage).type === "placeholder"
|
||||
);
|
||||
const placeholderNames = extractPlaceholderNames(promptMessages as PromptMessage[]);
|
||||
|
||||
if (hasPlaceholders) {
|
||||
const allVariables = [...extractedVariables, ...placeholderNames];
|
||||
|
||||
if (!Boolean(allVariables.length)) {
|
||||
return {
|
||||
isValid: false,
|
||||
message: "Selected prompt has placeholders, those are not yet supported for experiments.",
|
||||
message: "Selected prompt has no variables or placeholders.",
|
||||
};
|
||||
}
|
||||
|
||||
@@ -136,7 +130,7 @@ export const experimentsRouter = createTRPCRouter({
|
||||
|
||||
const variablesMap = validateDatasetItems(
|
||||
datasetItems,
|
||||
extractedVariables,
|
||||
allVariables,
|
||||
);
|
||||
|
||||
if (!Boolean(Object.keys(variablesMap).length)) {
|
||||
|
||||
@@ -619,7 +619,7 @@ export class OtelIngestionProcessor {
|
||||
}
|
||||
|
||||
private hasTraceUpdates(attributes: Record<string, unknown>): boolean {
|
||||
return [
|
||||
const hasExactMatchingAttributeName = [
|
||||
LangfuseOtelSpanAttributes.TRACE_NAME,
|
||||
LangfuseOtelSpanAttributes.TRACE_INPUT,
|
||||
LangfuseOtelSpanAttributes.TRACE_OUTPUT,
|
||||
@@ -637,6 +637,13 @@ export class OtelIngestionProcessor {
|
||||
`${LangfuseOtelSpanAttributes.TRACE_METADATA}.langfuse_user_id`,
|
||||
`${LangfuseOtelSpanAttributes.TRACE_METADATA}.langfuse_tags`,
|
||||
].some((traceAttribute) => Boolean(attributes[traceAttribute]));
|
||||
|
||||
const attributeKeys = Object.keys(attributes);
|
||||
const hasTraceMetadataKey = attributeKeys.some((key) =>
|
||||
key.startsWith(LangfuseOtelSpanAttributes.TRACE_METADATA),
|
||||
);
|
||||
|
||||
return hasExactMatchingAttributeName || hasTraceMetadataKey;
|
||||
}
|
||||
|
||||
private extractResourceAttributes(
|
||||
|
||||
@@ -484,7 +484,7 @@ export function PromptTable() {
|
||||
setFilterState={useDebounce(setFilterState)}
|
||||
columnsWithCustomSelect={["labels", "tags"]}
|
||||
searchConfig={{
|
||||
metadataSearchFields: ["Name"],
|
||||
metadataSearchFields: ["Name", "Tags"],
|
||||
updateQuery: useDebounce(setSearchQuery, 300),
|
||||
currentQuery: searchQuery ?? undefined,
|
||||
tableAllowsFullTextSearch: false,
|
||||
|
||||
@@ -92,7 +92,7 @@ export const promptRouter = createTRPCRouter({
|
||||
: Prisma.empty;
|
||||
|
||||
const searchFilter = input.searchQuery
|
||||
? Prisma.sql` AND p.name ILIKE ${`%${input.searchQuery}%`}`
|
||||
? Prisma.sql` AND (p.name ILIKE ${`%${input.searchQuery}%`} OR EXISTS (SELECT 1 FROM UNNEST(p.tags) AS tag WHERE tag ILIKE ${`%${input.searchQuery}%`}))`
|
||||
: Prisma.empty;
|
||||
|
||||
const [prompts, promptCount] = await Promise.all([
|
||||
@@ -119,19 +119,19 @@ export const promptRouter = createTRPCRouter({
|
||||
searchFilter,
|
||||
),
|
||||
),
|
||||
// promptCount
|
||||
ctx.prisma.$queryRaw<Array<{ totalCount: bigint }>>(
|
||||
generatePromptQuery(
|
||||
Prisma.sql` count(*) AS "totalCount"`,
|
||||
input.projectId,
|
||||
filterCondition,
|
||||
Prisma.empty,
|
||||
1, // limit
|
||||
0, // page,
|
||||
pathFilter,
|
||||
searchFilter,
|
||||
),
|
||||
// promptCount
|
||||
ctx.prisma.$queryRaw<Array<{ totalCount: bigint }>>(
|
||||
generatePromptQuery(
|
||||
Prisma.sql` count(*) AS "totalCount"`,
|
||||
input.projectId,
|
||||
filterCondition,
|
||||
Prisma.empty,
|
||||
1, // limit
|
||||
0, // page,
|
||||
pathFilter,
|
||||
searchFilter,
|
||||
),
|
||||
),
|
||||
]);
|
||||
|
||||
return {
|
||||
|
||||
@@ -48,15 +48,14 @@ import {
|
||||
Hash,
|
||||
BarChart3,
|
||||
Table,
|
||||
X,
|
||||
Plus,
|
||||
X,
|
||||
} from "lucide-react";
|
||||
import {
|
||||
buildWidgetName,
|
||||
buildWidgetDescription,
|
||||
formatMetricName,
|
||||
} from "@/src/features/widgets/utils";
|
||||
import { Badge } from "@/src/components/ui/badge";
|
||||
import {
|
||||
MAX_PIVOT_TABLE_DIMENSIONS,
|
||||
MAX_PIVOT_TABLE_METRICS,
|
||||
@@ -284,40 +283,74 @@ export function WidgetForm({
|
||||
setPivotDimensions(newDimensions);
|
||||
};
|
||||
|
||||
// Helper functions for multiple metrics management
|
||||
const addMetric = () => {
|
||||
// Check if we've reached the maximum metrics limit
|
||||
if (selectedMetrics.length >= MAX_PIVOT_TABLE_METRICS) {
|
||||
showErrorToast(
|
||||
"Error",
|
||||
`Maximum ${MAX_PIVOT_TABLE_METRICS} metrics allowed for pivot tables`,
|
||||
);
|
||||
return;
|
||||
// Helper function for updating pivot table metrics
|
||||
const updatePivotMetric = (
|
||||
index: number,
|
||||
measure: string,
|
||||
aggregation?: z.infer<typeof metricAggregations>,
|
||||
) => {
|
||||
const newMetrics = [...selectedMetrics];
|
||||
|
||||
if (measure && measure !== "none") {
|
||||
let finalAggregation: z.infer<typeof metricAggregations>;
|
||||
|
||||
if (measure === "count") {
|
||||
finalAggregation = "count";
|
||||
} else {
|
||||
// Get available aggregations for this measure at this index
|
||||
const availableAggregations = getAvailableAggregations(index, measure);
|
||||
|
||||
if (aggregation && availableAggregations.includes(aggregation)) {
|
||||
// Use provided aggregation if it's available
|
||||
finalAggregation = aggregation as z.infer<typeof metricAggregations>;
|
||||
} else {
|
||||
// Use the first available aggregation as default
|
||||
finalAggregation =
|
||||
availableAggregations.length > 0
|
||||
? availableAggregations[0]
|
||||
: ("sum" as z.infer<typeof metricAggregations>);
|
||||
}
|
||||
}
|
||||
|
||||
const newMetric: SelectedMetric = {
|
||||
id: `${finalAggregation}_${measure}`,
|
||||
measure: measure,
|
||||
aggregation: finalAggregation as z.infer<typeof metricAggregations>,
|
||||
label: `${startCase(finalAggregation)} ${startCase(measure)}`,
|
||||
};
|
||||
|
||||
// Set the metric at the specified index
|
||||
newMetrics[index] = newMetric;
|
||||
} else {
|
||||
// Clear this metric and all subsequent ones
|
||||
newMetrics.splice(index);
|
||||
}
|
||||
|
||||
const newMetric: SelectedMetric = {
|
||||
id: `${selectedAggregation}_${selectedMeasure}`,
|
||||
measure: selectedMeasure,
|
||||
aggregation: selectedAggregation,
|
||||
label: `${startCase(selectedAggregation)} ${startCase(selectedMeasure)}`,
|
||||
};
|
||||
setSelectedMetrics(newMetrics);
|
||||
};
|
||||
|
||||
// Check if this metric combination already exists
|
||||
const existingMetric = selectedMetrics.find((m) => m.id === newMetric.id);
|
||||
if (existingMetric) {
|
||||
showErrorToast("Error", "This metric combination is already selected");
|
||||
return;
|
||||
// Add a new empty metric slot
|
||||
const addNewMetricSlot = () => {
|
||||
if (selectedMetrics.length < MAX_PIVOT_TABLE_METRICS) {
|
||||
const newMetrics = [...selectedMetrics];
|
||||
newMetrics.push({
|
||||
id: `temp_${selectedMetrics.length}`,
|
||||
measure: "",
|
||||
aggregation: "sum" as z.infer<typeof metricAggregations>,
|
||||
label: "",
|
||||
});
|
||||
setSelectedMetrics(newMetrics);
|
||||
}
|
||||
|
||||
setSelectedMetrics([...selectedMetrics, newMetric]);
|
||||
};
|
||||
|
||||
const removeMetric = (metricId: string) => {
|
||||
setSelectedMetrics(selectedMetrics.filter((m) => m.id !== metricId));
|
||||
};
|
||||
|
||||
const clearAllMetrics = () => {
|
||||
setSelectedMetrics([]);
|
||||
// Remove a metric slot and roll up subsequent ones
|
||||
const removeMetricSlot = (index: number) => {
|
||||
if (index > 0) {
|
||||
// Can't remove the first metric (it's required)
|
||||
const newMetrics = [...selectedMetrics];
|
||||
newMetrics.splice(index, 1); // Remove only the metric at this index
|
||||
setSelectedMetrics(newMetrics);
|
||||
}
|
||||
};
|
||||
|
||||
const traceFilterOptions = api.traces.filterOptions.useQuery(
|
||||
@@ -542,6 +575,40 @@ export function WidgetForm({
|
||||
// Get available metrics for the selected view
|
||||
const availableMetrics = useMemo(() => {
|
||||
const viewDeclaration = viewDeclarations[selectedView];
|
||||
|
||||
// For pivot tables, only show measures that still have available aggregations
|
||||
if (selectedChartType === "PIVOT_TABLE") {
|
||||
return Object.entries(viewDeclaration.measures)
|
||||
.filter(([measureKey]) => {
|
||||
// For count, there's only one aggregation option
|
||||
if (measureKey === "count") {
|
||||
return !selectedMetrics.some((m) => m.measure === "count");
|
||||
}
|
||||
|
||||
// For other measures, check if there are any aggregations left
|
||||
const selectedAggregationsForMeasure = selectedMetrics
|
||||
.filter((m) => m.measure === measureKey)
|
||||
.map((m) => m.aggregation);
|
||||
|
||||
const availableAggregationsForMeasure =
|
||||
metricAggregations.options.filter(
|
||||
(agg) =>
|
||||
agg !== "histogram" &&
|
||||
!selectedAggregationsForMeasure.includes(agg),
|
||||
);
|
||||
|
||||
return availableAggregationsForMeasure.length > 0;
|
||||
})
|
||||
.map(([key]) => ({
|
||||
value: key,
|
||||
label: startCase(key),
|
||||
}))
|
||||
.sort((a, b) =>
|
||||
a.label.localeCompare(b.label, "en", { sensitivity: "base" }),
|
||||
);
|
||||
}
|
||||
|
||||
// For regular charts, show all metrics
|
||||
return Object.entries(viewDeclaration.measures)
|
||||
.map(([key]) => ({
|
||||
value: key,
|
||||
@@ -550,7 +617,64 @@ export function WidgetForm({
|
||||
.sort((a, b) =>
|
||||
a.label.localeCompare(b.label, "en", { sensitivity: "base" }),
|
||||
);
|
||||
}, [selectedView]);
|
||||
}, [selectedView, selectedChartType, selectedMetrics]);
|
||||
|
||||
// Get available aggregations for a specific metric index in pivot tables
|
||||
const getAvailableAggregations = (
|
||||
metricIndex: number,
|
||||
measureKey: string,
|
||||
): z.infer<typeof metricAggregations>[] => {
|
||||
if (selectedChartType === "PIVOT_TABLE" && measureKey) {
|
||||
return metricAggregations.options.filter(
|
||||
(agg) =>
|
||||
!selectedMetrics.some(
|
||||
(m, idx) =>
|
||||
idx !== metricIndex &&
|
||||
m.measure === measureKey &&
|
||||
m.aggregation === agg,
|
||||
),
|
||||
) as z.infer<typeof metricAggregations>[];
|
||||
}
|
||||
return metricAggregations.options as z.infer<typeof metricAggregations>[];
|
||||
};
|
||||
|
||||
// Get available metrics for a specific metric index in pivot tables
|
||||
const getAvailableMetrics = (metricIndex: number) => {
|
||||
if (selectedChartType === "PIVOT_TABLE") {
|
||||
const viewDeclaration = viewDeclarations[selectedView];
|
||||
return Object.entries(viewDeclaration.measures)
|
||||
.filter(([measureKey]) => {
|
||||
// For count, there's only one aggregation option
|
||||
if (measureKey === "count") {
|
||||
return !selectedMetrics.some(
|
||||
(m, idx) => idx !== metricIndex && m.measure === "count",
|
||||
);
|
||||
}
|
||||
|
||||
// For other measures, check if there are any aggregations left
|
||||
const selectedAggregationsForMeasure = selectedMetrics
|
||||
.filter((m, idx) => idx !== metricIndex && m.measure === measureKey)
|
||||
.map((m) => m.aggregation);
|
||||
|
||||
const availableAggregationsForMeasure =
|
||||
metricAggregations.options.filter(
|
||||
(agg) =>
|
||||
agg !== "histogram" &&
|
||||
!selectedAggregationsForMeasure.includes(agg),
|
||||
);
|
||||
|
||||
return availableAggregationsForMeasure.length > 0;
|
||||
})
|
||||
.map(([key]) => ({
|
||||
value: key,
|
||||
label: startCase(key),
|
||||
}))
|
||||
.sort((a, b) =>
|
||||
a.label.localeCompare(b.label, "en", { sensitivity: "base" }),
|
||||
);
|
||||
}
|
||||
return availableMetrics;
|
||||
};
|
||||
|
||||
// Get available dimensions for the selected view
|
||||
const availableDimensions = useMemo(() => {
|
||||
@@ -584,10 +708,12 @@ export function WidgetForm({
|
||||
// Determine metrics based on chart type
|
||||
const queryMetrics =
|
||||
selectedChartType === "PIVOT_TABLE"
|
||||
? selectedMetrics.map((metric) => ({
|
||||
measure: metric.measure,
|
||||
aggregation: metric.aggregation,
|
||||
}))
|
||||
? selectedMetrics
|
||||
.filter((metric) => metric.measure && metric.measure !== "")
|
||||
.map((metric) => ({
|
||||
measure: metric.measure,
|
||||
aggregation: metric.aggregation,
|
||||
}))
|
||||
: [
|
||||
{
|
||||
measure: selectedMeasure,
|
||||
@@ -701,7 +827,10 @@ export function WidgetForm({
|
||||
}
|
||||
|
||||
// Validate pivot table requirements
|
||||
if (selectedChartType === "PIVOT_TABLE" && selectedMetrics.length === 0) {
|
||||
const validMetrics = selectedMetrics.filter(
|
||||
(m) => m.measure && m.measure !== "",
|
||||
);
|
||||
if (selectedChartType === "PIVOT_TABLE" && validMetrics.length === 0) {
|
||||
showErrorToast(
|
||||
"Error",
|
||||
"At least one metric is required for pivot tables",
|
||||
@@ -721,7 +850,7 @@ export function WidgetForm({
|
||||
: [],
|
||||
metrics:
|
||||
selectedChartType === "PIVOT_TABLE"
|
||||
? selectedMetrics.map((metric) => ({
|
||||
? validMetrics.map((metric) => ({
|
||||
measure: metric.measure,
|
||||
agg: metric.aggregation,
|
||||
}))
|
||||
@@ -766,9 +895,13 @@ export function WidgetForm({
|
||||
|
||||
// For pivot tables, extract actual metric names for the new formatting
|
||||
const isPivotTable = selectedChartType === "PIVOT_TABLE";
|
||||
|
||||
const validMetricsForNaming = selectedMetrics.filter(
|
||||
(m) => m.measure && m.measure !== "",
|
||||
);
|
||||
const metricNames =
|
||||
isPivotTable && selectedMetrics.length > 0
|
||||
? selectedMetrics.map((m) => m.id) // Use the ID which is "${aggregation}_${measure}"
|
||||
isPivotTable && validMetricsForNaming.length > 0
|
||||
? validMetricsForNaming.map((m) => m.id) // Use the ID which is "${aggregation}_${measure}"
|
||||
: undefined;
|
||||
|
||||
const suggested = buildWidgetName({
|
||||
@@ -777,7 +910,7 @@ export function WidgetForm({
|
||||
dimension: dimensionForNaming,
|
||||
view: selectedView,
|
||||
metrics: metricNames,
|
||||
isMultiMetric: isPivotTable && selectedMetrics.length > 0,
|
||||
isMultiMetric: isPivotTable && validMetricsForNaming.length > 0,
|
||||
});
|
||||
|
||||
setWidgetName(suggested);
|
||||
@@ -804,9 +937,12 @@ export function WidgetForm({
|
||||
|
||||
// For pivot tables, extract actual metric names for the new formatting
|
||||
const isPivotTable = selectedChartType === "PIVOT_TABLE";
|
||||
const validMetricsForDescription = selectedMetrics.filter(
|
||||
(m) => m.measure && m.measure !== "",
|
||||
);
|
||||
const metricNames =
|
||||
isPivotTable && selectedMetrics.length > 0
|
||||
? selectedMetrics.map((m) => m.id) // Use the ID which is "${aggregation}_${measure}"
|
||||
isPivotTable && validMetricsForDescription.length > 0
|
||||
? validMetricsForDescription.map((m) => m.id) // Use the ID which is "${aggregation}_${measure}"
|
||||
: undefined;
|
||||
|
||||
const suggested = buildWidgetDescription({
|
||||
@@ -816,7 +952,7 @@ export function WidgetForm({
|
||||
view: selectedView,
|
||||
filters: userFilterState,
|
||||
metrics: metricNames,
|
||||
isMultiMetric: isPivotTable && selectedMetrics.length > 0,
|
||||
isMultiMetric: isPivotTable && validMetricsForDescription.length > 0,
|
||||
});
|
||||
|
||||
setWidgetDescription(suggested);
|
||||
@@ -855,9 +991,42 @@ export function WidgetForm({
|
||||
value={selectedView}
|
||||
onValueChange={(value) => {
|
||||
if (value !== selectedView) {
|
||||
const newView = value as z.infer<typeof views>;
|
||||
const newViewDeclaration = viewDeclarations[newView];
|
||||
|
||||
// Reset regular chart fields
|
||||
setSelectedMeasure("count");
|
||||
setSelectedAggregation("count");
|
||||
setSelectedDimension("none");
|
||||
|
||||
// Handle pivot table metrics - filter out invalid measures for the new view
|
||||
if (selectedChartType === "PIVOT_TABLE") {
|
||||
const validMetrics = selectedMetrics.filter(
|
||||
(metric) =>
|
||||
metric.measure in newViewDeclaration.measures,
|
||||
);
|
||||
|
||||
// Ensure we have at least one valid metric (count is always available)
|
||||
if (validMetrics.length === 0) {
|
||||
validMetrics.push({
|
||||
id: "count_count",
|
||||
measure: "count",
|
||||
aggregation: "count" as z.infer<
|
||||
typeof metricAggregations
|
||||
>,
|
||||
label: "Count Count",
|
||||
});
|
||||
}
|
||||
|
||||
setSelectedMetrics(validMetrics);
|
||||
|
||||
// Handle pivot table dimensions - filter out invalid dimensions for the new view
|
||||
const validDimensions = pivotDimensions.filter(
|
||||
(dimension) =>
|
||||
dimension in newViewDeclaration.dimensions,
|
||||
);
|
||||
setPivotDimensions(validDimensions);
|
||||
}
|
||||
}
|
||||
setSelectedView(value as z.infer<typeof views>);
|
||||
}}
|
||||
@@ -887,118 +1056,145 @@ export function WidgetForm({
|
||||
{/* For pivot tables: multiple metrics selection */}
|
||||
{selectedChartType === "PIVOT_TABLE" ? (
|
||||
<div className="space-y-3">
|
||||
{/* Selected metrics display */}
|
||||
{selectedMetrics.length > 0 && (
|
||||
<div className="space-y-2">
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-sm font-medium">
|
||||
Selected Metrics ({selectedMetrics.length})
|
||||
</span>
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={clearAllMetrics}
|
||||
className="h-auto p-1 text-xs"
|
||||
>
|
||||
Clear All
|
||||
</Button>
|
||||
</div>
|
||||
<div className="flex flex-wrap gap-1">
|
||||
{selectedMetrics.map((metric) => (
|
||||
<Badge
|
||||
key={metric.id}
|
||||
variant="secondary"
|
||||
className="flex items-center gap-1"
|
||||
>
|
||||
<span className="text-xs">{metric.label}</span>
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => removeMetric(metric.id)}
|
||||
className="h-auto p-0 hover:bg-transparent"
|
||||
>
|
||||
<X className="h-3 w-3" />
|
||||
</Button>
|
||||
</Badge>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
{/* Metric selection dropdowns */}
|
||||
{Array.from(
|
||||
{ length: Math.max(1, selectedMetrics.length) },
|
||||
(_, index) => {
|
||||
const isEnabled =
|
||||
index === 0 ||
|
||||
(selectedMetrics[index - 1] &&
|
||||
selectedMetrics[index - 1].measure);
|
||||
const currentMetric = selectedMetrics[index];
|
||||
const currentMeasure = currentMetric?.measure || "";
|
||||
const currentAggregation =
|
||||
currentMetric?.aggregation || "sum";
|
||||
|
||||
const metricsForIndex = getAvailableMetrics(index);
|
||||
const aggregationsForIndex = getAvailableAggregations(
|
||||
index,
|
||||
currentMeasure,
|
||||
);
|
||||
|
||||
const canEdit = metricsForIndex.length > 0;
|
||||
|
||||
return (
|
||||
<div key={index} className="space-y-2">
|
||||
<div className="flex items-center justify-between">
|
||||
<Label htmlFor={`pivot-metric-${index}`}>
|
||||
Metric {index + 1}{" "}
|
||||
{index === 0 ? "(Required)" : "(Optional)"}
|
||||
</Label>
|
||||
{index > 0 && (
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => removeMetricSlot(index)}
|
||||
className="h-6 w-6 p-0 text-muted-foreground hover:text-destructive"
|
||||
>
|
||||
<X className="h-4 w-4" />
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="flex-1">
|
||||
<Select
|
||||
value={currentMeasure}
|
||||
onValueChange={(value) =>
|
||||
updatePivotMetric(
|
||||
index,
|
||||
value,
|
||||
// Don't pass current aggregation when measure changes
|
||||
// Let the function determine the best default
|
||||
undefined,
|
||||
)
|
||||
}
|
||||
disabled={!isEnabled || !canEdit}
|
||||
>
|
||||
<SelectTrigger id={`pivot-metric-${index}`}>
|
||||
<SelectValue
|
||||
placeholder={
|
||||
!isEnabled
|
||||
? "Select previous metric first"
|
||||
: !canEdit
|
||||
? "No more measures available"
|
||||
: "Select measure"
|
||||
}
|
||||
/>
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{metricsForIndex.map((metric) => {
|
||||
const meta =
|
||||
viewDeclarations[selectedView]
|
||||
?.measures?.[metric.value];
|
||||
return (
|
||||
<WidgetPropertySelectItem
|
||||
key={metric.value}
|
||||
value={metric.value}
|
||||
label={metric.label}
|
||||
description={meta?.description}
|
||||
unit={meta?.unit}
|
||||
type={meta?.type}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
{currentMeasure && currentMeasure !== "count" && (
|
||||
<div className="flex-1">
|
||||
<Select
|
||||
value={currentAggregation}
|
||||
onValueChange={(value) =>
|
||||
updatePivotMetric(
|
||||
index,
|
||||
currentMeasure,
|
||||
value as z.infer<
|
||||
typeof metricAggregations
|
||||
>,
|
||||
)
|
||||
}
|
||||
>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="Select aggregation" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{aggregationsForIndex.map(
|
||||
(aggregation) => (
|
||||
<SelectItem
|
||||
key={aggregation}
|
||||
value={aggregation}
|
||||
>
|
||||
{startCase(aggregation)}
|
||||
</SelectItem>
|
||||
),
|
||||
)}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
)}
|
||||
|
||||
{/* Add new metric controls */}
|
||||
<div className="space-y-2 rounded-md border p-3">
|
||||
<span className="text-sm font-medium">Add Metric</span>
|
||||
<div className="grid grid-cols-2 gap-2">
|
||||
<Select
|
||||
value={selectedMeasure}
|
||||
onValueChange={(value) => setSelectedMeasure(value)}
|
||||
{/* Add new metric button */}
|
||||
{selectedMetrics.length < MAX_PIVOT_TABLE_METRICS &&
|
||||
getAvailableMetrics(selectedMetrics.length).length >
|
||||
0 && (
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={addNewMetricSlot}
|
||||
className="w-full"
|
||||
>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="Select measure" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{availableMetrics.map((metric) => {
|
||||
const meta =
|
||||
viewDeclarations[selectedView]?.measures?.[
|
||||
metric.value
|
||||
];
|
||||
return (
|
||||
<WidgetPropertySelectItem
|
||||
key={metric.value}
|
||||
value={metric.value}
|
||||
label={metric.label}
|
||||
description={meta?.description}
|
||||
unit={meta?.unit}
|
||||
type={meta?.type}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
|
||||
{selectedMeasure !== "count" && (
|
||||
<Select
|
||||
value={selectedAggregation}
|
||||
onValueChange={(value) =>
|
||||
setSelectedAggregation(
|
||||
value as z.infer<typeof metricAggregations>,
|
||||
)
|
||||
}
|
||||
>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="Aggregation" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{metricAggregations.options.map((aggregation) => (
|
||||
<SelectItem
|
||||
key={aggregation}
|
||||
value={aggregation}
|
||||
>
|
||||
{startCase(aggregation)}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
)}
|
||||
</div>
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={addMetric}
|
||||
className="w-full"
|
||||
disabled={
|
||||
selectedMetrics.length >= MAX_PIVOT_TABLE_METRICS
|
||||
}
|
||||
>
|
||||
<Plus className="mr-1 h-3 w-3" />
|
||||
Add Metric
|
||||
{selectedMetrics.length >= MAX_PIVOT_TABLE_METRICS &&
|
||||
` (Max ${MAX_PIVOT_TABLE_METRICS})`}
|
||||
</Button>
|
||||
</div>
|
||||
<Plus className="mr-1 h-3 w-3" />
|
||||
Add Metric {selectedMetrics.length + 1}
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
) : (
|
||||
/* For regular charts: single metric selection */
|
||||
|
||||
+2
-2
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "worker",
|
||||
"version": "3.78.0",
|
||||
"version": "3.78.2",
|
||||
"description": "",
|
||||
"license": "MIT",
|
||||
"private": true,
|
||||
@@ -13,7 +13,7 @@
|
||||
"start": "dotenv -e ../.env -- node dist/index.js",
|
||||
"build": "tsc",
|
||||
"dev": "dotenv -e ../.env -- nodemon src/index.ts",
|
||||
"lint": "eslint . --ext .js,.jsx,.ts,.tsx --max-warnings 21",
|
||||
"lint": "eslint . --ext .js,.jsx,.ts,.tsx --max-warnings 0",
|
||||
"lint:fix": "eslint . --ext .js,.jsx,.ts,.tsx --fix",
|
||||
"refill-ingestion-events": "dotenv -e ../.env -- tsx src/scripts/s3-ingestion-event-relpay.ts",
|
||||
"refill-billing-event": "dotenv -e ../.env -- tsx src/scripts/refill-billing-event.ts"
|
||||
|
||||
@@ -313,6 +313,187 @@ describe("create experiment jobs", () => {
|
||||
}, 10_000);
|
||||
});
|
||||
|
||||
describe("create experiment jobs with placeholders", () => {
|
||||
const setupPlaceholderTest = async (promptConfig: any, datasetItemInput: any) => {
|
||||
await pruneDatabase();
|
||||
const projectId = "7a88fb47-b4e2-43b8-a06c-a5ce950dc53a";
|
||||
const datasetId = randomUUID();
|
||||
const runId = randomUUID();
|
||||
const promptId = "03f834cc-c089-4bcb-9add-b14cadcdf47c";
|
||||
|
||||
// Create prompt
|
||||
await prisma.prompt.create({
|
||||
data: {
|
||||
id: promptId,
|
||||
projectId,
|
||||
name: promptConfig.name,
|
||||
prompt: promptConfig.prompt,
|
||||
type: "chat",
|
||||
version: 1,
|
||||
createdBy: "test-user",
|
||||
},
|
||||
});
|
||||
|
||||
// Create dataset
|
||||
await prisma.dataset.create({
|
||||
data: {
|
||||
id: datasetId,
|
||||
projectId,
|
||||
name: "Test Dataset",
|
||||
},
|
||||
});
|
||||
|
||||
// Create dataset run with metadata
|
||||
await kyselyPrisma.$kysely
|
||||
.insertInto("dataset_runs")
|
||||
.values({
|
||||
id: runId,
|
||||
name: "Test Run",
|
||||
project_id: projectId,
|
||||
dataset_id: datasetId,
|
||||
metadata: {
|
||||
prompt_id: promptId,
|
||||
provider: "openai",
|
||||
model: "gpt-3.5-turbo",
|
||||
model_params: { temperature: 0 },
|
||||
},
|
||||
})
|
||||
.execute();
|
||||
|
||||
// Create dataset item
|
||||
await prisma.datasetItem.create({
|
||||
data: {
|
||||
id: randomUUID(),
|
||||
projectId,
|
||||
datasetId,
|
||||
input: datasetItemInput,
|
||||
},
|
||||
});
|
||||
|
||||
// Create API key
|
||||
await prisma.llmApiKeys.create({
|
||||
data: {
|
||||
id: randomUUID(),
|
||||
projectId,
|
||||
provider: "openai",
|
||||
adapter: LLMAdapter.OpenAI,
|
||||
displaySecretKey: "test-key",
|
||||
secretKey: encrypt("test-key"),
|
||||
},
|
||||
});
|
||||
|
||||
return { projectId, datasetId, runId };
|
||||
};
|
||||
|
||||
test("creates experiment job with multiple placeholders containing variables", async () => {
|
||||
const { projectId, datasetId, runId } = await setupPlaceholderTest(
|
||||
{
|
||||
name: "Test Multiple Placeholders",
|
||||
prompt: [
|
||||
{ role: "system", content: "You are a helpful assistant." },
|
||||
{ type: "placeholder", name: "conversation_history" },
|
||||
{ type: "placeholder", name: "user_context" },
|
||||
{ role: "user", content: "Please help me." }
|
||||
]
|
||||
},
|
||||
{
|
||||
conversation_history: [
|
||||
{ role: "user", content: "Hello {{name}}!" },
|
||||
{ role: "assistant", content: "Hi there!" }
|
||||
],
|
||||
user_context: [
|
||||
{ role: "system", content: "User is a {{role}}" }
|
||||
],
|
||||
name: "John",
|
||||
role: "developer"
|
||||
}
|
||||
);
|
||||
|
||||
const payload = {
|
||||
projectId,
|
||||
datasetId,
|
||||
runId,
|
||||
};
|
||||
|
||||
await createExperimentJob({ event: payload });
|
||||
|
||||
const runItems = await kyselyPrisma.$kysely
|
||||
.selectFrom("dataset_run_items")
|
||||
.selectAll()
|
||||
.where("project_id", "=", projectId)
|
||||
.execute();
|
||||
|
||||
expect(runItems.length).toBe(1);
|
||||
expect(runItems[0].project_id).toBe(projectId);
|
||||
expect(runItems[0].dataset_run_id).toBe(runId);
|
||||
expect(runItems[0].trace_id).toBeDefined();
|
||||
}, 10_000);
|
||||
|
||||
test("handles empty placeholder arrays", async () => {
|
||||
const { projectId, datasetId, runId } = await setupPlaceholderTest(
|
||||
{
|
||||
name: "Test Empty Placeholder",
|
||||
prompt: [
|
||||
{ role: "system", content: "You are a helpful assistant." },
|
||||
{ type: "placeholder", name: "empty_history" },
|
||||
{ role: "user", content: "Start conversation." }
|
||||
]
|
||||
},
|
||||
{ empty_history: [] }
|
||||
);
|
||||
|
||||
const payload = {
|
||||
projectId,
|
||||
datasetId,
|
||||
runId,
|
||||
};
|
||||
|
||||
await createExperimentJob({ event: payload });
|
||||
|
||||
const runItems = await kyselyPrisma.$kysely
|
||||
.selectFrom("dataset_run_items")
|
||||
.selectAll()
|
||||
.where("project_id", "=", projectId)
|
||||
.execute();
|
||||
|
||||
expect(runItems.length).toBe(1);
|
||||
expect(runItems[0].project_id).toBe(projectId);
|
||||
expect(runItems[0].dataset_run_id).toBe(runId);
|
||||
expect(runItems[0].trace_id).toBeDefined();
|
||||
}, 10_000);
|
||||
|
||||
test("fails when placeholder has invalid message format", async () => {
|
||||
const { projectId, datasetId, runId } = await setupPlaceholderTest(
|
||||
{
|
||||
name: "Test Invalid Placeholder",
|
||||
prompt: [
|
||||
{ role: "system", content: "You are a helpful assistant." },
|
||||
{ type: "placeholder", name: "invalid_messages" },
|
||||
{ role: "user", content: "Help me." }
|
||||
]
|
||||
},
|
||||
{ invalid_messages: "this should be an array or object" }
|
||||
);
|
||||
|
||||
const payload = {
|
||||
projectId,
|
||||
datasetId,
|
||||
runId,
|
||||
};
|
||||
|
||||
await createExperimentJob({ event: payload });
|
||||
|
||||
// Should not create run items for invalid placeholder format
|
||||
const runItems = await kyselyPrisma.$kysely
|
||||
.selectFrom("dataset_run_items")
|
||||
.selectAll()
|
||||
.where("project_id", "=", projectId)
|
||||
.execute();
|
||||
|
||||
expect(runItems.length).toBe(0);
|
||||
}, 10_000);
|
||||
});
|
||||
|
||||
describe("create experiment job calls with langfuse server side tracing", async () => {
|
||||
await pruneDatabase();
|
||||
const mockEvent = {
|
||||
|
||||
@@ -1 +1 @@
|
||||
export const VERSION = "v3.78.0";
|
||||
export const VERSION = "v3.78.2";
|
||||
|
||||
+1
-1
@@ -232,6 +232,6 @@ const EnvSchema = z.object({
|
||||
});
|
||||
|
||||
export const env: z.infer<typeof EnvSchema> =
|
||||
process.env.DOCKER_BUILD === "1"
|
||||
process.env.DOCKER_BUILD === "1" // eslint-disable-line turbo/no-undeclared-env-vars
|
||||
? (process.env as any)
|
||||
: EnvSchema.parse(removeEmptyEnvVariables(process.env));
|
||||
|
||||
@@ -49,6 +49,7 @@ import {
|
||||
TraceDomain,
|
||||
Observation,
|
||||
DatasetItem,
|
||||
QUEUE_ERROR_MESSAGES,
|
||||
} from "@langfuse/shared";
|
||||
import { kyselyPrisma, prisma } from "@langfuse/shared/src/db";
|
||||
import { backOff } from "exponential-backoff";
|
||||
@@ -877,11 +878,11 @@ export async function extractVariablesFromTracingData({
|
||||
// user facing errors
|
||||
if (!observation) {
|
||||
logger.warn(
|
||||
`Observation ${mapping.objectName} for trace ${traceId} not found. Please ensure the mapped data exists and consider extending the job delay.`,
|
||||
`Observation ${mapping.objectName} for trace ${traceId} not found. ${QUEUE_ERROR_MESSAGES.MAPPED_DATA_ERROR}`,
|
||||
);
|
||||
// this should only happen for deleted data or data replication lags across clickhouse nodes.
|
||||
throw new LangfuseNotFoundError(
|
||||
`Observation ${mapping.objectName} for trace ${traceId} not found. Please ensure the mapped data exists and consider extending the job delay.`,
|
||||
`Observation ${mapping.objectName} for trace ${traceId} not found. ${QUEUE_ERROR_MESSAGES.MAPPED_DATA_ERROR}`,
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -18,11 +18,13 @@ import {
|
||||
import { kyselyPrisma, prisma } from "@langfuse/shared/src/db";
|
||||
import { type ExperimentCreateEventSchema } from "@langfuse/shared/src/server";
|
||||
import {
|
||||
datasetItemMatchesVariable,
|
||||
extractVariables,
|
||||
InvalidRequestError,
|
||||
LangfuseNotFoundError,
|
||||
type Prisma,
|
||||
extractVariables,
|
||||
datasetItemMatchesVariable,
|
||||
PromptType,
|
||||
QUEUE_ERROR_MESSAGES,
|
||||
stringifyValue,
|
||||
} from "@langfuse/shared";
|
||||
import { backOff } from "exponential-backoff";
|
||||
@@ -45,11 +47,14 @@ const replaceVariablesInPrompt = (
|
||||
prompt: PromptContent,
|
||||
itemInput: Record<string, any>,
|
||||
variables: string[],
|
||||
placeholderNames: string[] = [],
|
||||
): ChatMessage[] => {
|
||||
const processContent = (content: string) => {
|
||||
// Extract only relevant variables from itemInput
|
||||
// Extract only Handlebars variables from itemInput (exclude message placeholders)
|
||||
const filteredContext = Object.fromEntries(
|
||||
Object.entries(itemInput).filter(([key]) => variables.includes(key)),
|
||||
Object.entries(itemInput).filter(
|
||||
([key]) => variables.includes(key) && !placeholderNames.includes(key),
|
||||
),
|
||||
);
|
||||
|
||||
// Apply Handlebars ONLY if the content contains `{{variable}}` pattern
|
||||
@@ -70,42 +75,47 @@ const replaceVariablesInPrompt = (
|
||||
];
|
||||
}
|
||||
|
||||
const placeholderNames = extractPlaceholderNames(prompt as PromptMessage[]);
|
||||
const placeholderValues: MessagePlaceholderValues = {};
|
||||
// itemInput to placeholderValues
|
||||
for (const placeholderName of placeholderNames) {
|
||||
if (!(placeholderName in itemInput)) {
|
||||
// TODO: handle missing placeholder values
|
||||
// throw new Error(`Missing placeholder value for '${placeholderName}'`);
|
||||
continue;
|
||||
// TODO: should we throw?
|
||||
throw new Error(`Missing placeholder value for '${placeholderName}'`);
|
||||
}
|
||||
const value = itemInput[placeholderName];
|
||||
|
||||
// for stringified arrays (e.g. from dataset processing)
|
||||
let actualValue = value;
|
||||
if (typeof value === 'string') {
|
||||
if (typeof value === "string") {
|
||||
try {
|
||||
actualValue = JSON.parse(value);
|
||||
} catch (_e) {
|
||||
throw new Error(`Invalid placeholder value for '${placeholderName}': unable to parse JSON`);
|
||||
throw new Error(
|
||||
`Invalid placeholder value for '${placeholderName}': unable to parse JSON`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (!Array.isArray(actualValue)) {
|
||||
throw new Error(`Placeholder '${placeholderName}' must be an array of messages`);
|
||||
throw new Error(
|
||||
`Placeholder '${placeholderName}' must be an array of messages`,
|
||||
);
|
||||
}
|
||||
|
||||
const validMessages = actualValue.every(msg =>
|
||||
typeof msg === 'object' &&
|
||||
msg !== null &&
|
||||
'role' in msg &&
|
||||
'content' in msg
|
||||
const validMessages = actualValue.every(
|
||||
(msg) =>
|
||||
typeof msg === "object" &&
|
||||
msg !== null &&
|
||||
"role" in msg &&
|
||||
"content" in msg,
|
||||
);
|
||||
if (!validMessages) {
|
||||
throw new Error(`Invalid placeholder value for '${placeholderName}': messages must have 'role' and 'content' properties`);
|
||||
throw new Error(
|
||||
`Invalid placeholder value for '${placeholderName}': messages must have 'role' and 'content' properties`,
|
||||
);
|
||||
}
|
||||
|
||||
placeholderValues[placeholderName] = actualValue.map(msg => ({
|
||||
placeholderValues[placeholderName] = actualValue.map((msg) => ({
|
||||
...msg,
|
||||
type: ChatMessageType.PublicAPICreated as const,
|
||||
}));
|
||||
@@ -114,11 +124,9 @@ const replaceVariablesInPrompt = (
|
||||
const compiledMessages = compileChatMessages(
|
||||
prompt as PromptMessage[],
|
||||
placeholderValues,
|
||||
{}
|
||||
{},
|
||||
);
|
||||
|
||||
// TODO: validate correctness
|
||||
// handlebars variable substitution to all messages
|
||||
return compiledMessages.map((message) => ({
|
||||
...message,
|
||||
content: processContent(message.content),
|
||||
@@ -227,13 +235,13 @@ export const createExperimentJob = async ({
|
||||
});
|
||||
if (!apiKey) {
|
||||
throw new LangfuseNotFoundError(
|
||||
`API key for provider ${provider} not found`,
|
||||
`${QUEUE_ERROR_MESSAGES.API_KEY_ERROR} ${provider} not found`,
|
||||
);
|
||||
}
|
||||
const validatedApiKey = LLMApiKeySchema.safeParse(apiKey);
|
||||
if (!validatedApiKey.success) {
|
||||
throw new InvalidRequestError(
|
||||
`API key for provider ${provider} not found.`,
|
||||
`${QUEUE_ERROR_MESSAGES.API_KEY_ERROR} ${provider} not found.`,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -251,15 +259,16 @@ export const createExperimentJob = async ({
|
||||
|
||||
// extract variables from prompt
|
||||
const extractedVariables = extractVariables(
|
||||
prompt?.type === "text"
|
||||
prompt?.type === PromptType.Text
|
||||
? (prompt.prompt?.toString() ?? "")
|
||||
: JSON.stringify(prompt.prompt),
|
||||
);
|
||||
|
||||
// also extract placeholder names if prompt is an array
|
||||
const placeholderNames = prompt?.type !== "text" && Array.isArray(validatedPrompt.data)
|
||||
? extractPlaceholderNames(validatedPrompt.data as PromptMessage[])
|
||||
: [];
|
||||
// also extract placeholder names if prompt is a chat prompt
|
||||
const placeholderNames =
|
||||
prompt?.type === PromptType.Chat && Array.isArray(validatedPrompt.data)
|
||||
? extractPlaceholderNames(validatedPrompt.data as PromptMessage[])
|
||||
: [];
|
||||
const allVariables = [...extractedVariables, ...placeholderNames];
|
||||
|
||||
// validate dataset items against prompt configuration
|
||||
@@ -275,7 +284,7 @@ export const createExperimentJob = async ({
|
||||
|
||||
if (!validatedDatasetItems.length) {
|
||||
throw new InvalidRequestError(
|
||||
`No Dataset ${datasetId} item input matches expected prompt variable format`,
|
||||
`No Dataset ${datasetId} item input matches expected prompt variables or placeholders format`,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -305,7 +314,8 @@ export const createExperimentJob = async ({
|
||||
messages = replaceVariablesInPrompt(
|
||||
validatedPrompt.data,
|
||||
datasetItem.input, // validated format
|
||||
extractedVariables,
|
||||
allVariables,
|
||||
placeholderNames,
|
||||
);
|
||||
} catch (error) {
|
||||
// skip this dataset item if there is an error replacing variables
|
||||
|
||||
@@ -1,5 +1,10 @@
|
||||
import { Job } from "bullmq";
|
||||
import { ApiError, BaseError, LangfuseNotFoundError } from "@langfuse/shared";
|
||||
import {
|
||||
ApiError,
|
||||
BaseError,
|
||||
LangfuseNotFoundError,
|
||||
QUEUE_ERROR_MESSAGES,
|
||||
} from "@langfuse/shared";
|
||||
import { kyselyPrisma } from "@langfuse/shared/src/db";
|
||||
import { sql } from "kysely";
|
||||
import {
|
||||
@@ -150,22 +155,20 @@ export const evalJobExecutorQueueProcessor = async (
|
||||
e instanceof LangfuseNotFoundError ||
|
||||
(e instanceof BaseError &&
|
||||
e.message.includes(
|
||||
"Could not parse response content as the length limit was reached",
|
||||
QUEUE_ERROR_MESSAGES.OUTPUT_TOKENS_TOO_LONG_ERROR,
|
||||
)) || // output tokens too long
|
||||
(e instanceof BaseError && e.message.includes("API key for provider")) || // api key not provided
|
||||
(e instanceof BaseError &&
|
||||
e.message.includes(
|
||||
"`No default model or custom model found for project",
|
||||
)) || // api key not provided
|
||||
e.message.includes(QUEUE_ERROR_MESSAGES.API_KEY_ERROR)) || // api key not provided
|
||||
(e instanceof BaseError &&
|
||||
e.message.includes(QUEUE_ERROR_MESSAGES.NO_DEFAULT_MODEL_ERROR)) || // api key not provided
|
||||
(e instanceof ApiError && e.httpCode >= 400 && e.httpCode < 500) || // do not error and retry on 4xx errors. They are visible to the user in the UI but do not alert us.
|
||||
(e instanceof ApiError && e.message.includes("TypeError")) || // Zod parsing the response failed. User should update prompt to consistently return expected output structure.
|
||||
(e instanceof ApiError &&
|
||||
e.message.includes("Error: Unterminated string in JSON at position")) || // When evaluator model is configured with too low max_tokens, the structured output response is invalid JSON
|
||||
(e instanceof ApiError && e.message.includes("is not valid JSON")) || // When evaluator model is not consistently returning valid JSON on structured output calls
|
||||
e.message.includes(QUEUE_ERROR_MESSAGES.TOO_LOW_MAX_TOKENS_ERROR)) || // When evaluator model is configured with too low max_tokens, the structured output response is invalid JSON
|
||||
(e instanceof ApiError &&
|
||||
e.message.includes(QUEUE_ERROR_MESSAGES.INVALID_JSON_ERROR)) || // When evaluator model is not consistently returning valid JSON on structured output calls
|
||||
(e instanceof BaseError &&
|
||||
e.message.includes(
|
||||
"Please ensure the mapped data exists and consider extending the job delay.",
|
||||
)) // Trace not found.
|
||||
e.message.includes(QUEUE_ERROR_MESSAGES.MAPPED_DATA_ERROR)) // Trace not found.
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -26,7 +26,7 @@ export class ClickhouseWriter {
|
||||
queue: ClickhouseQueue;
|
||||
|
||||
isIntervalFlushInProgress: boolean;
|
||||
intervalId: NodeJS.Timeout | null = null;
|
||||
intervalId: NodeJS.Timeout | null = null; // eslint-disable-line no-undef
|
||||
|
||||
private constructor() {
|
||||
this.batchSize = env.LANGFUSE_INGESTION_CLICKHOUSE_WRITE_BATCH_SIZE;
|
||||
@@ -239,11 +239,11 @@ export class ClickhouseWriter {
|
||||
}
|
||||
|
||||
export enum TableName {
|
||||
Traces = "traces",
|
||||
TracesMt = "traces_mt",
|
||||
Scores = "scores",
|
||||
Observations = "observations",
|
||||
BlobStorageFileLog = "blob_storage_file_log",
|
||||
Traces = "traces", // eslint-disable-line no-unused-vars
|
||||
TracesMt = "traces_mt", // eslint-disable-line no-unused-vars
|
||||
Scores = "scores", // eslint-disable-line no-unused-vars
|
||||
Observations = "observations", // eslint-disable-line no-unused-vars
|
||||
BlobStorageFileLog = "blob_storage_file_log", // eslint-disable-line no-unused-vars
|
||||
}
|
||||
|
||||
type RecordInsertType<T extends TableName> = T extends TableName.Scores
|
||||
|
||||
@@ -71,7 +71,11 @@ CREATE TABLE traces_all_amt
|
||||
`name` SimpleAggregateFunction(anyLast, String),
|
||||
|
||||
-- Metadata properties
|
||||
`metadata` AggregateFunction(argMax, Map(LowCardinality(String), String), DateTime64(3)),
|
||||
-- Metadata takes the last seen value in this form.
|
||||
-- If we spread it across multiple rows (keys + values) we might be able to allow updates/overwrites.
|
||||
-- Indexing speed in this case is unclear though.
|
||||
`metadata_argmax` AggregateFunction(argMax, Map(LowCardinality(String), String), DateTime64(3)),
|
||||
`metadata` SimpleAggregateFunction(anyLast, Map(String, String)),
|
||||
`user_id_argmax` AggregateFunction(argMax, String, DateTime64(3)),
|
||||
`session_id_argmax` AggregateFunction(argMax, String, DateTime64(3)),
|
||||
`environment_argmax` AggregateFunction(argMax, String, DateTime64(3)),
|
||||
@@ -121,14 +125,15 @@ SELECT
|
||||
anyLast(t0.name) as name,
|
||||
|
||||
-- Metadata properties
|
||||
argMaxState(t0.metadata, t0.event_ts) as metadata,
|
||||
argMaxState(t0.metadata, if(t0.metadata <> '{}', t0.event_ts, toDateTime64(0,3))) as metadata_argmax,
|
||||
anyLastMap(t0.metadata) as metadata,
|
||||
argMaxState(t0.user_id, if(t0.user_id <> '', t0.event_ts, toDateTime64(0, 3))) as user_id_argmax,
|
||||
argMaxState(t0.session_id, if(t0.session_id <> '', t0.event_ts, toDateTime64(0, 3))) as session_id_argmax,
|
||||
argMaxState(t0.environment, if(t0.environment <> '', t0.event_ts, toDateTime64(0, 3))) as environment_argmax,
|
||||
anyLast(t0.user_id) as user_id,
|
||||
anyLast(t0.session_id) as session_id,
|
||||
anyLast(t0.environment) as environment,
|
||||
groupUniqArrayArray(t0.tags) as tags,
|
||||
groupUniqArrayArray(t0.tags) as tags,
|
||||
argMaxState(t0.version, if(t0.version <> '', t0.event_ts, toDateTime64(0, 3))) as version,
|
||||
argMaxState(t0.release, if(t0.release <> '', t0.event_ts, toDateTime64(0, 3))) as release,
|
||||
|
||||
@@ -165,7 +170,8 @@ CREATE TABLE traces_7d_amt
|
||||
`name` SimpleAggregateFunction(anyLast, String),
|
||||
|
||||
-- Metadata properties
|
||||
`metadata` AggregateFunction(argMax, Map(LowCardinality(String), String), DateTime64(3)),
|
||||
`metadata_argmax` AggregateFunction(argMax, Map(LowCardinality(String), String), DateTime64(3)),
|
||||
`metadata` SimpleAggregateFunction(anyLast, Map(String, String)),
|
||||
`user_id_argmax` AggregateFunction(argMax, String, DateTime64(3)),
|
||||
`session_id_argmax` AggregateFunction(argMax, String, DateTime64(3)),
|
||||
`environment_argmax` AggregateFunction(argMax, String, DateTime64(3)),
|
||||
@@ -215,14 +221,15 @@ SELECT
|
||||
anyLast(t0.name) as name,
|
||||
|
||||
-- Metadata properties
|
||||
argMaxState(t0.metadata, t0.event_ts) as metadata,
|
||||
argMaxState(t0.metadata, if(t0.metadata <> '{}', t0.event_ts, toDateTime64(0,3))) as metadata_argmax,
|
||||
anyLastMap(t0.metadata) as metadata,
|
||||
argMaxState(t0.user_id, if(t0.user_id <> '', t0.event_ts, toDateTime64(0, 3))) as user_id_argmax,
|
||||
argMaxState(t0.session_id, if(t0.session_id <> '', t0.event_ts, toDateTime64(0, 3))) as session_id_argmax,
|
||||
argMaxState(t0.environment, if(t0.environment <> '', t0.event_ts, toDateTime64(0, 3))) as environment_argmax,
|
||||
anyLast(t0.user_id) as user_id,
|
||||
anyLast(t0.session_id) as session_id,
|
||||
anyLast(t0.environment) as environment,
|
||||
groupUniqArrayArray(t0.tags) as tags,
|
||||
groupUniqArrayArray(t0.tags) as tags,
|
||||
argMaxState(t0.version, if(t0.version <> '', t0.event_ts, toDateTime64(0, 3))) as version,
|
||||
argMaxState(t0.release, if(t0.release <> '', t0.event_ts, toDateTime64(0, 3))) as release,
|
||||
|
||||
@@ -259,7 +266,8 @@ CREATE TABLE traces_30d_amt
|
||||
`name` SimpleAggregateFunction(anyLast, String),
|
||||
|
||||
-- Metadata properties
|
||||
`metadata` AggregateFunction(argMax, Map(LowCardinality(String), String), DateTime64(3)),
|
||||
`metadata_argmax` AggregateFunction(argMax, Map(LowCardinality(String), String), DateTime64(3)),
|
||||
`metadata` SimpleAggregateFunction(anyLast, Map(String, String)),
|
||||
`user_id_argmax` AggregateFunction(argMax, String, DateTime64(3)),
|
||||
`session_id_argmax` AggregateFunction(argMax, String, DateTime64(3)),
|
||||
`environment_argmax` AggregateFunction(argMax, String, DateTime64(3)),
|
||||
@@ -309,14 +317,15 @@ SELECT
|
||||
anyLast(t0.name) as name,
|
||||
|
||||
-- Metadata properties
|
||||
argMaxState(t0.metadata, t0.event_ts) as metadata,
|
||||
argMaxState(t0.metadata, if(t0.metadata <> '{}', t0.event_ts, toDateTime64(0,3))) as metadata_argmax,
|
||||
anyLastMap(t0.metadata) as metadata,
|
||||
argMaxState(t0.user_id, if(t0.user_id <> '', t0.event_ts, toDateTime64(0, 3))) as user_id_argmax,
|
||||
argMaxState(t0.session_id, if(t0.session_id <> '', t0.event_ts, toDateTime64(0, 3))) as session_id_argmax,
|
||||
argMaxState(t0.environment, if(t0.environment <> '', t0.event_ts, toDateTime64(0, 3))) as environment_argmax,
|
||||
anyLast(t0.user_id) as user_id,
|
||||
anyLast(t0.session_id) as session_id,
|
||||
anyLast(t0.environment) as environment,
|
||||
groupUniqArrayArray(t0.tags) as tags,
|
||||
groupUniqArrayArray(t0.tags) as tags,
|
||||
argMaxState(t0.version, if(t0.version <> '', t0.event_ts, toDateTime64(0, 3))) as version,
|
||||
argMaxState(t0.release, if(t0.release <> '', t0.event_ts, toDateTime64(0, 3))) as release,
|
||||
|
||||
@@ -342,6 +351,8 @@ FROM traces_mt t0
|
||||
GROUP BY project_id, id;
|
||||
```
|
||||
|
||||
## Query AggregatingMergeTrees
|
||||
|
||||
We can query the properties of the resulting AggregatingMergeTree via (make sure to pick the right timeframe:
|
||||
```sql
|
||||
SELECT
|
||||
@@ -386,3 +397,129 @@ SELECT
|
||||
FROM traces_all_amt
|
||||
LIMIT 100;
|
||||
```
|
||||
|
||||
## Find discrepancies
|
||||
|
||||
We can identify discrepancies in the original and the new data using
|
||||
```sql
|
||||
-- Query to compare traces_all_amt with traces table and identify discrepancies
|
||||
WITH amt_data AS (
|
||||
-- First get the finalized values from the AMT table
|
||||
SELECT project_id,
|
||||
id,
|
||||
start_time,
|
||||
end_time,
|
||||
finalizeAggregation(name_argmax) AS name_argmax_value,
|
||||
name AS name_anylast_value,
|
||||
|
||||
finalizeAggregation(metadata_argmax) AS metadata_argmax_value,
|
||||
metadata AS metadata_anylast_value,
|
||||
finalizeAggregation(user_id_argmax) AS user_id_argmax_value,
|
||||
user_id AS user_id_anylast_value,
|
||||
finalizeAggregation(session_id_argmax) AS session_id_argmax_value,
|
||||
session_id AS session_id_anylast_value,
|
||||
finalizeAggregation(environment_argmax) AS environment_argmax_value,
|
||||
environment AS environment_anylast_value,
|
||||
tags,
|
||||
finalizeAggregation(version) AS version_value,
|
||||
finalizeAggregation(release) AS release_value,
|
||||
|
||||
finalizeAggregation(bookmarked) AS bookmarked_value,
|
||||
finalizeAggregation(public) AS public_value,
|
||||
|
||||
finalizeAggregation(input_argmax) AS input_argmax_value,
|
||||
input AS input_anylast_value,
|
||||
finalizeAggregation(output_argmax) AS output_argmax_value,
|
||||
output AS output_anylast_value,
|
||||
|
||||
created_at,
|
||||
updated_at
|
||||
FROM traces_all_amt
|
||||
)
|
||||
|
||||
-- Main query to compare AMT with original traces table
|
||||
SELECT t.project_id,
|
||||
t.id,
|
||||
-- Identify differences between tables
|
||||
t.timestamp != a.start_time AS timestamp_diff,
|
||||
t.name != a.name_anylast_value AS name_diff,
|
||||
t.user_id != a.user_id_anylast_value AS user_id_diff,
|
||||
t.session_id != a.session_id_anylast_value AS session_id_diff,
|
||||
t.release != a.release_value AS release_diff,
|
||||
t.version != a.version_value AS version_diff,
|
||||
t.public != a.public_value AS public_diff,
|
||||
t.bookmarked != a.bookmarked_value AS bookmarked_diff,
|
||||
arraySort(t.tags) != arraySort(a.tags) AS tags_diff,
|
||||
t.input != a.input_anylast_value AS input_diff,
|
||||
t.output != a.output_anylast_value AS output_diff,
|
||||
t.metadata != a.metadata_anylast_value AS metadata_diff,
|
||||
|
||||
a.name_argmax_value != a.name_anylast_value AS amt_name_diff,
|
||||
a.user_id_argmax_value != a.user_id_anylast_value AS amt_user_id_diff,
|
||||
a.session_id_argmax_value != a.session_id_anylast_value AS amt_session_id_diff,
|
||||
a.environment_argmax_value != a.environment_anylast_value AS amt_environment_diff,
|
||||
a.input_argmax_value != a.input_anylast_value AS amt_input_diff,
|
||||
a.output_argmax_value != a.output_anylast_value AS amt_output_diff,
|
||||
a.metadata_argmax_value != a.metadata_anylast_value AS amt_metadata_diff,
|
||||
|
||||
-- Include original values for comparison
|
||||
t.timestamp AS traces_timestamp,
|
||||
a.start_time AS amt_start_time,
|
||||
t.name AS traces_name,
|
||||
a.name_anylast_value AS amt_name_anylast,
|
||||
a.name_argmax_value AS amt_name_argmax,
|
||||
t.user_id AS traces_user_id,
|
||||
a.user_id_anylast_value AS amt_user_id_anylast,
|
||||
a.user_id_argmax_value AS amt_user_id_argmax,
|
||||
t.session_id AS traces_session_id,
|
||||
a.session_id_anylast_value AS amt_session_id_anylast,
|
||||
a.session_id_argmax_value AS amt_session_id_argmax,
|
||||
t.environment AS traces_environment,
|
||||
a.environment_anylast_value AS amt_environment_anylast,
|
||||
a.environment_argmax_value AS amt_environment_argmax,
|
||||
|
||||
t.metadata AS traces_metadata,
|
||||
a.metadata_anylast_value AS amt_metadata_anylast,
|
||||
a.metadata_argmax_value AS amt_metadata_argmax,
|
||||
arraySort(t.tags) AS traces_tags,
|
||||
arraySort(a.tags) AS amt_tags,
|
||||
t.bookmarked AS traces_bookmarked,
|
||||
a.bookmarked_value AS amt_bookmarked,
|
||||
t.public AS traces_public,
|
||||
a.public_value AS amt_public,
|
||||
t.release AS traces_release,
|
||||
a.release_value AS amt_release,
|
||||
t.version AS traces_version,
|
||||
a.version_value AS amt_version,
|
||||
|
||||
t.input AS traces_input,
|
||||
a.input_anylast_value AS amt_input_anylast,
|
||||
a.input_argmax_value AS amt_input_argmax,
|
||||
t.output AS traces_output,
|
||||
a.output_anylast_value AS amt_output_anylast,
|
||||
a.output_argmax_value AS amt_output_argmax,
|
||||
|
||||
timestamp_diff +
|
||||
name_diff +
|
||||
user_id_diff +
|
||||
session_id_diff +
|
||||
release_diff +
|
||||
version_diff +
|
||||
public_diff +
|
||||
bookmarked_diff +
|
||||
tags_diff +
|
||||
input_diff +
|
||||
output_diff +
|
||||
metadata_diff
|
||||
AS total_diff
|
||||
|
||||
FROM traces t FINAL
|
||||
LEFT JOIN amt_data a ON t.project_id = a.project_id AND t.id = a.id
|
||||
where (t.timestamp >= '2025-07-01'
|
||||
or a.start_time >= '2025-07-01')
|
||||
and t.project_id in (
|
||||
'some-project-id'
|
||||
)
|
||||
ORDER BY total_diff desc
|
||||
LIMIT 1000;
|
||||
```
|
||||
@@ -93,8 +93,8 @@ export class IngestionService {
|
||||
constructor(
|
||||
private redis: Redis | Cluster,
|
||||
private prisma: PrismaClient,
|
||||
private clickHouseWriter: ClickhouseWriter,
|
||||
private clickhouseClient: ClickhouseClientType,
|
||||
private clickHouseWriter: ClickhouseWriter, // eslint-disable-line no-unused-vars
|
||||
private clickhouseClient: ClickhouseClientType, // eslint-disable-line no-unused-vars
|
||||
) {
|
||||
this.promptService = new PromptService(prisma, redis);
|
||||
}
|
||||
@@ -220,6 +220,14 @@ export class IngestionService {
|
||||
clickhouseScoreRecord?.created_at ?? createdAtTimestamp.getTime();
|
||||
|
||||
this.clickHouseWriter.addToQueue(TableName.Scores, finalScoreRecord);
|
||||
|
||||
if (
|
||||
env.LANGFUSE_EXPERIMENT_INSERT_INTO_AGGREGATING_MERGE_TREES === "true" &&
|
||||
finalScoreRecord.trace_id
|
||||
) {
|
||||
const traceMtRecord = this.convertScoreToTraceMt(finalScoreRecord);
|
||||
this.clickHouseWriter.addToQueue(TableName.TracesMt, traceMtRecord);
|
||||
}
|
||||
}
|
||||
|
||||
private async processTraceEventList(params: {
|
||||
@@ -453,6 +461,14 @@ export class IngestionService {
|
||||
TableName.Observations,
|
||||
finalObservationRecord,
|
||||
);
|
||||
|
||||
if (
|
||||
env.LANGFUSE_EXPERIMENT_INSERT_INTO_AGGREGATING_MERGE_TREES === "true" &&
|
||||
finalObservationRecord.trace_id
|
||||
) {
|
||||
const traceMtRecord = this.convertObservationToTraceMt(finalObservationRecord);
|
||||
this.clickHouseWriter.addToQueue(TableName.TracesMt, traceMtRecord);
|
||||
}
|
||||
}
|
||||
|
||||
private async mergeScoreRecords(params: {
|
||||
@@ -543,6 +559,90 @@ export class IngestionService {
|
||||
};
|
||||
}
|
||||
|
||||
private convertObservationToTraceMt(
|
||||
observationRecord: ObservationRecordInsertType,
|
||||
): TraceMtRecordInsertType {
|
||||
return {
|
||||
// Identifiers
|
||||
project_id: observationRecord.project_id,
|
||||
// Use trace_id as the id in traces_mt. Always set given the conditions around calling the function
|
||||
id: observationRecord.trace_id || "",
|
||||
start_time: observationRecord.start_time,
|
||||
end_time: observationRecord.end_time || null,
|
||||
name: "",
|
||||
|
||||
// Metadata properties
|
||||
metadata: {},
|
||||
user_id: '',
|
||||
session_id: '',
|
||||
environment: observationRecord.environment,
|
||||
tags: [],
|
||||
version: null,
|
||||
release: null,
|
||||
|
||||
// UI properties - nullable to prevent absent values being interpreted as overwrites
|
||||
bookmarked: null,
|
||||
public: null,
|
||||
|
||||
// Aggregations - include this observation ID
|
||||
observation_ids: [observationRecord.id],
|
||||
score_ids: [],
|
||||
// We can fill the cost details here, but we shouldn't trust them.
|
||||
// Only used for verification to estimate how big the double-counting is.
|
||||
cost_details: observationRecord.cost_details || {},
|
||||
usage_details: observationRecord.usage_details || {},
|
||||
|
||||
// Input/Output
|
||||
input: "",
|
||||
output: "",
|
||||
|
||||
created_at: observationRecord.created_at,
|
||||
updated_at: observationRecord.updated_at,
|
||||
event_ts: observationRecord.event_ts,
|
||||
};
|
||||
}
|
||||
|
||||
private convertScoreToTraceMt(
|
||||
scoreRecord: ScoreRecordInsertType,
|
||||
): TraceMtRecordInsertType {
|
||||
return {
|
||||
// Identifiers
|
||||
project_id: scoreRecord.project_id,
|
||||
// Use trace_id as the id in traces_mt. Always set given the conditions around calling the function
|
||||
id: scoreRecord.trace_id || "",
|
||||
start_time: scoreRecord.timestamp,
|
||||
end_time: null, // scores don't have end_time
|
||||
name: "",
|
||||
|
||||
// Metadata properties
|
||||
metadata: {},
|
||||
user_id: '',
|
||||
session_id: '',
|
||||
environment: scoreRecord.environment,
|
||||
tags: [], // scores don't have tags
|
||||
version: null, // scores don't have version
|
||||
release: null, // scores don't have release
|
||||
|
||||
// UI properties - nullable to prevent absent values being interpreted as overwrites
|
||||
bookmarked: null,
|
||||
public: null,
|
||||
|
||||
// Aggregations - include this score ID
|
||||
observation_ids: [],
|
||||
score_ids: [scoreRecord.id],
|
||||
cost_details: {},
|
||||
usage_details: {},
|
||||
|
||||
// Input/Output
|
||||
input: "", // scores don't have input
|
||||
output: "", // scores don't have output
|
||||
|
||||
created_at: scoreRecord.created_at,
|
||||
updated_at: scoreRecord.updated_at,
|
||||
event_ts: scoreRecord.event_ts,
|
||||
};
|
||||
}
|
||||
|
||||
private async mergeObservationRecords(params: {
|
||||
projectId: string;
|
||||
observationRecords: ObservationRecordInsertType[];
|
||||
@@ -713,7 +813,7 @@ export class IngestionService {
|
||||
> {
|
||||
const providedUsageDetails = Object.fromEntries(
|
||||
Object.entries(observationRecord.provided_usage_details).filter(
|
||||
([k, v]) => v != null && v >= 0,
|
||||
([k, v]) => v != null && v >= 0, // eslint-disable-line no-unused-vars
|
||||
),
|
||||
);
|
||||
|
||||
@@ -771,7 +871,7 @@ export class IngestionService {
|
||||
const { provided_cost_details } = observationRecord;
|
||||
|
||||
const providedCostKeys = Object.entries(provided_cost_details ?? {})
|
||||
.filter(([_, value]) => value != null)
|
||||
.filter(([_, value]) => value != null) // eslint-disable-line no-unused-vars
|
||||
.map(([key]) => key);
|
||||
|
||||
// If user has provided any cost point, do not calculate any other cost points
|
||||
@@ -818,7 +918,7 @@ export class IngestionService {
|
||||
finalTotalCost = finalCostDetails.total;
|
||||
} else if (finalCostEntries.length > 0) {
|
||||
finalTotalCost = finalCostEntries.reduce(
|
||||
(acc, [_, cost]) => acc + cost,
|
||||
(acc, [_, cost]) => acc + cost, // eslint-disable-line no-unused-vars
|
||||
0,
|
||||
);
|
||||
|
||||
@@ -882,7 +982,7 @@ export class IngestionService {
|
||||
return result;
|
||||
}
|
||||
|
||||
private async getClickhouseRecord(params: {
|
||||
private async getClickhouseRecord(params: { // eslint-disable-line no-unused-vars
|
||||
projectId: string;
|
||||
entityId: string;
|
||||
table: TableName.Traces;
|
||||
@@ -891,7 +991,7 @@ export class IngestionService {
|
||||
params: Record<string, unknown>;
|
||||
};
|
||||
}): Promise<TraceRecordInsertType | null>;
|
||||
private async getClickhouseRecord(params: {
|
||||
private async getClickhouseRecord(params: { // eslint-disable-line no-unused-vars, no-dupe-class-members
|
||||
projectId: string;
|
||||
entityId: string;
|
||||
table: TableName.Scores;
|
||||
@@ -900,7 +1000,7 @@ export class IngestionService {
|
||||
params: Record<string, unknown>;
|
||||
};
|
||||
}): Promise<ScoreRecordInsertType | null>;
|
||||
private async getClickhouseRecord(params: {
|
||||
private async getClickhouseRecord(params: { // eslint-disable-line no-unused-vars, no-dupe-class-members
|
||||
projectId: string;
|
||||
entityId: string;
|
||||
table: TableName.Observations;
|
||||
@@ -909,7 +1009,7 @@ export class IngestionService {
|
||||
params: Record<string, unknown>;
|
||||
};
|
||||
}): Promise<ObservationRecordInsertType | null>;
|
||||
private async getClickhouseRecord(params: {
|
||||
private async getClickhouseRecord(params: { // eslint-disable-line no-dupe-class-members
|
||||
projectId: string;
|
||||
entityId: string;
|
||||
table: TableName;
|
||||
@@ -1107,7 +1207,7 @@ export class IngestionService {
|
||||
...("usageDetails" in obs.body
|
||||
? (Object.fromEntries(
|
||||
Object.entries(obs.body.usageDetails ?? {}).filter(
|
||||
([_, val]) => val != null,
|
||||
([_, val]) => val != null, // eslint-disable-line no-unused-vars
|
||||
),
|
||||
) as Record<string, number>)
|
||||
: {}),
|
||||
@@ -1128,7 +1228,7 @@ export class IngestionService {
|
||||
...("costDetails" in obs.body
|
||||
? (Object.fromEntries(
|
||||
Object.entries(obs.body.costDetails ?? {}).filter(
|
||||
([_, val]) => val != null,
|
||||
([_, val]) => val != null, // eslint-disable-line no-unused-vars
|
||||
),
|
||||
) as Record<string, number>)
|
||||
: {}),
|
||||
|
||||
@@ -9,7 +9,7 @@ import { WorkerManager } from "../queues/workerManager";
|
||||
import { prisma } from "@langfuse/shared/src/db";
|
||||
import { BackgroundMigrationManager } from "../backgroundMigrations/backgroundMigrationManager";
|
||||
|
||||
export const onShutdown: NodeJS.SignalsListener = async (signal) => {
|
||||
export const onShutdown: NodeJS.SignalsListener = async (signal) => { // eslint-disable-line no-undef
|
||||
logger.info(`Received ${signal}, closing server...`);
|
||||
setSigtermReceived();
|
||||
|
||||
|
||||
Reference in New Issue
Block a user