mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-07-20 15:33:54 +00:00
refactor(cron): rebrand Cron Recipes -> Automation Blueprints
Product rename across every surface: module/file names (blueprint_catalog, tools/blueprints, blueprint_cmd), slash command /cron-recipe -> /blueprint (alias /bp), dashboard API /api/cron/blueprints, desktop deep-link hermes://blueprint/<key>, docs catalog page + extract script, and the skill frontmatter block metadata.hermes.blueprint. No behavior change.
This commit is contained in:
parent
3c489fda81
commit
cb29e8a82e
29 changed files with 627 additions and 627 deletions
|
|
@ -1,25 +1,25 @@
|
|||
"""Shared ``/cron-recipe`` command logic for CLI, TUI, and gateway.
|
||||
"""Shared ``/blueprint`` command logic for CLI, TUI, and gateway.
|
||||
|
||||
The conversational counterpart to the dashboard's Cron Recipes form. Where a
|
||||
The conversational counterpart to the dashboard's Automation Blueprints form. Where a
|
||||
surface has a screen, the user fills a form (dashboard / GUI app) and the API
|
||||
calls ``fill_recipe`` -> ``create_job`` directly. Where a surface is just a
|
||||
chat line, the user picks a recipe by name and the agent asks for what it
|
||||
needs — pick a recipe by name and the agent asks you for what it needs, one
|
||||
question at a time (the messaging-assistant model: pick a recipe → it asks you
|
||||
calls ``fill_blueprint`` -> ``create_job`` directly. Where a surface is just a
|
||||
chat line, the user picks a blueprint by name and the agent asks for what it
|
||||
needs — pick a blueprint by name and the agent asks you for what it needs, one
|
||||
question at a time (the messaging-assistant model: pick a blueprint → it asks you
|
||||
a couple things → done).
|
||||
|
||||
Subcommand shapes:
|
||||
/cron-recipe list the catalog
|
||||
/cron-recipe <name> name-match a recipe, then SEED THE AGENT to
|
||||
/blueprint list the catalog
|
||||
/blueprint <name> name-match a blueprint, then SEED THE AGENT to
|
||||
ask the user for each value conversationally
|
||||
/cron-recipe <name> slot=val … fill + create the cron job directly
|
||||
/blueprint <name> slot=val … fill + create the cron job directly
|
||||
(the deterministic dashboard / docs / power-
|
||||
user shortcut — no agent turn)
|
||||
|
||||
The ``<name>`` form is forgiving: exact key, unique prefix, or fuzzy match all
|
||||
resolve; an ambiguous query lists the candidates; an unknown one suggests the
|
||||
closest. When it resolves, the handler returns an ``agent_seed`` — a natural-
|
||||
language instruction built from the recipe's typed slots + schedule/prompt
|
||||
language instruction built from the blueprint's typed slots + schedule/prompt
|
||||
templates — that the calling surface feeds to the agent as a normal user turn
|
||||
(gateway: rewrite ``event.text`` and fall through, the ``/steer`` pattern; CLI:
|
||||
a one-shot pending seed the main loop runs). The agent then asks for each slot
|
||||
|
|
@ -41,12 +41,12 @@ logger = logging.getLogger(__name__)
|
|||
|
||||
|
||||
@dataclass
|
||||
class RecipeCommandResult:
|
||||
"""Outcome of a ``/cron-recipe`` invocation.
|
||||
class BlueprintCommandResult:
|
||||
"""Outcome of a ``/blueprint`` invocation.
|
||||
|
||||
``text`` is always shown to the user. When ``agent_seed`` is set, the
|
||||
calling surface should ALSO hand that seed to the agent as the user's next
|
||||
turn (the recipe was matched and now the agent gathers the slot values
|
||||
turn (the blueprint was matched and now the agent gathers the slot values
|
||||
conversationally). When ``agent_seed`` is None the command is fully handled
|
||||
(catalog listing, direct create, or an error) and nothing is sent to the
|
||||
agent.
|
||||
|
|
@ -91,11 +91,11 @@ def _parse_kv(tokens) -> Tuple[Dict[str, str], list]:
|
|||
return values, leftovers
|
||||
|
||||
|
||||
def match_recipe(query: str) -> Tuple[Optional[Any], List[Any]]:
|
||||
"""Resolve a free-typed recipe name to a recipe.
|
||||
def match_blueprint(query: str) -> Tuple[Optional[Any], List[Any]]:
|
||||
"""Resolve a free-typed blueprint name to a blueprint.
|
||||
|
||||
Returns ``(recipe, candidates)``:
|
||||
* exact key or unique prefix / fuzzy match -> ``(recipe, [])``
|
||||
Returns ``(blueprint, candidates)``:
|
||||
* exact key or unique prefix / fuzzy match -> ``(blueprint, [])``
|
||||
* ambiguous (2+ plausible) -> ``(None, [candidates…])``
|
||||
* no plausible match -> ``(None, [])``
|
||||
|
||||
|
|
@ -103,13 +103,13 @@ def match_recipe(query: str) -> Tuple[Optional[Any], List[Any]]:
|
|||
dashboard/Discord where it's picked): exact key first, then case-insensitive
|
||||
prefix on key or title, then a difflib fuzzy pass.
|
||||
"""
|
||||
from cron.recipe_catalog import CATALOG, get_recipe
|
||||
from cron.blueprint_catalog import CATALOG, get_blueprint
|
||||
|
||||
q = (query or "").strip().lower()
|
||||
if not q:
|
||||
return None, []
|
||||
|
||||
exact = get_recipe(q)
|
||||
exact = get_blueprint(q)
|
||||
if exact is not None:
|
||||
return exact, []
|
||||
|
||||
|
|
@ -138,43 +138,43 @@ def match_recipe(query: str) -> Tuple[Optional[Any], List[Any]]:
|
|||
keys = [r.key for r in CATALOG]
|
||||
close = difflib.get_close_matches(q, keys, n=3, cutoff=0.6)
|
||||
if len(close) == 1:
|
||||
return get_recipe(close[0]), []
|
||||
return get_blueprint(close[0]), []
|
||||
if len(close) > 1:
|
||||
return None, [get_recipe(k) for k in close]
|
||||
return None, [get_blueprint(k) for k in close]
|
||||
|
||||
return None, []
|
||||
|
||||
|
||||
def _humanize_schedule(recipe) -> str:
|
||||
from cron.recipe_catalog import _humanize_schedule as _h
|
||||
def _humanize_schedule(blueprint) -> str:
|
||||
from cron.blueprint_catalog import _humanize_schedule as _h
|
||||
|
||||
try:
|
||||
return _h(recipe)
|
||||
return _h(blueprint)
|
||||
except Exception:
|
||||
return "on a schedule"
|
||||
|
||||
|
||||
def build_recipe_seed(recipe) -> str:
|
||||
def build_blueprint_seed(blueprint) -> str:
|
||||
"""Build the natural-language fill-request the agent will act on.
|
||||
|
||||
The agent reads this as a normal user turn, asks the user for each unfilled
|
||||
slot one at a time, then calls the ``cronjob`` tool with the
|
||||
cron expression it builds from the recipe's ``schedule_template`` and the
|
||||
cron expression it builds from the blueprint's ``schedule_template`` and the
|
||||
rendered prompt. Defaults are stated so the agent can offer them.
|
||||
"""
|
||||
from cron.recipe_catalog import WEEKDAY_PRESETS
|
||||
from cron.blueprint_catalog import WEEKDAY_PRESETS
|
||||
|
||||
lines: List[str] = []
|
||||
lines.append(
|
||||
f"Set up the '{recipe.title}' automation for me (cron recipe "
|
||||
f"'{recipe.key}'). {recipe.description}"
|
||||
f"Set up the '{blueprint.title}' automation for me (automation blueprint "
|
||||
f"'{blueprint.key}'). {blueprint.description}"
|
||||
)
|
||||
lines.append("")
|
||||
lines.append(
|
||||
"Ask me for each of these, one at a time, offering the default in "
|
||||
"brackets if I don't have a preference:"
|
||||
)
|
||||
for s in recipe.slots:
|
||||
for s in blueprint.slots:
|
||||
bits = [f"- {s.label} ({s.name})"]
|
||||
if s.options:
|
||||
bits.append(f" — one of: {', '.join(map(str, s.options))}")
|
||||
|
|
@ -190,47 +190,47 @@ def build_recipe_seed(recipe) -> str:
|
|||
lines.append(
|
||||
"Once you have my answers, create the job by calling the cronjob tool "
|
||||
"with action='create'. Build the schedule as a cron expression from "
|
||||
f"this template: `{recipe.schedule_template}` "
|
||||
f"this template: `{blueprint.schedule_template}` "
|
||||
"(fill {minute}/{hour} from the chosen time, {dow} from the weekday "
|
||||
f"choice using {dict(WEEKDAY_PRESETS)}, {{interval_min}} from any "
|
||||
"interval). Use this exact prompt for the job (substituting my "
|
||||
f"answers into any {{slot}} placeholders): \"{recipe.prompt_template}\". "
|
||||
f"answers into any {{slot}} placeholders): \"{blueprint.prompt_template}\". "
|
||||
"Confirm the schedule and what it will do before you create it."
|
||||
)
|
||||
return "\n".join(lines)
|
||||
|
||||
|
||||
def _fmt_catalog() -> str:
|
||||
from cron.recipe_catalog import CATALOG
|
||||
from cron.blueprint_catalog import CATALOG
|
||||
|
||||
lines = ["Cron Recipes — `/cron-recipe <name>` and I'll ask you what I need:\n"]
|
||||
lines = ["Automation Blueprints — `/blueprint <name>` and I'll ask you what I need:\n"]
|
||||
for r in CATALOG:
|
||||
lines.append(f" • {r.key} — {r.title}")
|
||||
lines.append(f" {r.description}")
|
||||
lines.append(
|
||||
"\nTip: `/cron-recipe <name>` walks you through it. Power users can "
|
||||
"pass values inline, e.g. `/cron-recipe morning-brief time=08:00`."
|
||||
"\nTip: `/blueprint <name>` walks you through it. Power users can "
|
||||
"pass values inline, e.g. `/blueprint morning-brief time=08:00`."
|
||||
)
|
||||
return "\n".join(lines)
|
||||
|
||||
|
||||
def _fmt_candidates(query: str, candidates: List[Any]) -> str:
|
||||
lines = [f"'{query}' matches several recipes — which one?\n"]
|
||||
lines = [f"'{query}' matches several blueprints — which one?\n"]
|
||||
for r in candidates:
|
||||
lines.append(f" • {r.key} — {r.title}")
|
||||
lines.append("\nRun `/cron-recipe <name>` with one of the names above.")
|
||||
lines.append("\nRun `/blueprint <name>` with one of the names above.")
|
||||
return "\n".join(lines)
|
||||
|
||||
|
||||
def _fmt_no_match(query: str) -> str:
|
||||
from cron.recipe_catalog import CATALOG
|
||||
from cron.blueprint_catalog import CATALOG
|
||||
|
||||
keys = [r.key for r in CATALOG]
|
||||
close = difflib.get_close_matches((query or "").lower(), keys, n=3, cutoff=0.4)
|
||||
msg = f"No cron recipe matches '{query}'."
|
||||
msg = f"No automation blueprint matches '{query}'."
|
||||
if close:
|
||||
msg += " Did you mean: " + ", ".join(close) + "?"
|
||||
msg += " Run /cron-recipe to see the catalog."
|
||||
msg += " Run /blueprint to see the catalog."
|
||||
return msg
|
||||
|
||||
|
||||
|
|
@ -243,28 +243,28 @@ def _manage_hint(surface: str) -> str:
|
|||
return "Ask me to list, pause, or remove it any time."
|
||||
|
||||
|
||||
def handle_cron_recipe_command(
|
||||
def handle_blueprint_command(
|
||||
args: str,
|
||||
*,
|
||||
origin: Optional[Dict[str, Any]] = None,
|
||||
surface: str = "cli",
|
||||
) -> RecipeCommandResult:
|
||||
"""Dispatch a ``/cron-recipe`` invocation.
|
||||
) -> BlueprintCommandResult:
|
||||
"""Dispatch a ``/blueprint`` invocation.
|
||||
|
||||
Returns a :class:`RecipeCommandResult`. When ``agent_seed`` is set the
|
||||
Returns a :class:`BlueprintCommandResult`. When ``agent_seed`` is set the
|
||||
caller must feed it to the agent as the next user turn; otherwise the
|
||||
command is fully handled and only ``text`` is shown.
|
||||
|
||||
``args`` is everything after ``/cron-recipe``. ``origin`` lets a directly
|
||||
``args`` is everything after ``/blueprint``. ``origin`` lets a directly
|
||||
created job deliver back to the chat it was set up from. ``surface``
|
||||
(``"cli"`` | ``"gateway"``) picks the right wording for follow-up hints —
|
||||
``/cron`` only exists on the CLI.
|
||||
"""
|
||||
try:
|
||||
from cron.recipe_catalog import fill_recipe, RecipeFillError
|
||||
from cron.blueprint_catalog import fill_blueprint, BlueprintFillError
|
||||
except Exception as e: # pragma: no cover - import guard
|
||||
logger.debug("recipe catalog import failed: %s", e)
|
||||
return RecipeCommandResult("Cron Recipes are unavailable in this build.")
|
||||
logger.debug("blueprint catalog import failed: %s", e)
|
||||
return BlueprintCommandResult("Automation Blueprints are unavailable in this build.")
|
||||
|
||||
try:
|
||||
tokens = shlex.split(args or "")
|
||||
|
|
@ -273,33 +273,33 @@ def handle_cron_recipe_command(
|
|||
|
||||
# Bare -> list catalog.
|
||||
if not tokens:
|
||||
return RecipeCommandResult(_fmt_catalog())
|
||||
return BlueprintCommandResult(_fmt_catalog())
|
||||
|
||||
query = tokens[0]
|
||||
values, _leftover = _parse_kv(tokens[1:])
|
||||
|
||||
recipe, candidates = match_recipe(query)
|
||||
if recipe is None:
|
||||
blueprint, candidates = match_blueprint(query)
|
||||
if blueprint is None:
|
||||
if candidates:
|
||||
return RecipeCommandResult(_fmt_candidates(query, candidates))
|
||||
return RecipeCommandResult(_fmt_no_match(query))
|
||||
return BlueprintCommandResult(_fmt_candidates(query, candidates))
|
||||
return BlueprintCommandResult(_fmt_no_match(query))
|
||||
|
||||
# `<name>` with no inline slot values -> seed the agent to ask for them.
|
||||
if not values:
|
||||
seed = build_recipe_seed(recipe)
|
||||
seed = build_blueprint_seed(blueprint)
|
||||
text = (
|
||||
f"Setting up '{recipe.title}' ({_humanize_schedule(recipe)}). "
|
||||
f"Setting up '{blueprint.title}' ({_humanize_schedule(blueprint)}). "
|
||||
"I'll ask you a couple of things…"
|
||||
)
|
||||
return RecipeCommandResult(text, agent_seed=seed)
|
||||
return BlueprintCommandResult(text, agent_seed=seed)
|
||||
|
||||
# `<name> slot=val …` -> fill + create directly (deterministic shortcut).
|
||||
try:
|
||||
spec = fill_recipe(recipe, values, origin=_resolve_origin(origin))
|
||||
except RecipeFillError as e:
|
||||
return RecipeCommandResult(
|
||||
f"Can't set up '{recipe.title}': {e}\n"
|
||||
f"Or just run /cron-recipe {recipe.key} and I'll ask you for the values."
|
||||
spec = fill_blueprint(blueprint, values, origin=_resolve_origin(origin))
|
||||
except BlueprintFillError as e:
|
||||
return BlueprintCommandResult(
|
||||
f"Can't set up '{blueprint.title}': {e}\n"
|
||||
f"Or just run /blueprint {blueprint.key} and I'll ask you for the values."
|
||||
)
|
||||
|
||||
try:
|
||||
|
|
@ -307,12 +307,12 @@ def handle_cron_recipe_command(
|
|||
|
||||
job = create_job(**spec)
|
||||
except Exception as e:
|
||||
logger.debug("cron-recipe create_job failed: %s", e)
|
||||
return RecipeCommandResult(f"Failed to create the job: {e}")
|
||||
logger.debug("blueprint create_job failed: %s", e)
|
||||
return BlueprintCommandResult(f"Failed to create the job: {e}")
|
||||
|
||||
sched = job.get("schedule_display") or spec.get("schedule", "")
|
||||
return RecipeCommandResult(
|
||||
f"Scheduled '{recipe.title}'"
|
||||
return BlueprintCommandResult(
|
||||
f"Scheduled '{blueprint.title}'"
|
||||
+ (f" ({sched})" if sched else "")
|
||||
+ f", delivering to {spec.get('deliver', 'origin')}. {_manage_hint(surface)}"
|
||||
)
|
||||
|
|
@ -1276,13 +1276,13 @@ class CLICommandsMixin:
|
|||
output = f"Suggestions command failed: {e}"
|
||||
self._console_print(output)
|
||||
|
||||
def _handle_cron_recipe_command(self, cmd: str):
|
||||
"""Handle /cron-recipe — set up an automation from a recipe template.
|
||||
def _handle_blueprint_command(self, cmd: str):
|
||||
"""Handle /blueprint — set up an automation from a blueprint template.
|
||||
|
||||
Delegates to the shared handler. A bare ``/cron-recipe`` lists the
|
||||
catalog; ``/cron-recipe <name>`` name-matches a recipe and seeds the
|
||||
Delegates to the shared handler. A bare ``/blueprint`` lists the
|
||||
catalog; ``/blueprint <name>`` name-matches a blueprint and seeds the
|
||||
agent to ask the user for each value conversationally (the result's
|
||||
``agent_seed``); ``/cron-recipe <name> slot=val …`` creates the job
|
||||
``agent_seed``); ``/blueprint <name> slot=val …`` creates the job
|
||||
directly. When a seed is returned it is stashed as a one-shot pending
|
||||
message the interactive loop runs as the next agent turn.
|
||||
"""
|
||||
|
|
@ -1294,10 +1294,10 @@ class CLICommandsMixin:
|
|||
tokens = (cmd or "").split()[1:]
|
||||
args = " ".join(shlex.quote(t) for t in tokens)
|
||||
try:
|
||||
from hermes_cli.cron_recipe_cmd import handle_cron_recipe_command
|
||||
result = handle_cron_recipe_command(args)
|
||||
from hermes_cli.blueprint_cmd import handle_blueprint_command
|
||||
result = handle_blueprint_command(args)
|
||||
except Exception as e:
|
||||
self._console_print(f"Cron recipe command failed: {e}")
|
||||
self._console_print(f"Cron blueprint command failed: {e}")
|
||||
return
|
||||
self._console_print(result.text)
|
||||
seed = getattr(result, "agent_seed", None)
|
||||
|
|
|
|||
|
|
@ -182,8 +182,8 @@ COMMAND_REGISTRY: list[CommandDef] = [
|
|||
CommandDef("suggestions", "Review suggested automations (accept/dismiss)",
|
||||
"Tools & Skills", aliases=("suggest",), args_hint="[accept|dismiss N | catalog]",
|
||||
subcommands=("accept", "dismiss", "catalog", "clear")),
|
||||
CommandDef("cron-recipe", "Set up an automation from a recipe template",
|
||||
"Tools & Skills", aliases=("recipe",), args_hint="[name] [slot=value ...]"),
|
||||
CommandDef("blueprint", "Set up an automation from a blueprint template",
|
||||
"Tools & Skills", aliases=("bp",), args_hint="[name] [slot=value ...]"),
|
||||
CommandDef("curator", "Background skill maintenance (status, run, pin, archive, list-archived)",
|
||||
"Tools & Skills", args_hint="[subcommand]",
|
||||
subcommands=("status", "run", "pause", "resume", "pin", "unpin", "restore", "list-archived")),
|
||||
|
|
|
|||
|
|
@ -691,24 +691,24 @@ def do_install(identifier: str, category: str = "", force: bool = False,
|
|||
c.print(f"[bold green]Installed:[/] {install_dir.relative_to(SKILLS_DIR)}")
|
||||
c.print(f"[dim]Files: {', '.join(bundle.files.keys())}[/]\n")
|
||||
|
||||
# Recipe detection: if the installed skill declares a
|
||||
# metadata.hermes.recipe block, it is a runnable automation. Register it as
|
||||
# Blueprint detection: if the installed skill declares a
|
||||
# metadata.hermes.blueprint block, it is a runnable automation. Register it as
|
||||
# a Suggested Cron Job rather than auto-scheduling — installing never
|
||||
# silently creates a recurring job; the user accepts it via /suggestions.
|
||||
# This is the single surface every automation proposal flows through.
|
||||
try:
|
||||
from tools.recipes import RecipeError, recipe_spec_for_installed, register_recipe_suggestion
|
||||
from tools.blueprints import BlueprintError, blueprint_spec_for_installed, register_blueprint_suggestion
|
||||
|
||||
try:
|
||||
spec = recipe_spec_for_installed(bundle.name)
|
||||
except RecipeError as _rec_err:
|
||||
c.print(f"[yellow]Recipe block present but invalid:[/] {_rec_err}\n")
|
||||
spec = blueprint_spec_for_installed(bundle.name)
|
||||
except BlueprintError as _rec_err:
|
||||
c.print(f"[yellow]Blueprint block present but invalid:[/] {_rec_err}\n")
|
||||
spec = None
|
||||
if spec is not None:
|
||||
registered = register_recipe_suggestion(spec)
|
||||
registered = register_blueprint_suggestion(spec)
|
||||
if registered is not None:
|
||||
c.print(
|
||||
f"[bold cyan]Recipe:[/] '{bundle.name}' is an automation "
|
||||
f"[bold cyan]Blueprint:[/] '{bundle.name}' is an automation "
|
||||
f"(schedule [bold]{spec.schedule}[/])."
|
||||
)
|
||||
c.print(
|
||||
|
|
@ -720,7 +720,7 @@ def do_install(identifier: str, category: str = "", force: bool = False,
|
|||
# list is at its cap. Say so instead of silently doing nothing —
|
||||
# the user can still schedule it by hand.
|
||||
c.print(
|
||||
f"[bold cyan]Recipe:[/] '{bundle.name}' is an automation "
|
||||
f"[bold cyan]Blueprint:[/] '{bundle.name}' is an automation "
|
||||
f"(schedule [bold]{spec.schedule}[/]), but it wasn't added to "
|
||||
"your suggestions (already offered/dismissed, or the pending "
|
||||
"list is full — run [bold]/suggestions[/] to review)."
|
||||
|
|
@ -729,7 +729,7 @@ def do_install(identifier: str, category: str = "", force: bool = False,
|
|||
"[dim]You can still schedule it any time by asking the agent "
|
||||
"or via[/] [bold]hermes cron add[/][dim].[/]\n"
|
||||
)
|
||||
except Exception: # pragma: no cover - recipe detection is best-effort
|
||||
except Exception: # pragma: no cover - blueprint detection is best-effort
|
||||
pass
|
||||
|
||||
if invalidate_cache:
|
||||
|
|
|
|||
|
|
@ -25,7 +25,7 @@ def _fmt_pending(pending: list) -> str:
|
|||
return (
|
||||
"No suggested automations right now.\n"
|
||||
"Try `/suggestions catalog` to see the curated starter set, or "
|
||||
"install a recipe skill to get one."
|
||||
"install a blueprint skill to get one."
|
||||
)
|
||||
lines = ["Suggested automations — `/suggestions accept N` or `dismiss N`:\n"]
|
||||
for i, s in enumerate(pending, 1):
|
||||
|
|
|
|||
|
|
@ -6779,25 +6779,25 @@ async def delete_cron_job(job_id: str, profile: Optional[str] = None):
|
|||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Cron Recipes — parameterized automation templates. The dashboard renders the
|
||||
# Automation Blueprints — parameterized automation templates. The dashboard renders the
|
||||
# slot schema as a form; submitting instantiates a real cron job via the same
|
||||
# create_job path. See cron/recipe_catalog.py for the single source of truth.
|
||||
# create_job path. See cron/blueprint_catalog.py for the single source of truth.
|
||||
# ---------------------------------------------------------------------------
|
||||
class CronRecipeInstantiate(BaseModel):
|
||||
recipe: str # recipe key, e.g. "morning-brief"
|
||||
class AutomationBlueprintInstantiate(BaseModel):
|
||||
blueprint: str # blueprint key, e.g. "morning-brief"
|
||||
values: Dict[str, Any] = {} # filled slot values from the form
|
||||
|
||||
|
||||
@app.get("/api/cron/recipes")
|
||||
async def list_cron_recipes():
|
||||
"""Return the recipe catalog as form schemas for the dashboard gallery.
|
||||
@app.get("/api/cron/blueprints")
|
||||
async def list_cron_blueprints():
|
||||
"""Return the blueprint catalog as form schemas for the dashboard gallery.
|
||||
|
||||
The ``deliver`` slot's options are rewritten from the user's actually
|
||||
configured gateway platforms (plus the universal origin/local/all), so the
|
||||
form never offers a platform that isn't connected.
|
||||
"""
|
||||
try:
|
||||
from cron.recipe_catalog import CATALOG, recipe_catalog_entry
|
||||
from cron.blueprint_catalog import CATALOG, blueprint_catalog_entry
|
||||
|
||||
deliver_options = None
|
||||
try:
|
||||
|
|
@ -6810,40 +6810,40 @@ async def list_cron_recipes():
|
|||
|
||||
entries = []
|
||||
for r in CATALOG:
|
||||
entry = recipe_catalog_entry(r)
|
||||
entry = blueprint_catalog_entry(r)
|
||||
if deliver_options:
|
||||
for f in entry.get("fields", []):
|
||||
if f.get("name") == "deliver":
|
||||
f["options"] = deliver_options
|
||||
entries.append(entry)
|
||||
return {"recipes": entries}
|
||||
return {"blueprints": entries}
|
||||
except Exception as e:
|
||||
_log.exception("GET /api/cron/recipes failed")
|
||||
_log.exception("GET /api/cron/blueprints failed")
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
|
||||
@app.post("/api/cron/recipes/instantiate")
|
||||
async def instantiate_cron_recipe(body: CronRecipeInstantiate, profile: str = "default"):
|
||||
"""Fill a recipe's slots and create the cron job (form-submit path)."""
|
||||
@app.post("/api/cron/blueprints/instantiate")
|
||||
async def instantiate_blueprint(body: AutomationBlueprintInstantiate, profile: str = "default"):
|
||||
"""Fill a blueprint's slots and create the cron job (form-submit path)."""
|
||||
try:
|
||||
from cron.recipe_catalog import fill_recipe, get_recipe, RecipeFillError
|
||||
from cron.blueprint_catalog import fill_blueprint, get_blueprint, BlueprintFillError
|
||||
|
||||
recipe = get_recipe(body.recipe)
|
||||
if recipe is None:
|
||||
raise HTTPException(status_code=404, detail=f"Unknown recipe: {body.recipe}")
|
||||
blueprint = get_blueprint(body.blueprint)
|
||||
if blueprint is None:
|
||||
raise HTTPException(status_code=404, detail=f"Unknown blueprint: {body.blueprint}")
|
||||
try:
|
||||
spec = fill_recipe(recipe, body.values)
|
||||
except RecipeFillError as exc:
|
||||
spec = fill_blueprint(blueprint, body.values)
|
||||
except BlueprintFillError as exc:
|
||||
# Field-level validation error — 422 so the form can show it inline.
|
||||
raise HTTPException(status_code=422, detail=str(exc)) from exc
|
||||
# Recipe-created jobs deliver to the dashboard's configured target by
|
||||
# Blueprint-created jobs deliver to the dashboard's configured target by
|
||||
# default; the form's deliver slot overrides via spec["deliver"].
|
||||
spec.pop("origin", None)
|
||||
return _call_cron_for_profile(profile, "create_job", **spec)
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
_log.exception("POST /api/cron/recipes/instantiate failed")
|
||||
_log.exception("POST /api/cron/blueprints/instantiate failed")
|
||||
raise HTTPException(status_code=400, detail=str(e))
|
||||
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue