acp-plugin-gamesdk 0.1.19__py3-none-any.whl → 0.1.21__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- acp_plugin_gamesdk/acp_client.py +71 -65
- acp_plugin_gamesdk/acp_plugin.py +84 -75
- acp_plugin_gamesdk/acp_token.py +95 -81
- acp_plugin_gamesdk/configs.py +5 -3
- acp_plugin_gamesdk/interface.py +35 -22
- {acp_plugin_gamesdk-0.1.19.dist-info → acp_plugin_gamesdk-0.1.21.dist-info}/METADATA +1 -1
- acp_plugin_gamesdk-0.1.21.dist-info/RECORD +9 -0
- acp_plugin_gamesdk-0.1.19.dist-info/RECORD +0 -9
- {acp_plugin_gamesdk-0.1.19.dist-info → acp_plugin_gamesdk-0.1.21.dist-info}/WHEEL +0 -0
acp_plugin_gamesdk/acp_client.py
CHANGED
@@ -1,15 +1,20 @@
|
|
1
|
-
import json
|
2
|
-
import requests
|
3
1
|
import time
|
4
2
|
import traceback
|
5
|
-
|
6
|
-
from datetime import datetime, timedelta, timezone
|
3
|
+
from datetime import datetime, timezone
|
7
4
|
from typing import List, Optional
|
5
|
+
|
6
|
+
import requests
|
7
|
+
from dacite import Config, from_dict
|
8
8
|
from web3 import Web3
|
9
9
|
|
10
|
-
from acp_plugin_gamesdk.interface import AcpAgent, AcpJobPhases, AcpOffering, AcpState, AcpJobPhasesDesc
|
11
10
|
from acp_plugin_gamesdk.acp_token import AcpToken, MemoType
|
12
|
-
from
|
11
|
+
from acp_plugin_gamesdk.interface import (
|
12
|
+
AcpAgent,
|
13
|
+
AcpJobPhases,
|
14
|
+
AcpJobPhasesDesc,
|
15
|
+
AcpOffering,
|
16
|
+
AcpState,
|
17
|
+
)
|
13
18
|
|
14
19
|
|
15
20
|
class AcpClient:
|
@@ -26,22 +31,23 @@ class AcpClient:
|
|
26
31
|
return self.acp_token.get_agent_wallet_address()
|
27
32
|
|
28
33
|
def get_state(self) -> AcpState:
|
29
|
-
response =
|
34
|
+
response = requests.get(
|
30
35
|
f"{self.base_url}/states/{self.agent_wallet_address}",
|
31
36
|
headers={"x-api-key": self.api_key}
|
32
37
|
)
|
33
38
|
payload = response.json()
|
34
|
-
result = from_dict(data_class=AcpState, data=payload,
|
39
|
+
result = from_dict(data_class=AcpState, data=payload,
|
40
|
+
config=Config(type_hooks={AcpJobPhasesDesc: AcpJobPhasesDesc}))
|
35
41
|
return result
|
36
42
|
|
37
43
|
def browse_agents(
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
44
|
+
self,
|
45
|
+
cluster: Optional[str] = None,
|
46
|
+
query: Optional[str] = None,
|
47
|
+
rerank: Optional[bool] = True,
|
48
|
+
top_k: Optional[int] = 1,
|
43
49
|
) -> List[AcpAgent]:
|
44
|
-
|
50
|
+
|
45
51
|
url = f"{self.acp_base_url}/agents"
|
46
52
|
|
47
53
|
params = {
|
@@ -55,22 +61,22 @@ class AcpClient:
|
|
55
61
|
|
56
62
|
if response.status_code != 200:
|
57
63
|
raise Exception(
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
64
|
+
f"Error occured in browse_agents function. Failed to browse agents.\n"
|
65
|
+
f"Response status code: {response.status_code}\n"
|
66
|
+
f"Response description: {response.text}\n"
|
67
|
+
)
|
63
68
|
|
64
69
|
response_json = response.json()
|
65
|
-
|
70
|
+
|
66
71
|
result = []
|
67
|
-
|
72
|
+
|
68
73
|
for agent in response_json.get("data", []):
|
69
74
|
if agent["offerings"]:
|
70
|
-
offerings = [AcpOffering(name=offering["name"], price=offering["price"]) for offering in
|
75
|
+
offerings = [AcpOffering(name=offering["name"], price=offering["price"]) for offering in
|
76
|
+
agent["offerings"]]
|
71
77
|
else:
|
72
78
|
offerings = None
|
73
|
-
|
79
|
+
|
74
80
|
result.append(
|
75
81
|
AcpAgent(
|
76
82
|
id=agent["id"],
|
@@ -83,7 +89,7 @@ class AcpClient:
|
|
83
89
|
explanation=agent["explanation"]
|
84
90
|
)
|
85
91
|
)
|
86
|
-
|
92
|
+
|
87
93
|
return result
|
88
94
|
|
89
95
|
def create_job(
|
@@ -95,53 +101,53 @@ class AcpClient:
|
|
95
101
|
expired_at: datetime,
|
96
102
|
) -> int:
|
97
103
|
tx_result = self.acp_token.create_job(
|
98
|
-
provider_address
|
99
|
-
evaluator_address
|
100
|
-
expire_at
|
104
|
+
provider_address=provider_address,
|
105
|
+
evaluator_address=evaluator_address,
|
106
|
+
expire_at=expired_at
|
101
107
|
)
|
102
|
-
|
108
|
+
|
103
109
|
job_id = None
|
104
110
|
retry_count = 3
|
105
111
|
retry_delay = 3
|
106
|
-
|
107
|
-
time.sleep(retry_delay)
|
112
|
+
|
113
|
+
time.sleep(retry_delay)
|
108
114
|
for attempt in range(retry_count):
|
109
115
|
try:
|
110
116
|
response = self.acp_token.validate_transaction(tx_result["txHash"])
|
111
117
|
data = response.get("data", {})
|
112
118
|
if not data:
|
113
119
|
raise Exception("Invalid tx_hash!")
|
114
|
-
|
120
|
+
|
115
121
|
if data.get("status") == "retry":
|
116
122
|
raise Exception("Transaction failed, retrying...")
|
117
|
-
|
123
|
+
|
118
124
|
if data.get("status") == "failed":
|
119
125
|
break
|
120
|
-
|
126
|
+
|
121
127
|
if data.get("status") == "success":
|
122
128
|
job_id = int(data.get("result").get("jobId"))
|
123
|
-
|
129
|
+
|
124
130
|
if job_id is not None and job_id != "":
|
125
|
-
break
|
126
|
-
|
131
|
+
break
|
132
|
+
|
127
133
|
except Exception as e:
|
128
134
|
print(f"Error in create_job function: {e}")
|
129
135
|
print(traceback.format_exc())
|
130
136
|
if attempt < retry_count - 1:
|
131
|
-
time.sleep(retry_delay)
|
137
|
+
time.sleep(retry_delay)
|
132
138
|
else:
|
133
139
|
raise
|
134
|
-
|
140
|
+
|
135
141
|
if job_id is None or job_id == "":
|
136
142
|
raise Exception("Failed to create job")
|
137
|
-
|
143
|
+
|
138
144
|
self.acp_token.create_memo(
|
139
|
-
|
140
|
-
|
141
|
-
|
142
|
-
|
143
|
-
|
144
|
-
|
145
|
+
job_id=job_id,
|
146
|
+
content=job_description,
|
147
|
+
memo_type=MemoType.MESSAGE,
|
148
|
+
is_secured=False,
|
149
|
+
next_phase=AcpJobPhases.NEGOTIATION
|
150
|
+
)
|
145
151
|
|
146
152
|
payload = {
|
147
153
|
"jobId": job_id,
|
@@ -169,7 +175,7 @@ class AcpClient:
|
|
169
175
|
if accept:
|
170
176
|
self.acp_token.sign_memo(memo_id, accept, reasoning)
|
171
177
|
time.sleep(5)
|
172
|
-
|
178
|
+
|
173
179
|
return self.acp_token.create_memo(
|
174
180
|
job_id=job_id,
|
175
181
|
content=f"Job {job_id} accepted. {reasoning}",
|
@@ -189,7 +195,7 @@ class AcpClient:
|
|
189
195
|
def make_payment(self, job_id: int, amount: float, memo_id: int, reason: str):
|
190
196
|
# Convert amount to Wei (smallest ETH unit)
|
191
197
|
amount_wei = self.web3.to_wei(amount, 'ether')
|
192
|
-
|
198
|
+
|
193
199
|
self.acp_token.set_budget(job_id, amount_wei)
|
194
200
|
time.sleep(5)
|
195
201
|
self.acp_token.approve_allowance(amount_wei)
|
@@ -218,7 +224,7 @@ class AcpClient:
|
|
218
224
|
"tweetId": tweet_id,
|
219
225
|
"content": content
|
220
226
|
}
|
221
|
-
|
227
|
+
|
222
228
|
response = requests.post(
|
223
229
|
f"{self.base_url}/{job_id}/tweets/{self.agent_wallet_address}",
|
224
230
|
json=payload,
|
@@ -228,65 +234,65 @@ class AcpClient:
|
|
228
234
|
"x-api-key": self.api_key
|
229
235
|
}
|
230
236
|
)
|
231
|
-
|
237
|
+
|
232
238
|
if response.status_code != 200 and response.status_code != 201:
|
233
239
|
raise Exception(
|
234
|
-
f"Error occured in add_tweet function. Failed to add tweet.\n"
|
240
|
+
f"Error occured in add_tweet function. Failed to add tweet.\n"
|
235
241
|
f"Response status code: {response.status_code}\n"
|
236
242
|
f"Response description: {response.text}\n"
|
237
243
|
)
|
238
|
-
|
239
|
-
|
244
|
+
|
240
245
|
return response.json()
|
241
|
-
|
246
|
+
|
242
247
|
def reset_state(self) -> None:
|
243
248
|
response = requests.delete(
|
244
249
|
f"{self.base_url}/states/{self.agent_wallet_address}",
|
245
250
|
headers={"x-api-key": self.api_key}
|
246
251
|
)
|
247
|
-
|
252
|
+
|
248
253
|
if response.status_code not in [200, 204]:
|
249
254
|
raise Exception(
|
250
|
-
f"Error occured in reset_state function. Failed to reset state\n"
|
255
|
+
f"Error occured in reset_state function. Failed to reset state\n"
|
251
256
|
f"Response status code: {response.status_code}\n"
|
252
257
|
f"Response description: {response.text}\n"
|
253
258
|
)
|
254
|
-
|
259
|
+
|
255
260
|
def delete_completed_job(self, job_id: int) -> None:
|
256
261
|
response = requests.delete(
|
257
262
|
f"{self.base_url}/{job_id}/wallet/{self.agent_wallet_address}",
|
258
263
|
headers={"x-api-key": self.api_key}
|
259
264
|
)
|
260
|
-
|
265
|
+
|
261
266
|
if response.status_code not in [200, 204]:
|
262
267
|
raise Exception(
|
263
268
|
f"Error occurred in delete_completed_job function. Failed to delete job.\n"
|
264
269
|
f"Response status code: {response.status_code}\n"
|
265
270
|
f"Response description: {response.text}\n"
|
266
271
|
)
|
267
|
-
|
272
|
+
|
268
273
|
def get_agent_by_wallet_address(self, wallet_address: str) -> AcpAgent:
|
269
274
|
url = f"{self.acp_base_url}/agents?filters[walletAddress]={wallet_address}"
|
270
|
-
|
275
|
+
|
271
276
|
response = requests.get(
|
272
277
|
url,
|
273
278
|
)
|
274
|
-
|
279
|
+
|
275
280
|
if response.status_code != 200:
|
276
281
|
raise Exception(
|
277
282
|
f"Failed to get agent: {response.status_code} {response.text}"
|
278
283
|
)
|
279
|
-
|
284
|
+
|
280
285
|
response_json = response.json()
|
281
|
-
|
286
|
+
|
282
287
|
result = []
|
283
|
-
|
288
|
+
|
284
289
|
for agent in response_json.get("data", []):
|
285
290
|
if agent["offerings"]:
|
286
|
-
offerings = [AcpOffering(name=offering["name"], price=offering["price"]) for offering in
|
291
|
+
offerings = [AcpOffering(name=offering["name"], price=offering["price"]) for offering in
|
292
|
+
agent["offerings"]]
|
287
293
|
else:
|
288
294
|
offerings = None
|
289
|
-
|
295
|
+
|
290
296
|
result.append(
|
291
297
|
AcpAgent(
|
292
298
|
id=agent["id"],
|
acp_plugin_gamesdk/acp_plugin.py
CHANGED
@@ -1,22 +1,27 @@
|
|
1
|
-
|
1
|
+
import json
|
2
2
|
import signal
|
3
3
|
import sys
|
4
|
-
from typing import List, Dict, Any, Optional,Tuple
|
5
|
-
import json
|
6
|
-
from dataclasses import dataclass, asdict
|
7
|
-
from datetime import datetime, timezone, timedelta
|
8
4
|
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
|
9
9
|
|
10
10
|
import socketio
|
11
|
-
import socketio.client
|
12
11
|
|
13
12
|
from game_sdk.game.agent import WorkerConfig
|
14
13
|
from game_sdk.game.custom_types import Argument, Function, FunctionResultStatus
|
15
|
-
from twitter_plugin_gamesdk.twitter_plugin import TwitterPlugin
|
16
14
|
from twitter_plugin_gamesdk.game_twitter_plugin import GameTwitterPlugin
|
15
|
+
from twitter_plugin_gamesdk.twitter_plugin import TwitterPlugin
|
17
16
|
from acp_plugin_gamesdk.acp_client import AcpClient
|
18
17
|
from acp_plugin_gamesdk.acp_token import AcpToken
|
19
|
-
from acp_plugin_gamesdk.interface import
|
18
|
+
from acp_plugin_gamesdk.interface import (
|
19
|
+
AcpJob,
|
20
|
+
AcpJobPhasesDesc,
|
21
|
+
IDeliverable,
|
22
|
+
IInventory,
|
23
|
+
)
|
24
|
+
|
20
25
|
|
21
26
|
@dataclass
|
22
27
|
class AcpPluginOptions:
|
@@ -28,16 +33,17 @@ class AcpPluginOptions:
|
|
28
33
|
on_evaluate: Optional[Callable[[IDeliverable], Tuple[bool, str]]] = None
|
29
34
|
on_phase_change: Optional[Callable[[AcpJob], None]] = None
|
30
35
|
job_expiry_duration_mins: Optional[int] = None
|
31
|
-
|
36
|
+
|
32
37
|
|
33
38
|
SocketEvents = {
|
34
39
|
"JOIN_EVALUATOR_ROOM": "joinEvaluatorRoom",
|
35
|
-
"LEAVE_EVALUATOR_ROOM": "leaveEvaluatorRoom",
|
40
|
+
"LEAVE_EVALUATOR_ROOM": "leaveEvaluatorRoom",
|
36
41
|
"ON_EVALUATE": "onEvaluate",
|
37
|
-
"ROOM_JOINED"
|
42
|
+
"ROOM_JOINED": "roomJoined",
|
38
43
|
"ON_PHASE_CHANGE": "onPhaseChange"
|
39
44
|
}
|
40
45
|
|
46
|
+
|
41
47
|
class AcpPlugin:
|
42
48
|
def __init__(self, options: AcpPluginOptions):
|
43
49
|
print("Initializing AcpPlugin")
|
@@ -65,7 +71,7 @@ class AcpPlugin:
|
|
65
71
|
self.twitter_plugin = None
|
66
72
|
if options.twitter_plugin is not None:
|
67
73
|
self.twitter_plugin = options.twitter_plugin
|
68
|
-
|
74
|
+
|
69
75
|
self.produced_inventory: List[IInventory] = []
|
70
76
|
self.acp_base_url = self.acp_token_client.acp_base_url
|
71
77
|
if options.on_evaluate is not None or options.on_phase_change is not None:
|
@@ -74,15 +80,14 @@ class AcpPlugin:
|
|
74
80
|
if options.on_evaluate is not None:
|
75
81
|
self.on_evaluate = options.on_evaluate
|
76
82
|
if options.on_phase_change is not None:
|
77
|
-
def phase_change_wrapper(job
|
83
|
+
def phase_change_wrapper(job: AcpJob):
|
78
84
|
job["getAgentByWalletAddress"] = self.acp_client.get_agent_by_wallet_address
|
79
85
|
return options.on_phase_change(job)
|
86
|
+
|
80
87
|
self.on_phase_change = phase_change_wrapper
|
81
88
|
self.initialize_socket()
|
82
89
|
self.job_expiry_duration_mins = options.job_expiry_duration_mins if options.job_expiry_duration_mins is not None else 1440
|
83
|
-
|
84
|
-
|
85
|
-
|
90
|
+
|
86
91
|
def initialize_socket(self) -> Tuple[bool, str]:
|
87
92
|
"""
|
88
93
|
Initialize socket connection for real-time communication.
|
@@ -90,36 +95,36 @@ class AcpPlugin:
|
|
90
95
|
"""
|
91
96
|
try:
|
92
97
|
self.socket = socketio.Client()
|
93
|
-
|
98
|
+
|
94
99
|
# Set up authentication before connecting
|
95
100
|
self.socket.auth = {
|
96
101
|
"evaluatorAddress": self.acp_token_client.agent_wallet_address
|
97
102
|
}
|
98
|
-
|
103
|
+
|
99
104
|
# Connect socket to GAME SDK dev server
|
100
105
|
self.socket.connect(self.acp_client.base_url, auth=self.socket.auth)
|
101
|
-
|
106
|
+
|
102
107
|
if self.socket.connected:
|
103
108
|
self.socket.emit(SocketEvents["JOIN_EVALUATOR_ROOM"], self.acp_token_client.agent_wallet_address)
|
104
|
-
|
105
|
-
|
109
|
+
|
106
110
|
# Set up event handler for evaluation requests
|
107
111
|
@self.socket.on(SocketEvents["ON_EVALUATE"])
|
108
112
|
def on_evaluate(data):
|
109
113
|
if self.on_evaluate:
|
110
114
|
deliverable = data.get("deliverable")
|
111
115
|
memo_id = data.get("memoId")
|
112
|
-
|
116
|
+
|
113
117
|
is_approved, reasoning = self.on_evaluate(deliverable)
|
114
|
-
|
118
|
+
|
115
119
|
self.acp_token_client.sign_memo(memo_id, is_approved, reasoning)
|
116
|
-
|
117
|
-
|
120
|
+
|
121
|
+
# Set up event handler for phase changes
|
122
|
+
|
118
123
|
@self.socket.on(SocketEvents["ON_PHASE_CHANGE"])
|
119
124
|
def on_phase_change(data):
|
120
125
|
if hasattr(self, 'on_phase_change') and self.on_phase_change:
|
121
126
|
self.on_phase_change(data)
|
122
|
-
|
127
|
+
|
123
128
|
# Set up cleanup function for graceful shutdown
|
124
129
|
def cleanup():
|
125
130
|
if self.socket:
|
@@ -127,34 +132,31 @@ class AcpPlugin:
|
|
127
132
|
import time
|
128
133
|
time.sleep(1)
|
129
134
|
self.socket.disconnect()
|
130
|
-
|
131
|
-
|
132
|
-
|
135
|
+
|
133
136
|
def signal_handler(_sig, _frame):
|
134
137
|
cleanup()
|
135
138
|
sys.exit(0)
|
136
|
-
|
139
|
+
|
137
140
|
signal.signal(signal.SIGINT, signal_handler)
|
138
141
|
signal.signal(signal.SIGTERM, signal_handler)
|
139
|
-
|
142
|
+
|
140
143
|
return True, "Socket initialized successfully"
|
141
|
-
|
144
|
+
|
142
145
|
except Exception as e:
|
143
146
|
return False, f"Failed to initialize socket: {str(e)}"
|
144
|
-
|
145
|
-
|
147
|
+
|
146
148
|
def set_on_phase_change(self, on_phase_change: Callable[[AcpJob], None]) -> None:
|
147
149
|
self.on_phase_change = on_phase_change
|
148
150
|
|
149
151
|
def add_produce_item(self, item: IInventory) -> None:
|
150
152
|
self.produced_inventory.append(item)
|
151
|
-
|
153
|
+
|
152
154
|
def reset_state(self) -> None:
|
153
155
|
self.acp_client.reset_state()
|
154
|
-
|
156
|
+
|
155
157
|
def delete_completed_job(self, job_id: int) -> None:
|
156
158
|
self.acp_client.delete_completed_job(job_id)
|
157
|
-
|
159
|
+
|
158
160
|
def get_acp_state(self) -> Dict:
|
159
161
|
server_state = self.acp_client.get_state()
|
160
162
|
server_state.inventory.produced = self.produced_inventory
|
@@ -169,7 +171,7 @@ class AcpPlugin:
|
|
169
171
|
self.pay_job,
|
170
172
|
self.deliver_job,
|
171
173
|
]
|
172
|
-
|
174
|
+
|
173
175
|
def get_environment(_function_result, _current_state) -> Dict[str, Any]:
|
174
176
|
environment = data.get_environment() if hasattr(data, "get_environment") else {}
|
175
177
|
return {
|
@@ -184,7 +186,7 @@ class AcpPlugin:
|
|
184
186
|
get_state_fn=get_environment,
|
185
187
|
instruction=data.get("instructions") if data else None
|
186
188
|
)
|
187
|
-
|
189
|
+
|
188
190
|
return worker_config
|
189
191
|
|
190
192
|
@property
|
@@ -203,8 +205,8 @@ class AcpPlugin:
|
|
203
205
|
- Each job tracks:
|
204
206
|
* 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
|
205
207
|
"""
|
206
|
-
|
207
|
-
def _search_agents_executable(self,reasoning: str, keyword: str) -> Tuple[FunctionResultStatus, str, dict]:
|
208
|
+
|
209
|
+
def _search_agents_executable(self, reasoning: str, keyword: str) -> Tuple[FunctionResultStatus, str, dict]:
|
208
210
|
if not reasoning:
|
209
211
|
return FunctionResultStatus.FAILED, "Reasoning for the search must be provided. This helps track your decision-making process for future reference.", {}
|
210
212
|
|
@@ -291,21 +293,22 @@ class AcpPlugin:
|
|
291
293
|
type="string",
|
292
294
|
description="Detailed specifications for service-based items",
|
293
295
|
)
|
294
|
-
|
296
|
+
|
295
297
|
require_evaluation_arg = Argument(
|
296
298
|
name="require_evaluation",
|
297
299
|
type="boolean",
|
298
300
|
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
301
|
)
|
300
|
-
|
302
|
+
|
301
303
|
evaluator_keyword_arg = Argument(
|
302
304
|
name="evaluator_keyword",
|
303
305
|
type="string",
|
304
306
|
description="Keyword to search for a evaluator",
|
305
307
|
)
|
306
308
|
|
307
|
-
args = [seller_wallet_address_arg, price_arg, reasoning_arg, service_requirements_arg, require_evaluation_arg,
|
308
|
-
|
309
|
+
args = [seller_wallet_address_arg, price_arg, reasoning_arg, service_requirements_arg, require_evaluation_arg,
|
310
|
+
evaluator_keyword_arg]
|
311
|
+
|
309
312
|
if hasattr(self, 'twitter_plugin') and self.twitter_plugin is not None:
|
310
313
|
tweet_content_arg = Argument(
|
311
314
|
name="tweet_content",
|
@@ -313,7 +316,7 @@ class AcpPlugin:
|
|
313
316
|
description="Tweet content that will be posted about this job. Must include the seller's Twitter handle (with @ symbol) to notify them",
|
314
317
|
)
|
315
318
|
args.append(tweet_content_arg)
|
316
|
-
|
319
|
+
|
317
320
|
return Function(
|
318
321
|
fn_name="initiate_job",
|
319
322
|
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.",
|
@@ -321,7 +324,9 @@ class AcpPlugin:
|
|
321
324
|
executable=self._initiate_job_executable
|
322
325
|
)
|
323
326
|
|
324
|
-
def _initiate_job_executable(self, seller_wallet_address: str, price: str, reasoning: str,
|
327
|
+
def _initiate_job_executable(self, seller_wallet_address: str, price: str, reasoning: str,
|
328
|
+
service_requirements: str, require_evaluation: str, evaluator_keyword: str,
|
329
|
+
tweet_content: Optional[str] = None) -> Tuple[FunctionResultStatus, str, dict]:
|
325
330
|
if isinstance(require_evaluation, str):
|
326
331
|
require_evaluation = require_evaluation.lower() == 'true'
|
327
332
|
elif isinstance(require_evaluation, bool):
|
@@ -331,10 +336,10 @@ class AcpPlugin:
|
|
331
336
|
|
332
337
|
if not price:
|
333
338
|
return FunctionResultStatus.FAILED, "Missing price - specify how much you're offering per unit", {}
|
334
|
-
|
339
|
+
|
335
340
|
if not reasoning:
|
336
341
|
return FunctionResultStatus.FAILED, "Missing reasoning - explain why you're making this purchase request", {}
|
337
|
-
|
342
|
+
|
338
343
|
try:
|
339
344
|
state = self.get_acp_state()
|
340
345
|
|
@@ -345,24 +350,25 @@ class AcpPlugin:
|
|
345
350
|
)
|
346
351
|
|
347
352
|
if existing_job:
|
348
|
-
return FunctionResultStatus.FAILED, f"You already have an active job as a buyer with {existing_job['providerAddress']} - complete the current job before initiating a new one", {}
|
349
|
-
|
353
|
+
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", {}
|
354
|
+
|
350
355
|
if not seller_wallet_address:
|
351
356
|
return FunctionResultStatus.FAILED, "Missing seller wallet address - specify the agent you want to buy from", {}
|
352
|
-
|
357
|
+
|
353
358
|
if require_evaluation and not evaluator_keyword:
|
354
359
|
return FunctionResultStatus.FAILED, "Missing validator keyword - provide a keyword to search for a validator", {}
|
355
|
-
|
360
|
+
|
356
361
|
evaluator_address = self.acp_token_client.get_agent_wallet_address()
|
357
|
-
|
362
|
+
|
358
363
|
if require_evaluation:
|
359
|
-
validators = self.acp_client.browse_agents(self.evaluator_cluster, evaluator_keyword, rerank=True,
|
360
|
-
|
364
|
+
validators = self.acp_client.browse_agents(self.evaluator_cluster, evaluator_keyword, rerank=True,
|
365
|
+
top_k=1)
|
366
|
+
|
361
367
|
if len(validators) == 0:
|
362
368
|
return FunctionResultStatus.FAILED, "No evaluator found - try a different keyword", {}
|
363
369
|
|
364
370
|
evaluator_address = validators[0].wallet_address
|
365
|
-
|
371
|
+
|
366
372
|
# ... Rest of validation logic ...
|
367
373
|
expired_at = datetime.now(timezone.utc) + timedelta(minutes=self.job_expiry_duration_mins)
|
368
374
|
job_id = self.acp_client.create_job(
|
@@ -410,9 +416,9 @@ class AcpPlugin:
|
|
410
416
|
type="string",
|
411
417
|
description="Why you made this decision",
|
412
418
|
)
|
413
|
-
|
419
|
+
|
414
420
|
args = [job_id_arg, decision_arg, reasoning_arg]
|
415
|
-
|
421
|
+
|
416
422
|
if hasattr(self, 'twitter_plugin') and self.twitter_plugin is not None:
|
417
423
|
tweet_content_arg = Argument(
|
418
424
|
name="tweet_content",
|
@@ -428,19 +434,20 @@ class AcpPlugin:
|
|
428
434
|
executable=self._respond_job_executable
|
429
435
|
)
|
430
436
|
|
431
|
-
def _respond_job_executable(self, job_id: int, decision: str, reasoning: str,
|
437
|
+
def _respond_job_executable(self, job_id: int, decision: str, reasoning: str,
|
438
|
+
tweet_content: Optional[str] = None) -> Tuple[FunctionResultStatus, str, dict]:
|
432
439
|
if not job_id:
|
433
440
|
return FunctionResultStatus.FAILED, "Missing job ID - specify which job you're responding to", {}
|
434
|
-
|
441
|
+
|
435
442
|
if not decision or decision not in ["ACCEPT", "REJECT"]:
|
436
443
|
return FunctionResultStatus.FAILED, "Invalid decision - must be either 'ACCEPT' or 'REJECT'", {}
|
437
|
-
|
444
|
+
|
438
445
|
if not reasoning:
|
439
446
|
return FunctionResultStatus.FAILED, "Missing reasoning - explain why you made this decision", {}
|
440
447
|
|
441
448
|
try:
|
442
449
|
state = self.get_acp_state()
|
443
|
-
|
450
|
+
|
444
451
|
job = next(
|
445
452
|
(c for c in state["jobs"]["active"]["asASeller"] if c["jobId"] == job_id),
|
446
453
|
None
|
@@ -490,7 +497,7 @@ class AcpPlugin:
|
|
490
497
|
)
|
491
498
|
|
492
499
|
args = [job_id_arg, amount_arg, reasoning_arg]
|
493
|
-
|
500
|
+
|
494
501
|
if hasattr(self, 'twitter_plugin') and self.twitter_plugin is not None:
|
495
502
|
tweet_content_arg = Argument(
|
496
503
|
name="tweet_content",
|
@@ -506,7 +513,8 @@ class AcpPlugin:
|
|
506
513
|
executable=self._pay_job_executable
|
507
514
|
)
|
508
515
|
|
509
|
-
def _pay_job_executable(self, job_id: int, amount: float, reasoning: str, tweet_content: Optional[str] = None) ->
|
516
|
+
def _pay_job_executable(self, job_id: int, amount: float, reasoning: str, tweet_content: Optional[str] = None) -> \
|
517
|
+
Tuple[FunctionResultStatus, str, dict]:
|
510
518
|
if not job_id:
|
511
519
|
return FunctionResultStatus.FAILED, "Missing job ID - specify which job you're paying for", {}
|
512
520
|
|
@@ -518,7 +526,7 @@ class AcpPlugin:
|
|
518
526
|
|
519
527
|
try:
|
520
528
|
state = self.get_acp_state()
|
521
|
-
|
529
|
+
|
522
530
|
job = next(
|
523
531
|
(c for c in state["jobs"]["active"]["asABuyer"] if c["jobId"] == job_id),
|
524
532
|
None
|
@@ -530,7 +538,6 @@ class AcpPlugin:
|
|
530
538
|
if job["phase"] != AcpJobPhasesDesc.NEGOTIATION:
|
531
539
|
return FunctionResultStatus.FAILED, f"Cannot pay - job is in '{job['phase']}' phase, must be in 'negotiation' phase", {}
|
532
540
|
|
533
|
-
|
534
541
|
self.acp_client.make_payment(
|
535
542
|
job_id,
|
536
543
|
amount,
|
@@ -570,7 +577,7 @@ class AcpPlugin:
|
|
570
577
|
)
|
571
578
|
|
572
579
|
args = [job_id_arg, deliverable_arg, reasoning_arg]
|
573
|
-
|
580
|
+
|
574
581
|
if hasattr(self, 'twitter_plugin') and self.twitter_plugin is not None:
|
575
582
|
tweet_content_arg = Argument(
|
576
583
|
name="tweet_content",
|
@@ -586,19 +593,20 @@ class AcpPlugin:
|
|
586
593
|
executable=self._deliver_job_executable
|
587
594
|
)
|
588
595
|
|
589
|
-
def _deliver_job_executable(self, job_id: int, deliverable: str, reasoning: str,
|
596
|
+
def _deliver_job_executable(self, job_id: int, deliverable: str, reasoning: str,
|
597
|
+
tweet_content: Optional[str] = None) -> Tuple[FunctionResultStatus, str, dict]:
|
590
598
|
if not job_id:
|
591
599
|
return FunctionResultStatus.FAILED, "Missing job ID - specify which job you're delivering for", {}
|
592
|
-
|
600
|
+
|
593
601
|
if not reasoning:
|
594
602
|
return FunctionResultStatus.FAILED, "Missing reasoning - explain why you're making this delivery", {}
|
595
|
-
|
603
|
+
|
596
604
|
if not deliverable:
|
597
605
|
return FunctionResultStatus.FAILED, "Missing deliverable - specify what you're delivering", {}
|
598
606
|
|
599
607
|
try:
|
600
608
|
state = self.get_acp_state()
|
601
|
-
|
609
|
+
|
602
610
|
job = next(
|
603
611
|
(c for c in state["jobs"]["active"]["asASeller"] if c["jobId"] == job_id),
|
604
612
|
None
|
@@ -611,7 +619,8 @@ class AcpPlugin:
|
|
611
619
|
return FunctionResultStatus.FAILED, f"Cannot deliver - job is in '{job['phase']}' phase, must be in 'transaction' phase", {}
|
612
620
|
|
613
621
|
produced = next(
|
614
|
-
(i for i in self.produced_inventory if
|
622
|
+
(i for i in self.produced_inventory if
|
623
|
+
(i["jobId"] if isinstance(i, dict) else i.jobId) == job["jobId"]),
|
615
624
|
None
|
616
625
|
)
|
617
626
|
|
@@ -645,7 +654,7 @@ class AcpPlugin:
|
|
645
654
|
tweet_id = tweet_history[-1].get("tweetId") if tweet_history else None
|
646
655
|
if tweet_id is not None:
|
647
656
|
reply_tweet_fn = self.twitter_plugin.get_function('reply_tweet')
|
648
|
-
tweet_id = reply_tweet_fn(tweet_id,tweet_content, None).get('data', {}).get('id')
|
657
|
+
tweet_id = reply_tweet_fn(tweet_id, tweet_content, None).get('data', {}).get('id')
|
649
658
|
if tweet_id is not None:
|
650
|
-
self.acp_client.add_tweet(job.get("jobId")
|
659
|
+
self.acp_client.add_tweet(job.get("jobId"), tweet_id, tweet_content)
|
651
660
|
print("Tweet has been posted")
|
acp_plugin_gamesdk/acp_token.py
CHANGED
@@ -1,15 +1,18 @@
|
|
1
|
+
from datetime import datetime
|
1
2
|
from enum import IntEnum
|
3
|
+
import json
|
2
4
|
import time
|
5
|
+
import traceback
|
3
6
|
from typing import Optional, Tuple, TypedDict
|
4
|
-
|
5
|
-
from acp_plugin_gamesdk.configs import ACPContractConfig
|
6
|
-
from web3 import Web3
|
7
|
+
|
7
8
|
from eth_account import Account
|
8
|
-
from acp_plugin_gamesdk.acp_token_abi import ACP_TOKEN_ABI
|
9
|
-
import requests
|
10
9
|
from eth_account.messages import encode_defunct
|
11
|
-
import
|
12
|
-
import
|
10
|
+
import requests
|
11
|
+
from web3 import Web3
|
12
|
+
|
13
|
+
from acp_plugin_gamesdk.acp_token_abi import ACP_TOKEN_ABI
|
14
|
+
from acp_plugin_gamesdk.configs import ACPContractConfig
|
15
|
+
|
13
16
|
|
14
17
|
class MemoType(IntEnum):
|
15
18
|
MESSAGE = 0
|
@@ -19,6 +22,7 @@ class MemoType(IntEnum):
|
|
19
22
|
OBJECT_URL = 4
|
20
23
|
TXHASH = 5
|
21
24
|
|
25
|
+
|
22
26
|
class IMemo(TypedDict):
|
23
27
|
content: str
|
24
28
|
memoType: MemoType
|
@@ -28,6 +32,7 @@ class IMemo(TypedDict):
|
|
28
32
|
numApprovals: int
|
29
33
|
sender: str
|
30
34
|
|
35
|
+
|
31
36
|
class IJob(TypedDict):
|
32
37
|
id: int
|
33
38
|
client: str
|
@@ -39,14 +44,16 @@ class IJob(TypedDict):
|
|
39
44
|
expiredAt: int
|
40
45
|
evaluatorCount: int
|
41
46
|
|
47
|
+
|
42
48
|
JobResult = Tuple[int, str, str, str, str, str, str, str, int]
|
43
49
|
|
50
|
+
|
44
51
|
class AcpToken:
|
45
52
|
def __init__(
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
53
|
+
self,
|
54
|
+
wallet_private_key: str,
|
55
|
+
agent_wallet_address: str,
|
56
|
+
config: ACPContractConfig,
|
50
57
|
):
|
51
58
|
self.web3 = Web3(Web3.HTTPProvider(config.rpc_url))
|
52
59
|
self.account = Account.from_key(wallet_private_key)
|
@@ -68,7 +75,7 @@ class AcpToken:
|
|
68
75
|
},
|
69
76
|
{
|
70
77
|
"internalType": "uint256",
|
71
|
-
"name": "amount",
|
78
|
+
"name": "amount",
|
72
79
|
"type": "uint256"
|
73
80
|
}
|
74
81
|
],
|
@@ -86,101 +93,103 @@ class AcpToken:
|
|
86
93
|
)
|
87
94
|
self.acp_base_url = config.acp_api_url
|
88
95
|
self.game_api_url = config.game_api_url
|
89
|
-
|
96
|
+
|
90
97
|
def get_agent_wallet_address(self) -> str:
|
91
98
|
return self.agent_wallet_address
|
92
|
-
|
99
|
+
|
93
100
|
def get_contract_address(self) -> str:
|
94
101
|
return self.contract_address
|
95
102
|
|
96
103
|
def validate_transaction(self, hash_value: str) -> object:
|
97
104
|
try:
|
98
|
-
response = requests.post(f"{self.acp_base_url}/acp-agent-wallets/trx-result",
|
105
|
+
response = requests.post(f"{self.acp_base_url}/acp-agent-wallets/trx-result",
|
106
|
+
json={"userOpHash": hash_value})
|
99
107
|
return response.json()
|
100
108
|
except Exception as error:
|
101
109
|
print(traceback.format_exc())
|
102
110
|
raise Exception(f"Failed to get job_id {error}")
|
103
111
|
|
104
112
|
def create_job(
|
105
|
-
|
106
|
-
|
107
|
-
|
108
|
-
|
113
|
+
self,
|
114
|
+
provider_address: str,
|
115
|
+
evaluator_address: str,
|
116
|
+
expire_at: datetime
|
109
117
|
) -> dict:
|
110
118
|
try:
|
111
119
|
provider_address = Web3.to_checksum_address(provider_address)
|
112
120
|
evaluator_address = Web3.to_checksum_address(evaluator_address)
|
113
121
|
expire_timestamp = int(expire_at.timestamp())
|
114
|
-
|
122
|
+
|
115
123
|
# Sign the transaction
|
116
124
|
trx_data, signature = self._sign_transaction(
|
117
|
-
"createJob",
|
125
|
+
"createJob",
|
118
126
|
[provider_address, evaluator_address, expire_timestamp]
|
119
127
|
)
|
120
|
-
|
128
|
+
|
121
129
|
# Prepare payload
|
122
130
|
payload = {
|
123
131
|
"agentWallet": self.get_agent_wallet_address(),
|
124
132
|
"trxData": trx_data,
|
125
133
|
"signature": signature
|
126
134
|
}
|
127
|
-
|
135
|
+
|
128
136
|
# Submit to custom API
|
129
137
|
api_url = f"{self.acp_base_url}/acp-agent-wallets/transactions"
|
130
138
|
response = requests.post(api_url, json=payload)
|
131
|
-
|
132
|
-
|
139
|
+
|
133
140
|
if response.json().get("error"):
|
134
|
-
raise Exception(
|
135
|
-
|
141
|
+
raise Exception(
|
142
|
+
f"Failed to create job {response.json().get('error').get('status')}, Message: {response.json().get('error').get('message')}")
|
143
|
+
|
136
144
|
# Return transaction hash or response ID
|
137
145
|
return {"txHash": response.json().get("data", {}).get("userOpHash", "")}
|
138
|
-
|
146
|
+
|
139
147
|
except Exception as e:
|
140
148
|
raise
|
141
149
|
|
142
150
|
def approve_allowance(self, price_in_wei: int) -> str:
|
143
151
|
try:
|
144
152
|
trx_data, signature = self._sign_transaction(
|
145
|
-
"approve",
|
153
|
+
"approve",
|
146
154
|
[self.contract_address, price_in_wei],
|
147
155
|
self.virtuals_token_address
|
148
156
|
)
|
149
|
-
|
157
|
+
|
150
158
|
payload = {
|
151
159
|
"agentWallet": self.get_agent_wallet_address(),
|
152
160
|
"trxData": trx_data,
|
153
161
|
"signature": signature
|
154
162
|
}
|
155
|
-
|
163
|
+
|
156
164
|
api_url = f"{self.acp_base_url}/acp-agent-wallets/transactions"
|
157
165
|
response = requests.post(api_url, json=payload)
|
158
|
-
|
166
|
+
|
159
167
|
if (response.json().get("error")):
|
160
|
-
raise Exception(
|
161
|
-
|
168
|
+
raise Exception(
|
169
|
+
f"Failed to approve allowance {response.json().get('error').get('status')}, Message: {response.json().get('error').get('message')}")
|
170
|
+
|
162
171
|
return response.json()
|
163
172
|
except Exception as e:
|
164
173
|
print(f"An error occurred while approving allowance: {e}")
|
165
174
|
raise
|
166
175
|
|
167
176
|
def create_memo(
|
168
|
-
|
169
|
-
|
170
|
-
|
171
|
-
|
172
|
-
|
173
|
-
|
177
|
+
self,
|
178
|
+
job_id: int,
|
179
|
+
content: str,
|
180
|
+
memo_type: MemoType,
|
181
|
+
is_secured: bool,
|
182
|
+
next_phase: int
|
174
183
|
) -> dict:
|
175
184
|
retries = 3
|
176
185
|
error = None
|
177
186
|
while retries > 0:
|
178
187
|
try:
|
179
188
|
trx_data, signature = self._sign_transaction(
|
180
|
-
"createMemo",
|
189
|
+
"createMemo",
|
181
190
|
[job_id, content, memo_type, is_secured, next_phase]
|
182
191
|
)
|
183
|
-
|
192
|
+
|
184
193
|
payload = {
|
185
194
|
"agentWallet": self.get_agent_wallet_address(),
|
186
195
|
"trxData": trx_data,
|
@@ -189,99 +198,104 @@ class AcpToken:
|
|
189
198
|
|
190
199
|
api_url = f"{self.acp_base_url}/acp-agent-wallets/transactions"
|
191
200
|
response = requests.post(api_url, json=payload)
|
192
|
-
|
201
|
+
|
193
202
|
if (response.json().get("error")):
|
194
|
-
raise Exception(
|
195
|
-
|
196
|
-
|
203
|
+
raise Exception(
|
204
|
+
f"Failed to create memo {response.json().get('error').get('status')}, Message: {response.json().get('error').get('message')}")
|
205
|
+
|
206
|
+
return {"txHash": response.json().get("txHash", response.json().get("id", "")),
|
207
|
+
"memoId": response.json().get("memoId", "")}
|
197
208
|
except Exception as e:
|
198
209
|
print(f"{e}")
|
199
210
|
print(traceback.format_exc())
|
200
211
|
error = e
|
201
212
|
retries -= 1
|
202
213
|
time.sleep(2 * (3 - retries))
|
203
|
-
|
214
|
+
|
204
215
|
if error:
|
205
216
|
raise Exception(f"{error}")
|
206
217
|
|
207
|
-
def _sign_transaction(self, method_name: str, args: list, contract_address: Optional[str] = None) -> Tuple[
|
218
|
+
def _sign_transaction(self, method_name: str, args: list, contract_address: Optional[str] = None) -> Tuple[
|
219
|
+
dict, str]:
|
208
220
|
if contract_address:
|
209
221
|
encoded_data = self.virtuals_token_contract.encode_abi(method_name, args=args)
|
210
222
|
else:
|
211
223
|
encoded_data = self.contract.encode_abi(method_name, args=args)
|
212
|
-
|
224
|
+
|
213
225
|
trx_data = {
|
214
226
|
"target": contract_address if contract_address else self.get_contract_address(),
|
215
227
|
"value": "0",
|
216
228
|
"data": encoded_data
|
217
229
|
}
|
218
|
-
|
230
|
+
|
219
231
|
message_json = json.dumps(trx_data, separators=(",", ":"), sort_keys=False)
|
220
232
|
message_bytes = message_json.encode()
|
221
|
-
|
233
|
+
|
222
234
|
# Sign the transaction
|
223
235
|
message = encode_defunct(message_bytes)
|
224
236
|
signature = "0x" + self.account.sign_message(message).signature.hex()
|
225
|
-
|
237
|
+
|
226
238
|
return trx_data, signature
|
227
239
|
|
228
240
|
def sign_memo(
|
229
|
-
|
230
|
-
|
231
|
-
|
232
|
-
|
241
|
+
self,
|
242
|
+
memo_id: int,
|
243
|
+
is_approved: bool,
|
244
|
+
reason: Optional[str] = ""
|
233
245
|
) -> str:
|
234
246
|
retries = 3
|
235
247
|
error = None
|
236
248
|
while retries > 0:
|
237
249
|
try:
|
238
250
|
trx_data, signature = self._sign_transaction(
|
239
|
-
"signMemo",
|
251
|
+
"signMemo",
|
240
252
|
[memo_id, is_approved, reason]
|
241
253
|
)
|
242
|
-
|
254
|
+
|
243
255
|
payload = {
|
244
256
|
"agentWallet": self.get_agent_wallet_address(),
|
245
257
|
"trxData": trx_data,
|
246
258
|
"signature": signature
|
247
259
|
}
|
248
|
-
|
260
|
+
|
249
261
|
api_url = f"{self.acp_base_url}/acp-agent-wallets/transactions"
|
250
262
|
response = requests.post(api_url, json=payload)
|
251
|
-
|
263
|
+
|
252
264
|
if (response.json().get("error")):
|
253
|
-
raise Exception(
|
254
|
-
|
265
|
+
raise Exception(
|
266
|
+
f"Failed to sign memo {response.json().get('error').get('status')}, Message: {response.json().get('error').get('message')}")
|
267
|
+
|
255
268
|
return response.json()
|
256
|
-
|
269
|
+
|
257
270
|
except Exception as e:
|
258
271
|
error = e
|
259
272
|
print(f"{error}")
|
260
273
|
print(traceback.format_exc())
|
261
274
|
retries -= 1
|
262
275
|
time.sleep(2 * (3 - retries))
|
263
|
-
|
276
|
+
|
264
277
|
raise Exception(f"Failed to sign memo {error}")
|
265
278
|
|
266
279
|
def set_budget(self, job_id: int, budget: int) -> str:
|
267
280
|
try:
|
268
281
|
trx_data, signature = self._sign_transaction(
|
269
|
-
"setBudget",
|
282
|
+
"setBudget",
|
270
283
|
[job_id, budget]
|
271
284
|
)
|
272
|
-
|
285
|
+
|
273
286
|
payload = {
|
274
287
|
"agentWallet": self.get_agent_wallet_address(),
|
275
288
|
"trxData": trx_data,
|
276
289
|
"signature": signature
|
277
290
|
}
|
278
|
-
|
291
|
+
|
279
292
|
api_url = f"{self.acp_base_url}/acp-agent-wallets/transactions"
|
280
293
|
response = requests.post(api_url, json=payload)
|
281
|
-
|
294
|
+
|
282
295
|
if (response.json().get("error")):
|
283
|
-
raise Exception(
|
284
|
-
|
296
|
+
raise Exception(
|
297
|
+
f"Failed to set budget {response.json().get('error').get('status')}, Message: {response.json().get('error').get('message')}")
|
298
|
+
|
285
299
|
return response.json()
|
286
300
|
except Exception as error:
|
287
301
|
raise Exception(f"{error}")
|
@@ -289,10 +303,10 @@ class AcpToken:
|
|
289
303
|
def get_job(self, job_id: int) -> Optional[IJob]:
|
290
304
|
try:
|
291
305
|
job_data = self.contract.functions.jobs(job_id).call()
|
292
|
-
|
306
|
+
|
293
307
|
if not job_data:
|
294
308
|
return None
|
295
|
-
|
309
|
+
|
296
310
|
return {
|
297
311
|
'id': job_data[0],
|
298
312
|
'client': job_data[1],
|
@@ -308,13 +322,13 @@ class AcpToken:
|
|
308
322
|
raise Exception(f"{error}")
|
309
323
|
|
310
324
|
def get_memo_by_job(
|
311
|
-
|
312
|
-
|
313
|
-
|
325
|
+
self,
|
326
|
+
job_id: int,
|
327
|
+
memo_type: Optional[MemoType] = None
|
314
328
|
) -> Optional[IMemo]:
|
315
329
|
try:
|
316
330
|
memos = self.contract.functions.getAllMemos(job_id).call()
|
317
|
-
|
331
|
+
|
318
332
|
if memo_type is not None:
|
319
333
|
filtered_memos = [m for m in memos if m['memoType'] == memo_type]
|
320
334
|
return filtered_memos[-1] if filtered_memos else None
|
@@ -324,14 +338,14 @@ class AcpToken:
|
|
324
338
|
raise Exception(f"Failed to get memo by job {error}")
|
325
339
|
|
326
340
|
def get_memos_for_phase(
|
327
|
-
|
328
|
-
|
329
|
-
|
330
|
-
|
341
|
+
self,
|
342
|
+
job_id: int,
|
343
|
+
phase: int,
|
344
|
+
target_phase: int
|
331
345
|
) -> Optional[IMemo]:
|
332
346
|
try:
|
333
347
|
memos = self.contract.functions.getMemosForPhase(job_id, phase).call()
|
334
|
-
|
348
|
+
|
335
349
|
target_memos = [m for m in memos if m['nextPhase'] == target_phase]
|
336
350
|
return target_memos[-1] if target_memos else None
|
337
351
|
except Exception as error:
|
acp_plugin_gamesdk/configs.py
CHANGED
@@ -3,6 +3,7 @@ from typing import Literal
|
|
3
3
|
|
4
4
|
ChainEnv = Literal["base-sepolia", "base"]
|
5
5
|
|
6
|
+
|
6
7
|
@dataclass
|
7
8
|
class ACPContractConfig:
|
8
9
|
chain_env: ChainEnv
|
@@ -13,6 +14,7 @@ class ACPContractConfig:
|
|
13
14
|
acp_api_url: str
|
14
15
|
game_api_url: str
|
15
16
|
|
17
|
+
|
16
18
|
# Configuration for Base Sepolia
|
17
19
|
BASE_SEPOLIA_CONFIG = ACPContractConfig(
|
18
20
|
chain_env="base-sepolia",
|
@@ -27,16 +29,16 @@ BASE_SEPOLIA_CONFIG = ACPContractConfig(
|
|
27
29
|
# Configuration for Base Mainnet
|
28
30
|
BASE_MAINNET_CONFIG = ACPContractConfig(
|
29
31
|
chain_env="base",
|
30
|
-
rpc_url="https://mainnet.base.org",
|
32
|
+
rpc_url="https://mainnet.base.org",
|
31
33
|
chain_id=8453,
|
32
34
|
contract_address="0x6a1FE26D54ab0d3E1e3168f2e0c0cDa5cC0A0A4A",
|
33
35
|
virtuals_token_address="0x0b3e328455c4059EEb9e3f84b5543F74E24e7E1b",
|
34
|
-
acp_api_url="https://acpx.virtuals.io/api",
|
36
|
+
acp_api_url="https://acpx.virtuals.io/api", # PROD
|
35
37
|
game_api_url="https://sdk.game.virtuals.io"
|
36
38
|
)
|
37
39
|
|
38
40
|
# Define the default configuration for the SDK
|
39
41
|
# For a production-ready SDK, this would typically be BASE_MAINNET_CONFIG.
|
40
42
|
# For initial development/testing, BASE_SEPOLIA_CONFIG might be more appropriate.
|
41
|
-
DEFAULT_CONFIG = BASE_MAINNET_CONFIG
|
43
|
+
DEFAULT_CONFIG = BASE_MAINNET_CONFIG
|
42
44
|
# Or: DEFAULT_CONFIG = BASE_SEPOLIA_CONFIG
|
acp_plugin_gamesdk/interface.py
CHANGED
@@ -1,6 +1,7 @@
|
|
1
1
|
from dataclasses import dataclass
|
2
|
-
from enum import
|
3
|
-
from typing import List, Literal, Optional,
|
2
|
+
from enum import Enum, IntEnum
|
3
|
+
from typing import Any, Callable, Dict, List, Literal, Optional, Union
|
4
|
+
|
4
5
|
|
5
6
|
@dataclass
|
6
7
|
class AcpOffering:
|
@@ -12,7 +13,8 @@ class AcpOffering:
|
|
12
13
|
f"Offering(name={self.name}, price={self.price})"
|
13
14
|
)
|
14
15
|
return output
|
15
|
-
|
16
|
+
|
17
|
+
|
16
18
|
@dataclass
|
17
19
|
class AcpAgent:
|
18
20
|
id: str
|
@@ -28,7 +30,7 @@ class AcpAgent:
|
|
28
30
|
offer = ""
|
29
31
|
if self.offerings:
|
30
32
|
for index, off in enumerate(self.offerings):
|
31
|
-
offer += f"{index+1}. {str(off)}\n"
|
33
|
+
offer += f"{index + 1}. {str(off)}\n"
|
32
34
|
|
33
35
|
output = (
|
34
36
|
f"😎 Agent ID={self.id}\n"
|
@@ -38,7 +40,8 @@ class AcpAgent:
|
|
38
40
|
f"Explanation:\n{self.explanation}"
|
39
41
|
)
|
40
42
|
return output
|
41
|
-
|
43
|
+
|
44
|
+
|
42
45
|
class AcpJobPhases(IntEnum):
|
43
46
|
REQUEST = 0
|
44
47
|
NEGOTIATION = 1
|
@@ -47,6 +50,7 @@ class AcpJobPhases(IntEnum):
|
|
47
50
|
COMPLETED = 4
|
48
51
|
REJECTED = 5
|
49
52
|
|
53
|
+
|
50
54
|
class AcpJobPhasesDesc(str, Enum):
|
51
55
|
REQUEST = "request"
|
52
56
|
NEGOTIATION = "pending_payment"
|
@@ -55,6 +59,7 @@ class AcpJobPhasesDesc(str, Enum):
|
|
55
59
|
COMPLETED = "completed"
|
56
60
|
REJECTED = "rejected"
|
57
61
|
|
62
|
+
|
58
63
|
@dataclass
|
59
64
|
class AcpRequestMemo:
|
60
65
|
id: int
|
@@ -63,7 +68,8 @@ class AcpRequestMemo:
|
|
63
68
|
def __repr__(self) -> str:
|
64
69
|
output = f"Memo(ID: {self.id}, created at: {self.createdAt})"
|
65
70
|
return output
|
66
|
-
|
71
|
+
|
72
|
+
|
67
73
|
@dataclass
|
68
74
|
class ITweet:
|
69
75
|
type: Literal["buyer", "seller"]
|
@@ -71,24 +77,24 @@ class ITweet:
|
|
71
77
|
content: str
|
72
78
|
created_at: int
|
73
79
|
|
80
|
+
|
74
81
|
@dataclass
|
75
82
|
class AcpJob:
|
76
83
|
jobId: Optional[int]
|
77
|
-
clientName
|
84
|
+
clientName: Optional[str]
|
78
85
|
providerName: Optional[str]
|
79
|
-
desc: str
|
80
|
-
price:
|
86
|
+
desc: Optional[str]
|
87
|
+
price: float
|
81
88
|
providerAddress: Optional[str]
|
82
89
|
clientAddress: Optional[str]
|
83
90
|
phase: AcpJobPhasesDesc
|
84
91
|
memo: List[AcpRequestMemo]
|
85
|
-
tweetHistory
|
92
|
+
tweetHistory: ITweet | List
|
86
93
|
lastUpdated: int
|
87
94
|
getAgentByWalletAddress: Optional[Callable[[str], AcpAgent]]
|
88
|
-
|
89
95
|
|
90
96
|
def __repr__(self) -> str:
|
91
|
-
output =(
|
97
|
+
output = (
|
92
98
|
f"Job ID: {self.jobId}, "
|
93
99
|
f"Client Name: {self.clientName}, "
|
94
100
|
f"Provider Name: {self.providerName}, "
|
@@ -100,21 +106,25 @@ class AcpJob:
|
|
100
106
|
f"Memo: {self.memo}, "
|
101
107
|
f"Tweet History: {self.tweetHistory}, "
|
102
108
|
f"Last Updated: {self.lastUpdated})"
|
103
|
-
)
|
109
|
+
)
|
104
110
|
return output
|
105
111
|
|
112
|
+
|
106
113
|
@dataclass
|
107
114
|
class IDeliverable:
|
108
115
|
type: str
|
109
|
-
value: str
|
116
|
+
value: Union[str, Dict[str, Any], List[Any]]
|
110
117
|
clientName: Optional[str]
|
111
118
|
providerName: Optional[str]
|
119
|
+
|
120
|
+
|
112
121
|
@dataclass
|
113
122
|
class IInventory(IDeliverable):
|
114
123
|
jobId: int
|
115
124
|
clientName: Optional[str]
|
116
125
|
providerName: Optional[str]
|
117
126
|
|
127
|
+
|
118
128
|
@dataclass
|
119
129
|
class AcpJobsSection:
|
120
130
|
asABuyer: List[AcpJob]
|
@@ -123,11 +133,11 @@ class AcpJobsSection:
|
|
123
133
|
def __str__(self) -> str:
|
124
134
|
buyer_jobs = ""
|
125
135
|
for index, job in enumerate(self.asABuyer):
|
126
|
-
buyer_jobs += f"#{index+1} {str(job)} \n"
|
136
|
+
buyer_jobs += f"#{index + 1} {str(job)} \n"
|
127
137
|
|
128
138
|
seller_jobs = ""
|
129
139
|
for index, job in enumerate(self.asASeller):
|
130
|
-
seller_jobs += f"#{index+1} {str(job)} \n"
|
140
|
+
seller_jobs += f"#{index + 1} {str(job)} \n"
|
131
141
|
|
132
142
|
output = (
|
133
143
|
f"As Buyer:\n{buyer_jobs}\n"
|
@@ -135,6 +145,7 @@ class AcpJobsSection:
|
|
135
145
|
)
|
136
146
|
return output
|
137
147
|
|
148
|
+
|
138
149
|
@dataclass
|
139
150
|
class AcpJobs:
|
140
151
|
active: AcpJobsSection
|
@@ -149,7 +160,8 @@ class AcpJobs:
|
|
149
160
|
f"🔴 Cancelled:\n{self.cancelled}\n"
|
150
161
|
)
|
151
162
|
return output
|
152
|
-
|
163
|
+
|
164
|
+
|
153
165
|
@dataclass
|
154
166
|
class AcpInventory:
|
155
167
|
acquired: List[IInventory]
|
@@ -163,6 +175,7 @@ class AcpInventory:
|
|
163
175
|
)
|
164
176
|
return output
|
165
177
|
|
178
|
+
|
166
179
|
@dataclass
|
167
180
|
class AcpState:
|
168
181
|
inventory: AcpInventory
|
@@ -170,9 +183,9 @@ class AcpState:
|
|
170
183
|
|
171
184
|
def __str__(self) -> str:
|
172
185
|
output = (
|
173
|
-
|
174
|
-
|
175
|
-
|
176
|
-
|
186
|
+
f"🤖 Agent State".center(50, '=') + "\n" + \
|
187
|
+
f"{str(self.inventory)}\n" + \
|
188
|
+
f"{str(self.jobs)}\n" + \
|
189
|
+
f"State End".center(50, '=') + "\n"
|
177
190
|
)
|
178
|
-
return output
|
191
|
+
return output
|
@@ -0,0 +1,9 @@
|
|
1
|
+
acp_plugin_gamesdk/acp_client.py,sha256=fsSByXElEJuzFdhJTphHmtRv8Tqhs5eXuCGRm2l2JtM,10167
|
2
|
+
acp_plugin_gamesdk/acp_plugin.py,sha256=kJ0S_0Fzx0IHdKnAEHA08ZmLIIyv4URgEBRjdCyFx0I,27022
|
3
|
+
acp_plugin_gamesdk/acp_token.py,sha256=zjW-W7_4JO31K8eLBBw5dn5tb4FrCSag6qGYQi7xkaA,11804
|
4
|
+
acp_plugin_gamesdk/acp_token_abi.py,sha256=nllh9xOuDXxFFdhLklTTdtZxWZd2LcUTBoOP2d9xDTA,22319
|
5
|
+
acp_plugin_gamesdk/configs.py,sha256=4rLOOMbRPqf2RM-Lz5Az7mbUI_a5kgmySZlBiMkB3Mo,1406
|
6
|
+
acp_plugin_gamesdk/interface.py,sha256=WkeI0YLaMOGt5vYJzwuq5SuviaZKfwePQGhPE8HaU8w,4561
|
7
|
+
acp_plugin_gamesdk-0.1.21.dist-info/METADATA,sha256=PzI39ny2YjUrwZgUQ89v37kfClx-_GaAw6bLIhKwNdI,12791
|
8
|
+
acp_plugin_gamesdk-0.1.21.dist-info/WHEEL,sha256=XbeZDeTWKc1w7CSIyre5aMDU_-PohRwTQceYnisIYYY,88
|
9
|
+
acp_plugin_gamesdk-0.1.21.dist-info/RECORD,,
|
@@ -1,9 +0,0 @@
|
|
1
|
-
acp_plugin_gamesdk/acp_client.py,sha256=ScJc6Sqly8xrsvGWbEvRBeMFuLRS1UsZxgESJTfyBmw,10468
|
2
|
-
acp_plugin_gamesdk/acp_plugin.py,sha256=G55hy2-PPzyaJOL3Pb3WLUdQA5hgI9KxHgtJinB7FFQ,27357
|
3
|
-
acp_plugin_gamesdk/acp_token.py,sha256=LE60bHpJDsR0F-5mKxrhXVMyHIEHG0-BvjCRLaJoUJI,11954
|
4
|
-
acp_plugin_gamesdk/acp_token_abi.py,sha256=nllh9xOuDXxFFdhLklTTdtZxWZd2LcUTBoOP2d9xDTA,22319
|
5
|
-
acp_plugin_gamesdk/configs.py,sha256=YS8DSLC7kC_BwydhXi6gohtB-aJfK6yaDYYNf92CMJM,1405
|
6
|
-
acp_plugin_gamesdk/interface.py,sha256=rb4YnWmOZXE5iqfG9bHyJeTnF1oml_eU9DiDqgZLdaQ,4483
|
7
|
-
acp_plugin_gamesdk-0.1.19.dist-info/METADATA,sha256=AL0rkcvKmFEyj35znPhh_ApcWAL1248ePD0Erb3dh4c,12791
|
8
|
-
acp_plugin_gamesdk-0.1.19.dist-info/WHEEL,sha256=XbeZDeTWKc1w7CSIyre5aMDU_-PohRwTQceYnisIYYY,88
|
9
|
-
acp_plugin_gamesdk-0.1.19.dist-info/RECORD,,
|
File without changes
|