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
30 changes: 23 additions & 7 deletions python/packages/devui/agent_framework_devui/_discovery.py
Original file line number Diff line number Diff line change
Expand Up @@ -182,14 +182,29 @@ async def _load_directory_entity(self, entity_id: str, entity_info: EntityInfo)
f"{entity_id}.workflow",
]

# Track import errors to provide meaningful feedback
import_errors: list[tuple[str, Exception]] = []

for pattern in import_patterns:
module = self._load_module_from_pattern(pattern)
module, error = self._load_module_from_pattern(pattern)
if error:
import_errors.append((pattern, error))
if module:
# Find entity in module - pass entity_id so registration uses correct ID
entity_obj = await self._find_entity_in_module(module, entity_id, str(dir_path))
if entity_obj:
return entity_obj

# If we have import errors, raise the most informative one
if import_errors:
# Prefer errors from the main module pattern (entity_id) or agent submodule
for pattern, error in import_errors:
if pattern == entity_id or pattern.endswith(".agent"):
raise ValueError(f"Failed to load entity '{entity_id}': {error}") from error
# Fall back to first error
pattern, error = import_errors[0]
raise ValueError(f"Failed to load entity '{entity_id}': {error}") from error

raise ValueError(f"No valid entity found in {dir_path}")
# File-based entity
module = self._load_module_from_file(dir_path, entity_id)
Expand Down Expand Up @@ -632,31 +647,32 @@ def _load_env_file(self, env_path: Path) -> bool:
return True
return False

def _load_module_from_pattern(self, pattern: str) -> Any | None:
def _load_module_from_pattern(self, pattern: str) -> tuple[Any | None, Exception | None]:
"""Load module using import pattern.

Args:
pattern: Import pattern to try

Returns:
Loaded module or None if failed
Tuple of (loaded module or None, error or None)
"""
try:
# Check if module exists first
spec = importlib.util.find_spec(pattern)
if spec is None:
return None
return None, None

module = importlib.import_module(pattern)
logger.debug(f"Successfully imported {pattern}")
return module
return module, None

except ModuleNotFoundError:
logger.debug(f"Import pattern {pattern} not found")
return None
return None, None
except Exception as e:
# Capture the actual error for better error messages
logger.warning(f"Error importing {pattern}: {e}")
return None
return None, e

