Compare commits

...
11 Commits
Author SHA1 Message Date
Max Deichmann ac0c13f3e5 chore: release v2.47.4
CI/CD / lint (push) Waiting to run
CI/CD / test-docker-build (push) Waiting to run
CI/CD / tests-web (20) (push) Waiting to run
CI/CD / tests-worker (20) (push) Waiting to run
CI/CD / e2e-tests (push) Waiting to run
CI/CD / all-ci-passed (push) Blocked by required conditions
CI/CD / push-docker-image (push) Blocked by required conditions
release.yml / release (push) Waiting to run
2024-06-03 18:07:58 +02:00
Marc KlingenandGitHub a7b90fa32c feat(ui): preview model cost per 1k, 100k, and 1M units (#2216) 2024-06-03 13:45:10 +00:00
Marc Klingen 9eaf2a2e0f docs(api): add tags to prompt api response type 2024-06-03 14:24:48 +02:00
Max DeichmannandGitHub 61e3cc33bf fix: do not log expected eval errors as sentry errors (#2213) 2024-06-03 12:22:41 +00:00
2be198400d feat(api): add prompt tags via API (#2180)
---------

Co-authored-by: Marc Klingen <git@marcklingen.com>
2024-06-03 12:10:15 +00:00
Max Deichmann 830df682f9 chore: release v2.47.3
CI/CD / lint (push) Waiting to run
CI/CD / test-docker-build (push) Waiting to run
CI/CD / tests-web (20) (push) Waiting to run
CI/CD / tests-worker (20) (push) Waiting to run
CI/CD / e2e-tests (push) Waiting to run
CI/CD / all-ci-passed (push) Blocked by required conditions
CI/CD / push-docker-image (push) Blocked by required conditions
release.yml / release (push) Waiting to run
2024-06-03 12:19:27 +02:00
Max DeichmannandGitHub 2a420b2e90 chore: reduce sentry sampling (#2212) 2024-06-03 11:06:56 +02:00
Marc Klingen 53127c34ab chore: add posthog ui_host 2024-06-02 21:18:35 +02:00
Marc Klingen 9c57ff4853 chore: reduce cloud telemetry to 4 times a day 2024-06-02 20:18:55 +02:00
Marc Klingen a71176eda4 chore: disable posthog autocapture 2024-06-02 20:12:54 +02:00
Marc KlingenandGitHub f59a85f412 fix(ui): full prompt name on hover when saving as new version from playground (#2208) 2024-06-02 14:31:01 +00:00
20 changed files with 327 additions and 56 deletions
+18 -3
View File
@@ -70,14 +70,24 @@ types:
name: string
prompt: list<ChatMessage>
config: optional<unknown>
labels: optional<list<string>>
labels:
type: optional<list<string>>
docs: List of deployment labels of this prompt version.
tags:
type: optional<list<string>>
docs: List of tags to apply to all versions of this prompt.
CreateTextPromptRequest:
properties:
name: string
prompt: string
config: optional<unknown>
labels: optional<list<string>>
labels:
type: optional<list<string>>
docs: List of deployment labels of this prompt version.
tags:
type: optional<list<string>>
docs: List of tags to apply to all versions of this prompt.
Prompt:
union:
@@ -89,7 +99,12 @@ types:
name: string
version: integer
config: unknown
labels: list<string>
labels:
type: list<string>
docs: List of deployment labels of this prompt version.
tags:
type: list<string>
docs: List of tags. Used to filter via UI and API. The same across versions of a prompt.
ChatMessage:
properties:
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "langfuse",
"version": "2.47.2",
"version": "2.47.4",
"author": "engineering@langfuse.com",
"license": "MIT",
"private": true,
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "web",
"version": "2.47.2",
"version": "2.47.4",
"private": true,
"license": "MIT",
"engines": {
+23
View File
@@ -2654,6 +2654,13 @@ components:
items:
type: string
nullable: true
description: List of deployment labels of this prompt version.
tags:
type: array
items:
type: string
nullable: true
description: List of tags to apply to all versions of this prompt.
required:
- name
- prompt
@@ -2672,6 +2679,13 @@ components:
items:
type: string
nullable: true
description: List of deployment labels of this prompt version.
tags:
type: array
items:
type: string
nullable: true
description: List of tags to apply to all versions of this prompt.
required:
- name
- prompt
@@ -2713,11 +2727,20 @@ components:
type: array
items:
type: string
description: List of deployment labels of this prompt version.
tags:
type: array
items:
type: string
description: >-
List of tags. Used to filter via UI and API. The same across
versions of a prompt.
required:
- name
- version
- config
- labels
- tags
ChatMessage:
title: ChatMessage
type: object
+1 -1
View File
@@ -699,7 +699,7 @@
"auth": null,
"body": {
"mode": "raw",
"raw": "{\n \"type\": \"chat\",\n \"name\": \"example\",\n \"prompt\": [\n {\n \"role\": \"example\",\n \"content\": \"example\"\n }\n ],\n \"config\": \"UNKNOWN\",\n \"labels\": [\n \"example\"\n ]\n}",
"raw": "{\n \"type\": \"chat\",\n \"name\": \"example\",\n \"prompt\": [\n {\n \"role\": \"example\",\n \"content\": \"example\"\n }\n ],\n \"config\": \"UNKNOWN\",\n \"labels\": [\n \"example\"\n ],\n \"tags\": [\n \"example\"\n ]\n}",
"options": {
"raw": {
"language": "json"
+4 -4
View File
@@ -18,7 +18,7 @@ if (process.env.NEXT_PUBLIC_SENTRY_DSN)
samplingContext.request.url &&
samplingContext.request.url.includes("api/trpc")
) {
return 0.3;
return 0.1;
}
if (
samplingContext.request &&
@@ -27,12 +27,12 @@ if (process.env.NEXT_PUBLIC_SENTRY_DSN)
samplingContext.transactionContext.status !== "ok" &&
samplingContext.transactionContext.status !== "unauthenticated"
) {
return 1;
return 0.1;
}
return 0.1;
return 0.01;
},
profilesSampleRate: 0.2, // Profiling sample rate is relative to tracesSampleRate
profilesSampleRate: 0.1,
integrations: [
// Add profiling integration to list of integrations
new ProfilingIntegration(),
@@ -680,6 +680,71 @@ describe("/api/public/v2/prompts API Endpoint", () => {
expect(fetchedPrompt.body.createdBy).toBe("API");
expect(fetchedPrompt.body.config).toEqual({});
});
it("should update tags across versions", async () => {
const promptName = "prompt-name" + nanoid();
const createPromptVersion = async (tags?: string[]) => {
await makeAPICall("POST", baseURI, {
name: promptName,
prompt: "This is a test prompt",
type: PromptType.Text,
...(tags !== undefined && { tags: tags }),
});
};
const fetchPromptVersion = async (version: number) => {
const fetchedPrompt = await makeAPICall(
"GET",
`${baseURI}/${promptName}?version=${version}`,
undefined,
);
expect(fetchedPrompt.status).toBe(200);
if (!isPrompt(fetchedPrompt.body)) {
throw new Error("Expected body to be a prompt");
}
return fetchedPrompt.body;
};
// Create version 1 with ["tag"]
await createPromptVersion(["tag"]);
let fetchedPrompt1 = await fetchPromptVersion(1);
expect(fetchedPrompt1.tags).toEqual(["tag"]);
expect(fetchedPrompt1.version).toBe(1);
// Create version 2 with no tags provided (should use tags from version 1)
await createPromptVersion();
let fetchedPrompt2 = await fetchPromptVersion(2);
expect(fetchedPrompt2.tags).toEqual(["tag"]);
expect(fetchedPrompt2.version).toBe(2);
// Create version 3 with ["tag1", "tag2", "tag3"] (should update tags across versions)
await createPromptVersion(["tag1", "tag2", "tag3"]);
fetchedPrompt1 = await fetchPromptVersion(1);
fetchedPrompt2 = await fetchPromptVersion(2);
let fetchedPrompt3 = await fetchPromptVersion(3);
expect(fetchedPrompt1.tags).toEqual(["tag1", "tag2", "tag3"]);
expect(fetchedPrompt1.version).toBe(1);
expect(fetchedPrompt2.tags).toEqual(["tag1", "tag2", "tag3"]);
expect(fetchedPrompt2.version).toBe(2);
expect(fetchedPrompt3.tags).toEqual(["tag1", "tag2", "tag3"]);
expect(fetchedPrompt3.version).toBe(3);
// remove tags
await createPromptVersion([]);
fetchedPrompt1 = await fetchPromptVersion(1);
fetchedPrompt2 = await fetchPromptVersion(2);
fetchedPrompt3 = await fetchPromptVersion(3);
let fetchedPrompt4 = await fetchPromptVersion(4);
expect(fetchedPrompt1.tags).toEqual([]);
expect(fetchedPrompt1.version).toBe(1);
expect(fetchedPrompt2.tags).toEqual([]);
expect(fetchedPrompt2.version).toBe(2);
expect(fetchedPrompt3.tags).toEqual([]);
expect(fetchedPrompt3.version).toBe(3);
expect(fetchedPrompt4.tags).toEqual([]);
expect(fetchedPrompt4.version).toBe(4);
});
});
describe("when fetching a prompt list", () => {
+1 -1
View File
@@ -1 +1 @@
export const VERSION = "v2.47.2";
export const VERSION = "v2.47.4";
@@ -49,8 +49,7 @@ export const SaveToPromptButton: React.FC = () => {
)
.data?.prompts.filter((prompt) => prompt.type === PromptType.Chat)
.map((prompt) => ({
label:
prompt.name.slice(0, 20) + (prompt.name.length > 25 ? "..." : ""),
label: prompt.name,
value: prompt.id,
})) ?? [];
@@ -126,7 +125,9 @@ export const SaveToPromptButton: React.FC = () => {
: "opacity-0",
)}
/>
{promptName.label}
<span className="overflow-hidden text-ellipsis whitespace-nowrap">
{promptName.label}
</span>
</CommandItem>
))}
</CommandList>
@@ -28,6 +28,7 @@ import { api } from "@/src/utils/api";
import { zodResolver } from "@hookform/resolvers/zod";
import { JsonEditor } from "@/src/components/json-editor";
import { usePostHogClientCapture } from "@/src/features/posthog-analytics/usePostHogClientCapture";
import Link from "next/link";
const formSchema = z.object({
modelName: z.string().min(1),
@@ -255,11 +256,33 @@ export const NewModelForm = (props: {
name="inputPrice"
render={({ field }) => (
<FormItem>
<FormLabel>Input price (USD)</FormLabel>
<FormLabel>
Input price (USD per{" "}
{form.getValues("unit").toLowerCase().replace(/s$/, "")})
</FormLabel>
<FormControl>
<Input {...field} type="number" />
</FormControl>
<FormDescription>Cost per input unit.</FormDescription>
{field.value !== null && field.value !== "" ? (
<FormDescription>
<ul className="font-mono text-xs">
<li>
{(parseFloat(field.value ?? "0") * 1000).toFixed(4)} USD
/ 1k {form.getValues("unit").toLowerCase()}
</li>
<li>
{(parseFloat(field.value ?? "0") * 100_000).toFixed(4)}{" "}
USD / 100k {form.getValues("unit").toLowerCase()}
</li>
<li>
{(parseFloat(field.value ?? "0") * 1_000_000).toFixed(
4,
)}{" "}
USD / 1M {form.getValues("unit").toLowerCase()}
</li>
</ul>
</FormDescription>
) : null}
<FormMessage />
</FormItem>
)}
@@ -269,11 +292,33 @@ export const NewModelForm = (props: {
name="outputPrice"
render={({ field }) => (
<FormItem>
<FormLabel>Output price (USD)</FormLabel>
<FormLabel>
Output price (USD per{" "}
{form.getValues("unit").toLowerCase().replace(/s$/, "")})
</FormLabel>
<FormControl>
<Input {...field} type="number" />
</FormControl>
<FormDescription>Cost per output unit.</FormDescription>
{field.value !== null && field.value !== "" ? (
<FormDescription>
<ul className="font-mono text-xs">
<li>
{(parseFloat(field.value ?? "0") * 1000).toFixed(4)} USD
/ 1k {form.getValues("unit").toLowerCase()}
</li>
<li>
{(parseFloat(field.value ?? "0") * 100_000).toFixed(4)}{" "}
USD / 100k {form.getValues("unit").toLowerCase()}
</li>
<li>
{(parseFloat(field.value ?? "0") * 1_000_000).toFixed(
4,
)}{" "}
USD / 1M {form.getValues("unit").toLowerCase()}
</li>
</ul>
</FormDescription>
) : null}
<FormMessage />
</FormItem>
)}
@@ -283,12 +328,34 @@ export const NewModelForm = (props: {
name="totalPrice"
render={({ field }) => (
<FormItem>
<FormLabel>Total price (USD)</FormLabel>
<FormLabel>
Total price (USD per{" "}
{form.getValues("unit").toLowerCase().replace(/s$/, "")})
</FormLabel>
<FormControl>
<Input {...field} type="number" />
</FormControl>
<FormDescription>
Cost per unit, if no separate input/output prices.
{field.value !== null && field.value !== "" ? (
<ul className="mt-2 font-mono text-xs">
<li>
{(parseFloat(field.value ?? "0") * 1000).toFixed(4)} USD
/ 1k {form.getValues("unit").toLowerCase()}
</li>
<li>
{(parseFloat(field.value ?? "0") * 100_000).toFixed(4)}{" "}
USD / 100k {form.getValues("unit").toLowerCase()}
</li>
<li>
{(parseFloat(field.value ?? "0") * 1_000_000).toFixed(
4,
)}{" "}
USD / 1M {form.getValues("unit").toLowerCase()}
</li>
</ul>
) : (
"Enter total price only if no separate input and output prices are provided."
)}
</FormDescription>
<FormMessage />
</FormItem>
@@ -328,7 +395,15 @@ export const NewModelForm = (props: {
Optionally, Langfuse can tokenize the input and output of a
generation if no unit counts are ingested. This is useful for
e.g. streamed OpenAI completions. For details on the supported
tokenizers, see the docs.
tokenizers, see the{" "}
<Link
href="https://langfuse.com/docs/model-usage-and-cost"
className="underline"
target="_blank"
>
docs
</Link>
.
</FormDescription>
<FormMessage />
</FormItem>
@@ -346,8 +421,15 @@ export const NewModelForm = (props: {
onChange={field.onChange}
/>
<FormDescription>
The config for the tokenizer. Required for openai. See the
docs for details.
The config for the tokenizer. Required for openai. See the{" "}
<Link
href="https://langfuse.com/docs/model-usage-and-cost"
className="underline"
target="_blank"
>
docs
</Link>{" "}
for details.
</FormDescription>
<FormMessage />
</FormItem>
@@ -6,6 +6,8 @@ import { ValidationError } from "@langfuse/shared";
import { jsonSchema } from "@/src/utils/zod";
import { type PrismaClient } from "@langfuse/shared/src/db";
import { LATEST_PROMPT_LABEL } from "@/src/features/prompts/constants";
import { removeLabelsFromPreviousPromptVersions } from "@/src/features/prompts/server/utils/updatePromptLabels";
import { updatePromptTagsOnAllVersions } from "@/src/features/prompts/server/utils/updatePromptTags";
export type CreatePromptParams = CreatePromptTRPCType & {
createdBy: string;
@@ -21,6 +23,7 @@ export const createPrompt = async ({
config,
createdBy,
prisma,
tags,
}: CreatePromptParams) => {
const latestPrompt = await prisma.prompt.findFirst({
where: { projectId, name },
@@ -35,14 +38,8 @@ export const createPrompt = async ({
const finalLabels = [...labels, LATEST_PROMPT_LABEL]; // Newly created prompts are always labeled as 'latest'
const previousLabeledPrompts = await prisma.prompt.findMany({
where: {
projectId,
name,
labels: { hasSome: finalLabels },
},
orderBy: [{ version: "desc" }],
});
// If tags are undefined, use the tags from the latest prompt version
const finalTags = [...new Set(tags ?? latestPrompt?.tags ?? [])];
const create = [
prisma.prompt.create({
@@ -52,7 +49,7 @@ export const createPrompt = async ({
createdBy,
labels: [...new Set(finalLabels)], // Ensure labels are unique
type,
tags: latestPrompt?.tags,
tags: finalTags,
version: latestPrompt?.version ? latestPrompt.version + 1 : 1,
project: { connect: { id: projectId } },
config: jsonSchema.parse(config),
@@ -62,18 +59,28 @@ export const createPrompt = async ({
if (finalLabels.length > 0)
// If we're creating a new labeled prompt, we must remove those labels on previous prompts since labels are unique
previousLabeledPrompts.forEach((prevPrompt) => {
create.push(
prisma.prompt.update({
where: { id: prevPrompt.id },
data: {
labels: prevPrompt.labels.filter(
(prevLabel) => !finalLabels.includes(prevLabel),
),
},
}),
);
});
create.push(
...(await removeLabelsFromPreviousPromptVersions({
prisma,
projectId,
promptName: name,
labelsToRemove: finalLabels,
})),
);
const haveTagsChanged =
JSON.stringify([...new Set(finalTags)].sort()) !==
JSON.stringify([...new Set(latestPrompt?.tags)].sort());
if (haveTagsChanged)
// If we're creating a new prompt with tags, we must update those tags on previous prompts since tags are consistent across versions
create.push(
...(await updatePromptTagsOnAllVersions({
prisma,
projectId,
promptName: name,
tags: finalTags,
})),
);
const [createdPrompt] = await prisma.$transaction(create);
@@ -0,0 +1,33 @@
import { type PrismaClient } from "@langfuse/shared/src/db";
export const removeLabelsFromPreviousPromptVersions = async ({
prisma,
projectId,
promptName,
labelsToRemove,
}: {
prisma: PrismaClient;
projectId: string;
promptName: string;
labelsToRemove: string[];
}) => {
const previouslyLabeledPrompts = await prisma.prompt.findMany({
where: {
projectId,
name: promptName,
labels: { hasSome: labelsToRemove },
},
orderBy: [{ version: "desc" }],
});
return previouslyLabeledPrompts.map((prevPrompt) =>
prisma.prompt.update({
where: { id: prevPrompt.id },
data: {
labels: prevPrompt.labels.filter(
(prevLabel) => !labelsToRemove.includes(prevLabel),
),
},
}),
);
};
@@ -0,0 +1,28 @@
import { type PrismaClient } from "@langfuse/shared/src/db";
export const updatePromptTagsOnAllVersions = async ({
prisma,
projectId,
promptName,
tags,
}: {
prisma: PrismaClient;
projectId: string;
promptName: string;
tags: string[];
}) => {
const previousVersions = await prisma.prompt.findMany({
where: { projectId, name: promptName },
});
if (previousVersions.length === 0) return [];
return previousVersions.map((prevVersion) =>
prisma.prompt.update({
where: { id: prevVersion.id },
data: {
tags: [...new Set(tags)], // Ensure tags are unique
},
}),
);
};
@@ -27,6 +27,7 @@ export const CreateTextPromptSchema = z.object({
type: z.literal(PromptType.Text).optional(),
prompt: z.string(),
config: jsonSchema.nullable().default({}),
tags: z.array(z.string()).optional(),
});
export const CreateChatPromptSchema = z.object({
@@ -35,6 +36,7 @@ export const CreateChatPromptSchema = z.object({
type: z.literal(PromptType.Chat),
prompt: z.array(ChatMessageSchema),
config: jsonSchema.nullable().default({}),
tags: z.array(z.string()).optional(),
});
export const CreatePromptSchema = z.union([
+2
View File
@@ -53,10 +53,12 @@ if (
setProjectInPosthog();
posthog.init(process.env.NEXT_PUBLIC_POSTHOG_KEY, {
api_host: process.env.NEXT_PUBLIC_POSTHOG_HOST || "https://eu.posthog.com",
ui_host: "https://eu.posthog.com",
// Enable debug mode in development
loaded: (posthog) => {
if (process.env.NODE_ENV === "development") posthog.debug();
},
autocapture: false,
});
}
+1 -1
View File
@@ -2,7 +2,7 @@
"crons": [
{
"path": "/api/cron/ingestion-metrics",
"schedule": "*/15 * * * *"
"schedule": "0 */6 * * *"
}
]
}
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "worker",
"version": "2.47.2",
"version": "2.47.4",
"description": "",
"license": "MIT",
"main": "index.js",
+2 -2
View File
@@ -31,9 +31,9 @@ if (isSentryEnabled) {
Sentry.metrics.metricsAggregatorIntegration(),
],
// Performance Monitoring
tracesSampleRate: 0.1, // Capture 100% of the transactions
tracesSampleRate: 0.01, // Capture 100% of the transactions
// Set sampling rate for profiling - this is relative to tracesSampleRate
profilesSampleRate: 0.1,
profilesSampleRate: 0.01,
sampleRate: 0.1,
});
+1 -1
View File
@@ -1 +1 @@
export const VERSION = "v2.47.2";
export const VERSION = "v2.47.4";
+20 -7
View File
@@ -1,5 +1,10 @@
import { Job, Queue, Worker } from "bullmq";
import { BaseError, QueueName, TQueueJobTypes } from "@langfuse/shared";
import {
ApiError,
BaseError,
QueueName,
TQueueJobTypes,
} from "@langfuse/shared";
import { evaluate, createEvalJobs } from "../eval-service";
import { kyselyPrisma } from "@langfuse/shared/src/db";
import logger from "../logger";
@@ -59,11 +64,6 @@ export const evalJobExecutor = redis
await evaluate({ event: job.data.payload });
return true;
} catch (e) {
logger.error(
e,
`Failed Evaluation_Execution job for id ${job.data.payload.jobExecutionId} ${e}`
);
const displayError =
e instanceof BaseError ? e.message : "An internal error occurred";
@@ -76,7 +76,20 @@ export const evalJobExecutor = redis
.where("project_id", "=", job.data.payload.projectId)
.execute();
Sentry.captureException(e);
// do not log expected errors (api failures + missing api keys not provided by the user)
if (
!(e instanceof ApiError) &&
!(
e instanceof BaseError &&
e.message.includes("API key for provider")
)
) {
logger.error(
e,
`Failed Evaluation_Execution job for id ${job.data.payload.jobExecutionId} ${e}`
);
Sentry.captureException(e);
}
throw e;
} finally {