adcp 1.0.2__py3-none-any.whl → 1.0.3__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 CHANGED
@@ -46,7 +46,7 @@ from adcp.types.generated import (
46
46
  UpdateMediaBuyResponse,
47
47
  )
48
48
 
49
- __version__ = "1.0.2"
49
+ __version__ = "1.0.3"
50
50
 
51
51
  __all__ = [
52
52
  # Client classes
adcp/__main__.py CHANGED
@@ -36,13 +36,15 @@ def print_result(result: Any, json_output: bool = False) -> None:
36
36
  "data": result.data,
37
37
  "error": result.error,
38
38
  "metadata": result.metadata,
39
- "debug_info": {
40
- "request": result.debug_info.request,
41
- "response": result.debug_info.response,
42
- "duration_ms": result.debug_info.duration_ms,
43
- }
44
- if result.debug_info
45
- else None,
39
+ "debug_info": (
40
+ {
41
+ "request": result.debug_info.request,
42
+ "response": result.debug_info.response,
43
+ "duration_ms": result.debug_info.duration_ms,
44
+ }
45
+ if result.debug_info
46
+ else None
47
+ ),
46
48
  }
47
49
  )
48
50
  else:
@@ -73,10 +75,107 @@ async def execute_tool(
73
75
  config = AgentConfig(**agent_config)
74
76
 
75
77
  async with ADCPClient(config) as client:
76
- result = await client.call_tool(tool_name, payload)
78
+ # Dispatch to specific method based on tool name
79
+ result = await _dispatch_tool(client, tool_name, payload)
77
80
  print_result(result, json_output)
78
81
 
79
82
 
83
+ # Tool dispatch mapping - single source of truth for ADCP methods
84
+ # Types are filled at runtime to avoid circular imports
85
+ TOOL_DISPATCH: dict[str, tuple[str, type | None]] = {
86
+ "get_products": ("get_products", None),
87
+ "list_creative_formats": ("list_creative_formats", None),
88
+ "sync_creatives": ("sync_creatives", None),
89
+ "list_creatives": ("list_creatives", None),
90
+ "get_media_buy_delivery": ("get_media_buy_delivery", None),
91
+ "list_authorized_properties": ("list_authorized_properties", None),
92
+ "get_signals": ("get_signals", None),
93
+ "activate_signal": ("activate_signal", None),
94
+ "provide_performance_feedback": ("provide_performance_feedback", None),
95
+ }
96
+
97
+
98
+ async def _dispatch_tool(client: ADCPClient, tool_name: str, payload: dict[str, Any]) -> Any:
99
+ """Dispatch tool call to appropriate client method.
100
+
101
+ Args:
102
+ client: ADCP client instance
103
+ tool_name: Name of the tool to invoke
104
+ payload: Request payload as dict
105
+
106
+ Returns:
107
+ TaskResult with typed response or error
108
+
109
+ Raises:
110
+ ValidationError: If payload doesn't match request schema (caught and returned as TaskResult)
111
+ """
112
+ from pydantic import ValidationError
113
+
114
+ from adcp.types import generated as gen
115
+ from adcp.types.core import TaskResult, TaskStatus
116
+
117
+ # Lazy initialization of request types (avoid circular imports)
118
+ if TOOL_DISPATCH["get_products"][1] is None:
119
+ TOOL_DISPATCH["get_products"] = ("get_products", gen.GetProductsRequest)
120
+ TOOL_DISPATCH["list_creative_formats"] = (
121
+ "list_creative_formats",
122
+ gen.ListCreativeFormatsRequest,
123
+ )
124
+ TOOL_DISPATCH["sync_creatives"] = ("sync_creatives", gen.SyncCreativesRequest)
125
+ TOOL_DISPATCH["list_creatives"] = ("list_creatives", gen.ListCreativesRequest)
126
+ TOOL_DISPATCH["get_media_buy_delivery"] = (
127
+ "get_media_buy_delivery",
128
+ gen.GetMediaBuyDeliveryRequest,
129
+ )
130
+ TOOL_DISPATCH["list_authorized_properties"] = (
131
+ "list_authorized_properties",
132
+ gen.ListAuthorizedPropertiesRequest,
133
+ )
134
+ TOOL_DISPATCH["get_signals"] = ("get_signals", gen.GetSignalsRequest)
135
+ TOOL_DISPATCH["activate_signal"] = ("activate_signal", gen.ActivateSignalRequest)
136
+ TOOL_DISPATCH["provide_performance_feedback"] = (
137
+ "provide_performance_feedback",
138
+ gen.ProvidePerformanceFeedbackRequest,
139
+ )
140
+
141
+ # Check if tool exists
142
+ if tool_name not in TOOL_DISPATCH:
143
+ available = ", ".join(sorted(TOOL_DISPATCH.keys()))
144
+ return TaskResult(
145
+ status=TaskStatus.FAILED,
146
+ error=f"Unknown tool: {tool_name}. Available tools: {available}",
147
+ )
148
+
149
+ # Get method and request type
150
+ method_name, request_type = TOOL_DISPATCH[tool_name]
151
+
152
+ # Type guard - request_type should be initialized by this point
153
+ if request_type is None:
154
+ return TaskResult(
155
+ status=TaskStatus.FAILED,
156
+ error=f"Internal error: {tool_name} request type not initialized",
157
+ )
158
+
159
+ method = getattr(client, method_name)
160
+
161
+ # Validate and invoke
162
+ try:
163
+ request = request_type(**payload)
164
+ return await method(request)
165
+ except ValidationError as e:
166
+ # User-friendly error for invalid payloads
167
+ error_details = []
168
+ for error in e.errors():
169
+ field = ".".join(str(loc) for loc in error["loc"])
170
+ msg = error["msg"]
171
+ error_details.append(f" - {field}: {msg}")
172
+
173
+ return TaskResult(
174
+ status=TaskStatus.FAILED,
175
+ error=f"Invalid request payload for {tool_name}:\n" + "\n".join(error_details),
176
+ )
177
+
178
+
80
179
  def load_payload(payload_arg: str | None) -> dict[str, Any]:
81
180
  """Load payload from argument (JSON, @file, or stdin)."""
82
181
  if not payload_arg:
adcp/client.py CHANGED
@@ -41,6 +41,7 @@ from adcp.types.generated import (
41
41
  ProvidePerformanceFeedbackResponse,
42
42
  SyncCreativesRequest,
43
43
  SyncCreativesResponse,
44
+ WebhookPayload,
44
45
  )
45
46
  from adcp.utils.operation_id import create_operation_id
46
47
 
@@ -123,7 +124,7 @@ class ADCPClient:
123
124
  )
