acp-plugin-gamesdk 0.1.3__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.
@@ -47,23 +47,29 @@ class AcpClient:
47
47
  result = []
48
48
 
49
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
+
50
55
  result.append(
51
56
  AcpAgent(
52
57
  id=agent["id"],
53
58
  name=agent["name"],
54
59
  description=agent["description"],
55
60
  wallet_address=agent["walletAddress"],
56
- offerings=[AcpOffering(name=offering["name"], price=offering["price"]) for offering in agent["offerings"]]
61
+ offerings=offerings
57
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)
69
+
64
70
  tx_result = self.acp_token.create_job(
65
71
  provider_address=provider_address,
66
- evaluator_address=provider_address,
72
+ evaluator_address=evaluator_address,
67
73
  expire_at=expire_at
68
74
  )
69
75
 
@@ -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=int(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": int(job_id),
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(
@@ -136,7 +143,7 @@ class AcpClient:
136
143
  time.sleep(5)
137
144
 
138
145
  return self.acp_token.create_memo(
139
- job_id=int(job_id),
146
+ job_id=job_id,
140
147
  content=f"Job {job_id} accepted. {reasoning}",
141
148
  memo_type=MemoType.MESSAGE,
142
149
  is_secured=False,
@@ -144,7 +151,7 @@ class AcpClient:
144
151
  )
145
152
  else:
146
153
  return self.acp_token.create_memo(
147
- job_id=int(job_id),
154
+ job_id=job_id,
148
155
  content=f"Job {job_id} rejected. {reasoning}",
149
156
  memo_type=MemoType.MESSAGE,
150
157
  is_secured=False,
@@ -163,7 +170,7 @@ class AcpClient:
163
170
 
164
171
  def deliver_job(self, job_id: int, deliverable: str):
165
172
  return self.acp_token.create_memo(
166
- job_id=int(job_id),
173
+ job_id=job_id,
167
174
  content=deliverable,
168
175
  memo_type=MemoType.MESSAGE,
169
176
  is_secured=False,
@@ -192,14 +199,9 @@ class AcpClient:
192
199
 
193
200
  return response.json()
194
201
 
195
- def reset_state(self, wallet_address: str ) -> None:
196
- if not wallet_address:
197
- raise Exception("Wallet address is required")
198
-
199
- address = wallet_address
200
-
202
+ def reset_state(self) -> None:
201
203
  response = requests.delete(
202
- f"{self.base_url}/states/{address}",
204
+ f"{self.base_url}/states/{self.agent_wallet_address}",
203
205
  headers={"x-api-key": self.api_key}
204
206
  )
205
207
 
@@ -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
- acp_base_url: Optional[str] = None
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.acp_client = AcpClient(options.api_key, options.acp_token_client, options.acp_base_url)
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,17 +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 = options.acp_base_url if options.acp_base_url else "https://acpx-staging.virtuals.io/api"
48
-
49
-
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
+
50
123
 
51
124
  def add_produce_item(self, item: IInventory) -> None:
52
125
  self.produced_inventory.append(item)
53
126
 
54
127
  def reset_state(self) -> None:
55
- self.acp_client.reset_state(self.acp_client.agent_wallet_address)
128
+ self.acp_client.reset_state()
56
129
 
57
130
  def get_acp_state(self) -> Dict:
58
131
  server_state = self.acp_client.get_state()
@@ -75,7 +148,7 @@ class AcpPlugin:
75
148
  **(self.get_acp_state()),
76
149
  }
77
150
 
78
- data = WorkerConfig(
151
+ worker_config = WorkerConfig(
79
152
  id=self.id,
80
153
  worker_description=self.description,
81
154
  action_space=functions,
@@ -83,7 +156,7 @@ class AcpPlugin:
83
156
  instruction=data.get("instructions") if data else None
84
157
  )
85
158
 
86
- return data
159
+ return worker_config
87
160
 
88
161
  @property
89
162
  def agent_description(self) -> str:
@@ -112,7 +185,7 @@ class AcpPlugin:
112
185
  return FunctionResultStatus.FAILED, "No other trading agents found in the system. Please try again later when more agents are available.", {}
113
186
 
114
187
  return FunctionResultStatus.DONE, json.dumps({
115
- "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]} for agent in 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],
116
189
  "totalAgentsFound": len(agents),
117
190
  "timestamp": datetime.now().timestamp(),
118
191
  "note": "Use the walletAddress when initiating a job with your chosen trading partner."
@@ -120,74 +193,115 @@ class AcpPlugin:
120
193
 
121
194
  @property
122
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
+
123
208
  return Function(
124
209
  fn_name="search_agents",
125
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.",
126
- args=[
127
- {
128
- "name": "reasoning",
129
- "type": "string",
130
- "description": "Explain why you need to find trading partners at this time",
131
- },
132
- {
133
- "name": "keyword",
134
- "type": "string",
135
- "description": "Search for agents by name or description. Use this to find specific trading partners or products.",
136
- },
137
- ],
211
+ args=[reasoning_arg, keyword_arg],
138
212
  executable=self._search_agents_executable
139
213
  )
140
214
 
141
215
  @property
142
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
+
143
263
  return Function(
144
264
  fn_name="initiate_job",
145
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.",
146
- args=[
147
- {
148
- "name": "sellerWalletAddress",
149
- "type": "string",
150
- "description": "The seller's agent wallet address you want to buy from",
151
- },
152
- {
153
- "name": "price",
154
- "type": "string",
155
- "description": "Offered price for service",
156
- },
157
- {
158
- "name": "reasoning",
159
- "type": "string",
160
- "description": "Why you are making this purchase request",
161
- },
162
- {
163
- "name": "serviceRequirements",
164
- "type": "string",
165
- "description": "Detailed specifications for service-based items",
166
- },
167
- {
168
- "name": "tweetContent",
169
- "type": "string",
170
- "description": "Tweet content that will be posted about this job. Must include the seller's Twitter handle (with @ symbol) to notify them",
171
- },
172
- ],
266
+ args=args,
173
267
  executable=self._initiate_job_executable
174
268
  )
175
269
 
176
- def _initiate_job_executable(self, sellerWalletAddress: str, price: str, reasoning: str, serviceRequirements: str, tweetContent : str) -> Tuple[FunctionResultStatus, str, dict]:
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]:
177
271
  if not price:
178
272
  return FunctionResultStatus.FAILED, "Missing price - specify how much you're offering per unit", {}
179
-
273
+
274
+ if not reasoning:
275
+ return FunctionResultStatus.FAILED, "Missing reasoning - explain why you're making this purchase request", {}
276
+
180
277
  try:
181
278
  state = self.get_acp_state()
182
279
 
183
280
  if state["jobs"]["active"]["asABuyer"]:
184
281
  return FunctionResultStatus.FAILED, "You already have an active job as a buyer", {}
185
-
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
+
186
299
  # ... Rest of validation logic ...
187
300
  job_id = self.acp_client.create_job(
188
301
  sellerWalletAddress,
189
302
  float(price),
190
- serviceRequirements
303
+ serviceRequirements,
304
+ evaluatorAddress
191
305
  )
192
306
 
193
307
  if (self.twitter_plugin is not None and tweetContent is not None):
@@ -209,35 +323,42 @@ class AcpPlugin:
209
323
 
210
324
  @property
211
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
+
212
354
  return Function(
213
355
  fn_name="respond_to_job",
214
356
  fn_description="Accepts or rejects an incoming 'request' job",
215
- args=[
216
- {
217
- "name": "jobId",
218
- "type": "string",
219
- "description": "The job ID you are responding to",
220
- },
221
- {
222
- "name": "decision",
223
- "type": "string",
224
- "description": "Your response: 'ACCEPT' or 'REJECT'",
225
- },
226
- {
227
- "name": "reasoning",
228
- "type": "string",
229
- "description": "Why you made this decision",
230
- },
231
- {
232
- "name": "tweetContent",
233
- "type": "string",
234
- "description": "Tweet content that will be posted about this job. Must include the seller's Twitter handle (with @ symbol) to notify them",
235
- },
236
- ],
357
+ args=args,
237
358
  executable=self._respond_job_executable
238
359
  )
239
360
 
240
- def _respond_job_executable(self, jobId: str, decision: str, reasoning: str, tweetContent: str) -> Tuple[FunctionResultStatus, str, dict]:
361
+ def _respond_job_executable(self, jobId: int, decision: str, reasoning: str, tweetContent: Optional[str] = None) -> Tuple[FunctionResultStatus, str, dict]:
241
362
  if not jobId:
242
363
  return FunctionResultStatus.FAILED, "Missing job ID - specify which job you're responding to", {}
243
364
 
@@ -251,7 +372,7 @@ class AcpPlugin:
251
372
  state = self.get_acp_state()
252
373
 
253
374
  job = next(
254
- (c for c in state["jobs"]["active"]["asASeller"] if c["jobId"] == int(jobId)),
375
+ (c for c in state["jobs"]["active"]["asASeller"] if c["jobId"] == jobId),
255
376
  None
256
377
  )
257
378
 
@@ -262,13 +383,13 @@ class AcpPlugin:
262
383
  return FunctionResultStatus.FAILED, f"Cannot respond - job is in '{job['phase']}' phase, must be in 'request' phase", {}
263
384
 
264
385
  self.acp_client.response_job(
265
- int(jobId),
386
+ jobId,
266
387
  decision == "ACCEPT",
267
388
  job["memo"][0]["id"],
268
389
  reasoning
269
390
  )
270
391
 
271
- if (self.twitter_plugin is not None):
392
+ if (self.twitter_plugin is not None and tweetContent is not None):
272
393
  tweet_history = job.get("tweetHistory", [])
273
394
  tweet_id = tweet_history[-1].get("tweetId") if tweet_history else None
274
395
  if (tweet_id is not None):
@@ -288,35 +409,42 @@ class AcpPlugin:
288
409
 
289
410
  @property
290
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
+
291
440
  return Function(
292
441
  fn_name="pay_job",
293
442
  fn_description="Processes payment for an accepted purchase request",
294
- args=[
295
- {
296
- "name": "jobId",
297
- "type": "number",
298
- "description": "The job ID you are paying for",
299
- },
300
- {
301
- "name": "amount",
302
- "type": "number",
303
- "description": "The total amount to pay",
304
- },
305
- {
306
- "name": "reasoning",
307
- "type": "string",
308
- "description": "Why you are making this payment",
309
- },
310
- {
311
- "name": "tweetContent",
312
- "type": "string",
313
- "description": "Tweet content that will be posted about this job. Must include the seller's Twitter handle (with @ symbol) to notify them",
314
- },
315
- ],
443
+ args=args,
316
444
  executable=self._pay_job_executable
317
445
  )
318
446
 
319
- def _pay_job_executable(self, jobId: str, amount: str, reasoning: str, tweetContent: str) -> Tuple[FunctionResultStatus, str, dict]:
447
+ def _pay_job_executable(self, jobId: int, amount: float, reasoning: str, tweetContent: Optional[str] = None) -> Tuple[FunctionResultStatus, str, dict]:
320
448
  if not jobId:
321
449
  return FunctionResultStatus.FAILED, "Missing job ID - specify which job you're paying for", {}
322
450
 
@@ -330,7 +458,7 @@ class AcpPlugin:
330
458
  state = self.get_acp_state()
331
459
 
332
460
  job = next(
333
- (c for c in state["jobs"]["active"]["asABuyer"] if c["jobId"] == int(jobId)),
461
+ (c for c in state["jobs"]["active"]["asABuyer"] if c["jobId"] == jobId),
334
462
  None
335
463
  )
336
464
 
@@ -342,13 +470,13 @@ class AcpPlugin:
342
470
 
343
471
 
344
472
  self.acp_client.make_payment(
345
- int(jobId),
346
- float(amount),
473
+ jobId,
474
+ amount,
347
475
  job["memo"][0]["id"],
348
476
  reasoning
349
477
  )
350
478
 
351
- if (self.twitter_plugin is not None):
479
+ if (self.twitter_plugin is not None and tweetContent is not None):
352
480
  tweet_history = job.get("tweetHistory", [])
353
481
  tweet_id = tweet_history[-1].get("tweetId") if tweet_history else None
354
482
  if (tweet_id is not None):
@@ -368,40 +496,48 @@ class AcpPlugin:
368
496
 
369
497
  @property
370
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
+
371
533
  return Function(
372
534
  fn_name="deliver_job",
373
535
  fn_description="Completes a sale by delivering items to the buyer",
374
- args=[
375
- {
376
- "name": "jobId",
377
- "type": "string",
378
- "description": "The job ID you are delivering for",
379
- },
380
- {
381
- "name": "deliverableType",
382
- "type": "string",
383
- "description": "Type of the deliverable",
384
- },
385
- {
386
- "name": "deliverable",
387
- "type": "string",
388
- "description": "The deliverable item",
389
- },
390
- {
391
- "name": "reasoning",
392
- "type": "string",
393
- "description": "Why you are making this delivery",
394
- },
395
- {
396
- "name": "tweetContent",
397
- "type": "string",
398
- "description": "Tweet content that will be posted about this job. Must include the seller's Twitter handle (with @ symbol) to notify them",
399
- },
400
- ],
536
+ args=args,
401
537
  executable=self._deliver_job_executable
402
538
  )
403
539
 
404
- def _deliver_job_executable(self, jobId: str, deliverableType: str, deliverable: str, reasoning: str, tweetContent: str) -> Tuple[FunctionResultStatus, str, dict]:
540
+ def _deliver_job_executable(self, jobId: int, deliverableType: str, deliverable: str, reasoning: str, tweetContent: Optional[str] = None) -> Tuple[FunctionResultStatus, str, dict]:
405
541
  if not jobId:
406
542
  return FunctionResultStatus.FAILED, "Missing job ID - specify which job you're delivering for", {}
407
543
 
@@ -415,7 +551,7 @@ class AcpPlugin:
415
551
  state = self.get_acp_state()
416
552
 
417
553
  job = next(
418
- (c for c in state["jobs"]["active"]["asASeller"] if c["jobId"] == int(jobId)),
554
+ (c for c in state["jobs"]["active"]["asASeller"] if c["jobId"] == jobId),
419
555
  None
420
556
  )
421
557
 
@@ -433,17 +569,17 @@ class AcpPlugin:
433
569
  if not produced:
434
570
  return FunctionResultStatus.FAILED, "Cannot deliver - you should be producing the deliverable first before delivering it", {}
435
571
 
436
- deliverable = {
572
+ deliverable: dict = {
437
573
  "type": deliverableType,
438
574
  "value": deliverable
439
575
  }
440
576
 
441
577
  self.acp_client.deliver_job(
442
- int(jobId),
578
+ jobId,
443
579
  json.dumps(deliverable),
444
580
  )
445
581
 
446
- if (self.twitter_plugin is not None):
582
+ if (self.twitter_plugin is not None and tweetContent is not None):
447
583
  tweet_history = job.get("tweetHistory", [])
448
584
  tweet_id = tweet_history[-1].get("tweetId") if tweet_history else None
449
585
  if (tweet_id is not None):
@@ -1,6 +1,6 @@
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
5
  @dataclass
6
6
  class AcpOffering:
@@ -12,7 +12,7 @@ class AcpAgent:
12
12
  name: str
13
13
  description: str
14
14
  wallet_address: str
15
- offerings: List[AcpOffering]
15
+ offerings: Optional[List[AcpOffering]]
16
16
 
17
17
  class AcpJobPhases(IntEnum):
18
18
  REQUEST = 0
@@ -46,7 +46,7 @@ class AcpJob:
46
46
 
47
47
  @dataclass
48
48
  class IDeliverable:
49
- type: str
49
+ type: Literal["url", "text"]
50
50
  value: str
51
51
 
52
52
  @dataclass
@@ -1,12 +1,11 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: acp-plugin-gamesdk
3
- Version: 0.1.3
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.9,<4
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
- ```bash
86
- eval $(poetry env activate)
87
- ```
90
+
91
+ ```bash
92
+ eval $(poetry env activate)
93
+ ```
88
94
 
89
95
  2. Import acp_plugin by running:
90
96
 
91
- ```python
92
- from acp_plugin_gamesdk.acp_plugin import AcpPlugin, AdNetworkPluginOptions
93
- from acp_plugin_gamesdk.acp_token import AcpToken
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
- > Note:
112
- > - Your agent wallet address for your buyer and seller should be different.
113
- > - Speak to a DevRel (Celeste/John) to get a GAME Dev API key
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
- > To Whitelist your Wallet:
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
- > ![Agent Wallet Page](../../docs/imgs/agent-wallet-page.png)
131
+ > ![Agent Wallet Page](../../docs/imgs/agent-wallet-page.png)
119
132
  > - Whitelist your wallet here:
120
- > ![Whitelist Wallet](../../docs/imgs/whitelist-wallet.png)
121
- > ![Whitelist Wallet](../../docs/imgs/whitelist-wallet-info.png)
133
+ > ![Whitelist Wallet](../../docs/imgs/whitelist-wallet.png) > ![Whitelist Wallet](../../docs/imgs/whitelist-wallet-info.png)
122
134
  > - This is where you can get your session entity key ID:
123
- > ![Session Entity ID](../../docs/imgs/session-entity-id-location.png)
135
+ > ![Session Entity ID](../../docs/imgs/session-entity-id-location.png)
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
- *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/)
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
- 5. Integrate the ACP plugin worker into your agent by running:
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
- ```python
168
- 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."
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
- ```typescript
174
- agent_goal = "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."
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
- ```python
179
- # Get the current state of the ACP plugin which contains jobs and inventory
180
- state = acp_plugin.get_acp_state()
181
- # Find the job in the active seller jobs that matches the provided jobId
182
- job = next(
183
- (j for j in state.jobs.active.as_a_seller if j.job_id == int(jobId)),
184
- None
185
- )
186
-
187
- # If no matching job is found, return an error
188
- if not job:
189
- return FunctionResultStatus.FAILED, f"Job {jobId} is invalid. Should only respond to active as a seller job.", {}
190
-
191
- # Mock URL for the generated product
192
- url = "http://example.com/meme"
193
-
194
- # Add the generated product URL to the job's produced items
195
- acp_plugin.add_produce_item({
196
- "jobId": int(jobId),
197
- "type": "url",
198
- "value": url
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 | Description |
207
- | ------------- | ------------- |
208
- | search_agents_functions | Search for agents that can help with a job |
209
- | 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. |
210
- | respond_job | Respond to a job. Used when you are looking to sell a product or service to another agent. |
211
- | pay_job | Pay for a job. Used when you are looking to pay for a job. |
212
- | deliver_job | Deliver a job. Used when you are looking to deliver a job. |
213
- | reset_state | Resets the ACP plugin's internal state, clearing all active jobs. Useful for testing or when you need to start fresh. |
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
- <img src="../../docs/imgs/Join-acp.png" width="400" alt="ACP Agent Registry">
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
- <img src="../../docs/imgs/connect-wallet.png" width="400" alt="Connect Wallet">
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
- <img src="../../docs/imgs/register-agent.png" width="400" alt="Register Agent">
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=1mHE6h3E4DET-INSxw6O5REAX1drRpiAxphJt1EFxxQ,7320
2
- acp_plugin_gamesdk/acp_plugin.py,sha256=Cr7fsTXIeaww9CENmx0KPN65lf6KRir-n8jpVPi5ksE,20288
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=HxGHUc1VhdTGpFNk1JDwwb9n73IZdI08iDJttB0qB_s,1353
6
- acp_plugin_gamesdk-0.1.3.dist-info/METADATA,sha256=aHLfn_gLmFaKjyJrvzzIQ90UoZfmX9SK_bu7giWn1Bo,9691
7
- acp_plugin_gamesdk-0.1.3.dist-info/WHEEL,sha256=XbeZDeTWKc1w7CSIyre5aMDU_-PohRwTQceYnisIYYY,88
8
- acp_plugin_gamesdk-0.1.3.dist-info/RECORD,,