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/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 adcp.types.core import AgentConfig, TaskResult
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
- """Base class for protocol adapters."""
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
- @abstractmethod
19
- async def call_tool(self, tool_name: str, params: dict[str, Any]) -> TaskResult[Any]:
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
- Call a tool on the agent.
35
+ Parse raw TaskResult into typed TaskResult.
36
+
37
+ Handles both MCP content arrays and A2A dict responses.
22
38
 
23
39
  Args:
24
- tool_name: Name of the tool to call
25
- params: Tool parameters
40
+ raw_result: Raw TaskResult from adapter
41
+ response_type: Expected Pydantic response type
26
42
 
27
43
  Returns:
28
- TaskResult with the response
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 call_tool(self, tool_name: str, params: dict[str, Any]) -> TaskResult[Any]:
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