Skip to content

Commit b4f4a49

Browse files
committed
feat: improve acp sdk usage
and also improve agent prompts
1 parent bff09eb commit b4f4a49

File tree

6 files changed

+191
-206
lines changed

6 files changed

+191
-206
lines changed

plugins/acp/acp_plugin_gamesdk/acp_plugin.py

Lines changed: 94 additions & 111 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,18 @@
1+
import ast
12
import json
23
import traceback
4+
from contextlib import suppress
35
from dataclasses import dataclass
46
from datetime import datetime, timezone, timedelta
5-
from typing import List, Dict, Any, Optional,Tuple
7+
from typing import List, Dict, Any, Optional, Tuple, Union
68

79
import requests
810

911
from game_sdk.game.agent import WorkerConfig
1012
from game_sdk.game.custom_types import Argument, Function, FunctionResultStatus
1113
from twitter_plugin_gamesdk.twitter_plugin import TwitterPlugin
1214
from virtuals_acp import IDeliverable
13-
from virtuals_acp.models import ACPGraduationStatus, ACPOnlineStatus
15+
from virtuals_acp.models import ACPGraduationStatus, ACPOnlineStatus, ACPJobPhase
1416

1517
from acp_plugin_gamesdk.interface import AcpJobPhasesDesc, IInventory, ACP_JOB_PHASE_MAP
1618
from virtuals_acp.client import VirtualsACP
@@ -241,7 +243,11 @@ def _search_agents_executable(self, reasoning: str, keyword: str) -> Tuple[Funct
241243
"wallet_address": agent.wallet_address,
242244
"offerings": (
243245
[
244-
{"name": offering.type, "price": offering.price}
246+
{
247+
"name": offering.name,
248+
"price": offering.price,
249+
"requirement_schema": offering.requirement_schema,
250+
}
245251
for offering in agent.offerings
246252
]
247253
if agent.offerings
@@ -287,22 +293,20 @@ def initiate_job(self) -> Function:
287293
description="The seller's agent wallet address you want to buy from",
288294
)
289295

290-
price_arg = Argument(
291-
name="price",
296+
service_name_arg = Argument(
297+
name="service_name",
292298
type="string",
293-
description="Offered price for service",
299+
description="The service name you want to purchase",
294300
)
295301

296-
reasoning_arg = Argument(
297-
name="reasoning",
298-
type="string",
299-
description="Why you are making this purchase request",
300-
)
301-
302-
service_requirements_arg = Argument(
303-
name="service_requirements",
304-
type="string",
305-
description="Detailed specifications for service-based items",
302+
service_requirement_arg = Argument(
303+
name="service_requirement",
304+
type="string or python dictionary",
305+
description="""
306+
Detailed specifications for service-based items,
307+
give in python dictionary (NOT WRAPPED WITH '', just python dictionary as is) that MATCHES the service's
308+
requirement schema if required, else give in the requirement in a natural language sentence.
309+
""",
306310
)
307311

308312
require_evaluation_arg = Argument(
@@ -317,7 +321,20 @@ def initiate_job(self) -> Function:
317321
description="Keyword to search for a evaluator",
318322
)
319323

320-
args = [seller_wallet_address_arg, price_arg, reasoning_arg, service_requirements_arg, require_evaluation_arg, evaluator_keyword_arg]
324+
reasoning_arg = Argument(
325+
name="reasoning",
326+
type="string",
327+
description="Why you are making this purchase request",
328+
)
329+
330+
args = [
331+
seller_wallet_address_arg,
332+
service_name_arg,
333+
service_requirement_arg,
334+
require_evaluation_arg,
335+
evaluator_keyword_arg,
336+
reasoning_arg
337+
]
321338

322339
if hasattr(self, 'twitter_plugin') and self.twitter_plugin is not None:
323340
tweet_content_arg = Argument(
@@ -334,16 +351,13 @@ def initiate_job(self) -> Function:
334351
executable=self._initiate_job_executable
335352
)
336353

337-
def _initiate_job_executable(self, seller_wallet_address: str, price: str, reasoning: str, service_requirements: str, require_evaluation: str, evaluator_keyword: str, tweet_content: Optional[str] = None) -> Tuple[FunctionResultStatus, str, dict]:
354+
def _initiate_job_executable(self, seller_wallet_address: str, service_name: str, service_requirement: Union[str, Dict[str, Any]], require_evaluation: str, evaluator_keyword: str, reasoning: str, tweet_content: Optional[str] = None) -> Tuple[FunctionResultStatus, str, dict]:
338355
if isinstance(require_evaluation, str):
339356
require_evaluation = require_evaluation.lower() == 'true'
340357
elif isinstance(require_evaluation, bool):
341358
require_evaluation = require_evaluation
342359
else:
343360
require_evaluation = False
344-
345-
if not price:
346-
return FunctionResultStatus.FAILED, "Missing price - specify how much you're offering per unit", {}
347361

348362
if not reasoning:
349363
return FunctionResultStatus.FAILED, "Missing reasoning - explain why you're making this purchase request", {}
@@ -364,29 +378,48 @@ def _initiate_job_executable(self, seller_wallet_address: str, price: str, reaso
364378
return FunctionResultStatus.FAILED, "No evaluator found - try a different keyword", {}
365379

366380
evaluator_address = validators[0].wallet_address
367-
381+
368382
expired_at = datetime.now(timezone.utc) + timedelta(minutes=self.job_expiry_duration_mins)
369-
job_id = self.acp_client.initiate_job(
370-
seller_wallet_address,
371-
service_requirements,
372-
float(price),
383+
384+
agent = self.acp_client.get_agent(seller_wallet_address)
385+
offering = next(
386+
(o for o in agent.offerings if o.name == service_name),
387+
None
388+
)
389+
390+
if offering is None:
391+
return (
392+
FunctionResultStatus.FAILED,
393+
f"No offering found with name '{service_name}', available offerings are: {', '.join(str(agent.offerings))}",
394+
{}
395+
)
396+
397+
if isinstance(service_requirement, str):
398+
# failsafe if GAME still passes in dictionary wrapped with quotes and treated as a str
399+
if (sr := service_requirement.strip()).startswith("{") and sr.endswith("}"):
400+
with suppress(ValueError, SyntaxError):
401+
service_requirement = ast.literal_eval(sr)
402+
403+
job_id = offering.initiate_job(
404+
service_requirement,
373405
evaluator_address,
374406
expired_at
375407
)
408+
job = self.acp_client.get_job_by_onchain_id(job_id)
376409

377410
if hasattr(self, 'twitter_plugin') and self.twitter_plugin is not None and tweet_content is not None:
378-
self._tweet_job(job_id, f"{tweet_content} #{job_id}")
411+
self._tweet_job(job, f"{tweet_content} #{job_id}")
379412

380413
return FunctionResultStatus.DONE, json.dumps({
381414
"job_id": job_id,
382415
"seller_wallet_address": seller_wallet_address,
383-
"price": float(price),
384-
"service_requirements": service_requirements,
385-
"timestamp": datetime.now().timestamp(),
416+
"service_name": service_name,
417+
"service_requirements": service_requirement,
418+
"timestamp": datetime.now().timestamp()
386419
}), {}
387420
except Exception as e:
388421
print(traceback.format_exc())
389-
return FunctionResultStatus.FAILED, f"System error while initiating job - try again after a short delay. {str(e)}", {}
422+
return FunctionResultStatus.FAILED, f"Error while initiating job - try initiating the job again.\n{str(e)}", {}
390423

391424
@property
392425
def respond_job(self) -> Function:
@@ -436,30 +469,18 @@ def _respond_job_executable(self, job_id: int, decision: str, reasoning: str, tw
436469
return FunctionResultStatus.FAILED, "Missing reasoning - explain why you made this decision", {}
437470

438471
try:
439-
state = self.get_acp_state()
440-
441-
job = next(
442-
(c for c in state["jobs"]["active"]["as_a_seller"] if c["job_id"] == job_id),
443-
None
444-
)
472+
job = self.acp_client.get_job_by_onchain_id(job_id)
445473

446474
if not job:
447475
return FunctionResultStatus.FAILED, "Job not found in your seller jobs - check the ID and verify you're the seller", {}
448476

449-
if job["phase"] != AcpJobPhasesDesc.REQUEST:
450-
return FunctionResultStatus.FAILED, f"Cannot respond - job is in '{job['phase']}' phase, must be in 'request' phase", {}
477+
if job.phase != ACPJobPhase.REQUEST:
478+
return FunctionResultStatus.FAILED, f"Cannot respond - job is in '{ACP_JOB_PHASE_MAP.get(job.phase)}' phase, must be in 'request' phase", {}
451479

452-
self.acp_client.respond_to_job_memo(
453-
job_id,
454-
job["memo"][0]["id"],
455-
decision == "ACCEPT",
456-
reasoning
457-
)
480+
job.respond(decision == "ACCEPT", None, reasoning)
458481

459482
if hasattr(self, 'twitter_plugin') and self.twitter_plugin is not None and tweet_content is not None:
460-
tweet_id = job.get("tweet_history", [])[0].get("tweet_id") if job.get("tweet_history") else None
461-
if tweet_id:
462-
self._tweet_job(job_id, tweet_content, tweet_id)
483+
self._tweet_job(job, tweet_content)
463484

464485
return FunctionResultStatus.DONE, json.dumps({
465486
"job_id": job_id,
@@ -477,19 +498,13 @@ def pay_job(self) -> Function:
477498
description="The job ID you are paying for",
478499
)
479500

480-
amount_arg = Argument(
481-
name="amount",
482-
type="float",
483-
description="The total amount to pay", # in Ether
484-
)
485-
486501
reasoning_arg = Argument(
487502
name="reasoning",
488503
type="string",
489504
description="Why you are making this payment",
490505
)
491506

492-
args = [job_id_arg, amount_arg, reasoning_arg]
507+
args = [job_id_arg, reasoning_arg]
493508

494509
if hasattr(self, 'twitter_plugin') and self.twitter_plugin is not None:
495510
tweet_content_arg = Argument(
@@ -506,46 +521,30 @@ def pay_job(self) -> Function:
506521
executable=self._pay_job_executable
507522
)
508523

509-
def _pay_job_executable(self, job_id: int, amount: float, reasoning: str, tweet_content: Optional[str] = None) -> Tuple[FunctionResultStatus, str, dict]:
524+
def _pay_job_executable(self, job_id: int, reasoning: str, tweet_content: Optional[str] = None) -> Tuple[FunctionResultStatus, str, dict]:
510525
if not job_id:
511526
return FunctionResultStatus.FAILED, "Missing job ID - specify which job you're paying for", {}
512527

513-
if not amount:
514-
return FunctionResultStatus.FAILED, "Missing amount - specify how much you're paying", {}
515-
516528
if not reasoning:
517529
return FunctionResultStatus.FAILED, "Missing reasoning - explain why you're making this payment", {}
518530

519531
try:
520-
state = self.get_acp_state()
521-
522-
job = next(
523-
(c for c in state["jobs"]["active"]["as_a_buyer"] if c["job_id"] == job_id),
524-
None
525-
)
532+
job = self.acp_client.get_job_by_onchain_id(job_id)
526533

527534
if not job:
528535
return FunctionResultStatus.FAILED, "Job not found in your buyer jobs - check the ID and verify you're the buyer", {}
529536

530-
if job["phase"] != AcpJobPhasesDesc.NEGOTIATION:
531-
return FunctionResultStatus.FAILED, f"Cannot pay - job is in '{job['phase']}' phase, must be in 'negotiation' phase", {}
537+
if job.phase != ACPJobPhase.NEGOTIATION:
538+
return FunctionResultStatus.FAILED, f"Cannot pay - job is in '{ACP_JOB_PHASE_MAP.get(job.phase)}' phase, must be in 'negotiation' phase", {}
532539

533-
534-
self.acp_client.pay_for_job(
535-
job_id,
536-
job["memo"][0]["id"],
537-
amount,
538-
reasoning
539-
)
540+
job.pay(job.price, reasoning)
540541

541542
if hasattr(self, 'twitter_plugin') and self.twitter_plugin is not None and tweet_content is not None:
542-
tweet_id = job.get("tweet_history", [])[0].get("tweet_id") if job.get("tweet_history") else None
543-
if tweet_id:
544-
self._tweet_job(job_id, tweet_content, tweet_id)
543+
self._tweet_job(job, tweet_content)
545544

546545
return FunctionResultStatus.DONE, json.dumps({
547546
"job_id": job_id,
548-
"amount_paid": amount,
547+
"amount_paid": job.price,
549548
"timestamp": datetime.now().timestamp()
550549
}), {}
551550
except Exception as e:
@@ -578,7 +577,7 @@ def deliver_job(self) -> Function:
578577

579578
return Function(
580579
fn_name="deliver_job",
581-
fn_description="Completes a sale by delivering items to the buyer",
580+
fn_description="Deliver the requested result to the client. Use this function after producing the deliverable",
582581
args=args,
583582
executable=self._deliver_job_executable
584583
)
@@ -591,21 +590,16 @@ def _deliver_job_executable(self, job_id: int, reasoning: str, tweet_content: Op
591590
return FunctionResultStatus.FAILED, "Missing reasoning - explain why you're making this delivery", {}
592591

593592
try:
594-
state = self.get_acp_state()
595-
596-
job = next(
597-
(c for c in state["jobs"]["active"]["as_a_seller"] if c["job_id"] == job_id),
598-
None
599-
)
593+
job = self.acp_client.get_job_by_onchain_id(job_id)
600594

601595
if not job:
602596
return FunctionResultStatus.FAILED, "Job not found in your seller jobs - check the ID and verify you're the seller", {}
603597

604-
if job["phase"] != AcpJobPhasesDesc.TRANSACTION:
605-
return FunctionResultStatus.FAILED, f"Cannot deliver - job is in '{job['phase']}' phase, must be in 'transaction' phase", {}
598+
if job.phase != ACPJobPhase.TRANSACTION:
599+
return FunctionResultStatus.FAILED, f"Cannot deliver - job is in '{ACP_JOB_PHASE_MAP.get(job.phase)}' phase, must be in 'transaction' phase", {}
606600

607601
produced = next(
608-
(i for i in self.produced_inventory if i.job_id == job["job_id"]),
602+
(i for i in self.produced_inventory if i.job_id == job.id),
609603
None
610604
)
611605

@@ -617,15 +611,10 @@ def _deliver_job_executable(self, job_id: int, reasoning: str, tweet_content: Op
617611
value=produced.value
618612
)
619613

620-
self.acp_client.submit_job_deliverable(
621-
job_id,
622-
deliverable,
623-
)
614+
job.deliver(deliverable)
624615

625616
if hasattr(self, 'twitter_plugin') and self.twitter_plugin is not None and tweet_content is not None:
626-
tweet_id = job.get("tweet_history", [])[0].get("tweet_id") if job.get("tweet_history") else None
627-
if tweet_id:
628-
self._tweet_job(job_id, tweet_content, tweet_id)
617+
self._tweet_job(job, tweet_content)
629618

630619
return FunctionResultStatus.DONE, json.dumps({
631620
"status": "success",
@@ -637,46 +626,40 @@ def _deliver_job_executable(self, job_id: int, reasoning: str, tweet_content: Op
637626
print(traceback.format_exc())
638627
return FunctionResultStatus.FAILED, f"System error while delivering items - try again after a short delay. {str(e)}", {}
639628

640-
def _tweet_job(self, job_id: int, content: str, tweet_id: Optional[str] = None):
641-
if not hasattr(self, 'twitter_plugin') or self.twitter_plugin is None:
642-
return
643-
644-
job = self.acp_client.get_job_by_onchain_id(job_id)
645-
if not job:
646-
raise Exception("ERROR (tweetJob): Job not found")
647-
648-
649-
if tweet_id :
629+
def _tweet_job(self, job: ACPJob, tweet_content: str):
630+
tweets = job.context.get("tweets", []) if job.context else []
631+
if tweets:
632+
latest_tweet = max(tweets, key=lambda t: t["createdAt"])
633+
tweet_id = latest_tweet.get("tweetId", None)
650634
response = self.twitter_plugin.twitter_client.create_tweet(
651-
text=content,
635+
text=tweet_content,
652636
in_reply_to_tweet_id=tweet_id
653637
)
654638
else:
655-
response = self.twitter_plugin.twitter_client.create_tweet(text=content)
656-
639+
response = self.twitter_plugin.twitter_client.create_tweet(text=tweet_content)
657640

658641
role = "buyer" if job.client_address.lower() == self.acp_client.agent_address.lower() else "seller"
659642

660643
# Safely extract tweet ID
661644
tweet_id = None
662645
if isinstance(response, dict):
663646
tweet_id = response.get('data', {}).get('id') or response.get('id')
664-
647+
665648
context = {
666649
**(job.context or {}),
667650
'tweets': [
668651
*((job.context or {}).get('tweets', [])),
669652
{
670653
'type': role,
671654
'tweetId': tweet_id,
672-
'content': content,
655+
'content': tweet_content,
673656
'createdAt': int(datetime.now().timestamp() * 1000)
674657
},
675658
],
676659
}
677660

678661
response = requests.patch(
679-
f"{self.acp_base_url}/jobs/{job_id}/context",
662+
f"{self.acp_base_url}/jobs/{job.id}/context",
680663
headers={
681664
"Content-Type": "application/json",
682665
"wallet-address": self.acp_client.agent_address,

0 commit comments

Comments
 (0)