Skip to content
This repository was archived by the owner on Dec 2, 2025. It is now read-only.
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions packages/ai-agent-sdk/.env.example
Original file line number Diff line number Diff line change
Expand Up @@ -6,3 +6,10 @@ GROK_API_KEY

# Tool API Keys
GOLDRUSH_API_KEY


# Twitter API Keys
TWITTER_API_KEY=
TWITTER_API_SECRET=
TWITTER_ACCESS_TOKEN=
TWITTER_ACCESS_TOKEN_SECRET=
2 changes: 2 additions & 0 deletions packages/ai-agent-sdk/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@
"test:agent": "vitest src/core/agent/agent.test.ts",
"test:llm": "vitest src/core/llm/llm.test.ts",
"test:tools:goldrush": "vitest src/core/tools/goldrush/index.test.ts",
"test:tools:twitter": "vitest src/core/tools/twitter/index.test.ts",
"test:zee": "vitest src/core/zee/zee.test.ts",
"build": "tsc",
"clean": "rm -rf dist",
Expand All @@ -54,6 +55,7 @@
"openai": "^4.79.1",
"pino": "^9.6.0",
"pino-pretty": "^13.0.0",
"twitter-api-v2": "^1.18.2",
"typescript": "^5.7.3",
"zod": "^3.24.1",
"zod-to-json-schema": "^3.24.1"
Expand Down
55 changes: 55 additions & 0 deletions packages/ai-agent-sdk/src/core/tools/twitter/base.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import { Tool } from "../base";
import { TwitterApi, type TwitterApiTokens } from "twitter-api-v2";
import { type AnyZodObject } from "zod";

export interface TwitterToolConfig {
apiKey?: string;
apiSecret?: string;
accessToken?: string;
accessTokenSecret?: string;
}

export abstract class TwitterTool extends Tool {
protected client: TwitterApi;

constructor(
id: string,
description: string,
schema: AnyZodObject,
config: TwitterToolConfig = {}
) {
super(
id,
description,
schema,
async (parameters) => await this.executeOperation(parameters)
);

// Process config after super
const apiKey = config.apiKey ?? process.env["TWITTER_API_KEY"];
const apiSecret = config.apiSecret ?? process.env["TWITTER_API_SECRET"];
const accessToken =
config.accessToken ?? process.env["TWITTER_ACCESS_TOKEN"];
const accessTokenSecret =
config.accessTokenSecret ??
process.env["TWITTER_ACCESS_TOKEN_SECRET"];

if (!apiKey) throw new Error("TWITTER_API_KEY is not configured.");
if (!apiSecret)
throw new Error("TWITTER_API_SECRET is not configured.");
if (!accessToken)
throw new Error("TWITTER_ACCESS_TOKEN is not configured.");
if (!accessTokenSecret)
throw new Error("TWITTER_ACCESS_TOKEN_SECRET is not configured.");

// Initialize client after validation
this.client = new TwitterApi({
appKey: apiKey,
appSecret: apiSecret,
accessToken: accessToken,
accessSecret: accessTokenSecret,
} as TwitterApiTokens);
}

protected abstract executeOperation(params: unknown): Promise<string>;
}
152 changes: 152 additions & 0 deletions packages/ai-agent-sdk/src/core/tools/twitter/index.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,152 @@
import { Agent } from "../../agent";
import { user } from "../../base";
import { StateFn } from "../../state";
import { runToolCalls } from "../base";
import type { TwitterToolConfig } from "./base";
import { TwitterAccountDetailsTool } from "./twitter-account-details";
import { TwitterPostTweetTool } from "./twitter-post-tweet";
import { TwitterPostTweetReplyTool } from "./twitter-post-tweet-reply";
import "dotenv/config";
import type { ChatCompletionAssistantMessageParam } from "openai/resources";
import { beforeAll, describe, expect, test } from "vitest";

let config: TwitterToolConfig;

beforeAll(() => {
config = {
apiKey: process.env["TWITTER_API_KEY"],
apiSecret: process.env["TWITTER_API_SECRET"],
accessToken: process.env["TWITTER_ACCESS_TOKEN"],
accessTokenSecret: process.env["TWITTER_ACCESS_TOKEN_SECRET"],
};

// Validate Twitter API configuration
const requiredEnvVars = [
"TWITTER_API_KEY",
"TWITTER_API_SECRET",
"TWITTER_ACCESS_TOKEN",
"TWITTER_ACCESS_TOKEN_SECRET",
];

requiredEnvVars.forEach((envVar) => {
if (!process.env[envVar]) {
throw new Error(`${envVar} environment variable is not set`);
}
});
});

