1+ import ast
12import json
23import traceback
4+ from contextlib import suppress
35from dataclasses import dataclass
46from 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
79import requests
810
911from game_sdk .game .agent import WorkerConfig
1012from game_sdk .game .custom_types import Argument , Function , FunctionResultStatus
1113from twitter_plugin_gamesdk .twitter_plugin import TwitterPlugin
1214from virtuals_acp import IDeliverable
13- from virtuals_acp .models import ACPGraduationStatus , ACPOnlineStatus
15+ from virtuals_acp .models import ACPGraduationStatus , ACPOnlineStatus , ACPJobPhase
1416
1517from acp_plugin_gamesdk .interface import AcpJobPhasesDesc , IInventory , ACP_JOB_PHASE_MAP
1618from 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