diff --git a/ui/litellm-dashboard/src/app/(dashboard)/playground/components/systemOneUI/SystemOneForm.tsx b/ui/litellm-dashboard/src/app/(dashboard)/playground/components/systemOneUI/SystemOneForm.tsx index 5d53bc2d620..cfffb30bb5d 100644 --- a/ui/litellm-dashboard/src/app/(dashboard)/playground/components/systemOneUI/SystemOneForm.tsx +++ b/ui/litellm-dashboard/src/app/(dashboard)/playground/components/systemOneUI/SystemOneForm.tsx @@ -27,7 +27,12 @@ import { import type { SystemOnePayloadValidation } from "./lib/validatePayload"; const QUESTION_TYPES: readonly QuestionType[] = ["choice", "noul", "score"]; -const TYPE_LABELS: Record = { choice: "Choice", noul: "Yes / no", score: "Score" }; +const TYPE_LABELS: Record = { choice: "Choice", noul: "noul", score: "Score" }; +const TYPE_HELP: Record = { + choice: undefined, + noul: "noul is a yes / no question", + score: undefined, +}; const isQuestionType = (value: unknown): value is QuestionType => QUESTION_TYPES.some((type) => type === value); @@ -219,7 +224,7 @@ function QuestionCard({ name, question, taken, onChange, onRename, onRemove }: Q {QUESTION_TYPES.map((type) => ( - + {TYPE_LABELS[type]} ))} diff --git a/ui/litellm-dashboard/src/app/(dashboard)/playground/components/systemOneUI/SystemOneUI.integration.test.tsx b/ui/litellm-dashboard/src/app/(dashboard)/playground/components/systemOneUI/SystemOneUI.integration.test.tsx index c5c6023cf27..eb75c836b2b 100644 --- a/ui/litellm-dashboard/src/app/(dashboard)/playground/components/systemOneUI/SystemOneUI.integration.test.tsx +++ b/ui/litellm-dashboard/src/app/(dashboard)/playground/components/systemOneUI/SystemOneUI.integration.test.tsx @@ -11,7 +11,7 @@ import { import type { ReactElement } from "react"; import userEvent from "@testing-library/user-event"; import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; -import SystemOneUI from "./SystemOneUI"; +import SystemOneUI, { NOUL_HELP } from "./SystemOneUI"; import type { SystemOneResponse } from "./lib/schemas"; import type { OpenAIDecisionsResponse } from "./lib/openAIDecisions"; @@ -920,6 +920,22 @@ describe("SystemOneUI integration", () => { fireEvent.blur(field); } + it("explains noul on hover in the banner and on the answer type option", async () => { + const user = userEvent.setup(); + render(); + + await user.hover(screen.getByText("noul", { selector: "span.cursor-help" })); + expect(await screen.findByText(NOUL_HELP)).toBeInTheDocument(); + + question("has_repro_steps").getByRole("combobox", { name: "Answer type" }).focus(); + await user.keyboard("{ArrowDown}"); + expect(await screen.findByRole("option", { name: "noul" })).toHaveAttribute( + "title", + "noul is a yes / no question", + ); + expect(screen.getByRole("option", { name: "Choice" })).not.toHaveAttribute("title"); + }); + it("sends a choice and a yes/no question built in the form without opening JSON", async () => { const user = userEvent.setup(); render(); @@ -941,7 +957,7 @@ describe("SystemOneUI integration", () => { fill(question("team").getByRole("textbox", { name: "Description of infra" }), "Deploys"); await user.click(screen.getByRole("button", { name: "Add question" })); - await pickAnswerType(user, "question_1", "Yes / no"); + await pickAnswerType(user, "question_1", "noul"); rename(question("question_1").getByRole("textbox", { name: "Question name" }), "urgent"); fill(question("urgent").getByRole("textbox", { name: "Instructions" }), "Is this blocking users?"); fill(question("urgent").getByRole("textbox", { name: "Yes means" }), "Nobody can log in"); diff --git a/ui/litellm-dashboard/src/app/(dashboard)/playground/components/systemOneUI/SystemOneUI.tsx b/ui/litellm-dashboard/src/app/(dashboard)/playground/components/systemOneUI/SystemOneUI.tsx index c46238315ff..3d2d4c0f1a0 100644 --- a/ui/litellm-dashboard/src/app/(dashboard)/playground/components/systemOneUI/SystemOneUI.tsx +++ b/ui/litellm-dashboard/src/app/(dashboard)/playground/components/systemOneUI/SystemOneUI.tsx @@ -5,6 +5,7 @@ import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"; import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; +import { SimpleTooltip } from "@/components/ui/tooltip"; import { SearchSelect } from "@/components/shared/SearchSelect"; import { DECISIONS_DOCS_URL } from "@/lib/decisionModels"; import { uiHref } from "@/utils/uiHref"; @@ -35,6 +36,7 @@ interface SystemOneSendVariables { signal: AbortSignal; } +export const NOUL_HELP = "noul is a yes / no question. The model returns the probability that the answer is yes."; const CUSTOM_PRESET = "custom"; const DECISION_MODELS_DISCUSSION_URL = "https://github.com/BerriAI/litellm/discussions/44231"; @@ -265,9 +267,17 @@ export default function SystemOneUI({ accessToken, disabledPersonalKeyCreation = {endpoint} - {endpoint === "/v1/decisions" - ? "Uses input and a questions array with predicate, choice, and score questions. Edit the form or JSON and inspect the response JSON in the OpenAI Decisions format." - : "Uses state and a questions object with noul, choice, and score questions in the System One format."}{" "} + {endpoint === "/v1/decisions" ? ( + "Uses input and a questions array with predicate, choice, and score questions. Edit the form or JSON and inspect the response JSON in the OpenAI Decisions format." + ) : ( + <> + Uses state and a questions object with{" "} + + noul + + , choice, and score questions in the System One format. + + )}{" "} Pick a decision model under Model, or omit model to use the proxy's configured default.{" "} How to call /v1/decisions and /v1/systemone