Skip to content

Commit

Permalink
feat: implement workflow chat interface
Browse files Browse the repository at this point in the history
  • Loading branch information
ryanhopperlowe committed Nov 13, 2024
1 parent d57017d commit f2e0617
Show file tree
Hide file tree
Showing 8 changed files with 116 additions and 54 deletions.
30 changes: 24 additions & 6 deletions ui/admin/app/components/chat/Chat.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import { useState } from "react";

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

import { useChat } from "~/components/chat/ChatContext";
import { Chatbar } from "~/components/chat/Chatbar";
import { MessagePane } from "~/components/chat/MessagePane";
Expand All @@ -9,16 +11,23 @@ type ChatProps = React.HTMLAttributes<HTMLDivElement> & {
showStartButton?: boolean;
};

export function Chat({ className, showStartButton = false }: ChatProps) {
const { messages, threadId, mode, invoke, readOnly } = useChat();
export function Chat({ className }: ChatProps) {
const {
messages,
threadId,
mode,
invoke,
readOnly,
isInvoking,
isRunning,
} = useChat();
const [runTriggered, setRunTriggered] = useState(false);

const showMessagePane =
mode === "agent" ||
(mode === "workflow" && (threadId || runTriggered || !showStartButton));
(mode === "workflow" && (threadId || runTriggered || !readOnly));

const showStartButtonPane =
mode === "workflow" && showStartButton && !(threadId || runTriggered);
const showStartButtonPane = mode === "workflow" && !readOnly;

return (
<div className={`flex flex-col h-full ${className}`}>
Expand All @@ -34,13 +43,22 @@ export function Chat({ className, showStartButton = false }: ChatProps) {
{mode === "agent" && !readOnly && <Chatbar className="px-20" />}

{showStartButtonPane && (
<div className="flex justify-center items-center h-full px-20">
<div
className={cn("px-20 mb-4", {
"flex justify-center items-center h-full": !threadId,
})}
>
<Button
variant="secondary"
onClick={() => {
setRunTriggered(true);
invoke();
}}
className={cn({
"w-full": threadId,
})}
loading={isInvoking || isRunning}
disabled={isInvoking || isRunning}
>
Run
</Button>
Expand Down
35 changes: 7 additions & 28 deletions ui/admin/app/components/chat/ChatContext.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ type Mode = "agent" | "workflow";
interface ChatContextType {
messages: Message[];
mode: Mode;
processUserMessage: (text: string, sender: "user" | "agent") => void;
processUserMessage: (text: string) => void;
id: string;
threadId: string | undefined;
invoke: (prompt?: string) => void;
Expand All @@ -46,38 +46,17 @@ export function ChatProvider({
onCreateThreadId?: (threadId: string) => void;
readOnly?: boolean;
}) {
/**
* processUserMessage is responsible for adding the user's message to the chat and
* triggering the agent to respond to it.
*/
const processUserMessage = (text: string, sender: "user" | "agent") => {
if (mode === "workflow" || readOnly) return;
const newMessage: Message = { text, sender };

// insertMessage(newMessage);
handlePrompt(newMessage.text);
};

const invoke = (prompt?: string) => {
if (prompt && mode === "agent" && !readOnly) {
handlePrompt(prompt);
}
};
if (readOnly) return;

const handlePrompt = (prompt: string) => {
if (prompt && mode === "agent" && !readOnly) {
invokeAgent.execute({
slug: id,
prompt: prompt,
thread: threadId,
});
}
// do nothing if the mode is workflow
if (mode === "workflow") invokeAgent.execute({ slug: id });
else if (mode === "agent")
invokeAgent.execute({ slug: id, prompt: prompt, thread: threadId });
};

const invokeAgent = useAsync(InvokeService.invokeAgentWithStream, {
onSuccess: ({ threadId: responseThreadId }) => {
if (responseThreadId && !threadId) {
if (responseThreadId && responseThreadId !== threadId) {
// persist the threadId
onCreateThreadId?.(responseThreadId);

Expand All @@ -93,7 +72,7 @@ export function ChatProvider({
<ChatContext.Provider
value={{
messages,
processUserMessage,
processUserMessage: invoke,
mode,
id,
threadId,
Expand Down
2 changes: 1 addition & 1 deletion ui/admin/app/components/chat/Chatbar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ export function Chatbar({ className }: ChatbarProps) {
if (isRunning) return;

if (input.trim()) {
processUserMessage(input, "user");
processUserMessage(input);
setInput("");
}
};
Expand Down
4 changes: 2 additions & 2 deletions ui/admin/app/components/chat/MessagePane.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -24,9 +24,9 @@ export function MessagePane({
className,
classNames = {},
}: MessagePaneProps) {
const { readOnly, isRunning } = useChat();
const { readOnly, isRunning, mode } = useChat();

const isEmpty = messages.length === 0 && !readOnly;
const isEmpty = messages.length === 0 && !readOnly && mode === "agent";

return (
<div className={cn("flex flex-col h-full", className, classNames.root)}>
Expand Down
10 changes: 3 additions & 7 deletions ui/admin/app/components/chat/NoMessages.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,6 @@ import { Button } from "~/components/ui/button";
export function NoMessages() {
const { processUserMessage, isInvoking } = useChat();

const handleAddMessage = (content: string) => {
processUserMessage(content, "user");
};

return (
<div className="flex flex-col items-center justify-center space-y-4 text-center p-4 h-full">
<h2 className="text-2xl font-semibold">Start the conversation!</h2>
Expand All @@ -21,7 +17,7 @@ export function NoMessages() {
variant="secondary"
disabled={isInvoking}
onClick={() =>
handleAddMessage(
processUserMessage(
"Tell me who you are and what your objectives are."
)
}
Expand All @@ -33,7 +29,7 @@ export function NoMessages() {
variant="secondary"
disabled={isInvoking}
onClick={() =>
handleAddMessage(
processUserMessage(
"Tell me what tools you have available."
)
}
Expand All @@ -45,7 +41,7 @@ export function NoMessages() {
variant="secondary"
disabled={isInvoking}
onClick={() =>
handleAddMessage(
processUserMessage(
"Using your knowledge tools, tell me about your knowledge set."
)
}
Expand Down
4 changes: 1 addition & 3 deletions ui/admin/app/lib/service/api/primitives.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ interface ExtendedAxiosRequestConfig<D = unknown>
}

export async function request<T, R = AxiosResponse<T>, D = unknown>({
errorMessage = "Request failed",
errorMessage: _,
disableTokenRefresh,
...config
}: ExtendedAxiosRequestConfig<D>): Promise<R> {
Expand All @@ -34,8 +34,6 @@ export async function request<T, R = AxiosResponse<T>, D = unknown>({
...config,
});
} catch (error) {
console.error(errorMessage);

if (isAxiosError(error) && error.response?.status === 400) {
throw new BadRequestError(error.response.data);
}
Expand Down
15 changes: 14 additions & 1 deletion ui/admin/app/lib/service/routeService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,9 @@ const QueryParamSchemaMap = {
workflowId: z.string().optional(),
}),
"/workflows": z.undefined(),
"/workflows/:workflow": z.undefined(),
"/workflows/:workflow": z.object({
threadId: z.string().optional(),
}),
"/tools": z.undefined(),
"/users": z.undefined(),
} satisfies Record<keyof Routes, ZodSchema | null>;
Expand Down Expand Up @@ -61,6 +63,17 @@ function getUnknownQueryParams(pathname: string, search: string) {
} satisfies QueryParamInfo<"/threads">;
}

if (
new RegExp($path("/workflows/:workflow", { workflow: "(.*)" })).test(
pathname
)
) {
return {
path: "/workflows/:workflow",
query: parseSearchParams("/workflows/:workflow", search),
} satisfies QueryParamInfo<"/workflows/:workflow">;
}

return {};
}

Expand Down
70 changes: 64 additions & 6 deletions ui/admin/app/routes/_auth.workflows.$workflow.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,16 +2,36 @@ import {
ClientLoaderFunctionArgs,
redirect,
useLoaderData,
useNavigate,
} from "@remix-run/react";
import { $params } from "remix-routes";
import { $path } from "remix-routes";
import { z } from "zod";

import { WorkflowService } from "~/lib/service/api/workflowService";
import { RouteService } from "~/lib/service/routeService";
import { noop } from "~/lib/utils";

import { Chat } from "~/components/chat";
import { ChatProvider } from "~/components/chat/ChatContext";
import {
ResizableHandle,
ResizablePanel,
ResizablePanelGroup,
} from "~/components/ui/resizable";
import { Workflow } from "~/components/workflow";

export const clientLoader = async ({ params }: ClientLoaderFunctionArgs) => {
const { workflow: id } = $params("/workflows/:workflow", params);
export type SearchParams = z.infer<
(typeof RouteService.schemas)["/workflows/:workflow"]
>;

export const clientLoader = async ({
params,
request,
}: ClientLoaderFunctionArgs) => {
const { workflow: id } = RouteService.getPathParams(
"/workflows/:workflow",
params
);

if (!id) {
throw redirect("/threads");
Expand All @@ -20,11 +40,49 @@ export const clientLoader = async ({ params }: ClientLoaderFunctionArgs) => {
const workflow = await WorkflowService.getWorkflowById(id).catch(noop);
if (!workflow) throw redirect("/agents");

return { workflow };
const { threadId } =
RouteService.getQueryParams(
"/workflows/:workflow",
new URL(request.url).search
) || {};

return { workflow, threadId };
};

export default function ChatAgent() {
const { workflow } = useLoaderData<typeof clientLoader>();
const { workflow, threadId } = useLoaderData<typeof clientLoader>();

const navigate = useNavigate();

return <Workflow workflow={workflow} />;
return (
<div className="h-full flex flex-col overflow-hidden relative">
<ChatProvider
id={workflow.id}
mode="workflow"
threadId={threadId}
onCreateThreadId={(threadId) =>
navigate(
$path(
"/workflows/:workflow",
{ workflow: workflow.id },
{ threadId }
)
)
}
>
<ResizablePanelGroup
direction="horizontal"
className="flex-auto"
>
<ResizablePanel className="">
<Workflow workflow={workflow} />
</ResizablePanel>
<ResizableHandle withHandle />
<ResizablePanel>
<Chat />
</ResizablePanel>
</ResizablePanelGroup>
</ChatProvider>
</div>
);
}

0 comments on commit f2e0617

Please sign in to comment.