adcp 0.1.2__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.
- adcp/__init__.py +18 -0
- adcp/client.py +493 -0
- adcp/protocols/__init__.py +7 -0
- adcp/protocols/a2a.py +159 -0
- adcp/protocols/base.py +38 -0
- adcp/protocols/mcp.py +101 -0
- adcp/types/__init__.py +21 -0
- adcp/types/core.py +99 -0
- adcp/utils/__init__.py +5 -0
- adcp/utils/operation_id.py +13 -0
- adcp-0.1.2.dist-info/METADATA +276 -0
- adcp-0.1.2.dist-info/RECORD +15 -0
- adcp-0.1.2.dist-info/WHEEL +5 -0
- adcp-0.1.2.dist-info/licenses/LICENSE +17 -0
- adcp-0.1.2.dist-info/top_level.txt +1 -0
adcp/__init__.py
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
"""
|
|
2
|
+
AdCP Python Client Library
|
|
3
|
+
|
|
4
|
+
Official Python client for the Ad Context Protocol (AdCP).
|
|
5
|
+
Supports both A2A and MCP protocols with full type safety.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from adcp.client import ADCPClient, ADCPMultiAgentClient
|
|
9
|
+
from adcp.types.core import AgentConfig, TaskResult, WebhookMetadata
|
|
10
|
+
|
|
11
|
+
__version__ = "0.1.2"
|
|
12
|
+
__all__ = [
|
|
13
|
+
"ADCPClient",
|
|
14
|
+
"ADCPMultiAgentClient",
|
|
15
|
+
"AgentConfig",
|
|
16
|
+
"TaskResult",
|
|
17
|
+
"WebhookMetadata",
|
|
18
|
+
]
|
adcp/client.py
ADDED
|
@@ -0,0 +1,493 @@
|
|
|
1
|
+
"""Main client classes for AdCP."""
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import os
|
|
5
|
+
from collections.abc import Callable
|
|
6
|
+
from datetime import datetime
|
|
7
|
+
from typing import Any
|
|
8
|
+
from uuid import uuid4
|
|
9
|
+
|
|
10
|
+
from adcp.protocols.a2a import A2AAdapter
|
|
11
|
+
from adcp.protocols.base import ProtocolAdapter
|
|
12
|
+
from adcp.protocols.mcp import MCPAdapter
|
|
13
|
+
from adcp.types.core import (
|
|
14
|
+
Activity,
|
|
15
|
+
ActivityType,
|
|
16
|
+
AgentConfig,
|
|
17
|
+
Protocol,
|
|
18
|
+
TaskResult,
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def create_operation_id() -> str:
|
|
23
|
+
"""Generate a unique operation ID."""
|
|
24
|
+
return f"op_{uuid4().hex[:12]}"
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class ADCPClient:
|
|
28
|
+
"""Client for interacting with a single AdCP agent."""
|
|
29
|
+
|
|
30
|
+
def __init__(
|
|
31
|
+
self,
|
|
32
|
+
agent_config: AgentConfig,
|
|
33
|
+
webhook_url_template: str | None = None,
|
|
34
|
+
webhook_secret: str | None = None,
|
|
35
|
+
on_activity: Callable[[Activity], None] | None = None,
|
|
36
|
+
):
|
|
37
|
+
"""
|
|
38
|
+
Initialize ADCP client for a single agent.
|
|
39
|
+
|
|
40
|
+
Args:
|
|
41
|
+
agent_config: Agent configuration
|
|
42
|
+
webhook_url_template: Template for webhook URLs with {agent_id},
|
|
43
|
+
{task_type}, {operation_id}
|
|
44
|
+
webhook_secret: Secret for webhook signature verification
|
|
45
|
+
on_activity: Callback for activity events
|
|
46
|
+
"""
|
|
47
|
+
self.agent_config = agent_config
|
|
48
|
+
self.webhook_url_template = webhook_url_template
|
|
49
|
+
self.webhook_secret = webhook_secret
|
|
50
|
+
self.on_activity = on_activity
|
|
51
|
+
|
|
52
|
+
# Initialize protocol adapter
|
|
53
|
+
self.adapter: ProtocolAdapter
|
|
54
|
+
if agent_config.protocol == Protocol.A2A:
|
|
55
|
+
self.adapter = A2AAdapter(agent_config)
|
|
56
|
+
elif agent_config.protocol == Protocol.MCP:
|
|
57
|
+
self.adapter = MCPAdapter(agent_config)
|
|
58
|
+
else:
|
|
59
|
+
raise ValueError(f"Unsupported protocol: {agent_config.protocol}")
|
|
60
|
+
|
|
61
|
+
def get_webhook_url(self, task_type: str, operation_id: str) -> str:
|
|
62
|
+
"""Generate webhook URL for a task."""
|
|
63
|
+
if not self.webhook_url_template:
|
|
64
|
+
raise ValueError("webhook_url_template not configured")
|
|
65
|
+
|
|
66
|
+
return self.webhook_url_template.format(
|
|
67
|
+
agent_id=self.agent_config.id,
|
|
68
|
+
task_type=task_type,
|
|
69
|
+
operation_id=operation_id,
|
|
70
|
+
)
|
|
71
|
+
|
|
72
|
+
def _emit_activity(self, activity: Activity) -> None:
|
|
73
|
+
"""Emit activity event."""
|
|
74
|
+
if self.on_activity:
|
|
75
|
+
self.on_activity(activity)
|
|
76
|
+
|
|
77
|
+
async def get_products(self, brief: str, **kwargs: Any) -> TaskResult[Any]:
|
|
78
|
+
"""Get advertising products."""
|
|
79
|
+
operation_id = create_operation_id()
|
|
80
|
+
params = {"brief": brief, **kwargs}
|
|
81
|
+
|
|
82
|
+
self._emit_activity(
|
|
83
|
+
Activity(
|
|
84
|
+
type=ActivityType.PROTOCOL_REQUEST,
|
|
85
|
+
operation_id=operation_id,
|
|
86
|
+
agent_id=self.agent_config.id,
|
|
87
|
+
task_type="get_products",
|
|
88
|
+
timestamp=datetime.utcnow().isoformat(),
|
|
89
|
+
)
|
|
90
|
+
)
|
|
91
|
+
|
|
92
|
+
result = await self.adapter.call_tool("get_products", params)
|
|
93
|
+
|
|
94
|
+
self._emit_activity(
|
|
95
|
+
Activity(
|
|
96
|
+
type=ActivityType.PROTOCOL_RESPONSE,
|
|
97
|
+
operation_id=operation_id,
|
|
98
|
+
agent_id=self.agent_config.id,
|
|
99
|
+
task_type="get_products",
|
|
100
|
+
status=result.status,
|
|
101
|
+
timestamp=datetime.utcnow().isoformat(),
|
|
102
|
+
)
|
|
103
|
+
)
|
|
104
|
+
|
|
105
|
+
return result
|
|
106
|
+
|
|
107
|
+
async def list_creative_formats(self, **kwargs: Any) -> TaskResult[Any]:
|
|
108
|
+
"""List supported creative formats."""
|
|
109
|
+
operation_id = create_operation_id()
|
|
110
|
+
|
|
111
|
+
self._emit_activity(
|
|
112
|
+
Activity(
|
|
113
|
+
type=ActivityType.PROTOCOL_REQUEST,
|
|
114
|
+
operation_id=operation_id,
|
|
115
|
+
agent_id=self.agent_config.id,
|
|
116
|
+
task_type="list_creative_formats",
|
|
117
|
+
timestamp=datetime.utcnow().isoformat(),
|
|
118
|
+
)
|
|
119
|
+
)
|
|
120
|
+
|
|
121
|
+
result = await self.adapter.call_tool("list_creative_formats", kwargs)
|
|
122
|
+
|
|
123
|
+
self._emit_activity(
|
|
124
|
+
Activity(
|
|
125
|
+
type=ActivityType.PROTOCOL_RESPONSE,
|
|
126
|
+
operation_id=operation_id,
|
|
127
|
+
agent_id=self.agent_config.id,
|
|
128
|
+
task_type="list_creative_formats",
|
|
129
|
+
status=result.status,
|
|
130
|
+
timestamp=datetime.utcnow().isoformat(),
|
|
131
|
+
)
|
|
132
|
+
)
|
|
133
|
+
|
|
134
|
+
return result
|
|
135
|
+
|
|
136
|
+
async def create_media_buy(self, **kwargs: Any) -> TaskResult[Any]:
|
|
137
|
+
"""Create a new media buy."""
|
|
138
|
+
operation_id = create_operation_id()
|
|
139
|
+
|
|
140
|
+
self._emit_activity(
|
|
141
|
+
Activity(
|
|
142
|
+
type=ActivityType.PROTOCOL_REQUEST,
|
|
143
|
+
operation_id=operation_id,
|
|
144
|
+
agent_id=self.agent_config.id,
|
|
145
|
+
task_type="create_media_buy",
|
|
146
|
+
timestamp=datetime.utcnow().isoformat(),
|
|
147
|
+
)
|
|
148
|
+
)
|
|
149
|
+
|
|
150
|
+
result = await self.adapter.call_tool("create_media_buy", kwargs)
|
|
151
|
+
|
|
152
|
+
self._emit_activity(
|
|
153
|
+
Activity(
|
|
154
|
+
type=ActivityType.PROTOCOL_RESPONSE,
|
|
155
|
+
operation_id=operation_id,
|
|
156
|
+
agent_id=self.agent_config.id,
|
|
157
|
+
task_type="create_media_buy",
|
|
158
|
+
status=result.status,
|
|
159
|
+
timestamp=datetime.utcnow().isoformat(),
|
|
160
|
+
)
|
|
161
|
+
)
|
|
162
|
+
|
|
163
|
+
return result
|
|
164
|
+
|
|
165
|
+
async def update_media_buy(self, **kwargs: Any) -> TaskResult[Any]:
|
|
166
|
+
"""Update an existing media buy."""
|
|
167
|
+
operation_id = create_operation_id()
|
|
168
|
+
|
|
169
|
+
self._emit_activity(
|
|
170
|
+
Activity(
|
|
171
|
+
type=ActivityType.PROTOCOL_REQUEST,
|
|
172
|
+
operation_id=operation_id,
|
|
173
|
+
agent_id=self.agent_config.id,
|
|
174
|
+
task_type="update_media_buy",
|
|
175
|
+
timestamp=datetime.utcnow().isoformat(),
|
|
176
|
+
)
|
|
177
|
+
)
|
|
178
|
+
|
|
179
|
+
result = await self.adapter.call_tool("update_media_buy", kwargs)
|
|
180
|
+
|
|
181
|
+
self._emit_activity(
|
|
182
|
+
Activity(
|
|
183
|
+
type=ActivityType.PROTOCOL_RESPONSE,
|
|
184
|
+
operation_id=operation_id,
|
|
185
|
+
agent_id=self.agent_config.id,
|
|
186
|
+
task_type="update_media_buy",
|
|
187
|
+
status=result.status,
|
|
188
|
+
timestamp=datetime.utcnow().isoformat(),
|
|
189
|
+
)
|
|
190
|
+
)
|
|
191
|
+
|
|
192
|
+
return result
|
|
193
|
+
|
|
194
|
+
async def sync_creatives(self, **kwargs: Any) -> TaskResult[Any]:
|
|
195
|
+
"""Synchronize creatives with the agent."""
|
|
196
|
+
operation_id = create_operation_id()
|
|
197
|
+
|
|
198
|
+
self._emit_activity(
|
|
199
|
+
Activity(
|
|
200
|
+
type=ActivityType.PROTOCOL_REQUEST,
|
|
201
|
+
operation_id=operation_id,
|
|
202
|
+
agent_id=self.agent_config.id,
|
|
203
|
+
task_type="sync_creatives",
|
|
204
|
+
timestamp=datetime.utcnow().isoformat(),
|
|
205
|
+
)
|
|
206
|
+
)
|
|
207
|
+
|
|
208
|
+
result = await self.adapter.call_tool("sync_creatives", kwargs)
|
|
209
|
+
|
|
210
|
+
self._emit_activity(
|
|
211
|
+
Activity(
|
|
212
|
+
type=ActivityType.PROTOCOL_RESPONSE,
|
|
213
|
+
operation_id=operation_id,
|
|
214
|
+
agent_id=self.agent_config.id,
|
|
215
|
+
task_type="sync_creatives",
|
|
216
|
+
status=result.status,
|
|
217
|
+
timestamp=datetime.utcnow().isoformat(),
|
|
218
|
+
)
|
|
219
|
+
)
|
|
220
|
+
|
|
221
|
+
return result
|
|
222
|
+
|
|
223
|
+
async def list_creatives(self, **kwargs: Any) -> TaskResult[Any]:
|
|
224
|
+
"""List creatives for a media buy."""
|
|
225
|
+
operation_id = create_operation_id()
|
|
226
|
+
|
|
227
|
+
self._emit_activity(
|
|
228
|
+
Activity(
|
|
229
|
+
type=ActivityType.PROTOCOL_REQUEST,
|
|
230
|
+
operation_id=operation_id,
|
|
231
|
+
agent_id=self.agent_config.id,
|
|
232
|
+
task_type="list_creatives",
|
|
233
|
+
timestamp=datetime.utcnow().isoformat(),
|
|
234
|
+
)
|
|
235
|
+
)
|
|
236
|
+
|
|
237
|
+
result = await self.adapter.call_tool("list_creatives", kwargs)
|
|
238
|
+
|
|
239
|
+
self._emit_activity(
|
|
240
|
+
Activity(
|
|
241
|
+
type=ActivityType.PROTOCOL_RESPONSE,
|
|
242
|
+
operation_id=operation_id,
|
|
243
|
+
agent_id=self.agent_config.id,
|
|
244
|
+
task_type="list_creatives",
|
|
245
|
+
status=result.status,
|
|
246
|
+
timestamp=datetime.utcnow().isoformat(),
|
|
247
|
+
)
|
|
248
|
+
)
|
|
249
|
+
|
|
250
|
+
return result
|
|
251
|
+
|
|
252
|
+
async def get_media_buy_delivery(self, **kwargs: Any) -> TaskResult[Any]:
|
|
253
|
+
"""Get delivery metrics for a media buy."""
|
|
254
|
+
operation_id = create_operation_id()
|
|
255
|
+
|
|
256
|
+
self._emit_activity(
|
|
257
|
+
Activity(
|
|
258
|
+
type=ActivityType.PROTOCOL_REQUEST,
|
|
259
|
+
operation_id=operation_id,
|
|
260
|
+
agent_id=self.agent_config.id,
|
|
261
|
+
task_type="get_media_buy_delivery",
|
|
262
|
+
timestamp=datetime.utcnow().isoformat(),
|
|
263
|
+
)
|
|
264
|
+
)
|
|
265
|
+
|
|
266
|
+
result = await self.adapter.call_tool("get_media_buy_delivery", kwargs)
|
|
267
|
+
|
|
268
|
+
self._emit_activity(
|
|
269
|
+
Activity(
|
|
270
|
+
type=ActivityType.PROTOCOL_RESPONSE,
|
|
271
|
+
operation_id=operation_id,
|
|
272
|
+
agent_id=self.agent_config.id,
|
|
273
|
+
task_type="get_media_buy_delivery",
|
|
274
|
+
status=result.status,
|
|
275
|
+
timestamp=datetime.utcnow().isoformat(),
|
|
276
|
+
)
|
|
277
|
+
)
|
|
278
|
+
|
|
279
|
+
return result
|
|
280
|
+
|
|
281
|
+
async def list_authorized_properties(self, **kwargs: Any) -> TaskResult[Any]:
|
|
282
|
+
"""List properties this agent is authorized to sell."""
|
|
283
|
+
operation_id = create_operation_id()
|
|
284
|
+
|
|
285
|
+
self._emit_activity(
|
|
286
|
+
Activity(
|
|
287
|
+
type=ActivityType.PROTOCOL_REQUEST,
|
|
288
|
+
operation_id=operation_id,
|
|
289
|
+
agent_id=self.agent_config.id,
|
|
290
|
+
task_type="list_authorized_properties",
|
|
291
|
+
timestamp=datetime.utcnow().isoformat(),
|
|
292
|
+
)
|
|
293
|
+
)
|
|
294
|
+
|
|
295
|
+
result = await self.adapter.call_tool("list_authorized_properties", kwargs)
|
|
296
|
+
|
|
297
|
+
self._emit_activity(
|
|
298
|
+
Activity(
|
|
299
|
+
type=ActivityType.PROTOCOL_RESPONSE,
|
|
300
|
+
operation_id=operation_id,
|
|
301
|
+
agent_id=self.agent_config.id,
|
|
302
|
+
task_type="list_authorized_properties",
|
|
303
|
+
status=result.status,
|
|
304
|
+
timestamp=datetime.utcnow().isoformat(),
|
|
305
|
+
)
|
|
306
|
+
)
|
|
307
|
+
|
|
308
|
+
return result
|
|
309
|
+
|
|
310
|
+
async def get_signals(self, **kwargs: Any) -> TaskResult[Any]:
|
|
311
|
+
"""Get available signals for targeting."""
|
|
312
|
+
operation_id = create_operation_id()
|
|
313
|
+
|
|
314
|
+
self._emit_activity(
|
|
315
|
+
Activity(
|
|
316
|
+
type=ActivityType.PROTOCOL_REQUEST,
|
|
317
|
+
operation_id=operation_id,
|
|
318
|
+
agent_id=self.agent_config.id,
|
|
319
|
+
task_type="get_signals",
|
|
320
|
+
timestamp=datetime.utcnow().isoformat(),
|
|
321
|
+
)
|
|
322
|
+
)
|
|
323
|
+
|
|
324
|
+
result = await self.adapter.call_tool("get_signals", kwargs)
|
|
325
|
+
|
|
326
|
+
self._emit_activity(
|
|
327
|
+
Activity(
|
|
328
|
+
type=ActivityType.PROTOCOL_RESPONSE,
|
|
329
|
+
operation_id=operation_id,
|
|
330
|
+
agent_id=self.agent_config.id,
|
|
331
|
+
task_type="get_signals",
|
|
332
|
+
status=result.status,
|
|
333
|
+
timestamp=datetime.utcnow().isoformat(),
|
|
334
|
+
)
|
|
335
|
+
)
|
|
336
|
+
|
|
337
|
+
return result
|
|
338
|
+
|
|
339
|
+
async def activate_signal(self, **kwargs: Any) -> TaskResult[Any]:
|
|
340
|
+
"""Activate a signal for use in campaigns."""
|
|
341
|
+
operation_id = create_operation_id()
|
|
342
|
+
|
|
343
|
+
self._emit_activity(
|
|
344
|
+
Activity(
|
|
345
|
+
type=ActivityType.PROTOCOL_REQUEST,
|
|
346
|
+
operation_id=operation_id,
|
|
347
|
+
agent_id=self.agent_config.id,
|
|
348
|
+
task_type="activate_signal",
|
|
349
|
+
timestamp=datetime.utcnow().isoformat(),
|
|
350
|
+
)
|
|
351
|
+
)
|
|
352
|
+
|
|
353
|
+
result = await self.adapter.call_tool("activate_signal", kwargs)
|
|
354
|
+
|
|
355
|
+
self._emit_activity(
|
|
356
|
+
Activity(
|
|
357
|
+
type=ActivityType.PROTOCOL_RESPONSE,
|
|
358
|
+
operation_id=operation_id,
|
|
359
|
+
agent_id=self.agent_config.id,
|
|
360
|
+
task_type="activate_signal",
|
|
361
|
+
status=result.status,
|
|
362
|
+
timestamp=datetime.utcnow().isoformat(),
|
|
363
|
+
)
|
|
364
|
+
)
|
|
365
|
+
|
|
366
|
+
return result
|
|
367
|
+
|
|
368
|
+
async def provide_performance_feedback(self, **kwargs: Any) -> TaskResult[Any]:
|
|
369
|
+
"""Provide performance feedback for a campaign."""
|
|
370
|
+
operation_id = create_operation_id()
|
|
371
|
+
|
|
372
|
+
self._emit_activity(
|
|
373
|
+
Activity(
|
|
374
|
+
type=ActivityType.PROTOCOL_REQUEST,
|
|
375
|
+
operation_id=operation_id,
|
|
376
|
+
agent_id=self.agent_config.id,
|
|
377
|
+
task_type="provide_performance_feedback",
|
|
378
|
+
timestamp=datetime.utcnow().isoformat(),
|
|
379
|
+
)
|
|
380
|
+
)
|
|
381
|
+
|
|
382
|
+
result = await self.adapter.call_tool("provide_performance_feedback", kwargs)
|
|
383
|
+
|
|
384
|
+
self._emit_activity(
|
|
385
|
+
Activity(
|
|
386
|
+
type=ActivityType.PROTOCOL_RESPONSE,
|
|
387
|
+
operation_id=operation_id,
|
|
388
|
+
agent_id=self.agent_config.id,
|
|
389
|
+
task_type="provide_performance_feedback",
|
|
390
|
+
status=result.status,
|
|
391
|
+
timestamp=datetime.utcnow().isoformat(),
|
|
392
|
+
)
|
|
393
|
+
)
|
|
394
|
+
|
|
395
|
+
return result
|
|
396
|
+
|
|
397
|
+
async def handle_webhook(
|
|
398
|
+
self,
|
|
399
|
+
payload: dict[str, Any],
|
|
400
|
+
signature: str | None = None,
|
|
401
|
+
) -> None:
|
|
402
|
+
"""
|
|
403
|
+
Handle incoming webhook.
|
|
404
|
+
|
|
405
|
+
Args:
|
|
406
|
+
payload: Webhook payload
|
|
407
|
+
signature: Webhook signature for verification
|
|
408
|
+
"""
|
|
409
|
+
# TODO: Implement signature verification
|
|
410
|
+
if self.webhook_secret and signature:
|
|
411
|
+
# Verify signature
|
|
412
|
+
pass
|
|
413
|
+
|
|
414
|
+
operation_id = payload.get("operation_id", "unknown")
|
|
415
|
+
task_type = payload.get("task_type", "unknown")
|
|
416
|
+
|
|
417
|
+
self._emit_activity(
|
|
418
|
+
Activity(
|
|
419
|
+
type=ActivityType.WEBHOOK_RECEIVED,
|
|
420
|
+
operation_id=operation_id,
|
|
421
|
+
agent_id=self.agent_config.id,
|
|
422
|
+
task_type=task_type,
|
|
423
|
+
timestamp=datetime.utcnow().isoformat(),
|
|
424
|
+
metadata={"payload": payload},
|
|
425
|
+
)
|
|
426
|
+
)
|
|
427
|
+
|
|
428
|
+
|
|
429
|
+
class ADCPMultiAgentClient:
|
|
430
|
+
"""Client for managing multiple AdCP agents."""
|
|
431
|
+
|
|
432
|
+
def __init__(
|
|
433
|
+
self,
|
|
434
|
+
agents: list[AgentConfig],
|
|
435
|
+
webhook_url_template: str | None = None,
|
|
436
|
+
webhook_secret: str | None = None,
|
|
437
|
+
on_activity: Callable[[Activity], None] | None = None,
|
|
438
|
+
handlers: dict[str, Callable[..., Any]] | None = None,
|
|
439
|
+
):
|
|
440
|
+
"""
|
|
441
|
+
Initialize multi-agent client.
|
|
442
|
+
|
|
443
|
+
Args:
|
|
444
|
+
agents: List of agent configurations
|
|
445
|
+
webhook_url_template: Template for webhook URLs
|
|
446
|
+
webhook_secret: Secret for webhook verification
|
|
447
|
+
on_activity: Callback for activity events
|
|
448
|
+
handlers: Task completion handlers
|
|
449
|
+
"""
|
|
450
|
+
self.agents = {
|
|
451
|
+
agent.id: ADCPClient(
|
|
452
|
+
agent,
|
|
453
|
+
webhook_url_template=webhook_url_template,
|
|
454
|
+
webhook_secret=webhook_secret,
|
|
455
|
+
on_activity=on_activity,
|
|
456
|
+
)
|
|
457
|
+
for agent in agents
|
|
458
|
+
}
|
|
459
|
+
self.handlers = handlers or {}
|
|
460
|
+
|
|
461
|
+
def agent(self, agent_id: str) -> ADCPClient:
|
|
462
|
+
"""Get client for specific agent."""
|
|
463
|
+
if agent_id not in self.agents:
|
|
464
|
+
raise ValueError(f"Agent not found: {agent_id}")
|
|
465
|
+
return self.agents[agent_id]
|
|
466
|
+
|
|
467
|
+
@property
|
|
468
|
+
def agent_ids(self) -> list[str]:
|
|
469
|
+
"""Get list of agent IDs."""
|
|
470
|
+
return list(self.agents.keys())
|
|
471
|
+
|
|
472
|
+
async def get_products(self, brief: str, **kwargs: Any) -> list[TaskResult[Any]]:
|
|
473
|
+
"""Execute get_products across all agents in parallel."""
|
|
474
|
+
import asyncio
|
|
475
|
+
|
|
476
|
+
tasks = [agent.get_products(brief, **kwargs) for agent in self.agents.values()]
|
|
477
|
+
return await asyncio.gather(*tasks)
|
|
478
|
+
|
|
479
|
+
@classmethod
|
|
480
|
+
def from_env(cls) -> "ADCPMultiAgentClient":
|
|
481
|
+
"""Create client from environment variables."""
|
|
482
|
+
agents_json = os.getenv("ADCP_AGENTS")
|
|
483
|
+
if not agents_json:
|
|
484
|
+
raise ValueError("ADCP_AGENTS environment variable not set")
|
|
485
|
+
|
|
486
|
+
agents_data = json.loads(agents_json)
|
|
487
|
+
agents = [AgentConfig(**agent) for agent in agents_data]
|
|
488
|
+
|
|
489
|
+
return cls(
|
|
490
|
+
agents=agents,
|
|
491
|
+
webhook_url_template=os.getenv("WEBHOOK_URL_TEMPLATE"),
|
|
492
|
+
webhook_secret=os.getenv("WEBHOOK_SECRET"),
|
|
493
|
+
)
|
adcp/protocols/a2a.py
ADDED
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
"""A2A protocol adapter using HTTP client.
|
|
2
|
+
|
|
3
|
+
The official a2a-sdk is primarily for building A2A servers. For client functionality,
|
|
4
|
+
we implement the A2A protocol using HTTP requests as per the A2A specification.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from typing import Any
|
|
8
|
+
from uuid import uuid4
|
|
9
|
+
|
|
10
|
+
import httpx
|
|
11
|
+
|
|
12
|
+
from adcp.protocols.base import ProtocolAdapter
|
|
13
|
+
from adcp.types.core import TaskResult, TaskStatus
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class A2AAdapter(ProtocolAdapter):
|
|
17
|
+
"""Adapter for A2A protocol following the Agent2Agent specification."""
|
|
18
|
+
|
|
19
|
+
async def call_tool(self, tool_name: str, params: dict[str, Any]) -> TaskResult[Any]:
|
|
20
|
+
"""
|
|
21
|
+
Call a tool using A2A protocol.
|
|
22
|
+
|
|
23
|
+
A2A uses a tasks/send endpoint to initiate tasks. The agent responds with
|
|
24
|
+
task status and may require multiple roundtrips for completion.
|
|
25
|
+
"""
|
|
26
|
+
async with httpx.AsyncClient() as client:
|
|
27
|
+
headers = {"Content-Type": "application/json"}
|
|
28
|
+
|
|
29
|
+
if self.agent_config.auth_token:
|
|
30
|
+
headers["Authorization"] = f"Bearer {self.agent_config.auth_token}"
|
|
31
|
+
|
|
32
|
+
# Construct A2A message
|
|
33
|
+
message = {
|
|
34
|
+
"role": "user",
|
|
35
|
+
"parts": [
|
|
36
|
+
{
|
|
37
|
+
"type": "text",
|
|
38
|
+
"text": self._format_tool_request(tool_name, params),
|
|
39
|
+
}
|
|
40
|
+
],
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
# A2A uses message/send endpoint
|
|
44
|
+
url = f"{self.agent_config.agent_uri}/message/send"
|
|
45
|
+
|
|
46
|
+
request_data = {
|
|
47
|
+
"message": message,
|
|
48
|
+
"context_id": str(uuid4()),
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
try:
|
|
52
|
+
response = await client.post(
|
|
53
|
+
url,
|
|
54
|
+
json=request_data,
|
|
55
|
+
headers=headers,
|
|
56
|
+
timeout=30.0,
|
|
57
|
+
)
|
|
58
|
+
response.raise_for_status()
|
|
59
|
+
|
|
60
|
+
data = response.json()
|
|
61
|
+
|
|
62
|
+
# Parse A2A response format
|
|
63
|
+
# A2A tasks have lifecycle: submitted, working, completed, failed, input-required
|
|
64
|
+
task_status = data.get("task", {}).get("status")
|
|
65
|
+
|
|
66
|
+
if task_status in ("completed", "working"):
|
|
67
|
+
# Extract the result from the response message
|
|
68
|
+
result_data = self._extract_result(data)
|
|
69
|
+
|
|
70
|
+
return TaskResult[Any](
|
|
71
|
+
status=TaskStatus.COMPLETED,
|
|
72
|
+
data=result_data,
|
|
73
|
+
success=True,
|
|
74
|
+
metadata={"task_id": data.get("task", {}).get("id")},
|
|
75
|
+
)
|
|
76
|
+
elif task_status == "failed":
|
|
77
|
+
return TaskResult[Any](
|
|
78
|
+
status=TaskStatus.FAILED,
|
|
79
|
+
error=data.get("message", {})
|
|
80
|
+
.get("parts", [{}])[0]
|
|
81
|
+
.get("text", "Task failed"),
|
|
82
|
+
success=False,
|
|
83
|
+
)
|
|
84
|
+
else:
|
|
85
|
+
# Handle other states (submitted, input-required)
|
|
86
|
+
return TaskResult[Any](
|
|
87
|
+
status=TaskStatus.SUBMITTED,
|
|
88
|
+
data=data,
|
|
89
|
+
success=True,
|
|
90
|
+
metadata={"task_id": data.get("task", {}).get("id")},
|
|
91
|
+
)
|
|
92
|
+
|
|
93
|
+
except httpx.HTTPError as e:
|
|
94
|
+
return TaskResult[Any](
|
|
95
|
+
status=TaskStatus.FAILED,
|
|
96
|
+
error=str(e),
|
|
97
|
+
success=False,
|
|
98
|
+
)
|
|
99
|
+
|
|
100
|
+
def _format_tool_request(self, tool_name: str, params: dict[str, Any]) -> str:
|
|
101
|
+
"""Format tool request as natural language for A2A."""
|
|
102
|
+
# For AdCP tools, we format as a structured request
|
|
103
|
+
import json
|
|
104
|
+
|
|
105
|
+
return f"Execute tool: {tool_name}\nParameters: {json.dumps(params, indent=2)}"
|
|
106
|
+
|
|
107
|
+
def _extract_result(self, response_data: dict[str, Any]) -> Any:
|
|
108
|
+
"""Extract result data from A2A response."""
|
|
109
|
+
# Try to extract structured data from response
|
|
110
|
+
message = response_data.get("message", {})
|
|
111
|
+
parts = message.get("parts", [])
|
|
112
|
+
|
|
113
|
+
if not parts:
|
|
114
|
+
return response_data
|
|
115
|
+
|
|
116
|
+
# Return the first part's content
|
|
117
|
+
first_part = parts[0]
|
|
118
|
+
if first_part.get("type") == "text":
|
|
119
|
+
# Try to parse as JSON if it looks like structured data
|
|
120
|
+
text = first_part.get("text", "")
|
|
121
|
+
try:
|
|
122
|
+
import json
|
|
123
|
+
|
|
124
|
+
return json.loads(text)
|
|
125
|
+
except (json.JSONDecodeError, ValueError):
|
|
126
|
+
return text
|
|
127
|
+
|
|
128
|
+
return first_part
|
|
129
|
+
|
|
130
|
+
async def list_tools(self) -> list[str]:
|
|
131
|
+
"""
|
|
132
|
+
List available tools from A2A agent.
|
|
133
|
+
|
|
134
|
+
Note: A2A doesn't have a standard tools/list endpoint. Agents expose
|
|
135
|
+
their capabilities through the agent card. For AdCP, we rely on the
|
|
136
|
+
standard AdCP tool set.
|
|
137
|
+
"""
|
|
138
|
+
async with httpx.AsyncClient() as client:
|
|
139
|
+
headers = {"Content-Type": "application/json"}
|
|
140
|
+
|
|
141
|
+
if self.agent_config.auth_token:
|
|
142
|
+
headers["Authorization"] = f"Bearer {self.agent_config.auth_token}"
|
|
143
|
+
|
|
144
|
+
# Try to fetch agent card (OpenAPI spec)
|
|
145
|
+
url = f"{self.agent_config.agent_uri}/agent-card"
|
|
146
|
+
|
|
147
|
+
try:
|
|
148
|
+
response = await client.get(url, headers=headers, timeout=10.0)
|
|
149
|
+
response.raise_for_status()
|
|
150
|
+
|
|
151
|
+
data = response.json()
|
|
152
|
+
|
|
153
|
+
# Extract skills from agent card
|
|
154
|
+
skills = data.get("skills", [])
|
|
155
|
+
return [skill.get("name", "") for skill in skills if skill.get("name")]
|
|
156
|
+
|
|
157
|
+
except httpx.HTTPError:
|
|
158
|
+
# If agent card is not available, return empty list
|
|
159
|
+
return []
|
adcp/protocols/base.py
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
"""Base protocol adapter interface."""
|
|
2
|
+
|
|
3
|
+
from abc import ABC, abstractmethod
|
|
4
|
+
from typing import Any
|
|
5
|
+
|
|
6
|
+
from adcp.types.core import AgentConfig, TaskResult
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class ProtocolAdapter(ABC):
|
|
10
|
+
"""Base class for protocol adapters."""
|
|
11
|
+
|
|
12
|
+
def __init__(self, agent_config: AgentConfig):
|
|
13
|
+
"""Initialize adapter with agent configuration."""
|
|
14
|
+
self.agent_config = agent_config
|
|
15
|
+
|
|
16
|
+
@abstractmethod
|
|
17
|
+
async def call_tool(self, tool_name: str, params: dict[str, Any]) -> TaskResult[Any]:
|
|
18
|
+
"""
|
|
19
|
+
Call a tool on the agent.
|
|
20
|
+
|
|
21
|
+
Args:
|
|
22
|
+
tool_name: Name of the tool to call
|
|
23
|
+
params: Tool parameters
|
|
24
|
+
|
|
25
|
+
Returns:
|
|
26
|
+
TaskResult with the response
|
|
27
|
+
"""
|
|
28
|
+
pass
|
|
29
|
+
|
|
30
|
+
@abstractmethod
|
|
31
|
+
async def list_tools(self) -> list[str]:
|
|
32
|
+
"""
|
|
33
|
+
List available tools from the agent.
|
|
34
|
+
|
|
35
|
+
Returns:
|
|
36
|
+
List of tool names
|
|
37
|
+
"""
|
|
38
|
+
pass
|
adcp/protocols/mcp.py
ADDED
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
"""MCP protocol adapter using official Python MCP SDK."""
|
|
2
|
+
|
|
3
|
+
from typing import Any
|
|
4
|
+
from urllib.parse import urlparse
|
|
5
|
+
|
|
6
|
+
try:
|
|
7
|
+
from mcp import ClientSession # type: ignore[import-not-found]
|
|
8
|
+
from mcp.client.sse import sse_client # type: ignore[import-not-found]
|
|
9
|
+
|
|
10
|
+
MCP_AVAILABLE = True
|
|
11
|
+
except ImportError:
|
|
12
|
+
MCP_AVAILABLE = False
|
|
13
|
+
ClientSession = None
|
|
14
|
+
|
|
15
|
+
from adcp.protocols.base import ProtocolAdapter
|
|
16
|
+
from adcp.types.core import TaskResult, TaskStatus
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class MCPAdapter(ProtocolAdapter):
|
|
20
|
+
"""Adapter for MCP protocol using official Python MCP SDK."""
|
|
21
|
+
|
|
22
|
+
def __init__(self, *args: Any, **kwargs: Any):
|
|
23
|
+
super().__init__(*args, **kwargs)
|
|
24
|
+
if not MCP_AVAILABLE:
|
|
25
|
+
raise ImportError(
|
|
26
|
+
"MCP SDK not installed. Install with: pip install mcp (requires Python 3.10+)"
|
|
27
|
+
)
|
|
28
|
+
self._session: Any = None
|
|
29
|
+
self._exit_stack: Any = None
|
|
30
|
+
|
|
31
|
+
async def _get_session(self) -> ClientSession:
|
|
32
|
+
"""Get or create MCP client session."""
|
|
33
|
+
if self._session is not None:
|
|
34
|
+
return self._session
|
|
35
|
+
|
|
36
|
+
# Parse the agent URI to determine transport type
|
|
37
|
+
parsed = urlparse(self.agent_config.agent_uri)
|
|
38
|
+
|
|
39
|
+
# Use SSE transport for HTTP/HTTPS endpoints
|
|
40
|
+
if parsed.scheme in ("http", "https"):
|
|
41
|
+
from contextlib import AsyncExitStack
|
|
42
|
+
|
|
43
|
+
self._exit_stack = AsyncExitStack()
|
|
44
|
+
|
|
45
|
+
# Create SSE client with authentication header
|
|
46
|
+
headers = {}
|
|
47
|
+
if self.agent_config.auth_token:
|
|
48
|
+
headers["x-adcp-auth"] = self.agent_config.auth_token
|
|
49
|
+
|
|
50
|
+
read, write = await self._exit_stack.enter_async_context(
|
|
51
|
+
sse_client(self.agent_config.agent_uri, headers=headers)
|
|
52
|
+
)
|
|
53
|
+
|
|
54
|
+
self._session = await self._exit_stack.enter_async_context(ClientSession(read, write))
|
|
55
|
+
|
|
56
|
+
# Initialize the session
|
|
57
|
+
await self._session.initialize()
|
|
58
|
+
|
|
59
|
+
return self._session
|
|
60
|
+
else:
|
|
61
|
+
raise ValueError(f"Unsupported transport scheme: {parsed.scheme}")
|
|
62
|
+
|
|
63
|
+
async def call_tool(self, tool_name: str, params: dict[str, Any]) -> TaskResult[Any]:
|
|
64
|
+
"""Call a tool using MCP protocol."""
|
|
65
|
+
try:
|
|
66
|
+
session = await self._get_session()
|
|
67
|
+
|
|
68
|
+
# Call the tool using MCP client session
|
|
69
|
+
result = await session.call_tool(tool_name, params)
|
|
70
|
+
|
|
71
|
+
# MCP tool results contain a list of content items
|
|
72
|
+
# For AdCP, we expect the data in the content
|
|
73
|
+
return TaskResult[Any](
|
|
74
|
+
status=TaskStatus.COMPLETED,
|
|
75
|
+
data=result.content,
|
|
76
|
+
success=True,
|
|
77
|
+
)
|
|
78
|
+
|
|
79
|
+
except Exception as e:
|
|
80
|
+
return TaskResult[Any](
|
|
81
|
+
status=TaskStatus.FAILED,
|
|
82
|
+
error=str(e),
|
|
83
|
+
success=False,
|
|
84
|
+
)
|
|
85
|
+
|
|
86
|
+
async def list_tools(self) -> list[str]:
|
|
87
|
+
"""List available tools from MCP agent."""
|
|
88
|
+
try:
|
|
89
|
+
session = await self._get_session()
|
|
90
|
+
result = await session.list_tools()
|
|
91
|
+
return [tool.name for tool in result.tools]
|
|
92
|
+
except Exception:
|
|
93
|
+
# Return empty list on error
|
|
94
|
+
return []
|
|
95
|
+
|
|
96
|
+
async def close(self) -> None:
|
|
97
|
+
"""Close the MCP session."""
|
|
98
|
+
if self._exit_stack is not None:
|
|
99
|
+
await self._exit_stack.aclose()
|
|
100
|
+
self._exit_stack = None
|
|
101
|
+
self._session = None
|
adcp/types/__init__.py
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
"""Type definitions for AdCP client."""
|
|
2
|
+
|
|
3
|
+
from adcp.types.core import (
|
|
4
|
+
Activity,
|
|
5
|
+
ActivityType,
|
|
6
|
+
AgentConfig,
|
|
7
|
+
Protocol,
|
|
8
|
+
TaskResult,
|
|
9
|
+
TaskStatus,
|
|
10
|
+
WebhookMetadata,
|
|
11
|
+
)
|
|
12
|
+
|
|
13
|
+
__all__ = [
|
|
14
|
+
"AgentConfig",
|
|
15
|
+
"Protocol",
|
|
16
|
+
"TaskResult",
|
|
17
|
+
"TaskStatus",
|
|
18
|
+
"WebhookMetadata",
|
|
19
|
+
"Activity",
|
|
20
|
+
"ActivityType",
|
|
21
|
+
]
|
adcp/types/core.py
ADDED
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
"""Core type definitions."""
|
|
2
|
+
|
|
3
|
+
from enum import Enum
|
|
4
|
+
from typing import Any, Generic, Literal, TypeVar
|
|
5
|
+
|
|
6
|
+
from pydantic import BaseModel, Field
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class Protocol(str, Enum):
|
|
10
|
+
"""Supported protocols."""
|
|
11
|
+
|
|
12
|
+
A2A = "a2a"
|
|
13
|
+
MCP = "mcp"
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class AgentConfig(BaseModel):
|
|
17
|
+
"""Agent configuration."""
|
|
18
|
+
|
|
19
|
+
id: str
|
|
20
|
+
agent_uri: str
|
|
21
|
+
protocol: Protocol
|
|
22
|
+
auth_token: str | None = None
|
|
23
|
+
requires_auth: bool = False
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class TaskStatus(str, Enum):
|
|
27
|
+
"""Task execution status."""
|
|
28
|
+
|
|
29
|
+
COMPLETED = "completed"
|
|
30
|
+
SUBMITTED = "submitted"
|
|
31
|
+
NEEDS_INPUT = "needs_input"
|
|
32
|
+
FAILED = "failed"
|
|
33
|
+
WORKING = "working"
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
T = TypeVar("T")
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
class SubmittedInfo(BaseModel):
|
|
40
|
+
"""Information about submitted async task."""
|
|
41
|
+
|
|
42
|
+
webhook_url: str
|
|
43
|
+
operation_id: str
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
class NeedsInputInfo(BaseModel):
|
|
47
|
+
"""Information when agent needs clarification."""
|
|
48
|
+
|
|
49
|
+
message: str
|
|
50
|
+
field: str | None = None
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
class TaskResult(BaseModel, Generic[T]):
|
|
54
|
+
"""Result from task execution."""
|
|
55
|
+
|
|
56
|
+
status: TaskStatus
|
|
57
|
+
data: T | None = None
|
|
58
|
+
submitted: SubmittedInfo | None = None
|
|
59
|
+
needs_input: NeedsInputInfo | None = None
|
|
60
|
+
error: str | None = None
|
|
61
|
+
success: bool = Field(default=True)
|
|
62
|
+
metadata: dict[str, Any] | None = None
|
|
63
|
+
|
|
64
|
+
class Config:
|
|
65
|
+
arbitrary_types_allowed = True
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
class ActivityType(str, Enum):
|
|
69
|
+
"""Types of activity events."""
|
|
70
|
+
|
|
71
|
+
PROTOCOL_REQUEST = "protocol_request"
|
|
72
|
+
PROTOCOL_RESPONSE = "protocol_response"
|
|
73
|
+
WEBHOOK_RECEIVED = "webhook_received"
|
|
74
|
+
HANDLER_CALLED = "handler_called"
|
|
75
|
+
STATUS_CHANGE = "status_change"
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
class Activity(BaseModel):
|
|
79
|
+
"""Activity event for observability."""
|
|
80
|
+
|
|
81
|
+
type: ActivityType
|
|
82
|
+
operation_id: str
|
|
83
|
+
agent_id: str
|
|
84
|
+
task_type: str
|
|
85
|
+
status: TaskStatus | None = None
|
|
86
|
+
timestamp: str
|
|
87
|
+
metadata: dict[str, Any] | None = None
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
class WebhookMetadata(BaseModel):
|
|
91
|
+
"""Metadata passed to webhook handlers."""
|
|
92
|
+
|
|
93
|
+
operation_id: str
|
|
94
|
+
agent_id: str
|
|
95
|
+
task_type: str
|
|
96
|
+
status: TaskStatus
|
|
97
|
+
sequence_number: int | None = None
|
|
98
|
+
notification_type: Literal["scheduled", "final", "delayed"] | None = None
|
|
99
|
+
timestamp: str
|
adcp/utils/__init__.py
ADDED
|
@@ -0,0 +1,276 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: adcp
|
|
3
|
+
Version: 0.1.2
|
|
4
|
+
Summary: Official Python client for the Ad Context Protocol (AdCP)
|
|
5
|
+
Author-email: AdCP Community <maintainers@adcontextprotocol.org>
|
|
6
|
+
License: Apache-2.0
|
|
7
|
+
Project-URL: Homepage, https://github.com/adcontextprotocol/adcp-client-python
|
|
8
|
+
Project-URL: Documentation, https://docs.adcontextprotocol.org
|
|
9
|
+
Project-URL: Repository, https://github.com/adcontextprotocol/adcp-client-python
|
|
10
|
+
Project-URL: Issues, https://github.com/adcontextprotocol/adcp-client-python/issues
|
|
11
|
+
Keywords: adcp,mcp,a2a,protocol,advertising
|
|
12
|
+
Classifier: Development Status :: 3 - Alpha
|
|
13
|
+
Classifier: Intended Audience :: Developers
|
|
14
|
+
Classifier: License :: OSI Approved :: Apache Software License
|
|
15
|
+
Classifier: Programming Language :: Python :: 3
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
20
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
21
|
+
Requires-Python: >=3.10
|
|
22
|
+
Description-Content-Type: text/markdown
|
|
23
|
+
License-File: LICENSE
|
|
24
|
+
Requires-Dist: httpx>=0.24.0
|
|
25
|
+
Requires-Dist: pydantic>=2.0.0
|
|
26
|
+
Requires-Dist: typing-extensions>=4.5.0
|
|
27
|
+
Requires-Dist: a2a-sdk>=0.3.0
|
|
28
|
+
Requires-Dist: mcp>=0.9.0
|
|
29
|
+
Provides-Extra: dev
|
|
30
|
+
Requires-Dist: pytest>=7.0.0; extra == "dev"
|
|
31
|
+
Requires-Dist: pytest-asyncio>=0.21.0; extra == "dev"
|
|
32
|
+
Requires-Dist: pytest-cov>=4.0.0; extra == "dev"
|
|
33
|
+
Requires-Dist: mypy>=1.0.0; extra == "dev"
|
|
34
|
+
Requires-Dist: black>=23.0.0; extra == "dev"
|
|
35
|
+
Requires-Dist: ruff>=0.1.0; extra == "dev"
|
|
36
|
+
Dynamic: license-file
|
|
37
|
+
|
|
38
|
+
# adcp - Python Client for Ad Context Protocol
|
|
39
|
+
|
|
40
|
+
[](https://badge.fury.io/py/adcp)
|
|
41
|
+
[](https://opensource.org/licenses/Apache-2.0)
|
|
42
|
+
[](https://www.python.org/downloads/)
|
|
43
|
+
|
|
44
|
+
Official Python client for the **Ad Context Protocol (AdCP)**. Build distributed advertising operations that work synchronously OR asynchronously with the same code.
|
|
45
|
+
|
|
46
|
+
## The Core Concept
|
|
47
|
+
|
|
48
|
+
AdCP operations are **distributed and asynchronous by default**. An agent might:
|
|
49
|
+
- Complete your request **immediately** (synchronous)
|
|
50
|
+
- Need time to process and **send results via webhook** (asynchronous)
|
|
51
|
+
- Ask for **clarifications** before proceeding
|
|
52
|
+
- Send periodic **status updates** as work progresses
|
|
53
|
+
|
|
54
|
+
**Your code stays the same.** You write handlers once, and they work for both sync completions and webhook deliveries.
|
|
55
|
+
|
|
56
|
+
## Installation
|
|
57
|
+
|
|
58
|
+
```bash
|
|
59
|
+
pip install adcp
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
## Quick Start: Distributed Operations
|
|
63
|
+
|
|
64
|
+
```python
|
|
65
|
+
from adcp import ADCPMultiAgentClient
|
|
66
|
+
from adcp.types import AgentConfig
|
|
67
|
+
|
|
68
|
+
# Configure agents and handlers
|
|
69
|
+
client = ADCPMultiAgentClient(
|
|
70
|
+
agents=[
|
|
71
|
+
AgentConfig(
|
|
72
|
+
id="agent_x",
|
|
73
|
+
agent_uri="https://agent-x.com",
|
|
74
|
+
protocol="a2a"
|
|
75
|
+
),
|
|
76
|
+
AgentConfig(
|
|
77
|
+
id="agent_y",
|
|
78
|
+
agent_uri="https://agent-y.com/mcp/",
|
|
79
|
+
protocol="mcp"
|
|
80
|
+
)
|
|
81
|
+
],
|
|
82
|
+
# Webhook URL template (macros: {agent_id}, {task_type}, {operation_id})
|
|
83
|
+
webhook_url_template="https://myapp.com/webhook/{task_type}/{agent_id}/{operation_id}",
|
|
84
|
+
|
|
85
|
+
# Activity callback - fires for ALL events
|
|
86
|
+
on_activity=lambda activity: print(f"[{activity.type}] {activity.task_type}"),
|
|
87
|
+
|
|
88
|
+
# Status change handlers
|
|
89
|
+
handlers={
|
|
90
|
+
"on_get_products_status_change": lambda response, metadata: (
|
|
91
|
+
db.save_products(metadata.operation_id, response.products)
|
|
92
|
+
if metadata.status == "completed" else None
|
|
93
|
+
)
|
|
94
|
+
}
|
|
95
|
+
)
|
|
96
|
+
|
|
97
|
+
# Execute operation - library handles operation IDs, webhook URLs, context management
|
|
98
|
+
agent = client.agent("agent_x")
|
|
99
|
+
result = await agent.get_products(brief="Coffee brands")
|
|
100
|
+
|
|
101
|
+
# Check result
|
|
102
|
+
if result.status == "completed":
|
|
103
|
+
# Agent completed synchronously!
|
|
104
|
+
print(f"✅ Sync completion: {len(result.data.products)} products")
|
|
105
|
+
|
|
106
|
+
if result.status == "submitted":
|
|
107
|
+
# Agent will send webhook when complete
|
|
108
|
+
print(f"⏳ Async - webhook registered at: {result.submitted.webhook_url}")
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
## Features
|
|
112
|
+
|
|
113
|
+
### Full Protocol Support
|
|
114
|
+
- **A2A Protocol**: Native support for Agent-to-Agent protocol
|
|
115
|
+
- **MCP Protocol**: Native support for Model Context Protocol
|
|
116
|
+
- **Auto-detection**: Automatically detect which protocol an agent uses
|
|
117
|
+
|
|
118
|
+
### Type Safety
|
|
119
|
+
Full type hints with Pydantic validation:
|
|
120
|
+
|
|
121
|
+
```python
|
|
122
|
+
result = await agent.get_products(brief="Coffee brands")
|
|
123
|
+
# result: TaskResult[GetProductsResponse]
|
|
124
|
+
|
|
125
|
+
if result.success:
|
|
126
|
+
for product in result.data.products:
|
|
127
|
+
print(product.name, product.price) # Full IDE autocomplete!
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
### Multi-Agent Operations
|
|
131
|
+
Execute across multiple agents simultaneously:
|
|
132
|
+
|
|
133
|
+
```python
|
|
134
|
+
# Parallel execution across all agents
|
|
135
|
+
results = await client.get_products(brief="Coffee brands")
|
|
136
|
+
|
|
137
|
+
for result in results:
|
|
138
|
+
if result.status == "completed":
|
|
139
|
+
print(f"Sync: {len(result.data.products)} products")
|
|
140
|
+
elif result.status == "submitted":
|
|
141
|
+
print(f"Async: webhook to {result.submitted.webhook_url}")
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
### Webhook Handling
|
|
145
|
+
Single endpoint handles all webhooks:
|
|
146
|
+
|
|
147
|
+
```python
|
|
148
|
+
from fastapi import FastAPI, Request
|
|
149
|
+
|
|
150
|
+
app = FastAPI()
|
|
151
|
+
|
|
152
|
+
@app.post("/webhook/{task_type}/{agent_id}/{operation_id}")
|
|
153
|
+
async def webhook(task_type: str, agent_id: str, operation_id: str, request: Request):
|
|
154
|
+
payload = await request.json()
|
|
155
|
+
payload["task_type"] = task_type
|
|
156
|
+
payload["operation_id"] = operation_id
|
|
157
|
+
|
|
158
|
+
# Route to agent client - handlers fire automatically
|
|
159
|
+
agent = client.agent(agent_id)
|
|
160
|
+
await agent.handle_webhook(
|
|
161
|
+
payload,
|
|
162
|
+
request.headers.get("x-adcp-signature")
|
|
163
|
+
)
|
|
164
|
+
|
|
165
|
+
return {"received": True}
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
### Security
|
|
169
|
+
Webhook signature verification built-in:
|
|
170
|
+
|
|
171
|
+
```python
|
|
172
|
+
client = ADCPMultiAgentClient(
|
|
173
|
+
agents=agents,
|
|
174
|
+
webhook_secret=os.getenv("WEBHOOK_SECRET")
|
|
175
|
+
)
|
|
176
|
+
# Signatures verified automatically on handle_webhook()
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
## Available Tools
|
|
180
|
+
|
|
181
|
+
All AdCP tools with full type safety:
|
|
182
|
+
|
|
183
|
+
**Media Buy Lifecycle:**
|
|
184
|
+
- `get_products()` - Discover advertising products
|
|
185
|
+
- `list_creative_formats()` - Get supported creative formats
|
|
186
|
+
- `create_media_buy()` - Create new media buy
|
|
187
|
+
- `update_media_buy()` - Update existing media buy
|
|
188
|
+
- `sync_creatives()` - Upload/sync creative assets
|
|
189
|
+
- `list_creatives()` - List creative assets
|
|
190
|
+
- `get_media_buy_delivery()` - Get delivery performance
|
|
191
|
+
|
|
192
|
+
**Audience & Targeting:**
|
|
193
|
+
- `list_authorized_properties()` - Get authorized properties
|
|
194
|
+
- `get_signals()` - Get audience signals
|
|
195
|
+
- `activate_signal()` - Activate audience signals
|
|
196
|
+
- `provide_performance_feedback()` - Send performance feedback
|
|
197
|
+
|
|
198
|
+
## Property Discovery (AdCP v2.2.0)
|
|
199
|
+
|
|
200
|
+
Build agent registries by discovering properties agents can sell:
|
|
201
|
+
|
|
202
|
+
```python
|
|
203
|
+
from adcp.discovery import PropertyCrawler, get_property_index
|
|
204
|
+
|
|
205
|
+
# Crawl agents to discover properties
|
|
206
|
+
crawler = PropertyCrawler()
|
|
207
|
+
await crawler.crawl_agents([
|
|
208
|
+
{"agent_url": "https://agent-x.com", "protocol": "a2a"},
|
|
209
|
+
{"agent_url": "https://agent-y.com/mcp/", "protocol": "mcp"}
|
|
210
|
+
])
|
|
211
|
+
|
|
212
|
+
index = get_property_index()
|
|
213
|
+
|
|
214
|
+
# Query 1: Who can sell this property?
|
|
215
|
+
matches = index.find_agents_for_property("domain", "cnn.com")
|
|
216
|
+
|
|
217
|
+
# Query 2: What can this agent sell?
|
|
218
|
+
auth = index.get_agent_authorizations("https://agent-x.com")
|
|
219
|
+
|
|
220
|
+
# Query 3: Find by tags
|
|
221
|
+
premium = index.find_agents_by_property_tags(["premium", "ctv"])
|
|
222
|
+
```
|
|
223
|
+
|
|
224
|
+
## Environment Configuration
|
|
225
|
+
|
|
226
|
+
```bash
|
|
227
|
+
# .env
|
|
228
|
+
WEBHOOK_URL_TEMPLATE="https://myapp.com/webhook/{task_type}/{agent_id}/{operation_id}"
|
|
229
|
+
WEBHOOK_SECRET="your-webhook-secret"
|
|
230
|
+
|
|
231
|
+
ADCP_AGENTS='[
|
|
232
|
+
{
|
|
233
|
+
"id": "agent_x",
|
|
234
|
+
"agent_uri": "https://agent-x.com",
|
|
235
|
+
"protocol": "a2a",
|
|
236
|
+
"auth_token_env": "AGENT_X_TOKEN"
|
|
237
|
+
}
|
|
238
|
+
]'
|
|
239
|
+
AGENT_X_TOKEN="actual-token-here"
|
|
240
|
+
```
|
|
241
|
+
|
|
242
|
+
```python
|
|
243
|
+
# Auto-discover from environment
|
|
244
|
+
client = ADCPMultiAgentClient.from_env()
|
|
245
|
+
```
|
|
246
|
+
|
|
247
|
+
## Development
|
|
248
|
+
|
|
249
|
+
```bash
|
|
250
|
+
# Install with dev dependencies
|
|
251
|
+
pip install -e ".[dev]"
|
|
252
|
+
|
|
253
|
+
# Run tests
|
|
254
|
+
pytest
|
|
255
|
+
|
|
256
|
+
# Type checking
|
|
257
|
+
mypy src/
|
|
258
|
+
|
|
259
|
+
# Format code
|
|
260
|
+
black src/ tests/
|
|
261
|
+
ruff check src/ tests/
|
|
262
|
+
```
|
|
263
|
+
|
|
264
|
+
## Contributing
|
|
265
|
+
|
|
266
|
+
Contributions welcome! See [CONTRIBUTING.md](CONTRIBUTING.md) for guidelines.
|
|
267
|
+
|
|
268
|
+
## License
|
|
269
|
+
|
|
270
|
+
Apache 2.0 License - see [LICENSE](LICENSE) file for details.
|
|
271
|
+
|
|
272
|
+
## Support
|
|
273
|
+
|
|
274
|
+
- **Documentation**: [docs.adcontextprotocol.org](https://docs.adcontextprotocol.org)
|
|
275
|
+
- **Issues**: [GitHub Issues](https://github.com/adcontextprotocol/adcp-client-python/issues)
|
|
276
|
+
- **Protocol Spec**: [AdCP Specification](https://github.com/adcontextprotocol/adcp)
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
adcp/__init__.py,sha256=TpXs5-627LCcgm351m2Q1oD5WsFawGoLfe3Fj6Wi4-o,424
|
|
2
|
+
adcp/client.py,sha256=DaAA_5Jxw2N0xjRSdvplJdUU_QHHMJqNzow9QzeagJw,16095
|
|
3
|
+
adcp/protocols/__init__.py,sha256=hAGf5yAfgqpqa_-pMWXL35Bl-Aeca8Zdt_E1uEeI0_M,226
|
|
4
|
+
adcp/protocols/a2a.py,sha256=P9tnUYiVSRa8_OJPsZmr2_r1IasRIGS7z6LR8uTj0lg,5719
|
|
5
|
+
adcp/protocols/base.py,sha256=RhaeFyOj4lxCxGJajUo1ydpNDW2OZPnnMJ6QmodOESw,915
|
|
6
|
+
adcp/protocols/mcp.py,sha256=dLSmDs-qJrn_dcXUthABRblb2ripjNVYWi0wZ0VOOe8,3417
|
|
7
|
+
adcp/types/__init__.py,sha256=LuiNdxbVfS91LIt2DTW7xX0BR-chmqwXz4ScYS6Hd44,334
|
|
8
|
+
adcp/types/core.py,sha256=xybdwh1bsI_q1QiS8DB_6DiFdUNzWsycfVbi7sO2nfo,2119
|
|
9
|
+
adcp/utils/__init__.py,sha256=ME5OVQipWgFdECX0Lur8-ThcVrqHfQOtx27TwjDe9LE,117
|
|
10
|
+
adcp/utils/operation_id.py,sha256=_TcXbaSuaFhnN5F1zECspSJ5n08x_-LIzIWbdlkwzEQ,258
|
|
11
|
+
adcp-0.1.2.dist-info/licenses/LICENSE,sha256=PF39NR3Ae8PLgBhg3Uxw6ju7iGVIf8hfv9LRWQdii_U,629
|
|
12
|
+
adcp-0.1.2.dist-info/METADATA,sha256=pA0Ur9lBAYwdy_u-KsWhwPSYhWNHY_jzoWrPFsl8pGc,8429
|
|
13
|
+
adcp-0.1.2.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
14
|
+
adcp-0.1.2.dist-info/top_level.txt,sha256=T1_NF0GefncFU9v_k56oDwKSJREyCqIM8lAwNZf0EOs,5
|
|
15
|
+
adcp-0.1.2.dist-info/RECORD,,
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
Apache License
|
|
2
|
+
Version 2.0, January 2004
|
|
3
|
+
http://www.apache.org/licenses/
|
|
4
|
+
|
|
5
|
+
Copyright 2025 AdCP Community
|
|
6
|
+
|
|
7
|
+
Licensed under the Apache License, Version 2.0 (the "License");
|
|
8
|
+
you may not use this file except in compliance with the License.
|
|
9
|
+
You may obtain a copy of the License at
|
|
10
|
+
|
|
11
|
+
http://www.apache.org/licenses/LICENSE-2.0
|
|
12
|
+
|
|
13
|
+
Unless required by applicable law or agreed to in writing, software
|
|
14
|
+
distributed under the License is distributed on an "AS IS" BASIS,
|
|
15
|
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
16
|
+
See the License for the specific language governing permissions and
|
|
17
|
+
limitations under the License.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
adcp
|