fix(heartbeat): inject shared current time context into phase 1

This commit is contained in:
Xubin Ren
2026-03-16 02:47:45 +00:00
committed by Xubin Ren
parent 0dda2b23e6
commit 5d1528a5f3
4 changed files with 17 additions and 25 deletions

View File

@@ -3,11 +3,11 @@
import base64 import base64
import mimetypes import mimetypes
import platform import platform
import time
from datetime import datetime
from pathlib import Path from pathlib import Path
from typing import Any from typing import Any
from nanobot.utils.helpers import current_time_str
from nanobot.agent.memory import MemoryStore from nanobot.agent.memory import MemoryStore
from nanobot.agent.skills import SkillsLoader from nanobot.agent.skills import SkillsLoader
from nanobot.utils.helpers import build_assistant_message, detect_image_mime from nanobot.utils.helpers import build_assistant_message, detect_image_mime
@@ -99,9 +99,7 @@ Reply directly with text for conversations. Only use the 'message' tool to send
@staticmethod @staticmethod
def _build_runtime_context(channel: str | None, chat_id: str | None) -> str: def _build_runtime_context(channel: str | None, chat_id: str | None) -> str:
"""Build untrusted runtime metadata block for injection before the user message.""" """Build untrusted runtime metadata block for injection before the user message."""
now = datetime.now().strftime("%Y-%m-%d %H:%M (%A)") lines = [f"Current Time: {current_time_str()}"]
tz = time.strftime("%Z") or "UTC"
lines = [f"Current Time: {now} ({tz})"]
if channel and chat_id: if channel and chat_id:
lines += [f"Channel: {channel}", f"Chat ID: {chat_id}"] lines += [f"Channel: {channel}", f"Chat ID: {chat_id}"]
return ContextBuilder._RUNTIME_CONTEXT_TAG + "\n" + "\n".join(lines) return ContextBuilder._RUNTIME_CONTEXT_TAG + "\n" + "\n".join(lines)

View File

@@ -3,7 +3,6 @@
from __future__ import annotations from __future__ import annotations
import asyncio import asyncio
from datetime import datetime, timezone
from pathlib import Path from pathlib import Path
from typing import TYPE_CHECKING, Any, Callable, Coroutine from typing import TYPE_CHECKING, Any, Callable, Coroutine
@@ -88,19 +87,13 @@ class HeartbeatService:
Returns (action, tasks) where action is 'skip' or 'run'. Returns (action, tasks) where action is 'skip' or 'run'.
""" """
now_str = datetime.now(timezone.utc).strftime("%Y-%m-%d %H:%M UTC") from nanobot.utils.helpers import current_time_str
response = await self.provider.chat_with_retry( response = await self.provider.chat_with_retry(
messages=[ messages=[
{"role": "system", "content": ( {"role": "system", "content": "You are a heartbeat agent. Call the heartbeat tool to report your decision."},
"You are a heartbeat agent. Call the heartbeat tool to report your decision. "
"The current date/time is provided so you can evaluate time-based conditions. "
"Choose 'run' if there are active tasks to execute. "
"Choose 'skip' if the file has no actionable tasks, if blocking conditions "
"are not yet met, or if tasks are scheduled for a future time that has not arrived yet."
)},
{"role": "user", "content": ( {"role": "user", "content": (
f"Current date/time: {now_str}\n\n" f"Current Time: {current_time_str()}\n\n"
"Review the following HEARTBEAT.md and decide whether there are active tasks.\n\n" "Review the following HEARTBEAT.md and decide whether there are active tasks.\n\n"
f"{content}" f"{content}"
)}, )},

View File

@@ -2,6 +2,7 @@
import json import json
import re import re
import time
from datetime import datetime from datetime import datetime
from pathlib import Path from pathlib import Path
from typing import Any from typing import Any
@@ -33,6 +34,13 @@ def timestamp() -> str:
return datetime.now().isoformat() return datetime.now().isoformat()
def current_time_str() -> str:
"""Human-readable current time with weekday and timezone, e.g. '2026-03-15 22:30 (Saturday) (CST)'."""
now = datetime.now().strftime("%Y-%m-%d %H:%M (%A)")
tz = time.strftime("%Z") or "UTC"
return f"{now} ({tz})"
_UNSAFE_CHARS = re.compile(r'[<>:"/\\|?*]') _UNSAFE_CHARS = re.compile(r'[<>:"/\\|?*]')
def safe_filename(name: str) -> str: def safe_filename(name: str) -> str:

View File

@@ -253,8 +253,8 @@ async def test_decide_retries_transient_error_then_succeeds(tmp_path, monkeypatc
@pytest.mark.asyncio @pytest.mark.asyncio
async def test_decide_prompt_includes_current_datetime(tmp_path) -> None: async def test_decide_prompt_includes_current_time(tmp_path) -> None:
"""Phase 1 prompt must contain the current date/time so the LLM can judge task urgency.""" """Phase 1 user prompt must contain current time so the LLM can judge task urgency."""
captured_messages: list[dict] = [] captured_messages: list[dict] = []
@@ -283,14 +283,7 @@ async def test_decide_prompt_includes_current_datetime(tmp_path) -> None:
await service._decide("- [ ] check servers at 10:00 UTC") await service._decide("- [ ] check servers at 10:00 UTC")
# System prompt should mention date/time awareness
system_msg = captured_messages[0]
assert system_msg["role"] == "system"
assert "date/time" in system_msg["content"].lower()
# User prompt should contain a UTC timestamp
user_msg = captured_messages[1] user_msg = captured_messages[1]
assert user_msg["role"] == "user" assert user_msg["role"] == "user"
assert "Current date/time:" in user_msg["content"] assert "Current Time:" in user_msg["content"]
assert "UTC" in user_msg["content"]