From a7bf355703ea6f2867fe36401f67e2119430ce37 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D9=8A?= Date: Mon, 2 Mar 2026 22:01:12 +0530 Subject: [PATCH] Standardize agent event payloads and team activity timeline UI --- backend/services/agent_activity_service.py | 75 ++++- .../agents/core_agent_framework.py | 22 +- .../intelligence/agents/specialized_agents.py | 36 ++ backend/services/today_workflow_service.py | 8 +- .../components/TeamHuddleWidget.tsx | 311 ++++++++---------- 5 files changed, 258 insertions(+), 194 deletions(-) diff --git a/backend/services/agent_activity_service.py b/backend/services/agent_activity_service.py index 70cbdcb0..79699bfd 100644 --- a/backend/services/agent_activity_service.py +++ b/backend/services/agent_activity_service.py @@ -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) diff --git a/backend/services/intelligence/agents/core_agent_framework.py b/backend/services/intelligence/agents/core_agent_framework.py index cba44f77..e7cf6920 100644 --- a/backend/services/intelligence/agents/core_agent_framework.py +++ b/backend/services/intelligence/agents/core_agent_framework.py @@ -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, ) diff --git a/backend/services/intelligence/agents/specialized_agents.py b/backend/services/intelligence/agents/specialized_agents.py index 333799b0..6bc4e360 100644 --- a/backend/services/intelligence/agents/specialized_agents.py +++ b/backend/services/intelligence/agents/specialized_agents.py @@ -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 diff --git a/backend/services/today_workflow_service.py b/backend/services/today_workflow_service.py index e027bc92..06b6bd27 100644 --- a/backend/services/today_workflow_service.py +++ b/backend/services/today_workflow_service.py @@ -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", ) diff --git a/frontend/src/components/MainDashboard/components/TeamHuddleWidget.tsx b/frontend/src/components/MainDashboard/components/TeamHuddleWidget.tsx index 79bd709d..278f9bd2 100644 --- a/frontend/src/components/MainDashboard/components/TeamHuddleWidget.tsx +++ b/frontend/src/components/MainDashboard/components/TeamHuddleWidget.tsx @@ -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; +}; -// 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(null); + const [timeline, setTimeline] = React.useState>([]); + + 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 ( - + - - Team Huddle - - - - - - - - - + Team Activity + + + + + + - - {AGENT_TEAM.map((agent, index) => ( - - {index > 0 && } - - - - } - > - - - - - - - - {agent.name} - - - {agent.role} - - - } - secondary={ - - {agent.current_activity} + {loading && ( + + + + )} + + {!loading && error && ( + {error} + )} + + {!loading && !error && timeline.length === 0 && ( + No team activity yet. + )} + + {!loading && !error && timeline.length > 0 && ( + + {timeline.map(({ run, events }, index) => ( + + {index > 0 && } + + + + + + {run.started_at ? new Date(run.started_at).toLocaleString() : ''} - } - /> - - - ))} - - - - - View Full Team Activity - - + + + {events.map((event) => { + const payload = event.payload || {}; + return ( + + }> + + + {payload.step && } + + {event.message || payload.output_summary || 'Activity update'} + + {typeof payload.progress_percent === 'number' && ( + {payload.progress_percent}% + )} + + + + Tool: {payload.tool_name || '—'} + Input: {payload.input_summary || '—'} + Output: {payload.output_summary || '—'} + Decision: {payload.decision_reason || '—'} + Evidence: {(payload.evidence_refs || []).join(', ') || '—'} + Safe debug: {String(payload.safe_debug ?? true)} + + + ); + })} + + + ))} + + )} ); };