adcp 2.12.1__tar.gz → 2.13.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.
- {adcp-2.12.1/src/adcp.egg-info → adcp-2.13.0}/PKG-INFO +1 -1
- {adcp-2.12.1 → adcp-2.13.0}/pyproject.toml +45 -3
- {adcp-2.12.1 → adcp-2.13.0}/src/adcp/__init__.py +2 -1
- adcp-2.13.0/src/adcp/protocols/a2a.py +595 -0
- {adcp-2.12.1 → adcp-2.13.0}/src/adcp/protocols/mcp.py +95 -16
- {adcp-2.12.1 → adcp-2.13.0}/src/adcp/types/_generated.py +476 -97
- {adcp-2.12.1 → adcp-2.13.0}/src/adcp/types/aliases.py +1 -3
- adcp-2.13.0/src/adcp/types/base.py +219 -0
- {adcp-2.12.1 → adcp-2.13.0}/src/adcp/types/generated_poc/adagents.py +1 -1
- {adcp-2.12.1 → adcp-2.13.0}/src/adcp/types/generated_poc/core/activation_key.py +1 -1
- {adcp-2.12.1 → adcp-2.13.0}/src/adcp/types/generated_poc/core/assets/audio_asset.py +1 -1
- {adcp-2.12.1 → adcp-2.13.0}/src/adcp/types/generated_poc/core/assets/css_asset.py +1 -1
- {adcp-2.12.1 → adcp-2.13.0}/src/adcp/types/generated_poc/core/assets/daast_asset.py +1 -1
- {adcp-2.12.1 → adcp-2.13.0}/src/adcp/types/generated_poc/core/assets/html_asset.py +1 -1
- {adcp-2.12.1 → adcp-2.13.0}/src/adcp/types/generated_poc/core/assets/image_asset.py +1 -1
- {adcp-2.12.1 → adcp-2.13.0}/src/adcp/types/generated_poc/core/assets/javascript_asset.py +1 -1
- {adcp-2.12.1 → adcp-2.13.0}/src/adcp/types/generated_poc/core/assets/text_asset.py +1 -1
- {adcp-2.12.1 → adcp-2.13.0}/src/adcp/types/generated_poc/core/assets/url_asset.py +1 -1
- {adcp-2.12.1 → adcp-2.13.0}/src/adcp/types/generated_poc/core/assets/vast_asset.py +1 -1
- {adcp-2.12.1 → adcp-2.13.0}/src/adcp/types/generated_poc/core/assets/video_asset.py +1 -1
- {adcp-2.12.1 → adcp-2.13.0}/src/adcp/types/generated_poc/core/assets/webhook_asset.py +1 -1
- {adcp-2.12.1 → adcp-2.13.0}/src/adcp/types/generated_poc/core/brand_manifest.py +1 -1
- {adcp-2.12.1 → adcp-2.13.0}/src/adcp/types/generated_poc/core/context.py +1 -1
- {adcp-2.12.1 → adcp-2.13.0}/src/adcp/types/generated_poc/core/creative_asset.py +1 -1
- {adcp-2.12.1 → adcp-2.13.0}/src/adcp/types/generated_poc/core/creative_assignment.py +1 -1
- {adcp-2.12.1 → adcp-2.13.0}/src/adcp/types/generated_poc/core/creative_filters.py +1 -1
- {adcp-2.12.1 → adcp-2.13.0}/src/adcp/types/generated_poc/core/creative_manifest.py +1 -1
- {adcp-2.12.1 → adcp-2.13.0}/src/adcp/types/generated_poc/core/creative_policy.py +1 -1
- {adcp-2.12.1 → adcp-2.13.0}/src/adcp/types/generated_poc/core/delivery_metrics.py +1 -1
- {adcp-2.12.1 → adcp-2.13.0}/src/adcp/types/generated_poc/core/deployment.py +1 -1
- {adcp-2.12.1 → adcp-2.13.0}/src/adcp/types/generated_poc/core/destination.py +1 -1
- {adcp-2.12.1 → adcp-2.13.0}/src/adcp/types/generated_poc/core/dimensions.py +1 -1
- {adcp-2.12.1 → adcp-2.13.0}/src/adcp/types/generated_poc/core/error.py +1 -1
- {adcp-2.12.1 → adcp-2.13.0}/src/adcp/types/generated_poc/core/ext.py +1 -1
- {adcp-2.12.1 → adcp-2.13.0}/src/adcp/types/generated_poc/core/format.py +1 -1
- {adcp-2.12.1 → adcp-2.13.0}/src/adcp/types/generated_poc/core/format_id.py +1 -1
- {adcp-2.12.1 → adcp-2.13.0}/src/adcp/types/generated_poc/core/frequency_cap.py +1 -1
- {adcp-2.12.1 → adcp-2.13.0}/src/adcp/types/generated_poc/core/measurement.py +1 -1
- {adcp-2.12.1 → adcp-2.13.0}/src/adcp/types/generated_poc/core/media_buy.py +1 -1
- {adcp-2.12.1 → adcp-2.13.0}/src/adcp/types/generated_poc/core/package.py +1 -1
- {adcp-2.12.1 → adcp-2.13.0}/src/adcp/types/generated_poc/core/performance_feedback.py +1 -1
- {adcp-2.12.1 → adcp-2.13.0}/src/adcp/types/generated_poc/core/placement.py +1 -1
- {adcp-2.12.1 → adcp-2.13.0}/src/adcp/types/generated_poc/core/product.py +1 -1
- {adcp-2.12.1 → adcp-2.13.0}/src/adcp/types/generated_poc/core/product_filters.py +1 -1
- {adcp-2.12.1 → adcp-2.13.0}/src/adcp/types/generated_poc/core/promoted_offerings.py +1 -1
- {adcp-2.12.1 → adcp-2.13.0}/src/adcp/types/generated_poc/core/promoted_products.py +1 -1
- {adcp-2.12.1 → adcp-2.13.0}/src/adcp/types/generated_poc/core/property.py +1 -1
- {adcp-2.12.1 → adcp-2.13.0}/src/adcp/types/generated_poc/core/property_id.py +1 -1
- {adcp-2.12.1 → adcp-2.13.0}/src/adcp/types/generated_poc/core/property_tag.py +1 -1
- {adcp-2.12.1 → adcp-2.13.0}/src/adcp/types/generated_poc/core/protocol_envelope.py +1 -1
- {adcp-2.12.1 → adcp-2.13.0}/src/adcp/types/generated_poc/core/publisher_property_selector.py +1 -1
- {adcp-2.12.1 → adcp-2.13.0}/src/adcp/types/generated_poc/core/push_notification_config.py +1 -1
- {adcp-2.12.1 → adcp-2.13.0}/src/adcp/types/generated_poc/core/reporting_capabilities.py +1 -1
- {adcp-2.12.1 → adcp-2.13.0}/src/adcp/types/generated_poc/core/response.py +1 -1
- {adcp-2.12.1 → adcp-2.13.0}/src/adcp/types/generated_poc/core/signal_filters.py +1 -1
- {adcp-2.12.1 → adcp-2.13.0}/src/adcp/types/generated_poc/core/sub_asset.py +1 -1
- {adcp-2.12.1 → adcp-2.13.0}/src/adcp/types/generated_poc/core/targeting.py +1 -1
- {adcp-2.12.1 → adcp-2.13.0}/src/adcp/types/generated_poc/core/webhook_payload.py +1 -1
- {adcp-2.12.1 → adcp-2.13.0}/src/adcp/types/generated_poc/creative/list_creative_formats_request.py +1 -1
- {adcp-2.12.1 → adcp-2.13.0}/src/adcp/types/generated_poc/creative/list_creative_formats_response.py +1 -1
- {adcp-2.12.1 → adcp-2.13.0}/src/adcp/types/generated_poc/creative/preview_creative_request.py +1 -1
- {adcp-2.12.1 → adcp-2.13.0}/src/adcp/types/generated_poc/creative/preview_creative_response.py +1 -1
- {adcp-2.12.1 → adcp-2.13.0}/src/adcp/types/generated_poc/creative/preview_render.py +1 -1
- {adcp-2.12.1 → adcp-2.13.0}/src/adcp/types/generated_poc/enums/adcp_domain.py +1 -1
- {adcp-2.12.1 → adcp-2.13.0}/src/adcp/types/generated_poc/enums/asset_content_type.py +1 -1
- {adcp-2.12.1 → adcp-2.13.0}/src/adcp/types/generated_poc/enums/auth_scheme.py +1 -1
- {adcp-2.12.1 → adcp-2.13.0}/src/adcp/types/generated_poc/enums/available_metric.py +1 -1
- {adcp-2.12.1 → adcp-2.13.0}/src/adcp/types/generated_poc/enums/channels.py +1 -1
- {adcp-2.12.1 → adcp-2.13.0}/src/adcp/types/generated_poc/enums/co_branding_requirement.py +1 -1
- {adcp-2.12.1 → adcp-2.13.0}/src/adcp/types/generated_poc/enums/creative_action.py +1 -1
- {adcp-2.12.1 → adcp-2.13.0}/src/adcp/types/generated_poc/enums/creative_agent_capability.py +1 -1
- {adcp-2.12.1 → adcp-2.13.0}/src/adcp/types/generated_poc/enums/creative_sort_field.py +1 -1
- {adcp-2.12.1 → adcp-2.13.0}/src/adcp/types/generated_poc/enums/creative_status.py +1 -1
- {adcp-2.12.1 → adcp-2.13.0}/src/adcp/types/generated_poc/enums/daast_tracking_event.py +1 -1
- {adcp-2.12.1 → adcp-2.13.0}/src/adcp/types/generated_poc/enums/daast_version.py +1 -1
- {adcp-2.12.1 → adcp-2.13.0}/src/adcp/types/generated_poc/enums/delivery_type.py +1 -1
- {adcp-2.12.1 → adcp-2.13.0}/src/adcp/types/generated_poc/enums/dimension_unit.py +1 -1
- {adcp-2.12.1 → adcp-2.13.0}/src/adcp/types/generated_poc/enums/feed_format.py +1 -1
- {adcp-2.12.1 → adcp-2.13.0}/src/adcp/types/generated_poc/enums/feedback_source.py +1 -1
- {adcp-2.12.1 → adcp-2.13.0}/src/adcp/types/generated_poc/enums/format_category.py +1 -1
- {adcp-2.12.1 → adcp-2.13.0}/src/adcp/types/generated_poc/enums/format_id_parameter.py +1 -1
- {adcp-2.12.1 → adcp-2.13.0}/src/adcp/types/generated_poc/enums/frequency_cap_scope.py +1 -1
- {adcp-2.12.1 → adcp-2.13.0}/src/adcp/types/generated_poc/enums/history_entry_type.py +1 -1
- {adcp-2.12.1 → adcp-2.13.0}/src/adcp/types/generated_poc/enums/http_method.py +1 -1
- {adcp-2.12.1 → adcp-2.13.0}/src/adcp/types/generated_poc/enums/identifier_types.py +1 -1
- {adcp-2.12.1 → adcp-2.13.0}/src/adcp/types/generated_poc/enums/javascript_module_type.py +1 -1
- {adcp-2.12.1 → adcp-2.13.0}/src/adcp/types/generated_poc/enums/landing_page_requirement.py +1 -1
- {adcp-2.12.1 → adcp-2.13.0}/src/adcp/types/generated_poc/enums/markdown_flavor.py +1 -1
- {adcp-2.12.1 → adcp-2.13.0}/src/adcp/types/generated_poc/enums/media_buy_status.py +1 -1
- {adcp-2.12.1 → adcp-2.13.0}/src/adcp/types/generated_poc/enums/metric_type.py +1 -1
- {adcp-2.12.1 → adcp-2.13.0}/src/adcp/types/generated_poc/enums/notification_type.py +1 -1
- {adcp-2.12.1 → adcp-2.13.0}/src/adcp/types/generated_poc/enums/pacing.py +1 -1
- {adcp-2.12.1 → adcp-2.13.0}/src/adcp/types/generated_poc/enums/preview_output_format.py +1 -1
- {adcp-2.12.1 → adcp-2.13.0}/src/adcp/types/generated_poc/enums/pricing_model.py +1 -1
- {adcp-2.12.1 → adcp-2.13.0}/src/adcp/types/generated_poc/enums/property_type.py +1 -1
- {adcp-2.12.1 → adcp-2.13.0}/src/adcp/types/generated_poc/enums/publisher_identifier_types.py +1 -1
- {adcp-2.12.1 → adcp-2.13.0}/src/adcp/types/generated_poc/enums/reporting_frequency.py +1 -1
- {adcp-2.12.1 → adcp-2.13.0}/src/adcp/types/generated_poc/enums/signal_catalog_type.py +1 -1
- {adcp-2.12.1 → adcp-2.13.0}/src/adcp/types/generated_poc/enums/sort_direction.py +1 -1
- {adcp-2.12.1 → adcp-2.13.0}/src/adcp/types/generated_poc/enums/standard_format_ids.py +1 -1
- {adcp-2.12.1 → adcp-2.13.0}/src/adcp/types/generated_poc/enums/task_status.py +1 -1
- {adcp-2.12.1 → adcp-2.13.0}/src/adcp/types/generated_poc/enums/task_type.py +1 -1
- {adcp-2.12.1 → adcp-2.13.0}/src/adcp/types/generated_poc/enums/update_frequency.py +1 -1
- {adcp-2.12.1 → adcp-2.13.0}/src/adcp/types/generated_poc/enums/url_asset_type.py +1 -1
- {adcp-2.12.1 → adcp-2.13.0}/src/adcp/types/generated_poc/enums/validation_mode.py +1 -1
- {adcp-2.12.1 → adcp-2.13.0}/src/adcp/types/generated_poc/enums/vast_tracking_event.py +1 -1
- {adcp-2.12.1 → adcp-2.13.0}/src/adcp/types/generated_poc/enums/vast_version.py +1 -1
- {adcp-2.12.1 → adcp-2.13.0}/src/adcp/types/generated_poc/enums/webhook_response_type.py +1 -1
- {adcp-2.12.1 → adcp-2.13.0}/src/adcp/types/generated_poc/enums/webhook_security_method.py +1 -1
- {adcp-2.12.1 → adcp-2.13.0}/src/adcp/types/generated_poc/media_buy/build_creative_request.py +1 -1
- {adcp-2.12.1 → adcp-2.13.0}/src/adcp/types/generated_poc/media_buy/build_creative_response.py +1 -1
- {adcp-2.12.1 → adcp-2.13.0}/src/adcp/types/generated_poc/media_buy/create_media_buy_request.py +1 -1
- {adcp-2.12.1 → adcp-2.13.0}/src/adcp/types/generated_poc/media_buy/create_media_buy_response.py +1 -1
- {adcp-2.12.1 → adcp-2.13.0}/src/adcp/types/generated_poc/media_buy/get_media_buy_delivery_request.py +1 -1
- {adcp-2.12.1 → adcp-2.13.0}/src/adcp/types/generated_poc/media_buy/get_media_buy_delivery_response.py +1 -1
- {adcp-2.12.1 → adcp-2.13.0}/src/adcp/types/generated_poc/media_buy/get_products_request.py +1 -1
- {adcp-2.12.1 → adcp-2.13.0}/src/adcp/types/generated_poc/media_buy/get_products_response.py +1 -1
- {adcp-2.12.1 → adcp-2.13.0}/src/adcp/types/generated_poc/media_buy/list_authorized_properties_request.py +1 -1
- {adcp-2.12.1 → adcp-2.13.0}/src/adcp/types/generated_poc/media_buy/list_authorized_properties_response.py +1 -1
- {adcp-2.12.1 → adcp-2.13.0}/src/adcp/types/generated_poc/media_buy/list_creative_formats_request.py +1 -1
- {adcp-2.12.1 → adcp-2.13.0}/src/adcp/types/generated_poc/media_buy/list_creative_formats_response.py +1 -1
- {adcp-2.12.1 → adcp-2.13.0}/src/adcp/types/generated_poc/media_buy/list_creatives_request.py +1 -1
- {adcp-2.12.1 → adcp-2.13.0}/src/adcp/types/generated_poc/media_buy/list_creatives_response.py +1 -1
- {adcp-2.12.1 → adcp-2.13.0}/src/adcp/types/generated_poc/media_buy/package_request.py +1 -1
- {adcp-2.12.1 → adcp-2.13.0}/src/adcp/types/generated_poc/media_buy/provide_performance_feedback_request.py +1 -1
- {adcp-2.12.1 → adcp-2.13.0}/src/adcp/types/generated_poc/media_buy/provide_performance_feedback_response.py +1 -1
- {adcp-2.12.1 → adcp-2.13.0}/src/adcp/types/generated_poc/media_buy/sync_creatives_request.py +1 -1
- {adcp-2.12.1 → adcp-2.13.0}/src/adcp/types/generated_poc/media_buy/sync_creatives_response.py +1 -1
- {adcp-2.12.1 → adcp-2.13.0}/src/adcp/types/generated_poc/media_buy/update_media_buy_request.py +1 -1
- {adcp-2.12.1 → adcp-2.13.0}/src/adcp/types/generated_poc/media_buy/update_media_buy_response.py +1 -1
- {adcp-2.12.1 → adcp-2.13.0}/src/adcp/types/generated_poc/pricing_options/cpc_option.py +1 -1
- {adcp-2.12.1 → adcp-2.13.0}/src/adcp/types/generated_poc/pricing_options/cpcv_option.py +1 -1
- {adcp-2.12.1 → adcp-2.13.0}/src/adcp/types/generated_poc/pricing_options/cpm_auction_option.py +1 -1
- {adcp-2.12.1 → adcp-2.13.0}/src/adcp/types/generated_poc/pricing_options/cpm_fixed_option.py +1 -1
- {adcp-2.12.1 → adcp-2.13.0}/src/adcp/types/generated_poc/pricing_options/cpp_option.py +1 -1
- {adcp-2.12.1 → adcp-2.13.0}/src/adcp/types/generated_poc/pricing_options/cpv_option.py +1 -1
- {adcp-2.12.1 → adcp-2.13.0}/src/adcp/types/generated_poc/pricing_options/flat_rate_option.py +1 -1
- {adcp-2.12.1 → adcp-2.13.0}/src/adcp/types/generated_poc/pricing_options/vcpm_auction_option.py +1 -1
- {adcp-2.12.1 → adcp-2.13.0}/src/adcp/types/generated_poc/pricing_options/vcpm_fixed_option.py +1 -1
- {adcp-2.12.1 → adcp-2.13.0}/src/adcp/types/generated_poc/protocols/adcp_extension.py +1 -1
- {adcp-2.12.1 → adcp-2.13.0}/src/adcp/types/generated_poc/signals/activate_signal_request.py +1 -1
- {adcp-2.12.1 → adcp-2.13.0}/src/adcp/types/generated_poc/signals/activate_signal_response.py +1 -1
- {adcp-2.12.1 → adcp-2.13.0}/src/adcp/types/generated_poc/signals/get_signals_request.py +1 -1
- {adcp-2.12.1 → adcp-2.13.0}/src/adcp/types/generated_poc/signals/get_signals_response.py +1 -1
- {adcp-2.12.1 → adcp-2.13.0/src/adcp.egg-info}/PKG-INFO +1 -1
- {adcp-2.12.1 → adcp-2.13.0}/src/adcp.egg-info/SOURCES.txt +1 -0
- {adcp-2.12.1 → adcp-2.13.0}/tests/test_discriminated_unions.py +4 -2
- {adcp-2.12.1 → adcp-2.13.0}/tests/test_protocols.py +281 -281
- {adcp-2.12.1 → adcp-2.13.0}/tests/test_public_api.py +53 -9
- adcp-2.13.0/tests/test_response_str.py +350 -0
- {adcp-2.12.1 → adcp-2.13.0}/tests/test_simple_api.py +38 -0
- {adcp-2.12.1 → adcp-2.13.0}/tests/test_type_aliases.py +1 -3
- adcp-2.12.1/src/adcp/protocols/a2a.py +0 -484
- adcp-2.12.1/src/adcp/types/base.py +0 -26
- {adcp-2.12.1 → adcp-2.13.0}/LICENSE +0 -0
- {adcp-2.12.1 → adcp-2.13.0}/MANIFEST.in +0 -0
- {adcp-2.12.1 → adcp-2.13.0}/README.md +0 -0
- {adcp-2.12.1 → adcp-2.13.0}/setup.cfg +0 -0
- {adcp-2.12.1 → adcp-2.13.0}/src/adcp/ADCP_VERSION +0 -0
- {adcp-2.12.1 → adcp-2.13.0}/src/adcp/__main__.py +0 -0
- {adcp-2.12.1 → adcp-2.13.0}/src/adcp/adagents.py +0 -0
- {adcp-2.12.1 → adcp-2.13.0}/src/adcp/client.py +0 -0
- {adcp-2.12.1 → adcp-2.13.0}/src/adcp/config.py +0 -0
- {adcp-2.12.1 → adcp-2.13.0}/src/adcp/exceptions.py +0 -0
- {adcp-2.12.1 → adcp-2.13.0}/src/adcp/protocols/__init__.py +0 -0
- {adcp-2.12.1 → adcp-2.13.0}/src/adcp/protocols/base.py +0 -0
- {adcp-2.12.1 → adcp-2.13.0}/src/adcp/py.typed +0 -0
- {adcp-2.12.1 → adcp-2.13.0}/src/adcp/simple.py +0 -0
- {adcp-2.12.1 → adcp-2.13.0}/src/adcp/testing/__init__.py +0 -0
- {adcp-2.12.1 → adcp-2.13.0}/src/adcp/testing/test_helpers.py +0 -0
- {adcp-2.12.1 → adcp-2.13.0}/src/adcp/types/__init__.py +0 -0
- {adcp-2.12.1 → adcp-2.13.0}/src/adcp/types/core.py +0 -0
- {adcp-2.12.1 → adcp-2.13.0}/src/adcp/types/generated_poc/__init__.py +0 -0
- {adcp-2.12.1 → adcp-2.13.0}/src/adcp/types/generated_poc/core/__init__.py +0 -0
- {adcp-2.12.1 → adcp-2.13.0}/src/adcp/types/generated_poc/core/assets/__init__.py +0 -0
- {adcp-2.12.1 → adcp-2.13.0}/src/adcp/types/generated_poc/creative/__init__.py +0 -0
- {adcp-2.12.1 → adcp-2.13.0}/src/adcp/types/generated_poc/enums/__init__.py +0 -0
- {adcp-2.12.1 → adcp-2.13.0}/src/adcp/types/generated_poc/media_buy/__init__.py +0 -0
- {adcp-2.12.1 → adcp-2.13.0}/src/adcp/types/generated_poc/pricing_options/__init__.py +0 -0
- {adcp-2.12.1 → adcp-2.13.0}/src/adcp/types/generated_poc/protocols/__init__.py +0 -0
- {adcp-2.12.1 → adcp-2.13.0}/src/adcp/types/generated_poc/signals/__init__.py +0 -0
- {adcp-2.12.1 → adcp-2.13.0}/src/adcp/utils/__init__.py +0 -0
- {adcp-2.12.1 → adcp-2.13.0}/src/adcp/utils/operation_id.py +0 -0
- {adcp-2.12.1 → adcp-2.13.0}/src/adcp/utils/preview_cache.py +0 -0
- {adcp-2.12.1 → adcp-2.13.0}/src/adcp/utils/response_parser.py +0 -0
- {adcp-2.12.1 → adcp-2.13.0}/src/adcp/validation.py +0 -0
- {adcp-2.12.1 → adcp-2.13.0}/src/adcp.egg-info/dependency_links.txt +0 -0
- {adcp-2.12.1 → adcp-2.13.0}/src/adcp.egg-info/entry_points.txt +0 -0
- {adcp-2.12.1 → adcp-2.13.0}/src/adcp.egg-info/requires.txt +0 -0
- {adcp-2.12.1 → adcp-2.13.0}/src/adcp.egg-info/top_level.txt +0 -0
- {adcp-2.12.1 → adcp-2.13.0}/tests/test_adagents.py +0 -0
- {adcp-2.12.1 → adcp-2.13.0}/tests/test_cli.py +0 -0
- {adcp-2.12.1 → adcp-2.13.0}/tests/test_client.py +0 -0
- {adcp-2.12.1 → adcp-2.13.0}/tests/test_code_generation.py +0 -0
- {adcp-2.12.1 → adcp-2.13.0}/tests/test_format_id_validation.py +0 -0
- {adcp-2.12.1 → adcp-2.13.0}/tests/test_helpers.py +0 -0
- {adcp-2.12.1 → adcp-2.13.0}/tests/test_preview_html.py +0 -0
- {adcp-2.12.1 → adcp-2.13.0}/tests/test_response_parser.py +0 -0
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "adcp"
|
|
7
|
-
version = "2.
|
|
7
|
+
version = "2.13.0"
|
|
8
8
|
description = "Official Python client for the Ad Context Protocol (AdCP)"
|
|
9
9
|
authors = [
|
|
10
10
|
{name = "AdCP Community", email = "maintainers@adcontextprotocol.org"}
|
|
@@ -66,7 +66,7 @@ adcp = ["py.typed", "ADCP_VERSION"]
|
|
|
66
66
|
[tool.black]
|
|
67
67
|
line-length = 100
|
|
68
68
|
target-version = ["py310", "py311", "py312"]
|
|
69
|
-
extend-exclude = "/(
|
|
69
|
+
extend-exclude = "/(tasks)\\.py$"
|
|
70
70
|
|
|
71
71
|
[tool.ruff]
|
|
72
72
|
line-length = 100
|
|
@@ -79,7 +79,10 @@ extend-exclude = [
|
|
|
79
79
|
|
|
80
80
|
[tool.ruff.lint]
|
|
81
81
|
select = ["E", "F", "I", "N", "W", "UP"]
|
|
82
|
-
ignore = [
|
|
82
|
+
ignore = [
|
|
83
|
+
"E402", # Allow imports after module docstrings
|
|
84
|
+
"UP038", # isinstance() doesn't support X | Y syntax, only type hints do
|
|
85
|
+
]
|
|
83
86
|
|
|
84
87
|
[tool.mypy]
|
|
85
88
|
python_version = "3.10"
|
|
@@ -98,6 +101,45 @@ ignore_errors = true
|
|
|
98
101
|
[tool.pytest.ini_options]
|
|
99
102
|
testpaths = ["tests"]
|
|
100
103
|
asyncio_mode = "auto"
|
|
104
|
+
markers = [
|
|
105
|
+
"unit: Unit tests that don't require external services",
|
|
106
|
+
"integration: Integration tests that hit real endpoints (may be slow/flaky)",
|
|
107
|
+
"slow: Tests that take significant time to run",
|
|
108
|
+
]
|
|
109
|
+
# By default, skip integration tests for fast local development
|
|
110
|
+
addopts = "-m 'not integration'"
|
|
111
|
+
|
|
112
|
+
[tool.coverage.run]
|
|
113
|
+
source = ["src/adcp"]
|
|
114
|
+
omit = [
|
|
115
|
+
"*/tests/*",
|
|
116
|
+
"*/test_*.py",
|
|
117
|
+
]
|
|
118
|
+
branch = true
|
|
119
|
+
|
|
120
|
+
[tool.coverage.report]
|
|
121
|
+
precision = 2
|
|
122
|
+
show_missing = true
|
|
123
|
+
skip_covered = false
|
|
124
|
+
exclude_lines = [
|
|
125
|
+
"pragma: no cover",
|
|
126
|
+
"def __repr__",
|
|
127
|
+
"raise AssertionError",
|
|
128
|
+
"raise NotImplementedError",
|
|
129
|
+
"if __name__ == .__main__.:",
|
|
130
|
+
"if TYPE_CHECKING:",
|
|
131
|
+
"class .*\\bProtocol\\):",
|
|
132
|
+
"@(abc\\.)?abstractmethod",
|
|
133
|
+
]
|
|
134
|
+
# Maintain current 86% coverage, fail if it drops below 80%
|
|
135
|
+
fail_under = 80
|
|
136
|
+
|
|
137
|
+
[tool.coverage.html]
|
|
138
|
+
directory = "htmlcov"
|
|
139
|
+
|
|
140
|
+
[tool.bandit]
|
|
141
|
+
exclude_dirs = ["tests", "scripts"]
|
|
142
|
+
skips = ["B101"] # Allow assert in code (we're not using -O optimization)
|
|
101
143
|
|
|
102
144
|
[dependency-groups]
|
|
103
145
|
dev = [
|
|
@@ -179,7 +179,7 @@ from adcp.validation import (
|
|
|
179
179
|
validate_publisher_properties_item,
|
|
180
180
|
)
|
|
181
181
|
|
|
182
|
-
__version__ = "2.
|
|
182
|
+
__version__ = "2.13.0"
|
|
183
183
|
|
|
184
184
|
|
|
185
185
|
def get_adcp_version() -> str:
|
|
@@ -202,6 +202,7 @@ def get_adcp_version() -> str:
|
|
|
202
202
|
version_file = files("adcp") / "ADCP_VERSION"
|
|
203
203
|
return version_file.read_text().strip()
|
|
204
204
|
|
|
205
|
+
|
|
205
206
|
__all__ = [
|
|
206
207
|
# Version functions
|
|
207
208
|
"get_adcp_version",
|
|
@@ -0,0 +1,595 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
"""A2A protocol adapter using the official a2a-sdk client."""
|
|
4
|
+
|
|
5
|
+
import logging
|
|
6
|
+
import time
|
|
7
|
+
from typing import Any
|
|
8
|
+
from uuid import uuid4
|
|
9
|
+
|
|
10
|
+
import httpx
|
|
11
|
+
from a2a.client import A2ACardResolver, A2AClient
|
|
12
|
+
from a2a.types import (
|
|
13
|
+
DataPart,
|
|
14
|
+
Message,
|
|
15
|
+
MessageSendParams,
|
|
16
|
+
Part,
|
|
17
|
+
Role,
|
|
18
|
+
SendMessageRequest,
|
|
19
|
+
Task,
|
|
20
|
+
TextPart,
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
from adcp.exceptions import (
|
|
24
|
+
ADCPAuthenticationError,
|
|
25
|
+
ADCPConnectionError,
|
|
26
|
+
ADCPTimeoutError,
|
|
27
|
+
)
|
|
28
|
+
from adcp.protocols.base import ProtocolAdapter
|
|
29
|
+
from adcp.types.core import AgentConfig, DebugInfo, TaskResult, TaskStatus
|
|
30
|
+
|
|
31
|
+
logger = logging.getLogger(__name__)
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class A2AAdapter(ProtocolAdapter):
|
|
35
|
+
"""Adapter for A2A protocol using official a2a-sdk client."""
|
|
36
|
+
|
|
37
|
+
def __init__(self, agent_config: AgentConfig):
|
|
38
|
+
"""Initialize A2A adapter with official A2A client."""
|
|
39
|
+
super().__init__(agent_config)
|
|
40
|
+
self._httpx_client: httpx.AsyncClient | None = None
|
|
41
|
+
self._a2a_client: A2AClient | None = None
|
|
42
|
+
|
|
43
|
+
async def _get_httpx_client(self) -> httpx.AsyncClient:
|
|
44
|
+
"""Get or create the HTTP client with connection pooling."""
|
|
45
|
+
if self._httpx_client is None:
|
|
46
|
+
limits = httpx.Limits(
|
|
47
|
+
max_keepalive_connections=10,
|
|
48
|
+
max_connections=20,
|
|
49
|
+
keepalive_expiry=30.0,
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
headers = {}
|
|
53
|
+
if self.agent_config.auth_token:
|
|
54
|
+
if self.agent_config.auth_type == "bearer":
|
|
55
|
+
headers["Authorization"] = f"Bearer {self.agent_config.auth_token}"
|
|
56
|
+
else:
|
|
57
|
+
headers[self.agent_config.auth_header] = self.agent_config.auth_token
|
|
58
|
+
|
|
59
|
+
self._httpx_client = httpx.AsyncClient(
|
|
60
|
+
limits=limits,
|
|
61
|
+
headers=headers,
|
|
62
|
+
timeout=self.agent_config.timeout,
|
|
63
|
+
)
|
|
64
|
+
logger.debug(
|
|
65
|
+
f"Created HTTP client with connection pooling for agent {self.agent_config.id}"
|
|
66
|
+
)
|
|
67
|
+
return self._httpx_client
|
|
68
|
+
|
|
69
|
+
async def _get_a2a_client(self) -> A2AClient:
|
|
70
|
+
"""Get or create the A2A client."""
|
|
71
|
+
if self._a2a_client is None:
|
|
72
|
+
httpx_client = await self._get_httpx_client()
|
|
73
|
+
|
|
74
|
+
# Use A2ACardResolver to fetch the agent card
|
|
75
|
+
card_resolver = A2ACardResolver(
|
|
76
|
+
httpx_client=httpx_client,
|
|
77
|
+
base_url=self.agent_config.agent_uri,
|
|
78
|
+
)
|
|
79
|
+
|
|
80
|
+
try:
|
|
81
|
+
agent_card = await card_resolver.get_agent_card()
|
|
82
|
+
logger.debug(f"Fetched agent card for {self.agent_config.id}")
|
|
83
|
+
except httpx.HTTPStatusError as e:
|
|
84
|
+
status_code = e.response.status_code
|
|
85
|
+
if status_code in (401, 403):
|
|
86
|
+
raise ADCPAuthenticationError(
|
|
87
|
+
f"Authentication failed: HTTP {status_code}",
|
|
88
|
+
agent_id=self.agent_config.id,
|
|
89
|
+
agent_uri=self.agent_config.agent_uri,
|
|
90
|
+
) from e
|
|
91
|
+
else:
|
|
92
|
+
raise ADCPConnectionError(
|
|
93
|
+
f"Failed to fetch agent card: HTTP {status_code}",
|
|
94
|
+
agent_id=self.agent_config.id,
|
|
95
|
+
agent_uri=self.agent_config.agent_uri,
|
|
96
|
+
) from e
|
|
97
|
+
except httpx.TimeoutException as e:
|
|
98
|
+
raise ADCPTimeoutError(
|
|
99
|
+
f"Timeout fetching agent card: {e}",
|
|
100
|
+
agent_id=self.agent_config.id,
|
|
101
|
+
agent_uri=self.agent_config.agent_uri,
|
|
102
|
+
timeout=self.agent_config.timeout,
|
|
103
|
+
) from e
|
|
104
|
+
except httpx.HTTPError as e:
|
|
105
|
+
raise ADCPConnectionError(
|
|
106
|
+
f"Failed to fetch agent card: {e}",
|
|
107
|
+
agent_id=self.agent_config.id,
|
|
108
|
+
agent_uri=self.agent_config.agent_uri,
|
|
109
|
+
) from e
|
|
110
|
+
|
|
111
|
+
self._a2a_client = A2AClient(
|
|
112
|
+
httpx_client=httpx_client,
|
|
113
|
+
agent_card=agent_card,
|
|
114
|
+
)
|
|
115
|
+
logger.debug(f"Created A2A client for agent {self.agent_config.id}")
|
|
116
|
+
|
|
117
|
+
return self._a2a_client
|
|
118
|
+
|
|
119
|
+
async def close(self) -> None:
|
|
120
|
+
"""Close the HTTP client and clean up resources."""
|
|
121
|
+
if self._httpx_client is not None:
|
|
122
|
+
logger.debug(f"Closing A2A adapter client for agent {self.agent_config.id}")
|
|
123
|
+
await self._httpx_client.aclose()
|
|
124
|
+
self._httpx_client = None
|
|
125
|
+
self._a2a_client = None
|
|
126
|
+
|
|
127
|
+
async def _call_a2a_tool(
|
|
128
|
+
self, tool_name: str, params: dict[str, Any], use_explicit_skill: bool = True
|
|
129
|
+
) -> TaskResult[Any]:
|
|
130
|
+
"""
|
|
131
|
+
Call a tool using A2A protocol via official a2a-sdk client.
|
|
132
|
+
|
|
133
|
+
Args:
|
|
134
|
+
tool_name: Name of the skill/tool to invoke
|
|
135
|
+
params: Parameters to pass to the skill
|
|
136
|
+
use_explicit_skill: If True, use explicit skill invocation (deterministic).
|
|
137
|
+
If False, use natural language (flexible).
|
|
138
|
+
|
|
139
|
+
The default is explicit skill invocation for predictable, repeatable behavior.
|
|
140
|
+
See: https://docs.adcontextprotocol.org/docs/protocols/a2a-guide
|
|
141
|
+
"""
|
|
142
|
+
start_time = time.time() if self.agent_config.debug else None
|
|
143
|
+
a2a_client = await self._get_a2a_client()
|
|
144
|
+
|
|
145
|
+
# Build A2A message
|
|
146
|
+
message_id = str(uuid4())
|
|
147
|
+
|
|
148
|
+
if use_explicit_skill:
|
|
149
|
+
# Explicit skill invocation (deterministic)
|
|
150
|
+
# Use DataPart with skill name and parameters
|
|
151
|
+
data_part = DataPart(
|
|
152
|
+
data={
|
|
153
|
+
"skill": tool_name,
|
|
154
|
+
"parameters": params,
|
|
155
|
+
}
|
|
156
|
+
)
|
|
157
|
+
message = Message(
|
|
158
|
+
message_id=message_id,
|
|
159
|
+
role=Role.user,
|
|
160
|
+
parts=[Part(root=data_part)],
|
|
161
|
+
)
|
|
162
|
+
else:
|
|
163
|
+
# Natural language invocation (flexible)
|
|
164
|
+
# Agent interprets intent from text
|
|
165
|
+
text_part = TextPart(text=self._format_tool_request(tool_name, params))
|
|
166
|
+
message = Message(
|
|
167
|
+
message_id=message_id,
|
|
168
|
+
role=Role.user,
|
|
169
|
+
parts=[Part(root=text_part)],
|
|
170
|
+
)
|
|
171
|
+
|
|
172
|
+
# Build request params
|
|
173
|
+
params_obj = MessageSendParams(message=message)
|
|
174
|
+
|
|
175
|
+
# Build request
|
|
176
|
+
request = SendMessageRequest(
|
|
177
|
+
id=str(uuid4()),
|
|
178
|
+
params=params_obj,
|
|
179
|
+
)
|
|
180
|
+
|
|
181
|
+
debug_info = None
|
|
182
|
+
debug_request: dict[str, Any] = {}
|
|
183
|
+
if self.agent_config.debug:
|
|
184
|
+
debug_request = {
|
|
185
|
+
"method": "send_message",
|
|
186
|
+
"message_id": message_id,
|
|
187
|
+
"tool": tool_name,
|
|
188
|
+
"params": params,
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
try:
|
|
192
|
+
# Use official A2A client
|
|
193
|
+
sdk_response = await a2a_client.send_message(request)
|
|
194
|
+
|
|
195
|
+
# SendMessageResponse is a RootModel union - unwrap it to get the actual response
|
|
196
|
+
# (either JSONRPCSuccessResponse or JSONRPCErrorResponse)
|
|
197
|
+
response = sdk_response.root if hasattr(sdk_response, "root") else sdk_response
|
|
198
|
+
|
|
199
|
+
# Handle JSON-RPC error response
|
|
200
|
+
if hasattr(response, "error"):
|
|
201
|
+
error_msg = response.error.message if response.error.message else "Unknown error"
|
|
202
|
+
if self.agent_config.debug and start_time:
|
|
203
|
+
duration_ms = (time.time() - start_time) * 1000
|
|
204
|
+
debug_info = DebugInfo(
|
|
205
|
+
request=debug_request,
|
|
206
|
+
response={"error": response.error.model_dump()},
|
|
207
|
+
duration_ms=duration_ms,
|
|
208
|
+
)
|
|
209
|
+
return TaskResult[Any](
|
|
210
|
+
status=TaskStatus.FAILED,
|
|
211
|
+
error=error_msg,
|
|
212
|
+
success=False,
|
|
213
|
+
debug_info=debug_info,
|
|
214
|
+
)
|
|
215
|
+
|
|
216
|
+
# Handle success response
|
|
217
|
+
if hasattr(response, "result"):
|
|
218
|
+
result = response.result
|
|
219
|
+
|
|
220
|
+
if self.agent_config.debug and start_time:
|
|
221
|
+
duration_ms = (time.time() - start_time) * 1000
|
|
222
|
+
debug_info = DebugInfo(
|
|
223
|
+
request=debug_request,
|
|
224
|
+
response={"result": result.model_dump()},
|
|
225
|
+
duration_ms=duration_ms,
|
|
226
|
+
)
|
|
227
|
+
|
|
228
|
+
# Result can be either Task or Message
|
|
229
|
+
if isinstance(result, Task):
|
|
230
|
+
return self._process_task_response(result, debug_info)
|
|
231
|
+
else:
|
|
232
|
+
# Message response (shouldn't happen for send_message, but handle it)
|
|
233
|
+
agent_id = self.agent_config.id
|
|
234
|
+
logger.warning(f"Received Message instead of Task from A2A agent {agent_id}")
|
|
235
|
+
return TaskResult[Any](
|
|
236
|
+
status=TaskStatus.COMPLETED,
|
|
237
|
+
data=None,
|
|
238
|
+
message="Received message response",
|
|
239
|
+
success=True,
|
|
240
|
+
debug_info=debug_info,
|
|
241
|
+
)
|
|
242
|
+
|
|
243
|
+
# Shouldn't reach here
|
|
244
|
+
return TaskResult[Any](
|
|
245
|
+
status=TaskStatus.FAILED,
|
|
246
|
+
error="Invalid response from A2A client",
|
|
247
|
+
success=False,
|
|
248
|
+
debug_info=debug_info,
|
|
249
|
+
)
|
|
250
|
+
|
|
251
|
+
except httpx.HTTPStatusError as e:
|
|
252
|
+
status_code = e.response.status_code
|
|
253
|
+
if self.agent_config.debug and start_time:
|
|
254
|
+
duration_ms = (time.time() - start_time) * 1000
|
|
255
|
+
debug_info = DebugInfo(
|
|
256
|
+
request=debug_request,
|
|
257
|
+
response={"error": str(e), "status_code": status_code},
|
|
258
|
+
duration_ms=duration_ms,
|
|
259
|
+
)
|
|
260
|
+
|
|
261
|
+
if status_code in (401, 403):
|
|
262
|
+
error_msg = f"Authentication failed: HTTP {status_code}"
|
|
263
|
+
else:
|
|
264
|
+
error_msg = f"HTTP {status_code} error: {e}"
|
|
265
|
+
|
|
266
|
+
return TaskResult[Any](
|
|
267
|
+
status=TaskStatus.FAILED,
|
|
268
|
+
error=error_msg,
|
|
269
|
+
success=False,
|
|
270
|
+
debug_info=debug_info,
|
|
271
|
+
)
|
|
272
|
+
except httpx.TimeoutException as e:
|
|
273
|
+
if self.agent_config.debug and start_time:
|
|
274
|
+
duration_ms = (time.time() - start_time) * 1000
|
|
275
|
+
debug_info = DebugInfo(
|
|
276
|
+
request=debug_request,
|
|
277
|
+
response={"error": str(e)},
|
|
278
|
+
duration_ms=duration_ms,
|
|
279
|
+
)
|
|
280
|
+
return TaskResult[Any](
|
|
281
|
+
status=TaskStatus.FAILED,
|
|
282
|
+
error=f"Timeout: {e}",
|
|
283
|
+
success=False,
|
|
284
|
+
debug_info=debug_info,
|
|
285
|
+
)
|
|
286
|
+
except Exception as e:
|
|
287
|
+
if self.agent_config.debug and start_time:
|
|
288
|
+
duration_ms = (time.time() - start_time) * 1000
|
|
289
|
+
debug_info = DebugInfo(
|
|
290
|
+
request=debug_request,
|
|
291
|
+
response={"error": str(e)},
|
|
292
|
+
duration_ms=duration_ms,
|
|
293
|
+
)
|
|
294
|
+
return TaskResult[Any](
|
|
295
|
+
status=TaskStatus.FAILED,
|
|
296
|
+
error=str(e),
|
|
297
|
+
success=False,
|
|
298
|
+
debug_info=debug_info,
|
|
299
|
+
)
|
|
300
|
+
|
|
301
|
+
def _process_task_response(self, task: Task, debug_info: DebugInfo | None) -> TaskResult[Any]:
|
|
302
|
+
"""Process a Task response from A2A into our TaskResult format."""
|
|
303
|
+
task_state = task.status.state
|
|
304
|
+
|
|
305
|
+
if task_state == "completed":
|
|
306
|
+
# Extract the result from the artifacts array
|
|
307
|
+
result_data = self._extract_result_from_task(task)
|
|
308
|
+
|
|
309
|
+
# Check for task-level errors in the payload
|
|
310
|
+
errors = result_data.get("errors", []) if isinstance(result_data, dict) else []
|
|
311
|
+
has_errors = bool(errors)
|
|
312
|
+
|
|
313
|
+
return TaskResult[Any](
|
|
314
|
+
status=TaskStatus.COMPLETED,
|
|
315
|
+
data=result_data,
|
|
316
|
+
message=self._extract_text_from_task(task),
|
|
317
|
+
success=not has_errors,
|
|
318
|
+
metadata={
|
|
319
|
+
"task_id": task.id,
|
|
320
|
+
"context_id": task.context_id,
|
|
321
|
+
},
|
|
322
|
+
debug_info=debug_info,
|
|
323
|
+
)
|
|
324
|
+
elif task_state == "failed":
|
|
325
|
+
# Protocol-level failure - extract error message from TextPart
|
|
326
|
+
error_msg = self._extract_text_from_task(task) or "Task failed"
|
|
327
|
+
return TaskResult[Any](
|
|
328
|
+
status=TaskStatus.FAILED,
|
|
329
|
+
error=error_msg,
|
|
330
|
+
success=False,
|
|
331
|
+
debug_info=debug_info,
|
|
332
|
+
)
|
|
333
|
+
else:
|
|
334
|
+
# Handle all interim states (submitted, working, input-required, etc.)
|
|
335
|
+
return TaskResult[Any](
|
|
336
|
+
status=TaskStatus.SUBMITTED,
|
|
337
|
+
data=None, # Interim responses may not have structured AdCP content
|
|
338
|
+
message=self._extract_text_from_task(task),
|
|
339
|
+
success=True,
|
|
340
|
+
metadata={
|
|
341
|
+
"task_id": task.id,
|
|
342
|
+
"context_id": task.context_id,
|
|
343
|
+
"status": task_state,
|
|
344
|
+
},
|
|
345
|
+
debug_info=debug_info,
|
|
346
|
+
)
|
|
347
|
+
|
|
348
|
+
def _format_tool_request(self, tool_name: str, params: dict[str, Any]) -> str:
|
|
349
|
+
"""Format tool request as natural language for A2A."""
|
|
350
|
+
import json
|
|
351
|
+
|
|
352
|
+
return f"Execute tool: {tool_name}\nParameters: {json.dumps(params, indent=2)}"
|
|
353
|
+
|
|
354
|
+
def _extract_result_from_task(self, task: Task) -> Any:
|
|
355
|
+
"""
|
|
356
|
+
Extract result data from A2A Task following canonical format.
|
|
357
|
+
|
|
358
|
+
Per A2A response spec:
|
|
359
|
+
- Responses MUST include at least one DataPart (kind: "data")
|
|
360
|
+
- When multiple DataParts exist in an artifact, the last one is authoritative
|
|
361
|
+
- When multiple artifacts exist, use the last one (most recent in streaming)
|
|
362
|
+
- DataParts contain structured AdCP payload
|
|
363
|
+
"""
|
|
364
|
+
if not task.artifacts:
|
|
365
|
+
logger.warning("A2A Task missing required artifacts array")
|
|
366
|
+
return {}
|
|
367
|
+
|
|
368
|
+
# Use last artifact (most recent in streaming scenarios)
|
|
369
|
+
target_artifact = task.artifacts[-1]
|
|
370
|
+
|
|
371
|
+
if not target_artifact.parts:
|
|
372
|
+
logger.warning("A2A Task artifact has no parts")
|
|
373
|
+
return {}
|
|
374
|
+
|
|
375
|
+
# Find all DataParts (kind: "data")
|
|
376
|
+
# Note: Parts are wrapped in a Part union type, access via .root
|
|
377
|
+
from a2a.types import DataPart
|
|
378
|
+
|
|
379
|
+
data_parts = [p.root for p in target_artifact.parts if isinstance(p.root, DataPart)]
|
|
380
|
+
|
|
381
|
+
if not data_parts:
|
|
382
|
+
logger.warning("A2A Task missing required DataPart (kind: 'data')")
|
|
383
|
+
return {}
|
|
384
|
+
|
|
385
|
+
# Use last DataPart as authoritative (handles streaming scenarios within an artifact)
|
|
386
|
+
last_data_part = data_parts[-1]
|
|
387
|
+
data = last_data_part.data
|
|
388
|
+
|
|
389
|
+
# Some A2A implementations (e.g., ADK) wrap the response in {"response": {...}}
|
|
390
|
+
# Unwrap it to get the actual AdCP payload if present
|
|
391
|
+
if isinstance(data, dict) and "response" in data:
|
|
392
|
+
# If response is the only key, unwrap completely
|
|
393
|
+
if len(data) == 1:
|
|
394
|
+
return data["response"]
|
|
395
|
+
# If there are other keys alongside response, prefer the wrapped content
|
|
396
|
+
return data["response"]
|
|
397
|
+
|
|
398
|
+
return data
|
|
399
|
+
|
|
400
|
+
def _extract_text_from_task(self, task: Task) -> str | None:
|
|
401
|
+
"""Extract human-readable message from TextPart if present."""
|
|
402
|
+
if not task.artifacts:
|
|
403
|
+
return None
|
|
404
|
+
|
|
405
|
+
# Use last artifact (most recent in streaming scenarios)
|
|
406
|
+
target_artifact = task.artifacts[-1]
|
|
407
|
+
|
|
408
|
+
# Find TextPart (kind: "text")
|
|
409
|
+
# Note: Parts are wrapped in a Part union type, access via .root
|
|
410
|
+
for part in target_artifact.parts:
|
|
411
|
+
if isinstance(part.root, TextPart):
|
|
412
|
+
return part.root.text
|
|
413
|
+
|
|
414
|
+
return None
|
|
415
|
+
|
|
416
|
+
# ========================================================================
|
|
417
|
+
# ADCP Protocol Methods
|
|
418
|
+
# ========================================================================
|
|
419
|
+
|
|
420
|
+
async def get_products(self, params: dict[str, Any]) -> TaskResult[Any]:
|
|
421
|
+
"""Get advertising products."""
|
|
422
|
+
return await self._call_a2a_tool("get_products", params)
|
|
423
|
+
|
|
424
|
+
async def list_creative_formats(self, params: dict[str, Any]) -> TaskResult[Any]:
|
|
425
|
+
"""List supported creative formats."""
|
|
426
|
+
return await self._call_a2a_tool("list_creative_formats", params)
|
|
427
|
+
|
|
428
|
+
async def sync_creatives(self, params: dict[str, Any]) -> TaskResult[Any]:
|
|
429
|
+
"""Sync creatives."""
|
|
430
|
+
return await self._call_a2a_tool("sync_creatives", params)
|
|
431
|
+
|
|
432
|
+
async def list_creatives(self, params: dict[str, Any]) -> TaskResult[Any]:
|
|
433
|
+
"""List creatives."""
|
|
434
|
+
return await self._call_a2a_tool("list_creatives", params)
|
|
435
|
+
|
|
436
|
+
async def get_media_buy_delivery(self, params: dict[str, Any]) -> TaskResult[Any]:
|
|
437
|
+
"""Get media buy delivery."""
|
|
438
|
+
return await self._call_a2a_tool("get_media_buy_delivery", params)
|
|
439
|
+
|
|
440
|
+
async def list_authorized_properties(self, params: dict[str, Any]) -> TaskResult[Any]:
|
|
441
|
+
"""List authorized properties."""
|
|
442
|
+
return await self._call_a2a_tool("list_authorized_properties", params)
|
|
443
|
+
|
|
444
|
+
async def get_signals(self, params: dict[str, Any]) -> TaskResult[Any]:
|
|
445
|
+
"""Get signals."""
|
|
446
|
+
return await self._call_a2a_tool("get_signals", params)
|
|
447
|
+
|
|
448
|
+
async def activate_signal(self, params: dict[str, Any]) -> TaskResult[Any]:
|
|
449
|
+
"""Activate signal."""
|
|
450
|
+
return await self._call_a2a_tool("activate_signal", params)
|
|
451
|
+
|
|
452
|
+
async def provide_performance_feedback(self, params: dict[str, Any]) -> TaskResult[Any]:
|
|
453
|
+
"""Provide performance feedback."""
|
|
454
|
+
return await self._call_a2a_tool("provide_performance_feedback", params)
|
|
455
|
+
|
|
456
|
+
async def preview_creative(self, params: dict[str, Any]) -> TaskResult[Any]:
|
|
457
|
+
"""Generate preview URLs for a creative manifest."""
|
|
458
|
+
return await self._call_a2a_tool("preview_creative", params)
|
|
459
|
+
|
|
460
|
+
async def create_media_buy(self, params: dict[str, Any]) -> TaskResult[Any]:
|
|
461
|
+
"""Create media buy."""
|
|
462
|
+
return await self._call_a2a_tool("create_media_buy", params)
|
|
463
|
+
|
|
464
|
+
async def update_media_buy(self, params: dict[str, Any]) -> TaskResult[Any]:
|
|
465
|
+
"""Update media buy."""
|
|
466
|
+
return await self._call_a2a_tool("update_media_buy", params)
|
|
467
|
+
|
|
468
|
+
async def build_creative(self, params: dict[str, Any]) -> TaskResult[Any]:
|
|
469
|
+
"""Build creative."""
|
|
470
|
+
return await self._call_a2a_tool("build_creative", params)
|
|
471
|
+
|
|
472
|
+
async def list_tools(self) -> list[str]:
|
|
473
|
+
"""
|
|
474
|
+
List available tools from A2A agent.
|
|
475
|
+
|
|
476
|
+
Uses A2A client which already fetched the agent card during initialization.
|
|
477
|
+
"""
|
|
478
|
+
# Get the A2A client (which already fetched the agent card)
|
|
479
|
+
a2a_client = await self._get_a2a_client()
|
|
480
|
+
|
|
481
|
+
# Fetch the agent card using the official method
|
|
482
|
+
try:
|
|
483
|
+
agent_card = await a2a_client.get_card()
|
|
484
|
+
|
|
485
|
+
# Extract skills from agent card
|
|
486
|
+
tool_names = [skill.name for skill in agent_card.skills if skill.name]
|
|
487
|
+
|
|
488
|
+
logger.info(f"Found {len(tool_names)} tools from A2A agent {self.agent_config.id}")
|
|
489
|
+
return tool_names
|
|
490
|
+
|
|
491
|
+
except httpx.HTTPStatusError as e:
|
|
492
|
+
status_code = e.response.status_code
|
|
493
|
+
if status_code in (401, 403):
|
|
494
|
+
logger.error(f"Authentication failed for A2A agent {self.agent_config.id}")
|
|
495
|
+
raise ADCPAuthenticationError(
|
|
496
|
+
f"Authentication failed: HTTP {status_code}",
|
|
497
|
+
agent_id=self.agent_config.id,
|
|
498
|
+
agent_uri=self.agent_config.agent_uri,
|
|
499
|
+
) from e
|
|
500
|
+
else:
|
|
501
|
+
logger.error(f"HTTP {status_code} error fetching agent card: {e}")
|
|
502
|
+
raise ADCPConnectionError(
|
|
503
|
+
f"Failed to fetch agent card: HTTP {status_code}",
|
|
504
|
+
agent_id=self.agent_config.id,
|
|
505
|
+
agent_uri=self.agent_config.agent_uri,
|
|
506
|
+
) from e
|
|
507
|
+
except httpx.TimeoutException as e:
|
|
508
|
+
logger.error(f"Timeout fetching agent card for {self.agent_config.id}")
|
|
509
|
+
raise ADCPTimeoutError(
|
|
510
|
+
f"Timeout fetching agent card: {e}",
|
|
511
|
+
agent_id=self.agent_config.id,
|
|
512
|
+
agent_uri=self.agent_config.agent_uri,
|
|
513
|
+
timeout=self.agent_config.timeout,
|
|
514
|
+
) from e
|
|
515
|
+
except httpx.HTTPError as e:
|
|
516
|
+
logger.error(f"HTTP error fetching agent card: {e}")
|
|
517
|
+
raise ADCPConnectionError(
|
|
518
|
+
f"Failed to fetch agent card: {e}",
|
|
519
|
+
agent_id=self.agent_config.id,
|
|
520
|
+
agent_uri=self.agent_config.agent_uri,
|
|
521
|
+
) from e
|
|
522
|
+
|
|
523
|
+
async def get_agent_info(self) -> dict[str, Any]:
|
|
524
|
+
"""
|
|
525
|
+
Get agent information including AdCP extension metadata from A2A agent card.
|
|
526
|
+
|
|
527
|
+
Uses A2A client's get_card() method to fetch the agent card and extracts:
|
|
528
|
+
- Basic agent info (name, description, version)
|
|
529
|
+
- AdCP extension (extensions.adcp.adcp_version, extensions.adcp.protocols_supported)
|
|
530
|
+
- Available skills/tools
|
|
531
|
+
|
|
532
|
+
Returns:
|
|
533
|
+
Dictionary with agent metadata
|
|
534
|
+
"""
|
|
535
|
+
# Get the A2A client (which already fetched the agent card)
|
|
536
|
+
a2a_client = await self._get_a2a_client()
|
|
537
|
+
|
|
538
|
+
logger.debug(f"Fetching A2A agent info for {self.agent_config.id}")
|
|
539
|
+
|
|
540
|
+
try:
|
|
541
|
+
agent_card = await a2a_client.get_card()
|
|
542
|
+
|
|
543
|
+
# Extract basic info
|
|
544
|
+
info: dict[str, Any] = {
|
|
545
|
+
"name": agent_card.name,
|
|
546
|
+
"description": agent_card.description,
|
|
547
|
+
"version": agent_card.version,
|
|
548
|
+
"protocol": "a2a",
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
# Extract skills/tools
|
|
552
|
+
tool_names = [skill.name for skill in agent_card.skills if skill.name]
|
|
553
|
+
if tool_names:
|
|
554
|
+
info["tools"] = tool_names
|
|
555
|
+
|
|
556
|
+
# Extract AdCP extension metadata
|
|
557
|
+
# Note: AgentCard type doesn't include extensions in the SDK,
|
|
558
|
+
# but it may be present at runtime
|
|
559
|
+
extensions = getattr(agent_card, "extensions", None)
|
|
560
|
+
if extensions:
|
|
561
|
+
adcp_ext = extensions.get("adcp")
|
|
562
|
+
if adcp_ext:
|
|
563
|
+
info["adcp_version"] = adcp_ext.get("adcp_version")
|
|
564
|
+
info["protocols_supported"] = adcp_ext.get("protocols_supported")
|
|
565
|
+
|
|
566
|
+
logger.info(f"Retrieved agent info for {self.agent_config.id}")
|
|
567
|
+
return info
|
|
568
|
+
|
|
569
|
+
except httpx.HTTPStatusError as e:
|
|
570
|
+
status_code = e.response.status_code
|
|
571
|
+
if status_code in (401, 403):
|
|
572
|
+
raise ADCPAuthenticationError(
|
|
573
|
+
f"Authentication failed: HTTP {status_code}",
|
|
574
|
+
agent_id=self.agent_config.id,
|
|
575
|
+
agent_uri=self.agent_config.agent_uri,
|
|
576
|
+
) from e
|
|
577
|
+
else:
|
|
578
|
+
raise ADCPConnectionError(
|
|
579
|
+
f"Failed to fetch agent card: HTTP {status_code}",
|
|
580
|
+
agent_id=self.agent_config.id,
|
|
581
|
+
agent_uri=self.agent_config.agent_uri,
|
|
582
|
+
) from e
|
|
583
|
+
except httpx.TimeoutException as e:
|
|
584
|
+
raise ADCPTimeoutError(
|
|
585
|
+
f"Timeout fetching agent card: {e}",
|
|
586
|
+
agent_id=self.agent_config.id,
|
|
587
|
+
agent_uri=self.agent_config.agent_uri,
|
|
588
|
+
timeout=self.agent_config.timeout,
|
|
589
|
+
) from e
|
|
590
|
+
except httpx.HTTPError as e:
|
|
591
|
+
raise ADCPConnectionError(
|
|
592
|
+
f"Failed to fetch agent card: {e}",
|
|
593
|
+
agent_id=self.agent_config.id,
|
|
594
|
+
agent_uri=self.agent_config.agent_uri,
|
|
595
|
+
) from e
|