feat(payments): Add LangGraph integration for payment handling#546
feat(payments): Add LangGraph integration for payment handling#546ragsu43 wants to merge 4 commits into
Conversation
|
|
||
|
|
||
| @dataclass | ||
| class PaymentErrorContext: |
There was a problem hiding this comment.
We already have error defined in the common package. Let's use the existing error class.
There was a problem hiding this comment.
Given how the callback function would be implemented by developers without PaymentErrorContext, it seems to me that passing in a context object with the exception info and all necessary state/context fields for error handling gives the simplest dev experience. Based on what I have found, there are some big blockers against deprecating PaymentErrorContext in favor of just passing in the exception:
there are 4 main fields that langgraph devs cannot access from the callback unless it is passed into the callback:
- Retry state (number of retries on this tool call):
- can't track it directly in the config class because dev does not have access to "request" (field from wrap_tool_call), so no way to know what tool-name the retry is for unless we pass that in, so either way we would need to pass something into the callback.
- As mentioned above, middleware in langgraph doesn't receive graph state at all (which is the only equivalent of agent.state in strands), so even if we could add a custom field to the graph's State dict, the middleware can't read/write to it like "agent.state" in Strands.
- Only option is developer tracks it in some closure dict, but again we would need to pass in tool_call_id, in which case just having retry_count in the context object is much easier for the dev.
- tool_name:
- as mentioned above can't be accessed by dev if we deprecate PaymentErrorContext unless we pass it in anyway, since the developer has no access to the tool call "request" itself.
-
tool_args: same problem as tool_name... lives in ToolCallRequest inside wrap_tool_call with no external visibility WITHOUT some arg passing into the callback
-
payment_required_request (the parsed 402 body):
- only exists as a local var in wrap_tool_call, developer doesn't see the 402 response from callback function so they can't parse it manually either so we would have to pass this in.
Based on these constraints, it seems to me that keeping the context object and passing it into the callback is the best option (developer just accesses everything needed for state in one object), but if anything seems off or something I haven't considered please let me know!
Merge AgentCorePaymentsConfig (LangGraph) and AgentCorePaymentsPluginConfig (Strands) into a single dataclass in integrations/config.py. Both names remain available as aliases for backward compatibility.
… of in langgraph-specific package
Codecov Report❌ Patch coverage is Additional details and impacted files@@ Coverage Diff @@
## main #546 +/- ##
=======================================
Coverage ? 88.64%
=======================================
Files ? 103
Lines ? 9067
Branches ? 1378
=======================================
Hits ? 8037
Misses ? 663
Partials ? 367
Flags with carried forward coverage won't be shown. Click here to find out more. ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
|
|
||
| if TYPE_CHECKING: | ||
| from .config import AgentCorePaymentsConfig | ||
|
|
There was a problem hiding this comment.
Wrong import path under TYPE_CHECKING
This imports from .config (i.e. langgraph/config.py) but the config class lives at integrations/config.py. Should be:
from ..config import AgentCorePaymentsConfigWon't crash at runtime (guarded by TYPE_CHECKING), but will break mypy/pyright.
| payment_tool_allowlist: Optional[List[str]] = None | ||
| provide_http_request: bool = True | ||
| post_payment_retry_delay_seconds: float = 3.0 | ||
| max_interrupt_retries: int = 5 |
There was a problem hiding this comment.
Type annotation contradicts validation
The field is typed Dict[str, Any] but validation at __post_init__ enforces isinstance(v, PaymentResponseHandler) for all values. Consider:
custom_handlers: Optional[Dict[str, "PaymentResponseHandler"]] = field(default=None)This way the type signature matches the runtime contract and callers get proper type-checking.
| def apply_payment_header(self, tool_input, payment_header): | ||
| self.inject_called = True | ||
| return super().apply_payment_header(tool_input, payment_header) | ||
|
|
There was a problem hiding this comment.
Duplicate test block (copy-paste leftover)
Lines below this point duplicate the test_custom_handler_full_flow test verbatim — the TrackingHandler class, middleware setup, and assertions are repeated inside this method body. Appears to be a copy-paste accident. The duplicate code still runs (appended to this method), but adds confusion and redundant test time. Safe to remove.
| delay between signing and retry lets the chain advance one block so | ||
| the authorization is valid by the time the seller submits. Defaults | ||
| to 3.0 seconds (about one Base Sepolia block). Set to 0 to disable. | ||
| payment_connector_id: Payment connector ID (optional). |
There was a problem hiding this comment.
Is this a new attribute? A manager can have many connectiors. What use case this attribute is going to solve?
| payment_connector_id=self.config.payment_connector_id, | ||
| ) | ||
|
|
||
| def _create_auto_session(self) -> None: |
There was a problem hiding this comment.
Why are we creating autoSession? Agent may not have permission to create session. Creating session in agent will be risky as they can bypass the payments limit (budget) check.
| if retry_prepared is not None: | ||
| from bedrock_agentcore.payments.integrations.handlers import GenericPaymentHandler as _GH | ||
| _retry_handler = _GH() | ||
| retry_status = _retry_handler.extract_status_code(retry_prepared) | ||
| if retry_status != 402: | ||
| retry_fallback = self._fallback_detect_402(retry_result.content) | ||
| if retry_fallback is not None: | ||
| retry_status = 402 | ||
| _retry_handler = _FallbackHandler(retry_fallback) | ||
| if retry_status == 402: | ||
| retry_body = _retry_handler.extract_body(retry_prepared) or {} | ||
| error_detail = ( | ||
| retry_body.get("error", "unknown error") | ||
| if isinstance(retry_body, dict) | ||
| else "unknown error" | ||
| ) | ||
| return self._error_tool_message( | ||
| request, | ||
| PaymentError( | ||
| f"Payment was signed but rejected by the server ({error_detail})." | ||
| ), | ||
| ) |
There was a problem hiding this comment.
Duplicate code block. Can you make sure to remove duplicate code for the sake of maintainability?
Description of changes:
Added LangGraph middleware and config files for developer integration with ACP.