Skip to content

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

Open
wants to merge 7 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -18,3 +18,5 @@ __pycache__/

# macOS system files
.DS_Store

.myenv/
11 changes: 9 additions & 2 deletions examples/acp_base/external_evaluation/.env.example
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>
Copy link
Contributor

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

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>
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

evaluator agent need to post tweets?

8 changes: 4 additions & 4 deletions examples/acp_base/external_evaluation/buyer.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Copy link
Contributor

Choose a reason for hiding this comment

The 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
Expand All @@ -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")
Expand Down
3 changes: 1 addition & 2 deletions examples/acp_base/external_evaluation/evaluator.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Copy link
Contributor

Choose a reason for hiding this comment

The 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
Expand Down
5 changes: 3 additions & 2 deletions examples/acp_base/external_evaluation/seller.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Copy link
Contributor

Choose a reason for hiding this comment

The 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
Expand Down
8 changes: 6 additions & 2 deletions examples/acp_base/self_evaluation/.env.example
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>
Copy link
Contributor

Choose a reason for hiding this comment

The 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>
8 changes: 4 additions & 4 deletions examples/acp_base/self_evaluation/buyer.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand All @@ -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,
Copy link
Contributor

Choose a reason for hiding this comment

The 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
Expand All @@ -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")
Expand Down
5 changes: 3 additions & 2 deletions examples/acp_base/self_evaluation/seller.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,10 +33,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,
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
Expand Down
1 change: 1 addition & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ python-socketio = "^5.11.1"
websocket-client = "^1.7.0"
jsonschema = "^4.22.0"
pydantic-settings = "^2.0"
virtuals-tweepy = "^0.1.6"

[build-system]
requires = ["poetry-core"]
Expand Down
106 changes: 100 additions & 6 deletions virtuals_acp/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -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))
Expand All @@ -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
Expand Down Expand Up @@ -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"]
Expand All @@ -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"]
Expand Down Expand Up @@ -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", [])
Expand All @@ -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)
Expand Down Expand Up @@ -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")
Copy link
Contributor

Choose a reason for hiding this comment

The 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,
Expand All @@ -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)
Expand All @@ -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}")
Expand Down Expand Up @@ -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
}
Expand Down Expand Up @@ -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()
Expand Down
12 changes: 8 additions & 4 deletions virtuals_acp/env.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Copy link
Contributor

Choose a reason for hiding this comment

The 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
Copy link
Contributor

Choose a reason for hiding this comment

The 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
Copy link
Contributor

Choose a reason for hiding this comment

The 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")
Copy link
Contributor

Choose a reason for hiding this comment

The 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.")
Copy link
Contributor

Choose a reason for hiding this comment

The 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")
Expand Down
Loading