-
Notifications
You must be signed in to change notification settings - Fork 3
Feat/twitter client #18
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
637a889
9429c2d
529f596
f00a78a
37475fc
61ba3fa
782c30e
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -18,3 +18,5 @@ __pycache__/ | |
|
||
# macOS system files | ||
.DS_Store | ||
|
||
.myenv/ |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,4 +1,11 @@ | ||
WHITELISTED_WALLET_PRIVATE_KEY=<whitelisted-wallet-private-key> | ||
BUYER_WALLET_PRIVATE_KEY=<buyer-wallet-private-key> | ||
BUYER_AGENT_WALLET_ADDRESS=<buyer-agent-wallet-address> | ||
BUYER_GAME_TWITTER_ACCESS_TOKEN=<buyer-game-twitter-access-token> | ||
|
||
SELLER_WALLET_PRIVATE_KEY=<seller-wallet-private-key> | ||
SELLER_AGENT_WALLET_ADDRESS=<seller-agent-wallet-address> | ||
EVALUATOR_AGENT_WALLET_ADDRESS=<evaluator-agent-wallet-address> | ||
SELLER_GAME_TWITTER_ACCESS_TOKEN=<seller-game-twitter-access-token> | ||
|
||
EVALUATOR_WALLET_PRIVATE_KEY=<evaluator-wallet-private-key> | ||
EVALUATOR_AGENT_WALLET_ADDRESS=<evaluator-agent-wallet-address> | ||
EVALUATOR_GAME_TWITTER_ACCESS_TOKEN=<evaluator-game-twitter-access-token> | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. evaluator agent need to post tweets? |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -23,12 +23,12 @@ def on_new_task(job: ACPJob): | |
break | ||
elif job.phase == ACPJobPhase.COMPLETED: | ||
print("Job completed", job) | ||
|
||
acp = VirtualsACP( | ||
wallet_private_key=env.WHITELISTED_WALLET_PRIVATE_KEY, | ||
wallet_private_key=env.BUYER_WALLET_PRIVATE_KEY, | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. don't think this is needed |
||
agent_wallet_address=env.BUYER_AGENT_WALLET_ADDRESS, | ||
config=BASE_SEPOLIA_CONFIG, | ||
on_new_task=on_new_task | ||
on_new_task=on_new_task, | ||
game_twitter_access_token=env.BUYER_GAME_TWITTER_ACCESS_TOKEN | ||
) | ||
|
||
# Browse available agents based on a keyword and cluster name | ||
|
@@ -45,7 +45,7 @@ def on_new_task(job: ACPJob): | |
# Reference: (./images/specify_requirement_toggle_switch.png) | ||
service_requirement={"<your_schema_field>": "Help me to generate a flower meme."}, | ||
evaluator_address=env.EVALUATOR_AGENT_WALLET_ADDRESS, | ||
expired_at=datetime.now() + timedelta(days=1) | ||
expired_at=datetime.now() + timedelta(days=1), | ||
) | ||
|
||
print(f"Job {job_id} initiated") | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -13,15 +13,14 @@ def evaluator(): | |
def on_evaluate(job: ACPJob): | ||
# Find the deliverable memo | ||
for memo in job.memos: | ||
print(memo.next_phase, ACPJobPhase.COMPLETED) | ||
if memo.next_phase == ACPJobPhase.COMPLETED: | ||
print("Evaluating deliverable", job.id) | ||
job.evaluate(True) | ||
break | ||
|
||
# Initialize the ACP client | ||
acp_client = VirtualsACP( | ||
wallet_private_key=env.WHITELISTED_WALLET_PRIVATE_KEY, | ||
wallet_private_key=env.EVALUATOR_WALLET_PRIVATE_KEY, | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. don't think this is needed |
||
agent_wallet_address=env.EVALUATOR_AGENT_WALLET_ADDRESS, | ||
config=BASE_SEPOLIA_CONFIG, | ||
on_evaluate=on_evaluate | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -32,10 +32,11 @@ def on_new_task(job: ACPJob): | |
|
||
# Initialize the ACP client | ||
acp_client = VirtualsACP( | ||
wallet_private_key=env.WHITELISTED_WALLET_PRIVATE_KEY, | ||
wallet_private_key=env.SELLER_WALLET_PRIVATE_KEY, | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. don't think this is needed |
||
agent_wallet_address=env.SELLER_AGENT_WALLET_ADDRESS, | ||
config=BASE_SEPOLIA_CONFIG, | ||
on_new_task=on_new_task | ||
on_new_task=on_new_task, | ||
game_twitter_access_token=env.SELLER_GAME_TWITTER_ACCESS_TOKEN | ||
) | ||
|
||
# Keep the script running to listen for new tasks | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,3 +1,7 @@ | ||
WHITELISTED_WALLET_PRIVATE_KEY=<whitelisted-wallet-private-key> | ||
BUYER_WALLET_PRIVATE_KEY=<buyer-wallet-private-key> | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. can keep the current one? we just use the same whitelisted dev key |
||
BUYER_AGENT_WALLET_ADDRESS=<buyer-agent-wallet-address> | ||
SELLER_AGENT_WALLET_ADDRESS=<seller-agent-wallet-address> | ||
BUYER_GAME_TWITTER_ACCESS_TOKEN=<buyer-game-twitter-access-token> | ||
|
||
SELLER_WALLET_PRIVATE_KEY=<seller-wallet-private-key> | ||
SELLER_AGENT_WALLET_ADDRESS=<seller-agent-wallet-address> | ||
SELLER_GAME_TWITTER_ACCESS_TOKEN=<seller-game-twitter-access-token> |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -27,7 +27,6 @@ def on_new_task(job: ACPJob): | |
print("Job completed", job) | ||
|
||
def on_evaluate(job: ACPJob): | ||
print("Evaluation function called", job.memos) | ||
# Find the deliverable memo | ||
for memo in job.memos: | ||
if memo.next_phase == ACPJobPhase.COMPLETED: | ||
|
@@ -36,11 +35,12 @@ def on_evaluate(job: ACPJob): | |
break | ||
|
||
acp = VirtualsACP( | ||
wallet_private_key=env.WHITELISTED_WALLET_PRIVATE_KEY, | ||
wallet_private_key=env.BUYER_WALLET_PRIVATE_KEY, | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. don't think this is needed |
||
agent_wallet_address=env.BUYER_AGENT_WALLET_ADDRESS, | ||
config=BASE_SEPOLIA_CONFIG, | ||
on_new_task=on_new_task, | ||
on_evaluate=on_evaluate | ||
on_evaluate=on_evaluate, | ||
game_twitter_access_token=env.BUYER_GAME_TWITTER_ACCESS_TOKEN | ||
) | ||
|
||
# Browse available agents based on a keyword and cluster name | ||
|
@@ -58,7 +58,7 @@ def on_evaluate(job: ACPJob): | |
# Reference: (./images/specify_requirement_toggle_switch.png) | ||
service_requirement={"<your_schema_field>": "Help me to generate a flower meme."}, | ||
evaluator_address=env.BUYER_AGENT_WALLET_ADDRESS, | ||
expired_at=datetime.now() + timedelta(days=1) | ||
expired_at=datetime.now() + timedelta(days=1), | ||
) | ||
|
||
print(f"Job {job_id} initiated") | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -20,14 +20,17 @@ | |
from virtuals_acp.offering import ACPJobOffering | ||
from virtuals_acp.job import ACPJob | ||
from virtuals_acp.memo import ACPMemo | ||
import virtuals_tweepy | ||
|
||
class VirtualsACP: | ||
def __init__(self, | ||
wallet_private_key: str, | ||
agent_wallet_address: Optional[str] = None, | ||
config: Optional[ACPContractConfig] = DEFAULT_CONFIG, | ||
on_new_task: Optional[callable] = None, | ||
on_evaluate: Optional[callable] = None): | ||
on_evaluate: Optional[callable] = None, | ||
game_twitter_access_token: Optional[str] = None, | ||
): | ||
|
||
self.config = config | ||
self.w3 = Web3(Web3.HTTPProvider(config.rpc_url)) | ||
|
@@ -50,6 +53,12 @@ def __init__(self, | |
self.contract_manager = _ACPContractManager(self.w3, config, wallet_private_key) | ||
self.acp_api_url = config.acp_api_url | ||
|
||
self.game_twitter_client = None | ||
if (game_twitter_access_token): | ||
self.game_twitter_client = virtuals_tweepy.Client( | ||
game_twitter_access_token = game_twitter_access_token | ||
) | ||
|
||
# Socket.IO setup | ||
self.on_new_task = on_new_task | ||
self.on_evaluate = on_evaluate or self._default_on_evaluate | ||
|
@@ -79,6 +88,8 @@ def _on_evaluate(self, data): | |
acp_client=self, | ||
id=data["id"], | ||
provider_address=data["providerAddress"], | ||
client_address=data["clientAddress"], | ||
evaluator_address=data["evaluatorAddress"], | ||
memos=memos, | ||
phase=data["phase"], | ||
price=data["price"] | ||
|
@@ -98,11 +109,12 @@ def _on_new_task(self, data): | |
next_phase=memo["nextPhase"], | ||
) for memo in data["memos"]] | ||
|
||
|
||
job = ACPJob( | ||
acp_client=self, | ||
id=data["id"], | ||
provider_address=data["providerAddress"], | ||
client_address=data["clientAddress"], | ||
evaluator_address=data["evaluatorAddress"], | ||
memos=memos, | ||
phase=data["phase"], | ||
price=data["price"] | ||
|
@@ -176,6 +188,7 @@ def browse_agents(self, keyword: str, cluster: Optional[str] = None) -> List[IAC | |
provider_address=agent_data["walletAddress"], | ||
type=off["name"], | ||
price=off["price"], | ||
agent_twitter_handle=agent_data.get("twitterHandle"), | ||
requirementSchema=off.get("requirementSchema", None) | ||
) | ||
for off in agent_data.get("offerings", []) | ||
|
@@ -201,7 +214,8 @@ def initiate_job( | |
service_requirement: Union[Dict[str, Any], str], | ||
amount: float, | ||
evaluator_address: Optional[str] = None, | ||
expired_at: Optional[datetime] = None | ||
expired_at: Optional[datetime] = None, | ||
twitter_handle: Optional[str] = None | ||
) -> int: | ||
if expired_at is None: | ||
expired_at = datetime.now(timezone.utc) + timedelta(days=1) | ||
|
@@ -261,6 +275,25 @@ def initiate_job( | |
) | ||
print(f"Initial memo for job {job_id} created.") | ||
|
||
if (self.game_twitter_client): | ||
if (not twitter_handle): | ||
raise Exception("Provider twitter handle is required") | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. is this a good idea - what if users just don't want to use twitter client? possible to have a user flow which doesn't involve twitter? |
||
|
||
try: | ||
get_provider_user_fn = self.game_twitter_client.get_user | ||
provider_user = get_provider_user_fn(username=twitter_handle) | ||
user_id = provider_user.data.get("id", None) | ||
if (user_id is None): | ||
raise Exception(f"Unable to find user {twitter_handle} on Twitter") | ||
|
||
result =self.game_twitter_client.follow(user_id) | ||
|
||
if (not result.data.get("following")): | ||
raise Exception(f"Failed to follow {twitter_handle}") | ||
except Exception as e: | ||
print(f"Error following {twitter_handle}: {e}") | ||
raise e | ||
|
||
payload = { | ||
"jobId": job_id, | ||
"clientAddress": self.agent_address, | ||
|
@@ -281,16 +314,17 @@ def initiate_job( | |
"Content-Type": "application/json", | ||
} | ||
) | ||
#todo : move twitter logic here | ||
return job_id | ||
|
||
def respond_to_job_memo( | ||
self, | ||
job_id: int, | ||
memo_id: int, | ||
accept: bool, | ||
reason: Optional[str] = "" | ||
reason: Optional[str] = "", | ||
twitter_handle: Optional[str] = None | ||
) -> str: | ||
|
||
try: | ||
tx_hash = self.contract_manager.sign_memo(self.agent_address, memo_id, accept, reason or "") | ||
time.sleep(10) | ||
|
@@ -304,7 +338,27 @@ def respond_to_job_memo( | |
is_secured=False, | ||
next_phase=ACPJobPhase.TRANSACTION | ||
) | ||
#todo : move twitter logic here | ||
|
||
print(f"Responded to job {job_id} with memo {memo_id} and accept {accept} and reason {reason}") | ||
|
||
if (self.game_twitter_client and accept is True): | ||
try: | ||
if (not twitter_handle): | ||
raise Exception("Client twitter handle is required") | ||
|
||
get_user_fn = self.game_twitter_client.get_user | ||
user = get_user_fn(username=twitter_handle) | ||
user_id = user.data.get("id", None) | ||
if (not user_id): | ||
raise Exception(f"Unable to find user {twitter_handle} on Twitter") | ||
|
||
result =self.game_twitter_client.follow(user_id) | ||
|
||
if (not result.data.get("following")): | ||
raise Exception(f"Failed to follow {twitter_handle}") | ||
except Exception as e: | ||
raise e | ||
return tx_hash | ||
except Exception as e: | ||
print(f"Error in respond_to_job_memo: {e}") | ||
|
@@ -404,7 +458,7 @@ def get_active_jobs(self, page: int = 1, pageSize: int = 10) -> List["ACPJob"]: | |
|
||
|
||
def get_completed_jobs(self, page: int = 1, pageSize: int = 10) -> List["ACPJob"]: | ||
url = f"{self.acp_api_url}/jobs/completed?pagination[page]=${page}&pagination[pageSize]=${pageSize}" | ||
url = f"{self.acp_api_url}/jobs/completed?pagination[page]={page}&pagination[pageSize]={pageSize}" | ||
headers = { | ||
"wallet-address": self.agent_address | ||
} | ||
|
@@ -525,6 +579,46 @@ def get_memo_by_id(self, onchain_job_id: int, memo_id: int) -> 'ACPMemo': | |
|
||
except Exception as e: | ||
raise ACPApiError(f"Failed to get memo by ID: {e}") | ||
|
||
def get_agent(self, wallet_address: str) -> Optional[IACPAgent]: | ||
url = f"{self.acp_api_url}/agents?filters[walletAddress]={wallet_address}" | ||
|
||
try: | ||
response = requests.get(url) | ||
response.raise_for_status() | ||
data = response.json() | ||
|
||
agents_data = data.get("data", []) | ||
if not agents_data: | ||
return None | ||
|
||
agent_data = agents_data[0] | ||
|
||
offerings = [ | ||
ACPJobOffering( | ||
acp_client=self, | ||
provider_address=agent_data.get("walletAddress"), | ||
type=off["name"], | ||
agent_twitter_handle=agent_data.get("twitterHandle"), | ||
price=off["price"], | ||
requirementSchema=off.get("requirementSchema", None) | ||
) | ||
for off in agent_data.get("offerings", []) | ||
] | ||
|
||
return IACPAgent( | ||
id=agent_data["id"], | ||
name=agent_data.get("name"), | ||
description=agent_data.get("description"), | ||
wallet_address=Web3.to_checksum_address(agent_data.get("walletAddress")), | ||
offerings=offerings, | ||
twitter_handle=agent_data.get("twitterHandle") | ||
) | ||
|
||
except requests.exceptions.RequestException as e: | ||
raise ACPApiError(f"Failed to get agent: {e}") | ||
except Exception as e: | ||
raise ACPError(f"An unexpected error occurred while getting agent: {e}") | ||
|
||
# Rebuild the AcpJob model after VirtualsACP is defined | ||
ACPJob.model_rebuild() | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -3,16 +3,20 @@ | |
from pydantic import field_validator | ||
|
||
class EnvSettings(BaseSettings): | ||
WHITELISTED_WALLET_PRIVATE_KEY: Optional[str] = None | ||
BUYER_WALLET_PRIVATE_KEY: Optional[str] = None | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. i think we don't need this |
||
SELLER_WALLET_PRIVATE_KEY: Optional[str] = None | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. i think we don't need this |
||
EVALUATOR_WALLET_PRIVATE_KEY: Optional[str] = None | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. i think we don't need this |
||
BUYER_AGENT_WALLET_ADDRESS: Optional[str] = None | ||
SELLER_AGENT_WALLET_ADDRESS: Optional[str] = None | ||
EVALUATOR_AGENT_WALLET_ADDRESS: Optional[str] = None | ||
|
||
@field_validator("WHITELISTED_WALLET_PRIVATE_KEY") | ||
BUYER_GAME_TWITTER_ACCESS_TOKEN: Optional[str] = None | ||
SELLER_GAME_TWITTER_ACCESS_TOKEN: Optional[str] = None | ||
EVALUATOR_GAME_TWITTER_ACCESS_TOKEN: Optional[str] = None | ||
@field_validator("BUYER_WALLET_PRIVATE_KEY", "SELLER_WALLET_PRIVATE_KEY", "EVALUATOR_WALLET_PRIVATE_KEY") | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. i think we don't need this |
||
@classmethod | ||
def strip_0x_prefix(cls, v: str) -> str: | ||
if v and v.startswith("0x"): | ||
raise ValueError("WHITELISTED_WALLET_PRIVATE_KEY must not start with '0x'. Please remove it.") | ||
raise ValueError("WALLET_PRIVATE_KEY must not start with '0x'. Please remove it.") | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. i think we don't need this |
||
return v | ||
|
||
@field_validator("BUYER_AGENT_WALLET_ADDRESS", "SELLER_AGENT_WALLET_ADDRESS", "EVALUATOR_AGENT_WALLET_ADDRESS") | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
can keep the current envs? we can just use the same whitelisted dev key for all agents