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 +78 -0
- agentphone/_http.py +44 -0
- agentphone/async_client.py +210 -0
- agentphone/client.py +238 -0
- agentphone/models.py +362 -0
- agentphone/webhook.py +81 -0
- agentphone-0.1.0.dist-info/METADATA +161 -0
- agentphone-0.1.0.dist-info/RECORD +9 -0
- agentphone-0.1.0.dist-info/WHEEL +4 -0
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,,
|