124
125
  )
125
126
 
126
- result = await self.adapter.call_tool("get_products", params)
127
+ raw_result = await self.adapter.get_products(params)
127
128
 
128
129
  self._emit_activity(
129
130
  Activity(
@@ -131,12 +132,12 @@ class ADCPClient:
131
132
  operation_id=operation_id,
132
133
  agent_id=self.agent_config.id,
133
134
  task_type="get_products",
134
- status=result.status,
135
+ status=raw_result.status,
135
136
  timestamp=datetime.now(timezone.utc).isoformat(),
136
137
  )
137
138
  )
138
139
 
139
- return result
140
+ return self.adapter._parse_response(raw_result, GetProductsResponse)
140
141
 
141
142
  async def list_creative_formats(
142
143
  self,
@@ -164,7 +165,7 @@ class ADCPClient:
164
165
  )
165
166
  )
166
167
 
167
- result = await self.adapter.call_tool("list_creative_formats", params)
168
+ raw_result = await self.adapter.list_creative_formats(params)
168
169
 
169
170
  self._emit_activity(
170
171
  Activity(
@@ -172,12 +173,13 @@ class ADCPClient:
172
173
  operation_id=operation_id,
173
174
  agent_id=self.agent_config.id,
174
175
  task_type="list_creative_formats",
175
- status=result.status,
176
+ status=raw_result.status,
176
177
  timestamp=datetime.now(timezone.utc).isoformat(),
177
178
  )
178
179
  )
179
180
 
180
- return result
181
+ # Parse response using adapter's helper
182
+ return self.adapter._parse_response(raw_result, ListCreativeFormatsResponse)
181
183
 
