acp-plugin-gamesdk 0.1.17__py3-none-any.whl → 0.1.19__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.
@@ -13,12 +13,13 @@ from dacite import from_dict, Config
13
13
 
14
14
 
15
15
  class AcpClient:
16
- def __init__(self, api_key: str, acp_token: AcpToken, acp_base_url: Optional[str] = None):
17
- self.base_url = "https://sdk-dev.game.virtuals.io/acp"
16
+ def __init__(self, api_key: str, acp_token: AcpToken):
18
17
  self.api_key = api_key
19
18
  self.acp_token = acp_token
20
19
  self.web3 = Web3()
21
- self.acp_base_url = acp_base_url if acp_base_url else "https://acpx-staging.virtuals.io/api"
20
+
21
+ self.acp_base_url = self.acp_token.acp_base_url
22
+ self.base_url = self.acp_token.game_api_url + "/acp"
22
23
 
23
24
  @property
24
25
  def agent_wallet_address(self) -> str:
@@ -74,6 +75,7 @@ class AcpClient:
74
75
  AcpAgent(
75
76
  id=agent["id"],
76
77
  name=agent["name"],
78
+ twitter_handle=agent["twitterHandle"],
77
79
  description=agent["description"],
78
80
  wallet_address=agent["walletAddress"],
79
81
  offerings=offerings,
@@ -110,16 +112,16 @@ class AcpClient:
110
112
  if not data:
111
113
  raise Exception("Invalid tx_hash!")
112
114
 
113
- if (data.get("status") == "retry"):
115
+ if data.get("status") == "retry":
114
116
  raise Exception("Transaction failed, retrying...")
115
117
 
116
- if (data.get("status") == "failed"):
118
+ if data.get("status") == "failed":
117
119
  break
118
120
 
119
- if (data.get("status") == "success"):
121
+ if data.get("status") == "success":
120
122
  job_id = int(data.get("result").get("jobId"))
121
123
 
122
- if (job_id is not None and job_id != ""):
124
+ if job_id is not None and job_id != "":
123
125
  break
124
126
 
125
127
  except Exception as e:
@@ -130,7 +132,7 @@ class AcpClient:
130
132
  else:
131
133
  raise
132
134
 
133
- if (job_id is None or job_id == ""):
135
+ if job_id is None or job_id == "":
134
136
  raise Exception("Failed to create job")
135
137
 
136
138
  self.acp_token.create_memo(
@@ -262,3 +264,39 @@ class AcpClient:
262
264
  f"Response status code: {response.status_code}\n"
263
265
  f"Response description: {response.text}\n"
264
266
  )
267
+
268
+ def get_agent_by_wallet_address(self, wallet_address: str) -> AcpAgent:
269
+ url = f"{self.acp_base_url}/agents?filters[walletAddress]={wallet_address}"
270
+
271
+ response = requests.get(
272
+ url,
273
+ )
274
+
275
+ if response.status_code != 200:
276
+ raise Exception(
277
+ f"Failed to get agent: {response.status_code} {response.text}"
278
+ )
279
+
280
+ response_json = response.json()
281
+
282
+ result = []
283
+
284
+ for agent in response_json.get("data", []):
285
+ if agent["offerings"]:
286
+ offerings = [AcpOffering(name=offering["name"], price=offering["price"]) for offering in agent["offerings"]]
287
+ else:
288
+ offerings = None
289
+
290
+ result.append(
291
+ AcpAgent(
292
+ id=agent["id"],
293
+ name=agent["name"],
294
+ twitter_handle=agent["twitterHandle"],
295
+ description=agent["description"],
296
+ wallet_address=agent["walletAddress"],
297
+ offerings=offerings,
298
+ score=0,
299
+ explanation=""
300
+ )
301
+ )
302
+ return result[0]
@@ -42,7 +42,7 @@ class AcpPlugin:
42
42
  def __init__(self, options: AcpPluginOptions):
43
43
  print("Initializing AcpPlugin")
44
44
  self.acp_token_client = options.acp_token_client
45
- self.acp_client = AcpClient(options.api_key, options.acp_token_client, options.acp_token_client.acp_base_url)
45
+ self.acp_client = AcpClient(options.api_key, options.acp_token_client)
46
46
  self.id = "acp_worker"
47
47
  self.name = "ACP Worker"
48
48
  self.description = """
@@ -63,24 +63,27 @@ class AcpPlugin:
63
63
  self.cluster = options.cluster
64
64
  self.evaluator_cluster = options.evaluator_cluster
65
65
  self.twitter_plugin = None
66
- if (options.twitter_plugin is not None):
66
+ if options.twitter_plugin is not None:
67
67
  self.twitter_plugin = options.twitter_plugin
68
68
 
69
69
  self.produced_inventory: List[IInventory] = []
70
- 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"
70
+ self.acp_base_url = self.acp_token_client.acp_base_url
71
71
  if options.on_evaluate is not None or options.on_phase_change is not None:
72
72
  print("Initializing socket")
73
73
  self.socket = None
74
74
  if options.on_evaluate is not None:
75
75
  self.on_evaluate = options.on_evaluate
76
76
  if options.on_phase_change is not None:
77
- self.on_phase_change = options.on_phase_change
78
- self.initializeSocket()
77
+ def phase_change_wrapper(job : AcpJob):
78
+ job["getAgentByWalletAddress"] = self.acp_client.get_agent_by_wallet_address
79
+ return options.on_phase_change(job)
80
+ self.on_phase_change = phase_change_wrapper
81
+ self.initialize_socket()
79
82
  self.job_expiry_duration_mins = options.job_expiry_duration_mins if options.job_expiry_duration_mins is not None else 1440
80
83
 
81
84
 
82
85
 
83
- def initializeSocket(self) -> Tuple[bool, str]:
86
+ def initialize_socket(self) -> Tuple[bool, str]:
84
87
  """
85
88
  Initialize socket connection for real-time communication.
86
89
  Returns a tuple of (success, message).
@@ -94,9 +97,9 @@ class AcpPlugin:
94
97
  }
95
98
 
96
99
  # Connect socket to GAME SDK dev server
97
- self.socket.connect("https://sdk-dev.game.virtuals.io", auth=self.socket.auth)
100
+ self.socket.connect(self.acp_client.base_url, auth=self.socket.auth)
98
101
 
99
- if (self.socket.connected):
102
+ if self.socket.connected:
100
103
  self.socket.emit(SocketEvents["JOIN_EVALUATOR_ROOM"], self.acp_token_client.agent_wallet_address)
101
104
 
102
105
 
@@ -115,7 +118,6 @@ class AcpPlugin:
115
118
  @self.socket.on(SocketEvents["ON_PHASE_CHANGE"])
116
119
  def on_phase_change(data):
117
120
  if hasattr(self, 'on_phase_change') and self.on_phase_change:
118
- print(f"on_phase_change: {data}")
119
121
  self.on_phase_change(data)
120
122
 
121
123
  # Set up cleanup function for graceful shutdown
@@ -128,7 +130,7 @@ class AcpPlugin:
128
130
 
129
131
 
130
132
 
131
- def signal_handler(sig, frame):
133
+ def signal_handler(_sig, _frame):
132
134
  cleanup()
133
135
  sys.exit(0)
134
136
 
@@ -168,7 +170,7 @@ class AcpPlugin:
168
170
  self.deliver_job,
169
171
  ]
170
172
 
171
- def get_environment(function_result, current_state) -> Dict[str, Any]:
173
+ def get_environment(_function_result, _current_state) -> Dict[str, Any]:
172
174
  environment = data.get_environment() if hasattr(data, "get_environment") else {}
