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.
@@ -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)}", {}