From def0955aa384c8a2b1b83d07d00569c51eb72ec6 Mon Sep 17 00:00:00 2001 From: sasha Date: Wed, 18 Dec 2024 17:36:52 +0100 Subject: [PATCH] [OPIK-539]: add a config tab and proxy; (#921) * [OPIK-516]: add a configuration page and a proxy support; * [OPIK-516]: eslint issues; * [OPIK-516]: update the name of a variable; --------- Co-authored-by: Sasha --- apps/opik-frontend/e2e/fixtures/pages.ts | 19 +- .../ConfigurationPage/ConfigurationPage.ts | 9 + .../FeedbackDefinitionsTab.ts} | 14 +- apps/opik-frontend/e2e/pages/index.ts | 3 +- .../feedback-definition.spec.ts | 68 +++--- apps/opik-frontend/src/api/api.ts | 4 +- .../playground/useCompletionProxyStreaming.ts | 195 +++++++++++++++++ .../playground/useCreateOutputTraceAndSpan.ts | 10 +- .../api/playground/useOpenApiRunStreaming.ts | 197 ----------------- .../src/api/provider-keys/useProviderKeys.tsx | 35 +++ .../useProviderKeysCreateMutation.ts | 51 +++++ .../useProviderKeysDeleteMutation.ts | 44 ++++ .../useProviderKeysUpdateMutation.ts | 51 +++++ .../src/components/layout/SideBar/SideBar.tsx | 202 ++++++++---------- .../AIProvidersTab/AIProviderCell.tsx | 26 +++ .../AIProvidersRowActionsCell.tsx | 87 ++++++++ .../AIProvidersTab/AIProvidersTab.tsx | 153 +++++++++++++ .../ConfigurationPage/ConfigurationPage.tsx | 61 ++++++ .../FeedbackDefinitionsActionsPanel.tsx | 0 .../FeedbackDefinitionsRowActionsCell.tsx | 0 .../FeedbackDefinitionsTab.tsx} | 27 ++- .../FeedbackDefinitionsValueCell.tsx | 26 +-- .../PlaygroundOutputs/PlaygroundOutput.tsx | 30 ++- .../PlaygroundOutputs/PlaygroundOutputs.tsx | 36 +++- .../pages/PlaygroundPage/PlaygroundPage.tsx | 106 +++++++-- .../PlaygroundPrompt/PlaygroundPrompt.tsx | 22 +- .../PlaygroundPromptMessage.tsx | 18 +- .../PromptModelSelect/PromptModelSelect.tsx | 151 ++++++++----- .../PromptModelConfigs.tsx | 13 +- .../providerConfigs/OpenAIModelConfigs.tsx | 34 +-- .../pages/PromptsPage/PromptsPage.tsx | 4 +- .../AddEditAIProviderDialog.tsx | 139 ++++++++++++ .../components/shared/EyeInput/EyeInput.tsx | 40 ++++ .../components/shared/SelectBox/SelectBox.tsx | 27 ++- .../src/components/ui/select.tsx | 24 ++- .../opik-frontend/src/constants/playground.ts | 68 +++--- apps/opik-frontend/src/constants/providers.ts | 30 +++ .../src/icons/integrations/openai.svg | 2 +- apps/opik-frontend/src/lib/playground.ts | 25 +-- apps/opik-frontend/src/lib/provider.ts | 8 + apps/opik-frontend/src/router.tsx | 30 ++- apps/opik-frontend/src/types/playground.ts | 74 ++----- apps/opik-frontend/src/types/providers.ts | 56 +++++ 43 files changed, 1535 insertions(+), 684 deletions(-) create mode 100644 apps/opik-frontend/e2e/pages/ConfigurationPage/ConfigurationPage.ts rename apps/opik-frontend/e2e/pages/{FeedbackDefinitionsPage.ts => ConfigurationPage/FeedbackDefinitionsTab.ts} (90%) rename apps/opik-frontend/e2e/tests/{feedback-definitions => configuration}/feedback-definition.spec.ts (50%) create mode 100644 apps/opik-frontend/src/api/playground/useCompletionProxyStreaming.ts delete mode 100644 apps/opik-frontend/src/api/playground/useOpenApiRunStreaming.ts create mode 100644 apps/opik-frontend/src/api/provider-keys/useProviderKeys.tsx create mode 100644 apps/opik-frontend/src/api/provider-keys/useProviderKeysCreateMutation.ts create mode 100644 apps/opik-frontend/src/api/provider-keys/useProviderKeysDeleteMutation.ts create mode 100644 apps/opik-frontend/src/api/provider-keys/useProviderKeysUpdateMutation.ts create mode 100644 apps/opik-frontend/src/components/pages/ConfigurationPage/AIProvidersTab/AIProviderCell.tsx create mode 100644 apps/opik-frontend/src/components/pages/ConfigurationPage/AIProvidersTab/AIProvidersRowActionsCell.tsx create mode 100644 apps/opik-frontend/src/components/pages/ConfigurationPage/AIProvidersTab/AIProvidersTab.tsx create mode 100644 apps/opik-frontend/src/components/pages/ConfigurationPage/ConfigurationPage.tsx rename apps/opik-frontend/src/components/pages/{FeedbackDefinitionsPage => ConfigurationPage/FeedbackDefinitionsTab}/FeedbackDefinitionsActionsPanel.tsx (100%) rename apps/opik-frontend/src/components/pages/{FeedbackDefinitionsPage => ConfigurationPage/FeedbackDefinitionsTab}/FeedbackDefinitionsRowActionsCell.tsx (100%) rename apps/opik-frontend/src/components/pages/{FeedbackDefinitionsPage/FeedbackDefinitionsPage.tsx => ConfigurationPage/FeedbackDefinitionsTab/FeedbackDefinitionsTab.tsx} (91%) rename apps/opik-frontend/src/components/pages/{FeedbackDefinitionsPage => ConfigurationPage/FeedbackDefinitionsTab}/FeedbackDefinitionsValueCell.tsx (61%) create mode 100644 apps/opik-frontend/src/components/shared/AddEditAIProviderDialog/AddEditAIProviderDialog.tsx create mode 100644 apps/opik-frontend/src/components/shared/EyeInput/EyeInput.tsx create mode 100644 apps/opik-frontend/src/constants/providers.ts create mode 100644 apps/opik-frontend/src/lib/provider.ts create mode 100644 apps/opik-frontend/src/types/providers.ts diff --git a/apps/opik-frontend/e2e/fixtures/pages.ts b/apps/opik-frontend/e2e/fixtures/pages.ts index f358335d52..c7e41901f5 100644 --- a/apps/opik-frontend/e2e/fixtures/pages.ts +++ b/apps/opik-frontend/e2e/fixtures/pages.ts @@ -1,7 +1,7 @@ import { DatasetItemsPage, DatasetsPage, - FeedbackDefinitionsPage, + FeedbackDefinitionsTab, ProjectsPage, TracesPage, } from "@e2e/pages"; @@ -11,13 +11,15 @@ import { PlaywrightWorkerArgs, PlaywrightWorkerOptions, } from "@playwright/test"; +import { ConfigurationPage } from "@e2e/pages/ConfigurationPage/ConfigurationPage"; export type PagesFixtures = { datasetsPage: DatasetsPage; datasetItemsPage: DatasetItemsPage; - feedbackDefinitionsPage: FeedbackDefinitionsPage; projectsPage: ProjectsPage; tracesPage: TracesPage; + configurationPage: ConfigurationPage; + feedbackDefinitionsTab: FeedbackDefinitionsTab; }; export const pagesFixtures: Fixtures< @@ -32,13 +34,20 @@ export const pagesFixtures: Fixtures< datasetItemsPage: async ({ page }, use) => { await use(new DatasetItemsPage(page)); }, - feedbackDefinitionsPage: async ({ page }, use) => { - await use(new FeedbackDefinitionsPage(page)); - }, + projectsPage: async ({ page }, use) => { await use(new ProjectsPage(page)); }, + tracesPage: async ({ page }, use) => { await use(new TracesPage(page)); }, + + configurationPage: async ({ page }, use) => { + await use(new ConfigurationPage(page)); + }, + + feedbackDefinitionsTab: async ({ page }, use) => { + await use(new FeedbackDefinitionsTab(page)); + }, }; diff --git a/apps/opik-frontend/e2e/pages/ConfigurationPage/ConfigurationPage.ts b/apps/opik-frontend/e2e/pages/ConfigurationPage/ConfigurationPage.ts new file mode 100644 index 0000000000..967646b36c --- /dev/null +++ b/apps/opik-frontend/e2e/pages/ConfigurationPage/ConfigurationPage.ts @@ -0,0 +1,9 @@ +import { Page } from "@playwright/test"; + +export class ConfigurationPage { + constructor(readonly page: Page) {} + + async goto() { + await this.page.goto(`/default/configuration`); + } +} diff --git a/apps/opik-frontend/e2e/pages/FeedbackDefinitionsPage.ts b/apps/opik-frontend/e2e/pages/ConfigurationPage/FeedbackDefinitionsTab.ts similarity index 90% rename from apps/opik-frontend/e2e/pages/FeedbackDefinitionsPage.ts rename to apps/opik-frontend/e2e/pages/ConfigurationPage/FeedbackDefinitionsTab.ts index c0d1f334dd..994f94df01 100644 --- a/apps/opik-frontend/e2e/pages/FeedbackDefinitionsPage.ts +++ b/apps/opik-frontend/e2e/pages/ConfigurationPage/FeedbackDefinitionsTab.ts @@ -6,25 +6,23 @@ import { FeedbackDefinitionData, FeedbackDefinitionNumericalDetails, } from "@e2e/entities"; -import { Search } from "./components/Search"; -import { Table } from "./components/Table"; +import { Search } from "../components/Search"; +import { Table } from "../components/Table"; import { Columns } from "@e2e/pages/components/Columns"; -export class FeedbackDefinitionsPage { - readonly title: Locator; +export class FeedbackDefinitionsTab { readonly search: Search; readonly table: Table; readonly columns: Columns; constructor(readonly page: Page) { - this.title = page.getByRole("heading", { name: "Feedback definitions" }); this.search = new Search(page); this.table = new Table(page); this.columns = new Columns(page); } async goto() { - await this.page.goto("/default/feedback-definitions"); + await this.page.goto("/default/configuration?tab=feedback-definitions"); } async fillCategoricalData(details: FeedbackDefinitionCategoricalDetails) { @@ -121,7 +119,7 @@ export class FeedbackDefinitionsPage { .getRowLocatorByCellText(data.name) .locator("[data-cell-id$='_values']"); const details = data.details as FeedbackDefinitionNumericalDetails; - await expect(cell).toHaveText(`Min${details.min}Max${details.max}`); + await expect(cell).toHaveText(`Min: ${details.min}, Max: ${details.max}`); } async checkCategoricalValueColumn(data: FeedbackDefinitionData) { @@ -130,7 +128,7 @@ export class FeedbackDefinitionsPage { .locator("[data-cell-id$='_values']"); const details = data.details as FeedbackDefinitionCategoricalDetails; await expect(cell).toHaveText( - Object.keys(details.categories).sort().join(""), + Object.keys(details.categories).sort().join(", "), ); } diff --git a/apps/opik-frontend/e2e/pages/index.ts b/apps/opik-frontend/e2e/pages/index.ts index 78362915b7..01018bcd97 100644 --- a/apps/opik-frontend/e2e/pages/index.ts +++ b/apps/opik-frontend/e2e/pages/index.ts @@ -1,5 +1,6 @@ export * from "./DatasetsPage"; export * from "./DatasetItemsPage"; -export * from "./FeedbackDefinitionsPage"; +export * from "./ConfigurationPage/ConfigurationPage"; +export * from "./ConfigurationPage/FeedbackDefinitionsTab"; export * from "./ProjectsPage"; export * from "./TracesPage"; diff --git a/apps/opik-frontend/e2e/tests/feedback-definitions/feedback-definition.spec.ts b/apps/opik-frontend/e2e/tests/configuration/feedback-definition.spec.ts similarity index 50% rename from apps/opik-frontend/e2e/tests/feedback-definitions/feedback-definition.spec.ts rename to apps/opik-frontend/e2e/tests/configuration/feedback-definition.spec.ts index 15388af492..ceba2435b6 100644 --- a/apps/opik-frontend/e2e/tests/feedback-definitions/feedback-definition.spec.ts +++ b/apps/opik-frontend/e2e/tests/configuration/feedback-definition.spec.ts @@ -1,4 +1,4 @@ -import { expect, test } from "@e2e/fixtures"; +import { test } from "@e2e/fixtures"; import { CATEGORICAL_FEEDBACK_DEFINITION, CATEGORICAL_FEEDBACK_DEFINITION_MODIFIED, @@ -9,108 +9,102 @@ import { test.describe("Feedback definitions page", () => { test("Check search", async ({ categoricalFeedbackDefinition, - feedbackDefinitionsPage, + feedbackDefinitionsTab, numericalFeedbackDefinition, }) => { - await feedbackDefinitionsPage.goto(); + await feedbackDefinitionsTab.goto(); + await feedbackDefinitionsTab.table.hasRowCount(2); - await expect(feedbackDefinitionsPage.title).toBeVisible(); - - // search and validate feedback definitions - await feedbackDefinitionsPage.table.hasRowCount(2); - - await feedbackDefinitionsPage.search.search( + await feedbackDefinitionsTab.search.search( categoricalFeedbackDefinition.name, ); - await feedbackDefinitionsPage.table.hasRowCount(1); + await feedbackDefinitionsTab.table.hasRowCount(1); - await feedbackDefinitionsPage.search.search( + await feedbackDefinitionsTab.search.search( numericalFeedbackDefinition.name, ); - await feedbackDefinitionsPage.table.hasRowCount(1); + await feedbackDefinitionsTab.table.hasRowCount(1); - await feedbackDefinitionsPage.search.search("invalid_search_string"); - await feedbackDefinitionsPage.table.hasNoData(); + await feedbackDefinitionsTab.search.search("invalid_search_string"); + await feedbackDefinitionsTab.table.hasNoData(); }); - test("Check adding/deleting of items", async ({ - feedbackDefinitionsPage, - }) => { - await feedbackDefinitionsPage.goto(); + test("Check adding/deleting of items", async ({ feedbackDefinitionsTab }) => { + await feedbackDefinitionsTab.goto(); // create and validate categorical feedback definition - await feedbackDefinitionsPage.addFeedbackDefinition( + await feedbackDefinitionsTab.addFeedbackDefinition( CATEGORICAL_FEEDBACK_DEFINITION, ); - await feedbackDefinitionsPage.table.checkIsExist( + await feedbackDefinitionsTab.table.checkIsExist( CATEGORICAL_FEEDBACK_DEFINITION.name, ); // create and validate numerical feedback definition - await feedbackDefinitionsPage.addFeedbackDefinition( + await feedbackDefinitionsTab.addFeedbackDefinition( NUMERICAL_FEEDBACK_DEFINITION, ); - await feedbackDefinitionsPage.table.checkIsExist( + await feedbackDefinitionsTab.table.checkIsExist( NUMERICAL_FEEDBACK_DEFINITION.name, ); // delete and validate feedback definitions - await feedbackDefinitionsPage.deleteFeedbackDefinition( + await feedbackDefinitionsTab.deleteFeedbackDefinition( CATEGORICAL_FEEDBACK_DEFINITION, ); - await feedbackDefinitionsPage.table.checkIsNotExist( + await feedbackDefinitionsTab.table.checkIsNotExist( CATEGORICAL_FEEDBACK_DEFINITION.name, ); - await feedbackDefinitionsPage.deleteFeedbackDefinition( + await feedbackDefinitionsTab.deleteFeedbackDefinition( NUMERICAL_FEEDBACK_DEFINITION, ); - await feedbackDefinitionsPage.table.checkIsNotExist( + await feedbackDefinitionsTab.table.checkIsNotExist( NUMERICAL_FEEDBACK_DEFINITION.name, ); }); test("Check editing of items", async ({ categoricalFeedbackDefinition, - feedbackDefinitionsPage, + feedbackDefinitionsTab, numericalFeedbackDefinition, }) => { - await feedbackDefinitionsPage.goto(); + await feedbackDefinitionsTab.goto(); // modify and validate numeric to categorical feedback definition - await feedbackDefinitionsPage.editFeedbackDefinition( + await feedbackDefinitionsTab.editFeedbackDefinition( numericalFeedbackDefinition.name, CATEGORICAL_FEEDBACK_DEFINITION_MODIFIED, ); - await feedbackDefinitionsPage.table.checkIsExist( + await feedbackDefinitionsTab.table.checkIsExist( CATEGORICAL_FEEDBACK_DEFINITION_MODIFIED.name, ); // modify and validate categorical to numeric feedback definition - await feedbackDefinitionsPage.editFeedbackDefinition( + await feedbackDefinitionsTab.editFeedbackDefinition( categoricalFeedbackDefinition.name, NUMERICAL_FEEDBACK_DEFINITION_MODIFIED, ); - await feedbackDefinitionsPage.table.checkIsExist( + await feedbackDefinitionsTab.table.checkIsExist( NUMERICAL_FEEDBACK_DEFINITION_MODIFIED.name, ); }); test("Check values column", async ({ categoricalFeedbackDefinition, - feedbackDefinitionsPage, + feedbackDefinitionsTab, numericalFeedbackDefinition, }) => { - await feedbackDefinitionsPage.goto(); + await feedbackDefinitionsTab.goto(); - await feedbackDefinitionsPage.columns.selectAll(); + await feedbackDefinitionsTab.columns.selectAll(); - await feedbackDefinitionsPage.checkNumericValueColumn( + await feedbackDefinitionsTab.checkNumericValueColumn( numericalFeedbackDefinition, ); - await feedbackDefinitionsPage.checkCategoricalValueColumn( + await feedbackDefinitionsTab.checkCategoricalValueColumn( categoricalFeedbackDefinition, ); }); diff --git a/apps/opik-frontend/src/api/api.ts b/apps/opik-frontend/src/api/api.ts index 4fb26b5942..7cc278abca 100644 --- a/apps/opik-frontend/src/api/api.ts +++ b/apps/opik-frontend/src/api/api.ts @@ -1,7 +1,7 @@ import { UseQueryOptions } from "@tanstack/react-query"; import axios from "axios"; -const BASE_API_URL = import.meta.env.VITE_BASE_API_URL || "/api"; +export const BASE_API_URL = import.meta.env.VITE_BASE_API_URL || "/api"; const axiosInstance = axios.create({ baseURL: BASE_API_URL, }); @@ -16,11 +16,13 @@ export const FEEDBACK_DEFINITIONS_REST_ENDPOINT = export const TRACES_REST_ENDPOINT = "/v1/private/traces/"; export const SPANS_REST_ENDPOINT = "/v1/private/spans/"; export const PROMPTS_REST_ENDPOINT = "/v1/private/prompts/"; +export const PROVIDER_KEYS_REST_ENDPOINT = "/v1/private/llm-provider-key/"; export const COMPARE_EXPERIMENTS_KEY = "compare-experiments"; export const SPANS_KEY = "spans"; export const TRACES_KEY = "traces"; export const TRACE_KEY = "trace"; +export const PROVIDERS_KEYS_KEY = "providerKeys"; // stats for feedback export const STATS_COMET_ENDPOINT = "https://stats.comet.com/notify/event/"; diff --git a/apps/opik-frontend/src/api/playground/useCompletionProxyStreaming.ts b/apps/opik-frontend/src/api/playground/useCompletionProxyStreaming.ts new file mode 100644 index 0000000000..61d0fe6268 --- /dev/null +++ b/apps/opik-frontend/src/api/playground/useCompletionProxyStreaming.ts @@ -0,0 +1,195 @@ +import { useCallback, useRef } from "react"; + +import dayjs from "dayjs"; +import { UsageType } from "@/types/shared"; +import { + PlaygroundPromptConfigsType, + ProviderMessageType, + ChatCompletionMessageChoiceType, + ChatCompletionResponse, + ChatCompletionErrorMessageType, + ChatCompletionSuccessMessageType, + ChatCompletionProxyErrorMessageType, +} from "@/types/playground"; +import { safelyParseJSON, snakeCaseObj } from "@/lib/utils"; +import { BASE_API_URL } from "@/api/api"; +import { PROVIDER_MODEL_TYPE } from "@/types/providers"; + +interface GetCompletionProxyStreamParams { + model: PROVIDER_MODEL_TYPE | ""; + messages: ProviderMessageType[]; + signal: AbortSignal; + configs: PlaygroundPromptConfigsType; + workspaceName: string; +} + +const getCompletionProxyStream = async ({ + model, + messages, + signal, + configs, + workspaceName, +}: GetCompletionProxyStreamParams) => { + return fetch(`${BASE_API_URL}/v1/private/chat/completions`, { + method: "POST", + headers: { + "Content-Type": "application/json", + "Comet-Workspace": workspaceName, + }, + body: JSON.stringify({ + model, + messages, + stream: true, + stream_options: { include_usage: true }, + ...snakeCaseObj(configs), + }), + credentials: "include", + signal, + }); +}; + +export interface RunStreamingReturn { + result: null | string; + startTime: string; + endTime: string; + usage: UsageType | null; + choices: ChatCompletionMessageChoiceType[] | null; + platformError: null | string; + proxyError: null | string; +} + +interface UseCompletionProxyStreamingParameters { + model: PROVIDER_MODEL_TYPE | ""; + messages: ProviderMessageType[]; + onAddChunk: (accumulatedValue: string) => void; + configs: PlaygroundPromptConfigsType; + workspaceName: string; +} + +const useCompletionProxyStreaming = ({ + model, + messages, + configs, + onAddChunk, + workspaceName, +}: UseCompletionProxyStreamingParameters) => { + const abortControllerRef = useRef(null); + + const stop = useCallback(() => { + abortControllerRef.current?.abort(); + abortControllerRef.current = null; + }, []); + + const runStreaming = useCallback(async (): Promise => { + const startTime = dayjs().utc().toISOString(); + + let accumulatedValue = ""; + let usage = null; + let choices: ChatCompletionMessageChoiceType[] = []; + + // errors + let proxyError = null; + let platformError = null; + + try { + abortControllerRef.current = new AbortController(); + + const response = await getCompletionProxyStream({ + model, + messages, + configs, + signal: abortControllerRef.current?.signal, + workspaceName, + }); + + const reader = response?.body?.getReader(); + const decoder = new TextDecoder("utf-8"); + + const handleSuccessMessage = ( + parsed: ChatCompletionSuccessMessageType, + ) => { + choices = parsed?.choices; + const deltaContent = choices?.[0]?.delta?.content; + + if (parsed?.usage) { + usage = parsed.usage as UsageType; + } + + if (deltaContent) { + accumulatedValue += deltaContent; + onAddChunk(accumulatedValue); + } + }; + + const handleAIPlatformErrorMessage = ( + parsedMessage: ChatCompletionErrorMessageType, + ) => { + const message = safelyParseJSON(parsedMessage?.message); + + platformError = message?.error?.message; + }; + + const handleProxyErrorMessage = ( + parsedMessage: ChatCompletionProxyErrorMessageType, + ) => { + proxyError = parsedMessage.errors.join(" "); + }; + + // an analogue of true && reader + // we need it to wait till the stream is closed + while (reader) { + const { done, value } = await reader.read(); + + if (done || proxyError || platformError) { + break; + } + + const chunk = decoder.decode(value, { stream: true }); + const lines = chunk.split("\n").filter((line) => line.trim() !== ""); + + for (const line of lines) { + const parsed = safelyParseJSON(line) as ChatCompletionResponse; + + // handle different message types + if ("errors" in parsed) { + handleProxyErrorMessage(parsed); + } else if ("code" in parsed) { + handleAIPlatformErrorMessage(parsed); + } else { + handleSuccessMessage(parsed); + } + } + } + return { + startTime, + endTime: dayjs().utc().toISOString(), + result: accumulatedValue, + platformError, + proxyError, + usage, + choices, + }; + // abort signal also jumps into here + } catch (error) { + const typedError = error as Error; + const isStopped = typedError.name === "AbortError"; + + // no error if a run has been stopped + const defaultErrorMessage = isStopped ? null : "Unexpected error"; + + return { + startTime, + endTime: dayjs().utc().toISOString(), + result: accumulatedValue, + platformError, + proxyError: proxyError || defaultErrorMessage, + usage: null, + choices, + }; + } + }, [messages, model, onAddChunk, configs, workspaceName]); + + return { runStreaming, stop }; +}; + +export default useCompletionProxyStreaming; diff --git a/apps/opik-frontend/src/api/playground/useCreateOutputTraceAndSpan.ts b/apps/opik-frontend/src/api/playground/useCreateOutputTraceAndSpan.ts index 6250e03add..41461cb70d 100644 --- a/apps/opik-frontend/src/api/playground/useCreateOutputTraceAndSpan.ts +++ b/apps/opik-frontend/src/api/playground/useCreateOutputTraceAndSpan.ts @@ -4,15 +4,15 @@ import pick from "lodash/pick"; import useSpanCreateMutation from "@/api/traces/useSpanCreateMutation"; import useTraceCreateMutation from "@/api/traces/useTraceCreateMutation"; -import { RunStreamingReturn } from "@/api/playground/useOpenApiRunStreaming"; +import { RunStreamingReturn } from "@/api/playground/useCompletionProxyStreaming"; import { - PLAYGROUND_MODEL, PlaygroundPromptConfigsType, ProviderMessageType, } from "@/types/playground"; import { useToast } from "@/components/ui/use-toast"; import { SPAN_TYPE } from "@/types/traces"; +import { PROVIDER_MODEL_TYPE } from "@/types/providers"; const PLAYGROUND_TRACE_SPAN_NAME = "chat_completion_create"; @@ -25,7 +25,7 @@ const USAGE_FIELDS_TO_SEND = [ const PLAYGROUND_PROJECT_NAME = "playground"; interface CreateTraceSpanParams extends RunStreamingReturn { - model: PLAYGROUND_MODEL | ""; + model: PROVIDER_MODEL_TYPE | ""; providerMessages: ProviderMessageType[]; configs: PlaygroundPromptConfigsType; } @@ -42,7 +42,7 @@ const useCreateOutputTraceAndSpan = () => { endTime, result, usage, - error, + platformError, choices, model, providerMessages, @@ -59,7 +59,7 @@ const useCreateOutputTraceAndSpan = () => { startTime, endTime, input: { messages: providerMessages }, - output: { output: result || error }, + output: { output: result || platformError }, }); await createSpanMutateAsync({ diff --git a/apps/opik-frontend/src/api/playground/useOpenApiRunStreaming.ts b/apps/opik-frontend/src/api/playground/useOpenApiRunStreaming.ts deleted file mode 100644 index f62149bbb6..0000000000 --- a/apps/opik-frontend/src/api/playground/useOpenApiRunStreaming.ts +++ /dev/null @@ -1,197 +0,0 @@ -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(null); - - const stop = useCallback(() => { - abortControllerRef.current?.abort(); - abortControllerRef.current = null; - }, []); - - const runStreaming = useCallback(async (): Promise => { - 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; diff --git a/apps/opik-frontend/src/api/provider-keys/useProviderKeys.tsx b/apps/opik-frontend/src/api/provider-keys/useProviderKeys.tsx new file mode 100644 index 0000000000..47e3b3c669 --- /dev/null +++ b/apps/opik-frontend/src/api/provider-keys/useProviderKeys.tsx @@ -0,0 +1,35 @@ +import { QueryFunctionContext, useQuery } from "@tanstack/react-query"; +import api, { + PROVIDER_KEYS_REST_ENDPOINT, + PROVIDERS_KEYS_KEY, + QueryConfig, +} from "@/api/api"; +import { ProviderKey } from "@/types/providers"; + +type UseProviderKeysListParams = { + workspaceName: string; +}; + +type UseProviderKeysListResponse = { + content: ProviderKey[]; + total: number; +}; + +const getProviderKeys = async ({ signal }: QueryFunctionContext) => { + const { data } = await api.get(PROVIDER_KEYS_REST_ENDPOINT, { + signal, + }); + + return data; +}; + +export default function useProviderKeys( + params: UseProviderKeysListParams, + options?: QueryConfig, +) { + return useQuery({ + queryKey: [PROVIDERS_KEYS_KEY, params], + queryFn: (context) => getProviderKeys(context), + ...options, + }); +} diff --git a/apps/opik-frontend/src/api/provider-keys/useProviderKeysCreateMutation.ts b/apps/opik-frontend/src/api/provider-keys/useProviderKeysCreateMutation.ts new file mode 100644 index 0000000000..c047b92295 --- /dev/null +++ b/apps/opik-frontend/src/api/provider-keys/useProviderKeysCreateMutation.ts @@ -0,0 +1,51 @@ +import { useMutation, useQueryClient } from "@tanstack/react-query"; +import get from "lodash/get"; +import api, { + PROVIDER_KEYS_REST_ENDPOINT, + PROVIDERS_KEYS_KEY, +} from "@/api/api"; +import { AxiosError } from "axios"; +import { useToast } from "@/components/ui/use-toast"; +import { ProviderKeyWithAPIKey } from "@/types/providers"; + +type UseProviderKeysCreateMutationParams = { + providerKey: Partial; +}; + +const useProviderKeysCreateMutation = () => { + const queryClient = useQueryClient(); + const { toast } = useToast(); + + return useMutation({ + mutationFn: async ({ + providerKey, + }: UseProviderKeysCreateMutationParams) => { + const { data } = await api.post(PROVIDER_KEYS_REST_ENDPOINT, { + provider: providerKey.provider, + api_key: providerKey.apiKey, + }); + + return data; + }, + onError: (error: AxiosError) => { + const message = get( + error, + ["response", "data", "errors", "0"], + error.message, + ); + + toast({ + title: "Error", + description: message, + variant: "destructive", + }); + }, + onSettled: () => { + return queryClient.invalidateQueries({ + queryKey: [PROVIDERS_KEYS_KEY], + }); + }, + }); +}; + +export default useProviderKeysCreateMutation; diff --git a/apps/opik-frontend/src/api/provider-keys/useProviderKeysDeleteMutation.ts b/apps/opik-frontend/src/api/provider-keys/useProviderKeysDeleteMutation.ts new file mode 100644 index 0000000000..309dd086ea --- /dev/null +++ b/apps/opik-frontend/src/api/provider-keys/useProviderKeysDeleteMutation.ts @@ -0,0 +1,44 @@ +import { useMutation, useQueryClient } from "@tanstack/react-query"; +import get from "lodash/get"; +import { useToast } from "@/components/ui/use-toast"; +import api, { + PROVIDER_KEYS_REST_ENDPOINT, + PROVIDERS_KEYS_KEY, +} from "@/api/api"; + +type UseProviderKeysDeleteMutationParams = { + providerId: string; +}; + +const useProviderKeysDeleteMutation = () => { + const queryClient = useQueryClient(); + const { toast } = useToast(); + + return useMutation({ + mutationFn: async ({ providerId }: UseProviderKeysDeleteMutationParams) => { + const { data } = await api.post(`${PROVIDER_KEYS_REST_ENDPOINT}delete`, { + ids: [providerId], + }); + + return data; + }, + onError: (error) => { + const message = get( + error, + ["response", "data", "message"], + error.message, + ); + + toast({ + title: "Error", + description: message, + variant: "destructive", + }); + }, + onSettled: () => { + return queryClient.invalidateQueries({ queryKey: [PROVIDERS_KEYS_KEY] }); + }, + }); +}; + +export default useProviderKeysDeleteMutation; diff --git a/apps/opik-frontend/src/api/provider-keys/useProviderKeysUpdateMutation.ts b/apps/opik-frontend/src/api/provider-keys/useProviderKeysUpdateMutation.ts new file mode 100644 index 0000000000..b661c24378 --- /dev/null +++ b/apps/opik-frontend/src/api/provider-keys/useProviderKeysUpdateMutation.ts @@ -0,0 +1,51 @@ +import { useMutation, useQueryClient } from "@tanstack/react-query"; +import { AxiosError } from "axios"; +import get from "lodash/get"; + +import api, { + PROVIDER_KEYS_REST_ENDPOINT, + PROVIDERS_KEYS_KEY, +} from "@/api/api"; +import { useToast } from "@/components/ui/use-toast"; +import { ProviderKeyWithAPIKey } from "@/types/providers"; + +type UseProviderKeyUpdateMutationParams = { + providerKey: Partial; +}; + +const useProviderKeysUpdateMutation = () => { + const queryClient = useQueryClient(); + const { toast } = useToast(); + + return useMutation({ + mutationFn: async ({ providerKey }: UseProviderKeyUpdateMutationParams) => { + const { data } = await api.patch( + `${PROVIDER_KEYS_REST_ENDPOINT}${providerKey.id}`, + { + api_key: providerKey.apiKey, + }, + ); + return data; + }, + onError: (error: AxiosError) => { + const message = get( + error, + ["response", "data", "errors", "0"], + error.message, + ); + + toast({ + title: "Error", + description: message, + variant: "destructive", + }); + }, + onSettled: () => { + return queryClient.invalidateQueries({ + queryKey: [PROVIDERS_KEYS_KEY], + }); + }, + }); +}; + +export default useProviderKeysUpdateMutation; diff --git a/apps/opik-frontend/src/components/layout/SideBar/SideBar.tsx b/apps/opik-frontend/src/components/layout/SideBar/SideBar.tsx index 81226e88c0..d83681c1b9 100644 --- a/apps/opik-frontend/src/components/layout/SideBar/SideBar.tsx +++ b/apps/opik-frontend/src/components/layout/SideBar/SideBar.tsx @@ -1,4 +1,4 @@ -import React, { MouseEventHandler, useMemo, useState } from "react"; +import React, { MouseEventHandler, useState } from "react"; import isNumber from "lodash/isNumber"; import { Link, useMatchRoute } from "@tanstack/react-router"; import { @@ -8,12 +8,12 @@ import { GraduationCap, LayoutGrid, LucideIcon, - MessageSquare, PanelLeftClose, MessageCircleQuestion, FileTerminal, LucideHome, Blocks, + Bolt, } from "lucide-react"; import { keepPreviousData } from "@tanstack/react-query"; @@ -21,7 +21,6 @@ import useAppStore from "@/store/AppStore"; import useProjectsList from "@/api/projects/useProjectsList"; import useDatasetsList from "@/api/datasets/useDatasetsList"; import useExperimentsList from "@/api/datasets/useExperimentsList"; -import useFeedbackDefinitionsList from "@/api/feedback-definitions/useFeedbackDefinitionsList"; import { OnChangeFn } from "@/types/shared"; import { Button } from "@/components/ui/button"; import { Separator } from "@/components/ui/separator"; @@ -31,7 +30,6 @@ import Logo from "@/components/layout/Logo/Logo"; import usePluginsStore from "@/store/PluginsStore"; import ProvideFeedbackDialog from "@/components/layout/SideBar/FeedbackDialog/ProvideFeedbackDialog"; import usePromptsList from "@/api/prompts/usePromptsList"; -import { OPENAI_API_KEY } from "@/constants/playground"; enum MENU_ITEM_TYPE { link = "link", @@ -57,6 +55,91 @@ type MenuItemGroup = { const HOME_PATH = "/$workspaceName/home"; +const MENU_ITEMS: MenuItemGroup[] = [ + { + id: "home", + items: [ + { + id: "home", + path: "/$workspaceName/home", + type: MENU_ITEM_TYPE.router, + icon: LucideHome, + label: "Home", + }, + ], + }, + { + id: "observability", + label: "Observability", + items: [ + { + id: "projects", + path: "/$workspaceName/projects", + type: MENU_ITEM_TYPE.router, + icon: LayoutGrid, + label: "Projects", + count: "projects", + }, + ], + }, + { + id: "evaluation", + label: "Evaluation", + items: [ + { + id: "datasets", + path: "/$workspaceName/datasets", + type: MENU_ITEM_TYPE.router, + icon: Database, + label: "Datasets", + count: "datasets", + }, + { + id: "experiments", + path: "/$workspaceName/experiments", + type: MENU_ITEM_TYPE.router, + icon: FlaskConical, + label: "Experiments", + count: "experiments", + }, + ], + }, + { + id: "prompt_engineering", + label: "Prompt engineering", + items: [ + { + id: "prompts", + path: "/$workspaceName/prompts", + type: MENU_ITEM_TYPE.router, + icon: FileTerminal, + label: "Prompt library", + count: "prompts", + }, + { + id: "playground", + path: "/$workspaceName/playground", + type: MENU_ITEM_TYPE.router, + icon: Blocks, + label: "Playground", + }, + ], + }, + { + id: "configuration", + label: "Configuration", + items: [ + { + id: "configuration", + path: "/$workspaceName/configuration", + type: MENU_ITEM_TYPE.router, + icon: Bolt, + label: "Configuration", + }, + ], + }, +]; + type SideBarProps = { expanded: boolean; setExpanded: OnChangeFn; @@ -168,17 +251,6 @@ const SideBar: React.FunctionComponent = ({ enabled: expanded, }, ); - const { data: feedbackDefinitions } = useFeedbackDefinitionsList( - { - workspaceName, - page: 1, - size: 1, - }, - { - placeholderData: keepPreviousData, - enabled: expanded, - }, - ); const { data: promptsData } = usePromptsList( { @@ -196,7 +268,6 @@ const SideBar: React.FunctionComponent = ({ projects: projectData?.total, datasets: datasetsData?.total, experiments: experimentsData?.total, - feedbackDefinitions: feedbackDefinitions?.total, prompts: promptsData?.total, }; @@ -224,103 +295,6 @@ const SideBar: React.FunctionComponent = ({ }, ]; - const mainMenuItems = useMemo(() => { - const menuItems: MenuItemGroup[] = [ - { - id: "home", - items: [ - { - id: "home", - path: "/$workspaceName/home", - type: MENU_ITEM_TYPE.router, - icon: LucideHome, - label: "Home", - }, - ], - }, - { - id: "observability", - label: "Observability", - items: [ - { - id: "projects", - path: "/$workspaceName/projects", - type: MENU_ITEM_TYPE.router, - icon: LayoutGrid, - label: "Projects", - count: "projects", - }, - ], - }, - { - id: "evaluation", - label: "Evaluation", - items: [ - { - id: "datasets", - path: "/$workspaceName/datasets", - type: MENU_ITEM_TYPE.router, - icon: Database, - label: "Datasets", - count: "datasets", - }, - { - id: "experiments", - path: "/$workspaceName/experiments", - type: MENU_ITEM_TYPE.router, - icon: FlaskConical, - label: "Experiments", - count: "experiments", - }, - ], - }, - { - id: "prompt_engineering", - label: "Prompt engineering", - items: [ - { - id: "prompts", - path: "/$workspaceName/prompts", - type: MENU_ITEM_TYPE.router, - icon: FileTerminal, - label: "Prompt library", - count: "prompts", - }, - ], - }, - { - id: "configuration", - label: "Configuration", - items: [ - { - id: "feedback-definitions", - path: "/$workspaceName/feedback-definitions", - type: MENU_ITEM_TYPE.router, - icon: MessageSquare, - label: "Feedback definitions", - count: "feedbackDefinitions", - }, - ], - }, - ]; - - const isSetUpOpenAIApiKey = localStorage.getItem(OPENAI_API_KEY); - - if (isSetUpOpenAIApiKey) { - const playgroundItem = { - id: "playground", - path: "/$workspaceName/playground", - type: MENU_ITEM_TYPE.router, - icon: Blocks, - label: "Playground", - }; - - menuItems[3].items.push(playgroundItem); - } - - return menuItems; - }, []); - const linkClickHandler = (event: React.MouseEvent) => { const target = event.currentTarget; const isActive = target.getAttribute("data-status") === "active"; @@ -427,7 +401,7 @@ const SideBar: React.FunctionComponent = ({ )}
-
    {renderGroups(mainMenuItems)}
+
    {renderGroups(MENU_ITEMS)}
    diff --git a/apps/opik-frontend/src/components/pages/ConfigurationPage/AIProvidersTab/AIProviderCell.tsx b/apps/opik-frontend/src/components/pages/ConfigurationPage/AIProvidersTab/AIProviderCell.tsx new file mode 100644 index 0000000000..474e225bf1 --- /dev/null +++ b/apps/opik-frontend/src/components/pages/ConfigurationPage/AIProvidersTab/AIProviderCell.tsx @@ -0,0 +1,26 @@ +import React from "react"; +import { CellContext } from "@tanstack/react-table"; + +import CellWrapper from "@/components/shared/DataTableCells/CellWrapper"; +import { PROVIDERS } from "@/constants/providers"; +import { PROVIDER_TYPE } from "@/types/providers"; + +const AIProviderCell = (context: CellContext) => { + const provider = context.getValue(); + const Icon = PROVIDERS[provider].icon; + + const providerKeyLabel = PROVIDERS[provider].label; + + return ( + + + {providerKeyLabel} + + ); +}; + +export default AIProviderCell; diff --git a/apps/opik-frontend/src/components/pages/ConfigurationPage/AIProvidersTab/AIProvidersRowActionsCell.tsx b/apps/opik-frontend/src/components/pages/ConfigurationPage/AIProvidersTab/AIProvidersRowActionsCell.tsx new file mode 100644 index 0000000000..1141b02d66 --- /dev/null +++ b/apps/opik-frontend/src/components/pages/ConfigurationPage/AIProvidersTab/AIProvidersRowActionsCell.tsx @@ -0,0 +1,87 @@ +import React, { useCallback, useRef, useState } from "react"; +import { CellContext } from "@tanstack/react-table"; +import { MoreHorizontal, Pencil, Trash } from "lucide-react"; + +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuTrigger, +} from "@/components/ui/dropdown-menu"; +import { Button } from "@/components/ui/button"; +import ConfirmDialog from "@/components/shared/ConfirmDialog/ConfirmDialog"; +import CellWrapper from "@/components/shared/DataTableCells/CellWrapper"; +import AddEditAIProviderDialog from "@/components/shared/AddEditAIProviderDialog/AddEditAIProviderDialog"; +import { ProviderKey } from "@/types/providers"; +import useProviderKeysDeleteMutation from "@/api/provider-keys/useProviderKeysDeleteMutation"; +import { PROVIDERS } from "@/constants/providers"; + +const AIProvidersRowActionsCell: React.FunctionComponent< + CellContext +> = (context) => { + const resetKeyRef = useRef(0); + const providerKey = context.row.original; + const [open, setOpen] = useState(false); + + const { mutate: deleteProviderKey } = useProviderKeysDeleteMutation(); + + const deleteProviderKeyHandler = useCallback(() => { + deleteProviderKey({ + providerId: providerKey.id, + }); + }, [providerKey.id, deleteProviderKey]); + + return ( + + + + + + + + + { + setOpen(2); + resetKeyRef.current = resetKeyRef.current + 1; + }} + > + + Edit + + { + setOpen(1); + resetKeyRef.current = resetKeyRef.current + 1; + }} + > + + Delete + + + + + ); +}; + +export default AIProvidersRowActionsCell; diff --git a/apps/opik-frontend/src/components/pages/ConfigurationPage/AIProvidersTab/AIProvidersTab.tsx b/apps/opik-frontend/src/components/pages/ConfigurationPage/AIProvidersTab/AIProvidersTab.tsx new file mode 100644 index 0000000000..7be0b67315 --- /dev/null +++ b/apps/opik-frontend/src/components/pages/ConfigurationPage/AIProvidersTab/AIProvidersTab.tsx @@ -0,0 +1,153 @@ +import React, { useMemo, useRef, useState } from "react"; +import { keepPreviousData } from "@tanstack/react-query"; +import { ColumnPinningState } from "@tanstack/react-table"; + +import { convertColumnDataToColumn } from "@/lib/table"; +import { ProviderKey } from "@/types/providers"; +import useProviderKeys from "@/api/provider-keys/useProviderKeys"; +import useAppStore from "@/store/AppStore"; +import AddEditAIProviderDialog from "@/components/shared/AddEditAIProviderDialog/AddEditAIProviderDialog"; +import DataTable from "@/components/shared/DataTable/DataTable"; +import DataTableNoData from "@/components/shared/DataTableNoData/DataTableNoData"; +import { formatDate } from "@/lib/date"; +import { PROVIDERS } from "@/constants/providers"; +import AIProviderCell from "@/components/pages/ConfigurationPage/AIProvidersTab/AIProviderCell"; +import { generateActionsColumDef } from "@/components/shared/DataTable/utils"; +import AIProvidersRowActionsCell from "@/components/pages/ConfigurationPage/AIProvidersTab/AIProvidersRowActionsCell"; +import { areAllProvidersConfigured } from "@/lib/provider"; +import Loader from "@/components/shared/Loader/Loader"; +import SearchInput from "@/components/shared/SearchInput/SearchInput"; +import { Button } from "@/components/ui/button"; +import { COLUMN_NAME_ID, COLUMN_TYPE, ColumnData } from "@/types/shared"; + +export const DEFAULT_COLUMNS: ColumnData[] = [ + { + id: COLUMN_NAME_ID, + label: "Name", + type: COLUMN_TYPE.string, + accessorFn: (row) => PROVIDERS[row.provider]?.apiKeyName, + }, + { + id: "created_at", + label: "Created", + type: COLUMN_TYPE.time, + accessorFn: (row) => formatDate(row.created_at), + }, + { + id: "provider", + label: "Provider", + type: COLUMN_TYPE.string, + cell: AIProviderCell as never, + }, +]; + +export const DEFAULT_COLUMN_PINNING: ColumnPinningState = { + left: [COLUMN_NAME_ID], + right: [], +}; + +const AIProvidersTab = () => { + const workspaceName = useAppStore((state) => state.activeWorkspaceName); + + const [search, setSearch] = useState(""); + const resetDialogKeyRef = useRef(0); + const [openDialog, setOpenDialog] = useState(false); + + const { data, isPending } = useProviderKeys( + { + workspaceName, + }, + { + placeholderData: keepPreviousData, + refetchInterval: 30000, + }, + ); + + const providerKeys = useMemo(() => data?.content ?? [], [data?.content]); + + const filteredProviderKeys = useMemo(() => { + if (providerKeys?.length === 0 || search === "") { + return providerKeys; + } + + const searchLowerCase = search.toLowerCase(); + + return providerKeys.filter((p) => { + const providerDetails = PROVIDERS[p.provider]; + + return ( + providerDetails.apiKeyName.toLowerCase().includes(searchLowerCase) || + providerDetails.value.toLowerCase().includes(searchLowerCase) + ); + }); + }, [providerKeys, search]); + + const columns = useMemo(() => { + return [ + ...convertColumnDataToColumn( + DEFAULT_COLUMNS, + {}, + ), + generateActionsColumDef({ + cell: AIProvidersRowActionsCell, + }), + ]; + }, []); + + const handleAddConfigurationClick = () => { + resetDialogKeyRef.current += 1; + setOpenDialog(true); + }; + + const noDataLabel = + search === "" + ? "Configure AI providers to use the playground and online scoring." + : "No search results"; + + if (isPending) { + return ; + } + + return ( + <> +
    +
    + + +
    + + + {search === "" && ( + + )} + + } + /> +
    + + + ); +}; + +export default AIProvidersTab; diff --git a/apps/opik-frontend/src/components/pages/ConfigurationPage/ConfigurationPage.tsx b/apps/opik-frontend/src/components/pages/ConfigurationPage/ConfigurationPage.tsx new file mode 100644 index 0000000000..e8ea4dc2b0 --- /dev/null +++ b/apps/opik-frontend/src/components/pages/ConfigurationPage/ConfigurationPage.tsx @@ -0,0 +1,61 @@ +import React, { useEffect } from "react"; +import AIProvidersTab from "@/components/pages/ConfigurationPage/AIProvidersTab/AIProvidersTab"; +import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; +import { StringParam, useQueryParam } from "use-query-params"; +import FeedbackDefinitionsTab from "@/components/pages/ConfigurationPage/FeedbackDefinitionsTab/FeedbackDefinitionsTab"; + +enum CONFIGURATION_TABS { + FEEDBACK_DEFINITIONS = "feedback-definitions", + AI_PROVIDER = "ai-provider", +} + +const DEFAULT_TAB = CONFIGURATION_TABS.FEEDBACK_DEFINITIONS; + +const ConfigurationPage = () => { + const [tab, setTab] = useQueryParam("tab", StringParam); + + useEffect(() => { + if (!tab) { + setTab(DEFAULT_TAB, "replaceIn"); + } + }, [tab, setTab]); + + return ( +
    +

    Configuration

    + +
    + + + + Feedback definitions + + + AI Providers + + + + + + + + + + + +
    +
    + ); +}; + +export default ConfigurationPage; diff --git a/apps/opik-frontend/src/components/pages/FeedbackDefinitionsPage/FeedbackDefinitionsActionsPanel.tsx b/apps/opik-frontend/src/components/pages/ConfigurationPage/FeedbackDefinitionsTab/FeedbackDefinitionsActionsPanel.tsx similarity index 100% rename from apps/opik-frontend/src/components/pages/FeedbackDefinitionsPage/FeedbackDefinitionsActionsPanel.tsx rename to apps/opik-frontend/src/components/pages/ConfigurationPage/FeedbackDefinitionsTab/FeedbackDefinitionsActionsPanel.tsx diff --git a/apps/opik-frontend/src/components/pages/FeedbackDefinitionsPage/FeedbackDefinitionsRowActionsCell.tsx b/apps/opik-frontend/src/components/pages/ConfigurationPage/FeedbackDefinitionsTab/FeedbackDefinitionsRowActionsCell.tsx similarity index 100% rename from apps/opik-frontend/src/components/pages/FeedbackDefinitionsPage/FeedbackDefinitionsRowActionsCell.tsx rename to apps/opik-frontend/src/components/pages/ConfigurationPage/FeedbackDefinitionsTab/FeedbackDefinitionsRowActionsCell.tsx diff --git a/apps/opik-frontend/src/components/pages/FeedbackDefinitionsPage/FeedbackDefinitionsPage.tsx b/apps/opik-frontend/src/components/pages/ConfigurationPage/FeedbackDefinitionsTab/FeedbackDefinitionsTab.tsx similarity index 91% rename from apps/opik-frontend/src/components/pages/FeedbackDefinitionsPage/FeedbackDefinitionsPage.tsx rename to apps/opik-frontend/src/components/pages/ConfigurationPage/FeedbackDefinitionsTab/FeedbackDefinitionsTab.tsx index 018d69a272..f3547cc9cd 100644 --- a/apps/opik-frontend/src/components/pages/FeedbackDefinitionsPage/FeedbackDefinitionsPage.tsx +++ b/apps/opik-frontend/src/components/pages/ConfigurationPage/FeedbackDefinitionsTab/FeedbackDefinitionsTab.tsx @@ -5,8 +5,8 @@ import capitalize from "lodash/capitalize"; import useFeedbackDefinitionsList from "@/api/feedback-definitions/useFeedbackDefinitionsList"; import AddEditFeedbackDefinitionDialog from "@/components/shared/AddEditFeedbackDefinitionDialog/AddEditFeedbackDefinitionDialog"; -import FeedbackDefinitionsValueCell from "@/components/pages/FeedbackDefinitionsPage/FeedbackDefinitionsValueCell"; -import FeedbackDefinitionsRowActionsCell from "@/components/pages/FeedbackDefinitionsPage/FeedbackDefinitionsRowActionsCell"; +import FeedbackDefinitionsValueCell from "@/components/pages/ConfigurationPage/FeedbackDefinitionsTab/FeedbackDefinitionsValueCell"; +import FeedbackDefinitionsRowActionsCell from "@/components/pages/ConfigurationPage/FeedbackDefinitionsTab/FeedbackDefinitionsRowActionsCell"; import DataTable from "@/components/shared/DataTable/DataTable"; import DataTablePagination from "@/components/shared/DataTablePagination/DataTablePagination"; import DataTableNoData from "@/components/shared/DataTableNoData/DataTableNoData"; @@ -32,7 +32,8 @@ import { generateSelectColumDef, } from "@/components/shared/DataTable/utils"; import { Separator } from "@/components/ui/separator"; -import FeedbackDefinitionsActionsPanel from "@/components/pages/FeedbackDefinitionsPage/FeedbackDefinitionsActionsPanel"; +import FeedbackDefinitionsActionsPanel from "@/components/pages/ConfigurationPage/FeedbackDefinitionsTab/FeedbackDefinitionsActionsPanel"; +import FeedbackScoreNameCell from "@/components/shared/DataTableCells/FeedbackScoreNameCell"; export const getRowId = (f: FeedbackDefinition) => f.id; @@ -80,7 +81,7 @@ export const DEFAULT_COLUMN_PINNING: ColumnPinningState = { export const DEFAULT_SELECTED_COLUMNS: string[] = ["type", "values"]; -const FeedbackDefinitionsPage: React.FunctionComponent = () => { +const FeedbackDefinitionsTab: React.FunctionComponent = () => { const workspaceName = useAppStore((state) => state.activeWorkspaceName); const newFeedbackDefinitionDialogKeyRef = useRef(0); @@ -101,6 +102,7 @@ const FeedbackDefinitionsPage: React.FunctionComponent = () => { }, { placeholderData: keepPreviousData, + refetchInterval: 30000, }, ); @@ -142,10 +144,10 @@ const FeedbackDefinitionsPage: React.FunctionComponent = () => { return [ generateSelectColumDef(), mapColumnDataFields({ - id: COLUMN_NAME_ID, - label: "Name", - type: COLUMN_TYPE.string, - sortable: true, + id: "name", + label: "Feedback score", + type: COLUMN_TYPE.numberDictionary, + cell: FeedbackScoreNameCell as never, }), ...convertColumnDataToColumn( DEFAULT_COLUMNS, @@ -180,12 +182,7 @@ const FeedbackDefinitionsPage: React.FunctionComponent = () => { } return ( -
    -
    -

    - Feedback definitions -

    -
    +
    { ); }; -export default FeedbackDefinitionsPage; +export default FeedbackDefinitionsTab; diff --git a/apps/opik-frontend/src/components/pages/FeedbackDefinitionsPage/FeedbackDefinitionsValueCell.tsx b/apps/opik-frontend/src/components/pages/ConfigurationPage/FeedbackDefinitionsTab/FeedbackDefinitionsValueCell.tsx similarity index 61% rename from apps/opik-frontend/src/components/pages/FeedbackDefinitionsPage/FeedbackDefinitionsValueCell.tsx rename to apps/opik-frontend/src/components/pages/ConfigurationPage/FeedbackDefinitionsTab/FeedbackDefinitionsValueCell.tsx index e7ef798c3b..4f08d4df03 100644 --- a/apps/opik-frontend/src/components/pages/FeedbackDefinitionsPage/FeedbackDefinitionsValueCell.tsx +++ b/apps/opik-frontend/src/components/pages/ConfigurationPage/FeedbackDefinitionsTab/FeedbackDefinitionsValueCell.tsx @@ -1,6 +1,6 @@ import React from "react"; import { CellContext } from "@tanstack/react-table"; -import { Tag } from "@/components/ui/tag"; + import { FEEDBACK_DEFINITION_TYPE, FeedbackDefinition, @@ -17,27 +17,13 @@ const FeedbackDefinitionsValueCell = ( if (feedbackDefinition.type === FEEDBACK_DEFINITION_TYPE.categorical) { items = Object.keys(feedbackDefinition.details.categories || []) .sort() - .map((v) => ( - - {v} - - )); + .join(", "); } else { items = ( - <> -
    - Min - - {feedbackDefinition.details.min} - -
    -
    - Max - - {feedbackDefinition.details.max} - -
    - +

    + Min: {feedbackDefinition.details.min}, Max:{" "} + {feedbackDefinition.details.max} +

    ); } diff --git a/apps/opik-frontend/src/components/pages/PlaygroundPage/PlaygroundOutputs/PlaygroundOutput.tsx b/apps/opik-frontend/src/components/pages/PlaygroundPage/PlaygroundOutputs/PlaygroundOutput.tsx index d9bb4db787..31c24a51aa 100644 --- a/apps/opik-frontend/src/components/pages/PlaygroundPage/PlaygroundOutputs/PlaygroundOutput.tsx +++ b/apps/opik-frontend/src/components/pages/PlaygroundPage/PlaygroundOutputs/PlaygroundOutput.tsx @@ -1,24 +1,24 @@ import React, { forwardRef, useCallback, - useId, useImperativeHandle, useMemo, useState, } from "react"; import { - PLAYGROUND_MODEL, PlaygroundMessageType, PlaygroundPromptConfigsType, } from "@/types/playground"; -import useOpenApiRunStreaming from "@/api/playground/useOpenApiRunStreaming"; +import useCompletionProxyStreaming from "@/api/playground/useCompletionProxyStreaming"; import { getAlphabetLetter } from "@/lib/utils"; import { transformMessageIntoProviderMessage } from "@/lib/playground"; import PlaygroundOutputLoader from "@/components/pages/PlaygroundPage/PlaygroundOutputs/PlaygroundOutputLoader/PlaygroundOutputLoader"; import useCreateOutputTraceAndSpan from "@/api/playground/useCreateOutputTraceAndSpan"; +import useAppStore from "@/store/AppStore"; +import { PROVIDER_MODEL_TYPE } from "@/types/providers"; interface PlaygroundOutputProps { - model: PLAYGROUND_MODEL | ""; + model: PROVIDER_MODEL_TYPE | ""; messages: PlaygroundMessageType[]; index: number; configs: PlaygroundPromptConfigsType; @@ -31,7 +31,8 @@ export interface PlaygroundOutputRef { const PlaygroundOutput = forwardRef( ({ model, messages, index, configs }, ref) => { - const id = useId(); + const workspaceName = useAppStore((state) => state.activeWorkspaceName); + const [isLoading, setIsLoading] = useState(false); const [error, setError] = useState(null); @@ -42,19 +43,30 @@ const PlaygroundOutput = forwardRef( }, [messages]); // @ToDo: when we add providers, add a function to pick the provider - const { runStreaming, stop } = useOpenApiRunStreaming({ + const { runStreaming, stop } = useCompletionProxyStreaming({ model, configs, messages: providerMessages, onAddChunk: setOutputText, - onLoading: setIsLoading, - onError: setError, + workspaceName, }); const createOutputTraceAndSpan = useCreateOutputTraceAndSpan(); const exposedRun = useCallback(async () => { + setError(null); + setIsLoading(true); + setOutputText(null); + const streaming = await runStreaming(); + + setIsLoading(false); + + const error = streaming.platformError || streaming.proxyError; + if (error) { + setError(error); + } + createOutputTraceAndSpan({ ...streaming, model, @@ -91,7 +103,7 @@ const PlaygroundOutput = forwardRef( }; return ( -
    +

    Output {getAlphabetLetter(index)}

    diff --git a/apps/opik-frontend/src/components/pages/PlaygroundPage/PlaygroundOutputs/PlaygroundOutputs.tsx b/apps/opik-frontend/src/components/pages/PlaygroundPage/PlaygroundOutputs/PlaygroundOutputs.tsx index bdbe99d084..3571c07d46 100644 --- a/apps/opik-frontend/src/components/pages/PlaygroundPage/PlaygroundOutputs/PlaygroundOutputs.tsx +++ b/apps/opik-frontend/src/components/pages/PlaygroundPage/PlaygroundOutputs/PlaygroundOutputs.tsx @@ -7,6 +7,7 @@ import { PlaygroundPromptType } from "@/types/playground"; import PlaygroundOutput, { PlaygroundOutputRef, } from "@/components/pages/PlaygroundPage/PlaygroundOutputs/PlaygroundOutput"; +import TooltipWrapper from "@/components/shared/TooltipWrapper/TooltipWrapper"; interface PlaygroundOutputsProps { prompts: PlaygroundPromptType[]; @@ -17,6 +18,8 @@ const PlaygroundOutputs = ({ prompts }: PlaygroundOutputsProps) => { const outputRefs = useRef>(new Map()); + const areAllPromptsValid = prompts.every((p) => !!p.model); + // a recommended by react docs way to work with ref lists // https://react.dev/learn/manipulating-the-dom-with-refs#how-to-manage-a-list-of-refs-using-a-ref-callback const getOutputRefMap = () => { @@ -62,11 +65,34 @@ const PlaygroundOutputs = ({ prompts }: PlaygroundOutputsProps) => { ); } + const isDisabled = !areAllPromptsValid; + const style: React.CSSProperties = isDisabled + ? { pointerEvents: "auto" } + : {}; + + const selectLLMModelMessage = + prompts?.length === 1 + ? "Please select a LLM model for your prompt" + : "Please select a LLM model for your prompts"; + + const runMessage = + prompts?.length === 1 ? "Run your prompt" : "Run your prompts"; + + const tooltipMessage = isDisabled ? selectLLMModelMessage : runMessage; + return ( - + + + ); }; @@ -79,7 +105,7 @@ const PlaygroundOutputs = ({ prompts }: PlaygroundOutputsProps) => {
    {prompts?.map((prompt, promptIdx) => ( ; + setupProviders?: PROVIDER_TYPE[]; +} + +const generateDefaultPrompt = ({ + initPrompt = {}, + setupProviders = [], +}: GenerateDefaultPromptParams): PlaygroundPromptType => { + const defaultProviderKey = first(setupProviders); + const defaultModel = defaultProviderKey + ? PROVIDERS[defaultProviderKey].defaultModel + : ""; -const generateDefaultPrompt = ( - configs: Partial = {}, -): PlaygroundPromptType => { return { name: "Prompt", messages: [generateDefaultPlaygroundPromptMessage()], - model: "", - configs: {}, - ...configs, + model: defaultModel, + configs: defaultProviderKey + ? getDefaultConfigByProvider(defaultProviderKey) + : {}, + ...initPrompt, id: generateRandomString(), }; }; const PlaygroundPage = () => { - const [prompts, setPrompts] = useState([generateDefaultPrompt()]); + const workspaceName = useAppStore((state) => state.activeWorkspaceName); + + const { data: providerKeysData, isPending } = useProviderKeys({ + workspaceName, + }); + + const providerKeys = useMemo(() => { + return providerKeysData?.content?.map((c) => c.provider) || []; + }, [providerKeysData]); + + const [prompts, setPrompts] = useState([]); const handlePromptChange = useCallback( (id: string, changes: Partial) => { setPrompts((ps) => { - return ps.map((prompt) => - prompt.id !== id - ? prompt - : { - ...prompt, - ...changes, - }, - ); + return ps.map((prompt) => { + if (prompt.id !== id) { + return prompt; + } + + const result = { + ...prompt, + ...changes, + }; + + if (changes.model) { + const previousProvider = prompt.model + ? getModelProvider(prompt.model) + : ""; + + const newProvider = changes.model + ? getModelProvider(changes.model) + : ""; + + // if a provider is changed, we need to change configs to default of a new provider + if (newProvider !== previousProvider) { + result.configs = newProvider + ? getDefaultConfigByProvider(newProvider) + : {}; + } + } + + return result; + }); }); }, [], @@ -48,7 +101,7 @@ const PlaygroundPage = () => { const handlePromptDuplicate = useCallback( (prompt: PlaygroundPromptType, position: number) => { setPrompts((ps) => { - const newPrompt = generateDefaultPrompt(prompt); + const newPrompt = generateDefaultPrompt({ initPrompt: prompt }); const newPrompts = [...ps]; @@ -61,11 +114,21 @@ const PlaygroundPage = () => { ); const handleAddPrompt = () => { - const newPrompt = generateDefaultPrompt(); - + const newPrompt = generateDefaultPrompt({ setupProviders: providerKeys }); setPrompts((ps) => [...ps, newPrompt]); }; + useEffect(() => { + // hasn't been initialized yet + if (prompts.length === 0 && !isPending) { + setPrompts([generateDefaultPrompt({ setupProviders: providerKeys })]); + } + }, [prompts, providerKeys, isPending]); + + if (isPending) { + return ; + } + return (
    { onClickRemove={handlePromptRemove} onClickDuplicate={handlePromptDuplicate} hideRemoveButton={prompts.length === 1} + workspaceName={workspaceName} /> ))}
    diff --git a/apps/opik-frontend/src/components/pages/PlaygroundPage/PlaygroundPrompt/PlaygroundPrompt.tsx b/apps/opik-frontend/src/components/pages/PlaygroundPage/PlaygroundPrompt/PlaygroundPrompt.tsx index 21f2a342b5..c0f2424e06 100644 --- a/apps/opik-frontend/src/components/pages/PlaygroundPage/PlaygroundPrompt/PlaygroundPrompt.tsx +++ b/apps/opik-frontend/src/components/pages/PlaygroundPage/PlaygroundPrompt/PlaygroundPrompt.tsx @@ -1,10 +1,9 @@ -import React, { useCallback, useEffect, useRef } from "react"; +import React, { useCallback } from "react"; import { CopyPlus, Trash } from "lucide-react"; import last from "lodash/last"; import { PLAYGROUND_MESSAGE_ROLE, - PLAYGROUND_MODEL, PlaygroundMessageType, PlaygroundPromptConfigsType, PlaygroundPromptType, @@ -14,7 +13,6 @@ import { Separator } from "@/components/ui/separator"; import { generateDefaultPlaygroundPromptMessage, - getDefaultConfigByProvider, getModelProvider, } from "@/lib/playground"; import PlaygroundPromptMessages from "@/components/pages/PlaygroundPage/PlaygroundPrompt/PlaygroundPromptMessages/PlaygroundPromptMessages"; @@ -22,6 +20,7 @@ import PromptModelSelect from "@/components/pages/PlaygroundPage/PlaygroundPromp import { getAlphabetLetter } from "@/lib/utils"; import TooltipWrapper from "@/components/shared/TooltipWrapper/TooltipWrapper"; import PromptModelConfigs from "@/components/pages/PlaygroundPage/PlaygroundPrompt/PromptModelSettings/PromptModelConfigs"; +import { PROVIDER_MODEL_TYPE } from "@/types/providers"; const getNextMessageType = ( previousMessage: PlaygroundMessageType, @@ -34,6 +33,7 @@ const getNextMessageType = ( }; interface PlaygroundPromptProps extends PlaygroundPromptType { + workspaceName: string; index: number; hideRemoveButton: boolean; onChange: (id: string, changes: Partial) => void; @@ -42,6 +42,7 @@ interface PlaygroundPromptProps extends PlaygroundPromptType { } const PlaygroundPrompt = ({ + workspaceName, index, hideRemoveButton, onChange, @@ -53,8 +54,6 @@ const PlaygroundPrompt = ({ const provider = model ? getModelProvider(model) : ""; - const lastProviderNameToInitializeConfig = useRef(provider); - const handleAddMessage = useCallback(() => { const newMessage = generateDefaultPlaygroundPromptMessage(); const lastMessage = last(messages); @@ -88,22 +87,12 @@ const PlaygroundPrompt = ({ ); const handleUpdateModel = useCallback( - (model: PLAYGROUND_MODEL) => { + (model: PROVIDER_MODEL_TYPE) => { onChange(id, { model }); }, [onChange, id], ); - useEffect(() => { - if (lastProviderNameToInitializeConfig.current !== provider) { - lastProviderNameToInitializeConfig.current = provider; - - if (provider) { - handleUpdateConfig(getDefaultConfigByProvider(provider)); - } - } - }, [provider, configs, handleUpdateConfig]); - return (
    @@ -117,6 +106,7 @@ const PlaygroundPrompt = ({ value={model} onChange={handleUpdateModel} provider={provider} + workspaceName={workspaceName} />
    diff --git a/apps/opik-frontend/src/components/pages/PlaygroundPage/PlaygroundPrompt/PlaygroundPromptMessages/PlaygroundPromptMessage.tsx b/apps/opik-frontend/src/components/pages/PlaygroundPage/PlaygroundPrompt/PlaygroundPromptMessages/PlaygroundPromptMessage.tsx index 6f6bb04bd3..a6e2cd2e60 100644 --- a/apps/opik-frontend/src/components/pages/PlaygroundPage/PlaygroundPrompt/PlaygroundPromptMessages/PlaygroundPromptMessage.tsx +++ b/apps/opik-frontend/src/components/pages/PlaygroundPage/PlaygroundPrompt/PlaygroundPromptMessages/PlaygroundPromptMessage.tsx @@ -102,16 +102,14 @@ const PlaygroundPromptMessage = ({ {!hideDragButton && ( - - - + )}
    diff --git a/apps/opik-frontend/src/components/pages/PlaygroundPage/PlaygroundPrompt/PromptModelSelect/PromptModelSelect.tsx b/apps/opik-frontend/src/components/pages/PlaygroundPage/PlaygroundPrompt/PromptModelSelect/PromptModelSelect.tsx index ac7ea05afb..749fd7e209 100644 --- a/apps/opik-frontend/src/components/pages/PlaygroundPage/PlaygroundPrompt/PromptModelSelect/PromptModelSelect.tsx +++ b/apps/opik-frontend/src/components/pages/PlaygroundPage/PlaygroundPrompt/PromptModelSelect/PromptModelSelect.tsx @@ -1,10 +1,10 @@ import React, { useCallback, useMemo, useRef, useState } from "react"; import isNull from "lodash/isNull"; +import pick from "lodash/pick"; + +import { PROVIDER_MODELS } from "@/constants/playground"; +import { PROVIDERS } from "@/constants/providers"; -import { - PLAYGROUND_MODELS, - PLAYGROUND_PROVIDERS, -} from "@/constants/playground"; import { Select, SelectContent, @@ -19,43 +19,67 @@ import { Button } from "@/components/ui/button"; import { ChevronRight, Search } from "lucide-react"; import { Input } from "@/components/ui/input"; -import { PLAYGROUND_MODEL, PLAYGROUND_PROVIDER } from "@/types/playground"; import { Popover, PopoverContent, PopoverTrigger, } from "@/components/ui/popover"; import { cn } from "@/lib/utils"; +import { PROVIDER_MODEL_TYPE, PROVIDER_TYPE } from "@/types/providers"; +import useProviderKeys from "@/api/provider-keys/useProviderKeys"; +import AddEditAIProviderDialog from "@/components/shared/AddEditAIProviderDialog/AddEditAIProviderDialog"; +import { areAllProvidersConfigured } from "@/lib/provider"; interface PromptModelSelectProps { - value: PLAYGROUND_MODEL | ""; - onChange: (value: PLAYGROUND_MODEL) => void; - provider: PLAYGROUND_PROVIDER | ""; + value: PROVIDER_MODEL_TYPE | ""; + workspaceName: string; + onChange: (value: PROVIDER_MODEL_TYPE) => void; + provider: PROVIDER_TYPE | ""; } const PromptModelSelect = ({ value, + workspaceName, onChange, provider, }: PromptModelSelectProps) => { + const resetDialogKeyRef = useRef(0); const inputRef = useRef(null); + + const [openConfigDialog, setOpenConfigDialog] = React.useState(false); const [filterValue, setFilterValue] = useState(""); const [openProviderMenu, setOpenProviderMenu] = useState(null); + const { data } = useProviderKeys({ + workspaceName, + }); + + const configuredProviderKeys = useMemo( + () => data?.content?.map((p) => p.provider) ?? [], + [data?.content], + ); + const groupOptions = useMemo(() => { - return Object.entries(PLAYGROUND_MODELS).map( - ([providerName, providerModels]) => { + const filteredByConfiguredProviders = pick( + PROVIDER_MODELS, + configuredProviderKeys, + ); + + return Object.entries(filteredByConfiguredProviders).map( + ([pn, providerModels]) => { + const providerName = pn as PROVIDER_TYPE; + return { - label: providerName, + label: PROVIDERS[providerName].label, options: providerModels.map((providerModel) => ({ label: providerModel.label, value: providerModel.value, })), - icon: PLAYGROUND_PROVIDERS[providerName as PLAYGROUND_PROVIDER].icon, + icon: PROVIDERS[providerName].icon, }; }, ); - }, []); + }, [configuredProviderKeys]); const filteredOptions = useMemo(() => { if (filterValue === "") { @@ -85,7 +109,7 @@ const PromptModelSelect = ({ }, [filterValue, groupOptions]); const handleOnChange = useCallback( - (value: PLAYGROUND_MODEL) => { + (value: PROVIDER_MODEL_TYPE) => { onChange(value); }, [onChange], @@ -108,6 +132,14 @@ const PromptModelSelect = ({ }; const renderOptions = () => { + if (configuredProviderKeys?.length === 0) { + return ( +
    + No configured providers +
    + ); + } + if (filteredOptions.length === 0 && filterValue !== "") { return (
    @@ -188,7 +220,7 @@ const PromptModelSelect = ({ return null; } - const Icon = PLAYGROUND_PROVIDERS[provider].icon; + const Icon = PROVIDERS[provider].icon; if (!Icon) { return null; @@ -198,42 +230,61 @@ const PromptModelSelect = ({ }; return ( - + + +
    + {renderProviderValueIcon()} + {provider && PROVIDERS[provider].label} {value} +
    +
    +
    + +
    + + setFilterValue(e.target.value)} + />
    - - - -
    - - setFilterValue(e.target.value)} - /> -
    - - {renderOptions()} - - -
    - + + {renderOptions()} + + {!areAllProvidersConfigured(configuredProviderKeys) && ( + <> + + + + )} +
    + + + ); }; diff --git a/apps/opik-frontend/src/components/pages/PlaygroundPage/PlaygroundPrompt/PromptModelSettings/PromptModelConfigs.tsx b/apps/opik-frontend/src/components/pages/PlaygroundPage/PlaygroundPrompt/PromptModelSettings/PromptModelConfigs.tsx index b69a582b3d..821c44188d 100644 --- a/apps/opik-frontend/src/components/pages/PlaygroundPage/PlaygroundPrompt/PromptModelSettings/PromptModelConfigs.tsx +++ b/apps/opik-frontend/src/components/pages/PlaygroundPage/PlaygroundPrompt/PromptModelSettings/PromptModelConfigs.tsx @@ -1,10 +1,9 @@ import React from "react"; import { Settings2 } from "lucide-react"; -import { - PLAYGROUND_PROVIDER, - PlaygroundOpenAIConfigsType, - PlaygroundPromptConfigsType, -} from "@/types/playground"; + +import { PlaygroundPromptConfigsType } from "@/types/playground"; +import { PlaygroundOpenAIConfigsType, PROVIDER_TYPE } from "@/types/providers"; + import { DropdownMenu, DropdownMenuContent, @@ -15,7 +14,7 @@ import { Button } from "@/components/ui/button"; import OpenAIModelConfigs from "@/components/pages/PlaygroundPage/PlaygroundPrompt/PromptModelSettings/providerConfigs/OpenAIModelConfigs"; interface PromptModelConfigsProps { - provider: PLAYGROUND_PROVIDER | ""; + provider: PROVIDER_TYPE | ""; configs: PlaygroundPromptConfigsType; onChange: (configs: Partial) => void; } @@ -26,7 +25,7 @@ const PromptModelConfigs = ({ onChange, }: PromptModelConfigsProps) => { const getProviderForm = () => { - if (provider === PLAYGROUND_PROVIDER.OpenAI) { + if (provider === PROVIDER_TYPE.OPEN_AI) { return ( onChange({ maxTokens: v })} - id="maxTokens" + value={configs.maxCompletionTokens} + onChange={(v) => onChange({ maxCompletionTokens: v })} + id="maxCompletionTokens" min={0} max={10000} step={1} - defaultValue={DEFAULT_OPEN_AI_CONFIGS.MAX_TOKENS} + defaultValue={DEFAULT_OPEN_AI_CONFIGS.MAX_COMPLETION_TOKENS} label="Max output tokens" tooltip={ } /> -
    -
    - - - - } - > - - -
    - - onChange({ stop: event.target.value })} - /> -
    - onChange({ topP: v })} diff --git a/apps/opik-frontend/src/components/pages/PromptsPage/PromptsPage.tsx b/apps/opik-frontend/src/components/pages/PromptsPage/PromptsPage.tsx index e0bcda11a1..c6798daa07 100644 --- a/apps/opik-frontend/src/components/pages/PromptsPage/PromptsPage.tsx +++ b/apps/opik-frontend/src/components/pages/PromptsPage/PromptsPage.tsx @@ -196,7 +196,7 @@ const PromptsPage: React.FunctionComponent = () => { onSelectionChange={setSelectedColumns} order={columnsOrder} onOrderChange={setColumnsOrder} - > + /> @@ -229,7 +229,7 @@ const PromptsPage: React.FunctionComponent = () => { size={size} sizeChange={setSize} total={total} - > + />
    void; +}; + +const AddEditAIProviderDialog: React.FC = ({ + providerKey, + open, + setOpen, +}) => { + const { mutate: createMutate } = useProviderKeysCreateMutation(); + const { mutate: updateMutate } = useProviderKeysUpdateMutation(); + const [provider, setProvider] = useState( + providerKey?.provider || "", + ); + const [apiKey, setApiKey] = useState(""); + + const isEdit = Boolean(providerKey); + const isValid = Boolean(apiKey.length); + + const providerName = (provider && PROVIDERS[provider].label) || ""; + + const title = isEdit + ? "Edit AI provider configuration" + : "Add AI provider configuration"; + + const buttonText = isEdit ? "Update configuration" : "Save configuration"; + + const apiKeyLabel = provider ? `${providerName} API Key` : "API Key"; + + const submitHandler = useCallback(() => { + if (isEdit) { + updateMutate({ + providerKey: { + id: providerKey!.id, + apiKey, + }, + }); + } else if (provider) { + createMutate({ + providerKey: { + apiKey, + provider, + }, + }); + } + }, [createMutate, isEdit, apiKey, updateMutate, provider, providerKey]); + + const renderOption = (option: DropdownOption) => { + const Icon = PROVIDERS[option.value as PROVIDER_TYPE].icon; + + return ( + +
    + + {option.label} +
    +
    + ); + }; + + return ( + + + + {title} + +
    + + setProvider(v as PROVIDER_TYPE)} + options={PROVIDERS_OPTIONS} + placeholder="Select a provider" + /> +
    +
    + + setApiKey(e.target.value)} + /> + {provider && ( + + Get your {providerName} API key{" "} + + . + + )} +
    + + + + + + + + +
    +
    + ); +}; + +export default AddEditAIProviderDialog; diff --git a/apps/opik-frontend/src/components/shared/EyeInput/EyeInput.tsx b/apps/opik-frontend/src/components/shared/EyeInput/EyeInput.tsx new file mode 100644 index 0000000000..2427221b9f --- /dev/null +++ b/apps/opik-frontend/src/components/shared/EyeInput/EyeInput.tsx @@ -0,0 +1,40 @@ +import React, { useId, useState } from "react"; +import { Input, InputProps } from "@/components/ui/input"; +import { Eye, EyeOff } from "lucide-react"; +import { cn } from "@/lib/utils"; + +interface EyeInputProps extends InputProps {} + +const EyeInput = (props: EyeInputProps) => { + const [hidden, setHidden] = useState(true); + const id = useId(); + + const Icon = hidden ? Eye : EyeOff; + + return ( +
    + +
    { + e.preventDefault(); + setHidden((h) => !h); + }} + > + +
    +
    + ); +}; + +export default EyeInput; diff --git a/apps/opik-frontend/src/components/shared/SelectBox/SelectBox.tsx b/apps/opik-frontend/src/components/shared/SelectBox/SelectBox.tsx index c04e4cf694..14cebea678 100644 --- a/apps/opik-frontend/src/components/shared/SelectBox/SelectBox.tsx +++ b/apps/opik-frontend/src/components/shared/SelectBox/SelectBox.tsx @@ -8,6 +8,7 @@ import { } from "@/components/ui/select"; import { cn } from "@/lib/utils"; import { DropdownOption } from "@/types/shared"; +import isFunction from "lodash/isFunction"; export type SelectBoxProps = { value: string; @@ -18,6 +19,7 @@ export type SelectBoxProps = { placeholder?: string; disabled?: boolean; testId?: string; + renderOption?: (option: DropdownOption) => React.ReactNode; }; export const SelectBox = ({ @@ -28,6 +30,7 @@ export const SelectBox = ({ variant = "outline", placeholder = "Select value", disabled = false, + renderOption, testId, }: SelectBoxProps) => { const variantClass = @@ -35,15 +38,27 @@ export const SelectBox = ({ return ( ); diff --git a/apps/opik-frontend/src/components/ui/select.tsx b/apps/opik-frontend/src/components/ui/select.tsx index 6e7d29ff85..24aa6d1b11 100644 --- a/apps/opik-frontend/src/components/ui/select.tsx +++ b/apps/opik-frontend/src/components/ui/select.tsx @@ -109,23 +109,31 @@ const SelectLabel = React.forwardRef< )); SelectLabel.displayName = SelectPrimitive.Label.displayName; +interface SelectItemProps + extends React.ComponentPropsWithoutRef { + withoutCheck?: boolean; +} + const SelectItem = React.forwardRef< React.ElementRef, - React.ComponentPropsWithoutRef ->(({ className, children, ...props }, ref) => ( + SelectItemProps +>(({ className, children, withoutCheck = false, ...props }, ref) => ( - - - - - + {!withoutCheck && ( + + + + + + )} {children} diff --git a/apps/opik-frontend/src/constants/playground.ts b/apps/opik-frontend/src/constants/playground.ts index d415760b3a..4d2a0fe4cb 100644 --- a/apps/opik-frontend/src/constants/playground.ts +++ b/apps/opik-frontend/src/constants/playground.ts @@ -1,114 +1,109 @@ -import OpenAIIcon from "@/icons/integrations/openai.svg?react"; -import { PLAYGROUND_MODEL, PLAYGROUND_PROVIDER } from "@/types/playground"; +import { PROVIDER_TYPE, PROVIDER_MODEL_TYPE } from "@/types/providers"; -// @ToDo: remove it -export const OPENAI_API_KEY = "OPENAI_API_KEY"; - -export const PLAYGROUND_PROVIDERS = { - [PLAYGROUND_PROVIDER.OpenAI]: { - title: "Open AI", - value: PLAYGROUND_PROVIDER.OpenAI, - icon: OpenAIIcon, - }, +type PROVIDER_MODELS_TYPE = { + [key in PROVIDER_TYPE]: { + value: PROVIDER_MODEL_TYPE; + label: string; + }[]; }; -export const PLAYGROUND_MODELS = { - [PLAYGROUND_PROVIDER.OpenAI]: [ +export const PROVIDER_MODELS: PROVIDER_MODELS_TYPE = { + [PROVIDER_TYPE.OPEN_AI]: [ // GPT-4.0 Models { - value: PLAYGROUND_MODEL.GPT_4O, + value: PROVIDER_MODEL_TYPE.GPT_4O, label: "GPT 4o", }, { - value: PLAYGROUND_MODEL.GPT_4O_MINI, + value: PROVIDER_MODEL_TYPE.GPT_4O_MINI, label: "GPT 4o Mini", }, { - value: PLAYGROUND_MODEL.GPT_4O_MINI_2024_07_18, + value: PROVIDER_MODEL_TYPE.GPT_4O_MINI_2024_07_18, label: "GPT 4o Mini 2024-07-18", }, { - value: PLAYGROUND_MODEL.GPT_4O_2024_11_20, + value: PROVIDER_MODEL_TYPE.GPT_4O_2024_11_20, label: "GPT 4o 2024-11-20", }, { - value: PLAYGROUND_MODEL.GPT_4O_2024_08_06, + value: PROVIDER_MODEL_TYPE.GPT_4O_2024_08_06, label: "GPT 4o 2024-08-06", }, { - value: PLAYGROUND_MODEL.GPT_4O_2024_05_13, + value: PROVIDER_MODEL_TYPE.GPT_4O_2024_05_13, label: "GPT 4o 2024-05-13", }, // GPT-4 Models { - value: PLAYGROUND_MODEL.GPT_4_TURBO, + value: PROVIDER_MODEL_TYPE.GPT_4_TURBO, label: "GPT 4 Turbo", }, { - value: PLAYGROUND_MODEL.GPT_4, + value: PROVIDER_MODEL_TYPE.GPT_4, label: "GPT 4", }, { - value: PLAYGROUND_MODEL.GPT_4_TURBO_PREVIEW, + value: PROVIDER_MODEL_TYPE.GPT_4_TURBO_PREVIEW, label: "GPT 4 Turbo Preview", }, { - value: PLAYGROUND_MODEL.GPT_4_TURBO_2024_04_09, + value: PROVIDER_MODEL_TYPE.GPT_4_TURBO_2024_04_09, label: "GPT 4 Turbo 2024-04-09", }, { - value: PLAYGROUND_MODEL.GPT_4_1106_PREVIEW, + value: PROVIDER_MODEL_TYPE.GPT_4_1106_PREVIEW, label: "GPT 4 1106 Preview", }, { - value: PLAYGROUND_MODEL.GPT_4_0613, + value: PROVIDER_MODEL_TYPE.GPT_4_0613, label: "GPT 4 0613", }, { - value: PLAYGROUND_MODEL.GPT_4_0125_PREVIEW, + value: PROVIDER_MODEL_TYPE.GPT_4_0125_PREVIEW, label: "GPT 4 0125 Preview", }, // GPT-3.5 Models { - value: PLAYGROUND_MODEL.GPT_3_5_TURBO, + value: PROVIDER_MODEL_TYPE.GPT_3_5_TURBO, label: "GPT 3.5 Turbo", }, { - value: PLAYGROUND_MODEL.GPT_3_5_TURBO_16K, + value: PROVIDER_MODEL_TYPE.GPT_3_5_TURBO_16K, label: "GPT 3.5 Turbo 16k", }, { - value: PLAYGROUND_MODEL.GPT_3_5_TURBO_1106, + value: PROVIDER_MODEL_TYPE.GPT_3_5_TURBO_1106, label: "GPT 3.5 Turbo 1106", }, { - value: PLAYGROUND_MODEL.GPT_3_5_TURBO_0125, + value: PROVIDER_MODEL_TYPE.GPT_3_5_TURBO_0125, label: "GPT 3.5 Turbo 0125", }, // Reasoning Models { - value: PLAYGROUND_MODEL.O1_PREVIEW, + value: PROVIDER_MODEL_TYPE.O1_PREVIEW, label: "O1 Preview", }, { - value: PLAYGROUND_MODEL.O1_MINI, + value: PROVIDER_MODEL_TYPE.O1_MINI, label: "O1 Mini", }, { - value: PLAYGROUND_MODEL.O1_MINI_2024_09_12, + value: PROVIDER_MODEL_TYPE.O1_MINI_2024_09_12, label: "O1 Mini 2024-09-12", }, { - value: PLAYGROUND_MODEL.O1_PREVIEW_2024_09_12, + value: PROVIDER_MODEL_TYPE.O1_PREVIEW_2024_09_12, label: "O1 Preview 2024-09-12", }, // Other Models { - value: PLAYGROUND_MODEL.CHATGPT_4O_LATEST, + value: PROVIDER_MODEL_TYPE.CHATGPT_4O_LATEST, label: "ChatGPT 4o Latest", }, ], @@ -116,9 +111,8 @@ export const PLAYGROUND_MODELS = { export const DEFAULT_OPEN_AI_CONFIGS = { TEMPERATURE: 0, - MAX_TOKENS: 1024, + MAX_COMPLETION_TOKENS: 1024, TOP_P: 1, - STOP: "", FREQUENCY_PENALTY: 0, PRESENCE_PENALTY: 0, }; diff --git a/apps/opik-frontend/src/constants/providers.ts b/apps/opik-frontend/src/constants/providers.ts new file mode 100644 index 0000000000..239fcd9922 --- /dev/null +++ b/apps/opik-frontend/src/constants/providers.ts @@ -0,0 +1,30 @@ +import OpenAIIcon from "@/icons/integrations/openai.svg?react"; +import { PROVIDER_MODEL_TYPE, PROVIDER_TYPE } from "@/types/providers"; + +type IconType = typeof OpenAIIcon; + +export type PROVIDER_OPTION_TYPE = { + label: string; + value: PROVIDER_TYPE; + icon: IconType; + apiKeyName: string; + apiKeyURL: string; + defaultModel: PROVIDER_MODEL_TYPE; +}; + +type PROVIDERS_TYPE = { + [key in PROVIDER_TYPE]: PROVIDER_OPTION_TYPE; +}; + +export const PROVIDERS: PROVIDERS_TYPE = { + [PROVIDER_TYPE.OPEN_AI]: { + label: "OpenAI", + value: PROVIDER_TYPE.OPEN_AI, + icon: OpenAIIcon, + apiKeyName: "OPENAI_API_KEY", + apiKeyURL: "https://platform.openai.com/account/api-keys", + defaultModel: PROVIDER_MODEL_TYPE.GPT_4O, + }, +}; + +export const PROVIDERS_OPTIONS = Object.values(PROVIDERS); diff --git a/apps/opik-frontend/src/icons/integrations/openai.svg b/apps/opik-frontend/src/icons/integrations/openai.svg index 5cbd152a8b..b0c7d1b07a 100644 --- a/apps/opik-frontend/src/icons/integrations/openai.svg +++ b/apps/opik-frontend/src/icons/integrations/openai.svg @@ -5,6 +5,6 @@ + fill="currentColor"/> diff --git a/apps/opik-frontend/src/lib/playground.ts b/apps/opik-frontend/src/lib/playground.ts index d8ddc3dcad..97b63e9945 100644 --- a/apps/opik-frontend/src/lib/playground.ts +++ b/apps/opik-frontend/src/lib/playground.ts @@ -1,17 +1,19 @@ import { ProviderMessageType, PLAYGROUND_MESSAGE_ROLE, - PLAYGROUND_MODEL, - PLAYGROUND_PROVIDER, PlaygroundMessageType, PlaygroundPromptConfigsType, - PlaygroundOpenAIConfigsType, } from "@/types/playground"; import { generateRandomString } from "@/lib/utils"; import { DEFAULT_OPEN_AI_CONFIGS, - PLAYGROUND_MODELS, + PROVIDER_MODELS, } from "@/constants/playground"; +import { + PlaygroundOpenAIConfigsType, + PROVIDER_MODEL_TYPE, + PROVIDER_TYPE, +} from "@/types/providers"; export const generateDefaultPlaygroundPromptMessage = ( message: Partial = {}, @@ -25,9 +27,9 @@ export const generateDefaultPlaygroundPromptMessage = ( }; export const getModelProvider = ( - modelName: PLAYGROUND_MODEL, -): PLAYGROUND_PROVIDER | "" => { - const provider = Object.entries(PLAYGROUND_MODELS).find( + modelName: PROVIDER_MODEL_TYPE, +): PROVIDER_TYPE | "" => { + const provider = Object.entries(PROVIDER_MODELS).find( ([providerName, providerModels]) => { if (providerModels.find((pm) => modelName === pm.value)) { return providerName; @@ -43,18 +45,17 @@ export const getModelProvider = ( const [providerName] = provider; - return providerName as PLAYGROUND_PROVIDER; + return providerName as PROVIDER_TYPE; }; export const getDefaultConfigByProvider = ( - provider: PLAYGROUND_PROVIDER, + provider: PROVIDER_TYPE, ): PlaygroundPromptConfigsType => { - if (provider === PLAYGROUND_PROVIDER.OpenAI) { + if (provider === PROVIDER_TYPE.OPEN_AI) { return { temperature: DEFAULT_OPEN_AI_CONFIGS.TEMPERATURE, - maxTokens: DEFAULT_OPEN_AI_CONFIGS.MAX_TOKENS, + maxCompletionTokens: DEFAULT_OPEN_AI_CONFIGS.MAX_COMPLETION_TOKENS, topP: DEFAULT_OPEN_AI_CONFIGS.TOP_P, - stop: DEFAULT_OPEN_AI_CONFIGS.STOP, frequencyPenalty: DEFAULT_OPEN_AI_CONFIGS.FREQUENCY_PENALTY, presencePenalty: DEFAULT_OPEN_AI_CONFIGS.PRESENCE_PENALTY, } as PlaygroundOpenAIConfigsType; diff --git a/apps/opik-frontend/src/lib/provider.ts b/apps/opik-frontend/src/lib/provider.ts new file mode 100644 index 0000000000..1102cb70f0 --- /dev/null +++ b/apps/opik-frontend/src/lib/provider.ts @@ -0,0 +1,8 @@ +import { PROVIDER_TYPE, ProviderKey } from "@/types/providers"; +import { PROVIDERS } from "@/constants/providers"; + +export const areAllProvidersConfigured = ( + providers: (ProviderKey | PROVIDER_TYPE)[], +) => { + return providers.length === Object.keys(PROVIDERS).length; +}; diff --git a/apps/opik-frontend/src/router.tsx b/apps/opik-frontend/src/router.tsx index 051f81f064..7e831d2978 100644 --- a/apps/opik-frontend/src/router.tsx +++ b/apps/opik-frontend/src/router.tsx @@ -14,7 +14,6 @@ import DatasetPage from "@/components/pages/DatasetPage/DatasetPage"; import DatasetsPage from "@/components/pages/DatasetsPage/DatasetsPage"; import ExperimentsPage from "@/components/pages/ExperimentsPage/ExperimentsPage"; import CompareExperimentsPage from "@/components/pages/CompareExperimentsPage/CompareExperimentsPage"; -import FeedbackDefinitionsPage from "@/components/pages/FeedbackDefinitionsPage/FeedbackDefinitionsPage"; import QuickstartPage from "@/components/pages/QuickstartPage/QuickstartPage"; import HomePage from "@/components/pages/HomePage/HomePage"; import PartialPageLayout from "@/components/layout/PartialPageLayout/PartialPageLayout"; @@ -28,6 +27,7 @@ import RedirectProjects from "@/components/redirect/RedirectProjects"; import RedirectDatasets from "@/components/redirect/RedirectDatasets"; import PlaygroundPage from "@/components/pages/PlaygroundPage/PlaygroundPage"; import useAppStore from "@/store/AppStore"; +import ConfigurationPage from "@/components/pages/ConfigurationPage/ConfigurationPage"; const TanStackRouterDevtools = process.env.NODE_ENV === "production" @@ -138,21 +138,6 @@ const tracesRoute = createRoute({ component: TracesPage, }); -// ----------- feedback definitions -const feedbackDefinitionsRoute = createRoute({ - path: "/feedback-definitions", - getParentRoute: () => workspaceRoute, - staticData: { - title: "Feedback", - }, -}); - -const feedbackDefinitionsListRoute = createRoute({ - path: "/", - getParentRoute: () => feedbackDefinitionsRoute, - component: FeedbackDefinitionsPage, -}); - // ----------- experiments const experimentsRoute = createRoute({ path: "/experiments", @@ -270,6 +255,17 @@ const playgroundRoute = createRoute({ component: PlaygroundPage, }); +// --------- configuration + +const configurationRoute = createRoute({ + path: "/configuration", + getParentRoute: () => workspaceRoute, + staticData: { + title: "Configuration", + }, + component: ConfigurationPage, +}); + const routeTree = rootRoute.addChildren([ workspaceGuardPartialLayoutRoute.addChildren([ quickstartRoute, @@ -283,7 +279,6 @@ const routeTree = rootRoute.addChildren([ projectsListRoute, projectRoute.addChildren([tracesRoute]), ]), - feedbackDefinitionsRoute.addChildren([feedbackDefinitionsListRoute]), experimentsRoute.addChildren([ experimentsListRoute, compareExperimentsRoute, @@ -299,6 +294,7 @@ const routeTree = rootRoute.addChildren([ redirectDatasetsRoute, ]), playgroundRoute, + configurationRoute, ]), ]), ]); diff --git a/apps/opik-frontend/src/types/playground.ts b/apps/opik-frontend/src/types/playground.ts index 5260e08f4b..37413042b4 100644 --- a/apps/opik-frontend/src/types/playground.ts +++ b/apps/opik-frontend/src/types/playground.ts @@ -1,42 +1,9 @@ import { UsageType } from "@/types/shared"; - -export enum PLAYGROUND_PROVIDER { - "OpenAI" = "OpenAI", -} - -export enum PLAYGROUND_MODEL { - // Reasoning Models - O1_PREVIEW = "o1-preview", - O1_MINI = "o1-mini", - O1_MINI_2024_09_12 = "o1-mini-2024-09-12", - O1_PREVIEW_2024_09_12 = "o1-preview-2024-09-12", - - // GPT-4.0 Models - GPT_4O = "gpt-4o", - GPT_4O_MINI = "gpt-4o-mini", - GPT_4O_MINI_2024_07_18 = "gpt-4o-mini-2024-07-18", - GPT_4O_2024_11_20 = "gpt-4o-2024-11-20", - GPT_4O_2024_08_06 = "gpt-4o-2024-08-06", - GPT_4O_2024_05_13 = "gpt-4o-2024-05-13", - - // GPT-4 Models - GPT_4_TURBO = "gpt-4-turbo", - GPT_4 = "gpt-4", - GPT_4_TURBO_PREVIEW = "gpt-4-turbo-preview", - GPT_4_TURBO_2024_04_09 = "gpt-4-turbo-2024-04-09", - GPT_4_1106_PREVIEW = "gpt-4-1106-preview", - GPT_4_0613 = "gpt-4-0613", - GPT_4_0125_PREVIEW = "gpt-4-0125-preview", - - // GPT-3.5 Models - GPT_3_5_TURBO = "gpt-3.5-turbo", - GPT_3_5_TURBO_16K = "gpt-3.5-turbo-16k", - GPT_3_5_TURBO_1106 = "gpt-3.5-turbo-1106", - GPT_3_5_TURBO_0125 = "gpt-3.5-turbo-0125", - - // Other Models - CHATGPT_4O_LATEST = "chatgpt-4o-latest", -} +import { HttpStatusCode } from "axios"; +import { + PlaygroundOpenAIConfigsType, + PROVIDER_MODEL_TYPE, +} from "@/types/providers"; export enum PLAYGROUND_MESSAGE_ROLE { system = "system", @@ -50,15 +17,6 @@ export interface PlaygroundMessageType { role: PLAYGROUND_MESSAGE_ROLE; } -export interface PlaygroundOpenAIConfigsType { - temperature: number; - maxTokens: number; - topP: number; - stop: string; - frequencyPenalty: number; - presencePenalty: number; -} - export type PlaygroundPromptConfigsType = | Record | PlaygroundOpenAIConfigsType; @@ -67,7 +25,7 @@ export interface PlaygroundPromptType { name: string; id: string; messages: PlaygroundMessageType[]; - model: PLAYGROUND_MODEL | ""; + model: PROVIDER_MODEL_TYPE | ""; configs: PlaygroundPromptConfigsType; } @@ -76,13 +34,27 @@ export interface ProviderMessageType { role: PLAYGROUND_MESSAGE_ROLE; } -export interface ProviderStreamingMessageChoiceType { +export interface ChatCompletionMessageChoiceType { delta: { content: string; }; } -export interface ProviderStreamingMessageType { - choices: ProviderStreamingMessageChoiceType[]; +export interface ChatCompletionSuccessMessageType { + choices: ChatCompletionMessageChoiceType[]; usage: UsageType; } + +export interface ChatCompletionErrorMessageType { + code: HttpStatusCode; + message: string; +} + +export interface ChatCompletionProxyErrorMessageType { + errors: string[]; +} + +export type ChatCompletionResponse = + | ChatCompletionProxyErrorMessageType + | ChatCompletionSuccessMessageType + | ChatCompletionErrorMessageType; diff --git a/apps/opik-frontend/src/types/providers.ts b/apps/opik-frontend/src/types/providers.ts new file mode 100644 index 0000000000..a9d0cf11b8 --- /dev/null +++ b/apps/opik-frontend/src/types/providers.ts @@ -0,0 +1,56 @@ +export enum PROVIDER_TYPE { + OPEN_AI = "openai", +} + +export enum PROVIDER_MODEL_TYPE { + // Reasoning Models + O1_PREVIEW = "o1-preview", + O1_MINI = "o1-mini", + O1_MINI_2024_09_12 = "o1-mini-2024-09-12", + O1_PREVIEW_2024_09_12 = "o1-preview-2024-09-12", + + // GPT-4.0 Models + GPT_4O = "gpt-4o", + GPT_4O_MINI = "gpt-4o-mini", + GPT_4O_MINI_2024_07_18 = "gpt-4o-mini-2024-07-18", + GPT_4O_2024_11_20 = "gpt-4o-2024-11-20", + GPT_4O_2024_08_06 = "gpt-4o-2024-08-06", + GPT_4O_2024_05_13 = "gpt-4o-2024-05-13", + + // GPT-4 Models + GPT_4_TURBO = "gpt-4-turbo", + GPT_4 = "gpt-4", + GPT_4_TURBO_PREVIEW = "gpt-4-turbo-preview", + GPT_4_TURBO_2024_04_09 = "gpt-4-turbo-2024-04-09", + GPT_4_1106_PREVIEW = "gpt-4-1106-preview", + GPT_4_0613 = "gpt-4-0613", + GPT_4_0125_PREVIEW = "gpt-4-0125-preview", + + // GPT-3.5 Models + GPT_3_5_TURBO = "gpt-3.5-turbo", + GPT_3_5_TURBO_16K = "gpt-3.5-turbo-16k", + GPT_3_5_TURBO_1106 = "gpt-3.5-turbo-1106", + GPT_3_5_TURBO_0125 = "gpt-3.5-turbo-0125", + + // Other Models + CHATGPT_4O_LATEST = "chatgpt-4o-latest", +} + +export interface ProviderKey { + id: string; + keyName: string; + created_at: string; + provider: PROVIDER_TYPE; +} + +export interface PlaygroundOpenAIConfigsType { + temperature: number; + maxCompletionTokens: number; + topP: number; + frequencyPenalty: number; + presencePenalty: number; +} + +export interface ProviderKeyWithAPIKey extends ProviderKey { + apiKey: string; +}