Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d905623cb3 | ||
|
|
a87a3505b8 | ||
|
|
591de20a72 | ||
|
|
33d3ab80cb | ||
|
|
6b9773caec | ||
|
|
c90b477d40 | ||
|
|
0f448ccdde | ||
|
|
74f93aad79 | ||
|
|
9650e87f64 | ||
|
|
c5fc30a9d0 | ||
|
|
4a26f47971 | ||
|
|
e3a9e61d07 | ||
|
|
a12c2a3a97 | ||
|
|
80681e08af | ||
|
|
4e97494390 | ||
|
|
346e5fe7b6 | ||
|
|
16482f588a | ||
|
|
f047e21e13 | ||
|
|
534e832c13 | ||
|
|
5dbfec24f0 | ||
|
|
700705be84 | ||
|
|
611c31ce54 | ||
|
|
bdb5ddadd9 | ||
|
|
0fd1f065bd | ||
|
|
8aa063303a | ||
|
|
d0ff8a87fe | ||
|
|
0e61ffd621 | ||
|
|
9f20926bc4 | ||
|
|
81a232e4a4 | ||
|
|
c7e7d62f47 | ||
|
|
2aa8971dff | ||
|
|
85f4cb0c13 | ||
|
|
4bbba9d983 | ||
|
|
0b7dd5a1a7 | ||
|
|
b5cad2693f |
@@ -38,6 +38,9 @@ types:
|
||||
public:
|
||||
type: optional<boolean>
|
||||
docs: Public traces are accessible via url without login
|
||||
environment:
|
||||
type: optional<string>
|
||||
docs: The environment from which this trace originated. Can be any lowercase alphanumeric string with hyphens and underscores that does not start with 'langfuse'.
|
||||
TraceWithDetails: # GET /traces
|
||||
extends: Trace
|
||||
properties:
|
||||
@@ -145,6 +148,9 @@ types:
|
||||
costDetails:
|
||||
type: optional<map<string, double>>
|
||||
docs: The cost details of the observation. Key is the name of the cost metric, value is the cost in USD. The total key is the sum of all (non-total) cost metrics or the total value ingested.
|
||||
environment:
|
||||
type: optional<string>
|
||||
docs: The environment from which this observation originated. Can be any lowercase alphanumeric string with hyphens and underscores that does not start with 'langfuse'.
|
||||
|
||||
ObservationsView:
|
||||
extends: Observation
|
||||
@@ -249,6 +255,9 @@ types:
|
||||
queueId:
|
||||
type: optional<string>
|
||||
docs: Reference an annotation queue on a score. Populated if the score was initially created in an annotation queue.
|
||||
environment:
|
||||
type: optional<string>
|
||||
docs: The environment from which this score originated. Can be any lowercase alphanumeric string with hyphens and underscores that does not start with 'langfuse'.
|
||||
NumericScore:
|
||||
extends: BaseScore
|
||||
properties:
|
||||
|
||||
@@ -168,6 +168,7 @@ types:
|
||||
statusMessage: optional<string>
|
||||
parentObservationId: optional<string>
|
||||
version: optional<string>
|
||||
environment: optional<string>
|
||||
|
||||
CreateEventBody:
|
||||
extends: OptionalObservationBody
|
||||
@@ -232,6 +233,7 @@ types:
|
||||
level: optional<commons.ObservationLevel>
|
||||
statusMessage: optional<string>
|
||||
parentObservationId: optional<string>
|
||||
environment: optional<string>
|
||||
|
||||
TraceBody:
|
||||
properties:
|
||||
@@ -246,6 +248,7 @@ types:
|
||||
version: optional<string>
|
||||
metadata: optional<unknown>
|
||||
tags: optional<list<string>>
|
||||
environment: optional<string>
|
||||
public:
|
||||
type: optional<boolean>
|
||||
docs: Make trace publicly accessible via url
|
||||
@@ -259,6 +262,7 @@ types:
|
||||
id: optional<string>
|
||||
traceId: string
|
||||
name: string
|
||||
environment: optional<string>
|
||||
value:
|
||||
type: commons.CreateScoreValue
|
||||
docs: The value of the score. Must be passed as string for categorical scores, and numeric for boolean and numeric scores. Boolean score values must equal either 1 or 0 (true or false)
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "langfuse",
|
||||
"version": "3.30.0",
|
||||
"version": "3.34.1",
|
||||
"author": "engineering@langfuse.com",
|
||||
"license": "MIT",
|
||||
"private": true,
|
||||
|
||||
@@ -28,6 +28,7 @@ const BooleanData = z.object({
|
||||
const CreateAnnotationScoreBase = z.object({
|
||||
name: z.string(),
|
||||
projectId: z.string(),
|
||||
environment: z.string().default("default"),
|
||||
traceId: z.string(),
|
||||
configId: z.string().optional(),
|
||||
observationId: z.string().optional(),
|
||||
|
||||
@@ -45,6 +45,7 @@ const ScoreBase = z.object({
|
||||
id: z.string(),
|
||||
timestamp: z.coerce.date(),
|
||||
projectId: z.string(),
|
||||
environment: z.string().default("default"),
|
||||
name: z.string(),
|
||||
source: z.enum(ScoreSource),
|
||||
authorUserId: z.string().nullish(),
|
||||
|
||||
@@ -118,6 +118,15 @@ export const UsageOrCostDetails = z
|
||||
.union([OpenAIUsageSchema, RawUsageOrCostDetails])
|
||||
.nullish();
|
||||
|
||||
export const EnvironmentName = z
|
||||
.string()
|
||||
.max(40, "Maximum length is 40 characters")
|
||||
.regex(
|
||||
/^(?!langfuse)[a-z0-9-_]+$/,
|
||||
"Only alphanumeric lower case characters, hyphens, and underscores are allowed, and it must not start with 'langfuse'",
|
||||
)
|
||||
.default("default");
|
||||
|
||||
// Using z.any instead of jsonSchema for input/output as we saw huge CPU overhead for large numeric arrays.
|
||||
// With this setup parsing should be more lightweight and doesn't block other requests.
|
||||
// As we allow plain values, arrays, and objects the JSON parse via bodyParser should suffice.
|
||||
@@ -130,6 +139,7 @@ export const TraceBody = z.object({
|
||||
output: z.any().nullish(),
|
||||
sessionId: z.string().nullish(),
|
||||
userId: z.string().nullish(),
|
||||
environment: EnvironmentName,
|
||||
metadata: jsonSchema.nullish(),
|
||||
release: z.string().nullish(),
|
||||
version: z.string().nullish(),
|
||||
@@ -139,6 +149,7 @@ export const TraceBody = z.object({
|
||||
|
||||
export const OptionalObservationBody = z.object({
|
||||
traceId: z.string().nullish(),
|
||||
environment: EnvironmentName,
|
||||
name: z.string().nullish(),
|
||||
startTime: stringDateTime,
|
||||
metadata: jsonSchema.nullish(),
|
||||
@@ -230,6 +241,7 @@ const BaseScoreBody = z.object({
|
||||
id: z.string().nullish(),
|
||||
name: NonEmptyString,
|
||||
traceId: z.string(),
|
||||
environment: EnvironmentName,
|
||||
observationId: z.string().nullish(),
|
||||
comment: z.string().nullish(),
|
||||
source: z
|
||||
@@ -383,18 +395,6 @@ export const SdkLogEvent = z.object({
|
||||
id: z.string().nullish(), // Not used, but makes downstream processing easier.
|
||||
});
|
||||
|
||||
// definitions for the ingestion API
|
||||
|
||||
export const observationTypes = [
|
||||
"observation-create",
|
||||
"observation-update",
|
||||
"generation-create",
|
||||
"generation-update",
|
||||
"span-create",
|
||||
"span-update",
|
||||
"event-create",
|
||||
];
|
||||
|
||||
export const eventTypes = {
|
||||
TRACE_CREATE: "trace-create",
|
||||
SCORE_CREATE: "score-create",
|
||||
@@ -474,27 +474,12 @@ export const ingestionEvent = z.discriminatedUnion("type", [
|
||||
export type IngestionEventType = z.infer<typeof ingestionEvent>;
|
||||
|
||||
export const ingestionBatchEvent = z.array(ingestionEvent);
|
||||
export type IngestionBatchEventType = z.infer<typeof ingestionBatchEvent>;
|
||||
|
||||
export const ingestionEventWithProjectId = ingestionEvent.and(
|
||||
z.object({ projectId: z.string() }),
|
||||
);
|
||||
export type IngestionEventWithProjectIdType = z.infer<
|
||||
typeof ingestionEventWithProjectId
|
||||
>;
|
||||
|
||||
export const ingestionApiSchema = z.object({
|
||||
batch: ingestionBatchEvent,
|
||||
metadata: jsonSchema.nullish(),
|
||||
});
|
||||
|
||||
export const ingestionApiSchemaWithProjectId = ingestionApiSchema.extend({
|
||||
projectId: z.string(),
|
||||
});
|
||||
export type IngestionApiSchemaWithProjectId = z.infer<
|
||||
typeof ingestionApiSchemaWithProjectId
|
||||
>;
|
||||
|
||||
export type ObservationEvent =
|
||||
| z.infer<typeof legacyObservationCreateEvent>
|
||||
| z.infer<typeof legacyObservationUpdateEvent>
|
||||
|
||||
@@ -101,6 +101,8 @@ export const openAIModels = [
|
||||
"o1-preview-2024-09-12",
|
||||
"o1-mini",
|
||||
"o1-mini-2024-09-12",
|
||||
"gpt-4.5-preview",
|
||||
"gpt-4.5-preview-2025-02-27",
|
||||
"gpt-4-turbo-preview",
|
||||
"gpt-4-1106-preview",
|
||||
"gpt-4-0613",
|
||||
|
||||
@@ -36,6 +36,7 @@ export const observationRecordBaseSchema = z.object({
|
||||
project_id: z.string(),
|
||||
type: z.string(),
|
||||
parent_observation_id: z.string().nullish(),
|
||||
environment: z.string().default("default"),
|
||||
name: z.string().nullish(),
|
||||
metadata: z.record(z.string()),
|
||||
level: z.string().nullish(),
|
||||
@@ -52,9 +53,6 @@ export const observationRecordBaseSchema = z.object({
|
||||
prompt_version: z.number().nullish(),
|
||||
is_deleted: z.number(),
|
||||
});
|
||||
export type ObservationRecordBaseType = z.infer<
|
||||
typeof observationRecordBaseSchema
|
||||
>;
|
||||
|
||||
export const observationRecordReadSchema = observationRecordBaseSchema.extend({
|
||||
created_at: clickhouseStringDateSchema,
|
||||
@@ -98,6 +96,7 @@ export const traceRecordBaseSchema = z.object({
|
||||
release: z.string().nullish(),
|
||||
version: z.string().nullish(),
|
||||
project_id: z.string(),
|
||||
environment: z.string().default("default"),
|
||||
public: z.boolean(),
|
||||
bookmarked: z.boolean(),
|
||||
tags: z.array(z.string()),
|
||||
@@ -106,7 +105,6 @@ export const traceRecordBaseSchema = z.object({
|
||||
session_id: z.string().nullish(),
|
||||
is_deleted: z.number(),
|
||||
});
|
||||
export type TraceRecordBaseType = z.infer<typeof traceRecordBaseSchema>;
|
||||
|
||||
export const traceRecordReadSchema = traceRecordBaseSchema.extend({
|
||||
timestamp: clickhouseStringDateSchema,
|
||||
@@ -129,6 +127,7 @@ export const scoreRecordBaseSchema = z.object({
|
||||
project_id: z.string(),
|
||||
trace_id: z.string(),
|
||||
observation_id: z.string().nullish(),
|
||||
environment: z.string().default("default"),
|
||||
name: z.string(),
|
||||
value: z.number().nullish(),
|
||||
source: z.string(),
|
||||
@@ -140,7 +139,6 @@ export const scoreRecordBaseSchema = z.object({
|
||||
queue_id: z.string().nullish(),
|
||||
is_deleted: z.number(),
|
||||
});
|
||||
export type ScoreRecordBaseType = z.infer<typeof scoreRecordBaseSchema>;
|
||||
|
||||
export const scoreRecordReadSchema = scoreRecordBaseSchema.extend({
|
||||
created_at: clickhouseStringDateSchema,
|
||||
@@ -246,6 +244,7 @@ export const convertPostgresTraceToInsert = (
|
||||
: Array.isArray(trace.metadata)
|
||||
? { metadata: trace.metadata }
|
||||
: trace.metadata,
|
||||
environment: trace.environment,
|
||||
release: trace.release,
|
||||
version: trace.version,
|
||||
project_id: trace.project_id,
|
||||
@@ -282,6 +281,7 @@ export const convertPostgresObservationToInsert = (
|
||||
project_id: observation.project_id,
|
||||
type: observation.type,
|
||||
parent_observation_id: observation.parent_observation_id,
|
||||
environment: observation.environment,
|
||||
start_time: observation.start_time?.getTime(),
|
||||
end_time: observation.end_time?.getTime(),
|
||||
name: observation.name,
|
||||
@@ -345,6 +345,7 @@ export const convertPostgresScoreToInsert = (
|
||||
project_id: score.project_id,
|
||||
trace_id: score.trace_id,
|
||||
observation_id: score.observation_id,
|
||||
environment: score.environment,
|
||||
name: score.name,
|
||||
value: score.value,
|
||||
source: score.source,
|
||||
|
||||
@@ -114,6 +114,7 @@ export const getObservationsViewForTrace = async (
|
||||
project_id,
|
||||
type,
|
||||
parent_observation_id,
|
||||
environment,
|
||||
start_time,
|
||||
end_time,
|
||||
name,
|
||||
@@ -177,6 +178,7 @@ export const getObservationForTraceIdByName = async (
|
||||
project_id,
|
||||
type,
|
||||
parent_observation_id,
|
||||
environment,
|
||||
start_time,
|
||||
end_time,
|
||||
name,
|
||||
@@ -342,6 +344,7 @@ const getObservationByIdInternal = async (
|
||||
id,
|
||||
trace_id,
|
||||
project_id,
|
||||
environment,
|
||||
type,
|
||||
parent_observation_id,
|
||||
start_time,
|
||||
@@ -1136,6 +1139,7 @@ export const getObservationMetricsForPrompts = async (
|
||||
export const getLatencyAndTotalCostForObservations = async (
|
||||
projectId: string,
|
||||
observationIds: string[],
|
||||
timestamp?: Date,
|
||||
) => {
|
||||
const query = `
|
||||
SELECT
|
||||
@@ -1144,7 +1148,8 @@ export const getLatencyAndTotalCostForObservations = async (
|
||||
dateDiff('millisecond', start_time, end_time) AS latency_ms
|
||||
FROM observations FINAL
|
||||
WHERE project_id = {projectId: String}
|
||||
AND id IN ({observationIds: Array(String)})
|
||||
AND id IN ({observationIds: Array(String)})
|
||||
${timestamp ? `AND start_time >= {timestamp: DateTime64(3)}` : ""}
|
||||
`;
|
||||
const rows = await queryClickhouse<{
|
||||
id: string;
|
||||
@@ -1155,6 +1160,9 @@ export const getLatencyAndTotalCostForObservations = async (
|
||||
params: {
|
||||
projectId,
|
||||
observationIds,
|
||||
...(timestamp
|
||||
? { timestamp: convertDateToClickhouseDateTime(timestamp) }
|
||||
: {}),
|
||||
},
|
||||
tags: {
|
||||
feature: "tracing",
|
||||
@@ -1174,6 +1182,7 @@ export const getLatencyAndTotalCostForObservations = async (
|
||||
export const getLatencyAndTotalCostForObservationsByTraces = async (
|
||||
projectId: string,
|
||||
traceIds: string[],
|
||||
timestamp?: Date,
|
||||
) => {
|
||||
const query = `
|
||||
SELECT
|
||||
@@ -1183,6 +1192,7 @@ export const getLatencyAndTotalCostForObservationsByTraces = async (
|
||||
FROM observations FINAL
|
||||
WHERE project_id = {projectId: String}
|
||||
AND trace_id IN ({traceIds: Array(String)})
|
||||
${timestamp ? `AND start_time >= {timestamp: DateTime64(3)}` : ""}
|
||||
GROUP BY trace_id
|
||||
`;
|
||||
const rows = await queryClickhouse<{
|
||||
@@ -1194,6 +1204,9 @@ export const getLatencyAndTotalCostForObservationsByTraces = async (
|
||||
params: {
|
||||
projectId,
|
||||
traceIds,
|
||||
...(timestamp
|
||||
? { timestamp: convertDateToClickhouseDateTime(timestamp) }
|
||||
: {}),
|
||||
},
|
||||
tags: {
|
||||
feature: "tracing",
|
||||
|
||||
@@ -55,6 +55,7 @@ export const convertObservation = (
|
||||
traceId: record.trace_id ?? null,
|
||||
projectId: record.project_id,
|
||||
type: record.type as ObservationType,
|
||||
environment: record.environment,
|
||||
parentObservationId: record.parent_observation_id ?? null,
|
||||
startTime: parseClickhouseUTCDateTimeFormat(record.start_time),
|
||||
endTime: record.end_time
|
||||
|
||||
@@ -372,6 +372,7 @@ export const getScoresUiTable = async (props: {
|
||||
const rows = await getScoresUiGeneric<{
|
||||
id: string;
|
||||
project_id: string;
|
||||
environment: string;
|
||||
name: string;
|
||||
value: number;
|
||||
string_value: string | null;
|
||||
@@ -396,6 +397,7 @@ export const getScoresUiTable = async (props: {
|
||||
select: `
|
||||
s.id,
|
||||
s.project_id,
|
||||
s.environment,
|
||||
s.name,
|
||||
s.value,
|
||||
s.string_value,
|
||||
@@ -424,6 +426,7 @@ export const getScoresUiTable = async (props: {
|
||||
|
||||
return rows.map((row) => ({
|
||||
projectId: row.project_id,
|
||||
environment: row.environment,
|
||||
authorUserId: row.author_user_id,
|
||||
traceId: row.trace_id,
|
||||
observationId: row.observation_id,
|
||||
@@ -875,3 +878,27 @@ export const getScoresForPostHog = async function* (
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
export const hasAnyScore = async (projectId: string) => {
|
||||
const query = `
|
||||
SELECT 1
|
||||
FROM scores
|
||||
WHERE project_id = {projectId: String}
|
||||
LIMIT 1
|
||||
`;
|
||||
|
||||
const rows = await queryClickhouse<{ 1: number }>({
|
||||
query,
|
||||
params: {
|
||||
projectId,
|
||||
},
|
||||
tags: {
|
||||
feature: "tracing",
|
||||
type: "score",
|
||||
kind: "hasAny",
|
||||
projectId,
|
||||
},
|
||||
});
|
||||
|
||||
return rows.length > 0;
|
||||
};
|
||||
|
||||
@@ -17,6 +17,7 @@ export const convertToScore = (row: ScoreRecordReadType): Score => {
|
||||
id: row.id,
|
||||
timestamp: new Date(row.timestamp),
|
||||
projectId: row.project_id,
|
||||
environment: row.environment,
|
||||
traceId: row.trace_id,
|
||||
observationId: row.observation_id ?? null,
|
||||
name: row.name,
|
||||
|
||||
@@ -59,3 +59,14 @@ export const getPublicSessionsFilter = async (
|
||||
? [...filter.filter((f) => f.column !== "⭐️"), ...additionalBookmarkFilter]
|
||||
: [...additionalBookmarkFilter];
|
||||
};
|
||||
|
||||
export const hasAnySession = async (projectId: string) => {
|
||||
const count = await prisma.traceSession.count({
|
||||
where: {
|
||||
projectId,
|
||||
},
|
||||
take: 1,
|
||||
});
|
||||
|
||||
return count > 0;
|
||||
};
|
||||
|
||||
@@ -196,13 +196,13 @@ export const getTracesBySessionId = async (
|
||||
|
||||
export const hasAnyTrace = async (projectId: string) => {
|
||||
const query = `
|
||||
SELECT count(*) as count
|
||||
SELECT 1
|
||||
FROM traces
|
||||
WHERE project_id = {projectId: String}
|
||||
LIMIT 1
|
||||
`;
|
||||
|
||||
const rows = await queryClickhouse<{ count: string }>({
|
||||
const rows = await queryClickhouse<{ 1: number }>({
|
||||
query,
|
||||
params: {
|
||||
projectId,
|
||||
@@ -210,12 +210,12 @@ export const hasAnyTrace = async (projectId: string) => {
|
||||
tags: {
|
||||
feature: "tracing",
|
||||
type: "trace",
|
||||
kind: "exists",
|
||||
kind: "hasAny",
|
||||
projectId,
|
||||
},
|
||||
});
|
||||
|
||||
return rows.length > 0 && Number(rows[0].count) > 0;
|
||||
return rows.length > 0;
|
||||
};
|
||||
|
||||
export const getTraceCountsByProjectInCreationInterval = async ({
|
||||
@@ -562,6 +562,32 @@ export const deleteTracesByProjectId = async (projectId: string) => {
|
||||
});
|
||||
};
|
||||
|
||||
export const hasAnyUser = async (projectId: string) => {
|
||||
const query = `
|
||||
SELECT 1
|
||||
FROM traces
|
||||
WHERE project_id = {projectId: String}
|
||||
AND user_id IS NOT NULL
|
||||
AND user_id != ''
|
||||
LIMIT 1
|
||||
`;
|
||||
|
||||
const rows = await queryClickhouse<{ 1: number }>({
|
||||
query,
|
||||
params: {
|
||||
projectId,
|
||||
},
|
||||
tags: {
|
||||
feature: "tracing",
|
||||
type: "user",
|
||||
kind: "hasAny",
|
||||
projectId,
|
||||
},
|
||||
});
|
||||
|
||||
return rows.length > 0;
|
||||
};
|
||||
|
||||
export const getTotalUserCount = async (
|
||||
projectId: string,
|
||||
filter: FilterState,
|
||||
|
||||
@@ -14,6 +14,7 @@ export const convertTraceDomainToClickhouse = (
|
||||
name: trace.name,
|
||||
user_id: trace.userId,
|
||||
metadata: trace.metadata as Record<string, string>,
|
||||
environment: trace.environment,
|
||||
release: trace.release,
|
||||
version: trace.version,
|
||||
project_id: trace.projectId,
|
||||
@@ -38,6 +39,7 @@ export const convertClickhouseToDomain = (
|
||||
projectId: record.project_id,
|
||||
name: record.name ?? null,
|
||||
timestamp: parseClickhouseUTCDateTimeFormat(record.timestamp),
|
||||
environment: record.environment,
|
||||
tags: record.tags,
|
||||
bookmarked: record.bookmarked,
|
||||
release: record.release ?? null,
|
||||
|
||||
@@ -31,6 +31,7 @@ export type Observation = {
|
||||
id: string;
|
||||
traceId: string | null;
|
||||
projectId: string;
|
||||
environment: string;
|
||||
type: ObservationType;
|
||||
startTime: Date;
|
||||
endTime: Date | null;
|
||||
@@ -69,6 +70,7 @@ export type ObservationView = {
|
||||
type: ObservationType;
|
||||
startTime: Date;
|
||||
endTime: Date | null;
|
||||
environment: string;
|
||||
name: string | null;
|
||||
metadata: Prisma.JsonValue | null;
|
||||
parentObservationId: string | null;
|
||||
@@ -104,6 +106,7 @@ export type Score = {
|
||||
id: string;
|
||||
timestamp: Date;
|
||||
projectId: string;
|
||||
environment: string;
|
||||
name: string;
|
||||
value: number | null;
|
||||
source: ScoreSourceType;
|
||||
@@ -124,6 +127,7 @@ export type Trace = {
|
||||
timestamp: Date;
|
||||
name: string | null;
|
||||
userId: string | null;
|
||||
environment: string;
|
||||
metadata: Prisma.JsonValue | null;
|
||||
release: string | null;
|
||||
version: string | null;
|
||||
|
||||
@@ -271,7 +271,9 @@ const getSessionsTableGeneric = async <T>(props: FetchSessionsTableProps) => {
|
||||
? `
|
||||
,
|
||||
sum(o.obs_count) as total_observations,
|
||||
date_diff('millisecond', min(min_start_time), max(max_end_time)) as duration,
|
||||
-- Use minIf, because ClickHouse fills 1970-01-01 on left joins. We assume that no
|
||||
-- LLM session started on that date so this behaviour should yield better results.
|
||||
date_diff('millisecond', minIf(min_start_time, min_start_time > '1970-01-01'), max(max_end_time)) as duration,
|
||||
sumMap(o.sum_usage_details) as session_usage_details,
|
||||
sumMap(o.sum_cost_details) as session_cost_details,
|
||||
arraySum(mapValues(mapFilter(x -> positionCaseInsensitive(x.1, 'input') > 0, sumMap(o.sum_cost_details)))) as session_input_cost,
|
||||
|
||||
@@ -11,6 +11,7 @@ export const createTrace = (trace: Partial<TraceRecordInsertType>) => {
|
||||
project_id: v4(),
|
||||
session_id: v4(),
|
||||
timestamp: Date.now(),
|
||||
environment: "default",
|
||||
metadata: {
|
||||
source: "API",
|
||||
server: "Node",
|
||||
@@ -38,6 +39,7 @@ export const createObservation = (
|
||||
trace_id: v4(),
|
||||
project_id: v4(),
|
||||
type: "GENERATION",
|
||||
environment: "default",
|
||||
metadata: {
|
||||
source: "API",
|
||||
server: "Node",
|
||||
@@ -76,6 +78,7 @@ export const createScore = (score: Partial<ScoreRecordInsertType>) => {
|
||||
project_id: v4(),
|
||||
trace_id: v4(),
|
||||
observation_id: v4(),
|
||||
environment: "default",
|
||||
name: "test-score" + v4(),
|
||||
timestamp: Date.now(),
|
||||
value: 100.5,
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "web",
|
||||
"version": "3.30.0",
|
||||
"version": "3.34.1",
|
||||
"private": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
|
||||
@@ -2628,6 +2628,13 @@ components:
|
||||
type: boolean
|
||||
nullable: true
|
||||
description: Public traces are accessible via url without login
|
||||
environment:
|
||||
type: string
|
||||
nullable: true
|
||||
description: >-
|
||||
The environment from which this trace originated. Can be any
|
||||
lowercase alphanumeric string with hyphens and underscores that does
|
||||
not start with 'langfuse'.
|
||||
required:
|
||||
- id
|
||||
- timestamp
|
||||
@@ -2819,6 +2826,13 @@ components:
|
||||
The cost details of the observation. Key is the name of the cost
|
||||
metric, value is the cost in USD. The total key is the sum of all
|
||||
(non-total) cost metrics or the total value ingested.
|
||||
environment:
|
||||
type: string
|
||||
nullable: true
|
||||
description: >-
|
||||
The environment from which this observation originated. Can be any
|
||||
lowercase alphanumeric string with hyphens and underscores that does
|
||||
not start with 'langfuse'.
|
||||
required:
|
||||
- id
|
||||
- type
|
||||
@@ -3033,6 +3047,13 @@ components:
|
||||
description: >-
|
||||
Reference an annotation queue on a score. Populated if the score was
|
||||
initially created in an annotation queue.
|
||||
environment:
|
||||
type: string
|
||||
nullable: true
|
||||
description: >-
|
||||
The environment from which this score originated. Can be any
|
||||
lowercase alphanumeric string with hyphens and underscores that does
|
||||
not start with 'langfuse'.
|
||||
required:
|
||||
- id
|
||||
- traceId
|
||||
@@ -3734,6 +3755,9 @@ components:
|
||||
version:
|
||||
type: string
|
||||
nullable: true
|
||||
environment:
|
||||
type: string
|
||||
nullable: true
|
||||
CreateEventBody:
|
||||
title: CreateEventBody
|
||||
type: object
|
||||
@@ -3901,6 +3925,9 @@ components:
|
||||
parentObservationId:
|
||||
type: string
|
||||
nullable: true
|
||||
environment:
|
||||
type: string
|
||||
nullable: true
|
||||
required:
|
||||
- type
|
||||
TraceBody:
|
||||
@@ -3940,6 +3967,9 @@ components:
|
||||
items:
|
||||
type: string
|
||||
nullable: true
|
||||
environment:
|
||||
type: string
|
||||
nullable: true
|
||||
public:
|
||||
type: boolean
|
||||
nullable: true
|
||||
@@ -3964,6 +3994,9 @@ components:
|
||||
name:
|
||||
type: string
|
||||
example: novelty
|
||||
environment:
|
||||
type: string
|
||||
nullable: true
|
||||
value:
|
||||
$ref: '#/components/schemas/CreateScoreValue'
|
||||
description: >-
|
||||
|
||||
@@ -54,6 +54,21 @@ describe("/api/public/ingestion API Endpoint", () => {
|
||||
},
|
||||
},
|
||||
],
|
||||
[
|
||||
"non-default-environment",
|
||||
{
|
||||
id: randomUUID(),
|
||||
type: "trace-create",
|
||||
timestamp: new Date().toISOString(),
|
||||
body: {
|
||||
id: randomUUID(),
|
||||
timestamp: new Date().toISOString(),
|
||||
metadata: { hello: "world" },
|
||||
input: ["hello", { world: "world" }, [1, 2, 3]],
|
||||
environment: "production",
|
||||
},
|
||||
},
|
||||
],
|
||||
])(
|
||||
"should create traces via the ingestion API (%s)",
|
||||
async (_name: string, entity: any) => {
|
||||
@@ -71,6 +86,9 @@ describe("/api/public/ingestion API Endpoint", () => {
|
||||
expect(trace!.metadata).toEqual(entity.body?.metadata ?? {});
|
||||
expect(trace!.input).toEqual(entity.body?.input ?? null);
|
||||
expect(trace!.output).toEqual(entity.body?.output ?? null);
|
||||
expect(trace!.environment).toEqual(
|
||||
entity.body?.environment ?? "default",
|
||||
);
|
||||
});
|
||||
},
|
||||
);
|
||||
@@ -124,6 +142,23 @@ describe("/api/public/ingestion API Endpoint", () => {
|
||||
},
|
||||
},
|
||||
],
|
||||
[
|
||||
"span-non-default-environment",
|
||||
"SPAN",
|
||||
{
|
||||
id: randomUUID(),
|
||||
type: "span-create",
|
||||
timestamp: new Date().toISOString(),
|
||||
body: {
|
||||
id: randomUUID(),
|
||||
traceId: randomUUID(),
|
||||
startTime: new Date().toISOString(),
|
||||
input: ["hello", { world: "world" }, [1, 2, 3]],
|
||||
output: ["hello", { world: [2, 3, "test"] }, [1, 2, 3]],
|
||||
environment: "production",
|
||||
},
|
||||
},
|
||||
],
|
||||
])(
|
||||
"should create observations via the ingestion API (%s)",
|
||||
async (_name: string, type: string, entity: any) => {
|
||||
@@ -146,6 +181,9 @@ describe("/api/public/ingestion API Endpoint", () => {
|
||||
expect(observation!.input).toEqual(entity.body?.input ?? null);
|
||||
expect(observation!.output).toEqual(entity.body?.output ?? null);
|
||||
expect(observation!.type).toBe(type);
|
||||
expect(observation!.environment).toEqual(
|
||||
entity.body?.environment ?? "default",
|
||||
);
|
||||
});
|
||||
},
|
||||
);
|
||||
@@ -166,6 +204,22 @@ describe("/api/public/ingestion API Endpoint", () => {
|
||||
},
|
||||
},
|
||||
],
|
||||
[
|
||||
"non-default-environment",
|
||||
{
|
||||
id: randomUUID(),
|
||||
type: "score-create",
|
||||
timestamp: new Date().toISOString(),
|
||||
body: {
|
||||
id: randomUUID(),
|
||||
name: "score-name",
|
||||
traceId: randomUUID(),
|
||||
value: 100.5,
|
||||
observationId: randomUUID(),
|
||||
environment: "production",
|
||||
},
|
||||
},
|
||||
],
|
||||
])(
|
||||
"should create scores via the ingestion API (%s)",
|
||||
async (_name: string, entity: any) => {
|
||||
@@ -181,6 +235,9 @@ describe("/api/public/ingestion API Endpoint", () => {
|
||||
expect(score!.id).toBe(entity.body.id);
|
||||
expect(score!.projectId).toBe(projectId);
|
||||
expect(score!.value).toEqual(100.5);
|
||||
expect(score!.environment).toEqual(
|
||||
entity.body?.environment ?? "default",
|
||||
);
|
||||
});
|
||||
},
|
||||
);
|
||||
@@ -218,6 +275,36 @@ describe("/api/public/ingestion API Endpoint", () => {
|
||||
expect(response.body.errors[0].message).toBe("Invalid request data");
|
||||
});
|
||||
|
||||
it.each([
|
||||
"langfuse-test",
|
||||
".invalidcharacter!",
|
||||
"incrediblylongstringwithmorethan40characters",
|
||||
])(
|
||||
"should fail for invalid environments (%s)",
|
||||
async (environment: string) => {
|
||||
const entity = {
|
||||
id: randomUUID(),
|
||||
type: "score-create",
|
||||
timestamp: new Date().toISOString(),
|
||||
body: {
|
||||
id: randomUUID(),
|
||||
name: "score-name",
|
||||
traceId: randomUUID(),
|
||||
value: 100.5,
|
||||
observationId: randomUUID(),
|
||||
environment,
|
||||
},
|
||||
};
|
||||
|
||||
const response = await makeAPICall("POST", "/api/public/ingestion", {
|
||||
batch: [entity],
|
||||
});
|
||||
|
||||
expect(response.status).toBe(207);
|
||||
expect(response.body.errors[0].status).toBe(400);
|
||||
},
|
||||
);
|
||||
|
||||
// Disabled until eventLog becomes the default behaviour.
|
||||
it("should create a log entry for the S3 file", async () => {
|
||||
const traceId = v4();
|
||||
|
||||
@@ -322,4 +322,68 @@ describe("trpc.sessions", () => {
|
||||
expect(Number(session2?.session_output_usage)).toBeGreaterThan(0);
|
||||
expect(Number(session2?.session_total_usage)).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
it("LFE-4113: should GET correct metrics for a list of sessions without observations", async () => {
|
||||
const { projectId } = await createOrgProjectAndApiKey();
|
||||
const sessionId = v4();
|
||||
|
||||
await prisma.traceSession.createMany({
|
||||
data: [
|
||||
{
|
||||
id: sessionId,
|
||||
projectId: projectId,
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
const traces = [
|
||||
createTrace({
|
||||
session_id: sessionId,
|
||||
project_id: projectId,
|
||||
user_id: "user1",
|
||||
}),
|
||||
createTrace({
|
||||
session_id: sessionId,
|
||||
project_id: projectId,
|
||||
user_id: "user3",
|
||||
}),
|
||||
];
|
||||
|
||||
await createTracesCh(traces);
|
||||
|
||||
// Only trace 2 has observations
|
||||
const observations = [
|
||||
createObservation({
|
||||
trace_id: traces[1].id,
|
||||
project_id: projectId,
|
||||
start_time: new Date().getTime() - 1000,
|
||||
}),
|
||||
createObservation({
|
||||
trace_id: traces[1].id,
|
||||
project_id: projectId,
|
||||
start_time: new Date().getTime(),
|
||||
}),
|
||||
];
|
||||
|
||||
await createObservationsCh(observations);
|
||||
|
||||
const sessions = await getSessionsWithMetrics({
|
||||
projectId: projectId,
|
||||
filter: [
|
||||
{
|
||||
column: "id",
|
||||
type: "stringOptions",
|
||||
operator: "any of",
|
||||
value: [sessionId],
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
expect(sessions.length).toBe(1);
|
||||
|
||||
expect(sessions[0]).toBeDefined();
|
||||
expect(sessions[0]?.trace_count).toBe(2);
|
||||
expect(parseInt(sessions[0]?.duration as any)).toBeGreaterThan(995);
|
||||
expect(parseInt(sessions[0]?.duration as any)).toBeLessThan(1005);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -332,7 +332,75 @@ describe("OTel Resource Span Mapping", () => {
|
||||
},
|
||||
};
|
||||
|
||||
it("should interpret an empty buffer as an unset parentSpanId", async () => {
|
||||
// https://github.com/langchain4j/langchain4j/issues/2328#issuecomment-2686129552
|
||||
// Empty buffers where detected as truthy, i.e. behaved like they had a parent span.
|
||||
// Setup
|
||||
const resourceSpan = {
|
||||
scopeSpans: [
|
||||
{
|
||||
spans: [
|
||||
{
|
||||
...defaultSpanProps,
|
||||
parentSpanId: {
|
||||
type: "Buffer",
|
||||
data: [],
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
// When
|
||||
const langfuseEvents = convertOtelSpanToIngestionEvent(resourceSpan);
|
||||
|
||||
// Then
|
||||
// Expect a span and a trace to be created
|
||||
expect(langfuseEvents).toHaveLength(2);
|
||||
});
|
||||
|
||||
it.each([
|
||||
[
|
||||
"should cast input_tokens from string to number",
|
||||
{
|
||||
entity: "observation",
|
||||
otelAttributeKey: "gen_ai.usage.input_tokens",
|
||||
otelAttributeValue: { stringValue: "15" },
|
||||
entityAttributeKey: "usageDetails.input",
|
||||
entityAttributeValue: 15,
|
||||
},
|
||||
],
|
||||
[
|
||||
"should extract environment on trace for langfuse.environment",
|
||||
{
|
||||
entity: "trace",
|
||||
otelAttributeKey: "langfuse.environment",
|
||||
otelAttributeValue: { stringValue: "test" },
|
||||
entityAttributeKey: "environment",
|
||||
entityAttributeValue: "test",
|
||||
},
|
||||
],
|
||||
[
|
||||
"should extract environment on observation for deployment.environment.name",
|
||||
{
|
||||
entity: "observation",
|
||||
otelAttributeKey: "deployment.environment.name",
|
||||
otelAttributeValue: { stringValue: "test" },
|
||||
entityAttributeKey: "environment",
|
||||
entityAttributeValue: "test",
|
||||
},
|
||||
],
|
||||
[
|
||||
"should fallback to default on observation if no environment present",
|
||||
{
|
||||
entity: "observation",
|
||||
otelAttributeKey: "unused.key",
|
||||
otelAttributeValue: { stringValue: "" },
|
||||
entityAttributeKey: "environment",
|
||||
entityAttributeValue: "default",
|
||||
},
|
||||
],
|
||||
[
|
||||
"should extract promptName on observation from langfuse.prompt.name",
|
||||
{
|
||||
@@ -705,6 +773,36 @@ describe("OTel Resource Span Mapping", () => {
|
||||
entityAttributeValue: "1.0.5",
|
||||
},
|
||||
],
|
||||
[
|
||||
"should extract environment on trace for langfuse.environment",
|
||||
{
|
||||
entity: "trace",
|
||||
otelResourceAttributeKey: "langfuse.environment",
|
||||
otelResourceAttributeValue: { stringValue: "test" },
|
||||
entityAttributeKey: "environment",
|
||||
entityAttributeValue: "test",
|
||||
},
|
||||
],
|
||||
[
|
||||
"should extract environment on observation for deployment.environment.name",
|
||||
{
|
||||
entity: "observation",
|
||||
otelResourceAttributeKey: "deployment.environment.name",
|
||||
otelResourceAttributeValue: { stringValue: "test" },
|
||||
entityAttributeKey: "environment",
|
||||
entityAttributeValue: "test",
|
||||
},
|
||||
],
|
||||
[
|
||||
"should fallback to default on observation if no environment present",
|
||||
{
|
||||
entity: "observation",
|
||||
otelResourceAttributeKey: "unused.key",
|
||||
otelResourceAttributeValue: { stringValue: "" },
|
||||
entityAttributeKey: "environment",
|
||||
entityAttributeValue: "default",
|
||||
},
|
||||
],
|
||||
])(
|
||||
"ResourceAttributes: %s",
|
||||
(
|
||||
@@ -745,5 +843,125 @@ describe("OTel Resource Span Mapping", () => {
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
it.each([
|
||||
[
|
||||
"should extract input on trace from event attributes",
|
||||
{
|
||||
entity: "trace",
|
||||
otelEventName: "gen_ai.content.prompt",
|
||||
otelEventAttributeKey: "gen_ai.prompt",
|
||||
otelEventAttributeValue: {
|
||||
stringValue: "user: What is LLM Observability?",
|
||||
},
|
||||
entityAttributeKey: "input",
|
||||
entityAttributeValue: "user: What is LLM Observability?",
|
||||
},
|
||||
],
|
||||
[
|
||||
"should extract array input on trace event attributes",
|
||||
{
|
||||
entity: "trace",
|
||||
otelEventName: "gen_ai.content.prompt",
|
||||
otelEventAttributeKey: "gen_ai.prompt",
|
||||
otelEventAttributeValue: {
|
||||
arrayValue: {
|
||||
values: [
|
||||
{
|
||||
stringValue: "Reply with the word 'java'",
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
entityAttributeKey: "input",
|
||||
entityAttributeValue: ["Reply with the word 'java'"],
|
||||
},
|
||||
],
|
||||
[
|
||||
"should extract output on observation from event attributes",
|
||||
{
|
||||
entity: "observation",
|
||||
otelEventName: "gen_ai.content.completion",
|
||||
otelEventAttributeKey: "gen_ai.completion",
|
||||
otelEventAttributeValue: {
|
||||
stringValue:
|
||||
"assistant: LLM Observability stands for logs, metrics, and traces observability.",
|
||||
},
|
||||
entityAttributeKey: "output",
|
||||
entityAttributeValue:
|
||||
"assistant: LLM Observability stands for logs, metrics, and traces observability.",
|
||||
},
|
||||
],
|
||||
[
|
||||
"should extract output on observation from event attributes even if no gen_ai.completion attribute is available",
|
||||
{
|
||||
entity: "observation",
|
||||
otelEventName: "gen_ai.content.completion",
|
||||
otelEventAttributeKey: "gen_ai.something_else",
|
||||
otelEventAttributeValue: {
|
||||
stringValue:
|
||||
"assistant: LLM Observability stands for logs, metrics, and traces observability.",
|
||||
},
|
||||
entityAttributeKey: "output",
|
||||
entityAttributeValue: {
|
||||
"gen_ai.something_else":
|
||||
"assistant: LLM Observability stands for logs, metrics, and traces observability.",
|
||||
},
|
||||
},
|
||||
],
|
||||
])(
|
||||
"Events: %s",
|
||||
(
|
||||
_name: string,
|
||||
spec: {
|
||||
entity: string;
|
||||
otelEventName: string;
|
||||
otelEventAttributeKey: string;
|
||||
otelEventAttributeValue: any;
|
||||
entityAttributeKey: string;
|
||||
entityAttributeValue: any;
|
||||
},
|
||||
) => {
|
||||
// Setup
|
||||
const resourceSpan = {
|
||||
resource: {},
|
||||
scopeSpans: [
|
||||
{
|
||||
spans: [
|
||||
{
|
||||
...defaultSpanProps,
|
||||
events: [
|
||||
{
|
||||
timeUnixNano: {
|
||||
low: 1327691067,
|
||||
high: 404677085,
|
||||
unsigned: true,
|
||||
},
|
||||
name: spec.otelEventName,
|
||||
attributes: [
|
||||
{
|
||||
key: spec.otelEventAttributeKey,
|
||||
value: spec.otelEventAttributeValue,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
// When
|
||||
const langfuseEvents = convertOtelSpanToIngestionEvent(resourceSpan);
|
||||
|
||||
// Then
|
||||
const entity: { body: Record<string, any> } =
|
||||
spec.entity === "trace" ? langfuseEvents[0] : langfuseEvents[1];
|
||||
expect(entity.body[spec.entityAttributeKey]).toEqual(
|
||||
spec.entityAttributeValue,
|
||||
);
|
||||
},
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -0,0 +1,63 @@
|
||||
import React from "react";
|
||||
import {
|
||||
SplashScreen,
|
||||
type ValueProposition,
|
||||
} from "@/src/components/ui/splash-screen";
|
||||
import { ClipboardCheck, Users, BarChart4, GitMerge } from "lucide-react";
|
||||
import { CreateOrEditAnnotationQueueButton } from "@/src/ee/features/annotation-queues/components/CreateOrEditAnnotationQueueButton";
|
||||
|
||||
export function AnnotationQueuesOnboarding({
|
||||
projectId,
|
||||
}: {
|
||||
projectId: string;
|
||||
}) {
|
||||
const valuePropositions: ValueProposition[] = [
|
||||
{
|
||||
title: "Manage scoring workflows",
|
||||
description:
|
||||
"Create and manage annotation queues to streamline your scoring workflows",
|
||||
icon: <ClipboardCheck className="h-4 w-4" />,
|
||||
},
|
||||
{
|
||||
title: "Collaborate with annotators",
|
||||
description:
|
||||
"Invite team members to annotate and evaluate your LLM outputs",
|
||||
icon: <Users className="h-4 w-4" />,
|
||||
},
|
||||
{
|
||||
title: "Track annotation metrics",
|
||||
description:
|
||||
"Monitor annotation progress and quality metrics across your team",
|
||||
icon: <BarChart4 className="h-4 w-4" />,
|
||||
},
|
||||
{
|
||||
title: "Baseline evaluation efforts",
|
||||
description:
|
||||
"Use annotation data as a baseline to evaluate your other evaluation metrics",
|
||||
icon: <GitMerge className="h-4 w-4" />,
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<SplashScreen
|
||||
title="Get Started with Annotation Queues"
|
||||
description="Annotation queues help you manage manual annotation/labeling for your LLM projects. Create queues, define annotation metrics, and track progress."
|
||||
valuePropositions={valuePropositions}
|
||||
primaryAction={{
|
||||
label: "Create Annotation Queue",
|
||||
component: (
|
||||
<CreateOrEditAnnotationQueueButton
|
||||
variant="default"
|
||||
projectId={projectId}
|
||||
size="lg"
|
||||
/>
|
||||
),
|
||||
}}
|
||||
secondaryAction={{
|
||||
label: "Learn More",
|
||||
href: "https://langfuse.com/docs/scores/annotation",
|
||||
}}
|
||||
videoSrc="https://static.langfuse.com/prod-assets/onboarding/annotation-queue-overview-v1.mp4"
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
import React from "react";
|
||||
import {
|
||||
SplashScreen,
|
||||
type ValueProposition,
|
||||
} from "@/src/components/ui/splash-screen";
|
||||
import { Database, Beaker, Zap, Code } from "lucide-react";
|
||||
import { DatasetActionButton } from "@/src/features/datasets/components/DatasetActionButton";
|
||||
|
||||
export function DatasetsOnboarding({ projectId }: { projectId: string }) {
|
||||
const valuePropositions: ValueProposition[] = [
|
||||
{
|
||||
title: "Continuous improvement",
|
||||
description:
|
||||
"Create datasets from production edge cases to improve your application",
|
||||
icon: <Zap className="h-4 w-4" />,
|
||||
},
|
||||
{
|
||||
title: "Pre-deployment testing",
|
||||
description: "Benchmark new releases before deploying to production",
|
||||
icon: <Beaker className="h-4 w-4" />,
|
||||
},
|
||||
{
|
||||
title: "Structured testing",
|
||||
description:
|
||||
"Run experiments on collections of inputs and expected outputs",
|
||||
icon: <Database className="h-4 w-4" />,
|
||||
},
|
||||
{
|
||||
title: "Custom workflows",
|
||||
description:
|
||||
"Build custom workflows around your datasets via the API and SDKs, e.g. for fine-tuning, few-shotting",
|
||||
icon: <Code className="h-4 w-4" />,
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<SplashScreen
|
||||
title="Get Started with Datasets"
|
||||
description="Datasets in Langfuse are collections of inputs (and expected outputs) for your LLM application. You can for example use them to benchmark new releases before deployment to production."
|
||||
valuePropositions={valuePropositions}
|
||||
primaryAction={{
|
||||
label: "Create Dataset",
|
||||
component: (
|
||||
<DatasetActionButton
|
||||
variant="default"
|
||||
mode="create"
|
||||
projectId={projectId}
|
||||
size="lg"
|
||||
/>
|
||||
),
|
||||
}}
|
||||
secondaryAction={{
|
||||
label: "Learn More",
|
||||
href: "https://langfuse.com/docs/datasets",
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
import React from "react";
|
||||
import {
|
||||
SplashScreen,
|
||||
type ValueProposition,
|
||||
} from "@/src/components/ui/splash-screen";
|
||||
import { Bot, Gauge, Zap, BarChart4 } from "lucide-react";
|
||||
|
||||
interface EvaluatorsOnboardingProps {
|
||||
projectId: string;
|
||||
}
|
||||
|
||||
export function EvaluatorsOnboarding({ projectId }: EvaluatorsOnboardingProps) {
|
||||
const valuePropositions: ValueProposition[] = [
|
||||
{
|
||||
title: "Automate evaluations",
|
||||
description:
|
||||
"Use LLM-as-a-judge to automatically evaluate your traces without manual review",
|
||||
icon: <Bot className="h-4 w-4" />,
|
||||
},
|
||||
{
|
||||
title: "Measure quality",
|
||||
description:
|
||||
"Create custom evaluation criteria to measure the quality of your LLM outputs",
|
||||
icon: <Gauge className="h-4 w-4" />,
|
||||
},
|
||||
{
|
||||
title: "Scale efficiently",
|
||||
description:
|
||||
"Evaluate thousands of traces automatically with customizable sampling rates",
|
||||
icon: <Zap className="h-4 w-4" />,
|
||||
},
|
||||
{
|
||||
title: "Track performance",
|
||||
description:
|
||||
"Monitor evaluation metrics over time to identify trends and improvements",
|
||||
icon: <BarChart4 className="h-4 w-4" />,
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<SplashScreen
|
||||
title="Get Started with LLM-as-a-Judge Evaluations"
|
||||
description="Create evaluation templates and evaluators to automatically score your traces with LLM-as-a-judge. Set up custom evaluation criteria and let AI help you measure the quality of your outputs."
|
||||
valuePropositions={valuePropositions}
|
||||
primaryAction={{
|
||||
label: "Create Evaluator",
|
||||
href: `/project/${projectId}/evals/new`,
|
||||
}}
|
||||
secondaryAction={{
|
||||
label: "Learn More",
|
||||
href: "https://langfuse.com/docs/scores/model-based-evals",
|
||||
}}
|
||||
videoSrc="https://static.langfuse.com/prod-assets/onboarding/scores-llm-as-a-judge-overview-v1.mp4"
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
import React from "react";
|
||||
import {
|
||||
SplashScreen,
|
||||
type ValueProposition,
|
||||
} from "@/src/components/ui/splash-screen";
|
||||
import { FileText, GitBranch, Zap, BarChart4 } from "lucide-react";
|
||||
|
||||
export function PromptsOnboarding({ projectId }: { projectId: string }) {
|
||||
const valuePropositions: ValueProposition[] = [
|
||||
{
|
||||
title: "Decoupled from code",
|
||||
description:
|
||||
"Deploy new prompts without application redeployment, making updates faster and easier",
|
||||
icon: <FileText className="h-4 w-4" />,
|
||||
},
|
||||
{
|
||||
title: "Edit in UI or programmatically",
|
||||
description:
|
||||
"Non-technical users can easily edit prompts in the UI. Developers can optionally update prompts programmatically via the API and SDKs",
|
||||
icon: <GitBranch className="h-4 w-4" />,
|
||||
},
|
||||
{
|
||||
title: "Performance optimized",
|
||||
description:
|
||||
"Client-side caching prevents latency or availability issues for your applications",
|
||||
icon: <Zap className="h-4 w-4" />,
|
||||
},
|
||||
{
|
||||
title: "Compare metrics",
|
||||
description:
|
||||
"Track latency, cost, and evaluation metrics across different prompt versions",
|
||||
icon: <BarChart4 className="h-4 w-4" />,
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<SplashScreen
|
||||
title="Get Started with Prompt Management"
|
||||
description="Langfuse Prompt Management helps you centrally manage, version control, and collaboratively iterate on your prompts. Start using prompt management to improve your LLM application's performance and maintainability."
|
||||
valuePropositions={valuePropositions}
|
||||
primaryAction={{
|
||||
label: "Create Prompt",
|
||||
href: `/project/${projectId}/prompts/new`,
|
||||
}}
|
||||
secondaryAction={{
|
||||
label: "Learn More",
|
||||
href: "https://langfuse.com/docs/prompts",
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
import React from "react";
|
||||
import {
|
||||
SplashScreen,
|
||||
type ValueProposition,
|
||||
} from "@/src/components/ui/splash-screen";
|
||||
import { ThumbsUp, Star, LineChart, Code } from "lucide-react";
|
||||
|
||||
export function ScoresOnboarding() {
|
||||
const valuePropositions: ValueProposition[] = [
|
||||
{
|
||||
title: "Collect user feedback",
|
||||
description:
|
||||
"Gather thumbs up/down feedback from users to identify high and low quality outputs",
|
||||
icon: <ThumbsUp className="h-4 w-4" />,
|
||||
},
|
||||
{
|
||||
title: "Run model-based evaluations",
|
||||
description:
|
||||
"Use LLMs to automatically evaluate your application's outputs",
|
||||
icon: <Star className="h-4 w-4" />,
|
||||
},
|
||||
{
|
||||
title: "Track quality metrics",
|
||||
description:
|
||||
"Monitor quality metrics over time to identify trends and issues",
|
||||
icon: <LineChart className="h-4 w-4" />,
|
||||
},
|
||||
{
|
||||
title: "Use custom metrics",
|
||||
description:
|
||||
"Langfuse's scores are flexible and can be used to track any metric that's associated with an LLM application",
|
||||
icon: <Code className="h-4 w-4" />,
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<SplashScreen
|
||||
title="Get Started with Scores"
|
||||
description="Scores allow you to evaluate the quality/safety of your LLM application through user feedback, model-based evaluations, or manual review. Scores can be used programmatically via the API and SDKs to track custom metrics."
|
||||
valuePropositions={valuePropositions}
|
||||
secondaryAction={{
|
||||
label: "Learn More",
|
||||
href: "https://langfuse.com/docs/scores",
|
||||
}}
|
||||
videoSrc="https://static.langfuse.com/prod-assets/onboarding/scores-overview-v1.mp4"
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
import React from "react";
|
||||
import {
|
||||
SplashScreen,
|
||||
type ValueProposition,
|
||||
} from "@/src/components/ui/splash-screen";
|
||||
import { BarChart4, GitMerge, MessageSquare, Users } from "lucide-react";
|
||||
import Link from "next/link";
|
||||
|
||||
export function SessionsOnboarding() {
|
||||
const valuePropositions: ValueProposition[] = [
|
||||
{
|
||||
title: "Group related traces",
|
||||
description:
|
||||
"Sessions allow you to group related traces, such as a conversation or thread, for better organization and analysis",
|
||||
icon: <MessageSquare className="h-4 w-4" />,
|
||||
},
|
||||
{
|
||||
title: "Track user interactions",
|
||||
description: "Monitor how users interact with your application over time",
|
||||
icon: <Users className="h-4 w-4" />,
|
||||
},
|
||||
{
|
||||
title: "Analyze conversation flows",
|
||||
description: "Understand the complete flow of multi-turn conversations",
|
||||
icon: <GitMerge className="h-4 w-4" />,
|
||||
},
|
||||
{
|
||||
title: "Session-level metrics",
|
||||
description:
|
||||
"Get aggregated metrics for entire sessions, including costs and token usage",
|
||||
icon: <BarChart4 className="h-4 w-4" />,
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<SplashScreen
|
||||
title="Get Started with Sessions"
|
||||
description="Sessions allow you to group related traces together, such as a conversation or thread. Use sessions to track interactions over time and analyze conversation/thread flows."
|
||||
valuePropositions={valuePropositions}
|
||||
gettingStarted={
|
||||
<span>
|
||||
To start using sessions, you need to add a `sessionId` to your traces.
|
||||
See{" "}
|
||||
<Link
|
||||
href="https://langfuse.com/docs/tracing-features/sessions"
|
||||
className="underline"
|
||||
>
|
||||
documentation
|
||||
</Link>{" "}
|
||||
for more details.
|
||||
</span>
|
||||
}
|
||||
videoSrc="https://static.langfuse.com/prod-assets/onboarding/sessions-overview-v1.mp4"
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
import React from "react";
|
||||
import {
|
||||
SplashScreen,
|
||||
type ValueProposition,
|
||||
} from "@/src/components/ui/splash-screen";
|
||||
import { setupTracingRoute } from "@/src/features/setup/setupRoutes";
|
||||
import { BarChart4, GitMerge, Search, Zap } from "lucide-react";
|
||||
|
||||
interface TracesOnboardingProps {
|
||||
projectId: string;
|
||||
}
|
||||
|
||||
export function TracesOnboarding({ projectId }: TracesOnboardingProps) {
|
||||
const valuePropositions: ValueProposition[] = [
|
||||
{
|
||||
title: "Full context capture",
|
||||
description:
|
||||
"Track the complete execution flow including API calls, context, prompts, parallelism and more",
|
||||
icon: <GitMerge className="h-4 w-4" />,
|
||||
},
|
||||
{
|
||||
title: "Cost monitoring",
|
||||
description: "Track model usage and costs across your application",
|
||||
icon: <BarChart4 className="h-4 w-4" />,
|
||||
},
|
||||
{
|
||||
title: "Basis for evaluation",
|
||||
description:
|
||||
"Add evaluation scores to identify issues and track metrics over time",
|
||||
icon: <Search className="h-4 w-4" />,
|
||||
},
|
||||
{
|
||||
title: "Open and Multi-modal",
|
||||
description:
|
||||
"Langfuse traces can include images, audio, and other modalities. You can fully customize them to fit your needs",
|
||||
icon: <Zap className="h-4 w-4" />,
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<SplashScreen
|
||||
title="Get Started with LLM Tracing"
|
||||
description="Traces allow you to track every LLM call and other relevant logic in your app/agent. Nested traces in Langfuse help to understand what is happening and identify the root cause of problems."
|
||||
valuePropositions={valuePropositions}
|
||||
primaryAction={{
|
||||
label: "Configure Tracing",
|
||||
href: setupTracingRoute(projectId),
|
||||
}}
|
||||
secondaryAction={{
|
||||
label: "View Documentation",
|
||||
href: "https://langfuse.com/docs/tracing",
|
||||
}}
|
||||
videoSrc="https://static.langfuse.com/prod-assets/onboarding/tracing-overview-v1.mp4"
|
||||
className="bg-background dark:bg-background dark:text-white"
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
import React from "react";
|
||||
import {
|
||||
SplashScreen,
|
||||
type ValueProposition,
|
||||
} from "@/src/components/ui/splash-screen";
|
||||
import { Users, LineChart, Filter, BarChart4 } from "lucide-react";
|
||||
import Link from "next/link";
|
||||
export function UsersOnboarding() {
|
||||
const valuePropositions: ValueProposition[] = [
|
||||
{
|
||||
title: "Track user interactions",
|
||||
description:
|
||||
"Attribute data in Langfuse to specific users by adding a userId to your traces",
|
||||
icon: <Users className="h-4 w-4" />,
|
||||
},
|
||||
{
|
||||
title: "Analyze user behavior",
|
||||
description:
|
||||
"Understand how different users interact with your LLM applications",
|
||||
icon: <LineChart className="h-4 w-4" />,
|
||||
},
|
||||
{
|
||||
title: "Filter by user segments",
|
||||
description:
|
||||
"Compare performance across different user segments to identify patterns",
|
||||
icon: <Filter className="h-4 w-4" />,
|
||||
},
|
||||
{
|
||||
title: "Monitor usage metrics",
|
||||
description:
|
||||
"Track token usage, costs, and other metrics on a per-user basis",
|
||||
icon: <BarChart4 className="h-4 w-4" />,
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<SplashScreen
|
||||
title="Get Started with User Tracking"
|
||||
description="Correlate costs, evaluations and other LLM Application metrics to specific users. Start tracking users to better understand how they interact with your LLM applications."
|
||||
valuePropositions={valuePropositions}
|
||||
gettingStarted={
|
||||
<span>
|
||||
To start tracking users, you need to add a `userId` to your traces.
|
||||
See{" "}
|
||||
<Link
|
||||
href="https://langfuse.com/docs/tracing-features/users"
|
||||
className="underline"
|
||||
>
|
||||
documentation
|
||||
</Link>{" "}
|
||||
for more details.
|
||||
</span>
|
||||
}
|
||||
videoSrc="https://static.langfuse.com/prod-assets/onboarding/users-overview-v1.mp4"
|
||||
className="bg-background dark:bg-background dark:text-white"
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -4,7 +4,7 @@ import { cva, type VariantProps } from "class-variance-authority";
|
||||
import { cn } from "@/src/utils/tailwind";
|
||||
|
||||
const alertVariants = cva(
|
||||
"relative w-full rounded-lg border p-3 [&>svg~*]:pl-7 [&>svg+div]:translate-y-[-3px] [&>svg]:absolute [&>svg]:left-4 [&>svg]:top-4 [&>svg]:text-foreground",
|
||||
"relative w-full rounded-lg border p-3 [&>svg~*]:pl-6 [&>svg+div]:translate-y-[-3px] [&>svg]:absolute [&>svg]:left-3 [&>svg]:top-3 [&>svg]:text-foreground",
|
||||
{
|
||||
variants: {
|
||||
variant: {
|
||||
|
||||
@@ -0,0 +1,153 @@
|
||||
import React, { useState } from "react";
|
||||
import { cn } from "@/src/utils/tailwind";
|
||||
import Image from "next/image";
|
||||
import { InfoIcon } from "lucide-react";
|
||||
import { ActionButton } from "@/src/components/ActionButton";
|
||||
import { Alert, AlertTitle, AlertDescription } from "@/src/components/ui/alert";
|
||||
|
||||
export interface ValueProposition {
|
||||
title: string;
|
||||
description: string;
|
||||
icon?: React.ReactNode;
|
||||
}
|
||||
|
||||
export interface ActionConfig {
|
||||
label: string;
|
||||
href?: string;
|
||||
onClick?: () => void;
|
||||
component?: React.ReactNode;
|
||||
}
|
||||
|
||||
export interface SplashScreenProps {
|
||||
title: string;
|
||||
description: string;
|
||||
image?: {
|
||||
src: string;
|
||||
alt: string;
|
||||
width: number;
|
||||
height: number;
|
||||
};
|
||||
videoSrc?: string;
|
||||
valuePropositions?: ValueProposition[];
|
||||
primaryAction?: ActionConfig;
|
||||
secondaryAction?: ActionConfig;
|
||||
gettingStarted?: string | React.ReactNode;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
interface VideoPlayerProps {
|
||||
videoSrc: string;
|
||||
}
|
||||
|
||||
function VideoPlayer({ videoSrc }: VideoPlayerProps) {
|
||||
const [hasError, setHasError] = useState(false);
|
||||
const [isLoaded, setIsLoaded] = useState(false);
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
"my-6 w-full max-w-3xl overflow-hidden rounded-lg border border-border",
|
||||
{
|
||||
hidden: !isLoaded || hasError,
|
||||
},
|
||||
)}
|
||||
>
|
||||
<video
|
||||
src={videoSrc}
|
||||
controls
|
||||
autoPlay
|
||||
muted
|
||||
loop
|
||||
controlsList="nodownload"
|
||||
className="w-full"
|
||||
onError={() => setHasError(true)}
|
||||
onLoadedData={() => setIsLoaded(true)}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function SplashScreen({
|
||||
title,
|
||||
description,
|
||||
image,
|
||||
videoSrc,
|
||||
valuePropositions = [],
|
||||
primaryAction,
|
||||
secondaryAction,
|
||||
gettingStarted,
|
||||
className,
|
||||
}: SplashScreenProps) {
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
"mx-auto flex max-w-4xl flex-col items-center p-8",
|
||||
className,
|
||||
)}
|
||||
>
|
||||
<div className="mb-6 text-center">
|
||||
<h2 className="mb-2 text-2xl font-bold">{title}</h2>
|
||||
<p className="text-muted-foreground">{description}</p>
|
||||
</div>
|
||||
|
||||
<div className="mb-8 flex w-full flex-wrap justify-center gap-4">
|
||||
{primaryAction &&
|
||||
(primaryAction.component || (
|
||||
<ActionButton
|
||||
size="lg"
|
||||
onClick={primaryAction.onClick}
|
||||
href={primaryAction.href}
|
||||
>
|
||||
{primaryAction.label}
|
||||
</ActionButton>
|
||||
))}
|
||||
|
||||
{secondaryAction &&
|
||||
(secondaryAction.component || (
|
||||
<ActionButton
|
||||
variant="outline"
|
||||
size="lg"
|
||||
onClick={secondaryAction.onClick}
|
||||
href={secondaryAction.href}
|
||||
>
|
||||
{secondaryAction.label}
|
||||
</ActionButton>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{gettingStarted && (
|
||||
<Alert className="w-full max-w-3xl">
|
||||
<InfoIcon className="mr-2 h-4 w-4" />
|
||||
<AlertTitle>Getting Started</AlertTitle>
|
||||
<AlertDescription>{gettingStarted}</AlertDescription>
|
||||
</Alert>
|
||||
)}
|
||||
|
||||
{videoSrc && <VideoPlayer videoSrc={videoSrc} />}
|
||||
|
||||
{!videoSrc && image && (
|
||||
<div className="my-6 w-full max-w-3xl">
|
||||
<Image
|
||||
src={image.src}
|
||||
alt={image.alt}
|
||||
width={image.width}
|
||||
height={image.height}
|
||||
className="rounded-md"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{valuePropositions.length > 0 && (
|
||||
<div className="my-6 grid w-full max-w-3xl grid-cols-1 gap-4 md:grid-cols-2">
|
||||
{valuePropositions.map((prop, index) => (
|
||||
<Alert key={index}>
|
||||
{prop.icon}
|
||||
<AlertTitle>{prop.title}</AlertTitle>
|
||||
<AlertDescription>{prop.description}</AlertDescription>
|
||||
</Alert>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1 +1 @@
|
||||
export const VERSION = "v3.30.0";
|
||||
export const VERSION = "v3.34.1";
|
||||
|
||||
+5
-2
@@ -1,4 +1,4 @@
|
||||
import { Button } from "@/src/components/ui/button";
|
||||
import { Button, type ButtonProps } from "@/src/components/ui/button";
|
||||
import React, { useEffect, useState } from "react";
|
||||
import {
|
||||
Dialog,
|
||||
@@ -44,10 +44,12 @@ export const CreateOrEditAnnotationQueueButton = ({
|
||||
projectId,
|
||||
queueId,
|
||||
variant = "secondary",
|
||||
size,
|
||||
}: {
|
||||
projectId: string;
|
||||
queueId?: string;
|
||||
variant?: "secondary" | "ghost";
|
||||
variant?: ButtonProps["variant"];
|
||||
size?: ButtonProps["size"];
|
||||
}) => {
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const hasAccess = useHasProjectAccess({
|
||||
@@ -168,6 +170,7 @@ export const CreateOrEditAnnotationQueueButton = ({
|
||||
hasEntitlement={hasEntitlement}
|
||||
limitValue={queueCountData.data}
|
||||
limit={queueLimit}
|
||||
size={size}
|
||||
>
|
||||
<span className="ml-1 text-sm font-normal">
|
||||
{queueId ? "Edit" : "New queue"}
|
||||
|
||||
@@ -4,6 +4,8 @@ import { useHasProjectAccess } from "@/src/features/rbac/utils/checkProjectAcces
|
||||
import { useHasEntitlement } from "@/src/features/entitlements/hooks";
|
||||
import { SupportOrUpgradePage } from "@/src/ee/features/billing/components/SupportOrUpgradePage";
|
||||
import Page from "@/src/components/layouts/page";
|
||||
import { AnnotationQueuesOnboarding } from "@/src/components/onboarding/AnnotationQueuesOnboarding";
|
||||
import { api } from "@/src/utils/api";
|
||||
|
||||
export default function AnnotationQueues() {
|
||||
const router = useRouter();
|
||||
@@ -13,6 +15,23 @@ export default function AnnotationQueues() {
|
||||
scope: "annotationQueues:read",
|
||||
});
|
||||
const hasEntitlement = useHasEntitlement("annotation-queues");
|
||||
|
||||
// Check if the user has any annotation queues
|
||||
const { data: hasAnyQueue, isLoading } = api.annotationQueues.hasAny.useQuery(
|
||||
{ projectId },
|
||||
{
|
||||
enabled: !!projectId && hasEntitlement,
|
||||
trpc: {
|
||||
context: {
|
||||
skipBatch: true,
|
||||
},
|
||||
},
|
||||
refetchInterval: 10_000,
|
||||
},
|
||||
);
|
||||
|
||||
const showOnboarding = !isLoading && !hasAnyQueue;
|
||||
|
||||
if (!hasAccess || !hasEntitlement) return <SupportOrUpgradePage />;
|
||||
|
||||
return (
|
||||
@@ -25,8 +44,14 @@ export default function AnnotationQueues() {
|
||||
href: "https://langfuse.com/docs/scores/annotation",
|
||||
},
|
||||
}}
|
||||
scrollable={showOnboarding}
|
||||
>
|
||||
<AnnotationQueuesTable projectId={projectId} />
|
||||
{/* Show onboarding screen if user has no annotation queues */}
|
||||
{showOnboarding ? (
|
||||
<AnnotationQueuesOnboarding projectId={projectId} />
|
||||
) : (
|
||||
<AnnotationQueuesTable projectId={projectId} />
|
||||
)}
|
||||
</Page>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -19,6 +19,35 @@ import { TRPCError } from "@trpc/server";
|
||||
import { z } from "zod";
|
||||
|
||||
export const queueRouter = createTRPCRouter({
|
||||
hasAny: protectedProjectProcedure
|
||||
.input(
|
||||
z.object({
|
||||
projectId: z.string(),
|
||||
}),
|
||||
)
|
||||
.query(async ({ input, ctx }) => {
|
||||
throwIfNoEntitlement({
|
||||
entitlement: "annotation-queues",
|
||||
projectId: input.projectId,
|
||||
sessionUser: ctx.session.user,
|
||||
});
|
||||
|
||||
throwIfNoProjectAccess({
|
||||
session: ctx.session,
|
||||
projectId: input.projectId,
|
||||
scope: "annotationQueues:read",
|
||||
});
|
||||
|
||||
const queue = await ctx.prisma.annotationQueue.findFirst({
|
||||
where: {
|
||||
projectId: input.projectId,
|
||||
},
|
||||
select: { id: true },
|
||||
take: 1,
|
||||
});
|
||||
|
||||
return queue !== null;
|
||||
}),
|
||||
all: protectedProjectProcedure
|
||||
.input(
|
||||
z.object({
|
||||
|
||||
@@ -1,9 +1,22 @@
|
||||
// TODO: replace placeholder div with actual component
|
||||
import { AlertCircle } from "lucide-react";
|
||||
import { Alert, AlertTitle, AlertDescription } from "@/src/components/ui/alert";
|
||||
|
||||
export const SupportOrUpgradePage = () => {
|
||||
return (
|
||||
<div>
|
||||
You have no access to this feature. Check with your system administrator
|
||||
to get elevated access or upgrade your plan.
|
||||
<div className="flex h-full w-full items-center justify-center p-6">
|
||||
<div className="w-full max-w-md">
|
||||
<Alert>
|
||||
<AlertCircle className="h-4 w-4" />
|
||||
<AlertTitle>Access Restricted</AlertTitle>
|
||||
<AlertDescription>
|
||||
<p className="mb-2">This feature requires additional permissions</p>
|
||||
<p>
|
||||
Contact your system/project administrator for access or upgrade
|
||||
your plan. Need help? Reach out to support.
|
||||
</p>
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -12,27 +12,22 @@ import {
|
||||
} from "@/src/components/ui/tabs-bar";
|
||||
import { ActionButton } from "@/src/components/ActionButton";
|
||||
import { api } from "@/src/utils/api";
|
||||
import { useEntitlementLimit } from "@/src/features/entitlements/hooks";
|
||||
import {
|
||||
useEntitlementLimit,
|
||||
useHasEntitlement,
|
||||
} from "@/src/features/entitlements/hooks";
|
||||
import { SupportOrUpgradePage } from "@/src/ee/features/billing/components/SupportOrUpgradePage";
|
||||
import { EvaluatorsOnboarding } from "@/src/components/onboarding/EvaluatorsOnboarding";
|
||||
|
||||
export default function EvaluatorsPage() {
|
||||
const router = useRouter();
|
||||
const projectId = router.query.projectId as string;
|
||||
const capture = usePostHogClientCapture();
|
||||
|
||||
// only fetched first page to get count of active evaluators
|
||||
// ok as this includes the first 50 active evaluators
|
||||
const evaluatorsFirstPage = api.evals.allConfigs.useQuery({
|
||||
projectId,
|
||||
page: 0,
|
||||
limit: 50,
|
||||
});
|
||||
const evaluatorCountFirstPage = evaluatorsFirstPage.data?.configs.filter(
|
||||
(e) => e.status === "ACTIVE",
|
||||
).length;
|
||||
|
||||
const evaluatorLimit = useEntitlementLimit(
|
||||
"model-based-evaluations-count-evaluators",
|
||||
);
|
||||
const hasEntitlement = useHasEntitlement("model-based-evaluations");
|
||||
const hasWriteAccess = useHasProjectAccess({
|
||||
projectId,
|
||||
scope: "evalJob:CUD",
|
||||
@@ -43,8 +38,45 @@ export default function EvaluatorsPage() {
|
||||
scope: "evalJob:read",
|
||||
});
|
||||
|
||||
if (!hasReadAccess) {
|
||||
return null;
|
||||
// Fetch counts of evaluator configs and templates
|
||||
const countsQuery = api.evals.counts.useQuery(
|
||||
{
|
||||
projectId,
|
||||
},
|
||||
{
|
||||
enabled: !!projectId && hasEntitlement,
|
||||
trpc: {
|
||||
context: {
|
||||
skipBatch: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
const showOnboarding =
|
||||
countsQuery.data?.configCount === 0 &&
|
||||
countsQuery.data?.templateCount === 0;
|
||||
|
||||
if (!hasReadAccess || !hasEntitlement) {
|
||||
return <SupportOrUpgradePage />;
|
||||
}
|
||||
|
||||
if (showOnboarding) {
|
||||
return (
|
||||
<Page
|
||||
headerProps={{
|
||||
title: "Evaluators",
|
||||
help: {
|
||||
description:
|
||||
"Use LLM-as-a-judge evaluators as practical addition to human annotation. Configure an evaluation prompt and a model as judge to evaluate incoming traces.",
|
||||
href: "https://langfuse.com/docs/scores/model-based-evals",
|
||||
},
|
||||
}}
|
||||
scrollable
|
||||
>
|
||||
<EvaluatorsOnboarding projectId={projectId} />
|
||||
</Page>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
@@ -78,7 +110,7 @@ export default function EvaluatorsPage() {
|
||||
variant="outline"
|
||||
onClick={() => capture("eval_config:new_form_open")}
|
||||
href={`/project/${projectId}/evals/new`}
|
||||
limitValue={evaluatorCountFirstPage ?? 0}
|
||||
limitValue={countsQuery.data?.configCount ?? 0}
|
||||
limit={evaluatorLimit}
|
||||
>
|
||||
New evaluator
|
||||
|
||||
@@ -33,7 +33,7 @@ export default function TemplatesPage() {
|
||||
return (
|
||||
<Page
|
||||
headerProps={{
|
||||
title: "Eval Templates",
|
||||
title: "Evaluators",
|
||||
help: {
|
||||
description:
|
||||
"Create an evaluation template. Choose from one of the pre-defined templates or create your own.",
|
||||
|
||||
@@ -134,6 +134,40 @@ export const evalRouter = createTRPCRouter({
|
||||
});
|
||||
return env.LANGFUSE_MAX_HISTORIC_EVAL_CREATION_LIMIT;
|
||||
}),
|
||||
counts: protectedProjectProcedure
|
||||
.input(z.object({ projectId: z.string() }))
|
||||
.query(async ({ input, ctx }) => {
|
||||
throwIfNoEntitlement({
|
||||
entitlement: "model-based-evaluations",
|
||||
projectId: input.projectId,
|
||||
sessionUser: ctx.session.user,
|
||||
});
|
||||
|
||||
throwIfNoProjectAccess({
|
||||
session: ctx.session,
|
||||
projectId: input.projectId,
|
||||
scope: "evalJob:read",
|
||||
});
|
||||
|
||||
const [configCount, templateCount] = await Promise.all([
|
||||
ctx.prisma.jobConfiguration.count({
|
||||
where: {
|
||||
projectId: input.projectId,
|
||||
jobType: "EVAL",
|
||||
},
|
||||
}),
|
||||
ctx.prisma.evalTemplate.count({
|
||||
where: {
|
||||
projectId: input.projectId,
|
||||
},
|
||||
}),
|
||||
]);
|
||||
|
||||
return {
|
||||
configCount,
|
||||
templateCount,
|
||||
};
|
||||
}),
|
||||
allConfigs: protectedProjectProcedure
|
||||
.input(
|
||||
z.object({
|
||||
|
||||
@@ -1,12 +1,15 @@
|
||||
import { z } from "zod";
|
||||
import { createTRPCRouter, protectedProcedure } from "@/src/server/api/trpc";
|
||||
import {
|
||||
createTRPCRouter,
|
||||
protectedProcedureWithoutTracing,
|
||||
} from "@/src/server/api/trpc";
|
||||
import { updateUserPassword } from "@/src/features/auth-credentials/lib/credentialsServerUtils";
|
||||
import { TRPCError } from "@trpc/server";
|
||||
import { isEmailVerifiedWithinCutoff } from "@/src/features/auth-credentials/lib/credentialsUtils";
|
||||
import { passwordSchema } from "@/src/features/auth/lib/signupSchema";
|
||||
|
||||
export const credentialsRouter = createTRPCRouter({
|
||||
resetPassword: protectedProcedure
|
||||
resetPassword: protectedProcedureWithoutTracing
|
||||
.input(
|
||||
z.object({
|
||||
password: passwordSchema,
|
||||
|
||||
@@ -5,6 +5,14 @@ import { getSsoAuthProviderIdForDomain } from "@/src/ee/features/multi-tenant-ss
|
||||
import type { NextApiRequest, NextApiResponse } from "next";
|
||||
import { logger } from "@langfuse/shared/src/server";
|
||||
|
||||
export function getSSOBlockedDomains() {
|
||||
return (
|
||||
env.AUTH_DOMAINS_WITH_SSO_ENFORCEMENT?.split(",")
|
||||
.map((domain) => domain.trim().toLowerCase())
|
||||
.filter(Boolean) ?? []
|
||||
);
|
||||
}
|
||||
|
||||
/*
|
||||
* Sign-up endpoint (email/password users), creates user in database.
|
||||
* SSO users are created by the NextAuth adapters.
|
||||
@@ -41,8 +49,7 @@ export async function signupApiHandler(
|
||||
const body = validBody.data;
|
||||
|
||||
// check if email domain is blocked from email/password sign up via env
|
||||
const blockedDomains =
|
||||
env.AUTH_DOMAINS_WITH_SSO_ENFORCEMENT?.split(",") ?? [];
|
||||
const blockedDomains = getSSOBlockedDomains();
|
||||
const domain = body.email.split("@")[1]?.toLowerCase();
|
||||
if (domain && blockedDomains.includes(domain)) {
|
||||
res.status(422).json({
|
||||
@@ -58,6 +65,7 @@ export async function signupApiHandler(
|
||||
res.status(422).json({
|
||||
message: "You must sign in via SSO for this domain.",
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// create the user
|
||||
@@ -69,16 +77,12 @@ export async function signupApiHandler(
|
||||
body.name,
|
||||
);
|
||||
} catch (error) {
|
||||
if (error instanceof Error) {
|
||||
logger.warn(
|
||||
"Signup: Error creating user",
|
||||
error.message,
|
||||
body.email.toLowerCase(),
|
||||
body.name,
|
||||
error,
|
||||
);
|
||||
res.status(422).json({ message: error.message });
|
||||
}
|
||||
const message =
|
||||
"Signup: Error creating user: " +
|
||||
(error instanceof Error ? error.message : JSON.stringify(error));
|
||||
logger.warn(message, body.email.toLowerCase(), body.name);
|
||||
res.status(422).json({ message: message });
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Button } from "@/src/components/ui/button";
|
||||
import { Button, type ButtonProps } from "@/src/components/ui/button";
|
||||
import { Edit, LockIcon, PlusIcon, Trash } from "lucide-react";
|
||||
import {
|
||||
Dialog,
|
||||
@@ -18,7 +18,8 @@ interface BaseDatasetButtonProps {
|
||||
mode: "create" | "update" | "delete";
|
||||
projectId: string;
|
||||
className?: string;
|
||||
onFormSuccess?: () => void;
|
||||
size?: ButtonProps["size"];
|
||||
variant?: ButtonProps["variant"];
|
||||
}
|
||||
|
||||
interface CreateDatasetButtonProps extends BaseDatasetButtonProps {
|
||||
@@ -59,8 +60,8 @@ export const DatasetActionButton = (props: DatasetActionButtonProps) => {
|
||||
{props.mode === "update" ? (
|
||||
props.icon ? (
|
||||
<Button
|
||||
variant="outline"
|
||||
size={"icon"}
|
||||
variant={props.variant || "outline"}
|
||||
size={props.size || "icon"}
|
||||
className={props.className}
|
||||
disabled={!hasAccess}
|
||||
onClick={() =>
|
||||
@@ -73,7 +74,9 @@ export const DatasetActionButton = (props: DatasetActionButtonProps) => {
|
||||
</Button>
|
||||
) : (
|
||||
<Button
|
||||
variant="ghost"
|
||||
variant={props.variant || "ghost"}
|
||||
size={props.size || "icon"}
|
||||
className={props.className}
|
||||
onClick={() => {
|
||||
setOpen(true);
|
||||
capture("datasets:update_form_open", {
|
||||
@@ -91,7 +94,9 @@ export const DatasetActionButton = (props: DatasetActionButtonProps) => {
|
||||
)
|
||||
) : props.mode === "delete" ? (
|
||||
<Button
|
||||
variant="ghost"
|
||||
variant={props.variant || "ghost"}
|
||||
size={props.size}
|
||||
className={props.className}
|
||||
onClick={() => {
|
||||
setOpen(true);
|
||||
capture("datasets:delete_form_open", {
|
||||
@@ -104,10 +109,11 @@ export const DatasetActionButton = (props: DatasetActionButtonProps) => {
|
||||
</Button>
|
||||
) : (
|
||||
<Button
|
||||
size={props.size}
|
||||
className={props.className}
|
||||
disabled={!hasAccess}
|
||||
onClick={() => capture("datasets:new_form_open")}
|
||||
variant="secondary"
|
||||
variant={props.variant || "secondary"}
|
||||
>
|
||||
{hasAccess ? (
|
||||
<PlusIcon className="-ml-0.5 mr-1.5 h-4 w-4" aria-hidden="true" />
|
||||
|
||||
@@ -31,6 +31,23 @@ const formatDatasetItemData = (data: string | null | undefined) => {
|
||||
};
|
||||
|
||||
export const datasetRouter = createTRPCRouter({
|
||||
hasAny: protectedProjectProcedure
|
||||
.input(
|
||||
z.object({
|
||||
projectId: z.string(),
|
||||
}),
|
||||
)
|
||||
.query(async ({ input, ctx }) => {
|
||||
const dataset = await ctx.prisma.dataset.findFirst({
|
||||
where: {
|
||||
projectId: input.projectId,
|
||||
},
|
||||
select: { id: true },
|
||||
take: 1,
|
||||
});
|
||||
|
||||
return dataset !== null;
|
||||
}),
|
||||
allDatasetMeta: protectedProjectProcedure
|
||||
.input(z.object({ projectId: z.string() }))
|
||||
.query(async ({ input, ctx }) => {
|
||||
|
||||
@@ -495,21 +495,32 @@ export const getRunItemsByRunIdOrItemId = async (
|
||||
projectId: string,
|
||||
runItems: DatasetRunItems[],
|
||||
) => {
|
||||
const minTimestamp = runItems
|
||||
.map((ri) => ri.createdAt)
|
||||
.sort((a, b) => a.getTime() - b.getTime())
|
||||
.shift();
|
||||
// We assume that all events started at most 24h before the earliest run item.
|
||||
const filterTimestamp = minTimestamp
|
||||
? new Date(minTimestamp.getTime() - 24 * 60 * 60 * 1000)
|
||||
: undefined;
|
||||
const [traceScores, observationAggregates, traceAggregate] =
|
||||
await Promise.all([
|
||||
getScoresForTraces({
|
||||
projectId,
|
||||
traceIds: runItems.map((ri) => ri.traceId),
|
||||
timestamp: filterTimestamp,
|
||||
}),
|
||||
getLatencyAndTotalCostForObservations(
|
||||
projectId,
|
||||
runItems
|
||||
.filter((ri) => ri.observationId !== null)
|
||||
.map((ri) => ri.observationId) as string[],
|
||||
filterTimestamp,
|
||||
),
|
||||
getLatencyAndTotalCostForObservationsByTraces(
|
||||
projectId,
|
||||
runItems.map((ri) => ri.traceId),
|
||||
filterTimestamp,
|
||||
),
|
||||
]);
|
||||
|
||||
|
||||
@@ -128,7 +128,21 @@ const extractInputAndOutput = (
|
||||
event.name === "gen_ai.content.completion",
|
||||
)?.attributes;
|
||||
if (input || output) {
|
||||
return { input, output };
|
||||
input =
|
||||
input?.reduce((acc: any, attr: any) => {
|
||||
acc[attr.key] = convertValueToPlainJavascript(attr.value);
|
||||
return acc;
|
||||
}, {}) ?? {};
|
||||
output =
|
||||
output?.reduce((acc: any, attr: any) => {
|
||||
acc[attr.key] = convertValueToPlainJavascript(attr.value);
|
||||
return acc;
|
||||
}, {}) ?? {};
|
||||
// Here, we are interested in the attributes of the event. Usually gen_ai.prompt and gen_ai.completion.
|
||||
// We can use the current function again to extract them from the event attributes.
|
||||
const { input: eventInput } = extractInputAndOutput([], input);
|
||||
const { output: eventOutput } = extractInputAndOutput([], output);
|
||||
return { input: eventInput || input, output: eventOutput || output };
|
||||
}
|
||||
|
||||
// MLFlow sets mlflow.spanInputs and mlflow.spanOutputs
|
||||
@@ -177,6 +191,26 @@ const extractInputAndOutput = (
|
||||
return { input: null, output: null };
|
||||
};
|
||||
|
||||
const extractEnvironment = (
|
||||
attributes: Record<string, unknown>,
|
||||
resourceAttributes: Record<string, unknown>,
|
||||
): string => {
|
||||
const environmentAttributeKeys = [
|
||||
"langfuse.environment",
|
||||
"deployment.environment.name",
|
||||
"deployment.environment",
|
||||
];
|
||||
for (const key of environmentAttributeKeys) {
|
||||
if (resourceAttributes[key]) {
|
||||
return resourceAttributes[key] as string;
|
||||
}
|
||||
if (attributes[key]) {
|
||||
return attributes[key] as string;
|
||||
}
|
||||
}
|
||||
return "default";
|
||||
};
|
||||
|
||||
const extractUserId = (
|
||||
attributes: Record<string, unknown>,
|
||||
): string | undefined => {
|
||||
@@ -266,7 +300,11 @@ const extractUsageDetails = (
|
||||
.replace("llm.token_count.", "");
|
||||
const mappedUsageDetailKey =
|
||||
usageDetailKeyMapping[usageDetailKey] ?? usageDetailKey;
|
||||
acc[mappedUsageDetailKey] = attributes[key];
|
||||
// Cast the respective key to a number
|
||||
const value = Number(attributes[key]);
|
||||
if (!Number.isNaN(value)) {
|
||||
acc[mappedUsageDetailKey] = value;
|
||||
}
|
||||
return acc;
|
||||
}, {});
|
||||
};
|
||||
@@ -305,7 +343,13 @@ export const convertOtelSpanToIngestionEvent = (
|
||||
return acc;
|
||||
}, {}) ?? {};
|
||||
|
||||
if (!span?.parentSpanId) {
|
||||
const parentObservationId = span?.parentSpanId
|
||||
? Buffer.from(span.parentSpanId?.data ?? span.parentSpanId).toString(
|
||||
"hex",
|
||||
)
|
||||
: null;
|
||||
|
||||
if (!parentObservationId) {
|
||||
// Create a trace for any root span
|
||||
const trace = {
|
||||
id: Buffer.from(span.traceId?.data ?? span.traceId).toString("hex"),
|
||||
@@ -328,6 +372,8 @@ export const convertOtelSpanToIngestionEvent = (
|
||||
attributes?.["langfuse.public"] === "true",
|
||||
tags: attributes?.["langfuse.tags"] ?? [],
|
||||
|
||||
environment: extractEnvironment(attributes, resourceAttributes),
|
||||
|
||||
// Input and Output
|
||||
...extractInputAndOutput(span?.events ?? [], attributes),
|
||||
};
|
||||
@@ -345,15 +391,13 @@ export const convertOtelSpanToIngestionEvent = (
|
||||
traceId: Buffer.from(span.traceId?.data ?? span.traceId).toString(
|
||||
"hex",
|
||||
),
|
||||
parentObservationId: span?.parentSpanId
|
||||
? Buffer.from(span.parentSpanId?.data ?? span.parentSpanId).toString(
|
||||
"hex",
|
||||
)
|
||||
: null,
|
||||
parentObservationId,
|
||||
name: span.name,
|
||||
startTime: convertNanoTimestampToISO(span.startTimeUnixNano),
|
||||
endTime: convertNanoTimestampToISO(span.endTimeUnixNano),
|
||||
|
||||
environment: extractEnvironment(attributes, resourceAttributes),
|
||||
|
||||
// Additional fields
|
||||
metadata: {
|
||||
attributes,
|
||||
|
||||
@@ -125,18 +125,17 @@ export function SetPromptVersionLabels({
|
||||
isLive={label === PRODUCTION_LABEL}
|
||||
/>
|
||||
))}
|
||||
{promptLabels.length === 0 && (
|
||||
<Button
|
||||
variant="outline"
|
||||
className={cn(
|
||||
"h-6 w-6 bg-muted-gray text-primary",
|
||||
showOnlyOnHover && "opacity-0 group-hover:opacity-100",
|
||||
!hasAccess && "cursor-not-allowed group-hover:opacity-50",
|
||||
)}
|
||||
>
|
||||
<CircleFadingArrowUp className="h-3.5 w-3.5 shrink-0" />
|
||||
</Button>
|
||||
)}
|
||||
<Button
|
||||
variant="outline"
|
||||
title="Add prompt version label"
|
||||
className={cn(
|
||||
"h-6 w-6 bg-muted-gray text-primary",
|
||||
showOnlyOnHover && "opacity-0 group-hover:opacity-100",
|
||||
!hasAccess && "cursor-not-allowed group-hover:opacity-50",
|
||||
)}
|
||||
>
|
||||
<CircleFadingArrowUp className="h-3.5 w-3.5 shrink-0" />
|
||||
</Button>
|
||||
</div>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent
|
||||
|
||||
@@ -377,7 +377,7 @@ export const PromptDetail = () => {
|
||||
</span>
|
||||
</Button>
|
||||
</DialogTrigger>
|
||||
<DialogContent>
|
||||
<DialogContent className="max-h-[90vh] overflow-y-auto">
|
||||
<CreateExperimentsForm
|
||||
key={`create-experiment-form-${prompt.id}`}
|
||||
projectId={projectId as string}
|
||||
@@ -455,10 +455,12 @@ export const PromptDetail = () => {
|
||||
>
|
||||
<div className="mb-2 flex max-h-full min-h-0 w-full flex-col gap-2 overflow-y-auto">
|
||||
{prompt.type === PromptType.Chat && chatMessages ? (
|
||||
<OpenAiMessageView
|
||||
messages={chatMessages}
|
||||
collapseLongHistory={false}
|
||||
/>
|
||||
<div className="w-full">
|
||||
<OpenAiMessageView
|
||||
messages={chatMessages}
|
||||
collapseLongHistory={false}
|
||||
/>
|
||||
</div>
|
||||
) : typeof prompt.prompt === "string" ? (
|
||||
<CodeView content={prompt.prompt} title="Text Prompt" />
|
||||
) : (
|
||||
|
||||
@@ -36,6 +36,29 @@ const PromptFilterOptions = z.object({
|
||||
});
|
||||
|
||||
export const promptRouter = createTRPCRouter({
|
||||
hasAny: protectedProjectProcedure
|
||||
.input(
|
||||
z.object({
|
||||
projectId: z.string(),
|
||||
}),
|
||||
)
|
||||
.query(async ({ input, ctx }) => {
|
||||
throwIfNoProjectAccess({
|
||||
session: ctx.session,
|
||||
projectId: input.projectId,
|
||||
scope: "prompts:read",
|
||||
});
|
||||
|
||||
const prompt = await ctx.prisma.prompt.findFirst({
|
||||
where: {
|
||||
projectId: input.projectId,
|
||||
},
|
||||
select: { id: true },
|
||||
take: 1,
|
||||
});
|
||||
|
||||
return prompt !== null;
|
||||
}),
|
||||
all: protectedProjectProcedure
|
||||
.input(PromptFilterOptions)
|
||||
.query(async ({ input, ctx }) => {
|
||||
|
||||
@@ -531,7 +531,7 @@ export function CreateLLMApiKeyForm({
|
||||
{currentAdapter === LLMAdapter.Bedrock && (
|
||||
<FormDescription className="text-dark-yellow">
|
||||
{
|
||||
"For Bedrock, the model name is the Bedrock model ID, e.g. 'eu.anthropic.claude-3-5-sonnet-20240620-v1:0'"
|
||||
"For Bedrock, the model name is the Bedrock Inference Profile ID, e.g. 'eu.anthropic.claude-3-5-sonnet-20240620-v1:0'"
|
||||
}
|
||||
</FormDescription>
|
||||
)}
|
||||
|
||||
@@ -55,6 +55,7 @@ export const generateObservationsForPublicApi = async (props: QueryType) => {
|
||||
project_id,
|
||||
type,
|
||||
parent_observation_id,
|
||||
environment,
|
||||
start_time,
|
||||
end_time,
|
||||
name,
|
||||
|
||||
@@ -41,6 +41,7 @@ export const generateScoresForPublicApi = async (props: ScoreQueryType) => {
|
||||
s.id as id,
|
||||
s.project_id as project_id,
|
||||
s.timestamp as timestamp,
|
||||
s.environment as environment,
|
||||
s.name as name,
|
||||
s.value as value,
|
||||
s.string_value as string_value,
|
||||
|
||||
@@ -93,6 +93,7 @@ export const generateTracesForPublicApi = async (
|
||||
t.project_id as project_id,
|
||||
t.timestamp as timestamp,
|
||||
t.name as name,
|
||||
t.environment as environment,
|
||||
t.input as input,
|
||||
t.output as output,
|
||||
t.session_id as session_id,
|
||||
|
||||
@@ -21,6 +21,7 @@ export const APIObservation = z
|
||||
parentObservationId: z.string().nullable(),
|
||||
name: z.string().nullable(),
|
||||
type: ObservationType,
|
||||
environment: z.string().default("default"),
|
||||
startTime: z.coerce.date(),
|
||||
endTime: z.coerce.date().nullable(),
|
||||
version: z.string().nullable(),
|
||||
|
||||
@@ -23,6 +23,7 @@ export const APITrace = z
|
||||
release: z.string().nullable(),
|
||||
version: z.string().nullable(),
|
||||
projectId: z.string(),
|
||||
environment: z.string().default("default"),
|
||||
public: z.boolean(),
|
||||
bookmarked: z.boolean(),
|
||||
tags: z.array(z.string()),
|
||||
|
||||
@@ -1,11 +1,28 @@
|
||||
import { useRouter } from "next/router";
|
||||
import { DatasetsTable } from "@/src/features/datasets/components/DatasetsTable";
|
||||
import Page from "@/src/components/layouts/page";
|
||||
import { api } from "@/src/utils/api";
|
||||
import { DatasetsOnboarding } from "@/src/components/onboarding/DatasetsOnboarding";
|
||||
|
||||
export default function Traces() {
|
||||
export default function Datasets() {
|
||||
const router = useRouter();
|
||||
const projectId = router.query.projectId as string;
|
||||
|
||||
// Check if the project has any datasets
|
||||
const { data: hasAnyDataset, isLoading } = api.datasets.hasAny.useQuery(
|
||||
{ projectId },
|
||||
{
|
||||
enabled: !!projectId,
|
||||
trpc: {
|
||||
context: {
|
||||
skipBatch: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
const showOnboarding = !isLoading && !hasAnyDataset;
|
||||
|
||||
return (
|
||||
<Page
|
||||
headerProps={{
|
||||
@@ -16,8 +33,14 @@ export default function Traces() {
|
||||
href: "https://langfuse.com/docs/datasets",
|
||||
},
|
||||
}}
|
||||
scrollable={showOnboarding}
|
||||
>
|
||||
<DatasetsTable projectId={projectId} />
|
||||
{/* Show onboarding screen if project has no datasets */}
|
||||
{showOnboarding ? (
|
||||
<DatasetsOnboarding projectId={projectId} />
|
||||
) : (
|
||||
<DatasetsTable projectId={projectId} />
|
||||
)}
|
||||
</Page>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -30,7 +30,7 @@ export default function LogPage() {
|
||||
return (
|
||||
<Page
|
||||
headerProps={{
|
||||
title: "Eval Log",
|
||||
title: "Evaluators",
|
||||
help: {
|
||||
description: "View of all running evals.",
|
||||
href: "https://langfuse.com/docs/scores/model-based-evals",
|
||||
|
||||
@@ -147,7 +147,7 @@ export default function Dashboard() {
|
||||
scrollable
|
||||
headerProps={{
|
||||
title: "Dashboard",
|
||||
actionButtonsLeft: <SetupTracingButton />,
|
||||
actionButtonsRight: <SetupTracingButton />,
|
||||
}}
|
||||
>
|
||||
<div className="my-3 flex flex-wrap items-center justify-between gap-2">
|
||||
|
||||
@@ -1,11 +1,29 @@
|
||||
import { useRouter } from "next/router";
|
||||
import ObservationsTable from "@/src/components/table/use-cases/observations";
|
||||
import Page from "@/src/components/layouts/page";
|
||||
import { api } from "@/src/utils/api";
|
||||
import { TracesOnboarding } from "@/src/components/onboarding/TracesOnboarding";
|
||||
|
||||
export default function Generations() {
|
||||
const router = useRouter();
|
||||
const projectId = router.query.projectId as string;
|
||||
|
||||
// Check if the user has any traces
|
||||
const { data: hasAnyTrace, isLoading } = api.traces.hasAny.useQuery(
|
||||
{ projectId },
|
||||
{
|
||||
enabled: !!projectId,
|
||||
trpc: {
|
||||
context: {
|
||||
skipBatch: true,
|
||||
},
|
||||
},
|
||||
refetchInterval: 10_000,
|
||||
},
|
||||
);
|
||||
|
||||
const showOnboarding = !isLoading && !hasAnyTrace;
|
||||
|
||||
return (
|
||||
<Page
|
||||
headerProps={{
|
||||
@@ -16,8 +34,14 @@ export default function Generations() {
|
||||
href: "https://langfuse.com/docs/tracing",
|
||||
},
|
||||
}}
|
||||
scrollable={showOnboarding}
|
||||
>
|
||||
<ObservationsTable projectId={projectId} />
|
||||
{/* Show onboarding screen if user has no traces */}
|
||||
{showOnboarding ? (
|
||||
<TracesOnboarding projectId={projectId} />
|
||||
) : (
|
||||
<ObservationsTable projectId={projectId} />
|
||||
)}
|
||||
</Page>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,7 +1,28 @@
|
||||
import { useRouter } from "next/router";
|
||||
import Page from "@/src/components/layouts/page";
|
||||
import { PromptTable } from "@/src/features/prompts/components/prompts-table";
|
||||
import { api } from "@/src/utils/api";
|
||||
import { PromptsOnboarding } from "@/src/components/onboarding/PromptsOnboarding";
|
||||
|
||||
export default function Prompts() {
|
||||
const router = useRouter();
|
||||
const projectId = router.query.projectId as string;
|
||||
|
||||
// Check if the project has any prompts
|
||||
const { data: hasAnyPrompt, isLoading } = api.prompts.hasAny.useQuery(
|
||||
{ projectId },
|
||||
{
|
||||
enabled: !!projectId,
|
||||
trpc: {
|
||||
context: {
|
||||
skipBatch: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
const showOnboarding = !isLoading && !hasAnyPrompt;
|
||||
|
||||
return (
|
||||
<Page
|
||||
headerProps={{
|
||||
@@ -12,8 +33,14 @@ export default function Prompts() {
|
||||
href: "https://langfuse.com/docs/prompts",
|
||||
},
|
||||
}}
|
||||
scrollable={showOnboarding}
|
||||
>
|
||||
<PromptTable />
|
||||
{/* Show onboarding screen if project has no prompts */}
|
||||
{showOnboarding ? (
|
||||
<PromptsOnboarding projectId={projectId} />
|
||||
) : (
|
||||
<PromptTable />
|
||||
)}
|
||||
</Page>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,11 +1,29 @@
|
||||
import { useRouter } from "next/router";
|
||||
import ScoresTable from "@/src/components/table/use-cases/scores";
|
||||
import Page from "@/src/components/layouts/page";
|
||||
import { api } from "@/src/utils/api";
|
||||
import { ScoresOnboarding } from "@/src/components/onboarding/ScoresOnboarding";
|
||||
|
||||
export default function ScoresPage() {
|
||||
const router = useRouter();
|
||||
const projectId = router.query.projectId as string;
|
||||
|
||||
// Check if the user has any scores
|
||||
const { data: hasAnyScore, isLoading } = api.scores.hasAny.useQuery(
|
||||
{ projectId },
|
||||
{
|
||||
enabled: !!projectId,
|
||||
trpc: {
|
||||
context: {
|
||||
skipBatch: true,
|
||||
},
|
||||
},
|
||||
refetchInterval: 10_000,
|
||||
},
|
||||
);
|
||||
|
||||
const showOnboarding = !isLoading && !hasAnyScore;
|
||||
|
||||
return (
|
||||
<Page
|
||||
headerProps={{
|
||||
@@ -16,8 +34,14 @@ export default function ScoresPage() {
|
||||
href: "https://langfuse.com/docs/scores",
|
||||
},
|
||||
}}
|
||||
scrollable={showOnboarding}
|
||||
>
|
||||
<ScoresTable projectId={projectId} />
|
||||
{/* Show onboarding screen if user has no scores */}
|
||||
{showOnboarding ? (
|
||||
<ScoresOnboarding />
|
||||
) : (
|
||||
<ScoresTable projectId={projectId} />
|
||||
)}
|
||||
</Page>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,11 +1,28 @@
|
||||
import { useRouter } from "next/router";
|
||||
import SessionsTable from "@/src/components/table/use-cases/sessions";
|
||||
import Page from "@/src/components/layouts/page";
|
||||
import { SessionsOnboarding } from "@/src/components/onboarding/SessionsOnboarding";
|
||||
import { api } from "@/src/utils/api";
|
||||
|
||||
export default function Sessions() {
|
||||
const router = useRouter();
|
||||
const projectId = router.query.projectId as string;
|
||||
|
||||
const { data: hasAnySession, isLoading } = api.sessions.hasAny.useQuery(
|
||||
{ projectId },
|
||||
{
|
||||
enabled: !!projectId,
|
||||
trpc: {
|
||||
context: {
|
||||
skipBatch: true,
|
||||
},
|
||||
},
|
||||
refetchInterval: 10_000,
|
||||
},
|
||||
);
|
||||
|
||||
const showOnboarding = !isLoading && !hasAnySession;
|
||||
|
||||
return (
|
||||
<Page
|
||||
headerProps={{
|
||||
@@ -16,8 +33,14 @@ export default function Sessions() {
|
||||
href: "https://langfuse.com/docs/sessions",
|
||||
},
|
||||
}}
|
||||
scrollable={showOnboarding}
|
||||
>
|
||||
<SessionsTable projectId={projectId} />
|
||||
{/* Show onboarding screen if user has no sessions */}
|
||||
{showOnboarding ? (
|
||||
<SessionsOnboarding />
|
||||
) : (
|
||||
<SessionsTable projectId={projectId} />
|
||||
)}
|
||||
</Page>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -24,14 +24,13 @@ import { BatchExportsSettingsPage } from "@/src/features/batch-exports/component
|
||||
import { AuditLogsSettingsPage } from "@/src/ee/features/audit-log-viewer/AuditLogsSettingsPage";
|
||||
import { ModelsSettings } from "@/src/features/models/components/ModelSettings";
|
||||
import ConfigureRetention from "@/src/features/projects/components/ConfigureRetention";
|
||||
import { env } from "@/src/env.mjs";
|
||||
import ContainerPage from "@/src/components/layouts/container-page";
|
||||
|
||||
export default function SettingsPage() {
|
||||
const { project, organization } = useQueryProject();
|
||||
const router = useRouter();
|
||||
const showBillingSettings = useHasEntitlement("cloud-billing");
|
||||
const isLangfuseCloud = Boolean(env.NEXT_PUBLIC_LANGFUSE_CLOUD_REGION);
|
||||
const showRetentionSettings = useHasEntitlement("data-retention");
|
||||
if (!project || !organization) return null;
|
||||
return (
|
||||
<ContainerPage
|
||||
@@ -49,7 +48,7 @@ export default function SettingsPage() {
|
||||
<div className="flex flex-col gap-6">
|
||||
<HostNameProject />
|
||||
<RenameProject />
|
||||
{isLangfuseCloud && <ConfigureRetention />}
|
||||
{showRetentionSettings && <ConfigureRetention />}
|
||||
<div>
|
||||
<Header title="Debug Information" />
|
||||
<JSONView
|
||||
|
||||
@@ -1,12 +1,29 @@
|
||||
import { useRouter } from "next/router";
|
||||
import TracesTable from "@/src/components/table/use-cases/traces";
|
||||
import SetupTracingButton from "@/src/features/setup/components/SetupTracingButton";
|
||||
import Page from "@/src/components/layouts/page";
|
||||
import { api } from "@/src/utils/api";
|
||||
import { TracesOnboarding } from "@/src/components/onboarding/TracesOnboarding";
|
||||
|
||||
export default function Traces() {
|
||||
const router = useRouter();
|
||||
const projectId = router.query.projectId as string;
|
||||
|
||||
// Check if the user has any traces
|
||||
const { data: hasAnyTrace, isLoading } = api.traces.hasAny.useQuery(
|
||||
{ projectId },
|
||||
{
|
||||
enabled: !!projectId,
|
||||
trpc: {
|
||||
context: {
|
||||
skipBatch: true,
|
||||
},
|
||||
},
|
||||
refetchInterval: 10_000,
|
||||
},
|
||||
);
|
||||
|
||||
const showOnboarding = !isLoading && !hasAnyTrace;
|
||||
|
||||
return (
|
||||
<Page
|
||||
headerProps={{
|
||||
@@ -16,10 +33,15 @@ export default function Traces() {
|
||||
"A trace represents a single function/api invocation. Traces contain observations. See docs to learn more.",
|
||||
href: "https://langfuse.com/docs/tracing",
|
||||
},
|
||||
actionButtonsRight: <SetupTracingButton />,
|
||||
}}
|
||||
scrollable={showOnboarding}
|
||||
>
|
||||
<TracesTable projectId={projectId} />
|
||||
{/* Show onboarding screen if user has no traces */}
|
||||
{showOnboarding ? (
|
||||
<TracesOnboarding projectId={projectId} />
|
||||
) : (
|
||||
<TracesTable projectId={projectId} />
|
||||
)}
|
||||
</Page>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -23,6 +23,7 @@ import { joinTableCoreAndMetrics } from "@/src/components/table/utils/joinTableC
|
||||
import { useTableDateRange } from "@/src/hooks/useTableDateRange";
|
||||
import { useDebounce } from "@/src/hooks/useDebounce";
|
||||
import Page from "@/src/components/layouts/page";
|
||||
import { UsersOnboarding } from "@/src/components/onboarding/UsersOnboarding";
|
||||
|
||||
type RowData = {
|
||||
userId: string;
|
||||
@@ -37,6 +38,44 @@ export default function UsersPage() {
|
||||
const router = useRouter();
|
||||
const projectId = router.query.projectId as string;
|
||||
|
||||
// Check if the user has any users
|
||||
const { data: hasAnyUser, isLoading } = api.users.hasAny.useQuery(
|
||||
{ projectId },
|
||||
{
|
||||
enabled: !!projectId,
|
||||
trpc: {
|
||||
context: {
|
||||
skipBatch: true,
|
||||
},
|
||||
},
|
||||
refetchInterval: 10_000,
|
||||
},
|
||||
);
|
||||
|
||||
const showOnboarding = !isLoading && !hasAnyUser;
|
||||
|
||||
return (
|
||||
<Page
|
||||
headerProps={{
|
||||
title: "Users",
|
||||
help: {
|
||||
description:
|
||||
"Attribute data in Langfuse to a user by adding a userId to your traces. See docs to learn more.",
|
||||
href: "https://langfuse.com/docs/user-explorer",
|
||||
},
|
||||
}}
|
||||
scrollable={showOnboarding}
|
||||
>
|
||||
{/* Show onboarding screen if user has no users */}
|
||||
{showOnboarding ? <UsersOnboarding /> : <UsersTable />}
|
||||
</Page>
|
||||
);
|
||||
}
|
||||
|
||||
const UsersTable = () => {
|
||||
const router = useRouter();
|
||||
const projectId = router.query.projectId as string;
|
||||
|
||||
const [userFilterState, setUserFilterState] = useQueryFilterState(
|
||||
[],
|
||||
"users",
|
||||
@@ -244,16 +283,7 @@ export default function UsersPage() {
|
||||
];
|
||||
|
||||
return (
|
||||
<Page
|
||||
headerProps={{
|
||||
title: "Users",
|
||||
help: {
|
||||
description:
|
||||
"Attribute data in Langfuse to a user by adding a userId to your traces. See docs to learn more.",
|
||||
href: "https://langfuse.com/docs/user-explorer",
|
||||
},
|
||||
}}
|
||||
>
|
||||
<>
|
||||
<DataTableToolbar
|
||||
filterColumnDefinition={usersTableCols}
|
||||
filterState={userFilterState}
|
||||
@@ -308,6 +338,6 @@ export default function UsersPage() {
|
||||
state: paginationState,
|
||||
}}
|
||||
/>
|
||||
</Page>
|
||||
</>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -58,7 +58,8 @@ export default function UserPage() {
|
||||
title: userId,
|
||||
breadcrumb: [{ name: "Users", href: `/project/${projectId}/users` }],
|
||||
itemType: "USER",
|
||||
actionButtonsLeft: (
|
||||
|
||||
actionButtonsRight: (
|
||||
<>
|
||||
<ActionButton
|
||||
href={`/project/${projectId}?filter=user%3Bstring%3B%3B%3D%3B${userId}`} // dashboard filter serialization
|
||||
@@ -67,17 +68,15 @@ export default function UserPage() {
|
||||
>
|
||||
Dashboard
|
||||
</ActionButton>
|
||||
<DetailPageNav
|
||||
currentId={encodeURIComponent(userId)}
|
||||
path={(entry) =>
|
||||
`/project/${projectId}/users/${encodeURIComponent(entry.id)}`
|
||||
}
|
||||
listKey="users"
|
||||
/>
|
||||
</>
|
||||
),
|
||||
actionButtonsRight: (
|
||||
<DetailPageNav
|
||||
currentId={encodeURIComponent(userId)}
|
||||
path={(entry) =>
|
||||
`/project/${projectId}/users/${encodeURIComponent(entry.id)}`
|
||||
}
|
||||
listKey="users"
|
||||
/>
|
||||
),
|
||||
}}
|
||||
>
|
||||
<>
|
||||
|
||||
@@ -38,8 +38,8 @@ import {
|
||||
getScoreById,
|
||||
convertDateToClickhouseDateTime,
|
||||
searchExistingAnnotationScore,
|
||||
hasAnyScore,
|
||||
} from "@langfuse/shared/src/server";
|
||||
import { env } from "@/src/env.mjs";
|
||||
import { v4 } from "uuid";
|
||||
|
||||
const ScoreFilterOptions = z.object({
|
||||
@@ -166,9 +166,41 @@ export const scoresRouter = createTRPCRouter({
|
||||
scope: "scores:CUD",
|
||||
});
|
||||
|
||||
const clickhouseTrace = await getTraceById(
|
||||
input.traceId,
|
||||
input.projectId,
|
||||
);
|
||||
|
||||
if (!clickhouseTrace) {
|
||||
logger.error(
|
||||
`No trace with id ${input.traceId} in project ${input.projectId} in Clickhouse`,
|
||||
);
|
||||
throw new LangfuseNotFoundError(
|
||||
`No trace with id ${input.traceId} in project ${input.projectId} in Clickhouse`,
|
||||
);
|
||||
}
|
||||
|
||||
const clickhouseScore = await searchExistingAnnotationScore(
|
||||
input.projectId,
|
||||
input.traceId,
|
||||
input.observationId ?? null,
|
||||
input.name,
|
||||
input.configId,
|
||||
);
|
||||
|
||||
if (clickhouseScore) {
|
||||
logger.error(
|
||||
`Score for name ${input.name} already exists for trace ${input.traceId} in project ${input.projectId}`,
|
||||
);
|
||||
throw new InvalidRequestError(
|
||||
`Score for name ${input.name} already exists for trace ${input.traceId} in project ${input.projectId}`,
|
||||
);
|
||||
}
|
||||
|
||||
const score = {
|
||||
id: v4(),
|
||||
projectId: input.projectId,
|
||||
environment: input.environment ?? "default",
|
||||
traceId: input.traceId,
|
||||
observationId: input.observationId ?? null,
|
||||
value: input.value ?? null,
|
||||
@@ -185,57 +217,23 @@ export const scoresRouter = createTRPCRouter({
|
||||
timestamp: new Date(),
|
||||
};
|
||||
|
||||
const hasClickhouseConfigured = env.CLICKHOUSE_URL;
|
||||
|
||||
if (hasClickhouseConfigured) {
|
||||
const clickhouseTrace = await getTraceById(
|
||||
input.traceId,
|
||||
input.projectId,
|
||||
);
|
||||
|
||||
if (!clickhouseTrace) {
|
||||
logger.error(
|
||||
`No trace with id ${input.traceId} in project ${input.projectId} in Clickhouse`,
|
||||
);
|
||||
throw new LangfuseNotFoundError(
|
||||
`No trace with id ${input.traceId} in project ${input.projectId} in Clickhouse`,
|
||||
);
|
||||
}
|
||||
|
||||
const clickhouseScore = await searchExistingAnnotationScore(
|
||||
input.projectId,
|
||||
input.traceId,
|
||||
input.observationId ?? null,
|
||||
input.name,
|
||||
input.configId,
|
||||
);
|
||||
|
||||
if (clickhouseScore) {
|
||||
logger.error(
|
||||
`Score for name ${input.name} already exists for trace ${input.traceId} in project ${input.projectId}`,
|
||||
);
|
||||
throw new InvalidRequestError(
|
||||
`Score for name ${input.name} already exists for trace ${input.traceId} in project ${input.projectId}`,
|
||||
);
|
||||
}
|
||||
|
||||
await upsertScore({
|
||||
id: score.id, // Reuse ID that was generated by Prisma
|
||||
timestamp: convertDateToClickhouseDateTime(new Date()),
|
||||
project_id: input.projectId,
|
||||
trace_id: input.traceId,
|
||||
observation_id: input.observationId,
|
||||
name: input.name,
|
||||
value: input.value !== null ? input.value : undefined,
|
||||
source: ScoreSource.ANNOTATION,
|
||||
comment: input.comment,
|
||||
author_user_id: ctx.session.user.id,
|
||||
config_id: input.configId,
|
||||
data_type: input.dataType,
|
||||
string_value: input.stringValue,
|
||||
queue_id: input.queueId,
|
||||
});
|
||||
}
|
||||
await upsertScore({
|
||||
id: score.id, // Reuse ID that was generated by Prisma
|
||||
timestamp: convertDateToClickhouseDateTime(new Date()),
|
||||
project_id: input.projectId,
|
||||
environment: input.environment ?? "default",
|
||||
trace_id: input.traceId,
|
||||
observation_id: input.observationId,
|
||||
name: input.name,
|
||||
value: input.value !== null ? input.value : undefined,
|
||||
source: ScoreSource.ANNOTATION,
|
||||
comment: input.comment,
|
||||
author_user_id: ctx.session.user.id,
|
||||
config_id: input.configId,
|
||||
data_type: input.dataType,
|
||||
string_value: input.stringValue,
|
||||
queue_id: input.queueId,
|
||||
});
|
||||
|
||||
await auditLog({
|
||||
session: ctx.session,
|
||||
@@ -379,4 +377,13 @@ export const scoresRouter = createTRPCRouter({
|
||||
dataType: dataType,
|
||||
}));
|
||||
}),
|
||||
hasAny: protectedProjectProcedure
|
||||
.input(
|
||||
z.object({
|
||||
projectId: z.string(),
|
||||
}),
|
||||
)
|
||||
.query(async ({ input }) => {
|
||||
return await hasAnyScore(input.projectId);
|
||||
}),
|
||||
});
|
||||
|
||||
@@ -30,6 +30,7 @@ import {
|
||||
getPublicSessionsFilter,
|
||||
logger,
|
||||
getSessionsWithMetrics,
|
||||
hasAnySession,
|
||||
} from "@langfuse/shared/src/server";
|
||||
import { chunk } from "lodash";
|
||||
|
||||
@@ -41,6 +42,15 @@ const SessionFilterOptions = z.object({
|
||||
});
|
||||
|
||||
export const sessionRouter = createTRPCRouter({
|
||||
hasAny: protectedProjectProcedure
|
||||
.input(
|
||||
z.object({
|
||||
projectId: z.string(),
|
||||
}),
|
||||
)
|
||||
.query(async ({ input }) => {
|
||||
return await hasAnySession(input.projectId);
|
||||
}),
|
||||
all: protectedProjectProcedure
|
||||
.input(SessionFilterOptions)
|
||||
.query(async ({ input, ctx }) => {
|
||||
|
||||
@@ -9,6 +9,7 @@ import {
|
||||
getTotalUserCount,
|
||||
getTracesGroupedByUsers,
|
||||
getUserMetrics,
|
||||
hasAnyUser,
|
||||
} from "@langfuse/shared/src/server";
|
||||
|
||||
const UserFilterOptions = z.object({
|
||||
@@ -25,6 +26,16 @@ const UserAllOptions = UserFilterOptions.extend({
|
||||
});
|
||||
|
||||
export const userRouter = createTRPCRouter({
|
||||
hasAny: protectedProjectProcedure
|
||||
.input(
|
||||
z.object({
|
||||
projectId: z.string(),
|
||||
}),
|
||||
)
|
||||
.query(async ({ input }) => {
|
||||
return await hasAnyUser(input.projectId);
|
||||
}),
|
||||
|
||||
all: protectedProjectProcedure
|
||||
.input(UserAllOptions)
|
||||
.query(async ({ input, ctx }) => {
|
||||
|
||||
@@ -180,6 +180,10 @@ export const protectedProcedure = withOtelTracingProcedure
|
||||
.use(withErrorHandling)
|
||||
.use(enforceUserIsAuthed);
|
||||
|
||||
export const protectedProcedureWithoutTracing = t.procedure
|
||||
.use(withErrorHandling)
|
||||
.use(enforceUserIsAuthed);
|
||||
|
||||
const inputProjectSchema = z.object({
|
||||
projectId: z.string(),
|
||||
});
|
||||
|
||||
@@ -50,6 +50,7 @@ import {
|
||||
} from "@/src/features/entitlements/server/getPlan";
|
||||
import { projectRoleAccessRights } from "@/src/features/rbac/constants/projectAccessRights";
|
||||
import { hasEntitlementBasedOnPlan } from "@/src/features/entitlements/server/hasEntitlement";
|
||||
import { getSSOBlockedDomains } from "@/src/features/auth-credentials/server/signupApiHandler";
|
||||
|
||||
function canCreateOrganizations(userEmail: string | null): boolean {
|
||||
const instancePlan = getSelfHostedInstancePlanServerSide();
|
||||
@@ -111,8 +112,7 @@ const staticProviders: Provider[] = [
|
||||
}
|
||||
}
|
||||
|
||||
const blockedDomains =
|
||||
env.AUTH_DOMAINS_WITH_SSO_ENFORCEMENT?.split(",") ?? [];
|
||||
const blockedDomains = getSSOBlockedDomains();
|
||||
const domain = credentials.email.split("@")[1]?.toLowerCase();
|
||||
if (domain && blockedDomains.includes(domain)) {
|
||||
throw new Error(
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "worker",
|
||||
"version": "3.30.0",
|
||||
"version": "3.34.1",
|
||||
"description": "",
|
||||
"license": "MIT",
|
||||
"private": true,
|
||||
|
||||
@@ -732,99 +732,101 @@ describe("eval service tests", () => {
|
||||
|
||||
expect(jobs.length).toBe(0);
|
||||
}, 10_000);
|
||||
});
|
||||
|
||||
test("does create eval for trace which is way in the past if timestamp is provided", async () => {
|
||||
const traceId = randomUUID();
|
||||
test("does create eval for trace which is way in the past if timestamp is provided", async () => {
|
||||
const traceId = randomUUID();
|
||||
|
||||
const timestamp = new Date(Date.now() - 1000 * 60 * 60 * 24 * 365 * 1);
|
||||
const trace = createTrace({
|
||||
project_id: "7a88fb47-b4e2-43b8-a06c-a5ce950dc53a",
|
||||
id: traceId,
|
||||
timestamp: timestamp.getTime(),
|
||||
});
|
||||
const timestamp = new Date(Date.now() - 1000 * 60 * 60 * 24 * 365 * 1);
|
||||
const trace = createTrace({
|
||||
project_id: "7a88fb47-b4e2-43b8-a06c-a5ce950dc53a",
|
||||
id: traceId,
|
||||
timestamp: timestamp.getTime(),
|
||||
});
|
||||
|
||||
await createTracesCh([trace]);
|
||||
await createTracesCh([trace]);
|
||||
|
||||
const jobConfiguration = await prisma.jobConfiguration.create({
|
||||
data: {
|
||||
id: randomUUID(),
|
||||
projectId: "7a88fb47-b4e2-43b8-a06c-a5ce950dc53a",
|
||||
filter: JSON.parse("[]"),
|
||||
jobType: "EVAL",
|
||||
delay: 0,
|
||||
sampling: new Decimal("1"),
|
||||
targetObject: "trace",
|
||||
scoreName: "score",
|
||||
variableMapping: JSON.parse("[]"),
|
||||
timeScope: ["NEW"],
|
||||
},
|
||||
});
|
||||
const jobConfiguration = await prisma.jobConfiguration.create({
|
||||
data: {
|
||||
id: randomUUID(),
|
||||
projectId: "7a88fb47-b4e2-43b8-a06c-a5ce950dc53a",
|
||||
filter: JSON.parse("[]"),
|
||||
jobType: "EVAL",
|
||||
delay: 0,
|
||||
sampling: new Decimal("1"),
|
||||
targetObject: "trace",
|
||||
scoreName: "score",
|
||||
variableMapping: JSON.parse("[]"),
|
||||
timeScope: ["NEW"],
|
||||
},
|
||||
});
|
||||
|
||||
const payload = {
|
||||
projectId: "7a88fb47-b4e2-43b8-a06c-a5ce950dc53a",
|
||||
traceId: traceId,
|
||||
configId: jobConfiguration.id,
|
||||
timestamp: timestamp,
|
||||
};
|
||||
|
||||
await createEvalJobs({
|
||||
event: payload,
|
||||
enforcedJobTimeScope: "NEW", // the config must contain NEW
|
||||
});
|
||||
|
||||
const jobs = await kyselyPrisma.$kysely
|
||||
.selectFrom("job_executions")
|
||||
.selectAll()
|
||||
.where("project_id", "=", "7a88fb47-b4e2-43b8-a06c-a5ce950dc53a")
|
||||
.where("job_configuration_id", "in", [jobConfiguration.id])
|
||||
.where("job_input_trace_id", "=", traceId)
|
||||
.execute();
|
||||
|
||||
expect(jobs.length).toBe(1);
|
||||
}, 10_000);
|
||||
|
||||
test("creates eval for trace with timestamp in the future", async () => {
|
||||
const traceId = randomUUID();
|
||||
|
||||
await prisma.jobConfiguration.create({
|
||||
data: {
|
||||
id: randomUUID(),
|
||||
projectId: "7a88fb47-b4e2-43b8-a06c-a5ce950dc53a",
|
||||
filter: JSON.parse("[]"),
|
||||
jobType: "EVAL",
|
||||
delay: 0,
|
||||
sampling: new Decimal("1"),
|
||||
targetObject: "trace",
|
||||
scoreName: "score",
|
||||
variableMapping: JSON.parse("[]"),
|
||||
timeScope: ["NEW"],
|
||||
},
|
||||
});
|
||||
|
||||
const trace = createTrace({
|
||||
project_id: "7a88fb47-b4e2-43b8-a06c-a5ce950dc53a",
|
||||
id: traceId,
|
||||
timestamp: new Date(Date.now() + 1000 * 60 * 60 * 24 * 365 * 1).getTime(),
|
||||
});
|
||||
|
||||
await createTracesCh([trace]);
|
||||
|
||||
await createEvalJobs({
|
||||
event: {
|
||||
const payload = {
|
||||
projectId: "7a88fb47-b4e2-43b8-a06c-a5ce950dc53a",
|
||||
traceId: traceId,
|
||||
},
|
||||
});
|
||||
configId: jobConfiguration.id,
|
||||
timestamp: timestamp,
|
||||
};
|
||||
|
||||
const jobs = await kyselyPrisma.$kysely
|
||||
.selectFrom("job_executions")
|
||||
.selectAll()
|
||||
.where("project_id", "=", "7a88fb47-b4e2-43b8-a06c-a5ce950dc53a")
|
||||
.execute();
|
||||
await createEvalJobs({
|
||||
event: payload,
|
||||
enforcedJobTimeScope: "NEW", // the config must contain NEW
|
||||
});
|
||||
|
||||
expect(jobs.length).toBe(1);
|
||||
}, 10_000);
|
||||
const jobs = await kyselyPrisma.$kysely
|
||||
.selectFrom("job_executions")
|
||||
.selectAll()
|
||||
.where("project_id", "=", "7a88fb47-b4e2-43b8-a06c-a5ce950dc53a")
|
||||
.where("job_configuration_id", "in", [jobConfiguration.id])
|
||||
.where("job_input_trace_id", "=", traceId)
|
||||
.execute();
|
||||
|
||||
expect(jobs.length).toBe(1);
|
||||
}, 10_000);
|
||||
|
||||
test("creates eval for trace with timestamp in the future", async () => {
|
||||
const traceId = randomUUID();
|
||||
|
||||
await prisma.jobConfiguration.create({
|
||||
data: {
|
||||
id: randomUUID(),
|
||||
projectId: "7a88fb47-b4e2-43b8-a06c-a5ce950dc53a",
|
||||
filter: JSON.parse("[]"),
|
||||
jobType: "EVAL",
|
||||
delay: 0,
|
||||
sampling: new Decimal("1"),
|
||||
targetObject: "trace",
|
||||
scoreName: "score",
|
||||
variableMapping: JSON.parse("[]"),
|
||||
timeScope: ["NEW"],
|
||||
},
|
||||
});
|
||||
|
||||
const trace = createTrace({
|
||||
project_id: "7a88fb47-b4e2-43b8-a06c-a5ce950dc53a",
|
||||
id: traceId,
|
||||
timestamp: new Date(
|
||||
Date.now() + 1000 * 60 * 60 * 24 * 365 * 1,
|
||||
).getTime(),
|
||||
});
|
||||
|
||||
await createTracesCh([trace]);
|
||||
|
||||
await createEvalJobs({
|
||||
event: {
|
||||
projectId: "7a88fb47-b4e2-43b8-a06c-a5ce950dc53a",
|
||||
traceId: traceId,
|
||||
},
|
||||
});
|
||||
|
||||
const jobs = await kyselyPrisma.$kysely
|
||||
.selectFrom("job_executions")
|
||||
.selectAll()
|
||||
.where("project_id", "=", "7a88fb47-b4e2-43b8-a06c-a5ce950dc53a")
|
||||
.execute();
|
||||
|
||||
expect(jobs.length).toBe(1);
|
||||
}, 10_000);
|
||||
});
|
||||
|
||||
describe("execute evals", () => {
|
||||
test("evals a valid 'trace' event", async () => {
|
||||
@@ -1362,6 +1364,7 @@ describe("eval service tests", () => {
|
||||
id: traceId,
|
||||
project_id: "7a88fb47-b4e2-43b8-a06c-a5ce950dc53a",
|
||||
user_id: "a",
|
||||
environment: "production",
|
||||
input: JSON.stringify({ input: "This is a great prompt" }),
|
||||
output: JSON.stringify({ output: "This is a great response" }),
|
||||
timestamp: convertDateToClickhouseDateTime(new Date()),
|
||||
@@ -1393,10 +1396,12 @@ describe("eval service tests", () => {
|
||||
{
|
||||
value: '{"input":"This is a great prompt"}',
|
||||
var: "input",
|
||||
environment: "production",
|
||||
},
|
||||
{
|
||||
value: '{"output":"This is a great response"}',
|
||||
var: "output",
|
||||
environment: "production",
|
||||
},
|
||||
]);
|
||||
}, 10_000);
|
||||
@@ -1421,6 +1426,7 @@ describe("eval service tests", () => {
|
||||
project_id: "7a88fb47-b4e2-43b8-a06c-a5ce950dc53a",
|
||||
name: "great-llm-name",
|
||||
type: "GENERATION",
|
||||
environment: "production",
|
||||
input: JSON.stringify({ huhu: "This is a great prompt" }),
|
||||
output: JSON.stringify({ haha: "This is a great response" }),
|
||||
start_time: convertDateToClickhouseDateTime(new Date()),
|
||||
@@ -1454,10 +1460,12 @@ describe("eval service tests", () => {
|
||||
{
|
||||
value: '{"huhu":"This is a great prompt"}',
|
||||
var: "input",
|
||||
environment: "production",
|
||||
},
|
||||
{
|
||||
value: '{"haha":"This is a great response"}',
|
||||
var: "output",
|
||||
environment: "production",
|
||||
},
|
||||
]);
|
||||
}, 10_000);
|
||||
@@ -1544,10 +1552,12 @@ describe("eval service tests", () => {
|
||||
|
||||
expect(result).toEqual([
|
||||
{
|
||||
environment: "default",
|
||||
value: "",
|
||||
var: "input",
|
||||
},
|
||||
{
|
||||
environment: "default",
|
||||
value: "",
|
||||
var: "output",
|
||||
},
|
||||
@@ -1622,10 +1632,12 @@ describe("eval service tests", () => {
|
||||
|
||||
expect(result).toEqual([
|
||||
{
|
||||
environment: "default",
|
||||
value: '{"huhu":"This is a great prompt again"}',
|
||||
var: "input",
|
||||
},
|
||||
{
|
||||
environment: "default",
|
||||
value: '{"haha":"This is a great response again"}',
|
||||
var: "output",
|
||||
},
|
||||
|
||||
@@ -1 +1 @@
|
||||
export const VERSION = "v3.30.0";
|
||||
export const VERSION = "v3.34.1";
|
||||
|
||||
@@ -1446,9 +1446,9 @@
|
||||
{
|
||||
"id": "cm7ka7561000108js3t9tb3at",
|
||||
"model_name": "claude-3.7-sonnet-20250219",
|
||||
"match_pattern": "(?i)^(claude-3.7-sonnet-20250219|anthropic\\.claude-3.7-sonnet-20250219-v2:0|claude-3-7-sonnet-V2@20250219)$",
|
||||
"match_pattern": "(?i)^(claude-3.7-sonnet-20250219|anthropic\\.claude-3.7-sonnet-20250219-v1:0|claude-3-7-sonnet-V1@20250219)$",
|
||||
"created_at": "2025-02-25T09:35:39.000Z",
|
||||
"updated_at": "2025-02-25T09:35:39.000Z",
|
||||
"updated_at": "2025-02-27T12:07:29.000Z",
|
||||
"prices": {
|
||||
"input": 3e-6,
|
||||
"input_tokens": 3e-6,
|
||||
@@ -1480,5 +1480,33 @@
|
||||
},
|
||||
"tokenizer_config": null,
|
||||
"tokenizer_id": "claude"
|
||||
},
|
||||
{
|
||||
"id": "cm7nusjvk0000tvmz71o85jwg",
|
||||
"model_name": "gpt-4.5-preview",
|
||||
"match_pattern": "(?i)^(gpt-4.5-preview)$",
|
||||
"created_at": "2025-02-27T21:26:54.132Z",
|
||||
"updated_at": "2025-02-27T21:26:54.132Z",
|
||||
"prices": {
|
||||
"input": 75e-6,
|
||||
"input_cached_tokens": 37.5e-6,
|
||||
"input_cached_text_tokens": 37.5e-6,
|
||||
"input_cache_read": 37.5e-6,
|
||||
"output": 150e-6
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "cm7nusn640000tvmzf10z2x65",
|
||||
"model_name": "gpt-4.5-preview-2025-02-27",
|
||||
"match_pattern": "(?i)^(gpt-4.5-preview-2025-02-27)$",
|
||||
"created_at": "2025-02-27T21:26:54.132Z",
|
||||
"updated_at": "2025-02-27T21:26:54.132Z",
|
||||
"prices": {
|
||||
"input": 75e-6,
|
||||
"input_cached_tokens": 37.5e-6,
|
||||
"input_cached_text_tokens": 37.5e-6,
|
||||
"input_cache_read": 37.5e-6,
|
||||
"output": 150e-6
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
@@ -371,6 +371,9 @@ export const evaluate = async ({
|
||||
`Evaluating job ${event.jobExecutionId} extracted variables ${JSON.stringify(mappingResult)} `,
|
||||
);
|
||||
|
||||
// Get environment from trace or observation variables
|
||||
const environment = mappingResult.find((r) => r.environment)?.environment;
|
||||
|
||||
// compile the prompt and send out the LLM request
|
||||
let prompt;
|
||||
try {
|
||||
@@ -461,6 +464,7 @@ export const evaluate = async ({
|
||||
value: parsedLLMOutput.score,
|
||||
comment: parsedLLMOutput.reasoning,
|
||||
source: ScoreSource.EVAL,
|
||||
environment: environment ?? "default",
|
||||
};
|
||||
|
||||
// Write score to S3 and ingest into queue for Clickhouse processing
|
||||
@@ -542,7 +546,7 @@ export async function extractVariablesFromTracingData({
|
||||
// this here are variables which were inserted by users. Need to validate before DB query.
|
||||
variableMapping: z.infer<typeof variableMappingList>;
|
||||
datasetItemId?: string;
|
||||
}): Promise<{ var: string; value: string }[]> {
|
||||
}): Promise<{ var: string; value: string; environment?: string }[]> {
|
||||
return Promise.all(
|
||||
variables.map(async (variable) => {
|
||||
const mapping = variableMapping.find(
|
||||
@@ -616,10 +620,7 @@ export async function extractVariablesFromTracingData({
|
||||
return { var: variable, value: "" };
|
||||
}
|
||||
|
||||
const trace: Record<string, unknown> | undefined = await getTraceById(
|
||||
traceId,
|
||||
projectId,
|
||||
);
|
||||
const trace = await getTraceById(traceId, projectId);
|
||||
|
||||
// user facing errors
|
||||
if (!trace) {
|
||||
@@ -634,6 +635,7 @@ export async function extractVariablesFromTracingData({
|
||||
return {
|
||||
var: variable,
|
||||
value: parseDatabaseRowToString(trace, mapping),
|
||||
environment: trace.environment,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -656,7 +658,7 @@ export async function extractVariablesFromTracingData({
|
||||
return { var: variable, value: "" };
|
||||
}
|
||||
|
||||
const observation: Record<string, unknown> | undefined = (
|
||||
const observation = (
|
||||
await getObservationForTraceIdByName(
|
||||
traceId,
|
||||
projectId,
|
||||
@@ -678,7 +680,10 @@ export async function extractVariablesFromTracingData({
|
||||
|
||||
return {
|
||||
var: variable,
|
||||
value: parseUnknownToString(observation[mapping.selectedColumnId]),
|
||||
value: parseUnknownToString(
|
||||
(observation as Record<string, unknown>)[mapping.selectedColumnId],
|
||||
),
|
||||
environment: observation.environment,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -7,6 +7,7 @@ const EnvSchema = z.object({
|
||||
.enum(["development", "test", "production"])
|
||||
.default("development"),
|
||||
DATABASE_URL: z.string(),
|
||||
HOSTNAME: z.string().default("0.0.0.0"),
|
||||
PORT: z.coerce
|
||||
.number({
|
||||
description:
|
||||
|
||||
+2
-2
@@ -3,6 +3,6 @@ import app from "./app";
|
||||
import { env } from "./env";
|
||||
import { logger } from "@langfuse/shared/src/server";
|
||||
|
||||
export const server = app.listen(env.PORT, () => {
|
||||
logger.info(`Listening: http://localhost:${env.PORT}`);
|
||||
export const server = app.listen(env.PORT, env.HOSTNAME, () => {
|
||||
logger.info(`Listening: http://${env.HOSTNAME}:${env.PORT}`);
|
||||
});
|
||||
|
||||
@@ -61,13 +61,20 @@ const immutableEntityKeys: {
|
||||
[TableName.Scores]: (keyof ScoreRecordInsertType)[];
|
||||
[TableName.Observations]: (keyof ObservationRecordInsertType)[];
|
||||
} = {
|
||||
[TableName.Traces]: ["id", "project_id", "timestamp", "created_at"],
|
||||
[TableName.Traces]: [
|
||||
"id",
|
||||
"project_id",
|
||||
"timestamp",
|
||||
"created_at",
|
||||
"environment",
|
||||
],
|
||||
[TableName.Scores]: [
|
||||
"id",
|
||||
"project_id",
|
||||
"timestamp",
|
||||
"trace_id",
|
||||
"created_at",
|
||||
"environment",
|
||||
],
|
||||
[TableName.Observations]: [
|
||||
"id",
|
||||
@@ -75,6 +82,7 @@ const immutableEntityKeys: {
|
||||
"trace_id",
|
||||
"start_time",
|
||||
"created_at",
|
||||
"environment",
|
||||
],
|
||||
};
|
||||
|
||||
@@ -171,6 +179,7 @@ export class IngestionService {
|
||||
return {
|
||||
id: entityId,
|
||||
project_id: projectId,
|
||||
environment: validatedScore.environment,
|
||||
timestamp: this.getMillisecondTimestamp(scoreEvent.timestamp),
|
||||
name: validatedScore.name,
|
||||
value: validatedScore.value,
|
||||
@@ -411,6 +420,7 @@ export class IngestionService {
|
||||
id: finalObservationRecord.id,
|
||||
timestamp: finalObservationRecord.start_time,
|
||||
project_id: projectId,
|
||||
environment: finalObservationRecord.environment,
|
||||
created_at: Date.now(),
|
||||
updated_at: Date.now(),
|
||||
metadata: {},
|
||||
@@ -945,6 +955,7 @@ export class IngestionService {
|
||||
release: trace.body.release,
|
||||
version: trace.body.version,
|
||||
project_id: projectId,
|
||||
environment: trace.body.environment,
|
||||
public: trace.body.public ?? false,
|
||||
bookmarked: false,
|
||||
tags: trace.body.tags ?? [],
|
||||
@@ -1058,6 +1069,8 @@ export class IngestionService {
|
||||
trace_id: obs.body.traceId ?? v4(),
|
||||
type: observationType,
|
||||
name: obs.body.name,
|
||||
environment:
|
||||
"environment" in obs.body ? obs.body.environment : "default",
|
||||
start_time: this.getMillisecondTimestamp(
|
||||
obs.body.startTime ?? obs.timestamp,
|
||||
),
|
||||
|
||||
@@ -23,6 +23,7 @@ import { IngestionService } from "../../IngestionService";
|
||||
import { ModelUsageUnit, ScoreSource } from "@langfuse/shared";
|
||||
|
||||
const projectId = "7a88fb47-b4e2-43b8-a06c-a5ce950dc53a";
|
||||
const environment = "default";
|
||||
const IngestionEventBatchSchema = z.array(ingestionEvent);
|
||||
|
||||
describe("Ingestion end-to-end tests", () => {
|
||||
@@ -66,6 +67,7 @@ describe("Ingestion end-to-end tests", () => {
|
||||
body: {
|
||||
name: traceName,
|
||||
timestamp,
|
||||
environment,
|
||||
},
|
||||
},
|
||||
];
|
||||
@@ -368,6 +370,7 @@ describe("Ingestion end-to-end tests", () => {
|
||||
release: "1.0.0",
|
||||
version: "2.0.0",
|
||||
tags: ["tag-1", "tag-2"],
|
||||
environment,
|
||||
},
|
||||
},
|
||||
];
|
||||
@@ -388,6 +391,7 @@ describe("Ingestion end-to-end tests", () => {
|
||||
input: { key: "value" },
|
||||
metadata: { key: "value" },
|
||||
version: "2.0.0",
|
||||
environment,
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -400,6 +404,7 @@ describe("Ingestion end-to-end tests", () => {
|
||||
usage: testConfig.usage,
|
||||
usageDetails: testConfig.usageDetails,
|
||||
costDetails: testConfig.costDetails,
|
||||
environment,
|
||||
},
|
||||
},
|
||||
]);
|
||||
@@ -419,6 +424,7 @@ describe("Ingestion end-to-end tests", () => {
|
||||
input: { input: "value" },
|
||||
metadata: { meta: "value" },
|
||||
version: "2.0.0",
|
||||
environment,
|
||||
},
|
||||
},
|
||||
];
|
||||
@@ -435,6 +441,7 @@ describe("Ingestion end-to-end tests", () => {
|
||||
value: 100.5,
|
||||
source: ScoreSource.EVAL,
|
||||
traceId: traceId,
|
||||
environment,
|
||||
},
|
||||
},
|
||||
];
|
||||
@@ -755,6 +762,7 @@ describe("Ingestion end-to-end tests", () => {
|
||||
id: traceId,
|
||||
name: "trace-name",
|
||||
timestamp: new Date().toISOString(),
|
||||
environment,
|
||||
},
|
||||
},
|
||||
];
|
||||
@@ -775,6 +783,7 @@ describe("Ingestion end-to-end tests", () => {
|
||||
},
|
||||
input: "This is a great prompt",
|
||||
output: "This is a great gpt output",
|
||||
environment,
|
||||
},
|
||||
},
|
||||
];
|
||||
@@ -836,6 +845,7 @@ describe("Ingestion end-to-end tests", () => {
|
||||
body: {
|
||||
id: traceId,
|
||||
timestamp: new Date().toISOString(),
|
||||
environment,
|
||||
},
|
||||
},
|
||||
];
|
||||
@@ -849,6 +859,7 @@ describe("Ingestion end-to-end tests", () => {
|
||||
id: spanId,
|
||||
traceId: traceId,
|
||||
startTime: new Date().toISOString(),
|
||||
environment,
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -860,6 +871,7 @@ describe("Ingestion end-to-end tests", () => {
|
||||
traceId: traceId,
|
||||
name: "span-name",
|
||||
startTime: new Date().toISOString(),
|
||||
environment,
|
||||
},
|
||||
},
|
||||
];
|
||||
@@ -874,6 +886,7 @@ describe("Ingestion end-to-end tests", () => {
|
||||
traceId: traceId,
|
||||
startTime: new Date().toISOString(),
|
||||
parentObservationId: spanId,
|
||||
environment: environment,
|
||||
modelParameters: { someKey: ["user-1", "user-2"] },
|
||||
},
|
||||
},
|
||||
@@ -885,6 +898,7 @@ describe("Ingestion end-to-end tests", () => {
|
||||
id: generationId,
|
||||
name: "generation-name",
|
||||
startTime: new Date().toISOString(),
|
||||
environment,
|
||||
},
|
||||
},
|
||||
];
|
||||
@@ -900,6 +914,7 @@ describe("Ingestion end-to-end tests", () => {
|
||||
name: "event-name",
|
||||
startTime: new Date().toISOString(),
|
||||
parentObservationId: generationId,
|
||||
environment,
|
||||
},
|
||||
},
|
||||
];
|
||||
@@ -917,6 +932,7 @@ describe("Ingestion end-to-end tests", () => {
|
||||
source: ScoreSource.API,
|
||||
value: 100.5,
|
||||
observationId: generationId,
|
||||
environment,
|
||||
},
|
||||
},
|
||||
];
|
||||
@@ -1014,6 +1030,7 @@ describe("Ingestion end-to-end tests", () => {
|
||||
release: "1.0.0",
|
||||
version: "2.0.0",
|
||||
tags: ["tag-1", "tag-2", "tag-2"],
|
||||
environment,
|
||||
},
|
||||
},
|
||||
];
|
||||
@@ -1038,6 +1055,7 @@ describe("Ingestion end-to-end tests", () => {
|
||||
name: "trace-name",
|
||||
userId: "user-2",
|
||||
tags: ["tag-1", "tag-4", "tag-3"],
|
||||
environment,
|
||||
},
|
||||
},
|
||||
];
|
||||
@@ -1084,6 +1102,7 @@ describe("Ingestion end-to-end tests", () => {
|
||||
timestamp: latestEvent.toISOString(),
|
||||
name: "trace-name",
|
||||
userId: "user-1",
|
||||
environment,
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -1095,6 +1114,7 @@ describe("Ingestion end-to-end tests", () => {
|
||||
timestamp: new Date(oldEvent).toISOString(),
|
||||
name: "trace-name",
|
||||
userId: "user-2",
|
||||
environment,
|
||||
},
|
||||
},
|
||||
];
|
||||
@@ -1177,6 +1197,7 @@ describe("Ingestion end-to-end tests", () => {
|
||||
max_tokens: 1000,
|
||||
},
|
||||
usage: null,
|
||||
environment,
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -1195,6 +1216,7 @@ describe("Ingestion end-to-end tests", () => {
|
||||
total: -3,
|
||||
unit: "TOKENS",
|
||||
},
|
||||
environment,
|
||||
},
|
||||
},
|
||||
];
|
||||
@@ -1291,6 +1313,7 @@ describe("Ingestion end-to-end tests", () => {
|
||||
max_tokens: 1000,
|
||||
},
|
||||
usage: null,
|
||||
environment,
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -1309,6 +1332,7 @@ describe("Ingestion end-to-end tests", () => {
|
||||
total: 1313,
|
||||
unit: "TOKENS",
|
||||
},
|
||||
environment,
|
||||
},
|
||||
},
|
||||
];
|
||||
@@ -1364,6 +1388,7 @@ describe("Ingestion end-to-end tests", () => {
|
||||
startTime: new Date().toISOString(),
|
||||
output: "to overwrite",
|
||||
usage: undefined,
|
||||
environment,
|
||||
},
|
||||
},
|
||||
];
|
||||
@@ -1386,6 +1411,7 @@ describe("Ingestion end-to-end tests", () => {
|
||||
traceId: traceId,
|
||||
output: "overwritten",
|
||||
usage: undefined,
|
||||
environment,
|
||||
},
|
||||
},
|
||||
];
|
||||
@@ -1421,6 +1447,7 @@ describe("Ingestion end-to-end tests", () => {
|
||||
type: "GENERATION",
|
||||
startTime: new Date().toISOString(),
|
||||
output: { key: "this is a great gpt output" },
|
||||
environment,
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -1435,6 +1462,7 @@ describe("Ingestion end-to-end tests", () => {
|
||||
input: { key: "value" },
|
||||
output: "should be overwritten",
|
||||
model: "gpt-3.5",
|
||||
environment,
|
||||
},
|
||||
},
|
||||
];
|
||||
@@ -1480,6 +1508,7 @@ describe("Ingestion end-to-end tests", () => {
|
||||
timestamp,
|
||||
name: "trace-name",
|
||||
userId: "user-1",
|
||||
environment,
|
||||
},
|
||||
},
|
||||
];
|
||||
@@ -1496,6 +1525,7 @@ describe("Ingestion end-to-end tests", () => {
|
||||
startTime: new Date().toISOString(),
|
||||
name: "LiteLLM.run",
|
||||
// usage: null,
|
||||
environment,
|
||||
},
|
||||
},
|
||||
];
|
||||
@@ -1544,6 +1574,7 @@ describe("Ingestion end-to-end tests", () => {
|
||||
outputCost: 0.0007695,
|
||||
totalCost: 0.001412,
|
||||
},
|
||||
environment,
|
||||
},
|
||||
},
|
||||
];
|
||||
@@ -1608,6 +1639,7 @@ describe("Ingestion end-to-end tests", () => {
|
||||
name: "trace-name",
|
||||
timestamp: new Date().toISOString(),
|
||||
userId: "user-1",
|
||||
environment,
|
||||
},
|
||||
},
|
||||
];
|
||||
@@ -1625,6 +1657,7 @@ describe("Ingestion end-to-end tests", () => {
|
||||
name: "generation-name",
|
||||
input: { key: "value" },
|
||||
model: "gpt-3.5",
|
||||
environment,
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -1635,6 +1668,7 @@ describe("Ingestion end-to-end tests", () => {
|
||||
id: generationId,
|
||||
type: "GENERATION",
|
||||
output: { key: "this is a great gpt output" },
|
||||
environment,
|
||||
},
|
||||
},
|
||||
];
|
||||
@@ -1710,6 +1744,7 @@ describe("Ingestion end-to-end tests", () => {
|
||||
name: "trace-name",
|
||||
timestamp: new Date().toISOString(),
|
||||
userId: "user-1",
|
||||
environment,
|
||||
},
|
||||
},
|
||||
];
|
||||
@@ -1723,6 +1758,7 @@ describe("Ingestion end-to-end tests", () => {
|
||||
id: generationId,
|
||||
type: "GENERATION",
|
||||
output: { key: "this is a great gpt output" },
|
||||
environment,
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -1737,6 +1773,7 @@ describe("Ingestion end-to-end tests", () => {
|
||||
name: "generation-name",
|
||||
input: { key: "value" },
|
||||
model: "gpt-3.5",
|
||||
environment,
|
||||
},
|
||||
},
|
||||
];
|
||||
@@ -1795,6 +1832,7 @@ describe("Ingestion end-to-end tests", () => {
|
||||
metadata: { key: "value" },
|
||||
release: "1.0.0",
|
||||
version: "2.0.0",
|
||||
environment,
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -1808,6 +1846,7 @@ describe("Ingestion end-to-end tests", () => {
|
||||
// Do not set user_id here to validate behaviour for missing fields
|
||||
release: null,
|
||||
version: undefined,
|
||||
environment,
|
||||
},
|
||||
},
|
||||
];
|
||||
@@ -1899,6 +1938,7 @@ describe("Ingestion end-to-end tests", () => {
|
||||
timestamp: new Date().toISOString(),
|
||||
userId: "user-1",
|
||||
metadata: inputs[0],
|
||||
environment,
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -1910,6 +1950,7 @@ describe("Ingestion end-to-end tests", () => {
|
||||
name: "trace-name",
|
||||
timestamp: new Date().toISOString(),
|
||||
metadata: inputs[1],
|
||||
environment,
|
||||
},
|
||||
},
|
||||
];
|
||||
@@ -1926,6 +1967,7 @@ describe("Ingestion end-to-end tests", () => {
|
||||
type: "GENERATION",
|
||||
name: "generation-name",
|
||||
metadata: inputs[0],
|
||||
environment,
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -1938,6 +1980,7 @@ describe("Ingestion end-to-end tests", () => {
|
||||
startTime: new Date().toISOString(),
|
||||
type: "GENERATION",
|
||||
metadata: inputs[1],
|
||||
environment,
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
Reference in New Issue
Block a user