Auto-update: Sat Jun 29 10:26:14 PDT 2024

This commit is contained in:
sanj 2024-06-29 10:26:14 -07:00
parent bc1924dd4b
commit 7a064cd21d
7 changed files with 110 additions and 62 deletions

View file

@ -94,13 +94,22 @@ DAY_SHORT_FMT = os.getenv("DAY_SHORT_FMT")
### Large language model ### Large language model
LLM_URL = os.getenv("LLM_URL", "http://localhost:11434") LLM_URL = os.getenv("LLM_URL", "http://localhost:11434")
LLM_SYS_MSG = os.getenv("SYSTEM_MSG", "You are a helpful AI assistant.") LLM_SYS_MSG = os.getenv("SYSTEM_MSG", "You are a helpful AI assistant.")
SUMMARY_INSTRUCT = os.getenv('SUMMARY_INSTRUCT', "You are an AI assistant that provides accurate summaries of text -- nothing more and nothing less. You must not include ANY extraneous text other than the sumary. Do not include comments apart from the summary, do not preface the summary, and do not provide any form of postscript. Do not add paragraph breaks. Do not add any kind of formatting. Your response should begin with, consist of, and end with an accurate plaintext summary.")
SUMMARY_INSTRUCT_TTS = os.getenv('SUMMARY_INSTRUCT_TTS', "You are an AI assistant that provides email summaries for Sanjay. Your response will undergo Text-To-Speech conversion and added to Sanjay's private podcast. Providing adequate context (Sanjay did not send this question to you, he will only hear your response) but aiming for conciseness and precision, and bearing in mind the Text-To-Speech conversion (avoiding acronyms and formalities), summarize the following email.")
DEFAULT_LLM = os.getenv("DEFAULT_LLM", "llama3") DEFAULT_LLM = os.getenv("DEFAULT_LLM", "llama3")
DEFAULT_VISION = os.getenv("DEFAULT_VISION", "llava") DEFAULT_VISION = os.getenv("DEFAULT_VISION", "llava")
DEFAULT_VOICE = os.getenv("DEFAULT_VOICE", "Luna") DEFAULT_VOICE = os.getenv("DEFAULT_VOICE", "Luna")
DEFAULT_11L_VOICE = os.getenv("DEFAULT_11L_VOICE", "Victoria") DEFAULT_11L_VOICE = os.getenv("DEFAULT_11L_VOICE", "Victoria")
OPENAI_API_KEY = os.getenv("OPENAI_API_KEY") OPENAI_API_KEY = os.getenv("OPENAI_API_KEY")
### Summarization
SUMMARY_CHUNK_SIZE = int(os.getenv("SUMMARY_CHUNK_SIZE", 4000)) # measured in tokens
SUMMARY_CHUNK_OVERLAP = int(os.getenv("SUMMARY_CHUNK_OVERLAP", 100)) # measured in tokens
SUMMARY_TPW = float(os.getenv("SUMMARY_TPW", 1.3)) # measured in tokens
SUMMARY_LENGTH_RATIO = int(os.getenv("SUMMARY_LENGTH_RATIO", 4)) # measured as original to length ratio
SUMMARY_MIN_LENGTH = int(os.getenv("SUMMARY_MIN_LENGTH", 150)) # measured in tokens
SUMMARY_MODEL = os.getenv("SUMMARY_MODEL", "dolphin-llama3:8b-256k")
SUMMARY_TOKEN_LIMIT = int(os.getenv("SUMMARY_TOKEN_LIMIT", 4096))
SUMMARY_INSTRUCT = os.getenv('SUMMARY_INSTRUCT', "You are an AI assistant that provides accurate summaries of text -- nothing more and nothing less. You must not include ANY extraneous text other than the sumary. Do not include comments apart from the summary, do not preface the summary, and do not provide any form of postscript. Do not add paragraph breaks. Do not add any kind of formatting. Your response should begin with, consist of, and end with an accurate plaintext summary.")
SUMMARY_INSTRUCT_TTS = os.getenv('SUMMARY_INSTRUCT_TTS', "You are an AI assistant that provides email summaries for Sanjay. Your response will undergo Text-To-Speech conversion and added to Sanjay's private podcast. Providing adequate context (Sanjay did not send this question to you, he will only hear your response) but aiming for conciseness and precision, and bearing in mind the Text-To-Speech conversion (avoiding acronyms and formalities), summarize the following email.")
### Stable diffusion ### Stable diffusion
SD_IMAGE_DIR = DATA_DIR / "sd" / "images" SD_IMAGE_DIR = DATA_DIR / "sd" / "images"
@ -113,15 +122,7 @@ COMFYUI_OUTPUT_DIR = COMFYUI_DIR / 'output'
COMFYUI_LAUNCH_CMD = os.getenv('COMFYUI_LAUNCH_CMD', 'mamba activate comfyui && python main.py') COMFYUI_LAUNCH_CMD = os.getenv('COMFYUI_LAUNCH_CMD', 'mamba activate comfyui && python main.py')
SD_CONFIG_PATH = CONFIG_DIR / 'sd.yaml' SD_CONFIG_PATH = CONFIG_DIR / 'sd.yaml'
### Summarization
SUMMARY_CHUNK_SIZE = int(os.getenv("SUMMARY_CHUNK_SIZE", 4000)) # measured in tokens
SUMMARY_CHUNK_OVERLAP = int(os.getenv("SUMMARY_CHUNK_OVERLAP", 100)) # measured in tokens
SUMMARY_TPW = float(os.getenv("SUMMARY_TPW", 1.3)) # measured in tokens
SUMMARY_LENGTH_RATIO = int(os.getenv("SUMMARY_LENGTH_RATIO", 4)) # measured as original to length ratio
SUMMARY_MIN_LENGTH = int(os.getenv("SUMMARY_MIN_LENGTH", 150)) # measured in tokens
SUMMARY_INSTRUCT = os.getenv("SUMMARY_INSTRUCT", "Summarize the provided text. Respond with the summary and nothing else. Do not otherwise acknowledge the request. Just provide the requested summary.")
SUMMARY_MODEL = os.getenv("SUMMARY_MODEL", "llama3")
SUMMARY_TOKEN_LIMIT = int(os.getenv("SUMMARY_TOKEN_LIMIT", 4096))
### ASR ### ASR
ASR_DIR = DATA_DIR / "asr" ASR_DIR = DATA_DIR / "asr"

