dial-agentframework 0.13.0__tar.gz → 0.14.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.
@@ -1,10 +1,10 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: dial-agentframework
3
- Version: 0.13.0
3
+ Version: 0.14.0
4
4
  Summary: Official Dial tools for the Microsoft Agent Framework — phone numbers, SMS, OTP, and voice calls
5
5
  Requires-Python: >=3.11
6
6
  Requires-Dist: agent-framework-core>=1.0
7
- Requires-Dist: dial-sdk>=0.14.0
7
+ Requires-Dist: dial-sdk>=0.16.0
8
8
  Description-Content-Type: text/markdown
9
9
 
10
10
  # dial-agentframework
@@ -6,6 +6,8 @@ from .tools import (
6
6
  list_messages_tool,
7
7
  send_message_tool,
8
8
  reply_to_message_tool,
9
+ start_typing_tool,
10
+ stop_typing_tool,
9
11
  list_calls_tool,
10
12
  make_call_tool,
11
13
  get_call_tool,
@@ -21,6 +23,8 @@ __all__ = [
21
23
  "list_messages_tool",
22
24
  "send_message_tool",
23
25
  "reply_to_message_tool",
26
+ "start_typing_tool",
27
+ "stop_typing_tool",
24
28
  "list_calls_tool",
25
29
  "make_call_tool",
26
30
  "get_call_tool",
@@ -192,7 +192,16 @@ def send_message_tool(client: DialClient) -> FunctionTool:
192
192
  )
193
193
  async def send_message(
194
194
  to: Annotated[str, "Destination phone number in E.164 format, e.g. +1234567890"],
195
- from_number_id: Annotated[str, "ID of the Dial phone number to send from"],
195
+ from_number: Annotated[
196
+ Optional[str],
197
+ "Number to send from: a phone number ID, one of your numbers in E.164, or "
198
+ "a nickname. Exclusive with from_number_id — provide exactly one.",
199
+ ] = None,
200
+ from_number_id: Annotated[
201
+ Optional[str],
202
+ "ID of the Dial phone number to send from. Exclusive with from_number — "
203
+ "provide exactly one.",
204
+ ] = None,
196
205
  body: Annotated[
197
206
  str, "Message body text; optional when media_urls is given (media-only send)"
198
207
  ] = "",
@@ -211,6 +220,7 @@ def send_message_tool(client: DialClient) -> FunctionTool:
211
220
  msg = await client.send_message(
212
221
  SendMessageParams(
213
222
  to=to,
223
+ from_number=from_number,
214
224
  from_number_id=from_number_id,
215
225
  body=body,
216
226
  channel=channel,
@@ -263,6 +273,59 @@ def reply_to_message_tool(client: DialClient) -> FunctionTool:
263
273
  return reply_to_message
264
274
 
265
275
 
276
+ def start_typing_tool(client: DialClient) -> FunctionTool:
277
+ @tool(
278
+ name="start_typing",
279
+ description=(
280
+ "Show a typing indicator to the recipient, as if composing a message from "
281
+ "your number. iMessage numbers display it; standard (SMS) numbers have no "
282
+ "typing concept and silently ignore it, so it is always safe to call before "
283
+ "send_message. Fire-and-forget and free. Delivering a message or reaction "
284
+ "clears the indicator natively on the recipient's device — call start_typing "
285
+ "again after a send to keep composing, and stop_typing when you stop without sending."
286
+ ),
287
+ )
288
+ async def start_typing(
289
+ to_number: Annotated[
290
+ str, "Recipient phone number in E.164 format, e.g. +14155550123"
291
+ ],
292
+ from_number: Annotated[
293
+ str,
294
+ "Number the indicator appears from: a phone number ID, one of your numbers "
295
+ "in E.164, or a nickname",
296
+ ],
297
+ ) -> str:
298
+ await client.start_typing(to_number=to_number, from_number=from_number)
299
+ return "Typing indicator shown (iMessage numbers only — SMS numbers ignore it)."
300
+
301
+ return start_typing
302
+
303
+
304
+ def stop_typing_tool(client: DialClient) -> FunctionTool:
305
+ @tool(
306
+ name="stop_typing",
307
+ description=(
308
+ "Clear a typing indicator previously shown with start_typing. Delivering a "
309
+ "message or reaction already clears it natively on the recipient's device — "
310
+ "call this when you stop composing without sending."
311
+ ),
312
+ )
313
+ async def stop_typing(
314
+ to_number: Annotated[
315
+ str, "Recipient phone number in E.164 format, e.g. +14155550123"
316
+ ],
317
+ from_number: Annotated[
318
+ str,
319
+ "Number the indicator appears from: a phone number ID, one of your numbers "
320
+ "in E.164, or a nickname",
321
+ ],
322
+ ) -> str:
323
+ await client.stop_typing(to_number=to_number, from_number=from_number)
324
+ return "Typing indicator cleared."
325
+
326
+ return stop_typing
327
+
328
+
266
329
  # ── Calls ────────────────────────────────────────────────────────────────────
267
330
 
268
331
 
@@ -298,10 +361,19 @@ def make_call_tool(client: DialClient) -> FunctionTool:
298
361
  )
299
362
  async def make_call(
300
363
  to: Annotated[str, "Destination phone number in E.164 format, e.g. +1234567890"],
301
- from_number_id: Annotated[str, "ID of the Dial phone number to call from"],
302
364
  outbound_instruction: Annotated[
303
365
  str, "System prompt for the AI voice agent during this call"
304
366
  ],
367
+ from_number: Annotated[
368
+ Optional[str],
369
+ "Number to call from: a phone number ID, one of your numbers in E.164, or "
370
+ "a nickname. Exclusive with from_number_id — provide exactly one.",
371
+ ] = None,
372
+ from_number_id: Annotated[
373
+ Optional[str],
374
+ "ID of the Dial phone number to call from. Exclusive with from_number — "
375
+ "provide exactly one.",
376
+ ] = None,
305
377
  language: Annotated[
306
378
  Optional[str],
307
379
  "BCP-47 language tag, e.g. 'en-US', 'he-IL'. Omit to auto-detect from the "
@@ -332,6 +404,7 @@ def make_call_tool(client: DialClient) -> FunctionTool:
332
404
  call = await client.make_call(
333
405
  MakeCallParams(
334
406
  to=to,
407
+ from_number=from_number,
335
408
  from_number_id=from_number_id,
336
409
  outbound_instruction=outbound_instruction,
337
410
  language=language,
@@ -470,6 +543,8 @@ def dial_tools(client: DialClient) -> list[FunctionTool]:
470
543
  list_messages_tool(client),
471
544
  send_message_tool(client),
472
545
  reply_to_message_tool(client),
546
+ start_typing_tool(client),
547
+ stop_typing_tool(client),
473
548
  list_calls_tool(client),
474
549
  make_call_tool(client),
475
550
  get_call_tool(client),
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "dial-agentframework"
3
- version = "0.13.0"
3
+ version = "0.14.0"
4
4
  description = "Official Dial tools for the Microsoft Agent Framework — phone numbers, SMS, OTP, and voice calls"
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.11"
@@ -8,7 +8,7 @@ dependencies = [
8
8
  # Core has `tool` + `FunctionTool` (all this package needs). Chat clients ship in
9
9
  # separate integration packages (agent-framework-openai, …) the user installs.
10
10
  "agent-framework-core>=1.0",
11
- "dial-sdk>=0.14.0",
11
+ "dial-sdk>=0.16.0",
12
12
  ]
13
13
 
14
14
  [tool.uv.sources]
@@ -6,6 +6,8 @@ from dial_agentframework import (
6
6
  send_message_tool,
7
7
  reply_to_message_tool,
8
8
  make_call_tool,
9
+ start_typing_tool,
10
+ stop_typing_tool,
9
11
  wait_for_message_tool,
10
12
  )
11
13
 
@@ -55,7 +57,7 @@ def _run(tool, **args) -> str:
55
57
  # ── Tools ────────────────────────────────────────────────────────────────────
56
58
 
57
59
 
58
- def test_dial_tools_bundles_all_eleven():
60
+ def test_dial_tools_bundles_all_thirteen():
59
61
  names = sorted(t.name for t in dial_tools(FakeClient()))
60
62
  assert names == sorted(
61
63
  [
@@ -65,6 +67,8 @@ def test_dial_tools_bundles_all_eleven():
65
67
  "list_messages",
66
68
  "send_message",
67
69
  "reply_to_message",
70
+ "start_typing",
71
+ "stop_typing",
68
72
  "list_calls",
69
73
  "make_call",
70
74
  "get_call",
@@ -104,6 +108,44 @@ def test_send_message_media_only_with_force_audio_file():
104
108
  assert params.media == ["https://cdn.example.com/note.m4a"]
105
109
 
106
110
 
111
+ def test_send_message_passes_from_number():
112
+ client = FakeClient(send_message=_FakeMessage())
113
+ tool = send_message_tool(client)
114
+ out = _run(tool, to="+14155550123", from_number="Support line", body="hi")
115
+
116
+ assert "msg_1" in out
117
+ params = client.calls["send_message"]["args"][0]
118
+ assert params.from_number == "Support line"
119
+ assert params.from_number_id is None
120
+
121
+
122
+ def test_start_typing_invokes_client():
123
+ client = FakeClient(start_typing=None)
124
+ tool = start_typing_tool(client)
125
+ assert tool.name == "start_typing"
126
+ out = _run(tool, to_number="+14155550123", from_number="pn_9")
127
+
128
+ assert "Typing indicator shown" in out
129
+ call = client.calls["start_typing"]
130
+ assert call["args"] == ()
131
+ assert call["kwargs"] == {"to_number": "+14155550123", "from_number": "pn_9"}
132
+
133
+
134
+ def test_stop_typing_invokes_client():
135
+ client = FakeClient(stop_typing=None)
136
+ tool = stop_typing_tool(client)
137
+ assert tool.name == "stop_typing"
138
+ out = _run(tool, to_number="+14155550123", from_number="+14155559999")
139
+
140
+ assert "Typing indicator cleared" in out
141
+ call = client.calls["stop_typing"]
142
+ assert call["args"] == ()
143
+ assert call["kwargs"] == {
144
+ "to_number": "+14155550123",
145
+ "from_number": "+14155559999",
146
+ }
147
+
148
+
107
149
  def test_reply_to_message_sends_body_or_reaction():
108
150
  client = FakeClient(reply_to_message=_FakeMessage())
109
151
  tool = reply_to_message_tool(client)
@@ -176,6 +218,22 @@ def test_make_call_passes_idempotency_key():
176
218
  assert params.outbound_instruction == "Confirm the booking."
177
219
 
178
220
 
221
+ def test_make_call_passes_from_number():
222
+ client = FakeClient(make_call=_FakeCall())
223
+ tool = make_call_tool(client)
224
+ out = _run(
225
+ tool,
226
+ to="+14155550123",
227
+ from_number="+14155559999",
228
+ outbound_instruction="Confirm the booking.",
229
+ )
230
+
231
+ assert "call_x" in out
232
+ params = client.calls["make_call"]["args"][0]
233
+ assert params.from_number == "+14155559999"
234
+ assert params.from_number_id is None
235
+
236
+
179
237
  def test_wait_for_message_unknown_id():
180
238
  # list_numbers returns no match → reports the bad id without opening a stream.
181
239
  tool = wait_for_message_tool(FakeClient(list_numbers=[]))
@@ -345,7 +345,7 @@ wheels = [
345
345
 
346
346
  [[package]]
347
347
  name = "dial-agentframework"
348
- version = "0.13.0"
348
+ version = "0.14.0"
349
349
  source = { editable = "." }
350
350
  dependencies = [
351
351
  { name = "agent-framework-core" },
@@ -368,7 +368,7 @@ dev = [{ name = "pytest", specifier = ">=8" }]
368
368
 
369
369
  [[package]]
370
370
  name = "dial-sdk"
371
- version = "0.14.0"
371
+ version = "0.16.0"
372
372
  source = { editable = "../sdk-python" }
373
373
  dependencies = [
374
374
  { name = "httpx" },