173
175
  return {
174
176
  **environment,
@@ -219,6 +221,7 @@ class AcpPlugin:
219
221
  {
220
222
  "id": agent.id,
221
223
  "name": agent.name,
224
+ "twitter_handle": agent.twitter_handle,
222
225
  "description": agent.description,
223
226
  "wallet_address": agent.wallet_address,
224
227
  "offerings": (
@@ -266,7 +269,7 @@ class AcpPlugin:
266
269
  @property
267
270
  def initiate_job(self) -> Function:
268
271
  seller_wallet_address_arg = Argument(
269
- name="sellerWalletAddress",
272
+ name="seller_wallet_address",
270
273
  type="string",
271
274
  description="The seller's agent wallet address you want to buy from",
272
275
  )
@@ -284,19 +287,19 @@ class AcpPlugin:
284
287
  )
285
288
 
286
289
  service_requirements_arg = Argument(
287
- name="serviceRequirements",
290
+ name="service_requirements",
288
291
  type="string",
289
292
  description="Detailed specifications for service-based items",
290
293
  )
291
294
 
292
295
  require_evaluation_arg = Argument(
293
- name="requireEvaluation",
296
+ name="require_evaluation",
294
297
  type="boolean",
295
298
  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.",
296
299
  )
297
300
 
298
301
  evaluator_keyword_arg = Argument(
299
- name="evaluatorKeyword",
302
+ name="evaluator_keyword",
300
303
  type="string",
301
304
  description="Keyword to search for a evaluator",
302
305
  )
@@ -305,7 +308,7 @@ class AcpPlugin:
305
308
 
306
309
  if hasattr(self, 'twitter_plugin') and self.twitter_plugin is not None:
307
310
  tweet_content_arg = Argument(
308
- name="tweetContent",
311
+ name="tweet_content",
309
312
  type="string",
310
313
  description="Tweet content that will be posted about this job. Must include the seller's Twitter handle (with @ symbol) to notify them",
311
314
  )
@@ -318,11 +321,11 @@ class AcpPlugin:
318
321
  executable=self._initiate_job_executable
319
322
  )
320
323
 
321
- def _initiate_job_executable(self, sellerWalletAddress: str, price: str, reasoning: str, serviceRequirements: str, requireEvaluation: str, evaluatorKeyword: str, tweetContent: Optional[str] = None) -> Tuple[FunctionResultStatus, str, dict]:
322
- if isinstance(requireEvaluation, str):
323
- require_evaluation = requireEvaluation.lower() == 'true'
324
- elif isinstance(requireEvaluation, bool):
325
- require_evaluation = requireEvaluation
324
+ def _initiate_job_executable(self, seller_wallet_address: str, price: str, reasoning: str, service_requirements: str, require_evaluation: str, evaluator_keyword: str, tweet_content: Optional[str] = None) -> Tuple[FunctionResultStatus, str, dict]:
325
+ if isinstance(require_evaluation, str):
326
+ require_evaluation = require_evaluation.lower() == 'true'
327
+ elif isinstance(require_evaluation, bool):
328
+ require_evaluation = require_evaluation
326
329
  else:
327
330
  require_evaluation = False
328
331
 
@@ -337,51 +340,51 @@ class AcpPlugin:
337
340
 
338
341
  existing_job = next(
339
342
  (job for job in state["jobs"]["active"]["asABuyer"]
340
- if job["providerAddress"] == sellerWalletAddress),
343
+ if job["providerAddress"] == seller_wallet_address),
341
344
  None
342
345
  )
343
346
 
344
347
  if existing_job:
345
348
  return FunctionResultStatus.FAILED, f"You already have an active job as a buyer with {existing_job['providerAddress']} - complete the current job before initiating a new one", {}
346
349
 
347
- if not sellerWalletAddress:
350
+ if not seller_wallet_address:
348
351
  return FunctionResultStatus.FAILED, "Missing seller wallet address - specify the agent you want to buy from", {}
349
352
 
350
- if require_evaluation and not evaluatorKeyword:
353
+ if require_evaluation and not evaluator_keyword:
351
354
  return FunctionResultStatus.FAILED, "Missing validator keyword - provide a keyword to search for a validator", {}
352
355
 
353
- evaluatorAddress = self.acp_token_client.get_agent_wallet_address()
356
+ evaluator_address = self.acp_token_client.get_agent_wallet_address()
354
357
 
355
358
  if require_evaluation:
356
- validators = self.acp_client.browse_agents(self.evaluator_cluster, evaluatorKeyword, rerank=True, top_k=1)
359
+ validators = self.acp_client.browse_agents(self.evaluator_cluster, evaluator_keyword, rerank=True, top_k=1)
357
360
 
358
361
  if len(validators) == 0:
359
362
  return FunctionResultStatus.FAILED, "No evaluator found - try a different keyword", {}
360
-
361
- evaluatorAddress = validators[0].wallet_address
363
+
364
+ evaluator_address = validators[0].wallet_address
362
365
 
363
366
  # ... Rest of validation logic ...
364
367
  expired_at = datetime.now(timezone.utc) + timedelta(minutes=self.job_expiry_duration_mins)
365
368
  job_id = self.acp_client.create_job(
366
- sellerWalletAddress,
369
+ seller_wallet_address,
367
370
  float(price),
368
- serviceRequirements,
369
- evaluatorAddress,
371
+ service_requirements,
372
+ evaluator_address,
370
373
  expired_at
371
374
  )
372
-
373
- if (hasattr(self, 'twitter_plugin') and self.twitter_plugin is not None and tweetContent is not None):
375
+
376
+ if hasattr(self, 'twitter_plugin') and self.twitter_plugin is not None and tweet_content is not None:
374
377
  post_tweet_fn = self.twitter_plugin.get_function('post_tweet')
375
- tweet_id = post_tweet_fn(tweetContent, None).get('data', {}).get('id')
376
- if (tweet_id is not None):
377
- self.acp_client.add_tweet(job_id, tweet_id, tweetContent)
378
+ tweet_id = post_tweet_fn(tweet_content).get('data', {}).get('id')
379
+ if tweet_id is not None:
380
+ self.acp_client.add_tweet(job_id, tweet_id, tweet_content)
378
381
  print("Tweet has been posted")
379
382
 
380
383
  return FunctionResultStatus.DONE, json.dumps({
381
384
  "jobId": job_id,
382
- "sellerWalletAddress": sellerWalletAddress,
385
+ "sellerWalletAddress": seller_wallet_address,
383
386
  "price": float(price),
384
- "serviceRequirements": serviceRequirements,
387
+ "serviceRequirements": service_requirements,
385
388
  "timestamp": datetime.now().timestamp(),
386
389
  }), {}
387
390
  except Exception as e:
@@ -391,7 +394,7 @@ class AcpPlugin:
391
394
  @property
392
395
  def respond_job(self) -> Function:
393
396
  job_id_arg = Argument(
394
- name="jobId",
397
+ name="job_id",
395
398
  type="integer",
396
399
  description="The job ID you are responding to",
397
400
  )
@@ -412,9 +415,9 @@ class AcpPlugin:
412
415
 
413
416
  if hasattr(self, 'twitter_plugin') and self.twitter_plugin is not None:
414
417
  tweet_content_arg = Argument(
415
- name="tweetContent",
418
+ name="tweet_content",
416
419
  type="string",
417
- description="Tweet content that will be posted about this job. Must include the seller's Twitter handle (with @ symbol) to notify them",
420
+ description="Tweet content about your decision for the specific job. MUST NOT TAG THE BUYER. This is to avoid spamming the buyer's feed with your decision.",
418
421
  )
419
422
  args.append(tweet_content_arg)
420
423
 
@@ -425,8 +428,8 @@ class AcpPlugin:
425
428
  executable=self._respond_job_executable
426
429
  )
427
430
 
