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.
- sentinel/__init__.py +0 -0
- sentinel/auth.py +40 -0
- sentinel/cli.py +9 -0
- sentinel/core/__init__.py +0 -0
- sentinel/core/agent.py +298 -0
- sentinel/core/audit.py +48 -0
- sentinel/core/cognitive.py +94 -0
- sentinel/core/config.py +99 -0
- sentinel/core/llm.py +143 -0
- sentinel/core/registry.py +351 -0
- sentinel/core/scheduler.py +61 -0
- sentinel/core/schema.py +11 -0
- sentinel/core/setup.py +101 -0
- sentinel/core/ui.py +112 -0
- sentinel/main.py +110 -0
- sentinel/paths.py +77 -0
- sentinel/tools/__init__.py +0 -0
- sentinel/tools/apps.py +462 -0
- sentinel/tools/audio.py +30 -0
- sentinel/tools/browser.py +66 -0
- sentinel/tools/calendar_ops.py +163 -0
- sentinel/tools/clock.py +25 -0
- sentinel/tools/context.py +40 -0
- sentinel/tools/desktop.py +116 -0
- sentinel/tools/email_ops.py +62 -0
- sentinel/tools/factory.py +125 -0
- sentinel/tools/file_ops.py +81 -0
- sentinel/tools/flights.py +62 -0
- sentinel/tools/gmail_auth.py +47 -0
- sentinel/tools/indexer.py +156 -0
- sentinel/tools/installer.py +69 -0
- sentinel/tools/macros.py +58 -0
- sentinel/tools/memory_ops.py +281 -0
- sentinel/tools/navigation.py +109 -0
- sentinel/tools/notes.py +78 -0
- sentinel/tools/office.py +67 -0
- sentinel/tools/organizer.py +150 -0
- sentinel/tools/smart_index.py +76 -0
- sentinel/tools/sql_index.py +186 -0
- sentinel/tools/system_ops.py +86 -0
- sentinel/tools/vision.py +94 -0
- sentinel/tools/weather_ops.py +59 -0
- sentinel_ai_os-1.0.dist-info/METADATA +282 -0
- sentinel_ai_os-1.0.dist-info/RECORD +48 -0
- sentinel_ai_os-1.0.dist-info/WHEEL +5 -0
- sentinel_ai_os-1.0.dist-info/entry_points.txt +2 -0
- sentinel_ai_os-1.0.dist-info/licenses/LICENSE +21 -0
- 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
|
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
|
sentinel/core/config.py
ADDED
|
@@ -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}")
|