Skip to content

Commit

Permalink
Farcaster agent (#142)
Browse files Browse the repository at this point in the history
* Added first version of Farcaster agent

* Added placeholder for farcaster_client.py

* Agent posting on Farcaster

* formatting fixes

* Last fixes before review

* Last fixes before review (2)

* Added one PR comment

* Generating tweets from latest bets on Omen from think-thoroughly agent

* Adjusted prompts for 1st person

* Added twitter integration

* Fixing mypy

* Added lock file

* Implemented PR comments

* Fixed isort

* Fixed autoflake

* Added new PMAT version

* Formatting

* Updated lock file

* Updated lock file (2)

* Another round of PR comments

* Updated .env.example with Twitter credentials
  • Loading branch information
gabrielfior authored May 9, 2024
1 parent 52e5584 commit 95b3f2f
Show file tree
Hide file tree
Showing 13 changed files with 533 additions and 136 deletions.
8 changes: 7 additions & 1 deletion .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,10 @@ TAVILY_API_KEY=
BET_FROM_PRIVATE_KEY=
LANGFUSE_SECRET_KEY=
LANGFUSE_PUBLIC_KEY=
LANGFUSE_HOST=
LANGFUSE_HOST=
FARCASTER_PRIVATE_KEY=
TWITTER_ACCESS_TOKEN=
TWITTER_ACCESS_TOKEN_SECRET=
TWITTER_BEARER_TOKEN=
TWITTER_API_KEY=
TWITTER_API_KEY_SECRET=
308 changes: 174 additions & 134 deletions poetry.lock

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
Farcaster agent URL
https://warpcast.com/gnosis-ai-agent/

FID (Farcaster ID) - 518226

Public key - 0xF0AE06f9ca8bc3DC4C5C77CEC6F14D74AB21F782
Empty file.
62 changes: 62 additions & 0 deletions prediction_market_agent/agents/autogen_general_agent/deploy.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
from datetime import timedelta

from loguru import logger
from prediction_market_agent_tooling.config import PrivateCredentials
from prediction_market_agent_tooling.deploy.agent import DeployableTraderAgent
from prediction_market_agent_tooling.markets.data_models import Bet
from prediction_market_agent_tooling.markets.markets import MarketType
from prediction_market_agent_tooling.tools.utils import utcnow

from prediction_market_agent.agents.autogen_general_agent.social_agent import (
build_social_media_text,
)
from prediction_market_agent.agents.autogen_general_agent.social_media.abstract_handler import (
AbstractSocialMediaHandler,
)
from prediction_market_agent.agents.autogen_general_agent.social_media.farcaster_handler import (
FarcasterHandler,
)
from prediction_market_agent.agents.autogen_general_agent.social_media.twitter_handler import (
TwitterHandler,
)
from prediction_market_agent.utils import APIKeys


class DeployableSocialMediaAgent(DeployableTraderAgent):
model: str = "gpt-4-turbo-2024-04-09"
social_media_handlers: list[AbstractSocialMediaHandler] = [
FarcasterHandler(),
TwitterHandler(),
]

def run(self, market_type: MarketType) -> None:
# It should post a message (cast) on each run.
# We just need one market to get latest bets.

bets = self.get_bets(market_type=market_type)
# If no bets available for the last 24h, we skip posting.
if not bets:
logger.info("No bets available from last day. No post will be created.")
return
tweet = build_social_media_text(market_type, bets)
self.post(tweet)

def get_bets(self, market_type: MarketType) -> list[Bet]:
better_address = PrivateCredentials.from_api_keys(APIKeys()).public_key
one_day_ago = utcnow() - timedelta(days=1)
return market_type.market_class.get_bets_made_since(
better_address=better_address, start_time=one_day_ago
)

def post(self, tweet: str | None) -> None:
if not tweet:
logger.info("No tweet was produced. Exiting.")
return

for handler in self.social_media_handlers:
handler.post(tweet)


if __name__ == "__main__":
agent = DeployableSocialMediaAgent()
agent.deploy_local(market_type=MarketType.OMEN, sleep_time=540, timeout=180)
20 changes: 20 additions & 0 deletions prediction_market_agent/agents/autogen_general_agent/prompts.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
INFLUENCER_PROMPT = """
You are an AI agent that places bets on future events.
You can find below a list of recent BETS that you have placed.
[BETS]
$BETS
Write one engaging tweet that attracts attention to the recent bets that the AI agent has placed,
in order to increase his audience that wants to follow his betting activity. Make sure to include
the outcome that the AI agent has placed a bet on.
Pick a single topic for the tweet.
You must not add any reasoning or additional explanation, simply output the tweet.
"""

CRITIC_PROMPT = """
Reflect and provide critique on the following tweet. \n\n $TWEET
Note that it should not include inappropriate language.
Note also that the tweet should not sound robotic, instead as human-like as possible.
Also make sure to ask the recipient for an improved version of the tweet and nothing else.
"""
154 changes: 154 additions & 0 deletions prediction_market_agent/agents/autogen_general_agent/social_agent.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,154 @@
from datetime import datetime
from enum import Enum
from string import Template
from typing import Any, Dict, Optional

import autogen
from autogen import AssistantAgent, UserProxyAgent
from autogen.cache import Cache
from prediction_market_agent_tooling.markets.data_models import Bet
from pydantic import BaseModel

from prediction_market_agent.agents.autogen_general_agent.prompts import (
CRITIC_PROMPT,
INFLUENCER_PROMPT,
)
from prediction_market_agent.utils import APIKeys


class BetInputPrompt(BaseModel):
title: str
boolean_outcome: bool
collateral_amount: float
creation_datetime: datetime

@staticmethod
def from_bet(bet: Bet) -> "BetInputPrompt":
return BetInputPrompt(
title=bet.market_question,
boolean_outcome=bet.outcome,
collateral_amount=bet.amount.amount,
creation_datetime=bet.created_time,
)


class AutogenAgentType(str, Enum):
WRITER = "writer"
CRITIC = "critic"
USER = "user"


def reflection_message(
recipient: UserProxyAgent,
messages: list[Dict[Any, Any]],
sender: AssistantAgent,
config: Optional[Any] = None,
) -> str:
reflect_prompt = Template(CRITIC_PROMPT).substitute(
TWEET=recipient.chat_messages_for_summary(sender)[-1]["content"]
)
return reflect_prompt


def build_llm_config(model: str) -> Dict[str, Any]:
keys = APIKeys()
return {
"config_list": [
{"model": model, "api_key": keys.openai_api_key.get_secret_value()}
],
}


def build_agents(model: str) -> Dict[AutogenAgentType, autogen.ConversableAgent]:
llm_config = build_llm_config(model)

user_proxy = autogen.UserProxyAgent(
name="User",
human_input_mode="NEVER",
is_termination_msg=lambda x: x.get("content", "").find("TERMINATE") >= 0,
code_execution_config={
"last_n_messages": 1,
"work_dir": "tasks",
"use_docker": False,
},
)

writer = autogen.AssistantAgent(
name="Writer",
llm_config=llm_config,
system_message="""
You are a professional influencer, known for your insightful and engaging tweets.
You transform complex concepts into compelling narratives.
You should improve the quality of the content based on the feedback from the user.
You must always return only the tweet.
""",
)

critic = autogen.AssistantAgent(
name="Critic",
llm_config=llm_config,
system_message="""
You are a critic, known for your thoroughness and commitment to standards.
Your task is to scrutinize content for any harmful elements or regulatory violations, ensuring
all materials align with required guidelines.
References to betting and gambling are allowed.
""",
)

return {
AutogenAgentType.CRITIC: critic,
AutogenAgentType.WRITER: writer,
AutogenAgentType.USER: user_proxy,
}


def build_social_media_text(model: str, bets: list[Bet]) -> str:
"""
Builds a tweet based on past betting activity from a given participant.
This function utilizes the writer and critic agents to generate the tweet content. It first initializes the
necessary agents based on the provided model. Then, it registers a chat between the writer and critic agents.
The tweet content is generated using a template that includes questions about each market's title and likelihood.
"""

agents = build_agents(model)
user_proxy, writer, critic = (
agents[AutogenAgentType.USER],
agents[AutogenAgentType.WRITER],
agents[AutogenAgentType.CRITIC],
)
user_proxy.register_nested_chats(
[
{
"recipient": critic,
"message": reflection_message,
"summary_method": "last_msg",
"max_turns": 1,
}
],
trigger=writer,
)

# ToDO - fetch agent reasoning from DB and construct better tweets
# See https://github.com/gnosis/prediction-market-agent/issues/150

task = Template(INFLUENCER_PROMPT).substitute(
BETS=[BetInputPrompt.from_bet(bet) for bet in bets]
)

# in case we trigger repeated runs, Cache makes it faster.
with Cache.disk(cache_seed=42) as cache:
# max_turns = the maximum number of turns for the chat between the two agents. One turn means one conversation round trip.
res = user_proxy.initiate_chat(
recipient=writer,
message=task,
max_turns=2,
summary_method="last_msg",
cache=cache,
)

tweet = res.summary
return str(
tweet
) # Casting needed as summary is of type any and no Pydantic support
Empty file.
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import typing as t
from abc import ABCMeta, abstractmethod


class AbstractSocialMediaHandler(metaclass=ABCMeta):
client: t.Any

@abstractmethod
def post(self, text: str) -> None:
pass
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
from farcaster import Warpcast
from loguru import logger

from prediction_market_agent.agents.autogen_general_agent.social_media.abstract_handler import (
AbstractSocialMediaHandler,
)
from prediction_market_agent.utils import SocialMediaAPIKeys


class FarcasterHandler(AbstractSocialMediaHandler):
def __init__(self) -> None:
api_keys = SocialMediaAPIKeys()
self.client = Warpcast(
private_key=api_keys.farcaster_private_key.get_secret_value()
)

def post(self, text: str) -> None:
cast = self.client.post_cast(text=text)
logger.info(f"Posted cast {cast.cast.text} - hash {cast.cast.hash}")
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import tweepy
from loguru import logger
from tweepy import Client

from prediction_market_agent.agents.autogen_general_agent.social_media.abstract_handler import (
AbstractSocialMediaHandler,
)
from prediction_market_agent.utils import SocialMediaAPIKeys


class TwitterHandler(AbstractSocialMediaHandler):
client: Client

def __init__(self) -> None:
keys = SocialMediaAPIKeys()

self.client = tweepy.Client(
keys.twitter_bearer_token.get_secret_value(),
keys.twitter_api_key.get_secret_value(),
keys.twitter_api_key_secret.get_secret_value(),
keys.twitter_access_token.get_secret_value(),
keys.twitter_access_token_secret.get_secret_value(),
)

def post(self, text: str) -> None:
response = self.client.create_tweet(text=text)
logger.info(f"Posted tweet {text} - response {response}")
51 changes: 51 additions & 0 deletions prediction_market_agent/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,57 @@ def tavily_api_key(self) -> SecretStr:
)


