Compare commits

...
21 Commits
Author SHA1 Message Date
Marc Klingen 85343bf26c chore: release v2.47.7
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-04 16:56:48 +02:00
Richard KrümmelandGitHub e3b4dc1928 feat(api): add sessionId filter for traces endpoint (#2227) 2024-06-04 14:38:51 +00:00
Marc KlingenandGitHub a4939bacd8 perf: add index on scores.name (#2228) 2024-06-04 15:35:21 +02:00
Hassieb Pakzad 4441942cf5 chore: release v2.47.6
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-04 14:02:23 +02:00
Hassieb PakzadandGitHub 6597b02c09 perf: boost sessions table performance (#2225) 2024-06-04 13:42:09 +02:00
Marc KlingenandGitHub 0df3ad27dc perf: add index on dataset_items.source_trace_id (#2220) 2024-06-04 01:57:19 +02:00
Marc KlingenandGitHub 696d458a27 fix(cloud): handle posthog settings rbac correctly in frontend (#2223) 2024-06-03 23:35:49 +00:00
Marc KlingenandGitHub fd18ab605d feat(api): add count and cost to daily metrics api (#2219) 2024-06-03 22:41:01 +02:00
Marc Klingen d47d14eecd chore: release v2.47.5
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 19:56:18 +02:00
Richard KrümmelandGitHub 24a81e1589 fix: update validation schema for prompt tags (#2217) 2024-06-03 18:51:39 +02:00
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
31 changed files with 531 additions and 117 deletions
+17 -4
View File
@@ -48,12 +48,25 @@ types:
date: date
countTraces: integer
countObservations: integer
totalCost: double
totalCost:
type: double
docs: Total model cost in USD
usage: list<UsageByModel>
UsageByModel:
docs: Daily usage of a given model. Usage corresponds to the unit set for the specific model (e.g. tokens).
properties:
model: optional<string>
inputUsage: integer
outputUsage: integer
totalUsage: integer
inputUsage:
type: integer
docs: Total number of generation input units (e.g. tokens)
outputUsage:
type: integer
docs: Total number of generation output units (e.g. tokens)
totalUsage:
type: integer
docs: Total number of generation total units (e.g. tokens)
countTraces: integer
countObservations: integer
totalCost:
type: double
docs: Total model cost in USD
+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
View File
@@ -30,6 +30,7 @@ service:
docs: Limit of items per page. If you encounter api issues due to too large page sizes, try to reduce the limit.
userId: optional<string>
name: optional<string>
sessionId: optional<string>
fromTimestamp:
type: optional<datetime>
docs: Retrieve only traces newer than this datetime (ISO 8601).
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "langfuse",
"version": "2.47.2",
"version": "2.47.7",
"author": "engineering@langfuse.com",
"license": "MIT",
"private": true,
@@ -0,0 +1,2 @@
-- CreateIndex
CREATE INDEX CONCURRENTLY "dataset_items_source_trace_id_idx" ON "dataset_items" USING HASH ("source_trace_id");
@@ -0,0 +1,2 @@
-- CreateIndex
CREATE INDEX "scores_project_id_name_idx" ON "scores"("project_id", "name");
+2
View File
@@ -419,6 +419,7 @@ model Score {
@@index(timestamp)
@@index([value])
@@index([projectId])
@@index([projectId, name])
@@index([authorUserId])
@@index([configId])
@@index([traceId], type: Hash)
@@ -532,6 +533,7 @@ model DatasetItem {
updatedAt DateTime @default(now()) @updatedAt @map("updated_at")
datasetRunItems DatasetRunItems[]
@@index([sourceTraceId], type: Hash)
@@index([sourceObservationId], type: Hash)
@@index([datasetId], type: Hash)
@@index([createdAt])
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "web",
"version": "2.47.2",
"version": "2.47.7",
"private": true,
"license": "MIT",
"engines": {
+44
View File
@@ -1262,6 +1262,12 @@ paths:
schema:
type: string
nullable: true
- name: sessionId
in: query
required: false
schema:
type: string
nullable: true
- name: fromTimestamp
in: query
description: Retrieve only traces newer than this datetime (ISO 8601).
@@ -2499,6 +2505,7 @@ components:
totalCost:
type: number
format: double
description: Total model cost in USD
usage:
type: array
items:
@@ -2521,14 +2528,28 @@ components:
nullable: true
inputUsage:
type: integer
description: Total number of generation input units (e.g. tokens)
outputUsage:
type: integer
description: Total number of generation output units (e.g. tokens)
totalUsage:
type: integer
description: Total number of generation total units (e.g. tokens)
countTraces:
type: integer
countObservations:
type: integer
totalCost:
type: number
format: double
description: Total model cost in USD
required:
- inputUsage
- outputUsage
- totalUsage
- countTraces
- countObservations
- totalCost
Observations:
title: Observations
type: object
@@ -2654,6 +2675,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 +2700,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 +2748,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
+7 -2
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"
@@ -963,7 +963,7 @@
"request": {
"description": "Get list of traces.",
"url": {
"raw": "{{baseUrl}}/api/public/traces?page=&limit=&userId=&name=&fromTimestamp=&orderBy=&tags=",
"raw": "{{baseUrl}}/api/public/traces?page=&limit=&userId=&name=&sessionId=&fromTimestamp=&orderBy=&tags=",
"host": [
"{{baseUrl}}"
],
@@ -993,6 +993,11 @@
"value": "",
"description": null
},
{
"key": "sessionId",
"value": "",
"description": null
},
{
"key": "fromTimestamp",
"value": "",
+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(),
@@ -68,12 +68,18 @@ describe("/api/public/metrics/daily API Endpoint", () => {
inputUsage: 333,
outputUsage: 0,
totalUsage: 333,
countObservations: 1,
countTraces: 1,
totalCost: 0,
},
{
model: "modelC",
inputUsage: 666,
outputUsage: 777,
totalUsage: 1443,
countObservations: 1,
countTraces: 1,
totalCost: 1024.22,
},
]);
@@ -88,6 +94,9 @@ describe("/api/public/metrics/daily API Endpoint", () => {
inputUsage: 100,
outputUsage: 200,
totalUsage: 300,
countObservations: 1,
countTraces: 1,
totalCost: 0,
},
]);
});
@@ -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", () => {
+58
View File
@@ -238,4 +238,62 @@ describe("/api/public/traces API Endpoint", () => {
expect(trace.body.htmlPath).toContain(`/traces/${traceId}`);
expect(trace.body.htmlPath).toContain(`/project/`); // do not know the projectId
});
it("should filter traces by session ID", async () => {
const sessionId = "test-session-id";
const anotherSessionId = "another-session-id";
// Create traces with different session IDs
await makeAPICall("POST", "/api/public/traces", {
id: "trace-1",
name: "test-trace-1",
sessionId,
userId: "user-1",
projectId: "project-1",
metadata: { key: "value" },
release: "1.0.0",
version: "1.0.0",
});
await makeAPICall("POST", "/api/public/traces", {
id: "trace-2",
name: "test-trace-2",
sessionId: anotherSessionId,
userId: "user-2",
projectId: "project-1",
metadata: { key: "value" },
release: "1.0.0",
version: "1.0.0",
});
// Filter by session ID
const tracesBySessionId = await makeAPICall<GetTracesAPIResponse>(
"GET",
`/api/public/traces?sessionId=${sessionId}`,
);
expect(tracesBySessionId.status).toBe(200);
expect(tracesBySessionId.body.data).toHaveLength(1);
expect(tracesBySessionId.body.data[0].id).toBe("trace-1");
// Filter by another session ID
const tracesByAnotherSessionId = await makeAPICall<GetTracesAPIResponse>(
"GET",
`/api/public/traces?sessionId=${anotherSessionId}`,
);
expect(tracesByAnotherSessionId.status).toBe(200);
expect(tracesByAnotherSessionId.body.data).toHaveLength(1);
expect(tracesByAnotherSessionId.body.data[0].id).toBe("trace-2");
// Filter by non-existent session ID
const tracesByNonExistentSessionId =
await makeAPICall<GetTracesAPIResponse>(
"GET",
`/api/public/traces?sessionId=non-existent-session-id`,
);
expect(tracesByNonExistentSessionId.status).toBe(200);
expect(tracesByNonExistentSessionId.body.data).toHaveLength(0);
});
});
+1 -1
View File
@@ -1 +1 @@
export const VERSION = "v2.47.2";
export const VERSION = "v2.47.7";
@@ -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()).nullish(),
});
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()).nullish(),
});
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,
});
}
+16 -7
View File
@@ -68,12 +68,15 @@ export default async function handler(
DATE_TRUNC('DAY',
o.start_time) "date",
o.model,
SUM(o.prompt_tokens) inputUsage,
SUM(o.completion_tokens) outputUsage,
SUM(o.total_tokens) totalUsage
count(distinct o.id)::integer as "countObservations",
count(distinct t.id)::integer as "countTraces",
SUM(o.prompt_tokens) "inputUsage",
SUM(o.completion_tokens) "outputUsage",
SUM(o.total_tokens) "totalUsage",
COALESCE(SUM(o.calculated_total_cost), 0)::DOUBLE PRECISION as "totalCost"
FROM
traces t
LEFT JOIN observations o ON o.trace_id = t.id AND o.project_id = t.project_id
LEFT JOIN observations_view o ON o.trace_id = t.id AND o.project_id = t.project_id
WHERE o.start_time IS NOT NULL
AND t.project_id = ${authCheck.scope.projectId}
${traceNameCondition}
@@ -94,11 +97,17 @@ export default async function handler(
json_agg(json_build_object('model',
model,
'inputUsage',
inputUsage,
"inputUsage",
'outputUsage',
outputUsage,
"outputUsage",
'totalUsage',
totalUsage)) daily_usage_json
"totalUsage",
'totalCost',
"totalCost",
'countObservations',
"countObservations",
'countTraces',
"countTraces")) daily_usage_json
FROM
model_usage
GROUP BY
+6
View File
@@ -25,6 +25,7 @@ const GetTracesSchema = z.object({
userId: z.string().nullish(),
name: z.string().nullish(),
tags: z.union([z.array(z.string()), z.string()]).nullish(),
sessionId: z.string().nullish(),
fromTimestamp: stringDate,
orderBy: z
.string() // orderBy=timestamp.asc
@@ -105,6 +106,9 @@ export default async function handler(
", ",
)}] <@ t."tags"`
: Prisma.empty;
const sessionCondition = obj.sessionId
? Prisma.sql`AND t."session_id" = ${obj.sessionId}`
: Prisma.empty;
const fromTimestampCondition = obj.fromTimestamp
? Prisma.sql`AND t."timestamp" >= ${obj.fromTimestamp}::timestamp with time zone at time zone 'UTC'`
: Prisma.empty;
@@ -145,6 +149,7 @@ export default async function handler(
${userCondition}
${nameCondition}
${tagsCondition}
${sessionCondition}
${orderByCondition}
LIMIT ${obj.limit} OFFSET ${skipValue}
) AS t
@@ -168,6 +173,7 @@ export default async function handler(
projectId: authCheck.scope.projectId,
name: obj.name ? obj.name : undefined,
userId: obj.userId ? obj.userId : undefined,
sessionId: obj.sessionId ? obj.sessionId : undefined,
timestamp: obj.fromTimestamp
? { gte: new Date(obj.fromTimestamp) }
: undefined,
@@ -15,6 +15,7 @@ import { Switch } from "@/src/components/ui/switch";
import { env } from "@/src/env.mjs";
import { usePostHogClientCapture } from "@/src/features/posthog-analytics/usePostHogClientCapture";
import { posthogIntegrationFormSchema } from "@/src/features/posthog-integration/types";
import { useHasAccess } from "@/src/features/rbac/utils/checkAccess";
import { api } from "@/src/utils/api";
import { type RouterOutput } from "@/src/utils/types";
import { zodResolver } from "@hookform/resolvers/zod";
@@ -28,7 +29,13 @@ import { type z } from "zod";
export default function PosthogIntegrationSettings() {
const router = useRouter();
const projectId = router.query.projectId as string;
const state = api.posthogIntegration.get.useQuery({ projectId });
const hasAccess = useHasAccess({ projectId, scope: "integrations:CRUD" });
const state = api.posthogIntegration.get.useQuery(
{ projectId },
{
enabled: hasAccess,
},
);
if (env.NEXT_PUBLIC_LANGFUSE_CLOUD_REGION === undefined) return null;
return (
@@ -46,7 +53,7 @@ export default function PosthogIntegrationSettings() {
</Button>
}
status={
state.isInitialLoading
state.isInitialLoading || !hasAccess
? undefined
: state.data?.enabled
? "active"
@@ -63,15 +70,20 @@ export default function PosthogIntegrationSettings() {
on a daily schedule to PostHog. When first activated, it will sync all
historical data from the beginning of your project.
</p>
<div className="flex flex-col gap-10"></div>
{!state.isInitialLoading && (
{!hasAccess && (
<p className="text-sm">
You current role does not grant you access to these settings, please
reach out to your project admin or owner.
</p>
)}
{hasAccess && (
<>
<Header level="h3" title="Configuration" />
<Card className="p-4">
<PostHogIntegrationSettings
state={state.data}
projectId={projectId}
isLoading={state.isLoading}
/>
</Card>
</>
@@ -97,9 +109,11 @@ export default function PosthogIntegrationSettings() {
const PostHogIntegrationSettings = ({
state,
projectId,
isLoading,
}: {
state?: RouterOutput["posthogIntegration"]["get"];
projectId: string;
isLoading: boolean;
}) => {
const capture = usePostHogClientCapture();
const posthogForm = useForm<z.infer<typeof posthogIntegrationFormSchema>>({
@@ -109,6 +123,7 @@ const PostHogIntegrationSettings = ({
posthogProjectApiKey: state?.posthogApiKey ?? "",
enabled: state?.enabled ?? false,
},
disabled: isLoading,
});
useEffect(() => {
@@ -205,12 +220,14 @@ const PostHogIntegrationSettings = ({
<Button
loading={mut.isLoading}
onClick={posthogForm.handleSubmit(onSubmit)}
disabled={isLoading}
>
Save
</Button>
<Button
variant="ghost"
loading={mutDelete.isLoading}
disabled={isLoading || !!!state}
onClick={() => {
if (
confirm(
+42 -44
View File
@@ -62,56 +62,54 @@ export const sessionRouter = createTRPCRouter({
totalTokens: number;
}>
>(Prisma.sql`
WITH observation_metrics AS (
SELECT
t.session_id,
EXTRACT(EPOCH FROM COALESCE(MAX(o."end_time"), MAX(o."start_time"), MAX(t.timestamp))) - EXTRACT(EPOCH FROM COALESCE(MIN(o."start_time"), MIN(t.timestamp)))::double precision AS "sessionDuration",
SUM(COALESCE(o."calculated_input_cost", 0)) AS "inputCost",
SUM(COALESCE(o."calculated_output_cost", 0)) AS "outputCost",
SUM(COALESCE(o."calculated_total_cost", 0)) AS "totalCost",
SUM(o.prompt_tokens) AS "promptTokens",
SUM(o.completion_tokens) AS "completionTokens",
SUM(o.total_tokens) AS "totalTokens"
FROM traces t
LEFT JOIN observations_view o ON o.trace_id = t.id
WHERE
t."project_id" = ${input.projectId}
AND o."project_id" = ${input.projectId}
AND t.session_id IS NOT NULL
GROUP BY 1
),
trace_metrics AS (
SELECT
session_id,
array_agg(distinct t.user_id) "userIds",
count(t.id)::int "countTraces"
FROM traces t
WHERE
t."project_id" = ${input.projectId}
AND t.session_id IS NOT NULL
GROUP BY 1
)
SELECT
s.id,
s."created_at" "createdAt",
s. "created_at" AS "createdAt",
s.bookmarked,
s.public,
t."userIds",
t."countTraces",
o."sessionDuration",
COALESCE(o."totalCost", 0) AS "totalCost",
COALESCE(o."inputCost", 0) AS "inputCost",
COALESCE(o."outputCost", 0) AS "outputCost",
COALESCE(o."promptTokens", 0) AS "promptTokens",
COALESCE(o."completionTokens", 0) AS "completionTokens",
COALESCE(o."totalTokens", 0) AS "totalTokens",
t. "userIds",
t. "countTraces",
o. "sessionDuration",
o. "totalCost" AS "totalCost",
o. "inputCost" AS "inputCost",
o. "outputCost" AS "outputCost",
o. "promptTokens" AS "promptTokens",
o. "completionTokens" AS "completionTokens",
o. "totalTokens" AS "totalTokens",
(count(*) OVER ())::int AS "totalCount"
FROM trace_sessions s
LEFT JOIN trace_metrics t ON t.session_id = s.id
LEFT JOIN observation_metrics o ON o.session_id = s.id
FROM
trace_sessions AS s
LEFT JOIN LATERAL (
SELECT
t.session_id,
MAX(t. "timestamp") AS "max_timestamp",
MIN(t. "timestamp") AS "min_timestamp",
array_agg(t.id) AS "traceIds",
array_agg(DISTINCT t.user_id) AS "userIds",
count(t.id)::int AS "countTraces"
FROM
traces t
WHERE
t.project_id = ${input.projectId}
AND t.session_id = s.id
GROUP BY
t.session_id) AS t ON TRUE
LEFT JOIN LATERAL (
SELECT
EXTRACT(EPOCH FROM COALESCE(MAX(o. "end_time"), MAX(o. "start_time"), t. "max_timestamp")) - EXTRACT(EPOCH FROM COALESCE(MIN(o. "start_time"), t. "min_timestamp"))::double precision AS "sessionDuration",
SUM(COALESCE(o. "calculated_input_cost", 0)) AS "inputCost",
SUM(COALESCE(o. "calculated_output_cost", 0)) AS "outputCost",
SUM(COALESCE(o. "calculated_total_cost", 0)) AS "totalCost",
SUM(o.prompt_tokens) AS "promptTokens",
SUM(o.completion_tokens) AS "completionTokens",
SUM(o.total_tokens) AS "totalTokens"
FROM
observations_view o
WHERE
o.project_id = ${input.projectId}
AND o.trace_id = ANY (t. "traceIds")) AS o ON TRUE
WHERE
s."project_id" = ${input.projectId}
s. "project_id" = ${input.projectId}
${filterCondition}
${orderByCondition}
LIMIT ${input.limit}
+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.7",
"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.7";
+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 {