Metadata-Version: 2.4
Name: dooray-sdk
Version: 0.0.1a3
Summary: Python SDK for Dooray.com with Socket Mode support
Author-email: Dooray SDK Team <dooray-sdk@nhndooray.com>
License: MIT
Classifier: Development Status :: 3 - Alpha
Classifier: Intended Audience :: Developers
Classifier: License :: OSI Approved :: MIT License
Classifier: Operating System :: OS Independent
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.8
Classifier: Programming Language :: Python :: 3.9
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Requires-Python: >=3.8
Description-Content-Type: text/markdown
License-File: LICENSE
Requires-Dist: requests>=2.25.0
Requires-Dist: websocket-client>=1.0.0
Requires-Dist: python-dotenv>=1.0.0
Provides-Extra: aiohttp
Requires-Dist: aiohttp>=3.8.0; extra == "aiohttp"
Provides-Extra: websockets
Requires-Dist: websockets>=10.0; extra == "websockets"
Provides-Extra: templates
Requires-Dist: cookiecutter>=2.0.0; extra == "templates"
Provides-Extra: llm
Requires-Dist: langchain-core>=0.3.0; extra == "llm"
Requires-Dist: langchain-openai>=0.3.0; extra == "llm"
Requires-Dist: pydantic>=2.0.0; extra == "llm"
Provides-Extra: all
Requires-Dist: aiohttp>=3.8.0; extra == "all"
Requires-Dist: cookiecutter>=2.0.0; extra == "all"
Requires-Dist: langchain-core>=0.3.0; extra == "all"
Requires-Dist: langchain-openai>=0.3.0; extra == "all"
Requires-Dist: pydantic>=2.0.0; extra == "all"
Dynamic: license-file

# Dooray SDK - Python

A comprehensive Python SDK for building agents and bots on Dooray.com. This document covers the core classes, decorators, and usage patterns.

---

## Installation

### Install via pip

```bash
# Currently in alpha (requires --pre flag)
pip install --pre dooray-sdk

# Or specify version explicitly
pip install dooray-sdk==0.0.1a2
```

### Install with aiohttp (Recommended)

```bash
pip install --pre "dooray-sdk[aiohttp]"

# Or specify version explicitly
pip install "dooray-sdk[aiohttp]==0.0.1a2"
```

### Using Virtual Environment (Recommended)

```bash
# Create virtual environment
python3 -m venv .venv

# Activate (Linux/macOS)
source .venv/bin/activate

# Activate (Windows)
.venv\Scripts\activate

# Install
pip install --pre "dooray-sdk[aiohttp]"
```

### Dependencies

| Package | Version | Description |
| :------ | :------ | :---------- |
| `aiohttp` | >= 3.8.0 | Async HTTP/WebSocket client |
| `requests` | >= 2.28.0 | Sync HTTP client |

## Quick Start (Templates)

Use the CLI tool to quickly bootstrap your project.

### Install with Templates

```bash
pip install --pre "dooray-sdk[templates]"
```

### Usage

```bash
# List available templates
dooray-sdk list

# Create an echo bot project
dooray-sdk init echo-bot

# Create with default values (no prompts)
dooray-sdk init echo-bot --no-input

# Create in a specific directory
dooray-sdk init echo-bot -o ./projects
```

### Available Templates

| Template | Description |
| :------- | :---------- |
| `echo-bot` | A simple bot that echoes messages back |

## Environment Variables

The SDK uses the following environment variables:

| Variable | Required | Description | Example |
| :------- | :------- | :---------- | :------ |
| `DOORAY_AGENT_TOKEN` | Yes | Agent authentication token | `your_agent_token` |
| `DOORAY_DOMAIN` | Yes | Dooray domain | `company.dooray.com` |

## Classes

### `Agent`

The core class for building Dooray agents. Supports message handling, auto-replies, and event processing.

```python
class Agent:
    def __init__(
        self,
        token: str = None,
        domain: str = None,
        services: List[str] = None,
        **kwargs
    )
```

#### Constructor Parameters

