Add API usage tracking and dynamic task reloading
Features: - Usage tracking system (usage_tracker.py) - Tracks input/output tokens per API call - Calculates costs with support for cache pricing - Stores data in usage_data.json (gitignored) - Integrated into llm_interface.py - Dynamic task scheduler reloading - Auto-detects YAML changes every 60s - No restart needed for new tasks - reload_tasks() method for manual refresh - Example cost tracking scheduled task - Daily API usage report - Budget tracking ($5/month target) - Disabled by default in scheduled_tasks.yaml Improvements: - Fixed tool_use/tool_result pair splitting bug (CRITICAL) - Added thread safety to agent.chat() - Fixed N+1 query problem in hybrid search - Optimized database batch queries - Added conversation history pruning (50 messages max) Updated .gitignore: - Exclude user profiles (memory_workspace/users/*.md) - Exclude usage data (usage_data.json) - Exclude vector index (vectors.usearch) Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -79,6 +79,9 @@ class TaskScheduler:
|
||||
Callable[[ScheduledTask, str], None]
|
||||
] = None
|
||||
|
||||
# Track file modification time for auto-reload
|
||||
self._last_mtime: Optional[float] = None
|
||||
|
||||
self._load_tasks()
|
||||
|
||||
def _load_tasks(self) -> None:
|
||||
@@ -87,9 +90,13 @@ class TaskScheduler:
|
||||
self._create_default_config()
|
||||
return
|
||||
|
||||
# Track file modification time
|
||||
self._last_mtime = self.config_file.stat().st_mtime
|
||||
|
||||
with open(self.config_file) as f:
|
||||
config = yaml.safe_load(f) or {}
|
||||
|
||||
self.tasks.clear() # Clear existing tasks before reload
|
||||
for task_config in config.get("tasks", []):
|
||||
task = ScheduledTask(
|
||||
name=task_config["name"],
|
||||
@@ -187,17 +194,45 @@ class TaskScheduler:
|
||||
hour, minute = map(int, parts[2].split(":"))
|
||||
|
||||
days_ahead = target_day - now.weekday()
|
||||
if days_ahead <= 0:
|
||||
if days_ahead < 0:
|
||||
days_ahead += 7
|
||||
|
||||
next_run = now + timedelta(days=days_ahead)
|
||||
next_run = next_run.replace(
|
||||
hour=hour, minute=minute, second=0, microsecond=0
|
||||
)
|
||||
# If same day but time already passed, advance to next week
|
||||
if next_run <= now:
|
||||
next_run += timedelta(days=7)
|
||||
return next_run
|
||||
|
||||
raise ValueError(f"Unknown schedule format: {schedule}")
|
||||
|
||||
def reload_tasks(self) -> bool:
|
||||
"""Reload tasks from config file if it has changed.
|
||||
|
||||
Returns:
|
||||
True if tasks were reloaded, False if no changes detected.
|
||||
"""
|
||||
if not self.config_file.exists():
|
||||
return False
|
||||
|
||||
current_mtime = self.config_file.stat().st_mtime
|
||||
if self._last_mtime is not None and current_mtime == self._last_mtime:
|
||||
return False
|
||||
|
||||
print(f"[Scheduler] Config file changed, reloading tasks...")
|
||||
self._load_tasks()
|
||||
|
||||
if self.running:
|
||||
print("[Scheduler] Updated task schedule:")
|
||||
for task in self.tasks:
|
||||
if task.enabled and task.next_run:
|
||||
formatted = task.next_run.strftime("%Y-%m-%d %H:%M")
|
||||
print(f" - {task.name}: next run at {formatted}")
|
||||
|
||||
return True
|
||||
|
||||
def add_adapter(self, platform: str, adapter: Any) -> None:
|
||||
"""Register an adapter for sending task outputs."""
|
||||
self.adapters[platform] = adapter
|
||||
@@ -233,6 +268,9 @@ class TaskScheduler:
|
||||
"""Main scheduler loop (runs in background thread)."""
|
||||
while self.running:
|
||||
try:
|
||||
# Auto-reload tasks if config file changed
|
||||
self.reload_tasks()
|
||||
|
||||
now = datetime.now()
|
||||
|
||||
for task in self.tasks:
|
||||
@@ -269,7 +307,11 @@ class TaskScheduler:
|
||||
threading.Event().wait(_SCHEDULER_POLL_INTERVAL)
|
||||
|
||||
def _execute_task(self, task: ScheduledTask) -> None:
|
||||
"""Execute a single task using the Agent."""
|
||||
"""Execute a single task using the Agent.
|
||||
|
||||
Note: agent.chat() is thread-safe (uses internal lock), so this
|
||||
can safely run from the scheduler's background thread.
|
||||
"""
|
||||
try:
|
||||
print(f"[Scheduler] Running: {task.name}")
|
||||
|
||||
@@ -282,7 +324,19 @@ class TaskScheduler:
|
||||
print(f" Response: {response[:100]}...")
|
||||
|
||||
if task.send_to_platform and task.send_to_channel:
|
||||
asyncio.run(self._send_to_platform(task, response))
|
||||
# Use the running event loop if available, otherwise create one.
|
||||
# asyncio.run() fails if an event loop is already running
|
||||
# (which it is when the bot is active).
|
||||
try:
|
||||
loop = asyncio.get_running_loop()
|
||||
# Schedule on the existing loop from this background thread
|
||||
future = asyncio.run_coroutine_threadsafe(
|
||||
self._send_to_platform(task, response), loop
|
||||
)
|
||||
future.result(timeout=30) # Wait up to 30s
|
||||
except RuntimeError:
|
||||
# No running loop (e.g., standalone test mode)
|
||||
asyncio.run(self._send_to_platform(task, response))
|
||||
|
||||
if self.on_task_complete:
|
||||
self.on_task_complete(task, response)
|
||||
|
||||
Reference in New Issue
Block a user