Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions ag2-agents/payment-approval/.env.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
OPENAI_API_KEY=your-openai-api-key-here

# Optional
LLM_MODEL=gpt-4o-mini
OPENAI_BASE_URL=https://api.openai.com/v1
AGENT_PORT=8009
A2A_PORT=9998
AGENT_SEED=
46 changes: 46 additions & 0 deletions ag2-agents/payment-approval/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
# AG2 Payment Approval Agent

![ag2](https://img.shields.io/badge/ag2-00ADD8) ![uagents](https://img.shields.io/badge/uagents-4A90E2) ![a2a](https://img.shields.io/badge/a2a-000000) ![innovationlab](https://img.shields.io/badge/innovationlab-3D8BD3)

A two-agent payment approval workflow using [AG2](https://github.com/ag2ai/ag2) (formerly AutoGen)
integrated with the Fetch.ai uAgents ecosystem via the A2A protocol and payment protocol.

## Architecture

```
Buyer agent / ASI:One / other uAgent
SingleA2AAdapter (port 8009) → Agentverse
PaymentApprovalExecutor (A2A AgentExecutor)
AG2 Two-Agent Chat
├── researcher — investigates recipient, produces risk assessment
└── payment_executor — formats assessment, returns APPROVED/REJECTED verdict
Payment Protocol (via adapter)
├── APPROVED → buyer receives RequestPayment → CommitPayment / RejectPayment
└── REJECTED → buyer receives assessment text explaining why
```

## Prerequisites

- **Python 3.10–3.13** (uagents depends on Pydantic v1, which is incompatible with Python 3.14+)

## Quick Start

```bash
cd ag2-agents/payment-approval
pip install -r requirements.txt
cp .env.example .env # add OPENAI_API_KEY
python main.py
```

## AG2 Features Demonstrated

- **Two-agent `initiate_chat`** — researcher hands off to executor via the natural
conversation flow; the shared message history carries the assessment
- **`A2A AgentExecutor`** — same integration pattern used by other examples in this repo
- **Payment protocol integration** — `SingleA2AAdapter` bridges the assessment result
into Fetch.ai's `AgentPaymentProtocol`, enabling buyer agents to commit or reject
payments via standard protocol messages
79 changes: 79 additions & 0 deletions ag2-agents/payment-approval/agent_executor.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
"""
Wraps the AG2 payment assessment workflow as an A2A AgentExecutor
for use with SingleA2AAdapter.
"""
import re

from a2a.server.agent_execution import AgentExecutor, RequestContext
from a2a.server.events import EventQueue
from a2a.types import Part, TextPart
from a2a.utils import new_agent_text_message
from autogen import LLMConfig
from typing_extensions import override

from workflow import run_payment_assessment

# Pattern: "50 USDC to alice.fetch — reason: 'research report delivery'"
_PAYMENT_RE = re.compile(
r"(?P<amount>[\d.]+)\s+(?P<currency>\w+)\s+to\s+(?P<recipient>\S+)"
r"(?:\s*[—\-]+\s*reason:\s*['\"]?(?P<reason>[^'\"]+)['\"]?)?",
re.IGNORECASE,
)


def _parse_payment_request(text: str) -> tuple[str, float, str, str]:
"""Extract (recipient, amount, reason, currency) from free-text message.

Falls back to passing the entire message as the reason if parsing fails.
"""
m = _PAYMENT_RE.search(text)
if m:
return (
m.group("recipient"),
float(m.group("amount")),
m.group("reason") or "not specified",
m.group("currency"),
)
# Fallback: let the LLM agents figure it out
return ("unknown", 0.0, text, "USDC")


class PaymentApprovalExecutor(AgentExecutor):
"""A2A-compatible executor wrapping the AG2 payment assessment workflow."""

def __init__(self, llm_config: LLMConfig):
self.llm_config = llm_config

@override
async def execute(self, context: RequestContext, event_queue: EventQueue) -> None:
message_content = ""
for part in context.message.parts:
if isinstance(part, Part):
if isinstance(part.root, TextPart):
message_content = part.root.text
break

if not message_content:
await event_queue.enqueue_event(
new_agent_text_message("Error: No message content received.")
)
return

try:
recipient, amount, reason, currency = _parse_payment_request(message_content)
result = await run_payment_assessment(
recipient=recipient,
amount=amount,
reason=reason,
llm_config=self.llm_config,
currency=currency,
)
await event_queue.enqueue_event(new_agent_text_message(result))
except Exception as e:
await event_queue.enqueue_event(
new_agent_text_message(f"Payment assessment failed: {e}")
)

@override
async def cancel(self, context: RequestContext, event_queue: EventQueue) -> None:
raise Exception("Cancel not supported for this agent executor.")
36 changes: 36 additions & 0 deletions ag2-agents/payment-approval/agents.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
"""
AG2 payment approval agents.

researcher — investigates recipient, produces risk assessment
executor — formats assessment, decides proceed/reject (no human stdin in uAgent mode)
"""
from autogen import ConversableAgent, LLMConfig


def build_agents(llm_config: LLMConfig) -> tuple[ConversableAgent, ConversableAgent]:
researcher = ConversableAgent(
name="researcher",
system_message=(
"You are a payment risk analyst. Investigate the payment recipient using available "
"tools: check their Fetch.ai address history, reputation, and any known flags. "
"Produce a concise risk assessment with a clear recommendation (proceed / do not proceed). "
"End your assessment with ASSESSMENT COMPLETE."
),
llm_config=llm_config,
)

executor = ConversableAgent(
name="payment_executor",
system_message=(
"You handle payment execution. Present the researcher's risk assessment clearly, "
"state the exact payment details (recipient, amount, reason), then give a final "
"verdict: APPROVED or REJECTED based on the risk assessment.\n\n"
"If approved, end with: PAYMENT APPROVED. TERMINATE\n"
"If rejected, end with: PAYMENT REJECTED. TERMINATE"
),
llm_config=llm_config,
human_input_mode="NEVER",
is_termination_msg=lambda m: "TERMINATE" in (m.get("content") or ""),
)

return researcher, executor
57 changes: 57 additions & 0 deletions ag2-agents/payment-approval/main.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
"""
AG2 two-agent payment approval workflow, exposed via A2A protocol.

On each request, two AG2 ConversableAgents (researcher → payment_executor)
are created in agents.py and orchestrated by workflow.py to produce a risk
assessment. The result is served through SingleA2AAdapter, making the AG2
workflow discoverable on Agentverse and callable from ASI:One or other
agents in the ecosystem.

Requires Python ≤3.13 (uagents depends on Pydantic v1, incompatible with 3.14+).
"""
import sys

if sys.version_info >= (3, 14):
raise RuntimeError(
"uagents requires Python ≤3.13 (Pydantic v1 is incompatible with 3.14+). "
"Please use Python 3.10–3.13."
)

import os
from dotenv import load_dotenv
from uagents_adapter import SingleA2AAdapter
from autogen import LLMConfig

from agent_executor import PaymentApprovalExecutor

load_dotenv()

llm_config = LLMConfig(
{
"model": os.getenv("LLM_MODEL", "gpt-4o-mini"),
"api_key": os.getenv("OPENAI_API_KEY", ""),
"base_url": os.getenv("OPENAI_BASE_URL", "https://api.openai.com/v1"),
},
temperature=0.2,
cache_seed=None,
)

executor = PaymentApprovalExecutor(llm_config=llm_config)

adapter = SingleA2AAdapter(
agent_executor=executor,
name="AG2 Payment Approval Agent",
description=(
"Two-agent payment approval workflow using AG2 (formerly AutoGen). "
"A researcher investigates the recipient and a payment executor "
"produces a risk assessment with an APPROVED/REJECTED verdict. "
"Supports Fetch.ai payment protocol for commit/reject flows."
),
port=int(os.getenv("AGENT_PORT", "8009")),
a2a_port=int(os.getenv("A2A_PORT", "9998")),
mailbox=True,
seed=os.getenv("AGENT_SEED"),
)

if __name__ == "__main__":
adapter.run()
5 changes: 5 additions & 0 deletions ag2-agents/payment-approval/requirements.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
ag2[openai]>=0.11.0
a2a-sdk
uagents>=0.20.0
uagents-adapter>=0.4.0
python-dotenv>=1.0.0
17 changes: 17 additions & 0 deletions ag2-agents/payment-approval/tests/conftest.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
"""
Ensure payment-approval/ is on sys.path so local modules are importable.
"""
import sys
import os

parent = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
if parent not in sys.path:
sys.path.insert(0, parent)

# Evict the installed 'agents' package (OpenAI Agents SDK) from sys.modules
# so 'from agents import build_agents' finds our local agents.py instead.
for key in list(sys.modules.keys()):
if key == "agents" or key.startswith("agents."):
mod = sys.modules[key]
if hasattr(mod, "__file__") and mod.__file__ and parent not in (mod.__file__ or ""):
del sys.modules[key]
Loading
Loading