acp-plugin-gamesdk 0.1.18__py3-none-any.whl → 0.1.20__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 +99 -57
- acp_plugin_gamesdk/acp_plugin.py +88 -77
- acp_plugin_gamesdk/acp_token.py +96 -82
- acp_plugin_gamesdk/configs.py +5 -3
- acp_plugin_gamesdk/interface.py +36 -19
- {acp_plugin_gamesdk-0.1.18.dist-info → acp_plugin_gamesdk-0.1.20.dist-info}/METADATA +1 -1
- acp_plugin_gamesdk-0.1.20.dist-info/RECORD +9 -0
- acp_plugin_gamesdk-0.1.18.dist-info/RECORD +0 -9
- {acp_plugin_gamesdk-0.1.18.dist-info → acp_plugin_gamesdk-0.1.20.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,39 +234,75 @@ 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
|
)
|
272
|
+
|
273
|
+
def get_agent_by_wallet_address(self, wallet_address: str) -> AcpAgent:
|
274
|
+
url = f"{self.acp_base_url}/agents?filters[walletAddress]={wallet_address}"
|
275
|
+
|
276
|
+
response = requests.get(
|
277
|
+
url,
|
278
|
+
)
|
279
|
+
|
280
|
+
if response.status_code != 200:
|
281
|
+
raise Exception(
|
282
|
+
f"Failed to get agent: {response.status_code} {response.text}"
|
283
|
+
)
|
284
|
+
|
285
|
+
response_json = response.json()
|
286
|
+
|
287
|
+
result = []
|
288
|
+
|
289
|
+
for agent in response_json.get("data", []):
|
290
|
+
if agent["offerings"]:
|
291
|
+
offerings = [AcpOffering(name=offering["name"], price=offering["price"]) for offering in
|
292
|
+
agent["offerings"]]
|
293
|
+
else:
|
294
|
+
offerings = None
|
295
|
+
|
296
|
+
result.append(
|
297
|
+
AcpAgent(
|
298
|
+
id=agent["id"],
|
299
|
+
name=agent["name"],
|
300
|
+
twitter_handle=agent["twitterHandle"],
|
301
|
+
description=agent["description"],
|
302
|
+
wallet_address=agent["walletAddress"],
|
303
|
+
offerings=offerings,
|
304
|
+
score=0,
|
305
|
+
explanation=""
|
306
|
+
)
|
307
|
+
)
|
308
|
+
return result[0]
|
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
|
-
|
13
11
|
from game_sdk.game.agent import WorkerConfig
|
14
12
|
from game_sdk.game.custom_types import Argument, Function, FunctionResultStatus
|
15
|
-
from twitter_plugin_gamesdk.twitter_plugin import TwitterPlugin
|
16
13
|
from twitter_plugin_gamesdk.game_twitter_plugin import GameTwitterPlugin
|
14
|
+
from twitter_plugin_gamesdk.twitter_plugin import TwitterPlugin
|
15
|
+
|
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,12 +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
|
-
|
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
|
78
88
|
self.initialize_socket()
|
79
89
|
self.job_expiry_duration_mins = options.job_expiry_duration_mins if options.job_expiry_duration_mins is not None else 1440
|
80
|
-
|
81
|
-
|
82
|
-
|
90
|
+
|
83
91
|
def initialize_socket(self) -> Tuple[bool, str]:
|
84
92
|
"""
|
85
93
|
Initialize socket connection for real-time communication.
|
@@ -87,37 +95,36 @@ class AcpPlugin:
|
|
87
95
|
"""
|
88
96
|
try:
|
89
97
|
self.socket = socketio.Client()
|
90
|
-
|
98
|
+
|
91
99
|
# Set up authentication before connecting
|
92
100
|
self.socket.auth = {
|
93
101
|
"evaluatorAddress": self.acp_token_client.agent_wallet_address
|
94
102
|
}
|
95
|
-
|
103
|
+
|
96
104
|
# Connect socket to GAME SDK dev server
|
97
105
|
self.socket.connect(self.acp_client.base_url, auth=self.socket.auth)
|
98
|
-
|
106
|
+
|
99
107
|
if self.socket.connected:
|
100
108
|
self.socket.emit(SocketEvents["JOIN_EVALUATOR_ROOM"], self.acp_token_client.agent_wallet_address)
|
101
|
-
|
102
|
-
|
109
|
+
|
103
110
|
# Set up event handler for evaluation requests
|
104
111
|
@self.socket.on(SocketEvents["ON_EVALUATE"])
|
105
112
|
def on_evaluate(data):
|
106
113
|
if self.on_evaluate:
|
107
114
|
deliverable = data.get("deliverable")
|
108
115
|
memo_id = data.get("memoId")
|
109
|
-
|
116
|
+
|
110
117
|
is_approved, reasoning = self.on_evaluate(deliverable)
|
111
|
-
|
118
|
+
|
112
119
|
self.acp_token_client.sign_memo(memo_id, is_approved, reasoning)
|
113
|
-
|
114
|
-
|
120
|
+
|
121
|
+
# Set up event handler for phase changes
|
122
|
+
|
115
123
|
@self.socket.on(SocketEvents["ON_PHASE_CHANGE"])
|
116
124
|
def on_phase_change(data):
|
117
125
|
if hasattr(self, 'on_phase_change') and self.on_phase_change:
|
118
|
-
print(f"on_phase_change: {data}")
|
119
126
|
self.on_phase_change(data)
|
120
|
-
|
127
|
+
|
121
128
|
# Set up cleanup function for graceful shutdown
|
122
129
|
def cleanup():
|
123
130
|
if self.socket:
|
@@ -125,34 +132,31 @@ class AcpPlugin:
|
|
125
132
|
import time
|
126
133
|
time.sleep(1)
|
127
134
|
self.socket.disconnect()
|
128
|
-
|
129
|
-
|
130
|
-
|
135
|
+
|
131
136
|
def signal_handler(_sig, _frame):
|
132
137
|
cleanup()
|
133
138
|
sys.exit(0)
|
134
|
-
|
139
|
+
|
135
140
|
signal.signal(signal.SIGINT, signal_handler)
|
136
141
|
signal.signal(signal.SIGTERM, signal_handler)
|
137
|
-
|
142
|
+
|
138
143
|
return True, "Socket initialized successfully"
|
139
|
-
|
144
|
+
|
140
145
|
except Exception as e:
|
141
146
|
return False, f"Failed to initialize socket: {str(e)}"
|
142
|
-
|
143
|
-
|
147
|
+
|
144
148
|
def set_on_phase_change(self, on_phase_change: Callable[[AcpJob], None]) -> None:
|
145
149
|
self.on_phase_change = on_phase_change
|
146
150
|
|
147
151
|
def add_produce_item(self, item: IInventory) -> None:
|
148
152
|
self.produced_inventory.append(item)
|
149
|
-
|
153
|
+
|
150
154
|
def reset_state(self) -> None:
|
151
155
|
self.acp_client.reset_state()
|
152
|
-
|
156
|
+
|
153
157
|
def delete_completed_job(self, job_id: int) -> None:
|
154
158
|
self.acp_client.delete_completed_job(job_id)
|
155
|
-
|
159
|
+
|
156
160
|
def get_acp_state(self) -> Dict:
|
157
161
|
server_state = self.acp_client.get_state()
|
158
162
|
server_state.inventory.produced = self.produced_inventory
|
@@ -167,7 +171,7 @@ class AcpPlugin:
|
|
167
171
|
self.pay_job,
|
168
172
|
self.deliver_job,
|
169
173
|
]
|
170
|
-
|
174
|
+
|
171
175
|
def get_environment(_function_result, _current_state) -> Dict[str, Any]:
|
172
176
|
environment = data.get_environment() if hasattr(data, "get_environment") else {}
|
173
177
|
return {
|
@@ -182,7 +186,7 @@ class AcpPlugin:
|
|
182
186
|
get_state_fn=get_environment,
|
183
187
|
instruction=data.get("instructions") if data else None
|
184
188
|
)
|
185
|
-
|
189
|
+
|
186
190
|
return worker_config
|
187
191
|
|
188
192
|
@property
|
@@ -201,8 +205,8 @@ class AcpPlugin:
|
|
201
205
|
- Each job tracks:
|
202
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
|
203
207
|
"""
|
204
|
-
|
205
|
-
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]:
|
206
210
|
if not reasoning:
|
207
211
|
return FunctionResultStatus.FAILED, "Reasoning for the search must be provided. This helps track your decision-making process for future reference.", {}
|
208
212
|
|
@@ -289,21 +293,22 @@ class AcpPlugin:
|
|
289
293
|
type="string",
|
290
294
|
description="Detailed specifications for service-based items",
|
291
295
|
)
|
292
|
-
|
296
|
+
|
293
297
|
require_evaluation_arg = Argument(
|
294
298
|
name="require_evaluation",
|
295
299
|
type="boolean",
|
296
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.",
|
297
301
|
)
|
298
|
-
|
302
|
+
|
299
303
|
evaluator_keyword_arg = Argument(
|
300
304
|
name="evaluator_keyword",
|
301
305
|
type="string",
|
302
306
|
description="Keyword to search for a evaluator",
|
303
307
|
)
|
304
308
|
|
305
|
-
args = [seller_wallet_address_arg, price_arg, reasoning_arg, service_requirements_arg, require_evaluation_arg,
|
306
|
-
|
309
|
+
args = [seller_wallet_address_arg, price_arg, reasoning_arg, service_requirements_arg, require_evaluation_arg,
|
310
|
+
evaluator_keyword_arg]
|
311
|
+
|
307
312
|
if hasattr(self, 'twitter_plugin') and self.twitter_plugin is not None:
|
308
313
|
tweet_content_arg = Argument(
|
309
314
|
name="tweet_content",
|
@@ -311,7 +316,7 @@ class AcpPlugin:
|
|
311
316
|
description="Tweet content that will be posted about this job. Must include the seller's Twitter handle (with @ symbol) to notify them",
|
312
317
|
)
|
313
318
|
args.append(tweet_content_arg)
|
314
|
-
|
319
|
+
|
315
320
|
return Function(
|
316
321
|
fn_name="initiate_job",
|
317
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.",
|
@@ -319,7 +324,9 @@ class AcpPlugin:
|
|
319
324
|
executable=self._initiate_job_executable
|
320
325
|
)
|
321
326
|
|
322
|
-
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]:
|
323
330
|
if isinstance(require_evaluation, str):
|
324
331
|
require_evaluation = require_evaluation.lower() == 'true'
|
325
332
|
elif isinstance(require_evaluation, bool):
|
@@ -329,10 +336,10 @@ class AcpPlugin:
|
|
329
336
|
|
330
337
|
if not price:
|
331
338
|
return FunctionResultStatus.FAILED, "Missing price - specify how much you're offering per unit", {}
|
332
|
-
|
339
|
+
|
333
340
|
if not reasoning:
|
334
341
|
return FunctionResultStatus.FAILED, "Missing reasoning - explain why you're making this purchase request", {}
|
335
|
-
|
342
|
+
|
336
343
|
try:
|
337
344
|
state = self.get_acp_state()
|
338
345
|
|
@@ -343,24 +350,25 @@ class AcpPlugin:
|
|
343
350
|
)
|
344
351
|
|
345
352
|
if existing_job:
|
346
|
-
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", {}
|
347
|
-
|
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
|
+
|
348
355
|
if not seller_wallet_address:
|
349
356
|
return FunctionResultStatus.FAILED, "Missing seller wallet address - specify the agent you want to buy from", {}
|
350
|
-
|
357
|
+
|
351
358
|
if require_evaluation and not evaluator_keyword:
|
352
359
|
return FunctionResultStatus.FAILED, "Missing validator keyword - provide a keyword to search for a validator", {}
|
353
|
-
|
360
|
+
|
354
361
|
evaluator_address = self.acp_token_client.get_agent_wallet_address()
|
355
|
-
|
362
|
+
|
356
363
|
if require_evaluation:
|
357
|
-
validators = self.acp_client.browse_agents(self.evaluator_cluster, evaluator_keyword, rerank=True,
|
358
|
-
|
364
|
+
validators = self.acp_client.browse_agents(self.evaluator_cluster, evaluator_keyword, rerank=True,
|
365
|
+
top_k=1)
|
366
|
+
|
359
367
|
if len(validators) == 0:
|
360
368
|
return FunctionResultStatus.FAILED, "No evaluator found - try a different keyword", {}
|
361
369
|
|
362
370
|
evaluator_address = validators[0].wallet_address
|
363
|
-
|
371
|
+
|
364
372
|
# ... Rest of validation logic ...
|
365
373
|
expired_at = datetime.now(timezone.utc) + timedelta(minutes=self.job_expiry_duration_mins)
|
366
374
|
job_id = self.acp_client.create_job(
|
@@ -408,9 +416,9 @@ class AcpPlugin:
|
|
408
416
|
type="string",
|
409
417
|
description="Why you made this decision",
|
410
418
|
)
|
411
|
-
|
419
|
+
|
412
420
|
args = [job_id_arg, decision_arg, reasoning_arg]
|
413
|
-
|
421
|
+
|
414
422
|
if hasattr(self, 'twitter_plugin') and self.twitter_plugin is not None:
|
415
423
|
tweet_content_arg = Argument(
|
416
424
|
name="tweet_content",
|
@@ -426,19 +434,20 @@ class AcpPlugin:
|
|
426
434
|
executable=self._respond_job_executable
|
427
435
|
)
|
428
436
|
|
429
|
-
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]:
|
430
439
|
if not job_id:
|
431
440
|
return FunctionResultStatus.FAILED, "Missing job ID - specify which job you're responding to", {}
|
432
|
-
|
441
|
+
|
433
442
|
if not decision or decision not in ["ACCEPT", "REJECT"]:
|
434
443
|
return FunctionResultStatus.FAILED, "Invalid decision - must be either 'ACCEPT' or 'REJECT'", {}
|
435
|
-
|
444
|
+
|
436
445
|
if not reasoning:
|
437
446
|
return FunctionResultStatus.FAILED, "Missing reasoning - explain why you made this decision", {}
|
438
447
|
|
439
448
|
try:
|
440
449
|
state = self.get_acp_state()
|
441
|
-
|
450
|
+
|
442
451
|
job = next(
|
443
452
|
(c for c in state["jobs"]["active"]["asASeller"] if c["jobId"] == job_id),
|
444
453
|
None
|
@@ -488,7 +497,7 @@ class AcpPlugin:
|
|
488
497
|
)
|
489
498
|
|
490
499
|
args = [job_id_arg, amount_arg, reasoning_arg]
|
491
|
-
|
500
|
+
|
492
501
|
if hasattr(self, 'twitter_plugin') and self.twitter_plugin is not None:
|
493
502
|
tweet_content_arg = Argument(
|
494
503
|
name="tweet_content",
|
@@ -504,7 +513,8 @@ class AcpPlugin:
|
|
504
513
|
executable=self._pay_job_executable
|
505
514
|
)
|
506
515
|
|
507
|
-
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]:
|
508
518
|
if not job_id:
|
509
519
|
return FunctionResultStatus.FAILED, "Missing job ID - specify which job you're paying for", {}
|
510
520
|
|
@@ -516,7 +526,7 @@ class AcpPlugin:
|
|
516
526
|
|
517
527
|
try:
|
518
528
|
state = self.get_acp_state()
|
519
|
-
|
529
|
+
|
520
530
|
job = next(
|
521
531
|
(c for c in state["jobs"]["active"]["asABuyer"] if c["jobId"] == job_id),
|
522
532
|
None
|
@@ -528,7 +538,6 @@ class AcpPlugin:
|
|
528
538
|
if job["phase"] != AcpJobPhasesDesc.NEGOTIATION:
|
529
539
|
return FunctionResultStatus.FAILED, f"Cannot pay - job is in '{job['phase']}' phase, must be in 'negotiation' phase", {}
|
530
540
|
|
531
|
-
|
532
541
|
self.acp_client.make_payment(
|
533
542
|
job_id,
|
534
543
|
amount,
|
@@ -568,7 +577,7 @@ class AcpPlugin:
|
|
568
577
|
)
|
569
578
|
|
570
579
|
args = [job_id_arg, deliverable_arg, reasoning_arg]
|
571
|
-
|
580
|
+
|
572
581
|
if hasattr(self, 'twitter_plugin') and self.twitter_plugin is not None:
|
573
582
|
tweet_content_arg = Argument(
|
574
583
|
name="tweet_content",
|
@@ -584,19 +593,20 @@ class AcpPlugin:
|
|
584
593
|
executable=self._deliver_job_executable
|
585
594
|
)
|
586
595
|
|
587
|
-
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]:
|
588
598
|
if not job_id:
|
589
599
|
return FunctionResultStatus.FAILED, "Missing job ID - specify which job you're delivering for", {}
|
590
|
-
|
600
|
+
|
591
601
|
if not reasoning:
|
592
602
|
return FunctionResultStatus.FAILED, "Missing reasoning - explain why you're making this delivery", {}
|
593
|
-
|
603
|
+
|
594
604
|
if not deliverable:
|
595
605
|
return FunctionResultStatus.FAILED, "Missing deliverable - specify what you're delivering", {}
|
596
606
|
|
597
607
|
try:
|
598
608
|
state = self.get_acp_state()
|
599
|
-
|
609
|
+
|
600
610
|
job = next(
|
601
611
|
(c for c in state["jobs"]["active"]["asASeller"] if c["jobId"] == job_id),
|
602
612
|
None
|
@@ -609,7 +619,8 @@ class AcpPlugin:
|
|
609
619
|
return FunctionResultStatus.FAILED, f"Cannot deliver - job is in '{job['phase']}' phase, must be in 'transaction' phase", {}
|
610
620
|
|
611
621
|
produced = next(
|
612
|
-
(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"]),
|
613
624
|
None
|
614
625
|
)
|
615
626
|
|
@@ -643,7 +654,7 @@ class AcpPlugin:
|
|
643
654
|
tweet_id = tweet_history[-1].get("tweetId") if tweet_history else None
|
644
655
|
if tweet_id is not None:
|
645
656
|
reply_tweet_fn = self.twitter_plugin.get_function('reply_tweet')
|
646
|
-
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')
|
647
658
|
if tweet_id is not None:
|
648
|
-
self.acp_client.add_tweet(job.get("jobId")
|
659
|
+
self.acp_client.add_tweet(job.get("jobId"), tweet_id, tweet_content)
|
649
660
|
print("Tweet has been posted")
|
acp_plugin_gamesdk/acp_token.py
CHANGED
@@ -1,15 +1,18 @@
|
|
1
|
-
|
1
|
+
import json
|
2
2
|
import time
|
3
|
-
|
3
|
+
import traceback
|
4
4
|
from datetime import datetime
|
5
|
-
from
|
6
|
-
from
|
7
|
-
|
8
|
-
from acp_plugin_gamesdk.acp_token_abi import ACP_TOKEN_ABI
|
5
|
+
from enum import IntEnum
|
6
|
+
from typing import Optional, Tuple, TypedDict
|
7
|
+
|
9
8
|
import requests
|
9
|
+
from eth_account import Account
|
10
10
|
from eth_account.messages import encode_defunct
|
11
|
-
import
|
12
|
-
|
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,46 +77,54 @@ 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
86
|
desc: str
|
80
87
|
price: str
|
81
88
|
providerAddress: Optional[str]
|
89
|
+
clientAddress: Optional[str]
|
82
90
|
phase: AcpJobPhasesDesc
|
83
91
|
memo: List[AcpRequestMemo]
|
84
|
-
tweetHistory
|
92
|
+
tweetHistory: ITweet | List
|
85
93
|
lastUpdated: int
|
94
|
+
getAgentByWalletAddress: Optional[Callable[[str], AcpAgent]]
|
86
95
|
|
87
96
|
def __repr__(self) -> str:
|
88
|
-
output =(
|
97
|
+
output = (
|
89
98
|
f"Job ID: {self.jobId}, "
|
90
99
|
f"Client Name: {self.clientName}, "
|
91
100
|
f"Provider Name: {self.providerName}, "
|
92
101
|
f"Description: {self.desc}, "
|
93
102
|
f"Price: {self.price}, "
|
94
103
|
f"Provider Address: {self.providerAddress}, "
|
104
|
+
f"Client Address: {self.clientAddress}, "
|
95
105
|
f"Phase: {self.phase.value}, "
|
96
106
|
f"Memo: {self.memo}, "
|
97
107
|
f"Tweet History: {self.tweetHistory}, "
|
98
108
|
f"Last Updated: {self.lastUpdated})"
|
99
|
-
)
|
109
|
+
)
|
100
110
|
return output
|
101
111
|
|
112
|
+
|
102
113
|
@dataclass
|
103
114
|
class IDeliverable:
|
104
115
|
type: str
|
105
|
-
value: str
|
116
|
+
value: Union[str, Dict[str, Any], List[Any]]
|
106
117
|
clientName: Optional[str]
|
107
118
|
providerName: Optional[str]
|
119
|
+
|
120
|
+
|
108
121
|
@dataclass
|
109
122
|
class IInventory(IDeliverable):
|
110
123
|
jobId: int
|
111
124
|
clientName: Optional[str]
|
112
125
|
providerName: Optional[str]
|
113
126
|
|
127
|
+
|
114
128
|
@dataclass
|
115
129
|
class AcpJobsSection:
|
116
130
|
asABuyer: List[AcpJob]
|
@@ -119,11 +133,11 @@ class AcpJobsSection:
|
|
119
133
|
def __str__(self) -> str:
|
120
134
|
buyer_jobs = ""
|
121
135
|
for index, job in enumerate(self.asABuyer):
|
122
|
-
buyer_jobs += f"#{index+1} {str(job)} \n"
|
136
|
+
buyer_jobs += f"#{index + 1} {str(job)} \n"
|
123
137
|
|
124
138
|
seller_jobs = ""
|
125
139
|
for index, job in enumerate(self.asASeller):
|
126
|
-
seller_jobs += f"#{index+1} {str(job)} \n"
|
140
|
+
seller_jobs += f"#{index + 1} {str(job)} \n"
|
127
141
|
|
128
142
|
output = (
|
129
143
|
f"As Buyer:\n{buyer_jobs}\n"
|
@@ -131,6 +145,7 @@ class AcpJobsSection:
|
|
131
145
|
)
|
132
146
|
return output
|
133
147
|
|
148
|
+
|
134
149
|
@dataclass
|
135
150
|
class AcpJobs:
|
136
151
|
active: AcpJobsSection
|
@@ -145,7 +160,8 @@ class AcpJobs:
|
|
145
160
|
f"🔴 Cancelled:\n{self.cancelled}\n"
|
146
161
|
)
|
147
162
|
return output
|
148
|
-
|
163
|
+
|
164
|
+
|
149
165
|
@dataclass
|
150
166
|
class AcpInventory:
|
151
167
|
acquired: List[IInventory]
|
@@ -159,6 +175,7 @@ class AcpInventory:
|
|
159
175
|
)
|
160
176
|
return output
|
161
177
|
|
178
|
+
|
162
179
|
@dataclass
|
163
180
|
class AcpState:
|
164
181
|
inventory: AcpInventory
|
@@ -166,9 +183,9 @@ class AcpState:
|
|
166
183
|
|
167
184
|
def __str__(self) -> str:
|
168
185
|
output = (
|
169
|
-
|
170
|
-
|
171
|
-
|
172
|
-
|
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"
|
173
190
|
)
|
174
|
-
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=4-flS9WnQqvwX1mhJJ2SiDmGuQs_sIiec-1fFlipGHk,27022
|
3
|
+
acp_plugin_gamesdk/acp_token.py,sha256=uOeQ6_azL50sJekDlWYjwAtd2YxedDXjRuoCRLFZJ8Q,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=0u3exP_MXHjTF4rrAPQAkGAtSGCaOvhfXewepDr9Hb8,4549
|
7
|
+
acp_plugin_gamesdk-0.1.20.dist-info/METADATA,sha256=Uh-DfnSzyRgxWJnite0txHh2Qqhz9KKDdOioEiB4pGc,12791
|
8
|
+
acp_plugin_gamesdk-0.1.20.dist-info/WHEEL,sha256=XbeZDeTWKc1w7CSIyre5aMDU_-PohRwTQceYnisIYYY,88
|
9
|
+
acp_plugin_gamesdk-0.1.20.dist-info/RECORD,,
|
@@ -1,9 +0,0 @@
|
|
1
|
-
acp_plugin_gamesdk/acp_client.py,sha256=gaTZ6H7tRlz0dDyodhb-KhbzcYQj-KwPJA8YTAqvYWk,9221
|
2
|
-
acp_plugin_gamesdk/acp_plugin.py,sha256=g-DHfhiWmFmvKlXYlNaLaKmQLjXnJ42zDSafONKtJic,27205
|
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=jC6xXDsL8agw9flbszkYPxd01qER2vR2ukQnLJMHYKI,4316
|
7
|
-
acp_plugin_gamesdk-0.1.18.dist-info/METADATA,sha256=fkFk_092a90AHCUzQHmQSkc44bp_KE9p028KbtOHOhM,12791
|
8
|
-
acp_plugin_gamesdk-0.1.18.dist-info/WHEEL,sha256=XbeZDeTWKc1w7CSIyre5aMDU_-PohRwTQceYnisIYYY,88
|
9
|
-
acp_plugin_gamesdk-0.1.18.dist-info/RECORD,,
|
File without changes
|