Skip to content

Commit fd703a6

Browse files
claudebokelley
authored andcommitted
fix(webhooks): correct type annotations for webhook helpers
extract_webhook_result_data declared AdcpAsyncResponseData | None but every code path returned dict[str, Any] | None, forcing adopters to cast before using .get(). Return type corrected to dict[str, Any] | None and internal cast calls updated accordingly. create_mcp_webhook_payload and create_a2a_webhook_payload typed result narrowly as AdcpAsyncResponseData but accepted any BaseModel via hasattr check at runtime. Parameter widened to PydanticBaseModel | dict[str, Any] so callers pass any Pydantic model without type: ignore. Also drops the direct generated_poc import (CLAUDE.md layering rule) since AdcpAsyncResponseData is no longer referenced in this module, and fixes a bogus message= kwarg in the create_a2a_webhook_payload docstring example. https://claude.ai/code/session_013oiysa4CAeiSFFdnvFTHqh
1 parent 37d2cda commit fd703a6

1 file changed

Lines changed: 16 additions & 15 deletions

File tree

src/adcp/webhooks.py

Lines changed: 16 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@
4040
)
4141
from google.protobuf.json_format import MessageToDict, ParseDict
4242
from google.protobuf.struct_pb2 import Value
43+
from pydantic import BaseModel as PydanticBaseModel
4344

4445
from adcp.server.idempotency.backends import MemoryBackend as MemoryBackend
4546
from adcp.server.idempotency.webhook_dedup import WebhookDedupStore as WebhookDedupStore
@@ -57,7 +58,6 @@
5758
)
5859
from adcp.types import GeneratedTaskStatus
5960
from adcp.types.base import AdCPBaseModel
60-
from adcp.types.generated_poc.core.async_response_data import AdcpAsyncResponseData
6161
from adcp.webhook_receiver import (
6262
LegacyHmacFallback,
6363
VerifiedSignerLike,
@@ -86,7 +86,7 @@ def generate_webhook_idempotency_key() -> str:
8686
def create_mcp_webhook_payload(
8787
task_id: str,
8888
status: GeneratedTaskStatus | str,
89-
result: AdcpAsyncResponseData | dict[str, Any] | None = None,
89+
result: PydanticBaseModel | dict[str, Any] | None = None,
9090
timestamp: datetime | None = None,
9191
task_type: str | None = None,
9292
operation_id: str | None = None,
@@ -358,7 +358,7 @@ def sign_legacy_webhook(
358358
return signature_headers, body_bytes
359359

360360

361-
def extract_webhook_result_data(webhook_payload: dict[str, Any]) -> AdcpAsyncResponseData | None:
361+
def extract_webhook_result_data(webhook_payload: dict[str, Any]) -> dict[str, Any] | None:
362362
"""
363363
Extract result data from webhook payload (MCP or A2A format).
364364
@@ -376,8 +376,8 @@ def extract_webhook_result_data(webhook_payload: dict[str, Any]) -> AdcpAsyncRes
376376
webhook_payload: Raw webhook dictionary from HTTP request (JSON-deserialized)
377377
378378
Returns:
379-
AdcpAsyncResponseData union type containing the extracted AdCP response, or None
380-
if no result present. For A2A webhooks, unwraps data from artifacts/message parts
379+
dict[str, Any] containing the extracted AdCP response data, or None if no
380+
result is present. For A2A webhooks, unwraps data from artifacts/message parts
381381
structure. For MCP webhooks, returns the result field directly.
382382
383383
Examples:
@@ -464,8 +464,8 @@ def extract_webhook_result_data(webhook_payload: dict[str, Any]) -> AdcpAsyncRes
464464
data = part["data"]
465465
# Unwrap {"response": {...}} wrapper if present (A2A convention)
466466
if isinstance(data, dict) and "response" in data and len(data) == 1:
467-
return cast(AdcpAsyncResponseData, data["response"])
468-
return cast(AdcpAsyncResponseData, data)
467+
return cast(dict[str, Any], data["response"])
468+
return cast(dict[str, Any], data)
469469

470470
return None
471471

@@ -485,20 +485,20 @@ def extract_webhook_result_data(webhook_payload: dict[str, Any]) -> AdcpAsyncRes
485485
data = part["data"]
486486
# Unwrap {"response": {...}} wrapper if present
487487
if isinstance(data, dict) and "response" in data and len(data) == 1:
488-
return cast(AdcpAsyncResponseData, data["response"])
489-
return cast(AdcpAsyncResponseData, data)
488+
return cast(dict[str, Any], data["response"])
489+
return cast(dict[str, Any], data)
490490

491491
return None
492492

493493
# MCP format: result field directly
494-
return cast(AdcpAsyncResponseData | None, webhook_payload.get("result"))
494+
return cast(dict[str, Any] | None, webhook_payload.get("result"))
495495

496496

497497
def create_a2a_webhook_payload(
498498
task_id: str,
499499
status: GeneratedTaskStatus,
500500
context_id: str,
501-
result: AdcpAsyncResponseData | dict[str, Any],
501+
result: PydanticBaseModel | dict[str, Any],
502502
timestamp: datetime | None = None,
503503
) -> Task | TaskStatusUpdateEvent:
504504
"""
@@ -517,7 +517,7 @@ def create_a2a_webhook_payload(
517517
status: Current task status
518518
context_id: Session/conversation identifier (required by A2A protocol)
519519
timestamp: When the webhook was generated (defaults to current UTC time)
520-
result: Task-specific payload (AdCP response data)
520+
result: Task-specific payload — any Pydantic model or plain dict
521521
522522
Returns:
523523
Task object for terminated statuses, TaskStatusUpdateEvent for intermediate statuses
@@ -529,17 +529,18 @@ def create_a2a_webhook_payload(
529529
>>>
530530
>>> task = create_a2a_webhook_payload(
531531
... task_id="task_123",
532+
... context_id="ctx_123",
532533
... status=GeneratedTaskStatus.completed,
533534
... result={"products": [...]},
534-
... message="Found 5 products"
535535
... )
536536
>>> # task is a Task object with artifacts containing the result
537537
538538
Create a working status update:
539539
>>> event = create_a2a_webhook_payload(
540540
... task_id="task_456",
541+
... context_id="ctx_456",
541542
... status=GeneratedTaskStatus.working,
542-
... message="Processing 3 of 10 items"
543+
... result={"current_step": "processing", "percentage": 30},
543544
... )
544545
>>> # event is a TaskStatusUpdateEvent with status.message
545546
@@ -590,7 +591,7 @@ def create_a2a_webhook_payload(
590591
# Build parts for the message/artifact.
591592
parts: list[pb.Part] = []
592593

593-
# Convert AdcpAsyncResponseData to dict if it's a Pydantic model
594+
# Convert Pydantic model to dict if needed
594595
if hasattr(result, "model_dump"):
595596
result_dict: dict[str, Any] = result.model_dump(mode="json")
596597
else:

0 commit comments

Comments
 (0)