adcp 0.1.2__py3-none-any.whl → 1.0.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/client.py CHANGED
@@ -1,12 +1,17 @@
1
+ from __future__ import annotations
2
+
1
3
  """Main client classes for AdCP."""
2
4
 
5
+ import hashlib
6
+ import hmac
3
7
  import json
8
+ import logging
4
9
  import os
5
10
  from collections.abc import Callable
6
- from datetime import datetime
11
+ from datetime import datetime, timezone
7
12
  from typing import Any
8
- from uuid import uuid4
9
13
 
14
+ from adcp.exceptions import ADCPWebhookSignatureError
10
15
  from adcp.protocols.a2a import A2AAdapter
11
16
  from adcp.protocols.base import ProtocolAdapter
12
17
  from adcp.protocols.mcp import MCPAdapter
@@ -17,11 +22,29 @@ from adcp.types.core import (
17
22
  Protocol,
18
23
  TaskResult,
19
24
  )
25
+ from adcp.types.generated import (
26
+ ActivateSignalRequest,
27
+ ActivateSignalResponse,
28
+ GetMediaBuyDeliveryRequest,
29
+ GetMediaBuyDeliveryResponse,
30
+ GetProductsRequest,
31
+ GetProductsResponse,
32
+ GetSignalsRequest,
33
+ GetSignalsResponse,
34
+ ListAuthorizedPropertiesRequest,
35
+ ListAuthorizedPropertiesResponse,
36
+ ListCreativeFormatsRequest,
37
+ ListCreativeFormatsResponse,
38
+ ListCreativesRequest,
39
+ ListCreativesResponse,
40
+ ProvidePerformanceFeedbackRequest,
41
+ ProvidePerformanceFeedbackResponse,
42
+ SyncCreativesRequest,
43
+ SyncCreativesResponse,
44
+ )
45
+ from adcp.utils.operation_id import create_operation_id
20
46
 
21
-
22
- def create_operation_id() -> str:
23
- """Generate a unique operation ID."""
24
- return f"op_{uuid4().hex[:12]}"
47
+ logger = logging.getLogger(__name__)
25
48
 
26
49
 
27
50
  class ADCPClient:
@@ -74,10 +97,21 @@ class ADCPClient:
74
97
  if self.on_activity:
75
98
  self.on_activity(activity)
76
99
 
77
- async def get_products(self, brief: str, **kwargs: Any) -> TaskResult[Any]:
78
- """Get advertising products."""
100
+ async def get_products(
101
+ self,
102
+ request: GetProductsRequest,
103
+ ) -> TaskResult[GetProductsResponse]:
104
+ """
105
+ Get advertising products.
106
+
107
+ Args:
108
+ request: Request parameters
109
+
110
+ Returns:
111
+ TaskResult containing GetProductsResponse
112
+ """
79
113
  operation_id = create_operation_id()
80
- params = {"brief": brief, **kwargs}
114
+ params = request.model_dump(exclude_none=True)
81
115
 
82
116
  self._emit_activity(
83
117
  Activity(
@@ -85,7 +119,7 @@ class ADCPClient:
85
119
  operation_id=operation_id,
86
120
  agent_id=self.agent_config.id,
87
121
  task_type="get_products",
88
- timestamp=datetime.utcnow().isoformat(),
122
+ timestamp=datetime.now(timezone.utc).isoformat(),
89
123
  )
90
124
  )
91
125
 
@@ -98,15 +132,27 @@ class ADCPClient:
98
132
  agent_id=self.agent_config.id,
99
133
  task_type="get_products",
100
134
  status=result.status,
101
- timestamp=datetime.utcnow().isoformat(),
135
+ timestamp=datetime.now(timezone.utc).isoformat(),
102
136
  )
103
137
  )
104
138
 
105
139
  return result
106
140
 
107
- async def list_creative_formats(self, **kwargs: Any) -> TaskResult[Any]:
108
- """List supported creative formats."""
141
+ async def list_creative_formats(
142
+ self,
143
+ request: ListCreativeFormatsRequest,
144
+ ) -> TaskResult[ListCreativeFormatsResponse]:
145
+ """
146
+ List supported creative formats.
147
+
148
+ Args:
149
+ request: Request parameters
150
+
151
+ Returns:
152
+ TaskResult containing ListCreativeFormatsResponse
153
+ """
109
154
  operation_id = create_operation_id()
155
+ params = request.model_dump(exclude_none=True)
110
156
 
111
157
  self._emit_activity(
112
158
  Activity(
@@ -114,11 +160,11 @@ class ADCPClient:
114
160
  operation_id=operation_id,
115
161
  agent_id=self.agent_config.id,
116
162
  task_type="list_creative_formats",
117
- timestamp=datetime.utcnow().isoformat(),
163
+ timestamp=datetime.now(timezone.utc).isoformat(),
118
164
  )
119
165
  )
120
166
 
121
- result = await self.adapter.call_tool("list_creative_formats", kwargs)
167
+ result = await self.adapter.call_tool("list_creative_formats", params)
122
168
 
123
169
  self._emit_activity(
124
170
  Activity(
@@ -127,217 +173,310 @@ class ADCPClient:
127
173
  agent_id=self.agent_config.id,
128
174
  task_type="list_creative_formats",
129
175
  status=result.status,
130
- timestamp=datetime.utcnow().isoformat(),
176
+ timestamp=datetime.now(timezone.utc).isoformat(),
131
177
  )
132
178
  )
133
179
 
134
180
  return result
135
181
 
136
- async def create_media_buy(self, **kwargs: Any) -> TaskResult[Any]:
137
- """Create a new media buy."""
182
+ async def sync_creatives(
183
+ self,
184
+ request: SyncCreativesRequest,
185
+ ) -> TaskResult[SyncCreativesResponse]:
186
+ """
187
+ Sync Creatives.
188
+
189
+ Args:
190
+ request: Request parameters
191
+
192
+ Returns:
193
+ TaskResult containing SyncCreativesResponse
194
+ """
138
195
  operation_id = create_operation_id()
196
+ params = request.model_dump(exclude_none=True)
139
197
 
140
198
  self._emit_activity(
141
199
  Activity(
142
200
  type=ActivityType.PROTOCOL_REQUEST,
143
201
  operation_id=operation_id,
144
202
  agent_id=self.agent_config.id,
145
- task_type="create_media_buy",
146
- timestamp=datetime.utcnow().isoformat(),
203
+ task_type="sync_creatives",
204
+ timestamp=datetime.now(timezone.utc).isoformat(),
147
205
  )
148
206
  )
149
207
 
150
- result = await self.adapter.call_tool("create_media_buy", kwargs)
208
+ result = await self.adapter.call_tool("sync_creatives", params)
151
209
 
152
210
  self._emit_activity(
153
211
  Activity(
154
212
  type=ActivityType.PROTOCOL_RESPONSE,
155
213
  operation_id=operation_id,
156
214
  agent_id=self.agent_config.id,
157
- task_type="create_media_buy",
215
+ task_type="sync_creatives",
158
216
  status=result.status,
159
- timestamp=datetime.utcnow().isoformat(),
217
+ timestamp=datetime.now(timezone.utc).isoformat(),
160
218
  )
161
219
  )
162
220
 
163
221
  return result
164
222
 
165
- async def update_media_buy(self, **kwargs: Any) -> TaskResult[Any]:
166
- """Update an existing media buy."""
223
+ async def list_creatives(
224
+ self,
225
+ request: ListCreativesRequest,
226
+ ) -> TaskResult[ListCreativesResponse]:
227
+ """
228
+ List Creatives.
229
+
230
+ Args:
231
+ request: Request parameters
232
+
233
+ Returns:
234
+ TaskResult containing ListCreativesResponse
235
+ """
167
236
  operation_id = create_operation_id()
237
+ params = request.model_dump(exclude_none=True)
168
238
 
169
239
  self._emit_activity(
170
240
  Activity(
171
241
  type=ActivityType.PROTOCOL_REQUEST,
172
242
  operation_id=operation_id,
173
243
  agent_id=self.agent_config.id,
174
- task_type="update_media_buy",
175
- timestamp=datetime.utcnow().isoformat(),
244
+ task_type="list_creatives",
245
+ timestamp=datetime.now(timezone.utc).isoformat(),
176
246
  )
177
247
  )
