Standardize agent event payloads and team activity timeline UI

This commit is contained in:
ي
2026-03-02 22:01:12 +05:30
parent cb6a3a8042
commit a7bf355703
5 changed files with 258 additions and 194 deletions

View File

@@ -1,13 +1,81 @@
from __future__ import annotations
from dataclasses import asdict, dataclass, field
from datetime import datetime
from typing import Any, Dict, List, Optional
from typing import Any, Dict, List, Optional, Union
from sqlalchemy.orm import Session
from models.agent_activity_models import AgentAlert, AgentApprovalRequest, AgentEvent, AgentRun
@dataclass
class AgentEventPayload:
"""Shared schema for agent activity event payloads."""
phase: Optional[str] = None
step: Optional[str] = None
tool_name: Optional[str] = None
progress_percent: Optional[float] = None
input_summary: Optional[str] = None
output_summary: Optional[str] = None
decision_reason: Optional[str] = None
evidence_refs: List[str] = field(default_factory=list)
safe_debug: bool = True
metadata: Dict[str, Any] = field(default_factory=dict)
def build_agent_event_payload(
*,
phase: Optional[str] = None,
step: Optional[str] = None,
tool_name: Optional[str] = None,
progress_percent: Optional[float] = None,
input_summary: Optional[str] = None,
output_summary: Optional[str] = None,
decision_reason: Optional[str] = None,
evidence_refs: Optional[List[str]] = None,
safe_debug: bool = True,
metadata: Optional[Dict[str, Any]] = None,
) -> Dict[str, Any]:
return asdict(
AgentEventPayload(
phase=phase,
step=step,
tool_name=tool_name,
progress_percent=progress_percent,
input_summary=input_summary,
output_summary=output_summary,
decision_reason=decision_reason,
evidence_refs=list(evidence_refs or []),
safe_debug=bool(safe_debug),
metadata=dict(metadata or {}),
)
)
def _normalize_event_payload(payload: Optional[Union[Dict[str, Any], AgentEventPayload]]) -> Dict[str, Any]:
if payload is None:
return build_agent_event_payload()
if isinstance(payload, AgentEventPayload):
return asdict(payload)
if not isinstance(payload, dict):
return build_agent_event_payload(output_summary=str(payload)[:2000], safe_debug=False)
return build_agent_event_payload(
phase=payload.get("phase"),
step=payload.get("step"),
tool_name=payload.get("tool_name"),
progress_percent=payload.get("progress_percent"),
input_summary=payload.get("input_summary"),
output_summary=payload.get("output_summary"),
decision_reason=payload.get("decision_reason"),
evidence_refs=payload.get("evidence_refs") if isinstance(payload.get("evidence_refs"), list) else [],
safe_debug=bool(payload.get("safe_debug", True)),
metadata=payload.get("metadata") if isinstance(payload.get("metadata"), dict) else {},
)
class AgentActivityService:
def __init__(self, db: Session, user_id: str):
self.db = db
@@ -50,10 +118,11 @@ class AgentActivityService:
event_type: str,
severity: str = "info",
message: Optional[str] = None,
payload: Optional[Dict[str, Any]] = None,
payload: Optional[Union[Dict[str, Any], AgentEventPayload]] = None,
run_id: Optional[int] = None,
agent_type: Optional[str] = None,
) -> AgentEvent:
normalized_payload = _normalize_event_payload(payload)
evt = AgentEvent(
run_id=run_id,
user_id=self.user_id,
@@ -61,7 +130,7 @@ class AgentActivityService:
event_type=event_type,
severity=severity,
message=message,
payload=payload,
payload=normalized_payload,
created_at=datetime.utcnow(),
)
self.db.add(evt)

View File

