mirror of
https://github.com/simstudioai/sim.git
synced 2026-04-06 03:00:16 -04:00
fix(x): fix x optional tool params (#2307)
* fix(x): fix x optional tool params * ack pr comments
This commit is contained in:
@@ -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' },
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
@@ -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: {
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
})
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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),
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user