# Tool (함수 호출) (/docs/sdk/python/agent/tools)

## 기본 사용법

```python
@agent.tool
async def check_order(order_id: str) -> str:
    """주문 상태를 확인합니다.

    고객이 주문 번호를 말하면 이 함수를 호출하세요.
    """
    order = await db.get_order(order_id)
    return f"주문 {order_id}는 {order.status} 상태입니다."
```

## 작성 규칙

- 함수는 반드시 `async`여야 합니다
- 반환 타입은 `str`이어야 합니다
- **docstring이 AI에게 함수 설명으로 전달됩니다** — 상세하게 작성하세요
- 파라미터 타입 힌트(`str`, `int`, `float`, `bool`)가 자동으로 JSON Schema로 변환됩니다
- 기본값이 있는 파라미터는 optional로 처리됩니다

```python
@agent.tool
async def search_products(
    query: str,            # required
    category: str = "",    # optional
    limit: int = 10,       # optional
) -> str:
    """상품을 검색합니다. 고객이 상품을 찾을 때 사용하세요."""
    results = await product_api.search(query, category, limit)
    return "\n".join(f"- {r.name}: {r.price}원" for r in results)
```

---

## Tool Config (도구 실행 설정)

`tool_config`로 도구 실행 시 동작을 설정합니다.

```python
from clawops.agent import ClawOpsAgent, ToolConfig

agent = ClawOpsAgent(
    from_="07012341234",
    session=session,
    tool_config=ToolConfig(
        hold_audio=True,
    ),
)
```

### Hold Audio (대기 음악)

AI가 도구를 실행하는 동안 고객에게 대기 음악을 재생합니다. 외부 API 호출 등 시간이 걸리는 도구 실행 중 무음을 방지합니다.

| 값 | 설명 |
| :--- | :--- |
| `True` | 기본 차임 멜로디 재생 (~10초 루프) |
| `"./hold_music.wav"` | WAV 파일 경로 (16-bit PCM, 자동 리샘플링/모노 변환) |
| `b"..."` | raw µ-law 오디오 데이터 |
| `False` | 비활성화 **(기본값)** |

```python
# 기본 차임 사용
agent = ClawOpsAgent(
    from_="07012341234",
    session=session,
    tool_config=ToolConfig(hold_audio=True),
)

# 커스텀 WAV 파일
agent = ClawOpsAgent(
    from_="07012341234",
    session=session,
    tool_config=ToolConfig(hold_audio="./hold_music.wav"),
)
```

---

## 내장 Tool (Built-in Tools)

Agent는 통화 제어를 위한 내장 도구를 기본 제공합니다. `BuiltinTool`을 사용해 어떤 내장 도구를 활성화할지 제어할 수 있습니다.

### 내장 도구 목록

| 도구            | 상수                        | 설명                                                                                                                    |
| :-------------- | :-------------------------- | :---------------------------------------------------------------------------------------------------------------------- |
| `hang_up`       | `BuiltinTool.HANG_UP`       | 전화를 종료합니다. AI가 대화 완료를 판단하면 자동으로 호출합니다.                                                       |
| `collect_dtmf`  | `BuiltinTool.COLLECT_DTMF`  | 사용자의 키패드(DTMF) 입력을 수집합니다. 본인 인증, 메뉴 선택 등에 사용합니다.                                          |
| `send_dtmf`     | `BuiltinTool.SEND_DTMF`     | DTMF 신호를 전송합니다. ARS 메뉴 탐색, 내선번호 입력 등에 사용합니다.                                                   |
| `transfer_call` | `BuiltinTool.TRANSFER_CALL` | 통화를 다른 번호로 전환합니다. Blind(즉시 전환)와 Warm(안내 후 전환) 모드를 지원합니다. 고객센터 연결, 상담원 전환에 사용합니다. |

### 선택 상수

| 상수               | 설명                               |
| :----------------- | :--------------------------------- |
| `BuiltinTool.ALL`  | 모든 내장 도구 활성화 **(기본값)** |
| `BuiltinTool.NONE` | 모든 내장 도구 비활성화            |

### 사용법

```python
from clawops.agent import ClawOpsAgent, BuiltinTool

# 기본: 모든 내장 도구 활성화
agent = ClawOpsAgent(
    from_="07012341234",
    session=session,
)

# 명시적으로 전부 활성화 (위와 동일)
agent = ClawOpsAgent(
    from_="07012341234",
    session=session,
    builtin_tools=BuiltinTool.ALL,
)

# 내장 도구 전부 비활성화 (커스텀 도구만 사용)
agent = ClawOpsAgent(
    from_="07012341234",
    session=session,
    builtin_tools=BuiltinTool.NONE,
)

# hang_up만 사용 (DTMF 도구 제외)
agent = ClawOpsAgent(
    from_="07012341234",
    session=session,
    builtin_tools=[BuiltinTool.HANG_UP],
)

# DTMF만 사용, hang_up 제외 (AI가 전화를 끊지 못하게)
agent = ClawOpsAgent(
    from_="07012341234",
    session=session,
    builtin_tools=[BuiltinTool.COLLECT_DTMF, BuiltinTool.SEND_DTMF],
)
```

