Files
robbyczgw-cla_topic-monitor/scripts/config.py

264 lines
7.1 KiB
Python

#!/usr/bin/env python3
"""
Configuration loader for proactive-research skill.
"""
import json
import os
from pathlib import Path
from typing import Dict, List, Optional
SKILL_DIR = Path(__file__).parent.parent
CONFIG_FILE = SKILL_DIR / "config.json"
# State files: configurable via TOPIC_MONITOR_DATA_DIR env, defaults to skill-local .data/
MEMORY_DIR = Path(os.environ.get("TOPIC_MONITOR_DATA_DIR", os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))), ".data")))
STATE_FILE = MEMORY_DIR / "topic-monitor-state.json"
FINDINGS_DIR = MEMORY_DIR / "findings"
ALERTS_QUEUE = MEMORY_DIR / "alerts-queue.json"
def ensure_memory_dir():
"""Ensure memory directory structure exists."""
MEMORY_DIR.mkdir(parents=True, exist_ok=True)
FINDINGS_DIR.mkdir(parents=True, exist_ok=True)
def load_config() -> Dict:
"""Load configuration from config.json."""
if not CONFIG_FILE.exists():
raise FileNotFoundError(
f"Config file not found: {CONFIG_FILE}\n"
"Copy config.example.json to config.json and customize it."
)
with open(CONFIG_FILE) as f:
return json.load(f)
def save_config(config: Dict):
"""Save configuration to config.json."""
with open(CONFIG_FILE, 'w') as f:
json.dump(config, f, indent=2)
def load_state() -> Dict:
"""Load state from topic-monitor-state.json in memory/monitors/."""
ensure_memory_dir()
if STATE_FILE.exists():
with open(STATE_FILE) as f:
return json.load(f)
return {
"topics": {},
"deduplication": {"url_hash_map": {}},
"learning": {"interactions": []},
"feeds": {},
"sentiment": {}
}
def save_state(state: Dict):
"""Save state to topic-monitor-state.json in memory/monitors/."""
ensure_memory_dir()
with open(STATE_FILE, 'w') as f:
json.dump(state, f, indent=2)
def get_topics() -> List[Dict]:
"""Get all topics from config."""
config = load_config()
return config.get("topics", [])
def get_topic(topic_id: str) -> Optional[Dict]:
"""Get a specific topic by ID."""
topics = get_topics()
for topic in topics:
if topic.get("id") == topic_id:
return topic
return None
def get_settings() -> Dict:
"""Get global settings."""
config = load_config()
return config.get("settings", {})
def get_channel_config(channel: str) -> Dict:
"""Get channel-specific configuration."""
config = load_config()
channels = config.get("channels", {})
return channels.get(channel, {})
def ensure_findings_dir():
"""Ensure findings directory exists in memory/monitors/."""
ensure_memory_dir()
FINDINGS_DIR.mkdir(exist_ok=True)
def get_findings_file(topic_id: str, date_str: str) -> Path:
"""Get path to findings file for topic and date."""
ensure_findings_dir()
return FINDINGS_DIR / f"{date_str}_{topic_id}.json"
def save_finding(topic_id: str, date_str: str, finding: Dict):
"""Save a finding to the findings directory."""
findings_file = get_findings_file(topic_id, date_str)
# Load existing findings
findings = []
if findings_file.exists():
with open(findings_file) as f:
findings = json.load(f)
# Append new finding
findings.append(finding)
# Save
with open(findings_file, 'w') as f:
json.dump(findings, f, indent=2)
def load_findings(topic_id: str, date_str: str) -> List[Dict]:
"""Load findings for a topic and date."""
findings_file = get_findings_file(topic_id, date_str)
if findings_file.exists():
with open(findings_file) as f:
return json.load(f)
return []
# ============================================================================
# ALERTS QUEUE - For real-time alerting via OpenClaw agent
# ============================================================================
def queue_alert(alert: Dict):
"""
Queue an alert for delivery by the OpenClaw agent.
Alert format:
{
"id": "unique-id",
"timestamp": "ISO timestamp",
"priority": "high|medium|low",
"channel": "telegram|discord|email",
"topic_id": "topic-id",
"topic_name": "Topic Name",
"title": "Result title",
"snippet": "Result snippet",
"url": "https://...",
"score": 0.75,
"reason": "scoring reason",
"sent": false
}
"""
ensure_memory_dir()
# Load existing queue
queue = []
if ALERTS_QUEUE.exists():
try:
with open(ALERTS_QUEUE) as f:
queue = json.load(f)
except (json.JSONDecodeError, IOError):
queue = []
# Add alert with unique ID
import hashlib
from datetime import datetime
alert_id = hashlib.md5(
f"{alert.get('url', '')}{alert.get('timestamp', '')}".encode()
).hexdigest()[:12]
alert["id"] = alert_id
alert["sent"] = False
if "timestamp" not in alert:
alert["timestamp"] = datetime.now().isoformat()
# Avoid duplicates
existing_ids = {a.get("id") for a in queue}
if alert_id not in existing_ids:
queue.append(alert)
# Save queue
with open(ALERTS_QUEUE, 'w') as f:
json.dump(queue, f, indent=2)
return alert_id
def get_pending_alerts() -> List[Dict]:
"""Get all unsent alerts from the queue."""
ensure_memory_dir()
if not ALERTS_QUEUE.exists():
return []
try:
with open(ALERTS_QUEUE) as f:
queue = json.load(f)
except (json.JSONDecodeError, IOError):
return []
return [a for a in queue if not a.get("sent", False)]
def mark_alert_sent(alert_id: str):
"""Mark an alert as sent."""
ensure_memory_dir()
if not ALERTS_QUEUE.exists():
return
try:
with open(ALERTS_QUEUE) as f:
queue = json.load(f)
except (json.JSONDecodeError, IOError):
return
for alert in queue:
if alert.get("id") == alert_id:
alert["sent"] = True
alert["sent_at"] = json.dumps({"_": "now"})[7:-2] # hack for timestamp
from datetime import datetime
alert["sent_at"] = datetime.now().isoformat()
break
with open(ALERTS_QUEUE, 'w') as f:
json.dump(queue, f, indent=2)
def clear_old_alerts(max_age_hours: int = 168):
"""Clear alerts older than max_age_hours (default 7 days)."""
ensure_memory_dir()
if not ALERTS_QUEUE.exists():
return
from datetime import datetime, timedelta
try:
with open(ALERTS_QUEUE) as f:
queue = json.load(f)
except (json.JSONDecodeError, IOError):
return
cutoff = datetime.now() - timedelta(hours=max_age_hours)
new_queue = []
for alert in queue:
try:
ts = datetime.fromisoformat(alert.get("timestamp", ""))
if ts > cutoff:
new_queue.append(alert)
except (ValueError, TypeError):
# Keep alerts with invalid timestamps (let them be manually reviewed)
new_queue.append(alert)
with open(ALERTS_QUEUE, 'w') as f:
json.dump(new_queue, f, indent=2)