178
248
 
179
- result = await self.adapter.call_tool("update_media_buy", kwargs)
249
+ result = await self.adapter.call_tool("list_creatives", params)
180
250
 
181
251
  self._emit_activity(
182
252
  Activity(
183
253
  type=ActivityType.PROTOCOL_RESPONSE,
184
254
  operation_id=operation_id,
185
255
  agent_id=self.agent_config.id,
186
- task_type="update_media_buy",
256
+ task_type="list_creatives",
187
257
  status=result.status,
188
- timestamp=datetime.utcnow().isoformat(),
258
+ timestamp=datetime.now(timezone.utc).isoformat(),
189
259
  )
190
260
  )
191
261
 
192
262
  return result
193
263
 
194
- async def sync_creatives(self, **kwargs: Any) -> TaskResult[Any]:
195
- """Synchronize creatives with the agent."""
264
+ async def get_media_buy_delivery(
265
+ self,
266
+ request: GetMediaBuyDeliveryRequest,
267
+ ) -> TaskResult[GetMediaBuyDeliveryResponse]:
268
+ """
269
+ Get Media Buy Delivery.
270
+
271
+ Args:
272
+ request: Request parameters
273
+
274
+ Returns:
275
+ TaskResult containing GetMediaBuyDeliveryResponse
276
+ """
196
277
  operation_id = create_operation_id()
278
+ params = request.model_dump(exclude_none=True)
197
279
 
198
280
  self._emit_activity(
199
281
  Activity(
200
282
  type=ActivityType.PROTOCOL_REQUEST,
201
283
  operation_id=operation_id,
202
284
  agent_id=self.agent_config.id,
203
- task_type="sync_creatives",
204
- timestamp=datetime.utcnow().isoformat(),
285
+ task_type="get_media_buy_delivery",
286
+ timestamp=datetime.now(timezone.utc).isoformat(),
205
287
  )
206
288
  )
207
289
 
208
- result = await self.adapter.call_tool("sync_creatives", kwargs)
290
+ result = await self.adapter.call_tool("get_media_buy_delivery", params)
209
291
 
210
292
  self._emit_activity(
211
293
  Activity(
212
294
  type=ActivityType.PROTOCOL_RESPONSE,
213
295
  operation_id=operation_id,
214
296
  agent_id=self.agent_config.id,
215
- task_type="sync_creatives",
297
+ task_type="get_media_buy_delivery",
216
298
  status=result.status,
217
- timestamp=datetime.utcnow().isoformat(),
299
+ timestamp=datetime.now(timezone.utc).isoformat(),
218
300
  )
219
301
  )
220
302
 
221
303
  return result
222
304
 
223
- async def list_creatives(self, **kwargs: Any) -> TaskResult[Any]:
224
- """List creatives for a media buy."""
305
+ async def list_authorized_properties(
306
+ self,
307
+ request: ListAuthorizedPropertiesRequest,
308
+ ) -> TaskResult[ListAuthorizedPropertiesResponse]:
309
+ """
310
+ List Authorized Properties.
311
+
312
+ Args:
313
+ request: Request parameters
314
+
315
+ Returns:
316
+ TaskResult containing ListAuthorizedPropertiesResponse
317
+ """
225
318
  operation_id = create_operation_id()
319
+ params = request.model_dump(exclude_none=True)
226
320
 
227
321
  self._emit_activity(
228
322
  Activity(
229
323
  type=ActivityType.PROTOCOL_REQUEST,
230
324
  operation_id=operation_id,
231
325
  agent_id=self.agent_config.id,
232
- task_type="list_creatives",
233
- timestamp=datetime.utcnow().isoformat(),
326
+ task_type="list_authorized_properties",
327
+ timestamp=datetime.now(timezone.utc).isoformat(),
234
328
  )
235
329
  )
236
330
 
237
- result = await self.adapter.call_tool("list_creatives", kwargs)
331
+ result = await self.adapter.call_tool("list_authorized_properties", params)
238
332
 