@@ -31,7 +31,7 @@ from utils.logger_utils import get_service_logger
from services.database import get_session_for_user
from services.intelligence.monitoring.semantic_dashboard import RealTimeSemanticMonitor
from services.intelligence.agents.safety_framework import get_safety_framework
from services.agent_activity_service import AgentActivityService
from services.agent_activity_service import AgentActivityService, build_agent_event_payload
from services.intelligence.agents.agent_usage_tracking import track_agent_usage_sync
import time
@@ -426,7 +426,7 @@ class BaseALwrityAgent(ABC):
event_type="plan",
severity="info",
message=(prompt[:2000] if prompt else None),
payload={"kind": "prompt"},
payload=build_agent_event_payload(phase="planning", step="run_started", tool_name="agent_run", progress_percent=0, input_summary=prompt[:250], output_summary="Agent run initialized", decision_reason="Received run prompt", safe_debug=False, metadata={"kind": "prompt"}),
run_id=run_record.id,
agent_type=self.agent_type,
)
@@ -453,7 +453,7 @@ class BaseALwrityAgent(ABC):
event_type="final_summary",
severity="info",
message=(str(result)[:2000] if result is not None else None),
payload={"kind": "result"},
payload=build_agent_event_payload(phase="execution", step="run_completed", tool_name="agent_run", progress_percent=100, output_summary=str(result)[:400] if result is not None else "No output", decision_reason="Run completed", safe_debug=True, metadata={"kind": "result"}),
run_id=run_record.id,
agent_type=self.agent_type,
)
@@ -467,7 +467,7 @@ class BaseALwrityAgent(ABC):
event_type="error",
severity="error",
message=str(e)[:2000],
payload={"kind": "exception"},
payload=build_agent_event_payload(phase="execution", step="run_error", tool_name="agent_runtime", output_summary=str(e)[:400], decision_reason="Unhandled exception during run", safe_debug=False, metadata={"kind": "exception"}),
run_id=run_record.id,
agent_type=self.agent_type,
)
@@ -513,7 +513,7 @@ class BaseALwrityAgent(ABC):
event_type="plan",
severity="info",
message=f"{action.action_type} -> {action.target_resource}",
payload={"action": asdict(action)},
payload=build_agent_event_payload(phase="planning", step="action_received", tool_name=action.action_type, progress_percent=5, input_summary=f"target={action.target_resource}", output_summary="Action accepted for execution", decision_reason="Start run lifecycle", safe_debug=True, metadata={"action": asdict(action)}),
run_id=run_record.id,
agent_type=self.agent_type,
)
@@ -528,7 +528,7 @@ class BaseALwrityAgent(ABC):
event_type="decision",
severity="warning",
message="Action failed safety validation",
payload={"action_id": action.action_id, "action_type": action.action_type},
payload=build_agent_event_payload(phase="validation", step="safety_blocked", tool_name="safety_framework", progress_percent=10, input_summary=action.action_type, output_summary="Action blocked by safety validation", decision_reason="Safety framework rejected action", safe_debug=True, metadata={"action_id": action.action_id, "action_type": action.action_type}),
run_id=run_record.id,
agent_type=self.agent_type,
)
@@ -568,7 +568,7 @@ class BaseALwrityAgent(ABC):
event_type="decision",
severity="info",
message="Action requires approval",
payload={"approval_id": req.id, "action_id": action.action_id},
payload=build_agent_event_payload(phase="approval", step="awaiting_user_decision", tool_name=action.action_type, progress_percent=20, input_summary=action.target_resource, output_summary="Approval request created", decision_reason="Action requires human approval", safe_debug=True, metadata={"approval_id": req.id, "action_id": action.action_id}),
run_id=run_record.id,
agent_type=self.agent_type,
)
@@ -593,7 +593,7 @@ class BaseALwrityAgent(ABC):
event_type="progress",
severity="info",
message="Rollback checkpoint created",
payload={"checkpoint_id": checkpoint_id},
payload=build_agent_event_payload(phase="safety", step="checkpoint_created", tool_name="rollback_manager", progress_percent=35, output_summary="Rollback checkpoint created", decision_reason="Prepare rollback safety net", safe_debug=True, metadata={"checkpoint_id": checkpoint_id}),
run_id=run_record.id,
agent_type=self.agent_type,
)
@@ -604,7 +604,7 @@ class BaseALwrityAgent(ABC):
event_type="warning",
severity="warning",
message=str(e)[:2000],
payload={"checkpoint": "failed"},
payload=build_agent_event_payload(phase="safety", step="checkpoint_failed", tool_name="rollback_manager", progress_percent=30, output_summary="Checkpoint creation failed", decision_reason="Proceeding without checkpoint", safe_debug=False, metadata={"checkpoint": "failed"}),
run_id=run_record.id,
agent_type=self.agent_type,
)
@@ -639,7 +639,7 @@ class BaseALwrityAgent(ABC):
event_type="final_summary",
severity="info",
message=str(result)[:2000] if result is not None else None,
payload={"action_id": action.action_id},
payload=build_agent_event_payload(phase="execution", step="completed", tool_name=action.action_type, progress_percent=100, output_summary=str(result)[:400] if result is not None else "No output", decision_reason="Action execution completed", safe_debug=True, metadata={"action_id": action.action_id}),
run_id=run_record.id,
agent_type=self.agent_type,
)
@@ -690,7 +690,7 @@ class BaseALwrityAgent(ABC):
event_type="error",
severity="error",
message=str(e)[:2000],
payload={"action_id": action.action_id, "checkpoint_id": checkpoint_id},
payload=build_agent_event_payload(phase="execution", step="failed", tool_name=action.action_type, progress_percent=100, output_summary=str(e)[:400], decision_reason="Exception during action execution", safe_debug=False, metadata={"action_id": action.action_id, "checkpoint_id": checkpoint_id}),
run_id=run_record.id,
agent_type=self.agent_type,
)