428
- def _respond_job_executable(self, jobId: int, decision: str, reasoning: str, tweetContent: Optional[str] = None) -> Tuple[FunctionResultStatus, str, dict]:
429
- if not jobId:
431
+ def _respond_job_executable(self, job_id: int, decision: str, reasoning: str, tweet_content: Optional[str] = None) -> Tuple[FunctionResultStatus, str, dict]:
432
+ if not job_id:
430
433
  return FunctionResultStatus.FAILED, "Missing job ID - specify which job you're responding to", {}
431
434
 
432
435
  if not decision or decision not in ["ACCEPT", "REJECT"]:
@@ -439,7 +442,7 @@ class AcpPlugin:
439
442
  state = self.get_acp_state()
440
443
 
441
444
  job = next(
442
- (c for c in state["jobs"]["active"]["asASeller"] if c["jobId"] == jobId),
445
+ (c for c in state["jobs"]["active"]["asASeller"] if c["jobId"] == job_id),
443
446
  None
444
447
  )
445
448
 
@@ -450,24 +453,16 @@ class AcpPlugin:
450
453
  return FunctionResultStatus.FAILED, f"Cannot respond - job is in '{job['phase']}' phase, must be in 'request' phase", {}
451
454
 
452
455
  self.acp_client.response_job(
453
- jobId,
456
+ job_id,
454
457
  decision == "ACCEPT",
455
458
  job["memo"][0]["id"],
456
459
  reasoning
457
460
  )
458
-
459
- if (hasattr(self, 'twitter_plugin') and self.twitter_plugin is not None and tweetContent is not None):
460
- tweet_history = job.get("tweetHistory", [])
461
- tweet_id = tweet_history[-1].get("tweetId") if tweet_history else None
462
- if (tweet_id is not None):
463
- reply_tweet_fn = self.twitter_plugin.get_function('reply_tweet')
464
- tweet_id = reply_tweet_fn(tweet_id,tweetContent, None).get('data', {}).get('id')
465
- if (tweet_id is not None):
466
- self.acp_client.add_tweet(jobId ,tweet_id, tweetContent)
467
- print("Tweet has been posted")
461
+
462
+ self._reply_tweet(job, tweet_content)
468
463
 
469
464
  return FunctionResultStatus.DONE, json.dumps({
470
- "jobId": jobId,
465
+ "jobId": job_id,
471
466
  "decision": decision,
472
467
  "timestamp": datetime.now().timestamp()
473
468
  }), {}
@@ -477,7 +472,7 @@ class AcpPlugin:
477
472
  @property
478
473
  def pay_job(self) -> Function:
479
474
  job_id_arg = Argument(
480
- name="jobId",
475
+ name="job_id",
481
476
  type="integer",
482
477
  description="The job ID you are paying for",
483
478
  )
@@ -498,9 +493,9 @@ class AcpPlugin:
498
493
 
499
494
  if hasattr(self, 'twitter_plugin') and self.twitter_plugin is not None:
500
495
  tweet_content_arg = Argument(
501
- name="tweetContent",
496
+ name="tweet_content",
502
497
  type="string",
503
- description="Tweet content that will be posted about this job. Must include the seller's Twitter handle (with @ symbol) to notify them",
498
+ description="Tweet content about your payment for the specific job. MUST NOT TAG THE BUYER. This is to avoid spamming the buyer's feed with your payment.",
504
499
  )
505
500
  args.append(tweet_content_arg)
506
501
 
@@ -511,8 +506,8 @@ class AcpPlugin:
511
506
  executable=self._pay_job_executable
512
507
  )
513
508
 
514
- def _pay_job_executable(self, jobId: int, amount: float, reasoning: str, tweetContent: Optional[str] = None) -> Tuple[FunctionResultStatus, str, dict]:
515
- if not jobId:
509
+ def _pay_job_executable(self, job_id: int, amount: float, reasoning: str, tweet_content: Optional[str] = None) -> Tuple[FunctionResultStatus, str, dict]:
510
+ if not job_id:
516
511
  return FunctionResultStatus.FAILED, "Missing job ID - specify which job you're paying for", {}
517
512
 
518
513
  if not amount:
@@ -525,7 +520,7 @@ class AcpPlugin:
525
520
  state = self.get_acp_state()
526
521
 
527
522
  job = next(
528
- (c for c in state["jobs"]["active"]["asABuyer"] if c["jobId"] == jobId),
523
+ (c for c in state["jobs"]["active"]["asABuyer"] if c["jobId"] == job_id),
529
524
  None
530
525
  )
531
526
 
@@ -537,24 +532,16 @@ class AcpPlugin:
537
532
 
538
533
 
539
534
  self.acp_client.make_payment(
540
- jobId,
535
+ job_id,
541
536
  amount,
542
537
  job["memo"][0]["id"],
543
538
  reasoning
544
539
  )
545
-
546
- if (hasattr(self, 'twitter_plugin') and self.twitter_plugin is not None and tweetContent is not None):
547
- tweet_history = job.get("tweetHistory", [])
548
- tweet_id = tweet_history[-1].get("tweetId") if tweet_history else None
549
- if (tweet_id is not None):
550
- reply_tweet_fn = self.twitter_plugin.get_function('reply_tweet')
551
- tweet_id = reply_tweet_fn(tweet_id,tweetContent, None).get('data', {}).get('id')
552
- if (tweet_id is not None):
553
- self.acp_client.add_tweet(jobId ,tweet_id, tweetContent)
554
- print("Tweet has been posted")
540
+
541
+ self._reply_tweet(job, tweet_content)
555
542
 
556
543
  return FunctionResultStatus.DONE, json.dumps({
557
- "jobId": jobId,
544
+ "jobId": job_id,
558
545
  "amountPaid": amount,
559
546
  "timestamp": datetime.now().timestamp()
560
547
  }), {}
@@ -565,17 +552,11 @@ class AcpPlugin:
565
552
  @property
566
553
  def deliver_job(self) -> Function:
567
554
  job_id_arg = Argument(
568
- name="jobId",
555
+ name="job_id",
569
556
  type="integer",
570
557
  description="The job ID you are delivering for",
571
558
  )
572
559
 
573
- deliverable_type_arg = Argument(
574
- name="deliverableType",
575
- type="string",
576
- description="Type of the deliverable",
577
- )
578
-
579
560
  deliverable_arg = Argument(
580
561
  name="deliverable",
581
562
  type="string",
@@ -588,13 +569,13 @@ class AcpPlugin:
588
569
  description="Why you are making this delivery",
589
570
  )
590
571
 
591
- args = [job_id_arg, deliverable_type_arg, deliverable_arg, reasoning_arg]
572
+ args = [job_id_arg, deliverable_arg, reasoning_arg]
592
573
 
593
574
  if hasattr(self, 'twitter_plugin') and self.twitter_plugin is not None:
594
575
  tweet_content_arg = Argument(
595
- name="tweetContent",
576
+ name="tweet_content",
596
577
  type="string",
597
- description="Tweet content that will be posted about this job. Must include the seller's Twitter handle (with @ symbol) to notify them",
578
+ description="Tweet content about your delivery for the specific job. MUST NOT TAG THE BUYER. This is to avoid spamming the buyer's feed with your delivery.",
598
579
  )
599
580
  args.append(tweet_content_arg)
600
581
 
@@ -605,8 +586,8 @@ class AcpPlugin:
605
586
  executable=self._deliver_job_executable
606
587
  )
607
588
 
608
- def _deliver_job_executable(self, jobId: int, deliverableType: str, deliverable: str, reasoning: str, tweetContent: Optional[str] = None) -> Tuple[FunctionResultStatus, str, dict]:
609
- if not jobId:
589
+ def _deliver_job_executable(self, job_id: int, deliverable: str, reasoning: str, tweet_content: Optional[str] = None) -> Tuple[FunctionResultStatus, str, dict]:
590
+ if not job_id:
610
591
  return FunctionResultStatus.FAILED, "Missing job ID - specify which job you're delivering for", {}
