Skip to content

Commit e907ffd

Browse files
kczentindlePwuts
authored
feat(platform): Simplify Credentials UX (#8524)
- Change `provider` of default credentials to actual provider names (e.g. `anthropic`), remove `llm` provider - Add `discriminator` and `discriminator_mapping` to `CredentialsField` that allows to filter credentials input to only allow providers for matching models in `useCredentials` hook (thanks @ntindle for the idea!); e.g. user chooses `GPT4_TURBO` so then only OpenAI credentials are allowed - Choose credentials automatically and hide credentials input on the node completely if there's only one possible option - Move `getValue` and `parseKeys` to utils - Add `ANTHROPIC`, `GROQ` and `OLLAMA` to providers in frontend `types.ts` - Add `hidden` field to credentials that is used for default system keys to hide them in user profile - Now `provider` field in `CredentialsField` can accept multiple providers as a list ----------------- Co-authored-by: Nicholas Tindle <[email protected]> Co-authored-by: Reinier van der Leer <[email protected]>
1 parent ef7e504 commit e907ffd

File tree

11 files changed

+168
-82
lines changed

11 files changed

+168
-82
lines changed

autogpt_platform/autogpt_libs/autogpt_libs/supabase_integration_credentials_store/store.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -46,21 +46,21 @@
4646
)
4747
openai_credentials = APIKeyCredentials(
4848
id="53c25cb8-e3ee-465c-a4d1-e75a4c899c2a",
49-
provider="llm",
49+
provider="openai",
5050
api_key=SecretStr(settings.secrets.openai_api_key),
5151
title="Use Credits for OpenAI",
5252
expires_at=None,
5353
)
5454
anthropic_credentials = APIKeyCredentials(
5555
id="24e5d942-d9e3-4798-8151-90143ee55629",
56-
provider="llm",
56+
provider="anthropic",
5757
api_key=SecretStr(settings.secrets.anthropic_api_key),
5858
title="Use Credits for Anthropic",
5959
expires_at=None,
6060
)
6161
groq_credentials = APIKeyCredentials(
6262
id="4ec22295-8f97-4dd1-b42b-2c6957a02545",
63-
provider="llm",
63+
provider="groq",
6464
api_key=SecretStr(settings.secrets.groq_api_key),
6565
title="Use Credits for Groq",
6666
expires_at=None,

autogpt_platform/backend/backend/blocks/llm.py

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -30,11 +30,12 @@
3030
# "ollama": BlockSecret(value=""),
3131
# }
3232

33-
AICredentials = CredentialsMetaInput[Literal["llm"], Literal["api_key"]]
33+
LLMProviderName = Literal["anthropic", "groq", "openai", "ollama"]
34+
AICredentials = CredentialsMetaInput[LLMProviderName, Literal["api_key"]]
3435

3536
TEST_CREDENTIALS = APIKeyCredentials(
3637
id="ed55ac19-356e-4243-a6cb-bc599e9b716f",
37-
provider="llm",
38+
provider="openai",
3839
api_key=SecretStr("mock-openai-api-key"),
3940
title="Mock OpenAI API key",
4041
expires_at=None,
@@ -50,8 +51,12 @@
5051
def AICredentialsField() -> AICredentials:
5152
return CredentialsField(
5253
description="API key for the LLM provider.",
53-
provider="llm",
54+
provider=["anthropic", "groq", "openai", "ollama"],
5455
supported_credential_types={"api_key"},
56+
discriminator="model",
57+
discriminator_mapping={
58+
model.value: model.metadata.provider for model in LlmModel
59+
},
5560
)
5661

5762

autogpt_platform/backend/backend/data/model.py

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -152,10 +152,12 @@ class CredentialsMetaInput(BaseModel, Generic[CP, CT]):
152152

153153

154154
def CredentialsField(
155-
provider: CP,
155+
provider: CP | list[CP],
156156
supported_credential_types: set[CT],
157157
required_scopes: set[str] = set(),
158158
*,
159+
discriminator: Optional[str] = None,
160+
discriminator_mapping: Optional[dict[str, Any]] = None,
159161
title: Optional[str] = None,
160162
description: Optional[str] = None,
161163
**kwargs,
@@ -167,9 +169,13 @@ def CredentialsField(
167169
json_extra = {
168170
k: v
169171
for k, v in {
170-
"credentials_provider": provider,
172+
"credentials_provider": (
173+
[provider] if isinstance(provider, str) else provider
174+
),
171175
"credentials_scopes": list(required_scopes) or None, # omit if empty
172176
"credentials_types": list(supported_credential_types),
177+
"discriminator": discriminator,
178+
"discriminator_mapping": discriminator_mapping,
173179
}.items()
174180
if v is not None
175181
}

autogpt_platform/frontend/src/app/profile/page.tsx

Lines changed: 23 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -4,17 +4,14 @@ import { useSupabase } from "@/components/SupabaseProvider";
44
import { Button } from "@/components/ui/button";
55
import useUser from "@/hooks/useUser";
66
import { useRouter } from "next/navigation";
7-
import { useCallback, useContext } from "react";
7+
import { useCallback, useContext, useMemo } from "react";
88
import { FaSpinner } from "react-icons/fa";
99
import { Separator } from "@/components/ui/separator";
1010
import { useToast } from "@/components/ui/use-toast";
1111
import { IconKey, IconUser } from "@/components/ui/icons";
1212
import { LogOutIcon, Trash2Icon } from "lucide-react";
1313
import { providerIcons } from "@/components/integrations/credentials-input";
14-
import {
15-
CredentialsProviderName,
16-
CredentialsProvidersContext,
17-
} from "@/components/integrations/credentials-provider";
14+
import { CredentialsProvidersContext } from "@/components/integrations/credentials-provider";
1815
import {
1916
Table,
2017
TableBody,
@@ -23,6 +20,7 @@ import {
2320
TableHeader,
2421
TableRow,
2522
} from "@/components/ui/table";
23+
import { CredentialsProviderName } from "@/lib/autogpt-server-api";
2624

2725
export default function PrivatePage() {
2826
const { user, isLoading, error } = useUser();
@@ -62,7 +60,22 @@ export default function PrivatePage() {
6260
[providers, toast],
6361
);
6462

65-
if (isLoading || !providers || !providers) {
63+
//TODO: remove when the way system credentials are handled is updated
64+
// This contains ids for built-in "Use Credits for X" credentials
65+
const hiddenCredentials = useMemo(
66+
() => [
67+
"fdb7f412-f519-48d1-9b5f-d2f73d0e01fe", // Revid
68+
"760f84fc-b270-42de-91f6-08efe1b512d0", // Ideogram
69+
"6b9fc200-4726-4973-86c9-cd526f5ce5db", // Replicate
70+
"53c25cb8-e3ee-465c-a4d1-e75a4c899c2a", // OpenAI
71+
"24e5d942-d9e3-4798-8151-90143ee55629", // Anthropic
72+
"4ec22295-8f97-4dd1-b42b-2c6957a02545", // Groq
73+
"7f7b0654-c36b-4565-8fa7-9a52575dfae2", // D-ID
74+
],
75+
[],
76+
);
77+
78+
if (isLoading || !providers) {
6679
return (
6780
<div className="flex h-[80vh] items-center justify-center">
6881
<FaSpinner className="mr-2 h-16 w-16 animate-spin" />
@@ -76,15 +89,15 @@ export default function PrivatePage() {
7689
}
7790

7891
const allCredentials = Object.values(providers).flatMap((provider) =>
79-
[...provider.savedOAuthCredentials, ...provider.savedApiKeys].map(
80-
(credentials) => ({
92+
[...provider.savedOAuthCredentials, ...provider.savedApiKeys]
93+
.filter((cred) => !hiddenCredentials.includes(cred.id))
94+
.map((credentials) => ({
8195
...credentials,
8296
provider: provider.provider,
8397
providerName: provider.providerName,
8498
ProviderIcon: providerIcons[provider.provider],
8599
TypeIcon: { oauth2: IconUser, api_key: IconKey }[credentials.type],
86-
}),
87-
),
100+
})),
88101
);
89102

90103
return (

autogpt_platform/frontend/src/components/CustomNode.tsx

Lines changed: 10 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,13 @@ import {
1818
BlockUIType,
1919
BlockCost,
2020
} from "@/lib/autogpt-server-api/types";
21-
import { beautifyString, cn, setNestedProperty } from "@/lib/utils";
21+
import {
22+
beautifyString,
23+
cn,
24+
getValue,
25+
parseKeys,
26+
setNestedProperty,
27+
} from "@/lib/utils";
2228
import { Button } from "@/components/ui/button";
2329
import { Switch } from "@/components/ui/switch";
2430
import { history } from "./history";
@@ -36,8 +42,6 @@ import * as Separator from "@radix-ui/react-separator";
3642
import * as ContextMenu from "@radix-ui/react-context-menu";
3743
import { DotsVerticalIcon, TrashIcon, CopyIcon } from "@radix-ui/react-icons";
3844

39-
type ParsedKey = { key: string; index?: number };
40-
4145
export type ConnectionData = Array<{
4246
edge_id: string;
4347
source: string;
@@ -178,7 +182,7 @@ export function CustomNode({
178182
className=""
179183
selfKey={noteKey}
180184
schema={noteSchema as BlockIOStringSubSchema}
181-
value={getValue(noteKey)}
185+
value={getValue(noteKey, data.hardcodedValues)}
182186
handleInputChange={handleInputChange}
183187
handleInputClick={handleInputClick}
184188
error={data.errors?.[noteKey] ?? ""}
@@ -228,7 +232,7 @@ export function CustomNode({
228232
nodeId={id}
229233
propKey={getInputPropKey(propKey)}
230234
propSchema={propSchema}
231-
currentValue={getValue(getInputPropKey(propKey))}
235+
currentValue={getValue(propKey, data.hardcodedValues)}
232236
connections={data.connections}
233237
handleInputChange={handleInputChange}
234238
handleInputClick={handleInputClick}
@@ -283,48 +287,6 @@ export function CustomNode({
283287
setErrors({ ...errors });
284288
};
285289

286-
// Helper function to parse keys with array indices
287-
//TODO move to utils
288-
const parseKeys = (key: string): ParsedKey[] => {
289-
const splits = key.split(/_@_|_#_|_\$_|\./);
290-
const keys: ParsedKey[] = [];
291-
let currentKey: string | null = null;
292-
293-
splits.forEach((split) => {
294-
const isInteger = /^\d+$/.test(split);
295-
if (!isInteger) {
296-
if (currentKey !== null) {
297-
keys.push({ key: currentKey });
298-
}
299-
currentKey = split;
300-
} else {
301-
if (currentKey !== null) {
302-
keys.push({ key: currentKey, index: parseInt(split, 10) });
303-
currentKey = null;
304-
} else {
305-
throw new Error("Invalid key format: array index without a key");
306-
}
307-
}
308-
});
309-
310-
if (currentKey !== null) {
311-
keys.push({ key: currentKey });
312-
}
313-
314-
return keys;
315-
};
316-
317-
const getValue = (key: string) => {
318-
const keys = parseKeys(key);
319-
return keys.reduce((acc, k) => {
320-
if (acc === undefined) return undefined;
321-
if (k.index !== undefined) {
322-
return Array.isArray(acc[k.key]) ? acc[k.key][k.index] : undefined;
323-
}
324-
return acc[k.key];
325-
}, data.hardcodedValues as any);
326-
};
327-
328290
const isHandleConnected = (key: string) => {
329291
return (
330292
data.connections &&
@@ -347,7 +309,7 @@ export function CustomNode({
347309
const handleInputClick = (key: string) => {
348310
console.debug(`Opening modal for key: ${key}`);
349311
setActiveKey(key);
350-
const value = getValue(key);
312+
const value = getValue(key, data.hardcodedValues);
351313
setInputModalValue(
352314
typeof value === "object" ? JSON.stringify(value, null, 2) : value,
353315
);

autogpt_platform/frontend/src/components/integrations/credentials-input.tsx

Lines changed: 37 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -46,16 +46,18 @@ export const providerIcons: Record<
4646
CredentialsProviderName,
4747
React.FC<{ className?: string }>
4848
> = {
49+
anthropic: fallbackIcon,
4950
github: FaGithub,
5051
google: FaGoogle,
52+
groq: fallbackIcon,
5153
notion: NotionLogoIcon,
5254
discord: FaDiscord,
5355
d_id: fallbackIcon,
5456
google_maps: FaGoogle,
5557
jina: fallbackIcon,
5658
ideogram: fallbackIcon,
57-
llm: fallbackIcon,
5859
medium: FaMedium,
60+
ollama: fallbackIcon,
5961
openai: fallbackIcon,
6062
openweathermap: fallbackIcon,
6163
pinecone: fallbackIcon,
@@ -80,7 +82,7 @@ export type OAuthPopupResultMessage = { message_type: "oauth_popup_result" } & (
8082
export const CredentialsInput: FC<{
8183
className?: string;
8284
selectedCredentials?: CredentialsMetaInput;
83-
onSelectCredentials: (newValue: CredentialsMetaInput) => void;
85+
onSelectCredentials: (newValue?: CredentialsMetaInput) => void;
8486
}> = ({ className, selectedCredentials, onSelectCredentials }) => {
8587
const api = useMemo(() => new AutoGPTServerAPI(), []);
8688
const credentials = useCredentials();
@@ -91,14 +93,10 @@ export const CredentialsInput: FC<{
9193
useState<AbortController | null>(null);
9294
const [oAuthError, setOAuthError] = useState<string | null>(null);
9395

94-
if (!credentials) {
96+
if (!credentials || credentials.isLoading) {
9597
return null;
9698
}
9799

98-
if (credentials.isLoading) {
99-
return <div>Loading...</div>;
100-
}
101-
102100
const {
103101
schema,
104102
provider,
@@ -222,10 +220,21 @@ export const CredentialsInput: FC<{
222220
</>
223221
);
224222

223+
// Deselect credentials if they do not exist (e.g. provider was changed)
224+
if (
225+
selectedCredentials &&
226+
!savedApiKeys
227+
.concat(savedOAuthCredentials)
228+
.some((c) => c.id === selectedCredentials.id)
229+
) {
230+
onSelectCredentials(undefined);
231+
}
232+
225233
// No saved credentials yet
226234
if (savedApiKeys.length === 0 && savedOAuthCredentials.length === 0) {
227235
return (
228236
<>
237+
<span className="text-m green mb-0 text-gray-900">Credentials</span>
229238
<div className={cn("flex flex-row space-x-2", className)}>
230239
{supportsOAuth2 && (
231240
<Button onClick={handleOAuthLogin}>
@@ -248,6 +257,25 @@ export const CredentialsInput: FC<{
248257
);
249258
}
250259

260+
const singleCredential =
261+
savedApiKeys.length === 1 && savedOAuthCredentials.length === 0
262+
? savedApiKeys[0]
263+
: savedOAuthCredentials.length === 1 && savedApiKeys.length === 0
264+
? savedOAuthCredentials[0]
265+
: null;
266+
267+
if (singleCredential) {
268+
if (!selectedCredentials) {
269+
onSelectCredentials({
270+
id: singleCredential.id,
271+
type: singleCredential.type,
272+
provider,
273+
title: singleCredential.title,
274+
});
275+
}
276+
return null;
277+
}
278+
251279
function handleValueChange(newValue: string) {
252280
if (newValue === "sign-in") {
253281
// Trigger OAuth2 sign in flow
@@ -263,7 +291,7 @@ export const CredentialsInput: FC<{
263291
onSelectCredentials({
264292
id: selectedCreds.id,
265293
type: selectedCreds.type,
266-
provider: schema.credentials_provider,
294+
provider: provider,
267295
// title: customTitle, // TODO: add input for title
268296
});
269297
}
@@ -272,6 +300,7 @@ export const CredentialsInput: FC<{
272300
// Saved credentials exist
273301
return (
274302
<>
303+
<span className="text-m green mb-0 text-gray-900">Credentials</span>
275304
<Select value={selectedCredentials?.id} onValueChange={handleValueChange}>
276305
<SelectTrigger>
277306
<SelectValue placeholder={schema.placeholder} />

autogpt_platform/frontend/src/components/integrations/credentials-provider.tsx

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,16 +20,18 @@ const CREDENTIALS_PROVIDER_NAMES = Object.values(
2020

2121
// --8<-- [start:CredentialsProviderNames]
2222
const providerDisplayNames: Record<CredentialsProviderName, string> = {
23+
anthropic: "Anthropic",
2324
discord: "Discord",
2425
d_id: "D-ID",
2526
github: "GitHub",
2627
google: "Google",
2728
google_maps: "Google Maps",
29+
groq: "Groq",
2830
ideogram: "Ideogram",
2931
jina: "Jina",
3032
medium: "Medium",
31-
llm: "LLM",
3233
notion: "Notion",
34+
ollama: "Ollama",
3335
openai: "OpenAI",
3436
openweathermap: "OpenWeatherMap",
3537
pinecone: "Pinecone",

0 commit comments

Comments
 (0)