Files
wehub-resource-sync 426e9eeabd
Voice Workbench / headless workbench (mocked backends) (push) Has been cancelled
Voice Workbench / real acoustic lane (nightly, provisioned only) (push) Has been cancelled
ci / test (push) Has been cancelled
ci / lint-and-format (push) Has been cancelled
ci / build (push) Has been cancelled
ci / dev-startup (push) Has been cancelled
gitleaks / gitleaks (push) Has been cancelled
Markdown Links / Relative Markdown Links (push) Has been cancelled
Quality (Extended) / Homepage Build (PR smoke) (push) Has been cancelled
Quality (Extended) / Comment-only diff guard (push) Has been cancelled
Quality (Extended) / Format + Type Safety Ratchet (push) Has been cancelled
Quality (Extended) / Develop Gate (secret scan + UI determinism) (push) Has been cancelled
Quality (Extended) / Develop Gate (lint) (push) Has been cancelled
Chat shell gestures / Chat shell gesture + parity e2e (push) Has been cancelled
Cloud Gateway Discord / Test (push) Has been cancelled
Benchmark Bridge Tests / benchmark (bunx @biomejs/biome check packages/lifeops-bench/src, benchmark-lint) (push) Has been cancelled
Benchmark Bridge Tests / benchmark (bunx vitest run --config packages/lifeops-bench/vitest.config.ts --root packages/lifeops-bench --passWithNoTests, benchmark-tests) (push) Has been cancelled
Build Agent Image / build-and-push (push) Has been cancelled
Dev Smoke / bun run dev onboarding chat (push) Has been cancelled
Dev Smoke / Vite HMR dependency-level smoke (push) Has been cancelled
Electrobun Submodule Guard / electrobun gitlink is fetchable (push) Has been cancelled
Publish @elizaos/example-code / check_npm (push) Has been cancelled
Publish @elizaos/example-code / publish_npm (push) Has been cancelled
Publish @elizaos/plugin-elizacloud / verify_version (push) Has been cancelled
Publish @elizaos/plugin-elizacloud / publish_npm (push) Has been cancelled
Sandbox Live Smoke / Sandbox live smoke (push) Has been cancelled
Snap Build & Test / Build Snap (amd64) (push) Has been cancelled
Snap Build & Test / Build Snap (arm64) (push) Has been cancelled
Test Packaging / elizaos CLI global-install smoke (node + bun) (push) Has been cancelled
Cloud Gateway Webhook / Test (push) Has been cancelled
Cloud Tests / lint-and-types (push) Has been cancelled
Cloud Tests / unit-tests (push) Has been cancelled
Cloud Tests / integration-tests (push) Has been cancelled
Cloud Tests / e2e-tests (push) Has been cancelled
CodeQL Advanced / Analyze (javascript-typescript) (push) Has been cancelled
Deploy Apps Worker (Product 2) / Determine environment (push) Has been cancelled
Deploy Apps Worker (Product 2) / Deploy apps worker to apps-control host (${{ needs.determine-env.outputs.environment }}) (push) Has been cancelled
Deploy Eliza Provisioning Worker / Determine environment (push) Has been cancelled
Deploy Eliza Provisioning Worker / Deploy worker to Hetzner host (${{ needs.determine-env.outputs.environment }} @ ${{ needs.determine-env.outputs.deployment_sha }}) (push) Has been cancelled
Dev Smoke / Classify changed paths (push) Has been cancelled
supply-chain / sbom (push) Has been cancelled
supply-chain / vulnerability-scan (push) Has been cancelled
Build, Push & Deploy to Phala Cloud / build-and-push (push) Has been cancelled
Test Packaging / Validate Packaging Configs (push) Has been cancelled
Test Packaging / Build & Test PyPI Package (push) Has been cancelled
Test Packaging / PyPI on Python ${{ matrix.python }} (push) Has been cancelled
Test Packaging / Pack & Test JS Tarballs (push) Has been cancelled
UI Fixture E2E / ui-fixture-e2e (push) Has been cancelled
UI Fixture E2E / fixture-e2e (push) Has been cancelled
UI Story Gate / story-gate (push) Has been cancelled
vault-ci / test (macos-latest) (push) Has been cancelled
vault-ci / test (ubuntu-latest) (push) Has been cancelled
vault-ci / test (windows-latest) (push) Has been cancelled
vault-ci / app-core wiring tests (push) Has been cancelled
verify-patches / verify patches/CHECKSUMS.sha256 (push) Has been cancelled
Voice Benchmark Smoke / voice-emotion fixture smoke (push) Has been cancelled
Voice Benchmark Smoke / voiceagentbench fixture smoke (push) Has been cancelled
Voice Benchmark Smoke / voicebench-quality unit smoke (push) Has been cancelled
Voice Benchmark Smoke / voicebench TypeScript unit (no audio) (push) Has been cancelled
Voice Benchmark Smoke / voice bench smoke summary (push) Has been cancelled
Windows CI / windows ([bun run --cwd packages/app-core test bun run --cwd packages/elizaos test bun run --cwd packages/cloud/shared test], app-and-cli) (push) Has been cancelled
Windows CI / windows ([bun run --cwd packages/scenario-runner test bun run --cwd packages/vault test bun run --cwd packages/security test bun run --cwd plugins/plugin-coding-tools test], framework-packages) (push) Has been cancelled
Windows CI / windows ([bun run --cwd plugins/plugin-elizacloud test bun run --cwd plugins/plugin-discord test bun run --cwd plugins/plugin-anthropic test bun run --cwd plugins/plugin-openai test bun run --cwd plugins/plugin-app-control test bun run --cwd plugins/pl… (push) Has been cancelled
Windows CI / windows ([node packages/scripts/run-turbo.mjs run build --filter=@elizaos/core --filter=@elizaos/shared --filter=@elizaos/agent --concurrency=4 node packages/scripts/run-bash-linux-only.mjs scripts/verify-riscv64-buildpaths.sh node packages/scripts/run… (push) Has been cancelled
Windows CI / windows ([node packages/scripts/run-turbo.mjs run typecheck --filter=@elizaos/core --filter=@elizaos/shared --filter=@elizaos/cloud-shared --concurrency=4 bun run --cwd packages/core test bun run --cwd packages/shared test], core-runtime, 75) (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 12:43:05 +08:00

237 lines
7.8 KiB
Python

"""
REST HTTP runner for BFCL ``rest_api`` category.
Upstream BFCL REST tests target public HTTP endpoints (geocoding, weather
demos, etc.) and score by comparing the response against the expected
output. This is a faithful but lightweight executor — it uses ``httpx``
(synchronous client by default; async variant available) and:
* Hits the URL with the method/params/body/headers extracted from the
model's tool call.
* Returns a structured :class:`RESTResponse` (status code, parsed JSON,
raw text, elapsed seconds).
* 10-second per-call timeout.
* 429 responses raise :class:`RESTRateLimited` so the runner can bucket
them under ``SKIPPED_RATE_LIMITED`` instead of failing the test.
Gating: the runner refuses to make outbound calls unless constructed with
``enable_network=True``. Without it, every ``execute`` call raises
:class:`RESTExecutionError` and the caller is expected to translate that
into ``SKIPPED_NO_CREDENTIALS``.
Apache-2.0 attribution preserved for compatibility with upstream semantics.
"""
from __future__ import annotations
from dataclasses import dataclass, field
from time import perf_counter
from typing import Any, Optional
# httpx is an optional dep; we only require it when actually executing.
try: # pragma: no cover — import guard
import httpx as _httpx_module
_HAS_HTTPX = True
except ImportError: # pragma: no cover
_httpx_module = None # type: ignore[assignment]
_HAS_HTTPX = False
DEFAULT_TIMEOUT_SECONDS = 10.0
class RESTExecutionError(RuntimeError):
"""Raised when a REST call cannot be executed (network disabled,
missing httpx, validation error)."""
class RESTRateLimited(RuntimeError):
"""Raised when the upstream API returns HTTP 429. Caller should map
this to ``SKIPPED_RATE_LIMITED``."""
@dataclass
class RESTCallSpec:
"""A single REST request derived from a model tool call."""
method: str = "GET"
url: str = ""
params: dict[str, Any] = field(default_factory=dict)
headers: dict[str, str] = field(default_factory=dict)
json_body: Optional[Any] = None
data: Optional[Any] = None
timeout_seconds: float = DEFAULT_TIMEOUT_SECONDS
@classmethod
def from_call(
cls,
url: str,
method: str = "GET",
params: Optional[dict[str, Any]] = None,
headers: Optional[dict[str, str]] = None,
json_body: Optional[Any] = None,
data: Optional[Any] = None,
timeout_seconds: float = DEFAULT_TIMEOUT_SECONDS,
) -> "RESTCallSpec":
return cls(
method=method.upper(),
url=url,
params=dict(params or {}),
headers=dict(headers or {}),
json_body=json_body,
data=data,
timeout_seconds=timeout_seconds,
)
@dataclass
class RESTResponse:
"""Structured response returned by :class:`RESTRunner.execute`."""
status_code: int
json_body: Any
text: str
elapsed_seconds: float
headers: dict[str, str] = field(default_factory=dict)
def matches(self, expected: Any) -> bool:
"""Loose containment match against an expected JSON body.
Upstream's REST eval uses an expected-output comparison that's
either a full structural match or a substring match for text
responses. We implement the same semantics: dicts/lists must be
deep-equal (or contain expected as a subset), strings must appear
somewhere in the response text.
"""
if expected is None:
return True
if isinstance(expected, str):
return expected in self.text
if isinstance(expected, dict):
return _is_subset(expected, self.json_body)
if isinstance(expected, list):
return expected == self.json_body
return expected == self.json_body
def _is_subset(expected: Any, actual: Any) -> bool:
"""Deep-subset comparison: every key in ``expected`` exists in
``actual`` and matches. Used to make REST scoring tolerant of extra
fields that public APIs add over time."""
if isinstance(expected, dict):
if not isinstance(actual, dict):
return False
for k, v in expected.items():
if k not in actual:
return False
if not _is_subset(v, actual[k]):
return False
return True
if isinstance(expected, list):
if not isinstance(actual, list):
return False
if len(expected) != len(actual):
return False
return all(_is_subset(e, a) for e, a in zip(expected, actual))
return expected == actual
class RESTRunner:
"""Synchronous REST call executor with the gating + timeout policy
described in this module's docstring."""
def __init__(
self,
*,
enable_network: bool = False,
timeout_seconds: float = DEFAULT_TIMEOUT_SECONDS,
client: Optional[Any] = None,
) -> None:
self.enable_network = enable_network
self.timeout_seconds = timeout_seconds
# Allow the caller to inject a pre-built httpx.Client (used in
# tests with a MockTransport). When ``client`` is None we lazily
# build one on first use.
self._client = client
def _ensure_client(self) -> Any:
if self._client is not None:
return self._client
if not _HAS_HTTPX:
raise RESTExecutionError(
"REST execution requires the `httpx` package. "
"Install with `pip install httpx`."
)
self._client = _httpx_module.Client(timeout=self.timeout_seconds)
return self._client
def execute(self, spec: RESTCallSpec) -> RESTResponse:
"""Make the request. Raises :class:`RESTExecutionError` if the
runner was constructed without ``enable_network`` (the caller
should pre-check ``enable_network`` and bucket as
``SKIPPED_NO_CREDENTIALS`` rather than relying on this exception
in the hot path)."""
if not self.enable_network:
raise RESTExecutionError(
"Network is disabled. Pass enable_network=True to allow "
"live REST execution."
)
client = self._ensure_client()
started = perf_counter()
try:
response = client.request(
spec.method,
spec.url,
params=spec.params or None,
headers=spec.headers or None,
json=spec.json_body,
data=spec.data,
timeout=spec.timeout_seconds,
)
except Exception as exc:
# httpx-specific timeout / connect / decode errors all funnel
# through here. We surface them as RESTExecutionError so the
# caller can fail the test (not skip it — a network error on
# an opt-in run is a real signal).
raise RESTExecutionError(f"REST request failed: {exc}") from exc
elapsed_seconds = perf_counter() - started
if response.status_code == 429:
raise RESTRateLimited(
f"Rate-limited (HTTP 429) by {spec.url}"
)
try:
json_body = response.json()
except Exception:
json_body = None
return RESTResponse(
status_code=response.status_code,
json_body=json_body,
text=response.text,
elapsed_seconds=elapsed_seconds,
headers=dict(response.headers),
)
def close(self) -> None:
if self._client is not None and hasattr(self._client, "close"):
try:
self._client.close()
except Exception:
pass
self._client = None
__all__ = [
"DEFAULT_TIMEOUT_SECONDS",
"RESTCallSpec",
"RESTExecutionError",
"RESTRateLimited",
"RESTResponse",
"RESTRunner",
]