From d091ff012502a651a2b5df4e515cf04f5328c799 Mon Sep 17 00:00:00 2001 From: Jaden Fix Date: Wed, 20 May 2026 15:51:31 -0700 Subject: [PATCH 01/10] Phase 1: bump roe-ai to 2.0.0 (synced 4-package release) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Joins the synced 4-package family (roe-ai, roe-typescript, roe-golang/v2, roe-mcp) on a shared 2.0.0 baseline. No API surface changes — code that worked on 1.0.79 / 1.0.80 / 1.0.801 works on 2.0.0 unchanged. Why: the previous sub-patch versioning (1.0.811, 1.0.801) permanently shadowed future weekly releases because PyPI compares patches as integers (1.0.811 > 1.0.82). Resetting to a clean major + letting roe-main's release fan-out auto-bump the patch via `roe-sdk compute-next-version` makes the shadowing problem structurally impossible. Sibling PRs in the synced 4-package land: - roe-ai/roe-mcp: Phase 1+2 bootstrap contract + 2.0.0 bump - roe-ai/roe-typescript: Phase 1 1.0.80 -> 2.0.0 - roe-ai/roe-golang: Phase 1 1.0.80 -> 2.0.0 (v2 module path) - roe-ai/roe-main: Phase 3+4 adds mcp to targets.yml + the compute-sdk-version job that drives synced patch bumps. --- CHANGELOG.md | 10 ++++++++++ README.md | 4 ++++ pyproject.toml | 2 +- uv.lock | 2 +- 4 files changed, 16 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7f535c3..6e106db 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,15 @@ # Changelog +## 2.0.0 + +Version synchronization across `roe-ai` (Python), `roe-typescript`, +`roe-golang`, and `roe-mcp`. The four packages now share a single patch +counter, driven by the SDK OpenAPI spec via the roe-main release pipeline +(see `roe-main/roe-sdk/targets.yml`). + +**No API surface changes.** Code that worked on 1.0.79 / 1.0.80 / 1.0.801 +works on 2.0.0 unchanged — only the version number moves. + ## 1.0.0 **Generated-client migration.** The hand-written API surface diff --git a/README.md b/README.md index 97bcec1..6a320fc 100644 --- a/README.md +++ b/README.md @@ -2,6 +2,10 @@ A Python SDK for the [Roe AI](https://www.roe-ai.com/) API. +> **v2.0.0** — Version synchronization across roe-ai (Python) / roe-typescript +> / roe-golang / roe-mcp. No API surface changes vs. the 1.0.x line; the four +> packages now share a single patch counter, driven by the SDK OpenAPI spec. + > **v1.0.0** — The SDK delegates to OpenAPI-generated types and transports > (`roe._generated`); ergonomic wrappers on `client.agents` and > `client.policies` remain. Noteworthy API and behavioral changes compared diff --git a/pyproject.toml b/pyproject.toml index ecc87c5..ec9f297 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "roe-ai" -version = "1.0.801" +version = "2.0.0" authors = [ { name = "Roe AI", email = "founders@roe-ai.com" }, ] diff --git a/uv.lock b/uv.lock index eda1dc1..ae89d10 100644 --- a/uv.lock +++ b/uv.lock @@ -454,7 +454,7 @@ wheels = [ [[package]] name = "roe-ai" -version = "1.0.801" +version = "2.0.0" source = { editable = "." } dependencies = [ { name = "attrs" }, From d4c59779f595cf486bf2999fa6481ce818efcbd8 Mon Sep 17 00:00:00 2001 From: Jaden Fix Date: Thu, 21 May 2026 16:05:21 -0700 Subject: [PATCH 02/10] Clarify public SDK sync README --- README.md | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 6a320fc..7a1e1aa 100644 --- a/README.md +++ b/README.md @@ -2,9 +2,10 @@ A Python SDK for the [Roe AI](https://www.roe-ai.com/) API. -> **v2.0.0** — Version synchronization across roe-ai (Python) / roe-typescript -> / roe-golang / roe-mcp. No API surface changes vs. the 1.0.x line; the four -> packages now share a single patch counter, driven by the SDK OpenAPI spec. +> **v2.0.0** — Version synchronization across the public SDKs: roe-ai +> (Python), roe-typescript, and roe-golang. No API surface changes vs. the +> 1.0.x line; the public SDK packages now share a single patch counter, +> driven by the SDK OpenAPI spec. > **v1.0.0** — The SDK delegates to OpenAPI-generated types and transports > (`roe._generated`); ergonomic wrappers on `client.agents` and From c4e7054cef81cc72f9471af820d24dcaa5a945a0 Mon Sep 17 00:00:00 2001 From: Jaden Fix Date: Thu, 21 May 2026 17:11:16 -0700 Subject: [PATCH 03/10] Generate friendly SDK wrappers for MCP tools --- CHANGELOG.md | 18 +- README.md | 25 +- openapi/openapi.yml | 201 +++++++++++ openapi/wrappers.yml | 44 +++ pyproject.toml | 1 + scripts/generate-sdk | 2 + scripts/generate_wrappers.py | 331 ++++++++++++++++++ src/roe/_generated/api/discovery/__init__.py | 1 + .../discovery_agent_engine_types_list.py | 166 +++++++++ .../discovery_supported_models_list.py | 196 +++++++++++ src/roe/_generated/api/tables/__init__.py | 1 + src/roe/_generated/api/tables/upload_table.py | 202 +++++++++++ src/roe/_generated/models/__init__.py | 12 + .../models/agent_engine_type_list.py | 115 ++++++ .../agent_engine_type_list_engines_item.py | 65 ++++ .../_generated/models/supported_llm_model.py | 156 +++++++++ .../models/supported_llm_model_list.py | 111 ++++++ .../_generated/models/table_upload_request.py | 173 +++++++++ .../models/table_upload_response.py | 99 ++++++ src/roe/api/__init__.py | 13 +- src/roe/api/_generated_registry.py | 15 + src/roe/api/discovery.py | 49 +++ src/roe/api/tables.py | 130 +++++++ src/roe/client.py | 16 + tests/unit/test_discovery.py | 168 +++++++++ tests/unit/test_tables.py | 113 ++++++ uv.lock | 6 +- 27 files changed, 2419 insertions(+), 10 deletions(-) create mode 100644 openapi/wrappers.yml create mode 100644 scripts/generate_wrappers.py create mode 100644 src/roe/_generated/api/discovery/__init__.py create mode 100644 src/roe/_generated/api/discovery/discovery_agent_engine_types_list.py create mode 100644 src/roe/_generated/api/discovery/discovery_supported_models_list.py create mode 100644 src/roe/_generated/api/tables/__init__.py create mode 100644 src/roe/_generated/api/tables/upload_table.py create mode 100644 src/roe/_generated/models/agent_engine_type_list.py create mode 100644 src/roe/_generated/models/agent_engine_type_list_engines_item.py create mode 100644 src/roe/_generated/models/supported_llm_model.py create mode 100644 src/roe/_generated/models/supported_llm_model_list.py create mode 100644 src/roe/_generated/models/table_upload_request.py create mode 100644 src/roe/_generated/models/table_upload_response.py create mode 100644 src/roe/api/_generated_registry.py create mode 100644 src/roe/api/discovery.py create mode 100644 src/roe/api/tables.py create mode 100644 tests/unit/test_discovery.py create mode 100644 tests/unit/test_tables.py diff --git a/CHANGELOG.md b/CHANGELOG.md index 6e106db..dcf3578 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,12 +3,20 @@ ## 2.0.0 Version synchronization across `roe-ai` (Python), `roe-typescript`, -`roe-golang`, and `roe-mcp`. The four packages now share a single patch -counter, driven by the SDK OpenAPI spec via the roe-main release pipeline -(see `roe-main/roe-sdk/targets.yml`). +and `roe-golang`. The public SDK packages now share a single patch counter, +driven by the SDK OpenAPI spec via the roe-main release pipeline (see +`roe-main/roe-sdk/targets.yml`). -**No API surface changes.** Code that worked on 1.0.79 / 1.0.80 / 1.0.801 -works on 2.0.0 unchanged — only the version number moves. +`roe-mcp` is a private consumer of the published Python SDK, not a public +SDK release target. + +### Added + +- Generated friendly wrapper support via `openapi/wrappers.yml` and + `scripts/generate-sdk`. +- `client.discovery.list_agent_engine_types()`. +- `client.discovery.list_supported_models(capability=...)`. +- `client.tables.upload(...)`. ## 1.0.0 diff --git a/README.md b/README.md index 7a1e1aa..b927322 100644 --- a/README.md +++ b/README.md @@ -3,9 +3,10 @@ A Python SDK for the [Roe AI](https://www.roe-ai.com/) API. > **v2.0.0** — Version synchronization across the public SDKs: roe-ai -> (Python), roe-typescript, and roe-golang. No API surface changes vs. the -> 1.0.x line; the public SDK packages now share a single patch counter, -> driven by the SDK OpenAPI spec. +> (Python), roe-typescript, and roe-golang. The public SDK packages now +> share a single patch counter, driven by the SDK OpenAPI spec. Python +> friendly wrappers are generated from `openapi/wrappers.yml`; current +> generated facades include `client.discovery` and `client.tables`. > **v1.0.0** — The SDK delegates to OpenAPI-generated types and transports > (`roe._generated`); ergonomic wrappers on `client.agents` and @@ -129,6 +130,24 @@ print(response.status_code) For typed request/response models, call the generated operation module directly — see `roe/_generated/api/` for the current surface. +## Generated Friendly APIs + +Selected SDK-visible operations are exposed as ergonomic wrappers generated +from `openapi/wrappers.yml` during `scripts/generate-sdk`. This keeps the +raw OpenAPI client and friendly `RoeClient` surface in sync without writing +new wrapper classes by hand. + +```python +engines = client.discovery.list_agent_engine_types() +models = client.discovery.list_supported_models(capability="text") + +upload = client.tables.upload( + table_name="customers", + file="customers.csv", + with_headers=True, +) +``` + ## Agent Examples ### Multimodal Extraction diff --git a/openapi/openapi.yml b/openapi/openapi.yml index d30f8a1..030e802 100644 --- a/openapi/openapi.yml +++ b/openapi/openapi.yml @@ -1209,6 +1209,31 @@ paths: format: uuid description: Organization ID. This is required for access control. It can be provided via query or request body depending on the endpoint. + /v1/agents/models/: + get: + operationId: discovery_supported_models_list + description: Returns non-deprecated text-capable model IDs accepted in engine_config.model, + with capability and context metadata. Use this before create_agent or create_agent_version + when choosing a model. The list is tenant-agnostic and excludes customer-specific + or deployment-specific providers. + summary: List supported model IDs + parameters: + - in: query + name: capability + schema: + type: string + description: 'Optional capability filter: image, audio, or video (text-capable + models are always included)' + tags: + - discovery + - sdk + responses: + '200': + content: + application/json: + schema: + $ref: '#/components/schemas/SupportedLLMModelList' + description: '' /v1/agents/run/{agent_id}/: post: operationId: agents_run @@ -1621,6 +1646,24 @@ paths: value: error: Internal server error description: Internal server error + /v1/agents/types/: + get: + operationId: discovery_agent_engine_types_list + description: Returns the production engine_class_id values accepted by agent + creation APIs, plus human-readable metadata and input schemas. Use this before + create_agent or create_agent_version when choosing an engine and constructing + engine_config. + summary: List supported agent engine types + tags: + - discovery + - sdk + responses: + '200': + content: + application/json: + schema: + $ref: '#/components/schemas/AgentEngineTypeList' + description: '' /v1/policies/: get: operationId: policies_list @@ -1930,6 +1973,35 @@ paths: schema: $ref: '#/components/schemas/PolicyVersion' description: '' + /v1/tables/upload/: + post: + operationId: upload_table + description: Create a Roe table in the authenticated organization from an uploaded + CSV file. Organization API keys are scoped to one organization; if organization_id + is supplied, it must match that organization. + summary: Upload a CSV as a Roe table + tags: + - tables + - sdk + requestBody: + content: + multipart/form-data: + schema: + $ref: '#/components/schemas/TableUploadRequest' + required: true + responses: + '201': + content: + application/json: + schema: + $ref: '#/components/schemas/TableUploadResponse' + description: '' + '400': + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + description: Bad request /v1/users/current_user/: get: operationId: users_current_user_retrieve @@ -1964,6 +2036,30 @@ components: - data_type - key - value + AgentEngineTypeList: + type: object + description: Serializer for public agent engine type discovery. + properties: + engine_types: + type: array + items: + type: string + description: Valid agent engine_class_id values accepted by create-agent + APIs + total_count: + type: integer + description: Number of engine types returned + engines: + type: array + items: + type: object + additionalProperties: {} + description: Production agent engine metadata, including descriptions, input + schemas, and default engine_config values + required: + - engine_types + - engines + - total_count AgentExecutionRequestRequest: type: object description: Serializer for agent execution requests with dynamic input fields. @@ -2734,6 +2830,111 @@ components: - display_name - email - id + SupportedLLMModel: + type: object + description: Serializer for tenant-agnostic supported LLM metadata. + properties: + id: + type: string + description: Model identifier accepted in engine_config.model + providers: + type: array + items: + type: string + description: Non-customer-specific providers registered for this model + capabilities: + type: array + items: + type: string + description: Input capabilities supported by this model + context_window: + type: integer + description: Largest context window across global providers + max_output_tokens: + type: integer + description: Largest max output token limit across global providers + supports_system_message: + type: boolean + supports_temperature: + type: boolean + supports_reasoning_effort: + type: boolean + supports_json_output: + type: boolean + supports_json_schema: + type: boolean + required: + - capabilities + - context_window + - id + - max_output_tokens + - providers + - supports_json_output + - supports_json_schema + - supports_reasoning_effort + - supports_system_message + - supports_temperature + SupportedLLMModelList: + type: object + description: Serializer for non-deprecated LLM discovery. + properties: + models: + type: array + items: + $ref: '#/components/schemas/SupportedLLMModel' + total_count: + type: integer + tenant_scope: + type: string + description: Scope of the model list; this endpoint returns all-tenants + models + required: + - models + - tenant_scope + - total_count + TableUploadRequest: + type: object + description: Serializer for public CSV table uploads. + properties: + table_name: + type: string + minLength: 1 + description: Name of the Roe table to create from the uploaded CSV + maxLength: 128 + file: + type: string + format: binary + description: CSV file to upload + with_headers: + type: boolean + default: true + description: Whether the first row of the CSV contains column headers + organization_id: + type: + - string + - 'null' + format: uuid + description: Optional organization ID. Organization API keys are already + scoped to one organization; if supplied, this must match that organization. + required: + - file + - table_name + TableUploadResponse: + type: object + description: Response payload for a public CSV table upload. + properties: + table_name: + type: string + description: Created Roe table name + organization_id: + type: string + format: uuid + description: Organization that owns the table + summary: + description: ClickHouse import summary for the uploaded file + required: + - organization_id + - table_name UpdatePolicy: type: object description: Serializer for updating policy metadata (name, description) diff --git a/openapi/wrappers.yml b/openapi/wrappers.yml new file mode 100644 index 0000000..f9dc9d6 --- /dev/null +++ b/openapi/wrappers.yml @@ -0,0 +1,44 @@ +# Friendly SDK facades generated after the raw OpenAPI client. +# +# Add entries here when an OpenAPI operation should be exposed as +# `client..()`. `scripts/generate-sdk` reads this file and writes +# the corresponding `src/roe/api/*.py` modules plus the generated API registry. +apis: + discovery: + class_name: DiscoveryAPI + docstring: API for discovering valid agent engine types and model IDs. + operations: + - kind: simple + method_name: list_agent_engine_types + docstring: Return production engine_class_id values accepted by agent creation. + endpoint_module: roe._generated.api.discovery.discovery_agent_engine_types_list + return_type: AgentEngineTypeList + return_import: roe._generated.models.agent_engine_type_list.AgentEngineTypeList + empty_response_message: agent engine discovery returned an empty response + + - kind: simple + method_name: list_supported_models + docstring: Return non-deprecated model IDs accepted in engine_config.model. + endpoint_module: roe._generated.api.discovery.discovery_supported_models_list + return_type: SupportedLLMModelList + return_import: roe._generated.models.supported_llm_model_list.SupportedLLMModelList + empty_response_message: model discovery returned an empty response + parameters: + - name: capability + annotation: str | None + default: null + pass_unset_when_none: true + + tables: + class_name: TablesAPI + docstring: API for uploading CSV files into Roe tables. + operations: + - kind: table_upload + method_name: upload + docstring: Upload a CSV file and create a Roe table. + endpoint_module: roe._generated.api.tables.upload_table + body_type: TableUploadRequest + body_import: roe._generated.models.table_upload_request.TableUploadRequest + return_type: TableUploadResponse + return_import: roe._generated.models.table_upload_response.TableUploadResponse + empty_response_message: table upload returned an empty response diff --git a/pyproject.toml b/pyproject.toml index ec9f297..6703460 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -33,6 +33,7 @@ dev = [ ] codegen = [ "openapi-python-client>=0.28,<0.29", + "ruamel-yaml>=0.18.15", ] [tool.pytest.ini_options] diff --git a/scripts/generate-sdk b/scripts/generate-sdk index 27495ce..234013e 100755 --- a/scripts/generate-sdk +++ b/scripts/generate-sdk @@ -16,3 +16,5 @@ uv run --group codegen openapi-python-client generate \ rm -rf src/roe/_generated mv "$TMP_DIR/generated" src/roe/_generated + +uv run --group codegen python scripts/generate_wrappers.py diff --git a/scripts/generate_wrappers.py b/scripts/generate_wrappers.py new file mode 100644 index 0000000..3a9fe2c --- /dev/null +++ b/scripts/generate_wrappers.py @@ -0,0 +1,331 @@ +from __future__ import annotations + +from collections import defaultdict +from pathlib import Path +from typing import Any + +from ruamel.yaml import YAML + + +ROOT_DIR = Path(__file__).resolve().parents[1] +CONTRACT_PATH = ROOT_DIR / "openapi" / "wrappers.yml" +API_DIR = ROOT_DIR / "src" / "roe" / "api" +REGISTRY_PATH = API_DIR / "_generated_registry.py" + +HEADER = ( + '"""Auto-generated friendly API facades for the Roe AI SDK."""\n' + "\n" + "# Generated by scripts/generate-sdk from openapi/wrappers.yml.\n" + "# Do not edit by hand.\n" + "\n" + "from __future__ import annotations\n" + "\n" +) + + +def _load_contract() -> dict[str, Any]: + yaml = YAML(typ="safe") + data = yaml.load(CONTRACT_PATH.read_text()) + if not isinstance(data, dict) or not isinstance(data.get("apis"), dict): + raise ValueError(f"{CONTRACT_PATH} must contain an 'apis' mapping") + return data + + +def _module_import_parts(module_path: str) -> tuple[str, str]: + package, module_name = module_path.rsplit(".", 1) + return package, module_name + + +def _class_import_parts(import_path: str) -> tuple[str, str]: + module, class_name = import_path.rsplit(".", 1) + return module, class_name + + +def _default_expr(value: Any) -> str: + if value is None: + return "None" + return repr(value) + + +def _simple_method(operation: dict[str, Any]) -> str: + method_name = operation["method_name"] + endpoint_name = _module_import_parts(operation["endpoint_module"])[1] + return_type = operation["return_type"] + docstring = operation.get("docstring", "") + empty_message = operation["empty_response_message"] + params = operation.get("parameters") or [] + + signature_parts = ["self"] + for param in params: + signature_parts.append( + f"{param['name']}: {param['annotation']} = {_default_expr(param.get('default'))}" + ) + if len(signature_parts) == 1: + signature = "self" + signature_prefix = f" def {method_name}({signature}) -> {return_type}:\n" + else: + formatted_signature = ",\n".join(f" {part}" for part in signature_parts) + signature_prefix = ( + f" def {method_name}(\n{formatted_signature},\n ) -> {return_type}:\n" + ) + + call_args = ["client=self._raw"] + for param in params: + name = param["name"] + if param.get("pass_unset_when_none"): + call_args.append(f"{name}={name} if {name} is not None else UNSET") + else: + call_args.append(f"{name}={name}") + + call_args_block = ",\n".join(f" {arg}" for arg in call_args) + + return ( + signature_prefix + f' """{docstring}"""\n' + f" response = {endpoint_name}.sync_detailed(\n" + f"{call_args_block},\n" + f" )\n" + " translate_response(response)\n" + " if response.parsed is None:\n" + f' raise RoeAPIException("{empty_message}")\n' + " return response.parsed\n" + ) + + +def _table_upload_method(operation: dict[str, Any]) -> str: + endpoint_name = _module_import_parts(operation["endpoint_module"])[1] + method_name = operation["method_name"] + return_type = operation["return_type"] + body_type = operation["body_type"] + docstring = operation.get("docstring", "") + empty_message = operation["empty_response_message"] + + return f''' def {method_name}( + self, + *, + table_name: str, + file: str | Path | bytes | BinaryIO | FileUpload, + with_headers: bool = True, + organization_id: str | UUID | None = None, + filename: str | None = None, + mime_type: str | None = None, + ) -> {return_type}: + """{docstring} + + Args: + table_name: Name of the table to create. + file: CSV file path, bytes, binary file object, or ``FileUpload``. + with_headers: Whether the first CSV row contains column headers. + organization_id: Optional override; defaults to the client's configured org. + filename: Filename to use for bytes/file objects. + mime_type: MIME type override. Defaults to ``text/csv`` for ``.csv`` names. + """ + resolved_org: UUID | Unset + candidate = organization_id or self.config.organization_id + resolved_org = UUID(str(candidate)) if candidate else UNSET + + upload_file, close_after = self._as_generated_file(file, filename, mime_type) + try: + body = {body_type}( + table_name=table_name, + file=upload_file, + with_headers=with_headers, + organization_id=resolved_org, + ) + response = {endpoint_name}.sync_detailed(client=self._raw, body=body) + translate_response(response) + if not isinstance(response.parsed, {return_type}): + raise RoeAPIException("{empty_message}") + return response.parsed + finally: + if close_after: + upload_file.payload.close() + + @staticmethod + def _as_generated_file( + file: str | Path | bytes | BinaryIO | FileUpload, + filename: str | None, + mime_type: str | None, + ) -> tuple[File, bool]: + if isinstance(file, FileUpload): + payload = file.open() + effective_filename = filename or file.effective_filename + effective_mime_type = mime_type or file.effective_mime_type + return ( + File( + payload=payload, + file_name=effective_filename, + mime_type=effective_mime_type, + ), + file.path is not None, + ) + + if isinstance(file, (str, Path)): + path = Path(file) + payload = path.open("rb") + effective_filename = filename or path.name + return ( + File( + payload=payload, + file_name=effective_filename, + mime_type=_mime_type(effective_filename, mime_type), + ), + True, + ) + + if isinstance(file, bytes): + effective_filename = filename or "upload.csv" + return ( + File( + payload=BytesIO(file), + file_name=effective_filename, + mime_type=_mime_type(effective_filename, mime_type), + ), + True, + ) + + effective_filename = filename or Path(getattr(file, "name", "upload.csv")).name + return ( + File( + payload=file, + file_name=effective_filename, + mime_type=_mime_type(effective_filename, mime_type), + ), + False, + ) +''' + + +def _render_api_module(api_name: str, spec: dict[str, Any]) -> str: + class_name = spec["class_name"] + operations = spec.get("operations") or [] + + endpoint_imports: dict[str, list[str]] = defaultdict(list) + model_imports: dict[str, list[str]] = defaultdict(list) + needs_unset = False + needs_roe_api_exception = False + needs_table_upload_helpers = False + + methods: list[str] = [] + for operation in operations: + package, endpoint_name = _module_import_parts(operation["endpoint_module"]) + endpoint_imports[package].append(endpoint_name) + + return_module, return_class = _class_import_parts(operation["return_import"]) + model_imports[return_module].append(return_class) + + kind = operation.get("kind", "simple") + if kind == "simple": + needs_roe_api_exception = True + if any( + param.get("pass_unset_when_none") + for param in operation.get("parameters") or [] + ): + needs_unset = True + methods.append(_simple_method(operation)) + elif kind == "table_upload": + needs_roe_api_exception = True + needs_table_upload_helpers = True + needs_unset = True + body_module, body_class = _class_import_parts(operation["body_import"]) + model_imports[body_module].append(body_class) + methods.append(_table_upload_method(operation)) + else: + raise ValueError(f"Unsupported wrapper kind {kind!r} in {api_name}") + + lines = [HEADER] + if needs_table_upload_helpers: + lines.append("from io import BytesIO\n") + lines.append("import mimetypes\n") + lines.append("from pathlib import Path\n") + lines.append("from typing import BinaryIO\n") + lines.append("from uuid import UUID\n") + lines.append("\n") + + for package, names in sorted(endpoint_imports.items()): + unique_names = sorted(set(names)) + if len(unique_names) == 1: + lines.append(f"from {package} import {unique_names[0]}\n") + else: + lines.append(f"from {package} import (\n") + for name in unique_names: + lines.append(f" {name},\n") + lines.append(")\n") + lines.append("from roe._generated.client import AuthenticatedClient\n") + for module, names in sorted(model_imports.items()): + joined = ", ".join(sorted(set(names))) + lines.append(f"from {module} import {joined}\n") + if needs_table_upload_helpers: + lines.append("from roe._generated.types import File, UNSET, Unset\n") + elif needs_unset: + lines.append("from roe._generated.types import UNSET\n") + lines.append("from roe.config import RoeConfig\n") + if needs_roe_api_exception: + lines.append("from roe.exceptions import RoeAPIException, translate_response\n") + else: + lines.append("from roe.exceptions import translate_response\n") + if needs_table_upload_helpers: + lines.append("from roe.models import FileUpload\n") + + lines.append("\n\n") + lines.append(f"class {class_name}:\n") + lines.append(f' """{spec.get("docstring", "")}"""\n') + lines.append("\n") + lines.append( + " def __init__(self, config: RoeConfig, raw_client: AuthenticatedClient):\n" + ) + lines.append(" self.config = config\n") + lines.append(" self._raw = raw_client\n") + lines.append("\n") + lines.append("\n".join(methods)) + + if needs_table_upload_helpers: + lines.append( + "\n\ndef _mime_type(filename: str, override: str | None) -> str:\n" + " if override:\n" + " return override\n" + " guessed, _ = mimetypes.guess_type(filename)\n" + ' return guessed or "text/csv"\n' + ) + + content = "".join(lines) + return content.rstrip() + "\n" + + +def _render_registry(apis: dict[str, dict[str, Any]]) -> str: + lines = [ + '"""Auto-generated friendly API registry for the Roe AI SDK."""\n', + "\n", + "# Generated by scripts/generate-sdk from openapi/wrappers.yml.\n", + "# Do not edit by hand.\n", + "\n", + "from __future__ import annotations\n", + "\n", + ] + for api_name, spec in sorted(apis.items()): + lines.append(f"from roe.api.{api_name} import {spec['class_name']}\n") + lines.append("\n\n") + lines.append("GENERATED_API_CLASSES = {\n") + for api_name, spec in sorted(apis.items()): + lines.append(f' "{api_name}": {spec["class_name"]},\n') + lines.append("}\n") + return "".join(lines) + + +def main() -> None: + contract = _load_contract() + apis = contract["apis"] + API_DIR.mkdir(parents=True, exist_ok=True) + + for api_name, spec in sorted(apis.items()): + target = API_DIR / f"{api_name}.py" + target.write_text(_render_api_module(api_name, spec)) + + REGISTRY_PATH.write_text(_render_registry(apis)) + print( + f"Generated {len(apis)} friendly API wrapper modules from " + f"{CONTRACT_PATH.relative_to(ROOT_DIR)}" + ) + + +if __name__ == "__main__": + main() diff --git a/src/roe/_generated/api/discovery/__init__.py b/src/roe/_generated/api/discovery/__init__.py new file mode 100644 index 0000000..c9921b5 --- /dev/null +++ b/src/roe/_generated/api/discovery/__init__.py @@ -0,0 +1 @@ +""" Contains endpoint functions for accessing the API """ diff --git a/src/roe/_generated/api/discovery/discovery_agent_engine_types_list.py b/src/roe/_generated/api/discovery/discovery_agent_engine_types_list.py new file mode 100644 index 0000000..df4e940 --- /dev/null +++ b/src/roe/_generated/api/discovery/discovery_agent_engine_types_list.py @@ -0,0 +1,166 @@ +from http import HTTPStatus +from typing import Any, cast +from urllib.parse import quote + +import httpx + +from ...client import AuthenticatedClient, Client +from ...types import Response, UNSET +from ... import errors + +from ...models.agent_engine_type_list import AgentEngineTypeList +from typing import cast + + + +def _get_kwargs( + +) -> dict[str, Any]: + + + + + + + _kwargs: dict[str, Any] = { + "method": "get", + "url": "/v1/agents/types/", + } + + + return _kwargs + + + +def _parse_response(*, client: AuthenticatedClient | Client, response: httpx.Response) -> AgentEngineTypeList | None: + if response.status_code == 200: + response_200 = AgentEngineTypeList.from_dict(response.json()) + + + + return response_200 + + if client.raise_on_unexpected_status: + raise errors.UnexpectedStatus(response.status_code, response.content) + else: + return None + + +def _build_response(*, client: AuthenticatedClient | Client, response: httpx.Response) -> Response[AgentEngineTypeList]: + return Response( + status_code=HTTPStatus(response.status_code), + content=response.content, + headers=response.headers, + parsed=_parse_response(client=client, response=response), + ) + + +def sync_detailed( + *, + client: AuthenticatedClient | Client, + +) -> Response[AgentEngineTypeList]: + """ List supported agent engine types + + Returns the production engine_class_id values accepted by agent creation APIs, plus human-readable + metadata and input schemas. Use this before create_agent or create_agent_version when choosing an + engine and constructing engine_config. + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + Response[AgentEngineTypeList] + """ + + + kwargs = _get_kwargs( + + ) + + response = client.get_httpx_client().request( + **kwargs, + ) + + return _build_response(client=client, response=response) + +def sync( + *, + client: AuthenticatedClient | Client, + +) -> AgentEngineTypeList | None: + """ List supported agent engine types + + Returns the production engine_class_id values accepted by agent creation APIs, plus human-readable + metadata and input schemas. Use this before create_agent or create_agent_version when choosing an + engine and constructing engine_config. + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + AgentEngineTypeList + """ + + + return sync_detailed( + client=client, + + ).parsed + +async def asyncio_detailed( + *, + client: AuthenticatedClient | Client, + +) -> Response[AgentEngineTypeList]: + """ List supported agent engine types + + Returns the production engine_class_id values accepted by agent creation APIs, plus human-readable + metadata and input schemas. Use this before create_agent or create_agent_version when choosing an + engine and constructing engine_config. + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + Response[AgentEngineTypeList] + """ + + + kwargs = _get_kwargs( + + ) + + response = await client.get_async_httpx_client().request( + **kwargs + ) + + return _build_response(client=client, response=response) + +async def asyncio( + *, + client: AuthenticatedClient | Client, + +) -> AgentEngineTypeList | None: + """ List supported agent engine types + + Returns the production engine_class_id values accepted by agent creation APIs, plus human-readable + metadata and input schemas. Use this before create_agent or create_agent_version when choosing an + engine and constructing engine_config. + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + AgentEngineTypeList + """ + + + return (await asyncio_detailed( + client=client, + + )).parsed diff --git a/src/roe/_generated/api/discovery/discovery_supported_models_list.py b/src/roe/_generated/api/discovery/discovery_supported_models_list.py new file mode 100644 index 0000000..d06799e --- /dev/null +++ b/src/roe/_generated/api/discovery/discovery_supported_models_list.py @@ -0,0 +1,196 @@ +from http import HTTPStatus +from typing import Any, cast +from urllib.parse import quote + +import httpx + +from ...client import AuthenticatedClient, Client +from ...types import Response, UNSET +from ... import errors + +from ...models.supported_llm_model_list import SupportedLLMModelList +from ...types import UNSET, Unset +from typing import cast + + + +def _get_kwargs( + *, + capability: str | Unset = UNSET, + +) -> dict[str, Any]: + + + + + params: dict[str, Any] = {} + + params["capability"] = capability + + + params = {k: v for k, v in params.items() if v is not UNSET and v is not None} + + + _kwargs: dict[str, Any] = { + "method": "get", + "url": "/v1/agents/models/", + "params": params, + } + + + return _kwargs + + + +def _parse_response(*, client: AuthenticatedClient | Client, response: httpx.Response) -> SupportedLLMModelList | None: + if response.status_code == 200: + response_200 = SupportedLLMModelList.from_dict(response.json()) + + + + return response_200 + + if client.raise_on_unexpected_status: + raise errors.UnexpectedStatus(response.status_code, response.content) + else: + return None + + +def _build_response(*, client: AuthenticatedClient | Client, response: httpx.Response) -> Response[SupportedLLMModelList]: + return Response( + status_code=HTTPStatus(response.status_code), + content=response.content, + headers=response.headers, + parsed=_parse_response(client=client, response=response), + ) + + +def sync_detailed( + *, + client: AuthenticatedClient | Client, + capability: str | Unset = UNSET, + +) -> Response[SupportedLLMModelList]: + """ List supported model IDs + + Returns non-deprecated text-capable model IDs accepted in engine_config.model, with capability and + context metadata. Use this before create_agent or create_agent_version when choosing a model. The + list is tenant-agnostic and excludes customer-specific or deployment-specific providers. + + Args: + capability (str | Unset): + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + Response[SupportedLLMModelList] + """ + + + kwargs = _get_kwargs( + capability=capability, + + ) + + response = client.get_httpx_client().request( + **kwargs, + ) + + return _build_response(client=client, response=response) + +def sync( + *, + client: AuthenticatedClient | Client, + capability: str | Unset = UNSET, + +) -> SupportedLLMModelList | None: + """ List supported model IDs + + Returns non-deprecated text-capable model IDs accepted in engine_config.model, with capability and + context metadata. Use this before create_agent or create_agent_version when choosing a model. The + list is tenant-agnostic and excludes customer-specific or deployment-specific providers. + + Args: + capability (str | Unset): + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + SupportedLLMModelList + """ + + + return sync_detailed( + client=client, +capability=capability, + + ).parsed + +async def asyncio_detailed( + *, + client: AuthenticatedClient | Client, + capability: str | Unset = UNSET, + +) -> Response[SupportedLLMModelList]: + """ List supported model IDs + + Returns non-deprecated text-capable model IDs accepted in engine_config.model, with capability and + context metadata. Use this before create_agent or create_agent_version when choosing a model. The + list is tenant-agnostic and excludes customer-specific or deployment-specific providers. + + Args: + capability (str | Unset): + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + Response[SupportedLLMModelList] + """ + + + kwargs = _get_kwargs( + capability=capability, + + ) + + response = await client.get_async_httpx_client().request( + **kwargs + ) + + return _build_response(client=client, response=response) + +async def asyncio( + *, + client: AuthenticatedClient | Client, + capability: str | Unset = UNSET, + +) -> SupportedLLMModelList | None: + """ List supported model IDs + + Returns non-deprecated text-capable model IDs accepted in engine_config.model, with capability and + context metadata. Use this before create_agent or create_agent_version when choosing a model. The + list is tenant-agnostic and excludes customer-specific or deployment-specific providers. + + Args: + capability (str | Unset): + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + SupportedLLMModelList + """ + + + return (await asyncio_detailed( + client=client, +capability=capability, + + )).parsed diff --git a/src/roe/_generated/api/tables/__init__.py b/src/roe/_generated/api/tables/__init__.py new file mode 100644 index 0000000..c9921b5 --- /dev/null +++ b/src/roe/_generated/api/tables/__init__.py @@ -0,0 +1 @@ +""" Contains endpoint functions for accessing the API """ diff --git a/src/roe/_generated/api/tables/upload_table.py b/src/roe/_generated/api/tables/upload_table.py new file mode 100644 index 0000000..f265427 --- /dev/null +++ b/src/roe/_generated/api/tables/upload_table.py @@ -0,0 +1,202 @@ +from http import HTTPStatus +from typing import Any, cast +from urllib.parse import quote + +import httpx + +from ...client import AuthenticatedClient, Client +from ...types import Response, UNSET +from ... import errors + +from ...models.error_response import ErrorResponse +from ...models.table_upload_request import TableUploadRequest +from ...models.table_upload_response import TableUploadResponse +from typing import cast + + + +def _get_kwargs( + *, + body: TableUploadRequest, + +) -> dict[str, Any]: + headers: dict[str, Any] = {} + + + + + + + _kwargs: dict[str, Any] = { + "method": "post", + "url": "/v1/tables/upload/", + } + + _kwargs["files"] = body.to_multipart() + + + + _kwargs["headers"] = headers + return _kwargs + + + +def _parse_response(*, client: AuthenticatedClient | Client, response: httpx.Response) -> ErrorResponse | TableUploadResponse | None: + if response.status_code == 201: + response_201 = TableUploadResponse.from_dict(response.json()) + + + + return response_201 + + if response.status_code == 400: + response_400 = ErrorResponse.from_dict(response.json()) + + + + return response_400 + + if client.raise_on_unexpected_status: + raise errors.UnexpectedStatus(response.status_code, response.content) + else: + return None + + +def _build_response(*, client: AuthenticatedClient | Client, response: httpx.Response) -> Response[ErrorResponse | TableUploadResponse]: + return Response( + status_code=HTTPStatus(response.status_code), + content=response.content, + headers=response.headers, + parsed=_parse_response(client=client, response=response), + ) + + +def sync_detailed( + *, + client: AuthenticatedClient | Client, + body: TableUploadRequest, + +) -> Response[ErrorResponse | TableUploadResponse]: + """ Upload a CSV as a Roe table + + Create a Roe table in the authenticated organization from an uploaded CSV file. Organization API + keys are scoped to one organization; if organization_id is supplied, it must match that + organization. + + Args: + body (TableUploadRequest): Serializer for public CSV table uploads. + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + Response[ErrorResponse | TableUploadResponse] + """ + + + kwargs = _get_kwargs( + body=body, + + ) + + response = client.get_httpx_client().request( + **kwargs, + ) + + return _build_response(client=client, response=response) + +def sync( + *, + client: AuthenticatedClient | Client, + body: TableUploadRequest, + +) -> ErrorResponse | TableUploadResponse | None: + """ Upload a CSV as a Roe table + + Create a Roe table in the authenticated organization from an uploaded CSV file. Organization API + keys are scoped to one organization; if organization_id is supplied, it must match that + organization. + + Args: + body (TableUploadRequest): Serializer for public CSV table uploads. + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + ErrorResponse | TableUploadResponse + """ + + + return sync_detailed( + client=client, +body=body, + + ).parsed + +async def asyncio_detailed( + *, + client: AuthenticatedClient | Client, + body: TableUploadRequest, + +) -> Response[ErrorResponse | TableUploadResponse]: + """ Upload a CSV as a Roe table + + Create a Roe table in the authenticated organization from an uploaded CSV file. Organization API + keys are scoped to one organization; if organization_id is supplied, it must match that + organization. + + Args: + body (TableUploadRequest): Serializer for public CSV table uploads. + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + Response[ErrorResponse | TableUploadResponse] + """ + + + kwargs = _get_kwargs( + body=body, + + ) + + response = await client.get_async_httpx_client().request( + **kwargs + ) + + return _build_response(client=client, response=response) + +async def asyncio( + *, + client: AuthenticatedClient | Client, + body: TableUploadRequest, + +) -> ErrorResponse | TableUploadResponse | None: + """ Upload a CSV as a Roe table + + Create a Roe table in the authenticated organization from an uploaded CSV file. Organization API + keys are scoped to one organization; if organization_id is supplied, it must match that + organization. + + Args: + body (TableUploadRequest): Serializer for public CSV table uploads. + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + ErrorResponse | TableUploadResponse + """ + + + return (await asyncio_detailed( + client=client, +body=body, + + )).parsed diff --git a/src/roe/_generated/models/__init__.py b/src/roe/_generated/models/__init__.py index 7572c7c..87132f6 100644 --- a/src/roe/_generated/models/__init__.py +++ b/src/roe/_generated/models/__init__.py @@ -1,6 +1,8 @@ """ Contains all the data models used in inputs/outputs """ from .agent_datum import AgentDatum +from .agent_engine_type_list import AgentEngineTypeList +from .agent_engine_type_list_engines_item import AgentEngineTypeListEnginesItem from .agent_execution_request_request import AgentExecutionRequestRequest from .agent_input_definition import AgentInputDefinition from .agent_job_delete_data_response import AgentJobDeleteDataResponse @@ -32,12 +34,18 @@ from .policy import Policy from .policy_version import PolicyVersion from .policy_version_created_by import PolicyVersionCreatedBy +from .supported_llm_model import SupportedLLMModel +from .supported_llm_model_list import SupportedLLMModelList +from .table_upload_request import TableUploadRequest +from .table_upload_response import TableUploadResponse from .update_policy import UpdatePolicy from .update_policy_request import UpdatePolicyRequest from .user_info import UserInfo __all__ = ( "AgentDatum", + "AgentEngineTypeList", + "AgentEngineTypeListEnginesItem", "AgentExecutionRequestRequest", "AgentInputDefinition", "AgentJobDeleteDataResponse", @@ -69,6 +77,10 @@ "Policy", "PolicyVersion", "PolicyVersionCreatedBy", + "SupportedLLMModel", + "SupportedLLMModelList", + "TableUploadRequest", + "TableUploadResponse", "UpdatePolicy", "UpdatePolicyRequest", "UserInfo", diff --git a/src/roe/_generated/models/agent_engine_type_list.py b/src/roe/_generated/models/agent_engine_type_list.py new file mode 100644 index 0000000..0e07c91 --- /dev/null +++ b/src/roe/_generated/models/agent_engine_type_list.py @@ -0,0 +1,115 @@ +from __future__ import annotations + +from collections.abc import Mapping +from typing import Any, TypeVar, BinaryIO, TextIO, TYPE_CHECKING, Generator + +from attrs import define as _attrs_define +from attrs import field as _attrs_field + +from ..types import UNSET, Unset + +from typing import cast + +if TYPE_CHECKING: + from ..models.agent_engine_type_list_engines_item import AgentEngineTypeListEnginesItem + + + + + +T = TypeVar("T", bound="AgentEngineTypeList") + + + +@_attrs_define +class AgentEngineTypeList: + """ Serializer for public agent engine type discovery. + + Attributes: + engine_types (list[str]): Valid agent engine_class_id values accepted by create-agent APIs + total_count (int): Number of engine types returned + engines (list[AgentEngineTypeListEnginesItem]): Production agent engine metadata, including descriptions, input + schemas, and default engine_config values + """ + + engine_types: list[str] + total_count: int + engines: list[AgentEngineTypeListEnginesItem] + additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict) + + + + + + def to_dict(self) -> dict[str, Any]: + from ..models.agent_engine_type_list_engines_item import AgentEngineTypeListEnginesItem + engine_types = self.engine_types + + + + total_count = self.total_count + + engines = [] + for engines_item_data in self.engines: + engines_item = engines_item_data.to_dict() + engines.append(engines_item) + + + + + field_dict: dict[str, Any] = {} + field_dict.update(self.additional_properties) + field_dict.update({ + "engine_types": engine_types, + "total_count": total_count, + "engines": engines, + }) + + return field_dict + + + + @classmethod + def from_dict(cls: type[T], src_dict: Mapping[str, Any]) -> T: + from ..models.agent_engine_type_list_engines_item import AgentEngineTypeListEnginesItem + d = dict(src_dict) + engine_types = cast(list[str], d.pop("engine_types")) + + + total_count = d.pop("total_count") + + engines = [] + _engines = d.pop("engines") + for engines_item_data in (_engines): + engines_item = AgentEngineTypeListEnginesItem.from_dict(engines_item_data) + + + + engines.append(engines_item) + + + agent_engine_type_list = cls( + engine_types=engine_types, + total_count=total_count, + engines=engines, + ) + + + agent_engine_type_list.additional_properties = d + return agent_engine_type_list + + @property + def additional_keys(self) -> list[str]: + return list(self.additional_properties.keys()) + + def __getitem__(self, key: str) -> Any: + return self.additional_properties[key] + + def __setitem__(self, key: str, value: Any) -> None: + self.additional_properties[key] = value + + def __delitem__(self, key: str) -> None: + del self.additional_properties[key] + + def __contains__(self, key: str) -> bool: + return key in self.additional_properties diff --git a/src/roe/_generated/models/agent_engine_type_list_engines_item.py b/src/roe/_generated/models/agent_engine_type_list_engines_item.py new file mode 100644 index 0000000..7fca242 --- /dev/null +++ b/src/roe/_generated/models/agent_engine_type_list_engines_item.py @@ -0,0 +1,65 @@ +from __future__ import annotations + +from collections.abc import Mapping +from typing import Any, TypeVar, BinaryIO, TextIO, TYPE_CHECKING, Generator + +from attrs import define as _attrs_define +from attrs import field as _attrs_field + +from ..types import UNSET, Unset + + + + + + + +T = TypeVar("T", bound="AgentEngineTypeListEnginesItem") + + + +@_attrs_define +class AgentEngineTypeListEnginesItem: + """ + """ + + additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict) + + + + + + def to_dict(self) -> dict[str, Any]: + + field_dict: dict[str, Any] = {} + field_dict.update(self.additional_properties) + + return field_dict + + + + @classmethod + def from_dict(cls: type[T], src_dict: Mapping[str, Any]) -> T: + d = dict(src_dict) + agent_engine_type_list_engines_item = cls( + ) + + + agent_engine_type_list_engines_item.additional_properties = d + return agent_engine_type_list_engines_item + + @property + def additional_keys(self) -> list[str]: + return list(self.additional_properties.keys()) + + def __getitem__(self, key: str) -> Any: + return self.additional_properties[key] + + def __setitem__(self, key: str, value: Any) -> None: + self.additional_properties[key] = value + + def __delitem__(self, key: str) -> None: + del self.additional_properties[key] + + def __contains__(self, key: str) -> bool: + return key in self.additional_properties diff --git a/src/roe/_generated/models/supported_llm_model.py b/src/roe/_generated/models/supported_llm_model.py new file mode 100644 index 0000000..f6c08bb --- /dev/null +++ b/src/roe/_generated/models/supported_llm_model.py @@ -0,0 +1,156 @@ +from __future__ import annotations + +from collections.abc import Mapping +from typing import Any, TypeVar, BinaryIO, TextIO, TYPE_CHECKING, Generator + +from attrs import define as _attrs_define +from attrs import field as _attrs_field + +from ..types import UNSET, Unset + +from typing import cast + + + + + + +T = TypeVar("T", bound="SupportedLLMModel") + + + +@_attrs_define +class SupportedLLMModel: + """ Serializer for tenant-agnostic supported LLM metadata. + + Attributes: + id (str): Model identifier accepted in engine_config.model + providers (list[str]): Non-customer-specific providers registered for this model + capabilities (list[str]): Input capabilities supported by this model + context_window (int): Largest context window across global providers + max_output_tokens (int): Largest max output token limit across global providers + supports_system_message (bool): + supports_temperature (bool): + supports_reasoning_effort (bool): + supports_json_output (bool): + supports_json_schema (bool): + """ + + id: str + providers: list[str] + capabilities: list[str] + context_window: int + max_output_tokens: int + supports_system_message: bool + supports_temperature: bool + supports_reasoning_effort: bool + supports_json_output: bool + supports_json_schema: bool + additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict) + + + + + + def to_dict(self) -> dict[str, Any]: + id = self.id + + providers = self.providers + + + + capabilities = self.capabilities + + + + context_window = self.context_window + + max_output_tokens = self.max_output_tokens + + supports_system_message = self.supports_system_message + + supports_temperature = self.supports_temperature + + supports_reasoning_effort = self.supports_reasoning_effort + + supports_json_output = self.supports_json_output + + supports_json_schema = self.supports_json_schema + + + field_dict: dict[str, Any] = {} + field_dict.update(self.additional_properties) + field_dict.update({ + "id": id, + "providers": providers, + "capabilities": capabilities, + "context_window": context_window, + "max_output_tokens": max_output_tokens, + "supports_system_message": supports_system_message, + "supports_temperature": supports_temperature, + "supports_reasoning_effort": supports_reasoning_effort, + "supports_json_output": supports_json_output, + "supports_json_schema": supports_json_schema, + }) + + return field_dict + + + + @classmethod + def from_dict(cls: type[T], src_dict: Mapping[str, Any]) -> T: + d = dict(src_dict) + id = d.pop("id") + + providers = cast(list[str], d.pop("providers")) + + + capabilities = cast(list[str], d.pop("capabilities")) + + + context_window = d.pop("context_window") + + max_output_tokens = d.pop("max_output_tokens") + + supports_system_message = d.pop("supports_system_message") + + supports_temperature = d.pop("supports_temperature") + + supports_reasoning_effort = d.pop("supports_reasoning_effort") + + supports_json_output = d.pop("supports_json_output") + + supports_json_schema = d.pop("supports_json_schema") + + supported_llm_model = cls( + id=id, + providers=providers, + capabilities=capabilities, + context_window=context_window, + max_output_tokens=max_output_tokens, + supports_system_message=supports_system_message, + supports_temperature=supports_temperature, + supports_reasoning_effort=supports_reasoning_effort, + supports_json_output=supports_json_output, + supports_json_schema=supports_json_schema, + ) + + + supported_llm_model.additional_properties = d + return supported_llm_model + + @property + def additional_keys(self) -> list[str]: + return list(self.additional_properties.keys()) + + def __getitem__(self, key: str) -> Any: + return self.additional_properties[key] + + def __setitem__(self, key: str, value: Any) -> None: + self.additional_properties[key] = value + + def __delitem__(self, key: str) -> None: + del self.additional_properties[key] + + def __contains__(self, key: str) -> bool: + return key in self.additional_properties diff --git a/src/roe/_generated/models/supported_llm_model_list.py b/src/roe/_generated/models/supported_llm_model_list.py new file mode 100644 index 0000000..65c2b88 --- /dev/null +++ b/src/roe/_generated/models/supported_llm_model_list.py @@ -0,0 +1,111 @@ +from __future__ import annotations + +from collections.abc import Mapping +from typing import Any, TypeVar, BinaryIO, TextIO, TYPE_CHECKING, Generator + +from attrs import define as _attrs_define +from attrs import field as _attrs_field + +from ..types import UNSET, Unset + +from typing import cast + +if TYPE_CHECKING: + from ..models.supported_llm_model import SupportedLLMModel + + + + + +T = TypeVar("T", bound="SupportedLLMModelList") + + + +@_attrs_define +class SupportedLLMModelList: + """ Serializer for non-deprecated LLM discovery. + + Attributes: + models (list[SupportedLLMModel]): + total_count (int): + tenant_scope (str): Scope of the model list; this endpoint returns all-tenants models + """ + + models: list[SupportedLLMModel] + total_count: int + tenant_scope: str + additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict) + + + + + + def to_dict(self) -> dict[str, Any]: + from ..models.supported_llm_model import SupportedLLMModel + models = [] + for models_item_data in self.models: + models_item = models_item_data.to_dict() + models.append(models_item) + + + + total_count = self.total_count + + tenant_scope = self.tenant_scope + + + field_dict: dict[str, Any] = {} + field_dict.update(self.additional_properties) + field_dict.update({ + "models": models, + "total_count": total_count, + "tenant_scope": tenant_scope, + }) + + return field_dict + + + + @classmethod + def from_dict(cls: type[T], src_dict: Mapping[str, Any]) -> T: + from ..models.supported_llm_model import SupportedLLMModel + d = dict(src_dict) + models = [] + _models = d.pop("models") + for models_item_data in (_models): + models_item = SupportedLLMModel.from_dict(models_item_data) + + + + models.append(models_item) + + + total_count = d.pop("total_count") + + tenant_scope = d.pop("tenant_scope") + + supported_llm_model_list = cls( + models=models, + total_count=total_count, + tenant_scope=tenant_scope, + ) + + + supported_llm_model_list.additional_properties = d + return supported_llm_model_list + + @property + def additional_keys(self) -> list[str]: + return list(self.additional_properties.keys()) + + def __getitem__(self, key: str) -> Any: + return self.additional_properties[key] + + def __setitem__(self, key: str, value: Any) -> None: + self.additional_properties[key] = value + + def __delitem__(self, key: str) -> None: + del self.additional_properties[key] + + def __contains__(self, key: str) -> bool: + return key in self.additional_properties diff --git a/src/roe/_generated/models/table_upload_request.py b/src/roe/_generated/models/table_upload_request.py new file mode 100644 index 0000000..7c715ee --- /dev/null +++ b/src/roe/_generated/models/table_upload_request.py @@ -0,0 +1,173 @@ +from __future__ import annotations + +from collections.abc import Mapping +from typing import Any, TypeVar, BinaryIO, TextIO, TYPE_CHECKING, Generator + +from attrs import define as _attrs_define +from attrs import field as _attrs_field +import json +from .. import types + +from ..types import UNSET, Unset + +from ..types import File, FileTypes +from ..types import UNSET, Unset +from io import BytesIO +from typing import cast +from uuid import UUID + + + + + + +T = TypeVar("T", bound="TableUploadRequest") + + + +@_attrs_define +class TableUploadRequest: + """ Serializer for public CSV table uploads. + + Attributes: + table_name (str): Name of the Roe table to create from the uploaded CSV + file (File): CSV file to upload + with_headers (bool | Unset): Whether the first row of the CSV contains column headers Default: True. + organization_id (None | Unset | UUID): Optional organization ID. Organization API keys are already scoped to one + organization; if supplied, this must match that organization. + """ + + table_name: str + file: File + with_headers: bool | Unset = True + organization_id: None | Unset | UUID = UNSET + additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict) + + + + + + def to_dict(self) -> dict[str, Any]: + table_name = self.table_name + + file = self.file.to_tuple() + + + with_headers = self.with_headers + + organization_id: None | str | Unset + if isinstance(self.organization_id, Unset): + organization_id = UNSET + elif isinstance(self.organization_id, UUID): + organization_id = str(self.organization_id) + else: + organization_id = self.organization_id + + + field_dict: dict[str, Any] = {} + field_dict.update(self.additional_properties) + field_dict.update({ + "table_name": table_name, + "file": file, + }) + if with_headers is not UNSET: + field_dict["with_headers"] = with_headers + if organization_id is not UNSET: + field_dict["organization_id"] = organization_id + + return field_dict + + + def to_multipart(self) -> types.RequestFiles: + files: types.RequestFiles = [] + + files.append(("table_name", (None, str(self.table_name).encode(), "text/plain"))) + + + + files.append(("file", self.file.to_tuple())) + + + + if not isinstance(self.with_headers, Unset): + files.append(("with_headers", (None, str(self.with_headers).encode(), "text/plain"))) + + + + if not isinstance(self.organization_id, Unset): + if isinstance(self.organization_id, UUID): + + files.append(("organization_id", (None, str(self.organization_id), "text/plain"))) + else: + files.append(("organization_id", (None, str(self.organization_id).encode(), "text/plain"))) + + + + for prop_name, prop in self.additional_properties.items(): + files.append((prop_name, (None, str(prop).encode(), "text/plain"))) + + + + return files + + + @classmethod + def from_dict(cls: type[T], src_dict: Mapping[str, Any]) -> T: + d = dict(src_dict) + table_name = d.pop("table_name") + + file = File( + payload = BytesIO(d.pop("file")) + ) + + + + + with_headers = d.pop("with_headers", UNSET) + + def _parse_organization_id(data: object) -> None | Unset | UUID: + if data is None: + return data + if isinstance(data, Unset): + return data + try: + if not isinstance(data, str): + raise TypeError() + organization_id_type_0 = UUID(data) + + + + return organization_id_type_0 + except (TypeError, ValueError, AttributeError, KeyError): + pass + return cast(None | Unset | UUID, data) + + organization_id = _parse_organization_id(d.pop("organization_id", UNSET)) + + + table_upload_request = cls( + table_name=table_name, + file=file, + with_headers=with_headers, + organization_id=organization_id, + ) + + + table_upload_request.additional_properties = d + return table_upload_request + + @property + def additional_keys(self) -> list[str]: + return list(self.additional_properties.keys()) + + def __getitem__(self, key: str) -> Any: + return self.additional_properties[key] + + def __setitem__(self, key: str, value: Any) -> None: + self.additional_properties[key] = value + + def __delitem__(self, key: str) -> None: + del self.additional_properties[key] + + def __contains__(self, key: str) -> bool: + return key in self.additional_properties diff --git a/src/roe/_generated/models/table_upload_response.py b/src/roe/_generated/models/table_upload_response.py new file mode 100644 index 0000000..ed098ec --- /dev/null +++ b/src/roe/_generated/models/table_upload_response.py @@ -0,0 +1,99 @@ +from __future__ import annotations + +from collections.abc import Mapping +from typing import Any, TypeVar, BinaryIO, TextIO, TYPE_CHECKING, Generator + +from attrs import define as _attrs_define +from attrs import field as _attrs_field + +from ..types import UNSET, Unset + +from ..types import UNSET, Unset +from uuid import UUID + + + + + + +T = TypeVar("T", bound="TableUploadResponse") + + + +@_attrs_define +class TableUploadResponse: + """ Response payload for a public CSV table upload. + + Attributes: + table_name (str): Created Roe table name + organization_id (UUID): Organization that owns the table + summary (Any | Unset): ClickHouse import summary for the uploaded file + """ + + table_name: str + organization_id: UUID + summary: Any | Unset = UNSET + additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict) + + + + + + def to_dict(self) -> dict[str, Any]: + table_name = self.table_name + + organization_id = str(self.organization_id) + + summary = self.summary + + + field_dict: dict[str, Any] = {} + field_dict.update(self.additional_properties) + field_dict.update({ + "table_name": table_name, + "organization_id": organization_id, + }) + if summary is not UNSET: + field_dict["summary"] = summary + + return field_dict + + + + @classmethod + def from_dict(cls: type[T], src_dict: Mapping[str, Any]) -> T: + d = dict(src_dict) + table_name = d.pop("table_name") + + organization_id = UUID(d.pop("organization_id")) + + + + + summary = d.pop("summary", UNSET) + + table_upload_response = cls( + table_name=table_name, + organization_id=organization_id, + summary=summary, + ) + + + table_upload_response.additional_properties = d + return table_upload_response + + @property + def additional_keys(self) -> list[str]: + return list(self.additional_properties.keys()) + + def __getitem__(self, key: str) -> Any: + return self.additional_properties[key] + + def __setitem__(self, key: str, value: Any) -> None: + self.additional_properties[key] = value + + def __delitem__(self, key: str) -> None: + del self.additional_properties[key] + + def __contains__(self, key: str) -> bool: + return key in self.additional_properties diff --git a/src/roe/api/__init__.py b/src/roe/api/__init__.py index 39f5d6e..d9cd545 100644 --- a/src/roe/api/__init__.py +++ b/src/roe/api/__init__.py @@ -1,6 +1,17 @@ """API modules for the Roe AI SDK.""" +from roe.api._generated_registry import GENERATED_API_CLASSES from roe.api.agents import AgentsAPI from roe.api.policies import PoliciesAPI +from roe.api.users import UsersAPI -__all__ = ["AgentsAPI", "PoliciesAPI"] +globals().update( + {api_class.__name__: api_class for api_class in GENERATED_API_CLASSES.values()} +) +_generated_api_names = [ + api_class.__name__ for api_class in GENERATED_API_CLASSES.values() +] + +__all__ = ["AgentsAPI", "PoliciesAPI", "UsersAPI", *_generated_api_names] + +del _generated_api_names diff --git a/src/roe/api/_generated_registry.py b/src/roe/api/_generated_registry.py new file mode 100644 index 0000000..587b472 --- /dev/null +++ b/src/roe/api/_generated_registry.py @@ -0,0 +1,15 @@ +"""Auto-generated friendly API registry for the Roe AI SDK.""" + +# Generated by scripts/generate-sdk from openapi/wrappers.yml. +# Do not edit by hand. + +from __future__ import annotations + +from roe.api.discovery import DiscoveryAPI +from roe.api.tables import TablesAPI + + +GENERATED_API_CLASSES = { + "discovery": DiscoveryAPI, + "tables": TablesAPI, +} diff --git a/src/roe/api/discovery.py b/src/roe/api/discovery.py new file mode 100644 index 0000000..4798037 --- /dev/null +++ b/src/roe/api/discovery.py @@ -0,0 +1,49 @@ +"""Auto-generated friendly API facades for the Roe AI SDK.""" + +# Generated by scripts/generate-sdk from openapi/wrappers.yml. +# Do not edit by hand. + +from __future__ import annotations + +from roe._generated.api.discovery import ( + discovery_agent_engine_types_list, + discovery_supported_models_list, +) +from roe._generated.client import AuthenticatedClient +from roe._generated.models.agent_engine_type_list import AgentEngineTypeList +from roe._generated.models.supported_llm_model_list import SupportedLLMModelList +from roe._generated.types import UNSET +from roe.config import RoeConfig +from roe.exceptions import RoeAPIException, translate_response + + +class DiscoveryAPI: + """API for discovering valid agent engine types and model IDs.""" + + def __init__(self, config: RoeConfig, raw_client: AuthenticatedClient): + self.config = config + self._raw = raw_client + + def list_agent_engine_types(self) -> AgentEngineTypeList: + """Return production engine_class_id values accepted by agent creation.""" + response = discovery_agent_engine_types_list.sync_detailed( + client=self._raw, + ) + translate_response(response) + if response.parsed is None: + raise RoeAPIException("agent engine discovery returned an empty response") + return response.parsed + + def list_supported_models( + self, + capability: str | None = None, + ) -> SupportedLLMModelList: + """Return non-deprecated model IDs accepted in engine_config.model.""" + response = discovery_supported_models_list.sync_detailed( + client=self._raw, + capability=capability if capability is not None else UNSET, + ) + translate_response(response) + if response.parsed is None: + raise RoeAPIException("model discovery returned an empty response") + return response.parsed diff --git a/src/roe/api/tables.py b/src/roe/api/tables.py new file mode 100644 index 0000000..c7730fd --- /dev/null +++ b/src/roe/api/tables.py @@ -0,0 +1,130 @@ +"""Auto-generated friendly API facades for the Roe AI SDK.""" + +# Generated by scripts/generate-sdk from openapi/wrappers.yml. +# Do not edit by hand. + +from __future__ import annotations + +from io import BytesIO +import mimetypes +from pathlib import Path +from typing import BinaryIO +from uuid import UUID + +from roe._generated.api.tables import upload_table +from roe._generated.client import AuthenticatedClient +from roe._generated.models.table_upload_request import TableUploadRequest +from roe._generated.models.table_upload_response import TableUploadResponse +from roe._generated.types import File, UNSET, Unset +from roe.config import RoeConfig +from roe.exceptions import RoeAPIException, translate_response +from roe.models import FileUpload + + +class TablesAPI: + """API for uploading CSV files into Roe tables.""" + + def __init__(self, config: RoeConfig, raw_client: AuthenticatedClient): + self.config = config + self._raw = raw_client + + def upload( + self, + *, + table_name: str, + file: str | Path | bytes | BinaryIO | FileUpload, + with_headers: bool = True, + organization_id: str | UUID | None = None, + filename: str | None = None, + mime_type: str | None = None, + ) -> TableUploadResponse: + """Upload a CSV file and create a Roe table. + + Args: + table_name: Name of the table to create. + file: CSV file path, bytes, binary file object, or ``FileUpload``. + with_headers: Whether the first CSV row contains column headers. + organization_id: Optional override; defaults to the client's configured org. + filename: Filename to use for bytes/file objects. + mime_type: MIME type override. Defaults to ``text/csv`` for ``.csv`` names. + """ + resolved_org: UUID | Unset + candidate = organization_id or self.config.organization_id + resolved_org = UUID(str(candidate)) if candidate else UNSET + + upload_file, close_after = self._as_generated_file(file, filename, mime_type) + try: + body = TableUploadRequest( + table_name=table_name, + file=upload_file, + with_headers=with_headers, + organization_id=resolved_org, + ) + response = upload_table.sync_detailed(client=self._raw, body=body) + translate_response(response) + if not isinstance(response.parsed, TableUploadResponse): + raise RoeAPIException("table upload returned an empty response") + return response.parsed + finally: + if close_after: + upload_file.payload.close() + + @staticmethod + def _as_generated_file( + file: str | Path | bytes | BinaryIO | FileUpload, + filename: str | None, + mime_type: str | None, + ) -> tuple[File, bool]: + if isinstance(file, FileUpload): + payload = file.open() + effective_filename = filename or file.effective_filename + effective_mime_type = mime_type or file.effective_mime_type + return ( + File( + payload=payload, + file_name=effective_filename, + mime_type=effective_mime_type, + ), + file.path is not None, + ) + + if isinstance(file, (str, Path)): + path = Path(file) + payload = path.open("rb") + effective_filename = filename or path.name + return ( + File( + payload=payload, + file_name=effective_filename, + mime_type=_mime_type(effective_filename, mime_type), + ), + True, + ) + + if isinstance(file, bytes): + effective_filename = filename or "upload.csv" + return ( + File( + payload=BytesIO(file), + file_name=effective_filename, + mime_type=_mime_type(effective_filename, mime_type), + ), + True, + ) + + effective_filename = filename or Path(getattr(file, "name", "upload.csv")).name + return ( + File( + payload=file, + file_name=effective_filename, + mime_type=_mime_type(effective_filename, mime_type), + ), + False, + ) + + +def _mime_type(filename: str, override: str | None) -> str: + if override: + return override + guessed, _ = mimetypes.guess_type(filename) + return guessed or "text/csv" diff --git a/src/roe/client.py b/src/roe/client.py index 66aaef5..7efa92e 100644 --- a/src/roe/client.py +++ b/src/roe/client.py @@ -1,8 +1,11 @@ """Main client for the Roe AI SDK.""" +from typing import Any + import httpx from roe._generated.client import AuthenticatedClient as RawClient +from roe.api._generated_registry import GENERATED_API_CLASSES from roe.api.agents import AgentsAPI from roe.api.policies import PoliciesAPI from roe.api.users import UsersAPI @@ -94,6 +97,10 @@ def __init__( self._agents = AgentsAPI(self.config, self._raw) self._policies = PoliciesAPI(self.config, self._raw) self._users = UsersAPI(self.config, self._raw) + self._generated_apis = { + name: api_cls(self.config, self._raw) + for name, api_cls in GENERATED_API_CLASSES.items() + } @property def agents(self) -> AgentsAPI: @@ -163,6 +170,15 @@ def raw(self) -> RawClient: """Access the generated raw client.""" return self._raw + def __getattr__(self, name: str) -> Any: + """Expose generated friendly API groups as ``client.``.""" + generated_apis = self.__dict__.get("_generated_apis", {}) + if name in generated_apis: + return generated_apis[name] + raise AttributeError( + f"{type(self).__name__!s} object has no attribute {name!r}" + ) + def close(self) -> None: """Close the HTTP client and clean up resources.""" self._httpx_client.close() diff --git a/tests/unit/test_discovery.py b/tests/unit/test_discovery.py new file mode 100644 index 0000000..9a6cbb3 --- /dev/null +++ b/tests/unit/test_discovery.py @@ -0,0 +1,168 @@ +"""Unit tests for the generated ``roe.api.discovery.DiscoveryAPI`` facade.""" + +from __future__ import annotations + +from http import HTTPStatus +from unittest.mock import MagicMock, patch + +import pytest + +from roe._generated.models.agent_engine_type_list import AgentEngineTypeList +from roe._generated.models.supported_llm_model_list import SupportedLLMModelList +from roe._generated.types import UNSET, Response +from roe.api.discovery import DiscoveryAPI +from roe.exceptions import BadRequestError + + +ORG_ID = "00000000-0000-0000-0000-000000000000" + + +def _response(parsed, status: int = 200) -> Response: + return Response( + status_code=HTTPStatus(status), + content=b"{}", + headers={}, + parsed=parsed, + ) + + +def test_list_agent_engine_types_calls_generated_endpoint(): + raw_client = MagicMock() + api = DiscoveryAPI(MagicMock(), raw_client) + payload = AgentEngineTypeList( + engine_types=["ResearchEngine"], total_count=1, engines=[] + ) + + with patch( + "roe.api.discovery.discovery_agent_engine_types_list.sync_detailed", + return_value=_response(payload), + ) as mocked: + result = api.list_agent_engine_types() + + mocked.assert_called_once_with(client=raw_client) + assert result.engine_types == ["ResearchEngine"] + + +def test_list_supported_models_passes_capability_filter(): + raw_client = MagicMock() + api = DiscoveryAPI(MagicMock(), raw_client) + payload = SupportedLLMModelList( + models=[], total_count=0, tenant_scope="all_tenants" + ) + + with patch( + "roe.api.discovery.discovery_supported_models_list.sync_detailed", + return_value=_response(payload), + ) as mocked: + result = api.list_supported_models(capability="image") + + mocked.assert_called_once_with(client=raw_client, capability="image") + assert result.tenant_scope == "all_tenants" + + +def test_list_supported_models_translates_none_capability_to_unset(): + raw_client = MagicMock() + api = DiscoveryAPI(MagicMock(), raw_client) + payload = SupportedLLMModelList( + models=[], total_count=0, tenant_scope="all_tenants" + ) + + with patch( + "roe.api.discovery.discovery_supported_models_list.sync_detailed", + return_value=_response(payload), + ) as mocked: + api.list_supported_models() + + mocked.assert_called_once_with(client=raw_client, capability=UNSET) + + +def test_list_supported_models_translates_bad_request(): + raw_client = MagicMock() + api = DiscoveryAPI(MagicMock(), raw_client) + + with patch( + "roe.api.discovery.discovery_supported_models_list.sync_detailed", + return_value=_response(None, status=400), + ): + with pytest.raises(BadRequestError): + api.list_supported_models(capability="spreadsheet") + + +def test_discovery_via_roe_client_generated_registry(): + payload = AgentEngineTypeList( + engine_types=["ResearchEngine"], total_count=1, engines=[] + ) + + with patch( + "roe.api.discovery.discovery_agent_engine_types_list.sync_detailed", + return_value=_response(payload), + ): + from roe import RoeClient + + client = RoeClient( + api_key="test-key", + organization_id=ORG_ID, + base_url="https://example.invalid", + ) + try: + result = client.discovery.list_agent_engine_types() + finally: + client.close() + + assert result.engine_types == ["ResearchEngine"] + + +def test_agent_engine_type_list_deserializes_public_engine_payload(): + backend_response = { + "engine_types": ["ResearchEngine"], + "total_count": 1, + "engines": [ + { + "class_id": "ResearchEngine", + "display_name": "Research Engine", + "description": "Researches things.", + "summary": "Research workflow.", + "input_schema": {"type": "object", "properties": {}}, + "default_values": {}, + } + ], + } + + parsed = AgentEngineTypeList.from_dict(backend_response) + + assert parsed.engine_types == ["ResearchEngine"] + assert parsed.total_count == 1 + assert len(parsed.engines) == 1 + engine = parsed.engines[0] + assert engine["class_id"] == "ResearchEngine" + assert engine["display_name"] == "Research Engine" + assert engine["input_schema"] == {"type": "object", "properties": {}} + assert engine["default_values"] == {} + + +def test_supported_llm_model_list_deserializes_public_model_payload(): + backend_response = { + "models": [ + { + "id": "gpt-5", + "providers": ["openai"], + "capabilities": ["text"], + "context_window": 200000, + "max_output_tokens": 8192, + "supports_system_message": True, + "supports_temperature": True, + "supports_reasoning_effort": False, + "supports_json_output": True, + "supports_json_schema": True, + } + ], + "total_count": 1, + "tenant_scope": "all_tenants", + } + + parsed = SupportedLLMModelList.from_dict(backend_response) + + assert parsed.tenant_scope == "all_tenants" + assert parsed.total_count == 1 + assert parsed.models[0].id == "gpt-5" + assert parsed.models[0].capabilities == ["text"] diff --git a/tests/unit/test_tables.py b/tests/unit/test_tables.py new file mode 100644 index 0000000..cb488a5 --- /dev/null +++ b/tests/unit/test_tables.py @@ -0,0 +1,113 @@ +"""Unit tests for the generated ``roe.api.tables.TablesAPI`` facade.""" + +from __future__ import annotations + +import json +from http import HTTPStatus +from unittest.mock import MagicMock, patch + +from roe._generated.models.table_upload_response import TableUploadResponse +from roe._generated.types import UNSET, Response +from roe.api.tables import TablesAPI + + +ORG_ID = "323e4567-e89b-12d3-a456-426614174002" + + +def _fake_response(status: int, payload: dict) -> Response: + body = json.dumps(payload).encode("utf-8") + return Response( + status_code=HTTPStatus(status), + content=body, + headers={}, + parsed=TableUploadResponse.from_dict(payload), + ) + + +def test_tables_upload_builds_multipart_request_from_bytes(): + raw_client = MagicMock() + config = MagicMock(organization_id=ORG_ID) + api = TablesAPI(config, raw_client) + + with patch( + "roe.api.tables.upload_table.sync_detailed", + return_value=_fake_response( + 201, + { + "table_name": "customers", + "organization_id": ORG_ID, + "summary": {"written_rows": 1}, + }, + ), + ) as mocked: + result = api.upload( + table_name="customers", + file=b"name\nAda\n", + with_headers=True, + ) + + mocked.assert_called_once() + body = mocked.call_args.kwargs["body"] + assert body.table_name == "customers" + assert body.with_headers is True + assert str(body.organization_id) == ORG_ID + assert body.file.file_name == "upload.csv" + assert body.file.mime_type == "text/csv" + assert result.table_name == "customers" + assert str(result.organization_id) == ORG_ID + + +def test_tables_upload_omits_organization_id_when_not_configured(): + raw_client = MagicMock() + config = MagicMock(organization_id=None) + api = TablesAPI(config, raw_client) + + with patch( + "roe.api.tables.upload_table.sync_detailed", + return_value=_fake_response( + 201, + { + "table_name": "customers", + "organization_id": ORG_ID, + "summary": {"written_rows": 1}, + }, + ), + ) as mocked: + api.upload( + table_name="customers", + file=b"name\nAda\n", + ) + + body = mocked.call_args.kwargs["body"] + assert body.organization_id is UNSET + + +def test_tables_upload_via_roe_client_generated_registry(): + with patch( + "roe.api.tables.upload_table.sync_detailed", + return_value=_fake_response( + 201, + { + "table_name": "events", + "organization_id": ORG_ID, + "summary": {"written_rows": 2}, + }, + ), + ): + from roe import RoeClient + + client = RoeClient( + api_key="test-key", + organization_id=ORG_ID, + base_url="https://example.invalid", + ) + try: + result = client.tables.upload( + table_name="events", + file=b"id\n1\n2\n", + with_headers=True, + ) + finally: + client.close() + + assert result.table_name == "events" diff --git a/uv.lock b/uv.lock index ae89d10..db53274 100644 --- a/uv.lock +++ b/uv.lock @@ -466,6 +466,7 @@ dependencies = [ [package.dev-dependencies] codegen = [ { name = "openapi-python-client" }, + { name = "ruamel-yaml" }, ] dev = [ { name = "pytest" }, @@ -481,7 +482,10 @@ requires-dist = [ ] [package.metadata.requires-dev] -codegen = [{ name = "openapi-python-client", specifier = ">=0.28,<0.29" }] +codegen = [ + { name = "openapi-python-client", specifier = ">=0.28,<0.29" }, + { name = "ruamel-yaml", specifier = ">=0.18.15" }, +] dev = [ { name = "pytest", specifier = ">=8.3.0" }, { name = "ruff", specifier = ">=0.12.10" }, From 66fbfd27cc03165f97fcb373a49ffcb07f6335d2 Mon Sep 17 00:00:00 2001 From: Jaden Fix Date: Thu, 21 May 2026 17:31:28 -0700 Subject: [PATCH 04/10] Mark Python wrappers as roe-main synced --- openapi/wrappers.yml | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/openapi/wrappers.yml b/openapi/wrappers.yml index f9dc9d6..a6c15a8 100644 --- a/openapi/wrappers.yml +++ b/openapi/wrappers.yml @@ -1,8 +1,10 @@ -# Friendly SDK facades generated after the raw OpenAPI client. +# Friendly Python SDK facades generated after the raw OpenAPI client. # # Add entries here when an OpenAPI operation should be exposed as -# `client..()`. `scripts/generate-sdk` reads this file and writes -# the corresponding `src/roe/api/*.py` modules plus the generated API registry. +# `client..()`. The release fan-out copies this file into +# `roe-python/openapi/wrappers.yml`; `roe-python/scripts/generate-sdk` then +# writes the corresponding `src/roe/api/*.py` modules plus the generated API +# registry. apis: discovery: class_name: DiscoveryAPI From c370b7c5314861c1add3c561c196ec0fbb9926b6 Mon Sep 17 00:00:00 2001 From: Jaden Fix Date: Thu, 21 May 2026 18:06:56 -0700 Subject: [PATCH 05/10] Sync Python README blocks from SDK contract --- README.md | 7 ++-- openapi/readme_blocks.yml | 46 +++++++++++++++++++++++++ openapi/wrappers.yml | 67 +++++++++++++++++------------------- pyproject.toml | 3 ++ scripts/generate_wrappers.py | 38 ++++++++++++++++++++ 5 files changed, 121 insertions(+), 40 deletions(-) create mode 100644 openapi/readme_blocks.yml diff --git a/README.md b/README.md index b927322..b26111d 100644 --- a/README.md +++ b/README.md @@ -130,12 +130,10 @@ print(response.status_code) For typed request/response models, call the generated operation module directly — see `roe/_generated/api/` for the current surface. + ## Generated Friendly APIs -Selected SDK-visible operations are exposed as ergonomic wrappers generated -from `openapi/wrappers.yml` during `scripts/generate-sdk`. This keeps the -raw OpenAPI client and friendly `RoeClient` surface in sync without writing -new wrapper classes by hand. +This block is synced from `roe-main/roe-sdk/sdk_contract.yml` during SDK fan-out. ```python engines = client.discovery.list_agent_engine_types() @@ -147,6 +145,7 @@ upload = client.tables.upload( with_headers=True, ) ``` + ## Agent Examples diff --git a/openapi/readme_blocks.yml b/openapi/readme_blocks.yml new file mode 100644 index 0000000..91a18c1 --- /dev/null +++ b/openapi/readme_blocks.yml @@ -0,0 +1,46 @@ +# README blocks synced into SDK target repos during release fan-out. +# Generated from sdk_contract.yml by `uv run python -m roe_sdk sync-contract`. + +blocks: + generated_friendly_apis: + python: | + ## Generated Friendly APIs + + This block is synced from `roe-main/roe-sdk/sdk_contract.yml` during SDK fan-out. + + ```python + engines = client.discovery.list_agent_engine_types() + models = client.discovery.list_supported_models(capability="text") + + upload = client.tables.upload( + table_name="customers", + file="customers.csv", + with_headers=True, + ) + ``` + typescript: | + ## Generated Friendly APIs + + This block is synced from `roe-main/roe-sdk/sdk_contract.yml` during SDK fan-out. + + ```typescript + const engines = await client.discovery.listAgentEngineTypes(); + const models = await client.discovery.listSupportedModels("text"); + + const upload = await client.tables.upload({ + tableName: "customers", + file: "customers.csv", + withHeaders: true, + }); + ``` + go: | + ## Generated Friendly APIs + + This block is synced from `roe-main/roe-sdk/sdk_contract.yml` during SDK fan-out. + + ```go + engines, err := client.Discovery.ListAgentEngineTypes() + models, err := client.Discovery.ListSupportedModels("text") + + upload, err := client.Tables.Upload("customers", roe.FileUpload{Path: "customers.csv"}, true) + ``` diff --git a/openapi/wrappers.yml b/openapi/wrappers.yml index a6c15a8..ec8c9d3 100644 --- a/openapi/wrappers.yml +++ b/openapi/wrappers.yml @@ -1,46 +1,41 @@ # Friendly Python SDK facades generated after the raw OpenAPI client. -# -# Add entries here when an OpenAPI operation should be exposed as -# `client..()`. The release fan-out copies this file into -# `roe-python/openapi/wrappers.yml`; `roe-python/scripts/generate-sdk` then -# writes the corresponding `src/roe/api/*.py` modules plus the generated API -# registry. +# Generated from sdk_contract.yml by `uv run python -m roe_sdk sync-contract`. +# The release fan-out copies this file into roe-python/openapi/wrappers.yml. + apis: discovery: class_name: DiscoveryAPI docstring: API for discovering valid agent engine types and model IDs. operations: - - kind: simple - method_name: list_agent_engine_types - docstring: Return production engine_class_id values accepted by agent creation. - endpoint_module: roe._generated.api.discovery.discovery_agent_engine_types_list - return_type: AgentEngineTypeList - return_import: roe._generated.models.agent_engine_type_list.AgentEngineTypeList - empty_response_message: agent engine discovery returned an empty response - - - kind: simple - method_name: list_supported_models - docstring: Return non-deprecated model IDs accepted in engine_config.model. - endpoint_module: roe._generated.api.discovery.discovery_supported_models_list - return_type: SupportedLLMModelList - return_import: roe._generated.models.supported_llm_model_list.SupportedLLMModelList - empty_response_message: model discovery returned an empty response - parameters: - - name: capability - annotation: str | None - default: null - pass_unset_when_none: true - + - kind: simple + method_name: list_agent_engine_types + docstring: Return production engine_class_id values accepted by agent creation. + endpoint_module: roe._generated.api.discovery.discovery_agent_engine_types_list + return_type: AgentEngineTypeList + return_import: roe._generated.models.agent_engine_type_list.AgentEngineTypeList + empty_response_message: agent engine discovery returned an empty response + - kind: simple + method_name: list_supported_models + docstring: Return non-deprecated model IDs accepted in engine_config.model. + endpoint_module: roe._generated.api.discovery.discovery_supported_models_list + return_type: SupportedLLMModelList + return_import: roe._generated.models.supported_llm_model_list.SupportedLLMModelList + empty_response_message: model discovery returned an empty response + parameters: + - name: capability + annotation: str | None + default: null + pass_unset_when_none: true tables: class_name: TablesAPI docstring: API for uploading CSV files into Roe tables. operations: - - kind: table_upload - method_name: upload - docstring: Upload a CSV file and create a Roe table. - endpoint_module: roe._generated.api.tables.upload_table - body_type: TableUploadRequest - body_import: roe._generated.models.table_upload_request.TableUploadRequest - return_type: TableUploadResponse - return_import: roe._generated.models.table_upload_response.TableUploadResponse - empty_response_message: table upload returned an empty response + - kind: table_upload + method_name: upload + docstring: Upload a CSV file and create a Roe table. + endpoint_module: roe._generated.api.tables.upload_table + return_type: TableUploadResponse + return_import: roe._generated.models.table_upload_response.TableUploadResponse + empty_response_message: table upload returned an empty response + body_type: TableUploadRequest + body_import: roe._generated.models.table_upload_request.TableUploadRequest diff --git a/pyproject.toml b/pyproject.toml index 6703460..763e1b7 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -38,3 +38,6 @@ codegen = [ [tool.pytest.ini_options] testpaths = ["tests"] + +[tool.ruff] +extend-exclude = ["src/roe/_generated"] diff --git a/scripts/generate_wrappers.py b/scripts/generate_wrappers.py index 3a9fe2c..42f2986 100644 --- a/scripts/generate_wrappers.py +++ b/scripts/generate_wrappers.py @@ -2,6 +2,7 @@ from collections import defaultdict from pathlib import Path +import re from typing import Any from ruamel.yaml import YAML @@ -9,8 +10,12 @@ ROOT_DIR = Path(__file__).resolve().parents[1] CONTRACT_PATH = ROOT_DIR / "openapi" / "wrappers.yml" +README_BLOCKS_PATH = ROOT_DIR / "openapi" / "readme_blocks.yml" +README_PATH = ROOT_DIR / "README.md" API_DIR = ROOT_DIR / "src" / "roe" / "api" REGISTRY_PATH = API_DIR / "_generated_registry.py" +README_BLOCK_START = "" +README_BLOCK_END = "" HEADER = ( '"""Auto-generated friendly API facades for the Roe AI SDK."""\n' @@ -31,6 +36,38 @@ def _load_contract() -> dict[str, Any]: return data +def _load_readme_block() -> str: + yaml = YAML(typ="safe") + data = yaml.load(README_BLOCKS_PATH.read_text(encoding="utf-8")) + try: + block = data["blocks"]["generated_friendly_apis"]["python"] + except (KeyError, TypeError) as exc: + raise ValueError( + f"{README_BLOCKS_PATH} must contain blocks.generated_friendly_apis.python" + ) from exc + if not isinstance(block, str) or not block.strip(): + raise ValueError( + f"{README_BLOCKS_PATH} blocks.generated_friendly_apis.python must be a non-empty string" + ) + return block.strip() + + +def _sync_readme_block() -> None: + block = _load_readme_block() + readme = README_PATH.read_text(encoding="utf-8") + replacement = f"{README_BLOCK_START}\n{block}\n{README_BLOCK_END}" + pattern = re.compile( + rf"{re.escape(README_BLOCK_START)}.*?{re.escape(README_BLOCK_END)}", + re.DOTALL, + ) + updated, count = pattern.subn(replacement, readme, count=1) + if count != 1: + raise ValueError( + f"{README_PATH} must contain {README_BLOCK_START} and {README_BLOCK_END}" + ) + README_PATH.write_text(updated, encoding="utf-8") + + def _module_import_parts(module_path: str) -> tuple[str, str]: package, module_name = module_path.rsplit(".", 1) return package, module_name @@ -321,6 +358,7 @@ def main() -> None: target.write_text(_render_api_module(api_name, spec)) REGISTRY_PATH.write_text(_render_registry(apis)) + _sync_readme_block() print( f"Generated {len(apis)} friendly API wrapper modules from " f"{CONTRACT_PATH.relative_to(ROOT_DIR)}" From 0f1ba85721a84affad5c6598cc23ecdd0aaff24f Mon Sep 17 00:00:00 2001 From: Jaden Fix Date: Thu, 21 May 2026 18:21:10 -0700 Subject: [PATCH 06/10] Document SDK patch version policy --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index b26111d..66c69ed 100644 --- a/README.md +++ b/README.md @@ -7,6 +7,8 @@ A Python SDK for the [Roe AI](https://www.roe-ai.com/) API. > share a single patch counter, driven by the SDK OpenAPI spec. Python > friendly wrappers are generated from `openapi/wrappers.yml`; current > generated facades include `client.discovery` and `client.tables`. +> The shared SDK release logic bumps patch versions only: `2.0.9` becomes +> `2.0.10`, not `2.1.0`, unless the policy is intentionally changed. > **v1.0.0** — The SDK delegates to OpenAPI-generated types and transports > (`roe._generated`); ergonomic wrappers on `client.agents` and From d13c261b44dc19a4b296f64f0cd28d640e4e8348 Mon Sep 17 00:00:00 2001 From: Jaden Fix Date: Thu, 21 May 2026 23:57:27 -0700 Subject: [PATCH 07/10] Harden Python SDK CI drift checks --- .github/workflows/ci.yml | 31 ++++++++++++++++++++++++++++++- 1 file changed, 30 insertions(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index a364f47..35f3579 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -8,6 +8,28 @@ on: workflow_dispatch: jobs: + test: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Set up uv and Python + uses: astral-sh/setup-uv@v6 + with: + python-version: "3.11.9" + enable-cache: true + cache-dependency-glob: uv.lock + activate-environment: true + + - name: Install dependencies + run: uv sync --locked --group dev + + - name: Lint + run: uv run ruff check . + + - name: Test + run: uv run pytest + check-codegen-drift: runs-on: ubuntu-latest steps: @@ -28,4 +50,11 @@ jobs: run: bash scripts/generate-sdk - name: Check for codegen drift - run: git diff --exit-code -- openapi/openapi.yml src/roe/_generated + run: | + git diff --exit-code -- \ + README.md \ + openapi/openapi.yml \ + openapi/wrappers.yml \ + openapi/readme_blocks.yml \ + src/roe/_generated \ + src/roe/api From 3c31ebf7cb9e18890ff59cc6adb72c18ea2b85fd Mon Sep 17 00:00:00 2001 From: Jaden Fix Date: Fri, 22 May 2026 16:16:42 -0700 Subject: [PATCH 08/10] fix: retarget SDK sync release to 1.0.82 --- CHANGELOG.md | 8 +- README.md | 10 +- pyproject.toml | 2 +- src/roe/api/agents.py | 127 +++++++++++--------- src/roe/api/policies.py | 32 ++--- src/roe/utils/_dynamic_call.py | 11 +- tests/unit/test_agents_wrapper_transport.py | 56 +++++++++ uv.lock | 2 +- 8 files changed, 163 insertions(+), 85 deletions(-) create mode 100644 tests/unit/test_agents_wrapper_transport.py diff --git a/CHANGELOG.md b/CHANGELOG.md index dcf3578..3325a1c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,9 +1,9 @@ # Changelog -## 2.0.0 +## 1.0.82 Version synchronization across `roe-ai` (Python), `roe-typescript`, -and `roe-golang`. The public SDK packages now share a single patch counter, +and `roe-golang`. The public SDK packages now share a single 1.0.x patch counter, driven by the SDK OpenAPI spec via the roe-main release pipeline (see `roe-main/roe-sdk/targets.yml`). @@ -17,6 +17,10 @@ SDK release target. - `client.discovery.list_agent_engine_types()`. - `client.discovery.list_supported_models(capability=...)`. - `client.tables.upload(...)`. +- SDK wrappers used by `roe-mcp` now route common 404 cases through + `RoeAPIException` instead of generated error-body parsers. +- `client.agents.run(..., idempotency_key=...)` and + `client.agents.run_version(..., idempotency_key=...)`. ## 1.0.0 diff --git a/README.md b/README.md index 66c69ed..756202b 100644 --- a/README.md +++ b/README.md @@ -2,13 +2,11 @@ A Python SDK for the [Roe AI](https://www.roe-ai.com/) API. -> **v2.0.0** — Version synchronization across the public SDKs: roe-ai +> **v1.0.82** — Version synchronization across the public SDKs: roe-ai > (Python), roe-typescript, and roe-golang. The public SDK packages now -> share a single patch counter, driven by the SDK OpenAPI spec. Python -> friendly wrappers are generated from `openapi/wrappers.yml`; current -> generated facades include `client.discovery` and `client.tables`. -> The shared SDK release logic bumps patch versions only: `2.0.9` becomes -> `2.0.10`, not `2.1.0`, unless the policy is intentionally changed. +> share a single 1.0.x patch counter, driven by the SDK OpenAPI spec. +> Python friendly wrappers are generated from `openapi/wrappers.yml`; +> current generated facades include `client.discovery` and `client.tables`. > **v1.0.0** — The SDK delegates to OpenAPI-generated types and transports > (`roe._generated`); ergonomic wrappers on `client.agents` and diff --git a/pyproject.toml b/pyproject.toml index 763e1b7..7c262af 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "roe-ai" -version = "2.0.0" +version = "1.0.82" authors = [ { name = "Roe AI", email = "founders@roe-ai.com" }, ] diff --git a/src/roe/api/agents.py b/src/roe/api/agents.py index 04ebdd9..122803c 100644 --- a/src/roe/api/agents.py +++ b/src/roe/api/agents.py @@ -111,37 +111,42 @@ def _org_id(self) -> UUID: return UUID(str(self._agents_api.config.organization_id)) def list(self, agent_id: str) -> list[AgentVersion]: - resp = agents_versions_list.sync_detailed( - agent_id=UUID(str(agent_id)), - client=self._raw, + response = request_raw( + self._raw, + agents_versions_list, + UUID(str(agent_id)), organization_id=self._org_id, ) - translate_response(resp) - return resp.parsed # type: ignore[return-value] + data = response.json() + if not isinstance(data, list): + raise RoeAPIException( + f"agent versions returned unexpected response shape: {data!r}" + ) + return [AgentVersion.from_dict(item) for item in data] def retrieve( self, agent_id: str, version_id: str, get_supports_eval: bool | None = None ) -> AgentVersion: - resp = agents_versions_retrieve.sync_detailed( - agent_id=UUID(str(agent_id)), - agent_version_id=UUID(str(version_id)), - client=self._raw, + response = request_raw( + self._raw, + agents_versions_retrieve, + UUID(str(agent_id)), + UUID(str(version_id)), get_supports_eval=get_supports_eval if get_supports_eval is not None else UNSET, organization_id=self._org_id, ) - translate_response(resp) - return resp.parsed # type: ignore[return-value] + return AgentVersion.from_dict(response.json()) def retrieve_current(self, agent_id: str) -> AgentVersion: - resp = agents_versions_current_retrieve.sync_detailed( - agent_id=UUID(str(agent_id)), - client=self._raw, + response = request_raw( + self._raw, + agents_versions_current_retrieve, + UUID(str(agent_id)), organization_id=self._org_id, ) - translate_response(resp) - return resp.parsed # type: ignore[return-value] + return AgentVersion.from_dict(response.json()) def create( self, @@ -195,13 +200,13 @@ def update( ) def delete(self, agent_id: str, version_id: str) -> None: - resp = agents_versions_destroy.sync_detailed( - agent_id=UUID(str(agent_id)), - agent_version_id=UUID(str(version_id)), - client=self._raw, + request_raw( + self._raw, + agents_versions_destroy, + UUID(str(agent_id)), + UUID(str(version_id)), organization_id=self._org_id, ) - translate_response(resp) class AgentJobsAPI: @@ -226,22 +231,22 @@ def _iter_chunks(items, chunk_size: int): yield items[i : i + chunk_size] def retrieve_status(self, job_id: str) -> AgentJobStatus: - resp = agents_jobs_status_retrieve.sync_detailed( - job_id=UUID(str(job_id)), - client=self._raw, + response = request_raw( + self._raw, + agents_jobs_status_retrieve, + UUID(str(job_id)), organization_id=self._org_id, ) - translate_response(resp) - return resp.parsed # type: ignore[return-value] + return AgentJobStatus.from_dict(response.json()) def retrieve_result(self, job_id: str) -> AgentJobResultResponse: - resp = agents_jobs_result_retrieve.sync_detailed( - agent_job_id=UUID(str(job_id)), - client=self._raw, + response = request_raw( + self._raw, + agents_jobs_result_retrieve, + UUID(str(job_id)), organization_id=self._org_id, ) - translate_response(resp) - return resp.parsed # type: ignore[return-value] + return AgentJobResultResponse.from_dict(response.json()) def retrieve_status_many(self, job_ids: list[str]) -> list[AgentJobStatus]: results: list[AgentJobStatus] = [] @@ -310,29 +315,29 @@ def download_reference( return response.content def cancel(self, job_id: str) -> None: - resp = agents_jobs_cancel_create.sync_detailed( - job_id=UUID(str(job_id)), - client=self._raw, + request_raw( + self._raw, + agents_jobs_cancel_create, + UUID(str(job_id)), organization_id=self._org_id, ) - translate_response(resp) def cancel_all(self, agent_id: str) -> None: - resp = agents_jobs_cancel_all_create.sync_detailed( - agent_id=UUID(str(agent_id)), - client=self._raw, + request_raw( + self._raw, + agents_jobs_cancel_all_create, + UUID(str(agent_id)), organization_id=self._org_id, ) - translate_response(resp) def delete_data(self, job_id: str) -> AgentJobDeleteDataResponse: - resp = agents_jobs_delete_data_create.sync_detailed( - job_id=UUID(str(job_id)), - client=self._raw, + response = request_raw( + self._raw, + agents_jobs_delete_data_create, + UUID(str(job_id)), organization_id=self._org_id, ) - translate_response(resp) - return resp.parsed # type: ignore[return-value] + return AgentJobDeleteDataResponse.from_dict(response.json()) class AgentsAPI: @@ -368,14 +373,14 @@ def list( page: int | None = None, page_size: int | None = None, ) -> PaginatedBaseAgentList: - resp = agents_list.sync_detailed( - client=self._raw, + response = request_raw( + self._raw, + agents_list, page=page if page is not None else UNSET, page_size=page_size if page_size is not None else UNSET, organization_id=self._org_id, ) - translate_response(resp) - return resp.parsed # type: ignore[return-value] + return PaginatedBaseAgentList.from_dict(response.json()) def retrieve(self, agent_id: str) -> BaseAgent: response = request_raw( @@ -437,12 +442,12 @@ def update( return resp.parsed # type: ignore[return-value] def delete(self, agent_id: str) -> None: - resp = agents_destroy.sync_detailed( - agent_id=UUID(str(agent_id)), - client=self._raw, + request_raw( + self._raw, + agents_destroy, + UUID(str(agent_id)), organization_id=self._org_id, ) - translate_response(resp) def duplicate(self, agent_id: str) -> AgentVersion: """Duplicate an agent. Returns the resulting ``AgentVersion``. @@ -452,19 +457,20 @@ def duplicate(self, agent_id: str) -> AgentVersion: response as ``AgentVersion`` directly. Callers wanting the new base agent should read ``result.base_agent`` (already populated). """ - resp = agents_duplicate_create.sync_detailed( - agent_id=UUID(str(agent_id)), - client=self._raw, + response = request_raw( + self._raw, + agents_duplicate_create, + UUID(str(agent_id)), organization_id=self._org_id, ) - translate_response(resp) - return resp.parsed # type: ignore[return-value] + return AgentVersion.from_dict(response.json()) def run( self, agent_id: str, timeout_seconds: int | None = None, metadata: dict[str, Any] | None = None, + idempotency_key: str | None = None, **inputs: Any, ) -> Job: """Run an agent asynchronously and return a ``Job`` handle.""" @@ -475,6 +481,9 @@ def run( metadata=metadata, organization_id=self._org_id, agent_id=UUID(str(agent_id)), + extra_headers=( + {"Idempotency-Key": idempotency_key} if idempotency_key else None + ), ) job_id = response.json() if not isinstance(job_id, str): @@ -547,6 +556,7 @@ def run_version( version_id: str, timeout_seconds: int | None = None, metadata: dict[str, Any] | None = None, + idempotency_key: str | None = None, **inputs: Any, ) -> Job: response = call_dynamic( @@ -557,6 +567,9 @@ def run_version( organization_id=self._org_id, agent_id=UUID(str(agent_id)), agent_version_id=UUID(str(version_id)), + extra_headers=( + {"Idempotency-Key": idempotency_key} if idempotency_key else None + ), ) job_id = response.json() if not isinstance(job_id, str): diff --git a/src/roe/api/policies.py b/src/roe/api/policies.py index 336e69b..94f50fc 100644 --- a/src/roe/api/policies.py +++ b/src/roe/api/policies.py @@ -37,7 +37,6 @@ from roe._generated.models.policy_version import PolicyVersion from roe._generated.types import UNSET from roe.config import RoeConfig -from roe.exceptions import translate_response from roe.utils.generated_request import request_json, request_raw @@ -54,6 +53,7 @@ def _normalize_policy_version_wire(data: dict[str, Any]) -> dict[str, Any]: def _parse_policy_version(data: dict[str, Any]) -> PolicyVersion: return PolicyVersion.from_dict(_normalize_policy_version_wire(data)) + if TYPE_CHECKING: pass @@ -121,7 +121,9 @@ def create( ) created = resp.parsed if created is None or created.id is None: - raise ValueError(f"Unexpected response from server: status={resp.status_code}") + raise ValueError( + f"Unexpected response from server: status={resp.status_code}" + ) # POST returns partial data; re-fetch to get the full version. return self.retrieve(policy_id, str(created.id)) @@ -145,24 +147,24 @@ def list( page_size: int | None = None, ) -> PaginatedPolicyList: """List policies in the organization.""" - resp = policies_list.sync_detailed( - client=self._raw, + response = request_raw( + self._raw, + policies_list, page=page if page is not None else UNSET, page_size=page_size if page_size is not None else UNSET, organization_id=UUID(str(self.config.organization_id)), ) - translate_response(resp) - return resp.parsed # type: ignore[return-value] + return PaginatedPolicyList.from_dict(response.json()) def retrieve(self, policy_id: str) -> Policy: """Retrieve a specific policy by ID.""" - resp = policies_retrieve.sync_detailed( - id=UUID(str(policy_id)), - client=self._raw, + response = request_raw( + self._raw, + policies_retrieve, + UUID(str(policy_id)), organization_id=UUID(str(self.config.organization_id)), ) - translate_response(resp) - return resp.parsed # type: ignore[return-value] + return Policy.from_dict(response.json()) def create( self, @@ -208,9 +210,9 @@ def update( def delete(self, policy_id: str) -> None: """Delete a policy and all its versions.""" - resp = policies_destroy.sync_detailed( - id=UUID(str(policy_id)), - client=self._raw, + request_raw( + self._raw, + policies_destroy, + UUID(str(policy_id)), organization_id=UUID(str(self.config.organization_id)), ) - translate_response(resp) diff --git a/src/roe/utils/_dynamic_call.py b/src/roe/utils/_dynamic_call.py index 1795450..5c54050 100644 --- a/src/roe/utils/_dynamic_call.py +++ b/src/roe/utils/_dynamic_call.py @@ -27,6 +27,7 @@ def call_dynamic( inputs: dict[str, Any], metadata: dict[str, Any] | None, organization_id: UUID, + extra_headers: dict[str, str] | None = None, **path_params: Any, ) -> httpx.Response: """Send a multipart request through the generated endpoint's URL/auth machinery. @@ -43,9 +44,13 @@ def call_dynamic( kwargs.pop("files", None) kwargs["data"] = data kwargs["files"] = files - headers = kwargs.setdefault("headers", {}) - headers["x-roe-skip-retry"] = "1" # multipart POST — do not replay (aligns with TS) - headers.pop("Content-Type", None) # let httpx pick the boundary + request_headers = kwargs.setdefault("headers", {}) + request_headers["x-roe-skip-retry"] = ( + "1" # multipart POST — do not replay (aligns with TS) + ) + if extra_headers is not None: + request_headers.update(extra_headers) + request_headers.pop("Content-Type", None) # let httpx pick the boundary response = raw.get_httpx_client().request(**kwargs) translate_response(response) return response diff --git a/tests/unit/test_agents_wrapper_transport.py b/tests/unit/test_agents_wrapper_transport.py new file mode 100644 index 0000000..bba67c1 --- /dev/null +++ b/tests/unit/test_agents_wrapper_transport.py @@ -0,0 +1,56 @@ +"""Regression tests for SDK wrappers that MCP depends on directly.""" + +from __future__ import annotations + +from types import SimpleNamespace +from unittest.mock import MagicMock + +import httpx +import pytest + +from roe.api.agents import AgentsAPI +from roe.exceptions import NotFoundError + +ORG_ID = "00000000-0000-0000-0000-000000000123" +AGENT_ID = "00000000-0000-0000-0000-000000000111" +VERSION_ID = "00000000-0000-0000-0000-000000000222" +JOB_ID = "00000000-0000-0000-0000-000000000333" + + +def _api(response: httpx.Response) -> tuple[AgentsAPI, MagicMock]: + request = MagicMock(return_value=response) + raw_client = MagicMock() + raw_client.get_httpx_client.return_value = SimpleNamespace(request=request) + config = SimpleNamespace(organization_id=ORG_ID, batch_chunk_delay=0) + return AgentsAPI(config, raw_client), request + + +def test_agent_version_retrieve_404_uses_sdk_error_not_generated_error_parser(): + api, _ = _api( + httpx.Response(404, json={"detail": "No Agent matches the given query."}) + ) + + with pytest.raises(NotFoundError) as exc_info: + api.versions.retrieve(AGENT_ID, VERSION_ID) + + assert exc_info.value.message == "No Agent matches the given query." + + +def test_job_cancel_404_uses_sdk_error_not_generated_error_parser(): + api, _ = _api(httpx.Response(404, json={"detail": "No AgentJob found."})) + + with pytest.raises(NotFoundError) as exc_info: + api.jobs.cancel(JOB_ID) + + assert exc_info.value.message == "No AgentJob found." + + +def test_run_passes_idempotency_key_through_dynamic_wrapper(): + api, request = _api(httpx.Response(200, json=JOB_ID)) + + job = api.run(AGENT_ID, idempotency_key="idem-123", prompt="hello") + + kwargs = request.call_args.kwargs + assert kwargs["headers"]["Idempotency-Key"] == "idem-123" + assert kwargs["headers"]["x-roe-skip-retry"] == "1" + assert job.id == JOB_ID diff --git a/uv.lock b/uv.lock index db53274..34f8839 100644 --- a/uv.lock +++ b/uv.lock @@ -454,7 +454,7 @@ wheels = [ [[package]] name = "roe-ai" -version = "2.0.0" +version = "1.0.82" source = { editable = "." } dependencies = [ { name = "attrs" }, From f7a66b424bed83674a97b1a52cdf785f11ae3bdc Mon Sep 17 00:00:00 2001 From: Jaden Fix Date: Tue, 26 May 2026 13:54:49 -0700 Subject: [PATCH 09/10] docs: keep sdk sync wording public --- CHANGELOG.md | 5 +---- tests/unit/test_agents_wrapper_transport.py | 2 +- 2 files changed, 2 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3325a1c..a844b3a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,9 +7,6 @@ and `roe-golang`. The public SDK packages now share a single 1.0.x patch counter driven by the SDK OpenAPI spec via the roe-main release pipeline (see `roe-main/roe-sdk/targets.yml`). -`roe-mcp` is a private consumer of the published Python SDK, not a public -SDK release target. - ### Added - Generated friendly wrapper support via `openapi/wrappers.yml` and @@ -17,7 +14,7 @@ SDK release target. - `client.discovery.list_agent_engine_types()`. - `client.discovery.list_supported_models(capability=...)`. - `client.tables.upload(...)`. -- SDK wrappers used by `roe-mcp` now route common 404 cases through +- Public SDK wrappers now route common 404 cases through `RoeAPIException` instead of generated error-body parsers. - `client.agents.run(..., idempotency_key=...)` and `client.agents.run_version(..., idempotency_key=...)`. diff --git a/tests/unit/test_agents_wrapper_transport.py b/tests/unit/test_agents_wrapper_transport.py index bba67c1..1034c5e 100644 --- a/tests/unit/test_agents_wrapper_transport.py +++ b/tests/unit/test_agents_wrapper_transport.py @@ -1,4 +1,4 @@ -"""Regression tests for SDK wrappers that MCP depends on directly.""" +"""Regression tests for public SDK wrapper transport behavior.""" from __future__ import annotations From 7454042e73c5303173ae84d566afdea16ca311b9 Mon Sep 17 00:00:00 2001 From: Jaden Fix Date: Tue, 26 May 2026 14:42:38 -0700 Subject: [PATCH 10/10] chore: retarget sdk sync to 1.0.802 --- CHANGELOG.md | 2 +- README.md | 2 +- pyproject.toml | 2 +- uv.lock | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a844b3a..e5bb81a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,6 @@ # Changelog -## 1.0.82 +## 1.0.802 Version synchronization across `roe-ai` (Python), `roe-typescript`, and `roe-golang`. The public SDK packages now share a single 1.0.x patch counter, diff --git a/README.md b/README.md index 756202b..9d6d2e6 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ A Python SDK for the [Roe AI](https://www.roe-ai.com/) API. -> **v1.0.82** — Version synchronization across the public SDKs: roe-ai +> **v1.0.802** — Version synchronization across the public SDKs: roe-ai > (Python), roe-typescript, and roe-golang. The public SDK packages now > share a single 1.0.x patch counter, driven by the SDK OpenAPI spec. > Python friendly wrappers are generated from `openapi/wrappers.yml`; diff --git a/pyproject.toml b/pyproject.toml index 7c262af..afb6dde 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "roe-ai" -version = "1.0.82" +version = "1.0.802" authors = [ { name = "Roe AI", email = "founders@roe-ai.com" }, ] diff --git a/uv.lock b/uv.lock index 34f8839..a5cbcb3 100644 --- a/uv.lock +++ b/uv.lock @@ -454,7 +454,7 @@ wheels = [ [[package]] name = "roe-ai" -version = "1.0.82" +version = "1.0.802" source = { editable = "." } dependencies = [ { name = "attrs" },