Initial commit: VoIdeaAI - voice-first AI idea assistant
This commit is contained in:
@@ -0,0 +1,251 @@
|
||||
"""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,
|
||||
}
|
||||
Reference in New Issue
Block a user