mirror of
https://github.com/openswarm-ai/openswarm.git
synced 2026-09-12 20:57:42 +02:00
[eric] patterns: mine session history for repeated behaviors, offer one-click workflow creation
This commit is contained in:
@@ -0,0 +1,110 @@
|
||||
"""Routes + lifecycle for pattern suggestions. The miner runs shortly after
|
||||
boot and then daily; accepting a suggestion creates a REAL workflow through
|
||||
the same create path the Workflows UI uses, so the user reviews and owns it
|
||||
like any other."""
|
||||
|
||||
import asyncio
|
||||
import logging
|
||||
from contextlib import asynccontextmanager
|
||||
from typing import Optional
|
||||
|
||||
from fastapi import HTTPException
|
||||
|
||||
from backend.apps.patterns import store
|
||||
from backend.apps.patterns.miner import run_mining_pass
|
||||
from backend.apps.patterns.models import WorkflowSuggestion
|
||||
from backend.config.Apps import SubApp
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
BOOT_DELAY_SECONDS = 45.0
|
||||
CHECK_INTERVAL_SECONDS = 6 * 3600.0
|
||||
|
||||
p_loop_task: Optional["asyncio.Task"] = None
|
||||
|
||||
|
||||
async def p_mining_loop() -> None:
|
||||
await asyncio.sleep(BOOT_DELAY_SECONDS)
|
||||
while True:
|
||||
try:
|
||||
added = await run_mining_pass()
|
||||
if added:
|
||||
logger.info("[pattern-miner] added %d suggestion(s)", added)
|
||||
except Exception:
|
||||
logger.exception("pattern mining loop error")
|
||||
await asyncio.sleep(CHECK_INTERVAL_SECONDS)
|
||||
|
||||
|
||||
@asynccontextmanager
|
||||
async def patterns_lifespan():
|
||||
global p_loop_task
|
||||
p_loop_task = asyncio.create_task(p_mining_loop())
|
||||
try:
|
||||
yield
|
||||
finally:
|
||||
if p_loop_task is not None:
|
||||
p_loop_task.cancel()
|
||||
try:
|
||||
await p_loop_task
|
||||
except (asyncio.CancelledError, Exception):
|
||||
pass
|
||||
p_loop_task = None
|
||||
|
||||
|
||||
patterns = SubApp("patterns", patterns_lifespan)
|
||||
|
||||
|
||||
@patterns.router.get("/suggestions")
|
||||
async def list_suggestions():
|
||||
return {"suggestions": [s.model_dump(mode="json") for s in store.pending_suggestions()]}
|
||||
|
||||
|
||||
def p_get_pending_or_404(suggestion_id: str) -> WorkflowSuggestion:
|
||||
suggestion = store.get_suggestion(suggestion_id)
|
||||
if suggestion is None:
|
||||
raise HTTPException(status_code=404, detail="Suggestion not found")
|
||||
if suggestion.status != "pending":
|
||||
raise HTTPException(status_code=409, detail=f"Suggestion already {suggestion.status}")
|
||||
return suggestion
|
||||
|
||||
|
||||
@patterns.router.post("/suggestions/{suggestion_id}/accept")
|
||||
async def accept_suggestion(suggestion_id: str):
|
||||
from backend.apps.workflows.models import ScheduleConfig, WorkflowCreate, WorkflowStep
|
||||
from backend.apps.workflows.workflows import create_workflow
|
||||
|
||||
suggestion = p_get_pending_or_404(suggestion_id)
|
||||
if suggestion.cadence.kind == "weekly":
|
||||
schedule = ScheduleConfig(enabled=True, repeat_unit="week", repeat_every=1, on_days=suggestion.cadence.on_days, hour=suggestion.cadence.hour, minute=0)
|
||||
elif suggestion.cadence.kind == "daily":
|
||||
schedule = ScheduleConfig(enabled=True, repeat_unit="day", repeat_every=1, hour=suggestion.cadence.hour, minute=0)
|
||||
else:
|
||||
# No clear rhythm in the evidence: create it ready to run, let the user schedule or add a trigger.
|
||||
schedule = ScheduleConfig(enabled=False)
|
||||
body = WorkflowCreate(
|
||||
title=suggestion.workflow_title or "Suggested workflow",
|
||||
description=suggestion.description,
|
||||
steps=[WorkflowStep(text=t) for t in suggestion.workflow_steps],
|
||||
schedule=schedule,
|
||||
auto_named=False,
|
||||
)
|
||||
workflow = await create_workflow(body)
|
||||
suggestion.status = "accepted"
|
||||
suggestion.workflow_id = str(workflow.get("id") or "") or None
|
||||
store.update_suggestion(suggestion)
|
||||
return {"suggestion": suggestion.model_dump(mode="json"), "workflow": workflow}
|
||||
|
||||
|
||||
@patterns.router.post("/suggestions/{suggestion_id}/dismiss")
|
||||
async def dismiss_suggestion(suggestion_id: str):
|
||||
suggestion = p_get_pending_or_404(suggestion_id)
|
||||
suggestion.status = "dismissed"
|
||||
store.update_suggestion(suggestion)
|
||||
return {"ok": True}
|
||||
|
||||
|
||||
@patterns.router.post("/mine")
|
||||
async def mine_now():
|
||||
"""Force a mining pass (ignores the daily throttle; the settings kill switch still applies)."""
|
||||
added = await run_mining_pass(force=True)
|
||||
return {"added": added, "pending": len(store.pending_suggestions())}
|
||||
Reference in New Issue
Block a user