# Perceptic Workflow SDK

Python SDK for Perceptic Workflow definitions. This package provides cross-language Temporal workflow types that are compatible with the Java workflow definitions in perceptic-core-client.

## Installation

```bash
uv add perceptic-workflow-sdk
```

## Usage

### Runtime Input Contract

Workflows started through Perceptic Core receive a typed envelope:

- `AgentInput[T]` where:
  - `run_rid` identifies the run
  - `user_id` identifies the authenticated caller
  - `payload` contains your workflow-specific input model `T`

On the wire, Perceptic Core sends camelCase (`runRid`, `userId`, `payload`). In Python, use snake_case (`run_rid`, `user_id`, `payload`) per PEP 8. The SDK handles alias mapping automatically.

### Workflow Events

```python
from perceptic_workflow import (
    WorkflowEvent,
    InfoEvent,
    CheckpointEvent,
    UserInputEvent,
    UserInputRequestEvent,
    WorkflowCheckpointStatus,
)

# Create events
info_event = InfoEvent(
    event_id=1,
    type="progress",
    payload={"step": "processing"},
    timestamp=datetime.now()
)
```

### Implementing Workflows

The SDK provides a mixin class for implementing Perceptic-compatible workflows:

```python
from pydantic import BaseModel
from temporalio import workflow
from perceptic_workflow import AgentInput, PercepticWorkflow, WorkflowEvent


class ResearchInput(BaseModel):
    query: str
    context: str

@workflow.defn
class MyWorkflow(PercepticWorkflow):
    def __init__(self):
        self._events: list[WorkflowEvent] = []
        self._paused = False

    @workflow.run
    async def run(self, input: AgentInput[ResearchInput]) -> dict:
        # Envelope fields use snake_case in Python.
        run_rid = input.run_rid
        user_id = input.user_id
        request = input.payload

        if request is None:
            raise ValueError("payload is required")

        return {
            "runRid": run_rid,
            "userId": user_id,
            "query": request.query,
            "context": request.context,
        }

    @workflow.update(name=PercepticWorkflow.UPDATE_SUBMIT_USER_INPUT)
    async def submit_user_input(self, inputs: dict) -> None:
        self._paused = False
        # Handle user input

    @workflow.update(name=PercepticWorkflow.UPDATE_INTERRUPT)
    async def interrupt(self, reason: str) -> None:
        # Handle interruption
        pass

    @workflow.query(name=PercepticWorkflow.QUERY_IS_PAUSED)
    def is_paused(self) -> bool:
        return self._paused

    @workflow.query(name=PercepticWorkflow.QUERY_GET_EVENTS)
    def get_events(self, after_event_id: int | None = None) -> list[WorkflowEvent]:
        if after_event_id is None:
            return self._events
        return [e for e in self._events if e.event_id > after_event_id]
```

### Decorator Validation

`PercepticWorkflow` no longer performs runtime validation of Temporal handler
decorators during class creation. This avoids import-time failures for abstract
workflow bases and mixin-based inheritance patterns.

Handler contract enforcement is expected to happen through linting and type-check
tooling in your development pipeline:

```bash
uv run ty check
uv run workflow-typecheck
```

### Troubleshooting

- `ValidationError: Field required ... run_rid/user_id` during workflow activation usually means your `@workflow.run` signature expects the wrong shape.
- Use `AgentInput[YourPayloadModel]` for the first workflow argument, not a flattened domain model.
- For run-start requests, send `runRid` and `inputs`; `userId` is derived from auth context by Perceptic Core.

## Compatibility

This package is designed to be compatible with:

- perceptic-core-client (Java)
- perceptic-core-workflow-runtime (Java)

The workflow definitions and event types are generated from the same JSON Schema sources to ensure cross-language compatibility.
