fix(x): fix x optional tool params (#2307)

* fix(x): fix x optional tool params

* ack pr comments
This commit is contained in:
Waleed
2025-12-11 00:09:35 -08:00
committed by GitHub
parent 7fd912d8a9
commit 0997989f36
6 changed files with 224 additions and 99 deletions

View File

@@ -145,32 +145,23 @@ export const XBlock: BlockConfig<XResponse> = {
params: (params) => {
const { credential, ...rest } = params
// Convert string values to appropriate types
const parsedParams: Record<string, any> = {
credential: credential,
}
// Add other params
Object.keys(rest).forEach((key) => {
const value = rest[key]
// Convert string boolean values to actual booleans
if (value === 'true' || value === 'false') {
parsedParams[key] = value === 'true'
}
// Convert numeric strings to numbers where appropriate
else if (key === 'maxResults' && value) {
} else if (key === 'maxResults' && value) {
parsedParams[key] = Number.parseInt(value as string, 10)
}
// Handle mediaIds conversion from comma-separated string to array
else if (key === 'mediaIds' && typeof value === 'string') {
} else if (key === 'mediaIds' && typeof value === 'string') {
parsedParams[key] = value
.split(',')
.map((id) => id.trim())
.filter((id) => id !== '')
}
// Keep other values as is
else {
} else {
parsedParams[key] = value
}
})
@@ -197,13 +188,49 @@ export const XBlock: BlockConfig<XResponse> = {
includeRecentTweets: { type: 'boolean', description: 'Include recent tweets' },
},
outputs: {
tweet: { type: 'json', description: 'Tweet data' },
replies: { type: 'json', description: 'Tweet replies' },
context: { type: 'json', description: 'Tweet context' },
tweets: { type: 'json', description: 'Tweets data' },
includes: { type: 'json', description: 'Additional data' },
meta: { type: 'json', description: 'Response metadata' },
user: { type: 'json', description: 'User profile data' },
recentTweets: { type: 'json', description: 'Recent tweets data' },
// Write and Read operation outputs
tweet: {
type: 'json',
description: 'Tweet data including contextAnnotations and publicMetrics',
condition: { field: 'operation', value: ['x_write', 'x_read'] },
},
// Read operation outputs
replies: {
type: 'json',
description: 'Tweet replies (when includeReplies is true)',
condition: { field: 'operation', value: 'x_read' },
},
context: {
type: 'json',
description: 'Tweet context (parent and quoted tweets)',
condition: { field: 'operation', value: 'x_read' },
},
// Search operation outputs
tweets: {
type: 'json',
description: 'Tweets data including contextAnnotations and publicMetrics',
condition: { field: 'operation', value: 'x_search' },
},
includes: {
type: 'json',
description: 'Additional data (users, media, polls)',
condition: { field: 'operation', value: 'x_search' },
},
meta: {
type: 'json',
description: 'Response metadata',
condition: { field: 'operation', value: 'x_search' },
},
// User operation outputs
user: {
type: 'json',
description: 'User profile data',
condition: { field: 'operation', value: 'x_user' },
},
recentTweets: {
type: 'json',
description: 'Recent tweets data',
condition: { field: 'operation', value: 'x_user' },
},
},
}

View File

@@ -1,5 +1,9 @@
import { createLogger } from '@/lib/logs/console/logger'
import type { ToolConfig } from '@/tools/types'
import type { XReadParams, XReadResponse, XTweet } from '@/tools/x/types'
import { transformTweet } from '@/tools/x/types'
const logger = createLogger('XReadTool')
export const xReadTool: ToolConfig<XReadParams, XReadResponse> = {
id: 'x_read',
@@ -39,11 +43,36 @@ export const xReadTool: ToolConfig<XReadParams, XReadResponse> = {
'author_id',
'in_reply_to_user_id',
'referenced_tweets.id',
'referenced_tweets.id.author_id',
'attachments.media_keys',
'attachments.poll_ids',
].join(',')
return `https://api.twitter.com/2/tweets/${params.tweetId}?expansions=${expansions}`
const tweetFields = [
'created_at',
'conversation_id',
'in_reply_to_user_id',
'attachments',
'context_annotations',
'public_metrics',
].join(',')
const userFields = [
'name',
'username',
'description',
'profile_image_url',
'verified',
'public_metrics',
].join(',')
const queryParams = new URLSearchParams({
expansions,
'tweet.fields': tweetFields,
'user.fields': userFields,
})
return `https://api.twitter.com/2/tweets/${params.tweetId}?${queryParams.toString()}`
},
method: 'GET',
headers: (params) => ({
@@ -52,39 +81,79 @@ export const xReadTool: ToolConfig<XReadParams, XReadResponse> = {
}),
},
transformResponse: async (response) => {
transformResponse: async (response, params) => {
const data = await response.json()
const transformTweet = (tweet: any): XTweet => ({
id: tweet.id,
text: tweet.text,
createdAt: tweet.created_at,
authorId: tweet.author_id,
conversationId: tweet.conversation_id,
inReplyToUserId: tweet.in_reply_to_user_id,
attachments: {
mediaKeys: tweet.attachments?.media_keys,
pollId: tweet.attachments?.poll_ids?.[0],
},
})
if (data.errors && !data.data) {
logger.error('X Read API Error:', JSON.stringify(data, null, 2))
return {
success: false,
error: data.errors?.[0]?.detail || data.errors?.[0]?.message || 'Failed to fetch tweet',
output: {
tweet: {} as XTweet,
},
}
}
const mainTweet = transformTweet(data.data)
const context: { parentTweet?: XTweet; rootTweet?: XTweet } = {}
// Get parent and root tweets if available
if (data.includes?.tweets) {
const referencedTweets = data.data.referenced_tweets || []
const parentTweetRef = referencedTweets.find((ref: any) => ref.type === 'replied_to')
const rootTweetRef = referencedTweets.find((ref: any) => ref.type === 'replied_to_root')
const quotedTweetRef = referencedTweets.find((ref: any) => ref.type === 'quoted')
if (parentTweetRef) {
const parentTweet = data.includes.tweets.find((t: any) => t.id === parentTweetRef.id)
if (parentTweet) context.parentTweet = transformTweet(parentTweet)
}
if (rootTweetRef) {
const rootTweet = data.includes.tweets.find((t: any) => t.id === rootTweetRef.id)
if (rootTweet) context.rootTweet = transformTweet(rootTweet)
if (!parentTweetRef && quotedTweetRef) {
const quotedTweet = data.includes.tweets.find((t: any) => t.id === quotedTweetRef.id)
if (quotedTweet) context.rootTweet = transformTweet(quotedTweet)
}
}
let replies: XTweet[] = []
if (params?.includeReplies && mainTweet.id) {
try {
const repliesExpansions = ['author_id', 'referenced_tweets.id'].join(',')
const repliesTweetFields = [
'created_at',
'conversation_id',
'in_reply_to_user_id',
'public_metrics',
].join(',')
const conversationId = mainTweet.conversationId || mainTweet.id
const searchQuery = `conversation_id:${conversationId}`
const searchParams = new URLSearchParams({
query: searchQuery,
expansions: repliesExpansions,
'tweet.fields': repliesTweetFields,
max_results: '100', // Max allowed
})
const repliesResponse = await fetch(
`https://api.twitter.com/2/tweets/search/recent?${searchParams.toString()}`,
{
method: 'GET',
headers: {
Authorization: `Bearer ${params?.accessToken || ''}`,
'Content-Type': 'application/json',
},
}
)
const repliesData = await repliesResponse.json()
if (repliesData.data && Array.isArray(repliesData.data)) {
replies = repliesData.data
.filter((tweet: any) => tweet.id !== mainTweet.id)
.map(transformTweet)
}
} catch (error) {
logger.warn('Failed to fetch replies:', error)
}
}
@@ -92,7 +161,8 @@ export const xReadTool: ToolConfig<XReadParams, XReadResponse> = {
success: true,
output: {
tweet: mainTweet,
context,
replies: replies.length > 0 ? replies : undefined,
context: Object.keys(context).length > 0 ? context : undefined,
},
}
},

View File

@@ -1,6 +1,7 @@
import { createLogger } from '@/lib/logs/console/logger'
import type { ToolConfig } from '@/tools/types'
import type { XSearchParams, XSearchResponse, XTweet, XUser } from '@/tools/x/types'
import type { XSearchParams, XSearchResponse } from '@/tools/x/types'
import { transformTweet, transformUser } from '@/tools/x/types'
const logger = createLogger('XSearchTool')
@@ -67,7 +68,8 @@ export const xSearchTool: ToolConfig<XSearchParams, XSearchResponse> = {
const queryParams = new URLSearchParams({
query,
expansions,
'tweet.fields': 'created_at,conversation_id,in_reply_to_user_id,attachments',
'tweet.fields':
'created_at,conversation_id,in_reply_to_user_id,attachments,context_annotations,public_metrics',
'user.fields': 'name,username,description,profile_image_url,verified,public_metrics',
})
@@ -92,7 +94,6 @@ export const xSearchTool: ToolConfig<XSearchParams, XSearchResponse> = {
transformResponse: async (response) => {
const data = await response.json()
// Check if data.data is undefined/null or not an array
if (!data.data || !Array.isArray(data.data)) {
logger.error('X Search API Error:', JSON.stringify(data, null, 2))
return {
@@ -118,33 +119,6 @@ export const xSearchTool: ToolConfig<XSearchParams, XSearchResponse> = {
}
}
const transformTweet = (tweet: any): XTweet => ({
id: tweet.id,
text: tweet.text,
createdAt: tweet.created_at,
authorId: tweet.author_id,
conversationId: tweet.conversation_id,
inReplyToUserId: tweet.in_reply_to_user_id,
attachments: {
mediaKeys: tweet.attachments?.media_keys,
pollId: tweet.attachments?.poll_ids?.[0],
},
})
const transformUser = (user: any): XUser => ({
id: user.id,
username: user.username,
name: user.name,
description: user.description,
profileImageUrl: user.profile_image_url,
verified: user.verified,
metrics: {
followersCount: user.public_metrics.followers_count,
followingCount: user.public_metrics.following_count,
tweetCount: user.public_metrics.tweet_count,
},
})
return {
success: true,
output: {

View File

@@ -1,6 +1,34 @@
import type { ToolResponse } from '@/tools/types'
// Common Types
/**
* Context annotation domain from X API
*/
export interface XContextAnnotationDomain {
id: string
name: string
description?: string
}
/**
* Context annotation entity from X API
*/
export interface XContextAnnotationEntity {
id: string
name: string
description?: string
}
/**
* Context annotation from X API - provides semantic context about tweet content
*/
export interface XContextAnnotation {
domain: XContextAnnotationDomain
entity: XContextAnnotationEntity
}
/**
* Tweet object from X API
*/
export interface XTweet {
id: string
text: string
@@ -12,6 +40,13 @@ export interface XTweet {
mediaKeys?: string[]
pollId?: string
}
contextAnnotations?: XContextAnnotation[]
publicMetrics?: {
retweetCount: number
replyCount: number
likeCount: number
quoteCount: number
}
}
export interface XUser {
@@ -107,3 +142,45 @@ export interface XUserResponse extends ToolResponse {
}
export type XResponse = XWriteResponse | XReadResponse | XSearchResponse | XUserResponse
/**
* Transforms raw X API tweet data (snake_case) into the XTweet format (camelCase)
*/
export const transformTweet = (tweet: any): XTweet => ({
id: tweet.id,
text: tweet.text,
createdAt: tweet.created_at,
authorId: tweet.author_id,
conversationId: tweet.conversation_id,
inReplyToUserId: tweet.in_reply_to_user_id,
attachments: {
mediaKeys: tweet.attachments?.media_keys,
pollId: tweet.attachments?.poll_ids?.[0],
},
contextAnnotations: tweet.context_annotations,
publicMetrics: tweet.public_metrics
? {
retweetCount: tweet.public_metrics.retweet_count,
replyCount: tweet.public_metrics.reply_count,
likeCount: tweet.public_metrics.like_count,
quoteCount: tweet.public_metrics.quote_count,
}
: undefined,
})
/**
* Transforms raw X API user data (snake_case) into the XUser format (camelCase)
*/
export const transformUser = (user: any): XUser => ({
id: user.id,
username: user.username,
name: user.name || '',
description: user.description || '',
profileImageUrl: user.profile_image_url || '',
verified: !!user.verified,
metrics: {
followersCount: user.public_metrics?.followers_count || 0,
followingCount: user.public_metrics?.following_count || 0,
tweetCount: user.public_metrics?.tweet_count || 0,
},
})

View File

@@ -1,6 +1,7 @@
import { createLogger } from '@/lib/logs/console/logger'
import type { ToolConfig } from '@/tools/types'
import type { XUser, XUserParams, XUserResponse } from '@/tools/x/types'
import type { XUserParams, XUserResponse } from '@/tools/x/types'
import { transformUser } from '@/tools/x/types'
const logger = createLogger('XUserTool')
@@ -85,21 +86,7 @@ export const xUserTool: ToolConfig<XUserParams, XUserResponse> = {
}
const userData = responseData.data
// Create the base user object with defensive coding for missing properties
const user: XUser = {
id: userData.id,
username: userData.username,
name: userData.name || '',
description: userData.description || '',
profileImageUrl: userData.profile_image_url || '',
verified: !!userData.verified,
metrics: {
followersCount: userData.public_metrics?.followers_count || 0,
followingCount: userData.public_metrics?.following_count || 0,
tweetCount: userData.public_metrics?.tweet_count || 0,
},
}
const user = transformUser(userData)
return {
success: true,

View File

@@ -1,5 +1,6 @@
import type { ToolConfig } from '@/tools/types'
import type { XWriteParams, XWriteResponse } from '@/tools/x/types'
import { transformTweet } from '@/tools/x/types'
export const xWriteTool: ToolConfig<XWriteParams, XWriteResponse> = {
id: 'x_write',
@@ -81,18 +82,7 @@ export const xWriteTool: ToolConfig<XWriteParams, XWriteResponse> = {
return {
success: true,
output: {
tweet: {
id: data.data.id,
text: data.data.text,
createdAt: data.data.created_at,
authorId: data.data.author_id,
conversationId: data.data.conversation_id,
inReplyToUserId: data.data.in_reply_to_user_id,
attachments: {
mediaKeys: data.data.attachments?.media_keys,
pollId: data.data.attachments?.poll_ids?.[0],
},
},
tweet: transformTweet(data.data),
},
}
},