traia-iatp 0.1.2__py3-none-any.whl → 0.1.67__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.
- traia_iatp/__init__.py +105 -8
- traia_iatp/cli/main.py +85 -1
- traia_iatp/client/__init__.py +28 -3
- traia_iatp/client/crewai_a2a_tools.py +32 -12
- traia_iatp/client/d402_a2a_client.py +348 -0
- traia_iatp/contracts/__init__.py +11 -0
- traia_iatp/contracts/data/abis/contract-abis-localhost.json +4091 -0
- traia_iatp/contracts/data/abis/contract-abis-sepolia.json +4890 -0
- traia_iatp/contracts/data/addresses/contract-addresses.json +17 -0
- traia_iatp/contracts/data/addresses/contract-proxies.json +12 -0
- traia_iatp/contracts/iatp_contracts_config.py +263 -0
- traia_iatp/contracts/wallet_creator.py +369 -0
- traia_iatp/core/models.py +17 -3
- traia_iatp/d402/MIDDLEWARE_ARCHITECTURE.md +205 -0
- traia_iatp/d402/PRICE_BUILDER_USAGE.md +249 -0
- traia_iatp/d402/README.md +489 -0
- traia_iatp/d402/__init__.py +54 -0
- traia_iatp/d402/asgi_wrapper.py +469 -0
- traia_iatp/d402/chains.py +102 -0
- traia_iatp/d402/client.py +150 -0
- traia_iatp/d402/clients/__init__.py +7 -0
- traia_iatp/d402/clients/base.py +218 -0
- traia_iatp/d402/clients/httpx.py +266 -0
- traia_iatp/d402/common.py +114 -0
- traia_iatp/d402/encoding.py +28 -0
- traia_iatp/d402/examples/client_example.py +197 -0
- traia_iatp/d402/examples/server_example.py +171 -0
- traia_iatp/d402/facilitator.py +481 -0
- traia_iatp/d402/mcp_middleware.py +296 -0
- traia_iatp/d402/models.py +116 -0
- traia_iatp/d402/networks.py +98 -0
- traia_iatp/d402/path.py +43 -0
- traia_iatp/d402/payment_introspection.py +126 -0
- traia_iatp/d402/payment_signing.py +183 -0
- traia_iatp/d402/price_builder.py +164 -0
- traia_iatp/d402/servers/__init__.py +61 -0
- traia_iatp/d402/servers/base.py +139 -0
- traia_iatp/d402/servers/example_general_server.py +140 -0
- traia_iatp/d402/servers/fastapi.py +253 -0
- traia_iatp/d402/servers/mcp.py +304 -0
- traia_iatp/d402/servers/starlette.py +878 -0
- traia_iatp/d402/starlette_middleware.py +529 -0
- traia_iatp/d402/types.py +300 -0
- traia_iatp/mcp/D402_MCP_ADAPTER_FLOW.md +357 -0
- traia_iatp/mcp/__init__.py +3 -0
- traia_iatp/mcp/d402_mcp_tool_adapter.py +526 -0
- traia_iatp/mcp/mcp_agent_template.py +78 -13
- traia_iatp/mcp/templates/Dockerfile.j2 +27 -4
- traia_iatp/mcp/templates/README.md.j2 +104 -8
- traia_iatp/mcp/templates/cursor-rules.md.j2 +194 -0
- traia_iatp/mcp/templates/deployment_params.json.j2 +1 -2
- traia_iatp/mcp/templates/docker-compose.yml.j2 +13 -3
- traia_iatp/mcp/templates/env.example.j2 +60 -0
- traia_iatp/mcp/templates/mcp_health_check.py.j2 +2 -2
- traia_iatp/mcp/templates/pyproject.toml.j2 +11 -5
- traia_iatp/mcp/templates/pyrightconfig.json.j2 +22 -0
- traia_iatp/mcp/templates/run_local_docker.sh.j2 +320 -10
- traia_iatp/mcp/templates/server.py.j2 +174 -197
- traia_iatp/mcp/traia_mcp_adapter.py +182 -20
- traia_iatp/registry/__init__.py +47 -12
- traia_iatp/registry/atlas_search_indexes.json +108 -54
- traia_iatp/registry/iatp_search_api.py +169 -39
- traia_iatp/registry/mongodb_registry.py +241 -69
- traia_iatp/registry/readmes/EMBEDDINGS_SETUP.md +1 -1
- traia_iatp/registry/readmes/IATP_SEARCH_API_GUIDE.md +8 -8
- traia_iatp/registry/readmes/MONGODB_X509_AUTH.md +1 -1
- traia_iatp/registry/readmes/README.md +3 -3
- traia_iatp/registry/readmes/REFACTORING_SUMMARY.md +6 -6
- traia_iatp/scripts/__init__.py +2 -0
- traia_iatp/scripts/create_wallet.py +244 -0
- traia_iatp/server/a2a_server.py +22 -7
- traia_iatp/server/iatp_server_template_generator.py +23 -0
- traia_iatp/server/templates/.dockerignore.j2 +48 -0
- traia_iatp/server/templates/Dockerfile.j2 +23 -1
- traia_iatp/server/templates/README.md +2 -2
- traia_iatp/server/templates/README.md.j2 +5 -5
- traia_iatp/server/templates/__main__.py.j2 +374 -66
- traia_iatp/server/templates/agent.py.j2 +12 -11
- traia_iatp/server/templates/agent_config.json.j2 +3 -3
- traia_iatp/server/templates/agent_executor.py.j2 +45 -27
- traia_iatp/server/templates/env.example.j2 +32 -4
- traia_iatp/server/templates/gitignore.j2 +7 -0
- traia_iatp/server/templates/pyproject.toml.j2 +13 -12
- traia_iatp/server/templates/run_local_docker.sh.j2 +143 -11
- traia_iatp/server/templates/server.py.j2 +197 -10
- traia_iatp/special_agencies/registry_search_agency.py +1 -1
- traia_iatp/utils/iatp_utils.py +6 -6
- traia_iatp-0.1.67.dist-info/METADATA +320 -0
- traia_iatp-0.1.67.dist-info/RECORD +117 -0
- traia_iatp-0.1.2.dist-info/METADATA +0 -414
- traia_iatp-0.1.2.dist-info/RECORD +0 -72
- {traia_iatp-0.1.2.dist-info → traia_iatp-0.1.67.dist-info}/WHEEL +0 -0
- {traia_iatp-0.1.2.dist-info → traia_iatp-0.1.67.dist-info}/entry_points.txt +0 -0
- {traia_iatp-0.1.2.dist-info → traia_iatp-0.1.67.dist-info}/licenses/LICENSE +0 -0
- {traia_iatp-0.1.2.dist-info → traia_iatp-0.1.67.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
from decimal import Decimal
|
|
2
|
+
from typing import List, Optional
|
|
3
|
+
|
|
4
|
+
from .chains import (
|
|
5
|
+
get_chain_id,
|
|
6
|
+
get_token_decimals,
|
|
7
|
+
get_token_name,
|
|
8
|
+
get_token_version,
|
|
9
|
+
get_default_token_address,
|
|
10
|
+
)
|
|
11
|
+
from .types import Price, TokenAmount, PaymentRequirements, PaymentPayload
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def parse_money(amount: str | int, address: str, network: str) -> int:
|
|
15
|
+
"""Parse money string or int into int
|
|
16
|
+
|
|
17
|
+
Params:
|
|
18
|
+
amount: str | int - if int, should be the full amount including token specific decimals
|
|
19
|
+
"""
|
|
20
|
+
if isinstance(amount, str):
|
|
21
|
+
if amount.startswith("$"):
|
|
22
|
+
amount = amount[1:]
|
|
23
|
+
decimal_amount = Decimal(amount)
|
|
24
|
+
|
|
25
|
+
chain_id = get_chain_id(network)
|
|
26
|
+
decimals = get_token_decimals(chain_id, address)
|
|
27
|
+
decimal_amount = decimal_amount * Decimal(10**decimals)
|
|
28
|
+
return int(decimal_amount)
|
|
29
|
+
return amount
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def process_price_to_atomic_amount(
|
|
33
|
+
price: Price, network: str
|
|
34
|
+
) -> tuple[str, str, dict[str, str]]:
|
|
35
|
+
"""Process a Price into atomic amount, asset address, and EIP-712 domain info
|
|
36
|
+
|
|
37
|
+
Args:
|
|
38
|
+
price: Either Money (USD string/int) or TokenAmount
|
|
39
|
+
network: Network identifier
|
|
40
|
+
|
|
41
|
+
Returns:
|
|
42
|
+
Tuple of (max_amount_required, asset_address, eip712_domain)
|
|
43
|
+
|
|
44
|
+
Raises:
|
|
45
|
+
ValueError: If price format is invalid
|
|
46
|
+
"""
|
|
47
|
+
if isinstance(price, (str, int)):
|
|
48
|
+
# Money type - convert USD to USDC atomic units
|
|
49
|
+
try:
|
|
50
|
+
if isinstance(price, str) and price.startswith("$"):
|
|
51
|
+
price = price[1:]
|
|
52
|
+
amount = Decimal(str(price))
|
|
53
|
+
|
|
54
|
+
# Get USDC address for the network
|
|
55
|
+
chain_id = get_chain_id(network)
|
|
56
|
+
asset_address = get_usdc_address(chain_id)
|
|
57
|
+
decimals = get_token_decimals(chain_id, asset_address)
|
|
58
|
+
|
|
59
|
+
# Convert to atomic units
|
|
60
|
+
atomic_amount = int(amount * Decimal(10**decimals))
|
|
61
|
+
|
|
62
|
+
# Get EIP-712 domain info
|
|
63
|
+
eip712_domain = {
|
|
64
|
+
"name": get_token_name(chain_id, asset_address),
|
|
65
|
+
"version": get_token_version(chain_id, asset_address),
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
return str(atomic_amount), asset_address, eip712_domain
|
|
69
|
+
|
|
70
|
+
except (ValueError, KeyError) as e:
|
|
71
|
+
raise ValueError(f"Invalid price format: {price}. Error: {e}")
|
|
72
|
+
|
|
73
|
+
elif isinstance(price, TokenAmount):
|
|
74
|
+
# TokenAmount type - already in atomic units with asset info
|
|
75
|
+
return (
|
|
76
|
+
price.amount,
|
|
77
|
+
price.asset.address,
|
|
78
|
+
{
|
|
79
|
+
"name": price.asset.eip712.name,
|
|
80
|
+
"version": price.asset.eip712.version,
|
|
81
|
+
},
|
|
82
|
+
)
|
|
83
|
+
|
|
84
|
+
else:
|
|
85
|
+
raise ValueError(f"Invalid price type: {type(price)}")
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def get_usdc_address(chain_id: int | str) -> str:
|
|
89
|
+
"""Get the USDC contract address for a given chain ID"""
|
|
90
|
+
chain_id_str = str(chain_id) # Convert to string for consistency
|
|
91
|
+
return get_default_token_address(chain_id_str, "usdc")
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def find_matching_payment_requirements(
|
|
95
|
+
payment_requirements: List[PaymentRequirements],
|
|
96
|
+
payment: PaymentPayload,
|
|
97
|
+
) -> Optional[PaymentRequirements]:
|
|
98
|
+
"""
|
|
99
|
+
Finds the matching payment requirements for the given payment.
|
|
100
|
+
|
|
101
|
+
Args:
|
|
102
|
+
payment_requirements: The payment requirements to search through
|
|
103
|
+
payment: The payment to match against
|
|
104
|
+
|
|
105
|
+
Returns:
|
|
106
|
+
The matching payment requirements or None if no match is found
|
|
107
|
+
"""
|
|
108
|
+
for req in payment_requirements:
|
|
109
|
+
if req.scheme == payment.scheme and req.network == payment.network:
|
|
110
|
+
return req
|
|
111
|
+
return None
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
d402_VERSION = 1
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import base64
|
|
2
|
+
from typing import Union
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
def safe_base64_encode(data: Union[str, bytes]) -> str:
|
|
6
|
+
"""Safely encode string or bytes to base64 string.
|
|
7
|
+
|
|
8
|
+
Args:
|
|
9
|
+
data: String or bytes to encode
|
|
10
|
+
|
|
11
|
+
Returns:
|
|
12
|
+
Base64 encoded string
|
|
13
|
+
"""
|
|
14
|
+
if isinstance(data, str):
|
|
15
|
+
data = data.encode("utf-8")
|
|
16
|
+
return base64.b64encode(data).decode("utf-8")
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def safe_base64_decode(data: str) -> str:
|
|
20
|
+
"""Safely decode base64 string to bytes and then to utf-8 string.
|
|
21
|
+
|
|
22
|
+
Args:
|
|
23
|
+
data: Base64 encoded string
|
|
24
|
+
|
|
25
|
+
Returns:
|
|
26
|
+
Decoded utf-8 string
|
|
27
|
+
"""
|
|
28
|
+
return base64.b64decode(data).decode("utf-8")
|
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
"""Example: Client agent making payments to utility agents."""
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
import asyncio
|
|
5
|
+
from traia_iatp.client import create_d402_a2a_client
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
async def example_simple_payment():
|
|
9
|
+
"""Example 1: Simple payment to a utility agent."""
|
|
10
|
+
print("=" * 60)
|
|
11
|
+
print("Example 1: Simple Payment")
|
|
12
|
+
print("=" * 60)
|
|
13
|
+
|
|
14
|
+
# Create client with payment support
|
|
15
|
+
client = create_d402_a2a_client(
|
|
16
|
+
agent_endpoint="https://sentiment-agent.traia.io",
|
|
17
|
+
payment_private_key=os.getenv("CLIENT_PRIVATE_KEY"),
|
|
18
|
+
max_payment_usd=5.0 # Maximum $5 per request
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
# Send message - automatically handles payment if required
|
|
22
|
+
try:
|
|
23
|
+
response = await client.send_message_with_payment(
|
|
24
|
+
"Analyze sentiment: 'Tech stocks rally on strong earnings'"
|
|
25
|
+
)
|
|
26
|
+
print(f"Response: {response}")
|
|
27
|
+
except ValueError as e:
|
|
28
|
+
print(f"Payment error: {e}")
|
|
29
|
+
except Exception as e:
|
|
30
|
+
print(f"Error: {e}")
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
async def example_crewai_integration():
|
|
34
|
+
"""Example 2: Using paid agents in CrewAI."""
|
|
35
|
+
print("\n" + "=" * 60)
|
|
36
|
+
print("Example 2: CrewAI Integration with Paid Agents")
|
|
37
|
+
print("=" * 60)
|
|
38
|
+
|
|
39
|
+
from crewai import Agent, Task, Crew
|
|
40
|
+
from traia_iatp.client.crewai_a2a_tools import A2AToolkit
|
|
41
|
+
from traia_iatp.registry.iatp_search_api import find_utility_agent
|
|
42
|
+
|
|
43
|
+
# Find the utility agent
|
|
44
|
+
agent_info = find_utility_agent(agent_id="finbert-sentiment-agent")
|
|
45
|
+
|
|
46
|
+
if not agent_info:
|
|
47
|
+
print("Agent not found in registry")
|
|
48
|
+
return
|
|
49
|
+
|
|
50
|
+
print(f"Found agent: {agent_info.name}")
|
|
51
|
+
print(f"D402 Enabled: {agent_info.d402_enabled}")
|
|
52
|
+
|
|
53
|
+
# Create tool with payment support
|
|
54
|
+
sentiment_tool = A2AToolkit.create_tool_from_endpoint(
|
|
55
|
+
endpoint=agent_info.base_url,
|
|
56
|
+
name=agent_info.name,
|
|
57
|
+
description=agent_info.description,
|
|
58
|
+
# Payment configuration
|
|
59
|
+
payment_private_key=os.getenv("CLIENT_PRIVATE_KEY"),
|
|
60
|
+
max_payment_usd=1.0
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
# Create CrewAI agent
|
|
64
|
+
analyst = Agent(
|
|
65
|
+
role="Financial Sentiment Analyst",
|
|
66
|
+
goal="Analyze sentiment of financial news",
|
|
67
|
+
backstory="Expert financial analyst with access to AI sentiment tools",
|
|
68
|
+
tools=[sentiment_tool],
|
|
69
|
+
verbose=True
|
|
70
|
+
)
|
|
71
|
+
|
|
72
|
+
# Create task
|
|
73
|
+
task = Task(
|
|
74
|
+
description="Analyze sentiment of: 'Federal Reserve signals rate cuts ahead'",
|
|
75
|
+
expected_output="Sentiment analysis with confidence scores",
|
|
76
|
+
agent=analyst
|
|
77
|
+
)
|
|
78
|
+
|
|
79
|
+
# Run crew
|
|
80
|
+
crew = Crew(agents=[analyst], tasks=[task])
|
|
81
|
+
result = crew.kickoff()
|
|
82
|
+
|
|
83
|
+
print(f"\nResult: {result}")
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
async def example_multiple_agents():
|
|
87
|
+
"""Example 3: Using multiple paid utility agents."""
|
|
88
|
+
print("\n" + "=" * 60)
|
|
89
|
+
print("Example 3: Multiple Paid Agents")
|
|
90
|
+
print("=" * 60)
|
|
91
|
+
|
|
92
|
+
from traia_iatp.registry.iatp_search_api import search_utility_agents
|
|
93
|
+
from traia_iatp.client import create_d402_a2a_client
|
|
94
|
+
|
|
95
|
+
# Search for sentiment analysis agents
|
|
96
|
+
agents = search_utility_agents(
|
|
97
|
+
query="sentiment analysis",
|
|
98
|
+
limit=5
|
|
99
|
+
)
|
|
100
|
+
|
|
101
|
+
print(f"Found {len(agents)} sentiment analysis agents")
|
|
102
|
+
|
|
103
|
+
# Filter for paid agents
|
|
104
|
+
paid_agents = [a for a in agents if a.d402_enabled]
|
|
105
|
+
print(f"Found {len(paid_agents)} paid agents")
|
|
106
|
+
|
|
107
|
+
# Create clients for each paid agent
|
|
108
|
+
for agent_info in paid_agents[:2]: # Try first 2
|
|
109
|
+
print(f"\n--- Testing {agent_info.name} ---")
|
|
110
|
+
|
|
111
|
+
client = create_d402_a2a_client(
|
|
112
|
+
agent_endpoint=agent_info.base_url,
|
|
113
|
+
payment_private_key=os.getenv("CLIENT_PRIVATE_KEY"),
|
|
114
|
+
max_payment_usd=1.0
|
|
115
|
+
)
|
|
116
|
+
|
|
117
|
+
try:
|
|
118
|
+
response = await client.send_message_with_payment(
|
|
119
|
+
"Analyze: 'Stock market reaches all-time high'"
|
|
120
|
+
)
|
|
121
|
+
print(f"Response: {response}")
|
|
122
|
+
except Exception as e:
|
|
123
|
+
print(f"Error: {e}")
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
async def example_payment_limits():
|
|
127
|
+
"""Example 4: Payment limits and error handling."""
|
|
128
|
+
print("\n" + "=" * 60)
|
|
129
|
+
print("Example 4: Payment Limits and Error Handling")
|
|
130
|
+
print("=" * 60)
|
|
131
|
+
|
|
132
|
+
# Create client with very low payment limit
|
|
133
|
+
client = create_d402_a2a_client(
|
|
134
|
+
agent_endpoint="https://expensive-agent.traia.io",
|
|
135
|
+
payment_private_key=os.getenv("CLIENT_PRIVATE_KEY"),
|
|
136
|
+
max_payment_usd=0.001 # Only allow $0.001
|
|
137
|
+
)
|
|
138
|
+
|
|
139
|
+
try:
|
|
140
|
+
response = await client.send_message_with_payment("test")
|
|
141
|
+
print(f"Response: {response}")
|
|
142
|
+
except ValueError as e:
|
|
143
|
+
print(f"Payment rejected: {e}")
|
|
144
|
+
print("Solution: Increase max_payment_usd or choose a cheaper agent")
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
async def example_check_agent_pricing():
|
|
148
|
+
"""Example 5: Check agent pricing before calling."""
|
|
149
|
+
print("\n" + "=" * 60)
|
|
150
|
+
print("Example 5: Check Pricing Before Calling")
|
|
151
|
+
print("=" * 60)
|
|
152
|
+
|
|
153
|
+
from traia_iatp.registry.iatp_search_api import find_utility_agent
|
|
154
|
+
|
|
155
|
+
# Find agent and check pricing
|
|
156
|
+
agent_info = find_utility_agent(agent_id="finbert-sentiment-agent")
|
|
157
|
+
|
|
158
|
+
if agent_info and agent_info.d402_enabled:
|
|
159
|
+
payment_info = agent_info.d402_payment_info
|
|
160
|
+
|
|
161
|
+
print(f"Agent: {agent_info.name}")
|
|
162
|
+
print(f"D402 Enabled: {agent_info.d402_enabled}")
|
|
163
|
+
|
|
164
|
+
if payment_info:
|
|
165
|
+
default_price = payment_info.get("defaultPrice", {})
|
|
166
|
+
print(f"Default Price: ${default_price.get('usdAmount', 'N/A')} USD")
|
|
167
|
+
print(f"Network: {default_price.get('network', 'N/A')}")
|
|
168
|
+
print(f"Asset: {default_price.get('asset', 'N/A')}")
|
|
169
|
+
|
|
170
|
+
skill_prices = payment_info.get("skillPrices", {})
|
|
171
|
+
if skill_prices:
|
|
172
|
+
print("\nSkill-specific pricing:")
|
|
173
|
+
for skill_id, price in skill_prices.items():
|
|
174
|
+
print(f" {skill_id}: ${price.get('usdAmount', 'N/A')} USD")
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
async def main():
|
|
178
|
+
"""Run all examples."""
|
|
179
|
+
# Example 1: Simple payment
|
|
180
|
+
await example_simple_payment()
|
|
181
|
+
|
|
182
|
+
# Example 2: CrewAI integration
|
|
183
|
+
await example_crewai_integration()
|
|
184
|
+
|
|
185
|
+
# Example 3: Multiple agents
|
|
186
|
+
await example_multiple_agents()
|
|
187
|
+
|
|
188
|
+
# Example 4: Payment limits
|
|
189
|
+
await example_payment_limits()
|
|
190
|
+
|
|
191
|
+
# Example 5: Check pricing
|
|
192
|
+
await example_check_agent_pricing()
|
|
193
|
+
|
|
194
|
+
|
|
195
|
+
if __name__ == "__main__":
|
|
196
|
+
asyncio.run(main())
|
|
197
|
+
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
"""Example: Utility agent with d402 payment support."""
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
import asyncio
|
|
5
|
+
from fastapi import FastAPI, Request
|
|
6
|
+
import uvicorn
|
|
7
|
+
|
|
8
|
+
from traia_iatp.d402 import (
|
|
9
|
+
D402Config,
|
|
10
|
+
D402ServicePrice,
|
|
11
|
+
require_iatp_payment,
|
|
12
|
+
add_d402_info_to_agent_card
|
|
13
|
+
)
|
|
14
|
+
|
|
15
|
+
# Initialize FastAPI app
|
|
16
|
+
app = FastAPI(title="Paid Sentiment Analysis Agent")
|
|
17
|
+
|
|
18
|
+
# Configure d402 payments
|
|
19
|
+
d402_config = D402Config(
|
|
20
|
+
enabled=True,
|
|
21
|
+
# This would be the deployed utility agent contract address
|
|
22
|
+
pay_to_address=os.getenv("UTILITY_AGENT_CONTRACT_ADDRESS", "0x1234567890123456789012345678901234567890"),
|
|
23
|
+
|
|
24
|
+
# Default pricing: $0.01 per request
|
|
25
|
+
default_price=D402ServicePrice(
|
|
26
|
+
usd_amount="0.01",
|
|
27
|
+
network="base-mainnet",
|
|
28
|
+
asset_address="0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913", # USDC on Base
|
|
29
|
+
max_timeout_seconds=300
|
|
30
|
+
),
|
|
31
|
+
|
|
32
|
+
# Custom pricing per skill
|
|
33
|
+
skill_prices={
|
|
34
|
+
"sentiment_analysis": D402ServicePrice(
|
|
35
|
+
usd_amount="0.01",
|
|
36
|
+
network="base-mainnet",
|
|
37
|
+
asset_address="0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913",
|
|
38
|
+
max_timeout_seconds=300
|
|
39
|
+
),
|
|
40
|
+
"entity_extraction": D402ServicePrice(
|
|
41
|
+
usd_amount="0.02",
|
|
42
|
+
network="base-mainnet",
|
|
43
|
+
asset_address="0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913",
|
|
44
|
+
max_timeout_seconds=300
|
|
45
|
+
)
|
|
46
|
+
},
|
|
47
|
+
|
|
48
|
+
# Facilitator configuration
|
|
49
|
+
facilitator_url=os.getenv("FACILITATOR_URL", "https://api.traia.io/d402/facilitator"),
|
|
50
|
+
facilitator_api_key=os.getenv("TRAIA_RELAYER_API_KEY"),
|
|
51
|
+
|
|
52
|
+
# Service description for payment UI
|
|
53
|
+
service_description="AI-powered financial sentiment analysis using FinBERT models",
|
|
54
|
+
|
|
55
|
+
# Protect all paths by default
|
|
56
|
+
protected_paths=["*"]
|
|
57
|
+
)
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
# Add d402 middleware
|
|
61
|
+
@app.middleware("http")
|
|
62
|
+
async def payment_middleware(request: Request, call_next):
|
|
63
|
+
"""Middleware that requires payment for all requests."""
|
|
64
|
+
middleware = require_iatp_payment(d402_config)
|
|
65
|
+
return await middleware(request, call_next)
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
# Agent card endpoint (standard A2A protocol)
|
|
69
|
+
@app.get("/.well-known/agent.json")
|
|
70
|
+
async def agent_card():
|
|
71
|
+
"""Return agent card with d402 payment information."""
|
|
72
|
+
card = {
|
|
73
|
+
"name": "sentiment_analysis_agent",
|
|
74
|
+
"description": "AI-powered financial sentiment analysis",
|
|
75
|
+
"version": "1.0.0",
|
|
76
|
+
"capabilities": {
|
|
77
|
+
"streaming": False,
|
|
78
|
+
"pushNotifications": False,
|
|
79
|
+
"stateTransitionHistory": False
|
|
80
|
+
},
|
|
81
|
+
"skills": [
|
|
82
|
+
{
|
|
83
|
+
"id": "sentiment_analysis",
|
|
84
|
+
"name": "Sentiment Analysis",
|
|
85
|
+
"description": "Analyze sentiment of financial text",
|
|
86
|
+
"examples": [
|
|
87
|
+
"Analyze: 'Tech stocks rally on strong earnings'",
|
|
88
|
+
"What is the sentiment of: 'Markets tumble amid recession fears'"
|
|
89
|
+
]
|
|
90
|
+
}
|
|
91
|
+
]
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
# Add d402 payment information
|
|
95
|
+
card = await add_d402_info_to_agent_card(card, d402_config)
|
|
96
|
+
return card
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
# Protected endpoint - requires payment
|
|
100
|
+
@app.post("/analyze")
|
|
101
|
+
async def analyze_sentiment(request: Request):
|
|
102
|
+
"""Analyze sentiment of text. Requires payment."""
|
|
103
|
+
data = await request.json()
|
|
104
|
+
text = data.get("text", "")
|
|
105
|
+
|
|
106
|
+
# Simulate sentiment analysis
|
|
107
|
+
# In real implementation, this would call a model
|
|
108
|
+
result = {
|
|
109
|
+
"text": text,
|
|
110
|
+
"sentiment": "positive",
|
|
111
|
+
"confidence": 0.87,
|
|
112
|
+
"scores": {
|
|
113
|
+
"positive": 0.87,
|
|
114
|
+
"neutral": 0.08,
|
|
115
|
+
"negative": 0.05
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
return result
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
# Another protected endpoint
|
|
123
|
+
@app.post("/extract-entities")
|
|
124
|
+
async def extract_entities(request: Request):
|
|
125
|
+
"""Extract entities from text. Requires payment (higher price)."""
|
|
126
|
+
data = await request.json()
|
|
127
|
+
text = data.get("text", "")
|
|
128
|
+
|
|
129
|
+
# Simulate entity extraction
|
|
130
|
+
result = {
|
|
131
|
+
"text": text,
|
|
132
|
+
"entities": [
|
|
133
|
+
{"text": "Apple", "type": "ORGANIZATION", "confidence": 0.95},
|
|
134
|
+
{"text": "Tim Cook", "type": "PERSON", "confidence": 0.92}
|
|
135
|
+
]
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
return result
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
# Health check endpoint (not protected)
|
|
142
|
+
@app.get("/health")
|
|
143
|
+
async def health():
|
|
144
|
+
"""Health check endpoint - no payment required."""
|
|
145
|
+
# This endpoint bypasses payment because we can add specific path exclusions
|
|
146
|
+
return {"status": "healthy"}
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
def main():
|
|
150
|
+
"""Run the server."""
|
|
151
|
+
print("Starting Paid Sentiment Analysis Agent")
|
|
152
|
+
print(f"D402 Enabled: {d402_config.enabled}")
|
|
153
|
+
print(f"Pay-to Address: {d402_config.pay_to_address}")
|
|
154
|
+
print(f"Default Price: ${d402_config.default_price.usd_amount} USD")
|
|
155
|
+
print(f"Facilitator: {d402_config.facilitator_url}")
|
|
156
|
+
print()
|
|
157
|
+
print("Protected endpoints:")
|
|
158
|
+
print(" POST /analyze - $0.01 per request")
|
|
159
|
+
print(" POST /extract-entities - $0.02 per request")
|
|
160
|
+
print()
|
|
161
|
+
print("Free endpoints:")
|
|
162
|
+
print(" GET /health")
|
|
163
|
+
print(" GET /.well-known/agent.json")
|
|
164
|
+
print()
|
|
165
|
+
|
|
166
|
+
uvicorn.run(app, host="0.0.0.0", port=8000)
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
if __name__ == "__main__":
|
|
170
|
+
main()
|
|
171
|
+
|