| Parameter | Type | Default | Description |
| :-------- | :--- | :------ | :---------- |
| `token` | `str \| None` | `None` | Agent token (falls back to `DOORAY_AGENT_TOKEN` env var) |
| `domain` | `str \| None` | `None` | Dooray domain (falls back to `DOORAY_DOMAIN` env var) |
| `services` | `List[str] \| None` | `["messenger"]` | List of services to connect |

#### Properties

| Property | Type | Description |
| :------- | :--- | :---------- |
| `token` | `str` | Agent authentication token |
| `domain` | `str` | Dooray domain |
| `services` | `List[str]` | Connected service list |
| `base_url` | `str` | API base URL |
| `is_connected` | `bool` | Connection status |

#### Methods

| Method | Description |
| :----- | :---------- |
| `run()` | Start the agent (blocking) |
| `on(event_type, action, service)` | Event handler decorator |
| `add_handler(event_type, handler, action, service)` | Register handler (non-decorator) |
| `reply_to(trigger, response)` | Simple response mapping |
| `send_message(channel, text)` | Send a message |

#### Example - Basic Usage

```python
from dooray_sdk import Agent

agent = Agent()

@agent.messenger
def handle_message(req):
    """Handle messenger messages."""
    if req.text:
        req.reply(f"Received: {req.text}")

agent.run()
```

#### Example - Multi-Service

```python
from dooray_sdk import Agent

agent = Agent(services=["messenger", "task", "wiki"])

@agent.messenger
def handle_messenger(req):
    req.reply("This is a messenger message")

@agent.task
def handle_task(req):
    print(f"Task event: {req.action}")

@agent.wiki
def handle_wiki(req):
    print(f"Wiki event: {req.action}")

agent.run()
```

### `SocketModeRequest`

Represents an incoming request received via WebSocket.

```python
@dataclass
class SocketModeRequest:
    envelope_id: str
    type: str
    payload: Dict[str, Any]
    service: str = ""
    action: str = ""
    entity: EntityWrapper = None
    actor: ActorWrapper = None
    action_data: ActionWrapper = None
```

#### Properties

| Property | Type | Description |
| :------- | :--- | :---------- |
| `envelope_id` | `str` | Unique message ID |
| `type` | `str` | Event type (`message`, `task`, `page`, etc.) |
| `payload` | `Dict[str, Any]` | Raw payload data |
| `service` | `str` | Service name (`messenger`, `task`, `wiki`) |
| `action` | `str` | Action type (`create`, `update`, `delete`) |
| `entity` | `EntityWrapper` | Entity information wrapper |
| `actor` | `ActorWrapper` | Actor information wrapper |
| `action_data` | `ActionWrapper` | Action data wrapper |

#### Convenience Properties

| Property | Type | Description |
| :------- | :--- | :---------- |
| `is_message` | `bool` | Whether this is a message type |
| `text` | `str \| None` | Message text |
| `channel` | `str \| None` | Channel ID |
| `data` | `Dict[str, Any]` | Raw data (alias for payload) |

#### Methods

| Method | Description |
| :----- | :---------- |
| `reply(text)` | Send sync/async response |
| `is_type(types)` | Check type |
| `is_action(actions)` | Check action |
| `is_service(services)` | Check service |
| `to_dict()` | Convert to dictionary |
| `from_dict(data, default_service)` | Create from dictionary (class method) |

#### Example - Request Handling

```python
@agent.messenger
def handle(req):
    # Check message type
    if req.is_message:
        print(f"Text: {req.text}")
        print(f"Channel: {req.channel}")

    # Check action
    if req.is_action("create"):
        print("This is a new message")

    # Check service
    if req.is_service("messenger"):
        req.reply("Responding from messenger")
```

### `WebClient`

Synchronous REST API client.

```python
class WebClient:
    def __init__(
        self,
        token: str,
        base_url: str = "https://api.dooray.com/",
        timeout: int = 30
    )
```

#### Methods

| Method | Return Type | Description |
| :----- | :---------- | :---------- |
| `send_message(channel, text, **kwargs)` | `Dict` | Send a message |
| `get_member(member_id)` | `Dict` | Get organization member info |