### 활용 예시

**ARS 아웃바운드 봇** — 발신 후 ARS를 탐색해야 하므로 `SEND_DTMF`만 필요:

```python
agent = ClawOpsAgent(
    from_="07012341234",
    session=session,
    builtin_tools=[BuiltinTool.HANG_UP, BuiltinTool.SEND_DTMF],
)
```

**고객 인증 봇** — 고객이 주민번호 뒤 자리를 키패드로 입력:

```python
agent = ClawOpsAgent(
    from_="07012341234",
    session=session,
    builtin_tools=[BuiltinTool.HANG_UP, BuiltinTool.COLLECT_DTMF],
)
```

**고객센터 봇** — AI가 1차 응대 후 상담원으로 전환:

```python
agent = ClawOpsAgent(
    from_="07012341234",
    session=session,
    builtin_tools=[BuiltinTool.HANG_UP, BuiltinTool.TRANSFER_CALL],
)
```

---

## Call Transfer (통화 전환)

`transfer_call` 내장 도구를 활성화하면 AI가 대화 중 다른 번호로 통화를 전환할 수 있습니다.
코드에서 직접 `call.transfer()`를 호출할 수도 있습니다.

### Blind Transfer (즉시 전환)

고객을 대상 번호로 바로 연결합니다. 전환이 시작되면 AI 세션은 종료됩니다.

```python
await call.transfer("01012345678")
```

### Warm Transfer (안내 후 전환)

대상이 전화를 받으면 whisper 메시지를 먼저 들려준 후 고객과 연결합니다.
고객은 whisper를 들을 수 없으며, 연결 대기 중에는 대기 음악이 재생됩니다.

```python
await call.transfer(
    "01012345678",
    mode="warm",
    whisper="VIP 고객님이십니다. 주문 번호는 A1234입니다.",
)
```

### 전환 후 AI 복귀

전환된 통화가 끝나면 AI가 다시 고객과 대화를 이어갑니다.
예를 들어 전문 상담원과 통화 후 AI가 후속 안내를 하는 시나리오에 적합합니다.

```python
await call.transfer(
    "01012345678",
    after_transfer="return",  # 기본값: "terminate" (AI 세션 종료)
)
```

### 연결되지 않으면 안내하고 종료

`after_transfer="terminate"`(기본값)에서 대상이 받지 않거나 통화 중이면, 고객은 지금까지
**아무 말 없이** 끊겼습니다. `failure_message` 를 주면 끊기 전에 그 문장을 고객에게 들려줍니다.

```python
await call.transfer(
    "01012345678",
    failure_message="죄송합니다. 담당자와 연결되지 않았습니다. 잠시 후 다시 걸어 주세요.",
)
```

재생이 끝나면 통화가 종료됩니다. 고객이 재생 도중에 끊으면 거기서 끝납니다 — 전환 결과
(`no-answer`·`busy` 등)는 안내와 무관하게 그대로 기록되고, 실패 이벤트는 **안내를 기다리지 않고**
먼저 나갑니다.

> `after_transfer="return"` 에서는 재생되지 않습니다. 그 모드는 AI가 통화를 이어받으므로
> 무엇을 말할지는 여러분 코드가 정합니다. 다만 이어받을 채널이 사라져 서버가 `terminate` 로
> 내려앉힌 통화에서는 재생됩니다 — 그게 고객이 무음에 남는 경우이기 때문입니다.

### SIP 엔드포인트 전환

전화번호(PSTN) 대신 SIP 엔드포인트로 통화를 직접 전환합니다.
PSTN 통신사를 거치지 않고 SIP URI(`sip:user@host`)로 INVITE 브릿지합니다 (녹음·관측 유지).

```python
await call.transfer(
    "sip:agent@sip.example.com",
    destination_type="sip",  # 기본값: "pstn" (전화번호 전환)
)
```

> **`sip_trunk` 부가서비스가 필요합니다.** 부가서비스가 없는 계정이 SIP 전환을 시도하면
> 전환은 실패하고(`{"status": "failed"}`) 통화는 끊기지 않은 채 AI가 계속 응대합니다.
> 인바운드 BYOC/softphone 라우팅과 동일한 게이트입니다.

### Context 전달

전환 대상에게 고객 정보 등 구조화 데이터를 webhook으로 전달할 수 있습니다.

```python
await call.transfer(
    "01012345678",
    mode="warm",
    whisper="VIP 고객입니다.",
    hold_media="moh",        # 전환 중 고객에게 ("moh" | "ringback" | "silence")
    context={
        "customer_name": "홍길동",
        "order_id": "ORD-20260325-001",
        "priority": "high",
    },
)
```

### 파라미터

