fix: normalize unicode escape sequences and reformat multi-line expressions across codebase

This commit is contained in:
2026-06-09 16:48:08 +00:00
parent 66dfda88bc
commit c4f2937415
175 changed files with 12660 additions and 4175 deletions
@@ -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,
),
),
),
)
+115 -24
View File
@@ -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",
),
),
),
)
+49 -11
View File
@@ -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,
),
),
),
)
+22 -4
View File
@@ -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