forked from retoor/devplacepy
fix: normalize unicode escape sequences and reformat multi-line expressions across codebase
This commit is contained in:
@@ -5,8 +5,16 @@ from __future__ import annotations
|
||||
from .spec import Action, Param
|
||||
|
||||
|
||||
def arg(name: str, description: str, required: bool = False, kind: str = "string") -> Param:
|
||||
return Param(name=name, location="body", description=description, required=required, type=kind)
|
||||
def arg(
|
||||
name: str, description: str, required: bool = False, kind: str = "string"
|
||||
) -> Param:
|
||||
return Param(
|
||||
name=name,
|
||||
location="body",
|
||||
description=description,
|
||||
required=required,
|
||||
type=kind,
|
||||
)
|
||||
|
||||
|
||||
DEVII = (
|
||||
@@ -31,7 +39,8 @@ AVATAR_ACTIONS: tuple[Action, ...] = (
|
||||
path="",
|
||||
summary="Hide devii from the screen (plays a goodbye/hide animation)",
|
||||
description=(
|
||||
DEVII + " This already plays a goodbye animation, so do not queue a separate "
|
||||
DEVII
|
||||
+ " This already plays a goodbye animation, so do not queue a separate "
|
||||
"wave/goodbye animation immediately before calling it - hiding interrupts a still-"
|
||||
"queued animation."
|
||||
),
|
||||
@@ -66,7 +75,8 @@ AVATAR_ACTIONS: tuple[Action, ...] = (
|
||||
path="",
|
||||
summary="Play a specific named animation (use avatar_list_animations first)",
|
||||
description=(
|
||||
DEVII + " A following avatar_hide or avatar_stop interrupts an animation that is "
|
||||
DEVII
|
||||
+ " A following avatar_hide or avatar_stop interrupts an animation that is "
|
||||
"still playing, so do not hide immediately after if you want it to finish."
|
||||
),
|
||||
handler="avatar",
|
||||
@@ -144,6 +154,12 @@ AVATAR_ACTIONS: tuple[Action, ...] = (
|
||||
description=DEVII,
|
||||
handler="avatar",
|
||||
requires_auth=False,
|
||||
params=(arg("name", "Character name (e.g. Clippy, Merlin, Bonzi, Genie, Rover).", required=True),),
|
||||
params=(
|
||||
arg(
|
||||
"name",
|
||||
"Character name (e.g. Clippy, Merlin, Bonzi, Genie, Rover).",
|
||||
required=True,
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
|
||||
@@ -10,7 +10,9 @@ def path(name: str, description: str, required: bool = True) -> Param:
|
||||
|
||||
|
||||
def query(name: str, description: str, required: bool = False) -> Param:
|
||||
return Param(name=name, location="query", description=description, required=required)
|
||||
return Param(
|
||||
name=name, location="query", description=description, required=required
|
||||
)
|
||||
|
||||
|
||||
def body(name: str, description: str, required: bool = False) -> Param:
|
||||
@@ -110,7 +112,10 @@ ACTIONS: tuple[Action, ...] = (
|
||||
body("project_uid", "Attach the post to a project uid."),
|
||||
body("attachment_uids", ATTACHMENTS),
|
||||
body("poll_question", "Optional poll question."),
|
||||
body("poll_options", "Poll options as a JSON array of strings, or one option per line, or comma separated. At least two are required for the poll to be created."),
|
||||
body(
|
||||
"poll_options",
|
||||
"Poll options as a JSON array of strings, or one option per line, or comma separated. At least two are required for the poll to be created.",
|
||||
),
|
||||
),
|
||||
),
|
||||
Action(
|
||||
@@ -118,7 +123,12 @@ ACTIONS: tuple[Action, ...] = (
|
||||
method="GET",
|
||||
path="/posts/{post_slug}",
|
||||
summary="View a single post by slug",
|
||||
params=(path("post_slug", "Exact post slug copied from a /posts/... link in a feed or listing response; do not build it from the title."),),
|
||||
params=(
|
||||
path(
|
||||
"post_slug",
|
||||
"Exact post slug copied from a /posts/... link in a feed or listing response; do not build it from the title.",
|
||||
),
|
||||
),
|
||||
),
|
||||
Action(
|
||||
name="edit_post",
|
||||
@@ -126,12 +136,21 @@ ACTIONS: tuple[Action, ...] = (
|
||||
path="/posts/edit/{post_slug}",
|
||||
summary="Edit an existing post",
|
||||
params=(
|
||||
path("post_slug", "Exact post slug copied from a /posts/... link in a feed or listing response; do not build it from the title."),
|
||||
path(
|
||||
"post_slug",
|
||||
"Exact post slug copied from a /posts/... link in a feed or listing response; do not build it from the title.",
|
||||
),
|
||||
body("content", "Updated post body.", required=True),
|
||||
body("title", "Updated title."),
|
||||
body("topic", "Updated topic."),
|
||||
body("poll_question", "Optional poll question. Adds a poll to a post that does not already have one."),
|
||||
body("poll_options", "Poll options as a JSON array of strings, or one option per line, or comma separated. At least two are required for the poll to be created."),
|
||||
body(
|
||||
"poll_question",
|
||||
"Optional poll question. Adds a poll to a post that does not already have one.",
|
||||
),
|
||||
body(
|
||||
"poll_options",
|
||||
"Poll options as a JSON array of strings, or one option per line, or comma separated. At least two are required for the poll to be created.",
|
||||
),
|
||||
),
|
||||
),
|
||||
Action(
|
||||
@@ -139,7 +158,12 @@ ACTIONS: tuple[Action, ...] = (
|
||||
method="POST",
|
||||
path="/posts/delete/{post_slug}",
|
||||
summary="Delete a post",
|
||||
params=(path("post_slug", "Exact post slug copied from a /posts/... link in a feed or listing response; do not build it from the title."),),
|
||||
params=(
|
||||
path(
|
||||
"post_slug",
|
||||
"Exact post slug copied from a /posts/... link in a feed or listing response; do not build it from the title.",
|
||||
),
|
||||
),
|
||||
),
|
||||
Action(
|
||||
name="create_comment",
|
||||
@@ -180,7 +204,12 @@ ACTIONS: tuple[Action, ...] = (
|
||||
method="GET",
|
||||
path="/projects/{project_slug}",
|
||||
summary="View a project by slug",
|
||||
params=(path("project_slug", "Exact project slug copied from a /projects/... link in a listing response; do not build it from the title."),),
|
||||
params=(
|
||||
path(
|
||||
"project_slug",
|
||||
"Exact project slug copied from a /projects/... link in a listing response; do not build it from the title.",
|
||||
),
|
||||
),
|
||||
),
|
||||
Action(
|
||||
name="create_project",
|
||||
@@ -203,7 +232,12 @@ ACTIONS: tuple[Action, ...] = (
|
||||
method="POST",
|
||||
path="/projects/delete/{project_slug}",
|
||||
summary="Delete a project",
|
||||
params=(path("project_slug", "Exact project slug copied from a /projects/... link in a listing response; do not build it from the title."),),
|
||||
params=(
|
||||
path(
|
||||
"project_slug",
|
||||
"Exact project slug copied from a /projects/... link in a listing response; do not build it from the title.",
|
||||
),
|
||||
),
|
||||
),
|
||||
Action(
|
||||
name="project_set_private",
|
||||
@@ -218,8 +252,16 @@ ACTIONS: tuple[Action, ...] = (
|
||||
),
|
||||
params=(
|
||||
path("project_slug", "Project slug or uid."),
|
||||
body("value", "true to make the project private, false to make it public.", required=True),
|
||||
body("confirm", "Must be true, set only after the user has explicitly confirmed.", required=True),
|
||||
body(
|
||||
"value",
|
||||
"true to make the project private, false to make it public.",
|
||||
required=True,
|
||||
),
|
||||
body(
|
||||
"confirm",
|
||||
"Must be true, set only after the user has explicitly confirmed.",
|
||||
required=True,
|
||||
),
|
||||
),
|
||||
),
|
||||
Action(
|
||||
@@ -236,8 +278,16 @@ ACTIONS: tuple[Action, ...] = (
|
||||
),
|
||||
params=(
|
||||
path("project_slug", "Project slug or uid."),
|
||||
body("value", "true to make the project read-only, false to make it writable.", required=True),
|
||||
body("confirm", "Must be true, set only after the user has explicitly confirmed.", required=True),
|
||||
body(
|
||||
"value",
|
||||
"true to make the project read-only, false to make it writable.",
|
||||
required=True,
|
||||
),
|
||||
body(
|
||||
"confirm",
|
||||
"Must be true, set only after the user has explicitly confirmed.",
|
||||
required=True,
|
||||
),
|
||||
),
|
||||
),
|
||||
Action(
|
||||
@@ -257,7 +307,11 @@ ACTIONS: tuple[Action, ...] = (
|
||||
description="Returns the file metadata plus the text content. Binary files return a url instead of content.",
|
||||
params=(
|
||||
path("project_slug", "Project slug or uid."),
|
||||
query("path", "Relative file path inside the project, e.g. src/main.py.", required=True),
|
||||
query(
|
||||
"path",
|
||||
"Relative file path inside the project, e.g. src/main.py.",
|
||||
required=True,
|
||||
),
|
||||
),
|
||||
requires_auth=False,
|
||||
),
|
||||
@@ -328,7 +382,11 @@ ACTIONS: tuple[Action, ...] = (
|
||||
params=(
|
||||
path("project_slug", "Project slug or uid."),
|
||||
body("path", "Relative file path.", required=True),
|
||||
body("at", "Insert before this 1-indexed line (1 prepends, total+1 appends).", required=True),
|
||||
body(
|
||||
"at",
|
||||
"Insert before this 1-indexed line (1 prepends, total+1 appends).",
|
||||
required=True,
|
||||
),
|
||||
body("content", "Text to insert.", required=True),
|
||||
),
|
||||
),
|
||||
@@ -374,7 +432,9 @@ ACTIONS: tuple[Action, ...] = (
|
||||
summary="Create a directory (and parents) in a project filesystem",
|
||||
params=(
|
||||
path("project_slug", "Project slug or uid."),
|
||||
body("path", "Relative directory path, e.g. src/components.", required=True),
|
||||
body(
|
||||
"path", "Relative directory path, e.g. src/components.", required=True
|
||||
),
|
||||
),
|
||||
),
|
||||
Action(
|
||||
@@ -558,8 +618,15 @@ ACTIONS: tuple[Action, ...] = (
|
||||
ajax=True,
|
||||
params=(
|
||||
path("target_type", TARGET_TYPE),
|
||||
path("target_uid", "Uid of the target, copied from a listing response; do not invent it."),
|
||||
body("value", "Vote value: 1 to upvote, -1 to downvote (re-send to remove).", required=True),
|
||||
path(
|
||||
"target_uid",
|
||||
"Uid of the target, copied from a listing response; do not invent it.",
|
||||
),
|
||||
body(
|
||||
"value",
|
||||
"Vote value: 1 to upvote, -1 to downvote (re-send to remove).",
|
||||
required=True,
|
||||
),
|
||||
),
|
||||
),
|
||||
Action(
|
||||
@@ -571,7 +638,10 @@ ACTIONS: tuple[Action, ...] = (
|
||||
ajax=True,
|
||||
params=(
|
||||
path("target_type", TARGET_TYPE),
|
||||
path("target_uid", "Uid of the target, copied from a listing response; do not invent it."),
|
||||
path(
|
||||
"target_uid",
|
||||
"Uid of the target, copied from a listing response; do not invent it.",
|
||||
),
|
||||
body("emoji", "One of the allowed reaction emoji.", required=True),
|
||||
),
|
||||
),
|
||||
@@ -591,7 +661,10 @@ ACTIONS: tuple[Action, ...] = (
|
||||
ajax=True,
|
||||
params=(
|
||||
path("target_type", TARGET_TYPE),
|
||||
path("target_uid", "Uid of the target, copied from a listing response; do not invent it."),
|
||||
path(
|
||||
"target_uid",
|
||||
"Uid of the target, copied from a listing response; do not invent it.",
|
||||
),
|
||||
),
|
||||
),
|
||||
Action(
|
||||
@@ -681,7 +754,12 @@ ACTIONS: tuple[Action, ...] = (
|
||||
method="GET",
|
||||
path="/gists/{gist_slug}",
|
||||
summary="View a gist by slug",
|
||||
params=(path("gist_slug", "Exact gist slug copied from a /gists/... link in a listing response; do not build it from the title."),),
|
||||
params=(
|
||||
path(
|
||||
"gist_slug",
|
||||
"Exact gist slug copied from a /gists/... link in a listing response; do not build it from the title.",
|
||||
),
|
||||
),
|
||||
),
|
||||
Action(
|
||||
name="create_gist",
|
||||
@@ -702,7 +780,10 @@ ACTIONS: tuple[Action, ...] = (
|
||||
path="/gists/edit/{gist_slug}",
|
||||
summary="Edit a gist",
|
||||
params=(
|
||||
path("gist_slug", "Exact gist slug copied from a /gists/... link in a listing response; do not build it from the title."),
|
||||
path(
|
||||
"gist_slug",
|
||||
"Exact gist slug copied from a /gists/... link in a listing response; do not build it from the title.",
|
||||
),
|
||||
body("title", "Gist title.", required=True),
|
||||
body("source_code", "Gist source code.", required=True),
|
||||
body("description", "Gist description."),
|
||||
@@ -714,7 +795,12 @@ ACTIONS: tuple[Action, ...] = (
|
||||
method="POST",
|
||||
path="/gists/delete/{gist_slug}",
|
||||
summary="Delete a gist",
|
||||
params=(path("gist_slug", "Exact gist slug copied from a /gists/... link in a listing response; do not build it from the title."),),
|
||||
params=(
|
||||
path(
|
||||
"gist_slug",
|
||||
"Exact gist slug copied from a /gists/... link in a listing response; do not build it from the title.",
|
||||
),
|
||||
),
|
||||
),
|
||||
Action(
|
||||
name="list_news",
|
||||
@@ -728,7 +814,12 @@ ACTIONS: tuple[Action, ...] = (
|
||||
method="GET",
|
||||
path="/news/{news_slug}",
|
||||
summary="View a news article by slug",
|
||||
params=(path("news_slug", "Exact news slug copied from a /news/... link in a listing response; do not build it from the title."),),
|
||||
params=(
|
||||
path(
|
||||
"news_slug",
|
||||
"Exact news slug copied from a /news/... link in a listing response; do not build it from the title.",
|
||||
),
|
||||
),
|
||||
),
|
||||
Action(
|
||||
name="upload_file",
|
||||
|
||||
@@ -5,8 +5,16 @@ from __future__ import annotations
|
||||
from .spec import Action, Param
|
||||
|
||||
|
||||
def arg(name: str, description: str, required: bool = False, kind: str = "string") -> Param:
|
||||
return Param(name=name, location="body", description=description, required=required, type=kind)
|
||||
def arg(
|
||||
name: str, description: str, required: bool = False, kind: str = "string"
|
||||
) -> Param:
|
||||
return Param(
|
||||
name=name,
|
||||
location="body",
|
||||
description=description,
|
||||
required=required,
|
||||
type=kind,
|
||||
)
|
||||
|
||||
|
||||
CLIENT = (
|
||||
@@ -20,7 +28,8 @@ CLIENT_ACTIONS: tuple[Action, ...] = (
|
||||
method="LOCAL",
|
||||
path="",
|
||||
summary="Read the user's current page: URL, title, viewport, scroll, selected text, visible headings, and whether they are signed in",
|
||||
description=CLIENT + " Use this to understand where the user is and what they are looking at before acting or guiding them.",
|
||||
description=CLIENT
|
||||
+ " Use this to understand where the user is and what they are looking at before acting or guiding them.",
|
||||
handler="client",
|
||||
requires_auth=False,
|
||||
read_only=True,
|
||||
@@ -31,7 +40,8 @@ CLIENT_ACTIONS: tuple[Action, ...] = (
|
||||
path="",
|
||||
summary="Execute JavaScript in the user's browser and return its result",
|
||||
description=(
|
||||
CLIENT + " The code is the body of an async function; use 'return value' to return a "
|
||||
CLIENT
|
||||
+ " The code is the body of an async function; use 'return value' to return a "
|
||||
"JSON-serializable result. You have full access to window and document. Use this for "
|
||||
"anything not covered by the dedicated tools: read or change the DOM, drive a live "
|
||||
"demo, inspect state, or update the screen. Prefer the dedicated tools "
|
||||
@@ -40,7 +50,11 @@ CLIENT_ACTIONS: tuple[Action, ...] = (
|
||||
handler="client",
|
||||
requires_auth=False,
|
||||
params=(
|
||||
arg("code", "JavaScript to run as an async function body. Return a JSON-serializable value.", required=True),
|
||||
arg(
|
||||
"code",
|
||||
"JavaScript to run as an async function body. Return a JSON-serializable value.",
|
||||
required=True,
|
||||
),
|
||||
),
|
||||
),
|
||||
Action(
|
||||
@@ -48,11 +62,16 @@ CLIENT_ACTIONS: tuple[Action, ...] = (
|
||||
method="LOCAL",
|
||||
path="",
|
||||
summary="Highlight an element on screen with an outline and an optional callout label, for live tutorials",
|
||||
description=CLIENT + " Scrolls the element into view and draws an attention outline. Call clear_highlights to remove it.",
|
||||
description=CLIENT
|
||||
+ " Scrolls the element into view and draws an attention outline. Call clear_highlights to remove it.",
|
||||
handler="client",
|
||||
requires_auth=False,
|
||||
params=(
|
||||
arg("selector", "CSS selector, or the element's exact visible text (e.g. a heading or link label).", required=True),
|
||||
arg(
|
||||
"selector",
|
||||
"CSS selector, or the element's exact visible text (e.g. a heading or link label).",
|
||||
required=True,
|
||||
),
|
||||
arg("label", "Optional callout text shown next to the element."),
|
||||
),
|
||||
),
|
||||
@@ -75,7 +94,11 @@ CLIENT_ACTIONS: tuple[Action, ...] = (
|
||||
requires_auth=False,
|
||||
params=(
|
||||
arg("text", "Message to display.", required=True),
|
||||
arg("duration_ms", "How long to show it, in milliseconds (default 4000).", kind="integer"),
|
||||
arg(
|
||||
"duration_ms",
|
||||
"How long to show it, in milliseconds (default 4000).",
|
||||
kind="integer",
|
||||
),
|
||||
),
|
||||
),
|
||||
Action(
|
||||
@@ -86,7 +109,13 @@ CLIENT_ACTIONS: tuple[Action, ...] = (
|
||||
description=CLIENT,
|
||||
handler="client",
|
||||
requires_auth=False,
|
||||
params=(arg("selector", "CSS selector, or the element's exact visible text.", required=True),),
|
||||
params=(
|
||||
arg(
|
||||
"selector",
|
||||
"CSS selector, or the element's exact visible text.",
|
||||
required=True,
|
||||
),
|
||||
),
|
||||
),
|
||||
Action(
|
||||
name="navigate_to",
|
||||
@@ -94,7 +123,8 @@ CLIENT_ACTIONS: tuple[Action, ...] = (
|
||||
path="",
|
||||
summary="Send the user's browser to a URL",
|
||||
description=(
|
||||
CLIENT + " Use a same-origin path like /feed or /docs/index.html, or a full URL. The "
|
||||
CLIENT
|
||||
+ " Use a same-origin path like /feed or /docs/index.html, or a full URL. The "
|
||||
"page reloads; the user's Devii session and conversation persist and reconnect automatically."
|
||||
),
|
||||
handler="client",
|
||||
@@ -106,7 +136,8 @@ CLIENT_ACTIONS: tuple[Action, ...] = (
|
||||
method="LOCAL",
|
||||
path="",
|
||||
summary="Refresh the user's current page, e.g. after something changed",
|
||||
description=CLIENT + " The Devii session and conversation persist and reconnect automatically.",
|
||||
description=CLIENT
|
||||
+ " The Devii session and conversation persist and reconnect automatically.",
|
||||
handler="client",
|
||||
requires_auth=False,
|
||||
),
|
||||
@@ -116,14 +147,19 @@ CLIENT_ACTIONS: tuple[Action, ...] = (
|
||||
path="",
|
||||
summary="Open a floating interactive terminal window attached to a running container instance",
|
||||
description=(
|
||||
CLIENT + " Opens a new xterm.js window in the user's browser connected to the container's "
|
||||
CLIENT
|
||||
+ " Opens a new xterm.js window in the user's browser connected to the container's "
|
||||
"interactive shell (admin only). Resolve the instance first with container_list_instances, "
|
||||
"then pass the project slug and the instance slug or uid."
|
||||
),
|
||||
handler="client",
|
||||
requires_admin=True,
|
||||
params=(
|
||||
arg("project_slug", "Project slug or uid that owns the container.", required=True),
|
||||
arg(
|
||||
"project_slug",
|
||||
"Project slug or uid that owns the container.",
|
||||
required=True,
|
||||
),
|
||||
arg("instance", "Container instance slug or uid.", required=True),
|
||||
arg("label", "Optional window title (defaults to the instance name)."),
|
||||
),
|
||||
|
||||
@@ -3,51 +3,93 @@
|
||||
from .spec import Action, Param
|
||||
|
||||
|
||||
def arg(name: str, description: str, required: bool = False, kind: str = "string") -> Param:
|
||||
return Param(name=name, location="body", description=description, required=required, type=kind)
|
||||
def arg(
|
||||
name: str, description: str, required: bool = False, kind: str = "string"
|
||||
) -> Param:
|
||||
return Param(
|
||||
name=name,
|
||||
location="body",
|
||||
description=description,
|
||||
required=required,
|
||||
type=kind,
|
||||
)
|
||||
|
||||
|
||||
SLUG = arg("project_slug", "Project slug or uid that owns the container resources.", required=True)
|
||||
SLUG = arg(
|
||||
"project_slug",
|
||||
"Project slug or uid that owns the container resources.",
|
||||
required=True,
|
||||
)
|
||||
|
||||
CONTAINER_ACTIONS: tuple[Action, ...] = (
|
||||
Action(
|
||||
name="container_list_instances",
|
||||
method="LOCAL", path="", handler="container", requires_admin=True, read_only=True,
|
||||
method="LOCAL",
|
||||
path="",
|
||||
handler="container",
|
||||
requires_admin=True,
|
||||
read_only=True,
|
||||
summary="List a project's container instances and their status",
|
||||
params=(SLUG,),
|
||||
),
|
||||
Action(
|
||||
name="container_create_instance",
|
||||
method="LOCAL", path="", handler="container", requires_admin=True,
|
||||
method="LOCAL",
|
||||
path="",
|
||||
handler="container",
|
||||
requires_admin=True,
|
||||
summary="Create and start a container instance (runs the shared ppy image with the project files mounted at /app)",
|
||||
params=(
|
||||
SLUG,
|
||||
arg("name", "Instance name.", required=True),
|
||||
arg("boot_command", "Optional command to run on boot, e.g. 'python app.py'."),
|
||||
arg(
|
||||
"boot_command", "Optional command to run on boot, e.g. 'python app.py'."
|
||||
),
|
||||
arg("restart_policy", "never, always, on-failure, or unless-stopped."),
|
||||
arg("env", "Optional env vars as KEY=VALUE lines."),
|
||||
arg("ports", "Port maps per line or comma separated. Use a bare container port (e.g. '8899') to auto-assign a unique host port above 20000, or 'host:container' to pin one."),
|
||||
arg(
|
||||
"ports",
|
||||
"Port maps per line or comma separated. Use a bare container port (e.g. '8899') to auto-assign a unique host port above 20000, or 'host:container' to pin one.",
|
||||
),
|
||||
arg("cpu_limit", "Optional CPU limit, e.g. 1 or 1.5."),
|
||||
arg("mem_limit", "Optional memory limit, e.g. 512m or 1g."),
|
||||
arg("autostart", "Start immediately ('true' or 'false', default true)."),
|
||||
arg("ingress_slug", "Optional public ingress slug; the service is then reachable at /p/<slug>."),
|
||||
arg("ingress_port", "Container port to publish at /p/<slug> (must be one of the mapped ports).", kind="integer"),
|
||||
arg(
|
||||
"ingress_slug",
|
||||
"Optional public ingress slug; the service is then reachable at /p/<slug>.",
|
||||
),
|
||||
arg(
|
||||
"ingress_port",
|
||||
"Container port to publish at /p/<slug> (must be one of the mapped ports).",
|
||||
kind="integer",
|
||||
),
|
||||
),
|
||||
),
|
||||
Action(
|
||||
name="container_instance_action",
|
||||
method="LOCAL", path="", handler="container", requires_admin=True,
|
||||
method="LOCAL",
|
||||
path="",
|
||||
handler="container",
|
||||
requires_admin=True,
|
||||
summary="Control an instance: start, stop, restart, pause, resume, delete, or sync",
|
||||
description="sync imports the container /app workspace back into the project files.",
|
||||
params=(
|
||||
SLUG,
|
||||
arg("instance", "Instance name, slug, or uid.", required=True),
|
||||
arg("action", "start, stop, restart, pause, resume, delete, or sync.", required=True),
|
||||
arg(
|
||||
"action",
|
||||
"start, stop, restart, pause, resume, delete, or sync.",
|
||||
required=True,
|
||||
),
|
||||
),
|
||||
),
|
||||
Action(
|
||||
name="container_logs",
|
||||
method="LOCAL", path="", handler="container", requires_admin=True, read_only=True,
|
||||
method="LOCAL",
|
||||
path="",
|
||||
handler="container",
|
||||
requires_admin=True,
|
||||
read_only=True,
|
||||
summary="Read the recent logs of a running instance",
|
||||
params=(
|
||||
SLUG,
|
||||
@@ -57,7 +99,10 @@ CONTAINER_ACTIONS: tuple[Action, ...] = (
|
||||
),
|
||||
Action(
|
||||
name="container_exec",
|
||||
method="LOCAL", path="", handler="container", requires_admin=True,
|
||||
method="LOCAL",
|
||||
path="",
|
||||
handler="container",
|
||||
requires_admin=True,
|
||||
summary="Run a one-shot command inside a running instance and return its output",
|
||||
params=(
|
||||
SLUG,
|
||||
@@ -67,13 +112,20 @@ CONTAINER_ACTIONS: tuple[Action, ...] = (
|
||||
),
|
||||
Action(
|
||||
name="container_stats",
|
||||
method="LOCAL", path="", handler="container", requires_admin=True, read_only=True,
|
||||
method="LOCAL",
|
||||
path="",
|
||||
handler="container",
|
||||
requires_admin=True,
|
||||
read_only=True,
|
||||
summary="Get aggregated resource and runtime statistics for an instance",
|
||||
params=(SLUG, arg("instance", "Instance name, slug, or uid.", required=True)),
|
||||
),
|
||||
Action(
|
||||
name="container_schedule",
|
||||
method="LOCAL", path="", handler="container", requires_admin=True,
|
||||
method="LOCAL",
|
||||
path="",
|
||||
handler="container",
|
||||
requires_admin=True,
|
||||
summary="Schedule a start or stop of an instance (cron, interval, or one-time)",
|
||||
params=(
|
||||
SLUG,
|
||||
|
||||
@@ -5,8 +5,16 @@ from __future__ import annotations
|
||||
from .spec import Action, Param
|
||||
|
||||
|
||||
def arg(name: str, description: str, required: bool = False, kind: str = "string") -> Param:
|
||||
return Param(name=name, location="body", description=description, required=required, type=kind)
|
||||
def arg(
|
||||
name: str, description: str, required: bool = False, kind: str = "string"
|
||||
) -> Param:
|
||||
return Param(
|
||||
name=name,
|
||||
location="body",
|
||||
description=description,
|
||||
required=required,
|
||||
type=kind,
|
||||
)
|
||||
|
||||
|
||||
SCOPE = (
|
||||
@@ -58,7 +66,12 @@ CUSTOMIZATION_ACTIONS: tuple[Action, ...] = (
|
||||
params=(
|
||||
arg("scope", SCOPE, required=True),
|
||||
arg("css", "The full CSS source to save for this scope.", required=True),
|
||||
arg("confirm", "Must be true after the user has confirmed the scope.", required=True, kind="boolean"),
|
||||
arg(
|
||||
"confirm",
|
||||
"Must be true after the user has confirmed the scope.",
|
||||
required=True,
|
||||
kind="boolean",
|
||||
),
|
||||
),
|
||||
),
|
||||
Action(
|
||||
@@ -76,8 +89,17 @@ CUSTOMIZATION_ACTIONS: tuple[Action, ...] = (
|
||||
requires_auth=False,
|
||||
params=(
|
||||
arg("scope", SCOPE, required=True),
|
||||
arg("js", "The full JavaScript source to save for this scope.", required=True),
|
||||
arg("confirm", "Must be true after the user has confirmed the scope.", required=True, kind="boolean"),
|
||||
arg(
|
||||
"js",
|
||||
"The full JavaScript source to save for this scope.",
|
||||
required=True,
|
||||
),
|
||||
arg(
|
||||
"confirm",
|
||||
"Must be true after the user has confirmed the scope.",
|
||||
required=True,
|
||||
kind="boolean",
|
||||
),
|
||||
),
|
||||
),
|
||||
Action(
|
||||
@@ -92,9 +114,21 @@ CUSTOMIZATION_ACTIONS: tuple[Action, ...] = (
|
||||
handler="customization",
|
||||
requires_auth=False,
|
||||
params=(
|
||||
arg("scope", SCOPE + " Use 'all' to remove every customization.", required=True),
|
||||
arg("lang", "Optional: limit the reset to 'css' or 'js'. Omit to remove both."),
|
||||
arg("confirm", "Must be true after the user has confirmed the deletion.", required=True, kind="boolean"),
|
||||
arg(
|
||||
"scope",
|
||||
SCOPE + " Use 'all' to remove every customization.",
|
||||
required=True,
|
||||
),
|
||||
arg(
|
||||
"lang",
|
||||
"Optional: limit the reset to 'css' or 'js'. Omit to remove both.",
|
||||
),
|
||||
arg(
|
||||
"confirm",
|
||||
"Must be true after the user has confirmed the deletion.",
|
||||
required=True,
|
||||
kind="boolean",
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
|
||||
@@ -30,13 +30,24 @@ from .spec import Action, Catalog
|
||||
|
||||
MUTATING_METHODS = ("POST", "DELETE", "PUT", "PATCH")
|
||||
|
||||
CONFIRM_REQUIRED = {"project_set_readonly", "project_set_private", "customize_set_css", "customize_set_js", "customize_reset"}
|
||||
CONFIRM_REQUIRED = {
|
||||
"project_set_readonly",
|
||||
"project_set_private",
|
||||
"customize_set_css",
|
||||
"customize_set_js",
|
||||
"customize_reset",
|
||||
}
|
||||
|
||||
logger = logging.getLogger("devii.dispatch")
|
||||
|
||||
|
||||
def _is_confirmed(arguments: dict[str, Any]) -> bool:
|
||||
return str(arguments.get("confirm", "")).strip().lower() in ("true", "1", "yes", "on")
|
||||
return str(arguments.get("confirm", "")).strip().lower() in (
|
||||
"true",
|
||||
"1",
|
||||
"yes",
|
||||
"on",
|
||||
)
|
||||
|
||||
|
||||
def confirmation_error(name: str, arguments: dict[str, Any]) -> ToolInputError | None:
|
||||
@@ -55,7 +66,12 @@ def confirmation_error(name: str, arguments: dict[str, Any]) -> ToolInputError |
|
||||
"confirm=true."
|
||||
)
|
||||
if name == "project_set_private":
|
||||
making_private = str(arguments.get("value", "")).strip().lower() in ("true", "1", "yes", "on")
|
||||
making_private = str(arguments.get("value", "")).strip().lower() in (
|
||||
"true",
|
||||
"1",
|
||||
"yes",
|
||||
"on",
|
||||
)
|
||||
change = (
|
||||
"hide the project from everyone except its owner and administrators"
|
||||
if making_private
|
||||
@@ -102,15 +118,21 @@ class Dispatcher:
|
||||
self._chunks = ChunkController(settings)
|
||||
self._rsearch = RsearchController(settings)
|
||||
from ..container import ContainerController
|
||||
|
||||
self._container = ContainerController(client)
|
||||
from ..customization import CustomizationController
|
||||
|
||||
self._customization = CustomizationController(owner_kind, owner_id)
|
||||
self._virtual_tools = virtual_tools
|
||||
self._read_files: set[tuple[str, str]] = set()
|
||||
|
||||
@staticmethod
|
||||
def _file_key(arguments: dict[str, Any]) -> tuple[str, str] | None:
|
||||
from devplacepy.project_files import normalize_path, ProjectFileError as _PFError
|
||||
from devplacepy.project_files import (
|
||||
normalize_path,
|
||||
ProjectFileError as _PFError,
|
||||
)
|
||||
|
||||
raw = arguments.get("path")
|
||||
if not raw:
|
||||
return None
|
||||
@@ -162,7 +184,9 @@ class Dispatcher:
|
||||
result = await self._run(action, arguments)
|
||||
if action.handler == "chunks":
|
||||
return result
|
||||
return wrap_if_large(result, self._settings.max_response_chars, resource_key)
|
||||
return wrap_if_large(
|
||||
result, self._settings.max_response_chars, resource_key
|
||||
)
|
||||
except DeviiError as exc:
|
||||
logger.info("Dispatch %s failed: %s", name, exc.message)
|
||||
return error_result(exc)
|
||||
@@ -184,7 +208,8 @@ class Dispatcher:
|
||||
result = await self._client.login(
|
||||
email=self._require(arguments, "email"),
|
||||
password=self._require(arguments, "password"),
|
||||
remember_me=str(arguments.get("remember_me", "on")).lower() not in ("", "false", "off", "no"),
|
||||
remember_me=str(arguments.get("remember_me", "on")).lower()
|
||||
not in ("", "false", "off", "no"),
|
||||
)
|
||||
return json.dumps(result, ensure_ascii=False)
|
||||
|
||||
@@ -221,7 +246,9 @@ class Dispatcher:
|
||||
if action.handler == "virtual_tool":
|
||||
if self._virtual_tools is None:
|
||||
return error_result(
|
||||
ToolInputError("User-defined tools are not available in this context.")
|
||||
ToolInputError(
|
||||
"User-defined tools are not available in this context."
|
||||
)
|
||||
)
|
||||
return await self._virtual_tools.dispatch(action.name, arguments)
|
||||
|
||||
@@ -257,11 +284,14 @@ class Dispatcher:
|
||||
if param.name not in arguments or arguments[param.name] is None:
|
||||
if param.required:
|
||||
raise ToolInputError(
|
||||
f"Missing required parameter '{param.name}' for {action.name}.")
|
||||
f"Missing required parameter '{param.name}' for {action.name}."
|
||||
)
|
||||
continue
|
||||
value = arguments[param.name]
|
||||
if param.location == "path":
|
||||
url_path = url_path.replace("{" + param.name + "}", quote(str(value), safe=""))
|
||||
url_path = url_path.replace(
|
||||
"{" + param.name + "}", quote(str(value), safe="")
|
||||
)
|
||||
elif param.location == "query":
|
||||
params[param.name] = value
|
||||
elif param.location == "body":
|
||||
@@ -309,7 +339,11 @@ class Dispatcher:
|
||||
async def _run_http(self, action: Action, arguments: dict[str, Any]) -> str:
|
||||
if action.name == "project_write_file":
|
||||
key = self._file_key(arguments)
|
||||
if key is not None and key not in self._read_files and await self._file_exists(arguments):
|
||||
if (
|
||||
key is not None
|
||||
and key not in self._read_files
|
||||
and await self._file_exists(arguments)
|
||||
):
|
||||
raise ToolInputError(
|
||||
f"Read '{key[1]}' before overwriting it. It already exists; call "
|
||||
"project_read_file first. For an existing file prefer the line tools "
|
||||
@@ -328,7 +362,11 @@ class Dispatcher:
|
||||
file_field=file_field,
|
||||
headers=headers,
|
||||
)
|
||||
if action.name in ("project_read_file", "project_read_lines", "project_write_file"):
|
||||
if action.name in (
|
||||
"project_read_file",
|
||||
"project_read_lines",
|
||||
"project_write_file",
|
||||
):
|
||||
key = self._file_key(arguments)
|
||||
if key is not None:
|
||||
self._read_files.add(key)
|
||||
|
||||
@@ -25,11 +25,35 @@ RSEARCH_ACTIONS: tuple[Action, ...] = (
|
||||
requires_auth=False,
|
||||
read_only=True,
|
||||
params=(
|
||||
Param(name="query", location="body", description="The web search query.", required=True),
|
||||
Param(name="count", location="body", description="Number of results (1-100, default 10).", type="integer"),
|
||||
Param(name="content", location="body", description="Fetch full page content for each result.", type="boolean"),
|
||||
Param(name="deep", location="body", description="Run a deeper research pass.", type="boolean"),
|
||||
Param(name="type", location="body", description="Result type: 'web' (default) or 'images'."),
|
||||
Param(
|
||||
name="query",
|
||||
location="body",
|
||||
description="The web search query.",
|
||||
required=True,
|
||||
),
|
||||
Param(
|
||||
name="count",
|
||||
location="body",
|
||||
description="Number of results (1-100, default 10).",
|
||||
type="integer",
|
||||
),
|
||||
Param(
|
||||
name="content",
|
||||
location="body",
|
||||
description="Fetch full page content for each result.",
|
||||
type="boolean",
|
||||
),
|
||||
Param(
|
||||
name="deep",
|
||||
location="body",
|
||||
description="Run a deeper research pass.",
|
||||
type="boolean",
|
||||
),
|
||||
Param(
|
||||
name="type",
|
||||
location="body",
|
||||
description="Result type: 'web' (default) or 'images'.",
|
||||
),
|
||||
),
|
||||
),
|
||||
Action(
|
||||
@@ -46,9 +70,24 @@ RSEARCH_ACTIONS: tuple[Action, ...] = (
|
||||
requires_auth=False,
|
||||
read_only=True,
|
||||
params=(
|
||||
Param(name="query", location="body", description="The question or prompt to answer.", required=True),
|
||||
Param(name="content", location="body", description="Let the AI read full page content while answering.", type="boolean"),
|
||||
Param(name="count", location="body", description="Max sources to consider (1-100, default 10).", type="integer"),
|
||||
Param(
|
||||
name="query",
|
||||
location="body",
|
||||
description="The question or prompt to answer.",
|
||||
required=True,
|
||||
),
|
||||
Param(
|
||||
name="content",
|
||||
location="body",
|
||||
description="Let the AI read full page content while answering.",
|
||||
type="boolean",
|
||||
),
|
||||
Param(
|
||||
name="count",
|
||||
location="body",
|
||||
description="Max sources to consider (1-100, default 10).",
|
||||
type="integer",
|
||||
),
|
||||
),
|
||||
),
|
||||
Action(
|
||||
@@ -65,9 +104,23 @@ RSEARCH_ACTIONS: tuple[Action, ...] = (
|
||||
requires_auth=False,
|
||||
read_only=True,
|
||||
params=(
|
||||
Param(name="prompt", location="body", description="The prompt to send.", required=True),
|
||||
Param(name="json", location="body", description="Force a valid-JSON-only response.", type="boolean"),
|
||||
Param(name="system", location="body", description="Optional system message to steer the answer."),
|
||||
Param(
|
||||
name="prompt",
|
||||
location="body",
|
||||
description="The prompt to send.",
|
||||
required=True,
|
||||
),
|
||||
Param(
|
||||
name="json",
|
||||
location="body",
|
||||
description="Force a valid-JSON-only response.",
|
||||
type="boolean",
|
||||
),
|
||||
Param(
|
||||
name="system",
|
||||
location="body",
|
||||
description="Optional system message to steer the answer.",
|
||||
),
|
||||
),
|
||||
),
|
||||
Action(
|
||||
@@ -83,7 +136,12 @@ RSEARCH_ACTIONS: tuple[Action, ...] = (
|
||||
requires_auth=False,
|
||||
read_only=True,
|
||||
params=(
|
||||
Param(name="url", location="body", description="Public URL of the image to describe.", required=True),
|
||||
Param(
|
||||
name="url",
|
||||
location="body",
|
||||
description="Public URL of the image to describe.",
|
||||
required=True,
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
|
||||
@@ -28,8 +28,20 @@ class Action:
|
||||
requires_auth: bool = True
|
||||
requires_admin: bool = False
|
||||
handler: Literal[
|
||||
"http", "login", "logout", "status", "task", "agentic", "avatar", "client", "fetch",
|
||||
"docs", "cost", "chunks", "rsearch", "container"
|
||||
"http",
|
||||
"login",
|
||||
"logout",
|
||||
"status",
|
||||
"task",
|
||||
"agentic",
|
||||
"avatar",
|
||||
"client",
|
||||
"fetch",
|
||||
"docs",
|
||||
"cost",
|
||||
"chunks",
|
||||
"rsearch",
|
||||
"container",
|
||||
] = "http"
|
||||
freeform_body: bool = False
|
||||
ajax: bool = False
|
||||
@@ -62,7 +74,11 @@ class Action:
|
||||
"description": "Additional form fields as key/value string pairs.",
|
||||
"additionalProperties": {"type": "string"},
|
||||
}
|
||||
text = self.summary if not self.description else f"{self.summary}. {self.description}"
|
||||
text = (
|
||||
self.summary
|
||||
if not self.description
|
||||
else f"{self.summary}. {self.description}"
|
||||
)
|
||||
return {
|
||||
"type": "function",
|
||||
"function": {
|
||||
@@ -88,7 +104,9 @@ class Catalog:
|
||||
def tool_schemas(self) -> list[dict[str, Any]]:
|
||||
return [action.tool_schema() for action in self.actions]
|
||||
|
||||
def tool_schemas_for(self, authenticated: bool, is_admin: bool = False) -> list[dict[str, Any]]:
|
||||
def tool_schemas_for(
|
||||
self, authenticated: bool, is_admin: bool = False
|
||||
) -> list[dict[str, Any]]:
|
||||
return [
|
||||
action.tool_schema()
|
||||
for action in self.actions
|
||||
|
||||
Reference in New Issue
Block a user