From c6ad9606b57bc4899446e18db2d0c249e835c641 Mon Sep 17 00:00:00 2001 From: Muninn Date: Sat, 18 Jan 2025 00:14:31 +0800 Subject: [PATCH 1/7] chore: remove unused files --- debug/create_agent.py | 35 ----------------------------------- 1 file changed, 35 deletions(-) delete mode 100644 debug/create_agent.py diff --git a/debug/create_agent.py b/debug/create_agent.py deleted file mode 100644 index ea0265a..0000000 --- a/debug/create_agent.py +++ /dev/null @@ -1,35 +0,0 @@ -import logging - -from app.config.config import config -from models.agent import Agent -from models.db import get_db, init_db - -if config.env == "local": - # Set up logging configuration - logging.basicConfig() - logging.getLogger("sqlalchemy.engine").setLevel(logging.DEBUG) - -init_db(**config.db) -db = next(get_db()) -agent = Agent( - id="local", - name="IntentKit", - model="gpt-4o-mini", # This repetition could be omitted if default is intended - prompt="", # Confirm if an empty prompt is acceptable - autonomous_enabled=False, # This field must be provided - autonomous_content="", # Optional, provide if needed - autonomous_minutes=None, # Optional, provide if needed - cdp_enabled=True, - cdp_skills=[], # Confirm if loading all skills when empty is the desired behavior - cdp_wallet_data="", # Assuming wallet_data was meant to be cdp_wallet_data - cdp_network_id="base-sepolia", - twitter_enabled=False, - twitter_config={}, # Ensure this dict structure aligns with expected config format - twitter_skills=[], # Confirm if no specific Twitter skills are to be enabled - telegram_enabled=False, - telegram_config={}, # Ensure this dict structure aligns with expected config format - telegram_skills=[], # Confirm if no specific Telegram skills are to be enabled - common_skills=[], # Confirm if no common skills are to be added initially -) - -agent.create_or_update(db) From c0ba06d8d85597052c8e035c2842db6b99636e41 Mon Sep 17 00:00:00 2001 From: Muninn Date: Sat, 18 Jan 2025 00:16:30 +0800 Subject: [PATCH 2/7] doc: fix enso bug in create agent shell --- docs/create_agent.sh | 3 +++ 1 file changed, 3 insertions(+) diff --git a/docs/create_agent.sh b/docs/create_agent.sh index b895e52..6874d32 100755 --- a/docs/create_agent.sh +++ b/docs/create_agent.sh @@ -97,6 +97,9 @@ JSON_DATA=$(cat << EOF "cdp_skills": $CDP_SKILLS, "cdp_wallet_data": "$CDP_WALLET_DATA", "cdp_network_id": "$CDP_NETWORK_ID", + "enso_enabled": $ENSO_ENABLED, + "enso_config": $ENSO_CONFIG, + "enso_skills": $ENSO_SKILLS, "twitter_enabled": $TWITTER_ENTRYPOINT_ENABLED, "twitter_entrypoint_enabled": $TWITTER_ENTRYPOINT_ENABLED, "twitter_config": $TWITTER_CONFIG, From ed9f40cd49fa5418b0840cf89047871bef1ba555 Mon Sep 17 00:00:00 2001 From: Muninn Date: Sat, 18 Jan 2025 00:30:07 +0800 Subject: [PATCH 3/7] fix: slack notify bug --- app/admin/api.py | 86 +++++++++++++++++++++++------------------------- 1 file changed, 41 insertions(+), 45 deletions(-) diff --git a/app/admin/api.py b/app/admin/api.py index 209a1aa..46d33b0 100644 --- a/app/admin/api.py +++ b/app/admin/api.py @@ -46,52 +46,48 @@ def create_agent(agent: Agent, db: Session = Depends(get_db)) -> Agent: # Get the latest agent from create_or_update latest_agent = agent.create_or_update(db) - # Send Slack notification only for new agents + # Send Slack notification total_agents = db.exec(select(func.count()).select_from(Agent)).one() - if ( - total_agents == 1 - or not db.exec(select(Agent).filter(Agent.id == agent.id)).first() - ): - send_slack_message( - "New agent created ", - attachments=[ - { - "color": "good", - "fields": [ - {"title": "ENV", "short": True, "value": config.env}, - {"title": "Total", "short": True, "value": total_agents}, - {"title": "ID", "short": True, "value": latest_agent.id}, - {"title": "Name", "short": True, "value": latest_agent.name}, - {"title": "Model", "short": True, "value": latest_agent.model}, - { - "title": "Autonomous", - "short": True, - "value": str(latest_agent.autonomous_enabled), - }, - { - "title": "Twitter", - "short": True, - "value": str(latest_agent.twitter_enabled), - }, - { - "title": "Telegram", - "short": True, - "value": str(latest_agent.telegram_enabled), - }, - { - "title": "CDP Enabled", - "short": True, - "value": str(latest_agent.cdp_enabled), - }, - { - "title": "CDP Network", - "short": True, - "value": latest_agent.cdp_network_id or "Default", - }, - ], - } - ], - ) + send_slack_message( + "New agent created ", + attachments=[ + { + "color": "good", + "fields": [ + {"title": "ENV", "short": True, "value": config.env}, + {"title": "Total", "short": True, "value": total_agents}, + {"title": "ID", "short": True, "value": latest_agent.id}, + {"title": "Name", "short": True, "value": latest_agent.name}, + {"title": "Model", "short": True, "value": latest_agent.model}, + { + "title": "Autonomous", + "short": True, + "value": str(latest_agent.autonomous_enabled), + }, + { + "title": "Twitter", + "short": True, + "value": str(latest_agent.twitter_enabled), + }, + { + "title": "Telegram", + "short": True, + "value": str(latest_agent.telegram_enabled), + }, + { + "title": "CDP Enabled", + "short": True, + "value": str(latest_agent.cdp_enabled), + }, + { + "title": "CDP Network", + "short": True, + "value": latest_agent.cdp_network_id or "Default", + }, + ], + } + ], + ) # Mask sensitive data in response latest_agent.cdp_wallet_data = "forbidden" From 6fd988048ee103612051546ba4f600a773e3f64b Mon Sep 17 00:00:00 2001 From: Muninn Date: Sat, 18 Jan 2025 00:35:12 +0800 Subject: [PATCH 4/7] fix: improve the autonomous prompt in create agent shell to multi-line --- docs/create_agent.sh | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/docs/create_agent.sh b/docs/create_agent.sh index 6874d32..acab7de 100755 --- a/docs/create_agent.sh +++ b/docs/create_agent.sh @@ -39,7 +39,9 @@ END_OF_APPEND # If you enable autonomous mode, the agent will automatically run the autonomous_prompt every N minutes AUTONOMOUS_ENABLED=false AUTONOMOUS_MINUTES=60 -AUTONOMOUS_PROMPT="Autonomous mode prompt" +read -r -d '' AUTONOMOUS_PROMPT_TEXT << 'END_OF_AUTONOMOUS_PROMPT' +Check twitter for new mentions, choose the best one and reply it. If there is no mention, just have a rest, don't post anything. +END_OF_AUTONOMOUS_PROMPT # CDP settings (optional) # Skill list: https://docs.cdp.coinbase.com/agentkit/docs/wallet-management @@ -82,6 +84,9 @@ PROMPT="$(echo "$PROMPT_TEXT" | awk '{printf "%s\\n", $0}' | sed 's/"/\\"/g' | s # Convert multiline text to escaped string PROMPT_APPEND="$(echo "$PROMPT_APPEND_TEXT" | awk '{printf "%s\\n", $0}' | sed 's/"/\\"/g' | sed '$ s/\\n$//')" +# Autonomous mode prompt +AUTONOMOUS_PROMPT="$(echo "$AUTONOMOUS_PROMPT_TEXT" | awk '{printf "%s\\n", $0}' | sed 's/"/\\"/g' | sed '$ s/\\n$//')" + # Create JSON payload JSON_DATA=$(cat << EOF { From 64ecfd9067b16592dff41d3f837114d1e6f40dd4 Mon Sep 17 00:00:00 2001 From: Muninn Date: Sat, 18 Jan 2025 01:34:31 +0800 Subject: [PATCH 5/7] fix: oauth2 refresh improve --- app/services/twitter/oauth2_refresh.py | 32 ++++++++++++++++++-------- 1 file changed, 22 insertions(+), 10 deletions(-) diff --git a/app/services/twitter/oauth2_refresh.py b/app/services/twitter/oauth2_refresh.py index 939de40..6bf3cfc 100644 --- a/app/services/twitter/oauth2_refresh.py +++ b/app/services/twitter/oauth2_refresh.py @@ -35,27 +35,34 @@ def get_expiring_tokens(db: Session, minutes_threshold: int = 10) -> list[AgentD ).all() -def refresh_token(db: Session, agent: AgentData) -> bool: +def refresh_token(db: Session, agent: AgentData): """Refresh Twitter OAuth2 token for an agent. Args: db: Database session agent: Agent data record containing refresh token - - Returns: - bool: True if refresh successful, False otherwise """ try: # Get new token using refresh token token = oauth2_user_handler.refresh(agent.twitter_refresh_token) + token = {} if token is None else token + # Update token information - agent.twitter_access_token = token["access_token"] + if "access_token" in token: + agent.twitter_access_token = token["access_token"] + else: + agent.twitter_access_token = None if "refresh_token" in token: # Some providers return new refresh tokens agent.twitter_refresh_token = token["refresh_token"] - agent.twitter_access_token_expires_at = datetime.fromtimestamp( - token["expires_at"], tz=timezone.utc - ) + else: + agent.twitter_refresh_token = None + if "expires_at" in token: + agent.twitter_access_token_expires_at = datetime.fromtimestamp( + token["expires_at"], tz=timezone.utc + ) + else: + agent.twitter_access_token_expires_at = None # Save changes db.add(agent) @@ -63,10 +70,15 @@ def refresh_token(db: Session, agent: AgentData) -> bool: db.refresh(agent) logger.info(f"Refreshed token for agent {agent.id}") - return True except Exception as e: logger.error(f"Failed to refresh token for agent {agent.id}: {str(e)}") - return False + # if error, reset token + agent.twitter_access_token = None + agent.twitter_refresh_token = None + agent.twitter_access_token_expires_at = None + db.add(agent) + db.commit() + db.refresh(agent) def refresh_expiring_tokens(): From 6616decd43bb496c5e1daf4401d48602bb4c8706 Mon Sep 17 00:00:00 2001 From: Muninn Date: Sat, 18 Jan 2025 01:34:50 +0800 Subject: [PATCH 6/7] feat: twitter skill rate limit --- skills/twitter/base.py | 67 +++++++++++++++++++++++++++------ skills/twitter/follow_user.py | 7 ++++ skills/twitter/get_mentions.py | 9 +++++ skills/twitter/get_timeline.py | 15 ++++++-- skills/twitter/like_tweet.py | 7 ++++ skills/twitter/post_tweet.py | 5 +++ skills/twitter/reply_tweet.py | 13 +++++-- skills/twitter/retweet.py | 7 ++++ skills/twitter/search_tweets.py | 17 +++++++-- 9 files changed, 123 insertions(+), 24 deletions(-) diff --git a/skills/twitter/base.py b/skills/twitter/base.py index 2e7fd97..36aaa02 100644 --- a/skills/twitter/base.py +++ b/skills/twitter/base.py @@ -1,4 +1,4 @@ -from datetime import datetime +from datetime import datetime, timedelta, timezone from typing import Type from pydantic import BaseModel, Field @@ -8,17 +8,6 @@ from abstracts.twitter import TwitterABC -class Tweet(BaseModel): - """Model representing a Twitter tweet.""" - - id: str - text: str - author_id: str - created_at: datetime - referenced_tweets: list[dict] | None = None - attachments: dict | None = None - - class TwitterBaseTool(IntentKitSkill): """Base class for Twitter tools.""" @@ -31,3 +20,57 @@ class TwitterBaseTool(IntentKitSkill): description="The agent store for persisting data" ) store: SkillStoreABC = Field(description="The skill store for persisting data") + + def check_rate_limit( + self, max_requests: int = 1, interval: int = 15 + ) -> tuple[bool, str | None]: + """Check if the rate limit has been exceeded. + + Args: + max_requests: Maximum number of requests allowed within the rate limit window. + interval: Time interval in minutes for the rate limit window. + + Returns: + tuple[bool, str | None]: (is_rate_limited, error_message) + """ + rate_limit = self.store.get_agent_skill_data( + self.agent_id, self.name, "rate_limit" + ) + + current_time = datetime.now(tz=timezone.utc) + + if ( + rate_limit + and rate_limit.get("reset_time") + and rate_limit["count"] is not None + and datetime.fromisoformat(rate_limit["reset_time"]) > current_time + ): + if rate_limit["count"] >= max_requests: + return True, "Rate limit exceeded" + else: + rate_limit["count"] += 1 + self.store.save_agent_skill_data( + self.agent_id, self.name, "rate_limit", rate_limit + ) + return False, None + + # If no rate limit exists or it has expired, create a new one + new_rate_limit = { + "count": 1, + "reset_time": (current_time + timedelta(minutes=interval)).isoformat(), + } + self.store.save_agent_skill_data( + self.agent_id, self.name, "rate_limit", new_rate_limit + ) + return False, None + + +class Tweet(BaseModel): + """Model representing a Twitter tweet.""" + + id: str + text: str + author_id: str + created_at: datetime + referenced_tweets: list[dict] | None = None + attachments: dict | None = None diff --git a/skills/twitter/follow_user.py b/skills/twitter/follow_user.py index 7f0d891..2af33c3 100644 --- a/skills/twitter/follow_user.py +++ b/skills/twitter/follow_user.py @@ -46,6 +46,13 @@ def _run(self, user_id: str) -> TwitterFollowUserOutput: Exception: If there's an error accessing the Twitter API. """ try: + # Check rate limit + is_rate_limited, error = self.check_rate_limit(max_requests=1, interval=15) + if is_rate_limited: + return TwitterFollowUserOutput( + success=False, message=f"Error following user: {error}" + ) + client = self.twitter.get_client() if not client: return TwitterFollowUserOutput( diff --git a/skills/twitter/get_mentions.py b/skills/twitter/get_mentions.py index 2f4eb34..299ee48 100644 --- a/skills/twitter/get_mentions.py +++ b/skills/twitter/get_mentions.py @@ -41,6 +41,14 @@ def _run(self) -> TwitterGetMentionsOutput: Exception: If there's an error accessing the Twitter API. """ try: + # Check rate limit + is_rate_limited, error = self.check_rate_limit(max_requests=1) + if is_rate_limited: + return TwitterGetMentionsOutput( + mentions=[], + error=error, + ) + # get since id from store last = self.store.get_agent_skill_data(self.agent_id, self.name, "last") last = last or {} @@ -88,6 +96,7 @@ def _run(self) -> TwitterGetMentionsOutput: result = [] if mentions.data: + # Process and return results for tweet in mentions.data: mention = Tweet( id=str(tweet.id), diff --git a/skills/twitter/get_timeline.py b/skills/twitter/get_timeline.py index 80596ef..63c0968 100644 --- a/skills/twitter/get_timeline.py +++ b/skills/twitter/get_timeline.py @@ -31,20 +31,27 @@ class TwitterGetTimeline(TwitterBaseTool): description: str = "Get tweets from the authenticated user's timeline" args_schema: Type[BaseModel] = TwitterGetTimelineInput - def _run(self) -> TwitterGetTimelineOutput: - """Run the tool to get timeline tweets. + def _run(self, max_results: int = 10) -> TwitterGetTimelineOutput: + """Run the tool to get the user's timeline. + + Args: + max_results (int, optional): Maximum number of tweets to retrieve. Defaults to 10. Returns: - TwitterGetTimelineOutput: A structured output containing the timeline tweets data. + TwitterGetTimelineOutput: A structured output containing the timeline data. Raises: Exception: If there's an error accessing the Twitter API. """ try: + # Check rate limit + is_rate_limited, error = self.check_rate_limit(max_requests=1, interval=15) + if is_rate_limited: + return TwitterGetTimelineOutput(tweets=[], error=error) + # get since id from store last = self.store.get_agent_skill_data(self.agent_id, self.name, "last") last = last or {} - max_results = 10 since_id = last.get("since_id") if since_id: max_results = 100 diff --git a/skills/twitter/like_tweet.py b/skills/twitter/like_tweet.py index 263d5b3..5b0cc69 100644 --- a/skills/twitter/like_tweet.py +++ b/skills/twitter/like_tweet.py @@ -46,6 +46,13 @@ def _run(self, tweet_id: str) -> TwitterLikeTweetOutput: Exception: If there's an error accessing the Twitter API. """ try: + # Check rate limit + is_rate_limited, error = self.check_rate_limit(max_requests=1, interval=15) + if is_rate_limited: + return TwitterLikeTweetOutput( + success=False, message=f"Error liking tweet: {error}" + ) + client = self.twitter.get_client() if not client: return TwitterLikeTweetOutput( diff --git a/skills/twitter/post_tweet.py b/skills/twitter/post_tweet.py index fd7e290..6813b2c 100644 --- a/skills/twitter/post_tweet.py +++ b/skills/twitter/post_tweet.py @@ -41,6 +41,11 @@ def _run(self, text: str) -> str: Exception: If there's an error posting to the Twitter API. """ try: + # Check rate limit + is_rate_limited, error = self.check_rate_limit(max_requests=1, interval=15) + if is_rate_limited: + return f"Error posting tweet: {error}" + client = self.twitter.get_client() if not client: return "Failed to get Twitter client. Please check your authentication." diff --git a/skills/twitter/reply_tweet.py b/skills/twitter/reply_tweet.py index 8551fbc..f79dfdb 100644 --- a/skills/twitter/reply_tweet.py +++ b/skills/twitter/reply_tweet.py @@ -28,19 +28,24 @@ class TwitterReplyTweet(TwitterBaseTool): args_schema: Type[BaseModel] = TwitterReplyTweetInput def _run(self, tweet_id: str, text: str) -> str: - """Run the tool to post a reply tweet. + """Run the tool to reply to a tweet. Args: tweet_id (str): The ID of the tweet to reply to. - text (str): The text content of the reply tweet. + text (str): The text content of the reply. Returns: - str: A message indicating success or failure of the reply posting. + str: A message indicating success or failure of the reply action. Raises: - Exception: If there's an error posting to the Twitter API. + Exception: If there's an error replying via the Twitter API. """ try: + # Check rate limit + is_rate_limited, error = self.check_rate_limit(max_requests=1, interval=15) + if is_rate_limited: + return f"Error replying to tweet: {error}" + client = self.twitter.get_client() if not client: return "Failed to get Twitter client. Please check your authentication." diff --git a/skills/twitter/retweet.py b/skills/twitter/retweet.py index 04e1327..2391086 100644 --- a/skills/twitter/retweet.py +++ b/skills/twitter/retweet.py @@ -46,6 +46,13 @@ def _run(self, tweet_id: str) -> TwitterRetweetOutput: Exception: If there's an error accessing the Twitter API. """ try: + # Check rate limit + is_rate_limited, error = self.check_rate_limit(max_requests=1, interval=15) + if is_rate_limited: + return TwitterRetweetOutput( + success=False, message=f"Error retweeting: {error}" + ) + client = self.twitter.get_client() if not client: return TwitterRetweetOutput( diff --git a/skills/twitter/search_tweets.py b/skills/twitter/search_tweets.py index e46b428..8e6fe8f 100644 --- a/skills/twitter/search_tweets.py +++ b/skills/twitter/search_tweets.py @@ -37,19 +37,28 @@ class TwitterSearchTweets(TwitterBaseTool): description: str = "Search for recent tweets on Twitter using a query" args_schema: Type[BaseModel] = TwitterSearchTweetsInput - def _run(self, query: str) -> TwitterSearchTweetsOutput: - """Run the tool to search for tweets. + def _run( + self, query: str, max_results: int = 10, recent_only: bool = True + ) -> TwitterSearchTweetsOutput: + """Run the tool to search tweets. Args: - query: The search query to find tweets. + query (str): The search query to use. + max_results (int, optional): Maximum number of results to return. Defaults to 10. + recent_only (bool, optional): Whether to only search recent tweets. Defaults to True. Returns: TwitterSearchTweetsOutput: A structured output containing the search results. Raises: - Exception: If there's an error accessing the Twitter API. + Exception: If there's an error searching via the Twitter API. """ try: + # Check rate limit + is_rate_limited, error = self.check_rate_limit(max_requests=1, interval=15) + if is_rate_limited: + return TwitterSearchTweetsOutput(tweets=[], error=error) + client = self.twitter.get_client() if not client: return TwitterSearchTweetsOutput( From 5b313e250c3e8d8a559ea72616ff8d2b2cbfdf31 Mon Sep 17 00:00:00 2001 From: Muninn Date: Sat, 18 Jan 2025 01:50:50 +0800 Subject: [PATCH 7/7] fix: a bug in twitter search skill --- skills/twitter/search_tweets.py | 7 ------- 1 file changed, 7 deletions(-) diff --git a/skills/twitter/search_tweets.py b/skills/twitter/search_tweets.py index 8e6fe8f..cc1796b 100644 --- a/skills/twitter/search_tweets.py +++ b/skills/twitter/search_tweets.py @@ -1,5 +1,4 @@ import logging -from datetime import datetime, timedelta, timezone from typing import Type from pydantic import BaseModel, Field @@ -71,11 +70,6 @@ def _run( last = last or {} since_id = last.get("since_id") - # Always get tweets for the last day - start_time = (datetime.now(tz=timezone.utc) - timedelta(days=1)).isoformat( - timespec="milliseconds" - ) - tweet_fields = [ "created_at", "author_id", @@ -87,7 +81,6 @@ def _run( query=query, user_auth=self.twitter.use_key, since_id=since_id, - start_time=start_time, expansions=[ "referenced_tweets.id", "attachments.media_keys",