Initial commit with translated description
This commit is contained in:
263
scripts/config.py
Normal file
263
scripts/config.py
Normal file
@@ -0,0 +1,263 @@
|
||||
#!/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)
|
||||
Reference in New Issue
Block a user