sentinel-ai-os 1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (48) hide show
  1. sentinel/__init__.py +0 -0
  2. sentinel/auth.py +40 -0
  3. sentinel/cli.py +9 -0
  4. sentinel/core/__init__.py +0 -0
  5. sentinel/core/agent.py +298 -0
  6. sentinel/core/audit.py +48 -0
  7. sentinel/core/cognitive.py +94 -0
  8. sentinel/core/config.py +99 -0
  9. sentinel/core/llm.py +143 -0
  10. sentinel/core/registry.py +351 -0
  11. sentinel/core/scheduler.py +61 -0
  12. sentinel/core/schema.py +11 -0
  13. sentinel/core/setup.py +101 -0
  14. sentinel/core/ui.py +112 -0
  15. sentinel/main.py +110 -0
  16. sentinel/paths.py +77 -0
  17. sentinel/tools/__init__.py +0 -0
  18. sentinel/tools/apps.py +462 -0
  19. sentinel/tools/audio.py +30 -0
  20. sentinel/tools/browser.py +66 -0
  21. sentinel/tools/calendar_ops.py +163 -0
  22. sentinel/tools/clock.py +25 -0
  23. sentinel/tools/context.py +40 -0
  24. sentinel/tools/desktop.py +116 -0
  25. sentinel/tools/email_ops.py +62 -0
  26. sentinel/tools/factory.py +125 -0
  27. sentinel/tools/file_ops.py +81 -0
  28. sentinel/tools/flights.py +62 -0
  29. sentinel/tools/gmail_auth.py +47 -0
  30. sentinel/tools/indexer.py +156 -0
  31. sentinel/tools/installer.py +69 -0
  32. sentinel/tools/macros.py +58 -0
  33. sentinel/tools/memory_ops.py +281 -0
  34. sentinel/tools/navigation.py +109 -0
  35. sentinel/tools/notes.py +78 -0
  36. sentinel/tools/office.py +67 -0
  37. sentinel/tools/organizer.py +150 -0
  38. sentinel/tools/smart_index.py +76 -0
  39. sentinel/tools/sql_index.py +186 -0
  40. sentinel/tools/system_ops.py +86 -0
  41. sentinel/tools/vision.py +94 -0
  42. sentinel/tools/weather_ops.py +59 -0
  43. sentinel_ai_os-1.0.dist-info/METADATA +282 -0
  44. sentinel_ai_os-1.0.dist-info/RECORD +48 -0
  45. sentinel_ai_os-1.0.dist-info/WHEEL +5 -0
  46. sentinel_ai_os-1.0.dist-info/entry_points.txt +2 -0
  47. sentinel_ai_os-1.0.dist-info/licenses/LICENSE +21 -0
  48. sentinel_ai_os-1.0.dist-info/top_level.txt +1 -0
