Skip to content

Commit

Permalink
enhance: workflows to user tasks, task runs & chat threads (#1695)
Browse files Browse the repository at this point in the history
* enhance: workflows -> user tasks, task runs & chat threads

* add filter & update tests

* style fixes

* change useRowNavigate

* add meta to Tasks

* Update _auth.chat-threads._index.tsx

* cronjob, account for taskSchedule & remove unused ScheduleForm

* remove preauth from workflow/task tools
  • Loading branch information
ivyjeong13 authored Feb 12, 2025
1 parent c66ee15 commit daa59c2
Show file tree
Hide file tree
Showing 57 changed files with 1,600 additions and 1,743 deletions.
8 changes: 4 additions & 4 deletions ui/admin/app/components/agent/AgentAlias.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -92,11 +92,11 @@ export function AgentAlias({ agent, onChange }: AgentAliasProps) {
className="text-accent-foreground underline"
to={
refAssistant.type === "agent"
? $path("/agents/:agent", {
agent: refAssistant.entityID,
? $path("/agents/:id", {
id: refAssistant.entityID,
})
: $path("/workflows/:workflow", {
workflow: refAssistant.entityID,
: $path("/tasks/:id", {
id: refAssistant.entityID,
})
}
>
Expand Down
36 changes: 21 additions & 15 deletions ui/admin/app/components/agent/AgentForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -36,13 +36,15 @@ type AgentFormProps = {
onSubmit?: (values: AgentInfoFormValues) => void;
onChange?: (values: AgentInfoFormValues) => void;
hideImageField?: boolean;
hideInstructionsField?: boolean;
};

export function AgentForm({
agent,
onSubmit,
onChange,
hideImageField,
hideInstructionsField,
}: AgentFormProps) {
const form = useForm<AgentInfoFormValues>({
resolver: zodResolver(formSchema),
Expand Down Expand Up @@ -94,21 +96,25 @@ export function AgentForm({
</div>
)}

<h4 className="flex items-center gap-2 border-b pb-2">
<BrainIcon className="h-5 w-5" />
Instructions
</h4>

<CardDescription>
Give the agent instructions on how to behave and respond to input.
</CardDescription>

<ControlledAutosizeTextarea
control={form.control}
autoComplete="off"
name="prompt"
maxHeight={300}
/>
{!hideInstructionsField && (
<>
<h4 className="flex items-center gap-2 border-b pb-2">
<BrainIcon className="h-5 w-5" />
Instructions
</h4>

<CardDescription>
Give the agent instructions on how to behave and respond to input.
</CardDescription>

<ControlledAutosizeTextarea
control={form.control}
autoComplete="off"
name="prompt"
maxHeight={300}
/>
</>
)}
</form>
</Form>
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -104,7 +104,7 @@ export function ProviderConfigure({
</DialogHeader>
<DialogDescription>
When no model is specified, a default model is used for creating a
new agent, workflow, or working with some tools, etc. Select your
new agent or when working with some tools, etc. Select your
default models for the usage types below.
</DialogDescription>
<div className="mt-4">
Expand Down
30 changes: 30 additions & 0 deletions ui/admin/app/components/chat/shared/thread-helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -134,3 +134,33 @@ export function useThreadTableRows({

return useSWR(disabled ? null : key, fetcher);
}

export const useThreadTasks = (agentThreadId?: string) => {
const { data: tasks, ...rest } = useSWR(
...ThreadsService.getWorkflowsForThread.swr({ threadId: agentThreadId })
);

const getThreads = useSWR(
agentThreadId ? ThreadsService.getThreads.key() : null,
() => ThreadsService.getThreads()
);

const runCounts = getThreads.data?.reduce<Record<string, number>>(
(acc, thread) => {
if (!thread.workflowID) return acc;

acc[thread.workflowID] = (acc[thread.workflowID] || 0) + 1;
return acc;
},
{}
);

return {
tasks:
tasks?.map((task) => ({
...task,
runCount: runCounts?.[task.id] ?? 0,
})) ?? [],
...rest,
};
};
5 changes: 4 additions & 1 deletion ui/admin/app/components/composed/ComboBox.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { CheckIcon, ChevronsUpDownIcon } from "lucide-react";
import { ReactNode, useState } from "react";

import { Button } from "~/components/ui/button";
import { Button, ButtonProps } from "~/components/ui/button";
import {
Command,
CommandEmpty,
Expand Down Expand Up @@ -31,6 +31,7 @@ type GroupedOption<T extends BaseOption> = {
type ComboBoxProps<T extends BaseOption> = {
allowClear?: boolean;
allowCreate?: boolean;
buttonProps?: ButtonProps;
clearLabel?: ReactNode;
emptyLabel?: ReactNode;
onChange: (option: T | null) => void;
Expand All @@ -44,6 +45,7 @@ type ComboBoxProps<T extends BaseOption> = {
};

export function ComboBox<T extends BaseOption>({
buttonProps,
disabled,
placeholder,
value,
Expand Down Expand Up @@ -97,6 +99,7 @@ export function ComboBox<T extends BaseOption>({
classNames={{
content: "w-full justify-between",
}}
{...buttonProps}
>
<span className="overflow-hidden text-ellipsis">
{renderOption && value
Expand Down
55 changes: 53 additions & 2 deletions ui/admin/app/components/composed/DataTable.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,12 @@ import {
getSortedRowModel,
useReactTable,
} from "@tanstack/react-table";
import { ListFilterIcon } from "lucide-react";
import { useNavigate } from "react-router";

import { cn } from "~/lib/utils";

import { ComboBox } from "~/components/composed/ComboBox";
import {
Table,
TableBody,
Expand All @@ -29,6 +32,7 @@ interface DataTableProps<TData, TValue> {
cell?: string;
};
onRowClick?: (row: TData) => void;
onCtrlClick?: (row: TData) => void;
disableClickPropagation?: (cell: Cell<TData, TValue>) => boolean;
}

Expand All @@ -40,6 +44,7 @@ export function DataTable<TData, TValue>({
classNames,
disableClickPropagation,
onRowClick,
onCtrlClick,
}: DataTableProps<TData, TValue>) {
const table = useReactTable({
data,
Expand Down Expand Up @@ -102,8 +107,11 @@ export function DataTable<TData, TValue>({
className={cn("py-4", classNames?.cell, {
"cursor-pointer": !!onRowClick,
})}
onClick={() => {
if (!disableClickPropagation?.(cell)) {
onClick={(e) => {
if (disableClickPropagation?.(cell)) return;
if (e.ctrlKey || e.metaKey) {
onCtrlClick?.(cell.row.original);
} else {
onRowClick?.(cell.row.original);
}
}}
Expand All @@ -113,3 +121,46 @@ export function DataTable<TData, TValue>({
);
}
}

export const useRowNavigate = <TData extends object | string>(
getPath: (row: TData) => string
) => {
const navigate = useNavigate();

const handleAction = (row: TData, ctrl: boolean) => {
const path = getPath(row);
if (ctrl) {
window.open(`/admin${path}`, "_blank");
} else {
navigate(path);
}
};

return {
internal: (row: TData) => handleAction(row, false),
external: (row: TData) => handleAction(row, true),
};
};

export const DataTableFilter = ({
field,
values,
onSelect,
}: {
field: string;
onSelect: (value: string) => void;
values: { id: string; name: string }[];
}) => {
return (
<ComboBox
buttonProps={{
className: "px-0 w-full",
variant: "text",
endContent: <ListFilterIcon />,
}}
placeholder={field}
onChange={(option) => onSelect(option?.id ?? "")}
options={values}
/>
);
};
97 changes: 97 additions & 0 deletions ui/admin/app/components/composed/Filters.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
import { XIcon } from "lucide-react";
import { useMemo } from "react";
import { useNavigate, useSearchParams } from "react-router";
import { $path, Routes } from "safe-routes";

import { Agent } from "~/lib/model/agents";
import { User } from "~/lib/model/users";
import { Workflow } from "~/lib/model/workflows";
import { RouteService } from "~/lib/service/routeService";

import { Button } from "~/components/ui/button";

type QueryParams = {
agentId?: string;
userId?: string;
taskId?: string;
};

export function Filters({
agentMap,
userMap,
workflowMap,
url,
}: {
agentMap?: Map<string, Agent>;
userMap?: Map<string, User>;
workflowMap?: Map<string, Workflow>;
url: keyof Routes;
}) {
const [searchParams] = useSearchParams();
const navigate = useNavigate();

const filters = useMemo(() => {
const query =
(RouteService.getQueryParams(
url,
searchParams.toString()
) as QueryParams) ?? {};
const { ...filters } = query; // TODO: from

const updateFilters = (param: keyof QueryParams) => {
const newQuery = { ...query };
delete newQuery[param];
// Filter out null/undefined values and ensure all values are strings
const cleanQuery = Object.fromEntries(
Object.entries(newQuery)
.filter(([_, v]) => v != null)
.map(([k, v]) => [k, String(v)])
) as Parameters<typeof $path>[1];
return navigate($path(url, cleanQuery));
};

return [
"agentId" in filters &&
filters.agentId &&
agentMap && {
key: "agentId",
label: "Agent",
value: agentMap.get(filters.agentId)?.name ?? filters.agentId,
onRemove: () => updateFilters("agentId"),
},
"userId" in filters &&
filters.userId &&
userMap && {
key: "userId",
label: "User",
value: userMap.get(filters.userId)?.email ?? filters.userId,
onRemove: () => updateFilters("userId"),
},
"taskId" in filters &&
filters.taskId &&
workflowMap && {
key: "taskId",
label: "Task",
value: workflowMap?.get(filters.taskId)?.name ?? filters.taskId,
onRemove: () => updateFilters("taskId"),
},
].filter((x) => !!x);
}, [navigate, searchParams, agentMap, userMap, workflowMap, url]);

return (
<div className="flex gap-2 pb-2">
{filters.map((filter) => (
<Button
key={filter.key}
size="badge"
onClick={filter.onRemove}
variant="accent"
shape="pill"
endContent={<XIcon />}
>
<b>{filter.label}:</b> {filter.value}
</Button>
))}
</div>
);
}
31 changes: 31 additions & 0 deletions ui/admin/app/components/composed/SearchInput.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import { SearchIcon } from "lucide-react";
import { useState } from "react";

import { Input } from "~/components/ui/input";
import { useDebounce } from "~/hooks/useDebounce";

export function SearchInput({
onChange,
placeholder = "Search...",
}: {
onChange: (value: string) => void;
placeholder?: string;
}) {
const [searchQuery, setSearchQuery] = useState("");
const debounceOnChange = useDebounce(onChange, 300);
return (
<div className="relative">
<Input
type="text"
placeholder={placeholder}
value={searchQuery}
onChange={(e) => {
setSearchQuery(e.target.value);
debounceOnChange(e.target.value);
}}
startContent={<SearchIcon className="h-5 w-5 text-gray-400" />}
className="w-64"
/>
</div>
);
}
2 changes: 1 addition & 1 deletion ui/admin/app/components/composed/SetupBanner.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ export function SetupBanner() {
step: "Configure Model Provider",
configured: modelProviderConfigured,
path: $path("/model-providers"),
description: "To use Agents and Workflows, configure a Model Provider",
description: "To create Agents, configure a Model Provider",
label: "Model Provider",
},
{
Expand Down
Loading

0 comments on commit daa59c2

Please sign in to comment.