182
184
  async def sync_creatives(
183
185
  self,
@@ -205,7 +207,7 @@ class ADCPClient:
205
207
  )
206
208
  )
207
209
 
208
- result = await self.adapter.call_tool("sync_creatives", params)
210
+ raw_result = await self.adapter.sync_creatives(params)
209
211
 
210
212
  self._emit_activity(
211
213
  Activity(
@@ -213,12 +215,12 @@ class ADCPClient:
213
215
  operation_id=operation_id,
214
216
  agent_id=self.agent_config.id,
215
217
  task_type="sync_creatives",
216
- status=result.status,
218
+ status=raw_result.status,
217
219
  timestamp=datetime.now(timezone.utc).isoformat(),
218
220
  )
219
221
  )
220
222
 
221
- return result
223
+ return self.adapter._parse_response(raw_result, SyncCreativesResponse)
222
224
 
223
225
  async def list_creatives(
224
226
  self,
@@ -246,7 +248,7 @@ class ADCPClient:
246
248
  )
247
249
  )
248
250
 
249
- result = await self.adapter.call_tool("list_creatives", params)
251
+ raw_result = await self.adapter.list_creatives(params)
250
252
 
251
253
  self._emit_activity(
252
254
  Activity(
@@ -254,12 +256,12 @@ class ADCPClient:
254
256
  operation_id=operation_id,
255
257
  agent_id=self.agent_config.id,
256
258
  task_type="list_creatives",
257
- status=result.status,
259
+ status=raw_result.status,
258
260
  timestamp=datetime.now(timezone.utc).isoformat(),
259
261
  )
260
262
  )
261
263
 
262
- return result
264
+ return self.adapter._parse_response(raw_result, ListCreativesResponse)
263
265
 
264
266
  async def get_media_buy_delivery(
265
267
  self,
@@ -287,7 +289,7 @@ class ADCPClient:
287
289
  )
288
290
  )
289
291
 
290
- result = await self.adapter.call_tool("get_media_buy_delivery", params)
292
+ raw_result = await self.adapter.get_media_buy_delivery(params)
291
293
 
292
294
  self._emit_activity(
293
295
  Activity(
@@ -295,12 +297,12 @@ class ADCPClient:
295
297
  operation_id=operation_id,
296
298
  agent_id=self.agent_config.id,
297
299
  task_type="get_media_buy_delivery",
298
- status=result.status,
300
+ status=raw_result.status,
299
301
  timestamp=datetime.now(timezone.utc).isoformat(),
300
302
  )
301
303
  )
302
304
 
303
- return result
305
+ return self.adapter._parse_response(raw_result, GetMediaBuyDeliveryResponse)
304
306
 
305
307
  async def list_authorized_properties(
306
308
  self,
@@ -328,7 +330,7 @@ class ADCPClient:
328
330
  )
329
331
  )
330
332
 
331
- result = await self.adapter.call_tool("list_authorized_properties", params)
333
+ raw_result = await self.adapter.list_authorized_properties(params)
332
334
 
333
335
  self._emit_activity(
334
336
  Activity(
@@ -336,12 +338,12 @@ class ADCPClient:
336
338
  operation_id=operation_id,
337
339
  agent_id=self.agent_config.id,
338
340
  task_type="list_authorized_properties",
339
- status=result.status,
341
+ status=raw_result.status,
340
342
  timestamp=datetime.now(timezone.utc).isoformat(),
341
343
  )
342
344
  )
343
345
 
344
- return result
346
+ return self.adapter._parse_response(raw_result, ListAuthorizedPropertiesResponse)
345
347
 
346
348
  async def get_signals(
347
349
  self,
@@ -369,7 +371,7 @@ class ADCPClient:
369
371
  )
370
372
  )
371
373
 
372
- result = await self.adapter.call_tool("get_signals", params)
374
+ raw_result = await self.adapter.get_signals(params)
373
375
 
374
376
  self._emit_activity(
375
377
  Activity(
@@ -377,12 +379,12 @@ class ADCPClient:
377
379
  operation_id=operation_id,
378
380
  agent_id=self.agent_config.id,
379
381
  task_type="get_signals",
380
- status=result.status,
382
+ status=raw_result.status,
381
383
  timestamp=datetime.now(timezone.utc).isoformat(),
382
384
  )
383
385
  )