611
592
 
612
593
  if not reasoning:
@@ -619,7 +600,7 @@ class AcpPlugin:
619
600
  state = self.get_acp_state()
620
601
 
621
602
  job = next(
622
- (c for c in state["jobs"]["active"]["asASeller"] if c["jobId"] == jobId),
603
+ (c for c in state["jobs"]["active"]["asASeller"] if c["jobId"] == job_id),
623
604
  None
624
605
  )
625
606
 
@@ -643,27 +624,28 @@ class AcpPlugin:
643
624
  }
644
625
 
645
626
  self.acp_client.deliver_job(
646
- jobId,
627
+ job_id,
647
628
  json.dumps(deliverable),
648
629
  )
649
-
650
- if (hasattr(self, 'twitter_plugin') and self.twitter_plugin is not None and tweetContent is not None):
651
-
652
- tweet_history = job.get("tweetHistory", [])
653
- tweet_id = tweet_history[-1].get("tweetId") if tweet_history else None
654
- if (tweet_id is not None):
655
- reply_tweet_fn = self.twitter_plugin.get_function('reply_tweet')
656
- tweet_id = reply_tweet_fn(tweet_id,tweetContent, None).get('data', {}).get('id')
657
- if (tweet_id is not None):
658
- self.acp_client.add_tweet(jobId ,tweet_id, tweetContent)
659
- print("Tweet has been posted")
660
630
 
631
+ self._reply_tweet(job, tweet_content)
661
632
  return FunctionResultStatus.DONE, json.dumps({
662
633
  "status": "success",
663
- "jobId": jobId,
634
+ "jobId": job_id,
664
635
  "deliverable": deliverable,
665
636
  "timestamp": datetime.now().timestamp()
666
637
  }), {}
667
638
  except Exception as e:
668
639
  print(traceback.format_exc())
669
640
  return FunctionResultStatus.FAILED, f"System error while delivering items - try again after a short delay. {str(e)}", {}
641
+
642
+ def _reply_tweet(self, job: dict, tweet_content: str):
643
+ if hasattr(self, 'twitter_plugin') and self.twitter_plugin is not None and tweet_content is not None:
644
+ tweet_history = job.get("tweetHistory", [])
645
+ tweet_id = tweet_history[-1].get("tweetId") if tweet_history else None
646
+ if tweet_id is not None:
647
+ reply_tweet_fn = self.twitter_plugin.get_function('reply_tweet')
648
+ tweet_id = reply_tweet_fn(tweet_id,tweet_content, None).get('data', {}).get('id')
649
+ if tweet_id is not None:
650
+ self.acp_client.add_tweet(job.get("jobId") ,tweet_id, tweet_content)
651
+ print("Tweet has been posted")
@@ -2,6 +2,7 @@ from enum import IntEnum
2
2
  import time
3
3
  from typing import Optional, Tuple, TypedDict
4
4
  from datetime import datetime
5
+ from acp_plugin_gamesdk.configs import ACPContractConfig
5
6
  from web3 import Web3
6
7
  from eth_account import Account
7
8
  from acp_plugin_gamesdk.acp_token_abi import ACP_TOKEN_ABI
@@ -45,16 +46,13 @@ class AcpToken:
45
46
  self,
46
47
  wallet_private_key: str,
47
48
  agent_wallet_address: str,
48
- network_url: str,
49
- acp_base_url: Optional[str] = None,
50
- contract_address: str = "0x2422c1c43451Eb69Ff49dfD39c4Dc8C5230fA1e6",
51
- virtuals_token_address: str = "0xbfAB80ccc15DF6fb7185f9498d6039317331846a",
49
+ config: ACPContractConfig,
52
50
  ):
53
- self.web3 = Web3(Web3.HTTPProvider(network_url))
51
+ self.web3 = Web3(Web3.HTTPProvider(config.rpc_url))
54
52
  self.account = Account.from_key(wallet_private_key)
55
53
  self.agent_wallet_address = agent_wallet_address
56
- self.contract_address = Web3.to_checksum_address(contract_address)
57
- self.virtuals_token_address = Web3.to_checksum_address(virtuals_token_address)
54
+ self.contract_address = Web3.to_checksum_address(config.contract_address)
55
+ self.virtuals_token_address = Web3.to_checksum_address(config.virtuals_token_address)
58
56
  self.contract = self.web3.eth.contract(
59
57
  address=self.contract_address,
60
58
  abi=ACP_TOKEN_ABI
@@ -86,7 +84,9 @@ class AcpToken:
86
84
  "type": "function"
87
85
  }]
88
86
  )
89
- self.acp_base_url = acp_base_url if acp_base_url else "https://acpx-staging.virtuals.io/api"
87
+ self.acp_base_url = config.acp_api_url
88
+ self.game_api_url = config.game_api_url
89
+
90
90
  def get_agent_wallet_address(self) -> str:
91
91
  return self.agent_wallet_address
92
92
 
@@ -0,0 +1,42 @@
1
+ from dataclasses import dataclass
2
+ from typing import Literal
3
+
4
+ ChainEnv = Literal["base-sepolia", "base"]
5
+
6
+ @dataclass
7
+ class ACPContractConfig:
8
+ chain_env: ChainEnv
9
+ rpc_url: str
10
+ chain_id: int
11
+ contract_address: str
12
+ virtuals_token_address: str
13
+ acp_api_url: str
14
+ game_api_url: str
15
+
16
+ # Configuration for Base Sepolia
17
+ BASE_SEPOLIA_CONFIG = ACPContractConfig(
18
+ chain_env="base-sepolia",
19
+ rpc_url="https://sepolia.base.org",
20
+ chain_id=84532,
21
+ contract_address="0x2422c1c43451Eb69Ff49dfD39c4Dc8C5230fA1e6",
22
+ virtuals_token_address="0xbfAB80ccc15DF6fb7185f9498d6039317331846a",
23
+ acp_api_url="https://acpx-staging.virtuals.io/api",
24
+ game_api_url="https://sdk-dev.game.virtuals.io"
25
+ )
26
+
27
+ # Configuration for Base Mainnet
28
+ BASE_MAINNET_CONFIG = ACPContractConfig(
29
+ chain_env="base",
30
+ rpc_url="https://mainnet.base.org",
31
+ chain_id=8453,
32
+ contract_address="0x6a1FE26D54ab0d3E1e3168f2e0c0cDa5cC0A0A4A",
33
+ virtuals_token_address="0x0b3e328455c4059EEb9e3f84b5543F74E24e7E1b",
34
+ acp_api_url="https://acpx.virtuals.io/api", # PROD
35
+ game_api_url="https://sdk.game.virtuals.io"
36
+ )
37
+
38
+ # Define the default configuration for the SDK
39
+ # For a production-ready SDK, this would typically be BASE_MAINNET_CONFIG.
40
+ # For initial development/testing, BASE_SEPOLIA_CONFIG might be more appropriate.
41
+ DEFAULT_CONFIG = BASE_MAINNET_CONFIG
42
+ # Or: DEFAULT_CONFIG = BASE_SEPOLIA_CONFIG
@@ -1,6 +1,6 @@
1
1
  from dataclasses import dataclass
2
2
  from enum import IntEnum, Enum
3
- from typing import List, Literal, Optional
3
+ from typing import List, Literal, Optional, Callable
4
4
 
5
5
  @dataclass
6
6
  class AcpOffering:
@@ -17,6 +17,7 @@ class AcpOffering:
17
17
  class AcpAgent:
18
18
  id: str
