Standardize agent event payloads and team activity timeline UI
This commit is contained in:
@@ -1,13 +1,81 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from dataclasses import asdict, dataclass, field
|
||||||
from datetime import datetime
|
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 sqlalchemy.orm import Session
|
||||||
|
|
||||||
from models.agent_activity_models import AgentAlert, AgentApprovalRequest, AgentEvent, AgentRun
|
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:
|
class AgentActivityService:
|
||||||
def __init__(self, db: Session, user_id: str):
|
def __init__(self, db: Session, user_id: str):
|
||||||
self.db = db
|
self.db = db
|
||||||
@@ -50,10 +118,11 @@ class AgentActivityService:
|
|||||||
event_type: str,
|
event_type: str,
|
||||||
severity: str = "info",
|
severity: str = "info",
|
||||||
message: Optional[str] = None,
|
message: Optional[str] = None,
|
||||||
payload: Optional[Dict[str, Any]] = None,
|
payload: Optional[Union[Dict[str, Any], AgentEventPayload]] = None,
|
||||||
run_id: Optional[int] = None,
|
run_id: Optional[int] = None,
|
||||||
agent_type: Optional[str] = None,
|
agent_type: Optional[str] = None,
|
||||||
) -> AgentEvent:
|
) -> AgentEvent:
|
||||||
|
normalized_payload = _normalize_event_payload(payload)
|
||||||
evt = AgentEvent(
|
evt = AgentEvent(
|
||||||
run_id=run_id,
|
run_id=run_id,
|
||||||
user_id=self.user_id,
|
user_id=self.user_id,
|
||||||
@@ -61,7 +130,7 @@ class AgentActivityService:
|
|||||||
event_type=event_type,
|
event_type=event_type,
|
||||||
severity=severity,
|
severity=severity,
|
||||||
message=message,
|
message=message,
|
||||||
payload=payload,
|
payload=normalized_payload,
|
||||||
created_at=datetime.utcnow(),
|
created_at=datetime.utcnow(),
|
||||||
)
|
)
|
||||||
self.db.add(evt)
|
self.db.add(evt)
|
||||||
|
|||||||
@@ -31,7 +31,7 @@ from utils.logger_utils import get_service_logger
|
|||||||
from services.database import get_session_for_user
|
from services.database import get_session_for_user
|
||||||
from services.intelligence.monitoring.semantic_dashboard import RealTimeSemanticMonitor
|
from services.intelligence.monitoring.semantic_dashboard import RealTimeSemanticMonitor
|
||||||
from services.intelligence.agents.safety_framework import get_safety_framework
|
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
|
from services.intelligence.agents.agent_usage_tracking import track_agent_usage_sync
|
||||||
import time
|
import time
|
||||||
|
|
||||||
@@ -426,7 +426,7 @@ class BaseALwrityAgent(ABC):
|
|||||||
event_type="plan",
|
event_type="plan",
|
||||||
severity="info",
|
severity="info",
|
||||||
message=(prompt[:2000] if prompt else None),
|
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,
|
run_id=run_record.id,
|
||||||
agent_type=self.agent_type,
|
agent_type=self.agent_type,
|
||||||
)
|
)
|
||||||
@@ -453,7 +453,7 @@ class BaseALwrityAgent(ABC):
|
|||||||
event_type="final_summary",
|
event_type="final_summary",
|
||||||
severity="info",
|
severity="info",
|
||||||
message=(str(result)[:2000] if result is not None else None),
|
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,
|
run_id=run_record.id,
|
||||||
agent_type=self.agent_type,
|
agent_type=self.agent_type,
|
||||||
)
|
)
|
||||||
@@ -467,7 +467,7 @@ class BaseALwrityAgent(ABC):
|
|||||||
event_type="error",
|
event_type="error",
|
||||||
severity="error",
|
severity="error",
|
||||||
message=str(e)[:2000],
|
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,
|
run_id=run_record.id,
|
||||||
agent_type=self.agent_type,
|
agent_type=self.agent_type,
|
||||||
)
|
)
|
||||||
@@ -513,7 +513,7 @@ class BaseALwrityAgent(ABC):
|
|||||||
event_type="plan",
|
event_type="plan",
|
||||||
severity="info",
|
severity="info",
|
||||||
message=f"{action.action_type} -> {action.target_resource}",
|
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,
|
run_id=run_record.id,
|
||||||
agent_type=self.agent_type,
|
agent_type=self.agent_type,
|
||||||
)
|
)
|
||||||
@@ -528,7 +528,7 @@ class BaseALwrityAgent(ABC):
|
|||||||
event_type="decision",
|
event_type="decision",
|
||||||
severity="warning",
|
severity="warning",
|
||||||
message="Action failed safety validation",
|
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,
|
run_id=run_record.id,
|
||||||
agent_type=self.agent_type,
|
agent_type=self.agent_type,
|
||||||
)
|
)
|
||||||
@@ -568,7 +568,7 @@ class BaseALwrityAgent(ABC):
|
|||||||
event_type="decision",
|
event_type="decision",
|
||||||
severity="info",
|
severity="info",
|
||||||
message="Action requires approval",
|
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,
|
run_id=run_record.id,
|
||||||
agent_type=self.agent_type,
|
agent_type=self.agent_type,
|
||||||
)
|
)
|
||||||
@@ -593,7 +593,7 @@ class BaseALwrityAgent(ABC):
|
|||||||
event_type="progress",
|
event_type="progress",
|
||||||
severity="info",
|
severity="info",
|
||||||
message="Rollback checkpoint created",
|
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,
|
run_id=run_record.id,
|
||||||
agent_type=self.agent_type,
|
agent_type=self.agent_type,
|
||||||
)
|
)
|
||||||
@@ -604,7 +604,7 @@ class BaseALwrityAgent(ABC):
|
|||||||
event_type="warning",
|
event_type="warning",
|
||||||
severity="warning",
|
severity="warning",
|
||||||
message=str(e)[:2000],
|
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,
|
run_id=run_record.id,
|
||||||
agent_type=self.agent_type,
|
agent_type=self.agent_type,
|
||||||
)
|
)
|
||||||
@@ -639,7 +639,7 @@ class BaseALwrityAgent(ABC):
|
|||||||
event_type="final_summary",
|
event_type="final_summary",
|
||||||
severity="info",
|
severity="info",
|
||||||
message=str(result)[:2000] if result is not None else None,
|
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,
|
run_id=run_record.id,
|
||||||
agent_type=self.agent_type,
|
agent_type=self.agent_type,
|
||||||
)
|
)
|
||||||
@@ -690,7 +690,7 @@ class BaseALwrityAgent(ABC):
|
|||||||
event_type="error",
|
event_type="error",
|
||||||
severity="error",
|
severity="error",
|
||||||
message=str(e)[:2000],
|
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,
|
run_id=run_record.id,
|
||||||
agent_type=self.agent_type,
|
agent_type=self.agent_type,
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ from datetime import datetime
|
|||||||
from loguru import logger
|
from loguru import logger
|
||||||
from ..txtai_service import TxtaiIntelligenceService
|
from ..txtai_service import TxtaiIntelligenceService
|
||||||
from services.intelligence.agents.core_agent_framework import BaseALwrityAgent, AgentAction, TaskProposal
|
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.seo_tools.content_strategy_service import ContentStrategyService
|
||||||
from services.analytics import PlatformAnalyticsService
|
from services.analytics import PlatformAnalyticsService
|
||||||
from services.intelligence.sif_agents import SharedLLMWrapper, LocalLLMWrapper
|
from services.intelligence.sif_agents import SharedLLMWrapper, LocalLLMWrapper
|
||||||
@@ -62,6 +63,41 @@ class SIFBaseAgent(BaseALwrityAgent):
|
|||||||
if kwargs:
|
if kwargs:
|
||||||
logger.debug(f"[{self.__class__.__name__}] Parameters: {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):
|
def _create_txtai_agent(self):
|
||||||
"""
|
"""
|
||||||
SIF agents use the intelligence service directly, but we can expose
|
SIF agents use the intelligence service directly, but we can expose
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ from sqlalchemy.orm import Session
|
|||||||
|
|
||||||
from models.daily_workflow_models import DailyWorkflowPlan, DailyWorkflowTask
|
from models.daily_workflow_models import DailyWorkflowPlan, DailyWorkflowTask
|
||||||
from models.agent_activity_models import AgentAlert
|
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 services.llm_providers.main_text_generation import llm_text_gen
|
||||||
from loguru import logger
|
from loguru import logger
|
||||||
|
|
||||||
@@ -430,7 +430,7 @@ async def generate_agent_enhanced_plan(db: Session, user_id: str, date: str) ->
|
|||||||
event_type="plan",
|
event_type="plan",
|
||||||
severity="info",
|
severity="info",
|
||||||
message="Building grounded daily workflow plan",
|
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,
|
run_id=run.id,
|
||||||
agent_type="TodayWorkflowGenerator",
|
agent_type="TodayWorkflowGenerator",
|
||||||
)
|
)
|
||||||
@@ -449,7 +449,7 @@ async def generate_agent_enhanced_plan(db: Session, user_id: str, date: str) ->
|
|||||||
event_type="warning",
|
event_type="warning",
|
||||||
severity="warning",
|
severity="warning",
|
||||||
message=str(e)[:2000],
|
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,
|
run_id=run.id,
|
||||||
agent_type="TodayWorkflowGenerator",
|
agent_type="TodayWorkflowGenerator",
|
||||||
)
|
)
|
||||||
@@ -467,7 +467,7 @@ async def generate_agent_enhanced_plan(db: Session, user_id: str, date: str) ->
|
|||||||
event_type="final_summary",
|
event_type="final_summary",
|
||||||
severity="info",
|
severity="info",
|
||||||
message="Daily workflow plan generated",
|
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,
|
run_id=run.id,
|
||||||
agent_type="TodayWorkflowGenerator",
|
agent_type="TodayWorkflowGenerator",
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -3,202 +3,161 @@ import {
|
|||||||
Box,
|
Box,
|
||||||
Paper,
|
Paper,
|
||||||
Typography,
|
Typography,
|
||||||
Avatar,
|
|
||||||
AvatarGroup,
|
|
||||||
Chip,
|
Chip,
|
||||||
List,
|
List,
|
||||||
ListItem,
|
ListItem,
|
||||||
ListItemAvatar,
|
|
||||||
ListItemText,
|
|
||||||
Divider,
|
Divider,
|
||||||
IconButton,
|
IconButton,
|
||||||
Tooltip
|
Tooltip,
|
||||||
|
CircularProgress,
|
||||||
|
Accordion,
|
||||||
|
AccordionSummary,
|
||||||
|
AccordionDetails,
|
||||||
|
Stack,
|
||||||
} from '@mui/material';
|
} from '@mui/material';
|
||||||
import {
|
import {
|
||||||
Psychology as StrategyIcon,
|
|
||||||
Article as ContentIcon,
|
|
||||||
Search as SeoIcon,
|
|
||||||
Campaign as SocialIcon,
|
|
||||||
CompareArrows as CompetitorIcon,
|
|
||||||
Refresh as RefreshIcon,
|
Refresh as RefreshIcon,
|
||||||
MoreVert as MoreVertIcon
|
ExpandMore as ExpandMoreIcon,
|
||||||
} from '@mui/icons-material';
|
} from '@mui/icons-material';
|
||||||
|
import { apiClient } from '../../../api/client';
|
||||||
|
|
||||||
interface AgentStatus {
|
type EventPayload = {
|
||||||
id: string;
|
phase?: string | null;
|
||||||
name: string;
|
step?: string | null;
|
||||||
role: string;
|
tool_name?: string | null;
|
||||||
status: 'active' | 'thinking' | 'idle' | 'offline';
|
progress_percent?: number | null;
|
||||||
current_activity: string;
|
input_summary?: string | null;
|
||||||
icon: React.ElementType;
|
output_summary?: string | null;
|
||||||
color: string;
|
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
|
type TeamActivityEvent = {
|
||||||
// /api/agents/status or similar
|
id: number;
|
||||||
const AGENT_TEAM: AgentStatus[] = [
|
event_type: string;
|
||||||
{
|
severity: string;
|
||||||
id: 'strategy_architect',
|
message?: string | null;
|
||||||
name: 'Strategy Architect',
|
created_at?: string | null;
|
||||||
role: 'Team Lead',
|
payload?: EventPayload | null;
|
||||||
status: 'active',
|
};
|
||||||
current_activity: 'Analyzing content pillar performance',
|
|
||||||
icon: StrategyIcon,
|
type AgentRun = {
|
||||||
color: '#6366f1' // Indigo
|
id: number;
|
||||||
},
|
agent_type: string;
|
||||||
{
|
status: string;
|
||||||
id: 'content_strategist',
|
started_at?: string | null;
|
||||||
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
|
|
||||||
}
|
|
||||||
];
|
|
||||||
|
|
||||||
const TeamHuddleWidget: React.FC = () => {
|
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 (
|
return (
|
||||||
<Paper
|
<Paper elevation={0} sx={{ p: 2, borderRadius: 3, border: '1px solid', borderColor: 'divider', height: '100%' }}>
|
||||||
elevation={0}
|
|
||||||
sx={{
|
|
||||||
p: 2,
|
|
||||||
borderRadius: 3,
|
|
||||||
border: '1px solid',
|
|
||||||
borderColor: 'divider',
|
|
||||||
height: '100%',
|
|
||||||
background: 'linear-gradient(180deg, #ffffff 0%, #f8fafc 100%)'
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Box display="flex" justifyContent="space-between" alignItems="center" mb={2}>
|
<Box display="flex" justifyContent="space-between" alignItems="center" mb={2}>
|
||||||
<Box display="flex" alignItems="center" gap={1}>
|
<Box display="flex" alignItems="center" gap={1}>
|
||||||
<Typography variant="h6" fontWeight={700} color="text.primary">
|
<Typography variant="h6" fontWeight={700}>Team Activity</Typography>
|
||||||
Team Huddle
|
<Chip label="Live" size="small" color="success" sx={{ height: 20, fontSize: '0.65rem', fontWeight: 700 }} />
|
||||||
</Typography>
|
|
||||||
<Chip
|
|
||||||
label="Live"
|
|
||||||
size="small"
|
|
||||||
color="success"
|
|
||||||
sx={{ height: 20, fontSize: '0.65rem', fontWeight: 700 }}
|
|
||||||
/>
|
|
||||||
</Box>
|
</Box>
|
||||||
<Box>
|
<Tooltip title="Refresh Team Activity">
|
||||||
<Tooltip title="Refresh Team Status">
|
<IconButton size="small" onClick={loadTimeline}>
|
||||||
<IconButton size="small">
|
|
||||||
<RefreshIcon fontSize="small" />
|
<RefreshIcon fontSize="small" />
|
||||||
</IconButton>
|
</IconButton>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
</Box>
|
</Box>
|
||||||
</Box>
|
|
||||||
|
|
||||||
<List disablePadding>
|
{loading && (
|
||||||
{AGENT_TEAM.map((agent, index) => (
|
<Box py={4} textAlign="center">
|
||||||
<React.Fragment key={agent.id}>
|
<CircularProgress size={24} />
|
||||||
{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>
|
</Box>
|
||||||
}
|
)}
|
||||||
secondary={
|
|
||||||
<Typography
|
{!loading && error && (
|
||||||
variant="body2"
|
<Typography variant="body2" color="error">{error}</Typography>
|
||||||
color="text.secondary"
|
)}
|
||||||
sx={{
|
|
||||||
display: '-webkit-box',
|
{!loading && !error && timeline.length === 0 && (
|
||||||
WebkitLineClamp: 1,
|
<Typography variant="body2" color="text.secondary">No team activity yet.</Typography>
|
||||||
WebkitBoxOrient: 'vertical',
|
)}
|
||||||
overflow: 'hidden',
|
|
||||||
fontSize: '0.75rem',
|
{!loading && !error && timeline.length > 0 && (
|
||||||
mt: 0.25
|
<List disablePadding>
|
||||||
}}
|
{timeline.map(({ run, events }, index) => (
|
||||||
>
|
<React.Fragment key={run.id}>
|
||||||
{agent.current_activity}
|
{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>
|
</Typography>
|
||||||
}
|
</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>
|
</ListItem>
|
||||||
</React.Fragment>
|
</React.Fragment>
|
||||||
))}
|
))}
|
||||||
</List>
|
</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>
|
|
||||||
</Paper>
|
</Paper>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user