diff --git a/.roe-main-release-version b/.roe-main-release-version index a9038d5..f749893 100644 --- a/.roe-main-release-version +++ b/.roe-main-release-version @@ -1 +1 @@ -1-0-83 +1-1-1 diff --git a/README.md b/README.md index 7f11788..1e05a1c 100644 --- a/README.md +++ b/README.md @@ -3,12 +3,9 @@ A Python SDK for the [Roe](https://www.roe-ai.com/) API. -> **v1.1.0** - Schema synchronization across the public SDKs: roe-ai -> (Python), roe-typescript, and roe-golang. This release is generated from -> SDK OpenAPI marker `1-0-83`, and all public package metadata is bumped to -> 1.1.0. -> Python friendly wrappers are generated from `openapi/wrappers.yml`; -> current generated facades include `client.discovery` and `client.tables`. +> **v1.1.1** - SDK operation coverage is synchronized across Python, +> TypeScript, and Go. See `SDK_EXAMPLES.md` for copy-ready examples and +> use cases. > **v1.0.0** — The SDK delegates to OpenAPI-generated types and transports @@ -134,9 +131,9 @@ For typed request/response models, call the generated operation module directly — see `roe/_generated/api/` for the current surface. -## Generated Friendly APIs +## SDK Operation Groups -This block is synced from `roe-main/roe-sdk/sdk_contract.yml` during SDK fan-out. +Common operations are available directly on the SDK client. ```python engines = client.discovery.list_agent_engine_types() diff --git a/SDK_EXAMPLES.md b/SDK_EXAMPLES.md new file mode 100644 index 0000000..02799d8 --- /dev/null +++ b/SDK_EXAMPLES.md @@ -0,0 +1,1139 @@ +# Python SDK Examples + + + +## Examples + +Copy-ready calls for every SDK operation. Required and optional inputs are shown inline in each code block. + +### Agents + +#### `agents_list` + +List agents or create a new agent. + +```python +from roe import RoeClient + +client = RoeClient() + +result = client.agents.list( + page=1, # optional + page_size=1, # optional +) +``` + +#### `agents_create` + +Create a new base agent. + +```python +from roe import RoeClient + +client = RoeClient() + +result = client.agents.create( + name="name", # required + engine_class_id="engine_class_id", # required + input_definitions=[{"key": "text", "data_type": "text/plain"}], # optional + engine_config={"model": "gpt-4.1"}, # optional + version_name="version_name", # optional + description="description", # optional +) +``` + +#### `agents_jobs_results_create` + +Get results for multiple agent jobs + +```python +from roe import RoeClient + +client = RoeClient() + +result = client.agents.jobs.retrieve_result_many( + job_ids=["job_id"], +) +``` + +#### `agents_jobs_statuses_create` + +Get status for multiple agent jobs + +```python +from roe import RoeClient + +client = RoeClient() + +result = client.agents.jobs.retrieve_status_many( + job_ids=["job_id"], +) +``` + +#### `agents_jobs_references_retrieve` + +Serve a reference file associated with an agent job. + +```python +from roe import RoeClient + +client = RoeClient() + +content = client.agents.jobs.download_reference( + job_id="job_id", + resource_id="resource_id", + as_attachment=False, +) +``` + +#### `agents_jobs_result_retrieve` + +Get agent job result data. + +```python +from roe import RoeClient + +client = RoeClient() + +result = client.agents.jobs.retrieve_result( + job_id="job_id", # required +) +``` + +#### `agents_jobs_cancel_create` + +Cancel an agent job + +```python +from roe import RoeClient + +client = RoeClient() + +result = client.agents.jobs.cancel( + job_id="job_id", # required +) +``` + +#### `agents_jobs_delete_data_create` + +Delete agent job data + +```python +from roe import RoeClient + +client = RoeClient() + +result = client.agents.jobs.delete_data( + job_id="job_id", # required +) +``` + +#### `agents_jobs_status_retrieve` + +Get agent job status. + +```python +from roe import RoeClient + +client = RoeClient() + +result = client.agents.jobs.retrieve_status( + job_id="job_id", # required +) +``` + +#### `agents_run` + +Run agent synchronously + +```python +from roe import RoeClient + +client = RoeClient() + +result = client.agents.run_sync( + agent_id="agent_id", + text="text", + metadata={}, +) +``` + +#### `agents_run_async_create` + +Run agent asynchronously. + +```python +from roe import RoeClient + +client = RoeClient() + +job = client.agents.run( + agent_id="agent_id", + timeout_seconds=300, + text="text", + metadata={}, +) +``` + +#### `agents_run_async_many` + +Run agent asynchronously with multiple inputs + +```python +from roe import RoeClient + +client = RoeClient() + +batch = client.agents.run_many( + agent_id="agent_id", + batch_inputs=[{"text": "text"}], + timeout_seconds=300, + metadata={}, +) +``` + +#### `agents_run_version` + +Run agent version synchronously + +```python +from roe import RoeClient + +client = RoeClient() + +result = client.agents.run_version_sync( + agent_id="agent_id", + version_id="version_id", + text="text", + metadata={}, +) +``` + +#### `agents_run_versions_async_create` + +Run agent version asynchronously. + +```python +from roe import RoeClient + +client = RoeClient() + +job = client.agents.run_version( + agent_id="agent_id", + version_id="version_id", + timeout_seconds=300, + text="text", + metadata={}, +) +``` + +#### `agents_destroy` + +Delete a base agent. + +```python +from roe import RoeClient + +client = RoeClient() + +result = client.agents.delete( + agent_id="agent_id", # required +) +``` + +#### `agents_retrieve` + +Retrieve an agent. + +```python +from roe import RoeClient + +client = RoeClient() + +result = client.agents.retrieve( + agent_id="agent_id", # required +) +``` + +#### `agents_partial_update` + +Partially update an agent. + +```python +from roe import RoeClient + +client = RoeClient() + +result = client.agents.update( + agent_id="agent_id", # required + name="name", # optional + disable_cache=True, # optional + cache_failed_jobs=True, # optional +) +``` + +#### `agents_update` + +Update a base agent. + +```python +from roe import RoeClient + +client = RoeClient() + +agent_id = "agent_id" # required path + +response = client.raw.get_httpx_client().request( + "PUT", + f"/v1/agents/{agent_id}/", + params={ + "organization_id": "00000000-0000-0000-0000-000000000000", # optional + }, + json={ + "name": "name", # optional + "disable_cache": True, # optional + "cache_failed_jobs": True, # optional + }, +) +response.raise_for_status() +result = response.json() +``` + +#### `agents_duplicate_create` + +Duplicate an agent. + +```python +from roe import RoeClient + +client = RoeClient() + +result = client.agents.duplicate( + agent_id="agent_id", # required +) +``` + +#### `agents_jobs_cancel_all_create` + +Cancel all agent jobs + +```python +from roe import RoeClient + +client = RoeClient() + +result = client.agents.jobs.cancel_all( + agent_id="agent_id", # required +) +``` + +#### `agents_versions_list` + +List agent versions. + +```python +from roe import RoeClient + +client = RoeClient() + +result = client.agents.versions.list( + agent_id="agent_id", # required +) +``` + +#### `agents_versions_create` + +Create a new agent version. + +```python +from roe import RoeClient + +client = RoeClient() + +result = client.agents.versions.create( + agent_id="agent_id", # required + input_definitions=[{"key": "text", "data_type": "text/plain"}], # optional + engine_config={"model": "gpt-4.1"}, # optional + version_name="version_name", # optional + description="description", # optional +) +``` + +#### `agents_versions_current_retrieve` + +Retrieve the current version of an agent. + +```python +from roe import RoeClient + +client = RoeClient() + +result = client.agents.versions.retrieve_current( + agent_id="agent_id", # required +) +``` + +#### `agents_versions_destroy` + +Delete an agent version. + +```python +from roe import RoeClient + +client = RoeClient() + +result = client.agents.versions.delete( + agent_id="agent_id", # required + version_id="version_id", # required +) +``` + +#### `agents_versions_retrieve` + +Retrieve an agent version. + +```python +from roe import RoeClient + +client = RoeClient() + +result = client.agents.versions.retrieve( + agent_id="agent_id", # required + version_id="version_id", # required + get_supports_eval=True, # optional +) +``` + +#### `agents_versions_partial_update` + +Partially update an agent version. + +```python +from roe import RoeClient + +client = RoeClient() + +result = client.agents.versions.update( + agent_id="agent_id", # required + version_id="version_id", # required + version_name="version_name", # optional + description="description", # optional +) +``` + +#### `agents_versions_update` + +Update an agent version. + +```python +from roe import RoeClient + +client = RoeClient() + +agent_id = "agent_id" # required path +agent_version_id = "agent_version_id" # required path + +response = client.raw.get_httpx_client().request( + "PUT", + f"/v1/agents/{agent_id}/versions/{agent_version_id}/", + params={ + "organization_id": "00000000-0000-0000-0000-000000000000", # optional + }, + json={ + "version_name": "version_name", # optional + "description": "description", # optional + }, +) +response.raise_for_status() +result = response.json() +``` + +### Connections + +#### `connections_list` + +List/create connections. + +```python +from roe import RoeClient + +client = RoeClient() + +response = client.raw.get_httpx_client().request( + "GET", + "/v1/connections/", + params={ + "connector_type": "connector_type", # optional + "organization_id": "00000000-0000-0000-0000-000000000000", # optional + "page": 1, # optional + "page_size": 1, # optional + "search": "search", # optional + }, +) +response.raise_for_status() +result = response.json() +``` + +#### `connections_create` + +List/create connections. + +```python +from roe import RoeClient + +client = RoeClient() + +response = client.raw.get_httpx_client().request( + "POST", + "/v1/connections/", + params={ + "organization_id": "00000000-0000-0000-0000-000000000000", # optional + }, + json={ + "connector_type": "connector_type", # required + "name": "name", # required + "description": "description", # optional + "config": {}, # required + "auth_config": {}, # optional + "organization_id": "00000000-0000-0000-0000-000000000000", # optional + }, +) +response.raise_for_status() +result = response.json() +``` + +#### `connections_test_credentials_create` + +Test credentials without storing them. + +```python +from roe import RoeClient + +client = RoeClient() + +response = client.raw.get_httpx_client().request( + "POST", + "/v1/connections/test-credentials/", + json={ + "connector_type": "connector_type", # required + "config": {}, # required + "auth_config": {}, # optional + }, +) +response.raise_for_status() +result = response.json() +``` + +#### `connections_destroy` + +Manage connection. + +```python +from roe import RoeClient + +client = RoeClient() + +id = "id" # required path + +response = client.raw.get_httpx_client().request( + "DELETE", + f"/v1/connections/{id}/", + params={ + "organization_id": "00000000-0000-0000-0000-000000000000", # optional + }, +) +response.raise_for_status() +result = response.json() +``` + +#### `connections_retrieve` + +Manage connection. + +```python +from roe import RoeClient + +client = RoeClient() + +id = "id" # required path + +response = client.raw.get_httpx_client().request( + "GET", + f"/v1/connections/{id}/", + params={ + "organization_id": "00000000-0000-0000-0000-000000000000", # optional + }, +) +response.raise_for_status() +result = response.json() +``` + +#### `connections_partial_update` + +Manage connection. + +```python +from roe import RoeClient + +client = RoeClient() + +id = "id" # required path + +response = client.raw.get_httpx_client().request( + "PATCH", + f"/v1/connections/{id}/", + params={ + "organization_id": "00000000-0000-0000-0000-000000000000", # optional + }, + json={ + "name": "name", # optional + "description": "description", # optional + "config": {}, # optional + "auth_config": {}, # optional + }, +) +response.raise_for_status() +result = response.json() +``` + +#### `connections_update` + +Manage connection. + +```python +from roe import RoeClient + +client = RoeClient() + +id = "id" # required path + +response = client.raw.get_httpx_client().request( + "PUT", + f"/v1/connections/{id}/", + params={ + "organization_id": "00000000-0000-0000-0000-000000000000", # optional + }, + json={ + "name": "name", # optional + "description": "description", # optional + "config": {}, # optional + "auth_config": {}, # optional + }, +) +response.raise_for_status() +result = response.json() +``` + +#### `connections_test_create` + +Test connection. + +```python +from roe import RoeClient + +client = RoeClient() + +id = "id" # required path + +response = client.raw.get_httpx_client().request( + "POST", + f"/v1/connections/{id}/test/", + params={ + "organization_id": "00000000-0000-0000-0000-000000000000", # optional + }, +) +response.raise_for_status() +result = response.json() +``` + +### Connectors + +#### `connectors_retrieve` + +List all connector types. + +```python +from roe import RoeClient + +client = RoeClient() + +response = client.raw.get_httpx_client().request( + "GET", + "/v1/connectors/", +) +response.raise_for_status() +result = response.json() +``` + +#### `connectors_retrieve_by_type` + +Get connector details. + +```python +from roe import RoeClient + +client = RoeClient() + +connector_type = "connector_type" # required path + +response = client.raw.get_httpx_client().request( + "GET", + f"/v1/connectors/{connector_type}/", +) +response.raise_for_status() +result = response.json() +``` + +### Discovery + +#### `discovery_supported_models_list` + +List supported model IDs + +```python +from roe import RoeClient + +client = RoeClient() + +result = client.discovery.list_supported_models( + capability="capability", # optional +) +``` + +#### `discovery_agent_engine_types_list` + +List supported agent engine types + +```python +from roe import RoeClient + +client = RoeClient() + +result = client.discovery.list_agent_engine_types() +``` + +### Policies + +#### `policies_list` + +List all policies and create a new policy. + +```python +from roe import RoeClient + +client = RoeClient() + +result = client.policies.list( + page=1, # optional + page_size=1, # optional +) +``` + +#### `policies_create` + +List all policies and create a new policy. + +```python +from roe import RoeClient + +client = RoeClient() + +result = client.policies.create( + name="name", # required + content={}, # required + description="description", # optional + version_name="version_name", # optional +) +``` + +#### `policies_destroy` + +Retrieve, update, or delete a single policy by ID. + +```python +from roe import RoeClient + +client = RoeClient() + +result = client.policies.delete( + policy_id="policy_id", # required +) +``` + +#### `policies_retrieve` + +Retrieve, update, or delete a single policy by ID. + +```python +from roe import RoeClient + +client = RoeClient() + +result = client.policies.retrieve( + policy_id="policy_id", # required +) +``` + +#### `policies_partial_update` + +Retrieve, update, or delete a single policy by ID. + +```python +from roe import RoeClient + +client = RoeClient() + +result = client.policies.update( + policy_id="policy_id", # required + name="name", # optional + description="description", # optional +) +``` + +#### `policies_update` + +Retrieve, update, or delete a single policy by ID. + +```python +from roe import RoeClient + +client = RoeClient() + +id = "id" # required path + +response = client.raw.get_httpx_client().request( + "PUT", + f"/v1/policies/{id}/", + params={ + "organization_id": "00000000-0000-0000-0000-000000000000", # optional + }, + json={ + "name": "name", # required + "description": "description", # optional + }, +) +response.raise_for_status() +result = response.json() +``` + +#### `policies_versions_list` + +Create a new policy version or list all versions of a specific policy. + +```python +from roe import RoeClient + +client = RoeClient() + +result = client.policies.versions.list( + policy_id="policy_id", + page=1, + page_size=10, +) +``` + +#### `policies_versions_create` + +Create a new policy version or list all versions of a specific policy. + +```python +from roe import RoeClient + +client = RoeClient() + +result = client.policies.versions.create( + policy_id="policy_id", + content={}, + version_name="version_name", + base_version_id="base_version_id", +) +``` + +#### `policies_versions_retrieve` + +Get a specific policy version by policy_id and version_id. + +```python +from roe import RoeClient + +client = RoeClient() + +result = client.policies.versions.retrieve( + policy_id="policy_id", + version_id="version_id", +) +``` + +### Tables + +#### `tables_list` + +List Roe tables + +```python +from roe import RoeClient + +client = RoeClient() + +response = client.raw.get_httpx_client().request( + "GET", + "/v1/tables/", +) +response.raise_for_status() +result = response.json() +``` + +#### `tables_query_create` + +Run a read-only Roe table query + +```python +from roe import RoeClient + +client = RoeClient() + +response = client.raw.get_httpx_client().request( + "POST", + "/v1/tables/query/", + json={ + "sql": "sql", # required + "limit": 1, # optional + }, +) +response.raise_for_status() +result = response.json() +``` + +#### `tables_query_result_retrieve` + +Get a Roe table query result + +```python +from roe import RoeClient + +client = RoeClient() + +table_query_id = "table_query_id" # required path + +response = client.raw.get_httpx_client().request( + "GET", + f"/v1/tables/query/{table_query_id}/result/", +) +response.raise_for_status() +result = response.json() +``` + +#### `tables_destroy` + +Delete a Roe table + +```python +from roe import RoeClient + +client = RoeClient() + +table_name = "table_name" # required path + +response = client.raw.get_httpx_client().request( + "DELETE", + f"/v1/tables/{table_name}/", +) +response.raise_for_status() +result = response.json() +``` + +#### `tables_describe_retrieve` + +Describe a Roe table + +```python +from roe import RoeClient + +client = RoeClient() + +table_name = "table_name" # required path + +response = client.raw.get_httpx_client().request( + "GET", + f"/v1/tables/{table_name}/describe/", +) +response.raise_for_status() +result = response.json() +``` + +#### `tables_preview_retrieve` + +Preview a Roe table + +```python +from roe import RoeClient + +client = RoeClient() + +table_name = "table_name" # required path + +response = client.raw.get_httpx_client().request( + "GET", + f"/v1/tables/{table_name}/preview/", + params={ + "limit": 1, # optional + }, +) +response.raise_for_status() +result = response.json() +``` + +#### `upload_table` + +Upload a CSV as a Roe table + +```python +from roe import RoeClient + +client = RoeClient() + +result = client.tables.upload( + table_name="table_name", + file="file.csv", + with_headers=True, +) +``` + +### Users + +#### `users_current_user_retrieve` + +Get the current user + +```python +from roe import RoeClient + +client = RoeClient() + +result = client.users.me() +``` + +## Use Cases + +These workflows assume `ROE_API_KEY` and `ROE_ORGANIZATION_ID` are set. +The first example provisions a policy and an agent from scratch; the later two +reuse an existing agent id so they stay focused on the run-and-fetch calls. + +### Create a policy and run a policy-aware agent + +```python +from roe import RoeClient + +client = RoeClient() + +policy = client.policies.create( + name="AML Investigation Policy", + content={ + "guidelines": { + "categories": [ + { + "title": "Transaction Patterns", + "rules": [ + { + "title": "Structuring below reporting thresholds", + "flag": "RED_FLAG", + "description": "Deposits just under CTR thresholds in a short window.", + } + ], + } + ] + }, + "dispositions": { + "classifications": [ + {"name": "SAR", "description": "File a Suspicious Activity Report."}, + {"name": "DISMISS", "description": "Close as non-suspicious."}, + ] + }, + }, +) + +agent = client.agents.create( + name="AML Investigation Agent", + engine_class_id="AMLInvestigationEngine", + input_definitions=[ + { + "key": "alert_data", + "data_type": "text/plain", + "description": "Alert to investigate.", + } + ], + engine_config={ + "policy_version_id": str(policy.current_version_id), + "alert_data": "${alert_data}", + }, +) + +job = client.agents.run( + agent_id=str(agent.id), + timeout_seconds=300, + alert_data="Customer made 9 cash deposits of $9,500 over three days.", +) +result = job.wait(interval=5.0, timeout=300) + +for output in result.outputs: + print(f"{output.key}: {output.value}") +``` + +### Run an agent and download a saved reference + +```python +import json +import os +from pathlib import Path + +from roe import RoeClient + +client = RoeClient() +agent_id = os.environ["ROE_URL_AGENT_ID"] + +job = client.agents.run( + agent_id=agent_id, + timeout_seconds=300, + url="https://www.roe-ai.com/", + metadata={"use_case": "website-scan"}, +) +result = job.wait(interval=5.0, timeout=300) + +for output in result.outputs: + try: + payload = json.loads(output.value) + except json.JSONDecodeError: + continue + for ref in payload.get("references", []): + resource_id = ref.get("resource_id") + if resource_id: + content = client.agents.jobs.download_reference(job.id, resource_id) + Path(f"{resource_id}.bin").write_bytes(content) +``` + +### Run a batch of inputs + +```python +import os + +from roe import RoeClient + +client = RoeClient() +agent_id = os.environ["ROE_TEXT_AGENT_ID"] + +batch = client.agents.run_many( + agent_id=agent_id, + batch_inputs=[ + {"text": "Summarize the customer complaint."}, + {"text": "Extract the requested follow-up action."}, + ], + timeout_seconds=300, +) +results = batch.wait(interval=5.0, timeout=300) + +for job_result in results: + if job_result is None: + continue + for output in job_result.result or []: + print(f"{output.key}: {output.value}") +``` diff --git a/openapi/readme_blocks.yml b/openapi/readme_blocks.yml index 91a18c1..57287fc 100644 --- a/openapi/readme_blocks.yml +++ b/openapi/readme_blocks.yml @@ -4,9 +4,9 @@ blocks: generated_friendly_apis: python: | - ## Generated Friendly APIs + ## SDK Operation Groups - This block is synced from `roe-main/roe-sdk/sdk_contract.yml` during SDK fan-out. + Common operations are available directly on the SDK client. ```python engines = client.discovery.list_agent_engine_types() @@ -19,9 +19,9 @@ blocks: ) ``` typescript: | - ## Generated Friendly APIs + ## SDK Operation Groups - This block is synced from `roe-main/roe-sdk/sdk_contract.yml` during SDK fan-out. + Common operations are available directly on the SDK client. ```typescript const engines = await client.discovery.listAgentEngineTypes(); @@ -34,9 +34,9 @@ blocks: }); ``` go: | - ## Generated Friendly APIs + ## SDK Operation Groups - This block is synced from `roe-main/roe-sdk/sdk_contract.yml` during SDK fan-out. + Common operations are available directly on the SDK client. ```go engines, err := client.Discovery.ListAgentEngineTypes() diff --git a/openapi/wrappers.yml b/openapi/wrappers.yml index ec8c9d3..15464d0 100644 --- a/openapi/wrappers.yml +++ b/openapi/wrappers.yml @@ -39,3 +39,504 @@ apis: empty_response_message: table upload returned an empty response body_type: TableUploadRequest body_import: roe._generated.models.table_upload_request.TableUploadRequest + agents: + class_name: AgentsAPI + docstring: API for managing and running agents. + operations: + - kind: body + method_name: list + docstring: '' + method: GET + path: /v1/agents/ + endpoint_module: roe._generated.api.agents.agents_list + return_type: PaginatedBaseAgentList + return_import: roe._generated.models.paginated_base_agent_list.PaginatedBaseAgentList + parameters: + - name: page + location: query + annotation: int | None + default: null + pass_unset_when_none: true + - name: page_size + location: query + annotation: int | None + default: null + pass_unset_when_none: true + - kind: body + method_name: retrieve + docstring: '' + method: GET + path: /v1/agents/{agent_id}/ + endpoint_module: roe._generated.api.agents.agents_retrieve + return_type: BaseAgent + return_import: roe._generated.models.base_agent.BaseAgent + parameters: + - name: agent_id + location: path + annotation: str + coerce: uuid + - kind: body + method_name: create + docstring: '' + method: POST + path: /v1/agents/ + endpoint_module: roe._generated.api.agents.agents_create + return_type: BaseAgent + return_import: roe._generated.models.base_agent.BaseAgent + body_type: BaseAgentCreateRequest + body_import: roe._generated.models.base_agent_create_request.BaseAgentCreateRequest + inject_organization_id: true + parameters: + - name: name + location: body + annotation: str + - name: engine_class_id + location: body + annotation: str + - name: input_definitions + location: body + annotation: list[dict[str, Any]] | None + default: null + or_empty: list + - name: engine_config + location: body + annotation: dict[str, Any] | None + default: null + or_empty: dict + - name: version_name + location: body + annotation: str | None + default: null + pass_unset_when_none: true + - name: description + location: body + annotation: str | None + default: null + pass_unset_when_none: true + - kind: body + method_name: update + docstring: Update an agent via PATCH (partial update). + method: PATCH + path: /v1/agents/{agent_id}/ + endpoint_module: roe._generated.api.agents.agents_partial_update + return_type: BaseAgent + return_import: roe._generated.models.base_agent.BaseAgent + body_type: PatchedBaseAgentUpdateRequest + body_import: roe._generated.models.patched_base_agent_update_request.PatchedBaseAgentUpdateRequest + parameters: + - name: agent_id + location: path + annotation: str + coerce: uuid + - name: name + location: body + annotation: str | None + default: null + pass_unset_when_none: true + - name: disable_cache + location: body + annotation: bool | None + default: null + pass_unset_when_none: true + - name: cache_failed_jobs + location: body + annotation: bool | None + default: null + pass_unset_when_none: true + - kind: body + method_name: delete + docstring: '' + method: DELETE + path: /v1/agents/{agent_id}/ + endpoint_module: roe._generated.api.agents.agents_destroy + parameters: + - name: agent_id + location: path + annotation: str + coerce: uuid + - kind: body + method_name: duplicate + docstring: |- + Duplicate an agent. Returns the resulting ``AgentVersion``. + + The endpoint historically returned a JSON object with a ``base_agent`` + key wrapping the new agent; the generated client now models the + response as ``AgentVersion`` directly. Callers wanting the new base + agent should read ``result.base_agent`` (already populated). + method: POST + path: /v1/agents/{agent_id}/duplicate/ + endpoint_module: roe._generated.api.agents.agents_duplicate_create + return_type: AgentVersion + return_import: roe._generated.models.agent_version.AgentVersion + parameters: + - name: agent_id + location: path + annotation: str + coerce: uuid + - kind: manual + method_name: run + docstring: Run an agent asynchronously and return a Job handle. + method: POST + path: /v1/agents/run/{agent_id}/async/ + - kind: manual + method_name: run_many + docstring: Run an agent over a batch of inputs and return a JobBatch handle. + method: POST + path: /v1/agents/run/{agent_id}/async/many/ + - kind: manual + method_name: run_sync + docstring: Run an agent synchronously and return its output data. + method: POST + path: /v1/agents/run/{agent_id}/ + - kind: manual + method_name: run_version + docstring: Run a specific agent version asynchronously and return a Job handle. + method: POST + path: /v1/agents/run/{agent_id}/versions/{agent_version_id}/async/ + - kind: manual + method_name: run_version_sync + docstring: Run a specific agent version synchronously and return its output data. + method: POST + path: /v1/agents/run/{agent_id}/versions/{agent_version_id}/ + namespaces: + versions: + class_name: AgentVersionsAPI + attr: versions + docstring: Nested API for agent version operations. + operations: + - kind: body + method_name: list + docstring: '' + method: GET + path: /v1/agents/{agent_id}/versions/ + endpoint_module: roe._generated.api.agents.agents_versions_list + return_type: AgentVersion + return_import: roe._generated.models.agent_version.AgentVersion + return_shape: list + list_error_label: agent versions + parameters: + - name: agent_id + location: path + annotation: str + coerce: uuid + - kind: body + method_name: retrieve + docstring: '' + method: GET + path: /v1/agents/{agent_id}/versions/{agent_version_id}/ + endpoint_module: roe._generated.api.agents.agents_versions_retrieve + return_type: AgentVersion + return_import: roe._generated.models.agent_version.AgentVersion + parameters: + - name: agent_id + location: path + annotation: str + coerce: uuid + - name: version_id + location: path + annotation: str + coerce: uuid + - name: get_supports_eval + location: query + annotation: bool | None + default: null + pass_unset_when_none: true + - kind: body + method_name: retrieve_current + docstring: '' + method: GET + path: /v1/agents/{agent_id}/versions/current/ + endpoint_module: roe._generated.api.agents.agents_versions_current_retrieve + return_type: AgentVersion + return_import: roe._generated.models.agent_version.AgentVersion + parameters: + - name: agent_id + location: path + annotation: str + coerce: uuid + - kind: body + method_name: create + docstring: '' + method: POST + path: /v1/agents/{agent_id}/versions/ + endpoint_module: roe._generated.api.agents.agents_versions_create + return_type: AgentVersion + return_import: roe._generated.models.agent_version.AgentVersion + body_type: AgentVersionCreateRequest + body_import: roe._generated.models.agent_version_create_request.AgentVersionCreateRequest + refetch_with_retrieve: true + parameters: + - name: agent_id + location: path + annotation: str + coerce: uuid + - name: input_definitions + location: body + annotation: list[dict[str, Any]] | None + default: null + or_empty: list + - name: engine_config + location: body + annotation: dict[str, Any] | None + default: null + or_empty: dict + - name: version_name + location: body + annotation: str | None + default: null + pass_unset_when_none: true + - name: description + location: body + annotation: str | None + default: null + pass_unset_when_none: true + - kind: body + method_name: update + docstring: Update an agent version via PATCH (partial update). + method: PATCH + path: /v1/agents/{agent_id}/versions/{agent_version_id}/ + endpoint_module: roe._generated.api.agents.agents_versions_partial_update + body_type: PatchedAgentVersionUpdateRequest + body_import: roe._generated.models.patched_agent_version_update_request.PatchedAgentVersionUpdateRequest + parameters: + - name: agent_id + location: path + annotation: str + coerce: uuid + - name: version_id + location: path + annotation: str + coerce: uuid + - name: version_name + location: body + annotation: str | None + default: null + pass_unset_when_none: true + - name: description + location: body + annotation: str | None + default: null + pass_unset_when_none: true + - kind: body + method_name: delete + docstring: '' + method: DELETE + path: /v1/agents/{agent_id}/versions/{agent_version_id}/ + endpoint_module: roe._generated.api.agents.agents_versions_destroy + parameters: + - name: agent_id + location: path + annotation: str + coerce: uuid + - name: version_id + location: path + annotation: str + coerce: uuid + jobs: + class_name: AgentJobsAPI + attr: jobs + docstring: Nested API for agent job operations. + operations: + - kind: body + method_name: retrieve_status + docstring: '' + method: GET + path: /v1/agents/jobs/{job_id}/status/ + endpoint_module: roe._generated.api.agents.agents_jobs_status_retrieve + return_type: AgentJobSingleStatus + return_import: roe._generated.models.agent_job_single_status.AgentJobSingleStatus + parameters: + - name: job_id + location: path + annotation: str + coerce: uuid + - kind: body + method_name: retrieve_result + docstring: '' + method: GET + path: /v1/agents/jobs/{agent_job_id}/result/ + endpoint_module: roe._generated.api.agents.agents_jobs_result_retrieve + return_type: AgentJobResultResponse + return_import: roe._generated.models.agent_job_result_response.AgentJobResultResponse + parameters: + - name: job_id + location: path + annotation: str + coerce: uuid + - kind: body + method_name: cancel + docstring: '' + method: POST + path: /v1/agents/jobs/{job_id}/cancel/ + endpoint_module: roe._generated.api.agents.agents_jobs_cancel_create + parameters: + - name: job_id + location: path + annotation: str + coerce: uuid + - kind: body + method_name: cancel_all + docstring: '' + method: POST + path: /v1/agents/{agent_id}/jobs/cancel-all/ + endpoint_module: roe._generated.api.agents.agents_jobs_cancel_all_create + parameters: + - name: agent_id + location: path + annotation: str + coerce: uuid + - kind: body + method_name: delete_data + docstring: '' + method: POST + path: /v1/agents/jobs/{job_id}/delete-data/ + endpoint_module: roe._generated.api.agents.agents_jobs_delete_data_create + return_type: AgentJobDeleteDataResponse + return_import: roe._generated.models.agent_job_delete_data_response.AgentJobDeleteDataResponse + parameters: + - name: job_id + location: path + annotation: str + coerce: uuid + - kind: manual + method_name: retrieve_status_many + docstring: Retrieve statuses for many jobs (chunked batch calls). + method: POST + path: /v1/agents/jobs/statuses/ + - kind: manual + method_name: retrieve_result_many + docstring: Retrieve results for many jobs (chunked batch calls). + method: POST + path: /v1/agents/jobs/results/ + - kind: manual + method_name: download_reference + docstring: Download a binary reference produced by an agent job. + method: GET + path: /v1/agents/jobs/{agent_job_id}/references/{resource_id}/ + policies: + class_name: PoliciesAPI + docstring: API for managing policies used by agentic workflows. + operations: + - kind: body + method_name: list + docstring: List policies in the organization. + method: GET + path: /v1/policies/ + endpoint_module: roe._generated.api.policies.policies_list + return_type: PaginatedPolicyList + return_import: roe._generated.models.paginated_policy_list.PaginatedPolicyList + parameters: + - name: page + location: query + annotation: int | None + default: null + pass_unset_when_none: true + - name: page_size + location: query + annotation: int | None + default: null + pass_unset_when_none: true + - kind: body + method_name: retrieve + docstring: Retrieve a specific policy by ID. + method: GET + path: /v1/policies/{id}/ + endpoint_module: roe._generated.api.policies.policies_retrieve + return_type: Policy + return_import: roe._generated.models.policy.Policy + parameters: + - name: policy_id + location: path + annotation: str + coerce: uuid + - kind: body + method_name: create + docstring: Create a new policy with an initial version. + method: POST + path: /v1/policies/ + endpoint_module: roe._generated.api.policies.policies_create + return_type: CreatePolicy + return_import: roe._generated.models.create_policy.CreatePolicy + body_type: CreatePolicyRequest + body_import: roe._generated.models.create_policy_request.CreatePolicyRequest + parameters: + - name: name + location: body + annotation: str + - name: content + location: body + annotation: dict[str, Any] + - name: description + location: body + annotation: str + default: '' + - name: version_name + location: body + annotation: str | None + default: null + pass_unset_when_none: true + - kind: body + method_name: update + docstring: Update a policy's metadata via PATCH (partial update). + method: PATCH + path: /v1/policies/{id}/ + endpoint_module: roe._generated.api.policies.policies_partial_update + return_type: Any + body_type: PatchedUpdatePolicyRequest + body_import: roe._generated.models.patched_update_policy_request.PatchedUpdatePolicyRequest + parameters: + - name: policy_id + location: path + annotation: str + coerce: uuid + - name: name + location: body + annotation: str | None + default: null + pass_unset_when_none: true + - name: description + location: body + annotation: str | None + default: null + pass_unset_when_none: true + - kind: body + method_name: delete + docstring: Delete a policy and all its versions. + method: DELETE + path: /v1/policies/{id}/ + endpoint_module: roe._generated.api.policies.policies_destroy + parameters: + - name: policy_id + location: path + annotation: str + coerce: uuid + namespaces: + versions: + class_name: PolicyVersionsAPI + attr: versions + docstring: Nested API for policy version operations. + operations: + - kind: manual + method_name: list + docstring: List versions of a policy. + method: GET + path: /v1/policies/{policy_id}/versions/ + - kind: manual + method_name: retrieve + docstring: Retrieve a specific version of a policy. + method: GET + path: /v1/policies/{policy_id}/versions/{version_id}/ + - kind: manual + method_name: create + docstring: Create a new policy version (auto-set as current). + method: POST + path: /v1/policies/{policy_id}/versions/ + users: + class_name: UsersAPI + docstring: API for retrieving information about the authenticated user. + operations: + - kind: manual + method_name: me + docstring: Return the currently-authenticated user. + method: GET + path: /v1/users/current_user/ diff --git a/pyproject.toml b/pyproject.toml index 4f1dee9..bb92b20 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "roe-ai" -version = "1.1.0" +version = "1.1.1" authors = [ { name = "Roe", email = "founders@roe-ai.com" }, ] diff --git a/scripts/generate_wrappers.py b/scripts/generate_wrappers.py index 13e9604..c5fc020 100644 --- a/scripts/generate_wrappers.py +++ b/scripts/generate_wrappers.py @@ -85,25 +85,12 @@ def _load_project_version() -> str: return version -def _load_release_marker() -> str: - marker = ( - (ROOT_DIR / ".roe-main-release-version").read_text(encoding="utf-8").strip() - ) - if not marker: - raise ValueError(".roe-main-release-version must not be empty") - return marker - - def _render_release_banner() -> str: version = _load_project_version() - marker = _load_release_marker() return ( - f"> **v{version}** - Schema synchronization across the public SDKs: roe-ai\n" - f"> (Python), roe-typescript, and roe-golang. This release is generated from\n" - f"> SDK OpenAPI marker `{marker}`, and all public package metadata is bumped to\n" - f"> {version}.\n" - "> Python friendly wrappers are generated from `openapi/wrappers.yml`;\n" - "> current generated facades include `client.discovery` and `client.tables`." + f"> **v{version}** - SDK operation coverage is synchronized across Python,\n" + "> TypeScript, and Go. See `SDK_EXAMPLES.md` for copy-ready examples and\n" + "> use cases." ) @@ -294,6 +281,136 @@ def _as_generated_file( ''' +def _operation_kind(operation: dict[str, Any]) -> str: + return operation.get("kind", "simple") + + +def _iter_specs(spec: dict[str, Any]): + yield spec + for namespace in (spec.get("namespaces") or {}).values(): + yield namespace + + +def _has_manual_operations(spec: dict[str, Any]) -> bool: + return any( + _operation_kind(operation) == "manual" + for scoped_spec in _iter_specs(spec) + for operation in scoped_spec.get("operations") or [] + ) + + +def _field_expr(param: dict[str, Any]) -> str: + name = param["name"] + if param.get("coerce") == "uuid": + return f"UUID(str({name}))" + if param.get("or_empty") == "list": + return f"{name} or []" + if param.get("or_empty") == "dict": + return f"{name} or {{}}" + if param.get("pass_unset_when_none"): + return f"{name} if {name} is not None else UNSET" + return name + + +def _signature_param(param: dict[str, Any]) -> str: + if "default" in param: + return ( + f"{param['name']}: {param['annotation']} = " + f"{_default_expr(param['default'])}" + ) + return f"{param['name']}: {param['annotation']}" + + +def _body_method(operation: dict[str, Any]) -> str: + method_name = operation["method_name"] + return_type = operation.get("return_type") + docstring = operation.get("docstring", "") + params = operation.get("parameters") or [] + path_params = [param for param in params if param.get("location") == "path"] + query_params = [ + param for param in params if param.get("location", "query") == "query" + ] + body_params = [param for param in params if param.get("location") == "body"] + + signature_parts = ["self", *[_signature_param(param) for param in params]] + if len(signature_parts) == 1: + signature_prefix = f" def {method_name}(self)" + else: + formatted_signature = ",\n".join(f" {part}" for part in signature_parts) + signature_prefix = f" def {method_name}(\n{formatted_signature},\n )" + + annotated_return = ( + f"list[{return_type}]" + if return_type and operation.get("return_shape") == "list" + else return_type or "None" + ) + lines = [ + f"{signature_prefix} -> {annotated_return}:\n", + f' """{docstring}"""\n', + ] + body_type = operation.get("body_type") + if body_type: + lines.append(f" body = {body_type}(\n") + if operation.get("inject_organization_id"): + lines.append(" organization_id=self._org_id,\n") + for param in body_params: + lines.append(f" {param['name']}={_field_expr(param)},\n") + lines.append(" )\n") + + endpoint_name = _module_import_parts(operation["endpoint_module"])[1] + call_args = [" self._raw,\n", f" {endpoint_name},\n"] + call_args.extend(f" {_field_expr(param)},\n" for param in path_params) + if body_type: + call_args.append(" body=body,\n") + for param in query_params: + call_args.append(f" {param['name']}={_field_expr(param)},\n") + if operation.get("inject_organization_id"): + call_args.append(" organization_id=self._org_id,\n") + + if return_type or operation.get("refetch_with_retrieve"): + lines.append( + " resp = request_json(\n" + if body_type + else " response = request_raw(\n" + ) + else: + lines.append( + " request_json(\n" if body_type else " request_raw(\n" + ) + lines.extend(call_args) + lines.append(" )\n") + + if operation.get("refetch_with_retrieve"): + first_path = path_params[0]["name"] if path_params else "" + lines.extend( + [ + " created = resp.parsed\n", + " if created is None or created.id is None:\n", + ' raise RoeAPIException(f"Unexpected response from server: status={resp.status_code}")\n', + f" return self.retrieve({first_path}, str(created.id))\n", + ] + ) + elif return_type: + if body_type: + lines.append(" return resp.parsed # type: ignore[return-value]\n") + elif operation.get("return_shape") == "list": + label = operation.get("list_error_label", return_type) + lines.extend( + [ + " data = response.json()\n", + " if not isinstance(data, list):\n", + f' raise RoeAPIException(f"{label} returned unexpected response shape: {{data!r}}")\n', + f" return [{return_type}.from_dict(item) for item in data]\n", + ] + ) + else: + lines.append(f" return {return_type}.from_dict(response.json())\n") + else: + lines.append(" return None\n") + + return "".join(lines) + + def _render_api_module(api_name: str, spec: dict[str, Any]) -> str: class_name = spec["class_name"] operations = spec.get("operations") or [] @@ -414,12 +531,18 @@ def main() -> None: contract = _load_contract() apis = contract["apis"] API_DIR.mkdir(parents=True, exist_ok=True) + for stale_partial in API_DIR.glob("_*_generated.py"): + stale_partial.unlink() + registry_apis: dict[str, dict[str, Any]] = {} for api_name, spec in sorted(apis.items()): + if _has_manual_operations(spec): + continue target = API_DIR / f"{api_name}.py" target.write_text(_render_api_module(api_name, spec)) + registry_apis[api_name] = spec - REGISTRY_PATH.write_text(_render_registry(apis)) + REGISTRY_PATH.write_text(_render_registry(registry_apis)) _sync_readme_release_banner() _sync_readme_block() print( diff --git a/tests/unit/test_generate_wrappers.py b/tests/unit/test_generate_wrappers.py new file mode 100644 index 0000000..c8d4606 --- /dev/null +++ b/tests/unit/test_generate_wrappers.py @@ -0,0 +1,58 @@ +from __future__ import annotations + +import importlib.util +from pathlib import Path +import sys +import types + + +def _load_generate_wrappers_module(): + ruamel = types.ModuleType("ruamel") + ruamel_yaml = types.ModuleType("ruamel.yaml") + ruamel_yaml.YAML = object + sys.modules.setdefault("ruamel", ruamel) + sys.modules.setdefault("ruamel.yaml", ruamel_yaml) + + path = Path(__file__).resolve().parents[2] / "scripts" / "generate_wrappers.py" + spec = importlib.util.spec_from_file_location("generate_wrappers", path) + assert spec is not None + module = importlib.util.module_from_spec(spec) + assert spec.loader is not None + spec.loader.exec_module(module) + return module + + +def test_main_deletes_stale_partial_modules_for_hand_maintained_apis( + tmp_path, monkeypatch +): + module = _load_generate_wrappers_module() + stale = tmp_path / "_agents_generated.py" + stale.write_text("# stale\n") + + monkeypatch.setattr(module, "API_DIR", tmp_path) + monkeypatch.setattr(module, "REGISTRY_PATH", tmp_path / "_generated_registry.py") + monkeypatch.setattr( + module, + "_load_contract", + lambda: { + "apis": { + "agents": { + "class_name": "AgentsAPI", + "docstring": "API for agents.", + "operations": [ + { + "kind": "manual", + "method_name": "run", + "docstring": "Run an agent.", + } + ], + } + } + }, + ) + monkeypatch.setattr(module, "_sync_readme_release_banner", lambda: None) + monkeypatch.setattr(module, "_sync_readme_block", lambda: None) + + module.main() + + assert not stale.exists() diff --git a/tests/unit/test_translate_response.py b/tests/unit/test_translate_response.py index adb6b3d..9f99e31 100644 --- a/tests/unit/test_translate_response.py +++ b/tests/unit/test_translate_response.py @@ -102,7 +102,9 @@ def test_dict_body_uses_message_key_as_fallback(): assert exc_info.value.message == "broken" -def _resp_with_headers(status: int, headers: dict[str, str], content: bytes = b'{"detail":"x"}') -> httpx.Response: +def _resp_with_headers( + status: int, headers: dict[str, str], content: bytes = b'{"detail":"x"}' +) -> httpx.Response: return httpx.Response(status, headers=headers, content=content) diff --git a/uv.lock b/uv.lock index c14b7c4..65b3d72 100644 --- a/uv.lock +++ b/uv.lock @@ -454,7 +454,7 @@ wheels = [ [[package]] name = "roe-ai" -version = "1.1.0" +version = "1.1.1" source = { editable = "." } dependencies = [ { name = "attrs" },