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..77facf26 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" @@ -82,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 @@ -102,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) @@ -238,8 +237,20 @@ def initiate_job(self) -> Function: type="string", description="Detailed specifications for service-based items", ) + + require_evaluation_arg = Argument( + name="requireEvaluation", + type="boolean", + 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( + 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 +259,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 +267,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..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 @@ -34,8 +34,10 @@ acp_plugin = AcpPlugin( "", "" ), + cluster = "", twitter_plugin = "", on_evaluate = "" # will initialize socket connection for real-time communication + evaluator_cluster = "" ) ) ``` @@ -60,8 +62,8 @@ acp_plugin = AcpPlugin( "", "" ), + cluster = "", twitter_plugin = "", - on_evaluate = "" # will initialize socket connection for real-time communication ) ) ``` @@ -146,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 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) # )