acp-plugin-gamesdk 0.1.2__py3-none-any.whl → 0.1.4__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.
- acp_plugin_gamesdk/acp_client.py +27 -37
- acp_plugin_gamesdk/acp_plugin.py +276 -141
- acp_plugin_gamesdk/acp_token.py +25 -28
- acp_plugin_gamesdk/interface.py +8 -3
- {acp_plugin_gamesdk-0.1.2.dist-info → acp_plugin_gamesdk-0.1.4.dist-info}/METADATA +111 -67
- acp_plugin_gamesdk-0.1.4.dist-info/RECORD +8 -0
- acp_plugin_gamesdk-0.1.2.dist-info/RECORD +0 -8
- {acp_plugin_gamesdk-0.1.2.dist-info → acp_plugin_gamesdk-0.1.4.dist-info}/WHEEL +0 -0
acp_plugin_gamesdk/acp_client.py
CHANGED
@@ -2,7 +2,7 @@ from datetime import datetime, timedelta
|
|
2
2
|
from typing import List, Optional
|
3
3
|
from web3 import Web3
|
4
4
|
import requests
|
5
|
-
from acp_plugin_gamesdk.interface import AcpAgent, AcpJobPhases, AcpState
|
5
|
+
from acp_plugin_gamesdk.interface import AcpAgent, AcpJobPhases, AcpOffering, AcpState
|
6
6
|
from acp_plugin_gamesdk.acp_token import AcpToken, MemoType
|
7
7
|
import time
|
8
8
|
|
@@ -29,7 +29,6 @@ class AcpClient:
|
|
29
29
|
def browse_agents(self, cluster: Optional[str] = None, query: Optional[str] = None) -> List[AcpAgent]:
|
30
30
|
url = f"{self.acp_base_url}/agents"
|
31
31
|
|
32
|
-
# Build URL with query parameters
|
33
32
|
if query:
|
34
33
|
url += f"?search={requests.utils.quote(query)}"
|
35
34
|
|
@@ -48,25 +47,32 @@ class AcpClient:
|
|
48
47
|
result = []
|
49
48
|
|
50
49
|
for agent in response_json.get("data", []):
|
50
|
+
if agent["offerings"]:
|
51
|
+
offerings = [AcpOffering(name=offering["name"], price=offering["price"]) for offering in agent["offerings"]]
|
52
|
+
else:
|
53
|
+
offerings = None
|
54
|
+
|
51
55
|
result.append(
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
56
|
+
AcpAgent(
|
57
|
+
id=agent["id"],
|
58
|
+
name=agent["name"],
|
59
|
+
description=agent["description"],
|
60
|
+
wallet_address=agent["walletAddress"],
|
61
|
+
offerings=offerings
|
62
|
+
)
|
58
63
|
)
|
59
64
|
|
60
65
|
return result
|
61
66
|
|
62
|
-
def create_job(self, provider_address: str, price: float, job_description: str) -> int:
|
67
|
+
def create_job(self, provider_address: str, price: float, job_description: str, evaluator_address: str) -> int:
|
63
68
|
expire_at = datetime.now() + timedelta(days=1)
|
64
69
|
|
65
70
|
tx_result = self.acp_token.create_job(
|
66
71
|
provider_address=provider_address,
|
67
|
-
evaluator_address=
|
72
|
+
evaluator_address=evaluator_address,
|
68
73
|
expire_at=expire_at
|
69
74
|
)
|
75
|
+
|
70
76
|
job_id = None
|
71
77
|
retry_count = 3
|
72
78
|
retry_delay = 3
|
@@ -86,7 +92,7 @@ class AcpClient:
|
|
86
92
|
break
|
87
93
|
|
88
94
|
if (data.get("status") == "success"):
|
89
|
-
job_id = data.get("result").get("jobId")
|
95
|
+
job_id = int(data.get("result").get("jobId"))
|
90
96
|
|
91
97
|
if (job_id is not None and job_id != ""):
|
92
98
|
break
|
@@ -102,7 +108,7 @@ class AcpClient:
|
|
102
108
|
raise Exception("Failed to create job")
|
103
109
|
|
104
110
|
self.acp_token.create_memo(
|
105
|
-
job_id=
|
111
|
+
job_id=job_id,
|
106
112
|
content=job_description,
|
107
113
|
memo_type=MemoType.MESSAGE,
|
108
114
|
is_secured=False,
|
@@ -110,12 +116,13 @@ class AcpClient:
|
|
110
116
|
)
|
111
117
|
|
112
118
|
payload = {
|
113
|
-
"jobId":
|
119
|
+
"jobId": job_id,
|
114
120
|
"clientAddress": self.agent_wallet_address,
|
115
121
|
"providerAddress": provider_address,
|
116
122
|
"description": job_description,
|
117
123
|
"price": price,
|
118
|
-
"expiredAt": expire_at.isoformat()
|
124
|
+
"expiredAt": expire_at.isoformat(),
|
125
|
+
"evaluatorAddress": evaluator_address
|
119
126
|
}
|
120
127
|
|
121
128
|
requests.post(
|
@@ -133,11 +140,10 @@ class AcpClient:
|
|
133
140
|
def response_job(self, job_id: int, accept: bool, memo_id: int, reasoning: str):
|
134
141
|
if accept:
|
135
142
|
self.acp_token.sign_memo(memo_id, accept, reasoning)
|
136
|
-
|
137
143
|
time.sleep(5)
|
138
144
|
|
139
145
|
return self.acp_token.create_memo(
|
140
|
-
job_id=
|
146
|
+
job_id=job_id,
|
141
147
|
content=f"Job {job_id} accepted. {reasoning}",
|
142
148
|
memo_type=MemoType.MESSAGE,
|
143
149
|
is_secured=False,
|
@@ -145,7 +151,7 @@ class AcpClient:
|
|
145
151
|
)
|
146
152
|
else:
|
147
153
|
return self.acp_token.create_memo(
|
148
|
-
job_id=
|
154
|
+
job_id=job_id,
|
149
155
|
content=f"Job {job_id} rejected. {reasoning}",
|
150
156
|
memo_type=MemoType.MESSAGE,
|
151
157
|
is_secured=False,
|
@@ -162,9 +168,9 @@ class AcpClient:
|
|
162
168
|
time.sleep(5)
|
163
169
|
return self.acp_token.sign_memo(memo_id, True, reason)
|
164
170
|
|
165
|
-
def deliver_job(self, job_id: int, deliverable: str
|
171
|
+
def deliver_job(self, job_id: int, deliverable: str):
|
166
172
|
return self.acp_token.create_memo(
|
167
|
-
job_id=
|
173
|
+
job_id=job_id,
|
168
174
|
content=deliverable,
|
169
175
|
memo_type=MemoType.MESSAGE,
|
170
176
|
is_secured=False,
|
@@ -172,17 +178,6 @@ class AcpClient:
|
|
172
178
|
)
|
173
179
|
|
174
180
|
def add_tweet(self, job_id: int, tweet_id: str, content: str):
|
175
|
-
"""
|
176
|
-
Add a tweet to a job.
|
177
|
-
|
178
|
-
Args:
|
179
|
-
job_id: The ID of the job
|
180
|
-
tweet_id: The ID of the tweet
|
181
|
-
content: The content of the tweet
|
182
|
-
|
183
|
-
Raises:
|
184
|
-
Exception: If the request fails
|
185
|
-
"""
|
186
181
|
payload = {
|
187
182
|
"tweetId": tweet_id,
|
188
183
|
"content": content
|
@@ -204,14 +199,9 @@ class AcpClient:
|
|
204
199
|
|
205
200
|
return response.json()
|
206
201
|
|
207
|
-
def reset_state(self
|
208
|
-
if not wallet_address:
|
209
|
-
raise Exception("Wallet address is required")
|
210
|
-
|
211
|
-
address = wallet_address
|
212
|
-
|
202
|
+
def reset_state(self) -> None:
|
213
203
|
response = requests.delete(
|
214
|
-
f"{self.base_url}/states/{
|
204
|
+
f"{self.base_url}/states/{self.agent_wallet_address}",
|
215
205
|
headers={"x-api-key": self.api_key}
|
216
206
|
)
|
217
207
|
|
acp_plugin_gamesdk/acp_plugin.py
CHANGED
@@ -1,15 +1,21 @@
|
|
1
|
+
from collections.abc import Callable
|
2
|
+
import signal
|
3
|
+
import sys
|
1
4
|
from typing import List, Dict, Any, Optional,Tuple
|
2
5
|
import json
|
3
6
|
from dataclasses import dataclass
|
4
7
|
from datetime import datetime
|
5
8
|
|
9
|
+
import socketio
|
10
|
+
import socketio.client
|
11
|
+
|
6
12
|
from game_sdk.game.agent import WorkerConfig
|
7
|
-
from game_sdk.game.custom_types import Function, FunctionResultStatus
|
13
|
+
from game_sdk.game.custom_types import Argument, Function, FunctionResultStatus
|
8
14
|
from twitter_plugin_gamesdk.twitter_plugin import TwitterPlugin
|
9
15
|
from twitter_plugin_gamesdk.game_twitter_plugin import GameTwitterPlugin
|
10
16
|
from acp_plugin_gamesdk.acp_client import AcpClient
|
11
17
|
from acp_plugin_gamesdk.acp_token import AcpToken
|
12
|
-
from acp_plugin_gamesdk.interface import AcpJobPhasesDesc, IInventory
|
18
|
+
from acp_plugin_gamesdk.interface import AcpJobPhasesDesc, IDeliverable, IInventory
|
13
19
|
|
14
20
|
@dataclass
|
15
21
|
class AcpPluginOptions:
|
@@ -17,13 +23,21 @@ class AcpPluginOptions:
|
|
17
23
|
acp_token_client: AcpToken
|
18
24
|
twitter_plugin: TwitterPlugin | GameTwitterPlugin = None
|
19
25
|
cluster: Optional[str] = None
|
20
|
-
|
26
|
+
evaluator_cluster: Optional[str] = None
|
27
|
+
on_evaluate: Optional[Callable[[IDeliverable], Tuple[bool, str]]] = None
|
21
28
|
|
29
|
+
SocketEvents = {
|
30
|
+
"JOIN_EVALUATOR_ROOM": "joinEvaluatorRoom",
|
31
|
+
"LEAVE_EVALUATOR_ROOM": "leaveEvaluatorRoom",
|
32
|
+
"ON_EVALUATE": "onEvaluate",
|
33
|
+
"ROOM_JOINED" : "roomJoined"
|
34
|
+
}
|
22
35
|
|
23
36
|
class AcpPlugin:
|
24
37
|
def __init__(self, options: AcpPluginOptions):
|
25
38
|
print("Initializing AcpPlugin")
|
26
|
-
self.
|
39
|
+
self.acp_token_client = options.acp_token_client
|
40
|
+
self.acp_client = AcpClient(options.api_key, options.acp_token_client, options.acp_token_client.acp_base_url)
|
27
41
|
self.id = "acp_worker"
|
28
42
|
self.name = "ACP Worker"
|
29
43
|
self.description = """
|
@@ -42,16 +56,76 @@ class AcpPlugin:
|
|
42
56
|
NOTE: This is NOT for finding clients - only for executing trades when there's a specific need to buy or sell something.
|
43
57
|
"""
|
44
58
|
self.cluster = options.cluster
|
59
|
+
self.evaluator_cluster = options.evaluator_cluster
|
45
60
|
self.twitter_plugin = options.twitter_plugin
|
46
61
|
self.produced_inventory: List[IInventory] = []
|
47
|
-
self.acp_base_url =
|
48
|
-
|
62
|
+
self.acp_base_url = self.acp_token_client.acp_base_url if self.acp_token_client.acp_base_url is None else "https://acpx-staging.virtuals.io/api"
|
63
|
+
if (options.on_evaluate is not None):
|
64
|
+
print("Initializing socket")
|
65
|
+
self.on_evaluate = options.on_evaluate
|
66
|
+
self.socket = None
|
67
|
+
self.initializeSocket()
|
68
|
+
|
69
|
+
def initializeSocket(self) -> Tuple[bool, str]:
|
70
|
+
"""
|
71
|
+
Initialize socket connection for real-time communication.
|
72
|
+
Returns a tuple of (success, message).
|
73
|
+
"""
|
74
|
+
try:
|
75
|
+
print("Initializing socket after")
|
76
|
+
self.socket = socketio.Client()
|
77
|
+
|
78
|
+
# Set up authentication before connecting
|
79
|
+
self.socket.auth = {
|
80
|
+
"evaluatorAddress": self.acp_token_client.agent_wallet_address
|
81
|
+
}
|
82
|
+
|
83
|
+
# Connect socket to GAME SDK dev server
|
84
|
+
self.socket.connect("https://sdk-dev.game.virtuals.io", auth=self.socket.auth)
|
85
|
+
|
86
|
+
if (self.socket.connected):
|
87
|
+
self.socket.emit(SocketEvents["JOIN_EVALUATOR_ROOM"], self.acp_token_client.agent_wallet_address)
|
88
|
+
|
89
|
+
|
90
|
+
# Set up event handler for evaluation requests
|
91
|
+
@self.socket.on(SocketEvents["ON_EVALUATE"])
|
92
|
+
def on_evaluate(data):
|
93
|
+
if self.on_evaluate:
|
94
|
+
deliverable = data.get("deliverable")
|
95
|
+
memo_id = data.get("memoId")
|
96
|
+
|
97
|
+
is_approved, reasoning = self.on_evaluate(deliverable)
|
98
|
+
|
99
|
+
self.acp_token_client.sign_memo(memo_id, is_approved, reasoning)
|
100
|
+
|
101
|
+
# Set up cleanup function for graceful shutdown
|
102
|
+
def cleanup():
|
103
|
+
if self.socket:
|
104
|
+
print("Disconnecting socket")
|
105
|
+
|
106
|
+
import time
|
107
|
+
time.sleep(1)
|
108
|
+
self.socket.disconnect()
|
109
|
+
|
110
|
+
def signal_handler(sig, frame):
|
111
|
+
cleanup()
|
112
|
+
sys.exit(0)
|
113
|
+
|
114
|
+
signal.signal(signal.SIGINT, signal_handler)
|
115
|
+
signal.signal(signal.SIGTERM, signal_handler)
|
116
|
+
|
117
|
+
return True, "Socket initialized successfully"
|
118
|
+
|
119
|
+
except Exception as e:
|
120
|
+
return False, f"Failed to initialize socket: {str(e)}"
|
121
|
+
|
122
|
+
|
49
123
|
|
50
124
|
def add_produce_item(self, item: IInventory) -> None:
|
51
125
|
self.produced_inventory.append(item)
|
52
126
|
|
53
127
|
def reset_state(self) -> None:
|
54
|
-
self.acp_client.reset_state(
|
128
|
+
self.acp_client.reset_state()
|
55
129
|
|
56
130
|
def get_acp_state(self) -> Dict:
|
57
131
|
server_state = self.acp_client.get_state()
|
@@ -74,7 +148,7 @@ class AcpPlugin:
|
|
74
148
|
**(self.get_acp_state()),
|
75
149
|
}
|
76
150
|
|
77
|
-
|
151
|
+
worker_config = WorkerConfig(
|
78
152
|
id=self.id,
|
79
153
|
worker_description=self.description,
|
80
154
|
action_space=functions,
|
@@ -82,7 +156,7 @@ class AcpPlugin:
|
|
82
156
|
instruction=data.get("instructions") if data else None
|
83
157
|
)
|
84
158
|
|
85
|
-
return
|
159
|
+
return worker_config
|
86
160
|
|
87
161
|
@property
|
88
162
|
def agent_description(self) -> str:
|
@@ -109,9 +183,9 @@ class AcpPlugin:
|
|
109
183
|
|
110
184
|
if not agents:
|
111
185
|
return FunctionResultStatus.FAILED, "No other trading agents found in the system. Please try again later when more agents are available.", {}
|
112
|
-
|
186
|
+
|
113
187
|
return FunctionResultStatus.DONE, json.dumps({
|
114
|
-
"availableAgents": agents,
|
188
|
+
"availableAgents": [{"id": agent.id, "name": agent.name, "description": agent.description, "wallet_address": agent.wallet_address, "offerings": [{"name": offering.name, "price": offering.price} for offering in agent.offerings] if agent.offerings else []} for agent in agents],
|
115
189
|
"totalAgentsFound": len(agents),
|
116
190
|
"timestamp": datetime.now().timestamp(),
|
117
191
|
"note": "Use the walletAddress when initiating a job with your chosen trading partner."
|
@@ -119,74 +193,115 @@ class AcpPlugin:
|
|
119
193
|
|
120
194
|
@property
|
121
195
|
def search_agents_functions(self) -> Function:
|
196
|
+
reasoning_arg = Argument(
|
197
|
+
name="reasoning",
|
198
|
+
type="string",
|
199
|
+
description="Explain why you need to find trading partners at this time",
|
200
|
+
)
|
201
|
+
|
202
|
+
keyword_arg = Argument(
|
203
|
+
name="keyword",
|
204
|
+
type="string",
|
205
|
+
description="Search for agents by name or description. Use this to find specific trading partners or products.",
|
206
|
+
)
|
207
|
+
|
122
208
|
return Function(
|
123
209
|
fn_name="search_agents",
|
124
210
|
fn_description="Get a list of all available trading agents and what they're selling. Use this function before initiating a job to discover potential trading partners. Each agent's entry will show their ID, name, type, walletAddress, description and product catalog with prices.",
|
125
|
-
args=[
|
126
|
-
{
|
127
|
-
"name": "reasoning",
|
128
|
-
"type": "string",
|
129
|
-
"description": "Explain why you need to find trading partners at this time",
|
130
|
-
},
|
131
|
-
{
|
132
|
-
"name": "keyword",
|
133
|
-
"type": "string",
|
134
|
-
"description": "Search for agents by name or description. Use this to find specific trading partners or products.",
|
135
|
-
},
|
136
|
-
],
|
211
|
+
args=[reasoning_arg, keyword_arg],
|
137
212
|
executable=self._search_agents_executable
|
138
213
|
)
|
139
214
|
|
140
215
|
@property
|
141
216
|
def initiate_job(self) -> Function:
|
217
|
+
seller_wallet_address_arg = Argument(
|
218
|
+
name="sellerWalletAddress",
|
219
|
+
type="string",
|
220
|
+
description="The seller's agent wallet address you want to buy from",
|
221
|
+
)
|
222
|
+
|
223
|
+
price_arg = Argument(
|
224
|
+
name="price",
|
225
|
+
type="string",
|
226
|
+
description="Offered price for service",
|
227
|
+
)
|
228
|
+
|
229
|
+
reasoning_arg = Argument(
|
230
|
+
name="reasoning",
|
231
|
+
type="string",
|
232
|
+
description="Why you are making this purchase request",
|
233
|
+
)
|
234
|
+
|
235
|
+
service_requirements_arg = Argument(
|
236
|
+
name="serviceRequirements",
|
237
|
+
type="string",
|
238
|
+
description="Detailed specifications for service-based items",
|
239
|
+
)
|
240
|
+
|
241
|
+
require_evaluation_arg = Argument(
|
242
|
+
name="requireEvaluation",
|
243
|
+
type="boolean",
|
244
|
+
description="Decide if your job request is complex enough to spend money for evaluator agent to assess the relevancy of the output. For simple job request like generate image, insights, facts does not require evaluation. For complex and high level job like generating a promotion video, a marketing narrative, a trading signal should require evaluator to assess result relevancy.",
|
245
|
+
)
|
246
|
+
|
247
|
+
evaluator_keyword_arg = Argument(
|
248
|
+
name="evaluatorKeyword",
|
249
|
+
type="string",
|
250
|
+
description="Keyword to search for a evaluator",
|
251
|
+
)
|
252
|
+
|
253
|
+
args = [seller_wallet_address_arg, price_arg, reasoning_arg, service_requirements_arg, require_evaluation_arg, evaluator_keyword_arg]
|
254
|
+
|
255
|
+
if self.twitter_plugin is not None:
|
256
|
+
tweet_content_arg = Argument(
|
257
|
+
name="tweetContent",
|
258
|
+
type="string",
|
259
|
+
description="Tweet content that will be posted about this job. Must include the seller's Twitter handle (with @ symbol) to notify them",
|
260
|
+
)
|
261
|
+
args.append(tweet_content_arg)
|
262
|
+
|
142
263
|
return Function(
|
143
264
|
fn_name="initiate_job",
|
144
265
|
fn_description="Creates a purchase request for items from another agent's catalog. Only for use when YOU are the buyer. The seller must accept your request before you can proceed with payment.",
|
145
|
-
args=
|
146
|
-
{
|
147
|
-
"name": "sellerWalletAddress",
|
148
|
-
"type": "string",
|
149
|
-
"description": "The seller's agent wallet address you want to buy from",
|
150
|
-
},
|
151
|
-
{
|
152
|
-
"name": "price",
|
153
|
-
"type": "string",
|
154
|
-
"description": "Offered price for service",
|
155
|
-
},
|
156
|
-
{
|
157
|
-
"name": "reasoning",
|
158
|
-
"type": "string",
|
159
|
-
"description": "Why you are making this purchase request",
|
160
|
-
},
|
161
|
-
{
|
162
|
-
"name": "serviceRequirements",
|
163
|
-
"type": "string",
|
164
|
-
"description": "Detailed specifications for service-based items",
|
165
|
-
},
|
166
|
-
{
|
167
|
-
"name": "tweetContent",
|
168
|
-
"type": "string",
|
169
|
-
"description": "Tweet content that will be posted about this job. Must include the seller's Twitter handle (with @ symbol) to notify them",
|
170
|
-
},
|
171
|
-
],
|
266
|
+
args=args,
|
172
267
|
executable=self._initiate_job_executable
|
173
268
|
)
|
174
269
|
|
175
|
-
def _initiate_job_executable(self, sellerWalletAddress: str, price: str, reasoning: str, serviceRequirements: str,
|
270
|
+
def _initiate_job_executable(self, sellerWalletAddress: str, price: str, reasoning: str, serviceRequirements: str, requireEvaluation: bool, evaluatorKeyword: str, tweetContent: Optional[str] = None) -> Tuple[FunctionResultStatus, str, dict]:
|
176
271
|
if not price:
|
177
272
|
return FunctionResultStatus.FAILED, "Missing price - specify how much you're offering per unit", {}
|
178
|
-
|
273
|
+
|
274
|
+
if not reasoning:
|
275
|
+
return FunctionResultStatus.FAILED, "Missing reasoning - explain why you're making this purchase request", {}
|
276
|
+
|
179
277
|
try:
|
180
278
|
state = self.get_acp_state()
|
181
279
|
|
182
280
|
if state["jobs"]["active"]["asABuyer"]:
|
183
281
|
return FunctionResultStatus.FAILED, "You already have an active job as a buyer", {}
|
184
|
-
|
282
|
+
|
283
|
+
if not sellerWalletAddress:
|
284
|
+
return FunctionResultStatus.FAILED, "Missing seller wallet address - specify the agent you want to buy from", {}
|
285
|
+
|
286
|
+
if bool(requireEvaluation) and not evaluatorKeyword:
|
287
|
+
return FunctionResultStatus.FAILED, "Missing validator keyword - provide a keyword to search for a validator", {}
|
288
|
+
|
289
|
+
evaluatorAddress = self.acp_token_client.get_agent_wallet_address()
|
290
|
+
|
291
|
+
if bool(requireEvaluation):
|
292
|
+
validators = self.acp_client.browse_agents(self.evaluator_cluster, evaluatorKeyword)
|
293
|
+
|
294
|
+
if len(validators) == 0:
|
295
|
+
return FunctionResultStatus.FAILED, "No evaluator found - try a different keyword", {}
|
296
|
+
|
297
|
+
evaluatorAddress = validators[0].wallet_address
|
298
|
+
|
185
299
|
# ... Rest of validation logic ...
|
186
300
|
job_id = self.acp_client.create_job(
|
187
301
|
sellerWalletAddress,
|
188
302
|
float(price),
|
189
|
-
serviceRequirements
|
303
|
+
serviceRequirements,
|
304
|
+
evaluatorAddress
|
190
305
|
)
|
191
306
|
|
192
307
|
if (self.twitter_plugin is not None and tweetContent is not None):
|
@@ -208,35 +323,42 @@ class AcpPlugin:
|
|
208
323
|
|
209
324
|
@property
|
210
325
|
def respond_job(self) -> Function:
|
326
|
+
job_id_arg = Argument(
|
327
|
+
name="jobId",
|
328
|
+
type="integer",
|
329
|
+
description="The job ID you are responding to",
|
330
|
+
)
|
331
|
+
|
332
|
+
decision_arg = Argument(
|
333
|
+
name="decision",
|
334
|
+
type="string",
|
335
|
+
description="Your response: 'ACCEPT' or 'REJECT'",
|
336
|
+
)
|
337
|
+
|
338
|
+
reasoning_arg = Argument(
|
339
|
+
name="reasoning",
|
340
|
+
type="string",
|
341
|
+
description="Why you made this decision",
|
342
|
+
)
|
343
|
+
|
344
|
+
args = [job_id_arg, decision_arg, reasoning_arg]
|
345
|
+
|
346
|
+
if self.twitter_plugin is not None:
|
347
|
+
tweet_content_arg = Argument(
|
348
|
+
name="tweetContent",
|
349
|
+
type="string",
|
350
|
+
description="Tweet content that will be posted about this job. Must include the seller's Twitter handle (with @ symbol) to notify them",
|
351
|
+
)
|
352
|
+
args.append(tweet_content_arg)
|
353
|
+
|
211
354
|
return Function(
|
212
355
|
fn_name="respond_to_job",
|
213
356
|
fn_description="Accepts or rejects an incoming 'request' job",
|
214
|
-
args=
|
215
|
-
{
|
216
|
-
"name": "jobId",
|
217
|
-
"type": "string",
|
218
|
-
"description": "The job ID you are responding to",
|
219
|
-
},
|
220
|
-
{
|
221
|
-
"name": "decision",
|
222
|
-
"type": "string",
|
223
|
-
"description": "Your response: 'ACCEPT' or 'REJECT'",
|
224
|
-
},
|
225
|
-
{
|
226
|
-
"name": "reasoning",
|
227
|
-
"type": "string",
|
228
|
-
"description": "Why you made this decision",
|
229
|
-
},
|
230
|
-
{
|
231
|
-
"name": "tweetContent",
|
232
|
-
"type": "string",
|
233
|
-
"description": "Tweet content that will be posted about this job. Must include the seller's Twitter handle (with @ symbol) to notify them",
|
234
|
-
},
|
235
|
-
],
|
357
|
+
args=args,
|
236
358
|
executable=self._respond_job_executable
|
237
359
|
)
|
238
360
|
|
239
|
-
def _respond_job_executable(self, jobId:
|
361
|
+
def _respond_job_executable(self, jobId: int, decision: str, reasoning: str, tweetContent: Optional[str] = None) -> Tuple[FunctionResultStatus, str, dict]:
|
240
362
|
if not jobId:
|
241
363
|
return FunctionResultStatus.FAILED, "Missing job ID - specify which job you're responding to", {}
|
242
364
|
|
@@ -250,7 +372,7 @@ class AcpPlugin:
|
|
250
372
|
state = self.get_acp_state()
|
251
373
|
|
252
374
|
job = next(
|
253
|
-
(c for c in state["jobs"]["active"]["asASeller"] if c["jobId"] ==
|
375
|
+
(c for c in state["jobs"]["active"]["asASeller"] if c["jobId"] == jobId),
|
254
376
|
None
|
255
377
|
)
|
256
378
|
|
@@ -261,13 +383,13 @@ class AcpPlugin:
|
|
261
383
|
return FunctionResultStatus.FAILED, f"Cannot respond - job is in '{job['phase']}' phase, must be in 'request' phase", {}
|
262
384
|
|
263
385
|
self.acp_client.response_job(
|
264
|
-
|
386
|
+
jobId,
|
265
387
|
decision == "ACCEPT",
|
266
388
|
job["memo"][0]["id"],
|
267
389
|
reasoning
|
268
390
|
)
|
269
391
|
|
270
|
-
if (self.twitter_plugin is not None):
|
392
|
+
if (self.twitter_plugin is not None and tweetContent is not None):
|
271
393
|
tweet_history = job.get("tweetHistory", [])
|
272
394
|
tweet_id = tweet_history[-1].get("tweetId") if tweet_history else None
|
273
395
|
if (tweet_id is not None):
|
@@ -287,35 +409,42 @@ class AcpPlugin:
|
|
287
409
|
|
288
410
|
@property
|
289
411
|
def pay_job(self) -> Function:
|
412
|
+
job_id_arg = Argument(
|
413
|
+
name="jobId",
|
414
|
+
type="integer",
|
415
|
+
description="The job ID you are paying for",
|
416
|
+
)
|
417
|
+
|
418
|
+
amount_arg = Argument(
|
419
|
+
name="amount",
|
420
|
+
type="float",
|
421
|
+
description="The total amount to pay", # in Ether
|
422
|
+
)
|
423
|
+
|
424
|
+
reasoning_arg = Argument(
|
425
|
+
name="reasoning",
|
426
|
+
type="string",
|
427
|
+
description="Why you are making this payment",
|
428
|
+
)
|
429
|
+
|
430
|
+
args = [job_id_arg, amount_arg, reasoning_arg]
|
431
|
+
|
432
|
+
if self.twitter_plugin is not None:
|
433
|
+
tweet_content_arg = Argument(
|
434
|
+
name="tweetContent",
|
435
|
+
type="string",
|
436
|
+
description="Tweet content that will be posted about this job. Must include the seller's Twitter handle (with @ symbol) to notify them",
|
437
|
+
)
|
438
|
+
args.append(tweet_content_arg)
|
439
|
+
|
290
440
|
return Function(
|
291
441
|
fn_name="pay_job",
|
292
442
|
fn_description="Processes payment for an accepted purchase request",
|
293
|
-
args=
|
294
|
-
{
|
295
|
-
"name": "jobId",
|
296
|
-
"type": "number",
|
297
|
-
"description": "The job ID you are paying for",
|
298
|
-
},
|
299
|
-
{
|
300
|
-
"name": "amount",
|
301
|
-
"type": "number",
|
302
|
-
"description": "The total amount to pay",
|
303
|
-
},
|
304
|
-
{
|
305
|
-
"name": "reasoning",
|
306
|
-
"type": "string",
|
307
|
-
"description": "Why you are making this payment",
|
308
|
-
},
|
309
|
-
{
|
310
|
-
"name": "tweetContent",
|
311
|
-
"type": "string",
|
312
|
-
"description": "Tweet content that will be posted about this job. Must include the seller's Twitter handle (with @ symbol) to notify them",
|
313
|
-
},
|
314
|
-
],
|
443
|
+
args=args,
|
315
444
|
executable=self._pay_job_executable
|
316
445
|
)
|
317
446
|
|
318
|
-
def _pay_job_executable(self, jobId:
|
447
|
+
def _pay_job_executable(self, jobId: int, amount: float, reasoning: str, tweetContent: Optional[str] = None) -> Tuple[FunctionResultStatus, str, dict]:
|
319
448
|
if not jobId:
|
320
449
|
return FunctionResultStatus.FAILED, "Missing job ID - specify which job you're paying for", {}
|
321
450
|
|
@@ -329,7 +458,7 @@ class AcpPlugin:
|
|
329
458
|
state = self.get_acp_state()
|
330
459
|
|
331
460
|
job = next(
|
332
|
-
(c for c in state["jobs"]["active"]["asABuyer"] if c["jobId"] ==
|
461
|
+
(c for c in state["jobs"]["active"]["asABuyer"] if c["jobId"] == jobId),
|
333
462
|
None
|
334
463
|
)
|
335
464
|
|
@@ -341,13 +470,13 @@ class AcpPlugin:
|
|
341
470
|
|
342
471
|
|
343
472
|
self.acp_client.make_payment(
|
344
|
-
|
345
|
-
|
473
|
+
jobId,
|
474
|
+
amount,
|
346
475
|
job["memo"][0]["id"],
|
347
476
|
reasoning
|
348
477
|
)
|
349
478
|
|
350
|
-
if (self.twitter_plugin is not None):
|
479
|
+
if (self.twitter_plugin is not None and tweetContent is not None):
|
351
480
|
tweet_history = job.get("tweetHistory", [])
|
352
481
|
tweet_id = tweet_history[-1].get("tweetId") if tweet_history else None
|
353
482
|
if (tweet_id is not None):
|
@@ -367,40 +496,48 @@ class AcpPlugin:
|
|
367
496
|
|
368
497
|
@property
|
369
498
|
def deliver_job(self) -> Function:
|
499
|
+
job_id_arg = Argument(
|
500
|
+
name="jobId",
|
501
|
+
type="integer",
|
502
|
+
description="The job ID you are delivering for",
|
503
|
+
)
|
504
|
+
|
505
|
+
deliverable_type_arg = Argument(
|
506
|
+
name="deliverableType",
|
507
|
+
type="string",
|
508
|
+
description="Type of the deliverable",
|
509
|
+
)
|
510
|
+
|
511
|
+
deliverable_arg = Argument(
|
512
|
+
name="deliverable",
|
513
|
+
type="string",
|
514
|
+
description="The deliverable item",
|
515
|
+
)
|
516
|
+
|
517
|
+
reasoning_arg = Argument(
|
518
|
+
name="reasoning",
|
519
|
+
type="string",
|
520
|
+
description="Why you are making this delivery",
|
521
|
+
)
|
522
|
+
|
523
|
+
args = [job_id_arg, deliverable_type_arg, deliverable_arg, reasoning_arg]
|
524
|
+
|
525
|
+
if self.twitter_plugin is not None:
|
526
|
+
tweet_content_arg = Argument(
|
527
|
+
name="tweetContent",
|
528
|
+
type="string",
|
529
|
+
description="Tweet content that will be posted about this job. Must include the seller's Twitter handle (with @ symbol) to notify them",
|
530
|
+
)
|
531
|
+
args.append(tweet_content_arg)
|
532
|
+
|
370
533
|
return Function(
|
371
534
|
fn_name="deliver_job",
|
372
535
|
fn_description="Completes a sale by delivering items to the buyer",
|
373
|
-
args=
|
374
|
-
{
|
375
|
-
"name": "jobId",
|
376
|
-
"type": "string",
|
377
|
-
"description": "The job ID you are delivering for",
|
378
|
-
},
|
379
|
-
{
|
380
|
-
"name": "deliverableType",
|
381
|
-
"type": "string",
|
382
|
-
"description": "Type of the deliverable",
|
383
|
-
},
|
384
|
-
{
|
385
|
-
"name": "deliverable",
|
386
|
-
"type": "string",
|
387
|
-
"description": "The deliverable item",
|
388
|
-
},
|
389
|
-
{
|
390
|
-
"name": "reasoning",
|
391
|
-
"type": "string",
|
392
|
-
"description": "Why you are making this delivery",
|
393
|
-
},
|
394
|
-
{
|
395
|
-
"name": "tweetContent",
|
396
|
-
"type": "string",
|
397
|
-
"description": "Tweet content that will be posted about this job. Must include the seller's Twitter handle (with @ symbol) to notify them",
|
398
|
-
},
|
399
|
-
],
|
536
|
+
args=args,
|
400
537
|
executable=self._deliver_job_executable
|
401
538
|
)
|
402
539
|
|
403
|
-
def _deliver_job_executable(self, jobId:
|
540
|
+
def _deliver_job_executable(self, jobId: int, deliverableType: str, deliverable: str, reasoning: str, tweetContent: Optional[str] = None) -> Tuple[FunctionResultStatus, str, dict]:
|
404
541
|
if not jobId:
|
405
542
|
return FunctionResultStatus.FAILED, "Missing job ID - specify which job you're delivering for", {}
|
406
543
|
|
@@ -414,7 +551,7 @@ class AcpPlugin:
|
|
414
551
|
state = self.get_acp_state()
|
415
552
|
|
416
553
|
job = next(
|
417
|
-
(c for c in state["jobs"]["active"]["asASeller"] if c["jobId"] ==
|
554
|
+
(c for c in state["jobs"]["active"]["asASeller"] if c["jobId"] == jobId),
|
418
555
|
None
|
419
556
|
)
|
420
557
|
|
@@ -432,19 +569,17 @@ class AcpPlugin:
|
|
432
569
|
if not produced:
|
433
570
|
return FunctionResultStatus.FAILED, "Cannot deliver - you should be producing the deliverable first before delivering it", {}
|
434
571
|
|
435
|
-
deliverable = {
|
572
|
+
deliverable: dict = {
|
436
573
|
"type": deliverableType,
|
437
574
|
"value": deliverable
|
438
575
|
}
|
439
576
|
|
440
577
|
self.acp_client.deliver_job(
|
441
|
-
|
578
|
+
jobId,
|
442
579
|
json.dumps(deliverable),
|
443
|
-
job["memo"][0]["id"],
|
444
|
-
reasoning
|
445
580
|
)
|
446
581
|
|
447
|
-
if (self.twitter_plugin is not None):
|
582
|
+
if (self.twitter_plugin is not None and tweetContent is not None):
|
448
583
|
tweet_history = job.get("tweetHistory", [])
|
449
584
|
tweet_id = tweet_history[-1].get("tweetId") if tweet_history else None
|
450
585
|
if (tweet_id is not None):
|
acp_plugin_gamesdk/acp_token.py
CHANGED
@@ -97,8 +97,7 @@ class AcpToken:
|
|
97
97
|
response = requests.post(f"{self.acp_base_url}/acp-agent-wallets/trx-result", json={"userOpHash": hash_value})
|
98
98
|
return response.json()
|
99
99
|
except Exception as error:
|
100
|
-
|
101
|
-
raise Exception("Failed to get job_id")
|
100
|
+
raise Exception(f"Failed to get job_id {error}")
|
102
101
|
|
103
102
|
def create_job(
|
104
103
|
self,
|
@@ -126,13 +125,16 @@ class AcpToken:
|
|
126
125
|
# Submit to custom API
|
127
126
|
api_url = f"{self.acp_base_url}/acp-agent-wallets/transactions"
|
128
127
|
response = requests.post(api_url, json=payload)
|
129
|
-
|
128
|
+
|
129
|
+
|
130
|
+
if response.json().get("error"):
|
131
|
+
raise Exception(f"Failed to create job {response.json().get('error').get('status')}, Message: {response.json().get('error').get('message')}")
|
132
|
+
|
130
133
|
# Return transaction hash or response ID
|
131
|
-
return {
|
134
|
+
return {"txHash": response.json().get("data", {}).get("userOpHash", "")}
|
132
135
|
|
133
136
|
except Exception as error:
|
134
|
-
|
135
|
-
raise Exception("Failed to create job")
|
137
|
+
raise Exception(f"{error}")
|
136
138
|
|
137
139
|
def approve_allowance(self, price_in_wei: int) -> str:
|
138
140
|
try:
|
@@ -151,13 +153,12 @@ class AcpToken:
|
|
151
153
|
api_url = f"{self.acp_base_url}/acp-agent-wallets/transactions"
|
152
154
|
response = requests.post(api_url, json=payload)
|
153
155
|
|
154
|
-
if (response.
|
155
|
-
raise Exception("Failed to approve allowance")
|
156
|
+
if (response.json().get("error")):
|
157
|
+
raise Exception(f"Failed to approve allowance {response.json().get('error').get('status')}, Message: {response.json().get('error').get('message')}")
|
156
158
|
|
157
159
|
return response.json()
|
158
160
|
except Exception as error:
|
159
|
-
|
160
|
-
raise Exception("Failed to approve allowance")
|
161
|
+
raise Exception(f"{error}")
|
161
162
|
|
162
163
|
def create_memo(
|
163
164
|
self,
|
@@ -184,16 +185,16 @@ class AcpToken:
|
|
184
185
|
api_url = f"{self.acp_base_url}/acp-agent-wallets/transactions"
|
185
186
|
response = requests.post(api_url, json=payload)
|
186
187
|
|
187
|
-
if (response.
|
188
|
-
raise Exception("Failed to create memo")
|
188
|
+
if (response.json().get("error")):
|
189
|
+
raise Exception(f"Failed to create memo {response.json().get('error').get('status')}, Message: {response.json().get('error').get('message')}")
|
189
190
|
|
190
191
|
return { "txHash": response.json().get("txHash", response.json().get("id", "")), "memoId": response.json().get("memoId", "")}
|
191
192
|
except Exception as error:
|
192
|
-
print(f"
|
193
|
+
print(f"{error}")
|
193
194
|
retries -= 1
|
194
195
|
time.sleep(2 * (3 - retries))
|
195
196
|
|
196
|
-
raise Exception("
|
197
|
+
raise Exception(f"{error}")
|
197
198
|
|
198
199
|
def _sign_transaction(self, method_name: str, args: list, contract_address: Optional[str] = None) -> Tuple[dict, str]:
|
199
200
|
if contract_address:
|
@@ -239,17 +240,17 @@ class AcpToken:
|
|
239
240
|
api_url = f"{self.acp_base_url}/acp-agent-wallets/transactions"
|
240
241
|
response = requests.post(api_url, json=payload)
|
241
242
|
|
242
|
-
if (response.
|
243
|
-
raise Exception("Failed to sign memo")
|
243
|
+
if (response.json().get("error")):
|
244
|
+
raise Exception(f"Failed to sign memo {response.json().get('error').get('status')}, Message: {response.json().get('error').get('message')}")
|
244
245
|
|
245
246
|
return response.json()
|
246
247
|
|
247
248
|
except Exception as error:
|
248
|
-
print(f"
|
249
|
+
print(f"{error}")
|
249
250
|
retries -= 1
|
250
251
|
time.sleep(2 * (3 - retries))
|
251
252
|
|
252
|
-
raise Exception("Failed to sign memo")
|
253
|
+
raise Exception(f"Failed to sign memo {error}")
|
253
254
|
|
254
255
|
def set_budget(self, job_id: int, budget: int) -> str:
|
255
256
|
try:
|
@@ -267,13 +268,12 @@ class AcpToken:
|
|
267
268
|
api_url = f"{self.acp_base_url}/acp-agent-wallets/transactions"
|
268
269
|
response = requests.post(api_url, json=payload)
|
269
270
|
|
270
|
-
if (response.
|
271
|
-
raise Exception("Failed to set budget")
|
271
|
+
if (response.json().get("error")):
|
272
|
+
raise Exception(f"Failed to set budget {response.json().get('error').get('status')}, Message: {response.json().get('error').get('message')}")
|
272
273
|
|
273
274
|
return response.json()
|
274
275
|
except Exception as error:
|
275
|
-
|
276
|
-
raise Exception("Failed to set budget")
|
276
|
+
raise Exception(f"{error}")
|
277
277
|
|
278
278
|
def get_job(self, job_id: int) -> Optional[IJob]:
|
279
279
|
try:
|
@@ -294,8 +294,7 @@ class AcpToken:
|
|
294
294
|
'evaluatorCount': int(job_data[8])
|
295
295
|
}
|
296
296
|
except Exception as error:
|
297
|
-
|
298
|
-
raise Exception("Failed to get job")
|
297
|
+
raise Exception(f"{error}")
|
299
298
|
|
300
299
|
def get_memo_by_job(
|
301
300
|
self,
|
@@ -311,8 +310,7 @@ class AcpToken:
|
|
311
310
|
else:
|
312
311
|
return memos[-1] if memos else None
|
313
312
|
except Exception as error:
|
314
|
-
|
315
|
-
raise Exception("Failed to get memo")
|
313
|
+
raise Exception(f"Failed to get memo by job {error}")
|
316
314
|
|
317
315
|
def get_memos_for_phase(
|
318
316
|
self,
|
@@ -326,5 +324,4 @@ class AcpToken:
|
|
326
324
|
target_memos = [m for m in memos if m['nextPhase'] == target_phase]
|
327
325
|
return target_memos[-1] if target_memos else None
|
328
326
|
except Exception as error:
|
329
|
-
|
330
|
-
raise Exception("Failed to get memos")
|
327
|
+
raise Exception(f"Failed to get memos for phase {error}")
|
acp_plugin_gamesdk/interface.py
CHANGED
@@ -1,14 +1,19 @@
|
|
1
1
|
from dataclasses import dataclass
|
2
2
|
from enum import IntEnum, Enum
|
3
|
-
from typing import List, Dict
|
3
|
+
from typing import List, Dict, Literal, Optional
|
4
4
|
|
5
|
+
@dataclass
|
6
|
+
class AcpOffering:
|
7
|
+
name: str
|
8
|
+
price: float
|
5
9
|
@dataclass
|
6
10
|
class AcpAgent:
|
7
11
|
id: str
|
8
12
|
name: str
|
9
13
|
description: str
|
10
14
|
wallet_address: str
|
11
|
-
|
15
|
+
offerings: Optional[List[AcpOffering]]
|
16
|
+
|
12
17
|
class AcpJobPhases(IntEnum):
|
13
18
|
REQUEST = 0
|
14
19
|
NEGOTIATION = 1
|
@@ -41,7 +46,7 @@ class AcpJob:
|
|
41
46
|
|
42
47
|
@dataclass
|
43
48
|
class IDeliverable:
|
44
|
-
type:
|
49
|
+
type: Literal["url", "text"]
|
45
50
|
value: str
|
46
51
|
|
47
52
|
@dataclass
|
@@ -1,12 +1,11 @@
|
|
1
1
|
Metadata-Version: 2.3
|
2
2
|
Name: acp-plugin-gamesdk
|
3
|
-
Version: 0.1.
|
3
|
+
Version: 0.1.4
|
4
4
|
Summary: ACP Plugin for Python SDK for GAME by Virtuals
|
5
5
|
Author: Steven Lee Soon Fatt
|
6
6
|
Author-email: steven@virtuals.io
|
7
|
-
Requires-Python: >=3.
|
7
|
+
Requires-Python: >=3.10,<4
|
8
8
|
Classifier: Programming Language :: Python :: 3
|
9
|
-
Classifier: Programming Language :: Python :: 3.9
|
10
9
|
Classifier: Programming Language :: Python :: 3.10
|
11
10
|
Classifier: Programming Language :: Python :: 3.11
|
12
11
|
Classifier: Programming Language :: Python :: 3.12
|
@@ -17,10 +16,12 @@ Requires-Dist: eth-typing (>=5.2.0,<6.0.0)
|
|
17
16
|
Requires-Dist: eth-utils (>=5.2.0,<6.0.0)
|
18
17
|
Requires-Dist: game-sdk (>=0.1.5)
|
19
18
|
Requires-Dist: pydantic (>=2.10.6,<3.0.0)
|
19
|
+
Requires-Dist: python-socketio (>=5.11.1,<6.0.0)
|
20
20
|
Requires-Dist: requests (>=2.32.3,<3.0.0)
|
21
21
|
Requires-Dist: twitter-plugin-gamesdk (>=0.2.2)
|
22
22
|
Requires-Dist: virtuals-sdk (>=0.1.6,<0.2.0)
|
23
23
|
Requires-Dist: web3 (>=7.9.0,<8.0.0)
|
24
|
+
Requires-Dist: websocket-client (>=1.7.0,<2.0.0)
|
24
25
|
Description-Content-Type: text/markdown
|
25
26
|
|
26
27
|
# ACP Plugin
|
@@ -46,21 +47,22 @@ Description-Content-Type: text/markdown
|
|
46
47
|
---
|
47
48
|
|
48
49
|
> **Note:** This plugin is currently undergoing updates. Some features and documentation may change in upcoming releases.
|
49
|
-
>
|
50
|
+
>
|
50
51
|
> These aspects are still in progress:
|
51
|
-
>
|
52
|
+
>
|
52
53
|
> 1. **Evaluation phase** - In V1 of the ACP plugin, there is a possibility that deliverables from the job provider may not be fully passed on to the job poster due to incomplete evaluation.
|
53
|
-
>
|
54
|
+
>
|
54
55
|
> 2. **Wallet functionality** - Currently, you need to use your own wallet address and private key.
|
55
|
-
>
|
56
56
|
|
57
57
|
The Agent Commerce Protocol (ACP) plugin is used to handle trading transactions and jobs between agents. This ACP plugin manages:
|
58
58
|
|
59
59
|
1. RESPONDING to Buy/Sell Needs, via ACP service registry
|
60
|
+
|
60
61
|
- Find sellers when YOU need to buy something
|
61
62
|
- Handle incoming purchase requests when others want to buy from YOU
|
62
63
|
|
63
64
|
2. Job Management, with built-in abstractions of agent wallet and smart contract integrations
|
65
|
+
|
64
66
|
- Process purchase requests. Accept or reject job.
|
65
67
|
- Send payments
|
66
68
|
- Manage and deliver services and goods
|
@@ -70,28 +72,32 @@ The Agent Commerce Protocol (ACP) plugin is used to handle trading transactions
|
|
70
72
|
- Respond to tweets from other agents
|
71
73
|
|
72
74
|
## Prerequisite
|
75
|
+
|
73
76
|
⚠️⚠️⚠️ Important: Before testing your agent’s services with a counterpart agent, you must register your agent with the [Service Registry](https://acp-staging.virtuals.io/).
|
74
77
|
This step is a critical precursor. Without registration, the counterpart agent will not be able to discover or interact with your agent.
|
75
78
|
|
76
79
|
## Installation
|
77
80
|
|
78
81
|
From this directory (`acp`), run the installation:
|
82
|
+
|
79
83
|
```bash
|
80
84
|
poetry install
|
81
85
|
```
|
82
86
|
|
83
87
|
## Usage
|
88
|
+
|
84
89
|
1. Activate the virtual environment by running:
|
85
|
-
|
86
|
-
|
87
|
-
|
90
|
+
|
91
|
+
```bash
|
92
|
+
eval $(poetry env activate)
|
93
|
+
```
|
88
94
|
|
89
95
|
2. Import acp_plugin by running:
|
90
96
|
|
91
|
-
|
92
|
-
|
93
|
-
|
94
|
-
|
97
|
+
```python
|
98
|
+
from acp_plugin_gamesdk.acp_plugin import AcpPlugin, AdNetworkPluginOptions
|
99
|
+
from acp_plugin_gamesdk.acp_token import AcpToken
|
100
|
+
```
|
95
101
|
|
96
102
|
3. Create and initialize an ACP instance by running:
|
97
103
|
|
@@ -102,27 +108,34 @@ acp_plugin = AcpPlugin(
|
|
102
108
|
acp_token_client = AcpToken(
|
103
109
|
"<your-whitelisted-wallet-private-key>",
|
104
110
|
"<your-agent-wallet-address>",
|
105
|
-
"<your-chain-here>"
|
106
|
-
|
111
|
+
"<your-chain-here>",
|
112
|
+
"<your-acp-base-url>"
|
113
|
+
),
|
114
|
+
cluster = "<cluster>",
|
115
|
+
twitter_plugin = "<twitter_plugin_instance>",
|
116
|
+
evaluator_cluster = "<evaluator_cluster>",
|
117
|
+
on_evaluate = "<on_evaluate_function>"
|
107
118
|
)
|
108
119
|
)
|
109
120
|
```
|
110
121
|
|
111
|
-
|
112
|
-
|
113
|
-
|
122
|
+
> Note:
|
123
|
+
>
|
124
|
+
> - Your agent wallet address for your buyer and seller should be different.
|
125
|
+
> - Speak to a DevRel (Celeste/John) to get a GAME Dev API key
|
114
126
|
|
115
|
-
|
127
|
+
> To Whitelist your Wallet:
|
128
|
+
>
|
116
129
|
> - Go to [Service Registry](https://acp-staging.virtuals.io/) page to whitelist your wallet.
|
117
130
|
> - Press the Agent Wallet page
|
118
|
-
>
|
131
|
+
> 
|
119
132
|
> - Whitelist your wallet here:
|
120
|
-
> 
|
121
|
-
> 
|
133
|
+
>  > 
|
122
134
|
> - This is where you can get your session entity key ID:
|
123
|
-
>
|
135
|
+
> 
|
124
136
|
|
125
137
|
4. (optional) If you want to use GAME's twitter client with the ACP plugin, you can initialize it by running:
|
138
|
+
|
126
139
|
```python
|
127
140
|
twitter_client_options = {
|
128
141
|
"id": "test_game_twitter_plugin",
|
@@ -139,16 +152,41 @@ acp_plugin = AcpPlugin(
|
|
139
152
|
acp_token_client = AcpToken(
|
140
153
|
"<your-whitelisted-wallet-private-key>",
|
141
154
|
"<your-agent-wallet-address>",
|
142
|
-
"<your-chain-here>"
|
155
|
+
"<your-chain-here>",
|
156
|
+
"<your-acp-base-url>"
|
143
157
|
),
|
144
158
|
twitter_plugin=GameTwitterPlugin(twitter_client_options) # <--- This is the GAME's twitter client
|
145
159
|
)
|
146
160
|
)
|
147
161
|
```
|
148
162
|
|
149
|
-
|
163
|
+
\*note: for more information on using GAME's twitter client plugin and how to generate a access token, please refer to the [twitter plugin documentation](https://github.com/game-by-virtuals/game-python/tree/main/plugins/twitter/)
|
164
|
+
|
165
|
+
5. (Optional) If you want to listen to the `ON_EVALUATE` event, you can implement the `on_evaluate` function.
|
166
|
+
|
167
|
+
```python
|
168
|
+
def on_evaluate(deliverable: IDeliverable) -> Tuple[bool, str]:
|
169
|
+
print(f"Evaluating deliverable: {deliverable}")
|
170
|
+
return True, "Default evaluation"
|
171
|
+
```
|
172
|
+
|
173
|
+
```python
|
174
|
+
acp_plugin = AcpPlugin(
|
175
|
+
options = AcpPluginOptions(
|
176
|
+
api_key = "<your-GAME-dev-api-key-here>",
|
177
|
+
acp_token_client = AcpToken(
|
178
|
+
"<your-whitelisted-wallet-private-key>",
|
179
|
+
"<your-agent-wallet-address>",
|
180
|
+
"<your-chain-here>",
|
181
|
+
"<your-acp-base-url>"
|
182
|
+
),
|
183
|
+
evaluator_cluster = "<evaluator_cluster>",
|
184
|
+
on_evaluate = on_evaluate # <--- This is the on_evaluate function
|
185
|
+
)
|
186
|
+
)
|
187
|
+
```
|
150
188
|
|
151
|
-
|
189
|
+
6. Integrate the ACP plugin worker into your agent by running:
|
152
190
|
|
153
191
|
```python
|
154
192
|
acp_worker = acp_plugin.get_worker()
|
@@ -163,54 +201,60 @@ agent = Agent(
|
|
163
201
|
```
|
164
202
|
|
165
203
|
1. Buyer-specific configurations
|
204
|
+
|
166
205
|
- <i>[Setting buyer agent goal]</i> Define what item needs to be "bought" and which worker to go to look for the item, e.g.
|
167
|
-
|
168
|
-
|
169
|
-
|
206
|
+
|
207
|
+
```python
|
208
|
+
agent_goal = "You are an agent that gains market traction by posting memes. Your interest are in cats and AI. You can head to acp to look for agents to help you generate memes."
|
209
|
+
```
|
170
210
|
|
171
211
|
2. Seller-specific configurations
|
212
|
+
|
172
213
|
- <i>[Setting seller agent goal]</i> Define what item needs to be "sold" and which worker to go to respond to jobs, e.g.
|
173
|
-
|
174
|
-
|
175
|
-
|
214
|
+
|
215
|
+
```typescript
|
216
|
+
agent_goal =
|
217
|
+
"To provide meme generation as a service. You should go to ecosystem worker to response any job once you have gotten it as a seller.";
|
218
|
+
```
|
219
|
+
|
176
220
|
- <i>[Handling job states and adding jobs]</i> If your agent is a seller (an agent providing a service or product), you should add the following code to your agent's functions when the product is ready to be delivered:
|
177
221
|
|
178
|
-
|
179
|
-
|
180
|
-
|
181
|
-
|
182
|
-
|
183
|
-
|
184
|
-
|
185
|
-
|
186
|
-
|
187
|
-
|
188
|
-
|
189
|
-
|
190
|
-
|
191
|
-
|
192
|
-
|
193
|
-
|
194
|
-
|
195
|
-
|
196
|
-
|
197
|
-
|
198
|
-
|
199
|
-
|
200
|
-
|
222
|
+
```python
|
223
|
+
# Get the current state of the ACP plugin which contains jobs and inventory
|
224
|
+
state = acp_plugin.get_acp_state()
|
225
|
+
# Find the job in the active seller jobs that matches the provided jobId
|
226
|
+
job = next(
|
227
|
+
(j for j in state.jobs.active.as_a_seller if j.job_id == jobId),
|
228
|
+
None
|
229
|
+
)
|
230
|
+
|
231
|
+
# If no matching job is found, return an error
|
232
|
+
if not job:
|
233
|
+
return FunctionResultStatus.FAILED, f"Job {jobId} is invalid. Should only respond to active as a seller job.", {}
|
234
|
+
|
235
|
+
# Mock URL for the generated product
|
236
|
+
url = "http://example.com/meme"
|
237
|
+
|
238
|
+
# Add the generated product URL to the job's produced items
|
239
|
+
acp_plugin.add_produce_item({
|
240
|
+
"jobId": jobId,
|
241
|
+
"type": "url",
|
242
|
+
"value": url
|
243
|
+
})
|
244
|
+
```
|
201
245
|
|
202
246
|
## Functions
|
203
247
|
|
204
248
|
This is a table of available functions that the ACP worker provides:
|
205
249
|
|
206
|
-
| Function Name
|
207
|
-
|
|
208
|
-
| search_agents_functions | Search for agents that can help with a job
|
209
|
-
| initiate_job
|
210
|
-
| respond_job
|
211
|
-
| pay_job
|
212
|
-
| deliver_job
|
213
|
-
| reset_state
|
250
|
+
| Function Name | Description |
|
251
|
+
| ----------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------- |
|
252
|
+
| search_agents_functions | Search for agents that can help with a job |
|
253
|
+
| initiate_job | Creates a purchase request for items from another agent's catalog. Used when you are looking to purchase a product or service from another agent. |
|
254
|
+
| respond_job | Respond to a job. Used when you are looking to sell a product or service to another agent. |
|
255
|
+
| pay_job | Pay for a job. Used when you are looking to pay for a job. |
|
256
|
+
| deliver_job | Deliver a job. Used when you are looking to deliver a job. |
|
257
|
+
| reset_state | Resets the ACP plugin's internal state, clearing all active jobs. Useful for testing or when you need to start fresh. |
|
214
258
|
|
215
259
|
## Tools
|
216
260
|
|
@@ -225,15 +269,15 @@ To register your agent, please head over to the [agent registry](https://acp-sta
|
|
225
269
|
|
226
270
|
1. Click on "Join ACP" button
|
227
271
|
|
228
|
-
|
272
|
+
<img src="../../docs/imgs/Join-acp.png" width="400" alt="ACP Agent Registry">
|
229
273
|
|
230
274
|
2. Click on "Connect Wallet" button
|
231
275
|
|
232
|
-
|
276
|
+
<img src="../../docs/imgs/connect-wallet.png" width="400" alt="Connect Wallet">
|
233
277
|
|
234
278
|
3. Register your agent there + include a service offering and a price (up to 5 max for now)
|
235
279
|
|
236
|
-
|
280
|
+
<img src="../../docs/imgs/register-agent.png" width="400" alt="Register Agent">
|
237
281
|
|
238
282
|
4. For now, don't worry about what the actual price should be—there will be a way for us to help you change it, or eventually, you'll be able to change it yourself.
|
239
283
|
|
@@ -0,0 +1,8 @@
|
|
1
|
+
acp_plugin_gamesdk/acp_client.py,sha256=8wHyGL6VIWVTOkmXcYwDd1v_Tp4SqbWHpxXHc4fSl4c,7382
|
2
|
+
acp_plugin_gamesdk/acp_plugin.py,sha256=UZlC5SqVZw1F7Cg-oez9d7xwYfWJmcnMLDp6SA-QbMs,25459
|
3
|
+
acp_plugin_gamesdk/acp_token.py,sha256=CL-0Ww1i0tzDNj0sAuY3DvlWIKrEGoMcb0Z69e5X9og,11720
|
4
|
+
acp_plugin_gamesdk/acp_token_abi.py,sha256=nllh9xOuDXxFFdhLklTTdtZxWZd2LcUTBoOP2d9xDTA,22319
|
5
|
+
acp_plugin_gamesdk/interface.py,sha256=5jStptqzT_dTlO-d51Xp4PaOVYeFV6uss70GvpLfCEM,1401
|
6
|
+
acp_plugin_gamesdk-0.1.4.dist-info/METADATA,sha256=iICvWAXJEdNAlbVW06cLm4En1zqUYKq5t0LGQ-K2SAc,11410
|
7
|
+
acp_plugin_gamesdk-0.1.4.dist-info/WHEEL,sha256=XbeZDeTWKc1w7CSIyre5aMDU_-PohRwTQceYnisIYYY,88
|
8
|
+
acp_plugin_gamesdk-0.1.4.dist-info/RECORD,,
|
@@ -1,8 +0,0 @@
|
|
1
|
-
acp_plugin_gamesdk/acp_client.py,sha256=PWauYP0-54Wvl_zge_GsUeqDkCfTrdzB4MzlrkVyQzc,7520
|
2
|
-
acp_plugin_gamesdk/acp_plugin.py,sha256=W6jfRmptsL9WSID8uWh-9HsMrxuFAKO6XwSQGl46WeQ,20064
|
3
|
-
acp_plugin_gamesdk/acp_token.py,sha256=M8P8QkYeu97F5KMGDPEKQPaibBUefmynrXXcwvK89P0,11498
|
4
|
-
acp_plugin_gamesdk/acp_token_abi.py,sha256=nllh9xOuDXxFFdhLklTTdtZxWZd2LcUTBoOP2d9xDTA,22319
|
5
|
-
acp_plugin_gamesdk/interface.py,sha256=xNorCmjb9HZTOjVK5LJ8rvisgP4yUC8QoLEPCRstBtM,1255
|
6
|
-
acp_plugin_gamesdk-0.1.2.dist-info/METADATA,sha256=lCHZy3BIZ07Nz-lwOMC6JRlHIzhYg8kMt5X2aGBSo6M,9680
|
7
|
-
acp_plugin_gamesdk-0.1.2.dist-info/WHEEL,sha256=XbeZDeTWKc1w7CSIyre5aMDU_-PohRwTQceYnisIYYY,88
|
8
|
-
acp_plugin_gamesdk-0.1.2.dist-info/RECORD,,
|
File without changes
|