384
386
 
385
- return result
387
+ return self.adapter._parse_response(raw_result, GetSignalsResponse)
386
388
 
387
389
  async def activate_signal(
388
390
  self,
@@ -410,7 +412,7 @@ class ADCPClient:
410
412
  )
411
413
  )
412
414
 
413
- result = await self.adapter.call_tool("activate_signal", params)
415
+ raw_result = await self.adapter.activate_signal(params)
414
416
 
415
417
  self._emit_activity(
416
418
  Activity(
@@ -418,12 +420,12 @@ class ADCPClient:
418
420
  operation_id=operation_id,
419
421
  agent_id=self.agent_config.id,
420
422
  task_type="activate_signal",
421
- status=result.status,
423
+ status=raw_result.status,
422
424
  timestamp=datetime.now(timezone.utc).isoformat(),
423
425
  )
424
426
  )
425
427
 
426
- return result
428
+ return self.adapter._parse_response(raw_result, ActivateSignalResponse)
427
429
 
428
430
  async def provide_performance_feedback(
429
431
  self,
@@ -451,7 +453,7 @@ class ADCPClient:
451
453
  )
452
454
  )
453
455
 
454
- result = await self.adapter.call_tool("provide_performance_feedback", params)
456
+ raw_result = await self.adapter.provide_performance_feedback(params)
455
457
 
456
458
  self._emit_activity(
457
459
  Activity(
@@ -459,50 +461,12 @@ class ADCPClient:
459
461
  operation_id=operation_id,
460
462
  agent_id=self.agent_config.id,
461
463
  task_type="provide_performance_feedback",
462
- status=result.status,
464
+ status=raw_result.status,
463
465
  timestamp=datetime.now(timezone.utc).isoformat(),
464
466
  )
465
467
  )
466
468
 
467
- return result
468
-
469
- async def call_tool(self, tool_name: str, params: dict[str, Any]) -> TaskResult[Any]:
470
- """
471
- Call any tool on the agent.
472
-
473
- Args:
474
- tool_name: Name of the tool to call
475
- params: Tool parameters
476
-
477
- Returns:
478
- TaskResult with the response
479
- """
480
- operation_id = create_operation_id()
481
-
482
- self._emit_activity(
483
- Activity(
484
- type=ActivityType.PROTOCOL_REQUEST,
485
- operation_id=operation_id,
486
- agent_id=self.agent_config.id,
487
- task_type=tool_name,
488
- timestamp=datetime.now(timezone.utc).isoformat(),
489
- )
490
- )
491
-
492
- result = await self.adapter.call_tool(tool_name, params)
493
-
494
- self._emit_activity(
495
- Activity(
496
- type=ActivityType.PROTOCOL_RESPONSE,
497
- operation_id=operation_id,
498
- agent_id=self.agent_config.id,
499
- task_type=tool_name,
500
- status=result.status,
501
- timestamp=datetime.now(timezone.utc).isoformat(),
502
- )
503
- )
504
-
505
- return result
469
+ return self.adapter._parse_response(raw_result, ProvidePerformanceFeedbackResponse)
506
470
 
507
471
  async def list_tools(self) -> list[str]:
