Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Prompt demo UI and CLI command #222

Merged
merged 11 commits into from
May 8, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
213 changes: 213 additions & 0 deletions admin/app/clusters/[clusterId]/tasks/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,213 @@
"use client";

import { client } from "@/client/client";
import { DataTable } from "@/components/ui/DataTable";
import { useAuth } from "@clerk/nextjs";
import { ScrollArea } from "@radix-ui/react-scroll-area";
import { formatRelative } from "date-fns";
import { useEffect, useState } from "react";
import toast from "react-hot-toast";
import { ThreeDots } from "react-loader-spinner";
import { functionStatusToCircle } from "../helpers";
import Link from "next/link";
import { Button } from "@/components/ui/button";

export default function Page({ params }: { params: { clusterId: string } }) {
const { getToken, isLoaded, isSignedIn } = useAuth();

const [data, setData] = useState<{
loading: boolean;
taskId: string | null;
result: string | null;
jobs: {
id: string;
createdAt: Date;
targetFn: string;
status: string;
functionExecutionTime: number | null;
}[];
}>({
loading: false,
taskId: null,
result: null,
jobs: [],
});

const handleKeyDown = (event: React.KeyboardEvent<HTMLInputElement>) => {
if (event.key === "Enter") {
callPrompt();
}
};

const callPrompt = async () => {
setData({
loading: true,
taskId: null,
result: null,
jobs: [],
});

const taskPrompt = (document.getElementById("prompt") as HTMLInputElement)
.value;

const result = await client.executeTask({
headers: {
authorization: `Bearer ${getToken()}`,
},
params: {
clusterId: params.clusterId,
},
body: {
task: taskPrompt,
},
});

if (result.status === 200) {
setData({
loading: false,
taskId: result.body.taskId,
result: result.body.result,
jobs: data.jobs,
});
} else {
setData({
loading: false,
taskId: null,
result: "Failed to execute task",
jobs: data.jobs,
});
}
};

useEffect(() => {
const fetchData = async () => {
if (!data.taskId) {
return;
}

const clusterResult = await client.getClusterDetailsForUser({
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is really just to demo as it will only show recent job executions and files them on the FE for now.

headers: {
authorization: `Bearer ${await getToken()}`,
},
params: {
clusterId: params.clusterId,
},
});

if (clusterResult.status === 401) {
window.location.reload();
}

if (clusterResult.status === 200) {
setData({
loading: data.loading,
taskId: data.taskId,
result: data.result,
jobs: clusterResult.body.jobs,
});
} else {
toast.error("Failed to fetch cluster details.");
}
};

const interval = setInterval(fetchData, 500); // Refresh every 500ms

return () => {
clearInterval(interval); // Clear the interval when the component unmounts
};
}, [params.clusterId, isLoaded, isSignedIn, getToken, data]);

return (
<div className="flex flex-row mt-8 space-x-12 mb-12">
<div className="flex-grow">
<h2 className="text-xl mb-4">Execute Agent Task</h2>
<p className="text-gray-400 mb-8">
Prompt the cluster to execute a task.
</p>
<div className="flex flex-row space-x-4 mb-2">
<input
type="text"
placeholder="Enter your task prompt here"
disabled={data.loading || data.taskId !== null}
className="flex-grow p-2 rounded-md bg-blue-400 placeholder-blue-200 text-white"
id="prompt"
onKeyDown={handleKeyDown}
/>
<Button
size="sm"
disabled={data.loading || data.taskId !== null}
onClick={callPrompt}
>
Execute
</Button>
</div>
{data.result && (
<div className="flex-grow rounded-md border mb-4 p-4">
<pre>{data.result}</pre>
</div>
)}
{data.loading && (
<div className="flex-grow rounded-md border mb-4 p-4">
<ThreeDots
visible={true}
height="20"
width="20"
color="#9ca3af"
ariaLabel="three-dots-loading"
/>
</div>
)}
<ScrollArea className="rounded-md border" style={{ height: 400 }}>
<DataTable
data={data.jobs
.sort((a, b) => (a.createdAt > b.createdAt ? -1 : 1))
.filter((s) => s.id.startsWith(data.taskId || ""))
.map((s) => ({
jobId: s.id,
targetFn: s.targetFn,
status: s.status,
createdAt: formatRelative(new Date(s.createdAt), new Date()),
functionExecutionTime: s.functionExecutionTime,
}))}
noDataMessage="No functions have been performed as part of the task."
columnDef={[
{
accessorKey: "jobId",
header: "Execution ID",
cell: ({ row }) => {
const jobId: string = row.getValue("jobId");

return (
<Link
className="font-mono text-md underline"
href={`/clusters/${params.clusterId}/activity?jobId=${jobId}`}
>
{jobId.substring(jobId.length - 6)}
</Link>
);
},
},
{
accessorKey: "targetFn",
header: "Function",
},
{
accessorKey: "createdAt",
header: "Called",
},
{
accessorKey: "status",
header: "",
cell: ({ row }) => {
const status = row.getValue("status");

return functionStatusToCircle(status as string);
},
},
]}
/>
</ScrollArea>
</div>
</div>
);
}
23 changes: 23 additions & 0 deletions admin/client/contract.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ export const definition = {
.array(
z.object({
name: z.string(),
schema: z.string(),
}),
)
.optional(),
Expand Down Expand Up @@ -394,6 +395,7 @@ export const definition = {
query: z.object({
jobId: z.string().optional(),
deploymentId: z.string().optional(),
taskId: z.string().optional(),
}),
},
createDeployment: {
Expand Down Expand Up @@ -712,6 +714,27 @@ export const definition = {
401: z.undefined(),
},
},
executeTask: {
method: "POST",
path: "/clusters/:clusterId/task",
headers: z.object({
authorization: z.string(),
}),
body: z.object({
task: z.string(),
}),
responses: {
401: z.undefined(),
404: z.undefined(),
200: z.object({
result: z.any(),
taskId: z.string(),
}),
500: z.object({
error: z.string(),
}),
},
},
} as const;