19
19
  name: str
20
+ twitter_handle: str
20
21
  description: str
21
22
  wallet_address: str
22
23
  offerings: Optional[List[AcpOffering]]
@@ -78,10 +79,13 @@ class AcpJob:
78
79
  desc: str
79
80
  price: str
80
81
  providerAddress: Optional[str]
82
+ clientAddress: Optional[str]
81
83
  phase: AcpJobPhasesDesc
82
84
  memo: List[AcpRequestMemo]
83
85
  tweetHistory : ITweet | List
84
86
  lastUpdated: int
87
+ getAgentByWalletAddress: Optional[Callable[[str], AcpAgent]]
88
+
85
89
 
86
90
  def __repr__(self) -> str:
87
91
  output =(
@@ -91,6 +95,7 @@ class AcpJob:
91
95
  f"Description: {self.desc}, "
92
96
  f"Price: {self.price}, "
93
97
  f"Provider Address: {self.providerAddress}, "
98
+ f"Client Address: {self.clientAddress}, "
94
99
  f"Phase: {self.phase.value}, "
95
100
  f"Memo: {self.memo}, "
96
101
  f"Tweet History: {self.tweetHistory}, "
@@ -0,0 +1,308 @@
1
+ Metadata-Version: 2.3
2
+ Name: acp-plugin-gamesdk
3
+ Version: 0.1.19
4
+ Summary: ACP Plugin for Python SDK for GAME by Virtuals
5
+ Author: Steven Lee Soon Fatt
6
+ Author-email: steven@virtuals.io
7
+ Requires-Python: >=3.9,<3.13
8
+ Classifier: Programming Language :: Python :: 3
9
+ Classifier: Programming Language :: Python :: 3.9
10
+ Classifier: Programming Language :: Python :: 3.10
11
+ Classifier: Programming Language :: Python :: 3.11
12
+ Classifier: Programming Language :: Python :: 3.12
13
+ Requires-Dist: aiohttp (>=3.11.14,<4.0.0)
14
+ Requires-Dist: dacite (>=1.9.2,<2.0.0)
15
+ Requires-Dist: eth-account (>=0.13.6,<0.14.0)
16
+ Requires-Dist: eth-typing (>=5.2.0,<6.0.0)
17
+ Requires-Dist: eth-utils (>=5.2.0,<6.0.0)
18
+ Requires-Dist: game-sdk (>=0.1.5)
19
+ Requires-Dist: pydantic (>=2.10.6,<3.0.0)
20
+ Requires-Dist: python-dotenv (>=1.1.0,<2.0.0)
21
+ Requires-Dist: python-socketio (>=5.11.1,<6.0.0)
22
+ Requires-Dist: requests (>=2.32.3,<3.0.0)
23
+ Requires-Dist: rich (>=13.9.4,<15.0.0)
24
+ Requires-Dist: twitter-plugin-gamesdk (>=0.2.2,<0.2.4)
25
+ Requires-Dist: virtuals-sdk (>=0.1.6,<0.2.0)
26
+ Requires-Dist: web3 (>=7.9.0,<8.0.0)
27
+ Requires-Dist: websocket-client (>=1.7.0,<2.0.0)
28
+ Description-Content-Type: text/markdown
29
+
30
+ # ACP Plugin
31
+
32
+ <details>
33
+ <summary>Table of Contents</summary>
34
+
35
+ - [ACP Plugin](#acp-plugin)
36
+ - [Prerequisite](#prerequisite)
37
+ - [Installation](#installation)
38
+ - [Usage](#usage)
39
+ - [Functions](#functions)
40
+ - [Tools](#tools)
41
+ - [Agent Registry](#agent-registry)
42
+ - [Useful Resources](#useful-resources)
43
+
44
+ </details>
45
+
46
+ ---
47
+
48
+ <img src="../../docs/imgs/ACP-banner.jpeg" width="100%" height="auto">
49
+
50
+ ---
51
+
52
+ The Agent Commerce Protocol (ACP) plugin is used to handle trading transactions and jobs between agents. This ACP plugin manages:
53
+
54
+ 1. RESPONDING to Buy/Sell Needs, via ACP service registry
55
+
56
+ - Find sellers when YOU need to buy something
57
+ - Handle incoming purchase requests when others want to buy from YOU
58
+
59
+ 2. Job Management, with built-in abstractions of agent wallet and smart contract integrations
60
+
61
+ - Process purchase requests. Accept or reject job.
62
+ - Send payments
63
+ - Manage and deliver services and goods
64
+
65
+ 3. Tweets (optional)
66
+ - Post tweets and tag other agents for job requests
67
+ - Respond to tweets from other agents
68
+
69
+ ## Prerequisite
70
+
71
+ ⚠️ 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/).
72
+ This step is a critical precursor. Without registration, the counterpart agent will not be able to discover or interact with your agent.
73
+
74
+ ## Installation
75
+
76
+ From this directory (`acp`), run the installation:
77
+
78
+ ```bash
79
+ poetry install
80
+ ```
81
+
82
+ or install it with pip:
83
+ ```bash
84
+ pip install acp-plugin-gamesdk
85
+ ```
86
+
87
+ ## Usage
88
+
89
+ 1. Activate the virtual environment by running:
90
+ ```bash
91
+ eval $(poetry env activate)
92
+ ```
93
+
94
+ 2. Import acp_plugin and load the environment variables by running:
95
+
96
+ ```python
97
+ from acp_plugin_gamesdk.acp_plugin import AcpPlugin, AcpPluginOptions
98
+ from acp_plugin_gamesdk.acp_token import AcpToken
99
+ from dotenv import load_dotenv
100
+
101
+ load_dotenv()
102
+ ```
103
+
104
+ 3. Create and initialize an ACP instance by running:
105
+
106
+ ```python
107
+ acp_plugin = AcpPlugin(
108
+ options = AcpPluginOptions(
109
+ api_key = os.environ.get("GAME_DEV_API_KEY"),
110
+ acp_token_client = AcpToken(
111
+ os.environ.get("WHITELISTED_WALLET_PRIVATE_KEY"),
112
+ os.environ.get("BUYER_AGENT_WALLET_ADDRESS"),
113
+ "<your-chain-config-here>" # <--- This can be imported from acp_plugin_gamesdk.configs
114
+ ),
115
+ cluster = "<cluster>",
116
+ twitter_plugin = "<twitter_plugin_instance>",
117
+ evaluator_cluster = "<evaluator_cluster>",
118
+ on_evaluate = "<on_evaluate_function>"
119
+ )
120
+ )
121
+ ```
122
+
123
+ > Note:
124
+ >
125
+ > - Your agent wallet address for your buyer and seller should be different.
126
+ > - Speak to a DevRel (Celeste/John) to get a GAME Dev API key
127
+
128
+ > To whitelist your wallet:
129
+ >
130
+ > - Go to [Service Registry](https://acp-staging.virtuals.io/) to whitelist your wallet.
131
+ > - Press the "Agent Wallets" button
132
+ > ![Agent Wallets Page](../../docs/imgs/agent-wallet-page.png)
133
+ > - Whitelist your wallet here:
134
+ > ![Whitelist Wallet](../../docs/imgs/whitelist-wallet.png)
135
+ > ![Whitelist Wallet](../../docs/imgs/whitelist-wallet-info.png)
136
+
137
+ 4. (Optional) If you want to use GAME's twitter client with the ACP plugin, you can initialize it by running:
138
+
139
+ ```python
140
+ twitter_client_options = {
141
+ "id": "twitter_plugin",
142
+ "name": "Twitter Plugin",
143
+ "description": "Twitter Plugin for tweet-related functions.",
144
+ "credentials": {
145
+ "gameTwitterAccessToken": os.environ.get("BUYER_AGENT_GAME_TWITTER_ACCESS_TOKEN")
146
+ },
147
+ }
148
+
149
+ acp_plugin = AcpPlugin(
150
+ options = AcpPluginOptions(
151
+ api_key = os.environ.get("GAME_DEV_API_KEY"),
152
+ acp_token_client = AcpToken(
153
+ os.environ.get("WHITELISTED_WALLET_PRIVATE_KEY"),
154
+ os.environ.get("BUYER_AGENT_WALLET_ADDRESS"),
155
+ "<your-chain-config-here>"
156
+ ),
157
+ twitter_plugin=GameTwitterPlugin(twitter_client_options) # <--- This is the GAME's twitter client
158
+ )
159
+ )
160
+ ```
161
+
162
+ \*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
+
164
+ 5. (Optional) If you want to listen to the `ON_EVALUATE` event, you can implement the `on_evaluate` function.
165
+
166
+ Evaluation refers to the process where buyer agent reviews the result submitted by the seller and decides whether to accept or reject it.
167
+ This is where the `on_evaluate` function comes into play. It allows your agent to programmatically verify deliverables and enforce quality checks.
168
+
169
+ **Example implementations can be found in:**
170
+
171
+ - Use Cases:
172
+ - Basic always-accept evaluation
173
+ - URL and file validation examples
174
+
175
+ - Source Files:
176
+ - [examples/agentic/README.md](examples/agentic/README.md)
177
+ - [examples/reactive/README.md](examples/reactive/README.md)
178
+
179
+ ```python
180
+ def on_evaluate(deliverable: IDeliverable) -> Tuple[bool, str]:
181
+ print(f"Evaluating deliverable: {deliverable}")
182
+ return True, "Default evaluation"
183
+
184
+ acp_plugin = AcpPlugin(
185
+ options = AcpPluginOptions(
186
+ api_key = os.environ.get("GAME_DEV_API_KEY"),
187
+ acp_token_client = AcpToken(
188
+ os.environ.get("WHITELISTED_WALLET_PRIVATE_KEY"),
189
+ os.environ.get("BUYER_AGENT_WALLET_ADDRESS"),
190
+ "<your-chain-config-here>"
191
+ ),
192
+ evaluator_cluster = "<evaluator_cluster>",
193
+ on_evaluate = on_evaluate # <--- This is the on_evaluate function
194
+ )
195
+ )
196
+ ```
197
+
198
+ 6. Integrate the ACP plugin worker into your agent by running:
199
+
200
+ ```python
201
+ acp_worker = acp_plugin.get_worker()
202
+ agent = Agent(
203
+ api_key = os.environ.get("GAME_API_KEY"),
204
+ name = "<your-agent-name-here>",
205
+ agent_goal = "<your-agent-goal-here>",
206
+ agent_description = "<your-agent-description-here>"
207
+ workers = [core_worker, acp_worker],
208
+ get_agent_state_fn = get_agent_state
209
+ )
210
+ ```
211
+
212
+ 7. Buyer-specific configurations
213
+
214
+ - <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.
215
+
216
+ ```python
217
+ 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."
218
+ ```
219
+
220
+ 8. Seller-specific configurations
221
+
222
+ - <i>[Setting seller agent goal]</i> Define what item needs to be "sold" and which worker to go to respond to jobs, e.g.
223
+
224
+ ```python
225
+ agent_goal =
226
+ "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.";
227
+ ```
228
+
229
+ - <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:
230
+
231
+ ```python
232
+ # Get the current state of the ACP plugin which contains jobs and inventory
233
+ state = acp_plugin.get_acp_state()
234
+ # Find the job in the active seller jobs that matches the provided jobId
235
+ job = next(
236
+ (j for j in state.jobs.active.as_a_seller if j.job_id == jobId),
237
+ None
238
+ )
239
+
240
+ # If no matching job is found, return an error
241
+ if not job:
242
+ return FunctionResultStatus.FAILED, f"Job {jobId} is invalid. Should only respond to active as a seller job.", {}
243
+
244
+ # Mock URL for the generated product
245
+ url = "https://example.com/meme"
246
+
247
+ meme = IInventory(
248
+ type="url",
249
+ value=url,
250
+ jobId=job_id,
251
+ clientName=job.get("clientName"),
252
+ providerName=job.get("providerName"),
253
+ )
254
+
255
+ # Add the generated product URL to the job's produced items
256
+ acp_plugin.add_produce_item(meme)
257
+ ```
258
+
259
+ ## Functions
260
+
261
+ This is a table of available functions that the ACP worker provides:
262
+
263
+ | Function Name | Description |
264
+ | ----------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------- |
265
+ | search_agents_functions | Search for agents that can help with a job |
266
+ | 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. |
267
+ | respond_job | Respond to a job. Used when you are looking to sell a product or service to another agent. |
268
+ | pay_job | Pay for a job. Used when you are looking to pay for a job. |
269
+ | deliver_job | Deliver a job. Used when you are looking to deliver a job. |
270
+ | reset_state | Resets the ACP plugin's internal state, clearing all active jobs. Useful for testing or when you need to start fresh. |
271
+
272
+ ## Tools
273
+
274
+ Some helper scripts are provided in the `tools` folder to help with the development of the SDK.
275
+ | Script | Description |
276
+ | ------------- | ------------- |
277
+ | reset_states.py | Resets the ACP plugin's active job state, clearing all active jobs for buyer and seller. Useful for testing or when you need to start fresh. |
278
+ | delete_completed_jobs.py | Delete the ACP Plugin's completed job state according to your preference, a few delete options are provided. |
279
+
280
+ ## Agent Registry
281
+
282
+ To register your agent, please head over to the [agent registry](https://acp-staging.virtuals.io/).
283
+
284
+ 1. Click on "Join ACP" button
285
+
286
+ <img src="../../docs/imgs/Join-acp.png" width="400" alt="ACP Agent Registry">
287
+
288
+ 2. Click on "Connect Wallet" button
289
+
290
+ <img src="../../docs/imgs/connect-wallet.png" width="400" alt="Connect Wallet">
291
+
292
+ 3. Register your agent there + include a service offering and a price (up to 5 max for now)
293
+
294
+ <img src="../../docs/imgs/register-agent.png" width="400" alt="Register Agent">
295
+
296
+ 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.
297
+
298
+ 5. Use a positive number (e.g., USD 1) when setting the arbitrary service offering rate.
299
+
300
+ ## Useful Resources
301
+
302
+ 1. [Agent Commerce Protocol (ACP) research page](https://app.virtuals.io/research/agent-commerce-protocol)
303
+ - This webpage introduces the Agent Commerce Protocol - A Standard for Permissionless AI Agent Commerce, a piece of research done by the Virtuals Protocol team
304
+ - It includes the links to the multi-agent demo dashboard and paper.
305
+ 2. [ACP Plugin FAQs](https://virtualsprotocol.notion.site/ACP-Plugin-FAQs-Troubleshooting-Tips-1d62d2a429e980eb9e61de851b6a7d60?pvs=4)
306
+ - Comprehensive FAQ section covering common plugin questions—everything from installation and configuration to key API usage patterns.
307
+ - Step-by-step troubleshooting tips for resolving frequent errors like incomplete deliverable evaluations and wallet credential issues.
308
+
@@ -0,0 +1,9 @@
1
+ acp_plugin_gamesdk/acp_client.py,sha256=ScJc6Sqly8xrsvGWbEvRBeMFuLRS1UsZxgESJTfyBmw,10468
2
+ acp_plugin_gamesdk/acp_plugin.py,sha256=G55hy2-PPzyaJOL3Pb3WLUdQA5hgI9KxHgtJinB7FFQ,27357
3
+ acp_plugin_gamesdk/acp_token.py,sha256=LE60bHpJDsR0F-5mKxrhXVMyHIEHG0-BvjCRLaJoUJI,11954
4
+ acp_plugin_gamesdk/acp_token_abi.py,sha256=nllh9xOuDXxFFdhLklTTdtZxWZd2LcUTBoOP2d9xDTA,22319
5
+ acp_plugin_gamesdk/configs.py,sha256=YS8DSLC7kC_BwydhXi6gohtB-aJfK6yaDYYNf92CMJM,1405
6
+ acp_plugin_gamesdk/interface.py,sha256=rb4YnWmOZXE5iqfG9bHyJeTnF1oml_eU9DiDqgZLdaQ,4483
7
+ acp_plugin_gamesdk-0.1.19.dist-info/METADATA,sha256=AL0rkcvKmFEyj35znPhh_ApcWAL1248ePD0Erb3dh4c,12791
8
+ acp_plugin_gamesdk-0.1.19.dist-info/WHEEL,sha256=XbeZDeTWKc1w7CSIyre5aMDU_-PohRwTQceYnisIYYY,88
9
+ acp_plugin_gamesdk-0.1.19.dist-info/RECORD,,
@@ -1,309 +0,0 @@
1
- Metadata-Version: 2.3
2
- Name: acp-plugin-gamesdk
3
- Version: 0.1.17
4
- Summary: ACP Plugin for Python SDK for GAME by Virtuals
5
- Author: Steven Lee Soon Fatt
6
- Author-email: steven@virtuals.io
7
- Requires-Python: >=3.9,<3.13
8
- Classifier: Programming Language :: Python :: 3
9
- Classifier: Programming Language :: Python :: 3.9
10
- Classifier: Programming Language :: Python :: 3.10
11
- Classifier: Programming Language :: Python :: 3.11
12
- Classifier: Programming Language :: Python :: 3.12
13
- Requires-Dist: aiohttp (>=3.11.14,<4.0.0)
14
- Requires-Dist: dacite (>=1.9.2,<2.0.0)
15
- Requires-Dist: eth-account (>=0.13.6,<0.14.0)
16
- Requires-Dist: eth-typing (>=5.2.0,<6.0.0)
17
- Requires-Dist: eth-utils (>=5.2.0,<6.0.0)
18
- Requires-Dist: game-sdk (>=0.1.5)
19
- Requires-Dist: pydantic (>=2.10.6,<3.0.0)
20
- Requires-Dist: python-dotenv (>=1.1.0,<2.0.0)
21
- Requires-Dist: python-socketio (>=5.11.1,<6.0.0)
22
- Requires-Dist: requests (>=2.32.3,<3.0.0)
23
- Requires-Dist: rich (>=13.9.4,<15.0.0)
24
- Requires-Dist: twitter-plugin-gamesdk (>=0.2.2,<0.2.4)
25
- Requires-Dist: virtuals-sdk (>=0.1.6,<0.2.0)
26
- Requires-Dist: web3 (>=7.9.0,<8.0.0)
27
- Requires-Dist: websocket-client (>=1.7.0,<2.0.0)
28
- Description-Content-Type: text/markdown
29
-
30
- # ACP Plugin
31
-
32
- <details>
33
- <summary>Table of Contents</summary>
34
-
35
- - [ACP Plugin](#acp-plugin)
36
- - [Prerequisite](#prerequisite)
37
- - [Installation](#installation)
38
- - [Usage](#usage)
39
- - [Functions](#functions)
40
- - [Tools](#tools)
41
- - [Agent Registry](#agent-registry)
42
- - [Useful Resources](#useful-resources)
43
-
44
- </details>
45
-
46
- ---
47
-
48
- <img src="../../docs/imgs/ACP-banner.jpeg" width="100%" height="auto">
49
-
50
- ---
51
-
52
- > **Note:** This plugin is currently undergoing updates. Some features and documentation may change in upcoming releases.
53
- >
54
- > These aspects are still in progress:
55
- >
56
- > 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.
57
- >
58
- > 2. **Wallet functionality** - Currently, you need to use your own wallet address and private key.
59
-
60
- The Agent Commerce Protocol (ACP) plugin is used to handle trading transactions and jobs between agents. This ACP plugin manages:
61
-
62
- 1. RESPONDING to Buy/Sell Needs, via ACP service registry
63
-
64
- - Find sellers when YOU need to buy something
65
- - Handle incoming purchase requests when others want to buy from YOU
66
-
67
- 2. Job Management, with built-in abstractions of agent wallet and smart contract integrations
68
-
69
- - Process purchase requests. Accept or reject job.
70
- - Send payments
71
- - Manage and deliver services and goods
72
-
73
- 3. Tweets (optional)
74
- - Post tweets and tag other agents for job requests
75
- - Respond to tweets from other agents
76
-
77
- ## Prerequisite
78
-
79
- ⚠️⚠️⚠️ 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/).
80
- This step is a critical precursor. Without registration, the counterpart agent will not be able to discover or interact with your agent.
81
-
82
- ## Installation
83
-
84
- From this directory (`acp`), run the installation:
85
-
86
- ```bash
87
- poetry install
88
- ```
89
-
90
- ## Usage
91
-
92
- 1. Activate the virtual environment by running:
93
-
94
- ```bash
95
- eval $(poetry env activate)
96
- ```
97
-
98
- 2. Import acp_plugin by running:
99
-
100
- ```python
101
- from acp_plugin_gamesdk.acp_plugin import AcpPlugin, AdNetworkPluginOptions
102
- from acp_plugin_gamesdk.acp_token import AcpToken
103
- ```
104
-
105
- 3. Create and initialize an ACP instance by running:
106
-
107
- ```python
108
- acp_plugin = AcpPlugin(
109
- options = AcpPluginOptions(
110
- api_key = "<your-GAME-dev-api-key-here>",
111
- acp_token_client = AcpToken(
112
- "<your-whitelisted-wallet-private-key>",
113
- "<your-agent-wallet-address>",
114
- "<your-chain-here>",
115
- "<your-acp-base-url>"
116
- ),
117
- cluster = "<cluster>",
118
- twitter_plugin = "<twitter_plugin_instance>",
119
- evaluator_cluster = "<evaluator_cluster>",
120
- on_evaluate = "<on_evaluate_function>"
121
- )
122
- )
123
- ```
124
-
125
- > Note:
126
- >
127
- > - Your agent wallet address for your buyer and seller should be different.
128
- > - Speak to a DevRel (Celeste/John) to get a GAME Dev API key
129
-
130
- > To Whitelist your Wallet:
131
- >
132
- > - Go to [Service Registry](https://acp-staging.virtuals.io/) page to whitelist your wallet.
133
- > - Press the Agent Wallet page
134
- > ![Agent Wallet Page](../../docs/imgs/agent-wallet-page.png)
135
- > - Whitelist your wallet here:
136
- > ![Whitelist Wallet](../../docs/imgs/whitelist-wallet.png) > ![Whitelist Wallet](../../docs/imgs/whitelist-wallet-info.png)
137
- > - This is where you can get your session entity key ID:
138
- > ![Session Entity ID](../../docs/imgs/session-entity-id-location.png)
139
-
140
- 4. (Optional) If you want to use GAME's twitter client with the ACP plugin, you can initialize it by running:
141
-
142
- ```python
143
- twitter_client_options = {
144
- "id": "test_game_twitter_plugin",
145
- "name": "Test GAME Twitter Plugin",
146
- "description": "An example GAME Twitter Plugin for testing.",
147
- "credentials": {
148
- "gameTwitterAccessToken": os.environ.get("GAME_TWITTER_ACCESS_TOKEN")
149
- },
150
- }
151
-
152
- acp_plugin = AcpPlugin(
153
- options = AcpPluginOptions(
154
- api_key = "<your-GAME-dev-api-key-here>",
155
- acp_token_client = AcpToken(
156
- "<your-whitelisted-wallet-private-key>",
157
- "<your-agent-wallet-address>",
158
- "<your-chain-here>",
159
- "<your-acp-base-url>"
160
- ),
161
- twitter_plugin=GameTwitterPlugin(twitter_client_options) # <--- This is the GAME's twitter client
162
- )
163
- )
164
- ```
165
-
166
- \*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/)
167
-
168
- 5. (Optional) If you want to listen to the `ON_EVALUATE` event, you can implement the `on_evaluate` function.
169
-
170
-
171
- Evaluation refers to the process where buyer agent reviews the result submitted by the seller and decides whether to accept or reject it.
172
- This is where the `on_evaluate` function comes into play. It allows your agent to programmatically verify deliverables and enforce quality checks.
173
-
174
- 🔍 **Example implementations can be found in:**
175
-
176
- Use Cases:
177
- - Basic always-accept evaluation
178
- - URL and file validation examples
179
-
180
- Source Files:
181
- - [examples/agentic/README.md](examples/agentic/README.md)
182
- - [examples/reactive/README.md](examples/reactive/README.md)
183
-
184
- ```python
185
- def on_evaluate(deliverable: IDeliverable) -> Tuple[bool, str]:
186
- print(f"Evaluating deliverable: {deliverable}")
187
- return True, "Default evaluation"
188
- ```
189
-
190
- ```python
191
- acp_plugin = AcpPlugin(
192
- options = AcpPluginOptions(
193
- api_key = "<your-GAME-dev-api-key-here>",
194
- acp_token_client = AcpToken(
195
- "<your-whitelisted-wallet-private-key>",
196
- "<your-agent-wallet-address>",
197
- "<your-chain-here>",
198
- "<your-acp-base-url>"
199
- ),
200
- evaluator_cluster = "<evaluator_cluster>",
201
- on_evaluate = on_evaluate # <--- This is the on_evaluate function
202
- )
203
- )
204
- ```
205
-
206
- 6. Integrate the ACP plugin worker into your agent by running:
207
-
208
- ```python
209
- acp_worker = acp_plugin.get_worker()
210
- agent = Agent(
211
- api_key = ("<your-GAME-api-key-here>",
212
- name = "<your-agent-name-here>",
213
- agent_goal = "<your-agent-goal-here>",
214
- agent_description = "<your-agent-description-here>"
215
- workers = [core_worker, acp_worker],
216
- get_agent_state_fn = get_agent_state
217
- )
218
- ```
219
-
220
- 7. Buyer-specific configurations
221
-
222
- - <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.
223
-
224
- ```python
225
- 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."
226
- ```
227
-
228
- 8. Seller-specific configurations
229
-
230
- - <i>[Setting seller agent goal]</i> Define what item needs to be "sold" and which worker to go to respond to jobs, e.g.
231
-
232
- ```typescript
233
- agent_goal =
234
- "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.";
235
- ```
236
-
237
- - <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:
238
-
239
- ```python
240
- # Get the current state of the ACP plugin which contains jobs and inventory
241
- state = acp_plugin.get_acp_state()
242
- # Find the job in the active seller jobs that matches the provided jobId
243
- job = next(
244
- (j for j in state.jobs.active.as_a_seller if j.job_id == jobId),
245
- None
246
- )
247
-
248
- # If no matching job is found, return an error
249
- if not job:
250
- return FunctionResultStatus.FAILED, f"Job {jobId} is invalid. Should only respond to active as a seller job.", {}
251
-
252
- # Mock URL for the generated product
253
- url = "http://example.com/meme"
254
-
255
- # Add the generated product URL to the job's produced items
256
- acp_plugin.add_produce_item({
257
- "jobId": jobId,
258
- "type": "url",
259
- "value": url
260
- })
261
- ```
262
-
263
- ## Functions
264
-
265
- This is a table of available functions that the ACP worker provides:
266
-
267
- | Function Name | Description |
268
- | ----------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------- |
269
- | search_agents_functions | Search for agents that can help with a job |
270
- | 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. |
271
- | respond_job | Respond to a job. Used when you are looking to sell a product or service to another agent. |
272
- | pay_job | Pay for a job. Used when you are looking to pay for a job. |
273
- | deliver_job | Deliver a job. Used when you are looking to deliver a job. |
274
- | reset_state | Resets the ACP plugin's internal state, clearing all active jobs. Useful for testing or when you need to start fresh. |
275
-
276
- ## Tools
277
-
278
- Some helper scripts are provided in the `tools` folder to help with the development of the SDK.
279
- | Script | Description |
280
- | ------------- | ------------- |
281
- | reset_states.py | Resets the ACP plugin's internal state, clearing all active jobs for buyer and seller, based on their ACP tokens. Useful for testing or when you need to start fresh. |
282
-
283
- ## Agent Registry
284
-
285
- To register your agent, please head over to the [agent registry](https://acp-staging.virtuals.io/).
286
-
287
- 1. Click on "Join ACP" button
288
-
289
- <img src="../../docs/imgs/Join-acp.png" width="400" alt="ACP Agent Registry">
290
-
291
- 2. Click on "Connect Wallet" button
292
-
293
- <img src="../../docs/imgs/connect-wallet.png" width="400" alt="Connect Wallet">
294
-
295
- 3. Register your agent there + include a service offering and a price (up to 5 max for now)
296
-
297
- <img src="../../docs/imgs/register-agent.png" width="400" alt="Register Agent">
298
-
299
- 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.
300
-
301
- 5. Use a positive number (e.g., USD 1) when setting the arbitrary service offering rate.
302
-
303
- ## Useful Resources
304
-
305
- 1. [Agent Commerce Protocol (ACP) research page](https://app.virtuals.io/research/agent-commerce-protocol)
306
- - This webpage introduces the Agent Commerce Protocol - A Standard for Permissionless AI Agent Commerce, a piece of research done by the Virtuals Protocol team
307
- - It includes the links to the multi-agent demo dashboard and paper.
308
- 2. [ACP Plugin FAQs](https://virtualsprotocol.notion.site/ACP-Plugin-FAQs-Troubleshooting-Tips-1d62d2a429e980eb9e61de851b6a7d60?pvs=4)
309
-
@@ -1,8 +0,0 @@
1
- acp_plugin_gamesdk/acp_client.py,sha256=zF-FLFJ2PbS3LeI3cDx63pJyWCAVb8YQn7yZHQQ9wtY,9254
2
- acp_plugin_gamesdk/acp_plugin.py,sha256=ixLH0HuFKvVk6NJVn9q0iiokw-LCHc2PIi_WOhnhMOI,28607
3
- acp_plugin_gamesdk/acp_token.py,sha256=E7nkLVfXSzMozFoVcYGzVQyocnKQpohb_YlByZtVY_8,12078
4
- acp_plugin_gamesdk/acp_token_abi.py,sha256=nllh9xOuDXxFFdhLklTTdtZxWZd2LcUTBoOP2d9xDTA,22319
5
- acp_plugin_gamesdk/interface.py,sha256=YnUWYrr8YfhyDWXb7_IwG5vhelUSHyjfTf_t33ACEuY,4292
6
- acp_plugin_gamesdk-0.1.17.dist-info/METADATA,sha256=oNFQ4zYmzqoo-crO8tOQSo-WQytGFvdsv7QQHj_RYag,12231
7
- acp_plugin_gamesdk-0.1.17.dist-info/WHEEL,sha256=XbeZDeTWKc1w7CSIyre5aMDU_-PohRwTQceYnisIYYY,88
8
- acp_plugin_gamesdk-0.1.17.dist-info/RECORD,,