mirror of
https://github.com/microsoft/agent-framework.git
synced 2026-06-16 21:04:09 +08:00
Python: DevUI: Add OpenAI Responses API proxy support + HIL for Workflows (#1737)
* DevUI: Add OpenAI Responses API proxy support with enhanced UI features This commit adds support for proxying requests to OpenAI's Responses API, allowing DevUI to route conversations to OpenAI models when configured to enable testing. Backend changes: - Add OpenAI proxy executor with conversation routing logic - Enhance event mapper to support OpenAI Responses API format - Extend server endpoints to handle OpenAI proxy mode - Update models with OpenAI-specific response types - Remove emojis from logging and CLI output for cleaner text Frontend changes: - Add settings modal with OpenAI proxy configuration UI - Enhance agent and workflow views with improved state management - Add new UI components (separator, switch) for settings - Update debug panel with better event filtering - Improve message renderers for OpenAI content types - Update types and API client for OpenAI integration * update ui, settings modal and workflow input form, add register cleanup hooks. * add workflow HIL support, user mode, other fixes * feat(devui): add human-in-the-loop (HIL) support with dynamic response schemas Implement HIL workflow support allowing workflows to pause for user input with dynamically generated JSON schemas based on response handler type hints. Key Features: - Automatic response schema extraction from @response_handler decorators - Dynamic form generation in UI based on Pydantic/dataclass response types - Checkpoint-based conversation storage for HIL requests/responses - Resume workflow execution after user provides HIL response Backend Changes: - Add extract_response_type_from_executor() to introspect response handlers - Enrich RequestInfoEvent with response_schema via _enrich_request_info_event_with_response_schema() - Map RequestInfoEvent to response.input.requested OpenAI event format - Store HIL responses in conversation history and restore checkpoints Frontend Changes: - Add HILInputModal component with SchemaFormRenderer for dynamic forms - Support Pydantic BaseModel and dataclass response types - Render enum fields as dropdowns, strings as text/textarea, numbers, booleans, arrays, objects - Display original request context alongside response form Testing: - Add tests for checkpoint storage (test_checkpoints.py) - Add schema generation tests for all input types (test_schema_generation.py) - Validate end-to-end HIL flow with spam workflow sample This enables workflows to seamlessly pause execution and request structured user input with type-safe, validated forms generated automatically from response type annotations. * improve HIL support, improve workflow execution view * ui updates * ui updates * improve HIL for workflows, add auth and view modes * update workflow * security improvements , ui fixes * fix mypy error * update loading spinner in ui --------- Co-authored-by: Mark Wallace <127216156+markwallace-microsoft@users.noreply.github.com>
This commit is contained in:
co-authored by
Mark Wallace
parent
85484c0259
commit
94eae24082
@@ -20,12 +20,18 @@ import {
|
||||
Copy,
|
||||
CheckCircle2,
|
||||
ExternalLink,
|
||||
Loader2,
|
||||
AlertCircle,
|
||||
} from "lucide-react";
|
||||
import { useDevUIStore } from "@/stores";
|
||||
import { apiClient } from "@/services/api";
|
||||
import type { AgentInfo, WorkflowInfo } from "@/types";
|
||||
|
||||
interface DeploymentModalProps {
|
||||
open: boolean;
|
||||
onClose: () => void;
|
||||
agentName?: string;
|
||||
entity?: AgentInfo | WorkflowInfo;
|
||||
}
|
||||
|
||||
type Tab = "docker" | "azure";
|
||||
@@ -34,10 +40,108 @@ export function DeploymentModal({
|
||||
open,
|
||||
onClose,
|
||||
agentName = "Agent",
|
||||
entity,
|
||||
}: DeploymentModalProps) {
|
||||
const [activeTab, setActiveTab] = useState<Tab>("docker");
|
||||
// Get the Azure deployment feature flag from store
|
||||
const azureDeploymentEnabled = useDevUIStore((state) => state.azureDeploymentEnabled);
|
||||
|
||||
// Check if deployment is truly supported (both feature flag and backend support)
|
||||
const deploymentSupported = azureDeploymentEnabled && (entity?.deployment_supported ?? false);
|
||||
|
||||
// Context-aware tab ordering: Azure first if deployable, Docker first otherwise
|
||||
const [activeTab, setActiveTab] = useState<Tab>(
|
||||
deploymentSupported ? "azure" : "docker"
|
||||
);
|
||||
const [copiedTemplate, setCopiedTemplate] = useState<string | null>(null);
|
||||
const timeoutRef = useRef<NodeJS.Timeout | null>(null);
|
||||
const logsContainerRef = useRef<HTMLDivElement | null>(null);
|
||||
|
||||
// Deployment state from Zustand
|
||||
const isDeploying = useDevUIStore((state) => state.isDeploying);
|
||||
const deploymentLogs = useDevUIStore((state) => state.deploymentLogs);
|
||||
const lastDeployment = useDevUIStore((state) => state.lastDeployment);
|
||||
const startDeployment = useDevUIStore((state) => state.startDeployment);
|
||||
const addDeploymentLog = useDevUIStore((state) => state.addDeploymentLog);
|
||||
const setDeploymentResult = useDevUIStore((state) => state.setDeploymentResult);
|
||||
const stopDeployment = useDevUIStore((state) => state.stopDeployment);
|
||||
const clearDeploymentState = useDevUIStore((state) => state.clearDeploymentState);
|
||||
|
||||
// Generate Azure-compliant default app name from entity name
|
||||
const generateDefaultAppName = (entityName: string) => {
|
||||
// Convert to lowercase, replace spaces and underscores with hyphens
|
||||
// Remove any non-alphanumeric characters except hyphens
|
||||
// Ensure it starts with a letter and is under 32 chars
|
||||
const cleaned = entityName
|
||||
.toLowerCase()
|
||||
.replace(/[_\s]+/g, '-') // Replace underscores and spaces with hyphens
|
||||
.replace(/[^a-z0-9-]/g, '') // Remove any other special characters
|
||||
.replace(/--+/g, '-') // Replace multiple hyphens with single
|
||||
.replace(/^[^a-z]+/, '') // Remove non-letter prefix
|
||||
.replace(/-$/, ''); // Remove trailing hyphen
|
||||
|
||||
// Ensure it starts with a letter, add 'app-' prefix if needed
|
||||
const withPrefix = cleaned.match(/^[a-z]/) ? cleaned : `app-${cleaned}`;
|
||||
|
||||
// Truncate to 31 chars max (32 limit)
|
||||
return withPrefix.substring(0, 31);
|
||||
};
|
||||
|
||||
// Form state for deployment with smart defaults
|
||||
const defaultAppName = entity ? generateDefaultAppName(entity.id) : "";
|
||||
const [resourceGroup, setResourceGroup] = useState("my-test-rg");
|
||||
const [appName, setAppName] = useState(defaultAppName);
|
||||
const [region, setRegion] = useState("eastus");
|
||||
const [appNameError, setAppNameError] = useState<string | null>(null);
|
||||
|
||||
// Update app name when entity changes or modal opens
|
||||
useEffect(() => {
|
||||
if (entity) {
|
||||
const newDefaultName = generateDefaultAppName(entity.id);
|
||||
setAppName(newDefaultName);
|
||||
// Validate the default name
|
||||
const error = validateAppName(newDefaultName);
|
||||
setAppNameError(error);
|
||||
}
|
||||
}, [entity?.id]); // Only re-run when entity ID changes
|
||||
|
||||
// Auto-scroll deployment logs to bottom when new logs are added
|
||||
useEffect(() => {
|
||||
if (logsContainerRef.current && deploymentLogs.length > 0) {
|
||||
logsContainerRef.current.scrollTop = logsContainerRef.current.scrollHeight;
|
||||
}
|
||||
}, [deploymentLogs]);
|
||||
|
||||
// Validate Azure Container App name
|
||||
const validateAppName = (name: string): string | null => {
|
||||
if (!name) return null; // Don't show error for empty field
|
||||
|
||||
// Check length
|
||||
if (name.length >= 32) {
|
||||
return "App name must be less than 32 characters";
|
||||
}
|
||||
|
||||
// Check for valid characters (lowercase alphanumeric and hyphens only)
|
||||
if (!/^[a-z0-9-]+$/.test(name)) {
|
||||
return "App name must contain only lowercase letters, numbers, and hyphens (no underscores or uppercase)";
|
||||
}
|
||||
|
||||
// Must start with a letter
|
||||
if (!/^[a-z]/.test(name)) {
|
||||
return "App name must start with a lowercase letter";
|
||||
}
|
||||
|
||||
// Must end with alphanumeric
|
||||
if (!/[a-z0-9]$/.test(name)) {
|
||||
return "App name must end with a letter or number";
|
||||
}
|
||||
|
||||
// Cannot have double hyphens
|
||||
if (name.includes("--")) {
|
||||
return "App name cannot contain consecutive hyphens (--)";
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
// Cleanup timeout on unmount
|
||||
useEffect(() => {
|
||||
@@ -48,6 +152,48 @@ export function DeploymentModal({
|
||||
};
|
||||
}, []);
|
||||
|
||||
const handleDeploy = async () => {
|
||||
if (!entity?.id || !resourceGroup || !appName) return;
|
||||
|
||||
// Trim whitespace from inputs
|
||||
const trimmedResourceGroup = resourceGroup.trim();
|
||||
const trimmedAppName = appName.trim();
|
||||
|
||||
// Validate trimmed app name before deployment
|
||||
const nameError = validateAppName(trimmedAppName);
|
||||
if (nameError) {
|
||||
setAppNameError(nameError);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
startDeployment();
|
||||
|
||||
for await (const event of apiClient.streamDeployment({
|
||||
entity_id: entity.id,
|
||||
resource_group: trimmedResourceGroup,
|
||||
app_name: trimmedAppName,
|
||||
region,
|
||||
ui_mode: "user",
|
||||
})) {
|
||||
addDeploymentLog(event.message);
|
||||
|
||||
if (event.type === "deploy.completed" && event.url && event.auth_token) {
|
||||
setDeploymentResult({
|
||||
url: event.url,
|
||||
authToken: event.auth_token,
|
||||
});
|
||||
} else if (event.type === "deploy.failed") {
|
||||
// Stop deploying but keep logs visible
|
||||
stopDeployment();
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
addDeploymentLog(`Error: ${error instanceof Error ? error.message : "Deployment failed"}`);
|
||||
stopDeployment();
|
||||
}
|
||||
};
|
||||
|
||||
const handleCopy = async (template: string, templateName: string) => {
|
||||
try {
|
||||
await navigator.clipboard.writeText(template);
|
||||
@@ -64,8 +210,7 @@ export function DeploymentModal({
|
||||
timeoutRef.current = null;
|
||||
}, 2000);
|
||||
} catch (err) {
|
||||
console.error("Failed to copy template:", err);
|
||||
// Reset state on error
|
||||
// Reset state on error - clipboard write failed
|
||||
setCopiedTemplate(null);
|
||||
}
|
||||
};
|
||||
@@ -149,20 +294,22 @@ openai>=1.0.0
|
||||
<div className="absolute bottom-0 left-0 right-0 h-0.5 bg-primary" />
|
||||
)}
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setActiveTab("azure")}
|
||||
className={`px-4 py-2 text-sm font-medium transition-colors relative ${
|
||||
activeTab === "azure"
|
||||
? "text-foreground"
|
||||
: "text-muted-foreground hover:text-foreground"
|
||||
}`}
|
||||
>
|
||||
<Cloud className="h-4 w-4 mr-2 inline" />
|
||||
Azure
|
||||
{activeTab === "azure" && (
|
||||
<div className="absolute bottom-0 left-0 right-0 h-0.5 bg-primary" />
|
||||
)}
|
||||
</button>
|
||||
{deploymentSupported && (
|
||||
<button
|
||||
onClick={() => setActiveTab("azure")}
|
||||
className={`px-4 py-2 text-sm font-medium transition-colors relative ${
|
||||
activeTab === "azure"
|
||||
? "text-foreground"
|
||||
: "text-muted-foreground hover:text-foreground"
|
||||
}`}
|
||||
>
|
||||
<Cloud className="h-4 w-4 mr-2 inline" />
|
||||
Azure
|
||||
{activeTab === "azure" && (
|
||||
<div className="absolute bottom-0 left-0 right-0 h-0.5 bg-primary" />
|
||||
)}
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Tab Content */}
|
||||
@@ -360,34 +507,230 @@ openai>=1.0.0
|
||||
Deploy to Azure Container Apps
|
||||
</h3>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Azure Container Apps provides serverless containers with
|
||||
auto-scaling and integrated monitoring.
|
||||
{deploymentSupported
|
||||
? "One-click deployment to Azure with automatic containerization and authentication."
|
||||
: "Azure Container Apps provides serverless containers with auto-scaling and integrated monitoring."}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Prerequisites */}
|
||||
<div className="border rounded-lg p-4 space-y-3">
|
||||
<h4 className="font-medium text-sm">Prerequisites</h4>
|
||||
<ul className="text-xs space-y-1 list-disc list-inside text-muted-foreground">
|
||||
<li>Azure subscription</li>
|
||||
<li>
|
||||
Azure CLI installed (
|
||||
<code className="bg-muted px-1 rounded">
|
||||
az --version
|
||||
</code>
|
||||
)
|
||||
</li>
|
||||
{/* Prerequisites Notice */}
|
||||
<div className="bg-blue-50 dark:bg-blue-950/50 border border-blue-200 dark:border-blue-800 rounded-md p-3">
|
||||
<h4 className="text-sm font-semibold mb-2 text-blue-900 dark:text-blue-100">
|
||||
Prerequisites for Azure Deployment
|
||||
</h4>
|
||||
<ul className="text-xs space-y-1 list-disc list-inside text-blue-800 dark:text-blue-200">
|
||||
<li>Azure CLI installed and authenticated (<code className="bg-blue-100 dark:bg-blue-900 px-1 rounded">az login</code>)</li>
|
||||
<li>Docker installed and running</li>
|
||||
<li>
|
||||
Logged in to Azure:{" "}
|
||||
<code className="bg-muted px-1 rounded">az login</code>
|
||||
<li>Azure subscription with the following providers registered:
|
||||
<ul className="ml-4 mt-1 space-y-0.5">
|
||||
<li className="list-none">• <code className="bg-blue-100 dark:bg-blue-900 px-1 rounded text-xs">Microsoft.App</code> (Container Apps)</li>
|
||||
<li className="list-none">• <code className="bg-blue-100 dark:bg-blue-900 px-1 rounded text-xs">Microsoft.ContainerRegistry</code> (ACR)</li>
|
||||
<li className="list-none">• <code className="bg-blue-100 dark:bg-blue-900 px-1 rounded text-xs">Microsoft.OperationalInsights</code> (Logging)</li>
|
||||
</ul>
|
||||
</li>
|
||||
</ul>
|
||||
<details className="mt-2">
|
||||
<summary className="text-xs cursor-pointer hover:underline text-blue-700 dark:text-blue-300">
|
||||
How to register providers?
|
||||
</summary>
|
||||
<div className="mt-2 p-2 bg-blue-100 dark:bg-blue-900 rounded text-xs">
|
||||
<p className="mb-1">Run these commands once per subscription:</p>
|
||||
<code className="block font-mono">
|
||||
az provider register -n Microsoft.App --wait<br/>
|
||||
az provider register -n Microsoft.ContainerRegistry --wait<br/>
|
||||
az provider register -n Microsoft.OperationalInsights --wait
|
||||
</code>
|
||||
</div>
|
||||
</details>
|
||||
</div>
|
||||
|
||||
{/* Step-by-step */}
|
||||
<div className="space-y-3">
|
||||
<h4 className="font-medium text-sm">Deployment Steps</h4>
|
||||
{/* Functional Deployment Form (only if supported) */}
|
||||
{deploymentSupported && entity && !lastDeployment && (
|
||||
<div className="border rounded-lg p-4 space-y-4">
|
||||
{!isDeploying ? (
|
||||
<>
|
||||
<div className="space-y-3">
|
||||
<div>
|
||||
<label className="text-sm font-medium">Resource Group</label>
|
||||
<input
|
||||
type="text"
|
||||
className="w-full mt-1 px-3 py-2 border rounded-md text-sm"
|
||||
placeholder="my-test-rg"
|
||||
value={resourceGroup}
|
||||
onChange={(e) => setResourceGroup(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="text-sm font-medium">App Name</label>
|
||||
<input
|
||||
type="text"
|
||||
className={`w-full mt-1 px-3 py-2 border rounded-md text-sm ${
|
||||
appNameError ? "border-red-500" : ""
|
||||
}`}
|
||||
placeholder="my-agent-app"
|
||||
value={appName}
|
||||
onChange={(e) => {
|
||||
const newName = e.target.value;
|
||||
setAppName(newName);
|
||||
// Validate on change to provide immediate feedback
|
||||
// Trim for validation to match what will be sent
|
||||
const error = validateAppName(newName.trim());
|
||||
setAppNameError(error);
|
||||
}}
|
||||
/>
|
||||
{appNameError && (
|
||||
<p className="mt-1 text-xs text-red-600">{appNameError}</p>
|
||||
)}
|
||||
</div>
|
||||
<div>
|
||||
<label className="text-sm font-medium">Region</label>
|
||||
<select
|
||||
className="w-full mt-1 px-3 py-2 border rounded-md text-sm"
|
||||
value={region}
|
||||
onChange={(e) => setRegion(e.target.value)}
|
||||
>
|
||||
<option value="eastus">East US</option>
|
||||
<option value="westus">West US</option>
|
||||
<option value="westeurope">West Europe</option>
|
||||
<option value="eastasia">East Asia</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<Button
|
||||
onClick={handleDeploy}
|
||||
disabled={!resourceGroup || !appName || !!appNameError}
|
||||
className="w-full"
|
||||
>
|
||||
<Rocket className="h-4 w-4 mr-2" />
|
||||
Deploy to Azure
|
||||
</Button>
|
||||
</>
|
||||
) : (
|
||||
<div className="space-y-2">
|
||||
<div className="flex items-center gap-2 text-sm font-medium">
|
||||
<Loader2 className="h-4 w-4 animate-spin" />
|
||||
Deploying...
|
||||
</div>
|
||||
<div
|
||||
ref={logsContainerRef}
|
||||
className="bg-muted p-3 rounded-md text-xs font-mono max-h-60 overflow-y-auto space-y-1"
|
||||
>
|
||||
{deploymentLogs.map((log, i) => (
|
||||
<div key={i} className={log.includes("failed") || log.includes("Error") ? "text-red-600" : ""}>{log}</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Show logs after deployment stops (success or failure) */}
|
||||
{!isDeploying && deploymentLogs.length > 0 && !lastDeployment && (
|
||||
<div className="space-y-2">
|
||||
<div className="flex items-center gap-2 text-sm font-medium text-red-600">
|
||||
<AlertCircle className="h-4 w-4" />
|
||||
Deployment Failed
|
||||
</div>
|
||||
<div className="bg-muted p-3 rounded-md text-xs font-mono max-h-60 overflow-y-auto space-y-1">
|
||||
{deploymentLogs.map((log, i) => (
|
||||
<div key={i} className={log.includes("failed") || log.includes("Error") ? "text-red-600" : ""}>{log}</div>
|
||||
))}
|
||||
</div>
|
||||
<Button onClick={clearDeploymentState} variant="outline" className="w-full">
|
||||
Try Again
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Success Screen */}
|
||||
{lastDeployment && (
|
||||
<div className="border-2 border-green-200 bg-green-50 dark:bg-green-950/50 rounded-lg p-4 space-y-3">
|
||||
<div className="flex items-center gap-2">
|
||||
<CheckCircle2 className="h-5 w-5 text-green-600" />
|
||||
<h4 className="font-semibold text-green-900 dark:text-green-100">
|
||||
Deployment Successful!
|
||||
</h4>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<div>
|
||||
<label className="text-xs font-medium text-green-800 dark:text-green-200">
|
||||
Deployment URL
|
||||
</label>
|
||||
<div className="flex gap-2 mt-1">
|
||||
<code className="flex-1 bg-white dark:bg-gray-900 px-3 py-2 rounded border text-sm">
|
||||
{lastDeployment.url}
|
||||
</code>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="outline"
|
||||
onClick={() => window.open(lastDeployment.url, "_blank")}
|
||||
>
|
||||
<ExternalLink className="h-4 w-4" />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<label className="text-xs font-medium text-green-800 dark:text-green-200">
|
||||
Auth Token (save this - shown only once)
|
||||
</label>
|
||||
<div className="flex gap-2 mt-1">
|
||||
<code className="flex-1 bg-white dark:bg-gray-900 px-3 py-2 rounded border text-sm font-mono">
|
||||
{lastDeployment.authToken}
|
||||
</code>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="outline"
|
||||
onClick={() => navigator.clipboard.writeText(lastDeployment.authToken)}
|
||||
>
|
||||
<Copy className="h-4 w-4" />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<Button onClick={clearDeploymentState} variant="outline" className="w-full">
|
||||
Deploy Another
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Deployment Not Supported Warning */}
|
||||
{!deploymentSupported && entity?.deployment_reason && (
|
||||
<div className="bg-amber-50 dark:bg-amber-950/50 border border-amber-200 dark:border-amber-800 rounded-md p-3">
|
||||
<div className="flex items-start gap-2">
|
||||
<AlertCircle className="h-4 w-4 mt-0.5 text-amber-600 flex-shrink-0" />
|
||||
<div className="text-sm text-amber-800 dark:text-amber-200">
|
||||
<strong>Deployment not available:</strong> {entity.deployment_reason}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* CLI Instructions (only show when deployment not supported) */}
|
||||
{!deploymentSupported && (
|
||||
<>
|
||||
{/* Prerequisites */}
|
||||
<div className="border rounded-lg p-4 space-y-3">
|
||||
<h4 className="font-medium text-sm">Prerequisites</h4>
|
||||
<ul className="text-xs space-y-1 list-disc list-inside text-muted-foreground">
|
||||
<li>Azure subscription</li>
|
||||
<li>
|
||||
Azure CLI installed (
|
||||
<code className="bg-muted px-1 rounded">
|
||||
az --version
|
||||
</code>
|
||||
)
|
||||
</li>
|
||||
<li>Docker installed and running</li>
|
||||
<li>
|
||||
Logged in to Azure:{" "}
|
||||
<code className="bg-muted px-1 rounded">az login</code>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
{/* Step-by-step */}
|
||||
<div className="space-y-3">
|
||||
<h4 className="font-medium text-sm">Deployment Steps</h4>
|
||||
|
||||
<div className="space-y-3">
|
||||
{/* Step 1 */}
|
||||
@@ -508,6 +851,8 @@ az acr build --registry myregistry \\
|
||||
</a>
|
||||
</Button>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user