239
333
  self._emit_activity(
240
334
  Activity(
241
335
  type=ActivityType.PROTOCOL_RESPONSE,
242
336
  operation_id=operation_id,
243
337
  agent_id=self.agent_config.id,
244
- task_type="list_creatives",
338
+ task_type="list_authorized_properties",
245
339
  status=result.status,
246
- timestamp=datetime.utcnow().isoformat(),
340
+ timestamp=datetime.now(timezone.utc).isoformat(),
247
341
  )
248
342
  )
249
343
 
250
344
  return result
251
345
 
252
- async def get_media_buy_delivery(self, **kwargs: Any) -> TaskResult[Any]:
253
- """Get delivery metrics for a media buy."""
346
+ async def get_signals(
347
+ self,
348
+ request: GetSignalsRequest,
349
+ ) -> TaskResult[GetSignalsResponse]:
350
+ """
351
+ Get Signals.
352
+
353
+ Args:
354
+ request: Request parameters
355
+
356
+ Returns:
357
+ TaskResult containing GetSignalsResponse
358
+ """
254
359
  operation_id = create_operation_id()
360
+ params = request.model_dump(exclude_none=True)
255
361
 
256
362
  self._emit_activity(
257
363
  Activity(
258
364
  type=ActivityType.PROTOCOL_REQUEST,
259
365
  operation_id=operation_id,
260
366
  agent_id=self.agent_config.id,
261
- task_type="get_media_buy_delivery",
262
- timestamp=datetime.utcnow().isoformat(),
367
+ task_type="get_signals",
368
+ timestamp=datetime.now(timezone.utc).isoformat(),
263
369
  )
264
370
  )
265
371
 
266
- result = await self.adapter.call_tool("get_media_buy_delivery", kwargs)
372
+ result = await self.adapter.call_tool("get_signals", params)
267
373
 
268
374
  self._emit_activity(
269
375
  Activity(
270
376
  type=ActivityType.PROTOCOL_RESPONSE,
271
377
  operation_id=operation_id,
272
378
  agent_id=self.agent_config.id,
273
- task_type="get_media_buy_delivery",
379
+ task_type="get_signals",
274
380
  status=result.status,
275
- timestamp=datetime.utcnow().isoformat(),
381
+ timestamp=datetime.now(timezone.utc).isoformat(),
276
382
  )
277
383
  )
278
384
 
279
385
  return result
280
386
 
281
- async def list_authorized_properties(self, **kwargs: Any) -> TaskResult[Any]:
282
- """List properties this agent is authorized to sell."""
387
+ async def activate_signal(
388
+ self,
389
+ request: ActivateSignalRequest,
390
+ ) -> TaskResult[ActivateSignalResponse]:
391
+ """
392
+ Activate Signal.
393
+
394
+ Args:
395
+ request: Request parameters
396
+
397
+ Returns:
398
+ TaskResult containing ActivateSignalResponse
399
+ """
283
400
  operation_id = create_operation_id()
401
+ params = request.model_dump(exclude_none=True)
284
402
 
285
403
  self._emit_activity(
286
404
  Activity(
287
405
  type=ActivityType.PROTOCOL_REQUEST,
288
406
  operation_id=operation_id,
289
407
  agent_id=self.agent_config.id,
290
- task_type="list_authorized_properties",
291
- timestamp=datetime.utcnow().isoformat(),
408
+ task_type="activate_signal",
409
+ timestamp=datetime.now(timezone.utc).isoformat(),
292
410
  )
293
411
  )
294
412
 
295
- result = await self.adapter.call_tool("list_authorized_properties", kwargs)
413
+ result = await self.adapter.call_tool("activate_signal", params)
296
414
 
297
415
  self._emit_activity(
298
416
  Activity(
299
417
  type=ActivityType.PROTOCOL_RESPONSE,
300
418
  operation_id=operation_id,
301
419
  agent_id=self.agent_config.id,
302
- task_type="list_authorized_properties",
420
+ task_type="activate_signal",
303
421
  status=result.status,
304
- timestamp=datetime.utcnow().isoformat(),
422
+ timestamp=datetime.now(timezone.utc).isoformat(),
305
423
  )
306
424
  )
307
425
 
308
426
  return result
309
427
 
