From c986cdeaa9b59b57d58224a7817aad6397f6d2c0 Mon Sep 17 00:00:00 2001 From: StevenSF1998 Date: Mon, 14 Apr 2025 14:36:04 +0800 Subject: [PATCH 1/5] implement evaluator flow --- plugins/acp/README.md | 6 ++- plugins/acp/acp_plugin_gamesdk/acp_client.py | 7 +-- plugins/acp/acp_plugin_gamesdk/acp_plugin.py | 46 +++++++++++++++++--- plugins/acp/examples/README.md | 2 + plugins/acp/examples/test_buyer.py | 9 ++-- plugins/acp/examples/test_seller.py | 8 ++-- 6 files changed, 59 insertions(+), 19 deletions(-) diff --git a/plugins/acp/README.md b/plugins/acp/README.md index ee808c65..3ee21ded 100644 --- a/plugins/acp/README.md +++ b/plugins/acp/README.md @@ -85,8 +85,10 @@ acp_plugin = AcpPlugin( "", "" ), + cluster = "", twitter_plugin = "", - on_evaluate = "" # will initialize socket connection for real-time communication + evaluator_cluster = "", + on_evaluate = "" ) ) ``` @@ -152,7 +154,7 @@ acp_plugin = AcpPlugin( "", "" ), - twitter_plugin=GameTwitterPlugin(twitter_client_options), + evaluator_cluster = "", on_evaluate = on_evaluate # <--- This is the on_evaluate function ) ) diff --git a/plugins/acp/acp_plugin_gamesdk/acp_client.py b/plugins/acp/acp_plugin_gamesdk/acp_client.py index f1d86bd2..eb8c48e4 100644 --- a/plugins/acp/acp_plugin_gamesdk/acp_client.py +++ b/plugins/acp/acp_plugin_gamesdk/acp_client.py @@ -64,11 +64,12 @@ def browse_agents(self, cluster: Optional[str] = None, query: Optional[str] = No return result - def create_job(self, provider_address: str, price: float, job_description: str) -> int: + def create_job(self, provider_address: str, price: float, job_description: str, evaluator_address: str) -> int: expire_at = datetime.now() + timedelta(days=1) + tx_result = self.acp_token.create_job( provider_address=provider_address, - evaluator_address=self.agent_wallet_address, + evaluator_address=evaluator_address, expire_at=expire_at ) @@ -121,7 +122,7 @@ def create_job(self, provider_address: str, price: float, job_description: str) "description": job_description, "price": price, "expiredAt": expire_at.isoformat(), - "evaluatorAddress": self.agent_wallet_address + "evaluatorAddress": evaluator_address } requests.post( diff --git a/plugins/acp/acp_plugin_gamesdk/acp_plugin.py b/plugins/acp/acp_plugin_gamesdk/acp_plugin.py index 34bc4e1b..0c48cb5a 100644 --- a/plugins/acp/acp_plugin_gamesdk/acp_plugin.py +++ b/plugins/acp/acp_plugin_gamesdk/acp_plugin.py @@ -23,6 +23,7 @@ class AcpPluginOptions: acp_token_client: AcpToken twitter_plugin: TwitterPlugin | GameTwitterPlugin = None cluster: Optional[str] = None + evaluator_cluster: Optional[str] = None on_evaluate: Optional[Callable[[IDeliverable], Tuple[bool, str]]] = None SocketEvents = { @@ -55,6 +56,7 @@ def __init__(self, options: AcpPluginOptions): NOTE: This is NOT for finding clients - only for executing trades when there's a specific need to buy or sell something. """ self.cluster = options.cluster + self.evaluator_cluster = options.evaluator_cluster self.twitter_plugin = options.twitter_plugin self.produced_inventory: List[IInventory] = [] self.acp_base_url = self.acp_token_client.acp_base_url if self.acp_token_client.acp_base_url is None else "https://acpx-staging.virtuals.io/api" @@ -238,8 +240,20 @@ def initiate_job(self) -> Function: type="string", description="Detailed specifications for service-based items", ) + + require_evaluation_arg = Argument( + name="requireEvaluation", + type="boolean", + description="Whether to require a evaluator to verify the deliverable", + ) + + evaluator_keyword_arg = Argument( + name="evaluatorKeyword", + type="string", + description="Keyword to search for a evaluator", + ) - args = [seller_wallet_address_arg, price_arg, reasoning_arg, service_requirements_arg] + args = [seller_wallet_address_arg, price_arg, reasoning_arg, service_requirements_arg, require_evaluation_arg, evaluator_keyword_arg] if self.twitter_plugin is not None: tweet_content_arg = Argument( @@ -248,7 +262,7 @@ def initiate_job(self) -> Function: description="Tweet content that will be posted about this job. Must include the seller's Twitter handle (with @ symbol) to notify them", ) args.append(tweet_content_arg) - + return Function( fn_name="initiate_job", 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.", @@ -256,21 +270,41 @@ def initiate_job(self) -> Function: executable=self._initiate_job_executable ) - def _initiate_job_executable(self, sellerWalletAddress: str, price: str, reasoning: str, serviceRequirements: str, tweetContent : Optional[str] = None) -> Tuple[FunctionResultStatus, str, dict]: + def _initiate_job_executable(self, sellerWalletAddress: str, price: str, reasoning: str, serviceRequirements: str, requireEvaluation: bool, evaluatorKeyword: str, tweetContent: Optional[str] = None) -> Tuple[FunctionResultStatus, str, dict]: if not price: return FunctionResultStatus.FAILED, "Missing price - specify how much you're offering per unit", {} - + + if not reasoning: + return FunctionResultStatus.FAILED, "Missing reasoning - explain why you're making this purchase request", {} + try: state = self.get_acp_state() if state["jobs"]["active"]["asABuyer"]: return FunctionResultStatus.FAILED, "You already have an active job as a buyer", {} - + + if not sellerWalletAddress: + return FunctionResultStatus.FAILED, "Missing seller wallet address - specify the agent you want to buy from", {} + + if bool(requireEvaluation) and not evaluatorKeyword: + return FunctionResultStatus.FAILED, "Missing validator keyword - provide a keyword to search for a validator", {} + + evaluatorAddress = self.acp_token_client.get_agent_wallet_address() + + if bool(requireEvaluation): + validators = self.acp_client.browse_agents(self.evaluator_cluster, evaluatorKeyword) + + if len(validators) == 0: + return FunctionResultStatus.FAILED, "No evaluator found - try a different keyword", {} + + evaluatorAddress = validators[0].wallet_address + # ... Rest of validation logic ... job_id = self.acp_client.create_job( sellerWalletAddress, float(price), - serviceRequirements + serviceRequirements, + evaluatorAddress ) if (self.twitter_plugin is not None and tweetContent is not None): diff --git a/plugins/acp/examples/README.md b/plugins/acp/examples/README.md index c647af57..9ac54653 100644 --- a/plugins/acp/examples/README.md +++ b/plugins/acp/examples/README.md @@ -34,8 +34,10 @@ acp_plugin = AcpPlugin( "", "" ), + cluster = "", twitter_plugin = "", on_evaluate = "" # will initialize socket connection for real-time communication + evaluator_cluster = "" ) ) ``` diff --git a/plugins/acp/examples/test_buyer.py b/plugins/acp/examples/test_buyer.py index 1883f248..80bb3cba 100644 --- a/plugins/acp/examples/test_buyer.py +++ b/plugins/acp/examples/test_buyer.py @@ -49,18 +49,19 @@ def main(): "https://base-sepolia-rpc.publicnode.com/", # RPC "https://acpx-staging.virtuals.io/api" ), - acp_base_url="https://acpx-staging.virtuals.io/api", twitter_plugin=GameTwitterPlugin(options), - on_evaluate=on_evaluate # will initialize socket connection for real-time communication + on_evaluate=on_evaluate ) ) # Native Twitter Plugin # acp_plugin = AcpPlugin( # options=AdNetworkPluginOptions( - # api_key="xxx", + # api_key=os.environ.get("GAME_DEV_API_KEY"), # acp_token_client=AcpToken( - # "xxx", + # os.environ.get("ACP_TOKEN_BUYER"), + # os.environ.get("ACP_AGENT_WALLET_ADDRESS_BUYER"), # "https://base-sepolia-rpc.publicnode.com/" # RPC + # "https://acpx-staging.virtuals.io/api" # ), # twitter_plugin=TwitterPlugin(options) # ) diff --git a/plugins/acp/examples/test_seller.py b/plugins/acp/examples/test_seller.py index e17da1f0..562f7530 100644 --- a/plugins/acp/examples/test_seller.py +++ b/plugins/acp/examples/test_seller.py @@ -45,18 +45,18 @@ def test(): "https://base-sepolia-rpc.publicnode.com/", # Assuming this is the chain identifier "https://acpx-staging.virtuals.io/api" ), - acp_base_url="https://acpx-staging.virtuals.io/api", twitter_plugin=GameTwitterPlugin(options) ) ) # Native Twitter Plugin # acp_plugin = AcpPlugin( # options=AdNetworkPluginOptions( - # api_key="xxx", + # api_key=os.environ.get("GAME_DEV_API_KEY"), # acp_token_client=AcpToken( - # "xxx", + # os.environ.get("ACP_TOKEN_SELLER"), # os.environ.get("ACP_AGENT_WALLET_ADDRESS_SELLER"), - # "https://base-sepolia-rpc.publicnode.com/" # Assuming this is the chain identifier + # "https://base-sepolia-rpc.publicnode.com/" , + # "https://acpx-staging.virtuals.io/api" # ), # twitter_plugin=TwitterPlugin(options) # ) From 67bd91b7114a7858964ed50ad1c95b0511f76ac0 Mon Sep 17 00:00:00 2001 From: StevenSF1998 Date: Mon, 14 Apr 2025 15:32:30 +0800 Subject: [PATCH 2/5] adjust read me --- plugins/acp/examples/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/acp/examples/README.md b/plugins/acp/examples/README.md index 9ac54653..c03d50e9 100644 --- a/plugins/acp/examples/README.md +++ b/plugins/acp/examples/README.md @@ -62,8 +62,8 @@ acp_plugin = AcpPlugin( "", "" ), + cluster = "", twitter_plugin = "", - on_evaluate = "" # will initialize socket connection for real-time communication ) ) ``` From ceb23adca84ee047eb53222152b581c978e78079 Mon Sep 17 00:00:00 2001 From: StevenSF1998 Date: Mon, 14 Apr 2025 16:10:20 +0800 Subject: [PATCH 3/5] readme - Cluster --- plugins/acp/examples/README.md | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/plugins/acp/examples/README.md b/plugins/acp/examples/README.md index c03d50e9..5021cdde 100644 --- a/plugins/acp/examples/README.md +++ b/plugins/acp/examples/README.md @@ -11,7 +11,7 @@ In this example, we have two agents: ## Prerequisite -⚠️ Important: Before testing your agent’s services with a counterpart agent, you must register your agent with the [Service Registry](https://acp-staging.virtuals.io/). +⚠️ Important: Before testing your agent's services with a counterpart agent, you must register your agent with the [Service Registry](https://acp-staging.virtuals.io/). This step is a critical precursor. Without registration, the counterpart agent will not be able to discover or interact with your agent. ## Buyer Example @@ -148,6 +148,22 @@ def on_evaluate(deliverable: IDeliverable) -> Tuple[bool, str]: return True, "Default evaluation" ``` +## Understanding Clusters + +Clusters in ACP are categories that group agents together based on their functionality or domain: + +- **cluster**: Specifies the category your agent belongs to, making it easier for other agents to discover and interact with services in the same domain. +- **evaluator_cluster**: A specialized type of cluster specifically for agents that evaluate jobs generated by AI. These evaluator agents provide quality control and verification services. + +Clusters help with: + +- Organizing agents by their specialization +- Improving service discovery efficiency +- Creating ecosystems of complementary agents +- Enabling targeted searches for specific capabilities + +When configuring your agent, choose clusters that accurately represent your agent's capabilities to ensure it can be found by the right counterparts. + ## Note - Make sure to replace placeholder API keys and private keys with your own From b9d6c12c97b4d37c4f053d06b9d24974ae7bb2aa Mon Sep 17 00:00:00 2001 From: StevenSF1998 Date: Mon, 14 Apr 2025 17:54:31 +0800 Subject: [PATCH 4/5] adjust require_evaluation_arg desc --- plugins/acp/acp_plugin_gamesdk/acp_plugin.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/acp/acp_plugin_gamesdk/acp_plugin.py b/plugins/acp/acp_plugin_gamesdk/acp_plugin.py index 0c48cb5a..b16bd9b7 100644 --- a/plugins/acp/acp_plugin_gamesdk/acp_plugin.py +++ b/plugins/acp/acp_plugin_gamesdk/acp_plugin.py @@ -244,7 +244,7 @@ def initiate_job(self) -> Function: require_evaluation_arg = Argument( name="requireEvaluation", type="boolean", - description="Whether to require a evaluator to verify the deliverable", + 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.", ) evaluator_keyword_arg = Argument( From 75dfe901384c8b67835369bc7978dd4dabbec6b6 Mon Sep 17 00:00:00 2001 From: StevenSF1998 Date: Mon, 14 Apr 2025 18:09:03 +0800 Subject: [PATCH 5/5] clean up prints log --- plugins/acp/acp_plugin_gamesdk/acp_plugin.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/plugins/acp/acp_plugin_gamesdk/acp_plugin.py b/plugins/acp/acp_plugin_gamesdk/acp_plugin.py index b16bd9b7..77facf26 100644 --- a/plugins/acp/acp_plugin_gamesdk/acp_plugin.py +++ b/plugins/acp/acp_plugin_gamesdk/acp_plugin.py @@ -84,9 +84,7 @@ def initializeSocket(self) -> Tuple[bool, str]: self.socket.connect("https://sdk-dev.game.virtuals.io", auth=self.socket.auth) if (self.socket.connected): - print("Connecting socket") self.socket.emit(SocketEvents["JOIN_EVALUATOR_ROOM"], self.acp_token_client.agent_wallet_address) - print(f"Joined evaluator room with address: {self.acp_token_client.agent_wallet_address}") # Set up event handler for evaluation requests @@ -104,7 +102,6 @@ def on_evaluate(data): def cleanup(): if self.socket: print("Disconnecting socket") - self.socket.emit("leaveEvaluatorRoom", self.acp_token_client.agent_wallet_address, callback=lambda: print("Successfully left evaluator room")) import time time.sleep(1)