### `AsyncWebClient`

Asynchronous REST API client.

```python
class AsyncWebClient:
    def __init__(
        self,
        token: str,
        base_url: str = "https://api.dooray.com/"
    )
```

#### Methods

| Method | Return Type | Description |
| :----- | :---------- | :---------- |
| `send_message(channel, text, **kwargs)` | `Dict` | Send a message |
| `get_member(member_id)` | `Dict` | Get organization member info |

## Decorators

### Service Decorators

Register event handlers for each service.

```python
@agent.messenger   # All messenger service events
@agent.task        # All task service events
@agent.wiki        # All wiki service events
```

### `@agent.on()` Decorator

Unified event handler decorator.

```python
def on(
    event_type: Union[str, List[str]] = "all",
    action: Union[str, List[str]] = None,
    service: Union[str, List[str]] = None
) -> Callable
```

#### Parameters

| Parameter | Type | Default | Description |
| :-------- | :--- | :------ | :---------- |
| `event_type` | `str \| List[str]` | `"all"` | Event type filter |
| `action` | `str \| List[str] \| None` | `None` | Action filter |
| `service` | `str \| List[str] \| None` | `None` | Service filter |

#### Examples

```python
# Handle only message events
@agent.on("message")
def handle_message(req):
    pass

# Handle specific action
@agent.on("message", action="create")
def handle_new_message(req):
    pass

# Handle multiple types
@agent.on(["task", "page"])
def handle_task_or_page(req):
    pass

# Combine service and action
@agent.on(service="wiki", action=["create", "update"])
def handle_wiki_changes(req):
    pass
```

## Request Data Structure

The SDK provides wrapper types that enable dot notation access to dictionary data.

### Type Structure Overview

```
SocketModeRequest
├── entity: EntityWrapper      # Entity info (task, page, etc.)
│   ├── type: str              # Entity type
│   └── data: DataWrapper      # Entity data
├── actor: ActorWrapper        # Action performer info
│   ├── type: str              # Actor type
│   └── data: DataWrapper      # Actor data
└── action_data: ActionWrapper # Action details
    ├── type: str              # Action type
    └── data: DataWrapper      # Action data
```

### `DataWrapper`

The base class for all wrapper types. Enables dot notation access to dictionary data.

```python
class DataWrapper:
    def __getattr__(self, name: str) -> Any   # Dot notation access
    def __getitem__(self, key: str) -> Any    # Dictionary access
    def get(self, key: str, default=None)     # Safe access
    def to_dict(self) -> Dict[str, Any]       # Return original dictionary
```

**Usage:**

```python
# Dot notation access
data.id
data.subject
data.nested.value

# Dictionary access
data["id"]
data.get("subject", "default")

# Get original dictionary
data.to_dict()
```

### `EntityWrapper`

Wraps entity information (tasks, pages, etc.).

| Property | Type | Description |
| :------- | :--- | :---------- |
| `type` | `str` | Entity type (e.g., `"task"`, `"page"`) |
| `data` | `DataWrapper` | Entity detail data |

**Usage:**

```python
@agent.on("task")
def handle_task(req):
    print(req.entity.type)           # "task"
    print(req.entity.data.id)        # Task ID
    print(req.entity.data.subject)   # Task subject
```

#### Message Data

For messenger messages, `entity.type` is `"message"`.

| Property | Type | Description |
| :------- | :--- | :---------- |
| `id` | `str` | Message ID |
| `channelId` | `str` | Channel ID |
| `senderId` | `str` | Sender member ID |
| `text` | `str` | Message text |
| `sentAt` | `int` | Sent time (timestamp ms) |
| `seq` | `int` | Message sequence number |
| `directMemberId` | `str` | DM recipient member ID |
| `parentChannelId` | `str` | Parent channel ID (threads) |

```python
@agent.messenger
def handle(req):
    print(req.entity.message.text)
    print(req.entity.message.channelId)
    print(req.entity.message.senderId)
    print(req.entity.message.sentAt)
```

### `ActorWrapper`

Wraps information about the user who performed the action.

| Property | Type | Description |
| :------- | :--- | :---------- |
| `type` | `str` | Actor type (e.g., `"organizationMember"`) |
| `data` | `DataWrapper` | Actor detail data |
| `organizationMember` | `LazyMemberData \| None` | Organization member info (supports lazy loading) |

**Usage:**

```python
@agent.on("message")
def handle(req):
    if req.actor:
        print(req.actor.type)         # "organizationMember"

        # Access via organizationMember (recommended)
        member = req.actor.organizationMember
        print(member.id)              # Member ID (local data)
        print(member.name)            # Member name (lazy loading)
```

#### organizationMember Fields

| Property | Type | Description |
| :------- | :--- | :---------- |
| `id` | `str` | Member ID |
| `name` | `str` | Member name |
| `externalEmailAddress` | `str` | Email address |
| `nickname` | `str` | Nickname |
| `englishName` | `str` | English name |
| `nativeName` | `str` | Native name |
| `userCode` | `str` | User code |
| `locale` | `str` | Locale |
| `timezoneName` | `str` | Timezone |

#### Lazy Loading

`organizationMember` returns data included in the WebSocket message immediately, and fetches missing fields via API when accessed.

```python
@agent.messenger
async def handle(req):
    member = req.actor.organizationMember

    # Local data (no API call)
    member_id = member.id

    # Async access - API call when needed
    name = await member.name
    email = await member.externalEmailAddress

    # Sync access (print, comparison, etc.)
    print(member.name)
    if member.name == "John Doe":
        await req.reply("Hello!")

    # Get full data
    full_data = await member
```

### `ActionWrapper`

Wraps action detail information.

| Property | Type | Description |
| :------- | :--- | :---------- |
| `type` | `str` | Action type (e.g., `"create"`, `"update"`) |
| `data` | `DataWrapper` | Action detail data |

## Usage Examples

### Echo Agent

```python
from dooray_sdk import Agent

agent = Agent()

@agent.messenger
def echo(req):
    if req.text:
        req.reply(f"Echo: {req.text}")

agent.run()
```

### Auto-Reply Agent

```python
from dooray_sdk import Agent

agent = Agent()

# Simple mapping
agent.reply_to("ping", "pong")
agent.reply_to("hello", "Hello there!")

# Custom handler
@agent.messenger
def handle(req):
    text = req.text or ""
    if text.startswith("help"):
        req.reply("Available commands: ping, hello, help")

agent.run()
```

### Multi-Service Agent

```python
from dooray_sdk import Agent

agent = Agent(services=["messenger", "task", "wiki"])

@agent.messenger
def on_message(req):
    print(f"[Messenger] {req.text}")
    req.reply("Message received!")

@agent.task
def on_task(req):
    print(f"[Task] {req.action}: {req.entity.data.subject}")

@agent.wiki
def on_wiki(req):
    print(f"[Wiki] {req.action}: {req.entity.data.title}")

@agent.on("all")
def on_any(req):
    print(f"[All] {req.service}/{req.type}/{req.action}")

agent.run()
```

## Error Handling

### Common Exceptions

| Exception | Cause | Solution |
| :-------- | :---- | :------- |
| `ValueError` | Environment variables not set | Set `DOORAY_AGENT_TOKEN` and `DOORAY_DOMAIN` |
| `ConnectionError` | WebSocket connection failed | Check network and token |
| `RuntimeError` | Calling reply before agent starts | Use after `agent.run()` |

### Exception Handling Example

```python
from dooray_sdk import Agent

try:
    agent = Agent()
except ValueError as e:
    print(f"Configuration error: {e}")
    print("Please set DOORAY_AGENT_TOKEN and DOORAY_DOMAIN")
    exit(1)

@agent.messenger
def handle(req):
    try:
        # Business logic
        process_message(req)
    except Exception as e:
        # Log error (avoid exposing errors to users)
        logger.error(f"Processing error: {e}")

agent.run()
```

## License

MIT License