export const contract = c.router(definition);
1 change: 1 addition & 0 deletions admin/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@
"react-hook-form": "^7.50.1",
"react-hot-toast": "^2.4.1",
"react-json-pretty": "^2.2.0",
"react-loader-spinner": "^6.1.6",
"react-syntax-highlighter": "^15.5.0",
"recharts": "^2.10.4",
"tailwind-merge": "^2.2.0",
Expand Down
21 changes: 21 additions & 0 deletions cli/src/client/contract.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ export const definition = {
.array(
z.object({
name: z.string(),
schema: z.string(),
}),
)
.optional(),
Expand Down Expand Up @@ -712,6 +713,26 @@ export const definition = {
401: z.undefined(),
},
},
executeTask: {
method: "POST",
path: "/clusters/:clusterId/task",
headers: z.object({
authorization: z.string(),
}),
body: z.object({
task: z.string(),
}),
responses: {
401: z.undefined(),
404: z.undefined(),
200: z.object({
result: z.any(),
}),
500: z.object({
error: z.string(),
}),
},
},
} as const;

export const contract = c.router(definition);
70 changes: 70 additions & 0 deletions cli/src/commands/task.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
import { CommandModule } from "yargs";
import { selectCluster } from "../utils";
import { client } from "../lib/client";
import { input } from "@inquirer/prompts";

interface TaskArgs {
cluster?: string;
task?: string;
}

export const Task: CommandModule<{}, TaskArgs> = {
command: "task",
describe: "Execute a task in the cluster using a human readable prompt",
builder: (yargs) =>
yargs
.option("cluster", {
describe: "Cluster ID",
demandOption: false,
type: "string",
})
.option("task", {
describe: "Task for the cluster to perform",
demandOption: false,
type: "string",
}),
handler: async ({ cluster, task }) => {
if (!cluster) {
cluster = await selectCluster();
if (!cluster) {
console.log("No cluster selected");
return;
}
}

if (!task) {
task = await input({
message: "Human readable prompt for the cluster to perform",
validate: (value) => {
if (!value) {
return "Prompt is required";
}
return true;
},
});
}

try {
const result = await executeTask(cluster, task);
console.log(result);
} catch (e) {
console.error(e);
}
},
};

const executeTask = async (clusterId: string, task: string) => {
const result = await client.executeTask({
params: {
clusterId,
},
body: {
task,
},
});

if (result.status !== 200) {
throw new Error(`Failed to prompt cluster: ${result.status}`);
}
return result.body.result;
};
2 changes: 2 additions & 0 deletions cli/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import { Repl } from "./commands/repl";
import { Context } from "./commands/context";
import { setCurrentContext } from "./lib/context";
import { Services } from "./commands/services";
import { Task } from "./commands/task";

const cli = yargs(hideBin(process.argv))
.scriptName("differential")
Expand All @@ -35,6 +36,7 @@ if (authenticated) {
.command(ClientLibrary)
.command(Repl)
.command(Context)
.command(Task)
.command(Services);
} else {
console.log(
Expand Down
Loading
Loading