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
8 changes: 8 additions & 0 deletions agentic_ai/workflow/fraud_detection/backend.py
Original file line number Diff line number Diff line change
Expand Up @@ -204,6 +204,14 @@ async def broadcast(self, message: dict):
timestamp=datetime.now().isoformat(),
severity="high",
),
"ALERT-004": SuspiciousActivityAlert(
alert_id="ALERT-004",
customer_id=4,
alert_type="routine_check",
description="Routine security check - password changed from usual device",
timestamp=datetime.now().isoformat(),
severity="low",
),
}


Expand Down
12 changes: 9 additions & 3 deletions agentic_ai/workflow/fraud_detection/ui/src/App.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -68,8 +68,14 @@ function App() {
try {
const event = lastMessage;

// Add to event log
setEvents((prev) => [...prev, event]);
// Add to event log - prevent duplicates by checking timestamp + type + executor_id
setEvents((prev) => {
const eventKey = `${event.timestamp}-${event.type || event.event_type}-${event.executor_id || ''}`;
const isDuplicate = prev.some(
(e) => `${e.timestamp}-${e.type || e.event_type}-${e.executor_id || ''}` === eventKey
);
return isDuplicate ? prev : [...prev, event];
});

// Handle workflow initialization
if (event.type === 'workflow_initializing') {
Expand Down Expand Up @@ -212,7 +218,7 @@ function App() {
</Grid>

{/* Right Column - Event Log */}
<Grid item xs={12} md={3}>
<Grid item xs={12} md={3} sx={{ display: 'flex', flexDirection: 'column', height: '100%' }}>
<EventLog events={events} />
</Grid>
</Grid>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -58,10 +58,10 @@ function AnalystDecisionPanel({ decision, onSubmit }) {
<Paper
elevation={3}
sx={{
p: 3,
p: 1.5,
display: 'flex',
flexDirection: 'column',
gap: 2,
gap: 1,
border: 3,
borderColor: 'warning.main',
animation: 'pulse 2s ease-in-out infinite',
Expand All @@ -71,51 +71,49 @@ function AnalystDecisionPanel({ decision, onSubmit }) {
},
}}
>
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
<GavelIcon color="warning" />
<Typography variant="h6">Analyst Review Required</Typography>
<Box sx={{ display: 'flex', alignItems: 'center', gap: 0.5 }}>
<GavelIcon color="warning" fontSize="small" />
<Typography variant="subtitle1" fontWeight="bold">Analyst Review Required</Typography>
</Box>

<Alert severity="warning" sx={{ mb: 1 }}>
<Typography variant="body2" fontWeight="bold">
<Alert severity="warning" sx={{ py: 0.25, px: 1 }}>
<Typography variant="caption" fontWeight="bold">
Human Decision Needed
</Typography>
<Typography variant="caption">
The workflow is paused pending your review
</Typography>
</Alert>

<Divider />
<Divider sx={{ my: 0.5 }} />

{/* Risk Assessment */}
<Box>
<Typography variant="subtitle2" gutterBottom>
<Typography variant="caption" fontWeight="bold" display="block" sx={{ mb: 0.5 }}>
Risk Assessment
</Typography>
<Box sx={{ display: 'flex', gap: 1, alignItems: 'center', mb: 1 }}>
<Typography variant="body2">Risk Score:</Typography>
<Box sx={{ display: 'flex', gap: 0.5, alignItems: 'center', mb: 0.5 }}>
<Typography variant="caption">Risk Score:</Typography>
<Chip
label={`${(decision.data?.risk_score || 0).toFixed(2)} - ${getRiskLevel(
decision.data?.risk_score || 0
)}`}
color={getRiskColor(decision.data?.risk_score || 0)}
size="small"
sx={{ height: 20, fontSize: '0.7rem' }}
/>
</Box>
<Box sx={{ display: 'flex', gap: 1, alignItems: 'center' }}>
<Typography variant="body2">Alert ID:</Typography>
<Chip label={decision.data?.alert_id} size="small" variant="outlined" />
<Box sx={{ display: 'flex', gap: 0.5, alignItems: 'center' }}>
<Typography variant="caption">Alert ID:</Typography>
<Chip label={decision.data?.alert_id} size="small" variant="outlined" sx={{ height: 20, fontSize: '0.7rem' }} />
</Box>
</Box>

{/* Reasoning */}
{decision.data?.reasoning && (
<Box>
<Typography variant="subtitle2" gutterBottom>
<Typography variant="caption" fontWeight="bold" display="block" sx={{ mb: 0.5 }}>
AI Analysis
</Typography>
<Paper variant="outlined" sx={{ p: 1.5, bgcolor: 'grey.50', maxHeight: 150, overflow: 'auto' }}>
<Typography variant="caption" sx={{ whiteSpace: 'pre-wrap' }}>
<Paper variant="outlined" sx={{ p: 0.75, bgcolor: 'grey.50', maxHeight: 80, overflow: 'auto' }}>
<Typography variant="caption" sx={{ whiteSpace: 'pre-wrap', fontSize: '0.7rem' }}>
{decision.data.reasoning}
</Typography>
</Paper>
Expand All @@ -124,7 +122,7 @@ function AnalystDecisionPanel({ decision, onSubmit }) {

{/* Recommended Action */}
<Box>
<Typography variant="subtitle2" gutterBottom>
<Typography variant="caption" fontWeight="bold" display="block" sx={{ mb: 0.5 }}>
Recommended Action
</Typography>
<Chip
Expand All @@ -136,22 +134,24 @@ function AnalystDecisionPanel({ decision, onSubmit }) {
ACTION_OPTIONS.find((opt) => opt.value === decision.data?.recommended_action)
?.color || 'default'
}
size="medium"
size="small"
sx={{ height: 20, fontSize: '0.7rem' }}
/>
</Box>

<Divider />
<Divider sx={{ my: 0.5 }} />

{/* Decision Form */}
<FormControl fullWidth>
<InputLabel>Your Decision</InputLabel>
<FormControl fullWidth size="small" sx={{ minHeight: 40 }}>
<InputLabel sx={{ fontSize: '0.875rem' }}>Your Decision</InputLabel>
<Select
value={selectedAction}
label="Your Decision"
onChange={(e) => setSelectedAction(e.target.value)}
sx={{ fontSize: '0.875rem' }}
>
{ACTION_OPTIONS.map((option) => (
<MenuItem key={option.value} value={option.value}>
<MenuItem key={option.value} value={option.value} sx={{ fontSize: '0.875rem' }}>
{option.label}
</MenuItem>
))}
Expand All @@ -161,20 +161,23 @@ function AnalystDecisionPanel({ decision, onSubmit }) {
<TextField
label="Analyst Notes"
multiline
rows={3}
rows={2}
fullWidth
size="small"
value={notes}
onChange={(e) => setNotes(e.target.value)}
placeholder="Add your analysis and reasoning..."
placeholder="Add notes..."
sx={{ '& .MuiInputBase-input': { fontSize: '0.875rem' } }}
/>

<Button
variant="contained"
color="primary"
size="large"
size="small"
fullWidth
startIcon={<SendIcon />}
startIcon={<SendIcon fontSize="small" />}
onClick={handleSubmit}
sx={{ mt: 0.5, py: 0.75 }}
>
Submit Decision
</Button>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -53,35 +53,37 @@ function ControlPanel({ alerts, onStartWorkflow, workflowRunning, selectedAlert
};

return (
<Paper elevation={3} sx={{ p: 3, display: 'flex', flexDirection: 'column', gap: 2 }}>
<Typography variant="h6" gutterBottom>
<Paper elevation={3} sx={{ p: 1.5, display: 'flex', flexDirection: 'column', gap: 1 }}>
<Typography variant="subtitle1" fontWeight="bold" sx={{ mb: 0.5 }}>
Workflow Control
</Typography>

<FormControl fullWidth>
<InputLabel>Select Alert</InputLabel>
<FormControl fullWidth size="small">
<InputLabel sx={{ fontSize: '0.875rem' }}>Select Alert</InputLabel>
<Select
value={selectedAlertId}
label="Select Alert"
onChange={(e) => setSelectedAlertId(e.target.value)}
disabled={workflowRunning}
sx={{ fontSize: '0.875rem' }}
>
{alerts.map((alert) => (
<MenuItem key={alert.alert_id} value={alert.alert_id}>
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1, width: '100%' }}>
<MenuItem key={alert.alert_id} value={alert.alert_id} sx={{ py: 0.75 }}>
<Box sx={{ display: 'flex', alignItems: 'center', gap: 0.5, width: '100%' }}>
{getSeverityIcon(alert.severity)}
<Box sx={{ flex: 1 }}>
<Typography variant="body2" fontWeight="bold">
<Typography variant="caption" fontWeight="bold" display="block">
{alert.alert_id}
</Typography>
<Typography variant="caption" color="text.secondary">
<Typography variant="caption" color="text.secondary" sx={{ fontSize: '0.7rem' }}>
{alert.alert_type}
</Typography>
</Box>
<Chip
label={alert.severity}
size="small"
color={getSeverityColor(alert.severity)}
sx={{ height: 18, fontSize: '0.7rem' }}
/>
</Box>
</MenuItem>
Expand All @@ -90,46 +92,48 @@ function ControlPanel({ alerts, onStartWorkflow, workflowRunning, selectedAlert
</FormControl>

{selectedAlertId && !workflowRunning && (
<Box sx={{ p: 2, bgcolor: 'grey.100', borderRadius: 1 }}>
<Typography variant="body2" color="text.secondary" gutterBottom>
<Box sx={{ p: 1, bgcolor: 'grey.100', borderRadius: 1 }}>
<Typography variant="caption" color="text.secondary" display="block" sx={{ mb: 0.5 }}>
<strong>Description:</strong>
</Typography>
<Typography variant="body2">
<Typography variant="caption" display="block" sx={{ mb: 0.5 }}>
{alerts.find((a) => a.alert_id === selectedAlertId)?.description}
</Typography>
<Box sx={{ mt: 1, display: 'flex', gap: 1 }}>
<Box sx={{ display: 'flex', gap: 0.5, flexWrap: 'wrap' }}>
<Chip
label={`Customer ${alerts.find((a) => a.alert_id === selectedAlertId)?.customer_id}`}
size="small"
variant="outlined"
sx={{ height: 18, fontSize: '0.7rem' }}
/>
<Chip
label={alerts.find((a) => a.alert_id === selectedAlertId)?.alert_type}
size="small"
variant="outlined"
sx={{ height: 18, fontSize: '0.7rem' }}
/>
</Box>
</Box>
)}

<Button
variant="contained"
size="large"
size="small"
fullWidth
startIcon={workflowRunning ? <CircularProgress size={20} color="inherit" /> : <PlayArrowIcon />}
startIcon={workflowRunning ? <CircularProgress size={16} color="inherit" /> : <PlayArrowIcon fontSize="small" />}
onClick={handleStartClick}
disabled={!selectedAlertId || workflowRunning}
sx={{ mt: 1 }}
sx={{ mt: 0.5, py: 0.75, fontSize: '0.875rem' }}
>
{workflowRunning ? 'Workflow Running...' : 'Start Workflow'}
{workflowRunning ? 'Running...' : 'Start Workflow'}
</Button>

{selectedAlert && workflowRunning && (
<Box sx={{ p: 2, bgcolor: 'primary.main', color: 'white', borderRadius: 1 }}>
<Typography variant="body2" fontWeight="bold">
<Box sx={{ p: 1, bgcolor: 'primary.main', color: 'white', borderRadius: 1 }}>
<Typography variant="caption" fontWeight="bold" display="block">
Active Workflow
</Typography>
<Typography variant="caption">
<Typography variant="caption" sx={{ fontSize: '0.7rem' }}>
Processing {selectedAlert.alert_id}
</Typography>
</Box>
Expand Down
46 changes: 30 additions & 16 deletions agentic_ai/workflow/fraud_detection/ui/src/components/EventLog.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -107,10 +107,10 @@ function EventLog({ events }) {
};

return (
<Paper elevation={3} sx={{ height: '100%', display: 'flex', flexDirection: 'column' }}>
<Box sx={{ p: 2, borderBottom: 1, borderColor: 'divider' }}>
<Typography variant="h6">Event Log</Typography>
<Typography variant="caption" color="text.secondary">
<Paper elevation={3} sx={{ height: '100%', display: 'flex', flexDirection: 'column', overflow: 'hidden' }}>
<Box sx={{ p: 1.5, borderBottom: 1, borderColor: 'divider', flexShrink: 0 }}>
<Typography variant="subtitle1" fontWeight="bold">Event Log</Typography>
<Typography variant="caption" color="text.secondary" sx={{ fontSize: '0.7rem' }}>
{events.length} events
</Typography>
</Box>
Expand All @@ -120,13 +120,27 @@ function EventLog({ events }) {
sx={{
flex: 1,
overflow: 'auto',
px: 1,
px: 0.5,
py: 0,
minHeight: 0,
'&::-webkit-scrollbar': {
width: '8px',
},
'&::-webkit-scrollbar-track': {
backgroundColor: 'grey.100',
},
'&::-webkit-scrollbar-thumb': {
backgroundColor: 'grey.400',
borderRadius: '4px',
'&:hover': {
backgroundColor: 'grey.600',
},
},
}}
>
{events.length === 0 ? (
<Box sx={{ p: 3, textAlign: 'center' }}>
<Typography variant="body2" color="text.secondary">
<Box sx={{ p: 2, textAlign: 'center' }}>
<Typography variant="caption" color="text.secondary">
No events yet. Start a workflow to see events.
</Typography>
</Box>
Expand All @@ -135,38 +149,38 @@ function EventLog({ events }) {
<React.Fragment key={index}>
<ListItem
sx={{
py: 1.5,
px: 1,
py: 0.75,
px: 0.75,
'&:hover': {
bgcolor: 'action.hover',
},
}}
>
<ListItemIcon sx={{ minWidth: 40 }}>
{getEventIcon(event)}
<ListItemIcon sx={{ minWidth: 32 }}>
{React.cloneElement(getEventIcon(event), { fontSize: 'small' })}
</ListItemIcon>
<ListItemText
primary={
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
<Typography variant="body2" fontWeight="medium">
<Box sx={{ display: 'flex', alignItems: 'center', gap: 0.5, flexWrap: 'wrap' }}>
<Typography variant="caption" fontWeight="medium" sx={{ fontSize: '0.75rem' }}>
{getEventTitle(event)}
</Typography>
<Chip
label={event.event_type || event.type}
size="small"
color={getEventColor(event)}
sx={{ height: 20, fontSize: 10 }}
sx={{ height: 16, fontSize: '0.65rem' }}
/>
</Box>
}
secondary={
<Typography variant="caption" color="text.secondary">
<Typography variant="caption" color="text.secondary" sx={{ fontSize: '0.65rem' }}>
{formatTime(event.timestamp)}
</Typography>
}
/>
</ListItem>
{index < events.length - 1 && <Divider variant="inset" component="li" />}
{index < events.length - 1 && <Divider variant="inset" component="li" sx={{ ml: 4 }} />}
</React.Fragment>
))
)}
Expand Down