View file

@ -48,7 +48,6 @@ class Location(BaseModel):
} }
class Geocoder: class Geocoder:
def __init__(self, named_locs: Union[str, Path] = None, cache_file: Union[str, Path] = 'timezone_cache.json'): def __init__(self, named_locs: Union[str, Path] = None, cache_file: Union[str, Path] = 'timezone_cache.json'):
self.tf = TimezoneFinder() self.tf = TimezoneFinder()
@ -119,11 +118,13 @@ class Geocoder:
else: else:
raise ValueError(f"Unsupported unit: {unit}") raise ValueError(f"Unsupported unit: {unit}")
async def timezone(self, lat: float, lon: float): async def timezone(self, lat: float, lon: float):
loop = asyncio.get_running_loop() loop = asyncio.get_running_loop()
timezone = await loop.run_in_executor(self.executor, self.tf.timezone_at, lat, lon) timezone = await loop.run_in_executor(self.executor, lambda: self.tf.timezone_at(lat=lat, lng=lon))
return timezone if timezone else 'Unknown' return timezone if timezone else 'Unknown'
async def lookup(self, lat: float, lon: float): async def lookup(self, lat: float, lon: float):
city, state, country = (await self.location(lat, lon))[0]['name'], (await self.location(lat, lon))[0]['admin1'], (await self.location(lat, lon))[0]['cc'] city, state, country = (await self.location(lat, lon))[0]['name'], (await self.location(lat, lon))[0]['admin1'], (await self.location(lat, lon))[0]['cc']
elevation = await self.elevation(lat, lon) elevation = await self.elevation(lat, lon)
@ -357,6 +358,7 @@ class AutoResponder(BaseModel):
whitelist: List[str] whitelist: List[str]
blacklist: List[str] blacklist: List[str]
image_prompt: Optional[str] = None image_prompt: Optional[str] = None
image_scene: Optional[str] = None
smtp: SMTPConfig smtp: SMTPConfig
class EmailAccount(BaseModel): class EmailAccount(BaseModel):

