acp-plugin-gamesdk 0.1.0__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 +173 -0
- acp_plugin_gamesdk/acp_plugin.py +459 -0
- acp_plugin_gamesdk/acp_token.py +291 -0
- acp_plugin_gamesdk/acp_token_abi.py +690 -0
- acp_plugin_gamesdk/interface.py +70 -0
- acp_plugin_gamesdk-0.1.0.dist-info/METADATA +201 -0
- acp_plugin_gamesdk-0.1.0.dist-info/RECORD +8 -0
- acp_plugin_gamesdk-0.1.0.dist-info/WHEEL +4 -0
@@ -0,0 +1,173 @@
|
|
1
|
+
from datetime import datetime, timedelta
|
2
|
+
from typing import List, Optional
|
3
|
+
from web3 import Web3
|
4
|
+
import requests
|
5
|
+
|
6
|
+
import sys
|
7
|
+
import os
|
8
|
+
sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), "../../../")))
|
9
|
+
from .interface import AcpAgent, AcpJobPhases, AcpState
|
10
|
+
from .acp_token import AcpToken, MemoType
|
11
|
+
|
12
|
+
class AcpClient:
|
13
|
+
def __init__(self, api_key: str, acp_token: AcpToken):
|
14
|
+
self.base_url = "https://sdk-dev.game.virtuals.io/acp"
|
15
|
+
self.api_key = api_key
|
16
|
+
self.acp_token = acp_token
|
17
|
+
self.web3 = Web3()
|
18
|
+
|
19
|
+
@property
|
20
|
+
def wallet_address(self) -> str:
|
21
|
+
return self.acp_token.get_wallet_address()
|
22
|
+
|
23
|
+
def get_state(self) -> AcpState:
|
24
|
+
response = requests.get(
|
25
|
+
f"{self.base_url}/states/{self.wallet_address}",
|
26
|
+
headers={"x-api-key": self.api_key}
|
27
|
+
)
|
28
|
+
return response.json()
|
29
|
+
|
30
|
+
def browse_agents(self, cluster: Optional[str] = None) -> List[AcpAgent]:
|
31
|
+
url = "https://acpx.virtuals.gg/api/agents"
|
32
|
+
|
33
|
+
params = {}
|
34
|
+
if cluster:
|
35
|
+
params["filters[cluster]"] = cluster
|
36
|
+
|
37
|
+
response = requests.get(url, params=params)
|
38
|
+
|
39
|
+
if response.status_code != 200:
|
40
|
+
raise Exception(f"Failed to browse agents: {response.text}")
|
41
|
+
|
42
|
+
response_json = response.json()
|
43
|
+
|
44
|
+
return [
|
45
|
+
{
|
46
|
+
"id": agent["id"],
|
47
|
+
"name": agent["name"],
|
48
|
+
"description": agent["description"],
|
49
|
+
"walletAddress": agent["walletAddress"]
|
50
|
+
}
|
51
|
+
for agent in response_json.get("data", [])
|
52
|
+
]
|
53
|
+
|
54
|
+
def create_job(self, provider_address: str, price: float, job_description: str) -> int:
|
55
|
+
expire_at = datetime.now() + timedelta(days=1)
|
56
|
+
|
57
|
+
tx_result = self.acp_token.create_job(
|
58
|
+
provider_address=provider_address,
|
59
|
+
expire_at=expire_at
|
60
|
+
)
|
61
|
+
job_id = tx_result["jobId"]
|
62
|
+
memo_response = self.acp_token.create_memo(
|
63
|
+
job_id=job_id,
|
64
|
+
content=job_description,
|
65
|
+
memo_type=MemoType.MESSAGE,
|
66
|
+
is_secured=False,
|
67
|
+
next_phase=AcpJobPhases.NEGOTIATION
|
68
|
+
)
|
69
|
+
|
70
|
+
payload = {
|
71
|
+
"jobId": job_id,
|
72
|
+
"clientAddress": self.acp_token.get_wallet_address(),
|
73
|
+
"providerAddress": provider_address,
|
74
|
+
"description": job_description,
|
75
|
+
"price": price,
|
76
|
+
"expiredAt": expire_at.isoformat()
|
77
|
+
}
|
78
|
+
|
79
|
+
requests.post(
|
80
|
+
self.base_url,
|
81
|
+
json=payload,
|
82
|
+
headers={
|
83
|
+
"Accept": "application/json",
|
84
|
+
"Content-Type": "application/json",
|
85
|
+
"x-api-key": self.api_key
|
86
|
+
}
|
87
|
+
)
|
88
|
+
|
89
|
+
return job_id
|
90
|
+
|
91
|
+
def response_job(self, job_id: int, accept: bool, memo_id: int, reasoning: str):
|
92
|
+
if accept:
|
93
|
+
tx_hash = self.acp_token.sign_memo(memo_id, accept, reasoning)
|
94
|
+
|
95
|
+
return self.acp_token.create_memo(
|
96
|
+
job_id=job_id,
|
97
|
+
content=f"Job {job_id} accepted. {reasoning}",
|
98
|
+
memo_type=MemoType.MESSAGE,
|
99
|
+
is_secured=False,
|
100
|
+
next_phase=AcpJobPhases.TRANSACTION
|
101
|
+
)
|
102
|
+
else:
|
103
|
+
return self.acp_token.create_memo(
|
104
|
+
job_id=job_id,
|
105
|
+
content=f"Job {job_id} rejected. {reasoning}",
|
106
|
+
memo_type=MemoType.MESSAGE,
|
107
|
+
is_secured=False,
|
108
|
+
next_phase=AcpJobPhases.REJECTED
|
109
|
+
)
|
110
|
+
|
111
|
+
def make_payment(self, job_id: int, amount: float, memo_id: int, reason: str):
|
112
|
+
# Convert amount to Wei (smallest ETH unit)
|
113
|
+
amount_wei = self.web3.to_wei(amount, 'ether')
|
114
|
+
|
115
|
+
tx_hash = self.acp_token.set_budget(job_id, amount_wei)
|
116
|
+
approval_tx_hash = self.acp_token.approve_allowance(amount_wei)
|
117
|
+
return self.acp_token.sign_memo(memo_id, True, reason)
|
118
|
+
|
119
|
+
def deliver_job(self, job_id: int, deliverable: str, memo_id: int, reason: str):
|
120
|
+
return self.acp_token.create_memo(
|
121
|
+
job_id=job_id,
|
122
|
+
content=deliverable,
|
123
|
+
memo_type=MemoType.MESSAGE,
|
124
|
+
is_secured=False,
|
125
|
+
next_phase=AcpJobPhases.COMPLETED
|
126
|
+
)
|
127
|
+
|
128
|
+
def add_tweet(self, job_id: int, tweet_id: str, content: str):
|
129
|
+
"""
|
130
|
+
Add a tweet to a job.
|
131
|
+
|
132
|
+
Args:
|
133
|
+
job_id: The ID of the job
|
134
|
+
tweet_id: The ID of the tweet
|
135
|
+
content: The content of the tweet
|
136
|
+
|
137
|
+
Raises:
|
138
|
+
Exception: If the request fails
|
139
|
+
"""
|
140
|
+
payload = {
|
141
|
+
"tweetId": tweet_id,
|
142
|
+
"content": content
|
143
|
+
}
|
144
|
+
|
145
|
+
response = requests.post(
|
146
|
+
f"{self.base_url}/{job_id}/tweets/{self.wallet_address}",
|
147
|
+
json=payload,
|
148
|
+
headers={
|
149
|
+
"Accept": "application/json",
|
150
|
+
"Content-Type": "application/json",
|
151
|
+
"x-api-key": self.api_key
|
152
|
+
}
|
153
|
+
)
|
154
|
+
|
155
|
+
if response.status_code != 200 and response.status_code != 201:
|
156
|
+
raise Exception(f"Failed to add tweet: {response.status_code} {response.text}")
|
157
|
+
|
158
|
+
|
159
|
+
return response.json()
|
160
|
+
|
161
|
+
def reset_state(self, wallet_address: str ) -> None:
|
162
|
+
if not wallet_address:
|
163
|
+
raise Exception("Wallet address is required")
|
164
|
+
|
165
|
+
address = wallet_address
|
166
|
+
|
167
|
+
response = requests.delete(
|
168
|
+
f"{self.base_url}/states/{address}",
|
169
|
+
headers={"x-api-key": self.api_key}
|
170
|
+
)
|
171
|
+
|
172
|
+
if response.status_code not in [200, 204]:
|
173
|
+
raise Exception(f"Failed to reset state: {response.status_code} {response.text}")
|
@@ -0,0 +1,459 @@
|
|
1
|
+
from typing import List, Dict, Any, Optional,Tuple
|
2
|
+
import json
|
3
|
+
from dataclasses import dataclass
|
4
|
+
from datetime import datetime
|
5
|
+
|
6
|
+
from game_sdk.game.agent import WorkerConfig
|
7
|
+
from game_sdk.game.custom_types import Function, FunctionResultStatus
|
8
|
+
from twitter_plugin_gamesdk.twitter_plugin import TwitterPlugin
|
9
|
+
|
10
|
+
from twitter_plugin_gamesdk.twitter_plugin import TwitterPlugin
|
11
|
+
from twitter_plugin_gamesdk.game_twitter_plugin import GameTwitterPlugin
|
12
|
+
from .acp_client import AcpClient
|
13
|
+
from .acp_token import AcpToken
|
14
|
+
from .interface import AcpJobPhasesDesc, IInventory
|
15
|
+
|
16
|
+
@dataclass
|
17
|
+
class AdNetworkPluginOptions:
|
18
|
+
api_key: str
|
19
|
+
acp_token_client: AcpToken
|
20
|
+
twitter_plugin: TwitterPlugin | GameTwitterPlugin = None
|
21
|
+
cluster: Optional[str] = None
|
22
|
+
|
23
|
+
class AcpPlugin:
|
24
|
+
def __init__(self, options: AdNetworkPluginOptions):
|
25
|
+
print("Initializing AcpPlugin")
|
26
|
+
self.acp_client = AcpClient(options.api_key, options.acp_token_client)
|
27
|
+
|
28
|
+
self.id = "acp_worker"
|
29
|
+
self.name = "ACP Worker"
|
30
|
+
self.description = """
|
31
|
+
Handles trading transactions and jobs between agents. This worker ONLY manages:
|
32
|
+
|
33
|
+
1. RESPONDING to Buy/Sell Needs
|
34
|
+
- Find sellers when YOU need to buy something
|
35
|
+
- Handle incoming purchase requests when others want to buy from YOU
|
36
|
+
- NO prospecting or client finding
|
37
|
+
|
38
|
+
2. Job Management
|
39
|
+
- Process purchase requests. Accept or reject job.
|
40
|
+
- Send payments
|
41
|
+
- Manage and deliver services and goods
|
42
|
+
|
43
|
+
NOTE: This is NOT for finding clients - only for executing trades when there's a specific need to buy or sell something.
|
44
|
+
"""
|
45
|
+
self.cluster = options.cluster
|
46
|
+
self.twitter_plugin = options.twitter_plugin
|
47
|
+
self.produced_inventory: List[IInventory] = []
|
48
|
+
|
49
|
+
def add_produce_item(self, item: IInventory) -> None:
|
50
|
+
self.produced_inventory.append(item)
|
51
|
+
|
52
|
+
def reset_state(self) -> None:
|
53
|
+
self.acp_client.reset_state(self.acp_client.wallet_address)
|
54
|
+
|
55
|
+
def get_acp_state(self) -> Dict:
|
56
|
+
server_state = self.acp_client.get_state()
|
57
|
+
server_state["inventory"]["produced"] = self.produced_inventory
|
58
|
+
return server_state
|
59
|
+
|
60
|
+
def get_worker(self, data: Optional[Dict] = None) -> WorkerConfig:
|
61
|
+
functions = data.get("functions") if data else [
|
62
|
+
self.search_agents_functions,
|
63
|
+
self.initiate_job,
|
64
|
+
self.respond_job,
|
65
|
+
self.pay_job,
|
66
|
+
self.deliver_job,
|
67
|
+
]
|
68
|
+
|
69
|
+
def get_environment(_e, __) -> Dict[str, Any]:
|
70
|
+
environment = data.get_environment() if hasattr(data, "get_environment") else {}
|
71
|
+
return {
|
72
|
+
**environment,
|
73
|
+
**(self.get_acp_state()),
|
74
|
+
}
|
75
|
+
|
76
|
+
data = WorkerConfig(
|
77
|
+
id=self.id,
|
78
|
+
worker_description=self.description,
|
79
|
+
action_space=functions,
|
80
|
+
get_state_fn=get_environment,
|
81
|
+
instruction=data.get("instructions") if data else None
|
82
|
+
)
|
83
|
+
|
84
|
+
return data
|
85
|
+
|
86
|
+
@property
|
87
|
+
def agent_description(self) -> str:
|
88
|
+
return """
|
89
|
+
Inventory structure
|
90
|
+
- inventory.aquired: Deliverable that your have bought and can be use to achived your objective
|
91
|
+
- inventory.produced: Deliverable that needs to be delivered to your seller
|
92
|
+
|
93
|
+
Job Structure:
|
94
|
+
- jobs.active:
|
95
|
+
* asABuyer: Pending resource purchases
|
96
|
+
* asASeller: Pending design requests
|
97
|
+
- jobs.completed: Successfully fulfilled projects
|
98
|
+
- jobs.cancelled: Terminated or rejected requests
|
99
|
+
- Each job tracks:
|
100
|
+
* 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
|
101
|
+
"""
|
102
|
+
|
103
|
+
def _search_agents_executable(self,reasoning: str) -> Tuple[FunctionResultStatus, str, dict]:
|
104
|
+
if not reasoning:
|
105
|
+
return FunctionResultStatus.FAILED, "Reasoning for the search must be provided. This helps track your decision-making process for future reference.", {}
|
106
|
+
|
107
|
+
agents = self.acp_client.browse_agents(self.cluster)
|
108
|
+
|
109
|
+
if not agents:
|
110
|
+
return FunctionResultStatus.FAILED, "No other trading agents found in the system. Please try again later when more agents are available.", {}
|
111
|
+
|
112
|
+
return FunctionResultStatus.DONE, json.dumps({
|
113
|
+
"availableAgents": agents,
|
114
|
+
"totalAgentsFound": len(agents),
|
115
|
+
"timestamp": datetime.now().timestamp(),
|
116
|
+
"note": "Use the walletAddress when initiating a job with your chosen trading partner."
|
117
|
+
}), {}
|
118
|
+
|
119
|
+
@property
|
120
|
+
def search_agents_functions(self) -> Function:
|
121
|
+
return Function(
|
122
|
+
fn_name="search_agents",
|
123
|
+
fn_description="Get a list of all available trading agents and what they're selling. Use this function before initiating a job to discover potential trading partners. Each agent's entry will show their ID, name, type, walletAddress, description and product catalog with prices.",
|
124
|
+
args=[
|
125
|
+
{
|
126
|
+
"name": "reasoning",
|
127
|
+
"type": "string",
|
128
|
+
"description": "Explain why you need to find trading partners at this time",
|
129
|
+
}
|
130
|
+
],
|
131
|
+
executable=self._search_agents_executable
|
132
|
+
)
|
133
|
+
|
134
|
+
@property
|
135
|
+
def initiate_job(self) -> Function:
|
136
|
+
return Function(
|
137
|
+
fn_name="initiate_job",
|
138
|
+
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.",
|
139
|
+
args=[
|
140
|
+
{
|
141
|
+
"name": "sellerWalletAddress",
|
142
|
+
"type": "string",
|
143
|
+
"description": "The seller's agent wallet address you want to buy from",
|
144
|
+
},
|
145
|
+
{
|
146
|
+
"name": "price",
|
147
|
+
"type": "string",
|
148
|
+
"description": "Offered price for service",
|
149
|
+
},
|
150
|
+
{
|
151
|
+
"name": "reasoning",
|
152
|
+
"type": "string",
|
153
|
+
"description": "Why you are making this purchase request",
|
154
|
+
},
|
155
|
+
{
|
156
|
+
"name": "serviceRequirements",
|
157
|
+
"type": "string",
|
158
|
+
"description": "Detailed specifications for service-based items",
|
159
|
+
},
|
160
|
+
{
|
161
|
+
"name": "tweetContent",
|
162
|
+
"type": "string",
|
163
|
+
"description": "Tweet content that will be posted about this job. Must include the seller's Twitter handle (with @ symbol) to notify them",
|
164
|
+
},
|
165
|
+
],
|
166
|
+
executable=self._initiate_job_executable
|
167
|
+
)
|
168
|
+
|
169
|
+
def _initiate_job_executable(self, sellerWalletAddress: str, price: str, reasoning: str, serviceRequirements: str, tweetContent : str) -> Tuple[FunctionResultStatus, str, dict]:
|
170
|
+
if not price:
|
171
|
+
return FunctionResultStatus.FAILED, "Missing price - specify how much you're offering per unit", {}
|
172
|
+
|
173
|
+
try:
|
174
|
+
state = self.get_acp_state()
|
175
|
+
|
176
|
+
if state["jobs"]["active"]["asABuyer"]:
|
177
|
+
return FunctionResultStatus.FAILED, "You already have an active job as a buyer", {}
|
178
|
+
|
179
|
+
# ... Rest of validation logic ...
|
180
|
+
|
181
|
+
job_id = self.acp_client.create_job(
|
182
|
+
sellerWalletAddress,
|
183
|
+
float(price),
|
184
|
+
serviceRequirements
|
185
|
+
)
|
186
|
+
|
187
|
+
if (self.twitter_plugin is not None and tweetContent is not None):
|
188
|
+
post_tweet_fn = self.twitter_plugin.get_function('post_tweet')
|
189
|
+
tweet_id = post_tweet_fn(tweetContent, None).get('data', {}).get('id')
|
190
|
+
if (tweet_id is not None):
|
191
|
+
self.acp_client.add_tweet(job_id,tweet_id, tweetContent)
|
192
|
+
print("Tweet has been posted")
|
193
|
+
|
194
|
+
return FunctionResultStatus.DONE, json.dumps({
|
195
|
+
"jobId": job_id,
|
196
|
+
"sellerWalletAddress": sellerWalletAddress,
|
197
|
+
"price": float(price),
|
198
|
+
"serviceRequirements": serviceRequirements,
|
199
|
+
"timestamp": datetime.now().timestamp(),
|
200
|
+
}), {}
|
201
|
+
except Exception as e:
|
202
|
+
return FunctionResultStatus.FAILED, f"System error while initiating job - try again after a short delay. {str(e)}", {}
|
203
|
+
|
204
|
+
@property
|
205
|
+
def respond_job(self) -> Function:
|
206
|
+
return Function(
|
207
|
+
fn_name="respond_to_job",
|
208
|
+
fn_description="Accepts or rejects an incoming 'request' job",
|
209
|
+
args=[
|
210
|
+
{
|
211
|
+
"name": "jobId",
|
212
|
+
"type": "string",
|
213
|
+
"description": "The job ID you are responding to",
|
214
|
+
},
|
215
|
+
{
|
216
|
+
"name": "decision",
|
217
|
+
"type": "string",
|
218
|
+
"description": "Your response: 'ACCEPT' or 'REJECT'",
|
219
|
+
},
|
220
|
+
{
|
221
|
+
"name": "reasoning",
|
222
|
+
"type": "string",
|
223
|
+
"description": "Why you made this decision",
|
224
|
+
},
|
225
|
+
{
|
226
|
+
"name": "tweetContent",
|
227
|
+
"type": "string",
|
228
|
+
"description": "Tweet content that will be posted about this job. Must include the seller's Twitter handle (with @ symbol) to notify them",
|
229
|
+
},
|
230
|
+
],
|
231
|
+
executable=self._respond_job_executable
|
232
|
+
)
|
233
|
+
|
234
|
+
def _respond_job_executable(self, jobId: str, decision: str, reasoning: str, tweetContent: str) -> Tuple[FunctionResultStatus, str, dict]:
|
235
|
+
if not jobId:
|
236
|
+
return FunctionResultStatus.FAILED, "Missing job ID - specify which job you're responding to", {}
|
237
|
+
|
238
|
+
if not decision or decision not in ["ACCEPT", "REJECT"]:
|
239
|
+
return FunctionResultStatus.FAILED, "Invalid decision - must be either 'ACCEPT' or 'REJECT'", {}
|
240
|
+
|
241
|
+
if not reasoning:
|
242
|
+
return FunctionResultStatus.FAILED, "Missing reasoning - explain why you made this decision", {}
|
243
|
+
|
244
|
+
try:
|
245
|
+
state = self.get_acp_state()
|
246
|
+
|
247
|
+
job = next(
|
248
|
+
(c for c in state["jobs"]["active"]["asASeller"] if c["jobId"] == int(jobId)),
|
249
|
+
None
|
250
|
+
)
|
251
|
+
|
252
|
+
if not job:
|
253
|
+
return FunctionResultStatus.FAILED, "Job not found in your seller jobs - check the ID and verify you're the seller", {}
|
254
|
+
|
255
|
+
if job["phase"] != AcpJobPhasesDesc.REQUEST:
|
256
|
+
return FunctionResultStatus.FAILED, f"Cannot respond - job is in '{job['phase']}' phase, must be in 'request' phase", {}
|
257
|
+
|
258
|
+
self.acp_client.response_job(
|
259
|
+
int(jobId),
|
260
|
+
decision == "ACCEPT",
|
261
|
+
job["memo"][0]["id"],
|
262
|
+
reasoning
|
263
|
+
)
|
264
|
+
|
265
|
+
if (self.twitter_plugin is not None):
|
266
|
+
tweet_history = job.get("tweetHistory", [])
|
267
|
+
tweet_id = tweet_history[-1].get("tweetId") if tweet_history else None
|
268
|
+
if (tweet_id is not None):
|
269
|
+
reply_tweet_fn = self.twitter_plugin.get_function('reply_tweet')
|
270
|
+
tweet_id = reply_tweet_fn(tweet_id,tweetContent, None).get('data', {}).get('id')
|
271
|
+
if (tweet_id is not None):
|
272
|
+
self.acp_client.add_tweet(jobId ,tweet_id, tweetContent)
|
273
|
+
print("Tweet has been posted")
|
274
|
+
|
275
|
+
return FunctionResultStatus.DONE, json.dumps({
|
276
|
+
"jobId": jobId,
|
277
|
+
"decision": decision,
|
278
|
+
"timestamp": datetime.now().timestamp()
|
279
|
+
}), {}
|
280
|
+
except Exception as e:
|
281
|
+
return FunctionResultStatus.FAILED, f"System error while responding to job - try again after a short delay. {str(e)}", {}
|
282
|
+
|
283
|
+
@property
|
284
|
+
def pay_job(self) -> Function:
|
285
|
+
return Function(
|
286
|
+
fn_name="pay_job",
|
287
|
+
fn_description="Processes payment for an accepted purchase request",
|
288
|
+
args=[
|
289
|
+
{
|
290
|
+
"name": "jobId",
|
291
|
+
"type": "number",
|
292
|
+
"description": "The job ID you are paying for",
|
293
|
+
},
|
294
|
+
{
|
295
|
+
"name": "amount",
|
296
|
+
"type": "number",
|
297
|
+
"description": "The total amount to pay",
|
298
|
+
},
|
299
|
+
{
|
300
|
+
"name": "reasoning",
|
301
|
+
"type": "string",
|
302
|
+
"description": "Why you are making this payment",
|
303
|
+
},
|
304
|
+
{
|
305
|
+
"name": "tweetContent",
|
306
|
+
"type": "string",
|
307
|
+
"description": "Tweet content that will be posted about this job. Must include the seller's Twitter handle (with @ symbol) to notify them",
|
308
|
+
},
|
309
|
+
],
|
310
|
+
executable=self._pay_job_executable
|
311
|
+
)
|
312
|
+
|
313
|
+
def _pay_job_executable(self, jobId: str, amount: str, reasoning: str, tweetContent: str) -> Tuple[FunctionResultStatus, str, dict]:
|
314
|
+
if not jobId:
|
315
|
+
return FunctionResultStatus.FAILED, "Missing job ID - specify which job you're paying for", {}
|
316
|
+
|
317
|
+
if not amount:
|
318
|
+
return FunctionResultStatus.FAILED, "Missing amount - specify how much you're paying", {}
|
319
|
+
|
320
|
+
if not reasoning:
|
321
|
+
return FunctionResultStatus.FAILED, "Missing reasoning - explain why you're making this payment", {}
|
322
|
+
|
323
|
+
try:
|
324
|
+
state = self.get_acp_state()
|
325
|
+
|
326
|
+
job = next(
|
327
|
+
(c for c in state["jobs"]["active"]["asABuyer"] if c["jobId"] == int(jobId)),
|
328
|
+
None
|
329
|
+
)
|
330
|
+
|
331
|
+
if not job:
|
332
|
+
return FunctionResultStatus.FAILED, "Job not found in your buyer jobs - check the ID and verify you're the buyer", {}
|
333
|
+
|
334
|
+
if job["phase"] != AcpJobPhasesDesc.NEGOTIATION:
|
335
|
+
return FunctionResultStatus.FAILED, f"Cannot pay - job is in '{job['phase']}' phase, must be in 'negotiation' phase", {}
|
336
|
+
|
337
|
+
|
338
|
+
self.acp_client.make_payment(
|
339
|
+
int(jobId),
|
340
|
+
float(amount),
|
341
|
+
job["memo"][0]["id"],
|
342
|
+
reasoning
|
343
|
+
)
|
344
|
+
|
345
|
+
if (self.twitter_plugin is not None):
|
346
|
+
tweet_history = job.get("tweetHistory", [])
|
347
|
+
tweet_id = tweet_history[-1].get("tweetId") if tweet_history else None
|
348
|
+
if (tweet_id is not None):
|
349
|
+
reply_tweet_fn = self.twitter_plugin.get_function('reply_tweet')
|
350
|
+
tweet_id = reply_tweet_fn(tweet_id,tweetContent, None).get('data', {}).get('id')
|
351
|
+
if (tweet_id is not None):
|
352
|
+
self.acp_client.add_tweet(jobId ,tweet_id, tweetContent)
|
353
|
+
print("Tweet has been posted")
|
354
|
+
|
355
|
+
return FunctionResultStatus.DONE, json.dumps({
|
356
|
+
"jobId": jobId,
|
357
|
+
"amountPaid": amount,
|
358
|
+
"timestamp": datetime.now().timestamp()
|
359
|
+
}), {}
|
360
|
+
except Exception as e:
|
361
|
+
return FunctionResultStatus.FAILED, f"System error while processing payment - try again after a short delay. {str(e)}", {}
|
362
|
+
|
363
|
+
@property
|
364
|
+
def deliver_job(self) -> Function:
|
365
|
+
return Function(
|
366
|
+
fn_name="deliver_job",
|
367
|
+
fn_description="Completes a sale by delivering items to the buyer",
|
368
|
+
args=[
|
369
|
+
{
|
370
|
+
"name": "jobId",
|
371
|
+
"type": "string",
|
372
|
+
"description": "The job ID you are delivering for",
|
373
|
+
},
|
374
|
+
{
|
375
|
+
"name": "deliverableType",
|
376
|
+
"type": "string",
|
377
|
+
"description": "Type of the deliverable",
|
378
|
+
},
|
379
|
+
{
|
380
|
+
"name": "deliverable",
|
381
|
+
"type": "string",
|
382
|
+
"description": "The deliverable item",
|
383
|
+
},
|
384
|
+
{
|
385
|
+
"name": "reasoning",
|
386
|
+
"type": "string",
|
387
|
+
"description": "Why you are making this delivery",
|
388
|
+
},
|
389
|
+
{
|
390
|
+
"name": "tweetContent",
|
391
|
+
"type": "string",
|
392
|
+
"description": "Tweet content that will be posted about this job. Must include the seller's Twitter handle (with @ symbol) to notify them",
|
393
|
+
},
|
394
|
+
],
|
395
|
+
executable=self._deliver_job_executable
|
396
|
+
)
|
397
|
+
|
398
|
+
def _deliver_job_executable(self, jobId: str, deliverableType: str, deliverable: str, reasoning: str, tweetContent: str) -> Tuple[FunctionResultStatus, str, dict]:
|
399
|
+
if not jobId:
|
400
|
+
return FunctionResultStatus.FAILED, "Missing job ID - specify which job you're delivering for", {}
|
401
|
+
|
402
|
+
if not reasoning:
|
403
|
+
return FunctionResultStatus.FAILED, "Missing reasoning - explain why you're making this delivery", {}
|
404
|
+
|
405
|
+
if not deliverable:
|
406
|
+
return FunctionResultStatus.FAILED, "Missing deliverable - specify what you're delivering", {}
|
407
|
+
|
408
|
+
try:
|
409
|
+
state = self.get_acp_state()
|
410
|
+
|
411
|
+
job = next(
|
412
|
+
(c for c in state["jobs"]["active"]["asASeller"] if c["jobId"] == int(jobId)),
|
413
|
+
None
|
414
|
+
)
|
415
|
+
|
416
|
+
if not job:
|
417
|
+
return FunctionResultStatus.FAILED, "Job not found in your seller jobs - check the ID and verify you're the seller", {}
|
418
|
+
|
419
|
+
if job["phase"] != AcpJobPhasesDesc.TRANSACTION:
|
420
|
+
return FunctionResultStatus.FAILED, f"Cannot deliver - job is in '{job['phase']}' phase, must be in 'transaction' phase", {}
|
421
|
+
|
422
|
+
produced = next(
|
423
|
+
(i for i in self.produced_inventory if i["jobId"] == job["jobId"]),
|
424
|
+
None
|
425
|
+
)
|
426
|
+
|
427
|
+
if not produced:
|
428
|
+
return FunctionResultStatus.FAILED, "Cannot deliver - you should be producing the deliverable first before delivering it", {}
|
429
|
+
|
430
|
+
deliverable = {
|
431
|
+
"type": deliverableType,
|
432
|
+
"value": deliverable
|
433
|
+
}
|
434
|
+
|
435
|
+
self.acp_client.deliver_job(
|
436
|
+
int(jobId),
|
437
|
+
json.dumps(deliverable),
|
438
|
+
job["memo"][0]["id"],
|
439
|
+
reasoning
|
440
|
+
)
|
441
|
+
|
442
|
+
if (self.twitter_plugin is not None):
|
443
|
+
tweet_history = job.get("tweetHistory", [])
|
444
|
+
tweet_id = tweet_history[-1].get("tweetId") if tweet_history else None
|
445
|
+
if (tweet_id is not None):
|
446
|
+
reply_tweet_fn = self.twitter_plugin.get_function('reply_tweet')
|
447
|
+
tweet_id = reply_tweet_fn(tweet_id,tweetContent, None).get('data', {}).get('id')
|
448
|
+
if (tweet_id is not None):
|
449
|
+
self.acp_client.add_tweet(jobId ,tweet_id, tweetContent)
|
450
|
+
print("Tweet has been posted")
|
451
|
+
|
452
|
+
return FunctionResultStatus.DONE, json.dumps({
|
453
|
+
"status": "success",
|
454
|
+
"jobId": jobId,
|
455
|
+
"deliverable": deliverable,
|
456
|
+
"timestamp": datetime.now().timestamp()
|
457
|
+
}), {}
|
458
|
+
except Exception as e:
|
459
|
+
return FunctionResultStatus.FAILED, f"System error while delivering items - try again after a short delay. {str(e)}", {}
|