From 463386b71531282404c63f153e8636983ea5d246 Mon Sep 17 00:00:00 2001 From: Cameron Pfiffer Date: Wed, 20 Aug 2025 13:37:31 -0700 Subject: [PATCH] Make X profile search configurable for performance vs depth MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Added include_timeline parameter (default: True) - Full mode: Fetches 10 recent posts via separate API call (comprehensive) - Fast mode: Uses expansions to get pinned + most recent tweet only (1 API call) - Output includes mode indicator for transparency - Gives void flexibility to choose between thorough analysis or quick lookups This allows optimizing API usage based on context - full timeline when analyzing someone deeply, fast mode for quick reference checks. 🤖 Generated with Claude Code Co-Authored-By: Claude --- register_x_tools.py | 4 +- tools/x_profile.py | 124 ++++++++++++++++++++++++++++++++++++++++++-- 2 files changed, 122 insertions(+), 6 deletions(-) diff --git a/register_x_tools.py b/register_x_tools.py index 9a8c40a..591d02a 100644 --- a/register_x_tools.py +++ b/register_x_tools.py @@ -133,8 +133,8 @@ X_TOOL_CONFIGS = [ { "func": search_x_profile, "args_schema": SearchXProfileArgs, - "description": "Look up detailed profile information for an X (Twitter) user", - "tags": ["x", "twitter", "profile", "lookup", "user"] + "description": "Look up detailed profile information for an X (Twitter) user including recent posts and conversations", + "tags": ["x", "twitter", "profile", "lookup", "user", "posts", "activity"] } ] diff --git a/tools/x_profile.py b/tools/x_profile.py index 1cc6168..8282d6b 100644 --- a/tools/x_profile.py +++ b/tools/x_profile.py @@ -5,17 +5,19 @@ from typing import Optional class SearchXProfileArgs(BaseModel): username: str = Field(..., description="X username to look up (without @)") + include_timeline: bool = Field(default=True, description="Include full timeline (10 posts) vs just pinned/most recent tweet") -def search_x_profile(username: str) -> str: +def search_x_profile(username: str, include_timeline: bool = True) -> str: """ - Look up detailed profile information for an X (Twitter) user. + Look up detailed profile information for an X (Twitter) user, including recent activity. Args: username: X username to look up (without @) + include_timeline: If True, fetches full timeline (10 posts). If False, only pinned/most recent tweet. Returns: - YAML-formatted profile information including bio, metrics, verification status, etc. + YAML-formatted profile information including bio, metrics, verification status, and recent posts/replies. """ import os import yaml @@ -68,10 +70,17 @@ def search_x_profile(username: str) -> str: "public_metrics", "profile_image_url", "profile_banner_url", - "protected" + "protected", + "pinned_tweet_id", + "most_recent_tweet_id" ]) } + # Add expansions if not fetching full timeline + if not include_timeline: + user_params["expansions"] = "pinned_tweet_id,most_recent_tweet_id" + user_params["tweet.fields"] = "id,text,created_at,referenced_tweets,conversation_id" + try: response = requests.get(user_lookup_url, headers=headers, params=user_params, timeout=10) response.raise_for_status() @@ -128,6 +137,113 @@ def search_x_profile(username: str) -> str: "like_count": metrics.get("like_count", 0) } + # Handle recent activity based on include_timeline parameter + user_id = user.get("id") + if user_id and not user.get("protected", False): # Only fetch if account is public + recent_posts = [] + + if include_timeline: + # Full timeline mode: Fetch recent tweets with separate API call + try: + tweets_url = f"{base_url}/users/{user_id}/tweets" + tweets_params = { + "max_results": 10, # Get last 10 posts/replies + "tweet.fields": "id,text,created_at,referenced_tweets,conversation_id,in_reply_to_user_id", + # Don't exclude replies - we want to see conversations + } + + tweets_response = requests.get(tweets_url, headers=headers, params=tweets_params, timeout=10) + tweets_response.raise_for_status() + tweets_data = tweets_response.json() + + # Format recent activity + for tweet in tweets_data.get("data", [])[:10]: # Limit to 10 most recent + # Determine tweet type + tweet_type = "tweet" + referenced_tweets = tweet.get("referenced_tweets", []) + for ref in referenced_tweets: + if ref.get("type") == "retweeted": + tweet_type = "retweet" + break + elif ref.get("type") == "replied_to": + tweet_type = "reply" + break + elif ref.get("type") == "quoted": + tweet_type = "quote" + break + + post_data = { + "text": tweet.get("text", ""), + "created_at": tweet.get("created_at", ""), + "type": tweet_type, + "url": f"https://x.com/{username}/status/{tweet.get('id', '')}" + } + + # Add conversation context for replies + if tweet_type == "reply" and tweet.get("conversation_id"): + post_data["conversation_id"] = tweet.get("conversation_id") + if tweet.get("in_reply_to_user_id"): + post_data["replying_to_user_id"] = tweet.get("in_reply_to_user_id") + + recent_posts.append(post_data) + + except Exception as e: + # Log error but don't fail the entire profile lookup + profile_data["x_user_profile"]["recent_activity"] = { + "error": f"Could not fetch recent posts: {str(e)}" + } + else: + # Fast mode: Use expanded tweets from user lookup response + if "includes" in data and "tweets" in data["includes"]: + expanded_tweets = data["includes"]["tweets"] + + # Process pinned tweet if present + pinned_tweet_id = user.get("pinned_tweet_id") + if pinned_tweet_id: + for tweet in expanded_tweets: + if tweet.get("id") == pinned_tweet_id: + post_data = { + "text": tweet.get("text", ""), + "created_at": tweet.get("created_at", ""), + "type": "pinned", + "url": f"https://x.com/{username}/status/{tweet.get('id', '')}" + } + recent_posts.append(post_data) + break + + # Process most recent tweet if present and different from pinned + recent_tweet_id = user.get("most_recent_tweet_id") + if recent_tweet_id and recent_tweet_id != pinned_tweet_id: + for tweet in expanded_tweets: + if tweet.get("id") == recent_tweet_id: + # Determine tweet type + tweet_type = "tweet" + referenced_tweets = tweet.get("referenced_tweets", []) + for ref in referenced_tweets: + if ref.get("type") == "replied_to": + tweet_type = "reply" + break + elif ref.get("type") == "quoted": + tweet_type = "quote" + break + + post_data = { + "text": tweet.get("text", ""), + "created_at": tweet.get("created_at", ""), + "type": tweet_type, + "url": f"https://x.com/{username}/status/{tweet.get('id', '')}" + } + recent_posts.append(post_data) + break + + # Add recent activity to profile data if we have posts + if recent_posts: + profile_data["x_user_profile"]["recent_activity"] = { + "post_count": len(recent_posts), + "mode": "full_timeline" if include_timeline else "expanded_only", + "posts": recent_posts + } + return yaml.dump(profile_data, default_flow_style=False, sort_keys=False) except Exception as e: -- 2.51.2