class SocialMediaAPIKeys(APIKeys):
FARCASTER_PRIVATE_KEY: t.Optional[SecretStr] = None
TWITTER_ACCESS_TOKEN: t.Optional[SecretStr] = None
TWITTER_ACCESS_TOKEN_SECRET: t.Optional[SecretStr] = None
TWITTER_BEARER_TOKEN: t.Optional[SecretStr] = None
TWITTER_API_KEY: t.Optional[SecretStr] = None
TWITTER_API_KEY_SECRET: t.Optional[SecretStr] = None

@property
def farcaster_private_key(self) -> SecretStr:
return check_not_none(
self.FARCASTER_PRIVATE_KEY,
"FARCASTER_PRIVATE_KEY missing in the environment.",
)

@property
def twitter_access_token(self) -> SecretStr:
return check_not_none(
self.TWITTER_ACCESS_TOKEN,
"TWITTER_ACCESS_TOKEN missing in the environment.",
)

@property
def twitter_access_token_secret(self) -> SecretStr:
return check_not_none(
self.TWITTER_ACCESS_TOKEN_SECRET,
"TWITTER_ACCESS_TOKEN_SECRET missing in the environment.",
)

@property
def twitter_bearer_token(self) -> SecretStr:
return check_not_none(
self.TWITTER_BEARER_TOKEN,
"TWITTER_BEARER_TOKEN missing in the environment.",
)