508
472
  """
@@ -548,41 +512,132 @@ class ADCPClient:
548
512
 
549
513
  return hmac.compare_digest(signature, expected_signature)
550
514
 
515
+ def _parse_webhook_result(self, webhook: WebhookPayload) -> TaskResult[Any]:
516
+ """
517
+ Parse webhook payload into typed TaskResult based on task_type.
518
+
519
+ Args:
520
+ webhook: Validated webhook payload
521
+
522
+ Returns:
523
+ TaskResult with task-specific typed response data
524
+ """
525
+ from adcp.types.core import TaskStatus
526
+ from adcp.utils.response_parser import parse_json_or_text
527
+
528
+ # Map task types to their response types (using string literals, not enum)
529
+ response_type_map: dict[str, type] = {
530
+ "get_products": GetProductsResponse,
531
+ "list_creative_formats": ListCreativeFormatsResponse,
532
+ "sync_creatives": SyncCreativesResponse,
533
+ "list_creatives": ListCreativesResponse,
534
+ "get_media_buy_delivery": GetMediaBuyDeliveryResponse,
535
+ "list_authorized_properties": ListAuthorizedPropertiesResponse,
536
+ "get_signals": GetSignalsResponse,
537
+ "activate_signal": ActivateSignalResponse,
538
+ "provide_performance_feedback": ProvidePerformanceFeedbackResponse,
539
+ }
540
+
541
+ # Handle completed tasks with result parsing
542
+
543
+ if webhook.status == "completed" and webhook.result is not None:
544
+ response_type = response_type_map.get(webhook.task_type)
545
+ if response_type:
546
+ try:
547
+ parsed_result: Any = parse_json_or_text(webhook.result, response_type)
548
+ return TaskResult[Any](
549
+ status=TaskStatus.COMPLETED,
550
+ data=parsed_result,
551
+ success=True,
552
+ metadata={
553
+ "task_id": webhook.task_id,
554
+ "operation_id": webhook.operation_id,
555
+ "timestamp": webhook.timestamp,
556
+ "message": webhook.message,
557
+ },
558
+ )
559
+ except ValueError as e:
560
+ logger.warning(f"Failed to parse webhook result: {e}")
561
+ # Fall through to untyped result
562
+
563
+ # Handle failed, input-required, or unparseable results
564
+ # Convert webhook status string to TaskStatus enum
565
+ try:
566
+ task_status = TaskStatus(webhook.status)
567
+ except ValueError:
568
+ # Fallback to FAILED for unknown statuses
569
+ task_status = TaskStatus.FAILED
570
+
571
+ return TaskResult[Any](
572
+ status=task_status,
573
+ data=webhook.result,
574
+ success=webhook.status == "completed",
575
+ error=webhook.error if isinstance(webhook.error, str) else None,
576
+ metadata={
577
+ "task_id": webhook.task_id,
578
+ "operation_id": webhook.operation_id,
579
+ "timestamp": webhook.timestamp,
580
+ "message": webhook.message,
581
+ "context_id": webhook.context_id,
582
+ "progress": webhook.progress,
583
+ },
584
+ )
585
+
551
586
  async def handle_webhook(
552
587
  self,
553
588
  payload: dict[str, Any],
554
589
  signature: str | None = None,
555
- ) -> None:
590
+ ) -> TaskResult[Any]:
556
591
  """
557
- Handle incoming webhook.
592
+ Handle incoming webhook and return typed result.
593
+
594
+ This method:
595
+ 1. Verifies webhook signature (if provided)
596
+ 2. Validates payload against WebhookPayload schema
597
+ 3. Parses task-specific result data into typed response
598
+ 4. Emits activity for monitoring
558
599
 
559
600
  Args:
560
- payload: Webhook payload
561
- signature: Webhook signature for verification
601
+ payload: Webhook payload dict
602
+ signature: Optional HMAC-SHA256 signature for verification
603
+
604
+ Returns:
605
+ TaskResult with parsed task-specific response data
562
606
 
563
607
  Raises:
564
608
  ADCPWebhookSignatureError: If signature verification fails
