Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion studio/api/app/api/agents.py
Original file line number Diff line number Diff line change
Expand Up @@ -305,7 +305,9 @@ async def import_agent(
"code": "import_validation_failed",
"message": "Imported config has unresolved blocking issues.",
"issues": [issue.model_dump() for issue in blocking_issues],
"schema_issues": [issue.model_dump() for issue in final_result.schema_issues],
"schema_issues": [
issue.model_dump() for issue in final_result.schema_issues
],
"fulfillment_issues": [
issue.model_dump() for issue in final_result.fulfillment_issues
],
Expand Down
11 changes: 8 additions & 3 deletions studio/api/app/services/agent_import.py
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,9 @@ async def validate_import_config(
)

return ImportValidationResponse(
schema_valid=not any(i.blocking and i.severity == "error" for i in schema_issues),
schema_valid=not any(
i.blocking and i.severity == "error" for i in schema_issues
),
schema_issues=schema_issues,
fulfillable=not any(
i.blocking and i.severity == "error" for i in fulfillment_issues
Expand Down Expand Up @@ -147,7 +149,9 @@ def _base_suggested_mappings(
language = str(config.get("language", "en")).split("-")[0].lower()
default_provider = defaults.provider or ""
suggested_model = _pick_model(default_provider, language, defaults.model)
suggested_voice = defaults.voice_id or _pick_voice(default_provider, suggested_model, language)
suggested_voice = defaults.voice_id or _pick_voice(
default_provider, suggested_model, language
)

out: dict[str, str] = {}
if default_provider:
Expand All @@ -156,7 +160,8 @@ def _base_suggested_mappings(
out["tts_model"] = suggested_model
if suggested_voice and (
not suggested_model
or suggested_voice in _valid_voice_ids(default_provider, suggested_model, language)
or suggested_voice
in _valid_voice_ids(default_provider, suggested_model, language)
):
out["voice_id"] = suggested_voice
return out
Expand Down
84 changes: 41 additions & 43 deletions studio/web/src/app/dashboard/agents/[id]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -955,52 +955,50 @@ function AgentDetailPageContent({ params }: { params: Promise<{ id: string }> })
</div>
))}
</div>
) : (
importedConnections.length > 0 ? (
<div className="space-y-2">
{importedConnections.map((conn, idx) => (
<div
key={`${conn.provider}-${idx}`}
className="flex items-center justify-between p-3 rounded-xl border border-border bg-background"
>
<div className="flex items-center gap-2.5">
<div className="relative size-8 rounded-lg bg-secondary flex items-center justify-center shrink-0">
<IntegrationIcon
name={conn.provider}
iconHint="shield"
size="size-4"
brandSize="size-7"
/>
</div>
<div>
<span className="text-xs font-medium">
{conn.name || conn.provider}
</span>
<div className="flex items-center gap-1.5 mt-0.5">
<Badge variant="secondary" className="text-[10px] h-4 px-1.5">
{conn.provider}
</Badge>
<span className="text-[10px] text-muted-foreground">
{conn.status}
</span>
</div>
) : importedConnections.length > 0 ? (
<div className="space-y-2">
{importedConnections.map((conn, idx) => (
<div
key={`${conn.provider}-${idx}`}
className="flex items-center justify-between p-3 rounded-xl border border-border bg-background"
>
<div className="flex items-center gap-2.5">
<div className="relative size-8 rounded-lg bg-secondary flex items-center justify-center shrink-0">
<IntegrationIcon
name={conn.provider}
iconHint="shield"
size="size-4"
brandSize="size-7"
/>
</div>
<div>
<span className="text-xs font-medium">{conn.name || conn.provider}</span>
<div className="flex items-center gap-1.5 mt-0.5">
<Badge variant="secondary" className="text-[10px] h-4 px-1.5">
{conn.provider}
</Badge>
<span className="text-[10px] text-muted-foreground">{conn.status}</span>
</div>
</div>
<Button
variant="outline"
size="sm"
className="h-6 text-[10px] px-2 rounded-md gap-1 text-foreground"
onClick={() => openCredentialForProvider(conn.provider)}
>
<HugeiconsIcon icon={Settings03Icon} className="size-2.5" />
Connect
</Button>
</div>
))}
</div>
) : (
<EmptyState icon={<HugeiconsIcon icon={Key02Icon} className="size-5 opacity-30" />} title="No credentials" desc="Connect integrations from the chat or configure defaults in Integrations." />
)
<Button
variant="outline"
size="sm"
className="h-6 text-[10px] px-2 rounded-md gap-1 text-foreground"
onClick={() => openCredentialForProvider(conn.provider)}
>
<HugeiconsIcon icon={Settings03Icon} className="size-2.5" />
Connect
</Button>
</div>
))}
</div>
) : (
<EmptyState
icon={<HugeiconsIcon icon={Key02Icon} className="size-5 opacity-30" />}
title="No credentials"
desc="Connect integrations from the chat or configure defaults in Integrations."
/>
)}
</div>
)}
Expand Down
118 changes: 72 additions & 46 deletions studio/web/src/app/dashboard/agents/import/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -69,11 +69,7 @@ export default function ImportAgentPage() {
const [submitting, setSubmitting] = useState(false);

const handleParsedChange = useCallback(
(
nextParsed: unknown,
nextError: string | null,
nextStatus: ImportConfigParseStatus
) => {
(nextParsed: unknown, nextError: string | null, nextStatus: ImportConfigParseStatus) => {
if (!nextParsed || nextError) {
setFullConfig(null);
setParsedConfig(null);
Expand Down Expand Up @@ -103,10 +99,7 @@ export default function ImportAgentPage() {
() => allIssues.filter((issue) => issue.blocking && issue.severity === "error"),
[allIssues]
);
const fulfillmentIssues = useMemo(
() => validation?.fulfillment_issues ?? [],
[validation]
);
const fulfillmentIssues = useMemo(() => validation?.fulfillment_issues ?? [], [validation]);
const hasFulfillmentIssues = fulfillmentIssues.length > 0;

const mappingRows = useMemo(
Expand All @@ -118,10 +111,7 @@ export default function ImportAgentPage() {
fromValue: String(
validation?.normalized_config?.[issue.path as keyof AgentGraphConfig] ?? ""
),
mappedTo:
mappings[issue.path as string] ||
issue.suggested_value ||
"",
mappedTo: mappings[issue.path as string] || issue.suggested_value || "",
})),
[fulfillmentIssues, mappings, validation]
);
Expand All @@ -137,8 +127,7 @@ export default function ImportAgentPage() {
[blockingIssues, mappings]
);

const canProceedFromStep2 =
blockingIssues.length === 0 || canResolveBlockingViaMappings;
const canProceedFromStep2 = blockingIssues.length === 0 || canResolveBlockingViaMappings;

const runValidation = async () => {
if (!parsedConfig) return;
Expand Down Expand Up @@ -191,9 +180,7 @@ export default function ImportAgentPage() {
full_config: {
$schema: fullConfig?.$schema ?? FULL_CONFIG_SCHEMA_URL,
name: fullConfig?.name ?? name.trim(),
description:
fullConfig?.description ??
(description.trim() ? description.trim() : null),
description: fullConfig?.description ?? (description.trim() ? description.trim() : null),
config: validation.normalized_config,
mermaid_diagram: fullConfig?.mermaid_diagram ?? null,
connections: fullConfig?.connections ?? [],
Expand All @@ -208,14 +195,8 @@ export default function ImportAgentPage() {
if (error instanceof ApiError && error.detail && typeof error.detail === "object") {
const detail = error.detail as { issues?: unknown };
if (Array.isArray(detail.issues)) {
const parsedIssues = detail.issues.filter(
(item): item is ImportIssue =>
Boolean(
item &&
typeof item === "object" &&
"message" in item &&
"code" in item
)
const parsedIssues = detail.issues.filter((item): item is ImportIssue =>
Boolean(item && typeof item === "object" && "message" in item && "code" in item)
);
setSubmitIssueDetails(parsedIssues);
}
Expand All @@ -226,9 +207,7 @@ export default function ImportAgentPage() {

const renderSchemaIssues = (issues: ImportIssue[]) => (
<div className="space-y-3">
<p className="text-xs font-medium uppercase tracking-wide text-muted-foreground">
Schema
</p>
<p className="text-xs font-medium uppercase tracking-wide text-muted-foreground">Schema</p>
{issues.length === 0 ? (
<div className="rounded-lg border border-success/20 bg-success/5 px-3 py-2 text-sm text-success">
<span className="inline-flex items-center gap-1.5">
Expand All @@ -239,7 +218,10 @@ export default function ImportAgentPage() {
) : (
<div className="space-y-2">
{issues.map((issue, index) => (
<div key={`${issue.code}-${issue.path || index}`} className="rounded-lg border bg-muted/30 px-3 py-2">
<div
key={`${issue.code}-${issue.path || index}`}
className="rounded-lg border bg-muted/30 px-3 py-2"
>
<p className="text-sm font-medium">{issue.message}</p>
<p className="mt-1 text-xs text-muted-foreground">
{issue.code}
Expand Down Expand Up @@ -269,7 +251,10 @@ export default function ImportAgentPage() {
<>
<div className="space-y-2">
{issues.map((issue, index) => (
<div key={`${issue.code}-${issue.path || index}`} className="rounded-lg border bg-muted/30 px-3 py-2">
<div
key={`${issue.code}-${issue.path || index}`}
className="rounded-lg border bg-muted/30 px-3 py-2"
>
<p className="text-sm font-medium">{issue.message}</p>
<p className="mt-1 text-xs text-muted-foreground">
{issue.code}
Expand All @@ -290,11 +275,18 @@ export default function ImportAgentPage() {
<li key={`${row.property}-${index}`} className="text-muted-foreground">
<span className="mr-3 font-bold text-foreground">{row.property}</span>
<span className="inline-flex items-center gap-1.5">
<span className={`font-mono ${row.fromValue ? "text-foreground" : "text-destructive"}`}>
<span
className={`font-mono ${row.fromValue ? "text-foreground" : "text-destructive"}`}
>
{row.fromValue || "unknown"}
</span>
<HugeiconsIcon icon={ArrowRight02Icon} className="size-3.5 text-muted-foreground" />
<span className={`font-mono ${row.mappedTo ? "text-foreground" : "text-destructive"}`}>
<HugeiconsIcon
icon={ArrowRight02Icon}
className="size-3.5 text-muted-foreground"
/>
<span
className={`font-mono ${row.mappedTo ? "text-foreground" : "text-destructive"}`}
>
{row.mappedTo || "no default"}
</span>
</span>
Expand Down Expand Up @@ -326,7 +318,10 @@ export default function ImportAgentPage() {
<p className="text-muted-foreground mt-1">
Validate imported config, resolve issues, and finalize metadata.
</p>
<HugeiconsIcon icon={AudioWave01Icon} className="size-52 absolute text-primary/10 -right-8 -bottom-16" />
<HugeiconsIcon
icon={AudioWave01Icon}
className="size-52 absolute text-primary/10 -right-8 -bottom-16"
/>
</div>

<CardContent className="space-y-6 p-8 pt-6">
Expand Down Expand Up @@ -361,10 +356,10 @@ export default function ImportAgentPage() {
parseError
? "text-destructive"
: parseStatus === "invalid"
? "text-destructive"
: parseStatus === "valid"
? "text-success"
: "text-muted-foreground"
? "text-destructive"
: parseStatus === "valid"
? "text-success"
: "text-muted-foreground"
}`}
>
{parseError ? parseError : null}
Expand All @@ -389,7 +384,11 @@ export default function ImportAgentPage() {
disabled={!parsedConfig || Boolean(parseError) || validating}
className="h-8"
>
{validating ? <Spinner className="size-4" /> : <HugeiconsIcon icon={FileValidationIcon} />}
{validating ? (
<Spinner className="size-4" />
) : (
<HugeiconsIcon icon={FileValidationIcon} />
)}
Validate config
</Button>
</div>
Expand All @@ -403,10 +402,19 @@ export default function ImportAgentPage() {
{renderFulfillmentSection(validation.fulfillment_issues)}

<div className="flex items-center justify-between">
<Button type="button" variant="outline" onClick={() => setStep(1)} className="gap-1.5">
<Button
type="button"
variant="outline"
onClick={() => setStep(1)}
className="gap-1.5"
>
<HugeiconsIcon icon={ArrowLeft01Icon} className="size-4" /> Back
</Button>
<Button onClick={() => setStep(3)} disabled={!canProceedFromStep2} className="gap-1.5">
<Button
onClick={() => setStep(3)}
disabled={!canProceedFromStep2}
className="gap-1.5"
>
Continue <HugeiconsIcon icon={ArrowRight01Icon} className="size-4" />
</Button>
</div>
Expand Down Expand Up @@ -440,11 +448,24 @@ export default function ImportAgentPage() {
</div>

<div className="flex items-center justify-between">
<Button type="button" variant="outline" onClick={() => setStep(2)} className="gap-1.5">
<Button
type="button"
variant="outline"
onClick={() => setStep(2)}
className="gap-1.5"
>
<HugeiconsIcon icon={ArrowLeft01Icon} className="size-4" /> Back
</Button>
<Button onClick={handleSubmit} disabled={!name.trim() || submitting} className="gap-2">
{submitting ? <Spinner className="size-4" /> : <HugeiconsIcon icon={AiScanIcon} className="size-4" />}
<Button
onClick={handleSubmit}
disabled={!name.trim() || submitting}
className="gap-2"
>
{submitting ? (
<Spinner className="size-4" />
) : (
<HugeiconsIcon icon={AiScanIcon} className="size-4" />
)}
Import agent
</Button>
</div>
Expand All @@ -459,7 +480,12 @@ export default function ImportAgentPage() {
{submitIssueDetails.map((issue, index) => (
<li key={`${issue.code}-${issue.path || index}`}>
<span className="font-mono">{issue.code}</span>
{issue.path ? <span> • <span className="font-mono">{issue.path}</span></span> : null}
{issue.path ? (
<span>
{" "}
• <span className="font-mono">{issue.path}</span>
</span>
) : null}
<span> • {issue.message}</span>
{issue.suggested_value ? (
<span>
Expand Down
8 changes: 7 additions & 1 deletion studio/web/src/app/dashboard/agents/page.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,13 @@
"use client";

import { HugeiconsIcon } from "@hugeicons/react";
import { Add01Icon, AiScanIcon, HardDriveUploadIcon, Robot01Icon, Search01Icon } from "@hugeicons/core-free-icons";
import {
Add01Icon,
AiScanIcon,
HardDriveUploadIcon,
Robot01Icon,
Search01Icon,
} from "@hugeicons/core-free-icons";
import { useEffect, useState } from "react";
import { api, type Agent } from "@/lib/api/client";
import { Button } from "@/components/ui/button";
Expand Down
Loading
Loading