wirebox 0.2.0__tar.gz → 0.4.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.
- {wirebox-0.2.0 → wirebox-0.4.0}/PKG-INFO +1 -1
- wirebox-0.4.0/examples/auto_reply.py +155 -0
- wirebox-0.4.0/examples/personal_agent_identity.py +193 -0
- wirebox-0.4.0/examples/quickstart.py +87 -0
- {wirebox-0.2.0 → wirebox-0.4.0}/pyproject.toml +1 -1
- {wirebox-0.2.0 → wirebox-0.4.0}/src/wirebox/__init__.py +19 -0
- wirebox-0.4.0/src/wirebox/_config.py +110 -0
- wirebox-0.4.0/src/wirebox/_version.py +1 -0
- {wirebox-0.2.0 → wirebox-0.4.0}/src/wirebox/async_client.py +53 -17
- {wirebox-0.2.0 → wirebox-0.4.0}/src/wirebox/client.py +51 -12
- {wirebox-0.2.0 → wirebox-0.4.0}/src/wirebox/exceptions.py +5 -0
- {wirebox-0.2.0 → wirebox-0.4.0}/src/wirebox/identity.py +134 -0
- wirebox-0.4.0/src/wirebox/phone.py +293 -0
- {wirebox-0.2.0 → wirebox-0.4.0}/src/wirebox/tunnels.py +95 -34
- {wirebox-0.2.0 → wirebox-0.4.0}/src/wirebox/types.py +381 -30
- {wirebox-0.2.0 → wirebox-0.4.0}/tests/test_async_client.py +1 -1
- wirebox-0.4.0/tests/test_config.py +102 -0
- wirebox-0.4.0/tests/test_phone.py +254 -0
- {wirebox-0.2.0 → wirebox-0.4.0}/tests/test_types.py +73 -0
- {wirebox-0.2.0 → wirebox-0.4.0}/uv.lock +2 -2
- wirebox-0.2.0/src/wirebox/_version.py +0 -1
- {wirebox-0.2.0 → wirebox-0.4.0}/.github/workflows/ci.yml +0 -0
- {wirebox-0.2.0 → wirebox-0.4.0}/.github/workflows/publish.yml +0 -0
- {wirebox-0.2.0 → wirebox-0.4.0}/.gitignore +0 -0
- {wirebox-0.2.0 → wirebox-0.4.0}/AGENTS.md +0 -0
- {wirebox-0.2.0 → wirebox-0.4.0}/LICENSE +0 -0
- {wirebox-0.2.0 → wirebox-0.4.0}/README.md +0 -0
- {wirebox-0.2.0 → wirebox-0.4.0}/src/wirebox/_http.py +0 -0
- {wirebox-0.2.0 → wirebox-0.4.0}/src/wirebox/imessage.py +0 -0
- {wirebox-0.2.0 → wirebox-0.4.0}/src/wirebox/mail.py +0 -0
- {wirebox-0.2.0 → wirebox-0.4.0}/src/wirebox/py.typed +0 -0
- {wirebox-0.2.0 → wirebox-0.4.0}/src/wirebox/verify_webhook.py +0 -0
- {wirebox-0.2.0 → wirebox-0.4.0}/src/wirebox/webhooks.py +0 -0
- {wirebox-0.2.0 → wirebox-0.4.0}/tests/conftest.py +0 -0
- {wirebox-0.2.0 → wirebox-0.4.0}/tests/test_client.py +0 -0
- {wirebox-0.2.0 → wirebox-0.4.0}/tests/test_exceptions.py +0 -0
- {wirebox-0.2.0 → wirebox-0.4.0}/tests/test_imessage.py +0 -0
- {wirebox-0.2.0 → wirebox-0.4.0}/tests/test_tunnels.py +0 -0
- {wirebox-0.2.0 → wirebox-0.4.0}/tests/test_verify_webhook.py +0 -0
- {wirebox-0.2.0 → wirebox-0.4.0}/tests/test_webhooks.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: wirebox
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.4.0
|
|
4
4
|
Summary: Official Python SDK for Wirebox — Real-world identity and communication layer for AI agents
|
|
5
5
|
Project-URL: Homepage, https://wirebox.sh
|
|
6
6
|
Project-URL: Documentation, https://docs.wirebox.sh
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
"""Wirebox Python SDK — Autonomous Auto-Reply Agent Example.
|
|
2
|
+
|
|
3
|
+
Demonstrates:
|
|
4
|
+
1. Resolving the active AgentIdentity with zero-config credentials
|
|
5
|
+
2. Ensuring an agent-scoped webhook subscription is active
|
|
6
|
+
3. Connecting an in-memory WebSocket reverse-proxy tunnel
|
|
7
|
+
4. Intercepting "email.received" events in process memory
|
|
8
|
+
5. Automatically dispatching a threaded reply via agent.reply_email()
|
|
9
|
+
|
|
10
|
+
Usage:
|
|
11
|
+
python examples/auto_reply.py
|
|
12
|
+
python examples/auto_reply.py --once # Connects, verifies health ping, and exits cleanly
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
from __future__ import annotations
|
|
16
|
+
|
|
17
|
+
import asyncio
|
|
18
|
+
import datetime
|
|
19
|
+
import json
|
|
20
|
+
import sys
|
|
21
|
+
|
|
22
|
+
import httpx
|
|
23
|
+
|
|
24
|
+
from wirebox import AsyncWirebox
|
|
25
|
+
from wirebox.exceptions import WireboxAPIError
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
async def main() -> None:
|
|
29
|
+
once_mode = "--once" in sys.argv
|
|
30
|
+
|
|
31
|
+
print("=" * 64)
|
|
32
|
+
print(" Wirebox Python SDK — Autonomous Auto-Reply Agent")
|
|
33
|
+
print("========================================================\n")
|
|
34
|
+
|
|
35
|
+
async with AsyncWirebox() as client:
|
|
36
|
+
agent = await client.get_identity()
|
|
37
|
+
|
|
38
|
+
print(f"Agent Handle: @{agent.agent_handle}")
|
|
39
|
+
print(f"Display Name: {agent.display_name or 'N/A'}")
|
|
40
|
+
print(f"Mailbox Address: {agent.mailbox.email_address}")
|
|
41
|
+
print(f"Tunnel URL: {agent.tunnel.public_url}")
|
|
42
|
+
|
|
43
|
+
target_endpoint = f"{agent.tunnel.public_url}/webhook"
|
|
44
|
+
|
|
45
|
+
# 1. Ensure webhook subscription exists
|
|
46
|
+
webhooks = await agent.list_webhooks()
|
|
47
|
+
hook = next((w for w in webhooks if w.url == target_endpoint), None)
|
|
48
|
+
ephemeral = False
|
|
49
|
+
|
|
50
|
+
if not hook:
|
|
51
|
+
print(f"\nSetting up webhook subscription for {target_endpoint}...")
|
|
52
|
+
hook = await agent.create_webhook(
|
|
53
|
+
url=target_endpoint,
|
|
54
|
+
events=["email.received", "test.ping"],
|
|
55
|
+
)
|
|
56
|
+
ephemeral = True
|
|
57
|
+
print(f"✔ Webhook registered: {hook.id}")
|
|
58
|
+
else:
|
|
59
|
+
print(f"✔ Using existing webhook subscription: {hook.id}")
|
|
60
|
+
|
|
61
|
+
# 2. In-memory event handler
|
|
62
|
+
async def handle_request(request: httpx.Request) -> httpx.Response:
|
|
63
|
+
now = datetime.datetime.now().strftime("%H:%M:%S")
|
|
64
|
+
|
|
65
|
+
try:
|
|
66
|
+
payload = json.loads(request.content.decode("utf-8"))
|
|
67
|
+
except Exception:
|
|
68
|
+
payload = {}
|
|
69
|
+
|
|
70
|
+
event_type = payload.get("event_type") or payload.get("type") or "unknown"
|
|
71
|
+
|
|
72
|
+
if event_type == "test.ping":
|
|
73
|
+
print(f"[{now}] 🏓 Ping event received. Returning 200 OK.")
|
|
74
|
+
return httpx.Response(200, json={"pong": True})
|
|
75
|
+
|
|
76
|
+
if event_type == "email.received":
|
|
77
|
+
data = payload.get("data") or {}
|
|
78
|
+
incoming = data.get("message") or {}
|
|
79
|
+
sender = incoming.get("from") or incoming.get("from_address") or "Unknown"
|
|
80
|
+
subject = incoming.get("subject") or "(no subject)"
|
|
81
|
+
snippet = incoming.get("snippet") or incoming.get("preview") or ""
|
|
82
|
+
msg_id = incoming.get("id")
|
|
83
|
+
|
|
84
|
+
print(f"\n[{now}] 📬 Email received!")
|
|
85
|
+
print(f" From: {sender}")
|
|
86
|
+
print(f" Subject: {subject}")
|
|
87
|
+
if snippet:
|
|
88
|
+
print(f" Snippet: {snippet[:80]}")
|
|
89
|
+
|
|
90
|
+
if msg_id:
|
|
91
|
+
try:
|
|
92
|
+
print(" ⚡ Dispatching automated reply via agent.reply_email()...")
|
|
93
|
+
reply = await agent.reply_email(
|
|
94
|
+
msg_id,
|
|
95
|
+
text=(
|
|
96
|
+
f"Hello!\n\n"
|
|
97
|
+
f"This is an automated response from @{agent.agent_handle} "
|
|
98
|
+
f"powered by the Wirebox Python SDK.\n\n"
|
|
99
|
+
f'I have received your email: "{subject}".\n\n'
|
|
100
|
+
f"All systems operational!"
|
|
101
|
+
),
|
|
102
|
+
)
|
|
103
|
+
reply_id = reply.id or reply.message_id
|
|
104
|
+
print(f" ✔ Reply dispatched successfully! ID: {reply_id}")
|
|
105
|
+
except WireboxAPIError as reply_err:
|
|
106
|
+
print(f" ❌ Failed to reply: {reply_err}")
|
|
107
|
+
else:
|
|
108
|
+
print(f"[{now}] ℹ️ Received event: {event_type}")
|
|
109
|
+
|
|
110
|
+
return httpx.Response(200, json={"received": True})
|
|
111
|
+
|
|
112
|
+
# 3. Connect in-memory tunnel session
|
|
113
|
+
print("\nConnecting in-memory tunnel gateway...")
|
|
114
|
+
session = await agent.connect_tunnel(handler=handle_request)
|
|
115
|
+
print("✔ Tunnel connected and ready to receive real-world events!")
|
|
116
|
+
print("\n💡 Test this agent now by sending an email to:")
|
|
117
|
+
print(f" 👉 {agent.mailbox.email_address}")
|
|
118
|
+
|
|
119
|
+
if once_mode:
|
|
120
|
+
print("\n[--once flag detected] Testing tunnel ping before exit...")
|
|
121
|
+
# Fire an internal test ping if supported
|
|
122
|
+
try:
|
|
123
|
+
await client.webhooks.test(hook.id)
|
|
124
|
+
print("✔ Webhook test ping successfully delivered!")
|
|
125
|
+
except Exception as test_err:
|
|
126
|
+
print(f"ℹ️ Ping status: {test_err}")
|
|
127
|
+
|
|
128
|
+
print("\nDisconnecting tunnel session...")
|
|
129
|
+
await session.close()
|
|
130
|
+
if ephemeral and hook:
|
|
131
|
+
print(f"Removing ephemeral webhook {hook.id}...")
|
|
132
|
+
await client.webhooks.delete(hook.id)
|
|
133
|
+
print("[✓] Auto-reply test cycle completed successfully!")
|
|
134
|
+
return
|
|
135
|
+
|
|
136
|
+
print("\nWaiting for incoming events (Press Ctrl+C to exit)...\n")
|
|
137
|
+
|
|
138
|
+
try:
|
|
139
|
+
await session.wait_closed()
|
|
140
|
+
finally:
|
|
141
|
+
print("\nDisconnecting tunnel session...")
|
|
142
|
+
await session.close()
|
|
143
|
+
if ephemeral and hook:
|
|
144
|
+
print(f"Removing ephemeral webhook {hook.id}...")
|
|
145
|
+
await client.webhooks.delete(hook.id)
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
if __name__ == "__main__":
|
|
149
|
+
try:
|
|
150
|
+
asyncio.run(main())
|
|
151
|
+
except KeyboardInterrupt:
|
|
152
|
+
print("\nShutdown complete.")
|
|
153
|
+
except Exception as exc:
|
|
154
|
+
print(f"\n[!] Error: {exc}", file=sys.stderr)
|
|
155
|
+
sys.exit(1)
|
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
"""Wirebox Python SDK — Personal Agent Identity Layer.
|
|
2
|
+
|
|
3
|
+
Demonstrates how an AI platform or agent developer uses Wirebox as the
|
|
4
|
+
foundational IDENTITY and REAL-WORLD COMMUNICATION layer for autonomous agents.
|
|
5
|
+
|
|
6
|
+
Key Architectural Principle:
|
|
7
|
+
- Wirebox does NOT implement agent reasoning, LLM orchestration, or task planning.
|
|
8
|
+
- Wirebox provides the physical IDENTITY INFRASTRUCTURE that equips agents with:
|
|
9
|
+
1. Identity Presence: Unique global agent handle (@handle) and tenant isolation
|
|
10
|
+
2. Mailbox Infrastructure: Dedicated email address ({handle}@wireboxmail.com)
|
|
11
|
+
3. Telecom & Messaging: Dedicated carrier phone numbers, SMS, and Apple iMessage
|
|
12
|
+
4. Edge Gateway (Tunnel): Reverse-proxy WebSocket tunnel streaming real-world
|
|
13
|
+
inbound events directly into the agent's runtime process
|
|
14
|
+
|
|
15
|
+
Usage:
|
|
16
|
+
python examples/personal_agent_identity.py
|
|
17
|
+
python examples/personal_agent_identity.py --once # Self-test cycle and exit
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
from __future__ import annotations
|
|
21
|
+
|
|
22
|
+
import asyncio
|
|
23
|
+
import os
|
|
24
|
+
import sys
|
|
25
|
+
import time
|
|
26
|
+
|
|
27
|
+
from wirebox import AsyncWirebox
|
|
28
|
+
from wirebox.exceptions import WireboxAPIError
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
async def main() -> None:
|
|
32
|
+
once_mode = "--once" in sys.argv
|
|
33
|
+
|
|
34
|
+
print("=" * 64)
|
|
35
|
+
print(" Wirebox Python SDK — Personal Agent Identity Layer")
|
|
36
|
+
print(" Identity & Communication Infrastructure for AI Agents")
|
|
37
|
+
print("=" * 64 + "\n")
|
|
38
|
+
|
|
39
|
+
async with AsyncWirebox() as client:
|
|
40
|
+
# ----------------------------------------------------------------------
|
|
41
|
+
# Phase 1: Identity Provisioning & Resolution
|
|
42
|
+
# ----------------------------------------------------------------------
|
|
43
|
+
print("[Phase 1: Identity Provisioning & Resolution]")
|
|
44
|
+
whoami = await client.whoami()
|
|
45
|
+
is_identity_scoped = bool(whoami.scoped_identity_id)
|
|
46
|
+
|
|
47
|
+
if is_identity_scoped:
|
|
48
|
+
print("✔ Operating in Identity-Scoped Mode (Strict Tenant Isolation)")
|
|
49
|
+
identity = await client.get_identity()
|
|
50
|
+
print(
|
|
51
|
+
f"✔ Loaded active agent identity: @{identity.agent_handle} "
|
|
52
|
+
f"({identity.display_name or 'N/A'})\n"
|
|
53
|
+
)
|
|
54
|
+
else:
|
|
55
|
+
print("✔ Operating in Platform Admin Mode")
|
|
56
|
+
# Reuse existing identity if available to conserve tenant quotas
|
|
57
|
+
existing_agents = await client.list_identities(limit=1)
|
|
58
|
+
if existing_agents:
|
|
59
|
+
identity = existing_agents[0]
|
|
60
|
+
print(
|
|
61
|
+
f"✔ Reusing active agent identity: @{identity.agent_handle} "
|
|
62
|
+
f"({identity.display_name or 'N/A'})\n"
|
|
63
|
+
)
|
|
64
|
+
else:
|
|
65
|
+
agent_handle = f"assistant-{int(time.time()) % 10000}"
|
|
66
|
+
print(f"Provisioning new agent identity container: @{agent_handle}...")
|
|
67
|
+
identity = await client.create_identity(
|
|
68
|
+
agent_handle,
|
|
69
|
+
display_name="Personal Travel Coordinator",
|
|
70
|
+
description="Dedicated real-world identity for autonomous travel coordination",
|
|
71
|
+
)
|
|
72
|
+
print(f"✔ Identity container provisioned! ID: {identity.id}\n")
|
|
73
|
+
|
|
74
|
+
# ----------------------------------------------------------------------
|
|
75
|
+
# Phase 2: Inspect Communication Assets Bound to this Identity
|
|
76
|
+
# ----------------------------------------------------------------------
|
|
77
|
+
print("-" * 64)
|
|
78
|
+
print(f" Identity Assets Bound to @{identity.agent_handle}")
|
|
79
|
+
print("-" * 64)
|
|
80
|
+
print(f" 📧 Mailbox: {identity.mailbox.email_address}")
|
|
81
|
+
print(f" 🌐 Edge Tunnel: {identity.tunnel.public_url}")
|
|
82
|
+
print(" 📱 Messaging: Configured for Carrier SMS & Phone Numbers")
|
|
83
|
+
print("-" * 64 + "\n")
|
|
84
|
+
|
|
85
|
+
# ----------------------------------------------------------------------
|
|
86
|
+
# Phase 3: Phone & Telecom Infrastructure
|
|
87
|
+
# ----------------------------------------------------------------------
|
|
88
|
+
print("[Phase 2: Phone & Telecom Infrastructure]")
|
|
89
|
+
try:
|
|
90
|
+
phone_num = await identity.get_phone_number()
|
|
91
|
+
print(
|
|
92
|
+
f"✔ Bound carrier phone number: {phone_num.phone_number} "
|
|
93
|
+
f"(Status: {phone_num.status})\n"
|
|
94
|
+
)
|
|
95
|
+
except Exception:
|
|
96
|
+
print(f"Requesting carrier phone number allocation for @{identity.agent_handle}...")
|
|
97
|
+
try:
|
|
98
|
+
new_phone = await client.phone.numbers.provision(
|
|
99
|
+
identity.agent_handle,
|
|
100
|
+
country_code="US",
|
|
101
|
+
type="local",
|
|
102
|
+
)
|
|
103
|
+
print(
|
|
104
|
+
f"✔ Carrier phone number provisioned: {new_phone.phone_number} "
|
|
105
|
+
f"({new_phone.type})\n"
|
|
106
|
+
)
|
|
107
|
+
except WireboxAPIError as err:
|
|
108
|
+
if err.status_code == 403 and "phone_provisioning_restricted" in str(err):
|
|
109
|
+
print(
|
|
110
|
+
"ℹ️ [Telecom Gate] Phone number provisioning is currently in Private Beta."
|
|
111
|
+
)
|
|
112
|
+
print(
|
|
113
|
+
" (Once organization is whitelisted, dedicated US/CA cellular numbers activate instantly)\n"
|
|
114
|
+
)
|
|
115
|
+
elif err.status_code == 409:
|
|
116
|
+
print("✔ Agent already possesses an active phone number.\n")
|
|
117
|
+
else:
|
|
118
|
+
print(f"ℹ️ [Telecom Notice] {err}\n")
|
|
119
|
+
|
|
120
|
+
# ----------------------------------------------------------------------
|
|
121
|
+
# Phase 4: Outbound Real-World Communication (Using Identity Mailbox)
|
|
122
|
+
# ----------------------------------------------------------------------
|
|
123
|
+
print("[Phase 3: Outbound Communication via Identity Mailbox]")
|
|
124
|
+
print("Dispatching travel reservation confirmation from agent's dedicated identity...")
|
|
125
|
+
|
|
126
|
+
recipient = os.environ.get("AGENT_RECIPIENT") or identity.mailbox.email_address
|
|
127
|
+
try:
|
|
128
|
+
email_result = await identity.send_email(
|
|
129
|
+
to=recipient,
|
|
130
|
+
subject="[Confirmed] Flight SFO -> HND Reservation for Alex",
|
|
131
|
+
text="\n".join(
|
|
132
|
+
[
|
|
133
|
+
"Hello Alex,",
|
|
134
|
+
"",
|
|
135
|
+
"Your autonomous travel assistant has finalized your booking:",
|
|
136
|
+
" - Flight: NH 107 (San Francisco -> Tokyo Haneda)",
|
|
137
|
+
" - Status: Confirmed & Ticketed",
|
|
138
|
+
" - Booking Ref: WB-9921B",
|
|
139
|
+
"",
|
|
140
|
+
"This notification was dispatched using the agent's dedicated Wirebox identity mailbox.",
|
|
141
|
+
"You can reply directly to this thread anytime to request adjustments.",
|
|
142
|
+
"",
|
|
143
|
+
f"— {identity.display_name or 'Agent'} (@{identity.agent_handle})",
|
|
144
|
+
]
|
|
145
|
+
),
|
|
146
|
+
)
|
|
147
|
+
|
|
148
|
+
print("✔ Outbound email dispatched!")
|
|
149
|
+
print(f" Message ID: {email_result.id or email_result.message_id}")
|
|
150
|
+
print(f" Recipient: {recipient}\n")
|
|
151
|
+
except WireboxAPIError as err:
|
|
152
|
+
if "not a verified address" in str(err):
|
|
153
|
+
print(
|
|
154
|
+
f"ℹ️ [Sandbox Notice] Recipient '{recipient}' is not verified in sandbox mode."
|
|
155
|
+
)
|
|
156
|
+
print(" (Set AGENT_RECIPIENT=<your-verified-email> to test external delivery)\n")
|
|
157
|
+
else:
|
|
158
|
+
raise
|
|
159
|
+
|
|
160
|
+
# ----------------------------------------------------------------------
|
|
161
|
+
# Phase 5: Inbound Edge Gateway (Ingesting Real-World Signals into Agent)
|
|
162
|
+
# ----------------------------------------------------------------------
|
|
163
|
+
print("[Phase 4: Inbound Edge Gateway (Mail & Phone Events)]")
|
|
164
|
+
target_endpoint = f"{identity.tunnel.public_url}/webhook"
|
|
165
|
+
|
|
166
|
+
hook = await identity.create_webhook(
|
|
167
|
+
url=target_endpoint,
|
|
168
|
+
events=["email.received", "test.ping", "sms.received"],
|
|
169
|
+
)
|
|
170
|
+
print(f"✔ Webhook route bound: {hook.id} -> {target_endpoint}\n")
|
|
171
|
+
|
|
172
|
+
print("----------------------------------------------------------------")
|
|
173
|
+
print(" Personal Agent Identity Container Ready")
|
|
174
|
+
print(f" Agent Handle: @{identity.agent_handle}")
|
|
175
|
+
print(f" Email Address: {identity.mailbox.email_address}")
|
|
176
|
+
print(f" Gateway Tunnel: {identity.tunnel.public_url}")
|
|
177
|
+
print("----------------------------------------------------------------")
|
|
178
|
+
|
|
179
|
+
if once_mode:
|
|
180
|
+
print("\n[--once flag detected] Test cycle complete. Exiting cleanly.")
|
|
181
|
+
return
|
|
182
|
+
|
|
183
|
+
print("\nAgent identity infrastructure is operational. Press Ctrl+C to stop.\n")
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
if __name__ == "__main__":
|
|
187
|
+
try:
|
|
188
|
+
asyncio.run(main())
|
|
189
|
+
except KeyboardInterrupt:
|
|
190
|
+
print("\nShutdown requested by user.")
|
|
191
|
+
except Exception as exc:
|
|
192
|
+
print(f"\n[!] Error: {exc}", file=sys.stderr)
|
|
193
|
+
sys.exit(1)
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
"""Wirebox Python SDK — Quickstart Example.
|
|
2
|
+
|
|
3
|
+
Demonstrates zero-config initialization with Wirebox(), caller introspection,
|
|
4
|
+
identity discovery, sending an outbound email, and inspecting inbox messages.
|
|
5
|
+
|
|
6
|
+
Usage:
|
|
7
|
+
python -m examples.quickstart
|
|
8
|
+
# or
|
|
9
|
+
python examples/quickstart.py
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
import os
|
|
15
|
+
import sys
|
|
16
|
+
|
|
17
|
+
from wirebox import Wirebox
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def main() -> None:
|
|
21
|
+
print("=" * 60)
|
|
22
|
+
print("Wirebox Python SDK - Quickstart")
|
|
23
|
+
print("=" * 60)
|
|
24
|
+
|
|
25
|
+
# 1. Zero-config client initialization
|
|
26
|
+
# Automatically resolves API key from ~/.wirebox/credentials or ~/.wirebox/config
|
|
27
|
+
client = Wirebox()
|
|
28
|
+
|
|
29
|
+
# 2. Caller introspection via /v1/me
|
|
30
|
+
who = client.whoami()
|
|
31
|
+
print(f"\n[1] Authenticated Organization: {who.organization.name} ({who.organization.id})")
|
|
32
|
+
print(f" Billing Plan: {who.organization.billing_plan}")
|
|
33
|
+
print(f" Active Actor: {who.auth.actor_id} (Type: {who.auth.type})")
|
|
34
|
+
print(f" Usage: {who.usage.agents_count}/{who.usage.agents_limit} agents provisioned")
|
|
35
|
+
|
|
36
|
+
# 3. Retrieve or discover active agent identity
|
|
37
|
+
agents = client.list_identities(limit=10)
|
|
38
|
+
if not agents:
|
|
39
|
+
print("\n[!] No active agent identities found. Creating a starter identity...")
|
|
40
|
+
agent = client.create_identity("assistant-bot", display_name="AI Assistant")
|
|
41
|
+
else:
|
|
42
|
+
agent = agents[0]
|
|
43
|
+
|
|
44
|
+
print(f"\n[2] Active Agent Identity: @{agent.agent_handle}")
|
|
45
|
+
print(f" Display Name: {agent.display_name or 'N/A'}")
|
|
46
|
+
print(f" Primary Mailbox: {agent.mailbox.email_address}")
|
|
47
|
+
print(f" Network Tunnel: {agent.tunnel.public_url}")
|
|
48
|
+
|
|
49
|
+
# 4. Dispatch a verified outbound email
|
|
50
|
+
recipient = os.environ.get("AGENT_RECIPIENT") or agent.mailbox.email_address
|
|
51
|
+
print(f"\n[3] Dispatching verified outbound message to: {recipient}...")
|
|
52
|
+
|
|
53
|
+
dispatch_result = agent.send_email(
|
|
54
|
+
to=recipient,
|
|
55
|
+
subject="[Wirebox Quickstart] Autonomous Agent Online",
|
|
56
|
+
text=(
|
|
57
|
+
f"Hello!\n\n"
|
|
58
|
+
f"Your personal agent @{agent.agent_handle} is fully provisioned with real-world infrastructure:\n"
|
|
59
|
+
f"- Email Inbox: {agent.mailbox.email_address}\n"
|
|
60
|
+
f"- Edge Tunnel: {agent.tunnel.public_url}\n\n"
|
|
61
|
+
f"Sent autonomously via the Wirebox Edge Core API."
|
|
62
|
+
),
|
|
63
|
+
)
|
|
64
|
+
|
|
65
|
+
print(f" Message Dispatched! ID: {dispatch_result.id}")
|
|
66
|
+
if dispatch_result.thread_id:
|
|
67
|
+
print(f" Thread ID: {dispatch_result.thread_id}")
|
|
68
|
+
|
|
69
|
+
# 5. Inspect mailbox messages
|
|
70
|
+
print(f"\n[4] Querying recent messages for {agent.mailbox.email_address}...")
|
|
71
|
+
messages = agent.list_messages(limit=5)
|
|
72
|
+
print(f" Found {len(messages)} recent message(s):")
|
|
73
|
+
for msg in messages:
|
|
74
|
+
snippet_text = (msg.snippet or msg.preview)[:60]
|
|
75
|
+
print(f" - [{msg.direction.upper()}] from={msg.from_address}")
|
|
76
|
+
print(f" subject: {msg.subject}")
|
|
77
|
+
print(f" snippet: {snippet_text}...")
|
|
78
|
+
|
|
79
|
+
print("\n[✓] Wirebox Python Quickstart completed successfully!\n")
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
if __name__ == "__main__":
|
|
83
|
+
try:
|
|
84
|
+
main()
|
|
85
|
+
except Exception as exc:
|
|
86
|
+
print(f"\n[!] Error: {exc}", file=sys.stderr)
|
|
87
|
+
sys.exit(1)
|
|
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "wirebox"
|
|
7
|
-
version = "0.
|
|
7
|
+
version = "0.4.0"
|
|
8
8
|
description = "Official Python SDK for Wirebox — Real-world identity and communication layer for AI agents"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
requires-python = ">=3.11"
|
|
@@ -20,6 +20,7 @@ from wirebox.exceptions import (
|
|
|
20
20
|
)
|
|
21
21
|
from wirebox.identity import AgentIdentity, AsyncAgentIdentity
|
|
22
22
|
from wirebox.imessage import AsyncIMessageClient, IMessageClient
|
|
23
|
+
from wirebox.phone import AsyncPhoneClient, PhoneClient
|
|
23
24
|
from wirebox.tunnels import TunnelSession
|
|
24
25
|
from wirebox.types import (
|
|
25
26
|
EmailMessage,
|
|
@@ -29,9 +30,15 @@ from wirebox.types import (
|
|
|
29
30
|
ImessageConversationLastMessage,
|
|
30
31
|
ImessageMessage,
|
|
31
32
|
ImessageRouterInfo,
|
|
33
|
+
ListPhoneMessagesResult,
|
|
34
|
+
ListPhoneNumbersResult,
|
|
32
35
|
MailboxSummary,
|
|
33
36
|
MessageAttachmentSummary,
|
|
34
37
|
MessageSummary,
|
|
38
|
+
PhoneMediaItem,
|
|
39
|
+
PhoneMessage,
|
|
40
|
+
PhoneNumber,
|
|
41
|
+
PhoneNumberCapabilities,
|
|
35
42
|
SendEmailAttachment,
|
|
36
43
|
SendEmailResult,
|
|
37
44
|
SendImessageResult,
|
|
@@ -44,8 +51,10 @@ from wirebox.types import (
|
|
|
44
51
|
WebhookStatus,
|
|
45
52
|
WebhookTestResult,
|
|
46
53
|
WhoamiApiKey,
|
|
54
|
+
WhoamiAuth,
|
|
47
55
|
WhoamiOrganization,
|
|
48
56
|
WhoamiResult,
|
|
57
|
+
WhoamiUsage,
|
|
49
58
|
)
|
|
50
59
|
from wirebox.verify_webhook import verify_webhook
|
|
51
60
|
|
|
@@ -63,6 +72,8 @@ __all__ = [
|
|
|
63
72
|
"AsyncAgentIdentity",
|
|
64
73
|
"IMessageClient",
|
|
65
74
|
"AsyncIMessageClient",
|
|
75
|
+
"PhoneClient",
|
|
76
|
+
"AsyncPhoneClient",
|
|
66
77
|
"TunnelSession",
|
|
67
78
|
"verify_webhook",
|
|
68
79
|
# Exceptions
|
|
@@ -89,6 +100,12 @@ __all__ = [
|
|
|
89
100
|
"ImessageConversation",
|
|
90
101
|
"ImessageConversationLastMessage",
|
|
91
102
|
"ImessageMessage",
|
|
103
|
+
"PhoneNumberCapabilities",
|
|
104
|
+
"PhoneNumber",
|
|
105
|
+
"PhoneMediaItem",
|
|
106
|
+
"PhoneMessage",
|
|
107
|
+
"ListPhoneNumbersResult",
|
|
108
|
+
"ListPhoneMessagesResult",
|
|
92
109
|
"Tunnel",
|
|
93
110
|
"TunnelClientTelemetry",
|
|
94
111
|
"Webhook",
|
|
@@ -99,5 +116,7 @@ __all__ = [
|
|
|
99
116
|
"WebhookStatus",
|
|
100
117
|
"WhoamiOrganization",
|
|
101
118
|
"WhoamiApiKey",
|
|
119
|
+
"WhoamiAuth",
|
|
120
|
+
"WhoamiUsage",
|
|
102
121
|
"WhoamiResult",
|
|
103
122
|
]
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
"""Wirebox Python SDK — Client Configuration and Credentials Resolver.
|
|
2
|
+
|
|
3
|
+
Resolves API key and base URL from:
|
|
4
|
+
1. Explicit constructor arguments
|
|
5
|
+
2. Environment variables (WIREBOX_API_KEY, WIREBOX_BASE_URL)
|
|
6
|
+
3. ~/.wirebox/credentials or ~/.wirebox/config file
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import json
|
|
12
|
+
import os
|
|
13
|
+
from pathlib import Path
|
|
14
|
+
|
|
15
|
+
from wirebox._http import DEFAULT_BASE_URL
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def _get_config_search_paths() -> list[Path]:
|
|
19
|
+
custom_cred = os.environ.get("WIREBOX_CREDENTIALS_PATH")
|
|
20
|
+
custom_cfg = os.environ.get("WIREBOX_CONFIG_PATH")
|
|
21
|
+
if custom_cred or custom_cfg:
|
|
22
|
+
paths: list[Path] = []
|
|
23
|
+
if custom_cred:
|
|
24
|
+
paths.append(Path(custom_cred).expanduser())
|
|
25
|
+
if custom_cfg:
|
|
26
|
+
paths.append(Path(custom_cfg).expanduser())
|
|
27
|
+
return paths
|
|
28
|
+
|
|
29
|
+
home_dir = os.environ.get("WIREBOX_HOME")
|
|
30
|
+
base_dir = Path(home_dir).expanduser() / ".wirebox" if home_dir else Path.home() / ".wirebox"
|
|
31
|
+
|
|
32
|
+
return [base_dir / "credentials", base_dir / "config"]
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def _read_config_file(path: Path) -> dict[str, str]:
|
|
36
|
+
try:
|
|
37
|
+
text = path.read_text(encoding="utf-8").strip()
|
|
38
|
+
except OSError:
|
|
39
|
+
return {}
|
|
40
|
+
|
|
41
|
+
if not text:
|
|
42
|
+
return {}
|
|
43
|
+
|
|
44
|
+
# 1. Try JSON format
|
|
45
|
+
if text.startswith("{"):
|
|
46
|
+
try:
|
|
47
|
+
parsed = json.loads(text)
|
|
48
|
+
if isinstance(parsed, dict):
|
|
49
|
+
return {str(k): str(v) for k, v in parsed.items() if v is not None}
|
|
50
|
+
except Exception:
|
|
51
|
+
pass
|
|
52
|
+
|
|
53
|
+
out: dict[str, str] = {}
|
|
54
|
+
for line in text.splitlines():
|
|
55
|
+
line = line.strip()
|
|
56
|
+
if not line or line.startswith("#"):
|
|
57
|
+
continue
|
|
58
|
+
if "=" in line:
|
|
59
|
+
key, _, val = line.partition("=")
|
|
60
|
+
out[key.strip()] = val.strip().strip("\"'")
|
|
61
|
+
elif ":" in line:
|
|
62
|
+
key, _, val = line.partition(":")
|
|
63
|
+
out[key.strip()] = val.strip().strip("\"'")
|
|
64
|
+
elif line.startswith("wb_live_") or line.startswith("wb_test_"):
|
|
65
|
+
out["api_key"] = line
|
|
66
|
+
|
|
67
|
+
return out
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def resolve_client_settings(
|
|
71
|
+
*,
|
|
72
|
+
api_key: str | None = None,
|
|
73
|
+
base_url: str | None = None,
|
|
74
|
+
) -> tuple[str | None, str]:
|
|
75
|
+
"""Resolves (api_key, base_url) with fallback: explicit -> env -> file."""
|
|
76
|
+
# 1. Resolve API key
|
|
77
|
+
resolved_key = api_key.strip() if api_key and api_key.strip() else None
|
|
78
|
+
if not resolved_key:
|
|
79
|
+
env_key = os.environ.get("WIREBOX_API_KEY")
|
|
80
|
+
if env_key and env_key.strip():
|
|
81
|
+
resolved_key = env_key.strip()
|
|
82
|
+
|
|
83
|
+
# 2. Resolve Base URL
|
|
84
|
+
resolved_url = base_url.strip().rstrip("/") if base_url and base_url.strip() else None
|
|
85
|
+
if not resolved_url:
|
|
86
|
+
env_url = os.environ.get("WIREBOX_BASE_URL")
|
|
87
|
+
if env_url and env_url.strip():
|
|
88
|
+
resolved_url = env_url.strip().rstrip("/")
|
|
89
|
+
|
|
90
|
+
# 3. If either is still missing, search config files
|
|
91
|
+
if not resolved_key or not resolved_url:
|
|
92
|
+
for file_path in _get_config_search_paths():
|
|
93
|
+
cfg = _read_config_file(file_path)
|
|
94
|
+
if not cfg:
|
|
95
|
+
continue
|
|
96
|
+
|
|
97
|
+
if not resolved_key:
|
|
98
|
+
file_key = cfg.get("api_key") or cfg.get("apiKey") or cfg.get("token")
|
|
99
|
+
if file_key and file_key.strip():
|
|
100
|
+
resolved_key = file_key.strip()
|
|
101
|
+
|
|
102
|
+
if not resolved_url:
|
|
103
|
+
file_url = cfg.get("base_url") or cfg.get("baseUrl")
|
|
104
|
+
if file_url and file_url.strip():
|
|
105
|
+
resolved_url = file_url.strip().rstrip("/")
|
|
106
|
+
|
|
107
|
+
if resolved_key and resolved_url:
|
|
108
|
+
break
|
|
109
|
+
|
|
110
|
+
return (resolved_key, resolved_url or DEFAULT_BASE_URL)
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "0.4.0"
|