Files
voidea/app/agents/observer_agent.py
T

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,
}