mirror of
https://github.com/Skyvern-AI/skyvern.git
synced 2025-09-02 18:50:24 +00:00
Show steps in task page (#585)
This commit is contained in:
parent
1e043445a4
commit
240e591280
9 changed files with 157 additions and 51 deletions
18
poetry.lock
generated
18
poetry.lock
generated
|
@ -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"
|
||||||
|
|
|
@ -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"
|
||||||
|
|
9
skyvern-frontend/src/hooks/useCostCalculator.ts
Normal file
9
skyvern-frontend/src/hooks/useCostCalculator.ts
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
import { CostCalculatorContext } from "@/store/CostCalculatorContext";
|
||||||
|
import { useContext } from "react";
|
||||||
|
|
||||||
|
function useCostCalculator() {
|
||||||
|
const costCalculator = useContext(CostCalculatorContext);
|
||||||
|
return costCalculator;
|
||||||
|
}
|
||||||
|
|
||||||
|
export { useCostCalculator };
|
|
@ -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">
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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>
|
||||||
|
|
90
skyvern-frontend/src/routes/tasks/detail/TaskInfo.tsx
Normal file
90
skyvern-frontend/src/routes/tasks/detail/TaskInfo.tsx
Normal 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 };
|
|
@ -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 };
|
8
skyvern-frontend/src/store/CostCalculatorContext.ts
Normal file
8
skyvern-frontend/src/store/CostCalculatorContext.ts
Normal 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 };
|
Loading…
Add table
Reference in a new issue