adcp 1.0.4__tar.gz → 1.1.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-1.0.4/src/adcp.egg-info → adcp-1.1.0}/PKG-INFO +61 -16
- {adcp-1.0.4 → adcp-1.1.0}/README.md +60 -15
- {adcp-1.0.4 → adcp-1.1.0}/pyproject.toml +1 -1
- {adcp-1.0.4 → adcp-1.1.0}/src/adcp/__init__.py +1 -1
- {adcp-1.0.4 → adcp-1.1.0}/src/adcp/__main__.py +22 -21
- {adcp-1.0.4 → adcp-1.1.0}/src/adcp/client.py +96 -5
- {adcp-1.0.4 → adcp-1.1.0}/src/adcp/protocols/a2a.py +4 -0
- {adcp-1.0.4 → adcp-1.1.0}/src/adcp/protocols/base.py +3 -0
- {adcp-1.0.4 → adcp-1.1.0}/src/adcp/protocols/mcp.py +71 -47
- {adcp-1.0.4 → adcp-1.1.0}/src/adcp/types/core.py +4 -4
- {adcp-1.0.4 → adcp-1.1.0}/src/adcp/types/generated.py +58 -35
- adcp-1.1.0/src/adcp/utils/preview_cache.py +461 -0
- {adcp-1.0.4 → adcp-1.1.0/src/adcp.egg-info}/PKG-INFO +61 -16
- {adcp-1.0.4 → adcp-1.1.0}/src/adcp.egg-info/SOURCES.txt +2 -0
- {adcp-1.0.4 → adcp-1.1.0}/tests/test_client.py +107 -0
- adcp-1.1.0/tests/test_preview_html.py +376 -0
- {adcp-1.0.4 → adcp-1.1.0}/tests/test_protocols.py +161 -3
- {adcp-1.0.4 → adcp-1.1.0}/LICENSE +0 -0
- {adcp-1.0.4 → adcp-1.1.0}/setup.cfg +0 -0
- {adcp-1.0.4 → adcp-1.1.0}/src/adcp/config.py +0 -0
- {adcp-1.0.4 → adcp-1.1.0}/src/adcp/exceptions.py +0 -0
- {adcp-1.0.4 → adcp-1.1.0}/src/adcp/protocols/__init__.py +0 -0
- {adcp-1.0.4 → adcp-1.1.0}/src/adcp/types/__init__.py +0 -0
- {adcp-1.0.4 → adcp-1.1.0}/src/adcp/types/tasks.py +0 -0
- {adcp-1.0.4 → adcp-1.1.0}/src/adcp/utils/__init__.py +0 -0
- {adcp-1.0.4 → adcp-1.1.0}/src/adcp/utils/operation_id.py +0 -0
- {adcp-1.0.4 → adcp-1.1.0}/src/adcp/utils/response_parser.py +0 -0
- {adcp-1.0.4 → adcp-1.1.0}/src/adcp.egg-info/dependency_links.txt +0 -0
- {adcp-1.0.4 → adcp-1.1.0}/src/adcp.egg-info/entry_points.txt +0 -0
- {adcp-1.0.4 → adcp-1.1.0}/src/adcp.egg-info/requires.txt +0 -0
- {adcp-1.0.4 → adcp-1.1.0}/src/adcp.egg-info/top_level.txt +0 -0
- {adcp-1.0.4 → adcp-1.1.0}/tests/test_cli.py +0 -0
- {adcp-1.0.4 → adcp-1.1.0}/tests/test_code_generation.py +0 -0
- {adcp-1.0.4 → adcp-1.1.0}/tests/test_format_id_validation.py +0 -0
- {adcp-1.0.4 → adcp-1.1.0}/tests/test_response_parser.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: adcp
|
|
3
|
-
Version: 1.0
|
|
3
|
+
Version: 1.1.0
|
|
4
4
|
Summary: Official Python client for the Ad Context Protocol (AdCP)
|
|
5
5
|
Author-email: AdCP Community <maintainers@adcontextprotocol.org>
|
|
6
6
|
License: Apache-2.0
|
|
@@ -64,8 +64,8 @@ pip install adcp
|
|
|
64
64
|
```python
|
|
65
65
|
from adcp import ADCPMultiAgentClient, AgentConfig, GetProductsRequest
|
|
66
66
|
|
|
67
|
-
# Configure agents and handlers
|
|
68
|
-
|
|
67
|
+
# Configure agents and handlers (context manager ensures proper cleanup)
|
|
68
|
+
async with ADCPMultiAgentClient(
|
|
69
69
|
agents=[
|
|
70
70
|
AgentConfig(
|
|
71
71
|
id="agent_x",
|
|
@@ -91,21 +91,21 @@ client = ADCPMultiAgentClient(
|
|
|
91
91
|
if metadata.status == "completed" else None
|
|
92
92
|
)
|
|
93
93
|
}
|
|
94
|
-
)
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
result = await agent.get_products(request)
|
|
94
|
+
) as client:
|
|
95
|
+
# Execute operation - library handles operation IDs, webhook URLs, context management
|
|
96
|
+
agent = client.agent("agent_x")
|
|
97
|
+
request = GetProductsRequest(brief="Coffee brands")
|
|
98
|
+
result = await agent.get_products(request)
|
|
100
99
|
|
|
101
|
-
# Check result
|
|
102
|
-
if result.status == "completed":
|
|
103
|
-
|
|
104
|
-
|
|
100
|
+
# Check result
|
|
101
|
+
if result.status == "completed":
|
|
102
|
+
# Agent completed synchronously!
|
|
103
|
+
print(f"✅ Sync completion: {len(result.data.products)} products")
|
|
105
104
|
|
|
106
|
-
if result.status == "submitted":
|
|
107
|
-
|
|
108
|
-
|
|
105
|
+
if result.status == "submitted":
|
|
106
|
+
# Agent will send webhook when complete
|
|
107
|
+
print(f"⏳ Async - webhook registered at: {result.submitted.webhook_url}")
|
|
108
|
+
# Connections automatically cleaned up here
|
|
109
109
|
```
|
|
110
110
|
|
|
111
111
|
## Features
|
|
@@ -210,6 +210,51 @@ Or use the CLI:
|
|
|
210
210
|
uvx adcp --debug myagent get_products '{"brief":"TV ads"}'
|
|
211
211
|
```
|
|
212
212
|
|
|
213
|
+
### Resource Management
|
|
214
|
+
|
|
215
|
+
**Why use async context managers?**
|
|
216
|
+
- Ensures HTTP connections are properly closed, preventing resource leaks
|
|
217
|
+
- Handles cleanup even when exceptions occur
|
|
218
|
+
- Required for production applications with connection pooling
|
|
219
|
+
- Prevents issues with async task group cleanup in MCP protocol
|
|
220
|
+
|
|
221
|
+
The recommended pattern uses async context managers:
|
|
222
|
+
|
|
223
|
+
```python
|
|
224
|
+
from adcp import ADCPClient, AgentConfig, GetProductsRequest
|
|
225
|
+
|
|
226
|
+
# Recommended: Automatic cleanup with context manager
|
|
227
|
+
config = AgentConfig(id="agent_x", agent_uri="https://...", protocol="a2a")
|
|
228
|
+
async with ADCPClient(config) as client:
|
|
229
|
+
request = GetProductsRequest(brief="Coffee brands")
|
|
230
|
+
result = await client.get_products(request)
|
|
231
|
+
# Connection automatically closed on exit
|
|
232
|
+
|
|
233
|
+
# Multi-agent client also supports context managers
|
|
234
|
+
async with ADCPMultiAgentClient(agents) as client:
|
|
235
|
+
# Execute across all agents in parallel
|
|
236
|
+
results = await client.get_products(request)
|
|
237
|
+
# All agent connections closed automatically (even if some failed)
|
|
238
|
+
```
|
|
239
|
+
|
|
240
|
+
Manual cleanup is available for special cases (e.g., managing client lifecycle manually):
|
|
241
|
+
|
|
242
|
+
```python
|
|
243
|
+
# Use manual cleanup when you need fine-grained control over lifecycle
|
|
244
|
+
client = ADCPClient(config)
|
|
245
|
+
try:
|
|
246
|
+
result = await client.get_products(request)
|
|
247
|
+
finally:
|
|
248
|
+
await client.close() # Explicit cleanup
|
|
249
|
+
```
|
|
250
|
+
|
|
251
|
+
**When to use manual cleanup:**
|
|
252
|
+
- Managing client lifecycle across multiple functions
|
|
253
|
+
- Testing scenarios requiring explicit control
|
|
254
|
+
- Integration with frameworks that manage resources differently
|
|
255
|
+
|
|
256
|
+
In most cases, prefer the context manager pattern.
|
|
257
|
+
|
|
213
258
|
### Error Handling
|
|
214
259
|
|
|
215
260
|
The library provides a comprehensive exception hierarchy with helpful error messages:
|
|
@@ -27,8 +27,8 @@ pip install adcp
|
|
|
27
27
|
```python
|
|
28
28
|
from adcp import ADCPMultiAgentClient, AgentConfig, GetProductsRequest
|
|
29
29
|
|
|
30
|
-
# Configure agents and handlers
|
|
31
|
-
|
|
30
|
+
# Configure agents and handlers (context manager ensures proper cleanup)
|
|
31
|
+
async with ADCPMultiAgentClient(
|
|
32
32
|
agents=[
|
|
33
33
|
AgentConfig(
|
|
34
34
|
id="agent_x",
|
|
@@ -54,21 +54,21 @@ client = ADCPMultiAgentClient(
|
|
|
54
54
|
if metadata.status == "completed" else None
|
|
55
55
|
)
|
|
56
56
|
}
|
|
57
|
-
)
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
result = await agent.get_products(request)
|
|
57
|
+
) as client:
|
|
58
|
+
# Execute operation - library handles operation IDs, webhook URLs, context management
|
|
59
|
+
agent = client.agent("agent_x")
|
|
60
|
+
request = GetProductsRequest(brief="Coffee brands")
|
|
61
|
+
result = await agent.get_products(request)
|
|
63
62
|
|
|
64
|
-
# Check result
|
|
65
|
-
if result.status == "completed":
|
|
66
|
-
|
|
67
|
-
|
|
63
|
+
# Check result
|
|
64
|
+
if result.status == "completed":
|
|
65
|
+
# Agent completed synchronously!
|
|
66
|
+
print(f"✅ Sync completion: {len(result.data.products)} products")
|
|
68
67
|
|
|
69
|
-
if result.status == "submitted":
|
|
70
|
-
|
|
71
|
-
|
|
68
|
+
if result.status == "submitted":
|
|
69
|
+
# Agent will send webhook when complete
|
|
70
|
+
print(f"⏳ Async - webhook registered at: {result.submitted.webhook_url}")
|
|
71
|
+
# Connections automatically cleaned up here
|
|
72
72
|
```
|
|
73
73
|
|
|
74
74
|
## Features
|
|
@@ -173,6 +173,51 @@ Or use the CLI:
|
|
|
173
173
|
uvx adcp --debug myagent get_products '{"brief":"TV ads"}'
|
|
174
174
|
```
|
|
175
175
|
|
|
176
|
+
### Resource Management
|
|
177
|
+
|
|
178
|
+
**Why use async context managers?**
|
|
179
|
+
- Ensures HTTP connections are properly closed, preventing resource leaks
|
|
180
|
+
- Handles cleanup even when exceptions occur
|
|
181
|
+
- Required for production applications with connection pooling
|
|
182
|
+
- Prevents issues with async task group cleanup in MCP protocol
|
|
183
|
+
|
|
184
|
+
The recommended pattern uses async context managers:
|
|
185
|
+
|
|
186
|
+
```python
|
|
187
|
+
from adcp import ADCPClient, AgentConfig, GetProductsRequest
|
|
188
|
+
|
|
189
|
+
# Recommended: Automatic cleanup with context manager
|
|
190
|
+
config = AgentConfig(id="agent_x", agent_uri="https://...", protocol="a2a")
|
|
191
|
+
async with ADCPClient(config) as client:
|
|
192
|
+
request = GetProductsRequest(brief="Coffee brands")
|
|
193
|
+
result = await client.get_products(request)
|
|
194
|
+
# Connection automatically closed on exit
|
|
195
|
+
|
|
196
|
+
# Multi-agent client also supports context managers
|
|
197
|
+
async with ADCPMultiAgentClient(agents) as client:
|
|
198
|
+
# Execute across all agents in parallel
|
|
199
|
+
results = await client.get_products(request)
|
|
200
|
+
# All agent connections closed automatically (even if some failed)
|
|
201
|
+
```
|
|
202
|
+
|
|
203
|
+
Manual cleanup is available for special cases (e.g., managing client lifecycle manually):
|
|
204
|
+
|
|
205
|
+
```python
|
|
206
|
+
# Use manual cleanup when you need fine-grained control over lifecycle
|
|
207
|
+
client = ADCPClient(config)
|
|
208
|
+
try:
|
|
209
|
+
result = await client.get_products(request)
|
|
210
|
+
finally:
|
|
211
|
+
await client.close() # Explicit cleanup
|
|
212
|
+
```
|
|
213
|
+
|
|
214
|
+
**When to use manual cleanup:**
|
|
215
|
+
- Managing client lifecycle across multiple functions
|
|
216
|
+
- Testing scenarios requiring explicit control
|
|
217
|
+
- Integration with frameworks that manage resources differently
|
|
218
|
+
|
|
219
|
+
In most cases, prefer the context manager pattern.
|
|
220
|
+
|
|
176
221
|
### Error Handling
|
|
177
222
|
|
|
178
223
|
The library provides a comprehensive exception hierarchy with helpful error messages:
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "adcp"
|
|
7
|
-
version = "1.0
|
|
7
|
+
version = "1.1.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"}
|
|
@@ -23,37 +23,38 @@ from adcp.types.core import AgentConfig, Protocol
|
|
|
23
23
|
|
|
24
24
|
def print_json(data: Any) -> None:
|
|
25
25
|
"""Print data as JSON."""
|
|
26
|
-
|
|
26
|
+
from pydantic import BaseModel
|
|
27
|
+
|
|
28
|
+
# Handle Pydantic models
|
|
29
|
+
if isinstance(data, BaseModel):
|
|
30
|
+
print(data.model_dump_json(indent=2, exclude_none=True))
|
|
31
|
+
else:
|
|
32
|
+
print(json.dumps(data, indent=2, default=str))
|
|
27
33
|
|
|
28
34
|
|
|
29
35
|
def print_result(result: Any, json_output: bool = False) -> None:
|
|
30
36
|
"""Print result in formatted or JSON mode."""
|
|
31
37
|
if json_output:
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
"metadata": result.metadata,
|
|
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
|
-
),
|
|
48
|
-
}
|
|
49
|
-
)
|
|
38
|
+
# Match JavaScript client: output just the data for scripting
|
|
39
|
+
if result.success and result.data:
|
|
40
|
+
print_json(result.data)
|
|
41
|
+
else:
|
|
42
|
+
# On error, output error info
|
|
43
|
+
print_json({"error": result.error, "success": False})
|
|
50
44
|
else:
|
|
51
|
-
|
|
45
|
+
# Pretty output with message and data (like JavaScript client)
|
|
52
46
|
if result.success:
|
|
47
|
+
print("\nSUCCESS\n")
|
|
48
|
+
# Show protocol message if available
|
|
49
|
+
if hasattr(result, "message") and result.message:
|
|
50
|
+
print("Protocol Message:")
|
|
51
|
+
print(result.message)
|
|
52
|
+
print()
|
|
53
53
|
if result.data:
|
|
54
|
-
print("
|
|
54
|
+
print("Response:")
|
|
55
55
|
print_json(result.data)
|
|
56
56
|
else:
|
|
57
|
+
print("\nFAILED\n")
|
|
57
58
|
print(f"Error: {result.error}")
|
|
58
59
|
|
|
59
60
|
|
|
@@ -37,6 +37,8 @@ from adcp.types.generated import (
|
|
|
37
37
|
ListCreativeFormatsResponse,
|
|
38
38
|
ListCreativesRequest,
|
|
39
39
|
ListCreativesResponse,
|
|
40
|
+
PreviewCreativeRequest,
|
|
41
|
+
PreviewCreativeResponse,
|
|
40
42
|
ProvidePerformanceFeedbackRequest,
|
|
41
43
|
ProvidePerformanceFeedbackResponse,
|
|
42
44
|
SyncCreativesRequest,
|
|
@@ -101,16 +103,31 @@ class ADCPClient:
|
|
|
101
103
|
async def get_products(
|
|
102
104
|
self,
|
|
103
105
|
request: GetProductsRequest,
|
|
106
|
+
fetch_previews: bool = False,
|
|
107
|
+
preview_output_format: str = "url",
|
|
108
|
+
creative_agent_client: ADCPClient | None = None,
|
|
104
109
|
) -> TaskResult[GetProductsResponse]:
|
|
105
110
|
"""
|
|
106
111
|
Get advertising products.
|
|
107
112
|
|
|
108
113
|
Args:
|
|
109
114
|
request: Request parameters
|
|
115
|
+
fetch_previews: If True, generate preview URLs for each product's formats
|
|
116
|
+
(uses batch API for 5-10x performance improvement)
|
|
117
|
+
preview_output_format: "url" for iframe URLs (default), "html" for direct
|
|
118
|
+
embedding (2-3x faster, no iframe overhead)
|
|
119
|
+
creative_agent_client: Client for creative agent (required if
|
|
120
|
+
fetch_previews=True)
|
|
110
121
|
|
|
111
122
|
Returns:
|
|
112
|
-
TaskResult containing GetProductsResponse
|
|
123
|
+
TaskResult containing GetProductsResponse with optional preview URLs in metadata
|
|
124
|
+
|
|
125
|
+
Raises:
|
|
126
|
+
ValueError: If fetch_previews=True but creative_agent_client is not provided
|
|
113
127
|
"""
|
|
128
|
+
if fetch_previews and not creative_agent_client:
|
|
129
|
+
raise ValueError("creative_agent_client is required when fetch_previews=True")
|
|
130
|
+
|
|
114
131
|
operation_id = create_operation_id()
|
|
115
132
|
params = request.model_dump(exclude_none=True)
|
|
116
133
|
|
|
@@ -137,20 +154,40 @@ class ADCPClient:
|
|
|
137
154
|
)
|
|
138
155
|
)
|
|
139
156
|
|
|
140
|
-
|
|
157
|
+
result = self.adapter._parse_response(raw_result, GetProductsResponse)
|
|
158
|
+
|
|
159
|
+
if fetch_previews and result.success and result.data and creative_agent_client:
|
|
160
|
+
from adcp.utils.preview_cache import add_preview_urls_to_products
|
|
161
|
+
|
|
162
|
+
products_with_previews = await add_preview_urls_to_products(
|
|
163
|
+
result.data.products,
|
|
164
|
+
creative_agent_client,
|
|
165
|
+
use_batch=True,
|
|
166
|
+
output_format=preview_output_format,
|
|
167
|
+
)
|
|
168
|
+
result.metadata = result.metadata or {}
|
|
169
|
+
result.metadata["products_with_previews"] = products_with_previews
|
|
170
|
+
|
|
171
|
+
return result
|
|
141
172
|
|
|
142
173
|
async def list_creative_formats(
|
|
143
174
|
self,
|
|
144
175
|
request: ListCreativeFormatsRequest,
|
|
176
|
+
fetch_previews: bool = False,
|
|
177
|
+
preview_output_format: str = "url",
|
|
145
178
|
) -> TaskResult[ListCreativeFormatsResponse]:
|
|
146
179
|
"""
|
|
147
180
|
List supported creative formats.
|
|
148
181
|
|
|
149
182
|
Args:
|
|
150
183
|
request: Request parameters
|
|
184
|
+
fetch_previews: If True, generate preview URLs for each format using
|
|
185
|
+
sample manifests (uses batch API for 5-10x performance improvement)
|
|
186
|
+
preview_output_format: "url" for iframe URLs (default), "html" for direct
|
|
187
|
+
embedding (2-3x faster, no iframe overhead)
|
|
151
188
|
|
|
152
189
|
Returns:
|
|
153
|
-
TaskResult containing ListCreativeFormatsResponse
|
|
190
|
+
TaskResult containing ListCreativeFormatsResponse with optional preview URLs in metadata
|
|
154
191
|
"""
|
|
155
192
|
operation_id = create_operation_id()
|
|
156
193
|
params = request.model_dump(exclude_none=True)
|
|
@@ -178,8 +215,62 @@ class ADCPClient:
|
|
|
178
215
|
)
|
|
179
216
|
)
|
|
180
217
|
|
|
181
|
-
|
|
182
|
-
|
|
218
|
+
result = self.adapter._parse_response(raw_result, ListCreativeFormatsResponse)
|
|
219
|
+
|
|
220
|
+
if fetch_previews and result.success and result.data:
|
|
221
|
+
from adcp.utils.preview_cache import add_preview_urls_to_formats
|
|
222
|
+
|
|
223
|
+
formats_with_previews = await add_preview_urls_to_formats(
|
|
224
|
+
result.data.formats,
|
|
225
|
+
self,
|
|
226
|
+
use_batch=True,
|
|
227
|
+
output_format=preview_output_format,
|
|
228
|
+
)
|
|
229
|
+
result.metadata = result.metadata or {}
|
|
230
|
+
result.metadata["formats_with_previews"] = formats_with_previews
|
|
231
|
+
|
|
232
|
+
return result
|
|
233
|
+
|
|
234
|
+
async def preview_creative(
|
|
235
|
+
self,
|
|
236
|
+
request: PreviewCreativeRequest,
|
|
237
|
+
) -> TaskResult[PreviewCreativeResponse]:
|
|
238
|
+
"""
|
|
239
|
+
Generate preview of a creative manifest.
|
|
240
|
+
|
|
241
|
+
Args:
|
|
242
|
+
request: Request parameters
|
|
243
|
+
|
|
244
|
+
Returns:
|
|
245
|
+
TaskResult containing PreviewCreativeResponse with preview URLs
|
|
246
|
+
"""
|
|
247
|
+
operation_id = create_operation_id()
|
|
248
|
+
params = request.model_dump(exclude_none=True)
|
|
249
|
+
|
|
250
|
+
self._emit_activity(
|
|
251
|
+
Activity(
|
|
252
|
+
type=ActivityType.PROTOCOL_REQUEST,
|
|
253
|
+
operation_id=operation_id,
|
|
254
|
+
agent_id=self.agent_config.id,
|
|
255
|
+
task_type="preview_creative",
|
|
256
|
+
timestamp=datetime.now(timezone.utc).isoformat(),
|
|
257
|
+
)
|
|
258
|
+
)
|
|
259
|
+
|
|
260
|
+
raw_result = await self.adapter.preview_creative(params) # type: ignore[attr-defined]
|
|
261
|
+
|
|
262
|
+
self._emit_activity(
|
|
263
|
+
Activity(
|
|
264
|
+
type=ActivityType.PROTOCOL_RESPONSE,
|
|
265
|
+
operation_id=operation_id,
|
|
266
|
+
agent_id=self.agent_config.id,
|
|
267
|
+
task_type="preview_creative",
|
|
268
|
+
status=raw_result.status,
|
|
269
|
+
timestamp=datetime.now(timezone.utc).isoformat(),
|
|
270
|
+
)
|
|
271
|
+
)
|
|
272
|
+
|
|
273
|
+
return self.adapter._parse_response(raw_result, PreviewCreativeResponse)
|
|
183
274
|
|
|
184
275
|
async def sync_creatives(
|
|
185
276
|
self,
|
|
@@ -244,6 +244,10 @@ class A2AAdapter(ProtocolAdapter):
|
|
|
244
244
|
"""Provide performance feedback."""
|
|
245
245
|
return await self._call_a2a_tool("provide_performance_feedback", params)
|
|
246
246
|
|
|
247
|
+
async def preview_creative(self, params: dict[str, Any]) -> TaskResult[Any]:
|
|
248
|
+
"""Generate preview URLs for a creative manifest."""
|
|
249
|
+
return await self._call_a2a_tool("preview_creative", params)
|
|
250
|
+
|
|
247
251
|
async def list_tools(self) -> list[str]:
|
|
248
252
|
"""
|
|
249
253
|
List available tools from A2A agent.
|
|
@@ -49,6 +49,7 @@ class ProtocolAdapter(ABC):
|
|
|
49
49
|
return TaskResult[T](
|
|
50
50
|
status=raw_result.status,
|
|
51
51
|
data=None,
|
|
52
|
+
message=raw_result.message,
|
|
52
53
|
success=False,
|
|
53
54
|
error=raw_result.error or "No data returned from adapter",
|
|
54
55
|
metadata=raw_result.metadata,
|
|
@@ -66,6 +67,7 @@ class ProtocolAdapter(ABC):
|
|
|
66
67
|
return TaskResult[T](
|
|
67
68
|
status=raw_result.status,
|
|
68
69
|
data=parsed_data,
|
|
70
|
+
message=raw_result.message, # Preserve human-readable message from protocol
|
|
69
71
|
success=raw_result.success,
|
|
70
72
|
error=raw_result.error,
|
|
71
73
|
metadata=raw_result.metadata,
|
|
@@ -76,6 +78,7 @@ class ProtocolAdapter(ABC):
|
|
|
76
78
|
return TaskResult[T](
|
|
77
79
|
status=TaskStatus.FAILED,
|
|
78
80
|
error=f"Failed to parse response: {e}",
|
|
81
|
+
message=raw_result.message,
|
|
79
82
|
success=False,
|
|
80
83
|
debug_info=raw_result.debug_info,
|
|
81
84
|
)
|
|
@@ -40,6 +40,39 @@ class MCPAdapter(ProtocolAdapter):
|
|
|
40
40
|
self._session: Any = None
|
|
41
41
|
self._exit_stack: Any = None
|
|
42
42
|
|
|
43
|
+
async def _cleanup_failed_connection(self, context: str) -> None:
|
|
44
|
+
"""
|
|
45
|
+
Clean up resources after a failed connection attempt.
|
|
46
|
+
|
|
47
|
+
This method handles cleanup without raising exceptions to avoid
|
|
48
|
+
masking the original connection error.
|
|
49
|
+
|
|
50
|
+
Args:
|
|
51
|
+
context: Description of the context for logging (e.g., "during connection attempt")
|
|
52
|
+
"""
|
|
53
|
+
if self._exit_stack is not None:
|
|
54
|
+
old_stack = self._exit_stack
|
|
55
|
+
self._exit_stack = None
|
|
56
|
+
self._session = None
|
|
57
|
+
try:
|
|
58
|
+
await old_stack.aclose()
|
|
59
|
+
except asyncio.CancelledError:
|
|
60
|
+
logger.debug(f"MCP session cleanup cancelled {context}")
|
|
61
|
+
except RuntimeError as cleanup_error:
|
|
62
|
+
# Known anyio task group cleanup issue
|
|
63
|
+
error_msg = str(cleanup_error).lower()
|
|
64
|
+
if "cancel scope" in error_msg or "async context" in error_msg:
|
|
65
|
+
logger.debug(f"Ignoring anyio cleanup error {context}: {cleanup_error}")
|
|
66
|
+
else:
|
|
67
|
+
logger.warning(
|
|
68
|
+
f"Unexpected RuntimeError during cleanup {context}: {cleanup_error}"
|
|
69
|
+
)
|
|
70
|
+
except Exception as cleanup_error:
|
|
71
|
+
# Log unexpected cleanup errors but don't raise to preserve original error
|
|
72
|
+
logger.warning(
|
|
73
|
+
f"Unexpected error during cleanup {context}: {cleanup_error}", exc_info=True
|
|
74
|
+
)
|
|
75
|
+
|
|
43
76
|
async def _get_session(self) -> ClientSession:
|
|
44
77
|
"""
|
|
45
78
|
Get or create MCP client session with URL fallback handling.
|
|
@@ -115,35 +148,8 @@ class MCPAdapter(ProtocolAdapter):
|
|
|
115
148
|
return self._session # type: ignore[no-any-return]
|
|
116
149
|
except Exception as e:
|
|
117
150
|
last_error = e
|
|
118
|
-
# Clean up the exit stack on failure to avoid
|
|
119
|
-
|
|
120
|
-
old_stack = self._exit_stack
|
|
121
|
-
self._exit_stack = None # Clear immediately to prevent reuse
|
|
122
|
-
self._session = None
|
|
123
|
-
try:
|
|
124
|
-
await old_stack.aclose()
|
|
125
|
-
except asyncio.CancelledError:
|
|
126
|
-
# Expected during shutdown
|
|
127
|
-
pass
|
|
128
|
-
except RuntimeError as cleanup_error:
|
|
129
|
-
# Known MCP SDK async cleanup issue
|
|
130
|
-
if (
|
|
131
|
-
"async context" in str(cleanup_error).lower()
|
|
132
|
-
or "cancel scope" in str(cleanup_error).lower()
|
|
133
|
-
):
|
|
134
|
-
logger.debug(
|
|
135
|
-
"Ignoring MCP SDK async context error during cleanup: "
|
|
136
|
-
f"{cleanup_error}"
|
|
137
|
-
)
|
|
138
|
-
else:
|
|
139
|
-
logger.warning(
|
|
140
|
-
f"Unexpected RuntimeError during cleanup: {cleanup_error}"
|
|
141
|
-
)
|
|
142
|
-
except Exception as cleanup_error:
|
|
143
|
-
# Unexpected cleanup errors should be logged
|
|
144
|
-
logger.warning(
|
|
145
|
-
f"Unexpected error during cleanup: {cleanup_error}", exc_info=True
|
|
146
|
-
)
|
|
151
|
+
# Clean up the exit stack on failure to avoid resource leaks
|
|
152
|
+
await self._cleanup_failed_connection("during connection attempt")
|
|
147
153
|
|
|
148
154
|
# If this isn't the last URL to try, create a new exit stack and continue
|
|
149
155
|
if url != urls_to_try[-1]:
|
|
@@ -239,25 +245,49 @@ class MCPAdapter(ProtocolAdapter):
|
|
|
239
245
|
# Call the tool using MCP client session
|
|
240
246
|
result = await session.call_tool(tool_name, params)
|
|
241
247
|
|
|
242
|
-
#
|
|
243
|
-
|
|
248
|
+
# This SDK requires MCP tools to return structuredContent
|
|
249
|
+
# The content field may contain human-readable messages but the actual
|
|
250
|
+
# response data must be in structuredContent
|
|
251
|
+
if not hasattr(result, "structuredContent") or result.structuredContent is None:
|
|
252
|
+
raise ValueError(
|
|
253
|
+
f"MCP tool {tool_name} did not return structuredContent. "
|
|
254
|
+
f"This SDK requires MCP tools to provide structured responses. "
|
|
255
|
+
f"Got content: {result.content if hasattr(result, 'content') else 'none'}"
|
|
256
|
+
)
|
|
257
|
+
|
|
258
|
+
# Extract the structured data (required)
|
|
259
|
+
data_to_return = result.structuredContent
|
|
260
|
+
|
|
261
|
+
# Extract human-readable message from content (optional)
|
|
262
|
+
# This is typically a status message like "Found 42 creative formats"
|
|
263
|
+
message_text = None
|
|
264
|
+
if hasattr(result, "content") and result.content:
|
|
265
|
+
# Serialize content using the same method used for backward compatibility
|
|
266
|
+
serialized_content = self._serialize_mcp_content(result.content)
|
|
267
|
+
if isinstance(serialized_content, list):
|
|
268
|
+
for item in serialized_content:
|
|
269
|
+
is_text = isinstance(item, dict) and item.get("type") == "text"
|
|
270
|
+
if is_text and item.get("text"):
|
|
271
|
+
message_text = item["text"]
|
|
272
|
+
break
|
|
244
273
|
|
|
245
274
|
if self.agent_config.debug and start_time:
|
|
246
275
|
duration_ms = (time.time() - start_time) * 1000
|
|
247
276
|
debug_info = DebugInfo(
|
|
248
277
|
request=debug_request,
|
|
249
278
|
response={
|
|
250
|
-
"
|
|
279
|
+
"data": data_to_return,
|
|
280
|
+
"message": message_text,
|
|
251
281
|
"is_error": result.isError if hasattr(result, "isError") else False,
|
|
252
282
|
},
|
|
253
283
|
duration_ms=duration_ms,
|
|
254
284
|
)
|
|
255
285
|
|
|
256
|
-
#
|
|
257
|
-
# For AdCP, we expect the data in the content
|
|
286
|
+
# Return both the structured data and the human-readable message
|
|
258
287
|
return TaskResult[Any](
|
|
259
288
|
status=TaskStatus.COMPLETED,
|
|
260
|
-
data=
|
|
289
|
+
data=data_to_return,
|
|
290
|
+
message=message_text,
|
|
261
291
|
success=True,
|
|
262
292
|
debug_info=debug_info,
|
|
263
293
|
)
|
|
@@ -317,6 +347,10 @@ class MCPAdapter(ProtocolAdapter):
|
|
|
317
347
|
"""Provide performance feedback."""
|
|
318
348
|
return await self._call_mcp_tool("provide_performance_feedback", params)
|
|
319
349
|
|
|
350
|
+
async def preview_creative(self, params: dict[str, Any]) -> TaskResult[Any]:
|
|
351
|
+
"""Generate preview URLs for a creative manifest."""
|
|
352
|
+
return await self._call_mcp_tool("preview_creative", params)
|
|
353
|
+
|
|
320
354
|
async def list_tools(self) -> list[str]:
|
|
321
355
|
"""List available tools from MCP agent."""
|
|
322
356
|
session = await self._get_session()
|
|
@@ -324,15 +358,5 @@ class MCPAdapter(ProtocolAdapter):
|
|
|
324
358
|
return [tool.name for tool in result.tools]
|
|
325
359
|
|
|
326
360
|
async def close(self) -> None:
|
|
327
|
-
"""Close the MCP session."""
|
|
328
|
-
|
|
329
|
-
old_stack = self._exit_stack
|
|
330
|
-
self._exit_stack = None
|
|
331
|
-
self._session = None
|
|
332
|
-
try:
|
|
333
|
-
await old_stack.aclose()
|
|
334
|
-
except (asyncio.CancelledError, RuntimeError):
|
|
335
|
-
# Cleanup errors during shutdown are expected
|
|
336
|
-
pass
|
|
337
|
-
except Exception as e:
|
|
338
|
-
logger.debug(f"Error during MCP session cleanup: {e}")
|
|
361
|
+
"""Close the MCP session and clean up resources."""
|
|
362
|
+
await self._cleanup_failed_connection("during close")
|
|
@@ -5,7 +5,7 @@ from __future__ import annotations
|
|
|
5
5
|
from enum import Enum
|
|
6
6
|
from typing import Any, Generic, Literal, TypeVar
|
|
7
7
|
|
|
8
|
-
from pydantic import BaseModel, Field, field_validator
|
|
8
|
+
from pydantic import BaseModel, ConfigDict, Field, field_validator
|
|
9
9
|
|
|
10
10
|
|
|
11
11
|
class Protocol(str, Enum):
|
|
@@ -125,8 +125,11 @@ class DebugInfo(BaseModel):
|
|
|
125
125
|
class TaskResult(BaseModel, Generic[T]):
|
|
126
126
|
"""Result from task execution."""
|
|
127
127
|
|
|
128
|
+
model_config = ConfigDict(arbitrary_types_allowed=True)
|
|
129
|
+
|
|
128
130
|
status: TaskStatus
|
|
129
131
|
data: T | None = None
|
|
132
|
+
message: str | None = None # Human-readable message from agent (e.g., MCP content text)
|
|
130
133
|
submitted: SubmittedInfo | None = None
|
|
131
134
|
needs_input: NeedsInputInfo | None = None
|
|
132
135
|
error: str | None = None
|
|
@@ -134,9 +137,6 @@ class TaskResult(BaseModel, Generic[T]):
|
|
|
134
137
|
metadata: dict[str, Any] | None = None
|
|
135
138
|
debug_info: DebugInfo | None = None
|
|
136
139
|
|
|
137
|
-
class Config:
|
|
138
|
-
arbitrary_types_allowed = True
|
|
139
|
-
|
|
140
140
|
|
|
141
141
|
class ActivityType(str, Enum):
|
|
142
142
|
"""Types of activity events."""
|