mirror of
https://github.com/Significant-Gravitas/AutoGPT.git
synced 2026-04-08 03:00:28 -04:00
Merge remote-tracking branch 'origin/fix/copilot-credential-setup-ui' into combined-preview-test
This commit is contained in:
@@ -48,27 +48,41 @@ logger = logging.getLogger(__name__)
|
||||
def get_inputs_from_schema(
|
||||
input_schema: dict[str, Any],
|
||||
exclude_fields: set[str] | None = None,
|
||||
input_data: dict[str, Any] | None = None,
|
||||
) -> list[dict[str, Any]]:
|
||||
"""Extract input field info from JSON schema."""
|
||||
"""Extract input field info from JSON schema.
|
||||
|
||||
When *input_data* is provided, each field's ``value`` key is populated
|
||||
with the value the CoPilot already supplied — so the frontend can
|
||||
prefill the form instead of showing empty inputs. Fields marked
|
||||
``advanced`` in the schema are flagged so the frontend can hide them
|
||||
by default (matching the builder behaviour).
|
||||
"""
|
||||
if not isinstance(input_schema, dict):
|
||||
return []
|
||||
|
||||
exclude = exclude_fields or set()
|
||||
properties = input_schema.get("properties", {})
|
||||
required = set(input_schema.get("required", []))
|
||||
provided = input_data or {}
|
||||
|
||||
return [
|
||||
{
|
||||
results: list[dict[str, Any]] = []
|
||||
for name, schema in properties.items():
|
||||
if name in exclude:
|
||||
continue
|
||||
entry: dict[str, Any] = {
|
||||
"name": name,
|
||||
"title": schema.get("title", name),
|
||||
"type": schema.get("type", "string"),
|
||||
"description": schema.get("description", ""),
|
||||
"required": name in required,
|
||||
"default": schema.get("default"),
|
||||
"advanced": schema.get("advanced", False),
|
||||
}
|
||||
for name, schema in properties.items()
|
||||
if name not in exclude
|
||||
]
|
||||
if name in provided:
|
||||
entry["value"] = provided[name]
|
||||
results.append(entry)
|
||||
return results
|
||||
|
||||
|
||||
async def execute_block(
|
||||
@@ -446,7 +460,9 @@ async def prepare_block_for_execution(
|
||||
requirements={
|
||||
"credentials": missing_creds_list,
|
||||
"inputs": get_inputs_from_schema(
|
||||
input_schema, exclude_fields=credentials_fields
|
||||
input_schema,
|
||||
exclude_fields=credentials_fields,
|
||||
input_data=input_data,
|
||||
),
|
||||
"execution_modes": ["immediate"],
|
||||
},
|
||||
|
||||
@@ -30,7 +30,7 @@ class VirusScanResult(BaseModel):
|
||||
|
||||
class VirusScannerSettings(BaseSettings):
|
||||
# Tunables for the scanner layer (NOT the ClamAV daemon).
|
||||
clamav_service_host: str = "localhost"
|
||||
clamav_service_host: str = "clamav"
|
||||
clamav_service_port: int = 3310
|
||||
clamav_service_timeout: int = 60
|
||||
clamav_service_enabled: bool = True
|
||||
|
||||
@@ -98,6 +98,7 @@ services:
|
||||
- CLAMD_CONF_MaxScanSize=100M
|
||||
- CLAMD_CONF_MaxThreads=12
|
||||
- CLAMD_CONF_ReadTimeout=300
|
||||
- CLAMD_CONF_TCPAddr=0.0.0.0
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "clamdscan --version || exit 1"]
|
||||
interval: 30s
|
||||
|
||||
@@ -6,25 +6,21 @@ import { Text } from "@/components/atoms/Text/Text";
|
||||
import { CredentialsGroupedView } from "@/components/contextual/CredentialsInput/components/CredentialsGroupedView/CredentialsGroupedView";
|
||||
import { FormRenderer } from "@/components/renderers/InputRenderer/FormRenderer";
|
||||
import type { CredentialsMetaInput } from "@/lib/autogpt-server-api/types";
|
||||
import { useState } from "react";
|
||||
import { useMemo, useState } from "react";
|
||||
import { useCopilotChatActions } from "../../../../components/CopilotChatActionsProvider/useCopilotChatActions";
|
||||
import { ContentMessage } from "../../../../components/ToolAccordion/AccordionContent";
|
||||
import {
|
||||
buildExpectedInputsSchema,
|
||||
buildSiblingInputsFromCredentials,
|
||||
coerceCredentialFields,
|
||||
coerceExpectedInputs,
|
||||
extractInitialValues,
|
||||
} from "./helpers";
|
||||
|
||||
interface Props {
|
||||
output: SetupRequirementsResponse;
|
||||
/** Override the message sent to the chat when the user clicks Proceed after connecting credentials.
|
||||
* Defaults to "Please re-run this step now." */
|
||||
retryInstruction?: string;
|
||||
/** Override the label shown above the credentials section.
|
||||
* Defaults to "Credentials". */
|
||||
credentialsLabel?: string;
|
||||
/** Called after Proceed is clicked so the parent can persist the dismissed state
|
||||
* across remounts (avoids re-enabling the Proceed button on remount). */
|
||||
onComplete?: () => void;
|
||||
}
|
||||
|
||||
@@ -39,8 +35,8 @@ export function SetupRequirementsCard({
|
||||
const [inputCredentials, setInputCredentials] = useState<
|
||||
Record<string, CredentialsMetaInput | undefined>
|
||||
>({});
|
||||
const [inputValues, setInputValues] = useState<Record<string, unknown>>({});
|
||||
const [hasSent, setHasSent] = useState(false);
|
||||
const [showAdvanced, setShowAdvanced] = useState(false);
|
||||
|
||||
const { credentialFields, requiredCredentials } = coerceCredentialFields(
|
||||
output.setup_info.user_readiness?.missing_credentials,
|
||||
@@ -50,7 +46,27 @@ export function SetupRequirementsCard({
|
||||
(output.setup_info.requirements as Record<string, unknown>)?.inputs,
|
||||
);
|
||||
|
||||
const inputSchema = buildExpectedInputsSchema(expectedInputs);
|
||||
const initialValues = useMemo(
|
||||
() => extractInitialValues(expectedInputs),
|
||||
[expectedInputs],
|
||||
);
|
||||
|
||||
const [inputValues, setInputValues] =
|
||||
useState<Record<string, unknown>>(initialValues);
|
||||
|
||||
const hasAdvancedFields = expectedInputs.some((i) => i.advanced);
|
||||
const inputSchema = buildExpectedInputsSchema(expectedInputs, showAdvanced);
|
||||
|
||||
// Build siblingInputs for credential modal host prefill.
|
||||
// Prefer discriminator_values from the credential response, but also
|
||||
// include values from input_data (e.g. url field) so the host pattern
|
||||
// can be extracted even when discriminator_values is empty.
|
||||
const siblingInputs = useMemo(() => {
|
||||
const fromCreds = buildSiblingInputsFromCredentials(
|
||||
output.setup_info.user_readiness?.missing_credentials,
|
||||
);
|
||||
return { ...inputValues, ...fromCreds };
|
||||
}, [output.setup_info.user_readiness?.missing_credentials, inputValues]);
|
||||
|
||||
function handleCredentialChange(key: string, value?: CredentialsMetaInput) {
|
||||
setInputCredentials((prev) => ({ ...prev, [key]: value }));
|
||||
@@ -63,10 +79,10 @@ export function SetupRequirementsCard({
|
||||
|
||||
const needsInputs = inputSchema !== null;
|
||||
const requiredInputNames = expectedInputs
|
||||
.filter((i) => i.required)
|
||||
.filter((i) => i.required && !i.advanced)
|
||||
.map((i) => i.name);
|
||||
const isAllInputsComplete =
|
||||
needsInputs &&
|
||||
!needsInputs ||
|
||||
requiredInputNames.every((name) => {
|
||||
const v = inputValues[name];
|
||||
return v !== undefined && v !== null && v !== "";
|
||||
@@ -77,8 +93,7 @@ export function SetupRequirementsCard({
|
||||
}
|
||||
|
||||
const canRun =
|
||||
(!needsCredentials || isAllCredentialsComplete) &&
|
||||
(!needsInputs || isAllInputsComplete);
|
||||
(!needsCredentials || isAllCredentialsComplete) && isAllInputsComplete;
|
||||
|
||||
function handleRun() {
|
||||
setHasSent(true);
|
||||
@@ -118,7 +133,7 @@ export function SetupRequirementsCard({
|
||||
credentialFields={credentialFields}
|
||||
requiredCredentials={requiredCredentials}
|
||||
inputCredentials={inputCredentials}
|
||||
inputValues={{}}
|
||||
inputValues={siblingInputs}
|
||||
onCredentialChange={handleCredentialChange}
|
||||
/>
|
||||
</div>
|
||||
@@ -133,7 +148,9 @@ export function SetupRequirementsCard({
|
||||
<FormRenderer
|
||||
jsonSchema={inputSchema}
|
||||
className="mb-3 mt-3"
|
||||
handleChange={(v) => setInputValues(v.formData ?? {})}
|
||||
handleChange={(v) =>
|
||||
setInputValues((prev) => ({ ...prev, ...(v.formData ?? {}) }))
|
||||
}
|
||||
uiSchema={{
|
||||
"ui:submitButtonOptions": { norender: true },
|
||||
}}
|
||||
@@ -143,6 +160,15 @@ export function SetupRequirementsCard({
|
||||
size: "small",
|
||||
}}
|
||||
/>
|
||||
{hasAdvancedFields && (
|
||||
<button
|
||||
type="button"
|
||||
className="text-xs text-muted-foreground underline"
|
||||
onClick={() => setShowAdvanced((v) => !v)}
|
||||
>
|
||||
{showAdvanced ? "Hide advanced fields" : "Show advanced fields"}
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
|
||||
@@ -71,21 +71,58 @@ export function coerceCredentialFields(rawMissingCredentials: unknown): {
|
||||
return { credentialFields, requiredCredentials };
|
||||
}
|
||||
|
||||
export function coerceExpectedInputs(rawInputs: unknown): Array<{
|
||||
/**
|
||||
* Build a sibling-inputs dict from the missing_credentials discriminator values.
|
||||
*
|
||||
* When the backend resolves credentials for host-scoped blocks (e.g.
|
||||
* SendAuthenticatedWebRequestBlock), it adds the target URL to
|
||||
* `discriminator_values`. The credential modal uses `siblingInputs`
|
||||
* to extract the host and prefill the "Host Pattern" field.
|
||||
*
|
||||
* This function builds that mapping from the `discriminator` field name
|
||||
* and the first `discriminator_values` entry for each credential.
|
||||
*/
|
||||
export function buildSiblingInputsFromCredentials(
|
||||
rawMissingCredentials: unknown,
|
||||
): Record<string, unknown> {
|
||||
const result: Record<string, unknown> = {};
|
||||
if (!rawMissingCredentials || typeof rawMissingCredentials !== "object")
|
||||
return result;
|
||||
|
||||
const missing = rawMissingCredentials as Record<string, unknown>;
|
||||
for (const value of Object.values(missing)) {
|
||||
if (!value || typeof value !== "object") continue;
|
||||
const cred = value as Record<string, unknown>;
|
||||
|
||||
const discriminator =
|
||||
typeof cred.discriminator === "string" ? cred.discriminator : null;
|
||||
const discriminatorValues = Array.isArray(cred.discriminator_values)
|
||||
? cred.discriminator_values.filter(
|
||||
(v): v is string => typeof v === "string",
|
||||
)
|
||||
: [];
|
||||
|
||||
if (discriminator && discriminatorValues.length > 0) {
|
||||
result[discriminator] = discriminatorValues[0];
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
interface ExpectedInput {
|
||||
name: string;
|
||||
title: string;
|
||||
type: string;
|
||||
description?: string;
|
||||
required: boolean;
|
||||
}> {
|
||||
advanced: boolean;
|
||||
value?: unknown;
|
||||
}
|
||||
|
||||
export function coerceExpectedInputs(rawInputs: unknown): ExpectedInput[] {
|
||||
if (!Array.isArray(rawInputs)) return [];
|
||||
const results: Array<{
|
||||
name: string;
|
||||
title: string;
|
||||
type: string;
|
||||
description?: string;
|
||||
required: boolean;
|
||||
}> = [];
|
||||
const results: ExpectedInput[] = [];
|
||||
|
||||
rawInputs.forEach((value, index) => {
|
||||
if (!value || typeof value !== "object") return;
|
||||
@@ -105,15 +142,13 @@ export function coerceExpectedInputs(rawInputs: unknown): Array<{
|
||||
? input.description.trim()
|
||||
: undefined;
|
||||
const required = Boolean(input.required);
|
||||
const advanced = Boolean(input.advanced);
|
||||
|
||||
const item: {
|
||||
name: string;
|
||||
title: string;
|
||||
type: string;
|
||||
description?: string;
|
||||
required: boolean;
|
||||
} = { name, title, type, required };
|
||||
const item: ExpectedInput = { name, title, type, required, advanced };
|
||||
if (description) item.description = description;
|
||||
if (input.value !== undefined && input.value !== null) {
|
||||
item.value = input.value;
|
||||
}
|
||||
results.push(item);
|
||||
});
|
||||
|
||||
@@ -123,17 +158,20 @@ export function coerceExpectedInputs(rawInputs: unknown): Array<{
|
||||
/**
|
||||
* Build an RJSF schema from expected inputs so they can be rendered
|
||||
* as a dynamic form via FormRenderer.
|
||||
*
|
||||
* When ``showAdvanced`` is false (default), fields marked ``advanced``
|
||||
* are excluded — matching the builder behaviour where advanced fields
|
||||
* are hidden behind a toggle.
|
||||
*/
|
||||
export function buildExpectedInputsSchema(
|
||||
expectedInputs: Array<{
|
||||
name: string;
|
||||
title: string;
|
||||
type: string;
|
||||
description?: string;
|
||||
required: boolean;
|
||||
}>,
|
||||
expectedInputs: ExpectedInput[],
|
||||
showAdvanced = false,
|
||||
): RJSFSchema | null {
|
||||
if (expectedInputs.length === 0) return null;
|
||||
const visible = showAdvanced
|
||||
? expectedInputs
|
||||
: expectedInputs.filter((i) => !i.advanced);
|
||||
|
||||
if (visible.length === 0) return null;
|
||||
|
||||
const TYPE_MAP: Record<string, string> = {
|
||||
string: "string",
|
||||
@@ -150,12 +188,14 @@ export function buildExpectedInputsSchema(
|
||||
const properties: Record<string, Record<string, unknown>> = {};
|
||||
const required: string[] = [];
|
||||
|
||||
for (const input of expectedInputs) {
|
||||
properties[input.name] = {
|
||||
for (const input of visible) {
|
||||
const prop: Record<string, unknown> = {
|
||||
type: TYPE_MAP[input.type.toLowerCase()] ?? "string",
|
||||
title: input.title,
|
||||
...(input.description ? { description: input.description } : {}),
|
||||
};
|
||||
if (input.description) prop.description = input.description;
|
||||
if (input.value !== undefined) prop.default = input.value;
|
||||
properties[input.name] = prop;
|
||||
if (input.required) required.push(input.name);
|
||||
}
|
||||
|
||||
@@ -165,3 +205,19 @@ export function buildExpectedInputsSchema(
|
||||
...(required.length > 0 ? { required } : {}),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract initial form values from expected inputs that have a
|
||||
* prefilled ``value`` from the backend.
|
||||
*/
|
||||
export function extractInitialValues(
|
||||
expectedInputs: ExpectedInput[],
|
||||
): Record<string, unknown> {
|
||||
const values: Record<string, unknown> = {};
|
||||
for (const input of expectedInputs) {
|
||||
if (input.value !== undefined && input.value !== null) {
|
||||
values[input.name] = input.value;
|
||||
}
|
||||
}
|
||||
return values;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user