310
- async def get_signals(self, **kwargs: Any) -> TaskResult[Any]:
311
- """Get available signals for targeting."""
428
+ async def provide_performance_feedback(
429
+ self,
430
+ request: ProvidePerformanceFeedbackRequest,
431
+ ) -> TaskResult[ProvidePerformanceFeedbackResponse]:
432
+ """
433
+ Provide Performance Feedback.
434
+
435
+ Args:
436
+ request: Request parameters
437
+
438
+ Returns:
439
+ TaskResult containing ProvidePerformanceFeedbackResponse
440
+ """
312
441
  operation_id = create_operation_id()
442
+ params = request.model_dump(exclude_none=True)
313
443
 
314
444
  self._emit_activity(
315
445
  Activity(
316
446
  type=ActivityType.PROTOCOL_REQUEST,
317
447
  operation_id=operation_id,
318
448
  agent_id=self.agent_config.id,
319
- task_type="get_signals",
320
- timestamp=datetime.utcnow().isoformat(),
449
+ task_type="provide_performance_feedback",
450
+ timestamp=datetime.now(timezone.utc).isoformat(),
321
451
  )
322
452
  )
323
453
 
324
- result = await self.adapter.call_tool("get_signals", kwargs)
454
+ result = await self.adapter.call_tool("provide_performance_feedback", params)
325
455
 
326
456
  self._emit_activity(
327
457
  Activity(
328
458
  type=ActivityType.PROTOCOL_RESPONSE,
329
459
  operation_id=operation_id,
330
460
  agent_id=self.agent_config.id,
331
- task_type="get_signals",
461
+ task_type="provide_performance_feedback",
332
462
  status=result.status,
333
- timestamp=datetime.utcnow().isoformat(),
463
+ timestamp=datetime.now(timezone.utc).isoformat(),
334
464
  )
335
465
  )
336
466
 
337
467
  return result
338
468
 
339
- async def activate_signal(self, **kwargs: Any) -> TaskResult[Any]:
340
- """Activate a signal for use in campaigns."""
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
+ """
341
480
  operation_id = create_operation_id()
342
481
 
343
482
  self._emit_activity(
@@ -345,54 +484,69 @@ class ADCPClient:
345
484
  type=ActivityType.PROTOCOL_REQUEST,
346
485
  operation_id=operation_id,
347
486
  agent_id=self.agent_config.id,
348
- task_type="activate_signal",
349
- timestamp=datetime.utcnow().isoformat(),
487
+ task_type=tool_name,
488
+ timestamp=datetime.now(timezone.utc).isoformat(),
350
489
  )
351
490
  )
352
491
 
353
- result = await self.adapter.call_tool("activate_signal", kwargs)
492
+ result = await self.adapter.call_tool(tool_name, params)
354
493
 
355
494
  self._emit_activity(
356
495
  Activity(
357
496
  type=ActivityType.PROTOCOL_RESPONSE,
358
497
  operation_id=operation_id,
359
498
  agent_id=self.agent_config.id,
360
- task_type="activate_signal",
499
+ task_type=tool_name,
361
500
  status=result.status,
362
- timestamp=datetime.utcnow().isoformat(),
501
+ timestamp=datetime.now(timezone.utc).isoformat(),
363
502
  )
364
503
  )
365
504
 
366
505
  return result
367
506
 
368
- async def provide_performance_feedback(self, **kwargs: Any) -> TaskResult[Any]:
369
- """Provide performance feedback for a campaign."""
370
- operation_id = create_operation_id()
507
+ async def list_tools(self) -> list[str]:
508
+ """
509
+ List available tools from the agent.
371
510
 
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
- )
511
+ Returns:
512
+ List of tool names
513
+ """
514
+ return await self.adapter.list_tools()
381
515
 
382
- result = await self.adapter.call_tool("provide_performance_feedback", kwargs)
516
+ async def close(self) -> None:
517
+ """Close the adapter and clean up resources."""
518
+ if hasattr(self.adapter, "close"):
519
+ logger.debug(f"Closing adapter for agent {self.agent_config.id}")
520
+ await self.adapter.close()
383
521
 
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
- )
522
+ async def __aenter__(self) -> ADCPClient:
523
+ """Async context manager entry."""
524
+ return self
394
525
 
395
- return result
526
+ async def __aexit__(self, exc_type: Any, exc_val: Any, exc_tb: Any) -> None:
527
+ """Async context manager exit."""
528
+ await self.close()
529
+
530
+ def _verify_webhook_signature(self, payload: dict[str, Any], signature: str) -> bool:
531
+ """
532
+ Verify HMAC-SHA256 signature of webhook payload.
533
+
534
+ Args:
535
+ payload: Webhook payload dict
536
+ signature: Signature to verify
537
+
538
+ Returns:
539
+ True if signature is valid, False otherwise
540
+ """
541
+ if not self.webhook_secret:
542
+ return True
543
+
544
+ payload_bytes = json.dumps(payload, separators=(",", ":"), sort_keys=True).encode("utf-8")
545
+ expected_signature = hmac.new(
546
+ self.webhook_secret.encode("utf-8"), payload_bytes, hashlib.sha256
547
+ ).hexdigest()
548
+
549
+ return hmac.compare_digest(signature, expected_signature)
396
550
 
397
551
  async def handle_webhook(
398
552
  self,
@@ -405,11 +559,15 @@ class ADCPClient:
405
559
  Args:
406
560
  payload: Webhook payload
407
561
  signature: Webhook signature for verification
562
+
563
+ Raises:
564
+ ADCPWebhookSignatureError: If signature verification fails
408
565
  """
