Add files via upload
This commit is contained in:
1 parent
b599082b22
commit
c780b2ef21
16 files changed
+1324
No files matched your search
@@ -0,0 +1,45 @@
|
||||
# 🤖 Jarvis Mark X — Local AI Desktop Assistant
|
||||
|
||||
Jarvis Mark X is a **local, modular AI desktop assistant** inspired by Jarvis and built from scratch using **Python**.
|
||||
|
||||
It allows you to control your computer with voice commands and an intent → action system.
|
||||
|
||||
## 🚀 Features
|
||||
- Open applications
|
||||
- Send messages (WhatsApp)
|
||||
- Web search
|
||||
- Weather reports
|
||||
- Temporary & long-term memory
|
||||
- Chatting with good memory
|
||||
|
||||
|
||||
## 🎥 Tutorial
|
||||
This project is part of a **video tutorial series**.
|
||||
Please watch the tutorial to understand the architecture and how to safely extend the system.
|
||||
video: https://youtu.be/ZD6kf9w9Sy0?si=DvySsKIeL4RuAjVt
|
||||
|
||||
## 🔐 Security Notice
|
||||
- Runs **100% locally**
|
||||
- No remote access by default
|
||||
- Jarvis only executes what **YOU code**
|
||||
|
||||
⚠️ Do NOT paste unknown code or give full system control to untrusted scripts.
|
||||
|
||||
---
|
||||
|
||||
## ⚠️ Usage Warning
|
||||
- **No commercial use**
|
||||
- Do NOT sell, market, or monetize
|
||||
- Do NOT present this project as your own on social media
|
||||
|
||||
You may use and modify it for **personal and educational purposes**.
|
||||
|
||||
---
|
||||
|
||||
## 📲 Follow & Contact
|
||||
Instagram: https://www.instagram.com/fatihmakes/
|
||||
TikTok: https://www.tiktok.com/@fatihmakes
|
||||
|
||||
If you have issues or suggestions, feel free to reach out.
|
||||
|
||||
⭐ Like, follow, and subscribe for future Jarvis upgrades!
|
||||
@@ -0,0 +1,2 @@
|
||||
#EXAMPLE ACTION IDEA FOR IMPROVE YOURSELF
|
||||
#TAKE THE API KEY FROM A RADAR WEB SITE TO SEE ALL PLANES ON THE AIR
|
||||
@@ -0,0 +1,63 @@
|
||||
import time
|
||||
import pyautogui
|
||||
from tts import edge_speak
|
||||
|
||||
|
||||
def open_app(
|
||||
parameters: dict,
|
||||
response: str | None = None,
|
||||
player=None,
|
||||
session_memory=None
|
||||
) -> bool:
|
||||
"""
|
||||
Opens an application using Windows search.
|
||||
|
||||
parameters:
|
||||
- app_name (str)
|
||||
|
||||
Memory behavior:
|
||||
- Uses ONLY session memory
|
||||
- No long-term memory writes
|
||||
"""
|
||||
|
||||
app_name = (parameters or {}).get("app_name", "").strip()
|
||||
|
||||
if not app_name and session_memory:
|
||||
app_name = session_memory.open_app or ""
|
||||
|
||||
if not app_name:
|
||||
msg = "Sir, I couldn't determine which application to open."
|
||||
if player:
|
||||
player.write_log(msg)
|
||||
edge_speak(msg, player)
|
||||
return False
|
||||
|
||||
if response:
|
||||
if player:
|
||||
player.write_log(response)
|
||||
edge_speak(response, player)
|
||||
|
||||
try:
|
||||
pyautogui.PAUSE = 0.1
|
||||
|
||||
|
||||
pyautogui.press("win")
|
||||
time.sleep(0.3)
|
||||
|
||||
pyautogui.write(app_name, interval=0.03)
|
||||
time.sleep(0.2)
|
||||
|
||||
pyautogui.press("enter")
|
||||
time.sleep(0.6)
|
||||
|
||||
if session_memory:
|
||||
session_memory.set_open_app(app_name)
|
||||
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
msg = f"Sir, I failed to open {app_name}."
|
||||
if player:
|
||||
player.write_log(f"{msg} ({e})")
|
||||
edge_speak(msg, player)
|
||||
return False
|
||||
@@ -0,0 +1,96 @@
|
||||
import time
|
||||
import pyautogui
|
||||
from tts import edge_speak
|
||||
|
||||
REQUIRED_PARAMS = ["receiver", "message_text", "platform"]
|
||||
|
||||
def send_message(parameters: dict, response: str | None = None, player=None, session_memory=None) -> bool:
|
||||
"""
|
||||
Send a message via Windows app (WhatsApp, Telegram, etc.)
|
||||
|
||||
Multi-step support: asks for missing parameters using temporary memory.
|
||||
|
||||
Expected parameters:
|
||||
- receiver (str)
|
||||
- message_text (str)
|
||||
- platform (str, default: "WhatsApp")
|
||||
"""
|
||||
|
||||
if session_memory is None:
|
||||
msg = "Session memory missing, cannot proceed."
|
||||
if player:
|
||||
player.write_log(msg)
|
||||
edge_speak(msg, player)
|
||||
return False
|
||||
|
||||
if parameters:
|
||||
session_memory.update_parameters(parameters)
|
||||
|
||||
for param in REQUIRED_PARAMS:
|
||||
value = session_memory.get_parameter(param)
|
||||
if not value:
|
||||
|
||||
session_memory.set_current_question(param)
|
||||
question_text = ""
|
||||
if param == "receiver":
|
||||
question_text = "Sir, who should I send the message to?"
|
||||
elif param == "message_text":
|
||||
question_text = "Sir, what should I say?"
|
||||
elif param == "platform":
|
||||
question_text = "Sir, which platform should I use? (WhatsApp, Telegram, etc.)"
|
||||
else:
|
||||
question_text = f"Sir, please provide {param}."
|
||||
|
||||
if player:
|
||||
player.write_log("AI :", question_text)
|
||||
edge_speak(question_text, player)
|
||||
return False
|
||||
|
||||
receiver = session_memory.get_parameter("receiver").strip()
|
||||
platform = session_memory.get_parameter("platform").strip() or "WhatsApp"
|
||||
message_text = session_memory.get_parameter("message_text").strip()
|
||||
|
||||
if response:
|
||||
if player:
|
||||
player.write_log(response)
|
||||
edge_speak(response, player)
|
||||
|
||||
try:
|
||||
pyautogui.PAUSE = 0.1
|
||||
|
||||
pyautogui.press("win")
|
||||
time.sleep(0.3)
|
||||
pyautogui.write(platform, interval=0.03)
|
||||
pyautogui.press("enter")
|
||||
time.sleep(0.6)
|
||||
|
||||
pyautogui.hotkey("ctrl", "f")
|
||||
time.sleep(0.2)
|
||||
pyautogui.write(receiver, interval=0.03)
|
||||
time.sleep(0.2)
|
||||
pyautogui.press("enter")
|
||||
time.sleep(0.2)
|
||||
|
||||
pyautogui.write(message_text, interval=0.03)
|
||||
pyautogui.press("enter")
|
||||
|
||||
session_memory.clear_current_question()
|
||||
session_memory.clear_pending_intent()
|
||||
session_memory.update_parameters({})
|
||||
|
||||
# -----------------------------
|
||||
# Log success
|
||||
# -----------------------------
|
||||
success_msg = f"Sir, message sent to {receiver} via {platform}."
|
||||
if player:
|
||||
player.write_log(success_msg)
|
||||
edge_speak(success_msg, player)
|
||||
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
msg = f"Sir, I failed to send the message. ({e})"
|
||||
if player:
|
||||
player.write_log(msg)
|
||||
edge_speak(msg, player)
|
||||
return False
|
||||
@@ -0,0 +1,67 @@
|
||||
import webbrowser
|
||||
from urllib.parse import quote_plus
|
||||
from tts import edge_speak
|
||||
|
||||
|
||||
def weather_action(
|
||||
parameters: dict,
|
||||
player=None,
|
||||
session_memory=None
|
||||
):
|
||||
"""
|
||||
Weather report action.
|
||||
Opens a Google weather search and gives a short spoken confirmation.
|
||||
"""
|
||||
|
||||
city = parameters.get("city")
|
||||
time = parameters.get("time")
|
||||
if not city or not isinstance(city, str):
|
||||
msg = "Sir, the city is missing for the weather report."
|
||||
_speak_and_log(msg, player)
|
||||
return msg
|
||||
|
||||
city = city.strip()
|
||||
|
||||
if not time or not isinstance(time, str):
|
||||
time = "today"
|
||||
else:
|
||||
time = time.strip()
|
||||
|
||||
search_query = f"weather in {city} {time}"
|
||||
encoded_query = quote_plus(search_query)
|
||||
url = f"https://www.google.com/search?q={encoded_query}"
|
||||
|
||||
try:
|
||||
webbrowser.open(url)
|
||||
except Exception:
|
||||
msg = f"Sir, I couldn't open the browser for the weather report."
|
||||
_speak_and_log(msg, player)
|
||||
return msg
|
||||
|
||||
msg = f"Showing the weather for {city}, {time}, sir."
|
||||
_speak_and_log(msg, player)
|
||||
|
||||
if session_memory:
|
||||
try:
|
||||
session_memory.set_last_search(
|
||||
query=search_query,
|
||||
response=msg
|
||||
)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
return msg
|
||||
|
||||
|
||||
def _speak_and_log(message: str, player=None):
|
||||
"""Helper: log + TTS safely"""
|
||||
if player:
|
||||
try:
|
||||
player.write_log(f"JARVIS: {message}")
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
try:
|
||||
edge_speak(message)
|
||||
except Exception:
|
||||
pass
|
||||
@@ -0,0 +1,165 @@
|
||||
import re
|
||||
from serpapi import GoogleSearch
|
||||
from tts import edge_speak
|
||||
|
||||
|
||||
MAX_SNIPPETS = 3
|
||||
MIN_SENTENCE_LENGTH = 30
|
||||
|
||||
|
||||
def clean(text: str) -> str:
|
||||
"""Clean text: remove extra spaces, ..., brackets."""
|
||||
if not text:
|
||||
return ""
|
||||
|
||||
text = re.sub(r"\s+", " ", text)
|
||||
text = re.sub(r"\.\.\.+", ".", text)
|
||||
text = re.sub(r"\[.*?\]", "", text)
|
||||
text = re.sub(r"\(.*?\)", "", text)
|
||||
return text.strip()
|
||||
|
||||
|
||||
def split_sentences(text: str):
|
||||
"""Split text into sentences and avoid very short or incomplete ones."""
|
||||
sentences = re.split(r"(?<=[.!?])\s+", text)
|
||||
|
||||
valid_sentences = []
|
||||
buffer = ""
|
||||
|
||||
for s in sentences:
|
||||
s = clean(s)
|
||||
if not s:
|
||||
continue
|
||||
|
||||
last_word = s.split()[-1].lower()
|
||||
if last_word in [
|
||||
"of", "in", "at", "on", "by",
|
||||
"after", "before", "with", "for", "to"
|
||||
]:
|
||||
buffer += s + " "
|
||||
continue
|
||||
|
||||
if buffer:
|
||||
s = buffer + s
|
||||
buffer = ""
|
||||
|
||||
if len(s) >= MIN_SENTENCE_LENGTH:
|
||||
valid_sentences.append(s)
|
||||
|
||||
return valid_sentences
|
||||
|
||||
|
||||
def is_noise(text: str) -> bool:
|
||||
"""Check if a sentence is noise or irrelevant."""
|
||||
t = text.lower()
|
||||
|
||||
noise_keywords = [
|
||||
"read more",
|
||||
"learn more",
|
||||
"click here",
|
||||
"infographic",
|
||||
"google trends",
|
||||
"year in search",
|
||||
"most searched",
|
||||
"top 100",
|
||||
"subscribe",
|
||||
"share this",
|
||||
"advertisement",
|
||||
"ad:"
|
||||
]
|
||||
|
||||
return any(word in t for word in noise_keywords)
|
||||
|
||||
|
||||
def select_best_sentence(snippets):
|
||||
"""Combine up to MAX_SNIPPETS and return a single coherent sentence."""
|
||||
final_text = ""
|
||||
count = 0
|
||||
|
||||
for snippet in snippets:
|
||||
if not snippet or is_noise(snippet):
|
||||
continue
|
||||
|
||||
sentences = split_sentences(snippet)
|
||||
|
||||
for s in sentences:
|
||||
if is_noise(s):
|
||||
continue
|
||||
|
||||
final_text = f"{final_text} {s}".strip()
|
||||
count += 1
|
||||
|
||||
if count >= MAX_SNIPPETS:
|
||||
return final_text
|
||||
|
||||
return final_text if final_text else None
|
||||
|
||||
|
||||
def serpapi_answer(query: str, api_key: str) -> str:
|
||||
"""Perform search and return a single coherent sentence."""
|
||||
params = {
|
||||
"q": query,
|
||||
"engine": "google",
|
||||
"hl": "en",
|
||||
"gl": "us",
|
||||
"num": 3,
|
||||
"api_key": api_key
|
||||
}
|
||||
|
||||
try:
|
||||
data = GoogleSearch(params).get_dict()
|
||||
except Exception:
|
||||
return "Sir, the web search failed."
|
||||
|
||||
organic = data.get("organic_results", [])
|
||||
if not organic:
|
||||
return "Sir, I couldn't find relevant information."
|
||||
|
||||
snippets = [
|
||||
r.get("snippet", "")
|
||||
for r in organic
|
||||
if r.get("snippet")
|
||||
]
|
||||
|
||||
answer = select_best_sentence(snippets)
|
||||
|
||||
if not answer:
|
||||
return "Sir, I found information online, but couldn't summarize it clearly."
|
||||
|
||||
return answer
|
||||
|
||||
|
||||
|
||||
def web_search(
|
||||
parameters: dict,
|
||||
player=None,
|
||||
session_memory=None,
|
||||
api_key: str = ""
|
||||
):
|
||||
"""
|
||||
Main web search:
|
||||
- Returns 1 coherent sentence
|
||||
- Does NOT append previous answers
|
||||
- Combines multiple snippets if needed to avoid cut-offs
|
||||
"""
|
||||
|
||||
query = (parameters or {}).get("query", "").strip()
|
||||
|
||||
if not query:
|
||||
msg = "Sir, I couldn't understand the search request."
|
||||
if player:
|
||||
player.write_log(msg)
|
||||
edge_speak(msg)
|
||||
return msg
|
||||
|
||||
answer = serpapi_answer(query, api_key)
|
||||
|
||||
if player:
|
||||
player.write_log(f"AI: {answer}")
|
||||
|
||||
edge_speak(answer)
|
||||
|
||||
if session_memory:
|
||||
session_memory.set_last_search(query, answer)
|
||||
|
||||
return answer
|
||||
@@ -0,0 +1,44 @@
|
||||
You are Jarvis, Fully optimized, Fast react AI Computer Assistant
|
||||
|
||||
Intents:
|
||||
- send_message
|
||||
- open_app
|
||||
- search
|
||||
- weather_report
|
||||
- chat
|
||||
|
||||
Parameter Rules:
|
||||
- search → parameters.query (cleaned core question).
|
||||
- weather_report → city, time (time is the day)
|
||||
- if user wants to send a message to someone then return intent: send_message → receiver, message_text, platform (default WhatsApp) and also if user says something like "ask him how he is doing" then appoint this into message_text
|
||||
- open_app → app_name(If user asks, can you open please, create a lovely text and open the app that user asked you to open)
|
||||
- If user the app name incorrect, open the app by correcting it(f.e: Hey Jarvis open what's up = WhatsApp)
|
||||
- Missing a required field → ask one short clarification question, set needs_clarification:true, stop.
|
||||
|
||||
Chat Mode:
|
||||
- 1‑2 short sentences, no extra analysis.
|
||||
- Keep only identity, preferences, relationships, emotional_state.
|
||||
- temporary memory
|
||||
|
||||
Output format:
|
||||
|
||||
json
|
||||
{
|
||||
"intent": null,
|
||||
"parameters": {},
|
||||
"needs_clarification": false,
|
||||
"text": null,
|
||||
"memory_update": {
|
||||
"identity": {},
|
||||
"preferences": {},
|
||||
"relationships": {},
|
||||
"emotional_state": {}
|
||||
}
|
||||
}
|
||||
|
||||
Just add really important informations in the memory like;
|
||||
- Name of the user, age, city
|
||||
- The question that user's ask mostly doesn't include memory.
|
||||
|
||||
return json format.
|
||||
Create really fast responses. make it quick.
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 383 KiB |
+170
@@ -0,0 +1,170 @@
|
||||
# llm.py
|
||||
|
||||
import os
|
||||
import json
|
||||
import requests
|
||||
from dotenv import load_dotenv
|
||||
|
||||
load_dotenv()
|
||||
|
||||
OPENROUTER_API_KEY = os.getenv("OPENROUTER_API_KEY")
|
||||
OPENROUTER_URL = "https://openrouter.ai/api/v1/chat/completions"
|
||||
|
||||
MODEL = "arcee-ai/trinity-large-preview:free"
|
||||
|
||||
PROMPT_PATH = "core/prompt.txt"
|
||||
|
||||
|
||||
def load_system_prompt() -> str:
|
||||
try:
|
||||
with open(PROMPT_PATH, "r", encoding="utf-8") as f:
|
||||
return f.read()
|
||||
except Exception as e:
|
||||
print(f"⚠️ prompt.txt cannot installed: {e}")
|
||||
return "You are Jarvis, a helpful AI assistant."
|
||||
|
||||
|
||||
SYSTEM_PROMPT = load_system_prompt()
|
||||
|
||||
|
||||
def safe_json_parse(text: str) -> dict | None:
|
||||
if not text:
|
||||
return None
|
||||
|
||||
text = text.strip()
|
||||
|
||||
if "```json" in text:
|
||||
try:
|
||||
start = text.index("```json") + 7
|
||||
end = text.index("```", start)
|
||||
text = text[start:end].strip()
|
||||
except:
|
||||
pass
|
||||
elif "```" in text:
|
||||
try:
|
||||
start = text.index("```") + 3
|
||||
end = text.index("```", start)
|
||||
text = text[start:end].strip()
|
||||
except:
|
||||
pass
|
||||
|
||||
try:
|
||||
start = text.index("{")
|
||||
end = text.rindex("}") + 1
|
||||
json_str = text[start:end]
|
||||
return json.loads(json_str)
|
||||
except Exception as e:
|
||||
print(f"⚠️ JSON parse error: {e}")
|
||||
print(f"The error text: {text[:200]}")
|
||||
return None
|
||||
|
||||
|
||||
def get_llm_output(user_text: str, memory_block: dict = None) -> dict:
|
||||
|
||||
if not user_text or not user_text.strip():
|
||||
return {
|
||||
"intent": "chat",
|
||||
"parameters": {},
|
||||
"needs_clarification": False,
|
||||
"text": "Sir, I didn't catch that.",
|
||||
"memory_update": None
|
||||
}
|
||||
|
||||
if not OPENROUTER_API_KEY:
|
||||
print("❌ OPENROUTER_API_KEY couldn't found!")
|
||||
return {
|
||||
"intent": "chat",
|
||||
"parameters": {},
|
||||
"text": "API key is missing, Sir.",
|
||||
"needs_clarification": False,
|
||||
"memory_update": None
|
||||
}
|
||||
|
||||
# Memory'yi string'e çevir
|
||||
memory_str = ""
|
||||
if memory_block:
|
||||
memory_str = "\n".join(f"{k}: {v}" for k, v in memory_block.items())
|
||||
|
||||
user_prompt = f"""User message: "{user_text}"
|
||||
|
||||
Known user memory:
|
||||
{memory_str if memory_str else "No memory available"}"""
|
||||
|
||||
payload = {
|
||||
"model": MODEL,
|
||||
"messages": [
|
||||
{"role": "system", "content": SYSTEM_PROMPT},
|
||||
{"role": "user", "content": user_prompt}
|
||||
],
|
||||
"temperature": 0.2,
|
||||
"max_tokens": 500
|
||||
}
|
||||
|
||||
headers = {
|
||||
"Authorization": f"Bearer {OPENROUTER_API_KEY}",
|
||||
"Content-Type": "application/json",
|
||||
"HTTP-Referer": "http://localhost",
|
||||
"X-Title": "Jarvis-Assistant"
|
||||
}
|
||||
|
||||
try:
|
||||
|
||||
response = requests.post(
|
||||
OPENROUTER_URL,
|
||||
headers=headers,
|
||||
json=payload,
|
||||
timeout=30
|
||||
)
|
||||
|
||||
if response.status_code != 200:
|
||||
print(f"❌ API Hatası: {response.text}")
|
||||
return {
|
||||
"intent": "chat",
|
||||
"parameters": {},
|
||||
"text": f"Sir, API error: {response.status_code}",
|
||||
"needs_clarification": False,
|
||||
"memory_update": None
|
||||
}
|
||||
|
||||
data = response.json()
|
||||
content = data["choices"][0]["message"]["content"]
|
||||
|
||||
# JSON parse et
|
||||
parsed = safe_json_parse(content)
|
||||
|
||||
if parsed:
|
||||
return {
|
||||
"intent": parsed.get("intent", "chat"),
|
||||
"parameters": parsed.get("parameters", {}),
|
||||
"needs_clarification": parsed.get("needs_clarification", False),
|
||||
"text": parsed.get("text"),
|
||||
"memory_update": parsed.get("memory_update")
|
||||
}
|
||||
|
||||
return {
|
||||
"intent": "chat",
|
||||
"parameters": {},
|
||||
"needs_clarification": False,
|
||||
"text": content,
|
||||
"memory_update": None
|
||||
}
|
||||
|
||||
except requests.exceptions.Timeout:
|
||||
print("❌ API timeout!")
|
||||
return {
|
||||
"intent": "chat",
|
||||
"text": "Sir, the connection timed out.",
|
||||
"parameters": {},
|
||||
"needs_clarification": False,
|
||||
"memory_update": None
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
print(f"❌ LLM ERROR: {e}")
|
||||
return {
|
||||
"intent": "chat",
|
||||
"text": "Sir, I encountered a system error.",
|
||||
"parameters": {},
|
||||
"needs_clarification": False,
|
||||
"memory_update": None
|
||||
}
|
||||
+188
@@ -0,0 +1,188 @@
|
||||
import asyncio
|
||||
import threading
|
||||
|
||||
from speech_to_text import record_voice, stop_listening_flag
|
||||
from llm import get_llm_output
|
||||
from tts import edge_speak, stop_speaking
|
||||
from ui import JarvisUI
|
||||
|
||||
from actions.open_app import open_app
|
||||
from actions.web_search import web_search
|
||||
from actions.weather_report import weather_action
|
||||
from actions.send_message import send_message
|
||||
|
||||
from memory.memory_manager import load_memory, update_memory
|
||||
from memory.temporary_memory import TemporaryMemory
|
||||
|
||||
interrupt_commands = ["mute", "quit", "exit", "stop"]
|
||||
|
||||
|
||||
temp_memory = TemporaryMemory()
|
||||
|
||||
|
||||
async def get_voice_input():
|
||||
return await asyncio.to_thread(record_voice)
|
||||
|
||||
|
||||
async def ai_loop(ui: JarvisUI):
|
||||
while True:
|
||||
stop_listening_flag.clear()
|
||||
user_text = await get_voice_input()
|
||||
|
||||
if not user_text:
|
||||
continue
|
||||
|
||||
|
||||
if any(cmd in user_text.lower() for cmd in interrupt_commands):
|
||||
stop_speaking()
|
||||
temp_memory.reset()
|
||||
continue
|
||||
|
||||
ui.write_log(f"You: {user_text}")
|
||||
|
||||
|
||||
if temp_memory.get_current_question():
|
||||
param = temp_memory.get_current_question()
|
||||
temp_memory.update_parameters({param: user_text})
|
||||
temp_memory.clear_current_question()
|
||||
user_text = temp_memory.get_last_user_text()
|
||||
|
||||
temp_memory.set_last_user_text(user_text)
|
||||
|
||||
long_term_memory = load_memory()
|
||||
|
||||
def minimal_memory_for_prompt(memory: dict) -> dict:
|
||||
result = {}
|
||||
identity = memory.get("identity", {})
|
||||
preferences = memory.get("preferences", {})
|
||||
relationships = memory.get("relationships", {})
|
||||
emotional_state = memory.get("emotional_state", {})
|
||||
|
||||
if "name" in identity:
|
||||
result["user_name"] = identity["name"].get("value")
|
||||
|
||||
for k in ["favorite_color", "favorite_food", "favorite_music"]:
|
||||
if k in preferences:
|
||||
val = preferences[k].get("value")
|
||||
if isinstance(val, dict) and "value" in val:
|
||||
val = val["value"]
|
||||
result[k] = val
|
||||
|
||||
for rel, info in relationships.items():
|
||||
if isinstance(info, dict) and "name" in info and "value" in info["name"]:
|
||||
result[f"{rel}_name"] = info["name"]["value"]
|
||||
|
||||
for event, info in emotional_state.items():
|
||||
if "value" in info:
|
||||
result[f"emotion_{event}"] = info["value"]
|
||||
|
||||
return {k: v for k, v in result.items() if v}
|
||||
|
||||
memory_for_prompt = minimal_memory_for_prompt(long_term_memory)
|
||||
|
||||
history_lines = temp_memory.get_history_for_prompt()
|
||||
recent_history = "\n".join(history_lines.split("\n")[-5:])
|
||||
if recent_history:
|
||||
memory_for_prompt["recent_conversation"] = recent_history
|
||||
|
||||
if temp_memory.has_pending_intent():
|
||||
memory_for_prompt["_pending_intent"] = temp_memory.pending_intent
|
||||
memory_for_prompt["_collected_params"] = str(temp_memory.get_parameters())
|
||||
|
||||
try:
|
||||
llm_output = get_llm_output(
|
||||
user_text=user_text,
|
||||
memory_block=memory_for_prompt
|
||||
)
|
||||
except Exception as e:
|
||||
ui.write_log(f"AI ERROR: {e}")
|
||||
continue
|
||||
|
||||
intent = llm_output.get("intent", "chat")
|
||||
parameters = llm_output.get("parameters", {})
|
||||
response = llm_output.get("text")
|
||||
memory_update = llm_output.get("memory_update")
|
||||
|
||||
if memory_update and isinstance(memory_update, dict):
|
||||
update_memory(memory_update)
|
||||
|
||||
temp_memory.set_last_ai_response(response)
|
||||
|
||||
if intent == "send_message":
|
||||
temp_memory.set_pending_intent("send_message")
|
||||
temp_memory.update_parameters(parameters)
|
||||
|
||||
all_params_ready = all(temp_memory.get_parameter(p) for p in ["receiver", "message_text", "platform"])
|
||||
if all_params_ready:
|
||||
threading.Thread(
|
||||
target=send_message,
|
||||
kwargs={
|
||||
"parameters": temp_memory.get_parameters(),
|
||||
"player": ui,
|
||||
"session_memory": temp_memory
|
||||
},
|
||||
daemon=True
|
||||
).start()
|
||||
|
||||
elif intent == "open_app":
|
||||
app_name = parameters.get("app_name")
|
||||
if app_name:
|
||||
threading.Thread(
|
||||
target=open_app,
|
||||
kwargs={
|
||||
"parameters": parameters,
|
||||
"response": response,
|
||||
"player": ui,
|
||||
"session_memory": temp_memory
|
||||
},
|
||||
daemon=True
|
||||
).start()
|
||||
|
||||
elif intent == "weather_report":
|
||||
city = parameters.get("city")
|
||||
time = parameters.get("time")
|
||||
if city:
|
||||
threading.Thread(
|
||||
target=weather_action,
|
||||
kwargs={
|
||||
"parameters": parameters,
|
||||
"player": ui,
|
||||
"session_memory": temp_memory
|
||||
},
|
||||
daemon=True
|
||||
).start()
|
||||
|
||||
elif intent == "search":
|
||||
query = parameters.get("query")
|
||||
if query:
|
||||
threading.Thread(
|
||||
target=web_search,
|
||||
kwargs={
|
||||
"parameters": parameters,
|
||||
"player": ui,
|
||||
"session_memory": temp_memory,
|
||||
"api_key": "API_KEY"
|
||||
},
|
||||
daemon=True
|
||||
).start()
|
||||
|
||||
else:
|
||||
if response:
|
||||
ui.write_log(f"AI: {response}")
|
||||
edge_speak(response, ui)
|
||||
|
||||
await asyncio.sleep(0.01)
|
||||
|
||||
|
||||
def main():
|
||||
ui = JarvisUI("face.png", size=(900, 900))
|
||||
|
||||
def runner():
|
||||
asyncio.run(ai_loop(ui))
|
||||
|
||||
threading.Thread(target=runner, daemon=True).start()
|
||||
ui.root.mainloop()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1 @@
|
||||
#memory package
|
||||
@@ -0,0 +1,83 @@
|
||||
# memory/memory_manager.py
|
||||
import json
|
||||
import os
|
||||
from threading import Lock
|
||||
from datetime import datetime
|
||||
|
||||
MEMORY_PATH = "memory/memory.json"
|
||||
_lock = Lock()
|
||||
|
||||
|
||||
def _empty_memory() -> dict:
|
||||
"""Return an empty memory structure."""
|
||||
return {
|
||||
"identity": {},
|
||||
"preferences": {},
|
||||
"relationships": {},
|
||||
"emotional_state": {}
|
||||
}
|
||||
|
||||
|
||||
def load_memory() -> dict:
|
||||
"""Load memory from disk, return empty if not exists or invalid."""
|
||||
if not os.path.exists(MEMORY_PATH):
|
||||
return _empty_memory()
|
||||
|
||||
with _lock:
|
||||
try:
|
||||
with open(MEMORY_PATH, "r", encoding="utf-8") as f:
|
||||
data = json.load(f)
|
||||
if isinstance(data, dict):
|
||||
return data
|
||||
return _empty_memory()
|
||||
except Exception:
|
||||
return _empty_memory()
|
||||
|
||||
|
||||
def save_memory(memory: dict) -> None:
|
||||
"""Save memory to disk safely."""
|
||||
if not isinstance(memory, dict):
|
||||
return
|
||||
|
||||
os.makedirs(os.path.dirname(MEMORY_PATH), exist_ok=True)
|
||||
|
||||
with _lock:
|
||||
with open(MEMORY_PATH, "w", encoding="utf-8") as f:
|
||||
json.dump(memory, f, indent=2, ensure_ascii=False)
|
||||
|
||||
|
||||
def _recursive_update(target: dict, updates: dict) -> bool:
|
||||
"""Recursively merge updates into target memory. Returns True if changed."""
|
||||
changed = False
|
||||
now = datetime.utcnow().isoformat() + "Z"
|
||||
|
||||
for key, value in updates.items():
|
||||
if value is None or (isinstance(value, str) and not value.strip()):
|
||||
continue
|
||||
|
||||
if isinstance(value, dict) and "value" not in value:
|
||||
if key not in target or not isinstance(target[key], dict):
|
||||
target[key] = {}
|
||||
changed = True
|
||||
if _recursive_update(target[key], value):
|
||||
changed = True
|
||||
else:
|
||||
|
||||
entry = value if isinstance(value, dict) and "value" in value else {"value": value}
|
||||
if key not in target or target[key] != entry:
|
||||
target[key] = entry
|
||||
changed = True
|
||||
|
||||
return changed
|
||||
|
||||
|
||||
def update_memory(memory_update: dict) -> dict:
|
||||
"""Merge LLM memory update into global memory and save."""
|
||||
if not isinstance(memory_update, dict):
|
||||
return load_memory()
|
||||
|
||||
memory = load_memory()
|
||||
if _recursive_update(memory, memory_update):
|
||||
save_memory(memory)
|
||||
|
||||
return memory
|
||||
@@ -0,0 +1,136 @@
|
||||
# memory/temporary_memory.py
|
||||
from typing import Any
|
||||
|
||||
|
||||
class TemporaryMemory:
|
||||
"""
|
||||
Temporary runtime memory (session-only).
|
||||
Resets when program restarts.
|
||||
|
||||
Purpose:
|
||||
- Track multi-step intents
|
||||
- Keep short conversation context
|
||||
- Store last actions (search / open_app)
|
||||
- Support follow-up questions like:
|
||||
"why did he executed?"
|
||||
"""
|
||||
|
||||
def __init__(self, max_history: int = 5):
|
||||
self.max_history = max_history
|
||||
self.reset()
|
||||
|
||||
|
||||
def reset(self):
|
||||
self.pending_intent: str | None = None
|
||||
self.parameters: dict[str, Any] = {}
|
||||
self.current_question: str | None = None
|
||||
|
||||
self.last_user_text: str | None = None
|
||||
self.last_ai_response: str | None = None
|
||||
|
||||
# --- Action memory ---
|
||||
self.last_search: dict | None = None
|
||||
self.last_opened_app: str | None = None
|
||||
|
||||
# --- Conversation ---
|
||||
self.conversation_history: list[dict[str, str]] = []
|
||||
|
||||
|
||||
def set_pending_intent(self, intent: str):
|
||||
self.pending_intent = intent
|
||||
|
||||
def clear_pending_intent(self):
|
||||
self.pending_intent = None
|
||||
self.parameters = {}
|
||||
self.current_question = None
|
||||
|
||||
def has_pending_intent(self) -> bool:
|
||||
return self.pending_intent is not None
|
||||
|
||||
|
||||
def update_parameters(self, new_params: dict):
|
||||
if not isinstance(new_params, dict):
|
||||
return
|
||||
for k, v in new_params.items():
|
||||
if v not in (None, ""):
|
||||
self.parameters[k] = v
|
||||
|
||||
def get_parameters(self) -> dict:
|
||||
return self.parameters.copy()
|
||||
|
||||
def get_parameter(self, key: str):
|
||||
return self.parameters.get(key)
|
||||
|
||||
def set_current_question(self, param_name: str):
|
||||
self.current_question = param_name
|
||||
|
||||
def get_current_question(self) -> str | None:
|
||||
return self.current_question
|
||||
|
||||
def clear_current_question(self):
|
||||
self.current_question = None
|
||||
|
||||
def set_last_user_text(self, text: str):
|
||||
self.last_user_text = text
|
||||
self._add_to_history("user", text)
|
||||
|
||||
def set_last_ai_response(self, text: str):
|
||||
self.last_ai_response = text
|
||||
self._add_to_history("ai", text)
|
||||
|
||||
def get_last_user_text(self):
|
||||
return self.last_user_text
|
||||
|
||||
def get_last_ai_response(self):
|
||||
return self.last_ai_response
|
||||
|
||||
|
||||
def set_last_search(self, query: str, answer: str):
|
||||
self.last_search = {
|
||||
"query": query,
|
||||
"answer": answer
|
||||
}
|
||||
|
||||
def get_last_search(self):
|
||||
return self.last_search
|
||||
|
||||
def set_open_app(self, app_name: str):
|
||||
self.last_opened_app = app_name
|
||||
|
||||
def get_last_opened_app(self):
|
||||
return self.last_opened_app
|
||||
|
||||
def _add_to_history(self, role: str, text: str):
|
||||
if role not in ("user", "ai"):
|
||||
return
|
||||
|
||||
self.conversation_history.append({
|
||||
"role": role,
|
||||
"text": text
|
||||
})
|
||||
|
||||
if len(self.conversation_history) > self.max_history:
|
||||
self.conversation_history.pop(0)
|
||||
|
||||
def get_history_for_prompt(self) -> str:
|
||||
"""
|
||||
Returns compact history for LLM prompt.
|
||||
"""
|
||||
return "\n".join(
|
||||
f"{m['role'].capitalize()}: {m['text']}"
|
||||
for m in self.conversation_history
|
||||
)
|
||||
|
||||
|
||||
def get_context_summary(self) -> dict:
|
||||
"""
|
||||
Useful for debugging or smarter LLM prompts.
|
||||
"""
|
||||
return {
|
||||
"pending_intent": self.pending_intent,
|
||||
"parameters": self.parameters,
|
||||
"last_search": self.last_search,
|
||||
"last_opened_app": self.last_opened_app,
|
||||
"last_user_text": self.last_user_text,
|
||||
"last_ai_response": self.last_ai_response,
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
import sounddevice as sd
|
||||
import vosk
|
||||
import queue
|
||||
import sys
|
||||
import json
|
||||
import threading
|
||||
|
||||
MODEL_PATH = "C:/Users/90553/Downloads/vosk/vosk-model-small-en-us-0.15" # put your path
|
||||
model = vosk.Model(MODEL_PATH)
|
||||
|
||||
q = queue.Queue()
|
||||
stop_listening_flag = threading.Event()
|
||||
|
||||
def callback(indata, frames, time, status):
|
||||
if status:
|
||||
print(status, file=sys.stderr)
|
||||
q.put(bytes(indata))
|
||||
|
||||
def record_voice(prompt="🎙 I'm listening, sir..."):
|
||||
"""
|
||||
Blocking call, returns the first recognized sentence.
|
||||
"""
|
||||
print(prompt)
|
||||
rec = vosk.KaldiRecognizer(model, 16000)
|
||||
with sd.RawInputStream(samplerate=16000, blocksize=8000, dtype='int16',
|
||||
channels=1, callback=callback):
|
||||
while not stop_listening_flag.is_set():
|
||||
try:
|
||||
data = q.get(timeout=0.1)
|
||||
except queue.Empty:
|
||||
continue
|
||||
if rec.AcceptWaveform(data):
|
||||
result = json.loads(rec.Result())
|
||||
text = result.get("text", "")
|
||||
if text.strip():
|
||||
print("👤 You:", text)
|
||||
return text
|
||||
return ""
|
||||
@@ -0,0 +1,71 @@
|
||||
import io
|
||||
import threading
|
||||
import requests
|
||||
import sounddevice as sd
|
||||
import soundfile as sf
|
||||
|
||||
#Take the API key from ElevenLabs
|
||||
|
||||
ELEVEN_API_KEY = "GO ELEVENLABS"
|
||||
VOICE_ID = "JBFqnCBsd6RMkjVDRZzb"
|
||||
|
||||
stop_speaking_flag = threading.Event()
|
||||
|
||||
def edge_speak(text: str, ui=None, blocking=False):
|
||||
if not text.strip():
|
||||
return
|
||||
|
||||
|
||||
finished_event = threading.Event()
|
||||
|
||||
def _thread():
|
||||
if ui:
|
||||
ui.start_speaking()
|
||||
stop_speaking_flag.clear()
|
||||
|
||||
try:
|
||||
url = f"https://api.elevenlabs.io/v1/text-to-speech/{VOICE_ID}"
|
||||
headers = {
|
||||
"xi-api-key": ELEVEN_API_KEY,
|
||||
"Content-Type": "application/json"
|
||||
}
|
||||
payload = {
|
||||
"text": text.strip(),
|
||||
"voice_settings": {
|
||||
"stability": 0.55,
|
||||
"similarity_boost": 0.85
|
||||
}
|
||||
}
|
||||
|
||||
response = requests.post(url, json=payload, headers=headers)
|
||||
response.raise_for_status()
|
||||
|
||||
audio_data = io.BytesIO(response.content)
|
||||
data, samplerate = sf.read(audio_data, dtype="float32")
|
||||
|
||||
channels = data.shape[1] if len(data.shape) > 1 else 1
|
||||
with sd.OutputStream(
|
||||
samplerate=samplerate,
|
||||
channels=channels,
|
||||
dtype="float32"
|
||||
) as stream:
|
||||
block_size = 1024
|
||||
for start in range(0, len(data), block_size):
|
||||
if stop_speaking_flag.is_set():
|
||||
break
|
||||
stream.write(data[start:start + block_size])
|
||||
|
||||
except Exception as e:
|
||||
print("VOICE ERROR:", e)
|
||||
finally:
|
||||
if ui:
|
||||
ui.stop_speaking()
|
||||
finished_event.set()
|
||||
|
||||
threading.Thread(target=_thread, daemon=True).start()
|
||||
|
||||
if blocking:
|
||||
finished_event.wait()
|
||||
|
||||
def stop_speaking():
|
||||
stop_speaking_flag.set()
|
||||
+155
@@ -0,0 +1,155 @@
|
||||
import os
|
||||
import time
|
||||
import random
|
||||
import tkinter as tk
|
||||
from collections import deque
|
||||
from PIL import Image, ImageTk, ImageDraw, ImageFilter
|
||||
from tkinter.scrolledtext import ScrolledText
|
||||
|
||||
|
||||
class JarvisUI:
|
||||
def __init__(self, face_path, size=(760, 760)):
|
||||
self.root = tk.Tk()
|
||||
self.root.title("J.A.R.V.I.S")
|
||||
self.root.resizable(False, False)
|
||||
self.root.geometry("760x900")
|
||||
self.root.configure(bg="#000000")
|
||||
|
||||
self.size = size
|
||||
self.center_y = 0.42
|
||||
|
||||
self.canvas = tk.Canvas(
|
||||
self.root,
|
||||
width=size[0],
|
||||
height=size[1],
|
||||
bg="#000000",
|
||||
highlightthickness=0
|
||||
)
|
||||
self.canvas.place(relx=0.5, rely=self.center_y, anchor="center")
|
||||
|
||||
self.face_base = (
|
||||
Image.open(face_path)
|
||||
.convert("RGBA")
|
||||
.resize(size, Image.LANCZOS)
|
||||
)
|
||||
|
||||
self.halo_base = self._create_halo(size, radius=220, y_offset=-50)
|
||||
|
||||
self.speaking = False
|
||||
self.scale = 1.0
|
||||
self.target_scale = 1.0
|
||||
self.halo_alpha = 70
|
||||
self.target_halo_alpha = 70
|
||||
self.last_target_time = time.time()
|
||||
|
||||
self.text_box = ScrolledText(
|
||||
self.root,
|
||||
fg="#8ffcff",
|
||||
bg="#000000",
|
||||
insertbackground="#8ffcff",
|
||||
height=12,
|
||||
borderwidth=0,
|
||||
wrap="word",
|
||||
font=("Consolas", 10),
|
||||
padx=12,
|
||||
pady=12
|
||||
)
|
||||
self.text_box.place(relx=0.5, rely=0.86, anchor="center")
|
||||
self.text_box.configure(state="disabled")
|
||||
|
||||
self.typing_queue = deque()
|
||||
self.is_typing = False
|
||||
|
||||
self._animate()
|
||||
self.root.protocol("WM_DELETE_WINDOW", lambda: os._exit(0))
|
||||
|
||||
def _create_halo(self, size, radius, y_offset):
|
||||
w, h = size
|
||||
img = Image.new("RGBA", size, (0, 0, 0, 0))
|
||||
draw = ImageDraw.Draw(img)
|
||||
|
||||
cx = w // 2
|
||||
cy = h // 2 + y_offset
|
||||
|
||||
for r in range(radius, 0, -12):
|
||||
alpha = int(70 * (1 - r / radius))
|
||||
draw.ellipse(
|
||||
(cx - r, cy - r, cx + r, cy + r),
|
||||
fill=(0, 180, 255, alpha)
|
||||
)
|
||||
|
||||
return img.filter(ImageFilter.GaussianBlur(30))
|
||||
|
||||
def write_log(self, text: str):
|
||||
self.typing_queue.append(text)
|
||||
if not self.is_typing:
|
||||
self._start_typing()
|
||||
|
||||
def _start_typing(self):
|
||||
if not self.typing_queue:
|
||||
self.is_typing = False
|
||||
return
|
||||
|
||||
self.is_typing = True
|
||||
text = self.typing_queue.popleft()
|
||||
|
||||
self.text_box.configure(state="normal")
|
||||
self._type_char(text, 0)
|
||||
|
||||
def _type_char(self, text, i):
|
||||
if i < len(text):
|
||||
self.text_box.insert(tk.END, text[i])
|
||||
self.text_box.see(tk.END)
|
||||
self.root.after(12, self._type_char, text, i + 1)
|
||||
else:
|
||||
self.text_box.insert(tk.END, "\n")
|
||||
self.text_box.configure(state="disabled")
|
||||
self.root.after(40, self._start_typing)
|
||||
|
||||
def start_speaking(self):
|
||||
self.speaking = True
|
||||
|
||||
def stop_speaking(self):
|
||||
self.speaking = False
|
||||
|
||||
def _animate(self):
|
||||
now = time.time()
|
||||
|
||||
if now - self.last_target_time > (0.25 if self.speaking else 0.7):
|
||||
if self.speaking:
|
||||
self.target_scale = random.uniform(1.02, 1.1)
|
||||
self.target_halo_alpha = random.randint(120, 150)
|
||||
else:
|
||||
self.target_scale = random.uniform(1.004, 1.012)
|
||||
self.target_halo_alpha = random.randint(60, 80)
|
||||
|
||||
self.last_target_time = now
|
||||
|
||||
scale_speed = 0.45 if self.speaking else 0.25
|
||||
halo_speed = 0.40 if self.speaking else 0.25
|
||||
|
||||
self.scale += (self.target_scale - self.scale) * scale_speed
|
||||
self.halo_alpha += (self.target_halo_alpha - self.halo_alpha) * halo_speed
|
||||
|
||||
frame = Image.new("RGBA", self.size, (0, 0, 0, 255))
|
||||
|
||||
halo = self.halo_base.copy()
|
||||
halo.putalpha(int(self.halo_alpha))
|
||||
frame.alpha_composite(halo)
|
||||
|
||||
w, h = self.size
|
||||
face = self.face_base.resize(
|
||||
(int(w * self.scale), int(h * self.scale)),
|
||||
Image.LANCZOS
|
||||
)
|
||||
|
||||
fx = (w - face.size[0]) // 2
|
||||
fy = (h - face.size[1]) // 2
|
||||
frame.alpha_composite(face, (fx, fy))
|
||||
|
||||
img = ImageTk.PhotoImage(frame)
|
||||
self.canvas.delete("all")
|
||||
self.canvas.create_image(w // 2, h // 2, image=img)
|
||||
self.canvas.image = img
|
||||
|
||||
self.root.after(16, self._animate)
|
||||
Reference in new issue
Block a user