dial-crewai 0.10.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- dial_crewai-0.10.0/.gitignore +5 -0
- dial_crewai-0.10.0/PKG-INFO +104 -0
- dial_crewai-0.10.0/README.md +95 -0
- dial_crewai-0.10.0/dial_crewai/__init__.py +27 -0
- dial_crewai-0.10.0/dial_crewai/_bridge.py +43 -0
- dial_crewai-0.10.0/dial_crewai/tools.py +466 -0
- dial_crewai-0.10.0/examples/signup_crew.py +59 -0
- dial_crewai-0.10.0/pyproject.toml +25 -0
- dial_crewai-0.10.0/tests/test_tools.py +140 -0
- dial_crewai-0.10.0/uv.lock +3958 -0
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: dial-crewai
|
|
3
|
+
Version: 0.10.0
|
|
4
|
+
Summary: Official Dial CrewAI tools — phone numbers, SMS, OTP, and voice calls for multi-agent crews
|
|
5
|
+
Requires-Python: >=3.11
|
|
6
|
+
Requires-Dist: crewai>=0.80
|
|
7
|
+
Requires-Dist: dial-sdk
|
|
8
|
+
Description-Content-Type: text/markdown
|
|
9
|
+
|
|
10
|
+
# dial-crewai
|
|
11
|
+
|
|
12
|
+
Official Dial tools for [CrewAI](https://crewai.com) — give a multi-agent crew the
|
|
13
|
+
ability to send SMS, receive OTP codes, and place AI voice calls through
|
|
14
|
+
[Dial](https://getdial.ai). Agents automating signups and web tasks get a real
|
|
15
|
+
phone identity.
|
|
16
|
+
|
|
17
|
+
It's the CrewAI sibling of [`dial-langchain`](https://pypi.org/project/dial-langchain/):
|
|
18
|
+
each tool is a `crewai.tools.BaseTool` subclass wrapping the [`dial-sdk`](https://pypi.org/project/dial-sdk/)
|
|
19
|
+
client. It adds nothing to the REST contract — it just shapes Dial's operations into
|
|
20
|
+
CrewAI tools.
|
|
21
|
+
|
|
22
|
+
## Install
|
|
23
|
+
|
|
24
|
+
```bash
|
|
25
|
+
pip install dial-crewai
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
This pulls in `dial-sdk` and `crewai`.
|
|
29
|
+
|
|
30
|
+
## Give the tools to a crew
|
|
31
|
+
|
|
32
|
+
Build one `DialClient`, pass it to `dial_tools`, and hand the list to an agent. Every
|
|
33
|
+
tool shares that one client — a single connection pool for the whole crew:
|
|
34
|
+
|
|
35
|
+
```python
|
|
36
|
+
from crewai import Agent, Crew, Task
|
|
37
|
+
from dial_sdk import DialClient, DialConfig
|
|
38
|
+
from dial_crewai import dial_tools
|
|
39
|
+
|
|
40
|
+
dial = DialClient(DialConfig(api_key="sk_live_..."))
|
|
41
|
+
|
|
42
|
+
phone_agent = Agent(
|
|
43
|
+
role="Phone identity agent",
|
|
44
|
+
goal="Send and receive SMS so the crew can complete phone-verified signups",
|
|
45
|
+
backstory="You operate the crew's Dial phone number.",
|
|
46
|
+
tools=dial_tools(dial),
|
|
47
|
+
)
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
`DialConfig` also takes an optional `base_url` to target a non-default deployment.
|
|
51
|
+
|
|
52
|
+
### Or pick individual tools
|
|
53
|
+
|
|
54
|
+
```python
|
|
55
|
+
from dial_crewai import SendMessageTool, WaitForMessageTool
|
|
56
|
+
|
|
57
|
+
tools = [
|
|
58
|
+
SendMessageTool(client=dial),
|
|
59
|
+
WaitForMessageTool(client=dial),
|
|
60
|
+
]
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
## Available tools
|
|
64
|
+
|
|
65
|
+
Each tool takes your shared `DialClient` (`client=`):
|
|
66
|
+
|
|
67
|
+
| Tool | Action |
|
|
68
|
+
|---|---|
|
|
69
|
+
| `ListNumbersTool` | List your phone numbers |
|
|
70
|
+
| `PurchaseNumberTool` | Provision a new number (billable) |
|
|
71
|
+
| `SetNumberPropertiesTool` | Update a number's nickname / inbound instruction |
|
|
72
|
+
| `SendMessageTool` | Send an SMS (optionally MMS) |
|
|
73
|
+
| `ListMessagesTool` | List recent messages |
|
|
74
|
+
| `MakeCallTool` | Place an AI voice call |
|
|
75
|
+
| `ListCallsTool` | List recent calls |
|
|
76
|
+
| `GetCallTool` | Fetch one call by id |
|
|
77
|
+
| `GetBillingTool` | Credit balance, subscription, per-number mode |
|
|
78
|
+
| `WaitForMessageTool` | Block until the next inbound SMS arrives, or time out |
|
|
79
|
+
|
|
80
|
+
## OTP flow
|
|
81
|
+
|
|
82
|
+
The point of phone identity for a crew: send a code and read the reply.
|
|
83
|
+
`WaitForMessageTool` blocks until the next inbound SMS arrives, so an agent can:
|
|
84
|
+
|
|
85
|
+
1. trigger a signup that texts a code to your Dial number,
|
|
86
|
+
2. call `WaitForMessageTool` to read the inbound code,
|
|
87
|
+
3. enter it back into the signup form.
|
|
88
|
+
|
|
89
|
+
See [`examples/signup_crew.py`](./examples/signup_crew.py) for a runnable crew.
|
|
90
|
+
|
|
91
|
+
## Notes
|
|
92
|
+
|
|
93
|
+
- The `dial-sdk` client is async; these tools are **sync-first** (`_run`) with an
|
|
94
|
+
async→sync bridge, and also expose `_arun` for async crews. The shared client's
|
|
95
|
+
requests run on a single background event loop, so the connection pool is reused
|
|
96
|
+
across calls; a sync crew needn't close the client explicitly.
|
|
97
|
+
- `SendMessageTool` is a write action and **isn't idempotent** — a re-invoke after a
|
|
98
|
+
failure can send a duplicate. `MakeCallTool` accepts an `idempotency_key`.
|
|
99
|
+
- `WaitForMessageTool` is backed by Dial's **presence-based** event stream — for
|
|
100
|
+
durable, at-least-once delivery, register a
|
|
101
|
+
[webhook](https://docs.getdial.ai/documentation/platform/webhooks).
|
|
102
|
+
|
|
103
|
+
See the [CrewAI integration docs](https://docs.getdial.ai/documentation/sdks/crewai)
|
|
104
|
+
for the full guide.
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
# dial-crewai
|
|
2
|
+
|
|
3
|
+
Official Dial tools for [CrewAI](https://crewai.com) — give a multi-agent crew the
|
|
4
|
+
ability to send SMS, receive OTP codes, and place AI voice calls through
|
|
5
|
+
[Dial](https://getdial.ai). Agents automating signups and web tasks get a real
|
|
6
|
+
phone identity.
|
|
7
|
+
|
|
8
|
+
It's the CrewAI sibling of [`dial-langchain`](https://pypi.org/project/dial-langchain/):
|
|
9
|
+
each tool is a `crewai.tools.BaseTool` subclass wrapping the [`dial-sdk`](https://pypi.org/project/dial-sdk/)
|
|
10
|
+
client. It adds nothing to the REST contract — it just shapes Dial's operations into
|
|
11
|
+
CrewAI tools.
|
|
12
|
+
|
|
13
|
+
## Install
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
pip install dial-crewai
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
This pulls in `dial-sdk` and `crewai`.
|
|
20
|
+
|
|
21
|
+
## Give the tools to a crew
|
|
22
|
+
|
|
23
|
+
Build one `DialClient`, pass it to `dial_tools`, and hand the list to an agent. Every
|
|
24
|
+
tool shares that one client — a single connection pool for the whole crew:
|
|
25
|
+
|
|
26
|
+
```python
|
|
27
|
+
from crewai import Agent, Crew, Task
|
|
28
|
+
from dial_sdk import DialClient, DialConfig
|
|
29
|
+
from dial_crewai import dial_tools
|
|
30
|
+
|
|
31
|
+
dial = DialClient(DialConfig(api_key="sk_live_..."))
|
|
32
|
+
|
|
33
|
+
phone_agent = Agent(
|
|
34
|
+
role="Phone identity agent",
|
|
35
|
+
goal="Send and receive SMS so the crew can complete phone-verified signups",
|
|
36
|
+
backstory="You operate the crew's Dial phone number.",
|
|
37
|
+
tools=dial_tools(dial),
|
|
38
|
+
)
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
`DialConfig` also takes an optional `base_url` to target a non-default deployment.
|
|
42
|
+
|
|
43
|
+
### Or pick individual tools
|
|
44
|
+
|
|
45
|
+
```python
|
|
46
|
+
from dial_crewai import SendMessageTool, WaitForMessageTool
|
|
47
|
+
|
|
48
|
+
tools = [
|
|
49
|
+
SendMessageTool(client=dial),
|
|
50
|
+
WaitForMessageTool(client=dial),
|
|
51
|
+
]
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
## Available tools
|
|
55
|
+
|
|
56
|
+
Each tool takes your shared `DialClient` (`client=`):
|
|
57
|
+
|
|
58
|
+
| Tool | Action |
|
|
59
|
+
|---|---|
|
|
60
|
+
| `ListNumbersTool` | List your phone numbers |
|
|
61
|
+
| `PurchaseNumberTool` | Provision a new number (billable) |
|
|
62
|
+
| `SetNumberPropertiesTool` | Update a number's nickname / inbound instruction |
|
|
63
|
+
| `SendMessageTool` | Send an SMS (optionally MMS) |
|
|
64
|
+
| `ListMessagesTool` | List recent messages |
|
|
65
|
+
| `MakeCallTool` | Place an AI voice call |
|
|
66
|
+
| `ListCallsTool` | List recent calls |
|
|
67
|
+
| `GetCallTool` | Fetch one call by id |
|
|
68
|
+
| `GetBillingTool` | Credit balance, subscription, per-number mode |
|
|
69
|
+
| `WaitForMessageTool` | Block until the next inbound SMS arrives, or time out |
|
|
70
|
+
|
|
71
|
+
## OTP flow
|
|
72
|
+
|
|
73
|
+
The point of phone identity for a crew: send a code and read the reply.
|
|
74
|
+
`WaitForMessageTool` blocks until the next inbound SMS arrives, so an agent can:
|
|
75
|
+
|
|
76
|
+
1. trigger a signup that texts a code to your Dial number,
|
|
77
|
+
2. call `WaitForMessageTool` to read the inbound code,
|
|
78
|
+
3. enter it back into the signup form.
|
|
79
|
+
|
|
80
|
+
See [`examples/signup_crew.py`](./examples/signup_crew.py) for a runnable crew.
|
|
81
|
+
|
|
82
|
+
## Notes
|
|
83
|
+
|
|
84
|
+
- The `dial-sdk` client is async; these tools are **sync-first** (`_run`) with an
|
|
85
|
+
async→sync bridge, and also expose `_arun` for async crews. The shared client's
|
|
86
|
+
requests run on a single background event loop, so the connection pool is reused
|
|
87
|
+
across calls; a sync crew needn't close the client explicitly.
|
|
88
|
+
- `SendMessageTool` is a write action and **isn't idempotent** — a re-invoke after a
|
|
89
|
+
failure can send a duplicate. `MakeCallTool` accepts an `idempotency_key`.
|
|
90
|
+
- `WaitForMessageTool` is backed by Dial's **presence-based** event stream — for
|
|
91
|
+
durable, at-least-once delivery, register a
|
|
92
|
+
[webhook](https://docs.getdial.ai/documentation/platform/webhooks).
|
|
93
|
+
|
|
94
|
+
See the [CrewAI integration docs](https://docs.getdial.ai/documentation/sdks/crewai)
|
|
95
|
+
for the full guide.
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
from .tools import (
|
|
2
|
+
dial_tools,
|
|
3
|
+
ListNumbersTool,
|
|
4
|
+
PurchaseNumberTool,
|
|
5
|
+
SetNumberPropertiesTool,
|
|
6
|
+
ListMessagesTool,
|
|
7
|
+
SendMessageTool,
|
|
8
|
+
ListCallsTool,
|
|
9
|
+
MakeCallTool,
|
|
10
|
+
GetCallTool,
|
|
11
|
+
GetBillingTool,
|
|
12
|
+
WaitForMessageTool,
|
|
13
|
+
)
|
|
14
|
+
|
|
15
|
+
__all__ = [
|
|
16
|
+
"dial_tools",
|
|
17
|
+
"ListNumbersTool",
|
|
18
|
+
"PurchaseNumberTool",
|
|
19
|
+
"SetNumberPropertiesTool",
|
|
20
|
+
"ListMessagesTool",
|
|
21
|
+
"SendMessageTool",
|
|
22
|
+
"ListCallsTool",
|
|
23
|
+
"MakeCallTool",
|
|
24
|
+
"GetCallTool",
|
|
25
|
+
"GetBillingTool",
|
|
26
|
+
"WaitForMessageTool",
|
|
27
|
+
]
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
"""Drive async ``dial-sdk`` coroutines from CrewAI's synchronous ``_run``.
|
|
2
|
+
|
|
3
|
+
``dial-sdk`` is async, but CrewAI invokes tools synchronously via ``_run``. We run every
|
|
4
|
+
coroutine on a single, lazily-started **background event loop** (a daemon thread) rather
|
|
5
|
+
than calling ``asyncio.run`` per tool call.
|
|
6
|
+
|
|
7
|
+
Why one persistent loop instead of a fresh loop per call: the tools share one
|
|
8
|
+
``DialClient`` (passed in by the caller), and its underlying ``httpx.AsyncClient`` binds
|
|
9
|
+
its connection pool to the event loop it first runs on. ``asyncio.run`` creates and tears
|
|
10
|
+
down a new loop on every call, so a reused client would fail on the second call ("Event
|
|
11
|
+
loop is closed"). Running all coroutines on one long-lived loop keeps the shared client's
|
|
12
|
+
pool valid and reused across calls.
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
import asyncio
|
|
16
|
+
import threading
|
|
17
|
+
from typing import Awaitable, Optional, TypeVar
|
|
18
|
+
|
|
19
|
+
T = TypeVar("T")
|
|
20
|
+
|
|
21
|
+
_loop: Optional[asyncio.AbstractEventLoop] = None
|
|
22
|
+
_lock = threading.Lock()
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def _background_loop() -> asyncio.AbstractEventLoop:
|
|
26
|
+
"""Return the shared background event loop, starting it on first use."""
|
|
27
|
+
global _loop
|
|
28
|
+
if _loop is None:
|
|
29
|
+
with _lock:
|
|
30
|
+
if _loop is None:
|
|
31
|
+
loop = asyncio.new_event_loop()
|
|
32
|
+
threading.Thread(
|
|
33
|
+
target=loop.run_forever,
|
|
34
|
+
name="dial-crewai",
|
|
35
|
+
daemon=True,
|
|
36
|
+
).start()
|
|
37
|
+
_loop = loop
|
|
38
|
+
return _loop
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def run_sync(coro: Awaitable[T]) -> T:
|
|
42
|
+
"""Block the calling thread until ``coro`` completes on the background loop."""
|
|
43
|
+
return asyncio.run_coroutine_threadsafe(coro, _background_loop()).result()
|