Auto-update: Sat Jun 29 10:26:14 PDT 2024
This commit is contained in:
parent
bc1924dd4b
commit
7a064cd21d
7 changed files with 110 additions and 62 deletions
|
@ -94,13 +94,22 @@ DAY_SHORT_FMT = os.getenv("DAY_SHORT_FMT")
|
|||
### Large language model
|
||||
LLM_URL = os.getenv("LLM_URL", "http://localhost:11434")
|
||||
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_VISION = os.getenv("DEFAULT_VISION", "llava")
|
||||
DEFAULT_VOICE = os.getenv("DEFAULT_VOICE", "Luna")
|
||||
DEFAULT_11L_VOICE = os.getenv("DEFAULT_11L_VOICE", "Victoria")
|
||||
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
|
||||
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')
|
||||
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_DIR = DATA_DIR / "asr"
|
||||
|
|
|
@ -48,7 +48,6 @@ class Location(BaseModel):
|
|||
}
|
||||
|
||||
|
||||
|
||||
class Geocoder:
|
||||
def __init__(self, named_locs: Union[str, Path] = None, cache_file: Union[str, Path] = 'timezone_cache.json'):
|
||||
self.tf = TimezoneFinder()
|
||||
|
@ -119,11 +118,13 @@ class Geocoder:
|
|||
else:
|
||||
raise ValueError(f"Unsupported unit: {unit}")
|
||||
|
||||
|
||||
async def timezone(self, lat: float, lon: float):
|
||||
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'
|
||||
|
||||
|
||||
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']
|
||||
elevation = await self.elevation(lat, lon)
|
||||
|
@ -357,6 +358,7 @@ class AutoResponder(BaseModel):
|
|||
whitelist: List[str]
|
||||
blacklist: List[str]
|
||||
image_prompt: Optional[str] = None
|
||||
image_scene: Optional[str] = None
|
||||
smtp: SMTPConfig
|
||||
|
||||
class EmailAccount(BaseModel):
|
||||
|
|
1
sijapi/data/tzcache.json
Normal file
1
sijapi/data/tzcache.json
Normal file
|
@ -0,0 +1 @@
|
|||
{"last_timezone": "America/Los_Angeles", "last_update": "2024-06-29T09:36:32.143487", "last_location": [44.04645364336354, -123.08688060439617]}
|
|
@ -11,6 +11,7 @@ from pathlib import Path
|
|||
from shutil import move
|
||||
import tempfile
|
||||
import re
|
||||
import traceback
|
||||
from smtplib import SMTP_SSL, SMTP
|
||||
from email.mime.multipart import MIMEMultipart
|
||||
from email.mime.text import MIMEText
|
||||
|
@ -42,17 +43,84 @@ def get_imap_connection(account: EmailAccount):
|
|||
ssl=account.imap.encryption == 'SSL',
|
||||
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':
|
||||
return SMTP_SSL(autoresponder.smtp.host, autoresponder.smtp.port, context=context)
|
||||
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)
|
||||
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.starttls(context=context)
|
||||
return smtp
|
||||
except Exception as e:
|
||||
L.ERR(f"STARTTLS connection failed: {str(e)}")
|
||||
raise
|
||||
elif autoresponder.smtp.encryption == 'STARTTLS':
|
||||
smtp = SMTP(autoresponder.smtp.host, autoresponder.smtp.port)
|
||||
smtp.starttls(context=context)
|
||||
return smtp
|
||||
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:
|
||||
return SMTP(autoresponder.smtp.host, autoresponder.smtp.port)
|
||||
try:
|
||||
L.DEBUG(f"Attempting unencrypted connection to {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):
|
||||
|
@ -233,8 +301,9 @@ async def autorespond_single_email(message, uid_str: str, account: EmailAccount,
|
|||
response_body = await generate_response(this_email, profile, account)
|
||||
if response_body:
|
||||
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
|
||||
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:
|
||||
L.WARN(f"Auto-responded to email: {this_email.subject}")
|
||||
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)}")
|
||||
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:
|
||||
recipients = [EmailContact(email=recipient['email'], name=recipient.get('name', '')) for recipient in message.sent_to]
|
||||
|
|
|
@ -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.responses import HTMLResponse, JSONResponse
|
||||
|
@ -64,13 +64,13 @@ async def dt(
|
|||
|
||||
# If no timezone provided, only fill in missing timezone info
|
||||
elif date_time.tzinfo is None:
|
||||
tz_str = await get_timezone_without_timezone(date_time)
|
||||
try:
|
||||
tz = ZoneInfo(tz_str)
|
||||
except Exception as e:
|
||||
L.WARN(f"Invalid timezone string '{tz_str}' from Geocoder. Falling back to UTC. Error: {e}")
|
||||
tz = ZoneInfo('UTC')
|
||||
|
||||
#tz_str = await get_ti`mezone_without_timezone(date_time)
|
||||
#try:
|
||||
# tz = ZoneInfo(tz_str)
|
||||
#except Exception as e:
|
||||
# L.WARN(f"Invalid timezone string '{tz_str}' from Geocoder. Falling back to UTC. Error: {e}")
|
||||
# tz = ZoneInfo('UTC')
|
||||
tz = ZoneInfo('UTC') # force use of UTC when we can't find real timezone
|
||||
date_time = date_time.replace(tzinfo=tz)
|
||||
L.DEBUG(f"Filled in missing timezone info: {tz}")
|
||||
|
||||
|
|
|
@ -279,7 +279,7 @@ async def generate_banner(dt, location: Location = None, forecast: str = None, m
|
|||
display_name = "Location: "
|
||||
if location:
|
||||
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 ""
|
||||
if 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
|
||||
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:
|
||||
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}")
|
||||
|
||||
else:
|
||||
loc = await GEO.code(lat, lon)
|
||||
L.DEBUG(f"loc: {loc}")
|
||||
city = loc.name
|
||||
city = city if city else loc.city
|
||||
city = city if city else loc.house_number + ' ' + loc.road
|
||||
location = await GEO.code(lat, lon)
|
||||
L.DEBUG(f"location: {location}")
|
||||
city = location.name
|
||||
city = city if city else location.city
|
||||
city = city if city else location.house_number + ' ' + location.road
|
||||
|
||||
L.DEBUG(f"City geocoded: {city}")
|
||||
|
||||
|
|
|
@ -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"\'')
|
||||
|
||||
|
||||
# Asynchronous remote notification using paramiko SSH
|
||||
async def notify_remote(host: str, message: str, username: str = None, password: str = None, key_filename: str = None):
|
||||
ssh = paramiko.SSHClient()
|
||||
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()
|
||||
|
||||
|
||||
|
||||
async def notify_shellfish(alert: str):
|
||||
key = "d7e810e7601cd296a05776c169b4fe97a6a5ee1fd46abe38de54f415732b3f4b"
|
||||
user = "WuqPwm1VpGijF4U5AnIKzqNMVWGioANTRjJoonPm"
|
||||
|
|
Loading…
Reference in a new issue