acp-plugin-gamesdk 0.1.23__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.
- acp_plugin_gamesdk/acp_plugin.py +194 -202
- acp_plugin_gamesdk/env.py +35 -0
- acp_plugin_gamesdk/interface.py +73 -112
- {acp_plugin_gamesdk-0.1.23.dist-info → acp_plugin_gamesdk-0.1.26.dist-info}/METADATA +4 -14
- acp_plugin_gamesdk-0.1.26.dist-info/RECORD +6 -0
- acp_plugin_gamesdk/acp_client.py +0 -304
- acp_plugin_gamesdk/acp_token.py +0 -352
- acp_plugin_gamesdk/acp_token_abi.py +0 -678
- acp_plugin_gamesdk/configs.py +0 -44
- acp_plugin_gamesdk-0.1.23.dist-info/RECORD +0 -9
- {acp_plugin_gamesdk-0.1.23.dist-info → acp_plugin_gamesdk-0.1.26.dist-info}/WHEEL +0 -0
acp_plugin_gamesdk/acp_plugin.py
CHANGED
@@ -1,54 +1,32 @@
|
|
1
1
|
import json
|
2
|
-
import signal
|
3
|
-
import sys
|
4
2
|
import traceback
|
5
|
-
from
|
6
|
-
from
|
7
|
-
from
|
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
|
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
|
-
|
30
|
-
twitter_plugin: TwitterPlugin |
|
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.
|
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.
|
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
|
155
|
-
|
156
|
-
|
157
|
-
|
158
|
-
|
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
|
-
|
162
|
-
|
163
|
-
|
164
|
-
|
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
|
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.
|
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
|
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
|
-
|
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.
|
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
|
363
|
-
|
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.
|
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
|
-
|
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.
|
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
|
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
|
-
|
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
|
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
|
-
|
577
|
+
_deliverable: dict = {
|
629
578
|
"type": produced.type,
|
630
579
|
"value": produced.value
|
631
580
|
}
|
632
581
|
|
633
|
-
self.acp_client.
|
582
|
+
self.acp_client.submit_job_deliverable(
|
634
583
|
job_id,
|
635
|
-
json.dumps(
|
584
|
+
json.dumps(_deliverable),
|
636
585
|
)
|
637
586
|
|
638
|
-
self
|
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":
|
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
|
650
|
-
if hasattr(self, 'twitter_plugin')
|
651
|
-
|
652
|
-
|
653
|
-
|
654
|
-
|
655
|
-
|
656
|
-
|
657
|
-
|
658
|
-
|
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
|
+
|