Skip to content

Commit

Permalink
[OPIK-536]: playground FE; (#888)
Browse files Browse the repository at this point in the history
* [OPIK-536]: fix merge conflicts;

* [OPIK-536]: playground ui improvements;

* [OPIK-536]: fix the comments from the product [2];

* [OPIK-536]: update the color of a placeholder in select;

* [OPIK-536]: update the color of a placeholder in select;

* [OPIK-536]: update the openai api key;

* [OPIK-536]: finish the functionality of playground;

* [OPIK-536]: eslint fixes;

* [OPIK-536]: eslint fixes;

* [OPIK-536]: update package-lock.json;

* [OPIK-536]: merge problems fixes;

* [OPIK-536]: fix a type;

* [OPIK-536]: remove an old file and comments;

* [OPIK-536]: improve code readability;

* [OPIK-536]: add more models;

* [OPIK-536]: remove a useless comment;

* [OPIK-536]: hide playground if not ready for users;

* [OPIK-536]: remove a buggy tooltip;

* [OPIK-536]: fix the min width;

* [OPIK-536]: support tracing for stop button;

* [OPIK-536]: improve code readability;

* [OPIK-536]: pr comment fixes;

* [OPIK-536]: remove use memo;

* [OPIK-536]: fix the package lock json;

---------

Co-authored-by: Sasha <aliaksandr@comet.com>
  • Loading branch information
aadereiko and Sasha authored Dec 13, 2024
1 parent 40a5901 commit 71ed65c
Show file tree
Hide file tree
Showing 33 changed files with 2,832 additions and 92 deletions.
512 changes: 512 additions & 0 deletions apps/opik-frontend/package-lock.json

Large diffs are not rendered by default.

2 changes: 2 additions & 0 deletions apps/opik-frontend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@
"@radix-ui/react-popover": "1.1.1",
"@radix-ui/react-select": "^2.1.1",
"@radix-ui/react-separator": "^1.1.0",
"@radix-ui/react-slider": "^1.2.1",
"@radix-ui/react-slot": "^1.1.0",
"@radix-ui/react-switch": "^1.1.0",
"@radix-ui/react-tabs": "^1.1.0",
Expand Down Expand Up @@ -127,6 +128,7 @@
"tailwindcss": "^3.4.3",
"typescript": "^5.4.5",
"vite": "^5.2.11",
"vite-plugin-svgr": "^4.3.0",
"vite-tsconfig-paths": "^4.3.2",
"vitest": "^1.6.0"
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
import { useCallback } from "react";
import { v7 } from "uuid";
import pick from "lodash/pick";

import useSpanCreateMutation from "@/api/traces/useSpanCreateMutation";
import useTraceCreateMutation from "@/api/traces/useTraceCreateMutation";
import { RunStreamingReturn } from "@/api/playground/useOpenApiRunStreaming";

import {
PLAYGROUND_MODEL,
PlaygroundPromptConfigsType,
ProviderMessageType,
} from "@/types/playground";
import { useToast } from "@/components/ui/use-toast";
import { SPAN_TYPE } from "@/types/traces";

const PLAYGROUND_TRACE_SPAN_NAME = "chat_completion_create";

const USAGE_FIELDS_TO_SEND = [
"completion_tokens",
"prompt_tokens",
"total_tokens",
];

const PLAYGROUND_PROJECT_NAME = "playground";

interface CreateTraceSpanParams extends RunStreamingReturn {
model: PLAYGROUND_MODEL | "";
providerMessages: ProviderMessageType[];
configs: PlaygroundPromptConfigsType;
}

const useCreateOutputTraceAndSpan = () => {
const { toast } = useToast();

const { mutateAsync: createSpanMutateAsync } = useSpanCreateMutation();
const { mutateAsync: createTraceMutateAsync } = useTraceCreateMutation();

const createTraceSpan = useCallback(
async ({
startTime,
endTime,
result,
usage,
error,
choices,
model,
providerMessages,
configs,
}: CreateTraceSpanParams) => {
const traceId = v7();
const spanId = v7();

try {
await createTraceMutateAsync({
id: traceId,
projectName: PLAYGROUND_PROJECT_NAME,
name: PLAYGROUND_TRACE_SPAN_NAME,
startTime,
endTime,
input: { messages: providerMessages },
output: { output: result || error },
});

await createSpanMutateAsync({
id: spanId,
traceId,
projectName: PLAYGROUND_PROJECT_NAME,
type: SPAN_TYPE.llm,
name: PLAYGROUND_TRACE_SPAN_NAME,
startTime,
endTime,
input: { messages: providerMessages },
output: { choices },
usage: !usage ? undefined : pick(usage, USAGE_FIELDS_TO_SEND),
metadata: {
created_from: "openai",
usage,
model,
parameters: configs,
},
});
} catch {
toast({
title: "Error",
description: "There was an error while logging data",
variant: "destructive",
});
}
},
[createTraceMutateAsync, createSpanMutateAsync, toast],
);

return createTraceSpan;
};

export default useCreateOutputTraceAndSpan;
197 changes: 197 additions & 0 deletions apps/opik-frontend/src/api/playground/useOpenApiRunStreaming.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,197 @@
import { useCallback, useRef } from "react";

import dayjs from "dayjs";
import { UsageType } from "@/types/shared";
import {
PLAYGROUND_MODEL,
PlaygroundPromptConfigsType,
ProviderMessageType,
ProviderStreamingMessageChoiceType,
ProviderStreamingMessageType,
} from "@/types/playground";
import { safelyParseJSON, snakeCaseObj } from "@/lib/utils";
import { OPENAI_API_KEY } from "@/constants/playground";

interface GetOpenAIStreamParams {
model: PLAYGROUND_MODEL | "";
messages: ProviderMessageType[];
signal: AbortSignal;
configs: PlaygroundPromptConfigsType;
}

const getOpenAIStream = async ({
model,
messages,
signal,
configs,
}: GetOpenAIStreamParams) => {
const apiKey = window.localStorage.getItem(OPENAI_API_KEY) || "";

return fetch("https://api.openai.com/v1/chat/completions", {
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${apiKey}`,
},
body: JSON.stringify({
model,
messages,
stream: true,
stream_options: { include_usage: true },
...snakeCaseObj(configs),
}),
signal: signal,
});
};

const getResponseError = async (response: Response) => {
let error;

try {
error = (await response?.json())?.error?.message;
} catch {
error = "Unexpected error occurred.";
}

return error;
};

export interface RunStreamingReturn {
error: null | string;
result: null | string;
startTime: string;
endTime: string;
usage: UsageType | null;
choices: ProviderStreamingMessageChoiceType[] | null;
}

interface UseOpenApiRunStreamingParameters {
model: PLAYGROUND_MODEL | "";
messages: ProviderMessageType[];
onAddChunk: (accumulatedValue: string) => void;
onLoading: (v: boolean) => void;
onError: (errMsg: string | null) => void;
configs: PlaygroundPromptConfigsType;
}

const useOpenApiRunStreaming = ({
model,
messages,
configs,
onAddChunk,
onLoading,
onError,
}: UseOpenApiRunStreamingParameters) => {
const abortControllerRef = useRef<AbortController | null>(null);

const stop = useCallback(() => {
abortControllerRef.current?.abort();
abortControllerRef.current = null;
}, []);

const runStreaming = useCallback(async (): Promise<RunStreamingReturn> => {
const startTime = dayjs().utc().toISOString();
let accumulatedValue = "";
let usage = null;
let choices: ProviderStreamingMessageChoiceType[] = [];

onLoading(true);
onError(null);

try {
abortControllerRef.current = new AbortController();

const response = await getOpenAIStream({
model,
messages,
configs,
signal: abortControllerRef.current?.signal,
});

if (!response.ok || !response) {
const error = await getResponseError(response);
onError(error);
onLoading(false);

const endTime = dayjs().utc().toISOString();

return {
error,
result: null,
startTime,
endTime,
usage: null,
choices: null,
};
}

const reader = response?.body?.getReader();
const decoder = new TextDecoder("utf-8");

let done = false;

while (!done && reader) {
const { value, done: doneReading } = await reader.read();

done = doneReading;

if (value) {
const chunk = decoder.decode(value, { stream: true });
const lines = chunk.split("\n").filter((line) => line.trim() !== "");

for (const line of lines) {
if (line.startsWith("data:")) {
const data = line.replace(/^data:\s*/, "");

// stream finished
if (data === "[DONE]") {
break;
}

const parsed = safelyParseJSON(
data,
) as ProviderStreamingMessageType;

choices = parsed?.choices;
const deltaContent = choices?.[0]?.delta?.content;

if (parsed?.usage) {
usage = parsed.usage as UsageType;
}

if (deltaContent) {
accumulatedValue += deltaContent;
onAddChunk(accumulatedValue);
}
}
}
}
}

return {
startTime,
endTime: dayjs().utc().toISOString(),
result: accumulatedValue,
error: null,
usage,
choices,
};
// abort signal also jumps into here
} catch (error) {
return {
startTime,
endTime: dayjs().utc().toISOString(),
result: accumulatedValue,
error: null,
usage: null,
choices,
};
} finally {
onLoading(false);
}
}, [messages, model, onAddChunk, onError, onLoading, configs]);

return { runStreaming, stop };
};

export default useOpenApiRunStreaming;
67 changes: 67 additions & 0 deletions apps/opik-frontend/src/api/traces/useSpanCreateMutation.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
import { useMutation, useQueryClient } from "@tanstack/react-query";
import { AxiosError } from "axios";
import get from "lodash/get";

import api, { SPANS_KEY, SPANS_REST_ENDPOINT } from "@/api/api";
import { useToast } from "@/components/ui/use-toast";
import { SPAN_TYPE } from "@/types/traces";

import { JsonNode, UsageType } from "@/types/shared";
import { snakeCaseObj } from "@/lib/utils";

type UseSpanCreateMutationParams = {
id: string;
projectName?: string;
traceId: string;
parentSpanId?: string;
name: string;
type: SPAN_TYPE;
startTime: string;
endTime?: string;
input?: JsonNode;
output?: JsonNode;
model?: string;
provider?: string;
tags?: string[];
usage?: UsageType;
metadata?: object;
};

const useSpanCreateMutation = () => {
const queryClient = useQueryClient();
const { toast } = useToast();

return useMutation({
mutationFn: async (span: UseSpanCreateMutationParams) => {
const { data } = await api.post(SPANS_REST_ENDPOINT, snakeCaseObj(span));

return data;
},
onError: (error: AxiosError) => {
const message = get(
error,
["response", "data", "message"],
error.message,
);

toast({
title: "Error",
description: message,
variant: "destructive",
});
},
onSettled: (data, error, variables) => {
if (variables.projectName) {
queryClient.invalidateQueries({
queryKey: ["projects"],
});
}

queryClient.invalidateQueries({
queryKey: [SPANS_KEY],
});
},
});
};

export default useSpanCreateMutation;
Loading

0 comments on commit 71ed65c

Please sign in to comment.