Wire scheduler to API/UI and improve loop control and PR headlines
- Pass scheduler instance to API router and UI for loop timestamps and force-run endpoints - Add interruptible waits and force_heartbeat/force_poll methods - Record last_run timestamps for all scheduler loops in KV store - Update press release headline examples with real client headlines Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>cora-start
parent
ffa8ad49e5
commit
0e3e3bc945
|
|
@ -122,6 +122,7 @@ def main():
|
|||
log.warning("Notification bus not available: %s", e)
|
||||
|
||||
# Scheduler (uses default agent)
|
||||
scheduler = None
|
||||
try:
|
||||
from .scheduler import Scheduler
|
||||
|
||||
|
|
@ -132,7 +133,9 @@ def main():
|
|||
log.warning("Scheduler not available: %s", e)
|
||||
|
||||
log.info("Launching Gradio UI on %s:%s...", config.host, config.port)
|
||||
blocks = create_ui(registry, config, default_llm, notification_bus=notification_bus)
|
||||
blocks = create_ui(
|
||||
registry, config, default_llm, notification_bus=notification_bus, scheduler=scheduler
|
||||
)
|
||||
|
||||
# Build a parent FastAPI app so we can mount the dashboard alongside Gradio.
|
||||
# Inserting routes into blocks.app before launch() doesn't work because
|
||||
|
|
@ -148,7 +151,7 @@ def main():
|
|||
# Mount API endpoints
|
||||
from .api import create_api_router
|
||||
|
||||
api_router = create_api_router(config, db, registry)
|
||||
api_router = create_api_router(config, db, registry, scheduler=scheduler)
|
||||
fastapi_app.include_router(api_router)
|
||||
log.info("API router mounted at /api/")
|
||||
|
||||
|
|
|
|||
|
|
@ -49,6 +49,8 @@ class Scheduler:
|
|||
self.agent = agent
|
||||
self.notification_bus = notification_bus
|
||||
self._stop_event = threading.Event()
|
||||
self._force_heartbeat = threading.Event()
|
||||
self._force_poll = threading.Event()
|
||||
self._thread: threading.Thread | None = None
|
||||
self._heartbeat_thread: threading.Thread | None = None
|
||||
self._clickup_thread: threading.Thread | None = None
|
||||
|
|
@ -111,15 +113,49 @@ class Scheduler:
|
|||
else:
|
||||
log.info("Notification [%s]: %s", category, message)
|
||||
|
||||
# ── Loop control ──
|
||||
|
||||
def _interruptible_wait(self, seconds: float, force_event: threading.Event | None = None):
|
||||
"""Wait for *seconds*, returning early if stop or force event fires."""
|
||||
remaining = seconds
|
||||
while remaining > 0 and not self._stop_event.is_set():
|
||||
if force_event and force_event.is_set():
|
||||
force_event.clear()
|
||||
return
|
||||
self._stop_event.wait(min(5, remaining))
|
||||
remaining -= 5
|
||||
|
||||
def force_heartbeat(self):
|
||||
"""Wake the heartbeat loop immediately."""
|
||||
self._force_heartbeat.set()
|
||||
|
||||
def force_poll(self):
|
||||
"""Wake the scheduler poll loop immediately."""
|
||||
self._force_poll.set()
|
||||
|
||||
def get_loop_timestamps(self) -> dict[str, str | None]:
|
||||
"""Return last_run timestamps for all loops."""
|
||||
return {
|
||||
"heartbeat": self.db.kv_get("system:loop:heartbeat:last_run"),
|
||||
"poll": self.db.kv_get("system:loop:poll:last_run"),
|
||||
"clickup": self.db.kv_get("system:loop:clickup:last_run"),
|
||||
"folder_watch": self.db.kv_get("system:loop:folder_watch:last_run"),
|
||||
}
|
||||
|
||||
# ── Scheduled Tasks ──
|
||||
|
||||
def _poll_loop(self):
|
||||
while not self._stop_event.is_set():
|
||||
try:
|
||||
self._run_due_tasks()
|
||||
self.db.kv_set(
|
||||
"system:loop:poll:last_run", datetime.now(UTC).isoformat()
|
||||
)
|
||||
except Exception as e:
|
||||
log.error("Scheduler poll error: %s", e)
|
||||
self._stop_event.wait(self.config.scheduler.poll_interval_seconds)
|
||||
self._interruptible_wait(
|
||||
self.config.scheduler.poll_interval_seconds, self._force_poll
|
||||
)
|
||||
|
||||
def _run_due_tasks(self):
|
||||
tasks = self.db.get_due_tasks()
|
||||
|
|
@ -154,9 +190,12 @@ class Scheduler:
|
|||
while not self._stop_event.is_set():
|
||||
try:
|
||||
self._run_heartbeat()
|
||||
self.db.kv_set(
|
||||
"system:loop:heartbeat:last_run", datetime.now(UTC).isoformat()
|
||||
)
|
||||
except Exception as e:
|
||||
log.error("Heartbeat error: %s", e)
|
||||
self._stop_event.wait(interval)
|
||||
self._interruptible_wait(interval, self._force_heartbeat)
|
||||
|
||||
def _run_heartbeat(self):
|
||||
heartbeat_path = self.config.identity_dir / "HEARTBEAT.md"
|
||||
|
|
@ -201,9 +240,12 @@ class Scheduler:
|
|||
while not self._stop_event.is_set():
|
||||
try:
|
||||
self._poll_clickup()
|
||||
self.db.kv_set(
|
||||
"system:loop:clickup:last_run", datetime.now(UTC).isoformat()
|
||||
)
|
||||
except Exception as e:
|
||||
log.error("ClickUp poll error: %s", e)
|
||||
self._stop_event.wait(interval)
|
||||
self._interruptible_wait(interval)
|
||||
|
||||
def _discover_field_filter(self, client):
|
||||
"""Discover and cache the Work Category field UUID + option map."""
|
||||
|
|
@ -468,9 +510,12 @@ class Scheduler:
|
|||
while not self._stop_event.is_set():
|
||||
try:
|
||||
self._scan_watch_folder()
|
||||
self.db.kv_set(
|
||||
"system:loop:folder_watch:last_run", datetime.now(UTC).isoformat()
|
||||
)
|
||||
except Exception as e:
|
||||
log.error("Folder watcher error: %s", e)
|
||||
self._stop_event.wait(interval)
|
||||
self._interruptible_wait(interval)
|
||||
|
||||
def _scan_watch_folder(self):
|
||||
"""Scan the watch folder for new .xlsx files and match to ClickUp tasks."""
|
||||
|
|
|
|||
|
|
@ -20,8 +20,20 @@ When the user provides a press release topic, follow this workflow:
|
|||
- Title case
|
||||
- News-focused (not promotional)
|
||||
- Free of location keywords, superlatives (best/top/leading/#1), and questions
|
||||
- Contains actual news announcement
|
||||
- Not make up information that isn't true.
|
||||
- Present all 7 titles to an AI agent to judge which is best. This can be decided by looking at titles on Press Advantage for other businesses, and seeing how closely the headline follows the instructions.
|
||||
|
||||
** EXAMPLE GREAT HEADLINES: **
|
||||
- Dietz Electric Highlights Flameproof Motor Safety Options
|
||||
- MOD-TRONIC Reaffirms Position as Largest MINCO Stocking Distributor
|
||||
- Hogge Precision Parts Delivers Precision Machining for the Medical Industry
|
||||
- Lubrication Engineers Drives Awareness of Fuel Treatment Benefits for Year-Round Fleet Efficiency
|
||||
- Renown Electric Champions Proactive Downtime Protection With Contingency Planning Insights
|
||||
- MCM Composites Releases Enhanced Thermoset Comparison Resource
|
||||
- AGI Fabricators Publishes New Resource on Custom Process Hopper Fabrication
|
||||
- Paragon Steel Strengthens Support For Central Los Angeles Commercial Projects
|
||||
- McCormick Industries Reinforces Quality Standards With ISO 9001:2015-Certified Medical Machining
|
||||
|
||||
|
||||
2. **Gather Any Additional Required Information**:
|
||||
- If the user provides LSI terms explicitly, use them
|
||||
|
|
@ -58,19 +70,6 @@ When generating the 7 headline options:
|
|||
- No promotional language (e.g., "Revolutionary," "Game-Changing")
|
||||
- Focus on the news, not the hype
|
||||
|
||||
**Examples of Good Headlines**:
|
||||
- "TechCorp Launches AI-Powered Customer Service Platform" (56 chars)
|
||||
- "Green Solutions Secures $50M Series B Funding Round" (52 chars)
|
||||
- "Acme Industries Expands Operations to European Markets" (55 chars)
|
||||
- "DataFlow Announces Strategic Partnership with IBM" (50 chars)
|
||||
- "HealthTech Achieves ISO 27001 Certification" (44 chars)
|
||||
- Also check the headlines.md file (if it exists) for other examples of good headlines.
|
||||
|
||||
**Examples of Bad Headlines** (DO NOT USE):
|
||||
- ❌ "Is Your Business Ready for AI Customer Service?" (question)
|
||||
- ❌ "Chicago's Leading TechCorp Launches New Platform" (location + superlative)
|
||||
- ❌ "Best-in-Class AI Solution Revolutionizes Support" (superlative + hype)
|
||||
- ❌ "TechCorp: The #1 Choice for Customer Service AI" (superlative + promotional)
|
||||
|
||||
## Critical Press Advantage Requirements
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue