# opus-aaico Python SDK -- Complete Reference

> Drop this file into your AI coding assistant's context to provide full knowledge of the opus-aaico SDK. This enables AI-assisted integration code, debugging, and application development on top of OPUS workflows.

---

## What is opus-aaico

`opus-aaico` is a Python SDK for the OPUS workflow automation platform. It wraps the core OPUS REST API endpoints (workflows, jobs, files) into typed Python methods with both synchronous and asynchronous clients.

- **Package:** `pip install opus-aaico`
- **Import:** `import opus_aaico`
- **Python:** 3.9+
- **Dependencies:** httpx, pydantic v2
- **Source:** https://github.com/moibrahim-applied/opus-aaico-python

---

## Quick Start

```python
from opus_aaico import OpusClient

client = OpusClient(api_key="sk-...")

# Run a workflow end-to-end in one call
result = client.workflows.run(
    workflow_id="wf-123",
    payload={"query": {"value": "Analyze this", "type": "str"}},
)
print(result.status)   # "COMPLETED"
print(result.outputs)  # Workflow output variables
```

---

## Client Initialization

```python
from opus_aaico import OpusClient, AsyncOpusClient

# Synchronous client
client = OpusClient(
    api_key="sk-...",              # required (or set OPUS_API_KEY env var)
    workspace_id="ws-...",         # optional (or set OPUS_WORKSPACE_ID)
    base_url="https://operator.opus.com",  # optional (or set OPUS_BASE_URL)
    timeout=30.0,                  # request timeout in seconds
    max_retries=3,                 # retries on 429/5xx
    bearer_token=None,             # optional Bearer auth
)

# Asynchronous client
async_client = AsyncOpusClient(api_key="sk-...")

# Context manager support
with OpusClient(api_key="sk-...") as client:
    result = client.workflows.run(...)

async with AsyncOpusClient(api_key="sk-...") as client:
    result = await client.workflows.run(...)
```

Environment variables (read automatically if constructor params not provided):

| Variable | Purpose |
|----------|---------|
| `OPUS_API_KEY` | API key |
| `OPUS_WORKSPACE_ID` | Default workspace ID |
| `OPUS_BASE_URL` | Override base URL |

---

## Resource Namespaces

```python
client.workflows     # Workflow management + run() orchestrator
client.jobs          # Job lifecycle + polling
client.files         # File upload/download/search
```

---

## Workflows Resource

```python
# Get workflow details
workflow = client.workflows.get(workflow_id: str) -> Workflow

# List private workflows
workflows = client.workflows.list(
    query: str = None,
    industry: str = None,
    workspace_ids: list[str] = None,
    active: bool = None,
    has_jobs: bool = None,
    offset: int = 0,
    max_results: int = 25,
) -> PrivateWorkflowsResponse

# List public workflows
public = client.workflows.list_public(
    query: str = None,
    industry: str = None,
    country: str = None,
    source: str = None,        # "WKG" or "USER_GENERATED"
    offset: int = 0,
    max_results: int = 25,
) -> PublicWorkflowsResponse

# Get public workflow details
client.workflows.get_public(generation_id: str) -> Any

# Generate workflow from natural language
client.workflows.generate(
    user_query: str,
    user_id: str = None,
    make_private: bool = False,
    include_policies: bool = False,
    settings: dict = None,
) -> GenerateWorkflowResponse

# Feed an existing workflow
client.workflows.feed(workflow_id: str, private: bool = False) -> Any

# Feed external generation
client.workflows.feed_external(workflow_object, user_query: str, blueprint) -> Any

# Share workflow
client.workflows.share(workflow_id: str) -> Any

# Get shared workflow
client.workflows.get_shared(workflow_id: str) -> Any

# Send email on behalf of workflow
client.workflows.send_email(
    workflow_id: str,
    recipients: list[str],
    subject: str,
    body: str,
    attachments: list[dict] = None,
) -> None

# List available industries
client.workflows.list_industries() -> Any

# HIGH-LEVEL: Run workflow end-to-end
result = client.workflows.run(
    workflow_id: str,
    payload: dict,               # {var_name: {value: ..., type: "str"|"float"|...}}
    title: str = "SDK Job",
    description: str = "",
    poll_interval: float = 2.0,  # seconds between status checks
    timeout: float = 300.0,      # max wait in seconds
    on_status_change: callable = None,  # called on each status transition
) -> WorkflowRunResult
```

The `run()` method handles the full lifecycle: initiate job, execute with payload, poll until complete, fetch results and audit data.

---

## Jobs Resource

```python
# Initiate a job (creates shell, no execution)
job = client.jobs.initiate(
    workflow_id: str,
    title: str,                  # required
    description: str,            # required
    ref_user_id: str = None,
) -> JobInitiateResponse         # .job_execution_id

# Execute a job
client.jobs.execute(
    job_execution_id: str,
    payload: dict,               # {var_name: {value: ..., type: ...}}
    callback_url: str = None,    # webhook URL for status updates
) -> JobExecuteResponse

# Get job status
client.jobs.get_status(job_id: str) -> JobStatusResponse  # .status (JobStatus enum)

# Get job details
client.jobs.get(job_id: str) -> Any

# Get job results (only when COMPLETED)
client.jobs.get_results(job_id: str) -> JobResultsResponse

# Get job audit (per-node execution data)
client.jobs.get_audit(job_id: str) -> JobAudit

# Search jobs
client.jobs.search(
    workflow_id: str = None,
    workspace_ids: list[str] = None,
    status: list[str] = None,     # ["COMPLETED", "FAILED"]
    query: str = None,
    archive_status: str = None,   # "all", "archivedOnly", "nonArchivedOnly"
    start_date: str = None,       # ISO date
    end_date: str = None,
    offset: int = 0,
    max_results: int = 25,
) -> JobSearchResponse

# Archive / unarchive / duplicate / delete
client.jobs.archive(job_id: str) -> None
client.jobs.unarchive(job_id: str) -> None
client.jobs.duplicate(job_id: str) -> Any
client.jobs.delete(job_id: str) -> None

# Poll until terminal status
client.jobs.poll(
    job_execution_id: str,
    poll_interval: float = 2.0,
    timeout: float = 300.0,
    terminal_statuses: list[str] = ["COMPLETED", "FAILED", "CANCELLED"],
    on_status_change: callable = None,
) -> JobStatusResponse
```

### Payload Format

The OPUS API expects job payloads in this format:

```python
payload = {
    "variable_name": {"value": "actual value", "type": "str"},
    "amount": {"value": 1500, "type": "float"},
    "flag": {"value": True, "type": "bool"},
    "document": {"value": "https://files.opus.com/...", "type": "file"},
}
```

Valid types: `str`, `float`, `bool`, `date`, `file`, `array`, `array_files`, `object`.

---

## Files Resource

```python
# Upload a local file (handles presigned URL automatically)
file_url = client.files.upload(
    file_path: str,
    access_scope: str = "organization",
) -> str  # returns permanent file URL

# Upload from bytes
file_url = client.files.upload_bytes(
    data: bytes,
    file_extension: str,         # e.g. ".pdf"
    access_scope: str = "organization",
) -> str

# Get download URL
client.files.download(
    file_url: str,
    custom_expiry: int = None,   # seconds
) -> JobFileDownloadResponse     # .presigned_url, .content_type, .content_length

# Search files
client.files.search(
    query: str,                  # required
    media_type: str = None,      # "document" or "image"
    country: str = None,
    language: str = None,
    max_results: int = 25,
) -> FileSearchResponse

# Generate a file
client.files.generate(
    user_query: str,
    mime_type: str,
    file_extension: str,
    country: str = None,
) -> FileGenerateResponse

# Multipart upload for large files
file_url = client.files.multipart_upload(
    file_path: str,
    content_type: str,
    job_id: str = None,
    use_public_bucket: bool = False,
) -> str  # handles initiate + complete; aborts on failure

# Abort a multipart upload
client.files.multipart_abort(file_key: str, upload_id: str) -> AbortUploadResponse
```


---

## Error Handling

All errors inherit from `OpusError`:

```
OpusError (base)
    .message: str
    .status_code: int | None
    .body: Any | None
    .request_id: str | None

AuthenticationError          401 -- invalid API key
PermissionDeniedError        403 -- insufficient permissions
NotFoundError                404 -- resource not found
ValidationError              400 -- bad request parameters
RateLimitError               429 -- too many requests (.retry_after: float)
APIError                     5xx -- server error
TimeoutError                 request or polling timeout
ConnectionError              network failure
```

Usage:

```python
from opus_aaico import OpusClient, NotFoundError, RateLimitError, OpusError

try:
    result = client.workflows.run("wf-123", payload={...})
except NotFoundError:
    print("Workflow not found")
except RateLimitError as e:
    print(f"Rate limited, retry after {e.retry_after}s")
except OpusError as e:
    print(f"API error: {e.message} (status={e.status_code})")
```

The SDK automatically retries on 429 and 5xx with exponential backoff (1s, 2s, 4s, up to 30s). It does not retry on 400, 401, 403, 404.

---

## Enums

```python
from opus_aaico.types import JobStatus, MediaType, ArchiveStatus, WorkflowSource

# Job statuses
JobStatus.PENDING | IN_PROGRESS | COMPLETED | FAILED | WAITING | CANCELLED | UNKNOWN

# Media types
MediaType.IMAGE | DOCUMENT

# Archive status
ArchiveStatus.ALL | ARCHIVED_ONLY | NON_ARCHIVED_ONLY

# Workflow source
WorkflowSource.WKG | USER_GENERATED
```

---

## Key Pydantic Models

All models auto-convert between camelCase (API) and snake_case (Python).

### Workflow

```python
Workflow:
    workflow_id: str
    name: str
    description: str
    industry: str
    active: bool
    workflow_blueprint: WorkflowBlueprint
    execution_estimation: ExecutionEstimation
    job_payload_schema: dict[str, PayloadVariable]   # expected inputs
    job_results_payload_schema: dict
    created_at: str
```

### WorkflowRunResult

```python
WorkflowRunResult:
    status: str              # "COMPLETED", "FAILED", etc.
    job_id: str              # job execution ID
    outputs: dict | None     # workflow output variables
    execution_time: float    # seconds elapsed
    audit: JobAudit | None   # per-node execution data
```

### JobAudit

```python
JobAudit:
    nb_nodes: int
    nb_executed_nodes: int
    nb_failed_nodes: int
    executed_nodes: list[str]
    failed_nodes: list[str]
    remaining_nodes_to_execute: list[str]
    running_node: str | None
    nodes_execution_data: dict[str, NodeExecutionData]

NodeExecutionData:
    execution_status: str
    execution_time: int       # milliseconds
    execution_start_time: int
    execution_index: int
```

### PayloadVariable

```python
PayloadVariable:
    id: str
    variable_name: str
    display_name: str
    type: str                 # "str", "float", "bool", "date", "file", "array", "object"
    is_nullable: bool
```

### Other Key Models

```python
JobSearchItem:        title, description, job_execution_id, workflow_id, status, created_at
JobSearchResponse:    total_count, jobs: list[JobSearchItem]
FileMetadata:         file_id, file_uri, file_name, file_mime_type, file_extension
FileSearchResponse:   total_count, files: list[FileMetadata]
```

---

## Common Patterns

### Run workflow with file upload

```python
client = OpusClient(api_key="sk-...")
file_url = client.files.upload("./document.pdf")
result = client.workflows.run(
    workflow_id="wf-123",
    payload={
        "document": {"value": file_url, "type": "file"},
        "instructions": {"value": "Extract key findings", "type": "str"},
    },
)
```

### Poll with status callback

```python
def on_change(status):
    print(f"Status changed to: {status}")

result = client.workflows.run(
    workflow_id="wf-123",
    payload={...},
    on_status_change=on_change,
    poll_interval=3.0,
    timeout=600,
)
```

### Concurrent async execution

```python
import asyncio
from opus_aaico import AsyncOpusClient

async def run_batch():
    async with AsyncOpusClient(api_key="sk-...") as client:
        tasks = [
            client.workflows.run("wf-123", payload={"query": {"value": item, "type": "str"}})
            for item in ["doc1", "doc2", "doc3"]
        ]
        return await asyncio.gather(*tasks)
```

### Inspect workflow before running

```python
workflow = client.workflows.get("wf-123")
print(f"Name: {workflow.name}")
print(f"Active: {workflow.active}")
for var_name, var in (workflow.job_payload_schema or {}).items():
    print(f"  Input: {var.display_name} ({var.type}, nullable={var.is_nullable})")
```

### Search completed jobs

```python
jobs = client.jobs.search(
    workflow_id="wf-123",
    status=["COMPLETED"],
    start_date="2026-01-01",
    end_date="2026-03-31",
    max_results=100,
)
for job in jobs.jobs:
    print(f"{job.title}: {job.status} at {job.created_at}")
```

---

## Architecture Notes

- **HTTP layer:** `_client.py` contains `SyncHTTPClient` and `AsyncHTTPClient`, both extending `BaseClient`. Handles auth headers (`x-service-key`, `x-workspace-id`), retry logic, and error mapping.
- **Resources:** Each resource in `resources/` has both `Sync*` and `Async*` classes extending `SyncResource`/`AsyncResource`. Methods call `self._client.request(method, path, ...)`.
- **Types:** All in `types/` as Pydantic v2 models. Use `Field(alias="camelCase")` for API field mapping. Import from `opus_aaico.types`.
- **Polling:** `_utils/polling.py` provides `poll_sync`/`poll_async` with configurable interval, timeout, and status change callback.
- **File uploads:** `_utils/files.py` handles the presigned URL upload flow (get URL from API, PUT file to S3).
