mirror of
https://github.com/Skyvern-AI/skyvern.git
synced 2026-07-09 16:09:13 +00:00
SKY-9651: Clarify diagnostic Copilot routing (#5910)
This commit is contained in:
parent
be6840e6e7
commit
b4d301ce6c
40 changed files with 1504 additions and 545 deletions
|
|
@ -51,7 +51,7 @@ npm install @skyvern/client
|
|||
```
|
||||
</CodeGroup>
|
||||
|
||||
`pip install skyvern` is enough for Skyvern Cloud and remote API usage. If you are using `Skyvern.local()` or a local self-hosted server, install `pip install "skyvern[server]"`.
|
||||
`pip install skyvern` is enough for Skyvern Cloud and remote API usage. If you are using embedded local mode with `Skyvern.local()` or `launch_local_browser()`, install `pip install "skyvern[local]"`. For a local self-hosted server, install `pip install "skyvern[server]"`.
|
||||
|
||||
All code snippets below run inside an async function. See the [complete example](#complete-example) for the full runnable script.
|
||||
</Info>
|
||||
|
|
|
|||
|
|
@ -42,7 +42,7 @@ npm install @skyvern/client
|
|||
</CodeGroup>
|
||||
|
||||
<Info>
|
||||
`pip install skyvern` is the lightweight Python SDK for Skyvern Cloud and remote API usage. If you are setting up a local Skyvern server, self-hosted instance, `Skyvern.local()`, or local stdio MCP, install the server extra instead: `pip install "skyvern[server]"`. For full local setup with migrations and the web UI, use Docker Compose or a source checkout.
|
||||
`pip install skyvern` is the lightweight Python SDK for Skyvern Cloud and remote API usage. If you are using embedded local mode with `Skyvern.local()` or `launch_local_browser()`, install the local extra: `pip install "skyvern[local]"`. For a local Skyvern server, self-hosted instance, or local stdio MCP, install the server extra instead: `pip install "skyvern[server]"`. For full local setup with migrations and the web UI, use Docker Compose or a source checkout.
|
||||
</Info>
|
||||
|
||||
<Accordion title="Troubleshooting: Python version errors">
|
||||
|
|
@ -191,7 +191,7 @@ This is invaluable for debugging and understanding how Skyvern interprets your p
|
|||
|
||||
---
|
||||
|
||||
## Run with a local browser
|
||||
## Run with a local server
|
||||
|
||||
You can run Skyvern with a browser on your own machine. This is useful for development, debugging, or automating internal tools on your local network.
|
||||
|
||||
|
|
|
|||
|
|
@ -5,6 +5,13 @@ slug: sdk-reference/browser-automation/launch-local-browser
|
|||
|
||||
Launch a local Chromium browser with CDP enabled. Only available in embedded mode (`Skyvern.local()`).
|
||||
|
||||
Install the local extra and Playwright's Chromium browser before launching:
|
||||
|
||||
```bash
|
||||
pip install "skyvern[local]"
|
||||
python -m playwright install chromium
|
||||
```
|
||||
|
||||
```python
|
||||
skyvern = Skyvern.local()
|
||||
browser = await skyvern.launch_local_browser(headless=False)
|
||||
|
|
|
|||
|
|
@ -33,7 +33,7 @@ npm install @skyvern/client
|
|||
</CodeGroup>
|
||||
|
||||
<Note>
|
||||
`pip install skyvern` is the lightweight SDK for Skyvern Cloud and remote API calls. Local server features such as `Skyvern.local()`, local browser control, and local stdio MCP require `pip install "skyvern[server]"`. Guided quickstart and migrations currently require Docker Compose or a source checkout.
|
||||
`pip install skyvern` is the lightweight SDK for Skyvern Cloud and remote API calls. Embedded local SDK features such as `Skyvern.local()` and local browser control require `pip install "skyvern[local]"`. Local server and local stdio MCP commands require `pip install "skyvern[server]"`. Guided quickstart and migrations currently require Docker Compose or a source checkout.
|
||||
</Note>
|
||||
|
||||
<CodeGroup>
|
||||
|
|
@ -80,7 +80,8 @@ Skyvern(
|
|||
timeout: float | None = None, # HTTP request timeout (seconds)
|
||||
)
|
||||
|
||||
# Local mode (Python only - requires pip install "skyvern[server]")
|
||||
# Local mode (Python only - requires pip install "skyvern[local]")
|
||||
# For local browser control, also run: python -m playwright install chromium
|
||||
client = Skyvern.local()
|
||||
```
|
||||
|
||||
|
|
@ -693,6 +694,6 @@ request_options=RequestOptions(
|
|||
- `browser_profile_id` works with `run_workflow` only - silently ignored by `run_task`.
|
||||
- Python `download_files` does not support `wait_for_completion` - poll manually or use webhooks.
|
||||
- Only workflow runs with `persist_browser_session=True` produce archives for profile creation.
|
||||
- `launch_local_browser` requires Python local mode (`Skyvern.local()`) and the `skyvern[server]` extra.
|
||||
- `launch_local_browser` requires Python local mode (`Skyvern.local()`) and the `skyvern[local]` extra.
|
||||
- `page.agent` methods always wait for completion.
|
||||
- Python-only features: `launch_local_browser`, `get_page_for`, `locator`/`AILocator`, `type`, `hover`, `scroll`, `upload_file` (page-level), form automation (`fill_form`, `fill_multipage_form`, `fill_from_mapping`, `extract_form_fields`, `validate_mapping`, `fill_autocomplete`), iframe management (`frame_switch`, `frame_main`, `frame_list`).
|
||||
|
|
|
|||
|
|
@ -32,7 +32,7 @@ npm install @skyvern/client
|
|||
</CodeGroup>
|
||||
|
||||
<Note>
|
||||
`pip install skyvern` is the lightweight SDK for Skyvern Cloud and remote API calls. Local server features such as `Skyvern.local()`, local browser control, and local stdio MCP require `pip install "skyvern[server]"`. Guided quickstart and migrations currently require Docker Compose or a source checkout.
|
||||
`pip install skyvern` is the lightweight SDK for Skyvern Cloud and remote API calls. Embedded local SDK features such as `Skyvern.local()` and local browser control require `pip install "skyvern[local]"`. Local server and local stdio MCP commands require `pip install "skyvern[server]"`. Guided quickstart and migrations currently require Docker Compose or a source checkout.
|
||||
</Note>
|
||||
|
||||
---
|
||||
|
|
@ -144,7 +144,7 @@ const skyvern = new Skyvern({
|
|||
|
||||
Run Skyvern entirely on your machine - no cloud, no network calls. `Skyvern.local()` reads your `.env` file, boots the engine in-process, and connects the client to it.
|
||||
|
||||
**Prerequisite:** Install the server extra with `pip install "skyvern[server]"` and initialize local configuration from Docker Compose or a source checkout.
|
||||
**Prerequisite:** Install the local extra with `pip install "skyvern[local]"`. For local browser control, also run `python -m playwright install chromium`.
|
||||
|
||||
```python
|
||||
from skyvern import Skyvern
|
||||
|
|
|
|||
|
|
@ -19,6 +19,70 @@ dependencies = [
|
|||
]
|
||||
|
||||
[project.optional-dependencies]
|
||||
# Embedded local mode is currently a Forge-backed compatibility bridge. It is
|
||||
# smaller than the full server install because it excludes backend process,
|
||||
# Postgres, MCP server, and cloud product packages, but it is not a minimal
|
||||
# browser-only runtime until local stops importing Forge internals.
|
||||
local = [
|
||||
"azure-storage-blob>=12.26.0",
|
||||
"openai>=1.68.2",
|
||||
"sqlalchemy>=2.0.29,<3",
|
||||
"aiohttp>=3.13.4,<4",
|
||||
"python-multipart>=0.0.27,<1",
|
||||
"toml>=0.10.2,<0.11",
|
||||
"jinja2>=3.1.2,<4",
|
||||
"litellm==1.83.14",
|
||||
"playwright>1.46.0 ; python_version >= '3.12' and python_version < '3.14'",
|
||||
"playwright==1.46.0 ; python_version >= '3.11' and python_version < '3.12'",
|
||||
"greenlet>3.0.3 ; python_version >= '3.12' and python_version < '3.14'",
|
||||
"greenlet==3.0.3 ; python_version >= '3.11' and python_version < '3.12'",
|
||||
"pillow>=10.2.0",
|
||||
"starlette-context>=0.3.6,<0.6",
|
||||
"fastapi>=0.136.1,<0.137",
|
||||
"alembic>=1.12.1,<2",
|
||||
"PyJWT[crypto]>=2.12.0,<3",
|
||||
"aioboto3>=14.3.0,<15",
|
||||
"asyncache>=0.3.1,<0.4",
|
||||
"filetype>=1.2.0,<2",
|
||||
"fuzzysearch>=0.8,<1",
|
||||
"tldextract>=5.1.2,<6",
|
||||
"websockets>=12.0,<15.1",
|
||||
"email-validator>=2.2.0,<3",
|
||||
"requests-toolbelt>=1.0.0,<2",
|
||||
"aiofiles>=24.1.0,<25",
|
||||
"pyotp>=2.9.0,<3",
|
||||
"json5>=0.13.0,<1",
|
||||
"json-repair>=0.59.5,<0.60",
|
||||
"pypdf>=6.7.5,<7",
|
||||
"pdfplumber>=0.11.0,<0.12",
|
||||
"psutil>=7.0.0",
|
||||
"tiktoken>=0.9.0",
|
||||
"anthropic>=0.97.0,<0.98",
|
||||
"google-cloud-aiplatform>=1.149.0,<2",
|
||||
"google-auth>=2.49.2,<3",
|
||||
"google-auth-oauthlib>=1.3.1,<2",
|
||||
"onepassword-sdk==0.4.0",
|
||||
"lark>=1.2.2,<2",
|
||||
"libcst>=1.8.2,<2",
|
||||
"curlparser>=0.1.0,<0.2",
|
||||
"pandas>=2.3.1,<4",
|
||||
"azure-identity>=1.24.0,<2",
|
||||
"azure-keyvault-secrets>=4.2.0,<5",
|
||||
"jsonschema>=4.23.0",
|
||||
"python-calamine>=0.6.1",
|
||||
"python-docx>=1.1.0",
|
||||
"urllib3>=2.6.3",
|
||||
"zstandard>=0.25.0",
|
||||
"sse-starlette>=3.4.1,<4",
|
||||
"opentelemetry-api>=1.41.1,<2",
|
||||
"croniter>=1.3.8,<4",
|
||||
"authlib>=1.7.1",
|
||||
"pyasn1>=0.6.3",
|
||||
"tornado>=6.5.5",
|
||||
"aiosqlite>=0.21.0,<0.23",
|
||||
]
|
||||
# Server is intentionally a superset of local. Keep shared runtime deps in sync
|
||||
# with local until the embedded runtime is separated from Forge/server internals.
|
||||
server = [
|
||||
"azure-storage-blob>=12.26.0",
|
||||
"openai>=1.68.2",
|
||||
|
|
@ -29,10 +93,10 @@ server = [
|
|||
"python-dotenv==1.2.2",
|
||||
"sqlalchemy[mypy]>=2.0.29,<3",
|
||||
"aiohttp>=3.13.4,<4",
|
||||
"python-multipart>=0.0.22,<1",
|
||||
"python-multipart>=0.0.27,<1",
|
||||
"toml>=0.10.2,<0.11",
|
||||
"jinja2>=3.1.2,<4",
|
||||
"uvicorn[standard]>=0.35.0",
|
||||
"uvicorn[standard]>=0.44.0",
|
||||
"litellm==1.83.14",
|
||||
"playwright>1.46.0 ; python_version >= '3.12' and python_version < '3.14'",
|
||||
"playwright==1.46.0 ; python_version >= '3.11' and python_version < '3.12'",
|
||||
|
|
@ -40,7 +104,7 @@ server = [
|
|||
"greenlet==3.0.3 ; python_version >= '3.11' and python_version < '3.12'",
|
||||
"pillow>=10.2.0",
|
||||
"starlette-context>=0.3.6,<0.6",
|
||||
"fastapi>=0.136.0,<0.137",
|
||||
"fastapi>=0.136.1,<0.137",
|
||||
"psycopg[binary, pool]==3.1.18 ; python_version >= '3.11' and python_version < '3.13'",
|
||||
"psycopg[binary, pool]>=3.2.2,<3.3.0 ; python_version >= '3.13' and python_version < '3.14'",
|
||||
"alembic>=1.12.1,<2",
|
||||
|
|
@ -60,14 +124,14 @@ server = [
|
|||
"pyotp>=2.9.0,<3",
|
||||
"asyncpg>=0.30.0,<0.32",
|
||||
"json5>=0.13.0,<1",
|
||||
"json-repair>=0.59.4,<0.60",
|
||||
"json-repair>=0.59.5,<0.60",
|
||||
"pypdf>=6.7.5,<7",
|
||||
"pdfplumber>=0.11.0,<0.12",
|
||||
"fastmcp>=3.2.0,<4",
|
||||
"psutil>=7.0.0",
|
||||
"tiktoken>=0.9.0",
|
||||
"anthropic>=0.96.0,<0.97",
|
||||
"google-cloud-aiplatform>=1.90.0,<2",
|
||||
"anthropic>=0.97.0,<0.98",
|
||||
"google-cloud-aiplatform>=1.149.0,<2",
|
||||
"google-auth>=2.49.2,<3",
|
||||
"google-auth-oauthlib>=1.3.1,<2",
|
||||
"onepassword-sdk==0.4.0",
|
||||
|
|
@ -83,12 +147,11 @@ server = [
|
|||
"python-docx>=1.1.0",
|
||||
"urllib3>=2.6.3",
|
||||
"zstandard>=0.25.0",
|
||||
"sse-starlette>=3.0.3,<4",
|
||||
"sse-starlette>=3.4.1,<4",
|
||||
"opentelemetry-api>=1.41.1,<2",
|
||||
"croniter>=1.3.8,<4",
|
||||
"authlib>=1.6.9",
|
||||
"authlib>=1.7.1",
|
||||
"pyasn1>=0.6.3",
|
||||
"PyJWT>=2.12.0",
|
||||
"tornado>=6.5.5",
|
||||
"aiosqlite>=0.21.0,<0.23",
|
||||
]
|
||||
|
|
@ -98,8 +161,8 @@ cloud = [
|
|||
# Keep workflow-copilot on the newer Agents SDK for cloud/source installs.
|
||||
# uv's override-dependencies below relax LiteLLM's exact transitive pins,
|
||||
# but those overrides do not ship in public wheel metadata.
|
||||
"openai-agents>=0.14.4,<0.15",
|
||||
"stripe>=9.7.0,<10",
|
||||
"openai-agents>=0.14.8,<0.15",
|
||||
"stripe>=15.0.1,<16",
|
||||
"temporalio[opentelemetry]>=1.6.0,<2",
|
||||
"temporalio>=1.6.0,<2",
|
||||
"redis>=5.0.3,<6",
|
||||
|
|
@ -137,7 +200,7 @@ dev = [
|
|||
"pydevd-pycharm>=233.6745.319,<234",
|
||||
"ipython>=8.17.2,<9",
|
||||
"ipykernel>=6.26.0,<8",
|
||||
"notebook>=7.0.6,<8",
|
||||
"notebook>=7.5.6,<8",
|
||||
"freezegun>=1.2.2,<2",
|
||||
"fpdf>=1.7.2,<2",
|
||||
"twine>=6.1.0,<7",
|
||||
|
|
@ -187,7 +250,7 @@ override-dependencies = [
|
|||
"openai>=2.24.0",
|
||||
]
|
||||
constraint-dependencies = [
|
||||
"authlib>=1.6.9",
|
||||
"authlib>=1.7.1",
|
||||
"flask>=3.1.3",
|
||||
"joserfc>=1.6.3",
|
||||
"pyasn1>=0.6.3",
|
||||
|
|
|
|||
|
|
@ -444,9 +444,7 @@ function BrowserSessions() {
|
|||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="default">
|
||||
Default (Microsoft Edge)
|
||||
</SelectItem>
|
||||
<SelectItem value="default">Default</SelectItem>
|
||||
{BROWSER_TYPE_OPTIONS.map((browserType) => (
|
||||
<SelectItem
|
||||
key={browserType.value}
|
||||
|
|
|
|||
|
|
@ -20,7 +20,6 @@ import { dataSchemaExampleValue } from "../types";
|
|||
import type { LoopNode } from "./types";
|
||||
import { useIsFirstBlockInWorkflow } from "../../hooks/useIsFirstNodeInWorkflow";
|
||||
import { Checkbox } from "@/components/ui/checkbox";
|
||||
import { Tabs, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
||||
import type { WorkflowBlockType } from "@/routes/workflows/types/workflowTypes";
|
||||
import {
|
||||
inferBranchCriteriaTypeFromExpression,
|
||||
|
|
@ -145,40 +144,49 @@ function LoopNode({ id, data }: NodeProps<LoopNode>) {
|
|||
totpUrl={null}
|
||||
type={headerBlockType}
|
||||
/>
|
||||
<Tabs
|
||||
value={loopKind}
|
||||
onValueChange={(v) => {
|
||||
if (!data.editable) {
|
||||
return;
|
||||
}
|
||||
if (v !== "for_each" && v !== "while") {
|
||||
return;
|
||||
}
|
||||
if (v === "while") {
|
||||
const expr =
|
||||
data.whileConditionExpression.trim() === ""
|
||||
? "{{ true }}"
|
||||
: data.whileConditionExpression;
|
||||
update({
|
||||
loopKind: "while",
|
||||
whileConditionExpression: expr,
|
||||
whileConditionCriteriaType:
|
||||
inferBranchCriteriaTypeFromExpression(expr),
|
||||
});
|
||||
} else {
|
||||
update({ loopKind: "for_each" });
|
||||
}
|
||||
}}
|
||||
<div
|
||||
role="tablist"
|
||||
aria-label="Loop type"
|
||||
className="grid h-9 w-full grid-cols-2 rounded-lg bg-muted p-1 text-muted-foreground"
|
||||
>
|
||||
<TabsList className="grid h-9 w-full grid-cols-2">
|
||||
<TabsTrigger value="for_each" disabled={!data.editable}>
|
||||
For each
|
||||
</TabsTrigger>
|
||||
<TabsTrigger value="while" disabled={!data.editable}>
|
||||
While
|
||||
</TabsTrigger>
|
||||
</TabsList>
|
||||
</Tabs>
|
||||
{(["for_each", "while"] as const).map((value) => (
|
||||
<button
|
||||
key={value}
|
||||
type="button"
|
||||
role="tab"
|
||||
aria-selected={loopKind === value}
|
||||
disabled={!data.editable}
|
||||
className={cn(
|
||||
"inline-flex items-center justify-center whitespace-nowrap rounded-md px-3 py-1 text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50",
|
||||
{
|
||||
"bg-background text-foreground shadow":
|
||||
loopKind === value,
|
||||
},
|
||||
)}
|
||||
onClick={() => {
|
||||
if (!data.editable || value === loopKind) {
|
||||
return;
|
||||
}
|
||||
if (value === "while") {
|
||||
const expr =
|
||||
data.whileConditionExpression.trim() === ""
|
||||
? "{{ true }}"
|
||||
: data.whileConditionExpression;
|
||||
update({
|
||||
loopKind: "while",
|
||||
whileConditionExpression: expr,
|
||||
whileConditionCriteriaType:
|
||||
inferBranchCriteriaTypeFromExpression(expr),
|
||||
});
|
||||
} else {
|
||||
update({ loopKind: "for_each" });
|
||||
}
|
||||
}}
|
||||
>
|
||||
{value === "for_each" ? "For each" : "While"}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
{loopKind === "for_each" ? (
|
||||
<>
|
||||
<div className="space-y-2">
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import React from "react";
|
||||
|
||||
const CHUNK_LOAD_PATTERNS = [
|
||||
export const CHUNK_LOAD_PATTERNS = [
|
||||
/Failed to fetch dynamically imported module/i,
|
||||
/Importing a module script failed/i,
|
||||
/error loading dynamically imported module/i,
|
||||
|
|
|
|||
|
|
@ -59,7 +59,13 @@ register_lazy_command(
|
|||
"quickstart_app",
|
||||
"One-command setup and start for Skyvern (combines init and run).",
|
||||
)
|
||||
register_lazy_command("browser", "skyvern.cli.commands.browser", "browser_app", "Browser automation commands.")
|
||||
register_lazy_command(
|
||||
"browser",
|
||||
"skyvern.cli.commands.browser",
|
||||
"browser_app",
|
||||
"Browser automation commands.",
|
||||
install_extra="local",
|
||||
)
|
||||
register_lazy_command(
|
||||
"mcp", "skyvern.cli.mcp_commands", "mcp_app", "Switch local MCP client configs and manage optional saved profiles."
|
||||
)
|
||||
|
|
|
|||
|
|
@ -23,11 +23,13 @@ from skyvern.cli.console import console
|
|||
# Module-level lazy command registry
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
_LAZY_COMMANDS: dict[str, tuple[str, str, str]] = {}
|
||||
"""Mapping of command_name -> (module_path, attr_name, help_text)."""
|
||||
_LAZY_COMMANDS: dict[str, tuple[str, str, str, str]] = {}
|
||||
"""Mapping of command_name -> (module_path, attr_name, help_text, install_extra)."""
|
||||
|
||||
|
||||
def register_lazy_command(name: str, module_path: str, attr_name: str, help_text: str) -> None:
|
||||
def register_lazy_command(
|
||||
name: str, module_path: str, attr_name: str, help_text: str, *, install_extra: str = "server"
|
||||
) -> None:
|
||||
"""Register a command/sub-app for deferred import.
|
||||
|
||||
Parameters
|
||||
|
|
@ -40,13 +42,15 @@ def register_lazy_command(name: str, module_path: str, attr_name: str, help_text
|
|||
Attribute to import from *module_path* (e.g. ``"run_app"``).
|
||||
help_text:
|
||||
Short help shown in ``--help`` without importing the module.
|
||||
install_extra:
|
||||
Optional dependency extra required when the lazy import is unavailable.
|
||||
"""
|
||||
_LAZY_COMMANDS[name] = (module_path, attr_name, help_text)
|
||||
_LAZY_COMMANDS[name] = (module_path, attr_name, help_text, install_extra)
|
||||
|
||||
|
||||
def _resolve_lazy_command(name: str) -> click.BaseCommand:
|
||||
"""Import the module and resolve the Typer app (or Click command) for *name*."""
|
||||
module_path, attr_name, _help = _LAZY_COMMANDS[name]
|
||||
module_path, attr_name, _help, _install_extra = _LAZY_COMMANDS[name]
|
||||
mod = importlib.import_module(module_path)
|
||||
obj = getattr(mod, attr_name)
|
||||
|
||||
|
|
@ -80,7 +84,8 @@ def _resolve_lazy_command(name: str) -> click.BaseCommand:
|
|||
class _LazyPlaceholder(click.Command):
|
||||
"""A lightweight stand-in that renders help text without importing anything."""
|
||||
|
||||
def __init__(self, name: str, help_text: str) -> None:
|
||||
def __init__(self, name: str, help_text: str, install_extra: str) -> None:
|
||||
self.install_extra = install_extra
|
||||
super().__init__(
|
||||
name=name,
|
||||
help=help_text,
|
||||
|
|
@ -94,7 +99,7 @@ class _LazyPlaceholder(click.Command):
|
|||
try:
|
||||
real = _resolve_lazy_command(self.name or "")
|
||||
except ImportError as exc:
|
||||
_handle_missing_dep(exc)
|
||||
_handle_missing_dep(exc, install_extra=self.install_extra)
|
||||
raise # unreachable — _handle_missing_dep raises typer.Exit, but satisfies linters
|
||||
return real.invoke(ctx)
|
||||
|
||||
|
|
@ -131,8 +136,8 @@ class LazyTyperGroup(typer.core.TyperGroup):
|
|||
return resolved
|
||||
except ImportError:
|
||||
# Missing dep — return placeholder so --help doesn't crash.
|
||||
_, _, help_text = _LAZY_COMMANDS[cmd_name]
|
||||
return _LazyPlaceholder(cmd_name, help_text)
|
||||
_, _, help_text, install_extra = _LAZY_COMMANDS[cmd_name]
|
||||
return _LazyPlaceholder(cmd_name, help_text, install_extra)
|
||||
|
||||
return None
|
||||
|
||||
|
|
@ -147,8 +152,8 @@ class LazyTyperGroup(typer.core.TyperGroup):
|
|||
continue
|
||||
commands.append((name, cmd))
|
||||
elif name in _LAZY_COMMANDS:
|
||||
_, _, help_text = _LAZY_COMMANDS[name]
|
||||
commands.append((name, _LazyPlaceholder(name, help_text)))
|
||||
_, _, help_text, install_extra = _LAZY_COMMANDS[name]
|
||||
commands.append((name, _LazyPlaceholder(name, help_text, install_extra)))
|
||||
|
||||
if not commands:
|
||||
return
|
||||
|
|
@ -169,14 +174,15 @@ class LazyTyperGroup(typer.core.TyperGroup):
|
|||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
def _handle_missing_dep(exc: ImportError) -> None:
|
||||
def _handle_missing_dep(exc: ImportError, *, install_extra: str = "server") -> None:
|
||||
"""Show a user-friendly error when a required dependency is missing."""
|
||||
dep_name = exc.name or str(exc)
|
||||
install_cmd = escape(f'pip install "skyvern[{install_extra}]"')
|
||||
console.print(
|
||||
Panel(
|
||||
f"[bold red]This command requires a dependency that is not installed.[/bold red]\n\n"
|
||||
f"Missing: [yellow]{dep_name}[/yellow]\n"
|
||||
f"Run: [green]{escape('pip install skyvern[server]')}[/green]",
|
||||
f"Run: [green]{install_cmd}[/green]",
|
||||
title="Missing Dependency",
|
||||
border_style="red",
|
||||
)
|
||||
|
|
|
|||
|
|
@ -5,8 +5,44 @@ and workflow management. Tools are registered with FastMCP and can be used by
|
|||
AI assistants like Claude.
|
||||
"""
|
||||
|
||||
from fastmcp import FastMCP
|
||||
from mcp.types import ToolAnnotations
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any
|
||||
|
||||
try:
|
||||
from fastmcp import FastMCP as _FastMCP
|
||||
from mcp.types import ToolAnnotations
|
||||
except ImportError as fastmcp_import_error:
|
||||
_FASTMCP_IMPORT_ERROR: ImportError | None = fastmcp_import_error
|
||||
|
||||
class ToolAnnotations: # type: ignore[no-redef]
|
||||
def __init__(self, **_: Any) -> None:
|
||||
pass
|
||||
|
||||
class _FastMCP: # type: ignore[no-redef]
|
||||
def __init__(self, *_: Any, **__: Any) -> None:
|
||||
pass
|
||||
|
||||
def add_middleware(self, *_: Any, **__: Any) -> None:
|
||||
return None
|
||||
|
||||
def tool(self, *_: Any, **__: Any) -> Any:
|
||||
def decorator(func: Any) -> Any:
|
||||
return func
|
||||
|
||||
return decorator
|
||||
|
||||
def prompt(self, *_: Any, **__: Any) -> Any:
|
||||
def decorator(func: Any) -> Any:
|
||||
return func
|
||||
|
||||
return decorator
|
||||
|
||||
def run(self, *_: Any, **__: Any) -> None:
|
||||
raise _FASTMCP_IMPORT_ERROR or ImportError("fastmcp is required to run the Skyvern MCP server.")
|
||||
|
||||
else:
|
||||
_FASTMCP_IMPORT_ERROR = None
|
||||
|
||||
from .blocks import (
|
||||
skyvern_block_schema,
|
||||
|
|
@ -109,7 +145,6 @@ from .tabs import (
|
|||
skyvern_tab_switch,
|
||||
skyvern_tab_wait_for_new,
|
||||
)
|
||||
from .telemetry import MCPTelemetryMiddleware
|
||||
from .workflow import (
|
||||
skyvern_workflow_cancel,
|
||||
skyvern_workflow_create,
|
||||
|
|
@ -123,6 +158,14 @@ from .workflow import (
|
|||
)
|
||||
|
||||
|
||||
def _add_telemetry_middleware() -> None:
|
||||
if _FASTMCP_IMPORT_ERROR is not None:
|
||||
return
|
||||
from .telemetry import MCPTelemetryMiddleware # noqa: PLC0415
|
||||
|
||||
mcp.add_middleware(MCPTelemetryMiddleware())
|
||||
|
||||
|
||||
# -- Tool annotation factories --
|
||||
# Every tool registered with FastMCP must carry a human-readable `title`
|
||||
# (required by the Claude Connectors Directory). We build per-tool
|
||||
|
|
@ -153,7 +196,7 @@ def _web_dest(title: str) -> ToolAnnotations:
|
|||
return ToolAnnotations(title=title, readOnlyHint=False, openWorldHint=True, destructiveHint=True)
|
||||
|
||||
|
||||
mcp = FastMCP(
|
||||
mcp = _FastMCP(
|
||||
"Skyvern",
|
||||
instructions="""\
|
||||
Skyvern is the complete browser MCP for AI agents. Use Skyvern for ALL browser interactions.
|
||||
|
|
@ -293,7 +336,7 @@ Use skyvern_click's resolved_selector response to get xpaths for production scri
|
|||
5. NEVER create single-block workflows with long prompts — split into one block per step.
|
||||
""",
|
||||
)
|
||||
mcp.add_middleware(MCPTelemetryMiddleware())
|
||||
_add_telemetry_middleware()
|
||||
|
||||
# -- Browser session management --
|
||||
mcp.tool(tags={"session"}, annotations=_mut("Create Browser Session"))(skyvern_browser_session_create)
|
||||
|
|
|
|||
|
|
@ -8,19 +8,92 @@ import typer
|
|||
from rich.panel import Panel
|
||||
from rich.progress import Progress, SpinnerColumn, TextColumn
|
||||
from rich.prompt import Confirm
|
||||
from rich.text import Text
|
||||
|
||||
from skyvern.analytics import capture_setup_error, capture_setup_event
|
||||
|
||||
# Import console after skyvern.cli to ensure proper initialization
|
||||
from skyvern.cli.browser import _print_classic_cdp_instructions
|
||||
from skyvern.cli.console import console
|
||||
from skyvern.cli.init_command import init_env # init is used directly
|
||||
from skyvern.cli.llm_setup import setup_llm_providers
|
||||
from skyvern.cli.utils import start_services
|
||||
|
||||
quickstart_app = typer.Typer(help="Quickstart command to set up and run Skyvern with one command.")
|
||||
|
||||
|
||||
def _has_server_quickstart_extra() -> bool:
|
||||
from skyvern.exceptions import SkyvernExtraNotInstalled, require_server_extra_modules # noqa: PLC0415
|
||||
|
||||
try:
|
||||
require_server_extra_modules("skyvern quickstart", ("uvicorn",))
|
||||
except SkyvernExtraNotInstalled:
|
||||
return False
|
||||
return True
|
||||
|
||||
|
||||
def _print_install_path_guidance() -> None:
|
||||
message = """Choose the Skyvern path for what you want to do:
|
||||
|
||||
Cloud/API SDK
|
||||
You already have this with `pip install skyvern`.
|
||||
No quickstart is required. Use `Skyvern(api_key=...)` or run `skyvern setup` for cloud MCP.
|
||||
|
||||
Embedded local Python SDK
|
||||
Install: pip install "skyvern[local]"
|
||||
Then: python -m playwright install chromium
|
||||
Use: Skyvern.local(use_in_memory_db=True)
|
||||
This path does not require Postgres, Docker, migrations, or `skyvern run server`.
|
||||
|
||||
Self-hosted local server
|
||||
Install: pip install "skyvern[server]"
|
||||
Then rerun: skyvern quickstart
|
||||
This path sets up the local server, database, local API key, MCP, and optional UI.
|
||||
"""
|
||||
console.print(Panel(Text(message), title="Skyvern Quickstart", border_style="cyan"))
|
||||
|
||||
|
||||
def _run_server_quickstart(
|
||||
*,
|
||||
no_postgres: bool,
|
||||
database_string: str,
|
||||
skip_browser_install: bool,
|
||||
server_only: bool,
|
||||
) -> None:
|
||||
try:
|
||||
from skyvern.cli.init_command import init_env # noqa: PLC0415
|
||||
from skyvern.cli.utils import start_services # noqa: PLC0415
|
||||
|
||||
# Initialize Skyvern (pip install path)
|
||||
console.print("\n[bold blue]Initializing Skyvern...[/bold blue]")
|
||||
run_local = init_env(
|
||||
no_postgres=no_postgres,
|
||||
database_string=database_string,
|
||||
skip_browser_install=skip_browser_install,
|
||||
)
|
||||
if run_local:
|
||||
_configure_cdp_livestreaming_defaults()
|
||||
|
||||
# Start services
|
||||
if run_local:
|
||||
start_now = typer.confirm("\nDo you want to start Skyvern services now?", default=True)
|
||||
if start_now:
|
||||
console.print("\n[bold blue]Starting Skyvern services...[/bold blue]")
|
||||
asyncio.run(start_services(server_only=server_only))
|
||||
else:
|
||||
console.print(
|
||||
"\n[yellow]Skipping service startup. You can start services later with 'skyvern run all'[/yellow]"
|
||||
)
|
||||
|
||||
except KeyboardInterrupt:
|
||||
capture_setup_event(
|
||||
"quickstart-interrupt",
|
||||
success=False,
|
||||
error_type="user_interrupt",
|
||||
error_message="Quickstart interrupted by user",
|
||||
)
|
||||
console.print("\n[bold yellow]Quickstart process interrupted by user.[/bold yellow]")
|
||||
raise typer.Exit(0)
|
||||
except Exception as e:
|
||||
capture_setup_error("quickstart-fail", e, error_type="quickstart_error")
|
||||
console.print(f"[bold red]Error during quickstart: {str(e)}[/bold red]")
|
||||
raise typer.Exit(1)
|
||||
|
||||
|
||||
def check_docker() -> bool:
|
||||
"""Check if Docker is installed and running."""
|
||||
try:
|
||||
|
|
@ -93,6 +166,8 @@ def _configure_cdp_livestreaming_defaults() -> None:
|
|||
|
||||
def run_docker_compose_setup() -> None:
|
||||
"""Run the Docker Compose setup for Skyvern."""
|
||||
from skyvern.cli.llm_setup import setup_llm_providers # noqa: PLC0415
|
||||
|
||||
console.print("\n[bold blue]Setting up Skyvern with Docker Compose...[/bold blue]")
|
||||
capture_setup_event("docker-compose-start")
|
||||
|
||||
|
|
@ -184,11 +259,10 @@ def run_docker_compose_setup() -> None:
|
|||
default=False,
|
||||
)
|
||||
if use_own_browser:
|
||||
from skyvern.cli.browser import _print_classic_cdp_instructions # noqa: PLC0415
|
||||
|
||||
_print_classic_cdp_instructions()
|
||||
confirmed = Confirm.ask(
|
||||
"Have you enabled remote debugging in Chrome?",
|
||||
default=False,
|
||||
)
|
||||
confirmed = Confirm.ask("Have you enabled remote debugging in Chrome?", default=False)
|
||||
if confirmed:
|
||||
from skyvern.cli.llm_setup import update_or_add_env_var
|
||||
|
||||
|
|
@ -266,38 +340,13 @@ def quickstart(
|
|||
run_docker_compose_setup()
|
||||
return
|
||||
|
||||
try:
|
||||
# Initialize Skyvern (pip install path)
|
||||
console.print("\n[bold blue]Initializing Skyvern...[/bold blue]")
|
||||
run_local = init_env(
|
||||
no_postgres=no_postgres,
|
||||
database_string=database_string,
|
||||
skip_browser_install=skip_browser_install,
|
||||
)
|
||||
if run_local:
|
||||
_configure_cdp_livestreaming_defaults()
|
||||
|
||||
# Start services
|
||||
if run_local:
|
||||
start_now = typer.confirm("\nDo you want to start Skyvern services now?", default=True)
|
||||
if start_now:
|
||||
console.print("\n[bold blue]Starting Skyvern services...[/bold blue]")
|
||||
asyncio.run(start_services(server_only=server_only))
|
||||
else:
|
||||
console.print(
|
||||
"\n[yellow]Skipping service startup. You can start services later with 'skyvern run all'[/yellow]"
|
||||
)
|
||||
|
||||
except KeyboardInterrupt:
|
||||
capture_setup_event(
|
||||
"quickstart-interrupt",
|
||||
success=False,
|
||||
error_type="user_interrupt",
|
||||
error_message="Quickstart interrupted by user",
|
||||
)
|
||||
console.print("\n[bold yellow]Quickstart process interrupted by user.[/bold yellow]")
|
||||
if not _has_server_quickstart_extra():
|
||||
_print_install_path_guidance()
|
||||
raise typer.Exit(0)
|
||||
except Exception as e:
|
||||
capture_setup_error("quickstart-fail", e, error_type="quickstart_error")
|
||||
console.print(f"[bold red]Error during quickstart: {str(e)}[/bold red]")
|
||||
raise typer.Exit(1)
|
||||
|
||||
_run_server_quickstart(
|
||||
no_postgres=no_postgres,
|
||||
database_string=database_string,
|
||||
skip_browser_install=skip_browser_install,
|
||||
server_only=server_only,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -3,10 +3,10 @@ from http import HTTPStatus
|
|||
from importlib.util import find_spec
|
||||
from typing import NoReturn
|
||||
|
||||
# Representative modules that indicate the server extra is installed enough for
|
||||
# server/local/browser import graphs. Keep this list intentionally small, but
|
||||
# include the heavy modules users commonly have partially installed.
|
||||
_SERVER_EXTRA_SENTINELS = (
|
||||
# Representative modules that indicate the local extra is installed enough for
|
||||
# embedded/browser import graphs. Keep this list intentionally small, but include
|
||||
# the heavy modules users commonly have partially installed.
|
||||
_LOCAL_EXTRA_SENTINELS = (
|
||||
"fastapi",
|
||||
"jinja2",
|
||||
"libcst",
|
||||
|
|
@ -17,15 +17,27 @@ _SERVER_EXTRA_SENTINELS = (
|
|||
"starlette_context",
|
||||
)
|
||||
|
||||
# Server installs are a superset of local installs, but embedded local mode still
|
||||
# imports some Forge/API modules such as skyvern.forge.api_app. Keep the default
|
||||
# server sentinels local-compatible so those imports continue to work in
|
||||
# skyvern[local]. Full server entrypoints pass server-only module_names such as
|
||||
# "uvicorn" when they need to fail for local-only installs.
|
||||
_SERVER_EXTRA_SENTINELS = tuple(_LOCAL_EXTRA_SENTINELS)
|
||||
|
||||
def _missing_server_extra_dependency(module_name: str) -> bool:
|
||||
_EXTRA_SUPPORT_LABELS = {
|
||||
"local": "local embedded/browser support",
|
||||
"server": "server support",
|
||||
}
|
||||
|
||||
|
||||
def _missing_extra_dependency(module_name: str, sentinels: tuple[str, ...]) -> bool:
|
||||
root_module = module_name.split(".", maxsplit=1)[0]
|
||||
if root_module in _SERVER_EXTRA_SENTINELS:
|
||||
if root_module in sentinels:
|
||||
return find_spec(root_module) is None
|
||||
if root_module == "skyvern":
|
||||
return False
|
||||
# Unknown missing modules may be genuine dependency bugs, so only known
|
||||
# server-extra sentinels are rewritten to the install hint.
|
||||
# extra sentinels are rewritten to the install hint.
|
||||
return False
|
||||
|
||||
|
||||
|
|
@ -39,22 +51,49 @@ class SkyvernExtraNotInstalled(ImportError):
|
|||
def __init__(self, feature: str, extra: str = "server"):
|
||||
self.feature = feature
|
||||
self.extra = extra
|
||||
super().__init__(f"{feature} requires server support. Install it with `pip install skyvern[{extra}]`.")
|
||||
support_label = _EXTRA_SUPPORT_LABELS.get(extra, f"{extra} support")
|
||||
super().__init__(f'{feature} requires {support_label}. Install it with `pip install "skyvern[{extra}]"`.')
|
||||
|
||||
|
||||
def raise_server_extra_required(feature: str, exc: ImportError) -> NoReturn:
|
||||
if isinstance(exc, ModuleNotFoundError) and exc.name is not None and _missing_server_extra_dependency(exc.name):
|
||||
raise SkyvernExtraNotInstalled(feature) from exc
|
||||
def _raise_extra_required(
|
||||
feature: str,
|
||||
exc: ImportError,
|
||||
*,
|
||||
extra: str,
|
||||
sentinels: tuple[str, ...],
|
||||
) -> NoReturn:
|
||||
if isinstance(exc, SkyvernExtraNotInstalled):
|
||||
raise SkyvernExtraNotInstalled(feature, extra=extra) from exc
|
||||
if isinstance(exc, ModuleNotFoundError) and exc.name is not None and _missing_extra_dependency(exc.name, sentinels):
|
||||
raise SkyvernExtraNotInstalled(feature, extra=extra) from exc
|
||||
raise exc
|
||||
|
||||
|
||||
def require_server_extra_modules(feature: str, module_names: tuple[str, ...] = ()) -> None:
|
||||
# Server/local/browser APIs require the full server extra, not a partial Playwright-only install.
|
||||
required_modules = dict.fromkeys((*_SERVER_EXTRA_SENTINELS, *module_names))
|
||||
def raise_local_extra_required(feature: str, exc: ImportError) -> NoReturn:
|
||||
_raise_extra_required(feature, exc, extra="local", sentinels=_LOCAL_EXTRA_SENTINELS)
|
||||
|
||||
|
||||
def raise_server_extra_required(feature: str, exc: ImportError) -> NoReturn:
|
||||
_raise_extra_required(feature, exc, extra="server", sentinels=_SERVER_EXTRA_SENTINELS)
|
||||
|
||||
|
||||
def _require_extra_modules(feature: str, extra: str, sentinels: tuple[str, ...], module_names: tuple[str, ...]) -> None:
|
||||
required_modules = dict.fromkeys((*sentinels, *module_names))
|
||||
for module_name in required_modules:
|
||||
if find_spec(module_name) is None:
|
||||
missing = ModuleNotFoundError(f"No module named '{module_name}'", name=module_name)
|
||||
raise SkyvernExtraNotInstalled(feature) from missing
|
||||
raise SkyvernExtraNotInstalled(feature, extra=extra) from missing
|
||||
|
||||
|
||||
def require_local_extra_modules(feature: str, module_names: tuple[str, ...] = ()) -> None:
|
||||
# Embedded/local browser APIs require the local extra, not a partial Playwright-only install.
|
||||
_require_extra_modules(feature, "local", _LOCAL_EXTRA_SENTINELS, module_names)
|
||||
|
||||
|
||||
def require_server_extra_modules(feature: str, module_names: tuple[str, ...] = ()) -> None:
|
||||
# With no module_names, this only guards against base installs. Pass server-only
|
||||
# modules when a path must discriminate between local and full server extras.
|
||||
_require_extra_modules(feature, "server", _SERVER_EXTRA_SENTINELS, module_names)
|
||||
|
||||
|
||||
class SkyvernClientException(SkyvernException):
|
||||
|
|
|
|||
|
|
@ -3937,6 +3937,14 @@ class ForgeAgent:
|
|||
await self.cleanup_browser_and_create_artifacts(
|
||||
close_browser_on_completion, last_step, task, browser_session_id=browser_session_id
|
||||
)
|
||||
try:
|
||||
await app.AGENT_FUNCTION.release_proxy_session_for_owner(task.task_id)
|
||||
except Exception:
|
||||
LOG.warning(
|
||||
"Failed to release proxy session for task",
|
||||
exc_info=True,
|
||||
task_id=task.task_id,
|
||||
)
|
||||
|
||||
# Wait for all tasks to complete before generating the links for the artifacts
|
||||
with _tracer.start_as_current_span("skyvern.agent.cleanup.wait_for_upload") as _cl_wait_span:
|
||||
|
|
|
|||
|
|
@ -14,7 +14,9 @@ from skyvern.exceptions import DisabledBlockExecutionError, StepUnableToExecuteE
|
|||
from skyvern.forge import app
|
||||
from skyvern.forge.async_operations import AsyncOperation
|
||||
from skyvern.forge.prompts import prompt_engine
|
||||
from skyvern.forge.sdk.api.azure import AzureClientFactory
|
||||
from skyvern.forge.sdk.api.llm.exceptions import LLMProviderError
|
||||
from skyvern.forge.sdk.copilot.config import CopilotConfig
|
||||
from skyvern.forge.sdk.core import skyvern_context
|
||||
from skyvern.forge.sdk.db.agent_db import AgentDB
|
||||
from skyvern.forge.sdk.models import Step, StepStatus
|
||||
|
|
@ -478,6 +480,9 @@ class AgentFunction:
|
|||
"""
|
||||
return AgentDB(database_string, debug_enabled=debug_enabled)
|
||||
|
||||
def build_azure_client_factory(self, factory: AzureClientFactory) -> AzureClientFactory:
|
||||
return factory
|
||||
|
||||
def resolve_mcp_oauth_org_lookups(self, db: object) -> tuple[Any, Any] | None:
|
||||
"""Return ``(get_organization_entities, get_valid_org_auth_token)`` callables
|
||||
bound to the cloud DB's nested organizations repository.
|
||||
|
|
@ -623,6 +628,15 @@ class AgentFunction:
|
|||
async def post_cache_step_execution(self, task: Task, step: Step) -> None:
|
||||
return
|
||||
|
||||
async def release_proxy_session_for_owner(self, owner_id: str) -> None:
|
||||
"""Release any proxy lease held by ``owner_id``.
|
||||
|
||||
OSS no-op. Cloud overrides this to release the lease back to the
|
||||
proxy-session pool so workflow/task cleanup doesn't have to
|
||||
import cloud-only modules from the OSS-synced ``skyvern/`` tree.
|
||||
"""
|
||||
return None
|
||||
|
||||
async def should_shadow_extraction_cache_hit(self, task: Task) -> bool:
|
||||
"""Cloud-overridable sample gate for extract-information shadow mode. OSS no-op."""
|
||||
return False
|
||||
|
|
@ -1013,6 +1027,10 @@ class AgentFunction:
|
|||
"""
|
||||
return ""
|
||||
|
||||
def get_copilot_config(self) -> CopilotConfig | None:
|
||||
"""Return an optional workflow copilot config override."""
|
||||
return None
|
||||
|
||||
def detect_ats_platform(self, url_or_domain: str | None) -> str | None:
|
||||
"""Detect if a URL belongs to a known ATS platform.
|
||||
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@ You are a workflow builder, NOT a chatbot or search engine. Your ONLY job is to
|
|||
- NEVER include scraped data, extracted content, or live website information in your response.
|
||||
- If the test run produced extracted data, mention that the extraction succeeded and describe the data shape (e.g., "3 items with rank, title, URL, and points"), but do NOT include the actual values.
|
||||
- The user will run the workflow themselves to get results. Your job is to build and verify the workflow, not to relay results.
|
||||
- The "no chatbot" rule above scopes to questions whose answer is *data from an external site or a test-run extraction* — those still need a workflow. Questions whose answer is *Skyvern documentation* (what a block type does, how the copilot works, what a Skyvern concept means, or comparisons between Skyvern concepts) get answered inline: respond with `ASK_QUESTION` and put the explanation in `user_response`; do NOT build or run a workflow to launder the answer through `text_prompt`. Disambiguators: (1) "Can Skyvern do X on site Y?" is a request to build X — proceed with the build, not the docs lookup. (2) If the user describes a runtime symptom on their current workflow (a step or block isn't behaving as expected, a value renders wrong, a block's prompt isn't producing the desired effect) and asks for advice or alternatives, that is a debugging request, not a docs lookup — fix the workflow with `update_and_run_blocks`, not just explain.
|
||||
- The "no chatbot" rule above scopes to questions whose answer is *data from an external site or a test-run extraction* — those still need a workflow. Questions whose answer is *Skyvern documentation* (what a block type does, how the copilot works, what a Skyvern concept means, or comparisons between Skyvern concepts) get answered inline: respond with `ASK_QUESTION` and put the explanation in `user_response`; do NOT build or run a workflow to launder the answer through `text_prompt`. Disambiguators: (1) "Can Skyvern do X on site Y?" is a request to build X — proceed with the build, not the docs lookup. (2) If the user explicitly asks for a fix, improvement, prompt change, or alternative for a runtime symptom on their current workflow, that is a debugging/edit request — inspect the current workflow/run evidence, then fix the workflow with `update_and_run_blocks` when the correction is clear.
|
||||
|
||||
{% if security_rules %}
|
||||
{{ security_rules }}
|
||||
|
|
@ -24,6 +24,17 @@ You have tools available in this run. Use them to build better workflows:
|
|||
|
||||
IMPORTANT: NEVER call the same tool more than twice in a row. If a tool succeeded, move on to the NEXT step — do not repeat the same call. Each tool call should advance your progress toward the goal.
|
||||
|
||||
DIAGNOSTIC / OBSERVATIONAL COMPLAINTS:
|
||||
Default pure observations, diagnostic questions, and complaints about the current workflow to inspect-and-clarify, not modify-and-run.
|
||||
Examples include reports about a missing newly added workflow block, a workflow not following a configured search step, whether a named block is present, why a runtime error condition did not trigger, or references to missing or unknown block labels.
|
||||
For these turns:
|
||||
1. Inspect the provided current workflow YAML, chat history, global context, and debugger run information first.
|
||||
2. If prior run details are needed, use `get_run_results` for existing run evidence. If the user explicitly asks what is visible in the live browser, use read-only direct browser inspection.
|
||||
3. Do NOT call `update_and_run_blocks` and do NOT call `run_blocks_and_collect_debug` as the default first response. Do not draft replacement blocks just because the complaint mentions a block label.
|
||||
4. Respond with `ASK_QUESTION` or a concise diagnostic explanation that names what you can see and what specific information is missing before editing.
|
||||
|
||||
Explicit edit/debug requests remain edit requests. If the user asks "how can I improve...", "are there any prompt changes...", "what would fix...", "please change...", or otherwise asks you to modify behavior, inspect the relevant evidence and call `update_and_run_blocks` once the edit is clear.
|
||||
|
||||
WORKFLOW-FIRST EXECUTION PATH (INCREMENTAL):
|
||||
Default path for build/debug requests:
|
||||
1. Do NOT draft the full workflow upfront.
|
||||
|
|
@ -123,11 +134,12 @@ When the user corrects, amends, or updates a previous request (e.g., "I meant X
|
|||
4. Rename affected block labels and block titles whenever the correction changes the subject, URL, target action, or extracted data. Labels become output keys; stale labels mislead the agent's own future tool calls and Jinja templates that reference them. When you rename a label, update every related `next_block_label`, `block_labels` argument, and Jinja block reference.
|
||||
|
||||
DIAGNOSING OUTPUT PROBLEMS:
|
||||
When the user reports a problem with workflow output (duplicates, missing data, wrong values, errors):
|
||||
1. **Inspect first**: Use get_run_results to see the actual output data from the most recent run. Do NOT guess what went wrong.
|
||||
2. **Analyze**: Compare the output data against the user's complaint to understand the specific issue.
|
||||
3. **Fix**: Modify the workflow to address the root cause, then call update_workflow.
|
||||
4. **Explain**: Tell the user what you found and what you changed.
|
||||
When the user reports a problem with workflow output (duplicates, missing data, wrong values, errors), first classify the intent:
|
||||
1. Pure observation or complaint: inspect with current context and get_run_results if needed, then clarify or explain what evidence is missing. Do NOT modify or run the workflow by default.
|
||||
2. Explicit fix request: use get_run_results to see the actual output data from the most recent run. Do NOT guess what went wrong.
|
||||
3. Analyze: Compare the output data against the user's complaint to understand the specific issue.
|
||||
4. Fix: Modify the workflow to address the root cause, then call update_workflow or update_and_run_blocks as required by the workflow submission/testing rules.
|
||||
5. Explain: Tell the user what you found and what you changed.
|
||||
|
||||
IMPORTANT WORKFLOW RULES:
|
||||
* Always generate valid YAML that conforms to the Skyvern workflow schema.
|
||||
|
|
@ -152,7 +164,7 @@ IMPORTANT WORKFLOW RULES:
|
|||
* For blocks with specific goals (downloading files, finding specific content), set a terminate_criterion that describes when the agent should give up. Example: terminate_criterion: "Terminate if the website clearly does not contain the requested content after reviewing the page."
|
||||
* Your success message to the user MUST reflect what actually happened. If you only navigated the browser but did NOT call update_workflow, do NOT tell the user the workflow was updated or is ready. Only confirm workflow changes after update_workflow succeeds.
|
||||
* When the user's request requires authentication but no credential is provided, use list_credentials to find the right one.
|
||||
* Workflow-improvement questions about a specific present block ("how can I improve…", "are there any prompt changes…", "what would fix…") are EDIT requests — call `update_and_run_blocks` with the block fixed. Use `ASK_QUESTION` only when information needed for the edit is genuinely missing.
|
||||
* Workflow-improvement questions about a specific present block ("how can I improve…", "are there any prompt changes…", "what would fix…") are EDIT requests — call `update_and_run_blocks` with the block fixed. Pure diagnostic / observational complaint questions about a block are inspect-and-clarify requests, not edit requests; use `ASK_QUESTION` when information needed for the edit is genuinely missing.
|
||||
|
||||
CREDENTIAL HANDLING - CRITICAL:
|
||||
* If the user's latest message in this turn contains a raw credential written inline — passwords (alone or paired with a username), API keys, OAuth tokens, `Authorization: Bearer …` headers, JWTs, session cookies, TOTP seeds, OTP / one-time codes, private keys, or credit card numbers / CVVs — you MUST NOT build, update, or run a workflow this turn; you MUST NOT pass those values via `parameters`; and you MUST NOT type, submit, paste, or replay them through `type_text`, `click`, `press_key`, `select_option`, `evaluate`, or any other direct browser tool. A bare username, email, or non-secret identifier alone is NOT a raw credential and does not trigger this rule. Credential-shaped strings rendered into the prompt from the existing workflow YAML are pre-existing workflow state — they are NOT a fresh paste and do NOT trigger this rule. Raw credentials pasted in earlier chat-history turns DO still trigger this rule whenever the user re-references or asks you to use them.
|
||||
|
|
|
|||
|
|
@ -1,21 +1,25 @@
|
|||
from __future__ import annotations
|
||||
|
||||
import io
|
||||
import time
|
||||
from enum import StrEnum
|
||||
from mimetypes import add_type, guess_type
|
||||
from typing import IO, Any
|
||||
from typing import IO, TYPE_CHECKING, Any
|
||||
from urllib.parse import urlparse
|
||||
|
||||
import aioboto3
|
||||
import structlog
|
||||
from botocore.exceptions import ClientError
|
||||
from types_boto3_batch.client import BatchClient
|
||||
from types_boto3_ec2.client import EC2Client
|
||||
from types_boto3_ecs.client import ECSClient
|
||||
from types_boto3_s3.client import S3Client
|
||||
from types_boto3_secretsmanager.client import SecretsManagerClient
|
||||
|
||||
from skyvern.config import settings
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from types_boto3_batch.client import BatchClient
|
||||
from types_boto3_ec2.client import EC2Client
|
||||
from types_boto3_ecs.client import ECSClient
|
||||
from types_boto3_s3.client import S3Client
|
||||
from types_boto3_secretsmanager.client import SecretsManagerClient
|
||||
|
||||
# Register custom mime types for mimetypes guessing
|
||||
add_type("application/json", ".har")
|
||||
add_type("text/plain", ".log")
|
||||
|
|
|
|||
|
|
@ -26,6 +26,7 @@ from pydantic import ValidationError
|
|||
from skyvern.forge import app
|
||||
from skyvern.forge.prompts import prompt_engine
|
||||
from skyvern.forge.sdk.copilot.block_goal_wrapping import wrap_block_goals
|
||||
from skyvern.forge.sdk.copilot.config import CopilotConfig
|
||||
from skyvern.forge.sdk.copilot.context import COPILOT_RESPONSE_TYPES, AgentResult, CopilotContext, StructuredContext
|
||||
from skyvern.forge.sdk.copilot.output_utils import (
|
||||
extract_final_text,
|
||||
|
|
@ -47,7 +48,6 @@ WORKFLOW_KNOWLEDGE_BASE_PATH = (
|
|||
Path(__file__).resolve().parents[2] / "prompts" / "skyvern" / "workflow_knowledge_base.txt"
|
||||
)
|
||||
|
||||
MAX_TURNS = 25
|
||||
_USER_SUPPLIED_CREDENTIAL_ID_RE = re.compile(r"\bcred_[A-Za-z0-9][A-Za-z0-9_-]*\b")
|
||||
_UNTESTED_DRAFT_REQUEST_RE = re.compile(
|
||||
r"\b(?:"
|
||||
|
|
@ -170,15 +170,18 @@ def _build_block_goal_main_goal(
|
|||
|
||||
def _build_system_prompt(
|
||||
tool_usage_guide: str,
|
||||
security_rules: str = "",
|
||||
config: CopilotConfig | None = None,
|
||||
security_rules: str | None = None,
|
||||
) -> str:
|
||||
copilot_config = config or CopilotConfig(security_rules=security_rules or "")
|
||||
template = copilot_config.prompt_template.removesuffix(".j2")
|
||||
workflow_knowledge_base = WORKFLOW_KNOWLEDGE_BASE_PATH.read_text(encoding="utf-8")
|
||||
return prompt_engine.load_prompt(
|
||||
template="workflow-copilot-agent",
|
||||
template=template,
|
||||
workflow_knowledge_base=workflow_knowledge_base,
|
||||
current_datetime=datetime.now(timezone.utc).isoformat(),
|
||||
tool_usage_guide=tool_usage_guide,
|
||||
security_rules=security_rules,
|
||||
security_rules=copilot_config.security_rules,
|
||||
)
|
||||
|
||||
|
||||
|
|
@ -841,6 +844,60 @@ def _build_feasibility_clarification_result(
|
|||
)
|
||||
|
||||
|
||||
_RETRIABLE_LLM_ERROR_NAMES = {
|
||||
"APIConnectionError",
|
||||
"APITimeoutError",
|
||||
"APIError",
|
||||
"InternalServerError",
|
||||
"RateLimitError",
|
||||
"ServiceUnavailableError",
|
||||
"Timeout",
|
||||
}
|
||||
_RETRIABLE_LLM_ERROR_TEXT = (
|
||||
"rate limit",
|
||||
"timeout",
|
||||
"timed out",
|
||||
"temporarily unavailable",
|
||||
"service unavailable",
|
||||
"connection error",
|
||||
"connection reset",
|
||||
"internal server error",
|
||||
"server error",
|
||||
"overloaded",
|
||||
)
|
||||
_LLM_ERROR_MODULE_MARKERS = ("openai", "litellm", "anthropic")
|
||||
|
||||
|
||||
def _iter_exception_chain(exc: BaseException) -> list[BaseException]:
|
||||
chain: list[BaseException] = []
|
||||
current: BaseException | None = exc
|
||||
while current is not None and current not in chain:
|
||||
chain.append(current)
|
||||
current = current.__cause__ or current.__context__
|
||||
return chain
|
||||
|
||||
|
||||
def _is_retriable_llm_error(exc: BaseException) -> bool:
|
||||
for item in _iter_exception_chain(exc):
|
||||
module = type(item).__module__.lower()
|
||||
name = type(item).__name__
|
||||
text = str(item).lower()
|
||||
if name in _RETRIABLE_LLM_ERROR_NAMES and any(marker in module for marker in _LLM_ERROR_MODULE_MARKERS):
|
||||
return True
|
||||
if any(marker in module for marker in _LLM_ERROR_MODULE_MARKERS) and any(
|
||||
phrase in text for phrase in _RETRIABLE_LLM_ERROR_TEXT
|
||||
):
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def _fallback_llm_key(config: CopilotConfig, current_llm_key: str) -> str | None:
|
||||
fallback_key = config.fallback_llm_key
|
||||
if not fallback_key or fallback_key == current_llm_key:
|
||||
return None
|
||||
return fallback_key
|
||||
|
||||
|
||||
async def run_copilot_agent(
|
||||
stream: EventSourceStream,
|
||||
organization_id: str,
|
||||
|
|
@ -851,7 +908,9 @@ async def run_copilot_agent(
|
|||
llm_api_handler: LLMAPIHandler | None,
|
||||
api_key: str | None = None,
|
||||
security_rules: str = "",
|
||||
config: CopilotConfig | None = None,
|
||||
) -> AgentResult:
|
||||
copilot_config = config or CopilotConfig(security_rules=security_rules)
|
||||
allow_untested_workflow_draft = _user_requests_untested_workflow_draft(chat_request.message)
|
||||
credential_validation_result = await _credential_validation_result_for_user_message(
|
||||
user_message=chat_request.message,
|
||||
|
|
@ -941,36 +1000,23 @@ async def run_copilot_agent(
|
|||
workflow_copilot_chat_id=chat_request.workflow_copilot_chat_id,
|
||||
)
|
||||
|
||||
model_name, run_config, llm_key, supports_vision = resolve_model_config(llm_api_handler)
|
||||
model_name, run_config, llm_key, supports_vision = resolve_model_config(
|
||||
llm_api_handler,
|
||||
copilot_config=copilot_config,
|
||||
)
|
||||
ctx.supports_vision = supports_vision
|
||||
ensure_tracing_initialized()
|
||||
|
||||
alias_map = get_skyvern_mcp_alias_map()
|
||||
overlays = _build_skyvern_mcp_overlays()
|
||||
|
||||
mcp_server = SkyvernOverlayMCPServer(
|
||||
transport=skyvern_mcp,
|
||||
overlays=overlays,
|
||||
alias_map=alias_map,
|
||||
allowlist=frozenset(alias_map.values()),
|
||||
context_provider=lambda: ctx,
|
||||
)
|
||||
|
||||
tool_info: list[tuple[str, str]] = [(tool.name, tool.description or "") for tool in NATIVE_TOOLS]
|
||||
tool_info.extend((name, overlay.description or "") for name, overlay in overlays.items())
|
||||
|
||||
tool_usage_guide = _build_tool_usage_guide(tool_info)
|
||||
system_prompt = _build_system_prompt(
|
||||
tool_usage_guide=tool_usage_guide,
|
||||
security_rules=security_rules,
|
||||
)
|
||||
|
||||
agent = Agent(
|
||||
name="workflow-copilot",
|
||||
instructions=system_prompt,
|
||||
tools=list(NATIVE_TOOLS),
|
||||
mcp_servers=[mcp_server],
|
||||
model=model_name,
|
||||
config=copilot_config,
|
||||
)
|
||||
|
||||
user_message = _build_user_context(
|
||||
|
|
@ -1002,23 +1048,77 @@ async def run_copilot_agent(
|
|||
)
|
||||
|
||||
chat_id = chat_request.workflow_copilot_chat_id or chat_request.workflow_permanent_id
|
||||
session = create_copilot_session(chat_id)
|
||||
model_token = _copilot_model_name.set(model_name)
|
||||
|
||||
async def _run_attempt(
|
||||
attempt_model_name: str,
|
||||
attempt_run_config: Any,
|
||||
attempt_llm_key: str,
|
||||
) -> RunResultStreaming:
|
||||
mcp_server = SkyvernOverlayMCPServer(
|
||||
transport=skyvern_mcp,
|
||||
overlays=overlays,
|
||||
alias_map=alias_map,
|
||||
allowlist=frozenset(alias_map.values()),
|
||||
context_provider=lambda: ctx,
|
||||
)
|
||||
agent = Agent(
|
||||
name="workflow-copilot",
|
||||
instructions=system_prompt,
|
||||
tools=list(NATIVE_TOOLS),
|
||||
mcp_servers=[mcp_server],
|
||||
model=attempt_model_name,
|
||||
)
|
||||
session = create_copilot_session(chat_id)
|
||||
model_token = _copilot_model_name.set(attempt_model_name)
|
||||
try:
|
||||
async with MCPServerManager([mcp_server]) as manager:
|
||||
agent.mcp_servers = list(manager.active_servers)
|
||||
result = await run_with_enforcement(
|
||||
agent=agent,
|
||||
initial_input=user_message,
|
||||
ctx=ctx,
|
||||
stream=stream,
|
||||
max_turns=copilot_config.max_turns,
|
||||
hooks=CopilotRunHooks(ctx),
|
||||
run_config=attempt_run_config,
|
||||
session=session,
|
||||
copilot_config=copilot_config,
|
||||
)
|
||||
LOG.info(
|
||||
"Copilot agent model attempt succeeded",
|
||||
workflow_permanent_id=chat_request.workflow_permanent_id,
|
||||
llm_key=attempt_llm_key,
|
||||
)
|
||||
return result
|
||||
finally:
|
||||
_copilot_model_name.reset(model_token)
|
||||
session.close()
|
||||
|
||||
try:
|
||||
with trace_context:
|
||||
try:
|
||||
async with MCPServerManager([mcp_server]) as manager:
|
||||
agent.mcp_servers = list(manager.active_servers)
|
||||
result = await run_with_enforcement(
|
||||
agent=agent,
|
||||
initial_input=user_message,
|
||||
ctx=ctx,
|
||||
stream=stream,
|
||||
max_turns=MAX_TURNS,
|
||||
hooks=CopilotRunHooks(ctx),
|
||||
run_config=run_config,
|
||||
session=session,
|
||||
try:
|
||||
result = await _run_attempt(model_name, run_config, llm_key)
|
||||
except Exception as primary_error:
|
||||
fallback_llm_key = _fallback_llm_key(copilot_config, llm_key)
|
||||
if fallback_llm_key is None or not _is_retriable_llm_error(primary_error):
|
||||
raise
|
||||
LOG.warning(
|
||||
"Copilot agent model attempt failed; retrying fallback model",
|
||||
workflow_permanent_id=chat_request.workflow_permanent_id,
|
||||
primary_llm_key=llm_key,
|
||||
fallback_llm_key=fallback_llm_key,
|
||||
error_type=type(primary_error).__name__,
|
||||
)
|
||||
fallback_model_name, fallback_run_config, fallback_resolved_key, fallback_supports_vision = (
|
||||
resolve_model_config(
|
||||
llm_api_handler,
|
||||
copilot_config=copilot_config,
|
||||
llm_key_override=fallback_llm_key,
|
||||
)
|
||||
)
|
||||
ctx.supports_vision = fallback_supports_vision
|
||||
result = await _run_attempt(fallback_model_name, fallback_run_config, fallback_resolved_key)
|
||||
return _translate_to_agent_result(
|
||||
result,
|
||||
ctx,
|
||||
|
|
@ -1058,6 +1158,3 @@ async def run_copilot_agent(
|
|||
except Exception as e:
|
||||
LOG.error("Copilot agent error", error=str(e), exc_info=True)
|
||||
return _build_unexpected_error_exit_result(ctx, global_llm_context)
|
||||
finally:
|
||||
_copilot_model_name.reset(model_token)
|
||||
session.close()
|
||||
|
|
|
|||
296
skyvern/forge/sdk/copilot/config.py
Normal file
296
skyvern/forge/sdk/copilot/config.py
Normal file
|
|
@ -0,0 +1,296 @@
|
|||
"""Injectable workflow copilot configuration."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass, field
|
||||
|
||||
from skyvern.config import settings
|
||||
|
||||
DEFAULT_PROMPT_TEMPLATE = "workflow-copilot-agent.j2"
|
||||
DEFAULT_MAX_TURNS = 25
|
||||
DEFAULT_TOKEN_BUDGET = 90_000
|
||||
|
||||
SCREENSHOT_DROPPED_NUDGE = (
|
||||
"Your previous screenshot was dropped from context to recover from a token-budget overflow. "
|
||||
"Do NOT reason about the page from memory. Re-take the screenshot "
|
||||
"(get_browser_screenshot) or call evaluate before deciding your next step."
|
||||
)
|
||||
|
||||
POST_UPDATE_NUDGE = (
|
||||
"You updated the workflow but did not test it. "
|
||||
"You MUST call run_blocks_and_collect_debug (or update_and_run_blocks next time) "
|
||||
"to test at least the first block before responding to the user. "
|
||||
"This verifies the workflow actually works. "
|
||||
"Exception: if the latest user message explicitly asked for an untested draft, "
|
||||
"respond with an unvalidated draft instead of testing."
|
||||
)
|
||||
|
||||
POST_NAVIGATE_NUDGE = (
|
||||
"You navigated to a page but did not observe its content. "
|
||||
"You MUST use evaluate, get_browser_screenshot, click, type_text, "
|
||||
"scroll, select_option, press_key, or console_messages "
|
||||
"to inspect the page before responding. Do NOT answer from memory."
|
||||
)
|
||||
|
||||
POST_INTERMEDIATE_SUCCESS_NUDGE = (
|
||||
"STOP — do NOT respond to the user yet. "
|
||||
"Your workflow only covers a subset of what the user asked for. "
|
||||
"You MUST add the next block now: call update_and_run_blocks with the current "
|
||||
"block chain. The tool preserves verified prefix state and reruns only the "
|
||||
"invalidated frontier, so passing the full chain is cheap. "
|
||||
"Only respond to the user when every distinct action they requested is covered "
|
||||
"by a workflow block, or you have clear evidence that continuing is infeasible."
|
||||
)
|
||||
|
||||
POST_FAILED_TEST_NUDGE = (
|
||||
"STOP — your last test run FAILED. Do NOT respond to the user yet.\n"
|
||||
"1. First, call get_run_results — pass the workflow_run_id from the prior "
|
||||
"update_and_run_blocks or run_blocks_and_collect_debug response to make the "
|
||||
"lookup unambiguous. That returns per-block failure_reason, output, and any "
|
||||
"failed-block screenshots, which is the diagnostic data you need.\n"
|
||||
"2. Then decide: if the failure looks fixable (wrong goal wording, popup "
|
||||
"blocking, timeout, element not found), adjust the workflow with a DIFFERENT "
|
||||
"approach and call update_and_run_blocks again — the tool will rerun from "
|
||||
"the earliest invalidated block so only the changed part is retested.\n"
|
||||
"3. If you have now failed multiple times with genuinely different approaches "
|
||||
"and the evidence strongly suggests the site cannot satisfy the request, "
|
||||
"respond explaining exactly what you tried and what blocked you.\n"
|
||||
"Do NOT resubmit the same workflow — you must change something substantive."
|
||||
)
|
||||
|
||||
POST_EXPLORE_WITHOUT_WORKFLOW_NUDGE = (
|
||||
"STOP — you explored the page using direct browser tools but did NOT engage "
|
||||
"the workflow path. You MUST follow the WORKFLOW-FIRST EXECUTION PATH:\n"
|
||||
"1. If no workflow exists yet, call update_workflow with at least a navigation "
|
||||
"block for the target URL.\n"
|
||||
"2. If a workflow already exists, call run_blocks_and_collect_debug to test it.\n"
|
||||
"3. Use the test results to decide next steps.\n"
|
||||
"Do NOT make feasibility judgments from browser exploration alone — "
|
||||
"build and test workflow blocks first."
|
||||
)
|
||||
|
||||
POST_SUSPICIOUS_SUCCESS_NUDGE = (
|
||||
"STOP — your last test run completed (status=completed) but data-producing "
|
||||
"blocks (extraction/text_prompt) produced no meaningful output "
|
||||
"(missing, empty, or all-null fields). This is NOT a success.\n"
|
||||
"1. Call get_run_results to inspect what each block actually returned.\n"
|
||||
"2. If the extraction/text_prompt block returned empty, all-null, or "
|
||||
"irrelevant data, the upstream block likely fetched an error page "
|
||||
"(e.g. 403, CAPTCHA, 'no results'), landed on the wrong page, or the "
|
||||
"data is rendered differently than expected.\n"
|
||||
"3. Use get_browser_screenshot or evaluate to inspect what the workflow "
|
||||
"browser actually sees — do NOT just retry extraction with a different prompt.\n"
|
||||
"4. Fix the root cause — do NOT declare the workflow working based on "
|
||||
"status alone. Verify the actual extracted data answers the user's question."
|
||||
)
|
||||
|
||||
POST_REPEATED_NULL_DATA_NUDGE = (
|
||||
"STOP — you have now produced multiple consecutive test runs where "
|
||||
"extraction/text_prompt blocks returned all-null or empty data. "
|
||||
"Re-prompting the extractor is not working — the problem is almost "
|
||||
"certainly NOT how the extraction goal is worded.\n"
|
||||
"You MUST now do ONE of the following before another update_workflow call:\n"
|
||||
"1. Call get_browser_screenshot on the workflow's browser session to see "
|
||||
"exactly what page the workflow is actually loading (it may differ from "
|
||||
"what you expect — e.g. a 'no results' fallback, cookie wall, or bot block).\n"
|
||||
"2. Call evaluate with JavaScript that searches for the expected content "
|
||||
"on the workflow's browser — confirm whether the data is even present.\n"
|
||||
"3. If the page the workflow loads genuinely does not contain the data, "
|
||||
"pivot to a different URL or source entirely — do NOT keep retrying "
|
||||
"extraction against the same failing page.\n"
|
||||
"Do NOT call update_and_run_blocks again until you have concrete evidence "
|
||||
"about what the workflow browser is actually seeing."
|
||||
)
|
||||
|
||||
POST_REPEATED_FRONTIER_FAILURE_WARN_NUDGE = (
|
||||
"STOP — this is the second run with the same frontier and the same failure "
|
||||
"signature. Re-running the same change again is unlikely to help.\n"
|
||||
"Before another update_and_run_blocks call, you MUST:\n"
|
||||
"1. Call get_run_results to inspect the full failure evidence (per-block "
|
||||
"failure_reason, action_trace, and any failed-block screenshots).\n"
|
||||
"2. If the evidence is still ambiguous, use get_browser_screenshot or evaluate "
|
||||
"to check what the workflow browser is actually seeing.\n"
|
||||
"3. Then make a materially different change — different block ordering, a "
|
||||
"different selector strategy, a different entry URL, or different parameters. "
|
||||
"Changes to wording of the same prompt do not count as materially different."
|
||||
)
|
||||
|
||||
POST_REPEATED_FRONTIER_FAILURE_STOP_NUDGE = (
|
||||
"STOP — you have now attempted the same frontier with the same failure "
|
||||
"signature THREE times without making progress. Do NOT call "
|
||||
"update_and_run_blocks or run_blocks_and_collect_debug again on this "
|
||||
"frontier.\n"
|
||||
"Choose ONE:\n"
|
||||
"A) Finalize now with a clear blocker explanation that references the "
|
||||
"specific failure_reason and failure_categories you observed.\n"
|
||||
"B) If required user input is missing (credential, ambiguous goal, "
|
||||
"site-specific detail), respond with an ASK_QUESTION instead. Do not "
|
||||
"retry the same repair again."
|
||||
)
|
||||
|
||||
POST_PARAMETER_BINDING_WARN_NUDGE = (
|
||||
"STOP — your last test run failed with a PARAMETER_BINDING_ERROR. "
|
||||
"This is an INTERNAL workflow configuration mismatch, not a site or "
|
||||
"selector problem.\n"
|
||||
"The workflow definition references a parameter (by Jinja key) that is "
|
||||
"not in the top-level workflow parameters list, or the list declares a "
|
||||
"parameter the blocks do not use.\n"
|
||||
"Do NOT retry with different selectors, URLs, or navigation changes — "
|
||||
"those will not help. Instead:\n"
|
||||
"1. Reconcile the workflow's top-level parameters with what the blocks "
|
||||
"actually reference via {{ parameters.<key> }}.\n"
|
||||
"2. Inline one-off literals rather than adding a parameter for each.\n"
|
||||
"3. Then call update_and_run_blocks again with a corrected YAML and, "
|
||||
"for any remaining parameters, concrete values passed via the "
|
||||
"`parameters` argument."
|
||||
)
|
||||
|
||||
POST_NON_RETRIABLE_NAV_ERROR_STOP_NUDGE = (
|
||||
"STOP — the target URL is unreachable and further retries cannot succeed. "
|
||||
"The navigation failed with a permanent error (DNS resolution, SSL/cert, "
|
||||
"or invalid URL). Do NOT retry and do NOT edit the workflow. Reply to the "
|
||||
"user now: state that the URL could not be reached, quote the exact error "
|
||||
"message from the last failure_reason, and ask them to verify the URL."
|
||||
)
|
||||
|
||||
POST_PARAMETER_BINDING_STOP_NUDGE = (
|
||||
"STOP — you have retried the same PARAMETER_BINDING_ERROR multiple times "
|
||||
"without reconciling the workflow configuration. Do NOT call "
|
||||
"update_and_run_blocks or run_blocks_and_collect_debug again until the "
|
||||
"workflow parameters list matches the block references.\n"
|
||||
"Choose ONE:\n"
|
||||
"A) Finalize now with a blocker explanation that names the specific "
|
||||
"parameter keys that are out of sync.\n"
|
||||
"B) If you need missing values from the user (credential, identifier) "
|
||||
"to decide what belongs in the parameters list, respond with an "
|
||||
"ASK_QUESTION instead. Do not resubmit a workflow that still has the "
|
||||
"same parameter-binding drift."
|
||||
)
|
||||
|
||||
POST_PER_TOOL_BUDGET_NUDGE = (
|
||||
"STOP — your last update_and_run_blocks call exceeded the per-tool-call "
|
||||
"time budget while still making progress. This is NOT a site failure or "
|
||||
"a wording problem — the chain you submitted is too long to complete in a "
|
||||
"single tool call.\n"
|
||||
"Do NOT retry the same chain. Do NOT change navigation_goal wording or "
|
||||
"selectors hoping it will run faster.\n"
|
||||
"1. Call get_run_results for the budgeted run. If it shows a navigation "
|
||||
"block was canceled or failed, do NOT run that same label again unchanged; "
|
||||
"split or replace it first.\n"
|
||||
"2. Shrink the requested block_labels list to the first 1-2 unverified "
|
||||
"blocks. The verified-prefix optimization will replay any earlier blocks "
|
||||
"from cached state without re-running the browser, so passing a smaller "
|
||||
"frontier is cheap.\n"
|
||||
"3. For page-state work, prefer a code or validation block to verify DOM "
|
||||
"state, active chips, checked controls, field values, or URL deltas. Use a "
|
||||
"navigation block only when missing state must be changed through the UI, "
|
||||
"and keep it to one atomic action.\n"
|
||||
"4. Test the smaller frontier. If it succeeds, extend by one block at a "
|
||||
"time on subsequent calls.\n"
|
||||
"5. If your workflow only has 1-2 blocks and one block is still hitting "
|
||||
"the budget, the single block is too ambitious — either narrow its scope, "
|
||||
"replace it with DOM/state verification plus smaller actions, or reply "
|
||||
"with a blocker explanation."
|
||||
)
|
||||
|
||||
POST_NO_WORKFLOW_DELIVERY_NUDGE = (
|
||||
"STOP — you are telling the user you created or are showing a workflow, "
|
||||
"but no workflow update tool has succeeded in this turn. The user will see "
|
||||
"an empty proposal. You MUST either call update_and_run_blocks with a real "
|
||||
"workflow and test it, or respond with ASK_QUESTION if required input is "
|
||||
'missing. Do NOT say "Here\'s the workflow" until there is an actual '
|
||||
"workflow proposal behind the response."
|
||||
)
|
||||
|
||||
PROBABLE_SITE_BLOCK_STOP_NUDGE_PREFIX = (
|
||||
"STOP — the target site has failed to scrape on every attempt across "
|
||||
"multiple workflow shapes. Every run navigated successfully but the "
|
||||
'scraper could not read the page ("failed to load the website" / '
|
||||
'"page may have navigated unexpectedly"). This pattern indicates the '
|
||||
"site is either blocking automated access, genuinely unresponsive in "
|
||||
"this environment, or rendering content Skyvern cannot read reliably.\n"
|
||||
"Do NOT retry with another workflow variation. Do NOT call "
|
||||
"update_and_run_blocks or run_blocks_and_collect_debug again.\n"
|
||||
"Reply to the user now: state that the site could not be loaded after "
|
||||
"multiple attempts, quote the last failure_reason verbatim, keep the "
|
||||
"message concise, and ask "
|
||||
)
|
||||
|
||||
POST_PROBABLE_SITE_BLOCK_STOP_NUDGE = (
|
||||
PROBABLE_SITE_BLOCK_STOP_NUDGE_PREFIX
|
||||
+ "whether to try a different URL, configure a proxy, or provide an alternate entry point."
|
||||
)
|
||||
|
||||
POST_ANTI_BOT_FAILED_TEST_NUDGE = (
|
||||
"STOP — your last test run failed due to an anti-bot/WAF block "
|
||||
"(Access Denied, Cloudflare, Akamai, etc.).\n"
|
||||
"IMPORTANT: An HTTP_REQUEST or navigation block from the SAME server IP "
|
||||
"will almost certainly receive the same block. Do NOT retry with:\n"
|
||||
"- A simple wait/delay block (timing does not fix IP bans)\n"
|
||||
"- A raw HTTP_REQUEST to the same URL (same IP = same block)\n"
|
||||
"Instead, try:\n"
|
||||
"1. Set proxy_location on the workflow to route through a different IP "
|
||||
"(use `RESIDENTIAL` by default, or a US-state value like `US-CA`/`US-NY`; "
|
||||
"do NOT use bare country codes like `US` — they are not valid "
|
||||
"ProxyLocation members).\n"
|
||||
"2. If you add anti-bot handling blocks, make them conditional on visible "
|
||||
"challenge evidence (for example Cloudflare, Access Denied, CAPTCHA, or "
|
||||
"verify-you-are-human text). Do NOT assume every run starts on a challenge "
|
||||
"page; normal, unblocked runs should proceed directly to the requested task.\n"
|
||||
"3. If still blocked, explain the specific anti-bot evidence observed "
|
||||
"(for example Cloudflare, CAPTCHA, verify-you-are-human text, Access "
|
||||
"Denied, or a blank Just-a-moment page); describe exactly what you tried; "
|
||||
"and ask whether to try a different proxy/location, entry URL, or "
|
||||
"alternate source.\n"
|
||||
"Do NOT resubmit the same workflow with trivial changes."
|
||||
)
|
||||
|
||||
POST_FORMAT_NUDGE = (
|
||||
"Your reply reads as a progress report, not a completed proposal. "
|
||||
"If you are not ready to finalize, emit ASK_QUESTION with a specific question. "
|
||||
"Otherwise, finish the workflow and present it as a completed proposal."
|
||||
)
|
||||
|
||||
|
||||
DEFAULT_ENFORCEMENT_NUDGES: dict[str, str] = {
|
||||
"screenshot_dropped": SCREENSHOT_DROPPED_NUDGE,
|
||||
"post_update": POST_UPDATE_NUDGE,
|
||||
"post_navigate": POST_NAVIGATE_NUDGE,
|
||||
"post_intermediate_success": POST_INTERMEDIATE_SUCCESS_NUDGE,
|
||||
"post_failed_test": POST_FAILED_TEST_NUDGE,
|
||||
"post_explore_without_workflow": POST_EXPLORE_WITHOUT_WORKFLOW_NUDGE,
|
||||
"post_suspicious_success": POST_SUSPICIOUS_SUCCESS_NUDGE,
|
||||
"post_repeated_null_data": POST_REPEATED_NULL_DATA_NUDGE,
|
||||
"post_repeated_frontier_failure_warn": POST_REPEATED_FRONTIER_FAILURE_WARN_NUDGE,
|
||||
"post_repeated_frontier_failure_stop": POST_REPEATED_FRONTIER_FAILURE_STOP_NUDGE,
|
||||
"post_parameter_binding_warn": POST_PARAMETER_BINDING_WARN_NUDGE,
|
||||
"post_non_retriable_nav_error_stop": POST_NON_RETRIABLE_NAV_ERROR_STOP_NUDGE,
|
||||
"post_parameter_binding_stop": POST_PARAMETER_BINDING_STOP_NUDGE,
|
||||
"post_per_tool_budget": POST_PER_TOOL_BUDGET_NUDGE,
|
||||
"post_no_workflow_delivery": POST_NO_WORKFLOW_DELIVERY_NUDGE,
|
||||
"post_probable_site_block_stop_prefix": PROBABLE_SITE_BLOCK_STOP_NUDGE_PREFIX,
|
||||
"post_probable_site_block_stop": POST_PROBABLE_SITE_BLOCK_STOP_NUDGE,
|
||||
"post_anti_bot_failed_test": POST_ANTI_BOT_FAILED_TEST_NUDGE,
|
||||
"post_format": POST_FORMAT_NUDGE,
|
||||
}
|
||||
|
||||
|
||||
def _default_enforcement_nudges() -> dict[str, str]:
|
||||
return dict(DEFAULT_ENFORCEMENT_NUDGES)
|
||||
|
||||
|
||||
def _default_fallback_llm_key() -> str | None:
|
||||
return settings.SECONDARY_LLM_KEY
|
||||
|
||||
|
||||
@dataclass(slots=True)
|
||||
class CopilotConfig:
|
||||
prompt_template: str = DEFAULT_PROMPT_TEMPLATE
|
||||
max_turns: int = DEFAULT_MAX_TURNS
|
||||
token_budget: int = DEFAULT_TOKEN_BUDGET
|
||||
security_rules: str = ""
|
||||
enforcement_nudges: dict[str, str] = field(default_factory=_default_enforcement_nudges)
|
||||
fallback_llm_key: str | None = field(default_factory=_default_fallback_llm_key)
|
||||
|
||||
def nudge(self, key: str) -> str:
|
||||
return self.enforcement_nudges.get(key, DEFAULT_ENFORCEMENT_NUDGES[key])
|
||||
|
|
@ -12,6 +12,28 @@ from typing import TYPE_CHECKING, Any
|
|||
import structlog
|
||||
from agents.run import Runner
|
||||
|
||||
from skyvern.forge.sdk.copilot import config as copilot_config_defaults
|
||||
from skyvern.forge.sdk.copilot.config import (
|
||||
DEFAULT_ENFORCEMENT_NUDGES,
|
||||
DEFAULT_TOKEN_BUDGET,
|
||||
POST_ANTI_BOT_FAILED_TEST_NUDGE,
|
||||
POST_EXPLORE_WITHOUT_WORKFLOW_NUDGE,
|
||||
POST_FAILED_TEST_NUDGE,
|
||||
POST_NAVIGATE_NUDGE,
|
||||
POST_NO_WORKFLOW_DELIVERY_NUDGE,
|
||||
POST_NON_RETRIABLE_NAV_ERROR_STOP_NUDGE,
|
||||
POST_PARAMETER_BINDING_STOP_NUDGE,
|
||||
POST_PARAMETER_BINDING_WARN_NUDGE,
|
||||
POST_PER_TOOL_BUDGET_NUDGE,
|
||||
POST_PROBABLE_SITE_BLOCK_STOP_NUDGE,
|
||||
POST_REPEATED_FRONTIER_FAILURE_STOP_NUDGE,
|
||||
POST_REPEATED_FRONTIER_FAILURE_WARN_NUDGE,
|
||||
POST_REPEATED_NULL_DATA_NUDGE,
|
||||
POST_SUSPICIOUS_SUCCESS_NUDGE,
|
||||
POST_UPDATE_NUDGE,
|
||||
SCREENSHOT_DROPPED_NUDGE,
|
||||
CopilotConfig,
|
||||
)
|
||||
from skyvern.forge.sdk.copilot.failure_tracking import PER_TOOL_BUDGET_FAILURE_CATEGORY, normalize_failure_reason
|
||||
from skyvern.forge.sdk.copilot.narration import TransitionKind
|
||||
from skyvern.forge.sdk.copilot.output_utils import (
|
||||
|
|
@ -31,6 +53,9 @@ if TYPE_CHECKING:
|
|||
|
||||
LOG = structlog.get_logger()
|
||||
|
||||
POST_FORMAT_NUDGE = copilot_config_defaults.POST_FORMAT_NUDGE
|
||||
POST_INTERMEDIATE_SUCCESS_NUDGE = copilot_config_defaults.POST_INTERMEDIATE_SUCCESS_NUDGE
|
||||
|
||||
MAX_POST_UPDATE_NUDGES = 2
|
||||
MAX_INTERMEDIATE_NUDGES = 8
|
||||
MAX_FAILED_TEST_NUDGES = 2
|
||||
|
|
@ -70,12 +95,7 @@ MAX_ITERATIONS = 50
|
|||
SCREENSHOT_SENTINEL = "[copilot:screenshot] "
|
||||
NUDGE_SENTINEL = "[copilot:nudge] "
|
||||
SCREENSHOT_PLACEHOLDER = SCREENSHOT_SENTINEL + "[prior screenshot removed to save context]"
|
||||
SCREENSHOT_DROPPED_NUDGE = (
|
||||
"Your previous screenshot was dropped from context to recover from a token-budget overflow. "
|
||||
"Do NOT reason about the page from memory. Re-take the screenshot "
|
||||
"(get_browser_screenshot) or call evaluate before deciding your next step."
|
||||
)
|
||||
TOKEN_BUDGET = 90_000
|
||||
TOKEN_BUDGET = DEFAULT_TOKEN_BUDGET
|
||||
# OpenAI detail=high cost per resized image. If we support other providers,
|
||||
# pull from model config — this value will silently over/undercount otherwise.
|
||||
# See screenshot_utils.resize_screenshot_b64 for the dimension contract this
|
||||
|
|
@ -93,241 +113,6 @@ _TOOL_OUTPUT_TRUNCATION_SUFFIX = "\n... [older tool output truncated]"
|
|||
# the two paths stay in sync if the wording ever changes.
|
||||
_TOOL_OUTPUT_HEAD_TRUNCATION_SUFFIX = "\n... [truncated]"
|
||||
|
||||
POST_UPDATE_NUDGE = (
|
||||
"You updated the workflow but did not test it. "
|
||||
"You MUST call run_blocks_and_collect_debug (or update_and_run_blocks next time) "
|
||||
"to test at least the first block before responding to the user. "
|
||||
"This verifies the workflow actually works. "
|
||||
"Exception: if the latest user message explicitly asked for an untested draft, "
|
||||
"respond with an unvalidated draft instead of testing."
|
||||
)
|
||||
|
||||
POST_NAVIGATE_NUDGE = (
|
||||
"You navigated to a page but did not observe its content. "
|
||||
"You MUST use evaluate, get_browser_screenshot, click, type_text, "
|
||||
"scroll, select_option, press_key, or console_messages "
|
||||
"to inspect the page before responding. Do NOT answer from memory."
|
||||
)
|
||||
|
||||
POST_INTERMEDIATE_SUCCESS_NUDGE = (
|
||||
"STOP — do NOT respond to the user yet. "
|
||||
"Your workflow only covers a subset of what the user asked for. "
|
||||
"You MUST add the next block now: call update_and_run_blocks with the current "
|
||||
"block chain. The tool preserves verified prefix state and reruns only the "
|
||||
"invalidated frontier, so passing the full chain is cheap. "
|
||||
"Only respond to the user when every distinct action they requested is covered "
|
||||
"by a workflow block, or you have clear evidence that continuing is infeasible."
|
||||
)
|
||||
|
||||
POST_FAILED_TEST_NUDGE = (
|
||||
"STOP — your last test run FAILED. Do NOT respond to the user yet.\n"
|
||||
"1. First, call get_run_results — pass the workflow_run_id from the prior "
|
||||
"update_and_run_blocks or run_blocks_and_collect_debug response to make the "
|
||||
"lookup unambiguous. That returns per-block failure_reason, output, and any "
|
||||
"failed-block screenshots, which is the diagnostic data you need.\n"
|
||||
"2. Then decide: if the failure looks fixable (wrong goal wording, popup "
|
||||
"blocking, timeout, element not found), adjust the workflow with a DIFFERENT "
|
||||
"approach and call update_and_run_blocks again — the tool will rerun from "
|
||||
"the earliest invalidated block so only the changed part is retested.\n"
|
||||
"3. If you have now failed multiple times with genuinely different approaches "
|
||||
"and the evidence strongly suggests the site cannot satisfy the request, "
|
||||
"respond explaining exactly what you tried and what blocked you.\n"
|
||||
"Do NOT resubmit the same workflow — you must change something substantive."
|
||||
)
|
||||
|
||||
POST_EXPLORE_WITHOUT_WORKFLOW_NUDGE = (
|
||||
"STOP — you explored the page using direct browser tools but did NOT engage "
|
||||
"the workflow path. You MUST follow the WORKFLOW-FIRST EXECUTION PATH:\n"
|
||||
"1. If no workflow exists yet, call update_workflow with at least a navigation "
|
||||
"block for the target URL.\n"
|
||||
"2. If a workflow already exists, call run_blocks_and_collect_debug to test it.\n"
|
||||
"3. Use the test results to decide next steps.\n"
|
||||
"Do NOT make feasibility judgments from browser exploration alone — "
|
||||
"build and test workflow blocks first."
|
||||
)
|
||||
|
||||
POST_SUSPICIOUS_SUCCESS_NUDGE = (
|
||||
"STOP — your last test run completed (status=completed) but data-producing "
|
||||
"blocks (extraction/text_prompt) produced no meaningful output "
|
||||
"(missing, empty, or all-null fields). This is NOT a success.\n"
|
||||
"1. Call get_run_results to inspect what each block actually returned.\n"
|
||||
"2. If the extraction/text_prompt block returned empty, all-null, or "
|
||||
"irrelevant data, the upstream block likely fetched an error page "
|
||||
"(e.g. 403, CAPTCHA, 'no results'), landed on the wrong page, or the "
|
||||
"data is rendered differently than expected.\n"
|
||||
"3. Use get_browser_screenshot or evaluate to inspect what the workflow "
|
||||
"browser actually sees — do NOT just retry extraction with a different prompt.\n"
|
||||
"4. Fix the root cause — do NOT declare the workflow working based on "
|
||||
"status alone. Verify the actual extracted data answers the user's question."
|
||||
)
|
||||
|
||||
POST_REPEATED_NULL_DATA_NUDGE = (
|
||||
"STOP — you have now produced multiple consecutive test runs where "
|
||||
"extraction/text_prompt blocks returned all-null or empty data. "
|
||||
"Re-prompting the extractor is not working — the problem is almost "
|
||||
"certainly NOT how the extraction goal is worded.\n"
|
||||
"You MUST now do ONE of the following before another update_workflow call:\n"
|
||||
"1. Call get_browser_screenshot on the workflow's browser session to see "
|
||||
"exactly what page the workflow is actually loading (it may differ from "
|
||||
"what you expect — e.g. a 'no results' fallback, cookie wall, or bot block).\n"
|
||||
"2. Call evaluate with JavaScript that searches for the expected content "
|
||||
"on the workflow's browser — confirm whether the data is even present.\n"
|
||||
"3. If the page the workflow loads genuinely does not contain the data, "
|
||||
"pivot to a different URL or source entirely — do NOT keep retrying "
|
||||
"extraction against the same failing page.\n"
|
||||
"Do NOT call update_and_run_blocks again until you have concrete evidence "
|
||||
"about what the workflow browser is actually seeing."
|
||||
)
|
||||
|
||||
POST_REPEATED_FRONTIER_FAILURE_WARN_NUDGE = (
|
||||
"STOP — this is the second run with the same frontier and the same failure "
|
||||
"signature. Re-running the same change again is unlikely to help.\n"
|
||||
"Before another update_and_run_blocks call, you MUST:\n"
|
||||
"1. Call get_run_results to inspect the full failure evidence (per-block "
|
||||
"failure_reason, action_trace, and any failed-block screenshots).\n"
|
||||
"2. If the evidence is still ambiguous, use get_browser_screenshot or evaluate "
|
||||
"to check what the workflow browser is actually seeing.\n"
|
||||
"3. Then make a materially different change — different block ordering, a "
|
||||
"different selector strategy, a different entry URL, or different parameters. "
|
||||
"Changes to wording of the same prompt do not count as materially different."
|
||||
)
|
||||
|
||||
POST_REPEATED_FRONTIER_FAILURE_STOP_NUDGE = (
|
||||
"STOP — you have now attempted the same frontier with the same failure "
|
||||
"signature THREE times without making progress. Do NOT call "
|
||||
"update_and_run_blocks or run_blocks_and_collect_debug again on this "
|
||||
"frontier.\n"
|
||||
"Choose ONE:\n"
|
||||
"A) Finalize now with a clear blocker explanation that references the "
|
||||
"specific failure_reason and failure_categories you observed.\n"
|
||||
"B) If required user input is missing (credential, ambiguous goal, "
|
||||
"site-specific detail), respond with an ASK_QUESTION instead. Do not "
|
||||
"retry the same repair again."
|
||||
)
|
||||
|
||||
POST_PARAMETER_BINDING_WARN_NUDGE = (
|
||||
"STOP — your last test run failed with a PARAMETER_BINDING_ERROR. "
|
||||
"This is an INTERNAL workflow configuration mismatch, not a site or "
|
||||
"selector problem.\n"
|
||||
"The workflow definition references a parameter (by Jinja key) that is "
|
||||
"not in the top-level workflow parameters list, or the list declares a "
|
||||
"parameter the blocks do not use.\n"
|
||||
"Do NOT retry with different selectors, URLs, or navigation changes — "
|
||||
"those will not help. Instead:\n"
|
||||
"1. Reconcile the workflow's top-level parameters with what the blocks "
|
||||
"actually reference via {{ parameters.<key> }}.\n"
|
||||
"2. Inline one-off literals rather than adding a parameter for each.\n"
|
||||
"3. Then call update_and_run_blocks again with a corrected YAML and, "
|
||||
"for any remaining parameters, concrete values passed via the "
|
||||
"`parameters` argument."
|
||||
)
|
||||
|
||||
POST_NON_RETRIABLE_NAV_ERROR_STOP_NUDGE = (
|
||||
"STOP — the target URL is unreachable and further retries cannot succeed. "
|
||||
"The navigation failed with a permanent error (DNS resolution, SSL/cert, "
|
||||
"or invalid URL). Do NOT retry and do NOT edit the workflow. Reply to the "
|
||||
"user now: state that the URL could not be reached, quote the exact error "
|
||||
"message from the last failure_reason, and ask them to verify the URL."
|
||||
)
|
||||
|
||||
POST_PARAMETER_BINDING_STOP_NUDGE = (
|
||||
"STOP — you have retried the same PARAMETER_BINDING_ERROR multiple times "
|
||||
"without reconciling the workflow configuration. Do NOT call "
|
||||
"update_and_run_blocks or run_blocks_and_collect_debug again until the "
|
||||
"workflow parameters list matches the block references.\n"
|
||||
"Choose ONE:\n"
|
||||
"A) Finalize now with a blocker explanation that names the specific "
|
||||
"parameter keys that are out of sync.\n"
|
||||
"B) If you need missing values from the user (credential, identifier) "
|
||||
"to decide what belongs in the parameters list, respond with an "
|
||||
"ASK_QUESTION instead. Do not resubmit a workflow that still has the "
|
||||
"same parameter-binding drift."
|
||||
)
|
||||
|
||||
POST_PER_TOOL_BUDGET_NUDGE = (
|
||||
"STOP — your last update_and_run_blocks call exceeded the per-tool-call "
|
||||
"time budget while still making progress. This is NOT a site failure or "
|
||||
"a wording problem — the chain you submitted is too long to complete in a "
|
||||
"single tool call.\n"
|
||||
"Do NOT retry the same chain. Do NOT change navigation_goal wording or "
|
||||
"selectors hoping it will run faster.\n"
|
||||
"1. Call get_run_results for the budgeted run. If it shows a navigation "
|
||||
"block was canceled or failed, do NOT run that same label again unchanged; "
|
||||
"split or replace it first.\n"
|
||||
"2. Shrink the requested block_labels list to the first 1-2 unverified "
|
||||
"blocks. The verified-prefix optimization will replay any earlier blocks "
|
||||
"from cached state without re-running the browser, so passing a smaller "
|
||||
"frontier is cheap.\n"
|
||||
"3. For page-state work, prefer a code or validation block to verify DOM "
|
||||
"state, active chips, checked controls, field values, or URL deltas. Use a "
|
||||
"navigation block only when missing state must be changed through the UI, "
|
||||
"and keep it to one atomic action.\n"
|
||||
"4. Test the smaller frontier. If it succeeds, extend by one block at a "
|
||||
"time on subsequent calls.\n"
|
||||
"5. If your workflow only has 1-2 blocks and one block is still hitting "
|
||||
"the budget, the single block is too ambitious — either narrow its scope, "
|
||||
"replace it with DOM/state verification plus smaller actions, or reply "
|
||||
"with a blocker explanation."
|
||||
)
|
||||
|
||||
POST_NO_WORKFLOW_DELIVERY_NUDGE = (
|
||||
"STOP — you are telling the user you created or are showing a workflow, "
|
||||
"but no workflow update tool has succeeded in this turn. The user will see "
|
||||
"an empty proposal. You MUST either call update_and_run_blocks with a real "
|
||||
"workflow and test it, or respond with ASK_QUESTION if required input is "
|
||||
'missing. Do NOT say "Here\'s the workflow" until there is an actual '
|
||||
"workflow proposal behind the response."
|
||||
)
|
||||
|
||||
_PROBABLE_SITE_BLOCK_STOP_NUDGE_PREFIX = (
|
||||
"STOP — the target site has failed to scrape on every attempt across "
|
||||
"multiple workflow shapes. Every run navigated successfully but the "
|
||||
'scraper could not read the page ("failed to load the website" / '
|
||||
'"page may have navigated unexpectedly"). This pattern indicates the '
|
||||
"site is either blocking automated access, genuinely unresponsive in "
|
||||
"this environment, or rendering content Skyvern cannot read reliably.\n"
|
||||
"Do NOT retry with another workflow variation. Do NOT call "
|
||||
"update_and_run_blocks or run_blocks_and_collect_debug again.\n"
|
||||
"Reply to the user now: state that the site could not be loaded after "
|
||||
"multiple attempts, quote the last failure_reason verbatim, keep the "
|
||||
"message concise, and ask "
|
||||
)
|
||||
|
||||
POST_PROBABLE_SITE_BLOCK_STOP_NUDGE = (
|
||||
_PROBABLE_SITE_BLOCK_STOP_NUDGE_PREFIX
|
||||
+ "whether to try a different URL, configure a proxy, or provide an alternate entry point."
|
||||
)
|
||||
|
||||
POST_ANTI_BOT_FAILED_TEST_NUDGE = (
|
||||
"STOP — your last test run failed due to an anti-bot/WAF block "
|
||||
"(Access Denied, Cloudflare, Akamai, etc.).\n"
|
||||
"IMPORTANT: An HTTP_REQUEST or navigation block from the SAME server IP "
|
||||
"will almost certainly receive the same block. Do NOT retry with:\n"
|
||||
"- A simple wait/delay block (timing does not fix IP bans)\n"
|
||||
"- A raw HTTP_REQUEST to the same URL (same IP = same block)\n"
|
||||
"Instead, try:\n"
|
||||
"1. Set proxy_location on the workflow to route through a different IP "
|
||||
"(use `RESIDENTIAL` by default, or a US-state value like `US-CA`/`US-NY`; "
|
||||
"do NOT use bare country codes like `US` — they are not valid "
|
||||
"ProxyLocation members).\n"
|
||||
"2. If you add anti-bot handling blocks, make them conditional on visible "
|
||||
"challenge evidence (for example Cloudflare, Access Denied, CAPTCHA, or "
|
||||
"verify-you-are-human text). Do NOT assume every run starts on a challenge "
|
||||
"page; normal, unblocked runs should proceed directly to the requested task.\n"
|
||||
"3. If still blocked, explain the specific anti-bot evidence observed "
|
||||
"(for example Cloudflare, CAPTCHA, verify-you-are-human text, Access "
|
||||
"Denied, or a blank Just-a-moment page); describe exactly what you tried; "
|
||||
"and ask whether to try a different proxy/location, entry URL, or "
|
||||
"alternate source.\n"
|
||||
"Do NOT resubmit the same workflow with trivial changes."
|
||||
)
|
||||
|
||||
POST_FORMAT_NUDGE = (
|
||||
"Your reply reads as a progress report, not a completed proposal. "
|
||||
"If you are not ready to finalize, emit ASK_QUESTION with a specific question. "
|
||||
"Otherwise, finish the workflow and present it as a completed proposal."
|
||||
)
|
||||
|
||||
# A REPLY matching any of these is almost certainly the agent leaking internal
|
||||
# iteration state instead of finalizing or asking a specific question.
|
||||
_PROGRESS_NARRATION_PATTERNS = [
|
||||
|
|
@ -389,8 +174,8 @@ def _probable_site_block_proxy_options(ctx: Any, *, include_whether: bool = True
|
|||
return f"whether to {options}" if include_whether else options
|
||||
|
||||
|
||||
def _probable_site_block_stop_nudge(ctx: Any) -> str:
|
||||
return _PROBABLE_SITE_BLOCK_STOP_NUDGE_PREFIX + _probable_site_block_proxy_options(ctx)
|
||||
def _probable_site_block_stop_nudge(ctx: Any, config: CopilotConfig | None = None) -> str:
|
||||
return _nudge(config, "post_probable_site_block_stop_prefix") + _probable_site_block_proxy_options(ctx)
|
||||
|
||||
|
||||
def _single_line_failure_reason(ctx: Any) -> str:
|
||||
|
|
@ -477,6 +262,12 @@ _ACTION_CATEGORIES: list[list[str]] = [
|
|||
_SEQUENTIAL_CONNECTORS = [" and then ", " then ", " after that ", " next ", " followed by ", " afterward "]
|
||||
|
||||
|
||||
def _nudge(config: CopilotConfig | None, key: str) -> str:
|
||||
if config is None:
|
||||
return DEFAULT_ENFORCEMENT_NUDGES[key]
|
||||
return config.nudge(key)
|
||||
|
||||
|
||||
def _goal_likely_needs_more_blocks(user_message: Any, block_count: int) -> bool:
|
||||
"""Return True when the goal likely requires more blocks than currently exist."""
|
||||
if block_count >= MIN_BLOCKS_FOR_AUTO_COMPLETE:
|
||||
|
|
@ -492,7 +283,7 @@ def _goal_likely_needs_more_blocks(user_message: Any, block_count: int) -> bool:
|
|||
return block_count < estimated_min_blocks
|
||||
|
||||
|
||||
def _response_coverage_nudge(ctx: Any, parsed: dict[str, Any]) -> str | None:
|
||||
def _response_coverage_nudge(ctx: Any, parsed: dict[str, Any], config: CopilotConfig | None = None) -> str | None:
|
||||
"""Peek at the model's final output and return a nudge for coverage gaps
|
||||
or progress-narration format. ASK_QUESTION is always let through so the
|
||||
agent can request missing credentials or disambiguation.
|
||||
|
|
@ -511,7 +302,7 @@ def _response_coverage_nudge(ctx: Any, parsed: dict[str, Any]) -> str | None:
|
|||
nudge_count = getattr(ctx, "no_workflow_nudge_count", 0)
|
||||
if nudge_count < MAX_NO_WORKFLOW_NUDGES:
|
||||
ctx.no_workflow_nudge_count = nudge_count + 1
|
||||
return POST_NO_WORKFLOW_DELIVERY_NUDGE
|
||||
return _nudge(config, "post_no_workflow_delivery")
|
||||
|
||||
workflow_tested_ok = (
|
||||
getattr(ctx, "last_test_ok", None) is True
|
||||
|
|
@ -527,13 +318,13 @@ def _response_coverage_nudge(ctx: Any, parsed: dict[str, Any]) -> str | None:
|
|||
nudge_count = getattr(ctx, "coverage_nudge_count", 0)
|
||||
if nudge_count < MAX_INTERMEDIATE_NUDGES:
|
||||
ctx.coverage_nudge_count = nudge_count + 1
|
||||
return POST_INTERMEDIATE_SUCCESS_NUDGE
|
||||
return _nudge(config, "post_intermediate_success")
|
||||
|
||||
if _is_progress_narration(parsed.get("user_response")):
|
||||
nudge_count = getattr(ctx, "format_nudge_count", 0)
|
||||
if nudge_count < MAX_FORMAT_NUDGES:
|
||||
ctx.format_nudge_count = nudge_count + 1
|
||||
return POST_FORMAT_NUDGE
|
||||
return _nudge(config, "post_format")
|
||||
|
||||
return None
|
||||
|
||||
|
|
@ -639,7 +430,7 @@ def _get_int(ctx: Any, name: str, default: int = 0) -> int:
|
|||
return value if isinstance(value, int) else default
|
||||
|
||||
|
||||
def _repeated_frontier_failure_nudge(ctx: Any) -> str | None:
|
||||
def _repeated_frontier_failure_nudge(ctx: Any, config: CopilotConfig | None = None) -> str | None:
|
||||
"""Emit each escalation level at most once per streak. The streak itself
|
||||
keeps climbing on further identical failures (incremented elsewhere by
|
||||
update_repeated_failure_state), so the stop nudge fires naturally on the
|
||||
|
|
@ -659,16 +450,26 @@ def _repeated_frontier_failure_nudge(ctx: Any) -> str | None:
|
|||
is_param_binding = top_category == "PARAMETER_BINDING_ERROR"
|
||||
|
||||
if streak >= REPEATED_FRONTIER_STREAK_STOP_AT and emitted < REPEATED_FRONTIER_STREAK_STOP_AT:
|
||||
return POST_PARAMETER_BINDING_STOP_NUDGE if is_param_binding else POST_REPEATED_FRONTIER_FAILURE_STOP_NUDGE
|
||||
return _nudge(
|
||||
config,
|
||||
"post_parameter_binding_stop" if is_param_binding else "post_repeated_frontier_failure_stop",
|
||||
)
|
||||
if streak >= REPEATED_FRONTIER_STREAK_ESCALATE_AT and emitted < REPEATED_FRONTIER_STREAK_ESCALATE_AT:
|
||||
return POST_PARAMETER_BINDING_WARN_NUDGE if is_param_binding else POST_REPEATED_FRONTIER_FAILURE_WARN_NUDGE
|
||||
return _nudge(
|
||||
config,
|
||||
"post_parameter_binding_warn" if is_param_binding else "post_repeated_frontier_failure_warn",
|
||||
)
|
||||
return None
|
||||
|
||||
|
||||
_STOP_LEVEL_FRONTIER_NUDGES = frozenset({POST_REPEATED_FRONTIER_FAILURE_STOP_NUDGE, POST_PARAMETER_BINDING_STOP_NUDGE})
|
||||
def _is_stop_level_frontier_nudge(nudge: str, config: CopilotConfig | None = None) -> bool:
|
||||
return nudge in {
|
||||
_nudge(config, "post_repeated_frontier_failure_stop"),
|
||||
_nudge(config, "post_parameter_binding_stop"),
|
||||
}
|
||||
|
||||
|
||||
def _non_retriable_nav_error_nudge(ctx: Any) -> tuple[str, str] | None:
|
||||
def _non_retriable_nav_error_nudge(ctx: Any, config: CopilotConfig | None = None) -> tuple[str, str] | None:
|
||||
"""Emit POST_NON_RETRIABLE_NAV_ERROR_STOP_NUDGE at most once per distinct
|
||||
non-retriable nav-error signature. Returns ``(nudge, signature)`` when it
|
||||
should fire, ``None`` otherwise. Signature normalization is shared with
|
||||
|
|
@ -681,15 +482,19 @@ def _non_retriable_nav_error_nudge(ctx: Any) -> tuple[str, str] | None:
|
|||
last_emitted = getattr(ctx, "non_retriable_nav_error_last_emitted_signature", None)
|
||||
if signature == last_emitted:
|
||||
return None
|
||||
return POST_NON_RETRIABLE_NAV_ERROR_STOP_NUDGE, signature
|
||||
return _nudge(config, "post_non_retriable_nav_error_stop"), signature
|
||||
|
||||
|
||||
def _check_enforcement(ctx: Any, result: RunResultStreaming | None = None) -> str | None:
|
||||
def _check_enforcement(
|
||||
ctx: Any,
|
||||
result: RunResultStreaming | None = None,
|
||||
config: CopilotConfig | None = None,
|
||||
) -> str | None:
|
||||
# Terminal failure-mode signals must pre-empt tool-call hygiene nudges.
|
||||
# A permanent navigation error (DNS / cert / SSL / invalid URL) cannot be
|
||||
# resolved by observing a prior navigate or by testing an updated
|
||||
# workflow against the same bad URL, so let it speak first.
|
||||
non_retriable = _non_retriable_nav_error_nudge(ctx)
|
||||
non_retriable = _non_retriable_nav_error_nudge(ctx, config)
|
||||
if non_retriable is not None:
|
||||
nudge_msg, signature = non_retriable
|
||||
ctx.non_retriable_nav_error_last_emitted_signature = signature
|
||||
|
|
@ -697,32 +502,32 @@ def _check_enforcement(ctx: Any, result: RunResultStreaming | None = None) -> st
|
|||
|
||||
if ctx.navigate_called and not ctx.observation_after_navigate and not ctx.navigate_enforcement_done:
|
||||
ctx.navigate_enforcement_done = True
|
||||
return POST_NAVIGATE_NUDGE
|
||||
return _nudge(config, "post_navigate")
|
||||
|
||||
if _needs_explore_without_workflow_nudge(ctx):
|
||||
ctx.explore_without_workflow_nudge_count += 1
|
||||
return POST_EXPLORE_WITHOUT_WORKFLOW_NUDGE
|
||||
return _nudge(config, "post_explore_without_workflow")
|
||||
|
||||
if (
|
||||
ctx.update_workflow_called
|
||||
and not ctx.test_after_update_done
|
||||
and getattr(ctx, "allow_untested_workflow_draft", False) is not True
|
||||
):
|
||||
return POST_UPDATE_NUDGE
|
||||
return _nudge(config, "post_update")
|
||||
|
||||
# A budget-trip is a structural problem (chain too long), not a
|
||||
# workflow-shape problem — emit the targeted "split the chain" advice
|
||||
# before the generic repeated-frontier and failed-test paths can fire.
|
||||
if _needs_per_tool_budget_nudge(ctx):
|
||||
ctx.per_tool_budget_nudge_count = _get_int(ctx, "per_tool_budget_nudge_count") + 1
|
||||
return POST_PER_TOOL_BUDGET_NUDGE
|
||||
return _nudge(config, "post_per_tool_budget")
|
||||
|
||||
repeated_frontier_nudge = _repeated_frontier_failure_nudge(ctx)
|
||||
repeated_frontier_nudge = _repeated_frontier_failure_nudge(ctx, config)
|
||||
if repeated_frontier_nudge is not None:
|
||||
# Latch the emitted level so each escalation fires at most once per streak.
|
||||
ctx.repeated_failure_nudge_emitted_at_streak = (
|
||||
REPEATED_FRONTIER_STREAK_STOP_AT
|
||||
if repeated_frontier_nudge in _STOP_LEVEL_FRONTIER_NUDGES
|
||||
if _is_stop_level_frontier_nudge(repeated_frontier_nudge, config)
|
||||
else REPEATED_FRONTIER_STREAK_ESCALATE_AT
|
||||
)
|
||||
return repeated_frontier_nudge
|
||||
|
|
@ -731,31 +536,31 @@ def _check_enforcement(ctx: Any, result: RunResultStreaming | None = None) -> st
|
|||
# resets it on every new run; if the agent ignores the nudge and answers
|
||||
# without rerunning, we want _check_enforcement to re-emit the nudge.
|
||||
if _needs_repeated_null_data_nudge(ctx):
|
||||
return POST_REPEATED_NULL_DATA_NUDGE
|
||||
return _nudge(config, "post_repeated_null_data")
|
||||
|
||||
if _needs_suspicious_success_nudge(ctx):
|
||||
ctx.suspicious_success_nudge_count = getattr(ctx, "suspicious_success_nudge_count", 0) + 1
|
||||
return POST_SUSPICIOUS_SUCCESS_NUDGE
|
||||
return _nudge(config, "post_suspicious_success")
|
||||
|
||||
# Checked before the generic failed-test nudge so a scrape-wall streak
|
||||
# emits the specific STOP text and does not also consume a
|
||||
# failed_test_nudge_count slot.
|
||||
if _needs_probable_site_block_stop_nudge(ctx):
|
||||
ctx.probable_site_block_stop_nudge_count = getattr(ctx, "probable_site_block_stop_nudge_count", 0) + 1
|
||||
return _probable_site_block_stop_nudge(ctx)
|
||||
return _probable_site_block_stop_nudge(ctx, config)
|
||||
|
||||
if _needs_failed_test_nudge(ctx):
|
||||
ctx.failed_test_nudge_count += 1
|
||||
if getattr(ctx, "last_test_anti_bot", None):
|
||||
return POST_ANTI_BOT_FAILED_TEST_NUDGE
|
||||
return POST_FAILED_TEST_NUDGE
|
||||
return _nudge(config, "post_anti_bot_failed_test")
|
||||
return _nudge(config, "post_failed_test")
|
||||
|
||||
# Response-time gate: peek at the model's final output to tell ASK_QUESTION
|
||||
# (always allowed) from a REPLY with a coverage gap or progress-narration.
|
||||
# Only runs when no state-based nudge fired.
|
||||
if result is not None:
|
||||
parsed = parse_final_response(extract_final_text(result))
|
||||
return _response_coverage_nudge(ctx, parsed)
|
||||
return _response_coverage_nudge(ctx, parsed, config)
|
||||
|
||||
return None
|
||||
|
||||
|
|
@ -1051,9 +856,36 @@ _NUDGE_TYPE_BY_MESSAGE: dict[str, str] = {
|
|||
}
|
||||
|
||||
|
||||
def _nudge_type_for_log(nudge: str) -> str:
|
||||
if nudge.startswith(_PROBABLE_SITE_BLOCK_STOP_NUDGE_PREFIX):
|
||||
_NUDGE_TYPE_BY_KEY: dict[str, str] = {
|
||||
"post_update": "post_update",
|
||||
"post_navigate": "post_navigate",
|
||||
"post_explore_without_workflow": "explore_without_workflow",
|
||||
"post_suspicious_success": "suspicious_success",
|
||||
"post_repeated_null_data": "repeated_null_data",
|
||||
"post_repeated_frontier_failure_warn": "repeated_frontier_failure_warn",
|
||||
"post_repeated_frontier_failure_stop": "repeated_frontier_failure_stop",
|
||||
"post_non_retriable_nav_error_stop": "non_retriable_nav_error_stop",
|
||||
"post_parameter_binding_warn": "parameter_binding_warn",
|
||||
"post_parameter_binding_stop": "parameter_binding_stop",
|
||||
"post_anti_bot_failed_test": "anti_bot_block",
|
||||
"post_probable_site_block_stop": "probable_site_block_stop",
|
||||
"post_probable_site_block_stop_prefix": "probable_site_block_stop",
|
||||
"post_per_tool_budget": "per_tool_budget_split",
|
||||
"post_no_workflow_delivery": "no_workflow_delivery",
|
||||
"post_failed_test": "post_failed_test",
|
||||
"screenshot_dropped": "screenshot_dropped_on_recovery",
|
||||
"post_intermediate_success": "intermediate_success",
|
||||
"post_format": "format",
|
||||
}
|
||||
|
||||
|
||||
def _nudge_type_for_log(nudge: str, config: CopilotConfig | None = None) -> str:
|
||||
nudge_by_key = config.enforcement_nudges if config is not None else DEFAULT_ENFORCEMENT_NUDGES
|
||||
if nudge.startswith(nudge_by_key["post_probable_site_block_stop_prefix"]):
|
||||
return "probable_site_block_stop"
|
||||
for key, value in nudge_by_key.items():
|
||||
if value == nudge:
|
||||
return _NUDGE_TYPE_BY_KEY.get(key, key)
|
||||
return _NUDGE_TYPE_BY_MESSAGE.get(nudge, "intermediate_success")
|
||||
|
||||
|
||||
|
|
@ -1224,6 +1056,7 @@ async def run_with_enforcement(
|
|||
) -> RunResultStreaming:
|
||||
"""Run agent with enforcement nudges, preserving conversation history."""
|
||||
session = runner_kwargs.pop("session", None)
|
||||
copilot_config = runner_kwargs.pop("copilot_config", None) or CopilotConfig()
|
||||
current_input: str | list = initial_input
|
||||
start_time = time.monotonic()
|
||||
ctx.copilot_run_start_monotonic = start_time
|
||||
|
|
@ -1250,8 +1083,12 @@ async def run_with_enforcement(
|
|||
if isinstance(current_input, list):
|
||||
est = estimate_tokens(current_input)
|
||||
LOG.info("Token estimate before model call", tokens=est, iteration=iteration)
|
||||
if est > TOKEN_BUDGET:
|
||||
LOG.warning("Token estimate exceeds budget, aggressively pruning", tokens=est, budget=TOKEN_BUDGET)
|
||||
if est > copilot_config.token_budget:
|
||||
LOG.warning(
|
||||
"Token estimate exceeds budget, aggressively pruning",
|
||||
tokens=est,
|
||||
budget=copilot_config.token_budget,
|
||||
)
|
||||
current_input = aggressive_prune(current_input)
|
||||
|
||||
tracked_stream = _SendTrackingStream(stream)
|
||||
|
|
@ -1300,7 +1137,7 @@ async def run_with_enforcement(
|
|||
if images_stripped:
|
||||
# The agent could otherwise reason about the page from
|
||||
# memory on the next turn; warn it explicitly.
|
||||
pending_recovery_nudge = SCREENSHOT_DROPPED_NUDGE
|
||||
pending_recovery_nudge = _nudge(copilot_config, "screenshot_dropped")
|
||||
tracked_stream = _SendTrackingStream(stream)
|
||||
try:
|
||||
result = await _run_streamed_with_deadline(
|
||||
|
|
@ -1336,13 +1173,13 @@ async def run_with_enforcement(
|
|||
nudge: str | None = pending_recovery_nudge
|
||||
pending_recovery_nudge = None
|
||||
else:
|
||||
nudge = _check_enforcement(ctx, result)
|
||||
nudge = _check_enforcement(ctx, result, copilot_config)
|
||||
if nudge is None:
|
||||
_consume_pending_screenshots(ctx)
|
||||
_maybe_raise_non_retriable_nav(ctx)
|
||||
return result
|
||||
|
||||
if nudge == POST_UPDATE_NUDGE:
|
||||
if nudge == _nudge(copilot_config, "post_update"):
|
||||
if ctx.post_update_nudge_count >= MAX_POST_UPDATE_NUDGES:
|
||||
LOG.warning(
|
||||
"Enforcement exhausted post-update nudges, allowing response",
|
||||
|
|
@ -1353,7 +1190,7 @@ async def run_with_enforcement(
|
|||
return result
|
||||
ctx.post_update_nudge_count += 1
|
||||
|
||||
nudge_type = _nudge_type_for_log(nudge)
|
||||
nudge_type = _nudge_type_for_log(nudge, copilot_config)
|
||||
LOG.info("Enforcement nudge", nudge_type=nudge_type, iteration=iteration)
|
||||
|
||||
# OpenAI rejects images in tool messages, so a queued post-run
|
||||
|
|
|
|||
|
|
@ -27,9 +27,11 @@ from skyvern.config import settings
|
|||
from skyvern.forge.sdk.api.llm.config_registry import LLMConfigRegistry
|
||||
from skyvern.forge.sdk.api.llm.exceptions import InvalidLLMConfigError
|
||||
from skyvern.forge.sdk.api.llm.litellm_transport import configure_litellm_transport
|
||||
from skyvern.forge.sdk.copilot.config import CopilotConfig
|
||||
from skyvern.forge.sdk.copilot.session_factory import (
|
||||
copilot_call_model_input_filter,
|
||||
copilot_session_input_callback,
|
||||
make_copilot_call_model_input_filter,
|
||||
)
|
||||
from skyvern.forge.sdk.copilot.tracing_setup import is_tracing_enabled
|
||||
from skyvern.schemas.llm import LLMConfig, LLMRouterConfig
|
||||
|
|
@ -121,14 +123,19 @@ def _degrade_router_to_direct(llm_key: str, config: LLMRouterConfig) -> LLMConfi
|
|||
)
|
||||
|
||||
|
||||
def resolve_model_config(llm_api_handler: Any) -> tuple[str, RunConfig, str, bool]:
|
||||
def resolve_model_config(
|
||||
llm_api_handler: Any,
|
||||
*,
|
||||
copilot_config: CopilotConfig | None = None,
|
||||
llm_key_override: str | None = None,
|
||||
) -> tuple[str, RunConfig, str, bool]:
|
||||
"""Map Skyvern llm_key to OpenAI Agents SDK model string + RunConfig.
|
||||
|
||||
Returns (model_name, run_config, llm_key, supports_vision).
|
||||
"""
|
||||
configure_litellm_transport()
|
||||
|
||||
llm_key = getattr(llm_api_handler, "llm_key", None) or settings.LLM_KEY
|
||||
llm_key = llm_key_override or getattr(llm_api_handler, "llm_key", None) or settings.LLM_KEY
|
||||
config = LLMConfigRegistry.get_config(llm_key)
|
||||
|
||||
if isinstance(config, LLMRouterConfig):
|
||||
|
|
@ -201,7 +208,11 @@ def resolve_model_config(llm_api_handler: Any) -> tuple[str, RunConfig, str, boo
|
|||
model_settings=model_settings,
|
||||
tracing_disabled=not is_tracing_enabled(),
|
||||
session_input_callback=copilot_session_input_callback,
|
||||
call_model_input_filter=copilot_call_model_input_filter,
|
||||
call_model_input_filter=(
|
||||
make_copilot_call_model_input_filter(copilot_config.token_budget)
|
||||
if copilot_config is not None
|
||||
else copilot_call_model_input_filter
|
||||
),
|
||||
)
|
||||
|
||||
return config.model_name, run_config, llm_key, config.supports_vision
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@
|
|||
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Callable
|
||||
from typing import Any
|
||||
|
||||
import structlog
|
||||
|
|
@ -146,7 +147,18 @@ def copilot_session_input_callback(
|
|||
return [history_items[0]] + pruned_middle + list(recent) + list(new_items)
|
||||
|
||||
|
||||
def make_copilot_call_model_input_filter(token_budget: int) -> Callable[[CallModelData[Any]], ModelInputData]:
|
||||
def _filter(data: CallModelData[Any]) -> ModelInputData:
|
||||
return _copilot_call_model_input_filter(data, token_budget=token_budget)
|
||||
|
||||
return _filter
|
||||
|
||||
|
||||
def copilot_call_model_input_filter(data: CallModelData[Any]) -> ModelInputData:
|
||||
return _copilot_call_model_input_filter(data, token_budget=TOKEN_BUDGET)
|
||||
|
||||
|
||||
def _copilot_call_model_input_filter(data: CallModelData[Any], *, token_budget: int) -> ModelInputData:
|
||||
"""Token-budget enforcement applied just before each model call.
|
||||
|
||||
Graduated pruning:
|
||||
|
|
@ -173,7 +185,7 @@ def copilot_call_model_input_filter(data: CallModelData[Any]) -> ModelInputData:
|
|||
items = _compact_tool_items(items)
|
||||
|
||||
est = estimate_tokens(items)
|
||||
if est <= TOKEN_BUDGET:
|
||||
if est <= token_budget:
|
||||
LOG.info("Within budget after tool trim", tokens=est)
|
||||
return ModelInputData(input=items, instructions=model_data.instructions)
|
||||
|
||||
|
|
@ -187,7 +199,7 @@ def copilot_call_model_input_filter(data: CallModelData[Any]) -> ModelInputData:
|
|||
]
|
||||
|
||||
est = estimate_tokens(items)
|
||||
if est <= TOKEN_BUDGET:
|
||||
if est <= token_budget:
|
||||
LOG.info("Within budget after screenshot drop", tokens=est)
|
||||
return ModelInputData(input=items, instructions=model_data.instructions)
|
||||
|
||||
|
|
@ -195,12 +207,12 @@ def copilot_call_model_input_filter(data: CallModelData[Any]) -> ModelInputData:
|
|||
items = [_truncate_tool_output(item, TOOL_OUTPUT_TRUNCATE_EMERGENCY) for item in items]
|
||||
|
||||
est = estimate_tokens(items)
|
||||
if est <= TOKEN_BUDGET:
|
||||
if est <= token_budget:
|
||||
LOG.info("Within budget after emergency truncation", tokens=est)
|
||||
return ModelInputData(input=items, instructions=model_data.instructions)
|
||||
|
||||
# Layer 4: Aggressive prune as last resort
|
||||
LOG.warning("Aggressive prune needed", tokens=est, budget=TOKEN_BUDGET)
|
||||
LOG.warning("Aggressive prune needed", tokens=est, budget=token_budget)
|
||||
items = aggressive_prune(items)
|
||||
|
||||
est = estimate_tokens(items)
|
||||
|
|
|
|||
|
|
@ -3645,6 +3645,11 @@ async def run_blocks_tool(
|
|||
The workflow must be saved before running blocks.
|
||||
Block labels must match labels in the saved workflow.
|
||||
|
||||
For a diagnostic / observational complaint about the current workflow,
|
||||
this tool is not the first response. Follow the system prompt's
|
||||
inspect-and-clarify path first: inspect current workflow context and
|
||||
existing run evidence before deciding whether a fresh run is needed.
|
||||
|
||||
Pass runtime values for workflow parameters via the `parameters` dict —
|
||||
keys must match the workflow parameter `key` field. When the user has
|
||||
supplied concrete non-secret values in their message (names, emails, IDs),
|
||||
|
|
@ -3741,6 +3746,11 @@ async def update_and_run_blocks_tool(
|
|||
Use this instead of calling update_workflow and run_blocks_and_collect_debug separately.
|
||||
The workflow must validate successfully before blocks are run.
|
||||
|
||||
For a diagnostic / observational complaint about the current workflow,
|
||||
this tool is not the first response. Follow the system prompt's
|
||||
inspect-and-clarify path first, and only update/run when the user asked
|
||||
for an edit or the inspected evidence makes the correction clear.
|
||||
|
||||
Pass runtime values for workflow parameters via the `parameters` dict —
|
||||
keys must match the workflow parameter `key` field. When the user has
|
||||
supplied concrete non-secret values in their message (names, emails, IDs),
|
||||
|
|
|
|||
|
|
@ -146,6 +146,8 @@ from skyvern.webeye.actions.actions import Action
|
|||
|
||||
LOG = structlog.get_logger()
|
||||
|
||||
FORCE_TASK_V1_MAX_STEPS = 25
|
||||
|
||||
_create_from_prompt_adapter: TypeAdapter[CreateFromPromptRequest] = TypeAdapter(CreateFromPromptRequest)
|
||||
|
||||
|
||||
|
|
@ -188,6 +190,33 @@ async def run_task(
|
|||
await PermissionCheckerFactory.get_instance().check(current_org, browser_session_id=run_request.browser_session_id)
|
||||
await app.RATE_LIMITER.rate_limit_submit_run(current_org.organization_id)
|
||||
|
||||
if (
|
||||
run_request.engine == RunEngine.skyvern_v2
|
||||
and not run_request.publish_workflow
|
||||
and await app.EXPERIMENTATION_PROVIDER.is_feature_enabled_cached(
|
||||
"FORCE_TASK_V1",
|
||||
current_org.organization_id,
|
||||
properties={"organization_id": current_org.organization_id},
|
||||
)
|
||||
):
|
||||
cap = FORCE_TASK_V1_MAX_STEPS
|
||||
if current_org.max_steps_per_run is not None:
|
||||
cap = min(cap, current_org.max_steps_per_run)
|
||||
log_extra: dict[str, Any] = {}
|
||||
if run_request.run_with:
|
||||
log_extra["dropped_run_with"] = run_request.run_with
|
||||
LOG.info(
|
||||
"FORCE_TASK_V1 flag set; routing to v1 engine",
|
||||
organization_id=current_org.organization_id,
|
||||
requested_max_steps=run_request.max_steps,
|
||||
org_max_steps_per_run=current_org.max_steps_per_run,
|
||||
effective_cap=cap,
|
||||
**log_extra,
|
||||
)
|
||||
run_request.engine = RunEngine.skyvern_v1
|
||||
if not run_request.max_steps or run_request.max_steps > cap:
|
||||
run_request.max_steps = cap
|
||||
|
||||
if run_request.engine in CUA_ENGINES or run_request.engine == RunEngine.skyvern_v1:
|
||||
# create task v1
|
||||
# if there's no url, call task generation first to generate the url, data schema if any
|
||||
|
|
|
|||
|
|
@ -23,6 +23,7 @@ from skyvern.forge.sdk.api.llm.exceptions import LLMProviderError
|
|||
from skyvern.forge.sdk.artifact.models import Artifact, ArtifactType
|
||||
from skyvern.forge.sdk.copilot.agent import run_copilot_agent
|
||||
from skyvern.forge.sdk.copilot.attribution import is_copilot_born_initial_write
|
||||
from skyvern.forge.sdk.copilot.config import CopilotConfig
|
||||
from skyvern.forge.sdk.copilot.context import AgentResult
|
||||
from skyvern.forge.sdk.copilot.output_utils import truncate_output
|
||||
from skyvern.forge.sdk.core import skyvern_context
|
||||
|
|
@ -1330,7 +1331,7 @@ async def _new_copilot_chat_post(
|
|||
)
|
||||
return
|
||||
|
||||
security_rules = app.AGENT_FUNCTION.get_copilot_security_rules()
|
||||
copilot_config = app.AGENT_FUNCTION.get_copilot_config() or CopilotConfig()
|
||||
|
||||
# Spawn the cancel watcher only after the chat row exists; cancels
|
||||
# that land during pre-agent setup are not user-cancellable
|
||||
|
|
@ -1360,7 +1361,7 @@ async def _new_copilot_chat_post(
|
|||
debug_run_info_text=debug_run_info_text,
|
||||
llm_api_handler=llm_api_handler,
|
||||
api_key=api_key,
|
||||
security_rules=security_rules,
|
||||
config=copilot_config,
|
||||
)
|
||||
|
||||
if getattr(agent_result, "cancelled", False):
|
||||
|
|
|
|||
|
|
@ -5086,6 +5086,14 @@ class WorkflowService:
|
|||
organization_id=workflow_run.organization_id,
|
||||
child_workflow_run_ids=child_workflow_run_ids,
|
||||
)
|
||||
try:
|
||||
await app.AGENT_FUNCTION.release_proxy_session_for_owner(workflow_run.workflow_run_id)
|
||||
except Exception:
|
||||
LOG.warning(
|
||||
"Failed to release proxy session for workflow run",
|
||||
exc_info=True,
|
||||
workflow_run_id=workflow_run.workflow_run_id,
|
||||
)
|
||||
if browser_state:
|
||||
await self.persist_video_data(
|
||||
browser_state, workflow, workflow_run, close_browser_on_completion=close_browser_on_completion
|
||||
|
|
|
|||
|
|
@ -3,9 +3,9 @@ from __future__ import annotations
|
|||
# ruff: noqa: E402
|
||||
from typing import Any, Callable
|
||||
|
||||
from skyvern.exceptions import require_server_extra_modules
|
||||
from skyvern.exceptions import require_local_extra_modules
|
||||
|
||||
require_server_extra_modules("skyvern.library.ai_locator")
|
||||
require_local_extra_modules("skyvern.library.ai_locator")
|
||||
|
||||
from playwright.async_api import Locator, Page
|
||||
|
||||
|
|
|
|||
|
|
@ -225,9 +225,9 @@ class Skyvern(AsyncSkyvern):
|
|||
try:
|
||||
from skyvern.library.embedded_server_factory import create_embedded_server # noqa: PLC0415
|
||||
except ImportError as exc:
|
||||
from skyvern.exceptions import raise_server_extra_required # noqa: PLC0415
|
||||
from skyvern.exceptions import raise_local_extra_required # noqa: PLC0415
|
||||
|
||||
raise_server_extra_required("Skyvern.local()", exc)
|
||||
raise_local_extra_required("Skyvern.local()", exc)
|
||||
|
||||
from skyvern.utils.env_paths import resolve_backend_env_path # noqa: PLC0415
|
||||
|
||||
|
|
@ -652,14 +652,14 @@ class Skyvern(AsyncSkyvern):
|
|||
|
||||
async def _get_playwright(self) -> Playwright:
|
||||
if self._playwright is None:
|
||||
from skyvern.exceptions import raise_server_extra_required # noqa: PLC0415
|
||||
from skyvern.exceptions import raise_local_extra_required # noqa: PLC0415
|
||||
|
||||
# Cloud-API-only SDK users can instantiate Skyvern without server extras,
|
||||
# so browser startup keeps its own extra hint at the Playwright boundary.
|
||||
try:
|
||||
from playwright.async_api import async_playwright # noqa: PLC0415
|
||||
except ImportError as exc:
|
||||
raise_server_extra_required("Browser APIs", exc)
|
||||
raise_local_extra_required("Browser APIs", exc)
|
||||
|
||||
self._playwright = await async_playwright().start()
|
||||
return self._playwright
|
||||
|
|
|
|||
|
|
@ -1,9 +1,9 @@
|
|||
# ruff: noqa: E402
|
||||
from typing import TYPE_CHECKING, Any
|
||||
|
||||
from skyvern.exceptions import require_server_extra_modules
|
||||
from skyvern.exceptions import require_local_extra_modules
|
||||
|
||||
require_server_extra_modules("skyvern.library.skyvern_browser")
|
||||
require_local_extra_modules("skyvern.library.skyvern_browser")
|
||||
|
||||
from playwright.async_api import BrowserContext, Page
|
||||
|
||||
|
|
|
|||
|
|
@ -1,9 +1,9 @@
|
|||
# ruff: noqa: E402
|
||||
from typing import TYPE_CHECKING, Any
|
||||
|
||||
from skyvern.exceptions import require_server_extra_modules
|
||||
from skyvern.exceptions import require_local_extra_modules
|
||||
|
||||
require_server_extra_modules("skyvern.library.skyvern_browser_page")
|
||||
require_local_extra_modules("skyvern.library.skyvern_browser_page")
|
||||
|
||||
from playwright.async_api import Frame, Page
|
||||
|
||||
|
|
|
|||
|
|
@ -4,9 +4,9 @@ from typing import Any, Literal, overload
|
|||
|
||||
import structlog
|
||||
|
||||
from skyvern.exceptions import require_server_extra_modules
|
||||
from skyvern.exceptions import require_local_extra_modules
|
||||
|
||||
require_server_extra_modules("skyvern.library.skyvern_browser_page_agent")
|
||||
require_local_extra_modules("skyvern.library.skyvern_browser_page_agent")
|
||||
|
||||
from playwright.async_api import Page # noqa: E402
|
||||
|
||||
|
|
|
|||
|
|
@ -3,9 +3,9 @@ from typing import TYPE_CHECKING, Any
|
|||
|
||||
import structlog
|
||||
|
||||
from skyvern.exceptions import require_server_extra_modules
|
||||
from skyvern.exceptions import require_local_extra_modules
|
||||
|
||||
require_server_extra_modules("skyvern.library.skyvern_browser_page_ai")
|
||||
require_local_extra_modules("skyvern.library.skyvern_browser_page_ai")
|
||||
|
||||
from playwright.async_api import Page
|
||||
|
||||
|
|
|
|||
|
|
@ -1,9 +1,9 @@
|
|||
# ruff: noqa: E402
|
||||
from typing import Any, Pattern
|
||||
|
||||
from skyvern.exceptions import require_server_extra_modules
|
||||
from skyvern.exceptions import require_local_extra_modules
|
||||
|
||||
require_server_extra_modules("skyvern.library.skyvern_locator")
|
||||
require_local_extra_modules("skyvern.library.skyvern_locator")
|
||||
|
||||
from playwright.async_api import Locator
|
||||
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ from unittest.mock import AsyncMock, MagicMock
|
|||
import pytest
|
||||
|
||||
from skyvern.forge.sdk.copilot import agent as agent_module
|
||||
from skyvern.forge.sdk.copilot.config import CopilotConfig
|
||||
|
||||
|
||||
def _ctx(**overrides):
|
||||
|
|
@ -1147,3 +1148,104 @@ class TestResponseTypeClassificationRuleReachesAgent:
|
|||
assert "explicitly asks for an untested draft" in prompt
|
||||
assert "workflow was drafted without testing as requested" in prompt
|
||||
assert prompt.index("RESPONSE-TYPE CLASSIFICATION") < prompt.index("**Option 1: Reply to the user**")
|
||||
|
||||
|
||||
class TestCopilotConfig:
|
||||
def test_system_prompt_uses_custom_security_rules(self) -> None:
|
||||
prompt = agent_module._build_system_prompt(
|
||||
tool_usage_guide="",
|
||||
config=CopilotConfig(security_rules="CUSTOM SECURITY RULE"),
|
||||
)
|
||||
|
||||
assert "CUSTOM SECURITY RULE" in prompt
|
||||
|
||||
def test_retriable_llm_error_detects_openai_rate_limit(self) -> None:
|
||||
class FakeRateLimitError(Exception):
|
||||
pass
|
||||
|
||||
FakeRateLimitError.__module__ = "openai"
|
||||
|
||||
assert agent_module._is_retriable_llm_error(FakeRateLimitError("rate limit"))
|
||||
|
||||
def test_fallback_key_skips_missing_or_same_key(self) -> None:
|
||||
assert agent_module._fallback_llm_key(CopilotConfig(fallback_llm_key=None), "PRIMARY") is None
|
||||
assert agent_module._fallback_llm_key(CopilotConfig(fallback_llm_key="PRIMARY"), "PRIMARY") is None
|
||||
assert agent_module._fallback_llm_key(CopilotConfig(fallback_llm_key="SECONDARY"), "PRIMARY") == "SECONDARY"
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_run_copilot_agent_retries_retriable_failure_with_fallback(
|
||||
self, monkeypatch: pytest.MonkeyPatch
|
||||
) -> None:
|
||||
class FakeRateLimitError(Exception):
|
||||
pass
|
||||
|
||||
FakeRateLimitError.__module__ = "openai"
|
||||
|
||||
async def fake_feasibility_gate(**_kwargs):
|
||||
return SimpleNamespace(verdict="proceed", question=None, rationale=None)
|
||||
|
||||
class FakeMCPServerManager:
|
||||
def __init__(self, servers):
|
||||
self.active_servers = servers
|
||||
|
||||
async def __aenter__(self):
|
||||
return self
|
||||
|
||||
async def __aexit__(self, exc_type, exc, tb):
|
||||
return None
|
||||
|
||||
resolved_keys: list[str] = []
|
||||
|
||||
def fake_resolve_model_config(_handler, *, copilot_config=None, llm_key_override=None):
|
||||
del copilot_config
|
||||
key = llm_key_override or "PRIMARY"
|
||||
resolved_keys.append(key)
|
||||
return f"model-{key}", object(), key, True
|
||||
|
||||
run_with_enforcement = AsyncMock(
|
||||
side_effect=[
|
||||
FakeRateLimitError("rate limit"),
|
||||
_fake_run_result({"type": "REPLY", "user_response": "ok", "goal_reached": True}),
|
||||
]
|
||||
)
|
||||
|
||||
monkeypatch.setattr(
|
||||
"skyvern.forge.sdk.copilot.feasibility_gate.run_feasibility_gate",
|
||||
fake_feasibility_gate,
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
"skyvern.forge.sdk.copilot.agent._resolve_live_browser_session_id",
|
||||
AsyncMock(return_value=None),
|
||||
)
|
||||
monkeypatch.setattr("agents.mcp.MCPServerManager", FakeMCPServerManager)
|
||||
monkeypatch.setattr(
|
||||
"skyvern.forge.sdk.copilot.model_resolver.resolve_model_config",
|
||||
fake_resolve_model_config,
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
"skyvern.forge.sdk.copilot.enforcement.run_with_enforcement",
|
||||
run_with_enforcement,
|
||||
)
|
||||
|
||||
result = await agent_module.run_copilot_agent(
|
||||
stream=MagicMock(),
|
||||
organization_id="org-1",
|
||||
chat_request=SimpleNamespace(
|
||||
message="build it",
|
||||
workflow_id="wf-1",
|
||||
workflow_permanent_id="wfp-1",
|
||||
workflow_copilot_chat_id="chat-1",
|
||||
workflow_yaml="",
|
||||
browser_session_id=None,
|
||||
),
|
||||
chat_history=[],
|
||||
global_llm_context=None,
|
||||
debug_run_info_text="",
|
||||
llm_api_handler=SimpleNamespace(llm_key="PRIMARY"),
|
||||
api_key="sk-test",
|
||||
config=CopilotConfig(fallback_llm_key="SECONDARY"),
|
||||
)
|
||||
|
||||
assert result.user_response == "ok"
|
||||
assert resolved_keys == ["PRIMARY", "SECONDARY"]
|
||||
assert run_with_enforcement.await_count == 2
|
||||
|
|
|
|||
|
|
@ -149,6 +149,36 @@ class TestModelResolver:
|
|||
assert run_config.model_settings.temperature == 0.5
|
||||
assert run_config.model_settings.max_tokens == 4096
|
||||
|
||||
def test_llm_key_override_wins_for_fallback_attempt(self, monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
from skyvern.schemas.llm import LLMConfig
|
||||
|
||||
seen_keys: list[str] = []
|
||||
|
||||
def fake_get_config(key: str) -> LLMConfig:
|
||||
seen_keys.append(key)
|
||||
return LLMConfig(
|
||||
model_name=f"openai/{key.lower()}",
|
||||
required_env_vars=[],
|
||||
supports_vision=True,
|
||||
add_assistant_prefix=False,
|
||||
)
|
||||
|
||||
monkeypatch.setattr(
|
||||
"skyvern.forge.sdk.copilot.model_resolver.LLMConfigRegistry.get_config",
|
||||
fake_get_config,
|
||||
)
|
||||
|
||||
from skyvern.forge.sdk.copilot.model_resolver import resolve_model_config
|
||||
|
||||
handler = MagicMock()
|
||||
handler.llm_key = "PRIMARY_KEY"
|
||||
|
||||
model_name, _, llm_key, _ = resolve_model_config(handler, llm_key_override="FALLBACK_KEY")
|
||||
|
||||
assert model_name == "openai/fallback_key"
|
||||
assert llm_key == "FALLBACK_KEY"
|
||||
assert seen_keys == ["FALLBACK_KEY"]
|
||||
|
||||
def test_maps_basic_config_with_tracing_enabled(self, monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
from skyvern.schemas.llm import LLMConfig
|
||||
|
||||
|
|
|
|||
|
|
@ -9,7 +9,9 @@ from skyvern.exceptions import (
|
|||
SkyvernHTTPException,
|
||||
UnknownErrorWhileCreatingBrowserContext,
|
||||
get_user_facing_exception_message,
|
||||
raise_local_extra_required,
|
||||
raise_server_extra_required,
|
||||
require_local_extra_modules,
|
||||
require_server_extra_modules,
|
||||
)
|
||||
|
||||
|
|
@ -72,7 +74,7 @@ def test_raise_server_extra_required_translates_when_server_extra_missing(monkey
|
|||
)
|
||||
missing = ModuleNotFoundError("No module named 'starlette_context'", name="starlette_context")
|
||||
|
||||
with pytest.raises(SkyvernExtraNotInstalled, match=r"pip install skyvern\[server\]"):
|
||||
with pytest.raises(SkyvernExtraNotInstalled, match=r'pip install "skyvern\[server\]"'):
|
||||
raise_server_extra_required("skyvern.library.skyvern_browser", missing)
|
||||
|
||||
|
||||
|
|
@ -83,10 +85,28 @@ def test_raise_server_extra_required_translates_missing_server_marker(monkeypatc
|
|||
)
|
||||
missing = ModuleNotFoundError("No module named 'playwright'", name="playwright")
|
||||
|
||||
with pytest.raises(SkyvernExtraNotInstalled, match=r"pip install skyvern\[server\]"):
|
||||
with pytest.raises(SkyvernExtraNotInstalled, match=r'pip install "skyvern\[server\]"'):
|
||||
raise_server_extra_required("skyvern.library.skyvern_browser", missing)
|
||||
|
||||
|
||||
def test_raise_local_extra_required_translates_missing_local_marker(monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
monkeypatch.setattr(
|
||||
"skyvern.exceptions.find_spec",
|
||||
lambda module_name: None if module_name == "playwright" else object(),
|
||||
)
|
||||
missing = ModuleNotFoundError("No module named 'playwright'", name="playwright")
|
||||
|
||||
with pytest.raises(SkyvernExtraNotInstalled, match=r'pip install "skyvern\[local\]"'):
|
||||
raise_local_extra_required("Browser APIs", missing)
|
||||
|
||||
|
||||
def test_raise_local_extra_required_rewrites_nested_server_guard() -> None:
|
||||
missing = SkyvernExtraNotInstalled("skyvern.forge.api_app", extra="server")
|
||||
|
||||
with pytest.raises(SkyvernExtraNotInstalled, match=r'pip install "skyvern\[local\]"'):
|
||||
raise_local_extra_required("Skyvern.local()", missing)
|
||||
|
||||
|
||||
def test_raise_server_extra_required_preserves_installed_marker_submodule_failure(
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
) -> None:
|
||||
|
|
@ -147,7 +167,7 @@ def test_require_server_extra_modules_requires_server_sentinels(monkeypatch: pyt
|
|||
lambda module_name: None if module_name == "sqlalchemy" else object(),
|
||||
)
|
||||
|
||||
with pytest.raises(SkyvernExtraNotInstalled, match=r"pip install skyvern\[server\]"):
|
||||
with pytest.raises(SkyvernExtraNotInstalled, match=r'pip install "skyvern\[server\]"'):
|
||||
require_server_extra_modules("skyvern.library.skyvern_browser_page")
|
||||
|
||||
|
||||
|
|
@ -157,10 +177,33 @@ def test_require_server_extra_modules_catches_partial_server_graph(monkeypatch:
|
|||
lambda module_name: None if module_name == "jinja2" else object(),
|
||||
)
|
||||
|
||||
with pytest.raises(SkyvernExtraNotInstalled, match=r"pip install skyvern\[server\]"):
|
||||
with pytest.raises(SkyvernExtraNotInstalled, match=r'pip install "skyvern\[server\]"'):
|
||||
require_server_extra_modules("skyvern.library.skyvern_browser_page")
|
||||
|
||||
|
||||
def test_require_server_extra_modules_discriminates_from_local_with_explicit_modules(
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
) -> None:
|
||||
monkeypatch.setattr(
|
||||
"skyvern.exceptions.find_spec",
|
||||
lambda module_name: None if module_name == "uvicorn" else object(),
|
||||
)
|
||||
|
||||
require_server_extra_modules("skyvern.forge.api_app")
|
||||
with pytest.raises(SkyvernExtraNotInstalled, match=r'pip install "skyvern\[server\]"'):
|
||||
require_server_extra_modules("skyvern.forge", ("uvicorn",))
|
||||
|
||||
|
||||
def test_require_local_extra_modules_requires_local_sentinels(monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
monkeypatch.setattr(
|
||||
"skyvern.exceptions.find_spec",
|
||||
lambda module_name: None if module_name == "playwright" else object(),
|
||||
)
|
||||
|
||||
with pytest.raises(SkyvernExtraNotInstalled, match=r'pip install "skyvern\[local\]"'):
|
||||
require_local_extra_modules("skyvern.library.skyvern_browser_page")
|
||||
|
||||
|
||||
def test_browser_connection_error_connect_over_cdp_websocket() -> None:
|
||||
"""The exact error from SKY-8578: connect_over_cdp fails with 502 Bad Gateway."""
|
||||
raw_error = (
|
||||
|
|
|
|||
54
tests/unit/test_quickstart_command.py
Normal file
54
tests/unit/test_quickstart_command.py
Normal file
|
|
@ -0,0 +1,54 @@
|
|||
from __future__ import annotations
|
||||
|
||||
from typer.testing import CliRunner
|
||||
|
||||
import skyvern.cli.quickstart as quickstart_module
|
||||
|
||||
|
||||
def test_quickstart_without_server_extra_prints_install_paths(monkeypatch) -> None:
|
||||
monkeypatch.setattr(quickstart_module, "check_docker_compose_file", lambda: False)
|
||||
monkeypatch.setattr(quickstart_module, "_has_server_quickstart_extra", lambda: False)
|
||||
|
||||
result = CliRunner().invoke(quickstart_module.quickstart_app, [])
|
||||
|
||||
assert result.exit_code == 0
|
||||
assert "Cloud/API SDK" in result.output
|
||||
assert 'pip install "skyvern[local]"' in result.output
|
||||
assert "Skyvern.local(use_in_memory_db=True)" in result.output
|
||||
assert 'pip install "skyvern[server]"' in result.output
|
||||
assert "Postgres" in result.output
|
||||
assert "Missing Dependency" not in result.output
|
||||
assert "Missing:" not in result.output
|
||||
|
||||
|
||||
def test_quickstart_with_server_extra_preserves_existing_flow(monkeypatch) -> None:
|
||||
calls = []
|
||||
|
||||
monkeypatch.setattr(quickstart_module, "check_docker_compose_file", lambda: False)
|
||||
monkeypatch.setattr(quickstart_module, "_has_server_quickstart_extra", lambda: True)
|
||||
monkeypatch.setattr(
|
||||
quickstart_module,
|
||||
"_run_server_quickstart",
|
||||
lambda **kwargs: calls.append(kwargs),
|
||||
)
|
||||
|
||||
result = CliRunner().invoke(
|
||||
quickstart_module.quickstart_app,
|
||||
[
|
||||
"--no-postgres",
|
||||
"--database-string",
|
||||
"postgresql+psycopg://user/db",
|
||||
"--skip-browser-install",
|
||||
"--server-only",
|
||||
],
|
||||
)
|
||||
|
||||
assert result.exit_code == 0
|
||||
assert calls == [
|
||||
{
|
||||
"no_postgres": True,
|
||||
"database_string": "postgresql+psycopg://user/db",
|
||||
"skip_browser_install": True,
|
||||
"server_only": True,
|
||||
}
|
||||
]
|
||||
54
tests/unit/test_workflow_copilot_diagnostic_intent.py
Normal file
54
tests/unit/test_workflow_copilot_diagnostic_intent.py
Normal file
|
|
@ -0,0 +1,54 @@
|
|||
"""Prompt and tool-description guards for diagnostic copilot turns."""
|
||||
|
||||
from skyvern.forge.prompts import prompt_engine
|
||||
from skyvern.forge.sdk.copilot.tools import run_blocks_tool, update_and_run_blocks_tool
|
||||
|
||||
_AGENT_TEMPLATE_DEFAULTS = dict(
|
||||
workflow_knowledge_base="test kb",
|
||||
current_datetime="2026-01-01T00:00:00Z",
|
||||
tool_usage_guide="",
|
||||
security_rules="",
|
||||
)
|
||||
|
||||
|
||||
def _render_agent_prompt() -> str:
|
||||
return prompt_engine.load_prompt("workflow-copilot-agent", **_AGENT_TEMPLATE_DEFAULTS)
|
||||
|
||||
|
||||
class TestDiagnosticObservationIntent:
|
||||
"""Pin SKY-9651: observational complaints inspect first instead of edit/run by default."""
|
||||
|
||||
def test_agent_prompt_has_diagnostic_observation_section(self) -> None:
|
||||
rendered = _render_agent_prompt()
|
||||
|
||||
assert "DIAGNOSTIC / OBSERVATIONAL COMPLAINTS" in rendered
|
||||
assert "inspect-and-clarify" in rendered
|
||||
assert "Do NOT call `update_and_run_blocks`" in rendered
|
||||
assert "do NOT call `run_blocks_and_collect_debug`" in rendered
|
||||
|
||||
def test_agent_prompt_names_failed_repro_shapes(self) -> None:
|
||||
rendered = _render_agent_prompt()
|
||||
|
||||
for phrase in (
|
||||
"missing newly added workflow block",
|
||||
"not following a configured search step",
|
||||
"whether a named block is present",
|
||||
"runtime error condition did not trigger",
|
||||
"missing or unknown block labels",
|
||||
):
|
||||
assert phrase in rendered
|
||||
|
||||
def test_agent_prompt_preserves_explicit_edit_requests(self) -> None:
|
||||
rendered = _render_agent_prompt()
|
||||
|
||||
assert "Explicit edit/debug requests remain edit requests" in rendered
|
||||
assert "how can I improve" in rendered
|
||||
assert "what would fix" in rendered
|
||||
assert "call `update_and_run_blocks`" in rendered
|
||||
|
||||
def test_block_running_tools_defer_diagnostic_complaints(self) -> None:
|
||||
for tool in (run_blocks_tool, update_and_run_blocks_tool):
|
||||
desc = tool.description # type: ignore[attr-defined]
|
||||
assert "diagnostic / observational complaint" in desc
|
||||
assert "inspect-and-clarify" in desc
|
||||
assert "not the first response" in desc
|
||||
219
uv.lock
generated
219
uv.lock
generated
|
|
@ -15,7 +15,7 @@ resolution-markers = [
|
|||
|
||||
[manifest]
|
||||
constraints = [
|
||||
{ name = "authlib", specifier = ">=1.6.9" },
|
||||
{ name = "authlib", specifier = ">=1.7.1" },
|
||||
{ name = "flask", specifier = ">=3.1.3" },
|
||||
{ name = "joserfc", specifier = ">=1.6.3" },
|
||||
{ name = "pyasn1", specifier = ">=0.6.3" },
|
||||
|
|
@ -253,7 +253,7 @@ wheels = [
|
|||
|
||||
[[package]]
|
||||
name = "anthropic"
|
||||
version = "0.96.0"
|
||||
version = "0.97.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "anyio" },
|
||||
|
|
@ -265,9 +265,9 @@ dependencies = [
|
|||
{ name = "sniffio" },
|
||||
{ name = "typing-extensions" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/b9/7e/672f533dee813028d2c699bfd2a7f52c9118d7353680d9aa44b9e23f717f/anthropic-0.96.0.tar.gz", hash = "sha256:9de947b737f39452f68aa520f1c2239d44119c9b73b0fb6d4e6ca80f00279ee6", size = 658210, upload-time = "2026-04-16T14:28:02.846Z" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/14/93/f66ea8bfe39f2e6bb9da8e27fa5457ad2520e8f7612dfc547b17fad55c4d/anthropic-0.97.0.tar.gz", hash = "sha256:021e79fd8e21e90ad94dc5ba2bbbd8b1599f424f5b1fab6c06204009cab764be", size = 669502, upload-time = "2026-04-23T20:52:34.445Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/48/5a/72f33204064b6e87601a71a6baf8d855769f8a0c1eaae8d06a1094872371/anthropic-0.96.0-py3-none-any.whl", hash = "sha256:9a6e335a354602a521cd9e777e92bfd46ba6e115bf9bbfe6135311e8fb2015b2", size = 635930, upload-time = "2026-04-16T14:28:01.436Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/53/b6/8e851369fa661ad0fef2ae6266bf3b7d52b78ccf011720058f4adaca59e2/anthropic-0.97.0-py3-none-any.whl", hash = "sha256:8a1a472dfabcfc0c52ff6a3eecf724ac7e07107a2f6e2367be55ceb42f5d5613", size = 662126, upload-time = "2026-04-23T20:52:32.377Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
|
@ -425,15 +425,15 @@ wheels = [
|
|||
|
||||
[[package]]
|
||||
name = "authlib"
|
||||
version = "1.7.0"
|
||||
version = "1.7.1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "cryptography" },
|
||||
{ name = "joserfc" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/d9/82/4d0603f30c1b4629b1f091bb266b0d7986434891d6940a8c87f8098db24e/authlib-1.7.0.tar.gz", hash = "sha256:b3e326c9aa9cc3ea95fe7d89fd880722d3608da4d00e8a27e061e64b48d801d5", size = 175890, upload-time = "2026-04-18T11:00:28.559Z" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/3c/f2/e05664d5275ce811fd4e9df0a2b3f0086ee19a8a80358d95499fa82fd50c/authlib-1.7.1.tar.gz", hash = "sha256:8c09b0f9d080c823e594b52316af70f79a1fa4eed64d0363a076233c04ef063a", size = 175884, upload-time = "2026-05-04T08:11:25.033Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/ca/48/c954218b2a250e23f178f10167c4173fecb5a75d2c206f0a67ba58006c26/authlib-1.7.0-py2.py3-none-any.whl", hash = "sha256:e36817afb02f6f0b6bf55f150782499ddd6ddf44b402bb055d3263cc65ac9ae0", size = 258779, upload-time = "2026-04-18T11:00:26.64Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e0/82/730650ee5e5b598b7bfdc291b784bc2f6fe02a5671695485403365101088/authlib-1.7.1-py2.py3-none-any.whl", hash = "sha256:8470f4aa6b5590ac41bd81d6e6ee12448ce36a0da0af19bbed69fb53fb4e8ad9", size = 258826, upload-time = "2026-05-04T08:11:23.208Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
|
@ -1101,7 +1101,7 @@ wheels = [
|
|||
|
||||
[[package]]
|
||||
name = "fastapi"
|
||||
version = "0.136.0"
|
||||
version = "0.136.1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "annotated-doc" },
|
||||
|
|
@ -1110,9 +1110,9 @@ dependencies = [
|
|||
{ name = "typing-extensions" },
|
||||
{ name = "typing-inspection" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/4e/d9/e66315807e41e69e7f6a1b42a162dada2f249c5f06ad3f1a95f84ab336ef/fastapi-0.136.0.tar.gz", hash = "sha256:cf08e067cc66e106e102d9ba659463abfac245200752f8a5b7b1e813de4ff73e", size = 396607, upload-time = "2026-04-16T11:47:13.623Z" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/5d/45/c130091c2dfa061bbfe3150f2a5091ef1adf149f2a8d2ae769ecaf6e99a2/fastapi-0.136.1.tar.gz", hash = "sha256:7af665ad7acfa0a3baf8983d393b6b471b9da10ede59c60045f49fbc89a0fa7f", size = 397448, upload-time = "2026-04-23T16:49:44.046Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/26/a3/0bd5f0cdb0bbc92650e8dc457e9250358411ee5d1b65e42b6632387daf81/fastapi-0.136.0-py3-none-any.whl", hash = "sha256:8793d44ec7378e2be07f8a013cf7f7aa47d6327d0dfe9804862688ec4541a6b4", size = 117556, upload-time = "2026-04-16T11:47:11.922Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/5a/ff/2e4eca3ade2c22fe1dea7043b8ee9dabe47753349eb1b56a202de8af6349/fastapi-0.136.1-py3-none-any.whl", hash = "sha256:a6e9d7eeada96c93a4d69cb03836b44fa34e2854accb7244a1ece36cd4781c3f", size = 117683, upload-time = "2026-04-23T16:49:42.437Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
|
@ -1428,7 +1428,7 @@ wheels = [
|
|||
|
||||
[[package]]
|
||||
name = "google-cloud-aiplatform"
|
||||
version = "1.148.1"
|
||||
version = "1.149.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "docstring-parser" },
|
||||
|
|
@ -1444,9 +1444,9 @@ dependencies = [
|
|||
{ name = "pydantic" },
|
||||
{ name = "typing-extensions" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/c9/f3/b2a9417014c93858a2e3266134f931eefd972c2d410b25d7b8782fc6f143/google_cloud_aiplatform-1.148.1.tar.gz", hash = "sha256:75d605fba34e68714bd08e1e482755d0a6e3ae972805f809d088e686c30879e7", size = 10278758, upload-time = "2026-04-17T23:45:26.738Z" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/42/2c/fba4adc56f74c0ee0fbd91a39d414ca2c3588dd8b71f9be8a507015ca886/google_cloud_aiplatform-1.149.0.tar.gz", hash = "sha256:a4d73485bf1d727a9e1bbbd13d08d7031490686bbf7d125eb905c1a6c1559a35", size = 10451466, upload-time = "2026-04-27T23:11:54.513Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/56/5b/e3515d7bbba602c2b0f6a0da5431785e897252443682e4735d0e6873dc8f/google_cloud_aiplatform-1.148.1-py2.py3-none-any.whl", hash = "sha256:035101e2d8e65c6a706cc3930b2452de7ddcbde50dd130320fcea0d8b03b0c5a", size = 8434481, upload-time = "2026-04-17T23:45:22.919Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/bf/a0/27719ba23967ef62e52a1d54e013e0fc174bdab8dd84fb300bab9bf0d4a3/google_cloud_aiplatform-1.149.0-py2.py3-none-any.whl", hash = "sha256:e6b5299fa5d303e971cb29a19f03fdbb7b1e3b9d2faa3a788ca933341fba2f2e", size = 8570410, upload-time = "2026-04-27T23:11:50.495Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
|
@ -2230,11 +2230,11 @@ wheels = [
|
|||
|
||||
[[package]]
|
||||
name = "json-repair"
|
||||
version = "0.59.4"
|
||||
version = "0.59.5"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/32/41/4ae9c6e711647a41b4e0c04bce815113ce9c0286eff6dc6fb86979b2fb9f/json_repair-0.59.4.tar.gz", hash = "sha256:559ca1828f6f566530663cd96d64bee29f8282b9d2ff0e661e05fa87b4171ab3", size = 47624, upload-time = "2026-04-15T06:48:40.776Z" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/b7/67/eba7fad54ff6f5cce6db4e01f596fc68156b5c7e864af0aa07ad48e880a1/json_repair-0.59.5.tar.gz", hash = "sha256:bb886ee054e99066be8a337b67a986b6a50d79be9a5ad37ae81966e698990784", size = 48632, upload-time = "2026-04-24T11:41:38.133Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/74/c4/ec3068436d2275731539b7a43fbc947f502bc3fe149856a5d00368c7b087/json_repair-0.59.4-py3-none-any.whl", hash = "sha256:46052e646bc0b0c39db672ebbf732f774f3c1a5bde81a54f0b0e19d3af4f45cd", size = 46697, upload-time = "2026-04-15T06:48:39.61Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/3e/aa/0529dee460b745b93f6abc97b56b7527314c5167ba29ab7a5bd5c08de01f/json_repair-0.59.5-py3-none-any.whl", hash = "sha256:6869965bd1cc1aaaa04dc85865c26fbb76d9a2d83a20010f5eae2563b1567827", size = 47282, upload-time = "2026-04-24T11:41:36.653Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
|
@ -2432,7 +2432,7 @@ wheels = [
|
|||
|
||||
[[package]]
|
||||
name = "jupyterlab"
|
||||
version = "4.5.6"
|
||||
version = "4.5.7"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "async-lru" },
|
||||
|
|
@ -2449,9 +2449,9 @@ dependencies = [
|
|||
{ name = "tornado" },
|
||||
{ name = "traitlets" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/ac/d5/730628e03fff2e8a8e8ccdaedde1489ab1309f9a4fa2536248884e30b7c7/jupyterlab-4.5.6.tar.gz", hash = "sha256:642fe2cfe7f0f5922a8a558ba7a0d246c7bc133b708dfe43f7b3a826d163cf42", size = 23970670, upload-time = "2026-03-11T14:17:04.531Z" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/2b/22/8440ec827762146e7cdecf04335bd348795899d29dc6ae82238707353a2c/jupyterlab-4.5.7.tar.gz", hash = "sha256:55a9822c4754da305f41e113452c68383e214dcf96de760146af89ce5d5117b0", size = 23992763, upload-time = "2026-04-29T16:43:51.328Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/e1/1b/dad6fdcc658ed7af26fdf3841e7394072c9549a8b896c381ab49dd11e2d9/jupyterlab-4.5.6-py3-none-any.whl", hash = "sha256:d6b3dac883aa4d9993348e0f8e95b24624f75099aed64eab6a4351a9cdd1e580", size = 12447124, upload-time = "2026-03-11T14:17:00.229Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/3d/aa/537b8f7d80e799af19af35fb3ddfc970b951088a13c57dd9387dcfbb7f61/jupyterlab-4.5.7-py3-none-any.whl", hash = "sha256:fba4cb0e2c44a52859669d8c98b45de029d5e515f8407bf8534d2a8fc5f0964d", size = 12450123, upload-time = "2026-04-29T16:43:46.639Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
|
@ -3215,7 +3215,7 @@ wheels = [
|
|||
|
||||
[[package]]
|
||||
name = "notebook"
|
||||
version = "7.5.5"
|
||||
version = "7.5.6"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "jupyter-server" },
|
||||
|
|
@ -3224,9 +3224,9 @@ dependencies = [
|
|||
{ name = "notebook-shim" },
|
||||
{ name = "tornado" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/1f/6d/41052c48d6f6349ca0a7c4d1f6a78464de135e6d18f5829ba2510e62184c/notebook-7.5.5.tar.gz", hash = "sha256:dc0bfab0f2372c8278c457423d3256c34154ac2cc76bf20e9925260c461013c3", size = 14169167, upload-time = "2026-03-11T16:32:51.922Z" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/2a/c2/cf59bd2e6f2c8b976b52477e3e53bf6f97bc714ed046a51821afb428eaee/notebook-7.5.6.tar.gz", hash = "sha256:621174aade80108f0020b0f00738000b215f75fa3cd90771ad7aa0f24536a4e1", size = 14170814, upload-time = "2026-04-30T11:46:26.613Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/f8/aa/cbd1deb9f07446241e88f8d5fecccd95b249bca0b4e5482214a4d1714c49/notebook-7.5.5-py3-none-any.whl", hash = "sha256:a7c14dbeefa6592e87f72290ca982e0c10f5bbf3786be2a600fda9da2764a2b8", size = 14578929, upload-time = "2026-03-11T16:32:48.021Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e9/d6/1fd0646b9bbd9efbb0b8ae21b2325fbef515769a5621c03e31d8eb8da587/notebook-7.5.6-py3-none-any.whl", hash = "sha256:4dde3f8fb55fa8fb7946d58c6e869ce9baf46d00fc070664f62604569d0faca0", size = 14581730, upload-time = "2026-04-30T11:46:22.342Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
|
@ -3336,7 +3336,7 @@ wheels = [
|
|||
|
||||
[[package]]
|
||||
name = "openai"
|
||||
version = "2.32.0"
|
||||
version = "2.33.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "anyio" },
|
||||
|
|
@ -3348,14 +3348,14 @@ dependencies = [
|
|||
{ name = "tqdm" },
|
||||
{ name = "typing-extensions" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/ed/59/bdcc6b759b8c42dd73afaf5bf8f902c04b37987a5514dbc1c64dba390fef/openai-2.32.0.tar.gz", hash = "sha256:c54b27a9e4cb8d51f0dd94972ffd1a04437efeb259a9e60d8922b8bd26fe55e0", size = 693286, upload-time = "2026-04-15T22:28:19.434Z" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/f0/ee/d056c82f63c05f06baac0cffb4a90952d8274f90c49dfe244f20497b9bbd/openai-2.33.0.tar.gz", hash = "sha256:f850c435e2a4685bba3295bd54912dd26315d9c1b7733068186134d6e0599f9a", size = 693254, upload-time = "2026-04-28T14:04:42.428Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/1e/c1/d6e64ccd0536bf616556f0cad2b6d94a8125f508d25cfd814b1d2db4e2f1/openai-2.32.0-py3-none-any.whl", hash = "sha256:4dcc9badeb4bf54ad0d187453742f290226d30150890b7890711bda4f32f192f", size = 1162570, upload-time = "2026-04-15T22:28:17.714Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/7d/32/37734d769bc8b42e4938785313cc05aade6cb0fa72479d3220a0d61a4e78/openai-2.33.0-py3-none-any.whl", hash = "sha256:03ac37d70e8c9e3a8124214e3afa785e2cbc12e627fbd98177a086ef2fd87ad5", size = 1162695, upload-time = "2026-04-28T14:04:40.482Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "openai-agents"
|
||||
version = "0.14.4"
|
||||
version = "0.14.8"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "griffelib" },
|
||||
|
|
@ -3367,9 +3367,9 @@ dependencies = [
|
|||
{ name = "typing-extensions" },
|
||||
{ name = "websockets" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/49/f2/bc697eecb3efcdce4e2b5b003fc078a3c5693a1f114a9d4ca8ce327e25ec/openai_agents-0.14.4.tar.gz", hash = "sha256:897dd2b0d4d3da871753bf1db8742452f4a77736f3f63e73c8db544919c5917e", size = 5304591, upload-time = "2026-04-21T19:28:41.061Z" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/d5/8a/d36ab647f05e790ec97dda9e4c0eb39d8840269d6a5194887b5dec92bd0d/openai_agents-0.14.8.tar.gz", hash = "sha256:fe1cb58b4150a07292a94f15d8fd5217ee9195bd6bcd8a6a46fdb1d9b08a70b7", size = 5314520, upload-time = "2026-04-29T03:40:07.6Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/fd/aa/7b8b94150096df7fbc5602347815d6acc769daff72cd6a10011e91f25bbf/openai_agents-0.14.4-py3-none-any.whl", hash = "sha256:4f10b87432cd73f436accf01c762d34df977d32f0a1b49300c39eed080784f4c", size = 815389, upload-time = "2026-04-21T19:28:39.015Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/af/6e/1e9adcedcde7b163579b88a68f765a4915be4ead0713270386d9432cfd2f/openai_agents-0.14.8-py3-none-any.whl", hash = "sha256:2937ef582ccaa45d59e89839ed8948cb2a6d808bc9940f0881793c21f37f7776", size = 817332, upload-time = "2026-04-29T03:40:05.68Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
|
@ -4916,11 +4916,11 @@ wheels = [
|
|||
|
||||
[[package]]
|
||||
name = "python-multipart"
|
||||
version = "0.0.26"
|
||||
version = "0.0.27"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/88/71/b145a380824a960ebd60e1014256dbb7d2253f2316ff2d73dfd8928ec2c3/python_multipart-0.0.26.tar.gz", hash = "sha256:08fadc45918cd615e26846437f50c5d6d23304da32c341f289a617127b081f17", size = 43501, upload-time = "2026-04-10T14:09:59.473Z" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/69/9b/f23807317a113dc36e74e75eb265a02dd1a4d9082abc3c1064acd22997c4/python_multipart-0.0.27.tar.gz", hash = "sha256:9870a6a8c5a20a5bf4f07c017bd1489006ff8836cff097b6933355ee2b49b602", size = 44043, upload-time = "2026-04-27T10:51:26.649Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/9a/22/f1925cdda983ab66fc8ec6ec8014b959262747e58bdca26a4e3d1da29d56/python_multipart-0.0.26-py3-none-any.whl", hash = "sha256:c0b169f8c4484c13b0dcf2ef0ec3a4adb255c4b7d18d8e420477d2b1dd03f185", size = 28847, upload-time = "2026-04-10T14:09:58.131Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/99/78/4126abcbdbd3c559d43e0db7f7b9173fc6befe45d39a2856cc0b8ec2a5a6/python_multipart-0.0.27-py3-none-any.whl", hash = "sha256:6fccfad17a27334bd0193681b369f476eda3409f17381a2d65aa7df3f7275645", size = 29254, upload-time = "2026-04-27T10:51:24.997Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
|
@ -5514,6 +5514,64 @@ dependencies = [
|
|||
]
|
||||
|
||||
[package.optional-dependencies]
|
||||
local = [
|
||||
{ name = "aioboto3" },
|
||||
{ name = "aiofiles" },
|
||||
{ name = "aiohttp" },
|
||||
{ name = "aiosqlite" },
|
||||
{ name = "alembic" },
|
||||
{ name = "anthropic" },
|
||||
{ name = "asyncache" },
|
||||
{ name = "authlib" },
|
||||
{ name = "azure-identity" },
|
||||
{ name = "azure-keyvault-secrets" },
|
||||
{ name = "azure-storage-blob" },
|
||||
{ name = "croniter" },
|
||||
{ name = "curlparser" },
|
||||
{ name = "email-validator" },
|
||||
{ name = "fastapi" },
|
||||
{ name = "filetype" },
|
||||
{ name = "fuzzysearch" },
|
||||
{ name = "google-auth" },
|
||||
{ name = "google-auth-oauthlib" },
|
||||
{ name = "google-cloud-aiplatform" },
|
||||
{ name = "greenlet", version = "3.0.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.12'" },
|
||||
{ name = "greenlet", version = "3.4.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.12'" },
|
||||
{ name = "jinja2" },
|
||||
{ name = "json-repair" },
|
||||
{ name = "json5" },
|
||||
{ name = "jsonschema" },
|
||||
{ name = "lark" },
|
||||
{ name = "libcst" },
|
||||
{ name = "litellm" },
|
||||
{ name = "onepassword-sdk" },
|
||||
{ name = "openai" },
|
||||
{ name = "opentelemetry-api" },
|
||||
{ name = "pandas" },
|
||||
{ name = "pdfplumber" },
|
||||
{ name = "pillow" },
|
||||
{ name = "playwright", version = "1.46.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.12'" },
|
||||
{ name = "playwright", version = "1.58.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.12'" },
|
||||
{ name = "psutil" },
|
||||
{ name = "pyasn1" },
|
||||
{ name = "pyjwt", extra = ["crypto"] },
|
||||
{ name = "pyotp" },
|
||||
{ name = "pypdf" },
|
||||
{ name = "python-calamine" },
|
||||
{ name = "python-docx" },
|
||||
{ name = "python-multipart" },
|
||||
{ name = "requests-toolbelt" },
|
||||
{ name = "sqlalchemy" },
|
||||
{ name = "sse-starlette" },
|
||||
{ name = "starlette-context" },
|
||||
{ name = "tiktoken" },
|
||||
{ name = "tldextract" },
|
||||
{ name = "toml" },
|
||||
{ name = "tornado" },
|
||||
{ name = "urllib3" },
|
||||
{ name = "websockets" },
|
||||
{ name = "zstandard" },
|
||||
]
|
||||
server = [
|
||||
{ name = "aioboto3" },
|
||||
{ name = "aiofiles" },
|
||||
|
|
@ -5635,90 +5693,145 @@ dev = [
|
|||
|
||||
[package.metadata]
|
||||
requires-dist = [
|
||||
{ name = "aioboto3", marker = "extra == 'local'", specifier = ">=14.3.0,<15" },
|
||||
{ name = "aioboto3", marker = "extra == 'server'", specifier = ">=14.3.0,<15" },
|
||||
{ name = "aiofiles", marker = "extra == 'local'", specifier = ">=24.1.0,<25" },
|
||||
{ name = "aiofiles", marker = "extra == 'server'", specifier = ">=24.1.0,<25" },
|
||||
{ name = "aiohttp", marker = "extra == 'local'", specifier = ">=3.13.4,<4" },
|
||||
{ name = "aiohttp", marker = "extra == 'server'", specifier = ">=3.13.4,<4" },
|
||||
{ name = "aioredlock", marker = "extra == 'server'", specifier = ">=0.7.3,<0.8" },
|
||||
{ name = "aiosqlite", marker = "extra == 'local'", specifier = ">=0.21.0,<0.23" },
|
||||
{ name = "aiosqlite", marker = "extra == 'server'", specifier = ">=0.21.0,<0.23" },
|
||||
{ name = "alembic", marker = "extra == 'local'", specifier = ">=1.12.1,<2" },
|
||||
{ name = "alembic", marker = "extra == 'server'", specifier = ">=1.12.1,<2" },
|
||||
{ name = "anthropic", marker = "extra == 'server'", specifier = ">=0.96.0,<0.97" },
|
||||
{ name = "anthropic", marker = "extra == 'local'", specifier = ">=0.97.0,<0.98" },
|
||||
{ name = "anthropic", marker = "extra == 'server'", specifier = ">=0.97.0,<0.98" },
|
||||
{ name = "asyncache", marker = "extra == 'local'", specifier = ">=0.3.1,<0.4" },
|
||||
{ name = "asyncache", marker = "extra == 'server'", specifier = ">=0.3.1,<0.4" },
|
||||
{ name = "asyncpg", marker = "extra == 'server'", specifier = ">=0.30.0,<0.32" },
|
||||
{ name = "authlib", marker = "extra == 'server'", specifier = ">=1.6.9" },
|
||||
{ name = "authlib", marker = "extra == 'local'", specifier = ">=1.7.1" },
|
||||
{ name = "authlib", marker = "extra == 'server'", specifier = ">=1.7.1" },
|
||||
{ name = "azure-identity", marker = "extra == 'local'", specifier = ">=1.24.0,<2" },
|
||||
{ name = "azure-identity", marker = "extra == 'server'", specifier = ">=1.24.0,<2" },
|
||||
{ name = "azure-keyvault-secrets", marker = "extra == 'local'", specifier = ">=4.2.0,<5" },
|
||||
{ name = "azure-keyvault-secrets", marker = "extra == 'server'", specifier = ">=4.2.0,<5" },
|
||||
{ name = "azure-storage-blob", marker = "extra == 'local'", specifier = ">=12.26.0" },
|
||||
{ name = "azure-storage-blob", marker = "extra == 'server'", specifier = ">=12.26.0" },
|
||||
{ name = "cachetools", specifier = ">=5.3.2,<6" },
|
||||
{ name = "croniter", marker = "extra == 'local'", specifier = ">=1.3.8,<4" },
|
||||
{ name = "croniter", marker = "extra == 'server'", specifier = ">=1.3.8,<4" },
|
||||
{ name = "curlparser", marker = "extra == 'local'", specifier = ">=0.1.0,<0.2" },
|
||||
{ name = "curlparser", marker = "extra == 'server'", specifier = ">=0.1.0,<0.2" },
|
||||
{ name = "email-validator", marker = "extra == 'local'", specifier = ">=2.2.0,<3" },
|
||||
{ name = "email-validator", marker = "extra == 'server'", specifier = ">=2.2.0,<3" },
|
||||
{ name = "fastapi", marker = "extra == 'server'", specifier = ">=0.136.0,<0.137" },
|
||||
{ name = "fastapi", marker = "extra == 'local'", specifier = ">=0.136.1,<0.137" },
|
||||
{ name = "fastapi", marker = "extra == 'server'", specifier = ">=0.136.1,<0.137" },
|
||||
{ name = "fastmcp", marker = "extra == 'server'", specifier = ">=3.2.0,<4" },
|
||||
{ name = "filetype", marker = "extra == 'local'", specifier = ">=1.2.0,<2" },
|
||||
{ name = "filetype", marker = "extra == 'server'", specifier = ">=1.2.0,<2" },
|
||||
{ name = "fuzzysearch", marker = "extra == 'local'", specifier = ">=0.8,<1" },
|
||||
{ name = "fuzzysearch", marker = "extra == 'server'", specifier = ">=0.8,<1" },
|
||||
{ name = "google-auth", marker = "extra == 'local'", specifier = ">=2.49.2,<3" },
|
||||
{ name = "google-auth", marker = "extra == 'server'", specifier = ">=2.49.2,<3" },
|
||||
{ name = "google-auth-oauthlib", marker = "extra == 'local'", specifier = ">=1.3.1,<2" },
|
||||
{ name = "google-auth-oauthlib", marker = "extra == 'server'", specifier = ">=1.3.1,<2" },
|
||||
{ name = "google-cloud-aiplatform", marker = "extra == 'server'", specifier = ">=1.90.0,<2" },
|
||||
{ name = "google-cloud-aiplatform", marker = "extra == 'local'", specifier = ">=1.149.0,<2" },
|
||||
{ name = "google-cloud-aiplatform", marker = "extra == 'server'", specifier = ">=1.149.0,<2" },
|
||||
{ name = "greenlet", marker = "python_full_version == '3.11.*' and extra == 'local'", specifier = "==3.0.3" },
|
||||
{ name = "greenlet", marker = "python_full_version == '3.11.*' and extra == 'server'", specifier = "==3.0.3" },
|
||||
{ name = "greenlet", marker = "python_full_version >= '3.12' and python_full_version < '3.14' and extra == 'local'", specifier = ">3.0.3" },
|
||||
{ name = "greenlet", marker = "python_full_version >= '3.12' and python_full_version < '3.14' and extra == 'server'", specifier = ">3.0.3" },
|
||||
{ name = "httpx", extras = ["socks"], specifier = ">=0.27.0" },
|
||||
{ name = "jinja2", marker = "extra == 'local'", specifier = ">=3.1.2,<4" },
|
||||
{ name = "jinja2", marker = "extra == 'server'", specifier = ">=3.1.2,<4" },
|
||||
{ name = "json-repair", marker = "extra == 'server'", specifier = ">=0.59.4,<0.60" },
|
||||
{ name = "json-repair", marker = "extra == 'local'", specifier = ">=0.59.5,<0.60" },
|
||||
{ name = "json-repair", marker = "extra == 'server'", specifier = ">=0.59.5,<0.60" },
|
||||
{ name = "json5", marker = "extra == 'local'", specifier = ">=0.13.0,<1" },
|
||||
{ name = "json5", marker = "extra == 'server'", specifier = ">=0.13.0,<1" },
|
||||
{ name = "jsonschema", marker = "extra == 'local'", specifier = ">=4.23.0" },
|
||||
{ name = "jsonschema", marker = "extra == 'server'", specifier = ">=4.23.0" },
|
||||
{ name = "lark", marker = "extra == 'local'", specifier = ">=1.2.2,<2" },
|
||||
{ name = "lark", marker = "extra == 'server'", specifier = ">=1.2.2,<2" },
|
||||
{ name = "libcst", marker = "extra == 'local'", specifier = ">=1.8.2,<2" },
|
||||
{ name = "libcst", marker = "extra == 'server'", specifier = ">=1.8.2,<2" },
|
||||
{ name = "litellm", marker = "extra == 'local'", specifier = "==1.83.14" },
|
||||
{ name = "litellm", marker = "extra == 'server'", specifier = "==1.83.14" },
|
||||
{ name = "onepassword-sdk", marker = "extra == 'local'", specifier = "==0.4.0" },
|
||||
{ name = "onepassword-sdk", marker = "extra == 'server'", specifier = "==0.4.0" },
|
||||
{ name = "openai", marker = "extra == 'local'", specifier = ">=1.68.2" },
|
||||
{ name = "openai", marker = "extra == 'server'", specifier = ">=1.68.2" },
|
||||
{ name = "openai-agents", marker = "extra == 'server'", specifier = ">=0.10.5,<0.15" },
|
||||
{ name = "opentelemetry-api", marker = "extra == 'local'", specifier = ">=1.41.1,<2" },
|
||||
{ name = "opentelemetry-api", marker = "extra == 'server'", specifier = ">=1.41.1,<2" },
|
||||
{ name = "orjson", specifier = ">=3.9.10,<4" },
|
||||
{ name = "pandas", marker = "extra == 'local'", specifier = ">=2.3.1,<4" },
|
||||
{ name = "pandas", marker = "extra == 'server'", specifier = ">=2.3.1,<4" },
|
||||
{ name = "pdfplumber", marker = "extra == 'local'", specifier = ">=0.11.0,<0.12" },
|
||||
{ name = "pdfplumber", marker = "extra == 'server'", specifier = ">=0.11.0,<0.12" },
|
||||
{ name = "pillow", marker = "extra == 'local'", specifier = ">=10.2.0" },
|
||||
{ name = "pillow", marker = "extra == 'server'", specifier = ">=10.2.0" },
|
||||
{ name = "playwright", marker = "python_full_version == '3.11.*' and extra == 'local'", specifier = "==1.46.0" },
|
||||
{ name = "playwright", marker = "python_full_version == '3.11.*' and extra == 'server'", specifier = "==1.46.0" },
|
||||
{ name = "playwright", marker = "python_full_version >= '3.12' and python_full_version < '3.14' and extra == 'local'", specifier = ">1.46.0" },
|
||||
{ name = "playwright", marker = "python_full_version >= '3.12' and python_full_version < '3.14' and extra == 'server'", specifier = ">1.46.0" },
|
||||
{ name = "posthog", marker = "extra == 'server'", specifier = ">=3.7.0,<4" },
|
||||
{ name = "psutil", marker = "extra == 'local'", specifier = ">=7.0.0" },
|
||||
{ name = "psutil", marker = "extra == 'server'", specifier = ">=7.0.0" },
|
||||
{ name = "psycopg", extras = ["binary", "pool"], marker = "python_full_version >= '3.11' and python_full_version < '3.13' and extra == 'server'", specifier = "==3.1.18" },
|
||||
{ name = "psycopg", extras = ["binary", "pool"], marker = "python_full_version == '3.13.*' and extra == 'server'", specifier = ">=3.2.2,<3.3.0" },
|
||||
{ name = "pyasn1", marker = "extra == 'local'", specifier = ">=0.6.3" },
|
||||
{ name = "pyasn1", marker = "extra == 'server'", specifier = ">=0.6.3" },
|
||||
{ name = "pydantic", specifier = ">=2.5.2,<3" },
|
||||
{ name = "pydantic-settings", specifier = ">=2.1.0,<3" },
|
||||
{ name = "pyjwt", marker = "extra == 'server'", specifier = ">=2.12.0" },
|
||||
{ name = "pyjwt", extras = ["crypto"], marker = "extra == 'local'", specifier = ">=2.12.0,<3" },
|
||||
{ name = "pyjwt", extras = ["crypto"], marker = "extra == 'server'", specifier = ">=2.12.0,<3" },
|
||||
{ name = "pyotp", marker = "extra == 'local'", specifier = ">=2.9.0,<3" },
|
||||
{ name = "pyotp", marker = "extra == 'server'", specifier = ">=2.9.0,<3" },
|
||||
{ name = "pypdf", marker = "extra == 'local'", specifier = ">=6.7.5,<7" },
|
||||
{ name = "pypdf", marker = "extra == 'server'", specifier = ">=6.7.5,<7" },
|
||||
{ name = "python-calamine", marker = "extra == 'local'", specifier = ">=0.6.1" },
|
||||
{ name = "python-calamine", marker = "extra == 'server'", specifier = ">=0.6.1" },
|
||||
{ name = "python-docx", marker = "extra == 'local'", specifier = ">=1.1.0" },
|
||||
{ name = "python-docx", marker = "extra == 'server'", specifier = ">=1.1.0" },
|
||||
{ name = "python-dotenv", specifier = ">=1.0.0,<2" },
|
||||
{ name = "python-dotenv", marker = "extra == 'server'", specifier = "==1.2.2" },
|
||||
{ name = "python-multipart", marker = "extra == 'server'", specifier = ">=0.0.22,<1" },
|
||||
{ name = "python-multipart", marker = "extra == 'local'", specifier = ">=0.0.27,<1" },
|
||||
{ name = "python-multipart", marker = "extra == 'server'", specifier = ">=0.0.27,<1" },
|
||||
{ name = "requests-toolbelt", marker = "extra == 'local'", specifier = ">=1.0.0,<2" },
|
||||
{ name = "requests-toolbelt", marker = "extra == 'server'", specifier = ">=1.0.0,<2" },
|
||||
{ name = "rich", specifier = ">=13.7.0,<14" },
|
||||
{ name = "sqlalchemy", marker = "extra == 'local'", specifier = ">=2.0.29,<3" },
|
||||
{ name = "sqlalchemy", extras = ["mypy"], marker = "extra == 'server'", specifier = ">=2.0.29,<3" },
|
||||
{ name = "sse-starlette", marker = "extra == 'server'", specifier = ">=3.0.3,<4" },
|
||||
{ name = "sse-starlette", marker = "extra == 'local'", specifier = ">=3.4.1,<4" },
|
||||
{ name = "sse-starlette", marker = "extra == 'server'", specifier = ">=3.4.1,<4" },
|
||||
{ name = "starlette-context", marker = "extra == 'local'", specifier = ">=0.3.6,<0.6" },
|
||||
{ name = "starlette-context", marker = "extra == 'server'", specifier = ">=0.3.6,<0.6" },
|
||||
{ name = "structlog", specifier = ">=23.2.0,<24" },
|
||||
{ name = "tiktoken", marker = "extra == 'local'", specifier = ">=0.9.0" },
|
||||
{ name = "tiktoken", marker = "extra == 'server'", specifier = ">=0.9.0" },
|
||||
{ name = "tldextract", marker = "extra == 'local'", specifier = ">=5.1.2,<6" },
|
||||
{ name = "tldextract", marker = "extra == 'server'", specifier = ">=5.1.2,<6" },
|
||||
{ name = "toml", marker = "extra == 'local'", specifier = ">=0.10.2,<0.11" },
|
||||
{ name = "toml", marker = "extra == 'server'", specifier = ">=0.10.2,<0.11" },
|
||||
{ name = "tornado", marker = "extra == 'local'", specifier = ">=6.5.5" },
|
||||
{ name = "tornado", marker = "extra == 'server'", specifier = ">=6.5.5" },
|
||||
{ name = "typer", specifier = ">=0.16.0" },
|
||||
{ name = "types-boto3", extras = ["full"], marker = "extra == 'server'", specifier = ">=1.38.31,<2" },
|
||||
{ name = "types-toml", marker = "extra == 'server'", specifier = ">=0.10.8.7,<0.11" },
|
||||
{ name = "urllib3", marker = "extra == 'local'", specifier = ">=2.6.3" },
|
||||
{ name = "urllib3", marker = "extra == 'server'", specifier = ">=2.6.3" },
|
||||
{ name = "uvicorn", extras = ["standard"], marker = "extra == 'server'", specifier = ">=0.35.0" },
|
||||
{ name = "uvicorn", extras = ["standard"], marker = "extra == 'server'", specifier = ">=0.44.0" },
|
||||
{ name = "websockets", marker = "extra == 'local'", specifier = ">=12.0,<15.1" },
|
||||
{ name = "websockets", marker = "extra == 'server'", specifier = ">=12.0,<15.1" },
|
||||
{ name = "zstandard", marker = "extra == 'local'", specifier = ">=0.25.0" },
|
||||
{ name = "zstandard", marker = "extra == 'server'", specifier = ">=0.25.0" },
|
||||
]
|
||||
provides-extras = ["server"]
|
||||
provides-extras = ["local", "server"]
|
||||
|
||||
[package.metadata.requires-dev]
|
||||
cloud = [
|
||||
{ name = "kr8s", specifier = ">=0.20.14,<1" },
|
||||
{ name = "openai-agents", specifier = ">=0.14.4,<0.15" },
|
||||
{ name = "openai-agents", specifier = ">=0.14.8,<0.15" },
|
||||
{ name = "opentelemetry-distro", specifier = ">=0.60b1,<1" },
|
||||
{ name = "opentelemetry-exporter-otlp-proto-grpc", specifier = ">=1.41.1,<2" },
|
||||
{ name = "opentelemetry-instrumentation-aiohttp-client", specifier = ">=0.60b1,<1" },
|
||||
|
|
@ -5735,7 +5848,7 @@ cloud = [
|
|||
{ name = "opentelemetry-instrumentation-urllib3", specifier = ">=0.60b1,<1" },
|
||||
{ name = "pyrate-limiter", specifier = ">=3.7.0,<4" },
|
||||
{ name = "redis", specifier = ">=5.0.3,<6" },
|
||||
{ name = "stripe", specifier = ">=9.7.0,<10" },
|
||||
{ name = "stripe", specifier = ">=15.0.1,<16" },
|
||||
{ name = "temporalio", specifier = ">=1.6.0,<2" },
|
||||
{ name = "temporalio", extras = ["opentelemetry"], specifier = ">=1.6.0,<2" },
|
||||
]
|
||||
|
|
@ -5752,7 +5865,7 @@ dev = [
|
|||
{ name = "isort", specifier = ">=5.13.2" },
|
||||
{ name = "moto", extras = ["s3", "server"], specifier = ">=5.1.5,<6" },
|
||||
{ name = "mypy", specifier = ">=1.18.2,<2" },
|
||||
{ name = "notebook", specifier = ">=7.0.6,<8" },
|
||||
{ name = "notebook", specifier = ">=7.5.6,<8" },
|
||||
{ name = "openpyxl", specifier = ">=3.1.0" },
|
||||
{ name = "opentelemetry-sdk", specifier = ">=1.39.0,<2" },
|
||||
{ name = "pre-commit", specifier = ">=4.2.0,<5" },
|
||||
|
|
@ -5840,15 +5953,15 @@ mypy = [
|
|||
|
||||
[[package]]
|
||||
name = "sse-starlette"
|
||||
version = "3.3.4"
|
||||
version = "3.4.1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "anyio" },
|
||||
{ name = "starlette" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/26/8c/f9290339ef6d79badbc010f067cd769d6601ec11a57d78569c683fb4dd87/sse_starlette-3.3.4.tar.gz", hash = "sha256:aaf92fc067af8a5427192895ac028e947b484ac01edbc3caf00e7e7137c7bef1", size = 32427, upload-time = "2026-03-29T09:00:23.307Z" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/e1/9a/f35932a8c0eb6b2287b66fa65a0321df8c84e4e355a659c1841a37c39fdb/sse_starlette-3.4.1.tar.gz", hash = "sha256:f780bebcf6c8997fe514e3bd8e8c648d8284976b391c8bed0bcb1f611632b555", size = 35127, upload-time = "2026-04-26T13:32:32.292Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/f8/7f/3de5402f39890ac5660b86bcf5c03f9d855dad5c4ed764866d7b592b46fd/sse_starlette-3.3.4-py3-none-any.whl", hash = "sha256:84bb06e58939a8b38d8341f1bc9792f06c2b53f48c608dd207582b664fc8f3c1", size = 14330, upload-time = "2026-03-29T09:00:21.846Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ff/07/45c21ed03d708c477367305726b89919b020a3a2a01f72aaf5ad941caf35/sse_starlette-3.4.1-py3-none-any.whl", hash = "sha256:6b43cf21f1d574d582a6e1b0cfbde1c94dc86a32a701a7168c99c4475c6bd1d0", size = 16487, upload-time = "2026-04-26T13:32:30.819Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
|
@ -5892,15 +6005,15 @@ wheels = [
|
|||
|
||||
[[package]]
|
||||
name = "stripe"
|
||||
version = "9.12.0"
|
||||
version = "15.0.1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "requests" },
|
||||
{ name = "typing-extensions" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/80/7c/edcc5afbbd1a8827f3435f4ab02bd5c43bf89abec9bc562633988a483cd8/stripe-9.12.0.tar.gz", hash = "sha256:cbc526abd0f001c920c323ba7c40cce3dee1647d920e9dbecae3488f37367524", size = 1277151, upload-time = "2024-06-17T16:21:47.167Z" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/90/b4/bd4baf56cfb9ec7ee66b0d33611e196afaabd12ddcc2322f29449649ec5f/stripe-15.0.1.tar.gz", hash = "sha256:3c04e8cc9ec8d9542f9dd998e2a4361dbe3d0b6150ca17d3f352546cf22c5565", size = 1490326, upload-time = "2026-04-01T23:52:21.218Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/28/a0/507b3197ca7c2edefa97258548a1a699751aa13f09dbcf3da09784814107/stripe-9.12.0-py2.py3-none-any.whl", hash = "sha256:14dd20ef1e6386a52e1f7aea07701fc6a80223706d72e3509e4966adb599e138", size = 1516569, upload-time = "2024-06-17T16:21:38.886Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e4/ee/4537ba64d6c2f8bc58667f4381429c804bbc2e854b5857337cbc39add620/stripe-15.0.1-py3-none-any.whl", hash = "sha256:0a525e4550b5bf7fb44ff682266b9d46cfb38dd6f1f97a85d361c452f69e6b4b", size = 2132160, upload-time = "2026-04-01T23:52:18.647Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
|
@ -6276,15 +6389,15 @@ wheels = [
|
|||
|
||||
[[package]]
|
||||
name = "uvicorn"
|
||||
version = "0.46.0"
|
||||
version = "0.44.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "click" },
|
||||
{ name = "h11" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/1f/93/041fca8274050e40e6791f267d82e0e2e27dd165627bd640d3e0e378d877/uvicorn-0.46.0.tar.gz", hash = "sha256:fb9da0926999cc6cb22dc7cd71a94a632f078e6ae47ff683c5c420750fb7413d", size = 88758, upload-time = "2026-04-23T07:16:00.151Z" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/5e/da/6eee1ff8b6cbeed47eeb5229749168e81eb4b7b999a1a15a7176e51410c9/uvicorn-0.44.0.tar.gz", hash = "sha256:6c942071b68f07e178264b9152f1f16dfac5da85880c4ce06366a96d70d4f31e", size = 86947, upload-time = "2026-04-06T09:23:22.826Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/31/a3/5b1562db76a5a488274b2332a97199b32d0442aca0ed193697fd47786316/uvicorn-0.46.0-py3-none-any.whl", hash = "sha256:bbebbcbed972d162afca128605223022bedd345b7bc7855ce66deb31487a9048", size = 70926, upload-time = "2026-04-23T07:15:58.355Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b7/23/a5bbd9600dd607411fa644c06ff4951bec3a4d82c4b852374024359c19c0/uvicorn-0.44.0-py3-none-any.whl", hash = "sha256:ce937c99a2cc70279556967274414c087888e8cec9f9c94644dfca11bd3ced89", size = 69425, upload-time = "2026-04-06T09:23:21.524Z" },
|
||||
]
|
||||
|
||||
[package.optional-dependencies]
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue