fix(frontend): ask for credentials in onboarding agent run (#11146)

## Changes 🏗️

<img width="800" height="852" alt="Screenshot_2025-10-13_at_19 20 47"
src="https://github.com/user-attachments/assets/2fc150b9-1053-4e25-9018-24bcc2d93b43"
/>

<img width="800" height="669" alt="Screenshot 2025-10-13 at 19 23 41"
src="https://github.com/user-attachments/assets/9078b04e-0f65-42f3-ac4a-d2f3daa91215"
/>

- Onboarding “Run” step now renders required credentials (e.g., Google
OAuth) and includes them in execution.
- Run button remains disabled until required inputs and credentials are
provided.
- Logic extracted and strongly typed; removed any usage.

## Checklist 📋

### For code changes:
- [x] I have clearly listed my changes in the PR description
- [x] I have made a test plan
- [ ] I have tested my changes according to the test plan ( _once merged
in dev..._ )
  - [ ] Select an onboarding agent that requires Google OAuth:
  - [ ] Credentials selector appears.
  - [ ] After selecting/signing in, “Run agent” enables.
  - [ ]Run succeeds and navigates to the next step.

### For configuration changes:

None
This commit is contained in:
Ubbe
2025-10-13 16:51:45 +04:00
committed by GitHub
parent 99ac206272
commit c7dcbc64ec
3 changed files with 169 additions and 38 deletions

View File

@@ -0,0 +1,93 @@
import type { GraphMeta } from "@/lib/autogpt-server-api";
import type {
BlockIOCredentialsSubSchema,
CredentialsMetaInput,
} from "@/lib/autogpt-server-api/types";
import type { InputValues } from "./types";
export function computeInitialAgentInputs(
agent: GraphMeta | null,
existingInputs?: InputValues | null,
): InputValues {
const properties = agent?.input_schema?.properties || {};
const result: InputValues = {};
Object.entries(properties).forEach(([key, subSchema]) => {
if (
existingInputs &&
key in existingInputs &&
existingInputs[key] != null
) {
result[key] = existingInputs[key];
return;
}
// GraphIOSubSchema.default is typed as string, but server may return other primitives
const def = (subSchema as unknown as { default?: string | number }).default;
result[key] = def ?? "";
});
return result;
}
export function getAgentCredentialsInputFields(agent: GraphMeta | null) {
const hasNoInputs =
!agent?.credentials_input_schema ||
typeof agent.credentials_input_schema !== "object" ||
!("properties" in agent.credentials_input_schema) ||
!agent.credentials_input_schema.properties;
if (hasNoInputs) return {};
return agent.credentials_input_schema.properties;
}
export function areAllCredentialsSet(
fields: Record<string, BlockIOCredentialsSubSchema>,
inputs: Record<string, CredentialsMetaInput | undefined>,
) {
const required = Object.keys(fields || {});
return required.every((k) => Boolean(inputs[k]));
}
type IsRunDisabledParams = {
agent: GraphMeta | null;
isRunning: boolean;
agentInputs: InputValues | null | undefined;
credentialsRequired: boolean;
credentialsSatisfied: boolean;
};
export function isRunDisabled({
agent,
isRunning,
agentInputs,
credentialsRequired,
credentialsSatisfied,
}: IsRunDisabledParams) {
const hasEmptyInput = Object.values(agentInputs || {}).some(
(value) => String(value).trim() === "",
);
if (hasEmptyInput) return true;
if (!agent) return true;
if (isRunning) return true;
if (credentialsRequired && !credentialsSatisfied) return true;
return false;
}
export function getSchemaDefaultCredentials(
schema: BlockIOCredentialsSubSchema,
): CredentialsMetaInput | undefined {
return schema.default as CredentialsMetaInput | undefined;
}
export function sanitizeCredentials(
map: Record<string, CredentialsMetaInput | undefined>,
): Record<string, CredentialsMetaInput> {
const sanitized: Record<string, CredentialsMetaInput> = {};
for (const [key, value] of Object.entries(map)) {
if (value) sanitized[key] = value;
}
return sanitized;
}

View File

@@ -13,13 +13,24 @@ import {
} from "@/components/__legacy__/ui/card";
import { useToast } from "@/components/molecules/Toast/use-toast";
import { GraphMeta, StoreAgentDetails } from "@/lib/autogpt-server-api";
import type { InputValues } from "./types";
import { useBackendAPI } from "@/lib/autogpt-server-api/context";
import { cn } from "@/lib/utils";
import { Play } from "lucide-react";
import { useRouter } from "next/navigation";
import { useCallback, useEffect, useState } from "react";
import { useEffect, useState } from "react";
import { RunAgentInputs } from "@/app/(platform)/library/agents/[id]/components/AgentRunsView/components/RunAgentInputs/RunAgentInputs";
import { InformationTooltip } from "@/components/molecules/InformationTooltip/InformationTooltip";
import { CredentialsInput } from "@/app/(platform)/library/agents/[id]/components/AgentRunsView/components/CredentialsInputs/CredentialsInputs";
import type { CredentialsMetaInput } from "@/lib/autogpt-server-api/types";
import {
areAllCredentialsSet,
computeInitialAgentInputs,
getAgentCredentialsInputFields,
isRunDisabled,
getSchemaDefaultCredentials,
sanitizeCredentials,
} from "./helpers";
export default function Page() {
const { state, updateState, setStep } = useOnboarding(
@@ -30,13 +41,16 @@ export default function Page() {
const [agent, setAgent] = useState<GraphMeta | null>(null);
const [storeAgent, setStoreAgent] = useState<StoreAgentDetails | null>(null);
const [runningAgent, setRunningAgent] = useState(false);
const [inputCredentials, setInputCredentials] = useState<
Record<string, CredentialsMetaInput | undefined>
>({});
const { toast } = useToast();
const router = useRouter();
const api = useBackendAPI();
useEffect(() => {
setStep(5);
}, [setStep]);
}, []);
useEffect(() => {
if (!state?.selectedStoreListingVersionId) {
@@ -49,40 +63,36 @@ export default function Page() {
});
api
.getGraphMetaByStoreListingVersionID(state.selectedStoreListingVersionId)
.then((agent) => {
setAgent(agent);
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const update: { [key: string]: any } = {};
// Set default values from schema
Object.entries(agent.input_schema.properties).forEach(
([key, value]) => {
// Skip if already set
if (state.agentInput && state.agentInput[key]) {
update[key] = state.agentInput[key];
return;
}
update[key] = value.type !== "null" ? value.default || "" : "";
},
.then((meta) => {
setAgent(meta);
const update = computeInitialAgentInputs(
meta,
(state.agentInput as unknown as InputValues) || null,
);
updateState({
agentInput: update,
});
updateState({ agentInput: update });
});
}, [api, setAgent, updateState, state?.selectedStoreListingVersionId]);
const setAgentInput = useCallback(
(key: string, value: string) => {
updateState({
agentInput: {
...state?.agentInput,
[key]: value,
},
});
},
[state?.agentInput, updateState],
const agentCredentialsInputFields = getAgentCredentialsInputFields(agent);
const credentialsRequired =
Object.keys(agentCredentialsInputFields || {}).length > 0;
const allCredentialsAreSet = areAllCredentialsSet(
agentCredentialsInputFields,
inputCredentials,
);
const runAgent = useCallback(async () => {
function setAgentInput(key: string, value: string) {
updateState({
agentInput: {
...state?.agentInput,
[key]: value,
},
});
}
async function runAgent() {
if (!agent) {
return;
}
@@ -95,6 +105,7 @@ export default function Page() {
libraryAgent.graph_id,
libraryAgent.graph_version,
state?.agentInput || {},
sanitizeCredentials(inputCredentials),
);
updateState({
onboardingAgentExecutionId: runID,
@@ -111,7 +122,7 @@ export default function Page() {
});
setRunningAgent(false);
}
}, [api, agent, router, state?.agentInput, storeAgent, updateState, toast]);
}
const runYourAgent = (
<div className="ml-[104px] w-[481px] pl-5">
@@ -221,6 +232,30 @@ export default function Page() {
<span className="mt-4 text-base font-normal leading-normal text-zinc-600">
When you&apos;re done, click <b>Run Agent</b>.
</span>
{Object.entries(agentCredentialsInputFields || {}).map(
([key, inputSubSchema]) => (
<div key={key} className="mt-4">
<CredentialsInput
schema={inputSubSchema}
selectedCredentials={
inputCredentials[key] ??
getSchemaDefaultCredentials(inputSubSchema)
}
onSelectCredentials={(value) =>
setInputCredentials((prev) => ({
...prev,
[key]: value,
}))
}
siblingInputs={
(state?.agentInput || undefined) as
| Record<string, any>
| undefined
}
/>
</div>
),
)}
<Card className="agpt-box mt-4">
<CardHeader>
<CardTitle className="font-poppins text-lg">Input</CardTitle>
@@ -250,13 +285,14 @@ export default function Page() {
variant="violet"
className="mt-8 w-[136px]"
loading={runningAgent}
disabled={
Object.values(state?.agentInput || {}).some(
(value) => String(value).trim() === "",
) ||
!agent ||
runningAgent
}
disabled={isRunDisabled({
agent,
isRunning: runningAgent,
agentInputs:
(state?.agentInput as unknown as InputValues) || null,
credentialsRequired,
credentialsSatisfied: allCredentialsAreSet,
})}
onClick={runAgent}
icon={<Play className="mr-2" size={18} />}
>

View File

@@ -0,0 +1,2 @@
export type InputPrimitive = string | number;
export type InputValues = Record<string, InputPrimitive>;