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.
@@ -0,0 +1,5 @@
1
+ .venv/
2
+ __pycache__/
3
+ *.pyc
4
+ dist/
5
+ *.egg-info/
@@ -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())