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