609
+ ValidationError: If payload doesn't match WebhookPayload schema
610
+
611
+ Example:
612
+ >>> result = await client.handle_webhook(payload, signature)
613
+ >>> if result.success and isinstance(result.data, GetProductsResponse):
614
+ >>> print(f"Found {len(result.data.products)} products")
565
615
  """
616
+ # Verify signature before processing
566
617
  if signature and not self._verify_webhook_signature(payload, signature):
567
618
  logger.warning(
568
619
  f"Webhook signature verification failed for agent {self.agent_config.id}"
569
620
  )
570
621
  raise ADCPWebhookSignatureError("Invalid webhook signature")
571
622
 
572
- operation_id = payload.get("operation_id", "unknown")
573
- task_type = payload.get("task_type", "unknown")
623
+ # Validate and parse webhook payload
624
+ webhook = WebhookPayload.model_validate(payload)
574
625
 
626
+ # Emit activity for monitoring
575
627
  self._emit_activity(
576
628
  Activity(
577
629
  type=ActivityType.WEBHOOK_RECEIVED,
578
- operation_id=operation_id,
630
+ operation_id=webhook.operation_id or "unknown",
579
631
  agent_id=self.agent_config.id,
580
- task_type=task_type,
632
+ task_type=webhook.task_type,
581
633
  timestamp=datetime.now(timezone.utc).isoformat(),
582
634
  metadata={"payload": payload},
583
635
  )
584
636
  )
585
637
 
638
+ # Parse and return typed result
639
+ return self._parse_webhook_result(webhook)
640
+
586
641
 
587
642
  class ADCPMultiAgentClient:
588
643
  """Client for managing multiple AdCP agents."""
adcp/protocols/a2a.py CHANGED
@@ -54,7 +54,7 @@ class A2AAdapter(ProtocolAdapter):
54
54
  await self._client.aclose()
55
55
  self._client = None
56
56
 
57
- async def call_tool(self, tool_name: str, params: dict[str, Any]) -> TaskResult[Any]:
57
+ async def _call_a2a_tool(self, tool_name: str, params: dict[str, Any]) -> TaskResult[Any]:
58
58
  """
59
59
  Call a tool using A2A protocol.
60
60
 
@@ -98,9 +98,11 @@ class A2AAdapter(ProtocolAdapter):
98
98
  "url": url,
99
99
  "method": "POST",
100
100
  "headers": {
101
- k: v
102
- if k.lower() not in ("authorization", self.agent_config.auth_header.lower())
103
- else "***"
101
+ k: (
102
+ v
103
+ if k.lower() not in ("authorization", self.agent_config.auth_header.lower())
104
+ else "***"
105
+ )
104
106
  for k, v in headers.items()
105
107
  },
106
108
  "body": request_data,
@@ -202,6 +204,46 @@ class A2AAdapter(ProtocolAdapter):
202
204
 
203
205
  return first_part
204
206
 
207
+ # ========================================================================
208
+ # ADCP Protocol Methods
209
+ # ========================================================================
210
+
211
+ async def get_products(self, params: dict[str, Any]) -> TaskResult[Any]:
212
+ """Get advertising products."""
213
+ return await self._call_a2a_tool("get_products", params)
214
+
215
+ async def list_creative_formats(self, params: dict[str, Any]) -> TaskResult[Any]:
216
+ """List supported creative formats."""
217
+ return await self._call_a2a_tool("list_creative_formats", params)
218
+
219
+ async def sync_creatives(self, params: dict[str, Any]) -> TaskResult[Any]:
220
+ """Sync creatives."""
221
+ return await self._call_a2a_tool("sync_creatives", params)
222
+
223
+ async def list_creatives(self, params: dict[str, Any]) -> TaskResult[Any]:
224
+ """List creatives."""
225
+ return await self._call_a2a_tool("list_creatives", params)
226
+
227
+ async def get_media_buy_delivery(self, params: dict[str, Any]) -> TaskResult[Any]:
228
+ """Get media buy delivery."""
229
+ return await self._call_a2a_tool("get_media_buy_delivery", params)
230
+
231
+ async def list_authorized_properties(self, params: dict[str, Any]) -> TaskResult[Any]:
232
+ """List authorized properties."""
233
+ return await self._call_a2a_tool("list_authorized_properties", params)
234
+
235
+ async def get_signals(self, params: dict[str, Any]) -> TaskResult[Any]:
236
+ """Get signals."""
237
+ return await self._call_a2a_tool("get_signals", params)
238
+
239
+ async def activate_signal(self, params: dict[str, Any]) -> TaskResult[Any]:
240
+ """Activate signal."""
241
+ return await self._call_a2a_tool("activate_signal", params)
242
+
243
+ async def provide_performance_feedback(self, params: dict[str, Any]) -> TaskResult[Any]:
244
+ """Provide performance feedback."""
245
+ return await self._call_a2a_tool("provide_performance_feedback", params)
246
+
205
247
  async def list_tools(self) -> list[str]:
206
248
  """
207
249
  List available tools from A2A agent.