View File

@@ -14,6 +14,7 @@ from datetime import datetime
from loguru import logger
from ..txtai_service import TxtaiIntelligenceService
from services.intelligence.agents.core_agent_framework import BaseALwrityAgent, AgentAction, TaskProposal
from services.agent_activity_service import AgentActivityService, build_agent_event_payload
from services.seo_tools.content_strategy_service import ContentStrategyService
from services.analytics import PlatformAnalyticsService
from services.intelligence.sif_agents import SharedLLMWrapper, LocalLLMWrapper
@@ -62,6 +63,41 @@ class SIFBaseAgent(BaseALwrityAgent):
if kwargs:
logger.debug(f"[{self.__class__.__name__}] Parameters: {kwargs}")
db = None
try:
from services.database import get_session_for_user
db = get_session_for_user(self.user_id)
if not db:
return
activity = AgentActivityService(db, self.user_id)
activity.log_event(
event_type="progress",
severity="info",
message=f"{self.__class__.__name__}: {operation}",
payload=build_agent_event_payload(
phase="specialized_agent",
step=operation.lower().replace(" ", "_"),
tool_name=self.__class__.__name__,
input_summary=str(kwargs)[:300] if kwargs else None,
output_summary="Operation invoked",
decision_reason="Agent method execution trace",
safe_debug=True,
metadata={"params": kwargs} if kwargs else {},
),
run_id=None,
agent_type=self.agent_type,
)
except Exception:
pass
finally:
try:
if db:
db.close()
except Exception:
pass
def _create_txtai_agent(self):
"""
SIF agents use the intelligence service directly, but we can expose

View File

@@ -6,7 +6,7 @@ from sqlalchemy.orm import Session
from models.daily_workflow_models import DailyWorkflowPlan, DailyWorkflowTask
from models.agent_activity_models import AgentAlert
from services.agent_activity_service import AgentActivityService
from services.agent_activity_service import AgentActivityService, build_agent_event_payload
from services.llm_providers.main_text_generation import llm_text_gen
from loguru import logger
@@ -430,7 +430,7 @@ async def generate_agent_enhanced_plan(db: Session, user_id: str, date: str) ->
event_type="plan",
severity="info",
message="Building grounded daily workflow plan",
payload={"grounding": grounding},
payload=build_agent_event_payload(phase="planning", step="build_grounded_plan", tool_name="llm_text_gen", progress_percent=10, input_summary="Grounding data assembled from onboarding + alerts", output_summary="Preparing daily workflow generation", decision_reason="Need context-aware workflow", evidence_refs=["onboarding_data","recent_agent_alerts"], safe_debug=True, metadata={"grounding": grounding}),
run_id=run.id,
agent_type="TodayWorkflowGenerator",
)
@@ -449,7 +449,7 @@ async def generate_agent_enhanced_plan(db: Session, user_id: str, date: str) ->
event_type="warning",
severity="warning",
message=str(e)[:2000],
payload={"fallback": True},
payload=build_agent_event_payload(phase="generation", step="llm_failed_fallback", tool_name="llm_text_gen", progress_percent=70, output_summary="LLM generation failed, using fallback tasks", decision_reason="Exception during workflow generation", safe_debug=False, metadata={"fallback": True}),
run_id=run.id,
agent_type="TodayWorkflowGenerator",
)
@@ -467,7 +467,7 @@ async def generate_agent_enhanced_plan(db: Session, user_id: str, date: str) ->
event_type="final_summary",
severity="info",
message="Daily workflow plan generated",
payload={"date": date, "task_count": len(result.get("tasks", []))},
payload=build_agent_event_payload(phase="generation", step="workflow_generated", tool_name="llm_text_gen", progress_percent=100, output_summary=f"Generated {len(result.get('tasks', []))} tasks", decision_reason="Workflow assembled successfully", evidence_refs=[date], safe_debug=True, metadata={"date": date, "task_count": len(result.get("tasks", []))}),
run_id=run.id,
agent_type="TodayWorkflowGenerator",
)

View File

@@ -3,202 +3,161 @@ import {
Box,
Paper,
Typography,
Avatar,
AvatarGroup,
Chip,
List,
ListItem,
ListItemAvatar,
ListItemText,
Divider,
IconButton,
Tooltip
Tooltip,
CircularProgress,
Accordion,
AccordionSummary,
AccordionDetails,
Stack,
} from '@mui/material';
import {
Psychology as StrategyIcon,
Article as ContentIcon,
Search as SeoIcon,
Campaign as SocialIcon,
CompareArrows as CompetitorIcon,
Refresh as RefreshIcon,
MoreVert as MoreVertIcon
ExpandMore as ExpandMoreIcon,
} from '@mui/icons-material';
import { apiClient } from '../../../api/client';
interface AgentStatus {
id: string;
name: string;
role: string;
status: 'active' | 'thinking' | 'idle' | 'offline';
current_activity: string;
icon: React.ElementType;
color: string;
}
type EventPayload = {
phase?: string | null;
step?: string | null;
tool_name?: string | null;
progress_percent?: number | null;
input_summary?: string | null;
output_summary?: string | null;
decision_reason?: string | null;
evidence_refs?: string[] | null;
safe_debug?: boolean;
metadata?: Record<string, unknown>;
};
// Mock data - In real implementation, this would come from a backend endpoint
// /api/agents/status or similar
const AGENT_TEAM: AgentStatus[] = [
{
id: 'strategy_architect',
name: 'Strategy Architect',
role: 'Team Lead',
status: 'active',
current_activity: 'Analyzing content pillar performance',
icon: StrategyIcon,
color: '#6366f1' // Indigo
},
{
id: 'content_strategist',
name: 'Content Strategist',
role: 'Creative',
status: 'thinking',
current_activity: 'Identifying semantic gaps in "AI Tools"',
icon: ContentIcon,
color: '#10b981' // Emerald
},
{
id: 'seo_specialist',
name: 'SEO Specialist',
role: 'Technical',
status: 'idle',
current_activity: 'Monitoring SERP rankings',
icon: SeoIcon,
color: '#f59e0b' // Amber
},
{
id: 'social_manager',
name: 'Social Manager',
role: 'Engagement',
status: 'idle',
current_activity: 'Waiting for new content to schedule',
icon: SocialIcon,
color: '#ec4899' // Pink
},
{
id: 'competitor_analyst',
name: 'Competitor Analyst',
role: 'Intelligence',
status: 'active',
current_activity: 'Scanning competitor X for new posts',
icon: CompetitorIcon,
color: '#ef4444' // Red
}
];
type TeamActivityEvent = {
id: number;
event_type: string;
severity: string;
message?: string | null;
created_at?: string | null;
payload?: EventPayload | null;
};
type AgentRun = {
id: number;
agent_type: string;
status: string;
started_at?: string | null;
};
const TeamHuddleWidget: React.FC = () => {
const [loading, setLoading] = React.useState(true);
const [error, setError] = React.useState<string | null>(null);
const [timeline, setTimeline] = React.useState<Array<{ run: AgentRun; events: TeamActivityEvent[] }>>([]);
const loadTimeline = React.useCallback(async () => {
setLoading(true);
setError(null);
try {
const runsResp = await apiClient.get('/api/agents/runs', { params: { limit: 5 } });
const runs: AgentRun[] = runsResp?.data?.data?.runs || [];
const eventResponses = await Promise.all(
runs.slice(0, 3).map(async (run) => {
const eventsResp = await apiClient.get(`/api/agents/runs/${run.id}/events`, { params: { limit: 25 } });
return {
run,
events: (eventsResp?.data?.data?.events || []) as TeamActivityEvent[],
};
}),
);
setTimeline(eventResponses);
} catch (e: any) {
setError(e?.message || 'Failed to load team activity');
} finally {
setLoading(false);
}
}, []);
React.useEffect(() => {
loadTimeline();
}, [loadTimeline]);
return (
<Paper
elevation={0}
sx={{
p: 2,
borderRadius: 3,
border: '1px solid',
borderColor: 'divider',
height: '100%',
background: 'linear-gradient(180deg, #ffffff 0%, #f8fafc 100%)'
}}
>
<Paper elevation={0} sx={{ p: 2, borderRadius: 3, border: '1px solid', borderColor: 'divider', height: '100%' }}>
<Box display="flex" justifyContent="space-between" alignItems="center" mb={2}>
<Box display="flex" alignItems="center" gap={1}>
<Typography variant="h6" fontWeight={700} color="text.primary">
Team Huddle
</Typography>
<Chip
label="Live"
size="small"
color="success"
sx={{ height: 20, fontSize: '0.65rem', fontWeight: 700 }}
/>
</Box>
<Box>
<Tooltip title="Refresh Team Status">
<IconButton size="small">
<RefreshIcon fontSize="small" />
</IconButton>
</Tooltip>
<Typography variant="h6" fontWeight={700}>Team Activity</Typography>
<Chip label="Live" size="small" color="success" sx={{ height: 20, fontSize: '0.65rem', fontWeight: 700 }} />
</Box>
<Tooltip title="Refresh Team Activity">
<IconButton size="small" onClick={loadTimeline}>
<RefreshIcon fontSize="small" />
</IconButton>
</Tooltip>
</Box>
<List disablePadding>
{AGENT_TEAM.map((agent, index) => (
<React.Fragment key={agent.id}>
{index > 0 && <Divider variant="inset" component="li" sx={{ my: 1, ml: 7 }} />}
<ListItem
alignItems="flex-start"
disableGutters
sx={{ py: 0.5 }}
secondaryAction={
<Tooltip title={agent.status}>
<Box
sx={{
width: 8,
height: 8,
borderRadius: '50%',
bgcolor:
agent.status === 'active' ? '#22c55e' :
agent.status === 'thinking' ? '#3b82f6' :
'#94a3b8',
boxShadow: agent.status === 'active' ? '0 0 0 2px rgba(34, 197, 94, 0.2)' : 'none',
animation: agent.status === 'thinking' ? 'pulse 1.5s infinite' : 'none',
'@keyframes pulse': {
'0%': { opacity: 1, transform: 'scale(1)' },
'50%': { opacity: 0.6, transform: 'scale(1.2)' },
'100%': { opacity: 1, transform: 'scale(1)' }
}
}}
/>
</Tooltip>
}
>
<ListItemAvatar>
<Avatar
sx={{
bgcolor: `${agent.color}15`,
color: agent.color,
width: 40,
height: 40
}}
>
<agent.icon fontSize="small" />
</Avatar>
</ListItemAvatar>
<ListItemText
primary={
<Box display="flex" alignItems="center" gap={1}>
<Typography variant="subtitle2" fontWeight={600}>
{agent.name}
</Typography>
<Typography variant="caption" color="text.secondary" sx={{ fontSize: '0.65rem', border: '1px solid #e2e8f0', px: 0.5, borderRadius: 1 }}>
{agent.role}
</Typography>
</Box>
}
secondary={
<Typography
variant="body2"
color="text.secondary"
sx={{
display: '-webkit-box',
WebkitLineClamp: 1,
WebkitBoxOrient: 'vertical',
overflow: 'hidden',
fontSize: '0.75rem',
mt: 0.25
}}
>
{agent.current_activity}
{loading && (
<Box py={4} textAlign="center">
<CircularProgress size={24} />
</Box>
)}
{!loading && error && (
<Typography variant="body2" color="error">{error}</Typography>
)}
{!loading && !error && timeline.length === 0 && (
<Typography variant="body2" color="text.secondary">No team activity yet.</Typography>
)}
{!loading && !error && timeline.length > 0 && (
<List disablePadding>
{timeline.map(({ run, events }, index) => (
<React.Fragment key={run.id}>
{index > 0 && <Divider sx={{ my: 1 }} />}
<ListItem disableGutters sx={{ display: 'block' }}>
<Stack direction="row" spacing={1} alignItems="center" sx={{ mb: 1 }}>
<Chip size="small" label={run.agent_type || 'Agent'} />
<Chip size="small" color={run.status === 'completed' ? 'success' : 'warning'} label={run.status} />
<Typography variant="caption" color="text.secondary">
{run.started_at ? new Date(run.started_at).toLocaleString() : ''}
</Typography>
}
/>
</ListItem>
</React.Fragment>
))}
</List>
<Box mt={2} pt={2} borderTop="1px solid #eee" display="flex" justifyContent="center">
<Typography variant="caption" color="primary" sx={{ fontWeight: 600, cursor: 'pointer', '&:hover': { textDecoration: 'underline' } }}>
View Full Team Activity
</Typography>
</Box>
</Stack>
{events.map((event) => {
const payload = event.payload || {};
return (
<Accordion key={event.id} disableGutters elevation={0} sx={{ border: '1px solid #e5e7eb', mb: 1 }}>
<AccordionSummary expandIcon={<ExpandMoreIcon />}>
<Stack direction="row" spacing={1} alignItems="center" sx={{ width: '100%' }}>
<Chip size="small" label={payload.phase || event.event_type} />
{payload.step && <Chip size="small" variant="outlined" label={payload.step} />}
<Typography variant="body2" sx={{ flexGrow: 1 }}>
{event.message || payload.output_summary || 'Activity update'}
</Typography>
{typeof payload.progress_percent === 'number' && (
<Typography variant="caption" color="text.secondary">{payload.progress_percent}%</Typography>
)}
</Stack>
</AccordionSummary>
<AccordionDetails>
<Typography variant="caption" display="block">Tool: {payload.tool_name || '—'}</Typography>
<Typography variant="caption" display="block">Input: {payload.input_summary || '—'}</Typography>
<Typography variant="caption" display="block">Output: {payload.output_summary || '—'}</Typography>
<Typography variant="caption" display="block">Decision: {payload.decision_reason || '—'}</Typography>
<Typography variant="caption" display="block">Evidence: {(payload.evidence_refs || []).join(', ') || '—'}</Typography>
<Typography variant="caption" display="block">Safe debug: {String(payload.safe_debug ?? true)}</Typography>
</AccordionDetails>
</Accordion>
);
})}
</ListItem>
</React.Fragment>
))}
</List>
)}
</Paper>
);
};