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.
- acp_plugin_gamesdk/acp_client.py +46 -8
- acp_plugin_gamesdk/acp_plugin.py +82 -100
- acp_plugin_gamesdk/acp_token.py +8 -8
- acp_plugin_gamesdk/configs.py +42 -0
- acp_plugin_gamesdk/interface.py +6 -1
- acp_plugin_gamesdk-0.1.19.dist-info/METADATA +308 -0
- acp_plugin_gamesdk-0.1.19.dist-info/RECORD +9 -0
- acp_plugin_gamesdk-0.1.17.dist-info/METADATA +0 -309
- acp_plugin_gamesdk-0.1.17.dist-info/RECORD +0 -8
- {acp_plugin_gamesdk-0.1.17.dist-info → acp_plugin_gamesdk-0.1.19.dist-info}/WHEEL +0 -0
acp_plugin_gamesdk/acp_client.py
CHANGED
@@ -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
|
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
|
-
|
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
|
115
|
+
if data.get("status") == "retry":
|
114
116
|
raise Exception("Transaction failed, retrying...")
|
115
117
|
|
116
|
-
if
|
118
|
+
if data.get("status") == "failed":
|
117
119
|
break
|
118
120
|
|
119
|
-
if
|
121
|
+
if data.get("status") == "success":
|
120
122
|
job_id = int(data.get("result").get("jobId"))
|
121
123
|
|
122
|
-
if
|
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
|
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]
|
acp_plugin_gamesdk/acp_plugin.py
CHANGED
@@ -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
|
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
|
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
|
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
|
-
|
78
|
-
|
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
|
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(
|
100
|
+
self.socket.connect(self.acp_client.base_url, auth=self.socket.auth)
|
98
101
|
|
99
|
-
if
|
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(
|
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(
|
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="
|
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="
|
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="
|
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="
|
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="
|
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,
|
322
|
-
if isinstance(
|
323
|
-
require_evaluation =
|
324
|
-
elif isinstance(
|
325
|
-
require_evaluation =
|
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"] ==
|
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
|
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
|
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
|
-
|
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,
|
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
|
-
|
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
|
-
|
369
|
+
seller_wallet_address,
|
367
370
|
float(price),
|
368
|
-
|
369
|
-
|
371
|
+
service_requirements,
|
372
|
+
evaluator_address,
|
370
373
|
expired_at
|
371
374
|
)
|
372
|
-
|
373
|
-
if
|
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(
|
376
|
-
if
|
377
|
-
self.acp_client.add_tweet(job_id, tweet_id,
|
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":
|
385
|
+
"sellerWalletAddress": seller_wallet_address,
|
383
386
|
"price": float(price),
|
384
|
-
"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="
|
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="
|
418
|
+
name="tweet_content",
|
416
419
|
type="string",
|
417
|
-
description="Tweet content
|
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,
|
429
|
-
if not
|
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"] ==
|
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
|
-
|
456
|
+
job_id,
|
454
457
|
decision == "ACCEPT",
|
455
458
|
job["memo"][0]["id"],
|
456
459
|
reasoning
|
457
460
|
)
|
458
|
-
|
459
|
-
|
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":
|
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="
|
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="
|
496
|
+
name="tweet_content",
|
502
497
|
type="string",
|
503
|
-
description="Tweet content
|
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,
|
515
|
-
if not
|
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"] ==
|
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
|
-
|
535
|
+
job_id,
|
541
536
|
amount,
|
542
537
|
job["memo"][0]["id"],
|
543
538
|
reasoning
|
544
539
|
)
|
545
|
-
|
546
|
-
|
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":
|
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="
|
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,
|
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="
|
576
|
+
name="tweet_content",
|
596
577
|
type="string",
|
597
|
-
description="Tweet content
|
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,
|
609
|
-
if not
|
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"] ==
|
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
|
-
|
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":
|
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")
|
acp_plugin_gamesdk/acp_token.py
CHANGED
@@ -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
|
-
|
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(
|
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 =
|
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
|
acp_plugin_gamesdk/interface.py
CHANGED
@@ -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
|
+
> 
|
133
|
+
> - Whitelist your wallet here:
|
134
|
+
> 
|
135
|
+
> 
|
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
|
-
> 
|
135
|
-
> - Whitelist your wallet here:
|
136
|
-
>  > 
|
137
|
-
> - This is where you can get your session entity key ID:
|
138
|
-
> 
|
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,,
|
File without changes
|