Skip to content

Commit

Permalink
feat: allow users to trigger workflow authentication in the chat inte…
Browse files Browse the repository at this point in the history
…rface (#725)
  • Loading branch information
ryanhopperlowe authored Dec 2, 2024
1 parent 24cf54d commit dde0a22
Show file tree
Hide file tree
Showing 6 changed files with 125 additions and 53 deletions.
80 changes: 55 additions & 25 deletions ui/admin/app/components/chat/Message.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import { Button } from "~/components/ui/button";
import {
Dialog,
DialogContent,
DialogDescription,
DialogHeader,
DialogTitle,
DialogTrigger,
Expand Down Expand Up @@ -84,7 +85,7 @@ export const Message = React.memo(({ message }: MessageProps) => {
/>
)}

{message.prompt?.metadata ? (
{message.prompt ? (
<PromptMessage prompt={message.prompt} />
) : (
<Markdown
Expand Down Expand Up @@ -141,26 +142,51 @@ Message.displayName = "Message";

function PromptMessage({ prompt }: { prompt: AuthPrompt }) {
const [open, setOpen] = useState(false);
const [isSubmitted, setIsSubmitted] = useState(false);

if (!prompt.metadata) return null;
const getMessage = () => {
if (prompt.metadata?.authURL || prompt.metadata?.authType)
return `${prompt.metadata.category || "Tool call"} requires Authentication`;

return prompt.message;
};

const getCtaText = () => {
if (prompt.metadata?.authURL || prompt.metadata?.authType)
return ["Authenticate", prompt.metadata.category]
.filter(Boolean)
.join(" with ");

return "Submit Parameters";
};

const getSubmittedText = () => {
if (prompt.metadata?.authURL || prompt.metadata?.authType)
return "Authenticated";

return "Parameters Submitted";
};

if (isSubmitted) {
return (
<div className="flex-auto flex flex-col flex-wrap gap-2 w-fit">
<TypographyP className="min-w-fit">
{getSubmittedText()}
</TypographyP>
</div>
);
}

return (
<div className="flex-auto flex flex-col flex-wrap gap-2 w-fit">
<TypographyP className="min-w-fit">
<b>
{[prompt.metadata?.category, prompt.name]
.filter(Boolean)
.join(" - ")}
</b>
{": "}
Tool Call requires authentication
</TypographyP>

{prompt.metadata.authType === "oauth" && (
<TypographyP className="min-w-fit">{getMessage()}</TypographyP>

{prompt.metadata?.authURL && (
<Link
as="button"
rel="noreferrer"
target="_blank"
onClick={() => setIsSubmitted(true)}
to={prompt.metadata.authURL}
>
<ToolIcon
Expand All @@ -169,37 +195,41 @@ function PromptMessage({ prompt }: { prompt: AuthPrompt }) {
name={prompt.name}
disableTooltip
/>
Authenticate with {prompt.metadata.category}

{getCtaText()}
</Link>
)}

{prompt.metadata.authType === "basic" && prompt.fields && (
{prompt.fields && (
<Dialog open={open} onOpenChange={setOpen}>
<DialogTrigger asChild>
<DialogTrigger disabled={isSubmitted} asChild>
<Button
startContent={
<ToolIcon
icon={prompt.metadata.icon}
category={prompt.metadata.category}
icon={prompt.metadata?.icon}
category={prompt.metadata?.category}
name={prompt.name}
disableTooltip
/>
}
>
Authenticate with {prompt.metadata.category}
{getCtaText()}
</Button>
</DialogTrigger>

<DialogContent>
<DialogHeader>
<DialogTitle>
Authenticate with {prompt.metadata.category}
</DialogTitle>
<DialogTitle>{getCtaText()}</DialogTitle>
</DialogHeader>

<DialogDescription>{prompt.message}</DialogDescription>

<PromptAuthForm
prompt={prompt}
onSuccess={() => setOpen(false)}
onSuccess={() => {
setOpen(false);
setIsSubmitted(true);
}}
/>
</DialogContent>
</Dialog>
Expand Down Expand Up @@ -242,7 +272,7 @@ function PromptAuthForm({
control={form.control}
name={field}
label={field}
type={field.includes("password") ? "password" : "text"}
type={prompt.sensitive ? "password" : "text"}
/>
))}

Expand All @@ -251,7 +281,7 @@ function PromptAuthForm({
loading={authenticate.isLoading}
type="submit"
>
Authenticate
Submit
</Button>
</form>
</Form>
Expand Down
24 changes: 22 additions & 2 deletions ui/admin/app/components/workflow/Workflow.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,19 +2,22 @@ import {
KeyIcon,
Library,
List,
LockIcon,
PuzzleIcon,
Variable,
WrenchIcon,
} from "lucide-react";
import { useCallback, useState } from "react";

import { Workflow as WorkflowType } from "~/lib/model/workflows";
import { WorkflowService } from "~/lib/service/api/workflowService";
import { cn } from "~/lib/utils";

import { TypographyH4, TypographyP } from "~/components/Typography";
import { AgentForm } from "~/components/agent";
import { AgentKnowledgePanel } from "~/components/knowledge";
import { BasicToolForm } from "~/components/tools/BasicToolForm";
import { Button } from "~/components/ui/button";
import { CardDescription } from "~/components/ui/card";
import { ScrollArea } from "~/components/ui/scroll-area";
import { ParamsForm } from "~/components/workflow/ParamsForm";
Expand All @@ -25,10 +28,12 @@ import {
} from "~/components/workflow/WorkflowContext";
import { WorkflowEnvForm } from "~/components/workflow/WorkflowEnvForm";
import { StepsForm } from "~/components/workflow/steps/StepsForm";
import { useAsync } from "~/hooks/useAsync";
import { useDebounce } from "~/hooks/useDebounce";

type WorkflowProps = {
workflow: WorkflowType;
onPersistThreadId: (threadId: string) => void;
className?: string;
};

Expand All @@ -40,7 +45,7 @@ export function Workflow(props: WorkflowProps) {
);
}

function WorkflowContent({ className }: WorkflowProps) {
function WorkflowContent({ className, onPersistThreadId }: WorkflowProps) {
const { workflow, updateWorkflow, isUpdating, lastUpdated } = useWorkflow();

const [workflowUpdates, setWorkflowUpdates] = useState(workflow);
Expand All @@ -62,6 +67,10 @@ function WorkflowContent({ className }: WorkflowProps) {

const debouncedSetWorkflowInfo = useDebounce(partialSetWorkflow, 1000);

const authenticate = useAsync(WorkflowService.authenticateWorkflow, {
onSuccess: ({ threadId }) => onPersistThreadId(threadId),
});

return (
<div className="h-full flex flex-col">
<ScrollArea className={cn("h-full", className)}>
Expand Down Expand Up @@ -168,14 +177,25 @@ function WorkflowContent({ className }: WorkflowProps) {
</div>
</ScrollArea>

<footer className="flex justify-between items-center p-4 gap-4 text-muted-foreground">
<footer className="flex justify-between items-center p-4 gap-4 border-t text-muted-foreground">
{isUpdating ? (
<TypographyP>Saving...</TypographyP>
) : lastUpdated ? (
<TypographyP>Saved</TypographyP>
) : (
<div />
)}

<div className="flex items-center gap-2">
<Button
onClick={() => authenticate.execute(workflow.id)}
loading={authenticate.isLoading}
disabled={authenticate.isLoading}
startContent={<LockIcon />}
>
Authenticate
</Button>
</div>
</footer>
</div>
);
Expand Down
23 changes: 8 additions & 15 deletions ui/admin/app/lib/model/chatEvents.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,20 +13,13 @@ export type ToolCall = {
};
};

type PromptAuthMetaBase = {
category: string;
icon: string;
toolContext: string;
toolDisplayName: string;
};

type PromptOAuthMeta = PromptAuthMetaBase & {
authType: "oauth";
authURL: string;
};

type PromptAuthBasicMeta = PromptAuthMetaBase & {
authType: "basic";
type PromptAuthMeta = {
authURL?: string;
category?: string;
icon?: string;
toolContext?: string;
toolDisplayName?: string;
authType: "oauth" | "basic";
};

export type AuthPrompt = {
Expand All @@ -36,7 +29,7 @@ export type AuthPrompt = {
message: string;
fields?: string[];
sensitive?: boolean;
metadata?: PromptOAuthMeta | PromptAuthBasicMeta;
metadata?: PromptAuthMeta;
};

// note(ryanhopperlowe) renaming this to ChatEvent to differentiate itself specifically for a chat with an agent
Expand Down
2 changes: 2 additions & 0 deletions ui/admin/app/lib/routers/apiRoutes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,8 @@ export const ApiRoutes = {
buildUrl(`/workflows/${workflowId}/files/${fileName}`),
deleteKnowledge: (workflowId: string, fileName: string) =>
buildUrl(`/workflows/${workflowId}/files/${fileName}`),
authenticate: (workflowId: string) =>
buildUrl(`/workflows/${workflowId}/authenticate`),
},
threads: {
base: () => buildUrl("/threads"),
Expand Down
21 changes: 20 additions & 1 deletion ui/admin/app/lib/service/api/workflowService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import {
Workflow,
} from "~/lib/model/workflows";
import { ApiRoutes, revalidateWhere } from "~/lib/routers/apiRoutes";
import { request } from "~/lib/service/api/primitives";
import { ResponseHeaders, request } from "~/lib/service/api/primitives";

async function getWorkflows() {
const res = await request<{ items: Workflow[] }>({
Expand Down Expand Up @@ -69,11 +69,30 @@ async function deleteWorkflow(id: string) {
const revalidateWorkflows = () =>
revalidateWhere((url) => url.includes(ApiRoutes.workflows.base().path));

async function authenticateWorkflow(workflowId: string) {
const response = await request<ReadableStream>({
url: ApiRoutes.workflows.authenticate(workflowId).url,
method: "POST",
headers: { Accept: "text/event-stream" },
responseType: "stream",
errorMessage: "Failed to invoke agenticate workflow",
});

const reader = response.data
?.pipeThrough(new TextDecoderStream())
.getReader();

const threadId = response.headers[ResponseHeaders.ThreadId] as string;

return { reader, threadId };
}

export const WorkflowService = {
getWorkflows,
getWorkflowById,
createWorkflow,
updateWorkflow,
deleteWorkflow,
revalidateWorkflows,
authenticateWorkflow,
};
28 changes: 18 additions & 10 deletions ui/admin/app/routes/_auth.workflows.$workflow.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import {
useLoaderData,
useNavigate,
} from "@remix-run/react";
import { useCallback } from "react";
import { $path } from "remix-routes";
import { preload } from "swr";

Expand Down Expand Up @@ -48,28 +49,35 @@ export default function ChatAgent() {

const navigate = useNavigate();

const onPersistThreadId = useCallback(
(threadId: string) =>
navigate(
$path(
"/workflows/:workflow",
{ workflow: workflow.id },
{ threadId }
)
),
[navigate, workflow.id]
);

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 }
)
)
}
onCreateThreadId={onPersistThreadId}
>
<ResizablePanelGroup
direction="horizontal"
className="flex-auto"
>
<ResizablePanel className="">
<Workflow workflow={workflow} />
<Workflow
workflow={workflow}
onPersistThreadId={onPersistThreadId}
/>
</ResizablePanel>
<ResizableHandle withHandle />
<ResizablePanel>
Expand Down

0 comments on commit dde0a22

Please sign in to comment.