409
- # TODO: Implement signature verification
410
- if self.webhook_secret and signature:
411
- # Verify signature
412
- pass
566
+ if signature and not self._verify_webhook_signature(payload, signature):
567
+ logger.warning(
568
+ f"Webhook signature verification failed for agent {self.agent_config.id}"
569
+ )
570
+ raise ADCPWebhookSignatureError("Invalid webhook signature")
413
571
 
414
572
  operation_id = payload.get("operation_id", "unknown")
415
573
  task_type = payload.get("task_type", "unknown")
@@ -420,7 +578,7 @@ class ADCPClient:
420
578
  operation_id=operation_id,
421
579
  agent_id=self.agent_config.id,
422
580
  task_type=task_type,
423
- timestamp=datetime.utcnow().isoformat(),
581
+ timestamp=datetime.now(timezone.utc).isoformat(),
424
582
  metadata={"payload": payload},
425
583
  )
426
584
  )
@@ -469,15 +627,42 @@ class ADCPMultiAgentClient:
469
627
  """Get list of agent IDs."""
470
628
  return list(self.agents.keys())
471
629
 
472
- async def get_products(self, brief: str, **kwargs: Any) -> list[TaskResult[Any]]:
473
- """Execute get_products across all agents in parallel."""
630
+ async def close(self) -> None:
631
+ """Close all agent clients and clean up resources."""
632
+ import asyncio
633
+
634
+ logger.debug("Closing all agent clients in multi-agent client")
635
+ close_tasks = [client.close() for client in self.agents.values()]
636
+ await asyncio.gather(*close_tasks, return_exceptions=True)
637
+
638
+ async def __aenter__(self) -> ADCPMultiAgentClient:
639
+ """Async context manager entry."""
640
+ return self
641
+
642
+ async def __aexit__(self, exc_type: Any, exc_val: Any, exc_tb: Any) -> None:
643
+ """Async context manager exit."""
644
+ await self.close()
645
+
646
+ async def get_products(
647
+ self,
648
+ request: GetProductsRequest,
649
+ ) -> list[TaskResult[GetProductsResponse]]:
650
+ """
651
+ Execute get_products across all agents in parallel.
652
+
653
+ Args:
654
+ request: Request parameters
655
+
656
+ Returns:
657
+ List of TaskResults containing GetProductsResponse for each agent
658
+ """
474
659
  import asyncio
475
660
 
476
- tasks = [agent.get_products(brief, **kwargs) for agent in self.agents.values()]
661
+ tasks = [agent.get_products(request) for agent in self.agents.values()]
477
662
  return await asyncio.gather(*tasks)
478
663
 
479
664
  @classmethod
480
- def from_env(cls) -> "ADCPMultiAgentClient":
665
+ def from_env(cls) -> ADCPMultiAgentClient:
481
666
  """Create client from environment variables."""
482
667
  agents_json = os.getenv("ADCP_AGENTS")
483
668
  if not agents_json: