acp-plugin-gamesdk 0.1.24__py3-none-any.whl → 0.1.26__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.
@@ -1,54 +1,32 @@
1
1
  import json
2
- import signal
3
- import sys
4
2
  import traceback
5
- from collections.abc import Callable
6
- from dataclasses import asdict, dataclass
7
- from datetime import datetime, timedelta, timezone
8
- from typing import Any, Dict, List, Optional, Tuple
3
+ from dataclasses import dataclass
4
+ from datetime import datetime, timezone, timedelta
5
+ from typing import List, Dict, Any, Optional,Tuple
9
6
 
10
- import socketio
7
+ import requests
11
8
 
12
9
  from game_sdk.game.agent import WorkerConfig
13
10
  from game_sdk.game.custom_types import Argument, Function, FunctionResultStatus
14
- from twitter_plugin_gamesdk.game_twitter_plugin import GameTwitterPlugin
15
11
  from twitter_plugin_gamesdk.twitter_plugin import TwitterPlugin
16
- from acp_plugin_gamesdk.acp_client import AcpClient
17
- from acp_plugin_gamesdk.acp_token import AcpToken
18
- from acp_plugin_gamesdk.interface import (
19
- AcpJob,
20
- AcpJobPhasesDesc,
21
- IDeliverable,
22
- IInventory,
23
- )
24
12
 
13
+ from acp_plugin_gamesdk.interface import AcpJobPhasesDesc, IInventory, ACP_JOB_PHASE_MAP
14
+ from virtuals_acp.client import VirtualsACP
15
+ from virtuals_acp.job import ACPJob
25
16
 
26
17
  @dataclass
27
18
  class AcpPluginOptions:
28
19
  api_key: str
29
- acp_token_client: AcpToken
30
- twitter_plugin: TwitterPlugin | GameTwitterPlugin = None
20
+ acp_client: VirtualsACP
21
+ twitter_plugin: TwitterPlugin | None = None
31
22
  cluster: Optional[str] = None
32
23
  evaluator_cluster: Optional[str] = None
33
- on_evaluate: Optional[Callable[[IDeliverable], Tuple[bool, str]]] = None
34
- on_phase_change: Optional[Callable[[AcpJob], None]] = None
35
24
  job_expiry_duration_mins: Optional[int] = None
36
-
37
-
38
- SocketEvents = {
39
- "JOIN_EVALUATOR_ROOM": "joinEvaluatorRoom",
40
- "LEAVE_EVALUATOR_ROOM": "leaveEvaluatorRoom",
41
- "ON_EVALUATE": "onEvaluate",
42
- "ROOM_JOINED": "roomJoined",
43
- "ON_PHASE_CHANGE": "onPhaseChange"
44
- }
45
-
46
-
25
+
47
26
  class AcpPlugin:
48
27
  def __init__(self, options: AcpPluginOptions):
49
28
  print("Initializing AcpPlugin")
50
- self.acp_token_client = options.acp_token_client
51
- self.acp_client = AcpClient(options.api_key, options.acp_token_client)
29
+ self.acp_client = options.acp_client
52
30
  self.id = "acp_worker"
53
31
  self.name = "ACP Worker"
54
32
  self.description = """
@@ -71,97 +49,85 @@ class AcpPlugin:
71
49
  self.twitter_plugin = None
72
50
  if options.twitter_plugin is not None:
73
51
  self.twitter_plugin = options.twitter_plugin
74
-
52
+
75
53
  self.produced_inventory: List[IInventory] = []
76
- self.acp_base_url = self.acp_token_client.acp_base_url
77
- if options.on_evaluate is not None or options.on_phase_change is not None:
78
- print("Initializing socket")
79
- self.socket = None
80
- if options.on_evaluate is not None:
81
- self.on_evaluate = options.on_evaluate
82
- if options.on_phase_change is not None:
83
- def phase_change_wrapper(job: AcpJob):
84
- job["getAgentByWalletAddress"] = self.acp_client.get_agent_by_wallet_address
85
- return options.on_phase_change(job)
86
-
87
- self.on_phase_change = phase_change_wrapper
88
- self.initialize_socket()
54
+ self.acp_base_url = self.acp_client.acp_api_url
89
55
  self.job_expiry_duration_mins = options.job_expiry_duration_mins if options.job_expiry_duration_mins is not None else 1440
90
-
91
- def initialize_socket(self) -> Tuple[bool, str]:
92
- """
93
- Initialize socket connection for real-time communication.
94
- Returns a tuple of (success, message).
95
- """
96
- try:
97
- self.socket = socketio.Client()
98
-
99
- # Set up authentication before connecting
100
- self.socket.auth = {
101
- "evaluatorAddress": self.acp_token_client.agent_wallet_address
102
- }
103
-
104
- # Connect socket to GAME SDK dev server
105
- self.socket.connect(self.acp_client.base_url, auth=self.socket.auth)
106
-
107
- if self.socket.connected:
108
- self.socket.emit(SocketEvents["JOIN_EVALUATOR_ROOM"], self.acp_token_client.agent_wallet_address)
109
-
110
- # Set up event handler for evaluation requests
111
- @self.socket.on(SocketEvents["ON_EVALUATE"])
112
- def on_evaluate(data):
113
- if self.on_evaluate:
114
- deliverable = data.get("deliverable")
115
- memo_id = data.get("memoId")
116
-
117
- is_approved, reasoning = self.on_evaluate(deliverable)
118
-
119
- self.acp_token_client.sign_memo(memo_id, is_approved, reasoning)
120
-
121
- # Set up event handler for phase changes
122
-
123
- @self.socket.on(SocketEvents["ON_PHASE_CHANGE"])
124
- def on_phase_change(data):
125
- if hasattr(self, 'on_phase_change') and self.on_phase_change:
126
- self.on_phase_change(data)
127
-
128
- # Set up cleanup function for graceful shutdown
129
- def cleanup():
130
- if self.socket:
131
- print("Disconnecting socket")
132
- import time
133
- time.sleep(1)
134
- self.socket.disconnect()
135
-
136
- def signal_handler(_sig, _frame):
137
- cleanup()
138
- sys.exit(0)
139
-
140
- signal.signal(signal.SIGINT, signal_handler)
141
- signal.signal(signal.SIGTERM, signal_handler)
142
-
143
- return True, "Socket initialized successfully"
144
-
145
- except Exception as e:
146
- return False, f"Failed to initialize socket: {str(e)}"
147
-
148
- def set_on_phase_change(self, on_phase_change: Callable[[AcpJob], None]) -> None:
149
- self.on_phase_change = on_phase_change
150
-
56
+
151
57
  def add_produce_item(self, item: IInventory) -> None:
152
58
  self.produced_inventory.append(item)
153
-
154
- def reset_state(self) -> None:
155
- self.acp_client.reset_state()
156
-
157
- def delete_completed_job(self, job_id: int) -> None:
158
- self.acp_client.delete_completed_job(job_id)
59
+
60
+ def memo_to_dict(self, m):
61
+ return {
62
+ "id": m.id,
63
+ "type": m.type.name,
64
+ "content": m.content,
65
+ "next_phase": m.next_phase.name
66
+ }
67
+
68
+ def _to_state_acp_job(self, job: ACPJob) -> Dict:
69
+ memos = []
70
+ for memo in job.memos:
71
+ memos.append(self.memo_to_dict(memo))
72
+
73
+
74
+ return {
75
+ "jobId": job.id,
76
+ "clientName": job.client_agent.name if job.client_agent else "",
77
+ "providerName": job.provider_agent.name if job.provider_agent else "",
78
+ "desc": job.service_requirement or "",
79
+ "price": str(job.price),
80
+ "providerAddress": job.provider_address,
81
+ "phase": ACP_JOB_PHASE_MAP.get(job.phase),
82
+ "memo": list(reversed(memos)),
83
+ "tweetHistory": [
84
+ {
85
+ "type": tweet.get("type"),
86
+ "tweet_id": tweet.get("tweetId"),
87
+ "content": tweet.get("content"),
88
+ "created_at": tweet.get("createdAt")
89
+ }
90
+ for tweet in reversed(job.context.get('tweets', []) if job.context else [])
91
+ ],
92
+ }
159
93
 
160
94
  def get_acp_state(self) -> Dict:
161
- server_state = self.acp_client.get_state()
162
- server_state.inventory.produced = self.produced_inventory
163
- state = asdict(server_state)
164
- return state
95
+ active_jobs = self.acp_client.get_active_jobs()
96
+ completed_jobs = self.acp_client.get_completed_jobs()
97
+ cancelled_jobs = self.acp_client.get_cancelled_jobs()
98
+
99
+ agent_addr = self.acp_client.agent_address.lower()
100
+
101
+ active_buyer_jobs = []
102
+ active_seller_jobs = []
103
+
104
+ for job in active_jobs:
105
+ processed = self._to_state_acp_job(job)
106
+ client_addr = job.client_address.lower()
107
+ provider_addr = job.provider_address.lower()
108
+
109
+ if client_addr == agent_addr:
110
+ active_buyer_jobs.append(processed)
111
+ if provider_addr == agent_addr:
112
+ active_seller_jobs.append(processed)
113
+
114
+ completed = [self._to_state_acp_job(job) for job in completed_jobs]
115
+ cancelled = [self._to_state_acp_job(job) for job in cancelled_jobs]
116
+
117
+ return {
118
+ "inventory": {
119
+ "acquired": [],
120
+ "produced": [item.model_dump() for item in self.produced_inventory] if self.produced_inventory else [],
121
+ },
122
+ "jobs": {
123
+ "active": {
124
+ "asABuyer": active_buyer_jobs,
125
+ "asASeller": active_seller_jobs,
126
+ },
127
+ "completed": completed,
128
+ "cancelled": cancelled,
129
+ }
130
+ }
165
131
 
166
132
  def get_worker(self, data: Optional[Dict] = None) -> WorkerConfig:
167
133
  functions = data.get("functions") if data else [
@@ -171,7 +137,7 @@ class AcpPlugin:
171
137
  self.pay_job,
172
138
  self.deliver_job,
173
139
  ]
174
-
140
+
175
141
  def get_environment(_function_result, _current_state) -> Dict[str, Any]:
176
142
  environment = data.get_environment() if hasattr(data, "get_environment") else {}
177
143
  return {
@@ -186,7 +152,7 @@ class AcpPlugin:
186
152
  get_state_fn=get_environment,
187
153
  instruction=data.get("instructions") if data else None
188
154
  )
189
-
155
+
190
156
  return worker_config
191
157
 
192
158
  @property
@@ -205,12 +171,12 @@ class AcpPlugin:
205
171
  - Each job tracks:
206
172
  * phase: request (seller should response to accept/reject to the job) → pending_payment (as a buyer to make the payment for the service) → in_progress (seller to deliver the service) → evaluation → completed/rejected
207
173
  """
208
-
174
+
209
175
  def _search_agents_executable(self, reasoning: str, keyword: str) -> Tuple[FunctionResultStatus, str, dict]:
210
176
  if not reasoning:
211
177
  return FunctionResultStatus.FAILED, "Reasoning for the search must be provided. This helps track your decision-making process for future reference.", {}
212
178
 
213
- agents = self.acp_client.browse_agents(self.cluster, keyword, rerank=True, top_k=1)
179
+ agents = self.acp_client.browse_agents(keyword, self.cluster)
214
180
 
215
181
  if not agents:
216
182
  return FunctionResultStatus.FAILED, "No other trading agents found in the system. Please try again later when more agents are available.", {}
@@ -228,7 +194,7 @@ class AcpPlugin:
228
194
  "wallet_address": agent.wallet_address,
229
195
  "offerings": (
230
196
  [
231
- {"name": offering.name, "price": offering.price}
197
+ {"name": offering.type, "price": offering.price}
232
198
  for offering in agent.offerings
233
199
  ]
234
200
  if agent.offerings
@@ -239,7 +205,7 @@ class AcpPlugin:
239
205
  ],
240
206
  "totalAgentsFound": len(agents),
241
207
  "timestamp": datetime.now().timestamp(),
242
- "note": "Use the walletAddress when initiating a job with your chosen trading partner.",
208
+ "note": "Use the wallet_address when initiating a job with your chosen trading partner.",
243
209
  }
244
210
  ),
245
211
  {},
@@ -291,22 +257,21 @@ class AcpPlugin:
291
257
  type="string",
292
258
  description="Detailed specifications for service-based items",
293
259
  )
294
-
260
+
295
261
  require_evaluation_arg = Argument(
296
262
  name="require_evaluation",
297
263
  type="boolean",
298
264
  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.",
299
265
  )
300
-
266
+
301
267
  evaluator_keyword_arg = Argument(
302
268
  name="evaluator_keyword",
303
269
  type="string",
304
270
  description="Keyword to search for a evaluator",
305
271
  )
306
272
 
307
- args = [seller_wallet_address_arg, price_arg, reasoning_arg, service_requirements_arg, require_evaluation_arg,
308
- evaluator_keyword_arg]
309
-
273
+ args = [seller_wallet_address_arg, price_arg, reasoning_arg, service_requirements_arg, require_evaluation_arg, evaluator_keyword_arg]
274
+
310
275
  if hasattr(self, 'twitter_plugin') and self.twitter_plugin is not None:
311
276
  tweet_content_arg = Argument(
312
277
  name="tweet_content",
@@ -314,7 +279,7 @@ class AcpPlugin:
314
279
  description="Tweet content that will be posted about this job. Must include the seller's Twitter handle (with @ symbol) to notify them",
315
280
  )
316
281
  args.append(tweet_content_arg)
317
-
282
+
318
283
  return Function(
319
284
  fn_name="initiate_job",
320
285
  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.",
@@ -322,9 +287,7 @@ class AcpPlugin:
322
287
  executable=self._initiate_job_executable
323
288
  )
324
289
 
325
- def _initiate_job_executable(self, seller_wallet_address: str, price: str, reasoning: str,
326
- service_requirements: str, require_evaluation: str, evaluator_keyword: str,
327
- tweet_content: Optional[str] = None) -> Tuple[FunctionResultStatus, str, dict]:
290
+ 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]:
328
291
  if isinstance(require_evaluation, str):
329
292
  require_evaluation = require_evaluation.lower() == 'true'
330
293
  elif isinstance(require_evaluation, bool):
@@ -334,55 +297,38 @@ class AcpPlugin:
334
297
 
335
298
  if not price:
336
299
  return FunctionResultStatus.FAILED, "Missing price - specify how much you're offering per unit", {}
337
-
300
+
338
301
  if not reasoning:
339
302
  return FunctionResultStatus.FAILED, "Missing reasoning - explain why you're making this purchase request", {}
340
-
303
+
341
304
  try:
342
- state = self.get_acp_state()
343
-
344
- existing_job = next(
345
- (job for job in state["jobs"]["active"]["asABuyer"]
346
- if job["providerAddress"] == seller_wallet_address),
347
- None
348
- )
349
-
350
- if existing_job:
351
- 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", {}
352
-
353
305
  if not seller_wallet_address:
354
306
  return FunctionResultStatus.FAILED, "Missing seller wallet address - specify the agent you want to buy from", {}
355
-
307
+
356
308
  if require_evaluation and not evaluator_keyword:
357
309
  return FunctionResultStatus.FAILED, "Missing validator keyword - provide a keyword to search for a validator", {}
