Show steps in task page (#585)

This commit is contained in:
Kerem Yilmaz 2024-07-11 08:03:18 -07:00 committed by GitHub
parent 1e043445a4
commit 240e591280
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
9 changed files with 157 additions and 51 deletions

18
poetry.lock generated
View file

@ -2943,13 +2943,13 @@ files = [
[[package]] [[package]]
name = "litellm" name = "litellm"
version = "1.41.12" version = "1.35.35"
description = "Library to easily interface with LLM API providers" description = "Library to easily interface with LLM API providers"
optional = false optional = false
python-versions = "!=2.7.*,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,!=3.7.*,>=3.8" python-versions = "!=2.7.*,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,!=3.7.*,>=3.8"
files = [ files = [
{file = "litellm-1.41.12-py3-none-any.whl", hash = "sha256:9af8b65ca48f0aa5b8ef10a63c21b00553843aa1e498f2c9308738b97d4a50f3"}, {file = "litellm-1.35.35-py3-none-any.whl", hash = "sha256:9dcf4a1fbdb1864de03f2fbcc26cc19ee83449966ad0b373b1d61f64ade963f7"},
{file = "litellm-1.41.12.tar.gz", hash = "sha256:f94b5ac8857ea8b98b87f7d3071dbd10e24fe9e0d7831969adafb549688ce0ce"}, {file = "litellm-1.35.35.tar.gz", hash = "sha256:f8873892f4a2f082e2f5f4fed5740f341b7d1a7778445785b2af68adbc2793e9"},
] ]
[package.dependencies] [package.dependencies]
@ -2957,17 +2957,15 @@ aiohttp = "*"
click = "*" click = "*"
importlib-metadata = ">=6.8.0" importlib-metadata = ">=6.8.0"
jinja2 = ">=3.1.2,<4.0.0" jinja2 = ">=3.1.2,<4.0.0"
jsonschema = ">=4.22.0,<5.0.0" openai = ">=1.0.0"
openai = ">=1.27.0"
pydantic = ">=2.0.0,<3.0.0"
python-dotenv = ">=0.2.0" python-dotenv = ">=0.2.0"
requests = ">=2.31.0,<3.0.0" requests = ">=2.31.0,<3.0.0"
tiktoken = ">=0.7.0" tiktoken = ">=0.4.0"
tokenizers = "*" tokenizers = "*"
[package.extras] [package.extras]
extra-proxy = ["azure-identity (>=1.15.0,<2.0.0)", "azure-keyvault-secrets (>=4.8.0,<5.0.0)", "google-cloud-kms (>=2.21.3,<3.0.0)", "prisma (==0.11.0)", "pynacl (>=1.5.0,<2.0.0)", "resend (>=0.8.0,<0.9.0)"] extra-proxy = ["azure-identity (>=1.15.0,<2.0.0)", "azure-keyvault-secrets (>=4.8.0,<5.0.0)", "google-cloud-kms (>=2.21.3,<3.0.0)", "prisma (==0.11.0)", "resend (>=0.8.0,<0.9.0)"]
proxy = ["PyJWT (>=2.8.0,<3.0.0)", "apscheduler (>=3.10.4,<4.0.0)", "backoff", "cryptography (>=42.0.5,<43.0.0)", "fastapi (>=0.111.0,<0.112.0)", "fastapi-sso (>=0.10.0,<0.11.0)", "gunicorn (>=22.0.0,<23.0.0)", "orjson (>=3.9.7,<4.0.0)", "python-multipart (>=0.0.9,<0.0.10)", "pyyaml (>=6.0.1,<7.0.0)", "rq", "uvicorn (>=0.22.0,<0.23.0)"] proxy = ["PyJWT (>=2.8.0,<3.0.0)", "apscheduler (>=3.10.4,<4.0.0)", "backoff", "cryptography (>=42.0.5,<43.0.0)", "fastapi (>=0.109.1,<0.110.0)", "fastapi-sso (>=0.10.0,<0.11.0)", "gunicorn (>=21.2.0,<22.0.0)", "orjson (>=3.9.7,<4.0.0)", "python-multipart (>=0.0.9,<0.0.10)", "pyyaml (>=6.0.1,<7.0.0)", "rq", "uvicorn (>=0.22.0,<0.23.0)"]
[[package]] [[package]]
name = "lxml" name = "lxml"
@ -7277,4 +7275,4 @@ testing = ["big-O", "jaraco.functools", "jaraco.itertools", "more-itertools", "p
[metadata] [metadata]
lock-version = "2.0" lock-version = "2.0"
python-versions = "^3.11,<3.12" python-versions = "^3.11,<3.12"
content-hash = "40e1dba8128bed75196f0c8dc21894f1b37f76a16f19b1678697df7563f67a7a" content-hash = "b3bb150f1811469740857a9bf062345a9597ace0d292b83f08a9f3e0707519d8"

View file

@ -19,7 +19,7 @@ python-multipart = "^0.0.6"
toml = "^0.10.2" toml = "^0.10.2"
jinja2 = "^3.1.2" jinja2 = "^3.1.2"
uvicorn = {extras = ["standard"], version = "^0.24.0.post1"} uvicorn = {extras = ["standard"], version = "^0.24.0.post1"}
litellm = "1.41.12" litellm = "1.35.35"
duckduckgo-search = "^3.8.0" duckduckgo-search = "^3.8.0"
selenium = "^4.13.0" selenium = "^4.13.0"
bs4 = "^0.0.1" bs4 = "^0.0.1"

View file

@ -0,0 +1,9 @@
import { CostCalculatorContext } from "@/store/CostCalculatorContext";
import { useContext } from "react";
function useCostCalculator() {
const costCalculator = useContext(CostCalculatorContext);
return costCalculator;
}
export { useCostCalculator };

View file

@ -30,11 +30,7 @@ function StepArtifactsLayout() {
return <div>Error: {error?.message}</div>; return <div>Error: {error?.message}</div>;
} }
if (!steps) { const activeStep = steps?.[activeIndex];
return <div>No steps found</div>;
}
const activeStep = steps[activeIndex];
return ( return (
<div className="flex"> <div className="flex">

View file

@ -41,13 +41,9 @@ function StepNavigation({ activeIndex, onActiveIndexChange }: Props) {
return <div>Error: {error?.message}</div>; return <div>Error: {error?.message}</div>;
} }
if (!steps) {
return <div>No steps found</div>;
}
return ( return (
<nav className="flex flex-col gap-4"> <nav className="flex flex-col gap-4">
{steps.map((step, index) => { {steps?.map((step, index) => {
const isActive = activeIndex === index; const isActive = activeIndex === index;
return ( return (
<div <div

View file

@ -1,6 +1,5 @@
import { getClient } from "@/api/AxiosClient"; import { getClient } from "@/api/AxiosClient";
import { Status, TaskApiResponse } from "@/api/types"; import { Status } from "@/api/types";
import { StatusBadge } from "@/components/StatusBadge";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { import {
Dialog, Dialog,
@ -19,13 +18,10 @@ import { toast } from "@/components/ui/use-toast";
import { useCredentialGetter } from "@/hooks/useCredentialGetter"; import { useCredentialGetter } from "@/hooks/useCredentialGetter";
import { cn } from "@/util/utils"; import { cn } from "@/util/utils";
import { ReloadIcon } from "@radix-ui/react-icons"; import { ReloadIcon } from "@radix-ui/react-icons";
import { import { useMutation, useQueryClient } from "@tanstack/react-query";
keepPreviousData,
useMutation,
useQuery,
useQueryClient,
} from "@tanstack/react-query";
import { NavLink, Outlet, useParams } from "react-router-dom"; import { NavLink, Outlet, useParams } from "react-router-dom";
import { TaskInfo } from "./TaskInfo";
import { useTaskQuery } from "./hooks/useTaskQuery";
function TaskDetails() { function TaskDetails() {
const { taskId } = useParams(); const { taskId } = useParams();
@ -37,23 +33,7 @@ function TaskDetails() {
isLoading: taskIsLoading, isLoading: taskIsLoading,
isError: taskIsError, isError: taskIsError,
error: taskError, error: taskError,
} = useQuery<TaskApiResponse>({ } = useTaskQuery({ id: taskId });
queryKey: ["task", taskId],
queryFn: async () => {
const client = await getClient(credentialGetter);
return client.get(`/tasks/${taskId}`).then((response) => response.data);
},
refetchInterval: (query) => {
if (
query.state.data?.status === Status.Running ||
query.state.data?.status === Status.Queued
) {
return 10000;
}
return false;
},
placeholderData: keepPreviousData,
});
const cancelTaskMutation = useMutation({ const cancelTaskMutation = useMutation({
mutationFn: async () => { mutationFn: async () => {
@ -124,11 +104,7 @@ function TaskDetails() {
<div className="flex justify-between items-center"> <div className="flex justify-between items-center">
<div className="flex items-center gap-4"> <div className="flex items-center gap-4">
<span className="text-lg">{taskId}</span> <span className="text-lg">{taskId}</span>
{taskIsLoading ? ( {taskId && <TaskInfo id={taskId} />}
<Skeleton className="w-28 h-8" />
) : task ? (
<StatusBadge status={task?.status} />
) : null}
</div> </div>
{taskIsRunningOrQueued && ( {taskIsRunningOrQueued && (
<Dialog> <Dialog>

View file

@ -0,0 +1,90 @@
import { Badge } from "@/components/ui/badge";
import { StatusBadge } from "@/components/StatusBadge";
import { Skeleton } from "@/components/ui/skeleton";
import { Status, StepApiResponse } from "@/api/types";
import { keepPreviousData, useQuery } from "@tanstack/react-query";
import { useCredentialGetter } from "@/hooks/useCredentialGetter";
import { getClient } from "@/api/AxiosClient";
import { useTaskQuery } from "./hooks/useTaskQuery";
import { useCostCalculator } from "@/hooks/useCostCalculator";
type Props = {
id: string;
};
const formatter = Intl.NumberFormat("en-US", {
style: "currency",
currency: "USD",
});
function TaskInfo({ id }: Props) {
const credentialGetter = useCredentialGetter();
const costCalculator = useCostCalculator();
const {
data: task,
isLoading: taskIsLoading,
isError: taskIsError,
} = useTaskQuery({ id });
const taskIsRunningOrQueued =
task?.status === Status.Running || task?.status === Status.Queued;
const {
data: steps,
isLoading: stepsIsLoading,
isError: stepsIsError,
} = useQuery<Array<StepApiResponse>>({
queryKey: ["task", id, "steps"],
queryFn: async () => {
const client = await getClient(credentialGetter);
return client.get(`/tasks/${id}/steps`).then((response) => response.data);
},
refetchOnWindowFocus: taskIsRunningOrQueued,
refetchInterval: taskIsRunningOrQueued ? 5000 : false,
placeholderData: keepPreviousData,
});
if (stepsIsLoading || taskIsLoading) {
return (
<div className="flex gap-4">
<Skeleton className="w-20 h-6" />
<Skeleton className="w-20 h-6" />
<Skeleton className="w-20 h-6" />
<Skeleton className="w-20 h-6" />
</div>
);
}
if (stepsIsError || taskIsError) {
return null;
}
const actionCount = steps?.reduce((acc, step) => {
const actionsAndResults = step.output?.actions_and_results ?? [];
const actionCount = actionsAndResults.reduce((acc, actionAndResult) => {
const actionResult = actionAndResult[1];
if (actionResult.length === 0) {
return acc;
}
return acc + 1;
}, 0);
return acc + actionCount;
}, 0);
const showCost = typeof costCalculator === "function";
const notRunningSteps = steps?.filter((step) => step.status !== "running");
return (
<div className="flex gap-4">
{task && <StatusBadge status={task.status} />}
<Badge>Steps: {notRunningSteps?.length}</Badge>
<Badge>Actions: {actionCount}</Badge>
{showCost && (
<Badge>Cost: {formatter.format(costCalculator(steps ?? []))}</Badge>
)}
</div>
);
}
export { TaskInfo };

View file

@ -0,0 +1,33 @@
import { getClient } from "@/api/AxiosClient";
import { Status, TaskApiResponse } from "@/api/types";
import { useCredentialGetter } from "@/hooks/useCredentialGetter";
import { keepPreviousData, useQuery } from "@tanstack/react-query";
type Props = {
id?: string;
};
function useTaskQuery({ id }: Props) {
const credentialGetter = useCredentialGetter();
return useQuery<TaskApiResponse>({
queryKey: ["task", id],
queryFn: async () => {
const client = await getClient(credentialGetter);
return client.get(`/tasks/${id}`).then((response) => response.data);
},
enabled: !!id,
refetchInterval: (query) => {
if (
query.state.data?.status === Status.Running ||
query.state.data?.status === Status.Queued
) {
return 5000;
}
return false;
},
placeholderData: keepPreviousData,
});
}
export { useTaskQuery };

View file

@ -0,0 +1,8 @@
import { StepApiResponse } from "@/api/types";
import { createContext } from "react";
type TaskCostCalculator = (steps: Array<StepApiResponse>) => number;
const CostCalculatorContext = createContext<TaskCostCalculator | null>(null);
export { CostCalculatorContext };