diff --git a/.github/scripts/verify-wheel-install.py b/.github/scripts/verify-wheel-install.py new file mode 100644 index 00000000..7106ce31 --- /dev/null +++ b/.github/scripts/verify-wheel-install.py @@ -0,0 +1,531 @@ +#!/usr/bin/env python3 +"""Verify an isolated installation of an osmosis-ai wheel. + +CI executes this from a temporary directory with ``python -I`` and +``PYTHONPATH`` removed so a source checkout cannot satisfy any imports. Apart +from ``packaging`` (a declared base dependency used to inspect wheel metadata), +the verifier uses only the standard library. +""" + +from __future__ import annotations + +import argparse +import importlib +import importlib.metadata +import os +import re +import subprocess +import sys +from collections.abc import Callable, Iterable +from pathlib import Path +from typing import Any + +from packaging.markers import default_environment +from packaging.requirements import Requirement + +BASE_REQUIREMENTS = { + "cryptography", + "httpx", + "keyring", + "packaging", + "prompt-toolkit", + "pydantic", + "python-dotenv", + "questionary", + "requests", + "rich", + "typer", +} + +EXTRA_REQUIREMENTS: dict[str, set[str]] = { + "server": {"click", "fastapi", "uvicorn"}, + "strands": { + "aiohttp", + "click", + "litellm", + "mcp", + "orjson", + "strands-agents", + }, + "openai-agents": { + "aiohttp", + "click", + "litellm", + "mcp", + "openai-agents", + "orjson", + }, + "harbor": { + "aiohttp", + "click", + "dockerfile-parse", + "harbor", + "litellm", + "orjson", + "toml", + }, + "rubric": {"aiohttp", "click", "litellm", "orjson", "tqdm"}, + "parquet": {"pyarrow"}, + "full": {"osmosis-ai"}, +} + +_ANSI_ESCAPE_PATTERN = re.compile( + r"\x1b(?:\[[0-?]*[ -/]*[@-~]|\][^\x1b\x07]*(?:\x07|\x1b\\)|[@-Z\\-_])" +) + + +def _normalize_distribution(name: str) -> str: + """Return the normalized project name used for metadata comparisons.""" + return re.sub(r"[-_.]+", "-", name).lower() + + +def _strip_terminal_escapes(value: str) -> str: + """Remove ANSI styling and hyperlinks before semantic CLI assertions.""" + return _ANSI_ESCAPE_PATTERN.sub("", value) + + +def _installed_distributions() -> set[str]: + return { + _normalize_distribution(distribution.metadata["Name"]) + for distribution in importlib.metadata.distributions() + if distribution.metadata["Name"] + } + + +def _assert_distributions( + installed: set[str], + *, + present: Iterable[str] = (), + absent: Iterable[str] = (), +) -> None: + expected = {_normalize_distribution(name) for name in present} + forbidden = {_normalize_distribution(name) for name in absent} + missing = sorted(expected - installed) + unexpected = sorted(forbidden & installed) + assert not missing, f"Expected distributions are missing: {', '.join(missing)}" + assert not unexpected, "Unselected distributions were installed: " + ", ".join( + unexpected + ) + + +def _assert_dependency_metadata( + distribution: importlib.metadata.Distribution, +) -> None: + """Verify that every direct dependency belongs to its intended feature.""" + provided_extras = { + _normalize_distribution(extra) + for extra in distribution.metadata.get_all("Provides-Extra", []) + } + assert provided_extras == set(EXTRA_REQUIREMENTS), ( + "Unexpected Provides-Extra metadata: " + f"expected={sorted(EXTRA_REQUIREMENTS)}, actual={sorted(provided_extras)}" + ) + + actual_base: set[str] = set() + actual_extras = {extra: set() for extra in EXTRA_REQUIREMENTS} + full_self_references: list[Requirement] = [] + for raw_requirement in distribution.requires or []: + requirement = Requirement(raw_requirement) + name = _normalize_distribution(requirement.name) + if requirement.marker is None: + actual_base.add(name) + continue + + matched_extras: list[str] = [] + for extra in EXTRA_REQUIREMENTS: + environment = default_environment() + environment["extra"] = extra + if requirement.marker.evaluate(environment): + actual_extras[extra].add(name) + matched_extras.append(extra) + assert matched_extras, ( + "A marked requirement is not owned by any declared extra: " + f"{raw_requirement}" + ) + if name == "osmosis-ai" and "full" in matched_extras: + full_self_references.append(requirement) + + assert actual_base == BASE_REQUIREMENTS, ( + "Unexpected base dependency metadata: " + f"expected={sorted(BASE_REQUIREMENTS)}, actual={sorted(actual_base)}" + ) + assert actual_extras == EXTRA_REQUIREMENTS, ( + "Unexpected extra dependency metadata: " + f"expected={EXTRA_REQUIREMENTS}, actual={actual_extras}" + ) + assert len(full_self_references) == 1, ( + "The full extra must contain exactly one osmosis-ai self-reference" + ) + full_members = { + _normalize_distribution(extra) for extra in full_self_references[0].extras + } + expected_full_members = set(EXTRA_REQUIREMENTS) - {"full"} + assert full_members == expected_full_members, ( + "Unexpected full-extra members: " + f"expected={sorted(expected_full_members)}, actual={sorted(full_members)}" + ) + + +def _assert_public_exports(module_name: str, symbols: Iterable[str]) -> None: + module = importlib.import_module(module_name) + exported = set(getattr(module, "__all__", ())) + for symbol in symbols: + value = getattr(module, symbol) + assert value is not None, ( + f"{module_name}.{symbol} unexpectedly resolved to None" + ) + assert symbol in exported, f"{module_name}.{symbol} is missing from __all__" + + +def _smoke_bare() -> None: + osmosis_ai = importlib.import_module("osmosis_ai") + assert "evaluate_rubric" not in osmosis_ai.__all__ + star_namespace: dict[str, Any] = {} + exec("from osmosis_ai import *", star_namespace) + assert "RubricResult" in star_namespace + assert "evaluate_rubric" not in star_namespace + + _assert_public_exports( + "osmosis_ai.rollout", + ( + "AgentWorkflow", + "ExecutionBackend", + "Grader", + "LocalBackend", + "SampleSource", + ), + ) + atif = importlib.import_module("osmosis_ai.rollout.trajectory.atif") + assert atif.Trajectory is not None + + +def _smoke_server() -> None: + _assert_public_exports( + "osmosis_ai.rollout.server", + ("ControllerAuth", "create_rollout_server"), + ) + + +def _smoke_strands() -> None: + _assert_public_exports( + "osmosis_ai.rollout.integrations.agents.strands", + ("OsmosisRolloutModel", "OsmosisStrandsAgent"), + ) + + +def _smoke_openai_agents() -> None: + _assert_public_exports( + "osmosis_ai.rollout.integrations.agents.openai_agents", + ( + "OsmosisAgent", + "OsmosisLitellmModel", + "OsmosisMemorySession", + "OsmosisRolloutModel", + "SessionSampleSource", + ), + ) + + +def _smoke_harbor() -> None: + _assert_public_exports( + "osmosis_ai.rollout.backend.harbor", + ("HarborBackend", "OsmosisInstalledAgent"), + ) + + +def _smoke_rubric() -> None: + _assert_public_exports( + "osmosis_ai.eval.rubric", + ( + "MissingAPIKeyError", + "ModelNotFoundError", + "ProviderRequestError", + "RubricResult", + "evaluate_rubric", + ), + ) + + +def _smoke_parquet() -> None: + importlib.import_module("pyarrow.parquet") + dataset = importlib.import_module("osmosis_ai.platform.cli.dataset") + assert callable(dataset.validate) + + +SCENARIO_SMOKE: dict[str, Callable[[], None]] = { + "bare": _smoke_bare, + "server": _smoke_server, + "strands": _smoke_strands, + "openai-agents": _smoke_openai_agents, + "harbor": _smoke_harbor, + "rubric": _smoke_rubric, + "parquet": _smoke_parquet, +} + +SCENARIO_PRESENT: dict[str, set[str]] = { + "bare": set(), + "server": {"fastapi", "uvicorn"}, + "strands": {"litellm", "strands-agents"}, + "openai-agents": {"litellm", "openai-agents"}, + "harbor": {"dockerfile-parse", "harbor", "toml"}, + "rubric": {"litellm", "orjson", "tqdm"}, + "parquet": {"pyarrow"}, + "full": { + "fastapi", + "dockerfile-parse", + "harbor", + "litellm", + "openai-agents", + "orjson", + "pyarrow", + "strands-agents", + "toml", + "tqdm", + "uvicorn", + }, +} + +# These are feature-owner distributions, not incidental transitive packages. +# Harbor itself currently depends on FastAPI and LiteLLM, for example, so those +# cannot be used to infer whether the server/rubric extras were selected there. +SCENARIO_ABSENT: dict[str, set[str]] = { + "bare": { + "dockerfile-parse", + "fastapi", + "harbor", + "litellm", + "openai-agents", + "orjson", + "pyarrow", + "strands-agents", + "tqdm", + "toml", + "uvicorn", + }, + "server": { + "dockerfile-parse", + "harbor", + "litellm", + "openai-agents", + "orjson", + "pyarrow", + "strands-agents", + "toml", + "tqdm", + }, + "strands": {"dockerfile-parse", "harbor", "openai-agents", "pyarrow", "toml"}, + "openai-agents": { + "dockerfile-parse", + "harbor", + "pyarrow", + "strands-agents", + "toml", + }, + "harbor": {"openai-agents", "pyarrow", "strands-agents"}, + "rubric": { + "dockerfile-parse", + "fastapi", + "harbor", + "openai-agents", + "pyarrow", + "strands-agents", + "toml", + "uvicorn", + }, + "parquet": { + "dockerfile-parse", + "fastapi", + "harbor", + "litellm", + "openai-agents", + "orjson", + "strands-agents", + "toml", + "tqdm", + "uvicorn", + }, + "full": set(), +} + +# Sandbox runtimes are supplied by the remote rollout environment. The wheel +# must never pull retired Daytona or either SkyPilot distribution. +PROHIBITED_SANDBOX_DISTRIBUTIONS = {"daytona", "skypilot", "skypilot-nightly"} + + +def _assert_clean_import_state() -> None: + """Ensure importing rollout core did not initialize optional/CLI modules.""" + forbidden_prefixes = ( + "agents", + "aiohttp", + "click", + "dockerfile_parse", + "dotenv", + "fastapi", + "harbor", + "keyring", + "litellm", + "mcp", + "openai", + "orjson", + "prompt_toolkit", + "pyarrow", + "questionary", + "rich", + "strands", + "toml", + "tqdm", + "typer", + "uvicorn", + "osmosis_ai.cli", + "osmosis_ai.eval.rubric", + "osmosis_ai.platform", + "osmosis_ai.rollout.backend.harbor", + "osmosis_ai.rollout.integrations", + "osmosis_ai.rollout.server", + ) + loaded = sorted( + module_name + for module_name in sys.modules + if any( + module_name == prefix or module_name.startswith(f"{prefix}.") + for prefix in forbidden_prefixes + ) + ) + assert not loaded, "Rollout core eagerly loaded optional/CLI modules: " + ", ".join( + loaded + ) + + +def _assert_wheel_identity( + source_root: Path, +) -> tuple[Any, importlib.metadata.Distribution]: + assert sys.flags.isolated == 1, "Smoke verification must run with python -I" + assert "PYTHONPATH" not in os.environ, "PYTHONPATH must be removed for wheel smoke" + + cwd = Path.cwd().resolve() + source_root = source_root.resolve() + assert cwd != source_root and source_root not in cwd.parents, ( + f"Smoke verification must run outside the source checkout (cwd={cwd})" + ) + + osmosis_ai = importlib.import_module("osmosis_ai") + distribution = importlib.metadata.distribution("osmosis-ai") + module_path = Path(osmosis_ai.__file__).resolve() + metadata_path = Path(distribution.locate_file("osmosis_ai/__init__.py")).resolve() + + assert module_path == metadata_path, ( + "Imported osmosis_ai does not belong to the installed distribution: " + f"module={module_path}, metadata={metadata_path}" + ) + assert source_root not in module_path.parents, ( + f"Imported osmosis_ai from the source checkout: {module_path}" + ) + assert osmosis_ai.__version__ == distribution.version + return osmosis_ai, distribution + + +def _assert_cli_aliases(version: str) -> None: + expected = f"osmosis-ai {version}" + for alias in ("osmosis", "osmosis_ai", "osmosis-ai"): + executable = Path(sys.executable).parent / alias + assert executable.is_file(), f"Missing console script: {executable}" + result = subprocess.run( + [str(executable), "--version"], + check=True, + capture_output=True, + text=True, + timeout=30, + ) + assert result.stdout.strip() == expected, ( + f"Unexpected `{alias} --version` output: {result.stdout!r}" + ) + assert not result.stderr, ( + f"Unexpected `{alias} --version` stderr: {result.stderr!r}" + ) + + # Command registration touches every top-level CLI group. This catches a + # missing optional dependency that a short-circuited --version path would + # otherwise miss while keeping the three alias checks inexpensive. + help_result = subprocess.run( + [str(Path(sys.executable).parent / "osmosis"), "--help"], + check=True, + capture_output=True, + env={**os.environ, "FORCE_COLOR": "1"}, + text=True, + timeout=30, + ) + help_stdout = _strip_terminal_escapes(help_result.stdout) + assert "Usage: osmosis" in help_stdout, ( + f"Unexpected `osmosis --help` stdout: {help_result.stdout!r}" + ) + assert "Osmosis AI CLI." in help_stdout, ( + f"Unexpected `osmosis --help` stdout: {help_result.stdout!r}" + ) + assert not help_result.stderr, ( + f"Unexpected `osmosis --help` stderr: {help_result.stderr!r}" + ) + + rubric_result = subprocess.run( + [ + str(Path(sys.executable).parent / "osmosis"), + "eval", + "rubric", + "--data", + "/does-not-exist.jsonl", + "--rubric", + "score correctness", + "--model", + "openai/example", + ], + check=False, + capture_output=True, + text=True, + timeout=30, + ) + assert rubric_result.returncode == 1 + assert 'pip install "osmosis-ai[rubric]"' in rubric_result.stderr + + +def _parse_args() -> argparse.Namespace: + parser = argparse.ArgumentParser() + parser.add_argument("--scenario", required=True, choices=(*SCENARIO_SMOKE, "full")) + parser.add_argument("--source-root", required=True, type=Path) + return parser.parse_args() + + +def main() -> None: + args = _parse_args() + _, distribution = _assert_wheel_identity(args.source_root) + _assert_dependency_metadata(distribution) + + importlib.import_module("osmosis_ai.rollout") + _assert_clean_import_state() + + installed = _installed_distributions() + present = SCENARIO_PRESENT.get(args.scenario, set()) + absent = ( + SCENARIO_ABSENT.get(args.scenario, set()) | PROHIBITED_SANDBOX_DISTRIBUTIONS + ) + _assert_distributions( + installed, + present={"osmosis-ai", *present}, + absent=absent, + ) + + if args.scenario == "full": + for smoke in SCENARIO_SMOKE.values(): + smoke() + else: + SCENARIO_SMOKE[args.scenario]() + + if args.scenario == "bare": + _assert_cli_aliases(distribution.version) + + print( + f"Verified osmosis-ai {distribution.version} " + f"({args.scenario}, {len(installed)} distributions)" + ) + + +if __name__ == "__main__": + main() diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index a4de75b2..7212c8e9 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -40,7 +40,7 @@ jobs: python-version: "3.12" enable-cache: true - name: Install dependencies - run: uv sync --locked --extra dev + run: uv sync --locked --all-extras --group dev - name: Run pyright run: uv run pyright osmosis_ai/ - name: Verify public API types @@ -50,13 +50,10 @@ jobs: # Known --verifytypes baselines we intentionally skip: # * agent_adapter: inherits from harbor's BaseInstalledAgent which # has no type stubs ("base class unknown"). - # * convert_sample_to_trajectory: returns harbor's Trajectory which - # has no type stubs ("return type is unknown"). unexpected=$(echo "$output" | awk ' /^osmosis_ai\./ { sym = $0 } /error:/ { if (sym ~ /agent_adapter/) next - if (sym ~ /convert_sample_to_trajectory/) next print $0 } ') @@ -82,7 +79,7 @@ jobs: enable-cache: true - name: Install dependencies - run: uv sync --locked --extra dev + run: uv sync --locked --all-extras --group dev - name: Run tests with coverage if: matrix.python-version == '3.12' @@ -150,22 +147,73 @@ jobs: sys.exit(1) PY - - name: Smoke-test the built wheel - run: | - uv venv --python 3.12 /tmp/osmosis-wheel-smoke - uv pip install --python /tmp/osmosis-wheel-smoke/bin/python dist/*.whl - /tmp/osmosis-wheel-smoke/bin/python - <<'PY' - import osmosis_ai - from agents.usage import Usage - from osmosis_ai.rollout.integrations.agents.openai_agents import OsmosisLitellmModel - - assert Usage().input_tokens_details.cached_tokens == 0 - print(osmosis_ai.__version__, OsmosisLitellmModel.__name__) - PY - /tmp/osmosis-wheel-smoke/bin/osmosis --version - - name: Upload dist artifact uses: actions/upload-artifact@v7 with: name: dist path: dist/ + + wheel-smoke: + name: Wheel smoke (${{ matrix.scenario }}) + needs: build + runs-on: ubuntu-latest + timeout-minutes: 15 + strategy: + fail-fast: false + matrix: + include: + - scenario: bare + extra: "" + - scenario: server + extra: "[server]" + - scenario: strands + extra: "[strands]" + - scenario: openai-agents + extra: "[openai-agents]" + - scenario: harbor + extra: "[harbor]" + - scenario: rubric + extra: "[rubric]" + - scenario: parquet + extra: "[parquet]" + - scenario: full + extra: "[full]" + steps: + - uses: actions/checkout@v7 + + - uses: astral-sh/setup-uv@v7 + with: + python-version: "3.12" + enable-cache: true + + - name: Download built wheel + uses: actions/download-artifact@v8 + with: + name: dist + path: dist/ + + - name: Install and verify wheel + env: + INSTALL_EXTRA: ${{ matrix.extra }} + SMOKE_SCENARIO: ${{ matrix.scenario }} + run: | + mapfile -t wheels < <(find "$GITHUB_WORKSPACE/dist" -maxdepth 1 -type f -name '*.whl' -print | sort) + if [ "${#wheels[@]}" -ne 1 ]; then + echo "::error::Expected exactly one wheel, found ${#wheels[@]}" + exit 1 + fi + + smoke_root=$(mktemp -d) + python="$smoke_root/venv/bin/python" + install_spec="${wheels[0]}${INSTALL_EXTRA}" + + uv venv --python 3.12 "$smoke_root/venv" + ( + cd "$smoke_root" + env -u PYTHONPATH uv pip install --python "$python" "$install_spec" + env -u PYTHONPATH uv pip check --python "$python" + env -u PYTHONPATH "$python" -I \ + "$GITHUB_WORKSPACE/.github/scripts/verify-wheel-install.py" \ + --scenario "$SMOKE_SCENARIO" \ + --source-root "$GITHUB_WORKSPACE" + ) diff --git a/CHANGELOG.md b/CHANGELOG.md index b873d570..825a4e64 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,21 @@ This file records changes to `osmosis-ai`. For earlier versions, see [GitHub Releases](https://github.com/Osmosis-AI/osmosis-sdk-python/releases). +## Unreleased + +### Breaking Changes + +- Runtime features now use independent installation extras: `server`, `strands`, `openai-agents`, `harbor`, `rubric`, and `parquet`; `full` installs all of them. The former `platform` extra is replaced by `parquet`. +- Development tools are no longer published through the `dev` extra. From a source checkout, install the PEP 735 dependency group with `uv sync --all-extras --group dev` or `python -m pip install -e ".[full]" --group dev`. +- Rollout feature imports moved out of `osmosis_ai.rollout`: + - Server: `from osmosis_ai.rollout.server import create_rollout_server, ControllerAuth` + - Strands: `from osmosis_ai.rollout.integrations.agents.strands import OsmosisStrandsAgent, OsmosisRolloutModel` + - OpenAI Agents: `from osmosis_ai.rollout.integrations.agents.openai_agents import OsmosisAgent` + - Harbor: `from osmosis_ai.rollout.backend.harbor import HarborBackend` + - Harbor workflow context: `from osmosis_ai.rollout.context import HarborAgentWorkflowContext` +- `evaluate_rubric` is no longer included by `from osmosis_ai import *`. Import it explicitly from `osmosis_ai.eval.rubric` and install the `rubric` extra. +- The Harbor extra no longer installs Daytona or SkyPilot. Daytona support is retired; the rollout runtime must provide SkyPilot when it is used. + ## 0.3.0rc1 - 2026-07-28 ### Breaking Changes diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 74803b26..4eb5a4b2 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -7,7 +7,7 @@ ```bash git clone https://github.com/Osmosis-AI/osmosis-sdk-python cd osmosis-sdk-python -uv sync --extra dev +uv sync --all-extras --group dev pre-commit install uv run pytest ``` @@ -18,7 +18,8 @@ uv run pytest git clone https://github.com/Osmosis-AI/osmosis-sdk-python cd osmosis-sdk-python python -m venv .venv && source .venv/bin/activate -pip install -e ".[dev]" +python -m pip install --upgrade pip +python -m pip install -e ".[full]" --group dev pre-commit install pytest ``` @@ -52,9 +53,11 @@ Ruff is pinned to one version across `pyproject.toml`, `.pre-commit-config.yaml` ## Type Checking -[Pyright](https://microsoft.github.io/pyright/) is the type checker, included in the `dev` extras. +[Pyright](https://microsoft.github.io/pyright/) is the type checker, included in +the `dev` dependency group. -- **Pyright** — must pass. All errors must be resolved before merging. +- **Pyright** — must pass. All errors must be resolved before merging. It is + installed from the `dev` dependency group. - **Pyright `--verifytypes`** — must pass. Ensures all public API symbols have complete type annotations. Configuration lives in `pyproject.toml` under `[tool.pyright]`. diff --git a/README.md b/README.md index 1f6c5a03..e0bbee11 100644 --- a/README.md +++ b/README.md @@ -26,12 +26,18 @@ Python SDK and CLI for [Osmosis AI](https://platform.osmosis.ai), a platform for Requires **Python 3.12+**. ```bash -pip install osmosis-ai # Core SDK -pip install osmosis-ai[server] # + FastAPI rollout server +pip install osmosis-ai # CLI + framework-neutral rollout core +pip install "osmosis-ai[server]" # + generic FastAPI rollout server +pip install "osmosis-ai[strands]" # + Strands integration +pip install "osmosis-ai[openai-agents]" # + OpenAI Agents integration +pip install "osmosis-ai[harbor]" # + Harbor backend (uses an externally provided SkyPilot runtime) +pip install "osmosis-ai[rubric]" # + LLM-as-judge rubric evaluation +pip install "osmosis-ai[parquet]" # + Parquet dataset support +pip install "osmosis-ai[full]" # every optional feature # or with uv: uv add osmosis-ai ``` -See [Installation](https://docs.osmosis.ai/cli/installation) for the full extras matrix and [CONTRIBUTING.md](CONTRIBUTING.md) for development setup. +There is one distribution, `osmosis-ai`. The `harbor` extra installs plain Harbor only: Daytona is retired, and Harbor's `skypilot` extra must not be installed because the rollout runtime provides SkyPilot. See [Installation](https://docs.osmosis.ai/cli/installation) for product setup and [CONTRIBUTING.md](CONTRIBUTING.md) for development setup. ## Documentation diff --git a/docs/README.md b/docs/README.md index 8fd28e3b..3339e573 100644 --- a/docs/README.md +++ b/docs/README.md @@ -23,10 +23,12 @@ The package (`osmosis_ai/`) is organized into top-level domains. See [architectu | Eval helpers | [../osmosis_ai/eval/](../osmosis_ai/eval/) | Rubric (LLM-as-judge) + workflow/grader loader | `from osmosis_ai.eval.rubric import evaluate_rubric` | | Workspace templates | [../osmosis_ai/templates/](../osmosis_ai/templates/) | `osmosis template` recipe catalog + source resolution | (internal) | +The single `osmosis-ai` distribution always includes the CLI and framework-neutral rollout core. Install extras only for the feature you use: `server`, `strands`, `openai-agents`, `harbor`, `rubric`, `parquet`, or `full`. The Harbor extra installs plain Harbor for an externally provided SkyPilot runtime; Daytona is retired, and Harbor's `skypilot` extra is intentionally unsupported. + ## Pages - [architecture.md](./architecture.md) — package layout, domain boundaries, import paths, lazy-loading rules, and the remote rollout protocol (controller <-> rollout server). Start here. -- [rollout-sdk.md](./rollout-sdk.md) — the library API you implement against: `AgentWorkflow`, `Grader`, contexts, configs, `create_rollout_server`, execution backends, and framework integrations. +- [rollout-sdk.md](./rollout-sdk.md) — the library API you implement against: `AgentWorkflow`, `Grader`, contexts, configs, server/backends, and framework integrations. - [eval.md](./eval.md) — the `osmosis eval submit` config contract (SDK-vs-backend validation, submit flow), plus a brief note on the `evaluate_rubric` / `osmosis eval rubric` LLM-as-judge API. - [datasets.md](./datasets.md) — the dataset row contract enforced by the SDK validator. - [troubleshooting.md](./troubleshooting.md) — engineering issues (rollout timeouts, event-loop blocking, concurrency tuning). diff --git a/docs/architecture.md b/docs/architecture.md index ea92a81b..78eb1e1a 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -22,10 +22,10 @@ osmosis_ai/ │ ├── context.py # RolloutContext / AgentWorkflowContext / GraderContext │ ├── driver.py # RolloutDriver — eval-facing execution contract │ ├── validator.py # Static backend validation -│ ├── server/ # create_rollout_server (FastAPI) + ControllerAuth -│ ├── backend/ # ExecutionBackend ABC + Local / Harbor backends +│ ├── server/ # optional generic FastAPI server (`[server]`) +│ ├── backend/ # ExecutionBackend ABC + Local / optional Harbor backend │ ├── types/ # protocol.py, config.py, sample.py -│ └── integrations/ # Strands / OpenAI Agents adapters +│ └── integrations/agents/ # Strands / OpenAI Agents adapters ├── eval/ # Eval helpers │ ├── rubric/ # evaluate_rubric() LLM-as-judge engine │ └── common/cli.py # Workflow + grader loader (used by cloud submit preflight) @@ -39,7 +39,7 @@ osmosis_ai/ - `cli/` — the CLI framework layer plus every command group. Files in [../osmosis_ai/cli/commands/](../osmosis_ai/cli/commands/) are thin shells that delegate to business logic; see [cli.md](./cli.md). - `platform/` — anything that calls the Osmosis Platform API. Business-logic helpers (no Typer registration) live in [../osmosis_ai/platform/cli/](../osmosis_ai/platform/cli/). -- `rollout/` — the remote rollout protocol SDK: the `AgentWorkflow` + `Grader` abstraction, execution backends, and the FastAPI server. See [rollout-sdk.md](./rollout-sdk.md). +- `rollout/` — the remote rollout protocol SDK: the `AgentWorkflow` + `Grader` abstraction and framework-neutral execution core. The generic FastAPI server and framework/back-end adapters are explicit optional modules; see [rollout-sdk.md](./rollout-sdk.md). - `eval/` — `rubric/` powers `osmosis eval rubric` (see [eval.md](./eval.md)); `common/cli.py` exposes the workflow + grader loader that cloud `eval submit` / `train submit` preflight uses. ## Key import paths @@ -49,10 +49,14 @@ from osmosis_ai.cli.errors import CLIError from osmosis_ai.cli.console import Console from osmosis_ai.platform.auth import load_credentials from osmosis_ai.eval.rubric import evaluate_rubric, RubricResult -from osmosis_ai.rollout import AgentWorkflow, Grader, create_rollout_server +from osmosis_ai.rollout import AgentWorkflow, Grader, LocalBackend, SampleSource +from osmosis_ai.rollout.server import create_rollout_server +from osmosis_ai.rollout.backend.harbor import HarborBackend +from osmosis_ai.rollout.integrations.agents.strands import OsmosisStrandsAgent +from osmosis_ai.rollout.integrations.agents.openai_agents import OsmosisAgent ``` -`osmosis_ai.rollout` is **not** re-exported at the package top level — import it directly. +`osmosis_ai.rollout` is **not** re-exported at the package top level — import it directly. Its public surface is framework-neutral core only; it does not export the server or Strands integration. `server`, `harbor`, `strands`, and `openai-agents` each require their matching installation extra. The generic server has no Harbor dependency. ## Lazy loading diff --git a/docs/rollout-sdk.md b/docs/rollout-sdk.md index 5b1685f6..253cb553 100644 --- a/docs/rollout-sdk.md +++ b/docs/rollout-sdk.md @@ -2,22 +2,31 @@ > The library API you implement against. Anchored to [../osmosis_ai/rollout/__init__.py](../osmosis_ai/rollout/__init__.py). For how rollouts run end to end see [architecture.md](./architecture.md); for usage and the `osmosis rollout` CLI see [docs.osmosis.ai](https://docs.osmosis.ai/cli/rollout/overview). -A rollout has two halves you provide: an `AgentWorkflow` (the agent loop) and a `Grader` (turns the trajectory into rewards). The SDK runs them behind an execution backend and the FastAPI server. +A rollout has two halves you provide: an `AgentWorkflow` (the agent loop) and a `Grader` (turns the trajectory into rewards). The framework-neutral core runs them behind an execution backend; install the `server` extra when you also need the FastAPI server. ## Public surface -Everything below is re-exported from `osmosis_ai.rollout` unless noted. +`osmosis_ai.rollout` exports framework-neutral core only. Server, Harbor, and framework integrations have explicit import paths and installation extras. | Symbol | Source | Purpose | |--------|--------|---------| | `AgentWorkflow` | [agent_workflow.py](../osmosis_ai/rollout/agent_workflow.py) | ABC you subclass; implement `async run(ctx)` | | `Grader` | [grader.py](../osmosis_ai/rollout/grader.py) | ABC you subclass; implement `async grade(ctx)` | -| `AgentWorkflowContext`, `HarborAgentWorkflowContext`, `GraderContext`, `RolloutContext`, `get_rollout_context` | [context.py](../osmosis_ai/rollout/context.py) | Execution context passed to `run` / `grade` | +| `AgentWorkflowContext`, `GraderContext`, `RolloutContext`, `SampleSource`, `get_rollout_context` | [context.py](../osmosis_ai/rollout/context.py) | Execution contexts and sample-source contract | | `AgentWorkflowConfig`, `GraderConfig`, `ConcurrencyConfig` | [types/config.py](../osmosis_ai/rollout/types/config.py) | Pydantic config models | -| `RolloutSample`, `RolloutStatus`, `RolloutErrorCategory`, `MultiTurnMode` | [types/sample.py](../osmosis_ai/rollout/types/sample.py) | Sample + status types | -| `create_rollout_server`, `ControllerAuth` | [server/](../osmosis_ai/rollout/server/) | FastAPI factory + bearer auth | +| `RolloutSample`, `RolloutStatus`, `RolloutErrorCategory` | [types/sample.py](../osmosis_ai/rollout/types/sample.py) | Sample + status types | | `ExecutionBackend`, `LocalBackend` | [backend/](../osmosis_ai/rollout/backend/) | Execution backends | -| `OsmosisStrandsAgent`, `OsmosisRolloutModel` | [integrations/agents/strands.py](../osmosis_ai/rollout/integrations/agents/strands.py) | Strands integration | + +Optional features use these canonical modules: + +| Extra | Import | Purpose | +|-------|--------|---------| +| `server` | `from osmosis_ai.rollout.server import create_rollout_server, ControllerAuth` | Generic FastAPI rollout server | +| `harbor` | `from osmosis_ai.rollout.backend.harbor import HarborBackend` | Harbor execution backend | +| `strands` | `from osmosis_ai.rollout.integrations.agents.strands import OsmosisStrandsAgent, OsmosisRolloutModel` | Strands integration | +| `openai-agents` | `from osmosis_ai.rollout.integrations.agents.openai_agents import OsmosisAgent` | OpenAI Agents integration | + +The `harbor` extra installs plain Harbor for an externally provided SkyPilot runtime. Daytona is retired, and do not install Harbor's `skypilot` extra. ## AgentWorkflow @@ -45,8 +54,8 @@ class Grader(ABC): [../osmosis_ai/rollout/grader.py](../osmosis_ai/rollout/grader.py) -- `ctx.get_samples()` returns the collected `dict[str, RolloutSample]` (**sync**). -- Attach rewards with `ctx.set_sample_reward(sample_id, reward)` — it raises `ValueError` for an unknown `sample_id` ([context.py](../osmosis_ai/rollout/context.py)). +- `ctx.sample` is the single `RolloutSample` produced by the workflow; it may be `None` if no source was registered. +- Attach its scalar reward with `ctx.set_reward(reward)`; this raises `ValueError` when `ctx.sample` is `None` ([context.py](../osmosis_ai/rollout/context.py)). - `ctx.label` carries the dataset row's label (the ground-truth string). - `ctx.metadata` is the read-only input-side dataset row metadata. @@ -55,8 +64,8 @@ class Grader(ABC): [../osmosis_ai/rollout/context.py](../osmosis_ai/rollout/context.py) - `AgentWorkflowContext` — `prompt: list[dict]`, `config`. -- `HarborAgentWorkflowContext` — adds `environment` (Harbor `BaseEnvironment`) for `environment.exec()`, `environment.upload_file()`, etc. under `HarborBackend`. -- `GraderContext` — `label`, `samples`, `metadata` (input-side, read-only), plus `get_samples()` / `set_sample_reward()` (output-side). +- Under `HarborBackend`, the workflow receives a Harbor-specific context that additionally exposes `environment` (a Harbor `BaseEnvironment`) for `environment.exec()`, `environment.upload_file()`, and related operations. +- `GraderContext` — `label`, singular `sample`, and input-side `metadata`, plus `set_reward()` for grading output. - `RolloutContext` — ambient per-rollout context (chat completions URL, API key, rollout id). It is a context manager; the server enters it around execution. Local backends pass connection info directly; container runners read it from `OSMOSIS_CHAT_COMPLETIONS_URL` / `OSMOSIS_API_KEY` / `OSMOSIS_ROLLOUT_ID`. Fetch the current one with `get_rollout_context()`. ### Samples @@ -67,13 +76,15 @@ The workflow does not return samples; instead a `SampleSource` is registered on from osmosis_ai.rollout import get_rollout_context rollout_ctx = get_rollout_context() # the active RolloutContext -rollout_ctx.register_sample_source(name, source) # name must be unique per rollout -samples = await rollout_ctx.get_samples() # async -> {name: RolloutSample} +if rollout_ctx is None: + raise RuntimeError("no active rollout context") +rollout_ctx.set_sample_source(source) # exactly one source per rollout +sample = await rollout_ctx.get_sample() # async -> RolloutSample | None ``` -`OsmosisStrandsAgent` registers a source automatically (keyed by the agent `name`/`agent_id`), so most workflows never call `register_sample_source` directly. +`OsmosisStrandsAgent` and `OsmosisMemorySession` register a source automatically, so most workflows never call `set_sample_source` directly. -`RolloutSample` ([types/sample.py](../osmosis_ai/rollout/types/sample.py)) fields: `id`, `messages`, `label`, `reward`, `remove_sample`, `metrics`, `extra_fields`. +`RolloutSample` ([types/sample.py](../osmosis_ai/rollout/types/sample.py)) fields: `messages`, `trajectory_messages`, `label`, `reward`, `remove_sample`, `metrics`, `extra_fields`. ## Artifacts @@ -92,8 +103,7 @@ async def grade(self, ctx: GraderContext) -> Any: (ctx.artifacts_dir / "trace.json").write_text( json.dumps({"score_reason": "matched rubric"}) ) - for sample_id in ctx.get_samples(): - ctx.set_sample_reward(sample_id, 1.0) + ctx.set_reward(1.0) ``` After each rollout the artifacts land on the host under `~/.osmosis//artifacts/`. `LocalBackend` writes your files at that root. Harbor mirrors its collected-trial layout, so the `/logs/artifacts/` convention dir lands at `.../artifacts/logs/artifacts/`, next to any paths you declare in the task's `artifacts` config. @@ -104,19 +114,17 @@ The backend's directory setup and collection is best-effort and never affects re [../osmosis_ai/rollout/trajectory/](../osmosis_ai/rollout/trajectory/) -The server saves every finished rollout as an [ATIF](https://www.harborframework.com/docs/agents/trajectory-format) trajectory document (Harbor's Agent Trajectory Interchange Format). Saving is a server-level concern — it observes the `ExecutionResult` at the backend boundary and works identically with any backend. It is always on and needs no configuration: documents are written to the same platform-managed directory as file artifacts (`~/.osmosis//`), which the platform persists to durable storage. +The server saves every finished rollout as an SDK-owned implementation of the ATIF trajectory schema. Saving is a server-level concern — it observes the `ExecutionResult` at the backend boundary and works identically with any backend. The generic server does not import or depend on Harbor. Saving is always on and needs no configuration: documents are written to the same platform-managed directory as file artifacts (`~/.osmosis//`), which the platform persists to durable storage. Layout per rollout, keyed by `rollout_id` (callers that need position semantics — e.g. an eval run's row/run index — keep them in their own index and join on the rollout id, which is also echoed in `extra.osmosis`): ``` ~/.osmosis// ├── trajectory.json # the rollout's ATIF document -│ # (trajectory-.json per sample while the -│ # transitional multi-sample protocol is still in use) └── artifacts/... # file artifacts (see above) ``` -Each document carries a normalized, controller-compatible transcript as ATIF steps (tool calls fold into agent-step observations) and namespaces platform context under `extra.osmosis`: `rollout_id`, `sample_id`, `label`, `reward`, sample `metrics`/`extra_fields`, and the request's `metadata`/`extra_fields` (the natural channel for run identity such as an eval run id). +Each document carries a normalized, controller-compatible transcript as ATIF steps (tool calls fold into agent-step observations) and namespaces platform context under `extra.osmosis`: `rollout_id`, `label`, `reward`, sample `metrics`/`extra_fields`, and the request's `metadata`/`extra_fields` (the natural channel for run identity such as an eval run id). Built-in sample sources keep their framework-native `RolloutSample.messages` for graders and callbacks and prepare a separate `trajectory_messages` copy through the framework converter used for OpenAI-compatible `/chat/completions` traffic. `trajectory_messages` is SDK-internal: it crosses backend boundaries for persistence but is omitted from grader callbacks. This is not an exact wire replay because call-specific conversion arguments and separately supplied system instructions are not retained. Framework-native items omitted by the framework converter are outside the persisted transcript contract. Custom sample sources whose native history is already OpenAI chat-completions-shaped get the same behavior by default. A source with another native shape sets `RolloutSample.trajectory_messages` itself from `get_sample` (an explicit `None` marks conversion as unavailable and skips trajectory persistence for that sample). Like artifacts, conversion and saving are best-effort: failures are logged and never affect rewards, callbacks, or rollout status. @@ -127,7 +135,7 @@ ATIF has first-class slots for LLM operational data (`Step.metrics`, `Step.model 1. **Controller report (callback ack)** — the controller may attach a `trajectory` object to the JSON body of its completion/grader callback response ([report.py](../osmosis_ai/rollout/trajectory/report.py) defines the shape). Its LLM bridge serves every completion, so it is the party that has per-call usage. - **When to report**: snapshot the agent-phase calls into the **completion** ack, before resolving any internal future that triggers controller-side cleanup. Omit `trajectory` from the grader ack — an ack without a report keeps the earlier one, and grader-phase LLM calls (an LLM judge) would skew call counts and totals. A grader ack that does carry a report replaces the completion one entirely (no merge). - **Attribution**: `llm_call_metrics` map onto agent steps in dispatch order only when the counts match exactly; on a mismatch they are preserved under `extra.osmosis.unmatched_llm_call_metrics` instead of being mis-attributed, and totals still aggregate into `final_metrics`. The SDK always fills `final_metrics.total_steps` from the emitted ATIF steps. - - **Sample keys**: use the rollout's sample ids (the SDK integrations send them as the `x-sample-id` header on every completion). A controller that cannot know them may key its only entry arbitrarily — with exactly one sample and one entry they match regardless of key. Other unmatched entries are logged and, for single-sample rollouts, preserved under `extra.osmosis.unmatched_sample_reports`. + - **Sample keys**: the SDK no longer has sample ids. For a single-sample rollout, one `samples` entry is accepted regardless of its key. Multiple entries cannot be attributed; they are logged and preserved under `extra.osmosis.unmatched_sample_reports`. ```jsonc // response body of POST or @@ -136,7 +144,7 @@ ATIF has first-class slots for LLM operational data (`Step.metrics`, `Step.model "trajectory": { "model_name": "openai/gpt-5-mini", "samples": { - "": { + "": { "llm_call_metrics": [ {"prompt_tokens": 120, "completion_tokens": 40, "cached_tokens": 0, "cost_usd": 0.0003, "logprobs": [-0.1], "model_name": "...", @@ -152,7 +160,7 @@ ATIF has first-class slots for LLM operational data (`Step.metrics`, `Step.model The server reads only the `trajectory` key off the ack body; the surrounding ack fields — `status`, `ok`, or anything else the controller returns — are accepted and ignored. -2. **Inline message metadata** — custom workflows that manage their own message list can copy `response.usage` / `response.model` onto the assistant message (top-level `usage`/`model` keys, or the `extra.response` shape harbor's converters read). Both chat-completions (`prompt_tokens`) and Responses API (`input_tokens`) field names are accepted, and `created_at`/`timestamp` fields become `Step.timestamp`. The controller report overrides inline metadata when both are present. +2. **Inline message metadata** — custom workflows that manage their own message list can copy `response.usage` / `response.model` onto the assistant message (top-level `usage`/`model` keys, or a compatible `extra.response` shape). Both chat-completions (`prompt_tokens`) and Responses API (`input_tokens`) field names are accepted, and `created_at`/`timestamp` fields become `Step.timestamp`. The controller report overrides inline metadata when both are present. ## Configs @@ -178,7 +186,8 @@ class AgentWorkflowConfig(BaseConfig): # also GraderConfig `LocalBackend.__init__` is keyword-only and takes `workflow` / `grader` (a class or a dotted import string), plus optional `workflow_config` / `grader_config` ([backend/local/backend.py](../osmosis_ai/rollout/backend/local/backend.py)): ```python -from osmosis_ai.rollout import create_rollout_server, LocalBackend +from osmosis_ai.rollout import LocalBackend +from osmosis_ai.rollout.server import create_rollout_server backend = LocalBackend( workflow=MyWorkflow, @@ -189,22 +198,22 @@ backend = LocalBackend( app = create_rollout_server(backend=backend) # FastAPI: POST /rollout, GET /health ``` -- `create_rollout_server` ([server/app.py](../osmosis_ai/rollout/server/app.py)) wires the protocol: it runs the backend in a background task and posts the completion + grader callbacks. +- `create_rollout_server` ([server/app.py](../osmosis_ai/rollout/server/app.py)) is provided by the `server` extra. It wires the protocol: it runs the backend in a background task and posts the completion + grader callbacks. It has no Harbor dependency. - `ControllerAuth` ([server/auth.py](../osmosis_ai/rollout/server/auth.py)) supplies the bearer headers for callbacks. - `ExecutionBackend` ([backend/base.py](../osmosis_ai/rollout/backend/base.py)) is the ABC; pick one: - `LocalBackend` ([backend/local/](../osmosis_ai/rollout/backend/local/)) — runs workflow + grader in-process. Re-exported from `osmosis_ai.rollout`. Used by the scaffold and eval. - - `HarborBackend` ([backend/harbor/backend.py](../osmosis_ai/rollout/backend/harbor/backend.py)) — runs the agent inside a Harbor container; pairs with `HarborAgentWorkflowContext`. It is **not** re-exported (import `from osmosis_ai.rollout.backend.harbor.backend import HarborBackend`) and requires the external `harbor` dependency. + - `HarborBackend` ([backend/harbor/backend.py](../osmosis_ai/rollout/backend/harbor/backend.py)) — runs the agent inside a Harbor container and is available with the `harbor` extra: `from osmosis_ai.rollout.backend.harbor import HarborBackend`. ### Running a server -There is no `osmosis rollout serve` command. Scaffold a server with `osmosis rollout init `, which writes `rollouts//main.py` wiring `LocalBackend` + `create_rollout_server` + `uvicorn` ([../osmosis_ai/templates/_scaffolds/rollout/main.py.tpl](../osmosis_ai/templates/_scaffolds/rollout/main.py.tpl)), then run it with `python main.py` (it listens on `_OSMOSIS_ROLLOUT_PORT`, default 8000). +There is no `osmosis rollout serve` command. Scaffold a server with `osmosis rollout init `, which writes `rollouts//main.py` wiring `LocalBackend` + `create_rollout_server` + `uvicorn` ([../osmosis_ai/templates/_scaffolds/rollout/main.py.tpl](../osmosis_ai/templates/_scaffolds/rollout/main.py.tpl)), then run `python rollouts//main.py` from the workspace root (it listens on `_OSMOSIS_ROLLOUT_PORT`, default 8000). ## Integrations [../osmosis_ai/rollout/integrations/agents/](../osmosis_ai/rollout/integrations/agents/) -- **Strands** — `OsmosisStrandsAgent` / `OsmosisRolloutModel` are re-exported from `osmosis_ai.rollout`. `OsmosisStrandsAgent` is a drop-in for `strands.Agent`: it swaps in the rollout model from the active `RolloutContext` and auto-registers a sample source. -- **OpenAI Agents** — `OsmosisAgent` is a drop-in for `agents.Agent`, but it is **only** importable from the submodule, not re-exported by `osmosis_ai.rollout` or the integrations `__init__`: +- **Strands** — with `pip install "osmosis-ai[strands]"`, import `OsmosisStrandsAgent` and `OsmosisRolloutModel` from `osmosis_ai.rollout.integrations.agents.strands`. `OsmosisStrandsAgent` is a drop-in for `strands.Agent`: it swaps in the rollout model from the active `RolloutContext` and auto-registers a sample source. +- **OpenAI Agents** — with `pip install "osmosis-ai[openai-agents]"`, import `OsmosisAgent` from the canonical integration module: ```python from osmosis_ai.rollout.integrations.agents.openai_agents import OsmosisAgent @@ -221,8 +230,10 @@ from osmosis_ai.rollout import ( Grader, GraderConfig, GraderContext, - OsmosisStrandsAgent, +) +from osmosis_ai.rollout.integrations.agents.strands import ( OsmosisRolloutModel, + OsmosisStrandsAgent, ) @@ -240,9 +251,10 @@ class MyWorkflow(AgentWorkflow[MyConfig]): class MyGrader(Grader): async def grade(self, ctx: GraderContext) -> Any: - for sample_id, sample in ctx.get_samples().items(): - reward = 1.0 if str(ctx.label) in str(sample.messages[-1]) else 0.0 - ctx.set_sample_reward(sample_id, reward) + if ctx.sample is None: + raise ValueError("workflow did not produce a sample") + reward = 1.0 if str(ctx.label) in str(ctx.sample.messages[-1]) else 0.0 + ctx.set_reward(reward) ``` For complete, runnable rollouts (local Strands, local OpenAI Agents, Harbor) see the [Osmosis-AI/workspace-template](https://github.com/Osmosis-AI/workspace-template) `rollouts/` directory. diff --git a/docs/troubleshooting.md b/docs/troubleshooting.md index a6b860f0..3090dfdd 100644 --- a/docs/troubleshooting.md +++ b/docs/troubleshooting.md @@ -1,6 +1,6 @@ # Troubleshooting (engineering) -> Engineering-level failure modes when building rollouts and running evals. Install, login, and workspace-setup basics live at [docs.osmosis.ai](https://docs.osmosis.ai). One entry fact: the SDK requires **Python 3.12+** and the server extra (`pip install osmosis-ai[server]`) to run a rollout server (scaffold one with `osmosis rollout init`, then `python main.py`). +> Engineering-level failure modes when building rollouts and running evals. Install, login, and workspace-setup basics live at [docs.osmosis.ai](https://docs.osmosis.ai). One entry fact: the SDK requires **Python 3.12+** and the server extra (`pip install "osmosis-ai[server]"`) to run a rollout server (scaffold one with `osmosis rollout init `, then run `python rollouts//main.py`). ## Rollout timeouts diff --git a/osmosis_ai/__init__.py b/osmosis_ai/__init__.py index 4d5b8fe8..8bffbbb2 100644 --- a/osmosis_ai/__init__.py +++ b/osmosis_ai/__init__.py @@ -11,6 +11,7 @@ from typing import TYPE_CHECKING +from ._imports import public_module_dir, resolve_lazy_export from .consts import PACKAGE_VERSION as __version__ if TYPE_CHECKING: @@ -19,7 +20,9 @@ ModelNotFoundError, ProviderRequestError, RubricResult, - evaluate_rubric, + ) + from .eval.rubric import ( + evaluate_rubric as evaluate_rubric, ) # --------------------------------------------------------------------------- @@ -28,25 +31,26 @@ # openai, …) unless actually needed. # --------------------------------------------------------------------------- -_RUBRIC_EXPORTS: frozenset[str] = frozenset( - { - "MissingAPIKeyError", - "ModelNotFoundError", - "ProviderRequestError", - "RubricResult", - "evaluate_rubric", - } -) +_EXPORTS: dict[str, tuple[str, str]] = { + "MissingAPIKeyError": ("osmosis_ai.eval.rubric.types", "MissingAPIKeyError"), + "ModelNotFoundError": ("osmosis_ai.eval.rubric.types", "ModelNotFoundError"), + "ProviderRequestError": ("osmosis_ai.eval.rubric.types", "ProviderRequestError"), + "RubricResult": ("osmosis_ai.eval.rubric.types", "RubricResult"), + "evaluate_rubric": ("osmosis_ai.eval.rubric", "evaluate_rubric"), +} def __getattr__(name: str) -> object: - if name in _RUBRIC_EXPORTS: - from .eval import rubric + return resolve_lazy_export( + name, + module_name=__name__, + namespace=globals(), + exports=_EXPORTS, + ) + - value = getattr(rubric, name) - globals()[name] = value # cache so future access skips __getattr__ - return value - raise AttributeError(f"module 'osmosis_ai' has no attribute {name!r}") +def __dir__() -> list[str]: + return public_module_dir(globals(), _EXPORTS) __all__ = [ @@ -55,5 +59,4 @@ def __getattr__(name: str) -> object: "ProviderRequestError", "RubricResult", "__version__", - "evaluate_rubric", ] diff --git a/osmosis_ai/_imports.py b/osmosis_ai/_imports.py new file mode 100644 index 00000000..cbf32210 --- /dev/null +++ b/osmosis_ai/_imports.py @@ -0,0 +1,64 @@ +"""Helpers for lazy public exports and optional dependency errors.""" + +from __future__ import annotations + +from collections.abc import Mapping +from importlib import import_module +from typing import NoReturn + +LazyExports = Mapping[str, tuple[str, str]] + + +def import_attribute(module_name: str, attribute_name: str) -> object: + """Import one attribute without making its leaf module part of a facade.""" + return getattr(import_module(module_name), attribute_name) + + +def resolve_lazy_export( + name: str, + *, + module_name: str, + namespace: dict[str, object], + exports: LazyExports, +) -> object: + """Resolve and cache a name from a symbol-to-leaf export map.""" + try: + target_module, target_name = exports[name] + except KeyError: + raise AttributeError( + f"module {module_name!r} has no attribute {name!r}" + ) from None + + value = import_attribute(target_module, target_name) + namespace[name] = value + return value + + +def public_module_dir( + namespace: Mapping[str, object], exports: LazyExports +) -> list[str]: + """Return module attributes plus unresolved lazy public exports.""" + return sorted(namespace.keys() | exports.keys()) + + +def raise_optional_dependency_error( + error: ModuleNotFoundError, + *, + extra: str, + expected_modules: frozenset[str], + feature: str, +) -> NoReturn: + """Add an install hint only when an expected optional module is absent. + + A missing SDK module or a missing submodule inside an installed dependency + is left untouched. Those indicate a packaging bug or an incompatible + dependency rather than an omitted extra and should retain their traceback. + """ + if error.name not in expected_modules: + raise error + + message = ( + f"{feature} requires optional dependencies. Install them with " + f'`pip install "osmosis-ai[{extra}]"`.' + ) + raise ModuleNotFoundError(message, name=error.name, path=error.path) from error diff --git a/osmosis_ai/eval/rubric/__init__.py b/osmosis_ai/eval/rubric/__init__.py index 58a970a9..87737cf5 100644 --- a/osmosis_ai/eval/rubric/__init__.py +++ b/osmosis_ai/eval/rubric/__init__.py @@ -1,11 +1,54 @@ -from .engine import evaluate_rubric -from .types import ( - MissingAPIKeyError, - ModelNotFoundError, - ProviderRequestError, - RubricResult, +"""Public rubric API with the optional engine loaded on demand.""" + +from typing import TYPE_CHECKING + +from osmosis_ai._imports import ( + public_module_dir, + raise_optional_dependency_error, + resolve_lazy_export, ) +if TYPE_CHECKING: + from .engine import evaluate_rubric + from .types import ( + MissingAPIKeyError, + ModelNotFoundError, + ProviderRequestError, + RubricResult, + ) + +_EXPORTS: dict[str, tuple[str, str]] = { + "MissingAPIKeyError": ("osmosis_ai.eval.rubric.types", "MissingAPIKeyError"), + "ModelNotFoundError": ("osmosis_ai.eval.rubric.types", "ModelNotFoundError"), + "ProviderRequestError": ("osmosis_ai.eval.rubric.types", "ProviderRequestError"), + "RubricResult": ("osmosis_ai.eval.rubric.types", "RubricResult"), + "evaluate_rubric": ("osmosis_ai.eval.rubric.engine", "evaluate_rubric"), +} + + +def __getattr__(name: str) -> object: + try: + return resolve_lazy_export( + name, + module_name=__name__, + namespace=globals(), + exports=_EXPORTS, + ) + except ModuleNotFoundError as exc: + if name != "evaluate_rubric": + raise + raise_optional_dependency_error( + exc, + extra="rubric", + expected_modules=frozenset({"litellm", "orjson"}), + feature="Rubric evaluation", + ) + + +def __dir__() -> list[str]: + return public_module_dir(globals(), _EXPORTS) + + __all__ = [ "MissingAPIKeyError", "ModelNotFoundError", diff --git a/osmosis_ai/eval/rubric/cli.py b/osmosis_ai/eval/rubric/cli.py index e5512988..9c6bd041 100644 --- a/osmosis_ai/eval/rubric/cli.py +++ b/osmosis_ai/eval/rubric/cli.py @@ -5,12 +5,23 @@ from pathlib import Path from typing import Any +from osmosis_ai._imports import raise_optional_dependency_error from osmosis_ai.cli.errors import CLIError from osmosis_ai.cli.output import OperationResult, OutputFormat, get_output_context from osmosis_ai.cli.paths import parse_cli_path from .dataset import RubricRecord, load_rubric_dataset -from .engine import evaluate_rubric + +try: + from .engine import evaluate_rubric +except ModuleNotFoundError as _exc: + raise_optional_dependency_error( + _exc, + extra="rubric", + expected_modules=frozenset({"litellm", "orjson"}), + feature="Rubric evaluation", + ) + from .report import ( ConsoleReportRenderer, JsonReportWriter, diff --git a/osmosis_ai/platform/auth/__init__.py b/osmosis_ai/platform/auth/__init__.py index 21c50adf..8dd9529c 100644 --- a/osmosis_ai/platform/auth/__init__.py +++ b/osmosis_ai/platform/auth/__init__.py @@ -1,24 +1,97 @@ """Osmosis CLI authentication module.""" -from .config import CONFIG_DIR, CREDENTIALS_FILE, PLATFORM_URL, get_platform_url -from .credentials import ( - Credentials, - UserInfo, - delete_credentials, - get_credential_store, - get_valid_credentials, - load_credentials, - save_credentials, -) -from .flow import LoginError, LoginResult, device_login, verify_token -from .local_config import reset_session -from .platform_client import ( - AuthenticationExpiredError, - PlatformAPIError, - SubscriptionRequiredError, - UpgradeRequiredError, - platform_request, -) +from typing import TYPE_CHECKING + +from osmosis_ai._imports import public_module_dir, resolve_lazy_export + +if TYPE_CHECKING: + from .config import CONFIG_DIR, CREDENTIALS_FILE, PLATFORM_URL, get_platform_url + from .credentials import ( + Credentials, + UserInfo, + delete_credentials, + get_credential_store, + get_valid_credentials, + load_credentials, + save_credentials, + ) + from .flow import LoginError, LoginResult, device_login, verify_token + from .local_config import reset_session + from .platform_client import ( + AuthenticationExpiredError, + PlatformAPIError, + SubscriptionRequiredError, + UpgradeRequiredError, + platform_request, + ) + +_EXPORTS: dict[str, tuple[str, str]] = { + "CONFIG_DIR": ("osmosis_ai.platform.auth.config", "CONFIG_DIR"), + "CREDENTIALS_FILE": ("osmosis_ai.platform.auth.config", "CREDENTIALS_FILE"), + "PLATFORM_URL": ("osmosis_ai.platform.auth.config", "PLATFORM_URL"), + "AuthenticationExpiredError": ( + "osmosis_ai.platform.auth.platform_client", + "AuthenticationExpiredError", + ), + "Credentials": ("osmosis_ai.platform.auth.credentials", "Credentials"), + "LoginError": ("osmosis_ai.platform.auth.flow", "LoginError"), + "LoginResult": ("osmosis_ai.platform.auth.flow", "LoginResult"), + "PlatformAPIError": ( + "osmosis_ai.platform.auth.platform_client", + "PlatformAPIError", + ), + "SubscriptionRequiredError": ( + "osmosis_ai.platform.auth.platform_client", + "SubscriptionRequiredError", + ), + "UpgradeRequiredError": ( + "osmosis_ai.platform.auth.platform_client", + "UpgradeRequiredError", + ), + "UserInfo": ("osmosis_ai.platform.auth.credentials", "UserInfo"), + "delete_credentials": ( + "osmosis_ai.platform.auth.credentials", + "delete_credentials", + ), + "device_login": ("osmosis_ai.platform.auth.flow", "device_login"), + "get_credential_store": ( + "osmosis_ai.platform.auth.credentials", + "get_credential_store", + ), + "get_platform_url": ("osmosis_ai.platform.auth.config", "get_platform_url"), + "get_valid_credentials": ( + "osmosis_ai.platform.auth.credentials", + "get_valid_credentials", + ), + "load_credentials": ( + "osmosis_ai.platform.auth.credentials", + "load_credentials", + ), + "platform_request": ( + "osmosis_ai.platform.auth.platform_client", + "platform_request", + ), + "reset_session": ("osmosis_ai.platform.auth.local_config", "reset_session"), + "save_credentials": ( + "osmosis_ai.platform.auth.credentials", + "save_credentials", + ), + "verify_token": ("osmosis_ai.platform.auth.flow", "verify_token"), +} + + +def __getattr__(name: str) -> object: + return resolve_lazy_export( + name, + module_name=__name__, + namespace=globals(), + exports=_EXPORTS, + ) + + +def __dir__() -> list[str]: + return public_module_dir(globals(), _EXPORTS) + __all__ = [ "CONFIG_DIR", diff --git a/osmosis_ai/platform/cli/dataset.py b/osmosis_ai/platform/cli/dataset.py index b4ab943c..21248c19 100644 --- a/osmosis_ai/platform/cli/dataset.py +++ b/osmosis_ai/platform/cli/dataset.py @@ -87,7 +87,7 @@ def _abort_upload( PARQUET_VALIDATION_SKIPPED_WARNING = ( "pyarrow not installed; parquet content validation skipped. " - "Install with: pip install 'osmosis-ai[platform]'" + "Install with: pip install 'osmosis-ai[parquet]'" ) diff --git a/osmosis_ai/platform/cli/shared_config.py b/osmosis_ai/platform/cli/shared_config.py index 83a5ac78..354fac0c 100644 --- a/osmosis_ai/platform/cli/shared_config.py +++ b/osmosis_ai/platform/cli/shared_config.py @@ -5,7 +5,7 @@ import re import tomllib from pathlib import Path -from typing import Any, ClassVar, Self +from typing import TYPE_CHECKING, Any, ClassVar, Self from pydantic import ( BaseModel, @@ -15,10 +15,12 @@ field_validator, model_validator, ) -from pydantic_core import ErrorDetails from osmosis_ai.cli.errors import CLIError +if TYPE_CHECKING: + from pydantic_core import ErrorDetails + ENV_VAR_NAME_RE = re.compile(r"^[A-Z_][A-Z0-9_]*$") SECRET_NAME_RE = re.compile(r"^[A-Z][A-Z0-9_]*$") # A pinned commit SHA is a hex string. Git's default short form is 7 chars and a diff --git a/osmosis_ai/platform/cli/workspace_directory_contract.py b/osmosis_ai/platform/cli/workspace_directory_contract.py index 9d4a8345..79b752ad 100644 --- a/osmosis_ai/platform/cli/workspace_directory_contract.py +++ b/osmosis_ai/platform/cli/workspace_directory_contract.py @@ -8,11 +8,13 @@ import tomllib from importlib.metadata import PackageNotFoundError +from importlib.metadata import requires as installed_requirements from importlib.metadata import version as installed_version from pathlib import Path from typing import Any from packaging.requirements import InvalidRequirement, Requirement +from packaging.utils import canonicalize_name from osmosis_ai.cli.errors import CLIError from osmosis_ai.templates.catalog import required_workspace_paths @@ -124,13 +126,81 @@ def _format_backend_validation_errors(errors: list[Any]) -> str: return "\n".join(f" - [{error.code}] {error.message}" for error in errors) +def _requirement_problem( + requirement: Requirement, + *, + requested_by: str | None = None, +) -> str | None: + """Return why an installed requirement is unusable, if applicable.""" + if requirement.url: + return None + try: + have = installed_version(requirement.name) + except PackageNotFoundError: + if requested_by is not None: + return f"{requested_by} requires {requirement.name}, which is not installed" + return f"{requirement.name} is not installed" + else: + if not requirement.specifier or requirement.specifier.contains( + have, prereleases=True + ): + return None + if requested_by is not None: + return ( + f"{requested_by} requires {requirement.name}{requirement.specifier}, " + f"but {requirement.name} {have} is installed" + ) + return f"{requirement.name} {have} does not satisfy {requirement.specifier}" + + +def _unsatisfied_requested_extras(requirement: Requirement) -> list[str]: + """Check direct dependencies activated by extras on an installed package. + + Merely finding the parent distribution does not prove that dependencies for + a requested extra are present. Inspect its installed ``Requires-Dist`` + metadata so submit preflight does not mistake a base-only installation for + one that can import the rollout's selected integration. + """ + if not requirement.extras: + return [] + try: + declared = installed_requirements(requirement.name) or [] + except PackageNotFoundError: + return [] # The parent requirement reports this more clearly. + + parent = requirement.name + "[" + ",".join(sorted(requirement.extras)) + "]" + parent_name = canonicalize_name(requirement.name) + unsatisfied: list[str] = [] + for raw in declared: + try: + child = Requirement(raw) + except InvalidRequirement: + continue + if child.marker is None or not any( + child.marker.evaluate({"extra": extra}) for extra in requirement.extras + ): + continue + + # Composite extras in this project self-reference another set of extras. + # Inspect the expanded feature set without reporting the installed parent + # as its own missing dependency. + if canonicalize_name(child.name) == parent_name: + unsatisfied.extend(_unsatisfied_requested_extras(child)) + continue + + problem = _requirement_problem(child, requested_by=parent) + if problem is not None: + unsatisfied.append(problem) + return unsatisfied + + def _unsatisfied_rollout_requirements(rollout_dir: Path) -> list[str]: """Declared requirements this environment does not satisfy. Preflight imports the rollout into the workspace-root environment, not the - rollout's own, so the two can diverge. Only declared specifiers are checked - against installed versions; extras and transitive resolution are left to the - resolver. + rollout's own, so the two can diverge. Declared specifiers and direct + dependencies activated by their extras are checked; deeper transitive + resolution remains the resolver's responsibility. """ pyproject = rollout_dir / "pyproject.toml" if not pyproject.is_file(): @@ -154,20 +224,11 @@ def _unsatisfied_rollout_requirements(rollout_dir: Path) -> list[str]: continue if requirement.marker is not None and not requirement.marker.evaluate(): continue - if requirement.url: - # A direct URL or VCS pin carries no version to compare against. - continue - try: - have = installed_version(requirement.name) - except PackageNotFoundError: - unsatisfied.append(f"{requirement.name} is not installed") + problem = _requirement_problem(requirement) + if problem is not None: + unsatisfied.append(problem) continue - if requirement.specifier and not requirement.specifier.contains( - have, prereleases=True - ): - unsatisfied.append( - f"{requirement.name} {have} does not satisfy {requirement.specifier}" - ) + unsatisfied.extend(_unsatisfied_requested_extras(requirement)) return unsatisfied diff --git a/osmosis_ai/rollout/__init__.py b/osmosis_ai/rollout/__init__.py index 257da4fa..0942008c 100644 --- a/osmosis_ai/rollout/__init__.py +++ b/osmosis_ai/rollout/__init__.py @@ -5,24 +5,23 @@ from osmosis_ai.rollout.context import ( AgentWorkflowContext, GraderContext, - HarborAgentWorkflowContext, RolloutContext, + SampleSource, get_rollout_context, ) from osmosis_ai.rollout.grader import Grader -from osmosis_ai.rollout.integrations.agents.strands import ( - OsmosisRolloutModel, - OsmosisStrandsAgent, -) -from osmosis_ai.rollout.server import ControllerAuth, create_rollout_server from osmosis_ai.rollout.types import ( AgentWorkflowConfig, + BaseConfig, ConcurrencyConfig, ExecutionRequest, ExecutionResult, GraderCompleteRequest, GraderConfig, + GraderInitRequest, + GraderInitResponse, GraderStatus, + MessageDict, RolloutCompleteRequest, RolloutErrorCategory, RolloutInitRequest, @@ -35,8 +34,8 @@ "AgentWorkflow", "AgentWorkflowConfig", "AgentWorkflowContext", + "BaseConfig", "ConcurrencyConfig", - "ControllerAuth", "ExecutionBackend", "ExecutionRequest", "ExecutionResult", @@ -44,11 +43,11 @@ "GraderCompleteRequest", "GraderConfig", "GraderContext", + "GraderInitRequest", + "GraderInitResponse", "GraderStatus", - "HarborAgentWorkflowContext", "LocalBackend", - "OsmosisRolloutModel", - "OsmosisStrandsAgent", + "MessageDict", "RolloutCompleteRequest", "RolloutContext", "RolloutErrorCategory", @@ -56,6 +55,6 @@ "RolloutInitResponse", "RolloutSample", "RolloutStatus", - "create_rollout_server", + "SampleSource", "get_rollout_context", ] diff --git a/osmosis_ai/rollout/backend/harbor/__init__.py b/osmosis_ai/rollout/backend/harbor/__init__.py index 2b692b4a..1eeab8cf 100644 --- a/osmosis_ai/rollout/backend/harbor/__init__.py +++ b/osmosis_ai/rollout/backend/harbor/__init__.py @@ -1,5 +1,46 @@ -from osmosis_ai.rollout.backend.harbor.agent_adapter import OsmosisInstalledAgent -from osmosis_ai.rollout.backend.harbor.backend import HarborBackend +"""Public Harbor backend API, loaded only when a symbol is requested.""" + +from typing import TYPE_CHECKING + +from osmosis_ai._imports import ( + public_module_dir, + raise_optional_dependency_error, + resolve_lazy_export, +) + +if TYPE_CHECKING: + from osmosis_ai.rollout.backend.harbor.agent_adapter import OsmosisInstalledAgent + from osmosis_ai.rollout.backend.harbor.backend import HarborBackend + +_EXPORTS: dict[str, tuple[str, str]] = { + "HarborBackend": ("osmosis_ai.rollout.backend.harbor.backend", "HarborBackend"), + "OsmosisInstalledAgent": ( + "osmosis_ai.rollout.backend.harbor.agent_adapter", + "OsmosisInstalledAgent", + ), +} + + +def __getattr__(name: str) -> object: + try: + return resolve_lazy_export( + name, + module_name=__name__, + namespace=globals(), + exports=_EXPORTS, + ) + except ModuleNotFoundError as exc: + raise_optional_dependency_error( + exc, + extra="harbor", + expected_modules=frozenset({"harbor", "toml"}), + feature="The Harbor backend", + ) + + +def __dir__() -> list[str]: + return public_module_dir(globals(), _EXPORTS) + __all__ = [ "HarborBackend", diff --git a/osmosis_ai/rollout/integrations/__init__.py b/osmosis_ai/rollout/integrations/__init__.py index 822bf192..2b8f0c42 100644 --- a/osmosis_ai/rollout/integrations/__init__.py +++ b/osmosis_ai/rollout/integrations/__init__.py @@ -1,11 +1,6 @@ """Framework-specific rollout integrations.""" -from osmosis_ai.rollout.integrations.agents.strands import ( - OsmosisRolloutModel, - OsmosisStrandsAgent, -) - -__all__ = [ - "OsmosisRolloutModel", - "OsmosisStrandsAgent", -] +# Import integrations explicitly from their framework modules, for example: +# ``osmosis_ai.rollout.integrations.agents.strands``. Keeping this namespace +# neutral prevents one framework from becoming a dependency of every integration. +__all__: list[str] = [] diff --git a/osmosis_ai/rollout/integrations/agents/__init__.py b/osmosis_ai/rollout/integrations/agents/__init__.py index d313b385..e546297e 100644 --- a/osmosis_ai/rollout/integrations/agents/__init__.py +++ b/osmosis_ai/rollout/integrations/agents/__init__.py @@ -1,9 +1,7 @@ -from osmosis_ai.rollout.integrations.agents.strands import ( - OsmosisRolloutModel, - OsmosisStrandsAgent, -) +"""Framework-specific agent integrations. -__all__ = [ - "OsmosisRolloutModel", - "OsmosisStrandsAgent", -] +Import each framework explicitly so one agent framework does not become a +dependency of every integration. +""" + +__all__: list[str] = [] diff --git a/osmosis_ai/rollout/integrations/agents/openai_agents.py b/osmosis_ai/rollout/integrations/agents/openai_agents.py index ed4f950b..c1760220 100644 --- a/osmosis_ai/rollout/integrations/agents/openai_agents.py +++ b/osmosis_ai/rollout/integrations/agents/openai_agents.py @@ -1,15 +1,43 @@ +"""OpenAI Agents SDK adapter for Osmosis rollouts. + +Install ``osmosis-ai[openai-agents]`` before importing this module. +""" + import logging import uuid from collections.abc import AsyncIterator, Mapping, Sequence from typing import Any, cast -from agents import Agent -from agents.extensions.models.litellm_model import LitellmModel -from agents.items import ModelResponse, TResponseInputItem, TResponseStreamEvent -from agents.memory.session import SessionABC -from agents.model_settings import ModelSettings -from agents.models.chatcmpl_converter import Converter -from agents.usage import Usage +from osmosis_ai._imports import raise_optional_dependency_error + +try: + from agents import Agent + from agents.extensions.models.litellm_model import LitellmModel + from agents.items import ModelResponse, TResponseInputItem, TResponseStreamEvent + from agents.memory.session import SessionABC + from agents.model_settings import ModelSettings + from agents.models.chatcmpl_converter import Converter + from agents.usage import Usage +except ModuleNotFoundError as _exc: + raise_optional_dependency_error( + _exc, + extra="openai-agents", + expected_modules=frozenset({"agents", "litellm"}), + feature="The OpenAI Agents integration", + ) +except ImportError as _exc: + # openai-agents converts a missing LiteLLM extra into ImportError. Recover + # the original ModuleNotFoundError so users get the Osmosis installation + # command without masking unrelated import incompatibilities. + _cause = _exc.__cause__ + if not isinstance(_cause, ModuleNotFoundError) or _cause.name != "litellm": + raise + raise_optional_dependency_error( + _cause, + extra="openai-agents", + expected_modules=frozenset({"litellm"}), + feature="The OpenAI Agents integration", + ) from osmosis_ai.rollout.context import SampleSource, get_rollout_context from osmosis_ai.rollout.types import RolloutSample diff --git a/osmosis_ai/rollout/integrations/agents/strands.py b/osmosis_ai/rollout/integrations/agents/strands.py index 016fb087..8bb4f2e2 100644 --- a/osmosis_ai/rollout/integrations/agents/strands.py +++ b/osmosis_ai/rollout/integrations/agents/strands.py @@ -1,11 +1,26 @@ +"""Strands agent adapter for Osmosis rollouts. + +Install ``osmosis-ai[strands]`` before importing this module. +""" + import logging from collections.abc import Mapping, Sequence from typing import Any, cast -from strands import Agent as StrandsAgent -from strands.models.litellm import LiteLLMModel -from strands.models.model import Model -from strands.types.content import Messages +from osmosis_ai._imports import raise_optional_dependency_error + +try: + from strands import Agent as StrandsAgent + from strands.models.litellm import LiteLLMModel + from strands.models.model import Model + from strands.types.content import Messages +except ModuleNotFoundError as _exc: + raise_optional_dependency_error( + _exc, + extra="strands", + expected_modules=frozenset({"litellm", "strands"}), + feature="The Strands integration", + ) from osmosis_ai.rollout.context import ( SampleSource, @@ -16,6 +31,11 @@ logger: logging.Logger = logging.getLogger(__name__) +__all__ = [ + "OsmosisRolloutModel", + "OsmosisStrandsAgent", +] + class StrandsAgentSampleSource(SampleSource): """Produces the rollout sample from a Strands agent's ``messages`` field. diff --git a/osmosis_ai/rollout/server/__init__.py b/osmosis_ai/rollout/server/__init__.py index 374f4baa..ef48dedc 100644 --- a/osmosis_ai/rollout/server/__init__.py +++ b/osmosis_ai/rollout/server/__init__.py @@ -1,5 +1,48 @@ -from osmosis_ai.rollout.server.app import create_rollout_server -from osmosis_ai.rollout.server.auth import ControllerAuth +"""Public rollout server API, loaded only when a symbol is requested.""" + +from typing import TYPE_CHECKING + +from osmosis_ai._imports import ( + public_module_dir, + raise_optional_dependency_error, + resolve_lazy_export, +) + +if TYPE_CHECKING: + from osmosis_ai.rollout.server.app import create_rollout_server + from osmosis_ai.rollout.server.auth import ControllerAuth + +_EXPORTS: dict[str, tuple[str, str]] = { + "ControllerAuth": ("osmosis_ai.rollout.server.auth", "ControllerAuth"), + "create_rollout_server": ( + "osmosis_ai.rollout.server.app", + "create_rollout_server", + ), +} + + +def __getattr__(name: str) -> object: + try: + return resolve_lazy_export( + name, + module_name=__name__, + namespace=globals(), + exports=_EXPORTS, + ) + except ModuleNotFoundError as exc: + if name != "create_rollout_server": + raise + raise_optional_dependency_error( + exc, + extra="server", + expected_modules=frozenset({"fastapi", "uvicorn"}), + feature="The rollout server", + ) + + +def __dir__() -> list[str]: + return public_module_dir(globals(), _EXPORTS) + __all__ = [ "ControllerAuth", diff --git a/osmosis_ai/rollout/trajectory/__init__.py b/osmosis_ai/rollout/trajectory/__init__.py index 8ca4106c..78fc6242 100644 --- a/osmosis_ai/rollout/trajectory/__init__.py +++ b/osmosis_ai/rollout/trajectory/__init__.py @@ -1,11 +1,49 @@ """Save finished rollouts as ATIF trajectory documents (backend-agnostic).""" -from osmosis_ai.rollout.trajectory.converter import convert_sample_to_trajectory -from osmosis_ai.rollout.trajectory.report import ( - TrajectoryReport, - report_from_response, -) -from osmosis_ai.rollout.trajectory.save import save_trajectories +from typing import TYPE_CHECKING + +from osmosis_ai._imports import public_module_dir, resolve_lazy_export + +if TYPE_CHECKING: + from osmosis_ai.rollout.trajectory.converter import convert_sample_to_trajectory + from osmosis_ai.rollout.trajectory.report import ( + TrajectoryReport, + report_from_response, + ) + from osmosis_ai.rollout.trajectory.save import save_trajectories + +_EXPORTS: dict[str, tuple[str, str]] = { + "TrajectoryReport": ( + "osmosis_ai.rollout.trajectory.report", + "TrajectoryReport", + ), + "convert_sample_to_trajectory": ( + "osmosis_ai.rollout.trajectory.converter", + "convert_sample_to_trajectory", + ), + "report_from_response": ( + "osmosis_ai.rollout.trajectory.report", + "report_from_response", + ), + "save_trajectories": ( + "osmosis_ai.rollout.trajectory.save", + "save_trajectories", + ), +} + + +def __getattr__(name: str) -> object: + return resolve_lazy_export( + name, + module_name=__name__, + namespace=globals(), + exports=_EXPORTS, + ) + + +def __dir__() -> list[str]: + return public_module_dir(globals(), _EXPORTS) + __all__ = [ "TrajectoryReport", diff --git a/osmosis_ai/rollout/trajectory/atif.py b/osmosis_ai/rollout/trajectory/atif.py new file mode 100644 index 00000000..8bce9aca --- /dev/null +++ b/osmosis_ai/rollout/trajectory/atif.py @@ -0,0 +1,326 @@ +"""Internal ATIF v1.7 models used by trajectory persistence. + +These models were initially adapted from Harbor 0.20.0: + +- ``harbor/models/trajectories/`` for the ATIF models +- ``harbor/utils/trajectory_utils.py`` for JSON formatting + +They intentionally live in the SDK because trajectory persistence is shared by +all execution backends. Importing Harbor's models here would make the generic +rollout server and ``LocalBackend`` require the optional ``harbor`` extra. +This module is not re-exported as supported SDK API; external consumers that +need general-purpose ATIF models should use Harbor's implementation directly. + +When updating ATIF support, compare this module with both the upstream ATIF RFC +and those Harbor paths. Preserve SDK-specific hardening such as rejecting +non-finite floats, which prevents invalid JSON documents. +""" + +from __future__ import annotations + +import json +import re +from datetime import datetime +from typing import Any, ClassVar, Literal, Self + +from pydantic import ( + BaseModel, + ConfigDict, + Field, + field_validator, + model_validator, +) + +__all__: list[str] = [] + +ATIFSchemaVersion = Literal[ + "ATIF-v1.0", + "ATIF-v1.1", + "ATIF-v1.2", + "ATIF-v1.3", + "ATIF-v1.4", + "ATIF-v1.5", + "ATIF-v1.6", + "ATIF-v1.7", +] + + +class _ATIFModel(BaseModel): + """Base configuration shared by all ATIF document objects.""" + + model_config: ClassVar[ConfigDict] = ConfigDict( + extra="forbid", + allow_inf_nan=False, + ) + + +class ImageSource(_ATIFModel): + """A file or URL containing an image used in multimodal content.""" + + media_type: Literal["image/jpeg", "image/png", "image/gif", "image/webp"] + path: str + + +class ContentPart(_ATIFModel): + """One text or image part in a multimodal message.""" + + type: Literal["text", "image"] + text: str | None = None + source: ImageSource | None = None + + @model_validator(mode="after") + def _validate_content_type(self) -> Self: + if self.type == "text": + if self.text is None: + raise ValueError("'text' field is required when type='text'") + if self.source is not None: + raise ValueError("'source' field is not allowed when type='text'") + else: + if self.source is None: + raise ValueError("'source' field is required when type='image'") + if self.text is not None: + raise ValueError("'text' field is not allowed when type='image'") + return self + + +class Agent(_ATIFModel): + """Agent configuration recorded at the trajectory root.""" + + name: str + version: str + model_name: str | None = None + tool_definitions: list[dict[str, Any]] | None = None + extra: dict[str, Any] | None = None + + +class Metrics(_ATIFModel): + """Operational data for one LLM inference.""" + + prompt_tokens: int | None = None + completion_tokens: int | None = None + cached_tokens: int | None = None + cost_usd: float | None = None + prompt_token_ids: list[int] | None = None + completion_token_ids: list[int] | None = None + logprobs: list[float] | None = None + extra: dict[str, Any] | None = None + + +class FinalMetrics(_ATIFModel): + """Aggregate metrics for a complete trajectory.""" + + total_prompt_tokens: int | None = None + total_completion_tokens: int | None = None + total_cached_tokens: int | None = None + total_cost_usd: float | None = None + total_steps: int | None = Field(default=None, ge=0) + extra: dict[str, Any] | None = None + + +class ToolCall(_ATIFModel): + """A structured tool invocation made during an agent step.""" + + tool_call_id: str + function_name: str + arguments: dict[str, Any] + extra: dict[str, Any] | None = None + + +class SubagentTrajectoryRef(_ATIFModel): + """A resolvable reference to a delegated agent's trajectory.""" + + trajectory_id: str | None = None + session_id: str | None = None + trajectory_path: str | None = None + extra: dict[str, Any] | None = None + + @model_validator(mode="after") + def _validate_is_resolvable(self) -> Self: + if self.trajectory_id is None and self.trajectory_path is None: + raise ValueError( + "SubagentTrajectoryRef must be resolvable: set either " + "`trajectory_id` (for embedded references) or " + "`trajectory_path` (for external-file references). " + "`session_id` alone is not a resolution key -- it is " + "run-scoped and may collide across siblings." + ) + return self + + +class ObservationResult(_ATIFModel): + """One tool, environment, or delegated-agent result.""" + + source_call_id: str | None = None + content: str | list[ContentPart] | None = None + subagent_trajectory_ref: list[SubagentTrajectoryRef] | None = None + extra: dict[str, Any] | None = None + + +class Observation(_ATIFModel): + """Environment feedback attached to a trajectory step.""" + + results: list[ObservationResult] + + +class Step(_ATIFModel): + """One sequential turn in an ATIF trajectory.""" + + step_id: int = Field(ge=1) + timestamp: str | None = None + source: Literal["system", "user", "agent"] + model_name: str | None = None + reasoning_effort: str | float | None = None + message: str | list[ContentPart] + reasoning_content: str | None = None + tool_calls: list[ToolCall] | None = None + observation: Observation | None = None + metrics: Metrics | None = None + is_copied_context: bool | None = None + llm_call_count: int | None = Field(default=None, ge=0) + extra: dict[str, Any] | None = None + + @field_validator("timestamp") + @classmethod + def _validate_timestamp(cls, value: str | None) -> str | None: + if value is not None: + try: + datetime.fromisoformat(value.replace("Z", "+00:00")) + except ValueError as exc: + raise ValueError(f"Invalid ISO 8601 timestamp: {exc}") from exc + return value + + @model_validator(mode="after") + def _validate_agent_only_fields(self) -> Self: + if self.source == "agent": + return self + for field_name in ( + "model_name", + "reasoning_effort", + "reasoning_content", + "tool_calls", + "metrics", + ): + if getattr(self, field_name) is not None: + raise ValueError( + f"Field '{field_name}' is only applicable when source is " + f"'agent', but source is '{self.source}'" + ) + return self + + @model_validator(mode="after") + def _validate_zero_llm_call_fields(self) -> Self: + if self.source == "agent" and self.llm_call_count == 0: + for field_name in ("metrics", "reasoning_content"): + if getattr(self, field_name) is not None: + raise ValueError( + f"Field '{field_name}' must be absent when llm_call_count " + "is 0 (deterministic dispatch on a 'source: agent' step)" + ) + return self + + +class Trajectory(_ATIFModel): + """A complete Agent Trajectory Interchange Format document.""" + + schema_version: ATIFSchemaVersion = "ATIF-v1.7" + session_id: str | None = None + trajectory_id: str | None = None + agent: Agent + steps: list[Step] = Field(min_length=1) + notes: str | None = None + final_metrics: FinalMetrics | None = None + continued_trajectory_ref: str | None = None + extra: dict[str, Any] | None = None + subagent_trajectories: list[Trajectory] | None = None + + def to_json_dict(self, exclude_none: bool = True) -> dict[str, Any]: + """Return a JSON-compatible dictionary for persistence.""" + return self.model_dump(exclude_none=exclude_none, mode="json") + + @model_validator(mode="after") + def _validate_step_ids(self) -> Self: + for index, step in enumerate(self.steps): + expected_step_id = index + 1 + if step.step_id != expected_step_id: + raise ValueError( + f"steps[{index}].step_id: expected {expected_step_id} " + f"(sequential from 1), got {step.step_id}" + ) + return self + + @model_validator(mode="after") + def _validate_embedded_subagent_trajectory_ids(self) -> Self: + if not self.subagent_trajectories: + return self + seen: set[str] = set() + for index, subagent in enumerate(self.subagent_trajectories): + trajectory_id = subagent.trajectory_id + if trajectory_id is None: + raise ValueError( + f"subagent_trajectories[{index}].trajectory_id is required " + "for embedded subagents " + f"(agent.name={subagent.agent.name!r}, " + f"session_id={subagent.session_id!r})" + ) + if trajectory_id in seen: + raise ValueError( + f"subagent_trajectories[{index}].trajectory_id " + f"{trajectory_id!r} is not unique within subagent_trajectories" + ) + seen.add(trajectory_id) + return self + + @model_validator(mode="after") + def _validate_tool_call_references(self) -> Self: + for step in self.steps: + if step.observation is None: + continue + tool_call_ids = ( + {call.tool_call_id for call in step.tool_calls} + if step.tool_calls + else set() + ) + for result in step.observation.results: + source_call_id = result.source_call_id + if source_call_id is not None and source_call_id not in tool_call_ids: + raise ValueError( + "Observation result references source_call_id " + f"'{source_call_id}' which is not found in step " + f"{step.step_id}'s tool_calls" + ) + return self + + def has_multimodal_content(self) -> bool: + """Return whether any step or observation contains an image.""" + for step in self.steps: + if isinstance(step.message, list) and any( + part.type == "image" for part in step.message + ): + return True + if step.observation is not None: + for result in step.observation.results: + if isinstance(result.content, list) and any( + part.type == "image" for part in result.content + ): + return True + return False + + +_NUMERIC_ARRAY_PATTERN = re.compile( + r"\[\s*\n\s*-?\d+(?:\.\d+)?(?:[eE][+-]?\d+)?" + r"(?:\s*,\s*\n\s*-?\d+(?:\.\d+)?(?:[eE][+-]?\d+)?)*\s*\n\s*\]", + flags=re.MULTILINE, +) +_NUMBER_PATTERN = re.compile(r"-?\d+(?:\.\d+)?(?:[eE][+-]?\d+)?") + + +def format_trajectory_json(data: dict[str, Any]) -> str: + """Pretty-print a trajectory while keeping numeric arrays on one line.""" + + def compact_numeric_array(match: re.Match[str]) -> str: + return "[" + ", ".join(_NUMBER_PATTERN.findall(match.group(0))) + "]" + + return _NUMERIC_ARRAY_PATTERN.sub( + compact_numeric_array, + json.dumps(data, indent=2, allow_nan=False), + ) diff --git a/osmosis_ai/rollout/trajectory/converter.py b/osmosis_ai/rollout/trajectory/converter.py index 710fcb39..c42cd04c 100644 --- a/osmosis_ai/rollout/trajectory/converter.py +++ b/osmosis_ai/rollout/trajectory/converter.py @@ -1,6 +1,6 @@ """Convert ``RolloutSample.trajectory_messages`` (OpenAI chat shape) into ATIF -trajectories, using Harbor's reference models for spec validation. Anything -that does not fit the spec losslessly is preserved under ``extra``. +trajectories. Anything that does not fit the spec losslessly is preserved +under ``extra``. """ import json @@ -9,7 +9,10 @@ from datetime import UTC, datetime from typing import Any, Literal -from harbor.models.trajectories import ( +from pydantic import ValidationError + +from osmosis_ai.consts import PACKAGE_VERSION +from osmosis_ai.rollout.trajectory.atif import ( Agent, FinalMetrics, Metrics, @@ -19,9 +22,6 @@ ToolCall, Trajectory, ) -from pydantic import ValidationError - -from osmosis_ai.consts import PACKAGE_VERSION from osmosis_ai.rollout.trajectory.report import LlmCallMetrics, SampleReport from osmosis_ai.rollout.types import RolloutSample diff --git a/osmosis_ai/rollout/trajectory/save.py b/osmosis_ai/rollout/trajectory/save.py index 95e560c9..63775101 100644 --- a/osmosis_ai/rollout/trajectory/save.py +++ b/osmosis_ai/rollout/trajectory/save.py @@ -9,8 +9,7 @@ from pathlib import Path from typing import Any -from harbor.utils.trajectory_utils import format_trajectory_json - +from osmosis_ai.rollout.trajectory.atif import format_trajectory_json from osmosis_ai.rollout.trajectory.converter import convert_sample_to_trajectory from osmosis_ai.rollout.trajectory.report import SampleReport, TrajectoryReport from osmosis_ai.rollout.types import ExecutionResult @@ -112,7 +111,7 @@ async def _save( unmatched_sample_reports=unmatched_reports or None, ) dest = artifact_root / rollout_id / "trajectory.json" - # Harbor's formatter keeps numeric arrays on one line. + # Keep large token-id/logprob arrays compact inside the pretty document. data = format_trajectory_json(trajectory.to_json_dict()).encode() await asyncio.to_thread(_write_document, dest, data) logger.info("Saved trajectory document for rollout %s -> %s", rollout_id, dest) diff --git a/osmosis_ai/templates/__init__.py b/osmosis_ai/templates/__init__.py index e96a5ab2..61d19201 100644 --- a/osmosis_ai/templates/__init__.py +++ b/osmosis_ai/templates/__init__.py @@ -2,11 +2,39 @@ from __future__ import annotations -from osmosis_ai.templates.registry import ( - TemplateNotFoundError, - list_templates, - template_recipe, -) +from typing import TYPE_CHECKING + +from osmosis_ai._imports import public_module_dir, resolve_lazy_export + +if TYPE_CHECKING: + from osmosis_ai.templates.registry import ( + TemplateNotFoundError, + list_templates, + template_recipe, + ) + +_EXPORTS: dict[str, tuple[str, str]] = { + "TemplateNotFoundError": ( + "osmosis_ai.templates.registry", + "TemplateNotFoundError", + ), + "list_templates": ("osmosis_ai.templates.registry", "list_templates"), + "template_recipe": ("osmosis_ai.templates.registry", "template_recipe"), +} + + +def __getattr__(name: str) -> object: + return resolve_lazy_export( + name, + module_name=__name__, + namespace=globals(), + exports=_EXPORTS, + ) + + +def __dir__() -> list[str]: + return public_module_dir(globals(), _EXPORTS) + __all__ = [ "TemplateNotFoundError", diff --git a/osmosis_ai/templates/_scaffolds/rollout/main.py.tpl b/osmosis_ai/templates/_scaffolds/rollout/main.py.tpl index faebbd37..319e7b03 100644 --- a/osmosis_ai/templates/_scaffolds/rollout/main.py.tpl +++ b/osmosis_ai/templates/_scaffolds/rollout/main.py.tpl @@ -3,8 +3,8 @@ Fill in two methods, then run `python main.py` to start a FastAPI rollout server on $_OSMOSIS_ROLLOUT_PORT (default 8000): - - MyAgentWorkflow.run(): drive the LLM and register sample sources. - - MyGrader.grade(): turn samples into scalar rewards. + - MyAgentWorkflow.run(): drive the LLM and register a sample source. + - MyGrader.grade(): turn the sample into a scalar reward. Compare the multiply-* rollouts in the workspace-template repo for fully worked examples (Strands, OpenAI Agents, Harbor-backed). @@ -29,8 +29,8 @@ class MyAgentWorkflow(AgentWorkflow): # # ctx.prompt is the list[dict] of input messages for this rollout. # Drive the LLM with one of: - # * osmosis_ai.rollout.integrations.agents.strands (Strands) - # * osmosis_ai.rollout.integrations.agents.openai_agents (OpenAI Agents SDK) + # * osmosis_ai.rollout.integrations.agents.strands (Strands; install [strands]) + # * osmosis_ai.rollout.integrations.agents.openai_agents (OpenAI Agents; install [openai-agents]) # * raw HTTP against get_rollout_context().chat_completions_url # # Register a SampleSource so the grader can read the conversation: diff --git a/osmosis_ai/templates/_scaffolds/rollout/pyproject.toml.tpl b/osmosis_ai/templates/_scaffolds/rollout/pyproject.toml.tpl index 7abe159a..0d543006 100644 --- a/osmosis_ai/templates/_scaffolds/rollout/pyproject.toml.tpl +++ b/osmosis_ai/templates/_scaffolds/rollout/pyproject.toml.tpl @@ -4,7 +4,7 @@ description = "Placeholder rollout scaffolded by `osmosis rollout init`." version = "0.1.0" requires-python = ">=3.12" dependencies = [ - "osmosis-ai[server]>=0.2.29", + "osmosis-ai[server]>=0.3.0rc1,<0.4", ] [build-system] diff --git a/pyproject.toml b/pyproject.toml index cc9a6242..b6a4cda1 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -22,77 +22,109 @@ classifiers = [ ] requires-python = ">=3.12" dependencies = [ - # Security floors (CVE remediation): patched in python-dotenv 1.2.2 and - # requests 2.33.0. Lower bounds only, so they can still float up. + # CLI and rollout core. Security floors are kept with the feature that + # owns the dependency; lower bounds still allow patched releases to float. "python-dotenv>=1.2.2,<2.0.0", "requests>=2.33.0,<3.0.0", - # 1.91.1 is the tested compatibility floor and includes the proxy - # auth-bypass / SSTI / SQLi / RCE fixes from the earlier 1.84.0 floor. - "litellm>=1.91.1,<2.0.0", - # LiteLLM's tool-call path imports orjson at runtime, but only declares it - # through its larger proxy extra. - "orjson>=3.11.6,<4.0", - "tqdm>=4.0.0,<5.0.0", - # Remote rollout SDK dependencies "httpx>=0.25.0,<1.0.0", "pydantic>=2.0.0,<3.0.0", - "strands-agents[litellm]>=1.29.0", - # >=0.18.1 supports OpenAI Python 2.45's cache_write_tokens usage field. - "openai-agents[litellm]>=0.18.1,<0.19", # Secure credential storage (system keychain) "keyring>=25.0.0", + # Loaded via keyring's SecretService backend on Linux. >=48.0.1 is the + # security floor for the credential-storage feature. + "cryptography>=48.0.1", # Interactive CLI prompts "questionary>=2.1.0,<3.0.0", + # The CLI customizes prompt-toolkit directly rather than only through + # questionary, so keep it as an explicit dependency. + "prompt-toolkit>=3.0.0,<4.0.0", # Requirement parsing for the submit preflight. Transitive via setuptools and - # litellm, but declared because the CLI imports it directly. + # other optional features, but declared because the CLI imports it directly. "packaging>=24.0", # CLI framework. <0.28 keeps cli/_click_compat.py's private imports stable. "typer>=0.27,<0.28", # Do not directly constrain typer-slim: versions <0.22 shared typer/* with # typer, and upgrading that legacy distribution can delete Typer's files. "rich>=14.2.0", - # Pinned so the CLI preflight and the rollout server resolve the same harbor; - # a harbor release cannot change what a rollout runs without an SDK release. - # EnvironmentType.SKYPILOT needs >=0.20.0. Never the [skypilot] extra: its - # skypilot-nightly claims the `sky` namespace the rollout runtime provides. - "harbor[daytona]>=0.20.0,<0.21", - # Security floors for load-bearing transitive deps (CVE remediation): - # aiohttp is exercised via litellm's async transport, and cryptography is - # loaded via keyring's SecretService backend on Linux. Lower bounds only, - # so they can still float up with their parents. - "aiohttp>=3.14.1", - "cryptography>=48.0.1", - # mcp arrives transitively via strands-agents and openai-agents. The SDK - # itself never imports it, but >=1.28.1 is the floor that clears the - # server-transport advisories (GHSA-hvrp-rf83-w775, GHSA-jpw9-pfvf-9f58, - # GHSA-vj7q-gjh5-988w) for consumers that do build MCP servers on top. - "mcp>=1.28.1", - # External click arrives via litellm/pyiceberg/uvicorn. >=8.3.3 drops the - # shell=True in click.edit()/pager (CVE-2026-7246). NOTE: this is purely a - # floor — CLI code must still never import click, since Typer vendors its - # own copy (see osmosis_ai/cli/_click_compat.py); the vendored copy was - # never affected. - "click>=8.3.3", ] [project.optional-dependencies] -# Platform CLI (dataset upload/validate) -platform = [ - # Parquet file validation. >=23.0.1 is the CVE security floor. - "pyarrow>=23.0.1", -] - # Server dependencies for rollout serving server = [ - "osmosis-ai[platform]", "fastapi>=0.100.0,<1.0.0", "uvicorn>=0.23.0,<1.0.0", + # Uvicorn installs external Click. >=8.3.3 removes shell=True from + # click.edit()/pager (CVE-2026-7246). The SDK CLI uses Typer's vendored + # Click and must continue to import it only through typer._click. + "click>=8.3.3", +] + +# Strands Agents adapter. LiteLLM's tool-call path imports orjson at runtime, +# although LiteLLM declares it only through its much larger proxy extra. +strands = [ + "strands-agents[litellm]>=1.29.0,<2.0.0", + "litellm>=1.91.1,<2.0.0", + "orjson>=3.11.6,<4.0", + "aiohttp>=3.14.1", + "mcp>=1.28.1,<2.0.0", + "click>=8.3.3", +] + +# OpenAI Agents adapter. >=0.18.1 supports OpenAI Python 2.45's +# cache_write_tokens usage field. +openai-agents = [ + "openai-agents[litellm]>=0.18.1,<0.19", + "litellm>=1.91.1,<2.0.0", + "orjson>=3.11.6,<4.0", + "aiohttp>=3.14.1", + "mcp>=1.28.1,<2.0.0", + "click>=8.3.3", ] -# Development dependencies (testing, formatting, type checking, etc.) -# Includes server extra so pyright can resolve server-related imports. +# Harbor execution backend. Keep Harbor on the reviewed 0.20 release line so +# rollout behavior cannot cross a compatibility boundary without an SDK +# release. Daytona is retired. SkyPilot is supplied by the rollout runtime; +# never install Harbor's [skypilot] extra because its skypilot-nightly package +# claims the same `sky` namespace as that runtime. +harbor = [ + "harbor>=0.20.0,<0.21", + # Harbor's SkyPilot environment parses Dockerfile WORKDIR itself, but the + # parser is otherwise declared only by Harbor's conflicting [skypilot] + # extra. Supply that narrow dependency without installing SkyPilot. + "dockerfile-parse>=2.0.1,<3.0.0", + # Imported directly when generating Harbor task configuration. + "toml>=0.10.2,<1.0.0", + # Harbor currently uses LiteLLM and PyIceberg, which own these runtime and + # security floors in this feature. + "litellm>=1.91.1,<2.0.0", + "orjson>=3.11.6,<4.0", + "aiohttp>=3.14.1", + "click>=8.3.3", +] + +# LLM-as-judge rubric evaluation +rubric = [ + "litellm>=1.91.1,<2.0.0", + "orjson>=3.11.6,<4.0", + "tqdm>=4.0.0,<5.0.0", + "aiohttp>=3.14.1", + "click>=8.3.3", +] + +# Parquet dataset validation. >=23.0.1 is the CVE security floor. +parquet = [ + "pyarrow>=23.0.1", +] + +# Full installation with all optional features +full = [ + "osmosis-ai[server,strands,openai-agents,harbor,rubric,parquet]", +] + +[dependency-groups] +# Local development environment. Runtime features remain project extras, so +# sync this group together with --all-extras for full test/type-check coverage. dev = [ - "osmosis-ai[server]", "pytest>=9.0.3,<10.0.0", "pytest-asyncio>=1.4.0,<2.0.0", "pytest-cov>=7.1.0", @@ -102,11 +134,6 @@ dev = [ "types-requests>=2.0", ] -# Full installation with all optional features -full = [ - "osmosis-ai[server]", -] - [project.urls] Homepage = "https://github.com/Osmosis-AI/osmosis-sdk-python" Issues = "https://github.com/Osmosis-AI/osmosis-sdk-python/issues" diff --git a/tests/unit/cli/test_rollout_init.py b/tests/unit/cli/test_rollout_init.py index cafbdabc..69a73d62 100644 --- a/tests/unit/cli/test_rollout_init.py +++ b/tests/unit/cli/test_rollout_init.py @@ -124,7 +124,11 @@ def test_rollout_init_main_py_is_a_runnable_rollout_server( assert "from osmosis_ai.rollout.backend.local import LocalBackend" in main_py assert "from osmosis_ai.rollout.server import create_rollout_server" in main_py assert "osmosis_ai.rollout.integrations.agents.openai_agents" in main_py - assert "osmosis_ai.rollout.integrations.agents.openai " not in main_py + assert "osmosis_ai.rollout.integrations.openai_agents" not in main_py + pyproject_toml = ( + workspace_directory / "rollouts" / "my-agent" / "pyproject.toml" + ).read_text(encoding="utf-8") + assert '"osmosis-ai[server]>=0.3.0rc1,<0.4"' in pyproject_toml assert "osmosis rollout serve" not in main_py assert "def main()" in main_py assert 'if __name__ == "__main__":' in main_py diff --git a/tests/unit/platform/cli/test_workspace_directory_contract.py b/tests/unit/platform/cli/test_workspace_directory_contract.py index c35928d5..052231ae 100644 --- a/tests/unit/platform/cli/test_workspace_directory_contract.py +++ b/tests/unit/platform/cli/test_workspace_directory_contract.py @@ -302,6 +302,76 @@ def test_unsatisfied_requirements_reports_missing_distribution( assert "not installed" in unsatisfied[0] +def test_unsatisfied_requirements_reports_missing_extra_dependency( + tmp_path: Path, monkeypatch: pytest.MonkeyPatch +) -> None: + project = _make_workspace_directory(tmp_path / "project") + _make_rollout( + project, + "demo", + dependencies='"osmosis-ai[strands]"', + entrypoint="", + ) + real_version = workspace_directory_contract.installed_version + + def fake_version(name: str) -> str: + if name == "strands-agents": + raise workspace_directory_contract.PackageNotFoundError(name) + return real_version(name) + + monkeypatch.setattr(workspace_directory_contract, "installed_version", fake_version) + monkeypatch.setattr( + workspace_directory_contract, + "installed_requirements", + lambda name: ( + [ + 'strands-agents[litellm]>=1.29.0; extra == "strands"', + 'fastapi>=0.100.0; extra == "server"', + ] + if name == "osmosis-ai" + else [] + ), + ) + + unsatisfied = workspace_directory_contract._unsatisfied_rollout_requirements( + project / "rollouts" / "demo" + ) + + assert unsatisfied == [ + "osmosis-ai[strands] requires strands-agents, which is not installed" + ] + + +def test_unsatisfied_requirements_ignores_unrequested_extra_dependency( + tmp_path: Path, monkeypatch: pytest.MonkeyPatch +) -> None: + project = _make_workspace_directory(tmp_path / "project") + _make_rollout( + project, + "demo", + dependencies='"osmosis-ai[server]"', + entrypoint="", + ) + monkeypatch.setattr( + workspace_directory_contract, + "installed_requirements", + lambda name: ( + [ + 'strands-agents[litellm]>=1.29.0; extra == "strands"', + ] + if name == "osmosis-ai" + else [] + ), + ) + + assert ( + workspace_directory_contract._unsatisfied_rollout_requirements( + project / "rollouts" / "demo" + ) + == [] + ) + + @pytest.mark.parametrize( "dependency", [ diff --git a/tests/unit/rollout/integrations/test_openai_agents.py b/tests/unit/rollout/integrations/test_openai_agents.py index 4b9d50f5..014377e2 100644 --- a/tests/unit/rollout/integrations/test_openai_agents.py +++ b/tests/unit/rollout/integrations/test_openai_agents.py @@ -52,8 +52,7 @@ async def test_trajectory_conversion_failure_keeps_native_messages( await session.add_items(items) with patch( - "osmosis_ai.rollout.integrations.agents.openai_agents." - "Converter.items_to_messages", + "osmosis_ai.rollout.integrations.agents.openai_agents.Converter.items_to_messages", side_effect=RuntimeError("boom"), ): sample = await rollout_context.get_sample() diff --git a/tests/unit/rollout/integrations/test_strands_integration.py b/tests/unit/rollout/integrations/test_strands_integration.py index affdf97e..ea1685c6 100644 --- a/tests/unit/rollout/integrations/test_strands_integration.py +++ b/tests/unit/rollout/integrations/test_strands_integration.py @@ -29,8 +29,7 @@ async def test_sample_source_keeps_native_messages_when_conversion_fails( messages = [{"role": "user", "content": [{"text": "hello"}]}] with patch( - "osmosis_ai.rollout.integrations.agents.strands." - "LiteLLMModel.format_request_messages", + "osmosis_ai.rollout.integrations.agents.strands.LiteLLMModel.format_request_messages", side_effect=RuntimeError("boom"), ): sample = await StrandsAgentSampleSource( diff --git a/tests/unit/rollout/test_trajectory_atif.py b/tests/unit/rollout/test_trajectory_atif.py new file mode 100644 index 00000000..a9c68551 --- /dev/null +++ b/tests/unit/rollout/test_trajectory_atif.py @@ -0,0 +1,309 @@ +"""Contract tests for the SDK-owned ATIF v1.7 models and serializer.""" + +from typing import Any + +import pytest +from pydantic import BaseModel, ValidationError + +from osmosis_ai.rollout.trajectory.atif import ( + Agent, + ContentPart, + FinalMetrics, + ImageSource, + Metrics, + Observation, + ObservationResult, + Step, + SubagentTrajectoryRef, + ToolCall, + Trajectory, + format_trajectory_json, +) + + +def _trajectory(*steps: Step, **kwargs: Any) -> Trajectory: + return Trajectory( + agent=Agent(name="osmosis-rollout-sdk", version="0.3.0"), + steps=list(steps) or [Step(step_id=1, source="user", message="hello")], + **kwargs, + ) + + +def test_models_keep_the_atif_v17_wire_fields() -> None: + expected_fields: dict[type[BaseModel], list[str]] = { + Agent: ["name", "version", "model_name", "tool_definitions", "extra"], + ImageSource: ["media_type", "path"], + ContentPart: ["type", "text", "source"], + Metrics: [ + "prompt_tokens", + "completion_tokens", + "cached_tokens", + "cost_usd", + "prompt_token_ids", + "completion_token_ids", + "logprobs", + "extra", + ], + FinalMetrics: [ + "total_prompt_tokens", + "total_completion_tokens", + "total_cached_tokens", + "total_cost_usd", + "total_steps", + "extra", + ], + ToolCall: ["tool_call_id", "function_name", "arguments", "extra"], + SubagentTrajectoryRef: [ + "trajectory_id", + "session_id", + "trajectory_path", + "extra", + ], + ObservationResult: [ + "source_call_id", + "content", + "subagent_trajectory_ref", + "extra", + ], + Observation: ["results"], + Step: [ + "step_id", + "timestamp", + "source", + "model_name", + "reasoning_effort", + "message", + "reasoning_content", + "tool_calls", + "observation", + "metrics", + "is_copied_context", + "llm_call_count", + "extra", + ], + Trajectory: [ + "schema_version", + "session_id", + "trajectory_id", + "agent", + "steps", + "notes", + "final_metrics", + "continued_trajectory_ref", + "extra", + "subagent_trajectories", + ], + } + + for model, fields in expected_fields.items(): + assert list(model.model_fields) == fields + assert model.model_config["extra"] == "forbid" + + +def test_trajectory_serializes_to_the_existing_json_shape() -> None: + trajectory = _trajectory( + Step(step_id=1, source="user", message="calculate"), + Step( + step_id=2, + timestamp="2026-07-29T08:00:00Z", + source="agent", + model_name="model-a", + reasoning_effort="high", + message="", + reasoning_content="thinking", + tool_calls=[ + ToolCall( + tool_call_id="call-1", + function_name="add", + arguments={"a": 1, "b": 2}, + ) + ], + observation=Observation( + results=[ObservationResult(source_call_id="call-1", content="3")] + ), + metrics=Metrics( + prompt_tokens=2, + completion_tokens=1, + prompt_token_ids=[10, 11], + logprobs=[-0.25], + ), + is_copied_context=False, + llm_call_count=1, + ), + session_id="session-1", + trajectory_id="trajectory-1", + final_metrics=FinalMetrics(total_prompt_tokens=2, total_steps=2), + extra={"osmosis": {"reward": None}}, + ) + + assert trajectory.to_json_dict() == { + "schema_version": "ATIF-v1.7", + "session_id": "session-1", + "trajectory_id": "trajectory-1", + "agent": {"name": "osmosis-rollout-sdk", "version": "0.3.0"}, + "steps": [ + {"step_id": 1, "source": "user", "message": "calculate"}, + { + "step_id": 2, + "timestamp": "2026-07-29T08:00:00Z", + "source": "agent", + "model_name": "model-a", + "reasoning_effort": "high", + "message": "", + "reasoning_content": "thinking", + "tool_calls": [ + { + "tool_call_id": "call-1", + "function_name": "add", + "arguments": {"a": 1, "b": 2}, + } + ], + "observation": { + "results": [{"source_call_id": "call-1", "content": "3"}] + }, + "metrics": { + "prompt_tokens": 2, + "completion_tokens": 1, + "prompt_token_ids": [10, 11], + "logprobs": [-0.25], + }, + "is_copied_context": False, + "llm_call_count": 1, + }, + ], + "final_metrics": {"total_prompt_tokens": 2, "total_steps": 2}, + # None model fields are omitted, but arbitrary metadata is preserved. + "extra": {"osmosis": {"reward": None}}, + } + + +@pytest.mark.parametrize( + ("step", "message"), + [ + ( + {"step_id": 0, "source": "user", "message": "x"}, + "greater than or equal to 1", + ), + ( + { + "step_id": 1, + "source": "user", + "message": "x", + "metrics": {"prompt_tokens": 1}, + }, + "only applicable when source is 'agent'", + ), + ( + { + "step_id": 1, + "source": "agent", + "message": "x", + "reasoning_content": "thinking", + "llm_call_count": 0, + }, + "must be absent when llm_call_count is 0", + ), + ( + { + "step_id": 1, + "source": "user", + "message": "x", + "timestamp": "not-a-timestamp", + }, + "Invalid ISO 8601 timestamp", + ), + ], +) +def test_step_validation(step: dict[str, Any], message: str) -> None: + with pytest.raises(ValidationError, match=message): + Step.model_validate(step) + + +def test_trajectory_validates_step_order_and_tool_references() -> None: + with pytest.raises(ValidationError, match=r"expected 1 .* got 2"): + _trajectory(Step(step_id=2, source="user", message="x")) + + bad_reference = Step( + step_id=1, + source="agent", + message="x", + observation=Observation( + results=[ObservationResult(source_call_id="missing", content="result")] + ), + ) + with pytest.raises(ValidationError, match="not found in step 1's tool_calls"): + _trajectory(bad_reference) + + +def test_multimodal_and_subagent_contract_validation() -> None: + with pytest.raises(ValidationError, match="required when type='image'"): + ContentPart(type="image") + with pytest.raises(ValidationError, match="must be resolvable"): + SubagentTrajectoryRef(session_id="informational-only") + + image = ContentPart( + type="image", + source=ImageSource(media_type="image/png", path="image.png"), + ) + assert _trajectory( + Step(step_id=1, source="user", message=[image]) + ).has_multimodal_content() + + subagent = _trajectory(trajectory_id="subagent-1") + with pytest.raises(ValidationError, match="not unique"): + _trajectory(subagent_trajectories=[subagent, subagent.model_copy(deep=True)]) + + +@pytest.mark.parametrize( + "value", + [float("nan"), float("inf"), float("-inf")], + ids=["nan", "positive-infinity", "negative-infinity"], +) +def test_models_reject_non_finite_floats(value: float) -> None: + with pytest.raises(ValidationError, match="finite number"): + Metrics(cost_usd=value) + with pytest.raises(ValidationError, match="finite number"): + Metrics(logprobs=[value]) + with pytest.raises(ValidationError, match="finite number"): + FinalMetrics(total_cost_usd=value) + with pytest.raises(ValidationError, match="finite number"): + Step( + step_id=1, + source="agent", + message="x", + reasoning_effort=value, + ) + + +def test_formatter_rejects_non_finite_values_in_extra_data() -> None: + with pytest.raises(ValueError, match="Out of range float values"): + format_trajectory_json({"extra": {"score": float("nan")}}) + + +def test_formatter_pretty_prints_with_compact_numeric_arrays() -> None: + formatted = format_trajectory_json( + { + "token_ids": [1, 2, -3], + "logprobs": [-0.1, 2e-5], + "mixed": [1, None], + "nested": [[1, 2], [3, 4]], + "message": "café", + } + ) + + assert ( + formatted + == """{ + \"token_ids\": [1, 2, -3], + \"logprobs\": [-0.1, 2e-05], + \"mixed\": [ + 1, + null + ], + \"nested\": [ + [1, 2], + [3, 4] + ], + \"message\": \"caf\\u00e9\" +}""" + ) diff --git a/tests/unit/rollout/test_trajectory_converter.py b/tests/unit/rollout/test_trajectory_converter.py index 80d6c21b..224eb540 100644 --- a/tests/unit/rollout/test_trajectory_converter.py +++ b/tests/unit/rollout/test_trajectory_converter.py @@ -3,8 +3,8 @@ from typing import Any import pytest -from harbor.models.trajectories import Metrics +from osmosis_ai.rollout.trajectory.atif import Metrics from osmosis_ai.rollout.trajectory.converter import ( _messages_to_steps, convert_sample_to_trajectory, @@ -347,9 +347,8 @@ def two_turn_messages() -> list[dict[str, Any]]: ] -def test_llm_call_metrics_mirrors_harbor_metrics() -> None: - """Guards the wire/storage schema boundary: if a harbor upgrade adds a - Metrics field, LlmCallMetrics (and its conversion) must follow suit.""" +def test_llm_call_metrics_mirrors_atif_metrics() -> None: + """Keep the controller wire report and persisted ATIF schema in lockstep.""" assert set(LlmCallMetrics.model_fields) - {"model_name"} == set( Metrics.model_fields ) diff --git a/tests/unit/test_public_api_imports.py b/tests/unit/test_public_api_imports.py new file mode 100644 index 00000000..8feb85ef --- /dev/null +++ b/tests/unit/test_public_api_imports.py @@ -0,0 +1,258 @@ +"""Focused tests for lightweight package initializers and public facades.""" + +from __future__ import annotations + +import subprocess +import sys +import textwrap +from pathlib import Path +from types import ModuleType + +import pytest + +REPO_ROOT = Path(__file__).parents[2] + + +def _run_python(source: str) -> subprocess.CompletedProcess[str]: + return subprocess.run( + [sys.executable, "-c", textwrap.dedent(source)], + cwd=REPO_ROOT, + capture_output=True, + text=True, + check=False, + ) + + +def test_rollout_root_exports_only_framework_neutral_core() -> None: + result = _run_python( + """ + import sys + import osmosis_ai.rollout as rollout + + expected = { + "AgentWorkflow", + "AgentWorkflowConfig", + "AgentWorkflowContext", + "BaseConfig", + "ConcurrencyConfig", + "ExecutionBackend", + "ExecutionRequest", + "ExecutionResult", + "Grader", + "GraderCompleteRequest", + "GraderConfig", + "GraderContext", + "GraderInitRequest", + "GraderInitResponse", + "GraderStatus", + "LocalBackend", + "MessageDict", + "RolloutCompleteRequest", + "RolloutContext", + "RolloutErrorCategory", + "RolloutInitRequest", + "RolloutInitResponse", + "RolloutSample", + "RolloutStatus", + "SampleSource", + "get_rollout_context", + } + assert set(rollout.__all__) == expected + + removed = { + "ControllerAuth", + "HarborAgentWorkflowContext", + "OsmosisRolloutModel", + "OsmosisStrandsAgent", + "create_rollout_server", + } + assert not (removed & set(dir(rollout))) + + optional_roots = {"agents", "fastapi", "harbor", "litellm", "strands"} + loaded_roots = {name.partition(".")[0] for name in sys.modules} + assert not (optional_roots & loaded_roots) + """ + ) + assert result.returncode == 0, result.stderr + + +def test_top_level_star_import_is_safe_without_rubric_dependencies() -> None: + result = _run_python( + """ + import builtins + + real_import = builtins.__import__ + + def guarded_import(name, globals=None, locals=None, fromlist=(), level=0): + if name == "litellm" or name.startswith("litellm."): + raise ModuleNotFoundError("No module named 'litellm'", name="litellm") + return real_import(name, globals, locals, fromlist, level) + + builtins.__import__ = guarded_import + namespace = {} + exec("from osmosis_ai import *", namespace) + assert "evaluate_rubric" not in namespace + assert "RubricResult" in namespace + + import osmosis_ai + assert "evaluate_rubric" in dir(osmosis_ai) + """ + ) + assert result.returncode == 0, result.stderr + + +def test_integration_namespaces_do_not_select_a_framework() -> None: + result = _run_python( + """ + import sys + import osmosis_ai.rollout.integrations as integrations + import osmosis_ai.rollout.integrations.agents as agent_integrations + + assert integrations.__all__ == [] + assert agent_integrations.__all__ == [] + optional_roots = {"agents", "litellm", "strands"} + loaded_roots = {name.partition(".")[0] for name in sys.modules} + assert not (optional_roots & loaded_roots) + """ + ) + assert result.returncode == 0, result.stderr + + +def test_facades_resolve_each_symbol_from_its_leaf_module() -> None: + result = _run_python( + """ + import sys + + import osmosis_ai.eval.rubric as rubric + import osmosis_ai.platform.auth as auth + import osmosis_ai.rollout.server as server + import osmosis_ai.rollout.trajectory as trajectory + import osmosis_ai.templates as templates + + assert "osmosis_ai.eval.rubric.engine" not in sys.modules + assert rubric.RubricResult.__module__ == "osmosis_ai.eval.rubric.types" + assert "osmosis_ai.eval.rubric.engine" not in sys.modules + + assert "osmosis_ai.rollout.server.app" not in sys.modules + assert server.ControllerAuth.__module__ == "osmosis_ai.rollout.server.auth" + assert "osmosis_ai.rollout.server.app" not in sys.modules + + assert "osmosis_ai.rollout.trajectory.converter" not in sys.modules + assert trajectory.TrajectoryReport.__module__ == ( + "osmosis_ai.rollout.trajectory.report" + ) + assert "osmosis_ai.rollout.trajectory.converter" not in sys.modules + + assert "osmosis_ai.platform.auth.credentials" not in sys.modules + assert auth.CONFIG_DIR.name == "osmosis" + assert "osmosis_ai.platform.auth.credentials" not in sys.modules + + assert "osmosis_ai.templates.registry" not in sys.modules + assert "list_templates" in dir(templates) + assert "osmosis_ai.templates.registry" not in sys.modules + """ + ) + assert result.returncode == 0, result.stderr + + +@pytest.mark.parametrize( + ("module_path", "symbol", "missing_module", "extra"), + [ + ( + "osmosis_ai.rollout.server", + "create_rollout_server", + "fastapi", + "server", + ), + ( + "osmosis_ai.rollout.integrations.agents.strands", + "OsmosisStrandsAgent", + "strands", + "strands", + ), + ( + "osmosis_ai.rollout.integrations.agents.openai_agents", + "OsmosisAgent", + "agents", + "openai-agents", + ), + ( + "osmosis_ai.rollout.integrations.agents.openai_agents", + "OsmosisAgent", + "litellm", + "openai-agents", + ), + ( + "osmosis_ai.rollout.backend.harbor", + "HarborBackend", + "harbor", + "harbor", + ), + ( + "osmosis_ai.eval.rubric", + "evaluate_rubric", + "litellm", + "rubric", + ), + ( + "osmosis_ai.eval.rubric.cli", + "RubricCommand", + "litellm", + "rubric", + ), + ], +) +def test_missing_optional_dependency_names_the_install_extra( + module_path: str, + symbol: str, + missing_module: str, + extra: str, +) -> None: + result = _run_python( + f""" + import builtins + import importlib + + real_import = builtins.__import__ + + def guarded_import(name, globals=None, locals=None, fromlist=(), level=0): + if name == {missing_module!r} or name.startswith({missing_module!r} + "."): + raise ModuleNotFoundError( + "No module named " + repr({missing_module!r}), + name={missing_module!r}, + ) + return real_import(name, globals, locals, fromlist, level) + + builtins.__import__ = guarded_import + try: + module = importlib.import_module({module_path!r}) + getattr(module, {symbol!r}) + except ModuleNotFoundError as exc: + expected = 'pip install "osmosis-ai[{extra}]"' + assert expected in str(exc), str(exc) + assert exc.name == {missing_module!r} + else: + raise AssertionError("expected ModuleNotFoundError") + """ + ) + assert result.returncode == 0, result.stderr + + +def test_cli_main_from_package_remains_a_module() -> None: + from osmosis_ai.cli import main + + assert isinstance(main, ModuleType) + + +def test_agent_integration_classes_use_canonical_module_paths() -> None: + from osmosis_ai.rollout.integrations.agents.openai_agents import OsmosisAgent + from osmosis_ai.rollout.integrations.agents.strands import OsmosisStrandsAgent + + assert ( + OsmosisAgent.__module__ + == "osmosis_ai.rollout.integrations.agents.openai_agents" + ) + assert ( + OsmosisStrandsAgent.__module__ + == "osmosis_ai.rollout.integrations.agents.strands" + ) diff --git a/uv.lock b/uv.lock index a3580fad..92e16a3b 100644 --- a/uv.lock +++ b/uv.lock @@ -13,15 +13,6 @@ resolution-markers = [ "python_full_version < '3.14' and sys_platform != 'emscripten' and sys_platform != 'win32'", ] -[[package]] -name = "aiofiles" -version = "24.1.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/0b/03/a88171e277e8caa88a4c77808c20ebb04ba74cc4681bf1e9416c862de237/aiofiles-24.1.0.tar.gz", hash = "sha256:22a075c9e5a3810f0c2e48f3008c94d68c65d763b9b03857924c99e57355166c", size = 30247, upload-time = "2024-06-24T11:02:03.584Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/a5/45/30bb92d442636f570cb5651bc661f52b610e2eec3f891a5dc3a4c3667db0/aiofiles-24.1.0-py3-none-any.whl", hash = "sha256:b4ec55f4195e3eb5d7abd1bf7e061763e864dd4954231fb8539a0ef8bb8260e5", size = 15896, upload-time = "2024-06-24T11:02:01.529Z" }, -] - [[package]] name = "aiohappyeyeballs" version = "2.6.1" @@ -131,18 +122,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/2a/71/6e22be134a4061ada85a92951b842f2657f17d926b727f3f94c56ae963d6/aiohttp-3.14.1-cp314-cp314t-win_arm64.whl", hash = "sha256:90d53f1609c29ccc2193945ef732428382a28f78d0456ae4d3daf0d48b74f0f6", size = 469640, upload-time = "2026-06-07T21:09:33.028Z" }, ] -[[package]] -name = "aiohttp-retry" -version = "2.9.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "aiohttp" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/9d/61/ebda4d8e3d8cfa1fd3db0fb428db2dd7461d5742cea35178277ad180b033/aiohttp_retry-2.9.1.tar.gz", hash = "sha256:8eb75e904ed4ee5c2ec242fefe85bf04240f685391c4879d8f541d6028ff01f1", size = 13608, upload-time = "2024-11-06T10:44:54.574Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/1a/99/84ba7273339d0f3dfa57901b846489d2e5c2cd731470167757f1935fffbd/aiohttp_retry-2.9.1-py3-none-any.whl", hash = "sha256:66d2759d1921838256a05a3f80ad7e724936f083e35be5abb5e16eed6be6dc54", size = 9981, upload-time = "2024-11-06T10:44:52.917Z" }, -] - [[package]] name = "aiosignal" version = "1.4.0" @@ -196,15 +175,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/3a/2a/7cc015f5b9f5db42b7d48157e23356022889fc354a2813c15934b7cb5c0e/attrs-25.4.0-py3-none-any.whl", hash = "sha256:adcf7e2a1fb3b36ac48d97835bb6d8ade15b8dcce26aba8bf1d14847b57a3373", size = 67615, upload-time = "2025-10-06T13:54:43.17Z" }, ] -[[package]] -name = "bidict" -version = "0.23.1" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/9a/6e/026678aa5a830e07cd9498a05d3e7e650a4f56a42f267a53d22bcda1bdc9/bidict-0.23.1.tar.gz", hash = "sha256:03069d763bc387bbd20e7d49914e75fc4132a41937fa3405417e1a5a2d006d71", size = 29093, upload-time = "2024-02-18T19:09:05.748Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/99/37/e8730c3587a65eb5645d4aba2d27aae48e8003614d6aaf15dda67f702f1f/bidict-0.23.1-py3-none-any.whl", hash = "sha256:5dae8d4d79b552a71cbabc7deb25dfe8ce710b17ff41711e13010ead2abfc3e5", size = 32764, upload-time = "2024-02-18T19:09:04.156Z" }, -] - [[package]] name = "boto3" version = "1.42.62" @@ -529,145 +499,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/c2/e6/f60198ea8d9dfa15fff9ed4ca02ce362f6eadd9ba757dcc50634c4257b63/cryptography-49.0.0-cp39-abi3-win_amd64.whl", hash = "sha256:026ac7423e6fa66872d3bf889be5974507da3944f866f704fa200eadacd00001", size = 3785547, upload-time = "2026-06-12T20:02:26.847Z" }, ] -[[package]] -name = "daytona" -version = "0.198.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "aiofiles" }, - { name = "aiohttp" }, - { name = "daytona-analytics-api-client" }, - { name = "daytona-analytics-api-client-async" }, - { name = "daytona-api-client" }, - { name = "daytona-api-client-async" }, - { name = "daytona-toolbox-api-client" }, - { name = "daytona-toolbox-api-client-async" }, - { name = "deprecated" }, - { name = "httpx" }, - { name = "httpx-ws" }, - { name = "obstore" }, - { name = "opentelemetry-api" }, - { name = "opentelemetry-exporter-otlp-proto-http" }, - { name = "opentelemetry-instrumentation-aiohttp-client" }, - { name = "opentelemetry-sdk" }, - { name = "pydantic" }, - { name = "python-dotenv" }, - { name = "python-multipart" }, - { name = "python-socketio", extra = ["asyncio-client", "client"] }, - { name = "toml" }, - { name = "urllib3" }, - { name = "wsproto" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/5e/1b/dd2ce6f1c0d708710d20465454227cd39b962d4766bc00c88e2818bdc161/daytona-0.198.0.tar.gz", hash = "sha256:b9513be515c742f683d9d4c2ceb2f9ca95413223656821547d63563ebbfe0b2e", size = 176071, upload-time = "2026-07-16T09:17:54.435Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/00/71/d8b27a4f6ef97f7bd1b13acf25b2c6fe66c85e23b2301bc89844f4bd3e4d/daytona-0.198.0-py3-none-any.whl", hash = "sha256:8eb19b74ed5b0bdadb35f592db59aadc86f00787dc8438d3d0a9e0e77fcd7981", size = 211796, upload-time = "2026-07-16T09:17:55.823Z" }, -] - -[[package]] -name = "daytona-analytics-api-client" -version = "0.198.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "pydantic" }, - { name = "python-dateutil" }, - { name = "typing-extensions" }, - { name = "urllib3" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/5e/fc/92666cf4cfbd72b1ef866ce9e481114616d9435daa40d8644c620bc8ebb1/daytona_analytics_api_client-0.198.0.tar.gz", hash = "sha256:8c0d05f5711042ef7524a00696dd4d375274d005689d56823264b4099be4e75c", size = 30034, upload-time = "2026-07-16T09:17:10.326Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/c4/58/1bc386dd3e617ed17596f0b98983c0d585722ffa39e59dde317fa04f5808/daytona_analytics_api_client-0.198.0-py3-none-any.whl", hash = "sha256:3fc92faf102f0affcbdd81157ea7f88bb7c92e3651d940f59ffd532ca22c40cd", size = 45004, upload-time = "2026-07-16T09:17:11.293Z" }, -] - -[[package]] -name = "daytona-analytics-api-client-async" -version = "0.198.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "aiohttp" }, - { name = "aiohttp-retry" }, - { name = "pydantic" }, - { name = "python-dateutil" }, - { name = "typing-extensions" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/f0/28/ac54bc290aeea7cf434a998a6c55e92956a31c18ce7ffffadb81b5439a99/daytona_analytics_api_client_async-0.198.0.tar.gz", hash = "sha256:b7863766be03dda2d268d999d8fb02247b48f78803529d074067a983bc3c7308", size = 30046, upload-time = "2026-07-16T09:17:01.192Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/e6/39/1bd663fd1f7f00f9a4ed7bc3fa7cebcd0c0d36cf87ce36ab9df31668fe97/daytona_analytics_api_client_async-0.198.0-py3-none-any.whl", hash = "sha256:e189c37406e1d8f9a9e5c11fb8c8ed27f8a5c6c27f40728975a014205691e329", size = 45276, upload-time = "2026-07-16T09:17:02.066Z" }, -] - -[[package]] -name = "daytona-api-client" -version = "0.198.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "pydantic" }, - { name = "python-dateutil" }, - { name = "typing-extensions" }, - { name = "urllib3" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/5d/2f/9dbfdc70ccfe5de1a335c35dedf7b2714ca9fbf7aa7b4589a4db48d77c1f/daytona_api_client-0.198.0.tar.gz", hash = "sha256:0daf3dd33a21bbf3291bf2d93bb5c916afac7abeaa227012bf0f51b504b2f33b", size = 124346, upload-time = "2026-07-16T09:17:20.104Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/5b/b0/7cf252e2d680c960f4915a0a66b7eb8027e5b8a02255db28346343b40853/daytona_api_client-0.198.0-py3-none-any.whl", hash = "sha256:1b30649218fe6e1307387de81d041c66e19a4947a88eec4caea6d9c518d15b00", size = 315467, upload-time = "2026-07-16T09:17:21.397Z" }, -] - -[[package]] -name = "daytona-api-client-async" -version = "0.198.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "aiohttp" }, - { name = "aiohttp-retry" }, - { name = "pydantic" }, - { name = "python-dateutil" }, - { name = "typing-extensions" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/af/53/27d056246593c552023c836ae5c2d8629100c328d78efea043b712a6a171/daytona_api_client_async-0.198.0.tar.gz", hash = "sha256:c76c42c08b350729eb938551df11f6e3193b5bb2d12a11488aea441fed865a1d", size = 124869, upload-time = "2026-07-16T09:17:01.926Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/a1/0c/c6fd85b80561bb3fca5a9e706385544000e1eb6a8ed67dab0b8407c7bfae/daytona_api_client_async-0.198.0-py3-none-any.whl", hash = "sha256:522d0c2235c7125a02c45a55905fed6a6583d6fc0f2bd911f5fd62bfccb356f5", size = 318067, upload-time = "2026-07-16T09:17:03.751Z" }, -] - -[[package]] -name = "daytona-toolbox-api-client" -version = "0.198.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "pydantic" }, - { name = "python-dateutil" }, - { name = "typing-extensions" }, - { name = "urllib3" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/34/f1/e933c40e1320b2bff173d4254c6757870f929ecd4f2447d1a09b9139361d/daytona_toolbox_api_client-0.198.0.tar.gz", hash = "sha256:75624bccda97346019453129c89d9fcc5979847ee249eec36aba8b7ce22d2a26", size = 86297, upload-time = "2026-07-16T09:17:11.745Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/82/4d/0ab80c2d97eb7e11129e9069729c56e216f3e979a4f889b788af07b2598d/daytona_toolbox_api_client-0.198.0-py3-none-any.whl", hash = "sha256:179d82ed726e2508e4833ef8605988400e521552df5195d9ca53e28422974dd3", size = 247056, upload-time = "2026-07-16T09:17:13.051Z" }, -] - -[[package]] -name = "daytona-toolbox-api-client-async" -version = "0.198.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "aiohttp" }, - { name = "aiohttp-retry" }, - { name = "pydantic" }, - { name = "python-dateutil" }, - { name = "typing-extensions" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/1a/0d/5b4211851b5fc6467b5b73c25536d7575583aa511c093c5ccc774b235b47/daytona_toolbox_api_client_async-0.198.0.tar.gz", hash = "sha256:89a70ca15b6a1b6ec3bb3beec13488c05d30db1cf27f935ce732283bbc3c352c", size = 80151, upload-time = "2026-07-16T09:17:01.567Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/30/7b/d3dcf6a88fbb45bce0dbad570ff466d32d8b4431d3f8efc0da49626c7988/daytona_toolbox_api_client_async-0.198.0-py3-none-any.whl", hash = "sha256:2ce23b3c464e009b09f02c398a24aa8a558edd8ef02a77de2415c05905296b8b", size = 245525, upload-time = "2026-07-16T09:17:02.904Z" }, -] - -[[package]] -name = "deprecated" -version = "1.3.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "wrapt" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/49/85/12f0a49a7c4ffb70572b6c2ef13c90c88fd190debda93b23f026b25f9634/deprecated-1.3.1.tar.gz", hash = "sha256:b1b50e0ff0c1fddaa5708a2c6b0a6588bb09b892825ab2b214ac9ea9d92a5223", size = 2932523, upload-time = "2025-10-30T08:19:02.757Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/84/d0/205d54408c08b13550c733c4b85429e7ead111c7f0014309637425520a9a/deprecated-1.3.1-py2.py3-none-any.whl", hash = "sha256:597bfef186b6f60181535a29fbe44865ce137a5079f295b479886c82729d5f3f", size = 11298, upload-time = "2025-10-30T08:19:00.758Z" }, -] - [[package]] name = "deprecation" version = "2.1.0" @@ -710,6 +541,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/12/b3/231ffd4ab1fc9d679809f356cebee130ac7daa00d6d6f3206dd4fd137e9e/distro-1.9.0-py3-none-any.whl", hash = "sha256:7bffd925d65168f85027d8da9af6bddab658135b840670a223589bc0c8ef02b2", size = 20277, upload-time = "2023-12-24T09:54:30.421Z" }, ] +[[package]] +name = "dockerfile-parse" +version = "2.0.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/92/df/929ee0b5d2c8bd8d713c45e71b94ab57c7e11e322130724d54f469b2cd48/dockerfile-parse-2.0.1.tar.gz", hash = "sha256:3184ccdc513221983e503ac00e1aa504a2aa8f84e5de673c46b0b6eee99ec7bc", size = 24556, upload-time = "2023-07-18T13:36:07.897Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7a/6c/79cd5bc1b880d8c1a9a5550aa8dacd57353fa3bb2457227e1fb47383eb49/dockerfile_parse-2.0.1-py2.py3-none-any.whl", hash = "sha256:bdffd126d2eb26acf1066acb54cb2e336682e1d72b974a40894fac76a4df17f6", size = 14845, upload-time = "2023-07-18T13:36:06.052Z" }, +] + [[package]] name = "docstring-parser" version = "0.17.0" @@ -883,18 +723,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/01/c9/97cc5aae1648dcb851958a3ddf73ccd7dbe5650d95203ecb4d7720b4cdbf/fsspec-2026.1.0-py3-none-any.whl", hash = "sha256:cb76aa913c2285a3b49bdd5fc55b1d7c708d7208126b60f2eb8194fe1b4cbdcc", size = 201838, upload-time = "2026-01-09T15:21:34.041Z" }, ] -[[package]] -name = "googleapis-common-protos" -version = "1.73.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "protobuf" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/a1/c0/4a54c386282c13449eca8bbe2ddb518181dc113e78d240458a68856b4d69/googleapis_common_protos-1.73.1.tar.gz", hash = "sha256:13114f0e9d2391756a0194c3a8131974ed7bffb06086569ba193364af59163b6", size = 147506, upload-time = "2026-03-26T22:17:38.451Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/dc/82/fcb6520612bec0c39b973a6c0954b6a0d948aadfe8f7e9487f60ceb8bfa6/googleapis_common_protos-1.73.1-py3-none-any.whl", hash = "sha256:e51f09eb0a43a8602f5a915870972e6b4a394088415c79d79605a46d8e826ee8", size = 297556, upload-time = "2026-03-26T22:15:58.455Z" }, -] - [[package]] name = "griffelib" version = "2.0.2" @@ -958,11 +786,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/76/03/b6617f32385295729f3af0ae0d512cf87ba4793b9ce462ea020d776a9025/harbor-0.20.0-py3-none-any.whl", hash = "sha256:4b7e48223aea2384cdb8c9eff35eaebd482fc9b1ec09f8193a121c47356ff19a", size = 1792416, upload-time = "2026-07-18T21:25:19.206Z" }, ] -[package.optional-dependencies] -daytona = [ - { name = "daytona" }, -] - [[package]] name = "hf-xet" version = "1.2.0" @@ -1043,21 +866,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/d2/fd/6668e5aec43ab844de6fc74927e155a3b37bf40d7c3790e49fc0406b6578/httpx_sse-0.4.3-py3-none-any.whl", hash = "sha256:0ac1c9fe3c0afad2e0ebb25a934a59f4c7823b60792691f779fad2c5568830fc", size = 8960, upload-time = "2025-10-10T21:48:21.158Z" }, ] -[[package]] -name = "httpx-ws" -version = "0.9.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "anyio" }, - { name = "httpcore" }, - { name = "httpx" }, - { name = "wsproto" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/cd/cd/ca91a07ae446451f7476bf3fcc909e98cb942ff032ebfda0e3fe449aca7b/httpx_ws-0.9.0.tar.gz", hash = "sha256:797373326f70eec1ae96f6e43ae9f12002fd7d73aee139a4985eaab964338a08", size = 107105, upload-time = "2026-03-28T14:11:10.781Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/98/f8/a6bc80313a9e93c888fa10534dfce2ad76ff86911b6f485777ce6de6a073/httpx_ws-0.9.0-py3-none-any.whl", hash = "sha256:71640d2fb1bf9a225775015b33cd755cfd4c5f7e21c885192fe3adc4c387b248", size = 15759, upload-time = "2026-03-28T14:11:11.887Z" }, -] - [[package]] name = "huggingface-hub" version = "1.3.1" @@ -1649,55 +1457,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/ab/c4/7532325f968ecfc078e8a028e69a52e4c3f95fb800906bf6931ac1e89e2b/nodejs_wheel_binaries-24.13.1-py2.py3-none-win_arm64.whl", hash = "sha256:caec398cb9e94c560bacdcba56b3828df22a355749eb291f47431af88cbf26dc", size = 38881194, upload-time = "2026-02-12T17:31:00.214Z" }, ] -[[package]] -name = "obstore" -version = "0.8.2" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "typing-extensions", marker = "python_full_version < '3.13'" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/a3/8c/9ec984edd0f3b72226adfaa19b1c61b15823b35b52f311ca4af36d009d15/obstore-0.8.2.tar.gz", hash = "sha256:a467bc4e97169e2ba749981b4fd0936015428d9b8f3fb83a5528536b1b6f377f", size = 168852, upload-time = "2025-09-16T15:34:55.786Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/2b/dc/60fefbb5736e69eab56657bca04ca64dc07fdeccb3814164a31b62ad066b/obstore-0.8.2-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:bb70ce297a47392b1d9a3e310f18d59cd5ebbb9453428210fef02ed60e4d75d1", size = 3612955, upload-time = "2025-09-16T15:33:29.527Z" }, - { url = "https://files.pythonhosted.org/packages/d2/8b/844e8f382e5a12b8a3796a05d76a03e12c7aedc13d6900419e39207d7868/obstore-0.8.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:1619bf618428abf1f607e0b219b2e230a966dcf697b717deccfa0983dd91f646", size = 3346564, upload-time = "2025-09-16T15:33:30.698Z" }, - { url = "https://files.pythonhosted.org/packages/89/73/8537f99e09a38a54a6a15ede907aa25d4da089f767a808f0b2edd9c03cec/obstore-0.8.2-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:a4605c3ed7c9515aeb4c619b5f7f2c9986ed4a79fe6045e536b5e59b804b1476", size = 3460809, upload-time = "2025-09-16T15:33:31.837Z" }, - { url = "https://files.pythonhosted.org/packages/b4/99/7714dec721e43f521d6325a82303a002cddad089437640f92542b84e9cc8/obstore-0.8.2-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ce42670417876dd8668cbb8659e860e9725e5f26bbc86449fd259970e2dd9d18", size = 3692081, upload-time = "2025-09-16T15:33:33.028Z" }, - { url = "https://files.pythonhosted.org/packages/ec/bd/4ac4175fe95a24c220a96021c25c432bcc0c0212f618be0737184eebbaad/obstore-0.8.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c4a3e893b2a06585f651c541c1972fe1e3bf999ae2a5fda052ee55eb7e6516f5", size = 3957466, upload-time = "2025-09-16T15:33:34.528Z" }, - { url = "https://files.pythonhosted.org/packages/4e/04/caa288fb735484fc5cb019bdf3d896eaccfae0ac4622e520d05692c46790/obstore-0.8.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:08462b32f95a9948ed56ed63e88406e2e5a4cae1fde198f9682e0fb8487100ed", size = 3951293, upload-time = "2025-09-16T15:33:35.733Z" }, - { url = "https://files.pythonhosted.org/packages/44/2f/d380239da2d6a1fda82e17df5dae600a404e8a93a065784518ff8325d5f6/obstore-0.8.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4a0bf7763292a8fc47d01cd66e6f19002c5c6ad4b3ed4e6b2729f5e190fa8a0d", size = 3766199, upload-time = "2025-09-16T15:33:36.904Z" }, - { url = "https://files.pythonhosted.org/packages/28/41/d391be069d3da82969b54266948b2582aeca5dd735abeda4d63dba36e07b/obstore-0.8.2-cp312-cp312-manylinux_2_24_aarch64.whl", hash = "sha256:bcd47f8126cb192cbe86942b8f73b1c45a651ce7e14c9a82c5641dfbf8be7603", size = 3529678, upload-time = "2025-09-16T15:33:38.221Z" }, - { url = "https://files.pythonhosted.org/packages/b9/4c/4862fdd1a3abde459ee8eea699b1797df638a460af235b18ca82c8fffb72/obstore-0.8.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:57eda9fd8c757c3b4fe36cf3918d7e589cc1286591295cc10b34122fa36dd3fd", size = 3698079, upload-time = "2025-09-16T15:33:39.696Z" }, - { url = "https://files.pythonhosted.org/packages/68/ca/014e747bc53b570059c27e3565b2316fbe5c107d4134551f4cd3e24aa667/obstore-0.8.2-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:ea44442aad8992166baa69f5069750979e4c5d9ffce772e61565945eea5774b9", size = 3687154, upload-time = "2025-09-16T15:33:40.92Z" }, - { url = "https://files.pythonhosted.org/packages/6f/89/6db5f8edd93028e5b8bfbeee15e6bd3e56f72106107d31cb208b57659de4/obstore-0.8.2-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:41496a3ab8527402db4142aaaf0d42df9d7d354b13ba10d9c33e0e48dd49dd96", size = 3773444, upload-time = "2025-09-16T15:33:42.123Z" }, - { url = "https://files.pythonhosted.org/packages/26/e5/c9e2cc540689c873beb61246e1615d6e38301e6a34dec424f5a5c63c1afd/obstore-0.8.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:43da209803f052df96c7c3cbec512d310982efd2407e4a435632841a51143170", size = 3939315, upload-time = "2025-09-16T15:33:43.252Z" }, - { url = "https://files.pythonhosted.org/packages/4d/c9/bb53280ca50103c1ffda373cdc9b0f835431060039c2897cbc87ddd92e42/obstore-0.8.2-cp312-cp312-win_amd64.whl", hash = "sha256:1836f5dcd49f9f2950c75889ab5c51fb290d3ea93cdc39a514541e0be3af016e", size = 3978234, upload-time = "2025-09-16T15:33:44.393Z" }, - { url = "https://files.pythonhosted.org/packages/f0/5d/8c3316cc958d386d5e6ab03e9db9ddc27f8e2141cee4a6777ae5b92f3aac/obstore-0.8.2-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:212f033e53fe6e53d64957923c5c88949a400e9027f7038c705ec2e9038be563", size = 3612027, upload-time = "2025-09-16T15:33:45.6Z" }, - { url = "https://files.pythonhosted.org/packages/ea/4d/699359774ce6330130536d008bfc32827fab0c25a00238d015a5974a3d1d/obstore-0.8.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:bee21fa4ba148d08fa90e47a96df11161661ed31e09c056a373cb2154b0f2852", size = 3344686, upload-time = "2025-09-16T15:33:47.185Z" }, - { url = "https://files.pythonhosted.org/packages/82/37/55437341f10512906e02fd9fa69a8a95ad3f2f6a916d3233fda01763d110/obstore-0.8.2-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:4c66594b59832ff1ced4c72575d9beb8b5f9b4e404ac1150a42bfb226617fd50", size = 3459860, upload-time = "2025-09-16T15:33:48.382Z" }, - { url = "https://files.pythonhosted.org/packages/7a/51/4245a616c94ee4851965e33f7a563ab4090cc81f52cc73227ff9ceca2e46/obstore-0.8.2-cp313-cp313-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:089f33af5c2fe132d00214a0c1f40601b28f23a38e24ef9f79fb0576f2730b74", size = 3691648, upload-time = "2025-09-16T15:33:49.524Z" }, - { url = "https://files.pythonhosted.org/packages/4e/f1/4e2fb24171e3ca3641a4653f006be826e7e17634b11688a5190553b00b83/obstore-0.8.2-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d87f658dfd340d5d9ea2d86a7c90d44da77a0db9e00c034367dca335735110cf", size = 3956867, upload-time = "2025-09-16T15:33:51.082Z" }, - { url = "https://files.pythonhosted.org/packages/42/f5/b703115361c798c9c1744e1e700d5908d904a8c2e2bd38bec759c9ffb469/obstore-0.8.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:6e2e4fa92828c4fbc2d487f3da2d3588701a1b67d9f6ca3c97cc2afc912e9c63", size = 3950599, upload-time = "2025-09-16T15:33:52.173Z" }, - { url = "https://files.pythonhosted.org/packages/53/20/08c6dc0f20c1394e2324b9344838e4e7af770cdcb52c30757a475f50daeb/obstore-0.8.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ab440e89c5c37a8ec230857dd65147d4b923e0cada33297135d05e0f937d696a", size = 3765865, upload-time = "2025-09-16T15:33:53.291Z" }, - { url = "https://files.pythonhosted.org/packages/77/20/77907765e29b2eba6bd8821872284d91170d7084f670855b2dfcb249ea14/obstore-0.8.2-cp313-cp313-manylinux_2_24_aarch64.whl", hash = "sha256:b9beed107c5c9cd995d4a73263861fcfbc414d58773ed65c14f80eb18258a932", size = 3529807, upload-time = "2025-09-16T15:33:54.535Z" }, - { url = "https://files.pythonhosted.org/packages/a5/f5/f629d39cc30d050f52b1bf927e4d65c1cc7d7ffbb8a635cd546b5c5219a0/obstore-0.8.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:b75b4e7746292c785e31edcd5aadc8b758238372a19d4c5e394db5c305d7d175", size = 3693629, upload-time = "2025-09-16T15:33:56.016Z" }, - { url = "https://files.pythonhosted.org/packages/30/ff/106763fd10f2a1cb47f2ef1162293c78ad52f4e73223d8d43fc6b755445d/obstore-0.8.2-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:f33e6c366869d05ab0b7f12efe63269e631c5450d95d6b4ba4c5faf63f69de70", size = 3686176, upload-time = "2025-09-16T15:33:57.247Z" }, - { url = "https://files.pythonhosted.org/packages/ce/0c/d2ccb6f32feeca906d5a7c4255340df5262af8838441ca06c9e4e37b67d5/obstore-0.8.2-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:12c885a9ce5ceb09d13cc186586c0c10b62597eff21b985f6ce8ff9dab963ad3", size = 3773081, upload-time = "2025-09-16T15:33:58.475Z" }, - { url = "https://files.pythonhosted.org/packages/fa/79/40d1cc504cefc89c9b3dd8874287f3fddc7d963a8748d6dffc5880222013/obstore-0.8.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4accc883b93349a81c9931e15dd318cc703b02bbef2805d964724c73d006d00e", size = 3938589, upload-time = "2025-09-16T15:33:59.734Z" }, - { url = "https://files.pythonhosted.org/packages/14/dd/916c6777222db3271e9fb3cf9a97ed92b3a9b3e465bdeec96de9ab809d53/obstore-0.8.2-cp313-cp313-win_amd64.whl", hash = "sha256:ec850adf9980e5788a826ccfd5819989724e2a2f712bfa3258e85966c8d9981e", size = 3977768, upload-time = "2025-09-16T15:34:01.25Z" }, - { url = "https://files.pythonhosted.org/packages/f1/61/66f8dc98bbf5613bbfe5bf21747b4c8091442977f4bd897945895ab7325c/obstore-0.8.2-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:1431e40e9bb4773a261e51b192ea6489d0799b9d4d7dbdf175cdf813eb8c0503", size = 3623364, upload-time = "2025-09-16T15:34:02.957Z" }, - { url = "https://files.pythonhosted.org/packages/1a/66/6d527b3027e42f625c8fc816ac7d19b0d6228f95bfe7666e4d6b081d2348/obstore-0.8.2-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:ddb39d4da303f50b959da000aa42734f6da7ac0cc0be2d5a7838b62c97055bb9", size = 3347764, upload-time = "2025-09-16T15:34:04.236Z" }, - { url = "https://files.pythonhosted.org/packages/0d/79/c00103302b620192ea447a948921ad3fed031ce3d19e989f038e1183f607/obstore-0.8.2-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e01f4e13783db453e17e005a4a3ceff09c41c262e44649ba169d253098c775e8", size = 3460981, upload-time = "2025-09-16T15:34:05.595Z" }, - { url = "https://files.pythonhosted.org/packages/3d/d9/bfe4ed4b1aebc45b56644dd5b943cf8e1673505cccb352e66878a457e807/obstore-0.8.2-cp314-cp314-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:df0fc2d0bc17caff9b538564ddc26d7616f7e8b7c65b1a3c90b5048a8ad2e797", size = 3692711, upload-time = "2025-09-16T15:34:06.796Z" }, - { url = "https://files.pythonhosted.org/packages/13/47/cd6c2cbb18e1f40c77e7957a4a03d2d83f1859a2e876a408f1ece81cad4c/obstore-0.8.2-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e439d06c99a140348f046c9f598ee349cc2dcd9105c15540a4b231f9cc48bbae", size = 3958362, upload-time = "2025-09-16T15:34:08.277Z" }, - { url = "https://files.pythonhosted.org/packages/3d/ea/5ee82bf23abd71c7d6a3f2d008197ae8f8f569d41314c26a8f75318245be/obstore-0.8.2-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0e37d9046669fcc59522d0faf1d105fcbfd09c84cccaaa1e809227d8e030f32c", size = 3957082, upload-time = "2025-09-16T15:34:09.477Z" }, - { url = "https://files.pythonhosted.org/packages/cb/ee/46650405e50fdaa8d95f30375491f9c91fac9517980e8a28a4a6af66927f/obstore-0.8.2-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2646fdcc4bbe92dc2bb5bcdff15574da1211f5806c002b66d514cee2a23c7cb8", size = 3775539, upload-time = "2025-09-16T15:34:10.726Z" }, - { url = "https://files.pythonhosted.org/packages/35/d6/348a7ebebe2ca3d94dfc75344ea19675ae45472823e372c1852844078307/obstore-0.8.2-cp314-cp314-manylinux_2_24_aarch64.whl", hash = "sha256:e31a7d37675056d93dfc244605089dee67f5bba30f37c88436623c8c5ad9ba9d", size = 3535048, upload-time = "2025-09-16T15:34:12.076Z" }, - { url = "https://files.pythonhosted.org/packages/41/07/b7a16cc0da91a4b902d47880ad24016abfe7880c63f7cdafda45d89a2f91/obstore-0.8.2-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:656313dd8170dde0f0cd471433283337a63912e8e790a121f7cc7639c83e3816", size = 3699035, upload-time = "2025-09-16T15:34:13.331Z" }, - { url = "https://files.pythonhosted.org/packages/7f/74/3269a3a58347e0b019742d888612c4b765293c9c75efa44e144b1e884c0d/obstore-0.8.2-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:329038c9645d6d1741e77fe1a53e28a14b1a5c1461cfe4086082ad39ebabf981", size = 3687307, upload-time = "2025-09-16T15:34:14.501Z" }, - { url = "https://files.pythonhosted.org/packages/01/f9/4fd4819ad6a49d2f462a45be453561f4caebded0dc40112deeffc34b89b1/obstore-0.8.2-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:1e4df99b369790c97c752d126b286dc86484ea49bff5782843a265221406566f", size = 3776076, upload-time = "2025-09-16T15:34:16.207Z" }, - { url = "https://files.pythonhosted.org/packages/14/dd/7c4f958fa0b9fc4778fb3d232e38b37db8c6b260f641022fbba48b049d7e/obstore-0.8.2-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:9e1c65c65e20cc990414a8a9af88209b1bbc0dd9521b5f6b0293c60e19439bb7", size = 3947445, upload-time = "2025-09-16T15:34:17.423Z" }, -] - [[package]] name = "openai" version = "2.45.0" @@ -1753,36 +1512,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/cf/df/d3f1ddf4bb4cb50ed9b1139cc7b1c54c34a1e7ce8fd1b9a37c0d1551a6bd/opentelemetry_api-1.39.1-py3-none-any.whl", hash = "sha256:2edd8463432a7f8443edce90972169b195e7d6a05500cd29e6d13898187c9950", size = 66356, upload-time = "2025-12-11T13:32:17.304Z" }, ] -[[package]] -name = "opentelemetry-exporter-otlp-proto-common" -version = "1.39.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "opentelemetry-proto" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/e9/9d/22d241b66f7bbde88a3bfa6847a351d2c46b84de23e71222c6aae25c7050/opentelemetry_exporter_otlp_proto_common-1.39.1.tar.gz", hash = "sha256:763370d4737a59741c89a67b50f9e39271639ee4afc999dadfe768541c027464", size = 20409, upload-time = "2025-12-11T13:32:40.885Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/8c/02/ffc3e143d89a27ac21fd557365b98bd0653b98de8a101151d5805b5d4c33/opentelemetry_exporter_otlp_proto_common-1.39.1-py3-none-any.whl", hash = "sha256:08f8a5862d64cc3435105686d0216c1365dc5701f86844a8cd56597d0c764fde", size = 18366, upload-time = "2025-12-11T13:32:20.2Z" }, -] - -[[package]] -name = "opentelemetry-exporter-otlp-proto-http" -version = "1.39.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "googleapis-common-protos" }, - { name = "opentelemetry-api" }, - { name = "opentelemetry-exporter-otlp-proto-common" }, - { name = "opentelemetry-proto" }, - { name = "opentelemetry-sdk" }, - { name = "requests" }, - { name = "typing-extensions" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/80/04/2a08fa9c0214ae38880df01e8bfae12b067ec0793446578575e5080d6545/opentelemetry_exporter_otlp_proto_http-1.39.1.tar.gz", hash = "sha256:31bdab9745c709ce90a49a0624c2bd445d31a28ba34275951a6a362d16a0b9cb", size = 17288, upload-time = "2025-12-11T13:32:42.029Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/95/f1/b27d3e2e003cd9a3592c43d099d2ed8d0a947c15281bf8463a256db0b46c/opentelemetry_exporter_otlp_proto_http-1.39.1-py3-none-any.whl", hash = "sha256:d9f5207183dd752a412c4cd564ca8875ececba13be6e9c6c370ffb752fd59985", size = 19641, upload-time = "2025-12-11T13:32:22.248Z" }, -] - [[package]] name = "opentelemetry-instrumentation" version = "0.60b1" @@ -1798,22 +1527,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/77/d2/6788e83c5c86a2690101681aeef27eeb2a6bf22df52d3f263a22cee20915/opentelemetry_instrumentation-0.60b1-py3-none-any.whl", hash = "sha256:04480db952b48fb1ed0073f822f0ee26012b7be7c3eac1a3793122737c78632d", size = 33096, upload-time = "2025-12-11T13:35:33.067Z" }, ] -[[package]] -name = "opentelemetry-instrumentation-aiohttp-client" -version = "0.60b1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "opentelemetry-api" }, - { name = "opentelemetry-instrumentation" }, - { name = "opentelemetry-semantic-conventions" }, - { name = "opentelemetry-util-http" }, - { name = "wrapt" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/c0/79/95be90c555fd7efde79dcba36ea5c668815aa2d0a4250b63687e0f91c74a/opentelemetry_instrumentation_aiohttp_client-0.60b1.tar.gz", hash = "sha256:d0e7d5aa057791ca4d9090b0d3c9982f253c1a24b6bc78a734fc18d8dd97927b", size = 15907, upload-time = "2025-12-11T13:36:44.434Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/ca/f4/1a1ec632c86269750ae833c8fbdd4c8d15316eb1c21e3544e34791c805ee/opentelemetry_instrumentation_aiohttp_client-0.60b1-py3-none-any.whl", hash = "sha256:34c5097256a30b16c5a2a88a409ed82b92972a494c43212c85632d204a78c2a1", size = 12694, upload-time = "2025-12-11T13:35:35.034Z" }, -] - [[package]] name = "opentelemetry-instrumentation-threading" version = "0.60b1" @@ -1828,18 +1541,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/c7/a3/448738b927bcc1843ace7d4ed55dd54441a71363075eeeee89c5944dd740/opentelemetry_instrumentation_threading-0.60b1-py3-none-any.whl", hash = "sha256:92a52a60fee5e32bc6aa8f5acd749b15691ad0bc4457a310f5736b76a6d9d1de", size = 9312, upload-time = "2025-12-11T13:36:28.434Z" }, ] -[[package]] -name = "opentelemetry-proto" -version = "1.39.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "protobuf" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/49/1d/f25d76d8260c156c40c97c9ed4511ec0f9ce353f8108ca6e7561f82a06b2/opentelemetry_proto-1.39.1.tar.gz", hash = "sha256:6c8e05144fc0d3ed4d22c2289c6b126e03bcd0e6a7da0f16cedd2e1c2772e2c8", size = 46152, upload-time = "2025-12-11T13:32:48.681Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/51/95/b40c96a7b5203005a0b03d8ce8cd212ff23f1793d5ba289c87a097571b18/opentelemetry_proto-1.39.1-py3-none-any.whl", hash = "sha256:22cdc78efd3b3765d09e68bfbd010d4fc254c9818afd0b6b423387d9dee46007", size = 72535, upload-time = "2025-12-11T13:32:33.866Z" }, -] - [[package]] name = "opentelemetry-sdk" version = "1.39.1" @@ -1867,15 +1568,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/7a/5e/5958555e09635d09b75de3c4f8b9cae7335ca545d77392ffe7331534c402/opentelemetry_semantic_conventions-0.60b1-py3-none-any.whl", hash = "sha256:9fa8c8b0c110da289809292b0591220d3a7b53c1526a23021e977d68597893fb", size = 219982, upload-time = "2025-12-11T13:32:36.955Z" }, ] -[[package]] -name = "opentelemetry-util-http" -version = "0.60b1" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/50/fc/c47bb04a1d8a941a4061307e1eddfa331ed4d0ab13d8a9781e6db256940a/opentelemetry_util_http-0.60b1.tar.gz", hash = "sha256:0d97152ca8c8a41ced7172d29d3622a219317f74ae6bb3027cfbdcf22c3cc0d6", size = 11053, upload-time = "2025-12-11T13:37:25.115Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/16/5c/d3f1733665f7cd582ef0842fb1d2ed0bc1fba10875160593342d22bba375/opentelemetry_util_http-0.60b1-py3-none-any.whl", hash = "sha256:66381ba28550c91bee14dcba8979ace443444af1ed609226634596b4b0faf199", size = 8947, upload-time = "2025-12-11T13:36:37.151Z" }, -] - [[package]] name = "orjson" version = "3.11.9" @@ -1933,90 +1625,143 @@ wheels = [ name = "osmosis-ai" source = { editable = "." } dependencies = [ - { name = "aiohttp" }, - { name = "click" }, { name = "cryptography" }, - { name = "harbor", extra = ["daytona"] }, { name = "httpx" }, { name = "keyring" }, - { name = "litellm" }, - { name = "mcp" }, - { name = "openai-agents", extra = ["litellm"] }, - { name = "orjson" }, { name = "packaging" }, + { name = "prompt-toolkit" }, { name = "pydantic" }, { name = "python-dotenv" }, { name = "questionary" }, { name = "requests" }, { name = "rich" }, - { name = "strands-agents", extra = ["litellm"] }, - { name = "tqdm" }, { name = "typer" }, ] [package.optional-dependencies] -dev = [ - { name = "fastapi" }, - { name = "pre-commit" }, - { name = "pyarrow" }, - { name = "pyright", extra = ["nodejs"] }, - { name = "pytest" }, - { name = "pytest-asyncio" }, - { name = "pytest-cov" }, - { name = "ruff" }, - { name = "types-requests" }, - { name = "uvicorn" }, -] full = [ + { name = "aiohttp" }, + { name = "click" }, + { name = "dockerfile-parse" }, { name = "fastapi" }, + { name = "harbor" }, + { name = "litellm" }, + { name = "mcp" }, + { name = "openai-agents", extra = ["litellm"] }, + { name = "orjson" }, { name = "pyarrow" }, + { name = "strands-agents", extra = ["litellm"] }, + { name = "toml" }, + { name = "tqdm" }, { name = "uvicorn" }, ] -platform = [ +harbor = [ + { name = "aiohttp" }, + { name = "click" }, + { name = "dockerfile-parse" }, + { name = "harbor" }, + { name = "litellm" }, + { name = "orjson" }, + { name = "toml" }, +] +openai-agents = [ + { name = "aiohttp" }, + { name = "click" }, + { name = "litellm" }, + { name = "mcp" }, + { name = "openai-agents", extra = ["litellm"] }, + { name = "orjson" }, +] +parquet = [ { name = "pyarrow" }, ] +rubric = [ + { name = "aiohttp" }, + { name = "click" }, + { name = "litellm" }, + { name = "orjson" }, + { name = "tqdm" }, +] server = [ + { name = "click" }, { name = "fastapi" }, - { name = "pyarrow" }, { name = "uvicorn" }, ] +strands = [ + { name = "aiohttp" }, + { name = "click" }, + { name = "litellm" }, + { name = "mcp" }, + { name = "orjson" }, + { name = "strands-agents", extra = ["litellm"] }, +] + +[package.dev-dependencies] +dev = [ + { name = "pre-commit" }, + { name = "pyright", extra = ["nodejs"] }, + { name = "pytest" }, + { name = "pytest-asyncio" }, + { name = "pytest-cov" }, + { name = "ruff" }, + { name = "types-requests" }, +] [package.metadata] requires-dist = [ - { name = "aiohttp", specifier = ">=3.14.1" }, - { name = "click", specifier = ">=8.3.3" }, + { name = "aiohttp", marker = "extra == 'harbor'", specifier = ">=3.14.1" }, + { name = "aiohttp", marker = "extra == 'openai-agents'", specifier = ">=3.14.1" }, + { name = "aiohttp", marker = "extra == 'rubric'", specifier = ">=3.14.1" }, + { name = "aiohttp", marker = "extra == 'strands'", specifier = ">=3.14.1" }, + { name = "click", marker = "extra == 'harbor'", specifier = ">=8.3.3" }, + { name = "click", marker = "extra == 'openai-agents'", specifier = ">=8.3.3" }, + { name = "click", marker = "extra == 'rubric'", specifier = ">=8.3.3" }, + { name = "click", marker = "extra == 'server'", specifier = ">=8.3.3" }, + { name = "click", marker = "extra == 'strands'", specifier = ">=8.3.3" }, { name = "cryptography", specifier = ">=48.0.1" }, + { name = "dockerfile-parse", marker = "extra == 'harbor'", specifier = ">=2.0.1,<3.0.0" }, { name = "fastapi", marker = "extra == 'server'", specifier = ">=0.100.0,<1.0.0" }, - { name = "harbor", extras = ["daytona"], specifier = ">=0.20.0,<0.21" }, + { name = "harbor", marker = "extra == 'harbor'", specifier = ">=0.20.0,<0.21" }, { name = "httpx", specifier = ">=0.25.0,<1.0.0" }, { name = "keyring", specifier = ">=25.0.0" }, - { name = "litellm", specifier = ">=1.91.1,<2.0.0" }, - { name = "mcp", specifier = ">=1.28.1" }, - { name = "openai-agents", extras = ["litellm"], specifier = ">=0.18.1,<0.19" }, - { name = "orjson", specifier = ">=3.11.6,<4.0" }, - { name = "osmosis-ai", extras = ["platform"], marker = "extra == 'server'" }, - { name = "osmosis-ai", extras = ["server"], marker = "extra == 'dev'" }, - { name = "osmosis-ai", extras = ["server"], marker = "extra == 'full'" }, + { name = "litellm", marker = "extra == 'harbor'", specifier = ">=1.91.1,<2.0.0" }, + { name = "litellm", marker = "extra == 'openai-agents'", specifier = ">=1.91.1,<2.0.0" }, + { name = "litellm", marker = "extra == 'rubric'", specifier = ">=1.91.1,<2.0.0" }, + { name = "litellm", marker = "extra == 'strands'", specifier = ">=1.91.1,<2.0.0" }, + { name = "mcp", marker = "extra == 'openai-agents'", specifier = ">=1.28.1,<2.0.0" }, + { name = "mcp", marker = "extra == 'strands'", specifier = ">=1.28.1,<2.0.0" }, + { name = "openai-agents", extras = ["litellm"], marker = "extra == 'openai-agents'", specifier = ">=0.18.1,<0.19" }, + { name = "orjson", marker = "extra == 'harbor'", specifier = ">=3.11.6,<4.0" }, + { name = "orjson", marker = "extra == 'openai-agents'", specifier = ">=3.11.6,<4.0" }, + { name = "orjson", marker = "extra == 'rubric'", specifier = ">=3.11.6,<4.0" }, + { name = "orjson", marker = "extra == 'strands'", specifier = ">=3.11.6,<4.0" }, + { name = "osmosis-ai", extras = ["harbor", "openai-agents", "parquet", "rubric", "server", "strands"], marker = "extra == 'full'" }, { name = "packaging", specifier = ">=24.0" }, - { name = "pre-commit", marker = "extra == 'dev'", specifier = ">=4.6.0,<5.0.0" }, - { name = "pyarrow", marker = "extra == 'platform'", specifier = ">=23.0.1" }, + { name = "prompt-toolkit", specifier = ">=3.0.0,<4.0.0" }, + { name = "pyarrow", marker = "extra == 'parquet'", specifier = ">=23.0.1" }, { name = "pydantic", specifier = ">=2.0.0,<3.0.0" }, - { name = "pyright", extras = ["nodejs"], marker = "extra == 'dev'", specifier = "==1.1.411" }, - { name = "pytest", marker = "extra == 'dev'", specifier = ">=9.0.3,<10.0.0" }, - { name = "pytest-asyncio", marker = "extra == 'dev'", specifier = ">=1.4.0,<2.0.0" }, - { name = "pytest-cov", marker = "extra == 'dev'", specifier = ">=7.1.0" }, { name = "python-dotenv", specifier = ">=1.2.2,<2.0.0" }, { name = "questionary", specifier = ">=2.1.0,<3.0.0" }, { name = "requests", specifier = ">=2.33.0,<3.0.0" }, { name = "rich", specifier = ">=14.2.0" }, - { name = "ruff", marker = "extra == 'dev'", specifier = "==0.16.0" }, - { name = "strands-agents", extras = ["litellm"], specifier = ">=1.29.0" }, - { name = "tqdm", specifier = ">=4.0.0,<5.0.0" }, + { name = "strands-agents", extras = ["litellm"], marker = "extra == 'strands'", specifier = ">=1.29.0,<2.0.0" }, + { name = "toml", marker = "extra == 'harbor'", specifier = ">=0.10.2,<1.0.0" }, + { name = "tqdm", marker = "extra == 'rubric'", specifier = ">=4.0.0,<5.0.0" }, { name = "typer", specifier = ">=0.27,<0.28" }, - { name = "types-requests", marker = "extra == 'dev'", specifier = ">=2.0" }, { name = "uvicorn", marker = "extra == 'server'", specifier = ">=0.23.0,<1.0.0" }, ] -provides-extras = ["platform", "server", "dev", "full"] +provides-extras = ["server", "strands", "openai-agents", "harbor", "rubric", "parquet", "full"] + +[package.metadata.requires-dev] +dev = [ + { name = "pre-commit", specifier = ">=4.6.0,<5.0.0" }, + { name = "pyright", extras = ["nodejs"], specifier = "==1.1.411" }, + { name = "pytest", specifier = ">=9.0.3,<10.0.0" }, + { name = "pytest-asyncio", specifier = ">=1.4.0,<2.0.0" }, + { name = "pytest-cov", specifier = ">=7.1.0" }, + { name = "ruff", specifier = "==0.16.0" }, + { name = "types-requests", specifier = ">=2.0" }, +] [[package]] name = "packaging" @@ -2181,21 +1926,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/5b/5a/bc7b4a4ef808fa59a816c17b20c4bef6884daebbdf627ff2a161da67da19/propcache-0.4.1-py3-none-any.whl", hash = "sha256:af2a6052aeb6cf17d3e46ee169099044fd8224cbaf75c76a2ef596e8163e2237", size = 13305, upload-time = "2025-10-08T19:49:00.792Z" }, ] -[[package]] -name = "protobuf" -version = "6.33.6" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/66/70/e908e9c5e52ef7c3a6c7902c9dfbb34c7e29c25d2f81ade3856445fd5c94/protobuf-6.33.6.tar.gz", hash = "sha256:a6768d25248312c297558af96a9f9c929e8c4cee0659cb07e780731095f38135", size = 444531, upload-time = "2026-03-18T19:05:00.988Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/fc/9f/2f509339e89cfa6f6a4c4ff50438db9ca488dec341f7e454adad60150b00/protobuf-6.33.6-cp310-abi3-win32.whl", hash = "sha256:7d29d9b65f8afef196f8334e80d6bc1d5d4adedb449971fefd3723824e6e77d3", size = 425739, upload-time = "2026-03-18T19:04:48.373Z" }, - { url = "https://files.pythonhosted.org/packages/76/5d/683efcd4798e0030c1bab27374fd13a89f7c2515fb1f3123efdfaa5eab57/protobuf-6.33.6-cp310-abi3-win_amd64.whl", hash = "sha256:0cd27b587afca21b7cfa59a74dcbd48a50f0a6400cfb59391340ad729d91d326", size = 437089, upload-time = "2026-03-18T19:04:50.381Z" }, - { url = "https://files.pythonhosted.org/packages/5c/01/a3c3ed5cd186f39e7880f8303cc51385a198a81469d53d0fdecf1f64d929/protobuf-6.33.6-cp39-abi3-macosx_10_9_universal2.whl", hash = "sha256:9720e6961b251bde64edfdab7d500725a2af5280f3f4c87e57c0208376aa8c3a", size = 427737, upload-time = "2026-03-18T19:04:51.866Z" }, - { url = "https://files.pythonhosted.org/packages/ee/90/b3c01fdec7d2f627b3a6884243ba328c1217ed2d978def5c12dc50d328a3/protobuf-6.33.6-cp39-abi3-manylinux2014_aarch64.whl", hash = "sha256:e2afbae9b8e1825e3529f88d514754e094278bb95eadc0e199751cdd9a2e82a2", size = 324610, upload-time = "2026-03-18T19:04:53.096Z" }, - { url = "https://files.pythonhosted.org/packages/9b/ca/25afc144934014700c52e05103c2421997482d561f3101ff352e1292fb81/protobuf-6.33.6-cp39-abi3-manylinux2014_s390x.whl", hash = "sha256:c96c37eec15086b79762ed265d59ab204dabc53056e3443e702d2681f4b39ce3", size = 339381, upload-time = "2026-03-18T19:04:54.616Z" }, - { url = "https://files.pythonhosted.org/packages/16/92/d1e32e3e0d894fe00b15ce28ad4944ab692713f2e7f0a99787405e43533a/protobuf-6.33.6-cp39-abi3-manylinux2014_x86_64.whl", hash = "sha256:e9db7e292e0ab79dd108d7f1a94fe31601ce1ee3f7b79e0692043423020b0593", size = 323436, upload-time = "2026-03-18T19:04:55.768Z" }, - { url = "https://files.pythonhosted.org/packages/c4/72/02445137af02769918a93807b2b7890047c32bfb9f90371cbc12688819eb/protobuf-6.33.6-py3-none-any.whl", hash = "sha256:77179e006c476e69bf8e8ce866640091ec42e1beb80b213c3900006ecfba6901", size = 170656, upload-time = "2026-03-18T19:04:59.826Z" }, -] - [[package]] name = "pyarrow" version = "24.0.0" @@ -2554,18 +2284,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/0b/d7/1959b9648791274998a9c3526f6d0ec8fd2233e4d4acce81bbae76b44b2a/python_dotenv-1.2.2-py3-none-any.whl", hash = "sha256:1d8214789a24de455a8b8bd8ae6fe3c6b69a5e3d64aa8a8e5d68e694bbcb285a", size = 22101, upload-time = "2026-03-01T16:00:25.09Z" }, ] -[[package]] -name = "python-engineio" -version = "4.13.3" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "simple-websocket" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/fb/a0/f75491f942184d9960b15e763270f765fe9f239745ca5f9e16289011aed4/python_engineio-4.13.3.tar.gz", hash = "sha256:572b7783e341fed21edbc7cea297ccd378dad79265fdde96aa4664420a7c06c9", size = 79734, upload-time = "2026-06-20T22:53:52.197Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/5b/96/82f6328e410515fab21d5602ba35b9377a47b5a141a0c1f9efa00ce21eb4/python_engineio-4.13.3-py3-none-any.whl", hash = "sha256:1f60ecaf1358190f0e26c48c578a60428dc02a8f1295bc3dbf53d1b31116821f", size = 59993, upload-time = "2026-06-20T22:53:50.775Z" }, -] - [[package]] name = "python-multipart" version = "0.0.32" @@ -2575,28 +2293,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/e1/04/e8135ebd1ad02c56ec633277529b2602ff99ff634be76cdba5744cf554fd/python_multipart-0.0.32-py3-none-any.whl", hash = "sha256:ff6d3f776f16878c894e52e107296ffc890e913c611b1a4ec6c44e2821fe2e23", size = 30042, upload-time = "2026-06-04T16:18:57.319Z" }, ] -[[package]] -name = "python-socketio" -version = "5.16.3" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "bidict" }, - { name = "python-engineio" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/32/2d/ffce71017c106b75099fea569df6518c63fee5d6202ce0cfe7b01e6f22c3/python_socketio-5.16.3.tar.gz", hash = "sha256:89b136f677ae65607a84cecda9b4d6c5377b40a97582c504c25df89af16d520e", size = 128095, upload-time = "2026-06-15T22:07:04.003Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/0a/38/8c5e72d53ff8eb27497c4f268a7f6d9121e727a50b65248288ad79a93053/python_socketio-5.16.3-py3-none-any.whl", hash = "sha256:e7ad14202a5e6448824c7c2f86161d04e13dec05992257df5c709e6a2798c041", size = 82087, upload-time = "2026-06-15T22:07:02.498Z" }, -] - -[package.optional-dependencies] -asyncio-client = [ - { name = "aiohttp" }, -] -client = [ - { name = "requests" }, - { name = "websocket-client" }, -] - [[package]] name = "pywin32" version = "311" @@ -2976,18 +2672,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/c0/44/21d6bf170bf40b41396480d8d49ad640bca3f2b02139cd52aa1e272830a5/shortuuid-1.0.13-py3-none-any.whl", hash = "sha256:a482a497300b49b4953e15108a7913244e1bb0d41f9d332f5e9925dba33a3c5a", size = 10529, upload-time = "2024-03-11T20:11:04.807Z" }, ] -[[package]] -name = "simple-websocket" -version = "1.1.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "wsproto" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/b0/d4/bfa032f961103eba93de583b161f0e6a5b63cebb8f2c7d0c6e6efe1e3d2e/simple_websocket-1.1.0.tar.gz", hash = "sha256:7939234e7aa067c534abdab3a9ed933ec9ce4691b0713c78acb195560aa52ae4", size = 17300, upload-time = "2024-10-10T22:39:31.412Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/52/59/0782e51887ac6b07ffd1570e0364cf901ebc36345fea669969d2084baebb/simple_websocket-1.1.0-py3-none-any.whl", hash = "sha256:4af6069630a38ed6c561010f0e11a5bc0d4ca569b36306eb257cd9a192497c8c", size = 13842, upload-time = "2024-10-10T22:39:29.645Z" }, -] - [[package]] name = "six" version = "1.17.0" @@ -3376,15 +3060,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/68/5a/199c59e0a824a3db2b89c5d2dade7ab5f9624dbf6448dc291b46d5ec94d3/wcwidth-0.6.0-py3-none-any.whl", hash = "sha256:1a3a1e510b553315f8e146c54764f4fb6264ffad731b3d78088cdb1478ffbdad", size = 94189, upload-time = "2026-02-06T19:19:39.646Z" }, ] -[[package]] -name = "websocket-client" -version = "1.9.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/2c/41/aa4bf9664e4cda14c3b39865b12251e8e7d239f4cd0e3cc1b6c2ccde25c1/websocket_client-1.9.0.tar.gz", hash = "sha256:9e813624b6eb619999a97dc7958469217c3176312b3a16a4bd1bc7e08a46ec98", size = 70576, upload-time = "2025-10-07T21:16:36.495Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/34/db/b10e48aa8fff7407e67470363eac595018441cf32d5e1001567a7aeba5d2/websocket_client-1.9.0-py3-none-any.whl", hash = "sha256:af248a825037ef591efbf6ed20cc5faa03d3b47b9e5a2230a529eeee1c1fc3ef", size = 82616, upload-time = "2025-10-07T21:16:34.951Z" }, -] - [[package]] name = "websockets" version = "15.0.1" @@ -3465,18 +3140,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/1f/f6/a933bd70f98e9cf3e08167fc5cd7aaaca49147e48411c0bd5ae701bb2194/wrapt-1.17.3-py3-none-any.whl", hash = "sha256:7171ae35d2c33d326ac19dd8facb1e82e5fd04ef8c6c0e394d7af55a55051c22", size = 23591, upload-time = "2025-08-12T05:53:20.674Z" }, ] -[[package]] -name = "wsproto" -version = "1.3.2" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "h11" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/c7/79/12135bdf8b9c9367b8701c2c19a14c913c120b882d50b014ca0d38083c2c/wsproto-1.3.2.tar.gz", hash = "sha256:b86885dcf294e15204919950f666e06ffc6c7c114ca900b060d6e16293528294", size = 50116, upload-time = "2025-11-20T18:18:01.871Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/a4/f5/10b68b7b1544245097b2a1b8238f66f2fc6dcaeb24ba5d917f52bd2eed4f/wsproto-1.3.2-py3-none-any.whl", hash = "sha256:61eea322cdf56e8cc904bd3ad7573359a242ba65688716b0710a5eb12beab584", size = 24405, upload-time = "2025-11-20T18:18:00.454Z" }, -] - [[package]] name = "yarl" version = "1.22.0"