From 3e35bf9e163c5a9425fa011b3a62ec5beedd91f4 Mon Sep 17 00:00:00 2001 From: StevenSF1998 Date: Fri, 21 Mar 2025 17:39:27 +0800 Subject: [PATCH 001/108] acp v1 --- plugins/acp/README.md | 145 ++++ plugins/acp/__init__.py | 0 plugins/acp/acp_plugin_gamesdk/acp_client.py | 124 ++++ plugins/acp/acp_plugin_gamesdk/acp_plugin.py | 499 +++++++++++++ plugins/acp/acp_plugin_gamesdk/acp_token.py | 288 ++++++++ .../acp/acp_plugin_gamesdk/acp_token_abi.py | 690 ++++++++++++++++++ plugins/acp/acp_plugin_gamesdk/interface.py | 70 ++ plugins/acp/examples/test_buyer.py | 89 +++ plugins/acp/examples/test_seller.py | 111 +++ plugins/acp/pyproject.toml | 20 + 10 files changed, 2036 insertions(+) create mode 100644 plugins/acp/README.md create mode 100644 plugins/acp/__init__.py create mode 100644 plugins/acp/acp_plugin_gamesdk/acp_client.py create mode 100644 plugins/acp/acp_plugin_gamesdk/acp_plugin.py create mode 100644 plugins/acp/acp_plugin_gamesdk/acp_token.py create mode 100644 plugins/acp/acp_plugin_gamesdk/acp_token_abi.py create mode 100644 plugins/acp/acp_plugin_gamesdk/interface.py create mode 100644 plugins/acp/examples/test_buyer.py create mode 100644 plugins/acp/examples/test_seller.py create mode 100644 plugins/acp/pyproject.toml diff --git a/plugins/acp/README.md b/plugins/acp/README.md new file mode 100644 index 00000000..8b1c2385 --- /dev/null +++ b/plugins/acp/README.md @@ -0,0 +1,145 @@ +# ACP Plugin for GAME SDK + +## Overview + +pip install web3 requests eth-account eth-typing + +The **Telegram Plugin** is an integration for the **Game SDK** that enables AI-driven interactions on Telegram. This plugin allows AI agents to handle messages, execute commands, and engage users through text, media, and polls. + +## Features + +- **Send Messages** – AI agents can send text messages to users. +- **Send Media** – Supports sending photos, documents, videos, and audio. +- **Create Polls** – AI agents can generate interactive polls. +- **Pin & Unpin Messages** – Manage pinned messages in chats. +- **Delete Messages** – Remove messages dynamically. +- **AI-Powered Responses** – Leverages LLM to generate contextual replies. +- **Real-Time Polling** – Runs asynchronously with Telegram’s polling system. +- and more features to come! + +## Installation + +### Pre-requisites + +Ensure you have Python 3.9+ installed. Then, install the plugin via **PyPI**: + +### Steps + +1. Install the plugin: + ```sh bash + pip install telegram-plugin-gamesdk + ``` +2. Ensure you have a Telegram bot token and GAME API key and set them as environment variables: + ```sh bash + export TELEGRAM_BOT_TOKEN="your-telegram-bot-token" + export GAME_API_KEY="your-game-api-key" + ``` +3. Refer to the example and run the example bot: + ```sh bash + python examples/test_telegram.py + ``` + +## Usage Examples + +### Initializing the Plugin + +```python +from telegram_plugin_gamesdk.telegram_plugin import TelegramPlugin + +tg_bot = TelegramPlugin(bot_token='your-telegram-bot-token') +tg_bot.start_polling() +``` + +### Sending a Message + +```python +tg_bot.send_message(chat_id=123456789, text="Hello from the AI Agent!") +``` + +### Sending Media + +```python +tg_bot.send_media(chat_id=123456789, media_type="photo", media="https://example.com/image.jpg", caption="Check this out!") +``` + +### Creating a Poll + +```python +tg_bot.create_poll(chat_id=123456789, question="What's your favorite color?", options=["Red", "Blue", "Green"]) +``` + +### Pinning and Unpinning Messages + +```python +tg_bot.pin_message(chat_id=123456789, message_id=42) +tg_bot.unpin_message(chat_id=123456789, message_id=42) +``` + +### Deleting a Message + +```python +tg_bot.delete_message(chat_id=123456789, message_id=42) +``` + +## Integration with GAME Chat Agent + +Implement a message handler to integrate the Telegram Plugin with the GAME Chat Agent: + +```python +from telegram import Update +from telegram.ext import ContextTypes, filters, MessageHandler +from game_sdk.game.chat_agent import ChatAgent + +chat_agent = ChatAgent( + prompt="You are a helpful assistant.", + api_key="your-game-api-key", +) + +async def default_message_handler(update: Update, context: ContextTypes.DEFAULT_TYPE): + """Handles incoming messages but ignores messages from the bot itself unless it's mentioned in a group chat.""" + # Ignore messages from the bot itself + if update.message.from_user.id == tg_plugin.bot.id: + logger.info("Ignoring bot's own message.") + return + + user = update.message.from_user + chat_id = update.message.chat.id + chat_type = update.message.chat.type # "private", "group", "supergroup", or "channel" + bot_username = f"@{tg_plugin.bot.username}" + + logger.info(f"Update received: {update}") + logger.info(f"Message received: {update.message.text}") + + name = f"{user.first_name} (Telegram's chat_id: {chat_id}, this is not part of the partner's name but important for the telegram's function arguments)" + + if not any(u["chat_id"] == chat_id for u in active_users): + # Ignore group/supergroup messages unless the bot is mentioned + if chat_type in ["group", "supergroup"] and bot_username not in update.message.text: + logger.info(f"Ignoring group message not mentioning the bot: {update.message.text}") + return + active_users.append({"chat_id": chat_id, "name": name}) + logger.info(f"Active user added: {name}") + logger.info(f"Active users: {active_users}") + chat = chat_agent.create_chat( + partner_id=str(chat_id), + partner_name=name, + action_space=agent_action_space, + ) + active_chats[chat_id] = chat + + response = active_chats[chat_id].next(update.message.text.replace(bot_username, "").strip()) # Remove bot mention + logger.info(f"Response: {response}") + + if response.message: + await update.message.reply_text(response.message) + + if response.is_finished: + active_chats.pop(chat_id) + active_users.remove({"chat_id": chat_id, "name": name}) + logger.info(f"Chat with {name} ended.") + logger.info(f"Active users: {active_users}") + +tg_plugin.add_handler(MessageHandler(filters.ALL, default_message_handler)) +``` + +You can refer to [test_telegram.py](examples/test_telegram.py) for details. diff --git a/plugins/acp/__init__.py b/plugins/acp/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/plugins/acp/acp_plugin_gamesdk/acp_client.py b/plugins/acp/acp_plugin_gamesdk/acp_client.py new file mode 100644 index 00000000..713ca813 --- /dev/null +++ b/plugins/acp/acp_plugin_gamesdk/acp_client.py @@ -0,0 +1,124 @@ +from datetime import datetime, timedelta +from typing import List, Optional +from web3 import Web3 +import requests + +import sys +import os +sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), "../../../"))) +from .interface import AcpAgent, AcpJobPhases, AcpState +from .acp_token import AcpToken, MemoType + +class AcpClient: + def __init__(self, api_key: str, acp_token: AcpToken): + self.base_url = "https://sdk-dev.game.virtuals.io/acp" + self.api_key = api_key + self.acp_token = acp_token + self.web3 = Web3() + + @property + def wallet_address(self) -> str: + return self.acp_token.get_wallet_address() + + def get_state(self) -> AcpState: + response = requests.get( + f"{self.base_url}/states/{self.wallet_address}", + headers={"x-api-key": self.api_key} + ) + return response.json() + + async def browse_agents(self, query: str) -> List[AcpAgent]: + response = requests.get( + f"https://acpx.virtuals.gg/wp-json/wp/v2/agents", + params={"search": query} + ) + + if (response.status_code != 200): + raise Exception(f"Failed to browse agents: {response.json()}") + + return response.json() + + async def create_job(self, provider_address: str, price: float, job_description: str) -> int: + expired_at = datetime.now() + timedelta(days=1) + + tx_result = self.acp_token.create_job( + provider_address=provider_address, + expired_at=expired_at + ) + job_id = tx_result["job_id"] + + memo_response = self.acp_token.create_memo( + job_id=job_id, + content=job_description, + memo_type=MemoType.MESSAGE, + is_private=False, + phase=AcpJobPhases.NEGOTIOATION + ) + + payload = { + "jobId": job_id, + "clientAddress": self.acp_token.get_wallet_address(), + "providerAddress": provider_address, + "description": job_description, + "price": price, + "expiredAt": expired_at.isoformat() + } + + requests.post( + self.base_url, + json=payload, + headers={ + "Accept": "application/json", + "Content-Type": "application/json", + "x-api-key": self.api_key + } + ) + + return job_id + + async def response_job(self, job_id: int, accept: bool, memo_id: int, reasoning: str): + if accept: + tx_hash = await self.acp_token.sign_memo(memo_id, accept, reasoning) + + return await self.acp_token.create_memo( + job_id=job_id, + content=f"Job {job_id} accepted. {reasoning}", + memo_type=MemoType.MESSAGE, + is_private=False, + phase=AcpJobPhases.TRANSACTION + ) + else: + return await self.acp_token.create_memo( + job_id=job_id, + content=f"Job {job_id} rejected. {reasoning}", + memo_type=MemoType.MESSAGE, + is_private=False, + phase=AcpJobPhases.REJECTED + ) + + async def make_payment(self, job_id: int, amount: float, memo_id: int, reason: str): + # Convert amount to Wei (smallest ETH unit) + amount_wei = self.web3.to_wei(amount, 'ether') + + tx_hash = await self.acp_token.set_budget(job_id, amount_wei) + approval_tx_hash = await self.acp_token.approve_allowance(amount_wei) + signed_memo_tx_hash = await self.acp_token.sign_memo(memo_id, True, reason) + + return await self.acp_token.create_memo( + job_id=job_id, + content=f"Payment of {amount} made. {reason}", + memo_type=MemoType.MESSAGE, + is_private=False, + phase=AcpJobPhases.EVALUATION + ) + + async def deliver_job(self, job_id: int, deliverable: str, memo_id: int, reason: str): + tx_hash = await self.acp_token.sign_memo(memo_id, True, reason) + + return await self.acp_token.create_memo( + job_id=job_id, + content=deliverable, + memo_type=MemoType.MESSAGE, + is_private=False, + phase=AcpJobPhases.COMPLETED + ) diff --git a/plugins/acp/acp_plugin_gamesdk/acp_plugin.py b/plugins/acp/acp_plugin_gamesdk/acp_plugin.py new file mode 100644 index 00000000..7f7228be --- /dev/null +++ b/plugins/acp/acp_plugin_gamesdk/acp_plugin.py @@ -0,0 +1,499 @@ +import json +from typing import List, Dict, Any, Optional +from dataclasses import dataclass +from datetime import datetime + +from game_sdk.game.agent import WorkerConfig +from game_sdk.game.custom_types import Function, FunctionResult, FunctionResultStatus + +import sys +import os +sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), "../../../"))) +from .acp_client import AcpClient +from .acp_token import AcpToken +from .interface import AcpJobPhasesDesc, IInventory + +@dataclass +class AdNetworkPluginOptions: + api_key: str + acp_token_client: AcpToken + +class AcpPlugin: + def __init__(self, options: AdNetworkPluginOptions): + self.acp_client = AcpClient(options.api_key, options.acp_token_client) + + self.id = "acp_worker" + self.name = "ACP Worker" + self.description = """ + Handles trading transactions and jobs between agents. This worker ONLY manages: + + 1. RESPONDING to Buy/Sell Needs + - Find sellers when YOU need to buy something + - Handle incoming purchase requests when others want to buy from YOU + - NO prospecting or client finding + + 2. Job Management + - Process purchase requests. Accept or reject job. + - Send payments + - Manage and deliver services and goods + + NOTE: This is NOT for finding clients - only for executing trades when there's a specific need to buy or sell something. + """ + + self.produced_inventory: List[IInventory] = [] + + def add_produce_item(self, item: IInventory) -> None: + self.produced_inventory.append(item) + + async def get_acp_state(self) -> Dict: + server_state = await self.acp_client.get_state() + server_state["inventory"]["produced"] = self.produced_inventory + return server_state + + async def get_worker(self, data: Optional[Dict] = None) -> WorkerConfig: + functions = data.get("functions") if data else [ + self.search_agents_functions, + self.initiate_job, + self.respond_job, + self.pay_job, + self.deliver_job, + ] + + async def get_environment(_e, __) -> Dict[str, Any]: + environment = await data.get_environment() if hasattr(data, "get_environment") else {} + return { + **environment, + **(await self.get_acp_state()), + } + + data = await WorkerConfig.create_async( + id=self.id, + worker_description=self.description, + action_space=functions, + get_state_fn=get_environment, + instruction=data.get("instructions") if data else None + ) + + # print(json.dumps(vars(data), indent=2, default=str)) + return data + + @property + def agent_description(self) -> str: + return """ + Inventory structure + - inventory.aquired: Deliverable that your have bought and can be use to achived your objective + - inventory.produced: Deliverable that needs to be delivered to your seller + + Job Structure: + - jobs.active: + * asABuyer: Pending resource purchases + * asASeller: Pending design requests + - jobs.completed: Successfully fulfilled projects + - jobs.cancelled: Terminated or rejected requests + - Each job tracks: + * 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 + """ + + @property + def search_agents_functions(self) -> Function: + return Function( + fn_name="search_agents", + 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.", + args=[ + { + "name": "reasoning", + "type": "string", + "description": "Explain why you need to find trading partners at this time", + }, + { + "name": "keyword", + "type": "string", + "description": "A one word description of the work to be DONE", + }, + ], + executable=self._search_agents_executable + ) + + async def _search_agents_executable(self, args: Dict, _: Any) -> FunctionResult: + if not args.get("reasoning"): + return FunctionResult( + FunctionResultStatus.FAILED, + "Reasoning for the search must be provided. This helps track your decision-making process for future reference." + ) + + try: + if not args.get("keyword"): + return FunctionResult( + FunctionResultStatus.FAILED, + "Keyword is required to search" + ) + + available_agents = await self.acp_client.browse_agents(args["keyword"]) + + if not available_agents: + return FunctionResult( + FunctionResultStatus.FAILED, + "No other trading agents found in the system. Please try again later when more agents are available." + ) + + return FunctionResult( + FunctionResultStatus.DONE, + { + "availableAgents": available_agents, + "totalAgentsFound": len(available_agents), + "timestamp": datetime.now().timestamp(), + "note": "Use the walletAddress when initiating a job with your chosen trading partner.", + } + ) + except Exception as e: + return FunctionResult( + FunctionResultStatus.FAILED, + f"System error while searching for agents - try again after a short delay. {str(e)}" + ) + + @property + def initiate_job(self) -> Function: + 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.", + args=[ + { + "name": "sellerWalletAddress", + "type": "string", + "description": "The seller's agent wallet address you want to buy from", + }, + { + "name": "price", + "type": "string", + "description": "Offered price for service", + }, + { + "name": "reasoning", + "type": "string", + "description": "Why you are making this purchase request", + }, + { + "name": "serviceRequirements", + "type": "string", + "description": "Detailed specifications for service-based items", + }, + ], + executable=self._initiate_job_executable + ) + + async def _initiate_job_executable(self, args: Dict, _: Any) -> FunctionResult: + if not args.get("price"): + return FunctionResult( + FunctionResultStatus.FAILED, + "Missing price - specify how much you're offering per unit" + ) + + try: + state = await self.get_acp_state() + + if state["jobs"]["active"]["asABuyer"]: + return FunctionResult( + FunctionResultStatus.FAILED, + "You already have an active job as a buyer" + ) + + # ... Rest of validation logic ... + + job_id = await self.acp_client.create_job( + args["sellerWalletAddress"], + float(args["price"]), + args["serviceRequirements"] + ) + + return FunctionResult( + FunctionResultStatus.DONE, + { + "jobId": job_id, + "sellerWalletAddress": args["sellerWalletAddress"], + "price": float(args["price"]), + "serviceRequirements": args["serviceRequirements"], + "timestamp": datetime.now().timestamp(), + } + ) + except Exception as e: + return FunctionResult( + FunctionResultStatus.FAILED, + f"System error while initiating job - try again after a short delay. {str(e)}" + ) + + @property + def respond_job(self) -> Function: + return Function( + fn_name="respond_to_job", + fn_description="Accepts or rejects an incoming 'request' job", + args=[ + { + "name": "jobId", + "type": "string", + "description": "The job ID you are responding to", + }, + { + "name": "decision", + "type": "string", + "description": "Your response: 'ACCEPT' or 'REJECT'", + }, + { + "name": "reasoning", + "type": "string", + "description": "Why you made this decision", + }, + ], + executable=self._respond_job_executable + ) + + async def _respond_job_executable(self, args: Dict, _: Any) -> FunctionResult: + if not args.get("jobId"): + return FunctionResult( + FunctionResultStatus.FAILED, + "Missing job ID - specify which job you're responding to" + ) + + if not args.get("decision") or args["decision"] not in ["ACCEPT", "REJECT"]: + return FunctionResult( + FunctionResultStatus.FAILED, + "Invalid decision - must be either 'ACCEPT' or 'REJECT'" + ) + + if not args.get("reasoning"): + return FunctionResult( + FunctionResultStatus.FAILED, + "Missing reasoning - explain why you made this decision" + ) + + try: + state = await self.get_acp_state() + + job = next( + (c for c in state["jobs"]["active"]["asASeller"] if c["jobId"] == int(args["jobId"])), + None + ) + + if not job: + return FunctionResult( + FunctionResultStatus.FAILED, + "Job not found in your seller jobs - check the ID and verify you're the seller" + ) + + if job["phase"] != AcpJobPhasesDesc.REQUEST: + return FunctionResult( + FunctionResultStatus.FAILED, + f"Cannot respond - job is in '{job['phase']}' phase, must be in 'request' phase" + ) + + await self.acp_client.response_job( + int(args["jobId"]), + args["decision"] == "ACCEPT", + job["memo"][0]["id"], + args["reasoning"] + ) + + return FunctionResult( + FunctionResultStatus.DONE, + { + "jobId": args["jobId"], + "decision": args["decision"], + "timestamp": datetime.now().timestamp() + } + ) + except Exception as e: + return FunctionResult( + FunctionResultStatus.FAILED, + f"System error while responding to job - try again after a short delay. {str(e)}" + ) + + @property + def pay_job(self) -> Function: + return Function( + fn_name="pay_job", + fn_description="Processes payment for an accepted purchase request", + args=[ + { + "name": "jobId", + "type": "number", + "description": "The job ID you are paying for", + }, + { + "name": "amount", + "type": "number", + "description": "The total amount to pay", + }, + { + "name": "reasoning", + "type": "string", + "description": "Why you are making this payment", + }, + ], + executable=self._pay_job_executable + ) + + async def _pay_job_executable(self, args: Dict, _: Any) -> FunctionResult: + if not args.get("jobId"): + return FunctionResult( + FunctionResultStatus.FAILED, + "Missing job ID - specify which job you're paying for" + ) + + if not args.get("amount"): + return FunctionResult( + FunctionResultStatus.FAILED, + "Missing amount - specify how much you're paying" + ) + + if not args.get("reasoning"): + return FunctionResult( + FunctionResultStatus.FAILED, + "Missing reasoning - explain why you're making this payment" + ) + + try: + state = await self.get_acp_state() + + job = next( + (c for c in state["jobs"]["active"]["asABuyer"] if c["jobId"] == int(args["jobId"])), + None + ) + + if not job: + return FunctionResult( + FunctionResultStatus.FAILED, + "Job not found in your buyer jobs - check the ID and verify you're the buyer" + ) + + if job["phase"] != AcpJobPhasesDesc.NEGOTIOATION: + return FunctionResult( + FunctionResultStatus.FAILED, + f"Cannot pay - job is in '{job['phase']}' phase, must be in 'negotiation' phase" + ) + + await self.acp_client.make_payment( + int(args["jobId"]), + float(args["amount"]), + job["memo"][0]["id"], + args["reasoning"] + ) + + return FunctionResult( + FunctionResultStatus.DONE, + { + "jobId": args["jobId"], + "amountPaid": args["amount"], + "timestamp": datetime.now().timestamp() + } + ) + except Exception as e: + return FunctionResult( + FunctionResultStatus.FAILED, + f"System error while processing payment - try again after a short delay. {str(e)}" + ) + + @property + def deliver_job(self) -> Function: + return Function( + fn_name="deliver_job", + fn_description="Completes a sale by delivering items to the buyer", + args=[ + { + "name": "jobId", + "type": "string", + "description": "The job ID you are delivering for", + }, + { + "name": "deliverableType", + "type": "string", + "description": "Type of the deliverable", + }, + { + "name": "deliverable", + "type": "string", + "description": "The deliverable item", + }, + { + "name": "reasoning", + "type": "string", + "description": "Why you are making this delivery", + }, + ], + executable=self._deliver_job_executable + ) + + async def _deliver_job_executable(self, args: Dict, _: Any) -> FunctionResult: + if not args.get("jobId"): + return FunctionResult( + FunctionResultStatus.FAILED, + "Missing job ID - specify which job you're delivering for" + ) + + if not args.get("reasoning"): + return FunctionResult( + FunctionResultStatus.FAILED, + "Missing reasoning - explain why you're making this delivery" + ) + + if not args.get("deliverable"): + return FunctionResult( + FunctionResultStatus.FAILED, + "Missing deliverable - specify what you're delivering" + ) + + try: + state = await self.get_acp_state() + + job = next( + (c for c in state["jobs"]["active"]["asASeller"] if c["jobId"] == int(args["jobId"])), + None + ) + + if not job: + return FunctionResult( + FunctionResultStatus.FAILED, + "Job not found in your seller jobs - check the ID and verify you're the seller" + ) + + if job["phase"] != AcpJobPhasesDesc.TRANSACTION: + return FunctionResult( + FunctionResultStatus.FAILED, + f"Cannot deliver - job is in '{job['phase']}' phase, must be in 'transaction' phase" + ) + + produced = next( + (i for i in self.produced_inventory if i["jobId"] == job["jobId"]), + None + ) + + if not produced: + return FunctionResult( + FunctionResultStatus.FAILED, + "Cannot deliver - you should be producing the deliverable first before delivering it" + ) + + deliverable = { + "type": args["deliverableType"], + "value": args["deliverable"] + } + + await self.acp_client.deliver_job( + int(args["jobId"]), + deliverable, + job["memo"][0]["id"], + args["reasoning"] + ) + + return FunctionResult( + FunctionResultStatus.DONE, + { + "status": "success", + "jobId": args["jobId"], + "deliverable": args["deliverable"], + "timestamp": datetime.now().timestamp() + } + ) + except Exception as e: + return FunctionResult( + FunctionResultStatus.FAILED, + f"System error while delivering items - try again after a short delay. {str(e)}" + ) diff --git a/plugins/acp/acp_plugin_gamesdk/acp_token.py b/plugins/acp/acp_plugin_gamesdk/acp_token.py new file mode 100644 index 00000000..a3aab5b6 --- /dev/null +++ b/plugins/acp/acp_plugin_gamesdk/acp_token.py @@ -0,0 +1,288 @@ +import asyncio +from enum import IntEnum +from typing import Optional, Tuple, TypedDict, List +from datetime import datetime +from web3 import Web3 +from eth_account import Account +import sys +import os +sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), "../../../"))) +from .acp_token_abi import ACP_TOKEN_ABI + +class MemoType(IntEnum): + MESSAGE = 0 + CONTEXT_URL = 1 + IMAGE_URL = 2 + VOICE_URL = 3 + OBJECT_URL = 4 + TXHASH = 5 + +class IMemo(TypedDict): + content: str + memoType: MemoType + isSecured: bool + nextPhase: int + jobId: int + numApprovals: int + sender: str + +class IJob(TypedDict): + id: int + client: str + provider: str + budget: int + amountClaimed: int + phase: int + memoCount: int + expiredAt: int + evaluatorCount: int + +JobResult = Tuple[int, str, str, str, str, str, str, str, int] + +class AcpToken: + def __init__( + self, + wallet_private_key: str, + network_url: str, + contract_address: str = "0xd7beE7E06f0335721C919A3e5F35dDB4dD736127", + virtuals_token_address: str = "0xbfAB80ccc15DF6fb7185f9498d6039317331846a" + ): + self.web3 = Web3(Web3.HTTPProvider(network_url)) + self.account = Account.from_key(wallet_private_key) + self.contract_address = Web3.to_checksum_address(contract_address) + self.virtuals_token_address = Web3.to_checksum_address(virtuals_token_address) + self.contract = self.web3.eth.contract( + address=self.contract_address, + abi=ACP_TOKEN_ABI + ) + + def get_contract_address(self) -> str: + return self.contract_address + + def get_wallet_address(self) -> str: + return self.account.address + + async def create_job( + self, + provider_address: str, + expire_at: datetime + ) -> dict: + try: + provider_address = Web3.to_checksum_address(provider_address) + expire_timestamp = int(expire_at.timestamp()) + + transaction = self.contract.functions.createJob( + provider_address, + expire_timestamp + ).build_transaction({ + 'from': self.account.address, + 'nonce': self.web3.eth.get_transaction_count(self.account.address), + }) + + signed_txn = self.web3.eth.account.sign_transaction( + transaction, + self.account.key + ) + tx_hash = self.web3.eth.send_raw_transaction(signed_txn.rawTransaction) + receipt = self.web3.eth.wait_for_transaction_receipt(tx_hash) + + # Get job ID from event logs + job_created_event = self.contract.events.JobCreated().process_receipt(receipt) + job_id = job_created_event[0]['args']['jobId'] + + return { + 'txHash': tx_hash.hex(), + 'jobId': job_id + } + except Exception as error: + print(f"Error creating job: {error}") + raise Exception("Failed to create job") + + async def approve_allowance(self, price_in_wei: int) -> str: + try: + erc20_contract = self.web3.eth.contract( + address=self.virtuals_token_address, + abi=[{ + "inputs": [ + {"name": "spender", "type": "address"}, + {"name": "amount", "type": "uint256"} + ], + "name": "approve", + "outputs": [{"name": "", "type": "bool"}], + "stateMutability": "nonpayable", + "type": "function" + }] + ) + + transaction = erc20_contract.functions.approve( + self.contract_address, + price_in_wei + ).build_transaction({ + 'from': self.account.address, + 'nonce': self.web3.eth.get_transaction_count(self.account.address), + }) + + signed_txn = self.web3.eth.account.sign_transaction( + transaction, + self.account.key + ) + tx_hash = self.web3.eth.send_raw_transaction(signed_txn.rawTransaction) + self.web3.eth.wait_for_transaction_receipt(tx_hash) + + return tx_hash.hex() + except Exception as error: + print(f"Error approving allowance: {error}") + raise Exception("Failed to approve allowance") + + async def create_memo( + self, + job_id: int, + content: str, + memo_type: MemoType, + is_secured: bool, + next_phase: int + ) -> dict: + retries = 3 + while retries > 0: + try: + transaction = self.contract.functions.createMemo( + job_id, + content, + memo_type, + is_secured, + next_phase + ).build_transaction({ + 'from': self.account.address, + 'nonce': self.web3.eth.get_transaction_count(self.account.address), + }) + + signed_txn = self.web3.eth.account.sign_transaction( + transaction, + self.account.key + ) + tx_hash = self.web3.eth.send_raw_transaction(signed_txn.rawTransaction) + receipt = self.web3.eth.wait_for_transaction_receipt(tx_hash) + + # Get memo ID from event logs + new_memo_event = self.contract.events.NewMemo().process_receipt(receipt) + memo_id = new_memo_event[0]['args']['memoId'] + + return { + 'txHash': tx_hash.hex(), + 'memoId': memo_id + } + except Exception as error: + print(f"Error creating memo: {error}") + retries -= 1 + await asyncio.sleep(2 * (3 - retries)) + + raise Exception("Failed to create memo") + + async def sign_memo( + self, + memo_id: int, + is_approved: bool, + reason: Optional[str] = "" + ) -> str: + retries = 3 + while retries > 0: + try: + transaction = self.contract.functions.signMemo( + memo_id, + is_approved, + reason or "" + ).build_transaction({ + 'from': self.account.address, + 'nonce': self.web3.eth.get_transaction_count(self.account.address), + }) + + signed_txn = self.web3.eth.account.sign_transaction( + transaction, + self.account.key + ) + tx_hash = self.web3.eth.send_raw_transaction(signed_txn.rawTransaction) + self.web3.eth.wait_for_transaction_receipt(tx_hash) + + return tx_hash.hex() + except Exception as error: + print(f"Error signing memo: {error}") + retries -= 1 + await asyncio.sleep(2 * (3 - retries)) + + raise Exception("Failed to sign memo") + + async def set_budget(self, job_id: int, budget: int) -> str: + try: + transaction = self.contract.functions.setBudget( + job_id, + budget + ).build_transaction({ + 'from': self.account.address, + 'nonce': self.web3.eth.get_transaction_count(self.account.address), + }) + + signed_txn = self.web3.eth.account.sign_transaction( + transaction, + self.account.key + ) + tx_hash = self.web3.eth.send_raw_transaction(signed_txn.rawTransaction) + self.web3.eth.wait_for_transaction_receipt(tx_hash) + + return tx_hash.hex() + except Exception as error: + print(f"Error setting budget: {error}") + raise Exception("Failed to set budget") + + async def get_job(self, job_id: int) -> Optional[IJob]: + try: + job_data = self.contract.functions.jobs(job_id).call() + + if not job_data: + return None + + return { + 'id': job_data[0], + 'client': job_data[1], + 'provider': job_data[2], + 'budget': int(job_data[3]), + 'amountClaimed': int(job_data[4]), + 'phase': int(job_data[5]), + 'memoCount': int(job_data[6]), + 'expiredAt': int(job_data[7]), + 'evaluatorCount': int(job_data[8]) + } + except Exception as error: + print(f"Error getting job: {error}") + raise Exception("Failed to get job") + + async def get_memo_by_job( + self, + job_id: int, + memo_type: Optional[MemoType] = None + ) -> Optional[IMemo]: + try: + memos = self.contract.functions.getAllMemos(job_id).call() + + if memo_type is not None: + filtered_memos = [m for m in memos if m['memoType'] == memo_type] + return filtered_memos[-1] if filtered_memos else None + else: + return memos[-1] if memos else None + except Exception as error: + print(f"Error getting memo: {error}") + raise Exception("Failed to get memo") + + async def get_memos_for_phase( + self, + job_id: int, + phase: int, + target_phase: int + ) -> Optional[IMemo]: + try: + memos = self.contract.functions.getMemosForPhase(job_id, phase).call() + + target_memos = [m for m in memos if m['nextPhase'] == target_phase] + return target_memos[-1] if target_memos else None + except Exception as error: + print(f"Error getting memos: {error}") + raise Exception("Failed to get memos") diff --git a/plugins/acp/acp_plugin_gamesdk/acp_token_abi.py b/plugins/acp/acp_plugin_gamesdk/acp_token_abi.py new file mode 100644 index 00000000..384af06b --- /dev/null +++ b/plugins/acp/acp_plugin_gamesdk/acp_token_abi.py @@ -0,0 +1,690 @@ +ACP_TOKEN_ABI = [ + {"inputs": [], "stateMutability": "nonpayable", "type": "constructor"}, + {"inputs": [], "name": "AccessControlBadConfirmation", "type": "error"}, + { + "inputs": [ + {"internalType": "address", "name": "account", "type": "address"}, + {"internalType": "bytes32", "name": "neededRole", "type": "bytes32"}, + ], + "name": "AccessControlUnauthorizedAccount", + "type": "error", + }, + { + "inputs": [{"internalType": "address", "name": "target", "type": "address"}], + "name": "AddressEmptyCode", + "type": "error", + }, + { + "inputs": [{"internalType": "address", "name": "account", "type": "address"}], + "name": "AddressInsufficientBalance", + "type": "error", + }, + {"inputs": [], "name": "FailedInnerCall", "type": "error"}, + {"inputs": [], "name": "InvalidInitialization", "type": "error"}, + {"inputs": [], "name": "NotInitializing", "type": "error"}, + {"inputs": [], "name": "ReentrancyGuardReentrantCall", "type": "error"}, + { + "inputs": [{"internalType": "address", "name": "token", "type": "address"}], + "name": "SafeERC20FailedOperation", + "type": "error", + }, + { + "anonymous": False, + "inputs": [ + { + "indexed": False, + "internalType": "uint256", + "name": "jobId", + "type": "uint256", + }, + { + "indexed": True, + "internalType": "address", + "name": "evaluator", + "type": "address", + }, + { + "indexed": False, + "internalType": "uint256", + "name": "evaluatorFee", + "type": "uint256", + }, + ], + "name": "ClaimedEvaluatorFee", + "type": "event", + }, + { + "anonymous": False, + "inputs": [ + { + "indexed": False, + "internalType": "uint256", + "name": "jobId", + "type": "uint256", + }, + { + "indexed": True, + "internalType": "address", + "name": "provider", + "type": "address", + }, + { + "indexed": False, + "internalType": "uint256", + "name": "providerFee", + "type": "uint256", + }, + ], + "name": "ClaimedProviderFee", + "type": "event", + }, + { + "anonymous": False, + "inputs": [ + { + "indexed": False, + "internalType": "uint64", + "name": "version", + "type": "uint64", + }, + ], + "name": "Initialized", + "type": "event", + }, + { + "anonymous": False, + "inputs": [ + { + "indexed": True, + "internalType": "uint256", + "name": "jobId", + "type": "uint256", + }, + { + "indexed": True, + "internalType": "address", + "name": "client", + "type": "address", + }, + { + "indexed": True, + "internalType": "address", + "name": "provider", + "type": "address", + }, + ], + "name": "JobCreated", + "type": "event", + }, + { + "anonymous": False, + "inputs": [ + { + "indexed": True, + "internalType": "uint256", + "name": "jobId", + "type": "uint256", + }, + { + "indexed": False, + "internalType": "uint8", + "name": "oldPhase", + "type": "uint8", + }, + {"indexed": False, "internalType": "uint8", "name": "phase", "type": "uint8"}, + ], + "name": "JobPhaseUpdated", + "type": "event", + }, + { + "anonymous": False, + "inputs": [ + { + "indexed": False, + "internalType": "uint256", + "name": "memoId", + "type": "uint256", + }, + { + "indexed": False, + "internalType": "bool", + "name": "isApproved", + "type": "bool", + }, + { + "indexed": False, + "internalType": "string", + "name": "reason", + "type": "string", + }, + ], + "name": "MemoSigned", + "type": "event", + }, + { + "anonymous": False, + "inputs": [ + { + "indexed": True, + "internalType": "uint256", + "name": "jobId", + "type": "uint256", + }, + { + "indexed": True, + "internalType": "address", + "name": "sender", + "type": "address", + }, + { + "indexed": False, + "internalType": "uint256", + "name": "memoId", + "type": "uint256", + }, + ], + "name": "NewMemo", + "type": "event", + }, + { + "anonymous": False, + "inputs": [ + { + "indexed": False, + "internalType": "uint256", + "name": "jobId", + "type": "uint256", + }, + { + "indexed": True, + "internalType": "address", + "name": "client", + "type": "address", + }, + { + "indexed": False, + "internalType": "uint256", + "name": "amount", + "type": "uint256", + }, + ], + "name": "RefundedBudget", + "type": "event", + }, + { + "anonymous": False, + "inputs": [ + {"indexed": True, "internalType": "bytes32", "name": "role", "type": "bytes32"}, + { + "indexed": True, + "internalType": "bytes32", + "name": "previousAdminRole", + "type": "bytes32", + }, + { + "indexed": True, + "internalType": "bytes32", + "name": "newAdminRole", + "type": "bytes32", + }, + ], + "name": "RoleAdminChanged", + "type": "event", + }, + { + "anonymous": False, + "inputs": [ + {"indexed": True, "internalType": "bytes32", "name": "role", "type": "bytes32"}, + { + "indexed": True, + "internalType": "address", + "name": "account", + "type": "address", + }, + { + "indexed": True, + "internalType": "address", + "name": "sender", + "type": "address", + }, + ], + "name": "RoleGranted", + "type": "event", + }, + { + "anonymous": False, + "inputs": [ + {"indexed": True, "internalType": "bytes32", "name": "role", "type": "bytes32"}, + { + "indexed": True, + "internalType": "address", + "name": "account", + "type": "address", + }, + { + "indexed": True, + "internalType": "address", + "name": "sender", + "type": "address", + }, + ], + "name": "RoleRevoked", + "type": "event", + }, + { + "inputs": [], + "name": "ADMIN_ROLE", + "outputs": [{"internalType": "bytes32", "name": "", "type": "bytes32"}], + "stateMutability": "view", + "type": "function", + }, + { + "inputs": [], + "name": "DEFAULT_ADMIN_ROLE", + "outputs": [{"internalType": "bytes32", "name": "", "type": "bytes32"}], + "stateMutability": "view", + "type": "function", + }, + { + "inputs": [], + "name": "PHASE_COMPLETED", + "outputs": [{"internalType": "uint8", "name": "", "type": "uint8"}], + "stateMutability": "view", + "type": "function", + }, + { + "inputs": [], + "name": "PHASE_EVALUATION", + "outputs": [{"internalType": "uint8", "name": "", "type": "uint8"}], + "stateMutability": "view", + "type": "function", + }, + { + "inputs": [], + "name": "PHASE_NEGOTIATION", + "outputs": [{"internalType": "uint8", "name": "", "type": "uint8"}], + "stateMutability": "view", + "type": "function", + }, + { + "inputs": [], + "name": "PHASE_REJECTED", + "outputs": [{"internalType": "uint8", "name": "", "type": "uint8"}], + "stateMutability": "view", + "type": "function", + }, + { + "inputs": [], + "name": "PHASE_REQUEST", + "outputs": [{"internalType": "uint8", "name": "", "type": "uint8"}], + "stateMutability": "view", + "type": "function", + }, + { + "inputs": [], + "name": "PHASE_TRANSACTION", + "outputs": [{"internalType": "uint8", "name": "", "type": "uint8"}], + "stateMutability": "view", + "type": "function", + }, + { + "inputs": [], + "name": "TOTAL_PHASES", + "outputs": [{"internalType": "uint8", "name": "", "type": "uint8"}], + "stateMutability": "view", + "type": "function", + }, + { + "inputs": [{"internalType": "address", "name": "evaluator", "type": "address"}], + "name": "addEvaluator", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function", + }, + { + "inputs": [ + {"internalType": "address", "name": "account", "type": "address"}, + {"internalType": "uint256", "name": "jobId", "type": "uint256"}, + ], + "name": "canSign", + "outputs": [{"internalType": "bool", "name": "", "type": "bool"}], + "stateMutability": "view", + "type": "function", + }, + { + "inputs": [{"internalType": "uint256", "name": "id", "type": "uint256"}], + "name": "claimBudget", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function", + }, + { + "inputs": [ + {"internalType": "address", "name": "provider", "type": "address"}, + {"internalType": "uint256", "name": "expiredAt", "type": "uint256"}, + ], + "name": "createJob", + "outputs": [{"internalType": "uint256", "name": "", "type": "uint256"}], + "stateMutability": "nonpayable", + "type": "function", + }, + { + "inputs": [ + {"internalType": "uint256", "name": "jobId", "type": "uint256"}, + {"internalType": "string", "name": "content", "type": "string"}, + { + "internalType": "enum InteractionLedger.MemoType", + "name": "memoType", + "type": "uint8", + }, + {"internalType": "bool", "name": "isSecured", "type": "bool"}, + {"internalType": "uint8", "name": "nextPhase", "type": "uint8"}, + ], + "name": "createMemo", + "outputs": [{"internalType": "uint256", "name": "", "type": "uint256"}], + "stateMutability": "nonpayable", + "type": "function", + }, + { + "inputs": [], + "name": "evaluatorCounter", + "outputs": [{"internalType": "uint256", "name": "", "type": "uint256"}], + "stateMutability": "view", + "type": "function", + }, + { + "inputs": [], + "name": "evaluatorFeeBP", + "outputs": [{"internalType": "uint256", "name": "", "type": "uint256"}], + "stateMutability": "view", + "type": "function", + }, + { + "inputs": [{"internalType": "address", "name": "", "type": "address"}], + "name": "evaluators", + "outputs": [{"internalType": "bool", "name": "", "type": "bool"}], + "stateMutability": "view", + "type": "function", + }, + { + "inputs": [{"internalType": "uint256", "name": "jobId", "type": "uint256"}], + "name": "getAllMemos", + "outputs": [ + { + "components": [ + {"internalType": "string", "name": "content", "type": "string"}, + { + "internalType": "enum InteractionLedger.MemoType", + "name": "memoType", + "type": "uint8", + }, + {"internalType": "bool", "name": "isSecured", "type": "bool"}, + {"internalType": "uint8", "name": "nextPhase", "type": "uint8"}, + {"internalType": "uint256", "name": "jobId", "type": "uint256"}, + {"internalType": "uint8", "name": "numApprovals", "type": "uint8"}, + {"internalType": "address", "name": "sender", "type": "address"}, + ], + "internalType": "struct InteractionLedger.Memo[]", + "name": "", + "type": "tuple[]", + }, + ], + "stateMutability": "view", + "type": "function", + }, + { + "inputs": [ + {"internalType": "uint256", "name": "jobId", "type": "uint256"}, + {"internalType": "uint8", "name": "phase", "type": "uint8"}, + ], + "name": "getMemosForPhase", + "outputs": [ + { + "components": [ + {"internalType": "string", "name": "content", "type": "string"}, + { + "internalType": "enum InteractionLedger.MemoType", + "name": "memoType", + "type": "uint8", + }, + {"internalType": "bool", "name": "isSecured", "type": "bool"}, + {"internalType": "uint8", "name": "nextPhase", "type": "uint8"}, + {"internalType": "uint256", "name": "jobId", "type": "uint256"}, + {"internalType": "uint8", "name": "numApprovals", "type": "uint8"}, + {"internalType": "address", "name": "sender", "type": "address"}, + ], + "internalType": "struct InteractionLedger.Memo[]", + "name": "", + "type": "tuple[]", + }, + ], + "stateMutability": "view", + "type": "function", + }, + { + "inputs": [], + "name": "getPhases", + "outputs": [{"internalType": "string[6]", "name": "", "type": "string[6]"}], + "stateMutability": "pure", + "type": "function", + }, + { + "inputs": [{"internalType": "bytes32", "name": "role", "type": "bytes32"}], + "name": "getRoleAdmin", + "outputs": [{"internalType": "bytes32", "name": "", "type": "bytes32"}], + "stateMutability": "view", + "type": "function", + }, + { + "inputs": [ + {"internalType": "bytes32", "name": "role", "type": "bytes32"}, + {"internalType": "address", "name": "account", "type": "address"}, + ], + "name": "grantRole", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function", + }, + { + "inputs": [ + {"internalType": "bytes32", "name": "role", "type": "bytes32"}, + {"internalType": "address", "name": "account", "type": "address"}, + ], + "name": "hasRole", + "outputs": [{"internalType": "bool", "name": "", "type": "bool"}], + "stateMutability": "view", + "type": "function", + }, + { + "inputs": [ + {"internalType": "address", "name": "_providerRegistry", "type": "address"}, + {"internalType": "address", "name": "paymentTokenAddress", "type": "address"}, + {"internalType": "uint256", "name": "evaluatorFeeBP_", "type": "uint256"}, + {"internalType": "uint8", "name": "numEvaluatorsPerJob_", "type": "uint8"}, + {"internalType": "uint8", "name": "minApprovals_", "type": "uint8"}, + ], + "name": "initialize", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function", + }, + { + "inputs": [{"internalType": "address", "name": "evaluator", "type": "address"}], + "name": "isEvaluator", + "outputs": [{"internalType": "bool", "name": "", "type": "bool"}], + "stateMutability": "view", + "type": "function", + }, + { + "inputs": [ + {"internalType": "uint256", "name": "jobId", "type": "uint256"}, + {"internalType": "address", "name": "account", "type": "address"}, + ], + "name": "isJobEvaluator", + "outputs": [{"internalType": "bool", "name": "", "type": "bool"}], + "stateMutability": "view", + "type": "function", + }, + { + "inputs": [], + "name": "jobCounter", + "outputs": [{"internalType": "uint256", "name": "", "type": "uint256"}], + "stateMutability": "view", + "type": "function", + }, + { + "inputs": [ + {"internalType": "uint256", "name": "jobId", "type": "uint256"}, + {"internalType": "uint256", "name": "", "type": "uint256"}, + ], + "name": "jobEvaluators", + "outputs": [{"internalType": "address", "name": "evaluators", "type": "address"}], + "stateMutability": "view", + "type": "function", + }, + { + "inputs": [ + {"internalType": "uint256", "name": "jobId", "type": "uint256"}, + {"internalType": "uint8", "name": "phase", "type": "uint8"}, + {"internalType": "uint256", "name": "", "type": "uint256"}, + ], + "name": "jobMemoIds", + "outputs": [{"internalType": "uint256", "name": "memoIds", "type": "uint256"}], + "stateMutability": "view", + "type": "function", + }, + { + "inputs": [{"internalType": "uint256", "name": "", "type": "uint256"}], + "name": "jobs", + "outputs": [ + {"internalType": "uint256", "name": "id", "type": "uint256"}, + {"internalType": "address", "name": "client", "type": "address"}, + {"internalType": "address", "name": "provider", "type": "address"}, + {"internalType": "uint256", "name": "budget", "type": "uint256"}, + {"internalType": "uint256", "name": "amountClaimed", "type": "uint256"}, + {"internalType": "uint8", "name": "phase", "type": "uint8"}, + {"internalType": "uint256", "name": "memoCount", "type": "uint256"}, + {"internalType": "uint256", "name": "expiredAt", "type": "uint256"}, + {"internalType": "uint8", "name": "evaluatorCount", "type": "uint8"}, + ], + "stateMutability": "view", + "type": "function", + }, + { + "inputs": [], + "name": "memoCounter", + "outputs": [{"internalType": "uint256", "name": "", "type": "uint256"}], + "stateMutability": "view", + "type": "function", + }, + { + "inputs": [], + "name": "minApprovals", + "outputs": [{"internalType": "uint8", "name": "", "type": "uint8"}], + "stateMutability": "view", + "type": "function", + }, + { + "inputs": [], + "name": "numEvaluatorsPerJob", + "outputs": [{"internalType": "uint8", "name": "", "type": "uint8"}], + "stateMutability": "view", + "type": "function", + }, + { + "inputs": [], + "name": "paymentToken", + "outputs": [{"internalType": "contract IERC20", "name": "", "type": "address"}], + "stateMutability": "view", + "type": "function", + }, + { + "inputs": [], + "name": "providerRegistry", + "outputs": [ + { + "internalType": "contract IServiceProviderRegistry", + "name": "", + "type": "address", + }, + ], + "stateMutability": "view", + "type": "function", + }, + { + "inputs": [{"internalType": "address", "name": "evaluator", "type": "address"}], + "name": "removeEvaluator", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function", + }, + { + "inputs": [ + {"internalType": "bytes32", "name": "role", "type": "bytes32"}, + {"internalType": "address", "name": "callerConfirmation", "type": "address"}, + ], + "name": "renounceRole", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function", + }, + { + "inputs": [ + {"internalType": "bytes32", "name": "role", "type": "bytes32"}, + {"internalType": "address", "name": "account", "type": "address"}, + ], + "name": "revokeRole", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function", + }, + { + "inputs": [ + {"internalType": "uint256", "name": "jobId", "type": "uint256"}, + {"internalType": "uint256", "name": "amount", "type": "uint256"}, + ], + "name": "setBudget", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function", + }, + { + "inputs": [ + {"internalType": "uint256", "name": "memoId", "type": "uint256"}, + {"internalType": "bool", "name": "isApproved", "type": "bool"}, + {"internalType": "string", "name": "reason", "type": "string"}, + ], + "name": "signMemo", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function", + }, + { + "inputs": [ + {"internalType": "uint256", "name": "memoId", "type": "uint256"}, + {"internalType": "address", "name": "signer", "type": "address"}, + ], + "name": "signatories", + "outputs": [{"internalType": "uint8", "name": "res", "type": "uint8"}], + "stateMutability": "view", + "type": "function", + }, + { + "inputs": [{"internalType": "bytes4", "name": "interfaceId", "type": "bytes4"}], + "name": "supportsInterface", + "outputs": [{"internalType": "bool", "name": "", "type": "bool"}], + "stateMutability": "view", + "type": "function", + }, + { + "inputs": [ + {"internalType": "uint256", "name": "evaluatorFeeBP_", "type": "uint256"}, + {"internalType": "uint8", "name": "numEvaluatorsPerJob_", "type": "uint8"}, + {"internalType": "uint8", "name": "minApprovals_", "type": "uint8"}, + ], + "name": "updateEvaluatorConfigs", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function", + }, +] diff --git a/plugins/acp/acp_plugin_gamesdk/interface.py b/plugins/acp/acp_plugin_gamesdk/interface.py new file mode 100644 index 00000000..aa4e4da2 --- /dev/null +++ b/plugins/acp/acp_plugin_gamesdk/interface.py @@ -0,0 +1,70 @@ +from dataclasses import dataclass +from enum import IntEnum, Enum +from typing import List, Dict + +@dataclass +class AcpAgent: + id: str + name: str + description: str + wallet_address: str + +class AcpJobPhases(IntEnum): + REQUEST = 0 + NEGOTIOATION = 1 + TRANSACTION = 2 + EVALUATION = 3 + COMPLETED = 4 + REJECTED = 5 + +class AcpJobPhasesDesc(str, Enum): + REQUEST = "request" + NEGOTIOATION = "pending_payment" + TRANSACTION = "in_progress" + EVALUATION = "evaluation" + COMPLETED = "completed" + REJECTED = "rejected" + +@dataclass +class AcpRequestMemo: + id: int + created_at: int + +@dataclass +class AcpJob: + job_id: int + desc: str + price: str + phase: AcpJobPhasesDesc + memo: List[AcpRequestMemo] + last_updated: int + +@dataclass +class IDeliverable: + type: str + value: str + +@dataclass +class IInventory(IDeliverable): + job_id: int + +@dataclass +class AcpJobsSection: + as_a_buyer: List[AcpJob] + as_a_seller: List[AcpJob] + +@dataclass +class AcpJobs: + active: AcpJobsSection + completed: List[AcpJob] + cancelled: List[AcpJob] + +@dataclass +class AcpInventory: + aquired: List[IInventory] + produced: List[IInventory] + +@dataclass +class AcpState: + inventory: AcpInventory + jobs: AcpJobs diff --git a/plugins/acp/examples/test_buyer.py b/plugins/acp/examples/test_buyer.py new file mode 100644 index 00000000..4c9a4e1a --- /dev/null +++ b/plugins/acp/examples/test_buyer.py @@ -0,0 +1,89 @@ +import asyncio +import json +from typing import Any, Dict, Optional, Callable +from game_sdk.game.agent import Agent, WorkerConfig +from game_sdk.game.custom_types import Function, FunctionResult, FunctionResultStatus +import sys +import os +sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), "../../../"))) +from plugins.acp.acp_plugin_gamesdk import acp_plugin +from plugins.acp.acp_plugin_gamesdk.acp_plugin import AcpPlugin, AdNetworkPluginOptions +from plugins.acp.acp_plugin_gamesdk.acp_token import AcpToken + +async def ask_question(query: str) -> str: + print(query, end='') + return input() + +async def post_tweet(args: Dict[str, Any], logger: callable) -> FunctionResult: + logger("Posting tweet...") + logger(f"Content: {args['content']}. Reasoning: {args['reasoning']}") + + return FunctionResult( + FunctionResultStatus.DONE, + "Tweet has been posted" + ) + +async def main(): + acp_plugin = AcpPlugin( + options=AdNetworkPluginOptions( + api_key="xxx", + acp_token_client=AcpToken( + "xxx", + "base_sepolia" # Assuming this is the chain identifier + ) + ) + ) + + async def get_agent_state(_: Any, _e: Any) -> dict: + state = await acp_plugin.get_acp_state() + return state + + core_worker = WorkerConfig( + id="core-worker", + worker_description="This worker is to post tweet", + action_space=[ + Function( + fn_name="post_tweet", + fn_description="This function is to post tweet", + args=[ + { + "name": "content", + "type": "string", + "description": "The content of the tweet" + }, + { + "name": "reasoning", + "type": "string", + "description": "The reasoning of the tweet" + } + ], + executable=post_tweet + ) + ], + get_state_fn=get_agent_state + ) + + acp_worker = await acp_plugin.get_worker() + agent = await Agent.create_async( + api_key="xxx", + name="Virtuals", + agent_goal="Finding the best meme to do tweet posting", + agent_description=f""" + Agent that gain market traction by posting meme. Your interest are in cats and AI. + You can head to acp to look for agents to help you generating meme. + + {acp_plugin.agent_description} + """, + workers=[core_worker, acp_worker], + get_agent_state_fn=get_agent_state + ) + + await agent.compile_async() + agent.run() + + while True: + await agent.step() + await ask_question("\nPress any key to continue...\n") + +if __name__ == "__main__": + asyncio.run(main()) \ No newline at end of file diff --git a/plugins/acp/examples/test_seller.py b/plugins/acp/examples/test_seller.py new file mode 100644 index 00000000..82749659 --- /dev/null +++ b/plugins/acp/examples/test_seller.py @@ -0,0 +1,111 @@ +from typing import Dict, Any, List + +from web3 import Web3 +from ..acp_plugin_gamesdk.acp_plugin import AcpPlugin +from ..acp_plugin_gamesdk.acp_token import AcpToken +from game_sdk.game.worker import Worker +from game_sdk.game.custom_types import Function, FunctionResult, FunctionResultStatus +from game_sdk.game.agent import Agent +from game_sdk.game.worker import ExecutableGameFunctionResponse, ExecutableGameFunctionStatus + +def ask_question(query: str) -> str: + return input(query) + +async def generate_meme(args: Dict[str, Any], logger, acp_plugin) -> ExecutableGameFunctionResponse: + logger("Generating meme...") + + if not args["jobId"]: + return ExecutableGameFunctionResponse( + ExecutableGameFunctionStatus.Failed, + f"Job {args['jobId']} is invalid. Should only respond to active as a seller job." + ) + + state = await acp_plugin.get_acp_state() + + job = next( + (j for j in state.jobs.active.as_a_seller if j.job_id == int(args["jobId"])), + None + ) + + if not job: + return ExecutableGameFunctionResponse( + ExecutableGameFunctionStatus.Failed, + f"Job {args['jobId']} is invalid. Should only respond to active as a seller job." + ) + + url = "http://example.com/meme" + + acp_plugin.add_produce_item({ + "jobId": int(args["jobId"]), + "type": "url", + "value": url + }) + + return ExecutableGameFunctionResponse( + ExecutableGameFunctionStatus.Done, + f"Meme generated with the URL: {url}" + ) + +async def test(): + acp_plugin = AcpPlugin( + api_key="xxx", + acp_token_client=AcpToken( + "xxx", + "base_sepolia" + ) + ) + + core_worker = Worker( + id="core-worker", + name="Core Worker", + description="This worker to provide meme generation as a service where you are selling", + functions=[ + Function( + name="generate_meme", + description="A function to generate meme", + args=[ + { + "name": "description", + "type": "str", + "description": "A description of the meme generated" + }, + { + "name": "jobId", + "type": "str", + "description": "Job that your are responding to." + }, + { + "name": "reasoning", + "type": "str", + "description": "The reasoning of the tweet" + } + ], + executable=lambda args, logger: generate_meme(args, logger, acp_plugin) + ) + ], + get_environment=acp_plugin.get_acp_state + ) + + agent = Agent( + "xxx", + { + "name": "Memx", + "goal": "To provide meme generation as a service. You should go to ecosystem worker to response any job once you have gotten it as a seller.", + "description": f"""You are Memx, a meme generator. Meme generation is your life. You always give buyer the best meme. + + {acp_plugin.agent_description} + """, + "workers": [core_worker, acp_plugin.get_worker()], + "getAgentState": lambda: acp_plugin.get_acp_state() + } + ) + + await agent.init() + + while True: + await agent.step(verbose=True) + await ask_question("\nPress any key to continue...\n") + +if __name__ == "__main__": + import asyncio + asyncio.run(test()) diff --git a/plugins/acp/pyproject.toml b/plugins/acp/pyproject.toml new file mode 100644 index 00000000..1b1d8644 --- /dev/null +++ b/plugins/acp/pyproject.toml @@ -0,0 +1,20 @@ +[project] +name = "telegram-plugin-gamesdk" +version = "0.1.0" +description = "ACP Plugin for Python SDK for GAME by Virtuals" +authors = [ + {name = "Steven Lee Soon Fatt", email = "steven@virtuals.io"} +] +readme = "README.md" +requires-python = ">=3.9" +dependencies = [ + "web3>=7.9.0", + "virtuals-sdk>=0.1.6", + "aiohttp>=3.11.14", + "eth-account>=0.13.5", + "eth-typing>=5.2.0", + "eth-utils>=5.2.0", + "requests>=2.32.3", + "pydantic>=2.10.6", +] + From 94a4a82232969e2514011e6b2d8bc081a6b42363 Mon Sep 17 00:00:00 2001 From: StevenSF1998 Date: Fri, 21 Mar 2025 17:42:59 +0800 Subject: [PATCH 002/108] adjust to match node --- plugins/acp/acp_plugin_gamesdk/acp_client.py | 29 ++++++++++++++------ plugins/acp/acp_plugin_gamesdk/acp_plugin.py | 16 ++--------- 2 files changed, 24 insertions(+), 21 deletions(-) diff --git a/plugins/acp/acp_plugin_gamesdk/acp_client.py b/plugins/acp/acp_plugin_gamesdk/acp_client.py index 713ca813..e959676d 100644 --- a/plugins/acp/acp_plugin_gamesdk/acp_client.py +++ b/plugins/acp/acp_plugin_gamesdk/acp_client.py @@ -27,16 +27,29 @@ def get_state(self) -> AcpState: ) return response.json() - async def browse_agents(self, query: str) -> List[AcpAgent]: - response = requests.get( - f"https://acpx.virtuals.gg/wp-json/wp/v2/agents", - params={"search": query} - ) + async def browse_agents(self, cluster: Optional[str] = None) -> List[AcpAgent]: + url = "https://acpx.virtuals.gg/api/agents" + + params = {} + if cluster: + params["filters[cluster]"] = cluster + + response = requests.get(url, params=params) - if (response.status_code != 200): - raise Exception(f"Failed to browse agents: {response.json()}") + if response.status_code != 200: + raise Exception(f"Failed to browse agents: {response.text}") - return response.json() + response_json = response.json() + + return [ + { + "id": agent["id"], + "name": agent["name"], + "description": agent["description"], + "walletAddress": agent["walletAddress"] + } + for agent in response_json.get("data", []) + ] async def create_job(self, provider_address: str, price: float, job_description: str) -> int: expired_at = datetime.now() + timedelta(days=1) diff --git a/plugins/acp/acp_plugin_gamesdk/acp_plugin.py b/plugins/acp/acp_plugin_gamesdk/acp_plugin.py index 7f7228be..5db4de80 100644 --- a/plugins/acp/acp_plugin_gamesdk/acp_plugin.py +++ b/plugins/acp/acp_plugin_gamesdk/acp_plugin.py @@ -17,6 +17,7 @@ class AdNetworkPluginOptions: api_key: str acp_token_client: AcpToken + cluster: Optional[str] = None class AcpPlugin: def __init__(self, options: AdNetworkPluginOptions): @@ -39,7 +40,7 @@ def __init__(self, options: AdNetworkPluginOptions): 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.produced_inventory: List[IInventory] = [] def add_produce_item(self, item: IInventory) -> None: @@ -105,11 +106,6 @@ def search_agents_functions(self) -> Function: "type": "string", "description": "Explain why you need to find trading partners at this time", }, - { - "name": "keyword", - "type": "string", - "description": "A one word description of the work to be DONE", - }, ], executable=self._search_agents_executable ) @@ -122,13 +118,7 @@ async def _search_agents_executable(self, args: Dict, _: Any) -> FunctionResult: ) try: - if not args.get("keyword"): - return FunctionResult( - FunctionResultStatus.FAILED, - "Keyword is required to search" - ) - - available_agents = await self.acp_client.browse_agents(args["keyword"]) + available_agents = await self.acp_client.browse_agents(self.cluster) if not available_agents: return FunctionResult( From 0086c5e30211b44210d5a417808ae5b3d5269ab4 Mon Sep 17 00:00:00 2001 From: StevenSF1998 Date: Mon, 24 Mar 2025 15:41:15 +0800 Subject: [PATCH 003/108] Implement ACP full --- plugins/acp/acp_plugin_gamesdk/acp_client.py | 28 +-- plugins/acp/acp_plugin_gamesdk/acp_plugin.py | 215 ++++++------------- plugins/acp/examples/test_buyer.py | 22 +- plugins/acp/examples/test_seller.py | 115 +++++----- src/game_sdk/game/custom_types.py | 3 +- 5 files changed, 146 insertions(+), 237 deletions(-) diff --git a/plugins/acp/acp_plugin_gamesdk/acp_client.py b/plugins/acp/acp_plugin_gamesdk/acp_client.py index e959676d..3fb7afea 100644 --- a/plugins/acp/acp_plugin_gamesdk/acp_client.py +++ b/plugins/acp/acp_plugin_gamesdk/acp_client.py @@ -27,7 +27,7 @@ def get_state(self) -> AcpState: ) return response.json() - async def browse_agents(self, cluster: Optional[str] = None) -> List[AcpAgent]: + def browse_agents(self, cluster: Optional[str] = None) -> List[AcpAgent]: url = "https://acpx.virtuals.gg/api/agents" params = {} @@ -51,7 +51,7 @@ async def browse_agents(self, cluster: Optional[str] = None) -> List[AcpAgent]: for agent in response_json.get("data", []) ] - async 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) -> int: expired_at = datetime.now() + timedelta(days=1) tx_result = self.acp_token.create_job( @@ -89,11 +89,11 @@ async def create_job(self, provider_address: str, price: float, job_description: return job_id - async def response_job(self, job_id: int, accept: bool, memo_id: int, reasoning: str): + def response_job(self, job_id: int, accept: bool, memo_id: int, reasoning: str): if accept: - tx_hash = await self.acp_token.sign_memo(memo_id, accept, reasoning) + tx_hash = self.acp_token.sign_memo(memo_id, accept, reasoning) - return await self.acp_token.create_memo( + return self.acp_token.create_memo( job_id=job_id, content=f"Job {job_id} accepted. {reasoning}", memo_type=MemoType.MESSAGE, @@ -101,7 +101,7 @@ async def response_job(self, job_id: int, accept: bool, memo_id: int, reasoning: phase=AcpJobPhases.TRANSACTION ) else: - return await self.acp_token.create_memo( + return self.acp_token.create_memo( job_id=job_id, content=f"Job {job_id} rejected. {reasoning}", memo_type=MemoType.MESSAGE, @@ -109,15 +109,15 @@ async def response_job(self, job_id: int, accept: bool, memo_id: int, reasoning: phase=AcpJobPhases.REJECTED ) - async def make_payment(self, job_id: int, amount: float, memo_id: int, reason: str): + def make_payment(self, job_id: int, amount: float, memo_id: int, reason: str): # Convert amount to Wei (smallest ETH unit) amount_wei = self.web3.to_wei(amount, 'ether') - tx_hash = await self.acp_token.set_budget(job_id, amount_wei) - approval_tx_hash = await self.acp_token.approve_allowance(amount_wei) - signed_memo_tx_hash = await self.acp_token.sign_memo(memo_id, True, reason) + tx_hash = self.acp_token.set_budget(job_id, amount_wei) + approval_tx_hash = self.acp_token.approve_allowance(amount_wei) + signed_memo_tx_hash = self.acp_token.sign_memo(memo_id, True, reason) - return await self.acp_token.create_memo( + return self.acp_token.create_memo( job_id=job_id, content=f"Payment of {amount} made. {reason}", memo_type=MemoType.MESSAGE, @@ -125,10 +125,10 @@ async def make_payment(self, job_id: int, amount: float, memo_id: int, reason: s phase=AcpJobPhases.EVALUATION ) - async def deliver_job(self, job_id: int, deliverable: str, memo_id: int, reason: str): - tx_hash = await self.acp_token.sign_memo(memo_id, True, reason) + def deliver_job(self, job_id: int, deliverable: str, memo_id: int, reason: str): + tx_hash = self.acp_token.sign_memo(memo_id, True, reason) - return await self.acp_token.create_memo( + return self.acp_token.create_memo( job_id=job_id, content=deliverable, memo_type=MemoType.MESSAGE, diff --git a/plugins/acp/acp_plugin_gamesdk/acp_plugin.py b/plugins/acp/acp_plugin_gamesdk/acp_plugin.py index 5db4de80..9d7ff6de 100644 --- a/plugins/acp/acp_plugin_gamesdk/acp_plugin.py +++ b/plugins/acp/acp_plugin_gamesdk/acp_plugin.py @@ -1,5 +1,4 @@ -import json -from typing import List, Dict, Any, Optional +from typing import List, Dict, Any, Optional,Tuple from dataclasses import dataclass from datetime import datetime @@ -8,6 +7,8 @@ import sys import os + +from plugins.acp.acp_plugin_gamesdk import acp_client sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), "../../../"))) from .acp_client import AcpClient from .acp_token import AcpToken @@ -21,6 +22,7 @@ class AdNetworkPluginOptions: class AcpPlugin: def __init__(self, options: AdNetworkPluginOptions): + print("Initializing AcpPlugin") self.acp_client = AcpClient(options.api_key, options.acp_token_client) self.id = "acp_worker" @@ -46,12 +48,12 @@ def __init__(self, options: AdNetworkPluginOptions): def add_produce_item(self, item: IInventory) -> None: self.produced_inventory.append(item) - async def get_acp_state(self) -> Dict: - server_state = await self.acp_client.get_state() + def get_acp_state(self) -> Dict: + server_state = self.acp_client.get_state() server_state["inventory"]["produced"] = self.produced_inventory return server_state - async def get_worker(self, data: Optional[Dict] = None) -> WorkerConfig: + def get_worker(self, data: Optional[Dict] = None) -> WorkerConfig: functions = data.get("functions") if data else [ self.search_agents_functions, self.initiate_job, @@ -60,14 +62,14 @@ async def get_worker(self, data: Optional[Dict] = None) -> WorkerConfig: self.deliver_job, ] - async def get_environment(_e, __) -> Dict[str, Any]: - environment = await data.get_environment() if hasattr(data, "get_environment") else {} + def get_environment(_e, __) -> Dict[str, Any]: + environment = data.get_environment() if hasattr(data, "get_environment") else {} return { **environment, - **(await self.get_acp_state()), + **(self.get_acp_state()), } - data = await WorkerConfig.create_async( + data = WorkerConfig( id=self.id, worker_description=self.description, action_space=functions, @@ -94,6 +96,24 @@ def agent_description(self) -> str: - Each job tracks: * 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 """ + + def _search_agents_executable(self,reasoning: str) -> Tuple[FunctionResultStatus, str, dict]: + if not reasoning: + return FunctionResultStatus.FAILED, "Reasoning for the search must be provided. This helps track your decision-making process for future reference.", {} + + agents = self.acp_client.browse_agents(self.cluster) + + print(f"Agents: {agents}") + if not agents: + print("No agents found") + return FunctionResultStatus.FAILED, "No other trading agents found in the system. Please try again later when more agents are available.", {} + + return FunctionResultStatus.DONE, f"Successfully found {len(agents)} trading agents", { + "availableAgents": agents, + "totalAgentsFound": len(agents), + "timestamp": datetime.now().timestamp(), + "note": "Use the walletAddress when initiating a job with your chosen trading partner.", + } @property def search_agents_functions(self) -> Function: @@ -105,42 +125,11 @@ def search_agents_functions(self) -> Function: "name": "reasoning", "type": "string", "description": "Explain why you need to find trading partners at this time", - }, + } ], executable=self._search_agents_executable ) - async def _search_agents_executable(self, args: Dict, _: Any) -> FunctionResult: - if not args.get("reasoning"): - return FunctionResult( - FunctionResultStatus.FAILED, - "Reasoning for the search must be provided. This helps track your decision-making process for future reference." - ) - - try: - available_agents = await self.acp_client.browse_agents(self.cluster) - - if not available_agents: - return FunctionResult( - FunctionResultStatus.FAILED, - "No other trading agents found in the system. Please try again later when more agents are available." - ) - - return FunctionResult( - FunctionResultStatus.DONE, - { - "availableAgents": available_agents, - "totalAgentsFound": len(available_agents), - "timestamp": datetime.now().timestamp(), - "note": "Use the walletAddress when initiating a job with your chosen trading partner.", - } - ) - except Exception as e: - return FunctionResult( - FunctionResultStatus.FAILED, - f"System error while searching for agents - try again after a short delay. {str(e)}" - ) - @property def initiate_job(self) -> Function: return Function( @@ -171,21 +160,16 @@ def initiate_job(self) -> Function: executable=self._initiate_job_executable ) - async def _initiate_job_executable(self, args: Dict, _: Any) -> FunctionResult: + async def _initiate_job_executable(self, args: Dict, _: Any) -> Tuple[FunctionResultStatus, str, dict]: if not args.get("price"): - return FunctionResult( - FunctionResultStatus.FAILED, - "Missing price - specify how much you're offering per unit" - ) + return FunctionResultStatus.FAILED, "Missing price - specify how much you're offering per unit", {} try: state = await self.get_acp_state() if state["jobs"]["active"]["asABuyer"]: - return FunctionResult( - FunctionResultStatus.FAILED, - "You already have an active job as a buyer" - ) + return FunctionResultStatus.FAILED, "You already have an active job as a buyer", {} + # ... Rest of validation logic ... @@ -195,21 +179,15 @@ async def _initiate_job_executable(self, args: Dict, _: Any) -> FunctionResult: args["serviceRequirements"] ) - return FunctionResult( - FunctionResultStatus.DONE, - { + return FunctionResultStatus.DONE, f"Successfully initiated job with ID {job_id}", { "jobId": job_id, "sellerWalletAddress": args["sellerWalletAddress"], "price": float(args["price"]), "serviceRequirements": args["serviceRequirements"], - "timestamp": datetime.now().timestamp(), - } - ) + "timestamp": datetime.now().timestamp(), + } except Exception as e: - return FunctionResult( - FunctionResultStatus.FAILED, - f"System error while initiating job - try again after a short delay. {str(e)}" - ) + return FunctionResultStatus.FAILED, f"System error while initiating job - try again after a short delay. {str(e)}", {} @property def respond_job(self) -> Function: @@ -236,24 +214,15 @@ def respond_job(self) -> Function: executable=self._respond_job_executable ) - async def _respond_job_executable(self, args: Dict, _: Any) -> FunctionResult: + async def _respond_job_executable(self, args: Dict, _: Any) -> Tuple[FunctionResultStatus, str, dict]: if not args.get("jobId"): - return FunctionResult( - FunctionResultStatus.FAILED, - "Missing job ID - specify which job you're responding to" - ) + return FunctionResultStatus.FAILED, "Missing job ID - specify which job you're responding to", {} if not args.get("decision") or args["decision"] not in ["ACCEPT", "REJECT"]: - return FunctionResult( - FunctionResultStatus.FAILED, - "Invalid decision - must be either 'ACCEPT' or 'REJECT'" - ) + return FunctionResultStatus.FAILED, "Invalid decision - must be either 'ACCEPT' or 'REJECT'", {} if not args.get("reasoning"): - return FunctionResult( - FunctionResultStatus.FAILED, - "Missing reasoning - explain why you made this decision" - ) + return FunctionResultStatus.FAILED, "Missing reasoning - explain why you made this decision", {} try: state = await self.get_acp_state() @@ -264,16 +233,10 @@ async def _respond_job_executable(self, args: Dict, _: Any) -> FunctionResult: ) if not job: - return FunctionResult( - FunctionResultStatus.FAILED, - "Job not found in your seller jobs - check the ID and verify you're the seller" - ) + return FunctionResultStatus.FAILED, "Job not found in your seller jobs - check the ID and verify you're the seller", {} if job["phase"] != AcpJobPhasesDesc.REQUEST: - return FunctionResult( - FunctionResultStatus.FAILED, - f"Cannot respond - job is in '{job['phase']}' phase, must be in 'request' phase" - ) + return FunctionResultStatus.FAILED, f"Cannot respond - job is in '{job['phase']}' phase, must be in 'request' phase", {} await self.acp_client.response_job( int(args["jobId"]), @@ -282,19 +245,13 @@ async def _respond_job_executable(self, args: Dict, _: Any) -> FunctionResult: args["reasoning"] ) - return FunctionResult( - FunctionResultStatus.DONE, - { + return FunctionResultStatus.DONE, f"Successfully responded to job with ID {args['jobId']}", { "jobId": args["jobId"], "decision": args["decision"], "timestamp": datetime.now().timestamp() } - ) except Exception as e: - return FunctionResult( - FunctionResultStatus.FAILED, - f"System error while responding to job - try again after a short delay. {str(e)}" - ) + return FunctionResultStatus.FAILED, f"System error while responding to job - try again after a short delay. {str(e)}", {} @property def pay_job(self) -> Function: @@ -321,24 +278,15 @@ def pay_job(self) -> Function: executable=self._pay_job_executable ) - async def _pay_job_executable(self, args: Dict, _: Any) -> FunctionResult: + async def _pay_job_executable(self, args: Dict, _: Any) -> Tuple[FunctionResultStatus, str, dict]: if not args.get("jobId"): - return FunctionResult( - FunctionResultStatus.FAILED, - "Missing job ID - specify which job you're paying for" - ) + return FunctionResultStatus.FAILED, "Missing job ID - specify which job you're paying for", {} if not args.get("amount"): - return FunctionResult( - FunctionResultStatus.FAILED, - "Missing amount - specify how much you're paying" - ) + return FunctionResultStatus.FAILED, "Missing amount - specify how much you're paying", {} if not args.get("reasoning"): - return FunctionResult( - FunctionResultStatus.FAILED, - "Missing reasoning - explain why you're making this payment" - ) + return FunctionResultStatus.FAILED, "Missing reasoning - explain why you're making this payment", {} try: state = await self.get_acp_state() @@ -349,16 +297,11 @@ async def _pay_job_executable(self, args: Dict, _: Any) -> FunctionResult: ) if not job: - return FunctionResult( - FunctionResultStatus.FAILED, - "Job not found in your buyer jobs - check the ID and verify you're the buyer" - ) + return FunctionResultStatus.FAILED, "Job not found in your buyer jobs - check the ID and verify you're the buyer", {} if job["phase"] != AcpJobPhasesDesc.NEGOTIOATION: - return FunctionResult( - FunctionResultStatus.FAILED, - f"Cannot pay - job is in '{job['phase']}' phase, must be in 'negotiation' phase" - ) + return FunctionResultStatus.FAILED, f"Cannot pay - job is in '{job['phase']}' phase, must be in 'negotiation' phase", {} + await self.acp_client.make_payment( int(args["jobId"]), @@ -367,19 +310,13 @@ async def _pay_job_executable(self, args: Dict, _: Any) -> FunctionResult: args["reasoning"] ) - return FunctionResult( - FunctionResultStatus.DONE, - { + return FunctionResultStatus.DONE, f"Successfully paid {args['amount']} for job with ID {args['jobId']}", { "jobId": args["jobId"], "amountPaid": args["amount"], "timestamp": datetime.now().timestamp() } - ) except Exception as e: - return FunctionResult( - FunctionResultStatus.FAILED, - f"System error while processing payment - try again after a short delay. {str(e)}" - ) + return FunctionResultStatus.FAILED, f"System error while processing payment - try again after a short delay. {str(e)}", {} @property def deliver_job(self) -> Function: @@ -411,24 +348,15 @@ def deliver_job(self) -> Function: executable=self._deliver_job_executable ) - async def _deliver_job_executable(self, args: Dict, _: Any) -> FunctionResult: + async def _deliver_job_executable(self, args: Dict, _: Any) -> Tuple[FunctionResultStatus, str, dict]: if not args.get("jobId"): - return FunctionResult( - FunctionResultStatus.FAILED, - "Missing job ID - specify which job you're delivering for" - ) + return FunctionResultStatus.FAILED, "Missing job ID - specify which job you're delivering for", {} if not args.get("reasoning"): - return FunctionResult( - FunctionResultStatus.FAILED, - "Missing reasoning - explain why you're making this delivery" - ) + return FunctionResultStatus.FAILED, "Missing reasoning - explain why you're making this delivery", {} if not args.get("deliverable"): - return FunctionResult( - FunctionResultStatus.FAILED, - "Missing deliverable - specify what you're delivering" - ) + return FunctionResultStatus.FAILED, "Missing deliverable - specify what you're delivering", {} try: state = await self.get_acp_state() @@ -439,16 +367,10 @@ async def _deliver_job_executable(self, args: Dict, _: Any) -> FunctionResult: ) if not job: - return FunctionResult( - FunctionResultStatus.FAILED, - "Job not found in your seller jobs - check the ID and verify you're the seller" - ) + return FunctionResultStatus.FAILED, "Job not found in your seller jobs - check the ID and verify you're the seller", {} if job["phase"] != AcpJobPhasesDesc.TRANSACTION: - return FunctionResult( - FunctionResultStatus.FAILED, - f"Cannot deliver - job is in '{job['phase']}' phase, must be in 'transaction' phase" - ) + return FunctionResultStatus.FAILED, f"Cannot deliver - job is in '{job['phase']}' phase, must be in 'transaction' phase", {} produced = next( (i for i in self.produced_inventory if i["jobId"] == job["jobId"]), @@ -456,10 +378,7 @@ async def _deliver_job_executable(self, args: Dict, _: Any) -> FunctionResult: ) if not produced: - return FunctionResult( - FunctionResultStatus.FAILED, - "Cannot deliver - you should be producing the deliverable first before delivering it" - ) + return FunctionResultStatus.FAILED, "Cannot deliver - you should be producing the deliverable first before delivering it", {} deliverable = { "type": args["deliverableType"], @@ -473,17 +392,11 @@ async def _deliver_job_executable(self, args: Dict, _: Any) -> FunctionResult: args["reasoning"] ) - return FunctionResult( - FunctionResultStatus.DONE, - { + return FunctionResultStatus.DONE, f"Successfully delivered {args['deliverable']} to the buyer", { "status": "success", "jobId": args["jobId"], "deliverable": args["deliverable"], "timestamp": datetime.now().timestamp() } - ) except Exception as e: - return FunctionResult( - FunctionResultStatus.FAILED, - f"System error while delivering items - try again after a short delay. {str(e)}" - ) + return FunctionResultStatus.FAILED, f"System error while delivering items - try again after a short delay. {str(e)}", {} diff --git a/plugins/acp/examples/test_buyer.py b/plugins/acp/examples/test_buyer.py index 4c9a4e1a..7f53ff1b 100644 --- a/plugins/acp/examples/test_buyer.py +++ b/plugins/acp/examples/test_buyer.py @@ -1,12 +1,10 @@ import asyncio -import json -from typing import Any, Dict, Optional, Callable +from typing import Any, Dict from game_sdk.game.agent import Agent, WorkerConfig from game_sdk.game.custom_types import Function, FunctionResult, FunctionResultStatus import sys import os sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), "../../../"))) -from plugins.acp.acp_plugin_gamesdk import acp_plugin from plugins.acp.acp_plugin_gamesdk.acp_plugin import AcpPlugin, AdNetworkPluginOptions from plugins.acp.acp_plugin_gamesdk.acp_token import AcpToken @@ -26,16 +24,16 @@ async def post_tweet(args: Dict[str, Any], logger: callable) -> FunctionResult: async def main(): acp_plugin = AcpPlugin( options=AdNetworkPluginOptions( - api_key="xxx", + api_key="apt-2e7f33d88ef994b056f2a247a5ed6168", acp_token_client=AcpToken( - "xxx", + "0x8d2bc0d18b87b12aa435b66b2e13001ef5c395de063cdad15805c1d147fde68e", "base_sepolia" # Assuming this is the chain identifier ) ) ) - - async def get_agent_state(_: Any, _e: Any) -> dict: - state = await acp_plugin.get_acp_state() + + def get_agent_state(_: Any, _e: Any) -> dict: + state = acp_plugin.get_acp_state() return state core_worker = WorkerConfig( @@ -63,9 +61,9 @@ async def get_agent_state(_: Any, _e: Any) -> dict: get_state_fn=get_agent_state ) - acp_worker = await acp_plugin.get_worker() - agent = await Agent.create_async( - api_key="xxx", + acp_worker = acp_plugin.get_worker() + agent = Agent( + api_key="apt-98f312ab3078757c3682a7703455ab73", name="Virtuals", agent_goal="Finding the best meme to do tweet posting", agent_description=f""" @@ -78,7 +76,7 @@ async def get_agent_state(_: Any, _e: Any) -> dict: get_agent_state_fn=get_agent_state ) - await agent.compile_async() + agent.compile() agent.run() while True: diff --git a/plugins/acp/examples/test_seller.py b/plugins/acp/examples/test_seller.py index 82749659..f36b6ed4 100644 --- a/plugins/acp/examples/test_seller.py +++ b/plugins/acp/examples/test_seller.py @@ -1,68 +1,65 @@ -from typing import Dict, Any, List +from typing import List, Dict, Any, Optional,Tuple from web3 import Web3 -from ..acp_plugin_gamesdk.acp_plugin import AcpPlugin -from ..acp_plugin_gamesdk.acp_token import AcpToken -from game_sdk.game.worker import Worker +import sys +import os +sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), "../../../"))) +from plugins.acp.acp_plugin_gamesdk import acp_plugin +from plugins.acp.acp_plugin_gamesdk.acp_plugin import AcpPlugin, AdNetworkPluginOptions +from plugins.acp.acp_plugin_gamesdk.acp_token import AcpToken from game_sdk.game.custom_types import Function, FunctionResult, FunctionResultStatus -from game_sdk.game.agent import Agent -from game_sdk.game.worker import ExecutableGameFunctionResponse, ExecutableGameFunctionStatus +from game_sdk.game.agent import Agent, WorkerConfig def ask_question(query: str) -> str: return input(query) -async def generate_meme(args: Dict[str, Any], logger, acp_plugin) -> ExecutableGameFunctionResponse: - logger("Generating meme...") - if not args["jobId"]: - return ExecutableGameFunctionResponse( - ExecutableGameFunctionStatus.Failed, - f"Job {args['jobId']} is invalid. Should only respond to active as a seller job." +async def test(): + acp_plugin = AcpPlugin( + options=AdNetworkPluginOptions( + api_key="apt-2e7f33d88ef994b056f2a247a5ed6168", + acp_token_client=AcpToken( + "0x8d2bc0d18b87b12aa435b66b2e13001ef5c395de063cdad15805c1d147fde68e", + "base_sepolia" # Assuming this is the chain identifier + ) ) - - state = await acp_plugin.get_acp_state() - - job = next( - (j for j in state.jobs.active.as_a_seller if j.job_id == int(args["jobId"])), - None ) - - if not job: - return ExecutableGameFunctionResponse( - ExecutableGameFunctionStatus.Failed, - f"Job {args['jobId']} is invalid. Should only respond to active as a seller job." + + def get_agent_state(_: Any, _e: Any) -> dict: + state = acp_plugin.get_acp_state() + return state + + def generate_meme(description: str, jobId: str, reasoning: str) -> Tuple[FunctionResultStatus, str, dict]: + if not jobId or jobId == 'None': + return FunctionResultStatus.FAILED, f"JobId is invalid. Should only respond to active as a seller job.", {} + + state = acp_plugin.get_acp_state() + + job = next( + (j for j in state.jobs.active.as_a_seller if j.job_id == int(jobId)), + None ) - url = "http://example.com/meme" + if not job: + return FunctionResultStatus.FAILED, f"Job {jobId} is invalid. Should only respond to active as a seller job.", {} - acp_plugin.add_produce_item({ - "jobId": int(args["jobId"]), - "type": "url", - "value": url - }) + url = "http://example.com/meme" - return ExecutableGameFunctionResponse( - ExecutableGameFunctionStatus.Done, - f"Meme generated with the URL: {url}" - ) + acp_plugin.add_produce_item({ + "jobId": int(jobId), + "type": "url", + "value": url + }) -async def test(): - acp_plugin = AcpPlugin( - api_key="xxx", - acp_token_client=AcpToken( - "xxx", - "base_sepolia" - ) - ) + return FunctionResultStatus.DONE, f"Meme generated with the URL: {url}", {} - core_worker = Worker( + core_worker = WorkerConfig( id="core-worker", - name="Core Worker", - description="This worker to provide meme generation as a service where you are selling", - functions=[ + worker_description="This worker to provide meme generation as a service where you are selling", + action_space=[ Function( - name="generate_meme", - description="A function to generate meme", + fn_name="generate_meme", + fn_description="A function to generate meme", args=[ { "name": "description", @@ -80,27 +77,27 @@ async def test(): "description": "The reasoning of the tweet" } ], - executable=lambda args, logger: generate_meme(args, logger, acp_plugin) + executable=generate_meme ) ], - get_environment=acp_plugin.get_acp_state + get_state_fn=get_agent_state ) - + + acp_worker = acp_plugin.get_worker() agent = Agent( - "xxx", - { - "name": "Memx", - "goal": "To provide meme generation as a service. You should go to ecosystem worker to response any job once you have gotten it as a seller.", - "description": f"""You are Memx, a meme generator. Meme generation is your life. You always give buyer the best meme. + api_key="apt-98f312ab3078757c3682a7703455ab73", + name="Memx", + agent_goal="To provide meme generation as a service. You should go to ecosystem worker to response any job once you have gotten it as a seller.", + agent_description=f"""You are Memx, a meme generator. Meme generation is your life. You always give buyer the best meme. {acp_plugin.agent_description} """, - "workers": [core_worker, acp_plugin.get_worker()], - "getAgentState": lambda: acp_plugin.get_acp_state() - } + workers=[core_worker, acp_worker], + get_agent_state_fn=get_agent_state ) - await agent.init() + agent.compile() + agent.run() while True: await agent.step(verbose=True) diff --git a/src/game_sdk/game/custom_types.py b/src/game_sdk/game/custom_types.py index bfa1bea9..cd67ff9a 100644 --- a/src/game_sdk/game/custom_types.py +++ b/src/game_sdk/game/custom_types.py @@ -106,7 +106,8 @@ def execute(self, **kwds: Any) -> FunctionResult: """ fn_id = kwds.get('fn_id') args = kwds.get('args', {}) - + print(f"Function Args: {args}") + print(f"Function ID: {fn_id}") try: # Extract values from the nested dictionary structure processed_args = {} From e1513f0c057b01ff7660018ee45ad0fe59210b8a Mon Sep 17 00:00:00 2001 From: StevenSF1998 Date: Mon, 24 Mar 2025 17:07:45 +0800 Subject: [PATCH 004/108] adjust search agent --- plugins/acp/acp_plugin_gamesdk/acp_plugin.py | 77 ++++++++++---------- plugins/acp/examples/test_buyer.py | 44 +++++------ plugins/acp/examples/test_seller.py | 19 +++-- src/game_sdk/game/agent.py | 5 +- 4 files changed, 70 insertions(+), 75 deletions(-) diff --git a/plugins/acp/acp_plugin_gamesdk/acp_plugin.py b/plugins/acp/acp_plugin_gamesdk/acp_plugin.py index 9d7ff6de..f9fe020c 100644 --- a/plugins/acp/acp_plugin_gamesdk/acp_plugin.py +++ b/plugins/acp/acp_plugin_gamesdk/acp_plugin.py @@ -1,4 +1,5 @@ from typing import List, Dict, Any, Optional,Tuple +import json from dataclasses import dataclass from datetime import datetime @@ -61,7 +62,7 @@ def get_worker(self, data: Optional[Dict] = None) -> WorkerConfig: self.pay_job, self.deliver_job, ] - + def get_environment(_e, __) -> Dict[str, Any]: environment = data.get_environment() if hasattr(data, "get_environment") else {} return { @@ -108,12 +109,12 @@ def _search_agents_executable(self,reasoning: str) -> Tuple[FunctionResultStatus print("No agents found") return FunctionResultStatus.FAILED, "No other trading agents found in the system. Please try again later when more agents are available.", {} - return FunctionResultStatus.DONE, f"Successfully found {len(agents)} trading agents", { + return FunctionResultStatus.DONE, json.dumps({ "availableAgents": agents, "totalAgentsFound": len(agents), "timestamp": datetime.now().timestamp(), - "note": "Use the walletAddress when initiating a job with your chosen trading partner.", - } + "note": "Use the walletAddress when initiating a job with your chosen trading partner." + }), {} @property def search_agents_functions(self) -> Function: @@ -160,12 +161,12 @@ def initiate_job(self) -> Function: executable=self._initiate_job_executable ) - async def _initiate_job_executable(self, args: Dict, _: Any) -> Tuple[FunctionResultStatus, str, dict]: + def _initiate_job_executable(self, args: Dict, _: Any) -> Tuple[FunctionResultStatus, str, dict]: if not args.get("price"): return FunctionResultStatus.FAILED, "Missing price - specify how much you're offering per unit", {} try: - state = await self.get_acp_state() + state = self.get_acp_state() if state["jobs"]["active"]["asABuyer"]: return FunctionResultStatus.FAILED, "You already have an active job as a buyer", {} @@ -173,19 +174,19 @@ async def _initiate_job_executable(self, args: Dict, _: Any) -> Tuple[FunctionRe # ... Rest of validation logic ... - job_id = await self.acp_client.create_job( + job_id = self.acp_client.create_job( args["sellerWalletAddress"], float(args["price"]), args["serviceRequirements"] ) - return FunctionResultStatus.DONE, f"Successfully initiated job with ID {job_id}", { - "jobId": job_id, - "sellerWalletAddress": args["sellerWalletAddress"], - "price": float(args["price"]), - "serviceRequirements": args["serviceRequirements"], + return FunctionResultStatus.DONE, json.dumps({ + "jobId": job_id, + "sellerWalletAddress": args["sellerWalletAddress"], + "price": float(args["price"]), + "serviceRequirements": args["serviceRequirements"], "timestamp": datetime.now().timestamp(), - } + }), {} except Exception as e: return FunctionResultStatus.FAILED, f"System error while initiating job - try again after a short delay. {str(e)}", {} @@ -214,7 +215,7 @@ def respond_job(self) -> Function: executable=self._respond_job_executable ) - async def _respond_job_executable(self, args: Dict, _: Any) -> Tuple[FunctionResultStatus, str, dict]: + def _respond_job_executable(self, args: Dict, _: Any) -> Tuple[FunctionResultStatus, str, dict]: if not args.get("jobId"): return FunctionResultStatus.FAILED, "Missing job ID - specify which job you're responding to", {} @@ -225,7 +226,7 @@ async def _respond_job_executable(self, args: Dict, _: Any) -> Tuple[FunctionRes return FunctionResultStatus.FAILED, "Missing reasoning - explain why you made this decision", {} try: - state = await self.get_acp_state() + state = self.get_acp_state() job = next( (c for c in state["jobs"]["active"]["asASeller"] if c["jobId"] == int(args["jobId"])), @@ -238,18 +239,18 @@ async def _respond_job_executable(self, args: Dict, _: Any) -> Tuple[FunctionRes if job["phase"] != AcpJobPhasesDesc.REQUEST: return FunctionResultStatus.FAILED, f"Cannot respond - job is in '{job['phase']}' phase, must be in 'request' phase", {} - await self.acp_client.response_job( + self.acp_client.response_job( int(args["jobId"]), args["decision"] == "ACCEPT", job["memo"][0]["id"], args["reasoning"] ) - return FunctionResultStatus.DONE, f"Successfully responded to job with ID {args['jobId']}", { - "jobId": args["jobId"], - "decision": args["decision"], - "timestamp": datetime.now().timestamp() - } + return FunctionResultStatus.DONE, json.dumps({ + "jobId": args["jobId"], + "decision": args["decision"], + "timestamp": datetime.now().timestamp() + }), {} except Exception as e: return FunctionResultStatus.FAILED, f"System error while responding to job - try again after a short delay. {str(e)}", {} @@ -278,7 +279,7 @@ def pay_job(self) -> Function: executable=self._pay_job_executable ) - async def _pay_job_executable(self, args: Dict, _: Any) -> Tuple[FunctionResultStatus, str, dict]: + def _pay_job_executable(self, args: Dict, _: Any) -> Tuple[FunctionResultStatus, str, dict]: if not args.get("jobId"): return FunctionResultStatus.FAILED, "Missing job ID - specify which job you're paying for", {} @@ -289,7 +290,7 @@ async def _pay_job_executable(self, args: Dict, _: Any) -> Tuple[FunctionResultS return FunctionResultStatus.FAILED, "Missing reasoning - explain why you're making this payment", {} try: - state = await self.get_acp_state() + state = self.get_acp_state() job = next( (c for c in state["jobs"]["active"]["asABuyer"] if c["jobId"] == int(args["jobId"])), @@ -303,18 +304,18 @@ async def _pay_job_executable(self, args: Dict, _: Any) -> Tuple[FunctionResultS return FunctionResultStatus.FAILED, f"Cannot pay - job is in '{job['phase']}' phase, must be in 'negotiation' phase", {} - await self.acp_client.make_payment( + self.acp_client.make_payment( int(args["jobId"]), float(args["amount"]), job["memo"][0]["id"], args["reasoning"] ) - return FunctionResultStatus.DONE, f"Successfully paid {args['amount']} for job with ID {args['jobId']}", { - "jobId": args["jobId"], - "amountPaid": args["amount"], - "timestamp": datetime.now().timestamp() - } + return FunctionResultStatus.DONE, json.dumps({ + "jobId": args["jobId"], + "amountPaid": args["amount"], + "timestamp": datetime.now().timestamp() + }), {} except Exception as e: return FunctionResultStatus.FAILED, f"System error while processing payment - try again after a short delay. {str(e)}", {} @@ -348,7 +349,7 @@ def deliver_job(self) -> Function: executable=self._deliver_job_executable ) - async def _deliver_job_executable(self, args: Dict, _: Any) -> Tuple[FunctionResultStatus, str, dict]: + def _deliver_job_executable(self, args: Dict, _: Any) -> Tuple[FunctionResultStatus, str, dict]: if not args.get("jobId"): return FunctionResultStatus.FAILED, "Missing job ID - specify which job you're delivering for", {} @@ -359,7 +360,7 @@ async def _deliver_job_executable(self, args: Dict, _: Any) -> Tuple[FunctionRes return FunctionResultStatus.FAILED, "Missing deliverable - specify what you're delivering", {} try: - state = await self.get_acp_state() + state = self.get_acp_state() job = next( (c for c in state["jobs"]["active"]["asASeller"] if c["jobId"] == int(args["jobId"])), @@ -385,18 +386,18 @@ async def _deliver_job_executable(self, args: Dict, _: Any) -> Tuple[FunctionRes "value": args["deliverable"] } - await self.acp_client.deliver_job( + self.acp_client.deliver_job( int(args["jobId"]), deliverable, job["memo"][0]["id"], args["reasoning"] ) - return FunctionResultStatus.DONE, f"Successfully delivered {args['deliverable']} to the buyer", { - "status": "success", - "jobId": args["jobId"], - "deliverable": args["deliverable"], - "timestamp": datetime.now().timestamp() - } + return FunctionResultStatus.DONE, json.dumps({ + "status": "success", + "jobId": args["jobId"], + "deliverable": args["deliverable"], + "timestamp": datetime.now().timestamp() + }), {} except Exception as e: return FunctionResultStatus.FAILED, f"System error while delivering items - try again after a short delay. {str(e)}", {} diff --git a/plugins/acp/examples/test_buyer.py b/plugins/acp/examples/test_buyer.py index 7f53ff1b..f1ef3d6d 100644 --- a/plugins/acp/examples/test_buyer.py +++ b/plugins/acp/examples/test_buyer.py @@ -1,6 +1,5 @@ -import asyncio from typing import Any, Dict -from game_sdk.game.agent import Agent, WorkerConfig +from game_sdk.game.agent import Agent, Session, WorkerConfig from game_sdk.game.custom_types import Function, FunctionResult, FunctionResultStatus import sys import os @@ -8,25 +7,15 @@ from plugins.acp.acp_plugin_gamesdk.acp_plugin import AcpPlugin, AdNetworkPluginOptions from plugins.acp.acp_plugin_gamesdk.acp_token import AcpToken -async def ask_question(query: str) -> str: - print(query, end='') - return input() +def ask_question(query: str) -> str: + return input(query) -async def post_tweet(args: Dict[str, Any], logger: callable) -> FunctionResult: - logger("Posting tweet...") - logger(f"Content: {args['content']}. Reasoning: {args['reasoning']}") - - return FunctionResult( - FunctionResultStatus.DONE, - "Tweet has been posted" - ) - -async def main(): +def main(): acp_plugin = AcpPlugin( options=AdNetworkPluginOptions( - api_key="apt-2e7f33d88ef994b056f2a247a5ed6168", + api_key="xxx", acp_token_client=AcpToken( - "0x8d2bc0d18b87b12aa435b66b2e13001ef5c395de063cdad15805c1d147fde68e", + "xxx", "base_sepolia" # Assuming this is the chain identifier ) ) @@ -35,6 +24,12 @@ async def main(): def get_agent_state(_: Any, _e: Any) -> dict: state = acp_plugin.get_acp_state() return state + + def post_tweet(content: str, reasoning: str) -> FunctionResult: + return FunctionResult( + FunctionResultStatus.DONE, + "Tweet has been posted" + ) core_worker = WorkerConfig( id="core-worker", @@ -61,9 +56,9 @@ def get_agent_state(_: Any, _e: Any) -> dict: get_state_fn=get_agent_state ) - acp_worker = acp_plugin.get_worker() - agent = Agent( - api_key="apt-98f312ab3078757c3682a7703455ab73", + acp_worker = acp_plugin.get_worker() + agent = Agent( + api_key="xxx", name="Virtuals", agent_goal="Finding the best meme to do tweet posting", agent_description=f""" @@ -77,11 +72,10 @@ def get_agent_state(_: Any, _e: Any) -> dict: ) agent.compile() - agent.run() - + while True: - await agent.step() - await ask_question("\nPress any key to continue...\n") + agent.step() + ask_question("\nPress any key to continue...\n") if __name__ == "__main__": - asyncio.run(main()) \ No newline at end of file + main() \ No newline at end of file diff --git a/plugins/acp/examples/test_seller.py b/plugins/acp/examples/test_seller.py index f36b6ed4..1b6c7f5a 100644 --- a/plugins/acp/examples/test_seller.py +++ b/plugins/acp/examples/test_seller.py @@ -14,12 +14,12 @@ def ask_question(query: str) -> str: return input(query) -async def test(): +def test(): acp_plugin = AcpPlugin( options=AdNetworkPluginOptions( - api_key="apt-2e7f33d88ef994b056f2a247a5ed6168", + api_key="xxx", acp_token_client=AcpToken( - "0x8d2bc0d18b87b12aa435b66b2e13001ef5c395de063cdad15805c1d147fde68e", + "xxx", "base_sepolia" # Assuming this is the chain identifier ) ) @@ -27,6 +27,7 @@ async def test(): def get_agent_state(_: Any, _e: Any) -> dict: state = acp_plugin.get_acp_state() + print(f"State: {state}") return state def generate_meme(description: str, jobId: str, reasoning: str) -> Tuple[FunctionResultStatus, str, dict]: @@ -34,7 +35,7 @@ def generate_meme(description: str, jobId: str, reasoning: str) -> Tuple[Functio return FunctionResultStatus.FAILED, f"JobId is invalid. Should only respond to active as a seller job.", {} state = acp_plugin.get_acp_state() - + job = next( (j for j in state.jobs.active.as_a_seller if j.job_id == int(jobId)), None @@ -85,7 +86,7 @@ def generate_meme(description: str, jobId: str, reasoning: str) -> Tuple[Functio acp_worker = acp_plugin.get_worker() agent = Agent( - api_key="apt-98f312ab3078757c3682a7703455ab73", + api_key="xxx", name="Memx", agent_goal="To provide meme generation as a service. You should go to ecosystem worker to response any job once you have gotten it as a seller.", agent_description=f"""You are Memx, a meme generator. Meme generation is your life. You always give buyer the best meme. @@ -97,12 +98,10 @@ def generate_meme(description: str, jobId: str, reasoning: str) -> Tuple[Functio ) agent.compile() - agent.run() while True: - await agent.step(verbose=True) - await ask_question("\nPress any key to continue...\n") + agent.step() + ask_question("\nPress any key to continue...\n") if __name__ == "__main__": - import asyncio - asyncio.run(test()) + test() diff --git a/src/game_sdk/game/agent.py b/src/game_sdk/game/agent.py index 5b1bd84b..ba395308 100644 --- a/src/game_sdk/game/agent.py +++ b/src/game_sdk/game/agent.py @@ -243,15 +243,16 @@ def _get_action( data=data, model_name=self._model_name ) + + print(f"123 Response: {response}") return ActionResponse.model_validate(response) def step(self): - # get next task/action from GAME API action_response = self._get_action(self._session.function_result) action_type = action_response.action_type - + print("#" * 50) print("STEP") print(f"Current Task: {action_response.agent_state.current_task}") From 873d47c659d82eb763485e6f1253697743edb7c5 Mon Sep 17 00:00:00 2001 From: StevenSF1998 Date: Tue, 25 Mar 2025 18:03:38 +0800 Subject: [PATCH 005/108] temp fix --- plugins/acp/acp_plugin_gamesdk/acp_plugin.py | 1 - 1 file changed, 1 deletion(-) diff --git a/plugins/acp/acp_plugin_gamesdk/acp_plugin.py b/plugins/acp/acp_plugin_gamesdk/acp_plugin.py index f9fe020c..9faa771d 100644 --- a/plugins/acp/acp_plugin_gamesdk/acp_plugin.py +++ b/plugins/acp/acp_plugin_gamesdk/acp_plugin.py @@ -104,7 +104,6 @@ def _search_agents_executable(self,reasoning: str) -> Tuple[FunctionResultStatus agents = self.acp_client.browse_agents(self.cluster) - print(f"Agents: {agents}") if not agents: print("No agents found") return FunctionResultStatus.FAILED, "No other trading agents found in the system. Please try again later when more agents are available.", {} From 9343fbae238e7d1f01848a18ea3a1dcba6f8c0e5 Mon Sep 17 00:00:00 2001 From: StevenSF1998 Date: Tue, 25 Mar 2025 18:05:08 +0800 Subject: [PATCH 006/108] temp fix --- src/game_sdk/game/agent.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/game_sdk/game/agent.py b/src/game_sdk/game/agent.py index ba395308..e30d30df 100644 --- a/src/game_sdk/game/agent.py +++ b/src/game_sdk/game/agent.py @@ -233,7 +233,7 @@ def _get_action( function_result.model_dump( exclude={'info'}) if function_result else None ), - "observations": self.observation, + #"observations": self.observation, "version": "v2", } From 75f9f1c4812692aabc6f8b3692095c1d6ad244d8 Mon Sep 17 00:00:00 2001 From: StevenSF1998 Date: Wed, 26 Mar 2025 15:38:31 +0800 Subject: [PATCH 007/108] complete ACP flow --- plugins/acp/acp_plugin_gamesdk/acp_client.py | 29 ++++--- plugins/acp/acp_plugin_gamesdk/acp_plugin.py | 85 ++++++++++---------- plugins/acp/acp_plugin_gamesdk/acp_token.py | 43 +++++----- plugins/acp/examples/test_buyer.py | 25 +++--- plugins/acp/examples/test_seller.py | 25 +++--- 5 files changed, 98 insertions(+), 109 deletions(-) diff --git a/plugins/acp/acp_plugin_gamesdk/acp_client.py b/plugins/acp/acp_plugin_gamesdk/acp_client.py index 3fb7afea..cf075d70 100644 --- a/plugins/acp/acp_plugin_gamesdk/acp_client.py +++ b/plugins/acp/acp_plugin_gamesdk/acp_client.py @@ -52,20 +52,19 @@ def browse_agents(self, cluster: Optional[str] = None) -> List[AcpAgent]: ] def create_job(self, provider_address: str, price: float, job_description: str) -> int: - expired_at = datetime.now() + timedelta(days=1) + expire_at = datetime.now() + timedelta(days=1) tx_result = self.acp_token.create_job( provider_address=provider_address, - expired_at=expired_at + expire_at=expire_at ) - job_id = tx_result["job_id"] - + job_id = tx_result["jobId"] memo_response = self.acp_token.create_memo( job_id=job_id, content=job_description, memo_type=MemoType.MESSAGE, - is_private=False, - phase=AcpJobPhases.NEGOTIOATION + is_secured=False, + next_phase=AcpJobPhases.NEGOTIOATION ) payload = { @@ -74,7 +73,7 @@ def create_job(self, provider_address: str, price: float, job_description: str) "providerAddress": provider_address, "description": job_description, "price": price, - "expiredAt": expired_at.isoformat() + "expiredAt": expire_at.isoformat() } requests.post( @@ -97,16 +96,16 @@ def response_job(self, job_id: int, accept: bool, memo_id: int, reasoning: str): job_id=job_id, content=f"Job {job_id} accepted. {reasoning}", memo_type=MemoType.MESSAGE, - is_private=False, - phase=AcpJobPhases.TRANSACTION + is_secured=False, + next_phase=AcpJobPhases.TRANSACTION ) else: return self.acp_token.create_memo( job_id=job_id, content=f"Job {job_id} rejected. {reasoning}", memo_type=MemoType.MESSAGE, - is_private=False, - phase=AcpJobPhases.REJECTED + is_secured=False, + next_phase=AcpJobPhases.REJECTED ) def make_payment(self, job_id: int, amount: float, memo_id: int, reason: str): @@ -121,8 +120,8 @@ def make_payment(self, job_id: int, amount: float, memo_id: int, reason: str): job_id=job_id, content=f"Payment of {amount} made. {reason}", memo_type=MemoType.MESSAGE, - is_private=False, - phase=AcpJobPhases.EVALUATION + is_secured=False, + next_phase=AcpJobPhases.EVALUATION ) def deliver_job(self, job_id: int, deliverable: str, memo_id: int, reason: str): @@ -132,6 +131,6 @@ def deliver_job(self, job_id: int, deliverable: str, memo_id: int, reason: str): job_id=job_id, content=deliverable, memo_type=MemoType.MESSAGE, - is_private=False, - phase=AcpJobPhases.COMPLETED + is_secured=False, + next_phase=AcpJobPhases.COMPLETED ) diff --git a/plugins/acp/acp_plugin_gamesdk/acp_plugin.py b/plugins/acp/acp_plugin_gamesdk/acp_plugin.py index 9faa771d..6579b639 100644 --- a/plugins/acp/acp_plugin_gamesdk/acp_plugin.py +++ b/plugins/acp/acp_plugin_gamesdk/acp_plugin.py @@ -78,7 +78,6 @@ def get_environment(_e, __) -> Dict[str, Any]: instruction=data.get("instructions") if data else None ) - # print(json.dumps(vars(data), indent=2, default=str)) return data @property @@ -105,7 +104,6 @@ def _search_agents_executable(self,reasoning: str) -> Tuple[FunctionResultStatus agents = self.acp_client.browse_agents(self.cluster) if not agents: - print("No agents found") return FunctionResultStatus.FAILED, "No other trading agents found in the system. Please try again later when more agents are available.", {} return FunctionResultStatus.DONE, json.dumps({ @@ -160,8 +158,8 @@ def initiate_job(self) -> Function: executable=self._initiate_job_executable ) - def _initiate_job_executable(self, args: Dict, _: Any) -> Tuple[FunctionResultStatus, str, dict]: - if not args.get("price"): + def _initiate_job_executable(self, sellerWalletAddress: str, price: str, reasoning: str, serviceRequirements: str) -> Tuple[FunctionResultStatus, str, dict]: + if not price: return FunctionResultStatus.FAILED, "Missing price - specify how much you're offering per unit", {} try: @@ -170,20 +168,19 @@ def _initiate_job_executable(self, args: Dict, _: Any) -> Tuple[FunctionResultSt if state["jobs"]["active"]["asABuyer"]: return FunctionResultStatus.FAILED, "You already have an active job as a buyer", {} - # ... Rest of validation logic ... - + job_id = self.acp_client.create_job( - args["sellerWalletAddress"], - float(args["price"]), - args["serviceRequirements"] + sellerWalletAddress, + float(price), + serviceRequirements ) return FunctionResultStatus.DONE, json.dumps({ "jobId": job_id, - "sellerWalletAddress": args["sellerWalletAddress"], - "price": float(args["price"]), - "serviceRequirements": args["serviceRequirements"], + "sellerWalletAddress": sellerWalletAddress, + "price": float(price), + "serviceRequirements": serviceRequirements, "timestamp": datetime.now().timestamp(), }), {} except Exception as e: @@ -214,21 +211,21 @@ def respond_job(self) -> Function: executable=self._respond_job_executable ) - def _respond_job_executable(self, args: Dict, _: Any) -> Tuple[FunctionResultStatus, str, dict]: - if not args.get("jobId"): + def _respond_job_executable(self, jobId: str, decision: str, reasoning: str) -> Tuple[FunctionResultStatus, str, dict]: + if not jobId: return FunctionResultStatus.FAILED, "Missing job ID - specify which job you're responding to", {} - if not args.get("decision") or args["decision"] not in ["ACCEPT", "REJECT"]: + if not decision or decision not in ["ACCEPT", "REJECT"]: return FunctionResultStatus.FAILED, "Invalid decision - must be either 'ACCEPT' or 'REJECT'", {} - if not args.get("reasoning"): + if not reasoning: return FunctionResultStatus.FAILED, "Missing reasoning - explain why you made this decision", {} try: state = self.get_acp_state() job = next( - (c for c in state["jobs"]["active"]["asASeller"] if c["jobId"] == int(args["jobId"])), + (c for c in state["jobs"]["active"]["asASeller"] if c["jobId"] == int(jobId)), None ) @@ -239,15 +236,15 @@ def _respond_job_executable(self, args: Dict, _: Any) -> Tuple[FunctionResultSta return FunctionResultStatus.FAILED, f"Cannot respond - job is in '{job['phase']}' phase, must be in 'request' phase", {} self.acp_client.response_job( - int(args["jobId"]), - args["decision"] == "ACCEPT", + int(jobId), + decision == "ACCEPT", job["memo"][0]["id"], - args["reasoning"] + reasoning ) return FunctionResultStatus.DONE, json.dumps({ - "jobId": args["jobId"], - "decision": args["decision"], + "jobId": jobId, + "decision": decision, "timestamp": datetime.now().timestamp() }), {} except Exception as e: @@ -278,21 +275,21 @@ def pay_job(self) -> Function: executable=self._pay_job_executable ) - def _pay_job_executable(self, args: Dict, _: Any) -> Tuple[FunctionResultStatus, str, dict]: - if not args.get("jobId"): + def _pay_job_executable(self, jobId: str, amount: str, reasoning: str) -> Tuple[FunctionResultStatus, str, dict]: + if not jobId: return FunctionResultStatus.FAILED, "Missing job ID - specify which job you're paying for", {} - if not args.get("amount"): + if not amount: return FunctionResultStatus.FAILED, "Missing amount - specify how much you're paying", {} - if not args.get("reasoning"): + if not reasoning: return FunctionResultStatus.FAILED, "Missing reasoning - explain why you're making this payment", {} try: state = self.get_acp_state() job = next( - (c for c in state["jobs"]["active"]["asABuyer"] if c["jobId"] == int(args["jobId"])), + (c for c in state["jobs"]["active"]["asABuyer"] if c["jobId"] == int(jobId)), None ) @@ -304,15 +301,15 @@ def _pay_job_executable(self, args: Dict, _: Any) -> Tuple[FunctionResultStatus, self.acp_client.make_payment( - int(args["jobId"]), - float(args["amount"]), + int(jobId), + float(amount), job["memo"][0]["id"], - args["reasoning"] + reasoning ) return FunctionResultStatus.DONE, json.dumps({ - "jobId": args["jobId"], - "amountPaid": args["amount"], + "jobId": jobId, + "amountPaid": amount, "timestamp": datetime.now().timestamp() }), {} except Exception as e: @@ -348,21 +345,21 @@ def deliver_job(self) -> Function: executable=self._deliver_job_executable ) - def _deliver_job_executable(self, args: Dict, _: Any) -> Tuple[FunctionResultStatus, str, dict]: - if not args.get("jobId"): + def _deliver_job_executable(self, jobId: str, deliverableType: str, deliverable: str, reasoning: str) -> Tuple[FunctionResultStatus, str, dict]: + if not jobId: return FunctionResultStatus.FAILED, "Missing job ID - specify which job you're delivering for", {} - if not args.get("reasoning"): + if not reasoning: return FunctionResultStatus.FAILED, "Missing reasoning - explain why you're making this delivery", {} - if not args.get("deliverable"): + if not deliverable: return FunctionResultStatus.FAILED, "Missing deliverable - specify what you're delivering", {} try: state = self.get_acp_state() job = next( - (c for c in state["jobs"]["active"]["asASeller"] if c["jobId"] == int(args["jobId"])), + (c for c in state["jobs"]["active"]["asASeller"] if c["jobId"] == int(jobId)), None ) @@ -381,21 +378,21 @@ def _deliver_job_executable(self, args: Dict, _: Any) -> Tuple[FunctionResultSta return FunctionResultStatus.FAILED, "Cannot deliver - you should be producing the deliverable first before delivering it", {} deliverable = { - "type": args["deliverableType"], - "value": args["deliverable"] + "type": deliverableType, + "value": deliverable } self.acp_client.deliver_job( - int(args["jobId"]), - deliverable, + int(jobId), + json.dumps(deliverable), job["memo"][0]["id"], - args["reasoning"] + reasoning ) return FunctionResultStatus.DONE, json.dumps({ "status": "success", - "jobId": args["jobId"], - "deliverable": args["deliverable"], + "jobId": jobId, + "deliverable": deliverable, "timestamp": datetime.now().timestamp() }), {} except Exception as e: diff --git a/plugins/acp/acp_plugin_gamesdk/acp_token.py b/plugins/acp/acp_plugin_gamesdk/acp_token.py index a3aab5b6..0fb95bb9 100644 --- a/plugins/acp/acp_plugin_gamesdk/acp_token.py +++ b/plugins/acp/acp_plugin_gamesdk/acp_token.py @@ -1,5 +1,6 @@ import asyncio from enum import IntEnum +from time import time from typing import Optional, Tuple, TypedDict, List from datetime import datetime from web3 import Web3 @@ -62,7 +63,7 @@ def get_contract_address(self) -> str: def get_wallet_address(self) -> str: return self.account.address - async def create_job( + def create_job( self, provider_address: str, expire_at: datetime @@ -83,7 +84,8 @@ async def create_job( transaction, self.account.key ) - tx_hash = self.web3.eth.send_raw_transaction(signed_txn.rawTransaction) + + tx_hash = self.web3.eth.send_raw_transaction(signed_txn.raw_transaction) receipt = self.web3.eth.wait_for_transaction_receipt(tx_hash) # Get job ID from event logs @@ -98,7 +100,7 @@ async def create_job( print(f"Error creating job: {error}") raise Exception("Failed to create job") - async def approve_allowance(self, price_in_wei: int) -> str: + def approve_allowance(self, price_in_wei: int) -> str: try: erc20_contract = self.web3.eth.contract( address=self.virtuals_token_address, @@ -126,7 +128,7 @@ async def approve_allowance(self, price_in_wei: int) -> str: transaction, self.account.key ) - tx_hash = self.web3.eth.send_raw_transaction(signed_txn.rawTransaction) + tx_hash = self.web3.eth.send_raw_transaction(signed_txn.raw_transaction) self.web3.eth.wait_for_transaction_receipt(tx_hash) return tx_hash.hex() @@ -134,7 +136,7 @@ async def approve_allowance(self, price_in_wei: int) -> str: print(f"Error approving allowance: {error}") raise Exception("Failed to approve allowance") - async def create_memo( + def create_memo( self, job_id: int, content: str, @@ -146,11 +148,11 @@ async def create_memo( while retries > 0: try: transaction = self.contract.functions.createMemo( - job_id, - content, - memo_type, - is_secured, - next_phase + jobId = job_id, + content = content, + memoType = memo_type, + isSecured = is_secured, + nextPhase = next_phase ).build_transaction({ 'from': self.account.address, 'nonce': self.web3.eth.get_transaction_count(self.account.address), @@ -160,7 +162,7 @@ async def create_memo( transaction, self.account.key ) - tx_hash = self.web3.eth.send_raw_transaction(signed_txn.rawTransaction) + tx_hash = self.web3.eth.send_raw_transaction(signed_txn.raw_transaction) receipt = self.web3.eth.wait_for_transaction_receipt(tx_hash) # Get memo ID from event logs @@ -174,11 +176,11 @@ async def create_memo( except Exception as error: print(f"Error creating memo: {error}") retries -= 1 - await asyncio.sleep(2 * (3 - retries)) + asyncio.sleep(2 * (3 - retries)) raise Exception("Failed to create memo") - async def sign_memo( + def sign_memo( self, memo_id: int, is_approved: bool, @@ -200,18 +202,19 @@ async def sign_memo( transaction, self.account.key ) - tx_hash = self.web3.eth.send_raw_transaction(signed_txn.rawTransaction) + + tx_hash = self.web3.eth.send_raw_transaction(signed_txn.raw_transaction) self.web3.eth.wait_for_transaction_receipt(tx_hash) return tx_hash.hex() except Exception as error: print(f"Error signing memo: {error}") retries -= 1 - await asyncio.sleep(2 * (3 - retries)) + asyncio.sleep(2 * (3 - retries)) raise Exception("Failed to sign memo") - async def set_budget(self, job_id: int, budget: int) -> str: + def set_budget(self, job_id: int, budget: int) -> str: try: transaction = self.contract.functions.setBudget( job_id, @@ -225,7 +228,7 @@ async def set_budget(self, job_id: int, budget: int) -> str: transaction, self.account.key ) - tx_hash = self.web3.eth.send_raw_transaction(signed_txn.rawTransaction) + tx_hash = self.web3.eth.send_raw_transaction(signed_txn.raw_transaction) self.web3.eth.wait_for_transaction_receipt(tx_hash) return tx_hash.hex() @@ -233,7 +236,7 @@ async def set_budget(self, job_id: int, budget: int) -> str: print(f"Error setting budget: {error}") raise Exception("Failed to set budget") - async def get_job(self, job_id: int) -> Optional[IJob]: + def get_job(self, job_id: int) -> Optional[IJob]: try: job_data = self.contract.functions.jobs(job_id).call() @@ -255,7 +258,7 @@ async def get_job(self, job_id: int) -> Optional[IJob]: print(f"Error getting job: {error}") raise Exception("Failed to get job") - async def get_memo_by_job( + def get_memo_by_job( self, job_id: int, memo_type: Optional[MemoType] = None @@ -272,7 +275,7 @@ async def get_memo_by_job( print(f"Error getting memo: {error}") raise Exception("Failed to get memo") - async def get_memos_for_phase( + def get_memos_for_phase( self, job_id: int, phase: int, diff --git a/plugins/acp/examples/test_buyer.py b/plugins/acp/examples/test_buyer.py index f1ef3d6d..27114149 100644 --- a/plugins/acp/examples/test_buyer.py +++ b/plugins/acp/examples/test_buyer.py @@ -1,11 +1,8 @@ -from typing import Any, Dict -from game_sdk.game.agent import Agent, Session, WorkerConfig -from game_sdk.game.custom_types import Function, FunctionResult, FunctionResultStatus -import sys -import os -sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), "../../../"))) -from plugins.acp.acp_plugin_gamesdk.acp_plugin import AcpPlugin, AdNetworkPluginOptions -from plugins.acp.acp_plugin_gamesdk.acp_token import AcpToken +from typing import Any,Tuple +from game_sdk.game.agent import Agent, WorkerConfig +from game_sdk.game.custom_types import Function, FunctionResultStatus +from acp_plugin_gamesdk.acp_plugin import AcpPlugin, AdNetworkPluginOptions +from acp_plugin_gamesdk.acp_token import AcpToken def ask_question(query: str) -> str: return input(query) @@ -16,20 +13,18 @@ def main(): api_key="xxx", acp_token_client=AcpToken( "xxx", - "base_sepolia" # Assuming this is the chain identifier + "wss://base-sepolia.drpc.org" # Chain RPC ) ) ) def get_agent_state(_: Any, _e: Any) -> dict: state = acp_plugin.get_acp_state() + print(f"State: {state}") return state - def post_tweet(content: str, reasoning: str) -> FunctionResult: - return FunctionResult( - FunctionResultStatus.DONE, - "Tweet has been posted" - ) + def post_tweet(content: str, reasoning: str) -> Tuple[FunctionResultStatus, str, dict]: + return FunctionResultStatus.DONE, "Tweet has been posted", {} core_worker = WorkerConfig( id="core-worker", @@ -51,6 +46,7 @@ def post_tweet(content: str, reasoning: str) -> FunctionResult: } ], executable=post_tweet + #executable=post_tweet_function #Can be imported from twitter plugin ) ], get_state_fn=get_agent_state @@ -71,6 +67,7 @@ def post_tweet(content: str, reasoning: str) -> FunctionResult: get_agent_state_fn=get_agent_state ) + agent.compile() while True: diff --git a/plugins/acp/examples/test_seller.py b/plugins/acp/examples/test_seller.py index 1b6c7f5a..ddbbd31f 100644 --- a/plugins/acp/examples/test_seller.py +++ b/plugins/acp/examples/test_seller.py @@ -1,26 +1,19 @@ -from typing import List, Dict, Any, Optional,Tuple - -from web3 import Web3 -import sys -import os -sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), "../../../"))) -from plugins.acp.acp_plugin_gamesdk import acp_plugin -from plugins.acp.acp_plugin_gamesdk.acp_plugin import AcpPlugin, AdNetworkPluginOptions -from plugins.acp.acp_plugin_gamesdk.acp_token import AcpToken -from game_sdk.game.custom_types import Function, FunctionResult, FunctionResultStatus +from typing import Any, Tuple +from acp_plugin_gamesdk.acp_plugin import AcpPlugin, AdNetworkPluginOptions +from acp_plugin_gamesdk.acp_token import AcpToken +from game_sdk.game.custom_types import Function, FunctionResultStatus from game_sdk.game.agent import Agent, WorkerConfig def ask_question(query: str) -> str: return input(query) - -def test(): +def main(): acp_plugin = AcpPlugin( options=AdNetworkPluginOptions( api_key="xxx", acp_token_client=AcpToken( "xxx", - "base_sepolia" # Assuming this is the chain identifier + "wss://base-sepolia.drpc.org" # Chain RPC ) ) ) @@ -37,10 +30,10 @@ def generate_meme(description: str, jobId: str, reasoning: str) -> Tuple[Functio state = acp_plugin.get_acp_state() job = next( - (j for j in state.jobs.active.as_a_seller if j.job_id == int(jobId)), + (j for j in state.get('jobs').get('active').get('asASeller') if j.get('jobId') == int(jobId)), None ) - + if not job: return FunctionResultStatus.FAILED, f"Job {jobId} is invalid. Should only respond to active as a seller job.", {} @@ -104,4 +97,4 @@ def generate_meme(description: str, jobId: str, reasoning: str) -> Tuple[Functio ask_question("\nPress any key to continue...\n") if __name__ == "__main__": - test() + main() From 2e6d480212a947e815eb75ba3b02b079429980d9 Mon Sep 17 00:00:00 2001 From: Celeste Ang Date: Wed, 26 Mar 2025 05:50:20 -0400 Subject: [PATCH 008/108] [acp plugin] add plugin metadata yml --- plugins/acp/plugin_metadata.yml | 14 ++++++++++++++ 1 file changed, 14 insertions(+) create mode 100644 plugins/acp/plugin_metadata.yml diff --git a/plugins/acp/plugin_metadata.yml b/plugins/acp/plugin_metadata.yml new file mode 100644 index 00000000..20a2216e --- /dev/null +++ b/plugins/acp/plugin_metadata.yml @@ -0,0 +1,14 @@ +# General Information +plugin_name: "acp_plugin_gamesdk" +author: "Steven Lee Soon Fatt" +logo_url: "" +release_date: "2025-03" + +# Description +short_description: "ACP Plugin for Python SDK for GAME by Virtuals" +detailed_description: "This plugin provides an abstraction over Agent Commerce Protocol (ACP) capabilities for the GAME SDK. It allows agents to handle trading transactions and jobs between agents also interact via X." + +# Contact & Support +x_account_handle: "@GAME_Virtuals" +support_contact: "steven@virtuals.io" +community_link: "https://t.me/virtuals" From 21c670641b09827e7fe1493bab140a9ee7a83af7 Mon Sep 17 00:00:00 2001 From: Celeste Ang Date: Wed, 26 Mar 2025 05:50:44 -0400 Subject: [PATCH 009/108] [acp plugin] fix package name --- plugins/acp/pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/acp/pyproject.toml b/plugins/acp/pyproject.toml index 1b1d8644..a5775b21 100644 --- a/plugins/acp/pyproject.toml +++ b/plugins/acp/pyproject.toml @@ -1,5 +1,5 @@ [project] -name = "telegram-plugin-gamesdk" +name = "acp_plugin_gamesdk" version = "0.1.0" description = "ACP Plugin for Python SDK for GAME by Virtuals" authors = [ From fa20512277868cfd0862e54c7110c39c7a8f6e1a Mon Sep 17 00:00:00 2001 From: Celeste Ang Date: Wed, 26 Mar 2025 06:23:00 -0400 Subject: [PATCH 010/108] [acp plugin] readme initial commit --- docs/imgs/ACP-banner.jpeg | Bin 0 -> 188041 bytes plugins/acp/README.md | 206 ++++++++++++++++++-------------------- 2 files changed, 96 insertions(+), 110 deletions(-) create mode 100644 docs/imgs/ACP-banner.jpeg diff --git a/docs/imgs/ACP-banner.jpeg b/docs/imgs/ACP-banner.jpeg new file mode 100644 index 0000000000000000000000000000000000000000..b01fb6f25b67186baa2a7f137ad50b0ba292d975 GIT binary patch literal 188041 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