204 lines
5.9 KiB
Python
204 lines
5.9 KiB
Python
"""Data aggregation logic for scorecard generation."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import logging
|
|
from datetime import datetime
|
|
from typing import TYPE_CHECKING
|
|
|
|
from dashboard.services.scorecard.types import TRACKED_AGENTS, AgentMetrics
|
|
from dashboard.services.scorecard.validators import (
|
|
extract_actor_from_event,
|
|
is_tracked_agent,
|
|
)
|
|
from infrastructure.events.bus import get_event_bus
|
|
|
|
if TYPE_CHECKING:
|
|
from infrastructure.events.bus import Event
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
def collect_events_for_period(
|
|
start: datetime, end: datetime, agent_id: str | None = None
|
|
) -> list[Event]:
|
|
"""Collect events from the event bus for a time period.
|
|
|
|
Args:
|
|
start: Period start time
|
|
end: Period end time
|
|
agent_id: Optional agent filter
|
|
|
|
Returns:
|
|
List of matching events
|
|
"""
|
|
bus = get_event_bus()
|
|
events: list[Event] = []
|
|
|
|
# Query persisted events for relevant types
|
|
event_types = [
|
|
"gitea.push",
|
|
"gitea.issue.opened",
|
|
"gitea.issue.comment",
|
|
"gitea.pull_request",
|
|
"agent.task.completed",
|
|
"test.execution",
|
|
]
|
|
|
|
for event_type in event_types:
|
|
try:
|
|
type_events = bus.replay(
|
|
event_type=event_type,
|
|
source=agent_id,
|
|
limit=1000,
|
|
)
|
|
events.extend(type_events)
|
|
except Exception as exc:
|
|
logger.debug("Failed to replay events for %s: %s", event_type, exc)
|
|
|
|
# Filter by timestamp
|
|
filtered = []
|
|
for event in events:
|
|
try:
|
|
event_time = datetime.fromisoformat(event.timestamp.replace("Z", "+00:00"))
|
|
if start <= event_time < end:
|
|
filtered.append(event)
|
|
except (ValueError, AttributeError):
|
|
continue
|
|
|
|
return filtered
|
|
|
|
|
|
def aggregate_metrics(events: list[Event]) -> dict[str, AgentMetrics]:
|
|
"""Aggregate metrics from events grouped by agent.
|
|
|
|
Args:
|
|
events: List of events to process
|
|
|
|
Returns:
|
|
Dict mapping agent_id -> AgentMetrics
|
|
"""
|
|
metrics_by_agent: dict[str, AgentMetrics] = {}
|
|
|
|
for event in events:
|
|
actor = extract_actor_from_event(event)
|
|
|
|
# Skip non-agent events unless they explicitly have an agent_id
|
|
if not is_tracked_agent(actor) and "agent_id" not in event.data:
|
|
continue
|
|
|
|
if actor not in metrics_by_agent:
|
|
metrics_by_agent[actor] = AgentMetrics(agent_id=actor)
|
|
|
|
metrics = metrics_by_agent[actor]
|
|
|
|
# Process based on event type
|
|
event_type = event.type
|
|
|
|
if event_type == "gitea.push":
|
|
metrics.commits += event.data.get("num_commits", 1)
|
|
|
|
elif event_type == "gitea.issue.opened":
|
|
issue_num = event.data.get("issue_number", 0)
|
|
if issue_num:
|
|
metrics.issues_touched.add(issue_num)
|
|
|
|
elif event_type == "gitea.issue.comment":
|
|
metrics.comments += 1
|
|
issue_num = event.data.get("issue_number", 0)
|
|
if issue_num:
|
|
metrics.issues_touched.add(issue_num)
|
|
|
|
elif event_type == "gitea.pull_request":
|
|
pr_num = event.data.get("pr_number", 0)
|
|
action = event.data.get("action", "")
|
|
merged = event.data.get("merged", False)
|
|
|
|
if pr_num:
|
|
if action == "opened":
|
|
metrics.prs_opened.add(pr_num)
|
|
elif action == "closed" and merged:
|
|
metrics.prs_merged.add(pr_num)
|
|
# Also count as touched issue for tracking
|
|
metrics.issues_touched.add(pr_num)
|
|
|
|
elif event_type == "agent.task.completed":
|
|
# Extract test files from task data
|
|
affected = event.data.get("tests_affected", [])
|
|
for test in affected:
|
|
metrics.tests_affected.add(test)
|
|
|
|
# Token rewards from task completion
|
|
reward = event.data.get("token_reward", 0)
|
|
if reward:
|
|
metrics.tokens_earned += reward
|
|
|
|
elif event_type == "test.execution":
|
|
# Track test files that were executed
|
|
test_files = event.data.get("test_files", [])
|
|
for test in test_files:
|
|
metrics.tests_affected.add(test)
|
|
|
|
return metrics_by_agent
|
|
|
|
|
|
def query_token_transactions(agent_id: str, start: datetime, end: datetime) -> tuple[int, int]:
|
|
"""Query the lightning ledger for token transactions.
|
|
|
|
Args:
|
|
agent_id: The agent to query for
|
|
start: Period start
|
|
end: Period end
|
|
|
|
Returns:
|
|
Tuple of (tokens_earned, tokens_spent)
|
|
"""
|
|
try:
|
|
from lightning.ledger import get_transactions
|
|
|
|
transactions = get_transactions(limit=1000)
|
|
|
|
earned = 0
|
|
spent = 0
|
|
|
|
for tx in transactions:
|
|
# Filter by agent if specified
|
|
if tx.agent_id and tx.agent_id != agent_id:
|
|
continue
|
|
|
|
# Filter by timestamp
|
|
try:
|
|
tx_time = datetime.fromisoformat(tx.created_at.replace("Z", "+00:00"))
|
|
if not (start <= tx_time < end):
|
|
continue
|
|
except (ValueError, AttributeError):
|
|
continue
|
|
|
|
if tx.tx_type.value == "incoming":
|
|
earned += tx.amount_sats
|
|
else:
|
|
spent += tx.amount_sats
|
|
|
|
return earned, spent
|
|
|
|
except Exception as exc:
|
|
logger.debug("Failed to query token transactions: %s", exc)
|
|
return 0, 0
|
|
|
|
|
|
def ensure_all_tracked_agents(
|
|
metrics_by_agent: dict[str, AgentMetrics],
|
|
) -> dict[str, AgentMetrics]:
|
|
"""Ensure all tracked agents have metrics entries.
|
|
|
|
Args:
|
|
metrics_by_agent: Current metrics dictionary
|
|
|
|
Returns:
|
|
Updated metrics with all tracked agents included
|
|
"""
|
|
for agent_id in TRACKED_AGENTS:
|
|
if agent_id not in metrics_by_agent:
|
|
metrics_by_agent[agent_id] = AgentMetrics(agent_id=agent_id)
|
|
return metrics_by_agent
|