describe("Twitter Tools Test Suite", () => {
const tools = {
accountDetails: new TwitterAccountDetailsTool(config),
postTweet: new TwitterPostTweetTool(config),
postTweetReply: new TwitterPostTweetReplyTool(config),
};

const createTwitterAgent = (
description: string,
instructions: string[]
) => {
return new Agent({
name: "twitter-agent",
model: {
provider: "OPEN_AI",
name: "gpt-4o-mini",
},
description,
instructions,
tools,
});
};

test("post a tweet about a blockchain joke", async () => {
const agent = createTwitterAgent("A humorous blockchain agent", [
"Tell a funny blockchain joke and post it on Twitter",
]);

const state = StateFn.root(agent.description);
state.messages.push(
user("Tell a funny blockchain joke and post it on Twitter")
);

try {
const result = await agent.run(state);
expect(result.messages.length).toBeGreaterThan(0);
expect(result.status).toEqual("paused");
} catch (error) {
console.error("Tweet posting test failed:", error);
throw error;
}
});

test("retrieves account details", async () => {
const agent = createTwitterAgent(
"You are a blockchain developer exploring Twitter account details.",
["Retrieve account details", "Analyze Twitter profile information"]
);

const state = StateFn.root(agent.description);
state.messages.push(user("Retrieve account details"));

const result = await agent.run(state);
expect(result.status).toEqual("paused");

const toolCall = result.messages[
result.messages.length - 1
] as ChatCompletionAssistantMessageParam;
expect(toolCall?.tool_calls).toBeDefined();

const toolResponses = await runToolCalls(
tools,
toolCall?.tool_calls ?? []
);

const updatedState = {
...result,
status: "running" as const,
messages: [...result.messages, ...toolResponses],
};

const finalResult = await agent.run(updatedState);

expect(finalResult.status).toEqual("finished");
expect(
finalResult.messages[finalResult.messages.length - 1]?.content
).toBeDefined();
});

test("post a tweet reply", async () => {
const agent = createTwitterAgent(
"You are a blockchain developer exploring Twitter account details.",
["Retrieve account details", "Analyze Twitter profile information"]
);

const state = StateFn.root(agent.description);
state.messages.push(user("Retrieve account details"));

const result = await agent.run(state);
expect(result.status).toEqual("paused");

const toolCall = result.messages[
result.messages.length - 1
] as ChatCompletionAssistantMessageParam;
expect(toolCall?.tool_calls).toBeDefined();

const toolResponses = await runToolCalls(
tools,
toolCall?.tool_calls ?? []
);

const updatedState = {
...result,
status: "running" as const,
messages: [...result.messages, ...toolResponses],
};

const finalResult = await agent.run(updatedState);

expect(finalResult.status).toEqual("finished");
expect(
finalResult.messages[finalResult.messages.length - 1]?.content
).toBeDefined();
});
});
4 changes: 4 additions & 0 deletions packages/ai-agent-sdk/src/core/tools/twitter/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
export * from "./base";
export * from "./twitter-account-details";
export * from "./twitter-post-tweet-reply";
export * from "./twitter-post-tweet";
19 changes: 19 additions & 0 deletions packages/ai-agent-sdk/src/core/tools/twitter/schemas.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
// schemas.ts
import { z } from "zod";

export const TwitterPostTweetSchema = z.object({
tweet: z.string().describe("Tweet content (max 280 characters)"),
});

export const TwitterPostTweetReplySchema = z.object({
tweetId: z.string().describe("ID of the tweet to reply to"),
tweetReply: z.string().describe("Reply content (max 280 characters)"),
});

export const TwitterAccountDetailsSchema = z
.object({})
.describe("No parameters needed for account details");

export const TwitterAccountMentionsSchema = z.object({
userId: z.string().describe("Twitter user ID to fetch mentions for"),
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import { TwitterTool, type TwitterToolConfig } from "./base";
import { TwitterAccountDetailsSchema } from "./schemas";
import type { z } from "zod";

export type TwitterAccountDetailsParams = z.infer<
typeof TwitterAccountDetailsSchema
>;

export class TwitterAccountDetailsTool extends TwitterTool {
constructor(config?: TwitterToolConfig) {
super(
"twitter-account-details",
"Get account details",
TwitterAccountDetailsSchema,
config
);
}

protected async executeOperation(
_?: TwitterAccountDetailsParams
): Promise<string> {
try {
const response = await this.client.v2.me();
response.data.url = `https://x.com/${response.data.username}`;
return `Successfully retrieved authenticated user account details:\n${JSON.stringify(response)}`;
} catch (error) {
return `Error in Twitter operation: ${error instanceof Error ? error.message : "Unknown error"}`;
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import { TwitterTool, type TwitterToolConfig } from "./base";
import { TwitterPostTweetReplySchema } from "./schemas";
import type { z } from "zod";

export type TwitterPostTweetReplyParams = z.infer<
typeof TwitterPostTweetReplySchema
>;

export class TwitterPostTweetReplyTool extends TwitterTool {
constructor(config?: TwitterToolConfig) {
super(
"post-tweet-reply",
"Post a tweet reply",
TwitterPostTweetReplySchema,
config
);
}

protected async executeOperation(
params: TwitterPostTweetReplyParams
): Promise<string> {
try {
const response = await this.client.v2.tweet(params.tweetReply, {
reply: { in_reply_to_tweet_id: params.tweetId },
});
return `Successfully posted tweet:\n${JSON.stringify(response)}`;
} catch (error) {
return `Error in Twitter operation: ${error instanceof Error ? error.message : "Unknown error"}`;
}
}
}
27 changes: 27 additions & 0 deletions packages/ai-agent-sdk/src/core/tools/twitter/twitter-post-tweet.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import { TwitterTool, type TwitterToolConfig } from "./base";
import { TwitterPostTweetSchema } from "./schemas";
import type { z } from "zod";

export type TwitterPostTweetParams = z.infer<typeof TwitterPostTweetSchema>;

export class TwitterPostTweetTool extends TwitterTool {
constructor(config?: TwitterToolConfig) {
super(
"twitter-post-tweet",
"Post a tweet to the authenticated Twitter/X account",
TwitterPostTweetSchema,
config
);
}

protected async executeOperation(
params: TwitterPostTweetParams
): Promise<string> {
try {
const response = await this.client.v2.tweet(params.tweet);
return `Successfully posted tweet:\n${JSON.stringify(response)}`;
} catch (error) {
return `Error in Twitter operation: ${error instanceof Error ? error.message : "Unknown error"}`;
}
}
}
8 changes: 8 additions & 0 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.