@property
def twitter_api_key(self) -> SecretStr:
return check_not_none(
self.TWITTER_API_KEY,
"TWITTER_API_KEY missing in the environment.",
)

@property
def twitter_api_key_secret(self) -> SecretStr:
return check_not_none(
self.TWITTER_API_KEY_SECRET,
"TWITTER_API_KEY_SECRET missing in the environment.",
)


def get_market_prompt(question: str) -> str:
prompt = (
f"Research and report on the following question:\n\n"
Expand Down
4 changes: 3 additions & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ poetry = "^1.7.1"
poetry-plugin-export = "^1.6.0"
functions-framework = "^3.5.0"
cron-validator = "^1.0.8"
prediction-market-agent-tooling = { version = "^0.25.0", extras = ["langchain", "google"] }
prediction-market-agent-tooling = { version = "^0.26.0", extras = ["langchain", "google"] }
pydantic-settings = "^2.1.0"
autoflake = "^2.2.1"
isort = "^5.13.2"
Expand All @@ -53,6 +53,8 @@ pypdf2 = "^3.0.1" # TODO remove with https://github.com/gnosis/prediction-market
faiss-cpu = "^1.8.0" # TODO remove with https://github.com/gnosis/prediction-market-agent/issues/97
psycopg2-binary = "^2.9.9"
sqlmodel = "^0.0.18"

farcaster = "^0.7.11"
mech-client = "^0.2.13"
streamlit-extras = "^0.4.2"

Expand Down

0 comments on commit 95b3f2f

Please sign in to comment.