Skip to content

Commit

Permalink
Merge pull request giselles-ai#232 from toyamarinyon/copy-agent-for-d…
Browse files Browse the repository at this point in the history
…eveloper-mode

feat(playground): Add agent duplication feature with file handling
  • Loading branch information
toyamarinyon authored Dec 16, 2024
2 parents 4a5a130 + 1b7ebbc commit 6b512e5
Show file tree
Hide file tree
Showing 5 changed files with 212 additions and 2 deletions.
3 changes: 2 additions & 1 deletion app/(playground)/p/[agentId]/canary/actions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import { vercelBlobFileFolder, vercelBlobGraphFolder } from "./constants";

import { textGenerationPrompt } from "./lib/prompts";
import {
buildFileFolderPath,
buildGraphPath,
elementsToMarkdown,
langfuseModel,
Expand Down Expand Up @@ -418,7 +419,7 @@ export async function putGraph(graph: Graph) {

export async function remove(fileData: FileData) {
const blobList = await list({
prefix: pathJoin(vercelBlobFileFolder, fileData.id),
prefix: buildFileFolderPath(fileData.id),
});

if (blobList.blobs.length > 0) {
Expand Down
5 changes: 4 additions & 1 deletion app/(playground)/p/[agentId]/canary/lib/utils.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { createId } from "@paralleldrive/cuid2";
import { vercelBlobGraphFolder } from "../constants";
import { vercelBlobFileFolder, vercelBlobGraphFolder } from "../constants";
import type {
ArtifactId,
ConnectionId,
Expand Down Expand Up @@ -180,6 +180,9 @@ export function initGraph(): Graph {
};
}

export function buildFileFolderPath(fileId: FileId) {
return pathJoin(vercelBlobFileFolder, fileId);
}
export function buildGraphFolderPath(graphId: GraphId) {
return pathJoin(vercelBlobGraphFolder, graphId);
}
Expand Down
122 changes: 122 additions & 0 deletions app/dev/copy-agent/action.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
"use server";

import { agents, db } from "@/drizzle";
import { fetchCurrentUser } from "@/services/accounts";
import { fetchCurrentTeam } from "@/services/teams";
import { createId } from "@paralleldrive/cuid2";
import { copy, list } from "@vercel/blob";
import { putGraph } from "../../(playground)/p/[agentId]/canary/actions";
import {
buildFileFolderPath,
createFileId,
pathJoin,
} from "../../(playground)/p/[agentId]/canary/lib/utils";
import type {
AgentId,
Graph,
Node,
} from "../../(playground)/p/[agentId]/canary/types";

interface AgentDuplicationSuccess {
result: "success";
agentId: AgentId;
}
interface AgentDuplicationError {
result: "error";
message: string;
}
type AgentDuplicationResult = AgentDuplicationSuccess | AgentDuplicationError;

export async function copyAgentAction(
prev: AgentDuplicationResult | null,
formData: FormData,
): Promise<AgentDuplicationResult> {
const agentId = formData.get("agentId");
if (typeof agentId !== "string" || agentId.length === 0) {
return { result: "error", message: "Please fill in the agent id" };
}
const agent = await db.query.agents.findFirst({
where: (agents, { eq }) => eq(agents.id, agentId as AgentId),
});
if (agent === undefined || agent.graphUrl === null) {
return { result: "error", message: `${agentId} is not found.` };
}
const [user, team, graph] = await Promise.all([
fetchCurrentUser(),
fetchCurrentTeam(),
fetch(agent.graphUrl).then((res) => res.json() as unknown as Graph),
]);
const newNodes = await Promise.all(
graph.nodes.map(async (node) => {
if (node.content.type !== "files") {
return node;
}
const newData = await Promise.all(
node.content.data.map(async (fileData) => {
if (fileData.status !== "completed") {
return null;
}
const newFileId = createFileId();
const blobList = await list({
prefix: buildFileFolderPath(fileData.id),
});
let newFileBlobUrl = "";
let newTextDataUrl = "";
await Promise.all(
blobList.blobs.map(async (blob) => {
const copyResult = await copy(
blob.url,
buildFileFolderPath(newFileId),
{
access: "public",
},
);
if (blob.url === fileData.fileBlobUrl) {
newFileBlobUrl = copyResult.url;
}
if (blob.url === fileData.textDataUrl) {
newTextDataUrl = copyResult.url;
}
}),
);
return {
...fileData,
id: newFileId,
fileBlobUrl: newFileBlobUrl,
textDataUrl: newTextDataUrl,
};
}),
).then((data) => data.filter((d) => d !== null));
return {
...node,
content: {
...node.content,
data: newData,
},
} as Node;
}),
);
const { url } = await putGraph({ ...graph, nodes: newNodes });
const newAgentId = `agnt_${createId()}` as AgentId;
const newAgent = await db.insert(agents).values({
id: newAgentId,
name: `Copy of ${agent.name ?? agentId}`,
teamDbId: team.dbId,
creatorDbId: user.dbId,
graphUrl: url,
graphv2: {
agentId: newAgentId,
nodes: [],
xyFlow: {
nodes: [],
edges: [],
},
connectors: [],
artifacts: [],
webSearches: [],
mode: "edit",
flowIndexes: [],
},
});
return { result: "success", agentId: newAgentId };
}
55 changes: 55 additions & 0 deletions app/dev/copy-agent/agent-id-form.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
"use client";

import { CheckIcon, CopyCheckIcon } from "lucide-react";
import Link from "next/link";
import { useActionState } from "react";
import { Spinner } from "../tabui/components/spinner";
import { copyAgentAction } from "./action";

export default function AgentIdForm() {
const [state, formAction, isPending] = useActionState(copyAgentAction, null);
return (
<form action={formAction} data-pending={isPending}>
<div className="text-black--30">
<div>
<span>{">"} </span>
<input
type="text"
name="agentId"
className="outline-0"
ref={(ref) => {
ref?.focus();
function submitAgentId() {
if (/agnt_[a-z0-9]{24}/.test(ref?.value ?? "")) {
ref?.form?.requestSubmit();
}
}
ref?.addEventListener("input", submitAgentId);
return () => {
ref?.removeEventListener("input", submitAgentId);
};
}}
/>
</div>
{isPending && <Spinner />}
{!isPending && state?.result === "error" ? (
<p>{state.message}</p>
) : state?.result === "success" ? (
<div>
<div className="flex items-center gap-[4px]">
<CheckIcon size={16} className="text-green" /> <p>Success</p>
</div>
<a
className="underline"
href={`/p/${state.agentId}`}
rel="noopener noreferrer"
target="_blank"
>
Open in a new tab
</a>
</div>
) : null}
</div>
</form>
);
}
29 changes: 29 additions & 0 deletions app/dev/copy-agent/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import { agents, db } from "@/drizzle";
import { developerFlag } from "@/flags";
import { fetchCurrentUser } from "@/services/accounts";
import { fetchCurrentTeam } from "@/services/teams";
import { createId } from "@paralleldrive/cuid2";
import { notFound } from "next/navigation";
import { putGraph } from "../../(playground)/p/[agentId]/canary/actions";
import type {
AgentId,
Graph,
} from "../../(playground)/p/[agentId]/canary/types";
import AgentIdForm from "./agent-id-form";

export default async function CopyAgentPage() {
const developerMode = await developerFlag();
if (!developerMode) {
return notFound();
}

return (
<div className="font-mono p-8 text-black-30">
<p>
Please fill in the agent id, then copy it in your
account.(agnt_[0-9a-zA-Z]+)
</p>
<AgentIdForm />
</div>
);
}

0 comments on commit 6b512e5

Please sign in to comment.