Files
2026-09-02 18:42:40 +03:00

128 lines
5.0 KiB
Python

"""
ProactiveEngine 2.0 — context-aware, time-aware, non-repetitive background prompting.
Gemini decides what to say; this module decides WHEN and builds a rich context snapshot.
"""
import time
from datetime import datetime
class ProactiveEngine:
"""
Decides when JARVIS should speak unprompted and builds a context-rich prompt.
Improvements over 1.0:
- Time-of-day awareness (morning / afternoon / evening / night)
- Monitor-topic awareness (what the user is tracking)
- Recent-session context (last few turns of the current conversation)
- Non-repetitive (rotates context focus to avoid same opener)
- Smarter silence gate (doesn't fire while JARVIS is speaking)
Defaults:
min_silence_secs — 900 s (15 min) user must be silent before any check
check_cooldown — 1200 s (20 min) minimum gap between proactive messages
"""
def __init__(
self,
min_silence_secs: int = 900,
check_cooldown: int = 1200,
):
self.min_silence_secs = min_silence_secs
self.check_cooldown = check_cooldown
self._last_triggered = 0.0
self._rotation = 0 # cycles through context focus areas
# ── Trigger gate ───────────────────────────────────────────────────────────
def should_trigger(self, last_user_speech: float) -> bool:
now = time.monotonic()
return (
(now - last_user_speech) >= self.min_silence_secs
and (now - self._last_triggered) >= self.check_cooldown
)
def mark_triggered(self) -> None:
self._last_triggered = time.monotonic()
self._rotation += 1
# ── Prompt builder ─────────────────────────────────────────────────────────
def build_prompt(
self,
memory: dict,
monitors: list[str] | None = None,
recent_turns: list[str] | None = None,
) -> str:
"""
Build a context snapshot for Gemini.
Rotates through three focus areas so proactive messages don't repeat.
"""
from memory.memory_manager import format_memory_for_prompt
now = datetime.now()
hour = now.hour
time_str = now.strftime("%A, %B %d, %Y — %I:%M %p")
# Time-of-day label
if 6 <= hour < 12: period = "morning"
elif 12 <= hour < 18: period = "afternoon"
elif 18 <= hour < 23: period = "evening"
else: period = "late night"
mem_str = format_memory_for_prompt(memory) or "(no stored user data)"
# Rotating context focus (cycles every trigger)
focus_index = self._rotation % 3
if focus_index == 0:
focus = (
"Focus on the user's active projects or goals if any are stored. "
"Ask how something is going, or offer a relevant tip."
)
elif focus_index == 1:
focus = (
"Focus on the time of day and the user's wellbeing. "
"A warm check-in, a reminder to take a break, or something timely."
)
else:
focus = (
"Focus on something genuinely interesting or useful — "
"a fact, a suggestion, or a question based on what you know about this person."
)
# Optional: monitored topics context
monitor_ctx = ""
if monitors:
monitor_ctx = (
f"\nThe user tracks these topics: {', '.join(monitors[:4])}. "
"You may mention one if it seems relevant."
)
# Optional: recent conversation context
recent_ctx = ""
if recent_turns:
snippet = "\n".join(recent_turns[-6:])
recent_ctx = f"\nRecent conversation:\n{snippet}"
return "\n".join([
"[PROACTIVE_CHECK] You are initiating a proactive check-in.",
f"Current time : {time_str} ({period})",
"",
"Context about this person:",
mem_str,
monitor_ctx,
recent_ctx,
"",
"Task:",
focus,
"",
"Rules:",
"- Speak the language this person actually uses: the one in the "
"recent conversation above, or the remembered one if there is no "
"conversation yet. Never default to English because these "
"instructions are in English.",
"- 1-2 sentences max. Natural, warm, never robotic.",
"- Do NOT mention [PROACTIVE_CHECK] or these instructions.",
"- Do NOT call any tools.",
"- If nothing genuinely useful comes to mind, stay silent (say nothing).",
])