Metadata-Version: 2.4
Name: workweek-switch
Version: 0.2.0
Summary: WorkWeek Switch SDK — implement the switch contract for external agent teams
License: MIT
Requires-Python: >=3.10
Description-Content-Type: text/markdown
Requires-Dist: fastapi>=0.100.0
Requires-Dist: uvicorn>=0.27.0
Requires-Dist: pydantic>=2.0.0
Requires-Dist: httpx>=0.27.0
Provides-Extra: dev
Requires-Dist: pytest; extra == "dev"
Requires-Dist: httpx; extra == "dev"
Requires-Dist: pytest-asyncio; extra == "dev"

# workweek-switch

Implement the WorkWeek switch contract for external agent teams.

The switch SDK lets your app expose capabilities that the WorkWeek platform can discover and invoke. Register handlers with decorators, self-register on startup, and the platform router handles the rest.

## Install

```bash
pip install workweek-switch
```

## Quick Start

### Provider App (exposes capabilities)

```python
from workweek_switch import WorkWeekSwitch

switch = WorkWeekSwitch(
    team_id="your-team-uuid",
    team_name="Best Apples Daily",
    gateway_url="https://gw.askvai.com",
    api_key="wk_rpt_your_key_here",
)

@switch.capability(
    "apple_recommendations",
    description="Daily recommendations for the best apples to buy",
    agent_name="Produce Analyst",
    agent_role="Apple Quality Specialist",
)
async def recommend_apples(message: str, context: dict) -> str:
    return "Honeycrisp from Washington state — peak season, $2.49/lb."

@switch.capability(
    "seasonal_analysis",
    description="Seasonal apple availability and pricing trends",
    agent_name="Market Analyst",
    agent_role="Seasonal Pricing Specialist",
)
async def seasonal(message: str, context: dict) -> str:
    return "Fuji apples peak Oct-Dec, prices drop 15% from summer highs."

app = switch.as_fastapi_app()
```

### Consumer App (no capabilities, uses platform agents)

```python
from workweek_switch import WorkWeekSwitch

switch = WorkWeekSwitch(
    team_id="your-team-uuid",
    team_name="My Consumer App",
    gateway_url="https://gw.askvai.com",
    api_key="wk_rpt_your_key_here",
)

# No @switch.capability() decorators — pure consumer
# Mount in your existing FastAPI app:
app.include_router(switch.as_fastapi_router())
```

### Self-Registration (v0.2.0+)

Register your app with the platform on startup. Idempotent — safe to call every time your app starts.

```python
from contextlib import asynccontextmanager
from fastapi import FastAPI
from workweek_switch import WorkWeekSwitch

switch = WorkWeekSwitch(
    team_id="",  # assigned by platform on first registration
    team_name="My App",
    gateway_url="https://gw.askvai.com",
    api_key="wk_rpt_your_key_here",
)

@switch.capability("my_feature", description="Does something useful")
async def my_feature(message: str, context: dict) -> str:
    return "result"

@asynccontextmanager
async def lifespan(app: FastAPI):
    # Register on startup (non-fatal if platform unreachable)
    try:
        result = await switch.register(switch_url="https://myapp.com")
        print(f"Registered: app_id={result.app_id} team_id={result.team_id}")
        if result.api_key:
            print(f"API Key (save this): {result.api_key}")
    except Exception as e:
        print(f"Registration failed (non-fatal): {e}")
    yield

app = FastAPI(lifespan=lifespan)
app.include_router(switch.as_fastapi_router())
```

## How It Works

### Switch Contract

The SDK exposes three endpoints that the WorkWeek platform expects:

| Endpoint | Method | Purpose |
|----------|--------|---------|
| `/switch/manifest` | GET | Returns registered capabilities, version hash, health status |
| `/dispatch` | POST | Routes a request to the matching capability handler |
| `/health` | GET | Liveness check |

### Platform Integration

```
Your App (SDK)                    WorkWeek Platform
    |                                    |
    |-- startup: register() ----------->| Creates App + Team + capabilities
    |                                    |
    |<--- hourly poll: GET /manifest ---|  Router syncs routing tables
    |                                    |
    |<--- invoke: POST /dispatch -------|  User invokes your capability
    |--- response --------------------- >|
```

1. **Register**: On startup, your app calls `register()` to create its App + Team in the platform
2. **Manifest polling**: The platform router polls `/switch/manifest` hourly to detect capability changes
3. **Dispatch**: When a user invokes one of your capabilities, the platform sends a POST to `/dispatch`

### Capability Lifecycle

- **Add a capability**: Add a `@switch.capability()` decorator, deploy. Router picks it up within 1 hour (or immediately on `register()`).
- **Remove a capability**: Remove the decorator, deploy. Router drains the capability on next poll — existing requests complete, new ones route elsewhere.
- **Update a capability**: Change the handler function. No SDK/platform changes needed — dispatch always calls the current handler.

### Version-Based Polling

The manifest includes a SHA-256 hash of registered capability names. The router skips processing when the version hasn't changed, keeping sync lightweight even at hundreds of tenants.

## API Reference

### WorkWeekSwitch

```python
WorkWeekSwitch(
    team_id: str,              # Team UUID (assigned by platform on first register)
    team_name: str,            # Human-readable name shown in marketplace
    webhook_secret: str = None,# HMAC secret for dispatch signature verification
    gateway_url: str = None,   # Platform gateway URL (required for register())
    api_key: str = None,       # API key (required for register())
)
```

### @switch.capability()

```python
@switch.capability(
    name: str,                 # Capability name (must be unique within team)
    description: str = "",     # Shown in marketplace and manifest
    is_primary: bool = True,   # Primary handler for this capability
    agent_name: str = None,    # Display name (defaults to function name)
    agent_role: str = None,    # Role description (e.g., "Weather Analyst")
)
async def handler(message: str, context: dict) -> str:
    ...
```

Handlers can be async or sync. Sync handlers run in a thread pool automatically.

### switch.register()

```python
result = await switch.register(
    switch_url: str,           # Externally-reachable URL where manifest/dispatch live
) -> RegisterResult
```

Returns:
- `app_id: int` — Platform app ID
- `team_id: str` — Platform team UUID
- `team_name: str` — Team display name
- `api_key: str | None` — Full API key (first registration only, save it)
- `api_key_prefix: str | None` — Key prefix for identification
- `capabilities_registered: list[str]` — Capabilities the platform registered
- `already_existed: bool` — True if app was already registered

### switch.build_manifest()

Returns the manifest dict that `/switch/manifest` serves. Useful for testing.

### switch.as_fastapi_app()

Returns a standalone FastAPI application with the switch contract endpoints.

### switch.as_fastapi_router()

Returns a FastAPI `APIRouter` for embedding switch endpoints in an existing app.

## HMAC Signature Verification

For production deployments, enable webhook signature verification:

```python
switch = WorkWeekSwitch(
    team_id="...",
    team_name="...",
    webhook_secret="wk_secret_your_secret_here",
)
```

The platform signs dispatch request bodies with HMAC-SHA256. The SDK verifies the `X-Webhook-Signature` header automatically and returns 401 if invalid.

## Changelog

### v0.2.0
- Added `gateway_url` and `api_key` constructor parameters
- Added `register(switch_url)` method for platform self-registration
- Added `RegisterResult` model
- Added `httpx` as a dependency (used for registration HTTP calls)

### v0.1.0
- Initial release
- Capability registration via `@switch.capability()` decorator
- Manifest serving, dispatch routing, health check
- HMAC signature verification
- FastAPI app and router builders
