dial-langchain 0.5.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_langchain-0.5.0/.gitignore +5 -0
- dial_langchain-0.5.0/PKG-INFO +70 -0
- dial_langchain-0.5.0/README.md +61 -0
- dial_langchain-0.5.0/dial_langchain/__init__.py +23 -0
- dial_langchain-0.5.0/dial_langchain/tools.py +365 -0
- dial_langchain-0.5.0/pyproject.toml +20 -0
- dial_langchain-0.5.0/test.py +35 -0
- dial_langchain-0.5.0/uv.lock +1587 -0
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: dial-langchain
|
|
3
|
+
Version: 0.5.0
|
|
4
|
+
Summary: Official Dial LangChain tools — phone numbers, SMS, WhatsApp, and voice calls for AI agents
|
|
5
|
+
Requires-Python: >=3.11
|
|
6
|
+
Requires-Dist: dial-sdk
|
|
7
|
+
Requires-Dist: langchain-core>=0.2
|
|
8
|
+
Description-Content-Type: text/markdown
|
|
9
|
+
|
|
10
|
+
# dial-langchain
|
|
11
|
+
|
|
12
|
+
Official [LangChain](https://www.langchain.com/) tools for [Dial](https://getdial.ai) — phone numbers, SMS, WhatsApp, and voice calls for AI agents.
|
|
13
|
+
|
|
14
|
+
## Install
|
|
15
|
+
|
|
16
|
+
```bash
|
|
17
|
+
pip install dial-langchain
|
|
18
|
+
# or
|
|
19
|
+
uv add dial-langchain
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
This pulls in [`dial-sdk`](https://pypi.org/project/dial-sdk/) and `langchain-core` automatically. Requires Python 3.11+.
|
|
23
|
+
|
|
24
|
+
## Quickstart
|
|
25
|
+
|
|
26
|
+
Each Dial capability is a LangChain `BaseTool`. Construct it with your API key (and optional `base_url`) and hand it to an agent:
|
|
27
|
+
|
|
28
|
+
```python
|
|
29
|
+
from dial_langchain import (
|
|
30
|
+
ListNumbersTool,
|
|
31
|
+
SendMessageTool,
|
|
32
|
+
MakeCallTool,
|
|
33
|
+
ListCallsTool,
|
|
34
|
+
GetCallTool,
|
|
35
|
+
WaitForMessageTool,
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
tools = [
|
|
39
|
+
ListNumbersTool(api_key="sk_live_..."),
|
|
40
|
+
SendMessageTool(api_key="sk_live_..."),
|
|
41
|
+
MakeCallTool(api_key="sk_live_..."),
|
|
42
|
+
]
|
|
43
|
+
|
|
44
|
+
# Drop into any LangChain agent:
|
|
45
|
+
from langchain.agents import create_react_agent
|
|
46
|
+
agent = create_react_agent(model, tools, prompt)
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
`base_url` defaults to `https://getdial.ai`; override it for local or self-hosted setups.
|
|
50
|
+
|
|
51
|
+
## Available tools
|
|
52
|
+
|
|
53
|
+
| Tool | Tool name (for the LLM) |
|
|
54
|
+
| --- | --- |
|
|
55
|
+
| `ListNumbersTool` | `list_numbers` |
|
|
56
|
+
| `PurchaseNumberTool` | `purchase_number` |
|
|
57
|
+
| `SetNumberPropertiesTool` | `set_number_properties` |
|
|
58
|
+
| `ListMessagesTool` | `list_messages` |
|
|
59
|
+
| `SendMessageTool` | `send_message` |
|
|
60
|
+
| `ListCallsTool` | `list_calls` |
|
|
61
|
+
| `MakeCallTool` | `make_call` |
|
|
62
|
+
| `GetCallTool` | `get_call` |
|
|
63
|
+
| `WaitForMessageTool` | `wait_for_message` |
|
|
64
|
+
|
|
65
|
+
Each tool wraps the corresponding [`dial-sdk`](https://pypi.org/project/dial-sdk/) call under the hood.
|
|
66
|
+
|
|
67
|
+
## Related
|
|
68
|
+
|
|
69
|
+
- [`dial-sdk`](https://pypi.org/project/dial-sdk/) — the underlying async Python SDK.
|
|
70
|
+
- [Documentation](https://docs.getdial.ai)
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
# dial-langchain
|
|
2
|
+
|
|
3
|
+
Official [LangChain](https://www.langchain.com/) tools for [Dial](https://getdial.ai) — phone numbers, SMS, WhatsApp, and voice calls for AI agents.
|
|
4
|
+
|
|
5
|
+
## Install
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
pip install dial-langchain
|
|
9
|
+
# or
|
|
10
|
+
uv add dial-langchain
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
This pulls in [`dial-sdk`](https://pypi.org/project/dial-sdk/) and `langchain-core` automatically. Requires Python 3.11+.
|
|
14
|
+
|
|
15
|
+
## Quickstart
|
|
16
|
+
|
|
17
|
+
Each Dial capability is a LangChain `BaseTool`. Construct it with your API key (and optional `base_url`) and hand it to an agent:
|
|
18
|
+
|
|
19
|
+
```python
|
|
20
|
+
from dial_langchain import (
|
|
21
|
+
ListNumbersTool,
|
|
22
|
+
SendMessageTool,
|
|
23
|
+
MakeCallTool,
|
|
24
|
+
ListCallsTool,
|
|
25
|
+
GetCallTool,
|
|
26
|
+
WaitForMessageTool,
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
tools = [
|
|
30
|
+
ListNumbersTool(api_key="sk_live_..."),
|
|
31
|
+
SendMessageTool(api_key="sk_live_..."),
|
|
32
|
+
MakeCallTool(api_key="sk_live_..."),
|
|
33
|
+
]
|
|
34
|
+
|
|
35
|
+
# Drop into any LangChain agent:
|
|
36
|
+
from langchain.agents import create_react_agent
|
|
37
|
+
agent = create_react_agent(model, tools, prompt)
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
`base_url` defaults to `https://getdial.ai`; override it for local or self-hosted setups.
|
|
41
|
+
|
|
42
|
+
## Available tools
|
|
43
|
+
|
|
44
|
+
| Tool | Tool name (for the LLM) |
|
|
45
|
+
| --- | --- |
|
|
46
|
+
| `ListNumbersTool` | `list_numbers` |
|
|
47
|
+
| `PurchaseNumberTool` | `purchase_number` |
|
|
48
|
+
| `SetNumberPropertiesTool` | `set_number_properties` |
|
|
49
|
+
| `ListMessagesTool` | `list_messages` |
|
|
50
|
+
| `SendMessageTool` | `send_message` |
|
|
51
|
+
| `ListCallsTool` | `list_calls` |
|
|
52
|
+
| `MakeCallTool` | `make_call` |
|
|
53
|
+
| `GetCallTool` | `get_call` |
|
|
54
|
+
| `WaitForMessageTool` | `wait_for_message` |
|
|
55
|
+
|
|
56
|
+
Each tool wraps the corresponding [`dial-sdk`](https://pypi.org/project/dial-sdk/) call under the hood.
|
|
57
|
+
|
|
58
|
+
## Related
|
|
59
|
+
|
|
60
|
+
- [`dial-sdk`](https://pypi.org/project/dial-sdk/) — the underlying async Python SDK.
|
|
61
|
+
- [Documentation](https://docs.getdial.ai)
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
from .tools import (
|
|
2
|
+
ListNumbersTool,
|
|
3
|
+
PurchaseNumberTool,
|
|
4
|
+
SetNumberPropertiesTool,
|
|
5
|
+
ListMessagesTool,
|
|
6
|
+
SendMessageTool,
|
|
7
|
+
ListCallsTool,
|
|
8
|
+
MakeCallTool,
|
|
9
|
+
GetCallTool,
|
|
10
|
+
WaitForMessageTool,
|
|
11
|
+
)
|
|
12
|
+
|
|
13
|
+
__all__ = [
|
|
14
|
+
"ListNumbersTool",
|
|
15
|
+
"PurchaseNumberTool",
|
|
16
|
+
"SetNumberPropertiesTool",
|
|
17
|
+
"ListMessagesTool",
|
|
18
|
+
"SendMessageTool",
|
|
19
|
+
"ListCallsTool",
|
|
20
|
+
"MakeCallTool",
|
|
21
|
+
"GetCallTool",
|
|
22
|
+
"WaitForMessageTool",
|
|
23
|
+
]
|
|
@@ -0,0 +1,365 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
from typing import Optional
|
|
3
|
+
|
|
4
|
+
from langchain_core.tools import BaseTool
|
|
5
|
+
from pydantic import BaseModel, Field
|
|
6
|
+
from dial_sdk import (
|
|
7
|
+
DialClient,
|
|
8
|
+
DialConfig,
|
|
9
|
+
MakeCallParams,
|
|
10
|
+
PurchaseNumberParams,
|
|
11
|
+
SendMessageParams,
|
|
12
|
+
)
|
|
13
|
+
|
|
14
|
+
DEFAULT_BASE_URL = "https://getdial.ai"
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
# ── Numbers ──────────────────────────────────────────────────────────────────
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class ListNumbersTool(BaseTool):
|
|
21
|
+
name: str = "list_numbers"
|
|
22
|
+
description: str = "List all available Dial phone numbers on the account."
|
|
23
|
+
api_key: str
|
|
24
|
+
base_url: str = DEFAULT_BASE_URL
|
|
25
|
+
|
|
26
|
+
def _run(self, *args, **kwargs):
|
|
27
|
+
raise NotImplementedError("Use async")
|
|
28
|
+
|
|
29
|
+
async def _arun(self) -> str:
|
|
30
|
+
client = DialClient(DialConfig(api_key=self.api_key, base_url=self.base_url))
|
|
31
|
+
try:
|
|
32
|
+
numbers = await client.list_numbers()
|
|
33
|
+
if not numbers:
|
|
34
|
+
return "No numbers found."
|
|
35
|
+
return "\n".join(
|
|
36
|
+
f"{n.id} | {n.number} | {n.country} | {n.capabilities}"
|
|
37
|
+
+ (f' | "{n.nickname}"' if n.nickname else "")
|
|
38
|
+
for n in numbers
|
|
39
|
+
)
|
|
40
|
+
finally:
|
|
41
|
+
await client.close()
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
class PurchaseNumberInput(BaseModel):
|
|
45
|
+
country: str = Field(default="US", description="ISO-3166 alpha-2 country code, e.g. 'US'")
|
|
46
|
+
area_code: str | None = Field(
|
|
47
|
+
default=None, description="Optional area code to prefer when provisioning, e.g. '415'"
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
class PurchaseNumberTool(BaseTool):
|
|
52
|
+
name: str = "purchase_number"
|
|
53
|
+
description: str = "Provision (buy) a new Dial phone number. Billable."
|
|
54
|
+
args_schema: type[BaseModel] = PurchaseNumberInput
|
|
55
|
+
api_key: str
|
|
56
|
+
base_url: str = DEFAULT_BASE_URL
|
|
57
|
+
|
|
58
|
+
def _run(self, *args, **kwargs):
|
|
59
|
+
raise NotImplementedError("Use async")
|
|
60
|
+
|
|
61
|
+
async def _arun(self, country: str = "US", area_code: str | None = None) -> str:
|
|
62
|
+
client = DialClient(DialConfig(api_key=self.api_key, base_url=self.base_url))
|
|
63
|
+
try:
|
|
64
|
+
number = await client.purchase_number(
|
|
65
|
+
PurchaseNumberParams(country=country, area_code=area_code)
|
|
66
|
+
)
|
|
67
|
+
return f"Purchased {number.number} (id: {number.id}, country: {number.country})"
|
|
68
|
+
finally:
|
|
69
|
+
await client.close()
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
class SetNumberPropertiesInput(BaseModel):
|
|
73
|
+
number_id: str = Field(description="ID of the Dial phone number to update")
|
|
74
|
+
inbound_instruction: str | None = Field(
|
|
75
|
+
default=None, description="New system prompt for inbound calls to this number"
|
|
76
|
+
)
|
|
77
|
+
nickname: str | None = Field(
|
|
78
|
+
default=None,
|
|
79
|
+
description="Human-readable label for the number, e.g. 'Support line'. "
|
|
80
|
+
"Pass an empty string to clear it.",
|
|
81
|
+
)
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
class SetNumberPropertiesTool(BaseTool):
|
|
85
|
+
name: str = "set_number_properties"
|
|
86
|
+
description: str = (
|
|
87
|
+
"Update a Dial phone number's properties: its inbound instruction (the system "
|
|
88
|
+
"prompt its AI voice agent uses on inbound calls) and/or its nickname. "
|
|
89
|
+
"Provide at least one."
|
|
90
|
+
)
|
|
91
|
+
args_schema: type[BaseModel] = SetNumberPropertiesInput
|
|
92
|
+
api_key: str
|
|
93
|
+
base_url: str = DEFAULT_BASE_URL
|
|
94
|
+
|
|
95
|
+
def _run(self, *args, **kwargs):
|
|
96
|
+
raise NotImplementedError("Use async")
|
|
97
|
+
|
|
98
|
+
async def _arun(
|
|
99
|
+
self,
|
|
100
|
+
number_id: str,
|
|
101
|
+
inbound_instruction: str | None = None,
|
|
102
|
+
nickname: str | None = None,
|
|
103
|
+
) -> str:
|
|
104
|
+
client = DialClient(DialConfig(api_key=self.api_key, base_url=self.base_url))
|
|
105
|
+
try:
|
|
106
|
+
kwargs: dict = {}
|
|
107
|
+
if inbound_instruction is not None:
|
|
108
|
+
kwargs["inbound_instruction"] = inbound_instruction
|
|
109
|
+
if nickname is not None:
|
|
110
|
+
kwargs["nickname"] = nickname
|
|
111
|
+
if not kwargs:
|
|
112
|
+
return "Provide at least one of inbound_instruction or nickname."
|
|
113
|
+
number = await client.set_number_properties(number_id, **kwargs)
|
|
114
|
+
nick = f' | nickname: "{number.nickname}"' if number.nickname else ""
|
|
115
|
+
return f"Updated {number.number} (id: {number.id}){nick}"
|
|
116
|
+
finally:
|
|
117
|
+
await client.close()
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
# ── Messages ─────────────────────────────────────────────────────────────────
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
class ListMessagesInput(BaseModel):
|
|
124
|
+
number_id: str | None = Field(
|
|
125
|
+
default=None, description="Restrict to messages on a single owned phone number id"
|
|
126
|
+
)
|
|
127
|
+
direction: str | None = Field(
|
|
128
|
+
default=None, description="Filter by direction: 'inbound' or 'outbound'"
|
|
129
|
+
)
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
class ListMessagesTool(BaseTool):
|
|
133
|
+
name: str = "list_messages"
|
|
134
|
+
description: str = "List recent inbound and outbound messages on the account."
|
|
135
|
+
args_schema: type[BaseModel] = ListMessagesInput
|
|
136
|
+
api_key: str
|
|
137
|
+
base_url: str = DEFAULT_BASE_URL
|
|
138
|
+
|
|
139
|
+
def _run(self, *args, **kwargs):
|
|
140
|
+
raise NotImplementedError("Use async")
|
|
141
|
+
|
|
142
|
+
async def _arun(self, number_id: str | None = None, direction: str | None = None) -> str:
|
|
143
|
+
client = DialClient(DialConfig(api_key=self.api_key, base_url=self.base_url))
|
|
144
|
+
try:
|
|
145
|
+
messages = await client.list_messages(number_id=number_id, direction=direction)
|
|
146
|
+
if not messages:
|
|
147
|
+
return "No messages found."
|
|
148
|
+
return "\n".join(
|
|
149
|
+
f"[{m.direction}] {m.from_} → {m.to}: {m.body[:50]}" for m in messages
|
|
150
|
+
)
|
|
151
|
+
finally:
|
|
152
|
+
await client.close()
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
class SendMessageInput(BaseModel):
|
|
156
|
+
to: str = Field(description="Destination phone number in E.164 format, e.g. +1234567890")
|
|
157
|
+
from_number_id: str = Field(description="ID of the Dial phone number to send from")
|
|
158
|
+
body: str = Field(description="Message body text")
|
|
159
|
+
channel: str = Field(default="sms", description="Channel to use: 'sms' or 'whatsapp'")
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
class SendMessageTool(BaseTool):
|
|
163
|
+
name: str = "send_message"
|
|
164
|
+
description: str = "Send an SMS or WhatsApp message to a phone number."
|
|
165
|
+
args_schema: type[BaseModel] = SendMessageInput
|
|
166
|
+
api_key: str
|
|
167
|
+
base_url: str = DEFAULT_BASE_URL
|
|
168
|
+
|
|
169
|
+
def _run(self, *args, **kwargs):
|
|
170
|
+
raise NotImplementedError("Use async")
|
|
171
|
+
|
|
172
|
+
async def _arun(self, to: str, from_number_id: str, body: str, channel: str = "sms") -> str:
|
|
173
|
+
client = DialClient(DialConfig(api_key=self.api_key, base_url=self.base_url))
|
|
174
|
+
try:
|
|
175
|
+
msg = await client.send_message(
|
|
176
|
+
SendMessageParams(to=to, from_number_id=from_number_id, body=body, channel=channel)
|
|
177
|
+
)
|
|
178
|
+
return f"Message sent successfully. ID: {msg.id}, status: {msg.status}"
|
|
179
|
+
finally:
|
|
180
|
+
await client.close()
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
# ── Calls ────────────────────────────────────────────────────────────────────
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
class ListCallsInput(BaseModel):
|
|
187
|
+
number_id: str | None = Field(
|
|
188
|
+
default=None, description="Restrict to calls on a single owned phone number id"
|
|
189
|
+
)
|
|
190
|
+
direction: str | None = Field(
|
|
191
|
+
default=None, description="Filter by direction: 'inbound' or 'outbound'"
|
|
192
|
+
)
|
|
193
|
+
|
|
194
|
+
|
|
195
|
+
class ListCallsTool(BaseTool):
|
|
196
|
+
name: str = "list_calls"
|
|
197
|
+
description: str = "List recent inbound and outbound calls on the account."
|
|
198
|
+
args_schema: type[BaseModel] = ListCallsInput
|
|
199
|
+
api_key: str
|
|
200
|
+
base_url: str = DEFAULT_BASE_URL
|
|
201
|
+
|
|
202
|
+
def _run(self, *args, **kwargs):
|
|
203
|
+
raise NotImplementedError("Use async")
|
|
204
|
+
|
|
205
|
+
async def _arun(self, number_id: str | None = None, direction: str | None = None) -> str:
|
|
206
|
+
client = DialClient(DialConfig(api_key=self.api_key, base_url=self.base_url))
|
|
207
|
+
try:
|
|
208
|
+
calls = await client.list_calls(number_id=number_id, direction=direction)
|
|
209
|
+
if not calls:
|
|
210
|
+
return "No calls found."
|
|
211
|
+
return "\n".join(
|
|
212
|
+
f"{c.id} | [{c.direction}] {c.from_} → {c.to} | "
|
|
213
|
+
f"{c.status.get('label')} | {c.duration}s"
|
|
214
|
+
for c in calls
|
|
215
|
+
)
|
|
216
|
+
finally:
|
|
217
|
+
await client.close()
|
|
218
|
+
|
|
219
|
+
|
|
220
|
+
class MakeCallInput(BaseModel):
|
|
221
|
+
to: str = Field(description="Destination phone number in E.164 format, e.g. +1234567890")
|
|
222
|
+
from_number_id: str = Field(description="ID of the Dial phone number to call from")
|
|
223
|
+
outbound_instruction: str = Field(
|
|
224
|
+
description="System prompt for the AI voice agent during this call"
|
|
225
|
+
)
|
|
226
|
+
language: Optional[str] = Field(
|
|
227
|
+
default=None,
|
|
228
|
+
description=(
|
|
229
|
+
"BCP-47 language tag, e.g. 'en-US', 'he-IL'. Omit to auto-detect from the "
|
|
230
|
+
"destination number's country (the agent also handles en-US)."
|
|
231
|
+
),
|
|
232
|
+
)
|
|
233
|
+
idempotency_key: Optional[str] = Field(
|
|
234
|
+
default=None,
|
|
235
|
+
description=(
|
|
236
|
+
"Unique key (e.g. a UUID) making the placement idempotent: re-invoking with "
|
|
237
|
+
"the same key returns the already-placed call instead of dialing again."
|
|
238
|
+
),
|
|
239
|
+
)
|
|
240
|
+
|
|
241
|
+
|
|
242
|
+
class MakeCallTool(BaseTool):
|
|
243
|
+
name: str = "make_call"
|
|
244
|
+
description: str = "Initiate an AI voice call to a phone number."
|
|
245
|
+
args_schema: type[BaseModel] = MakeCallInput
|
|
246
|
+
api_key: str
|
|
247
|
+
base_url: str = DEFAULT_BASE_URL
|
|
248
|
+
|
|
249
|
+
def _run(self, *args, **kwargs):
|
|
250
|
+
raise NotImplementedError("Use async")
|
|
251
|
+
|
|
252
|
+
async def _arun(
|
|
253
|
+
self,
|
|
254
|
+
to: str,
|
|
255
|
+
from_number_id: str,
|
|
256
|
+
outbound_instruction: str,
|
|
257
|
+
language: Optional[str] = None,
|
|
258
|
+
idempotency_key: Optional[str] = None,
|
|
259
|
+
) -> str:
|
|
260
|
+
client = DialClient(DialConfig(api_key=self.api_key, base_url=self.base_url))
|
|
261
|
+
try:
|
|
262
|
+
call = await client.make_call(
|
|
263
|
+
MakeCallParams(
|
|
264
|
+
to=to,
|
|
265
|
+
from_number_id=from_number_id,
|
|
266
|
+
outbound_instruction=outbound_instruction,
|
|
267
|
+
language=language,
|
|
268
|
+
idempotency_key=idempotency_key,
|
|
269
|
+
)
|
|
270
|
+
)
|
|
271
|
+
return f"Call initiated. ID: {call.id}, state: {call.status.get('state')}"
|
|
272
|
+
finally:
|
|
273
|
+
await client.close()
|
|
274
|
+
|
|
275
|
+
|
|
276
|
+
class GetCallInput(BaseModel):
|
|
277
|
+
call_id: str = Field(description="ID of the call to fetch")
|
|
278
|
+
|
|
279
|
+
|
|
280
|
+
class GetCallTool(BaseTool):
|
|
281
|
+
name: str = "get_call"
|
|
282
|
+
description: str = "Fetch a single call by its id, including its current status."
|
|
283
|
+
args_schema: type[BaseModel] = GetCallInput
|
|
284
|
+
api_key: str
|
|
285
|
+
base_url: str = DEFAULT_BASE_URL
|
|
286
|
+
|
|
287
|
+
def _run(self, *args, **kwargs):
|
|
288
|
+
raise NotImplementedError("Use async")
|
|
289
|
+
|
|
290
|
+
async def _arun(self, call_id: str) -> str:
|
|
291
|
+
client = DialClient(DialConfig(api_key=self.api_key, base_url=self.base_url))
|
|
292
|
+
try:
|
|
293
|
+
call = await client.get_call(call_id)
|
|
294
|
+
return (
|
|
295
|
+
f"Call {call.id}: [{call.direction}] {call.from_} → {call.to} | "
|
|
296
|
+
f"state={call.status.get('state')} | label={call.status.get('label')} | "
|
|
297
|
+
f"duration={call.duration}s"
|
|
298
|
+
)
|
|
299
|
+
finally:
|
|
300
|
+
await client.close()
|
|
301
|
+
|
|
302
|
+
|
|
303
|
+
# ── Events (wait-for) ──────────────────────────────────────────────────────────
|
|
304
|
+
|
|
305
|
+
|
|
306
|
+
class WaitForMessageInput(BaseModel):
|
|
307
|
+
phone_number_id: str = Field(
|
|
308
|
+
default="",
|
|
309
|
+
description="Optional id of the Dial phone number to wait on. "
|
|
310
|
+
"If omitted, returns the first inbound message on any number.",
|
|
311
|
+
)
|
|
312
|
+
timeout_seconds: int = Field(
|
|
313
|
+
default=30, description="How long to wait for a message in seconds"
|
|
314
|
+
)
|
|
315
|
+
|
|
316
|
+
|
|
317
|
+
class WaitForMessageTool(BaseTool):
|
|
318
|
+
name: str = "wait_for_message"
|
|
319
|
+
description: str = (
|
|
320
|
+
"Wait for the next inbound SMS or WhatsApp message and return it. "
|
|
321
|
+
"Blocks until a message arrives or the timeout elapses."
|
|
322
|
+
)
|
|
323
|
+
args_schema: type[BaseModel] = WaitForMessageInput
|
|
324
|
+
api_key: str
|
|
325
|
+
base_url: str = DEFAULT_BASE_URL
|
|
326
|
+
|
|
327
|
+
def _run(self, *args, **kwargs):
|
|
328
|
+
raise NotImplementedError("Use async")
|
|
329
|
+
|
|
330
|
+
async def _arun(self, phone_number_id: str = "", timeout_seconds: int = 30) -> str:
|
|
331
|
+
client = DialClient(DialConfig(api_key=self.api_key, base_url=self.base_url))
|
|
332
|
+
try:
|
|
333
|
+
# Resolve the phone number id to its E.164 number so we can match the
|
|
334
|
+
# inbound event's `to` field (events carry the number, not its id).
|
|
335
|
+
target_number: str | None = None
|
|
336
|
+
if phone_number_id:
|
|
337
|
+
numbers = await client.list_numbers()
|
|
338
|
+
match = next((n for n in numbers if n.id == phone_number_id), None)
|
|
339
|
+
if match is None:
|
|
340
|
+
return f"No owned number with id {phone_number_id}."
|
|
341
|
+
target_number = match.number
|
|
342
|
+
|
|
343
|
+
async def _first_message() -> dict | None:
|
|
344
|
+
async with client.new_events_connection() as conn:
|
|
345
|
+
async for event in conn:
|
|
346
|
+
if event.get("type") != "message.received":
|
|
347
|
+
continue
|
|
348
|
+
# Event fields live under `data` in the standardized envelope.
|
|
349
|
+
data = event.get("data", {})
|
|
350
|
+
if target_number and data.get("to") != target_number:
|
|
351
|
+
continue
|
|
352
|
+
return event
|
|
353
|
+
return None
|
|
354
|
+
|
|
355
|
+
try:
|
|
356
|
+
event = await asyncio.wait_for(_first_message(), timeout=timeout_seconds)
|
|
357
|
+
except asyncio.TimeoutError:
|
|
358
|
+
return f"No message received within {timeout_seconds}s."
|
|
359
|
+
|
|
360
|
+
if not event:
|
|
361
|
+
return f"No message received within {timeout_seconds}s."
|
|
362
|
+
data = event.get("data", {})
|
|
363
|
+
return f"Received message from {data.get('from')}: {data.get('body')}"
|
|
364
|
+
finally:
|
|
365
|
+
await client.close()
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "dial-langchain"
|
|
3
|
+
version = "0.5.0"
|
|
4
|
+
description = "Official Dial LangChain tools — phone numbers, SMS, WhatsApp, and voice calls for AI agents"
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
requires-python = ">=3.11"
|
|
7
|
+
dependencies = [
|
|
8
|
+
"langchain-core>=0.2",
|
|
9
|
+
"dial-sdk",
|
|
10
|
+
]
|
|
11
|
+
|
|
12
|
+
[tool.uv.sources]
|
|
13
|
+
dial-sdk = { path = "../sdk-python", editable = true }
|
|
14
|
+
|
|
15
|
+
[build-system]
|
|
16
|
+
requires = ["hatchling"]
|
|
17
|
+
build-backend = "hatchling.build"
|
|
18
|
+
|
|
19
|
+
[tool.hatch.build.targets.wheel]
|
|
20
|
+
packages = ["dial_langchain"]
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Quick test script for the Dial LangChain tools.
|
|
3
|
+
|
|
4
|
+
Usage:
|
|
5
|
+
uv sync
|
|
6
|
+
uv run python test.py
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
import asyncio
|
|
10
|
+
import sys
|
|
11
|
+
import os
|
|
12
|
+
sys.path.insert(0, os.path.dirname(__file__))
|
|
13
|
+
|
|
14
|
+
from dial_langchain import ListNumbersTool, ListMessagesTool, ListCallsTool, SendMessageTool, MakeCallTool, WaitForMessageTool
|
|
15
|
+
|
|
16
|
+
API_KEY = "sk_live_..."
|
|
17
|
+
BASE_URL = "https://getdial.ai"
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
async def main():
|
|
21
|
+
list_numbers = ListNumbersTool(api_key=API_KEY, base_url=BASE_URL)
|
|
22
|
+
list_messages = ListMessagesTool(api_key=API_KEY, base_url=BASE_URL)
|
|
23
|
+
list_calls = ListCallsTool(api_key=API_KEY, base_url=BASE_URL)
|
|
24
|
+
|
|
25
|
+
print("--- Numbers ---")
|
|
26
|
+
print(await list_numbers._arun())
|
|
27
|
+
|
|
28
|
+
print("\n--- Messages ---")
|
|
29
|
+
print(await list_messages._arun())
|
|
30
|
+
|
|
31
|
+
print("\n--- Calls ---")
|
|
32
|
+
print(await list_calls._arun())
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
asyncio.run(main())
|