Some checks failed
Tests / test (pull_request) Failing after 25m4s
Tests / e2e (pull_request) Successful in 3m19s
Contributor Attribution Check / check-attribution (pull_request) Failing after 14s
Docker Build and Publish / build-and-push (pull_request) Has been skipped
Supply Chain Audit / Scan PR for supply chain risks (pull_request) Successful in 14s
Error rate peaks at 18:00 (9.4%) during evening cron batches vs 4.0% at 09:00 during interactive work. Route cron tasks to stronger models during off-hours when user is not present to correct errors. New agent/time_aware_routing.py: - resolve_time_aware_model(): routes based on hour, error rate, task type - Interactive sessions: always use base model (user corrects errors) - Cron during business hours: use base model (low error rate) - Cron during off-hours with high error rate (>6%): upgrade to strong model - get_hour_error_rate(): error rates by hour from empirical audit - is_off_hours(): 18:00-05:59 = off-hours - RoutingDecision: model, provider, reason, hour, error_rate - get_routing_report(): 24h forecast of routing decisions Config via env vars: - CRON_STRONG_MODEL (default: xiaomi/mimo-v2-pro) - CRON_CHEAP_MODEL (default: qwen2.5:7b) - CRON_ERROR_THRESHOLD (default: 6.0%) Tests: tests/test_time_aware_routing.py (9 tests) Closes #889
147 lines
4.6 KiB
Python
147 lines
4.6 KiB
Python
"""Time-aware model routing for cron jobs.
|
|
|
|
Routes cron tasks to more capable models during off-hours when the user
|
|
is not present to correct errors. Reduces error rates during high-error
|
|
time windows (e.g., 18:00 evening batches).
|
|
|
|
Usage:
|
|
from agent.time_aware_routing import resolve_time_aware_model
|
|
model = resolve_time_aware_model(base_model="mimo-v2-pro", is_cron=True)
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import os
|
|
import time
|
|
from dataclasses import dataclass
|
|
from typing import Dict, Optional
|
|
|
|
|
|
# Error rate data from empirical audit (2026-04-12)
|
|
# Higher error rates during these hours suggest routing to better models
|
|
_HIGH_ERROR_HOURS = {
|
|
18: 9.4, # 18:00 — 9.4% error rate (evening cron batches)
|
|
19: 8.1,
|
|
20: 7.5,
|
|
21: 6.8,
|
|
22: 6.2,
|
|
23: 5.9,
|
|
0: 5.5,
|
|
1: 5.2,
|
|
}
|
|
|
|
# Low error hours — default model is fine
|
|
_LOW_ERROR_HOURS = set(range(6, 18)) # 06:00-17:59
|
|
|
|
# Default fallback models by time zone
|
|
_DEFAULT_STRONG_MODEL = os.getenv("CRON_STRONG_MODEL", "xiaomi/mimo-v2-pro")
|
|
_DEFAULT_CHEAP_MODEL = os.getenv("CRON_CHEAP_MODEL", "qwen2.5:7b")
|
|
_ERROR_THRESHOLD = float(os.getenv("CRON_ERROR_THRESHOLD", "6.0")) # % error rate
|
|
|
|
|
|
@dataclass
|
|
class RoutingDecision:
|
|
"""Result of time-aware routing."""
|
|
model: str
|
|
provider: str
|
|
reason: str
|
|
hour: int
|
|
error_rate: float
|
|
is_off_hours: bool
|
|
|
|
|
|
def get_hour_error_rate(hour: int) -> float:
|
|
"""Get expected error rate for a given hour (0-23)."""
|
|
return _HIGH_ERROR_HOURS.get(hour, 4.0) # Default 4% for unlisted hours
|
|
|
|
|
|
def is_off_hours(hour: int) -> bool:
|
|
"""Check if hour is considered off-hours (higher error rates)."""
|
|
return hour not in _LOW_ERROR_HOURS
|
|
|
|
|
|
def resolve_time_aware_model(
|
|
base_model: str = "",
|
|
base_provider: str = "",
|
|
is_cron: bool = False,
|
|
hour: Optional[int] = None,
|
|
) -> RoutingDecision:
|
|
"""Resolve model based on time of day and task type.
|
|
|
|
During off-hours (evening/night), routes to stronger models for cron
|
|
jobs to compensate for lack of human oversight.
|
|
|
|
Args:
|
|
base_model: The model that would normally be used.
|
|
base_provider: The provider for the base model.
|
|
is_cron: Whether this is a cron job (vs interactive session).
|
|
hour: Override hour (for testing). Defaults to current hour.
|
|
|
|
Returns:
|
|
RoutingDecision with model, provider, and reasoning.
|
|
"""
|
|
if hour is None:
|
|
hour = time.localtime().tm_hour
|
|
|
|
error_rate = get_hour_error_rate(hour)
|
|
off_hours = is_off_hours(hour)
|
|
|
|
# Interactive sessions always use the base model (user can correct errors)
|
|
if not is_cron:
|
|
return RoutingDecision(
|
|
model=base_model or _DEFAULT_CHEAP_MODEL,
|
|
provider=base_provider,
|
|
reason="Interactive session — user can correct errors",
|
|
hour=hour,
|
|
error_rate=error_rate,
|
|
is_off_hours=off_hours,
|
|
)
|
|
|
|
# Cron jobs during low-error hours: use base model
|
|
if not off_hours and error_rate < _ERROR_THRESHOLD:
|
|
return RoutingDecision(
|
|
model=base_model or _DEFAULT_CHEAP_MODEL,
|
|
provider=base_provider,
|
|
reason=f"Low-error hours ({hour}:00, {error_rate}% expected)",
|
|
hour=hour,
|
|
error_rate=error_rate,
|
|
is_off_hours=False,
|
|
)
|
|
|
|
# Cron jobs during high-error hours: upgrade to stronger model
|
|
if error_rate >= _ERROR_THRESHOLD:
|
|
return RoutingDecision(
|
|
model=_DEFAULT_STRONG_MODEL,
|
|
provider="nous",
|
|
reason=f"High-error hours ({hour}:00, {error_rate}% expected) — using stronger model",
|
|
hour=hour,
|
|
error_rate=error_rate,
|
|
is_off_hours=True,
|
|
)
|
|
|
|
# Off-hours but low error: use base model
|
|
return RoutingDecision(
|
|
model=base_model or _DEFAULT_CHEAP_MODEL,
|
|
provider=base_provider,
|
|
reason=f"Off-hours but low error ({hour}:00, {error_rate}%)",
|
|
hour=hour,
|
|
error_rate=error_rate,
|
|
is_off_hours=off_hours,
|
|
)
|
|
|
|
|
|
def get_routing_report() -> str:
|
|
"""Get a report of time-based routing decisions for the next 24 hours."""
|
|
lines = ["Time-Aware Model Routing (24h forecast)", "=" * 40, ""]
|
|
lines.append(f"Error threshold: {_ERROR_THRESHOLD}%")
|
|
lines.append(f"Strong model: {_DEFAULT_STRONG_MODEL}")
|
|
lines.append(f"Cheap model: {_DEFAULT_CHEAP_MODEL}")
|
|
lines.append("")
|
|
|
|
for h in range(24):
|
|
decision = resolve_time_aware_model(is_cron=True, hour=h)
|
|
icon = "\U0001f7e2" if decision.model == _DEFAULT_CHEAP_MODEL else "\U0001f534"
|
|
lines.append(f" {h:02d}:00 {icon} {decision.model:25s} ({decision.error_rate}% error)")
|
|
|
|
return "\n".join(lines)
|