Merge branch 'dev' into Torantulino-patch-1

This commit is contained in:
Bently
2025-05-14 15:21:20 +01:00
committed by GitHub
8 changed files with 258 additions and 179 deletions

View File

@@ -1,5 +1,11 @@
"use client";
import React, { useCallback, useEffect, useMemo, useState } from "react";
import React, {
useCallback,
useEffect,
useMemo,
useRef,
useState,
} from "react";
import { useParams, useRouter } from "next/navigation";
import { exportAsJSONFile } from "@/lib/utils";
@@ -41,7 +47,7 @@ export default function AgentRunsPage(): React.ReactElement {
// ============================ STATE =============================
const [graph, setGraph] = useState<Graph | null>(null);
const [graph, setGraph] = useState<Graph | null>(null); // Graph version corresponding to LibraryAgent
const [agent, setAgent] = useState<LibraryAgent | null>(null);
const [agentRuns, setAgentRuns] = useState<GraphExecutionMeta[]>([]);
const [schedules, setSchedules] = useState<Schedule[]>([]);
@@ -60,7 +66,8 @@ export default function AgentRunsPage(): React.ReactElement {
useState<boolean>(false);
const [confirmingDeleteAgentRun, setConfirmingDeleteAgentRun] =
useState<GraphExecutionMeta | null>(null);
const { state, updateState } = useOnboarding();
const { state: onboardingState, updateState: updateOnboardingState } =
useOnboarding();
const [copyAgentDialogOpen, setCopyAgentDialogOpen] = useState(false);
const { toast } = useToast();
@@ -77,34 +84,43 @@ export default function AgentRunsPage(): React.ReactElement {
setSelectedSchedule(schedule);
}, []);
const [graphVersions, setGraphVersions] = useState<Record<number, Graph>>({});
const graphVersions = useRef<Record<number, Graph>>({});
const loadingGraphVersions = useRef<Record<number, Promise<Graph>>>({});
const getGraphVersion = useCallback(
async (graphID: GraphID, version: number) => {
if (graphVersions[version]) return graphVersions[version];
if (version in graphVersions.current)
return graphVersions.current[version];
if (version in loadingGraphVersions.current)
return loadingGraphVersions.current[version];
const graphVersion = await api.getGraph(graphID, version);
setGraphVersions((prev) => ({
...prev,
[version]: graphVersion,
}));
return graphVersion;
const pendingGraph = api.getGraph(graphID, version).then((graph) => {
graphVersions.current[version] = graph;
return graph;
});
// Cache promise as well to avoid duplicate requests
loadingGraphVersions.current[version] = pendingGraph;
return pendingGraph;
},
[api, graphVersions],
[api, graphVersions, loadingGraphVersions],
);
// Reward user for viewing results of their onboarding agent
useEffect(() => {
if (!state || !selectedRun || state.completedSteps.includes("GET_RESULTS"))
if (
!onboardingState ||
!selectedRun ||
onboardingState.completedSteps.includes("GET_RESULTS")
)
return;
if (selectedRun.id === state.onboardingAgentExecutionId) {
updateState({
completedSteps: [...state.completedSteps, "GET_RESULTS"],
if (selectedRun.id === onboardingState.onboardingAgentExecutionId) {
updateOnboardingState({
completedSteps: [...onboardingState.completedSteps, "GET_RESULTS"],
});
}
}, [selectedRun, state]);
}, [selectedRun, onboardingState, updateOnboardingState]);
const fetchAgents = useCallback(() => {
const refreshPageData = useCallback(() => {
api.getLibraryAgent(agentID).then((agent) => {
setAgent(agent);
@@ -119,38 +135,40 @@ export default function AgentRunsPage(): React.ReactElement {
new Set(agentRuns.map((run) => run.graph_version)).forEach((version) =>
getGraphVersion(agent.graph_id, version),
);
if (!selectedView.id && isFirstLoad && agentRuns.length > 0) {
// only for first load or first execution
setIsFirstLoad(false);
const latestRun = agentRuns.reduce((latest, current) => {
if (latest.started_at && !current.started_at) return current;
else if (!latest.started_at) return latest;
return latest.started_at > current.started_at ? latest : current;
}, agentRuns[0]);
selectView({ type: "run", id: latestRun.id });
}
});
});
if (selectedView.type == "run" && selectedView.id && agent) {
api
.getGraphExecutionInfo(agent.graph_id, selectedView.id)
.then(setSelectedRun);
}
}, [api, agentID, getGraphVersion, graph, selectedView, isFirstLoad, agent]);
}, [api, agentID, getGraphVersion, graph]);
// On first load: select the latest run
useEffect(() => {
fetchAgents();
// Only for first load or first execution
if (selectedView.id || !isFirstLoad || agentRuns.length == 0) return;
setIsFirstLoad(false);
const latestRun = agentRuns.reduce((latest, current) => {
if (latest.started_at && !current.started_at) return current;
else if (!latest.started_at) return latest;
return latest.started_at > current.started_at ? latest : current;
}, agentRuns[0]);
selectView({ type: "run", id: latestRun.id });
}, [agentRuns, isFirstLoad, selectedView.id, selectView]);
// Initial load
useEffect(() => {
refreshPageData();
}, []);
// Subscribe to websocket updates for agent runs
// Subscribe to WebSocket updates for agent runs
useEffect(() => {
if (!agent) return;
if (!agent?.graph_id) return;
// Subscribe to all executions for this agent
api.subscribeToGraphExecutions(agent.graph_id);
}, [api, agent]);
return api.onWebSocketConnect(() => {
refreshPageData(); // Sync up on (re)connect
// Subscribe to all executions for this agent
api.subscribeToGraphExecutions(agent.graph_id);
});
}, [api, agent?.graph_id, refreshPageData]);
// Handle execution updates
useEffect(() => {
@@ -179,24 +197,29 @@ export default function AgentRunsPage(): React.ReactElement {
};
}, [api, agent?.graph_id, selectedView.id]);
// load selectedRun based on selectedView
// Pre-load selectedRun based on selectedView
useEffect(() => {
if (selectedView.type != "run" || !selectedView.id || !agent) return;
if (selectedView.type != "run" || !selectedView.id) return;
const newSelectedRun = agentRuns.find((run) => run.id == selectedView.id);
if (selectedView.id !== selectedRun?.id) {
// Pull partial data from "cache" while waiting for the rest to load
setSelectedRun(newSelectedRun ?? null);
// Ensure corresponding graph version is available before rendering I/O
api
.getGraphExecutionInfo(agent.graph_id, selectedView.id)
.then(async (run) => {
await getGraphVersion(run.graph_id, run.graph_version);
setSelectedRun(run);
});
}
}, [api, selectedView, agent, agentRuns, selectedRun?.id, getGraphVersion]);
}, [api, selectedView, agentRuns, selectedRun?.id]);
// Load selectedRun based on selectedView; refresh on agent refresh
useEffect(() => {
if (selectedView.type != "run" || !selectedView.id || !agent) return;
api
.getGraphExecutionInfo(agent.graph_id, selectedView.id)
.then(async (run) => {
// Ensure corresponding graph version is available before rendering I/O
await getGraphVersion(run.graph_id, run.graph_version);
setSelectedRun(run);
});
}, [api, selectedView, agent, getGraphVersion]);
const fetchSchedules = useCallback(async () => {
if (!agent) return;
@@ -328,7 +351,7 @@ export default function AgentRunsPage(): React.ReactElement {
selectedRun && (
<AgentRunDetailsView
agent={agent}
graph={graphVersions[selectedRun.graph_version] ?? graph}
graph={graphVersions.current[selectedRun.graph_version] ?? graph}
run={selectedRun}
agentActions={agentActions}
onRun={(runID) => selectRun(runID)}

View File

@@ -176,6 +176,7 @@ export default function LoginPage() {
</AuthButton>
</form>
<AuthFeedback
type="login"
message={feedback}
isError={!!feedback}
behaveAs={getBehaveAs()}

View File

@@ -200,6 +200,7 @@ export default function ResetPasswordPage() {
Update password
</AuthButton>
<AuthFeedback
type="login"
message={feedback}
isError={isError}
behaveAs={getBehaveAs()}
@@ -242,6 +243,7 @@ export default function ResetPasswordPage() {
Send reset email
</AuthButton>
<AuthFeedback
type="login"
message={feedback}
isError={isError}
behaveAs={getBehaveAs()}

View File

@@ -106,7 +106,7 @@ export default function SignupPage() {
}
return (
<AuthCard className="mx-auto">
<AuthCard className="mx-auto mt-12">
<AuthHeader>Create a new account</AuthHeader>
<Form {...form}>
<form onSubmit={form.handleSubmit(onSignup)}>
@@ -215,6 +215,7 @@ export default function SignupPage() {
</form>
</Form>
<AuthFeedback
type="signup"
message={feedback}
isError={!!feedback}
behaveAs={getBehaveAs()}

View File

@@ -1,16 +1,17 @@
import { AlertCircle, CheckCircle } from "lucide-react";
import { Card, CardContent } from "@/components/ui/card";
import { Card, CardContent, CardHeader } from "@/components/ui/card";
import { HelpItem } from "@/components/auth/help-item";
import Link from "next/link";
import { BehaveAs } from "@/lib/utils";
interface Props {
type: "login" | "signup";
message?: string | null;
isError?: boolean;
behaveAs?: BehaveAs;
}
export default function AuthFeedback({
type,
message = "",
isError = false,
behaveAs = BehaveAs.CLOUD,
@@ -39,48 +40,45 @@ export default function AuthFeedback({
)}
{/* Cloud-specific help */}
{isError && behaveAs === BehaveAs.CLOUD && (
<div className="mt-2 space-y-2 text-sm">
<span className="block text-center font-medium text-red-500">
The provided email may not be allowed to sign up.
</span>
<ul className="space-y-2 text-slate-700">
<li className="flex items-start">
<span className="mr-2">-</span>
<span>
AutoGPT Platform is currently in closed beta. You can join{" "}
<Link
{isError &&
behaveAs === BehaveAs.CLOUD &&
(type === "signup" ? (
<Card className="overflow-hidden rounded-lg border border-slate-200 bg-white shadow-sm">
<CardContent className="p-0">
<div className="divide-y divide-slate-100">
<span className="my-3 block text-center text-sm font-medium text-red-500">
The provided email may not be allowed to sign up.
</span>
<HelpItem
title="AutoGPT Platform is currently in closed beta. "
description="You can join "
linkText="the waitlist here"
href="https://agpt.co/waitlist"
className="font-medium text-slate-950 underline hover:text-slate-700"
>
the waitlist here
</Link>
.
</span>
</li>
<li className="flex items-start">
<span className="mr-2">-</span>
<span>
Make sure you use the same email address you used to sign up for
the waitlist.
</span>
</li>
<li className="flex items-start">
<span className="mr-2">-</span>
<span>
You can self host the platform, visit our{" "}
<Link
/>
<HelpItem title="Make sure you use the same email address you used to sign up for the waitlist." />
<HelpItem
title="You can self host the platform!"
description="Visit our"
linkText="GitHub repository"
href="https://github.com/Significant-Gravitas/AutoGPT"
className="font-medium text-slate-950 underline hover:text-slate-700"
>
GitHub repository
</Link>
.
</span>
</li>
</ul>
</div>
)}
/>
</div>
</CardContent>
</Card>
) : (
<Card className="overflow-hidden rounded-lg border border-slate-200 bg-white shadow-sm">
<CardContent className="p-0">
<div className="divide-y divide-slate-100">
<HelpItem
title="Having trouble logging in?"
description="Make sure you've already "
linkText="signed up"
href="/signup"
/>
</div>
</CardContent>
</Card>
))}
{/* Local-specific help */}
{isError && behaveAs === BehaveAs.LOCAL && (

View File

@@ -3,29 +3,33 @@ import Link from "next/link";
interface HelpItemProps {
title: string;
description: string;
linkText: string;
href: string;
description?: string;
linkText?: string;
href?: string;
}
export function HelpItem({
title,
description,
linkText,
href,
href = "",
}: HelpItemProps) {
const external = !href.startsWith("/");
return (
<div className="p-4">
<h3 className="mb-1 text-sm font-medium text-slate-950">{title}</h3>
<p className="text-sm text-slate-600">
{description}{" "}
<Link
href={href}
className="inline-flex items-center font-medium text-slate-950 hover:text-slate-700"
>
{linkText}
<ExternalLink className="ml-1 h-3 w-3" />
</Link>
{linkText && (
<Link
href={href}
className="inline-flex items-center font-medium text-slate-950 hover:text-slate-700"
>
{linkText}
{external && <ExternalLink className="ml-1 h-3 w-3" />}
</Link>
)}
</p>
</div>
);

View File

@@ -110,28 +110,50 @@ export default function useAgentGraph(
// Subscribe to execution events
useEffect(() => {
api.onWebSocketMessage("node_execution_event", (data) => {
if (data.graph_exec_id != flowExecutionID) {
return;
}
setUpdateQueue((prev) => [...prev, data]);
});
const deregisterMessageHandler = api.onWebSocketMessage(
"node_execution_event",
(data) => {
if (data.graph_exec_id != flowExecutionID) {
return;
}
setUpdateQueue((prev) => [...prev, data]);
},
);
if (flowExecutionID) {
api
.subscribeToGraphExecution(flowExecutionID)
.then(() =>
console.debug(
`Subscribed to updates for execution #${flowExecutionID}`,
),
)
.catch((error) =>
console.error(
`Failed to subscribe to updates for execution #${flowExecutionID}:`,
error,
),
);
}
const deregisterConnectHandler =
flowID && flowExecutionID
? api.onWebSocketConnect(() => {
// Subscribe to execution updates
api
.subscribeToGraphExecution(flowExecutionID)
.then(() =>
console.debug(
`Subscribed to updates for execution #${flowExecutionID}`,
),
)
.catch((error) =>
console.error(
`Failed to subscribe to updates for execution #${flowExecutionID}:`,
error,
),
);
// Sync execution info to ensure it's up-to-date after (re)connect
api
.getGraphExecutionInfo(flowID, flowExecutionID)
.then((execution) =>
setUpdateQueue((prev) => {
if (!execution.node_executions) return prev;
return [...prev, ...execution.node_executions];
}),
);
})
: () => {};
return () => {
deregisterMessageHandler();
deregisterConnectHandler();
};
}, [api, flowID, flowVersion, flowExecutionID]);
const getOutputType = useCallback(

View File

@@ -1,5 +1,8 @@
import { SupabaseClient } from "@supabase/supabase-js";
import {
import getServerSupabase from "@/lib/supabase/getServerSupabase";
import { createBrowserClient } from "@supabase/ssr";
import type { SupabaseClient } from "@supabase/supabase-js";
import type {
AddUserCreditsResponse,
AnalyticsDetails,
AnalyticsMetrics,
APIKey,
@@ -13,6 +16,7 @@ import {
Credentials,
CredentialsDeleteNeedConfirmationResponse,
CredentialsDeleteResponse,
CredentialsMetaInput,
CredentialsMetaResponse,
Graph,
GraphCreatable,
@@ -32,8 +36,11 @@ import {
NodeExecutionResult,
NotificationPreference,
NotificationPreferenceDTO,
OttoQuery,
OttoResponse,
ProfileDetails,
RefundRequest,
ReviewSubmissionRequest,
Schedule,
ScheduleCreatable,
ScheduleID,
@@ -45,20 +52,13 @@ import {
StoreSubmission,
StoreSubmissionRequest,
StoreSubmissionsResponse,
SubmissionStatus,
TransactionHistory,
User,
UserPasswordCredentials,
OttoQuery,
OttoResponse,
UserOnboarding,
ReviewSubmissionRequest,
SubmissionStatus,
AddUserCreditsResponse,
UserPasswordCredentials,
UsersBalanceHistoryResponse,
CredentialsMetaInput,
} from "./types";
import { createBrowserClient } from "@supabase/ssr";
import getServerSupabase from "../supabase/getServerSupabase";
const isClient = typeof window !== "undefined";
@@ -67,11 +67,13 @@ export default class BackendAPI {
private wsUrl: string;
private webSocket: WebSocket | null = null;
private wsConnecting: Promise<void> | null = null;
private wsOnConnectHandlers: Set<() => void> = new Set();
private wsMessageHandlers: Record<string, Set<(data: any) => void>> = {};
heartbeatInterval: number | null = null;
readonly HEARTBEAT_INTERVAL = 10_0000; // 100 seconds
readonly HEARTBEAT_INTERVAL = 100_000; // 100 seconds
readonly HEARTBEAT_TIMEOUT = 10_000; // 10 seconds
heartbeatTimeoutId: number | null = null;
heartbeatIntervalID: number | null = null;
heartbeatTimeoutID: number | null = null;
constructor(
baseUrl: string = process.env.NEXT_PUBLIC_AGPT_SERVER_URL ||
@@ -699,6 +701,14 @@ export default class BackendAPI {
);
}
//////////////////////////////////
////////////// OTTO //////////////
//////////////////////////////////
async askOtto(query: OttoQuery): Promise<OttoResponse> {
return this._request("POST", "/otto/ask", query);
}
////////////////////////////////////////
////////// INTERNAL FUNCTIONS //////////
////////////////////////////////////////
@@ -707,10 +717,6 @@ export default class BackendAPI {
return this._request("GET", path, query);
}
async askOtto(query: OttoQuery): Promise<OttoResponse> {
return this._request("POST", "/otto/ask", query);
}
private async _uploadFile(path: string, file: File): Promise<string> {
// Get session with retry logic
let token = "no-token-found";
@@ -914,6 +920,25 @@ export default class BackendAPI {
return () => this.wsMessageHandlers[method].delete(handler);
}
/**
* All handlers are invoked when the WebSocket (re)connects. If it's already connected
* when this function is called, the passed handler is invoked immediately.
*
* Use this hook to subscribe to topics and refresh state,
* to ensure re-subscription and re-sync on re-connect.
*
* @returns a detacher for the passed handler.
*/
onWebSocketConnect(handler: () => void): () => void {
this.wsOnConnectHandlers.add(handler);
this.connectWebSocket();
if (this.webSocket?.readyState == WebSocket.OPEN) handler();
// Return detacher
return () => this.wsOnConnectHandlers.delete(handler);
}
async connectWebSocket(): Promise<void> {
this.wsConnecting ??= new Promise(async (resolve, reject) => {
try {
@@ -925,6 +950,7 @@ export default class BackendAPI {
this.webSocket.onopen = () => {
this._startWSHeartbeat(); // Start heartbeat when connection opens
this.wsOnConnectHandlers.forEach((handler) => handler());
resolve();
};
@@ -943,24 +969,7 @@ export default class BackendAPI {
reject(error);
};
this.webSocket.onmessage = (event) => {
const message: WebsocketMessage = JSON.parse(event.data);
// Handle heartbeat response
if (message.method === "heartbeat" && message.data === "pong") {
this._handleWSHeartbeatResponse();
return;
}
if (message.method === "node_execution_event") {
message.data = parseNodeExecutionResultTimestamps(message.data);
} else if (message.method == "graph_execution_event") {
message.data = parseGraphExecutionTimestamps(message.data);
}
this.wsMessageHandlers[message.method]?.forEach((handler) =>
handler(message.data),
);
};
this.webSocket.onmessage = (event) => this._handleWSMessage(event);
} catch (error) {
console.error("Error connecting to WebSocket:", error);
reject(error);
@@ -976,9 +985,28 @@ export default class BackendAPI {
}
}
_startWSHeartbeat() {
private _handleWSMessage(event: MessageEvent): void {
const message: WebsocketMessage = JSON.parse(event.data);
// Handle heartbeat response
if (message.method === "heartbeat" && message.data === "pong") {
this._handleWSHeartbeatResponse();
return;
}
if (message.method === "node_execution_event") {
message.data = parseNodeExecutionResultTimestamps(message.data);
} else if (message.method == "graph_execution_event") {
message.data = parseGraphExecutionTimestamps(message.data);
}
this.wsMessageHandlers[message.method]?.forEach((handler) =>
handler(message.data),
);
}
private _startWSHeartbeat() {
this._stopWSHeartbeat();
this.heartbeatInterval = window.setInterval(() => {
this.heartbeatIntervalID = window.setInterval(() => {
if (this.webSocket?.readyState === WebSocket.OPEN) {
this.webSocket.send(
JSON.stringify({
@@ -988,7 +1016,7 @@ export default class BackendAPI {
}),
);
this.heartbeatTimeoutId = window.setTimeout(() => {
this.heartbeatTimeoutID = window.setTimeout(() => {
console.warn("Heartbeat timeout - reconnecting");
this.webSocket?.close();
this.connectWebSocket();
@@ -997,21 +1025,21 @@ export default class BackendAPI {
}, this.HEARTBEAT_INTERVAL);
}
_stopWSHeartbeat() {
if (this.heartbeatInterval) {
clearInterval(this.heartbeatInterval);
this.heartbeatInterval = null;
private _stopWSHeartbeat() {
if (this.heartbeatIntervalID) {
clearInterval(this.heartbeatIntervalID);
this.heartbeatIntervalID = null;
}
if (this.heartbeatTimeoutId) {
clearTimeout(this.heartbeatTimeoutId);
this.heartbeatTimeoutId = null;
if (this.heartbeatTimeoutID) {
clearTimeout(this.heartbeatTimeoutID);
this.heartbeatTimeoutID = null;
}
}
_handleWSHeartbeatResponse() {
if (this.heartbeatTimeoutId) {
clearTimeout(this.heartbeatTimeoutId);
this.heartbeatTimeoutId = null;
private _handleWSHeartbeatResponse() {
if (this.heartbeatTimeoutID) {
clearTimeout(this.heartbeatTimeoutID);
this.heartbeatTimeoutID = null;
}
}
}