def _load_module_from_file(self, file_path: Path, module_name: str) -> Any | None:
"""Load module directly from file path.
Expand Down
18 changes: 16 additions & 2 deletions python/packages/devui/agent_framework_devui/_executor.py
Original file line number Diff line number Diff line change
Expand Up @@ -642,12 +642,26 @@ def _convert_openai_input_to_chat_message(
media_type = "audio/mp4" if ext == "m4a" else f"audio/{ext}"

# Use file_data or file_url
# Include filename in additional_properties for OpenAI/Azure file handling
additional_props = {"filename": filename} if filename else None
if file_data:
# Assume file_data is base64, create data URI
data_uri = f"data:{media_type};base64,{file_data}"
contents.append(DataContent(uri=data_uri, media_type=media_type))
contents.append(
DataContent(
uri=data_uri,
media_type=media_type,
additional_properties=additional_props,
)
)
elif file_url:
contents.append(DataContent(uri=file_url, media_type=media_type))
contents.append(
DataContent(
uri=file_url,
media_type=media_type,
additional_properties=additional_props,
)
)

elif content_type == "function_approval_response":
# Handle function approval response (DevUI extension)
Expand Down
9 changes: 7 additions & 2 deletions python/packages/devui/agent_framework_devui/_server.py
Original file line number Diff line number Diff line change
Expand Up @@ -537,9 +537,14 @@ async def get_entity_info(entity_id: str) -> EntityInfo:
except HTTPException:
raise
except ValueError as e:
# ValueError from load_entity indicates entity not found or invalid
# ValueError from load_entity - could be "not found" or "failed to load"
error_str = str(e)
error_msg = self._format_error(e, "Entity loading")
raise HTTPException(status_code=404, detail=error_msg) from e
# Use 404 for "not found", 422 for load failures (entity exists but can't load)
if "not found" in error_str.lower() and "failed to load" not in error_str.lower():
raise HTTPException(status_code=404, detail=error_msg) from e
# Entity exists but failed to load (e.g., missing env vars, import errors)
raise HTTPException(status_code=422, detail=error_msg) from e
except Exception as e:
error_msg = self._format_error(e, "Entity info retrieval")
raise HTTPException(status_code=500, detail=error_msg) from e
Expand Down

Large diffs are not rendered by default.

192 changes: 96 additions & 96 deletions python/packages/devui/agent_framework_devui/ui/assets/index.js

Large diffs are not rendered by default.

17 changes: 15 additions & 2 deletions python/packages/devui/frontend/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,7 @@ export default function App() {

// Toast state and actions
const toasts = useDevUIStore((state) => state.toasts);
const addToast = useDevUIStore((state) => state.addToast);
const removeToast = useDevUIStore((state) => state.removeToast);

// Initialize app - load agents and workflows
Expand Down Expand Up @@ -174,6 +175,12 @@ export default function App() {
`Failed to load full info for first entity ${selectedEntity.id}:`,
error
);
// Show toast for entity load errors (don't use setEntityError - that kills the whole UI)
const errorMessage = error instanceof Error ? error.message : String(error);
addToast({
type: "error",
message: `Failed to load "${selectedEntity.id}": ${errorMessage}`,
});
}
}
}
Expand All @@ -194,7 +201,7 @@ export default function App() {
};

loadData();
}, [setAgents, setWorkflows, selectEntity, updateAgent, updateWorkflow, setIsLoadingEntities, setEntityError, setShowEntityNotFoundToast]);
}, [setAgents, setWorkflows, selectEntity, updateAgent, updateWorkflow, setIsLoadingEntities, setEntityError, setShowEntityNotFoundToast, addToast, setEntities]);

// Handle auth token submission
const handleAuthTokenSubmit = useCallback(async () => {
Expand Down Expand Up @@ -284,10 +291,16 @@ export default function App() {
}
} catch (error) {
console.error(`Failed to load full info for ${item.id}:`, error);
// Show toast for entity load errors (don't use setEntityError - that kills the whole UI)
const errorMessage = error instanceof Error ? error.message : String(error);
addToast({
type: "error",
message: `Failed to load "${item.id}": ${errorMessage}`,
});
}
}
},
[selectEntity, updateAgent, updateWorkflow]
[selectEntity, updateAgent, updateWorkflow, addToast]
);

// Handle debug events from active view
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
* This is the CORRECT implementation that works with OpenAI types only
*/

import { useState } from "react";
import { useState, useEffect } from "react";
import {
Download,
FileText,
Expand Down Expand Up @@ -80,8 +80,59 @@ function ImageContentRenderer({ content, className }: ContentRendererProps) {
);
}

// Helper to convert base64 (or data URI) to blob URL for better browser compatibility
function useBase64ToBlobUrl(data: string | undefined, mimeType: string): string | null {
const [blobUrl, setBlobUrl] = useState<string | null>(null);

useEffect(() => {
if (!data) {
setBlobUrl(null);
return;
}

try {
// Handle both data URI format and raw base64
let base64Data: string;
if (data.startsWith('data:')) {
// Extract base64 from data URI (e.g., "data:application/pdf;base64,...")
const parts = data.split(',');
if (parts.length !== 2) {
setBlobUrl(null);
return;
}
base64Data = parts[1];
} else {
// Raw base64 data
base64Data = data;
}

const binaryString = atob(base64Data);
const bytes = new Uint8Array(binaryString.length);
for (let i = 0; i < binaryString.length; i++) {
bytes[i] = binaryString.charCodeAt(i);
}

const blob = new Blob([bytes], { type: mimeType });
const url = URL.createObjectURL(blob);
setBlobUrl(url);

// Cleanup on unmount or when data changes
return () => {
URL.revokeObjectURL(url);
};
} catch (error) {
console.error('Failed to convert base64 to blob URL:', error);
setBlobUrl(null);
}
}, [data, mimeType]);

return blobUrl;
}

// File content renderer (handles both input and output files)
function FileContentRenderer({ content, className }: ContentRendererProps) {
const [isExpanded, setIsExpanded] = useState(true);

if (content.type !== "input_file" && content.type !== "output_file") return null;

const fileUrl = content.file_url || content.file_data;
Expand All @@ -91,31 +142,73 @@ function FileContentRenderer({ content, className }: ContentRendererProps) {
const isPdf = filename?.toLowerCase().endsWith(".pdf") || fileUrl?.includes("application/pdf");
const isAudio = filename?.toLowerCase().match(/\.(mp3|wav|m4a|ogg|flac|aac)$/);

// For PDFs, try to embed
// Convert base64 to blob URL for PDFs (better browser compatibility)
// Use file_data (raw base64) if available, otherwise try file_url
const pdfData = isPdf ? (content.file_data || content.file_url) : undefined;
const pdfBlobUrl = useBase64ToBlobUrl(pdfData, 'application/pdf');

// Use blob URL if available, otherwise fall back to original URL
const effectivePdfUrl = pdfBlobUrl || fileUrl;

// Helper to open PDF in new tab
const openPdfInNewTab = () => {
if (effectivePdfUrl) {
window.open(effectivePdfUrl, '_blank');
}
};

// For PDFs - show a clean card with actions (inline preview is unreliable across browsers)
if (isPdf && fileUrl) {
return (
<div className={`my-2 ${className || ""}`}>
<div className="border rounded-lg overflow-hidden">
<iframe
src={fileUrl}
className="w-full h-96"
title={filename}
/>
</div>
<div className="flex items-center gap-2 mt-2">
<FileText className="h-4 w-4 text-muted-foreground" />
<span className="text-sm text-muted-foreground">{filename}</span>
{fileUrl && (
<a
href={fileUrl}
download={filename}
className="ml-auto text-xs text-primary hover:underline flex items-center gap-1"
>
<Download className="h-3 w-3" />
Download
</a>
)}
{/* Header with filename and controls */}
<div className="flex items-center gap-2 mb-2 px-1">
<FileText className="h-4 w-4 text-red-500" />
<span className="text-sm font-medium truncate flex-1">{filename}</span>
<button
onClick={() => setIsExpanded(!isExpanded)}
className="text-xs text-muted-foreground hover:text-foreground flex items-center gap-1"
>
{isExpanded ? (
<>
<ChevronDown className="h-3 w-3" />
Collapse
</>
) : (
<>
<ChevronRight className="h-3 w-3" />
Expand
</>
)}
</button>
</div>

{/* PDF Card with actions */}
{isExpanded && (
<div className="border rounded-lg p-6 bg-muted/50 flex flex-col items-center justify-center gap-4">
<FileText className="h-16 w-16 text-red-400" />
<div className="text-center">
<p className="text-sm font-medium mb-1">{filename}</p>
<p className="text-xs text-muted-foreground">PDF Document</p>
</div>
<div className="flex gap-3">
<button
onClick={openPdfInNewTab}
className="text-sm bg-primary text-primary-foreground hover:bg-primary/90 flex items-center gap-2 px-4 py-2 rounded-md transition-colors"
>
Open in new tab
</button>
<a
href={effectivePdfUrl || fileUrl}
download={filename}
className="text-sm text-foreground hover:bg-accent flex items-center gap-2 px-4 py-2 border rounded-md transition-colors"
>
<Download className="h-4 w-4" />
Download
</a>
</div>
</div>
)}
</div>
);
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
# Azure OpenAI Responses API Configuration
# The Responses API supports PDF uploads, images, and other multimodal content.
# Requires api-version 2025-03-01-preview or later.

# Option 1: Use API key authentication
AZURE_OPENAI_API_KEY=your-azure-openai-api-key-here

# Option 2: Use Azure CLI authentication (run 'az login' first)
# No API key needed - just leave AZURE_OPENAI_API_KEY unset

# Required: Azure OpenAI endpoint with Responses API support
AZURE_OPENAI_ENDPOINT=https://your-resource.cognitiveservices.azure.com/

# Required: Deployment name (must support Responses API)
AZURE_OPENAI_RESPONSES_DEPLOYMENT_NAME=gpt-4.1-mini
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
# Copyright (c) Microsoft. All rights reserved.
"""Azure Responses Agent sample for DevUI."""

from .agent import agent

__all__ = ["agent"]
Loading
Loading