251 lines
7.7 KiB
Python
251 lines
7.7 KiB
Python
"""ObserverAgent - User behavior observation for VoIdea.
|
|
|
|
This agent:
|
|
- Collects user metrics
|
|
- Generates insights
|
|
- Monitors feature usage
|
|
- Reports anomalies
|
|
"""
|
|
|
|
from datetime import datetime, timezone
|
|
from typing import Any
|
|
from uuid import uuid4
|
|
|
|
from sqlalchemy import select, func
|
|
from sqlalchemy.ext.asyncio import AsyncSession
|
|
|
|
from app.agents.base import AgentResult, AgentStatus, AgentTrigger, BaseAgent
|
|
from app.agents.models import Observation
|
|
from app.core.config import get_settings
|
|
|
|
settings = get_settings()
|
|
|
|
|
|
class ObserverAgent(BaseAgent):
|
|
"""User behavior observation agent."""
|
|
|
|
name = "observer_agent"
|
|
version = "1.0.0"
|
|
description = "Monitors user behavior and generates insights"
|
|
triggers = [
|
|
AgentTrigger.MANUAL,
|
|
AgentTrigger.CRON,
|
|
AgentTrigger.EVENT,
|
|
]
|
|
|
|
def __init__(self, session: AsyncSession | None = None):
|
|
super().__init__()
|
|
self._session = session
|
|
self.enabled = settings.observer_enabled
|
|
self.sample_rate = settings.observer_sample_rate
|
|
|
|
async def run(self, context: dict[str, Any] | None = None) -> AgentResult:
|
|
"""Execute observation task.
|
|
|
|
Context can contain:
|
|
- action: str (collect, report, analyze, metrics)
|
|
- observation_type: str
|
|
- user_id: str
|
|
- metric_name: str
|
|
- metric_value: float
|
|
"""
|
|
await self.set_running("observation")
|
|
|
|
try:
|
|
action = context.get("action", "metrics") if context else "metrics"
|
|
|
|
if action == "collect":
|
|
result = await self._collect_observation(context or {})
|
|
elif action == "report":
|
|
result = await self._generate_report(context or {})
|
|
elif action == "analyze":
|
|
result = await self._analyze_trends(context or {})
|
|
else:
|
|
result = await self._get_metrics(context or {})
|
|
|
|
await self.set_idle()
|
|
return result
|
|
|
|
except Exception as e:
|
|
await self.set_error(str(e))
|
|
return AgentResult(
|
|
success=False,
|
|
message=f"ObserverAgent failed: {str(e)}",
|
|
errors=[str(e)],
|
|
)
|
|
|
|
async def health_check(self) -> bool:
|
|
"""Check if ObserverAgent is operational."""
|
|
return True
|
|
|
|
async def _collect_observation(self, context: dict[str, Any]) -> AgentResult:
|
|
"""Collect a single observation."""
|
|
if not self._session:
|
|
return AgentResult(
|
|
success=True,
|
|
message="Session not configured, observation skipped",
|
|
data={"skipped": True},
|
|
)
|
|
|
|
if not self.enabled:
|
|
return AgentResult(
|
|
success=True,
|
|
message="Observer disabled",
|
|
data={"enabled": False},
|
|
)
|
|
|
|
observation_type = context.get("observation_type", "custom")
|
|
user_id = context.get("user_id")
|
|
metric_name = context.get("metric_name", "unknown")
|
|
metric_value = context.get("metric_value", 0.0)
|
|
metadata = context.get("metadata", {})
|
|
session_id = context.get("session_id")
|
|
|
|
observation = Observation(
|
|
id=uuid4(),
|
|
observation_type=observation_type,
|
|
user_id=user_id,
|
|
metric_name=metric_name,
|
|
metric_value=metric_value,
|
|
metadata=metadata,
|
|
session_id=session_id,
|
|
)
|
|
|
|
self._session.add(observation)
|
|
await self._session.commit()
|
|
|
|
return AgentResult(
|
|
success=True,
|
|
message=f"Collected observation: {metric_name}",
|
|
data={
|
|
"id": str(observation.id),
|
|
"type": observation_type,
|
|
"metric": metric_name,
|
|
},
|
|
)
|
|
|
|
async def _generate_report(self, context: dict[str, Any]) -> AgentResult:
|
|
"""Generate daily/weekly observation report."""
|
|
if not self._session:
|
|
return AgentResult(
|
|
success=False,
|
|
message="Session not configured",
|
|
)
|
|
|
|
period = context.get("period", "daily")
|
|
days = 1 if period == "daily" else 7
|
|
|
|
result = await self._session.execute(
|
|
select(
|
|
Observation.observation_type,
|
|
Observation.metric_name,
|
|
func.count(Observation.id).label("count"),
|
|
func.avg(Observation.metric_value).label("avg_value"),
|
|
)
|
|
.where(
|
|
Observation.created_at >= datetime.now(timezone.utc)
|
|
- datetime.timedelta(days=days)
|
|
)
|
|
.group_by(
|
|
Observation.observation_type,
|
|
Observation.metric_name,
|
|
)
|
|
)
|
|
|
|
metrics = result.all()
|
|
|
|
report = {
|
|
"period": period,
|
|
"metrics": [
|
|
{
|
|
"type": m.observation_type,
|
|
"name": m.metric_name,
|
|
"count": m.count,
|
|
"avg_value": float(m.avg_value) if m.avg_value else 0,
|
|
}
|
|
for m in metrics
|
|
],
|
|
"total_observations": sum(m.count for m in metrics),
|
|
}
|
|
|
|
return AgentResult(
|
|
success=True,
|
|
message=f"Generated {period} report",
|
|
data=report,
|
|
)
|
|
|
|
async def _analyze_trends(self, context: dict[str, Any]) -> AgentResult:
|
|
"""Analyze trends in observations."""
|
|
if not self._session:
|
|
return AgentResult(
|
|
success=False,
|
|
message="Session not configured",
|
|
)
|
|
|
|
metric_name = context.get("metric_name")
|
|
|
|
query = (
|
|
select(Observation)
|
|
.where(Observation.metric_name == metric_name)
|
|
.order_by(Observation.created_at.desc())
|
|
.limit(100)
|
|
)
|
|
|
|
result = await self._session.execute(query)
|
|
observations = result.scalars().all()
|
|
|
|
values = [o.metric_value for o in observations]
|
|
avg = sum(values) / len(values) if values else 0
|
|
max_val = max(values) if values else 0
|
|
min_val = min(values) if values else 0
|
|
|
|
return AgentResult(
|
|
success=True,
|
|
message=f"Analyzed {len(observations)} observations for {metric_name}",
|
|
data={
|
|
"metric_name": metric_name,
|
|
"count": len(observations),
|
|
"average": avg,
|
|
"max": max_val,
|
|
"min": min_val,
|
|
"trend": "stable",
|
|
},
|
|
)
|
|
|
|
async def _get_metrics(self, context: dict[str, Any]) -> AgentResult:
|
|
"""Get aggregated metrics."""
|
|
if not self._session:
|
|
return AgentResult(
|
|
success=False,
|
|
message="Session not configured",
|
|
)
|
|
|
|
result = await self._session.execute(
|
|
select(
|
|
func.count(Observation.id).label("total"),
|
|
func.count(func.distinct(Observation.user_id)).label("unique_users"),
|
|
)
|
|
)
|
|
|
|
stats = result.one()
|
|
|
|
return AgentResult(
|
|
success=True,
|
|
message="Retrieved observer metrics",
|
|
data={
|
|
"total_observations": stats.total,
|
|
"unique_users": stats.unique_users or 0,
|
|
"enabled": self.enabled,
|
|
"sample_rate": self.sample_rate,
|
|
},
|
|
)
|
|
|
|
async def get_metrics(self) -> dict[str, Any]:
|
|
"""Get ObserverAgent metrics."""
|
|
return {
|
|
"agent_id": self.name,
|
|
"version": self.version,
|
|
"status": self.status.value,
|
|
"enabled": self.enabled,
|
|
"sample_rate": self.sample_rate,
|
|
} |