sentinel/__init__.py ADDED
File without changes
sentinel/auth.py ADDED
@@ -0,0 +1,40 @@
1
+ # sentinel/auth.py
2
+
3
+ from google_auth_oauthlib.flow import InstalledAppFlow
4
+ from sentinel.paths import CREDS_FILE, TOKEN_FILE
5
+
6
+ SCOPES = [
7
+ 'https://www.googleapis.com/auth/gmail.readonly',
8
+ 'https://www.googleapis.com/auth/gmail.send',
9
+ 'https://www.googleapis.com/auth/calendar'
10
+ ]
11
+
12
+
13
+ def fix_authentication():
14
+ print("--- SENTINEL GOOGLE AUTH ---\n")
15
+
16
+ if not CREDS_FILE.exists():
17
+ print("❌ credentials.json not found.\n")
18
+ print("Download it from Google Cloud Console and place it here:\n")
19
+ print(CREDS_FILE)
20
+ return
21
+
22
+ if TOKEN_FILE.exists():
23
+ print("⚠ Found old token. Deleting...")
24
+ TOKEN_FILE.unlink()
25
+
26
+ print("Opening browser for Google login...\n")
27
+
28
+ flow = InstalledAppFlow.from_client_secrets_file(
29
+ str(CREDS_FILE),
30
+ SCOPES
31
+ )
32
+
33
+ creds = flow.run_local_server(port=0)
34
+
35
+ with open(TOKEN_FILE, "w") as f:
36
+ f.write(creds.to_json())
37
+
38
+ print("\n✅ Authentication complete.")
39
+ print("Token saved to:")
40
+ print(TOKEN_FILE)
sentinel/cli.py ADDED
@@ -0,0 +1,9 @@
1
+ from sentinel.main import app
2
+ from sentinel.core.config import ConfigManager
3
+ from sentinel.core.setup import setup_wizard
4
+
5
+ def main():
6
+ cfg = ConfigManager()
7
+ if not cfg.exists():
8
+ setup_wizard()
9
+ app()
File without changes
sentinel/core/agent.py ADDED
@@ -0,0 +1,298 @@
1
+ # FILE: core/agent.py
2
+
3
+ import sys
4
+ import json
5
+ import os
6
+ import shutil
7
+
8
+ from sentinel.core.config import ConfigManager
9
+ from sentinel.core.llm import LLMEngine
10
+ from sentinel.core.registry import TOOLS, SYSTEM_PROMPT
11
+ from sentinel.core.ui import UI
12
+ from sentinel.core.schema import AgentAction
13
+ from sentinel.tools import memory_ops
14
+ from sentinel.paths import USER_DATA_DIR, DB_PATH, VECTOR_PATH, AUDIT_LOG_PATH as AUDIT_LOG
15
+
16
+ # Absolute path to scripts folder
17
+ BASE_DIR = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
18
+ SCRIPTS_DIR = os.path.join(BASE_DIR, "scripts")
19
+
20
+ FACTORY_RESET_SCRIPT = os.path.join(SCRIPTS_DIR, "factory_reset.bat")
21
+ WIPE_VECTOR_SCRIPT = os.path.join(SCRIPTS_DIR, "wipe_vector.bat")
22
+
23
+ class SentinelAgent:
24
+ def __init__(self, config_manager):
25
+ self.config_manager = config_manager
26
+ self.brain = LLMEngine(self.config_manager)
27
+ self.history = []
28
+ self.window_size = self.config_manager.get("memory.window_size", 15)
29
+
30
+ def _parse_action(self, text) -> AgentAction | None:
31
+ json_data = None
32
+
33
+ try:
34
+ json_data = json.loads(text)
35
+ except:
36
+ # Extract first valid JSON object using a stack
37
+ start = text.find("{")
38
+ if start == -1:
39
+ return None
40
+
41
+ stack = 0
42
+ for i in range(start, len(text)):
43
+ if text[i] == "{":
44
+ stack += 1
45
+ elif text[i] == "}":
46
+ stack -= 1
47
+ if stack == 0:
48
+ try:
49
+ json_data = json.loads(text[start:i + 1])
50
+ break
51
+ except:
52
+ return None
53
+
54
+ if not json_data:
55
+ return None
56
+
57
+ try:
58
+ return AgentAction(**json_data)
59
+ except:
60
+ return None
61
+
62
+ def _enforce_memory_limit(self):
63
+ max_msgs = self.window_size * 2
64
+ while len(self.history) > max_msgs:
65
+ if len(self.history) < 2: break
66
+ old_user = self.history.pop(0)
67
+ old_ai = self.history.pop(0)
68
+ try:
69
+ memory_ops.archive_interaction(old_user.get('content', ''), old_ai.get('content', ''))
70
+ except:
71
+ pass
72
+
73
+ def process_slash_command(self, user_input):
74
+ if not user_input.startswith("/"): return False
75
+ parts = user_input[1:].split()
76
+ cmd = parts[0].lower()
77
+ args = parts[1:]
78
+
79
+ if cmd == "memory":
80
+ if not args:
81
+ UI.print_system(f"Current Context Window: {self.window_size} turns.")
82
+ else:
83
+ try:
84
+ self.window_size = int(args[0])
85
+ self.config_manager.set("memory.window_size", self.window_size)
86
+ self._enforce_memory_limit()
87
+ UI.print_success(f"Context Window updated to {self.window_size} turns.")
88
+ except:
89
+ UI.print_error("Invalid number.")
90
+ return True
91
+
92
+ if cmd == "wipe":
93
+ if input("⚠️ ARE YOU SURE? This will delete ALL long-term memories. (y/n): ").lower() == 'y':
94
+ UI.print_system("Initiating Wipe...")
95
+
96
+ # 1. Stop background processes
97
+ try:
98
+ from sentinel.core import scheduler
99
+ scheduler.stop_all_jobs()
100
+ except:
101
+ pass
102
+
103
+ # 2. Release Database Locks
104
+ try:
105
+ memory_ops.teardown()
106
+ except:
107
+ pass
108
+
109
+ # 3. Delete the specific files (Cross-Platform)
110
+ UI.print_system(f"Wiping data from {USER_DATA_DIR}...")
111
+
112
+ # Delete Vector DB Folder
113
+ if os.path.exists(VECTOR_PATH):
114
+ try:
115
+ shutil.rmtree(VECTOR_PATH)
116
+ UI.print_success("Deleted Vector Memory.")
117
+ except Exception as e:
118
+ UI.print_error(f"Failed to delete vectors: {e}")
119
+
120
+ # Delete SQLite DB
121
+ if os.path.exists(DB_PATH):
122
+ try:
123
+ os.remove(DB_PATH)
124
+ UI.print_success("Deleted Brain Database.")
125
+ except Exception as e:
126
+ UI.print_error(f"Failed to delete DB: {e}")
127
+
128
+ UI.print_system("Wipe Complete. Restarting Sentinel is recommended.")
129
+ sys.exit(0)
130
+
131
+ return True
132
+
133
+ if cmd == "factory_reset":
134
+ confirm = input("⚠️ This will DELETE EVERYTHING (Config, Keys, Memories). Type YES to confirm: ")
135
+ if confirm == "YES":
136
+ try:
137
+ # Teardown everything
138
+ from sentinel.core import scheduler
139
+ scheduler.stop_all_jobs()
140
+ memory_ops.teardown()
141
+
142
+ # Nuke the entire .sentinel folder
143
+ if os.path.exists(USER_DATA_DIR):
144
+ shutil.rmtree(USER_DATA_DIR)
145
+ print(f"✔ Deleted {USER_DATA_DIR}")
146
+
147
+ print("Factory Reset Complete. Sentinel is now fresh.")
148
+ sys.exit(0)
149
+ except Exception as e:
150
+ print(f"Reset failed: {e}")
151
+ # Fallback: If locked, tell user to delete manually
152
+ print(f"Please manually delete this folder: {USER_DATA_DIR}")
153
+ return True
154
+
155
+ if cmd == "status":
156
+ UI.print_agent(
157
+ f"**Provider:** {self.brain.provider.upper()}\n**Model:** {self.brain.model}\n**Window:** {self.window_size} turns\n**Active Memory:** {len(self.history)} messages",
158
+ model=self.brain.model
159
+ )
160
+ return True
161
+
162
+ if cmd == "clear":
163
+ self.history = []
164
+ UI.print_success("Short-term memory cleared.")
165
+ return True
166
+
167
+ if cmd == "help":
168
+ UI.print_help()
169
+ return True
170
+
171
+ if cmd in ["switch", "model"]:
172
+ # (Keeping it brief for copy-paste)
173
+ provider = args[0] if args else "openai"
174
+ model = args[1] if len(args) > 1 else None
175
+ from sentinel.tools import system_ops
176
+ res = system_ops.switch_model(provider, model)
177
+ self.brain.reload_config(verbose=True)
178
+ UI.console.print(res)
179
+ return True
180
+
181
+ if cmd == "log":
182
+ if not args:
183
+ state = ConfigManager().get("system.audit_logging", True)
184
+ UI.print_system(f"Logging is currently: {'ON' if state else 'OFF'}")
185
+ return True
186
+
187
+ mode = args[0].lower()
188
+ if mode in ["on", "true", "enable"]:
189
+ from sentinel.core.audit import audit
190
+ UI.print_success(audit.toggle(True))
191
+ elif mode in ["off", "false", "disable"]:
192
+ from sentinel.core.audit import audit
193
+ UI.print_success(audit.toggle(False))
194
+ return True
195
+
196
+ if cmd == "setkey":
197
+ if len(args) < 2: return False
198
+ self.config_manager.set_key(args[0], args[1])
199
+ self.brain.reload_config()
200
+ UI.print_success("Key updated.")
201
+ return True
202
+
203
+ if cmd == "auth":
204
+ from sentinel.auth import fix_authentication
205
+ fix_authentication()
206
+ return True
207
+
208
+ if cmd == "config":
209
+ from sentinel.core.setup import setup_wizard
210
+ setup_wizard()
211
+ return True
212
+
213
+ return False
214
+
215
+ def run_loop(self):
216
+ if self.config_manager.get_key(self.brain.provider):
217
+ UI.print_system("Systems Online. Waiting for input...")
218
+
219
+ while True:
220
+ try:
221
+ user_input = UI.get_input()
222
+ if not user_input:
223
+ continue
224
+
225
+ if user_input.lower().strip() in ["exit", "quit"]:
226
+ sys.exit(0)
227
+
228
+ if self.process_slash_command(user_input):
229
+ continue
230
+
231
+ # ---- Recall Memory ----
232
+ relevant_context = memory_ops.retrieve_relevant_context(user_input)
233
+ current_sys = SYSTEM_PROMPT
234
+ if relevant_context:
235
+ current_sys += f"\n\n[RECALLED MEMORIES]\n{relevant_context}\n"
236
+
237
+ self.history.append({"role": "user", "content": user_input})
238
+ memory_ops.log_activity("chat", user_input)
239
+
240
+ for _ in range(5):
241
+ messages = self.history[-self.window_size * 2:]
242
+ full_resp = self.brain.query(current_sys, messages)
243
+ action = self._parse_action(full_resp)
244
+
245
+ # ---- Normal LLM response ----
246
+ if not action:
247
+ clean = full_resp.replace("```json", "").replace("```", "").strip()
248
+
249
+ if not clean:
250
+ clean = "I don't have any stored long-term information about you yet."
251
+
252
+ UI.print_agent(clean, model=self.brain.model)
253
+
254
+ if full_resp and full_resp.strip():
255
+ self.history.append({"role": "assistant", "content": full_resp})
256
+ break
257
+
258
+ tool, args = action.tool, action.args
259
+
260
+ # ---- Explicit response tool ----
261
+ if tool == "response":
262
+ text = args.get("text", "").strip()
263
+ if not text:
264
+ text = "I don't have any stored long-term information about you yet."
265
+
266
+ UI.print_agent(text, model=self.brain.model)
267
+ self.history.append({"role": "assistant", "content": action.model_dump_json()})
268
+ break
269
+
270
+ # ---- Tool execution ----
271
+ if tool in TOOLS:
272
+ UI.print_tool(tool)
273
+ try:
274
+ res = TOOLS[tool](**args)
275
+
276
+ # UX fallback
277
+ if not res or not str(res).strip():
278
+ res = "No long-term memories stored about you yet."
279
+
280
+ UI.print_result(res)
281
+
282
+ self.history.append({"role": "assistant", "content": action.model_dump_json()})
283
+
284
+ if res and str(res).strip():
285
+ self.history.append({"role": "user", "content": str(res)})
286
+
287
+ except Exception as e:
288
+ UI.print_error(f"Tool Error: {e}")
289
+ self.history.append({"role": "system", "content": f"Error: {e}"})
290
+ else:
291
+ break
292
+
293
+ self._enforce_memory_limit()
294
+
295
+ except KeyboardInterrupt:
296
+ sys.exit(0)
297
+ except Exception as e:
298
+ UI.print_error(f"System Error: {e}")
sentinel/core/audit.py ADDED
@@ -0,0 +1,48 @@
1
+ import time
2
+ import json
3
+ import os
4
+ from datetime import datetime
5
+ from sentinel.core.config import ConfigManager
6
+ from pathlib import Path
7
+
8
+ BASE_DIR = Path.home() / ".sentinel-1"
9
+ BASE_DIR.mkdir(exist_ok=True)
10
+ LOG_FILE = BASE_DIR / "audit_log.jsonl" # JSON Lines for easy parsing
11
+
12
+
13
+ class AuditLogger:
14
+ def __init__(self):
15
+ self.cfg = ConfigManager()
16
+
17
+ def is_enabled(self):
18
+ # Check config (default to False if not set)
19
+ return self.cfg.get("system.audit_logging", False)
20
+
21
+ def toggle(self, state: bool):
22
+ self.cfg.set("system.audit_logging", state)
23
+ status = "ENABLED" if state else "DISABLED"
24
+ return f"Audit Logging is now {status}."
25
+
26
+ def log_event(self, event_type, provider, model, input_data, output_data, duration_ms):
27
+ if not self.is_enabled():
28
+ return
29
+
30
+ entry = {
31
+ "timestamp": datetime.now().isoformat(),
32
+ "event": event_type,
33
+ "provider": provider,
34
+ "model": model,
35
+ "duration_ms": round(duration_ms, 2),
36
+ "input": str(input_data)[:2000], # Truncate massive inputs
37
+ "output": str(output_data)
38
+ }
39
+
40
+ try:
41
+ with open(LOG_FILE, "a", encoding="utf-8") as f:
42
+ f.write(json.dumps(entry) + "\n")
43
+ except Exception as e:
44
+ print(f"Logger Error: {e}")
45
+
46
+
47
+ # Global Instance
48
+ audit = AuditLogger()
@@ -0,0 +1,94 @@
1
+ import datetime
2
+ from sentinel.core.llm import LLMEngine
3
+ from sentinel.tools import calendar_ops, weather_ops, memory_ops, notes, email_ops
4
+
5
+
6
+ def get_daily_briefing(config_manager):
7
+ """
8
+ The 'Brain Boot' sequence.
9
+ Gathers context -> Sends to LLM -> Returns Morning Briefing.
10
+ """
11
+ print("🧠 Gathering Daily Context...")
12
+
13
+ # 1. Get Date/Time
14
+ now = datetime.datetime.now()
15
+ date_str = now.strftime("%A, %B %d")
16
+
17
+ # 2. Get Weather
18
+ settings = config_manager.load()
19
+ location = "New York"
20
+
21
+ if "weather" in settings and "location" in settings["weather"]:
22
+ location = settings["weather"]["location"]
23
+ elif "location" in settings:
24
+ location = settings["location"]
25
+
26
+ try:
27
+ weather = weather_ops.get_current_weather(location)
28
+ except Exception as e:
29
+ weather = f"Weather unavailable ({e})."
30
+
31
+ # 3. Get Calendar (Next 24h)
32
+ try:
33
+ cal_events = calendar_ops.list_upcoming_events(limit=5)
34
+ except Exception:
35
+ cal_events = "Calendar access unavailable."
36
+
37
+ # 4. Get Recent Emails
38
+ try:
39
+ emails = email_ops.read_emails(limit=5)
40
+ except Exception:
41
+ emails = "Email access unavailable."
42
+
43
+ # 5. Get Todo List
44
+ try:
45
+ todos = notes.list_notes("todo")
46
+ except Exception:
47
+ todos = "No notes found."
48
+
49
+ # 6. Get Yesterday's Reflection
50
+ try:
51
+ yesterday = (now - datetime.timedelta(days=1)).strftime("%Y-%m-%d")
52
+ history = memory_ops.reflect_on_day(yesterday)
53
+ except Exception:
54
+ history = "Memory unavailable."
55
+
56
+ # 7. Synthesis Prompt
57
+ context = f"""
58
+ DATE: {date_str}
59
+ LOCATION: {location}
60
+
61
+ WEATHER:
62
+ {weather}
63
+
64
+ CALENDAR:
65
+ {cal_events}
66
+
67
+ RECENT EMAILS:
68
+ {emails}
69
+
70
+ PENDING TASKS:
71
+ {todos}
72
+
73
+ YESTERDAY'S HISTORY:
74
+ {history}
75
+ """
76
+
77
+ sys_prompt = (
78
+ "You are Sentinel, a proactive Chief of Staff. "
79
+ "Summarize this context into a high-level Morning Briefing. "
80
+ "1. Highlight immediate calendar priorities. "
81
+ "2. Mention important or urgent emails. "
82
+ "3. Note the weather if it impacts travel. "
83
+ "4. Remind me of unfinished tasks. "
84
+ "5. Briefly recall where we left off yesterday. "
85
+ "Be concise, strategic, and motivating."
86
+ )
87
+
88
+ brain = LLMEngine(config_manager)
89
+
90
+ if not brain.api_key and brain.provider != "ollama":
91
+ return f"⚠️ Briefing skipped: No API key for {brain.provider.upper()}."
92
+
93
+ report = brain.query(sys_prompt, [{"role": "user", "content": context}])
94
+ return report
@@ -0,0 +1,99 @@
1
+ # FILE: sentinel/core/config.py
2
+ import json
3
+ import os
4
+ import keyring
5
+ from typing import Any, Optional
6
+ from sentinel.paths import CONFIG_PATH # <-- IMPORTED FROM CENTRAL PATHS
7
+
8
+ APP_NAME = "sentinel-1"
9
+
10
+
11
+ class ConfigManager:
12
+ def __init__(self):
13
+ self._ensure_config_exists()
14
+
15
+ def _ensure_config_exists(self):
16
+ # Check if the file exists at ~/.sentinel/config.json
17
+ if not CONFIG_PATH.exists():
18
+ default_config = {
19
+ "user": {"name": "User", "location": "New York"},
20
+ "system": {"setup_completed": False, "audit_logging": False},
21
+ "llm": {"provider": "openai", "model": "gpt-4o"}
22
+ }
23
+ self.save(default_config)
24
+
25
+ def exists(self) -> bool:
26
+ """Checks if setup has been completed."""
27
+ return self.get("system.setup_completed", False)
28
+
29
+ def load(self) -> dict:
30
+ try:
31
+ # CONFIG_PATH is a Path object, open() handles it natively
32
+ with open(CONFIG_PATH, "r") as f:
33
+ return json.load(f)
34
+ except (FileNotFoundError, json.JSONDecodeError):
35
+ return {}
36
+
37
+ def save(self, data: dict):
38
+ # Ensure the directory exists (just in case the folder was deleted manually)
39
+ CONFIG_PATH.parent.mkdir(parents=True, exist_ok=True)
40
+
41
+ with open(CONFIG_PATH, "w") as f:
42
+ json.dump(data, f, indent=2)
43
+
44
+ def get(self, dot_path: str, default: Any = None) -> Any:
45
+ """
46
+ Retrieves a value using dot notation (e.g., "user.name").
47
+ """
48
+ data = self.load()
49
+ keys = dot_path.split(".")
50
+ for key in keys:
51
+ if isinstance(data, dict):
52
+ data = data.get(key)
53
+ else:
54
+ return default
55
+ return data if data is not None else default
56
+
57
+ def set(self, dot_path: str, value: Any):
58
+ """
59
+ Sets a value using dot notation and saves immediately.
60
+ """
61
+ data = self.load()
62
+ keys = dot_path.split(".")
63
+
64
+ # Traverse to the last key
65
+ current = data
66
+ for key in keys[:-1]:
67
+ if key not in current:
68
+ current[key] = {}
69
+ current = current[key]
70
+
71
+ # Set value
72
+ current[keys[-1]] = value
73
+ self.save(data)
74
+
75
+ # --- KEYRING INTEGRATION (Secrets) ---
76
+
77
+ def set_key(self, service: str, api_key: str):
78
+ """Stores a secret in the OS Keychain."""
79
+ if not api_key: return
80
+ try:
81
+ # Service = "openai", "anthropic", etc.
82
+ keyring.set_password(APP_NAME, service, api_key)
83
+ except Exception as e:
84
+ print(f"[Config] Error saving to Keychain: {e}")
85
+
86
+ def get_key(self, service: str) -> Optional[str]:
87
+ """Retrieves a secret from the OS Keychain."""
88
+ try:
89
+ return keyring.get_password(APP_NAME, service)
90
+ except Exception:
91
+ return None
92
+
93
+ def update_llm(self, provider, model):
94
+ """
95
+ Helper used by system_ops.py to switch brains and save to disk.
96
+ """
97
+ self.set("llm.provider", provider)
98
+ self.set("llm.model", model)
99
+ print(f"[Config] Saved new brain settings: {provider} / {model}")