1
sijapi/data/tzcache.json Normal file
View file

@ -0,0 +1 @@
{"last_timezone": "America/Los_Angeles", "last_update": "2024-06-29T09:36:32.143487", "last_location": [44.04645364336354, -123.08688060439617]}

View file

@ -11,6 +11,7 @@ from pathlib import Path
from shutil import move from shutil import move
import tempfile import tempfile
import re import re
import traceback
from smtplib import SMTP_SSL, SMTP from smtplib import SMTP_SSL, SMTP
from email.mime.multipart import MIMEMultipart from email.mime.multipart import MIMEMultipart
from email.mime.text import MIMEText from email.mime.text import MIMEText
@ -42,17 +43,84 @@ def get_imap_connection(account: EmailAccount):
ssl=account.imap.encryption == 'SSL', ssl=account.imap.encryption == 'SSL',
starttls=account.imap.encryption == 'STARTTLS') starttls=account.imap.encryption == 'STARTTLS')
def get_smtp_connection(autoresponder: AutoResponder):
context = ssl._create_unverified_context()
def get_smtp_connection(autoresponder):
# Create an SSL context that doesn't verify certificates
context = ssl.create_default_context()
context.check_hostname = False
context.verify_mode = ssl.CERT_NONE
if autoresponder.smtp.encryption == 'SSL': if autoresponder.smtp.encryption == 'SSL':
try:
L.DEBUG(f"Attempting SSL connection to {autoresponder.smtp.host}:{autoresponder.smtp.port}")
return SMTP_SSL(autoresponder.smtp.host, autoresponder.smtp.port, context=context) return SMTP_SSL(autoresponder.smtp.host, autoresponder.smtp.port, context=context)
elif autoresponder.smtp.encryption == 'STARTTLS': except ssl.SSLError as e:
L.ERR(f"SSL connection failed: {str(e)}")
# If SSL fails, try TLS
try:
L.DEBUG(f"Attempting STARTTLS connection to {autoresponder.smtp.host}:{autoresponder.smtp.port}")
smtp = SMTP(autoresponder.smtp.host, autoresponder.smtp.port) smtp = SMTP(autoresponder.smtp.host, autoresponder.smtp.port)
smtp.starttls(context=context) smtp.starttls(context=context)
return smtp return smtp
except Exception as e:
L.ERR(f"STARTTLS connection failed: {str(e)}")
raise
elif autoresponder.smtp.encryption == 'STARTTLS':
try:
L.DEBUG(f"Attempting STARTTLS connection to {autoresponder.smtp.host}:{autoresponder.smtp.port}")
smtp = SMTP(autoresponder.smtp.host, autoresponder.smtp.port)
smtp.starttls(context=context)
return smtp
except Exception as e:
L.ERR(f"STARTTLS connection failed: {str(e)}")
raise
else: else:
try:
L.DEBUG(f"Attempting unencrypted connection to {autoresponder.smtp.host}:{autoresponder.smtp.port}")
return SMTP(autoresponder.smtp.host, autoresponder.smtp.port) return SMTP(autoresponder.smtp.host, autoresponder.smtp.port)
except Exception as e:
L.ERR(f"Unencrypted connection failed: {str(e)}")
raise
async def send_response(to_email: str, subject: str, body: str, profile: AutoResponder, image_attachment: Path = None) -> bool:
server = None
try:
message = MIMEMultipart()
message['From'] = profile.smtp.username
message['To'] = to_email
message['Subject'] = subject
message.attach(MIMEText(body, 'plain'))
if image_attachment and os.path.exists(image_attachment):
with open(image_attachment, 'rb') as img_file:
img = MIMEImage(img_file.read(), name=os.path.basename(image_attachment))
message.attach(img)
L.DEBUG(f"Sending auto-response to {to_email} concerning {subject} from account {profile.name}...")
server = get_smtp_connection(profile)
L.DEBUG(f"SMTP connection established: {type(server)}")
server.login(profile.smtp.username, profile.smtp.password)
server.send_message(message)
L.INFO(f"Auto-response sent to {to_email} concerning {subject} from account {profile.name}!")
return True
except Exception as e:
L.ERR(f"Error in preparing/sending auto-response from account {profile.name}: {str(e)}")
L.ERR(f"SMTP details - Host: {profile.smtp.host}, Port: {profile.smtp.port}, Encryption: {profile.smtp.encryption}")
L.ERR(traceback.format_exc())
return False
finally:
if server:
try:
server.quit()
except Exception as e:
L.ERR(f"Error closing SMTP connection: {str(e)}")
def clean_email_content(html_content): def clean_email_content(html_content):
@ -233,8 +301,9 @@ async def autorespond_single_email(message, uid_str: str, account: EmailAccount,
response_body = await generate_response(this_email, profile, account) response_body = await generate_response(this_email, profile, account)
if response_body: if response_body:
subject = f"Re: {this_email.subject}" subject = f"Re: {this_email.subject}"
# add back scene=profile.image_scene, to workflow call
jpg_path = await sd.workflow(profile.image_prompt, earlyout=False, downscale_to_fit=True) if profile.image_prompt else None jpg_path = await sd.workflow(profile.image_prompt, earlyout=False, downscale_to_fit=True) if profile.image_prompt else None
success = await send_response(this_email.sender, subject, response_body, profile, account, jpg_path) success = await send_response(this_email.sender, subject, response_body, profile, jpg_path)
if success: if success:
L.WARN(f"Auto-responded to email: {this_email.subject}") L.WARN(f"Auto-responded to email: {this_email.subject}")
await save_processed_uid(log_file, account.name, uid_str) await save_processed_uid(log_file, account.name, uid_str)
@ -273,31 +342,7 @@ Respond on behalf of {account.fullname}, who is unable to respond personally bec
L.ERR(f"Error generating auto-response: {str(e)}") L.ERR(f"Error generating auto-response: {str(e)}")
return None return None
async def send_response(to_email: str, subject: str, body: str, profile: AutoResponder, image_attachment: Path = None) -> bool:
try:
message = MIMEMultipart()
message['From'] = profile.smtp.username
message['To'] = to_email
message['Subject'] = subject
message.attach(MIMEText(body, 'plain'))
if image_attachment and os.path.exists(image_attachment):
with open(image_attachment, 'rb') as img_file:
img = MIMEImage(img_file.read(), name=os.path.basename(image_attachment))
message.attach(img)
L.DEBUG(f"Sending auto-response to {to_email} concerning {subject} from account {profile.name}...")
with get_smtp_connection(profile) as server:
server.login(profile.smtp.username, profile.smtp.password)
server.send_message(message)
L.INFO(f"Auto-response sent to {to_email} concerning {subject} from account {profile.name}!")
return True
except Exception as e:
L.ERR(f"Error in preparing/sending auto-response from account {profile.name}: {e}")
return False
async def create_incoming_email(message) -> IncomingEmail: async def create_incoming_email(message) -> IncomingEmail:
recipients = [EmailContact(email=recipient['email'], name=recipient.get('name', '')) for recipient in message.sent_to] recipients = [EmailContact(email=recipient['email'], name=recipient.get('name', '')) for recipient in message.sent_to]

View file

@ -1,5 +1,5 @@
''' '''
Uses Postgres/PostGIS for for location tracking (data obtained via the companion mobile Pythonista scripts), and for geocoding purposes. Uses Postgres/PostGIS for location tracking (data obtained via the companion mobile Pythonista scripts), and for geocoding purposes.
''' '''
from fastapi import APIRouter, HTTPException, Query from fastapi import APIRouter, HTTPException, Query
from fastapi.responses import HTMLResponse, JSONResponse from fastapi.responses import HTMLResponse, JSONResponse
@ -64,13 +64,13 @@ async def dt(
# If no timezone provided, only fill in missing timezone info # If no timezone provided, only fill in missing timezone info
elif date_time.tzinfo is None: elif date_time.tzinfo is None:
tz_str = await get_timezone_without_timezone(date_time) #tz_str = await get_ti`mezone_without_timezone(date_time)
try: #try:
tz = ZoneInfo(tz_str) # tz = ZoneInfo(tz_str)
except Exception as e: #except Exception as e:
L.WARN(f"Invalid timezone string '{tz_str}' from Geocoder. Falling back to UTC. Error: {e}") # L.WARN(f"Invalid timezone string '{tz_str}' from Geocoder. Falling back to UTC. Error: {e}")
tz = ZoneInfo('UTC') # tz = ZoneInfo('UTC')
tz = ZoneInfo('UTC') # force use of UTC when we can't find real timezone
date_time = date_time.replace(tzinfo=tz) date_time = date_time.replace(tzinfo=tz)
L.DEBUG(f"Filled in missing timezone info: {tz}") L.DEBUG(f"Filled in missing timezone info: {tz}")

View file

@ -279,7 +279,7 @@ async def generate_banner(dt, location: Location = None, forecast: str = None, m
display_name = "Location: " display_name = "Location: "
if location: if location:
lat, lon = location.latitude, location.longitude lat, lon = location.latitude, location.longitude
override_location = await loc.find_override_locations(lat, lon) override_location = GEO.find_override_location(lat, lon)
display_name += f"{override_location}, " if override_location else "" display_name += f"{override_location}, " if override_location else ""
if location.display_name: if location.display_name:
display_name += f"{location.display_name}" display_name += f"{location.display_name}"
@ -383,7 +383,8 @@ async def update_dn_weather(date_time: dt_datetime, lat: float = None, lon: floa
lat = place.latitude lat = place.latitude
lon = place.longitude lon = place.longitude
city = await loc.find_override_locations(lat, lon) L.DEBUG(f"lat: {lat}, lon: {lon}, place: {place}")
city = GEO.find_override_location(lat, lon)
if city: if city:
L.INFO(f"Using override location: {city}") L.INFO(f"Using override location: {city}")
@ -393,11 +394,11 @@ async def update_dn_weather(date_time: dt_datetime, lat: float = None, lon: floa
L.INFO(f"City in data: {city}") L.INFO(f"City in data: {city}")
else: else:
loc = await GEO.code(lat, lon) location = await GEO.code(lat, lon)
L.DEBUG(f"loc: {loc}") L.DEBUG(f"location: {location}")
city = loc.name city = location.name
city = city if city else loc.city city = city if city else location.city
city = city if city else loc.house_number + ' ' + loc.road city = city if city else location.house_number + ' ' + location.road
L.DEBUG(f"City geocoded: {city}") L.DEBUG(f"City geocoded: {city}")

View file

@ -178,7 +178,6 @@ async def notify_local(message: str):
await asyncio.to_thread(os.system, f'osascript -e \'display notification "{message}" with title "Notification Title"\'') await asyncio.to_thread(os.system, f'osascript -e \'display notification "{message}" with title "Notification Title"\'')
# Asynchronous remote notification using paramiko SSH
async def notify_remote(host: str, message: str, username: str = None, password: str = None, key_filename: str = None): async def notify_remote(host: str, message: str, username: str = None, password: str = None, key_filename: str = None):
ssh = paramiko.SSHClient() ssh = paramiko.SSHClient()
ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy()) ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy())
@ -194,7 +193,6 @@ async def notify_remote(host: str, message: str, username: str = None, password:
ssh.close() ssh.close()
async def notify_shellfish(alert: str): async def notify_shellfish(alert: str):
key = "d7e810e7601cd296a05776c169b4fe97a6a5ee1fd46abe38de54f415732b3f4b" key = "d7e810e7601cd296a05776c169b4fe97a6a5ee1fd46abe38de54f415732b3f4b"
user = "WuqPwm1VpGijF4U5AnIKzqNMVWGioANTRjJoonPm" user = "WuqPwm1VpGijF4U5AnIKzqNMVWGioANTRjJoonPm"