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
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
# sentinel/tools/calendar_ops.py
|
|
2
|
+
|
|
3
|
+
import datetime
|
|
4
|
+
from google.auth.transport.requests import Request
|
|
5
|
+
from google.oauth2.credentials import Credentials
|
|
6
|
+
from google_auth_oauthlib.flow import InstalledAppFlow
|
|
7
|
+
from googleapiclient.discovery import build
|
|
8
|
+
import tzlocal
|
|
9
|
+
|
|
10
|
+
from sentinel.paths import CREDENTIALS_PATH as CREDS_FILE, TOKEN_PATH as TOKEN_FILE
|
|
11
|
+
|
|
12
|
+
# If modifying these scopes, delete token.json
|
|
13
|
+
SCOPES = [
|
|
14
|
+
'https://www.googleapis.com/auth/gmail.readonly',
|
|
15
|
+
'https://www.googleapis.com/auth/gmail.send',
|
|
16
|
+
'https://www.googleapis.com/auth/calendar'
|
|
17
|
+
]
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def get_service():
|
|
21
|
+
"""Handles Authentication for both Gmail and Calendar."""
|
|
22
|
+
creds = None
|
|
23
|
+
|
|
24
|
+
# Load existing token
|
|
25
|
+
if TOKEN_FILE.exists():
|
|
26
|
+
creds = Credentials.from_authorized_user_file(str(TOKEN_FILE), SCOPES)
|
|
27
|
+
|
|
28
|
+
# Refresh or login
|
|
29
|
+
if not creds or not creds.valid:
|
|
30
|
+
if creds and creds.expired and creds.refresh_token:
|
|
31
|
+
creds.refresh(Request())
|
|
32
|
+
else:
|
|
33
|
+
if not CREDS_FILE.exists():
|
|
34
|
+
return None
|
|
35
|
+
|
|
36
|
+
flow = InstalledAppFlow.from_client_secrets_file(
|
|
37
|
+
str(CREDS_FILE),
|
|
38
|
+
SCOPES
|
|
39
|
+
)
|
|
40
|
+
creds = flow.run_local_server(port=0)
|
|
41
|
+
|
|
42
|
+
# Save token
|
|
43
|
+
with open(TOKEN_FILE, "w") as token:
|
|
44
|
+
token.write(creds.to_json())
|
|
45
|
+
|
|
46
|
+
return build('calendar', 'v3', credentials=creds)
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def list_upcoming_events(max_results=10):
|
|
50
|
+
try:
|
|
51
|
+
service = get_service()
|
|
52
|
+
if not service:
|
|
53
|
+
return f"Error: credentials.json missing.\nPlace it at:\n{CREDS_FILE}"
|
|
54
|
+
|
|
55
|
+
now = datetime.datetime.utcnow().isoformat() + 'Z'
|
|
56
|
+
|
|
57
|
+
events_result = service.events().list(
|
|
58
|
+
calendarId='primary',
|
|
59
|
+
timeMin=now,
|
|
60
|
+
maxResults=max_results,
|
|
61
|
+
singleEvents=True,
|
|
62
|
+
orderBy='startTime'
|
|
63
|
+
).execute()
|
|
64
|
+
|
|
65
|
+
events = events_result.get('items', [])
|
|
66
|
+
if not events:
|
|
67
|
+
return "No upcoming events found."
|
|
68
|
+
|
|
69
|
+
output = []
|
|
70
|
+
for event in events:
|
|
71
|
+
start = event['start'].get('dateTime', event['start'].get('date'))
|
|
72
|
+
summary = event.get('summary', 'No Title')
|
|
73
|
+
output.append(f"- {start}: {summary}")
|
|
74
|
+
|
|
75
|
+
return "\n".join(output)
|
|
76
|
+
|
|
77
|
+
except Exception as e:
|
|
78
|
+
return f"Calendar Error: {e}"
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def get_events_in_frame(start_iso, end_iso):
|
|
82
|
+
try:
|
|
83
|
+
service = get_service()
|
|
84
|
+
if not service:
|
|
85
|
+
return f"Error: credentials.json missing.\nPlace it at:\n{CREDS_FILE}"
|
|
86
|
+
|
|
87
|
+
if not start_iso.endswith("Z"):
|
|
88
|
+
start_iso += "Z"
|
|
89
|
+
if not end_iso.endswith("Z"):
|
|
90
|
+
end_iso += "Z"
|
|
91
|
+
|
|
92
|
+
events_result = service.events().list(
|
|
93
|
+
calendarId='primary',
|
|
94
|
+
timeMin=start_iso,
|
|
95
|
+
timeMax=end_iso,
|
|
96
|
+
singleEvents=True,
|
|
97
|
+
orderBy='startTime'
|
|
98
|
+
).execute()
|
|
99
|
+
|
|
100
|
+
events = events_result.get('items', [])
|
|
101
|
+
if not events:
|
|
102
|
+
return "No events in that range."
|
|
103
|
+
|
|
104
|
+
return "\n".join([
|
|
105
|
+
f"- {e['start'].get('dateTime', e['start'].get('date'))}: {e.get('summary')}"
|
|
106
|
+
for e in events
|
|
107
|
+
])
|
|
108
|
+
|
|
109
|
+
except Exception as e:
|
|
110
|
+
return f"Error: {e}"
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def create_event(summary, start_time, duration_mins=60, description=""):
|
|
114
|
+
try:
|
|
115
|
+
service = get_service()
|
|
116
|
+
if not service:
|
|
117
|
+
return f"Error: credentials.json missing.\nPlace it at:\n{CREDS_FILE}"
|
|
118
|
+
|
|
119
|
+
start_time = start_time.replace("Z", "")
|
|
120
|
+
start_dt = datetime.datetime.fromisoformat(start_time)
|
|
121
|
+
end_dt = start_dt + datetime.timedelta(minutes=int(duration_mins))
|
|
122
|
+
|
|
123
|
+
local_tz = tzlocal.get_localzone_name()
|
|
124
|
+
|
|
125
|
+
event = {
|
|
126
|
+
'summary': summary,
|
|
127
|
+
'description': description,
|
|
128
|
+
'start': {
|
|
129
|
+
'dateTime': start_dt.isoformat(),
|
|
130
|
+
'timeZone': local_tz,
|
|
131
|
+
},
|
|
132
|
+
'end': {
|
|
133
|
+
'dateTime': end_dt.isoformat(),
|
|
134
|
+
'timeZone': local_tz,
|
|
135
|
+
},
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
event = service.events().insert(
|
|
139
|
+
calendarId='primary',
|
|
140
|
+
body=event
|
|
141
|
+
).execute()
|
|
142
|
+
|
|
143
|
+
return f"Event created: {event.get('htmlLink')}"
|
|
144
|
+
|
|
145
|
+
except Exception as e:
|
|
146
|
+
return f"Error creating event: {e}"
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
def quick_add(text):
|
|
150
|
+
try:
|
|
151
|
+
service = get_service()
|
|
152
|
+
if not service:
|
|
153
|
+
return f"Error: credentials.json missing.\nPlace it at:\n{CREDS_FILE}"
|
|
154
|
+
|
|
155
|
+
created_event = service.events().quickAdd(
|
|
156
|
+
calendarId='primary',
|
|
157
|
+
text=text
|
|
158
|
+
).execute()
|
|
159
|
+
|
|
160
|
+
return f"Quick event created: {created_event.get('htmlLink')}"
|
|
161
|
+
|
|
162
|
+
except Exception as e:
|
|
163
|
+
return f"Error: {e}"
|
sentinel/tools/clock.py
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import time
|
|
2
|
+
import threading
|
|
3
|
+
from plyer import notification
|
|
4
|
+
from datetime import datetime, timedelta
|
|
5
|
+
|
|
6
|
+
def get_time():
|
|
7
|
+
return datetime.now().strftime("%A, %Y-%m-%d %H:%M:%S")
|
|
8
|
+
|
|
9
|
+
def _notify(msg, wait_sec):
|
|
10
|
+
time.sleep(wait_sec)
|
|
11
|
+
notification.notify(title='Sentinel', message=msg, app_name='Sentinel', timeout=10)
|
|
12
|
+
|
|
13
|
+
def set_timer(minutes, message="Timer Done"):
|
|
14
|
+
threading.Thread(target=_notify, args=(message, minutes*60), daemon=True).start()
|
|
15
|
+
return f"Timer set for {minutes} mins."
|
|
16
|
+
|
|
17
|
+
def set_alarm(time_str, message="Alarm"):
|
|
18
|
+
try:
|
|
19
|
+
now = datetime.now()
|
|
20
|
+
tgt = datetime.strptime(time_str, "%H:%M").replace(year=now.year, month=now.month, day=now.day)
|
|
21
|
+
if tgt < now: tgt += timedelta(days=1)
|
|
22
|
+
wait = (tgt - now).total_seconds()
|
|
23
|
+
threading.Thread(target=_notify, args=(message, wait), daemon=True).start()
|
|
24
|
+
return f"Alarm set for {tgt.strftime('%H:%M')}."
|
|
25
|
+
except: return "Invalid format. Use HH:MM."
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import pygetwindow as gw
|
|
2
|
+
import platform
|
|
3
|
+
import psutil
|
|
4
|
+
import time
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def get_active_app():
|
|
8
|
+
"""
|
|
9
|
+
Returns the title and process name of the currently focused window.
|
|
10
|
+
"""
|
|
11
|
+
try:
|
|
12
|
+
if platform.system() == "Windows":
|
|
13
|
+
window = gw.getActiveWindow()
|
|
14
|
+
if not window:
|
|
15
|
+
return "No active window detected."
|
|
16
|
+
|
|
17
|
+
return {
|
|
18
|
+
"title": window.title,
|
|
19
|
+
"app_name": "Unknown (Window API limitation)",
|
|
20
|
+
# Getting precise .exe from window is complex in pure python without win32gui
|
|
21
|
+
"status": "Active"
|
|
22
|
+
}
|
|
23
|
+
else:
|
|
24
|
+
return "Active app detection is currently Windows-optimized."
|
|
25
|
+
|
|
26
|
+
except Exception as e:
|
|
27
|
+
return f"Context Error: {e}"
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def watch_app_switch(interval=5):
|
|
31
|
+
"""
|
|
32
|
+
Monitors for app switches (For the 'digital twin' log).
|
|
33
|
+
"""
|
|
34
|
+
last_window = ""
|
|
35
|
+
while True:
|
|
36
|
+
curr = get_active_app()
|
|
37
|
+
if isinstance(curr, dict) and curr['title'] != last_window:
|
|
38
|
+
print(f"[Context] User switched to: {curr['title']}")
|
|
39
|
+
last_window = curr['title']
|
|
40
|
+
time.sleep(interval)
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import pyautogui
|
|
2
|
+
import pygetwindow as gw
|
|
3
|
+
import screen_brightness_control as sbc
|
|
4
|
+
import platform
|
|
5
|
+
import pyttsx3
|
|
6
|
+
|
|
7
|
+
# Fail-safe
|
|
8
|
+
pyautogui.FAILSAFE = True
|
|
9
|
+
|
|
10
|
+
_ENGINE = None
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def _safe_str(x):
|
|
14
|
+
"""Force any output to be a string (prevents Anthropic crashes)."""
|
|
15
|
+
try:
|
|
16
|
+
return str(x)
|
|
17
|
+
except:
|
|
18
|
+
return "Unknown result"
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def set_volume(level):
|
|
22
|
+
system = platform.system()
|
|
23
|
+
|
|
24
|
+
try:
|
|
25
|
+
target = int(level)
|
|
26
|
+
|
|
27
|
+
# Mute first to calibrate
|
|
28
|
+
pyautogui.press('volumedown', presses=50)
|
|
29
|
+
|
|
30
|
+
clicks = int(target / 2)
|
|
31
|
+
if clicks > 0:
|
|
32
|
+
pyautogui.press('volumeup', presses=clicks)
|
|
33
|
+
|
|
34
|
+
return _safe_str(f"Volume adjusted to approx {target}% (OS: {system}).")
|
|
35
|
+
|
|
36
|
+
except Exception as e:
|
|
37
|
+
return _safe_str(f"Volume control failed: {e}")
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def set_brightness(level):
|
|
41
|
+
try:
|
|
42
|
+
sbc.set_brightness(int(level))
|
|
43
|
+
return _safe_str(f"Brightness set to {level}%.")
|
|
44
|
+
except Exception as e:
|
|
45
|
+
return _safe_str(f"Error setting brightness: {e}")
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def _find_window_fuzzy(name):
|
|
49
|
+
"""Find first window containing name (case-insensitive)."""
|
|
50
|
+
name = name.lower()
|
|
51
|
+
for w in gw.getAllWindows():
|
|
52
|
+
if name in w.title.lower():
|
|
53
|
+
return w
|
|
54
|
+
return None
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def minimize_window(app_name=None):
|
|
58
|
+
try:
|
|
59
|
+
if not app_name:
|
|
60
|
+
win = gw.getActiveWindow()
|
|
61
|
+
else:
|
|
62
|
+
win = _find_window_fuzzy(app_name)
|
|
63
|
+
|
|
64
|
+
if not win:
|
|
65
|
+
return _safe_str(f"No window found for '{app_name}'.")
|
|
66
|
+
|
|
67
|
+
win.minimize()
|
|
68
|
+
return _safe_str(f"Minimized: {win.title}")
|
|
69
|
+
|
|
70
|
+
except Exception as e:
|
|
71
|
+
return _safe_str(f"Minimize error: {e}")
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def maximize_window(app_name=None):
|
|
75
|
+
try:
|
|
76
|
+
if not app_name:
|
|
77
|
+
win = gw.getActiveWindow()
|
|
78
|
+
else:
|
|
79
|
+
win = _find_window_fuzzy(app_name)
|
|
80
|
+
|
|
81
|
+
if not win:
|
|
82
|
+
return _safe_str(f"No window found for '{app_name}'.")
|
|
83
|
+
|
|
84
|
+
win.maximize()
|
|
85
|
+
return _safe_str(f"Maximized: {win.title}")
|
|
86
|
+
|
|
87
|
+
except Exception as e:
|
|
88
|
+
return _safe_str(f"Maximize error: {e}")
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def type_text(text):
|
|
92
|
+
try:
|
|
93
|
+
pyautogui.write(str(text), interval=0.001)
|
|
94
|
+
return _safe_str("Typed text successfully.")
|
|
95
|
+
except Exception as e:
|
|
96
|
+
return _safe_str(f"Typing error: {e}")
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def take_screenshot(filename="screenshot.png"):
|
|
100
|
+
try:
|
|
101
|
+
pyautogui.screenshot(filename)
|
|
102
|
+
return _safe_str(f"Screenshot saved to {filename}")
|
|
103
|
+
except Exception as e:
|
|
104
|
+
return _safe_str(f"Screenshot failed: {e}")
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def speak(text):
|
|
108
|
+
global _ENGINE
|
|
109
|
+
try:
|
|
110
|
+
if _ENGINE is None:
|
|
111
|
+
_ENGINE = pyttsx3.init()
|
|
112
|
+
_ENGINE.say(str(text))
|
|
113
|
+
_ENGINE.runAndWait()
|
|
114
|
+
return _safe_str("Spoken.")
|
|
115
|
+
except Exception as e:
|
|
116
|
+
return _safe_str(f"TTS Error: {e}")
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import base64
|
|
2
|
+
from email.message import EmailMessage
|
|
3
|
+
from sentinel.tools.gmail_auth import get_gmail_service
|
|
4
|
+
import os
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def send_email(to, subject, body):
|
|
9
|
+
if not os.path.exists('credentials.json') and not os.path.exists('token.json'):
|
|
10
|
+
return "Error: Missing 'credentials.json'. Please setup Google OAuth first."
|
|
11
|
+
|
|
12
|
+
try:
|
|
13
|
+
service = get_gmail_service()
|
|
14
|
+
|
|
15
|
+
# Create the email
|
|
16
|
+
message = EmailMessage()
|
|
17
|
+
message.set_content(body)
|
|
18
|
+
message['To'] = to
|
|
19
|
+
message['Subject'] = subject
|
|
20
|
+
|
|
21
|
+
# Encode the message (Base64 required by Gmail API)
|
|
22
|
+
encoded_message = base64.urlsafe_b64encode(message.as_bytes()).decode()
|
|
23
|
+
create_message = {'raw': encoded_message}
|
|
24
|
+
|
|
25
|
+
# Send
|
|
26
|
+
send_message = (service.users().messages().send(userId="me", body=create_message).execute())
|
|
27
|
+
return f"Email sent! ID: {send_message['id']}"
|
|
28
|
+
except Exception as e:
|
|
29
|
+
return f"Error sending email: {e}"
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def read_emails(limit=5):
|
|
33
|
+
if not os.path.exists('credentials.json') and not os.path.exists('token.json'):
|
|
34
|
+
return "Error: Missing 'credentials.json'. Please setup Google OAuth first."
|
|
35
|
+
|
|
36
|
+
try:
|
|
37
|
+
service = get_gmail_service()
|
|
38
|
+
|
|
39
|
+
# List messages (INBOX only)
|
|
40
|
+
results = service.users().messages().list(userId='me', labelIds=['INBOX'], maxResults=limit).execute()
|
|
41
|
+
messages = results.get('messages', [])
|
|
42
|
+
|
|
43
|
+
if not messages:
|
|
44
|
+
return "No emails found."
|
|
45
|
+
|
|
46
|
+
email_summaries = []
|
|
47
|
+
for msg in messages:
|
|
48
|
+
# Fetch full details for each ID
|
|
49
|
+
txt = service.users().messages().get(userId='me', id=msg['id']).execute()
|
|
50
|
+
|
|
51
|
+
# Extract Headers
|
|
52
|
+
headers = txt['payload']['headers']
|
|
53
|
+
subject = next((h['value'] for h in headers if h['name'] == 'Subject'), "No Subject")
|
|
54
|
+
sender = next((h['value'] for h in headers if h['name'] == 'From'), "Unknown")
|
|
55
|
+
snippet = txt.get('snippet', '')
|
|
56
|
+
|
|
57
|
+
email_summaries.append(f"FROM: {sender}\nSUBJECT: {subject}\nSNIPPET: {snippet}\n")
|
|
58
|
+
|
|
59
|
+
return "\n---\n".join(email_summaries)
|
|
60
|
+
|
|
61
|
+
except Exception as e:
|
|
62
|
+
return f"Error reading emails: {e}"
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import platform
|
|
3
|
+
import subprocess
|
|
4
|
+
from docx import Document
|
|
5
|
+
from docx.shared import Pt, Inches
|
|
6
|
+
from docx.enum.text import WD_ALIGN_PARAGRAPH
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def _convert_to_pdf(docx_path):
|
|
10
|
+
"""
|
|
11
|
+
Converts a .docx file to .pdf using LibreOffice (if available) or basic fallback.
|
|
12
|
+
Note: Perfect .docx -> .pdf conversion usually requires Word installed or LibreOffice headless.
|
|
13
|
+
For this MVP, we will try a pure Python fallback if system tools aren't found.
|
|
14
|
+
"""
|
|
15
|
+
try:
|
|
16
|
+
# 1. Try LibreOffice (Best Quality, Cross Platform)
|
|
17
|
+
# This requires LibreOffice to be in your PATH (soffice/libreoffice)
|
|
18
|
+
cmd = f"soffice --headless --convert-to pdf \"{docx_path}\" --outdir \"{os.path.dirname(docx_path)}\""
|
|
19
|
+
if os.system(cmd) == 0:
|
|
20
|
+
return docx_path.replace(".docx", ".pdf")
|
|
21
|
+
|
|
22
|
+
# 2. Try Microsoft Word (Windows Only, requires 'docx2pdf')
|
|
23
|
+
# pip install docx2pdf
|
|
24
|
+
try:
|
|
25
|
+
from docx2pdf import convert
|
|
26
|
+
convert(docx_path)
|
|
27
|
+
return docx_path.replace(".docx", ".pdf")
|
|
28
|
+
except:
|
|
29
|
+
pass
|
|
30
|
+
|
|
31
|
+
return "PDF conversion requires LibreOffice or MS Word installed."
|
|
32
|
+
except Exception as e:
|
|
33
|
+
return f"PDF Error: {e}"
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def create_document(filename, blocks):
|
|
37
|
+
"""
|
|
38
|
+
Universal Document Generator.
|
|
39
|
+
|
|
40
|
+
Args:
|
|
41
|
+
filename (str): Output name (e.g. "Project_Plan")
|
|
42
|
+
blocks (list): A list of dictionaries defining the structure.
|
|
43
|
+
|
|
44
|
+
Block Types:
|
|
45
|
+
- {"type": "heading", "text": "My Title", "level": 1}
|
|
46
|
+
- {"type": "paragraph", "text": "Body content...", "bold": False}
|
|
47
|
+
- {"type": "list", "items": ["Bullet 1", "Bullet 2"], "ordered": False}
|
|
48
|
+
- {"type": "table", "rows": [["Header 1", "Header 2"], ["Cell A", "Cell B"]]}
|
|
49
|
+
- {"type": "page_break"}
|
|
50
|
+
"""
|
|
51
|
+
try:
|
|
52
|
+
doc = Document()
|
|
53
|
+
|
|
54
|
+
# Set default style (Optional)
|
|
55
|
+
style = doc.styles['Normal']
|
|
56
|
+
font = style.font
|
|
57
|
+
font.name = 'Calibri'
|
|
58
|
+
font.size = Pt(11)
|
|
59
|
+
|
|
60
|
+
for block in blocks:
|
|
61
|
+
b_type = block.get("type", "paragraph")
|
|
62
|
+
|
|
63
|
+
# --- HEADING ---
|
|
64
|
+
if b_type == "heading":
|
|
65
|
+
text = block.get("text", "")
|
|
66
|
+
level = block.get("level", 1)
|
|
67
|
+
doc.add_heading(text, level)
|
|
68
|
+
|
|
69
|
+
# --- PARAGRAPH ---
|
|
70
|
+
elif b_type == "paragraph":
|
|
71
|
+
text = block.get("text", "")
|
|
72
|
+
p = doc.add_paragraph()
|
|
73
|
+
run = p.add_run(text)
|
|
74
|
+
if block.get("bold"): run.bold = True
|
|
75
|
+
if block.get("italic"): run.italic = True
|
|
76
|
+
|
|
77
|
+
align = block.get("align", "left").lower()
|
|
78
|
+
if align == "center":
|
|
79
|
+
p.alignment = WD_ALIGN_PARAGRAPH.CENTER
|
|
80
|
+
elif align == "right":
|
|
81
|
+
p.alignment = WD_ALIGN_PARAGRAPH.RIGHT
|
|
82
|
+
elif align == "justify":
|
|
83
|
+
p.alignment = WD_ALIGN_PARAGRAPH.JUSTIFY
|
|
84
|
+
|
|
85
|
+
# --- LISTS ---
|
|
86
|
+
elif b_type == "list":
|
|
87
|
+
items = block.get("items", [])
|
|
88
|
+
style = 'List Number' if block.get("ordered") else 'List Bullet'
|
|
89
|
+
for item in items:
|
|
90
|
+
doc.add_paragraph(item, style=style)
|
|
91
|
+
|
|
92
|
+
# --- TABLES ---
|
|
93
|
+
elif b_type == "table":
|
|
94
|
+
rows = block.get("rows", [])
|
|
95
|
+
if rows:
|
|
96
|
+
cols = len(rows[0])
|
|
97
|
+
table = doc.add_table(rows=0, cols=cols)
|
|
98
|
+
table.style = 'Table Grid'
|
|
99
|
+
|
|
100
|
+
for row_data in rows:
|
|
101
|
+
row_cells = table.add_row().cells
|
|
102
|
+
for i, cell_text in enumerate(row_data):
|
|
103
|
+
if i < len(row_cells):
|
|
104
|
+
row_cells[i].text = str(cell_text)
|
|
105
|
+
|
|
106
|
+
# --- BREAKS ---
|
|
107
|
+
elif b_type == "page_break":
|
|
108
|
+
doc.add_page_break()
|
|
109
|
+
|
|
110
|
+
# Save Logic
|
|
111
|
+
if not os.path.exists("drafts"):
|
|
112
|
+
os.makedirs("drafts")
|
|
113
|
+
|
|
114
|
+
if not filename.endswith(".docx"): filename += ".docx"
|
|
115
|
+
docx_path = os.path.join("drafts", filename)
|
|
116
|
+
|
|
117
|
+
doc.save(docx_path)
|
|
118
|
+
|
|
119
|
+
# Try converting to PDF automatically
|
|
120
|
+
pdf_result = _convert_to_pdf(docx_path)
|
|
121
|
+
|
|
122
|
+
return f"Document created: {docx_path} (PDF Status: {pdf_result})"
|
|
123
|
+
|
|
124
|
+
except Exception as e:
|
|
125
|
+
return f"Document Factory Error: {e}"
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
# FILE: tools/file_ops.py
|
|
2
|
+
import os
|
|
3
|
+
import fitz # PyMuPDF
|
|
4
|
+
import pandas as pd
|
|
5
|
+
from docx import Document
|
|
6
|
+
from sentinel.tools.smart_index import index_file
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def read_file(path):
|
|
10
|
+
"""
|
|
11
|
+
Reads a file from a specific path.
|
|
12
|
+
Supports: PDF, DOCX, XLSX, CSV, TXT, PY, MD, JSON.
|
|
13
|
+
"""
|
|
14
|
+
index_file(path)
|
|
15
|
+
|
|
16
|
+
if not os.path.exists(path):
|
|
17
|
+
return "Error: File not found."
|
|
18
|
+
|
|
19
|
+
ext = os.path.splitext(path)[1].lower()
|
|
20
|
+
|
|
21
|
+
try:
|
|
22
|
+
# --- PDF (Smart Read using PyMuPDF) ---
|
|
23
|
+
if ext == '.pdf':
|
|
24
|
+
text = ""
|
|
25
|
+
try:
|
|
26
|
+
with fitz.open(path) as doc:
|
|
27
|
+
# Limit to first 20 pages to avoid overloading the LLM context
|
|
28
|
+
for page in doc[:20]:
|
|
29
|
+
text += page.get_text() + "\n"
|
|
30
|
+
return text if text.strip() else "[PDF contains no selectable text (Scanned?)]"
|
|
31
|
+
except Exception as e:
|
|
32
|
+
return f"[Error reading PDF: {e}]"
|
|
33
|
+
|
|
34
|
+
# --- Word Docs ---
|
|
35
|
+
elif ext == '.docx':
|
|
36
|
+
try:
|
|
37
|
+
doc = Document(path)
|
|
38
|
+
return "\n".join([p.text for p in doc.paragraphs])
|
|
39
|
+
except Exception as e:
|
|
40
|
+
return f"[Error reading DOCX: {e}]"
|
|
41
|
+
|
|
42
|
+
# --- Excel (Data View) ---
|
|
43
|
+
elif ext in ['.xlsx', '.xls']:
|
|
44
|
+
try:
|
|
45
|
+
# Read first sheet, max 50 rows to keep it readable
|
|
46
|
+
df = pd.read_excel(path, nrows=50)
|
|
47
|
+
return df.to_markdown(index=False)
|
|
48
|
+
except Exception as e:
|
|
49
|
+
return f"[Error reading Excel: {e}]"
|
|
50
|
+
|
|
51
|
+
# --- CSV ---
|
|
52
|
+
elif ext == '.csv':
|
|
53
|
+
try:
|
|
54
|
+
df = pd.read_csv(path, nrows=50)
|
|
55
|
+
return df.to_markdown(index=False)
|
|
56
|
+
except Exception as e:
|
|
57
|
+
return f"[Error reading CSV: {e}]"
|
|
58
|
+
|
|
59
|
+
# --- Code / Text ---
|
|
60
|
+
else:
|
|
61
|
+
with open(path, 'r', encoding='utf-8', errors='ignore') as f:
|
|
62
|
+
return f.read(10000) # Limit to first 10k characters to prevent lag
|
|
63
|
+
|
|
64
|
+
except Exception as e:
|
|
65
|
+
return f"Error reading file: {e}"
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def write_file(path, content):
|
|
69
|
+
index_file(path)
|
|
70
|
+
|
|
71
|
+
try:
|
|
72
|
+
# Ensure directory exists
|
|
73
|
+
folder = os.path.dirname(path)
|
|
74
|
+
if folder and not os.path.exists(folder):
|
|
75
|
+
os.makedirs(folder)
|
|
76
|
+
|
|
77
|
+
with open(path, 'w', encoding='utf-8') as f:
|
|
78
|
+
f.write(content)
|
|
79
|
+
return f"Saved to {path}"
|
|
80
|
+
except Exception as e:
|
|
81
|
+
return str(e)
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import requests
|
|
2
|
+
from sentinel.core.config import ConfigManager
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
def search_flights(departure_id, arrival_id, date, travel_type=2):
|
|
6
|
+
cfg = ConfigManager()
|
|
7
|
+
key = cfg.get_key("serp_api")
|
|
8
|
+
|
|
9
|
+
if not key:
|
|
10
|
+
return "Error: SerpAPI key missing."
|
|
11
|
+
|
|
12
|
+
params = {
|
|
13
|
+
"api_key": key,
|
|
14
|
+
"engine": "google_flights",
|
|
15
|
+
"departure_id": departure_id,
|
|
16
|
+
"arrival_id": arrival_id,
|
|
17
|
+
"outbound_date": date,
|
|
18
|
+
"type": travel_type,
|
|
19
|
+
"hl": "en",
|
|
20
|
+
"currency": "USD"
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
try:
|
|
24
|
+
response = requests.get(
|
|
25
|
+
"https://serpapi.com/search",
|
|
26
|
+
params=params,
|
|
27
|
+
timeout=15
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
if response.status_code != 200:
|
|
31
|
+
return f"SerpAPI HTTP Error: {response.status_code}"
|
|
32
|
+
|
|
33
|
+
data = response.json()
|
|
34
|
+
|
|
35
|
+
if "error" in data:
|
|
36
|
+
return f"API Error: {data['error']}"
|
|
37
|
+
|
|
38
|
+
flights = data.get("best_flights") or data.get("other_flights")
|
|
39
|
+
if not flights:
|
|
40
|
+
return "No flights found."
|
|
41
|
+
|
|
42
|
+
summary = [f"Found {len(flights)} flights (Top 5 shown):"]
|
|
43
|
+
|
|
44
|
+
for i, f in enumerate(flights[:5], 1):
|
|
45
|
+
price = f.get("price", "N/A")
|
|
46
|
+
duration = f.get("total_duration", "N/A") # FIXED
|
|
47
|
+
|
|
48
|
+
leg = f["flights"][0]
|
|
49
|
+
airline = leg.get("airline", "Unknown")
|
|
50
|
+
dep_time = leg["departure_airport"]["time"]
|
|
51
|
+
arr_time = leg["arrival_airport"]["time"]
|
|
52
|
+
|
|
53
|
+
summary.append(
|
|
54
|
+
f"{i}. {airline} | ${price} | {duration} min\n"
|
|
55
|
+
f" Departs: {dep_time} ({departure_id}) -> "
|
|
56
|
+
f"Arrives: {arr_time} ({arrival_id})"
|
|
57
|
+
)
|
|
58
|
+
|
|
59
|
+
return "\n".join(summary)
|
|
60
|
+
|
|
61
|
+
except Exception as e:
|
|
62
|
+
return f"Flight search failed: {e}"
|