358
-
359
- evaluator_address = self.acp_token_client.get_agent_wallet_address()
360
-
310
+
311
+ evaluator_address = self.acp_client.agent_address
312
+
361
313
  if require_evaluation:
362
- validators = self.acp_client.browse_agents(self.evaluator_cluster, evaluator_keyword, rerank=True,
363
- top_k=1)
364
-
314
+ validators = self.acp_client.browse_agents(evaluator_keyword, self.evaluator_cluster)
315
+
365
316
  if len(validators) == 0:
366
317
  return FunctionResultStatus.FAILED, "No evaluator found - try a different keyword", {}
367
318
 
368
319
  evaluator_address = validators[0].wallet_address
369
-
370
- # ... Rest of validation logic ...
320
+
371
321
  expired_at = datetime.now(timezone.utc) + timedelta(minutes=self.job_expiry_duration_mins)
372
- job_id = self.acp_client.create_job(
322
+ job_id = self.acp_client.initiate_job(
373
323
  seller_wallet_address,
374
- float(price),
375
324
  service_requirements,
325
+ float(price),
376
326
  evaluator_address,
377
327
  expired_at
378
328
  )
379
329
 
380
330
  if hasattr(self, 'twitter_plugin') and self.twitter_plugin is not None and tweet_content is not None:
381
- post_tweet_fn = self.twitter_plugin.get_function('post_tweet')
382
- tweet_id = post_tweet_fn(tweet_content).get('data', {}).get('id')
383
- if tweet_id is not None:
384
- self.acp_client.add_tweet(job_id, tweet_id, tweet_content)
385
- print("Tweet has been posted")
331
+ self._tweet_job(job_id, f"{tweet_content} #{job_id}")
386
332
 
387
333
  return FunctionResultStatus.DONE, json.dumps({
388
334
  "jobId": job_id,
@@ -414,9 +360,9 @@ class AcpPlugin:
414
360
  type="string",
415
361
  description="Why you made this decision",
416
362
  )
417
-
363
+
418
364
  args = [job_id_arg, decision_arg, reasoning_arg]
419
-
365
+
420
366
  if hasattr(self, 'twitter_plugin') and self.twitter_plugin is not None:
421
367
  tweet_content_arg = Argument(
422
368
  name="tweet_content",
@@ -432,20 +378,19 @@ class AcpPlugin:
432
378
  executable=self._respond_job_executable
433
379
  )
434
380
 
435
- def _respond_job_executable(self, job_id: int, decision: str, reasoning: str,
436
- tweet_content: Optional[str] = None) -> Tuple[FunctionResultStatus, str, dict]:
381
+ def _respond_job_executable(self, job_id: int, decision: str, reasoning: str, tweet_content: Optional[str] = None) -> Tuple[FunctionResultStatus, str, dict]:
437
382
  if not job_id:
438
383
  return FunctionResultStatus.FAILED, "Missing job ID - specify which job you're responding to", {}
439
-
384
+
440
385
  if not decision or decision not in ["ACCEPT", "REJECT"]:
441
386
  return FunctionResultStatus.FAILED, "Invalid decision - must be either 'ACCEPT' or 'REJECT'", {}
442
-
387
+
443
388
  if not reasoning:
444
389
  return FunctionResultStatus.FAILED, "Missing reasoning - explain why you made this decision", {}
445
390
 
446
391
  try:
447
392
  state = self.get_acp_state()
448
-
393
+
449
394
  job = next(
450
395
  (c for c in state["jobs"]["active"]["asASeller"] if c["jobId"] == job_id),
451
396
  None
@@ -457,14 +402,17 @@ class AcpPlugin:
457
402
  if job["phase"] != AcpJobPhasesDesc.REQUEST:
458
403
  return FunctionResultStatus.FAILED, f"Cannot respond - job is in '{job['phase']}' phase, must be in 'request' phase", {}
459
404
 
460
- self.acp_client.response_job(
405
+ self.acp_client.respond_to_job_memo(
461
406
  job_id,
462
- decision == "ACCEPT",
463
407
  job["memo"][0]["id"],
408
+ decision == "ACCEPT",
464
409
  reasoning
465
410
  )
466
411
 
467
- self._reply_tweet(job, tweet_content)
412
+ if hasattr(self, 'twitter_plugin') and self.twitter_plugin is not None and tweet_content is not None:
413
+ tweet_id = job.get("tweetHistory", [])[0].get("tweet_id") if job.get("tweetHistory") else None
414
+ if tweet_id:
415
+ self._tweet_job(job_id, tweet_content, tweet_id)
468
416
 
469
417
  return FunctionResultStatus.DONE, json.dumps({
470
418
  "jobId": job_id,
@@ -495,7 +443,7 @@ class AcpPlugin:
495
443
  )
496
444
 
497
445
  args = [job_id_arg, amount_arg, reasoning_arg]
498
-
446
+
499
447
  if hasattr(self, 'twitter_plugin') and self.twitter_plugin is not None:
500
448
  tweet_content_arg = Argument(
501
449
  name="tweet_content",
@@ -511,8 +459,7 @@ class AcpPlugin:
511
459
  executable=self._pay_job_executable
512
460
  )
513
461
 
514
- def _pay_job_executable(self, job_id: int, amount: float, reasoning: str, tweet_content: Optional[str] = None) -> \
515
- Tuple[FunctionResultStatus, str, dict]:
462
+ def _pay_job_executable(self, job_id: int, amount: float, reasoning: str, tweet_content: Optional[str] = None) -> Tuple[FunctionResultStatus, str, dict]:
516
463
  if not job_id:
517
464
  return FunctionResultStatus.FAILED, "Missing job ID - specify which job you're paying for", {}
518
465
 
@@ -524,7 +471,7 @@ class AcpPlugin:
524
471
 
525
472
  try:
526
473
  state = self.get_acp_state()
527
-
474
+
528
475
  job = next(
529
476
  (c for c in state["jobs"]["active"]["asABuyer"] if c["jobId"] == job_id),
530
477
  None
@@ -536,14 +483,18 @@ class AcpPlugin:
536
483
  if job["phase"] != AcpJobPhasesDesc.NEGOTIATION:
537
484
  return FunctionResultStatus.FAILED, f"Cannot pay - job is in '{job['phase']}' phase, must be in 'negotiation' phase", {}
538
485
 
539
- self.acp_client.make_payment(
486
+
487
+ self.acp_client.pay_for_job(
540
488
  job_id,
541
- amount,
542
489
  job["memo"][0]["id"],
490
+ amount,
543
491
  reasoning
544
492
  )
545
493
 
546
- self._reply_tweet(job, tweet_content)
494
+ if hasattr(self, 'twitter_plugin') and self.twitter_plugin is not None and tweet_content is not None:
495
+ tweet_id = job.get("tweetHistory", [])[0].get("tweet_id") if job.get("tweetHistory") else None
496
+ if tweet_id:
497
+ self._tweet_job(job_id, tweet_content, tweet_id)
547
498
 
548
499
  return FunctionResultStatus.DONE, json.dumps({
549
500
  "jobId": job_id,
@@ -575,7 +526,7 @@ class AcpPlugin:
575
526
  )
576
527
 
577
528
  args = [job_id_arg, deliverable_arg, reasoning_arg]
578
-
529
+
579
530
  if hasattr(self, 'twitter_plugin') and self.twitter_plugin is not None:
580
531
  tweet_content_arg = Argument(
581
532
  name="tweet_content",
@@ -591,20 +542,19 @@ class AcpPlugin:
591
542
  executable=self._deliver_job_executable
592
543
  )
593
544
 
594
- def _deliver_job_executable(self, job_id: int, deliverable: str, reasoning: str,
595
- tweet_content: Optional[str] = None) -> Tuple[FunctionResultStatus, str, dict]:
545
+ def _deliver_job_executable(self, job_id: int, deliverable: str, reasoning: str, tweet_content: Optional[str] = None) -> Tuple[FunctionResultStatus, str, dict]:
596
546
  if not job_id:
597
547
  return FunctionResultStatus.FAILED, "Missing job ID - specify which job you're delivering for", {}
598
-
548
+
599
549
  if not reasoning:
600
550
  return FunctionResultStatus.FAILED, "Missing reasoning - explain why you're making this delivery", {}
601
-
551
+
602
552
  if not deliverable:
603
553
  return FunctionResultStatus.FAILED, "Missing deliverable - specify what you're delivering", {}
604
554
 
605
555
  try:
606
556
  state = self.get_acp_state()
607
-
557
+
608
558
  job = next(
609
559
  (c for c in state["jobs"]["active"]["asASeller"] if c["jobId"] == job_id),
610
560
  None
@@ -617,42 +567,84 @@ class AcpPlugin:
617
567
  return FunctionResultStatus.FAILED, f"Cannot deliver - job is in '{job['phase']}' phase, must be in 'transaction' phase", {}
618
568
 
619
569
  produced = next(
620
- (i for i in self.produced_inventory if
621
- (i["jobId"] if isinstance(i, dict) else i.jobId) == job["jobId"]),
570
+ (i for i in self.produced_inventory if (i["jobId"] if isinstance(i, dict) else i.jobId) == job["jobId"]),
622
571
  None
623
572
  )
624
573
 
625
574
  if not produced:
626
575
  return FunctionResultStatus.FAILED, "Cannot deliver - you should be producing the deliverable first before delivering it", {}
627
576
 
628
- deliverable: dict = {
577
+ _deliverable: dict = {
629
578
  "type": produced.type,
630
579
  "value": produced.value
631
580
  }
632
581
 
633
- self.acp_client.deliver_job(
582
+ self.acp_client.submit_job_deliverable(
634
583
  job_id,
635
- json.dumps(deliverable),
584
+ json.dumps(_deliverable),
636
585
  )
637
586
 
638
- self._reply_tweet(job, tweet_content)
587
+ if hasattr(self, 'twitter_plugin') and self.twitter_plugin is not None and tweet_content is not None:
588
+ tweet_id = job.get("tweetHistory", [])[0].get("tweet_id") if job.get("tweetHistory") else None
589
+ if tweet_id:
590
+ self._tweet_job(job_id, tweet_content, tweet_id)
591
+
639
592
  return FunctionResultStatus.DONE, json.dumps({
640
593
  "status": "success",
641
594
  "jobId": job_id,
642
- "deliverable": deliverable,
595
+ "deliverable": _deliverable,
643
596
  "timestamp": datetime.now().timestamp()
644
597
  }), {}
645
598
  except Exception as e:
646
599
  print(traceback.format_exc())
647
600
  return FunctionResultStatus.FAILED, f"System error while delivering items - try again after a short delay. {str(e)}", {}
648
601
 
649
- def _reply_tweet(self, job: dict, tweet_content: str):
650
- if hasattr(self, 'twitter_plugin') and self.twitter_plugin is not None and tweet_content is not None:
651
- tweet_history = job.get("tweetHistory", [])
652
- tweet_id = tweet_history[-1].get("tweetId") if tweet_history else None
653
- if tweet_id is not None:
654
- reply_tweet_fn = self.twitter_plugin.get_function('reply_tweet')
655
- tweet_id = reply_tweet_fn(tweet_id, tweet_content, None).get('data', {}).get('id')
656
- if tweet_id is not None:
657
- self.acp_client.add_tweet(job.get("jobId"), tweet_id, tweet_content)
658
- print("Tweet has been posted")
602
+ def _tweet_job(self, job_id: int, content: str, tweet_id: Optional[str] = None):
603
+ if not hasattr(self, 'twitter_plugin') or self.twitter_plugin is None:
604
+ return
605
+
606
+ job = self.acp_client.get_job_by_onchain_id(job_id)
607
+ if not job:
608
+ raise Exception("ERROR (tweetJob): Job not found")
609
+
610
+
611
+ if tweet_id :
612
+ response = self.twitter_plugin.twitter_client.create_tweet(
613
+ text=content,
614
+ in_reply_to_tweet_id=tweet_id
615
+ )
616
+ else:
617
+ response = self.twitter_plugin.twitter_client.create_tweet(text=content)
618
+
619
+
620
+ role = "buyer" if job.client_address.lower() == self.acp_client.agent_address.lower() else "seller"
621
+
622
+ # Safely extract tweet ID
623
+ tweet_id = None
624
+ if isinstance(response, dict):
625
+ tweet_id = response.get('data', {}).get('id') or response.get('id')
626
+
627
+ context = {
628
+ **(job.context or {}),
629
+ 'tweets': [
630
+ *((job.context or {}).get('tweets', [])),
631
+ {
632
+ 'type': role,
633
+ 'tweetId': tweet_id,
634
+ 'content': content,
635
+ 'createdAt': int(datetime.now().timestamp() * 1000)
636
+ },
637
+ ],
638
+ }
639
+
640
+ response = requests.patch(
641
+ f"{self.acp_base_url}/jobs/{job_id}/context",
642
+ headers={
643
+ "Content-Type": "application/json",
644
+ "wallet-address": self.acp_client.agent_address,
645
+ },
646
+ json={"data": {"context": context}}
647
+ )
648
+
649
+ if not response.ok:
650
+ raise Exception(f"ERROR (tweetJob): {response.status_code} {response.text}")
@@ -0,0 +1,35 @@
1
+ from typing import Optional
2
+ from virtuals_acp.env import EnvSettings
3
+ from pydantic import field_validator
4
+
5
+ class PluginEnvSettings(EnvSettings):
6
+ GAME_DEV_API_KEY: str
7
+ GAME_API_KEY: str
8
+ BUYER_AGENT_GAME_TWITTER_ACCESS_TOKEN: str
9
+ SELLER_AGENT_GAME_TWITTER_ACCESS_TOKEN: str
10
+ WHITELISTED_WALLET_ENTITY_ID: Optional[int] = None
11
+ # BUYER_AGENT_TWITTER_BEARER_TOKEN: str
12
+ # BUYER_AGENT_TWITTER_API_KEY: str
13
+ # BUYER_AGENT_TWITTER_API_SECRET_KEY: str
14
+ # BUYER_AGENT_TWITTER_ACCESS_TOKEN: str
15
+ # BUYER_AGENT_TWITTER_ACCESS_TOKEN_SECRET: str
16
+ # SELLER_AGENT_TWITTER_BEARER_TOKEN: str
17
+ # SELLER_AGENT_TWITTER_API_KEY: str
18
+ # SELLER_AGENT_TWITTER_API_SECRET_KEY: str
19
+ # SELLER_AGENT_TWITTER_ACCESS_TOKEN: str
20
+ # SELLER_AGENT_TWITTER_ACCESS_TOKEN_SECRET: str
21
+
22
+ @field_validator("GAME_DEV_API_KEY", "GAME_API_KEY")
23
+ @classmethod
24
+ def check_apt_prefix(cls, v: str) -> str:
25
+ if v and not v.startswith("apt-"):
26
+ raise ValueError("GAME key must start with 'apt-'")
27
+ return v
28
+
29
+ @field_validator("BUYER_AGENT_GAME_TWITTER_ACCESS_TOKEN", "SELLER_AGENT_GAME_TWITTER_ACCESS_TOKEN")
30
+ @classmethod
31
+ def check_apx_prefix(cls, v: str) -> str:
32
+ if v and not v.startswith("apx-"):
33
+ raise ValueError("SELLER_AGENT_GAME_TWITTER_ACCESS_TOKEN must start with 'apx-'")
34
+ return v
35
+