agentphone 0.1.0__py3-none-any.whl

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.
agentphone/__init__.py ADDED
@@ -0,0 +1,78 @@
1
+ """
2
+ AgentPhone Python SDK
3
+
4
+ Give your AI agents phone numbers, SMS, and voice calls.
5
+
6
+ Usage::
7
+
8
+ from agentphone import AgentPhone
9
+
10
+ client = AgentPhone(api_key="your-api-key")
11
+ number = client.numbers.buy(country="US")
12
+ call = client.calls.make_conversation(
13
+ agent_id="...",
14
+ to_number="+14155551234",
15
+ topic="You are a friendly assistant. Ask about their day.",
16
+ )
17
+ """
18
+
19
+ from .async_client import AsyncAgentPhone
20
+ from .client import AgentPhone
21
+ from ._http import AgentPhoneError, AuthenticationError, NotFoundError, RateLimitError
22
+ from .models import (
23
+ Agent,
24
+ AgentList,
25
+ AgentNumber,
26
+ Call,
27
+ CallList,
28
+ CallTranscript,
29
+ Conversation,
30
+ ConversationList,
31
+ ConversationMessage,
32
+ Message,
33
+ MessageList,
34
+ PhoneNumber,
35
+ PhoneNumberList,
36
+ Webhook,
37
+ WebhookDelivery,
38
+ WebhookEvent,
39
+ WebhookEventData,
40
+ WebhookHistoryItem,
41
+ )
42
+ from .webhook import WebhookVerificationError, construct_event, verify_webhook
43
+
44
+ __version__ = "0.1.0"
45
+
46
+ __all__ = [
47
+ # Clients
48
+ "AgentPhone",
49
+ "AsyncAgentPhone",
50
+ # Errors
51
+ "AgentPhoneError",
52
+ "AuthenticationError",
53
+ "NotFoundError",
54
+ "RateLimitError",
55
+ "WebhookVerificationError",
56
+ # Webhook helpers
57
+ "verify_webhook",
58
+ "construct_event",
59
+ # Models
60
+ "Agent",
61
+ "AgentList",
62
+ "AgentNumber",
63
+ "Call",
64
+ "CallList",
65
+ "CallTranscript",
66
+ "Conversation",
67
+ "ConversationList",
68
+ "ConversationMessage",
69
+ "Message",
70
+ "MessageList",
71
+ "PhoneNumber",
72
+ "PhoneNumberList",
73
+ "Webhook",
74
+ "WebhookDelivery",
75
+ "WebhookEvent",
76
+ "WebhookEventData",
77
+ "WebhookHistoryItem",
78
+ ]
agentphone/_http.py ADDED
@@ -0,0 +1,44 @@
1
+ """
2
+ Shared HTTP logic for sync and async clients.
3
+ """
4
+
5
+ from __future__ import annotations
6
+
7
+ DEFAULT_BASE_URL = "https://api.agentphone.to"
8
+
9
+
10
+ class AgentPhoneError(Exception):
11
+ """Raised when the AgentPhone API returns a non-2xx response."""
12
+
13
+ def __init__(self, status: int, message: str):
14
+ self.status = status
15
+ self.message = message
16
+ super().__init__(f"AgentPhone API error {status}: {message}")
17
+
18
+
19
+ class AuthenticationError(AgentPhoneError):
20
+ pass
21
+
22
+
23
+ class NotFoundError(AgentPhoneError):
24
+ pass
25
+
26
+
27
+ class RateLimitError(AgentPhoneError):
28
+ pass
29
+
30
+
31
+ def _raise_for_status(status: int, text: str) -> None:
32
+ try:
33
+ import json as _json
34
+ detail = _json.loads(text).get("detail") or _json.loads(text).get("message") or text
35
+ except Exception:
36
+ detail = text
37
+
38
+ if status == 401:
39
+ raise AuthenticationError(status, detail)
40
+ if status == 404:
41
+ raise NotFoundError(status, detail)
42
+ if status == 429:
43
+ raise RateLimitError(status, detail)
44
+ raise AgentPhoneError(status, detail)
@@ -0,0 +1,210 @@
1
+ """
2
+ Asynchronous AgentPhone client (requires httpx).
3
+ """
4
+
5
+ from __future__ import annotations
6
+
7
+ from typing import Optional, TYPE_CHECKING
8
+
9
+ if TYPE_CHECKING:
10
+ import httpx
11
+
12
+ from ._http import DEFAULT_BASE_URL, _raise_for_status
13
+ from .models import (
14
+ Agent,
15
+ AgentList,
16
+ Call,
17
+ CallList,
18
+ Conversation,
19
+ ConversationList,
20
+ MessageList,
21
+ PhoneNumber,
22
+ PhoneNumberList,
23
+ Webhook,
24
+ WebhookDelivery,
25
+ )
26
+
27
+
28
+ class _AsyncResource:
29
+ def __init__(self, client: "AsyncAgentPhone") -> None:
30
+ self._client = client
31
+
32
+ async def _get(self, path: str, **params) -> dict:
33
+ return await self._client._request("GET", path, params=params or None)
34
+
35
+ async def _post(self, path: str, **body) -> dict:
36
+ return await self._client._request(
37
+ "POST", path, json={k: v for k, v in body.items() if v is not None}
38
+ )
39
+
40
+ async def _delete(self, path: str) -> dict:
41
+ return await self._client._request("DELETE", path)
42
+
43
+
44
+ class AsyncNumbersResource(_AsyncResource):
45
+ async def list(self, limit: int = 20, offset: int = 0) -> PhoneNumberList:
46
+ return PhoneNumberList.from_dict(await self._get("/v1/numbers", limit=limit, offset=offset))
47
+
48
+ async def buy(self, country: str = "US", agent_id: Optional[str] = None) -> PhoneNumber:
49
+ return PhoneNumber.from_dict(await self._post("/v1/numbers", country=country, agentId=agent_id))
50
+
51
+ async def release(self, number_id: str) -> dict:
52
+ return await self._delete(f"/v1/numbers/{number_id}")
53
+
54
+ async def get_messages(self, number_id: str, limit: int = 50) -> MessageList:
55
+ return MessageList.from_dict(await self._get(f"/v1/numbers/{number_id}/messages", limit=limit))
56
+
57
+
58
+ class AsyncAgentsResource(_AsyncResource):
59
+ async def list(self, limit: int = 20, offset: int = 0) -> AgentList:
60
+ return AgentList.from_dict(await self._get("/v1/agents", limit=limit, offset=offset))
61
+
62
+ async def create(self, name: str, description: Optional[str] = None) -> Agent:
63
+ return Agent.from_dict(await self._post("/v1/agents", name=name, description=description))
64
+
65
+ async def get(self, agent_id: str) -> Agent:
66
+ return Agent.from_dict(await self._get(f"/v1/agents/{agent_id}"))
67
+
68
+ async def attach_number(self, agent_id: str, number_id: str) -> dict:
69
+ return await self._post(f"/v1/agents/{agent_id}/numbers", numberId=number_id)
70
+
71
+
72
+ class AsyncCallsResource(_AsyncResource):
73
+ async def list(self, limit: int = 20, offset: int = 0) -> CallList:
74
+ return CallList.from_dict(await self._get("/v1/calls", limit=limit, offset=offset))
75
+
76
+ async def get(self, call_id: str) -> Call:
77
+ return Call.from_dict(await self._get(f"/v1/calls/{call_id}"))
78
+
79
+ async def make(
80
+ self,
81
+ agent_id: str,
82
+ to_number: str,
83
+ initial_greeting: Optional[str] = None,
84
+ ) -> Call:
85
+ return Call.from_dict(
86
+ await self._post(
87
+ "/v1/calls",
88
+ agentId=agent_id,
89
+ toNumber=to_number,
90
+ initialGreeting=initial_greeting,
91
+ )
92
+ )
93
+
94
+ async def make_conversation(
95
+ self,
96
+ agent_id: str,
97
+ to_number: str,
98
+ topic: str,
99
+ initial_greeting: Optional[str] = None,
100
+ model: Optional[str] = None,
101
+ ) -> Call:
102
+ return Call.from_dict(
103
+ await self._post(
104
+ "/v1/calls",
105
+ agentId=agent_id,
106
+ toNumber=to_number,
107
+ systemPrompt=topic,
108
+ initialGreeting=initial_greeting,
109
+ model=model,
110
+ )
111
+ )
112
+
113
+
114
+ class AsyncConversationsResource(_AsyncResource):
115
+ async def list(self, limit: int = 20, offset: int = 0) -> ConversationList:
116
+ return ConversationList.from_dict(await self._get("/v1/conversations", limit=limit, offset=offset))
117
+
118
+ async def get(self, conversation_id: str, message_limit: int = 50) -> Conversation:
119
+ return Conversation.from_dict(
120
+ await self._get(f"/v1/conversations/{conversation_id}", message_limit=message_limit)
121
+ )
122
+
123
+
124
+ class AsyncWebhooksResource(_AsyncResource):
125
+ async def get(self) -> Optional[Webhook]:
126
+ data = await self._get("/v1/webhooks")
127
+ return Webhook.from_dict(data) if data else None
128
+
129
+ async def set(self, url: str, context_limit: Optional[int] = None) -> Webhook:
130
+ return Webhook.from_dict(await self._post("/v1/webhooks", url=url, contextLimit=context_limit))
131
+
132
+ async def delete(self) -> dict:
133
+ return await self._delete("/v1/webhooks")
134
+
135
+ async def list_deliveries(self, limit: int = 50) -> list[WebhookDelivery]:
136
+ data = await self._get("/v1/webhooks/deliveries", limit=limit)
137
+ return [WebhookDelivery.from_dict(d) for d in data]
138
+
139
+ async def test(self) -> dict:
140
+ return await self._post("/v1/webhooks/test")
141
+
142
+
143
+ class AsyncAgentPhone:
144
+ """
145
+ Asynchronous AgentPhone client.
146
+
147
+ Requires: pip install agentphone[async]
148
+
149
+ Example::
150
+
151
+ from agentphone import AsyncAgentPhone
152
+
153
+ async with AsyncAgentPhone(api_key="your-api-key") as client:
154
+ number = await client.numbers.buy(country="US")
155
+ call = await client.calls.make_conversation(
156
+ agent_id="...",
157
+ to_number="+14155551234",
158
+ topic="You are a friendly assistant.",
159
+ )
160
+ """
161
+
162
+ def __init__(
163
+ self,
164
+ api_key: str,
165
+ base_url: str = DEFAULT_BASE_URL,
166
+ timeout: float = 30.0,
167
+ ) -> None:
168
+ try:
169
+ import httpx as _httpx
170
+ except ImportError:
171
+ raise ImportError(
172
+ "The async client requires httpx. Install it with: pip install agentphone[async]"
173
+ )
174
+ self.api_key = api_key
175
+ self.base_url = base_url.rstrip("/")
176
+ self.timeout = timeout
177
+ self._client = _httpx.AsyncClient(
178
+ headers={
179
+ "Authorization": f"Bearer {api_key}",
180
+ "Content-Type": "application/json",
181
+ "User-Agent": "agentphone-python/0.1.0",
182
+ },
183
+ timeout=timeout,
184
+ )
185
+
186
+ self.numbers = AsyncNumbersResource(self)
187
+ self.agents = AsyncAgentsResource(self)
188
+ self.calls = AsyncCallsResource(self)
189
+ self.conversations = AsyncConversationsResource(self)
190
+ self.webhooks = AsyncWebhooksResource(self)
191
+
192
+ async def _request(self, method: str, path: str, **kwargs) -> dict: # type: ignore[override]
193
+ url = f"{self.base_url}{path}"
194
+ resp = await self._client.request(method, url, **kwargs)
195
+
196
+ if not resp.is_success:
197
+ _raise_for_status(resp.status_code, resp.text)
198
+
199
+ if resp.status_code == 204:
200
+ return {}
201
+ return resp.json()
202
+
203
+ async def close(self) -> None:
204
+ await self._client.aclose()
205
+
206
+ async def __aenter__(self) -> "AsyncAgentPhone":
207
+ return self
208
+
209
+ async def __aexit__(self, *args) -> None:
210
+ await self.close()
agentphone/client.py ADDED
@@ -0,0 +1,238 @@
1
+ """
2
+ Synchronous AgentPhone client.
3
+ """
4
+
5
+ from __future__ import annotations
6
+
7
+ from typing import Optional
8
+
9
+ import requests
10
+
11
+ from ._http import DEFAULT_BASE_URL, _raise_for_status
12
+ from .models import (
13
+ Agent,
14
+ AgentList,
15
+ Call,
16
+ CallList,
17
+ Conversation,
18
+ ConversationList,
19
+ MessageList,
20
+ PhoneNumber,
21
+ PhoneNumberList,
22
+ Webhook,
23
+ WebhookDelivery,
24
+ )
25
+
26
+
27
+ class _Resource:
28
+ def __init__(self, client: "AgentPhone") -> None:
29
+ self._client = client
30
+
31
+ def _get(self, path: str, **params) -> dict:
32
+ return self._client._request("GET", path, params=params or None)
33
+
34
+ def _post(self, path: str, **body) -> dict:
35
+ return self._client._request("POST", path, json={k: v for k, v in body.items() if v is not None})
36
+
37
+ def _delete(self, path: str) -> dict:
38
+ return self._client._request("DELETE", path)
39
+
40
+
41
+ class NumbersResource(_Resource):
42
+ def list(self, limit: int = 20, offset: int = 0) -> PhoneNumberList:
43
+ """List all phone numbers."""
44
+ return PhoneNumberList.from_dict(self._get("/v1/numbers", limit=limit, offset=offset))
45
+
46
+ def buy(self, country: str = "US", area_code: Optional[str] = None, agent_id: Optional[str] = None) -> PhoneNumber:
47
+ """Purchase a new phone number. Optionally specify an area code (US/CA only)."""
48
+ return PhoneNumber.from_dict(self._post("/v1/numbers", country=country, areaCode=area_code, agentId=agent_id))
49
+
50
+ def release(self, number_id: str) -> dict:
51
+ """Release a phone number (irreversible)."""
52
+ return self._delete(f"/v1/numbers/{number_id}")
53
+
54
+ def get_messages(self, number_id: str, limit: int = 50) -> MessageList:
55
+ """Get SMS messages received on a number."""
56
+ return MessageList.from_dict(self._get(f"/v1/numbers/{number_id}/messages", limit=limit))
57
+
58
+
59
+ class AgentsResource(_Resource):
60
+ def list(self, limit: int = 20, offset: int = 0) -> AgentList:
61
+ """List all agents."""
62
+ return AgentList.from_dict(self._get("/v1/agents", limit=limit, offset=offset))
63
+
64
+ def create(self, name: str, description: Optional[str] = None) -> Agent:
65
+ """Create a new agent."""
66
+ return Agent.from_dict(self._post("/v1/agents", name=name, description=description))
67
+
68
+ def get(self, agent_id: str) -> Agent:
69
+ """Get agent details including attached numbers."""
70
+ return Agent.from_dict(self._get(f"/v1/agents/{agent_id}"))
71
+
72
+ def attach_number(self, agent_id: str, number_id: str) -> dict:
73
+ """Attach a phone number to an agent."""
74
+ return self._post(f"/v1/agents/{agent_id}/numbers", numberId=number_id)
75
+
76
+
77
+ class CallsResource(_Resource):
78
+ def list(self, limit: int = 20, offset: int = 0) -> CallList:
79
+ """List recent calls."""
80
+ return CallList.from_dict(self._get("/v1/calls", limit=limit, offset=offset))
81
+
82
+ def get(self, call_id: str) -> Call:
83
+ """Get call details and transcript."""
84
+ return Call.from_dict(self._get(f"/v1/calls/{call_id}"))
85
+
86
+ def make(
87
+ self,
88
+ agent_id: str,
89
+ to_number: str,
90
+ initial_greeting: Optional[str] = None,
91
+ ) -> Call:
92
+ """
93
+ Make an outbound call (webhook-based).
94
+
95
+ The agent must have a webhook configured to handle the conversation.
96
+ """
97
+ return Call.from_dict(
98
+ self._post(
99
+ "/v1/calls",
100
+ agentId=agent_id,
101
+ toNumber=to_number,
102
+ initialGreeting=initial_greeting,
103
+ )
104
+ )
105
+
106
+ def make_conversation(
107
+ self,
108
+ agent_id: str,
109
+ to_number: str,
110
+ topic: str,
111
+ initial_greeting: Optional[str] = None,
112
+ model: Optional[str] = None,
113
+ ) -> Call:
114
+ """
115
+ Make an outbound call with a built-in LLM conversation.
116
+
117
+ No webhook required. The AI handles the full conversation
118
+ based on the topic/system prompt.
119
+ """
120
+ return Call.from_dict(
121
+ self._post(
122
+ "/v1/calls",
123
+ agentId=agent_id,
124
+ toNumber=to_number,
125
+ systemPrompt=topic,
126
+ initialGreeting=initial_greeting,
127
+ model=model,
128
+ )
129
+ )
130
+
131
+
132
+ class ConversationsResource(_Resource):
133
+ def list(self, limit: int = 20, offset: int = 0) -> ConversationList:
134
+ """List SMS conversations."""
135
+ return ConversationList.from_dict(self._get("/v1/conversations", limit=limit, offset=offset))
136
+
137
+ def get(self, conversation_id: str, message_limit: int = 50) -> Conversation:
138
+ """Get a conversation with full message history."""
139
+ return Conversation.from_dict(
140
+ self._get(f"/v1/conversations/{conversation_id}", message_limit=message_limit)
141
+ )
142
+
143
+
144
+ class WebhooksResource(_Resource):
145
+ def get(self) -> Optional[Webhook]:
146
+ """Get the configured webhook endpoint."""
147
+ data = self._get("/v1/webhooks")
148
+ return Webhook.from_dict(data) if data else None
149
+
150
+ def set(self, url: str, context_limit: Optional[int] = None) -> Webhook:
151
+ """
152
+ Register or update the webhook URL.
153
+
154
+ Args:
155
+ url: Publicly accessible HTTPS URL.
156
+ context_limit: Number of recent messages to include in payloads (0-50).
157
+ """
158
+ return Webhook.from_dict(self._post("/v1/webhooks", url=url, contextLimit=context_limit))
159
+
160
+ def delete(self) -> dict:
161
+ """Remove the webhook configuration."""
162
+ return self._delete("/v1/webhooks")
163
+
164
+ def list_deliveries(self, limit: int = 50) -> list[WebhookDelivery]:
165
+ """Get recent webhook delivery attempts."""
166
+ data = self._get("/v1/webhooks/deliveries", limit=limit)
167
+ return [WebhookDelivery.from_dict(d) for d in data]
168
+
169
+ def test(self) -> dict:
170
+ """Send a test event to the configured webhook."""
171
+ return self._post("/v1/webhooks/test")
172
+
173
+
174
+ class AgentPhone:
175
+ """
176
+ Synchronous AgentPhone client.
177
+
178
+ Example::
179
+
180
+ from agentphone import AgentPhone
181
+
182
+ client = AgentPhone(api_key="your-api-key")
183
+
184
+ # Buy a number
185
+ number = client.numbers.buy(country="US")
186
+
187
+ # Make a conversation call
188
+ call = client.calls.make_conversation(
189
+ agent_id="...",
190
+ to_number="+14155551234",
191
+ topic="You are a friendly assistant. Ask about their day.",
192
+ )
193
+ """
194
+
195
+ def __init__(
196
+ self,
197
+ api_key: str,
198
+ base_url: str = DEFAULT_BASE_URL,
199
+ timeout: float = 30.0,
200
+ ) -> None:
201
+ self.api_key = api_key
202
+ self.base_url = base_url.rstrip("/")
203
+ self.timeout = timeout
204
+ self._session = requests.Session()
205
+ self._session.headers.update(
206
+ {
207
+ "Authorization": f"Bearer {api_key}",
208
+ "Content-Type": "application/json",
209
+ "User-Agent": "agentphone-python/0.1.0",
210
+ }
211
+ )
212
+
213
+ self.numbers = NumbersResource(self)
214
+ self.agents = AgentsResource(self)
215
+ self.calls = CallsResource(self)
216
+ self.conversations = ConversationsResource(self)
217
+ self.webhooks = WebhooksResource(self)
218
+
219
+ def _request(self, method: str, path: str, **kwargs) -> dict:
220
+ url = f"{self.base_url}{path}"
221
+ resp = self._session.request(method, url, timeout=self.timeout, **kwargs)
222
+
223
+ if not resp.ok:
224
+ _raise_for_status(resp.status_code, resp.text)
225
+
226
+ if resp.status_code == 204:
227
+ return {}
228
+ return resp.json()
229
+
230
+ def close(self) -> None:
231
+ """Close the underlying HTTP session."""
232
+ self._session.close()
233
+
234
+ def __enter__(self) -> "AgentPhone":
235
+ return self
236
+
237
+ def __exit__(self, *args) -> None:
238
+ self.close()
agentphone/models.py ADDED
@@ -0,0 +1,362 @@
1
+ """
2
+ AgentPhone response models.
3
+ """
4
+
5
+ from __future__ import annotations
6
+
7
+ from dataclasses import dataclass, field
8
+ from typing import Optional
9
+
10
+
11
+ # --- Numbers ---
12
+
13
+ @dataclass
14
+ class PhoneNumber:
15
+ id: str
16
+ phone_number: str
17
+ country: str
18
+ status: str
19
+ agent_id: Optional[str]
20
+ created_at: str
21
+
22
+ @classmethod
23
+ def from_dict(cls, d: dict) -> "PhoneNumber":
24
+ return cls(
25
+ id=d["id"],
26
+ phone_number=d["phoneNumber"],
27
+ country=d["country"],
28
+ status=d["status"],
29
+ agent_id=d.get("agentId"),
30
+ created_at=d["createdAt"],
31
+ )
32
+
33
+
34
+ @dataclass
35
+ class PhoneNumberList:
36
+ data: list[PhoneNumber]
37
+ has_more: bool
38
+ total: int
39
+
40
+ @classmethod
41
+ def from_dict(cls, d: dict) -> "PhoneNumberList":
42
+ return cls(
43
+ data=[PhoneNumber.from_dict(n) for n in d["data"]],
44
+ has_more=d["hasMore"],
45
+ total=d["total"],
46
+ )
47
+
48
+
49
+ # --- Messages ---
50
+
51
+ @dataclass
52
+ class Message:
53
+ id: str
54
+ from_number: str
55
+ to_number: str
56
+ body: str
57
+ received_at: str
58
+
59
+ @classmethod
60
+ def from_dict(cls, d: dict) -> "Message":
61
+ return cls(
62
+ id=d["id"],
63
+ from_number=d["from"],
64
+ to_number=d["to"],
65
+ body=d["body"],
66
+ received_at=d["receivedAt"],
67
+ )
68
+
69
+
70
+ @dataclass
71
+ class MessageList:
72
+ data: list[Message]
73
+ has_more: bool
74
+
75
+ @classmethod
76
+ def from_dict(cls, d: dict) -> "MessageList":
77
+ return cls(
78
+ data=[Message.from_dict(m) for m in d["data"]],
79
+ has_more=d["hasMore"],
80
+ )
81
+
82
+
83
+ # --- Agents ---
84
+
85
+ @dataclass
86
+ class AgentNumber:
87
+ id: str
88
+ phone_number: str
89
+ status: str
90
+
91
+ @classmethod
92
+ def from_dict(cls, d: dict) -> "AgentNumber":
93
+ return cls(id=d["id"], phone_number=d["phoneNumber"], status=d["status"])
94
+
95
+
96
+ @dataclass
97
+ class Agent:
98
+ id: str
99
+ name: str
100
+ description: Optional[str]
101
+ created_at: str
102
+ numbers: list[AgentNumber] = field(default_factory=list)
103
+
104
+ @classmethod
105
+ def from_dict(cls, d: dict) -> "Agent":
106
+ return cls(
107
+ id=d["id"],
108
+ name=d["name"],
109
+ description=d.get("description"),
110
+ created_at=d["createdAt"],
111
+ numbers=[AgentNumber.from_dict(n) for n in d.get("numbers", [])],
112
+ )
113
+
114
+
115
+ @dataclass
116
+ class AgentList:
117
+ data: list[Agent]
118
+ has_more: bool
119
+ total: int
120
+
121
+ @classmethod
122
+ def from_dict(cls, d: dict) -> "AgentList":
123
+ return cls(
124
+ data=[Agent.from_dict(a) for a in d["data"]],
125
+ has_more=d.get("hasMore", False),
126
+ total=d["total"],
127
+ )
128
+
129
+
130
+ # --- Calls ---
131
+
132
+ @dataclass
133
+ class CallTranscript:
134
+ id: str
135
+ transcript: str
136
+ response: Optional[str]
137
+ created_at: str
138
+
139
+ @classmethod
140
+ def from_dict(cls, d: dict) -> "CallTranscript":
141
+ return cls(
142
+ id=d["id"],
143
+ transcript=d["transcript"],
144
+ response=d.get("response"),
145
+ created_at=d["createdAt"],
146
+ )
147
+
148
+
149
+ @dataclass
150
+ class Call:
151
+ id: str
152
+ from_number: str
153
+ to_number: str
154
+ direction: str
155
+ status: str
156
+ started_at: str
157
+ ended_at: Optional[str]
158
+ agent_id: Optional[str]
159
+ transcripts: list[CallTranscript] = field(default_factory=list)
160
+
161
+ @classmethod
162
+ def from_dict(cls, d: dict) -> "Call":
163
+ return cls(
164
+ id=d["id"],
165
+ from_number=d["fromNumber"],
166
+ to_number=d["toNumber"],
167
+ direction=d["direction"],
168
+ status=d["status"],
169
+ started_at=d["startedAt"],
170
+ ended_at=d.get("endedAt"),
171
+ agent_id=d.get("agentId"),
172
+ transcripts=[CallTranscript.from_dict(t) for t in d.get("transcripts", [])],
173
+ )
174
+
175
+
176
+ @dataclass
177
+ class CallList:
178
+ data: list[Call]
179
+ has_more: bool
180
+ total: int
181
+
182
+ @classmethod
183
+ def from_dict(cls, d: dict) -> "CallList":
184
+ return cls(
185
+ data=[Call.from_dict(c) for c in d["data"]],
186
+ has_more=d["hasMore"],
187
+ total=d["total"],
188
+ )
189
+
190
+
191
+ # --- Conversations ---
192
+
193
+ @dataclass
194
+ class ConversationMessage:
195
+ id: str
196
+ from_number: str
197
+ to_number: str
198
+ body: str
199
+ received_at: str
200
+
201
+ @classmethod
202
+ def from_dict(cls, d: dict) -> "ConversationMessage":
203
+ return cls(
204
+ id=d["id"],
205
+ from_number=d["from"],
206
+ to_number=d["to"],
207
+ body=d["body"],
208
+ received_at=d["receivedAt"],
209
+ )
210
+
211
+
212
+ @dataclass
213
+ class Conversation:
214
+ id: str
215
+ phone_number: str
216
+ participant: str
217
+ last_message_at: Optional[str]
218
+ message_count: int
219
+ agent_id: Optional[str]
220
+ messages: list[ConversationMessage] = field(default_factory=list)
221
+
222
+ @classmethod
223
+ def from_dict(cls, d: dict) -> "Conversation":
224
+ return cls(
225
+ id=d["id"],
226
+ phone_number=d["phoneNumber"],
227
+ participant=d.get("participant") or d.get("contactNumber", ""),
228
+ last_message_at=d.get("lastMessageAt"),
229
+ message_count=d.get("messageCount", 0),
230
+ agent_id=d.get("agentId"),
231
+ messages=[ConversationMessage.from_dict(m) for m in d.get("messages", [])],
232
+ )
233
+
234
+
235
+ @dataclass
236
+ class ConversationList:
237
+ data: list[Conversation]
238
+ has_more: bool
239
+ total: int
240
+
241
+ @classmethod
242
+ def from_dict(cls, d: dict) -> "ConversationList":
243
+ return cls(
244
+ data=[Conversation.from_dict(c) for c in d["data"]],
245
+ has_more=d["hasMore"],
246
+ total=d["total"],
247
+ )
248
+
249
+
250
+ # --- Webhooks ---
251
+
252
+ @dataclass
253
+ class Webhook:
254
+ id: str
255
+ url: str
256
+ secret: str
257
+ status: str
258
+ context_limit: int
259
+ created_at: str
260
+
261
+ @classmethod
262
+ def from_dict(cls, d: dict) -> "Webhook":
263
+ return cls(
264
+ id=d["id"],
265
+ url=d["url"],
266
+ secret=d["secret"],
267
+ status=d["status"],
268
+ context_limit=d["contextLimit"],
269
+ created_at=d["createdAt"],
270
+ )
271
+
272
+
273
+ @dataclass
274
+ class WebhookDelivery:
275
+ id: str
276
+ message_id: str
277
+ status: str
278
+ http_status: Optional[int]
279
+ error_message: Optional[str]
280
+ attempt_count: int
281
+ last_attempt_at: Optional[str]
282
+ next_retry_at: Optional[str]
283
+ created_at: str
284
+
285
+ @classmethod
286
+ def from_dict(cls, d: dict) -> "WebhookDelivery":
287
+ return cls(
288
+ id=d["id"],
289
+ message_id=d["messageId"],
290
+ status=d["status"],
291
+ http_status=d.get("httpStatus"),
292
+ error_message=d.get("errorMessage"),
293
+ attempt_count=d["attemptCount"],
294
+ last_attempt_at=d.get("lastAttemptAt"),
295
+ next_retry_at=d.get("nextRetryAt"),
296
+ created_at=d["createdAt"],
297
+ )
298
+
299
+
300
+ # --- Webhook event payload ---
301
+
302
+ @dataclass
303
+ class WebhookHistoryItem:
304
+ content: str
305
+ direction: str
306
+ channel: str
307
+ at: str
308
+
309
+ @classmethod
310
+ def from_dict(cls, d: dict) -> "WebhookHistoryItem":
311
+ return cls(
312
+ content=d["content"],
313
+ direction=d["direction"],
314
+ channel=d["channel"],
315
+ at=d["at"],
316
+ )
317
+
318
+
319
+ @dataclass
320
+ class WebhookEventData:
321
+ conversation_id: Optional[str]
322
+ number_id: Optional[str]
323
+ from_number: str
324
+ to_number: str
325
+ message: str
326
+ direction: str
327
+ received_at: str
328
+
329
+ @classmethod
330
+ def from_dict(cls, d: dict) -> "WebhookEventData":
331
+ return cls(
332
+ conversation_id=d.get("conversationId"),
333
+ number_id=d.get("numberId"),
334
+ from_number=d["from"],
335
+ to_number=d["to"],
336
+ message=d["message"],
337
+ direction=d["direction"],
338
+ received_at=d["receivedAt"],
339
+ )
340
+
341
+
342
+ @dataclass
343
+ class WebhookEvent:
344
+ event: str
345
+ channel: str
346
+ timestamp: str
347
+ agent_id: Optional[str]
348
+ data: WebhookEventData
349
+ recent_history: list[WebhookHistoryItem] = field(default_factory=list)
350
+ conversation_state: Optional[dict] = None
351
+
352
+ @classmethod
353
+ def from_dict(cls, d: dict) -> "WebhookEvent":
354
+ return cls(
355
+ event=d["event"],
356
+ channel=d["channel"],
357
+ timestamp=d["timestamp"],
358
+ agent_id=d.get("agentId"),
359
+ data=WebhookEventData.from_dict(d["data"]),
360
+ recent_history=[WebhookHistoryItem.from_dict(h) for h in d.get("recentHistory", [])],
361
+ conversation_state=d.get("conversationState"),
362
+ )
agentphone/webhook.py ADDED
@@ -0,0 +1,81 @@
1
+ """
2
+ Webhook signature verification.
3
+ """
4
+
5
+ from __future__ import annotations
6
+
7
+ import hashlib
8
+ import hmac
9
+ import json
10
+ from typing import Union
11
+
12
+ from .models import WebhookEvent
13
+
14
+
15
+ class WebhookVerificationError(Exception):
16
+ pass
17
+
18
+
19
+ def verify_webhook(
20
+ payload: Union[bytes, str],
21
+ signature: str,
22
+ secret: str,
23
+ ) -> None:
24
+ """
25
+ Verify an incoming AgentPhone webhook signature.
26
+
27
+ Args:
28
+ payload: Raw request body (bytes or str).
29
+ signature: Value of the X-Webhook-Signature header (e.g. "sha256=abc123").
30
+ secret: Your webhook secret from client.webhooks.get().secret
31
+
32
+ Raises:
33
+ WebhookVerificationError: If the signature is invalid.
34
+
35
+ Example::
36
+
37
+ from agentphone import verify_webhook, WebhookVerificationError
38
+
39
+ @app.post("/webhook")
40
+ async def handle(request: Request):
41
+ body = await request.body()
42
+ sig = request.headers["X-Webhook-Signature"]
43
+ try:
44
+ verify_webhook(body, sig, secret="whsec_...")
45
+ except WebhookVerificationError:
46
+ return Response(status_code=403)
47
+ ...
48
+ """
49
+ if isinstance(payload, str):
50
+ payload = payload.encode()
51
+
52
+ expected = "sha256=" + hmac.new(
53
+ secret.encode(), payload, hashlib.sha256
54
+ ).hexdigest()
55
+
56
+ if not hmac.compare_digest(expected, signature):
57
+ raise WebhookVerificationError("Invalid webhook signature")
58
+
59
+
60
+ def construct_event(
61
+ payload: Union[bytes, str],
62
+ signature: str,
63
+ secret: str,
64
+ ) -> WebhookEvent:
65
+ """
66
+ Verify the webhook signature and parse the event payload.
67
+
68
+ Returns a WebhookEvent on success, raises WebhookVerificationError if invalid.
69
+
70
+ Example::
71
+
72
+ event = construct_event(body, sig, secret="whsec_...")
73
+ if event.event == "agent.message":
74
+ print(event.data.message)
75
+ """
76
+ verify_webhook(payload, signature, secret)
77
+
78
+ if isinstance(payload, bytes):
79
+ payload = payload.decode()
80
+
81
+ return WebhookEvent.from_dict(json.loads(payload))
@@ -0,0 +1,161 @@
1
+ Metadata-Version: 2.4
2
+ Name: agentphone
3
+ Version: 0.1.0
4
+ Summary: AgentPhone Python SDK — give your AI agents phone numbers, SMS, and voice calls
5
+ Project-URL: Homepage, https://agentphone.to
6
+ Project-URL: Repository, https://github.com/AgentPhone-AI/agentphone-python
7
+ Project-URL: Documentation, https://docs.agentphone.to
8
+ Author-email: AgentPhone <hello@agentphone.to>
9
+ License: MIT
10
+ Keywords: agentphone,ai,phone,sms,telephony,voice
11
+ Classifier: Development Status :: 4 - Beta
12
+ Classifier: Intended Audience :: Developers
13
+ Classifier: License :: OSI Approved :: MIT License
14
+ Classifier: Programming Language :: Python :: 3
15
+ Classifier: Programming Language :: Python :: 3.9
16
+ Classifier: Programming Language :: Python :: 3.10
17
+ Classifier: Programming Language :: Python :: 3.11
18
+ Classifier: Programming Language :: Python :: 3.12
19
+ Classifier: Topic :: Communications :: Telephony
20
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
21
+ Requires-Python: >=3.9
22
+ Requires-Dist: requests>=2.28
23
+ Provides-Extra: async
24
+ Requires-Dist: httpx>=0.24; extra == 'async'
25
+ Provides-Extra: dev
26
+ Requires-Dist: hatch; extra == 'dev'
27
+ Requires-Dist: httpx; extra == 'dev'
28
+ Requires-Dist: pytest; extra == 'dev'
29
+ Requires-Dist: pytest-asyncio; extra == 'dev'
30
+ Description-Content-Type: text/markdown
31
+
32
+ # AgentPhone Python SDK
33
+
34
+ Official Python SDK for [AgentPhone](https://agentphone.to) — give your AI agents real phone numbers, SMS, and voice calls.
35
+
36
+ ## Installation
37
+
38
+ ```bash
39
+ pip install agentphone
40
+ ```
41
+
42
+ For async support:
43
+
44
+ ```bash
45
+ pip install agentphone[async]
46
+ ```
47
+
48
+ ## Quickstart
49
+
50
+ ```python
51
+ from agentphone import AgentPhone
52
+
53
+ client = AgentPhone(api_key="your-api-key")
54
+
55
+ # Create an agent and buy a number
56
+ agent = client.agents.create(name="My Agent")
57
+ number = client.numbers.buy(country="US", agent_id=agent.id)
58
+
59
+ # Make an AI conversation call — no webhook needed
60
+ call = client.calls.make_conversation(
61
+ agent_id=agent.id,
62
+ to_number="+14155551234",
63
+ topic="You are a friendly assistant. Ask about their day.",
64
+ initial_greeting="Hey! This is an AI calling from AgentPhone.",
65
+ )
66
+ print(call.status) # registered
67
+ ```
68
+
69
+ ## Async
70
+
71
+ ```python
72
+ from agentphone import AsyncAgentPhone
73
+
74
+ async with AsyncAgentPhone(api_key="your-api-key") as client:
75
+ numbers = await client.numbers.list()
76
+ call = await client.calls.make_conversation(...)
77
+ ```
78
+
79
+ ## Resources
80
+
81
+ | Resource | Methods |
82
+ |---|---|
83
+ | `client.numbers` | `list()`, `buy()`, `release()`, `get_messages()` |
84
+ | `client.agents` | `list()`, `create()`, `get()`, `attach_number()` |
85
+ | `client.calls` | `list()`, `get()`, `make()`, `make_conversation()` |
86
+ | `client.conversations` | `list()`, `get()` |
87
+ | `client.webhooks` | `get()`, `set()`, `delete()`, `list_deliveries()`, `test()` |
88
+
89
+ ## Webhook Verification
90
+
91
+ ```python
92
+ from agentphone import construct_event, WebhookVerificationError
93
+
94
+ @app.post("/webhook")
95
+ async def handle(request: Request):
96
+ body = await request.body()
97
+ sig = request.headers["X-Webhook-Signature"]
98
+ try:
99
+ event = construct_event(body, sig, secret="whsec_...")
100
+ except WebhookVerificationError:
101
+ return Response(status_code=403)
102
+
103
+ if event.event == "agent.message":
104
+ print(f"SMS from {event.data.from_number}: {event.data.message}")
105
+ ```
106
+
107
+ ## Error Handling
108
+
109
+ ```python
110
+ from agentphone import AgentPhoneError, AuthenticationError, NotFoundError
111
+
112
+ try:
113
+ call = client.calls.get("bad-id")
114
+ except NotFoundError:
115
+ print("Call not found")
116
+ except AgentPhoneError as e:
117
+ print(f"API error {e.status}: {e.message}")
118
+ ```
119
+
120
+ ## Publishing to PyPI
121
+
122
+ 1. **Install build tools** (one-time):
123
+ ```bash
124
+ pip install hatch twine
125
+ ```
126
+
127
+ 2. **Build** the package:
128
+ ```bash
129
+ hatch build
130
+ ```
131
+ This creates a `dist/` folder with the `.tar.gz` and `.whl` files.
132
+
133
+ 3. **Upload to PyPI**:
134
+ ```bash
135
+ twine upload dist/*
136
+ ```
137
+ You'll be prompted for your PyPI credentials. To use an API token instead:
138
+ ```bash
139
+ twine upload dist/* -u __token__ -p pypi-your-token-here
140
+ ```
141
+
142
+ 4. **Publishing a new version** — bump the version in `pyproject.toml` first:
143
+ ```toml
144
+ version = "0.1.1"
145
+ ```
146
+ Then rebuild and upload:
147
+ ```bash
148
+ hatch build && twine upload dist/*
149
+ ```
150
+
151
+ ## Requirements
152
+
153
+ - Python 3.9+
154
+ - `requests` (sync client)
155
+ - `httpx` (async client, optional)
156
+
157
+ ## Links
158
+
159
+ - [AgentPhone Dashboard](https://agentphone.to)
160
+ - [MCP Server](https://github.com/AgentPhone-AI/agentphone-mcp)
161
+ - [Node.js SDK](https://github.com/AgentPhone-AI/agentphone-node)
@@ -0,0 +1,9 @@
1
+ agentphone/__init__.py,sha256=RbW0R7PMIuvu1CvSIxucuQmISdHiQn9sVZdkTBpmJyQ,1653
2
+ agentphone/_http.py,sha256=MKxDv0UBuWxnJWtcr1Xyg5-jLq2pUxK0jNYbjg_eBFc,1059
3
+ agentphone/async_client.py,sha256=bKs9CTmnJWnBDJV0wElPXklJy-VSJlw9R-pIYbfjmmM,6978
4
+ agentphone/client.py,sha256=VvkrxZd52aTDNUJAg7iaSNKxneWwWPAcWNF1OPdlSaI,7705
5
+ agentphone/models.py,sha256=6bNICQZThDoT3r8RK93bVo3lHrw4WH4t7bALpDPb5zI,8341
6
+ agentphone/webhook.py,sha256=45Nig-cxKFOJorl1K7gK0njfIxMT4cdvoQ-QAUHee8Y,2041
7
+ agentphone-0.1.0.dist-info/METADATA,sha256=42rZUpb2bLSXryDy2yYGi4T641Duv_mtNRgRDBpVYSo,4464
8
+ agentphone-0.1.0.dist-info/WHEEL,sha256=QccIxa26bgl1E6uMy58deGWi-0aeIkkangHcxk2kWfw,87
9
+ agentphone-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.29.0
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any