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/core/llm.py
ADDED
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
# FILE: core/llm.py
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
from openai import OpenAI
|
|
5
|
+
import anthropic
|
|
6
|
+
from sentinel.core.ui import UI
|
|
7
|
+
from sentinel.core.audit import audit
|
|
8
|
+
import time
|
|
9
|
+
|
|
10
|
+
try:
|
|
11
|
+
from groq import Groq
|
|
12
|
+
except ImportError:
|
|
13
|
+
Groq = None
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class LLMEngine:
|
|
17
|
+
def __init__(self, config_manager, verbose=True):
|
|
18
|
+
"""
|
|
19
|
+
Args:
|
|
20
|
+
verbose (bool): If True, prints "Brain Loaded" on init.
|
|
21
|
+
Set False for background tasks.
|
|
22
|
+
"""
|
|
23
|
+
self.cfg_manager = config_manager
|
|
24
|
+
self.provider = None
|
|
25
|
+
self.model = None
|
|
26
|
+
self.api_key = None
|
|
27
|
+
self.reload_config(verbose=verbose)
|
|
28
|
+
|
|
29
|
+
def reload_config(self, verbose=False):
|
|
30
|
+
"""Reloads settings and credentials from the config manager."""
|
|
31
|
+
settings = self.cfg_manager.load()
|
|
32
|
+
llm_settings = settings.get("llm", {})
|
|
33
|
+
|
|
34
|
+
self.provider = llm_settings.get("provider", "openai").lower()
|
|
35
|
+
self.model = llm_settings.get("model", "gpt-4o")
|
|
36
|
+
self.api_key = self.cfg_manager.get_key(self.provider)
|
|
37
|
+
|
|
38
|
+
if verbose:
|
|
39
|
+
is_ready = self.api_key is not None or self.provider == "ollama"
|
|
40
|
+
if is_ready:
|
|
41
|
+
# We can comment this out if we want it strictly in the UI panel now
|
|
42
|
+
# UI.print_system(f"Brain Loaded: [bold cyan]{self.provider.upper()}[/bold cyan] | Model: [bold white]{self.model}[/bold white]")
|
|
43
|
+
pass
|
|
44
|
+
|
|
45
|
+
def stream_query(self, system_prompt, history):
|
|
46
|
+
# Hot-reload config (Silent)
|
|
47
|
+
self.reload_config(verbose=False)
|
|
48
|
+
|
|
49
|
+
if not self.api_key and self.provider != "ollama":
|
|
50
|
+
yield f"\n[bold red]Error:[/bold red] No API key found for '{self.provider}'.\n"
|
|
51
|
+
yield f"Please run: [cyan]/setkey {self.provider} YOUR_KEY_HERE[/cyan]"
|
|
52
|
+
return
|
|
53
|
+
|
|
54
|
+
# --- PREPARE MESSAGES ---
|
|
55
|
+
# 1. System Prompt is separate
|
|
56
|
+
messages = []
|
|
57
|
+
for msg in history:
|
|
58
|
+
# Anthropic hates "system" role in messages list
|
|
59
|
+
if msg["role"] != "system":
|
|
60
|
+
messages.append(msg)
|
|
61
|
+
|
|
62
|
+
try:
|
|
63
|
+
if self.provider == "groq":
|
|
64
|
+
if not Groq:
|
|
65
|
+
yield "Error: 'groq' library not installed. Run 'pip install groq'."
|
|
66
|
+
return
|
|
67
|
+
# Groq (via OpenAI client) EXPECTS system message in list
|
|
68
|
+
groq_msgs = [{"role": "system", "content": system_prompt}] + history
|
|
69
|
+
|
|
70
|
+
client = Groq(api_key=self.api_key)
|
|
71
|
+
stream = client.chat.completions.create(
|
|
72
|
+
messages=groq_msgs, model=self.model, temperature=0.1, stream=True
|
|
73
|
+
)
|
|
74
|
+
for chunk in stream:
|
|
75
|
+
if chunk.choices[0].delta.content:
|
|
76
|
+
yield chunk.choices[0].delta.content
|
|
77
|
+
|
|
78
|
+
elif self.provider == "openai":
|
|
79
|
+
# OpenAI EXPECTS system message in list
|
|
80
|
+
openai_msgs = [{"role": "system", "content": system_prompt}] + history
|
|
81
|
+
|
|
82
|
+
client = OpenAI(api_key=self.api_key)
|
|
83
|
+
stream = client.chat.completions.create(
|
|
84
|
+
model=self.model, messages=openai_msgs, temperature=0.1, stream=True
|
|
85
|
+
)
|
|
86
|
+
for chunk in stream:
|
|
87
|
+
if chunk.choices[0].delta.content:
|
|
88
|
+
yield chunk.choices[0].delta.content
|
|
89
|
+
|
|
90
|
+
elif self.provider == "anthropic":
|
|
91
|
+
# --- ANTHROPIC SPECIFIC FIX ---
|
|
92
|
+
# Pass 'system' as a top-level parameter
|
|
93
|
+
client = anthropic.Anthropic(api_key=self.api_key)
|
|
94
|
+
|
|
95
|
+
with client.messages.stream(
|
|
96
|
+
max_tokens=4096,
|
|
97
|
+
system=system_prompt,
|
|
98
|
+
messages=messages,
|
|
99
|
+
model=self.model
|
|
100
|
+
) as stream:
|
|
101
|
+
for text in stream.text_stream:
|
|
102
|
+
yield text
|
|
103
|
+
|
|
104
|
+
elif self.provider == "ollama":
|
|
105
|
+
# Ollama likes system message in list
|
|
106
|
+
ollama_msgs = [{"role": "system", "content": system_prompt}] + history
|
|
107
|
+
import requests
|
|
108
|
+
import json
|
|
109
|
+
payload = {"model": self.model, "messages": ollama_msgs, "stream": True}
|
|
110
|
+
with requests.post("http://localhost:11434/api/chat", json=payload, stream=True) as r:
|
|
111
|
+
for line in r.iter_lines():
|
|
112
|
+
if line:
|
|
113
|
+
body = json.loads(line)
|
|
114
|
+
if "message" in body and "content" in body["message"]:
|
|
115
|
+
yield body["message"]["content"]
|
|
116
|
+
|
|
117
|
+
except Exception as e:
|
|
118
|
+
error_str = str(e)
|
|
119
|
+
if "401" in error_str or "invalid_api_key" in error_str:
|
|
120
|
+
yield f"\n[bold red]🔑 Authentication Failed:[/bold red] The API key for [cyan]{self.provider.upper()}[/cyan] is invalid or expired."
|
|
121
|
+
elif "429" in error_str or "rate_limit_exceeded" in error_str:
|
|
122
|
+
yield f"\n[bold yellow]⏳ Rate Limit Reached:[/bold yellow] {self.provider.upper()} is busy."
|
|
123
|
+
else:
|
|
124
|
+
yield f"\n[bold red]System Error ({self.provider}):[/bold red] {error_str}"
|
|
125
|
+
|
|
126
|
+
def query(self, system_prompt, history):
|
|
127
|
+
start_time = time.time()
|
|
128
|
+
full_response = ""
|
|
129
|
+
for token in self.stream_query(system_prompt, history):
|
|
130
|
+
full_response += token
|
|
131
|
+
|
|
132
|
+
duration = (time.time() - start_time) * 1000
|
|
133
|
+
|
|
134
|
+
audit.log_event(
|
|
135
|
+
event_type="LLM_QUERY",
|
|
136
|
+
provider=self.provider,
|
|
137
|
+
model=self.model,
|
|
138
|
+
input_data=[{"role": "system", "content": system_prompt}] + history,
|
|
139
|
+
output_data=full_response,
|
|
140
|
+
duration_ms=duration
|
|
141
|
+
)
|
|
142
|
+
|
|
143
|
+
return full_response
|
|
@@ -0,0 +1,351 @@
|
|
|
1
|
+
# FILE: core/registry.py
|
|
2
|
+
|
|
3
|
+
import platform
|
|
4
|
+
import datetime
|
|
5
|
+
import threading
|
|
6
|
+
import os
|
|
7
|
+
from sentinel.core.config import ConfigManager
|
|
8
|
+
import schedule
|
|
9
|
+
from sentinel.tools.smart_index import smart_find
|
|
10
|
+
|
|
11
|
+
# Import all tools
|
|
12
|
+
from sentinel.tools import (
|
|
13
|
+
apps, browser, clock, email_ops, file_ops, indexer, notes, office,
|
|
14
|
+
system_ops, navigation, flights, sql_index, desktop, vision, audio,
|
|
15
|
+
context, calendar_ops, memory_ops, weather_ops, organizer, macros,
|
|
16
|
+
factory, installer
|
|
17
|
+
)
|
|
18
|
+
from sentinel.core import scheduler, cognitive
|
|
19
|
+
|
|
20
|
+
# --- DETECT OS ---
|
|
21
|
+
CURRENT_OS = platform.system()
|
|
22
|
+
OS_VERSION = platform.release()
|
|
23
|
+
NOW = datetime.datetime.now()
|
|
24
|
+
|
|
25
|
+
# --- LOAD CONFIG ---
|
|
26
|
+
cfg = ConfigManager()
|
|
27
|
+
settings = cfg.load() if cfg.exists() else {}
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def initialize_tools():
|
|
31
|
+
print("\n[System] 🔄 Initializing File Systems...")
|
|
32
|
+
|
|
33
|
+
# 1. Fast Filename Indexer (Metadata Only)
|
|
34
|
+
t1 = threading.Thread(target=sql_index.build_index, args=(True,))
|
|
35
|
+
t1.daemon = True
|
|
36
|
+
t1.start()
|
|
37
|
+
|
|
38
|
+
# REMOVE: The content indexer thread (t2)
|
|
39
|
+
|
|
40
|
+
# 3. Schedule recurring updates (Metadata only)
|
|
41
|
+
schedule.every(60).minutes.do(sql_index.build_index, silent=True)
|
|
42
|
+
|
|
43
|
+
scheduler.start_scheduler_service()
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
# --- SAFETY WRAPPERS ---
|
|
47
|
+
|
|
48
|
+
def ask_permission(tool_name, func, **kwargs):
|
|
49
|
+
"""
|
|
50
|
+
Intervention Layer: Pauses execution to ask the user for confirmation.
|
|
51
|
+
"""
|
|
52
|
+
print(f"\n[🛑 SECURITY ALERT] Agent wants to run: {tool_name}")
|
|
53
|
+
# Hide agent_config from display
|
|
54
|
+
display_args = {k: v for k, v in kwargs.items() if k != 'agent_config'}
|
|
55
|
+
print(f" Arguments: {display_args}")
|
|
56
|
+
|
|
57
|
+
choice = input(f" >>> Allow this? (y/N): ").lower()
|
|
58
|
+
|
|
59
|
+
if choice == 'y':
|
|
60
|
+
try:
|
|
61
|
+
# Log successful dangerous actions to memory
|
|
62
|
+
log_args = {k: v for k, v in kwargs.items() if k != 'agent_config'}
|
|
63
|
+
memory_ops.log_activity(tool_name, str(log_args))
|
|
64
|
+
except:
|
|
65
|
+
pass
|
|
66
|
+
return func(**kwargs)
|
|
67
|
+
else:
|
|
68
|
+
return f"Action '{tool_name}' denied by user."
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def safe_run_cmd(cmd):
|
|
72
|
+
"""Extra guardrails for terminal commands."""
|
|
73
|
+
dangerous_keywords = ["del", "rm", "format", "shutdown", "reboot", ">"]
|
|
74
|
+
if any(k in cmd.lower() for k in dangerous_keywords):
|
|
75
|
+
print(f"\n[⚠️ HIGH RISK] Command contains dangerous keywords: '{cmd}'")
|
|
76
|
+
confirm = input(" >>> TYPE 'CONFIRM' TO EXECUTE: ")
|
|
77
|
+
if confirm != "CONFIRM":
|
|
78
|
+
return "Safety block: Command denied."
|
|
79
|
+
|
|
80
|
+
return ask_permission("run_cmd", system_ops.run_cmd, cmd=cmd)
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def draft_code(filename, content):
|
|
84
|
+
"""Safe Coding: Writes to 'drafts/' instead of executing."""
|
|
85
|
+
safe_path = os.path.join("drafts", os.path.basename(filename))
|
|
86
|
+
try:
|
|
87
|
+
with open(safe_path, "w", encoding="utf-8") as f:
|
|
88
|
+
f.write(content)
|
|
89
|
+
return f"Code saved to '{safe_path}'. Review it manually before running."
|
|
90
|
+
except Exception as e:
|
|
91
|
+
return f"Error drafting code: {e}"
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
# --- TOOL MAPPING ---
|
|
95
|
+
TOOLS = {
|
|
96
|
+
# System & Apps
|
|
97
|
+
"open_app": apps.open_app,
|
|
98
|
+
"close_app": lambda **k: ask_permission("close_app", apps.close_app, **k),
|
|
99
|
+
"run_cmd": safe_run_cmd,
|
|
100
|
+
"get_clipboard": system_ops.get_clipboard,
|
|
101
|
+
"kill_process": lambda **k: ask_permission("kill_process", system_ops.kill_process, **k),
|
|
102
|
+
"get_system_stats": system_ops.get_system_stats,
|
|
103
|
+
"play_music": apps.play_music,
|
|
104
|
+
|
|
105
|
+
# Browser
|
|
106
|
+
"search_web": browser.search_web,
|
|
107
|
+
"open_url": browser.open_url,
|
|
108
|
+
"read_webpage": browser.read_webpage,
|
|
109
|
+
|
|
110
|
+
# Files & Index
|
|
111
|
+
"read_file": file_ops.read_file,
|
|
112
|
+
"write_file": file_ops.write_file,
|
|
113
|
+
"draft_code": draft_code,
|
|
114
|
+
"build_index": indexer.build_index,
|
|
115
|
+
"search_index": indexer.search_index,
|
|
116
|
+
"find_file": sql_index.search_db,
|
|
117
|
+
"rebuild_memory": sql_index.build_index,
|
|
118
|
+
"organize_files": lambda **k: ask_permission("organize_files", organizer.organize_files, **k),
|
|
119
|
+
"bulk_rename": lambda **k: ask_permission("bulk_rename", organizer.bulk_rename, **k),
|
|
120
|
+
|
|
121
|
+
# Office & Documents
|
|
122
|
+
"create_word": office.create_word,
|
|
123
|
+
"create_excel": office.create_excel,
|
|
124
|
+
"append_excel": office.append_excel,
|
|
125
|
+
"read_excel": office.read_excel,
|
|
126
|
+
"create_document": factory.create_document,
|
|
127
|
+
|
|
128
|
+
# Time & Email
|
|
129
|
+
"get_time": clock.get_time,
|
|
130
|
+
"set_timer": clock.set_timer,
|
|
131
|
+
"set_alarm": clock.set_alarm,
|
|
132
|
+
"send_email": lambda **k: ask_permission("send_email", email_ops.send_email, **k),
|
|
133
|
+
"read_emails": email_ops.read_emails,
|
|
134
|
+
|
|
135
|
+
# Memory & Cognitive
|
|
136
|
+
"add_note": notes.add_note,
|
|
137
|
+
"list_notes": notes.list_notes,
|
|
138
|
+
"store_fact": memory_ops.store_fact,
|
|
139
|
+
"delete_fact": memory_ops.delete_fact,
|
|
140
|
+
"retrieve_knowledge": lambda **kwargs: memory_ops.retrieve_relevant_context(
|
|
141
|
+
query=" ".join([str(v) for v in kwargs.values() if v])
|
|
142
|
+
),
|
|
143
|
+
|
|
144
|
+
"reflect_on_day": memory_ops.reflect_on_day,
|
|
145
|
+
"daily_briefing": lambda: cognitive.get_daily_briefing(cfg),
|
|
146
|
+
|
|
147
|
+
# Navigation & Flights
|
|
148
|
+
"geocode": navigation.geocode,
|
|
149
|
+
"reverse_geocode": navigation.reverse_geocode,
|
|
150
|
+
"calc_distance": navigation.calc_distance,
|
|
151
|
+
"get_directions": navigation.get_directions,
|
|
152
|
+
"find_nearby": navigation.find_nearby,
|
|
153
|
+
"search_flights": flights.search_flights,
|
|
154
|
+
|
|
155
|
+
# Desktop Control
|
|
156
|
+
"set_volume": desktop.set_volume,
|
|
157
|
+
"set_brightness": desktop.set_brightness,
|
|
158
|
+
"minimize_window": desktop.minimize_window,
|
|
159
|
+
"maximize_window": desktop.maximize_window,
|
|
160
|
+
"type_text": desktop.type_text,
|
|
161
|
+
"speak": desktop.speak,
|
|
162
|
+
|
|
163
|
+
# Perception
|
|
164
|
+
"listen": audio.listen,
|
|
165
|
+
"analyze_screen": vision.analyze_screen,
|
|
166
|
+
"capture_webcam": vision.capture_webcam,
|
|
167
|
+
"get_active_app": context.get_active_app,
|
|
168
|
+
"get_weather": weather_ops.get_current_weather,
|
|
169
|
+
|
|
170
|
+
# Autonomy (Scheduler)
|
|
171
|
+
"schedule_task": lambda interval, task: ask_permission(
|
|
172
|
+
"schedule_task",
|
|
173
|
+
scheduler.schedule_task,
|
|
174
|
+
interval_minutes=interval,
|
|
175
|
+
task_description=task,
|
|
176
|
+
agent_config=settings
|
|
177
|
+
),
|
|
178
|
+
"stop_tasks": scheduler.stop_all_jobs,
|
|
179
|
+
|
|
180
|
+
# Calendar
|
|
181
|
+
"list_calendar_events": calendar_ops.list_upcoming_events,
|
|
182
|
+
"get_calendar_range": calendar_ops.get_events_in_frame,
|
|
183
|
+
"create_calendar_event": lambda **k: ask_permission("create_event", calendar_ops.create_event, **k),
|
|
184
|
+
"calendar_quick_add": lambda **k: ask_permission("quick_add", calendar_ops.quick_add, **k),
|
|
185
|
+
|
|
186
|
+
# Macros & Installers
|
|
187
|
+
"run_macro": macros.run_macro,
|
|
188
|
+
"install_software": installer.install_software,
|
|
189
|
+
"list_installed_apps": installer.list_installed,
|
|
190
|
+
|
|
191
|
+
"find_my_file": lambda query: "\n".join(smart_find(query)),
|
|
192
|
+
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
# --- PROMPT ---
|
|
196
|
+
# Note: JSON examples use DOUBLE BRACES {{ }} to escape f-string formatting
|
|
197
|
+
SYSTEM_PROMPT = f"""
|
|
198
|
+
You are **Sentinel**, an autonomous AI Operating System layer.
|
|
199
|
+
Your role is to translate user intent into safe, deterministic system actions.
|
|
200
|
+
|
|
201
|
+
System Context:
|
|
202
|
+
- OS: {CURRENT_OS} {OS_VERSION}
|
|
203
|
+
- Current Time: {NOW}
|
|
204
|
+
|
|
205
|
+
USER PROFILE:
|
|
206
|
+
Name: {cfg.get("user.name")}
|
|
207
|
+
Location: {cfg.get("user.location")}
|
|
208
|
+
|
|
209
|
+
CORE BEHAVIOR:
|
|
210
|
+
1. You operate in **command mode**, not conversation mode.
|
|
211
|
+
2. Your output must be valid JSON for tool execution.
|
|
212
|
+
3. Never explain tools unless explicitly asked.
|
|
213
|
+
4. Prefer direct action over discussion.
|
|
214
|
+
|
|
215
|
+
DECISION POLICY:
|
|
216
|
+
- If the user intent is ambiguous → ask a minimal clarifying question.
|
|
217
|
+
- If the intent maps to a tool → call it immediately.
|
|
218
|
+
- If no tool fits → respond with {{"tool": "response", "args": {{"text": "..."}}}}
|
|
219
|
+
|
|
220
|
+
CRITICAL SAFETY PROTOCOLS:
|
|
221
|
+
1. **NO CHITCHAT**: Output JSON only.
|
|
222
|
+
2. **HUMAN-IN-THE-LOOP**:
|
|
223
|
+
- Killing processes
|
|
224
|
+
- Running shell commands
|
|
225
|
+
- Sending emails
|
|
226
|
+
- Scheduling tasks
|
|
227
|
+
Must go through `ask_permission`.
|
|
228
|
+
3. **CODE SAFETY**:
|
|
229
|
+
- NEVER execute generated code.
|
|
230
|
+
- ALWAYS use `draft_code`.
|
|
231
|
+
|
|
232
|
+
MEMORY PROTOCOLS:
|
|
233
|
+
1. **Active Listening**:
|
|
234
|
+
If the user reveals a stable preference, habit, goal, or identity:
|
|
235
|
+
→ Immediately call `store_fact`.
|
|
236
|
+
|
|
237
|
+
2. **Context Recall**:
|
|
238
|
+
Before solving complex tasks:
|
|
239
|
+
→ Check `retrieve_knowledge` or `list_notes`.
|
|
240
|
+
|
|
241
|
+
3. **Persistence Triggers**:
|
|
242
|
+
Phrases like:
|
|
243
|
+
- "remember this"
|
|
244
|
+
- "from now on"
|
|
245
|
+
- "in the future"
|
|
246
|
+
Must result in memory storage.
|
|
247
|
+
|
|
248
|
+
AUTONOMY RULES:
|
|
249
|
+
- Background agents must be:
|
|
250
|
+
- Explicitly approved
|
|
251
|
+
- Clearly scoped
|
|
252
|
+
- Easy to stop
|
|
253
|
+
|
|
254
|
+
AVAILABLE TOOLS:
|
|
255
|
+
(only use tools listed below, no hallucinations)
|
|
256
|
+
|
|
257
|
+
SYSTEM & APPS:
|
|
258
|
+
- open_app(name): launch an application
|
|
259
|
+
- close_app(name): close a running application [REQUIRES APPROVAL]
|
|
260
|
+
- run_cmd(cmd): run shell command [REQUIRES APPROVAL]
|
|
261
|
+
- get_clipboard(): get clipboard text
|
|
262
|
+
- kill_process(name): kill a running process [REQUIRES APPROVAL]
|
|
263
|
+
- get_system_stats(): CPU, RAM, battery status
|
|
264
|
+
- play_music(song_name): play music
|
|
265
|
+
|
|
266
|
+
BROWSER:
|
|
267
|
+
- search_web(query): search internet
|
|
268
|
+
- open_url(url): open website
|
|
269
|
+
- read_webpage(url): extract webpage text
|
|
270
|
+
|
|
271
|
+
FILES & INDEX:
|
|
272
|
+
- read_file(path): read file
|
|
273
|
+
- write_file(path, content): write file
|
|
274
|
+
- draft_code(filename, content): save code safely
|
|
275
|
+
- build_index(verbose): scan filesystem
|
|
276
|
+
- search_index(query): semantic file search
|
|
277
|
+
- find_file(query): exact filename search (SQL, literal)
|
|
278
|
+
- find_my_file(query): semantic + recency search (vague queries like "that pdf I edited last week")
|
|
279
|
+
- rebuild_memory(verbose): rebuild index
|
|
280
|
+
- organize_files(directory, strategy): organize files [REQUIRES APPROVAL]
|
|
281
|
+
- bulk_rename(directory, pattern, replace_with): rename files [REQUIRES APPROVAL]
|
|
282
|
+
|
|
283
|
+
OFFICE & DOCUMENTS:
|
|
284
|
+
- create_word(filename, content)
|
|
285
|
+
- create_excel(filename, data_list)
|
|
286
|
+
- append_excel(filename, data_list)
|
|
287
|
+
- read_excel(filename)
|
|
288
|
+
- create_document(filename, blocks)
|
|
289
|
+
|
|
290
|
+
TIME & EMAIL:
|
|
291
|
+
- get_time()
|
|
292
|
+
- set_timer(seconds)
|
|
293
|
+
- set_alarm(time_str)
|
|
294
|
+
- send_email(to, subject, body) [REQUIRES APPROVAL]
|
|
295
|
+
- read_emails(limit)
|
|
296
|
+
|
|
297
|
+
MEMORY & COGNITIVE:
|
|
298
|
+
- add_note(category, content)
|
|
299
|
+
- list_notes(category)
|
|
300
|
+
- store_fact(subject, predicate, obj)
|
|
301
|
+
- delete_fact(subject, predicate, obj): remove stored memory facts
|
|
302
|
+
- retrieve_knowledge(subject, predicate, obj)
|
|
303
|
+
- reflect_on_day(date_str)
|
|
304
|
+
- daily_briefing()
|
|
305
|
+
|
|
306
|
+
NAVIGATION & FLIGHTS:
|
|
307
|
+
- geocode(address)
|
|
308
|
+
- reverse_geocode(lat, lon)
|
|
309
|
+
- calc_distance(origin, destination, mode)
|
|
310
|
+
- get_directions(origin, destination)
|
|
311
|
+
- find_nearby(lat, lon, type)
|
|
312
|
+
- search_flights(departure_id, arrival_id, date)
|
|
313
|
+
|
|
314
|
+
DESKTOP CONTROL:
|
|
315
|
+
- set_volume(level)
|
|
316
|
+
- set_brightness(level)
|
|
317
|
+
- minimize_window(app_name)
|
|
318
|
+
- maximize_window(app_name)
|
|
319
|
+
- type_text(text)
|
|
320
|
+
- speak(text)
|
|
321
|
+
|
|
322
|
+
PERCEPTION:
|
|
323
|
+
- listen(timeout)
|
|
324
|
+
- analyze_screen(prompt)
|
|
325
|
+
- capture_webcam(prompt)
|
|
326
|
+
- get_active_app()
|
|
327
|
+
- get_weather(location): retrieve the current location first/else ask user if cannot find
|
|
328
|
+
|
|
329
|
+
AUTONOMY:
|
|
330
|
+
- schedule_task(interval, task) [REQUIRES APPROVAL]
|
|
331
|
+
- stop_tasks()
|
|
332
|
+
|
|
333
|
+
CALENDAR:
|
|
334
|
+
- list_calendar_events(max_results)
|
|
335
|
+
- get_calendar_range(start_iso, end_iso)
|
|
336
|
+
- create_calendar_event(summary, start_time, duration_mins, description) [REQUIRES APPROVAL]
|
|
337
|
+
- calendar_quick_add(text) [REQUIRES APPROVAL]
|
|
338
|
+
|
|
339
|
+
MACROS & INSTALLERS:
|
|
340
|
+
- run_macro(name)
|
|
341
|
+
- install_software(package_list): install Windows apps via winget
|
|
342
|
+
- list_installed_apps(): list installed applications
|
|
343
|
+
|
|
344
|
+
|
|
345
|
+
FINAL RULE:
|
|
346
|
+
Your response MUST ALWAYS be one of:
|
|
347
|
+
1. Tool call JSON
|
|
348
|
+
2. {{"tool": "response", "args": {{"text": "..."}}}}
|
|
349
|
+
|
|
350
|
+
NO prose. NO markdown. NO explanations.
|
|
351
|
+
"""
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import schedule
|
|
2
|
+
import time
|
|
3
|
+
import threading
|
|
4
|
+
from sentinel.core.llm import LLMEngine
|
|
5
|
+
|
|
6
|
+
# Global registry
|
|
7
|
+
ACTIVE_JOBS = {}
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def _job_runner(task_description, agent_config):
|
|
11
|
+
"""Runs the background task."""
|
|
12
|
+
print(f"\n[Scheduler] ⏰ Executing background task: {task_description}")
|
|
13
|
+
|
|
14
|
+
# Create a transient brain for this task
|
|
15
|
+
brain = LLMEngine(agent_config)
|
|
16
|
+
|
|
17
|
+
# Security: Background agents get a strictly limited prompt
|
|
18
|
+
sys_prompt = f"You are a background monitoring agent. Current Task: {task_description}. Output JSON only."
|
|
19
|
+
|
|
20
|
+
try:
|
|
21
|
+
# We pass an empty history because background tasks should be stateless
|
|
22
|
+
response = brain.query(sys_prompt, [])
|
|
23
|
+
print(f"[Scheduler Result]: {response}")
|
|
24
|
+
except Exception as e:
|
|
25
|
+
print(f"[Scheduler Error]: {e}")
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def schedule_task(interval_minutes, task_description, agent_config):
|
|
29
|
+
"""Schedules a new task."""
|
|
30
|
+
job_id = f"job_{len(ACTIVE_JOBS) + 1}"
|
|
31
|
+
|
|
32
|
+
# Schedule the job
|
|
33
|
+
job = schedule.every(int(interval_minutes)).minutes.do(
|
|
34
|
+
_job_runner, task_description, agent_config
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
ACTIVE_JOBS[job_id] = job
|
|
38
|
+
return f"✅ Scheduled: '{task_description}' every {interval_minutes}m. (ID: {job_id})"
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def stop_all_jobs():
|
|
42
|
+
"""
|
|
43
|
+
EMERGENCY STOP: Clears all background schedules.
|
|
44
|
+
"""
|
|
45
|
+
count = len(ACTIVE_JOBS)
|
|
46
|
+
schedule.clear()
|
|
47
|
+
ACTIVE_JOBS.clear()
|
|
48
|
+
return f"🛑 STOPPED {count} background jobs. Scheduler is now empty."
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def start_scheduler_service():
|
|
52
|
+
"""Starts the loop in a background thread."""
|
|
53
|
+
|
|
54
|
+
def loop():
|
|
55
|
+
while True:
|
|
56
|
+
schedule.run_pending()
|
|
57
|
+
time.sleep(1)
|
|
58
|
+
|
|
59
|
+
t = threading.Thread(target=loop, daemon=True)
|
|
60
|
+
t.start()
|
|
61
|
+
print(">> System: Scheduler Service Online.")
|
sentinel/core/schema.py
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
# FILE: core/schema.py
|
|
2
|
+
from pydantic import BaseModel, Field, ValidationError
|
|
3
|
+
from typing import Dict, Any, Optional
|
|
4
|
+
|
|
5
|
+
class AgentAction(BaseModel):
|
|
6
|
+
tool: str
|
|
7
|
+
args: Dict[str, Any] = Field(default_factory=dict)
|
|
8
|
+
|
|
9
|
+
class Config:
|
|
10
|
+
# Allows the model to ignore extra fields if the LLM hallucinates them
|
|
11
|
+
extra = "ignore"
|
sentinel/core/setup.py
ADDED
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
# sentinel/core/setup.py
|
|
2
|
+
|
|
3
|
+
import time
|
|
4
|
+
from rich.console import Console
|
|
5
|
+
from rich.panel import Panel
|
|
6
|
+
from rich.prompt import Prompt, Confirm
|
|
7
|
+
from rich.progress import Progress, SpinnerColumn, TextColumn
|
|
8
|
+
|
|
9
|
+
from sentinel.core.config import ConfigManager
|
|
10
|
+
from sentinel.paths import CREDENTIALS_PATH as CREDS_FILE
|
|
11
|
+
|
|
12
|
+
console = Console()
|
|
13
|
+
cfg = ConfigManager()
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def print_step(title):
|
|
17
|
+
console.print(f"\n[bold cyan]🔹 {title}[/bold cyan]")
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def setup_wizard():
|
|
21
|
+
console.clear()
|
|
22
|
+
console.print(Panel.fit(
|
|
23
|
+
"[bold white]Welcome to Sentinel OS[/bold white]\n[dim]Autonomous AI Agent System[/dim]",
|
|
24
|
+
border_style="cyan",
|
|
25
|
+
padding=(1, 4)
|
|
26
|
+
))
|
|
27
|
+
|
|
28
|
+
console.print("\n[italic]Let's get your system configured. Press Enter to skip optional fields.[/italic]\n")
|
|
29
|
+
|
|
30
|
+
# 1. User Profile
|
|
31
|
+
print_step("User Profile")
|
|
32
|
+
name = Prompt.ask("What should I call you?", default="User")
|
|
33
|
+
location = Prompt.ask("What is your city? (for weather/time)", default="New York")
|
|
34
|
+
|
|
35
|
+
cfg.set("user.name", name)
|
|
36
|
+
cfg.set("user.location", location)
|
|
37
|
+
|
|
38
|
+
# 2. Intelligence Engine
|
|
39
|
+
print_step("Intelligence Engine")
|
|
40
|
+
provider = Prompt.ask(
|
|
41
|
+
"Select Primary Brain",
|
|
42
|
+
choices=["openai", "anthropic", "groq", "ollama"],
|
|
43
|
+
default="openai"
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
api_key = ""
|
|
47
|
+
if provider != "ollama":
|
|
48
|
+
api_key = Prompt.ask(f"Enter API Key for [cyan]{provider}[/cyan]", password=True)
|
|
49
|
+
|
|
50
|
+
cfg.set("llm.provider", provider)
|
|
51
|
+
|
|
52
|
+
if api_key:
|
|
53
|
+
cfg.set_key(provider, api_key)
|
|
54
|
+
|
|
55
|
+
# 3. Search
|
|
56
|
+
print_step("Search Capabilities")
|
|
57
|
+
console.print("Sentinel uses [bold green]Tavily[/bold green] for advanced research, falling back to DuckDuckGo.")
|
|
58
|
+
|
|
59
|
+
tavily_key = Prompt.ask("Enter Tavily API Key (Optional)", password=True)
|
|
60
|
+
if tavily_key:
|
|
61
|
+
cfg.set_key("tavily", tavily_key)
|
|
62
|
+
|
|
63
|
+
serp_key = Prompt.ask("Enter SerpAPI Key (Optional - for Flights)", password=True)
|
|
64
|
+
if serp_key:
|
|
65
|
+
cfg.set_key("serp_api", serp_key)
|
|
66
|
+
|
|
67
|
+
gmaps_key = Prompt.ask("Enter Google Maps API Key (Optional - for Navigation)", password=True)
|
|
68
|
+
if gmaps_key:
|
|
69
|
+
cfg.set_key("google_maps", gmaps_key)
|
|
70
|
+
|
|
71
|
+
# 4. Google Workspace
|
|
72
|
+
print_step("Google Workspace")
|
|
73
|
+
if Confirm.ask("Do you want to link Gmail/Calendar now?"):
|
|
74
|
+
if CREDS_FILE.exists():
|
|
75
|
+
console.print("[green]✔ credentials.json found.[/green]")
|
|
76
|
+
console.print("Authentication will trigger automatically on first use.")
|
|
77
|
+
else:
|
|
78
|
+
console.print("[red]❌ credentials.json not found.[/red]\n")
|
|
79
|
+
console.print("1. Go to Google Cloud Console")
|
|
80
|
+
console.print("2. Enable Gmail + Calendar APIs")
|
|
81
|
+
console.print("3. Create OAuth Desktop App")
|
|
82
|
+
console.print("4. Download credentials.json")
|
|
83
|
+
console.print("\nPlace it here:\n")
|
|
84
|
+
console.print(f"[bold cyan]{CREDS_FILE}[/bold cyan]")
|
|
85
|
+
console.print("\nThen run: sentinel auth")
|
|
86
|
+
|
|
87
|
+
# 5. Finalize
|
|
88
|
+
with Progress(
|
|
89
|
+
SpinnerColumn(),
|
|
90
|
+
TextColumn("[progress.description]{task.description}"),
|
|
91
|
+
transient=True,
|
|
92
|
+
) as progress:
|
|
93
|
+
task = progress.add_task("Initializing Neural Interfaces...", total=100)
|
|
94
|
+
time.sleep(0.5)
|
|
95
|
+
progress.update(task, advance=50)
|
|
96
|
+
cfg.set("system.setup_completed", True)
|
|
97
|
+
time.sleep(0.5)
|
|
98
|
+
progress.update(task, advance=50)
|
|
99
|
+
|
|
100
|
+
console.print("\n[bold green]✔ Setup Complete.[/bold green] Launching Sentinel...")
|
|
101
|
+
time.sleep(1)
|