diff --git a/.env.example b/.env.example new file mode 100644 index 00000000000..c465df94cf6 --- /dev/null +++ b/.env.example @@ -0,0 +1,13 @@ +# Database Connection (PostgreSQL) +DATABASE_URL=postgresql://user:password@host/database + +# Anthropic API Key +ANTHROPIC_API_KEY=sk-ant-... + +# Google Cloud (for compute/storage) +GCP_SA_KEY={"type":"service_account",...} +GCP_PROJECT_ID=your-project +GCS_BUCKET_NAME=your-bucket + +# App URL (for callbacks/webhooks) +APP_URL=http://localhost:3000 diff --git a/.gitignore b/.gitignore new file mode 100644 index 00000000000..7b8da95f5e5 --- /dev/null +++ b/.gitignore @@ -0,0 +1,42 @@ +# See https://help.github.com/articles/ignoring-files/ for more about ignoring files. + +# dependencies +/node_modules +/.pnp +.pnp.* +.yarn/* +!.yarn/patches +!.yarn/plugins +!.yarn/releases +!.yarn/versions + +# testing +/coverage + +# next.js +/.next/ +/out/ + +# production +/build + +# misc +.DS_Store +*.pem + +# debug +npm-debug.log* +yarn-debug.log* +yarn-error.log* +.pnpm-debug.log* + +# env files (can opt-in for committing if needed) +.env* +!.env.example + +# vercel +.vercel + +# typescript +*.tsbuildinfo +next-env.d.ts diff --git a/README.md b/README.md index 01c868d68ce..611fff91b5e 100644 --- a/README.md +++ b/README.md @@ -1 +1,26 @@ -# vellum-assistant \ No newline at end of file +# Vellum Assistant + +AI-powered assistant platform by Vellum. + +## Repository Structure + +``` +/ +├── web/ # Next.js web application +├── editor-templates/ # Agent editor templates +└── .github/ # GitHub Actions workflows +``` + +## Web Application + +The web app lives in `/web`. See [web/README.md](./web/README.md) for setup instructions. + +```bash +cd web +npm install +npm run dev +``` + +## License + +Proprietary - Vellum AI diff --git a/editor-templates/default-editor.tsx b/editor-templates/default-editor.tsx new file mode 100644 index 00000000000..223c8d6f7fc --- /dev/null +++ b/editor-templates/default-editor.tsx @@ -0,0 +1,1383 @@ +interface EditorProps { + agentId: string; + username: string | null; +} + +interface Agent { + id: string; + name: string; + description: string; + created_by: string | null; + configuration: Record; + created_at: string; + updated_at: string; +} + +interface Message { + id: string; + role: "user" | "assistant"; + content: string; + timestamp: string; +} + +interface AgentDetails { + agentType: string | null; + instanceName: string | null; + zone: string | null; + ipAddress: string | null; + agentEmail: string | null; +} + +type AgentStatus = + | "healthy" + | "unhealthy" + | "stopped" + | "unreachable" + | "unknown" + | "checking" + | "starting" + | "getting_set_up" + | "setting_up" + | "provisioning_failed"; + +const SETUP_GRACE_PERIOD_MS = 10 * 60 * 1000; + +type TabId = + | "interaction" + | "architecture" + | "filesystem" + | "logs" + | "details"; + +interface FileEntry { + name: string; + type: "file" | "directory"; + size?: number; + modified?: string; + path: string; + children?: FileEntry[]; + isLoading?: boolean; +} + +const TABS: { id: TabId; label: string }[] = [ + { id: "interaction", label: "Interaction" }, + { id: "architecture", label: "Architecture" }, + { id: "filesystem", label: "File System" }, + { id: "logs", label: "Logs" }, + { id: "details", label: "Details" }, +]; + +function Editor({ agentId, username }: EditorProps) { + const [agent, setAgent] = useState(null); + const [isPageLoading, setIsPageLoading] = useState(true); + const [isSaving, setIsSaving] = useState(false); + const [isKilling, setIsKilling] = useState(false); + const [error, setError] = useState(null); + const [activeTab, setActiveTab] = useState("interaction"); + const [name, setName] = useState(""); + + const isOwner = !agent?.created_by || agent.created_by === username; + + const hasUnsavedChanges = useMemo(() => { + if (!agent) { + return false; + } + return name !== agent.name; + }, [agent, name]); + + const fetchAgent = useCallback(async () => { + try { + const response = await fetch(`/api/agents/${agentId}`); + if (!response.ok) { + throw new Error("Failed to fetch agent"); + } + const data = await response.json(); + setAgent(data); + setName(data.name); + } catch (err) { + setError(err instanceof Error ? err.message : "An error occurred"); + } finally { + setIsPageLoading(false); + } + }, [agentId]); + + useEffect(() => { + fetchAgent(); + }, [fetchAgent]); + + const handleSave = useCallback(async () => { + setIsSaving(true); + setError(null); + try { + const response = await fetch(`/api/agents/${agentId}`, { + method: "PUT", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ name }), + }); + if (!response.ok) { + throw new Error("Failed to save agent"); + } + const updatedAgent = await response.json(); + setAgent(updatedAgent); + } catch (err) { + setError(err instanceof Error ? err.message : "Failed to save agent"); + } finally { + setIsSaving(false); + } + }, [agentId, name]); + + const handleKill = useCallback(async () => { + if ( + !window.confirm( + "Are you sure you want to permanently delete this agent and its compute instance? This action cannot be undone." + ) + ) { + return; + } + setIsKilling(true); + setError(null); + try { + const response = await fetch(`/api/agents/${agentId}/kill`, { + method: "POST", + }); + if (!response.ok) { + throw new Error("Failed to kill agent"); + } + window.location.href = "/agents"; + } catch (err) { + setError(err instanceof Error ? err.message : "Failed to kill agent"); + setIsKilling(false); + } + }, [agentId]); + + if (isPageLoading) { + return ( +
+
+
+ ); + } + + if (error && !agent) { + return ( +
+

{error}

+ + Back to Agents + +
+ ); + } + + return ( + <> +
+
+
+ + ← Back to Agents + +
+
+ + setName(e.target.value) + } + disabled={!isOwner} + className="w-full bg-transparent text-base font-semibold text-zinc-900 focus:outline-none disabled:cursor-not-allowed disabled:opacity-60 sm:text-lg dark:text-white" + placeholder="Agent Name" + /> +
+
+
+ {error && ( + + {error} + + )} + {isOwner && ( + <> + + + + )} +
+ {error && ( + + {error} + + )} +
+
+ {TABS.map((tab) => ( + + ))} +
+
+ +
+
+ {activeTab === "interaction" && ( + + )} + {activeTab === "architecture" && ( + + )} + {activeTab === "filesystem" && ( + + )} + {activeTab === "logs" && } + {activeTab === "details" && } +
+
+ + ); +} + +function InteractionView({ + agentId, + agentName, + agentCreatedAt, +}: { + agentId: string; + agentName: string; + agentCreatedAt: string; +}) { + const [agentStatus, setAgentStatus] = useState("checking"); + const [statusMessage, setStatusMessage] = useState(null); + const [messages, setMessages] = useState([]); + const [input, setInput] = useState(""); + const [isLoading, setIsLoading] = useState(false); + const [isToggling, setIsToggling] = useState(false); + + const fetchMessages = useCallback(async () => { + try { + const response = await fetch(`/api/agents/${agentId}/messages`); + if (!response.ok) { + return; + } + const data = await response.json(); + setMessages( + (data.messages || []).map( + (msg: { + id: string; + role: "user" | "assistant"; + content: string; + timestamp: string; + }) => ({ + id: msg.id, + role: msg.role, + content: msg.content, + timestamp: msg.timestamp, + }) + ) + ); + } catch (fetchErr) { + console.error("Failed to fetch messages:", fetchErr); + } + }, [agentId]); + + useEffect(() => { + fetchMessages(); + const interval = setInterval(fetchMessages, 5000); + return () => clearInterval(interval); + }, [fetchMessages]); + + const checkHealth = useCallback(async () => { + try { + const response = await fetch(`/api/agents/${agentId}/health`); + if (!response.ok) { + setAgentStatus("unknown"); + return; + } + const data = await response.json(); + if ( + data.status === "unknown" && + data.message === "No compute instance configured" + ) { + setAgentStatus("getting_set_up"); + setStatusMessage(null); + } else if (data.status === "provisioning_failed") { + setAgentStatus("provisioning_failed"); + setStatusMessage(data.message || "Failed to create compute instance"); + } else if (data.status === "setting_up") { + setAgentStatus("setting_up"); + setStatusMessage(data.progress || null); + } else if (data.status === "unreachable" && agentCreatedAt) { + const agentAge = Date.now() - new Date(agentCreatedAt).getTime(); + if (agentAge < SETUP_GRACE_PERIOD_MS) { + setAgentStatus("getting_set_up"); + setStatusMessage(null); + } else { + setAgentStatus("unreachable"); + setStatusMessage(data.message || null); + } + } else { + setAgentStatus(data.status as AgentStatus); + setStatusMessage(data.message || null); + } + } catch (healthErr) { + console.error("Health check failed:", healthErr); + setAgentStatus("unknown"); + } + }, [agentId, agentCreatedAt]); + + useEffect(() => { + checkHealth(); + const interval = setInterval(checkHealth, 10000); + return () => clearInterval(interval); + }, [checkHealth]); + + const isAlive = agentStatus === "healthy"; + + const handleStart = useCallback(async () => { + setIsToggling(true); + try { + const response = await fetch(`/api/agents/${agentId}/start`, { + method: "POST", + }); + if (response.ok) { + setAgentStatus("checking"); + setTimeout(checkHealth, 5000); + } + } catch (startErr) { + console.error("Failed to start agent:", startErr); + } finally { + setIsToggling(false); + } + }, [agentId, checkHealth]); + + const handleStop = useCallback(async () => { + setIsToggling(true); + try { + const response = await fetch(`/api/agents/${agentId}/stop`, { + method: "POST", + }); + if (response.ok) { + setAgentStatus("checking"); + setTimeout(checkHealth, 5000); + } + } catch (stopErr) { + console.error("Failed to stop agent:", stopErr); + } finally { + setIsToggling(false); + } + }, [agentId, checkHealth]); + + const handleSubmit = useCallback( + async (e: { preventDefault: () => void }) => { + e.preventDefault(); + if (!input.trim() || isLoading || !isAlive) { + return; + } + + const userMessage: Message = { + id: crypto.randomUUID(), + role: "user", + content: input.trim(), + timestamp: new Date().toISOString(), + }; + + setMessages((prev) => [...prev, userMessage]); + setInput(""); + setIsLoading(true); + + try { + const response = await fetch(`/api/agents/${agentId}/messages`, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ content: userMessage.content }), + }); + + if (!response.ok) { + throw new Error("Failed to send message"); + } + + // Don't call fetchMessages() here - it causes UI flicker by replacing + // the optimistic message before the server has stored it. + // The polling interval will sync messages automatically. + } catch (sendErr) { + console.error("Failed to send message:", sendErr); + } finally { + setIsLoading(false); + } + }, + [input, isLoading, isAlive, agentId] + ); + + const getStatusDisplay = () => { + switch (agentStatus) { + case "healthy": + return { text: "Agent is alive", color: "bg-green-500", pulse: true }; + case "checking": + return { + text: "Checking status...", + color: "bg-yellow-500", + pulse: true, + }; + case "getting_set_up": + return { + text: "Getting set up...", + color: "bg-yellow-500", + pulse: true, + }; + case "setting_up": + return { + text: statusMessage ? `Setting up: ${statusMessage}` : "Setting up...", + color: "bg-yellow-500", + pulse: true, + }; + case "provisioning_failed": + return { text: "Setup failed", color: "bg-red-500", pulse: false }; + case "starting": + return { + text: "Agent is starting up...", + color: "bg-yellow-500", + pulse: true, + }; + case "stopped": + return { + text: "Agent is stopped", + color: "bg-zinc-400 dark:bg-zinc-600", + pulse: false, + }; + case "unreachable": + return { + text: "Agent is unreachable", + color: "bg-red-500", + pulse: false, + }; + case "unhealthy": + return { + text: "Agent is unhealthy", + color: "bg-red-500", + pulse: false, + }; + default: + return { + text: "Status unknown", + color: "bg-zinc-400 dark:bg-zinc-600", + pulse: false, + }; + } + }; + + const statusDisplay = getStatusDisplay(); + + return ( +
+
+
+
+
+ + {statusDisplay.text} + +
+ +
+
+ + {!isAlive ? ( +
+

+ {agentStatus === "checking" + ? "Checking agent status..." + : agentStatus === "starting" + ? "Agent is starting up..." + : agentStatus === "getting_set_up" + ? "Getting set up..." + : agentStatus === "setting_up" + ? "Setting up..." + : agentStatus === "provisioning_failed" + ? "Setup failed" + : "Agent is not running"} +

+

+ {agentStatus === "checking" + ? "Please wait while we check the agent status" + : agentStatus === "starting" + ? "Your agent's compute instance is booting up. This may take a minute." + : agentStatus === "getting_set_up" + ? "Your agent's compute instance is being created." + : agentStatus === "setting_up" + ? statusMessage ?? "Your agent is being configured." + : agentStatus === "provisioning_failed" + ? statusMessage ?? + "Failed to create the compute instance for this agent." + : "Start the agent to interact with it directly"} +

+
+ ) : ( + <> +
+ {messages.length === 0 ? ( +
+

+ Chat directly with {agentName} +

+
+ ) : ( +
+ {messages.map((message) => ( +
+ {message.role === "assistant" && ( +
+ + A + +
+ )} +
+

+ {message.content} +

+
+ {message.role === "user" && ( +
+ + U + +
+ )} +
+ ))} + {isLoading && ( +
+
+ + A + +
+
+
+
+
+
+
+
+
+ )} +
+ )} +
+ +
+
+