first commit
This commit is contained in:
commit
7d3770dba3
|
|
@ -0,0 +1,207 @@
|
|||
# Byte-compiled / optimized / DLL files
|
||||
__pycache__/
|
||||
*.py[cod]
|
||||
*$py.class
|
||||
|
||||
# C extensions
|
||||
*.so
|
||||
|
||||
# Distribution / packaging
|
||||
.Python
|
||||
build/
|
||||
develop-eggs/
|
||||
dist/
|
||||
downloads/
|
||||
eggs/
|
||||
.eggs/
|
||||
lib/
|
||||
lib64/
|
||||
parts/
|
||||
sdist/
|
||||
var/
|
||||
wheels/
|
||||
share/python-wheels/
|
||||
*.egg-info/
|
||||
.installed.cfg
|
||||
*.egg
|
||||
MANIFEST
|
||||
|
||||
# PyInstaller
|
||||
# Usually these files are written by a python script from a template
|
||||
# before PyInstaller builds the exe, so as to inject date/other infos into it.
|
||||
*.manifest
|
||||
*.spec
|
||||
|
||||
# Installer logs
|
||||
pip-log.txt
|
||||
pip-delete-this-directory.txt
|
||||
|
||||
# Unit test / coverage reports
|
||||
htmlcov/
|
||||
.tox/
|
||||
.nox/
|
||||
.coverage
|
||||
.coverage.*
|
||||
.cache
|
||||
nosetests.xml
|
||||
coverage.xml
|
||||
*.cover
|
||||
*.py,cover
|
||||
.hypothesis/
|
||||
.pytest_cache/
|
||||
cover/
|
||||
|
||||
# Translations
|
||||
*.mo
|
||||
*.pot
|
||||
|
||||
# Django stuff:
|
||||
*.log
|
||||
local_settings.py
|
||||
db.sqlite3
|
||||
db.sqlite3-journal
|
||||
|
||||
# Flask stuff:
|
||||
instance/
|
||||
.webassets-cache
|
||||
|
||||
# Scrapy stuff:
|
||||
.scrapy
|
||||
|
||||
# Sphinx documentation
|
||||
docs/_build/
|
||||
|
||||
# PyBuilder
|
||||
.pybuilder/
|
||||
target/
|
||||
|
||||
# Jupyter Notebook
|
||||
.ipynb_checkpoints
|
||||
|
||||
# IPython
|
||||
profile_default/
|
||||
ipython_config.py
|
||||
|
||||
# pyenv
|
||||
# For a library or package, you might want to ignore these files since the code is
|
||||
# intended to run in multiple environments; otherwise, check them in:
|
||||
# .python-version
|
||||
|
||||
# pipenv
|
||||
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
|
||||
# However, in case of collaboration, if having platform-specific dependencies or dependencies
|
||||
# having no cross-platform support, pipenv may install dependencies that don't work, or not
|
||||
# install all needed dependencies.
|
||||
#Pipfile.lock
|
||||
|
||||
# poetry
|
||||
# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
|
||||
# This is especially recommended for binary packages to ensure reproducibility, and is more
|
||||
# commonly ignored for libraries.
|
||||
# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
|
||||
#poetry.lock
|
||||
|
||||
# pdm
|
||||
# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
|
||||
#pdm.lock
|
||||
# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it
|
||||
# in version control.
|
||||
# https://pdm.fming.dev/#use-with-ide
|
||||
.pdm.toml
|
||||
|
||||
# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
|
||||
__pypackages__/
|
||||
|
||||
# Celery stuff
|
||||
celerybeat-schedule
|
||||
celerybeat.pid
|
||||
|
||||
# SageMath parsed files
|
||||
*.sage.py
|
||||
|
||||
# Environments
|
||||
.env
|
||||
.venv
|
||||
env/
|
||||
venv/
|
||||
ENV/
|
||||
env.bak/
|
||||
venv.bak/
|
||||
|
||||
# Spyder project settings
|
||||
.spyderproject
|
||||
.spyproject
|
||||
|
||||
# Rope project settings
|
||||
.ropeproject
|
||||
|
||||
# mkdocs documentation
|
||||
/site
|
||||
|
||||
# mypy
|
||||
.mypy_cache/
|
||||
.dmypy.json
|
||||
dmypy.json
|
||||
|
||||
# Pyre type checker
|
||||
.pyre/
|
||||
|
||||
# pytype static type analyzer
|
||||
.pytype/
|
||||
|
||||
# Cython debug symbols
|
||||
cython_debug/
|
||||
|
||||
# PyCharm
|
||||
# JetBrains specific template is maintained in a separate JetBrains.gitignore that can
|
||||
# be added to the global gitignore or merged into this project gitignore. For a PyCharm
|
||||
# project, it is recommended to include the following files:
|
||||
# .idea/
|
||||
# *.iml
|
||||
# *.ipr
|
||||
# *.iws
|
||||
.idea/
|
||||
*.iml
|
||||
*.ipr
|
||||
*.iws
|
||||
|
||||
# VS Code
|
||||
.vscode/
|
||||
*.code-workspace
|
||||
|
||||
# Claude AI
|
||||
.claude/
|
||||
|
||||
# OS generated files
|
||||
.DS_Store
|
||||
.DS_Store?
|
||||
._*
|
||||
.Spotlight-V100
|
||||
.Trashes
|
||||
ehthumbs.db
|
||||
Thumbs.db
|
||||
|
||||
# Temporary files
|
||||
*.tmp
|
||||
*.temp
|
||||
*.swp
|
||||
*.swo
|
||||
*~
|
||||
|
||||
# Logs
|
||||
*.log
|
||||
logs/
|
||||
|
||||
# Local configuration files
|
||||
config.local.json
|
||||
*.local.json
|
||||
.env.local
|
||||
|
||||
# Database files
|
||||
*.db
|
||||
*.sqlite
|
||||
*.sqlite3
|
||||
|
||||
# Backup files
|
||||
*.bak
|
||||
*.backup
|
||||
|
|
@ -0,0 +1,19 @@
|
|||
Project Description:
|
||||
|
||||
This is a python based project. keep it lightweighted
|
||||
|
||||
|
||||
Features:
|
||||
1. the app can invoke ollama client to send prompt and get response.
|
||||
2. send the llm response to 3rd party rest apis, this apis can push notification to ios/andorid. the apis are not in the scope
|
||||
3. the prompt can have different template, each time it will pick the template ramdonly.
|
||||
4. each template have some placeholders, for example, fill in the date, time of the real time
|
||||
5. the invoke ollama and send notifications will be triggered at random intervals(around 45 minutes, it can be 3 or 5 minutes, can be 2 or 3 hours)
|
||||
6. the invoke will be set with a slient time, for example every day 12pm to 8am.
|
||||
7. the silent time, ollama api endpoint, etc, can be configurable
|
||||
8. Notification APIs:
|
||||
regarding notification api, there are 2 apis to push:
|
||||
bark:
|
||||
https://bark.day.app/#/tutorial?id=%e5%8f%91%e9%80%81%e6%8e%a8%e9%80%81
|
||||
ntfy:
|
||||
https://docs.ntfy.sh/publish/
|
||||
|
|
@ -0,0 +1,14 @@
|
|||
{
|
||||
"ollama_endpoint": "http://localhost:11434",
|
||||
"ollama_model": "llama2",
|
||||
"silent_start": "20:00",
|
||||
"silent_end": "12:00",
|
||||
"min_interval": 3,
|
||||
"max_interval": 180,
|
||||
"bark_api_url": "",
|
||||
"bark_device_key": "",
|
||||
"ntfy_api_url": "",
|
||||
"ntfy_topic": "",
|
||||
"ntfy_access_token": "",
|
||||
"templates_dir": "templates"
|
||||
}
|
||||
|
|
@ -0,0 +1,47 @@
|
|||
import json
|
||||
from pathlib import Path
|
||||
from typing import Dict, Any
|
||||
from dataclasses import dataclass, asdict
|
||||
|
||||
@dataclass
|
||||
class Config:
|
||||
"""Configuration management for the notification system."""
|
||||
|
||||
ollama_endpoint: str = "http://localhost:11434"
|
||||
ollama_model: str = "llama2"
|
||||
|
||||
# Silent time configuration (12pm to 8am)
|
||||
silent_start: str = "20:00"
|
||||
silent_end: str = "12:00"
|
||||
|
||||
# Interval configuration (in minutes)
|
||||
min_interval: int = 3
|
||||
max_interval: int = 180
|
||||
|
||||
# Bark notification service
|
||||
bark_api_url: str = ""
|
||||
bark_device_key: str = ""
|
||||
|
||||
# Ntfy notification service
|
||||
ntfy_api_url: str = ""
|
||||
ntfy_topic: str = ""
|
||||
ntfy_access_token: str = ""
|
||||
|
||||
# Template settings
|
||||
templates_dir: str = "templates"
|
||||
|
||||
def __post_init__(self):
|
||||
"""Load configuration from file if exists."""
|
||||
config_file = Path("config.json")
|
||||
if config_file.exists():
|
||||
with open(config_file, 'r') as f:
|
||||
data = json.load(f)
|
||||
for key, value in data.items():
|
||||
if hasattr(self, key):
|
||||
setattr(self, key, value)
|
||||
|
||||
def save(self):
|
||||
"""Save current configuration to file."""
|
||||
config_file = Path("config.json")
|
||||
with open(config_file, 'w') as f:
|
||||
json.dump(asdict(self), f, indent=2)
|
||||
|
|
@ -0,0 +1,75 @@
|
|||
#!/usr/bin/env python3
|
||||
"""CLI tool for managing configuration."""
|
||||
|
||||
import argparse
|
||||
import asyncio
|
||||
from config import Config
|
||||
|
||||
async def main():
|
||||
parser = argparse.ArgumentParser(description="Ollama Notification System Configuration")
|
||||
parser.add_argument("--set-ollama-endpoint", help="Set Ollama endpoint URL")
|
||||
parser.add_argument("--set-ollama-model", help="Set Ollama model name")
|
||||
parser.add_argument("--set-silent-time", nargs=2, metavar=("START", "END"),
|
||||
help="Set silent time range (e.g., 20:00 12:00)")
|
||||
parser.add_argument("--set-interval-range", nargs=2, type=int, metavar=("MIN", "MAX"),
|
||||
help="Set notification interval range in minutes")
|
||||
parser.add_argument("--set-bark-url", help="Set Bark API URL")
|
||||
parser.add_argument("--set-bark-key", help="Set Bark device key")
|
||||
parser.add_argument("--set-ntfy-url", help="Set Ntfy API URL")
|
||||
parser.add_argument("--set-ntfy-topic", help="Set Ntfy topic")
|
||||
parser.add_argument("--set-ntfy-token", help="Set Ntfy access token")
|
||||
parser.add_argument("--show", action="store_true", help="Show current configuration")
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
config = Config()
|
||||
|
||||
if args.set_ollama_endpoint:
|
||||
config.ollama_endpoint = args.set_ollama_endpoint
|
||||
|
||||
if args.set_ollama_model:
|
||||
config.ollama_model = args.set_ollama_model
|
||||
|
||||
if args.set_silent_time:
|
||||
config.silent_start = args.set_silent_time[0]
|
||||
config.silent_end = args.set_silent_time[1]
|
||||
|
||||
if args.set_interval_range:
|
||||
config.min_interval = args.set_interval_range[0]
|
||||
config.max_interval = args.set_interval_range[1]
|
||||
|
||||
if args.set_bark_url:
|
||||
config.bark_api_url = args.set_bark_url
|
||||
|
||||
if args.set_bark_key:
|
||||
config.bark_device_key = args.set_bark_key
|
||||
|
||||
if args.set_ntfy_url:
|
||||
config.ntfy_api_url = args.set_ntfy_url
|
||||
|
||||
if args.set_ntfy_topic:
|
||||
config.ntfy_topic = args.set_ntfy_topic
|
||||
|
||||
if args.set_ntfy_token:
|
||||
config.ntfy_access_token = args.set_ntfy_token
|
||||
|
||||
if any([args.set_ollama_endpoint, args.set_ollama_model, args.set_silent_time,
|
||||
args.set_interval_range, args.set_bark_url, args.set_bark_key,
|
||||
args.set_ntfy_url, args.set_ntfy_topic, args.set_ntfy_token]):
|
||||
config.save()
|
||||
print("Configuration saved successfully!")
|
||||
|
||||
if args.show or not any(vars(args).values()):
|
||||
print("Current Configuration:")
|
||||
print(f" Ollama Endpoint: {config.ollama_endpoint}")
|
||||
print(f" Ollama Model: {config.ollama_model}")
|
||||
print(f" Silent Time: {config.silent_start} - {config.silent_end}")
|
||||
print(f" Interval Range: {config.min_interval} - {config.max_interval} minutes")
|
||||
print(f" Bark API URL: {config.bark_api_url or 'Not set'}")
|
||||
print(f" Bark Device Key: {'***' if config.bark_device_key else 'Not set'}")
|
||||
print(f" Ntfy API URL: {config.ntfy_api_url or 'Not set'}")
|
||||
print(f" Ntfy Topic: {config.ntfy_topic or 'Not set'}")
|
||||
print(f" Ntfy Token: {'***' if config.ntfy_access_token else 'Not set'}")
|
||||
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(main())
|
||||
|
|
@ -0,0 +1,23 @@
|
|||
import asyncio
|
||||
import logging
|
||||
from pathlib import Path
|
||||
from scheduler import NotificationScheduler
|
||||
from config import Config
|
||||
|
||||
logging.basicConfig(level=logging.INFO)
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
async def main():
|
||||
"""Main entry point for the notification system."""
|
||||
config = Config()
|
||||
scheduler = NotificationScheduler(config)
|
||||
|
||||
try:
|
||||
await scheduler.start()
|
||||
except KeyboardInterrupt:
|
||||
logger.info("Shutting down...")
|
||||
except Exception as e:
|
||||
logger.error(f"Error in main: {e}")
|
||||
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(main())
|
||||
|
|
@ -0,0 +1,119 @@
|
|||
import aiohttp
|
||||
import logging
|
||||
from typing import Dict, Any, Optional, List
|
||||
from config import Config
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
class NotificationClient:
|
||||
"""Client for sending notifications to both Bark and Ntfy services."""
|
||||
|
||||
def __init__(self, config: Config):
|
||||
self.config = config
|
||||
self.session = None
|
||||
|
||||
async def __aenter__(self):
|
||||
self.session = aiohttp.ClientSession()
|
||||
return self
|
||||
|
||||
async def __aexit__(self, exc_type, exc_val, exc_tb):
|
||||
if self.session:
|
||||
await self.session.close()
|
||||
|
||||
async def send_notification(self, message: str, title: str = "AI Thought") -> bool:
|
||||
"""Send notification to both Bark and Ntfy services."""
|
||||
results = []
|
||||
|
||||
# Send to Bark if configured
|
||||
if self.config.bark_api_url and self.config.bark_device_key:
|
||||
bark_success = await self._send_bark_notification(message, title)
|
||||
results.append(bark_success)
|
||||
|
||||
# Send to Ntfy if configured
|
||||
if self.config.ntfy_api_url and self.config.ntfy_topic:
|
||||
ntfy_success = await self._send_ntfy_notification(message, title)
|
||||
results.append(ntfy_success)
|
||||
|
||||
# Return True if at least one service succeeded
|
||||
return any(results) if results else True
|
||||
|
||||
async def _send_bark_notification(self, message: str, title: str) -> bool:
|
||||
"""Send notification to Bark service."""
|
||||
if not self.session:
|
||||
self.session = aiohttp.ClientSession()
|
||||
|
||||
# Bark API format: https://api.day.app/{device_key}/{title}/{body}
|
||||
url = f"{self.config.bark_api_url}/{self.config.bark_device_key}/{title}/{message}"
|
||||
|
||||
try:
|
||||
async with self.session.get(url) as response:
|
||||
if response.status == 200:
|
||||
data = await response.json()
|
||||
if data.get("code") == 200:
|
||||
logger.info("Bark notification sent successfully")
|
||||
return True
|
||||
else:
|
||||
logger.error(f"Bark API error: {data}")
|
||||
return False
|
||||
else:
|
||||
logger.error(f"Bark HTTP error: {response.status}")
|
||||
return False
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error sending Bark notification: {e}")
|
||||
return False
|
||||
|
||||
async def _send_ntfy_notification(self, message: str, title: str) -> bool:
|
||||
"""Send notification to Ntfy service."""
|
||||
if not self.session:
|
||||
self.session = aiohttp.ClientSession()
|
||||
|
||||
url = f"{self.config.ntfy_api_url}/{self.config.ntfy_topic}"
|
||||
|
||||
headers = {
|
||||
"Title": title,
|
||||
"Content-Type": "text/plain",
|
||||
"User-Agent": "Ollama-Notification-System/1.0"
|
||||
}
|
||||
|
||||
if self.config.ntfy_access_token:
|
||||
headers["Authorization"] = f"Bearer {self.config.ntfy_access_token}"
|
||||
|
||||
try:
|
||||
async with self.session.post(url, data=message, headers=headers) as response:
|
||||
if response.status == 200:
|
||||
logger.info("Ntfy notification sent successfully")
|
||||
return True
|
||||
else:
|
||||
logger.error(f"Ntfy HTTP error: {response.status}")
|
||||
return False
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error sending Ntfy notification: {e}")
|
||||
return False
|
||||
|
||||
async def test_connections(self) -> Dict[str, bool]:
|
||||
"""Test connections to both notification services."""
|
||||
results = {}
|
||||
|
||||
# Test Bark
|
||||
if self.config.bark_api_url:
|
||||
try:
|
||||
async with self.session.get(self.config.bark_api_url, timeout=aiohttp.ClientTimeout(total=10)) as response:
|
||||
results["bark"] = response.status < 400
|
||||
except Exception:
|
||||
results["bark"] = False
|
||||
else:
|
||||
results["bark"] = None
|
||||
|
||||
# Test Ntfy
|
||||
if self.config.ntfy_api_url:
|
||||
try:
|
||||
async with self.session.get(self.config.ntfy_api_url, timeout=aiohttp.ClientTimeout(total=10)) as response:
|
||||
results["ntfy"] = response.status < 400
|
||||
except Exception:
|
||||
results["ntfy"] = False
|
||||
else:
|
||||
results["ntfy"] = None
|
||||
|
||||
return results
|
||||
|
|
@ -0,0 +1,58 @@
|
|||
import aiohttp
|
||||
import json
|
||||
import logging
|
||||
from typing import Dict, Any, Optional
|
||||
from config import Config
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
class OllamaClient:
|
||||
"""Client for interacting with Ollama API."""
|
||||
|
||||
def __init__(self, config: Config):
|
||||
self.config = config
|
||||
self.session = None
|
||||
|
||||
async def __aenter__(self):
|
||||
self.session = aiohttp.ClientSession()
|
||||
return self
|
||||
|
||||
async def __aexit__(self, exc_type, exc_val, exc_tb):
|
||||
if self.session:
|
||||
await self.session.close()
|
||||
|
||||
async def generate_response(self, prompt: str) -> Optional[str]:
|
||||
"""Generate a response from Ollama for the given prompt."""
|
||||
if not self.session:
|
||||
self.session = aiohttp.ClientSession()
|
||||
|
||||
url = f"{self.config.ollama_endpoint}/api/generate"
|
||||
payload = {
|
||||
"model": self.config.ollama_model,
|
||||
"prompt": prompt,
|
||||
"stream": False
|
||||
}
|
||||
|
||||
try:
|
||||
async with self.session.post(url, json=payload) as response:
|
||||
if response.status == 200:
|
||||
data = await response.json()
|
||||
return data.get("response", "").strip()
|
||||
else:
|
||||
logger.error(f"Ollama API error: {response.status}")
|
||||
return None
|
||||
except Exception as e:
|
||||
logger.error(f"Error calling Ollama API: {e}")
|
||||
return None
|
||||
|
||||
async def check_health(self) -> bool:
|
||||
"""Check if Ollama service is available."""
|
||||
if not self.session:
|
||||
self.session = aiohttp.ClientSession()
|
||||
|
||||
try:
|
||||
async with self.session.get(f"{self.config.ollama_endpoint}/api/tags") as response:
|
||||
return response.status == 200
|
||||
except Exception as e:
|
||||
logger.error(f"Ollama health check failed: {e}")
|
||||
return False
|
||||
|
|
@ -0,0 +1,2 @@
|
|||
aiohttp>=3.8.0
|
||||
asyncio
|
||||
|
|
@ -0,0 +1,108 @@
|
|||
import asyncio
|
||||
import random
|
||||
import logging
|
||||
from datetime import datetime, time, timedelta
|
||||
from typing import Optional
|
||||
from config import Config
|
||||
from ollama_client import OllamaClient
|
||||
from template_manager import TemplateManager
|
||||
from notification_client import NotificationClient
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
class NotificationScheduler:
|
||||
"""Handles scheduling of notifications with random intervals and silent time."""
|
||||
|
||||
def __init__(self, config: Config):
|
||||
self.config = config
|
||||
self.template_manager = TemplateManager(config.templates_dir)
|
||||
self.notification_client = NotificationClient(config)
|
||||
self.running = False
|
||||
|
||||
def _is_silent_time(self) -> bool:
|
||||
"""Check if current time is within silent hours."""
|
||||
current_time = datetime.now().time()
|
||||
|
||||
# Parse silent time configuration
|
||||
silent_start = datetime.strptime(self.config.silent_start, "%H:%M").time()
|
||||
silent_end = datetime.strptime(self.config.silent_end, "%H:%M").time()
|
||||
|
||||
# Handle overnight silent period (e.g., 20:00 to 12:00)
|
||||
if silent_start >= silent_end:
|
||||
return current_time >= silent_start or current_time <= silent_end
|
||||
else:
|
||||
return silent_start <= current_time <= silent_end
|
||||
|
||||
def _get_next_interval(self) -> int:
|
||||
"""Get random interval in seconds between min and max."""
|
||||
minutes = random.randint(self.config.min_interval, self.config.max_interval)
|
||||
return minutes * 60
|
||||
|
||||
async def _send_notification(self) -> bool:
|
||||
"""Send a notification with Ollama response."""
|
||||
try:
|
||||
async with OllamaClient(self.config) as client:
|
||||
# Check if Ollama is available
|
||||
if not await client.check_health():
|
||||
logger.error("Ollama service is not available")
|
||||
return False
|
||||
|
||||
# Get random prompt
|
||||
prompt = self.template_manager.get_random_prompt()
|
||||
logger.info(f"Using prompt: {prompt}")
|
||||
|
||||
# Get response from Ollama
|
||||
response = await client.generate_response(prompt)
|
||||
if not response:
|
||||
logger.error("Failed to get response from Ollama")
|
||||
return False
|
||||
|
||||
# Send notification
|
||||
success = await self.notification_client.send_notification(response)
|
||||
if success:
|
||||
logger.info("Notification sent successfully")
|
||||
else:
|
||||
logger.error("Failed to send notification")
|
||||
|
||||
return success
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error sending notification: {e}")
|
||||
return False
|
||||
|
||||
async def start(self):
|
||||
"""Start the notification scheduler."""
|
||||
self.running = True
|
||||
logger.info("Starting notification scheduler...")
|
||||
|
||||
while self.running:
|
||||
try:
|
||||
# Check if it's silent time
|
||||
if self._is_silent_time():
|
||||
logger.info("Silent time - skipping notification")
|
||||
# Wait 30 minutes before checking again
|
||||
await asyncio.sleep(1800)
|
||||
continue
|
||||
|
||||
# Send notification
|
||||
await self._send_notification()
|
||||
|
||||
# Calculate next interval
|
||||
interval = self._get_next_interval()
|
||||
next_time = datetime.now() + timedelta(seconds=interval)
|
||||
logger.info(f"Next notification scheduled for: {next_time.strftime('%Y-%m-%d %H:%M:%S')}")
|
||||
|
||||
# Wait for next interval
|
||||
await asyncio.sleep(interval)
|
||||
|
||||
except asyncio.CancelledError:
|
||||
logger.info("Scheduler cancelled")
|
||||
break
|
||||
except Exception as e:
|
||||
logger.error(f"Error in scheduler: {e}")
|
||||
# Wait 5 minutes on error
|
||||
await asyncio.sleep(300)
|
||||
|
||||
def stop(self):
|
||||
"""Stop the notification scheduler."""
|
||||
self.running = False
|
||||
|
|
@ -0,0 +1,122 @@
|
|||
import json
|
||||
import random
|
||||
from pathlib import Path
|
||||
from datetime import datetime
|
||||
from typing import List, Dict, Any
|
||||
import logging
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
class TemplateManager:
|
||||
"""Manages prompt templates with placeholders and random selection."""
|
||||
|
||||
def __init__(self, templates_dir: str = "templates"):
|
||||
self.templates_dir = Path(templates_dir)
|
||||
self.templates_dir.mkdir(exist_ok=True)
|
||||
self.templates = []
|
||||
self._load_templates()
|
||||
|
||||
def _load_templates(self):
|
||||
"""Load templates from JSON files in the templates directory."""
|
||||
if not self.templates_dir.exists():
|
||||
self._create_default_templates()
|
||||
|
||||
for template_file in self.templates_dir.glob("*.json"):
|
||||
try:
|
||||
with open(template_file, 'r') as f:
|
||||
template_data = json.load(f)
|
||||
if isinstance(template_data, list):
|
||||
self.templates.extend(template_data)
|
||||
else:
|
||||
self.templates.append(template_data)
|
||||
except Exception as e:
|
||||
logger.error(f"Error loading template {template_file}: {e}")
|
||||
|
||||
if not self.templates:
|
||||
self._create_default_templates()
|
||||
|
||||
def _create_default_templates(self):
|
||||
"""Create default templates if none exist."""
|
||||
default_templates = [
|
||||
{
|
||||
"name": "Daily Reflection",
|
||||
"prompt": "What are your thoughts about {date} at {time}?",
|
||||
"description": "A simple daily reflection prompt"
|
||||
},
|
||||
{
|
||||
"name": "Mindfulness Check",
|
||||
"prompt": "How are you feeling right now on this {day} at {time}?",
|
||||
"description": "A mindfulness check-in prompt"
|
||||
},
|
||||
{
|
||||
"name": "Gratitude Prompt",
|
||||
"prompt": "What are three things you're grateful for on {date} during this {time_of_day}?",
|
||||
"description": "A gratitude reflection prompt"
|
||||
}
|
||||
]
|
||||
|
||||
template_file = self.templates_dir / "default.json"
|
||||
with open(template_file, 'w') as f:
|
||||
json.dump(default_templates, f, indent=2)
|
||||
|
||||
self.templates.extend(default_templates)
|
||||
|
||||
def _get_time_of_day(self, hour: int) -> str:
|
||||
"""Return time of day based on hour."""
|
||||
if 5 <= hour < 12:
|
||||
return "morning"
|
||||
elif 12 <= hour < 17:
|
||||
return "afternoon"
|
||||
elif 17 <= hour < 21:
|
||||
return "evening"
|
||||
else:
|
||||
return "night"
|
||||
|
||||
def _fill_placeholders(self, template: str) -> str:
|
||||
"""Fill placeholders in template with current values."""
|
||||
now = datetime.now()
|
||||
placeholders = {
|
||||
"date": now.strftime("%Y-%m-%d"),
|
||||
"time": now.strftime("%H:%M"),
|
||||
"day": now.strftime("%A"),
|
||||
"time_of_day": self._get_time_of_day(now.hour),
|
||||
"weekday": now.strftime("%A"),
|
||||
"month": now.strftime("%B"),
|
||||
"day_of_month": str(now.day)
|
||||
}
|
||||
|
||||
for placeholder, value in placeholders.items():
|
||||
template = template.replace(f"{{{placeholder}}}", value)
|
||||
|
||||
return template
|
||||
|
||||
def get_random_prompt(self) -> str:
|
||||
"""Get a random prompt template with placeholders filled."""
|
||||
if not self.templates:
|
||||
logger.warning("No templates available")
|
||||
return "What are your thoughts right now?"
|
||||
|
||||
template = random.choice(self.templates)
|
||||
prompt = template.get("prompt", "")
|
||||
return self._fill_placeholders(prompt)
|
||||
|
||||
def add_template(self, name: str, prompt: str, description: str = ""):
|
||||
"""Add a new template to the system."""
|
||||
new_template = {
|
||||
"name": name,
|
||||
"prompt": prompt,
|
||||
"description": description
|
||||
}
|
||||
self.templates.append(new_template)
|
||||
|
||||
# Save to file
|
||||
template_file = self.templates_dir / "custom.json"
|
||||
custom_templates = []
|
||||
if template_file.exists():
|
||||
with open(template_file, 'r') as f:
|
||||
custom_templates = json.load(f)
|
||||
|
||||
custom_templates.append(new_template)
|
||||
|
||||
with open(template_file, 'w') as f:
|
||||
json.dump(custom_templates, f, indent=2)
|
||||
Loading…
Reference in New Issue