Add files via upload

This commit is contained in:
FatihMakes authored and GitHub committed 2026-07-23 16:57:15 +03:00
1 parent 215ff44d3d
commit b4d6ae9bc7
7 files changed
+581 -154

No files matched your search

+159
View File
@@ -0,0 +1,159 @@
"""
BackgroundMonitor — user-configured topic watching.
Checks DDG news once per day per topic; alerts JARVIS when a new headline appears.
No crypto, no finance, no uninvited tracking.
"""
import hashlib
import json
import re
from datetime import datetime
from pathlib import Path
# ── Blocked categories (never monitor regardless of what user says) ────────────
_BLOCKED = {
# Marka / varlık adları — her dilde aynı yazılır
"bitcoin", "ethereum", "dogecoin", "solana", "binance",
"nft", "blockchain", "defi", "altcoin", "memecoin", "coin", "token",
# "kripto" kökünün farklı dillerdeki yazılışları
"crypto", "kripto", "cripto", "krypto", "крипто", "仮想通貨", "暗号資産",
"cryptocurrency",
}
def _is_blocked(topic: str) -> bool:
t = topic.lower()
return any(word in t for word in _BLOCKED)
# ── Slug / hash helpers ────────────────────────────────────────────────────────
def _slug(topic: str) -> str:
return re.sub(r"[^a-z0-9]+", "_", topic.lower().strip())[:40].strip("_")
def _title_hash(title: str) -> str:
return hashlib.md5(title.encode("utf-8", errors="ignore")).hexdigest()[:12]
# ── Memory I/O ─────────────────────────────────────────────────────────────────
def _load() -> dict:
from memory.memory_manager import load_memory
data = load_memory().get("monitors", {})
return data if isinstance(data, dict) else {}
def _save(monitors: dict) -> None:
from memory.memory_manager import load_memory, MEMORY_PATH, _lock
memory = load_memory()
memory["monitors"] = monitors
with _lock:
MEMORY_PATH.parent.mkdir(parents=True, exist_ok=True)
MEMORY_PATH.write_text(
json.dumps(memory, indent=2, ensure_ascii=False),
encoding="utf-8",
)
# ── Public API ─────────────────────────────────────────────────────────────────
def add_monitor(topic: str) -> str:
topic = topic.strip()
if not topic:
return "Please specify a topic to monitor."
if _is_blocked(topic):
return "I don't monitor crypto or financial topics."
monitors = _load()
slug = _slug(topic)
if slug in monitors:
return f"Already monitoring: {monitors[slug]['topic']}"
monitors[slug] = {
"topic": topic,
"added": datetime.now().strftime("%Y-%m-%d"),
"last_check": "",
"last_hash": "",
}
_save(monitors)
print(f"[Monitor] ➕ Added: {topic}")
return f"Now monitoring: {topic}"
def remove_monitor(topic: str) -> str:
topic = topic.strip().lower()
monitors = _load()
# exact slug match first
slug = _slug(topic)
if slug in monitors:
label = monitors.pop(slug)["topic"]
_save(monitors)
return f"Stopped monitoring: {label}"
# partial match fallback
for key, val in list(monitors.items()):
if topic in val.get("topic", "").lower():
label = monitors.pop(key)["topic"]
_save(monitors)
return f"Stopped monitoring: {label}"
return f"Not found in monitored topics: {topic}"
def list_monitors() -> list[str]:
return [v.get("topic", k) for k, v in _load().items()]
def check_all() -> list[str]:
"""
Run all pending topic checks (once per day per topic).
Returns a list of [MONITOR_ALERT] strings — empty if nothing new.
"""
from actions.web_search import _ddg_news
monitors = _load()
if not monitors:
return []
today = datetime.now().strftime("%Y-%m-%d")
alerts = []
changed = False
for slug, data in monitors.items():
if data.get("last_check") == today:
continue # already checked today
topic = data.get("topic", slug)
try:
results = _ddg_news(topic, max_results=5)
if not results:
monitors[slug]["last_check"] = today
changed = True
continue
top = results[0]
title = top.get("title", "").strip()
if not title:
continue
h = _title_hash(title)
monitors[slug]["last_check"] = today
changed = True
if h == data.get("last_hash"):
continue # same headline as last check — no alert
monitors[slug]["last_hash"] = h
snippet = top.get("snippet", "")[:150]
source = top.get("source", "")
parts = [f"[MONITOR_ALERT] {topic}", f"Headline: {title}"]
if snippet:
parts.append(snippet)
if source:
parts.append(f"Source: {source}")
alerts.append("\n".join(parts))
print(f"[Monitor] 🔔 New headline for '{topic}': {title[:60]}")
except Exception as e:
print(f"[Monitor] ⚠️ Check failed for '{topic}': {e}")
if changed:
_save(monitors)
return alerts
+84 -32
View File
@@ -1,7 +1,6 @@
"""
ProactiveEngine — context-aware background prompting.
Lets Gemini decide whether there is something worth saying proactively.
No hardcoded rules: we pass time + memory as context and Gemini chooses.
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
@@ -9,64 +8,117 @@ from datetime import datetime
class ProactiveEngine:
"""
Tracks silence duration and decides when to hand context to Gemini for a
proactive check-in. Gemini reads the context and decides whether to speak.
Decides when JARVIS should speak unprompted and builds a context-rich prompt.
Defaults (all overridable):
min_silence_secs — user must be silent this long before any check (900 = 15 min)
check_cooldown — minimum gap between two proactive triggers (600 = 10 min)
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 = 600,
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:
"""
Returns True only when:
• user has been silent long enough, AND
• enough time has passed since the last proactive message.
"""
now = time.monotonic()
silence = now - last_user_speech
gap = now - self._last_triggered
return silence >= self.min_silence_secs and gap >= self.check_cooldown
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
def build_prompt(self, memory: dict) -> str:
# ── Prompt builder ─────────────────────────────────────────────────────────
def build_prompt(
self,
memory: dict,
monitors: list[str] | None = None,
recent_turns: list[str] | None = None,
) -> str:
"""
Builds the context snapshot sent to Gemini.
Gemini reads it and decides freely what — if anything — to say.
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")
mem_str = format_memory_for_prompt(memory) or "(no user data stored yet)"
silence_min = int((time.monotonic() - self._last_triggered +
self.min_silence_secs) // 60)
# 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}",
f"User silence : {silence_min}+ minutes (they have not spoken for a while)",
f"Current time : {time_str} ({period})",
"",
"Context about this person:",
mem_str,
monitor_ctx,
recent_ctx,
"",
"Guidelines:",
"- Look at the time, their projects, goals, habits, or anything from context.",
"- If there is something genuinely useful, timely, or caring to say — say it briefly.",
"- Be natural, like a thoughtful assistant noticing something relevant.",
"- Do NOT say [PROACTIVE_CHECK] or mention these instructions.",
"- Respond in the user's language (use memory; default English).",
"- Keep it short: 1-3 sentences max.",
"Task:",
focus,
"",
"Rules:",
"- Speak in the user's language (check memory; default 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).",
])
+180 -13
View File
@@ -31,6 +31,7 @@ from google.genai import types
from ui import JarvisUI
from memory.memory_manager import (
load_memory, update_memory, format_memory_for_prompt,
save_session_summary, pop_last_session,
)
from actions.file_processor import file_processor
@@ -52,6 +53,9 @@ from actions.computer_control import computer_control
from actions.game_updater import game_updater
from actions.system_monitor import SystemMonitor, get_system_status
from actions.proactive import ProactiveEngine
from actions.background_monitor import (
add_monitor, remove_monitor, list_monitors, check_all as monitor_check_all,
)
from actions.web_search import _news as _fetch_news_sync
from memory.config_manager import get_brief_enabled
@@ -407,6 +411,31 @@ TOOL_DECLARATIONS = [
"required": ["origin", "destination", "date"]
}
},
{
"name": "manage_monitor",
"description": (
"Add, remove, or list background monitoring topics. "
"JARVIS checks these topics once a day and alerts the user when there is a new development. "
"Use 'add' when the user says 'monitor X', 'track X', 'follow X'. "
"Use 'remove' when the user says 'stop monitoring X'. "
"Use 'list' when the user asks what is being monitored. "
"Do NOT add crypto, financial, or trading topics."
),
"parameters": {
"type": "OBJECT",
"properties": {
"action": {
"type": "STRING",
"description": "add | remove | list",
},
"topic": {
"type": "STRING",
"description": "Topic to monitor or stop monitoring (e.g. 'space exploration', 'AI news')",
},
},
"required": ["action"],
},
},
{
"name": "shutdown_jarvis",
"description": (
@@ -548,6 +577,7 @@ class JarvisLive:
self._sys_monitor = SystemMonitor() # persistent cooldown state
self._proactive = ProactiveEngine()
self._last_user_speech = time.monotonic() # updated on every user utterance
self._session_log: list[str] = [] # conversation turns for end-of-session summary
def _make_remote_key(self):
"""Called from Qt main thread when user presses Remote Control."""
@@ -811,14 +841,35 @@ class JarvisLive:
r = await loop.run_in_executor(None, get_system_status)
result = str(r)
elif name == "manage_monitor":
action = args.get("action", "").lower().strip()
topic = args.get("topic", "").strip()
if action == "add" and topic:
result = await asyncio.to_thread(add_monitor, topic)
elif action == "remove" and topic:
result = await asyncio.to_thread(remove_monitor, topic)
elif action == "list":
topics = await asyncio.to_thread(list_monitors)
result = ("Monitoring: " + ", ".join(topics)) if topics else "No topics are being monitored."
else:
result = "Specify action (add/remove/list) and a topic."
elif name == "shutdown_jarvis":
self.ui.write_log("SYS: Shutdown requested.")
self.speak("Goodbye, sir.")
def _shutdown():
import time, os
time.sleep(1)
os._exit(0)
threading.Thread(target=_shutdown, daemon=True).start()
async def _do_shutdown():
await self._save_session_summary()
if self.session:
try:
await self.session.send_client_content(
turns={"parts": [{"text": "Say a brief natural goodbye to the user."}]},
turn_complete=True,
)
except Exception:
pass
await asyncio.sleep(1.5)
import os as _os
_os._exit(0)
asyncio.create_task(_do_shutdown())
else:
result = f"Unknown tool: {name}"
@@ -921,6 +972,7 @@ class JarvisLive:
full_in = " ".join(in_buf).strip()
if full_in:
self.ui.write_log(f"You: {full_in}")
self._session_log.append(f"User: {full_in}")
if self._dashboard:
asyncio.create_task(self._dashboard.broadcast({
"type": "log", "speaker": "user",
@@ -932,6 +984,7 @@ class JarvisLive:
full_out = " ".join(out_buf).strip()
if full_out:
self.ui.write_log(f"{self._asst_name}: {full_out}")
self._session_log.append(f"{self._asst_name}: {full_out}")
if self._dashboard:
asyncio.create_task(self._dashboard.broadcast({
"type": "log", "speaker": "jarvis",
@@ -1012,9 +1065,21 @@ class JarvisLive:
self.set_speaking(False)
self._turn_done_event.clear()
continue
self.set_speaking(True)
# Batch all immediately-available chunks into one write to reduce
# thread-pool round-trips (was one asyncio.to_thread per 50ms slice).
# Cap at ~200 ms so interrupt() still stops audio within ~200 ms.
batch = bytearray(chunk)
while len(batch) < 9600: # 9600 bytes ≈ 200 ms at 24 kHz / 16-bit mono
try:
await asyncio.to_thread(stream.write, chunk)
batch.extend(self.audio_in_queue.get_nowait())
except asyncio.QueueEmpty:
break
try:
await asyncio.to_thread(stream.write, bytes(batch))
except (RuntimeError, asyncio.CancelledError):
break # executor shutting down — exit cleanly
except Exception as e:
@@ -1058,9 +1123,23 @@ class JarvisLive:
# ── Phase 1: instant greeting ─────────────────────────────────────────
lang_clause = f" Respond in {lang}." if lang else ""
name_clause = f" Address the user as {name}." if name else ""
# Inject last session context if available — pop removes it so it's never repeated
last = await asyncio.to_thread(pop_last_session)
session_clause = ""
if last:
try:
_delta = (datetime.now() - datetime.strptime(last["date"], "%Y-%m-%d")).days
_when = "earlier today" if _delta == 0 else ("yesterday" if _delta == 1 else f"{_delta} days ago")
except Exception:
_when = "last time"
session_clause = (
f" Also briefly and naturally mention that {_when}: {last['summary']}"
)
p1 = (
f"Greet the user, mention it is {time_str}, and say you are fetching today's news now. "
f"One short sentence only. Do not call any tools.{lang_clause}{name_clause}"
f"Greet the user warmly, mention it is {time_str}, and say you are fetching today's news now.{session_clause} "
f"Keep it to 2 short sentences max. Do not call any tools.{lang_clause}{name_clause}"
)
# Clear the turn-done event so we can wait for Phase 1 to finish
@@ -1089,8 +1168,13 @@ class JarvisLive:
except asyncio.TimeoutError:
pass
# If turn_complete didn't fire (timeout), give a small buffer
if not turn_waited:
# Extra buffer: turn_complete fires when Gemini finishes *generating*
# Phase 1, but audio may still be playing. Waiting a beat here
# prevents Phase 2 audio from arriving while Phase 1 is mid-sentence
# (which sounds like a "repeated first response" to the user).
if turn_waited:
await asyncio.sleep(0.8)
else:
await asyncio.sleep(1.0)
try:
@@ -1127,6 +1211,40 @@ class JarvisLive:
asyncio.create_task(_deliver_news())
# ── Session memory ──────────────────────────────────────────────────────────
async def _save_session_summary(self) -> None:
"""Summarise the current session in 1-2 sentences and save to long_term.json."""
log = self._session_log
if len(log) < 3: # need at least one exchange to be worth saving
return
self._session_log = [] # reset immediately so the next session starts clean
memory = load_memory()
lang_entry = memory.get("identity", {}).get("language", {})
lang = (lang_entry.get("value", "") if isinstance(lang_entry, dict) else str(lang_entry)).strip()
lang = lang or "English"
convo = "\n".join(log[-40:]) # cap at last 40 turns to stay within token budget
prompt = (
f"Summarize this conversation in 1-2 sentences in {lang}. "
"Focus on what the user accomplished or discussed. "
"Output ONLY the summary text, nothing else:\n\n" + convo
)
try:
from google import genai as _genai
client = _genai.Client(api_key=_get_api_key())
resp = await asyncio.to_thread(
client.models.generate_content,
model="gemini-2.5-flash",
contents=prompt,
)
summary = (resp.text or "").strip()
if summary:
save_session_summary(summary, lang)
except Exception as e:
print(f"[Memory] ⚠️ Session summary failed: {e}")
# ── System monitor ──────────────────────────────────────────────────────────
async def _run_system_monitor(self) -> None:
@@ -1134,7 +1252,13 @@ class JarvisLive:
while True:
await asyncio.sleep(10)
alert = await asyncio.to_thread(self._sys_monitor.check)
if alert and self.session:
if not alert or not self.session:
continue
# Don't interrupt an active conversation
with self._speaking_lock:
speaking = self._is_speaking
if speaking or (time.monotonic() - self._last_user_speech) < 10:
continue
try:
await self.session.send_client_content(
turns={"parts": [{"text": alert}]},
@@ -1143,6 +1267,39 @@ class JarvisLive:
except Exception as e:
print(f"[Monitor] ⚠️ Could not send alert: {e}")
# ── Background monitor ──────────────────────────────────────────────────────
async def _run_background_monitor(self) -> None:
"""Check user-configured topics once per day; speak alerts when new headlines appear."""
await asyncio.sleep(300) # wait 5 min after startup before first check
while True:
if self.session:
# Don't interrupt if user spoke recently or JARVIS is mid-sentence
with self._speaking_lock:
speaking = self._is_speaking
recent_speech = (time.monotonic() - self._last_user_speech) < 30
if not speaking and not recent_speech:
try:
alerts = await asyncio.to_thread(monitor_check_all)
memory = load_memory()
lang_e = memory.get("identity", {}).get("language", {})
lang = (lang_e.get("value", "") if isinstance(lang_e, dict) else str(lang_e)).strip() or "English"
for alert in alerts:
msg = (
f"{alert}\n\n"
f"Inform the user about this development naturally in {lang}. "
"One brief sentence only."
)
await self.session.send_client_content(
turns={"parts": [{"text": msg}]},
turn_complete=True,
)
self.ui.write_log(f"SYS: Monitor alert sent.")
await asyncio.sleep(6) # gap between consecutive alerts
except Exception as e:
print(f"[Monitor] ⚠️ Background check error: {e}")
await asyncio.sleep(1800) # check every 30 minutes
# ── Proactive mode ──────────────────────────────────────────────────────────
async def _run_proactive_mode(self) -> None:
@@ -1169,7 +1326,13 @@ class JarvisLive:
try:
memory = await asyncio.to_thread(load_memory)
prompt = self._proactive.build_prompt(memory)
monitors = await asyncio.to_thread(list_monitors)
recent_turns = self._session_log[-8:] if self._session_log else []
prompt = self._proactive.build_prompt(
memory = memory,
monitors = monitors or None,
recent_turns = recent_turns or None,
)
await self.session.send_client_content(
turns={"parts": [{"text": prompt}]},
turn_complete=True,
@@ -1290,6 +1453,7 @@ class JarvisLive:
tg.create_task(self._receive_audio())
tg.create_task(self._play_audio())
tg.create_task(self._run_system_monitor())
tg.create_task(self._run_background_monitor())
tg.create_task(self._run_proactive_mode())
if self._dashboard:
tg.create_task(self._relay_phone_audio())
@@ -1340,6 +1504,9 @@ class JarvisLive:
self._conn_backoff = 3
finally:
self.session = None
# Only save if there was a real conversation (≥3 turns)
if len(self._session_log) >= 3:
asyncio.create_task(self._save_session_summary())
self.set_speaking(False)
self.ui.set_state("SLEEPING")
+56
View File
@@ -213,3 +213,59 @@ def forget(key: str, category: str = "notes") -> str:
forget_memory = forget
# ── Session memory ─────────────────────────────────────────────────────────────
_SESSION_MAX = 3 # safety cap — in practice 0-1 entries after pop
def save_session_summary(summary: str, language: str = "") -> None:
"""Append a 1-2 sentence session summary to long_term.json['sessions']."""
summary = (summary or "").strip()
if not summary:
return
memory = load_memory()
sessions = memory.get("sessions", [])
if not isinstance(sessions, list):
sessions = []
entry: dict = {
"date": datetime.now().strftime("%Y-%m-%d"),
"summary": summary[:280],
}
if language:
entry["language"] = language
sessions.append(entry)
memory["sessions"] = sessions[-_SESSION_MAX:]
with _lock:
MEMORY_PATH.parent.mkdir(parents=True, exist_ok=True)
MEMORY_PATH.write_text(
json.dumps(memory, indent=2, ensure_ascii=False),
encoding="utf-8",
)
print(f"[Memory] 📝 Session saved ({entry['date']}): {summary[:60]}…")
def pop_last_session() -> dict | None:
"""
Return AND remove the most recent session entry.
Calling this consumes the entry so it is never repeated in future briefings.
"""
with _lock:
if not MEMORY_PATH.exists():
return None
try:
memory = json.loads(MEMORY_PATH.read_text(encoding="utf-8"))
sessions = memory.get("sessions", [])
if not isinstance(sessions, list) or not sessions:
return None
entry = sessions.pop() # remove the last entry
memory["sessions"] = sessions
MEMORY_PATH.write_text(
json.dumps(memory, indent=2, ensure_ascii=False),
encoding="utf-8",
)
return entry
except Exception as e:
print(f"[Memory] ⚠️ pop_last_session error: {e}")
return None
+47 -26
View File
@@ -1,7 +1,7 @@
# ⚙️ MARK XLIX
# ⚙️ MARK L (50)
### The Ultimate Cross-Platform Personal AI Assistant — By FatihMakes
> 📺 **[Watch the full setup video on YouTube](https://youtu.be/CiGdcIlnXb8))**
> 📺 **[Watch the full setup video on YouTube](https://www.youtube.com/@FatihMakes)**
A real-time voice AI that can hear, see, understand, and control your computer — on any OS. Supports Windows, macOS, and Linux. Built on the Gemini Live API for native audio streaming, delivering zero subscriptions and total digital autonomy.
@@ -9,7 +9,9 @@ A real-time voice AI that can hear, see, understand, and control your computer
## ✨ Overview
MARK XLIX deepens the personal assistant foundation. Rather than adding more tools, this build focused on making the assistant truly *yours*: it starts with your computer, learns your name, and pays attention to what you're doing. The goal before the plugin era begins is a core that feels alive — not just reactive.
MARK L is where the assistant stops being a tool and starts being a presence. It remembers yesterday's conversation, watches the topics you care about, and speaks first when it has something worth saying. The goal of this build was continuity — JARVIS should feel like it never fully left, even after you close it.
It's not just an assistant — it's an extension of your digital life.
---
@@ -24,8 +26,10 @@ MARK XLIX deepens the personal assistant foundation. Rather than adding more too
| 👁️ Visual Awareness | Real-time screen capture and webcam vision piped into your main Gemini session |
| 🧠 Persistent Memory | Deeply remembers projects, preferences, and personal context across sessions |
| ⌨️ Hybrid Input | Seamlessly switch between keyboard typing and voice commands |
| 🌅 Morning Briefing | On first boot: greets you, reads the time, fetches live news headlines, and checks weather |
| 🔔 Proactive Check-ins | After 15 minutes of silence, checks context and offers something genuinely useful |
| 🌅 Morning Briefing | On first boot: greets you, reads the time, recaps yesterday, and fetches live news |
| 🔔 Proactive 2.0 | Time-aware, context-aware check-ins — knows the time of day, your projects, and what you've been discussing |
| 🗓️ Session Memory | Summarises each conversation and mentions it naturally next morning — consumed after use, never repeats |
| 👁️‍🗨️ Background Monitoring | User-configured topic watching — checks for new headlines once a day and alerts naturally |
| 📊 Hardware Monitoring | Continuous CPU, RAM, GPU and temperature telemetry with localized voice alerts |
| 🌤️ Weather Report | Live weather data for your city, personalized from memory |
| 🗺️ Dynamic Content Panel | Scrollable display layer beneath the HUD that renders web results, news, and search data |
@@ -39,28 +43,42 @@ MARK XLIX deepens the personal assistant foundation. Rather than adding more too
| 📨 Send Message | Compose and send messages through WhatsApp, Telegram, and more |
| 🎬 YouTube Control | Search, play, and control YouTube playback by voice |
| 🖱️ Desktop Control | Taskbar, window management, and desktop-level operations |
| 🧑‍💻 Silent Language Memory | Detects spoken language on first use and saves it — all future sessions adapt automatically |
| 🧑‍💻 Silent Language Memory | Detects spoken language on first use — all future sessions adapt automatically |
| 📱 Remote Dashboard | Control the assistant from your phone via QR code pairing |
| ⚡ Auto-Start on Boot | Registers with the OS startup system (registry / LaunchAgent / .desktop) |
| 📋 Clipboard Intelligence | Copy any text → floating panel with Translate / Summarise / Explain / Fix |
| 🎨 Assistant Customization | Change the assistant name and your name from the UI — takes effect immediately |
---
## 🆕 What's New in XLIX
## 🆕 What's New in Mark L
### ⚡ Auto-Start on Boot
The assistant now registers itself with the operating system's startup system. One click in the UI toggles it on or off. On Windows, it writes to the registry using `pythonw.exe` so no console window ever appears. On macOS it installs a LaunchAgent plist; on Linux a `.desktop` autostart entry. The button reflects the current state every time the app launches.
### 🗓️ Session Memory — JARVIS Remembers Yesterday
At the end of every session, JARVIS generates a 1-2 sentence summary of what was discussed and saves it to memory. The next morning, it's mentioned naturally in the briefing:
> *"Good morning, sir — it's 09:15. Yesterday you were working on the Mark L background monitoring feature. Fetching today's headlines now."*
### 🎨 Assistant Customization
The assistant is no longer locked to the name "JARVIS". Click `⚙ CUSTOMISE ASSISTANT` in the right panel to change:
- **Assistant name** — displayed everywhere in the UI (title bar, header, HUD, log, footer) and injected into the Gemini system prompt so the AI knows its own name
- **Your name** — how the assistant addresses you. Leave blank for the default language-aware addressing (`sir` / `efendim`), or set your actual name for a more personal feel
The summary is consumed immediately after use — it never repeats in future briefings and adds zero long-term bloat to memory.
Changes take effect immediately without restarting.
### 👁️‍🗨️ Background Monitoring — JARVIS Watches While You're Away
Tell JARVIS to monitor any topic and it checks for new developments once a day using DuckDuckGo news. When a headline changes, it reports back naturally in your language:
> *"Efendim, takip ettiğiniz yapay zeka haberlerinde bir gelişme var: Google yeni bir model duyurdu."*
### 📋 Clipboard Intelligence
Copy any text of 10 or more characters and a floating panel appears at the bottom of the window. Four quick actions — **TRANSLATE**, **SUMMARISE**, **EXPLAIN**, **FIX** — send the copied content directly to the assistant with one click. The panel auto-dismisses after 8 seconds. This turns the clipboard into a silent command channel for anything on your screen.
Fully opt-in — JARVIS monitors nothing without being explicitly asked. Crypto, financial, and trading topics are blocked at the code level regardless of what is requested. Same headline never triggers twice.
### ☀ Morning Brief Toggle + Speed Optimization
The morning briefing can now be turned on or off with one click from the settings drawer (`⚙` → `☀ MORNING BRIEF: ON/OFF`). Users who don't want a startup briefing can disable it permanently; the setting survives restarts. The briefing itself was also re-engineered: news is now pre-fetched in a background thread the moment the session starts, running in parallel while the greeting plays. By the time the greeting finishes, the results are already ready — no extra Gemini tool-call round-trip needed. Briefing delivery is noticeably faster as a result.
### 🔔 Proactive System 2.0 — Context-Aware, Time-Aware, Non-Repetitive
The proactive engine was rebuilt from the ground up. Instead of a generic check-in after 15 minutes of silence, JARVIS now:
- Knows the **time of day** — morning tone differs from evening tone
- Knows your **active projects** from memory and can ask how something is going
- Knows your **monitored topics** and can bring one up naturally
- Knows **what you were just talking about** (last 8 conversation turns)
- **Rotates** between three focus areas so it never opens with the same line twice
- Has a 20-minute cooldown (up from 10) — less intrusive, more meaningful
### 👁️ Instant Vision Acknowledgment — No More Silent Waiting
When you ask JARVIS to look at your screen or camera, it no longer goes silent while processing. It immediately says something natural ("Looking at your screen now, sir" / "Ekrana bakıyorum efendim") while the capture runs. The actual analysis follows as the next response.
### 📰 Parallel News Search — First Result Wins
News queries now run Gemini Grounded Search and DuckDuckGo news simultaneously in two threads. Whichever delivers a valid result first is used; the other is silently discarded. A Gemini 503 error no longer delays results — the DDG fallback is already running in parallel.
---
@@ -70,16 +88,16 @@ The morning briefing can now be turned on or off with one click from the setting
|---|---|
| **XLVIII** | Instant interrupt · parallel news · two-phase briefing · exponential backoff · vision cooldown |
| **XLIX** | Auto-start · clipboard intelligence · assistant customization |
| **L** | Wake word · proactive system 2.0 · session memory / daily continuity |
| **LI+** | Plugin system · email · quiz mode · calorie counter · and more |
| **L** | Session memory · background monitoring · proactive 2.0 · instant vision · parallel news search |
| **LI+** | Plugin system · email · quiz mode · calorie counter · calendar |
---
## ⚡ Quick Start
```bash
git clone https://github.com/FatihMakes/Mark-XLIX.git
cd Mark-XLIX
git clone https://github.com/FatihMakes/Mark-L.git
cd Mark-L
pip install -r requirements.txt
python main.py
```
@@ -102,13 +120,15 @@ python main.py
## 🗂️ Project Structure
```
Mark XLIX/
Mark L/
├── main.py # Core loop — Gemini Live session, audio I/O, tool dispatch
├── ui.py # PyQt6 HUD — waveform, log panel, interrupt button, camera feed
├── setup.py # First-run configuration wizard
├── actions/
│ ├── web_search.py # Gemini + DDG parallel search (news, research, price, compare)
│ ├── screen_processor.py # Screen capture & webcam vision via Gemini Live
│ ├── background_monitor.py # User-configured topic watching — daily DDG check, no crypto
│ ├── proactive.py # Proactive 2.0 — time/context/rotation-aware check-ins
│ ├── reminder.py # OS-native scheduled notifications
│ ├── system_monitor.py # CPU / RAM / GPU / temperature telemetry
│ ├── computer_settings.py # Volume, brightness, WiFi, power
@@ -124,9 +144,10 @@ Mark XLIX/
│ ├── game_updater.py # Game update management (Steam / Epic)
│ ├── code_helper.py # Code review and generation
│ ├── dev_agent.py # Developer task agent
│ ├── desktop.py # Desktop and taskbar control
│ └── proactive.py # Proactive silence-break suggestions
├── memory/ # Persistent key-value memory store
│ └── desktop.py # Desktop and taskbar control
├── memory/
│ ├── memory_manager.py # Load/save long_term.json — sessions, monitors, identity
│ └── long_term.json # Persistent store: identity, preferences, projects, sessions, monitors
├── core/
│ └── prompt.txt # Assistant personality and tool-routing rules
└── config/
+1 -1
View File
@@ -22,5 +22,5 @@ if platform.system() == "Windows":
f' "{sys.executable}" "{postinstall}" -install\n'
)
print("\n✅ Setup complete! Run 'python main.py' to start MARK XXV.")
print("\n✅ Setup complete! Run 'python main.py' to start MARK L.")
+54 -82
View File
@@ -2076,6 +2076,57 @@ class MainWindow(QMainWindow):
print(f"[Shortcut] ⚠️ Icon generation failed: {e}")
return False
@staticmethod
def _create_lnk_windows(lnk: str, target: str, args: str,
work_dir: str, icon_loc: str) -> None:
"""
Create a Windows .lnk shortcut WITHOUT launching PowerShell or cmd.
Tries win32com (pywin32) first; falls back to wscript.exe + VBScript.
wscript.exe is a GUI-mode host — it never opens a console window.
"""
# ── Option 1: pywin32 (pure Python COM, zero subprocess) ──────────
try:
from win32com.client import Dispatch # type: ignore
sh = Dispatch("WScript.Shell")
sc = sh.CreateShortCut(lnk)
sc.TargetPath = target
sc.Arguments = f'"{args}"'
sc.WorkingDirectory = work_dir
sc.Description = "J.A.R.V.I.S AI Assistant"
sc.IconLocation = icon_loc
sc.save()
return
except ImportError:
pass
# ── Option 2: wscript.exe + VBScript (always available on Windows,
# GUI-mode executable — never opens a console window) ────────────
vbs = "\n".join([
'Set ws = CreateObject("WScript.Shell")',
f'Set sc = ws.CreateShortcut("{lnk}")',
f'sc.TargetPath = "{target}"',
f'sc.Arguments = Chr(34) & "{args}" & Chr(34)',
f'sc.WorkingDirectory = "{work_dir}"',
'sc.Description = "J.A.R.V.I.S AI Assistant"',
f'sc.IconLocation = "{icon_loc}"',
'sc.Save',
])
import tempfile
fd, tmp = tempfile.mkstemp(suffix=".vbs")
try:
with os.fdopen(fd, "w", encoding="utf-8") as f:
f.write(vbs)
proc = subprocess.Popen(
["wscript.exe", "/nologo", tmp],
creationflags=subprocess.DETACHED_PROCESS | subprocess.CREATE_NO_WINDOW,
)
proc.wait(timeout=10)
finally:
try:
os.unlink(tmp)
except Exception:
pass
@staticmethod
def _get_desktop_dir() -> Path:
"""
@@ -2157,72 +2208,6 @@ class MainWindow(QMainWindow):
# display-only). Everything else lands here as a last resort.
return home / "Desktop"
@staticmethod
def _create_lnk_windows(lnk: str, target: str, args: str,
work_dir: str, icon_loc: str) -> None:
"""
Create a Windows .lnk shortcut WITHOUT launching PowerShell or cmd.
Tries win32com (pywin32) first; falls back to wscript.exe + VBScript.
wscript.exe is a GUI-mode host — it never opens a console window.
Raises on failure so the caller can log a useful error.
"""
# ── Option 1: pywin32 (pure Python COM, zero subprocess) ──────────
com_err: Exception | None = None
try:
from win32com.client import Dispatch # type: ignore
sh = Dispatch("WScript.Shell")
sc = sh.CreateShortCut(lnk)
sc.TargetPath = target
sc.Arguments = f'"{args}"'
sc.WorkingDirectory = work_dir
sc.Description = "J.A.R.V.I.S AI Assistant"
sc.IconLocation = icon_loc
sc.save()
return
except ImportError:
pass
except Exception as e: # COM error — still try VBScript
com_err = e
# ── Option 2: wscript.exe + VBScript (always available on Windows,
# GUI-mode executable — never opens a console window) ────────────
def q(s: str) -> str: # escape for a VBScript string literal
return s.replace('"', '""')
vbs = "\n".join([
'On Error Resume Next',
'Set ws = CreateObject("WScript.Shell")',
f'Set sc = ws.CreateShortcut("{q(lnk)}")',
f'sc.TargetPath = "{q(target)}"',
f'sc.Arguments = Chr(34) & "{q(args)}" & Chr(34)',
f'sc.WorkingDirectory = "{q(work_dir)}"',
'sc.Description = "J.A.R.V.I.S AI Assistant"',
f'sc.IconLocation = "{q(icon_loc)}"',
'sc.Save',
'If Err.Number <> 0 Then WScript.Quit 1',
])
import tempfile
fd, tmp = tempfile.mkstemp(suffix=".vbs")
try:
with os.fdopen(fd, "w", encoding="utf-8") as f:
f.write(vbs)
proc = subprocess.Popen(
["wscript.exe", "/nologo", tmp],
creationflags=subprocess.DETACHED_PROCESS | subprocess.CREATE_NO_WINDOW,
)
proc.wait(timeout=10)
finally:
try:
os.unlink(tmp)
except Exception:
pass
if not Path(lnk).exists():
raise RuntimeError(
f"could not create '{lnk}'"
+ (f" ({com_err})" if com_err else "")
)
def _create_desktop_shortcut(self):
"""
Create a desktop shortcut on Windows / macOS / Linux.
@@ -2240,7 +2225,6 @@ class MainWindow(QMainWindow):
try:
_os = platform.system()
desktop.mkdir(parents=True, exist_ok=True)
# ── Windows ───────────────────────────────────────────────────────
if _os == "Windows":
@@ -2321,7 +2305,7 @@ class MainWindow(QMainWindow):
desk.write_text(
"[Desktop Entry]\n"
"Name=J.A.R.V.I.S\n"
f'Exec="{python}" "{script}"\n'
f"Exec={python} {script}\n"
f"Path={script.parent}\n"
"Type=Application\n"
"Terminal=false\n"
@@ -2329,22 +2313,10 @@ class MainWindow(QMainWindow):
+ icon_line
)
desk.chmod(desk.stat().st_mode | 0o755)
# GNOME refuses to launch desktop files until they are
# marked trusted ("Allow Launching") — do it automatically.
try:
subprocess.run(
["gio", "set", str(desk),
"metadata::trusted", "true"],
capture_output=True, timeout=5,
)
except Exception:
pass # non-GNOME desktops don't need (or have) gio
self._log.append_log(f"SYS: Desktop shortcut created in '{desktop}'.")
self._log.append_log("SYS: Desktop shortcut created.")
except Exception as e:
self._log.append_log(
f"ERR: Shortcut failed — {e} (desktop dir: '{desktop}')"
)
self._log.append_log(f"ERR: Shortcut failed — {e}")
def _toggle_fullscreen(self):
if self.isFullScreen():