adcp 1.0.1__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 +1 -1
- adcp/__main__.py +107 -8
- adcp/client.py +130 -75
- adcp/protocols/a2a.py +46 -4
- adcp/protocols/base.py +110 -9
- adcp/protocols/mcp.py +41 -1
- adcp/types/generated.py +19 -2
- adcp/types/tasks.py +313 -83
- adcp/utils/response_parser.py +119 -0
- {adcp-1.0.1.dist-info → adcp-1.0.3.dist-info}/METADATA +1 -1
- adcp-1.0.3.dist-info/RECORD +22 -0
- adcp-1.0.1.dist-info/RECORD +0 -21
- {adcp-1.0.1.dist-info → adcp-1.0.3.dist-info}/WHEEL +0 -0
- {adcp-1.0.1.dist-info → adcp-1.0.3.dist-info}/entry_points.txt +0 -0
- {adcp-1.0.1.dist-info → adcp-1.0.3.dist-info}/licenses/LICENSE +0 -0
- {adcp-1.0.1.dist-info → adcp-1.0.3.dist-info}/top_level.txt +0 -0
adcp/protocols/base.py
CHANGED
|
@@ -3,30 +3,131 @@ from __future__ import annotations
|
|
|
3
3
|
"""Base protocol adapter interface."""
|
|
4
4
|
|
|
5
5
|
from abc import ABC, abstractmethod
|
|
6
|
-
from typing import Any
|
|
6
|
+
from typing import Any, TypeVar
|
|
7
7
|
|
|
8
|
-
from
|
|
8
|
+
from pydantic import BaseModel
|
|
9
|
+
|
|
10
|
+
from adcp.types.core import AgentConfig, TaskResult, TaskStatus
|
|
11
|
+
from adcp.utils.response_parser import parse_json_or_text, parse_mcp_content
|
|
12
|
+
|
|
13
|
+
T = TypeVar("T", bound=BaseModel)
|
|
9
14
|
|
|
10
15
|
|
|
11
16
|
class ProtocolAdapter(ABC):
|
|
12
|
-
"""
|
|
17
|
+
"""
|
|
18
|
+
Base class for protocol adapters.
|
|
19
|
+
|
|
20
|
+
Each adapter implements the ADCP protocol methods and handles
|
|
21
|
+
protocol-specific translation (MCP/A2A) while returning properly
|
|
22
|
+
typed responses.
|
|
23
|
+
"""
|
|
13
24
|
|
|
14
25
|
def __init__(self, agent_config: AgentConfig):
|
|
15
26
|
"""Initialize adapter with agent configuration."""
|
|
16
27
|
self.agent_config = agent_config
|
|
17
28
|
|
|
18
|
-
|
|
19
|
-
|
|
29
|
+
# ========================================================================
|
|
30
|
+
# Helper methods for response parsing
|
|
31
|
+
# ========================================================================
|
|
32
|
+
|
|
33
|
+
def _parse_response(self, raw_result: TaskResult[Any], response_type: type[T]) -> TaskResult[T]:
|
|
20
34
|
"""
|
|
21
|
-
|
|
35
|
+
Parse raw TaskResult into typed TaskResult.
|
|
36
|
+
|
|
37
|
+
Handles both MCP content arrays and A2A dict responses.
|
|
22
38
|
|
|
23
39
|
Args:
|
|
24
|
-
|
|
25
|
-
|
|
40
|
+
raw_result: Raw TaskResult from adapter
|
|
41
|
+
response_type: Expected Pydantic response type
|
|
26
42
|
|
|
27
43
|
Returns:
|
|
28
|
-
TaskResult
|
|
44
|
+
Typed TaskResult
|
|
29
45
|
"""
|
|
46
|
+
# Handle failed results or missing data
|
|
47
|
+
if not raw_result.success or raw_result.data is None:
|
|
48
|
+
# Explicitly construct typed result to satisfy type checker
|
|
49
|
+
return TaskResult[T](
|
|
50
|
+
status=raw_result.status,
|
|
51
|
+
data=None,
|
|
52
|
+
success=False,
|
|
53
|
+
error=raw_result.error or "No data returned from adapter",
|
|
54
|
+
metadata=raw_result.metadata,
|
|
55
|
+
debug_info=raw_result.debug_info,
|
|
56
|
+
)
|
|
57
|
+
|
|
58
|
+
try:
|
|
59
|
+
# Handle MCP content arrays
|
|
60
|
+
if isinstance(raw_result.data, list):
|
|
61
|
+
parsed_data = parse_mcp_content(raw_result.data, response_type)
|
|
62
|
+
else:
|
|
63
|
+
# Handle A2A or direct responses
|
|
64
|
+
parsed_data = parse_json_or_text(raw_result.data, response_type)
|
|
65
|
+
|
|
66
|
+
return TaskResult[T](
|
|
67
|
+
status=raw_result.status,
|
|
68
|
+
data=parsed_data,
|
|
69
|
+
success=raw_result.success,
|
|
70
|
+
error=raw_result.error,
|
|
71
|
+
metadata=raw_result.metadata,
|
|
72
|
+
debug_info=raw_result.debug_info,
|
|
73
|
+
)
|
|
74
|
+
except ValueError as e:
|
|
75
|
+
# Parsing failed - return error result
|
|
76
|
+
return TaskResult[T](
|
|
77
|
+
status=TaskStatus.FAILED,
|
|
78
|
+
error=f"Failed to parse response: {e}",
|
|
79
|
+
success=False,
|
|
80
|
+
debug_info=raw_result.debug_info,
|
|
81
|
+
)
|
|
82
|
+
|
|
83
|
+
# ========================================================================
|
|
84
|
+
# ADCP Protocol Methods - Type-safe, spec-aligned interface
|
|
85
|
+
# Each adapter MUST implement these methods explicitly.
|
|
86
|
+
# ========================================================================
|
|
87
|
+
|
|
88
|
+
@abstractmethod
|
|
89
|
+
async def get_products(self, params: dict[str, Any]) -> TaskResult[Any]:
|
|
90
|
+
"""Get advertising products."""
|
|
91
|
+
pass
|
|
92
|
+
|
|
93
|
+
@abstractmethod
|
|
94
|
+
async def list_creative_formats(self, params: dict[str, Any]) -> TaskResult[Any]:
|
|
95
|
+
"""List supported creative formats."""
|
|
96
|
+
pass
|
|
97
|
+
|
|
98
|
+
@abstractmethod
|
|
99
|
+
async def sync_creatives(self, params: dict[str, Any]) -> TaskResult[Any]:
|
|
100
|
+
"""Sync creatives."""
|
|
101
|
+
pass
|
|
102
|
+
|
|
103
|
+
@abstractmethod
|
|
104
|
+
async def list_creatives(self, params: dict[str, Any]) -> TaskResult[Any]:
|
|
105
|
+
"""List creatives."""
|
|
106
|
+
pass
|
|
107
|
+
|
|
108
|
+
@abstractmethod
|
|
109
|
+
async def get_media_buy_delivery(self, params: dict[str, Any]) -> TaskResult[Any]:
|
|
110
|
+
"""Get media buy delivery."""
|
|
111
|
+
pass
|
|
112
|
+
|
|
113
|
+
@abstractmethod
|
|
114
|
+
async def list_authorized_properties(self, params: dict[str, Any]) -> TaskResult[Any]:
|
|
115
|
+
"""List authorized properties."""
|
|
116
|
+
pass
|
|
117
|
+
|
|
118
|
+
@abstractmethod
|
|
119
|
+
async def get_signals(self, params: dict[str, Any]) -> TaskResult[Any]:
|
|
120
|
+
"""Get signals."""
|
|
121
|
+
pass
|
|
122
|
+
|
|
123
|
+
@abstractmethod
|
|
124
|
+
async def activate_signal(self, params: dict[str, Any]) -> TaskResult[Any]:
|
|
125
|
+
"""Activate signal."""
|
|
126
|
+
pass
|
|
127
|
+
|
|
128
|
+
@abstractmethod
|
|
129
|
+
async def provide_performance_feedback(self, params: dict[str, Any]) -> TaskResult[Any]:
|
|
130
|
+
"""Provide performance feedback."""
|
|
30
131
|
pass
|
|
31
132
|
|
|
32
133
|
@abstractmethod
|
adcp/protocols/mcp.py
CHANGED
|
@@ -186,7 +186,7 @@ class MCPAdapter(ProtocolAdapter):
|
|
|
186
186
|
else:
|
|
187
187
|
raise ValueError(f"Unsupported transport scheme: {parsed.scheme}")
|
|
188
188
|
|
|
189
|
-
async def
|
|
189
|
+
async def _call_mcp_tool(self, tool_name: str, params: dict[str, Any]) -> TaskResult[Any]:
|
|
190
190
|
"""Call a tool using MCP protocol."""
|
|
191
191
|
start_time = time.time() if self.agent_config.debug else None
|
|
192
192
|
debug_info = None
|
|
@@ -240,6 +240,46 @@ class MCPAdapter(ProtocolAdapter):
|
|
|
240
240
|
debug_info=debug_info,
|
|
241
241
|
)
|
|
242
242
|
|
|
243
|
+
# ========================================================================
|
|
244
|
+
# ADCP Protocol Methods
|
|
245
|
+
# ========================================================================
|
|
246
|
+
|
|
247
|
+
async def get_products(self, params: dict[str, Any]) -> TaskResult[Any]:
|
|
248
|
+
"""Get advertising products."""
|
|
249
|
+
return await self._call_mcp_tool("get_products", params)
|
|
250
|
+
|
|
251
|
+
async def list_creative_formats(self, params: dict[str, Any]) -> TaskResult[Any]:
|
|
252
|
+
"""List supported creative formats."""
|
|
253
|
+
return await self._call_mcp_tool("list_creative_formats", params)
|
|
254
|
+
|
|
255
|
+
async def sync_creatives(self, params: dict[str, Any]) -> TaskResult[Any]:
|
|
256
|
+
"""Sync creatives."""
|
|
257
|
+
return await self._call_mcp_tool("sync_creatives", params)
|
|
258
|
+
|
|
259
|
+
async def list_creatives(self, params: dict[str, Any]) -> TaskResult[Any]:
|
|
260
|
+
"""List creatives."""
|
|
261
|
+
return await self._call_mcp_tool("list_creatives", params)
|
|
262
|
+
|
|
263
|
+
async def get_media_buy_delivery(self, params: dict[str, Any]) -> TaskResult[Any]:
|
|
264
|
+
"""Get media buy delivery."""
|
|
265
|
+
return await self._call_mcp_tool("get_media_buy_delivery", params)
|
|
266
|
+
|
|
267
|
+
async def list_authorized_properties(self, params: dict[str, Any]) -> TaskResult[Any]:
|
|
268
|
+
"""List authorized properties."""
|
|
269
|
+
return await self._call_mcp_tool("list_authorized_properties", params)
|
|
270
|
+
|
|
271
|
+
async def get_signals(self, params: dict[str, Any]) -> TaskResult[Any]:
|
|
272
|
+
"""Get signals."""
|
|
273
|
+
return await self._call_mcp_tool("get_signals", params)
|
|
274
|
+
|
|
275
|
+
async def activate_signal(self, params: dict[str, Any]) -> TaskResult[Any]:
|
|
276
|
+
"""Activate signal."""
|
|
277
|
+
return await self._call_mcp_tool("activate_signal", params)
|
|
278
|
+
|
|
279
|
+
async def provide_performance_feedback(self, params: dict[str, Any]) -> TaskResult[Any]:
|
|
280
|
+
"""Provide performance feedback."""
|
|
281
|
+
return await self._call_mcp_tool("provide_performance_feedback", params)
|
|
282
|
+
|
|
243
283
|
async def list_tools(self) -> list[str]:
|
|
244
284
|
"""List available tools from MCP agent."""
|
|
245
285
|
session = await self._get_session()
|
adcp/types/generated.py
CHANGED
|
@@ -11,9 +11,10 @@ To regenerate:
|
|
|
11
11
|
|
|
12
12
|
from __future__ import annotations
|
|
13
13
|
|
|
14
|
+
import re
|
|
14
15
|
from typing import Any, Literal
|
|
15
16
|
|
|
16
|
-
from pydantic import BaseModel, Field
|
|
17
|
+
from pydantic import BaseModel, Field, field_validator
|
|
17
18
|
|
|
18
19
|
|
|
19
20
|
# ============================================================================
|
|
@@ -22,7 +23,6 @@ from pydantic import BaseModel, Field
|
|
|
22
23
|
|
|
23
24
|
# These types are referenced in schemas but don't have schema files
|
|
24
25
|
# Defining them as type aliases to maintain type safety
|
|
25
|
-
FormatId = str
|
|
26
26
|
PackageRequest = dict[str, Any]
|
|
27
27
|
PushNotificationConfig = dict[str, Any]
|
|
28
28
|
ReportingCapabilities = dict[str, Any]
|
|
@@ -32,6 +32,23 @@ ReportingCapabilities = dict[str, Any]
|
|
|
32
32
|
# CORE DOMAIN TYPES
|
|
33
33
|
# ============================================================================
|
|
34
34
|
|
|
35
|
+
class FormatId(BaseModel):
|
|
36
|
+
"""Structured format identifier with agent URL and format name"""
|
|
37
|
+
|
|
38
|
+
agent_url: str = Field(description="URL of the agent that defines this format (e.g., 'https://creatives.adcontextprotocol.org' for standard formats, or 'https://publisher.com/.well-known/adcp/sales' for custom formats)")
|
|
39
|
+
id: str = Field(description="Format identifier within the agent's namespace (e.g., 'display_300x250', 'video_standard_30s')")
|
|
40
|
+
|
|
41
|
+
@field_validator("id")
|
|
42
|
+
@classmethod
|
|
43
|
+
def validate_id_pattern(cls, v: str) -> str:
|
|
44
|
+
"""Validate format ID contains only alphanumeric characters, hyphens, and underscores."""
|
|
45
|
+
if not re.match(r"^[a-zA-Z0-9_-]+$", v):
|
|
46
|
+
raise ValueError(
|
|
47
|
+
f"Invalid format ID: {v!r}. Must contain only alphanumeric characters, hyphens, and underscores"
|
|
48
|
+
)
|
|
49
|
+
return v
|
|
50
|
+
|
|
51
|
+
|
|
35
52
|
class Product(BaseModel):
|
|
36
53
|
"""Represents available advertising inventory"""
|
|
37
54
|
|