| 파라미터 | 타입 | 기본값 | 설명 |
|:---------|:-----|:-------|:-----|
| `to` | `str` | (필수) | 전환 대상. `destination_type="pstn"`이면 전화번호, `"sip"`이면 SIP URI(`sip:user@host`) |
| `destination_type` | `str` | `"pstn"` | `"pstn"`: 통신사 경유 전화번호 전환, `"sip"`: SIP 엔드포인트 직접 전환 (**`sip_trunk` 부가서비스 필요**) |
| `mode` | `str` | `"blind"` | `"blind"`: 즉시 전환, `"warm"`: whisper 후 전환 |
| `after_transfer` | `str` | `"terminate"` | `"terminate"`: AI 세션 종료, `"return"`: 전환 통화 종료 후 AI가 다시 대화를 이어감 |
| `hold_media` | `str` | `"ringback"` | 전환 중 고객에게 재생할 대기 음원. `"moh"`: 대기 음악, `"ringback"`: 통화 연결음, `"silence"`: 무음 |
| `whisper` | `str` | `None` | Warm 모드에서 대상이 전화를 받았을 때 전달할 안내 메시지 (TTS). 고객에게는 들리지 않음 |
| `context` | `dict` | `None` | 전환 대상에게 webhook으로 전달할 구조화 데이터 (예: 고객 정보, 주문 번호 등) |
| `caller_id_mode` | `str` | `None` | 전환받는 쪽에 표시할 번호를 **의도**로 지정. `"account"`: 계정 번호(기본과 같음), `"original"`: 걸려온 통화의 발신자 번호 표시 |
| `caller_id` | `str` | `None` | 표시할 번호를 **직접** 지정. 허용 범위를 벗어나면 전환이 실패합니다 (아래 참고) |
| `timeout` | `int` | `30` | **대상이 전화를 받기까지** 기다리는 시간 (초). 초과 시 전환 실패 처리 |
| `failure_message` | `str` | `None` | 전환이 연결되지 않았을 때 **고객에게** 들려주고 끊을 문장 (TTS). 주지 않으면 아무 말 없이 종료 |
| `failure_voice` | `str` | `None` | `failure_message` 를 읽을 음성. `<Say voice>` 와 같은 표기(`cartesia:<음성 ID>`). 생략하면 기본 음성 |

> `timeout` 은 벨이 울리는 구간에만 적용됩니다. 대상이 받은 뒤의 통화가 얼마나 길어지든
> `transfer_call()` 은 전환이 끝날 때까지 기다렸다가 결과를 돌려줍니다 — 긴 상담 통화 때문에
> 호출이 타임아웃되지 않습니다.

### 전환받는 쪽에 표시되는 발신번호

**기본값은 계정이 보유한 번호입니다.** 인바운드 통화를 전환하면 그 통화를 받은 번호(예: 070)가
표시됩니다. 직원 전화에는 "우리 대표번호에서 걸려온 전화"로 보입니다.

전화를 건 고객의 번호가 보이게 하려면 `caller_id_mode="original"` 을 씁니다.

```python
await call.transfer("021234567", caller_id_mode="original")
```

바뀌는 것은 **받는 쪽에 표시되는 번호**뿐입니다. 통화 자체는 계정 번호에서 넘기는 전환이라,
받는 장비에 따라 계정 번호가 보일 수 있습니다.

`caller_id_mode` 와 `caller_id` 는 성격이 다릅니다.

| | 성격 | 표시·검증에 실패하면 |
|:--|:--|:--|
| `caller_id_mode="original"` | **선호** | 계정 번호로 내려앉고 **전환은 성사됩니다** |
| `caller_id="01012345678"` | **지시** | `UNOWNED_CALLER_ID` 로 **전환 자체가 실패합니다** |

걸려온 통화의 발신자 번호는 다음 경우에 표시할 수 없습니다. 이때 `caller_id_mode="original"` 은 조용히
계정 번호로 돌아가고, `caller_id` 로 같은 번호를 직접 넘겼다면 전환이 실패합니다.

- 통신사 직결로 들어온 인바운드가 아닌 경우 (착신전환·중계 경로로 들어온 통화)
- 발신번호가 국내 번호로 정규화되지 않는 경우 (국제전화, 표시 제한 등)
- 아웃바운드 통화 (걸려온 통화가 없어 무시됩니다)

`caller_id` 로 직접 지정할 수 있는 번호는 **계정이 보유한 번호**이거나 **그 통화의 발신자**
뿐입니다. 그 외 번호는 발신번호 변작 방지를 위해 거절됩니다. 둘 다 지정하면 `caller_id` 가
이기고 `caller_id_mode` 는 무시됩니다.

> VoiceML `<Dial>` 은 기본값이 다릅니다 — 통신사 직결 인바운드면 **걸려온 통화의 발신자 번호를 기본으로 표시**합니다.
> 같은 의도라도 표면에 따라 기본 동작이 다르니, 두 방식을 함께 쓴다면 명시적으로 지정하세요.

**단순 안내 봇** — 통화 종료만 가능하면 충분:

```python
agent = ClawOpsAgent(
    from_="07012341234",
    session=session,
    builtin_tools=[BuiltinTool.HANG_UP],
)
```
